From 9cddb79893449d37a8d9655ae280a49ebd7d5698 Mon Sep 17 00:00:00 2001 From: Dag Brattli Date: Wed, 30 Sep 2026 18:58:23 +0200 Subject: [PATCH] fix(python): avoid name collisions in nested tuple matches --- src/Fable.Transforms/FSharp2Fable.Util.fs | 7 ++++ tests/Python/TestPatternMatch.fs | 50 +++++++++++++++++++++++ 2 files changed, 57 insertions(+) diff --git a/src/Fable.Transforms/FSharp2Fable.Util.fs b/src/Fable.Transforms/FSharp2Fable.Util.fs index 61eb69055e..13d68c3871 100644 --- a/src/Fable.Transforms/FSharp2Fable.Util.fs +++ b/src/Fable.Transforms/FSharp2Fable.Util.fs @@ -810,6 +810,13 @@ module Helpers = || ctx.UsedNamesInDeclarationScope.Contains name let getIdentUniqueName (com: Compiler) (ctx: Context) name = + // Reserve the emitted Python name so tuple-match temporaries cannot shadow + // existing bindings when camelCase is converted to snake_case. + let name = + match com.Options.Language with + | Python -> Fable.Py.Naming.toPythonNaming name + | _ -> name + let sanitizeIdent = match com.Options.Language with | Python -> Fable.Py.Naming.sanitizeIdent diff --git a/tests/Python/TestPatternMatch.fs b/tests/Python/TestPatternMatch.fs index 932d0eb987..0095333d90 100644 --- a/tests/Python/TestPatternMatch.fs +++ b/tests/Python/TestPatternMatch.fs @@ -311,6 +311,56 @@ let ``test nested match complex`` () = nestedComplex 2 2 |> equal "x=2, y=other" nestedComplex 3 1 |> equal "x=other" +// https://github.com/fable-compiler/Fable/issues/5026 +module NestedMatchNames = + type Shape = + | Circle of radius: int + | Square + + let describe (shapes: Shape list) n = + match List.head shapes with + | Circle radius -> + match n + 1, radius with + | 2, 1 -> "unit circle" + | _ -> "other" + | Square -> "square" + + let describeInClosure (shapes: Shape list) = + match List.head shapes with + | Circle radius -> + fun xs -> + match List.tryHead xs, radius with + | Some 2, 1 -> "unit circle" + | _ -> "other" + | Square -> fun _ -> "square" + + let describeWithUserBinding (match_value: string) n = + match n + 1, n + 2 with + | 2, 3 -> match_value + | _ -> "other" + +[] +let ``test nested tuple match preserves outer union fields`` () = + NestedMatchNames.describe [ NestedMatchNames.Circle 1 ] 1 |> equal "unit circle" + NestedMatchNames.describe [ NestedMatchNames.Circle 2 ] 1 |> equal "other" + NestedMatchNames.describe [ NestedMatchNames.Circle 1 ] 2 |> equal "other" + NestedMatchNames.describe [ NestedMatchNames.Square ] 1 |> equal "square" + +[] +let ``test tuple match in closure preserves captured union fields`` () = + let describe = NestedMatchNames.describeInClosure [ NestedMatchNames.Circle 1 ] + describe [ 2 ] |> equal "unit circle" + describe [ 3 ] |> equal "other" + describe [] |> equal "other" + describe [ 2 ] |> equal "unit circle" + NestedMatchNames.describeInClosure [ NestedMatchNames.Circle 2 ] [ 2 ] |> equal "other" + NestedMatchNames.describeInClosure [ NestedMatchNames.Square ] [ 2 ] |> equal "square" + +[] +let ``test tuple match temporaries preserve user bindings`` () = + NestedMatchNames.describeWithUserBinding "matched" 1 |> equal "matched" + NestedMatchNames.describeWithUserBinding "matched" 2 |> equal "other" + // ---------------------------------------------------------------------------- // 7. Option Matching // ----------------------------------------------------------------------------