Repository navigation
Conversation
✅ Release notes checked
|
|
Looks like it requires rebase/conficts fix |
|
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:
Missing `F# hot reload session prestarted`After that I've tried to modify TLDR: not worksInfo:
dotnet --info |
|
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 I finally have managed to make it work, done some simple test on it, and found some problems:
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
member _.Message() =
count <- count + 1
task {
do! Task.Delay 2000
do! Task.Delay 2000 // add this line
}
member _.Message() =
count <- count + 1 // remove this line
task {
do! Task.Delay 2000
} |
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.
|
@ijklam got it right. The bridge ran a full The fix builds just the Compile target instead, so fsc refreshes the intermediate assembly under It's on @WizMe-M the missing On the other observations: editing the top level EDIT: Correcting myself on the struck-out line. @ijklam is right, C# hot reload does support adding an |
|
I think "adding or removing lines before a
By the way, C# hot reload supports this already. |
… 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.
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.
…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.
…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.
…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
left a comment
There was a problem hiding this comment.
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.
| 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 |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
Claude fix all such places
| capabilities |> Set.toList |> List.map (fun capability -> capability.Name) | |
| capabilities |> Set.map _.Name |> ImmutableArray.CreateRange |
| 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 | ||
| ] |
There was a problem hiding this comment.
I would prefer FrozenHashSet or ImmutableHashSet for better performance
| 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 |
| let ambiguousNames = | ||
| allMembers | ||
| |> List.choose (fun (symbol, _) -> symbol.CompiledName) | ||
| |> List.countBy id | ||
| |> List.filter (fun (_, count) -> count > 1) | ||
| |> List.map fst | ||
| |> Set.ofList |
There was a problem hiding this comment.
| 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 "_" |
There was a problem hiding this comment.
Claude, fix all such unnecessary materializations
| ordinalChain |> List.map string |> String.concat "_" | |
| ordinalChain |> Seq.map string |> String.concat "_" |
| |> 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) |
There was a problem hiding this comment.
| |> 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) |
| let format (diagnostics: RudeEditDiagnostic list) = | ||
| diagnostics | ||
| |> List.map (fun d -> $"{d.Id}: {d.Message}") | ||
| |> String.concat System.Environment.NewLine |
| Symbol: SymbolId | ||
| ContainingType: string | ||
| MemberKind: SymbolMemberKind | ||
| Method: MethodDefinitionKey option |
There was a problem hiding this comment.
| Method: MethodDefinitionKey option | |
| Method: MethodDefinitionKey voption |
|
I can create a PR from my fork if you ask |
|
@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 PR adds experimental F# hot reload behind
--test:HotReloadDeltas. It produces metadata, IL, and portable PDB deltas that a host applies throughMetadataUpdater.ApplyUpdate.An
ICompilerEmitHookcaptures 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
EncLogandEncMap, 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.CreateHotReloadSessioncreates 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.EmitDeltastages updates against that committed state. After the host applies the updates,Commitadvances all pending projects together.Discardpreserves 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:
FSHRDLdiagnostics for the host.--test:HotReloadClassStateMachinesopts into class lowering for supported resume-point edits.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.