From 8d3b4a61f47c70d4a7152b82a1c2daa2ac82eb06 Mon Sep 17 00:00:00 2001 From: Jakub Majocha <1760221+majocha@users.noreply.github.com> Date: Tue, 20 Oct 2026 23:57:45 +0200 Subject: [PATCH 1/8] Keep type parameters of struct state machines that only their type mentions GetIlxClosureFreeVars collects the type variables of a struct state machine from its MoveNext body only. A state machine whose data type mentions a type variable the body does not, as in generic code kept as calls by #19548, got no type parameter for it. The GenTypeAux fallback from #19710 then hid the missing type variable by emitting obj in its place, so the struct was non-generic and implemented IResumableStateMachine>. Collect the type variables of the state machine's own type as well. The SRTP 14 baseline now initializes the generic struct through a TypeSpec. --- .../.FSharp.Compiler.Service/11.0.200.md | 1 + src/Compiler/CodeGen/IlxGen.fs | 8 ++++++++ ...nresolved trait from composed inline function.bsl | 12 ++++++------ 3 files changed, 15 insertions(+), 6 deletions(-) diff --git a/docs/release-notes/.FSharp.Compiler.Service/11.0.200.md b/docs/release-notes/.FSharp.Compiler.Service/11.0.200.md index 21ec3750767..0ad8d02b9a9 100644 --- a/docs/release-notes/.FSharp.Compiler.Service/11.0.200.md +++ b/docs/release-notes/.FSharp.Compiler.Service/11.0.200.md @@ -6,6 +6,7 @@ ### Fixed +* Fix struct state machines in generic code dropping type parameters that only their data type mentions, which code generation had replaced with `obj`. * Fix `--pathmap` not producing path-independent binaries for portable PDBs: the debug directory was reserved from the original PDB path rather than the mapped one, so output retained padding proportional to the original path's length. ([PR #20509](https://github.com/dotnet/fsharp/pull/20509)) * Capture diagnostics emitted when transparent project checking finishes a project, including FS0247 for namespace-module name collisions. ([Issue #6036](https://github.com/dotnet/fsharp/issues/6036), [PR #20706](https://github.com/dotnet/fsharp/pull/20706)) * Fix false IDE type-mismatch diagnostics for interpolated strings with multiple formatted holes. ([Issue #20752](https://github.com/dotnet/fsharp/issues/20752), [PR #20775](https://github.com/dotnet/fsharp/pull/20775)) diff --git a/src/Compiler/CodeGen/IlxGen.fs b/src/Compiler/CodeGen/IlxGen.fs index ed0c29ac3cb..7626138a652 100644 --- a/src/Compiler/CodeGen/IlxGen.fs +++ b/src/Compiler/CodeGen/IlxGen.fs @@ -7757,6 +7757,14 @@ and GetIlxClosureFreeVars cenv m (thisVars: ValRef list) boxity eenv takenNames | Local(_, _, Some(moreFtyvs, _)) -> unionFreeTyvars ftyvs moreFtyvs | _ -> ftyvs) + // A struct state machine's type can mention type variables its MoveNext body does not, e.g. through its Data type + let cloFreeTyvars = + match boxity with + | ILBoxity.AsValue -> + (cloFreeTyvars, thisVars) + ||> List.fold (fun ftyvs v -> unionFreeTyvars ftyvs (freeInType CollectTyparsNoCaching v.Type)) + | ILBoxity.AsObject -> cloFreeTyvars + let cloFreeTyvars = cloFreeTyvars.FreeTypars |> Zset.elements // Work out if the closure captures any witnesses. diff --git a/tests/FSharp.Compiler.ComponentTests/EmittedIL/DebugInlineAsCall/SRTP 14 - StateMachine with unresolved trait from composed inline function.bsl b/tests/FSharp.Compiler.ComponentTests/EmittedIL/DebugInlineAsCall/SRTP 14 - StateMachine with unresolved trait from composed inline function.bsl index 8c3aca87f16..880a773e6a1 100644 --- a/tests/FSharp.Compiler.ComponentTests/EmittedIL/DebugInlineAsCall/SRTP 14 - StateMachine with unresolved trait from composed inline function.bsl +++ b/tests/FSharp.Compiler.ComponentTests/EmittedIL/DebugInlineAsCall/SRTP 14 - StateMachine with unresolved trait from composed inline function.bsl @@ -47,7 +47,7 @@ Test::h IL_0008: callvirt Invoke IL_000d: stloc.0 IL_000e: ldloca.s 2 - IL_0010: initobj h@19-1 + IL_0010: initobj 0x1b000009 IL_0016: ldloca.s 2 IL_0018: stloc.3 IL_0019: ldc.i4.0 @@ -63,7 +63,7 @@ Test::h$W IL_0009: callvirt Invoke IL_000e: stloc.0 IL_000f: ldloca.s 2 - IL_0011: initobj h@19-3 + IL_0011: initobj 0x1b00000b IL_0017: ldloca.s 2 IL_0019: stloc.3 IL_001a: ldc.i4.0 @@ -91,14 +91,14 @@ S`1::Equals IL_0000: ldarg.1 IL_0001: stloc.0 IL_0002: ldloc.0 - IL_0003: isinst 0x1b00000a + IL_0003: isinst 0x1b00000c IL_0008: ldnull IL_0009: cgt.un IL_000b: brfalse.s IL_001d IL_000d: ldarg.1 - IL_000e: unbox.any 0x1b00000a + IL_000e: unbox.any 0x1b00000c IL_0013: stloc.1 IL_0014: ldarg.0 IL_0015: ldloc.1 @@ -119,14 +119,14 @@ S`1::Equals IL_0000: ldarg.1 IL_0001: stloc.0 IL_0002: ldloc.0 - IL_0003: isinst 0x1b00000a + IL_0003: isinst 0x1b00000c IL_0008: ldnull IL_0009: cgt.un IL_000b: brfalse.s IL_001c IL_000d: ldarg.1 - IL_000e: unbox.any 0x1b00000a + IL_000e: unbox.any 0x1b00000c IL_0013: stloc.1 IL_0014: ldarg.0 IL_0015: ldloc.1 From 11648958b52e919f63fb82407573f11248ad85b4 Mon Sep 17 00:00:00 2001 From: Jakub Majocha <1760221+majocha@users.noreply.github.com> Date: Tue, 20 Oct 2026 23:57:52 +0200 Subject: [PATCH 2/8] Revert the GenTypeAux type-variable fallback from #19710 #19710 made GenTypeAux default any type variable missing from the IL type environment with ChooseTyparSolution instead of reporting FS0073. It applies to every type variable, rigid declared ones included, so a broken codegen invariant now produces wrong IL silently. It masked #20812, where a class type parameter 'C is missing under parallel compilation and gets emitted as obj. The fallback fixed nothing reproducible. The #19709 repro and the #19710 regression test compile cleanly without it on main, at #19710's merge parent, and with SDKs 8.0.426 through 11.0 rc2, SDK 10.0.202 included. The regression test stays. --- .../.FSharp.Compiler.Service/11.0.200.md | 1 + src/Compiler/CodeGen/IlxGen.fs | 13 +------------ 2 files changed, 2 insertions(+), 12 deletions(-) diff --git a/docs/release-notes/.FSharp.Compiler.Service/11.0.200.md b/docs/release-notes/.FSharp.Compiler.Service/11.0.200.md index 0ad8d02b9a9..a14cf06463f 100644 --- a/docs/release-notes/.FSharp.Compiler.Service/11.0.200.md +++ b/docs/release-notes/.FSharp.Compiler.Service/11.0.200.md @@ -34,6 +34,7 @@ ### Changed +* Revert the IlxGen fallback from [PR #19710](https://github.com/dotnet/fsharp/pull/19710) that silently defaulted type variables missing from the IL type environment. Code generation reports FS0073 again instead of emitting wrong IL, which had masked [Issue #20812](https://github.com/dotnet/fsharp/issues/20812). * Remove the unused internal `--test:NoErrorText` switch and its argument-only diagnostic formatting. ([PR #20742](https://github.com/dotnet/fsharp/pull/20742)) * Assign seven previously preview-only features to [F# 11.2](https://fsharp.github.io/fsharp-compiler-docs/release-notes/Language.html#11.2), now the default language version. `default`, `latest`, and `latestmajor` select F# 11.2; explicit `11` and `11.0` retain F# 11.0 behavior. Preview SDK projects use the same compiler default unless `LangVersion` is explicitly set. `FromEndSlicing` remains preview-only. ([PR #20617](https://github.com/dotnet/fsharp/pull/20617)) * Add `SynExceptionDefnReprTrivia` with the range of the `exception` keyword to `SynExceptionDefnRepr`. ([Fantomas Issue #3483](https://github.com/fsprojects/fantomas/issues/3483), [PR #20612](https://github.com/dotnet/fsharp/pull/20612)) diff --git a/src/Compiler/CodeGen/IlxGen.fs b/src/Compiler/CodeGen/IlxGen.fs index 7626138a652..e293694c2fb 100644 --- a/src/Compiler/CodeGen/IlxGen.fs +++ b/src/Compiler/CodeGen/IlxGen.fs @@ -553,9 +553,6 @@ type TypeReprEnv // Random value for post-hoc diagnostic analysis on generated tree * uint16 666 - /// Check if a type parameter is in the environment - member _.ContainsKey(tp: Typar) = reprs.ContainsKey(tp.Stamp) - /// Add an additional type parameter to the environment. If the parameter is a units-of-measure parameter /// then it is ignored, since it doesn't correspond to a .NET type parameter. member tyenv.AddOne(tp: Typar) = @@ -736,15 +733,7 @@ and GenTypeAux cenv m (tyenv: TypeReprEnv) voidOK ptrsOK ty = else EraseClosures.mkILTyFuncTy cenv.ilxPubCloEnv - | TType_var(tp, _) -> - if tyenv.ContainsKey tp then - mkILTyvarTy tyenv[tp, m] - else - // Unsolved type variable not in the TypeReprEnv — can arise for inline SRTP - // functions where constraint resolution leaves phantom typars unsolved. - // Default the typar and generate the type for the default to avoid an ICE. - let defaultTy = TypeRelations.ChooseTyparSolution g cenv.amap tp - GenTypeAux cenv m tyenv voidOK ptrsOK defaultTy + | TType_var(tp, _) -> mkILTyvarTy tyenv[tp, m] | TType_measure _ -> g.ilg.typ_Int32 From 8aabe5f97abf05b554edc686081a104c3c393e6e Mon Sep 17 00:00:00 2001 From: Jakub Majocha <1760221+majocha@users.noreply.github.com> Date: Wed, 21 Oct 2026 00:31:24 +0200 Subject: [PATCH 3/8] Add PR link to release notes --- docs/release-notes/.FSharp.Compiler.Service/11.0.200.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/release-notes/.FSharp.Compiler.Service/11.0.200.md b/docs/release-notes/.FSharp.Compiler.Service/11.0.200.md index a14cf06463f..f2fe7dac393 100644 --- a/docs/release-notes/.FSharp.Compiler.Service/11.0.200.md +++ b/docs/release-notes/.FSharp.Compiler.Service/11.0.200.md @@ -6,7 +6,7 @@ ### Fixed -* Fix struct state machines in generic code dropping type parameters that only their data type mentions, which code generation had replaced with `obj`. +* Fix struct state machines in generic code dropping type parameters that only their data type mentions, which code generation had replaced with `obj`. ([PR #20821](https://github.com/dotnet/fsharp/pull/20821)) * Fix `--pathmap` not producing path-independent binaries for portable PDBs: the debug directory was reserved from the original PDB path rather than the mapped one, so output retained padding proportional to the original path's length. ([PR #20509](https://github.com/dotnet/fsharp/pull/20509)) * Capture diagnostics emitted when transparent project checking finishes a project, including FS0247 for namespace-module name collisions. ([Issue #6036](https://github.com/dotnet/fsharp/issues/6036), [PR #20706](https://github.com/dotnet/fsharp/pull/20706)) * Fix false IDE type-mismatch diagnostics for interpolated strings with multiple formatted holes. ([Issue #20752](https://github.com/dotnet/fsharp/issues/20752), [PR #20775](https://github.com/dotnet/fsharp/pull/20775)) @@ -34,7 +34,7 @@ ### Changed -* Revert the IlxGen fallback from [PR #19710](https://github.com/dotnet/fsharp/pull/19710) that silently defaulted type variables missing from the IL type environment. Code generation reports FS0073 again instead of emitting wrong IL, which had masked [Issue #20812](https://github.com/dotnet/fsharp/issues/20812). +* Revert the IlxGen fallback from [PR #19710](https://github.com/dotnet/fsharp/pull/19710) that silently defaulted type variables missing from the IL type environment. Code generation reports FS0073 again instead of emitting wrong IL, which had masked [Issue #20812](https://github.com/dotnet/fsharp/issues/20812). ([PR #20821](https://github.com/dotnet/fsharp/pull/20821)) * Remove the unused internal `--test:NoErrorText` switch and its argument-only diagnostic formatting. ([PR #20742](https://github.com/dotnet/fsharp/pull/20742)) * Assign seven previously preview-only features to [F# 11.2](https://fsharp.github.io/fsharp-compiler-docs/release-notes/Language.html#11.2), now the default language version. `default`, `latest`, and `latestmajor` select F# 11.2; explicit `11` and `11.0` retain F# 11.0 behavior. Preview SDK projects use the same compiler default unless `LangVersion` is explicitly set. `FromEndSlicing` remains preview-only. ([PR #20617](https://github.com/dotnet/fsharp/pull/20617)) * Add `SynExceptionDefnReprTrivia` with the range of the `exception` keyword to `SynExceptionDefnRepr`. ([Fantomas Issue #3483](https://github.com/fsprojects/fantomas/issues/3483), [PR #20612](https://github.com/dotnet/fsharp/pull/20612)) From cd9e6589f40bfc1279b719dc95fe1d2662214731 Mon Sep 17 00:00:00 2001 From: Jakub Majocha <1760221+majocha@users.noreply.github.com> Date: Wed, 21 Oct 2026 01:11:48 +0200 Subject: [PATCH 4/8] Default undetermined method type parameters of codegen trait solutions After inlining, the optimizer and IlxGen re-solve trait calls in a throwaway constraint solver state. Overload resolution freshens the chosen method's type parameters, and one the trait's types do not determine, such as 'X of 'static member M<'X>(x: int) : int', stays unsolved: unlike type checking, the codegen solve has no later pass that defaults it. Without --optimize the call to M survives into IL and IlxGen reports FS0073. SDK 8.0 and 10.0.1xx fail this way; the GenTypeAux fallback from #19710 hid it by emitting obj. Default such parameters right after the codegen solve, with ChooseTyparSolution as inference does. Only flexible, unsolved type parameters of the solution's method instantiation that do not occur in the trait's own types are touched. --- .../.FSharp.Compiler.Service/11.0.200.md | 1 + src/Compiler/Checking/ConstraintSolver.fs | 38 +++++++++++++--- .../ConstraintSolver/MemberConstraints.fs | 45 +++++++++++++++++++ 3 files changed, 77 insertions(+), 7 deletions(-) diff --git a/docs/release-notes/.FSharp.Compiler.Service/11.0.200.md b/docs/release-notes/.FSharp.Compiler.Service/11.0.200.md index f2fe7dac393..4935b5c1863 100644 --- a/docs/release-notes/.FSharp.Compiler.Service/11.0.200.md +++ b/docs/release-notes/.FSharp.Compiler.Service/11.0.200.md @@ -6,6 +6,7 @@ ### Fixed +* Fix internal error FS0073 when a trait resolved during code generation picks a method whose type parameter the trait does not determine, such as `static member M<'X>(x: int) : int`; the parameter is now defaulted as type inference would. ([PR #20821](https://github.com/dotnet/fsharp/pull/20821)) * Fix struct state machines in generic code dropping type parameters that only their data type mentions, which code generation had replaced with `obj`. ([PR #20821](https://github.com/dotnet/fsharp/pull/20821)) * Fix `--pathmap` not producing path-independent binaries for portable PDBs: the debug directory was reserved from the original PDB path rather than the mapped one, so output retained padding proportional to the original path's length. ([PR #20509](https://github.com/dotnet/fsharp/pull/20509)) * Capture diagnostics emitted when transparent project checking finishes a project, including FS0247 for namespace-module name collisions. ([Issue #6036](https://github.com/dotnet/fsharp/issues/6036), [PR #20706](https://github.com/dotnet/fsharp/pull/20706)) diff --git a/src/Compiler/Checking/ConstraintSolver.fs b/src/Compiler/Checking/ConstraintSolver.fs index 062d4ac376a..e71423b369f 100644 --- a/src/Compiler/Checking/ConstraintSolver.fs +++ b/src/Compiler/Checking/ConstraintSolver.fs @@ -4425,19 +4425,43 @@ let ApplyTyparDefaultAtPriority denv css priority (tp: Typar) = let CreateCodegenState tcVal g amap = ConstraintSolverState.New(g, amap, InfoReader(g, amap), tcVal) +/// Overload resolution freshens the chosen method's type parameters. A codegen solve has no later inference pass +/// to default those the trait's own types do not determine, e.g. 'X of 'static member M<'X>: int -> int', so +/// default them here as inference does at the end of checking. +let DefaultUndeterminedMethodTyparsOfCodegenSolution (csenv: ConstraintSolverEnv) m (traitInfo: TraitConstraintInfo) = + match traitInfo.Solution with + | Some(FSMethSln(minst = minst)) + | Some(ILMethSln(minst = minst)) -> + let traitTypars = + (freeInTypes CollectTypars (traitInfo.SupportTypes @ traitInfo.CompiledObjectAndArgumentTypes @ Option.toList traitInfo.CompiledReturnType)).FreeTypars + + (freeInTypes CollectTypars minst).FreeTypars + |> Zset.elements + |> IterateD (fun tp -> + if tp.IsSolved || tp.Rigidity <> TyparRigidity.Flexible || Zset.contains tp traitTypars then + CompleteD + else + SolveTyparEqualsType csenv 0 m NoTrace (mkTyparTy tp) (ChooseTyparSolution csenv.g csenv.amap tp)) + | _ -> CompleteD + let SolveMemberConstraintForCodegen tcVal g amap m (traitInfo: TraitConstraintInfo) = match traitInfo.Solution with | Some _ -> ResultD true | None -> let css = CreateCodegenState tcVal g amap let csenv = MakeConstraintSolverEnv ContextInfo.NoContext css m (DisplayEnv.Empty g) - if g.langVersion.SupportsFeature LanguageFeature.ExtensionConstraintSolutions then - // Failed probes must not persist provisional solutions in generic inline bodies. - NoTrace.CollectThenUndoOrCommit - (fun (res: OperationResult<_>) -> res.IsOkResult) - (fun trace -> SolveMemberConstraint csenv true PermitWeakResolution.Yes 0 m (WithTrace trace) traitInfo) - else - SolveMemberConstraint csenv true PermitWeakResolution.Yes 0 m NoTrace traitInfo + trackErrors { + let! res = + if g.langVersion.SupportsFeature LanguageFeature.ExtensionConstraintSolutions then + // Failed probes must not persist provisional solutions in generic inline bodies. + NoTrace.CollectThenUndoOrCommit + (fun (res: OperationResult<_>) -> res.IsOkResult) + (fun trace -> SolveMemberConstraint csenv true PermitWeakResolution.Yes 0 m (WithTrace trace) traitInfo) + else + SolveMemberConstraint csenv true PermitWeakResolution.Yes 0 m NoTrace traitInfo + do! DefaultUndeterminedMethodTyparsOfCodegenSolution csenv m traitInfo + return res + } /// Determine if a codegen witness for a trait will require witness args to be available, e.g. in generic code let CodegenWitnessExprForTraitConstraintWillRequireWitnessArgs tcVal g amap m (traitInfo:TraitConstraintInfo) = diff --git a/tests/FSharp.Compiler.ComponentTests/ConstraintSolver/MemberConstraints.fs b/tests/FSharp.Compiler.ComponentTests/ConstraintSolver/MemberConstraints.fs index ae5f1b2fbab..b653d543556 100644 --- a/tests/FSharp.Compiler.ComponentTests/ConstraintSolver/MemberConstraints.fs +++ b/tests/FSharp.Compiler.ComponentTests/ConstraintSolver/MemberConstraints.fs @@ -348,6 +348,51 @@ let main _ = |> compileExeAndRun |> shouldSucceed + // Resolving the trait after inlining picks a method whose type parameter the trait does not determine. + // Without --optimize the call to M survives into IL, where the undefaulted parameter caused FS0073. + [] + [] + [] + let ``Trait solved by a static method with an undetermined type parameter`` (optimize: bool) = + FSharp + """ +type C = + static member M<'X>(x: int) : int = x + 1 + +let inline call (w: ^T) = (^T : (static member M : int -> int) 1) + +[] +let main _ = + if call Unchecked.defaultof <> 2 then failwith "call failed" + 0 + """ + |> withOptimization optimize + |> asExe + |> compileExeAndRun + |> shouldSucceed + + [] + [] + [] + let ``Trait solved by an instance method with an undetermined type parameter`` (optimize: bool) = + FSharp + """ +type C() = + member _.M<'X>(x: int) : int = x + 1 + +let inline call (w: ^T) = (^T : (member M : int -> int) (w, 1)) +let inline wrap (w: ^T) = call w + +[] +let main _ = + if wrap (C()) <> 2 then failwith "wrap failed" + 0 + """ + |> withOptimization optimize + |> asExe + |> compileExeAndRun + |> shouldSucceed + // Regression for PR #19602 (RFC FS-1043): a non-inline binding with an unsatisfiable operator/SRTP // trait must fail at compile time (FS0041), not compile into a NotSupportedException stub that throws // at runtime (which also leaked at feature-off langversions). The deleted neg116 shape '(1.0 - t) * p' From b05b5d9eb4e61770ebd96e6ce24252308680d764 Mon Sep 17 00:00:00 2001 From: Jakub Majocha <1760221+majocha@users.noreply.github.com> Date: Wed, 21 Oct 2026 12:09:17 +0200 Subject: [PATCH 5/8] Finish codegen trait solutions where type inference would Replace the defaulting at the codegen solve from the previous commit. That point is too early: FSharpPlus has inline solution methods whose type parameters are only determined once the method is inlined and its nested traits are solved, and defaulting them to obj there broke its build. A codegen solve of a ground trait is a closed inference problem whose open type parameters belong to whatever processes the code generated from the solution. FinishCodegenTraitSolution finishes them the way ApplyDefaults and SolveInternalUnknowns finish a file (member constraints, defaults by priority, chosen solution, failures as FS0071), and is called once that processing is done: after OptimizeExpr in OptimizeTraitCall, after optimizeDuringCodeGen in GenTraitCall, and right after the solve for quotations and witness lambdas, which are emitted directly. Traits over generic types belong to inline templates and stay open. Besides the FS0073 for undetermined method type parameters, this rejects at compile time the programs that compiled into a "Dynamic invocation is not supported" throw (#8690), and makes #7384 independent of the runtime path. --- .../.FSharp.Compiler.Service/11.0.200.md | 2 +- src/Compiler/Checking/ConstraintSolver.fs | 95 +++++++++----- src/Compiler/Checking/ConstraintSolver.fsi | 6 + src/Compiler/Checking/QuotationTranslator.fs | 1 + src/Compiler/CodeGen/IlxGen.fs | 4 + src/Compiler/Optimize/Optimizer.fs | 6 +- .../ConstraintSolver/MemberConstraints.fs | 121 ++++++++++++++++++ 7 files changed, 204 insertions(+), 31 deletions(-) diff --git a/docs/release-notes/.FSharp.Compiler.Service/11.0.200.md b/docs/release-notes/.FSharp.Compiler.Service/11.0.200.md index 4935b5c1863..92381533194 100644 --- a/docs/release-notes/.FSharp.Compiler.Service/11.0.200.md +++ b/docs/release-notes/.FSharp.Compiler.Service/11.0.200.md @@ -6,7 +6,7 @@ ### Fixed -* Fix internal error FS0073 when a trait resolved during code generation picks a method whose type parameter the trait does not determine, such as `static member M<'X>(x: int) : int`; the parameter is now defaulted as type inference would. ([PR #20821](https://github.com/dotnet/fsharp/pull/20821)) +* Finish type inference for traits resolved during code generation: type parameters of the chosen method that the trait does not determine are inferred from their member constraints and defaulted as for a direct call. This fixes internal error FS0073 for methods such as `static member M<'X>(x: int) : int`, and programs that compiled into a `NotSupportedException` ("Dynamic invocation of ... is not supported") are now rejected at compile time. ([Issue #8690](https://github.com/dotnet/fsharp/issues/8690), [Issue #7384](https://github.com/dotnet/fsharp/issues/7384), [PR #20821](https://github.com/dotnet/fsharp/pull/20821)) * Fix struct state machines in generic code dropping type parameters that only their data type mentions, which code generation had replaced with `obj`. ([PR #20821](https://github.com/dotnet/fsharp/pull/20821)) * Fix `--pathmap` not producing path-independent binaries for portable PDBs: the debug directory was reserved from the original PDB path rather than the mapped one, so output retained padding proportional to the original path's length. ([PR #20509](https://github.com/dotnet/fsharp/pull/20509)) * Capture diagnostics emitted when transparent project checking finishes a project, including FS0247 for namespace-module name collisions. ([Issue #6036](https://github.com/dotnet/fsharp/issues/6036), [PR #20706](https://github.com/dotnet/fsharp/pull/20706)) diff --git a/src/Compiler/Checking/ConstraintSolver.fs b/src/Compiler/Checking/ConstraintSolver.fs index e71423b369f..f9ae9184a0b 100644 --- a/src/Compiler/Checking/ConstraintSolver.fs +++ b/src/Compiler/Checking/ConstraintSolver.fs @@ -4425,43 +4425,78 @@ let ApplyTyparDefaultAtPriority denv css priority (tp: Typar) = let CreateCodegenState tcVal g amap = ConstraintSolverState.New(g, amap, InfoReader(g, amap), tcVal) -/// Overload resolution freshens the chosen method's type parameters. A codegen solve has no later inference pass -/// to default those the trait's own types do not determine, e.g. 'X of 'static member M<'X>: int -> int', so -/// default them here as inference does at the end of checking. -let DefaultUndeterminedMethodTyparsOfCodegenSolution (csenv: ConstraintSolverEnv) m (traitInfo: TraitConstraintInfo) = - match traitInfo.Solution with - | Some(FSMethSln(minst = minst)) - | Some(ILMethSln(minst = minst)) -> - let traitTypars = - (freeInTypes CollectTypars (traitInfo.SupportTypes @ traitInfo.CompiledObjectAndArgumentTypes @ Option.toList traitInfo.CompiledReturnType)).FreeTypars - - (freeInTypes CollectTypars minst).FreeTypars - |> Zset.elements - |> IterateD (fun tp -> - if tp.IsSolved || tp.Rigidity <> TyparRigidity.Flexible || Zset.contains tp traitTypars then - CompleteD - else - SolveTyparEqualsType csenv 0 m NoTrace (mkTyparTy tp) (ChooseTyparSolution csenv.g csenv.amap tp)) - | _ -> CompleteD - let SolveMemberConstraintForCodegen tcVal g amap m (traitInfo: TraitConstraintInfo) = match traitInfo.Solution with | Some _ -> ResultD true | None -> let css = CreateCodegenState tcVal g amap let csenv = MakeConstraintSolverEnv ContextInfo.NoContext css m (DisplayEnv.Empty g) + if g.langVersion.SupportsFeature LanguageFeature.ExtensionConstraintSolutions then + // Failed probes must not persist provisional solutions in generic inline bodies. + NoTrace.CollectThenUndoOrCommit + (fun (res: OperationResult<_>) -> res.IsOkResult) + (fun trace -> SolveMemberConstraint csenv true PermitWeakResolution.Yes 0 m (WithTrace trace) traitInfo) + else + SolveMemberConstraint csenv true PermitWeakResolution.Yes 0 m NoTrace traitInfo + +/// Overload resolution freshens the chosen method's type parameters, and a codegen solve leaves those the trait +/// does not determine to whatever processes the code generated from the solution: inlining the method and solving +/// its nested traits. Once that is done, any still unsolved, e.g. 'X of 'static member M<'X>(x: int) : int', are +/// finished the way ApplyDefaults and SolveInternalUnknowns finish a file: their member constraints are solved, +/// then defaults by priority apply, then a solution is chosen. A trait over generic types belongs to an inline +/// template solved again at each call site, so its solution stays open. +let FinishCodegenTraitSolution tcVal g amap m (traitInfo: TraitConstraintInfo) = + let traitTys = + traitInfo.SupportTypes @ traitInfo.CompiledObjectAndArgumentTypes @ Option.toList traitInfo.CompiledReturnType + + match traitInfo.Solution with + | Some(FSMethSln(minst = minst)) + | Some(ILMethSln(minst = minst)) when (freeInTypes CollectTypars traitTys).FreeTypars.IsEmpty -> + let denv = DisplayEnv.Empty g + let csenv = MakeConstraintSolverEnv ContextInfo.NoContext (CreateCodegenState tcVal g amap) m denv + + let unsolved () = + (freeInTypes CollectTypars minst).FreeTypars + |> Zset.elements + |> List.filter (fun tp -> tp.Rigidity <> TyparRigidity.Rigid) + + let solveAsDefault tp ty = + TryD + (fun () -> SolveTyparEqualsType csenv 0 m NoTrace (mkTyparTy tp) ty) + (fun err -> ErrorD(ErrorFromApplyingDefault(g, denv, tp, ty, err, m))) + trackErrors { - let! res = - if g.langVersion.SupportsFeature LanguageFeature.ExtensionConstraintSolutions then - // Failed probes must not persist provisional solutions in generic inline bodies. - NoTrace.CollectThenUndoOrCommit - (fun (res: OperationResult<_>) -> res.IsOkResult) - (fun trace -> SolveMemberConstraint csenv true PermitWeakResolution.Yes 0 m (WithTrace trace) traitInfo) - else - SolveMemberConstraint csenv true PermitWeakResolution.Yes 0 m NoTrace traitInfo - do! DefaultUndeterminedMethodTyparsOfCodegenSolution csenv m traitInfo - return res + do! + IgnoreFailedMemberConstraintResolution + (fun () -> + let csenv = { csenv with ErrorOnFailedMemberConstraintResolution = true } + + RepeatWhileD 0 (fun ndeep -> + unsolved () + |> AtLeastOneD (fun tp -> + tp.Constraints + |> AtLeastOneD (function + | TyparConstraint.MayResolveMember(traitInfo2, m2) when traitInfo2.Solution.IsNone -> + SolveMemberConstraint csenv true PermitWeakResolution.Yes ndeep m2 NoTrace traitInfo2 + | _ -> ResultD false)))) + ErrorD + + do! + [ 10..-1..0 ] + |> IterateD (fun priority -> + unsolved () + |> IterateD (fun tp -> + tp.Constraints + |> IterateD (function + | TyparConstraint.DefaultsTo(priority2, defaultTy, _) when priority2 = priority && not tp.IsSolved -> + solveAsDefault tp defaultTy + | _ -> CompleteD))) + + do! + unsolved () + |> IterateD (fun tp -> if tp.IsSolved then CompleteD else solveAsDefault tp (ChooseTyparSolution g amap tp)) } + | _ -> CompleteD /// Determine if a codegen witness for a trait will require witness args to be available, e.g. in generic code let CodegenWitnessExprForTraitConstraintWillRequireWitnessArgs tcVal g amap m (traitInfo:TraitConstraintInfo) = @@ -4502,6 +4537,7 @@ let CodegenWitnessesForTyparInstWith tcVal g amap m typars tyargs f = traitInfo.Solution <- TryGetSelectedTraitSolution g (instTrait actualInst traitInfo) | _ -> () let! _res = SolveTyparsEqualTypesAux csenv 0 m NoTrace tinst tyargs + do! traitInfos |> IterateD (FinishCodegenTraitSolution tcVal g amap m) return [ for traitInfo in traitInfos -> f traitInfo (GenWitnessExprLambda amap g m traitInfo) ] } @@ -4513,6 +4549,7 @@ let CodegenWitnessArgForTraitConstraint tcVal g amap m traitInfo = let traitInfo = GetTraitConstraintForCodegen g traitInfo trackErrors { let! _res = SolveMemberConstraintForCodegen tcVal g amap m traitInfo + do! FinishCodegenTraitSolution tcVal g amap m traitInfo return GenWitnessExprLambda amap g m traitInfo } diff --git a/src/Compiler/Checking/ConstraintSolver.fsi b/src/Compiler/Checking/ConstraintSolver.fsi index 5dbb4ca9fa0..7a87958de8d 100644 --- a/src/Compiler/Checking/ConstraintSolver.fsi +++ b/src/Compiler/Checking/ConstraintSolver.fsi @@ -374,6 +374,12 @@ val ApplyTyparDefaultAtPriority: DisplayEnv -> ConstraintSolverState -> priority val CodegenWitnessExprForTraitConstraint: TcValF -> TcGlobals -> ImportMap -> range -> TraitConstraintInfo -> Expr list -> OperationResult +/// Finish the type parameters a codegen solution of a ground trait left open, once the code generated from the +/// solution has been processed: inlined and its nested traits solved. Callers of CodegenWitnessExprForTraitConstraint +/// call it with the same trait; the witness lambda functions call it themselves. +val FinishCodegenTraitSolution: + TcValF -> TcGlobals -> ImportMap -> range -> TraitConstraintInfo -> OperationResult + /// Determine if a codegen witness for a trait will require witness args to be available, e.g. in generic code val CodegenWitnessExprForTraitConstraintWillRequireWitnessArgs: TcValF -> TcGlobals -> ImportMap -> range -> TraitConstraintInfo -> OperationResult diff --git a/src/Compiler/Checking/QuotationTranslator.fs b/src/Compiler/Checking/QuotationTranslator.fs index 8c801a7b213..5420108e5f9 100644 --- a/src/Compiler/Checking/QuotationTranslator.fs +++ b/src/Compiler/Checking/QuotationTranslator.fs @@ -755,6 +755,7 @@ and private ConvExprCore cenv (env : QuotationTranslationEnv) (expr: Expr) : Exp | None -> wfail(Error(FSComp.SR.crefQuotationsCantCallTraitMembers(), m)) | Some expr -> + ConstraintSolver.FinishCodegenTraitSolution cenv.tcVal g cenv.amap m traitInfo |> CommitOperationResult ConvExpr cenv env expr | _ -> diff --git a/src/Compiler/CodeGen/IlxGen.fs b/src/Compiler/CodeGen/IlxGen.fs index e293694c2fb..50c44c6e452 100644 --- a/src/Compiler/CodeGen/IlxGen.fs +++ b/src/Compiler/CodeGen/IlxGen.fs @@ -6118,6 +6118,10 @@ and GenTraitCall (cenv: cenv) cgbuf eenv (traitInfo: TraitConstraintInfo, argExp GenExpr cenv cgbuf eenv replacementExpr sequel | Some expr -> let expr = cenv.optimizeDuringCodeGen false expr + + ConstraintSolver.FinishCodegenTraitSolution cenv.tcVal g cenv.amap m traitInfo + |> CommitOperationResult + GenExpr cenv cgbuf eenv expr sequel //-------------------------------------------------------------------------- diff --git a/src/Compiler/Optimize/Optimizer.fs b/src/Compiler/Optimize/Optimizer.fs index b05747b9fe9..bd8a038fb0a 100644 --- a/src/Compiler/Optimize/Optimizer.fs +++ b/src/Compiler/Optimize/Optimizer.fs @@ -3479,7 +3479,11 @@ and OptimizeTraitCall cenv env (traitInfo, args, m) = // Resolve the static overloading early (during the compulsory rewrite phase) so we can inline. match ConstraintSolver.CodegenWitnessExprForTraitConstraint cenv.TcVal g cenv.amap m traitInfoForResolution args with - | OkResult (_, Some expr) -> OptimizeExpr cenv env expr + | OkResult (_, Some expr) -> + let res = OptimizeExpr cenv env expr + // Inlining the solution and solving its nested traits can determine its open type parameters, so finish after. + ConstraintSolver.FinishCodegenTraitSolution cenv.TcVal g cenv.amap m traitInfoForResolution |> RaiseOperationResult + res // Resolution fails when optimizing generic code, ignore the failure | _ -> diff --git a/tests/FSharp.Compiler.ComponentTests/ConstraintSolver/MemberConstraints.fs b/tests/FSharp.Compiler.ComponentTests/ConstraintSolver/MemberConstraints.fs index b653d543556..41b9fcedbad 100644 --- a/tests/FSharp.Compiler.ComponentTests/ConstraintSolver/MemberConstraints.fs +++ b/tests/FSharp.Compiler.ComponentTests/ConstraintSolver/MemberConstraints.fs @@ -393,6 +393,127 @@ let main _ = |> compileExeAndRun |> shouldSucceed + // Inlining the method does not determine 'X either: only its body mentions it. + [] + [] + [] + let ``Trait solved by an inline method with an undetermined type parameter`` (optimize: bool) = + FSharp + """ +type C = + static member inline M<'X>(x: int) : int = + box Unchecked.defaultof<'X> |> ignore + x + 1 + +let inline call (w: ^T) = (^T : (static member M : int -> int) 1) + +[] +let main _ = + if call Unchecked.defaultof <> 2 then failwith "call failed" + 0 + """ + |> withOptimization optimize + |> asExe + |> compileExeAndRun + |> shouldSucceed + + // The undetermined type parameter carries a member constraint obj cannot satisfy. Called directly, M is + // rejected when 'X is defaulted; reached through a trait it compiled into a NotSupportedException throw. + [] + [] + [] + let ``Trait solved by a method whose undetermined type parameter has an unsatisfiable member constraint`` (optimize: bool) = + FSharp + """ +type C = + static member inline M< ^X when ^X : (static member Foo : unit -> int)> (x: int) : int = + x + (^X : (static member Foo : unit -> int) ()) + +let inline call (w: ^T) = (^T : (static member M : int -> int) 1) + +[] +let main _ = + printfn "%d" (call Unchecked.defaultof) + 0 + """ + |> withOptimization optimize + |> asExe + |> compile + |> shouldFail + |> withErrorCode 71 + + // https://github.com/dotnet/fsharp/issues/8690 + [] + [] + [] + let ``Issue 8690 - unsatisfiable trait reached through overload resolution is a compile error`` (optimize: bool) = + FSharp + """ +type T = T with + static member inline Mtd (f, x: 'FT) = ((^FT or ^FU) : (static member Mtd : _*_ -> _) f, x) + static member inline Mtd (_, _:'t when 't: struct) = () + +let inline invoke x : 'FV = + let inline call (mthd : ^M, i1: ^I1, _output: ^R) = ((^M or ^R) : (static member Mtd : _*_ -> _) (), i1) + call (T, x, Unchecked.defaultof<'FV>) + +[] +let main _ = + invoke [|1|] + 0 + """ + |> withOptimization optimize + |> asExe + |> compile + |> shouldFail + |> withErrorCode 71 + + // https://github.com/dotnet/fsharp/issues/7384 + // 'mt is determined only by the member constraints the dead branch contributes, which the optimizer drops. + [] + [] + [] + let ``Issue 7384 - type inference does not depend on the runtime path`` (optimize: bool) = + FSharp + """ +let compiles = false + +type A<'t> = A of 't +type B<'t> = B of 't + +type Bind = Bind with + static member (>>=) (A t, f: 'T -> A<'U> ) = f t + static member (>>=) (B t, f: 'T -> B<'U> ) = f t + static member inline Invoke (source: 'MT) (binder: 'T -> 'MU) : 'MU = + let inline call (_mthd: 'M, input: 'I, _output: 'R, f) = ((^M or ^I or ^R) : (static member (>>=) : _*_ -> _) input, f) + call (Bind, source, Unchecked.defaultof<'MU>, binder) + +type Result = Result with + static member Return (_: A<'a>) = A + static member Return (_: B<'a>) = B + static member inline Invoke (x: 'T) : 'MT = + let inline call (_mthd: ^M, output: ^R) = ((^M or ^R) : (static member Return : _ -> _) output) + call (Result, Unchecked.defaultof<'MT>) x + +type T<'m> () = class end +type U<'m,'t> () = class end + +let inline createT (_: 'mit) = + let _ = if compiles then Bind.Invoke (Result.Invoke Unchecked.defaultof<'t> : 'mt) ((fun _ -> Result.Invoke (U<'mt,'t>()))) else Unchecked.defaultof<'mit> + T() : T<'mt> + +type T<'m> with static member inline Return (_) = fun (_:'t) -> U() |> Result.Invoke |> createT + +let (u: T>) = Result.Invoke () + +[] +let main _ = 0 + """ + |> withOptimization optimize + |> asExe + |> compileExeAndRun + |> shouldSucceed + // Regression for PR #19602 (RFC FS-1043): a non-inline binding with an unsatisfiable operator/SRTP // trait must fail at compile time (FS0041), not compile into a NotSupportedException stub that throws // at runtime (which also leaked at feature-off langversions). The deleted neg116 shape '(1.0 - t) * p' From d48aa12a30773b1e72a7810cb4ae04c152448538 Mon Sep 17 00:00:00 2001 From: Jakub Majocha <1760221+majocha@users.noreply.github.com> Date: Wed, 21 Oct 2026 12:40:30 +0200 Subject: [PATCH 6/8] Share defaulting between type checking and codegen trait solutions FinishCodegenTraitSolution reimplemented the checker's last inference steps. Move the priority loop out of CheckDeclarations.ApplyDefaults into ApplyTyparDefaultsByPriority and split ChooseTyparSolutionAndSolve into an OperationResult core, SolveTyparWithChosenSolution, so both paths use the same code. The checker keeps raising wrappers; the codegen path runs its solver state's postponed checks at once, since no end of file follows to run them. --- src/Compiler/Checking/CheckDeclarations.fs | 8 +-- src/Compiler/Checking/ConstraintSolver.fs | 76 +++++++++++++--------- src/Compiler/Checking/ConstraintSolver.fsi | 3 +- 3 files changed, 47 insertions(+), 40 deletions(-) diff --git a/src/Compiler/Checking/CheckDeclarations.fs b/src/Compiler/Checking/CheckDeclarations.fs index df9037a8d47..02152edb446 100644 --- a/src/Compiler/Checking/CheckDeclarations.fs +++ b/src/Compiler/Checking/CheckDeclarations.fs @@ -6102,13 +6102,7 @@ let ApplyDefaults (cenv: cenv) g denvAtEnd m moduleContents extraAttribs = CanonicalizePartialInferenceProblem cenv.css denvAtEnd m unsolved - // The priority order comes from the order of declaration of the defaults in FSharp.Core. - for priority = 10 downto 0 do - unsolved |> List.iter (fun tp -> - if not tp.IsSolved then - // Apply the first default. If we're defaulting one type variable to another then - // the defaults will be propagated to the new type variable. - ApplyTyparDefaultAtPriority denvAtEnd cenv.css priority tp) + ApplyTyparDefaultsByPriority denvAtEnd cenv.css unsolved |> RaiseOperationResult // OK, now apply defaults for any unsolved TyparStaticReq.HeadType or typars with SRTP (MayResolveMember) constraints // Note: We also check for MayResolveMember constraints because some SRTP typars may not have StaticReq set diff --git a/src/Compiler/Checking/ConstraintSolver.fs b/src/Compiler/Checking/ConstraintSolver.fs index f9ae9184a0b..6333c0eaef8 100644 --- a/src/Compiler/Checking/ConstraintSolver.fs +++ b/src/Compiler/Checking/ConstraintSolver.fs @@ -4406,8 +4406,8 @@ let SolveTypeAsError denv css m ty = let csenv = MakeConstraintSolverEnv ContextInfo.NoContext css m denv SolveTypeEqualsTypeKeepAbbrevs csenv 0 m NoTrace ty ty2 |> ignore -let ApplyTyparDefaultAtPriority denv css priority (tp: Typar) = - tp.Constraints |> List.iter (fun tpc -> +let private ApplyTyparDefaultAtPriority denv css priority (tp: Typar) = + tp.Constraints |> IterateD (fun tpc -> match tpc with | TyparConstraint.DefaultsTo(priority2, ty2, m) when priority2 = priority -> let ty1 = mkTyparTy tp @@ -4419,8 +4419,26 @@ let ApplyTyparDefaultAtPriority denv css priority (tp: Typar) = (fun res -> SolveTypeAsError denv css m ty1 ErrorD(ErrorFromApplyingDefault(css.g, denv, tp, ty2, res, m))) - |> RaiseOperationResult - | _ -> ()) + else + CompleteD + | _ -> CompleteD) + +/// Apply the 'default' constraints of the type variables, highest priority first. A default's priority is its +/// position from the end of its 'when' clause (see TcTyparConstraints), so FSharp.Core's operators try e.g. +/// 'default ^T2: ^T3' before 'default ^T1: int'. Defaulting one type variable to another propagates the defaults +/// to the other. +let ApplyTyparDefaultsByPriority denv css (tps: Typar list) = + [ 10..-1..0 ] |> IterateD (fun priority -> tps |> IterateD (ApplyTyparDefaultAtPriority denv css priority)) + +/// Choose a solution for a type parameter subject to its constraints and apply it by using a constraint. +let SolveTyparWithChosenSolution css denv tp = + let g = css.g + let amap = css.amap + let max, m = ChooseTyparSolutionAndRange g amap tp + let csenv = MakeConstraintSolverEnv ContextInfo.NoContext css m denv + PostponeOnFailedMemberConstraintResolution csenv NoTrace + (fun csenv -> SolveTyparEqualsType csenv 0 m NoTrace (mkTyparTy tp) max) + (fun err -> ErrorD(ErrorFromApplyingDefault(g, denv, tp, max, err, m))) let CreateCodegenState tcVal g amap = ConstraintSolverState.New(g, amap, InfoReader(g, amap), tcVal) @@ -4443,8 +4461,8 @@ let SolveMemberConstraintForCodegen tcVal g amap m (traitInfo: TraitConstraintIn /// does not determine to whatever processes the code generated from the solution: inlining the method and solving /// its nested traits. Once that is done, any still unsolved, e.g. 'X of 'static member M<'X>(x: int) : int', are /// finished the way ApplyDefaults and SolveInternalUnknowns finish a file: their member constraints are solved, -/// then defaults by priority apply, then a solution is chosen. A trait over generic types belongs to an inline -/// template solved again at each call site, so its solution stays open. +/// then ApplyTyparDefaultsByPriority and SolveTyparWithChosenSolution apply. A trait over generic types belongs to +/// an inline template solved again at each call site, so its solution stays open. let FinishCodegenTraitSolution tcVal g amap m (traitInfo: TraitConstraintInfo) = let traitTys = traitInfo.SupportTypes @ traitInfo.CompiledObjectAndArgumentTypes @ Option.toList traitInfo.CompiledReturnType @@ -4452,24 +4470,24 @@ let FinishCodegenTraitSolution tcVal g amap m (traitInfo: TraitConstraintInfo) = match traitInfo.Solution with | Some(FSMethSln(minst = minst)) | Some(ILMethSln(minst = minst)) when (freeInTypes CollectTypars traitTys).FreeTypars.IsEmpty -> + let css = CreateCodegenState tcVal g amap let denv = DisplayEnv.Empty g - let csenv = MakeConstraintSolverEnv ContextInfo.NoContext (CreateCodegenState tcVal g amap) m denv let unsolved () = (freeInTypes CollectTypars minst).FreeTypars |> Zset.elements |> List.filter (fun tp -> tp.Rigidity <> TyparRigidity.Rigid) - let solveAsDefault tp ty = - TryD - (fun () -> SolveTyparEqualsType csenv 0 m NoTrace (mkTyparTy tp) ty) - (fun err -> ErrorD(ErrorFromApplyingDefault(g, denv, tp, ty, err, m))) - trackErrors { + // Unlike CanonicalizePartialInferenceProblem, which walks the constraints recorded in a solver state, + // canonicalize from the type parameters themselves: FreshenMethInfo copied their constraints onto them. do! IgnoreFailedMemberConstraintResolution (fun () -> - let csenv = { csenv with ErrorOnFailedMemberConstraintResolution = true } + let csenv = + { MakeConstraintSolverEnv ContextInfo.NoContext css m denv with + ErrorOnFailedMemberConstraintResolution = true + } RepeatWhileD 0 (fun ndeep -> unsolved () @@ -4481,20 +4499,21 @@ let FinishCodegenTraitSolution tcVal g amap m (traitInfo: TraitConstraintInfo) = | _ -> ResultD false)))) ErrorD - do! - [ 10..-1..0 ] - |> IterateD (fun priority -> - unsolved () - |> IterateD (fun tp -> - tp.Constraints - |> IterateD (function - | TyparConstraint.DefaultsTo(priority2, defaultTy, _) when priority2 = priority && not tp.IsSolved -> - solveAsDefault tp defaultTy - | _ -> CompleteD))) + do! ApplyTyparDefaultsByPriority denv css (unsolved ()) do! unsolved () - |> IterateD (fun tp -> if tp.IsSolved then CompleteD else solveAsDefault tp (ChooseTyparSolution g amap tp)) + |> IterateD (fun tp -> if tp.IsSolved then CompleteD else SolveTyparWithChosenSolution css denv tp) + + // Nothing later resolves what solving postponed, so run those checks now, as the end of checking does. + do! + [ yield! css.GetPostInferenceChecksPreDefaults(); yield! css.GetPostInferenceChecksFinal() ] + |> IterateD (fun check -> + try + check () + CompleteD + with RecoverableException err -> + ErrorD err) } | _ -> CompleteD @@ -4557,14 +4576,7 @@ let CodegenWitnessArgForTraitConstraint tcVal g amap m traitInfo = /// for an interior type variable. This chooses a solution for a type parameter subject /// to its constraints and applies that solution by using a constraint. let ChooseTyparSolutionAndSolve css denv tp = - let g = css.g - let amap = css.amap - let max, m = ChooseTyparSolutionAndRange g amap tp - let csenv = MakeConstraintSolverEnv ContextInfo.NoContext css m denv - PostponeOnFailedMemberConstraintResolution csenv NoTrace - (fun csenv -> SolveTyparEqualsType csenv 0 m NoTrace (mkTyparTy tp) max) - (fun err -> ErrorD(ErrorFromApplyingDefault(g, denv, tp, max, err, m))) - |> RaiseOperationResult + SolveTyparWithChosenSolution css denv tp |> RaiseOperationResult let CheckDeclaredTypars denv css m typars1 typars2 = let csenv = MakeConstraintSolverEnv ContextInfo.NoContext css m denv diff --git a/src/Compiler/Checking/ConstraintSolver.fsi b/src/Compiler/Checking/ConstraintSolver.fsi index 7a87958de8d..e50c98a6dea 100644 --- a/src/Compiler/Checking/ConstraintSolver.fsi +++ b/src/Compiler/Checking/ConstraintSolver.fsi @@ -368,7 +368,8 @@ val AddCxTyparDefaultsTo: DisplayEnv -> ConstraintSolverState -> range -> Contex val SolveTypeAsError: DisplayEnv -> ConstraintSolverState -> range -> TType -> unit -val ApplyTyparDefaultAtPriority: DisplayEnv -> ConstraintSolverState -> priority: int -> Typar -> unit +/// Apply the 'default' constraints of the type variables, highest priority first. +val ApplyTyparDefaultsByPriority: DisplayEnv -> ConstraintSolverState -> Typar list -> OperationResult /// Generate a witness expression if none is otherwise available, e.g. in legacy non-witness-passing code val CodegenWitnessExprForTraitConstraint: From 220e3798b16256ce395e2b161633bb2ae7ea1ab5 Mon Sep 17 00:00:00 2001 From: Jakub Majocha <1760221+majocha@users.noreply.github.com> Date: Wed, 21 Oct 2026 14:07:55 +0200 Subject: [PATCH 7/8] Skip finishing codegen trait solutions with nothing open Check for open type parameters before the ground check and before creating a solver state: nearly every solution has none, and a solver state allocates a new InfoReader with its caches. --- src/Compiler/Checking/ConstraintSolver.fs | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/src/Compiler/Checking/ConstraintSolver.fs b/src/Compiler/Checking/ConstraintSolver.fs index 6333c0eaef8..7d7e68adaf2 100644 --- a/src/Compiler/Checking/ConstraintSolver.fs +++ b/src/Compiler/Checking/ConstraintSolver.fs @@ -4464,19 +4464,23 @@ let SolveMemberConstraintForCodegen tcVal g amap m (traitInfo: TraitConstraintIn /// then ApplyTyparDefaultsByPriority and SolveTyparWithChosenSolution apply. A trait over generic types belongs to /// an inline template solved again at each call site, so its solution stays open. let FinishCodegenTraitSolution tcVal g amap m (traitInfo: TraitConstraintInfo) = - let traitTys = - traitInfo.SupportTypes @ traitInfo.CompiledObjectAndArgumentTypes @ Option.toList traitInfo.CompiledReturnType + let openTypars minst = + (freeInTypes CollectTypars minst).FreeTypars + |> Zset.elements + |> List.filter (fun tp -> tp.Rigidity <> TyparRigidity.Rigid) + let isGround () = + (freeInTypes CollectTypars (traitInfo.SupportTypes @ traitInfo.CompiledObjectAndArgumentTypes @ Option.toList traitInfo.CompiledReturnType)) + .FreeTypars + .IsEmpty + + // Nearly every solution has nothing open, so check that before creating a solver state. match traitInfo.Solution with | Some(FSMethSln(minst = minst)) - | Some(ILMethSln(minst = minst)) when (freeInTypes CollectTypars traitTys).FreeTypars.IsEmpty -> + | Some(ILMethSln(minst = minst)) when not (List.isEmpty (openTypars minst)) && isGround () -> let css = CreateCodegenState tcVal g amap let denv = DisplayEnv.Empty g - - let unsolved () = - (freeInTypes CollectTypars minst).FreeTypars - |> Zset.elements - |> List.filter (fun tp -> tp.Rigidity <> TyparRigidity.Rigid) + let unsolved () = openTypars minst trackErrors { // Unlike CanonicalizePartialInferenceProblem, which walks the constraints recorded in a solver state, From d29902d85ac8d1d4d960d938256bb1e602344162 Mon Sep 17 00:00:00 2001 From: Jakub Majocha <1760221+majocha@users.noreply.github.com> Date: Sat, 24 Oct 2026 21:37:02 +0200 Subject: [PATCH 8/8] Finish codegen trait solutions in generic code too FinishCodegenTraitSolution left the solution of a trait over generic types open, assuming an inline template solves it again at each call site. It does not: once a trait in generic code resolves, the processed solution is final, and an inline template stores and reuses it for every instantiation. So a phantom method type parameter solved in a generic caller, inline or not, still reached IlxGen unsolved and reported FS0073, where the removed fallback had emitted obj (review of #20821). Finish every solution. Type checking does the same for direct calls in generic code: it defaults the undetermined type parameter to obj, and rejects one with a member constraint obj cannot satisfy with FS0071. --- src/Compiler/Checking/ConstraintSolver.fs | 12 ++-- src/Compiler/Checking/ConstraintSolver.fsi | 4 +- .../ConstraintSolver/MemberConstraints.fs | 55 +++++++++++++++++++ 3 files changed, 61 insertions(+), 10 deletions(-) diff --git a/src/Compiler/Checking/ConstraintSolver.fs b/src/Compiler/Checking/ConstraintSolver.fs index 7d7e68adaf2..ad77f5dd84a 100644 --- a/src/Compiler/Checking/ConstraintSolver.fs +++ b/src/Compiler/Checking/ConstraintSolver.fs @@ -4461,23 +4461,19 @@ let SolveMemberConstraintForCodegen tcVal g amap m (traitInfo: TraitConstraintIn /// does not determine to whatever processes the code generated from the solution: inlining the method and solving /// its nested traits. Once that is done, any still unsolved, e.g. 'X of 'static member M<'X>(x: int) : int', are /// finished the way ApplyDefaults and SolveInternalUnknowns finish a file: their member constraints are solved, -/// then ApplyTyparDefaultsByPriority and SolveTyparWithChosenSolution apply. A trait over generic types belongs to -/// an inline template solved again at each call site, so its solution stays open. +/// then ApplyTyparDefaultsByPriority and SolveTyparWithChosenSolution apply. This holds in generic code too: in an +/// inline template the processed solution is stored and reused by every instantiation, so nothing later determines +/// them, just as type checking defaults a direct call's undetermined type parameters there. let FinishCodegenTraitSolution tcVal g amap m (traitInfo: TraitConstraintInfo) = let openTypars minst = (freeInTypes CollectTypars minst).FreeTypars |> Zset.elements |> List.filter (fun tp -> tp.Rigidity <> TyparRigidity.Rigid) - let isGround () = - (freeInTypes CollectTypars (traitInfo.SupportTypes @ traitInfo.CompiledObjectAndArgumentTypes @ Option.toList traitInfo.CompiledReturnType)) - .FreeTypars - .IsEmpty - // Nearly every solution has nothing open, so check that before creating a solver state. match traitInfo.Solution with | Some(FSMethSln(minst = minst)) - | Some(ILMethSln(minst = minst)) when not (List.isEmpty (openTypars minst)) && isGround () -> + | Some(ILMethSln(minst = minst)) when not (List.isEmpty (openTypars minst)) -> let css = CreateCodegenState tcVal g amap let denv = DisplayEnv.Empty g let unsolved () = openTypars minst diff --git a/src/Compiler/Checking/ConstraintSolver.fsi b/src/Compiler/Checking/ConstraintSolver.fsi index e50c98a6dea..71f0c37cfb5 100644 --- a/src/Compiler/Checking/ConstraintSolver.fsi +++ b/src/Compiler/Checking/ConstraintSolver.fsi @@ -375,8 +375,8 @@ val ApplyTyparDefaultsByPriority: DisplayEnv -> ConstraintSolverState -> Typar l val CodegenWitnessExprForTraitConstraint: TcValF -> TcGlobals -> ImportMap -> range -> TraitConstraintInfo -> Expr list -> OperationResult -/// Finish the type parameters a codegen solution of a ground trait left open, once the code generated from the -/// solution has been processed: inlined and its nested traits solved. Callers of CodegenWitnessExprForTraitConstraint +/// Finish the type parameters a codegen trait solution left open, once the code generated from the solution has +/// been processed: inlined and its nested traits solved. Callers of CodegenWitnessExprForTraitConstraint /// call it with the same trait; the witness lambda functions call it themselves. val FinishCodegenTraitSolution: TcValF -> TcGlobals -> ImportMap -> range -> TraitConstraintInfo -> OperationResult diff --git a/tests/FSharp.Compiler.ComponentTests/ConstraintSolver/MemberConstraints.fs b/tests/FSharp.Compiler.ComponentTests/ConstraintSolver/MemberConstraints.fs index 41b9fcedbad..20dcea2ff00 100644 --- a/tests/FSharp.Compiler.ComponentTests/ConstraintSolver/MemberConstraints.fs +++ b/tests/FSharp.Compiler.ComponentTests/ConstraintSolver/MemberConstraints.fs @@ -442,6 +442,61 @@ let main _ = |> shouldFail |> withErrorCode 71 + // Generic callers reuse the solution for every instantiation, so 'X is defaulted there too, as for a direct call. + [] + [] + [] + let ``Trait solved in generic code by a method with an undetermined type parameter`` (optimize: bool) = + FSharp + """ +type C = + static member inline M<'T, 'X>(x: 'T) = + x, typeof<'X> + +let inline call (w: ^W) (x: 'T) = + (^W : (static member M : 'T -> 'T * System.Type) x) + +let generic (x: 'T) = call Unchecked.defaultof x +let inline genericInline (x: 'T) = call Unchecked.defaultof x + +[] +let main _ = + if snd (generic 1) <> typeof || snd (generic "a") <> typeof then failwith "generic failed" + if snd (genericInline 1) <> typeof || snd (genericInline "a") <> typeof then failwith "genericInline failed" + 0 + """ + |> withOptimization optimize + |> asExe + |> compileExeAndRun + |> shouldSucceed + + // Generic code cannot satisfy a member constraint on 'X that involves 'T; called directly, M is rejected the same way. + [] + [] + [] + let ``Trait solved in generic code by a method whose undetermined type parameter has an unsatisfiable member constraint`` (optimize: bool) = + FSharp + """ +type C = + static member inline M<'T, ^X when ^X : (static member Foo : 'T -> int)>(x: 'T) = + (^X : (static member Foo : 'T -> int) x) + +let inline call (w: ^W) (x: 'T) = + (^W : (static member M : 'T -> int) x) + +let generic (x: 'T) = call Unchecked.defaultof x + +[] +let main _ = + printfn "%d" (generic 1) + 0 + """ + |> withOptimization optimize + |> asExe + |> compile + |> shouldFail + |> withErrorCode 71 + // https://github.com/dotnet/fsharp/issues/8690 [] []