Skip to content

Commit 90aec48

Browse files
dbrattliclaude
andauthored
fix(python): avoid duplicate captured argument in hoisted guards (#4610) (#4611)
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 773c098 commit 90aec48

2 files changed

Lines changed: 31 additions & 0 deletions

File tree

src/Fable.Transforms/Python/Fable2Python.Transforms.fs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3007,6 +3007,16 @@ let transformFunction
30073007
let cleanName (input: string) =
30083008
Regex.Replace(input, @"_mut(_\d+)?$", "")
30093009

3010+
// Names of this function's own arguments. TCO capture parameters must not
3011+
// collide with these, otherwise we emit a duplicate argument (see #4610).
3012+
let ownArgNames =
3013+
args
3014+
|> List.map (fun id ->
3015+
let (Identifier name) = ident com ctx id
3016+
name
3017+
)
3018+
|> Set.ofList
3019+
30103020
// For Python we need to append the TC-arguments to any declared (arrow) function inside the while-loop of the
30113021
// TCO. We will set them as default values to themselves e.g `i=i` to capture the value and not the variable.
30123022
let tcArgs, tcDefaults =
@@ -3019,6 +3029,11 @@ let transformFunction
30193029

30203030
match name with
30213031
| "tupled_arg_m" -> None // Remove these arguments (not sure why)
3032+
// Don't capture a TCO variable as a default parameter when this
3033+
// function already declares an argument with the same name: the
3034+
// local argument shadows the outer one and a duplicate parameter
3035+
// is a Python syntax error. See #4610.
3036+
| _ when ownArgNames.Contains name -> None
30223037
// Only capture TCO variables actually referenced in the function body.
30233038
// This avoids unnecessary default parameters on nested lambdas that don't
30243039
// use the outer TCO variables. See #3877.

tests/Python/TestPatternMatch.fs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,22 @@ let ``test guard expression capture with 5 cases`` () =
237237
guardCaptureMultiple 5 |> equal "small: 5"
238238
guardCaptureMultiple 0 |> equal "zero or negative"
239239

240+
// Guard that uses the bound value multiple times. The guard is hoisted into a
241+
// helper function and must not capture the outer argument as a duplicate
242+
// default parameter (see #4610).
243+
let mkTag (tag: string option) =
244+
match tag with
245+
| None -> ""
246+
| Some s when s.StartsWith("!") && not (s.StartsWith("!!")) -> s + " "
247+
| Some s -> "!<" + s + "> "
248+
249+
[<Fact>]
250+
let ``test guard reusing binding does not duplicate captured argument`` () =
251+
mkTag None |> equal ""
252+
mkTag (Some "!foo") |> equal "!foo "
253+
mkTag (Some "!!foo") |> equal "!<!!foo> "
254+
mkTag (Some "foo") |> equal "!<foo> "
255+
240256
// ----------------------------------------------------------------------------
241257
// 6. Nested Matching
242258
// ----------------------------------------------------------------------------

0 commit comments

Comments
 (0)