Skip to content

Runtime async via intrinsic - #20235

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

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

Conversation

@majocha

@majocha majocha 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

__runtimeAsyncReturn: '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. Wrapping the code in its Run method in __runtimeAsyncReturn will compile the whole CE into a single runtime async method:

member inline _.Run([<InlineIfLambda>] code) = __runtimeAsyncReturn(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

runtime spec :
Runtime-async specification

interesting docs on C# implementation

To do:

  • 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, test YieldFromFInal behavior
  • update ildasm - separate PR, because of bulk baseline changes

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

@majocha
majocha force-pushed the runtime-async-intrinsic branch from 55e8c16 to a88067d Compare August 24, 2026 10:08
Comment thread src/FSharp.Core/resumable.fs
Comment thread src/Compiler/Checking/Expressions/CheckExpressions.fs
Comment thread src/Compiler/CodeGen/IlxGen.fs Outdated
@dotnet dotnet deleted a comment from majocha Aug 25, 2026
@majocha

majocha commented Aug 26, 2026

Copy link
Copy Markdown
Contributor Author

Surprisingly, debug stepping across AsyncHelpers.Await more or less works out of the box:
image
image
at least with net11.0

However, it works like this only in simplest scenarios. Add some task CE in the mix and the call stack becomes mostly empty.

Stepping through runtimeTask CEs is possible, but misbehaves a lot, call stacks are non-existent, too.

Sign up for free to 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