From 8982b41ec7be74a377e91938a9cbab3f81fe728e Mon Sep 17 00:00:00 2001 From: Copilot Date: Fri, 18 Sep 2026 21:37:14 +0200 Subject: [PATCH 1/2] Reject runtime Await in ordinary task state-machine methods Reset runtime-async and inline emission context at generated method boundaries, preserving the enclosing runtime-async body. Cover FS3918 diagnostics, imported inline templates, staged legal composition, and emitted method flags in both optimization modes. Fixes #20576 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../.FSharp.Compiler.Service/11.0.100.md | 1 + src/Compiler/CodeGen/IlxGen.fs | 6 + .../Language/RuntimeAsyncTests.fs | 164 ++++++++++++++++++ 3 files changed, 171 insertions(+) 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 f02f04c8dcf..f0553a33f69 100644 --- a/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md +++ b/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md @@ -157,6 +157,7 @@ * Avoid per-instance lock object in InterruptibleLazy and DelayInitArrayMap (PR [#20088](https://github.com/dotnet/fsharp/pull/20088)) * IL: fix leaking binary view ([PR #20250](https://github.com/dotnet/fsharp/pull/20250)) * FCS: fix races that made the background builder repeat work ([PR #20481](https://github.com/dotnet/fsharp/pull/20481)) +* Reject runtime-only Await calls inside ordinary task state-machine methods with FS3918, while preserving normal task composition inside runtime-async methods. ([Issue #20576](https://github.com/dotnet/fsharp/issues/20576)) * Fix dependency ordering and stack safety for recursive inline bindings. ([PR #20111](https://github.com/dotnet/fsharp/pull/20111)) ### Added diff --git a/src/Compiler/CodeGen/IlxGen.fs b/src/Compiler/CodeGen/IlxGen.fs index cb7a345c986..d8e4561cf8e 100644 --- a/src/Compiler/CodeGen/IlxGen.fs +++ b/src/Compiler/CodeGen/IlxGen.fs @@ -6639,6 +6639,12 @@ and GenStructStateMachine cenv cgbuf eenvouter (res: LoweredStateMachine) sequel let sequel = if retTy.IsNone then discardAndReturnVoid else Return let ilCode = + let eenvinner = + { eenvinner with + inRuntimeAsyncMethod = false + inInlineMethod = false + } + CodeGenMethodForExpr cenv cgbuf.mgbuf ([], imethName, eenvinner, 1 + argVals.Length, None, bodyR, sequel) let ilParams = diff --git a/tests/FSharp.Compiler.ComponentTests/Language/RuntimeAsyncTests.fs b/tests/FSharp.Compiler.ComponentTests/Language/RuntimeAsyncTests.fs index 2482107f5f7..f13ba8fed0d 100644 --- a/tests/FSharp.Compiler.ComponentTests/Language/RuntimeAsyncTests.fs +++ b/tests/FSharp.Compiler.ComponentTests/Language/RuntimeAsyncTests.fs @@ -1,8 +1,10 @@ module Language.RuntimeAsyncTests open Xunit +open FSharp.Test open FSharp.Test.Compiler open System.IO +open System.Text.RegularExpressions let private runtimeAsyncSource = """ module RuntimeAsyncTest @@ -79,6 +81,168 @@ type Calculator() = """ #if NETCOREAPP +let private nestedTaskSource = """module NestedTask + +open System.Threading.Tasks +open System.Runtime.CompilerServices +open Microsoft.FSharp.Core.CompilerServices.StateMachineHelpers + +let run (ready: Task) : Task = + __runtimeAsyncReturn ( + let child = task { + let value = AsyncHelpers.Await ready + return value + 1 + } + AsyncHelpers.Await child) +""" + +[] +[] +[] +[] +[] +[] +[] +[] +[] +let ``Issue 20576 rejects runtime Await in ordinary task methods`` (optimize: bool, shape: string) = + let configure = withLangVersionPreview >> withFSharpCoreShippedNet >> withOptimization optimize + let helper = "let inline awaitValue (ready: Task) = System.Runtime.CompilerServices.AsyncHelpers.Await ready" + let source, references = + match shape with + | "direct" -> nestedTaskSource, [] + | "local" -> + nestedTaskSource + .Replace("let child = task {", $"{helper}\n let child = task {{") + .Replace("let value = AsyncHelpers.Await ready", "let value = awaitValue ready"), [] + | "imported" -> + let library = FSharp($"module AwaitLibrary\nopen System.Threading.Tasks\n{helper}") |> asLibrary |> withName "AwaitLibrary" |> configure + library |> compile |> shouldSucceed |> ignore + nestedTaskSource.Replace("let value = AsyncHelpers.Await ready", "let value = AwaitLibrary.awaitValue ready"), [ library ] + | "unit" -> + nestedTaskSource.Replace("Task", "Task").Replace("let value = AsyncHelpers.Await ready", "AsyncHelpers.Await ready").Replace("return value + 1", "return ()"), [] + | _ -> failwith $"Unexpected shape: {shape}" + let result = + FSharp source + |> asLibrary + |> configure + |> withReferences references + |> compile + result |> shouldFail |> withErrorCode 3918 |> ignore + let ranges = + match optimize, shape with + // Optimized task-template inlining attributes the call to the inner task keyword. + | true, "local" -> [10, 21, 25] + | true, _ -> [9, 21, 25] + | false, "direct" -> [10, 25, 49] + | false, "local" -> [9, 52, 108; 11, 25, 41] + | false, "imported" -> [10, 25, 54] + | false, "unit" -> [10, 13, 37] + | _ -> failwith $"Unexpected shape: {shape}" + Assert.Equal(ranges.Length, result.Output.Diagnostics.Length) + Assert.Equal(ranges.Length, result.Output.PerFileErrors.Length) + result + |> withDiagnostics [ + for line, startCol, endCol in ranges -> + Error 3918, Line line, Col startCol, Line line, Col endCol, + "Runtime async suspension method 'Await' may only be called from a runtime async method." + ] + |> ignore + +[] +[] +[] +let ``Issue 20576 preserves ordinary task composition`` (optimize: bool) = + let result = + FSharp """ +module TaskComposition +open System +open System.Threading.Tasks +open System.Runtime.CompilerServices +open Microsoft.FSharp.Core.CompilerServices.StateMachineHelpers + +let child (ready: Task<'T>) : Task<'T> = __runtimeAsyncReturn (AsyncHelpers.Await ready) + +let run (before: Task) (ready: Task<'T>) (after: Task) + (enteredReady: TaskCompletionSource) (enteredAfter: TaskCompletionSource) marked transform : Task<'U> = + __runtimeAsyncReturn ( + AsyncHelpers.Await before + let nested = task { + enteredReady.SetResult () + let! value = if marked then child ready else ready + return transform value + } + let result = AsyncHelpers.Await nested + enteredAfter.SetResult () + AsyncHelpers.Await after + result) + +let gate<'T> () = TaskCompletionSource<'T>(TaskCreationOptions.RunContinuationsAsynchronously) +let wait (work: Task<'T>) = work.WaitAsync(TimeSpan.FromSeconds 30.).GetAwaiter().GetResult() +let pending (work: Task) = if work.IsCompleted then failwith "Expected pending operation" + +let check marked input transform expected = + let before, ready, after = gate(), gate<_>(), gate() + let enteredReady, enteredAfter = gate(), gate() + let work = run before.Task ready.Task after.Task enteredReady enteredAfter marked transform + pending work + before.SetResult () + wait enteredReady.Task + pending work + ready.SetResult input + wait enteredAfter.Task + pending work + after.SetResult () + if wait work <> expected then failwith "Unexpected result" + +[] +let main _ = + for marked in [false; true] do + check marked 41 ((+) 1) 42 + check marked "forty" (fun value -> value + "-two") "forty-two" + let mutable observed = false + check marked () (fun () -> observed <- true) () + if not observed then failwith "Missing unit side effect" + 0 +""" + |> withLangVersionPreview + |> withFSharpCoreShippedNet + |> withOptimization optimize + |> compileExeAndRun + |> shouldSucceed + result |> withMetadataReader (fun md -> + let methods = [ for handle in md.MethodDefinitions -> md.GetMethodDefinition handle ] + let generatedNames = ["MoveNext"; "SetStateMachine"; "get_ResumptionPoint"; "get_Data"; "set_Data"] + for name in generatedNames do + let method = methods |> List.filter (fun method -> md.GetString method.Name = name) |> Assert.Single + Assert.Equal(0, int method.ImplAttributes &&& 0x2000) + if name = "MoveNext" then + let mutable signature = md.GetBlobReader method.Signature + Assert.False(signature.ReadSignatureHeader().IsGeneric) + Assert.Equal(0, signature.ReadCompressedInteger()) + Assert.Equal(System.Reflection.Metadata.SignatureTypeCode.Void, signature.ReadSignatureTypeCode()) + for name in ["child"; "run"] do + let method = methods |> List.filter (fun method -> md.GetString method.Name = name) |> Assert.Single + Assert.Equal(0x2000, int method.ImplAttributes &&& 0x2000) + if optimize then + let liftedChild = + methods |> List.filter (fun method -> md.GetString method.Name = "Invoke" && int method.ImplAttributes &&& 0x2000 <> 0) + Assert.Single(liftedChild) |> ignore) + let _, _, il = ILChecker.verifyILAndReturnActual [] result.OutputPath.Value [] + let methodBody name = + Regex.Match(il, @"(?ms)^(?[ \t]*)\.method[^{}]*\b" + name + @"(?:<[^>]+>)?\([^{}]*\{.*?^\k\}").Value + let moveNext = methodBody "MoveNext" + Assert.NotEmpty(moveNext) + Assert.Contains("void", moveNext) + Assert.DoesNotContain("AsyncHelpers::Await", moveNext) + if optimize then + Assert.Contains("AsyncHelpers::Await", methodBody "Invoke") + Assert.Contains("FSharpFunc`2", moveNext) + else + Assert.Contains("TaskComposition::child", moveNext) + for name in ["child"; "run"] do + Assert.Contains("AsyncHelpers::Await", methodBody name) + let private runtimeAsyncCrossAssemblyLibrary = """ module RuntimeAsyncCrossAssemblyLibrary From ae667cc9d7a4acb1bb486545fbe41d328eec4381 Mon Sep 17 00:00:00 2001 From: Copilot Date: Fri, 18 Sep 2026 23:31:42 +0200 Subject: [PATCH 2/2] Add release notes Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- docs/release-notes/.FSharp.Compiler.Service/11.0.100.md | 2 +- 1 file changed, 1 insertion(+), 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 f0553a33f69..744d06e7b5c 100644 --- a/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md +++ b/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md @@ -157,7 +157,7 @@ * Avoid per-instance lock object in InterruptibleLazy and DelayInitArrayMap (PR [#20088](https://github.com/dotnet/fsharp/pull/20088)) * IL: fix leaking binary view ([PR #20250](https://github.com/dotnet/fsharp/pull/20250)) * FCS: fix races that made the background builder repeat work ([PR #20481](https://github.com/dotnet/fsharp/pull/20481)) -* Reject runtime-only Await calls inside ordinary task state-machine methods with FS3918, while preserving normal task composition inside runtime-async methods. ([Issue #20576](https://github.com/dotnet/fsharp/issues/20576)) +* Reject runtime-only Await calls inside ordinary task state-machine methods with FS3918, while preserving normal task composition inside runtime-async methods. ([Issue #20576](https://github.com/dotnet/fsharp/issues/20576), [PR #20584](https://github.com/dotnet/fsharp/pull/20584)) * Fix dependency ordering and stack safety for recursive inline bindings. ([PR #20111](https://github.com/dotnet/fsharp/pull/20111)) ### Added