diff --git a/mathics/builtin/arithfns/basic.py b/mathics/builtin/arithfns/basic.py index 91dcd7c2a5..47190b0daf 100644 --- a/mathics/builtin/arithfns/basic.py +++ b/mathics/builtin/arithfns/basic.py @@ -62,7 +62,7 @@ class CubeRoot(Builtin): } rules = { - "CubeRoot[n_?NumberQ]": "If[n > 0, Power[n, Divide[1, 3]], Times[-1, Power[Times[-1, n], Divide[1, 3]]]]", + "CubeRoot[n_Integer|n_Real|n_Rational]": "If[n > 0, Power[n, Divide[1, 3]], Times[-1, Power[Times[-1, n], Divide[1, 3]]]]", "CubeRoot[n_]": "Power[n, Divide[1, 3]]", "MakeBoxes[CubeRoot[x_], f:StandardForm|TraditionalForm]": ( "RadicalBox[MakeBoxes[x, f], 3]" diff --git a/mathics/builtin/symbol/properties.py b/mathics/builtin/symbol/properties.py index 3cc71dc9ee..036a0573a8 100644 --- a/mathics/builtin/symbol/properties.py +++ b/mathics/builtin/symbol/properties.py @@ -194,7 +194,7 @@ class DownValues(Builtin): >> f[x_Integer] := 2 >> f[x_Real] := 3 >> DownValues[f] - = {HoldPattern[f[x_Real]] ⧴ 3, HoldPattern[f[x_Integer]] ⧴ 2, HoldPattern[f[x_]] ⧴ x ^ 2} + = {HoldPattern[f[x_Integer]] ⧴ 2, HoldPattern[f[x_Real]] ⧴ 3, HoldPattern[f[x_]] ⧴ x ^ 2} >> f[3] = 2 >> f[3.] diff --git a/mathics/core/builtin.py b/mathics/core/builtin.py index cd48318b2a..adcd3aab9e 100644 --- a/mathics/core/builtin.py +++ b/mathics/core/builtin.py @@ -309,19 +309,6 @@ def contribute(self, definitions: Definitions, is_pymodule=False): PyMathicsDefinitions() if is_pymodule else SystemDefinitions() ) - for pattern, function in self.get_functions( - prefix="eval", is_pymodule=is_pymodule - ): - pat_attr = attributes if pattern.get_head_name() == name else None - rules.append( - FunctionApplyRule( - name, - pattern, - function, - check_options, - attributes=pat_attr, - ) - ) for pattern_str, replace_str in self.rules.items(): pattern_str = pattern_str % {"name": name} pattern = parse_builtin_rule(pattern_str, definition_class) @@ -335,6 +322,19 @@ def contribute(self, definitions: Definitions, is_pymodule=False): ) ) + for pattern, function in self.get_functions( + prefix="eval", is_pymodule=is_pymodule + ): + pat_attr = attributes if pattern.get_head_name() == name else None + rules.append( + FunctionApplyRule( + name, + pattern, + function, + check_options, + attributes=pat_attr, + ) + ) box_rules = [] # FIXME: Why a special case for System`MakeBoxes? Remove this if name != "System`MakeBoxes": @@ -371,17 +371,6 @@ def contextify_form_name(f): return forms, pattern formatvalues: dict[str, List[BaseRule]] = {"": []} - for pattern, function in self.get_functions("format_"): - forms, pattern = extract_forms(pattern) - pat_attr = attributes if pattern.get_head_name() == name else None - for form in forms: - if form not in formatvalues: - formatvalues[form] = [] - formatvalues[form].append( - FunctionApplyRule( - name, pattern, function, None, attributes=pat_attr - ) - ) for pattern, replace in self.formats.items(): forms, pattern = extract_forms(pattern) for form in forms: @@ -395,6 +384,18 @@ def contextify_form_name(f): RewriteRule(pattern, parse_builtin_rule(replace)) ) + for pattern, function in self.get_functions("format_"): + forms, pattern = extract_forms(pattern) + pat_attr = attributes if pattern.get_head_name() == name else None + for form in forms: + if form not in formatvalues: + formatvalues[form] = [] + formatvalues[form].append( + FunctionApplyRule( + name, pattern, function, None, attributes=pat_attr + ) + ) + formatvalues.setdefault("_MakeBoxes", []).extend(box_rules) for form, formatrules in formatvalues.items(): diff --git a/mathics/core/definitions.py b/mathics/core/definitions.py index c6f3dc8132..37f9261c4d 100644 --- a/mathics/core/definitions.py +++ b/mathics/core/definitions.py @@ -5,7 +5,6 @@ """ import base64 -import bisect import os.path as osp import pickle import re @@ -19,6 +18,7 @@ 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.pattern.specificity import rule_le from mathics.core.rules import BaseRule, RewriteRule from mathics.core.symbols import Atom, Symbol, SymbolList, strip_context from mathics.core.systemsymbols import ( @@ -1013,9 +1013,35 @@ def is_conditional(x) -> bool: del values[index] break - # use insort_left to guarantee that if equal rules exist, newer rules will - # get higher precedence by being inserted before them. see DownValues[]. - bisect.insort_left(values, rule, key=lambda x: x.pattern_precedence) + # Mathematica orders rules with a partial order: a rule goes before + # another one only if it is strictly more specific. Rules which are + # not comparable keep their definition order. `insort_right` only + # uses `<`, so a key wrapper with a partial `__lt__` is enough. + # When `rule` is equivalent to an existing rule, it is placed before it, + # so newer rules get higher precedence. see DownValues[]. + # Insert before the first existing rule which strictly contains the new + # one. A rule which is not comparable with the others keeps its + # definition order, as in Mathematica. A linear scan is used because + # the relation is partial, so a binary search is not reliable. + i = 0 + for existing in values: + if _rule_lt(rule, existing): + break + i += 1 + values.insert(i, rule) + + +def _rule_lt(new: BaseRule, existing: BaseRule) -> bool: + """True if `new` should go before `existing`: `new` matches a subset of + what `existing` matches (or they are equivalent). + + If the patterns involved are not understood by + `mathics.core.pattern.specificity`, fall back to the sort keys. + """ + contained = rule_le(new, existing) + if contained is None: + return new.pattern_precedence <= existing.pattern_precedence + return contained def merge_definitions(candidates: List[Definition]) -> Definition: diff --git a/mathics/core/pattern/specificity.py b/mathics/core/pattern/specificity.py new file mode 100644 index 0000000000..15100d0e5c --- /dev/null +++ b/mathics/core/pattern/specificity.py @@ -0,0 +1,323 @@ +""" +Partial order between rules, based on set inclusion of the expressions +that each rule can match. + +Mathematica does not sort DownValues with a total order. A rule is +moved before another one only when it is *strictly more specific* +(everything it matches is also matched by the other one). Rules that +are not comparable keep the order in which they were defined. Packages +such as Combinatorica rely on that. + +``rule_le(r, e)`` is True when it can be proved that the set of +expressions matched by rule ``r`` is contained in the one matched by +``e``. The test is conservative: when it is unsure, it answers False, +which means "not comparable". + +A rule is analyzed into a ``RuleShape``: + + * ``head``: the head of the left-hand side; + * ``items``: the arguments, each one a tuple ``(kind, ...)``; + * ``restrictions``: opaque conditions (``/;`` in the left or right + hand side, ``?test``, repeated pattern names). A rule with + restrictions matches a subset of what the same rule without them + would match, but nothing can be said about two different + restrictions. + +If some part of a pattern is not understood (``Alternatives``, +``Except``, ``Optional``, ...), ``analyze_rule`` returns None and the +caller should fall back to the old sort keys. +""" + +from enum import Enum +from typing import FrozenSet, List, Optional, Tuple + +from mathics.core.expression import Expression +from mathics.core.symbols import Atom +from mathics.core.systemsymbols import ( + SymbolAlternatives, + SymbolBlank, + SymbolBlankNullSequence, + SymbolBlankSequence, + SymbolCondition, + SymbolDefault, + SymbolExcept, + SymbolHoldPattern, + SymbolLongest, + SymbolOptional, + SymbolOptionsPattern, + SymbolPattern, + SymbolPatternSequence, + SymbolPatternTest, + SymbolRepeated, + SymbolRepeatedNull, + SymbolShortest, + SymbolVerbatim, +) + + +# Kinds of items +class ItemType(Enum): + LIT = "lit" # ("lit", expr) exactly one element: expr + ONE = "one" # ("one", head_or_None, restr) Blank + PLUS = "plus" # ("plus", head_or_None, restr) BlankSequence + STAR = "star" # ("star", head_or_None, restr) BlankNullSequence + EXPR = "expr" # ("expr", head, items, restr) compound subpattern + + +_BLANKS = { + SymbolBlank: ItemType.ONE, + SymbolBlankSequence: ItemType.PLUS, + SymbolBlankNullSequence: ItemType.STAR, +} + +FIXED_SIZE_ITEM_TYPES = (ItemType.LIT, ItemType.ONE, ItemType.EXPR) + + +class RuleShape: + __slots__ = ("head", "items", "restrictions") + + def __init__(self, head, items, restrictions): + self.head = head + self.items = items + self.restrictions = restrictions + + +class _Unsupported(Exception): + pass + + +_SPECIAL_HEADS = { + SymbolAlternatives, + SymbolExcept, + SymbolOptional, + SymbolRepeated, + SymbolRepeatedNull, + SymbolLongest, + SymbolShortest, + SymbolVerbatim, + SymbolPatternSequence, + SymbolOptionsPattern, + SymbolDefault, + SymbolHoldPattern, +} + + +def _is_pattern_free(expr) -> bool: + """True if `expr` contains no pattern constructs at all.""" + if isinstance(expr, Atom): + return True + head = expr.head + if head in ( + SymbolBlank, + SymbolBlankSequence, + SymbolBlankNullSequence, + SymbolPattern, + SymbolCondition, + SymbolPatternTest, + ): + return False + if head in _SPECIAL_HEADS: + return False + return _is_pattern_free(head) and all(_is_pattern_free(e) for e in expr.elements) + + +def _item(expr) -> tuple: + """Convert one argument of a pattern into an item tuple.""" + restr: List = [] + while isinstance(expr, Expression): + head = expr.head + if head is SymbolPattern and len(expr.elements) == 2: + expr = expr.elements[1] + elif head is SymbolPatternTest and len(expr.elements) == 2: + restr.append(("test", expr.elements[1])) + expr = expr.elements[0] + elif head is SymbolCondition and len(expr.elements) == 2: + restr.append(("cond", expr.elements[1])) + expr = expr.elements[0] + else: + break + restr_set = frozenset(restr) + if isinstance(expr, Expression) and expr.head in _BLANKS: + kind = _BLANKS[expr.head] + if len(expr.elements) == 0: + return (kind, None, restr_set) + if len(expr.elements) == 1: + return (kind, expr.elements[0], restr_set) + raise _Unsupported + if _is_pattern_free(expr): + if restr_set: + raise _Unsupported + return (ItemType.LIT, expr) + if ( + isinstance(expr, Expression) + and _is_pattern_free(expr.head) + and expr.head not in _SPECIAL_HEADS + ): + items = tuple(_item(e) for e in expr.elements) + return (ItemType.EXPR, expr.head, items, restr_set) + raise _Unsupported + + +def _repeated_names(elements) -> FrozenSet: + """Repeated names among the top level arguments, as a restriction.""" + groups: dict = {} + for i, e in enumerate(elements): + if isinstance(e, Expression) and e.head is SymbolPattern: + groups.setdefault(e.elements[0], []).append(i) + return frozenset(("same", tuple(ix)) for ix in groups.values() if len(ix) > 1) + + +def analyze_rule(rule) -> Optional[RuleShape]: + """Return the RuleShape of `rule`, or None if it is not understood.""" + cached: Optional[RuleShape] | int = getattr(rule, "_specificity_shape", 0) + if not isinstance(cached, int): + return cached + shape = None + try: + if type(rule.pattern).__name__ == "OrderlessExpressionPattern": + raise _Unsupported + expr = rule.pattern.expr + restr: List = [] + while ( + isinstance(expr, Expression) + and expr.head is SymbolCondition + and len(expr.elements) == 2 + ): + restr.append(("cond", expr.elements[1])) + expr = expr.elements[0] + if ( + isinstance(expr, Expression) + and _is_pattern_free(expr.head) + and expr.head not in _SPECIAL_HEADS + ): + items = tuple(_item(e) for e in expr.elements) + replace = getattr(rule, "replace", None) + if ( + replace is not None + and isinstance(replace, Expression) + and replace.head is SymbolCondition + and len(replace.elements) == 2 + ): + restr.append(("cond", replace.elements[1])) + restr.extend(_repeated_names(expr.elements)) + shape = RuleShape(expr.head, items, frozenset(restr)) + except _Unsupported: + shape = None + try: + rule._specificity_shape = shape + except AttributeError: + pass + return shape + + +def _head_of_literal(expr): + return expr.get_head() + + +def _head_ok(h_inner, h_outer) -> bool: + return h_outer is None or (h_inner is not None and h_inner.sameQ(h_outer)) + + +def _item_le(a: tuple, b: tuple) -> bool: + """Every single element matched by item `a` is matched by item `b`. + + Only the "per element" part is considered here: whether the item is + one element or a run of elements is handled by `_items_le`. + """ + ka, kb = a[0], b[0] + if kb is ItemType.LIT: + return ka is ItemType.LIT and a[1].sameQ(b[1]) + # restrictions of b must all be present in a + rb = b[-1] + ra = a[-1] if ka is not ItemType.LIT else frozenset() + if not rb <= ra: + return False + if kb is ItemType.EXPR: + if ka is not ItemType.EXPR or not a[1].sameQ(b[1]): + return False + return _items_le(a[2], b[2]) + # b is some kind of blank + hb = b[1] + if ka is ItemType.LIT: + return _head_ok(_head_of_literal(a[1]), hb) + if ka is ItemType.EXPR: + return _head_ok(a[1], hb) + return _head_ok(a[1], hb) + + +def _min_len(it: tuple) -> int: + return 0 if it[0] is ItemType.STAR else 1 + + +def _is_fixed(it: tuple) -> bool: + return it[0] in (ItemType.LIT, ItemType.ONE, ItemType.EXPR) + + +def _items_le(p: Tuple, q: Tuple) -> bool: + """Every sequence of arguments matched by `p` is matched by `q`. + + Sufficient condition: the arguments of `p` can be assigned, in order, + to the items of `q`, such that a fixed item of `q` receives exactly + one fixed item, and variable items of `q` receive items whose + elements they accept (and a ``__`` receives at least one element). + """ + n, m = len(p), len(q) + memo: dict = {} + + def go(i: int, k: int) -> bool: + if (i, k) in memo: + return memo[(i, k)] + if k == m: + res = i == n + else: + qk = q[k] + res = False + if qk[0] in FIXED_SIZE_ITEM_TYPES: + res = ( + i < n + and (p[i][0] in FIXED_SIZE_ITEM_TYPES) + and _item_le(p[i], qk) + and go(i + 1, k + 1) + ) + else: + need = 1 if qk[0] is ItemType.PLUS else 0 + taken_min = 0 + j = i + # q[k] receives p[i:j] + while True: + if taken_min >= need and go(j, k + 1): + res = True + break + if j >= n or not _item_le(p[j], qk): + break + taken_min += _min_len(p[j]) + j += 1 + memo[(i, k)] = res + return res + + return go(0, 0) + + +def rule_le(r, e) -> Optional[bool]: + """ + True if the set matched by rule `r` is provably contained in the set + matched by rule `e` (including the case where both are equivalent). + False if it is not provable. None if some rule is not understood. + """ + sr, se = analyze_rule(r), analyze_rule(e) + if sr is None or se is None: + return None + if not sr.head.sameQ(se.head): + return False + if not se.restrictions <= sr.restrictions: + return False + return _items_le(sr.items, se.items) + + +def rule_lt(r, e) -> Optional[bool]: + """Like `rule_le`, but strict: `r` is contained in `e` and `e` is not + contained in `r`.""" + a = rule_le(r, e) + if a is None: + return None + return a and not rule_le(e, r) diff --git a/mathics/core/systemsymbols.py b/mathics/core/systemsymbols.py index 167121b026..ed2b6bf949 100644 --- a/mathics/core/systemsymbols.py +++ b/mathics/core/systemsymbols.py @@ -278,6 +278,7 @@ SymbolPart: Final[Symbol] = Symbol("System`Part") SymbolPath: Final[Symbol] = Symbol("System`$Path") SymbolPattern: Final[Symbol] = Symbol("System`Pattern") +SymbolPatternSequence: Final[Symbol] = Symbol("System`PatternSequence") SymbolPatternTest: Final[Symbol] = Symbol("System`PatternTest") SymbolPause: Final[Symbol] = Symbol("System`Pause") SymbolPi: Final[Symbol] = Symbol("System`Pi") diff --git a/test/builtin/test_patterns.py b/test/builtin/test_patterns.py index 6f1ccbe288..28182dd24d 100644 --- a/test/builtin/test_patterns.py +++ b/test/builtin/test_patterns.py @@ -181,3 +181,9 @@ def test_pattern(str_expr, msgs, str_expected, fail_msg): failure_message=fail_msg, expected_messages=msgs, ) + + +def check_pattern_definition_order1(): + check_evaluation("NeQ[u_,v_] := u != v;", None) + check_evaluation("NeQ[args___] := Null /; CheckArguments[NeQ[args], 2];", None) + check_evaluation("NeQ[1,2]", "True", failure_message="issue #1978.") diff --git a/test/core/test_rule_order.py b/test/core/test_rule_order.py new file mode 100644 index 0000000000..7187435d51 --- /dev/null +++ b/test/core/test_rule_order.py @@ -0,0 +1,207 @@ +# -*- coding: utf-8 -*- +""" +Rule ordering: partial order by inclusion of matched sets. +Expected values come from Mathematica +(incomparable rules keep their definition order). +""" +import pytest + +from mathics.core.load_builtin import import_and_load_builtins +from mathics.core.pattern.specificity import rule_le, rule_lt +from mathics.session import MathicsSession + +import_and_load_builtins() +session = MathicsSession() + + +def rules_of(name): + return session.definitions.get_definition("Global`" + name).downvalues + + +def lhs_order(name, defs): + session.evaluate(f"ClearAll[{name}]") + for d in defs: + session.evaluate(d) + return [ + str(r.pattern.expr).replace("System`", "").replace("Global`", "") + for r in rules_of(name) + ] + + +GEN = "A[Pattern[n, Blank[Integer]], Blank[]]" +ESP = "A[Pattern[n, Blank[Integer]], Pattern[m, Blank[Integer]]]" +GEN_LHS = "Condition[" + GEN + ", Less[n, 0]]" +ESP1 = "A[Pattern[n, Blank[Integer]], 1]" + +A_CASES = [ + (["A[n_Integer,_]=0", "A[n_Integer,m_Integer]=1"], [ESP, GEN]), + (["A[n_Integer,_]:=0/;n<0", "A[n_Integer,m_Integer]:=1"], [GEN, ESP]), + (["A[n_Integer,m_Integer]:=1", "A[n_Integer,_]:=0/;n<0"], [ESP, GEN]), + (["A[n_Integer,_]/;n<0:=0", "A[n_Integer,m_Integer]:=1"], [GEN_LHS, ESP]), + (["A[n_Integer,m_Integer]:=1", "A[n_Integer,_]/;n<0:=0"], [ESP, GEN_LHS]), + (["A[n_Integer,_]:=0", "A[n_Integer,m_Integer]:=1/;n<0"], [ESP, GEN]), + (["A[n_Integer,_]:=0/;n<0", "A[n_Integer,m_Integer]:=1/;n>5"], [GEN, ESP]), + (["A[n_Integer,m_Integer]:=1/;n>5", "A[n_Integer,_]:=0/;n<0"], [ESP, GEN]), + ( + ["A[n_Integer,_]:=0/;n<0", "A[n_Integer,m_Integer]:=1", "A[n_Integer,1]:=2"], + [GEN, ESP1, ESP], + ), +] + + +@pytest.mark.parametrize(("defs", "expected"), A_CASES) +def test_A_cases(defs, expected): + assert lhs_order("A", defs) == expected + + +def test_F_G(): + f = lhs_order( + "F", + [ + "F[x_Integer,y_Integer]=1", + "F[x_Integer,_]=2", + "F[_,x_Integer]=3", + "F[__Integer]=4", + ], + ) + assert [x.count("Blank[]") for x in f] == [0, 1, 1, 0] + assert f[-1].startswith("F[BlankSequence[Integer]") + g = lhs_order( + "G", + [ + "G[__Integer]=4", + "G[_,x_Integer]=3", + "G[x_Integer,_]=2", + "G[x_Integer,y_Integer]=1", + ], + ) + assert g[0].startswith("G[Pattern[x, Blank[Integer]], Pattern[y") + assert g[1].startswith("G[BlankSequence[Integer]") + assert g[2].startswith("G[Blank[], Pattern[x") + assert g[3].startswith("G[Pattern[x, Blank[Integer]], Blank[]") + + +PARTITIONS = [ + "P[n_Integer]:=P[n,n]", + "P[n_Integer,_]:={}/;n<0", + "P[0,_]:={{}}", + "P[n_Integer,1]:={Table[1,{n}]}", + "P[_,0]:={}", + "P[n_Integer,maxpart_Integer]:=Join[Map[(Prepend[#,maxpart])&,P[n-maxpart,maxpart]],P[n,maxpart-1]]", +] + + +def test_partitions_order_and_termination(): + order = lhs_order("P", PARTITIONS) + assert order[0] == "P[Pattern[n, Blank[Integer]]]" + assert order[-1].startswith("P[Pattern[n, Blank[Integer]], Pattern[maxpart") + assert session.evaluate("Length[P[5]]").value == 7 + assert str(session.evaluate("P[-3, 2]")) == "{}" + assert str(session.evaluate("P[4, 0]")) == "{}" + + +# ssm patterns, as in the Mathematica experiment +SSM = { + "X": "ssm[_a,_a]", + "B": "ssm[_b]", + "A": "ssm[_a]", + "U": "ssm[_]", + "S": "ssm[__a]", + "N": "ssm[___a]", + "T": "ssm[__a,__b]", + "D": "ssm[__]", + "Z": "ssm[___]", + "E": "ssm[]", +} +MATRIX_ORDER = "XBAUSNTDZ" +# Mathematica: 1 if the order of the pair does not depend on definition order +MATRIX = [ + [1, 0, 0, 0, 1, 1, 0, 1, 1], + [0, 1, 0, 1, 0, 0, 0, 1, 1], + [0, 0, 1, 1, 1, 1, 0, 1, 1], + [0, 1, 1, 1, 0, 0, 0, 1, 1], + [1, 0, 1, 0, 1, 1, 0, 1, 1], + [1, 0, 1, 0, 1, 1, 0, 0, 1], + [0, 0, 0, 0, 0, 0, 1, 1, 1], + [1, 1, 1, 1, 1, 0, 1, 1, 1], + [1, 1, 1, 1, 1, 1, 1, 1, 1], +] + +# Orders produced by Mathematica for random insertion orders +MMA_OUTPUTS = [ + "EBXASNTUDZ", + "ETXBAUSDNZ", + "EATBUXSDNZ", + "EXTABUSDNZ", + "EBAUXSNTDZ", + "ETAXSNBUDZ", + "EABUXSTDNZ", + "EABXSNUTDZ", +] + + +def ssm_rule(code): + session.evaluate("ClearAll[ssm]") + session.evaluate(SSM[code] + " = 1") + return rules_of("ssm")[0] + + +def ssm_rules(): + return {c: ssm_rule(c) for c in SSM} + + +def test_ssm_comparability_matrix(): + # Rules must survive ClearAll, so keep copies + rules = {} + for c in SSM: + rules[c] = ssm_rule(c) + for i, ci in enumerate(MATRIX_ORDER): + for j, cj in enumerate(MATRIX_ORDER): + comparable = bool( + rule_le(rules[ci], rules[cj]) or rule_le(rules[cj], rules[ci]) + ) + assert comparable == bool(MATRIX[i][j]), (SSM[ci], SSM[cj]) + + +@pytest.mark.parametrize("out", MMA_OUTPUTS) +def test_ssm_mathematica_outputs_are_consistent(out): + rules = {c: ssm_rule(c) for c in SSM} + # no later rule is strictly more specific than an earlier one + for i in range(len(out)): + for j in range(i + 1, len(out)): + assert not rule_lt(rules[out[j]], rules[out[i]]), (out, out[i], out[j]) + # inserting in Mathematica's output order must reproduce it + session.evaluate("ClearAll[ssm]") + for c in out: + session.evaluate(SSM[c] + " = 1") + got = [str(r.pattern.expr) for r in rules_of("ssm")] + session.evaluate("ClearAll[ssm]") + for c in out: + session.evaluate(SSM[c] + " = 1") + expected = [str(rules[c].pattern.expr) for c in out] + assert got == expected + + +H_CASES = [ + # (definitions, order obtained in Mathematica) + ( + ["h[_]=1", "h[x_Integer,_]=1", "h[_Integer]=1"], + ["h[Blank[Integer]]", "h[Blank[]]", "h[Pattern[x, Blank[Integer]], Blank[]]"], + ), + ( + ["h[_]=1", "h[x_Integer,_]=1", "h[_Integer]=1", "h[x_Integer,y_Integer]=1"], + [ + "h[Blank[Integer]]", + "h[Blank[]]", + "h[Pattern[x, Blank[Integer]], Pattern[y, Blank[Integer]]]", + "h[Pattern[x, Blank[Integer]], Blank[]]", + ], + ), +] + + +@pytest.mark.parametrize(("defs", "expected"), H_CASES) +def test_insertion_scheme_matches_mathematica(defs, expected): + """A new rule goes before the first existing rule that contains it + (a binary search or a backwards scan give different results).""" + assert lhs_order("h", defs) == expected