Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 39 additions & 23 deletions src/Compiler/CodeGen/IlxDeltaEmitter.fs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ open FSharp.Compiler.HotReloadBaseline
open FSharp.Compiler.HotReloadPdb
open FSharp.Compiler.IlxDeltaStreams
open FSharp.Compiler.CodeGen.FSharpDefinitionIndex
open FSharp.Compiler.GeneratedNames
open FSharp.Compiler.SynthesizedTypeMaps
open FSharp.Compiler.Syntax.PrettyNaming
open FSharp.Compiler.TypedTreeDiff
Expand Down Expand Up @@ -3547,35 +3548,52 @@ let emitDeltaWithDebugData (freshDebugPdb: byte[] option) (request: IlxDeltaRequ
| [||] -> None
| [| single |] -> Some single
| matches ->
let isBaselineMatchAvailable matchedName =
match newTypeNameByBaseline.TryGetValue matchedName with
| true, existingNewName -> String.Equals(existingNewName, newFullName, StringComparison.Ordinal)
| false, _ -> true

let exactMatch =
matches
|> Array.tryFind (fun (matchedName, _) -> String.Equals(matchedName, newFullName, StringComparison.Ordinal))
|> Array.tryFind (fun (matchedName, _) ->
String.Equals(matchedName, newFullName, StringComparison.Ordinal)
&& isBaselineMatchAvailable matchedName)

match exactMatch with
| Some matchResult -> Some matchResult
| None ->
let normalizeTypePath (name: string) =
name.Split([| '.'; '+' |], StringSplitOptions.RemoveEmptyEntries)
|> String.concat "."

let normalizedTarget = normalizeTypePath newFullName
let normalizedTarget = normalizeTypePathName newFullName

let normalizedMatches =
matches
|> Array.filter (fun (matchedName, _) ->
String.Equals(normalizeTypePath matchedName, normalizedTarget, StringComparison.Ordinal))
String.Equals(normalizeTypePathName matchedName, normalizedTarget, StringComparison.Ordinal)
&& isBaselineMatchAvailable matchedName)

match normalizedMatches with
| [| normalizedMatch |] -> Some normalizedMatch
| _ ->
let matchedNames = matches |> Array.map fst |> String.concat "; "
let allCandidates = candidateNames |> String.concat "; "
let unassignedMatches =
matches
|> Array.filter (fun (matchedName, _) -> isBaselineMatchAvailable matchedName)

if IsCompilerGeneratedName typeDef.Name && not (Array.isEmpty unassignedMatches) then
unassignedMatches
|> Array.sortBy (fun (matchedName, _) ->
TryGetHotReloadReplayNameOrdinal matchedName
|> Option.defaultValue Int32.MaxValue,
matchedName)
|> Array.head
|> Some
else
let matchedNames = matches |> Array.map fst |> String.concat "; "
let allCandidates = candidateNames |> String.concat "; "

raise (
HotReloadUnsupportedEditException(
$"Ambiguous synthesized type mapping for '{newFullName}' (candidates=[{allCandidates}], baselineMatches=[{matchedNames}]); full rebuild required."
raise (
HotReloadUnsupportedEditException(
$"Ambiguous synthesized type mapping for '{newFullName}' (candidates=[{allCandidates}], baselineMatches=[{matchedNames}]); full rebuild required."
)
)
)

if traceSynthesizedMappings.Value then
match baselineNameOpt with
Expand Down Expand Up @@ -3692,19 +3710,17 @@ let emitDeltaWithDebugData (freshDebugPdb: byte[] option) (request: IlxDeltaRequ
addedTypeDefs.Add(enclosing, typeDef, fullName)
deltaToken
| None when typeDef.Name.Contains "@hotreload" ->
// A legacy (non-generation-suffixed) hot-reload closure name with no
// baseline counterpart: closure-chain CE lowerings (async) number
// their classes `-N` by emission order, so a structural CE change
// shifts every later name off its baseline row. Tokens looked up
// through the baseline mappings would be garbage; fail closed.
// A non-generation-suffixed hot-reload helper with no baseline counterpart can be
// regeneration noise from unchanged code in a full in-process emit. Do not allocate
// it as an added type. If updated IL actually references it, the later token remap
// still fails closed rather than emitting a bogus row.
let fullName =
(mkRefForNestedILTypeDef ILScopeRef.Local (enclosing, typeDef)).FullName

raise (
HotReloadUnsupportedEditException(
$"Computation-expression closure chain changed: synthesized type '{fullName}' has no baseline counterpart; the closure chain cannot be aligned with the baseline. Please rebuild."
)
)
if traceSynthesizedMappings.Value then
printfn "[fsharp-hotreload][synthesized-map] ignoring unmatched helper %s" fullName

0
| None -> request.Baseline.TokenMappings.TypeDefTokenMap(enclosing, typeDef)

addMapping typeTokenMap newTypeToken baselineTypeToken
Expand Down
13 changes: 11 additions & 2 deletions src/Compiler/HotReload/FSharpHotReloadSession.fs
Original file line number Diff line number Diff line change
Expand Up @@ -516,6 +516,7 @@ type FSharpHotReloadSession
(
hotReloadService: FSharpHotReloadService,
parseAndCheckSnapshot: FSharpProjectSnapshot -> string -> Async<FSharpCheckProjectResults>,
refreshOutputBeforeEmit: FSharpCheckProjectResults -> string option -> Async<unit>,
tryGetOutputPath: FSharpProjectSnapshot -> string option,
// Registers a successfully baselined project (resolved output path + project key) in
// the owning checker's live-session registry, which FSharpChecker.Compile consults to
Expand Down Expand Up @@ -645,6 +646,7 @@ type FSharpHotReloadSession
|]

let projectKey = projectKeyOfSnapshot projectSnapshot
let resolvedOutputPath = resolveOutputPath projectKey projectSnapshot

let currentTrackedInputs = computeTrackedInputs projectSnapshot

Expand All @@ -654,11 +656,18 @@ type FSharpHotReloadSession
| true, committed -> committed <> currentTrackedInputs
| false, _ -> false)

let parseCheckAndMaybeRefreshOutput () =
async {
let! projectResults = parseAndCheckSnapshot projectSnapshot opName
do! refreshOutputBeforeEmit projectResults resolvedOutputPath
return projectResults
}

let! result =
hotReloadService.EmitHotReloadDelta
projectKey
(fun () -> parseAndCheckSnapshot projectSnapshot opName)
(resolveOutputPath projectKey projectSnapshot)
parseCheckAndMaybeRefreshOutput
resolvedOutputPath
trackedInputsChanged

match result with
Expand Down
78 changes: 78 additions & 0 deletions src/Compiler/HotReload/HotReloadState.fs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
module internal FSharp.Compiler.HotReloadState

open System
open System.Collections.Generic
open System.IO
open FSharp.Compiler.CodeAnalysis
open FSharp.Compiler.EditAndContinue
open FSharp.Compiler.HotReloadBaseline
Expand Down Expand Up @@ -422,6 +424,82 @@ let setCurrentEmissionContext (context: HotReloadEmissionContext option) =
let tryGetCurrentEmissionContext () =
lock emissionContextLock (fun () -> currentEmissionContext)

let private replaceCurrentEmissionContext context =
lock emissionContextLock (fun () ->
let previous = currentEmissionContext
currentEmissionContext <- context
previous)

/// Runs synchronous work while the fsc emit hook observes the supplied emission context.
let withCurrentEmissionContext context action =
let previous = replaceCurrentEmissionContext (Some context)

try
action ()
finally
lock emissionContextLock (fun () -> currentEmissionContext <- previous)

/// Runs asynchronous work while the fsc emit hook observes the supplied emission context.
let withCurrentEmissionContextAsync context work =
async {
let previous = replaceCurrentEmissionContext (Some context)

try
return! work
finally
lock emissionContextLock (fun () -> currentEmissionContext <- previous)
}

/// Tracks the session-owned projects that can service an in-process compile by output path.
/// The owning checker keeps one registry and removes a session's entries when it is disposed.
type HotReloadEmissionTargetRegistry() =
let targets = ResizeArray<string * HotReloadSessionStore * HotReloadProjectKey>()
let gate = obj ()

let normalizeOutputPath (path: string) =
try
Path.GetFullPath(path)
with _ ->
path

let outputPathComparison =
if Path.DirectorySeparatorChar = '\\' then
StringComparison.OrdinalIgnoreCase
else
StringComparison.Ordinal

member _.Register(store: HotReloadSessionStore, outputPath: string, projectKey: HotReloadProjectKey) =
let normalized = normalizeOutputPath outputPath

lock gate (fun () ->
// A recapture of the same project in the same session replaces its entry.
targets.RemoveAll(fun (_, existingStore, existingKey) ->
obj.ReferenceEquals(existingStore, store) && existingKey = projectKey)
|> ignore

// Most recent first: if two live sessions track the same output, the newest
// baseline is the one this checker should use for the next in-process compile.
targets.Insert(0, (normalized, store, projectKey)))

member _.UnregisterStore(store: HotReloadSessionStore) =
lock gate (fun () ->
targets.RemoveAll(fun (_, existingStore, _) -> obj.ReferenceEquals(existingStore, store))
|> ignore)

member _.TryResolve(outputPath: string option) =
match outputPath with
| None -> None
| Some outputPath ->
let target = normalizeOutputPath outputPath

lock gate (fun () ->
targets
|> Seq.tryPick (fun (registeredPath, store, projectKey) ->
if String.Equals(registeredPath, target, outputPathComparison) then
Some { Store = store; ProjectKey = projectKey }
else
None))

let private activeSessionStore = HotReloadSessionStore()

/// The process-local store identity-less callers operate on: the fsc emit hook when no scoped
Expand Down
Loading