refactor: AbstractIL EnC foundations (hot reload stack 1/n)#2
Draft
NatElkins wants to merge 3 commits into
Draft
refactor: AbstractIL EnC foundations (hot reload stack 1/n)#2NatElkins wants to merge 3 commits into
NatElkins wants to merge 3 commits into
Conversation
…Log writer seam Restructures the AbstractIL writer so that Edit-and-Continue delta emission can later plug into it without changing emitted output: - MetadataTable<'T> becomes a sealed class with encapsulated storage; external consumers go through Count/Entries/KeyValueSeq instead of reaching into the record fields. - Method body encoding is factored into EncodedMethodBody/EncodeMethodBody so the same encoding path can be reused for delta method bodies. - writeBinaryAux gains a snapshot sink seam (writeBinaryAuxWithSnapshotSink); the default path passes a no-op sink, and WriteILBinaryInMemoryWithArtifacts exposes token mappings plus a MetadataSnapshot (heap sizes, table row counts, GUID heap start). - New ILDeltaHandles module provides typed row handles, coded-index unions, and EnC operation values; new ILEncLogWriter module defines the IEncLogWriter abstraction with a no-op NullEncLogWriter used by full-assembly emission and a recording DeltaEncLogWriter for future delta emission. The new entry points are internal and not yet used by the product. Full-assembly emission behavior is unchanged and output stays byte-identical.
Adds internal modules that read baseline metadata needed to seed Edit-and-Continue delta generation: - ILBaselineReader parses the metadata root, stream headers, table row counts, and heap sizes directly from PE image bytes (and the analogous information from portable PDB images), producing the MetadataSnapshot shape introduced in the previous commit. It mirrors what System.Reflection.Metadata's MetadataReader would provide while matching SRM's #Strings trimming semantics that EnC heap aggregation depends on. - ILMetadataHeaps defines the IMetadataHeaps abstraction over string/blob/GUID/user-string heap indexing. - ILRowIndexing defines the IRowIndexStrategy abstraction for row ID assignment, with the sequential full-assembly implementation. These modules are internal and not yet used by the product; they are the reading-side foundation for delta emission that follows in subsequent changes. No behavior change.
This was referenced Jun 12, 2026
Closed
Owner
Author
|
The single-branch upstream draft PR is now open: dotnet#19941 — that's the one place for review feedback. This foundations PR stays as the first carve-out for the stacked-PR sequencing described there. |
NatElkins
added a commit
that referenced
this pull request
Jul 2, 2026
Two synthesized-closure families could not be aligned across compiles after a
line-adding edit, so such edits always fell back to restart on closure-dense
real-world files (reproduced on Giraffe's endpoint list, failing identically on
the external and in-process paths):
- debug-pipe closures embed the source line in the basic name itself
('Pipe #1 stage #2 at line 28'), so a line shift changes the name and the
fresh type has no baseline counterpart;
- value-binding list closures use sequential ordinals (endpoints@hotreload-N)
allocated in encounter order, so shifts make one fresh name match several
baseline candidates.
Add a last-resort positional pairing to the delta emitter's synthesized-type
match ladder, consulted only after exact and path-normalized matching fail or
tie. Groups are keyed by enclosing type plus a line-normalized basic name; the
normalizer recognizes only the known generated forms (pipe labels, @hotreload
ordinals, legacy @line ordinals) and refuses everything else, including
occurrence-keyed names, which already match exactly. Pairing requires equal
group counts, distinct ordinals on both sides, an unused baseline token, and a
structural shape match per pair (generic arity, base type, interfaces, field
type multiset, method name/arity set, with self-references normalized), and
produces a bijection; any failure keeps today's fail-closed rude edit, so a
structural change such as adding a pipe stage or list lambda still requires a
rebuild rather than risking a mispair, which would be silent corruption.
Baseline synthesized-type shapes are derived from the module at session start
and chained for delta-added types; no persisted format changes.
This is the recovery layer: it makes line-shift edits on these families apply
in place when the replayed names drift. A follow-up addresses the drift at its
source by keying the replay name map on line-normalized identity so matched
closures keep their generation-0 names, Roslyn's model; exact matching then
succeeds without inference and this pairing remains a guarded fallback.
Component acceptance: in-process and disk-started line-shifted pipe closures
and ordinal-shifted value-list closures each emit with exactly one user-authored
updated method; count-mismatch cases stay UnsupportedEdit. Service suite 423
passed, component 234 passed (2 expected manual-host skips).
NatElkins
added a commit
that referenced
this pull request
Jul 2, 2026
Replay of compiler-generated closure names was keyed by the raw basic name, and
debug-pipe closures embed the source line in the basic name itself ('Pipe #1
stage #2 at line 28'), so any line-adding edit changed the bucket key, missed
the recorded generation-0 allocation, and produced fresh names the baseline
could not match. Value-binding list closures shared a bucket whose sequential
ordinals depended on encounter order and drifted similarly. Both families then
depended on the positional-pairing recovery layer or fell back to restart.
Key replay buckets by the line-normalized basic name and keep the original
generation-0 full names as the values, so a matched closure whose code moves
from line 28 to line 30 is handed back its line-28 birth name. This is Roslyn's
Edit and Continue model: synthesized-member identity is established at first
allocation and replayed exactly, making downstream matching exact rather than
inferred. The normalization grammar is extracted to one shared home in
GeneratedNames and reused by the map, the baseline snapshot collector, the
symbol matcher, and the delta emitter's pairing layer, which now acts purely as
a recovery net behind exact matching.
Snapshot compatibility is handled at load: raw legacy keys are normalized,
duplicate buckets merge, replay buckets canonicalize by ordinal, and gapped
buckets recover the unambiguous raw birth basic name from recorded entries
rather than synthesizing a name that would lose the original line text (a
normalized-key fallback remains for genuinely ambiguous recovery). The
ICompilerGeneratedNameMap surface and the persisted Map<string, string[]>
snapshot shape are unchanged; behavior without an installed map is untouched.
Acceptance: the flag-on in-process session test decodes TypeDef names from the
baseline and fresh assemblies after a line-adding edit and asserts exact name
equality for the unchanged closure families. Service HotReload suite: 426
passed, 0 failed. Component suite: 234 passed, 0 failed, 2 expected skips.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
AbstractIL EnC foundations (hot reload stack 1/n)
First PR of the F# hot reload stack: the behavior-neutral AbstractIL/ilwrite groundwork
that Edit-and-Continue delta emission builds on. No feature flag, no behavior change, no
public surface change — evidence below.
Context: the complete, working implementation this slice comes from is reviewable as
PR #3 (inspection overview), and you can
try the end-to-end
dotnet watchexperience viadocs/hot-reload-quickstart.md.
The design docs (architecture/entity model, closure mapping, emission templates,
capabilities, active statements) are linked from PR #3's description.
What's in this slice (9 files, +2,234/−92)
1.
ilwrite.fs/.fsi— structural refactor with an EnC seam (commit 1)MetadataTable<'T>converted from a record to a sealed class (call-site changes:KeyValueSeq/Nameaccessors), making table row bookkeeping abstractableIEncLogWriterseam: the metadata writer reports row additions/updates throughan interface whose default implementation (
createNullEncLogWriter) is a no-op — zerocost and zero behavior on every existing path. EnC delta writers (later in the stack)
implement it to record EncLog/EncMap entries
MetadataHeapSizes/MetadataSnapshot: a read-only snapshot of emitted table row counts +heap sizes, exposed via an optional snapshot sink (
writeBinaryAuxWithSnapshotSink,default no-op) and
WriteILBinaryInMemoryWithArtifacts— this is how a baseline's exactlayout is captured for later delta computation
t→tag) andEncodeMethodBodyfactoring in touched code2. New pure AbstractIL modules (commit 2) — not yet referenced by the product
ILDeltaHandles.fsEditAndContinueOperation(the EncLog op codes the CLR'sApplyDeltaexpects)ILEncLogWriter.fsIEncLogWriterimplementations: no-op default + a recordingDeltaEncLogWriterILMetadataHeaps.fsILRowIndexing.fsILBaselineReader.fsAll
internal. They are intentionally unused here so this PR stays purely structural; thenext slice (the hot reload session entity + classification) is their first consumer.
3. Release notes entry (commit 3)
Evidence of behavior-neutrality
FSharp.slnxDebug build: 0 errors / 0 warningsis byte-equivalent to main
internal)The stack
each with its tests and design doc