Skip to content

Add the F# hot reload session and FCS service surface - #20030

Open
NatElkins wants to merge 200 commits into
dotnet:mainfrom
NatElkins:hotreload-session
Open

NatElkins wants to merge 200 commits into
dotnet:mainfrom
NatElkins:hotreload-session

Conversation

@NatElkins

@NatElkins NatElkins commented Jul 3, 2026 •

Copy link
Copy Markdown
Contributor

Adds the experimental FSharpChecker.CreateHotReloadSession API so a host can coordinate F# hot reload across multiple projects. It connects project snapshots to the typed-tree differ and delta emitter through an explicit session lifecycle.

Each session owns independent committed baselines, typed trees, and generation chains. The session keys projects by project path and output path. Runtime capabilities and debugger-supplied active statements apply across the session. Disposal releases only that session's state.

The lifecycle follows Roslyn's separation between update emission and commit:

  • AddProject captures a baseline from an existing output assembly and the project's checked snapshot. It reconstructs closure identities from portable PDB metadata and restores the synthesized-name map.
  • EmitDelta compares the updated snapshot and rebuilt output with the committed baseline. It returns metadata, IL, optional PDB data, required capabilities, and debugger updates. The resulting baseline remains pending.
  • Commit advances every pending project baseline after the host applies the updates. Discard drops pending updates and leaves the committed view unchanged.

The session retains original assembly bytes and metadata row identities. Compiler capture remains opt-in through --test:HotReloadDeltas. Recorded closure identities and synthesized names provide continuity across generations and sessions that start from disk.

Output fingerprints reject stale binaries when semantic edits require a rebuild. The session also tracks non-source inputs such as resources, key files, and loaded scripts. Changes to those inputs trigger the same freshness check even when the typed trees remain unchanged. Pending input state advances with Commit and clears with Discard.

The checker requires keepAssemblyContents=true, and a built output assembly is required before AddProject or EmitDelta. Runtime capabilities gate supported edits. Unsupported edits return structured FSHRDL diagnostics, while compilation failures and unavailable outputs return explicit errors. The host remains responsible for rebuilds, runtime update application, and restart behavior.

This PR supplies the session layer for the full hot reload implementation and supports the external-build workflow. The optional in-process compiler path belongs to #20031.

Tests cover independent sessions, multiple projects, commit and discard behavior, output-path isolation, stale outputs, and tracked inputs. Additional coverage verifies generated-name continuity, capability changes, active statements, metadata, and runtime application.

@github-actions

github-actions Bot commented Jul 3, 2026 •

Copy link
Copy Markdown
Contributor

✅ Release-note check exempted

The NO_RELEASE_NOTES label or localization-bot authorship exempts this pull request.

NatElkins added 23 commits July 17, 2026 12:27
…ethod CDI emission

Adds an internal AbstractIL module implementing, byte for byte, the three Portable PDB
CustomDebugInformation blob formats Roslyn persists per method for Edit and Continue
(EnC Local Slot Map, EnC Lambda and Closure Map, EnC State Machine State Map), with
serializers, deserializers, a portable PDB read-back helper, and an occurrence-key
packing helper for deterministic syntax-offset slots.

Plumbs an optional methodCustomDebugInfoRows side channel through the IL binary writer
options into the portable PDB generator so a compilation can attach CDI rows to named
methods. Names that do not identify exactly one method row are dropped. All existing
writer call sites pass an empty map, so emitted PDBs are byte-identical to before.

No in-tree caller populates the map yet; the consumer is the F# hot reload work in
dotnet#19941, following the same pattern as dotnet#20017 (land isolated, test-covered
infrastructure first, wire the feature later).

Tests: blob round-trips, Roslyn golden-byte encodings, cross-validation against
CDI blobs emitted by a real Roslyn compilation, fail-closed occurrence-key packing
(including an int32-overflow regression where a wrapped negative key previously
escaped the bound check), and end-to-end synthetic PDB emission proving correct
MethodDef parenting, zero rows for an empty map, and no rows for absent or
ambiguous names.
ProcessStartInfo.ArgumentList does not exist on net472, which the component tests
also target on Windows CI. Build the quoted argument string by hand instead.
xunit 3.2.2 no longer discovers internal test classes, so the module was
silently skipped after rebasing onto current main (pre-existing internal test
modules like CompilerService.Caches are likewise undiscovered there). Public
visibility restores discovery; 17 tests run and pass.
Add internal generated-name normalization and synthesized-name map replay support as a standalone slice. The new map state is side-channel based, all new compiler modules remain internal, and CompilerGlobalState preserves the existing no-map counter path while checking an accessor captured once per compiler state.

Route existing IlxGen generated-name allocations through inert helper wrappers, add pure name-map and normalizer tests, add a normal compilation determinism guard over emitted generated names, and document the extracted seams in P5_REPORT.md.

Verification: built FSharp.Compiler.Service, FSharp.Compiler.Service.Tests, FSharp.Compiler.ComponentTests, and FSharpSuite.Tests in Release; ran the migrated service test classes, the component determinism class, FSharpSuite DeterministicTests, and the FCS SurfaceArea class successfully.
Adds an internal, standalone ECMA-335 Edit-and-Continue metadata delta writer to
AbstractIL: delta #- table stream and heap construction (DeltaMetadataTables,
DeltaMetadataSerializer, DeltaTableLayout, DeltaIndexSizing), ECMA-335 II.24.2.6
coded-index encoding (DeltaMetadataEncoding), EncLog/EncMap emission, generation GUID
chaining, user-string and standalone-signature token calculators (IlxDeltaStreams),
and the coordinating writer (FSharpDeltaMetadataWriter) over a plain row-description
input model (DeltaMetadataTypes, ILDeltaHandles, ILMetadataHeaps).

The writer's inputs are row records (names, tokens, signatures, RVAs) plus heap
offsets; it has no dependency on any semantic diffing or session machinery. It
compiles with no in-tree consumer by design: the consumer is the F# hot reload work
in dotnet#19941, following the same upstreaming pattern as dotnet#20017 and dotnet#20018
(land isolated, test-covered infrastructure first, wire the feature in a later PR).

One line of ilwrite.fsi is touched to expose the pre-existing markerForUnicodeBytes
so the delta writer reuses the exact string-marker logic of the full writer. No
behavior change for any existing code path.

Tests (130): coded-index encodings asserted against the production definitions and
ECMA-335 II.24.2.6 order, System.Reflection.Metadata reader parity over emitted
deltas, EncLog/EncMap correctness, stream layout, heap and index sizing,
multi-generation heap-offset chaining asserted against computed expected values,
standalone-signature rows asserted at baseline+1 from a real seeded baseline, and
serializer failure paths.
Add an internal TypedTreeDiff module that snapshots CheckedImplFile bindings and entities, then classifies body, signature, inline, declaration add/remove, and type layout changes without depending on hot reload sessions, runtime capability negotiation, EnC capability names, baseline state, or delta emission.

Add focused FCS tests for unchanged/reference-equal files, body edits, signature edits, additions, deletions, layout changes, and logical-name arity handling. Wire the module and tests into compile order, add a release note, and include P6_REPORT.md.

Verification:

- ./.dotnet/dotnet build src/Compiler/FSharp.Compiler.Service.fsproj -c Debug /p:BUILDING_USING_DOTNET=true

- ./.dotnet/dotnet test tests/FSharp.Compiler.Service.Tests/FSharp.Compiler.Service.Tests.fsproj -c Debug /p:BUILDING_USING_DOTNET=true -- --filter-class "*TypedTreeDiffTests*"

- ./.dotnet/dotnet test tests/FSharp.Compiler.Service.Tests/FSharp.Compiler.Service.Tests.fsproj -c Debug /p:BUILDING_USING_DOTNET=true -- --filter-class "*SurfaceAreaTest*"
…CI images

The Roslyn cross-validation test shells out to dotnet build to produce a
real Roslyn PDB. The process launch had two problems. It computed the
host path by hand from __SOURCE_DIRECTORY__, which misses on CI images
that carry no repo-local .dotnet at that depth, failing with
Win32Exception before the build starts. It also left UseShellExecute at
its default, which is true on net472 and rejects redirected streams, so
every Desktop test leg failed deterministically with
InvalidOperationException.

Resolve the host like the rest of the test framework via
TestFramework.initialConfig.DotNetExe, which prefers the repo-local
.dotnet and falls back to PATH, and set UseShellExecute to false
explicitly.

Verified: FSharp.Compiler.ComponentTests builds clean;
EncMethodDebugInformationTests 17 passed, 0 failed (net10.0); fantomas
clean on the touched file.
Add standalone baseline PE and portable PDB readers for hot reload, including token maps and MVID/PDB table snapshots.

Carry the F# synthesized-name snapshot module CDI codec and portable PDB read path, with recorded snapshots taking precedence over IL reconstruction.

Add focused component tests for snapshot round-trip, direct module CDI reading, baseline token maps, recorded fallback behavior, and EnC closure-name reconstruction.
Add the internal hot reload delta emitter, symbol matcher, and direct emitter tests.

Keep session and service integration deferred.
…erload

CI compiles the snapshot round-trip and baseline reader tests with
FS0193: the explicit Assert.Equal<string> type application commits
overload resolution to the scalar Equal(T, T) shape while both
arguments are string arrays. Use Assert.Equal<string[]>, the form the
neighboring name-map tests already use, so resolution lands on the
structural array comparison everywhere.

Verified: FSharp.Compiler.ComponentTests builds clean;
EncMethodDebugInformationTests 22 passed, 0 failed (net10.0); fantomas
clean on the touched file.
…cation

This slice carries the full typed-tree differ, which treats a
parameter-type change as a removal plus an insert of distinct symbols
rather than one matched symbol with a changed signature. The removal
surfaces as the rude edit (DeclarationRemoved) and the replacement as
an insert semantic edit, so the earlier expectations (no semantic
edits, SignatureChange) no longer describe this tree. Assert the
observable contract instead: exactly one rude edit with the removal
classification. Hosts reject the whole update when any rude edit is
present, so the accompanying insert never applies.

Verified: TypedTreeDiffTests 8 passed 0 failed, fantomas clean on the
touched file.
Give declaration matching, registration, and no-edit cleanup one private
owner. Prepare added field, constant, type, generic, and interface rows from
read-only registry views. Keep debug-line analysis in its own typed helper.

Preserve synthesized-name snapshot timing, parent-first matching, reference
allocation order, and every existing fail-closed condition. The extracted
algorithm bodies match their previous forms after parameter binding.

Roslyn parity reference: eb789e2741f6f22d9e283e2049dc1378871323e0
(2026-06-09). The refactor retains the existing metadata encodings.

Validation: HotReload service/component suites, actual mdv parity and runtime
ApplyUpdate, plus 1660 passing Release EmittedIL cases on the umbrella branch.
Enumerate pending dictionaries under their existing locks. The loop changes only the separate committed dictionary and clears pending state afterward.

Validation: service-project Debug build, Fantomas check, and six existing session and tracked-input cases pass with no skips.
Match stable identities explicitly so the local fallback string is allocated only when no stable identity exists.

Validation: all 101 typed-tree diff tests pass after the service project rebuild.
The truncated path prefix matches all namespace segments only when the path contains the full prefix.
Skip the initializer body comparison for ordinary methods. Retain the exact comparison for field-backed values and preserve diagnostic order.
Iterate over the equal-length point lists directly. Preserve all hidden-point, offset, column, and line-shift checks.
Select the points with the lowest start line and highest end line directly. Preserve the nonempty-list guard and zero-based output coordinates.
Recorded synthesized names retain exact identities. Select ordinary matching before copying and sorting unused alias buckets. Reconstructed snapshots retain alias expansion.

Add both snapshot modes to the matcher coverage, including emitted-name and ordinary method lookups. The Debug component build and three matcher cases pass.
Filter through the existing structural hash set to retain each first occurrence. This avoids the temporary reversed result list.

Extend the multiple-method test with repeated requests. Exact EncLog and EncMap assertions still pass. The Debug component build and all four focused cases pass. Metadata encoding remains unchanged.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

⚠️ Affects-Build-Infra Tooling check: PR touches build infrastructure ⚠️ Affects-Compiler-Output Tooling check: PR touches IL emission or codegen AI-reviewed PR reviewed by AI review council AI-Tooling-Check-Scanned-Clean Tooling check: diff analyzed, no interesting infrastructure files NO_RELEASE_NOTES Label for pull requests which signals, that user opted-out of providing release notes

Projects

Status: In Progress

Development

Successfully merging this pull request may close these issues.

2 participants