Runtime Async Feature#19449
Conversation
…dDef.IsAsync - Add RuntimeAsync DU case to LanguageFeature, mapped to previewVersion - Add ILMethodDef.IsAsync property and WithAsync method using enum<MethodImplAttributes>(0x2000) - Add 'async' keyword output in ilprint.fs goutput_mbody - Add error messages 3884-3887 to FSComp.txt (tcRuntimeAsync*) - Bump MicrosoftNETCoreILDAsmVersion to 10.0.0 in eng/Versions.props - Add Task/ValueTask type refs to TcGlobals (system_Task_tcref, system_GenericTask_tcref, system_ValueTask_tcref, system_GenericValueTask_tcref) - Update all 13 XLF translation files
- Add hasAsyncImplFlag extraction in ComputeMethodImplAttribs (bit 0x2000) - Extend return tuple to 6-tuple with hasAsyncImplFlag - Update tuple destructuring in GenMethodForBinding and GenAbstractBinding - Add .WithAsync(hasAsyncImplFlag) to method def builder chain in both paths
- Add HasMethodImplAsyncAttribute helper (detects 0x2000 flag) - Add HasMethodImplSynchronizedAttribute helper (detects 0x20 flag) - Add IsTaskLikeType helper using TcGlobals Task/ValueTask type refs - Add validation in TcNormalizedBinding gated behind LanguageFeature.RuntimeAsync: - Error 3885 if async+synchronized combo - Error 3884 if async method doesn't return Task/ValueTask - Error 3886 if async method returns byref
- Add isRuntimeFeatureAsyncSupported lazy check in InfoReader.fs - Wire into IsLanguageFeatureRuntimeSupported for LanguageFeature.RuntimeAsync - Add error 3887 (tcRuntimeAsyncNotSupported) in CheckExpressions.fs validation when runtime doesn't support RuntimeFeature.Async (.NET 10+)
- Add ExprContainsAsyncHelpersAwaitCall function that walks TAST expression tree - Detects calls to System.Runtime.CompilerServices.AsyncHelpers.Await (any overload) - Handles all Expr forms: Let, LetRec, Sequential, Lambda, App, Match, Obj, etc. - In GenMethodForBinding: hasAsyncImplFlag = fromAttr || ExprContainsAsyncHelpersAwaitCall body - Propagates async flag through inlined CE methods
- Add UnwrapTaskLikeType helper: extracts T from Task<T>/ValueTask<T>, unit from Task/ValueTask - In TcNormalizedBinding: when method has MethodImpl(0x2000) and returns Task<T>, body is type-checked against T (not Task<T>) - Method's declared return type in IL remains Task<T> unchanged - Runtime handles wrapping T -> Task<T> for the caller
- Create src/FSharp.Core/runtimeAsync.fs with RuntimeAsyncAttribute - Attribute available on all TFMs (netstandard2.0, netstandard2.1) - Meaningful on .NET 10.0+ where runtime-async is supported - Add file to FSharp.Core.fsproj compilation order (before tasks.fsi)
- Create src/FSharp.Core/runtimeTasks.fs with RuntimeTaskBuilder - All builder members are inline (no state machine generated) - Run method has [<MethodImplAttribute(enum<MethodImplOptions> 0x2000)>] - RuntimeTaskBuilderUnsafe.cast bridges T -> Task<T> type mismatch - runtimeTask CE instance exposed via [<AutoOpen>] module - Entire builder gated with #if NET10_0_OR_GREATER (AsyncHelpers only on .NET 10+) - Add file to FSharp.Core.fsproj after runtimeAsync.fs
- Add RuntimeAsync IL baseline tests (verify 'cil managed async' in IL output) - Add RuntimeAsync validation error tests (async+synchronized, non-Task return, langversion gating) - Add checkLanguageFeatureAndRecover for proper langversion:preview gating - Remove tcRuntimeAsyncNotSupported check (runtime support check) - Fix FSharp.Core.fsproj: revert net10.0 TFM (breaks bootstrap build)
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
… in CheckExpressions Strip SynExpr.Typed wrapper, use fresh bodyExprTy, and pre-unify overallPatTy with Task<T> before type inference to avoid type mismatch errors. Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
… keyword positioning in IL IlxGen.fs: discard unit value before ret for non-generic Task/ValueTask return types. ilprint.fs: fix async keyword positioning in IL output. Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
…oral tests Compiler.fs: add runNewProcess and compileExeAndRunNewProcess helpers for out-of-process test execution. MethodImplAttribute.fs: update behavioral tests to use compileExeAndRunNewProcess and set DOTNET_RuntimeAsync env var in host process. Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
- Add DOTNET_RuntimeAsync=1 to TestFramework.executeProcess so child processes inherit the runtime-async feature flag - Fix IlxGen.fs: do not propagate async flag from NoDynamicInvocation methods (their bodies are replaced with 'throw' at runtime, so the async flag would cause CLR to reject the type)
…impl detail - RuntimeTaskBuilder.Run is the ONLY method with [<MethodImplAttribute(0x2000)>] - Consumer API functions need NO attribute (addFromTaskAndValueTask, etc.) - Delay returns thunk (unit -> 'T), Run has inline + 0x2000 + cast -> Task<'T> - Fix ExprContainsAsyncHelpersAwaitCall to stop at lambda boundaries, preventing double-wrapping when consumer calls Run (both would be async) - CheckExpressions.fs: skip special async handling for inline methods with 0x2000 - PostInferenceChecks.fs: allow InlineIfLambda on runtime-async method params Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-Claude) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
…nalysis Update ExprContainsAsyncHelpersAwaitCall (IlxGen) and exprContainsAsyncHelpersAwait (Optimizer) to also match AwaitAwaiter and UnsafeAwaitAwaiter method names on AsyncHelpers, not just Await. This ensures functions using the generic awaitable path (e.g. Task.Yield via UnsafeAwaitAwaiter) are correctly detected for cil managed async flag propagation and cross-module anti-inlining. Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
…ield support RuntimeTaskBuilder: add intrinsic Bind overloads for ConfiguredTaskAwaitable, ConfiguredTaskAwaitable<T>, ConfiguredValueTaskAwaitable, ConfiguredValueTaskAwaitable<T> using optimized AsyncHelpers.Await. Add generic SRTP extension Bind for any awaitable with GetAwaiter() via AsyncHelpers.UnsafeAwaitAwaiter — enables Task.Yield() and custom awaitables. Api.fs: replace Task.Delay(0) with real Task.Yield() in taskDelayYieldAndRun, add configureAwaitExample (.ConfigureAwait(false)), add inlineNestedRuntimeTask (nesting via helper functions). Program.fs: call and print new examples (11 total, all verified passing). Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
…amples Reflect 11 examples (up from 9), generic SRTP Bind for any awaitable, ConfiguredTaskAwaitable Bind overloads, Task.Yield() support, updated expected output and IL verification notes. Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
… RuntimeAsyncAttribute Task 4: IlxGen.fs - hasAsyncImplFlag now requires enclosing entity to have [<RuntimeAsync>]. Both the explicit [<MethodImpl(0x2000)>] path and the body-analysis (ExprContainsAsyncHelpersAwaitCall) path are gated behind enclosingEntityHasRuntimeAsync (via v.TryDeclaringEntity). Task 5: Optimizer.fs - cut function now only blocks inlining of AsyncHelpers.Await-containing lambdas when the enclosing type has [<RuntimeAsync>]. Module-level functions (MemberInfo = None) are allowed to inline freely.
…ctions Remove the enclosingHasRuntimeAsync guard from Optimizer.fs cut function so that ALL functions containing AsyncHelpers.Await calls get UnknownValue (no cross-module inlining), not just those in RuntimeAsync-annotated types. Previously, consumer functions in plain modules (e.g. Api.consumeOlderTaskCE, Api.taskDelayYieldAndRun) were being inlined into main by the optimizer because their enclosing module lacked [<RuntimeAsync>]. This caused NullReferenceException at runtime because the cast trick only works inside 'cil managed async' methods. Also: - Fix TcGlobals.fs: attrib_RuntimeAsyncAttribute now uses mk_MFControl_attrib (RuntimeAsyncAttribute lives in Microsoft.FSharp.Control, not Core) - Fix TcGlobals.fs: correct indentation on attrib_RuntimeAsyncAttribute line - Fix IlxGen.fs: remove enclosingEntityHasRuntimeAsync guard from body-analysis path so consumer functions in plain modules get 'cil managed async' - Fix IlxGen.fs: set withinSEH=true for hasAsyncImplFlagEarly methods to suppress tail calls (required for 'cil managed async' suspension to work) - Fix CheckExpressions.fs: use NewTyparsOK + shared domain types for Run<T> so generic return types like Task<'T> type-check correctly - Update RuntimeTaskBuilder.fs: use [<RuntimeAsync>] on builder type (implicit NoDynamicInvocation), rename module to RuntimeTaskBuilderHelpers, remove explicit [<NoDynamicInvocation>] from individual members - Update sample .fsproj files: use repo-built FSharp.Core explicitly
…ecture - RuntimeTasks.fs: update preamble to use [<RuntimeAsync; Sealed>] on builder, remove explicit [<NoDynamicInvocation>] from Bind members (implicit from [<RuntimeAsync>]), Run now returns Task<'T> with Await sentinel + cast, remove [<MethodImplAttribute(0x2000)>] from all consumer functions, add 2 new tests (inline-nested via separate functions, no MethodImpl needed) - MethodImplAttribute.fs: add 3 new tests for [<RuntimeAsync>] builder behavior: implicit NoDynamicInvocation (IL body replaced with throw), consumer gets cil managed async without MethodImpl attribute, behavioral test (consumer executes correctly) - README.md: update Key Design section to show [<RuntimeAsync; Sealed>], RuntimeTaskBuilderHelpers, no explicit NoDynamicInvocation; add RuntimeAsync Attribute subsection explaining design entry point
… RuntimeAsyncAttribute Gate both the IlxGen.fs body-analysis 'cil managed async' path and the Optimizer.fs anti-inlining guard behind [<RuntimeAsync>] on the enclosing entity. IlxGen.fs: Add enclosingEntityHasRuntimeAsync check using v.TryDeclaringEntity. Both hasAsyncImplFlagFromAttr and body-analysis paths now require RuntimeAsync on the enclosing entity. This enables the non-inline Run architecture: only Run (in RuntimeTaskBuilder with [<RuntimeAsync>]) gets 'cil managed async', not consumer functions in plain modules. Optimizer.fs: cut function now checks vref.MemberInfo for RuntimeAsync before returning UnknownValue. Functions in plain modules (without [<RuntimeAsync>]) can be cross-module inlined normally. MethodImplAttribute.fs: Add [<RuntimeAsync>] to module TestModule in all tests that use explicit [<MethodImpl(0x2000)>] or AsyncHelpers.Await directly, since the gating now requires RuntimeAsync on the enclosing entity.
…EntityHasRuntimeAsync gate from body-analysis, block all Await-containing functions from cross-module inlining, revert Run to cast(f())
…inline Run with Await(f()), true inline-nested CEs - Add cloIsAsync field to IlxClosureInfo (ilx.fs/ilx.fsi): marks closure Invoke as cil managed async - Add ilBodyContainsAsyncHelpersAwait in IlxGen.fs: detects Await calls in closure IL body - Add isILTypeTaskLike in EraseClosures.fs: guards cil managed async to Task-returning closures - EraseClosures.fs: emit Invoke as cil managed async when cloIsAsync && isILTypeTaskLike - RuntimeTaskBuilder.fs: Delay returns unit->Task<T> (closure is cil managed async via cloIsAsync) Run is non-inline with [<MethodImplAttribute(0x2000)>], takes unit->Task<T>, does AsyncHelpers.Await(f()) - Api.fs: add trueInlineNestedRuntimeTask example (12th example, inline-nested CEs work) - Program.fs: call trueInlineNestedRuntimeTask, print result - README.md: update to describe non-inline Run + async closures architecture, add Fix 3
…ey Design section, add trailing newline - IlxGen.fs: fix 1 extra leading space on .WithAsync(hasAsyncImplFlag) in GenMethodForBinding and GenAbstractBinding - README.md: update Key Design code block to show correct Delay (unit->Task<T> with sentinel+cast) and Run (unit->Task<T> with Await(f())) - MethodImplAttribute.fs: add trailing newline at end of file
❗ Release notes requiredCaution No release notes found for the changed paths (see table below). Please make sure to add an entry with an informative description of the change as well as link to this pull request, issue and language suggestion if applicable. Release notes for this repository are based on Keep A Changelog format. The following format is recommended for this repository:
If you believe that release notes are not necessary for this PR, please add NO_RELEASE_NOTES label to the pull request. You can open this PR in browser to add release notes: open in github.dev
|
| /// Delay creates a closure that wraps the CE body. | ||
| /// [<InlineIfLambda>] on f inlines the CE body into the Delay closure, so there is only ONE | ||
| /// closure containing all AsyncHelpers.Await calls. The compiler automatically injects a | ||
| /// sentinel to ensure the Delay closure is always 'cil managed async', even when the CE body | ||
| /// has no let!/do! bindings. The compiler also handles the 'T → Task<'T> bridging automatically | ||
| /// for [<RuntimeAsync>] builders, so no cast is needed. | ||
| member inline _.Delay([<InlineIfLambda>] f: unit -> 'T) : unit -> Task<'T> = | ||
| fun () -> f() |
There was a problem hiding this comment.
This is maybe the one thing that makes me uneasy.
This introduces a special case of something like a type-driven return type conversion.
I wonder if it would be possible to have a special function instead, let's say:
val __asyncReturn: 't -> Task<'t>compiled to no-op or whatever dotnet expects for the async runtime feature.
There was a problem hiding this comment.
I guess so, similar to current StateMachineHelpers. Although I gave no thought how this should behave with ns20.
There was a problem hiding this comment.
I don't like very much that the feature resides at the level of computation expressions.
IMO it should be possible to write a working low-level runtime async method using dotnet's AsyncHelpers and some FSharp.Core helpers. Ideally runtime async CEs should compile to such code without any special treatment in CheckComputationExpressions.fs. I don't know if it's possible.
There was a problem hiding this comment.
We need to encode an invariant about the retTy of the method behaving differently, while also making sure it meets the runtime verification and is only used for MethodImpl.Async methods.
If you can do that and also manually annotate your method with the right attributes, it should also work without a CE ?
(I assume your goal is the ability to write some low level primitives without a CE, and only leave runtimeTask for orchestrating bigger workflows?)
There was a problem hiding this comment.
Yes, my thinking is basically to get it to work from first principles, so the changes to compiler are as minimal as possible. It seems no additional intrinsic functions are really needed, just an unsafe cast helper.
For example with just adding the async flag emit to the compiler the following code runs fine in net11.0
open System.Runtime.CompilerServices
open System.Threading.Tasks
module internal Helpers =
let inline runtimeAsyncReturn<'t> (v: 't) : Task<'t> =
(# "" v : Task<'t> #)
[<MethodImpl(MethodImplOptions.Async)>]
let asyncMethod() =
printfn "starting"
for x in 1 .. 5 do
printfn "tick %d" x
AsyncHelpers.Await (Task.Delay 1000)
printfn "done"
Helpers.runtimeAsyncReturn 42
asyncMethod().Result |> printfn "result: %d"There was a problem hiding this comment.
Yeah, this is basically just the proof of concept I got working a while ago. I would think it might be nice to get some compiler help like saying you can't return a TaskLike from a method with [<MethodImpl(MethodImplOptions.Async)>].
There was a problem hiding this comment.
Yes, this is exactly that. It just bothers me a bit that the rewriting happens at CheckComputationExpressions level. It would be maybe prettier to have it working at lower level. To deal with inlining, lambdas, return types generally in such a way as to allow builders to naturally use it. I don't know if it's practical to do so.
|
Is there a chance we could also emit the async metadata along the way? 🙂 This would allow a much better tooling integration for stepping. |
|
One observation: runtimeTask {
for x = 1 to 10 do
do! Task.Delay(500)
printfn "tick %d" x
printfn "Done with simple for loop"
}results in 2 separate I would expect just a single method with everything inlined and just one I think the problem is that currently this PR makes CE closures themselves I wonder if it wouldn't be better to have an explicit type representing the delayed chunks. Let's say type RuntimeAsyncCode<'T> = delegate of unit -> 'T // maybe it can be a DU not a delegate?This way we could inline it consistently into a single final async method with less magic. |
|
Currently C# compiler allows creating runtime-async lambdas. var f = async () => ...creates a Not having this in F# could be a limitation wrt to C# interop and performance at some point. |
|
How would we go around the compiler/library split in that case? Or would we abandon that and let the compiler fully learn about I am definitely open on building up a primitive in the typechecker+codegen (outside of ComputationExpressions and outside of FSharp.Core - leaving that spice for higher-level features). What we do not have is a low-level user gesture (equivalent to compiler recognized |
Yes, intrinsic + convention seems like a good way around the existing limitations. As first approximation, I'm thinking of usage like: where I think we only need to worry about providing valid managed async methods, consumption is taken care of by the runtime with |
Description
Fixes #19056
This was 99% written by Oh-My-Opencode planner/executor with Opus 4.6. I'm not claiming it's good but it did make it work.
docs/samples/runtime-async-library/README.mdis where to look at how to run the sample library.Checklist
Test cases added
Performance benchmarks added in case of performance changes
Release notes entry updated: