Skip to content

Enable runtime async via compiler intrinsics - #20235

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

majocha wants to merge 103 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
  • runtime async iterators - built-in prototype

@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 the ⚠️ Affects-Build-Infra Tooling check: PR touches build infrastructure label Sep 9, 2026
@github-actions

This comment has been minimized.

@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

majocha and others added 15 commits September 10, 2026 12:15
Record the eight task and async members already declared in async.fsi and tasks.fsi at b004333. This keeps the generated Debug baseline synchronized before adding the sequence proposal APIs.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Reuse sequence lowering for yield state while keeping runtime awaits inside generated reference-type MoveNextAsync and DisposeAsync methods. Normalize explicit recipes independently of optimization and keep ordinary input sequences synchronous.

Demonstrate library-defined awaits, synchronous and asynchronous enumeration, cleanup, and explicit cancellation policy. Keep measured continuation costs and incomplete builder-generated source ranges visible as proposal limitations.

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

This comment has been minimized.

Implement enumerator interfaces directly and reuse the first instance without sharing later acquisitions. Preserve runtime source-call/await fusion without crossing potentially throwing prefixes. Keep cold async-cleanup exception state out of every continuation and use direct configured-awaitable intrinsics.

Retain the direct ValueTask<bool> producer contract and cleanup semantics. Add exception-order, aliasing, acquisition and failure regressions; update affected API and IL expectations.

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

T-Gro commented Sep 15, 2026

Copy link
Copy Markdown
Member

🤖🕵️ Rechecked at 4dd3acc9.

Legend: checked = verified fixed; unchecked = partially fixed.

  • Stack allocation
  • Late byref capture
  • Curried evaluation order
  • Synchronized rejection
  • Fixture assertions
  • Duplicate FS3917
  • reraise
  • Callback construction effects
  • Preview gating
  • Debug surface baseline
  • Feature-off performance
Remaining details and examples

Imports and test harness omitted.

reraise — throws outer, not inner, after suspension.

__runtimeAsyncReturn (
    try raise outer
    with _ ->
        AsyncHelpers.Await gate
        try raise inner
        with _ -> reraise ())

Callback construction — executes twice instead of once.

let mutable constructed = 0
let inline twice ([<InlineIfLambda>] f: unit -> int) =
    __runtimeAsyncReturn (f () + f ())

let run () =
    twice (constructed <- constructed + 1; fun () -> 21)

Calling run leaves constructed = 2. An unused-callback variant leaves it at 0. The opaque effect() control passes.

Preview gate — this compiles and emits runtime-async code under F# 9.

// --langversion:9 --optimize-
let f x = x |> __runtimeAsyncReturn

(f 42).Result returns 42; expected FS3350. Optimization enabled correctly rejects it.

Nested ordinary seq — synchronous MoveNext contains an illegal Await.

__runtimeAsyncReturn (
    seq {
        let value = AsyncHelpers.Await gate
        yield value
        yield value + 1
    })

With an incomplete gate, enumeration throws NullReferenceException. Expected FS3916, not an async conversion of ordinary seq.

Imported pin lifetime — rejected locally, accepted across assemblies.

// Library.dll
let inline comparePin (array: byte[]) ([<InlineIfLambda>] action) =
    use before = fixed array
    action ()
    use after = fixed array
    struct (NativePtr.toNativeInt before, NativePtr.toNativeInt after)

// Consumer.dll
__runtimeAsyncReturn (comparePin data (fun () -> AsyncHelpers.Await gate))

After suspension and compacting GC, the addresses differ: the pin did not survive. The witness never dereferences a stale address.

Debug baseline: original intrinsic omissions fixed; eight other entries remain missing.

Performance: bounded optimized task/seq A/B measured +6.6% optimizer time; not a general performance claim.

Cross-assembly inlining: the new test passes, but consumer IL still contains the inlined body. It does not verify the claimed restriction.

@github-actions

Copy link
Copy Markdown
Contributor

🔍 Tooling Safety Check — Affects-Bootstrap, Affects-Build-Infra, Affects-Compiler-Output, Affects-Test-Tooling
Affects-Bootstrap: Compiler source changes alter bootstrap compiler behavior.
Affects-Build-Infra: FSharp.Core project changes alter build inputs.
Affects-Compiler-Output: Optimizer and codegen changes alter emitted IL.
Affects-Test-Tooling: Shared SurfaceArea utility changes verification behavior.

Generated by PR Tooling Safety Check · gpt56 966.4K ·

@majocha

majocha commented Sep 16, 2026

Copy link
Copy Markdown
Contributor Author

Handling the very edgy case of pinned locals is problematic:

// Library.dll
let inline comparePin (array: byte[]) ([<InlineIfLambda>] action) =
    use before = fixed array
    action ()
    use after = fixed array
    struct (NativePtr.toNativeInt before, NativePtr.toNativeInt after)

// Consumer.dll
__runtimeAsyncReturn (comparePin data (fun () -> AsyncHelpers.Await gate))

We could preserve the isPinning flag in metadata but that does not solve the problem fully. Older assemblies will not have this flag. What then? Reject inlining from older libraries altogether? A more general compiler warning when inlining into runtime-async method?

This is downstream from the design decision to not make the concept of inline runtime-async code fragment explicit. The implicit assumption is that any inline function may contain runtime suspensions (AsyncHelpers awaits).

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