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/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/internals.py b/mathics/builtin/assignments/internals.py index 132d9e390..371d057bd 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 @@ -10,6 +12,7 @@ from mathics.core.symbols import ( Symbol, SymbolFalse, + SymbolList, SymbolMinPrecision, SymbolMaxPrecision, SymbolN, @@ -23,8 +26,15 @@ SymbolHoldPattern, SymbolMachinePrecision, SymbolOptionValue, + SymbolPart, SymbolPattern, SymbolRuleDelayed, + SymbolSet, + SymbolSetDelayed, + SymbolTagSet, + SymbolTagSetDelayed, + SymbolUpSet, + SymbolUpSetDelayed, ) @@ -33,6 +43,61 @@ from functools import reduce +# In Set* operators, the default behavior is that the +# elements of the LHS are evaluated before the assignment. +# So, if we define +# +# F[x_]:=G[x] +# +# and then +# +# M[F[x_]]:=x^2 +# +# The rule that is stored is +# +# M[G[x_]]->x^2 +# +# +# This behaviour does not aplies to a reduces subset of expressions, like +# in +# +# A={1,2,3} +# Part[A,1]:=s +# +# in a way that the result of the second line is to change a part of `A` +# A->{s, 2, 3} +# +# instead of trying to assign 1:=s +# +# Something similar happens with the Set* expressions. For example, +# the expected behavior of +# +# Set[F[x_],rhs_]:=Print["Do not set to F"] +# +# is not to forbid assignments to `G`, but to F: +# +# G[x_]:=x^4 +# still set the rule G[x_]->x^2 +# +# while +# +# F[x_]:=x^4 +# just will print the warning "Do not set to F". +# +# +NOT_EVALUATE_ELEMENTS_IN_ASSIGNMENTS = ( + SymbolSet, + SymbolSetDelayed, + SymbolUpSet, + SymbolUpSetDelayed, + SymbolTagSet, + SymbolTagSetDelayed, + SymbolList, + SymbolPart, + Symbol("System`MessageName"), +) + + class AssignmentException(Exception): def __init__(self, lhs, rhs) -> None: super().__init__(" %s cannot be assigned to %s" % (rhs, lhs)) @@ -41,13 +106,26 @@ 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) position = "up" if upset else None @@ -110,6 +188,52 @@ def is_protected(tag, defin): return A_PROTECTED & defin.get_attributes(tag) +def normalize_lhs(lhs, evaluation): + """ + Process the lhs in a way that + * if it is a conditional expression, reduce it to + a shallow conditional expression + ( Conditional[Conditional[...],tst] -> Conditional[uncondlhs, tst]) + with `uncondlhs` the result of strip all the conditions from lhs. + * if `uncondlhs` is not a `List` or a `Part` expression, evaluate the + elements. + + returns a tuple with the normalized lhs, and the lookup_name of the head in uncondlhs. + """ + cond = [] + hold_pattern = False + + core = lhs + while True: + if core.has_form("System`Condition", 2): + core, new_cond = core.elements[0], core.elements[1] + cond.append(new_cond) + continue + if core.has_form("System`HoldPattern", 1): + core, hold_pattern = core.elements[0], True + continue + break + + lookup_name = core.get_lookup_name() + # if hold_pattern was set, or the original lhs was not + # an expression, or is one of the special cases in which + # the elements of the lhs must not be evaluated, + # just return the lhs as it was, together with the lookup_name: + if ( + hold_pattern + or not isinstance(lhs, Expression) + or core.get_head() in NOT_EVALUATE_ELEMENTS_IN_ASSIGNMENTS + ): + return lhs, lookup_name + + # For the general case, evaluate the elements + lhs = core.evaluate_elements(evaluation) + # and add the conditions as a single condition if any: + if cond: + lhs = Expression(SymbolCondition, core, Expression(SymbolAnd, *cond)) + return lhs, lookup_name + + def repl_pattern_by_symbol(expr): elements = expr.get_elements() if len(elements) == 0: @@ -132,7 +256,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 +290,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 +302,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 +344,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 +371,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 +385,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 +398,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: @@ -376,7 +527,7 @@ def process_assign_options(self, lhs, rhs, evaluation, tags, upset): def process_assign_numericq(self, lhs, rhs, evaluation, tags, upset): - lhs, condition = unroll_conditions(lhs) + # lhs, condition = unroll_conditions(lhs) lhs, rhs = unroll_patterns(lhs, rhs, evaluation) if rhs not in (SymbolTrue, SymbolFalse): evaluation.message("NumericQ", "set", lhs, rhs) @@ -430,10 +581,22 @@ 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 ) + lhs_name = lhs.get_name() if lhs_name == "System`$RecursionLimit": process_assign_recursion_limit(lhs, rhs, evaluation) @@ -455,6 +618,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 +725,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 +804,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() @@ -607,153 +832,163 @@ def process_rhs_conditions(lhs, rhs, condition, evaluation): return lhs, rhs +def find_tag(focus): + name = focus.get_lookup_name() + if name == "System`HoldPattern": + if len(focus.elements) == 1: + return find_tag(focus.elements[0]) + # If HoldPattern appears with more than one element, + # the message + # "HoldPattern::argx: HoldPattern called with 2 arguments; 1 argument is expected." + # must be shown. + raise AssignmentException(lhs, None) + if name == "System`Optional": + return None + if name == "System`Pattern" and len(focus.elements) == 2: + pat = focus.elements[1] + if pat.get_head_name() in ( + "System`Blank", + "System`BlankSequence", + "System`BlankNullSequence", + ): + elems = pat.elements + if len(elems) == 0: + return None + return find_tag(elems[0]) + else: + return find_tag(pat) + return name + + 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 + + if ( + isinstance(focus, Expression) + and focus.head not in NOT_EVALUATE_ELEMENTS_IN_ASSIGNMENTS + ): + focus = focus.evaluate_elements(evaluation) + name = lhs.get_head_name() if tags is None and not upset: - name = focus.get_lookup_name() - if not name: + name = find_tag(focus) + if name == "": evaluation.message(self.get_name(), "setraw", focus) raise AssignmentException(lhs, None) - tags = [name] + tags = [] if name is None else [name] elif upset: - tags = [focus.get_lookup_name()] + name = find_tag(focus) + tags = [] if name is None else [name] else: - allowed_names = [focus.get_lookup_name()] + name = find_tag(focus) + allowed_names = [] if name is None else [name] for name in tags: if name not in allowed_names: evaluation.message(self.get_name(), "tagnfd", Symbol(name)) raise AssignmentException(lhs, None) + + if len(tags) == 0: + evaluation.message(self.get_name(), "nosym", focus) + raise AssignmentException(lhs, None) return tags 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 isinstance(focus, Expression) and focus.head not in ( + SymbolSet, + SymbolSetDelayed, + SymbolUpSet, + SymbolUpSetDelayed, + SymbolTagSet, + SymbolTagSetDelayed, + SymbolList, + SymbolPart, + ): + focus = focus.evaluate_elements(evaluation) + if tags is None and not upset: - name = focus.get_lookup_name() - if not name: + name = find_tag(focus) + if name == "": evaluation.message(self.get_name(), "setraw", focus) raise AssignmentException(lhs, None) - tags = [name] + tags = [] if name is None else [name] elif upset: tags = [] if isinstance(focus, Atom): evaluation.message(self.get_name(), "normal") raise AssignmentException(lhs, None) for element in focus.elements: - name = element.get_lookup_name() - tags.append(name) + name = find_tag(element) + if name != "" and name is not None: + tags.append(name) else: - allowed_names = [focus.get_lookup_name()] + allowed_names = [find_tag(focus)] for element in focus.get_elements(): - if not isinstance(element, Symbol) and element.get_head_name() in ( - "System`HoldPattern", - ): - element = element.elements[0] - if not isinstance(element, Symbol) and element.get_head_name() in ( - "System`Pattern", - ): - element = element.elements[1] - if not isinstance(element, Symbol) and element.get_head_name() in ( - "System`Blank", - "System`BlankSequence", - "System`BlankNullSequence", - ): - if len(element.elements) == 1: - element = element.elements[0] - - allowed_names.append(element.get_lookup_name()) + element_tag = find_tag(element) + if element_tag is not None and element_tag != "": + allowed_names.append(element_tag) for name in tags: if name not in allowed_names: evaluation.message(self.get_name(), "tagnfd", Symbol(name)) raise AssignmentException(lhs, None) + if len(tags) == 0: + evaluation.message(self.get_name(), "nosym", focus) + raise AssignmentException(lhs, None) return tags, focus 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): - if isinstance(lhs, Symbol): - name = lhs.name - else: - name = lhs.get_head_name() - # lhs._format_cache = None + def assign(self, lhs, rhs, evaluation, tags=None, upset=False): + lhs, lookup_name = normalize_lhs(lhs, evaluation) try: # Deal with direct assignation to properties of # the definition object - func = self.special_cases.get(name, None) + func = self.special_cases.get(lookup_name, None) if func: return func(self, lhs, rhs, evaluation, tags, upset) - 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..800178b3f 100644 --- a/mathics/builtin/attributes.py +++ b/mathics/builtin/attributes.py @@ -467,6 +467,8 @@ class OneIdentity(Predefined): 'OneIdentity' affects pattern matching: >> SetAttributes[f, OneIdentity] >> a /. f[args___] -> {args} + = a + >> a /. f[x_:0, u_] -> {u} = {a} It does not affect evaluation: >> f[a] diff --git a/mathics/builtin/box/layout.py b/mathics/builtin/box/layout.py index f9e4a7401..d37d3e5b8 100644 --- a/mathics/builtin/box/layout.py +++ b/mathics/builtin/box/layout.py @@ -201,7 +201,10 @@ class RowBox(BoxExpression): summary_text = "horizontal arrange of boxes" def __repr__(self): - return "RowBox[List[" + self.items.__repr__() + "]]" + try: + return "RowBox[List[" + self.items.__repr__() + "]]" + except: + return "RowBox[List[{uninitialized}]]" def apply_list(self, boxes, evaluation): """RowBox[boxes_List]""" diff --git a/mathics/builtin/files_io/files.py b/mathics/builtin/files_io/files.py index e989eba9f..c91dc99bc 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/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/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/expression.py b/mathics/core/expression.py index 04025e0ef..1d9382526 100644 --- a/mathics/core/expression.py +++ b/mathics/core/expression.py @@ -530,6 +530,10 @@ def evaluate( return expr def evaluate_elements(self, evaluation) -> "Expression": + """ + return a new expression with the same head, and the + evaluable elements evaluated. + """ elements = [ element.evaluate(evaluation) if isinstance(element, EvalMixin) else element for element in self._elements diff --git a/mathics/core/list.py b/mathics/core/list.py index 4dd21d6d1..187e667fb 100644 --- a/mathics/core/list.py +++ b/mathics/core/list.py @@ -86,6 +86,10 @@ def __str__(self) -> str: # @timeit def evaluate_elements(self, evaluation): + """ + return a new expression with the same head, and the + evaluable elements evaluated. + """ elements_changed = False # Make tuple self._elements mutable by turning it into a list. elements = list(self._elements) @@ -98,16 +102,17 @@ def evaluate_elements(self, evaluation): elements_changed = True elements[index] = new_value - if elements_changed: - # Save changed elements, making them immutable again. - self._elements = tuple(elements) + if not elements_changed: + return self - # TODO: we could have a specialized version of this - # that keeps self.value up to date when that is - # easy to do. That is left of some future time to - # decide whether doing this this is warranted. - self._build_elements_properties() - self.value = None + new_list = ListExpression(*elements) + # TODO: we could have a specialized version of this + # that keeps self.value up to date when that is + # easy to do. That is left of some future time to + # decide whether doing this this is warranted. + new_list._build_elements_properties() + new_list.value = None + return new_list @property def is_literal(self) -> bool: @@ -142,7 +147,7 @@ def rewrite_apply_eval_step(self, evaluation) -> Tuple[Expression, bool]: self._build_elements_properties() if not self.elements_properties.elements_fully_evaluated: new = self.shallow_copy() - new.evaluate_elements(evaluation) + new = new.evaluate_elements(evaluation) return new, False return self, False diff --git a/mathics/core/pattern.py b/mathics/core/pattern.py index 39dc1511e..5f354c6d3 100644 --- a/mathics/core/pattern.py +++ b/mathics/core/pattern.py @@ -3,8 +3,9 @@ # -*- coding: utf-8 -*- -from mathics.core.element import ensure_context -from mathics.core.expression import Expression +from mathics.core.atoms import Integer +from mathics.core.element import ensure_context, BoxElementMixin +from mathics.core.expression import Expression, SymbolDefault from mathics.core.symbols import Atom, Symbol, system_symbols from mathics.core.systemsymbols import SymbolSequence from mathics.core.util import subsets, subranges, permutations @@ -33,6 +34,11 @@ def Pattern_create(expr): + """ + Creates an AtomPattern or an ExpressionPattern or a + (builtin) pattern object according + to the class of the parameter `expr`. + """ from mathics.builtin import pattern_objects # from mathics.core.pattern import AtomPattern, ExpressionPattern @@ -48,6 +54,13 @@ def Pattern_create(expr): 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 @@ -82,22 +95,33 @@ 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): + fully=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) + element_index, element_count, fully) return result""" def does_match(self, expression, evaluation, vars=None, fully=True): + """ + returns True if `expression` matches self. + """ if vars is None: vars = {} @@ -187,7 +211,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 +230,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 +266,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 +332,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 +347,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 +370,79 @@ 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 + assert pat.get_head_name() == "System`Pattern" + key = pat.elements[0].atom.name + optionals[key] = value + elif len(pat_elem.elements) == 1: + pat = pat_elem.elements[0] + assert pat.get_head_name() == "System`Pattern" + key = pat.elements[0].atom.name + # 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 + + # Load the default values in vars + 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,11 +531,14 @@ 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): + if isinstance(expr, BoxElementMixin): + expr = expr.to_expression() + self.head = Pattern.create(expr.head) self.elements = [Pattern.create(element) for element in expr.elements] self.expr = expr @@ -545,7 +595,6 @@ def match_element( first=False, fully=True, depth=1, - wrap_oneid=True, ): if rest_expression is None: @@ -626,6 +675,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 +685,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 +731,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 +746,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/systemsymbols.py b/mathics/core/systemsymbols.py index b9d9c47fb..e4a96c494 100644 --- a/mathics/core/systemsymbols.py +++ b/mathics/core/systemsymbols.py @@ -132,6 +132,7 @@ SymbolOutputForm = Symbol("System`OutputForm") SymbolOverflow = Symbol("System`Overflow") SymbolPackages = Symbol("System`$Packages") +SymbolPart = Symbol("System`Part") SymbolPattern = Symbol("System`Pattern") SymbolPower = Symbol("System`Power") SymbolPi = Symbol("System`Pi") @@ -157,6 +158,7 @@ SymbolSeries = Symbol("System`Series") SymbolSeriesData = Symbol("System`SeriesData") SymbolSet = Symbol("System`Set") +SymbolSetDelayed = Symbol("System`SetDelayed") SymbolSign = Symbol("System`Sign") SymbolSimplify = Symbol("System`Simplify") SymbolSin = Symbol("System`Sin") @@ -173,6 +175,8 @@ SymbolSubsuperscriptBox = Symbol("System`SubsuperscriptBox") SymbolSuperscriptBox = Symbol("System`SuperscriptBox") SymbolTable = Symbol("System`Table") +SymbolTagSet = Symbol("System`TagSet") +SymbolTagSetDelayed = Symbol("System`TagSetDelayed") SymbolTeXForm = Symbol("System`TeXForm") SymbolThrow = Symbol("System`Throw") SymbolToString = Symbol("System`ToString") @@ -181,4 +185,6 @@ SymbolUndefined = Symbol("System`Undefined") SymbolUnequal = Symbol("System`Unequal") SymbolUnevaluated = Symbol("System`Unevaluated") +SymbolUpSet = Symbol("System`UpSet") +SymbolUpSetDelayed = Symbol("System`UpSetDelayed") 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/builtin/test_assignment.py b/test/builtin/test_assignment.py index ca775564e..4261fbeb1 100644 --- a/test/builtin/test_assignment.py +++ b/test/builtin/test_assignment.py @@ -1,4 +1,5 @@ # -*- coding: utf-8 -*- + import pytest from test.helper import check_evaluation, session from mathics_scanner.errors import IncompleteSyntaxError @@ -149,6 +150,58 @@ def test_setdelayed_oneidentity(): "{a + b, Q[a, b], a + b}", None, ), + (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", + ), ( None, None, 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, + ) diff --git a/test/test_rules_patterns.py b/test/test_rules_patterns.py index b1c7ac476..401a2849c 100644 --- a/test/test_rules_patterns.py +++ b/test/test_rules_patterns.py @@ -1,5 +1,195 @@ # -*- coding: utf-8 -*- +import os + +import pytest + from .helper import check_evaluation +import pytest + + +@pytest.mark.parametrize( + ("str_expr", "str_expected", "msg"), + [ + (None, None, None), + # F has the attribute, but G doesn't. + ("SetAttributes[F, OneIdentity]", "None", None), + ("SetAttributes[r, Flat]", "None", None), + ("SetAttributes[s, Flat]", "None", None), + ("SetAttributes[s, OneIdentity]", "None", 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[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), + # 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", + ), + ( + "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", + ), + ], +) +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, + ) + + +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", + ), + ( + "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_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, + ) def test_downvalues():