diff --git a/docs/release-notes/.FSharp.Compiler.Service/11.0.200.md b/docs/release-notes/.FSharp.Compiler.Service/11.0.200.md index 4cd1275d7d6..57a4981e417 100644 --- a/docs/release-notes/.FSharp.Compiler.Service/11.0.200.md +++ b/docs/release-notes/.FSharp.Compiler.Service/11.0.200.md @@ -8,6 +8,7 @@ * Split Linux CoreCLR CI into two parallel test batches to reduce test turnaround time. ([PR #20587](https://github.com/dotnet/fsharp/pull/20587)) * Fix `NativePtr.stackalloc` used inside the `with` handler, filter or `finally` block of a `try` expression producing an assembly that throws `InvalidProgramException` at method load. Direct or mandatory-inline allocations are now reported at compile time as error FS3924; calls to non-inline allocation helpers, including handler-local helpers, remain legal and retain their method boundary during inlining and let-binding elimination. ([Issue #20295](https://github.com/dotnet/fsharp/issues/20295), [PR #20491](https://github.com/dotnet/fsharp/pull/20491)) * `FsiEvaluationSession.EvalInteraction` and `EvalInteractionNonThrowing` evaluate every `;;`-separated interaction in the text they are given, as their documentation has always said, instead of silently discarding everything after the first. ([PR #20396](https://github.com/dotnet/fsharp/pull/20396)) +* Preserve stateful conditional callbacks in runtime-async inline expansions by outlining residual suspending functions and delegates through a FSharp.Core template. Construction runs once, mutable captures remain shared, and each invocation awaits the selected fragment and restores its final ambient contexts before continuing or rethrowing. This removes callback-body copying and its size budget. The additional async boundary produces context-change notifications and represents captured locals as closure fields instead of parent-local debugger aliases. ([Issue #20577](https://github.com/dotnet/fsharp/issues/20577), [PR #20647](https://github.com/dotnet/fsharp/pull/20647)) * Prevent `InvalidProgramException` when runtime async awaits a `Task` upcast to `Task`, including through `ConfigureAwait`. ([Issue #20686](https://github.com/dotnet/fsharp/issues/20686), [PR #20726](https://github.com/dotnet/fsharp/pull/20726)) * Preserve constraint inference order when explicit generic arguments share a caller type parameter, avoiding spurious FS0001 errors with multiply implemented generic interfaces. ([PR #20491](https://github.com/dotnet/fsharp/pull/20491)) * Build parser diagnostic messages without runtime printf specialization or reflective function construction, so Native AOT hosts report indentation and invalid-character diagnostics instead of failing during formatting. Preserve `#r` reference tooltips while script project options remain alive, including after garbage collection or cache eviction. ([PR #20658](https://github.com/dotnet/fsharp/pull/20658)) diff --git a/docs/release-notes/.FSharp.Core/11.0.200.md b/docs/release-notes/.FSharp.Core/11.0.200.md index 3948a0f42b4..9658ee10c47 100644 --- a/docs/release-notes/.FSharp.Core/11.0.200.md +++ b/docs/release-notes/.FSharp.Core/11.0.200.md @@ -1,5 +1,7 @@ ### Added +* Add the runtime-async outlining template and a context-restoring fragment awaiter to the net10.0 asset. Fragment outcomes preserve final execution and synchronization contexts, flow-suppression state, and captured exceptions. ([PR #20647](https://github.com/dotnet/fsharp/pull/20647)) + ### Fixed ### Changed diff --git a/docs/runtime-async.md b/docs/runtime-async.md index a7670bc8517..ea313f6f070 100644 --- a/docs/runtime-async.md +++ b/docs/runtime-async.md @@ -48,12 +48,14 @@ instructions, including before `ConfigureAwait`. These casts prevent the runtime from incorrectly optimizing a generic task-producing call followed by a non-generic await. -Known runtime restrictions (currently **not** diagnosed by the F# compiler): - -* `tail.` and `localloc` are forbidden. -* generated suspension points cannot occur inside exception-handling regions. - Awaiting in a protected `try` body now works on the current runtime. Direct - intrinsic bodies rewrite suspending `try/with` handlers and filters, and +Runtime restrictions and their compiler handling: + +* `tail.` is forbidden; runtime-async methods suppress tail-call prefixes. +* `localloc` is forbidden and reported as FS3920, even without a suspension. +* `MethodImplOptions.Synchronized` is rejected with FS3921. +* Suspension cannot occur inside exception handlers, filters, or `finally` + blocks. Awaiting in a protected `try` body is supported. Return-marker + bodies rewrite suspending `try/with` handlers and filters, and `try/finally` compensations, so the suspension runs outside the EH region. C# avoids this by rewriting EH-region awaits at lowering time (see the @@ -64,8 +66,11 @@ Known runtime restrictions (currently **not** diagnosed by the F# compiler): runs `DisposeAsync` (possibly suspending) *outside* the `try`, then restores the pending exception. This makes `use` on an `IAsyncDisposable` work under runtime async. + Byref, byref-like, and pinned locals that are used after a suspension are -rejected with diagnostic FS3917. +rejected with diagnostic FS3919 when their non-preservable provenance is +tracked. Pinned provenance is not currently propagated through ref cells or +captured delegates, so those uses are not reliably diagnosed. Calls to `AsyncHelpers` suspension methods emitted outside a runtime-async method are rejected during code generation. Explicitly `inline` method bodies @@ -85,14 +90,14 @@ val __runtimeAsyncReturnUnit : unit -> System.Threading.Tasks.Task val __runtimeAsyncReturnValueTaskUnit : unit -> System.Threading.Tasks.ValueTask ``` -Their FSharp.Core implementations throw; the compiler consumes every -occurrence before code generation, so those bodies are never executed. They -are marked `NoInlining` so a missed consumption does not silently fold into a -caller. +Their FSharp.Core implementations throw; code generation consumes recognized +marker applications instead of emitting calls to those stubs. They are 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). +The feature is available from F# 11.2, including `default`, `latest`, and +`preview` (`LanguageFeature.RuntimeAsync`), and gated 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. @@ -139,35 +144,114 @@ declaration. ## Optimization `Optimizer.fs` preserves the marker application as-is, optimizing its -argument and rewriting any suspending exception handlers in that argument. -The marked expression is forced to `HasEffect = true` and `UnknownValue`, so -the optimizer never inlines, duplicates, or discards it. The marker therefore -survives optimization as an ordinary `Expr.App` node; nothing else in the -typed tree records that a method is runtime-async. +argument. The marked expression is forced to `HasEffect = true` and +`UnknownValue`, so ordinary value propagation cannot treat it as a pure result. +The marker therefore survives optimization as an ordinary `Expr.App` node; +nothing else in the typed tree records that a method is runtime-async. Inline values whose bodies contain a return marker or an `AsyncHelpers` suspension are recursively specialized at their call sites, including when optimization is disabled. The analysis follows inline and local values with a cycle guard, and `InlineIfLambda` arguments are forced through when the caller -is already in a runtime-async context. The optimizer follows nested inline -calls and does not create a generated helper method for the specialized -suspension fragment, keeping every suspension in the eventual runtime-async -method. - -After specialization, lambda arguments are substituted and their applications -are beta-reduced before and after runtime-async reoptimization. This includes -debug-point-wrapped lambdas, compiler-generated `let` wrappers, curried -applications, and multi-argument lambdas. -That step is required for computation-expression shapes where `Bind` returns a -closure containing `Await`, and later `Combine`/`Delay` calls apply that closure. +is already in a runtime-async context. Ordinary inlining is tried first; +applications of returned lambdas are reduced when their remaining arguments +can safely move across the returned closure's construction. This handles +computation-expression shapes where `Bind` returns a closure containing +`Await`, and later `Combine`/`Delay` calls apply it. Inside a runtime-async expansion, captures +that wrap an `InlineIfLambda` lambda or delegate (`let p = (let c = e in fun +...)`) are floated above the binding so the callback can be inlined; they are +still evaluated at the same point. + +## Runtime-async lowering + +`LowerRuntimeAsync.fs` runs once per file after the first optimization loop, +so it sees the final inlined shape rather than an intermediate optimizer +state, and before `LowerLocalMutables`, so captured mutable locals receive +the ordinary shared reference-cell representation. It has two steps. + +Callback-use analysis, outlining, and invocation rewriting live in +`LowerRuntimeAsync.fs`. `RuntimeAsyncAnalysis.fs` supplies fragment detection, +optimizer-side returned-closure reduction, and suspension-lifetime analysis. +Both transformations share match-target rebuilding from +`TypedTree/RuntimeAsync.fs`. + +First, if a compiler-owned `InlineIfLambda` function or delegate still +contains a suspension, a fully rewritable, single-argument callback is +outlined as a separate runtime-async closure. The ordinary optimizer already inlines a +callback whose construction ends in one lambda, including after `let` or +effect prefixes and at repeated invocations. What remains is a construction +that selects a lambda by branching (`if`/`match`). Construction effects and +branch selection stay at the original binding; each invocation calls the +selected closure and awaits its outcome before continuing. Callback bodies +are not copied into each invocation, so there is no callback-copy budget. +Only the specialized copy changes, not the exported inline definition or +source signature. This applies within a runtime-async body or sequence +recipe, and to a callback bound immediately before a runtime-async body +together with the callbacks its construction captures. Opaque consumers, +escaping callbacks, unsupported callback shapes (including `try`/`with` in +tail position), and unsafe byref or pinned captures are not rewritten; +suspensions remaining in an ordinary method, such as a closure, are diagnosed +with FS3918. + +Inner callback bindings are processed before the outer delegate. +If optimization leaves a construction expression directly as an `Invoke` +receiver, it is first bound to a compiler-owned callback and lowered through +the same path. Receiver construction still precedes argument evaluation. +Effectful precomputations that capture a delegate's inputs are not moved to +its invocation, so a pending operation is created once and repeated invokes +share captured state. Captured mutable locals +retain their identities and are handled by ordinary closure conversion, not +copied into and out of each fragment. Callback uses are checked before +rewriting; opaque consumers and quotations retaining the callback reject the +candidate. IlxGen checks each generated closure or delegate `Invoke` as its +own method: an `Await` left there without a return marker produces FS3918, +even when the enclosing method is runtime-async. + +### Context handoff + +The `net10.0` FSharp.Core asset supplies an inline +`StateMachineHelpers.__runtimeAsyncOutline` template. It wraps a callback +in a `__runtimeAsyncReturnValueTask` lambda, captures success or +`ExceptionDispatchInfo` on failure, and constructs a +`RuntimeAsyncFragmentResult` holding the final execution context, +synchronization context, and flow-suppression state. Lowering expands this +template through the existing expression optimizer rather than generating +the exception and context protocol itself. + +At each invocation the compiler constructs a `RuntimeAsyncFragmentAwaiter`, +uses the runtime's ordinary `AwaitAwaiter` entry point if the fragment is +incomplete, then calls its synchronous `GetResult`. Completed fragments do +not introduce an additional suspension. Core restores the captured contexts +before returning the value or rethrowing. Restoration therefore executes in the +parent, not in another async method whose return would undo it. + +This preserves final context values but is not transparent: returning from +the child and reinstalling its context produces additional `AsyncLocal` +context-change notifications. Unlike an inlined callback, an outlined +callback can allocate a closure and shared reference cells. Captured locals +are represented as closure fields at invocation rather than aliases to +locals in the parent method; their original construction scopes remain in +the PDB. + +### Body preparation + +Second, every return-marker body is prepared once after callbacks are outlined, +innermost first: locals that cannot be preserved across a suspension are +reported (once per range across the compilation), and suspending exception +handlers are rewritten. Preparing afterwards means an `Await` that callback +or delegate inlining moves into a handler is still rewritten. +`__runtimeAsyncSequence` recipes are not prepared here; `LowerAsyncSeq` +prepares their `MoveNextAsync` body after state-machine conversion, which is +where sequence `try`/`with` is lowered. When runtime-async specialization is forced in a debug build, the builder -combinator is copied with its definition-site debug ranges remarked before -arguments are substituted. User continuation arguments keep their own ranges, -so `let!`, `do!`, `yield`, and other -computation-expression statements remain associated with the source that -authored them without exposing the implementation ranges of `Run`, `Bind`, -`Combine`, or `Yield`. +combinator is copied with its definition-site debug ranges remarked at the +call site. User continuation arguments keep their own ranges, and the marked +method retains a call-site sequence point even when forced inlining reduces +its intermediate closures. + +Callback lowering retains source debug points when rebuilding construction +wrappers. Outlined methods keep the callback body's statement points. Dead branches eliminated by optimization do not reach code generation and do not produce a suspension-outside-runtime-async diagnostic. @@ -178,13 +262,20 @@ Runtime-async boundary recognition is centralized in use the shared recognizers rather than matching typed-tree shapes independently. -The optimizer uses a context-local `RuntimeAsyncAnalyzer`. It memoizes +The optimizer and the lowering pass use a context-local `RuntimeAsyncAnalyzer`. It memoizes completed expression results by reference identity and inline-value results by value stamp, with a visiting set for recursive inline-value graphs. The cache is not global: optimizer environments can provide different inline bodies, and optimization creates new expression trees. Context-dependent decisions such as `runtimeAsyncContext` remain outside the cached facts. +An optimizer analyzer lives for one application-inlining attempt; callback +lowering shares an analyzer for one implementation file, and exception +rewriting creates one per body. Flow-summary caches are also per body. +Custom recursive analysis and callback reconstruction use stack guards, +including traversals between inline-value bodies and through decision trees. +Callback capture analysis uses the guarded free-variable collector. + ## Code generation `IlxGen.fs` recognises the return-marker family in three placements through the @@ -197,9 +288,10 @@ shared runtime-async boundary contract, which strips `DebugPoint` wrappers: assemblies do not define the enum member). 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. + method's IL body (`ILMethodBody.IsRuntimeAsync`). Local type functions + mark `DirectInvoke` explicitly; for first-class closures, + `EraseClosures.convIlxClosureDef` copies the flag onto the emitted + `Invoke` method. 3. **Any other expression position** (`GenRuntimeAsyncReturnAsStartedTask`), e.g. a `let`-bound value initializer: the marker application is wrapped in a fresh `fun () -> ...` lambda that is immediately applied to `unit` and @@ -209,6 +301,10 @@ shared runtime-async boundary contract, which strips `DebugPoint` wrappers: emits a closure plus an indirect call); see the comment at `GenRuntimeAsyncReturnAsStartedTask`. +Named methods, closures, and sequence methods share marker unwrapping and +unit-return classification. Both closure emission paths also share captured +value validation, runtime-async environment setup, and IL-body marking. + 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 @@ -240,10 +336,9 @@ body (`DecideExpr`), promoting its free mutable locals to reference cells so the synthesized closure and the enclosing scope share them. `InvokeFast` is not a separate runtime-async path. It is the closure-erasure -shape for an indirect call with multiple arguments. Fragment substitution and -beta reduction happen before closure erasure; if a suspending fragment survives -until an indirect `InvokeFast` call, it is still outside a runtime-async method -and is rejected by code generation. +shape for an indirect call with multiple arguments. Forced inlining and +eligible callback inlining happen before closure erasure; a suspension left +behind an opaque indirect invocation is rejected by code generation. ## Runtime capability check @@ -329,12 +424,12 @@ Builder-generated `MoveNextAsync` can lose visible sequence points. The optimize This is a proposal, not a drop-in TaskSeq replacement. Producer `try/with` is lowered through an ordinary nested sequence, while runtime-async suspensions inside that handler remain unsupported. Opaque recipes and optimized tail handoff are rejected. Early disposal retains ordinary sequence exception precedence: an outer cleanup failure replaces an inner cleanup failure. Concurrent move/dispose calls are unsupported. Awaiting a non-cancellable operation does not make it cancellable. Performance qualification must include genuinely pending operations, not only completed awaits. -Build and run the small [usage and lifecycle example](../tests/FSharp.Compiler.ComponentTests/Language/RuntimeAsync/RuntimeAsyncSequence.fs) with the matching preview SDK: +Build and run the small [usage and lifecycle example](../tests/FSharp.Compiler.ComponentTests/Language/RuntimeAsync/RuntimeAsyncSequence.fs) with the matching SDK: ```sh ./build.sh -c Release dotnet test --project tests/FSharp.Compiler.ComponentTests/FSharp.Compiler.ComponentTests.fsproj \ - -c Release --no-build --filter-class 'Language.RuntimeAsyncSequenceTests' + -c Release --no-build -- --filter-class 'Language.RuntimeAsyncSequenceTests' ``` The tests compile the library and consumer together and separately, with optimization enabled and disabled. @@ -344,20 +439,20 @@ The tests compile the library and consumer together and separately, with optimiz An inline fragment that escapes as a first-class value, is passed to a non-inline function, or is dynamically dispatched cannot be preserved as a runtime-async suspension fragment. If the suspension remains in the generated -non-runtime-async method, code generation reports FS3916 rather than emitting +non-runtime-async method, code generation reports FS3918 rather than emitting an unsafe closure. Fragments in statically eliminated branches do not trigger this diagnostic. ## Not yet implemented -* Complete diagnostics for runtime restrictions. The optimizer rewrites +* Complete diagnostics for runtime restrictions. Runtime-async lowering rewrites suspending `try/with` handlers and filters, and `try/finally` compensations, so they execute outside exception-handling regions. There is no general diagnostic for runtime-contract violations in other generated or imported - shapes, and `localloc` has no dedicated diagnostic. Runtime-async methods - suppress `tail.` emission rather than reporting it. + shapes. Stack allocation has a dedicated diagnostic (FS3920), and + runtime-async methods suppress `tail.` emission rather than reporting it. * A builder in FSharp.Core; builders using the feature are currently application/library code. -* 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). +* Compile-time enforcement that every marker use was consumed during code + generation (a missed marker throws only when its FSharp.Core stub is reached + at run time). diff --git a/src/Compiler/CodeGen/IlxGen.fs b/src/Compiler/CodeGen/IlxGen.fs index bb617176b69..903c0e731f3 100644 --- a/src/Compiler/CodeGen/IlxGen.fs +++ b/src/Compiler/CodeGen/IlxGen.fs @@ -2952,6 +2952,10 @@ type CodeGenBuffer(m: range, mgbuf: AssemblyBuilder, methodName, alreadyUsedArgs locals.Add((ranges, ty, isFixed, canBeReallocd)) j + member _.AddLocalDebugRange(index, name, scopeMarks) = + let ranges, ty, isFixed, canBeReallocd = locals[index] + locals[index] <- ((name, scopeMarks) :: ranges, ty, isFixed, canBeReallocd) + member cgbuf.ReallocLocal(cond, ranges, ty, isFixed, canBeReallocd) = match ResizeArray.tryFindIndexi cond locals with | Some j -> @@ -7183,16 +7187,12 @@ and GenSequenceExpr ) let closeMethod = - let marker = TryGetRuntimeAsyncReturn g closeExpr - - let body = - marker |> Option.map (fun info -> info.Body) |> Option.defaultValue closeExpr - - let name = if marker.IsSome then "DisposeAsync" else "Close" + let struct (isRuntimeAsync, _, body) = GetRuntimeAsyncMethodBody g closeExpr + let name = if isRuntimeAsync then "DisposeAsync" else "Close" let methodEnv = { eenvinner with - inRuntimeAsyncMethod = marker.IsSome + inRuntimeAsyncMethod = isRuntimeAsync inInlineMethod = false } @@ -7200,11 +7200,11 @@ and GenSequenceExpr CodeGenMethodForExpr cenv cgbuf.mgbuf ([], name, methodEnv, 1, None, body, discardAndReturnVoid) |> fun code -> { code with - IsRuntimeAsync = marker.IsSome + IsRuntimeAsync = isRuntimeAsync } let resultTy = - if marker.IsSome then + if isRuntimeAsync then GenType cenv m eenvinner.tyenv (tyOfExpr g closeExpr) else ILType.Void @@ -7216,7 +7216,7 @@ and GenSequenceExpr mkILReturn resultTy, MethodBody.IL(InterruptibleLazy.FromValue ilCode) ) - |> fun methodDef -> methodDef.WithAsync(marker.IsSome) + |> fun methodDef -> methodDef.WithAsync(isRuntimeAsync) let checkCloseMethod = let ilCode = @@ -7235,35 +7235,29 @@ and GenSequenceExpr let eenvinner = eenvinner |> AddStorageForLocalVals g [ (nextEnumeratorValRef.Deref, Arg 1) ] - let marker = TryGetRuntimeAsyncReturn g generateNextExpr - - let body = - marker - |> Option.map (fun info -> info.Body) - |> Option.defaultValue generateNextExpr - - let name = if marker.IsSome then "MoveNextAsync" else "GenerateNext" + let struct (isRuntimeAsync, _, body) = GetRuntimeAsyncMethodBody g generateNextExpr + let name = if isRuntimeAsync then "MoveNextAsync" else "GenerateNext" let methodEnv = { eenvinner with - inRuntimeAsyncMethod = marker.IsSome + inRuntimeAsyncMethod = isRuntimeAsync inInlineMethod = false } let ilParams = - if marker.IsSome then + if isRuntimeAsync then [] else [ mkILParamNamed ("next", ILType.Byref ilCloEnumerableTy) ] let resultTy = - if marker.IsSome then + if isRuntimeAsync then GenType cenv m eenvinner.tyenv (tyOfExpr g generateNextExpr) else g.ilg.typ_Int32 let ilReturn = mkILReturn resultTy - let usedArgs = if marker.IsSome then 1 else 2 + let usedArgs = if isRuntimeAsync then 1 else 2 let ilCode = MethodBody.IL( @@ -7271,12 +7265,12 @@ and GenSequenceExpr CodeGenMethodForExpr cenv cgbuf.mgbuf ([], name, methodEnv, usedArgs, None, body, Return) |> fun code -> { code with - IsRuntimeAsync = marker.IsSome + IsRuntimeAsync = isRuntimeAsync }) ) mkILNonGenericVirtualInstanceMethod (name, ILMemberAccess.Public, ilParams, ilReturn, ilCode) - |> fun methodDef -> methodDef.WithAsync(marker.IsSome) + |> fun methodDef -> methodDef.WithAsync(isRuntimeAsync) let lastGeneratedMethod = let name = @@ -7462,6 +7456,36 @@ and CheckRuntimeAsyncFreeVars g m (cloinfo: IlxClosureInfo) = if fv.IsPinning || isByrefTy g fv.Type || isByrefLikeTy g m fv.Type then errorR (Error(FSComp.SR.chkByrefUsedInInvalidWay (richTextOfValName g fv), fv.Range)) +and GetRuntimeAsyncMethodBody g body = + match TryGetRuntimeAsyncReturn g body with + | Some info -> struct (true, List.isEmpty info.TypeArgs, info.Body) + | None -> struct (false, false, body) + +and GenClosureBody cenv (cgbuf: CodeGenBuffer) eenv (cloinfo: IlxClosureInfo) entryPointInfo body m = + let struct (isRuntimeAsync, isRuntimeAsyncUnit, body) = + GetRuntimeAsyncMethodBody cenv.g body + + if isRuntimeAsync then + CheckRuntimeAsyncFreeVars cenv.g m cloinfo + + let eenv = + { eenv with + inRuntimeAsyncMethod = isRuntimeAsync + } + + let sequel = if isRuntimeAsyncUnit then discardAndReturnVoid else Return + + let ilBody = + CodeGenMethodForExpr cenv cgbuf.mgbuf (entryPointInfo, cloinfo.cloName, eenv, 1, None, body, sequel) + + let ilBody = + if isRuntimeAsync then + { ilBody with IsRuntimeAsync = true } + else + ilBody + + struct (isRuntimeAsync, ilBody) + /// Generate a local type function contract class and implementation and GenClosureAsLocalTypeFunction cenv (cgbuf: CodeGenBuffer) eenv thisVars expr m = let g = cenv.g @@ -7489,28 +7513,8 @@ and GenClosureAsLocalTypeFunction cenv (cgbuf: CodeGenBuffer) eenv thisVars expr strip cloinfo.ilCloLambdas - let isRuntimeAsync, isRuntimeAsyncUnit, body = - match TryGetRuntimeAsyncReturn g body with - | Some info -> true, List.isEmpty info.TypeArgs, info.Body - | None -> false, false, body - - if isRuntimeAsync then - CheckRuntimeAsyncFreeVars g m cloinfo - - let eenvinner = - { eenvinner with - inRuntimeAsyncMethod = isRuntimeAsync - } - - let ilCloBody = - let sequel = if isRuntimeAsyncUnit then discardAndReturnVoid else Return - CodeGenMethodForExpr cenv cgbuf.mgbuf (entryPointInfo, cloinfo.cloName, eenvinner, 1, None, body, sequel) - - let ilCloBody = - if isRuntimeAsync then - { ilCloBody with IsRuntimeAsync = true } - else - ilCloBody + let struct (isRuntimeAsync, ilCloBody) = + GenClosureBody cenv cgbuf eenvinner cloinfo entryPointInfo body m let ilCtorBody = mkILMethodBody (true, [], 8, nonBranchingInstrsToCode (mkCallBaseConstructor (g.ilg.typ_Object, [])), None, eenv.imports) @@ -7557,28 +7561,8 @@ and GenClosureAsFirstClassFunction cenv (cgbuf: CodeGenBuffer) eenv thisVars m e let ilCloTypeRef = cloinfo.cloSpec.TypeRef - let isRuntimeAsync, isRuntimeAsyncUnit, body = - match TryGetRuntimeAsyncReturn g body with - | Some info -> true, List.isEmpty info.TypeArgs, info.Body - | None -> false, false, body - - if isRuntimeAsync then - CheckRuntimeAsyncFreeVars g m cloinfo - - let eenvinner = - { eenvinner with - inRuntimeAsyncMethod = isRuntimeAsync - } - - let ilCloBody = - let sequel = if isRuntimeAsyncUnit then discardAndReturnVoid else Return - CodeGenMethodForExpr cenv cgbuf.mgbuf (entryPointInfo, cloinfo.cloName, eenvinner, 1, None, body, sequel) - - let ilCloBody = - if isRuntimeAsync then - { ilCloBody with IsRuntimeAsync = true } - else - ilCloBody + let struct (_, ilCloBody) = + GenClosureBody cenv cgbuf eenvinner cloinfo entryPointInfo body m let cloTypeDefs = GenClosureTypeDefs @@ -8053,7 +8037,8 @@ and GenDelegateExpr cenv cgbuf eenvouter expr (TObjExprMethod(slotsig, _attribs, methInst, leadingArgs) -> if - ilMethodDirectlyBindable + not (IsRuntimeAsyncSuspensionMethod g ilMethRef) + && ilMethodDirectlyBindable (Optimizer.ExprHasEffect Optimizer.EffectContext.Emit) g tmvs @@ -8142,8 +8127,14 @@ and GenDelegateExpr cenv cgbuf eenvouter expr (TObjExprMethod(slotsig, _attribs, let ilDelegeeParams, ilDelegeeRet = GenActualSlotsig m cenv envForDelegeeUnderTypars slotsig methTyparsOfOverridingMethod tmvs + // The enclosing method's runtime-async marker does not apply to the delegate's Invoke. let envForDelegeeMeth = - AddStorageForLocalVals g (List.mapi (fun i v -> (v, Arg(i + numthis))) tmvs) envForDelegeeUnderTypars + let env = + AddStorageForLocalVals g (List.mapi (fun i v -> (v, Arg(i + numthis))) tmvs) envForDelegeeUnderTypars + + { env with + inRuntimeAsyncMethod = false + } let ilMethodBody = CodeGenMethodForExpr @@ -10232,10 +10223,8 @@ and GenMethodForBinding | h :: t -> [ h ], t, true | _ -> [], methLambdaVars, false - let isRuntimeAsync, isRuntimeAsyncUnit, methLambdaBody = - match TryGetRuntimeAsyncReturn g methLambdaBody with - | Some info -> true, List.isEmpty info.TypeArgs, info.Body - | None -> false, false, methLambdaBody + let struct (isRuntimeAsync, isRuntimeAsyncUnit, methLambdaBody) = + GetRuntimeAsyncMethodBody g methLambdaBody if isRuntimeAsync then checkLanguageFeatureError g.langVersion LanguageFeature.RuntimeAsync m diff --git a/src/Compiler/Driver/OptimizeInputs.fs b/src/Compiler/Driver/OptimizeInputs.fs index 0fcd75ee924..81a24d7e808 100644 --- a/src/Compiler/Driver/OptimizeInputs.fs +++ b/src/Compiler/Driver/OptimizeInputs.fs @@ -397,6 +397,24 @@ let ApplyAllOptimizations addPhase "firstLoop" firstLoop + let runtimeAsyncReportedRanges = + System.Collections.Concurrent.ConcurrentDictionary() + + let lowerRuntimeAsync + ({ + File = file + PrevPhase = prevPhase + PrevFile = _prevFile + }: PhaseInputs) + : PhaseRes = + let file = + LowerRuntimeAsync.TransformImplFile tcGlobals importMap prevPhase.FirstLoopRes.OptDuringCodeGen runtimeAsyncReportedRanges file + + file, prevPhase + + // Must precede lowerLocalMutables: outlined callbacks capture shared mutable locals. + addPhase "lowerRuntimeAsync" lowerRuntimeAsync + let lowerLocalMutables ({ File = file diff --git a/src/Compiler/FSharp.Compiler.Service.fsproj b/src/Compiler/FSharp.Compiler.Service.fsproj index 003c78ba3e8..3b67b36008e 100644 --- a/src/Compiler/FSharp.Compiler.Service.fsproj +++ b/src/Compiler/FSharp.Compiler.Service.fsproj @@ -473,6 +473,8 @@ + + diff --git a/src/Compiler/Optimize/LowerLocalMutables.fs b/src/Compiler/Optimize/LowerLocalMutables.fs index 5ab29e065b1..37d3231b5c1 100644 --- a/src/Compiler/Optimize/LowerLocalMutables.fs +++ b/src/Compiler/Optimize/LowerLocalMutables.fs @@ -202,4 +202,3 @@ let TransformImplFile g amap implFile = PostTransform = (fun _ -> None) RewriteQuotations = true StackGuard = StackGuard("AutoboxRewriteStackGuardDepth") } - diff --git a/src/Compiler/Optimize/LowerRuntimeAsync.fs b/src/Compiler/Optimize/LowerRuntimeAsync.fs new file mode 100644 index 00000000000..01722bb84b3 --- /dev/null +++ b/src/Compiler/Optimize/LowerRuntimeAsync.fs @@ -0,0 +1,325 @@ +// Copyright (c) Microsoft Corporation. All Rights Reserved. See License.txt in the project root for license information. + +module internal FSharp.Compiler.LowerRuntimeAsync + +open System.Collections.Concurrent + +open Internal.Utilities.Collections +open Internal.Utilities.Library +open Internal.Utilities.Library.Extras + +open FSharp.Compiler +open FSharp.Compiler.AbstractIL.IL +open FSharp.Compiler.AccessibilityLogic +open FSharp.Compiler.DiagnosticsLogger +open FSharp.Compiler.Features +open FSharp.Compiler.InfoReader +open FSharp.Compiler.MethodCalls +open FSharp.Compiler.RuntimeAsync +open FSharp.Compiler.RuntimeAsyncAnalysis +open FSharp.Compiler.RuntimeAsyncExceptionRewrite +open FSharp.Compiler.Syntax +open FSharp.Compiler.TcGlobals +open FSharp.Compiler.Text +open FSharp.Compiler.TypedTree +open FSharp.Compiler.TypedTreeBasics +open FSharp.Compiler.TypedTreeOps +open FSharp.Compiler.TypeRelations + +let private isQuotationUsing (v: Val) expr = + match expr with + | Expr.Quote(body, _, _, _, _) -> + ExistsExpr + (function + | Expr.Val(vref, _, _) -> valEq v vref.Deref + | _ -> false) + body + | _ -> false + +let private tryCallbackInvocation (g: TcGlobals) (callback: Val) isDelegate expression = + match expression with + | Expr.App(Expr.Val(vref, _, _), _, [], [ arg ], m) when not isDelegate && valEq callback vref.Deref -> ValueSome(struct (arg, m)) + | DelegateInvokeExpr g (_, _, _, Expr.Val(vref, _, _), arg, m) when isDelegate && valEq callback vref.Deref -> + ValueSome(struct (arg, m)) + | _ -> ValueNone + +let private canRewriteCallbackUses g callback isDelegate continuation = + let folder = + { ExprFolder0 with + exprIntercept = + fun recurse noIntercept valid expression -> + if not valid then + false + else + match tryCallbackInvocation g callback isDelegate expression with + | ValueSome(struct (arg, _)) -> recurse true arg + | ValueNone -> + match expression with + | Expr.Val(vref, _, _) when valEq callback vref.Deref -> false + | Expr.Quote(_, dataCell, _, _, _) -> + if isQuotationUsing callback expression then + false + else + match dataCell.Value with + | Some((_, _, args, _), (_, _, legacyArgs, _)) -> + List.fold recurse (List.fold recurse true args) legacyArgs + | None -> true + | _ -> noIntercept true expression + } + + FoldExpr folder true continuation + +let rec private tryDelegateSignature g expr = + match expr with + | NewDelegateExpr g (_, [ parameter ], body, _, _) -> Some(parameter.Type, tyOfExpr g body) + | Expr.DebugPoint(_, rest) + | Expr.Sequential(_, rest, NormalSeq, _) + | Expr.Let(_, rest, _, _) -> tryDelegateSignature g rest + | Expr.Match(_, _, _, targets, _, _) when targets.Length > 0 -> + let (TTarget(_, body, _)) = targets[0] + tryDelegateSignature g body + | _ -> None + +let private awaitFragment (g: TcGlobals) amap resultTy invocation m = + let awaiterTy = mkWoNullAppTy g.runtimeAsyncFragmentAwaiter_tcref [ resultTy ] + let infoReader = InfoReader(g, amap) + + let constructor = + match GetIntrinsicConstructorInfosOfType infoReader m awaiterTy with + | [ constructor ] -> constructor + | _ -> error (InternalError("runtime-async fragment awaiter constructor not found", m)) + + let awaiter, awaiterExpr = mkCompGenLocal m "runtimeAsyncFragmentAwaiter" awaiterTy + let createAwaiter = MakeMethInfoCall amap m constructor [] [ invocation ] None + + let awaitRef = + mkILMethRef ( + g.FindSysILTypeRef "System.Runtime.CompilerServices.AsyncHelpers", + ILCallingConv.Static, + "AwaitAwaiter", + 1, + [ mkILTyvarTy 0us ], + ILType.Void + ) + + let suspend = + Expr.Op(TOp.ILCall(false, false, false, false, NormalValUse, false, false, awaitRef, [], [ awaiterTy ], []), [], [ awaiterExpr ], m) + + let callAwaiterMember name = + let methodInfo = + match TryFindIntrinsicMethInfo infoReader m AccessorDomain.AccessibleFromEverywhere name awaiterTy with + | [ methodInfo ] -> methodInfo + | _ -> error (InternalError($"runtime-async fragment awaiter {name} not found", m)) + + let wrap, address, _, _ = + mkExprAddrOfExpr g true false NeverMutates awaiterExpr None m + + MakeMethInfoCall amap m methodInfo [] [ address ] None |> wrap + + let suspend = + mkCond DebugPointAtBinding.NoneAtInvisible m g.unit_ty (callAwaiterMember "get_IsCompleted") (mkUnit g m) suspend + + let result = callAwaiterMember "GetResult" + mkCompGenLet m awaiter createAwaiter (mkCompGenSequential m suspend result) + +let private outlineCallback (g: TcGlobals) amap optimizeExpr (analyzer: RuntimeAsyncAnalyzer) runtimeAsyncContext expr = + let stackGuard = StackGuard("OutlineRuntimeAsyncCallback") + let freeVarOptions = CollectLocalsWithStackGuard() + + let rec outlineBranches resultTy expr = + stackGuard.Guard(fun () -> + match expr with + | Expr.Lambda(_, None, None, [ parameter ], body, m, _) + | NewDelegateExpr g (_, [ parameter ], body, m, _) -> + let helper = g.cgh__runtimeAsyncOutline_vref + let callback = mkLambda m parameter (body, resultTy) + + primMkApp (exprForValRef m helper, helper.Type) [ parameter.Type; resultTy ] [ callback ] m + |> optimizeExpr true + |> Some + | Expr.DebugPoint(point, body) -> + outlineBranches resultTy body + |> Option.map (fun body -> Expr.DebugPoint(point, body)) + | Expr.Sequential(first, rest, NormalSeq, m) -> + outlineBranches resultTy rest + |> Option.map (fun rest -> Expr.Sequential(first, rest, NormalSeq, m)) + | Expr.Let(binding, rest, m, _) -> outlineBranches resultTy rest |> Option.map (mkLetBind m binding) + | Expr.Match(point, matchRange, tree, targets, m, _) -> + TryMapRuntimeAsyncMatchTargets g (point, matchRange, tree, targets, m) (fun _ -> outlineBranches resultTy) + | _ -> None) + + let rec outline expr = + stackGuard.Guard(fun () -> + match expr with + | Expr.Let(TBind(callback, construction, point), continuation, m, _) when + callback.InlineIfLambda && analyzer.ContainsSuspension construction + -> + let keep construction = + mkLetBind m (TBind(callback, construction, point)) (outline continuation) + + let shape = + match tryDestFunTy g callback.Type with + | ValueSome(argTy, resultTy) -> Some(argTy, resultTy, false) + | ValueNone when isFSharpDelegateTy g callback.Type -> + tryDelegateSignature g construction + |> Option.map (fun (argTy, resultTy) -> argTy, resultTy, true) + | _ -> None + + match shape with + | Some(argTy, resultTy, isDelegate) when + (runtimeAsyncContext || (TryGetRuntimeAsyncReturn g continuation).IsSome) + && not (isByrefLikeTy g m argTy || isByrefLikeTy g m resultTy) + -> + let canCapture = + (freeInExpr freeVarOptions construction).FreeLocals + |> Zset.forall (fun v -> not v.IsPinning && not (isByrefTy g v.Type || isByrefLikeTy g m v.Type)) + + if + not canCapture + || not (canRewriteCallbackUses g callback isDelegate continuation) + then + keep construction + else + let construction = outline construction + + match outlineBranches resultTy construction with + | None -> keep construction + | Some construction -> + let callbackTy = tyOfExpr g construction + let outlined, outlinedExpr = mkCompGenLocal m "runtimeAsyncFragment" callbackTy + + let continuation = + RewriteExpr + { + PreIntercept = + Some(fun rewrite expression -> + match tryCallbackInvocation g callback isDelegate expression with + | ValueSome(struct (arg, callRange)) -> + let invocation = + mkApps g ((outlinedExpr, callbackTy), [], [ rewrite arg ], callRange) + + Some(awaitFragment g amap resultTy invocation callRange) + | ValueNone -> None) + PreInterceptBinding = None + PostTransform = (fun _ -> None) + RewriteQuotations = false + StackGuard = stackGuard + } + continuation + + mkLet point m outlined construction (outline continuation) + | _ -> keep construction + | Expr.Let(binding, continuation, m, _) -> mkLetBind m binding (outline continuation) + | Expr.DebugPoint(point, body) -> Expr.DebugPoint(point, outline body) + | Expr.Sequential(first, rest, NormalSeq, m) -> Expr.Sequential(first, outline rest, NormalSeq, m) + | _ -> expr) + + outline expr + +let private isRuntimeAsyncEntry g expr = + (TryGetRuntimeAsyncReturn g expr).IsSome + || (TryGetRuntimeAsyncSequence g expr).IsSome + +let private containsRuntimeAsyncEntry g implFile = + let folder = + { ExprFolder0 with + exprIntercept = + fun _ noInterceptF acc expr -> + if acc || isRuntimeAsyncEntry g expr then + true + else + noInterceptF acc expr + } + + FoldImplFile folder false implFile + +/// `inContext` follows runtime-async bodies and sequence recipes, but not ordinary object-expression methods. +let private outlineCallbacks g amap optimizeExpr implFile = + let analyzer = RuntimeAsyncAnalyzer g + let stackGuard = StackGuard("OutlineRuntimeAsyncCallbacks") + + let rec leadsToRuntimeAsyncReturn expr = + match expr with + | Expr.Let(_, rest, _, _) + | Expr.DebugPoint(_, rest) + | Expr.Sequential(_, rest, NormalSeq, _) -> leadsToRuntimeAsyncReturn rest + | _ -> (TryGetRuntimeAsyncReturn g expr).IsSome + + let rec rewriter inContext = + { + PreIntercept = Some(intercept inContext) + PreInterceptBinding = None + PostTransform = postTransform inContext + RewriteQuotations = false + StackGuard = stackGuard + } + + and rewrite inContext expr = RewriteExpr (rewriter inContext) expr + + and intercept inContext _ expr = + match expr with + | Expr.App(f, fty, tyargs, [ body ], m) when (TryGetRuntimeAsyncReturn g expr).IsSome -> + Some(Expr.App(f, fty, tyargs, [ rewrite true body ], m)) + | Expr.App(f, fty, tyargs, [ recipe ], m) when (TryGetRuntimeAsyncSequence g expr).IsSome -> + Some(Expr.App(f, fty, tyargs, [ rewrite true recipe ], m)) + | Expr.Let(TBind(callback, construction, point), continuation, m, _) when + not inContext + && callback.InlineIfLambda + && leadsToRuntimeAsyncReturn continuation + -> + Some(mkLetBind m (TBind(callback, rewrite true construction, point)) (rewrite false continuation)) + | DelegateInvokeExpr g (invokeRef, invokeTy, tyargs, receiver, arg, m) when inContext && analyzer.ContainsSuspension receiver -> + let callback, callbackExpr = + mkCompGenLocal m "runtimeAsyncDelegate" (tyOfExpr g receiver) + + callback.SetInlineIfLambda() + let invoke = Expr.App(invokeRef, invokeTy, tyargs, [ callbackExpr; arg ], m) + Some(rewrite inContext (mkCompGenLet m callback receiver invoke)) + | NewDelegateExpr g _ -> None + | Expr.Obj _ when inContext -> Some(rewrite false expr) + | _ -> None + + and postTransform inContext expr = + match expr with + | Expr.Let(TBind(callback, construction, _), _, _, _) when callback.InlineIfLambda && analyzer.ContainsSuspension construction -> + Some(outlineCallback g amap optimizeExpr analyzer inContext expr) + | _ -> None + + RewriteImplFile (rewriter false) implFile + +/// Prepares each runtime-async body once its final shape is known, innermost first. +let private prepareBodies g (reportedRanges: ConcurrentDictionary) implFile = + let prepare body = + for v in GetRuntimeAsyncNonPreservableUses g body do + if reportedRanges.TryAdd(v.Range, ()) then + errorR (Error(FSComp.SR.ilRuntimeAsyncLocalUsedAfterSuspension (RichText.mkText v.DisplayName), v.Range)) + + RewriteRuntimeAsyncExceptionHandlers g body + + RewriteImplFile + { + PreIntercept = None + PreInterceptBinding = None + PostTransform = + fun expr -> + match expr with + | Expr.App(f, fty, tyargs, [ body ], m) when (TryGetRuntimeAsyncReturn g expr).IsSome -> + Some(Expr.App(f, fty, tyargs, [ prepare body ], m)) + | _ -> None + RewriteQuotations = false + StackGuard = StackGuard("PrepareRuntimeAsyncBodies") + } + implFile + +let TransformImplFile (g: TcGlobals) amap optimizeExpr reportedRanges (implFile: CheckedImplFile) = + if containsRuntimeAsyncEntry g implFile then + // Bodies inlined from an assembly compiled with the feature are still prepared. + let implFile = + if g.langVersion.SupportsFeature LanguageFeature.RuntimeAsync then + outlineCallbacks g amap optimizeExpr implFile + else + implFile + + prepareBodies g reportedRanges implFile + else + implFile diff --git a/src/Compiler/Optimize/LowerRuntimeAsync.fsi b/src/Compiler/Optimize/LowerRuntimeAsync.fsi new file mode 100644 index 00000000000..07fc70b9d88 --- /dev/null +++ b/src/Compiler/Optimize/LowerRuntimeAsync.fsi @@ -0,0 +1,27 @@ +// Copyright (c) Microsoft Corporation. All Rights Reserved. See License.txt in the project root for license information. + +/// Post-optimization lowering for runtime-async bodies. +/// +/// Runs after the first optimization loop and before LowerLocalMutables. It outlines suspending +/// InlineIfLambda callbacks that the optimizer could not inline, using the Core context-handoff template, then +/// prepares every `__runtimeAsyncReturn*` body exactly once (non-preservable local diagnostics and +/// exception-handler rewriting). `__runtimeAsyncSequence` recipes are not prepared here: LowerAsyncSeq +/// prepares their MoveNextAsync body after state-machine conversion. +/// +/// `reportedRanges` is shared by all files of a compilation so that a diagnostic in an inline body is +/// reported once, not again at every inlining site. +module internal FSharp.Compiler.LowerRuntimeAsync + +open System.Collections.Concurrent +open FSharp.Compiler.Import +open FSharp.Compiler.TcGlobals +open FSharp.Compiler.Text +open FSharp.Compiler.TypedTree + +val TransformImplFile: + g: TcGlobals -> + amap: ImportMap -> + optimizeExpr: (bool -> Expr -> Expr) -> + reportedRanges: ConcurrentDictionary -> + implFile: CheckedImplFile -> + CheckedImplFile diff --git a/src/Compiler/Optimize/Optimizer.fs b/src/Compiler/Optimize/Optimizer.fs index 15c18a9ac49..d792dec8345 100644 --- a/src/Compiler/Optimize/Optimizer.fs +++ b/src/Compiler/Optimize/Optimizer.fs @@ -17,7 +17,6 @@ open FSharp.Compiler.DiagnosticsLogger open FSharp.Compiler.Features open FSharp.Compiler.RuntimeAsync open FSharp.Compiler.RuntimeAsyncAnalysis -open FSharp.Compiler.RuntimeAsyncExceptionRewrite open FSharp.Compiler.Text.Range open FSharp.Compiler.Syntax.PrettyNaming open FSharp.Compiler.Syntax @@ -499,9 +498,6 @@ type IncrementalOptimizationEnv = /// Indicates that the expression being optimized is the body of a runtime-async marker. runtimeAsyncContext: bool - /// Runtime async diagnostics must only be reported once across optimization passes. - runtimeAsyncReportedRanges: HashSet - } static member Empty = @@ -517,8 +513,7 @@ type IncrementalOptimizationEnv = methEnv = { pipelineCount = 0 } referencedCcus = [] earlierImplFileSignatures = [] - runtimeAsyncContext = false - runtimeAsyncReportedRanges = HashSet() } + runtimeAsyncContext = false } override x.ToString() = "" @@ -2719,6 +2714,38 @@ let EtaExpandUnderAppliedValBinding g expr = | EtaFloatableValLet g (bind, body, m, etaExpanded) -> floatEtaCaptures bind body m etaExpanded | _ -> expr +/// `let p = (let c = e in fun x -> ...)` ~> `let c = e in let p = fun x -> ...`, p an [] +/// binding, so the lambda or delegate becomes a `CurriedLambdaValue` the optimizer can inline. The captures +/// are evaluated at the same point and are not in scope in the body. +let FloatInlineIfLambdaCaptures g expr = + let rec isFloatable rhs = + match rhs with + | Expr.Let(_, rest, _, _) + | Expr.Sequential(_, rest, NormalSeq, _) -> isFloatable rest + | _ -> + match stripDebugPoints rhs with + | Expr.Lambda _ + | Expr.TyLambda _ + | NewDelegateExpr g _ -> true + | _ -> false + + match expr with + | Expr.Let(bind, body, m, _) when + bind.Var.InlineIfLambda + && (match bind.Expr with + | Expr.Let _ + | Expr.Sequential(_, _, NormalSeq, _) -> isFloatable bind.Expr + | _ -> false) + -> + let rec rebind rhs = + match rhs with + | Expr.Let(capture, rest, mLet, _) -> mkLetBind mLet capture (rebind rest) + | Expr.Sequential(first, rest, NormalSeq, mSeq) -> Expr.Sequential(first, rebind rest, NormalSeq, mSeq) + | rhs -> mkLet bind.DebugPoint m bind.Var rhs body + + rebind bind.Expr + | _ -> expr + /// Optimize/analyze an expression let rec OptimizeExpr cenv (env: IncrementalOptimizationEnv) expr = cenv.stackGuard.Guard(fun () -> @@ -2797,11 +2824,6 @@ let rec OptimizeExpr cenv (env: IncrementalOptimizationEnv) expr = | Expr.App (f, fty, tyargs, _, m) when runtimeAsyncReturn.IsSome -> let info = runtimeAsyncReturn.Value let bodyR, bodyInfo = OptimizeExpr cenv { env with runtimeAsyncContext = true } info.Body - for v in GetRuntimeAsyncNonPreservableUses g bodyR do - if env.runtimeAsyncReportedRanges.Add v.Range then - errorR(Error(FSComp.SR.ilRuntimeAsyncLocalUsedAfterSuspension(RichText.mkText v.DisplayName), v.Range)) - - let bodyR = RewriteRuntimeAsyncExceptionHandlers g bodyR Expr.App(f, fty, tyargs, [ bodyR ], m), { bodyInfo with HasEffect = true @@ -3271,6 +3293,7 @@ and OptimizeLinearExpr cenv env expr contf = let expr = DetectAndOptimizeForEachExpression g OptimizeAllForExpressions expr let expr = if cenv.settings.ExpandStructuralValues() then ExpandStructuralBinding cenv expr else expr let expr = if cenv.settings.alwaysInline then EtaExpandUnderAppliedValBinding g expr else expr + let expr = if env.runtimeAsyncContext && cenv.settings.alwaysInline then FloatInlineIfLambdaCaptures g expr else expr let expr = stripExpr expr // Matching on 'match __resumableEntry() with ...` is really a first-class language construct which we @@ -3890,15 +3913,22 @@ and TryInlineApplication cenv env finfo (valExpr: Expr) (tyargs: TType list, arg | None -> false let reoptimizeRuntimeAsync reduced = - let reduced = InlineRuntimeAsyncLambdaArgument g containsRuntimeAsyncFragment reduced - - let reduced = - if containsRuntimeAsyncFragment reduced then - fst (OptimizeExpr cenv { env with runtimeAsyncContext = true } reduced) - else - reduced - - InlineRuntimeAsyncLambdaArgument g containsRuntimeAsyncFragment reduced + if containsRuntimeAsyncFragment reduced then + let preserveCallSite = not cenv.settings.LocalOptimizationsEnabled + let cenv = + { cenv with + settings = + { cenv.settings with + alwaysInline = true + localOptUser = Some true } } + let result = fst (OptimizeExpr cenv { env with runtimeAsyncContext = true } reduced) + match runtimeAsyncAnalyzer with + | Some analyzer -> + let result = ReduceRuntimeAsyncReturnedClosureApplications g analyzer result + if preserveCallSite then PreserveRuntimeAsyncCallSiteDebugPoint g m result else result + | None -> result + else + reduced let mustInlineRuntimeAsync = match runtimeAsyncAnalyzer, stripExpr valExpr with @@ -4031,8 +4061,9 @@ and TryInlineApplication cenv env finfo (valExpr: Expr) (tyargs: TType list, arg else specialize () + // InlineIfLambda callbacks keep their source points; forced inline helpers belong at the call site. let specLambdaR = - if mustInlineRuntimeAsync then + if mustInlineRuntimeAsync && not vref.InlineIfLambda then remarkExpr m specLambdaR else specLambdaR diff --git a/src/Compiler/Optimize/RuntimeAsyncAnalysis.fs b/src/Compiler/Optimize/RuntimeAsyncAnalysis.fs index e76aa5caeed..b4a0fc2a551 100644 --- a/src/Compiler/Optimize/RuntimeAsyncAnalysis.fs +++ b/src/Compiler/Optimize/RuntimeAsyncAnalysis.fs @@ -2,22 +2,19 @@ module internal FSharp.Compiler.RuntimeAsyncAnalysis +open System.Collections.Generic + open Internal.Utilities.Collections open Internal.Utilities.Library open Internal.Utilities.Library.Extras -open System.Collections.Generic - open FSharp.Compiler open FSharp.Compiler.DiagnosticsLogger +open FSharp.Compiler.RuntimeAsync open FSharp.Compiler.TcGlobals -open FSharp.Compiler.Text open FSharp.Compiler.TypedTree open FSharp.Compiler.TypedTreeBasics open FSharp.Compiler.TypedTreeOps -open FSharp.Compiler.TypeRelations - -open FSharp.Compiler.RuntimeAsync let rec private containsRecipeConstruction visit expr = match stripDebugPoints expr with @@ -32,6 +29,7 @@ type RuntimeAsyncAnalyzer(g: TcGlobals, getLambdaBody: ValRef -> Expr option) = let suspensionCache = Dictionary(HashIdentity.Reference) let valueCache = Dictionary() let visitingValues = HashSet() + let stackGuard = StackGuard("RuntimeAsyncAnalyzer") let rec containsValue (vref: ValRef) = match valueCache.TryGetValue vref.Stamp with @@ -53,6 +51,9 @@ type RuntimeAsyncAnalyzer(g: TcGlobals, getLambdaBody: ValRef -> Expr option) = result, complete and containsExpression expr = + stackGuard.Guard(fun () -> containsExpressionCore expr) + + and containsExpressionCore expr = match expressionCache.TryGetValue expr with | true, result -> result, true | _ -> @@ -90,6 +91,9 @@ type RuntimeAsyncAnalyzer(g: TcGlobals, getLambdaBody: ValRef -> Expr option) = member _.ContainsFragment expr = containsExpression expr |> fst member this.ContainsSuspension expr = + stackGuard.Guard(fun () -> this.ContainsSuspensionCore expr) + + member private this.ContainsSuspensionCore expr = match suspensionCache.TryGetValue expr with | true, result -> result | _ -> @@ -138,134 +142,87 @@ let ShouldForceRuntimeAsyncApplication (analyzer: RuntimeAsyncAnalyzer) runtimeA | _ -> false) args) -let rec private IsRuntimeAsyncEffectFree expr = - match stripExpr expr with - | Expr.Const _ - | Expr.Lambda _ - | Expr.TyLambda _ -> true - | Expr.Val(vref, _, _) -> not vref.IsMutable && not vref.IsTypeFunction - | Expr.App(funcExpr, _, _, [], _) -> IsRuntimeAsyncEffectFree funcExpr - | Expr.Op(TOp.Tuple _, _, args, _) - | Expr.Op(TOp.AnonRecd _, _, args, _) -> List.forall IsRuntimeAsyncEffectFree args - | _ -> false - -let InlineRuntimeAsyncLambdaArgument (g: TcGlobals) (isRuntimeAsyncFragment: Expr -> bool) expr = - let rec isLambdaExpression expr = +let ReduceRuntimeAsyncReturnedClosureApplications (g: TcGlobals) (analyzer: RuntimeAsyncAnalyzer) expr = + let stackGuard = StackGuard("ReduceRuntimeAsyncReturnedClosureApplications") + + let rec effectFree expr = match stripExpr expr with - | Expr.DebugPoint(_, innerExpr) -> isLambdaExpression innerExpr - | Expr.Let(TBind(_, rhs, _), innerExpr, _, _) -> IsRuntimeAsyncEffectFree rhs && isLambdaExpression innerExpr + | Expr.Const _ | Expr.Lambda _ | Expr.TyLambda _ -> true + | Expr.Val(vref, _, _) -> not vref.IsMutable && not vref.IsTypeFunction + | Expr.App(funcExpr, _, _, [], _) -> effectFree funcExpr | _ -> false - let rec stripLambdaDebugPoints expr = - match expr with - | Expr.DebugPoint(_, innerExpr) -> - match stripDebugPoints innerExpr with - | Expr.Lambda _ - | Expr.TyLambda _ -> stripLambdaDebugPoints innerExpr - | _ -> expr - | Expr.Lambda(unique, ctorThisValOpt, baseValOpt, valParams, bodyExpr, m, overallType) -> - match bodyExpr with - | Expr.DebugPoint(_, innerExpr) -> - match stripDebugPoints innerExpr with - | Expr.Lambda _ - | Expr.TyLambda _ -> - Expr.Lambda(unique, ctorThisValOpt, baseValOpt, valParams, stripLambdaDebugPoints bodyExpr, m, overallType) - | _ -> expr - | _ -> expr - | Expr.TyLambda(unique, typeParams, bodyExpr, m, overallType) -> - match bodyExpr with - | Expr.DebugPoint(_, innerExpr) -> - match stripDebugPoints innerExpr with - | Expr.Lambda _ - | Expr.TyLambda _ -> Expr.TyLambda(unique, typeParams, stripLambdaDebugPoints bodyExpr, m, overallType) - | _ -> expr - | _ -> expr - | _ -> expr - - let rec betaReduceLambdaApplication expr = - let rec apply f fty tyargs args m = - match args with - | [] -> None - | firstArg :: rest -> - let f = stripLambdaDebugPoints f - - match f with - | Expr.Let(bind, body, mLet, _) -> apply body (tyOfExpr g body) tyargs args m |> Option.map (mkLetBind mLet bind) - | Expr.Lambda(_, _, _, valParams, body, _, _) when - valParams.Length = 1 - && not rest.IsEmpty - && (IsRuntimeAsyncEffectFree body || List.forall IsRuntimeAsyncEffectFree rest) - -> - let reduced = MakeApplicationAndBetaReduce g (f, fty, [ tyargs ], [ firstArg ], m) - - match reduced with - | Expr.Let(bind, body, mLet, _) -> - match apply body (tyOfExpr g body) [] rest m with - | Some bodyR -> Some(mkLetBind mLet bind bodyR) - | None -> Some(mkAppsAux g reduced (tyOfExpr g reduced) [] rest m) - | _ -> Some reduced - | Expr.Lambda _ - | Expr.TyLambda _ -> Some(MakeApplicationAndBetaReduce g (f, fty, [ tyargs ], args, m)) - | _ -> None - - match stripDebugPoints expr with - | Expr.App(f, fty, tyargs, args, m) -> apply f fty tyargs args m + let rec apply f fty tyargs args m = + stackGuard.Guard(fun () -> applyCore f fty tyargs args m) + + and applyCore f fty tyargs args m = + match f, args with + | Expr.DebugPoint(_, inner), _ when + match stripDebugPoints inner with + | Expr.Lambda _ -> true + | _ -> false + -> + apply inner fty tyargs args m + | Expr.DebugPoint(point, inner), _ -> + apply inner fty tyargs args m + |> Option.map (fun body -> Expr.DebugPoint(point, body)) + | Expr.Let(binding, body, mLet, _), _ -> + apply body (tyOfExpr g body) tyargs args m + |> Option.map (mkLetBind mLet binding) + | Expr.LetRec(bindings, body, mLet, _), _ -> + apply body (tyOfExpr g body) tyargs args m + |> Option.map (mkLetRecBinds mLet bindings) + | Expr.Sequential(first, rest, NormalSeq, mSeq), _ -> + apply rest (tyOfExpr g rest) tyargs args m + |> Option.map (fun rest -> Expr.Sequential(first, rest, NormalSeq, mSeq)) + | Expr.Match(point, matchRange, tree, targets, mMatch, _), _ when List.forall effectFree args -> + // Each target after the first needs its own copy of any values bound by the arguments. + TryMapRuntimeAsyncMatchTargets g (point, matchRange, tree, targets, mMatch) (fun i body -> + let args = if i = 0 then args else List.map (copyExpr g CloneAll) args + apply body (tyOfExpr g body) tyargs args m) + | Expr.Lambda(_, _, _, [ _ ], _, _, _), first :: (_ :: _ as rest) when List.forall effectFree rest -> + let reduced = MakeApplicationAndBetaReduce g (f, fty, [ tyargs ], [ first ], m) + apply reduced (tyOfExpr g reduced) [] rest m + | Expr.Lambda _, _ :: _ -> Some(MakeApplicationAndBetaReduce g (f, fty, [ tyargs ], args, m)) + | _ when not (analyzer.ContainsSuspension f) && List.forall effectFree args -> Some(mkAppsAux g f fty [ tyargs ] args m) | _ -> None - let mkRwenv (preIntercept: (Expr -> Expr) -> Expr -> Expr option) : ExprRewritingEnv = + let rwenv = { - PreIntercept = Some preIntercept + PreIntercept = None PreInterceptBinding = None - PostTransform = betaReduceLambdaApplication + PostTransform = + (fun expression -> + match expression with + | Expr.App((Expr.Lambda _ | Expr.Let _ | Expr.LetRec _ | Expr.Match _ | Expr.DebugPoint _) as f, fty, tyargs, args, m) when + not args.IsEmpty && analyzer.ContainsSuspension expression + -> + apply f fty tyargs args m + | _ -> None) RewriteQuotations = false - StackGuard = StackGuard("InlineRuntimeAsyncLambdaArgument") + StackGuard = stackGuard } - let rec inlineBinding (boundVal: Val) boundExpr body = - match boundExpr with - | Expr.DebugPoint(point, inner) -> Expr.DebugPoint(point, inlineBinding boundVal inner body) - | Expr.Let(binding, rest, m, _) -> mkLetBind m binding (inlineBinding boundVal rest body) - | Expr.Sequential(first, rest, NormalSeq, m) -> Expr.Sequential(first, inlineBinding boundVal rest body, NormalSeq, m) - | _ -> - let rwenv = - mkRwenv (fun _ expr -> - match betaReduceLambdaApplication expr with - | Some reduced -> Some reduced - | None -> - match stripExpr expr with - | Expr.App(f, _, tyargs, args, m) -> - match stripDebugPoints f with - | Expr.Val(vref, _, _) when valEq boundVal vref.Deref -> - Some( - MakeApplicationAndBetaReduce - g - (copyExpr g CloneAll boundExpr, tyOfExpr g boundExpr, [ tyargs ], args, m) - ) - | _ -> None - | Expr.Val(vref, _, _) when valEq boundVal vref.Deref -> Some(copyExpr g CloneAll boundExpr) - | _ -> None) - - RewriteExpr rwenv body + RewriteExpr rwenv expr +let PreserveRuntimeAsyncCallSiteDebugPoint (g: TcGlobals) m expr = let rwenv = - mkRwenv (fun cont expr -> - match stripExpr expr with - | Expr.Let(TBind(boundVal, boundExpr, _), body, _, _) when - (boundVal.InlineIfLambda - && (isLambdaExpression boundExpr - || isRuntimeAsyncFragment boundExpr - || match stripExpr boundExpr with - | Expr.App(_, _, _, args, _) -> List.isEmpty args - | _ -> true)) - || (isLambdaExpression boundExpr && isRuntimeAsyncFragment boundExpr) - -> - if not boundVal.InlineIfLambda then - boundVal.SetInlineIfLambda() - - Some(cont (inlineBinding boundVal boundExpr body)) - | _ -> None) + { + PreIntercept = None + PreInterceptBinding = None + PostTransform = + (fun expression -> + match expression, TryGetRuntimeAsyncReturn g expression with + | Expr.App(f, fty, tyargs, [ body ], range), Some _ -> + match body with + | Expr.DebugPoint _ -> None + | _ -> Some(Expr.App(f, fty, tyargs, [ mkDebugPoint m body ], range)) + | _ -> None) + RewriteQuotations = false + StackGuard = StackGuard("PreserveRuntimeAsyncCallSiteDebugPoint") + } RewriteExpr rwenv expr @@ -321,12 +278,13 @@ let private TryGetRuntimeAsyncNonPreservableAlias (g: TcGlobals) expr = let private analyzeRuntimeAsyncExpr (g: TcGlobals) expr = let cache = Dictionary(HashIdentity.Reference) + let stackGuard = StackGuard("AnalyzeRuntimeAsyncFlow") let rec analyzeExpr expr = match cache.TryGetValue expr with | true, summary -> summary | _ -> - let summary = analyzeExprCore expr + let summary = stackGuard.Guard(fun () -> analyzeExprCore expr) cache[expr] <- summary summary @@ -502,6 +460,9 @@ let private analyzeRuntimeAsyncExpr (g: TcGlobals) expr = analyzeExpr targetExpr |> removeRuntimeAsyncBoundVals boundVals let rec analyzeTree tree = + stackGuard.Guard(fun () -> analyzeTreeCore tree) + + and analyzeTreeCore tree = match tree with | TDSuccess(results, targetNum) -> let resultSummary = diff --git a/src/Compiler/Optimize/RuntimeAsyncAnalysis.fsi b/src/Compiler/Optimize/RuntimeAsyncAnalysis.fsi index 54518fc605d..015ebbc0346 100644 --- a/src/Compiler/Optimize/RuntimeAsyncAnalysis.fsi +++ b/src/Compiler/Optimize/RuntimeAsyncAnalysis.fsi @@ -24,7 +24,9 @@ val ShouldForceRuntimeAsyncApplication: args: Expr list -> bool -val InlineRuntimeAsyncLambdaArgument: g: TcGlobals -> isRuntimeAsyncFragment: (Expr -> bool) -> expr: Expr -> Expr +val ReduceRuntimeAsyncReturnedClosureApplications: g: TcGlobals -> analyzer: RuntimeAsyncAnalyzer -> expr: Expr -> Expr + +val PreserveRuntimeAsyncCallSiteDebugPoint: g: TcGlobals -> m: FSharp.Compiler.Text.range -> expr: Expr -> Expr val GetRuntimeAsyncNonPreservableUses: g: TcGlobals -> expr: Expr -> Val list diff --git a/src/Compiler/TypedTree/RuntimeAsync.fs b/src/Compiler/TypedTree/RuntimeAsync.fs index ff7983cf918..95ceac9be98 100644 --- a/src/Compiler/TypedTree/RuntimeAsync.fs +++ b/src/Compiler/TypedTree/RuntimeAsync.fs @@ -10,6 +10,19 @@ open FSharp.Compiler.TypedTreeOps type RuntimeAsyncReturnInfo = { Body: Expr; TypeArgs: TType list } +let TryMapRuntimeAsyncMatchTargets (g: TcGlobals) matchInfo mapTarget = + let point, matchRange, tree, (targets: DecisionTreeTarget array), m = matchInfo + + let targets = + targets + |> Array.mapi (fun i (TTarget(vals, body, flags)) -> mapTarget i body |> Option.map (fun body -> TTarget(vals, body, flags))) + + if targets.Length > 0 && Array.forall Option.isSome targets then + let targets = Array.map Option.get targets + Some(Expr.Match(point, matchRange, tree, targets, m, tyOfExpr g targets[0].TargetExpression)) + else + None + let (|RuntimeAsyncReturn|_|) (g: TcGlobals) (vref: ValRef) = valRefEq g vref g.cgh__runtimeAsyncReturn_vref || valRefEq g vref g.cgh__runtimeAsyncReturnValueTask_vref diff --git a/src/Compiler/TypedTree/RuntimeAsync.fsi b/src/Compiler/TypedTree/RuntimeAsync.fsi index f59d8909d7a..357394e9e53 100644 --- a/src/Compiler/TypedTree/RuntimeAsync.fsi +++ b/src/Compiler/TypedTree/RuntimeAsync.fsi @@ -3,12 +3,20 @@ module internal FSharp.Compiler.RuntimeAsync open FSharp.Compiler.AbstractIL.IL +open FSharp.Compiler.Syntax open FSharp.Compiler.TcGlobals open FSharp.Compiler.Text open FSharp.Compiler.TypedTree type RuntimeAsyncReturnInfo = { Body: Expr; TypeArgs: TType list } +/// Rebuilds a match with every target rewritten, or returns None if any target cannot be. +val TryMapRuntimeAsyncMatchTargets: + g: TcGlobals -> + matchInfo: DebugPointAtBinding * range * DecisionTree * DecisionTreeTarget array * range -> + mapTarget: (int -> Expr -> Expr option) -> + Expr option + val TryGetRuntimeAsyncReturn: g: TcGlobals -> expr: Expr -> RuntimeAsyncReturnInfo option val (|RuntimeAsyncReturnFunction|_|): g: TcGlobals -> expr: Expr -> (ValRef * ValUseFlag * range) voption diff --git a/src/Compiler/TypedTree/TcGlobals.fs b/src/Compiler/TypedTree/TcGlobals.fs index a55bc5f5de4..2493f797ca1 100644 --- a/src/Compiler/TypedTree/TcGlobals.fs +++ b/src/Compiler/TypedTree/TcGlobals.fs @@ -903,6 +903,10 @@ type TcGlobals( let v_cgh__runtimeAsyncReturnValueTaskUnit_info = makeIntrinsicValRef(fslib_MFStateMachineHelpers_nleref, "__runtimeAsyncReturnValueTaskUnit" , None , None , [], ([[v_unit_ty]], mkNonGenericTy v_valueTask_nonGeneric_tcr)) // handled specially by the checker let v_cgh__runtimeAsyncSequence_info = makeIntrinsicValRef(fslib_MFStateMachineHelpers_nleref, "__runtimeAsyncSequence", None, None, [vara], ([[v_unit_ty --> mkSeqTy varaTy]], TType_app(v_IAsyncEnumerable_tcr, [varaTy], v_knownWithoutNull))) let v_cgh__runtimeAsyncSequenceCancellationToken_info = makeIntrinsicValRef(fslib_MFStateMachineHelpers_nleref, "__runtimeAsyncSequenceCancellationToken", None, None, [], ([[v_unit_ty]], v_system_CancellationToken_ty)) + let v_runtimeAsyncFragmentResult_tcr = mk_MFCompilerServices_tcref fslibCcu "RuntimeAsyncFragmentResult`1" + let v_runtimeAsyncFragmentResult_ty = TType_app(v_runtimeAsyncFragmentResult_tcr, [varbTy], v_knownWithoutNull) + let v_runtimeAsyncFragmentAwaiter_tcr = mk_MFCompilerServices_tcref fslibCcu "RuntimeAsyncFragmentAwaiter`1" + let v_cgh__runtimeAsyncOutline_info = makeIntrinsicValRef(fslib_MFStateMachineHelpers_nleref, "__runtimeAsyncOutline", None, None, [vara;varb], ([[varaTy --> varbTy]], varaTy --> TType_app(v_valueTask_tcr, [v_runtimeAsyncFragmentResult_ty], v_knownWithoutNull))) 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)) @@ -1812,6 +1816,8 @@ type TcGlobals( member val cgh__runtimeAsyncReturnValueTaskUnit_vref = ValRefForIntrinsic v_cgh__runtimeAsyncReturnValueTaskUnit_info member val cgh__runtimeAsyncSequence_vref = ValRefForIntrinsic v_cgh__runtimeAsyncSequence_info member val cgh__runtimeAsyncSequenceCancellationToken_vref = ValRefForIntrinsic v_cgh__runtimeAsyncSequenceCancellationToken_info + member val runtimeAsyncFragmentAwaiter_tcref = v_runtimeAsyncFragmentAwaiter_tcr + member val cgh__runtimeAsyncOutline_vref = ValRefForIntrinsic v_cgh__runtimeAsyncOutline_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 f4c3ec4caea..16f569ca9d8 100644 --- a/src/Compiler/TypedTree/TcGlobals.fsi +++ b/src/Compiler/TypedTree/TcGlobals.fsi @@ -453,6 +453,8 @@ type internal TcGlobals = member cgh__runtimeAsyncSequence_vref: TypedTree.ValRef member cgh__runtimeAsyncSequenceCancellationToken_vref: TypedTree.ValRef + member runtimeAsyncFragmentAwaiter_tcref: TypedTree.EntityRef + member cgh__runtimeAsyncOutline_vref: TypedTree.ValRef member cgh__useResumableCode_vref: TypedTree.ValRef diff --git a/src/FSharp.Core/resumable.fs b/src/FSharp.Core/resumable.fs index 439230ff5a0..e148eb833e1 100644 --- a/src/FSharp.Core/resumable.fs +++ b/src/FSharp.Core/resumable.fs @@ -76,6 +76,57 @@ type SetStateMachineMethodImpl<'Data> = delegate of byref = delegate of byref> -> 'Result +#if NET +[] +type RuntimeAsyncFragmentResult<'T>(result: Result<'T, System.Runtime.ExceptionServices.ExceptionDispatchInfo>) = + + let flowSuppressed = ExecutionContext.IsFlowSuppressed() + + let context = + if flowSuppressed then + ExecutionContext.RestoreFlow() + + try + ExecutionContext.Capture() + finally + ExecutionContext.SuppressFlow() |> ignore + else + ExecutionContext.Capture() + + let synchronizationContext = SynchronizationContext.Current + + member internal _.Restore() = + ExecutionContext.Restore(context) + SynchronizationContext.SetSynchronizationContext(synchronizationContext) + + if flowSuppressed then + ExecutionContext.SuppressFlow() |> ignore + + match result with + | Ok value -> value + | Error error -> + error.Throw() + Unchecked.defaultof<'T> + +[] +type RuntimeAsyncFragmentAwaiter<'T> = + val private Awaiter: ValueTaskAwaiter> + + new(fragment: ValueTask>) = { Awaiter = fragment.GetAwaiter() } + + member this.IsCompleted = this.Awaiter.IsCompleted + + member this.GetResult() = + this.Awaiter.GetResult().Restore() + + interface ICriticalNotifyCompletion with + member this.OnCompleted(continuation) = + this.Awaiter.OnCompleted(continuation) + + member this.UnsafeOnCompleted(continuation) = + this.Awaiter.UnsafeOnCompleted(continuation) +#endif + [] module StateMachineHelpers = @@ -126,6 +177,20 @@ module StateMachineHelpers = failwith "__runtimeAsyncReturnValueTask is a compiler intrinsic and should only be used in runtime-async method bodies" + let inline __runtimeAsyncOutline + ([] callback: 'Arg -> 'T) + : 'Arg -> ValueTask> = + fun argument -> + __runtimeAsyncReturnValueTask ( + let result = + try + Ok(callback argument) + with error -> + Error(System.Runtime.ExceptionServices.ExceptionDispatchInfo.Capture(error)) + + RuntimeAsyncFragmentResult(result) + ) + [] let __runtimeAsyncReturnUnit () : Task = failwith diff --git a/src/FSharp.Core/resumable.fsi b/src/FSharp.Core/resumable.fsi index 0082b32f3b8..8323cec9d5c 100644 --- a/src/FSharp.Core/resumable.fsi +++ b/src/FSharp.Core/resumable.fsi @@ -8,6 +8,30 @@ open System open System.Runtime.CompilerServices open System.Threading +#if NET +/// Carries an outlined runtime-async fragment's outcome and final ambient contexts. +[] +type RuntimeAsyncFragmentResult<'T> = + /// Captures an outcome and the current execution and synchronization contexts. + new: result: Result<'T, System.Runtime.ExceptionServices.ExceptionDispatchInfo> -> RuntimeAsyncFragmentResult<'T> + +/// Awaits an outlined fragment and restores its final contexts when retrieving the outcome. +[] +type RuntimeAsyncFragmentAwaiter<'T> = + val private Awaiter: ValueTaskAwaiter> + + /// Creates an awaiter for an outlined fragment. + new: fragment: System.Threading.Tasks.ValueTask> -> RuntimeAsyncFragmentAwaiter<'T> + + /// Indicates whether the fragment has completed. + member IsCompleted: bool + + /// Restores the fragment's final contexts, then returns its value or rethrows its exception. + member GetResult: unit -> 'T + + interface ICriticalNotifyCompletion +#endif + /// Acts as a template for struct state machines introduced by __stateMachine, and also as a reflective implementation [] type ResumableStateMachine<'Data> = @@ -204,6 +228,10 @@ module StateMachineHelpers = [] val __runtimeAsyncReturnValueTask : 'T -> System.Threading.Tasks.ValueTask<'T> + /// Outlines a callback as a runtime-async fragment that captures its outcome and final ambient contexts. + val inline __runtimeAsyncOutline: + [] callback: ('Arg -> 'T) -> ('Arg -> System.Threading.Tasks.ValueTask>) + [] val __runtimeAsyncReturnUnit : unit -> System.Threading.Tasks.Task diff --git a/tests/FSharp.Compiler.ComponentTests/Language/RuntimeAsyncEdgeCaseTests.fs b/tests/FSharp.Compiler.ComponentTests/Language/RuntimeAsyncEdgeCaseTests.fs index a15d77fee7c..af2c6ef57be 100644 --- a/tests/FSharp.Compiler.ComponentTests/Language/RuntimeAsyncEdgeCaseTests.fs +++ b/tests/FSharp.Compiler.ComponentTests/Language/RuntimeAsyncEdgeCaseTests.fs @@ -12,9 +12,6 @@ module Language.RuntimeAsyncEdgeCaseTests // sources (single async method, clean assembly), because ILChecker's NotPresent check is // assembly-scoped and the CE builder's own inline members legitimately contain `tail.`/`MoveNext`. // The CE `Run` lowers `do!`/`let!` to exactly this intrinsic form (see the execution facts). -// -// The "undiagnosed forbidden pattern" facts below pin restrictions that docs/runtime-async.md -// records as known and currently NOT diagnosed by the F# compiler (tail./localloc forbidden). open Xunit open FSharp.Test.Compiler @@ -26,7 +23,7 @@ let private builderPath = Path.Combine(runtimeAsyncDir, "RuntimeTaskBuilder.fs") // Builds a minimal direct-intrinsic compilation unit: the module header plus the opens every // StateMachineHelpers.__runtimeAsyncReturn body needs, then the supplied one-liner body. Used for the -// shape / absence / undiagnosed-pattern assertions, which must run on a single-method assembly. +// IL-shape and diagnostic assertions that need a single-method assembly. let private directIntrinsicSource body = String.concat "\n" [ "module M" diff --git a/tests/FSharp.Compiler.ComponentTests/Language/RuntimeAsyncTests.fs b/tests/FSharp.Compiler.ComponentTests/Language/RuntimeAsyncTests.fs index 8b3dc9dd07f..a15b6f7d2e8 100644 --- a/tests/FSharp.Compiler.ComponentTests/Language/RuntimeAsyncTests.fs +++ b/tests/FSharp.Compiler.ComponentTests/Language/RuntimeAsyncTests.fs @@ -1,12 +1,14 @@ -module Language.RuntimeAsyncTests +namespace Language -open Xunit -open FSharp.Test -open FSharp.Test.Compiler -open System.IO -open System.Text.RegularExpressions +module RuntimeAsyncTests = + open Xunit + open FSharp.Test + open FSharp.Test.Compiler + open System.IO + open System.Reflection.Metadata + open System.Text.RegularExpressions -let private runtimeAsyncSource = """ + let private runtimeAsyncSource = """ module RuntimeAsyncTest open System.Threading.Tasks @@ -31,7 +33,7 @@ type Calculator() = StateMachineHelpers.__runtimeAsyncReturn (x + 1) """ -let private runtimeAsyncRawSource = """ + let private runtimeAsyncRawSource = """ module RuntimeAsyncRawTest open System.Threading.Tasks @@ -81,7 +83,7 @@ type Calculator() = """ #if NETCOREAPP -let private nestedTaskSource = """module NestedTask + let private nestedTaskSource = """module NestedTask open System.Threading.Tasks open System.Runtime.CompilerServices @@ -96,65 +98,65 @@ let run (ready: Task) : Task = AsyncHelpers.Await child) """ -[] -[] -[] -[] -[] -[] -[] -[] -[] -let ``Issue 20576 rejects runtime Await in ordinary task methods`` (optimize: bool, shape: string) = - let configure = withLangVersionPreview >> withFSharpCoreShippedNet >> withOptimization optimize - let helper = "let inline awaitValue (ready: Task) = System.Runtime.CompilerServices.AsyncHelpers.Await ready" - let source, references = - match shape with - | "direct" -> nestedTaskSource, [] - | "local" -> - nestedTaskSource - .Replace("let child = task {", $"{helper}\n let child = task {{") - .Replace("let value = AsyncHelpers.Await ready", "let value = awaitValue ready"), [] - | "imported" -> - let library = FSharp($"module AwaitLibrary\nopen System.Threading.Tasks\n{helper}") |> asLibrary |> withName "AwaitLibrary" |> configure - library |> compile |> shouldSucceed |> ignore - nestedTaskSource.Replace("let value = AsyncHelpers.Await ready", "let value = AwaitLibrary.awaitValue ready"), [ library ] - | "unit" -> - nestedTaskSource.Replace("Task", "Task").Replace("let value = AsyncHelpers.Await ready", "AsyncHelpers.Await ready").Replace("return value + 1", "return ()"), [] - | _ -> failwith $"Unexpected shape: {shape}" - let result = - FSharp source - |> asLibrary - |> configure - |> withReferences references - |> compile - result |> shouldFail |> withErrorCode 3918 |> ignore - let ranges = - match optimize, shape with - // Optimized task-template inlining attributes the call to the inner task keyword. - | true, "local" -> [10, 21, 25] - | true, _ -> [9, 21, 25] - | false, "direct" -> [10, 25, 49] - | false, "local" -> [9, 52, 108; 11, 25, 41] - | false, "imported" -> [10, 25, 54] - | false, "unit" -> [10, 13, 37] - | _ -> failwith $"Unexpected shape: {shape}" - Assert.Equal(ranges.Length, result.Output.Diagnostics.Length) - Assert.Equal(ranges.Length, result.Output.PerFileErrors.Length) - result - |> withDiagnostics [ - for line, startCol, endCol in ranges -> - Error 3918, Line line, Col startCol, Line line, Col endCol, - "Runtime async suspension method 'Await' may only be called from a runtime async method." - ] - |> ignore - -[] -[] -[] -let ``Issue 20576 preserves ordinary task composition`` (optimize: bool) = - let result = - FSharp """ + [] + [] + [] + [] + [] + [] + [] + [] + [] + let ``Issue 20576 rejects runtime Await in ordinary task methods`` (optimize: bool, shape: string) = + let configure = withLangVersionPreview >> withFSharpCoreShippedNet >> withOptimization optimize + let helper = "let inline awaitValue (ready: Task) = System.Runtime.CompilerServices.AsyncHelpers.Await ready" + let source, references = + match shape with + | "direct" -> nestedTaskSource, [] + | "local" -> + nestedTaskSource + .Replace("let child = task {", $"{helper}\n let child = task {{") + .Replace("let value = AsyncHelpers.Await ready", "let value = awaitValue ready"), [] + | "imported" -> + let library = FSharp($"module AwaitLibrary\nopen System.Threading.Tasks\n{helper}") |> asLibrary |> withName "AwaitLibrary" |> configure + library |> compile |> shouldSucceed |> ignore + nestedTaskSource.Replace("let value = AsyncHelpers.Await ready", "let value = AwaitLibrary.awaitValue ready"), [ library ] + | "unit" -> + nestedTaskSource.Replace("Task", "Task").Replace("let value = AsyncHelpers.Await ready", "AsyncHelpers.Await ready").Replace("return value + 1", "return ()"), [] + | _ -> failwith $"Unexpected shape: {shape}" + let result = + FSharp source + |> asLibrary + |> configure + |> withReferences references + |> compile + result |> shouldFail |> withErrorCode 3918 |> ignore + let ranges = + match optimize, shape with + // Optimized task-template inlining attributes the call to the inner task keyword. + | true, "local" -> [10, 21, 25] + | true, _ -> [9, 21, 25] + | false, "direct" -> [10, 25, 49] + | false, "local" -> [9, 52, 108; 11, 25, 41] + | false, "imported" -> [10, 25, 54] + | false, "unit" -> [10, 13, 37] + | _ -> failwith $"Unexpected shape: {shape}" + Assert.Equal(ranges.Length, result.Output.Diagnostics.Length) + Assert.Equal(ranges.Length, result.Output.PerFileErrors.Length) + result + |> withDiagnostics [ + for line, startCol, endCol in ranges -> + Error 3918, Line line, Col startCol, Line line, Col endCol, + "Runtime async suspension method 'Await' may only be called from a runtime async method." + ] + |> ignore + + [] + [] + [] + let ``Issue 20576 preserves ordinary task composition`` (optimize: bool) = + let result = + FSharp """ module TaskComposition open System open System.Threading.Tasks @@ -205,45 +207,45 @@ let main _ = if not observed then failwith "Missing unit side effect" 0 """ - |> withLangVersionPreview - |> withFSharpCoreShippedNet - |> withOptimization optimize - |> compileExeAndRun - |> shouldSucceed - result |> withMetadataReader (fun md -> - let methods = [ for handle in md.MethodDefinitions -> md.GetMethodDefinition handle ] - let generatedNames = ["MoveNext"; "SetStateMachine"; "get_ResumptionPoint"; "get_Data"; "set_Data"] - for name in generatedNames do - let method = methods |> List.filter (fun method -> md.GetString method.Name = name) |> Assert.Single - Assert.Equal(0, int method.ImplAttributes &&& 0x2000) - if name = "MoveNext" then - let mutable signature = md.GetBlobReader method.Signature - Assert.False(signature.ReadSignatureHeader().IsGeneric) - Assert.Equal(0, signature.ReadCompressedInteger()) - Assert.Equal(System.Reflection.Metadata.SignatureTypeCode.Void, signature.ReadSignatureTypeCode()) - for name in ["child"; "run"] do - let method = methods |> List.filter (fun method -> md.GetString method.Name = name) |> Assert.Single - Assert.Equal(0x2000, int method.ImplAttributes &&& 0x2000) + |> withLangVersionPreview + |> withFSharpCoreShippedNet + |> withOptimization optimize + |> compileExeAndRun + |> shouldSucceed + result |> withMetadataReader (fun md -> + let methods = [ for handle in md.MethodDefinitions -> md.GetMethodDefinition handle ] + let generatedNames = ["MoveNext"; "SetStateMachine"; "get_ResumptionPoint"; "get_Data"; "set_Data"] + for name in generatedNames do + let method = methods |> List.filter (fun method -> md.GetString method.Name = name) |> Assert.Single + Assert.Equal(0, int method.ImplAttributes &&& 0x2000) + if name = "MoveNext" then + let mutable signature = md.GetBlobReader method.Signature + Assert.False(signature.ReadSignatureHeader().IsGeneric) + Assert.Equal(0, signature.ReadCompressedInteger()) + Assert.Equal(System.Reflection.Metadata.SignatureTypeCode.Void, signature.ReadSignatureTypeCode()) + for name in ["child"; "run"] do + let method = methods |> List.filter (fun method -> md.GetString method.Name = name) |> Assert.Single + Assert.Equal(0x2000, int method.ImplAttributes &&& 0x2000) + if optimize then + let liftedChild = + methods |> List.filter (fun method -> md.GetString method.Name = "Invoke" && int method.ImplAttributes &&& 0x2000 <> 0) + Assert.Single(liftedChild) |> ignore) + let _, _, il = ILChecker.verifyILAndReturnActual [] result.OutputPath.Value [] + let methodBody name = + Regex.Match(il, @"(?ms)^(?[ \t]*)\.method[^{}]*\b" + name + @"(?:<[^>]+>)?\([^{}]*\{.*?^\k\}").Value + let moveNext = methodBody "MoveNext" + Assert.NotEmpty(moveNext) + Assert.Contains("void", moveNext) + Assert.DoesNotContain("AsyncHelpers::Await", moveNext) if optimize then - let liftedChild = - methods |> List.filter (fun method -> md.GetString method.Name = "Invoke" && int method.ImplAttributes &&& 0x2000 <> 0) - Assert.Single(liftedChild) |> ignore) - let _, _, il = ILChecker.verifyILAndReturnActual [] result.OutputPath.Value [] - let methodBody name = - Regex.Match(il, @"(?ms)^(?[ \t]*)\.method[^{}]*\b" + name + @"(?:<[^>]+>)?\([^{}]*\{.*?^\k\}").Value - let moveNext = methodBody "MoveNext" - Assert.NotEmpty(moveNext) - Assert.Contains("void", moveNext) - Assert.DoesNotContain("AsyncHelpers::Await", moveNext) - if optimize then - Assert.Contains("AsyncHelpers::Await", methodBody "Invoke") - Assert.Contains("FSharpFunc`2", moveNext) - else - Assert.Contains("TaskComposition::child", moveNext) - for name in ["child"; "run"] do - Assert.Contains("AsyncHelpers::Await", methodBody name) + Assert.Contains("AsyncHelpers::Await", methodBody "Invoke") + Assert.Contains("FSharpFunc`2", moveNext) + else + Assert.Contains("TaskComposition::child", moveNext) + for name in ["child"; "run"] do + Assert.Contains("AsyncHelpers::Await", methodBody name) -let private runtimeAsyncCrossAssemblyLibrary = """ + let private runtimeAsyncCrossAssemblyLibrary = """ module RuntimeAsyncCrossAssemblyLibrary open System.Threading.Tasks @@ -256,7 +258,7 @@ let inline getValue () : Task = 42) """ -let private runtimeAsyncCrossAssemblyConsumer = """ + let private runtimeAsyncCrossAssemblyConsumer = """ module RuntimeAsyncCrossAssemblyConsumer open RuntimeAsyncCrossAssemblyLibrary @@ -266,12 +268,12 @@ let main _ = if getValue().GetAwaiter().GetResult() = 42 then 0 else 1 """ -[] -[] -[] -let ``runtime async rejects pinning from an imported inline function after suspension`` (optimize: bool) = - let library = - FSharp """ + [] + [] + [] + let ``runtime async rejects pinning from an imported inline function after suspension`` (optimize: bool) = + let library = + FSharp """ module RuntimeAsyncPinningCrossAssemblyLibrary open FSharp.NativeInterop @@ -282,12 +284,12 @@ let inline comparePin (array: byte[]) ([] action) = use after = fixed array struct (NativePtr.toNativeInt before, NativePtr.toNativeInt after) """ - |> withName "RuntimeAsyncPinningCrossAssemblyLibrary" - |> withLangVersionPreview - |> withFSharpCoreShippedNet - |> withNoWarn 9 + |> withName "RuntimeAsyncPinningCrossAssemblyLibrary" + |> withLangVersionPreview + |> withFSharpCoreShippedNet + |> withNoWarn 9 - FSharp """ + FSharp """ module RuntimeAsyncPinningCrossAssemblyConsumer open System.Threading.Tasks @@ -299,34 +301,34 @@ let run (data: byte[]) (gate: Task) = StateMachineHelpers.__runtimeAsyncReturn ( comparePin data (fun () -> AsyncHelpers.Await gate)) """ - |> withLangVersionPreview - |> withFSharpCoreShippedNet - |> withOptimization optimize - |> withNoWarn 9 - |> withReferences [ library ] - |> compile - |> shouldFail - |> withErrorCode 3919 - -[] -[] -[] -let ``runtime async methods execute across assemblies`` (optimize: bool) = - FSharp runtimeAsyncCrossAssemblyConsumer - |> withLangVersionPreview - |> withFSharpCoreShippedNet - |> withOptimization optimize - |> withReferences [ - FSharp runtimeAsyncCrossAssemblyLibrary - |> withName "RuntimeAsyncCrossAssemblyLibrary" |> withLangVersionPreview |> withFSharpCoreShippedNet - ] - |> compileExeAndRun - |> shouldSucceed + |> withOptimization optimize + |> withNoWarn 9 + |> withReferences [ library ] + |> compile + |> shouldFail + |> withErrorCode 3919 + + [] + [] + [] + let ``runtime async methods execute across assemblies`` (optimize: bool) = + FSharp runtimeAsyncCrossAssemblyConsumer + |> withLangVersionPreview + |> withFSharpCoreShippedNet + |> withOptimization optimize + |> withReferences [ + FSharp runtimeAsyncCrossAssemblyLibrary + |> withName "RuntimeAsyncCrossAssemblyLibrary" + |> withLangVersionPreview + |> withFSharpCoreShippedNet + ] + |> compileExeAndRun + |> shouldSucceed #endif -let private runtimeAsyncNestedInlineSource = """ + let private runtimeAsyncNestedInlineSource = """ module RuntimeAsyncNestedInlineTest open System.Threading.Tasks @@ -344,9 +346,9 @@ let f (task: Task) : Task = """ #if NETCOREAPP -[] -let ``runtime async is unavailable in FSharp 11.0`` () = - FSharp """ + [] + let ``runtime async is unavailable in FSharp 11.0`` () = + FSharp """ module RuntimeAsyncPreviewTest open System.Threading.Tasks @@ -355,15 +357,15 @@ open Microsoft.FSharp.Core.CompilerServices let f : Task = StateMachineHelpers.__runtimeAsyncReturn 1 """ - |> withLangVersion11 - |> withFSharpCoreShippedNet - |> compile - |> shouldFail - |> withErrorCode 3350 + |> withLangVersion11 + |> withFSharpCoreShippedNet + |> compile + |> shouldFail + |> withErrorCode 3350 -[] -let ``runtime async suspension outside runtime async is rejected`` () = - FSharp """ + [] + let ``runtime async suspension outside runtime async is rejected`` () = + FSharp """ module RuntimeAsyncSuspensionContextTest open System.Threading.Tasks @@ -374,15 +376,15 @@ let f () = AsyncHelpers.AwaitAwaiter(Task.Delay(1).GetAwaiter()) AsyncHelpers.UnsafeAwaitAwaiter(Task.Delay(1).GetAwaiter()) """ - |> withLangVersionPreview - |> withFSharpCoreShippedNet - |> compile - |> shouldFail - |> withErrorCodes [ 3918; 3918; 3918 ] + |> withLangVersionPreview + |> withFSharpCoreShippedNet + |> compile + |> shouldFail + |> withErrorCodes [ 3918; 3918; 3918 ] -[] -let ``runtime async rejects non Task result carriers`` () = - FSharp """ + [] + let ``runtime async rejects non Task result carriers`` () = + FSharp """ module RuntimeAsyncCarrierTest open Microsoft.FSharp.Core.CompilerServices @@ -390,83 +392,130 @@ open Microsoft.FSharp.Core.CompilerServices let f : string = StateMachineHelpers.__runtimeAsyncReturn "result" """ - |> withLangVersionPreview - |> withFSharpCoreShippedNet - |> compile - |> shouldFail - |> withErrorCode 1 + |> withLangVersionPreview + |> withFSharpCoreShippedNet + |> compile + |> shouldFail + |> withErrorCode 1 -[] -let ``runtime async intrinsic does not capture user-defined same-named values`` () = - FSharp """ + [] + let ``runtime async intrinsic does not capture user-defined same-named values`` () = + FSharp """ let __runtimeAsyncReturn value = value let result = __runtimeAsyncReturn 1 """ - |> typecheck - |> shouldSucceed - -[] -[] -[] -[] -let ``runtime async compiles functions and members`` langVersion = - FSharp runtimeAsyncSource - |> withLangVersion langVersion - |> withFSharpCoreShippedNet - |> compile - |> shouldSucceed - -[] -let ``runtime async supports Task and ValueTask return intrinsics`` () = - FSharp """ + |> typecheck + |> shouldSucceed + + [] + [] + [] + [] + let ``runtime async compiles functions and members`` langVersion = + FSharp runtimeAsyncSource + |> withLangVersion langVersion + |> withFSharpCoreShippedNet + |> compile + |> shouldSucceed + + [] + [] + [] + [] + [] + let ``runtime async supports Task and ValueTask return intrinsics`` (optimize: bool, asClosure: bool) = + let prefix, suffix = if asClosure then "invoke (fun () -> ", ")" else "", "" + let result = + FSharp $""" module RuntimeAsyncReturnShapesTest open System.Threading.Tasks +open System.Runtime.CompilerServices open Microsoft.FSharp.Core.CompilerServices -let taskResult () : Task = - StateMachineHelpers.__runtimeAsyncReturn 1 +[] +let invoke callback = callback () + +[] +let taskResult (gate: Task) : Task = + {prefix}StateMachineHelpers.__runtimeAsyncReturn (AsyncHelpers.Await gate + 1){suffix} + +[] +let valueTaskResult (gate: Task) : ValueTask = + {prefix}StateMachineHelpers.__runtimeAsyncReturnValueTask (AsyncHelpers.Await gate + 1){suffix} + +[] +let taskUnit (gate: Task) : Task = + {prefix}StateMachineHelpers.__runtimeAsyncReturnUnit (AsyncHelpers.Await gate |> ignore){suffix} -let valueTaskResult () : ValueTask = - StateMachineHelpers.__runtimeAsyncReturnValueTask 1 +[] +let valueTaskUnit (gate: Task) : ValueTask = + {prefix}StateMachineHelpers.__runtimeAsyncReturnValueTaskUnit (AsyncHelpers.Await gate |> ignore){suffix} -let taskUnit () : Task = - StateMachineHelpers.__runtimeAsyncReturnUnit () +[] +let taskGenericUnit (gate: Task) : Task = + {prefix}StateMachineHelpers.__runtimeAsyncReturn (AsyncHelpers.Await gate |> ignore){suffix} -let valueTaskUnit () : ValueTask = - StateMachineHelpers.__runtimeAsyncReturnValueTaskUnit () +[] +let valueTaskGenericUnit (gate: Task) : ValueTask = + {prefix}StateMachineHelpers.__runtimeAsyncReturnValueTask (AsyncHelpers.Await gate |> ignore){suffix} [] let main _ = - taskUnit().Wait() - taskResult().Result |> ignore - valueTaskResult().Result |> ignore - valueTaskUnit().AsTask().Wait() + let gate = TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously) + let taskValue = taskResult gate.Task + let valueTaskValue = valueTaskResult gate.Task + let taskVoid = taskUnit gate.Task + let valueTaskVoid = valueTaskUnit gate.Task + let taskUnitValue = taskGenericUnit gate.Task + let valueTaskUnitValue = valueTaskGenericUnit gate.Task + if taskValue.IsCompleted || valueTaskValue.IsCompleted || taskVoid.IsCompleted + || valueTaskVoid.IsCompleted || taskUnitValue.IsCompleted || valueTaskUnitValue.IsCompleted then + failwith "Return marker did not suspend" + gate.SetResult 41 + if taskValue.GetAwaiter().GetResult() <> 42 || valueTaskValue.AsTask().GetAwaiter().GetResult() <> 42 then + failwith "Generic result changed" + taskVoid.GetAwaiter().GetResult() + valueTaskVoid.AsTask().GetAwaiter().GetResult() + taskUnitValue.GetAwaiter().GetResult() + valueTaskUnitValue.AsTask().GetAwaiter().GetResult() 0 """ - |> withLangVersionPreview - |> withFSharpCoreShippedNet - |> compileExeAndRun - |> shouldSucceed - -[] -[] -[] -[")>] -[")>] -[] -[] -[")>] -[")>] -let ``Issue 20686 - await upcast Task`` (optimize: bool, carrier: string) = - let marker, value, toTask = - match carrier with - | "Task" -> "__runtimeAsyncReturnUnit", "()", "" - | "Task" -> "__runtimeAsyncReturn", "42", "" - | "ValueTask" -> "__runtimeAsyncReturnValueTaskUnit", "()", ".AsTask()" - | "ValueTask" -> "__runtimeAsyncReturnValueTask", "42", ".AsTask()" - | _ -> failwith $"Unexpected carrier: {carrier}" - FSharp $""" + |> withLangVersionPreview + |> withFSharpCoreShippedNet + |> withOptimization optimize + |> compileExeAndRun + |> shouldSucceed + + result |> withMetadataReader (fun md -> + let asyncMethods = + [ for handle in md.MethodDefinitions do + let method = md.GetMethodDefinition handle + if int method.ImplAttributes &&& 0x2000 <> 0 then + yield md.GetString method.Name ] + let expected = + if asClosure then List.replicate 6 "Invoke" + else [ "taskResult"; "valueTaskResult"; "taskUnit"; "valueTaskUnit"; "taskGenericUnit"; "valueTaskGenericUnit" ] + Assert.Equal(List.sort expected, List.sort asyncMethods)) + + [] + [] + [] + [")>] + [")>] + [] + [] + [")>] + [")>] + let ``Issue 20686 - await upcast Task`` (optimize: bool, carrier: string) = + let marker, value, toTask = + match carrier with + | "Task" -> "__runtimeAsyncReturnUnit", "()", "" + | "Task" -> "__runtimeAsyncReturn", "42", "" + | "ValueTask" -> "__runtimeAsyncReturnValueTaskUnit", "()", ".AsTask()" + | "ValueTask" -> "__runtimeAsyncReturnValueTask", "42", ".AsTask()" + | _ -> failwith $"Unexpected carrier: {carrier}" + FSharp $""" module TaskUpcasts open System @@ -532,118 +581,15 @@ let main _ = | _ -> failwith "Unexpected outcome" 0 """ - |> withLangVersionPreview - |> withFSharpCoreShippedNet - |> withOptimization optimize - |> compileExeAndRun - |> shouldSucceed - -[] -let ``runtime async supports inlining of a lambda`` () = - FSharp """ -module RuntimeAsyncInlineLambdaTest - -open System.Threading.Tasks -open System.Runtime.CompilerServices -open Microsoft.FSharp.Core.CompilerServices.StateMachineHelpers - -let inline makeFragment () = - fun x -> - AsyncHelpers.Await (Task.Delay 1000) - printfn "Hello from async function with input: %d" x - -let inline consume([] f) = - __runtimeAsyncReturn(f 42) - -[] -let main _ = - consume (makeFragment()) |> _.Result |> ignore - 0 -""" - |> withLangVersionPreview - |> withFSharpCoreShippedNet - |> compileExeAndRun - |> shouldSucceed - -[] -let ``runtime async supports inlining of a multi argument lambda`` () = - FSharp """ -module RuntimeAsyncInlineMultiArgumentLambdaTest - -open System.Threading.Tasks -open System.Runtime.CompilerServices -open Microsoft.FSharp.Core.CompilerServices.StateMachineHelpers - -let inline makeFragment () = - fun x y -> - AsyncHelpers.Await (Task.Delay 1) - x + y - -let inline consume([] f) = - __runtimeAsyncReturn(f 40 2) - -[] -let main _ = - if (consume (makeFragment())).Result <> 42 then 1 else 0 -""" - |> withLangVersionPreview - |> withFSharpCoreShippedNet - |> compileExeAndRun - |> shouldSucceed - -[] -let ``runtime async fuses suspension in inline returned closures`` () = - FSharp """ -module RuntimeAsyncInlineReturnedClosureTest - -open System -open System.Threading.Tasks -open System.Runtime.CompilerServices -open Microsoft.FSharp.Core.CompilerServices - -type Code = obj -> unit - -type Builder() = - member inline _.Delay([] generator: unit -> Code) : Code = - fun state -> generator() state - - member inline _.Zero() : Code = - fun _ -> () - - member inline _.Yield(_: int) : Code = - fun _ -> () - - member inline _.Bind(task: Task, [] continuation: unit -> Code) : Code = - fun state -> - AsyncHelpers.Await task - continuation() state - - member inline _.Combine(first: Code, [] second: Code) : Code = - fun state -> - first state - second state - - member inline _.Run([] code: Code) : Task = - StateMachineHelpers.__runtimeAsyncReturnUnit (code null) - -[] -let main _ = - let builder = Builder() - builder { - yield 1 - do! Task.Delay(1) - } - |> _.Wait() - 0 -""" - |> withLangVersionPreview - |> withFSharpCoreShippedNet - |> compileExeAndRun - |> shouldSucceed + |> withLangVersionPreview + |> withFSharpCoreShippedNet + |> withOptimization optimize + |> compileExeAndRun + |> shouldSucceed -[] -let ``runtime async ignores unreachable suspension`` () = - FSharp """ + [] + let ``runtime async ignores unreachable suspension`` () = + FSharp """ module RuntimeAsyncUnreachableSuspensionTest open System.Threading.Tasks @@ -658,16 +604,16 @@ let main _ = f () 0 """ - |> withLangVersionPreview - |> withFSharpCoreShippedNet - |> compileExeAndRun - |> shouldSucceed + |> withLangVersionPreview + |> withFSharpCoreShippedNet + |> compileExeAndRun + |> shouldSucceed -[] -[] -[] -let ``runtime async preserves reraise after a suspending handler`` (optimize: bool) = - FSharp """ + [] + [] + [] + let ``runtime async preserves reraise after a suspending handler`` (optimize: bool) = + FSharp """ module RuntimeAsyncReraiseTest open System @@ -693,56 +639,56 @@ let main _ = | e when e.Message = "boom" -> 0 | _ -> 1 """ - |> withLangVersionPreview - |> withFSharpCoreShippedNet - |> withOptimization optimize - |> compileExeAndRun - |> shouldSucceed - -let private checkReraiseOwnership optimized mode = - let methods = - match mode with - | "CONTROLS" -> [ "synchronousSelection", false; "synchronousCleanup", false; "filteredReraise", false; "Await", false ] - | "PRIMARY" -> [ "recover", true ] - | _ -> - [ "recover", true; "recoverString", true; "recoverValue", true; "innerOwner", true - "selection", true; "cleanup", true ] - let result = - FsFromPath(Path.Combine(__SOURCE_DIRECTORY__, "RuntimeAsync", "RuntimeAsyncReraiseOwnership.fs")) |> withLangVersionPreview |> withFSharpCoreShippedNet - |> withOptimization optimized - |> withDefines [mode] - |> asExe - |> compile + |> withOptimization optimize + |> compileExeAndRun |> shouldSucceed - result - |> verifyRuntimeAsyncExceptionRegions (methods |> List.map (fun (name, awaits) -> $"Reraise::{name}", awaits)) - |> run - |> shouldSucceed - -[] -[] -[] -let ``Issue 20575 runtime async nested reraise ownership`` optimized = - checkReraiseOwnership optimized "PRIMARY" - -[] -[] -[] -let ``Issue 20575 runtime async ownership matrix`` optimized = - checkReraiseOwnership optimized "MATRIX" - -[] -[] -[] -let ``Issue 20575 legal synchronous exception region controls`` optimized = - checkReraiseOwnership optimized "CONTROLS" - -let private compileInspectionProbe (body: string) = - // C# leaves these calls in their EH regions; these libraries must never be executed. - CSharp $""" + let private checkReraiseOwnership optimized mode = + let methods = + match mode with + | "CONTROLS" -> [ "synchronousSelection", false; "synchronousCleanup", false; "filteredReraise", false; "Await", false ] + | "PRIMARY" -> [ "recover", true ] + | _ -> + [ "recover", true; "recoverString", true; "recoverValue", true; "innerOwner", true + "selection", true; "cleanup", true ] + let result = + FsFromPath(Path.Combine(__SOURCE_DIRECTORY__, "RuntimeAsync", "RuntimeAsyncReraiseOwnership.fs")) + |> withLangVersionPreview + |> withFSharpCoreShippedNet + |> withOptimization optimized + |> withDefines [mode] + |> asExe + |> compile + |> shouldSucceed + + result + |> verifyRuntimeAsyncExceptionRegions (methods |> List.map (fun (name, awaits) -> $"Reraise::{name}", awaits)) + |> run + |> shouldSucceed + + [] + [] + [] + let ``Issue 20575 runtime async nested reraise ownership`` optimized = + checkReraiseOwnership optimized "PRIMARY" + + [] + [] + [] + let ``Issue 20575 runtime async ownership matrix`` optimized = + checkReraiseOwnership optimized "MATRIX" + + [] + [] + [] + let ``Issue 20575 legal synchronous exception region controls`` optimized = + checkReraiseOwnership optimized "CONTROLS" + + let private compileInspectionProbe (body: string) = + // C# leaves these calls in their EH regions; these libraries must never be executed. + CSharp $""" using System; using System.Runtime.CompilerServices; using System.Threading.Tasks; @@ -755,61 +701,61 @@ public static class Inspection }} }} """ - |> withName "Inspection" - |> asLibrary - |> compile - |> shouldSucceed - -[] -[] -[] -[] -[] -[] -[] -[] -[] -let ``Issue 20575 inspection rejects suspension in exception regions`` body forbidden = - let result = compileInspectionProbe body - if forbidden then - let error = - Assert.Throws(fun () -> - result |> verifyRuntimeAsyncExceptionRegions ["Inspection::Probe", false] |> ignore) - Assert.StartsWith("Inspection::Probe: suspension in exception handler/filter at IL_", error.Message) - Assert.Contains("regions (kind, try offset/length, handler offset/length, filter offset):", error.Message) - else - result |> verifyRuntimeAsyncExceptionRegions ["Inspection::Probe", false] |> ignore - -[] -[] -[] -[] -let ``Issue 20575 inspection rejects missing probes and suspension`` methodName requiresAwait message = - let result = - FSharp (runtimeAsyncSource + "\ntype AbstractProbe = abstract MissingBody: unit -> unit\n") - |> withLangVersionPreview - |> withFSharpCoreShippedNet + |> withName "Inspection" |> asLibrary |> compile |> shouldSucceed - let error = - Assert.Throws(fun () -> - result |> verifyRuntimeAsyncExceptionRegions [methodName, requiresAwait] |> ignore) - Assert.Equal($"{message}{methodName}", error.Message) - -[] -let ``Issue 20575 inspection requires runtime async metadata even with suspension`` () = - let result = compileInspectionProbe "AsyncHelpers.Await(audit);" - result |> verifyRuntimeAsyncExceptionRegions ["Inspection::Probe", false] |> ignore - let error = - Assert.Throws(fun () -> - result |> verifyRuntimeAsyncExceptionRegions ["Inspection::Probe", true] |> ignore) - Assert.Equal("Missing runtime-async body with suspension in Inspection::Probe", error.Message) - -[] -let ``runtime async rejects stackalloc across suspension`` () = - FSharp """ + [] + [] + [] + [] + [] + [] + [] + [] + [] + let ``Issue 20575 inspection rejects suspension in exception regions`` body forbidden = + let result = compileInspectionProbe body + if forbidden then + let error = + Assert.Throws(fun () -> + result |> verifyRuntimeAsyncExceptionRegions ["Inspection::Probe", false] |> ignore) + Assert.StartsWith("Inspection::Probe: suspension in exception handler/filter at IL_", error.Message) + Assert.Contains("regions (kind, try offset/length, handler offset/length, filter offset):", error.Message) + else + result |> verifyRuntimeAsyncExceptionRegions ["Inspection::Probe", false] |> ignore + + [] + [] + [] + [] + let ``Issue 20575 inspection rejects missing probes and suspension`` methodName requiresAwait message = + let result = + FSharp (runtimeAsyncSource + "\ntype AbstractProbe = abstract MissingBody: unit -> unit\n") + |> withLangVersionPreview + |> withFSharpCoreShippedNet + |> asLibrary + |> compile + |> shouldSucceed + + let error = + Assert.Throws(fun () -> + result |> verifyRuntimeAsyncExceptionRegions [methodName, requiresAwait] |> ignore) + Assert.Equal($"{message}{methodName}", error.Message) + + [] + let ``Issue 20575 inspection requires runtime async metadata even with suspension`` () = + let result = compileInspectionProbe "AsyncHelpers.Await(audit);" + result |> verifyRuntimeAsyncExceptionRegions ["Inspection::Probe", false] |> ignore + let error = + Assert.Throws(fun () -> + result |> verifyRuntimeAsyncExceptionRegions ["Inspection::Probe", true] |> ignore) + Assert.Equal("Missing runtime-async body with suspension in Inspection::Probe", error.Message) + + [] + let ``runtime async rejects stackalloc across suspension`` () = + FSharp """ module RuntimeAsyncStackallocTest open System.Threading.Tasks @@ -824,15 +770,15 @@ let f () : Task = AsyncHelpers.Await(Task.Delay 1) NativePtr.read p) """ - |> withLangVersionPreview - |> withFSharpCoreShippedNet - |> compile - |> shouldFail - |> withErrorCode 3920 + |> withLangVersionPreview + |> withFSharpCoreShippedNet + |> compile + |> shouldFail + |> withErrorCode 3920 -[] -let ``runtime async rejects stackalloc without suspension`` () = - FSharp """ + [] + let ``runtime async rejects stackalloc without suspension`` () = + FSharp """ module RuntimeAsyncStackallocWithoutSuspensionTest open System.Threading.Tasks @@ -846,15 +792,15 @@ let f () : Task = NativePtr.write p 42 NativePtr.read p) """ - |> withLangVersionPreview - |> withFSharpCoreShippedNet - |> compile - |> shouldFail - |> withErrorCode 3920 + |> withLangVersionPreview + |> withFSharpCoreShippedNet + |> compile + |> shouldFail + |> withErrorCode 3920 -[] -let ``runtime async rejects a byref captured by an inlined closure`` () = - FSharp """ + [] + let ``runtime async rejects a byref captured by an inlined closure`` () = + FSharp """ module RuntimeAsyncByrefClosureTest open System.Threading.Tasks @@ -865,62 +811,15 @@ let f (x: byref) : Task = let y = x StateMachineHelpers.__runtimeAsyncReturn (x + y) """ - |> withLangVersionPreview - |> withFSharpCoreShippedNet - |> compile - |> shouldFail - |> withErrorCode 406 - -[] -[] -[] -let ``runtime async does not duplicate effectful InlineIfLambda arguments`` (optimize: bool) = - FSharp """ -module RuntimeAsyncInlineIfLambdaEffectsTest - -open System.Threading.Tasks -open System.Runtime.CompilerServices -open Microsoft.FSharp.Core.CompilerServices.StateMachineHelpers - -let mutable calls = 0 - -let effect () = - calls <- calls + 1 - fun () -> 1 - -let inline plainTwice ([] f: unit -> int) = f () + f () -let inline twice ([] f: unit -> int) = - __runtimeAsyncReturn (f () + f ()) -let inline unused ([] f: unit -> int) = - __runtimeAsyncReturn 20 - -[] -let main _ = - let plainResult = plainTwice (effect ()) - let plainCalls = calls - calls <- 0 - let twiceResult = (twice (effect ())).GetAwaiter().GetResult() - let twiceCalls = calls - calls <- 0 - let unusedResult = (unused (effect ())).GetAwaiter().GetResult() - let unusedCalls = calls - - if plainResult = 2 && plainCalls = 1 - && twiceResult = 2 && twiceCalls = 1 - && unusedResult = 20 && unusedCalls = 1 then - 0 - else - 1 -""" - |> withLangVersionPreview - |> withFSharpCoreShippedNet - |> withOptimization optimize - |> compileExeAndRun - |> shouldSucceed + |> withLangVersionPreview + |> withFSharpCoreShippedNet + |> compile + |> shouldFail + |> withErrorCode 406 -[] -let ``runtime async pipe syntax is gated by the language version`` () = - FSharp """ + [] + let ``runtime async pipe syntax is gated by the language version`` () = + FSharp """ module RuntimeAsyncPipeGateTest open System.Threading.Tasks @@ -929,57 +828,15 @@ open Microsoft.FSharp.Core.CompilerServices let f (x: int) : Task = x |> StateMachineHelpers.__runtimeAsyncReturn """ - |> withLangVersion90 - |> withFSharpCoreShippedNet - |> compile - |> shouldFail - |> withErrorCode 3350 - -[] -[] -[] -let ``runtime async preserves evaluation order for curried inline applications`` (optimize: bool) = - FSharp """ -module RuntimeAsyncCurriedApplicationTest - -open System.Threading.Tasks -open Microsoft.FSharp.Core.CompilerServices - -let events = ResizeArray() - -let step name value = - events.Add name - value - -let inline apply f x y = f x y - -let f () : Task = - StateMachineHelpers.__runtimeAsyncReturn ( - apply - (fun x -> - events.Add "body" - fun y -> x + y) - (step "arg1" 1) - (step "arg2" 2)) - -[] -let main _ = - let result = f().GetAwaiter().GetResult() - - if result = 3 && (events |> Seq.toList) = [ "arg1"; "arg2"; "body" ] then - 0 - else - 1 -""" - |> withLangVersionPreview - |> withFSharpCoreShippedNet - |> withOptimization optimize - |> compileExeAndRun - |> shouldSucceed + |> withLangVersion90 + |> withFSharpCoreShippedNet + |> compile + |> shouldFail + |> withErrorCode 3350 -[] -let ``runtime async rejects synchronized methods`` () = - FSharp """ + [] + let ``runtime async rejects synchronized methods`` () = + FSharp """ module RuntimeAsyncSynchronizedTest open System @@ -992,144 +849,108 @@ let f () : Task = StateMachineHelpers.__runtimeAsyncReturn ( AsyncHelpers.Await(Task.Delay(1).ContinueWith(fun _ -> 1))) """ - |> withLangVersionPreview - |> withFSharpCoreShippedNet - |> compile - |> shouldFail - |> withErrorCode 3921 - -[] -let ``runtime async combines awaited chunks without delegates`` () = - FSharp runtimeAsyncRawSource - |> withLangVersionPreview - |> withFSharpCoreShippedNet - |> compile - |> verifyILContains [ - "Task::Delay(int32)" - "AsyncHelpers::Await(class [runtime]System.Threading.Tasks.Task)" - ] - |> shouldSucceed - -[] -let ``runtime async specializes nested inline suspensions without optimization`` () = - FSharp runtimeAsyncNestedInlineSource - |> withLangVersionPreview - |> withFSharpCoreShippedNet - |> withNoOptimize - |> compile - |> verifyILContains [ "AsyncHelpers::Await(class [runtime]System.Threading.Tasks.Task`1)" ] - -[] -[] -[] -let ``runtime task builder fixture executes through runtime async`` (optimize: bool) = - FsFromPath (Path.Combine(__SOURCE_DIRECTORY__, "RuntimeAsync", "RuntimeTaskBuilder.fs")) - |> withAdditionalSourceFile ( - SourceFromPath (Path.Combine(__SOURCE_DIRECTORY__, "RuntimeAsync", "RuntimeTasks.fs")) - ) - |> withLangVersionPreview - |> withFSharpCoreShippedNet - |> withOptimization optimize - |> compileExeAndRun - |> shouldSucceed - -[] -let ``runtime task AsyncLocal values propagate through runtime async`` () = - FsFromPath (Path.Combine(__SOURCE_DIRECTORY__, "RuntimeAsync", "RuntimeTaskBuilder.fs")) - |> withAdditionalSourceFile ( - SourceFromPath (Path.Combine(__SOURCE_DIRECTORY__, "RuntimeAsync", "RuntimeAsyncAsyncLocal.fs")) - ) - |> withLangVersionPreview - |> withFSharpCoreShippedNet - |> compileExeAndRun - |> shouldSucceed - -[] -let ``runtime async direct intrinsic fixture executes`` () = - Path.Combine(__SOURCE_DIRECTORY__, "RuntimeAsync", "RuntimeAsyncBasic.fs") - |> FsFromPath - |> withLangVersionPreview - |> withFSharpCoreShippedNet - |> compileExeAndRun - |> shouldSucceed - -[] -[] -[] -let ``runtime async low level async enumerable fixture executes`` (optimize: bool) = - Path.Combine(__SOURCE_DIRECTORY__, "RuntimeAsync", "RuntimeAsyncEnumerableLowLevel.fs") - |> FsFromPath - |> withLangVersionPreview - |> withFSharpCoreShippedNet - |> withOptimization optimize - |> compileExeAndRun - |> shouldSucceed - -[] -[] -[] -let ``runtime async enumerable builder fixture executes`` (optimize: bool) = - FsFromPath (Path.Combine(__SOURCE_DIRECTORY__, "RuntimeAsync", "RuntimeTaskBuilder.fs")) - |> withAdditionalSourceFile ( - SourceFromPath (Path.Combine(__SOURCE_DIRECTORY__, "RuntimeAsync", "RuntimeAsyncEnumerable.fs")) - ) - |> withAdditionalSourceFile ( - SourceFromPath (Path.Combine(__SOURCE_DIRECTORY__, "RuntimeAsync", "RuntimeAsyncEnumerableTests.fs")) - ) - |> withLangVersionPreview - |> withFSharpCoreShippedNet - |> withOptimization optimize - |> compileExeAndRun - |> shouldSucceed - -[] -let ``runtime async enumerable CE debug points stay at call sites`` () = - let source = - """module RuntimeAsyncEnumerableDebug -open System.Threading.Tasks -open RuntimeAsyncEnumerable - -let first () = - asyncSeq { - do! Task.Delay 1 - yield 1 - } + |> withLangVersionPreview + |> withFSharpCoreShippedNet + |> compile + |> shouldFail + |> withErrorCode 3921 -let second () = - asyncSeq { - do! Task.Delay 1 - yield 2 - } -""" + [] + let ``runtime async combines awaited chunks without delegates`` () = + FSharp runtimeAsyncRawSource + |> withLangVersionPreview + |> withFSharpCoreShippedNet + |> compile + |> verifyILContains [ + "Task::Delay(int32)" + "AsyncHelpers::Await(class [runtime]System.Threading.Tasks.Task)" + ] + |> shouldSucceed + + [] + let ``runtime async specializes nested inline suspensions without optimization`` () = + FSharp runtimeAsyncNestedInlineSource + |> withLangVersionPreview + |> withFSharpCoreShippedNet + |> withNoOptimize + |> compile + |> verifyILContains [ "AsyncHelpers::Await(class [runtime]System.Threading.Tasks.Task`1)" ] + + [] + [] + [] + let ``runtime task builder fixture executes through runtime async`` (optimize: bool) = + FsFromPath (Path.Combine(__SOURCE_DIRECTORY__, "RuntimeAsync", "RuntimeTaskBuilder.fs")) + |> withAdditionalSourceFile ( + SourceFromPath (Path.Combine(__SOURCE_DIRECTORY__, "RuntimeAsync", "RuntimeTasks.fs")) + ) + |> withLangVersionPreview + |> withFSharpCoreShippedNet + |> withOptimization optimize + |> compileExeAndRun + |> shouldSucceed + + [] + let ``runtime task AsyncLocal values propagate through runtime async`` () = + FsFromPath (Path.Combine(__SOURCE_DIRECTORY__, "RuntimeAsync", "RuntimeTaskBuilder.fs")) + |> withAdditionalSourceFile ( + SourceFromPath (Path.Combine(__SOURCE_DIRECTORY__, "RuntimeAsync", "RuntimeAsyncAsyncLocal.fs")) + ) + |> withLangVersionPreview + |> withFSharpCoreShippedNet + |> compileExeAndRun + |> shouldSucceed - FsFromPath (Path.Combine(__SOURCE_DIRECTORY__, "RuntimeAsync", "RuntimeTaskBuilder.fs")) - |> withAdditionalSourceFile ( - SourceFromPath (Path.Combine(__SOURCE_DIRECTORY__, "RuntimeAsync", "RuntimeAsyncEnumerable.fs")) - ) - |> withAdditionalSourceFile (FsSourceWithFileName "RuntimeAsyncEnumerableDebug.fs" source) - |> withLangVersionPreview - |> withFSharpCoreShippedNet - |> withPortablePdb - |> withNoOptimize - |> compile - |> shouldSucceed - |> verifyPdb [ - VerifyRuntimeAsyncMethodSequencePointsInSource("RuntimeAsyncEnumerableDebug.fs", 6, 8) - VerifyRuntimeAsyncMethodSequencePointsInSource("RuntimeAsyncEnumerableDebug.fs", 12, 14) - ] - -[] -let ``runtime async suspension in exception region executes`` () = - Path.Combine(__SOURCE_DIRECTORY__, "RuntimeAsync", "RuntimeTasksAsyncDisposalException.fs") - |> FsFromPath - |> withLangVersionPreview - |> withFSharpCoreShippedNet - |> compileExeAndRun - |> shouldSucceed - -[] -let ``runtime async reraise preserves the innermost exception after suspension`` () = - FSharp """ + [] + let ``runtime async direct intrinsic fixture executes`` () = + Path.Combine(__SOURCE_DIRECTORY__, "RuntimeAsync", "RuntimeAsyncBasic.fs") + |> FsFromPath + |> withLangVersionPreview + |> withFSharpCoreShippedNet + |> compileExeAndRun + |> shouldSucceed + + [] + [] + [] + let ``runtime async low level async enumerable fixture executes`` (optimize: bool) = + Path.Combine(__SOURCE_DIRECTORY__, "RuntimeAsync", "RuntimeAsyncEnumerableLowLevel.fs") + |> FsFromPath + |> withLangVersionPreview + |> withFSharpCoreShippedNet + |> withOptimization optimize + |> compileExeAndRun + |> shouldSucceed + + [] + [] + [] + let ``runtime async enumerable builder fixture executes`` (optimize: bool) = + FsFromPath (Path.Combine(__SOURCE_DIRECTORY__, "RuntimeAsync", "RuntimeTaskBuilder.fs")) + |> withAdditionalSourceFile ( + SourceFromPath (Path.Combine(__SOURCE_DIRECTORY__, "RuntimeAsync", "RuntimeAsyncEnumerable.fs")) + ) + |> withAdditionalSourceFile ( + SourceFromPath (Path.Combine(__SOURCE_DIRECTORY__, "RuntimeAsync", "RuntimeAsyncEnumerableTests.fs")) + ) + |> withLangVersionPreview + |> withFSharpCoreShippedNet + |> withOptimization optimize + |> compileExeAndRun + |> shouldSucceed + + [] + let ``runtime async suspension in exception region executes`` () = + Path.Combine(__SOURCE_DIRECTORY__, "RuntimeAsync", "RuntimeTasksAsyncDisposalException.fs") + |> FsFromPath + |> withLangVersionPreview + |> withFSharpCoreShippedNet + |> compileExeAndRun + |> shouldSucceed + + [] + let ``runtime async reraise preserves the innermost exception after suspension`` () = + FSharp """ module RuntimeAsyncReraiseTest open System @@ -1157,16 +978,16 @@ let main _ = | :? ArgumentException as ex when ex.Message = "inner" -> 0 | _ -> 1 """ - |> withLangVersionPreview - |> withFSharpCoreShippedNet - |> compileExeAndRun - |> shouldSucceed + |> withLangVersionPreview + |> withFSharpCoreShippedNet + |> compileExeAndRun + |> shouldSucceed -[] -[] -[] -let ``runtime async preserves an outer reraise after a nested suspension`` (optimize: bool) = - FSharp """ + [] + [] + [] + let ``runtime async preserves an outer reraise after a nested suspension`` (optimize: bool) = + FSharp """ module RuntimeAsyncNestedReraiseTest open System @@ -1200,17 +1021,17 @@ let main _ = | ex when obj.ReferenceEquals(ex, outer) && trace.ToArray() = [| "finally" |] -> 0 | _ -> 1 """ - |> withLangVersionPreview - |> withFSharpCoreShippedNet - |> withOptimization optimize - |> compileExeAndRun - |> shouldSucceed - -[] -[] -[] -let ``runtime async rejects suspension in an unmarked object member`` (optimize: bool) = - FSharp """ + |> withLangVersionPreview + |> withFSharpCoreShippedNet + |> withOptimization optimize + |> compileExeAndRun + |> shouldSucceed + + [] + [] + [] + let ``runtime async rejects suspension in an unmarked object member`` (optimize: bool) = + FSharp """ module RuntimeAsyncUnmarkedMemberTest open System.Threading.Tasks @@ -1225,18 +1046,18 @@ let make (gate: Task) = { new IInt with member _.Get() = AsyncHelpers.Await gate }) """ - |> withLangVersionPreview - |> withFSharpCoreShippedNet - |> withOptimization optimize - |> compile - |> shouldFail - |> withErrorCode 3918 - -[] -[] -[] -let ``runtime async marker inside an object member remains supported`` (optimize: bool) = - FSharp """ + |> withLangVersionPreview + |> withFSharpCoreShippedNet + |> withOptimization optimize + |> compile + |> shouldFail + |> withErrorCode 3918 + + [] + [] + [] + let ``runtime async marker inside an object member remains supported`` (optimize: bool) = + FSharp """ module RuntimeAsyncMarkedObjectMemberTest open System.Threading.Tasks @@ -1256,64 +1077,17 @@ let main _ = let value = (make (Task.FromResult 41)).Get().GetAwaiter().GetResult() if value = 41 then 0 else 1 """ - |> withLangVersionPreview - |> withFSharpCoreShippedNet - |> withOptimization optimize - |> compileExeAndRun - |> shouldSucceed - -[] -[] -[] -let ``runtime async evaluates conditional callback construction once`` (optimize: bool) = - FSharp """ -module RuntimeAsyncConditionalCallbackConstructionTest - -open System.Threading.Tasks -open System.Runtime.CompilerServices -open Microsoft.FSharp.Core.CompilerServices - -let events = ResizeArray() -let note text = events.Add text - -[] -let choose () = - note "choose" - true - -let inline twice ([] body: unit -> int) = - StateMachineHelpers.__runtimeAsyncReturn (body() + body()) - -let run (gate: Task) = - twice ( - if choose() then - note "construct" - fun () -> - note "body" - AsyncHelpers.Await gate - else - fun () -> 0) - -[] -let main _ = - let gate = TaskCompletionSource() - let work = run gate.Task - gate.SetResult(21) - let result = work.GetAwaiter().GetResult() + |> withLangVersionPreview + |> withFSharpCoreShippedNet + |> withOptimization optimize + |> compileExeAndRun + |> shouldSucceed - if result = 42 && events.ToArray() = [| "choose"; "construct"; "body"; "body" |] then 0 else 1 -""" - |> withLangVersionPreview - |> withFSharpCoreShippedNet - |> withOptimization optimize - |> compileExeAndRun - |> shouldSucceed - -[] -[] -[] -let ``runtime async evaluates a suspending exception filter once`` (optimize: bool) = - FSharp """ + [] + [] + [] + let ``runtime async evaluates a suspending exception filter once`` (optimize: bool) = + FSharp """ module RuntimeAsyncExceptionFilterTest open System.Threading.Tasks @@ -1344,41 +1118,15 @@ let main _ = with _ -> 1 """ - |> withLangVersionPreview - |> withFSharpCoreShippedNet - |> withOptimization optimize - |> compileExeAndRun - |> shouldSucceed - -[] -let ``runtime async evaluates inline callback construction once`` () = - FSharp """ -module RuntimeAsyncCallbackConstructionTest - -open System.Threading.Tasks -open Microsoft.FSharp.Core.CompilerServices - -let mutable constructed = 0 - -let inline twice ([] f: unit -> int) = - StateMachineHelpers.__runtimeAsyncReturn (f () + f ()) - -let run () = - twice (constructed <- constructed + 1; fun () -> 21) - -[] -let main _ = - let result = run().GetAwaiter().GetResult() - if result = 42 && constructed = 1 then 0 else 1 -""" - |> withLangVersionPreview - |> withFSharpCoreShippedNet - |> compileExeAndRun - |> shouldSucceed + |> withLangVersionPreview + |> withFSharpCoreShippedNet + |> withOptimization optimize + |> compileExeAndRun + |> shouldSucceed -[] -let ``runtime async is gated without preview when optimization is disabled`` () = - FSharp """ + [] + let ``runtime async is unavailable in FSharp 9.0 when optimization is disabled`` () = + FSharp """ module RuntimeAsyncNoOptimizePreviewTest open System.Threading.Tasks @@ -1387,16 +1135,16 @@ open Microsoft.FSharp.Core.CompilerServices let f (x: int) : Task = x |> StateMachineHelpers.__runtimeAsyncReturn """ - |> withLangVersion90 - |> withFSharpCoreShippedNet - |> withNoOptimize - |> compile - |> shouldFail - |> withErrorCode 3350 + |> withLangVersion90 + |> withFSharpCoreShippedNet + |> withNoOptimize + |> compile + |> shouldFail + |> withErrorCode 3350 -[] -let ``runtime async rejects suspension inside an ordinary sequence`` () = - FSharp """ + [] + let ``runtime async rejects suspension inside an ordinary sequence`` () = + FSharp """ module RuntimeAsyncNestedSequenceTest open System.Threading.Tasks @@ -1411,23 +1159,1906 @@ let f (gate: Task) : Task = yield value + 1 }) """ - |> withLangVersionPreview - |> withFSharpCoreShippedNet - |> compile - |> shouldFail - |> withErrorCode 3918 + |> withLangVersionPreview + |> withFSharpCoreShippedNet + |> compile + |> shouldFail + |> withErrorCode 3918 #else -[] -let ``runtime async intrinsic is only available in the shipped net FSharp.Core`` () = - FSharp """ + [] + let ``runtime async intrinsic is only available in the shipped net FSharp.Core`` () = + FSharp """ open System.Threading.Tasks open Microsoft.FSharp.Core.CompilerServices let f : Task = StateMachineHelpers.__runtimeAsyncReturn 1 """ - |> typecheck - |> shouldFail - |> withErrorCode 39 + |> typecheck + |> shouldFail + |> withErrorCode 39 +#endif + +module RuntimeAsyncCallbackTests = + open Xunit + open FSharp.Test + open FSharp.Test.Compiler + open System.IO + +#if NETCOREAPP + [] + let ``runtime async supports inlining of a lambda`` () = + FSharp """ +module RuntimeAsyncInlineLambdaTest + +open System.Threading.Tasks +open System.Runtime.CompilerServices +open Microsoft.FSharp.Core.CompilerServices.StateMachineHelpers + +let inline makeFragment () = + fun x -> + AsyncHelpers.Await (Task.Delay 1000) + printfn "Hello from async function with input: %d" x + +let inline consume([] f) = + __runtimeAsyncReturn(f 42) + +[] +let main _ = + consume (makeFragment()) |> _.Result |> ignore + 0 +""" + |> withLangVersionPreview + |> withFSharpCoreShippedNet + |> compileExeAndRun + |> shouldSucceed + + [] + let ``runtime async supports inlining of a multi argument lambda`` () = + FSharp """ +module RuntimeAsyncInlineMultiArgumentLambdaTest + +open System.Threading.Tasks +open System.Runtime.CompilerServices +open Microsoft.FSharp.Core.CompilerServices.StateMachineHelpers + +let inline makeFragment () = + fun x y -> + AsyncHelpers.Await (Task.Delay 1) + x + y + +let inline consume([] f) = + __runtimeAsyncReturn(f 40 2) + +[] +let main _ = + if (consume (makeFragment())).Result <> 42 then 1 else 0 +""" + |> withLangVersionPreview + |> withFSharpCoreShippedNet + |> compileExeAndRun + |> shouldSucceed + + [] + let ``runtime async fuses suspension in inline returned closures`` () = + FSharp """ +module RuntimeAsyncInlineReturnedClosureTest + +open System +open System.Threading.Tasks +open System.Runtime.CompilerServices +open Microsoft.FSharp.Core.CompilerServices + +type Code = obj -> unit + +type Builder() = + member inline _.Delay([] generator: unit -> Code) : Code = + fun state -> generator() state + + member inline _.Zero() : Code = + fun _ -> () + + member inline _.Yield(_: int) : Code = + fun _ -> () + + member inline _.Bind(task: Task, [] continuation: unit -> Code) : Code = + fun state -> + AsyncHelpers.Await task + continuation() state + + member inline _.Combine(first: Code, [] second: Code) : Code = + fun state -> + first state + second state + + member inline _.Run([] code: Code) : Task = + StateMachineHelpers.__runtimeAsyncReturnUnit (code null) + +[] +let main _ = + let builder = Builder() + builder { + yield 1 + do! Task.Delay(1) + } + |> _.Wait() + 0 +""" + |> withLangVersionPreview + |> withFSharpCoreShippedNet + |> compileExeAndRun + |> shouldSucceed + + [] + [] + [] + [] + [] + let ``runtime async fuses returned closures across recursive bindings`` (optimize: bool, nestedRuntimeAsync: bool) = + let loopBody = + if nestedRuntimeAsync then + "__runtimeAsyncReturn (if count = 0 then AsyncHelpers.Await ready else AsyncHelpers.Await (loop (count - 1)))" + else + "if count = 0 then ready else loop (count - 1)" + + FSharp $""" +module RuntimeAsyncRecursiveReturnedClosureTest + +open System.Threading.Tasks +open System.Runtime.CompilerServices +open Microsoft.FSharp.Core.CompilerServices.StateMachineHelpers + +let mutable constructions = 0 +let mutable invocations = 0 + +let inline runBody ([] body: unit -> unit -> int) = + __runtimeAsyncReturn (body () ()) + +let run (ready: Task) = + runBody (fun () -> + constructions <- constructions + 1 + let rec loop count : Task = + {loopBody} + fun () -> + invocations <- invocations + 1 + AsyncHelpers.Await (loop 2)) + +[] +let main _ = + let ready = TaskCompletionSource() + let result = run ready.Task + if result.IsCompleted || constructions <> 1 || invocations <> 1 then + failwith "Callback evaluation or suspension was lost" + ready.SetResult 42 + if result.GetAwaiter().GetResult() <> 42 || constructions <> 1 || invocations <> 1 then + failwith "Recursive callback returned an incorrect result" + 0 +""" + |> withLangVersionPreview + |> withFSharpCoreShippedNet + |> withOptimization optimize + |> compileExeAndRun + |> shouldSucceed + + [] + [] + [] + let ``runtime async does not duplicate effectful InlineIfLambda arguments`` (optimize: bool) = + FSharp """ +module RuntimeAsyncInlineIfLambdaEffectsTest + +open System.Threading.Tasks +open System.Runtime.CompilerServices +open Microsoft.FSharp.Core.CompilerServices.StateMachineHelpers + +let mutable calls = 0 + +let effect () = + calls <- calls + 1 + fun () -> 1 + +let inline plainTwice ([] f: unit -> int) = f () + f () +let inline twice ([] f: unit -> int) = + __runtimeAsyncReturn (f () + f ()) +let inline unused ([] f: unit -> int) = + __runtimeAsyncReturn 20 + +[] +let main _ = + let plainResult = plainTwice (effect ()) + let plainCalls = calls + calls <- 0 + let twiceResult = (twice (effect ())).GetAwaiter().GetResult() + let twiceCalls = calls + calls <- 0 + let unusedResult = (unused (effect ())).GetAwaiter().GetResult() + let unusedCalls = calls + + if plainResult = 2 && plainCalls = 1 + && twiceResult = 2 && twiceCalls = 1 + && unusedResult = 20 && unusedCalls = 1 then + 0 + else + 1 +""" + |> withLangVersionPreview + |> withFSharpCoreShippedNet + |> withOptimization optimize + |> compileExeAndRun + |> shouldSucceed + + [] + [] + [] + let ``runtime async preserves evaluation order for curried inline applications`` (optimize: bool) = + FSharp """ +module RuntimeAsyncCurriedApplicationTest + +open System.Threading.Tasks +open Microsoft.FSharp.Core.CompilerServices + +let events = ResizeArray() + +let step name value = + events.Add name + value + +let inline apply f x y = f x y + +let f () : Task = + StateMachineHelpers.__runtimeAsyncReturn ( + apply + (fun x -> + events.Add "body" + fun y -> x + y) + (step "arg1" 1) + (step "arg2" 2)) + +[] +let main _ = + let result = f().GetAwaiter().GetResult() + + if result = 3 && (events |> Seq.toList) = [ "arg1"; "arg2"; "body" ] then + 0 + else + 1 +""" + |> withLangVersionPreview + |> withFSharpCoreShippedNet + |> withOptimization optimize + |> compileExeAndRun + |> shouldSucceed + + [] + [] + [] + let ``runtime async evaluates conditional callback construction once`` (optimize: bool) = + FSharp """ +module RuntimeAsyncConditionalCallbackConstructionTest + +open System.Threading.Tasks +open System.Runtime.CompilerServices +open Microsoft.FSharp.Core.CompilerServices + +let events = ResizeArray() +let note text = events.Add text + +[] +let choose () = + note "choose" + true + +let inline twice ([] body: unit -> int) = + StateMachineHelpers.__runtimeAsyncReturn (body() + body()) + +let run (gate: Task) = + twice ( + if choose() then + note "construct" + fun () -> + note "body" + AsyncHelpers.Await gate + else + fun () -> 0) + +[] +let main _ = + let gate = TaskCompletionSource() + let work = run gate.Task + gate.SetResult(21) + let result = work.GetAwaiter().GetResult() + + if result = 42 && events.ToArray() = [| "choose"; "construct"; "body"; "body" |] then 0 else 1 +""" + |> withLangVersionPreview + |> withFSharpCoreShippedNet + |> withOptimization optimize + |> compileExeAndRun + |> shouldSucceed + + [] + [] + [] + [] + [] + let ``Issue 20577 preserves stateful conditional fold callbacks`` (optimize: bool, useList: bool) = + let source = $""" +module RuntimeAsyncConditionalFoldTest + +open System.Collections.Generic +open System.Threading.Tasks +open RuntimeTaskBuilder.RuntimeTask + +let run deduplicate (ready: Task) = + runtimeTask {{ + let! initial = ready + let folder = + if deduplicate then + let seen = HashSet() + fun total item -> if seen.Add item then total + item else total + else + fun total item -> total + item + + return + {if useList then "List.fold folder initial [ 1; 1; 2 ]" else "Array.fold folder initial [| 1; 1; 2 |]"} + }} + +[] +let main _ = + for deduplicate in [ true; false ] do + let gate = TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously) + let work = run deduplicate gate.Task + if work.IsCompleted then failwith "Expected suspension" + gate.SetResult 0 + let expected = if deduplicate then 3 else 4 + if work.GetAwaiter().GetResult() <> expected then failwith "Callback state was lost" + 0 +""" + + FsFromPath (Path.Combine(__SOURCE_DIRECTORY__, "RuntimeAsync", "RuntimeTaskBuilder.fs")) + |> withAdditionalSourceFile (FsSourceWithFileName "RuntimeAsyncConditionalFoldTest.fs" source) + |> withLangVersionPreview + |> withFSharpCoreShippedNet + |> withOptimization optimize + |> compileExeAndRun + |> shouldSucceed + + [] + [] + [] + let ``runtime async conditional callbacks share mutable captures with closures`` (optimize: bool) = + FSharp """ +module RuntimeAsyncConditionalMutableCapture + +open System.Threading.Tasks +open System.Runtime.CompilerServices +open Microsoft.FSharp.Core.CompilerServices + +let mutable observe = fun () -> -1 + +let inline invoke ([] callback: unit -> int) = + StateMachineHelpers.__runtimeAsyncReturn (callback ()) + +let run flag = + invoke ( + if flag then + let mutable count = 0 + observe <- fun () -> count + fun () -> + AsyncHelpers.Await(Task.FromResult 0) |> ignore + count <- count + 1 + count + else + fun () -> 0) + +let runMultiple flag = + invoke ( + if flag then + let mutable count = 0 + let mutable additional = 0 + observe <- fun () -> count + additional + fun () -> + AsyncHelpers.Await(Task.FromResult 0) |> ignore + count <- count + 1 + additional <- additional + 1 + count + additional + else + fun () -> 0) + +[] +let main _ = + let actual = (run true).Result, observe () + let multiple = (runMultiple true).Result, observe () + if actual = (1, 1) && multiple = (2, 2) then 0 else 1 +""" + |> withLangVersionPreview + |> withFSharpCoreShippedNet + |> withOptimization optimize + |> compileExeAndRun + |> shouldSucceed + + [] + [] + [] + let ``runtime async inlines a stateful conditional callback from an imported inline`` (optimize: bool) = + let library = + FSharp """ +module RuntimeAsyncCallbackLibrary + +let inline invokeTwice ([] callback: unit -> int) = + callback() + callback() +""" + |> withName "RuntimeAsyncCallbackLibrary" + |> withLangVersionPreview + |> withFSharpCoreShippedNet + + FSharp """ +module RuntimeAsyncImportedCallbackTest + +open System.Collections.Generic +open System.Threading.Tasks +open System.Runtime.CompilerServices +open Microsoft.FSharp.Core.CompilerServices +open RuntimeAsyncCallbackLibrary + +let mutable constructions = 0 +let mutable invocations = 0 + +let run (gate: Task) deduplicate = + StateMachineHelpers.__runtimeAsyncReturn ( + invokeTwice ( + if deduplicate then + constructions <- constructions + 1 + let seen = HashSet() + fun () -> + invocations <- invocations + 1 + let first = seen.Add 1 + AsyncHelpers.Await gate + if first then 1 else 0 + else + fun () -> 2)) + +[] +let main _ = + let gate = TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously) + let work = run gate.Task true + if work.IsCompleted || constructions <> 1 || invocations <> 1 then + failwith "Callback construction or first invocation did not run before suspension" + gate.SetResult(()) + if work.GetAwaiter().GetResult() <> 1 || constructions <> 1 || invocations <> 2 then + failwith "Callback state was not shared" + if (run gate.Task false).GetAwaiter().GetResult() <> 4 || constructions <> 1 || invocations <> 2 then + failwith "The other branch changed" + 0 +""" + |> withLangVersionPreview + |> withFSharpCoreShippedNet + |> withOptimization optimize + |> withReferences [ library ] + |> compileExeAndRun + |> shouldSucceed + + [] + [] + [] + let ``runtime async inlined conditional callbacks preserve cleanup after suspension`` (optimize: bool) = + FSharp """ +module RuntimeAsyncConditionalCallbackCleanupTest + +open System.Collections.Generic +open System.Threading.Tasks +open System.Runtime.CompilerServices +open Microsoft.FSharp.Core.CompilerServices + +let mutable cleaned = 0 + +let inline invokeTwice ([] callback: unit -> int) = + StateMachineHelpers.__runtimeAsyncReturn (callback() + callback()) + +let run (gate: Task) (cleanupGate: Task) (cleanupStarted: TaskCompletionSource) = + invokeTwice ( + if true then + let seen = HashSet() + fun () -> + try + let first = seen.Add 1 + AsyncHelpers.Await gate + if first then 1 else 0 + finally + cleanupStarted.TrySetResult(()) |> ignore + AsyncHelpers.Await cleanupGate + cleaned <- cleaned + 1 + else + fun () -> 0) + +[] +let main _ = + let gate = TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously) + let cleanupGate = TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously) + let cleanupStarted = TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously) + let work = run gate.Task cleanupGate.Task cleanupStarted + if work.IsCompleted || cleaned <> 0 then failwith "Cleanup ran before suspension" + gate.SetResult(()) + if not (cleanupStarted.Task.Wait(5000)) || work.IsCompleted then + failwith "Cleanup did not suspend" + cleanupGate.SetResult(()) + if work.GetAwaiter().GetResult() <> 1 || cleaned <> 2 then + failwith "Cleanup or callback state was lost" + 0 +""" + |> withLangVersionPreview + |> withFSharpCoreShippedNet + |> withOptimization optimize + |> compileExeAndRun + |> shouldSucceed + + [] + [] + [] + [] + [] + let ``runtime async does not rewrite opaque callback consumers`` (optimize: bool, quoted: bool) = + let opaqueUse = + if quoted then + "let saved = <@ callback() @> in saved.ToString() |> ignore" + else + "consume callback |> ignore" + + FSharp $""" +module RuntimeAsyncOpaqueCallbackTest + +open System.Threading.Tasks +open System.Runtime.CompilerServices +open Microsoft.FSharp.Core.CompilerServices + +[] +let consume (callback: unit -> int) = callback() + +let inline invoke ([] callback: unit -> int) = + StateMachineHelpers.__runtimeAsyncReturn ( + let result = callback() + {opaqueUse} + result) + +let run (gate: Task) flag = + invoke (if flag then fun () -> AsyncHelpers.Await gate else fun () -> 0) +""" + |> withLangVersionPreview + |> withFSharpCoreShippedNet + |> withOptimization optimize + |> compile + |> shouldFail + |> withErrorCode 3918 + + [] + let ``runtime async evaluates inline callback construction once`` () = + FSharp """ +module RuntimeAsyncCallbackConstructionTest + +open System.Threading.Tasks +open Microsoft.FSharp.Core.CompilerServices + +let mutable constructed = 0 + +let inline twice ([] f: unit -> int) = + StateMachineHelpers.__runtimeAsyncReturn (f () + f ()) + +let run () = + twice (constructed <- constructed + 1; fun () -> 21) + +[] +let main _ = + let result = run().GetAwaiter().GetResult() + if result = 42 && constructed = 1 then 0 else 1 +""" + |> withLangVersionPreview + |> withFSharpCoreShippedNet + |> compileExeAndRun + |> shouldSucceed + + [] + [] + [] + let ``runtime async inlines a callback after one that cannot be inlined`` (optimize: bool) = + FSharp """ +module RuntimeAsyncInlineAfterOpaqueCallback +open System.Threading.Tasks +open System.Runtime.CompilerServices +open Microsoft.FSharp.Core.CompilerServices + +[] +let consume (f: unit -> int) = f () + +let inline invokeBoth ([] a: unit -> int) ([] b: unit -> int) = + consume a + b() + b() + +let run (gate: Task) flag = + StateMachineHelpers.__runtimeAsyncReturn ( + invokeBoth + (let v = AsyncHelpers.Await gate in fun () -> v) + (if flag then (fun () -> AsyncHelpers.Await gate + 1) else (fun () -> 2))) + +[] +let main _ = + if (run (Task.FromResult 10) true).Result <> 32 then failwith "bad" + if (run (Task.FromResult 10) false).Result <> 14 then failwith "bad2" + 0 +""" + |> withLangVersionPreview + |> withFSharpCoreShippedNet + |> withOptimization optimize + |> compileExeAndRun + |> shouldSucceed + + [] + [] + [] + let ``runtime async inlines pattern-bound captures of a branch-selected callback`` (optimize: bool) = + FSharp """ +module RuntimeAsyncPatternCapturedCallback +open System.Threading.Tasks +open System.Runtime.CompilerServices +open Microsoft.FSharp.Core.CompilerServices + +let mutable constructions = 0 + +let inline twice ([] f: int -> int) = f 1 + f 2 + +let run (gate: Task) (choice: int option) = + StateMachineHelpers.__runtimeAsyncReturn ( + twice ( + constructions <- constructions + 1 + match choice with + | Some offset -> + let scaled = offset * 10 + fun x -> AsyncHelpers.Await gate + scaled + x + | None -> fun x -> x)) + +[] +let main _ = + let gate = TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously) + let pending = run gate.Task (Some 4) + if pending.IsCompleted then failwith "Expected pending result" + gate.SetResult 100 + if pending.GetAwaiter().GetResult() <> 283 then failwith "bad some" + if (run gate.Task None).Result <> 3 then failwith "bad none" + if constructions <> 2 then failwith "construction repeated" + 0 +""" + |> withLangVersionPreview + |> withFSharpCoreShippedNet + |> withOptimization optimize + |> compileExeAndRun + |> shouldSucceed + + [] + [] + [] + let ``runtime async reconstructs a branch-selected callback on each loop iteration`` (optimize: bool) = + FSharp """ +module RuntimeAsyncCallbackInLoop +open System.Threading.Tasks +open System.Runtime.CompilerServices +open Microsoft.FSharp.Core.CompilerServices + +let inline invoke ([] f: int -> int) = f 1 + f 2 + +let run (gate: Task) = + StateMachineHelpers.__runtimeAsyncReturn ( + let mutable total = 0 + for i in 0 .. 2 do + total <- + total + + invoke ( + if i % 2 = 0 then + (let k = i * 100 in fun x -> AsyncHelpers.Await gate + k + x) + else + (fun x -> x)) + total) + +[] +let main _ = + let result = (run (Task.FromResult 1000)).Result + if result <> 4409 then failwithf "Unexpected: %d" result + 0 +""" + |> withLangVersionPreview + |> withFSharpCoreShippedNet + |> withOptimization optimize + |> compileExeAndRun + |> shouldSucceed + + [] + [] + [] + let ``runtime async inlines nested stateful branch-selected callbacks`` (optimize: bool) = + FSharp """ +module RuntimeAsyncNestedBranchCallbacks +open System.Threading.Tasks +open System.Runtime.CompilerServices +open Microsoft.FSharp.Core.CompilerServices + +let inline invoke ([] f: int -> int) = f 1 + f 2 + f 3 + +let inline pick flag ([] a: int -> int) ([] b: int -> int) = + invoke (if flag then (fun x -> a x + b x) else (fun x -> b (a x))) + +let run (gate: Task) f1 f2 = + StateMachineHelpers.__runtimeAsyncReturn ( + pick + f1 + (if f2 then + let mutable count = 0 + fun x -> + count <- count + 1 + AsyncHelpers.Await gate + x + count + else fun x -> x * 2) + (if f2 then fun x -> x + 1 + else + let mutable count = 0 + fun x -> + count <- count + 1 + AsyncHelpers.Await gate - x + count)) + +[] +let main _ = + let r a b = (run (Task.FromResult 10) a b).Result + let results = r true true, r true false, r false true, r false false + if results <> (51, 42, 45, 24) then failwithf "Unexpected: %A" results + 0 +""" + |> withLangVersionPreview + |> withFSharpCoreShippedNet + |> withOptimization optimize + |> compileExeAndRun + |> shouldSucceed + + [] + [] + [] + [] + [] + let ``runtime async lowers deeply nested callback constructions`` (optimize: bool, asDelegate: bool) = + let depth = 1500 + let statements = String.replicate depth " touch ()\n" + let callbackType, invocation, wrap = + if asDelegate then + "Callback", "callback.Invoke", fun body -> $"Callback(fun x -> {body})" + else + "int -> int", "callback", fun body -> $"(fun x -> {body})" + + let source = $""" +module RuntimeAsyncDeepConstruction +open System +open System.Threading.Tasks +open System.Runtime.CompilerServices +open Microsoft.FSharp.Core.CompilerServices + +type Callback = delegate of int -> int +let mutable constructions = 0 +[] +let touch () = constructions <- constructions + 1 +let inline twice ([] callback: {callbackType}) = {invocation} 1 + {invocation} 2 + +[] +let run (gate: Task) flag = + StateMachineHelpers.__runtimeAsyncReturn ( + twice ( +{statements} let offset = 7 + if flag then {wrap "AsyncHelpers.Await gate + offset + x"} + else {wrap "offset + x"})) + +[] +let main _ = + let gate = TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously) + let pending = run gate.Task true + if pending.IsCompleted then failwith "Expected pending result" + if constructions <> {depth} then failwith "Construction was not evaluated once" + gate.SetResult 41 + if pending.GetAwaiter().GetResult() <> 99 then failwith "Suspending branch changed" + if (run gate.Task false).Result <> 17 then failwith "Non-suspending branch changed" + if constructions <> {2 * depth} then failwith "Construction was repeated" + 0 +""" + let result = + FSharp source + |> withLangVersionPreview + |> withFSharpCoreShippedNet + |> withOptimization optimize + |> compileExeAndRun + |> shouldSucceed + + // In-process compilation can have a larger stack than command-line compilation. + let outputDirectory = result.Output.OutputPath |> Option.get |> Path.GetDirectoryName + let sourcePath = Path.Combine(outputDirectory, "DeepConstruction.fs") + let outputPath = Path.Combine(outputDirectory, "DeepConstruction.exe") + File.WriteAllText(sourcePath, source) + let cliResult = + runFscProcess [ + yield! CompilerAssert.DefaultProjectOptions(Utilities.TargetFramework.FSharpCoreShippedNet).OtherOptions + yield "--target:exe" + yield "--langversion:preview" + yield if optimize then "--optimize+" else "--optimize-" + yield $"-o:\"{outputPath}\"" + yield $"\"{sourcePath}\"" + ] + Assert.True(cliResult.ExitCode = 0, $"{cliResult.StdOut}\n{cliResult.StdErr}") + + [] + [] + [] + [] + [] + [] + let ``runtime async outlines branch-selected callbacks without copying bodies per invocation`` (invocations: int, nested: bool) = + let calls = + if nested then + [1 .. invocations] |> List.fold (fun arg _ -> $"f ({arg})") "1" + else + List.init invocations (sprintf "f %d") |> String.concat " + " + + let terms = List.init 30 (sprintf "x * %d") |> String.concat " + " + + let compilation = + FSharp $""" +module RuntimeAsyncOversizedCallback +open System.Threading.Tasks +open System.Runtime.CompilerServices +open Microsoft.FSharp.Core.CompilerServices + +let inline invoke ([] f: int -> int) = + let unrelated = <@ 42 @> + unrelated.ToString() |> ignore + {calls} + +let run (gate: Task) flag = + StateMachineHelpers.__runtimeAsyncReturn ( + invoke (if flag then (fun x -> AsyncHelpers.Await gate + {terms}) else (fun x -> x))) + +[] +let main _ = + let evaluate x = List.fold (fun total n -> total + x * n) 10 [0 .. 29] + let expected = + if {nested.ToString().ToLowerInvariant()} then + [1 .. {invocations}] |> List.fold (fun arg _ -> evaluate arg) 1 + else + [0 .. {invocations - 1}] |> List.sumBy evaluate + let expectedOtherBranch = if {nested.ToString().ToLowerInvariant()} then 1 else {invocations * (invocations - 1) / 2} + let result flag = (run (Task.FromResult 10) flag).Result + if result true <> expected then failwith "Unexpected suspending branch result" + if result false <> expectedOtherBranch then failwith "Unexpected other branch result" + 0 +""" + |> asExe + |> withLangVersionPreview + |> withFSharpCoreShippedNet + + compilation + |> compileExeAndRun + |> shouldSucceed + |> withMetadataReader (fun md -> + let asyncInvokes = + md.MethodDefinitions + |> Seq.filter (fun handle -> + let method = md.GetMethodDefinition handle + md.GetString method.Name = "Invoke" && int method.ImplAttributes &&& 0x2000 <> 0) + |> Seq.length + Assert.Equal(4, asyncInvokes)) + + [] + [] + [] + let ``runtime async callback selected by a branch keeps AsyncLocal changes`` (optimize: bool) = + FSharp """ +module RuntimeAsyncBranchCallbackAsyncLocal +open System.Threading +open System.Threading.Tasks +open System.Runtime.CompilerServices +open Microsoft.FSharp.Core.CompilerServices + +let context = AsyncLocal() + +let inline invoke ([] f: unit -> string) = + let seen = f () + seen + "/" + context.Value + +let run (gate: Task) flag = + StateMachineHelpers.__runtimeAsyncReturn ( + context.Value <- "outer" + invoke ( + if flag then + (let captured = context.Value + ":" + fun () -> + let before = context.Value + AsyncHelpers.Await gate |> ignore + let after = context.Value + context.Value <- "inner" + captured + before + "-" + after) + else + (fun () -> "none"))) + +[] +let main _ = + let gate = TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously) + let pending = run gate.Task true + if pending.IsCompleted then failwith "Expected pending result" + gate.SetResult 1 + let result = pending.GetAwaiter().GetResult() + if result <> "outer:outer-outer/inner" then failwithf "Unexpected: %s" result + let completed = (run (Task.FromResult 1) true).Result + if completed <> "outer:outer-outer/inner" then failwithf "Unexpected completed: %s" completed + if (run (Task.FromResult 1) false).Result <> "none/outer" then failwith "bad2" + if not (isNull context.Value) then failwith "AsyncLocal leaked to caller" + 0 +""" + |> withLangVersionPreview + |> withFSharpCoreShippedNet + |> withOptimization optimize + |> compileExeAndRun + |> shouldSucceed + + [] + [] + [] + [] + [] + let ``runtime async Core outlining helper preserves context and shared mutation`` (optimize: bool, pending: bool) = + FSharp $""" +module RuntimeAsyncCoreOutline +open System.Threading +open System.Threading.Tasks +open System.Runtime.CompilerServices +open Microsoft.FSharp.Core.CompilerServices + +let context = AsyncLocal() + +[] +let run (gate: Task) = + StateMachineHelpers.__runtimeAsyncReturn( + context.Value <- "outer" + let mutable count = 0 + let observe () = count + let fragment = + StateMachineHelpers.__runtimeAsyncOutline (fun increment -> + AsyncHelpers.Await gate + count <- count + increment + context.Value <- "inner" + count) + let first = RuntimeAsyncFragmentAwaiter(fragment 2) + AsyncHelpers.AwaitAwaiter first + if first.GetResult() <> 2 || observe() <> 2 || context.Value <> "inner" then + failwith "First fragment lost context or shared state" + let second = RuntimeAsyncFragmentAwaiter(fragment 3) + AsyncHelpers.AwaitAwaiter second + if second.GetResult() <> 5 || observe() <> 5 || context.Value <> "inner" then + failwith "Repeated fragment lost context or shared state" + count) + +[] +let main _ = + let gate = TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously) + if {if pending then "false" else "true"} then gate.SetResult(()) + let result = run gate.Task + if not (isNull context.Value) then failwith "Context leaked to caller" + gate.TrySetResult(()) |> ignore + if result.GetAwaiter().GetResult() <> 5 then failwith "Incorrect result" + 0 +""" + |> withLangVersionPreview + |> withFSharpCoreShippedNet + |> withOptimization optimize + |> compileExeAndRun + |> shouldSucceed + + [] + [] + [] + [] + [] + let ``runtime async callback preserves final context on success and failure`` (optimize: bool, pending: bool) = + let result = + FSharp $""" +module RuntimeAsyncContextHandoff +open System +open System.Threading +open System.Threading.Tasks +open System.Runtime.CompilerServices +open Microsoft.FSharp.Core.CompilerServices + +let context = AsyncLocal() +let synchronizationContext = SynchronizationContext() +let expectedError = InvalidOperationException("fragment") + +let inline invoke ([] callback: unit -> int) = + let seen = callback() + seen + context.Value.Length + +[] +let run (gate: Task) (next: Task) flag fail suppress = + StateMachineHelpers.__runtimeAsyncReturn( + context.Value <- "outer" + let value = + try + invoke ( + if flag then + let captured = context.Value + ":" + fun () -> + AsyncHelpers.Await gate + if captured <> "outer:" then failwith "Captured state changed" + context.Value <- "inner" + SynchronizationContext.SetSynchronizationContext(synchronizationContext) + if suppress then ExecutionContext.SuppressFlow() |> ignore + if fail then raise expectedError + 36 + else + fun () -> 0) + with error when obj.ReferenceEquals(error, expectedError) && context.Value = "inner" -> + 42 + if context.Value <> "inner" then failwith "Fragment context was not restored" + if not (obj.ReferenceEquals(SynchronizationContext.Current, synchronizationContext)) then + failwith "Synchronization context was not restored" + if ExecutionContext.IsFlowSuppressed() <> suppress then failwith "Suppression state was not restored" + if suppress then ExecutionContext.RestoreFlow() + SynchronizationContext.SetSynchronizationContext(null) + AsyncHelpers.Await next + if context.Value <> "inner" then failwith "Context was lost at a subsequent await" + value) + +[] +let main _ = + for fail in [false; true] do + for suppress in [false; true] do + let gate = TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously) + let next = TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously) + if {if pending then "false" else "true"} then gate.SetResult(()) + let result = run gate.Task next.Task true fail suppress + if result.IsCompleted then failwith "Expected a pending result" + if not (isNull context.Value) || not (isNull SynchronizationContext.Current) then + failwith "Fragment context leaked to the outer caller" + gate.TrySetResult(()) |> ignore + next.SetResult(()) + let expected = if fail then 42 else 41 + if result.GetAwaiter().GetResult() <> expected then failwith "Incorrect fragment outcome" + 0 +""" + |> withLangVersionPreview + |> withFSharpCoreShippedNet + |> withOptimization optimize + |> compileExeAndRun + |> shouldSucceed + + result + |> verifyILContains [ + "call instance !0 valuetype [FSharp.Core]Microsoft.FSharp.Core.CompilerServices.RuntimeAsyncFragmentAwaiter`1::GetResult()" + ] + |> ignore + + [] + [] + [] + [] + [] + let ``runtime async outlined callback reports child context exit and restoration`` (optimize: bool, pending: bool) = + let result = + FSharp $""" +module RuntimeAsyncContextNotifications +open System.Collections.Generic +open System.Threading +open System.Threading.Tasks +open System.Runtime.CompilerServices +open Microsoft.FSharp.Core.CompilerServices + +let trace = ResizeArray() +let mutable recording = false +let context = + AsyncLocal(fun change -> + if recording then + trace.Add($"{{change.PreviousValue}}->{{change.CurrentValue}}:{{change.ThreadContextChanged}}")) + +let inline invoke ([] callback: unit -> int) = + let seen = callback() + seen + context.Value.Length + +[] +let run (gate: Task) flag = + StateMachineHelpers.__runtimeAsyncReturn( + context.Value <- "outer" + let value = + invoke ( + if flag then + let captured = context.Value + ":" + fun () -> + AsyncHelpers.Await gate + if captured <> "outer:" then failwith "Captured state changed" + recording <- true + context.Value <- "inner" + 36 + else + fun () -> 0) + recording <- false + if context.Value <> "inner" then failwith "Incorrect final context" + if trace.ToArray() <> [| "outer->inner:False"; "inner->outer:True"; "outer->inner:True" |] then + failwithf "Unexpected context notifications: %%A" (trace.ToArray()) + value) + +[] +let main _ = + let gate = TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously) + if {if pending then "false" else "true"} then gate.SetResult(()) + let result = run gate.Task true + if {if pending then "false" else "true"} && not result.IsCompleted then failwith "Completed callback suspended" + gate.TrySetResult(()) |> ignore + if result.GetAwaiter().GetResult() <> 41 then failwith "Incorrect result" + 0 +""" + |> withLangVersionPreview + |> withFSharpCoreShippedNet + |> withOptimization optimize + |> compileExeAndRun + |> shouldSucceed + + result + |> verifyILContains [ + "call instance !0 valuetype [FSharp.Core]Microsoft.FSharp.Core.CompilerServices.RuntimeAsyncFragmentAwaiter`1::GetResult()" + ] + |> ignore + + [] + [] + [] + let ``runtime async callback preserves context exception filter ordering`` (optimize: bool) = + FSharp """ +module RuntimeAsyncContextExceptionFilter +open System.Threading.Tasks +open System.Runtime.CompilerServices +open Microsoft.FSharp.Core.CompilerServices + +let mutable finallyRan = false +let mutable filterSawFinally = false +let inline invoke ([] callback: unit -> int) = + let value = callback() + value + 1 + +[] +let run flag = + StateMachineHelpers.__runtimeAsyncReturn( + try + invoke ( + if flag then + let captured = System.DateTime.UtcNow.Ticks + fun () -> + AsyncHelpers.Await Task.CompletedTask + try + failwithf "fragment %d" captured + finally + finallyRan <- true + else + fun () -> 0) + with error when ( + filterSawFinally <- finallyRan + error.Message.StartsWith("fragment", System.StringComparison.Ordinal)) -> + 42) + +[] +let main _ = + if (run true).GetAwaiter().GetResult() <> 42 then failwith "Incorrect exception outcome" + if not finallyRan then failwith "Fragment finally did not run" + if not filterSawFinally then failwith "Parent exception filter ran before the fragment finally" + 0 +""" + |> withLangVersionPreview + |> withFSharpCoreShippedNet + |> withOptimization optimize + |> compileExeAndRun + |> shouldSucceed + + [] + [] + [] + let ``runtime async prepares a body after a callback is inlined into its handler`` (optimize: bool) = + FSharp """ +module RuntimeAsyncCallbackInHandler +open System.Threading.Tasks +open Microsoft.FSharp.Core.CompilerServices + +type Started<'T> = delegate of unit -> 'T + +let inline run ([] code: Started) = + StateMachineHelpers.__runtimeAsyncReturn ( + try + failwith "boom" + with _ -> + code.Invoke()) + +let execute (pending: Task) chooseFirst = + run ( + if chooseFirst then + Started(fun () -> System.Runtime.CompilerServices.AsyncHelpers.Await pending) + else + Started(fun () -> System.Runtime.CompilerServices.AsyncHelpers.Await pending + 1)) + +[] +let main _ = + let gate = TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously) + let first = execute gate.Task true + let second = execute gate.Task false + if first.IsCompleted || second.IsCompleted then failwith "Expected pending results" + gate.SetResult 41 + if first.GetAwaiter().GetResult() <> 41 || second.GetAwaiter().GetResult() <> 42 then failwith "Wrong result" + 0 +""" + |> withLangVersionPreview + |> withFSharpCoreShippedNet + |> withOptimization optimize + |> compileExeAndRun + |> shouldSucceed +#endif + +module RuntimeAsyncDelegateTests = + open Xunit + open FSharp.Test.Compiler + +#if NETCOREAPP + [] + [] + [] + [] + [] + let ``runtime async delegate await does not repeat a pending sequence move`` (optimize: bool, recipe: bool) = + FSharp $""" +module RuntimeAsyncDelegateAwait + +open System +open System.Collections.Generic +open System.Threading +open System.Threading.Tasks +open System.Runtime.CompilerServices +open Microsoft.FSharp.Core.CompilerServices + +type Started<'T> = delegate of unit -> 'T +let mutable constructions = 0 + +type Builder() = + member inline _.Delay([] code: unit -> 'T) = code + member inline _.Run([] code: unit -> 'T) : Task<'T> = + StateMachineHelpers.__runtimeAsyncReturn(code()) + member inline _.Source(work: ValueTask<'T>) = + constructions <- constructions + 1 + Started(fun () -> AsyncHelpers.Await work) + member inline _.Bind([] work: Started<'T>, [] next: 'T -> 'U) = + next (work.Invoke()) + member inline _.While([] guard: unit -> bool, [] body: unit -> unit) = + while guard() do body() + member inline _.Zero() = () + member inline _.Combine(_: unit, [] next: unit -> 'T) = next() + member inline _.Return(value) = value + +let builder = Builder() + +type GuardedEnumerator(gate: Task) = + let mutable active = 0 + let mutable count = 0 + interface IAsyncEnumerator with + member _.Current = count + member _.MoveNextAsync() = + if Interlocked.Exchange(&active, 1) = 1 then + invalidOp "MoveNextAsync cannot be called concurrently." + StateMachineHelpers.__runtimeAsyncReturnValueTask( + try + AsyncHelpers.Await gate + count <- count + 1 + count = 1 + finally + Interlocked.Exchange(&active, 0) |> ignore) + member _.DisposeAsync() = ValueTask() + +let collect (e: IAsyncEnumerator) = + builder {{ + let values = ResizeArray() + while! e.MoveNextAsync() do + values.Add(e.Current) + return values.ToArray() + }} + +[] +let main _ = + let gate = TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously) + let enumerator = + if {if recipe then "true" else "false"} then + let source = StateMachineHelpers.__runtimeAsyncSequence(fun () -> seq {{ + do AsyncHelpers.Await gate.Task + yield 1 + }}) + source.GetAsyncEnumerator() + else + GuardedEnumerator(gate.Task) :> IAsyncEnumerator + let pending = collect enumerator + if pending.IsCompleted then + pending.GetAwaiter().GetResult() |> ignore + failwith "Expected a pending first move" + if constructions <> 1 then failwith "Expected one construction before suspension" + gate.SetResult(()) + if pending.GetAwaiter().GetResult() <> [| 1 |] || constructions <> 2 then + failwith "Unexpected sequence values or delegate constructions" + 0 +""" + |> withLangVersionPreview + |> withFSharpCoreShippedNet + |> withOptimization optimize + |> compileExeAndRun + |> shouldSucceed + + [] + [] + [] + let ``runtime async handles imported InlineIfLambda delegate invocations`` (optimize: bool) = + let library = + FSharp """ +module ImportedDelegateAwait +open Microsoft.FSharp.Core.CompilerServices + +type Started<'T> = delegate of unit -> 'T + +let inline run ([] callback: Started) = + StateMachineHelpers.__runtimeAsyncReturn(callback.Invoke()) +""" + |> withName "ImportedDelegateAwait" + |> withLangVersionPreview + |> withFSharpCoreShippedNet + + FSharp """ +module RuntimeAsyncImportedDelegate +open System.Threading.Tasks +open System.Runtime.CompilerServices +open ImportedDelegateAwait + +let run (gate: Task) (cleanup: Task) (entered: TaskCompletionSource) = + ImportedDelegateAwait.run (Started(fun () -> + try + AsyncHelpers.Await gate + finally + entered.SetResult(()) + AsyncHelpers.Await cleanup)) + +[] +let main _ = + let gate = TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously) + let cleanup = TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously) + let entered = TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously) + let pending = run gate.Task cleanup.Task entered + if pending.IsCompleted then failwith "Expected suspension" + gate.SetResult 42 + if not (entered.Task.Wait(5000)) || pending.IsCompleted then + failwith "Cleanup must suspend before completing the delegate" + cleanup.SetResult(()) + if pending.GetAwaiter().GetResult() <> 42 then failwith "Lost delegate await" + 0 +""" + |> withLangVersionPreview + |> withFSharpCoreShippedNet + |> withOptimization optimize + |> withReferences [ library ] + |> compileExeAndRun + |> shouldSucceed + + [] + [] + [] + let ``runtime async outlines nested single-use delegate sources`` (optimize: bool) = + let source = + FSharp """ +module NestedDelegateSource +open System.Threading.Tasks +open System.Runtime.CompilerServices +open Microsoft.FSharp.Core.CompilerServices + +type Started<'T> = delegate of unit -> 'T +let mutable constructions = 0 + +let inline compose ([] source: Started) = + Started(fun () -> source.Invoke() + 1) + +let inline run ([] code: Started) = + StateMachineHelpers.__runtimeAsyncReturn(code.Invoke()) + +[] +let execute (pending: Task) chooseFirst = + run (compose ( + if chooseFirst then + Started(fun () -> AsyncHelpers.Await pending) + else + Started(fun () -> AsyncHelpers.Await pending + 1))) + +let inline runAfterConstruction ([] code: Started) = + StateMachineHelpers.__runtimeAsyncReturn( + if constructions = 0 then failwith "Callback constructed after entering run" + code.Invoke()) + +[] +let executeWithConstruction (pending: Task) = + runAfterConstruction (compose ( + constructions <- constructions + 1 + Started(fun () -> AsyncHelpers.Await pending))) + +[] +let main _ = + let gate = TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously) + let first = execute gate.Task true + let second = execute gate.Task false + let third = executeWithConstruction gate.Task + if first.IsCompleted || second.IsCompleted || third.IsCompleted || constructions <> 1 then + failwith "Delegate construction or suspension changed" + gate.SetResult 41 + if first.GetAwaiter().GetResult() <> 42 || second.GetAwaiter().GetResult() <> 43 || third.GetAwaiter().GetResult() <> 42 then + failwith "Nested delegate result changed" + 0 +""" + |> withLangVersionPreview + |> withFSharpCoreShippedNet + |> withOptimization optimize + |> compileExeAndRun + |> shouldSucceed + + source |> withMetadataReader (fun md -> + let asyncInvokes = + [ for handle in md.TypeDefinitions do + let ty = md.GetTypeDefinition handle + for methodHandle in ty.GetMethods() do + let method = md.GetMethodDefinition methodHandle + if md.GetString method.Name = "Invoke" && int method.ImplAttributes &&& 0x2000 <> 0 then + yield md.GetString ty.Name ] + Assert.Equal(4, asyncInvokes.Length) + Assert.Contains(asyncInvokes, fun name -> name.StartsWith("executeWithConstruction@", System.StringComparison.Ordinal))) + + [] + [] + [] + let ``runtime async merges delegate sources`` (optimize: bool) = + FSharp """ +module RuntimeAsyncMergedDelegates +open System.Threading.Tasks +open System.Runtime.CompilerServices +open Microsoft.FSharp.Core.CompilerServices + +type Started<'T> = delegate of unit -> 'T + +type Builder() = + member inline _.Delay([] code: unit -> 'T) = code + member inline _.Source(task: Task<'T>) = Started(fun () -> AsyncHelpers.Await task) + member inline _.MergeSources([] left: Started<'A>, [] right: Started<'B>) = + Started(fun () -> struct (left.Invoke(), right.Invoke())) + member inline _.Bind([] source: Started<'T>, [] next: 'T -> 'U) = + next (source.Invoke()) + member inline _.Return(value: 'T) = value + member inline _.Run([] code: unit -> 'T) : Task<'T> = + StateMachineHelpers.__runtimeAsyncReturn (code()) + +let builder = Builder() + +let run (left: Task) (right: Task) = + builder { + let! x = left + and! y = right + return x + y + } + +let runThree (first: Task) (second: Task) (third: Task) = + builder { + let! x = first + and! y = second + and! z = third + return x + y + z + } + +[] +let main _ = + let left = TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously) + let right = TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously) + let pending = run left.Task right.Task + if pending.IsCompleted then failwith "Expected a pending result" + left.SetResult 1 + if pending.IsCompleted then failwith "Right source was not awaited" + right.SetResult 2 + if pending.GetAwaiter().GetResult() <> 3 then failwith "Wrong result" + let third = TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously) + let nested = runThree left.Task right.Task third.Task + if nested.IsCompleted then failwith "Nested merge completed before the third source" + third.SetResult 3 + if nested.GetAwaiter().GetResult() <> 6 then failwith "Wrong nested result" + 0 +""" + |> withLangVersionPreview + |> withFSharpCoreShippedNet + |> withOptimization optimize + |> compileExeAndRun + |> shouldSucceed + + [] + [] + [] + let ``runtime async inlines repeated conditional delegate invocations`` (optimize: bool) = + let library = + FSharp """ +module ImportedDelegateCallbacks +open Microsoft.FSharp.Core.CompilerServices + +type Started<'T> = delegate of unit -> 'T + +let inline invokeTwice ([] callback: Started) = + callback.Invoke() + callback.Invoke() +""" + |> withName "ImportedDelegateCallbacks" + |> withLangVersionPreview + |> withFSharpCoreShippedNet + + FSharp """ +module RuntimeAsyncRepeatedDelegate +open System.Collections.Generic +open System.Threading.Tasks +open System.Runtime.CompilerServices +open Microsoft.FSharp.Core.CompilerServices +open ImportedDelegateCallbacks + +let mutable constructions = 0 +let mutable invocations = 0 + +let run (gate: Task) deduplicate = + StateMachineHelpers.__runtimeAsyncReturn ( + invokeTwice ( + if deduplicate then + constructions <- constructions + 1 + let seen = HashSet() + Started(fun () -> + invocations <- invocations + 1 + let first = seen.Add 1 + AsyncHelpers.Await gate + if first then 1 else 0) + else + Started(fun () -> 2))) + +[] +let main _ = + let gate = TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously) + let pending = run gate.Task true + if pending.IsCompleted || constructions <> 1 || invocations <> 1 then + failwith "Callback construction or first invocation changed" + gate.SetResult(()) + if pending.GetAwaiter().GetResult() <> 1 || constructions <> 1 || invocations <> 2 then + failwith "Repeated calls did not share callback state" + if (run gate.Task false).GetAwaiter().GetResult() <> 4 || constructions <> 1 then + failwith "Other branch changed" + 0 +""" + |> withLangVersionPreview + |> withFSharpCoreShippedNet + |> withOptimization optimize + |> withReferences [ library ] + |> compileExeAndRun + |> shouldSucceed + + [] + [] + [] + [] + [] + let ``runtime async does not rewrite delegates passed to opaque consumers`` (optimize: bool, quoted: bool) = + let opaqueUse = + if quoted then + "let saved = <@ callback.Invoke() @> in saved.ToString() |> ignore" + else + "consume callback |> ignore" + + FSharp $""" +module RuntimeAsyncOpaqueDelegate + +open System.Threading.Tasks +open System.Runtime.CompilerServices +open Microsoft.FSharp.Core.CompilerServices + +type Started<'T> = delegate of unit -> 'T + +[] +let consume (callback: Started) = callback.Invoke() + +let inline invoke ([] callback: Started) = + StateMachineHelpers.__runtimeAsyncReturn( + let result = callback.Invoke() + {opaqueUse} + result) + +let run (gate: Task) flag = + invoke (if flag then Started(fun () -> AsyncHelpers.Await gate) else Started(fun () -> 0)) +""" + |> withLangVersionPreview + |> withFSharpCoreShippedNet + |> withOptimization optimize + |> compile + |> shouldFail + |> withErrorCode 3918 + + [] + [] + [] + [] + [] + let ``runtime async rejects an unmarked delegate method`` (optimize: bool, marked: bool) = + let invocation = "consume (Started(fun () -> AsyncHelpers.Await gate))" + let body = + if marked then + $"StateMachineHelpers.__runtimeAsyncReturn ({invocation})" + else + invocation + + FSharp $""" +module RuntimeAsyncUnmarkedDelegate +open System.Threading.Tasks +open System.Runtime.CompilerServices +open Microsoft.FSharp.Core.CompilerServices + +type Started<'T> = delegate of unit -> 'T + +[] +let consume (callback: Started) = callback.Invoke() + +let run (gate: Task) = {body} +""" + |> withLangVersionPreview + |> withFSharpCoreShippedNet + |> withOptimization optimize + |> compile + |> shouldFail + |> withErrorCode 3918 + + [] + [] + [] + let ``runtime async evaluates a conditional delegate receiver before its argument`` (optimize: bool) = + FSharp """ +module RuntimeAsyncDelegateEvaluationOrder +open System.Collections.Generic +open System.Threading.Tasks +open System.Runtime.CompilerServices +open Microsoft.FSharp.Core.CompilerServices + +type Callback = delegate of int -> int + +let inline invoke ([] callback: Callback) (trace: ResizeArray) = + callback.Invoke(trace.Add "argument"; 3) + +let run (gate: Task) flag (trace: ResizeArray) = + StateMachineHelpers.__runtimeAsyncReturn ( + invoke ( + if flag then + trace.Add "construction" + let mutable count = 10 + Callback(fun x -> + let value = AsyncHelpers.Await gate + count <- count + value + x + count) + else + trace.Add "other" + Callback(fun x -> x * 2)) trace) + +[] +let main _ = + let gate = TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously) + let trace = ResizeArray() + let pending = run gate.Task true trace + if pending.IsCompleted || trace.ToArray() <> [| "construction"; "argument" |] then + failwith "Receiver or argument evaluation order changed" + gate.SetResult 41 + if pending.GetAwaiter().GetResult() <> 54 then failwith "Mutable capture result changed" + trace.Clear() + if (run gate.Task false trace).GetAwaiter().GetResult() <> 6 + || trace.ToArray() <> [| "other"; "argument" |] then + failwith "Other branch evaluation changed" + 0 +""" + |> withLangVersionPreview + |> withFSharpCoreShippedNet + |> withOptimization optimize + |> compileExeAndRun + |> shouldSucceed +#endif + +module RuntimeAsyncDebugTests = + open Xunit + open FSharp.Test + open FSharp.Test.Compiler + open System.IO + open System.Reflection.Metadata + +#if NETCOREAPP + [] + let ``runtime async enumerable CE debug points stay at call sites`` () = + let source = + """module RuntimeAsyncEnumerableDebug +open System.Threading.Tasks +open RuntimeAsyncEnumerable + +let first () = + asyncSeq { + do! Task.Delay 1 + yield 1 + } + +let second () = + asyncSeq { + do! Task.Delay 1 + yield 2 + } +""" + + FsFromPath (Path.Combine(__SOURCE_DIRECTORY__, "RuntimeAsync", "RuntimeTaskBuilder.fs")) + |> withAdditionalSourceFile ( + SourceFromPath (Path.Combine(__SOURCE_DIRECTORY__, "RuntimeAsync", "RuntimeAsyncEnumerable.fs")) + ) + |> withAdditionalSourceFile (FsSourceWithFileName "RuntimeAsyncEnumerableDebug.fs" source) + |> withLangVersionPreview + |> withFSharpCoreShippedNet + |> withPortablePdb + |> withNoOptimize + |> compile + |> shouldSucceed + |> verifyPdb [ + VerifyRuntimeAsyncMethodSequencePointsInSource("RuntimeAsyncEnumerableDebug.fs", 6, 8) + VerifyRuntimeAsyncMethodSequencePointsInSource("RuntimeAsyncEnumerableDebug.fs", 12, 14) + ] + + [] + [] + [] + let ``runtime async inlined InlineIfLambda callback keeps its statement debug points`` (optimize: bool) = + let statementPoints = [ Line 13, Col 9, Line 13, Col 45; Line 14, Col 9, Line 14, Col 27; Line 15, Col 9, Line 15, Col 18 ] + let callSitePoint = [ Line 12, Col 5, Line 15, Col 19 ] + + FSharp """ +module CallbackDebugPoints + +open System.Threading.Tasks +open System.Runtime.CompilerServices +open Microsoft.FSharp.Core.CompilerServices + +let inline run ([] body: unit -> int) : Task = + StateMachineHelpers.__runtimeAsyncReturn (body ()) + +let work (ready: Task) = + run (fun () -> + let value = AsyncHelpers.Await ready + printfn "%d" value + value + 1) +""" + |> withLangVersionPreview + |> withFSharpCoreShippedNet + |> withPortablePdb + |> withOptimization optimize + |> compile + |> shouldSucceed + |> verifyPdb [ VerifyMethodSequencePoints("work", (if optimize then statementPoints else callSitePoint @ statementPoints)) ] + + [] + [] + [] + [] + [] + let ``runtime async conditional callback keeps mutable capture in its lexical scope`` (insideContext: bool, asDelegate: bool) = + let callbackType, callbackCall, callbackStart, callbackEnd = + if asDelegate then "Started", "callback.Invoke()", "Started(", ")" + else "unit -> int", "callback ()", "", "" + + let source = $"""module MissingMutableLocal + +open System.Threading.Tasks +open System.Runtime.CompilerServices +open Microsoft.FSharp.Core.CompilerServices +type Started<'T> = delegate of unit -> 'T +let inline invoke ([] callback: {callbackType}) : Task = + StateMachineHelpers.__runtimeAsyncReturn ({callbackCall}) + +let run flag (ready: Task) = + let mutable count = 99 + invoke ( + if flag then + printfn "outer = %%d" count + let mutable count = 0 + printfn "%%d" count + {callbackStart}fun () -> + let value = AsyncHelpers.Await ready + count <- count + value + count{callbackEnd} + else + printfn "else = %%d" count + {callbackStart}fun () -> 0{callbackEnd}) +""" + let source = + if insideContext then + source + .Replace(" : Task =", " =") + .Replace($"StateMachineHelpers.__runtimeAsyncReturn ({callbackCall})", callbackCall) + .Replace("let run flag (ready: Task) =", "let run flag (ready: Task) = StateMachineHelpers.__runtimeAsyncReturn (") + .Replace($"fun () -> 0{callbackEnd})", $"fun () -> 0{callbackEnd}))") + else + source + let source = source + """ +[] +let main _ = + let gate = TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously) + let pending = run true gate.Task + if pending.IsCompleted then failwith "Callback did not suspend" + gate.SetResult 41 + if pending.GetAwaiter().GetResult() <> 41 then failwith "Mutable capture result changed" + if (run false gate.Task).GetAwaiter().GetResult() <> 0 then failwith "Other branch result changed" + 0 +""" + let result = + FSharp source + |> asExe + |> withLangVersionPreview + |> withFSharpCoreShippedNet + |> withPortablePdb + |> withNoOptimize + |> compileExeAndRun + |> shouldSucceed + + use stream = File.OpenRead(Path.ChangeExtension(result.OutputPath.Value, "pdb")) + use provider = MetadataReaderProvider.FromPortablePdbStream stream + let pdb = provider.GetMetadataReader() + + result |> withMetadataReader (fun md -> + let method = + md.MethodDefinitions + |> Seq.filter (fun handle -> md.GetString(md.GetMethodDefinition(handle).Name) = "run") + |> Assert.Single + let localsAt (method: MethodDefinitionHandle) line = + let point = + pdb.GetMethodDebugInformation(method).GetSequencePoints() + |> Seq.filter (fun point -> not point.IsHidden && point.StartLine = line) + |> Assert.Single + [ for handle in pdb.GetLocalScopes method do + let scope = pdb.GetLocalScope handle + if scope.StartOffset <= point.Offset && point.Offset < scope.EndOffset then + for local in scope.GetLocalVariables() do + let variable = pdb.GetLocalVariable local + yield pdb.GetString variable.Name, variable ] + let outerLocals = localsAt method 14 + Assert.DoesNotContain("count (shadowed)", outerLocals |> List.map fst) + let outer = + outerLocals + |> List.filter (fun (name, _) -> name = "count") + |> Assert.Single + |> snd + let inner = + localsAt method 16 + |> List.filter (fun (name, _) -> name = "count") + |> Assert.Single + |> snd + Assert.NotEqual(outer.Index, inner.Index) + Assert.Equal(LocalVariableAttributes.None, inner.Attributes) + let elseLocals = localsAt method 22 + Assert.DoesNotContain("count (shadowed)", elseLocals |> List.map fst) + let restoredOuter = + elseLocals + |> List.filter (fun (name, _) -> name = "count") + |> Assert.Single + |> snd + Assert.Equal(outer.Index, restoredOuter.Index) + let invocationMethod = + md.MethodDefinitions + |> Seq.filter (fun handle -> + pdb.GetMethodDebugInformation(handle).GetSequencePoints() + |> Seq.exists (fun point -> not point.IsHidden && point.StartLine = 19)) + |> Assert.Single + if invocationMethod = method then + let invoked = + localsAt invocationMethod 19 + |> List.filter (fun (name, _) -> name = "count") + |> Assert.Single + |> snd + Assert.Equal(LocalVariableAttributes.None, invoked.Attributes) + Assert.Equal(inner.Index, invoked.Index) + else + let closure = md.GetTypeDefinition(md.GetMethodDefinition(invocationMethod).GetDeclaringType()) + let fields = closure.GetFields() |> Seq.map (fun handle -> md.GetString(md.GetFieldDefinition(handle).Name)) + Assert.Contains("count", fields)) #endif diff --git a/tests/FSharp.Core.UnitTests/FSharp.Core.SurfaceArea.netcore.debug.bsl b/tests/FSharp.Core.UnitTests/FSharp.Core.SurfaceArea.netcore.debug.bsl index 6964d31c0e3..ba2c8f3362d 100644 --- a/tests/FSharp.Core.UnitTests/FSharp.Core.SurfaceArea.netcore.debug.bsl +++ b/tests/FSharp.Core.UnitTests/FSharp.Core.SurfaceArea.netcore.debug.bsl @@ -1010,6 +1010,11 @@ Microsoft.FSharp.Core.CompilerServices.ResumptionFunc`1[TData]: Boolean EndInvok Microsoft.FSharp.Core.CompilerServices.ResumptionFunc`1[TData]: Boolean Invoke(Microsoft.FSharp.Core.CompilerServices.ResumableStateMachine`1[TData] ByRef) Microsoft.FSharp.Core.CompilerServices.ResumptionFunc`1[TData]: System.IAsyncResult BeginInvoke(Microsoft.FSharp.Core.CompilerServices.ResumableStateMachine`1[TData] ByRef, System.AsyncCallback, System.Object) Microsoft.FSharp.Core.CompilerServices.ResumptionFunc`1[TData]: Void .ctor(System.Object, IntPtr) +Microsoft.FSharp.Core.CompilerServices.RuntimeAsyncFragmentAwaiter`1[T]: Boolean IsCompleted +Microsoft.FSharp.Core.CompilerServices.RuntimeAsyncFragmentAwaiter`1[T]: Boolean get_IsCompleted() +Microsoft.FSharp.Core.CompilerServices.RuntimeAsyncFragmentAwaiter`1[T]: T GetResult() +Microsoft.FSharp.Core.CompilerServices.RuntimeAsyncFragmentAwaiter`1[T]: Void .ctor(System.Threading.Tasks.ValueTask`1[Microsoft.FSharp.Core.CompilerServices.RuntimeAsyncFragmentResult`1[T]]) +Microsoft.FSharp.Core.CompilerServices.RuntimeAsyncFragmentResult`1[T]: Void .ctor(Microsoft.FSharp.Core.FSharpResult`2[T,System.Runtime.ExceptionServices.ExceptionDispatchInfo]) Microsoft.FSharp.Core.CompilerServices.RuntimeHelpers: Microsoft.FSharp.Control.IEvent`2[TDelegate,TArgs] CreateEvent[TDelegate,TArgs](Microsoft.FSharp.Core.FSharpFunc`2[TDelegate,Microsoft.FSharp.Core.Unit], Microsoft.FSharp.Core.FSharpFunc`2[TDelegate,Microsoft.FSharp.Core.Unit], Microsoft.FSharp.Core.FSharpFunc`2[Microsoft.FSharp.Core.FSharpFunc`2[System.Object,Microsoft.FSharp.Core.FSharpFunc`2[TArgs,Microsoft.FSharp.Core.Unit]],TDelegate]) Microsoft.FSharp.Core.CompilerServices.RuntimeHelpers: System.Collections.Generic.IEnumerable`1[TResult] EnumerateFromFunctions[T,TResult](Microsoft.FSharp.Core.FSharpFunc`2[Microsoft.FSharp.Core.Unit,T], Microsoft.FSharp.Core.FSharpFunc`2[T,System.Boolean], Microsoft.FSharp.Core.FSharpFunc`2[T,TResult]) Microsoft.FSharp.Core.CompilerServices.RuntimeHelpers: System.Collections.Generic.IEnumerable`1[TResult] EnumerateUsing[T,TCollection,TResult](T, Microsoft.FSharp.Core.FSharpFunc`2[T,TCollection]) @@ -1021,6 +1026,7 @@ Microsoft.FSharp.Core.CompilerServices.SetStateMachineMethodImpl`1[TData]: Void Microsoft.FSharp.Core.CompilerServices.SetStateMachineMethodImpl`1[TData]: Void EndInvoke(System.IAsyncResult) 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.FSharpFunc`2[TArg,System.Threading.Tasks.ValueTask`1[Microsoft.FSharp.Core.CompilerServices.RuntimeAsyncFragmentResult`1[T]]] __runtimeAsyncOutline[TArg,T](Microsoft.FSharp.Core.FSharpFunc`2[TArg,T]) Microsoft.FSharp.Core.CompilerServices.StateMachineHelpers: Microsoft.FSharp.Core.FSharpOption`1[System.Int32] __resumableEntry() Microsoft.FSharp.Core.CompilerServices.StateMachineHelpers: System.Collections.Generic.IAsyncEnumerable`1[T] __runtimeAsyncSequence[T](Microsoft.FSharp.Core.FSharpFunc`2[Microsoft.FSharp.Core.Unit,System.Collections.Generic.IEnumerable`1[T]]) Microsoft.FSharp.Core.CompilerServices.StateMachineHelpers: System.Threading.CancellationToken __runtimeAsyncSequenceCancellationToken() diff --git a/tests/FSharp.Core.UnitTests/FSharp.Core.SurfaceArea.netcore.release.bsl b/tests/FSharp.Core.UnitTests/FSharp.Core.SurfaceArea.netcore.release.bsl index 4b13d5aa212..d50d8462ed9 100644 --- a/tests/FSharp.Core.UnitTests/FSharp.Core.SurfaceArea.netcore.release.bsl +++ b/tests/FSharp.Core.UnitTests/FSharp.Core.SurfaceArea.netcore.release.bsl @@ -1010,6 +1010,11 @@ Microsoft.FSharp.Core.CompilerServices.ResumptionFunc`1[TData]: Boolean EndInvok Microsoft.FSharp.Core.CompilerServices.ResumptionFunc`1[TData]: Boolean Invoke(Microsoft.FSharp.Core.CompilerServices.ResumableStateMachine`1[TData] ByRef) Microsoft.FSharp.Core.CompilerServices.ResumptionFunc`1[TData]: System.IAsyncResult BeginInvoke(Microsoft.FSharp.Core.CompilerServices.ResumableStateMachine`1[TData] ByRef, System.AsyncCallback, System.Object) Microsoft.FSharp.Core.CompilerServices.ResumptionFunc`1[TData]: Void .ctor(System.Object, IntPtr) +Microsoft.FSharp.Core.CompilerServices.RuntimeAsyncFragmentAwaiter`1[T]: Boolean IsCompleted +Microsoft.FSharp.Core.CompilerServices.RuntimeAsyncFragmentAwaiter`1[T]: Boolean get_IsCompleted() +Microsoft.FSharp.Core.CompilerServices.RuntimeAsyncFragmentAwaiter`1[T]: T GetResult() +Microsoft.FSharp.Core.CompilerServices.RuntimeAsyncFragmentAwaiter`1[T]: Void .ctor(System.Threading.Tasks.ValueTask`1[Microsoft.FSharp.Core.CompilerServices.RuntimeAsyncFragmentResult`1[T]]) +Microsoft.FSharp.Core.CompilerServices.RuntimeAsyncFragmentResult`1[T]: Void .ctor(Microsoft.FSharp.Core.FSharpResult`2[T,System.Runtime.ExceptionServices.ExceptionDispatchInfo]) Microsoft.FSharp.Core.CompilerServices.RuntimeHelpers: Microsoft.FSharp.Control.IEvent`2[TDelegate,TArgs] CreateEvent[TDelegate,TArgs](Microsoft.FSharp.Core.FSharpFunc`2[TDelegate,Microsoft.FSharp.Core.Unit], Microsoft.FSharp.Core.FSharpFunc`2[TDelegate,Microsoft.FSharp.Core.Unit], Microsoft.FSharp.Core.FSharpFunc`2[Microsoft.FSharp.Core.FSharpFunc`2[System.Object,Microsoft.FSharp.Core.FSharpFunc`2[TArgs,Microsoft.FSharp.Core.Unit]],TDelegate]) Microsoft.FSharp.Core.CompilerServices.RuntimeHelpers: System.Collections.Generic.IEnumerable`1[TResult] EnumerateFromFunctions[T,TResult](Microsoft.FSharp.Core.FSharpFunc`2[Microsoft.FSharp.Core.Unit,T], Microsoft.FSharp.Core.FSharpFunc`2[T,System.Boolean], Microsoft.FSharp.Core.FSharpFunc`2[T,TResult]) Microsoft.FSharp.Core.CompilerServices.RuntimeHelpers: System.Collections.Generic.IEnumerable`1[TResult] EnumerateUsing[T,TCollection,TResult](T, Microsoft.FSharp.Core.FSharpFunc`2[T,TCollection]) @@ -1021,6 +1026,7 @@ Microsoft.FSharp.Core.CompilerServices.SetStateMachineMethodImpl`1[TData]: Void Microsoft.FSharp.Core.CompilerServices.SetStateMachineMethodImpl`1[TData]: Void EndInvoke(System.IAsyncResult) 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.FSharpFunc`2[TArg,System.Threading.Tasks.ValueTask`1[Microsoft.FSharp.Core.CompilerServices.RuntimeAsyncFragmentResult`1[T]]] __runtimeAsyncOutline[TArg,T](Microsoft.FSharp.Core.FSharpFunc`2[TArg,T]) Microsoft.FSharp.Core.CompilerServices.StateMachineHelpers: Microsoft.FSharp.Core.FSharpOption`1[System.Int32] __resumableEntry() Microsoft.FSharp.Core.CompilerServices.StateMachineHelpers: System.Collections.Generic.IAsyncEnumerable`1[T] __runtimeAsyncSequence[T](Microsoft.FSharp.Core.FSharpFunc`2[Microsoft.FSharp.Core.Unit,System.Collections.Generic.IEnumerable`1[T]]) Microsoft.FSharp.Core.CompilerServices.StateMachineHelpers: System.Threading.CancellationToken __runtimeAsyncSequenceCancellationToken()