From 5f92aec10d8bc9fbd0086406895c05a2ac833343 Mon Sep 17 00:00:00 2001 From: Mangel Maxime Date: Mon, 2 Nov 2026 11:49:00 +0100 Subject: [PATCH] fix(all): rename idents of a witness inlined twice --- src/Fable.Transforms/FSharp2Fable.Util.fs | 30 +++++++++++++ src/Fable.Transforms/FSharp2Fable.fs | 4 +- tests/Js/Main/ApplicativeTests.fs | 50 ++++++++++++++++++++++ tests/Python/TestApplicative.fs | 51 +++++++++++++++++++++++ 4 files changed, 134 insertions(+), 1 deletion(-) diff --git a/src/Fable.Transforms/FSharp2Fable.Util.fs b/src/Fable.Transforms/FSharp2Fable.Util.fs index 9fcd034fa6..b9ff3043a9 100644 --- a/src/Fable.Transforms/FSharp2Fable.Util.fs +++ b/src/Fable.Transforms/FSharp2Fable.Util.fs @@ -1998,6 +1998,36 @@ module Identifiers = let tryGetIdentFromScope (ctx: Context) r typ (fsRef: FSharpMemberOrFunctionOrValue) = tryGetIdentFromScopeIf ctx r typ fsRef.Equals + let getDeclaredIdentNames (expr: Fable.Expr) = + let names = HashSet() + + let rec loop (e: Fable.Expr) = + let declared: Fable.Ident list = + match e with + | Fable.Let(i, _, _) + | Fable.Lambda(i, _, _) + | Fable.ForLoop(i, _, _, _, _, _) + | Fable.TryCatch(_, Some(i, _), _, _) -> [ i ] + | Fable.LetRec(bindings, _) -> List.map fst bindings + | Fable.Delegate(args, _, _, _) -> args + | Fable.DecisionTree(_, targets) -> List.collect fst targets + | Fable.ObjectExpr(members, _, _) -> List.collect (fun (m: Fable.ObjectExprMember) -> m.Args) members + | _ -> [] + + for i in declared do + names.Add(i.Name) |> ignore + + match e with + | Fable.Unresolved(Fable.UnresolvedTraitCall(argExprs = argExprs), _, _) -> argExprs + | Fable.Unresolved(Fable.UnresolvedInlineCall(_, _, callee, info), _, _) -> + Option.toList callee @ Option.toList info.ThisArg @ info.Args + | Fable.Unresolved(Fable.UnresolvedReplaceCall(thisArg, args, _, _), _, _) -> Option.toList thisArg @ args + | e -> getSubExpressions e + |> List.iter loop + + loop expr + set names + module Util = open Helpers open Patterns diff --git a/src/Fable.Transforms/FSharp2Fable.fs b/src/Fable.Transforms/FSharp2Fable.fs index 46edbf930e..724142ae23 100644 --- a/src/Fable.Transforms/FSharp2Fable.fs +++ b/src/Fable.Transforms/FSharp2Fable.fs @@ -2875,13 +2875,15 @@ let resolveInlineExpr (com: IFableCompiler) ctx info expr = | None -> transformTraitCall com ctx r t sourceTypes traitName isInstance argTypes argExprs | Some w -> // As witnesses come from the context, idents may be duplicated, see #2855 + // The same witness can be inlined several times in one scope, see #3921 let info = { info with + ScopeIdents = Set.union info.ScopeIdents (getDeclaredIdentNames w.Expr) ResolvedIdents = Dictionary() FileName = w.FileName } - let callee = resolveInlineExpr com ctx { info with FileName = w.FileName } w.Expr + let callee = resolveInlineExpr com ctx info w.Expr let callInfo = makeCallInfo None argExprs argTypes makeCall r t callInfo callee diff --git a/tests/Js/Main/ApplicativeTests.fs b/tests/Js/Main/ApplicativeTests.fs index 4916bed08e..1077f7f75a 100644 --- a/tests/Js/Main/ApplicativeTests.fs +++ b/tests/Js/Main/ApplicativeTests.fs @@ -1727,6 +1727,53 @@ module MultipleInlines = let private mapMyList (xs: NonEmptyList) : NonEmptyList = map (fun s -> s + "_") xs + let mutable codecCalls = 0 + + type Codec<'a> = Codec of (unit -> int) + + type Codecs = + static member inline Codecs(_output: int, _mthd: Codecs) : Codec = Codec(fun () -> 0) + static member inline Codecs(_output: bool, _mthd: Codecs) : Codec = Codec(fun () -> 0) + + static member inline Invoke() = + let inline call_2 (a: ^a, b: ^b) = + ((^a or ^b): (static member Codecs: _ * _ -> _) b, a) + + let inline call (a: 'a) = + call_2 (a, Unchecked.defaultof<'r>): Codec<'r> + + call Unchecked.defaultof + + type Codecs with + static member inline Codecs< ^a, ^b + when (^a): (static member codecInner: Codec< ^b > -> Codec< ^a >) + and (Codecs or ^b): (static member Codecs: ^b * Codecs -> Codec< ^b >)> + (_output: ^a, _mthd: Codecs) + : Codec< ^a > = + codecCalls <- codecCalls + 1 + let x = codecCalls * 10 + let (Codec inner) = 'a.codecInner (Codecs.Invoke()) + Codec(fun () -> x + inner ()) + + let inline getCodecs<'a, 'b + when (Codecs or ^a): (static member Codecs: ^a * Codecs -> Codec< ^a >) + and (Codecs or ^b): (static member Codecs: ^b * Codecs -> Codec< ^b >)> + () + : Codec< ^a > * Codec< ^b > = + let a: Codec<'a> = Codecs.Invoke() + let b: Codec<'b> = Codecs.Invoke() + (a, b) + + type CodecContainer<'T> = + | CodecContainer + + static member codecInner(Codec f: Codec<'T>) : Codec> = Codec(fun () -> f () + 1) + + let getContainerCodecs () = + codecCalls <- 0 + let (Codec a), (Codec b) = (getCodecs () : Codec> * Codec>) + a (), b () + let tests = [ testCase "Trait call works with multiple inlined functions I" <| fun () -> // See #2809 let account = Some { Details = { Tag = "Test"; InsurancePolicyData = None }} @@ -1748,6 +1795,9 @@ module MultipleInlines = testCase "Identifiers from witnesses don't get duplicated when resolving inline expressions" <| fun () -> // See #2855 NonEmptyList("a", ["b"; "c"]) |> mapMyList |> equal (NonEmptyList("a_", ["b_"; "c_"])) + + testCase "Identifiers from a witness inlined twice in the same scope don't get duplicated" <| fun () -> // See #3921 + getContainerCodecs () |> equal (11, 21) ] module AccessorFunctionShorthand = diff --git a/tests/Python/TestApplicative.fs b/tests/Python/TestApplicative.fs index af0164b7c2..a3d636ca8b 100644 --- a/tests/Python/TestApplicative.fs +++ b/tests/Python/TestApplicative.fs @@ -1659,6 +1659,53 @@ module MultipleInlines = let mapMyList (xs: NonEmptyList) : NonEmptyList = map (fun s -> s + "_") xs + let mutable codecCalls = 0 + + type Codec<'a> = Codec of (unit -> int) + + type Codecs = + static member inline Codecs(_output: int, _mthd: Codecs) : Codec = Codec(fun () -> 0) + static member inline Codecs(_output: bool, _mthd: Codecs) : Codec = Codec(fun () -> 0) + + static member inline Invoke() = + let inline call_2 (a: ^a, b: ^b) = + ((^a or ^b): (static member Codecs: _ * _ -> _) b, a) + + let inline call (a: 'a) = + call_2 (a, Unchecked.defaultof<'r>): Codec<'r> + + call Unchecked.defaultof + + type Codecs with + static member inline Codecs< ^a, ^b + when (^a): (static member codecInner: Codec< ^b > -> Codec< ^a >) + and (Codecs or ^b): (static member Codecs: ^b * Codecs -> Codec< ^b >)> + (_output: ^a, _mthd: Codecs) + : Codec< ^a > = + codecCalls <- codecCalls + 1 + let x = codecCalls * 10 + let (Codec inner) = 'a.codecInner (Codecs.Invoke()) + Codec(fun () -> x + inner ()) + + let inline getCodecs<'a, 'b + when (Codecs or ^a): (static member Codecs: ^a * Codecs -> Codec< ^a >) + and (Codecs or ^b): (static member Codecs: ^b * Codecs -> Codec< ^b >)> + () + : Codec< ^a > * Codec< ^b > = + let a: Codec<'a> = Codecs.Invoke() + let b: Codec<'b> = Codecs.Invoke() + (a, b) + + type CodecContainer<'T> = + | CodecContainer + + static member codecInner(Codec f: Codec<'T>) : Codec> = Codec(fun () -> f () + 1) + + let getContainerCodecs () = + codecCalls <- 0 + let (Codec a), (Codec b) = (getCodecs () : Codec> * Codec>) + a (), b () + open MultipleInlines [] @@ -1685,6 +1732,10 @@ let ``test Trait call works with multiple inlined functions II`` () = // See #28 let ``test Identifiers from witnesses don't get duplicated when resolving inline expressions`` () = // See #2855 NonEmptyList("a", ["b"; "c"]) |> mapMyList |> equal (NonEmptyList("a_", ["b_"; "c_"])) +[] +let ``test Identifiers from a witness inlined twice in the same scope don't get duplicated`` () = // See #3921 + getContainerCodecs () |> equal (11, 21) + module AccessorFunctionShorthand = type User =