From 76b6bbe03ab92cc1ce4f8266bfd7c442c7bd4aa2 Mon Sep 17 00:00:00 2001 From: Juan Mauricio Matera Date: Sun, 30 Aug 2026 21:14:15 -0300 Subject: [PATCH 1/9] specialized simple patterns add subtract test --- mathics/core/definitions.py | 31 ++- mathics/core/pattern/base.py | 7 +- mathics/core/pattern/common.py | 163 +++++++++++++-- mathics/core/pattern/deferred.py | 47 ++++- mathics/core/pattern/ordered.py | 327 +++++++++++++++++++++++++++++-- mathics/core/rules.py | 12 ++ test/timings/test_regressions.py | 1 + 7 files changed, 540 insertions(+), 48 deletions(-) diff --git a/mathics/core/definitions.py b/mathics/core/definitions.py index 46624b2e68..59cd025e22 100644 --- a/mathics/core/definitions.py +++ b/mathics/core/definitions.py @@ -10,7 +10,7 @@ import pickle import re from collections import defaultdict -from typing import Dict, List, Optional, Sequence, Set, Tuple, Union +from typing import TYPE_CHECKING, Dict, List, Optional, Sequence, Set, Tuple, Union from mathics_scanner.tokeniser import full_names_pattern @@ -29,6 +29,9 @@ from mathics.core.util import canonic_filename from mathics.settings import ROOT_DIR +if TYPE_CHECKING: + from mathics.core.evaluation import Evaluation + # Collections of format symbols. Here we load some basic cases. # More symbols are populated from FormMeta classes (see `mathics.builtin.forms.base`) @@ -82,6 +85,25 @@ def __init__( if not self.add_rule(rule): print(f"{rule.pattern.expr} could not be associated with {self.name}") + def _resolve(self, evaluation: "Evaluation"): + """ + Go over all the rules, and ensure that the corresponding patterns are in its final state + according to the current evaluation state. + """ + for rule_list in ( + self.ownvalues, + self.downvalues, + self.subvalues, + self.upvalues, + self.nvalues, + self.defaultvalues, + ): + for rule in rule_list: + rule._resolve(evaluation) + for rule_list in self.formatvalues.values(): + for rule in rule_list: + rule._resolve(evaluation) + def get_values_list(self, pos: str) -> List[BaseRule]: """Return one of the value lists""" assert pos.isalpha() @@ -1105,6 +1127,7 @@ def load_builtin_definitions( """ Load definitions from Builtin classes, autoload files and extension modules. """ + from mathics.core.evaluation import Evaluation from mathics.core.load_builtin import ( definition_contribute, mathics3_builtins_modules, @@ -1132,5 +1155,11 @@ def load_builtin_definitions( with open(builtin_filename, "wb") as builtin_file: pickle.dump(self.builtin, builtin_file, -1) + # Loop over definitions, to resolve on each rule + # which special kind of pattern must be considered. + evaluation = Evaluation(self) + for definition in self.builtin.values(): + definition._resolve(evaluation) + autoload_files(self, ROOT_DIR, "Autoload") autoload_files(self, osp.join(ROOT_DIR, "SystemFiles"), "Formats") diff --git a/mathics/core/pattern/base.py b/mathics/core/pattern/base.py index 6bd1509cd5..afa28aee7c 100644 --- a/mathics/core/pattern/base.py +++ b/mathics/core/pattern/base.py @@ -452,6 +452,7 @@ class ExpressionPattern(BasePattern): # get_pre_choices = pattern_nocython.get_pre_choices # match = pattern_nocython.match attributes: int + elements: Tuple[BasePattern, ...] def __init__( self, @@ -463,10 +464,10 @@ def __init__( self.location = expr.location if hasattr(expr, "location") else None head = expr.head self.head = BasePattern.create(head, evaluation=evaluation) - self.elements = [ + self.elements = tuple( BasePattern.create(element, evaluation=evaluation) for element in expr.elements - ] + ) def _build_pattern_sort_key(self) -> tuple: return ( @@ -690,7 +691,7 @@ def get_match_count(self, vars_dict: Optional[dict] = None) -> Tuple[int, int]: def sort(self): """Sort the elements according to their sort key""" - self.elements.sort(key=lambda e: e.pattern_precedence) + self.elements = tuple(sorted(self.elements, key=lambda e: e.pattern_precedence)) def expression_pattern_match_element_process_items( diff --git a/mathics/core/pattern/common.py b/mathics/core/pattern/common.py index 8b308a7139..2d008f7580 100644 --- a/mathics/core/pattern/common.py +++ b/mathics/core/pattern/common.py @@ -15,13 +15,21 @@ from mathics.core.expression import Expression from mathics.core.rules import is_option_rule from mathics.core.symbols import SymbolList -from mathics.core.systemsymbols import SymbolDefault, SymbolOptional +from mathics.core.systemsymbols import ( + SymbolBlank, + SymbolBlankNullSequence, + SymbolBlankSequence, + SymbolDefault, + SymbolOptional, + SymbolPattern, +) from .base import AtomPattern, BasePattern, ExpressionPattern def _is_option_like(candidate: BaseElement) -> bool: """Same shape check OptionsPattern.get_match_candidates() uses.""" + return is_option_rule(candidate) or candidate.has_form(SymbolList, None) @@ -136,24 +144,149 @@ def match_expression_with_one_identity( vars_dict[k] = old +def _is_bare_blank(element: BaseElement) -> bool: + """ + True for an unnamed Blank[] or Blank[Type] (0 or 1 sub-elements). + """ + return element.has_form(SymbolBlank, 0, 1) + + +def classify_fixed_blank_tuple(elements: tuple) -> Optional[tuple]: + """ + If every entry of `elements` (zero or more of them) is either: + - a bare (unnamed) Blank[] / Blank[Type], or + - a named blank Pattern[name, Blank[...]] (`x_`, `x_Integer`, ...) + return `elements` back unchanged, as a signal that this + ExpressionPattern has a fixed arity with no possible backtracking + between slots: matching is exactly "N elements, checked + positionally, one does_match() per slot, in order", nothing else -- + including the case of a name repeated across slots (`f[a_, a_]`), + since that's already handled *inside* Pattern.match's own + already-bound-name check (does_match on the second occurrence will + correctly require sameQ against the value bound by the first) -- + this fast path doesn't need to special-case it. + + Returns None (fall through to the general machinery) if any + element doesn't have this exact shape. + """ + for element in elements: + if _is_bare_blank(element): + continue + if element.has_form(SymbolPattern, 2) and _is_bare_blank(element.elements[1]): + continue + return None + return elements + + +def classify_single_sequence(elements: tuple) -> Optional[BaseElement]: + """ + If `elements` is a 1-tuple whose sole entry is a NAMED + BlankSequence or BlankNullSequence -- `Pattern[name, BlankSequence[...]]` + (`s__`, `s__Integer`) or `Pattern[name, BlankNullSequence[...]]` + (`s___`, `s___Integer`) -- return that raw `Pattern[...]` element + unchanged, as a signal that this ExpressionPattern has exactly ONE + possible match: there is nothing else in the pattern to backtrack + against, so the whole expression's elements (however many there + are) are the only candidate for this single (Blank)(Null)Sequence. + + BARE (unnamed) `__`/`___` are deliberately NOT included here, even + though structurally similar: as the sole element of a pattern with + no `rest_elements` after it, `less_first` is already `False` in + `_regular_match_element_sets`, so `subranges()` tries the + full-length split FIRST and succeeds immediately there -- no + combinatorial search actually happens for the bare case today, so + a dedicated class has nothing measurable to save (confirmed by + benchmarking: near-identical timings with/without this + classification for `head[__]`/`head[___]`, both typed and + untyped). + + The NAMED case is different: profiling `head[s__Integer]` (see + session notes) against a matched Range[6000] showed the actual, + unavoidable per-element type check (`BlankSequence.match` itself) + takes about 15% of the total match time; the other ~85% is spent + BEFORE `match_element`'s `subranges()` call even gets to try + anything -- `basic_match_expression.yield_choice`'s pre-check via + `get_match_candidates_count`, followed by `match_element` building + `element_candidates` via `get_match_candidates` AGAIN (the exact + same O(n) type-scan, discarded immediately after: `subranges()` -- + unlike `subsets()`, used by the Orderless path -- never even + consults its `included` argument, see its module docstring), plus + the `Expression(Sequence, *items)` (re)allocation in + `_yield_sequence_wrappings`. All of that exists to support + backtracking against OTHER elements/candidates that, for this + exact shape, don't exist: there is exactly one pattern element and + it must absorb the entire (possibly empty, possibly + single-element, possibly multi-element) sequence of expression + elements, or the match fails outright -- no split to search for. + + Deliberately still excludes: + - Anything wrapped in Condition/Optional/PatternTest/Alternatives + around the (Blank)(Null)Sequence -- not exactly + `Pattern[name, Blank(Null)Sequence[...]]` shaped, conservatively + rejected (same policy as `classify_fixed_blank_tuple`). + - More than one element (`head[s__, t_]`, etc.) -- there IS + backtracking to do there (where does `s__`'s block end?), so + this fast path does not apply; that shape stays on the general + `subranges()`-based search. + + Returns None (fall through to the general machinery) if `elements` + doesn't have exactly this shape. + """ + if len(elements) != 1: + return None + element = elements[0] + if not element.has_form(SymbolPattern, 2): + return None + inner = element.elements[1] + if inner.has_form(SymbolBlankSequence, 0, 1) or inner.has_form( + SymbolBlankNullSequence, 0, 1 + ): + return element + return None + + +def match_fixed_blank_tuple( + blanks: tuple, + expr_elements: tuple, + vars_dict: dict, + evaluation: Evaluation, +) -> Optional[dict]: + """ + Positionally match `blanks[i]` against `expr_elements[i]` for every + i, threading vars_dict THROUGH EACH SLOT'S OWN yield_func -- same + protocol basic_match_expression/match_element use everywhere else + in this package -- rather than mutating a single shared dict in + place. + + Returns the fully-threaded vars_dict on success (a dict that may or + may not be `is vars_dict`, depending on what each slot's match() + did), or None if any slot failed to match. Never mutates the + `vars_dict` object passed in. + """ + current_vars = vars_dict + for blank, elt in zip(blanks, expr_elements): + result_box: list = [] + blank.match( + elt, + { + "yield_func": lambda sub_vars, _rest, _box=result_box: _box.append( + sub_vars + ), + "vars_dict": current_vars, + "evaluation": evaluation, + "fully": True, + }, + ) + if not result_box: + return None + current_vars = result_box[0] + return current_vars + + def _options_pattern_split( element: "BasePattern", rest_elements: tuple, candidates: tuple ): """ - WMA quirk: when a pattern contains more than one OptionsPattern[] - element, only the LAST one ever collects any option-like arguments -- - every earlier OptionsPattern[] always matches an empty sequence, - regardless of what option-like values are available. E.g. - `F[x_Integer, opt1:OptionsPattern[], opt2:OptionsPattern[]]` applied - to `F[z->p, 2, a->2, m->2]` binds opt1 to `{}` and opt2 to - `{a->2, m->2, z->p}` in WMA -- never split between the two. - - The general subranges()/subsets()-based search doesn't know this: it - just finds *some* backtracking split between opt1 and opt2 (both have - unbounded, untyped match counts, so nothing else disambiguates which - split "is the one"), which usually isn't the WMA split and is wasted - search besides. - Returns a `sets` list (same (items, (before, after)) shape the subranges()/subsets()-based paths produce) if `element` is an OptionsPattern[] (bare or Pattern-wrapped), or None if this fast path diff --git a/mathics/core/pattern/deferred.py b/mathics/core/pattern/deferred.py index b925b04ad5..11f913e936 100644 --- a/mathics/core/pattern/deferred.py +++ b/mathics/core/pattern/deferred.py @@ -7,13 +7,20 @@ from typing import Optional -from mathics.core.attributes import A_ORDERLESS +from mathics.core.attributes import A_FLAT, A_ONE_IDENTITY, A_ORDERLESS from mathics.core.element import BaseElement from mathics.core.evaluation import Evaluation from mathics.core.expression import Expression +from mathics.core.symbols import Symbol from .base import BasePattern, ExpressionPattern -from .ordered import OrderedExpressionPattern +from .common import classify_fixed_blank_tuple, classify_single_sequence +from .ordered import ( + FixedBlankTupleExpressionPattern, + OrderedExpressionPattern, + SimpleOrderedExpressionPattern, + SingleSequenceExpressionPattern, +) from .orderless import OrderlessExpressionPattern @@ -62,7 +69,7 @@ def __init__(self, expr: Expression): # pattern_precedence (see _build_pattern_sort_key below), which # is purely structural and must work even before resolution. self.head = BasePattern.create(expr.head) - self.elements = [BasePattern.create(element) for element in expr.elements] + self.elements = tuple(BasePattern.create(element) for element in expr.elements) self._impl: Optional[ExpressionPattern] = None def _resolve(self, evaluation: Evaluation) -> ExpressionPattern: @@ -95,9 +102,31 @@ class that declares its own attributes statically in Python, or any Definitions is fully populated), use DeferredExpressionPattern instead. """ - cls = ( - OrderlessExpressionPattern - if A_ORDERLESS & attributes - else OrderedExpressionPattern - ) - return cls(expr, attributes, evaluation) + if A_ORDERLESS & attributes: + return OrderlessExpressionPattern(expr, attributes, evaluation) + if (A_FLAT + A_ONE_IDENTITY) & attributes: + return OrderedExpressionPattern(expr, attributes, evaluation) + # No Orderless/Flat/OneIdentity: this is where the fixed-arity + # Blank-tuple shape (head[_], head[_,_], head[a_,b_], ...) gets + # decided -- BEFORE constructing any pattern objects, straight off + # the raw expr.elements (classify_fixed_blank_tuple is duck-typed + # to work on raw Expression children exactly like it does on + # BasePattern-wrapped ones). Doing the classification here, rather + # than as a runtime check inside a class's __init__/match, means + # neither SimpleOrderedExpressionPattern nor + # FixedBlankTupleExpressionPattern ever has to ask "wait, does this + # actually apply to me?" -- each is only ever constructed for the + # shape it's responsible for. + if isinstance(expr.head, Symbol): + if classify_fixed_blank_tuple(expr.elements) is not None: + return FixedBlankTupleExpressionPattern(expr, attributes, evaluation) + # head[s__], head[s__HEAD], head[s___], head[s___HEAD]: the + # single-named-Blank(Null)Sequence shape -- see + # classify_single_sequence's docstring in common.py for why + # this is a distinct, non-overlapping shape from the + # fixed-arity Blank tuple above (this one has exactly one + # element instead of two-or-more, and it's a Sequence, not a + # Blank). + if classify_single_sequence(expr.elements) is not None: + return SingleSequenceExpressionPattern(expr, attributes, evaluation) + return SimpleOrderedExpressionPattern(expr, attributes, evaluation) diff --git a/mathics/core/pattern/ordered.py b/mathics/core/pattern/ordered.py index 881331fc15..9f8caf5604 100644 --- a/mathics/core/pattern/ordered.py +++ b/mathics/core/pattern/ordered.py @@ -16,10 +16,16 @@ from mathics.core.evaluation import Evaluation from mathics.core.expression import Expression from mathics.core.interrupt import TimeoutInterrupt +from mathics.core.systemsymbols import SymbolSequence from mathics.core.util import subranges -from .base import BasePattern, ExpressionPattern, StopGenerator_ExpressionPattern_match -from .common import match_expression_with_one_identity +from .base import ( + AtomPattern, + BasePattern, + ExpressionPattern, + StopGenerator_ExpressionPattern_match, +) +from .common import match_expression_with_one_identity, match_fixed_blank_tuple def basic_match_expression( @@ -237,29 +243,20 @@ def get_pre_choices_with_order( pattern_context["yield_choice"](pattern_context["vars_dict"]) -# --- Ordered/Orderless/Deferred class split --- -# -# ExpressionPattern is a virtual base class for these three subclasses. -# OrderlessExpressionPattern and OrderedExpressionPattern classes -# implement patterns when we have access to the attributes of the Head -# element, and the Orderless attribute is or not set for it. -# This attribute defines the principal branches on how pattern matching -# happends. -# Initially, when the Definitions object is not already populated, -# we do not have access to the Pattern attributes. We handle this case -# using the DeferredExpressionPattern. When the evaluation tries to -# use these patterns to check if they match with an expression, the -# attributes becomes available. Then, DeferredExpressionPattern -# delegates on the right object class (Ordered/Orderless) according -# to the available attribute. After the first evaluation, delegation -# becomes permanent. class OrderedExpressionPattern(ExpressionPattern): """ - ExpressionPattern for a head known NOT to have the Orderless + ExpressionPattern for a head without the Orderless attribute. Only constructed via make_expression_pattern() / DeferredExpressionPattern, where `attributes` is already known. - No method overrides needed: ExpressionPattern's own + This is the general non-Orderless case, including Flat and/or + OneIdentity heads. For the common sub-case where NEITHER Flat nor + OneIdentity is set either, make_expression_pattern() constructs + SimpleOrderedExpressionPattern (below) instead, which skips the + Flat/OneIdentity-related runtime checks this class still needs and + adds the fixed-arity Blank-tuple fast path. + + No method overrides needed beyond match(): ExpressionPattern's own `_yield_sequence_wrappings`/`_regular_match_element_sets` already implement the non-Orderless behavior (see class-split note above). """ @@ -415,3 +412,293 @@ def match(self, expression: BaseElement, pattern_context: dict): pattern_context["element_count"] = old_element_count else: pattern_context.pop("element_count", None) + + +class SimpleOrderedExpressionPattern(OrderedExpressionPattern): + """ + Specialization of OrderedExpressionPattern for a head with NONE of + Orderless, Flat, OneIdentity -- the common case, since most heads + declare no special attributes at all. Only constructed via + make_expression_pattern(), which is the sole place that decides + between this class and the general OrderedExpressionPattern (used + when Flat and/or OneIdentity IS present). + + Skips the Flat/OneIdentity-related runtime checks that + OrderedExpressionPattern.match still needs + (`if not A_FLAT & attributes: fully = True`, + `if A_ONE_IDENTITY & attributes: match_expression_with_one_identity(...)`) + -- for this class they always resolve the same way, so they're + simply absent rather than evaluated every match() call. + + """ + + def __init__( + self, + expression: Expression, + attributes: int, + evaluation: Optional[Evaluation] = None, + ): + super().__init__(expression, attributes, evaluation) + + def match(self, expression: BaseElement, pattern_context: dict): + """ + Try to match the pattern against an Expression, for a head + known to have none of Orderless/Flat/OneIdentity. + """ + from mathics.core.atoms.associations import Association + + evaluation = pattern_context["evaluation"] + yield_func = pattern_context["yield_func"] + vars_dict = pattern_context["vars_dict"] + + evaluation.check_stopped() + if self.isliteral: + if expression.sameQ(self.expr): + yield_func(vars_dict, None) + return + + # --- use mutation with undo instead of copy --- + old_fully = pattern_context.get("fully") + old_attributes = pattern_context.get("attributes") + old_head = pattern_context.get("head") + old_element_index = pattern_context.get("element_index") + old_element_count = pattern_context.get("element_count") + try: + # No A_FLAT here (guaranteed by construction), so fully is + # unconditionally True -- unlike the general + # OrderedExpressionPattern.match, there's no runtime check + # to make. + pattern_context["fully"] = True + pattern_context["attributes"] = self.attributes + pattern_context["head"] = None + pattern_context["element_index"] = None + pattern_context["element_count"] = None + + if isinstance(expression, Association): + expression = expression.expr + + if isinstance(expression, Expression): + try: + basic_match_expression(self, expression, pattern_context) + except (StopGenerator_ExpressionPattern_match, TimeoutInterrupt): + return + # No A_ONE_IDENTITY check -- guaranteed absent for this class. + finally: + # restore old values + if old_fully is not None: + pattern_context["fully"] = old_fully + else: + pattern_context.pop("fully", None) + if old_attributes is not None: + pattern_context["attributes"] = old_attributes + else: + pattern_context.pop("attributes", None) + if old_head is not None: + pattern_context["head"] = old_head + else: + pattern_context.pop("head", None) + if old_element_index is not None: + pattern_context["element_index"] = old_element_index + else: + pattern_context.pop("element_index", None) + if old_element_count is not None: + pattern_context["element_count"] = old_element_count + else: + pattern_context.pop("element_count", None) + + +class FixedBlankTupleExpressionPattern(ExpressionPattern): + """ + ExpressionPattern for a LITERAL (bare Symbol) head applied to a + fixed-length tuple of elements that are ALL bare or named Blanks + -- head[_], head[_,_], head[_,_,_], head[_String], head[_List], + head[_Integer], head[a_,b_], head[a_,a_], etc. (see + classify_fixed_blank_tuple in common.py for the exact shape). + + Only ever constructed by make_expression_pattern(), which performs + the classification BEFORE building any pattern objects -- directly + on the raw, not-yet-wrapped expr.elements (classify_fixed_blank_tuple + is duck-typed to work on either raw Expression children or + BasePattern-wrapped ones). + + For this shape there is exactly ONE possible match: a positional, + slot-by-slot check (arity + per-slot does_match/match), with zero + ambiguity for Sequence[...]/subranges/subsets to search over -- + so match() below IS the whole algorithm, not a fast path in front + of a fallback. There is deliberately no fallback to + basic_match_expression: nothing it could find that this doesn't + already decide. + + """ + + def __init__( + self, + expression: Expression, + attributes: int, + evaluation: Optional[Evaluation] = None, + ): + super().__init__(expression, attributes, evaluation) + assert isinstance(self.elements, tuple) + self.attributes = attributes + + def match(self, expression: BaseElement, pattern_context: dict): + """ + The one and only match algorithm for this shape: positional, + slot-by-slot, via match_fixed_blank_tuple (see its docstring + in common.py for why it threads vars_dict through each slot's + own match()/yield_func rather than using does_match(), which + would silently drop any binding a named + Pattern[name, Blank[...]] slot produces). + """ + from mathics.core.atoms.associations import Association + + evaluation = pattern_context["evaluation"] + yield_func = pattern_context["yield_func"] + vars_dict = pattern_context["vars_dict"] + + evaluation.check_stopped() + + if isinstance(expression, Association): + expression = expression.expr + + if not isinstance(expression, Expression): + return + + expr_elements = expression.elements + if len(expr_elements) != len(self.elements): + return + # self.head is guaranteed (by make_expression_pattern's + # isinstance(expr.head, Symbol) check, before this class is + # ever constructed) to be an AtomPattern wrapping a Symbol -- + # whose own match_symbol() is exactly `expression is self.atom` + # (see base.py). does_match() would get to the same answer, + # but only after building its own pattern_context, defining a + # closure, and raising+catching StopGenerator_Pattern to turn + # that into a bool -- a full exception round trip to compute + # an identity check. Skip all of that. + self_head = self.head + if ( + not isinstance(self_head, AtomPattern) + or expression.get_head() is not self_head.atom + ): + return + + result_vars = match_fixed_blank_tuple( + self.elements, expr_elements, vars_dict, evaluation + ) + if result_vars is not None: + yield_func(result_vars, None) + + +class SingleSequenceExpressionPattern(ExpressionPattern): + """ + ExpressionPattern for a LITERAL (bare Symbol) head applied to + EXACTLY ONE element, which is a NAMED BlankSequence or + BlankNullSequence, typed or not -- head[s__], head[s__HEAD], + head[s___], head[s___HEAD] (see classify_single_sequence in + common.py for the exact shape and the profiling numbers that + motivate this class). + + Only ever constructed by make_expression_pattern(), which performs + the classification BEFORE building any pattern objects, same + convention as FixedBlankTupleExpressionPattern. + + For this shape there is exactly ONE possible match: the single + (Blank)(Null)Sequence element must absorb the expression's entire + element tuple (empty, one, or many elements) or the match fails -- + there is no other pattern element to backtrack against, so no + subranges()/get_wrappings/match_element search is needed. match() + below goes straight from "is this the right head" to "hand the + whole element tuple to the wrapped Pattern[name, ...] and let IT + do the (unavoidable) per-element type check exactly once" -- + skipping the redundant double candidate-scan + (get_match_candidates_count then get_match_candidates, both doing + the same O(n) type check for a typed s__HEAD, only for + match_element's `included` argument that the Ordered path's + subranges() doesn't even consult -- see subranges()'s own + docstring/TODO) and the extra Expression(Sequence, *items) + (re)allocation that get_wrappings/_yield_sequence_wrappings would + otherwise perform on top of the one this class already needs. + + Deliberately preserves the exact binding shape a named + (Blank)(Null)Sequence produces via the ordinary path (see + get_wrappings): zero expression elements bind the name to an empty + Sequence[], exactly one binds the name to that RAW element itself + (never wrapped), and two or more bind the name to + Sequence[elem1, ..., elemN] -- this matters for typed patterns + like x_Integer nested inside a rule using this binding downstream, + which check the head of whatever they're handed. + """ + + def __init__( + self, + expression: Expression, + attributes: int, + evaluation: Optional[Evaluation] = None, + ): + super().__init__(expression, attributes, evaluation) + assert len(self.elements) == 1 + self.attributes = attributes + + def match(self, expression: BaseElement, pattern_context: dict): + """ + The one and only match algorithm for this shape: check the + head, then hand the expression's entire element tuple -- + wrapped exactly the way get_wrappings would wrap it -- to the + single wrapped Pattern[name, Blank(Null)Sequence[...]] element, + and let its own match() (which only ever needs `yield_func` + and `vars_dict` from pattern_context -- see + composite.py:Pattern.match and basic.py:BlankSequence.match / + BlankNullSequence.match) do the actual type check and binding. + """ + from mathics.core.atoms.associations import Association + + evaluation = pattern_context["evaluation"] + yield_func = pattern_context["yield_func"] + vars_dict = pattern_context["vars_dict"] + + evaluation.check_stopped() + + if isinstance(expression, Association): + expression = expression.expr + + if not isinstance(expression, Expression): + return + + # Same head-identity fast check as FixedBlankTupleExpressionPattern + # (see its match() for why does_match() would be strictly more + # expensive here): self.head is guaranteed to be an AtomPattern + # wrapping a Symbol by make_expression_pattern's + # isinstance(expr.head, Symbol) check, done before this class + # is ever constructed. + self_head = self.head + if ( + not isinstance(self_head, AtomPattern) + or expression.get_head() is not self_head.atom + ): + return + + expr_elements = expression.elements + if len(expr_elements) == 1: + # Bind the raw element itself, not Sequence[element] -- see + # class docstring and get_wrappings' own len(items) == 1 + # special case, which this replicates. + matched = expr_elements[0] + else: + # Zero elements (empty Sequence[], only reachable when the + # wrapped pattern is a BlankNullSequence -- a BlankSequence + # will simply fail to match an empty Sequence[], same as + # it fails today) or two-or-more: wrap exactly as + # ExpressionPattern._yield_sequence_wrappings would. + matched = Expression(SymbolSequence, *expr_elements) + matched.pattern_sequence = True + + self.elements[0].match( + matched, + { + "yield_func": yield_func, + "vars_dict": vars_dict, + "evaluation": evaluation, + "fully": True, + }, + ) diff --git a/mathics/core/rules.py b/mathics/core/rules.py index 0a0b83e5a6..68e9a89796 100644 --- a/mathics/core/rules.py +++ b/mathics/core/rules.py @@ -145,6 +145,18 @@ def __init__( attributes=attributes, ) + def _resolve(self, evaluation: Evaluation): + """ + Convert DeferredExpressionPattern in the specific + kind of pattern according to the attributes given + by `evaluation`. + """ + from mathics.core.pattern.deferred import DeferredExpressionPattern + + pattern = self.pattern + if isinstance(pattern, DeferredExpressionPattern): + self.pattern = pattern._resolve(evaluation) + def apply( self, expression: BaseElement, diff --git a/test/timings/test_regressions.py b/test/timings/test_regressions.py index 096000980b..fec80c12ed 100644 --- a/test/timings/test_regressions.py +++ b/test/timings/test_regressions.py @@ -30,6 +30,7 @@ ("1/2+3/4", 100, 20), ("5^3", 100, 20), ("10^100", 100, 20), + ("Subtract[2,1]", 100, 20), ], # Assign. This should come before any other test. "assign": [ From c748b11620c7820a7acbf87971a745a556f16fbe Mon Sep 17 00:00:00 2001 From: Juan Mauricio Matera Date: Sun, 6 Sep 2026 01:25:26 -0300 Subject: [PATCH 2/9] fix Verbatim patterns --- mathics/builtin/assignments/clear.py | 6 +- mathics/builtin/patterns/basic.py | 5 + mathics/builtin/patterns/composite.py | 35 +++- mathics/builtin/patterns/restrictions.py | 5 +- mathics/core/assignment.py | 61 ------- mathics/core/definitions.py | 192 +++++++------------- mathics/core/expression.py | 7 +- mathics/core/pattern/base.py | 78 +++++--- mathics/eval/assignments/__init__.py | 7 +- mathics/eval/assignments/assignment.py | 20 +- test/builtin/assignments/test_assignment.py | 31 ++++ test/core/test_definitions.py | 67 ++++++- 12 files changed, 284 insertions(+), 230 deletions(-) diff --git a/mathics/builtin/assignments/clear.py b/mathics/builtin/assignments/clear.py index 13c9354799..af5bed9fc5 100644 --- a/mathics/builtin/assignments/clear.py +++ b/mathics/builtin/assignments/clear.py @@ -25,6 +25,7 @@ SymbolFailed, SymbolOptions, ) +from mathics.eval.assignments import get_lookup_reference_name class Clear(Builtin): @@ -268,7 +269,7 @@ def eval(self, expr, evaluation): empty = [] evaluation.definitions.set_values(symbol, expr.get_head_name(), empty) return SymbolNull - name = expr.get_lookup_name() + name = get_lookup_reference_name(expr) if not name: evaluation.message("Unset", "usraw", expr) return SymbolFailed @@ -300,8 +301,6 @@ class TagUnset(PostfixOperator):
removes any value belonging to the patter $patt$ from $f$. - - Let's consider we define an UpValue for a symbol g: >> Sin[g[x_]]^:=Sing[x]; in a way that @@ -325,7 +324,6 @@ class TagUnset(PostfixOperator): def eval_general(self, tag, expr, evaluation): "TagUnset[tag_, expr_]" - if not isinstance(tag, Symbol): evaluation.message("TagSet", "sym", tag) return SymbolNull diff --git a/mathics/builtin/patterns/basic.py b/mathics/builtin/patterns/basic.py index 3e6f447166..ccf42523a0 100644 --- a/mathics/builtin/patterns/basic.py +++ b/mathics/builtin/patterns/basic.py @@ -44,6 +44,11 @@ def __new__(cls, *args, **kwargs): cls._instance = super().__new__(cls, *args, **kwargs) return cls._instance + def get_tag_position(self, target: Symbol) -> OptionalType[str]: + if self.target_head is target: + return "downvalues" + return None + def init( self, expr: Expression, evaluation: OptionalType[Evaluation] = None ) -> None: diff --git a/mathics/builtin/patterns/composite.py b/mathics/builtin/patterns/composite.py index 9be501d67d..8e831de79f 100644 --- a/mathics/builtin/patterns/composite.py +++ b/mathics/builtin/patterns/composite.py @@ -21,9 +21,10 @@ ) from mathics.core.list import ListExpression from mathics.core.pattern import BasePattern, StopGenerator -from mathics.core.symbols import SymbolList +from mathics.core.symbols import Atom, Symbol, SymbolList from mathics.core.systemsymbols import ( SymbolBlank, + SymbolN, SymbolRule, SymbolRuleDelayed, SymbolVerbatim, @@ -209,6 +210,9 @@ def init( super().init(expr, evaluation=evaluation) self.pattern = BasePattern.create(expr.elements[0], evaluation=evaluation) + def get_tag_position(self, target: Symbol) -> OptionalType[str]: + return self.pattern.get_tag_position(target) + def match(self, expression: Expression, pattern_context: dict): # for new_vars_dict, rest in self.pattern.match( # expression, vars_dict, evaluation): @@ -475,6 +479,9 @@ def init( def __repr__(self): return "" % repr(self.pattern) + def get_tag_position(self, target: Symbol) -> OptionalType[str]: + return self.pattern.get_tag_position(target) + def get_match_count( self, vars_dict: OptionalType[dict] = None ) -> Union[int, tuple]: @@ -731,7 +738,31 @@ def init( self, expr: Expression, evaluation: OptionalType[Evaluation] = None ) -> None: super().init(expr, evaluation=evaluation) - self.content = expr.elements[0] + elements = expr.elements + self.content = elements[0] if len(elements) == 1 else None + + def get_tag_position(self, target: Symbol) -> OptionalType[str]: + content = self.content + if content is None: + return None + + if content is target: + return "ownvalues" + if isinstance(content, Atom): + return None + if content.has_form(target, None): + return "downvalues" + if ( + content.has_form(SymbolN, 2) + and content.elements[0].get_lookup_name() == target.get_name() + ): + return "nvalues" + if content.get_lookup_name() == target.get_name(): + return "subvalues" + for element in content.elements: + if element is target or element.has_form(target, None): + return "upvalues" + return None def match(self, expression: Expression, pattern_context: dict): """Match with Verbatim Pattern""" diff --git a/mathics/builtin/patterns/restrictions.py b/mathics/builtin/patterns/restrictions.py index a33ce94d90..02316233d9 100644 --- a/mathics/builtin/patterns/restrictions.py +++ b/mathics/builtin/patterns/restrictions.py @@ -16,7 +16,7 @@ PATTERN_SORT_KEY_PATTERNTEST, ) from mathics.core.pattern import BasePattern -from mathics.core.symbols import Atom, SymbolTrue +from mathics.core.symbols import Atom, Symbol, SymbolTrue # This tells documentation how to sort this module sort_order = "mathics.builtin.rules-and-patterns.restrictions" @@ -67,6 +67,9 @@ def init( # else: self.pattern = BasePattern.create(expr.elements[0], evaluation=evaluation) + def get_tag_position(self, target: Symbol) -> OptionalType[str]: + return self.pattern.get_tag_position(target) + def match(self, expression: Expression, pattern_context: dict): """Match with Condition pattern""" # for new_vars_dict, rest in self.pattern.match(expression, vars_dict, diff --git a/mathics/core/assignment.py b/mathics/core/assignment.py index d273518c7a..c429529b90 100644 --- a/mathics/core/assignment.py +++ b/mathics/core/assignment.py @@ -17,13 +17,10 @@ from mathics.core.rules import RewriteRule from mathics.core.symbols import Atom, Symbol, SymbolList from mathics.core.systemsymbols import ( - SymbolAnd, SymbolBlank, - SymbolCondition, SymbolFormat, SymbolHoldPattern, SymbolOptionValue, - SymbolPart, SymbolPattern, SymbolRuleDelayed, ) @@ -182,34 +179,6 @@ def is_protected(tag: str, definitions: Definitions) -> bool: return bool(A_PROTECTED & definitions.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[stripped_lhs, tst]) - with `stripped_lhs` the result of stripping all the conditions from lhs. - - * if ``stripped_lhs`` 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 stripped_lhs. - """ - cond = None - if lhs.get_head() is SymbolCondition: - lhs, cond = unroll_conditions(lhs) - - lookup_name = lhs.get_lookup_name() - # In WMA, before the assignment, the elements of the (stripped) LHS are evaluated. - if isinstance(lhs, Expression) and lhs.get_head() not in (SymbolList, SymbolPart): - lhs = lhs.evaluate_elements(evaluation) - # If there was a conditional expression, rebuild it with the processed lhs - if cond: - lhs = Expression(cond.get_head(), lhs, cond.elements[1]) - return lhs, lookup_name - - def pop_reference_head(lhs: Expression, lhs_reference: BaseElement): """ Convert expressions of the form @@ -311,36 +280,6 @@ def rejected_because_protected( return False -def unroll_conditions(lhs: BaseElement) -> Tuple[BaseElement, Optional[Expression]]: - """ - If `lhs` is a nested `Condition` expression, - gather all the conditions in a single one, and return a tuple - with the `lhs` stripped from the conditions and the shallow condition. - If there is no condition, return the `lhs` and `None`. - """ - if isinstance(lhs, Symbol): - return lhs, None - - name, lhs_elements = lhs.get_head_name(), lhs.get_elements() - conditions = [] - # This handles the case of many successive conditions: - # f[x_]/; cond1 /; cond2 ... -> f[x_]/; And[cond1, cond2, ...] - while name == "System`Condition" and len(lhs_elements) == 2: - conditions.append(lhs_elements[1]) - lhs = lhs_elements[0] - if isinstance(lhs, Atom): - break - name, lhs_elements = lhs.get_head_name(), lhs.get_elements() - if len(conditions) == 0: - return lhs, None - - condition: BaseElement = ( - Expression(SymbolAnd, *conditions) if len(conditions) > 1 else conditions[0] - ) - condition = Expression(SymbolCondition, lhs, condition) - return lhs, condition - - def unroll_patterns( lhs: BaseElement, rhs: BaseElement, evaluation: Evaluation ) -> Tuple[BaseElement, BaseElement]: diff --git a/mathics/core/definitions.py b/mathics/core/definitions.py index 19efc539fc..207000c6f3 100644 --- a/mathics/core/definitions.py +++ b/mathics/core/definitions.py @@ -18,13 +18,17 @@ from mathics.core.attributes import A_NO_ATTRIBUTES from mathics.core.convert.expression import to_mathics_list from mathics.core.element import BaseElement, fully_qualified_symbol_name +from mathics.core.pattern import BasePattern from mathics.core.rules import BaseRule, RewriteRule from mathics.core.symbols import Atom, Symbol, SymbolList, strip_context from mathics.core.systemsymbols import ( + BLANK_PATTERN_HEADS, SymbolCondition, SymbolDirectedInfinity, SymbolHoldPattern, - SymbolPatternTest, + SymbolN, + SymbolPattern, + SymbolVerbatim, ) from mathics.core.util import canonic_filename from mathics.settings import ROOT_DIR @@ -63,6 +67,7 @@ def __init__( is_numeric: bool = False, ) -> None: self.name = name + self.symbol = Symbol(name) rules_dict = rules_dict or {} self.ownvalues = rules_dict.get("ownvalues", []) self.downvalues = rules_dict.get("downvalues", []) @@ -102,7 +107,7 @@ def add_rule_at(self, rule: BaseRule, position: str) -> bool: def add_rule(self, rule: BaseRule) -> bool: """Add a rule. The position is automatically determined.""" - pos = get_tag_position(rule.pattern.expr, self.name) + pos = get_tag_position(rule.pattern.expr, self.symbol) if pos: return self.add_rule_at(rule, pos) return False @@ -118,7 +123,7 @@ def remove_format_rule(self, lhs: BaseElement, form: str) -> bool: def remove_rule(self, lhs: BaseElement) -> bool: """Remove a rule""" - position = get_tag_position(lhs, self.name) + position = get_tag_position(lhs, self.symbol) if position: values = self.get_values_list(position) for index, existing in enumerate(values): @@ -875,141 +880,80 @@ def _valuesname(name: str) -> str: return name[7:].lower() -def get_tag_position(pattern: BaseElement, name: str) -> Optional[str]: +def get_tag_position( + pattern: BaseElement | BasePattern, target: Symbol +) -> Optional[str]: """ Determine the position of a pattern in - the definition of the symbol ``name`` + the definition of the symbol ``target`` """ - blanks = ( - "System`Blank", - "System`BlankSequence", - "System`BlankNullSequence", - ) - - def strip_pattern_name_and_condition(pat) -> BaseElement: - """ - In ``Pattern[name_, pattern_]`` and - ``Condition[pattern_, cond_]`` - the tag is determined by pat. - This function strips it to ensure that - ``pat`` does not have that form. - """ - - # Is "pat" as ExpressionPattern or an AtomPattern? - # Note: the below test could also be on ExpressionPattern or - # AtomPattern, but using hasattr is more flexible if more - # kinds of patterns are added. - if not hasattr(pat, "head"): - return pat - - if hasattr(pat, "elements"): - # 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.has_form((SymbolCondition, SymbolPatternTest), 2): - return strip_pattern_name_and_condition(pat.elements[0]) - if pat.has_form(SymbolHoldPattern, 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": - if len(pat.elements) == 2: - return strip_pattern_name_and_condition(pat.elements[1]) - - return pat - - def is_pattern_a_kind_of(pattern: BaseElement, pattern_name: str) -> bool: - """ - Returns `True` if `pattern` or any of its alternates is a - pattern with name `pattern_name` and `False` otherwise.""" - - if pattern_name == pattern.get_lookup_name(): - return True - - # Try again after stripping Pattern and Condition wrappers: - head = strip_pattern_name_and_condition(pattern.get_head()) - head_name = head.get_lookup_name() - if pattern_name == head_name: - return True - - # The head is of the form ``_SymbolName|__SymbolName|___SymbolName`` - # If name matches with SymbolName, then it is a kind of: - if head_name in blanks: - if isinstance(head, Symbol): - return False - assert hasattr(head, "elements") - sub_elements = head.elements - if len(sub_elements) == 1: - head_name = head.elements[0].get_name() - if head_name == pattern_name: - return True - return False - - # If pattern is a Symbol, and coincides with - # name, it is an ownvalue: - - if pattern.get_name() == name: + assert isinstance(target, Symbol), "target must be a symbol" + # If pattern is a PatternObject, use the method. + # In Set* and when Builtin symbols are loaded, + # a pattern object is available. + # The reason to handle BaseElement is that + # `Unset` uses it without compiling the pattern. + # We could compile the pattern for that case too + # but it would introduce some overhead. + if isinstance(pattern, BasePattern): + return pattern.get_tag_position(target) + # pattern is a BaseElement + if pattern is target: return "ownvalues" - # If pattern is an ``Atom``, does not have - # a position if isinstance(pattern, Atom): return None - - # The pattern is an Expression. - head_name = pattern.get_head_name() - # If the name is the head name, is a downvalue: - if head_name == name: - return "downvalues" - - # Handle special cases - if head_name == "System`N": - if len(pattern.get_elements()) == 2: + if pattern.has_form(SymbolN, 2): + tag = get_tag_position(pattern.get_elements()[0], target) + if tag in ( + None, + "upvalues", + ): + return None + return "nvalues" + if pattern.has_form(SymbolHoldPattern, 1): + return get_tag_position(pattern.get_elements()[0], target) + if pattern.has_form(SymbolPattern, 2): + return get_tag_position(pattern.get_elements()[1], target) + if pattern.has_form(SymbolCondition, 2): + return get_tag_position(pattern.get_elements()[0], target) + if pattern.has_form(BLANK_PATTERN_HEADS, 1): + if target is pattern.get_elements()[0]: + return "downvalues" + if pattern.has_form(SymbolVerbatim, 1): + content = pattern.get_elements()[0] + if target is content: + return "ownvalues" + if isinstance(content, Atom): + return None + if content.has_form(target, None): + return "downvalues" + if ( + content.has_form(SymbolN, 2) + and content.get_elements()[0].get_lookup_name() == target.get_name() + ): return "nvalues" + if content.get_lookup_name() == target.get_name(): + return "subvalues" + for element in content.get_elements(): + if element is target or element.has_form(target, None): + return "upvalues" + return None - # The pattern has the form `_SymbolName | __SymbolName | ___SymbolName` - # Then it only can be a downvalue - if head_name in blanks: - elements = pattern.get_elements() - if len(elements) == 1: - head_name = elements[0].get_name() - return "downvalues" if head_name == name else None - - # TODO: Consider process format_values + head = pattern.get_head() - if head_name != "": - # Check - strip_pattern = strip_pattern_name_and_condition(pattern) - if strip_pattern is not pattern: - return get_tag_position(strip_pattern, name) + if head is target: + return "downvalues" - # The head is not a symbol. Is pattern is "name" kind of pattern? - if is_pattern_a_kind_of(pattern, name): + head_pos = get_tag_position(head, target) + if head_pos == "ownvalues": + return "downvalues" + if head_pos in ("downvalues", "subvalues"): return "subvalues" - # If we are here, pattern is not an Ownvalue, DownValue, SubValue or NValue - # Let's check the elements for UpValues for element in pattern.get_elements(): - lookup_name = element.get_lookup_name() - if lookup_name == name: + elem_tag = get_tag_position(element, target) + if elem_tag in ("ownvalues", "downvalues", "subvalues"): return "upvalues" - - # Strip Pattern and Condition wrappers and check again - if lookup_name in ( - "System`Condition", - "System`Pattern", - ): - element = strip_pattern_name_and_condition(element) - lookup_name = element.get_lookup_name() - if lookup_name == name: - return "upvalues" - # Check if one of the elements is not a "Blank" - - if element.get_head_name() in blanks: - sub_elements = element.get_elements() - if len(sub_elements) == 1: - if sub_elements[0].get_name() == name: - return "upvalues" - # ``pattern`` does not have a tag position in the Definition return None diff --git a/mathics/core/expression.py b/mathics/core/expression.py index f3eeedafcb..5767b9e3a7 100644 --- a/mathics/core/expression.py +++ b/mathics/core/expression.py @@ -1907,9 +1907,10 @@ def thread(self, evaluation, head=None) -> tuple[bool, "Expression"]: def is_numeric(self, evaluation=None) -> bool: if evaluation: - if not A_NUMERIC_FUNCTION & evaluation.definitions.get_attributes( - self._head.get_name() - ): + name = self._head.get_name() + if name == "": + return False + if not A_NUMERIC_FUNCTION & evaluation.definitions.get_attributes(name): return False for element in self._elements: if not element.is_numeric(evaluation): diff --git a/mathics/core/pattern/base.py b/mathics/core/pattern/base.py index 6bd1509cd5..7b1ee82cf2 100644 --- a/mathics/core/pattern/base.py +++ b/mathics/core/pattern/base.py @@ -30,38 +30,12 @@ BASIC_EXPRESSION_PATTERN_SORT_KEY, END_OF_LIST_PATTERN_SORT_KEY, ) -from mathics.core.symbols import Atom, Symbol, symbol_set -from mathics.core.systemsymbols import ( - SymbolAlternatives, - SymbolBlank, - SymbolBlankNullSequence, - SymbolBlankSequence, - SymbolCondition, - SymbolOptional, - SymbolOptionsPattern, - SymbolPattern, - SymbolPatternTest, - SymbolRepeated, - SymbolRepeatedNull, - SymbolSequence, -) +from mathics.core.symbols import Atom, Symbol +from mathics.core.systemsymbols import SYSTEM_SYMBOLS_PATTERNS, SymbolN, SymbolSequence if TYPE_CHECKING: from mathics.core.builtin import PatternObject -SYSTEM_SYMBOLS_PATTERNS = symbol_set( - SymbolAlternatives, - SymbolBlank, - SymbolBlankNullSequence, - SymbolBlankSequence, - SymbolCondition, - SymbolOptional, - SymbolOptionsPattern, - SymbolPattern, - SymbolPatternTest, - SymbolRepeated, - SymbolRepeatedNull, -) pattern_objects: Dict[str, Type["PatternObject"]] = {} @@ -262,6 +236,26 @@ def get_sequence(self): """The sequence of elements in the expression""" return self.expr.get_sequence() + def get_tag_position(self, target: Symbol) -> Optional[str]: + """ + Return the position relative to the Symbol `target`. + When the position cannot be decided, returns `None`. + + If a pattern matches with a Symbol, the position is + "own" if the symbol coincides with target. + + For expressions of the form head_Symbol[...], the + position is "down" if head coincides with target, + and "up" if target coincides with the head of one of + the elements. + If the expression is of the form head_Symbol[...][...] + the position is "sub" if the lookup name coincides with + the name of the target. + Special cases like `Condition[...]`, `Verbatim[...]` + or `HoldPattern[]` are handled by the subclasses. + """ + return None + def has_form( self, heads: Sequence[str | Symbol] | str | Symbol, @@ -385,6 +379,11 @@ def _build_pattern_sort_key(self) -> tuple: def __repr__(self): return f"" + def get_tag_position(self, target: Symbol) -> Optional[str]: + if target is self.atom: + return "ownvalues" + return None + def match_symbol( self, expression: BaseElement, @@ -487,6 +486,29 @@ def filter_elements(self, head_name: str): element for element in self.elements if element.get_head_name() == head_name ] + def get_tag_position(self, target: Symbol) -> Optional[str]: + # Special case: Nvalues + if self.expr.has_form(SymbolN, 2): + tag = self.elements[0].get_tag_position(target) + if tag in ( + None, + "upvalues", + ): + return None + return "nvalues" + + head = self.head + head_pos = head.get_tag_position(target) + if head_pos == "ownvalues": + return "downvalues" + if head_pos in ("downvalues", "subvalues"): + return "subvalues" + for element in self.elements: + elem_tag = element.get_tag_position(target) + if elem_tag in ("ownvalues", "downvalues", "subvalues"): + return "upvalues" + return None + def __repr__(self): return f"" diff --git a/mathics/eval/assignments/__init__.py b/mathics/eval/assignments/__init__.py index 147ef0a569..1678911294 100644 --- a/mathics/eval/assignments/__init__.py +++ b/mathics/eval/assignments/__init__.py @@ -3,9 +3,14 @@ mathics.builtins.assignments. """ -from mathics.eval.assignments.assignment import ASSIGNMENT_FUNCTION_MAP, eval_assign +from mathics.eval.assignments.assignment import ( + ASSIGNMENT_FUNCTION_MAP, + eval_assign, + get_lookup_reference_name, +) __all__ = [ "ASSIGNMENT_FUNCTION_MAP", "eval_assign", + "get_lookup_reference_name", ] diff --git a/mathics/eval/assignments/assignment.py b/mathics/eval/assignments/assignment.py index 19fc3cfc46..6601bf20a8 100644 --- a/mathics/eval/assignments/assignment.py +++ b/mathics/eval/assignments/assignment.py @@ -38,6 +38,7 @@ valid_context_name, ) from mathics.core.systemsymbols import ( + BLANK_PATTERN_HEADS, SymbolBlank, SymbolBlankNullSequence, SymbolBlankSequence, @@ -118,13 +119,18 @@ def eval_assign( try: # Handle special cases using the lookup name associated to the lhs_reference - lookup_name = lhs_reference_expr.get_lookup_name() + if lhs_reference_expr.has_form(SymbolVerbatim, 1): + lookup_name = lhs_reference_expr.elements[0].get_lookup_name() + else: + lookup_name = lhs_reference_expr.get_lookup_name() assignment_func = ASSIGNMENT_FUNCTION_MAP.get(lookup_name, None) if assignment_func: return assignment_func( op_name, lhs, lhs_reference, rhs, evaluation, tags, upset ) - if isinstance(lhs, Expression) and not lhs.has_form(SymbolHoldPattern, 1): + if isinstance(lhs, Expression) and not lhs.has_form( + (SymbolVerbatim, SymbolHoldPattern), 1 + ): lhs = lhs.evaluate_elements(evaluation) lhs_reference_expr = get_reference_expression(lhs) lhs_reference = ( @@ -1382,7 +1388,12 @@ def get_lookup_reference_name(expr: BaseElement) -> str: if expr.has_form(SymbolVerbatim, 1): # For Verbatim pick the lookup name directly from the expression. return expr.elements[0].get_lookup_name() - if expr.has_form((SymbolBlank, SymbolBlankSequence, SymbolBlankNullSequence), None): + if isinstance(expr, Atom): + return expr.get_lookup_name() + expr_head = expr.head + if expr_head.has_form(SymbolVerbatim, 1): + return expr_head.elements[0].get_lookup_name() + if expr.has_form(BLANK_PATTERN_HEADS, None): if len(expr.elements) == 1: return get_lookup_reference_name(expr.elements[0]) return "" @@ -1484,7 +1495,8 @@ def process_tags_and_upset_allow_custom( # set to its argument. If it does not have arguments (or have many) # skip it. element_name = get_lookup_reference_name(element) - if element_name is not None: + if element_name != "": + assert element_name is not None tags_set.add(element_name) return list(tags_set), lhs_reference_expr diff --git a/test/builtin/assignments/test_assignment.py b/test/builtin/assignments/test_assignment.py index 814d56243e..e9d49c6860 100644 --- a/test/builtin/assignments/test_assignment.py +++ b/test/builtin/assignments/test_assignment.py @@ -746,3 +746,34 @@ def test_regression_of_assignment_issues( hold_expected=hold_expected, expected_messages=messages, ) + + +@pytest.mark.parametrize( + ("str_expr", "str_expected", "msg"), + [ + (None, None, None), + ( + "Verbatim[F][x_Integer]:=3; DownValues[F]", + "{HoldPattern[Verbatim[F][x_Integer]] :> 3}", + None, + ), + ("Verbatim[F][x_Integer]=.; DownValues[F]", "{}", None), + ( + "M[Verbatim[F][u_]]^:=3; UpValues[F]", + "{HoldPattern[M[Verbatim[F][u_]]] :> 3}", + None, + ), + ("F/:M[Verbatim[F][u_]]=.; UpValues[F]", "{}", None), + ( + "Unprotect[N]; Verbatim[N][F[x_Integer],_]:=0.;DownValues[N]", + "{HoldPattern[Verbatim[N][F[x_Integer], _]] :> 0.}", + None, + ), + ], +) +def test_verbatim_assignment(str_expr, str_expected, msg): + check_evaluation( + str_expr, + str_expected, + failure_message=msg, + ) diff --git a/test/core/test_definitions.py b/test/core/test_definitions.py index fb319818cd..00b9c6f3c7 100644 --- a/test/core/test_definitions.py +++ b/test/core/test_definitions.py @@ -7,6 +7,40 @@ from mathics.core.definitions import get_tag_position from mathics.core.parser import parse_builtin_rule +from mathics.core.pattern import BasePattern + + +def load_pattern_objects(): + """Ensure that pattern objects are already loaded""" + from mathics.builtin.patterns import basic, composite, defaults, restrictions + from mathics.core.builtin import PatternObject + from mathics.core.pattern import pattern_objects + + pattern_modules = ( + basic, + composite, + defaults, + restrictions, + ) + + if len(pattern_objects): + return + + for module in pattern_modules: + for name in dir(module): + candidate = getattr(module, name) + if name[0] == "_": + continue + try: + if issubclass(candidate, PatternObject): + full_name = f"System`{name}" + pattern_objects[full_name] = candidate + except: + # Not a class + continue + + +load_pattern_objects() @pytest.mark.parametrize( @@ -48,6 +82,9 @@ ("S[x_, q:A]", "A", "upvalues"), ("S[x_, q:(A[t_]/;t>0)]", "A", "upvalues"), ("A[x_][s[y]]", "s", "upvalues"), + ("N[_A,_]", "A", "nvalues"), + ("N[A[_],_]", "A", "nvalues"), + ("N[F[_A],_]", "A", None), ("DisplayForm[boxexpr_InterpretationBox]", "InterpretationBox", "upvalues"), ( "ToExpression[boxexpr_InterpretationBox, form___]", @@ -56,8 +93,34 @@ ), # Just one argument, must be an upvalue ("N[A[s_]]", "A", "upvalues"), + # Verbatim + ("Verbatim[F][Verbatim[G][x_]]", "F", "downvalues"), + ("Verbatim[F,3][Verbatim[G][x_]]", "F", None), + ("expr:Verbatim[F][Verbatim[G][x_]]", "F", "downvalues"), + ("Verbatim[F][Verbatim[G][x_]]", "G", "upvalues"), + ("Verbatim[F][expr:Verbatim[G][x_]]", "G", "upvalues"), + ("Verbatim[N][Verbatim[G][x_]]", "N", "downvalues"), + ("Verbatim[N][Verbatim[G][x_],_]", "N", "downvalues"), + ("Verbatim[N][Verbatim[G][x_],_]", "G", "upvalues"), + ("Verbatim[Condition][Verbatim[G][x_],_]", "Condition", "downvalues"), + ("Verbatim[Condition][Verbatim[G][x_],_]", "G", "upvalues"), + ("Verbatim[Verbatim][F[x_]]", "Verbatim", "downvalues"), + # HoldPattern + ("HoldPattern[F[Verbatim[G][x_]]]", "F", "downvalues"), + ("expr:HoldPattern[F[Verbatim[G][x_]]]", "F", "downvalues"), + ("HoldPattern[expr:F[Verbatim[G][x_]]]", "F", "downvalues"), + ("HoldPattern[F][HoldPattern[G][x_]]", "G", "upvalues"), + ("HoldPattern[F][HoldPattern[expr:G[x_]]]", "G", "upvalues"), + ("HoldPattern[N[Verbatim[G][x_]]]", "N", "downvalues"), + ("HoldPattern[N][Verbatim[G][x_],_]", "N", "downvalues"), + ("Verbatim[N][HoldPattern[G][x_],_]", "G", "upvalues"), + ("HoldPattern[Condition[Verbatim[G][x_],_]]", "Condition", None), + ("HoldPattern[Condition[Verbatim[G][x_],_]]", "G", "downvalues"), ], ) def test_get_tag_position(pattern_str, tag, position): - pattern = parse_builtin_rule(pattern_str) - assert get_tag_position(pattern, f"System`{tag}") == position + target = parse_builtin_rule(tag) + pattern_expr = parse_builtin_rule(pattern_str) + pattern_pat = BasePattern.create(pattern_expr) + assert get_tag_position(pattern_pat, target) == position + assert get_tag_position(pattern_expr, target) == position From db9279b8d37ed9467d6e2c04d94a0714038c67e0 Mon Sep 17 00:00:00 2001 From: Juan Mauricio Matera Date: Sun, 6 Sep 2026 01:26:45 -0300 Subject: [PATCH 3/9] do not use bare except --- test/core/test_definitions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/core/test_definitions.py b/test/core/test_definitions.py index 00b9c6f3c7..ef6eea254b 100644 --- a/test/core/test_definitions.py +++ b/test/core/test_definitions.py @@ -35,7 +35,7 @@ def load_pattern_objects(): if issubclass(candidate, PatternObject): full_name = f"System`{name}" pattern_objects[full_name] = candidate - except: + except TypeError: # Not a class continue From 397974f41c2380c9b9ab1eb8b028ecd5da215a59 Mon Sep 17 00:00:00 2001 From: Juan Mauricio Matera Date: Sun, 6 Sep 2026 15:23:23 -0300 Subject: [PATCH 4/9] target ->tag --- mathics/core/definitions.py | 42 ++++++++++++++++++------------------- 1 file changed, 20 insertions(+), 22 deletions(-) diff --git a/mathics/core/definitions.py b/mathics/core/definitions.py index 207000c6f3..d5f3cb61d1 100644 --- a/mathics/core/definitions.py +++ b/mathics/core/definitions.py @@ -880,14 +880,12 @@ def _valuesname(name: str) -> str: return name[7:].lower() -def get_tag_position( - pattern: BaseElement | BasePattern, target: Symbol -) -> Optional[str]: +def get_tag_position(pattern: BaseElement | BasePattern, tag: Symbol) -> Optional[str]: """ Determine the position of a pattern in - the definition of the symbol ``target`` + the definition of the symbol ``tag`` """ - assert isinstance(target, Symbol), "target must be a symbol" + assert isinstance(tag, Symbol), "tag must be a symbol" # If pattern is a PatternObject, use the method. # In Set* and when Builtin symbols are loaded, # a pattern object is available. @@ -896,63 +894,63 @@ def get_tag_position( # We could compile the pattern for that case too # but it would introduce some overhead. if isinstance(pattern, BasePattern): - return pattern.get_tag_position(target) + return pattern.get_tag_position(tag) # pattern is a BaseElement - if pattern is target: + if pattern is tag: return "ownvalues" if isinstance(pattern, Atom): return None if pattern.has_form(SymbolN, 2): - tag = get_tag_position(pattern.get_elements()[0], target) - if tag in ( + position = get_tag_position(pattern.get_elements()[0], tag) + if position in ( None, "upvalues", ): return None return "nvalues" if pattern.has_form(SymbolHoldPattern, 1): - return get_tag_position(pattern.get_elements()[0], target) + return get_tag_position(pattern.get_elements()[0], tag) if pattern.has_form(SymbolPattern, 2): - return get_tag_position(pattern.get_elements()[1], target) + return get_tag_position(pattern.get_elements()[1], tag) if pattern.has_form(SymbolCondition, 2): - return get_tag_position(pattern.get_elements()[0], target) + return get_tag_position(pattern.get_elements()[0], tag) if pattern.has_form(BLANK_PATTERN_HEADS, 1): - if target is pattern.get_elements()[0]: + if tag is pattern.get_elements()[0]: return "downvalues" if pattern.has_form(SymbolVerbatim, 1): content = pattern.get_elements()[0] - if target is content: + if tag is content: return "ownvalues" if isinstance(content, Atom): return None - if content.has_form(target, None): + if content.has_form(tag, None): return "downvalues" if ( content.has_form(SymbolN, 2) - and content.get_elements()[0].get_lookup_name() == target.get_name() + and content.get_elements()[0].get_lookup_name() == tag.get_name() ): return "nvalues" - if content.get_lookup_name() == target.get_name(): + if content.get_lookup_name() == tag.get_name(): return "subvalues" for element in content.get_elements(): - if element is target or element.has_form(target, None): + if element is tag or element.has_form(tag, None): return "upvalues" return None head = pattern.get_head() - if head is target: + if head is tag: return "downvalues" - head_pos = get_tag_position(head, target) + head_pos = get_tag_position(head, tag) if head_pos == "ownvalues": return "downvalues" if head_pos in ("downvalues", "subvalues"): return "subvalues" for element in pattern.get_elements(): - elem_tag = get_tag_position(element, target) - if elem_tag in ("ownvalues", "downvalues", "subvalues"): + elem_position = get_tag_position(element, tag) + if elem_position in ("ownvalues", "downvalues", "subvalues"): return "upvalues" return None From b9a0b4adadde2896ba1cdf3fafdaa1fcbcf49944 Mon Sep 17 00:00:00 2001 From: Juan Mauricio Matera Date: Sun, 6 Sep 2026 15:40:37 -0300 Subject: [PATCH 5/9] split assignment.py tests --- .../assignments/test_assign_messages.py | 137 +++++ .../assignments/test_assign_one_identity.py | 50 ++ .../assignments/test_assign_set_and_clear.py | 338 ++++++++++++ .../assignments/test_assign_verbatim_lhs.py | 45 ++ test/builtin/assignments/test_assignment.py | 515 ------------------ 5 files changed, 570 insertions(+), 515 deletions(-) create mode 100644 test/builtin/assignments/test_assign_messages.py create mode 100644 test/builtin/assignments/test_assign_one_identity.py create mode 100644 test/builtin/assignments/test_assign_set_and_clear.py create mode 100644 test/builtin/assignments/test_assign_verbatim_lhs.py diff --git a/test/builtin/assignments/test_assign_messages.py b/test/builtin/assignments/test_assign_messages.py new file mode 100644 index 0000000000..9052e43843 --- /dev/null +++ b/test/builtin/assignments/test_assign_messages.py @@ -0,0 +1,137 @@ +# -*- 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. +""" +# TODO: consider splitting this module into sub-modules. + +from test.helper import check_arg_counts, check_evaluation, session + +import pytest +from mathics_scanner.errors import IncompleteSyntaxError + + +@pytest.mark.parametrize( + ("function_name", "msg_fragment"), + [ + ( + "Set", + "1 or more arguments are", + ), + ( + "SetDelayed", + "1 or more arguments are", + ), + ( + "TagSet", + "2 or more arguments are", + ), + ( + "UpSet", + "1 or more arguments are", + ), + ( + "UpSetDelayed", + "1 or more arguments are", + ), + ], +) +def test_arg_errors(function_name, msg_fragment): + """ """ + check_arg_counts(function_name, msg_fragment) + + +def test_upset(): + """ + Test UpSet[] builtin + """ + check_evaluation( + "a ^= 3", + "a ^= 3", + failure_message="Should not be able to use UpSet on a Symbol", + expected_messages=("Nonatomic expression expected at position 1 in a ^= 3.",), + ) + check_evaluation( + "f[g, a + b, h] ^= 2", + "2", + failure_message="UpSet on a protected value should fail", + expected_messages=("Tag Plus in f[g, a + b, h] is Protected.",), + ) + check_evaluation("UpValues[h]", "{HoldPattern[f[g, a + b, h]] ⧴ 2}") + + +def test_process_assign_other(): + # FIXME: beef up check_evaluation so it allows regexps in matching. + # Then this code would be less fragile. + for prefix in ("", "System`"): + for kind, suffix in ( + ( + "Recursion", + "512; use the MATHICS_MAX_RECURSION_DEPTH environment variable to allow higher limits", + ), + ("Iteration", "Infinity"), + ): + limit = f"${kind}Limit" + check_evaluation(f"{prefix}{limit} = 511", "511") + check_evaluation( + f"{prefix}{limit} = 2", + "2", + expected_messages=( + f"Cannot set {limit} to 2; value must be an integer between 20 and {suffix}.", + ), + ) + check_evaluation(f"{prefix}$ModuleNumber = 3", "3") + check_evaluation( + f"{prefix}$ModuleNumber = -1", + "-1", + expected_messages=( + "Cannot set $ModuleNumber to -1; value must be a positive integer.", + ), + ) + + +@pytest.mark.parametrize( + ["expr", "expect", "assert_msg", "expected_msgs"], + [ + # Trivial cases on protected symbols. We should + # get a message in every situation. + (None, None, None, None), # reset session + ( + "List:=1;", + None, + "assign to protected element", + ("Symbol List is Protected.",), + ), + ( + "HoldPattern[List]:=1;", + None, + "assign to wrapped protected element. Test 1.", + ("Tag List in HoldPattern[List] is Protected.",), + ), + ( + "PatternTest[List, x]:=1;", + None, + "assign to wrapped protected element. Test 2.", + ("Tag List in List ? x is Protected.",), + ), + ( + "Condition[List, x]:=1;", + None, + "assign to wrapped protected element. Test 3.", + ("Tag List in List /; x is Protected.",), + ), + ( + "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_with_messages(expr, expect, assert_msg, expected_msgs): + check_evaluation( + expr, expect, failure_message=assert_msg, expected_messages=expected_msgs + ) diff --git a/test/builtin/assignments/test_assign_one_identity.py b/test/builtin/assignments/test_assign_one_identity.py new file mode 100644 index 0000000000..b6492b8c06 --- /dev/null +++ b/test/builtin/assignments/test_assign_one_identity.py @@ -0,0 +1,50 @@ +# -*- 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. +""" +# TODO: consider splitting this module into sub-modules. + +from test.helper import check_arg_counts, check_evaluation, session + +import pytest +from mathics_scanner.errors import IncompleteSyntaxError + +STR_TEST_SET_WITH_ONE_IDENTITY = """ +SetAttributes[SUNIndex, {OneIdentity}]; +SetAttributes[SUNFIndex, {OneIdentity}]; + +SUNIndex[SUNFIndex[___]]:= + (Print["This error shouldn't be triggered here!"]; + Abort[]); + +SUNFIndex[SUNIndex[___]]:= + (Print["This error shouldn't be triggered here!"]; + Abort[]); + +SUNIndex /: MakeBoxes[SUNIndex[p_], TraditionalForm]:=ToBoxes[p, TraditionalForm]; + +SUNFIndex /: MakeBoxes[SUNFIndex[p_], TraditionalForm]:=ToBoxes[p, TraditionalForm]; +""" + + +def test_setdelayed_oneidentity(): + """ + This test checks the behavior of DelayedSet over + symbols with the attribute OneIdentity. + """ + expr = "" + for line in STR_TEST_SET_WITH_ONE_IDENTITY.split("\n"): + if line in ("", "\n"): + continue + expr = expr + line + try: + check_evaluation( + expr, "Null", to_string_expr=False, to_string_expected=False + ) + expr = "" + except IncompleteSyntaxError: + continue diff --git a/test/builtin/assignments/test_assign_set_and_clear.py b/test/builtin/assignments/test_assign_set_and_clear.py new file mode 100644 index 0000000000..b863b3eef2 --- /dev/null +++ b/test/builtin/assignments/test_assign_set_and_clear.py @@ -0,0 +1,338 @@ +# -*- 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. +""" +# TODO: consider splitting this module into sub-modules. + +from test.helper import check_arg_counts, check_evaluation, session + +import pytest +from mathics_scanner.errors import IncompleteSyntaxError + + +@pytest.mark.parametrize( + ("str_expr", "str_expected", "msg"), + [ + ( + None, + None, + None, + ), # Reset session + ("Attributes[Pi]", "{Constant, Protected, ReadProtected}", None), + ("Unprotect[Pi]; Pi=.; Attributes[Pi]", "{Constant, ReadProtected}", None), + ("Unprotect[Pi];Clear[Pi]; Attributes[Pi]", "{Constant, ReadProtected}", None), + ("Unprotect[Pi];ClearAll[Pi]; Attributes[Pi]", "{}", None), + ("Options[Expand]", "{Modulus ⇾ 0, Trig ⇾ False}", None), + ( + "Unprotect[Expand]; Expand=.; Options[Expand]", + "{Modulus ⇾ 0, Trig ⇾ False}", + None, + ), + ( + "Clear[Expand];Options[Expand]=Join[Options[Expand], {MyOption ⇾ Automatic}]; Options[Expand]", + "{MyOption ⇾ Automatic, Modulus ⇾ 0, Trig ⇾ False}", + "Mathics3 stores options in a dictionary. This is why ``MyOption`` appears first.", + ), + # ( + # "ClearAll[Expand]; Options[Expand]", + # "{}", + # "In WMA, options are erased, including the builtin options", + # ), + (None, None, None), # reset session + # Check over a builtin symbol + ( + "{Pi, Unprotect[Pi];Pi=3;Pi, Clear[Pi];Pi}", + "{Pi, 3, Pi}", + None, + ), + ( + "{Pi, Unprotect[Pi];Pi=3;Pi, ClearAll[Pi];Pi}", + "{Pi, 3, Pi}", + None, + ), + ( + "{Pi, Unprotect[Pi];Pi=3;Pi, Pi = .; Pi}", + "{Pi, 3, Pi}", + None, + ), + # Check over a user defined symbol using Set + ( + "{F[a, b], F=Q; F[a,b], Clear[F]; F[a,b]}", + "{F[a, b], Q[a, b], F[a, b]}", + None, + ), + ( + "{F[a, b], F=Q; F[a,b], ClearAll[F]; F[a,b]}", + "{F[a, b], Q[a, b], F[a, b]}", + None, + ), + ( + "{F[a, b], F=Q; F[a,b], F=.; F[a,b]}", + "{F[a, b], Q[a, b], F[a, b]}", + None, + ), + # Check over a user defined symbol using SetDelayed + ( + "{F[a, b], F[x__]:=H[x]; F[a,b], Clear[F]; F[a,b]}", + "{F[a, b], H[a, b], F[a, b]}", + None, + ), + ( + "{F[a, b], F[x__]:=H[x]; F[a,b], ClearAll[F]; F[a,b]}", + "{F[a, b], H[a, b], F[a, b]}", + None, + ), + ( + None, + None, + None, + ), + ( + "{F[a, b], F[x__]:=H[x]; F[a,b], F=.; F[a,b]}", + "{F[a, b], H[a, b], H[a, b]}", + None, + ), + ( + None, + None, + None, + ), + ( + "{F[a, b], F[x__]:=H[x]; F[a,b], F[x__]=.; F[a,b]}", + "{F[a, b], H[a, b], F[a, b]}", + None, + ), + # Check over a builtin operator + ( + "{a+b, Unprotect[Plus]; Plus=Q; a+b, Plus=.; a+b}", + "{a + b, Q[a, b], a + b}", + None, + ), + ( + "{a+b, Unprotect[Plus]; Plus=Q; a+b, Clear[Plus]; a+b}", + "{a + b, Q[a, b], a + b}", + None, + ), + ( + "{a+b, Unprotect[Plus]; Plus=Q; a+b, ClearAll[Plus]; a+b}", + "{a + b, Q[a, b], a + b}", + None, + ), + (None, None, None), # reset session + (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]]}", + "Arguments on the LHS are evaluated before the assignment in := after F reset", + ), + (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]]}", + "Arguments on the LHS are evaluated before the assignment in ^:= after F reset", + ), + (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 in := after G reset", + ), + (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 in ^:= after G reset", + ), + (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, + None, + ), + ], +) +def test_set_and_clear(str_expr, str_expected, msg): + """ + Test calls to Set, Clear and ClearAll. If + str_expr is None, the session is reset, + in a way that the next test run over a fresh + environment. + """ + check_evaluation( + str_expr, + str_expected, + to_string_expr=True, + to_string_expected=True, + hold_expected=True, + failure_message=msg, + ) + + +@pytest.mark.parametrize( + ("str_expr", "str_expected", "msg"), + [ + (None, None, None), + (r"a=b; a=4; {a, b}", "{4, b}", None), + (None, None, None), + (r"a=b; b=4; {a,b}", "{4, 4}", None), + (None, None, None), + (r"a=b; b=4; Clear[a]; {a,b}", "{a, 4}", None), + (None, None, None), + ("a=b; b=4; Clear[b]; {a, b}", "{b, b}", None), + (None, None, None), + ("F[x_]:=x^2; G[x_]:=F[x]; ClearAll[F]; G[u]", "F[u]", None), + (None, None, None), + ("F[x_]:=G[x]; G[x_]:=x^2; ClearAll[G]; F[u]", "G[u]", None), + (None, None, None), + ( + "F[x_]:=G[x]; H[F[y_]]:=Q[y]; ClearAll[F]; {H[G[5]],H[F[5]]}", + "{Q[5], H[F[5]]}", + "The arguments on the LHS are evaluated before the assignment", + ), + (None, None, None), + ( + "F[x_]:=G[x]; H[F[y_]]^:=Q[y]; ClearAll[F]; {H[G[5]],H[F[5]]}", + "{Q[5], H[F[5]]}", + "The arguments on the LHS are evaluated before the assignment", + ), + (None, None, None), + ( + "F[x_]:=G[x]; H[F[y_]]:=Q[y]; ClearAll[G]; {H[G[5]],H[F[5]]}", + "{Q[5], Q[5]}", + "The arguments on the LHS are evaluated before the assignment", + ), + (None, None, None), + ( + "F[x_]=G[x]; H[F[y_]]^=Q[y]; ClearAll[G]; {H[G[5]],H[F[5]]}", + "{H[G[5]], H[G[5]]}", + "The arguments on the LHS are evaluated before the assignment", + ), + (None, None, None), + ( + ( + "A[x_]=B[x];B[x_]=F[x];F[x_]=G[x];" + "H[A[y_]]=Q[y]; ClearAll[F];" + "{H[A[5]],H[B[5]],H[F[5]],H[G[5]]}" + ), + "{H[F[5]], H[F[5]], H[F[5]], Q[5]}", + "The arguments on the LHS are completely evaluated before the assignment", + ), + (None, None, None), + ( + "F[x_]=G[x];N[F[x_]]=x^2;ClearAll[F];{N[F[2]],N[G[2]]}", + "{F[2.], 4.}", + "Assign N rule", + ), + ], +) +def test_set_and_clear_to_fix(str_expr, str_expected, msg): + """ + Test calls to Set, Clear and ClearAll. If + str_expr is None, the session is reset, + in a way that the next test run over a fresh + environment. + """ + check_evaluation( + str_expr, + str_expected, + to_string_expr=True, + to_string_expected=True, + hold_expected=True, + failure_message=msg, + ) + + +@pytest.mark.parametrize( + ("str_expr", "str_expected", "message", "out_msgs"), + [ + ("Pi=4", "4", "Trying to set a protected symbol", ("Symbol Pi is Protected.",)), + ( + "Clear[Pi]", + "Null", + "Trying to clear a protected symbol", + ("Symbol Pi is Protected.",), + ), + ( + "Unprotect[$ContextPath];Clear[$Context]", + "Null", + "Clearing $Context", + ("Special symbol $Context cannot be cleared.",), + ), + ( + "Unprotect[$ContextPath];Clear[$ContextPath]", + "Null", + "Clearing $ContextPath", + ("Special symbol $ContextPath cannot be cleared.",), + ), + ( + "A=1; B=2; Clear[A, $Context, B];{A,$Context,B}", + "{A, Global`, B}", + "This clears A and B, but not $Context", + ("Special symbol $Context cannot be cleared.",), + ), + ( + "A=1; B=2; ClearAll[A, $Context, B];{A,$Context,B}", + "{A, Global`, B}", + "This clears A and B, but not $Context", + ("Special symbol $Context cannot be cleared.",), + ), + ( + "A=1; B=2; ClearAll[A, $ContextPath, B];{A,$ContextPath,B}", + "{A, {System`, Global`}, B}", + "This clears A and B, but not $ContextPath", + ("Special symbol $ContextPath cannot be cleared.",), + ), + ( + "A=1; B=2; ClearAll[A, $ContextPath, B];{A,$ContextPath,B}", + "{A, {System`, Global`}, B}", + "This clears A and B, but not $ContextPath", + ("Special symbol $ContextPath cannot be cleared.",), + ), + # `This test was in mathics.builtin.arithmetic.Sum`. It does not + # belong there. On the other hand, this is something to check at the level of the interpreter, + # and is not related to Sum or Set. + # ("a=Sum[x^k*Sum[y^l,{l,0,4}],{k,0,4}]]", "None" , "syntax error", + # ('"a=Sum[x^k*Sum[y^l,{l,0,4}],{k,0,4}]" cannot be followed by "]" (line 1 of "").',)) + ], +) +def test_set_and_clear_messages(str_expr, str_expected, message, out_msgs): + session.evaluate("ClearAll[a, b, A, B, F, H, Q]") + check_evaluation( + str_expr, + str_expected, + to_string_expr=True, + to_string_expected=True, + hold_expected=True, + failure_message=message, + expected_messages=out_msgs, + ) diff --git a/test/builtin/assignments/test_assign_verbatim_lhs.py b/test/builtin/assignments/test_assign_verbatim_lhs.py new file mode 100644 index 0000000000..4f6b0e481c --- /dev/null +++ b/test/builtin/assignments/test_assign_verbatim_lhs.py @@ -0,0 +1,45 @@ +# -*- 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. +""" +# TODO: consider splitting this module into sub-modules. + +from test.helper import check_arg_counts, check_evaluation, session + +import pytest +from mathics_scanner.errors import IncompleteSyntaxError + + +@pytest.mark.parametrize( + ("str_expr", "str_expected", "msg"), + [ + (None, None, None), + ( + "Verbatim[F][x_Integer]:=3; DownValues[F]", + "{HoldPattern[Verbatim[F][x_Integer]] :> 3}", + None, + ), + ("Verbatim[F][x_Integer]=.; DownValues[F]", "{}", None), + ( + "M[Verbatim[F][u_]]^:=3; UpValues[F]", + "{HoldPattern[M[Verbatim[F][u_]]] :> 3}", + None, + ), + ("F/:M[Verbatim[F][u_]]=.; UpValues[F]", "{}", None), + ( + "Unprotect[N]; Verbatim[N][F[x_Integer],_]:=0.;DownValues[N]", + "{HoldPattern[Verbatim[N][F[x_Integer], _]] :> 0.}", + None, + ), + ], +) +def test_verbatim_assignment(str_expr, str_expected, msg): + check_evaluation( + str_expr, + str_expected, + failure_message=msg, + ) diff --git a/test/builtin/assignments/test_assignment.py b/test/builtin/assignments/test_assignment.py index e9d49c6860..16ea2bae4c 100644 --- a/test/builtin/assignments/test_assignment.py +++ b/test/builtin/assignments/test_assignment.py @@ -14,55 +14,6 @@ from mathics_scanner.errors import IncompleteSyntaxError -@pytest.mark.parametrize( - ("function_name", "msg_fragment"), - [ - ( - "Set", - "1 or more arguments are", - ), - ( - "SetDelayed", - "1 or more arguments are", - ), - ( - "TagSet", - "2 or more arguments are", - ), - ( - "UpSet", - "1 or more arguments are", - ), - ( - "UpSetDelayed", - "1 or more arguments are", - ), - ], -) -def test_arg_errors(function_name, msg_fragment): - """ """ - check_arg_counts(function_name, msg_fragment) - - -def test_upset(): - """ - Test UpSet[] builtin - """ - check_evaluation( - "a ^= 3", - "a ^= 3", - failure_message="Should not be able to use UpSet on a Symbol", - expected_messages=("Nonatomic expression expected at position 1 in a ^= 3.",), - ) - check_evaluation( - "f[g, a + b, h] ^= 2", - "2", - failure_message="UpSet on a protected value should fail", - expected_messages=("Tag Plus in f[g, a + b, h] is Protected.",), - ) - check_evaluation("UpValues[h]", "{HoldPattern[f[g, a + b, h]] ⧴ 2}") - - def test_order(): check_evaluation(None, None) # Reset session check_evaluation( @@ -70,401 +21,10 @@ def test_order(): ) -STR_TEST_SET_WITH_ONE_IDENTITY = """ -SetAttributes[SUNIndex, {OneIdentity}]; -SetAttributes[SUNFIndex, {OneIdentity}]; - -SUNIndex[SUNFIndex[___]]:= - (Print["This error shouldn't be triggered here!"]; - Abort[]); - -SUNFIndex[SUNIndex[___]]:= - (Print["This error shouldn't be triggered here!"]; - Abort[]); - -SUNIndex /: MakeBoxes[SUNIndex[p_], TraditionalForm]:=ToBoxes[p, TraditionalForm]; - -SUNFIndex /: MakeBoxes[SUNFIndex[p_], TraditionalForm]:=ToBoxes[p, TraditionalForm]; -""" - - -def test_setdelayed_oneidentity(): - """ - This test checks the behavior of DelayedSet over - symbols with the attribute OneIdentity. - """ - expr = "" - for line in STR_TEST_SET_WITH_ONE_IDENTITY.split("\n"): - if line in ("", "\n"): - continue - expr = expr + line - try: - check_evaluation( - expr, "Null", to_string_expr=False, to_string_expected=False - ) - expr = "" - except IncompleteSyntaxError: - continue - - -@pytest.mark.parametrize( - ("str_expr", "str_expected", "msg"), - [ - ( - None, - None, - None, - ), # Reset session - ("Attributes[Pi]", "{Constant, Protected, ReadProtected}", None), - ("Unprotect[Pi]; Pi=.; Attributes[Pi]", "{Constant, ReadProtected}", None), - ("Unprotect[Pi];Clear[Pi]; Attributes[Pi]", "{Constant, ReadProtected}", None), - ("Unprotect[Pi];ClearAll[Pi]; Attributes[Pi]", "{}", None), - ("Options[Expand]", "{Modulus ⇾ 0, Trig ⇾ False}", None), - ( - "Unprotect[Expand]; Expand=.; Options[Expand]", - "{Modulus ⇾ 0, Trig ⇾ False}", - None, - ), - ( - "Clear[Expand];Options[Expand]=Join[Options[Expand], {MyOption ⇾ Automatic}]; Options[Expand]", - "{MyOption ⇾ Automatic, Modulus ⇾ 0, Trig ⇾ False}", - "Mathics3 stores options in a dictionary. This is why ``MyOption`` appears first.", - ), - # ( - # "ClearAll[Expand]; Options[Expand]", - # "{}", - # "In WMA, options are erased, including the builtin options", - # ), - (None, None, None), # reset session - # Check over a builtin symbol - ( - "{Pi, Unprotect[Pi];Pi=3;Pi, Clear[Pi];Pi}", - "{Pi, 3, Pi}", - None, - ), - ( - "{Pi, Unprotect[Pi];Pi=3;Pi, ClearAll[Pi];Pi}", - "{Pi, 3, Pi}", - None, - ), - ( - "{Pi, Unprotect[Pi];Pi=3;Pi, Pi = .; Pi}", - "{Pi, 3, Pi}", - None, - ), - # Check over a user defined symbol using Set - ( - "{F[a, b], F=Q; F[a,b], Clear[F]; F[a,b]}", - "{F[a, b], Q[a, b], F[a, b]}", - None, - ), - ( - "{F[a, b], F=Q; F[a,b], ClearAll[F]; F[a,b]}", - "{F[a, b], Q[a, b], F[a, b]}", - None, - ), - ( - "{F[a, b], F=Q; F[a,b], F=.; F[a,b]}", - "{F[a, b], Q[a, b], F[a, b]}", - None, - ), - # Check over a user defined symbol using SetDelayed - ( - "{F[a, b], F[x__]:=H[x]; F[a,b], Clear[F]; F[a,b]}", - "{F[a, b], H[a, b], F[a, b]}", - None, - ), - ( - "{F[a, b], F[x__]:=H[x]; F[a,b], ClearAll[F]; F[a,b]}", - "{F[a, b], H[a, b], F[a, b]}", - None, - ), - ( - None, - None, - None, - ), - ( - "{F[a, b], F[x__]:=H[x]; F[a,b], F=.; F[a,b]}", - "{F[a, b], H[a, b], H[a, b]}", - None, - ), - ( - None, - None, - None, - ), - ( - "{F[a, b], F[x__]:=H[x]; F[a,b], F[x__]=.; F[a,b]}", - "{F[a, b], H[a, b], F[a, b]}", - None, - ), - # Check over a builtin operator - ( - "{a+b, Unprotect[Plus]; Plus=Q; a+b, Plus=.; a+b}", - "{a + b, Q[a, b], a + b}", - None, - ), - ( - "{a+b, Unprotect[Plus]; Plus=Q; a+b, Clear[Plus]; a+b}", - "{a + b, Q[a, b], a + b}", - None, - ), - ( - "{a+b, Unprotect[Plus]; Plus=Q; a+b, ClearAll[Plus]; a+b}", - "{a + b, Q[a, b], a + b}", - None, - ), - (None, None, None), # reset session - (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]]}", - "Arguments on the LHS are evaluated before the assignment in := after F reset", - ), - (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]]}", - "Arguments on the LHS are evaluated before the assignment in ^:= after F reset", - ), - (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 in := after G reset", - ), - (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 in ^:= after G reset", - ), - (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, - None, - ), - ], -) -def test_set_and_clear(str_expr, str_expected, msg): - """ - Test calls to Set, Clear and ClearAll. If - str_expr is None, the session is reset, - in a way that the next test run over a fresh - environment. - """ - check_evaluation( - str_expr, - str_expected, - to_string_expr=True, - to_string_expected=True, - hold_expected=True, - failure_message=msg, - ) - - -@pytest.mark.parametrize( - ("str_expr", "str_expected", "msg"), - [ - (None, None, None), - (r"a=b; a=4; {a, b}", "{4, b}", None), - (None, None, None), - (r"a=b; b=4; {a,b}", "{4, 4}", None), - (None, None, None), - (r"a=b; b=4; Clear[a]; {a,b}", "{a, 4}", None), - (None, None, None), - ("a=b; b=4; Clear[b]; {a, b}", "{b, b}", None), - (None, None, None), - ("F[x_]:=x^2; G[x_]:=F[x]; ClearAll[F]; G[u]", "F[u]", None), - (None, None, None), - ("F[x_]:=G[x]; G[x_]:=x^2; ClearAll[G]; F[u]", "G[u]", None), - (None, None, None), - ( - "F[x_]:=G[x]; H[F[y_]]:=Q[y]; ClearAll[F]; {H[G[5]],H[F[5]]}", - "{Q[5], H[F[5]]}", - "The arguments on the LHS are evaluated before the assignment", - ), - (None, None, None), - ( - "F[x_]:=G[x]; H[F[y_]]^:=Q[y]; ClearAll[F]; {H[G[5]],H[F[5]]}", - "{Q[5], H[F[5]]}", - "The arguments on the LHS are evaluated before the assignment", - ), - (None, None, None), - ( - "F[x_]:=G[x]; H[F[y_]]:=Q[y]; ClearAll[G]; {H[G[5]],H[F[5]]}", - "{Q[5], Q[5]}", - "The arguments on the LHS are evaluated before the assignment", - ), - (None, None, None), - ( - "F[x_]=G[x]; H[F[y_]]^=Q[y]; ClearAll[G]; {H[G[5]],H[F[5]]}", - "{H[G[5]], H[G[5]]}", - "The arguments on the LHS are evaluated before the assignment", - ), - (None, None, None), - ( - ( - "A[x_]=B[x];B[x_]=F[x];F[x_]=G[x];" - "H[A[y_]]=Q[y]; ClearAll[F];" - "{H[A[5]],H[B[5]],H[F[5]],H[G[5]]}" - ), - "{H[F[5]], H[F[5]], H[F[5]], Q[5]}", - "The arguments on the LHS are completely evaluated before the assignment", - ), - (None, None, None), - ( - "F[x_]=G[x];N[F[x_]]=x^2;ClearAll[F];{N[F[2]],N[G[2]]}", - "{F[2.], 4.}", - "Assign N rule", - ), - ], -) -def test_set_and_clear_to_fix(str_expr, str_expected, msg): - """ - Test calls to Set, Clear and ClearAll. If - str_expr is None, the session is reset, - in a way that the next test run over a fresh - environment. - """ - check_evaluation( - str_expr, - str_expected, - to_string_expr=True, - to_string_expected=True, - hold_expected=True, - failure_message=msg, - ) - - -@pytest.mark.parametrize( - ("str_expr", "str_expected", "message", "out_msgs"), - [ - ("Pi=4", "4", "Trying to set a protected symbol", ("Symbol Pi is Protected.",)), - ( - "Clear[Pi]", - "Null", - "Trying to clear a protected symbol", - ("Symbol Pi is Protected.",), - ), - ( - "Unprotect[$ContextPath];Clear[$Context]", - "Null", - "Clearing $Context", - ("Special symbol $Context cannot be cleared.",), - ), - ( - "Unprotect[$ContextPath];Clear[$ContextPath]", - "Null", - "Clearing $ContextPath", - ("Special symbol $ContextPath cannot be cleared.",), - ), - ( - "A=1; B=2; Clear[A, $Context, B];{A,$Context,B}", - "{A, Global`, B}", - "This clears A and B, but not $Context", - ("Special symbol $Context cannot be cleared.",), - ), - ( - "A=1; B=2; ClearAll[A, $Context, B];{A,$Context,B}", - "{A, Global`, B}", - "This clears A and B, but not $Context", - ("Special symbol $Context cannot be cleared.",), - ), - ( - "A=1; B=2; ClearAll[A, $ContextPath, B];{A,$ContextPath,B}", - "{A, {System`, Global`}, B}", - "This clears A and B, but not $ContextPath", - ("Special symbol $ContextPath cannot be cleared.",), - ), - ( - "A=1; B=2; ClearAll[A, $ContextPath, B];{A,$ContextPath,B}", - "{A, {System`, Global`}, B}", - "This clears A and B, but not $ContextPath", - ("Special symbol $ContextPath cannot be cleared.",), - ), - # `This test was in mathics.builtin.arithmetic.Sum`. It does not - # belong there. On the other hand, this is something to check at the level of the interpreter, - # and is not related to Sum or Set. - # ("a=Sum[x^k*Sum[y^l,{l,0,4}],{k,0,4}]]", "None" , "syntax error", - # ('"a=Sum[x^k*Sum[y^l,{l,0,4}],{k,0,4}]" cannot be followed by "]" (line 1 of "").',)) - ], -) -def test_set_and_clear_messages(str_expr, str_expected, message, out_msgs): - session.evaluate("ClearAll[a, b, A, B, F, H, Q]") - check_evaluation( - str_expr, - str_expected, - to_string_expr=True, - to_string_expected=True, - hold_expected=True, - failure_message=message, - expected_messages=out_msgs, - ) - - def test_assign_list(): check_evaluation("G[x_Real]=x^2; a={G[x]}; {x=1.; a, x=.; a}", "{{1.}, {G[x]}}") -def test_process_assign_other(): - # FIXME: beef up check_evaluation so it allows regexps in matching. - # Then this code would be less fragile. - for prefix in ("", "System`"): - for kind, suffix in ( - ( - "Recursion", - "512; use the MATHICS_MAX_RECURSION_DEPTH environment variable to allow higher limits", - ), - ("Iteration", "Infinity"), - ): - limit = f"${kind}Limit" - check_evaluation(f"{prefix}{limit} = 511", "511") - check_evaluation( - f"{prefix}{limit} = 2", - "2", - expected_messages=( - f"Cannot set {limit} to 2; value must be an integer between 20 and {suffix}.", - ), - ) - check_evaluation(f"{prefix}$ModuleNumber = 3", "3") - check_evaluation( - f"{prefix}$ModuleNumber = -1", - "-1", - expected_messages=( - "Cannot set $ModuleNumber to -1; value must be a positive integer.", - ), - ) - - @pytest.mark.parametrize( ["expr", "expect", "assert_msg"], [ @@ -644,50 +204,6 @@ def test_assignment(expr, expect, assert_msg): ) -@pytest.mark.parametrize( - ["expr", "expect", "assert_msg", "expected_msgs"], - [ - # Trivial cases on protected symbols. We should - # get a message in every situation. - (None, None, None, None), # reset session - ( - "List:=1;", - None, - "assign to protected element", - ("Symbol List is Protected.",), - ), - ( - "HoldPattern[List]:=1;", - None, - "assign to wrapped protected element. Test 1.", - ("Tag List in HoldPattern[List] is Protected.",), - ), - ( - "PatternTest[List, x]:=1;", - None, - "assign to wrapped protected element. Test 2.", - ("Tag List in List ? x is Protected.",), - ), - ( - "Condition[List, x]:=1;", - None, - "assign to wrapped protected element. Test 3.", - ("Tag List in List /; x is Protected.",), - ), - ( - "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_with_messages(expr, expect, assert_msg, expected_msgs): - check_evaluation( - expr, expect, failure_message=assert_msg, expected_messages=expected_msgs - ) - - # Regression check of some assignment issues encountered. @pytest.mark.parametrize( ["expr", "expect", "assert_msg", "hold_expected", "messages"], @@ -746,34 +262,3 @@ def test_regression_of_assignment_issues( hold_expected=hold_expected, expected_messages=messages, ) - - -@pytest.mark.parametrize( - ("str_expr", "str_expected", "msg"), - [ - (None, None, None), - ( - "Verbatim[F][x_Integer]:=3; DownValues[F]", - "{HoldPattern[Verbatim[F][x_Integer]] :> 3}", - None, - ), - ("Verbatim[F][x_Integer]=.; DownValues[F]", "{}", None), - ( - "M[Verbatim[F][u_]]^:=3; UpValues[F]", - "{HoldPattern[M[Verbatim[F][u_]]] :> 3}", - None, - ), - ("F/:M[Verbatim[F][u_]]=.; UpValues[F]", "{}", None), - ( - "Unprotect[N]; Verbatim[N][F[x_Integer],_]:=0.;DownValues[N]", - "{HoldPattern[Verbatim[N][F[x_Integer], _]] :> 0.}", - None, - ), - ], -) -def test_verbatim_assignment(str_expr, str_expected, msg): - check_evaluation( - str_expr, - str_expected, - failure_message=msg, - ) From 58242dff7b342717ee15eb64bd63164edf59ded6 Mon Sep 17 00:00:00 2001 From: Juan Mauricio Matera Date: Mon, 7 Sep 2026 09:50:37 -0300 Subject: [PATCH 6/9] get_tag_position -> determine_value_role --- mathics/builtin/patterns/basic.py | 4 +-- mathics/builtin/patterns/composite.py | 20 +++++------ mathics/builtin/patterns/restrictions.py | 4 +-- mathics/core/definitions.py | 42 +++++++++++++----------- mathics/core/pattern/base.py | 14 ++++---- test/core/test_definitions.py | 8 ++--- 6 files changed, 47 insertions(+), 45 deletions(-) diff --git a/mathics/builtin/patterns/basic.py b/mathics/builtin/patterns/basic.py index ccf42523a0..13952a56b2 100644 --- a/mathics/builtin/patterns/basic.py +++ b/mathics/builtin/patterns/basic.py @@ -44,8 +44,8 @@ def __new__(cls, *args, **kwargs): cls._instance = super().__new__(cls, *args, **kwargs) return cls._instance - def get_tag_position(self, target: Symbol) -> OptionalType[str]: - if self.target_head is target: + def determine_value_role(self, tag_symbol: Symbol) -> OptionalType[str]: + if self.target_head is tag_symbol: return "downvalues" return None diff --git a/mathics/builtin/patterns/composite.py b/mathics/builtin/patterns/composite.py index 8e831de79f..99d2c4553a 100644 --- a/mathics/builtin/patterns/composite.py +++ b/mathics/builtin/patterns/composite.py @@ -210,8 +210,8 @@ def init( super().init(expr, evaluation=evaluation) self.pattern = BasePattern.create(expr.elements[0], evaluation=evaluation) - def get_tag_position(self, target: Symbol) -> OptionalType[str]: - return self.pattern.get_tag_position(target) + def determine_value_role(self, tag_symbol: Symbol) -> OptionalType[str]: + return self.pattern.determine_value_role(tag_symbol) def match(self, expression: Expression, pattern_context: dict): # for new_vars_dict, rest in self.pattern.match( @@ -479,8 +479,8 @@ def init( def __repr__(self): return "" % repr(self.pattern) - def get_tag_position(self, target: Symbol) -> OptionalType[str]: - return self.pattern.get_tag_position(target) + def determine_value_role(self, tag_symbol: Symbol) -> OptionalType[str]: + return self.pattern.determine_value_role(tag_symbol) def get_match_count( self, vars_dict: OptionalType[dict] = None @@ -741,26 +741,26 @@ def init( elements = expr.elements self.content = elements[0] if len(elements) == 1 else None - def get_tag_position(self, target: Symbol) -> OptionalType[str]: + def determine_value_role(self, tag_symbol: Symbol) -> OptionalType[str]: content = self.content if content is None: return None - if content is target: + if content is tag_symbol: return "ownvalues" if isinstance(content, Atom): return None - if content.has_form(target, None): + if content.has_form(tag_symbol, None): return "downvalues" if ( content.has_form(SymbolN, 2) - and content.elements[0].get_lookup_name() == target.get_name() + and content.elements[0].get_lookup_name() == tag_symbol.get_name() ): return "nvalues" - if content.get_lookup_name() == target.get_name(): + if content.get_lookup_name() == tag_symbol.get_name(): return "subvalues" for element in content.elements: - if element is target or element.has_form(target, None): + if element is tag_symbol or element.has_form(tag_symbol, None): return "upvalues" return None diff --git a/mathics/builtin/patterns/restrictions.py b/mathics/builtin/patterns/restrictions.py index 02316233d9..403bd1a7ea 100644 --- a/mathics/builtin/patterns/restrictions.py +++ b/mathics/builtin/patterns/restrictions.py @@ -67,8 +67,8 @@ def init( # else: self.pattern = BasePattern.create(expr.elements[0], evaluation=evaluation) - def get_tag_position(self, target: Symbol) -> OptionalType[str]: - return self.pattern.get_tag_position(target) + def determine_value_role(self, tag_symbol: Symbol) -> OptionalType[str]: + return self.pattern.determine_value_role(tag_symbol) def match(self, expression: Expression, pattern_context: dict): """Match with Condition pattern""" diff --git a/mathics/core/definitions.py b/mathics/core/definitions.py index d5f3cb61d1..a8dae50119 100644 --- a/mathics/core/definitions.py +++ b/mathics/core/definitions.py @@ -107,7 +107,7 @@ def add_rule_at(self, rule: BaseRule, position: str) -> bool: def add_rule(self, rule: BaseRule) -> bool: """Add a rule. The position is automatically determined.""" - pos = get_tag_position(rule.pattern.expr, self.symbol) + pos = determine_value_role(rule.pattern.expr, self.symbol) if pos: return self.add_rule_at(rule, pos) return False @@ -123,7 +123,7 @@ def remove_format_rule(self, lhs: BaseElement, form: str) -> bool: def remove_rule(self, lhs: BaseElement) -> bool: """Remove a rule""" - position = get_tag_position(lhs, self.symbol) + position = determine_value_role(lhs, self.symbol) if position: values = self.get_values_list(position) for index, existing in enumerate(values): @@ -880,12 +880,14 @@ def _valuesname(name: str) -> str: return name[7:].lower() -def get_tag_position(pattern: BaseElement | BasePattern, tag: Symbol) -> Optional[str]: +def determine_value_role( + pattern: BaseElement | BasePattern, tagger_symbol: Symbol +) -> Optional[str]: """ Determine the position of a pattern in - the definition of the symbol ``tag`` + the definition of the symbol ``tagger_symbol`` """ - assert isinstance(tag, Symbol), "tag must be a symbol" + assert isinstance(tagger_symbol, Symbol), "tagger_symbol must be a symbol" # If pattern is a PatternObject, use the method. # In Set* and when Builtin symbols are loaded, # a pattern object is available. @@ -894,14 +896,14 @@ def get_tag_position(pattern: BaseElement | BasePattern, tag: Symbol) -> Optiona # We could compile the pattern for that case too # but it would introduce some overhead. if isinstance(pattern, BasePattern): - return pattern.get_tag_position(tag) + return pattern.determine_value_role(tagger_symbol) # pattern is a BaseElement - if pattern is tag: + if pattern is tagger_symbol: return "ownvalues" if isinstance(pattern, Atom): return None if pattern.has_form(SymbolN, 2): - position = get_tag_position(pattern.get_elements()[0], tag) + position = determine_value_role(pattern.get_elements()[0], tagger_symbol) if position in ( None, "upvalues", @@ -909,47 +911,47 @@ def get_tag_position(pattern: BaseElement | BasePattern, tag: Symbol) -> Optiona return None return "nvalues" if pattern.has_form(SymbolHoldPattern, 1): - return get_tag_position(pattern.get_elements()[0], tag) + return determine_value_role(pattern.get_elements()[0], tagger_symbol) if pattern.has_form(SymbolPattern, 2): - return get_tag_position(pattern.get_elements()[1], tag) + return determine_value_role(pattern.get_elements()[1], tagger_symbol) if pattern.has_form(SymbolCondition, 2): - return get_tag_position(pattern.get_elements()[0], tag) + return determine_value_role(pattern.get_elements()[0], tagger_symbol) if pattern.has_form(BLANK_PATTERN_HEADS, 1): - if tag is pattern.get_elements()[0]: + if tagger_symbol is pattern.get_elements()[0]: return "downvalues" if pattern.has_form(SymbolVerbatim, 1): content = pattern.get_elements()[0] - if tag is content: + if tagger_symbol is content: return "ownvalues" if isinstance(content, Atom): return None - if content.has_form(tag, None): + if content.has_form(tagger_symbol, None): return "downvalues" if ( content.has_form(SymbolN, 2) - and content.get_elements()[0].get_lookup_name() == tag.get_name() + and content.get_elements()[0].get_lookup_name() == tagger_symbol.get_name() ): return "nvalues" - if content.get_lookup_name() == tag.get_name(): + if content.get_lookup_name() == tagger_symbol.get_name(): return "subvalues" for element in content.get_elements(): - if element is tag or element.has_form(tag, None): + if element is tagger_symbol or element.has_form(tagger_symbol, None): return "upvalues" return None head = pattern.get_head() - if head is tag: + if head is tagger_symbol: return "downvalues" - head_pos = get_tag_position(head, tag) + head_pos = determine_value_role(head, tagger_symbol) if head_pos == "ownvalues": return "downvalues" if head_pos in ("downvalues", "subvalues"): return "subvalues" for element in pattern.get_elements(): - elem_position = get_tag_position(element, tag) + elem_position = determine_value_role(element, tagger_symbol) if elem_position in ("ownvalues", "downvalues", "subvalues"): return "upvalues" return None diff --git a/mathics/core/pattern/base.py b/mathics/core/pattern/base.py index 7b1ee82cf2..d998ecc61f 100644 --- a/mathics/core/pattern/base.py +++ b/mathics/core/pattern/base.py @@ -236,7 +236,7 @@ def get_sequence(self): """The sequence of elements in the expression""" return self.expr.get_sequence() - def get_tag_position(self, target: Symbol) -> Optional[str]: + def determine_value_role(self, tag_symbol: Symbol) -> Optional[str]: """ Return the position relative to the Symbol `target`. When the position cannot be decided, returns `None`. @@ -379,8 +379,8 @@ def _build_pattern_sort_key(self) -> tuple: def __repr__(self): return f"" - def get_tag_position(self, target: Symbol) -> Optional[str]: - if target is self.atom: + def determine_value_role(self, tag_symbol: Symbol) -> Optional[str]: + if tag_symbol is self.atom: return "ownvalues" return None @@ -486,10 +486,10 @@ def filter_elements(self, head_name: str): element for element in self.elements if element.get_head_name() == head_name ] - def get_tag_position(self, target: Symbol) -> Optional[str]: + def determine_value_role(self, tag_symbol: Symbol) -> Optional[str]: # Special case: Nvalues if self.expr.has_form(SymbolN, 2): - tag = self.elements[0].get_tag_position(target) + tag = self.elements[0].determine_value_role(tag_symbol) if tag in ( None, "upvalues", @@ -498,13 +498,13 @@ def get_tag_position(self, target: Symbol) -> Optional[str]: return "nvalues" head = self.head - head_pos = head.get_tag_position(target) + head_pos = head.determine_value_role(tag_symbol) if head_pos == "ownvalues": return "downvalues" if head_pos in ("downvalues", "subvalues"): return "subvalues" for element in self.elements: - elem_tag = element.get_tag_position(target) + elem_tag = element.determine_value_role(tag_symbol) if elem_tag in ("ownvalues", "downvalues", "subvalues"): return "upvalues" return None diff --git a/test/core/test_definitions.py b/test/core/test_definitions.py index ef6eea254b..2a4bdc3327 100644 --- a/test/core/test_definitions.py +++ b/test/core/test_definitions.py @@ -5,7 +5,7 @@ import pytest -from mathics.core.definitions import get_tag_position +from mathics.core.definitions import determine_value_role from mathics.core.parser import parse_builtin_rule from mathics.core.pattern import BasePattern @@ -118,9 +118,9 @@ def load_pattern_objects(): ("HoldPattern[Condition[Verbatim[G][x_],_]]", "G", "downvalues"), ], ) -def test_get_tag_position(pattern_str, tag, position): +def test_determine_value_role(pattern_str, tag, position): target = parse_builtin_rule(tag) pattern_expr = parse_builtin_rule(pattern_str) pattern_pat = BasePattern.create(pattern_expr) - assert get_tag_position(pattern_pat, target) == position - assert get_tag_position(pattern_expr, target) == position + assert determine_value_role(pattern_pat, target) == position + assert determine_value_role(pattern_expr, target) == position From fb4cf717a5b2858f5f4a0584e041adb8fd01190d Mon Sep 17 00:00:00 2001 From: Juan Mauricio Matera Date: Sun, 20 Sep 2026 13:41:19 -0300 Subject: [PATCH 7/9] merge --- .../assignments/test_assign_messages.py | 3 +- .../assignments/test_assign_one_identity.py | 3 +- .../assignments/test_assign_set_and_clear.py | 43 +------------------ .../assignments/test_assign_verbatim_lhs.py | 3 +- test/builtin/assignments/test_assignment.py | 3 +- 5 files changed, 10 insertions(+), 45 deletions(-) diff --git a/test/builtin/assignments/test_assign_messages.py b/test/builtin/assignments/test_assign_messages.py index 17b6356827..9052e43843 100644 --- a/test/builtin/assignments/test_assign_messages.py +++ b/test/builtin/assignments/test_assign_messages.py @@ -8,9 +8,10 @@ """ # TODO: consider splitting this module into sub-modules. -from test.helper import check_arg_counts, check_evaluation +from test.helper import check_arg_counts, check_evaluation, session import pytest +from mathics_scanner.errors import IncompleteSyntaxError @pytest.mark.parametrize( diff --git a/test/builtin/assignments/test_assign_one_identity.py b/test/builtin/assignments/test_assign_one_identity.py index c6bba6282e..b6492b8c06 100644 --- a/test/builtin/assignments/test_assign_one_identity.py +++ b/test/builtin/assignments/test_assign_one_identity.py @@ -8,8 +8,9 @@ """ # TODO: consider splitting this module into sub-modules. -from test.helper import check_evaluation +from test.helper import check_arg_counts, check_evaluation, session +import pytest from mathics_scanner.errors import IncompleteSyntaxError STR_TEST_SET_WITH_ONE_IDENTITY = """ diff --git a/test/builtin/assignments/test_assign_set_and_clear.py b/test/builtin/assignments/test_assign_set_and_clear.py index 90bd2a8ac3..84f65ae83c 100644 --- a/test/builtin/assignments/test_assign_set_and_clear.py +++ b/test/builtin/assignments/test_assign_set_and_clear.py @@ -8,9 +8,10 @@ """ # TODO: consider splitting this module into sub-modules. -from test.helper import check_evaluation, session +from test.helper import check_arg_counts, check_evaluation, session import pytest +from mathics_scanner.errors import IncompleteSyntaxError @pytest.mark.parametrize( @@ -222,46 +223,6 @@ def test_set_and_clear(str_expr, str_expected, msg): ("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", - ), ], ) def test_set_and_clear_to_fix(str_expr, str_expected, msg): diff --git a/test/builtin/assignments/test_assign_verbatim_lhs.py b/test/builtin/assignments/test_assign_verbatim_lhs.py index 6f8e2b7cd8..4f6b0e481c 100644 --- a/test/builtin/assignments/test_assign_verbatim_lhs.py +++ b/test/builtin/assignments/test_assign_verbatim_lhs.py @@ -8,9 +8,10 @@ """ # TODO: consider splitting this module into sub-modules. -from test.helper import check_evaluation +from test.helper import check_arg_counts, check_evaluation, session import pytest +from mathics_scanner.errors import IncompleteSyntaxError @pytest.mark.parametrize( diff --git a/test/builtin/assignments/test_assignment.py b/test/builtin/assignments/test_assignment.py index f097d0247c..16ea2bae4c 100644 --- a/test/builtin/assignments/test_assignment.py +++ b/test/builtin/assignments/test_assignment.py @@ -8,9 +8,10 @@ """ # TODO: consider splitting this module into sub-modules. -from test.helper import check_evaluation +from test.helper import check_arg_counts, check_evaluation, session import pytest +from mathics_scanner.errors import IncompleteSyntaxError def test_order(): From c5b726ef4f764b1e2a37a337e65472fd5af774bc Mon Sep 17 00:00:00 2001 From: Juan Mauricio Matera Date: Sun, 20 Sep 2026 14:11:35 -0300 Subject: [PATCH 8/9] merge --- mathics/Autoload/rules/Element.wl | 1 - mathics/builtin/numbers/calculus.py | 1 + mathics/core/definitions.py | 33 ++++++++++++++--------------- mathics/eval/numeric.py | 3 --- mathics/format/form/outputform.py | 1 - 5 files changed, 17 insertions(+), 22 deletions(-) diff --git a/mathics/Autoload/rules/Element.wl b/mathics/Autoload/rules/Element.wl index f71d2dcd01..213bb9ce69 100644 --- a/mathics/Autoload/rules/Element.wl +++ b/mathics/Autoload/rules/Element.wl @@ -1,6 +1,5 @@ (*Rules for Elements*) - System`Integers::usage="Represents the set of the Integers numbers"; System`Primes::usage="Represents the set of the prime numbers"; System`Rationals::usage="Represents the set of the Rational numbers"; diff --git a/mathics/builtin/numbers/calculus.py b/mathics/builtin/numbers/calculus.py index b3d92e3577..51cb027c26 100644 --- a/mathics/builtin/numbers/calculus.py +++ b/mathics/builtin/numbers/calculus.py @@ -70,6 +70,7 @@ SymbolList, SymbolLog, SymbolNIntegrate, + SymbolRoot, SymbolRule, SymbolSequence, SymbolSeries, diff --git a/mathics/core/definitions.py b/mathics/core/definitions.py index 1534027d87..c6f196ba7b 100644 --- a/mathics/core/definitions.py +++ b/mathics/core/definitions.py @@ -92,6 +92,22 @@ def __init__( if not self.add_rule(rule): print(f"{rule.pattern.expr} could not be associated with {self.name}") + def __repr__(self) -> str: + repr_str = ( + "" + ).format( + self.name, + self.ownvalues, + self.downvalues, + self.formatvalues, + self.attributes, + ) + return repr_str + def _resolve(self, evaluation: "Evaluation"): """ Go over all the rules, and ensure that the corresponding patterns are in its final state @@ -111,22 +127,6 @@ def _resolve(self, evaluation: "Evaluation"): for rule in rule_list: rule._resolve(evaluation) - def __repr__(self) -> str: - repr_str = ( - "" - ).format( - self.name, - self.ownvalues, - self.downvalues, - self.formatvalues, - self.attributes, - ) - return repr_str - def add_rule(self, rule: BaseRule) -> bool: """Add a rule to one of the Rule lists. The specific rule list is selected using `determine_value_role().` @@ -224,7 +224,6 @@ def __init__( # decided what information to show self.trace_evaluation = False self.trace_show_rewrite = False - if add_builtin: load_builtin_definitions(self, builtin_filename, extension_modules) diff --git a/mathics/eval/numeric.py b/mathics/eval/numeric.py index 4e398ce5d6..2ec62b61fd 100644 --- a/mathics/eval/numeric.py +++ b/mathics/eval/numeric.py @@ -43,12 +43,9 @@ SymbolI, SymbolIndeterminate, SymbolLog, - SymbolPlus, - SymbolPower, SymbolRealSign, SymbolSign, SymbolSqrt, - SymbolTimes, ) RationalMOneHalf = Rational(-1, 2) diff --git a/mathics/format/form/outputform.py b/mathics/format/form/outputform.py index 1ea7f548a6..64e12fde1c 100644 --- a/mathics/format/form/outputform.py +++ b/mathics/format/form/outputform.py @@ -261,7 +261,6 @@ def render_output_form(expr: BaseElement, evaluation: Evaluation, **kwargs): """ Build a pretty-print text from an `Expression` """ - lookup_name: str format_expr: Expression = do_format(expr, evaluation, SymbolOutputForm) # type: ignore while format_expr.has_form("HoldForm", 1): # type: ignore From 257335f83d5b24b76c2db8bdba94d96a50f6239d Mon Sep 17 00:00:00 2001 From: Juan Mauricio Matera Date: Sun, 20 Sep 2026 16:38:24 -0300 Subject: [PATCH 9/9] restore elements.wl --- mathics/Autoload/rules/Element.wl | 1 + 1 file changed, 1 insertion(+) diff --git a/mathics/Autoload/rules/Element.wl b/mathics/Autoload/rules/Element.wl index 213bb9ce69..f71d2dcd01 100644 --- a/mathics/Autoload/rules/Element.wl +++ b/mathics/Autoload/rules/Element.wl @@ -1,5 +1,6 @@ (*Rules for Elements*) + System`Integers::usage="Represents the set of the Integers numbers"; System`Primes::usage="Represents the set of the prime numbers"; System`Rationals::usage="Represents the set of the Rational numbers";