Skip to content

Runtime async via intrinsic - #20235

Draft
majocha wants to merge 28 commits into
dotnet:mainfrom
majocha:runtime-async-intrinsic
Draft

Runtime async via intrinsic #20235
majocha wants to merge 28 commits into
dotnet:mainfrom
majocha:runtime-async-intrinsic

Conversation

@majocha

@majochamajocha commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

Yet another proof of concept (see also #19449)

The experiment here is to explore viability of working CE builders with just a minimal compiler support.

consider

__runtimeAsync: 't -> Task<'t>

intrinsic, which when used as a member or function body, promotes it to managed async.

We can have a fully inlining builder which wraps the code in it's Run method:

member inline_.Run([<InlineIfLambda>]code)=__runtimeAsync (code())

Because resumption is handled by the runtime, the builder is effectively just a sync builder with

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

There are more concerns than suspension / resumption that the runtime apparently does not handle and they would have to be covered either by the compiler or the builder implementations:

  • awaits in EH blocks (IAsyncDisposable) - this has a PoC in the tests here.

  • byref locals and execution context / sync context, see C# docs below.

runtime spec :
Runtime-async specification

interesting docs on C# implementation

@github-actions

github-actionsBot 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 pathRelease notes pathDescription
`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

majochaand 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 threadsrc/FSharp.Core/resumable.fs Outdated
"__stateMachine should always be guarded by __useResumableCode and only used in valid state machine implementations"

[<MethodImpl(MethodImplOptions.NoInlining)>]
let __runtimeAsync<'T> (value: 'T) : Task<'T> =

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.

Why a fail and not a retype?

I also wonder about the "older compiler, newer Fsharp.Core" consequences here.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Retype as in different nominal type than actual resulting type? The idea here is to have it typecheck correctly with no extra compiler work. By the spec we need the resulting method to actually return 't, but have a Task<'t> nominal return type, so this covers it. During typecheck we got Task<'t> just from the signature here. Optimizer erases __runtimeAsync and we emit async method which returns 't just like the runtime expects.

As for FSharp.Core / compiler mismatch, this is, kind of like __stateMachine, an "expert" feature for authoring CE builders or low level runtime async methods. The fail with unsupported compiler is same behavior as __stateMachine.

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.

Got it, this is just for the final return/Run.

Maybe the name could indicate it?
Like __return_from_runtimeAsync ?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Yes, that's the intent . more ideas: __asyncReturn, __runtimeAsyncReturn

@T-GroT-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.

🕵️🤖 A couple more notes without inline anchors:

  • Non-generic Task/ValueTask returns are silently unsupported — worth a diagnostic so it errors instead of miscompiling (docs L31).
  • ILVerify can't verify async-returning methods yet, so this codegen stays unverified in CI — known gap.

Backed by a small runtime-async edge-case suite (runtime behavior + emitted IL) driven through a test-only runtimeTask CE.

Comment threadsrc/Compiler/CodeGen/IlxGen.fs
Comment threaddocs/runtime-async.md Outdated
Comment threaddocs/runtime-async.md Outdated
@majocha

Copy link
Copy Markdown
ContributorAuthor

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

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

Copy link
Copy Markdown
Member

🕵️🤖 Parked a runtime-async edge-case suite on T-Gro/fsharp:runtime-async-edge-tests (rebased on your current head — top two commits, test-only) if you want to pick it up: runtime behavior + full-body / sequence-point IL through a test-only runtimeTask CE, pinning the four still-undiagnosed gaps (.tail after Await, await in finally/catch, byref/ref-struct across a suspension, non-generic Task/ValueTask) as regression tests. No rush — waits on this landing and the __runtimeAsyncReturn naming settling.

T-Groand 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

Copy link
Copy Markdown
ContributorAuthor

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

Copy link
Copy Markdown
ContributorAuthor

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.

@majocha

Copy link
Copy Markdown
ContributorAuthor

🕵️🤖 Parked a runtime-async edge-case suite on T-Gro/fsharp:runtime-async-edge-tests (rebased on your current head — top two commits, test-only) if you want to pick it up: runtime behavior + full-body / sequence-point IL through a test-only runtimeTask CE, pinning the four still-undiagnosed gaps (.tail after Await, await in finally/catch, byref/ref-struct across a suspension, non-generic Task/ValueTask) as regression tests.

I pulled them in and adjusted to withFSharpCoreShippedNet.

@majocha

Copy link
Copy Markdown
ContributorAuthor

VerifySurfaceAreaFSharpCore still runs against ns21 baseline, I think.

@majocha

Copy link
Copy Markdown
ContributorAuthor

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.

The downside is the errors surface only during build, not in the IDE, just like with resumable state machines.

@majocha

Copy link
Copy Markdown
ContributorAuthor

Regarding suspension in exception handling blocks, it's not hard to do a rewrite during optimization, just like C# compiler does.
If we are to detect and error on suspensions in EH, we might as well just rewrite.

@majocha
majochaforce-pushed the runtime-async-intrinsic branch from 04e7fb3 to 27944e7CompareAugust 22, 2026 08:26
@majocha

Copy link
Copy Markdown
ContributorAuthor

This needs to work in debug mode, too. Now that's going to be fun.

@majocha
majochaforce-pushed the runtime-async-intrinsic branch from eeeec60 to 25e87baCompareAugust 23, 2026 10:53
@majocha
majochaforce-pushed the runtime-async-intrinsic branch from 585df73 to 34060f9CompareAugust 23, 2026 18:13
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: New

Development

Successfully merging this pull request may close these issues.

2 participants

@majocha@T-Gro