Skip to content

Enable runtime async via compiler intrinsics - #20235

Open
majocha wants to merge 86 commits into
dotnet:mainfrom
majocha:runtime-async-intrinsic
Open

Enable runtime async via compiler intrinsics#20235
majocha wants to merge 86 commits into
dotnet:mainfrom
majocha:runtime-async-intrinsic

Conversation

@majocha

@majocha majocha commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

Add preview F# compiler support for .NET runtime-async methods. Compiler-recognized __runtimeAsyncReturn intrinsics mark Task/ValueTask methods and lambdas with MethodImplOptions.Async, while AsyncHelpers.Await* calls become runtime suspension points.

The optimizer preserves and specializes inline suspension fragments, rewrites suspending exception handlers and finally compensations, and reports unsupported byref or suspension patterns. The feature is gated by langversion:preview and target-runtime metadata support; FSharp.Core exposes the intrinsics only for net10.0, with builders remaining application-defined.


Consider an inline "sync" CE builder. applying __runtimeAsyncReturn to the inlined code in its Run method compiles the computation expression into a single runtime async method:

member inline _.Run([<InlineIfLambda>] code) = __runtimeAsyncReturn(code())

Resumption is handled by the runtime, calling AsyncHelpers.Await in Bind is all that is needed to make the resulting CE async:

member inline _.Bind(task, [<InlineIfLambda>] continuation) = AsyncHelpers.Await task |> continuation

runtime spec :
Runtime-async specification

interesting docs and links:

  • decide on naming
  • design and implement other allowed return types (ValueTask<_> and unit versions)
  • awaits in EH blocks (IAsyncDisposable) - handled by rewriting the block to take suspensions outside
  • byref locals not preserved across suspension - added diagnostic
  • use of AsyncHelpers suspending methods outside of runtime async - added diagnostic
  • handle --optimize- (debug configuration)
  • fixed (pinned) locals not preserved across suspension - needs separate codepath
  • implement IAsyncEnumerable, low level production and consumption
  • test debug stepping / stack traces - tested manually, they are not great
  • test AsyncLocals propagation
  • add sample low level implementation of IAsyncEnumerable
  • implement sample asyncSeq builder
  • implement YieldFromFInal handover in asyncSeq
  • update ildasm - separate PR, because of bulk baseline changes
  • extend resumable state machines to allow runtime-async MoveNext for async iterators

@github-actions

github-actions Bot commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

❗ Release notes required

You can open this PR in browser to add release notes: open in github.dev


✅ Found changes and release notes in following paths:

Change path Release notes path Description
`src/FSharp.Core` docs/release-notes/.FSharp.Core/11.0.100.md
`src/Compiler` docs/release-notes/.FSharp.Compiler.Service/11.0.100.md
`src/Compiler/Facilities/LanguageFeatures.fsi` docs/release-notes/.Language/preview.md

majocha and others added 5 commits August 8, 2026 09:20
…c; add Language preview release notes

The features dictionary lost ImplicitDIMCoverage, MethodOverloadsCache,
ErrorOnMissingSignatureAttribute, DirectDelegateConstruction,
AccessProtectedBaseFieldFromClosure and RecordSpreads entries, causing
54 CI test failures ('Unable to find feature' internal errors and
preview features not enabled).

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Comment thread tests/FSharp.Core.UnitTests/FSharp.Core.SurfaceArea.netstandard20.release.bsl Outdated
Comment thread src/FSharp.Core/resumable.fs Outdated
T-Gro

This comment was marked as outdated.

@majocha

majocha commented Aug 13, 2026

Copy link
Copy Markdown
Contributor Author

Another thing to think through is inlining. Currently there are no checks at all for use of suspending AsyncHelpers members outside of an async method. According to spec, this is illegal but the idea is that any AsyncHelpers.Await calls should be contained by or inlined into the resulting async method (see the sample runtimeTask builder in the tests here). It seems to get an efficient single method from a CE the builder needs to declare every method inline and make use of InlineIfLambda.

Currently it is up to the "expert" user to not misuse AsyncHelpers. Ideally the compiler should check for any such illegal calls only after inlining.

This is still a sketch, but it successfully compiles runtimeTask builder. The builder passes ported Tasks.fs tests, which is promising.

@T-Gro

T-Gro commented Aug 14, 2026

Copy link
Copy Markdown
Member

Ideally the compiler should check for any such illegal calls only after inlining.

We could have a notion of PostIlxGen checks.
Agree it must run after all optimizations.

T-Gro and others added 2 commits August 18, 2026 14:59
Roslyn-async2-inspired edge cases for the runtime-async intrinsic, driven through
the test-only runtimeTask CE (treated as a hypothetical library):
  * execution fixture (RuntimeAsync/RuntimeAsyncEdgeCases.fs): locals/loops across
    suspension, non-ref struct across suspension, ValueTask operand, exception
    propagation, IAsyncDisposable with genuinely-async DisposeAsync.
  * facts (RuntimeAsyncEdgeCaseTests.fs): Await overload selection per operand type,
    no compiler state machine (direct + CE), the C1 forbidden `tail.` prefix, and a
    parametrized set of currently-undiagnosed contract-forbidden patterns
    (await-in-finally/catch, ref-struct- and byref-across-suspension).

Every asserted IL substring and runtime symptom was captured empirically on the
pinned net11 preview; the forbidden patterns match docs/runtime-async.md.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 55dd72c8-46d3-4959-9677-c52d41779596
The sequence-points baseline (and ildasm) cannot render MethodImplOptions.Async
(0x2000), so the lifted __runtimeAsync body shows up as a plain outer@<line>
closure. Factor the metadata flag check into assertAsyncFlagOnLiftedClosureOnly
and chain it onto the sequence-points fact so the exact program that emits the
.bsl also proves the async marker lands only on the lifted closure, never on the
user's outer/helper methods.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 55dd72c8-46d3-4959-9677-c52d41779596
@majocha

majocha commented Aug 20, 2026

Copy link
Copy Markdown
Contributor Author

Looks like the runtime feature is quickly evolving, see #19056 (comment)

runtime-async-tiering-and-tail-await-optimizations

T-Gro added a commit that referenced this pull request Aug 20, 2026
The previous run was SIGKILL'd by the OOM-killer mid-suite
(0 real test failures; 229 tests never ran). Empty commit to re-run
the pipeline. Same exit-137 flake also hit unrelated PRs #20274 and
#20235 at the same time.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@majocha

majocha commented Aug 20, 2026

Copy link
Copy Markdown
Contributor Author

I wonder how to support the other allowed return types.

__runtimeAsyncReturn<'T>       : 'T -> Task<'T>
__runtimeAsyncReturnValueTask<'T> : 'T -> ValueTask<'T>
__runtimeAsyncReturnUnit       : unit -> Task
__runtimeAsyncReturnValueTaskUnit : unit -> ValueTask

and it quickly becomes a whole zoo. Do we need the non-generic versions at all? Only for potential C# interop, I guess. The upside is that the current type check is all we need to keep it correct, without any extra handling.

The other alternative it to have a unconstrained __runtimeAsyncReturn: 'T -> 'Carrier and do extra checks that the 'Carrier type is supported.

@github-actions github-actions Bot added ⚠️ Affects-Test-Tooling Tooling check: PR touches test framework infrastructure ⚠️ Affects-Compiler-Output Tooling check: PR touches IL emission or codegen ⚠️ Affects-Bootstrap Tooling check: PR touches compiler bootstrap chain labels Sep 2, 2026
@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

@majocha

majocha commented Sep 7, 2026

Copy link
Copy Markdown
Contributor Author

I ordered the AI to take the sample runtime async asyncSeq and make a full FSharp.Control.TaskSeq reimplementation:

https://github.com/majocha/FSharp.Control.TaskSeq/tree/runtime-async

Remarkably it passes the whole test suite. (in Release).

It also reveals some more debug configuration bugs in this PR.

@github-actions

This comment has been minimized.

T-Gro pushed a commit that referenced this pull request Sep 7, 2026
The coreclr_release job was canceled after the 120-minute limit due to a
flaky infrastructure timeout (memory pressure hanging an unrelated test
assembly). The same job timed out on unrelated PRs #20235 and #20393 in the
last 10 days. ComponentTests (all DIM/interface tests affected by this PR)
passed fully, so the merge resolution is correct.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

@T-Gro T-Gro left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖🕵️ AI-assisted review. Every finding reproduced on a local build of dac78834: captured output, attribution controls, --optimize+ and --optimize-.

Comment thread src/Compiler/Optimize/RuntimeAsyncExceptionRewrite.fs Outdated
Comment thread src/Compiler/Optimize/RuntimeAsyncAnalysis.fs
// application of local type functions with type parameters = measure types and body = local value - inline the body
GenExpr cenv cgbuf eenv v sequel

| Expr.App _ when TryGetRuntimeAsyncReturn g expr |> Option.isSome -> GenRuntimeAsyncReturnAsStartedTask cenv cgbuf eenv expr sequel

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖🕵️ byref captured by the closure synthesized after PostInferenceChecks, so FS0406 has already run. The flag lands on f@8::Invoke, not f.

[<NoCompilerInlining>]
let f (x: byref<int>) : Task<int> =
    let y = x
    __runtimeAsyncReturn (x + y)
  • now: compiles clean; ILVerify StackByRef @0x06; run → NRE; both opt modes
  • expected: rejected, or lowered without a capturing closure
  • no leading let, or y unused → flag on M::f, verifies clean, returns 2
  • fix: reject byref/byref-like/pinning free values before the lambda is built

Comment thread src/Compiler/Optimize/RuntimeAsyncAnalysis.fs Outdated
Comment thread src/Compiler/Checking/Expressions/CheckExpressions.fs
Comment thread src/Compiler/Optimize/RuntimeAsyncAnalysis.fs Outdated
Comment thread src/Compiler/CodeGen/IlxGen.fs
Comment thread tests/FSharp.Compiler.ComponentTests/Language/RuntimeAsyncTests.fs
Comment thread src/Compiler/Optimize/Optimizer.fs Outdated
@github-actions github-actions Bot added the ⚠️ Affects-Build-Infra Tooling check: PR touches build infrastructure label Sep 9, 2026
@github-actions

github-actions Bot commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

🔍 Tooling Safety Check — Affects-Bootstrap, Affects-Build-Infra, Affects-Compiler-Output, Affects-Test-Tooling
Affects-Bootstrap: Compiler sources alter the self-hosting compiler chain.
Affects-Build-Infra: Compiler project changes participate in builds.
Affects-Compiler-Output: Code generation changes alter emitted binaries.
Affects-Test-Tooling: Component-test project infrastructure is modified.

Generated by PR Tooling Safety Check · gpt56 2.6M ·

@majocha

majocha commented Sep 10, 2026

Copy link
Copy Markdown
Contributor Author

Unfortunatelly to make async iterators like TaskSeq implementable efficiently, we still need resumable state machine. In fact we need to extend it to generate a runtime-async MoveNext. It would allow to do runtime-async AsyncHelpers.Await directly in a state machine step.

See:

https://github.com/dotnet/roslyn/blob/features/runtime-async-streams/docs/compilers/CSharp/Runtime%20Async-Streams%20Design.md

@majocha

majocha commented Sep 10, 2026

Copy link
Copy Markdown
Contributor Author

AI thoughts on runtime async state machines implementation:
https://gist.github.com/majocha/785b8cf30de031eec340cc4227e93061

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

⚠️ Affects-Bootstrap Tooling check: PR touches compiler bootstrap chain ⚠️ Affects-Build-Infra Tooling check: PR touches build infrastructure ⚠️ Affects-Compiler-Output Tooling check: PR touches IL emission or codegen ⚠️ Affects-Test-Tooling Tooling check: PR touches test framework infrastructure

Projects

Status: New

Development

Successfully merging this pull request may close these issues.

2 participants