Skip to content

Commit 26bbbcd

Browse files
committed
Extract stable synthesized-name replay layer
Add internal generated-name normalization and synthesized-name map replay support as a standalone slice. The new map state is side-channel based, all new compiler modules remain internal, and CompilerGlobalState preserves the existing no-map counter path while checking an accessor captured once per compiler state. Route existing IlxGen generated-name allocations through inert helper wrappers, add pure name-map and normalizer tests, add a normal compilation determinism guard over emitted generated names, and document the extracted seams in P5_REPORT.md. Verification: built FSharp.Compiler.Service, FSharp.Compiler.Service.Tests, FSharp.Compiler.ComponentTests, and FSharpSuite.Tests in Release; ran the migrated service test classes, the component determinism class, FSharpSuite DeterministicTests, and the FCS SurfaceArea class successfully.
1 parent cceb2b8 commit 26bbbcd

12 files changed

Lines changed: 1159 additions & 67 deletions

File tree

‎docs/release-notes/.FSharp.Compiler.Service/11.0.100.md‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,7 @@
115115

116116
### Added
117117

118+
* Added internal synthesized-name replay infrastructure for compiler-generated names, preserving normal compilation output while enabling future hot reload name stability work.
118119
* Added `FSharpMemberOrFunctionOrValue.IsPropertyAccessor` convenience property that returns true for compiler-generated property accessors (`get_X` / `set_X`). ([Issue #18157](https://github.com/dotnet/fsharp/issues/18157), [PR #19883](https://github.com/dotnet/fsharp/pull/19883))
119120
* Added warning FS3884 when a function or delegate value is used as an interpolated string argument. ([PR #19289](https://github.com/dotnet/fsharp/pull/19289))
120121
* Symbols: add ObsoleteDiagnosticInfo ([PR #19359](https://github.com/dotnet/fsharp/pull/19359))
@@ -137,4 +138,4 @@
137138
* Exception field serialization (`GetObjectData` and field-restoring constructor) is now gated behind `langversion:11` (`LanguageFeature.ExceptionFieldSerializationSupport`). With langversion ≤10, exception codegen is unchanged from pre-#19342 behavior. ([PR #19746](https://github.com/dotnet/fsharp/pull/19746))
138139

139140
### Breaking Changes
140-
* Optimizer: don't inline named functions in debug builds ([PR #19548](https://github.com/dotnet/fsharp/pull/19548)
141+
* Optimizer: don't inline named functions in debug builds ([PR #19548](https://github.com/dotnet/fsharp/pull/19548)

‎src/Compiler/CodeGen/IlxGen.fs‎

Lines changed: 23 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,16 @@ open FSharp.Compiler.TypedTreeOps.DebugPrint
4545
open FSharp.Compiler.TypeHierarchy
4646
open FSharp.Compiler.TypeRelations
4747

48+
// Naming wrappers routed through here so synthesized-name replay stays enforceable.
49+
let private freshIlxName (g: TcGlobals) name m =
50+
g.CompilerGlobalState.Value.IlxGenNiceNameGenerator.FreshCompilerGeneratedName(name, m)
51+
52+
let private freshCoreName (g: TcGlobals) name m =
53+
g.CompilerGlobalState.Value.NiceNameGenerator.FreshCompilerGeneratedName(name, m)
54+
55+
let private nextIlxOrdinal (g: TcGlobals) name m =
56+
g.CompilerGlobalState.Value.IlxGenNiceNameGenerator.IncrementOnly(name, m)
57+
4858
let getEmptyStackGuard () = StackGuard("IlxAssemblyGenerator")
4959

5060
let IsNonErasedTypar (tp: Typar) = not tp.IsErased
@@ -876,16 +886,12 @@ let GenFieldSpecForStaticField (isInteractive, g: TcGlobals, ilContainerTy, vspe
876886
elif g.realsig then
877887
assert (g.CompilerGlobalState |> Option.isSome)
878888

879-
mkILFieldSpecInTy (
880-
ilContainerTy,
881-
CompilerGeneratedName(g.CompilerGlobalState.Value.IlxGenNiceNameGenerator.FreshCompilerGeneratedName(nm, m)),
882-
ilTy
883-
)
889+
mkILFieldSpecInTy (ilContainerTy, CompilerGeneratedName(freshIlxName g nm m), ilTy)
884890
else
885891
let fieldName =
886892
// Ensure that we have an g.CompilerGlobalState
887893
assert (g.CompilerGlobalState |> Option.isSome)
888-
g.CompilerGlobalState.Value.IlxGenNiceNameGenerator.FreshCompilerGeneratedName(nm, m)
894+
freshIlxName g nm m
889895

890896
let ilFieldContainerTy = mkILTyForCompLoc (CompLocForInitClass cloc)
891897
mkILFieldSpecInTy (ilFieldContainerTy, fieldName, ilTy)
@@ -2492,7 +2498,7 @@ and AssemblyBuilder(cenv: cenv, anonTypeTable: AnonTypeGenerationTable) as mgbuf
24922498
// prime walk never sees) stays same-flags deterministic: IncrementOnly buckets per
24932499
// (name, FileIndex) and the delayed-codegen drain is sequential within a file, so each
24942500
// file gets a stable counter. (#19929 removes this counter via content-derived naming.)
2495-
| _ -> g.CompilerGlobalState.Value.IlxGenNiceNameGenerator.IncrementOnly("@T", cloc.Range)
2501+
| _ -> nextIlxOrdinal g "@T" cloc.Range
24962502

24972503
let name = CompilerGeneratedName $"T{unique}_{size}Bytes" // Type names ending ...$T<unique>_37Bytes
24982504

@@ -2565,10 +2571,7 @@ and AssemblyBuilder(cenv: cenv, anonTypeTable: AnonTypeGenerationTable) as mgbuf
25652571
else
25662572
cloc
25672573

2568-
primedRawTypeCounter.GetOrAdd(
2569-
(cloc, size),
2570-
(fun _ -> g.CompilerGlobalState.Value.IlxGenNiceNameGenerator.IncrementOnly("@T", cloc.Range))
2571-
)
2574+
primedRawTypeCounter.GetOrAdd((cloc, size), (fun _ -> nextIlxOrdinal g "@T" cloc.Range))
25722575
|> ignore
25732576

25742577
member _.GenerateAnonType(genToStringMethod, anonInfo: AnonRecdTypeInfo) =
@@ -2973,8 +2976,7 @@ let GenConstArray cenv (cgbuf: CodeGenBuffer) eenv ilElementType (data: 'a[]) (w
29732976
else
29742977
let vtspec = cgbuf.mgbuf.GenerateRawDataValueType(eenv.cloc, bytes.Length)
29752978

2976-
let unique =
2977-
g.CompilerGlobalState.Value.IlxGenNiceNameGenerator.IncrementOnly("@field", eenv.cloc.Range)
2979+
let unique = nextIlxOrdinal g "@field" eenv.cloc.Range
29782980

29792981
let ilFieldName = CompilerGeneratedName $"field{unique}"
29802982
let fty = ILType.Value vtspec
@@ -4714,7 +4716,7 @@ and GenApp (cenv: cenv) cgbuf eenv (f, fty, tyargs, curriedArgs, m) sequel =
47144716
let locName =
47154717
// Ensure that we have an g.CompilerGlobalState
47164718
assert (g.CompilerGlobalState |> Option.isSome)
4717-
g.CompilerGlobalState.Value.IlxGenNiceNameGenerator.FreshCompilerGeneratedName("arg", m), ilTy, false
4719+
freshIlxName g "arg" m, ilTy, false
47184720

47194721
let loc, _realloc, eenv = AllocLocal cenv cgbuf eenv true locName scopeMarks
47204722
GenExpr cenv cgbuf eenv laterArg Continue
@@ -5051,13 +5053,7 @@ and GenTry cenv cgbuf eenv scopeMarks (e1, m, resultTy, spTry) =
50515053
assert (cenv.g.CompilerGlobalState |> Option.isSome)
50525054

50535055
let whereToSave, _realloc, eenvinner =
5054-
AllocLocal
5055-
cenv
5056-
cgbuf
5057-
eenvinner
5058-
true
5059-
(cenv.g.CompilerGlobalState.Value.IlxGenNiceNameGenerator.FreshCompilerGeneratedName("tryres", m), ilResultTy, false)
5060-
(startTryMark, endTryMark)
5056+
AllocLocal cenv cgbuf eenvinner true (freshIlxName cenv.g "tryres" m, ilResultTy, false) (startTryMark, endTryMark)
50615057

50625058
Some(whereToSave, ilResultTy), eenvinner
50635059

@@ -5332,8 +5328,7 @@ and GenIntegerForLoop cenv cgbuf eenv (spFor, spTo, v, e1, dir, e2, loopBody, m)
53325328
// Ensure that we have an g.CompilerGlobalState
53335329
assert (g.CompilerGlobalState |> Option.isSome)
53345330

5335-
let vName =
5336-
g.CompilerGlobalState.Value.IlxGenNiceNameGenerator.FreshCompilerGeneratedName("endLoop", m)
5331+
let vName = freshIlxName g "endLoop" m
53375332

53385333
let v, _realloc, eenvinner =
53395334
AllocLocal cenv cgbuf eenvinner true (vName, g.ilg.typ_Int32, false) (start, finish)
@@ -5961,13 +5956,7 @@ and GenDefaultValue cenv cgbuf eenv (ty, m) =
59615956
// Ensure that we have an g.CompilerGlobalState
59625957
assert (g.CompilerGlobalState |> Option.isSome)
59635958

5964-
AllocLocal
5965-
cenv
5966-
cgbuf
5967-
eenv
5968-
true
5969-
(g.CompilerGlobalState.Value.IlxGenNiceNameGenerator.FreshCompilerGeneratedName("default", m), ilTy, false)
5970-
scopeMarks
5959+
AllocLocal cenv cgbuf eenv true (freshIlxName g "default" m, ilTy, false) scopeMarks
59715960
// We can normally rely on .NET IL zero-initialization of the temporaries
59725961
// we create to get zero values for struct types.
59735962
//
@@ -6646,25 +6635,11 @@ and GenStructStateMachine cenv cgbuf eenvouter (res: LoweredStateMachine) sequel
66466635

66476636
// The local for the state machine
66486637
let locIdx, realloc, _ =
6649-
AllocLocal
6650-
cenv
6651-
cgbuf
6652-
eenvouter
6653-
true
6654-
(g.CompilerGlobalState.Value.IlxGenNiceNameGenerator.FreshCompilerGeneratedName("machine", m), ilCloTy, false)
6655-
scopeMarks
6638+
AllocLocal cenv cgbuf eenvouter true (freshIlxName g "machine" m, ilCloTy, false) scopeMarks
66566639

66576640
// The local for the state machine address
66586641
let locIdx2, _realloc2, _ =
6659-
AllocLocal
6660-
cenv
6661-
cgbuf
6662-
eenvouter
6663-
true
6664-
(g.CompilerGlobalState.Value.IlxGenNiceNameGenerator.FreshCompilerGeneratedName(afterCodeThisVar.DisplayName, m),
6665-
ilMachineAddrTy,
6666-
false)
6667-
scopeMarks
6642+
AllocLocal cenv cgbuf eenvouter true (freshIlxName g afterCodeThisVar.DisplayName m, ilMachineAddrTy, false) scopeMarks
66686643

66696644
let eenvouter =
66706645
eenvouter
@@ -9433,7 +9408,7 @@ and GenParams
94339408
if takenNames.Contains(id.idText) then
94349409
// Ensure that we have an g.CompilerGlobalState
94359410
assert (g.CompilerGlobalState |> Option.isSome)
9436-
g.CompilerGlobalState.Value.NiceNameGenerator.FreshCompilerGeneratedName(id.idText, id.idRange)
9411+
freshCoreName g id.idText id.idRange
94379412
else
94389413
id.idText
94399414

@@ -10502,13 +10477,7 @@ and EmitSaveStack cenv cgbuf eenv m scopeMarks =
1050210477
// Ensure that we have an g.CompilerGlobalState
1050310478
assert (cenv.g.CompilerGlobalState |> Option.isSome)
1050410479

10505-
AllocLocal
10506-
cenv
10507-
cgbuf
10508-
eenv
10509-
true
10510-
(cenv.g.CompilerGlobalState.Value.IlxGenNiceNameGenerator.FreshCompilerGeneratedName("spill", m), ty, false)
10511-
scopeMarks
10480+
AllocLocal cenv cgbuf eenv true (freshIlxName cenv.g "spill" m, ty, false) scopeMarks
1051210481

1051310482
idx, eenv)
1051410483

‎src/Compiler/FSharp.Compiler.Service.fsproj‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -306,6 +306,8 @@
306306
<Compile Include="SyntaxTree\LexHelpers.fs">
307307
<Link>SyntaxTree\LexHelpers.fs</Link>
308308
</Compile>
309+
<Compile Include="TypedTree\GeneratedNames.fs" />
310+
<Compile Include="TypedTree\CompilerGeneratedNameMapState.fs" />
309311
<Compile Include="$(FsYaccOutputFolder)pplex.fsi">
310312
<Link>SyntaxTree\FsLexOutput\pplex.fsi</Link>
311313
</Compile>
@@ -326,6 +328,7 @@
326328
<Compile Include="TypedTree\TypeProviders.fs" />
327329
<Compile Include="TypedTree\QuotationPickler.fsi" />
328330
<Compile Include="TypedTree\QuotationPickler.fs" />
331+
<Compile Include="TypedTree\SynthesizedTypeMaps.fs" />
329332
<Compile Include="TypedTree\CompilerGlobalState.fsi" />
330333
<Compile Include="TypedTree\CompilerGlobalState.fs" />
331334
<Compile Include="TypedTree\WellKnownAttribs.fsi" />
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
module internal FSharp.Compiler.CompilerGeneratedNameMapState
2+
3+
open System.Runtime.CompilerServices
4+
5+
/// Minimal abstraction for compiler-generated name replay/state.
6+
/// Implementations can be hot-reload aware without coupling core compiler paths
7+
/// to a concrete synthesized-name map type.
8+
type ICompilerGeneratedNameMap =
9+
abstract BeginSession: unit -> unit
10+
abstract GetOrAddName: basicName: string -> string
11+
abstract Snapshot: seq<struct (string * string[])>
12+
abstract LoadSnapshot: snapshot: seq<struct (string * string[])> -> unit
13+
14+
// Keep optional name-map state external to CompilerGlobalState so core signatures can remain stable.
15+
type private NameMapHolder() =
16+
// Reads vastly outnumber writes. Installs happen at most a handful of times per
17+
// compile, so the slot is a single volatile field rather than a lock-guarded one.
18+
// Reference reads and writes are atomic, and the volatile semantics preserve the
19+
// visibility ordering the lock provided.
20+
[<VolatileField>]
21+
let mutable current: ICompilerGeneratedNameMap option = None
22+
23+
member _.TryGet() = current
24+
member _.Set(value: ICompilerGeneratedNameMap option) = current <- value
25+
26+
let private holders = ConditionalWeakTable<obj, NameMapHolder>()
27+
28+
let private getOrCreateHolder (owner: obj) =
29+
holders.GetValue(owner, fun _ -> NameMapHolder())
30+
31+
/// Pure read: never inserts, so a compile that never installs a map pays a single
32+
/// failed weak-table lookup.
33+
let private tryGetHolder (owner: obj) =
34+
match holders.TryGetValue owner with
35+
| true, holder -> Some holder
36+
| _ -> None
37+
38+
let tryGetCompilerGeneratedNameMap (owner: obj) =
39+
match tryGetHolder owner with
40+
| Some holder -> holder.TryGet()
41+
| None -> None
42+
43+
/// A reader for the owner's name-map slot. The holder is resolved exactly once here
44+
/// and captured by the returned closure, so each generated name costs a single
45+
/// volatile field read rather than a ConditionalWeakTable probe and lock.
46+
///
47+
/// The holder is created eagerly on purpose: the emit hook can install the map later
48+
/// in the compile, after CompilerGlobalState and therefore this accessor have been
49+
/// constructed, and it installs through the same owner. Pre-creating the holder means
50+
/// that later install mutates the object this closure captured, so the map is observed.
51+
let getCompilerGeneratedNameMapAccessor (owner: obj) : unit -> ICompilerGeneratedNameMap option =
52+
let holder = getOrCreateHolder owner
53+
fun () -> holder.TryGet()
54+
55+
let setCompilerGeneratedNameMap (owner: obj) (map: ICompilerGeneratedNameMap) = (getOrCreateHolder owner).Set(Some map)
56+
57+
let setCompilerGeneratedNameMapOpt (owner: obj) (map: ICompilerGeneratedNameMap option) = (getOrCreateHolder owner).Set(map)
58+
59+
let clearCompilerGeneratedNameMap (owner: obj) = (getOrCreateHolder owner).Set(None)

‎src/Compiler/TypedTree/CompilerGlobalState.fs‎

Lines changed: 36 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ module FSharp.Compiler.CompilerGlobalState
77
open System
88
open System.Collections.Concurrent
99
open System.Threading
10+
open FSharp.Compiler.CompilerGeneratedNameMapState
1011
open FSharp.Compiler.Syntax.PrettyNaming
1112
open FSharp.Compiler.Text
1213

@@ -17,7 +18,7 @@ open FSharp.Compiler.Text
1718
/// It is made concurrency-safe since a global instance of the type is allocated in tast.fs, and it is good
1819
/// policy to make all globally-allocated objects concurrency safe in case future versions of the compiler
1920
/// are used to host multiple concurrent instances of compilation.
20-
type NiceNameGenerator() =
21+
type NiceNameGenerator(getCompilerGeneratedNameMap: unit -> ICompilerGeneratedNameMap option) =
2122
let basicNameCounts = ConcurrentDictionary<struct (string * int), int ref>(max Environment.ProcessorCount 1, 127)
2223
// Cache this as a delegate.
2324
let basicNameCountsAddDelegate = Func<struct (string * int), int ref>(fun _ -> ref 0)
@@ -33,8 +34,11 @@ type NiceNameGenerator() =
3334
CompilerGeneratedNameSuffix basicName (string m.StartLine + (match (count - 1) with 0 -> "" | n -> "-" + string n))
3435

3536
member _.FreshCompilerGeneratedNameOfBasicName (basicName, m: range) =
36-
let count = increment basicName m
37-
mkName basicName m count
37+
match getCompilerGeneratedNameMap() with
38+
| Some map -> map.GetOrAddName basicName
39+
| None ->
40+
let count = increment basicName m
41+
mkName basicName m count
3842

3943
member this.FreshCompilerGeneratedName (name, m: range) =
4044
this.FreshCompilerGeneratedNameOfBasicName (GetBasicNameOfPossibleCompilerGeneratedName name, m)
@@ -43,19 +47,32 @@ type NiceNameGenerator() =
4347

4448
member _.FreshCompilerGeneratedNameInScope (scopeFileIndex: int, name: string, m: range) =
4549
let basicName = GetBasicNameOfPossibleCompilerGeneratedName name
46-
let count = incrementBucket basicName scopeFileIndex
47-
mkName basicName m count
50+
51+
// The replay map must win over per-file occurrence buckets, exactly as it
52+
// does in FreshCompilerGeneratedNameOfBasicName. When a session installs the
53+
// map, every allocation path replays the baseline's stable names. Otherwise,
54+
// line-based per-file names would drift under edits. The map is only ever
55+
// installed by the hot reload emit hook or by an in-process compile, so the
56+
// deterministic per-file bucketing from https://github.com/dotnet/fsharp/issues/19732
57+
// is untouched in normal compilation.
58+
match getCompilerGeneratedNameMap() with
59+
| Some map -> map.GetOrAddName basicName
60+
| None ->
61+
let count = incrementBucket basicName scopeFileIndex
62+
mkName basicName m count
63+
64+
new () = NiceNameGenerator(fun () -> None)
4865

4966
/// Generates compiler-generated names marked up with a source code location, but if given the same unique value then
5067
/// return precisely the same name. Each name generated also includes the StartLine number of the range passed in
5168
/// at the point of first generation.
5269
///
5370
/// This type may be accessed concurrently, though in practice it is only used from the compilation thread.
5471
/// It is made concurrency-safe since a global instance of the type is allocated in tast.fs.
55-
type StableNiceNameGenerator() =
72+
type StableNiceNameGenerator(getCompilerGeneratedNameMap: unit -> ICompilerGeneratedNameMap option) =
5673

5774
let niceNames = ConcurrentDictionary<string * int64, Lazy<string>>(max Environment.ProcessorCount 1, 127)
58-
let innerGenerator = NiceNameGenerator()
75+
let innerGenerator = NiceNameGenerator(getCompilerGeneratedNameMap)
5976

6077
member x.GetUniqueCompilerGeneratedName (name, m: range, uniq) =
6178
let basicName = GetBasicNameOfPossibleCompilerGeneratedName name
@@ -65,21 +82,28 @@ type StableNiceNameGenerator() =
6582
lazy innerGenerator.FreshCompilerGeneratedNameOfBasicName(basicName, m))
6683
lazyName.Value
6784

85+
new () = StableNiceNameGenerator(fun () -> None)
86+
6887
[<Sealed>]
6988
type PerFileNamingScope internal (nng: NiceNameGenerator, fileIndex: int) =
7089

7190
member _.Fresh (name: string, m: range) =
7291
nng.FreshCompilerGeneratedNameInScope(fileIndex, name, m)
7392

74-
type internal CompilerGlobalState () =
93+
type internal CompilerGlobalState () as this =
94+
/// Reader for the optional synthesized-name map attached to this instance. The
95+
/// accessor resolves the side-channel slot once, so each generated name costs a
96+
/// single None check, not a weak-table probe and lock, when no map is installed.
97+
let getCompilerGeneratedNameMap = getCompilerGeneratedNameMapAccessor (this :> obj)
98+
7599
/// A global generator of compiler generated names
76-
let globalNng = NiceNameGenerator()
100+
let globalNng = NiceNameGenerator(getCompilerGeneratedNameMap)
77101

78102
/// A global generator of stable compiler generated names
79-
let globalStableNameGenerator = StableNiceNameGenerator ()
103+
let globalStableNameGenerator = StableNiceNameGenerator(getCompilerGeneratedNameMap)
80104

81105
/// A name generator used by IlxGen for static fields, some generated arguments and other things.
82-
let ilxgenGlobalNng = NiceNameGenerator ()
106+
let ilxgenGlobalNng = NiceNameGenerator(getCompilerGeneratedNameMap)
83107

84108
member _.NiceNameGenerator = globalNng
85109

@@ -102,4 +126,4 @@ let newUnique() = Interlocked.Increment &uniqueCount
102126
let mutable private stampCount = 0L
103127
let newStamp() =
104128
let stamp = Interlocked.Increment &stampCount
105-
stamp
129+
stamp

0 commit comments

Comments
 (0)