From 15fff461626fbc0046ca7ab0e4a800a6fbff35e3 Mon Sep 17 00:00:00 2001 From: majocha <1760221+majocha@users.noreply.github.com> Date: Sat, 8 Aug 2026 08:34:44 +0200 Subject: [PATCH 1/9] docs --- docs/index.md | 1 + docs/runtime-async.md | 235 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 236 insertions(+) create mode 100644 docs/runtime-async.md diff --git a/docs/index.md b/docs/index.md index 7eca2572ae2..dd84774305d 100644 --- a/docs/index.md +++ b/docs/index.md @@ -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) diff --git a/docs/runtime-async.md b/docs/runtime-async.md new file mode 100644 index 00000000000..90d22b803de --- /dev/null +++ b/docs/runtime-async.md @@ -0,0 +1,235 @@ +--- +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 SomeAsyncMethod(...) +call int32 AsyncHelpers::Await(Task) +``` + +Known runtime restrictions (currently **not** diagnosed by the F# compiler): + +* `tail.` and `localloc` are forbidden. +* suspension cannot occur inside exception-handling regions. Awaiting in a + `try` body now works on the current runtime; awaiting inside a `finally` + handler compiles and then terminates the process at execution + (`0xC0000409`). See `RuntimeTasksAsyncDisposalException.fs`, which is + compile-only for this reason. + + 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 cannot be preserved across suspension. + +## F# surface + +The source-level marker is the compiler intrinsic +`Microsoft.FSharp.Core.CompilerServices.StateMachineHelpers.__runtimeAsync`, +declared in `resumable.fsi` alongside the other compiler intrinsics: + +```fsharp +val __runtimeAsync<'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 = + __runtimeAsync ( + let first = AsyncHelpers.Await (getTask x) + first + y) + +type C() = + member _.Add(x: int, y: int) : Task = + __runtimeAsync ( + AsyncHelpers.Await (getTask x) + y) + +// Let-bound value (not a function): also supported. +let answer : Task = __runtimeAsync 42 +``` + +There is no implicit awaiting: the argument of `__runtimeAsync` is checked +as the logical `'T` result, and flattening requires an explicit +`AsyncHelpers.Await`. + +## Type checking + +`__runtimeAsync` 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 `__runtimeAsync` 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 only its +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. + +## Code generation + +`IlxGen.fs` recognises the marker in three placements +(`TryUnwrapRuntimeAsyncExpr`, 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** (`GenRuntimeAsyncAsStartedTask`), 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 + `GenRuntimeAsyncAsStartedTask`. + +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 `runtime-async=on` + (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 `__runtimeAsync` 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, byref/byref-like + or pinned locals across suspension, `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). From 0cc21082d2cf901048ea868ad8d67059e194a60b Mon Sep 17 00:00:00 2001 From: majocha <1760221+majocha@users.noreply.github.com> Date: Sat, 8 Aug 2026 08:40:52 +0200 Subject: [PATCH 2/9] feature --- src/Compiler/AbstractIL/il.fs | 7 ++ src/Compiler/AbstractIL/il.fsi | 3 + src/Compiler/AbstractIL/ilread.fs | 2 + .../Checking/Expressions/CheckExpressions.fs | 69 +++++++++++++++++-- src/Compiler/Checking/InfoReader.fs | 10 +++ src/Compiler/CodeGen/EraseClosures.fs | 1 + src/Compiler/CodeGen/IlxGen.fs | 53 +++++++++++++- src/Compiler/FSComp.txt | 1 + src/Compiler/Facilities/LanguageFeatures.fs | 3 + src/Compiler/Facilities/LanguageFeatures.fsi | 1 + src/Compiler/Optimize/Optimizer.fs | 11 ++- src/Compiler/TypedTree/TcGlobals.fs | 5 ++ src/Compiler/TypedTree/TcGlobals.fsi | 4 ++ src/FSharp.Core/resumable.fs | 7 ++ src/FSharp.Core/resumable.fsi | 5 ++ 15 files changed, 173 insertions(+), 9 deletions(-) diff --git a/src/Compiler/AbstractIL/il.fs b/src/Compiler/AbstractIL/il.fs index e2002731aa8..cf143ad80a1 100644 --- a/src/Compiler/AbstractIL/il.fs +++ b/src/Compiler/AbstractIL/il.fs @@ -1585,6 +1585,7 @@ type ILMethodBody = MaxStack: int32 NoInlining: bool AggressiveInlining: bool + IsRuntimeAsync: bool Locals: ILLocals Code: ILCode DebugRange: ILDebugPoint option @@ -2225,6 +2226,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 0x2000 + x.With(implAttributes = (x.ImplAttributes |> conditionalAdd condition asyncFlag)) + [] member x.DebugText = x.ToString() @@ -3923,6 +3929,7 @@ let mkILMethodBody (initlocals, locals, maxstack, code, tag, imports) : ILMethod MaxStack = maxstack NoInlining = false AggressiveInlining = false + IsRuntimeAsync = false Locals = locals Code = code DebugRange = tag diff --git a/src/Compiler/AbstractIL/il.fsi b/src/Compiler/AbstractIL/il.fsi index 050921650c3..ce32a48563e 100644 --- a/src/Compiler/AbstractIL/il.fsi +++ b/src/Compiler/AbstractIL/il.fsi @@ -808,6 +808,7 @@ type internal ILMethodBody = MaxStack: int32 NoInlining: bool AggressiveInlining: bool + IsRuntimeAsync: bool Locals: ILLocals Code: ILCode DebugRange: ILDebugPoint option @@ -1241,6 +1242,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. diff --git a/src/Compiler/AbstractIL/ilread.fs b/src/Compiler/AbstractIL/ilread.fs index 09fc311367a..7be919293e6 100644 --- a/src/Compiler/AbstractIL/ilread.fs +++ b/src/Compiler/AbstractIL/ilread.fs @@ -3819,6 +3819,7 @@ and seekReadMethodRVA (pectxt: PEReader) (ctxt: ILMetadataReader) (nm, noinline, MaxStack = 8 NoInlining = noinline AggressiveInlining = aggressiveinline + IsRuntimeAsync = false Locals = List.empty Code = code DebugRange = None @@ -3967,6 +3968,7 @@ and seekReadMethodRVA (pectxt: PEReader) (ctxt: ILMetadataReader) (nm, noinline, MaxStack = maxstack NoInlining = noinline AggressiveInlining = aggressiveinline + IsRuntimeAsync = false Locals = locals Code = code DebugRange = None diff --git a/src/Compiler/Checking/Expressions/CheckExpressions.fs b/src/Compiler/Checking/Expressions/CheckExpressions.fs index e4b3e755841..687ee49daf6 100644 --- a/src/Compiler/Checking/Expressions/CheckExpressions.fs +++ b/src/Compiler/Checking/Expressions/CheckExpressions.fs @@ -8687,8 +8687,18 @@ and Propagate (cenv: cenv) (overallTy: OverallTy) (env: TcEnv) tpenv (expr: Appl | DelayedApp (atomicFlag, isSugar, synLeftExprOpt, synArg, mExprAndArg) :: delayedList' -> let denv = env.DisplayEnv - match UnifyFunctionTypeUndoIfFailed cenv denv mExpr exprTy with - | ValueSome (_, resultTy) -> + + let isRuntimeAsync = + match expr.Expr with + | Expr.Val(vref, _, _) + | Expr.App(Expr.Val(vref, _, _), _, [ _ ], [], _) + when valRefEq g vref g.cgh__runtimeAsync_vref -> true + | _ -> false + + match isRuntimeAsync, UnifyFunctionTypeUndoIfFailed cenv denv mExpr exprTy with + | true, _ -> + () + | false, ValueSome (_, resultTy) -> // We add tag parameter to the return type for "&x" and 'NativePtr.toByRef' // See RFC FS-1053.md @@ -8701,7 +8711,7 @@ and Propagate (cenv: cenv) (overallTy: OverallTy) (env: TcEnv) tpenv (expr: Appl propagate isAddrOf delayedList' mExprAndArg resultTy - | _ -> + | false, _ -> let mArg = synArg.Range match synArg with // async { ... } @@ -8997,10 +9007,57 @@ and TcApplicationThen (cenv: cenv) (overallTy: OverallTy) env tpenv mExprAndArg else None + let tryTcRuntimeAsyncApplication () = + let intrinsic = + match leftExpr with + | ApplicableExpr(expr=Expr.Val (vref, flags, m)) + when valRefEq g vref g.cgh__runtimeAsync_vref -> + Some(vref, flags, m) + | ApplicableExpr(expr=Expr.App (Expr.Val (vref, flags, m), _, [ _ ], [], _)) + when valRefEq g vref g.cgh__runtimeAsync_vref -> + Some(vref, flags, m) + | _ -> + None + + match intrinsic with + | None -> + None + | Some(vref, flags, m) -> + checkLanguageFeatureAndRecover g.langVersion LanguageFeature.RuntimeAsync m + + let _, carrierTy = stripFunTy g exprTy + + // The intrinsic's signature is 'T -> Task<'T>, so the carrier is always Task<'T>. + let bodyResultTy = + match stripTyEqns g carrierTy with + | AppTy g (_, [ resultTy ]) -> resultTy + | _ -> NewInferenceType g + + checkLanguageFeatureRuntimeAndRecover cenv.infoReader LanguageFeature.RuntimeAsync m + + let arg, tpenv = TcExprFlex2 cenv bodyResultTy env false tpenv synArg + let marker = + Expr.App(Expr.Val(vref, flags, m), vref.Type, [ bodyResultTy ], [ arg ], mExprAndArg) + + Some( + TcDelayed + cenv + overallTy + env + tpenv + mExprAndArg + (MakeApplicableExprNoFlex cenv marker) + carrierTy + atomicFlag + delayed + ) + // If the type of 'synArg' unifies as a function type, then this is a function application, otherwise // it is an error or a computation expression or indexer or delegate invoke - match UnifyFunctionTypeUndoIfFailed cenv denv mLeftExpr exprTy with - | ValueSome (domainTy, resultTy) -> + match tryTcRuntimeAsyncApplication (), UnifyFunctionTypeUndoIfFailed cenv denv mLeftExpr exprTy with + | Some result, _ -> + result + | None, ValueSome (domainTy, resultTy) -> // atomicLeftExpr[idx] unifying as application gives a warning if not isSugar then @@ -9066,7 +9123,7 @@ and TcApplicationThen (cenv: cenv) (overallTy: OverallTy) env tpenv mExprAndArg let exprAndArg, resultTy = buildApp cenv leftExpr resultTy arg mExprAndArg TcDelayed cenv overallTy env tpenv mExprAndArg exprAndArg resultTy atomicFlag delayed - | ValueNone -> + | None, ValueNone -> // Type-directed invocables match synArg with diff --git a/src/Compiler/Checking/InfoReader.fs b/src/Compiler/Checking/InfoReader.fs index 2a4e75135f1..57a4847f213 100644 --- a/src/Compiler/Checking/InfoReader.fs +++ b/src/Compiler/Checking/InfoReader.fs @@ -860,6 +860,15 @@ type InfoReader(g: TcGlobals, amap: ImportMap) as this = let isRuntimeFeatureVirtualStaticsInInterfacesSupported = lazy isRuntimeFeatureSupported "VirtualStaticsInInterfaces" + let isRuntimeAsyncSupported = + lazy ( + match g.System_Runtime_CompilerServices_MethodImplOptions_ty with + | Some methodImplOptionsTy -> + GetIntrinsicILFieldInfosUncached ((None, AccessorDomain.AccessibleFromEverywhere), range0, methodImplOptionsTy) + |> List.exists (fun (ilFieldInfo: ILFieldInfo) -> ilFieldInfo.FieldName = "Async") + | _ -> + false) + member _.g = g member _.amap = amap @@ -924,6 +933,7 @@ type InfoReader(g: TcGlobals, amap: ImportMap) as this = // Both default and static interface method consumption features are tied to the runtime support of DIMs. | LanguageFeature.DefaultInterfaceMemberConsumption -> isRuntimeFeatureDefaultImplementationsOfInterfacesSupported.Value | LanguageFeature.InterfacesWithAbstractStaticMembers -> isRuntimeFeatureVirtualStaticsInInterfacesSupported.Value + | LanguageFeature.RuntimeAsync -> isRuntimeAsyncSupported.Value | _ -> true /// Get the declared constructors of any F# type diff --git a/src/Compiler/CodeGen/EraseClosures.fs b/src/Compiler/CodeGen/EraseClosures.fs index 9aad82b0521..77fee73c948 100644 --- a/src/Compiler/CodeGen/EraseClosures.fs +++ b/src/Compiler/CodeGen/EraseClosures.fs @@ -722,6 +722,7 @@ let rec convIlxClosureDef cenv encl (td: ILTypeDef) clo = mkILReturn fixedNowReturnTy, MethodBody.IL(notlazy convil) ) + |> fun mdef -> mdef.WithAsync(clo.cloCode.Value.IsRuntimeAsync).WithNoInlining(clo.cloCode.Value.IsRuntimeAsync) let ctorMethodDef = mkILStorageCtor ( diff --git a/src/Compiler/CodeGen/IlxGen.fs b/src/Compiler/CodeGen/IlxGen.fs index c4fbea22a66..44f133736e1 100644 --- a/src/Compiler/CodeGen/IlxGen.fs +++ b/src/Compiler/CodeGen/IlxGen.fs @@ -3127,6 +3127,19 @@ let ComputeDebugPointForBinding g bind = | _, (Expr.Lambda _ | Expr.TyLambda _) -> false, None | DebugPointAtBinding.Yes m, _ -> false, Some m +let IsRuntimeAsyncVref (g: TcGlobals) (vref: ValRef) = + valRefEq g vref g.cgh__runtimeAsync_vref + +let rec TryUnwrapRuntimeAsyncExpr (g: TcGlobals) expr = + + match expr with + | Expr.DebugPoint(_, innerExpr) -> + match TryUnwrapRuntimeAsyncExpr g innerExpr with + | true, body -> true, body + | false, _ -> false, expr + | Expr.App(Expr.Val(vref, _, _), _, [ _ ], [ body ], _) when IsRuntimeAsyncVref g vref -> true, body + | _ -> false, expr + //------------------------------------------------------------------------- // Generate expressions //------------------------------------------------------------------------- @@ -3271,6 +3284,9 @@ and GenExprAux (cenv: cenv) (cgbuf: CodeGenBuffer) eenv expr (sequel: sequel) = // 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(Expr.Val(vref, _, _), _, [ _ ], [ _ ], _) when IsRuntimeAsyncVref g vref -> + GenRuntimeAsyncAsStartedTask cenv cgbuf eenv expr sequel + | Expr.App(f, fty, tyargs, curriedArgs, m) -> GenApp cenv cgbuf eenv (f, fty, tyargs, curriedArgs, m) sequel | Expr.Val(v, _, m) -> GenGetVal cenv cgbuf eenv (v, m) sequel @@ -3372,6 +3388,21 @@ and GenExprAux (cenv: cenv) (cgbuf: CodeGenBuffer) eenv expr (sequel: sequel) = | Expr.TyChoose(_, _, m) -> error (InternalError("Unexpected Expr.TyChoose", m)) +// A __runtimeAsync marker that is not at the top of a method or closure body is lowered +// as a "started task": the marked expression becomes the body of a fresh closure whose +// Invoke method is the runtime-async method, and the closure is invoked immediately. +// This relies on GenApp never beta-reducing a lambda application - it always emits a +// closure value followed by an indirect call (the "worst case" path), which routes the +// lambda through the closure generation that consumes the marker. If that invariant ever +// changes, the marker expression would reach GenExprAux again and recurse without bound. +and GenRuntimeAsyncAsStartedTask cenv cgbuf eenv expr sequel = + let m = expr.Range + let unitVal, _ = mkLocal m "unit" cenv.g.unit_ty + let lambdaExpr = mkLambda m unitVal (expr, tyOfExpr cenv.g expr) + let lambdaTy = tyOfExpr cenv.g lambdaExpr + let application = mkApps cenv.g ((lambdaExpr, lambdaTy), [], [ mkUnit cenv.g m ], m) + GenExpr cenv cgbuf eenv application sequel + and GenExprs cenv cgbuf eenv es = List.iter (fun e -> GenExpr cenv cgbuf eenv e Continue) es @@ -7102,9 +7133,17 @@ and GenClosureAsLocalTypeFunction cenv (cgbuf: CodeGenBuffer) eenv thisVars expr strip cloinfo.ilCloLambdas + let isRuntimeAsync, body = TryUnwrapRuntimeAsyncExpr g body + let ilCloBody = CodeGenMethodForExpr cenv cgbuf.mgbuf (entryPointInfo, cloinfo.cloName, eenvinner, 1, None, body, Return) + let ilCloBody = + if isRuntimeAsync then + { ilCloBody with IsRuntimeAsync = true } + else + ilCloBody + let ilCtorBody = mkILMethodBody (true, [], 8, nonBranchingInstrsToCode (mkCallBaseConstructor (g.ilg.typ_Object, [])), None, eenv.imports) @@ -7119,6 +7158,7 @@ and GenClosureAsLocalTypeFunction cenv (cgbuf: CodeGenBuffer) eenv thisVars expr mkILReturn ilCloFormalReturnTy, MethodBody.IL(InterruptibleLazy.FromValue ilCloBody) ) + |> fun mdef -> mdef.WithAsync(isRuntimeAsync).WithNoInlining(isRuntimeAsync) ] let cloTypeDefs = @@ -7149,9 +7189,17 @@ and GenClosureAsFirstClassFunction cenv (cgbuf: CodeGenBuffer) eenv thisVars m e let ilCloTypeRef = cloinfo.cloSpec.TypeRef + let isRuntimeAsync, body = TryUnwrapRuntimeAsyncExpr g body + let ilCloBody = CodeGenMethodForExpr cenv cgbuf.mgbuf (entryPointInfo, cloinfo.cloName, eenvinner, 1, None, body, Return) + let ilCloBody = + if isRuntimeAsync then + { ilCloBody with IsRuntimeAsync = true } + else + ilCloBody + let cloTypeDefs = GenClosureTypeDefs cenv @@ -9797,6 +9845,8 @@ and GenMethodForBinding | h :: t -> [ h ], t, true | _ -> [], methLambdaVars, false + let isRuntimeAsync, methLambdaBody = TryUnwrapRuntimeAsyncExpr g methLambdaBody + let nonUnitNonSelfMethodVars, body = BindUnitVars cenv.g (nonSelfMethodVars, paramInfos, methLambdaBody) @@ -10229,8 +10279,9 @@ and GenMethodForBinding .WithPInvoke(hasDllImport) .WithPreserveSig(hasPreserveSigImplFlag || hasPreserveSigNamedArg) .WithSynchronized(hasSynchronizedImplFlag) - .WithNoInlining(hasNoInliningFlag) .WithAggressiveInlining(hasAggressiveInliningImplFlag) + .WithAsync(isRuntimeAsync) + .WithNoInlining(hasNoInliningFlag || isRuntimeAsync) .With(isEntryPoint = isExplicitEntryPoint, securityDecls = secDecls) let mdef = diff --git a/src/Compiler/FSComp.txt b/src/Compiler/FSComp.txt index 26699fa4d9b..83bc970001a 100644 --- a/src/Compiler/FSComp.txt +++ b/src/Compiler/FSComp.txt @@ -1576,6 +1576,7 @@ featureFixedIndexSlice3d4d,"fixed-index slice 3d/4d" featureAndBang,"applicative computation expressions" featureNullnessChecking,"nullness checking" featureResumableStateMachines,"resumable state machines" +featureRuntimeAsync,"runtime async" featureNullableOptionalInterop,"nullable optional interop" featureDefaultInterfaceMemberConsumption,"default interface member consumption" featureStringInterpolation,"string interpolation" diff --git a/src/Compiler/Facilities/LanguageFeatures.fs b/src/Compiler/Facilities/LanguageFeatures.fs index c7b75365c60..699f127e95e 100644 --- a/src/Compiler/Facilities/LanguageFeatures.fs +++ b/src/Compiler/Facilities/LanguageFeatures.fs @@ -29,6 +29,7 @@ type LanguageFeature = | FixedIndexSlice3d4d | AndBang | ResumableStateMachines + | RuntimeAsync | NullableOptionalInterop | DefaultInterfaceMemberConsumption | WitnessPassing @@ -257,6 +258,7 @@ type LanguageVersion(versionText, ?disabledFeaturesArray: LanguageFeature array) LanguageFeature.ExceptionFieldSerializationSupport, languageVersion110 LanguageFeature.NotNullIfNotNull, languageVersion110 LanguageFeature.ImprovedImpliedArgumentNamesPartTwo, languageVersion110 + LanguageFeature.RuntimeAsync, previewVersion // Difference between languageVersion110 and preview - 11.0 gets turned on automatically by picking a preview .NET 11 SDK // previewVersion is only when "preview" is specified explicitly in project files and users also need a preview SDK @@ -377,6 +379,7 @@ type LanguageVersion(versionText, ?disabledFeaturesArray: LanguageFeature array) | LanguageFeature.AndBang -> FSComp.SR.featureAndBang () | LanguageFeature.NullnessChecking -> FSComp.SR.featureNullnessChecking () | LanguageFeature.ResumableStateMachines -> FSComp.SR.featureResumableStateMachines () + | LanguageFeature.RuntimeAsync -> FSComp.SR.featureRuntimeAsync () | LanguageFeature.NullableOptionalInterop -> FSComp.SR.featureNullableOptionalInterop () | LanguageFeature.DefaultInterfaceMemberConsumption -> FSComp.SR.featureDefaultInterfaceMemberConsumption () | LanguageFeature.WitnessPassing -> FSComp.SR.featureWitnessPassing () diff --git a/src/Compiler/Facilities/LanguageFeatures.fsi b/src/Compiler/Facilities/LanguageFeatures.fsi index 8c7ebd7e3c3..adec3cf897c 100644 --- a/src/Compiler/Facilities/LanguageFeatures.fsi +++ b/src/Compiler/Facilities/LanguageFeatures.fsi @@ -19,6 +19,7 @@ type LanguageFeature = | FixedIndexSlice3d4d | AndBang | ResumableStateMachines + | RuntimeAsync | NullableOptionalInterop | DefaultInterfaceMemberConsumption | WitnessPassing diff --git a/src/Compiler/Optimize/Optimizer.fs b/src/Compiler/Optimize/Optimizer.fs index a6b21b577eb..a74adc7400c 100644 --- a/src/Compiler/Optimize/Optimizer.fs +++ b/src/Compiler/Optimize/Optimizer.fs @@ -2496,6 +2496,13 @@ let rec OptimizeExpr cenv (env: IncrementalOptimizationEnv) expr = | Expr.App (f, fty, tyargs, argsl, m) -> match expr with + | Expr.App(Expr.Val(vref, flags, _), fty, [ _ ], [ body ], _) + when valRefEq g vref g.cgh__runtimeAsync_vref -> + let bodyR, bodyInfo = OptimizeExpr cenv env body + Expr.App(Expr.Val(vref, flags, m), fty, tyargs, [ bodyR ], m), + { bodyInfo with + HasEffect = true + Info = UnknownValue } | DelegateInvokeExpr g (delInvokeRef, delInvokeTy, tyargs, delExpr, delInvokeArg, m) -> OptimizeFSharpDelegateInvoke cenv env (delInvokeRef, delExpr, delInvokeTy, tyargs, delInvokeArg, m) | _ -> @@ -4383,8 +4390,8 @@ and OptimizeBinding cenv isRec env (TBind(vref, expr, spBind)) = let env = if vref.IsCompilerGenerated && Option.isSome env.latestBoundId then env else {env with latestBoundId=Some vref.Id} let cenv = if vref.InlineInfo.ShouldInline then { cenv with optimizing=false} else cenv let arityInfo = InferValReprInfoOfBinding g AllowTypeDirectedDetupling.No vref expr - let exprOptimized, einfo = OptimizeLambdas (Some vref) cenv env arityInfo expr vref.Type - let size = localVarSize + let exprOptimized, einfo = OptimizeLambdas (Some vref) cenv env arityInfo expr vref.Type + let size = localVarSize exprOptimized, {einfo with FunctionSize=einfo.FunctionSize+size; TotalSize = einfo.TotalSize+size} // Trim out optimization information for large lambdas we'll never inline diff --git a/src/Compiler/TypedTree/TcGlobals.fs b/src/Compiler/TypedTree/TcGlobals.fs index 3f983633574..9140b620a25 100644 --- a/src/Compiler/TypedTree/TcGlobals.fs +++ b/src/Compiler/TypedTree/TcGlobals.fs @@ -393,6 +393,7 @@ type TcGlobals( let v_tcref_IObservable = findSysTyconRef sys "IObservable`1" let v_tcref_IObserver = findSysTyconRef sys "IObserver`1" let v_fslib_IDelegateEvent_tcr = mk_MFControl_tcref fslibCcu "IDelegateEvent`1" + let v_task_tcr = findSysTyconRef ["System"; "Threading"; "Tasks"] "Task`1" let v_option_tcr_nice = mk_MFCore_tcref fslibCcu "option`1" let v_valueoption_tcr_nice = mk_MFCore_tcref fslibCcu "voption`1" @@ -884,6 +885,7 @@ type TcGlobals( let v_cgh__resumeAt_info = makeIntrinsicValRef(fslib_MFStateMachineHelpers_nleref, "__resumeAt" , None , None , [vara], ([[v_int_ty]; [varaTy]], varaTy)) let v_cgh__stateMachine_info = makeIntrinsicValRef(fslib_MFStateMachineHelpers_nleref, "__stateMachine" , None , None , [vara; varb], ([[varaTy]], varbTy)) // inaccurate type but it doesn't matter for linking let v_cgh__resumableEntry_info = makeIntrinsicValRef(fslib_MFStateMachineHelpers_nleref, "__resumableEntry" , None , None , [vara], ([[v_int_ty --> varaTy]; [v_unit_ty --> varaTy]], varaTy)) + let v_cgh__runtimeAsync_info = makeIntrinsicValRef(fslib_MFStateMachineHelpers_nleref, "__runtimeAsync" , None , None , [vara], ([[varaTy]], TType_app(v_task_tcr, [varaTy], v_knownWithoutNull))) // handled specially by the checker let v_seq_to_array_info = makeIntrinsicValRef(fslib_MFSeqModule_nleref, "toArray" , None , Some "ToArray", [varb], ([[mkSeqTy varbTy]], mkArrayType 1 varbTy)) let v_seq_to_list_info = makeIntrinsicValRef(fslib_MFSeqModule_nleref, "toList" , None , Some "ToList" , [varb], ([[mkSeqTy varbTy]], mkListTy varbTy)) let v_seq_map_info = makeIntrinsicValRef(fslib_MFSeqModule_nleref, "map" , None , Some "Map" , [vara;varb], ([[varaTy --> varbTy]; [mkSeqTy varaTy]], mkSeqTy varbTy)) @@ -1463,6 +1465,8 @@ type TcGlobals( // Review: Does this need to be an option type? member val System_Runtime_CompilerServices_RuntimeFeature_ty = tryFindSysTyconRef sysCompilerServices "RuntimeFeature" |> Option.map mkNonGenericTy + member val System_Runtime_CompilerServices_MethodImplOptions_ty = + tryFindSysTyconRef sysCompilerServices "MethodImplOptions" |> Option.map mkNonGenericTy member val iltyp_StreamingContext = tryFindSysILTypeRef tname_StreamingContext |> Option.map mkILNonGenericValueTy member val iltyp_SerializationInfo = tryFindSysILTypeRef tname_SerializationInfo |> Option.map mkILNonGenericBoxedTy @@ -1771,6 +1775,7 @@ type TcGlobals( member val cgh__stateMachine_vref = ValRefForIntrinsic v_cgh__stateMachine_info + member val cgh__runtimeAsync_vref = ValRefForIntrinsic v_cgh__runtimeAsync_info member val cgh__useResumableCode_vref = ValRefForIntrinsic v_cgh__useResumableCode_info member val cgh__debugPoint_vref = ValRefForIntrinsic v_cgh__debugPoint_info member val cgh__resumeAt_vref = ValRefForIntrinsic v_cgh__resumeAt_info diff --git a/src/Compiler/TypedTree/TcGlobals.fsi b/src/Compiler/TypedTree/TcGlobals.fsi index 709abfc5b18..096bbf585b2 100644 --- a/src/Compiler/TypedTree/TcGlobals.fsi +++ b/src/Compiler/TypedTree/TcGlobals.fsi @@ -270,6 +270,8 @@ type internal TcGlobals = member System_Runtime_CompilerServices_RuntimeFeature_ty: TypedTree.TType option + member System_Runtime_CompilerServices_MethodImplOptions_ty: TypedTree.TType option + member addrof2_vref: TypedTree.ValRef member addrof_vref: TypedTree.ValRef @@ -434,6 +436,8 @@ type internal TcGlobals = member cgh__stateMachine_vref: TypedTree.ValRef + member cgh__runtimeAsync_vref: TypedTree.ValRef + member cgh__useResumableCode_vref: TypedTree.ValRef member char_operator_info: IntrinsicValRef diff --git a/src/FSharp.Core/resumable.fs b/src/FSharp.Core/resumable.fs index 1ace0d12241..2fc9a0d21ab 100644 --- a/src/FSharp.Core/resumable.fs +++ b/src/FSharp.Core/resumable.fs @@ -11,6 +11,7 @@ namespace Microsoft.FSharp.Core.CompilerServices open System open System.Runtime.CompilerServices +open System.Threading.Tasks open Microsoft.FSharp.Core open Microsoft.FSharp.Core.LanguagePrimitives.IntrinsicOperators open Microsoft.FSharp.Collections @@ -110,6 +111,12 @@ module StateMachineHelpers = failwith "__stateMachine should always be guarded by __useResumableCode and only used in valid state machine implementations" + [] + let __runtimeAsync<'T> (value: 'T) : Task<'T> = + ignore value + + failwith "__runtimeAsync is a compiler intrinsic and should only be used in runtime-async method bodies" + module ResumableCode = open System.Runtime.ExceptionServices diff --git a/src/FSharp.Core/resumable.fsi b/src/FSharp.Core/resumable.fsi index e62a6597729..73c08775d05 100644 --- a/src/FSharp.Core/resumable.fsi +++ b/src/FSharp.Core/resumable.fsi @@ -194,6 +194,11 @@ module StateMachineHelpers = afterCode: AfterCode<'Data, 'Result> -> 'Result + /// Marks an expression result for lowering as a .NET runtime-async method. + /// This function is compiler-recognised and must not be called directly. + [] + val __runtimeAsync<'T> : 'T -> System.Threading.Tasks.Task<'T> + /// Adding this attribute to the method adjusts the processing of some generic methods /// during overload resolution. /// From 719d00fbb5100de267125fe60196576944e31635 Mon Sep 17 00:00:00 2001 From: majocha <1760221+majocha@users.noreply.github.com> Date: Sat, 8 Aug 2026 08:41:21 +0200 Subject: [PATCH 3/9] tests --- .../FSharp.Compiler.ComponentTests.fsproj | 2 + .../RuntimeAsync/RuntimeAsyncBasic.fs | 68 + .../RuntimeAsync/RuntimeTaskBuilder.fs | 206 +++ .../Language/RuntimeAsync/RuntimeTasks.fs | 1292 +++++++++++++++++ .../RuntimeTasksAsyncDisposalException.fs | 25 + .../Language/RuntimeAsyncTests.fs | 178 +++ 6 files changed, 1771 insertions(+) create mode 100644 tests/FSharp.Compiler.ComponentTests/Language/RuntimeAsync/RuntimeAsyncBasic.fs create mode 100644 tests/FSharp.Compiler.ComponentTests/Language/RuntimeAsync/RuntimeTaskBuilder.fs create mode 100644 tests/FSharp.Compiler.ComponentTests/Language/RuntimeAsync/RuntimeTasks.fs create mode 100644 tests/FSharp.Compiler.ComponentTests/Language/RuntimeAsync/RuntimeTasksAsyncDisposalException.fs create mode 100644 tests/FSharp.Compiler.ComponentTests/Language/RuntimeAsyncTests.fs diff --git a/tests/FSharp.Compiler.ComponentTests/FSharp.Compiler.ComponentTests.fsproj b/tests/FSharp.Compiler.ComponentTests/FSharp.Compiler.ComponentTests.fsproj index 476b903efcf..5df4af18719 100644 --- a/tests/FSharp.Compiler.ComponentTests/FSharp.Compiler.ComponentTests.fsproj +++ b/tests/FSharp.Compiler.ComponentTests/FSharp.Compiler.ComponentTests.fsproj @@ -17,6 +17,7 @@ $(DefineConstants);DEBUG true + runtime-async=on true @@ -385,6 +386,7 @@ + diff --git a/tests/FSharp.Compiler.ComponentTests/Language/RuntimeAsync/RuntimeAsyncBasic.fs b/tests/FSharp.Compiler.ComponentTests/Language/RuntimeAsync/RuntimeAsyncBasic.fs new file mode 100644 index 00000000000..2df7b814a47 --- /dev/null +++ b/tests/FSharp.Compiler.ComponentTests/Language/RuntimeAsync/RuntimeAsyncBasic.fs @@ -0,0 +1,68 @@ +module RuntimeAsyncBasic + +open System +open System.Runtime.CompilerServices +open System.Threading.Tasks +open Microsoft.FSharp.Core.CompilerServices + +let private delayed value = + Task.Delay(1).ContinueWith(fun (_: Task) -> value) + +let add (x: int) (y: int) : Task = + StateMachineHelpers.__runtimeAsync ( + let first = AsyncHelpers.Await(delayed x) + first + y) + +let lambdaAdd : int -> Task = + fun value -> + StateMachineHelpers.__runtimeAsync ( + let result = AsyncHelpers.Await(delayed value) + result + 1) + +let makeAdder (offset: int) : int -> Task = + fun value -> + StateMachineHelpers.__runtimeAsync ( + let result = AsyncHelpers.Await(delayed value) + result + offset) + +let inline apply ([] operation: int -> int) (value: int) = + operation value + +let inline awaitAndAdd (value: int) = + let result = + AsyncHelpers.Await(Task.Delay(1).ContinueWith(fun (_: Task) -> value)) + + apply (fun current -> current + 1) result + +let addWithInline (value: int) : Task = + StateMachineHelpers.__runtimeAsync (awaitAndAdd value) + +type Calculator() = + member _.Add(x: int, y: int) : Task = + StateMachineHelpers.__runtimeAsync ( + let first = AsyncHelpers.Await(delayed x) + first + y) + + static member Double(value: int) : Task = + StateMachineHelpers.__runtimeAsync (value * 2) + +let private resultOf (task: Task) = + task.GetAwaiter().GetResult() + +[] +let main _ = + let calculator = Calculator() + let capturedAdder = makeAdder 10 + + let results = + [ + add 20 22 + lambdaAdd 41 + capturedAdder 32 + addWithInline 41 + calculator.Add(20, 22) + Calculator.Double 21 + ] + |> List.map resultOf + + if results = [ 42; 42; 42; 42; 42; 42 ] then 0 else 1 diff --git a/tests/FSharp.Compiler.ComponentTests/Language/RuntimeAsync/RuntimeTaskBuilder.fs b/tests/FSharp.Compiler.ComponentTests/Language/RuntimeAsync/RuntimeTaskBuilder.fs new file mode 100644 index 00000000000..a37691833f8 --- /dev/null +++ b/tests/FSharp.Compiler.ComponentTests/Language/RuntimeAsync/RuntimeTaskBuilder.fs @@ -0,0 +1,206 @@ +module RuntimeTaskBuilder + +open System +open System.Runtime.CompilerServices +open System.Threading.Tasks +open Microsoft.FSharp.Control +open Microsoft.FSharp.Core.CompilerServices + +type RuntimeTask<'T> = unit -> 'T + +let inline bindAwaiter + ([] getAwaiter: unit -> 'Awaiter) + ([] getResult: 'Awaiter -> 'T) + ([] continuation: 'T -> 'U) + = + let awaiter = getAwaiter() + AsyncHelpers.AwaitAwaiter awaiter + let result = getResult awaiter + continuation result + +type RuntimeTaskBuilder() = + member inline _.Delay([] generator: unit -> 'T) : unit -> 'T = generator + member inline _.Run([] code: unit -> 'T) : Task<'T> = + StateMachineHelpers.__runtimeAsync (code()) + member inline _.Zero() = () + member inline _.Return(value: 'T) = value + member inline _.ReturnFrom(task: Task<'T>) = AsyncHelpers.Await task + member inline _.ReturnFrom(task: Task) = AsyncHelpers.Await task + member inline _.ReturnFrom(task: ValueTask<'T>) = AsyncHelpers.Await task + member inline _.ReturnFrom(task: ValueTask) = AsyncHelpers.Await task + member inline _.ReturnFrom(computation: Async<'T>) = AsyncHelpers.Await(Async.StartImmediateAsTask computation) + member inline _.Bind(task: Task, [] continuation: unit -> 'U) = + AsyncHelpers.Await task + continuation() + member inline _.Bind(task: Task<'T>, [] continuation: 'T -> 'U) = + continuation (AsyncHelpers.Await task) + member inline _.Bind(code: struct ('T1 * 'T2), [] continuation: struct ('T1 * 'T2) -> 'U) = + continuation code + member inline _.Bind(computation: RuntimeTask<'T>, [] continuation: 'T -> 'U) = + continuation (computation ()) + member inline _.Bind(task: ValueTask, [] continuation: unit -> 'U) = + AsyncHelpers.Await task + continuation() + member inline _.Bind(task: ValueTask<'T>, [] continuation: 'T -> 'U) = + continuation (AsyncHelpers.Await task) + member inline _.Bind(computation: Async<'T>, [] continuation: 'T -> 'U) = + continuation (AsyncHelpers.Await(Async.StartImmediateAsTask computation)) + member inline _.Combine(first, [] second) = + first() + second() + member inline _.Combine(first: unit, [] second: unit -> 'T) = second() + member inline _.TryWith([] body: unit -> 'T, [] handler: exn -> 'T) = + try body() with error -> handler error + member inline _.TryFinally([] body: unit -> 'T, compensation: unit -> unit) = + try body() finally compensation() + member inline _.Using(resource: 'Resource, [] body: 'Resource -> 'T) = + // Awaiting in a finally region is forbidden by the runtime-async contract. + // Hoist the DisposeAsync suspension out of the region: capture any exception + // from the body in a catch-all, run disposal (possibly suspending) outside + // the handler, then restore the pending exception. Mirrors the Roslyn + // runtime-async lowering for `await` in `finally`. + let mutable pendingException: exn = null + + let result = + try + Choice1Of2(body resource) + with error -> + pendingException <- error + Choice2Of2() + + match box resource with + | :? IAsyncDisposable as disposable -> AsyncHelpers.Await(disposable.DisposeAsync()) + | :? IDisposable as disposable -> disposable.Dispose() + | _ -> () + + match pendingException with + | null -> () + | error -> raise error + + match result with + | Choice1Of2 value -> value + | Choice2Of2() -> Unchecked.defaultof<'T> + member inline _.While(guard: unit -> bool, [] body: unit -> unit) = + while guard() do body() + member inline _.For(sequence: seq<'T>, [] body: 'T -> unit) = + for item in sequence do body item + member inline _.MergeSources(left: Task<'T1>, right: Task<'T2>) = + struct (AsyncHelpers.Await left, AsyncHelpers.Await right) + member inline _.MergeSources(left: ValueTask<'T1>, right: ValueTask<'T2>) = + struct (AsyncHelpers.Await left, AsyncHelpers.Await right) + member inline _.MergeSources(left: Task<'T1>, right: ValueTask<'T2>) = + struct (AsyncHelpers.Await left, AsyncHelpers.Await right) + member inline _.MergeSources(left: ValueTask<'T1>, right: Task<'T2>) = + struct (AsyncHelpers.Await left, AsyncHelpers.Await right) + member inline _.MergeSources(left: Task<'T1>, right: Async<'T2>) = + struct (AsyncHelpers.Await left, AsyncHelpers.Await(Async.StartImmediateAsTask right)) + member inline _.MergeSources(left: Async<'T1>, right: Task<'T2>) = + struct (AsyncHelpers.Await(Async.StartImmediateAsTask left), AsyncHelpers.Await right) + member inline _.MergeSources(left: Async<'T1>, right: Async<'T2>) = + struct (AsyncHelpers.Await(Async.StartImmediateAsTask left), AsyncHelpers.Await(Async.StartImmediateAsTask right)) + member inline _.MergeSources(left: Async<'T1>, right: ValueTask<'T2>) = + struct (AsyncHelpers.Await(Async.StartImmediateAsTask left), AsyncHelpers.Await right) + member inline _.MergeSources(left: ValueTask<'T1>, right: Async<'T2>) = + struct (AsyncHelpers.Await left, AsyncHelpers.Await(Async.StartImmediateAsTask right)) + member inline _.MergeSources(left: YieldAwaitable, right: Task<'T2>) = + AsyncHelpers.AwaitAwaiter(left.GetAwaiter()) + struct ((), AsyncHelpers.Await right) + member inline _.MergeSources(left: Task<'T1>, right: YieldAwaitable) = + let leftResult = AsyncHelpers.Await left + AsyncHelpers.AwaitAwaiter(right.GetAwaiter()) + struct (leftResult, ()) + member inline _.MergeSources(left: YieldAwaitable, right: ValueTask<'T2>) = + AsyncHelpers.AwaitAwaiter(left.GetAwaiter()) + struct ((), AsyncHelpers.Await right) + member inline _.MergeSources(left: ValueTask<'T1>, right: YieldAwaitable) = + let leftResult = AsyncHelpers.Await left + AsyncHelpers.AwaitAwaiter(right.GetAwaiter()) + struct (leftResult, ()) + member inline _.MergeSources(left: YieldAwaitable, right: Async<'T2>) = + AsyncHelpers.AwaitAwaiter(left.GetAwaiter()) + struct ((), AsyncHelpers.Await(Async.StartImmediateAsTask right)) + member inline _.MergeSources(left: Async<'T1>, right: YieldAwaitable) = + let leftResult = AsyncHelpers.Await(Async.StartImmediateAsTask left) + AsyncHelpers.AwaitAwaiter(right.GetAwaiter()) + struct (leftResult, ()) + member inline _.MergeSources(left: YieldAwaitable, right: struct ('T2 * 'T3)) = + AsyncHelpers.AwaitAwaiter(left.GetAwaiter()) + struct ((), right) + member inline _.MergeSources(left: struct ('T1 * 'T2), right: YieldAwaitable) = + AsyncHelpers.AwaitAwaiter(right.GetAwaiter()) + struct (left, ()) + member inline _.MergeSources(left: Task<'T1>, right: struct ('T2 * 'T3)) = + struct (AsyncHelpers.Await left, right) + member inline _.MergeSources(left: ValueTask<'T1>, right: struct ('T2 * 'T3)) = + struct (AsyncHelpers.Await left, right) + member inline _.MergeSources(left: Async<'T1>, right: struct ('T2 * 'T3)) = + struct (AsyncHelpers.Await(Async.StartImmediateAsTask left), right) + member inline _.MergeSources(left: struct ('T1 * 'T2), right: Task<'T3>) = + struct (left, AsyncHelpers.Await right) + member inline _.MergeSources(left: struct ('T1 * 'T2), right: ValueTask<'T3>) = + struct (left, AsyncHelpers.Await right) + member inline _.MergeSources(left: struct ('T1 * 'T2), right: Async<'T3>) = + struct (left, AsyncHelpers.Await(Async.StartImmediateAsTask right)) + +module RuntimeTaskAwaitableExtensions = + type RuntimeTaskBuilder with + // SRTP fallbacks mirroring the task builder's task-like Bind/ReturnFrom/MergeSources, + // so custom awaitables compose without dedicated overloads. + [] + member inline _.ReturnFrom< ^TaskLike, ^Awaiter, 'T + when ^TaskLike: (member GetAwaiter: unit -> ^Awaiter) + and ^Awaiter :> ICriticalNotifyCompletion + and ^Awaiter: (member get_IsCompleted: unit -> bool) + and ^Awaiter: (member GetResult: unit -> 'T)> + (task: ^TaskLike) + : 'T = + bindAwaiter + (fun () -> (^TaskLike: (member GetAwaiter: unit -> ^Awaiter) task)) + (fun awaiter -> (^Awaiter: (member GetResult: unit -> 'T) awaiter)) + id + + [] + member inline _.MergeSources< ^TaskLike1, ^TaskLike2, ^Awaiter1, ^Awaiter2, 'T1, 'T2 + when ^TaskLike1: (member GetAwaiter: unit -> ^Awaiter1) + and ^TaskLike2: (member GetAwaiter: unit -> ^Awaiter2) + and ^Awaiter1 :> ICriticalNotifyCompletion + and ^Awaiter2 :> ICriticalNotifyCompletion + and ^Awaiter1: (member get_IsCompleted: unit -> bool) + and ^Awaiter1: (member GetResult: unit -> 'T1) + and ^Awaiter2: (member get_IsCompleted: unit -> bool) + and ^Awaiter2: (member GetResult: unit -> 'T2)> + (task1: ^TaskLike1, task2: ^TaskLike2) + : struct ('T1 * 'T2) = + let await1 () = + bindAwaiter + (fun () -> (^TaskLike1: (member GetAwaiter: unit -> ^Awaiter1) task1)) + (fun awaiter -> (^Awaiter1: (member GetResult: unit -> 'T1) awaiter)) + id + + let await2 () = + bindAwaiter + (fun () -> (^TaskLike2: (member GetAwaiter: unit -> ^Awaiter2) task2)) + (fun awaiter -> (^Awaiter2: (member GetResult: unit -> 'T2) awaiter)) + id + // Sequential awaits, matching the task builder's MergeSources; concurrency + // comes from the sources being already-started hot tasks. + struct (await1 (), await2 ()) + + [] + member inline _.Bind< ^TaskLike, ^Awaiter, 'T, 'U + when ^TaskLike: (member GetAwaiter: unit -> ^Awaiter) + and ^Awaiter :> ICriticalNotifyCompletion + and ^Awaiter: (member get_IsCompleted: unit -> bool) + and ^Awaiter: (member GetResult: unit -> 'T)> + (task: ^TaskLike, [] continuation: 'T -> 'U) + : 'U = + bindAwaiter + (fun () -> (^TaskLike: (member GetAwaiter: unit -> ^Awaiter) task)) + (fun awaiter -> (^Awaiter: (member GetResult: unit -> 'T) awaiter)) + continuation + +open RuntimeTaskAwaitableExtensions + +[] +module RuntimeTask = + let runtimeTask = RuntimeTaskBuilder() diff --git a/tests/FSharp.Compiler.ComponentTests/Language/RuntimeAsync/RuntimeTasks.fs b/tests/FSharp.Compiler.ComponentTests/Language/RuntimeAsync/RuntimeTasks.fs new file mode 100644 index 00000000000..0a5f91d4a3d --- /dev/null +++ b/tests/FSharp.Compiler.ComponentTests/Language/RuntimeAsync/RuntimeTasks.fs @@ -0,0 +1,1292 @@ +// Tests for the runtime-async RuntimeTaskBuilder, ported from the TaskBuilder tests in +// tests/FSharp.Core.UnitTests/FSharp.Core/Microsoft.FSharp.Control/Tasks.fs +// with `task {` replaced by `runtimeTask {`. Test names and bodies are kept as +// close to the originals as possible. +// +// Tests that require suspending inside an exception-handling region are in the +// "Known failing" section at the bottom and are NOT called from main: the .NET +// runtime-async contract forbids suspension in EH regions, and depending on the +// case this currently either loses the finally or terminates the process +// (0xC0000409). `backgroundTask` tests have no runtimeTask equivalent and are +// omitted. + +module RuntimeTasks + +open System +open System.Collections +open System.Collections.Generic +open System.Diagnostics +open System.Threading +open System.Threading.Tasks +open Microsoft.FSharp.Control +open Microsoft.FSharp.Core.CompilerServices + +open RuntimeTaskBuilder.RuntimeTask +open RuntimeTaskBuilder.RuntimeTaskAwaitableExtensions + +exception TestException of string + +let BIG = 10 +let require x msg = if not x then failwith msg +let failtest str = raise (TestException str) +let resultOf (task: Task<'T>) = task.GetAwaiter().GetResult() + +let private delayed value = + Task.Delay(1).ContinueWith(fun (_: Task) -> value) + +// --------------------------------------------------------------------------- +// SmokeTestsForCompilation +// --------------------------------------------------------------------------- + +let tinyTask () = + runtimeTask { + return 1 + } + |> fun t -> + t.Wait() + if t.Result <> 1 then failwith "failed" + +let tbind () = + runtimeTask { + let! x = Task.FromResult(1) + return 1 + x + } + |> fun t -> + t.Wait() + if t.Result <> 2 then failwith "failed" + +let tnested () = + runtimeTask { + let! x = runtimeTask { return 1 } + return x + } + |> fun t -> + t.Wait() + if t.Result <> 1 then failwith "failed" + +let tcatch0 () = + runtimeTask { + try + return 1 + with e -> + return 2 + } + |> fun t -> + t.Wait() + if t.Result <> 1 then failwith "failed" + +let tcatch1 () = + runtimeTask { + try + let! x = Task.FromResult 1 + return x + with e -> + return 2 + } + |> fun t -> + t.Wait() + if t.Result <> 1 then failwith "failed" + +let t3 () = + let t2() = + runtimeTask { + System.Console.WriteLine("hello") + return 1 + } + runtimeTask { + System.Console.WriteLine("hello") + let! x = t2() + System.Console.WriteLine("world") + return 1 + x + } + |> fun t -> + t.Wait() + if t.Result <> 2 then failwith "failed" + +let t3b () = + runtimeTask { + System.Console.WriteLine("hello") + let! x = Task.FromResult(1) + System.Console.WriteLine("world") + return 1 + x + } + |> fun t -> + t.Wait() + if t.Result <> 2 then failwith "failed" + +let t3c () = + runtimeTask { + System.Console.WriteLine("hello") + do! Task.Delay(100) + System.Console.WriteLine("world") + return 1 + } + |> fun t -> + t.Wait() + if t.Result <> 1 then failwith "failed" + +// This tests an exception match +let t67 () = + runtimeTask { + try + do! Task.Delay(0) + with + | :? ArgumentException -> + () + | _ -> + () + } + |> fun t -> + t.Wait() + if t.Result <> () then failwith "failed" + +// This tests compiling an incomplete exception match +let t68 () = + runtimeTask { + try + do! Task.Delay(0) + with + | :? ArgumentException -> + () + } + |> fun t -> + t.Wait() + if t.Result <> () then failwith "failed" + +let testCompileAsyncWhileLoop () = + runtimeTask { + let mutable i = 0 + while i < 5 do + i <- i + 1 + do! Task.Yield() + return i + } + |> fun t -> + t.Wait() + if t.Result <> 5 then failwith "failed" + +let merge2tasks () = + runtimeTask { + let! x = Task.FromResult(1) + and! y = Task.FromResult(2) + return x + y + } + |> fun t -> + t.Wait() + if t.Result <> 3 then failwith "failed" + +let merge3tasks () = + runtimeTask { + let! x = Task.FromResult(1) + and! y = Task.FromResult(2) + and! z = Task.FromResult(3) + return x + y + z + } + |> fun t -> + t.Wait() + if t.Result <> 6 then failwith "failed" + +let mergeYieldAndTask () = + runtimeTask { + let! _ = Task.Yield() + and! y = Task.FromResult(1) + return y + } + |> fun t -> + t.Wait() + if t.Result <> 1 then failwith "failed" + +let mergeTaskAndYield () = + runtimeTask { + let! x = Task.FromResult(1) + and! _ = Task.Yield() + return x + } + |> fun t -> + t.Wait() + if t.Result <> 1 then failwith "failed" + +let merge2valueTasks () = + runtimeTask { + let! x = ValueTask(Task.FromResult(1)) + and! y = ValueTask(Task.FromResult(2)) + return x + y + } + |> fun t -> + t.Wait() + if t.Result <> 3 then failwith "failed" + +let merge2valueTasksAndYield () = + runtimeTask { + let! x = ValueTask(Task.FromResult(1)) + and! y = ValueTask(Task.FromResult(2)) + and! _ = Task.Yield() + return x + y + } + |> fun t -> + t.Wait() + if t.Result <> 3 then failwith "failed" + +let mergeYieldAnd2tasks () = + runtimeTask { + let! _ = Task.Yield() + and! x = Task.FromResult(1) + and! y = Task.FromResult(2) + return x + y + } + |> fun t -> + t.Wait() + if t.Result <> 3 then failwith "failed" + +let merge2tasksAndValueTask () = + runtimeTask { + let! x = Task.FromResult(1) + and! y = Task.FromResult(2) + and! z = ValueTask(Task.FromResult(3)) + return x + y + z + } + |> fun t -> + t.Wait() + if t.Result <> 6 then failwith "failed" + +let merge2asyncs () = + runtimeTask { + let! x = async { return 1 } + and! y = async { return 2 } + return x + y + } + |> fun t -> + t.Wait() + if t.Result <> 3 then failwith "failed" + +let merge3asyncs () = + runtimeTask { + let! x = async { return 1 } + and! y = async { return 2 } + and! z = async { return 3 } + return x + y + z + } + |> fun t -> + t.Wait() + if t.Result <> 6 then failwith "failed" + +let mergeYieldAndAsync () = + runtimeTask { + let! _ = Task.Yield() + and! y = async { return 1 } + return y + } + |> fun t -> + t.Wait() + if t.Result <> 1 then failwith "failed" + +let mergeAsyncAndYield () = + runtimeTask { + let! x = async { return 1 } + and! _ = Task.Yield() + return x + } + |> fun t -> + t.Wait() + if t.Result <> 1 then failwith "failed" + +let mergeYieldAnd2asyncs () = + runtimeTask { + let! _ = Task.Yield() + and! x = async { return 1 } + and! y = async { return 2 } + return x + y + } + |> fun t -> + t.Wait() + if t.Result <> 3 then failwith "failed" + +let merge2asyncsAndValueTask () = + runtimeTask { + let! x = async { return 1 } + and! y = async { return 2 } + and! z = ValueTask(Task.FromResult(3)) + return x + y + z + } + |> fun t -> + t.Wait() + if t.Result <> 6 then failwith "failed" + +// --------------------------------------------------------------------------- +// Basics +// --------------------------------------------------------------------------- + +let testShortCircuitResult () = + let t = + runtimeTask { + let! x = Task.FromResult(1) + let! y = Task.FromResult(2) + return x + y + } + require t.IsCompleted "didn't short-circuit already completed tasks" + require (t.Result = 3) "wrong result" + +let testDelay () = + let mutable x = 0 + let t = + runtimeTask { + do! Task.Delay(50) + x <- x + 1 + } + require (x = 0) "task already ran" + t.Wait() + +// KNOWN DIVERGENCE: moved to the known-failing section; the current runtime +// build does not run a runtime-async body synchronously up to its first real +// suspension, so "first part didn't run yet" fails. + +let testNonBlocking () = + let allowContinue = new SemaphoreSlim(0) + let continueToFinish = new ManualResetEventSlim(false) + let finished = new ManualResetEventSlim() + let t = + runtimeTask { + do! allowContinue.WaitAsync() + continueToFinish.Wait() + finished.Set() + } + allowContinue.Release() |> ignore + require (not finished.IsSet) "sleep blocked caller" + continueToFinish.Set() + t.Wait() + +// The knownFailing_* tests below suspend inside try/with in non-tail position +// (or require synchronous start before the first suspension). Suspension in +// exception-handling regions is forbidden by the runtime-async contract; these +// compile but are not run from main. + +let knownFailing_testCatching1 () = + let mutable x = 0 + let mutable y = 0 + let t = + runtimeTask { + try + do! Task.Delay(0) + failtest "hello" + x <- 1 + do! Task.Delay(100) + with + | TestException msg -> + require (msg = "hello") "message tampered" + | _ -> + require false "other exn type" + y <- 1 + } + t.Wait() + require (y = 1) "bailed after exn" + require (x = 0) "ran past failure" + +let knownFailing_testCatching2 () = + let mutable x = 0 + let mutable y = 0 + let t = + runtimeTask { + try + do! Task.Yield() // can't skip through this + failtest "hello" + x <- 1 + do! Task.Delay(100) + with + | TestException msg -> + require (msg = "hello") "message tampered" + | _ -> + require false "other exn type" + y <- 1 + } + t.Wait() + require (y = 1) "bailed after exn" + require (x = 0) "ran past failure" + +let knownFailing_testCatchingInApplicative () = + let mutable x = 0 + let mutable y = 0 + let t = + runtimeTask { + try + let! _ = runtimeTask { + do! Task.Delay(100) + x <- 1 + } + and! _ = runtimeTask { + failtest "hello" + } + () + with + | TestException msg -> + require (msg = "hello") "message tampered" + | _ -> + require false "other exn type" + y <- 1 + } + t.Wait() + require (y = 1) "bailed after exn" + require (x = 1) "exit too early" + +let knownFailing_testNestedCatching () = + let mutable counter = 1 + let mutable caughtInner = 0 + let mutable caughtOuter = 0 + let t1() = + runtimeTask { + try + do! Task.Yield() + failtest "hello" + with + | TestException msg as exn -> + caughtInner <- counter + counter <- counter + 1 + raise exn + } + let t2 = + runtimeTask { + try + do! t1() + with + | TestException msg as exn -> + caughtOuter <- counter + raise exn + | e -> + require false (sprintf "invalid msg type %s" e.Message) + } + try + t2.Wait() + require false "ran past failed task wait" + with + | :? AggregateException as exn -> + require (exn.InnerExceptions.Count = 1) "more than 1 exn" + require (caughtInner = 1) "didn't catch inner" + require (caughtOuter = 2) "didn't catch outer" + +let testWhileLoopSync () = + let t = + runtimeTask { + let mutable i = 0 + while i < 10 do + i <- i + 1 + return i + } + //t.Wait() no wait required for sync loop + require (t.IsCompleted) "didn't do sync while loop properly - not completed" + require (t.Result = 10) "didn't do sync while loop properly - wrong result" + +let testWhileLoopAsyncZeroIteration () = + for i in 1 .. 5 do + let t = + runtimeTask { + let mutable i = 0 + while i < 0 do + i <- i + 1 + do! Task.Yield() + return i + } + t.Wait() + require (t.Result = 0) "didn't do while loop properly" + +let testWhileLoopAsyncOneIteration () = + for i in 1 .. 5 do + let t = + runtimeTask { + let mutable i = 0 + while i < 1 do + i <- i + 1 + do! Task.Yield() + return i + } + t.Wait() + require (t.Result = 1) "didn't do while loop properly" + +let testWhileLoopAsync () = + for i in 1 .. 5 do + let t = + runtimeTask { + let mutable i = 0 + while i < 10 do + i <- i + 1 + do! Task.Yield() + return i + } + t.Wait() + require (t.Result = 10) "didn't do while loop properly" + +let testForLoopA () = + let list = ["a"; "b"; "c"] |> Seq.ofList + let t = + runtimeTask { + let mutable x = Unchecked.defaultof<_> + let e = list.GetEnumerator() + while e.MoveNext() do + x <- e.Current + do! Task.Yield() + } + t.Wait() + +let testForLoopComplex () = + let mutable disposed = false + let wrapList = + let raw = ["a"; "b"; "c"] |> Seq.ofList + let getEnumerator() = + let raw = raw.GetEnumerator() + { new IEnumerator with + member _.MoveNext() = + require (not disposed) "moved next after disposal" + raw.MoveNext() + member _.Current = + require (not disposed) "accessed current after disposal" + raw.Current + member _.Current = + require (not disposed) "accessed current (boxed) after disposal" + box raw.Current + member _.Dispose() = + require (not disposed) "disposed twice" + disposed <- true + raw.Dispose() + member _.Reset() = + require (not disposed) "reset after disposal" + raw.Reset() + } + { new IEnumerable with + member _.GetEnumerator() : IEnumerator = getEnumerator() + member _.GetEnumerator() : IEnumerator = upcast getEnumerator() + } + let t = + runtimeTask { + let mutable index = 0 + do! Task.Yield() + for x in wrapList do + do! Task.Yield() + do! Task.Yield() + match index with + | 0 -> require (x = "a") "wrong first value" + | 1 -> require (x = "b") "wrong second value" + | 2 -> require (x = "c") "wrong third value" + | _ -> require false "iterated too far!" + index <- index + 1 + do! Task.Yield() + do! Task.Yield() + do! Task.Yield() + return 1 + } + t.Wait() + require disposed "never disposed D" + require (t.Result = 1) "wrong result" + +let testForLoopSadPath () = + for i in 1 .. 5 do + let wrapList = ["a"; "b"; "c"] + let t = + runtimeTask { + let mutable index = 0 + do! Task.Yield() + for x in wrapList do + do! Task.Yield() + index <- index + 1 + return 1 + } + require (t.Result = 1) "wrong result" + +let knownFailing_testForLoopSadPathComplex () = + for i in 1 .. 5 do + let mutable disposed = false + let wrapList = + let raw = ["a"; "b"; "c"] |> Seq.ofList + let getEnumerator() = + let raw = raw.GetEnumerator() + { new IEnumerator with + member _.MoveNext() = + require (not disposed) "moved next after disposal" + raw.MoveNext() + member _.Current = + require (not disposed) "accessed current after disposal" + raw.Current + member _.Current = + require (not disposed) "accessed current (boxed) after disposal" + box raw.Current + member _.Dispose() = + require (not disposed) "disposed twice" + disposed <- true + raw.Dispose() + member _.Reset() = + require (not disposed) "reset after disposal" + raw.Reset() + } + { new IEnumerable with + member _.GetEnumerator() : IEnumerator = getEnumerator() + member _.GetEnumerator() : IEnumerator = upcast getEnumerator() + } + let mutable caught = false + let t = + runtimeTask { + try + let mutable index = 0 + do! Task.Yield() + for x in wrapList do + do! Task.Yield() + match index with + | 0 -> require (x = "a") "wrong first value" + | _ -> failtest "uhoh" + index <- index + 1 + do! Task.Yield() + do! Task.Yield() + return 1 + with + | TestException "uhoh" -> + caught <- true + return 2 + } + require (t.Result = 2) "wrong result" + require caught "didn't catch exception" + require disposed "never disposed A" + +let knownFailing_testExceptionAttachedToTaskWithoutAwait () = + for i in 1 .. 5 do + let mutable ranA = false + let mutable ranB = false + let t = + runtimeTask { + ranA <- true + failtest "uhoh" + ranB <- true + } + require ranA "didn't run immediately" + require (not ranB) "ran past exception" + require (not (isNull t.Exception)) "didn't capture exception" + require (t.Exception.InnerExceptions.Count = 1) "captured more exceptions" + require (t.Exception.InnerException = TestException "uhoh") "wrong exception" + let mutable caught = false + let mutable ranCatcher = false + let catcher = + runtimeTask { + try + ranCatcher <- true + let! result = t + return false + with + | TestException "uhoh" -> + caught <- true + return true + } + require ranCatcher "didn't run" + require catcher.Result "didn't catch" + require caught "didn't catch" + +let knownFailing_testExceptionAttachedToTaskWithAwait () = + for i in 1 .. 5 do + let mutable ranA = false + let mutable ranB = false + let t = + runtimeTask { + ranA <- true + failtest "uhoh" + do! Task.Delay(100) + ranB <- true + } + require ranA "didn't run immediately" + require (not ranB) "ran past exception" + require (not (isNull t.Exception)) "didn't capture exception" + require (t.Exception.InnerExceptions.Count = 1) "captured more exceptions" + require (t.Exception.InnerException = TestException "uhoh") "wrong exception" + let mutable caught = false + let mutable ranCatcher = false + let catcher = + runtimeTask { + try + ranCatcher <- true + let! result = t + return false + with + | TestException "uhoh" -> + caught <- true + return true + } + require ranCatcher "didn't run" + require catcher.Result "didn't catch" + require caught "didn't catch" + +let testFixedStackWhileLoop () = + for i in 1 .. 100 do + let t = + runtimeTask { + let mutable maxDepth = Nullable() + let mutable i = 0 + while i < BIG do + i <- i + 1 + do! Task.Yield() + if i % 100 = 0 then + let stackDepth = StackTrace().FrameCount + if maxDepth.HasValue && stackDepth > maxDepth.Value then + failwith "Stack depth increased!" + maxDepth <- Nullable(stackDepth) + return i + } + t.Wait() + require (t.Result = BIG) "didn't get to big number" + +let knownFailing_testFixedStackForLoop () = // needs investigation: code after a suspending for loop is not run + for i in 1 .. 100 do + let mutable ran = false + let t = + runtimeTask { + let mutable maxDepth = Nullable() + for i in Seq.init BIG id do + do! Task.Yield() + if i % 100 = 0 then + let stackDepth = StackTrace().FrameCount + if maxDepth.HasValue && stackDepth > maxDepth.Value then + failwith "Stack depth increased!" + maxDepth <- Nullable(stackDepth) + ran <- true + return () + } + t.Wait() + require ran "didn't run all" + +let testTypeInference () = + let t1 : string Task = + runtimeTask { + return "hello" + } + let t2 = + runtimeTask { + // Divergence from task {}: the runtimeTask Bind overload set does not + // propagate the element type here, so the annotation is required. + let! (s: string) = t1 + return s.Length + } + t2.Wait() + +let testNoStackOverflowWithImmediateResult () = + let longLoop = + runtimeTask { + let mutable n = 0 + while n < BIG do + n <- n + 1 + return! Task.FromResult(()) + } + longLoop.Wait() + +let testNoStackOverflowWithYieldResult () = + let longLoop = + runtimeTask { + let mutable n = 0 + while n < BIG do + let! _ = + runtimeTask { + do! Task.Yield() + let! _ = Task.FromResult(0) + n <- n + 1 + } + n <- n + 1 + } + longLoop.Wait() + +let testSmallTailRecursion () = + let rec loop n = + runtimeTask { + if n < 100 then + do! Task.Yield() + let! _ = Task.FromResult(0) + return! loop (n + 1) + else + return () + } + let shortLoop = + runtimeTask { + return! loop 0 + } + shortLoop.Wait() + +let testTryOverReturnFrom () = + let inner() = + runtimeTask { + do! Task.Yield() + failtest "inner" + return 1 + } + let t = + runtimeTask { + try + do! Task.Yield() + return! inner() + with + | TestException "inner" -> return 2 + } + require (t.Result = 2) "didn't catch" + +let testAsyncsMixedWithTasks () = + let t = + runtimeTask { + do! Task.Delay(1) + do! Async.Sleep(1) + let! x = + async { + do! Async.Sleep(1) + return 5 + } + return! async { return x + 3 } + } + let result = t.Result + require (result = 8) "something weird happened" + +let testAsyncsMixedWithTasks_ShouldNotSwitchContext () = + let t = runtimeTask { + let a = Thread.CurrentThread.ManagedThreadId + let! b = async { + return Thread.CurrentThread.ManagedThreadId + } + let c = Thread.CurrentThread.ManagedThreadId + return $"Before: {a}, in async: {b}, after async: {c}" + } + let d = Thread.CurrentThread.ManagedThreadId + let actual = $"{t.Result}, after task: {d}" + + require (actual = $"Before: {d}, in async: {d}, after async: {d}, after task: {d}") actual + +// no need to call this, we just want to check that it compiles w/o warnings +let testTrivialReturnCompiles (x : 'a) : 'a Task = + runtimeTask { + do! Task.Yield() + return x + } + +// no need to call this, we just want to check that it compiles w/o warnings +let testTrivialTransformedReturnCompiles (x : 'a) (f : 'a -> 'b) : 'b Task = + runtimeTask { + do! Task.Yield() + return f x + } + +// no need to call this, we just want to check that it compiles w/o warnings +let testDefaultInferenceForReturnFrom () = + let t = runtimeTask { return Some "x" } + runtimeTask { + let! r = t + if r = None then + // Divergence from task {}: ReturnFrom is overloaded, so the generic + // failwithf result needs an explicit Task<_> annotation. + return! (failwithf "Could not find x" : string option Task) + else + return r + } + |> ignore + +// no need to call this, just check that it compiles +let testCompilerInfersArgumentOfReturnFrom () = + runtimeTask { + if true then return 1 + else return! (failwith "" : int Task) + } + |> ignore + +// Overload-resolution cases from the bottom of Tasks.fs (Issue12184*), compile-only. +type Issue12184() = + member this.TaskMethod() = + runtimeTask { + // The overload resolution for Bind commits to 'Async' since the type annotation is present. + let! result = this.AsyncMethod(21) + return result + } + + member _.AsyncMethod(value: int) : Async = + async { + return (value * 2) + } + +type Issue12184b() = + member this.TaskMethod() = + runtimeTask { + // The overload resolution for Bind commits to 'YieldAwaitable' since the type annotation is present. + let! result = this.AsyncMethod(21) + return result + } + + member _.AsyncMethod(_value: int) : System.Runtime.CompilerServices.YieldAwaitable = + Task.Yield() + +// Issue12184c from Tasks.fs is omitted: it relies on task {}'s Bind overload +// resolution committing to Task<_> for an unannotated argument, which the +// runtimeTask builder's overload set does not support. + +module Issue12184d = + let TaskMethod(t: ValueTask) = + runtimeTask { + let! result = t + return result + } + +module Issue12184e = + let TaskMethod(t: ValueTask) = + runtimeTask { + let! result = t + return result + } + +module Issue12184f = + let TaskMethod(t: Task) = + runtimeTask { + let! result = t + return result + } + +// --------------------------------------------------------------------------- +// Known failing: these tests suspend inside an exception-handling region +// (try/finally or an `Using` finally that awaits an IAsyncDisposable), which +// the runtime-async contract forbids. Today they either lose the finally or +// terminate the process (0xC0000409), so they are compiled but not run. +// RuntimeTasksAsyncDisposalException.fs keeps the minimal crash repro. +// +// A second group relies on synchronous (hot) start of the task body up to the +// first suspension. On the current runtime build a runtime-async body does not +// observably run before the returned Task is awaited, so these are not run +// either. +// --------------------------------------------------------------------------- + +let knownDivergent_testNoDelay () = + let mutable x = 0 + let t = + runtimeTask { + x <- x + 1 + do! Task.Delay(5) + x <- x + 1 + } + require (x = 1) "first part didn't run yet" + t.Wait() + +let knownFailing_testTryFinallyHappyPath () = + for i in 1 .. 5 do + let mutable ran = false + let t = + runtimeTask { + try + require (not ran) "ran way early" + do! Task.Delay(100) + require (not ran) "ran kinda early" + finally + ran <- true + } + t.Wait() + require ran "never ran" + +let knownFailing_testTryFinallySadPath () = + for i in 1 .. 5 do + let mutable ran = false + let t = + runtimeTask { + try + require (not ran) "ran way early" + do! Task.Delay(100) + require (not ran) "ran kinda early" + failtest "uhoh" + finally + ran <- true + } + try + t.Wait() + with + | _ -> () + require ran "never ran" + +let knownFailing_testTryFinallyCaught () = + for i in 1 .. 5 do + let mutable ran = false + let t = + runtimeTask { + try + try + require (not ran) "ran way early" + do! Task.Delay(100) + require (not ran) "ran kinda early" + failtest "uhoh" + finally + ran <- true + return 1 + with + | _ -> return 2 + } + require (t.Result = 2) "wrong return" + require ran "never ran" + +let knownFailing_testUsing () = + for i in 1 .. 5 do + let mutable disposed = false + let t = + runtimeTask { + use d = { new IDisposable with member _.Dispose() = disposed <- true } + require (not disposed) "disposed way early" + do! Task.Delay(100) + require (not disposed) "disposed kinda early" + } + t.Wait() + require disposed "never disposed B" + +let knownFailing_testUsingFromTask () = + let mutable disposedInner = false + let mutable disposed = false + let t = + runtimeTask { + use! d = + runtimeTask { + do! Task.Delay(50) + use i = { new IDisposable with member _.Dispose() = disposedInner <- true } + require (not disposed && not disposedInner) "disposed inner early" + return { new IDisposable with member _.Dispose() = disposed <- true } + } + require disposedInner "did not dispose inner after task completion" + require (not disposed) "disposed way early" + do! Task.Delay(50) + require (not disposed) "disposed kinda early" + } + t.Wait() + require disposed "never disposed C" + +let knownFailing_testUsingSadPath () = + let mutable disposedInner = false + let mutable disposed = false + let t = + runtimeTask { + try + use! d = + runtimeTask { + do! Task.Delay(50) + use i = { new IDisposable with member _.Dispose() = disposedInner <- true } + failtest "uhoh" + require (not disposed && not disposedInner) "disposed inner early" + return { new IDisposable with member _.Dispose() = disposed <- true } + } + () + with + | TestException msg -> + require disposedInner "did not dispose inner after task completion" + require (not disposed) "disposed way early" + do! Task.Delay(50) + require (not disposed) "disposed kinda early" + } + t.Wait() + require (not disposed) "disposed thing that never should've existed" + +let testUsingAsyncDisposableSync () = + for i in 1 .. 5 do + let mutable disposed = 0 + let t = + runtimeTask { + use d = + { new IAsyncDisposable with + member _.DisposeAsync() = + runtimeTask { + disposed <- disposed + 1 } + |> ValueTask + } + require (disposed = 0) "disposed way early" + do! Task.Delay(100) + require (disposed = 0) "disposed kinda early" + } + t.Wait() + require (disposed >= 1) "never disposed B" + require (disposed <= 1) "too many dispose on B" + +let knownFailing_testExceptionThrownInFinally () = + for i in 1 .. 5 do + use stepOutside = new SemaphoreSlim(0) + use ranInitial = new ManualResetEventSlim() + use ranNext = new ManualResetEventSlim() + let mutable ranFinally = 0 + let t = + runtimeTask { + try + ranInitial.Set() + do! Task.Yield() + do! stepOutside.WaitAsync() + ranNext.Set() + finally + ranFinally <- ranFinally + 1 + failtest "finally exn!" + } + require ranInitial.IsSet "didn't run initial" + require (not ranNext.IsSet) "ran next too early" + stepOutside.Release() |> ignore + try + t.Wait() + require false "shouldn't get here" + with + | _ -> () + require ranNext.IsSet "didn't run next" + require (ranFinally = 1) "didn't run finally exactly once" + +let knownFailing_test2ndExceptionThrownInFinally () = + for i in 1 .. 5 do + use ranInitial = new ManualResetEventSlim() + use continueTask = new SemaphoreSlim(0) + use ranNext = new ManualResetEventSlim() + let mutable ranFinally = 0 + let t = + runtimeTask { + try + ranInitial.Set() + do! continueTask.WaitAsync() + ranNext.Set() + do! Task.Yield() + failtest "uhoh" + finally + ranFinally <- ranFinally + 1 + failtest "2nd exn!" + } + ranInitial.Wait() + continueTask.Release() |> ignore + try + t.Wait() + require false "shouldn't get here" + with + | _ -> () + require ranNext.IsSet "didn't run next" + require (ranFinally = 1) "didn't run finally exactly once" + +let knownFailing_testTryFinallyOverReturnFromWithException () = + let inner() = + runtimeTask { + do! Task.Yield() + failtest "inner" + return 1 + } + let mutable m = 0 + let t = + runtimeTask { + try + do! Task.Yield() + return! inner() + finally + m <- 1 + } + try + t.Wait() + with + | :? AggregateException -> () + require (m = 1) "didn't run finally" + +let knownFailing_testTryFinallyOverReturnFromWithoutException () = + let inner() = + runtimeTask { + do! Task.Yield() + return 1 + } + let mutable m = 0 + let t = + runtimeTask { + try + do! Task.Yield() + return! inner() + finally + m <- 1 + } + try + t.Wait() + with + | :? AggregateException -> () + require (m = 1) "didn't run finally" + +// A minimal custom awaitable, exercising the SRTP Bind/ReturnFrom/MergeSources +// fallbacks (task {} supports arbitrary task-likes the same way). +type CustomAwaitable(result: int) = + member _.GetAwaiter() = (Task.FromResult result).GetAwaiter() + +let testCustomAwaitable () = + let t = + runtimeTask { + let! x = CustomAwaitable 20 + let! y = CustomAwaitable 20 + return x + y + } + require (t.Result = 40) "custom awaitable bind" + + let t2 = + runtimeTask { + return! CustomAwaitable 42 + } + require (t2.Result = 42) "custom awaitable return from" + + let t3 = + runtimeTask { + let! x = CustomAwaitable 20 + and! y = CustomAwaitable 22 + return x + y + } + require (t3.Result = 42) "custom awaitable merge sources" + +let knownFailing_testTaskUsesSyncContext () = // task completes without the body observably running when a SynchronizationContext is installed + for i in 1 .. 5 do + let mutable ran = false + let mutable posted = false + let oldSyncContext = SynchronizationContext.Current + let syncContext = { new SynchronizationContext() with member _.Post(d,state) = posted <- true; d.Invoke(state) } + try + SynchronizationContext.SetSynchronizationContext syncContext + let tid = System.Threading.Thread.CurrentThread.ManagedThreadId + require (not (isNull SynchronizationContext.Current)) "need sync context non null on foreground thread A" + require (SynchronizationContext.Current = syncContext) "need sync context known on foreground thread A" + let t = + runtimeTask { + let tid2 = System.Threading.Thread.CurrentThread.ManagedThreadId + require (not (isNull SynchronizationContext.Current)) "need sync context non null on foreground thread B" + require (SynchronizationContext.Current = syncContext) "need sync context known on foreground thread B" + do! Task.Yield() + require (not (isNull SynchronizationContext.Current)) "need sync context non null on foreground thread C" + require (SynchronizationContext.Current = syncContext) "need sync context known on foreground thread C" + ran <- true + } + t.Wait() + require ran "never ran" + require posted "never posted" + finally + SynchronizationContext.SetSynchronizationContext oldSyncContext + +[] +let main _ = + tinyTask() + tbind() + tnested() + tcatch0() + tcatch1() + t3() + t3b() + t3c() + t67() + t68() + testCompileAsyncWhileLoop() + merge2tasks() + merge3tasks() + mergeYieldAndTask() + mergeTaskAndYield() + merge2valueTasks() + merge2valueTasksAndYield() + mergeYieldAnd2tasks() + merge2tasksAndValueTask() + merge2asyncs() + merge3asyncs() + mergeYieldAndAsync() + mergeAsyncAndYield() + mergeYieldAnd2asyncs() + merge2asyncsAndValueTask() + testShortCircuitResult() + testDelay() + testNonBlocking() + testWhileLoopSync() + testWhileLoopAsyncZeroIteration() + testWhileLoopAsyncOneIteration() + testWhileLoopAsync() + testForLoopA() + testForLoopComplex() + testForLoopSadPath() + testFixedStackWhileLoop() + testTypeInference() + testNoStackOverflowWithImmediateResult() + testNoStackOverflowWithYieldResult() + testSmallTailRecursion() + testTryOverReturnFrom() + testAsyncsMixedWithTasks() + testAsyncsMixedWithTasks_ShouldNotSwitchContext() + testCustomAwaitable() + testUsingAsyncDisposableSync() + 0 diff --git a/tests/FSharp.Compiler.ComponentTests/Language/RuntimeAsync/RuntimeTasksAsyncDisposalException.fs b/tests/FSharp.Compiler.ComponentTests/Language/RuntimeAsync/RuntimeTasksAsyncDisposalException.fs new file mode 100644 index 00000000000..61feac5e73d --- /dev/null +++ b/tests/FSharp.Compiler.ComponentTests/Language/RuntimeAsync/RuntimeTasksAsyncDisposalException.fs @@ -0,0 +1,25 @@ +// Minimal repro: suspending with AsyncHelpers.Await inside the *handler* of an +// exception-handling region of a __runtimeAsync method. This is what `use` on an +// IAsyncDisposable lowers to (the DisposeAsync await sits in the finally). +// +// Today this compiles cleanly but terminates the process at execution +// (0xC0000409), so the component test compiles this file without running it. +// Awaiting in the try *body* with a plain finally works; awaiting inside the +// finally itself does not. +module RuntimeAsyncAwaitInExceptionRegion + +open System.Runtime.CompilerServices +open System.Threading.Tasks +open Microsoft.FSharp.Core.CompilerServices + +let run () : Task = + StateMachineHelpers.__runtimeAsync ( + try + 1 + finally + AsyncHelpers.Await(Task.Delay(1)) + ) + +[] +let main _ = + if (run ()).GetAwaiter().GetResult() = 1 then 0 else 1 diff --git a/tests/FSharp.Compiler.ComponentTests/Language/RuntimeAsyncTests.fs b/tests/FSharp.Compiler.ComponentTests/Language/RuntimeAsyncTests.fs new file mode 100644 index 00000000000..6aec6ffaec2 --- /dev/null +++ b/tests/FSharp.Compiler.ComponentTests/Language/RuntimeAsyncTests.fs @@ -0,0 +1,178 @@ +module Language.RuntimeAsyncTests + +open Xunit +open FSharp.Test.Compiler +open System.IO + +let private runtimeAsyncSource = """ +module RuntimeAsyncTest + +open System.Threading.Tasks +open System.Runtime.CompilerServices +open Microsoft.FSharp.Core.CompilerServices + +let add (x: int) (y: int) : Task = + StateMachineHelpers.__runtimeAsync ( + AsyncHelpers.Await(Task.Delay(1)) + x + y) + +let rawBody () : Task = + StateMachineHelpers.__runtimeAsync 1 + +type Calculator() = + member _.Add(x: int, y: int) : Task = + StateMachineHelpers.__runtimeAsync ( + AsyncHelpers.Await(Task.Delay(1)) + x + y) + + member _.AddRaw(x: int) : Task = + StateMachineHelpers.__runtimeAsync (x + 1) +""" + +let private runtimeAsyncRawSource = """ +module RuntimeAsyncRawTest + +open System.Threading.Tasks +open Microsoft.FSharp.Core.CompilerServices +open System.Runtime.CompilerServices + +type RuntimeTaskBuilder() = + member inline _.Delay([] generator: unit -> 'T) = + generator + + member inline _.Run([] code: unit -> 'T) : Task<'T> = + StateMachineHelpers.__runtimeAsync (code()) + + member inline _.Zero() = () + + member inline _.Return(value: 'T) = value + + member inline _.Bind(task: Task, [] continuation: unit -> 'U) = + AsyncHelpers.Await task + continuation() + + member inline _.Combine( + [] first: unit -> unit, + [] second: unit -> 'T + ) = + first() + second() + +[] +module RuntimeTask = + let runtimeTask = RuntimeTaskBuilder() + +type ICalculator = + abstract Combined: unit -> Task + +type Calculator() = + member _.Combined() : Task = + runtimeTask { + do! Task.Delay(1) + do! Task.Delay(1) + return 42 + } + + interface ICalculator with + member this.Combined() = this.Combined() + +""" + +[] +let ``runtime async requires preview language version`` () = + FSharp """ +open System.Threading.Tasks +open Microsoft.FSharp.Core.CompilerServices + +let f : Task = + StateMachineHelpers.__runtimeAsync 1 +""" + |> typecheck + |> shouldFail + |> withErrorCode 3350 + +[] +let ``runtime async rejects non Task result carriers`` () = + FSharp """ +open Microsoft.FSharp.Core.CompilerServices + +let f : string = + StateMachineHelpers.__runtimeAsync "result" +""" + |> withLangVersionPreview + |> typecheck + |> shouldFail + |> withErrorCode 1 + +[] +let ``runtime async intrinsic does not capture user-defined same-named values`` () = + FSharp """ +let __runtimeAsync value = value +let result = __runtimeAsync 1 +""" + |> typecheck + |> shouldSucceed + +#if NETCOREAPP +[] +let ``runtime async compiles functions and members`` () = + FSharp runtimeAsyncSource + |> withLangVersionPreview + |> compile + |> shouldSucceed + +[] +let ``runtime async combines awaited chunks without delegates`` () = + FSharp runtimeAsyncRawSource + |> withLangVersionPreview + |> compile + |> verifyILContains [ + "Task::Delay(int32)" + "AsyncHelpers::Await(class [runtime]System.Threading.Tasks.Task)" + ] + |> shouldSucceed + +[] +let ``runtime task builder fixture executes through runtime async`` () = + FsFromPath (Path.Combine(__SOURCE_DIRECTORY__, "RuntimeAsync", "RuntimeTaskBuilder.fs")) + |> withAdditionalSourceFile ( + SourceFromPath (Path.Combine(__SOURCE_DIRECTORY__, "RuntimeAsync", "RuntimeTasks.fs")) + ) + |> withLangVersionPreview + |> compileExeAndRun + |> shouldSucceed + +[] +let ``runtime async direct intrinsic fixture executes`` () = + Path.Combine(__SOURCE_DIRECTORY__, "RuntimeAsync", "RuntimeAsyncBasic.fs") + |> FsFromPath + |> withLangVersionPreview + |> compileExeAndRun + |> shouldSucceed + +[] +// Minimal repro: awaiting inside an exception-handling region. Compilation +// succeeds, but executing the fixture currently terminates the process with +// 0xC0000409 (suspension in EH regions is forbidden by the runtime contract). +let ``runtime async suspension in exception region compiles (runtime execution is failing)`` () = + Path.Combine(__SOURCE_DIRECTORY__, "RuntimeAsync", "RuntimeTasksAsyncDisposalException.fs") + |> FsFromPath + |> withLangVersionPreview + |> compileExeAndRun + |> shouldSucceed + +#else +[] +let ``runtime async reports unsupported target runtime`` () = + FSharp """ +open System.Threading.Tasks +open Microsoft.FSharp.Core.CompilerServices + +let f : Task = + StateMachineHelpers.__runtimeAsync 1 +""" + |> withLangVersionPreview + |> typecheck + |> shouldFail + |> withErrorCode 3351 +#endif From f9bb48341c9948f831b25de6e93e99f3fe12f4fb Mon Sep 17 00:00:00 2001 From: majocha <1760221+majocha@users.noreply.github.com> Date: Sat, 8 Aug 2026 08:41:34 +0200 Subject: [PATCH 4/9] surface area --- .../FSharp.Core.SurfaceArea.netstandard21.release.bsl | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/FSharp.Core.UnitTests/FSharp.Core.SurfaceArea.netstandard21.release.bsl b/tests/FSharp.Core.UnitTests/FSharp.Core.SurfaceArea.netstandard21.release.bsl index ed913ea04d3..411ac2e63e0 100644 --- a/tests/FSharp.Core.UnitTests/FSharp.Core.SurfaceArea.netstandard21.release.bsl +++ b/tests/FSharp.Core.UnitTests/FSharp.Core.SurfaceArea.netstandard21.release.bsl @@ -964,6 +964,7 @@ Microsoft.FSharp.Core.CompilerServices.SetStateMachineMethodImpl`1[TData]: Void Microsoft.FSharp.Core.CompilerServices.StateMachineHelpers: Boolean __useResumableCode[T]() Microsoft.FSharp.Core.CompilerServices.StateMachineHelpers: Microsoft.FSharp.Core.FSharpOption`1[System.Int32] __resumableEntry() Microsoft.FSharp.Core.CompilerServices.StateMachineHelpers: T __resumeAt[T](Int32) +Microsoft.FSharp.Core.CompilerServices.StateMachineHelpers: System.Threading.Tasks.Task`1[T] __runtimeAsync[T](T) Microsoft.FSharp.Core.CompilerServices.StateMachineHelpers: TResult __stateMachine[TData,TResult](Microsoft.FSharp.Core.CompilerServices.MoveNextMethodImpl`1[TData], Microsoft.FSharp.Core.CompilerServices.SetStateMachineMethodImpl`1[TData], Microsoft.FSharp.Core.CompilerServices.AfterCode`2[TData,TResult]) Microsoft.FSharp.Core.CompilerServices.StateMachineHelpers: Void __debugPoint(System.String) Microsoft.FSharp.Core.CompilerServices.TypeProviderAssemblyAttribute: System.String AssemblyName From dfe4109390e728b75a64ea215663c78050738a0a Mon Sep 17 00:00:00 2001 From: majocha <1760221+majocha@users.noreply.github.com> Date: Sat, 8 Aug 2026 08:41:58 +0200 Subject: [PATCH 5/9] rns --- docs/release-notes/.FSharp.Compiler.Service/11.0.100.md | 1 + docs/release-notes/.FSharp.Core/11.0.100.md | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md b/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md index 0f816258bbf..2b7f464b9e4 100644 --- a/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md +++ b/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md @@ -137,6 +137,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 `__runtimeAsync` intrinsic, including carrier validation and target-runtime capability checks. * 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)) diff --git a/docs/release-notes/.FSharp.Core/11.0.100.md b/docs/release-notes/.FSharp.Core/11.0.100.md index 3349ac75260..1941c8e9f6f 100644 --- a/docs/release-notes/.FSharp.Core/11.0.100.md +++ b/docs/release-notes/.FSharp.Core/11.0.100.md @@ -4,3 +4,7 @@ * Fix `Array.exists2` documentation examples to use equal-length arrays; the previous examples would throw `ArgumentException` at runtime instead of returning the documented `false`/`true` values. ([PR #19672](https://github.com/dotnet/fsharp/pull/19672)) * Move `Async.StartChild` to the "Starting Async Computations" docs category alongside `Async.StartChildAsTask`. ([Issue #19667](https://github.com/dotnet/fsharp/issues/19667)) * Add `InlineIfLambda` to `Array.init` ([PR #19869](https://github.com/dotnet/fsharp/pull/19869)) + +### Added + +* Add the compiler-recognized `StateMachineHelpers.__runtimeAsync` intrinsic for .NET runtime-async methods. From e4eb267327f51483c2034336f676efed6144877e Mon Sep 17 00:00:00 2001 From: majocha <1760221+majocha@users.noreply.github.com> Date: Sat, 8 Aug 2026 09:33:16 +0200 Subject: [PATCH 6/9] translations --- src/Compiler/xlf/FSComp.txt.cs.xlf | 5 +++++ src/Compiler/xlf/FSComp.txt.de.xlf | 5 +++++ src/Compiler/xlf/FSComp.txt.es.xlf | 5 +++++ src/Compiler/xlf/FSComp.txt.fr.xlf | 5 +++++ src/Compiler/xlf/FSComp.txt.it.xlf | 5 +++++ src/Compiler/xlf/FSComp.txt.ja.xlf | 5 +++++ src/Compiler/xlf/FSComp.txt.ko.xlf | 5 +++++ src/Compiler/xlf/FSComp.txt.pl.xlf | 5 +++++ src/Compiler/xlf/FSComp.txt.pt-BR.xlf | 5 +++++ src/Compiler/xlf/FSComp.txt.ru.xlf | 5 +++++ src/Compiler/xlf/FSComp.txt.tr.xlf | 5 +++++ src/Compiler/xlf/FSComp.txt.zh-Hans.xlf | 5 +++++ src/Compiler/xlf/FSComp.txt.zh-Hant.xlf | 5 +++++ 13 files changed, 65 insertions(+) diff --git a/src/Compiler/xlf/FSComp.txt.cs.xlf b/src/Compiler/xlf/FSComp.txt.cs.xlf index afb00396c5d..82911a6b995 100644 --- a/src/Compiler/xlf/FSComp.txt.cs.xlf +++ b/src/Compiler/xlf/FSComp.txt.cs.xlf @@ -652,6 +652,11 @@ Sdílení podkladových polí v rozlišeném sjednocení [<Struct>] za předpokladu, že mají stejný název a typ + + runtime async + runtime async + + Support for scoped enabling / disabling of warnings by #warn and #nowarn directives, also inside modules Support for scoped enabling / disabling of warnings by #warn and #nowarn directives, also inside modules diff --git a/src/Compiler/xlf/FSComp.txt.de.xlf b/src/Compiler/xlf/FSComp.txt.de.xlf index 222cab682fd..4b5118178fc 100644 --- a/src/Compiler/xlf/FSComp.txt.de.xlf +++ b/src/Compiler/xlf/FSComp.txt.de.xlf @@ -652,6 +652,11 @@ Teilen sie zugrunde liegende Felder in einen [<Struct>]-diskriminierten Union, solange sie denselben Namen und Typ aufweisen. + + runtime async + runtime async + + Support for scoped enabling / disabling of warnings by #warn and #nowarn directives, also inside modules Support for scoped enabling / disabling of warnings by #warn and #nowarn directives, also inside modules diff --git a/src/Compiler/xlf/FSComp.txt.es.xlf b/src/Compiler/xlf/FSComp.txt.es.xlf index d3cbfab11f1..17a49aefae0 100644 --- a/src/Compiler/xlf/FSComp.txt.es.xlf +++ b/src/Compiler/xlf/FSComp.txt.es.xlf @@ -652,6 +652,11 @@ Compartir campos subyacentes en una unión discriminada [<Struct>] siempre y cuando tengan el mismo nombre y tipo + + runtime async + runtime async + + Support for scoped enabling / disabling of warnings by #warn and #nowarn directives, also inside modules Support for scoped enabling / disabling of warnings by #warn and #nowarn directives, also inside modules diff --git a/src/Compiler/xlf/FSComp.txt.fr.xlf b/src/Compiler/xlf/FSComp.txt.fr.xlf index 59712f7a2f5..d3e097f1da1 100644 --- a/src/Compiler/xlf/FSComp.txt.fr.xlf +++ b/src/Compiler/xlf/FSComp.txt.fr.xlf @@ -652,6 +652,11 @@ Partager les champs sous-jacents dans une union discriminée [<Struct>] tant qu’ils ont le même nom et le même type + + runtime async + runtime async + + Support for scoped enabling / disabling of warnings by #warn and #nowarn directives, also inside modules Support for scoped enabling / disabling of warnings by #warn and #nowarn directives, also inside modules diff --git a/src/Compiler/xlf/FSComp.txt.it.xlf b/src/Compiler/xlf/FSComp.txt.it.xlf index 92de61c5737..df2d64bbedf 100644 --- a/src/Compiler/xlf/FSComp.txt.it.xlf +++ b/src/Compiler/xlf/FSComp.txt.it.xlf @@ -652,6 +652,11 @@ Condividi i campi sottostanti in un'unione discriminata di [<Struct>] purché abbiano lo stesso nome e tipo + + runtime async + runtime async + + Support for scoped enabling / disabling of warnings by #warn and #nowarn directives, also inside modules Support for scoped enabling / disabling of warnings by #warn and #nowarn directives, also inside modules diff --git a/src/Compiler/xlf/FSComp.txt.ja.xlf b/src/Compiler/xlf/FSComp.txt.ja.xlf index b4f048784cb..8c2537747a0 100644 --- a/src/Compiler/xlf/FSComp.txt.ja.xlf +++ b/src/Compiler/xlf/FSComp.txt.ja.xlf @@ -652,6 +652,11 @@ 名前と型が同じである限り、[<Struct>] 判別可能な共用体で基になるフィールドを共有する + + runtime async + runtime async + + Support for scoped enabling / disabling of warnings by #warn and #nowarn directives, also inside modules Support for scoped enabling / disabling of warnings by #warn and #nowarn directives, also inside modules diff --git a/src/Compiler/xlf/FSComp.txt.ko.xlf b/src/Compiler/xlf/FSComp.txt.ko.xlf index 9d12c1d82b0..4a14347838c 100644 --- a/src/Compiler/xlf/FSComp.txt.ko.xlf +++ b/src/Compiler/xlf/FSComp.txt.ko.xlf @@ -652,6 +652,11 @@ 이름과 형식이 같으면 [<Struct>] 구분된 공용 구조체에서 기본 필드 공유 + + runtime async + runtime async + + Support for scoped enabling / disabling of warnings by #warn and #nowarn directives, also inside modules Support for scoped enabling / disabling of warnings by #warn and #nowarn directives, also inside modules diff --git a/src/Compiler/xlf/FSComp.txt.pl.xlf b/src/Compiler/xlf/FSComp.txt.pl.xlf index 4ecb28aa71f..d099556e076 100644 --- a/src/Compiler/xlf/FSComp.txt.pl.xlf +++ b/src/Compiler/xlf/FSComp.txt.pl.xlf @@ -652,6 +652,11 @@ Udostępnij pola źródłowe w unii rozłącznej [<Struct>], o ile mają taką samą nazwę i ten sam typ + + runtime async + runtime async + + Support for scoped enabling / disabling of warnings by #warn and #nowarn directives, also inside modules Support for scoped enabling / disabling of warnings by #warn and #nowarn directives, also inside modules diff --git a/src/Compiler/xlf/FSComp.txt.pt-BR.xlf b/src/Compiler/xlf/FSComp.txt.pt-BR.xlf index 736ab22b139..921a3f062e5 100644 --- a/src/Compiler/xlf/FSComp.txt.pt-BR.xlf +++ b/src/Compiler/xlf/FSComp.txt.pt-BR.xlf @@ -652,6 +652,11 @@ Compartilhar campos subjacentes em uma união discriminada [<Struct>], desde que tenham o mesmo nome e tipo + + runtime async + runtime async + + Support for scoped enabling / disabling of warnings by #warn and #nowarn directives, also inside modules Support for scoped enabling / disabling of warnings by #warn and #nowarn directives, also inside modules diff --git a/src/Compiler/xlf/FSComp.txt.ru.xlf b/src/Compiler/xlf/FSComp.txt.ru.xlf index 170bcfc7385..50d8ade7920 100644 --- a/src/Compiler/xlf/FSComp.txt.ru.xlf +++ b/src/Compiler/xlf/FSComp.txt.ru.xlf @@ -652,6 +652,11 @@ Совместное использование базовых полей в дискриминируемом объединении [<Struct>], если они имеют одинаковое имя и тип. + + runtime async + runtime async + + Support for scoped enabling / disabling of warnings by #warn and #nowarn directives, also inside modules Support for scoped enabling / disabling of warnings by #warn and #nowarn directives, also inside modules diff --git a/src/Compiler/xlf/FSComp.txt.tr.xlf b/src/Compiler/xlf/FSComp.txt.tr.xlf index 5595108617e..750c6cc3c31 100644 --- a/src/Compiler/xlf/FSComp.txt.tr.xlf +++ b/src/Compiler/xlf/FSComp.txt.tr.xlf @@ -652,6 +652,11 @@ Aynı ada ve türe sahip oldukları sürece temel alınan alanları [<Struct>] ayırt edici birleşim biçiminde paylaşın + + runtime async + runtime async + + Support for scoped enabling / disabling of warnings by #warn and #nowarn directives, also inside modules Support for scoped enabling / disabling of warnings by #warn and #nowarn directives, also inside modules diff --git a/src/Compiler/xlf/FSComp.txt.zh-Hans.xlf b/src/Compiler/xlf/FSComp.txt.zh-Hans.xlf index c020d652bf0..e99c8a1a981 100644 --- a/src/Compiler/xlf/FSComp.txt.zh-Hans.xlf +++ b/src/Compiler/xlf/FSComp.txt.zh-Hans.xlf @@ -652,6 +652,11 @@ 只要它们具有相同的名称和类型,即可在 [<Struct>] 中共享基础字段 + + runtime async + runtime async + + Support for scoped enabling / disabling of warnings by #warn and #nowarn directives, also inside modules Support for scoped enabling / disabling of warnings by #warn and #nowarn directives, also inside modules diff --git a/src/Compiler/xlf/FSComp.txt.zh-Hant.xlf b/src/Compiler/xlf/FSComp.txt.zh-Hant.xlf index 8ed6744afb6..7af11175f44 100644 --- a/src/Compiler/xlf/FSComp.txt.zh-Hant.xlf +++ b/src/Compiler/xlf/FSComp.txt.zh-Hant.xlf @@ -652,6 +652,11 @@ 只要 [<Struct>] 具有相同名稱和類型,就以強制聯集共用基礎欄位 + + runtime async + runtime async + + Support for scoped enabling / disabling of warnings by #warn and #nowarn directives, also inside modules Support for scoped enabling / disabling of warnings by #warn and #nowarn directives, also inside modules From 38578d48e98e9ea7e9986f904b3e843de72a85b3 Mon Sep 17 00:00:00 2001 From: majocha <1760221+majocha@users.noreply.github.com> Date: Sat, 8 Aug 2026 09:39:51 +0200 Subject: [PATCH 7/9] add pr numbers --- docs/release-notes/.FSharp.Compiler.Service/11.0.100.md | 2 +- docs/release-notes/.FSharp.Core/11.0.100.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md b/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md index 68421f83ba8..da68192887f 100644 --- a/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md +++ b/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md @@ -138,7 +138,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 `__runtimeAsync` intrinsic, including carrier validation and target-runtime capability checks. +* Add preview compiler support for runtime-async `Task<'T>` methods through the `__runtimeAsync` intrinsic, including carrier validation and target-runtime capability checks. ([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)) diff --git a/docs/release-notes/.FSharp.Core/11.0.100.md b/docs/release-notes/.FSharp.Core/11.0.100.md index 2a17638c4d7..b873296b18b 100644 --- a/docs/release-notes/.FSharp.Core/11.0.100.md +++ b/docs/release-notes/.FSharp.Core/11.0.100.md @@ -7,5 +7,5 @@ ### Added -* Add the compiler-recognized `StateMachineHelpers.__runtimeAsync` intrinsic for .NET runtime-async methods. +* Add the compiler-recognized `StateMachineHelpers.__runtimeAsync` intrinsic for .NET runtime-async methods. ([PR #20235](https://github.com/dotnet/fsharp/pull/20235)) * `Async.RunSynchronouslyImmediate`: runs work on the calling thread until the first asynchronous suspension (as opposed to `RunSynchronously`, which immediately offloads if not on a background and/or threadpool thread). ([Issue #1042](https://github.com/fsharp/fslang-suggestions/issues/1042), [PR #19804](https://github.com/dotnet/fsharp/pull/19804)) From caaad4d4b08d3b0c5a31f1096ac88934f40785e2 Mon Sep 17 00:00:00 2001 From: majocha <1760221+majocha@users.noreply.github.com> Date: Sat, 8 Aug 2026 10:41:40 +0200 Subject: [PATCH 8/9] Restore F# 11.0 feature registrations dropped when adding RuntimeAsync; 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> --- docs/release-notes/.Language/preview.md | 1 + src/Compiler/Facilities/LanguageFeatures.fs | 8 +++++++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/docs/release-notes/.Language/preview.md b/docs/release-notes/.Language/preview.md index 41ffd8cf6fb..1513c19bceb 100644 --- a/docs/release-notes/.Language/preview.md +++ b/docs/release-notes/.Language/preview.md @@ -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)) * Allow constructing a record via its all-fields constructor, e.g. `MyRecord(a, b)`, with positional or named arguments (`RecordConstructorSyntax` preview feature). Accessibility matches `{ ... }` construction. ([Suggestion #722](https://github.com/fsharp/fslang-suggestions/issues/722), [RFC FS-1073](https://github.com/fsharp/fslang-design/blob/main/RFCs/FS-1073-record-constructors.md), [PR #19974](https://github.com/dotnet/fsharp/pull/19974)) ### Fixed diff --git a/src/Compiler/Facilities/LanguageFeatures.fs b/src/Compiler/Facilities/LanguageFeatures.fs index 28b2dd18b01..4074659eaf5 100644 --- a/src/Compiler/Facilities/LanguageFeatures.fs +++ b/src/Compiler/Facilities/LanguageFeatures.fs @@ -255,12 +255,18 @@ type LanguageVersion(versionText, ?disabledFeaturesArray: LanguageFeature array) LanguageFeature.ExceptionFieldSerializationSupport, languageVersion110 LanguageFeature.NotNullIfNotNull, languageVersion110 LanguageFeature.ImprovedImpliedArgumentNamesPartTwo, languageVersion110 - LanguageFeature.RuntimeAsync, previewVersion + LanguageFeature.ImplicitDIMCoverage, languageVersion110 + LanguageFeature.MethodOverloadsCache, languageVersion110 // Performance optimization for overload resolution + LanguageFeature.ErrorOnMissingSignatureAttribute, languageVersion110 // Turn FS3888 from warning into error + LanguageFeature.DirectDelegateConstruction, languageVersion110 + LanguageFeature.AccessProtectedBaseFieldFromClosure, languageVersion110 // #5302: read a protected base field from a closure + LanguageFeature.RecordSpreads, languageVersion110 // Difference between languageVersion110 and preview - 11.0 gets turned on automatically by picking a preview .NET 11 SDK // previewVersion is only when "preview" is specified explicitly in project files and users also need a preview SDK // F# preview + LanguageFeature.RuntimeAsync, previewVersion LanguageFeature.RecordConstructorSyntax, previewVersion // Allow constructing a record via its all-fields constructor, e.g. MyRecord(a, b) // Unfinished features that still need work before they can be assigned a release language version. From 48d1691df6714a316679df84b0e55df2df1b3c0c Mon Sep 17 00:00:00 2001 From: Jakub Majocha <1760221+majocha@users.noreply.github.com> Date: Sat, 8 Aug 2026 13:02:34 +0200 Subject: [PATCH 9/9] surface area --- .../FSharp.Core.SurfaceArea.netstandard20.release.bsl | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/FSharp.Core.UnitTests/FSharp.Core.SurfaceArea.netstandard20.release.bsl b/tests/FSharp.Core.UnitTests/FSharp.Core.SurfaceArea.netstandard20.release.bsl index 6d29205d290..975302eea93 100644 --- a/tests/FSharp.Core.UnitTests/FSharp.Core.SurfaceArea.netstandard20.release.bsl +++ b/tests/FSharp.Core.UnitTests/FSharp.Core.SurfaceArea.netstandard20.release.bsl @@ -603,8 +603,8 @@ Microsoft.FSharp.Collections.SetModule: Microsoft.FSharp.Collections.FSharpSet`1 Microsoft.FSharp.Collections.SetModule: Microsoft.FSharp.Collections.FSharpSet`1[T] UnionMany[T](System.Collections.Generic.IEnumerable`1[Microsoft.FSharp.Collections.FSharpSet`1[T]]) Microsoft.FSharp.Collections.SetModule: Microsoft.FSharp.Collections.FSharpSet`1[T] Union[T](Microsoft.FSharp.Collections.FSharpSet`1[T], Microsoft.FSharp.Collections.FSharpSet`1[T]) Microsoft.FSharp.Collections.SetModule: System.Collections.Generic.IEnumerable`1[T] ToSeq[T](Microsoft.FSharp.Collections.FSharpSet`1[T]) -Microsoft.FSharp.Collections.SetModule: System.Tuple`2[Microsoft.FSharp.Collections.FSharpSet`1[T],Microsoft.FSharp.Collections.FSharpSet`1[T]] Partition[T](Microsoft.FSharp.Core.FSharpFunc`2[T,System.Boolean], Microsoft.FSharp.Collections.FSharpSet`1[T]) Microsoft.FSharp.Collections.SetModule: System.Tuple`2[Microsoft.FSharp.Collections.FSharpSet`1[T1],Microsoft.FSharp.Collections.FSharpSet`1[T2]] PartitionWith[T,T1,T2](Microsoft.FSharp.Core.FSharpFunc`2[T,Microsoft.FSharp.Core.FSharpChoice`2[T1,T2]], Microsoft.FSharp.Collections.FSharpSet`1[T]) +Microsoft.FSharp.Collections.SetModule: System.Tuple`2[Microsoft.FSharp.Collections.FSharpSet`1[T],Microsoft.FSharp.Collections.FSharpSet`1[T]] Partition[T](Microsoft.FSharp.Core.FSharpFunc`2[T,System.Boolean], Microsoft.FSharp.Collections.FSharpSet`1[T]) Microsoft.FSharp.Collections.SetModule: T MaxElement[T](Microsoft.FSharp.Collections.FSharpSet`1[T]) Microsoft.FSharp.Collections.SetModule: T MinElement[T](Microsoft.FSharp.Collections.FSharpSet`1[T]) Microsoft.FSharp.Collections.SetModule: TState FoldBack[T,TState](Microsoft.FSharp.Core.FSharpFunc`2[T,Microsoft.FSharp.Core.FSharpFunc`2[TState,TState]], Microsoft.FSharp.Collections.FSharpSet`1[T], TState) @@ -671,8 +671,8 @@ Microsoft.FSharp.Control.FSharpAsync: System.Threading.CancellationToken get_Def Microsoft.FSharp.Control.FSharpAsync: System.Threading.Tasks.Task`1[T] StartAsTask[T](Microsoft.FSharp.Control.FSharpAsync`1[T], Microsoft.FSharp.Core.FSharpOption`1[System.Threading.Tasks.TaskCreationOptions], Microsoft.FSharp.Core.FSharpOption`1[System.Threading.CancellationToken]) Microsoft.FSharp.Control.FSharpAsync: System.Threading.Tasks.Task`1[T] StartImmediateAsTask[T](Microsoft.FSharp.Control.FSharpAsync`1[T], Microsoft.FSharp.Core.FSharpOption`1[System.Threading.CancellationToken]) Microsoft.FSharp.Control.FSharpAsync: System.Tuple`3[Microsoft.FSharp.Core.FSharpFunc`2[System.Tuple`3[TArg,System.AsyncCallback,System.Object],System.IAsyncResult],Microsoft.FSharp.Core.FSharpFunc`2[System.IAsyncResult,T],Microsoft.FSharp.Core.FSharpFunc`2[System.IAsyncResult,Microsoft.FSharp.Core.Unit]] AsBeginEnd[TArg,T](Microsoft.FSharp.Core.FSharpFunc`2[TArg,Microsoft.FSharp.Control.FSharpAsync`1[T]]) -Microsoft.FSharp.Control.FSharpAsync: T RunSynchronously[T](Microsoft.FSharp.Control.FSharpAsync`1[T], Microsoft.FSharp.Core.FSharpOption`1[System.Int32], Microsoft.FSharp.Core.FSharpOption`1[System.Threading.CancellationToken]) Microsoft.FSharp.Control.FSharpAsync: T RunSynchronouslyImmediate[T](Microsoft.FSharp.Control.FSharpAsync`1[T], Microsoft.FSharp.Core.FSharpOption`1[System.Threading.CancellationToken]) +Microsoft.FSharp.Control.FSharpAsync: T RunSynchronously[T](Microsoft.FSharp.Control.FSharpAsync`1[T], Microsoft.FSharp.Core.FSharpOption`1[System.Int32], Microsoft.FSharp.Core.FSharpOption`1[System.Threading.CancellationToken]) Microsoft.FSharp.Control.FSharpAsync: Void CancelDefaultToken() Microsoft.FSharp.Control.FSharpAsync: Void Start(Microsoft.FSharp.Control.FSharpAsync`1[Microsoft.FSharp.Core.Unit], Microsoft.FSharp.Core.FSharpOption`1[System.Threading.CancellationToken]) Microsoft.FSharp.Control.FSharpAsync: Void StartImmediate(Microsoft.FSharp.Control.FSharpAsync`1[Microsoft.FSharp.Core.Unit], Microsoft.FSharp.Core.FSharpOption`1[System.Threading.CancellationToken]) @@ -961,6 +961,7 @@ Microsoft.FSharp.Core.CompilerServices.SetStateMachineMethodImpl`1[TData]: Void Microsoft.FSharp.Core.CompilerServices.SetStateMachineMethodImpl`1[TData]: Void Invoke(Microsoft.FSharp.Core.CompilerServices.ResumableStateMachine`1[TData] ByRef, System.Runtime.CompilerServices.IAsyncStateMachine) Microsoft.FSharp.Core.CompilerServices.StateMachineHelpers: Boolean __useResumableCode[T]() Microsoft.FSharp.Core.CompilerServices.StateMachineHelpers: Microsoft.FSharp.Core.FSharpOption`1[System.Int32] __resumableEntry() +Microsoft.FSharp.Core.CompilerServices.StateMachineHelpers: System.Threading.Tasks.Task`1[T] __runtimeAsync[T](T) Microsoft.FSharp.Core.CompilerServices.StateMachineHelpers: T __resumeAt[T](Int32) Microsoft.FSharp.Core.CompilerServices.StateMachineHelpers: TResult __stateMachine[TData,TResult](Microsoft.FSharp.Core.CompilerServices.MoveNextMethodImpl`1[TData], Microsoft.FSharp.Core.CompilerServices.SetStateMachineMethodImpl`1[TData], Microsoft.FSharp.Core.CompilerServices.AfterCode`2[TData,TResult]) Microsoft.FSharp.Core.CompilerServices.StateMachineHelpers: Void __debugPoint(System.String)