Skip to content

gh-145019: improve SyntaxError when match patterns bind different names#145939

Open
picnixz wants to merge 3 commits intopython:mainfrom
picnixz:feat/parser/syntax-error-match-145019
Open

gh-145019: improve SyntaxError when match patterns bind different names#145939
picnixz wants to merge 3 commits intopython:mainfrom
picnixz:feat/parser/syntax-error-match-145019

Conversation

@picnixz
Copy link
Copy Markdown
Member

@picnixz picnixz commented Mar 14, 2026

Co-authored-by: AN Long <aisk@users.noreply.github.com>
_PyCompile_Error(
c, LOC(p),
"alternative patterns bind different names "
"(first pattern binds %S, pattern %zd binds %S)",
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: rather than "first pattern... pattern X...", let's be consistent.

"second/third/etc pattern" is tricky, so:

Suggested change
"(first pattern binds %S, pattern %zd binds %S)",
"(pattern 1 binds %S, pattern %zd binds %S)",

Comment on lines +1520 to +1530
SyntaxError: alternative patterns bind different names (first pattern binds no names, pattern 2 binds ['x'])

>>> match 1:
... case ("user", {"id": id}) | ("admin", {"name": name}): pass
Traceback (most recent call last):
SyntaxError: alternative patterns bind different names (first pattern binds ['id'], pattern 2 binds ['name'])

>>> match 1:
... case ("user", {"id": id}) | ("admin", {"id": id}) | ("other", {"ip": ip}): pass
Traceback (most recent call last):
SyntaxError: alternative patterns bind different names (first pattern binds ['id'], pattern 3 binds ['ip'])
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
SyntaxError: alternative patterns bind different names (first pattern binds no names, pattern 2 binds ['x'])
>>> match 1:
... case ("user", {"id": id}) | ("admin", {"name": name}): pass
Traceback (most recent call last):
SyntaxError: alternative patterns bind different names (first pattern binds ['id'], pattern 2 binds ['name'])
>>> match 1:
... case ("user", {"id": id}) | ("admin", {"id": id}) | ("other", {"ip": ip}): pass
Traceback (most recent call last):
SyntaxError: alternative patterns bind different names (first pattern binds ['id'], pattern 3 binds ['ip'])
SyntaxError: alternative patterns bind different names (pattern 1 binds no names, pattern 2 binds ['x'])
>>> match 1:
... case ("user", {"id": id}) | ("admin", {"name": name}): pass
Traceback (most recent call last):
SyntaxError: alternative patterns bind different names (pattern 1 binds ['id'], pattern 2 binds ['name'])
>>> match 1:
... case ("user", {"id": id}) | ("admin", {"id": id}) | ("other", {"ip": ip}): pass
Traceback (most recent call last):
SyntaxError: alternative patterns bind different names (pattern 1 binds ['id'], pattern 3 binds ['ip'])

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I actually wondered whether it was better to display the list as is or use some join to have a string. How do you feel about it?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you give an example?

Copy link
Copy Markdown
Member Author

@picnixz picnixz Apr 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Currently:l (I think)

  • pattern 1 binds ["a", "b"] but pattern k binds ["u", "v"]

Suggestion:

  1. (formatted) pattern 1 binds "a" and "b" but pattern k binds "u" and "v"
  2. (minimal) pattern k binds "u' and "v' but pattern 1 does not
  3. (minimal swapped) pattern 1 binds "a" and "b" but pattern k does not.
  4. If pattern 1 binds a and b and pattern k binds a and c, only report that patten k does not bind b (or vice-versa)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think either current or number 1. Current will be easier :)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes current is simple and easier to parse. It may however be better if we remove common entries (unless it is already done).

@picnixz
Copy link
Copy Markdown
Member Author

picnixz commented Apr 3, 2026

Thanks Hugo for the review! I am unavailable this week-end so I will address this next week. If you want to apply your changes, feel free to do so! Otherwise I will do it once I have my laptop.

_PyCompile_Error(c, LOC(p), "alternative patterns bind different names");
diff:;
PyObject *no_names = NULL;
if (PyList_GET_SIZE(control) == 0 || !mismatched_names) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unless I am missing something here the condition if (PyList_GET_SIZE(control) == 0 || !mismatched_names) has a permanently dead second operand. Both paths to goto diff (lines 6309-6311 and 6321-6323) unconditionally execute mismatched_names = Py_NewRef(pc->stores) before jumping, so mismatched_names is always non-NULL at the diff: label.

Similarly, the ternary at line 6428 (mismatched_names == NULL ? no_names : mismatched_names) always evaluates to mismatched_names.

diff:;
PyObject *no_names = NULL;
if (PyList_GET_SIZE(control) == 0 || !mismatched_names) {
no_names = PyUnicode_FromString("no names");
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This guy is only substituted when the first pattern (control) has 0 names. When a later pattern binds nothing (e.g., case (a, b) | 1:), mismatched_names is a valid empty list, so the error message would read:

alternative patterns bind different names (first pattern binds ['a', 'b'], pattern 2 binds [])

Showing [] instead of no names is inconsistent with the reverse case

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh good catch. I should have increased test coverage. I will put more cases to exercise this path. Thanks!

diff:;
PyObject *no_names = NULL;
if (PyList_GET_SIZE(control) == 0 || !mismatched_names) {
no_names = PyUnicode_FromString("no names");
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is freed later before the error: label, but error: itself doesn't free it. Currently safe because the only goto error from diff: is when no_names is NULL. But this is fragile as if future code between allocation and Py_XDECREF(no_names) adds a goto error, it would leak. Other variables (mismatched_names) use the pattern of cleanup in error:. Consider moving no_names to function scope and adding Py_XDECREF(no_names) to the error: block.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Improve SyntaxError message for inconsistent name binding in OR-patterns

4 participants