From 607755eed4b2b2f1996b5f85b8151b549347156e Mon Sep 17 00:00:00 2001 From: Stanislav Date: Fri, 16 Oct 2026 13:59:23 +0400 Subject: [PATCH] Flat patterns: a blank can take several elements before a literal; no element is lost Two bugs in matching against an expression whose head is Flat: * basic_match_expression pairs leading pattern elements of match count 1 with expression elements one by one. With Flat a blank x_ can take several elements (x_ in f[x_, z] matches f[x, y] in f[x, y, z]), so f[a_, 0] never matched f[x, y, 0]. Skip that shortcut for Flat heads as it is already skipped for Orderless ones. * After the first pattern element matched, the next one was still matched with "first" set, so in a partial match (fully=False) it could start anywhere, skipping elements; the skipped ones were then dropped from the result: f[x, y, z] /. f[a__, z] :> {a} gave {x}. Only the first element may leave elements before it. This also fixes #1381 and the examples in the Scope section of the Flat reference page (f[a, b, c, d, e] /. f[b, c, d] -> x). Co-Authored-By: Claude Opus 5.5 --- mathics/core/pattern/base.py | 3 +++ mathics/core/pattern/ordered.py | 6 ++++- test/builtin/test_patterns.py | 43 +++++++++++++++++++++++++++++++++ 3 files changed, 51 insertions(+), 1 deletion(-) diff --git a/mathics/core/pattern/base.py b/mathics/core/pattern/base.py index 16b6f96c41..2e942a5876 100644 --- a/mathics/core/pattern/base.py +++ b/mathics/core/pattern/base.py @@ -780,6 +780,9 @@ def match_yield(new_vars, _): new_parms["vars_dict"] = new_vars new_parms["element_index"] = parms["element_index"] + 1 new_parms["yield_func"] = element_yield + # Only the first element may leave elements unmatched before it (a + # partial match of a Flat expression); the next one starts right after. + new_parms["first"] = False del new_parms["element"] pattern.match_element( element=parms["next_element"], pattern_context=new_parms diff --git a/mathics/core/pattern/ordered.py b/mathics/core/pattern/ordered.py index 881331fc15..b66b3fb4d9 100644 --- a/mathics/core/pattern/ordered.py +++ b/mathics/core/pattern/ordered.py @@ -57,9 +57,13 @@ def yield_choice(pre_vars): # # without "leading_blanks", Range[5000] will be tested against {a__, b_} in a # call to get_match_candidates_count(), which is slow. + # + # With Flat, a pattern element whose match count is (1, 1) can still take + # several elements (x_ in f[x_, z] matches f[x, y] in f[x, y, z]), so the + # elements can not be paired one by one there either. unmatched_elements = expression.elements - leading_blanks = not A_ORDERLESS & attributes + leading_blanks = not (A_ORDERLESS | A_FLAT) & attributes for element in self.elements: match_count = element.get_match_count() diff --git a/test/builtin/test_patterns.py b/test/builtin/test_patterns.py index 6f1ccbe288..5cae352e18 100644 --- a/test/builtin/test_patterns.py +++ b/test/builtin/test_patterns.py @@ -82,6 +82,49 @@ def test_dispatch(str_expr, msgs, str_expected, fail_msg): ) +@pytest.mark.parametrize( + ("str_expr", "str_expected", "fail_msg"), + [ + ("ClearAll[f, g, o]; SetAttributes[f, Flat];", "Null", None), + ( + "MatchQ[f[x, y, z], f[a_, z]]", + "True", + "A blank before a literal can take several elements", + ), + ("Replace[f[x, y, z], f[a_, z] :> {a}]", "{f[x, y]}", None), + ( + "Replace[f[x, y, z], f[a_, z[_]] :> {a}]", + "Replace[f[x, y, z], f[a_, z[_]] :> {a}]", + None, + ), + ("Replace[f[x, y, z[1]], f[a_, z[_]] :> {a}]", "{f[x, y]}", None), + ("f[x, y, z] /. f[a__, z] :> {a}", "{x, y}", "No element is lost"), + ("f[x, y, z] /. f[a___, y, b___] :> {{a}, {b}}", "{{x}, {z}}", None), + ("f[a, b, c, d, e] /. f[b, c, d] -> x", "f[a, x, e]", "WMA docs for Flat"), + ("f[a, b, c, d, e] /. f[b, c, d] -> f[x, y]", "f[a, x, y, e]", None), + ("f[a, b, c, d, e] /. f[b] -> x", "f[a, x, c, d, e]", None), + ("f[a, b, c, d, e] /. f[d, b] -> x", "f[a, b, c, d, e]", "Order matters"), + ("f[w, x, y, z] /. f[x, z] -> q", "f[w, x, y, z]", "Not contiguous"), + ( + "SetAttributes[o, {Flat, OneIdentity}]; o[a_, 0] := 0; o[x, y, 0]", + "0", + None, + ), + ("SetAttributes[g, Flat]; g[a_, 0] := h[a]; g[x, y, 0]", "h[g[x, y]]", None), + ( + "ClearAll[g]; SetAttributes[g, Flat]; g[l__, SameTest -> s_] := {{l}, s};" + " g[{1, -1}, {2, 3}, SameTest -> t]", + "{{{1, -1}, {2, 3}}, t}", + "Issue #1381", + ), + ("ClearAll[f, g, o];", "Null", None), + ], +) +def test_flat_patterns(str_expr, str_expected, fail_msg): + """Pattern matching against expressions whose head is Flat""" + check_evaluation(str_expr, str_expected, failure_message=fail_msg) + + @pytest.mark.parametrize( ("str_expr", "msgs", "str_expected", "fail_msg"), [