Skip to content
Draft
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
1 change: 1 addition & 0 deletions docs/release-notes/.FSharp.Compiler.Service/11.0.100.md
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@
* Debug: fix if and match condition sequence points ([PR #19932](https://github.com/dotnet/fsharp/pull/19932))
* Checker: recover on checking language version ([PR ##19970](https://github.com/dotnet/fsharp/pull/19970))
* Implied argument names for function-to-delegate coercions now fall back to the delegate's `Invoke` parameter names when the function has no recoverable names (e.g. a partial application like `System.Func<int, int>((+) 1)`), instead of synthetic `delegateArg0`, `delegateArg1`, … names. ([PR #20001](https://github.com/dotnet/fsharp/pull/20001))
* Add internal `ResetCompilerGeneratedNameState` to `CompilerGlobalState` name generators so warm-checker re-compilation can produce fresh-process-identical generated names. ([PR #20017](https://github.com/dotnet/fsharp/pull/20017))

### Improved

Expand Down
20 changes: 20 additions & 0 deletions src/Compiler/TypedTree/CompilerGlobalState.fs
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,11 @@ type NiceNameGenerator() =
let count = incrementBucket basicName scopeFileIndex
mkName basicName m count

/// Reset the per-(basicName, file) occurrence counters so a subsequent codegen run assigns the
/// same compiler-generated occurrence names a fresh process would. Callers must ensure no
/// concurrent codegen is using this generator when resetting.
member _.ResetCompilerGeneratedNameState() = basicNameCounts.Clear()

/// Generates compiler-generated names marked up with a source code location, but if given the same unique value then
/// return precisely the same name. Each name generated also includes the StartLine number of the range passed in
/// at the point of first generation.
Expand All @@ -65,6 +70,12 @@ type StableNiceNameGenerator() =
lazy innerGenerator.FreshCompilerGeneratedNameOfBasicName(basicName, m))
lazyName.Value

/// Reset the stable-name cache and inner occurrence counters, so both the cached stable names and
/// the underlying occurrence counters are cleared. See NiceNameGenerator.ResetCompilerGeneratedNameState.
member _.ResetCompilerGeneratedNameState() =
niceNames.Clear()
innerGenerator.ResetCompilerGeneratedNameState()

[<Sealed>]
type PerFileNamingScope internal (nng: NiceNameGenerator, fileIndex: int) =

Expand All @@ -90,6 +101,15 @@ type internal CompilerGlobalState () =
member _.NewFileScope (fileRange: range) =
PerFileNamingScope(globalNng, fileRange.FileIndex)

/// Reset all compiler-generated-name occurrence counters on this state, so successive in-process
/// codegen runs over the same source produce identical generated names (a fresh-process layout).
/// Callers must ensure no compilation is concurrently generating names (quiescence). Needed by
/// Edit-and-Continue style scenarios that re-emit from a warm checker.
member _.ResetCompilerGeneratedNameState() =
globalNng.ResetCompilerGeneratedNameState()
globalStableNameGenerator.ResetCompilerGeneratedNameState()
ilxgenGlobalNng.ResetCompilerGeneratedNameState()

/// Unique name generator for stamps attached to lambdas and object expressions
type Unique = int64

Expand Down
15 changes: 15 additions & 0 deletions src/Compiler/TypedTree/CompilerGlobalState.fsi
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@ type NiceNameGenerator =
member FreshCompilerGeneratedName: name: string * m: range -> string
member IncrementOnly: name: string * m: range -> int

/// Reset the per-(basicName, file) occurrence counters so a subsequent codegen run assigns the
/// same compiler-generated occurrence names a fresh process would. Callers must ensure no
/// concurrent codegen is using this generator when resetting.
member ResetCompilerGeneratedNameState: unit -> unit

/// Generates compiler-generated names marked up with a source code location, but if given the same unique value then
/// return precisely the same name. Each name generated also includes the StartLine number of the range passed in
/// at the point of first generation.
Expand All @@ -30,6 +35,10 @@ type StableNiceNameGenerator =
new: unit -> StableNiceNameGenerator
member GetUniqueCompilerGeneratedName: name: string * m: range * uniq: int64 -> string

/// Reset the stable-name cache and inner occurrence counters, so both the cached stable names and
/// the underlying occurrence counters are cleared. See NiceNameGenerator.ResetCompilerGeneratedNameState.
member ResetCompilerGeneratedNameState: unit -> unit

/// A compiler-generated-name allocation scope bound to a single ImplFile being optimized.
/// Instances can only be obtained from CompilerGlobalState.NewFileScope so a call site can't
/// accidentally bucket names by the wrong (e.g. inlined-source) file and reintroduce the
Expand Down Expand Up @@ -59,6 +68,12 @@ type internal CompilerGlobalState =
/// under parallel optimization. See https://github.com/dotnet/fsharp/issues/19732.
member NewFileScope: fileRange: range -> PerFileNamingScope

/// Reset all compiler-generated-name occurrence counters on this state, so successive in-process
/// codegen runs over the same source produce identical generated names (a fresh-process layout).
/// Callers must ensure no compilation is concurrently generating names (quiescence). Needed by
/// Edit-and-Continue style scenarios that re-emit from a warm checker.
member ResetCompilerGeneratedNameState: unit -> unit

type Unique = int64

/// Concurrency-safe
Expand Down
96 changes: 96 additions & 0 deletions tests/FSharp.Compiler.Service.Tests/CompilerGlobalStateTests.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
// Copyright (c) Microsoft Corporation. All Rights Reserved. See License.txt in the project root for license information.

module FSharp.Compiler.Service.Tests.CompilerGlobalStateTests

open FSharp.Compiler.CompilerGlobalState
open FSharp.Compiler.Text.Range
open FSharp.Test.Assert
open Xunit

[<Fact>]
let ``NiceNameGenerator drifts across calls and ResetCompilerGeneratedNameState restores a fresh-process layout`` () =
let nng = NiceNameGenerator()
let r = rangeN "niceNameGenerator.fs" 10

// First batch: occurrence counters start from zero, so names drift f@10, f@10-1, f@10-2.
let batch1 = [ for _ in 1 .. 3 -> nng.FreshCompilerGeneratedName("f", r) ]
batch1 |> shouldEqual [ "f@10"; "f@10-1"; "f@10-2" ]

// Without a reset, further calls keep drifting from where the counters left off.
let keepsDriftingWithoutReset = [ for _ in 1 .. 2 -> nng.FreshCompilerGeneratedName("f", r) ]
keepsDriftingWithoutReset |> shouldEqual [ "f@10-3"; "f@10-4" ]

// Resetting clears the occurrence counters, so a subsequent run reproduces the very first batch.
nng.ResetCompilerGeneratedNameState()
let batch2 = [ for _ in 1 .. 3 -> nng.FreshCompilerGeneratedName("f", r) ]
batch2 |> shouldEqual batch1

[<Fact>]
let ``StableNiceNameGenerator caches by uniq and ResetCompilerGeneratedNameState clears both the cache and the counters`` () =
let gen = StableNiceNameGenerator()
let r = rangeN "stableNiceNameGenerator.fs" 20

// First occurrence of "h" for uniq 1.
let first = gen.GetUniqueCompilerGeneratedName("h", r, 1L)
first |> shouldEqual "h@20"

// A different uniq for the same basic name advances the shared occurrence counter.
let second = gen.GetUniqueCompilerGeneratedName("h", r, 2L)
second |> shouldEqual "h@20-1"

// Re-querying uniq 1 must return the cached name, not a recomputed (drifted) one, even though
// the shared occurrence counter for "h" has since advanced to produce `second`.
let cachedAgain = gen.GetUniqueCompilerGeneratedName("h", r, 1L)
cachedAgain |> shouldEqual first

gen.ResetCompilerGeneratedNameState()

// Replaying the exact same sequence of calls after a reset reproduces the exact same names
// ("h@20" then "h@20-1"), because both the stable-name cache and the shared occurrence
// counter were cleared: a fresh call for uniq 1 is once again the first-ever occurrence.
let afterResetForUniq1 = gen.GetUniqueCompilerGeneratedName("h", r, 1L)
afterResetForUniq1 |> shouldEqual first

let afterResetForUniq2 = gen.GetUniqueCompilerGeneratedName("h", r, 2L)
afterResetForUniq2 |> shouldEqual second

// The cache is fully functional again after reset: re-querying uniq 1 still returns the
// cached (post-reset) name rather than drifting further.
let cachedAgainAfterReset = gen.GetUniqueCompilerGeneratedName("h", r, 1L)
cachedAgainAfterReset |> shouldEqual afterResetForUniq1

// Prove the stable-name CACHE itself was cleared, not just the inner counters: after another
// reset, the same (name, uniq) key queried with a DIFFERENT range must be recomputed from the
// new range ("h@99"). A stale cache entry would instead return the pre-reset "h@20".
gen.ResetCompilerGeneratedNameState()
let differentRange = rangeN "stableNiceNameGenerator.fs" 99
let recomputedForUniq1 = gen.GetUniqueCompilerGeneratedName("h", differentRange, 1L)
recomputedForUniq1 |> shouldEqual "h@99"

[<Fact>]
let ``CompilerGlobalState.ResetCompilerGeneratedNameState resets all three generators together`` () =
let state = CompilerGlobalState()
let r = rangeN "compilerGlobalState.fs" 30

let niceName1 = state.NiceNameGenerator.FreshCompilerGeneratedName("f", r)
let ilxName1 = state.IlxGenNiceNameGenerator.FreshCompilerGeneratedName("g", r)
let stableName1 = state.StableNameGenerator.GetUniqueCompilerGeneratedName("h", r, 1L)

// Drift each generator away from its first-occurrence name before resetting.
state.NiceNameGenerator.FreshCompilerGeneratedName("f", r) |> ignore
state.IlxGenNiceNameGenerator.FreshCompilerGeneratedName("g", r) |> ignore
state.StableNameGenerator.GetUniqueCompilerGeneratedName("h", r, 2L) |> ignore

state.ResetCompilerGeneratedNameState()

let niceName2 = state.NiceNameGenerator.FreshCompilerGeneratedName("f", r)
let ilxName2 = state.IlxGenNiceNameGenerator.FreshCompilerGeneratedName("g", r)
// Replaying the same first call (uniq 1) after the aggregate reset reproduces the original
// stable name, confirming the reset reached the StableNameGenerator too. (StableNiceNameGenerator's
// own tests separately confirm that the reset actually clears its cache, rather than merely
// resetting the shared occurrence counter.)
let stableName2 = state.StableNameGenerator.GetUniqueCompilerGeneratedName("h", r, 1L)

niceName2 |> shouldEqual niceName1
ilxName2 |> shouldEqual ilxName1
stableName2 |> shouldEqual stableName1
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@
<Compile Include="XmlDocTests.fs" />
<Compile Include="XmlDocTests - Units of Measure.fs" />
<Compile Include="RangeTests.fs" />
<Compile Include="CompilerGlobalStateTests.fs" />
<Compile Include="TooltipTests.fs" />
<Compile Include="TokenizerTests.fs" />
<Compile Include="QuickParseTests.fs" />
Expand Down
Loading