From 11d0246ab8ab14a52ec4312d948ab512d2d3fd04 Mon Sep 17 00:00:00 2001 From: Dag Brattli Date: Fri, 22 May 2026 17:39:54 +0200 Subject: [PATCH] fix(python): avoid duplicate captured argument in hoisted guards (#4610) 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) --- .../Python/Fable2Python.Transforms.fs | 15 +++++++++++++++ tests/Python/TestPatternMatch.fs | 16 ++++++++++++++++ 2 files changed, 31 insertions(+) diff --git a/src/Fable.Transforms/Python/Fable2Python.Transforms.fs b/src/Fable.Transforms/Python/Fable2Python.Transforms.fs index 5106ed88a2..91ff116623 100644 --- a/src/Fable.Transforms/Python/Fable2Python.Transforms.fs +++ b/src/Fable.Transforms/Python/Fable2Python.Transforms.fs @@ -3007,6 +3007,16 @@ let transformFunction let cleanName (input: string) = Regex.Replace(input, @"_mut(_\d+)?$", "") + // Names of this function's own arguments. TCO capture parameters must not + // collide with these, otherwise we emit a duplicate argument (see #4610). + let ownArgNames = + args + |> List.map (fun id -> + let (Identifier name) = ident com ctx id + name + ) + |> Set.ofList + // For Python we need to append the TC-arguments to any declared (arrow) function inside the while-loop of the // TCO. We will set them as default values to themselves e.g `i=i` to capture the value and not the variable. let tcArgs, tcDefaults = @@ -3019,6 +3029,11 @@ let transformFunction match name with | "tupled_arg_m" -> None // Remove these arguments (not sure why) + // Don't capture a TCO variable as a default parameter when this + // function already declares an argument with the same name: the + // local argument shadows the outer one and a duplicate parameter + // is a Python syntax error. See #4610. + | _ when ownArgNames.Contains name -> None // Only capture TCO variables actually referenced in the function body. // This avoids unnecessary default parameters on nested lambdas that don't // use the outer TCO variables. See #3877. diff --git a/tests/Python/TestPatternMatch.fs b/tests/Python/TestPatternMatch.fs index db829b4be1..dd852c25ce 100644 --- a/tests/Python/TestPatternMatch.fs +++ b/tests/Python/TestPatternMatch.fs @@ -237,6 +237,22 @@ let ``test guard expression capture with 5 cases`` () = guardCaptureMultiple 5 |> equal "small: 5" guardCaptureMultiple 0 |> equal "zero or negative" +// Guard that uses the bound value multiple times. The guard is hoisted into a +// helper function and must not capture the outer argument as a duplicate +// default parameter (see #4610). +let mkTag (tag: string option) = + match tag with + | None -> "" + | Some s when s.StartsWith("!") && not (s.StartsWith("!!")) -> s + " " + | Some s -> "!<" + s + "> " + +[] +let ``test guard reusing binding does not duplicate captured argument`` () = + mkTag None |> equal "" + mkTag (Some "!foo") |> equal "!foo " + mkTag (Some "!!foo") |> equal "! " + mkTag (Some "foo") |> equal "! " + // ---------------------------------------------------------------------------- // 6. Nested Matching // ----------------------------------------------------------------------------