Skip to content

Commit 5a9be05

Browse files
committed
Emit class-form resumable state machines under hot reload (EnC)
Add an Edit-and-Continue codegen mode (--test:HotReloadClassStateMachines) that lowers task/taskSeq/user resumable computation expressions to reference-type (class) state machines instead of structs, so adding, removing, or reordering a let!/do!/yield becomes an AddInstanceFieldToExistingType + method update at runtime rather than a forbidden value-type re-layout. This mirrors Roslyn's struct->class flip under EnC, generalized to the resumable-code substrate (the gate lives in the shared GenStructStateMachine). - IlxGen: boxity threaded through TemplateReplacement; class type-def (parameterless ctor, newobj, reference-based capture init); a byref-this bridge (spill 'this' to a byref local via ldarga + ldobj deref before field access) so the byref-threaded resumable ABI works on a reference type. - TypedTreeDiff: under class mode a resumable-member shape change is an allowed, capability-gated method/field update rather than a StateMachineShapeChange rude edit. - IlxDeltaEmitter: a fresh instance field on an existing compiler-generated class state machine is emitted as an added field (AddInstanceFieldToExistingType) instead of being skipped. - TcGlobals/TcConfig: the flag drives codegen and the classifier consistently across the baseline and delta compiles. - Tests: runtime ApplyUpdate parity tests (add/remove let!, add inside a loop, backgroundTask) for the edits Roslyn allows. Flag-off is byte-identical (mkILValueTy = mkILNamedTy AsValue); the deref and spill only fire under an active class-state-machine template replacement.
1 parent d917915 commit 5a9be05

12 files changed

Lines changed: 366 additions & 28 deletions

File tree

‎src/Compiler/CodeGen/IlxDeltaEmitter.fs‎

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3773,6 +3773,23 @@ let emitDeltaWithDebugData (freshDebugPdb: byte[] option) (request: IlxDeltaRequ
37733773
$"Edit adds instance field '{fieldDisplay}' to a struct; struct layouts cannot change under hot reload. Please rebuild."
37743774

37753775
raise (HotReloadUnsupportedEditException message)
3776+
| None when
3777+
not fieldDef.IsStatic
3778+
&& not typeDef.IsStructOrEnum
3779+
&& request.Baseline.TypeTokens |> Map.containsKey baselineDeclaringType
3780+
->
3781+
// A fresh instance field on an EXISTING compiler-generated CLASS: a class-form
3782+
// resumable state machine (hot reload) that gained a hoisted local / awaiter
3783+
// when a let!/do!/yield was added. Unlike a struct, a reference type CAN grow at
3784+
// runtime (AddInstanceFieldToExistingType), so the field must be emitted rather
3785+
// than dropped by the synthesized-name skip below - the patched MoveNext
3786+
// references it, and dropping it crashes at runtime with an invalid field token.
3787+
// (Must run before the synthesized-name skip and after the struct-layout gate.)
3788+
if not (addedFieldTokens.ContainsKey fieldKey) then
3789+
let newFieldToken =
3790+
emittedTokenMappings.FieldDefTokenMap (enclosing, typeDef) fieldDef
3791+
3792+
addedFieldTokens[fieldKey] <- newFieldToken
37763793
| None when synthesizedBuckets.IsSome && IsCompilerGeneratedName typeDef.Name -> ()
37773794
| None when
37783795
(fieldDef.IsStatic || not typeDef.IsStructOrEnum)

‎src/Compiler/CodeGen/IlxGen.fs‎

Lines changed: 114 additions & 24 deletions
Large diffs are not rendered by default.

‎src/Compiler/Driver/CompilerConfig.fs‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -741,6 +741,8 @@ type TcConfigBuilder =
741741

742742
mutable realsig: bool
743743

744+
mutable emitHotReloadClassStateMachines: bool
745+
744746
mutable compilationMode: TcGlobals.CompilationMode
745747
}
746748

@@ -942,6 +944,7 @@ type TcConfigBuilder =
942944
}
943945
dumpSignatureData = false
944946
realsig = false
947+
emitHotReloadClassStateMachines = false
945948
strictIndentation = None
946949
compilationMode = TcGlobals.CompilationMode.Unset
947950
}
@@ -1487,6 +1490,7 @@ type TcConfig private (data: TcConfigBuilder, validate: bool) =
14871490
member _.typeCheckingConfig = data.typeCheckingConfig
14881491
member _.dumpSignatureData = data.dumpSignatureData
14891492
member _.realsig = data.realsig
1493+
member _.emitHotReloadClassStateMachines = data.emitHotReloadClassStateMachines
14901494
member _.compilationMode = data.compilationMode
14911495

14921496
static member Create(builder, validate) =

‎src/Compiler/Driver/CompilerConfig.fsi‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -590,6 +590,8 @@ type TcConfigBuilder =
590590

591591
mutable realsig: bool
592592

593+
mutable emitHotReloadClassStateMachines: bool
594+
593595
mutable compilationMode: TcGlobals.CompilationMode
594596
}
595597

@@ -965,6 +967,8 @@ type TcConfig =
965967

966968
member realsig: bool
967969

970+
member emitHotReloadClassStateMachines: bool
971+
968972
member compilationMode: TcGlobals.CompilationMode
969973

970974
/// Represents a computation to return a TcConfig. Normally this is just a constant immutable TcConfig,

‎src/Compiler/Driver/CompilerImports.fs‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2602,6 +2602,7 @@ and [<Sealed>] TcImports
26022602
tcConfig.pathMap,
26032603
tcConfig.langVersion,
26042604
tcConfig.realsig,
2605+
tcConfig.emitHotReloadClassStateMachines,
26052606
tcConfig.compilationMode
26062607
)
26072608

‎src/Compiler/Driver/CompilerOptions.fs‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1469,6 +1469,13 @@ let testFlag tcConfigB =
14691469
// Hook-only replay mode: keeps synthesized-name replay active for hot reload sessions
14701470
// without enabling baseline-capture emission for the current compilation.
14711471
configureHotReloadEmitHook tcConfigB
1472+
| "HotReloadClassStateMachines" ->
1473+
// Emit resumable (task/taskSeq/user CE) state machines as reference types
1474+
// (classes) so adding/removing a let!/do!/yield is an
1475+
// AddInstanceFieldToExistingType + method update (Roslyn parity) rather than a
1476+
// forbidden struct re-layout. Must be set on BOTH the baseline and the delta
1477+
// compile so they agree on the state machine shape.
1478+
tcConfigB.emitHotReloadClassStateMachines <- true
14721479
#if DEBUG
14731480
| "ShowParserStackOnParseError" -> showParserStackOnParseError <- true
14741481
#endif

‎src/Compiler/Service/IncrementalBuild.fs‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -563,7 +563,8 @@ type FrameworkImportsCache(size) =
563563
// and updated realsig and langversion
564564
let tcGlobals =
565565
if tcGlobals.langVersion <> tcConfig.langVersion
566-
|| tcGlobals.realsig <> tcConfig.realsig then
566+
|| tcGlobals.realsig <> tcConfig.realsig
567+
|| tcGlobals.emitHotReloadClassStateMachines <> tcConfig.emitHotReloadClassStateMachines then
567568
TcGlobals(
568569
tcGlobals.compilingFSharpCore,
569570
tcGlobals.ilg,
@@ -578,6 +579,7 @@ type FrameworkImportsCache(size) =
578579
tcGlobals.pathMap,
579580
tcConfig.langVersion,
580581
tcConfig.realsig,
582+
tcConfig.emitHotReloadClassStateMachines,
581583
tcConfig.compilationMode
582584
)
583585

‎src/Compiler/Service/TransparentCompiler.fs‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -969,6 +969,7 @@ type internal TransparentCompiler
969969
if
970970
tcGlobals.langVersion <> tcConfig.langVersion
971971
|| tcGlobals.realsig <> tcConfig.realsig
972+
|| tcGlobals.emitHotReloadClassStateMachines <> tcConfig.emitHotReloadClassStateMachines
972973
then
973974
TcGlobals(
974975
tcGlobals.compilingFSharpCore,
@@ -984,6 +985,7 @@ type internal TransparentCompiler
984985
tcGlobals.pathMap,
985986
tcConfig.langVersion,
986987
tcConfig.realsig,
988+
tcConfig.emitHotReloadClassStateMachines,
987989
tcConfig.compilationMode
988990
)
989991
else

‎src/Compiler/TypedTree/TcGlobals.fs‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,7 @@ type TcGlobals(
197197
pathMap: PathMap,
198198
langVersion: LanguageVersion,
199199
realsig: bool,
200+
emitHotReloadClassStateMachines: bool,
200201
compilationMode: CompilationMode) =
201202

202203
let v_langFeatureNullness = langVersion.SupportsFeature LanguageFeature.NullnessChecking
@@ -1145,6 +1146,11 @@ type TcGlobals(
11451146

11461147
member _.realsig = realsig
11471148

1149+
/// Hot reload: emit resumable (task/taskSeq/user CE) state machines as reference types
1150+
/// (classes) so adding/removing a let!/do!/yield is an AddInstanceFieldToExistingType +
1151+
/// method update rather than a forbidden struct re-layout. Set by --test:HotReloadClassStateMachines.
1152+
member _.emitHotReloadClassStateMachines = emitHotReloadClassStateMachines
1153+
11481154
member _.unionCaseRefEq x y = primUnionCaseRefEq compilingFSharpCore fslibCcu x y
11491155

11501156
member _.valRefEq x y = primValRefEq compilingFSharpCore fslibCcu x y

‎src/Compiler/TypedTree/TcGlobals.fsi‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,7 @@ type internal TcGlobals =
151151
pathMap: Internal.Utilities.PathMap *
152152
langVersion: Features.LanguageVersion *
153153
realsig: bool *
154+
emitHotReloadClassStateMachines: bool *
154155
compilationMode: CompilationMode ->
155156
TcGlobals
156157

@@ -901,6 +902,11 @@ type internal TcGlobals =
901902

902903
member realsig: bool
903904

905+
/// Hot reload: emit resumable (task/taskSeq/user CE) state machines as reference types
906+
/// (classes), so adding/removing a let!/do!/yield is an AddInstanceFieldToExistingType +
907+
/// method update rather than a forbidden struct re-layout.
908+
member emitHotReloadClassStateMachines: bool
909+
904910
member ref_tuple1_tcr: TypedTree.EntityRef
905911

906912
member ref_tuple2_tcr: TypedTree.EntityRef

0 commit comments

Comments
 (0)