diff --git a/src/Compiler/Checking/Expressions/CheckExpressions.fs b/src/Compiler/Checking/Expressions/CheckExpressions.fs index 5a6bbf619fa..ff9f3c89bf0 100644 --- a/src/Compiler/Checking/Expressions/CheckExpressions.fs +++ b/src/Compiler/Checking/Expressions/CheckExpressions.fs @@ -1690,6 +1690,11 @@ let GeneralizeVal (cenv: cenv) denv enclosingDeclaredTypars generalizedTyparsFor // This is just about the only place we form a GeneralizedType let tyScheme = GeneralizedType(generalizedTypars, ty) + // Decouple SRTP solution ref cells so codegen mutations don't bleed across FSI submissions. + // In compiled code, pickling naturally creates fresh cells. See #12386. + if cenv.g.isInteractive then + decoupleTraitSolutions generalizedTypars + PrelimVal2(id, tyScheme, prelimValReprInfo, memberInfoOpt, isMutable, inlineFlag, baseOrThis, argAttribs, vis, isCompGen, hasDeclaredTypars) let GeneralizeVals (cenv: cenv) denv enclosingDeclaredTypars generalizedTypars types = diff --git a/src/Compiler/TypedTree/TypedTree.fs b/src/Compiler/TypedTree/TypedTree.fs index 995071138b5..733b269b588 100644 --- a/src/Compiler/TypedTree/TypedTree.fs +++ b/src/Compiler/TypedTree/TypedTree.fs @@ -2631,6 +2631,10 @@ type TraitConstraintInfo = with get() = (let (TTrait(solution = sln)) = x in sln.Value) and set v = (let (TTrait(solution = sln)) = x in sln.Value <- v) + member x.CloneWithFreshSolution() = + let (TTrait(a, b, c, d, e, f, sln)) = x + TTrait(a, b, c, d, e, f, ref sln.Value) + member x.WithMemberKind(kind) = (let (TTrait(a, b, c, d, e, f, g)) = x in TTrait(a, b, { c with MemberKind=kind }, d, e, f, g)) member x.WithSupportTypes(tys) = (let (TTrait(_, b, c, d, e, f, g)) = x in TTrait(tys, b, c, d, e, f, g)) diff --git a/src/Compiler/TypedTree/TypedTree.fsi b/src/Compiler/TypedTree/TypedTree.fsi index 0cd4bfd2305..7fbe446641a 100644 --- a/src/Compiler/TypedTree/TypedTree.fsi +++ b/src/Compiler/TypedTree/TypedTree.fsi @@ -1791,6 +1791,8 @@ type TraitConstraintInfo = /// Get or set the solution of the member constraint during inference member Solution: TraitConstraintSln option with get, set + member CloneWithFreshSolution: unit -> TraitConstraintInfo + /// The member kind is irrelevant to the logical properties of a trait. However it adjusts /// the extension property MemberDisplayNameCore member WithMemberKind: SynMemberKind -> TraitConstraintInfo diff --git a/src/Compiler/TypedTree/TypedTreeOps.Remap.fs b/src/Compiler/TypedTree/TypedTreeOps.Remap.fs index b598accceff..605b189ce17 100644 --- a/src/Compiler/TypedTree/TypedTreeOps.Remap.fs +++ b/src/Compiler/TypedTree/TypedTreeOps.Remap.fs @@ -497,6 +497,20 @@ module internal TypeRemapping = let copySlotSig ss = remapSlotSig (fun _ -> []) Remap.Empty ss + /// Decouple SRTP constraint solution ref cells on typars from any shared expression-tree nodes. + /// In FSI, codegen mutates shared TTrait solution cells after typechecking; decoupling at + /// generalization prevents stale solutions from bleeding into subsequent submissions. See #12386. + let decoupleTraitSolutions (typars: Typars) = + for tp in typars do + tp.SetConstraints( + tp.Constraints + |> List.map (fun cx -> + match cx with + | TyparConstraint.MayResolveMember(traitInfo, m) -> + TyparConstraint.MayResolveMember(traitInfo.CloneWithFreshSolution(), m) + | c -> c) + ) + let mkTyparToTyparRenaming tpsorig tps = let tinst = generalizeTypars tps mkTyparInst tpsorig tinst, tinst diff --git a/src/Compiler/TypedTree/TypedTreeOps.Remap.fsi b/src/Compiler/TypedTree/TypedTreeOps.Remap.fsi index 090d07beeb1..04cc615a670 100644 --- a/src/Compiler/TypedTree/TypedTreeOps.Remap.fsi +++ b/src/Compiler/TypedTree/TypedTreeOps.Remap.fsi @@ -193,6 +193,9 @@ module internal TypeRemapping = /// Copy a method slot signature, including new generic type parameters if the slot signature represents a generic method val copySlotSig: SlotSig -> SlotSig + /// Decouple SRTP constraint solution ref cells on typars from shared expression-tree nodes. + val decoupleTraitSolutions: Typars -> unit + val mkTyparToTyparRenaming: Typars -> Typars -> TyparInstantiation * TTypes val mkTyconInst: Tycon -> TypeInst -> TyparInstantiation diff --git a/tests/FSharp.Compiler.ComponentTests/ConstraintSolver/MemberConstraints.fs b/tests/FSharp.Compiler.ComponentTests/ConstraintSolver/MemberConstraints.fs index 9e04bc75dfc..d9b31531d15 100644 --- a/tests/FSharp.Compiler.ComponentTests/ConstraintSolver/MemberConstraints.fs +++ b/tests/FSharp.Compiler.ComponentTests/ConstraintSolver/MemberConstraints.fs @@ -114,6 +114,34 @@ ignore ["1" .. "42"] |> withSingleDiagnostic (Error 1, Line 2, Col 9, Line 2, Col 12, "The type 'string' does not support the operator 'op_Range'") + // https://github.com/dotnet/fsharp/issues/12386 + [] + let ``Issue 12386 - SRTP trait call should resolve correct overload at runtime`` () = + FSharp + """ +type A = + | A + static member ($) (A, _a: float) = 0.0 + static member ($) (A, _a: decimal) = 0M + static member ($) (A, _a: 't) = 0 + +let inline call x = ($) A x + +[] +let main _ = + let resultFloat = call 42.0 + let resultDecimal = call 42M + let resultInt = call 42 + if resultFloat <> 0.0 then failwithf "Expected 0.0 but got %A" resultFloat + if resultDecimal <> 0M then failwithf "Expected 0M but got %A" resultDecimal + if resultInt <> 0 then failwithf "Expected 0 but got %A" resultInt + printfn "All SRTP overload resolutions correct" + 0 + """ + |> asExe + |> compileExeAndRun + |> shouldSucceed + // https://github.com/dotnet/fsharp/issues/6648 [] let ``Issue 6648 - DU of DUs with inline static members should compile`` () = diff --git a/tests/FSharp.Compiler.ComponentTests/Scripting/Interactive.fs b/tests/FSharp.Compiler.ComponentTests/Scripting/Interactive.fs index ea7e3fbe312..0d597f182e2 100644 --- a/tests/FSharp.Compiler.ComponentTests/Scripting/Interactive.fs +++ b/tests/FSharp.Compiler.ComponentTests/Scripting/Interactive.fs @@ -153,6 +153,76 @@ module MultiEmit = """if B.v <> 9.3 then failwith $"9: Failed {A.v} <> 9.3" """ |] |> Seq.iter(fun item -> item |> scriptIt) + // https://github.com/dotnet/fsharp/issues/12386 + // The bug manifests when the SRTP-constrained inline function is defined in one FSI submission + // and called in a separate submission. Single-unit compilation works fine. + // When a type has 2+ specific overloads plus a generic catch-all for operator ($), the SRTP + // constraint stays unresolved across submissions, causing value restriction (FS0030) or wrong + // runtime dispatch (NullReferenceException). Works fine within a single submission and in + // multi-file compiled projects. + [] + [] + [] + let ``Issue 12386 - SRTP trait call resolves correct overload across FSI submissions`` (useMultiEmit) = + let args: string array = [| if useMultiEmit then "--multiemit+" else "--multiemit-" |] + use session = new FSharpScript(additionalArgs = args) + + // Submission 1: Define type with overloaded ($) and an inline function using SRTP + session.Eval( + """ +type A = A with + static member ($) (A, a: float ) = 0.0 + static member ($) (A, a: decimal) = 0M + static member ($) (A, a: 't ) = 0 + +let inline call x = ($) A x +""" + ) + |> ignoreValue + + // Submission 2: Call with float - should resolve to the float overload, not the generic one + let result = session.Eval("call 42.") |> getValue + let fsiVal = result.Value + Assert.Equal(typeof, fsiVal.ReflectionType) + Assert.Equal(0.0, fsiVal.ReflectionValue :?> float) + + // Submission 3: Call with decimal - should resolve to the decimal overload + let result2 = session.Eval("call 42M") |> getValue + let fsiVal2 = result2.Value + Assert.Equal(typeof, fsiVal2.ReflectionType) + Assert.Equal(0M, fsiVal2.ReflectionValue :?> decimal) + + // Same scenario as Issue 12386 but via compiled cross-project reference (not FSI). + // This verifies whether the bug is FSI-specific or also affects project references. + [] + let ``Issue 12386 - SRTP trait call resolves correct overload across project references`` () = + let lib = + FSharp + """ +namespace Lib +type A = A with + static member ($) (A, a: float ) = 0.0 + static member ($) (A, a: decimal) = 0M + static member ($) (A, a: 't ) = 0 + +module Calls = + let inline call x = ($) A x +""" + |> asLibrary + + FSharp + """ +module App +open Lib.Calls + +let result = call 42. +if result <> 0.0 then failwithf "Expected 0.0 but got %A" result +""" + |> withReferences [ lib ] + |> asExe + |> compileExeAndRun + |> shouldSucceed + [] let ``Version directive displays version and environment info``() = Fsx """