diff --git a/mathics/core/definitions.py b/mathics/core/definitions.py index 46624b2e6..59cd025e2 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 6bd1509cd..afa28aee7 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 8b308a713..2d008f758 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 b925b04ad..11f913e93 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 881331fc1..9f8caf560 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 0a0b83e5a..68e9a8979 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 096000980..fec80c12e 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": [