Skip to content

F# hot reload: Edit-and-Continue delta emission behind --test:HotReloadDeltas - #19941

Open
NatElkins wants to merge 500 commits into
dotnet:mainfrom
NatElkins:hot-reload-v2
Open

NatElkins wants to merge 500 commits into
dotnet:mainfrom
NatElkins:hot-reload-v2

Conversation

@NatElkins

@NatElkins NatElkins commented Jun 12, 2026 •

Copy link
Copy Markdown
Contributor

This PR adds experimental F# hot reload behind --test:HotReloadDeltas. It produces metadata, IL, and portable PDB deltas that a host applies through MetadataUpdater.ApplyUpdate.

An ICompilerEmitHook captures compiler and debug information at emission boundaries. Hot reload compiles use deterministic output and sequential code generation so synthesized names and metadata layout remain reproducible. Disk sessions read the original DLL and PDB, retaining the loaded module's metadata row identities and heap sizes. Normal compilation does not install this hook or retain baseline state.

The typed-tree differ compares declarations, exact body identities, signatures, and metadata against committed snapshots. It classifies edits against the target runtime's capabilities before emission. Supported edits include method bodies, member and type additions, closures, generic methods, parameter renames, and selected attribute changes. Each edit must also satisfy the compiler's layout and lowering constraints.

The emitter matches definitions to the baseline, reuses existing tokens, and remaps references in changed bodies and signatures. It appends required rows and heaps, records edits in EncLog and EncMap, and chains generation identifiers for subsequent updates. Metadata and PDB encodings follow Roslyn's Edit-and-Continue conventions.

Structural occurrence keys and PDB debug maps preserve closure identities across recompilations. Compatible occurrences reuse names, while new occurrences receive generation-specific names. State-machine maps preserve supported resume points and reject incompatible edits. The closure mapping design explains the relationship between source occurrences, generated names, and metadata identities.

FSharpChecker.CreateHotReloadSession creates independent sessions with per-project state keyed by project and output paths. Each project retains committed typed trees, an emission baseline, and its generation chain. EmitDelta stages updates against that committed state. After the host applies the updates, Commit advances all pending projects together. Discard preserves the committed state so the next attempt compares against the same baseline. Tracked inputs and output fingerprints prevent stale build artifacts from becoming a new baseline.

Sequence-point analysis can report line-only updates without new method bodies or a metadata generation. These updates also participate in the session's commit and discard lifecycle. Active-statement remapping uses host-supplied debugger information and rejects ambiguous or unsafe mappings. The compiler exposes these contracts, but debugger integration remains the host's responsibility.

An optional in-process path recompiles checked trees and can reuse a validated optimizer prefix. It preserves file order and threaded compiler state, with full optimization as the fallback if incremental execution fails.

The feature has explicit limits:

  • Baseline builds require debug information and disabled optimization. The compiler options and runtime must support hot reload.
  • Signature changes, unsafe layout changes, and edits to field-backed module initializers require a rebuild. Unsupported edits return FSHRDL diagnostics for the host.
  • Ordinary struct state machines reject incompatible resume-point changes. --test:HotReloadClassStateMachines opts into class lowering for supported resume-point edits.
  • Compiler-side delta support does not add official SDK watch or debugger integration. The standalone fsharp-watch host supplies separate watch integration.

The tests compare metadata and PDB structures with Roslyn reference deltas and inspect complete metadata output through mdv. Runtime tests apply successive generations and verify behavior and retained state. Additional coverage checks session isolation, commit and discard behavior, deterministic output, and normal compilation with hot reload disabled.

This umbrella contains the implementation slices in #20025, #20027, #20030, and #20031. Those PRs separate typed-tree classification, delta emission, session APIs, and optional in-process compilation for review. The capability model describes runtime restrictions and unsupported edit classes.

@github-actions

github-actions Bot commented Jun 12, 2026 •

Copy link
Copy Markdown
Contributor

✅ Release notes checked


✅ Found changes and release notes in following paths:

Change path Release notes path Description
`src/Compiler` docs/release-notes/.FSharp.Compiler.Service/11.0.200.md

@NatElkins NatElkins mentioned this pull request Jun 12, 2026
@xperiandri

Copy link
Copy Markdown
Contributor

Looks like it requires rebase/conficts fix

Comment thread src/Compiler/Driver/fsc.fs Resolved
Comment thread src/Compiler/TypedTree/TypedTreeDiff.fs Resolved
Comment thread src/Compiler/CodeGen/HotReloadPdb.fs Resolved
Comment thread src/Compiler/Service/service.fs Resolved
@WizMe-M

WizMe-M commented Jun 21, 2026 •

Copy link
Copy Markdown

Hi, I gave a try to hot-reload demo. I followed steps 1-5 and failed. What am I doing wrong?

First of all step 4 says:

Run it:
dotnet watch run --non-interactive
You should see ⌚ F# hot reload session prestarted, then the counter ticking once a second.
But my output was:

Missing `F# hot reload session prestarted`
HotReloadDemo> dotnet watch run --non-interactive
dotnet watch 🔥 Hot reload enabled. For a list of supported edits, see https://aka.ms/dotnet/hot-reload.
dotnet watch 💡 Press Ctrl+R to restart.
Restore complete (0,4s)
    info NETSDK1057: You are using a preview version of .NET. See: https://aka.ms/dotnet-support-policy
  HotReloadDemo net11.0 succeeded (2,7s) → bin\Debug\net11.0\HotReloadDemo.dll

Build succeeded in 3,7s
dotnet watch ⌚ Loading projects ...
dotnet watch ⌚ Loaded 1 project(s) in 0,4s.
dotnet watch ⌚ Waiting for changes
hello (count: 1) <--------------------------- missing 'F# hot reload session prestarted'
Looks like SDK/F#-compiler with hot-reload wasn't built.

After that I've tried to modify Program.fs and got expected result (TLDR: not works):

TLDR: not works
HotReloadDemo> dotnet watch run --non-interactive
dotnet watch 🔥 Hot reload enabled. For a list of supported edits, see https://aka.ms/dotnet/hot-reload.
dotnet watch 💡 Press Ctrl+R to restart.
Restore complete (0,4s)
    info NETSDK1057: You are using a preview version of .NET. See: https://aka.ms/dotnet-support-policy
  HotReloadDemo net11.0 succeeded (2,7s) → bin\Debug\net11.0\HotReloadDemo.dll

Build succeeded in 3,7s
dotnet watch ⌚ Loading projects ...
dotnet watch ⌚ Loaded 1 project(s) in 0,4s.
dotnet watch ⌚ Waiting for changes
hello (count: 1)
...
hello (count: 32)
dotnet watch ⌚ File updated: .\Program.fs
hello (count: 33)
...
hello (count: 56)
dotnet watch ⌚ File updated: .\Program.fs
hello (count: 57)
...
hello (count: 68)
dotnet watch 🔄 Restart requested. <------------------- ctrl+R
hello (count: 69)
hello (count: 70)
hello (count: 71)
dotnet watch ⌚ [HotReloadDemo (net11.0)] Exited
dotnet watch 🔄 Restarting.
Restore complete (0,5s)
    info NETSDK1057: You are using a preview version of .NET. See: https://aka.ms/dotnet-support-policy
  HotReloadDemo net11.0 succeeded (0,2s) → bin\Debug\net11.0\HotReloadDemo.dll

Build succeeded in 1,3s
dotnet watch ⌚ Loading projects ...
dotnet watch ⌚ Loaded 1 project(s) in 0,1s.
dotnet watch ⌚ Waiting for changes
HOT-RELOAD works (count: 1)   <------------------- only after ctrl+R
HOT-RELOAD works (count: 2)

Info:

  • OS: Windows 11 Pro (ran all steps from Powershell)
  • All $env variables were set correcly following manual
  • F#-compiler was built with no errors and warnings
  • SDK was built on third try with no errors and warnings
dotnet --info
HotReloadDemo> dotnet --info
.NET SDK:
 Version:           11.0.100-dev
 Commit:            ab1a0a0dbb
 Workload version:  11.0.100-manifests.844758e0
 MSBuild version:   18.8.0-preview-26277-111+6ca055abb

Runtime Environment:
 OS Name:     Windows
 OS Version:  10.0.26200
 OS Platform: Windows
 RID:         win-x64
 Base Path:   ~\sdk-hotreload\artifacts\bin\redist\Debug\dotnet\sdk\11.0.100-dev\

.NET workloads installed:
 [maui-windows]
   Installation Source: VS 17.14.37328.6
   Manifest Version:    11.0.0-preview.1.26102.3/11.0.100-preview.1
   Manifest Path:       ~\sdk-hotreload\artifacts\bin\redist\Debug\dotnet\sdk-manifests\11.0.100-preview.1\microsoft.net.sdk.maui\11.0.0-preview.1.26102.3\WorkloadManifest.json
   Install Type:        FileBased

 [maccatalyst]
   Installation Source: VS 17.14.37328.6
   Manifest Version:    26.2.11310-net11-p1/11.0.100-preview.1
   Manifest Path:       ~\sdk-hotreload\artifacts\bin\redist\Debug\dotnet\sdk-manifests\11.0.100-preview.1\microsoft.net.sdk.maccatalyst\26.2.11310-net11-p1\WorkloadManifest.json
   Install Type:        FileBased

 [ios]
   Installation Source: VS 17.14.37328.6
   Manifest Version:    26.2.11310-net11-p1/11.0.100-preview.1
   Manifest Path:       ~\sdk-hotreload\artifacts\bin\redist\Debug\dotnet\sdk-manifests\11.0.100-preview.1\microsoft.net.sdk.ios\26.2.11310-net11-p1\WorkloadManifest.json
   Install Type:        FileBased

 [android]
   Installation Source: VS 17.14.37328.6
   Manifest Version:    36.1.99-preview.1.119/11.0.100-preview.1
   Manifest Path:       ~\sdk-hotreload\artifacts\bin\redist\Debug\dotnet\sdk-manifests\11.0.100-preview.1\microsoft.net.sdk.android\36.1.99-preview.1.119\WorkloadManifest.json
   Install Type:        FileBased

Configured to use workload sets when installing new manifests.
No workload sets are installed. Run "dotnet workload restore" to install a workload set.

Host:
  Version:      11.0.0-preview.6.26277.111
  Architecture: x64
  Commit:       6ca055abbe

.NET SDKs installed:
  11.0.100-dev [~\sdk-hotreload\artifacts\bin\redist\Debug\dotnet\sdk]

.NET runtimes installed:
  Microsoft.AspNetCore.App 11.0.0-preview.5.26227.104 [~\sdk-hotreload\artifacts\bin\redist\Debug\dotnet\shared\Microsoft.AspNetCore.App]
  Microsoft.AspNetCore.App 11.0.0-preview.6.26277.111 [~\sdk-hotreload\artifacts\bin\redist\Debug\dotnet\shared\Microsoft.AspNetCore.App]
  Microsoft.NETCore.App 6.0.36 [~\sdk-hotreload\artifacts\bin\redist\Debug\dotnet\shared\Microsoft.NETCore.App]
  Microsoft.NETCore.App 7.0.20 [~\sdk-hotreload\artifacts\bin\redist\Debug\dotnet\shared\Microsoft.NETCore.App]
  Microsoft.NETCore.App 8.0.28 [~\sdk-hotreload\artifacts\bin\redist\Debug\dotnet\shared\Microsoft.NETCore.App]
  Microsoft.NETCore.App 9.0.17 [~\sdk-hotreload\artifacts\bin\redist\Debug\dotnet\shared\Microsoft.NETCore.App]
  Microsoft.NETCore.App 10.0.9 [~\sdk-hotreload\artifacts\bin\redist\Debug\dotnet\shared\Microsoft.NETCore.App]
  Microsoft.NETCore.App 11.0.0-preview.5.26227.104 [~\sdk-hotreload\artifacts\bin\redist\Debug\dotnet\shared\Microsoft.NETCore.App]
  Microsoft.NETCore.App 11.0.0-preview.6.26277.111 [~\sdk-hotreload\artifacts\bin\redist\Debug\dotnet\shared\Microsoft.NETCore.App]
  Microsoft.WindowsDesktop.App 11.0.0-preview.5.26227.104 [~\sdk-hotreload\artifacts\bin\redist\Debug\dotnet\shared\Microsoft.WindowsDesktop.App]
  Microsoft.WindowsDesktop.App 11.0.0-preview.6.26277.111 [~\sdk-hotreload\artifacts\bin\redist\Debug\dotnet\shared\Microsoft.WindowsDesktop.App]

Other architectures found:
  x86   [C:\Program Files (x86)\dotnet]
    registered at [HKLM\SOFTWARE\dotnet\Setup\InstalledVersions\x86\InstallLocation]

Environment variables:
  DOTNET_MSBUILD_SDK_RESOLVER_CLI_DIR      [~/sdk-hotreload/artifacts/bin/redist/Debug/dotnet]
  DOTNET_MULTILEVEL_LOOKUP                 [0]
  DOTNET_ROLL_FORWARD_TO_PRERELEASE        [1]
  DOTNET_ROOT                              [~/sdk-hotreload/artifacts/bin/redist/Debug/dotnet]

global.json file:
  Not found

Learn more:
  https://aka.ms/dotnet/info

Download .NET:
  https://aka.ms/dotnet/download

@ijklam

ijklam commented Jun 21, 2026 •

Copy link
Copy Markdown
Contributor

After spending one or two hours debugging this, I found why this cannot work on Windows. The issue is that on the Windows, when the program is running, the program file will be locked. As a result, the "dotnet build" executed by the dotnet-watch's FSharpHotReloadService fails, cause the hot reload cannot work.

I finally have managed to make it work, done some simple test on it, and found some problems:

  1. It seems working only when editing the content in the method body.
type Greeter() =
    let mutable count = 0

    member _.Message() =
        count <- count + 1  // Modifying this number is OK
        task {
           do! Task.Delay 2000 // Modifying this number does not take effect until restart
        }
        |> _.GetAwaiter().GetResult()
        sprintf "hello (count: %d)" count  // Modifying this string is OK

let greeter = Greeter()

while true do
    printfn "%s" (greeter.Message())   // Modifying this string doesn't take effect, until restart the program
    System.Threading.Thread.Sleep(1000)   // Changing this 1000 to 2000 doesn't take effect, until restart the program
  1. Simply adding a do! ... in a task block will need to restart the program.
    member _.Message() =
        count <- count + 1
        task {
          do! Task.Delay 2000
          do! Task.Delay 2000  // add this line
        }
  1. Adding or removing lines around a task needs restart program.
    member _.Message() =
        count <- count + 1  // remove this line
        task {
          do! Task.Delay 2000
        }

NatElkins added a commit to NatElkins/sdk that referenced this pull request Jun 22, 2026
The per-edit dotnet build refreshed the bin output the running process has
loaded. Windows locks that file against writes while the app runs, so the
build failed on every edit and the change fell back to a full restart instead
of applying in place.

Build the Compile target only (the obj intermediate assembly fsc writes) and
point the hot reload session's baseline and emit reads at that intermediate
assembly via FSharpProjectInfo.IntermediateAssemblyPath. The running process
never loads that file, so nothing is locked, and the bin output is left at
generation 0 until the next real restart. macOS and Linux already tolerated
overwriting a mapped file; this lines all three platforms up on the same path.

Reported on dotnet/fsharp#19941.
@NatElkins

NatElkins commented Jun 22, 2026 •

Copy link
Copy Markdown
Contributor Author

@ijklam got it right. The bridge ran a full dotnet build on every edit, and the last step of that build copies the assembly into bin, which is the file the running app has loaded. Windows keeps that locked while the process is alive, so the copy failed, the build failed with it, and the edit fell back to a restart. macOS and Linux let you replace a file that's mapped into a live process, so it only bit on Windows.

The fix builds just the Compile target instead, so fsc refreshes the intermediate assembly under obj and nothing writes to the loaded bin copy. The session reads the delta from that intermediate assembly now. bin stays put until the next real restart, and all three platforms take the same path.

It's on fsharp-hotreload-watch-v2 (fcf9b49). Only the dotnet-watch side changed, so you just need to pull that branch and rebuild the sdk clone, no need to rebuild the compiler or recopy the FCS dll. I don't have a Windows machine handy at the moment, so if one of you can confirm it works there now that would help a lot.

@WizMe-M the missing F# hot reload session prestarted line was a red herring. It only prints with DOTNET_WATCH_TRACE_FSHARP_HOTRELOAD=1 set, so not seeing it didn't mean anything was broken. I've reworded the quickstart to say that, and to point at the counter instead (it keeps climbing across edits when reloads land, and resets to 1 on a restart).

On the other observations: editing the top level while loop, or adding a do! to a task block, or moving lines around one, are mostly the expected limits rather than the Windows bug. The running loop is the active frame, so it can't be swapped while it's sitting on the stack, and inserting an await into a state machine is a rude edit in C# too. The count and string edits applying in place is exactly the case that should work. If the line-shift-around-a-task case still misbehaves after the rebuild, ping me, that one is worth a second look.

EDIT: Correcting myself on the struck-out line. @ijklam is right, C# hot reload does support adding an await/do! into a state machine, I read the EnC rules wrong. Roslyn treats it as a normal method update (gated on the runtime's AddInstanceFieldToExistingType capability), and only makes it a rude edit when the method is suspended at an active statement at the moment you save. So adding a do! to a task is a genuine parity gap on the F# side today, not an expected limit. I'm going to look into closing it.

@ijklam

ijklam commented Jun 22, 2026

Copy link
Copy Markdown
Contributor

I think "adding or removing lines before a task block needs a restart" is something worth to be solved, if we hope the feature can help us in the routine coding, as there may be many task in a file, to make the program running asynchronously.

inserting an await into a state machine is a rude edit in C# too

By the way, C# hot reload supports this already.

@NatElkins NatElkins changed the title F# hot reload: Edit-and-Continue delta emission behind --enable:hotreloaddeltas F# hot reload: Edit-and-Continue delta emission behind --test:HotReloadDeltas Jun 23, 2026
NatElkins added a commit to NatElkins/fsharp that referenced this pull request Jun 23, 2026
… identity

The resumable-code/trait shape digest rendered builder-call type
instantiations through TType.ToString() (tyToString), whose depth-limited
LimitedToString(4) collapses a deeply-solved typar to the literal "True".
That made the digest non-injective: a `task` whose return type is `int`
both sides could render Bind<int,int,int> before an edit and
Bind<int,True,True> after, producing a false StateMachineShapeChange
(FSHRDL013) rude edit.

Render the digest through tryTypeIdentityFromTType -- the same injective
runtime-identity encoder the capture path already stores -- threading the
method's typar->ordinal map into collectLoweredShapeInfo and
traitConstraintShapeDigest, with a structured formatter and a display-string
fallback only for types the encoder cannot represent. Update the architecture
guard for the new traitConstraintShapeDigest signature.

Addresses review feedback on dotnet#19941.
NatElkins added a commit to NatElkins/fsharp that referenced this pull request Jun 23, 2026
FSharpEditAndContinueLanguageService.UpdateActiveStatements had no callers:
the live path is SetActiveStatements (FSharpHotReloadSession.SetActiveStatements
-> SetSessionActiveStatements -> editAndContinueService.SetActiveStatements).
Remove the dead member and its now-orphaned HotReloadSessionStore.UpdateActiveStatements
backing, whose only caller was the dead forwarder.

Addresses review feedback on dotnet#19941.
Comment thread src/Compiler/FSComp.txt Resolved
Comment thread src/Compiler/Driver/CompilerConfig.fs Resolved
NatElkins added a commit to NatElkins/fsharp that referenced this pull request Jun 23, 2026
…error channel

Rude-edit reasons were flattened to a single string at the point of failure
(UnsupportedEdit of string), discarding the per-edit Id, Severity and symbol
before they reached the public API and the dotnet-watch bridge. Carry them
structurally instead:

* RudeEditDiagnostic gains a Severity (FSharpDiagnosticSeverity). Every kind is
  Error today; severityOf is the single place to introduce a non-blocking
  Warning later (e.g. a "might not take effect" edit).
* HotReloadError.UnsupportedEdit and the public FSharpHotReloadError.UnsupportedEdit
  now carry a list of structured diagnostics. A new public FSharpHotReloadRudeEdit
  record exposes Id, Severity, Message and SymbolName.
* The active-statement, deleted-symbol, mapping-error and emit-exception paths
  wrap their ad-hoc reason via RudeEditDiagnostics.unsupported so every error
  still carries an id.

The id namespace is unchanged and still owned solely by
RudeEditDiagnostics.diagnosticId, so the channel itself is id-agnostic.

Addresses review feedback on dotnet#19941.
NatElkins added a commit to NatElkins/fsharp that referenced this pull request Jun 23, 2026
…he swap point

Document, at RudeEditDiagnostics.diagnosticId (the single place the rude-edit id
namespace is decided), why F# keeps its own FSHRDL* codes rather than emitting
Roslyn's ENC* codes, and note the closest ENC analogs. Kept as a separate commit
from the channel work so aligning with the ENC codes later is an isolated change.

Addresses review feedback on dotnet#19941.
NatElkins added a commit to NatElkins/sdk that referenced this pull request Jun 23, 2026
…t-watch

The F# bridge collapsed the rude-edit reason to a record/DU ToString() dump and
logged it at Debug only, so an edit that forces a restart gave the user no reason.
Now that FSharpHotReloadError.UnsupportedEdit carries a structured rude-edit list
(Id + Severity + Message), read it via reflection (TryFormatRudeEdits) into a clean
"{Id}: {Message}" reason and report it as a warning instead of at Debug. The
extraction is defensive: any shape mismatch falls back to ToString(), so the bridge
stays correct against older FCS builds (where UnsupportedEdit still carries a string).

Pairs with the FCS-side structured diagnostics channel on dotnet/fsharp#19941.
Addresses review feedback on #1.
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.
Build the ordered output list directly from the completed cache steps. Preserve prefix reuse, threaded optimizer state, and atomic cache publication.

Validation: service test project build passed with no warnings or errors. Eleven session, byte-equivalence, and fallback tests passed. Scoped Fantomas and git diff checks passed.

@xperiandri xperiandri left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I would ask Claude to refactor this implementaiton by applying the instructions from original repo main. We intentionally added them to guide agents how to write modern F# 10 performant code instead of F# 6 code.
Also I would prefer using System.Collections.Immutable unless some specific functions from List module or pattern matching is required.

Comment on lines +5 to +11
let isEnvVarTruthy (name: string) =
match Environment.GetEnvironmentVariable(name) with
| null
| "" -> false
| value when String.Equals(value, "1", StringComparison.OrdinalIgnoreCase) -> true
| value when String.Equals(value, "true", StringComparison.OrdinalIgnoreCase) -> true
| _ -> false

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I thought something like this already exists in the codebase. Or was it moved here?


/// <summary>The names of the negotiated capabilities, in stable order, for tracing and diagnostics.</summary>
member _.CapabilityNames =
capabilities |> Set.toList |> List.map (fun capability -> capability.Name)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Claude fix all such places

Suggested change
capabilities |> Set.toList |> List.map (fun capability -> capability.Name)
capabilities |> Set.map _.Name |> ImmutableArray.CreateRange

Comment on lines +71 to +85
Set.ofList
[
EditAndContinueCapability.Baseline
EditAndContinueCapability.AddMethodToExistingType
EditAndContinueCapability.AddStaticFieldToExistingType
EditAndContinueCapability.AddInstanceFieldToExistingType
EditAndContinueCapability.NewTypeDefinition
EditAndContinueCapability.ChangeCustomAttributes
EditAndContinueCapability.UpdateParameters
EditAndContinueCapability.GenericAddMethodToExistingType
EditAndContinueCapability.GenericUpdateMethod
EditAndContinueCapability.GenericAddFieldToExistingType
EditAndContinueCapability.AddExplicitInterfaceImplementation
EditAndContinueCapability.AddFieldRva
]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I would prefer FrozenHashSet or ImmutableHashSet for better performance

Suggested change
Set.ofList
[
EditAndContinueCapability.Baseline
EditAndContinueCapability.AddMethodToExistingType
EditAndContinueCapability.AddStaticFieldToExistingType
EditAndContinueCapability.AddInstanceFieldToExistingType
EditAndContinueCapability.NewTypeDefinition
EditAndContinueCapability.ChangeCustomAttributes
EditAndContinueCapability.UpdateParameters
EditAndContinueCapability.GenericAddMethodToExistingType
EditAndContinueCapability.GenericUpdateMethod
EditAndContinueCapability.GenericAddFieldToExistingType
EditAndContinueCapability.AddExplicitInterfaceImplementation
EditAndContinueCapability.AddFieldRva
]
seq {
EditAndContinueCapability.Baseline
EditAndContinueCapability.AddMethodToExistingType
EditAndContinueCapability.AddStaticFieldToExistingType
EditAndContinueCapability.AddInstanceFieldToExistingType
EditAndContinueCapability.NewTypeDefinition
EditAndContinueCapability.ChangeCustomAttributes
EditAndContinueCapability.UpdateParameters
EditAndContinueCapability.GenericAddMethodToExistingType
EditAndContinueCapability.GenericUpdateMethod
EditAndContinueCapability.GenericAddFieldToExistingType
EditAndContinueCapability.AddExplicitInterfaceImplementation
EditAndContinueCapability.AddFieldRva
}
|> FrozenHashSet.CreateRange

Comment on lines +262 to +268
let ambiguousNames =
allMembers
|> List.choose (fun (symbol, _) -> symbol.CompiledName)
|> List.countBy id
|> List.filter (fun (_, count) -> count > 1)
|> List.map fst
|> Set.ofList

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggested change
let ambiguousNames =
allMembers
|> List.choose (fun (symbol, _) -> symbol.CompiledName)
|> List.countBy id
|> List.filter (fun (_, count) -> count > 1)
|> List.map fst
|> Set.ofList
let ambiguousNames =
allMembers
|> Seq.choose (fun (symbol, _) -> symbol.CompiledName)
|> Seq.countBy id
|> Seq.filter (fun (_, count) -> count > 1)
|> Seq.map fst
|> ImmutableHashSet.CreateRange

/// is unambiguous.
/// </summary>
let formatOccurrenceChainKey (ordinalChain: int list) =
ordinalChain |> List.map string |> String.concat "_"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Claude, fix all such unnecessary materializations

Suggested change
ordinalChain |> List.map string |> String.concat "_"
ordinalChain |> Seq.map string |> String.concat "_"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Use ValueOption

Comment thread src/Compiler/HotReload/SymbolMatcher.fs Outdated
|> List.iter (fun methodDef -> addMethodMatch typeRef enclosing typeDef methodDef methods)

typeDef.NestedTypes.AsList()
|> List.iter (fun nested -> addTypeMatches synthesizedBuckets (enclosing @ [ typeDef ]) types methods (depth + 1) nested)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggested change
|> List.iter (fun nested -> addTypeMatches synthesizedBuckets (enclosing @ [ typeDef ]) types methods (depth + 1) nested)
|> List.iter (fun nested -> addTypeMatches synthesizedBuckets ([ yield! enclosing; yield typeDef ]) types methods (depth + 1) nested)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Use ValueOption

Comment on lines +128 to +131
let format (diagnostics: RudeEditDiagnostic list) =
diagnostics
|> List.map (fun d -> $"{d.Id}: {d.Message}")
|> String.concat System.Environment.NewLine

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Use StringBuilder

Symbol: SymbolId
ContainingType: string
MemberKind: SymbolMemberKind
Method: MethodDefinitionKey option

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggested change
Method: MethodDefinitionKey option
Method: MethodDefinitionKey voption

@github-project-automation github-project-automation Bot moved this from New to In Progress in F# Compiler and Tooling Oct 30, 2026
@xperiandri

Copy link
Copy Markdown
Contributor

I can create a PR from my fork if you ask

@NatElkins

NatElkins commented Oct 30, 2026 •

Copy link
Copy Markdown
Contributor Author

@xperiandri This is the PR that has the full implementation of hot reload in case someone wanted to see it all in one place, but I've been splitting out the parts into other PRs. Thanks for the feedback though, I will apply it to this PR and its constituent PRs (along with using the AI repo guidance).

The constituent PRs are:

This is just for the compiler side, there's also some SDK changes that need to get in, but we're waiting on some decisions to be made. In the meantime, I made a fork of the SDK and published a dotnet tool that can be used once these changes land in the compiler but while we're still waiting for SDK support. Fork here.

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-Bootstrap Tooling check: PR touches compiler bootstrap chain ⚠️ Affects-Build-Infra Tooling check: PR touches build infrastructure ⚠️ Affects-Compiler-Output Tooling check: PR touches IL emission or codegen ⚠️ Affects-Test-Tooling Tooling check: PR touches test framework infrastructure

Projects

Status: In Progress

Development

Successfully merging this pull request may close these issues.

5 participants