Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
15fff46
docs
majocha Aug 8, 2026
0cc2108
feature
majocha Aug 8, 2026
719d00f
tests
majocha Aug 8, 2026
f9bb483
surface area
majocha Aug 8, 2026
dfe4109
rns
majocha Aug 8, 2026
b06dd3d
merge main
majocha Aug 8, 2026
e4eb267
translations
majocha Aug 8, 2026
38578d4
add pr numbers
majocha Aug 8, 2026
caaad4d
Restore F# 11.0 feature registrations dropped when adding RuntimeAsyn…
majocha Aug 8, 2026
48d1691
surface area
majocha Aug 8, 2026
eb32d05
Merge branch 'main' into runtime-async-intrinsic
majocha Aug 13, 2026
1961943
test: runtime-async edge-case suite (runtime behavior + emitted IL)
T-Gro Aug 13, 2026
31889ba
Make composed runtime-async exhibits self-prove the async impl flag
T-Gro Aug 13, 2026
d29dc42
Merge branch 'main' into runtime-async-intrinsic
majocha Aug 20, 2026
ca0ca40
Merge branch 'main' into runtime-async-intrinsic
majocha Aug 20, 2026
6f8cadc
restrict to net10, rename to __runtimeAsyncReturn
majocha Aug 20, 2026
d5bd674
ns surfacearea
majocha Aug 21, 2026
19117fe
Merge branch 'runtime-async-edge-tests' of https://github.com/T-Gro/f…
majocha Aug 21, 2026
5154fa9
adjust edge case tests to name change and net10 FSharp.Core
majocha Aug 21, 2026
b645d98
rewrite runtime async exception handling blocks during optimization
majocha Aug 21, 2026
d50c33c
fix surface area
majocha Aug 21, 2026
b9f9ef9
Merge branch 'main' into runtime-async-intrinsic
majocha Aug 21, 2026
27944e7
surf
majocha Aug 22, 2026
1d6f3b8
do not emit .tail in runtime async methods
majocha Aug 22, 2026
7738664
error on AsyncHelpers use outside of async method
majocha Aug 22, 2026
6f2338d
add non-preservable-value diagnostics
majocha Aug 22, 2026
eeeec60
make it work in debug
majocha Aug 22, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ Welcome to [the F# compiler and tools repository](https://github.com/dotnet/fsha
* [Memory usage](memory-usage.md)
* [Optimizations](optimizations.md)
* [Equality optimizations](optimizations-equality.md)
* [Runtime async](runtime-async.md)
* [Project builds](project-builds.md)
* [Tooling features](tooling-features.md)

Expand Down
1 change: 1 addition & 0 deletions docs/release-notes/.FSharp.Compiler.Service/11.0.100.md
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,7 @@
* Added warning FS3884 when a function or delegate value is used as an interpolated string argument. ([PR #19289](https://github.com/dotnet/fsharp/pull/19289))
* Symbols: add ObsoleteDiagnosticInfo ([PR #19359](https://github.com/dotnet/fsharp/pull/19359))
* FCS: add FSharpCheckFileResults.HasErrors ([PR #19892](https://github.com/dotnet/fsharp/pull/19892))
* Add preview compiler support for runtime-async `Task<'T>` methods through the `__runtimeAsyncReturn` intrinsic, including carrier validation, target-runtime capability checks, diagnostics for suspension calls outside runtime-async methods, diagnostics for byref, byref-like, or pinned values used after suspension, and recursive specialization of inline suspension fragments when optimization is disabled. ([PR #20235](https://github.com/dotnet/fsharp/pull/20235))
* Add `#version;;` directive to F# Interactive to display version and environment information. ([Issue #13307](https://github.com/dotnet/fsharp/issues/13307), [PR #19332](https://github.com/dotnet/fsharp/pull/19332))
* Add diagnostic FS3889 when a namespace and a type have the same fully-qualified name in the same assembly, replacing the misleading FS0247 "namespace and a module" error. ([Issue #17827](https://github.com/dotnet/fsharp/issues/17827), [PR #19802](https://github.com/dotnet/fsharp/pull/19802))
* Debug: rework for expressions stepping ([PR #19894](https://github.com/dotnet/fsharp/pull/19894))
Expand Down
1 change: 1 addition & 0 deletions docs/release-notes/.FSharp.Core/11.0.100.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

### Added

* Add the compiler-recognized `StateMachineHelpers.__runtimeAsyncReturn` intrinsic to the `net10.0` FSharp.Core target for .NET runtime-async methods. ([PR #20235](https://github.com/dotnet/fsharp/pull/20235))
* Add `Unchecked.withNull`, an interop escape hatch that re-types any `'T` to `'T | null` without the usual `not null`/`not struct` constraints, so unconstrained C# nullable-generic APIs (e.g. `T? M<T>()`) can be implemented and consumed from F#. ([Issue #17734](https://github.com/dotnet/fsharp/issues/17734), [PR #20232](https://github.com/dotnet/fsharp/pull/20232))
* Added generic `print` and `printn` functions (`'T -> unit`) to `ExtraTopLevelOperators` for simple value printing to stdout. ([RFC FS-1125](https://github.com/fsharp/fslang-design/blob/main/RFCs/FS-1125-print-printn-functions.md), [PR #19265](https://github.com/dotnet/fsharp/pull/19265))
* Ship `FSharp.Core` with an additional `net10.0` target framework (next to `netstandard2.0` and `netstandard2.1`). The `net`-TFM assembly is public-surface-identical to the `netstandard2.1` one; the target version is a pinned, deliberately advanced knob. ([PR #20229](https://github.com/dotnet/fsharp/pull/20229))
Expand Down
1 change: 1 addition & 0 deletions docs/release-notes/.Language/preview.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
### Added

* Runtime async: `task`/`async`-style computation expressions can be compiled to use the .NET runtime async support (RuntimeAsync preview feature). ([PR #20235](https://github.com/dotnet/fsharp/pull/20235))
* **Extension members for operators and SRTP constraints** ([RFC FS-1043](https://github.com/fsharp/fslang-design/blob/main/RFCs/FS-1043-extension-members-for-operators-and-srtp-constraints.md), [fslang-suggestions#230](https://github.com/fsharp/fslang-suggestions/issues/230), [PR #19602](https://github.com/dotnet/fsharp/pull/19602)): Extension methods now participate in SRTP constraint resolution. This allows defining operators on types you don't own via type extensions:

```fsharp
Expand Down
247 changes: 247 additions & 0 deletions docs/runtime-async.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,247 @@
---
title: Runtime async
category: Compiler Internals
categoryindex: 200
index: 375
---

# Runtime async

This document describes the current proof-of-concept implementation of F#
support for the .NET runtime-async feature. It describes the code as
implemented, not an aspirational design. The .NET design is still evolving:

* [Runtime-async specification](https://github.com/dotnet/runtime/blob/main/docs/design/specs/runtime-async.md)
* [Runtime-async code-generation contract](https://github.com/dotnet/runtime/blob/main/docs/design/coreclr/botr/runtime-async-codegen.md)
* [Roslyn runtime async design](https://github.com/dotnet/roslyn/blob/main/docs/compilers/CSharp/Runtime%20Async%20Design.md) —
how C# lowers `await` (including the exception-handling hoisting described below)

The implementation targets functions, lambdas, and members returning
`System.Threading.Tasks.Task<'T>`. A computation-expression builder exists in
the component tests and works for a subset of the surface, but is not part of
FSharp.Core.

## Runtime contract

Runtime-async methods are CIL methods marked with
`MethodImplOptions.Async` (`0x2000`). The runtime, rather than a compiler
generated state machine and method builder, owns suspension and resumption.

Only the generic return shape `System.Threading.Tasks.Task<'T>` is supported.
Non-generic `Task` and `ValueTask`/`ValueTask<'T>` returns are not.

Suspension is explicit, via `System.Runtime.CompilerServices.AsyncHelpers`:

* `Await` for `Task`, `ValueTask`, and configured awaitables
* `AwaitAwaiter` for awaiters (used by the test builder's SRTP `Bind`)

The compiler emits the adjacent IL sequence the runtime specification expects:

```il
call Task<int32> SomeAsyncMethod(...)
call int32 AsyncHelpers::Await<int32>(Task<int32>)
```

Known runtime restrictions (currently **not** diagnosed by the F# compiler):

* `tail.` and `localloc` are forbidden.
* generated suspension points cannot occur inside exception-handling regions.
Awaiting in a protected `try` body now works on the current runtime. Direct
intrinsic bodies rewrite suspending `catch`, filter, and `finally`
expressions so the suspension runs outside the EH region.

C# avoids this by rewriting EH-region awaits at lowering time (see the
Roslyn design doc): `try B finally { await x }` becomes
`try B catch-all { pend e }`, then `await x` outside the region, then
rethrow the pending exception. The test `RuntimeTaskBuilder.Using`
prototypes this pattern in F# source: it captures the body result/exception
in a `Choice`, runs `DisposeAsync` (possibly suspending) *outside* the
`try`, then restores a pending exception. This makes `use` on an
`IAsyncDisposable` work under runtime async (`testUsingAsyncDisposableSync`
executes).
Byref, byref-like, and pinned locals that are used after a suspension are
rejected with diagnostic FS3357.

Calls to `AsyncHelpers` suspension methods emitted outside a runtime-async
method are rejected during code generation. Explicitly `inline` method bodies
are treated as templates and checked at their eventual use site.

## F# surface

The source-level marker is the compiler intrinsic
`Microsoft.FSharp.Core.CompilerServices.StateMachineHelpers.__runtimeAsyncReturn`,
available from the `net10.0` FSharp.Core target,
declared in `resumable.fsi` alongside the other compiler intrinsics:

```fsharp
val __runtimeAsyncReturn<'T> : 'T -> System.Threading.Tasks.Task<'T>
```

Its FSharp.Core implementation throws; the compiler consumes every
occurrence before code generation, so the body is never executed. It is
marked `NoInlining` so a missed consumption does not silently fold into a
caller.

The feature is gated on `langversion:preview`
(`LanguageFeature.RuntimeAsync`) and on the target reference assemblies
exposing `MethodImplOptions.Async` (see "Runtime capability check" below).
Without the language version the checker reports error 3350; without runtime
support it reports 3351.

Typical forms:

```fsharp
let add (x: int) (y: int) : Task<int> =
__runtimeAsyncReturn (
let first = AsyncHelpers.Await (getTask x)
first + y)

type C() =
member _.Add(x: int, y: int) : Task<int> =
__runtimeAsyncReturn (
AsyncHelpers.Await (getTask x) + y)

// Let-bound value (not a function): also supported.
let answer : Task<int> = __runtimeAsyncReturn 42
```

There is no implicit awaiting: the argument of `__runtimeAsyncReturn` is checked
as the logical `'T` result, and flattening requires an explicit
`AsyncHelpers.Await`.

## Type checking

`__runtimeAsyncReturn` is an ordinary generic value in the typed tree; no new
expression node or `Val` flag is added. Type checking special-cases its
application in two places in `CheckExpressions.fs`:

* `Propagate` skips function-type propagation for the intrinsic so the
argument is not checked against a function domain.
* `TcApplicationThen` (`tryTcRuntimeAsyncApplication`) recognises the
intrinsic (possibly type-applied), gates the language feature and runtime
capability, extracts the result type `'T` from the intrinsic's own
instantiated signature `'T -> Task<'T>`, and checks the argument against
`'T` with `TcExprFlex2`. The result type of the application is `Task<'T>`,
which unifies with the declared return type of the enclosing binding in
the usual way. A non-`Task<'T>` declared return type therefore fails with
the ordinary FS0001 type-mismatch error.

User code that defines its own `__runtimeAsyncReturn` is unaffected: the intrinsic
is only recognised when the `ValRef` resolves (via `valRefEq`) to the
FSharp.Core declaration.

## Optimization

`Optimizer.fs` preserves the marker application as-is, optimizing its
argument and rewriting any suspending exception handlers in that argument.
The marked expression is forced to `HasEffect = true` and `UnknownValue`, so
the optimizer never inlines, duplicates, or discards it. The marker therefore
survives optimization as an ordinary `Expr.App` node; nothing else in the
typed tree records that a method is runtime-async.

Inline values whose bodies contain the marker or an `AsyncHelpers` suspension
are recursively specialized at their call sites, including when optimization
is disabled. The optimizer follows nested inline calls and does not create a
generated helper method for the specialized suspension fragment, keeping every
suspension in the eventual runtime-async method.

## Code generation

`IlxGen.fs` recognises the marker in three placements
(`TryUnwrapRuntimeAsyncReturnExpr`, which strips `DebugPoint` wrappers):

1. **Method body** (`GenMethodForBinding`): the marker is unwrapped from the
top of the method lambda body; the generated `ILMethodDef` gets
`.WithAsync(true)`, which sets impl attribute bit `0x2000`
(`MethodImplOptions.Async`, written as a literal because older reference
assemblies do not define the enum member). `NoInlining` is forced on the
method.
2. **Closure body** (`GenClosureAsLocalTypeFunction` and
`GenClosureAsFirstClassFunction`): the same unwrapping marks the closure
`Invoke` method's IL body (`ILMethodBody.IsRuntimeAsync`).
`EraseClosures.convIlxClosureDef` copies that flag onto the emitted
method, again with `NoInlining`.
3. **Any other expression position** (`GenRuntimeAsyncReturnAsStartedTask`), e.g.
a `let`-bound value initializer: the marker application is wrapped in a
fresh `fun () -> ...` lambda that is immediately applied to `unit` and
regenerated. The lambda flows through the closure path (2), producing a
generated runtime-async helper method whose call starts the task. This
relies on `GenApp` never beta-reducing a lambda application (it always
emits a closure plus an indirect call); see the comment at
`GenRuntimeAsyncReturnAsStartedTask`.

A marker that ends up wrapped in anything other than `DebugPoint` at the top
of a method or closure body is not detected there, but still reaches the
catch-all case (3), so compilation stays correct — the cost is an extra
nested runtime-async helper method rather than marking the enclosing method
directly.

## Runtime capability check

`InfoReader` gates `LanguageFeature.RuntimeAsync` on the target reference
assemblies: it looks up the `Async` field on
`System.Runtime.CompilerServices.MethodImplOptions`. This is a metadata-only
probe of the *reference* assemblies; it does not prove the *executing* host
JIT supports runtime-async. Compiling against new reference assemblies and
running on an older runtime is not a supported configuration.

## Test infrastructure

Tests live in `tests/FSharp.Compiler.ComponentTests/Language/RuntimeAsync*`:

* The component test project sets `<Features>runtime-async=on</Features>`
(the .NET runtime opt-in), as does the project template in
`FSharp.Test.Utilities` used by `compileExeAndRun`.
* Type-check tests assert the preview gate (3350) and the unsupported-runtime
gate (3351, on non-.NET-Core targets).
* IL tests verify direct `AsyncHelpers.Await` calls appear without
intervening delegates.
* Execution tests (`RuntimeAsyncBasic.fs`, `RuntimeTasks.fs` with the shared
`RuntimeTaskBuilder.fs`) run with `compileExeAndRun`, so they compile with
the compiler under test and execute on the host runtime.
* `RuntimeTasksAsyncDisposalException.fs` documents the known
EH-region-suspension crash: it is compiled but not executed.

### Test builder

`RuntimeTaskBuilder.fs` is a quasi-synchronous builder aiming for feature
parity with FSharp.Core's `task` builder: `Delay` is the identity on
`unit -> 'T`, so all combinators are plain inline functions over delayed
code; only `Run` introduces `__runtimeAsyncReturn` and returns `Task<'T>`.
`Bind` lowers directly to `AsyncHelpers.Await` with SRTP fallbacks
(`AwaitAwaiter`) for arbitrary task-likes, as do `ReturnFrom` and
`MergeSources`. `MergeSources` awaits its sources sequentially, matching the
task builder — concurrency comes from the sources being hot tasks.
`Async<'T>` binds via `Async.StartImmediateAsTask`, matching `task {}`'s
current-thread semantics.

`RuntimeTasks.fs` ports the TaskBuilder test suite
(`tests/FSharp.Core.UnitTests/.../Tasks.fs`) test-for-test with
`task {` replaced by `runtimeTask {`. Tests that hit the known runtime-async
restrictions or divergences are kept in the file with `knownFailing_` /
`knownDivergent_` prefixes, compiled but not run:

* suspension in `try/finally`, or in `try/with` in non-tail position
(forbidden by the runtime contract; crashes with `0xC0000409` or loses the
finally);
* `use`/`use!` whose disposal awaits an `IAsyncDisposable` (the `Using`
compensation suspends in a `finally`);
* tests requiring synchronous (hot) start of the body before the first
suspension — on the current runtime build the body is not observably run
before the returned `Task` is awaited;
* `SynchronizationContext` capture: with a sync context installed, the task
completes without the body observably running.

Two `task {}` inference behaviors are not matched by the overload set:
element-type propagation through `Bind` without an annotation, and unannotated
`return! failwith ...` (both need explicit annotations in the port).

## Not yet implemented

* Diagnostics for suspension in exception-handling regions, `tail.`, and
`localloc`.
* Non-generic `Task` and `ValueTask`/`ValueTask<'T>` return shapes.
* Any FSharp.Core builder (the test builder is test-only).
* Compile-time enforcement that the marker was actually consumed before
code generation (a missed marker throws only when its FSharp.Core stub is
reached at run time, or produces invalid IL as described above).
7 changes: 7 additions & 0 deletions src/Compiler/AbstractIL/il.fs
Original file line number Diff line number Diff line change
Expand Up @@ -1634,6 +1634,7 @@ type ILMethodBody =
MaxStack: int32
NoInlining: bool
AggressiveInlining: bool
IsRuntimeAsync: bool
Locals: ILLocals
Code: ILCode
DebugRange: ILDebugPoint option
Expand Down Expand Up @@ -2274,6 +2275,11 @@ type ILMethodDef
member x.WithRuntime(condition) =
x.With(implAttributes = (x.ImplAttributes |> conditionalAdd condition MethodImplAttributes.Runtime))

member x.WithAsync(condition) =
// MethodImplOptions.Async is not present in all target reference assemblies.
let asyncFlag = enum<MethodImplAttributes> 0x2000
x.With(implAttributes = (x.ImplAttributes |> conditionalAdd condition asyncFlag))

[<DebuggerBrowsable(DebuggerBrowsableState.Never)>]
member x.DebugText = x.ToString()

Expand Down Expand Up @@ -4283,6 +4289,7 @@ let mkILMethodBody (initlocals, locals, maxstack, code, tag, imports) : ILMethod
MaxStack = maxstack
NoInlining = false
AggressiveInlining = false
IsRuntimeAsync = false
Locals = locals
Code = code
DebugRange = tag
Expand Down
3 changes: 3 additions & 0 deletions src/Compiler/AbstractIL/il.fsi
Original file line number Diff line number Diff line change
Expand Up @@ -813,6 +813,7 @@ type internal ILMethodBody =
MaxStack: int32
NoInlining: bool
AggressiveInlining: bool
IsRuntimeAsync: bool
Locals: ILLocals
Code: ILCode
DebugRange: ILDebugPoint option
Expand Down Expand Up @@ -1247,6 +1248,8 @@ type ILMethodDef =

member internal WithRuntime: bool -> ILMethodDef

member internal WithAsync: bool -> ILMethodDef

/// Tables of methods. Logically equivalent to a list of methods but
/// the table is kept in a form optimized for looking up methods by
/// name and arity.
Expand Down
2 changes: 2 additions & 0 deletions src/Compiler/AbstractIL/ilread.fs
Original file line number Diff line number Diff line change
Expand Up @@ -3820,6 +3820,7 @@ and seekReadMethodRVA (pectxt: PEReader) (ctxt: ILMetadataReader) (nm, noinline,
MaxStack = 8
NoInlining = noinline
AggressiveInlining = aggressiveinline
IsRuntimeAsync = false
Locals = List.empty
Code = code
DebugRange = None
Expand Down Expand Up @@ -3968,6 +3969,7 @@ and seekReadMethodRVA (pectxt: PEReader) (ctxt: ILMetadataReader) (nm, noinline,
MaxStack = maxstack
NoInlining = noinline
AggressiveInlining = aggressiveinline
IsRuntimeAsync = false
Locals = locals
Code = code
DebugRange = None
Expand Down
Loading
Loading