From 2c0cbae36f01f49c8c1d2e36980135224cf8229c Mon Sep 17 00:00:00 2001 From: Jakub Majocha <1760221+majocha@users.noreply.github.com> Date: Mon, 16 Feb 2026 13:00:34 +0100 Subject: [PATCH 01/16] add test --- .../Language/StateMachineTests.fs | 45 +++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/tests/FSharp.Compiler.ComponentTests/Language/StateMachineTests.fs b/tests/FSharp.Compiler.ComponentTests/Language/StateMachineTests.fs index 86e11cf2029..96828085587 100644 --- a/tests/FSharp.Compiler.ComponentTests/Language/StateMachineTests.fs +++ b/tests/FSharp.Compiler.ComponentTests/Language/StateMachineTests.fs @@ -5,6 +5,7 @@ namespace Language open Xunit open FSharp.Test.Compiler open FSharp.Test +open FSharp.Test.Assert module StateMachineTests = @@ -241,3 +242,47 @@ let test = task { return 42 } IL_0059: ret } """ ] + + [] + let ``Nested __useResumableCode is expanded correctly`` () = + FSharp """ +module TestStateMachine + +open FSharp.Core.CompilerServices +open FSharp.Core.CompilerServices.StateMachineHelpers +open System.Runtime.CompilerServices + +let inline MoveOnce(x: byref<'T> when 'T :> IAsyncStateMachine and 'T :> IResumableStateMachine<'Data>) = + x.MoveNext() + x.Data + +// An inline helper returning ResumableCode must be fully expanded +// before the compiler tries to recognize the enclosing __stateMachine construct. +let inline helper x = + ResumableCode(fun sm -> + if __useResumableCode then + sm.Data <- x + true + else + failwith "unexpected dynamic branch at runtime") + +#nowarn 3513 +let inline repro x = + if __useResumableCode then + __stateMachine + (MoveNextMethodImpl<_>(fun sm -> (helper x).Invoke(&sm) |> ignore)) + (SetStateMachineMethodImpl<_>(fun _ _ -> ())) + (AfterCode<_, _>(fun sm -> MoveOnce(&sm))) + else + failwith "dynamic state machine" + +[] +let main _ = + let result = repro 42 + if result <> 42 then + failwithf "Expected 42 but got %d" result + 0 +""" + |> withOptimize + |> compileExeAndRun + |> shouldSucceed From 2f16b593f5fc4cee02b390fdaa98cb946c76d647 Mon Sep 17 00:00:00 2001 From: Jakub Majocha <1760221+majocha@users.noreply.github.com> Date: Mon, 16 Feb 2026 13:02:01 +0100 Subject: [PATCH 02/16] fix expansion of nested if __useResumableCode --- src/Compiler/Optimize/LowerStateMachines.fs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/Compiler/Optimize/LowerStateMachines.fs b/src/Compiler/Optimize/LowerStateMachines.fs index 90238e0dc3c..36db5081ee6 100644 --- a/src/Compiler/Optimize/LowerStateMachines.fs +++ b/src/Compiler/Optimize/LowerStateMachines.fs @@ -349,7 +349,11 @@ type LowerStateMachine(g: TcGlobals) = // Repeated top-down rewrite let makeRewriteEnv (env: env) = - { PreIntercept = Some (fun cont e -> match TryReduceExpr env e [] id with Some e2 -> Some (cont e2) | None -> None) + { PreIntercept = Some (fun cont e -> + match e with + | IfUseResumableStateMachinesExpr g (thenExpr, _) -> Some (cont thenExpr) + | _ -> + match TryReduceExpr env e [] id with Some e2 -> Some (cont e2) | None -> None) PostTransform = (fun _ -> None) PreInterceptBinding = None RewriteQuotations=true From 904da826a9bc738284fcb13b6c6f28fdf4ea1e97 Mon Sep 17 00:00:00 2001 From: Jakub Majocha <1760221+majocha@users.noreply.github.com> Date: Mon, 16 Feb 2026 16:02:34 +0100 Subject: [PATCH 03/16] wip --- src/Compiler/Optimize/LowerStateMachines.fs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/Compiler/Optimize/LowerStateMachines.fs b/src/Compiler/Optimize/LowerStateMachines.fs index 36db5081ee6..2c4b8d326f7 100644 --- a/src/Compiler/Optimize/LowerStateMachines.fs +++ b/src/Compiler/Optimize/LowerStateMachines.fs @@ -351,6 +351,14 @@ type LowerStateMachine(g: TcGlobals) = let makeRewriteEnv (env: env) = { PreIntercept = Some (fun cont e -> match e with + // Don't recurse into nested state machine expressions - they will be + // processed by their own LowerStateMachineExpr during codegen. + // This prevents modification of the nested machine's internal + // 'if __useResumableCode' patterns which select its dynamic fallback. + | _ when Option.isSome (IsStateMachineExpr g e) -> Some e + // Eliminate 'if __useResumableCode' - nested state machines are already + // guarded above, so any remaining occurrences at this level are from + // beta-reduced inline helpers and should take the static branch. | IfUseResumableStateMachinesExpr g (thenExpr, _) -> Some (cont thenExpr) | _ -> match TryReduceExpr env e [] id with Some e2 -> Some (cont e2) | None -> None) From 52c64f023a3035e4283ace3c9d0cbbee0e4b3e9d Mon Sep 17 00:00:00 2001 From: Jakub Majocha <1760221+majocha@users.noreply.github.com> Date: Mon, 16 Feb 2026 20:44:06 +0100 Subject: [PATCH 04/16] fix static compilation of some nested tasks Improve reduction of resumable code in state machines Enhance application reduction in state machine lowering for F# computation expressions by tracking let-bound resumable code in the environment and resolving references during reduction. This enables correct handling of optimizer-generated continuations and deeper reduction of nested applications. Also, update test comments to reflect resolved state machine compilation issues. --- src/Compiler/Optimize/LowerStateMachines.fs | 19 ++++++++++++++++++- .../NestedTaskFailures.fs | 4 +--- 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/src/Compiler/Optimize/LowerStateMachines.fs b/src/Compiler/Optimize/LowerStateMachines.fs index 2c4b8d326f7..0f51c070e07 100644 --- a/src/Compiler/Optimize/LowerStateMachines.fs +++ b/src/Compiler/Optimize/LowerStateMachines.fs @@ -235,7 +235,16 @@ type LowerStateMachine(g: TcGlobals) = TryReduceApp env expandedExpr laterArgs | Expr.Let (bind, bodyExpr, m, _) -> - match TryReduceApp env bodyExpr args with + // If the binding returns resumable code, add it to the env so that + // references to it in the body can be resolved during reduction. + // This handles patterns like 'let cont = (fun () -> ...; Zero()) in cont()' + // generated by the optimizer for CE if-then branches. + let envR = + if isExpandVar g bind.Var then + { env with ResumableCodeDefns = env.ResumableCodeDefns.Add bind.Var bind.Expr } + else + env + match TryReduceApp envR bodyExpr args with | Some bodyExpr2 -> Some (mkLetBind m bind bodyExpr2) | None -> None @@ -308,6 +317,14 @@ type LowerStateMachine(g: TcGlobals) = | Some innerExpr2 -> Some (Expr.DebugPoint (dp, innerExpr2)) | None -> None + // Resolve variables known to the env, e.g. locally-bound resumable code continuations + | Expr.Val (vref, _, _) when env.ResumableCodeDefns.ContainsVal vref.Deref -> + TryReduceApp env env.ResumableCodeDefns[vref.Deref] args + + // Push through function applications by combining the arg lists + | Expr.App (f, _fty, _tyargs, fArgs, _m) -> + TryReduceApp env f (fArgs @ args) + | _ -> None diff --git a/tests/FSharp.Core.UnitTests/FSharp.Core/Microsoft.FSharp.Control/NestedTaskFailures.fs b/tests/FSharp.Core.UnitTests/FSharp.Core/Microsoft.FSharp.Control/NestedTaskFailures.fs index 7313ad61b87..e051ee0c40e 100644 --- a/tests/FSharp.Core.UnitTests/FSharp.Core/Microsoft.FSharp.Control/NestedTaskFailures.fs +++ b/tests/FSharp.Core.UnitTests/FSharp.Core/Microsoft.FSharp.Control/NestedTaskFailures.fs @@ -1,10 +1,8 @@ namespace FSharp.Core.UnitTests.Control.Tasks -// The tasks below fail state machine compilation. This failure was causing subsequent problems in code generation. +// The tasks below used to fail state machine compilation. This failure was causing subsequent problems in code generation. // See https://github.com/dotnet/fsharp/issues/13404 -#nowarn "3511" // state machine not statically compilable - this is a separate issue, see https://github.com/dotnet/fsharp/issues/13404 - open System open Microsoft.FSharp.Control open Xunit From d63dc853c9d2e98574c800521e981e7c2c1ccd70 Mon Sep 17 00:00:00 2001 From: Jakub Majocha <1760221+majocha@users.noreply.github.com> Date: Mon, 16 Feb 2026 22:13:12 +0100 Subject: [PATCH 05/16] directly compile the test --- .../Language/StateMachineTests.fs | 85 +++++++++---------- 1 file changed, 39 insertions(+), 46 deletions(-) diff --git a/tests/FSharp.Compiler.ComponentTests/Language/StateMachineTests.fs b/tests/FSharp.Compiler.ComponentTests/Language/StateMachineTests.fs index 96828085587..b121b63a501 100644 --- a/tests/FSharp.Compiler.ComponentTests/Language/StateMachineTests.fs +++ b/tests/FSharp.Compiler.ComponentTests/Language/StateMachineTests.fs @@ -3,9 +3,41 @@ namespace Language open Xunit -open FSharp.Test.Compiler -open FSharp.Test open FSharp.Test.Assert +open FSharp.Test.Compiler + + +// Inlined helper containing a "if __useResumableCode ..." construct failed to expand correctly, +// executing the dynmamic branch at runtime even when the state maching was compiled statically. +// see https://github.com/dotnet/fsharp/issues/19296 +module FailingInlinedHelper = + open FSharp.Core.CompilerServices + open FSharp.Core.CompilerServices.StateMachineHelpers + open System.Runtime.CompilerServices + + let inline MoveOnce(x: byref<'T> when 'T :> IAsyncStateMachine and 'T :> IResumableStateMachine<'Data>) = + x.MoveNext() + x.Data + + // An inline helper returning ResumableCode must be fully expanded + // before the compiler tries to recognize the enclosing __stateMachine construct. + let inline helper x = + ResumableCode(fun sm -> + if __useResumableCode then + sm.Data <- x + true + else + failwith "unexpected dynamic branch at runtime") + + #nowarn 3513 + let inline repro x = + if __useResumableCode then + __stateMachine + (MoveNextMethodImpl<_>(fun sm -> (helper x).Invoke(&sm) |> ignore)) + (SetStateMachineMethodImpl<_>(fun _ _ -> ())) + (AfterCode<_, _>(fun sm -> MoveOnce(&sm))) + else + failwith "dynamic state machine" module StateMachineTests = @@ -22,6 +54,11 @@ module StateMachineTests = |> withOptions ["--nowarn:3511"] |> compileExeAndRun + [] + let ``Nested __useResumableCode is expanded correctly`` () = + FailingInlinedHelper.repro 42 + |> shouldEqual 42 + [] // https://github.com/dotnet/fsharp/issues/13067 let ``Local function with a flexible type``() = """ @@ -242,47 +279,3 @@ let test = task { return 42 } IL_0059: ret } """ ] - - [] - let ``Nested __useResumableCode is expanded correctly`` () = - FSharp """ -module TestStateMachine - -open FSharp.Core.CompilerServices -open FSharp.Core.CompilerServices.StateMachineHelpers -open System.Runtime.CompilerServices - -let inline MoveOnce(x: byref<'T> when 'T :> IAsyncStateMachine and 'T :> IResumableStateMachine<'Data>) = - x.MoveNext() - x.Data - -// An inline helper returning ResumableCode must be fully expanded -// before the compiler tries to recognize the enclosing __stateMachine construct. -let inline helper x = - ResumableCode(fun sm -> - if __useResumableCode then - sm.Data <- x - true - else - failwith "unexpected dynamic branch at runtime") - -#nowarn 3513 -let inline repro x = - if __useResumableCode then - __stateMachine - (MoveNextMethodImpl<_>(fun sm -> (helper x).Invoke(&sm) |> ignore)) - (SetStateMachineMethodImpl<_>(fun _ _ -> ())) - (AfterCode<_, _>(fun sm -> MoveOnce(&sm))) - else - failwith "dynamic state machine" - -[] -let main _ = - let result = repro 42 - if result <> 42 then - failwithf "Expected 42 but got %d" result - 0 -""" - |> withOptimize - |> compileExeAndRun - |> shouldSucceed From 574b9d872b3e324b914b2395a2a4cbc11574a627 Mon Sep 17 00:00:00 2001 From: Jakub Majocha <1760221+majocha@users.noreply.github.com> Date: Mon, 16 Feb 2026 22:22:09 +0100 Subject: [PATCH 06/16] fix comments --- .../Language/StateMachineTests.fs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/tests/FSharp.Compiler.ComponentTests/Language/StateMachineTests.fs b/tests/FSharp.Compiler.ComponentTests/Language/StateMachineTests.fs index b121b63a501..1a6ef7da947 100644 --- a/tests/FSharp.Compiler.ComponentTests/Language/StateMachineTests.fs +++ b/tests/FSharp.Compiler.ComponentTests/Language/StateMachineTests.fs @@ -8,7 +8,7 @@ open FSharp.Test.Compiler // Inlined helper containing a "if __useResumableCode ..." construct failed to expand correctly, -// executing the dynmamic branch at runtime even when the state maching was compiled statically. +// executing the dynmamic branch at runtime even when the state machine was compiled statically. // see https://github.com/dotnet/fsharp/issues/19296 module FailingInlinedHelper = open FSharp.Core.CompilerServices @@ -19,8 +19,6 @@ module FailingInlinedHelper = x.MoveNext() x.Data - // An inline helper returning ResumableCode must be fully expanded - // before the compiler tries to recognize the enclosing __stateMachine construct. let inline helper x = ResumableCode(fun sm -> if __useResumableCode then @@ -29,7 +27,7 @@ module FailingInlinedHelper = else failwith "unexpected dynamic branch at runtime") - #nowarn 3513 + #nowarn 3513 // Resumable code invocation. let inline repro x = if __useResumableCode then __stateMachine @@ -38,6 +36,7 @@ module FailingInlinedHelper = (AfterCode<_, _>(fun sm -> MoveOnce(&sm))) else failwith "dynamic state machine" + #warnon 3513 module StateMachineTests = From 250c218e413cf7c4a407707b759a108c27fdefd0 Mon Sep 17 00:00:00 2001 From: Jakub Majocha <1760221+majocha@users.noreply.github.com> Date: Mon, 16 Feb 2026 23:21:14 +0100 Subject: [PATCH 07/16] add repro cases - for loop over tuples --- .../Language/StateMachineTests.fs | 90 +++++++------------ 1 file changed, 30 insertions(+), 60 deletions(-) diff --git a/tests/FSharp.Compiler.ComponentTests/Language/StateMachineTests.fs b/tests/FSharp.Compiler.ComponentTests/Language/StateMachineTests.fs index 1a6ef7da947..07cd86a8545 100644 --- a/tests/FSharp.Compiler.ComponentTests/Language/StateMachineTests.fs +++ b/tests/FSharp.Compiler.ComponentTests/Language/StateMachineTests.fs @@ -218,63 +218,33 @@ module TestStateMachine let test = task { return 42 } """ |> compile - |> verifyIL [ """ -.method public strict virtual instance void MoveNext() cil managed -{ - .override [runtime]System.Runtime.CompilerServices.IAsyncStateMachine::MoveNext - - .maxstack 4 - .locals init (int32 V_0, - class [runtime]System.Exception V_1, - bool V_2, - class [runtime]System.Exception V_3) - IL_0000: ldarg.0 - IL_0001: ldfld int32 TestStateMachine/test@3::ResumptionPoint - IL_0006: stloc.0 - .try - { - IL_0007: ldarg.0 - IL_0008: ldflda valuetype [FSharp.Core]Microsoft.FSharp.Control.TaskStateMachineData`1 TestStateMachine/test@3::Data - IL_000d: ldc.i4.s 42 - IL_000f: stfld !0 valuetype [FSharp.Core]Microsoft.FSharp.Control.TaskStateMachineData`1::Result - IL_0014: ldc.i4.1 - IL_0015: stloc.2 - IL_0016: ldloc.2 - IL_0017: brfalse.s IL_0036 - - IL_0019: ldarg.0 - IL_001a: ldflda valuetype [FSharp.Core]Microsoft.FSharp.Control.TaskStateMachineData`1 TestStateMachine/test@3::Data - IL_001f: ldflda valuetype [runtime]System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1 valuetype [FSharp.Core]Microsoft.FSharp.Control.TaskStateMachineData`1::MethodBuilder - IL_0024: ldarg.0 - IL_0025: ldflda valuetype [FSharp.Core]Microsoft.FSharp.Control.TaskStateMachineData`1 TestStateMachine/test@3::Data - IL_002a: ldfld !0 valuetype [FSharp.Core]Microsoft.FSharp.Control.TaskStateMachineData`1::Result - IL_002f: call instance void valuetype [netstandard]System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1::SetResult(!0) - IL_0034: leave.s IL_0042 - - IL_0036: leave.s IL_0042 - - } - catch [runtime]System.Object - { - IL_0038: castclass [runtime]System.Exception - IL_003d: stloc.3 - IL_003e: ldloc.3 - IL_003f: stloc.1 - IL_0040: leave.s IL_0042 - - } - IL_0042: ldloc.1 - IL_0043: stloc.3 - IL_0044: ldloc.3 - IL_0045: brtrue.s IL_0048 - - IL_0047: ret - - IL_0048: ldarg.0 - IL_0049: ldflda valuetype [FSharp.Core]Microsoft.FSharp.Control.TaskStateMachineData`1 TestStateMachine/test@3::Data - IL_004e: ldflda valuetype [runtime]System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1 valuetype [FSharp.Core]Microsoft.FSharp.Control.TaskStateMachineData`1::MethodBuilder - IL_0053: ldloc.3 - IL_0054: call instance void valuetype [netstandard]System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1::SetException(class [netstandard]System.Exception) - IL_0059: ret -} -""" ] + |> verifyIL [ ".override [runtime]System.Runtime.CompilerServices.IAsyncStateMachine::MoveNext" ] + + // The original repro from https://github.com/dotnet/fsharp/pull/14930 + [] + let ``Task with for loop over tuples compiles statically`` () = + FSharp """ +module TestStateMachine +let what (f: seq) = task { + for name, _whatever in f do + System.Console.Write name +} + """ + |> compile + |> verifyIL [ ".override [runtime]System.Runtime.CompilerServices.IAsyncStateMachine::MoveNext" ] + + // The original repro from https://github.com/dotnet/fsharp/issues/12839#issuecomment-2562121004 + [] + let ``Task with for loop over tuples compiles statically 2`` () = + FSharp """ +module TestStateMachine +let test = task { + for _ in [ "a", "b" ] do + () +} + """ + |> compile + |> verifyIL [ ".override [runtime]System.Runtime.CompilerServices.IAsyncStateMachine::MoveNext" ] + + + From 78d272e4d78c033fc62d5186c4e33e433eb51f7e Mon Sep 17 00:00:00 2001 From: Jakub Majocha <1760221+majocha@users.noreply.github.com> Date: Tue, 17 Feb 2026 09:31:03 +0100 Subject: [PATCH 08/16] include test and debugpoint handling from #14930 --- src/Compiler/Optimize/LowerStateMachines.fs | 5 + .../Language/StateMachineTests.fs | 92 +++++++++++++++++++ 2 files changed, 97 insertions(+) diff --git a/src/Compiler/Optimize/LowerStateMachines.fs b/src/Compiler/Optimize/LowerStateMachines.fs index 0f51c070e07..5d0bcad002f 100644 --- a/src/Compiler/Optimize/LowerStateMachines.fs +++ b/src/Compiler/Optimize/LowerStateMachines.fs @@ -189,6 +189,11 @@ type LowerStateMachine(g: TcGlobals) = if sm_verbose then printfn "eliminating 'if __useResumableCode...'" BindResumableCodeDefinitions env thenExpr + // Look through debug points to find resumable code bindings inside + | Expr.DebugPoint (_, innerExpr) -> + let envR, _ = BindResumableCodeDefinitions env innerExpr + (envR, expr) + | _ -> (env, expr) diff --git a/tests/FSharp.Compiler.ComponentTests/Language/StateMachineTests.fs b/tests/FSharp.Compiler.ComponentTests/Language/StateMachineTests.fs index 07cd86a8545..d5095dc7625 100644 --- a/tests/FSharp.Compiler.ComponentTests/Language/StateMachineTests.fs +++ b/tests/FSharp.Compiler.ComponentTests/Language/StateMachineTests.fs @@ -247,4 +247,96 @@ let test = task { |> verifyIL [ ".override [runtime]System.Runtime.CompilerServices.IAsyncStateMachine::MoveNext" ] + [] // https://github.com/dotnet/fsharp/issues/12839#issuecomment-1292310944 + let ``Tasks with a for loop over tuples are statically compilable``() = + FSharp """ +module TestProject1 + +let ret i = task { return i } + +let one (f: seq) = task { + let mutable sum = 0 + + let! x = ret 1 + sum <- sum + x + + for name, _whatever, i in f do + let! x = ret i + sum <- sum + x + + System.Console.Write name + + let! x = ret i + sum <- sum + x + + let! x = ret 1 + sum <- sum + x + + return sum +} + +let two (f: seq) = task { + let mutable sum = 0 + + let! x = ret 1 + sum <- sum + x + + for name, _whatever, i in f do + let! x = ret i + sum <- sum + x + + System.Console.Write name + + let! x = ret 1 + sum <- sum + x + + return sum +} + +let three (f: seq) = task { + let mutable sum = 0 + + let! x = ret 1 + sum <- sum + x + + for name, _whatever, i in f do + let! x = ret i + sum <- sum + x + + System.Console.Write name + + return sum +} + +let four (f: seq) = task { + let mutable sum = 0 + + let! x = ret 5 + sum <- sum + x + + for name, _i in f do + System.Console.Write name + + let! x = ret 1 + sum <- sum + x + + return sum +} + +if (one [ ("", "", 1); ("", "", 2) ]).Result <> 8 then + failwith "unexpected result one" +if (one []).Result <> 2 then + failwith "unexpected result one" +if (two [ ("", "", 2) ]).Result <> 4 then + failwith "unexpected result two" +if (three [ ("", "", 5) ]).Result <> 6 then + failwith "unexpected result three" +if (four [ ("", 10) ]).Result <> 6 then + failwith "unexpected result four" +""" + |> withOptimize + |> compileExeAndRun + |> shouldSucceed + + From 3491f3a186d2388bcae5f6938cd9f21d4e557ae2 Mon Sep 17 00:00:00 2001 From: Jakub Majocha <1760221+majocha@users.noreply.github.com> Date: Tue, 17 Feb 2026 10:53:47 +0100 Subject: [PATCH 09/16] add release notes --- docs/release-notes/.FSharp.Compiler.Service/10.0.300.md | 1 + 1 file changed, 1 insertion(+) 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 e7d1bd2e1bf..9e6b955c182 100644 --- a/docs/release-notes/.FSharp.Compiler.Service/10.0.300.md +++ b/docs/release-notes/.FSharp.Compiler.Service/10.0.300.md @@ -14,6 +14,7 @@ * Fix FS0229 B-stream misalignment when reading metadata from assemblies compiled with LangVersion < 9.0, introduced by [#17706](https://github.com/dotnet/fsharp/pull/17706). ([PR #19260](https://github.com/dotnet/fsharp/pull/19260)) * 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)) * F# Scripts: Fix default reference paths resolving when an SDK directory is specified. ([PR #19270](https://github.com/dotnet/fsharp/pull/19270)) +* Improve static compilation of state machines. ([PR #19297](https://github.com/dotnet/fsharp/pull/19297)) ### Added From 273e3cf3d2c30308b3b91660b9fe49e98ea13e67 Mon Sep 17 00:00:00 2001 From: Jakub Majocha <1760221+majocha@users.noreply.github.com> Date: Tue, 17 Feb 2026 20:25:53 +0100 Subject: [PATCH 10/16] add more repros to tests --- .../Language/StateMachineTests.fs | 87 +++++++++++++++++++ 1 file changed, 87 insertions(+) diff --git a/tests/FSharp.Compiler.ComponentTests/Language/StateMachineTests.fs b/tests/FSharp.Compiler.ComponentTests/Language/StateMachineTests.fs index d5095dc7625..f7d2ebb906c 100644 --- a/tests/FSharp.Compiler.ComponentTests/Language/StateMachineTests.fs +++ b/tests/FSharp.Compiler.ComponentTests/Language/StateMachineTests.fs @@ -246,6 +246,93 @@ let test = task { |> compile |> verifyIL [ ".override [runtime]System.Runtime.CompilerServices.IAsyncStateMachine::MoveNext" ] + // see https://github.com/dotnet/fsharp/pull/14930#issuecomment-1528981395 + [] + let ``Task with some anonymous records`` () = + FSharp """ +module TestStateMachine +let bad () = task { + let res = {| ResultSet2 = [| {| im = Some 1; lc = 3 |} |] |} + + match [| |] with + | [| |] -> + let c = res.ResultSet2 |> Array.map (fun x -> {| Name = x.lc |}) + let c = res.ResultSet2 |> Array.map (fun x -> {| Name = x.lc |}) + let c = res.ResultSet2 |> Array.map (fun x -> {| Name = x.lc |}) + return Some c + | _ -> + return None +} +""" + |> compile + |> verifyIL [ ".override [runtime]System.Runtime.CompilerServices.IAsyncStateMachine::MoveNext" ] + + + + // repro of https://github.com/dotnet/fsharp/issues/12839 + [] + let ``Big record`` () = + FSharp """ +module TestStateMachine +type Foo = { X: int option } + +type BigRecord = + { + a1: string + a2: string + a3: string + a4: string + a5: string + a6: string + a7: string + a8: string + a9: string + a10: string + a11: string + a12: string + a13: string + a14: string + a15: string + a16: string + a17: string + a18: string + a19: string + a20: string + a21: string + a22: string + a23: string + a24: string + a25: string + a26: string + a27: string + a28: string + a29: string + a30: string + a31: string + a32: string + a33: string + a34: string + a35: string + a36: string // no warning if at least one field removed + + a37Optional: string option + } + +let testStateMachine (bigRecord: BigRecord) = + task { + match Some 5 with // no warn if this match removed and only inner one kept + | Some _ -> + match Unchecked.defaultof.X with // no warning if replaced with `match Some 5 with` + | Some _ -> + let d = { bigRecord with a37Optional = None } // no warning if d renamed as _ or ignore function used + () + | None -> () + | _ -> () + } +""" + |> compile + |> verifyIL [ ".override [runtime]System.Runtime.CompilerServices.IAsyncStateMachine::MoveNext" ] + [] // https://github.com/dotnet/fsharp/issues/12839#issuecomment-1292310944 let ``Tasks with a for loop over tuples are statically compilable``() = From 98e472709439f7bc23d100ce46d263f9fd34572b Mon Sep 17 00:00:00 2001 From: Jakub Majocha <1760221+majocha@users.noreply.github.com> Date: Thu, 19 Feb 2026 09:39:49 +0100 Subject: [PATCH 11/16] remove catch all 3511 nowarns --- src/FSharp.Core/FSharp.Core.fsproj | 2 -- .../FSharp.Core/Microsoft.FSharp.Control/TasksDynamic.fs | 5 ++++- vsintegration/src/FSharp.Editor/CodeFixes/CodeFixHelpers.fs | 1 - .../src/FSharp.Editor/CodeFixes/ImplementInterface.fs | 4 ++-- .../tests/FSharp.Editor.Tests/FSharp.Editor.Tests.fsproj | 1 - 5 files changed, 6 insertions(+), 7 deletions(-) diff --git a/src/FSharp.Core/FSharp.Core.fsproj b/src/FSharp.Core/FSharp.Core.fsproj index 688ba098645..cad8ee1c930 100644 --- a/src/FSharp.Core/FSharp.Core.fsproj +++ b/src/FSharp.Core/FSharp.Core.fsproj @@ -18,8 +18,6 @@ $(OtherFlags) --warnon:3520 $(OtherFlags) --nowarn:57 - - $(OtherFlags) --nowarn:3511 $(OtherFlags) --nowarn:3513 $(OtherFlags) --compiling-fslib --compiling-fslib-40 --maxerrors:100 --extraoptimizationloops:1 diff --git a/tests/FSharp.Core.UnitTests/FSharp.Core/Microsoft.FSharp.Control/TasksDynamic.fs b/tests/FSharp.Core.UnitTests/FSharp.Core/Microsoft.FSharp.Control/TasksDynamic.fs index 430b1b526e4..7f9eaef3890 100644 --- a/tests/FSharp.Core.UnitTests/FSharp.Core/Microsoft.FSharp.Control/TasksDynamic.fs +++ b/tests/FSharp.Core.UnitTests/FSharp.Core/Microsoft.FSharp.Control/TasksDynamic.fs @@ -17,7 +17,6 @@ namespace FSharp.Core.UnitTests.Control.TasksDynamic #nowarn "1204" // construct only for use in compiled code -#nowarn "3511" // state machine not statically compilable - the one in 'Run' open System open System.Collections open System.Collections.Generic @@ -33,7 +32,9 @@ open System.Runtime.CompilerServices type TaskBuilderDynamic() = [] + #nowarn 3511 member _.Run(code) = task.Run(code) // warning 3511 is generated here: state machine not compilable + #warnon 3511 member inline _.Delay f = task.Delay(f) [] @@ -55,7 +56,9 @@ type TaskBuilderDynamic() = type BackgroundTaskBuilderDynamic() = [] + #nowarn 3511 member _.Run(code) = backgroundTask.Run(code) // warning 3511 is generated here: state machine not compilable + #warnon 3511 member inline _.Delay f = backgroundTask.Delay(f) [] diff --git a/vsintegration/src/FSharp.Editor/CodeFixes/CodeFixHelpers.fs b/vsintegration/src/FSharp.Editor/CodeFixes/CodeFixHelpers.fs index e703fb648ac..ce646f98f25 100644 --- a/vsintegration/src/FSharp.Editor/CodeFixes/CodeFixHelpers.fs +++ b/vsintegration/src/FSharp.Editor/CodeFixes/CodeFixHelpers.fs @@ -139,7 +139,6 @@ module internal CodeFixExtensions = // This cannot be an extension on the code fix context // because the underlying GetFixAllProvider method doesn't take the context in. -#nowarn "3511" // state machine not statically compilable [] module IFSharpCodeFixProviderExtensions = diff --git a/vsintegration/src/FSharp.Editor/CodeFixes/ImplementInterface.fs b/vsintegration/src/FSharp.Editor/CodeFixes/ImplementInterface.fs index 1805428b8d7..52e04a0ce06 100644 --- a/vsintegration/src/FSharp.Editor/CodeFixes/ImplementInterface.fs +++ b/vsintegration/src/FSharp.Editor/CodeFixes/ImplementInterface.fs @@ -30,9 +30,7 @@ type internal InterfaceState = Tokens: Tokenizer.SavedTokenInfo[] } -// state machine not statically compilable // TODO: rewrite token arithmetics properly here -#nowarn "3511" [] type internal ImplementInterfaceCodeFixProvider [] () = @@ -173,6 +171,7 @@ type internal ImplementInterfaceCodeFixProvider [] () = interface IFSharpMultiCodeFixProvider with member _.GetCodeFixesAsync context = + #nowarn 3511 // let rec in state machine cancellableTask { let! cancellationToken = CancellableTask.getCancellationToken () @@ -289,3 +288,4 @@ type internal ImplementInterfaceCodeFixProvider [] () = ) | _ -> return Seq.empty } + #warnon 3511 diff --git a/vsintegration/tests/FSharp.Editor.Tests/FSharp.Editor.Tests.fsproj b/vsintegration/tests/FSharp.Editor.Tests/FSharp.Editor.Tests.fsproj index 1625be60b9a..826cc53ac0d 100644 --- a/vsintegration/tests/FSharp.Editor.Tests/FSharp.Editor.Tests.fsproj +++ b/vsintegration/tests/FSharp.Editor.Tests/FSharp.Editor.Tests.fsproj @@ -6,7 +6,6 @@ false false true - $(NoWarn);FS3511 true From 7a0418aab77b90937b22a0f0a50a671c41192e0f Mon Sep 17 00:00:00 2001 From: Jakub Majocha <1760221+majocha@users.noreply.github.com> Date: Thu, 19 Feb 2026 09:57:45 +0100 Subject: [PATCH 12/16] fix handling outer expandVars ("The resumable code value(s) 'code' does not have a definition.") consider a Run(code: ResumableCode<...>) method. In some cases code can get optimized away resulting in warning 3511 "The resumable code value(s) 'code' does not have a definition." --- src/Compiler/CodeGen/IlxGen.fs | 20 +++++++++++++++++++- src/Compiler/Optimize/LowerStateMachines.fs | 14 ++++++++------ src/Compiler/Optimize/LowerStateMachines.fsi | 4 +++- 3 files changed, 30 insertions(+), 8 deletions(-) diff --git a/src/Compiler/CodeGen/IlxGen.fs b/src/Compiler/CodeGen/IlxGen.fs index 1e2f26b011e..290bba23e4d 100644 --- a/src/Compiler/CodeGen/IlxGen.fs +++ b/src/Compiler/CodeGen/IlxGen.fs @@ -1261,6 +1261,11 @@ and IlxGenEnv = intraAssemblyInfo: IlxGenIntraAssemblyInfo realsig: bool + + /// Expression definitions of variables returning resumable code from outer scopes. + /// Used by state machine lowering to resolve otherwise-free expand variables + /// when the state machine is inside a lambda whose outer let-binding provides the definition. + resumableCodeDefinitions: ValMap } override _.ToString() = "" @@ -3004,7 +3009,9 @@ and GenExprPreSteps (cenv: cenv) (cgbuf: CodeGenBuffer) eenv expr sequel = true | None -> - match LowerStateMachineExpr cenv.g expr with + let smResult = LowerStateMachineExpr cenv.g eenv.resumableCodeDefinitions expr + + match smResult with | LoweredStateMachineResult.Lowered res -> let eenv = RemoveTemplateReplacement eenv checkLanguageFeatureError cenv.g.langVersion LanguageFeature.ResumableStateMachines expr.Range @@ -3499,6 +3506,16 @@ and GenLinearExpr cenv cgbuf eenv expr sequel preSteps (contf: FakeUnit -> FakeU GenDebugPointForBind cenv cgbuf bind GenBindingAfterDebugPoint cenv cgbuf eenv bind false (Some startMark) + // Track expand-var (resumable code) definitions so state machine lowering + // inside nested lambdas can resolve otherwise-free expand variables. + let eenv = + if isReturnsResumableCodeTy cenv.g bind.Var.TauType then + { eenv with + resumableCodeDefinitions = eenv.resumableCodeDefinitions.Add bind.Var bind.Expr + } + else + eenv + // Generate the body GenLinearExpr cenv cgbuf eenv body (EndLocalScope(sequel, endMark)) true contf @@ -12048,6 +12065,7 @@ let GetEmptyIlxGenEnv (g: TcGlobals) ccu = intraAssemblyInfo = IlxGenIntraAssemblyInfo.Create() realsig = g.realsig initClassFieldSpec = None + resumableCodeDefinitions = ValMap<_>.Empty } type IlxGenResults = diff --git a/src/Compiler/Optimize/LowerStateMachines.fs b/src/Compiler/Optimize/LowerStateMachines.fs index 5d0bcad002f..2c5dea5ff07 100644 --- a/src/Compiler/Optimize/LowerStateMachines.fs +++ b/src/Compiler/Optimize/LowerStateMachines.fs @@ -167,7 +167,7 @@ type LoweredStateMachineResult = | NotAStateMachine /// Used to scope the action of lowering a state machine expression -type LowerStateMachine(g: TcGlobals) = +type LowerStateMachine(g: TcGlobals, outerResumableCodeDefns: ValMap) = let mutable pcCount = 0 let genPC() = @@ -409,7 +409,9 @@ type LowerStateMachine(g: TcGlobals) = [] let (|ExpandedStateMachineInContext|_|) inputExpr = // All expanded resumable code state machines e.g. 'task { .. }' begin with a bind of @builder or 'defn' - let env, expr = BindResumableCodeDefinitions env.Empty inputExpr + // Seed the env with any expand-var definitions from outer scopes (e.g. across lambda boundaries) + let initialEnv = { env.Empty with ResumableCodeDefns = outerResumableCodeDefns } + let env, expr = BindResumableCodeDefinitions initialEnv inputExpr match expr with | StructStateMachineExpr g (dataTy, @@ -892,8 +894,8 @@ type LowerStateMachine(g: TcGlobals) = let env, codeExprR = RepeatBindAndApplyOuterDefinitions env codeExpr let frees = (freeInExpr CollectLocals overallExpr).FreeLocals - if frees |> Zset.exists (isExpandVar g) then - let nonfree = frees |> Zset.elements |> List.filter (isExpandVar g) |> List.map (fun v -> v.DisplayName) |> String.concat "," + if frees |> Zset.exists (fun v -> isExpandVar g v && not (env.ResumableCodeDefns.ContainsVal v)) then + let nonfree = frees |> Zset.elements |> List.filter (fun v -> isExpandVar g v && not (env.ResumableCodeDefns.ContainsVal v)) |> List.map (fun v -> v.DisplayName) |> String.concat "," let msg = FSComp.SR.reprResumableCodeValueHasNoDefinition(nonfree) fallback msg else @@ -947,7 +949,7 @@ type LowerStateMachine(g: TcGlobals) = let msg = FSComp.SR.reprStateMachineInvalidForm() fallback msg -let LowerStateMachineExpr g (overallExpr: Expr) : LoweredStateMachineResult = +let LowerStateMachineExpr g (outerResumableCodeDefns: ValMap) (overallExpr: Expr) : LoweredStateMachineResult = // Detect a state machine and convert it let stateMachine = IsStateMachineExpr g overallExpr @@ -955,4 +957,4 @@ let LowerStateMachineExpr g (overallExpr: Expr) : LoweredStateMachineResult = | None -> LoweredStateMachineResult.NotAStateMachine | Some altExprOpt -> - LowerStateMachine(g).Apply(overallExpr, altExprOpt) + LowerStateMachine(g, outerResumableCodeDefns).Apply(overallExpr, altExprOpt) diff --git a/src/Compiler/Optimize/LowerStateMachines.fsi b/src/Compiler/Optimize/LowerStateMachines.fsi index 4ee177174e2..814b7a45d14 100644 --- a/src/Compiler/Optimize/LowerStateMachines.fsi +++ b/src/Compiler/Optimize/LowerStateMachines.fsi @@ -3,6 +3,7 @@ module internal FSharp.Compiler.LowerStateMachines open FSharp.Compiler.TypedTree +open FSharp.Compiler.TypedTreeOps open FSharp.Compiler.TcGlobals type LoweredStateMachine = @@ -30,4 +31,5 @@ type LoweredStateMachineResult = /// Analyze a TAST expression to detect the elaborated form of a state machine expression, a special kind /// of object expression that uses special code generation constructs. -val LowerStateMachineExpr: g: TcGlobals -> overallExpr: Expr -> LoweredStateMachineResult +val LowerStateMachineExpr: + g: TcGlobals -> outerResumableCodeDefns: ValMap -> overallExpr: Expr -> LoweredStateMachineResult From d477745b63c33aea057ee50caa34d633e1d6d693 Mon Sep 17 00:00:00 2001 From: Jakub Majocha <1760221+majocha@users.noreply.github.com> Date: Thu, 19 Feb 2026 09:58:21 +0100 Subject: [PATCH 13/16] fix double wrapped Delay in CancellableTask --- .../src/FSharp.Editor/Common/CancellableTasks.fs | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/vsintegration/src/FSharp.Editor/Common/CancellableTasks.fs b/vsintegration/src/FSharp.Editor/Common/CancellableTasks.fs index 4299936d05a..5056ddee461 100644 --- a/vsintegration/src/FSharp.Editor/Common/CancellableTasks.fs +++ b/vsintegration/src/FSharp.Editor/Common/CancellableTasks.fs @@ -123,11 +123,9 @@ module CancellableTasks = member inline _.Delay ([] generator: unit -> CancellableTaskCode<'TOverall, 'T>) : CancellableTaskCode<'TOverall, 'T> = - ResumableCode.Delay(fun () -> - CancellableTaskCode(fun sm -> - sm.Data.ThrowIfCancellationRequested() - (generator ()).Invoke(&sm) - ) + CancellableTaskCode(fun sm -> + sm.Data.ThrowIfCancellationRequested() + (generator ()).Invoke(&sm) ) From ba1d9a5c74ac7e92631272a8aa0e0cb5b2df0abb Mon Sep 17 00:00:00 2001 From: Jakub Majocha <1760221+majocha@users.noreply.github.com> Date: Thu, 19 Feb 2026 10:20:48 +0100 Subject: [PATCH 14/16] fix fantomas --- .../src/FSharp.Editor/CodeFixes/ImplementInterface.fs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/vsintegration/src/FSharp.Editor/CodeFixes/ImplementInterface.fs b/vsintegration/src/FSharp.Editor/CodeFixes/ImplementInterface.fs index 52e04a0ce06..92f0c0077d7 100644 --- a/vsintegration/src/FSharp.Editor/CodeFixes/ImplementInterface.fs +++ b/vsintegration/src/FSharp.Editor/CodeFixes/ImplementInterface.fs @@ -31,6 +31,8 @@ type internal InterfaceState = } // TODO: rewrite token arithmetics properly here +// state machine not statically compilable (let rec in state machine) +#nowarn "3511" [] type internal ImplementInterfaceCodeFixProvider [] () = @@ -171,7 +173,6 @@ type internal ImplementInterfaceCodeFixProvider [] () = interface IFSharpMultiCodeFixProvider with member _.GetCodeFixesAsync context = - #nowarn 3511 // let rec in state machine cancellableTask { let! cancellationToken = CancellableTask.getCancellationToken () @@ -288,4 +289,3 @@ type internal ImplementInterfaceCodeFixProvider [] () = ) | _ -> return Seq.empty } - #warnon 3511 From 8c45c13d3942b2382f9dac43ba4cf69d171cbe25 Mon Sep 17 00:00:00 2001 From: Jakub Majocha <1760221+majocha@users.noreply.github.com> Date: Thu, 19 Feb 2026 10:24:12 +0100 Subject: [PATCH 15/16] release notes --- docs/release-notes/.VisualStudio/18.vNext.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes/.VisualStudio/18.vNext.md b/docs/release-notes/.VisualStudio/18.vNext.md index 587f3e8bad0..7131fc8e418 100644 --- a/docs/release-notes/.VisualStudio/18.vNext.md +++ b/docs/release-notes/.VisualStudio/18.vNext.md @@ -3,5 +3,6 @@ * Fixed Rename incorrectly renaming `get` and `set` keywords for properties with explicit accessors. ([Issue #18270](https://github.com/dotnet/fsharp/issues/18270), [PR #19252](https://github.com/dotnet/fsharp/pull/19252)) * Fixed Find All References crash when F# project contains non-F# files like `.cshtml`. ([Issue #16394](https://github.com/dotnet/fsharp/issues/16394), [PR #19252](https://github.com/dotnet/fsharp/pull/19252)) * Find All References for external DLL symbols now only searches projects that reference the specific assembly. ([Issue #10227](https://github.com/dotnet/fsharp/issues/10227), [PR #19252](https://github.com/dotnet/fsharp/pull/19252)) +* Improve static compilation of state machines. ([PR #19297](https://github.com/dotnet/fsharp/pull/19297)) ### Changed From 84783c8f12d4f2d005ff1d590e997df1a751e4d9 Mon Sep 17 00:00:00 2001 From: Jakub Majocha <1760221+majocha@users.noreply.github.com> Date: Wed, 25 Feb 2026 13:12:11 +0100 Subject: [PATCH 16/16] state-machine tests: bind tasks as top level values --- tests/fsharp/core/state-machines/test.fsx | 36 +++++++++++------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/tests/fsharp/core/state-machines/test.fsx b/tests/fsharp/core/state-machines/test.fsx index 7e502839d6d..3152cf2c41c 100644 --- a/tests/fsharp/core/state-machines/test.fsx +++ b/tests/fsharp/core/state-machines/test.fsx @@ -484,17 +484,17 @@ let inline checkStateMachine nm = // These simply detect state machine compilation module ``Check simple task compiles to state machine`` = - let test1() = + let test1 = task { checkStateMachine "vevroerhn11" return 1 } - test1().Wait() + test1.Wait() module ``Check simple task with bind compiles to state machine`` = - let test2() = + let test2 = task { checkStateMachine "vevroerhn12" @@ -503,14 +503,14 @@ module ``Check simple task with bind compiles to state machine`` = return 1 } - test2().Wait() + test2.Wait() module ``Check task with multiple bind doesn't cause code explosion from inlining and compiles to state machine`` = let syncTask() = Task.FromResult 100 - let tenBindSync_Task() = + let tenBindSync_Task = task { let! res1 = syncTask() checkStateMachine "vevroerhn121" @@ -535,10 +535,10 @@ module ``Check task with multiple bind doesn't cause code explosion from inlinin return res1 + res2 + res3 + res4 + res5 + res6 + res7 + res8 + res9 + res10 } - tenBindSync_Task().Wait() + tenBindSync_Task.Wait() module ``Check task with try with compiles to state machine`` = - let t67() = + let t67 = task { checkStateMachine "vevroerhn180" try @@ -552,10 +552,10 @@ module ``Check task with try with compiles to state machine`` = () } - t67().Wait() + t67.Wait() module ``Check task with try with and incomplete match compiles to state machine`` = - let t68() = + let t68 = task { try checkStateMachine "vevroerhn190" @@ -566,10 +566,10 @@ module ``Check task with try with and incomplete match compiles to state machine () } - t68().Wait() + t68.Wait() module ``Check task with while loop with resumption points compiles to state machine`` = - let t68() : Task = + let t68 : Task = task { checkStateMachine "vevroerhn200" let mutable i = 0 @@ -582,10 +582,10 @@ module ``Check task with while loop with resumption points compiles to state mac return i } - t68().Wait() + t68.Wait() module ``Check task with try finally compiles to state machine`` = - let t68() = + let t68 = task { let mutable ran = false try @@ -598,10 +598,10 @@ module ``Check task with try finally compiles to state machine`` = return ran } - t68().Wait() + t68.Wait() module ``Check task with use compiles to state machine`` = - let t68() = + let t68 = task { let mutable disposed = false use d = { new System.IDisposable with member __.Dispose() = disposed <- true } @@ -610,10 +610,10 @@ module ``Check task with use compiles to state machine`` = checkStateMachine "vevroerhn221" } - t68().Wait() + t68.Wait() module ``Check nested task compiles to state machine`` = - let t68() = + let t68 = task { let mutable n = 0 checkStateMachine "vevroerhn230" @@ -628,7 +628,7 @@ module ``Check nested task compiles to state machine`` = n <- n + 1 } - t68().Wait() + t68.Wait() module ``Check after code may include closures`` = let makeStateMachine x =