Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
d52977b
runtime-async outlining fallback
majocha Sep 27, 2026
a4824bf
hande delegates
majocha Sep 27, 2026
169d504
Outline residual runtime-async delegate invokes
majocha Sep 27, 2026
f1581fe
release notes
majocha Sep 27, 2026
b780314
Outline nested runtime-async delegate sources
majocha Sep 27, 2026
0eb10c3
Inline single-use runtime-async delegate sources before outlining
majocha Sep 27, 2026
0a17526
add comment
majocha Sep 27, 2026
36b2c68
Refine runtime-async callback outlining
majocha Sep 28, 2026
d409d94
Move runtime-async callback outlining into a lowering pass
majocha Sep 28, 2026
fe36540
Restrict runtime-async outlining tests to NETCOREAPP
majocha Sep 28, 2026
e8e53b0
Inline branch-selected runtime-async callbacks instead of outlining
majocha Sep 28, 2026
94c8df4
Limit copies of branch-selected runtime-async callbacks
majocha Sep 28, 2026
f16aa5f
Merge branch 'main' into majocha-runtime-async-outlining
majocha Oct 5, 2026
68ce5fa
share captured mutables
majocha Oct 5, 2026
8ffc76e
preserve callback debug points
majocha Oct 5, 2026
e08541e
Scope runtime async debug point preservation
majocha Oct 5, 2026
670520e
Merge branch 'main' into majocha-runtime-async-outlining
majocha Oct 5, 2026
e22645f
Merge branch 'main' into majocha-runtime-async-outlining
majocha Oct 7, 2026
a77827c
Preserve mutable runtime-async capture debugger metadata
majocha Oct 7, 2026
52eb317
Reduce runtime-async returned closures across recursive bindings
majocha Oct 8, 2026
c1c326c
Link runtime-async let-rec fix to issue and callback PR
majocha Oct 8, 2026
123ba9f
Merge branch 'main' into majocha-runtime-async-outlining
majocha Oct 8, 2026
a46c872
Merge branch 'main' into majocha-runtime-async-outlining
majocha Oct 8, 2026
f9cf02f
Preserve lexical debug scopes for runtime-async captures
majocha Oct 8, 2026
2967313
Bound runtime-async callback copying before expansion
majocha Oct 8, 2026
375ce9b
Analyze runtime-async callback uses before rewriting
majocha Oct 8, 2026
67ce735
Share runtime-async debug wrappers and lower direct delegate receivers
majocha Oct 8, 2026
cf97cb7
Share runtime-async codegen preparation
majocha Oct 9, 2026
775a670
Guard runtime-async analysis and callback reconstruction stacks
majocha Oct 9, 2026
006b843
Organize runtime-async tests and update design documentation
majocha Oct 9, 2026
fa64b3e
Separate runtime-async callback lowering and unify delegate preparation
majocha Oct 9, 2026
8d90cdb
Merge branch 'main' into majocha-runtime-async-outlining
majocha Oct 10, 2026
bc9aca1
fix merge
majocha Oct 10, 2026
fef20d8
Document runtime-async custom optimizations concisely
majocha Oct 10, 2026
994d184
simplify rewrite
majocha Oct 10, 2026
bb65b2b
Share runtime-async construction prefix handling
majocha Oct 10, 2026
b987025
Merge branch 'main' into majocha-runtime-async-outlining
majocha Oct 12, 2026
fac2306
Merge branch 'main' into majocha-runtime-async-outlining
majocha Oct 12, 2026
4d81c58
Update tests/FSharp.Compiler.ComponentTests/Language/RuntimeAsyncTest…
majocha Oct 12, 2026
4f1fad8
Merge branch 'main' into majocha-runtime-async-outlining
majocha Oct 13, 2026
1e6186f
Merge branch 'main' into majocha-runtime-async-outlining
majocha Oct 13, 2026
4f2f847
Merge branch 'main' into majocha-runtime-async-outlining
majocha Oct 15, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/release-notes/.FSharp.Compiler.Service/11.0.200.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
* 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))
* Preserve SRTP code generation when extension constraint solutions are disabled. ([PR #20773](https://github.com/dotnet/fsharp/pull/20773))
* `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: a suspending function or delegate selected by a branch is constructed once, recording the selected branch and immutable captures while keeping mutable captures shared with other closures. Each invocation inlines the selected body into the enclosing runtime-async method, so construction effects are not duplicated and `AsyncLocal` changes made by the callback remain visible. Inline single-use delegates through effect-free conditional construction, including nested merged sources and async-sequence consumers; also lower conditional receivers left directly on `Invoke` after optimization, while rejecting suspensions in unmarked delegate methods. Callback inlining and runtime-async body preparation now run in a dedicated lowering pass after optimization, so awaits inlined into exception handlers are rewritten. Shared debug-wrapper handling preserves callback statement points and captured mutable locals' lexical scopes during construction and invocation without adding reference-cell allocations. Codegen shares marker preparation and closure validation. Stack guards protect callback reconstruction, capture analysis, and suspension-flow analysis for deeply nested bodies. Branch sizes and captures are cached, and callback uses and the existing copy budget are checked before expanding invocations. Single-use delegates share one construction analysis for eligibility and invocation rewriting. ([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<T>` upcast to `Task`, including through `ConfigureAwait`. ([Issue #20686](https://github.com/dotnet/fsharp/issues/20686), [PR #20726](https://github.com/dotnet/fsharp/pull/20726))
* Struct/reference anonymous-record mismatches now name anonymous records rather than tuples. ([Issue #6110](https://github.com/dotnet/fsharp/issues/6110), [PR #20681](https://github.com/dotnet/fsharp/pull/20681))
* 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))
Expand Down
177 changes: 123 additions & 54 deletions docs/runtime-async.md

Large diffs are not rendered by default.

159 changes: 85 additions & 74 deletions src/Compiler/CodeGen/IlxGen.fs
Original file line number Diff line number Diff line change
Expand Up @@ -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 ->
Expand Down Expand Up @@ -3349,6 +3353,28 @@ and GenExprAux (cenv: cenv) (cgbuf: CodeGenBuffer) eenv expr (sequel: sequel) =

| Expr.LetRec(binds, body, m, _) -> GenLetRec cenv cgbuf eenv (binds, body, m) sequel

| Expr.Op(TOp.DebugLocalScope(vref, name), [], [ body ], m) ->
if cenv.options.generateDebugSymbols then
let _, endMark as scopeMarks = StartLocalScope "debugLocal" cgbuf

match StorageForValRef m vref eenv with
| Local(index, _, _) -> cgbuf.AddLocalDebugRange(index, name, scopeMarks)
| _ when not cenv.options.localOptimizationsEnabled ->
if vref.IsMutable && not (Optimizer.IsKnownOnlyMutableBeforeUse vref) then
error (InternalError("debug local scope requires shared storage for a mutable capture", m))

// Escaping mutables are reference cells, so the alias copies their shared home.
let index =
cgbuf.AllocLocal([ (name, scopeMarks) ], GenTypeOfVal cenv eenv vref.Deref, false, false)

GenGetVal cenv cgbuf eenv (vref, m) Continue
EmitSetLocal cgbuf index
| _ -> ()

GenExpr cenv cgbuf eenv body (EndLocalScope(sequel, endMark))
else
GenExpr cenv cgbuf eenv body sequel

| Expr.Lambda _
| Expr.TyLambda _ -> GenLambda cenv cgbuf eenv false [] expr sequel

Expand Down Expand Up @@ -7183,28 +7209,24 @@ 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
}

let ilCode =
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
Expand All @@ -7216,7 +7238,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 =
Expand All @@ -7235,48 +7257,42 @@ 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(
InterruptibleLazy(fun _ ->
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 =
Expand Down Expand Up @@ -7462,6 +7478,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
Expand Down Expand Up @@ -7489,28 +7535,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)
Expand Down Expand Up @@ -7557,28 +7583,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
Expand Down Expand Up @@ -8053,7 +8059,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
Expand Down Expand Up @@ -8142,8 +8149,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
Expand Down Expand Up @@ -10232,10 +10245,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
Expand Down
18 changes: 18 additions & 0 deletions src/Compiler/Driver/OptimizeInputs.fs
Original file line number Diff line number Diff line change
Expand Up @@ -397,6 +397,24 @@ let ApplyAllOptimizations

addPhase "firstLoop" firstLoop

let runtimeAsyncReportedRanges =
System.Collections.Concurrent.ConcurrentDictionary<range, unit>()

let lowerRuntimeAsync
({
File = file
PrevPhase = prevPhase
PrevFile = _prevFile
}: PhaseInputs)
: PhaseRes =
let file =
LowerRuntimeAsync.TransformImplFile tcGlobals runtimeAsyncReportedRanges file

file, prevPhase

// Must precede lowerLocalMutables: inlined callbacks introduce mutable locals that closures can capture.
addPhase "lowerRuntimeAsync" lowerRuntimeAsync

let lowerLocalMutables
({
File = file
Expand Down
2 changes: 2 additions & 0 deletions src/Compiler/FSharp.Compiler.Service.fsproj
Original file line number Diff line number Diff line change
Expand Up @@ -473,6 +473,8 @@
<Compile Include="Optimize\LowerComputedCollections.fs" />
<Compile Include="Optimize\LowerStateMachines.fsi" />
<Compile Include="Optimize\LowerStateMachines.fs" />
<Compile Include="Optimize\LowerRuntimeAsync.fsi" />
<Compile Include="Optimize\LowerRuntimeAsync.fs" />
<Compile Include="Optimize\LowerLocalMutables.fsi" />
<Compile Include="Optimize\LowerLocalMutables.fs" />
<Compile Include="CodeGen\IlxGenSupport.fsi" />
Expand Down
5 changes: 4 additions & 1 deletion src/Compiler/Optimize/LowerLocalMutables.fs
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,10 @@ let DecideImplFile g amap implFile =
let TransformExpr g (heapValMap: ValMap<_>) exprF expr =

match expr with
| Expr.Op (TOp.DebugLocalScope (ValDeref(v), name), [], [body], m) when heapValMap.ContainsVal v ->
let nv, _ = heapValMap[v]
Some (Expr.Op (TOp.DebugLocalScope (mkLocalValRef nv, name), [], [exprF body], m))

// Rewrite uses of mutable values
| Expr.Val (ValDeref(v), _, m) when heapValMap.ContainsVal v ->

Expand Down Expand Up @@ -202,4 +206,3 @@ let TransformImplFile g amap implFile =
PostTransform = (fun _ -> None)
RewriteQuotations = true
StackGuard = StackGuard("AutoboxRewriteStackGuardDepth") }

Loading
Loading