Skip to content

Add stable synthesized-name replay infrastructure for hot reload#20024

Open
NatElkins wants to merge 5 commits into
dotnet:mainfrom
NatElkins:hotreload-stable-names
Open

Add stable synthesized-name replay infrastructure for hot reload#20024
NatElkins wants to merge 5 commits into
dotnet:mainfrom
NatElkins:hotreload-stable-names

Conversation

@NatElkins

@NatElkins NatElkins commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

Adds the stable synthesized-name replay layer used by F# hot reload: a normalization grammar for compiler-generated names (GeneratedNames.fs), a replay map (FSharpSynthesizedTypeMaps) that hands back recorded generation-0 names by allocation slot, and an internal side-channel (ICompilerGeneratedNameMap) threaded through CompilerGlobalState so a hot reload session can install the map for a delta compile.

Why: Edit and Continue needs "small IL diff for small source diff". Generated names embed source lines, so adding one line renumbers every closure below it and a delta would rewrite unedited types. With the map installed, the compile replays the names the baseline allocated, so unchanged closures keep their names byte for byte. This mirrors how Roslyn preserves synthesized member names across generations.

Behavior without a map installed is unchanged: the accessor resolves once per CompilerGlobalState and each allocation takes the existing deterministic per-file path. A new emitted-metadata determinism guard plus the existing DeterministicTests suite (#19732) verify that. Everything added is internal; the surface area baseline is untouched.

Tests: 23 unit tests for the grammar and replay map (slot replay, snapshot canonicalization incl. mixed and gapped buckets, recorded-snapshot load, line-normalized pipe names), 1 no-map determinism guard, SurfaceAreaTest and DeterministicTests green.

Sequencing

This PR is part of splitting the F# hot reload work (#19941) into small, independently reviewable PRs. The planned order:

  1. Wave 1 (independent of each other, no cross dependencies): Add ResetCompilerGeneratedNameState to compiler-generated name generators #20017 (generated-name counter reset), Add Roslyn-format EnC CustomDebugInformation codec and portable PDB method CDI emission #20018 (EnC CustomDebugInformation codec and method CDI emission), Add ECMA-335 EnC metadata delta writer #20019 (EnC metadata delta writer), this PR, and the typed-tree differ with rude-edit classification.
  2. Wave 2: baseline reading and recorded CDI state (depends on Add Roslyn-format EnC CustomDebugInformation codec and portable PDB method CDI emission #20018 and this PR).
  3. Wave 3: the delta emitter and symbol matcher.
  4. Wave 4: the hot reload session, FCS surface, and the --test:HotReloadDeltas capture hook (F# hot reload: Edit-and-Continue delta emission behind --test:HotReloadDeltas #19941 in its final, much smaller form).
  5. Last, explicitly experimental: Add an experimental flag-gated in-process compile path for hot reload sessions #20031 (the flag-gated in-process compile perf path).

Role in the train: this is the naming foundation the baseline reader (wave 2) and delta emitter (wave 3) build on. Nothing on main calls the map yet; the session slice installs it.

Refresh status (2026-07-17)

  • Refreshed against dotnet/fsharp main at 5928e91; the current reviewed head is 3f6b342.
  • A dedicated review pass completed for this PR, its findings were fixed in the lowest owning slice, and the PR has no unresolved review threads.
  • The downstream session, in-process compiler, umbrella, and SDK branches were restacked after the fixes, so this slice remains part of the decomposed review train.
  • The complete compiler stack passed the 11-step hot reload verifier, 456 service tests, 243 component tests with 2 expected skips, and 1411 EmittedIL tests with 3 expected skips. Replacement CI passed on this exact head.

@github-actions

github-actions Bot commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

❗ Release notes required

You can open this PR in browser to add release notes: open in github.dev


✅ Found changes and release notes in following paths:

Warning

No PR link found in some release notes, please consider adding it.

Change path Release notes path Description
src/Compiler docs/release-notes/.FSharp.Compiler.Service/11.0.100.md No current pull request URL (#20024) found, please consider adding it

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.
@NatElkins
NatElkins force-pushed the hotreload-stable-names branch from 42d793f to 7ea9a1f Compare July 17, 2026 23:11
Verified with the repository-wide Fantomas check.
@NatElkins
NatElkins marked this pull request as ready for review July 18, 2026 02:19
@NatElkins
NatElkins requested a review from a team as a code owner July 18, 2026 02:19
@github-actions github-actions Bot added the ⚠️ Affects-Compiler-Output Tooling check: PR touches IL emission or codegen label Jul 21, 2026
@github-actions

Copy link
Copy Markdown
Contributor

🔍 Tooling Safety Check — Affects-Compiler-Output
Affects-Compiler-Output: modifies IlxGen.fs

Generated by PR Tooling Safety Check · opus46 5.4M ·

@T-Gro T-Gro left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 This review was generated by AI (@expert-reviewer agent). Findings may contain inaccuracies — please verify independently.

Overall this is careful, well-tested, well-documented infrastructure that is inert on the normal (no-map-installed) path — the added cost there is a single None check per generated name, and DeterministicTests plus the new guard cover regression. Two substantive notes below, both about the replay map's concurrency/determinism model rather than the untouched normal path.

member _.Snapshot: seq<struct (string * string[])> =
lock syncLock (fun () ->
[|
for KeyValue(key, bucket) in buckets do

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Snapshot yields buckets in ConcurrentDictionary enumeration order, which is not guaranteed to be stable across processes/runs. Within each bucket the order is deterministic (ResizeArray insertion order), but the top-level (key, names) grouping is not. For a feature whose whole premise is byte-for-byte stable output, any downstream consumer that persists or hashes/compares this sequence directly (baseline blob, snapshot diff) would see non-deterministic ordering unless it re-sorts by key. Recommend sorting by key with StringComparer.Ordinal here before returning so the snapshot is deterministic at the source rather than relying on every consumer to sort.

/// </summary>
type FSharpSynthesizedTypeMaps() =
let syncLock = obj ()
let buckets = ConcurrentDictionary<string, ResizeArray<string>>()

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

buckets and ordinals are ConcurrentDictionary, but every access to both goes through lock syncLock (GetOrAddName, BeginSession, Snapshot, loadSnapshotCore, UsesRecordedSnapshot). The concurrent collections' internal striped locking is therefore pure overhead and, more importantly, muddies the concurrency contract — a future reader may assume lock-free access is safe and add an unlocked read. Since correctness already depends entirely on syncLock, plain Dictionary would be clearer and cheaper. Not a correctness bug today; flagging to keep the intended single-lock model unambiguous.

@T-Gro
T-Gro self-requested a review July 21, 2026 10:58
@T-Gro T-Gro added the AI-reviewed PR reviewed by AI review council label Jul 21, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

⚠️ Affects-Compiler-Output Tooling check: PR touches IL emission or codegen AI-reviewed PR reviewed by AI review council

Projects

Status: New

Development

Successfully merging this pull request may close these issues.

2 participants