dotnet-watch: F# hot reload integration (inspection draft)#1
Draft
NatElkins wants to merge 12 commits into
Draft
dotnet-watch: F# hot reload integration (inspection draft)#1NatElkins wants to merge 12 commits into
NatElkins wants to merge 12 commits into
Conversation
Mirror the C#/VB SupportsHotReload project capability for F# projects targeting .NET 6.0 or newer, with an opt-out via SupportsHotReload=false. Add coverage in GivenThatWeWantToBuildALibraryWithFSharp.
Add FSharpHotReloadService and FSharpProjectInfo, which bridge dotnet-watch to FSharp.Compiler.Service hot reload APIs (StartHotReloadSession, EmitHotReloadDelta, EndHotReloadSession) via reflection. The service discovers F# projects in the project graph, tracks per-project compiler inputs, and emits managed code deltas for changed F# sources, with an optional FSharpWorkspace snapshot bridge for custom compiler service builds.
Route file changes through FSharpHotReloadService alongside the Roslyn update path in CompilationHandler. Managed code updates are now tracked as ManagedCodeUpdateEnvelope (project path + runtime update) so that F# deltas, which have no Roslyn ProjectId, flow through the same builder, previous-update replay, and discard-on-rebuild logic as C#/VB updates. F# edits that cannot be applied as deltas fall back to rebuild + restart of the affected project. The agent reports trace-only diagnostics when no loaded assembly matches an update's module id and DOTNET_WATCH_TRACE_FSHARP_HOTRELOAD is set.
Add unit tests for FSharpHotReloadService change classification and the reflection host F# option accessor handling, and integration scenarios covering in-place apply, computation expression edits, whitespace-only edits, dependency and XAML dependency edits, and rude-edit restart fallback for F# projects.
Pass the same aggregate runtime edit-and-continue capabilities that the Roslyn hot reload service receives into FSharpHotReloadService, and probe the loaded FCS surface for the optional capabilities parameter on StartHotReloadSession via reflection. The capabilities are forwarded as FSharpOption<seq<string>> when the parameter exists and gracefully omitted for older FCS builds without it.
…t them The F# hot reload session is prestarted before any agent connects, freezing an empty capability set into the session and pinning classification to baseline-only edits. Update the live session via the compiler service's UpdateHotReloadCapabilities once the real aggregate set is available. Restarting the session instead is not an option: a restart re-captures the baseline from sources that may already contain the pending edit, producing an empty diff against the running module. Older compiler services without the update API keep the session's original capabilities.
When the loaded FSharp.Compiler.Service exposes FSharpChecker.CreateHotReloadSession, the watch bridge holds one FSharpHotReloadSession per watch session instead of switching the process-wide checker session between projects: - the session is created at StartSessionAsync with the current aggregate runtime capabilities and refreshed in place via the session's UpdateCapabilities; - every discovered F# project is captured into the session with AddProject(snapshot, outputPath) (eagerly at session start, lazily on first edit otherwise), so edits to F# library projects loaded into a running process now match and emit per-project deltas; - edits emit through the session's EmitDelta(snapshot); pending updates are committed at delta hand-off (the watch applies them immediately, mirroring Roslyn's CommitUpdate point) and discarded when hot reload is blocked or the restart prompt is declined; - EndSession disposes the session object. When CreateHotReloadSession is absent (older compiler service builds) the existing single-active-project switching path is kept unchanged as the fallback.
Two fixes surfaced by running the F# watch e2e suite against a compiler service with the session-object API: - Changed files owned exclusively by F# projects the session tracks are no longer surfaced to the Roslyn workspace. The Roslyn EnC service has no F# support, so it reported rude edit ENC1009 with a redundant auto-rebuild for every F# edit; when the F# emit was a no-op the empty Roslyn result swallowed the user-facing decision message and dotnet-watch reported nothing. Legacy single-session mode keeps the Roslyn auto-rebuild as the fallback for edits the single-active-project bridge cannot target. - The module id of the output captured by AddProject is recorded as the runtime target id for the project. Forced design-time rebuilds (after a no-op or dependency-only emit) move the on-disk MVID without the process reloading, which made the next delta target a module the runtime never loaded. A blocked emit keeps the recorded id (the loaded module is unchanged); a restart drops it so re-adding the project recaptures both. Also adds DOTNET_WATCH_FSHARP_USE_SESSION_OBJECT=0 as a safety valve forcing the legacy single-active-project path when the session-object API is present.
Adds the FSharpAppWithLib test asset (an F# App referencing an F# Lib, both compiled with --enable:hotreloaddeltas so baseline builds are deterministic) and an end-to-end watch test that interleaves edits across the two projects: edit Lib's method body (in-place apply targeting the Lib module loaded into the running App process), then App's body, then Lib again. The legacy single-active-session bridge could not interleave projects without recapturing baselines from already-edited sources; the session object keeps each project's committed baseline and generation chain independent. Verified locally: the new test passes, and the full F# watch test classes (FSharpHotReloadTests, FSharpHotReloadServiceTests, FSharpReflectionHostTests) pass 19/19 against an SDK layout provisioned with the hot-reload-v2 FSharp.Compiler.Service.
This was referenced Jun 12, 2026
Owner
Author
|
Compiler-side counterpart is now under upstream review at dotnet/fsharp#19941. This branch gained: a one-variable master kill switch (DOTNET_WATCH_FSHARP_HOTRELOAD=0), the SupportsHotReload capability now advertised only under dotnet-watch builds (no VS blast radius), and session-object-only FCS support (the bridge no longer requires the retired legacy checker surface — e2e suite 8/0 against current FCS). |
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.
dotnet-watch: F# hot reload integration (inspection draft)
The dotnet-watch half of F# hot reload, paired with the compiler work in
NatElkins/fsharp#3 (full implementation).
Not for upstream until the FCS API ships publicly — this draft exists for review and for
anyone who wants to run the end-to-end experience.
🚀 Try it
hot-reload-quickstart.md
— two clones, two builds, copy-paste steps, ending with
dotnet watch runhot-patching arunning F# console app (including adding lambdas to live methods). This branch builds the
complete .NET CLI (
artifacts/bin/redist/Debug/dotnet) that the guide uses.What this branch contains
FSharpHotReloadService+FSharpProjectInfo(src/Dotnet.Watch/Watch/HotReload/FSharp/):F# project discovery from the MSBuild graph, source/dependency change mapping, forced-build
orchestration, and a reflection bridge into FSharp.Compiler.Service — no compile-time FCS
dependency, graceful restart-fallback on stock toolsets
FSharpHotReloadSessionper watch run (the FCS-sideDebuggingSessionanalogue), every F# project added at prestart, per-project deltas withsolution-wide commit. Multi-project interleaved edits apply in place — e2e
ChangeFilesInFSharpAppAndLib_InterleavedEditsApplyInPlaceedits a referenced library,then the app, then the library again, all without restart. Legacy single-project path
preserved as the fallback for older FCS builds
(
DOTNET_WATCH_FSHARP_USE_SESSION_OBJECT=0kill switch)kept out of the Roslyn workspace (prevents spurious
ENC1009restarts); runtime EnCcapability forwarding with in-place refresh once agents connect (the session is prestarted
before the app launches)
SupportsHotReloadproject capability inMicrosoft.NET.Sdk.FSharp.targets(C#/VB parity)AddProjecttime: forced design-time rebuilds move the on-disk MVID;deltas must keep targeting the module the runtime actually loaded
DOTNET_WATCH_TRACE_FSHARP_HOTRELOAD=1Evidence
19/19 F# watch test classes (
FSharpHotReloadTests,FSharpHotReloadServiceTests,FSharpReflectionHostTests) including the multi-project e2e, no-op edit handling (norestart on whitespace saves), and dependency-edit handling — run against the locally built
FCS from
hot-reload-v2.Design docs
The architecture (session entity, snapshot contract, capability negotiation, determinism)
is documented on the compiler branch — start at
hot-reload-architecture.md.