diff --git a/CHANGES.rst b/CHANGES.rst index 836d64e9d..d763134ae 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,4 +1,4 @@ -.. contents:: + .. contents:: CHANGES ======= @@ -41,6 +41,8 @@ Internals #. Operator name to unicode or ASCII comes from Mathics scanner character tables. #. ``eval*`` methods in `Builtin` classes are considerer as synonyms of ``apply*`` methods. #. Modularize and improve the way in which `Builtin` classes are selected to have an associated `Definition`. +#. `_SetOperator.assign_elementary` was renamed as `_SetOperator.assign`. All the special cases are not handled by the `_SetOperator.special_cases` dict. + Bugs diff --git a/mathics/builtin/__init__.py b/mathics/builtin/__init__.py index f7e9b0a61..79e6e1897 100755 --- a/mathics/builtin/__init__.py +++ b/mathics/builtin/__init__.py @@ -20,6 +20,8 @@ PatternObject, ) +from mathics.core.pattern import pattern_objects + from mathics.settings import ENABLE_FILES_MODULE from mathics.version import __version__ # noqa used in loading to check consistency. @@ -267,7 +269,6 @@ def sanity_check(cls, module): mathics_to_python = {} # here we have: name -> string sympy_to_mathics = {} -pattern_objects = {} builtins_precedence = {} new_builtins = _builtins diff --git a/mathics/builtin/assignments/assignment.py b/mathics/builtin/assignments/assignment.py index f1637c927..856942f18 100644 --- a/mathics/builtin/assignments/assignment.py +++ b/mathics/builtin/assignments/assignment.py @@ -144,8 +144,30 @@ class SetDelayed(Set): = 2 / 3 >> F[-3, 2] = -2 / 3 + We can use conditional delayed assignments to define \ + symbols with values conditioned to the context. For example, + >> ClearAll[a,b]; a/; b>0:= 3 + Set $a$ to have a value of $3$ if certain variable $b$ is positive.\ + So, if this variable is not set, $a$ stays unevaluated: + >> a + = a + If now we assign a positive value to $b$, then $a$ is evaluated: + >> b=2; a + = 3 """ + # I WMA, if we assign a value without a condition on the LHS, + # conditional values are never reached. So, + # + # Notice however that if we assign an unconditional value to $a$, \ + # this overrides the condition: + # >> a:=0; a/; b>1:= 3 + # >> a + # = 0 + # + # In Mathics, this last line would return 3 + # """ + operator = ":=" attributes = A_HOLD_ALL | A_PROTECTED | A_SEQUENCE_HOLD @@ -203,7 +225,7 @@ def apply(self, f, lhs, rhs, evaluation): return rhs = rhs.evaluate(evaluation) - self.assign_elementary(lhs, rhs, evaluation, tags=[name]) + self.assign(lhs, rhs, evaluation, tags=[name]) return rhs @@ -228,7 +250,7 @@ def apply(self, f, lhs, rhs, evaluation): evaluation.message(self.get_name(), "sym", f, 1) return - if self.assign_elementary(lhs, rhs, evaluation, tags=[name]): + if self.assign(lhs, rhs, evaluation, tags=[name]): return SymbolNull else: return SymbolFailed diff --git a/mathics/builtin/assignments/clear.py b/mathics/builtin/assignments/clear.py index 471eb5692..e46dd3ab3 100644 --- a/mathics/builtin/assignments/clear.py +++ b/mathics/builtin/assignments/clear.py @@ -22,14 +22,20 @@ Atom, Symbol, SymbolNull, - system_symbols, + symbol_set, ) from mathics.core.systemsymbols import ( SymbolContext, SymbolContextPath, + SymbolDownValues, SymbolFailed, + SymbolMessages, + SymbolNValues, SymbolOptions, + SymbolOwnValues, + SymbolSubValues, + SymbolUpValues, ) from mathics.core.atoms import String @@ -320,12 +326,12 @@ def apply(self, expr, evaluation): return SymbolNull -SYSTEM_SYMBOL_VALUES = system_symbols( - "OwnValues", - "DownValues", - "SubValues", - "UpValues", - "NValues", - "Options", - "Messages", +SYSTEM_SYMBOL_VALUES = symbol_set( + SymbolDownValues, + SymbolMessages, + SymbolNValues, + SymbolOptions, + SymbolOwnValues, + SymbolSubValues, + SymbolUpValues, ) diff --git a/mathics/builtin/assignments/internals.py b/mathics/builtin/assignments/internals.py index 132d9e390..64588d72e 100644 --- a/mathics/builtin/assignments/internals.py +++ b/mathics/builtin/assignments/internals.py @@ -1,8 +1,10 @@ # -*- coding: utf-8 -*- +from typing import Optional, Tuple from mathics.algorithm.parts import walk_parts from mathics.core.atoms import Atom, Integer +from mathics.core.element import BaseElement from mathics.core.evaluation import MAX_RECURSION_DEPTH, set_python_recursion_limit from mathics.core.expression import Expression, SymbolDefault from mathics.core.list import ListExpression @@ -41,12 +43,22 @@ def __init__(self, lhs, rhs) -> None: def assign_store_rules_by_tag(self, lhs, rhs, evaluation, tags, upset=None): + """ + This is the default assignment. Stores a rule of the form lhs->rhs + as a value associated to each symbol listed in tags. + For special cases, such like conditions or patterns in the lhs, + lhs and rhs are rewritten in a normal form, where + conditions are associated to the lhs. + """ lhs, condition = unroll_conditions(lhs) lhs, rhs = unroll_patterns(lhs, rhs, evaluation) defs = evaluation.definitions ignore_protection, tags = process_assign_other( self, lhs, rhs, evaluation, tags, upset ) + # In WMA, this does not happens. However, if we remove this, + # some combinatorica tests fail. + # Also, should not be at the begining? lhs, rhs = process_rhs_conditions(lhs, rhs, condition, evaluation) count = 0 rule = Rule(lhs, rhs) @@ -132,7 +144,7 @@ def repl_pattern_by_symbol(expr): return expr -# Here are the functions related to assign_elementary +# Here are the functions related to assign # Auxiliary routines @@ -166,7 +178,11 @@ def find_tag_and_check(lhs, tags, evaluation): return tag -def unroll_patterns(lhs, rhs, evaluation): +def unroll_patterns(lhs, rhs, evaluation) -> Tuple[BaseElement, BaseElement]: + """ + Pattern[symb, pat]=rhs -> pat = (rhs/.(symb->pat)) + HoldPattern[lhs] = rhs -> lhs = rhs + """ if isinstance(lhs, Atom): return lhs, rhs name = lhs.get_head_name() @@ -174,15 +190,24 @@ def unroll_patterns(lhs, rhs, evaluation): if name == "System`Pattern": lhs = lhs_elements[1] rulerepl = (lhs_elements[0], repl_pattern_by_symbol(lhs)) + # Maybe this replamement should be delayed instead, + # like + # rhs = Expression(Symbol("System`Replace"), Rule(*rulerepl)) + # TODO: check if this is the correct behavior. rhs, status = rhs.do_apply_rules([Rule(*rulerepl)], evaluation) name = lhs.get_head_name() - - if name == "System`HoldPattern": + elif name == "System`HoldPattern": lhs = lhs_elements[0] return lhs, rhs -def unroll_conditions(lhs): +def unroll_conditions(lhs) -> Tuple[BaseElement, Optional[Expression]]: + """ + If lhs is a nested `Condition` expression, + gather all the conditions in a single one, and returns a tuple + with the lhs stripped from the conditions and the shallow condition. + If there is not any condition, returns the lhs and None + """ if isinstance(lhs, Symbol): return lhs, None else: @@ -207,12 +232,15 @@ def unroll_conditions(lhs): return lhs, condition -# Here starts the functions that implement `assign_elementary` for different +# Here starts the functions that implement `assign` for different # kind of expressions. Maybe they should be put in a separated module, or # maybe they should be member functions of _SetOperator. def process_assign_recursion_limit(lhs, rhs, evaluation): + """ + Set ownvalue for the $RecursionLimit symbol. + """ rhs_int_value = rhs.get_int_value() # if (not rhs_int_value or rhs_int_value < 20) and not # rhs.get_name() == 'System`Infinity': @@ -231,6 +259,10 @@ def process_assign_recursion_limit(lhs, rhs, evaluation): def process_assign_iteration_limit(lhs, rhs, evaluation): + """ + Set ownvalue for the $IterationLimit symbol. + """ + rhs_int_value = rhs.get_int_value() if ( not rhs_int_value or rhs_int_value < 20 @@ -241,6 +273,9 @@ def process_assign_iteration_limit(lhs, rhs, evaluation): def process_assign_module_number(lhs, rhs, evaluation): + """ + Set ownvalue for the $ModuleNumber symbol. + """ rhs_int_value = rhs.get_int_value() if not rhs_int_value or rhs_int_value <= 0: evaluation.message("$ModuleNumber", "set", rhs) @@ -251,6 +286,10 @@ def process_assign_module_number(lhs, rhs, evaluation): def process_assign_line_number_and_history_length( self, lhs, rhs, evaluation, tags, upset ): + """ + Set ownvalue for the $Line and $HistoryLength symbols. + """ + lhs_name = lhs.get_name() rhs_int_value = rhs.get_int_value() if rhs_int_value is None or rhs_int_value < 0: @@ -430,7 +469,18 @@ def process_assign_n(self, lhs, rhs, evaluation, tags, upset): return count > 0 -def process_assign_other(self, lhs, rhs, evaluation, tags=None, upset=False): +def process_assign_other( + self, lhs, rhs, evaluation, tags=None, upset=False +) -> Tuple[bool, list]: + """ + Process special cases, performing certain side effects, like modifying + the value of internal variables that are not stored as rules. + + The function returns a tuple of a bool value and a list of tags. + If lhs is one of the special cases, then the bool variable is + True, meaning that the `Protected` attribute should not be taken into accout. + Otherwise, the value is False. + """ tags, focus = process_tags_and_upset_allow_custom( tags, upset, self, lhs, evaluation ) @@ -455,6 +505,10 @@ def process_assign_other(self, lhs, rhs, evaluation, tags=None, upset=False): def process_assign_attributes(self, lhs, rhs, evaluation, tags, upset): + """ + Process the case where lhs is of the form + `Attribute[symbol]` + """ name = lhs.get_head_name() if len(lhs.elements) != 1: evaluation.message_args(name, len(lhs.elements), 1) @@ -558,6 +612,61 @@ def process_assign_format(self, lhs, rhs, evaluation, tags, upset): return count > 0 +def process_assign_list(self, lhs, rhs, evaluation, tags, upset): + if not ( + rhs.get_head_name() == "System`List" and len(lhs.elements) == len(rhs.elements) + ): # nopep8 + evaluation.message(self.get_name(), "shape", lhs, rhs) + return False + result = True + for left, right in zip(lhs.elements, rhs.elements): + if not self.assign(left, right, evaluation): + result = False + return result + + +def process_assign_makeboxes(self, lhs, rhs, evaluation, tags, upset): + # FIXME: the below is a big hack. + # Currently MakeBoxes boxing is implemented as a bunch of rules. + # See mathics.builtin.base contribute(). + # I think we want to change this so it works like normal SetDelayed + # That is: + # MakeBoxes[CubeRoot, StandardForm] := RadicalBox[3, StandardForm] + # rather than: + # MakeBoxes[CubeRoot, StandardForm] -> RadicalBox[3, StandardForm] + + makeboxes_rule = Rule(lhs, rhs, system=False) + definitions = evaluation.definitions + definitions.add_rule("System`MakeBoxes", makeboxes_rule, "down") + # makeboxes_defs = evaluation.definitions.builtin["System`MakeBoxes"] + # makeboxes_defs.add_rule(makeboxes_rule) + return True + + +def process_assing_part(self, lhs, rhs, evaluation, tags, upset): + """ + Special case `A[[i,j,...]]=....` + """ + defs = evaluation.definitions + if len(lhs.elements) < 1: + evaluation.message(self.get_name(), "setp", lhs) + return False + symbol = lhs.elements[0] + name = symbol.get_name() + if not name: + evaluation.message(self.get_name(), "setps", symbol) + return False + if is_protected(name, defs): + evaluation.message(self.get_name(), "wrsym", symbol) + return False + rule = defs.get_ownvalue(name) + if rule is None: + evaluation.message(self.get_name(), "noval", symbol) + return False + indices = lhs.elements[1:] + return walk_parts([rule.replace], indices, evaluation, rhs) + + def process_assign_messagename(self, lhs, rhs, evaluation, tags, upset): lhs, condition = unroll_conditions(lhs) lhs, rhs = unroll_patterns(lhs, rhs, evaluation) @@ -582,6 +691,9 @@ def process_assign_messagename(self, lhs, rhs, evaluation, tags, upset): def process_rhs_conditions(lhs, rhs, condition, evaluation): + """ + lhs = Condition[rhs, test] -> Condition[lhs, test] = rhs + """ # To Handle `OptionValue` in `Condition` rulopc = build_rulopc(lhs.get_head()) rhs_name = rhs.get_head_name() @@ -608,12 +720,7 @@ def process_rhs_conditions(lhs, rhs, condition, evaluation): def process_tags_and_upset_dont_allow_custom(tags, upset, self, lhs, focus, evaluation): - # TODO: the following provides a hacky fix for 1259. I know @rocky loves - # this kind of things, but otherwise we need to work on rebuild the pattern - # matching mechanism... - flag_ioi, evaluation.ignore_oneidentity = evaluation.ignore_oneidentity, True focus = focus.evaluate_elements(evaluation) - evaluation.ignore_oneidentity = flag_ioi name = lhs.get_head_name() if tags is None and not upset: name = focus.get_lookup_name() @@ -633,14 +740,9 @@ def process_tags_and_upset_dont_allow_custom(tags, upset, self, lhs, focus, eval def process_tags_and_upset_allow_custom(tags, upset, self, lhs, evaluation): - # TODO: the following provides a hacky fix for 1259. I know @rocky loves - # this kind of things, but otherwise we need to work on rebuild the pattern - # matching mechanism... name = lhs.get_head_name() focus = lhs - flag_ioi, evaluation.ignore_oneidentity = evaluation.ignore_oneidentity, True focus = focus.evaluate_elements(evaluation) - evaluation.ignore_oneidentity = flag_ioi if tags is None and not upset: name = focus.get_lookup_name() if not name: @@ -684,27 +786,47 @@ def process_tags_and_upset_allow_custom(tags, upset, self, lhs, evaluation): class _SetOperator: + """ + This is the base class for assignment Builtin operators. + + Special cases are determined by the head of the expression. Then + they are processed by specific routines, which are poke from + the `special_cases` dict. + """ + + # There are several idea about how to reimplement this. One possibility + # would be to use a Symbol instead of a name as the key of this dictionary. + # + # Another possibility would be to move the specific function to be a + # class method associated to the corresponding builtins. In any case, + # the present implementation should be clear enough to understand the + # logic. + # + special_cases = { - "System`OwnValues": process_assign_definition_values, - "System`DownValues": process_assign_definition_values, - "System`SubValues": process_assign_definition_values, - "System`UpValues": process_assign_definition_values, - "System`NValues": process_assign_definition_values, - "System`DefaultValues": process_assign_definition_values, - "System`Messages": process_assign_definition_values, - "System`Attributes": process_assign_attributes, - "System`Options": process_assign_options, - "System`$RandomState": process_assign_random_state, "System`$Context": process_assign_context, "System`$ContextPath": process_assign_context_path, - "System`N": process_assign_n, - "System`NumericQ": process_assign_numericq, - "System`MessageName": process_assign_messagename, + "System`$RandomState": process_assign_random_state, + "System`Attributes": process_assign_attributes, "System`Default": process_assign_default, + "System`DefaultValues": process_assign_definition_values, + "System`DownValues": process_assign_definition_values, "System`Format": process_assign_format, + "System`List": process_assign_list, + "System`MakeBoxes": process_assign_makeboxes, + "System`MessageName": process_assign_messagename, + "System`Messages": process_assign_definition_values, + "System`N": process_assign_n, + "System`NValues": process_assign_definition_values, + "System`NumericQ": process_assign_numericq, + "System`Options": process_assign_options, + "System`OwnValues": process_assign_definition_values, + "System`Part": process_assing_part, + "System`SubValues": process_assign_definition_values, + "System`UpValues": process_assign_definition_values, } - def assign_elementary(self, lhs, rhs, evaluation, tags=None, upset=False): + def assign(self, lhs, rhs, evaluation, tags=None, upset=False): if isinstance(lhs, Symbol): name = lhs.name else: @@ -720,40 +842,3 @@ def assign_elementary(self, lhs, rhs, evaluation, tags=None, upset=False): return assign_store_rules_by_tag(self, lhs, rhs, evaluation, tags, upset) except AssignmentException: return False - - def assign(self, lhs, rhs, evaluation): - # lhs._format_cache = None - defs = evaluation.definitions - if lhs.get_head_name() == "System`List": - if not (rhs.get_head_name() == "System`List") or len(lhs.elements) != len( - rhs.elements - ): # nopep8 - - evaluation.message(self.get_name(), "shape", lhs, rhs) - return False - else: - result = True - for left, right in zip(lhs.elements, rhs.elements): - if not self.assign(left, right, evaluation): - result = False - return result - elif lhs.get_head_name() == "System`Part": - if len(lhs.elements) < 1: - evaluation.message(self.get_name(), "setp", lhs) - return False - symbol = lhs.elements[0] - name = symbol.get_name() - if not name: - evaluation.message(self.get_name(), "setps", symbol) - return False - if is_protected(name, defs): - evaluation.message(self.get_name(), "wrsym", symbol) - return False - rule = defs.get_ownvalue(name) - if rule is None: - evaluation.message(self.get_name(), "noval", symbol) - return False - indices = lhs.elements[1:] - return walk_parts([rule.replace], indices, evaluation, rhs) - else: - return self.assign_elementary(lhs, rhs, evaluation) diff --git a/mathics/builtin/assignments/upvalues.py b/mathics/builtin/assignments/upvalues.py index ac794fa48..817a74272 100644 --- a/mathics/builtin/assignments/upvalues.py +++ b/mathics/builtin/assignments/upvalues.py @@ -58,7 +58,7 @@ class UpSet(BinaryOperator, _SetOperator): def apply(self, lhs, rhs, evaluation): "lhs_ ^= rhs_" - self.assign_elementary(lhs, rhs, evaluation, upset=True) + self.assign(lhs, rhs, evaluation, upset=True) return rhs @@ -92,7 +92,7 @@ class UpSetDelayed(UpSet): def apply(self, lhs, rhs, evaluation): "lhs_ ^:= rhs_" - if self.assign_elementary(lhs, rhs, evaluation, upset=True): + if self.assign(lhs, rhs, evaluation, upset=True): return SymbolNull else: return SymbolFailed diff --git a/mathics/builtin/attributes.py b/mathics/builtin/attributes.py index eea6b3006..5c161ecae 100644 --- a/mathics/builtin/attributes.py +++ b/mathics/builtin/attributes.py @@ -460,15 +460,24 @@ class OneIdentity(Predefined):
'OneIdentity' -
is an attribute specifying that '$f$[$x$]' should be treated \ - as equivalent to $x$ in pattern matching. +
is an attribute assigned to a symbol, say $f$, indicating that '$f$[$x$]', $f$[$f$[$x$]], etc. are all \ + equivalent to $x$ in pattern matching.
- 'OneIdentity' affects pattern matching: + >> a /. f[x_:0, u_] -> {u} + = a + + Here is how 'OneIdentity' changes the pattern matched above : + >> SetAttributes[f, OneIdentity] - >> a /. f[args___] -> {args} + >> a /. f[x_:0, u_] -> {u} = {a} - It does not affect evaluation: + + However, without a default argument, the pattern does not match: + >> a /. f[u_] -> {u} + = a + + 'OneIdentity' does not change evaluation: >> f[a] = f[a] """ @@ -736,6 +745,13 @@ def eval(self, symbols, attributes, evaluation): evaluation.message("Attributes", "attnf", Symbol(value)) return SymbolNull + def eval_arg_error(self, args, evaluation): + "SetAttributes[args___]" + # We should only come here when we don't have 2 args, because + # eval() should be called otherwise. + nargs = len(args.elements) if isinstance(args, Expression) else 1 + evaluation.message("SetAttributes", "argrx", "SetAttributes", nargs, 2) + class Unprotect(Builtin): """ diff --git a/mathics/builtin/base.py b/mathics/builtin/base.py index 57a8473eb..b6faf3aac 100644 --- a/mathics/builtin/base.py +++ b/mathics/builtin/base.py @@ -283,15 +283,16 @@ def check_options(options_to_check, evaluation): rules.append( BuiltinRule(name, pattern, function, check_options, system=True) ) - for pattern, replace in self.rules.items(): - if not isinstance(pattern, BaseElement): - pattern = pattern % {"name": name} - pattern = parse_builtin_rule(pattern, definition_class) - replace = replace % {"name": name} - # FIXME: Should system=True be system=not is_pymodule ? - rules.append(Rule(pattern, parse_builtin_rule(replace), system=True)) + for pattern_str, replace_str in self.rules.items(): + pattern_str = pattern_str % {"name": name} + pattern = parse_builtin_rule(pattern_str, definition_class) + replace_str = replace_str % {"name": name} + rules.append( + Rule(pattern, parse_builtin_rule(replace_str), system=not is_pymodule) + ) box_rules = [] + # FIXME: Why a special case for System`MakeBoxes? Remove this if name != "System`MakeBoxes": new_rules = [] for rule in rules: diff --git a/mathics/builtin/files_io/files.py b/mathics/builtin/files_io/files.py index eba2bd3a1..e801649bb 100644 --- a/mathics/builtin/files_io/files.py +++ b/mathics/builtin/files_io/files.py @@ -296,7 +296,7 @@ class FilePrint(Builtin): } def apply(self, path, evaluation, options): - "FilePrint[path_ OptionsPattern[FilePrint]]" + "FilePrint[path_, OptionsPattern[FilePrint]]" pypath = path.to_python() if not ( isinstance(pypath, str) diff --git a/mathics/builtin/files_io/filesystem.py b/mathics/builtin/files_io/filesystem.py index a11299cb5..9cf5da9de 100644 --- a/mathics/builtin/files_io/filesystem.py +++ b/mathics/builtin/files_io/filesystem.py @@ -64,7 +64,7 @@ class AbsoluteFileName(Builtin): """
-
'AbsoluteFileName["$name$"]' +
'AbsoluteFileName["$name$"]'
returns the absolute version of the given filename.
@@ -82,7 +82,7 @@ class AbsoluteFileName(Builtin): } summary_text = "absolute path" - def apply(self, name, evaluation): + def eval(self, name, evaluation): "AbsoluteFileName[name_]" py_name = name.to_python() @@ -106,7 +106,7 @@ def apply(self, name, evaluation): class BaseDirectory(Predefined): """
-
'$UserBaseDirectory' +
'$UserBaseDirectory'
returns the folder where user configurations are stored.
@@ -125,13 +125,12 @@ def evaluate(self, evaluation): class CopyDirectory(Builtin): """
-
'CopyDirectory["$dir1$", "$dir2$"]' +
'CopyDirectory["$dir1$", "$dir2$"]'
copies directory $dir1$ to $dir2$.
""" messages = { - "argr": "called with `1` argument; 2 arguments are expected.", "fstr": ( "File specification `1` is not a string of " "one or more characters." ), @@ -140,12 +139,12 @@ class CopyDirectory(Builtin): } summary_text = "copy a directory into a new path" - def apply(self, dirs, evaluation): + def eval(self, dirs, evaluation): "CopyDirectory[dirs__]" seq = dirs.get_sequence() if len(seq) != 2: - evaluation.message("CopyDirectory", "argr", len(seq)) + evaluation.message("CopyDirectory", "argr", "CopyDirectory", 2) return (dir1, dir2) = (s.to_python() for s in seq) @@ -174,7 +173,7 @@ def apply(self, dirs, evaluation): class CopyFile(Builtin): """
-
'CopyFile["$file1$", "$file2$"]' +
'CopyFile["$file1$", "$file2$"]'
copies $file1$ to $file2$.
@@ -192,7 +191,7 @@ class CopyFile(Builtin): } summary_text = "copy a file into a new path" - def apply(self, source, dest, evaluation): + def eval(self, source, dest, evaluation): "CopyFile[source_, dest_]" py_source = source.to_python() @@ -233,9 +232,10 @@ def apply(self, source, dest, evaluation): class CreateDirectory(Builtin): """
-
'CreateDirectory["$dir$"]' +
'CreateDirectory["$dir$"]'
creates a directory called $dir$. -
'CreateDirectory[]' + +
'CreateDirectory[]'
creates a temporary directory.
@@ -261,7 +261,7 @@ class CreateDirectory(Builtin): } summary_text = "create a directory" - def apply(self, dirname, evaluation, options): + def eval(self, dirname, evaluation, options): "CreateDirectory[dirname_, OptionsPattern[CreateDirectory]]" expr = to_expression("CreateDirectory", dirname) @@ -285,7 +285,7 @@ def apply(self, dirname, evaluation, options): return String(osp.abspath(py_dirname)) - def apply_empty(self, evaluation, options): + def eval_empty(self, evaluation, options): "CreateDirectory[OptionsPattern[CreateDirectory]]" dirname = tempfile.mkdtemp(prefix="m", dir=TMP_DIR) return String(dirname) @@ -294,10 +294,11 @@ def apply_empty(self, evaluation, options): class CreateFile(Builtin): """
-
'CreateFile["filename"]' -
Creates a file named "filename" temporary file, but do not open it. -
'CreateFile[]' -
Creates a temporary file, but do not open it. +
'CreateFile["filename"]' +
Creates a file named "filename" temporary file, but do not open it. + +
'CreateFile[]' +
Creates a temporary file, but do not open it.
""" @@ -311,7 +312,7 @@ class CreateFile(Builtin): } summary_text = "create a file" - def apply(self, filename, evaluation, **options): + def eval(self, filename, evaluation, **options): "CreateFile[filename_String, OptionsPattern[CreateFile]]" try: # TODO: Implement options @@ -329,14 +330,14 @@ def apply(self, filename, evaluation, **options): class CreateTemporary(Builtin): """
-
'CreateTemporary[]' -
Creates a temporary file, but do not open it. +
'CreateTemporary[]' +
Creates a temporary file, but do not open it.
""" summary_text = "create a temporary file" - def apply_0(self, evaluation): + def eval_0(self, evaluation): "CreateTemporary[]" try: res = create_temporary_file() @@ -348,7 +349,7 @@ def apply_0(self, evaluation): class DeleteDirectory(Builtin): """
-
'DeleteDirectory["$dir$"]' +
'DeleteDirectory["$dir$"]'
deletes a directory called $dir$.
@@ -375,7 +376,7 @@ class DeleteDirectory(Builtin): } summary_text = "delete a directory" - def apply(self, dirname, evaluation, options): + def eval(self, dirname, evaluation, options): "DeleteDirectory[dirname_, OptionsPattern[DeleteDirectory]]" expr = to_expression("DeleteDirectory", dirname) @@ -410,9 +411,10 @@ def apply(self, dirname, evaluation, options): class DeleteFile(Builtin): """
-
'Delete["$file$"]' +
'Delete["$file$"]'
deletes $file$. -
'Delete[{"$file1$", "$file2$", ...}]' + +
'Delete[{"$file1$", "$file2$", ...}]'
deletes a list of files.
@@ -433,7 +435,7 @@ class DeleteFile(Builtin): } summary_text = "delete a file" - def apply(self, filename, evaluation): + def eval(self, filename, evaluation): "DeleteFile[filename_]" py_path = filename.to_python() @@ -474,7 +476,7 @@ def apply(self, filename, evaluation): class Directory(Builtin): """
-
'Directory[]' +
'Directory[]'
returns the current working directory.
@@ -484,7 +486,7 @@ class Directory(Builtin): summary_text = "current working directory" - def apply(self, evaluation): + def eval(self, evaluation): "Directory[]" result = os.getcwd() return String(result) @@ -493,7 +495,7 @@ def apply(self, evaluation): class DirectoryName(Builtin): """
-
'DirectoryName["$name$"]' +
'DirectoryName["$name$"]'
extracts the directory name from a filename.
@@ -531,7 +533,7 @@ class DirectoryName(Builtin): } summary_text = "directory part of a filename" - def apply_with_n(self, name, n, evaluation, options): + def eval_with_n(self, name, n, evaluation, options): "DirectoryName[name_, n_, OptionsPattern[DirectoryName]]" if n is None: @@ -557,15 +559,15 @@ def apply_with_n(self, name, n, evaluation, options): return String(result) - def apply(self, name, evaluation, options): + def eval(self, name, evaluation, options): "DirectoryName[name_, OptionsPattern[DirectoryName]]" - return self.apply_with_n(name, None, evaluation, options) + return self.eval_with_n(name, None, evaluation, options) class DirectoryStack(Builtin): """
-
'DirectoryStack[]' +
'DirectoryStack[]'
returns the directory stack.
@@ -575,7 +577,7 @@ class DirectoryStack(Builtin): summary_text = "list the sequence of current directories in use" - def apply(self, evaluation): + def eval(self, evaluation): "DirectoryStack[]" global DIRECTORY_STACK return from_python(DIRECTORY_STACK) @@ -584,7 +586,7 @@ def apply(self, evaluation): class DirectoryQ(Builtin): """
-
'DirectoryQ["$name$"]' +
'DirectoryQ["$name$"]'
returns 'True' if the directory called $name$ exists and 'False' otherwise.
@@ -607,7 +609,7 @@ class DirectoryQ(Builtin): } summary_text = "test whether a path exists and is a directory" - def apply(self, pathname, evaluation): + def eval(self, pathname, evaluation): "DirectoryQ[pathname_]" path = pathname.to_python() @@ -626,7 +628,7 @@ def apply(self, pathname, evaluation): class ExpandFileName(Builtin): """
-
'ExpandFileName["$name$"]' +
'ExpandFileName["$name$"]'
expands $name$ to an absolute filename for your system.
@@ -639,7 +641,7 @@ class ExpandFileName(Builtin): } summary_text = "absolute path" - def apply(self, name, evaluation): + def eval(self, name, evaluation): "ExpandFileName[name_]" py_name = name.to_python() @@ -657,7 +659,7 @@ def apply(self, name, evaluation): class File(Builtin): """
-
'File["$file$"]' +
'File["$file$"]'
is a symbolic representation of an element in the local file system.
""" @@ -668,7 +670,7 @@ class File(Builtin): class FileBaseName(Builtin): """
-
'FileBaseName["$file$"]' +
'FileBaseName["$file$"]'
gives the base name for the specified file name.
@@ -690,7 +692,7 @@ class FileBaseName(Builtin): } summary_text = "base name of the file" - def apply(self, filename, evaluation, options): + def eval(self, filename, evaluation, options): "FileBaseName[filename_String, OptionsPattern[FileBaseName]]" path = filename.to_python()[1:-1] @@ -701,7 +703,7 @@ def apply(self, filename, evaluation, options): class FileByteCount(Builtin): """
-
'FileByteCount[$file$]' +
'FileByteCount[$file$]'
returns the number of bytes in $file$.
@@ -714,7 +716,7 @@ class FileByteCount(Builtin): } summary_text = "length of the file" - def apply(self, filename, evaluation): + def eval(self, filename, evaluation): "FileByteCount[filename_]" py_filename = filename.to_python() if not ( @@ -745,7 +747,7 @@ def apply(self, filename, evaluation): class FileDate(Builtin): """
-
'FileDate[$file$, $types$]' +
'FileDate[$file$, $types$]'
returns the time and date at which the file was last modified.
@@ -796,7 +798,7 @@ class FileDate(Builtin): } summary_text = "date and time of the last change in a file" - def apply(self, path, timetype, evaluation): + def eval(self, path, timetype, evaluation): "FileDate[path_, timetype_]" py_path, is_temparary_file = path_search(path.to_python()[1:-1]) @@ -839,15 +841,15 @@ def apply(self, path, timetype, evaluation): return to_expression("DateList", Real(result)) - def apply_default(self, path, evaluation): + def eval_default(self, path, evaluation): "FileDate[path_]" - return self.apply(path, None, evaluation) + return self.eval(path, None, evaluation) class FileExistsQ(Builtin): """
-
'FileExistsQ["$file$"]' +
'FileExistsQ["$file$"]'
returns 'True' if $file$ exists and 'False' otherwise.
@@ -864,7 +866,7 @@ class FileExistsQ(Builtin): } summary_text = "test whether a file exists" - def apply(self, filename, evaluation): + def eval(self, filename, evaluation): "FileExistsQ[filename_]" path = filename.to_python() if not (isinstance(path, str) and path[0] == path[-1] == '"'): @@ -882,7 +884,7 @@ def apply(self, filename, evaluation): class FileExtension(Builtin): """
-
'FileExtension["$file$"]' +
'FileExtension["$file$"]'
gives the extension for the specified file name.
@@ -903,7 +905,7 @@ class FileExtension(Builtin): } summary_text = "file extension" - def apply(self, filename, evaluation, options): + def eval(self, filename, evaluation, options): "FileExtension[filename_String, OptionsPattern[FileExtension]]" path = filename.to_python()[1:-1] filename_base, filename_ext = osp.splitext(path) @@ -914,12 +916,14 @@ def apply(self, filename, evaluation, options): class FileHash(Builtin): """
-
'FileHash[$file$]' +
'FileHash[$file$]'
returns an integer hash for the given $file$. -
'FileHash[$file$, $type$]' + +
'FileHash[$file$, $type$]'
returns an integer hash of the specified $type$ for the given $file$.
The types supported are "MD5", "Adler32", "CRC32", "SHA", "SHA224", "SHA256", "SHA384", and "SHA512". -
'FileHash[$file$, $type$, $format$]' + +
'FileHash[$file$, $type$, $format$]'
gives a hash code in the specified format.
@@ -961,7 +965,7 @@ class FileHash(Builtin): } summary_text = "compute a hash from the content of a file" - def apply(self, filename, hashtype, format, evaluation): + def eval(self, filename, hashtype, format, evaluation): "FileHash[filename_String, hashtype_String, format_String]" py_filename = filename.get_string_value() @@ -985,7 +989,7 @@ def apply(self, filename, hashtype, format, evaluation): class FileInformation(Builtin): """
-
'FileInformation["$file$"]' +
'FileInformation["$file$"]'
returns information about $file$.
@@ -1007,7 +1011,7 @@ class FileInformation(Builtin): class FileNameDepth(Builtin): """
-
'FileNameDepth["$name$"]' +
'FileNameDepth["$name$"]'
gives the number of path parts in the given filename.
@@ -1065,7 +1069,7 @@ class FileNameJoin(Builtin): } summary_text = "join parts into a path" - def apply(self, pathlist, evaluation, options): + def eval(self, pathlist, evaluation, options): "FileNameJoin[pathlist_List, OptionsPattern[FileNameJoin]]" py_pathlist = pathlist.to_python() @@ -1107,7 +1111,7 @@ def apply(self, pathlist, evaluation, options): class FileType(Builtin): """
-
'FileType["$file$"]' +
'FileType["$file$"]'
gives the type of a file, a string. This is typically 'File', 'Directory' or 'None'.
@@ -1130,7 +1134,7 @@ class FileType(Builtin): } summary_text = "type of a file" - def apply(self, filename, evaluation): + def eval(self, filename, evaluation): "FileType[filename_]" if not isinstance(filename, String): evaluation.message("FileType", "fstr", filename) @@ -1151,7 +1155,7 @@ def apply(self, filename, evaluation): class FindFile(Builtin): """
-
'FindFile[$name$]' +
'FindFile[$name$]'
searches '$Path' for the given filename.
@@ -1175,7 +1179,7 @@ class FindFile(Builtin): "search the path of of a file in the current directory and its subdirectories" ) - def apply(self, name, evaluation): + def eval(self, name, evaluation): "FindFile[name_]" py_name = name.to_python() @@ -1231,21 +1235,21 @@ class FileNames(Builtin): } summary_text = "list file names in the current directory" - def apply_0(self, evaluation, **options): + def eval_0(self, evaluation, **options): """FileNames[OptionsPattern[FileNames]]""" - return self.apply_3( + return self.eval_3( String("*"), String(os.getcwd()), None, evaluation, **options ) - def apply_1(self, forms, evaluation, **options): + def eval_1(self, forms, evaluation, **options): """FileNames[forms_, OptionsPattern[FileNames]]""" - return self.apply_3(forms, String(os.getcwd()), None, evaluation, **options) + return self.eval_3(forms, String(os.getcwd()), None, evaluation, **options) - def apply_2(self, forms, paths, evaluation, **options): + def eval_2(self, forms, paths, evaluation, **options): """FileNames[forms_, paths_, OptionsPattern[FileNames]]""" - return self.apply_3(forms, paths, None, evaluation, **options) + return self.eval_3(forms, paths, None, evaluation, **options) - def apply_3(self, forms, paths, n, evaluation, **options): + def eval_3(self, forms, paths, n, evaluation, **options): """FileNames[forms_, paths_, n_, OptionsPattern[FileNames]]""" filenames = set() # Building a list of forms @@ -1358,7 +1362,7 @@ class FileNameSplit(Builtin): summary_text = "split the file name in a list of parts" - def apply(self, filename, evaluation, options): + def eval(self, filename, evaluation, options): "FileNameSplit[filename_String, OptionsPattern[FileNameSplit]]" path = filename.to_python()[1:-1] @@ -1419,12 +1423,12 @@ class FileNameTake(Builtin): } summary_text = "take a part of the filename" - def apply(self, filename, evaluation, options): + def eval(self, filename, evaluation, options): "FileNameTake[filename_String, OptionsPattern[FileBaseName]]" path = pathlib.Path(filename.to_python()[1:-1]) return String(path.name) - def apply_n(self, filename, n, evaluation, options): + def eval_n(self, filename, n, evaluation, options): "FileNameTake[filename_String, n_Integer, OptionsPattern[FileBaseName]]" n_int = n.get_int_value() parts = pathlib.Path(filename.to_python()[1:-1]).parts @@ -1479,11 +1483,11 @@ class FindList(Builtin): # TODO: Extra options AnchoredSearch, IgnoreCase RecordSeparators, # WordSearch, WordSeparators this is probably best done with a regex - def apply_without_n(self, filename, text, evaluation, options): + def eval_without_n(self, filename, text, evaluation, options): "FindList[filename_, text_, OptionsPattern[FindList]]" - return self.apply(filename, text, None, evaluation, options) + return self.eval(filename, text, None, evaluation, options) - def apply(self, filename, text, n, evaluation, options): + def eval(self, filename, text, n, evaluation, options): "FindList[filename_, text_, n_, OptionsPattern[FindList]]" py_text = text.to_python() py_name = filename.to_python() @@ -1713,7 +1717,7 @@ class Needs(Builtin): } summary_text = "load a package if it is not already loaded" - def apply(self, context, evaluation): + def eval(self, context, evaluation): "Needs[context_String]" contextstr = context.get_string_value() if contextstr == "": @@ -1789,7 +1793,7 @@ class ParentDirectory(Builtin): } summary_text = "parent directory of the current working directory" - def apply(self, path, evaluation): + def eval(self, path, evaluation): "ParentDirectory[path_]" if not isinstance(path, String): @@ -1848,7 +1852,6 @@ class RenameDirectory(Builtin): """ messages = { - "argr": "called with `1` argument; 2 arguments are expected.", "fstr": ( "File specification `1` is not a string of " "one or more characters." ), @@ -1857,12 +1860,12 @@ class RenameDirectory(Builtin): } summary_text = "change the name of a directory" - def apply(self, dirs, evaluation): + def eval(self, dirs, evaluation): "RenameDirectory[dirs__]" seq = dirs.get_sequence() if len(seq) != 2: - evaluation.message("RenameDirectory", "argr", len(seq)) + evaluation.message("RenameDirectory", "argr", "RenameDirectory", 2) return (dir1, dir2) = (s.to_python() for s in seq) @@ -1911,7 +1914,7 @@ class RenameFile(Builtin): } summary_text = "change the name of a file" - def apply(self, source, dest, evaluation): + def eval(self, source, dest, evaluation): "RenameFile[source_, dest_]" py_source = source.to_python() @@ -1963,7 +1966,7 @@ class ResetDirectory(Builtin): } summary_text = "return to the directory before the last SetDirectory call" - def apply(self, evaluation): + def eval(self, evaluation): "ResetDirectory[]" try: tmp = DIRECTORY_STACK.pop() @@ -2020,7 +2023,7 @@ class SetDirectory(Builtin): } summary_text = "set the working directory" - def apply(self, path, evaluation): + def eval(self, path, evaluation): "SetDirectory[path_]" if not isinstance(path, String): @@ -2097,7 +2100,7 @@ class SetFileDate(Builtin): } summary_text = "set the access/modification time of a file in the filesystem" - def apply(self, filename, datelist, attribute, evaluation): + def eval(self, filename, datelist, attribute, evaluation): "SetFileDate[filename_, datelist_, attribute_]" py_filename = filename.to_python() @@ -2170,19 +2173,19 @@ def apply(self, filename, datelist, attribute, evaluation): os.utime(py_filename, (osp.getatime(py_filename), stattime)) if py_attr == "All": os.utime(py_filename, (stattime, stattime)) - except OSError as e: + except OSError: # as e: # evaluation.message(...) return SymbolFailed return SymbolNull - def apply_1arg(self, filename, evaluation): + def eval_1arg(self, filename, evaluation): "SetFileDate[filename_]" - return self.apply(filename, None, None, evaluation) + return self.eval(filename, None, None, evaluation) - def apply_2arg(self, filename, datelist, evaluation): + def eval_2arg(self, filename, datelist, evaluation): "SetFileDate[filename_, datelist_]" - return self.apply(filename, datelist, None, evaluation) + return self.eval(filename, datelist, None, evaluation) class TemporaryDirectory(Predefined): @@ -2252,10 +2255,11 @@ def evaluate(self, evaluation): class URLSave(Builtin): """
-
'URLSave["url"]' -
Save "url" in a temporary file. -
'URLSave["url", $filename$]' -
Save "url" in $filename$. +
'URLSave["url"]' +
Save "url" in a temporary file. + +
'URLSave["url", $filename$]' +
Save "url" in $filename$.
""" @@ -2265,11 +2269,11 @@ class URLSave(Builtin): } summary_text = "save the content of an URL" - def apply_1(self, url, evaluation, **options): + def eval_1(self, url, evaluation, **options): "URLSave[url_String, OptionsPattern[URLSave]]" - return self.apply_2(url, None, evaluation, **options) + return self.eval_2(url, None, evaluation, **options) - def apply_2(self, url, filename, evaluation, **options): + def eval_2(self, url, filename, evaluation, **options): "URLSave[url_String, filename_, OptionsPattern[URLSave]]" url = url.value if filename is None: diff --git a/mathics/builtin/graphics.py b/mathics/builtin/graphics.py index 9651cefb7..de1e3f3a2 100644 --- a/mathics/builtin/graphics.py +++ b/mathics/builtin/graphics.py @@ -47,13 +47,16 @@ from mathics.core.list import ListExpression from mathics.core.symbols import ( Symbol, - system_symbols, + symbol_set, system_symbols_dict, SymbolList, SymbolNull, ) from mathics.core.systemsymbols import ( + SymbolEdgeForm, + SymbolFaceForm, SymbolMakeBoxes, + SymbolRule, ) from mathics.core.formatter import lookup_method @@ -61,9 +64,6 @@ from mathics.core.attributes import A_PROTECTED, A_READ_PROTECTED -SymbolEdgeForm = Symbol("System`EdgeForm") -SymbolFaceForm = Symbol("System`FaceForm") - GRAPHICS_OPTIONS = { "AspectRatio": "Automatic", "Axes": "False", @@ -1427,26 +1427,26 @@ class Tiny(Builtin): element_heads = frozenset( - system_symbols( - "Arrow", - "BezierCurve", - "Circle", - "Cone", - "Cuboid", - "Cylinder", - "Disk", - "FilledCurve", - "Inset", - "Line", - "Point", - "Polygon", - "Rectangle", - "RegularPolygon", - "Sphere", - "Style", - "Text", - "Tube", - "UniformPolyhedron", + symbol_set( + Symbol("System`Arrow"), + Symbol("System`BezierCurve"), + Symbol("System`Circle"), + Symbol("System`Cone"), + Symbol("System`Cuboid"), + Symbol("System`Cylinder"), + Symbol("System`Disk"), + Symbol("System`FilledCurve"), + Symbol("System`Inset"), + Symbol("System`Line"), + Symbol("System`Point"), + Symbol("System`Polygon"), + Symbol("System`Rectangle"), + Symbol("System`RegularPolygon"), + Symbol("System`Sphere"), + Symbol("System`Style"), + Symbol("System`Text"), + Symbol("System`Tube"), + Symbol("System`UniformPolyhedron"), ) ) @@ -1477,7 +1477,7 @@ class Tiny(Builtin): style_heads = frozenset(styles.keys()) style_and_form_heads = frozenset( - style_heads.union(system_symbols("System`EdgeForm", "System`FaceForm")) + style_heads.union(symbol_set(SymbolEdgeForm, SymbolFaceForm)) ) GLOBALS.update( @@ -1497,8 +1497,8 @@ class Tiny(Builtin): GLOBALS.update(styles) GRAPHICS_SYMBOLS = { - Symbol("System`List"), - Symbol("System`Rule"), + SymbolList, + SymbolRule, Symbol("System`VertexColors"), *element_heads, *[Symbol(element.name + "Box") for element in element_heads], diff --git a/mathics/builtin/lists.py b/mathics/builtin/lists.py index 004e90d6b..68785ff4b 100644 --- a/mathics/builtin/lists.py +++ b/mathics/builtin/lists.py @@ -172,7 +172,7 @@ def check_options(self, expr, evaluation, options): return evaluation.message("ContainsOnly", "optx", Symbol(key), expr) return None - def apply(self, list1, list2, evaluation, options={}): + def eval(self, list1, list2, evaluation, options={}): "ContainsOnly[list1_List, list2_List, OptionsPattern[ContainsOnly]]" same_test = self.get_option(options, "SameTest", evaluation) @@ -188,7 +188,7 @@ def sameQ(a, b) -> bool: return SymbolFalse return SymbolTrue - def apply_msg(self, e1, e2, evaluation, options={}): + def eval_msg(self, e1, e2, evaluation, options={}): "ContainsOnly[e1_, e2_, OptionsPattern[ContainsOnly]]" opts = ( @@ -295,6 +295,8 @@ class Delete(Builtin): """ messages = { + # FIXME: This message doesn't exist in more modern WMA, and + # Delete *can* take more than 2 arguments. "argr": "Delete called with 1 argument; 2 arguments are expected.", "argt": "Delete called with `1` arguments; 2 arguments are expected.", "psl": "Position specification `1` in `2` is not a machine-sized integer or a list of machine-sized integers.", @@ -302,7 +304,7 @@ class Delete(Builtin): } summary_text = "delete elements from a list at given positions" - def apply_one(self, expr, position: Integer, evaluation): + def eval_one(self, expr, position: Integer, evaluation): "Delete[expr_, position_Integer]" pos = position.value try: @@ -310,7 +312,7 @@ def apply_one(self, expr, position: Integer, evaluation): except PartRangeError: evaluation.message("Part", "partw", ListExpression(position), expr) - def apply(self, expr, positions, evaluation): + def eval(self, expr, positions, evaluation): "Delete[expr_, positions___]" positions = positions.get_sequence() if len(positions) > 1: @@ -443,7 +445,7 @@ class Level(Builtin): } summary_text = "parts specified by a given number of indices" - def apply(self, expr, ls, evaluation, options={}): + def eval(self, expr, ls, evaluation, options={}): "Level[expr_, ls_, OptionsPattern[Level]]" try: @@ -509,7 +511,7 @@ class List(Builtin): attributes = A_LOCKED | A_PROTECTED summary_text = "specify a list explicitly" - def apply(self, elements, evaluation): + def eval(self, elements, evaluation): """List[elements___]""" # Pick out the elements part of the parameter elements; # we we will call that `elements_part_of_elements__`. @@ -518,7 +520,7 @@ def apply(self, elements, evaluation): elements_part_of_elements__ = elements.get_sequence() return ListExpression(*elements_part_of_elements__) - def apply_makeboxes(self, items, f, evaluation): + def eval_makeboxes(self, items, f, evaluation): """MakeBoxes[{items___}, f:StandardForm|TraditionalForm|OutputForm|InputForm|FullForm]""" @@ -649,7 +651,7 @@ class Split(Builtin): } summary_text = "split into runs of identical elements" - def apply(self, mlist, test, evaluation): + def eval(self, mlist, test, evaluation): "Split[mlist_, test_]" expr = Expression(SymbolSplit, mlist, test) @@ -702,7 +704,7 @@ class SplitBy(Builtin): summary_text = "split based on values of a function applied to elements" - def apply(self, mlist, func, evaluation): + def eval(self, mlist, func, evaluation): "SplitBy[mlist_, func_?NotListQ]" expr = Expression(SymbolSplit, mlist, func) @@ -727,7 +729,7 @@ def apply(self, mlist, func, evaluation): outer = structure(mlist.head, inner, evaluation) return outer([inner(t) for t in result]) - def apply_multiple(self, mlist, funcs, evaluation): + def eval_multiple(self, mlist, funcs, evaluation): "SplitBy[mlist_, funcs_List]" expr = Expression(SymbolSplit, mlist, funcs) @@ -737,7 +739,7 @@ def apply_multiple(self, mlist, funcs, evaluation): result = mlist for f in funcs.elements[::-1]: - result = self.apply(result, f, evaluation) + result = self.eval(result, f, evaluation) return result @@ -781,7 +783,7 @@ class LeafCount(Builtin): } summary_text = "the total number of atomic subexpressions" - def apply(self, expr, evaluation): + def eval(self, expr, evaluation): "LeafCount[expr___]" from mathics.core.atoms import Rational, Complex @@ -820,7 +822,7 @@ class _IterationFunction(Builtin): def get_result(self, items): pass - def apply_symbol(self, expr, iterator, evaluation): + def eval_symbol(self, expr, iterator, evaluation): "%(name)s[expr_, iterator_Symbol]" iterator = iterator.evaluate(evaluation) if iterator.has_form(["List", "Range", "Sequence"], None): @@ -830,19 +832,19 @@ def apply_symbol(self, expr, iterator, evaluation): elif len(elements) == 2: if elements[1].has_form(["List", "Sequence"], None): seq = Expression(SymbolSequence, *(elements[1].elements)) - return self.apply_list(expr, elements[0], seq, evaluation) + return self.eval_list(expr, elements[0], seq, evaluation) else: - return self.apply_range(expr, *elements, evaluation) + return self.eval_range(expr, *elements, evaluation) elif len(elements) == 3: - return self.apply_iter_nostep(expr, *elements, evaluation) + return self.eval_iter_nostep(expr, *elements, evaluation) elif len(elements) == 4: - return self.apply_iter(expr, *elements, evaluation) + return self.eval_iter(expr, *elements, evaluation) if self.throw_iterb: evaluation.message(self.get_name(), "iterb") return - def apply_range(self, expr, i, imax, evaluation): + def eval_range(self, expr, i, imax, evaluation): "%(name)s[expr_, {i_Symbol, imax_}]" imax = imax.evaluate(evaluation) if imax.has_form("Range", None): @@ -852,11 +854,11 @@ def apply_range(self, expr, i, imax, evaluation): return self.apply_list(expr, i, seq, evaluation) elif imax.has_form("List", None): seq = Expression(SymbolSequence, *(imax.elements)) - return self.apply_list(expr, i, seq, evaluation) + return self.eval_list(expr, i, seq, evaluation) else: - return self.apply_iter(expr, i, Integer1, imax, Integer1, evaluation) + return self.eval_iter(expr, i, Integer1, imax, Integer1, evaluation) - def apply_max(self, expr, imax, evaluation): + def eval_max(self, expr, imax, evaluation): "%(name)s[expr_, {imax_}]" # Even though `imax` should be an integeral value, its type does not @@ -912,11 +914,11 @@ def do_iteration(): return self.get_result(result) - def apply_iter_nostep(self, expr, i, imin, imax, evaluation): + def eval_iter_nostep(self, expr, i, imin, imax, evaluation): "%(name)s[expr_, {i_Symbol, imin_, imax_}]" - return self.apply_iter(expr, i, imin, imax, Integer1, evaluation) + return self.eval_iter(expr, i, imin, imax, Integer1, evaluation) - def apply_iter(self, expr, i, imin, imax, di, evaluation): + def eval_iter(self, expr, i, imin, imax, di, evaluation): "%(name)s[expr_, {i_Symbol, imin_, imax_, di_}]" if isinstance(self, SympyFunction) and di.get_int_value() == 1: @@ -977,7 +979,7 @@ def apply_iter(self, expr, i, imin, imax, di, evaluation): index = Expression(SymbolPlus, index, di).evaluate(evaluation) return self.get_result(result) - def apply_list(self, expr, i, items, evaluation): + def eval_list(self, expr, i, items, evaluation): "%(name)s[expr_, {i_Symbol, {items___}}]" items = items.evaluate(evaluation).get_sequence() result = [] @@ -1003,7 +1005,7 @@ def apply_list(self, expr, i, items, evaluation): raise return self.get_result(result) - def apply_multi(self, expr, first, sequ, evaluation): + def eval_multi(self, expr, first, sequ, evaluation): "%(name)s[expr_, first_, sequ__]" sequ = sequ.get_sequence() @@ -1027,7 +1029,7 @@ class Insert(Builtin): summary_text = "insert an element at a given position" - def apply(self, expr, elem, n: Integer, evaluation): + def eval(self, expr, elem, n: Integer, evaluation): "Insert[expr_List, elem_, n_Integer]" py_n = n.value @@ -1232,7 +1234,7 @@ class TakeLargestBy(_RankedTakeLargest): summary_text = "sublist of n largest elements according to a given criteria" - def apply(self, element, f, n, evaluation, options): + def eval(self, element, f, n, evaluation, options): "TakeLargestBy[element_List, f_, n_, OptionsPattern[TakeLargestBy]]" return self._compute(element, n, evaluation, options, f=f) @@ -1256,7 +1258,7 @@ class TakeSmallestBy(_RankedTakeSmallest): summary_text = "sublist of n largest elements according to a criteria" - def apply(self, element, f, n, evaluation, options): + def eval(self, element, f, n, evaluation, options): "TakeSmallestBy[element_List, f_, n_, OptionsPattern[TakeSmallestBy]]" return self._compute(element, n, evaluation, options, f=f) @@ -1416,7 +1418,7 @@ def levels(k): ) return None - def apply_zero(self, element, n, evaluation): + def eval_zero(self, element, n, evaluation): "%(name)s[element_, n_]" return self._pad( element, @@ -1427,7 +1429,7 @@ def apply_zero(self, element, n, evaluation): lambda: Expression(self.get_name(), element, n), ) - def apply(self, element, n, x, evaluation): + def eval(self, element, n, x, evaluation): "%(name)s[element_, n_, x_]" return self._pad( element, @@ -1438,7 +1440,7 @@ def apply(self, element, n, x, evaluation): lambda: Expression(self.get_name(), element, n, x), ) - def apply_margin(self, element, n, x, m, evaluation): + def eval_margin(self, element, n, x, m, evaluation): "%(name)s[element_, n_, x_, m_]" return self._pad( element, @@ -1820,7 +1822,7 @@ class FindClusters(_Cluster): summary_text = "divide data into lists of similar elements" - def apply(self, p, evaluation, options): + def eval(self, p, evaluation, options): "FindClusters[p_, OptionsPattern[%(name)s]]" return self._cluster( p, @@ -1831,7 +1833,7 @@ def apply(self, p, evaluation, options): Expression(SymbolFindClusters, p, *options_to_rules(options)), ) - def apply_manual_k(self, p, k: Integer, evaluation, options): + def eval_manual_k(self, p, k: Integer, evaluation, options): "FindClusters[p_, k_Integer, OptionsPattern[%(name)s]]" return self._cluster( p, @@ -1867,7 +1869,7 @@ class ClusteringComponents(_Cluster): summary_text = "label data with the index of the cluster it is in" - def apply(self, p, evaluation, options): + def eval(self, p, evaluation, options): "ClusteringComponents[p_, OptionsPattern[%(name)s]]" return self._cluster( p, @@ -1878,7 +1880,7 @@ def apply(self, p, evaluation, options): Expression(SymbolClusteringComponents, p, *options_to_rules(options)), ) - def apply_manual_k(self, p, k: Integer, evaluation, options): + def eval_manual_k(self, p, k: Integer, evaluation, options): "ClusteringComponents[p_, k_Integer, OptionsPattern[%(name)s]]" return self._cluster( p, @@ -1941,7 +1943,7 @@ class Nearest(Builtin): } summary_text = "the nearest element from a list" - def apply(self, items, pivot, limit, expression, evaluation, options): + def eval(self, items, pivot, limit, expression, evaluation, options): "Nearest[items_, pivot_, limit_, OptionsPattern[%(name)s]]" method = self.get_option(options, "Method", evaluation) @@ -2078,6 +2080,8 @@ class SubsetQ(Builtin): """ messages = { + # FIXME: This message doesn't exist in more modern WMA, and + # Subset *can* take more than 2 arguments. "argr": "SubsetQ called with 1 argument; 2 arguments are expected.", "argrx": "SubsetQ called with `1` arguments; 2 arguments are expected.", "heads": "Heads `1` and `2` at positions 1 and 2 are expected to be the same.", @@ -2085,7 +2089,7 @@ class SubsetQ(Builtin): } summary_text = "test if a list is a subset of another list" - def apply(self, expr, subset, evaluation): + def eval(self, expr, subset, evaluation): "SubsetQ[expr_, subset___]" if isinstance(expr, Atom): diff --git a/mathics/builtin/options.py b/mathics/builtin/options.py index e9d0371e5..9df8bee11 100644 --- a/mathics/builtin/options.py +++ b/mathics/builtin/options.py @@ -79,11 +79,11 @@ def eval(self, f, i, evaluation): i = [index.get_int_value() for index in i] for index in i: if index is None or index < 1: - evaluation.message(SymbolDefault, "intp") + evaluation.message(SymbolDefault.name, "intp") return name = f.get_name() if not name: - evaluation.message(SymbolDefault, "sym", f, 1) + evaluation.message(SymbolDefault.name, "sym", f, 1) return result = get_default_value(name, evaluation, *i) return result diff --git a/mathics/builtin/patterns.py b/mathics/builtin/patterns.py index 7faba7731..72fae8e61 100644 --- a/mathics/builtin/patterns.py +++ b/mathics/builtin/patterns.py @@ -975,7 +975,6 @@ class Pattern_(PatternObject): "nodef": ( "No default setting found for `1` in " "position `2` when length is `3`." ), - "argr": "Pattern called with 1 argument; 2 arguments are expected.", } rules = { diff --git a/mathics/core/atoms.py b/mathics/core/atoms.py index 9134b16d9..57e8661c5 100644 --- a/mathics/core/atoms.py +++ b/mathics/core/atoms.py @@ -24,9 +24,9 @@ Symbol, SymbolNull, SymbolTrue, - system_symbols, + symbol_set, ) -from mathics.core.systemsymbols import SymbolInfinity +from mathics.core.systemsymbols import SymbolInfinity, SymbolInputForm, SymbolFullForm # Imperical number that seems to work. # We have to be able to match mpmath values with sympy values @@ -35,7 +35,7 @@ SymbolI = Symbol("I") SymbolString = Symbol("String") -SYSTEM_SYMBOLS_INPUT_OR_FULL_FORM = system_symbols("InputForm", "FullForm") +SYSTEM_SYMBOLS_INPUT_OR_FULL_FORM = symbol_set(SymbolInputForm, SymbolFullForm) class Number(Atom, ImmutableValueMixin, NumericOperators): diff --git a/mathics/core/definitions.py b/mathics/core/definitions.py index 81a156b0b..badbb7768 100644 --- a/mathics/core/definitions.py +++ b/mathics/core/definitions.py @@ -95,11 +95,17 @@ class Definitions: - definition_cache: keep definitions obtained by merging builtins, pymathics, and user definitions associated to the same symbol. """ + _already_loaded = False + builtin = {} + def __init__( - self, add_builtin=False, builtin_filename=None, extension_modules=[] + self, + add_builtin: bool = False, # Set to True to ensure that builtin get loaded. + builtin_filename: Optional[str] = None, # Not used by now. + extension_modules: list = [] # The list of extension modules to be loaded + # in this instance. ) -> None: super(Definitions, self).__init__() - self.builtin = {} self.user = {} self.pymathics = {} self.definitions_cache = {} @@ -112,6 +118,46 @@ def __init__( "System`", "Global`", ) + self.trace_evaluation = False + self.timing_trace_evaluation = False + + # All the following code must be run if add_builtin is set to True + # and self.builtin was not initialized. + if add_builtin: + # self.builtin now is a class attribute, so + # the loading is done just once, the first time + # that Definitions is created with the ``add_builtin`` + # parameter set to ``True``. + self.do_load_builtins() # (builtin_filename) + + # Load the extension modules. + # TODO: Consider to move this to do_add_builtins. + for module in extension_modules: + try: + self.load_pymathics_module(module, remove_on_quit=False) + except PyMathicsLoadException: + raise + except ImportError: + raise + + # PrintForms and OutputForms are populated when + # mathics.builtin.forms.base is loaded for the first time. + # Incidentally, this happens when mathics.format is loaded. + # but in general, we should assume that this happens after + # the builtins are stored. + self.printforms = list(PrintForms) + self.outputforms = list(OutputForms) + + def do_load_builtins(self, builtin_filename: Optional[str] = None): + """ + This function is called just once, to populate + the builtin dictionary. + """ + from mathics.builtin import modules, contribute + from mathics.settings import ROOT_DIR + + # I move this here, because this must be loaded just + # the first time that the builtins are loaded. # Importing "mathics.format" populates the Symbol of the # PrintForms and OutputForms sets. @@ -121,64 +167,66 @@ def __init__( # 2 were given # Rocky: this smells of something not quite right in terms of # modularity. - import mathics.format # noqa - self.printforms = list(PrintForms) - self.outputforms = list(OutputForms) - self.trace_evaluation = False - self.timing_trace_evaluation = False + if self._already_loaded: + return - if add_builtin: - from mathics.builtin import modules, contribute - from mathics.settings import ROOT_DIR - - loaded = False - if builtin_filename is not None: - builtin_dates = [get_file_time(module.__file__) for module in modules] - builtin_time = max(builtin_dates) - if get_file_time(builtin_filename) > builtin_time: - builtin_file = open(builtin_filename, "rb") - self.builtin = pickle.load(builtin_file) - loaded = True - if not loaded: - contribute(self) - for module in extension_modules: - try: - self.load_pymathics_module(module, remove_on_quit=False) - except PyMathicsLoadException: - raise - except ImportError: - raise - - if builtin_filename is not None: - builtin_file = open(builtin_filename, "wb") - pickle.dump(self.builtin, builtin_file, -1) - - autoload_files(self, ROOT_DIR, "autoload") - - # Move any user definitions created by autoloaded files to - # builtins, and clear out the user definitions list. This - # means that any autoloaded definitions become shared - # between users and no longer disappear after a Quit[]. - # - # Autoloads that accidentally define a name in Global` - # could cause confusion, so check for this. - # - for name in self.user: - if name.startswith("Global`"): - raise ValueError("autoload defined %s." % name) - - # Move symbols defined in autoload modules - # to Builtin definitions. - # This is important to avoid that reset_user_definitions - # getting rid of the autoloaded definitions (including) - # the needed by Import/Export. - for name in self.user: - self.builtin[name] = self.get_definition(name) + # # This chunck of code is here because at some moment we considered to store + # # the builtins as a pickled file, to speedup the initialization. + # # This didn't work, and we do not use this by now, so let's comment out + # # by now... + # + # loaded = False + # if builtin_filename is not None: + # builtin_dates = [get_file_time(module.__file__) for module in modules] + # builtin_time = max(builtin_dates) + # if get_file_time(builtin_filename) > builtin_time: + # builtin_file = open(builtin_filename, "rb") + # self.builtin = pickle.load(builtin_file) + # loaded = True + # if not loaded: + contribute(self) + # # This would store the builtins in a picke. Also it + # # is not used by now... + # if builtin_filename is not None: + # builtin_file = open(builtin_filename, "wb") + # pickle.dump(self.builtin, builtin_file, -1) + + # Notice that definitions here are stored in the user + # dict of the first instance of the class which is called + # with the `add_builtin` parameter set to True. + + autoload_files(self, ROOT_DIR, "autoload") - self.user = {} - self.clear_cache() + # Move any user definitions created by autoloaded files to + # builtins, and clear out the user definitions list. This + # means that any autoloaded definitions become shared + # between users and no longer disappear after a Quit[]. + # + # Autoloads that accidentally define a name in Global` + # could cause confusion, so check for this. + # + for name in self.user: + if name.startswith("Global`"): + raise ValueError("autoload defined %s." % name) + + # Move symbols defined in autoload modules + # to Builtin definitions. + # This is important to avoid that reset_user_definitions + # getting rid of the autoloaded definitions (including) + # the needed by Import/Export. + + for name in self.user: + self.builtin[name] = self.get_definition(name) + + self.user = {} + self.clear_cache() + # Restore the context and trace variables. + self.current_context = "Global`" + self.trace_evaluation = False + self.timing_trace_evaluation = False + Definitions._already_loaded = True def load_pymathics_module(self, module, remove_on_quit=True): """ diff --git a/mathics/core/evaluation.py b/mathics/core/evaluation.py index 3c1dfa174..3ac098309 100644 --- a/mathics/core/evaluation.py +++ b/mathics/core/evaluation.py @@ -262,9 +262,6 @@ def __init__( # status of last evaluate self.exc_result = self.SymbolNull self.last_eval = None - # Necesary to handle OneIdentity on - # lhs in assignment - self.ignore_oneidentity = False # Used in ``mathics.builtin.numbers.constants.get_constant`` and # ``mathics.builtin.numeric.N``. self._preferred_n_method = [] diff --git a/mathics/core/evaluators.py b/mathics/core/evaluators.py index 1485e121e..8f0dc1762 100644 --- a/mathics/core/evaluators.py +++ b/mathics/core/evaluators.py @@ -21,10 +21,11 @@ ) from mathics.core.convert.sympy import from_sympy from mathics.core.definitions import PyMathicsLoadException +from mathics.core.element import BaseElement from mathics.core.evaluation import Evaluation from mathics.core.expression import Expression from mathics.core.number import PrecisionValueError, get_precision -from mathics.core.symbols import Atom, BaseElement +from mathics.core.symbols import Atom from mathics.core.systemsymbols import SymbolMachinePrecision, SymbolN diff --git a/mathics/core/expression.py b/mathics/core/expression.py index 04025e0ef..5cb02211f 100644 --- a/mathics/core/expression.py +++ b/mathics/core/expression.py @@ -37,11 +37,14 @@ Monomial, NumericOperators, Symbol, + SymbolAbs, + SymbolDivide, SymbolList, SymbolN, + SymbolPlus, SymbolTimes, SymbolTrue, - system_symbols, + symbol_set, ) from mathics.core.systemsymbols import ( SymbolAborted, @@ -50,9 +53,14 @@ SymbolCondition, SymbolDirectedInfinity, SymbolFunction, + SymbolMinus, SymbolPattern, + SymbolPower, SymbolSequence, + SymbolSin, SymbolSlot, + SymbolSqrt, + SymbolSubtract, SymbolUnevaluated, ) @@ -70,16 +78,16 @@ SymbolVerbatim = Symbol("Verbatim") -symbols_arithmetic_operations = system_symbols( - "Sqrt", - "Times", - "Plus", - "Subtract", - "Minus", - "Power", - "Abs", - "Divide", - "Sin", +symbols_arithmetic_operations = symbol_set( + SymbolAbs, + SymbolDivide, + SymbolMinus, + SymbolPlus, + SymbolPower, + SymbolSin, + SymbolSqrt, + SymbolSubtract, + SymbolTimes, ) diff --git a/mathics/core/list.py b/mathics/core/list.py index 4dd21d6d1..bd5818681 100644 --- a/mathics/core/list.py +++ b/mathics/core/list.py @@ -41,7 +41,7 @@ def __init__( # call_frame = inspect.getouterframes(curframe, 2) # print("caller name:", call_frame[1][3]) - # from mathics.core.symbols import BaseElement + # from mathics.core.element import BaseElement # for element in elements: # if not isinstance(element, BaseElement): # from trepan.api import debug; debug() diff --git a/mathics/core/pattern.py b/mathics/core/pattern.py index 39dc1511e..b0a398d58 100644 --- a/mathics/core/pattern.py +++ b/mathics/core/pattern.py @@ -2,52 +2,58 @@ # cython: profile=False # -*- coding: utf-8 -*- - -from mathics.core.element import ensure_context -from mathics.core.expression import Expression -from mathics.core.symbols import Atom, Symbol, system_symbols -from mathics.core.systemsymbols import SymbolSequence +from typing import Optional + +from mathics.core.atoms import Integer +from mathics.core.element import BaseElement, ensure_context +from mathics.core.evaluation import Evaluation +from mathics.core.expression import Expression, SymbolDefault +from mathics.core.symbols import Atom, Symbol, symbol_set +from mathics.core.systemsymbols import ( + SymbolAlternatives, + SymbolBlank, + SymbolBlankNullSequence, + SymbolBlankSequence, + SymbolCondition, + SymbolOptional, + SymbolOptionsPattern, + SymbolPattern, + SymbolPatternTest, + SymbolRepeated, + SymbolRepeatedNull, + SymbolSequence, +) from mathics.core.util import subsets, subranges, permutations from itertools import chain from mathics.core.attributes import A_FLAT, A_ONE_IDENTITY, A_ORDERLESS -# from mathics.core.pattern_nocython import ( -# StopGenerator #, Pattern #, ExpressionPattern) -# from mathics.core import pattern_nocython - - -SYSTEM_SYMBOLS_PATTERNS = system_symbols( - "Pattern", - "PatternTest", - "Condition", - "Optional", - "Blank", - "BlankSequence", - "BlankNullSequence", - "Alternatives", - "OptionsPattern", - "Repeated", - "RepeatedNull", +# FIXME: create definitions in systemsymbols for missing items below. +SYSTEM_SYMBOLS_PATTERNS = symbol_set( + SymbolAlternatives, + SymbolBlank, + SymbolBlankNullSequence, + SymbolBlankSequence, + SymbolCondition, + SymbolOptional, + SymbolOptionsPattern, + SymbolPattern, + SymbolPatternTest, + SymbolRepeated, + SymbolRepeatedNull, ) - -def Pattern_create(expr): - from mathics.builtin import pattern_objects - - # from mathics.core.pattern import AtomPattern, ExpressionPattern - - name = expr.get_head_name() - pattern_object = pattern_objects.get(name) - if pattern_object is not None: - return pattern_object(expr) - if isinstance(expr, Atom): - return AtomPattern(expr) - else: - return ExpressionPattern(expr) +pattern_objects = {} class StopGenerator(Exception): + """ + StopGenerator is the exception raised when + an expression matches a pattern. + The exception holds the attribute `value` + that is used as a return value in `match`. + """ + def __init__(self, value=None): self.value = value @@ -70,7 +76,22 @@ class Pattern: When the pattern matches, the symbol is bound to the parameter ``x``. """ - create = staticmethod(Pattern_create) + @staticmethod + def create(expr: BaseElement) -> "Pattern": + """ + If ``expr`` is listed in ``pattern_object`` return the pattern found there. + Otherwise, if ``expr`` is an ``Atom``, create and return ``AtomPattern`` for ``expr``. + Otherwise, create and return and ``ExpressionPattern`` for ``expr``. + """ + + name = expr.get_head_name() + pattern_object = pattern_objects.get(name) + if pattern_object is not None: + return pattern_object(expr) + if isinstance(expr, Atom): + return AtomPattern(expr) + else: + return ExpressionPattern(expr) def match( self, @@ -82,22 +103,29 @@ def match( element_index=None, element_count=None, fully=True, - wrap_oneid=True, ): + """ + Check if the expression matches the pattern (self). + If it does, calls `yield_func`. + vars collects subexpressions associated to subpatterns. + head ? + element_index ? + element_count ? + fully is used in match_elements, for the case of Orderless patterns. + """ raise NotImplementedError - """def match(self, expression, vars, evaluation, - head=None, element_index=None, element_count=None, - fully=True, wrap_oneid=True): - #raise NotImplementedError - result = [] - def yield_func(vars, rest): - result.append(vars, rest) - self._match(yield_func, expression, vars, evaluation, head, - element_index, element_count, fully, wrap_oneid) - return result""" + def does_match( + self, + expression: BaseElement, + evaluation: Evaluation, + vars=Optional[dict], + fully: bool = True, + ) -> bool: - def does_match(self, expression, evaluation, vars=None, fully=True): + """ + returns True if `expression` matches self. + """ if vars is None: vars = {} @@ -187,7 +215,6 @@ def match_symbol( element_index=None, element_count=None, fully=True, - wrap_oneid=True, ): if expression is self.atom: yield_func(vars, None) @@ -207,7 +234,6 @@ def match( element_index=None, element_count=None, fully=True, - wrap_oneid=True, ): if isinstance(expression, Atom) and expression.sameQ(self.atom): # yield vars, None @@ -244,7 +270,6 @@ def match( element_index=None, element_count=None, fully=True, - wrap_oneid=True, ): evaluation.check_stopped() attributes = self.head.get_attributes(evaluation.definitions) @@ -311,8 +336,7 @@ def yield_choice(pre_vars): # for new_vars, rest in self.match_element( # nopep8 # self.elements[0], self.elements[1:], ([], expression.elements), # pre_vars, expression, attributes, evaluation, first=True, - # fully=fully, element_count=len(self.elements), - # wrap_oneid=expression.get_head_name() != 'System`MakeBoxes'): + # fully=fully, element_count=len(self.elements)): # def yield_element(new_vars, rest): # yield_func(new_vars, rest) self.match_element( @@ -327,7 +351,6 @@ def yield_choice(pre_vars): first=True, fully=fully, element_count=len(self.elements), - wrap_oneid=expression.get_head_name() != "System`MakeBoxes", ) # for head_vars, _ in self.head.match(expression.get_head(), vars, @@ -351,51 +374,88 @@ def yield_head(head_vars, _): self.head.match(yield_head, expression.get_head(), vars, evaluation) except StopGenerator_ExpressionPattern_match: return - if ( - wrap_oneid - and not evaluation.ignore_oneidentity - and A_ONE_IDENTITY & attributes - and not self.head.expr.sameQ(expression.get_head()) # nopep8 - and not self.head.expr.sameQ(expression) - ): - # and not OneIdentity & - # (expression.get_attributes(evaluation.definitions) | - # expression.get_head().get_attributes(evaluation.definitions)): - new_expression = Expression(self.head.expr, expression) - for element in self.elements: - element.match_count = element.get_match_count() - element.candidates = [expression] - # element.get_match_candidates( - # new_expression.elements, new_expression, attributes, - # evaluation, vars) - if len(element.candidates) < element.match_count[0]: - return - # for new_vars, rest in self.match_element( - # self.elements[0], self.elements[1:], - # ([], [expression]), vars, new_expression, attributes, - # evaluation, first=True, fully=fully, - # element_count=len(self.elements), wrap_oneid=True): - # def yield_element(new_vars, rest): - # yield_func(new_vars, rest) - self.match_element( + + if A_ONE_IDENTITY & attributes: + # This is all about the pattern. We do this + # each time because at some point we should need + # to check the default values each time... + + # This tries to reduce the pattern to a non empty + # set of default values, and a single pattern. + default_indx = 0 + optionals = {} + new_pattern = None + pattern_head = self.head.expr + for pat_elem in self.elements: + default_indx += 1 + if isinstance(pat_elem, AtomPattern): + if new_pattern is not None: + return + new_pattern = pat_elem + # TODO: check into account the second argument, + # and if there is a default value... + elif pat_elem.get_head_name() == "System`Optional": + if len(pat_elem.elements) == 2: + pat, value = pat_elem.elements + if pat.get_head_name() == "System`Pattern": + key = pat.elements[0].atom.name + else: + # if the first element of the Optional + # is not a `Pattern`, then we need to + # store an empty element. + key = "" + optionals[key] = value + elif len(pat_elem.elements) == 1: + pat = pat_elem.elements[0] + if pat.get_head_name() == "System`Pattern": + key = pat.elements[0].atom.name + else: + key = "" + # Now, determine the default value + defaultvalue_expr = Expression( + SymbolDefault, pattern_head, Integer(default_indx) + ) + value = defaultvalue_expr.evaluate(evaluation) + if value.sameQ(defaultvalue_expr): + return + optionals[key] = value + else: + return + else: + if new_pattern is not None: + return + new_pattern = pat_elem + + # If there is not optional values in the pattern, then + # it can not match any expression as a OneIdentity pattern: + if len(optionals) == 0: + return + + # Remove the empty key and load the default values in vars + if "" in optionals: + del optionals[""] + vars.update(optionals) + # Try to match the non-optional element with the expression + new_pattern.match( yield_func, - self.elements[0], - self.elements[1:], - ([], [expression]), + expression, vars, - new_expression, - attributes, evaluation, - first=True, + head=head, + element_index=element_index, + element_count=element_count, fully=fully, - element_count=len(self.elements), - wrap_oneid=True, ) - def get_pre_choices(self, yield_func, expression, attributes, vars): + def get_pre_choices(self, yield_choice, expression, attributes, vars): + """ + If not Orderless, call yield_choice with vars as the parameter. + """ if A_ORDERLESS & attributes: self.sort() patterns = self.filter_elements("Pattern") + # a dict with entries having patterns with the same name + # which are not in vars. groups = {} prev_pattern = prev_name = None for pattern in patterns: @@ -484,9 +544,9 @@ def yield_next(next): # for setting in per_name(groups.items(), vars): # def yield_name(setting): # yield_func(setting) - per_name(yield_func, list(groups.items()), vars) + per_name(yield_choice, list(groups.items()), vars) else: - yield_func(vars) + yield_choice(vars) def __init__(self, expr): self.head = Pattern.create(expr.head) @@ -545,7 +605,6 @@ def match_element( first=False, fully=True, depth=1, - wrap_oneid=True, ): if rest_expression is None: @@ -626,6 +685,9 @@ def match_element( *set_lengths ) else: + # a generator that yields partitions of + # candidates as [before | block | after ] + sets = subranges( candidates, flexible_start=first and not fully, @@ -633,7 +695,6 @@ def match_element( less_first=less_first, *set_lengths ) - if rest_elements: next_element = rest_elements[0] next_rest_elements = rest_elements[1:] @@ -680,7 +741,6 @@ def match_yield(new_vars, _): depth=next_depth, element_index=next_index, element_count=element_count, - wrap_oneid=wrap_oneid, ) else: if not fully or (not items_rest[0] and not items_rest[1]): @@ -696,7 +756,6 @@ def yield_wrapping(item): head=expression.head, element_index=element_index, element_count=element_count, - wrap_oneid=wrap_oneid, ) self.get_wrappings( diff --git a/mathics/core/symbols.py b/mathics/core/symbols.py index 9ea745368..beb75ab7e 100644 --- a/mathics/core/symbols.py +++ b/mathics/core/symbols.py @@ -3,8 +3,7 @@ import sympy import time -import typing -from typing import Any, Optional +from typing import Any, FrozenSet, List, Optional, Tuple from mathics.core.element import ( BaseElement, @@ -251,7 +250,7 @@ def evaluate_elements(self, evaluation) -> "Atom": def get_atom_name(self) -> str: return self.__class__.__name__ - def get_atoms(self, include_heads=True) -> typing.List["Atom"]: + def get_atoms(self, include_heads=True) -> List["Atom"]: return [self] # We seem to need this because the caller doesn't distinguish something with elements @@ -667,15 +666,14 @@ def is_uncertain_final_definitions(self, definitions) -> bool: return False -# system_symbols('A', 'B', ...) -> [Symbol('System`A'), Symbol('System`B'), ...] -def system_symbols(*symbols) -> typing.FrozenSet[Symbol]: +def symbol_set(*symbols: Tuple[Symbol]) -> FrozenSet[Symbol]: """ - Return a frozenset of symbols from a list of names (strings). + Return a frozenset of symbols from a Symbol arguments. We will use this in testing membership, so an immutable object is fine. In 2021, we benchmarked frozenset versus list, tuple, and set and frozenset was the fastest. """ - return frozenset(Symbol(s) for s in symbols) + return frozenset(symbols) # Symbols used in this module. diff --git a/mathics/core/systemsymbols.py b/mathics/core/systemsymbols.py index b9d9c47fb..15088a264 100644 --- a/mathics/core/systemsymbols.py +++ b/mathics/core/systemsymbols.py @@ -35,6 +35,8 @@ SymbolAttributes = Symbol("System`Attributes") SymbolAutomatic = Symbol("System`Automatic") SymbolBlank = Symbol("System`Blank") +SymbolBlankNullSequence = Symbol("System`BlankNullSequence") +SymbolBlankSequence = Symbol("System`BlankSequence") SymbolBlend = Symbol("System`Blend") SymbolBreak = Symbol("System`Break") SymbolByteArray = Symbol("System`ByteArray") @@ -60,6 +62,7 @@ SymbolDirectedInfinity = Symbol("System`DirectedInfinity") SymbolDispatch = Symbol("System`Dispatch") SymbolDot = Symbol("System`Dot") +SymbolDownValues = Symbol("System`DownValues") SymbolE = Symbol("System`E") SymbolEdgeForm = Symbol("System`EdgeForm") SymbolEqual = Symbol("System`Equal") @@ -67,6 +70,7 @@ SymbolExpandAll = Symbol("System`ExpandAll") SymbolExport = Symbol("System`Export") SymbolExportString = Symbol("System`ExportString") +SymbolFaceForm = Symbol("System`FaceForm") SymbolFactorial = Symbol("System`Factorial") SymbolFailed = Symbol("System`$Failed") SymbolFloor = Symbol("System`Floor") @@ -112,11 +116,12 @@ SymbolMean = Symbol("System`Mean") SymbolMemberQ = Symbol("System`MemberQ") SymbolMessageName = Symbol("System`MessageName") -SymbolMinus = Symbol("System`Minus") +SymbolMessages = Symbol("System`Messages") SymbolMinus = Symbol("System`Minus") SymbolMissing = Symbol("System`Missing") SymbolN = Symbol("System`N") SymbolNIntegrate = Symbol("System`NIntegrate") +SymbolNValues = Symbol("System`NValues") SymbolNeeds = Symbol("System`Needs") SymbolNone = Symbol("System`None") SymbolNorm = Symbol("System`Norm") @@ -126,13 +131,17 @@ SymbolNumericQ = Symbol("System`NumericQ") SymbolO = Symbol("System`O") SymbolOptionValue = Symbol("System`OptionValue") +SymbolOptional = Symbol("System`Optional") SymbolOptions = Symbol("System`Options") +SymbolOptionsPattern = Symbol("System`OptionsPattern") SymbolOr = Symbol("System`Or") SymbolOut = Symbol("System`Out") SymbolOutputForm = Symbol("System`OutputForm") SymbolOverflow = Symbol("System`Overflow") +SymbolOwnValues = Symbol("System`OwnValues") SymbolPackages = Symbol("System`$Packages") SymbolPattern = Symbol("System`Pattern") +SymbolPatternTest = Symbol("System`PatternTest") SymbolPower = Symbol("System`Power") SymbolPi = Symbol("System`Pi") SymbolPiecewise = Symbol("System`Piecewise") @@ -146,6 +155,8 @@ SymbolRe = Symbol("System`Re") SymbolReal = Symbol("System`Real") SymbolRealDigits = Symbol("System`RealDigits") +SymbolRepeated = Symbol("System`Repeated") +SymbolRepeatedNull = Symbol("System`RepeatedNull") SymbolReturn = Symbol("System`Return") SymbolRound = Symbol("System`Round") SymbolRow = Symbol("System`Row") @@ -167,6 +178,7 @@ SymbolStringForm = Symbol("System`StringForm") SymbolStringQ = Symbol("System`StringQ") SymbolStyle = Symbol("System`Style") +SymbolSubValues = Symbol("System`SubValues") SymbolSubsetQ = Symbol("System`SubsetQ") SymbolSubtract = Symbol("System`Subtract") SymbolSubscriptBox = Symbol("System`SubscriptBox") @@ -181,4 +193,5 @@ SymbolUndefined = Symbol("System`Undefined") SymbolUnequal = Symbol("System`Unequal") SymbolUnevaluated = Symbol("System`Unevaluated") +SymbolUpValues = Symbol("System`UpValues") SymbolXor = Symbol("System`Xor") diff --git a/mathics/core/util.py b/mathics/core/util.py index 0dc06064e..b1bfef55e 100644 --- a/mathics/core/util.py +++ b/mathics/core/util.py @@ -64,6 +64,12 @@ def decide(chosen, not_chosen, rest, count): def subranges( items, min_count, max, flexible_start=False, included=None, less_first=False ): + """ + generator that yields possible divisions of items as + ([items_inside],([previos_items],[remaining_items])) + with items_inside of variable lengths. + If flexible_start, then [previos_items] also has a variable size. + """ # TODO: take into account included if max is None: diff --git a/test/test_rules_patterns.py b/test/builtin/atomic/test_symbols.py similarity index 51% rename from test/test_rules_patterns.py rename to test/builtin/atomic/test_symbols.py index b1c7ac476..bba780e0c 100644 --- a/test/test_rules_patterns.py +++ b/test/builtin/atomic/test_symbols.py @@ -1,5 +1,9 @@ # -*- coding: utf-8 -*- -from .helper import check_evaluation +""" +Unit tests from mathics.builtin.atomic.symbols. +""" + +from test.helper import check_evaluation def test_downvalues(): @@ -21,25 +25,3 @@ def test_downvalues(): ), ): check_evaluation(str_expr, str_expected, message) - - -def test_blank(): - for str_expr, str_expected, message in ( - ( - "g[i] /. _[i] :> a", - "a", - "Issue #203", - ), - ): - check_evaluation(str_expr, str_expected, message) - - -def test_complex_rule(): - for str_expr, str_expected, message in ( - ( - "a == d b + d c /. a_ x_ + a_ y_ -> a (x + y)", - "a == (b + c) d", - "Issue #212", - ), - ): - check_evaluation(str_expr, str_expected, message) diff --git a/test/builtin/test_assignment.py b/test/builtin/test_assignment.py index ca775564e..5d6cc791c 100644 --- a/test/builtin/test_assignment.py +++ b/test/builtin/test_assignment.py @@ -1,8 +1,17 @@ # -*- coding: utf-8 -*- +import os + import pytest from test.helper import check_evaluation, session from mathics_scanner.errors import IncompleteSyntaxError +DEBUGASSIGN = int(os.environ.get("DEBUGSET", "0")) == 1 + +if DEBUGASSIGN: + skip_or_fail = pytest.mark.xfail +else: + skip_or_fail = pytest.mark.skip + str_test_set_with_oneidentity = """ SetAttributes[SUNIndex, {OneIdentity}]; @@ -173,6 +182,81 @@ def test_set_and_clear(str_expr, str_expected, msg): ) +@pytest.mark.parametrize( + ("str_expr", "str_expected", "msg"), + [ + (None, None, None), + (r"a=b; a=4; {a, b}", "{4, b}", None), + (None, None, None), + (r"a=b; b=4; {a,b}", "{4, 4}", None), + (None, None, None), + (r"a=b; b=4; Clear[a]; {a,b}", "{a, 4}", None), + (None, None, None), + ("a=b; b=4; Clear[b]; {a, b}", "{b, b}", None), + (None, None, None), + ("F[x_]:=x^2; G[x_]:=F[x]; ClearAll[F]; G[u]", "F[u]", None), + (None, None, None), + ("F[x_]:=G[x]; G[x_]:=x^2; ClearAll[G]; F[u]", "G[u]", None), + (None, None, None), + ( + "F[x_]:=G[x]; H[F[y_]]:=Q[y]; ClearAll[F]; {H[G[5]],H[F[5]]}", + "{Q[5], H[F[5]]}", + "The arguments on the LHS are evaluated before the assignment", + ), + (None, None, None), + ( + "F[x_]:=G[x]; H[F[y_]]^:=Q[y]; ClearAll[F]; {H[G[5]],H[F[5]]}", + "{Q[5], H[F[5]]}", + "The arguments on the LHS are evaluated before the assignment", + ), + (None, None, None), + ( + "F[x_]:=G[x]; H[F[y_]]:=Q[y]; ClearAll[G]; {H[G[5]],H[F[5]]}", + "{Q[5], Q[5]}", + "The arguments on the LHS are evaluated before the assignment", + ), + (None, None, None), + ( + "F[x_]:=G[x]; H[F[y_]]^:=Q[y]; ClearAll[G]; {H[G[5]],H[F[5]]}", + "{H[G[5]], H[G[5]]}", + "The arguments on the LHS are evaluated before the assignment", + ), + (None, None, None), + ( + ( + "A[x_]:=B[x];B[x_]:=F[x_];F[x_]:=G[x];" + "H[A[y_]]:=Q[y]; ClearAll[F];" + "{H[A[5]],H[B[5]],H[F[5]],H[G[5]]}" + ), + "{H[F[5]], H[F[5]], H[F[5]], Q[5]}", + "The arguments on the LHS are completely evaluated before the assignment", + ), + (None, None, None), + ( + "F[x_]:=G[x];N[F[x_]]:=x^2;ClearAll[F];{N[F[2]],N[G[2]]}", + "{F[2.], 4.}", + "Assign N rule", + ), + ], +) +@skip_or_fail +def test_set_and_clear_to_fix(str_expr, str_expected, msg): + """ + Test calls to Set, Clear and ClearAll. If + str_expr is None, the session is reset, + in a way that the next test run over a fresh + environment. + """ + check_evaluation( + str_expr, + str_expected, + to_string_expr=True, + to_string_expected=True, + hold_expected=True, + failure_message=msg, + ) + + @pytest.mark.parametrize( ("str_expr", "str_expected", "message", "out_msgs"), [ diff --git a/test/builtin/test_attributes.py b/test/builtin/test_attributes.py new file mode 100644 index 000000000..41f76df66 --- /dev/null +++ b/test/builtin/test_attributes.py @@ -0,0 +1,228 @@ +# -*- coding: utf-8 -*- +""" +Unit tests from mathics.builtin.attributes. +""" + +import os +import pytest + +from test.helper import check_evaluation + +DEBUGRULESPAT = int(os.environ.get("DEBUGRULESPAT", "0")) == 1 + +if DEBUGRULESPAT: + skip_or_fail = pytest.mark.xfail +else: + skip_or_fail = pytest.mark.skip + + +@pytest.mark.parametrize( + ("str_expr", "str_expected", "msg"), + [ + (None, None, None), + # F has the attribute, but G doesn't. + ("SetAttributes[F, OneIdentity]", "Null", None), + ("SetAttributes[r, Flat]", "Null", None), + ("SetAttributes[s, Flat]", "Null", None), + ("SetAttributes[s, OneIdentity]", "Null", None), + ("MatchQ[x, F[y_]]", "False", "With OneIdentity"), + ("MatchQ[x, G[y_]]", "False", "Without OneIdentity"), + ("MatchQ[x, F[x_:0,y_]]", "True", "With OneIdentity, and Default"), + ("MatchQ[x, G[x_:0,y_]]", "False", "Without OneIdentity, and Default"), + ("MatchQ[F[x], F[x_:0,y_]]", "True", "With OneIdentity, and Default"), + ("MatchQ[G[x], G[x_:0,y_]]", "True", "Without OneIdentity, and Default"), + ("MatchQ[F[F[F[x]]], F[x_:0,y_]]", "True", "With OneIdentity, nested"), + ("MatchQ[G[G[G[x]]], G[x_:0,y_]]", "True", "Without OneIdentity, nested"), + ("MatchQ[F[3, F[F[x]]], F[x_:0,y_]]", "True", "With OneIdentity, nested"), + ("MatchQ[G[3, G[G[x]]], G[x_:0,y_]]", "True", "Without OneIdentity, nested"), + ( + "MatchQ[x, F[x1_:0, F[x2_:0,y_]]]", + "True", + "With OneIdentity, pattern nested", + ), + ( + "MatchQ[x, G[x1_:0, G[x2_:0,y_]]]", + "False", + "With OneIdentity, pattern nested", + ), + ( + "MatchQ[x, F[x1___:0, F[x2_:0,y_]]]", + "True", + "With OneIdentity, pattern nested", + ), + ( + "MatchQ[x, G[x1___:0, G[x2_:0,y_]]]", + "False", + "With OneIdentity, pattern nested", + ), + ("MatchQ[x, F[F[x2_:0,y_],x1_:0]]", "True", "With OneIdentity, pattern nested"), + ( + "MatchQ[x, G[G[x2_:0,y_],x1_:0]]", + "False", + "With OneIdentity, pattern nested", + ), + ("MatchQ[x, F[x_.,y_]]", "False", "With OneIdentity, and Optional, no default"), + ( + "MatchQ[x, G[x_.,y_]]", + "False", + "Without OneIdentity, and Optional, no default", + ), + ("Default[F, 1]=1.", "1.", None), + ("Default[G, 1]=2.", "2.", None), + ("MatchQ[x, F[x_.,y_]]", "True", "With OneIdentity, and Optional, default"), + ("MatchQ[x, G[x_.,y_]]", "False", "Without OneIdentity, and Optional, default"), + ("MatchQ[F[F[H[y]]],F[x_:0,u_H]]", "False", None), + ("MatchQ[G[G[H[y]]],G[x_:0,u_H]]", "False", None), + ("MatchQ[F[p, F[p, H[y]]],F[x_:0,u_H]]", "False", None), + ("MatchQ[G[p, G[p, H[y]]],G[x_:0,u_H]]", "False", None), + (None, None, None), + ], +) +def test_one_identity(str_expr, str_expected, msg): + check_evaluation( + str_expr, + str_expected, + to_string_expr=True, + to_string_expected=True, + hold_expected=True, + failure_message=msg, + ) + + +@pytest.mark.parametrize( + ("str_expr", "str_expected", "msg"), + [ + (None, None, None), + # F has the attribute, but G doesn't. + ("SetAttributes[F, OneIdentity]", "Null", None), + ("SetAttributes[r, Flat]", "Null", None), + ("SetAttributes[s, Flat]", "Null", None), + ("SetAttributes[s, OneIdentity]", "Null", None), + # Replace also takes into account the OneIdentity attribute, + # and also modifies the interpretation of the Flat attribute. + ( + "F[a,b,b,c]/.F[x_,x_]->Fp[x]", + "F[a, b, b, c]", + "https://reference.wolfram.com/language/tutorial/Patterns.html", + ), + # When fixed, Add the difference between the next two as a + # doctest example. + ( + "r[a,b,b,c]/.r[x_,x_]->rp[x]", + "r[a, rp[r[b]], c]", + "https://reference.wolfram.com/language/tutorial/Patterns.html", + ), + ( + "s[a,b,b,c]/.s[x_,x_]->sp[x]", + "s[a, rp[b], c]", + "https://reference.wolfram.com/language/tutorial/Patterns.html", + ), + (None, None, None), + ], +) +@skip_or_fail +def test_one_identity_still_failing(str_expr, str_expected, msg): + check_evaluation( + str_expr, + str_expected, + to_string_expr=True, + to_string_expected=True, + hold_expected=True, + failure_message=msg, + ) + + +@pytest.mark.parametrize( + ("str_expr", "str_expected", "msg"), + [ + (None, None, None), + # F has the attribute, but G doesn't. + ("SetAttributes[F, OneIdentity]", "Null", None), + ("SetAttributes[r, Flat]", "Null", None), + ("SetAttributes[s, Flat]", "Null", None), + ("SetAttributes[s, OneIdentity]", "Null", None), + ("MatchQ[x, F[y_]]", "False", "With OneIdentity"), + ("MatchQ[x, G[y_]]", "False", "Without OneIdentity"), + ("MatchQ[x, F[x_:0,y_]]", "True", "With OneIdentity, and Default"), + ("MatchQ[x, G[x_:0,y_]]", "False", "Without OneIdentity, and Default"), + ("MatchQ[F[x], F[x_:0,y_]]", "True", "With OneIdentity, and Default"), + ("MatchQ[G[x], G[x_:0,y_]]", "True", "Without OneIdentity, and Default"), + ("MatchQ[F[F[F[x]]], F[x_:0,y_]]", "True", "With OneIdentity, nested"), + ("MatchQ[G[G[G[x]]], G[x_:0,y_]]", "True", "Without OneIdentity, nested"), + ("MatchQ[F[3, F[F[x]]], F[x_:0,y_]]", "True", "With OneIdentity, nested"), + ("MatchQ[G[3, G[G[x]]], G[x_:0,y_]]", "True", "Without OneIdentity, nested"), + ( + "MatchQ[x, F[x1_:0, F[x2_:0,y_]]]", + "True", + "With OneIdentity, pattern nested", + ), + ( + "MatchQ[x, G[x1_:0, G[x2_:0,y_]]]", + "False", + "With OneIdentity, pattern nested", + ), + ( + "MatchQ[x, F[x1___:0, F[x2_:0,y_]]]", + "True", + "With OneIdentity, pattern nested", + ), + ( + "MatchQ[x, G[x1___:0, G[x2_:0,y_]]]", + "False", + "With OneIdentity, pattern nested", + ), + ("MatchQ[x, F[F[x2_:0,y_],x1_:0]]", "True", "With OneIdentity, pattern nested"), + ( + "MatchQ[x, G[G[x2_:0,y_],x1_:0]]", + "False", + "With OneIdentity, pattern nested", + ), + ("MatchQ[x, F[x_.,y_]]", "False", "With OneIdentity, and Optional, no default"), + ( + "MatchQ[x, G[x_.,y_]]", + "False", + "Without OneIdentity, and Optional, no default", + ), + ("Default[F, 1]=1.", "1.", None), + ("Default[G, 1]=2.", "2.", None), + ("MatchQ[x, F[x_.,y_]]", "True", "With OneIdentity, and Optional, default"), + ("MatchQ[x, G[x_.,y_]]", "False", "Without OneIdentity, and Optional, default"), + ("MatchQ[F[F[H[y]]],F[x_:0,u_H]]", "False", None), + ("MatchQ[G[G[H[y]]],G[x_:0,u_H]]", "False", None), + ("MatchQ[F[p, F[p, H[y]]],F[x_:0,u_H]]", "False", None), + ("MatchQ[G[p, G[p, H[y]]],G[x_:0,u_H]]", "False", None), + (None, None, None), + ], +) +@skip_or_fail +def test_one_identity_stil_failing(str_expr, str_expected, msg): + check_evaluation( + str_expr, + str_expected, + to_string_expr=True, + to_string_expected=True, + hold_expected=True, + failure_message=msg, + ) + + +@pytest.mark.parametrize( + ("str_expr", "arg_count"), + [ + ("SetAttributes[F]", 1), + ("SetAttributes[]", 0), + ("SetAttributes[F, F, F]", 3), + ], +) +def test_Attributes_wrong_args(str_expr, arg_count): + check_evaluation( + str_expr=str_expr, + str_expected=str_expr, + failure_message=f"Arg count mismatch test with {arg_count} args", + hold_expected=True, + to_string_expr=True, + to_string_expected=True, + expected_messages=( + f"SetAttributes called with {arg_count} arguments; 2 arguments are expected.", + ), + ) diff --git a/test/builtin/test_patterns.py b/test/builtin/test_patterns.py new file mode 100644 index 000000000..9fb3d8a0f --- /dev/null +++ b/test/builtin/test_patterns.py @@ -0,0 +1,28 @@ +# -*- coding: utf-8 -*- +""" +Unit tests from mathics.builtin.patterns. +""" + +from test.helper import check_evaluation + + +def test_blank(): + for str_expr, str_expected, message in ( + ( + "g[i] /. _[i] :> a", + "a", + "Issue #203", + ), + ): + check_evaluation(str_expr, str_expected, message) + + +def test_replace_all(): + for str_expr, str_expected, message in ( + ( + "a == d b + d c /. a_ x_ + a_ y_ -> a (x + y)", + "a == (b + c) d", + "Issue #212", + ), + ): + check_evaluation(str_expr, str_expected, message) diff --git a/test/test_context.py b/test/test_context.py index aec68fc7d..480121de0 100644 --- a/test/test_context.py +++ b/test/test_context.py @@ -1,5 +1,7 @@ # -*- coding: utf-8 -*- from .helper import check_evaluation, reset_session +import pytest + from mathics_scanner.errors import IncompleteSyntaxError @@ -60,37 +62,37 @@ def test_context1(): check_evaluation("bar[]", "42", to_string_expr=False, to_string_expected=False) -def test_context2(): - nomessage = tuple([]) - for expr, expected, lst_messages, msg in [ +@pytest.mark.parametrize( + ("expr", "expected", "lst_messages", "msg"), + [ ( """globalvarY = 37;""", None, - nomessage, + None, "set the value of a global symbol", ), ( """globalvarZ = 37;""", None, - nomessage, + None, "set the value of a global symbol", ), ( """BeginPackage["apackage`"];""", None, - nomessage, + None, "Start a context. Add it to the context path", ), ( """Minus::usage=" usage string setted in the package for Minus";""", None, - nomessage, + None, "set the usage string for a protected symbol ->no error", ), ( """Minus::mymessage=" custom message string for Minus";""", None, - nomessage, + None, "set a custom message for a protected symbol ->no error", ), ( @@ -106,44 +108,44 @@ def test_context2(): ( """X::usage = "package variable";""", None, - nomessage, + None, "set the usage string for a package variable", ), ( """globalvarZ::usage = "a global variable";""", None, - nomessage, + None, "set the usage string for a global symbol", ), ( """globalvarZ = 57;""", None, - nomessage, + None, "reset the value of a global symbol", ), - ("""B = 6;""", None, nomessage, "Set a symbol value in the package context"), + ("""B = 6;""", None, None, "Set a symbol value in the package context"), ( """Begin["`implementation`"];""", None, - nomessage, + None, "Start a context. Do not add it to the context path", ), ( """{Context[A], Context[B], Context[X], Context[globalvarY], Context[globalvarZ]}""", """{"apackage`implementation`", "apackage`", "apackage`", "apackage`implementation`", "apackage`"}""", - nomessage, + None, None, # "context of the variables" ), ( """globalvarY::usage = "a global variable";""", None, - nomessage, + None, "set the usage string for a global symbol", ), ( """globalvarY = 97;""", None, - nomessage, + None, "reset the value of a global symbol", ), ( @@ -159,115 +161,119 @@ def test_context2(): ( """Plus::usage=" usage string setted in the package for Plus";""", None, - nomessage, + None, "set the usage string for a protected symbol ->no error", ), ( """Plus::mymessage=" custom message string for Plus";""", None, - nomessage, + None, "set a custom message for a protected symbol ->no error", ), - ("""A = 7;""", None, nomessage, "Set a symbol value in the context"), - ("""X = 9;""", None, nomessage, "set the value of the package variable"), - ("""End[];""", None, nomessage, "go back to the previous context"), + ("""A = 7;""", None, None, "Set a symbol value in the context"), + ("""X = 9;""", None, None, "set the value of the package variable"), + ("""End[];""", None, None, "go back to the previous context"), ( """{Context[A], Context[B], Context[X], Context[globalvarY], Context[globalvarZ]}""", """{"apackage`", "apackage`", "apackage`", "apackage`", "apackage`"}""", - nomessage, + None, None, # "context of the variables in the package" ), ( """EndPackage[];""", None, - nomessage, + None, "go back to the previous context. Keep the context in the contextpath", ), ( """{Context[A], Context[B], Context[X], Context[globalvarY], Context[globalvarZ]}""", """{"apackage`", "apackage`", "apackage`", "apackage`", "apackage`"}""", - nomessage, + None, None, # "context of the variables at global level" ), - ("""A""", "A", nomessage, "A is not in any context of the context path. "), - ("""B""", "6", nomessage, "B is in a context of the context path"), - ("""Global`globalvarY""", "37", nomessage, ""), + ("""A""", "A", None, "A is not in any context of the context path. "), + ("""B""", "6", None, "B is in a context of the context path"), + ("""Global`globalvarY""", "37", None, ""), ( """Global`globalvarY::usage""", "Global`globalvarY::usage", - nomessage, + None, "In WMA, the value would be set in the package", ), - ("""Global`globalvarZ""", "37", nomessage, "the value set inside the package"), + ("""Global`globalvarZ""", "37", None, "the value set inside the package"), ( """Global`globalvarZ::usage""", "Global`globalvarZ::usage", - nomessage, + None, "not affected by the package", ), - ("""globalvarY""", "apackage`globalvarY", nomessage, ""), + ("""globalvarY""", "apackage`globalvarY", None, ""), ( """globalvarY::usage""", "apackage`globalvarY::usage", - nomessage, + None, "In WMA, the value would be set in the package", ), - ("""globalvarZ""", "57", nomessage, "the value set inside the package"), + ("""globalvarZ""", "57", None, "the value set inside the package"), ( """globalvarZ::usage""", '"a global variable"', - nomessage, + None, "not affected by the package", ), - ("""X""", "9", nomessage, "X is in a context of the context path"), + ("""X""", "9", None, "X is in a context of the context path"), ( """X::usage""", '"package variable"', - nomessage, + None, "X is in a context of the context path", ), ( """apackage`implementation`A""", "7", - nomessage, + None, "get A using its fully qualified name", ), - ("""apackage`B""", "6", nomessage, "get B using its fully qualified name"), + ("""apackage`B""", "6", None, "get B using its fully qualified name"), ( """Plus::usage""", ' " usage string setted in the package for Plus" ', - nomessage, + None, "custom usage for Plus", ), ( """Minus::usage""", '" usage string setted in the package for Minus"', - nomessage, + None, "custom usage for Minus", ), ( """Plus::mymessage""", '" custom message string for Plus"', - nomessage, + None, "custom message for Plus", ), ( """Minus::mymessage""", '" custom message string for Minus"', - nomessage, + None, "custom message for Minus", ), - ]: + (None, None, None, None), + ], +) +def test_context2(expr, expected, lst_messages, msg): + if expr is not None and expected is None: + expected = "System`Null" - if expected is None: - expected = "System`Null" - check_evaluation( - expr, - expected, - failure_message=msg, - to_string_expr=False, - to_string_expected=False, - expected_messages=lst_messages, - hold_expected=True, - ) - reset_session() + if lst_messages is None: + lst_messages = tuple([]) + check_evaluation( + expr, + expected, + failure_message=msg, + to_string_expr=False, + to_string_expected=False, + expected_messages=lst_messages, + hold_expected=True, + )