Skip to content

fix(python): avoid duplicate captured argument in hoisted guards (#4610)#4611

Merged
dbrattli merged 1 commit into
mainfrom
fix/4610-python-guard-duplicate-arg
May 23, 2026
Merged

fix(python): avoid duplicate captured argument in hoisted guards (#4610)#4611
dbrattli merged 1 commit into
mainfrom
fix/4610-python-guard-duplicate-arg

Conversation

@dbrattli
Copy link
Copy Markdown
Collaborator

Problem

Fixes #4610. A pattern-match guard that reuses its bound value generates invalid Python with a duplicate argument:

let mkTag (tag: string option) =
    match tag with
    | None -> ""
    | Some s when s.StartsWith("!") && not (s.StartsWith("!!")) -> s + " "
    | Some s -> "!<" + s + "> "
def _arrow0(tag: str, tag: Any=tag) -> bool:  # SyntaxError: duplicate argument 'tag'

This regressed in 5.0.0 (worked in 5.0.0-alpha.21).

Root cause

When a guard reuses its binding, it is hoisted into a helper function that already takes the scrutinee as its own parameter (tag). Fable's tail-call optimization in transformFunction appends the enclosing function's arguments as default-valued capture parameters (x: Any=x) to nested functions — and it appended tag again, even though the helper already declares it.

Fix

Skip a TCO capture parameter when the function already declares an argument with the same name. The local argument shadows the outer one, so the capture is both redundant and a Python syntax error.

Testing

  • Added test guard reusing binding does not duplicate captured argument to tests/Python/TestPatternMatch.fs using the exact repro from the issue.
  • Full Python test suite passes (2349 tests).

🤖 Generated with Claude Code

A pattern-match guard that reuses its bound value is hoisted into a
helper function which already takes the scrutinee as its own argument.
The tail-call optimization then also appended that name as a captured
default parameter (`x: Any=x`), producing `def _arrow(x, x=x)` which is
a Python `SyntaxError: duplicate argument`.

Skip a TCO capture parameter when the function already declares an
argument with the same name: the local argument shadows the outer one,
so the capture is redundant.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@github-actions
Copy link
Copy Markdown
Contributor

Python Type Checking Results (Pyright)

Metric Value
Total errors 34
Files with errors 4
Excluded files 4
New errors ✅ No
Excluded files with errors (4 files)

These files have known type errors and are excluded from CI. Remove from pyrightconfig.ci.json as errors are fixed.

File Errors Status
temp/tests/Python/test_hash_set.py 18 Excluded
temp/tests/Python/test_applicative.py 12 Excluded
temp/tests/Python/test_nested_and_recursive_pattern.py 2 Excluded
temp/tests/Python/fable_modules/thoth_json_python/encode.py 2 Excluded

@dbrattli dbrattli merged commit 90aec48 into main May 23, 2026
32 checks passed
@dbrattli dbrattli deleted the fix/4610-python-guard-duplicate-arg branch May 23, 2026 07:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Python] Pattern matching guard with multiple usage of binding fails

1 participant