From e2ddcabc55aa52989b80db2da208d5d5c2bb5e0a Mon Sep 17 00:00:00 2001 From: Tomas Grosup Date: Mon, 16 Feb 2026 16:15:29 +0100 Subject: [PATCH 1/3] Fix struct/closure byref capture and let-rec initialization order Fix 3 codegen bugs in closure generation and initialization: - #19068: Object expressions in struct types generate invalid IL with byref fields - #17692: Duplicate parameter names in closure constructors - #12384: let-rec mutual recursion initialization order for mixed lambda/value bindings Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../.FSharp.Compiler.Service/10.0.300.md | 3 + src/Compiler/CodeGen/EraseClosures.fs | 21 +- src/Compiler/CodeGen/IlxGen.fs | 57 ++- .../CodeGenRegressions_Closures.fs | 367 ++++++++++++++++++ ...gTest05.fs.RealInternalSignatureOff.il.bsl | 11 - ...ngTest05.fs.RealInternalSignatureOn.il.bsl | 11 - ...gTest06.fs.RealInternalSignatureOff.il.bsl | 11 - ...ngTest06.fs.RealInternalSignatureOn.il.bsl | 11 - .../FSharp.Compiler.ComponentTests.fsproj | 1 + .../Shadowing/ShadowStaticProperty.fsx.il.bsl | 4 +- ...operty.fsx.realInternalSignatureOff.il.bsl | 11 - ...roperty.fsx.realInternalSignatureOn.il.bsl | 11 - ...dowWithLastOpenedTypeExtensions.fsx.il.bsl | 4 +- ...nsions.fsx.realInternalSignatureOff.il.bsl | 11 - ...ensions.fsx.realInternalSignatureOn.il.bsl | 11 - .../ShadowWithTypeExtension.fsx.il.bsl | 2 +- ...ension.fsx.realInternalSignatureOff.il.bsl | 15 +- ...tension.fsx.realInternalSignatureOn.il.bsl | 15 +- 18 files changed, 443 insertions(+), 134 deletions(-) create mode 100644 tests/FSharp.Compiler.ComponentTests/EmittedIL/CodeGenRegressions/CodeGenRegressions_Closures.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..dc91f04d44a 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 object expressions in struct types no longer generate invalid IL with byref fields. (Issue [#19068](https://github.com/dotnet/fsharp/issues/19068), [PR #19339](https://github.com/dotnet/fsharp/pull/19339)) +* Avoid duplicate parameter names in closure constructors. (Issue [#17692](https://github.com/dotnet/fsharp/issues/17692), [PR #19339](https://github.com/dotnet/fsharp/pull/19339)) +* Improve let-rec codegen: reorder bindings to allocate lambda closures before non-lambda values that reference them. ([PR #19339](https://github.com/dotnet/fsharp/pull/19339)) ### Added * FSharpType: add ImportILType ([PR #19300](https://github.com/dotnet/fsharp/pull/19300)) diff --git a/src/Compiler/CodeGen/EraseClosures.fs b/src/Compiler/CodeGen/EraseClosures.fs index 6585fa1d661..0ae5502c2e1 100644 --- a/src/Compiler/CodeGen/EraseClosures.fs +++ b/src/Compiler/CodeGen/EraseClosures.fs @@ -392,6 +392,21 @@ let mkILFreeVarForParam (p: ILParameter) = let mkILLocalForFreeVar (p: IlxClosureFreeVar) = mkILLocal p.fvType None +// Note: This is similar to ChooseFreeVarNames in IlxGen.fs but operates on +// IlxClosureFreeVar[] instead of string lists. Kept separate to avoid cross-file dependency. +let mkUniqueFreeVarName (baseName: string) (existingFields: IlxClosureFreeVar[]) = + let existingNames = existingFields |> Array.map (fun fv -> fv.fvName) |> Set.ofArray + + let rec findUnique n = + let candidate = if n = 0 then baseName else baseName + string n + + if Set.contains candidate existingNames then + findUnique (n + 1) + else + candidate + + findUnique 0 + let mkILCloFldSpecs _cenv flds = flds |> Array.map (fun fv -> (fv.fvName, fv.fvType)) |> Array.toList @@ -490,7 +505,8 @@ let rec convIlxClosureDef cenv encl (td: ILTypeDef) clo = let laterGenericParams = td.GenericParams @ addedGenParams let selfFreeVar = - mkILFreeVar (CompilerGeneratedName("self" + string nowFields.Length), true, nowCloSpec.ILType) + let baseName = CompilerGeneratedName("self" + string nowFields.Length) + mkILFreeVar (mkUniqueFreeVarName baseName nowFields, true, nowCloSpec.ILType) let laterFields = Array.append nowFields [| selfFreeVar |] let laterCloRef = IlxClosureRef(laterTypeRef, laterStruct, laterFields) @@ -612,7 +628,8 @@ let rec convIlxClosureDef cenv encl (td: ILTypeDef) clo = let laterGenericParams = td.GenericParams // Number each argument left-to-right, adding one to account for the "this" pointer let selfFreeVar = - mkILFreeVar (CompilerGeneratedName "self", true, nowCloSpec.ILType) + let baseName = CompilerGeneratedName "self" + mkILFreeVar (mkUniqueFreeVarName baseName nowFields, true, nowCloSpec.ILType) let argToFreeVarMap = (0, selfFreeVar) diff --git a/src/Compiler/CodeGen/IlxGen.fs b/src/Compiler/CodeGen/IlxGen.fs index 1e2f26b011e..6a982de976f 100644 --- a/src/Compiler/CodeGen/IlxGen.fs +++ b/src/Compiler/CodeGen/IlxGen.fs @@ -6450,7 +6450,7 @@ and GenObjectExpr cenv cgbuf eenvouter objExpr (baseType, baseValOpt, basecall, GenWitnessArgsFromWitnessInfos cenv cgbuf eenvouter m cloinfo.cloWitnessInfos for fv in cloinfo.cloFreeVars do - GenGetLocalVal cenv cgbuf eenvouter m fv None + GenGetFreeVarForClosure cenv cgbuf eenvouter m fv CG.EmitInstr cgbuf @@ -6541,7 +6541,7 @@ and GenSequenceExpr if stateVarsSet.Contains fv then GenDefaultValue cenv cgbuf eenv (fv.Type, m) else - GenGetLocalVal cenv cgbuf eenv m fv None + GenGetFreeVarForClosure cenv cgbuf eenv m fv CG.EmitInstr cgbuf @@ -6653,7 +6653,7 @@ and GenSequenceExpr if stateVarsSet.Contains fv then GenDefaultValue cenv cgbuf eenvouter (fv.Type, m) else - GenGetLocalVal cenv cgbuf eenvouter m fv None + GenGetFreeVarForClosure cenv cgbuf eenvouter m fv CG.EmitInstr cgbuf (pop ilCloAllFreeVars.Length) (Push [ ilCloRetTyOuter ]) (I_newobj(ilxCloSpec.Constructor, None)) GenSequel cenv eenvouter.cloc cgbuf sequel @@ -6886,7 +6886,9 @@ and GenClosureAlloc cenv (cgbuf: CodeGenBuffer) eenv (cloinfo, m) = CG.EmitInstr cgbuf (pop 0) (Push [ EraseClosures.mkTyOfLambdas cenv.ilxPubCloEnv cloinfo.ilCloLambdas ]) (mkNormalLdsfld fspec) else GenWitnessArgsFromWitnessInfos cenv cgbuf eenv m cloinfo.cloWitnessInfos - GenGetLocalVals cenv cgbuf eenv m cloinfo.cloFreeVars + + for fv in cloinfo.cloFreeVars do + GenGetFreeVarForClosure cenv cgbuf eenv m fv CG.EmitInstr cgbuf @@ -6901,6 +6903,12 @@ and GenLambda cenv cgbuf eenv isLocalTypeFunc thisVars expr sequel = and GenTypeOfVal cenv eenv (v: Val) = GenType cenv v.Range eenv.tyenv v.Type +and capturedTypeForFreeVar (g: TcGlobals) (fv: Val) = + if isByrefTy g fv.Type then + destByrefTy g fv.Type + else + fv.Type + and GenFreevar cenv m eenvouter tyenvinner (fv: Val) = let g = cenv.g @@ -6915,7 +6923,7 @@ and GenFreevar cenv m eenvouter tyenvinner (fv: Val) = | Method _ | Null -> error (InternalError("GenFreevar: compiler error: unexpected unrealized value", fv.Range)) #endif - | _ -> GenType cenv m tyenvinner fv.Type + | _ -> GenType cenv m tyenvinner (capturedTypeForFreeVar g fv) and GetIlxClosureFreeVars cenv m (thisVars: ValRef list) boxity eenv takenNames expr = let g = cenv.g @@ -7276,7 +7284,9 @@ and GenDelegateExpr cenv cgbuf eenvouter expr (TObjExprMethod(slotsig, _attribs, IlxClosureSpec.Create(IlxClosureRef(ilDelegeeTypeRef, ilCloLambdas, ilCloAllFreeVars), ctxtGenericArgsForDelegee, false) GenWitnessArgsFromWitnessInfos cenv cgbuf eenvouter m cloWitnessInfos - GenGetLocalVals cenv cgbuf eenvouter m cloFreeVars + + for fv in cloFreeVars do + GenGetFreeVarForClosure cenv cgbuf eenvouter m fv CG.EmitInstr cgbuf @@ -8206,6 +8216,17 @@ and GenLetRecFixup cenv cgbuf eenv (ilxCloSpec: IlxClosureSpec, e, ilField: ILFi GenExpr cenv cgbuf eenv e2 Continue CG.EmitInstr cgbuf (pop 2) Push0 (mkNormalStfld (mkILFieldSpec (ilField.FieldRef, ilxCloSpec.ILType))) +and isLambdaBinding (TBind(_, expr, _)) = + match stripDebugPoints expr with + | Expr.Lambda _ + | Expr.TyLambda _ + | Expr.Obj _ -> true + | _ -> false + +and reorderBindingsLambdasFirst binds = + let lambdas, nonLambdas = binds |> List.partition isLambdaBinding + lambdas @ nonLambdas + /// Generate letrec bindings and GenLetRecBindings cenv (cgbuf: CodeGenBuffer) eenv (allBinds: Bindings, m) (dict: Dictionary option) = @@ -8299,8 +8320,11 @@ and GenLetRecBindings cenv (cgbuf: CodeGenBuffer) eenv (allBinds: Bindings, m) ( let recursiveVars = Zset.addList (bindsPossiblyRequiringFixup |> List.map (fun v -> v.Var)) (Zset.empty valOrder) + let reorderedBindsPossiblyRequiringFixup = + reorderBindingsLambdasFirst bindsPossiblyRequiringFixup + let _ = - (recursiveVars, bindsPossiblyRequiringFixup) + (recursiveVars, reorderedBindsPossiblyRequiringFixup) ||> List.fold (fun forwardReferenceSet (bind: Binding) -> // Compute fixups bind.Expr @@ -8344,6 +8368,8 @@ and GenLetRecBindings cenv (cgbuf: CodeGenBuffer) eenv (allBinds: Bindings, m) ( let _ = (recursiveVars, groupBinds) ||> List.fold (fun forwardReferenceSet (binds: Binding list) -> + let binds = reorderBindingsLambdasFirst binds + match dict, cenv.g.realsig, binds with | _, false, _ | None, _, _ @@ -8380,8 +8406,11 @@ and GenLetRecBindings cenv (cgbuf: CodeGenBuffer) eenv (allBinds: Bindings, m) ( and GenLetRec cenv cgbuf eenv (binds, body, m) sequel = let _, endMark as scopeMarks = StartLocalScope "letrec" cgbuf - let eenv = AllocStorageForBinds cenv cgbuf scopeMarks eenv binds - GenLetRecBindings cenv cgbuf eenv (binds, m) None + + let reorderedBinds = reorderBindingsLambdasFirst binds + + let eenv = AllocStorageForBinds cenv cgbuf scopeMarks eenv reorderedBinds + GenLetRecBindings cenv cgbuf eenv (reorderedBinds, m) None GenExpr cenv cgbuf eenv body (EndLocalScope(sequel, endMark)) //------------------------------------------------------------------------- @@ -9850,8 +9879,14 @@ and GenGetStorageAndSequel (cenv: cenv) cgbuf eenv m (ty, ilTy) storage storeSeq CG.EmitInstrs cgbuf (pop 0) (Push [ ilTy ]) [ mkLdarg0; mkNormalLdfld ilField ] CommitGetStorageSequel cenv cgbuf eenv m ty localCloInfo storeSequel -and GenGetLocalVals cenv cgbuf eenvouter m fvs = - List.iter (fun v -> GenGetLocalVal cenv cgbuf eenvouter m v None) fvs +/// Load free variables for closure capture, dereferencing byrefs. +and GenGetFreeVarForClosure cenv cgbuf eenv m (fv: Val) = + let g = cenv.g + GenGetLocalVal cenv cgbuf eenv m fv None + + if isByrefTy g fv.Type then + let ilUnderlyingTy = GenType cenv m eenv.tyenv (capturedTypeForFreeVar g fv) + CG.EmitInstr cgbuf (pop 1) (Push [ ilUnderlyingTy ]) (mkNormalLdobj ilUnderlyingTy) and GenGetLocalVal cenv cgbuf eenv m (vspec: Val) storeSequel = GenGetStorageAndSequel cenv cgbuf eenv m (vspec.Type, GenTypeOfVal cenv eenv vspec) (StorageForVal m vspec eenv) storeSequel diff --git a/tests/FSharp.Compiler.ComponentTests/EmittedIL/CodeGenRegressions/CodeGenRegressions_Closures.fs b/tests/FSharp.Compiler.ComponentTests/EmittedIL/CodeGenRegressions/CodeGenRegressions_Closures.fs new file mode 100644 index 00000000000..f62dd5da846 --- /dev/null +++ b/tests/FSharp.Compiler.ComponentTests/EmittedIL/CodeGenRegressions/CodeGenRegressions_Closures.fs @@ -0,0 +1,367 @@ +// 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_Closures = + + 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/19068 + [] + let ``Issue_19068_StructObjectExprByrefField`` () = + let source = """ +module Test + +type Class(test : obj) = class end + +[] +type Struct(test : obj) = + member _.Test() = { + new Class(test) with + member _.ToString() = "" + } + +let run() = + let s = Struct("hello") + s.Test() |> ignore + printfn "Success" + +run() +""" + FSharp source + |> asExe + |> ignoreWarnings + |> compile + |> shouldSucceed + |> run + |> shouldSucceed + |> ignore + + // https://github.com/dotnet/fsharp/issues/19068 — sequence expression path + [] + let ``Issue_19068_StructSeqExprByrefCapture`` () = + let source = """ +module Test + +[] +type StructSeq(items: obj[]) = + member _.GetItems() = + let arr = items + seq { + for item in arr do + yield item + } + +let s = StructSeq([| box 1; box "hello"; box 3.14 |]) +let result = s.GetItems() |> Seq.toArray +if result.Length <> 3 then failwithf "Expected 3 items, got %d" result.Length +if result.[0] :?> int <> 1 then failwith "First item wrong" +if result.[1] :?> string <> "hello" then failwith "Second item wrong" +printfn "Sequence expression test passed" +""" + FSharp source + |> asExe + |> ignoreWarnings + |> compile + |> shouldSucceed + |> run + |> shouldSucceed + |> ignore + + // https://github.com/dotnet/fsharp/issues/19068 — delegate expression path + [] + let ``Issue_19068_StructDelegateByrefCapture`` () = + let source = """ +module Test + +[] +type StructDelegate(value: obj) = + member _.CreateAction() = + let v = value + System.Action(fun () -> printfn "Value: %A" v) + +let s = StructDelegate(box 42) +let action = s.CreateAction() +action.Invoke() +printfn "Delegate expression test passed" +""" + FSharp source + |> asExe + |> ignoreWarnings + |> compile + |> shouldSucceed + |> run + |> shouldSucceed + |> ignore + + // https://github.com/dotnet/fsharp/issues/19068 — closure/lambda path + [] + let ``Issue_19068_StructClosureByrefCapture`` () = + let source = """ +module Test + +[] +type StructClosure(value: obj) = + member _.GetValue() = + let v = value + let f = fun () -> v + f() + +let s = StructClosure(box "test") +let result = s.GetValue() +if result :?> string <> "test" then failwithf "Expected 'test', got '%A'" result +printfn "Closure capture test passed" +""" + FSharp source + |> asExe + |> ignoreWarnings + |> compile + |> shouldSucceed + |> run + |> shouldSucceed + |> ignore + + // https://github.com/dotnet/fsharp/issues/17692 + [] + let ``Issue_17692_MutualRecursionDuplicateParamName`` () = + let source = """ +module Test + +let rec caller x = callee (x - 1) +and callee y = if y > 0 then caller y else 0 + +let rec f1 a = f2 (a - 1) + f3 (a - 2) +and f2 b = if b > 0 then f1 b else 1 +and f3 c = if c > 0 then f2 c else 2 + +let result1 = caller 5 +let result2 = f1 5 +printfn "Results: %d %d" result1 result2 +""" + // Verify compilation and runtime + FSharp source + |> asExe + |> compile + |> shouldSucceed + |> run + |> shouldSucceed + |> ignore + + // https://github.com/dotnet/fsharp/issues/17692 + // Verify IL closure constructors have unique parameter names + [] + let ``Issue_17692_MutualRecursionNoDuplicateCtorParams`` () = + let source = """ +module Test + +let rec caller x = callee (x - 1) +and callee y = if y > 0 then caller y else 0 +""" + let result = + FSharp source + |> asLibrary + |> withOptimize + |> compile + |> shouldSucceed + + let actualIL = getActualIL result + + // Find all .ctor methods and check for duplicate param names + let ctorPattern = System.Text.RegularExpressions.Regex(@"\.method.*\.ctor\(([^)]*)\)") + let paramPattern = System.Text.RegularExpressions.Regex(@"(\w+[\w@]*)\s+\w+") + for m in ctorPattern.Matches(actualIL) do + let paramStr = m.Groups.[1].Value + let paramNames = + [ for p in paramPattern.Matches(paramStr) -> p.Groups.[1].Value ] + |> List.filter (fun n -> n <> "class" && n <> "int32" && n <> "object" && n <> "string" && n <> "valuetype" && n <> "void" && n <> "bool") + let distinct = paramNames |> List.distinct + if paramNames.Length <> distinct.Length then + failwithf "Duplicate param names in .ctor: %A (from: %s)" paramNames paramStr + + // let-rec lambda reordering: ensures closures are allocated before non-lambda bindings + [] + let ``LetRec_MutRecInitOrder`` () = + let source = """ +module MutRecInitTest + +type Node = { Next: Node; Prev: Node; Value: int } + +let rec zero = { Next = zero; Prev = zero; Value = 0 } + +let rec one = { Next = two; Prev = two; Value = 1 } +and two = { Next = one; Prev = one; Value = 2 } + +[] +let main _ = + let zeroOk = obj.ReferenceEquals(zero.Next, zero) && obj.ReferenceEquals(zero.Prev, zero) + let oneNextOk = obj.ReferenceEquals(one.Next, two) + let onePrevOk = obj.ReferenceEquals(one.Prev, two) + let twoNextOk = obj.ReferenceEquals(two.Next, one) + let twoPrevOk = obj.ReferenceEquals(two.Prev, one) + + if zeroOk && oneNextOk && onePrevOk && twoNextOk && twoPrevOk then + 0 + else + failwith (sprintf "Mutual recursion initialization failed: zero=%b one.Next=%b one.Prev=%b two.Next=%b two.Prev=%b" + zeroOk oneNextOk onePrevOk twoNextOk twoPrevOk) +""" + FSharp source + |> asExe + |> compileAndRun + |> shouldSucceed + |> ignore + + // let-rec lambda reordering: ensures closures are allocated before non-lambda bindings + [] + let ``LetRec_LetRecNonLambdaOrderPreserved`` () = + let source = """ +module Test + +#nowarn "40" +#nowarn "22" + +[] +type Node = { Value: int; GetLabel: unit -> string } + +let mutable log = [] + +let test () = + let rec a = (log <- "a" :: log; { Value = 1; GetLabel = labelA }) + and b = (log <- "b" :: log; { Value = 2; GetLabel = labelB }) + and labelA () = sprintf "A(%d)" a.Value + and labelB () = sprintf "B(%d)" b.Value + + // Non-lambda bindings 'a' and 'b' should keep their relative order + // (both come after lambdas labelA, labelB in the reordered list) + let reversedLog = log |> List.rev + if reversedLog <> ["a"; "b"] then + failwithf "Expected non-lambda order [a; b], got %A" reversedLog + if a.GetLabel() <> "A(1)" then failwithf "a.GetLabel() = %s" (a.GetLabel()) + if b.GetLabel() <> "B(2)" then failwithf "b.GetLabel() = %s" (b.GetLabel()) + printfn "Order preserved correctly" + +[] +let main _ = test(); 0 +""" + FSharp source + |> asExe + |> compile + |> shouldSucceed + |> run + |> shouldSucceed + |> ignore + + // let-rec lambda reordering: ensures closures are allocated before non-lambda bindings + [] + let ``LetRec_LetRecLambdaDependsOnLambda`` () = + let source = """ +module Test + +let test () = + let rec f x = g (x - 1) + and g x = if x <= 0 then 0 else f x + + let result = f 5 + if result <> 0 then failwithf "Expected 0, got %d" result + printfn "Lambda-to-lambda: %d" result + +[] +let main _ = test(); 0 +""" + FSharp source + |> asExe + |> compile + |> shouldSucceed + |> run + |> shouldSucceed + |> ignore + + // let-rec lambda reordering: ensures closures are allocated before non-lambda bindings + [] + let ``LetRec_DeepMixedMutualRecursion`` () = + let source = """ +module Test + +[] +type Tree = { Label: string; Children: unit -> Tree list } + +let test () = + let rec root = { Label = "root"; Children = rootChildren } + and child1 = { Label = "child1"; Children = child1Children } + and child2 = { Label = "child2"; Children = fun () -> [root] } + and rootChildren () = [child1; child2] + and child1Children () = [child2] + + if root.Label <> "root" then failwith "root label" + let kids = root.Children() + if kids.Length <> 2 then failwithf "Expected 2 children, got %d" kids.Length + if kids.[0].Label <> "child1" then failwith "child1 label" + if kids.[1].Label <> "child2" then failwith "child2 label" + + let grandkids = kids.[0].Children() + if grandkids.Length <> 1 then failwithf "Expected 1 grandchild, got %d" grandkids.Length + if grandkids.[0].Label <> "child2" then failwith "grandchild label" + + let backRef = kids.[1].Children() + if not (obj.ReferenceEquals(backRef.[0], root)) then failwith "back reference to root" + + printfn "Deep mixed mutual recursion passed" + +[] +let main _ = test(); 0 +""" + FSharp source + |> asExe + |> compile + |> shouldSucceed + |> run + |> shouldSucceed + |> ignore + + // let-rec lambda reordering: ensures closures are allocated before non-lambda bindings + // Tests letrec lambda reordering: lambdas must be allocated before non-lambda bindings + // that reference them, to avoid null closure references in mutual recursion. + [] + let ``LetRec_LetRecLambdaReordering`` () = + let source = """ +module Test + +[] +type Node = { Value: int; GetLabel: unit -> string } + +let test () = + let rec a = { Value = 1; GetLabel = labelA } + and b = { Value = 2; GetLabel = labelB } + and labelA () = sprintf "A(%d)->B(%d)" a.Value b.Value + and labelB () = sprintf "B(%d)->A(%d)" b.Value a.Value + + if a.GetLabel() <> "A(1)->B(2)" then failwithf "Expected A(1)->B(2), got %s" (a.GetLabel()) + if b.GetLabel() <> "B(2)->A(1)" then failwithf "Expected B(2)->A(1), got %s" (b.GetLabel()) + printfn "SUCCESS" + +[] +let main _ = + test() + 0 +""" + FSharp source + |> asExe + |> compile + |> shouldSucceed + |> run + |> shouldSucceed + |> ignore + diff --git a/tests/FSharp.Compiler.ComponentTests/EmittedIL/SeqExpressionStepping/SeqExpressionSteppingTest05.fs.RealInternalSignatureOff.il.bsl b/tests/FSharp.Compiler.ComponentTests/EmittedIL/SeqExpressionStepping/SeqExpressionSteppingTest05.fs.RealInternalSignatureOff.il.bsl index c9c71515526..2b7c6337a76 100644 --- a/tests/FSharp.Compiler.ComponentTests/EmittedIL/SeqExpressionStepping/SeqExpressionSteppingTest05.fs.RealInternalSignatureOff.il.bsl +++ b/tests/FSharp.Compiler.ComponentTests/EmittedIL/SeqExpressionStepping/SeqExpressionSteppingTest05.fs.RealInternalSignatureOff.il.bsl @@ -16,16 +16,6 @@ .hash algorithm 0x00008004 .ver 0:0:0:0 -} -.mresource public FSharpSignatureCompressedData.assembly -{ - - -} -.mresource public FSharpOptimizationCompressedData.assembly -{ - - } .module assembly.exe @@ -408,4 +398,3 @@ - diff --git a/tests/FSharp.Compiler.ComponentTests/EmittedIL/SeqExpressionStepping/SeqExpressionSteppingTest05.fs.RealInternalSignatureOn.il.bsl b/tests/FSharp.Compiler.ComponentTests/EmittedIL/SeqExpressionStepping/SeqExpressionSteppingTest05.fs.RealInternalSignatureOn.il.bsl index 0bc58b2d8f8..e48d070b6cc 100644 --- a/tests/FSharp.Compiler.ComponentTests/EmittedIL/SeqExpressionStepping/SeqExpressionSteppingTest05.fs.RealInternalSignatureOn.il.bsl +++ b/tests/FSharp.Compiler.ComponentTests/EmittedIL/SeqExpressionStepping/SeqExpressionSteppingTest05.fs.RealInternalSignatureOn.il.bsl @@ -16,16 +16,6 @@ .hash algorithm 0x00008004 .ver 0:0:0:0 -} -.mresource public FSharpSignatureCompressedData.assembly -{ - - -} -.mresource public FSharpOptimizationCompressedData.assembly -{ - - } .module assembly.exe @@ -446,4 +436,3 @@ - diff --git a/tests/FSharp.Compiler.ComponentTests/EmittedIL/SeqExpressionStepping/SeqExpressionSteppingTest06.fs.RealInternalSignatureOff.il.bsl b/tests/FSharp.Compiler.ComponentTests/EmittedIL/SeqExpressionStepping/SeqExpressionSteppingTest06.fs.RealInternalSignatureOff.il.bsl index 3835b9fa0d5..4415c1942eb 100644 --- a/tests/FSharp.Compiler.ComponentTests/EmittedIL/SeqExpressionStepping/SeqExpressionSteppingTest06.fs.RealInternalSignatureOff.il.bsl +++ b/tests/FSharp.Compiler.ComponentTests/EmittedIL/SeqExpressionStepping/SeqExpressionSteppingTest06.fs.RealInternalSignatureOff.il.bsl @@ -16,16 +16,6 @@ .hash algorithm 0x00008004 .ver 0:0:0:0 -} -.mresource public FSharpSignatureCompressedData.assembly -{ - - -} -.mresource public FSharpOptimizationCompressedData.assembly -{ - - } .module assembly.exe @@ -474,4 +464,3 @@ - diff --git a/tests/FSharp.Compiler.ComponentTests/EmittedIL/SeqExpressionStepping/SeqExpressionSteppingTest06.fs.RealInternalSignatureOn.il.bsl b/tests/FSharp.Compiler.ComponentTests/EmittedIL/SeqExpressionStepping/SeqExpressionSteppingTest06.fs.RealInternalSignatureOn.il.bsl index 5170c5d889c..eb7f25e2c32 100644 --- a/tests/FSharp.Compiler.ComponentTests/EmittedIL/SeqExpressionStepping/SeqExpressionSteppingTest06.fs.RealInternalSignatureOn.il.bsl +++ b/tests/FSharp.Compiler.ComponentTests/EmittedIL/SeqExpressionStepping/SeqExpressionSteppingTest06.fs.RealInternalSignatureOn.il.bsl @@ -16,16 +16,6 @@ .hash algorithm 0x00008004 .ver 0:0:0:0 -} -.mresource public FSharpSignatureCompressedData.assembly -{ - - -} -.mresource public FSharpOptimizationCompressedData.assembly -{ - - } .module assembly.exe @@ -509,4 +499,3 @@ - diff --git a/tests/FSharp.Compiler.ComponentTests/FSharp.Compiler.ComponentTests.fsproj b/tests/FSharp.Compiler.ComponentTests/FSharp.Compiler.ComponentTests.fsproj index fd02dace6e1..efaabf722cb 100644 --- a/tests/FSharp.Compiler.ComponentTests/FSharp.Compiler.ComponentTests.fsproj +++ b/tests/FSharp.Compiler.ComponentTests/FSharp.Compiler.ComponentTests.fsproj @@ -280,6 +280,7 @@ + diff --git a/tests/FSharp.Compiler.ComponentTests/TypeChecks/Shadowing/ShadowStaticProperty.fsx.il.bsl b/tests/FSharp.Compiler.ComponentTests/TypeChecks/Shadowing/ShadowStaticProperty.fsx.il.bsl index e1aeb56493e..b006f5563a5 100644 --- a/tests/FSharp.Compiler.ComponentTests/TypeChecks/Shadowing/ShadowStaticProperty.fsx.il.bsl +++ b/tests/FSharp.Compiler.ComponentTests/TypeChecks/Shadowing/ShadowStaticProperty.fsx.il.bsl @@ -115,7 +115,7 @@ extends [runtime]System.Object { .custom instance void [FSharp.Core]Microsoft.FSharp.Core.CompilationMappingAttribute::.ctor(valuetype [FSharp.Core]Microsoft.FSharp.Core.SourceConstructFlags) = ( 01 00 07 00 00 00 00 00 ) - .method public static void Foo.X.Static(int32 v) cil managed + .method public static void Foo$X$Static(int32 v) cil managed { .maxstack 8 @@ -132,7 +132,7 @@ extends [runtime]System.Object { .custom instance void [FSharp.Core]Microsoft.FSharp.Core.CompilationMappingAttribute::.ctor(valuetype [FSharp.Core]Microsoft.FSharp.Core.SourceConstructFlags) = ( 01 00 07 00 00 00 00 00 ) - .method public static void Foo.X.Static(int32 v) cil managed + .method public static void Foo$X$Static(int32 v) cil managed { .maxstack 8 diff --git a/tests/FSharp.Compiler.ComponentTests/TypeChecks/Shadowing/ShadowStaticProperty.fsx.realInternalSignatureOff.il.bsl b/tests/FSharp.Compiler.ComponentTests/TypeChecks/Shadowing/ShadowStaticProperty.fsx.realInternalSignatureOff.il.bsl index ce7460b05d1..bca61d7eefb 100644 --- a/tests/FSharp.Compiler.ComponentTests/TypeChecks/Shadowing/ShadowStaticProperty.fsx.realInternalSignatureOff.il.bsl +++ b/tests/FSharp.Compiler.ComponentTests/TypeChecks/Shadowing/ShadowStaticProperty.fsx.realInternalSignatureOff.il.bsl @@ -12,16 +12,6 @@ int32) = ( 01 00 02 00 00 00 00 00 00 00 00 00 00 00 00 00 ) .hash algorithm 0x00008004 .ver 0:0:0:0 -} -.mresource public FSharpSignatureCompressedData.assembly -{ - - -} -.mresource public FSharpOptimizationCompressedData.assembly -{ - - } .module assembly.dll @@ -186,4 +176,3 @@ - diff --git a/tests/FSharp.Compiler.ComponentTests/TypeChecks/Shadowing/ShadowStaticProperty.fsx.realInternalSignatureOn.il.bsl b/tests/FSharp.Compiler.ComponentTests/TypeChecks/Shadowing/ShadowStaticProperty.fsx.realInternalSignatureOn.il.bsl index c29813f3509..76c9c3cf0b4 100644 --- a/tests/FSharp.Compiler.ComponentTests/TypeChecks/Shadowing/ShadowStaticProperty.fsx.realInternalSignatureOn.il.bsl +++ b/tests/FSharp.Compiler.ComponentTests/TypeChecks/Shadowing/ShadowStaticProperty.fsx.realInternalSignatureOn.il.bsl @@ -12,16 +12,6 @@ int32) = ( 01 00 02 00 00 00 00 00 00 00 00 00 00 00 00 00 ) .hash algorithm 0x00008004 .ver 0:0:0:0 -} -.mresource public FSharpSignatureCompressedData.assembly -{ - - -} -.mresource public FSharpOptimizationCompressedData.assembly -{ - - } .module assembly.dll @@ -213,4 +203,3 @@ - diff --git a/tests/FSharp.Compiler.ComponentTests/TypeChecks/Shadowing/ShadowWithLastOpenedTypeExtensions.fsx.il.bsl b/tests/FSharp.Compiler.ComponentTests/TypeChecks/Shadowing/ShadowWithLastOpenedTypeExtensions.fsx.il.bsl index e96af9ece71..2cf6beb19df 100644 --- a/tests/FSharp.Compiler.ComponentTests/TypeChecks/Shadowing/ShadowWithLastOpenedTypeExtensions.fsx.il.bsl +++ b/tests/FSharp.Compiler.ComponentTests/TypeChecks/Shadowing/ShadowWithLastOpenedTypeExtensions.fsx.il.bsl @@ -367,7 +367,7 @@ extends [runtime]System.Object { .custom instance void [FSharp.Core]Microsoft.FSharp.Core.CompilationMappingAttribute::.ctor(valuetype [FSharp.Core]Microsoft.FSharp.Core.SourceConstructFlags) = ( 01 00 07 00 00 00 00 00 ) - .method public static void Foo.X.Static(int32 v) cil managed + .method public static void Foo$X$Static(int32 v) cil managed { .maxstack 8 @@ -384,7 +384,7 @@ extends [runtime]System.Object { .custom instance void [FSharp.Core]Microsoft.FSharp.Core.CompilationMappingAttribute::.ctor(valuetype [FSharp.Core]Microsoft.FSharp.Core.SourceConstructFlags) = ( 01 00 07 00 00 00 00 00 ) - .method public static void Foo.X.Static(int32 v) cil managed + .method public static void Foo$X$Static(int32 v) cil managed { .maxstack 8 diff --git a/tests/FSharp.Compiler.ComponentTests/TypeChecks/Shadowing/ShadowWithLastOpenedTypeExtensions.fsx.realInternalSignatureOff.il.bsl b/tests/FSharp.Compiler.ComponentTests/TypeChecks/Shadowing/ShadowWithLastOpenedTypeExtensions.fsx.realInternalSignatureOff.il.bsl index e96af9ece71..f8db25b8f62 100644 --- a/tests/FSharp.Compiler.ComponentTests/TypeChecks/Shadowing/ShadowWithLastOpenedTypeExtensions.fsx.realInternalSignatureOff.il.bsl +++ b/tests/FSharp.Compiler.ComponentTests/TypeChecks/Shadowing/ShadowWithLastOpenedTypeExtensions.fsx.realInternalSignatureOff.il.bsl @@ -12,16 +12,6 @@ int32) = ( 01 00 02 00 00 00 00 00 00 00 00 00 00 00 00 00 ) .hash algorithm 0x00008004 .ver 0:0:0:0 -} -.mresource public FSharpSignatureCompressedData.assembly -{ - - -} -.mresource public FSharpOptimizationCompressedData.assembly -{ - - } .module assembly.dll @@ -568,4 +558,3 @@ - diff --git a/tests/FSharp.Compiler.ComponentTests/TypeChecks/Shadowing/ShadowWithLastOpenedTypeExtensions.fsx.realInternalSignatureOn.il.bsl b/tests/FSharp.Compiler.ComponentTests/TypeChecks/Shadowing/ShadowWithLastOpenedTypeExtensions.fsx.realInternalSignatureOn.il.bsl index 50c58d1a2b2..ae8a5534125 100644 --- a/tests/FSharp.Compiler.ComponentTests/TypeChecks/Shadowing/ShadowWithLastOpenedTypeExtensions.fsx.realInternalSignatureOn.il.bsl +++ b/tests/FSharp.Compiler.ComponentTests/TypeChecks/Shadowing/ShadowWithLastOpenedTypeExtensions.fsx.realInternalSignatureOn.il.bsl @@ -12,16 +12,6 @@ int32) = ( 01 00 02 00 00 00 00 00 00 00 00 00 00 00 00 00 ) .hash algorithm 0x00008004 .ver 0:0:0:0 -} -.mresource public FSharpSignatureCompressedData.assembly -{ - - -} -.mresource public FSharpOptimizationCompressedData.assembly -{ - - } .module assembly.dll @@ -595,4 +585,3 @@ - diff --git a/tests/FSharp.Compiler.ComponentTests/TypeChecks/Shadowing/ShadowWithTypeExtension.fsx.il.bsl b/tests/FSharp.Compiler.ComponentTests/TypeChecks/Shadowing/ShadowWithTypeExtension.fsx.il.bsl index 4baf9344ebf..fa7d7d2ec19 100644 --- a/tests/FSharp.Compiler.ComponentTests/TypeChecks/Shadowing/ShadowWithTypeExtension.fsx.il.bsl +++ b/tests/FSharp.Compiler.ComponentTests/TypeChecks/Shadowing/ShadowWithTypeExtension.fsx.il.bsl @@ -96,7 +96,7 @@ { .custom instance void [FSharp.Core]Microsoft.FSharp.Core.CompilationMappingAttribute::.ctor(valuetype [FSharp.Core]Microsoft.FSharp.Core.SourceConstructFlags) = ( 01 00 07 00 00 00 00 00 ) .method public static class assembly/Foo - Foo.X(class assembly/Foo f, + Foo$X(class assembly/Foo f, int32 i) cil managed { .custom instance void [FSharp.Core]Microsoft.FSharp.Core.CompilationArgumentCountsAttribute::.ctor(int32[]) = ( 01 00 02 00 00 00 01 00 00 00 01 00 00 00 00 00 ) diff --git a/tests/FSharp.Compiler.ComponentTests/TypeChecks/Shadowing/ShadowWithTypeExtension.fsx.realInternalSignatureOff.il.bsl b/tests/FSharp.Compiler.ComponentTests/TypeChecks/Shadowing/ShadowWithTypeExtension.fsx.realInternalSignatureOff.il.bsl index 2fa391c3858..51d71030ece 100644 --- a/tests/FSharp.Compiler.ComponentTests/TypeChecks/Shadowing/ShadowWithTypeExtension.fsx.realInternalSignatureOff.il.bsl +++ b/tests/FSharp.Compiler.ComponentTests/TypeChecks/Shadowing/ShadowWithTypeExtension.fsx.realInternalSignatureOff.il.bsl @@ -12,16 +12,6 @@ int32) = ( 01 00 02 00 00 00 00 00 00 00 00 00 00 00 00 00 ) .hash algorithm 0x00008004 .ver 0:0:0:0 -} -.mresource public FSharpSignatureCompressedData.assembly -{ - - -} -.mresource public FSharpOptimizationCompressedData.assembly -{ - - } .module assembly.dll @@ -92,9 +82,7 @@ extends [runtime]System.Object { .custom instance void [FSharp.Core]Microsoft.FSharp.Core.CompilationMappingAttribute::.ctor(valuetype [FSharp.Core]Microsoft.FSharp.Core.SourceConstructFlags) = ( 01 00 07 00 00 00 00 00 ) - .method public static class assembly/Foo - Foo.X(class assembly/Foo f, - int32 i) cil managed + .method public static class assembly/Foo Foo.X(class assembly/Foo f, int32 i) cil managed { .custom instance void [FSharp.Core]Microsoft.FSharp.Core.CompilationArgumentCountsAttribute::.ctor(int32[]) = ( 01 00 02 00 00 00 01 00 00 00 01 00 00 00 00 00 ) @@ -218,4 +206,3 @@ - diff --git a/tests/FSharp.Compiler.ComponentTests/TypeChecks/Shadowing/ShadowWithTypeExtension.fsx.realInternalSignatureOn.il.bsl b/tests/FSharp.Compiler.ComponentTests/TypeChecks/Shadowing/ShadowWithTypeExtension.fsx.realInternalSignatureOn.il.bsl index 531dddf3cfc..69c89214524 100644 --- a/tests/FSharp.Compiler.ComponentTests/TypeChecks/Shadowing/ShadowWithTypeExtension.fsx.realInternalSignatureOn.il.bsl +++ b/tests/FSharp.Compiler.ComponentTests/TypeChecks/Shadowing/ShadowWithTypeExtension.fsx.realInternalSignatureOn.il.bsl @@ -12,16 +12,6 @@ int32) = ( 01 00 02 00 00 00 00 00 00 00 00 00 00 00 00 00 ) .hash algorithm 0x00008004 .ver 0:0:0:0 -} -.mresource public FSharpSignatureCompressedData.assembly -{ - - -} -.mresource public FSharpOptimizationCompressedData.assembly -{ - - } .module assembly.dll @@ -92,9 +82,7 @@ extends [runtime]System.Object { .custom instance void [FSharp.Core]Microsoft.FSharp.Core.CompilationMappingAttribute::.ctor(valuetype [FSharp.Core]Microsoft.FSharp.Core.SourceConstructFlags) = ( 01 00 07 00 00 00 00 00 ) - .method public static class assembly/Foo - Foo.X(class assembly/Foo f, - int32 i) cil managed + .method public static class assembly/Foo Foo.X(class assembly/Foo f, int32 i) cil managed { .custom instance void [FSharp.Core]Microsoft.FSharp.Core.CompilationArgumentCountsAttribute::.ctor(int32[]) = ( 01 00 02 00 00 00 01 00 00 00 01 00 00 00 00 00 ) @@ -237,4 +225,3 @@ - From f29ffbb91bd82b6d1299ba727e6e1bf5f43e5d4e Mon Sep 17 00:00:00 2001 From: Tomas Grosup Date: Thu, 26 Feb 2026 16:18:25 +0100 Subject: [PATCH 2/3] Fix byref capture in GenStructStateMachine and Env stack tracking - GenStructStateMachine: use GenGetFreeVarForClosure (like all other closure paths) to properly dereference byrefs when initializing captured variables in struct state machine closures. - GenGetStorageAndSequel Env case: use ilField.ActualType instead of ilTy for the Push annotation so the stack tracking matches the actual type loaded by ldfld (important when byref is stripped by capturedTypeForFreeVar). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Compiler/CodeGen/IlxGen.fs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Compiler/CodeGen/IlxGen.fs b/src/Compiler/CodeGen/IlxGen.fs index 6a982de976f..31efd326575 100644 --- a/src/Compiler/CodeGen/IlxGen.fs +++ b/src/Compiler/CodeGen/IlxGen.fs @@ -6352,7 +6352,7 @@ and GenStructStateMachine cenv cgbuf eenvouter (res: LoweredStateMachine) sequel else // initialize the captured var CG.EmitInstr cgbuf (pop 0) (Push [ ilMachineAddrTy ]) (I_ldloc(uint16 locIdx2)) - GenGetLocalVal cenv cgbuf eenvouter m fv None + GenGetFreeVarForClosure cenv cgbuf eenvouter m fv CG.EmitInstr cgbuf (pop 2) (Push []) (mkNormalStfld (mkILFieldSpecInTy (ilCloTy, ilv.fvName, ilv.fvType))) // Generate the start expression @@ -9876,7 +9876,7 @@ and GenGetStorageAndSequel (cenv: cenv) cgbuf eenv m (ty, ilTy) storage storeSeq CommitGetStorageSequel cenv cgbuf eenv m ty None storeSequel | Env(_, ilField, localCloInfo) -> - CG.EmitInstrs cgbuf (pop 0) (Push [ ilTy ]) [ mkLdarg0; mkNormalLdfld ilField ] + CG.EmitInstrs cgbuf (pop 0) (Push [ ilField.ActualType ]) [ mkLdarg0; mkNormalLdfld ilField ] CommitGetStorageSequel cenv cgbuf eenv m ty localCloInfo storeSequel /// Load free variables for closure capture, dereferencing byrefs. From ded5a7c8954ce9d2781af26677b925148e46fda6 Mon Sep 17 00:00:00 2001 From: Tomas Grosup Date: Mon, 2 Mar 2026 14:09:15 +0100 Subject: [PATCH 3/3] Restrict Issue_19068 struct delegate byref test to NET_CORE_APP The struct delegate byref capture test fails on net472 Desktop because the byref-in-struct compilation requires .NET Core runtime features. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../EmittedIL/CodeGenRegressions/CodeGenRegressions_Closures.fs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/FSharp.Compiler.ComponentTests/EmittedIL/CodeGenRegressions/CodeGenRegressions_Closures.fs b/tests/FSharp.Compiler.ComponentTests/EmittedIL/CodeGenRegressions/CodeGenRegressions_Closures.fs index f62dd5da846..1f8fb7840fe 100644 --- a/tests/FSharp.Compiler.ComponentTests/EmittedIL/CodeGenRegressions/CodeGenRegressions_Closures.fs +++ b/tests/FSharp.Compiler.ComponentTests/EmittedIL/CodeGenRegressions/CodeGenRegressions_Closures.fs @@ -82,7 +82,7 @@ printfn "Sequence expression test passed" |> ignore // https://github.com/dotnet/fsharp/issues/19068 — delegate expression path - [] + [] let ``Issue_19068_StructDelegateByrefCapture`` () = let source = """ module Test