diff --git a/mathics/builtin/assignments/assignment.py b/mathics/builtin/assignments/assignment.py index d839afab4..cc85e83ca 100644 --- a/mathics/builtin/assignments/assignment.py +++ b/mathics/builtin/assignments/assignment.py @@ -401,6 +401,14 @@ def eval( self, lhs: BaseElement, rhs: BaseElement, evaluation: Evaluation ) -> Symbol: "lhs_ ^:= rhs_" + if isinstance(lhs, Atom): + evaluation.message( + "UpSetDelayed", + "normal", + 1, + Expression(Symbol(self.get_name()), lhs, rhs), + ) + return if isinstance(lhs, Atom): evaluation.message( diff --git a/mathics/core/assignment.py b/mathics/core/assignment.py index e02a8ad99..ae12aa99d 100644 --- a/mathics/core/assignment.py +++ b/mathics/core/assignment.py @@ -202,6 +202,35 @@ def normalize_lhs(lhs, evaluation): return lhs, lookup_name +def pop_reference_head(lhs: Expression, lhs_reference: BaseElement): + """ + Convert expressions of the form + ``` + Head1[Head2[...Headn[ReferenceHead[a,b],p1],p2,...]..] + ``` + into + ``` + ReferenceHead[Head1[Head2[...Headn[a],p1],p2,...]..],b] + ``` + Used in eval_assign_[n|format|...] + """ + if lhs is lhs_reference: + return lhs + + lhs_head = lhs.get_head() + if lhs_head is lhs_reference: + return lhs + + elems = lhs.elements + lhs_reference_expr = elems[0] + if lhs_reference_expr.get_head() is not lhs_reference: + lhs_reference_expr = pop_reference_head(lhs_reference_expr, lhs_reference) + + lhs_reference_elems = lhs_reference_expr.elements + inner = Expression(lhs_head, lhs_reference_elems[0], *elems[1:]) + return Expression(lhs_reference, inner, *lhs_reference_elems[1:]) + + def repl_pattern_by_symbol(expr: BaseElement) -> BaseElement: """ If `expr` is a named pattern expression `Pattern[symb, pat]`, diff --git a/mathics/core/builtin.py b/mathics/core/builtin.py index 8614b90ae..f3ecadc59 100644 --- a/mathics/core/builtin.py +++ b/mathics/core/builtin.py @@ -462,12 +462,17 @@ def get_functions(self, prefix="eval", is_pymodule=False): if pattern is None: # Fixes PyPy bug continue else: - # TODO: consider to use a more sophisticated + # TODO 1: consider to use a more sophisticated # regular expression, which handles breaklines # more properly, that supports format names # with contexts (context`name) and be less # fragile against leaving spaces between the # elements. + # + # TODO 2: allow + # expr: pat + # to allow passing the whole expression instead their elements. + # This requires to change how Format rules are stored... m = re.match( r"[(]([\w,]+),[ ]*[)]\:\s*(.*)", pattern.replace("\n", " ") ) diff --git a/mathics/core/definitions.py b/mathics/core/definitions.py index 982cea574..b5aa0aa7a 100644 --- a/mathics/core/definitions.py +++ b/mathics/core/definitions.py @@ -161,9 +161,8 @@ def __init__( "Global`", ) self.inputfile = "" - - # These are used by TraceEvaluation to``` - # whether what information to show. + # TraceEvaluation uses these to + # decided what information to show self.trace_evaluation = False self.trace_show_rewrite = False self.timing_trace_evaluation = False @@ -872,9 +871,10 @@ def strip_pattern_name_and_condition(pat) -> BaseElement: # We have to use get_head_name() below because # pat can either SymbolCondition or . # In the latter case, comparing to SymbolCondition is not sufficient. - if pat.get_head_name() == "System`Condition": - if len(pat.elements) > 1: - return strip_pattern_name_and_condition(pat.elements[0]) + if pat.has_form(("System`Condition", "System`PatternTest"), 2): + return strip_pattern_name_and_condition(pat.elements[0]) + if pat.has_form("System`HoldPattern", 1): + return strip_pattern_name_and_condition(pat.elements[0]) # The same kind of get_head_name() check is needed here as well and # is not the same as testing against SymbolPattern. if pat.get_head_name() == "System`Pattern": diff --git a/mathics/core/expression.py b/mathics/core/expression.py index a459eab80..c592dff4d 100644 --- a/mathics/core/expression.py +++ b/mathics/core/expression.py @@ -621,19 +621,35 @@ def evaluate( def evaluate_elements(self, evaluation) -> "Expression": """ - return a new expression with the same head, and the - evaluable elements evaluated. + return a new expression with the head and the + evaluable elements evaluated, according to the attributes. """ + head = self._head + if isinstance(head, EvalMixin): + head = head.evaluate(evaluation) or head + attributes = head.get_attributes(evaluation.definitions) + if (A_HOLD_ALL | A_HOLD_ALL_COMPLETE) & attributes: + return Expression(head, *self._elements) + if A_HOLD_REST & attributes: + first, *rest = self._elements + if isinstance(first, EvalMixin): + first = first.evaluate(evaluation) or first + return Expression(head, first, *rest) + elements = [] - for element in self._elements: + for pos, element in enumerate(self._elements): + if pos == 0 and (A_HOLD_FIRST & attributes): + elements.append(element) + continue + if isinstance(element, EvalMixin): result = element.evaluate(evaluation) if result is not None: element = result elements.append(element) - head = self._head - if isinstance(head, Expression): - head = head.evaluate_elements(evaluation) + + # if isinstance(head, Expression): + # head = head.evaluate_elements(evaluation) return Expression(head, *elements) def filter(self, head, cond, evaluation: Evaluation, count: Optional[int] = None): diff --git a/mathics/eval/assignments/assignment.py b/mathics/eval/assignments/assignment.py index 8f48a50e2..2be1f8f96 100644 --- a/mathics/eval/assignments/assignment.py +++ b/mathics/eval/assignments/assignment.py @@ -14,6 +14,7 @@ get_symbol_list, is_protected, normalize_lhs, + pop_reference_head, rejected_because_protected, unroll_conditions, unroll_patterns, @@ -41,7 +42,9 @@ from mathics.core.systemsymbols import ( SymbolCondition, SymbolDefault, + SymbolHoldPattern, SymbolMachinePrecision, + SymbolPatternTest, ) from mathics.eval.list.eol import eval_Part @@ -86,15 +89,46 @@ def eval_assign( True if the assignment was successful. """ - lhs, lookup_name = normalize_lhs(lhs, evaluation) + # An expression can be wrapped inside structures like `Condition[...]` + # or HoldPattern[...]. The `lhs_reference` is the head of the expression once + # we strip out all these wrappings. + lhs_reference_expr = get_reference_expression(lhs) + lhs_reference = ( + lhs_reference_expr + if isinstance(lhs_reference_expr, Symbol) + else lhs_reference_expr.get_head() + ) + if isinstance(lhs_reference_expr, Symbol): + if upset: + evaluation.message(self.get_name(), "nosym", lhs) + if tags and lhs_reference_expr.get_name() not in tags: + evaluation.message("tagnf", lhs_reference_expr, lhs) + + try: + return eval_assign_to_symbol(self, lhs, lhs_reference_expr, rhs, evaluation) + except AssignmentException: + return False + try: - # Using a builtin name, find which assignment procedure to perform, - # and then call that function. + # Handle special cases using the lookup name associated to the lhs_reference + lookup_name = lhs_reference_expr.get_lookup_name() assignment_func = ASSIGNMENT_FUNCTION_MAP.get(lookup_name, None) if assignment_func: - return assignment_func(self, lhs, rhs, evaluation, tags, upset) + return assignment_func( + self, lhs, lhs_reference, rhs, evaluation, tags, upset + ) + if isinstance(lhs, Expression) and not lhs.has_form("System`HoldPattern", 1): + lhs = lhs.evaluate_elements(evaluation) + lhs_reference_expr = get_reference_expression(lhs) + lhs_reference = ( + lhs_reference_expr + if isinstance(lhs_reference_expr, Symbol) + else lhs_reference_expr.get_head() + ) - return eval_assign_store_rules_by_tag(self, lhs, rhs, evaluation, tags, upset) + return eval_assign_store_rules_by_tag( + self, lhs, lhs_reference, rhs, evaluation, tags, upset + ) except AssignmentException: return False @@ -102,6 +136,7 @@ def eval_assign( def eval_assign_attributes( self: Builtin, lhs: BaseElement, + lhs_reference: BaseElement, rhs: BaseElement, evaluation: Evaluation, tags: list, @@ -117,6 +152,10 @@ def eval_assign_attributes( The builtin assignment operator lhs : BaseElement The pattern of the rule to be assigned. + lhs_reference: BaseElement + The head of the expression after `Condition`, + `PatternTest` and `HoldPattern` wrappers are + stripped out. rhs : BaseElement the expression representing the replacement. evaluation : Evaluation @@ -136,11 +175,18 @@ def eval_assign_attributes( True if the assignment was successful. """ - name = lhs.get_head_name() + # UpSet and TagSet for this symbol are handled in + # the standard way. The same if the expression is wrapped: + if lhs.get_head() is not lhs_reference: + return eval_assign_store_rules_by_tag(self, lhs, lhs_reference, rhs, evaluation) + + name = lhs_reference.get_head_name() if len(lhs.elements) != 1: evaluation.message_args(name, len(lhs.elements), 1) raise AssignmentException(lhs, rhs) - tag = lhs.elements[0].get_name() + + tag_expr = get_reference_expression(lhs.elements[0]) + tag = tag_expr.get_lookup_name() if not tag: evaluation.message(name, "sym", lhs.elements[0], 1) raise AssignmentException(lhs, rhs) @@ -179,8 +225,6 @@ def eval_assign_context( lhs: BaseElement, rhs: BaseElement, evaluation: Evaluation, - tags: List, - upset: bool, ) -> bool: """ Process the case where lhs is ``$Context`` @@ -195,10 +239,6 @@ def eval_assign_context( the expression representing the replacement. evaluation : Evaluation DESCRIPTION. - tags : list - the list of symbols to be associated to the rule. - upset : bool - `True` if the rule is an Up value. Raises ------ @@ -242,8 +282,6 @@ def eval_assign_context_path( lhs: BaseElement, rhs: BaseElement, evaluation: Evaluation, - tags: list, - upset: bool, ) -> bool: """ Assignment to the `$ContextPath` variable. @@ -258,10 +296,6 @@ def eval_assign_context_path( the expression representing the replacement. evaluation : Evaluation DESCRIPTION. - tags : list - the list of symbols to be associated to the rule. - upset : bool - `True` if the rule is an Up value. Raises ------ @@ -290,6 +324,7 @@ def eval_assign_context_path( def eval_assign_default( self: Builtin, lhs: BaseElement, + lhs_reference: BaseElement, rhs: BaseElement, evaluation: Evaluation, tags: list, @@ -304,6 +339,9 @@ def eval_assign_default( The builtin assignment operator lhs : BaseElement The pattern of the rule to be assigned. + lhs_reference: BaseElement + The lhs expression stripped from conditions and + wrappers. rhs : BaseElement the expression representing the replacement. evaluation : Evaluation @@ -323,19 +361,24 @@ def eval_assign_default( True if the assignment was successful. """ - lhs, condition = unroll_conditions(lhs) - lhs, rhs = unroll_patterns(lhs, rhs, evaluation) + # UpSet and TagSet for this symbol are handled in + # the standard way. The same if the expression is wrapped: + if lhs.get_head() is not lhs_reference: + return eval_assign_store_rules_by_tag(self, lhs, lhs_reference, rhs, evaluation) + count = 0 defs = evaluation.definitions if len(lhs.elements) not in (1, 2, 3): evaluation.message_args(SymbolDefault, len(lhs.elements), 1, 2, 3) raise AssignmentException(lhs, None) - focus = lhs.elements[0] + lhs_reference = get_reference_expression(lhs.elements[0]) + lhs_reference = ( + lhs_reference if isinstance(lhs_reference, Symbol) else lhs_reference.get_head() + ) tags = process_tags_and_upset_dont_allow_custom( - tags, upset, self, lhs, focus, evaluation + tags, upset, self, lhs, lhs_reference, evaluation ) - lhs = put_back_lhs_conditions(lhs, condition, evaluation) rule = Rule(lhs, rhs) for tag in tags: if rejected_because_protected(self, lhs, tag, evaluation): @@ -348,6 +391,7 @@ def eval_assign_default( def eval_assign_definition_values( self: Builtin, lhs: BaseElement, + lhs_reference: BaseElement, rhs: BaseElement, evaluation: Evaluation, tags: list, @@ -382,6 +426,9 @@ def eval_assign_definition_values( True if the assignment was successful. """ + if lhs.get_head() is not lhs_reference: + return eval_assign_store_rules_by_tag(self, lhs, lhs_reference, rhs, evaluation) + name = lhs.get_head_name() tag = find_tag_and_check(lhs, tags, evaluation) rules = rhs.get_rules_list() @@ -395,6 +442,7 @@ def eval_assign_definition_values( def eval_assign_format( self: Builtin, lhs: BaseElement, + lhs_reference: BaseElement, rhs: BaseElement, evaluation: Evaluation, tags: list, @@ -429,8 +477,8 @@ def eval_assign_format( True if the assignment was successful. """ - lhs, condition = unroll_conditions(lhs) - lhs, rhs = unroll_patterns(lhs, rhs, evaluation) + lhs = pop_reference_head(lhs, lhs_reference) + lhs = lhs.evaluate_elements(evaluation) count = 0 defs = evaluation.definitions @@ -456,11 +504,16 @@ def eval_assign_format( "System`TeXForm", "System`MathMLForm", ] - lhs = focus = lhs.elements[0] + lhs = lhs.elements[0] + lhs_reference = get_reference_expression(lhs) + lhs_reference = ( + lhs_reference.get_head() + if isinstance(lhs_reference, Expression) + else lhs_reference + ) tags = process_tags_and_upset_dont_allow_custom( - tags, upset, self, lhs, focus, evaluation + tags, upset, self, lhs, lhs_reference, evaluation ) - lhs = put_back_lhs_conditions(lhs, condition, evaluation) rule = Rule(lhs, rhs) for tag in tags: if rejected_because_protected(self, lhs, tag, evaluation): @@ -471,7 +524,7 @@ def eval_assign_format( def eval_assign_iteration_limit( - lhs: BaseElement, rhs: BaseElement, evaluation: Evaluation + self, lhs: BaseElement, rhs: BaseElement, evaluation: Evaluation ) -> bool: """ Set ownvalue for the $IterationLimit symbol. @@ -491,8 +544,6 @@ def eval_assign_line_number_and_history_length( lhs: BaseElement, rhs: BaseElement, evaluation: Evaluation, - tags: list, - upset: bool, ) -> bool: """ Set ownvalue for the $Line and $HistoryLength symbols. @@ -507,10 +558,6 @@ def eval_assign_line_number_and_history_length( the expression representing the replacement. evaluation : Evaluation DESCRIPTION. - tags : list - the list of symbols to be associated to the rule. - upset : bool - `True` if the rule is an Up value. Raises ------ @@ -534,6 +581,7 @@ def eval_assign_line_number_and_history_length( def eval_assign_list( self: Builtin, lhs: BaseElement, + lhs_reference: BaseElement, rhs: BaseElement, evaluation: Evaluation, tags: list, @@ -582,6 +630,7 @@ def eval_assign_list( def eval_assign_makeboxes( self: Builtin, lhs: BaseElement, + lhs_reference: BaseElement, rhs: BaseElement, evaluation: Evaluation, tags: list, @@ -636,8 +685,6 @@ def eval_assign_minprecision( lhs: BaseElement, rhs: BaseElement, evaluation: Evaluation, - tags: list, - upset: bool, ) -> bool: """ Implement the assignment to the `$MinPrecision` symbol. @@ -652,10 +699,6 @@ def eval_assign_minprecision( the expression representing the replacement. evaluation : Evaluation DESCRIPTION. - tags : list - the list of symbols to be associated to the rule. - upset : bool - `True` if the rule is an Up value. Raises ------ @@ -686,8 +729,6 @@ def eval_assign_maxprecision( lhs: BaseElement, rhs: BaseElement, evaluation: Evaluation, - tags: list, - upset: bool, ) -> bool: """ Implement the assignment to the `$MaxPrecision` symbol. @@ -702,10 +743,6 @@ def eval_assign_maxprecision( the expression representing the replacement. evaluation : Evaluation DESCRIPTION. - tags : list - the list of symbols to be associated to the rule. - upset : bool - `True` if the rule is an Up value. Raises ------ @@ -735,6 +772,7 @@ def eval_assign_maxprecision( def eval_assign_messagename( self: Builtin, lhs: BaseElement, + lhs_reference: BaseElement, rhs: BaseElement, evaluation: Evaluation, tags: list, @@ -769,18 +807,20 @@ def eval_assign_messagename( True if the assignment was successful. """ - lhs, condition = unroll_conditions(lhs) - lhs, rhs = unroll_patterns(lhs, rhs, evaluation) + if lhs.get_head() is not lhs_reference: + return eval_assign_store_rules_by_tag(self, lhs, lhs_reference, rhs, evaluation) + + lhs = pop_reference_head(lhs, lhs_reference) + count = 0 defs = evaluation.definitions if len(lhs.elements) != 2: evaluation.message_args("MessageName", len(lhs.elements), 2) raise AssignmentException(lhs, None) - focus = lhs.elements[0] + lhs_reference = lhs.elements[0] tags = process_tags_and_upset_dont_allow_custom( - tags, upset, self, lhs, focus, evaluation + tags, upset, self, lhs, lhs_reference, evaluation ) - lhs = put_back_lhs_conditions(lhs, condition, evaluation) rule = Rule(lhs, rhs) for tag in tags: # Messages can be assigned even if the symbol is protected... @@ -792,7 +832,7 @@ def eval_assign_messagename( def eval_assign_module_number( - lhs: BaseElement, rhs: BaseElement, evaluation: Evaluation + self, lhs: BaseElement, rhs: BaseElement, evaluation: Evaluation ) -> bool: """ Set ownvalue for the $ModuleNumber symbol. @@ -807,6 +847,7 @@ def eval_assign_module_number( def eval_assign_options( self: Builtin, lhs: BaseElement, + lhs_reference: BaseElement, rhs: BaseElement, evaluation: Evaluation, tags: list, @@ -840,6 +881,9 @@ def eval_assign_options( True if the assignment was successful. """ + if lhs.get_head() is not lhs_reference: + return eval_assign_store_rules_by_tag(self, lhs, lhs_reference, rhs, evaluation) + lhs_elements = lhs.elements name = lhs.get_head_name() if len(lhs_elements) != 1: @@ -866,6 +910,7 @@ def eval_assign_options( def eval_assign_numericq( self: Builtin, lhs: BaseElement, + lhs_reference: BaseElement, rhs: BaseElement, evaluation: Evaluation, tags: list, @@ -899,8 +944,8 @@ def eval_assign_numericq( True if the assignment was successful. """ - # lhs, condition = unroll_conditions(lhs) - lhs, rhs = unroll_patterns(lhs, rhs, evaluation) + lhs = pop_reference_head(lhs, lhs_reference) + if rhs not in (SymbolTrue, SymbolFalse): evaluation.message("NumericQ", "set", lhs, rhs) # raise AssignmentException(lhs, rhs) @@ -927,6 +972,7 @@ def eval_assign_numericq( def eval_assign_n( self: Builtin, lhs: BaseElement, + lhs_reference: BaseElement, rhs: BaseElement, evaluation: Evaluation, tags: list, @@ -941,6 +987,8 @@ def eval_assign_n( The builtin assignment operator lhs : BaseElement The pattern of the rule to be assigned. + lhs_reference: BaseElement + Expression of the form N[___] rhs : BaseElement the expression representing the replacement. evaluation : Evaluation @@ -960,27 +1008,28 @@ def eval_assign_n( True if the assignment was successful. """ - lhs, condition = unroll_conditions(lhs) - lhs, rhs = unroll_patterns(lhs, rhs, evaluation) + if isinstance(lhs, Expression): + lhs = lhs.evaluate_elements(evaluation) + + lhs = pop_reference_head(lhs, lhs_reference) defs = evaluation.definitions - # If we try to set `N=4`, (issue #210) just deal with it as with a generic expression: - if lhs is SymbolN: - return eval_assign_store_rules_by_tag(self, lhs, rhs, evaluation, tags, upset) if len(lhs.elements) not in (1, 2): evaluation.message_args("N", len(lhs.elements), 1, 2) raise AssignmentException(lhs, None) + if len(lhs.elements) == 1: nprec = SymbolMachinePrecision + lhs = Expression(SymbolN, lhs.elements[0], nprec) else: nprec = lhs.elements[1] - focus = lhs.elements[0] - lhs = Expression(SymbolN, focus, nprec) + + lhs_reference = get_reference_expression(lhs.elements[0]) + tags = process_tags_and_upset_dont_allow_custom( - tags, upset, self, lhs, focus, evaluation + tags, upset, self, lhs, lhs_reference, evaluation ) count = 0 - lhs = put_back_lhs_conditions(lhs, condition, evaluation) rule = Rule(lhs, rhs) for tag in tags: if rejected_because_protected(self, lhs, tag, evaluation): @@ -990,76 +1039,10 @@ def eval_assign_n( return count > 0 -def eval_assign_other( - self: Builtin, - lhs: BaseElement, - rhs: BaseElement, - evaluation: Evaluation, - tags: Optional[list] = None, - upset: bool = 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 account. - Otherwise, the value is False. - - - Parameters - ---------- - self : Builtin - The builtin assignment operator - lhs : BaseElement - The pattern of the rule to be assigned. - rhs : BaseElement - the expression representing the replacement. - evaluation : Evaluation - DESCRIPTION. - tags : list - the list of symbols to be associated to the rule. - upset : bool - `True` if the rule is an Up value. - - Raises - ------ - AssignmentException - - Returns - ------- - bool - True if the assignment was successful. - - """ - tags, _ = process_tags_and_upset_allow_custom( - tags, upset, self, lhs, rhs, evaluation - ) - lhs_name = lhs.get_name() - if lhs_name == "System`$RecursionLimit": - eval_assign_recursion_limit(lhs, rhs, evaluation) - elif lhs_name in ("System`$Line", "System`$HistoryLength"): - eval_assign_line_number_and_history_length( - self, lhs, rhs, evaluation, tags, upset - ) - elif lhs_name == "System`$IterationLimit": - eval_assign_iteration_limit(lhs, rhs, evaluation) - elif lhs_name == "System`$ModuleNumber": - eval_assign_module_number(lhs, rhs, evaluation) - elif lhs_name == "System`$MinPrecision": - eval_assign_minprecision(self, lhs, rhs, evaluation, tags, upset) - elif lhs_name == "System`$MaxPrecision": - eval_assign_maxprecision(self, lhs, rhs, evaluation, tags, upset) - else: - return False, tags - return True, tags - - def eval_assign_part( self: Builtin, lhs: BaseElement, + lhs_reference: BaseElement, rhs: BaseElement, evaluation: Evaluation, tags: Optional[List], @@ -1119,8 +1102,6 @@ def eval_assign_random_state( lhs: BaseElement, rhs: BaseElement, evaluation: Evaluation, - tags: list, - upset: bool, ) -> bool: """ Assign to expressions of the form `$RandomState`. @@ -1135,10 +1116,6 @@ def eval_assign_random_state( the expression representing the replacement. evaluation : Evaluation DESCRIPTION. - tags : list - the list of symbols to be associated to the rule. - upset : bool - `True` if the rule is an Up value. Raises ------ @@ -1170,7 +1147,7 @@ def eval_assign_random_state( return False -def eval_assign_recursion_limit(lhs, rhs, evaluation): +def eval_assign_recursion_limit(self, lhs, rhs, evaluation): """ Set ownvalue for the $RecursionLimit symbol. """ @@ -1191,7 +1168,7 @@ def eval_assign_recursion_limit(lhs, rhs, evaluation): def eval_assign_store_rules_by_tag( - self, lhs, rhs, evaluation, tags, upset=False + self, lhs, lhs_reference, rhs, evaluation, tags, upset=False ) -> bool: """ This is the default assignment. Stores a rule of the form lhs->rhs @@ -1226,25 +1203,62 @@ def eval_assign_store_rules_by_tag( True if the assignment was successful. """ - lhs, condition = unroll_conditions(lhs) - lhs, rhs = unroll_patterns(lhs, rhs, evaluation) defs = evaluation.definitions - ignore_protection, tags = eval_assign_other(self, lhs, rhs, evaluation, tags, upset) + tags, lhs_reference_expr = process_tags_and_upset_allow_custom( + tags, upset, self, lhs, rhs, evaluation + ) # In WMA, this does not happens. However, if we remove this, # some combinatorica tests fail. # Also, should not be at the beginning? - lhs = put_back_lhs_conditions(lhs, condition, evaluation) count = 0 rule = Rule(lhs, rhs) position = "upvalues" if upset else None for tag in tags: - if rejected_because_protected(self, lhs, tag, evaluation, ignore_protection): + if rejected_because_protected(self, lhs, tag, evaluation, False): continue count += 1 defs.add_rule(tag, rule, position=position) return count > 0 +def eval_assign_to_symbol( + self, + lhs: BaseElement, + lhs_reference: BaseElement, + rhs: BaseElement, + evaluation: Evaluation, +) -> bool: + """ + self: + The builtin class. + lhs : BaseElement + The pattern of the rule to be included. + lhs_reference: + The symbol to be assigned + rhs : BaseElement. + the RHS. + evaluation : Evaluation + The evaluation object. + + """ + # This is how WMA works: if the LHS is a symbol, do the special + # evaluation. So, HoldPattern[$RecursionLimit]:=10 set the + # ownvalue of $RecursionLimit to 10, but in evaluations, $RecursionLimit + # is not modified. + special_fn = EVAL_ASSIGN_SPECIAL_SYMBOLS.get(lhs.get_name(), None) + if special_fn: + ignore_protection = True + special_fn(self, lhs, rhs, evaluation) + else: + ignore_protection = False + + tag = lhs_reference.get_name() + if rejected_because_protected(self, lhs, tag, evaluation, ignore_protection): + return False + evaluation.definitions.add_rule(tag, Rule(lhs, rhs), position="ownvalues") + return True + + def find_tag_and_check( lhs: BaseElement, tags: Optional[List[str]], evaluation: Evaluation ) -> str: @@ -1288,32 +1302,47 @@ def find_tag_and_check( return tag -def put_back_lhs_conditions( - lhs: BaseElement, condition: Expression, evaluation: Evaluation -) -> BaseElement: +def get_reference_expression(lhs: BaseElement) -> BaseElement: """ - Add back the lhs conditions. + Strip `Condition`, `PatternTest` and `HoldPattern` from an expression """ - # To Handle `OptionValue` in `Condition` - rulopc = build_rulopc(lhs.get_head()) - # Now, let's add the conditions on the LHS - if condition: - lhs = Expression( - SymbolCondition, - lhs, - condition.elements[1].do_apply_rules([rulopc], evaluation)[0], - ) + strip_headers = ( + SymbolHoldPattern, + SymbolCondition, + SymbolPatternTest, + ) + # If atom, just return + if not hasattr(lhs, "elements"): + return lhs + + lhs_head = lhs.get_head() + # If the head is wrapped, strip it + + if lhs_head.get_head() in strip_headers: + lhs = Expression(get_reference_expression(lhs_head), *lhs.elements) + lhs_head = lhs.get_head() + + while lhs_head in strip_headers: + lhs = lhs.elements[0] + if not hasattr(lhs, "elements"): + return lhs + lhs_head = lhs.get_head() + if lhs_head.get_head() in strip_headers: + lhs = Expression(get_reference_expression(lhs_head), *lhs.elements) + + lhs_head = lhs.get_head() + return lhs -def process_tags_and_upset_dont_allow_custom( - tags: Optional[list], +def process_tags_and_upset_allow_custom( + tags: Optional[List], upset: bool, self: Builtin, lhs: BaseElement, - focus: BaseElement, + rhs: BaseElement, evaluation: Evaluation, -) -> list: +) -> Tuple[list, BaseElement]: """ If `upset` is `True`, collect a list of tag candidates from the elements of the lhs. @@ -1338,42 +1367,70 @@ def process_tags_and_upset_dont_allow_custom( Raises ------ - AssignmentException + AssignmentException. Returns ------- - tags: list - the list of allowed tags. + (tags, lhs_reference,): Tuple[list, BaseElement] + tags: the list of symbols to which the rule must be associated. + lhs_reference: the lhs """ - if isinstance(focus, Expression): - focus = focus.evaluate_elements(evaluation) name = lhs.get_head_name() + lhs_reference_expr = get_reference_expression(lhs) + + def get_lookup_name(expr): + expr = get_reference_expression(expr) + if expr.has_form("System`Pattern", 2): + return expr.elements[1].get_lookup_name() + if expr.has_form( + ("System`Blank", "System`BlankSequence", "System`BlankNullSequence"), 1 + ): + return expr.elements[0].get_lookup_name() + return expr.get_lookup_name() + if upset: - tags = [focus.get_lookup_name()] - elif tags is None: - name = focus.get_lookup_name() + tags = [] + if isinstance(lhs_reference_expr, Atom): + symbol_name = self.get_name() + evaluation.message( + symbol_name, + "normal", + Integer1, + Expression(Symbol(symbol_name), lhs, rhs), + ) + raise AssignmentException(lhs, None) + for element in lhs_reference_expr.get_elements(): + name = get_lookup_name(element) + tags.append(name) + return tags, lhs_reference_expr + + if tags is None: + name = get_lookup_name(lhs_reference_expr) if not name: - evaluation.message(self.get_name(), "setraw", focus) + evaluation.message(self.get_name(), "setraw", lhs_reference_expr) raise AssignmentException(lhs, None) tags = [name] else: - allowed_names = [focus.get_lookup_name()] + allowed_names = [get_lookup_name(lhs_reference_expr)] + for element in lhs_reference_expr.get_elements(): + allowed_names.append(get_lookup_name(element)) for name in tags: if name not in allowed_names: evaluation.message(self.get_name(), "tagnfd", Symbol(name)) raise AssignmentException(lhs, None) - return tags + return tags, lhs_reference_expr -def process_tags_and_upset_allow_custom( - tags: Optional[List], + +def process_tags_and_upset_dont_allow_custom( + tags: Optional[list], upset: bool, self: Builtin, lhs: BaseElement, - rhs: BaseElement, + lhs_reference: BaseElement, evaluation: Evaluation, -) -> Tuple[list, BaseElement]: +) -> list: """ If `upset` is `True`, collect a list of tag candidates from the elements of the lhs. @@ -1398,75 +1455,36 @@ def process_tags_and_upset_allow_custom( Raises ------ - AssignmentException. + AssignmentException Returns ------- - (tags, focus,): Tuple[list, BaseElement] - tags: the list of symbols to which the rule must be associated. - focus: the lhs + tags: list + the list of allowed tags. """ + if isinstance(lhs_reference, Expression): + lhs_reference = lhs_reference.evaluate_elements(evaluation) name = lhs.get_head_name() - focus = lhs - if isinstance(focus, Expression): - focus = focus.evaluate_elements(evaluation) - if upset: - tags = [] - if isinstance(focus, Atom): - symbol_name = self.get_name() - evaluation.message( - symbol_name, - "normal", - Integer1, - Expression(Symbol(symbol_name), lhs, rhs), - ) - raise AssignmentException(lhs, None) - for element in focus.get_elements(): - name = element.get_lookup_name() - tags.append(name) - return tags, focus - - if tags is None: - name = focus.get_lookup_name() + tags = [lhs_reference.get_lookup_name()] + elif tags is None: + name = lhs_reference.get_lookup_name() if not name: - evaluation.message(self.get_name(), "setraw", focus) + evaluation.message(self.get_name(), "setraw", lhs_reference) raise AssignmentException(lhs, None) tags = [name] else: - allowed_names = [focus.get_lookup_name()] - for element in focus.get_elements(): - if not isinstance(element, Symbol) and element.get_head_name() in ( - "System`HoldPattern", - ): - element = element.get_element(0) - if not isinstance(element, Symbol) and element.get_head_name() in ( - "System`Pattern", - ): - element = element.get_element(1) - if not isinstance(element, Symbol) and element.get_head_name() in ( - "System`Blank", - "System`BlankSequence", - "System`BlankNullSequence", - ): - if len(element.get_elements()) == 1: - element = element.get_element(0) - - allowed_names.append(element.get_lookup_name()) + allowed_names = [lhs_reference.get_lookup_name()] for name in tags: if name not in allowed_names: evaluation.message(self.get_name(), "tagnfd", Symbol(name)) raise AssignmentException(lhs, None) - - return tags, focus + return tags # Below is a mapping from Symbol name (as a string) into an assignment eval function. ASSIGNMENT_FUNCTION_MAP = { - "System`$Context": eval_assign_context, - "System`$ContextPath": eval_assign_context_path, - "System`$RandomState": eval_assign_random_state, "System`Attributes": eval_assign_attributes, "System`Default": eval_assign_default, "System`DefaultValues": eval_assign_definition_values, @@ -1485,3 +1503,17 @@ def process_tags_and_upset_allow_custom( "System`SubValues": eval_assign_definition_values, "System`UpValues": eval_assign_definition_values, } + + +EVAL_ASSIGN_SPECIAL_SYMBOLS = { + "System`$Context": eval_assign_context, + "System`$ContextPath": eval_assign_context_path, + "System`$HistoryLength": eval_assign_line_number_and_history_length, + "System`$IterationLimit": eval_assign_iteration_limit, + "System`$Line": eval_assign_line_number_and_history_length, + "System`$MaxPrecision": eval_assign_maxprecision, + "System`$MinPrecision": eval_assign_minprecision, + "System`$ModuleNumber": eval_assign_module_number, + "System`$RandomState": eval_assign_random_state, + "System`$RecursionLimit": eval_assign_recursion_limit, +} diff --git a/test/builtin/assignments/test_assign_binaryop.py b/test/builtin/assignments/test_assign_binaryop.py index a02a5e31a..ff114c345 100644 --- a/test/builtin/assignments/test_assign_binaryop.py +++ b/test/builtin/assignments/test_assign_binaryop.py @@ -2,6 +2,8 @@ """ Unit tests for mathics.builtins.assignments.assign_binaryop """ +# TODO: reconsider the name of the test and module, because all the test +# involve unary operators... from test.helper import check_evaluation, session diff --git a/test/builtin/assignments/test_assignment.py b/test/builtin/assignments/test_assignment.py index b689ddbeb..941607ac5 100644 --- a/test/builtin/assignments/test_assignment.py +++ b/test/builtin/assignments/test_assignment.py @@ -1,20 +1,18 @@ # -*- coding: utf-8 -*- """ Unit tests for mathics.builtins.assignments.assignment + +Tests here check the compatibility of +the default behavior of the different assignment operators +with WMA. """ -import os +# TODO: consider to split this module in sub-modules. + from test.helper import check_evaluation, session import pytest 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 - def test_upset(): """ @@ -42,7 +40,7 @@ def test_order(): ) -str_test_set_with_oneidentity = """ +STR_TEST_SET_WITH_ONE_IDENTITY = """ SetAttributes[SUNIndex, {OneIdentity}]; SetAttributes[SUNFIndex, {OneIdentity}]; @@ -66,7 +64,7 @@ def test_setdelayed_oneidentity(): symbols with the attribute OneIdentity. """ expr = "" - for line in str_test_set_with_oneidentity.split("\n"): + for line in STR_TEST_SET_WITH_ONE_IDENTITY.split("\n"): if line in ("", "\n"): continue expr = expr + line @@ -298,15 +296,15 @@ def test_set_and_clear(str_expr, str_expected, msg): ), (None, None, None), ( - "F[x_]:=G[x]; H[F[y_]]^:=Q[y]; ClearAll[G]; {H[G[5]],H[F[5]]}", + "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];" + "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]}", @@ -314,13 +312,12 @@ def test_set_and_clear(str_expr, str_expected, msg): ), (None, None, None), ( - "F[x_]:=G[x];N[F[x_]]:=x^2;ClearAll[F];{N[F[2]],N[G[2]]}", + "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 @@ -465,3 +462,247 @@ def test_private_doctests(str_expr, str_expected, msgs, failure_msg): check_evaluation( str_expr, str_expected, expected_messages=msgs, failure_message=failure_msg ) + + +@pytest.mark.parametrize( + ["expr", "expect", "fail_msg", "expected_msgs"], + [ + (None, None, None, None), + # Trivial cases on protected symbols + ( + "List:=1;", + None, + "assign to protected element", + ("Symbol List is Protected.",), + ), + ( + "HoldPattern[List]:=1;", + None, + "assign to wrapped protected element", + ("Tag List in HoldPattern[List] is Protected.",), + ), + ( + "PatternTest[List, x]:=1;", + None, + "assign to wrapped protected element", + ("Tag List in List ? x is Protected.",), + ), + ( + "Condition[List, x]:=1;", + None, + "assign to wrapped protected element", + ("Tag List in List /; x is Protected.",), + ), + # Behavior with symbols in the LHS + ("ClearAll[A,T];A=T; T=2; {A, T}", "{2, 2}", "Assignment to symbols", None), + ( + "ClearAll[A,T];A=T; A=2; Clear[A]; A=3; {A, T}", + "{3, T}", + "Assignment to symbols. Rewrite value.", + None, + ), + ( + "ClearAll[A,T];A=T; A[x_]=x^2; {A[u], T[u]}", + "{u^2, u^2}", + "Assignment to symbols.", + None, + ), + ( + "ClearAll[A,T];A=T; A[x_]=x^2; ClearAll[A]; {A[u], T[u]}", + "{A[u], u^2}", + ( + "Rules are associated to T, not A, " + "because the LHS is evaluated before the assignment." + ), + None, + ), + ( + "ClearAll[A, T]; A=T; HoldPattern[A[x_]]=x^2; {A[u], T[u]}", + "{T[u], T[u]}", + "Hold Pattern prevents the evaluation of the LHS. The ownvalue comes first...", + None, + ), + ( + "ClearAll[A, T]; A=T; HoldPattern[A[x_]]=x^2; A=.; {A[u], T[u]}", + "{u^2, T[u]}", + "Hold Pattern prevents the evaluation of the LHS. Removing the ownvalue.", + None, + ), + # HoldPattern on the LHS + ( + "ClearAll[A,T];A=T; HoldPattern[T]=2; {2, 2}", + "{2, 2}", + "Assignment to symbols", + None, + ), + ( + "ClearAll[A,T];A=T; HoldPattern[A]=2; {2, T}", + "{2, T}", + "Assignment to symbols. Rewrite value.", + None, + ), + ( + "ClearAll[A,T];A=T; HoldPattern[A[x_]]:=x^2; {A[u], T[u]}", + "{T[u], T[u]}", + "Assignment to symbols.", + None, + ), + ( + "ClearAll[A,T];A=T; HoldPattern[A][x_]:=x^2; {A[u], T[u]}", + "{T[u], T[u]}", + "Assignment to symbols.", + None, + ), + ( + "ClearAll[A,T];A=T; HoldPattern[A[x_]]:=x^2;A=.; {A[u], T[u]}", + "{u ^ 2, T[u]}", + "Once the downvalue of A is gone, the rule applies...", + None, + ), + # In this case, we erase all the rules associated to A: + ( + "ClearAll[A, T]; A=T; HoldPattern[A[x_]]:=x^2; ClearAll[A]; {A[u], T[u]}", + "{A[u], T[u]}", + "Head and elements on the LHS are evaluated before the assignment.", + None, + ), + ( + "ClearAll[A,T];A=T; HoldPattern[HoldPattern[A[x_]]]:=x^2;A=.; {A[u], T[u]}", + "{u ^ 2, T[u]}", + "Nested HoldPattern", + None, + ), + # Conditions on the LHS + ( + "ClearAll[A,T,x];A=T;x=3; Condition[T,x>2]=2; {2, 2}", + "{2, 2}", + "Assignment to symbols", + None, + ), + ( + "ClearAll[A,T,x];A=T;x=3; Condition[A, x>2]=2; {2, T}", + "{2, T}", + "Assignment to symbols. Rewrite value.", + None, + ), + ( + "ClearAll[A,T,x];A=T;x=3; Condition[A[x_],x>2]:=x^2; {A[u], T[u], A[4], T[4]}", + "{A[u], T[u], 16, 16}", + "Assignment to symbols.", + None, + ), + ( + "ClearAll[A,T,x];A=T;x=3; Condition[A[x_],x>2]:=x^2;A=.; {A[u], T[u], A[4], T[4]}", + "{A[u], T[u], A[4], 16}", + "Assignment to symbols.", + None, + ), + ( + "ClearAll[A,T,x];A=T;x=3; Condition[A[x_],x>2]:=x^2; ClearAll[A]; {A[u], T[u]}", + "{A[u], T[u]}", + ( + "Head and elements on the LHS are evaluated before the assignment, but noticing that " + "Condition has the attribute `HoldRest`..." + ), + None, + ), + ( + "ClearAll[A, T]; A=T; HoldPattern[A[x_]]:=x^2; {A[u], T[u]}", + "{T[u], T[u]}", + "Hold Pattern prevents the evaluation of the LHS.", + None, + ), + ( + "ClearAll[A, T]; A=T; HoldPattern[A[x_]]:=x^2;A=.; {A[u], T[u]}", + "{u^2, T[u]}", + "Hold Pattern prevents the evaluation of the LHS.", + None, + ), + # Format + ( + 'ClearAll[A,T,x]; Format[A[x_]]:={x,"a"}; A[2]//ToString', + '"{2, a}"', + None, + None, + ), + ( + 'ClearAll[A,T,x]; A=T; Format[A[x_]]:={x,"a"}; T[2]//ToString', + '"{2, a}"', + "Define the format for T", + None, + ), + ( + 'ClearAll[A,T,x]; A=T; Format[A[x_]]:={x,"a"}; A=.;A[2]//ToString', + '"A[2]"', + "but not for A", + None, + ), + # Now, using HoldPattern + ( + 'ClearAll[A,T,x]; A=T; Format[HoldPattern[A][x_]]:={x,"a"}; T[2]//ToString', + '"T[2]"', + ("Define the format for A, " "because the HoldPattern. Do not affect T"), + None, + ), + ( + 'ClearAll[A,T,x]; A=T; Format[HoldPattern[A][x_]]:={x,"a"}; A[2]//ToString', + '"T[2]"', + "but A evals to T befor format...", + None, + ), + ( + 'ClearAll[A,T,x]; A=T; Format[HoldPattern[A][x_]]:={x,"a"}; A=.; A[2]//ToString', + '"{2, a}"', + "Now A do not eval to T...", + None, + ), + ( + 'ClearAll[A,T,x]; A=T; HoldPattern[Format[A[x_]]]:={x,"a"}; A=.; A[2]//ToString', + '"{2, a}"', + "The same that put HoldPattern inside format...", + None, + ), + # Conditionals + ( + 'ClearAll[A,T,x]; A=T; Format[Condition[A[x_],x>0]]:={x,"a"}; A=.; A[2]//ToString', + '"A[2]"', + "store the conditional rule for T...", + None, + ), + ( + 'ClearAll[A,T,x]; A=T; Format[Condition[A[x_],x>0]]:={x,"a"}; A=.; T[2]//ToString', + '"{2, a}"', + "store the conditional rule for T...", + None, + ), + # Upvalues + ( + "ClearAll[F,A,Y,x]; A=T; F[A[x_],Y[x_]]^:=x^2; ClearAll[A,F,Y]; F[T[2],Y[2]]", + "4", + "the rule is still stored in T.", + None, + ), + ( + "ClearAll[F,A,Y,x]; A=T; F[HoldPattern[A[x_]],Y[x_]]^:=x^2; ClearAll[A,F,Y]; F[T[2],Y[2]]", + "F[T[2],Y[2]]", + "the rule is still stored in T.", + None, + ), + ( + "ClearAll[F,A,Y,x]; A=T; F[HoldPattern[A[x_]],Y[x_]]^:=x^2; ClearAll[A,F]; F[A[2],Y[2]]", + "4", + "the rule is still stored in Y.", + None, + ), + ( + "ClearAll[F,A,Y,x]; A=T; F[{a,b,c},Y[x_]]^:=x^2; ClearAll[A,F]; F[{a,b,c},Y[2]]", + "4", + "There is a warning, because a rule cannot be associated to List, but it is stored on Y.", + ("Tag List in F[{a, b, c}, Y[x_]] is Protected.",), + ), + ], +) +def test_assignment(expr, expect, fail_msg, expected_msgs): + check_evaluation( + expr, expect, failure_message=fail_msg, expected_messages=expected_msgs + ) diff --git a/test/core/test_assignment.py b/test/core/test_assignment.py new file mode 100644 index 000000000..063760fa9 --- /dev/null +++ b/test/core/test_assignment.py @@ -0,0 +1,96 @@ +# -*- coding: utf-8 -*- + +from test.helper import session + +import pytest + +from mathics.core.assignment import pop_reference_head +from mathics.core.symbols import Symbol +from mathics.eval.assignments.assignment import get_reference_expression + +evaluation = session.evaluation + + +@pytest.mark.parametrize( + ("expr_str", "expected_str"), + [ + ( + "A", + "A", + ), + ( + "A[x]", + "A[x]", + ), + ( + "HoldPattern[A[x]]", + "A[x]", + ), + ( + "HoldPattern[A][x]", + "A[x]", + ), + ( + "Condition[A[x],3]", + "A[x]", + ), + ( + "HoldPattern[Condition[A[x],3]]", + "A[x]", + ), + ( + "Condition[HoldPattern[A][x],3]", + "A[x]", + ), + ], +) +def test_get_reference_expression(expr_str, expected_str): + expr = evaluation.parse(expr_str) + result = get_reference_expression(expr) + expected = evaluation.parse(expected_str) + assert str(result) == str(expected) + + +@pytest.mark.parametrize( + ("expr_str", "lhs_ref_str", "expected_str"), + [ + ( + "A", + "A", + "A", + ), + ( + "A[x]", + "A", + "A[x]", + ), + ( + "A[B[x],y]", + "A", + "A[B[x],y]", + ), + ( + "B[A[x,y],z]", + "A", + "A[B[x,z],y]", + ), + ( + "F[B[A[x,y],z]]", + "A", + "A[F[B[x,z]],y]", + ), + ( + "F[B[A[x,y],z],t]", + "A", + "A[F[B[x,z],t],y]", + ), + ], +) +def test_pop_reference_head(expr_str, lhs_ref_str, expected_str): + lhs_ref = evaluation.parse(lhs_ref_str) + expr = evaluation.parse(expr_str) + print("reference:", lhs_ref) + print("expr:", expr) + result = pop_reference_head(expr, lhs_ref) + expected = evaluation.parse(expected_str) + assert str(result) == str(expected)