From 4b98317eec213d04d95f64098a105e55f6755a77 Mon Sep 17 00:00:00 2001 From: Tomas Grosup Date: Mon, 16 Feb 2026 16:11:46 +0100 Subject: [PATCH 1/8] Fix method signatures and calling conventions Fix 4 codegen bugs in method call emission and parameter metadata: - #13468: outref parameter compiled as byref - #18135: Static abstract interface members with byref params - #18140: callvirt on value types causes ILVerify errors - #18374: RuntimeWrappedException cannot be caught Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../.FSharp.Compiler.Service/10.0.300.md | 3 + src/Compiler/AbstractIL/ilwrite.fs | 16 +- src/Compiler/CodeGen/IlxGen.fs | 61 +++++-- .../CodeGenRegressions_Signatures.fs | 166 ++++++++++++++++++ .../FSharp.Compiler.ComponentTests.fsproj | 1 + 5 files changed, 228 insertions(+), 19 deletions(-) create mode 100644 tests/FSharp.Compiler.ComponentTests/EmittedIL/CodeGenRegressions/CodeGenRegressions_Signatures.fs diff --git a/docs/release-notes/.FSharp.Compiler.Service/10.0.300.md b/docs/release-notes/.FSharp.Compiler.Service/10.0.300.md index ca425fb63c4..5452b8742c5 100644 --- a/docs/release-notes/.FSharp.Compiler.Service/10.0.300.md +++ b/docs/release-notes/.FSharp.Compiler.Service/10.0.300.md @@ -21,6 +21,9 @@ * Fix FS3356 false positive for instance extension members with same name on different types, introduced by [#18821](https://github.com/dotnet/fsharp/pull/18821). ([PR #19260](https://github.com/dotnet/fsharp/pull/19260)) * Fix graph-based type checking incorrectly resolving dependencies when the same module name is defined across multiple files in the same namespace. ([PR #19280](https://github.com/dotnet/fsharp/pull/19280)) * F# Scripts: Fix default reference paths resolving when an SDK directory is specified. ([PR #19270](https://github.com/dotnet/fsharp/pull/19270)) +* Fix outref parameter compiled as byref. (Issue [#13468](https://github.com/dotnet/fsharp/issues/13468), [PR #19340](https://github.com/dotnet/fsharp/pull/19340)) +* Fix static abstract interface members with byref params. (Issue [#18135](https://github.com/dotnet/fsharp/issues/18135), [PR #19340](https://github.com/dotnet/fsharp/pull/19340)) +* Use constrained.callvirt for method calls on value types. (Issue [#18140](https://github.com/dotnet/fsharp/issues/18140), [PR #19340](https://github.com/dotnet/fsharp/pull/19340)) ### Added * FSharpType: add ImportILType ([PR #19300](https://github.com/dotnet/fsharp/pull/19300)) diff --git a/src/Compiler/AbstractIL/ilwrite.fs b/src/Compiler/AbstractIL/ilwrite.fs index 40254a11965..cfa10ad46ac 100644 --- a/src/Compiler/AbstractIL/ilwrite.fs +++ b/src/Compiler/AbstractIL/ilwrite.fs @@ -446,16 +446,24 @@ type MethodDefKey(ilg:ILGlobals, tidx: int, garity: int, nm: string, retTy: ILTy override _.Equals(obj: obj) = match obj with | :? MethodDefKey as y -> - let compareILTypes o1 o2 = + let rec compareILTypes o1 o2 = match o1, o2 with - | ILType.Value v1, ILType.Value v2 -> v1.EqualsWithPrimaryScopeRef(ilg.primaryAssemblyScopeRef, v2 :> obj ) + | ILType.Value v1, ILType.Value v2 -> v1.EqualsWithPrimaryScopeRef(ilg.primaryAssemblyScopeRef, v2 :> obj) + | ILType.Boxed v1, ILType.Boxed v2 -> v1.EqualsWithPrimaryScopeRef(ilg.primaryAssemblyScopeRef, v2 :> obj) + | ILType.Byref t1, ILType.Byref t2 -> compareILTypes t1 t2 + | ILType.Ptr t1, ILType.Ptr t2 -> compareILTypes t1 t2 + | ILType.Array(sh1, t1), ILType.Array(sh2, t2) -> sh1 = sh2 && compareILTypes t1 t2 + | ILType.Modified(req1, tref1, t1), ILType.Modified(req2, tref2, t2) -> + req1 = req2 && tref1.EqualsWithPrimaryScopeRef(ilg.primaryAssemblyScopeRef, tref2 :> obj) && compareILTypes t1 t2 + | ILType.Modified(_, _, t1), t2 -> compareILTypes t1 t2 + | t1, ILType.Modified(_, _, t2) -> compareILTypes t1 t2 | _ -> o1 = o2 tidx = y.TypeIdx && garity = y.GenericArity && nm = y.Name && - // note: these next two use structural equality on AbstractIL ILType values - retTy = y.ReturnType && List.lengthsEqAndForall2 compareILTypes argTys y.ArgTypes && + // note: these next two use scope-aware equality on AbstractIL ILType values + compareILTypes retTy y.ReturnType && List.lengthsEqAndForall2 compareILTypes argTys y.ArgTypes && isStatic = y.IsStatic | _ -> false diff --git a/src/Compiler/CodeGen/IlxGen.fs b/src/Compiler/CodeGen/IlxGen.fs index 1e2f26b011e..d07ca6bf719 100644 --- a/src/Compiler/CodeGen/IlxGen.fs +++ b/src/Compiler/CodeGen/IlxGen.fs @@ -605,6 +605,10 @@ let voidCheck m g permits ty = [] type DuFieldCoordinates = { CaseIdx: int; FieldIdx: int } +/// Flags propagated from interface slot signatures to parameter metadata. +[] +type SlotParamFlags = { IsIn: bool; IsOut: bool } + /// Structure for maintaining field reuse across struct unions type UnionFieldReuseMap = MultiMap @@ -3491,7 +3495,6 @@ and GenLinearExpr cenv cgbuf eenv expr sequel preSteps (contf: FakeUnit -> FakeU if preSteps && GenExprPreSteps cenv cgbuf eenv expr sequel then contf Fake else - // This case implemented here to get a guaranteed tailcall // Make sure we generate the debug point outside the scope of the variable let startMark, endMark as scopeMarks = StartDelayedLocalScope "let" cgbuf @@ -5423,6 +5426,7 @@ and GenILCall (virt, valu, newobj, valUseFlags, isDllImport, ilMethRef: ILMethodRef, enclArgTys, methArgTys, argExprs, returnTys, m) sequel = + let g = cenv.g let hasByrefArg = ilMethRef.ArgTypes |> List.exists IsILTypeByref let isSuperInit = @@ -5445,6 +5449,24 @@ and GenILCall let makesNoCriticalTailcalls = (newobj || not virt) // Don't tailcall for 'newobj', or 'call' to IL code let hasStructObjArg = valu && ilMethRef.CallingConv.IsInstance + let ilEnclArgTys = GenTypeArgs cenv m eenv.tyenv enclArgTys + let ilMethArgTys = GenTypeArgs cenv m eenv.tyenv methArgTys + let ilReturnTys = GenTypes cenv m eenv.tyenv returnTys + let ilMethSpec = mkILMethSpec (ilMethRef, boxity, ilEnclArgTys, ilMethArgTys) + + let useICallVirt = + (virt || useCallVirt cenv boxity ilMethSpec isBaseCall) + && ilMethRef.CallingConv.IsInstance + + let ccallInfo = + ccallInfo + |> Option.orElseWith (fun () -> + match argExprs with + | objArgExpr :: _ when useICallVirt -> + let objArgTy = tyOfExpr g objArgExpr + if isStructTy g objArgTy then Some objArgTy else None + | _ -> None) + let tail = CanTailcall( hasStructObjArg, @@ -5459,15 +5481,6 @@ and GenILCall sequel ) - let ilEnclArgTys = GenTypeArgs cenv m eenv.tyenv enclArgTys - let ilMethArgTys = GenTypeArgs cenv m eenv.tyenv methArgTys - let ilReturnTys = GenTypes cenv m eenv.tyenv returnTys - let ilMethSpec = mkILMethSpec (ilMethRef, boxity, ilEnclArgTys, ilMethArgTys) - - let useICallVirt = - (virt || useCallVirt cenv boxity ilMethSpec isBaseCall) - && ilMethRef.CallingConv.IsInstance - // Load the 'this' pointer to pass to the superclass constructor. This argument is not // in the expression tree since it can't be treated like an ordinary value if isSuperInit then @@ -8975,6 +8988,7 @@ and GenParams (argInfos: ArgReprInfo list) methArgTys (implValsOpt: Val list option) + (slotSigParamFlags: SlotParamFlags list option) = let g = cenv.g let ilWitnessParams = GenWitnessParams cenv eenv m witnessInfos @@ -8993,11 +9007,18 @@ and GenParams | _ -> List.map (fun x -> x, None) ilArgTysAndInfos let ilParams, _ = - (Set.empty, List.zip methArgTys ilArgTysAndInfoAndVals) - ||> List.mapFold (fun takenNames (methodArgTy, ((ilArgTy, topArgInfo), implValOpt)) -> + ((Set.empty, 0), List.zip methArgTys ilArgTysAndInfoAndVals) + ||> List.mapFold (fun (takenNames, paramIdx) (methodArgTy, ((ilArgTy, topArgInfo), implValOpt)) -> let inFlag, outFlag, optionalFlag, defaultParamValue, Marshal, attribs = GenParamAttribs cenv methodArgTy topArgInfo.Attribs + let inFlag, outFlag = + match slotSigParamFlags with + | Some flags when paramIdx < flags.Length -> + let slotFlags = flags[paramIdx] + (inFlag || slotFlags.IsIn, outFlag || slotFlags.IsOut) + | _ -> (inFlag, outFlag) + let idOpt = match topArgInfo.Name with | Some v -> Some v @@ -9036,7 +9057,7 @@ and GenParams MetadataIndex = NoMetadataIdx } - param, takenNames) + param, (takenNames, paramIdx + 1)) ilWitnessParams @ ilParams @@ -9389,8 +9410,18 @@ and GenMethodForBinding let ilTypars = GenGenericParams cenv eenvUnderMethLambdaTypars methLambdaTypars + let slotSigParamFlags = + match v.ImplementedSlotSigs with + | slotsig :: _ -> + let slotParams = slotsig.FormalParams |> List.concat + + slotParams + |> List.map (fun (TSlotParam(_, _, inFlag, outFlag, _, _)) -> { IsIn = inFlag; IsOut = outFlag }) + |> Some + | [] -> None + let ilParams = - GenParams cenv eenvUnderMethTypeTypars m mspec witnessInfos paramInfos argTys (Some nonUnitNonSelfMethodVars) + GenParams cenv eenvUnderMethTypeTypars m mspec witnessInfos paramInfos argTys (Some nonUnitNonSelfMethodVars) slotSigParamFlags let ilReturn = GenReturnInfo cenv eenvUnderMethTypeTypars (Some returnTy) mspec.FormalReturnType retInfo @@ -10720,7 +10751,7 @@ and GenAbstractBinding cenv eenv tref (vref: ValRef) = let ilReturn = GenReturnInfo cenv eenvForMeth returnTy mspec.FormalReturnType retInfo - let ilParams = GenParams cenv eenvForMeth m mspec [] argInfos methArgTys None + let ilParams = GenParams cenv eenvForMeth m mspec [] argInfos methArgTys None None let compileAsInstance = ValRefIsCompiledAsInstanceMember g vref diff --git a/tests/FSharp.Compiler.ComponentTests/EmittedIL/CodeGenRegressions/CodeGenRegressions_Signatures.fs b/tests/FSharp.Compiler.ComponentTests/EmittedIL/CodeGenRegressions/CodeGenRegressions_Signatures.fs new file mode 100644 index 00000000000..d36eb0c4d9c --- /dev/null +++ b/tests/FSharp.Compiler.ComponentTests/EmittedIL/CodeGenRegressions/CodeGenRegressions_Signatures.fs @@ -0,0 +1,166 @@ +// Copyright (c) Microsoft Corporation. All Rights Reserved. See License.txt in the project root for license information. + +namespace EmittedIL + +open Xunit +open FSharp.Test +open FSharp.Test.Compiler +open FSharp.Test.Utilities + +module CodeGenRegressions_Signatures = + + let private getActualIL (result: CompilationResult) = + match result with + | CompilationResult.Success s -> + match s.OutputPath with + | Some p -> + let (_, _, actualIL) = ILChecker.verifyILAndReturnActual [] p [ "// dummy" ] + actualIL + | None -> failwith "No output path" + | _ -> failwith "Compilation failed" + + // https://github.com/dotnet/fsharp/issues/18140 + [] + let ``Issue_18140_CallvirtOnValueType`` () = + let source = """ +module Test + +[] +type MyRange = + val Value: int + new(v) = { Value = v } + +let comparer = + { new System.Collections.Generic.IEqualityComparer with + member _.Equals(x1, x2) = x1.Value = x2.Value + member _.GetHashCode o = o.GetHashCode() + } + +let test() = + let s = MyRange(42) + comparer.GetHashCode(s) +""" + let actualIL = + FSharp source + |> asLibrary + |> compile + |> shouldSucceed + |> getActualIL + + Assert.Contains("constrained.", actualIL) + + // https://github.com/dotnet/fsharp/issues/18140 + [] + let ``Issue_18140_CallvirtOnValueType_Runtime`` () = + let source = """ +[] +type MyRange = + val Value: int + new(v) = { Value = v } + +let comparer = + { new System.Collections.Generic.IEqualityComparer with + member _.Equals(x1, x2) = x1.Value = x2.Value + member _.GetHashCode o = o.GetHashCode() + } + +let s = MyRange(42) +let h = comparer.GetHashCode(s) +let eq = comparer.Equals(s, MyRange(42)) +if not eq then failwith "Expected equality" +printfn "Hash: %d, Equal: %b" h eq +""" + FSharp source + |> asExe + |> compile + |> shouldSucceed + |> run + |> shouldSucceed + |> ignore + + // https://github.com/dotnet/fsharp/issues/18135 + [] + let ``Issue_18135_StaticAbstractByrefParams`` () = + let source = """ +module Test + +#nowarn "3535" + +[] +type I = + static abstract Foo: int inref -> int + +type T = + interface I with + static member Foo i = i + +let f<'T when 'T :> I>() = + let x = 123 + printfn "%d" ('T.Foo &x) + +[] +let main _ = + f() + 0 +""" + FSharp source + |> asExe + |> compile + |> shouldSucceed + |> run + |> shouldSucceed + |> ignore + + // https://github.com/dotnet/fsharp/issues/13468 + [] + let ``Issue_13468_OutrefAsByref_IL`` () = + let csCode = "namespace CSharpLib { public interface IOutTest { void TryGet(string k, out int v); } }" + let csLib = CSharp csCode |> withName "CSharpLib" + let fsCode = "module Test\nopen CSharpLib\ntype MyImpl() =\n interface IOutTest with\n member this.TryGet(k, v) = v <- 42" + let actualIL = + FSharp fsCode + |> withReferences [csLib] + |> asLibrary + |> compile + |> shouldSucceed + |> getActualIL + Assert.Contains("[out]", actualIL) + + // https://github.com/dotnet/fsharp/issues/13468 + [] + let ``Issue_13468_OutrefAsByref_Runtime`` () = + let csCode = """ +namespace CSharpLib { + public interface IOutTest { bool TryGet(string k, out int v); } + public static class OutTestHelper { + public static string Run(IOutTest impl) { + int v; + bool ok = impl.TryGet("key", out v); + return ok ? v.ToString() : "fail"; + } + } +}""" + let csLib = CSharp csCode |> withName "CSharpLib" + let fsCode = """ +module Test +open CSharpLib +type MyImpl() = + interface IOutTest with + member this.TryGet(k, v) = v <- 42; true + +[] +let main _ = + let result = OutTestHelper.Run(MyImpl()) + if result <> "42" then failwithf "Expected 42, got %s" result + printfn "Success: %s" result + 0 +""" + FSharp fsCode + |> withReferences [csLib] + |> asExe + |> compile + |> shouldSucceed + |> run + |> shouldSucceed + |> ignore + diff --git a/tests/FSharp.Compiler.ComponentTests/FSharp.Compiler.ComponentTests.fsproj b/tests/FSharp.Compiler.ComponentTests/FSharp.Compiler.ComponentTests.fsproj index fd02dace6e1..8f43773cd54 100644 --- a/tests/FSharp.Compiler.ComponentTests/FSharp.Compiler.ComponentTests.fsproj +++ b/tests/FSharp.Compiler.ComponentTests/FSharp.Compiler.ComponentTests.fsproj @@ -280,6 +280,7 @@ + From c55e1ccbb0a4e44739b27377f618365b35d8fe9b Mon Sep 17 00:00:00 2001 From: Tomas Grosup Date: Thu, 26 Feb 2026 15:20:00 +0100 Subject: [PATCH 2/8] Fix isSlotSig for static abstract members with byref params Static members implementing static abstract interface members with inref/outref parameters were missing the modreq(InAttribute)/modreq(OutAttribute) on their IL parameter types. This is because GetMethodSpecForMemberVal always used isSlotSig=false for static members, unlike instance members which check IsDispatchSlot and IsOverrideOrExplicitImpl. This caused the method def type signature to not match the override spec, leading to MethodDefNotFound errors (issue #18135). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Compiler/CodeGen/IlxGen.fs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/Compiler/CodeGen/IlxGen.fs b/src/Compiler/CodeGen/IlxGen.fs index d07ca6bf719..bc726144dab 100644 --- a/src/Compiler/CodeGen/IlxGen.fs +++ b/src/Compiler/CodeGen/IlxGen.fs @@ -1495,7 +1495,12 @@ let GetMethodSpecForMemberVal cenv (memberInfo: ValMemberInfo) (vref: ValRef) = mspec, mspecW, ctps, mtps, curriedArgInfos, paramInfos, retInfo, witnessInfos, methodArgTys, returnTy else let methodArgTys, paramInfos = List.unzip flatArgInfos - let ilMethodArgTys = GenParamTypes cenv m tyenvUnderTypars false methodArgTys + + let isSlotSig = + memberInfo.MemberFlags.IsDispatchSlot + || memberInfo.MemberFlags.IsOverrideOrExplicitImpl + + let ilMethodArgTys = GenParamTypes cenv m tyenvUnderTypars isSlotSig methodArgTys let ilMethodInst = GenTypeArgs cenv m tyenvUnderTypars (List.map mkTyparTy mtps) let mspec = From 5146b04f6f27631dc1aea96b032a7a11ebe941cd Mon Sep 17 00:00:00 2001 From: Tomas Grosup Date: Mon, 2 Mar 2026 14:14:59 +0100 Subject: [PATCH 3/8] Fix MethodDefKey equality and restrict IWSAM test to NET_CORE_APP Remove asymmetric Modified type stripping from MethodDefKey.Equals that incorrectly made Modified(_, _, T) == T, causing method deduplication and corrupt IL. Restrict Issue_18135 test to NET_CORE_APP since static abstract interface members require .NET 7+. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Compiler/AbstractIL/ilwrite.fs | 8 ++++---- .../CodeGenRegressions/CodeGenRegressions_Signatures.fs | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Compiler/AbstractIL/ilwrite.fs b/src/Compiler/AbstractIL/ilwrite.fs index cfa10ad46ac..388072f0c16 100644 --- a/src/Compiler/AbstractIL/ilwrite.fs +++ b/src/Compiler/AbstractIL/ilwrite.fs @@ -453,10 +453,10 @@ type MethodDefKey(ilg:ILGlobals, tidx: int, garity: int, nm: string, retTy: ILTy | ILType.Byref t1, ILType.Byref t2 -> compareILTypes t1 t2 | ILType.Ptr t1, ILType.Ptr t2 -> compareILTypes t1 t2 | ILType.Array(sh1, t1), ILType.Array(sh2, t2) -> sh1 = sh2 && compareILTypes t1 t2 - | ILType.Modified(req1, tref1, t1), ILType.Modified(req2, tref2, t2) -> - req1 = req2 && tref1.EqualsWithPrimaryScopeRef(ilg.primaryAssemblyScopeRef, tref2 :> obj) && compareILTypes t1 t2 - | ILType.Modified(_, _, t1), t2 -> compareILTypes t1 t2 - | t1, ILType.Modified(_, _, t2) -> compareILTypes t1 t2 + | ILType.Modified(req1, tref1, t1), ILType.Modified(req2, tref2, t2) -> + req1 = req2 + && tref1.EqualsWithPrimaryScopeRef(ilg.primaryAssemblyScopeRef, tref2 :> obj) + && compareILTypes t1 t2 | _ -> o1 = o2 tidx = y.TypeIdx && diff --git a/tests/FSharp.Compiler.ComponentTests/EmittedIL/CodeGenRegressions/CodeGenRegressions_Signatures.fs b/tests/FSharp.Compiler.ComponentTests/EmittedIL/CodeGenRegressions/CodeGenRegressions_Signatures.fs index d36eb0c4d9c..2e7c0907dea 100644 --- a/tests/FSharp.Compiler.ComponentTests/EmittedIL/CodeGenRegressions/CodeGenRegressions_Signatures.fs +++ b/tests/FSharp.Compiler.ComponentTests/EmittedIL/CodeGenRegressions/CodeGenRegressions_Signatures.fs @@ -79,7 +79,7 @@ printfn "Hash: %d, Equal: %b" h eq |> ignore // https://github.com/dotnet/fsharp/issues/18135 - [] + [] let ``Issue_18135_StaticAbstractByrefParams`` () = let source = """ module Test From 581965f2d1e82a823f4029713e1ae5b5f1875353 Mon Sep 17 00:00:00 2001 From: Tomas Grosup Date: Fri, 6 Mar 2026 19:18:11 +0100 Subject: [PATCH 4/8] Move release notes to 11.0.100 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- docs/release-notes/.FSharp.Compiler.Service/10.0.300.md | 3 --- docs/release-notes/.FSharp.Compiler.Service/11.0.100.md | 3 +++ 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/release-notes/.FSharp.Compiler.Service/10.0.300.md b/docs/release-notes/.FSharp.Compiler.Service/10.0.300.md index aeab9b595d4..057bf1d18a9 100644 --- a/docs/release-notes/.FSharp.Compiler.Service/10.0.300.md +++ b/docs/release-notes/.FSharp.Compiler.Service/10.0.300.md @@ -23,9 +23,6 @@ * Fix FS3356 false positive for instance extension members with same name on different types, introduced by [#18821](https://github.com/dotnet/fsharp/pull/18821). ([PR #19260](https://github.com/dotnet/fsharp/pull/19260)) * Fix graph-based type checking incorrectly resolving dependencies when the same module name is defined across multiple files in the same namespace. ([PR #19280](https://github.com/dotnet/fsharp/pull/19280)) * F# Scripts: Fix default reference paths resolving when an SDK directory is specified. ([PR #19270](https://github.com/dotnet/fsharp/pull/19270)) -* Fix outref parameter compiled as byref. (Issue [#13468](https://github.com/dotnet/fsharp/issues/13468), [PR #19340](https://github.com/dotnet/fsharp/pull/19340)) -* Fix static abstract interface members with byref params. (Issue [#18135](https://github.com/dotnet/fsharp/issues/18135), [PR #19340](https://github.com/dotnet/fsharp/pull/19340)) -* Use constrained.callvirt for method calls on value types. (Issue [#18140](https://github.com/dotnet/fsharp/issues/18140), [PR #19340](https://github.com/dotnet/fsharp/pull/19340)) * Improve static compilation of state machines. ([PR #19297](https://github.com/dotnet/fsharp/pull/19297)) * Fix a bug where `let!` and `use!` were incorrectly allowed outside computation expressions. [PR #19347](https://github.com/dotnet/fsharp/pull/19347) diff --git a/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md b/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md index da89cec27a2..cd84347cf56 100644 --- a/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md +++ b/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md @@ -1,4 +1,7 @@ ### Fixed +* Fix outref parameter compiled as byref. (Issue [#13468](https://github.com/dotnet/fsharp/issues/13468), [PR #19340](https://github.com/dotnet/fsharp/pull/19340)) +* Fix static abstract interface members with byref params. (Issue [#18135](https://github.com/dotnet/fsharp/issues/18135), [PR #19340](https://github.com/dotnet/fsharp/pull/19340)) +* Use constrained.callvirt for method calls on value types. (Issue [#18140](https://github.com/dotnet/fsharp/issues/18140), [PR #19340](https://github.com/dotnet/fsharp/pull/19340)) ### Added From fd8cd078aa5e3d6eb2ce4a8be3a568a83d7aa7e0 Mon Sep 17 00:00:00 2001 From: Tomas Grosup Date: Fri, 6 Mar 2026 21:07:37 +0100 Subject: [PATCH 5/8] Revert ilwrite.fs MethodDefKey.Equals change to isolate vs_release NRE The more aggressive scope-aware type comparison for Byref/Boxed/Modified types may be causing incorrect method deduplication in CompressedMetadata mode, corrupting the VS extension DLL and causing 1820 NRE failures. This reverts just the ilwrite.fs change to test the hypothesis. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Compiler/AbstractIL/ilwrite.fs | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/src/Compiler/AbstractIL/ilwrite.fs b/src/Compiler/AbstractIL/ilwrite.fs index 388072f0c16..40254a11965 100644 --- a/src/Compiler/AbstractIL/ilwrite.fs +++ b/src/Compiler/AbstractIL/ilwrite.fs @@ -446,24 +446,16 @@ type MethodDefKey(ilg:ILGlobals, tidx: int, garity: int, nm: string, retTy: ILTy override _.Equals(obj: obj) = match obj with | :? MethodDefKey as y -> - let rec compareILTypes o1 o2 = + let compareILTypes o1 o2 = match o1, o2 with - | ILType.Value v1, ILType.Value v2 -> v1.EqualsWithPrimaryScopeRef(ilg.primaryAssemblyScopeRef, v2 :> obj) - | ILType.Boxed v1, ILType.Boxed v2 -> v1.EqualsWithPrimaryScopeRef(ilg.primaryAssemblyScopeRef, v2 :> obj) - | ILType.Byref t1, ILType.Byref t2 -> compareILTypes t1 t2 - | ILType.Ptr t1, ILType.Ptr t2 -> compareILTypes t1 t2 - | ILType.Array(sh1, t1), ILType.Array(sh2, t2) -> sh1 = sh2 && compareILTypes t1 t2 - | ILType.Modified(req1, tref1, t1), ILType.Modified(req2, tref2, t2) -> - req1 = req2 - && tref1.EqualsWithPrimaryScopeRef(ilg.primaryAssemblyScopeRef, tref2 :> obj) - && compareILTypes t1 t2 + | ILType.Value v1, ILType.Value v2 -> v1.EqualsWithPrimaryScopeRef(ilg.primaryAssemblyScopeRef, v2 :> obj ) | _ -> o1 = o2 tidx = y.TypeIdx && garity = y.GenericArity && nm = y.Name && - // note: these next two use scope-aware equality on AbstractIL ILType values - compareILTypes retTy y.ReturnType && List.lengthsEqAndForall2 compareILTypes argTys y.ArgTypes && + // note: these next two use structural equality on AbstractIL ILType values + retTy = y.ReturnType && List.lengthsEqAndForall2 compareILTypes argTys y.ArgTypes && isStatic = y.IsStatic | _ -> false From c560f2929b28b9333dac6699a6f3d0676d1c5796 Mon Sep 17 00:00:00 2001 From: Tomas Grosup Date: Fri, 6 Mar 2026 23:55:32 +0100 Subject: [PATCH 6/8] Revert constrained.callvirt change to isolate vs_release NRE MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Keep the outref [out] byref fix and slot sig param flags. Revert the GenILCall ccallInfo extension that added constrained. prefix for all struct callvirt — this is the likely cause of 1820 vs_release NRE failures on net472. Issue #18140 (constrained.callvirt) needs a different approach. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Compiler/AbstractIL/ilwrite.fs | 16 ++++++++++++---- src/Compiler/CodeGen/IlxGen.fs | 28 +++++++++------------------- 2 files changed, 21 insertions(+), 23 deletions(-) diff --git a/src/Compiler/AbstractIL/ilwrite.fs b/src/Compiler/AbstractIL/ilwrite.fs index 40254a11965..388072f0c16 100644 --- a/src/Compiler/AbstractIL/ilwrite.fs +++ b/src/Compiler/AbstractIL/ilwrite.fs @@ -446,16 +446,24 @@ type MethodDefKey(ilg:ILGlobals, tidx: int, garity: int, nm: string, retTy: ILTy override _.Equals(obj: obj) = match obj with | :? MethodDefKey as y -> - let compareILTypes o1 o2 = + let rec compareILTypes o1 o2 = match o1, o2 with - | ILType.Value v1, ILType.Value v2 -> v1.EqualsWithPrimaryScopeRef(ilg.primaryAssemblyScopeRef, v2 :> obj ) + | ILType.Value v1, ILType.Value v2 -> v1.EqualsWithPrimaryScopeRef(ilg.primaryAssemblyScopeRef, v2 :> obj) + | ILType.Boxed v1, ILType.Boxed v2 -> v1.EqualsWithPrimaryScopeRef(ilg.primaryAssemblyScopeRef, v2 :> obj) + | ILType.Byref t1, ILType.Byref t2 -> compareILTypes t1 t2 + | ILType.Ptr t1, ILType.Ptr t2 -> compareILTypes t1 t2 + | ILType.Array(sh1, t1), ILType.Array(sh2, t2) -> sh1 = sh2 && compareILTypes t1 t2 + | ILType.Modified(req1, tref1, t1), ILType.Modified(req2, tref2, t2) -> + req1 = req2 + && tref1.EqualsWithPrimaryScopeRef(ilg.primaryAssemblyScopeRef, tref2 :> obj) + && compareILTypes t1 t2 | _ -> o1 = o2 tidx = y.TypeIdx && garity = y.GenericArity && nm = y.Name && - // note: these next two use structural equality on AbstractIL ILType values - retTy = y.ReturnType && List.lengthsEqAndForall2 compareILTypes argTys y.ArgTypes && + // note: these next two use scope-aware equality on AbstractIL ILType values + compareILTypes retTy y.ReturnType && List.lengthsEqAndForall2 compareILTypes argTys y.ArgTypes && isStatic = y.IsStatic | _ -> false diff --git a/src/Compiler/CodeGen/IlxGen.fs b/src/Compiler/CodeGen/IlxGen.fs index 8fdcf7fa556..24df3fae425 100644 --- a/src/Compiler/CodeGen/IlxGen.fs +++ b/src/Compiler/CodeGen/IlxGen.fs @@ -5448,7 +5448,6 @@ and GenILCall (virt, valu, newobj, valUseFlags, isDllImport, ilMethRef: ILMethodRef, enclArgTys, methArgTys, argExprs, returnTys, m) sequel = - let g = cenv.g let hasByrefArg = ilMethRef.ArgTypes |> List.exists IsILTypeByref let isSuperInit = @@ -5471,24 +5470,6 @@ and GenILCall let makesNoCriticalTailcalls = (newobj || not virt) // Don't tailcall for 'newobj', or 'call' to IL code let hasStructObjArg = valu && ilMethRef.CallingConv.IsInstance - let ilEnclArgTys = GenTypeArgs cenv m eenv.tyenv enclArgTys - let ilMethArgTys = GenTypeArgs cenv m eenv.tyenv methArgTys - let ilReturnTys = GenTypes cenv m eenv.tyenv returnTys - let ilMethSpec = mkILMethSpec (ilMethRef, boxity, ilEnclArgTys, ilMethArgTys) - - let useICallVirt = - (virt || useCallVirt cenv boxity ilMethSpec isBaseCall) - && ilMethRef.CallingConv.IsInstance - - let ccallInfo = - ccallInfo - |> Option.orElseWith (fun () -> - match argExprs with - | objArgExpr :: _ when useICallVirt -> - let objArgTy = tyOfExpr g objArgExpr - if isStructTy g objArgTy then Some objArgTy else None - | _ -> None) - let tail = CanTailcall( hasStructObjArg, @@ -5503,6 +5484,15 @@ and GenILCall sequel ) + let ilEnclArgTys = GenTypeArgs cenv m eenv.tyenv enclArgTys + let ilMethArgTys = GenTypeArgs cenv m eenv.tyenv methArgTys + let ilReturnTys = GenTypes cenv m eenv.tyenv returnTys + let ilMethSpec = mkILMethSpec (ilMethRef, boxity, ilEnclArgTys, ilMethArgTys) + + let useICallVirt = + (virt || useCallVirt cenv boxity ilMethSpec isBaseCall) + && ilMethRef.CallingConv.IsInstance + // Load the 'this' pointer to pass to the superclass constructor. This argument is not // in the expression tree since it can't be treated like an ordinary value if isSuperInit then From ee848033bacd563f643f4b82fb273ff6c824d282 Mon Sep 17 00:00:00 2001 From: Tomas Grosup Date: Sat, 7 Mar 2026 00:04:00 +0100 Subject: [PATCH 7/8] =?UTF-8?q?Drop=20#18140=20from=20release=20notes=20?= =?UTF-8?q?=E2=80=94=20handled=20by=20#19372?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- docs/release-notes/.FSharp.Compiler.Service/11.0.100.md | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md b/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md index cd84347cf56..4207e092109 100644 --- a/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md +++ b/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md @@ -2,6 +2,5 @@ * Fix outref parameter compiled as byref. (Issue [#13468](https://github.com/dotnet/fsharp/issues/13468), [PR #19340](https://github.com/dotnet/fsharp/pull/19340)) * Fix static abstract interface members with byref params. (Issue [#18135](https://github.com/dotnet/fsharp/issues/18135), [PR #19340](https://github.com/dotnet/fsharp/pull/19340)) -* Use constrained.callvirt for method calls on value types. (Issue [#18140](https://github.com/dotnet/fsharp/issues/18140), [PR #19340](https://github.com/dotnet/fsharp/pull/19340)) ### Added From da60f6e8ac1fdb805897409687480b9629d043ad Mon Sep 17 00:00:00 2001 From: Tomas Grosup Date: Sat, 7 Mar 2026 00:06:35 +0100 Subject: [PATCH 8/8] =?UTF-8?q?Remove=20#18140=20tests=20=E2=80=94=20fix?= =?UTF-8?q?=20handled=20by=20#19372?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../CodeGenRegressions_Signatures.fs | 59 ------------------- 1 file changed, 59 deletions(-) diff --git a/tests/FSharp.Compiler.ComponentTests/EmittedIL/CodeGenRegressions/CodeGenRegressions_Signatures.fs b/tests/FSharp.Compiler.ComponentTests/EmittedIL/CodeGenRegressions/CodeGenRegressions_Signatures.fs index 2e7c0907dea..f404e80efc6 100644 --- a/tests/FSharp.Compiler.ComponentTests/EmittedIL/CodeGenRegressions/CodeGenRegressions_Signatures.fs +++ b/tests/FSharp.Compiler.ComponentTests/EmittedIL/CodeGenRegressions/CodeGenRegressions_Signatures.fs @@ -19,65 +19,6 @@ module CodeGenRegressions_Signatures = | None -> failwith "No output path" | _ -> failwith "Compilation failed" - // https://github.com/dotnet/fsharp/issues/18140 - [] - let ``Issue_18140_CallvirtOnValueType`` () = - let source = """ -module Test - -[] -type MyRange = - val Value: int - new(v) = { Value = v } - -let comparer = - { new System.Collections.Generic.IEqualityComparer with - member _.Equals(x1, x2) = x1.Value = x2.Value - member _.GetHashCode o = o.GetHashCode() - } - -let test() = - let s = MyRange(42) - comparer.GetHashCode(s) -""" - let actualIL = - FSharp source - |> asLibrary - |> compile - |> shouldSucceed - |> getActualIL - - Assert.Contains("constrained.", actualIL) - - // https://github.com/dotnet/fsharp/issues/18140 - [] - let ``Issue_18140_CallvirtOnValueType_Runtime`` () = - let source = """ -[] -type MyRange = - val Value: int - new(v) = { Value = v } - -let comparer = - { new System.Collections.Generic.IEqualityComparer with - member _.Equals(x1, x2) = x1.Value = x2.Value - member _.GetHashCode o = o.GetHashCode() - } - -let s = MyRange(42) -let h = comparer.GetHashCode(s) -let eq = comparer.Equals(s, MyRange(42)) -if not eq then failwith "Expected equality" -printfn "Hash: %d, Equal: %b" h eq -""" - FSharp source - |> asExe - |> compile - |> shouldSucceed - |> run - |> shouldSucceed - |> ignore - // https://github.com/dotnet/fsharp/issues/18135 [] let ``Issue_18135_StaticAbstractByrefParams`` () =