Skip to content

Commit 7986396

Browse files
T-GroCopilot
andcommitted
Intern provided namespaces to fix parallel-compilation resolution race
Follow-up to #19969, which interned provided *type* entities behind a lock-free CAS append + entitiesVersion design. The namespace entities on a provided type's path never got the same treatment: AddModuleOrNamespaceByMutation appended non-atomically (losing updates that race AddProvidedTypeEntity's CAS append) and the two namespace-materialization sites deduped with a non-atomic check-then-act, so two threads could each build a disjoint namespace subtree and strand the provided types interned under the orphan (spurious FS0001/FS0039). Make the namespace append atomic (shared AppendEntityByMutation CAS loop) and add GetOrInternNamespaceEntity, reusing the single providedEntitiesByMangledName intern table (one mangled name -> one entity per parent). Its factory reuses any pre-existing module/namespace of that name before creating, so a provider extending a real namespace no longer forks a duplicate. Both injection sites route through it. modulesByDemangledNameCache is now version-stamped since namespace appends can run concurrently with ModulesAndNamespacesByDemangledName reads. Fixes #20020 Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
1 parent aa4b044 commit 7986396

5 files changed

Lines changed: 161 additions & 74 deletions

File tree

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
* Restore packaging of an F# design-time type provider that is activated via a `ProjectReference` carrying `IsFSharpDesignTimeProvider="true"`. The provider assembly is again included under `fsharp41` when packing (including `pack --no-build`); `PackageFSharpDesignTimeTools` now resolves the provider via `GetTargetPath`, which works in `dotnet pack`'s `BuildProjectReferences=false` content build without forcing an early `ResolveReferences`. ([Issue #18924](https://github.com/dotnet/fsharp/issues/18924), [PR #19979](https://github.com/dotnet/fsharp/pull/19979))
44
* Provided types used from multiple files no longer produce spurious FS0001 type mismatches under parallel compilation; provided-type entities are now interned so every file linking a given provided type shares one entity. ([PR #19969](https://github.com/dotnet/fsharp/pull/19969))
55
* TypeProviders-SDK providers now load under an unoptimized compiler; the `systemRuntimeContainsType` closure field the SDK reflects on (`tcImports`) is captured stably regardless of optimization settings. ([PR #19969](https://github.com/dotnet/fsharp/pull/19969))
6+
* Provided namespaces are now interned and appended atomically alongside provided types, completing the #19969 fix: under parallel compilation two files linking the same provided namespace no longer build disjoint subtrees that strand the provided types under them (spurious FS0001/FS0039). ([Issue #20020](https://github.com/dotnet/fsharp/issues/20020), [PR #20021](https://github.com/dotnet/fsharp/pull/20021))
67
* Fixed: Inheriting from an undefined type now reports `FS0039` exactly once instead of three times. Phase 1F and Phase 2A of inherit-clause type-checking now skip re-resolving a syntactic clause whose Phase 1D resolution already failed with `UndefinedName`, eliminating both the duplicate diagnostic and the redundant work. ([Issue #16432](https://github.com/dotnet/fsharp/issues/16432), [PR #19862](https://github.com/dotnet/fsharp/pull/19862))
78
* Fix several F# editor semantic-classification errors: F# delegate declarations no longer highlight the `delegate of …` syntax as a method, computation-expression builders inside list/array comprehensions are classified as `ComputationExpression`, the closing `]` of an open-ended slice (e.g. `xs[0..]`) is no longer classified as `Function`/`Method`, and `open type T` is no longer reported as unused when its imported members (static members, static fields, or DU union cases) are used. ([Issue #19905](https://github.com/dotnet/fsharp/issues/19905), [PR #19960](https://github.com/dotnet/fsharp/pull/19960))
89
* Diagnostic FS0027 now emits a parameter-specific message (suggesting a `let mutable x = x` shadow or `byref<_>`) instead of the illegal `let mutable x = expression` shadow when the assignment target is a function or method parameter. ([Issue #15803](https://github.com/dotnet/fsharp/issues/15803), [PR #19866](https://github.com/dotnet/fsharp/pull/19866))

src/Compiler/Driver/CompilerImports.fs

Lines changed: 27 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1746,47 +1746,36 @@ and [<Sealed>] TcImports
17461746
match remainingNamespace with
17471747
| next :: rest ->
17481748
// Inject the namespace entity
1749-
match entity.ModuleOrNamespaceType.ModulesAndNamespacesByDemangledName.TryFind next with
1750-
| Some childEntity ->
1751-
tcImports.InjectProvidedNamespaceOrTypeIntoEntity(
1752-
typeProviderEnvironment,
1753-
tcConfig,
1754-
m,
1755-
childEntity,
1756-
next :: injectedNamespace,
1757-
rest,
1758-
provider,
1759-
st
1760-
)
1761-
| None ->
1762-
// Build up the artificial namespace if there is not a real one.
1763-
let cpath =
1764-
CompPath(
1765-
ILScopeRef.Local,
1766-
SyntaxAccess.Unknown,
1767-
injectedNamespace
1768-
|> List.rev
1769-
|> List.map (fun n -> (n, ModuleOrNamespaceKind.Namespace true))
1770-
)
1771-
1772-
let mid = ident (next, rangeStartup)
1773-
let mty = Construct.NewEmptyModuleOrNamespaceType(Namespace true)
1774-
1775-
let newNamespace =
1776-
Construct.NewModuleOrNamespace (Some cpath) taccessPublic mid XmlDoc.Empty [] (MaybeLazy.Strict mty)
1749+
let childEntity =
1750+
entity.ModuleOrNamespaceType.GetOrInternNamespaceEntity(
1751+
next,
1752+
(fun () ->
1753+
// Build up the artificial namespace if there is not a real one.
1754+
let cpath =
1755+
CompPath(
1756+
ILScopeRef.Local,
1757+
SyntaxAccess.Unknown,
1758+
injectedNamespace
1759+
|> List.rev
1760+
|> List.map (fun n -> (n, ModuleOrNamespaceKind.Namespace true))
1761+
)
17771762

1778-
entity.ModuleOrNamespaceType.AddModuleOrNamespaceByMutation newNamespace
1763+
let mid = ident (next, rangeStartup)
1764+
let mty = Construct.NewEmptyModuleOrNamespaceType(Namespace true)
17791765

1780-
tcImports.InjectProvidedNamespaceOrTypeIntoEntity(
1781-
typeProviderEnvironment,
1782-
tcConfig,
1783-
m,
1784-
newNamespace,
1785-
next :: injectedNamespace,
1786-
rest,
1787-
provider,
1788-
st
1766+
Construct.NewModuleOrNamespace (Some cpath) taccessPublic mid XmlDoc.Empty [] (MaybeLazy.Strict mty))
17891767
)
1768+
1769+
tcImports.InjectProvidedNamespaceOrTypeIntoEntity(
1770+
typeProviderEnvironment,
1771+
tcConfig,
1772+
m,
1773+
childEntity,
1774+
next :: injectedNamespace,
1775+
rest,
1776+
provider,
1777+
st
1778+
)
17901779
| [] ->
17911780
match st with
17921781
| Some st ->

src/Compiler/TypedTree/TypedTree.fs

Lines changed: 52 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -2024,7 +2024,7 @@ type ModuleOrNamespaceType(kind: ModuleOrNamespaceKind, vals: QueueList<Val>, en
20242024
let mutable entities = entities
20252025

20262026
#if !NO_TYPEPROVIDERS
2027-
// One Entity per provided type even when linked concurrently from several files (graph-based checking).
2027+
// One Entity per provided type or namespace even when linked concurrently from several files (graph-based checking).
20282028
let mutable providedEntitiesByMangledName: ConcurrentDictionary<string, Lazy<Entity>> | null = null
20292029
#endif
20302030

@@ -2041,12 +2041,12 @@ type ModuleOrNamespaceType(kind: ModuleOrNamespaceKind, vals: QueueList<Val>, en
20412041
// We should probably change to 'mutable'.
20422042
//
20432043
// We do not need to lock most of this mutable state since it is only ever accessed from the compiler thread.
2044-
// The exception is the four lookup tables invalidated by mutating 'entities' (provided-type linking can run
2045-
// on several graph-based-checking threads): those are read through 'cacheOptByrefByVersion' tagged with
2046-
// 'entitiesVersion', which stays coherent under concurrent appends without any lock.
2044+
// The exception is the lookup tables invalidated by mutating 'entities' (provided-type and provided-namespace
2045+
// linking can run on several graph-based-checking threads): those are read through 'cacheOptByrefByVersion'
2046+
// tagged with 'entitiesVersion', which stays coherent under concurrent appends without any lock.
20472047
let activePatternElemRefCache: NameMap<ActivePatternElemRef> option ref = ref None
20482048

2049-
let mutable modulesByDemangledNameCache: NameMap<ModuleOrNamespace> option = None
2049+
let mutable modulesByDemangledNameCache: (int * NameMap<ModuleOrNamespace>) option = None
20502050

20512051
let mutable exconsByDemangledNameCache: NameMap<Tycon> option = None
20522052

@@ -2074,20 +2074,9 @@ type ModuleOrNamespaceType(kind: ModuleOrNamespaceKind, vals: QueueList<Val>, en
20742074
//// "FooException" --> Tycon with exception info
20752075
member _.AllEntities = entities
20762076

2077-
/// Mutation used during compilation of FSharp.Core.dll
2078-
member _.AddModuleOrNamespaceByMutation(modul: ModuleOrNamespace) =
2079-
entities <- QueueList.appendOne entities modul
2080-
modulesByDemangledNameCache <- None
2081-
allEntitiesByMangledNameCache <- None
2082-
System.Threading.Interlocked.Increment(&entitiesVersion) |> ignore
2083-
2084-
#if !NO_TYPEPROVIDERS
2085-
/// Mutation used in hosting scenarios to hold the hosted types in this module or namespace
2086-
member _.AddProvidedTypeEntity(entity: Entity) =
2087-
// Several graph-based-checking threads may link provided types into this module at once, so append
2088-
// atomically with a CAS loop. Bump 'entitiesVersion' last (release): a reader observing the new version
2089-
// is guaranteed to also see 'entity' in 'entities', so the version-stamped lookup caches recompute and
2090-
// never strand a table missing it. The caches need no explicit invalidation - the version bump does it.
2077+
/// Append 'entity', then publish 'entitiesVersion' last (release) so a reader seeing the new version also sees
2078+
/// 'entity' in 'entities' - the version-stamped lookup caches recompute rather than strand it.
2079+
member private _.AppendEntityByMutation(entity: Entity) =
20912080
let rec append () =
20922081
let current = entities
20932082
let updated = QueueList.appendOne current entity
@@ -2096,17 +2085,41 @@ type ModuleOrNamespaceType(kind: ModuleOrNamespaceKind, vals: QueueList<Val>, en
20962085
append ()
20972086
System.Threading.Interlocked.Increment(&entitiesVersion) |> ignore
20982087

2088+
/// Mutation used during compilation of FSharp.Core.dll
2089+
member mtyp.AddModuleOrNamespaceByMutation(modul: ModuleOrNamespace) =
2090+
mtyp.AppendEntityByMutation modul
2091+
2092+
#if !NO_TYPEPROVIDERS
2093+
/// Mutation used in hosting scenarios to hold the hosted types in this module or namespace
2094+
member mtyp.AddProvidedTypeEntity(entity: Entity) =
2095+
mtyp.AppendEntityByMutation entity
2096+
2097+
member private _.ProvidedEntityInternTable =
2098+
match providedEntitiesByMangledName with
2099+
| null ->
2100+
let created = ConcurrentDictionary<string, Lazy<Entity>>()
2101+
match System.Threading.Interlocked.CompareExchange(&providedEntitiesByMangledName, created, null) with
2102+
| null -> created
2103+
| existing -> existing
2104+
| existing -> existing
2105+
20992106
/// Interns a provided-type entity by mangled name; callers must use the returned entity.
21002107
member mtyp.GetOrInternProvidedEntity(mangledName: string, create: unit -> Entity) : Entity =
2101-
let table =
2102-
match providedEntitiesByMangledName with
2103-
| null ->
2104-
let created = ConcurrentDictionary<string, Lazy<Entity>>()
2105-
match System.Threading.Interlocked.CompareExchange(&providedEntitiesByMangledName, created, null) with
2106-
| null -> created
2107-
| existing -> existing
2108-
| existing -> existing
2109-
table.GetOrAdd(mangledName, fun _ -> lazy (let entity = create () in mtyp.AddProvidedTypeEntity entity; entity)).Value
2108+
mtyp.ProvidedEntityInternTable.GetOrAdd(mangledName, fun _ -> lazy (let entity = create () in mtyp.AddProvidedTypeEntity entity; entity)).Value
2109+
2110+
/// Interns a provided-namespace entity by mangled name, reusing any existing entity of that name; callers must use the returned entity.
2111+
member mtyp.GetOrInternNamespaceEntity(mangledName: string, create: unit -> Entity) : Entity =
2112+
mtyp.ProvidedEntityInternTable.GetOrAdd(
2113+
mangledName,
2114+
fun _ ->
2115+
lazy
2116+
match (mtyp.ModulesAndNamespacesByDemangledName: NameMap<ModuleOrNamespace>).TryFind mangledName with
2117+
| Some existing -> existing
2118+
| None ->
2119+
let entity = create ()
2120+
mtyp.AddModuleOrNamespaceByMutation entity
2121+
entity)
2122+
.Value
21102123
#endif
21112124

21122125
/// Return a new module or namespace type with an entity added.
@@ -2226,7 +2239,8 @@ type ModuleOrNamespaceType(kind: ModuleOrNamespaceKind, vals: QueueList<Val>, en
22262239
if entity.IsModuleOrNamespace then
22272240
NameMap.add entity.DemangledModuleOrNamespaceName entity acc
22282241
else acc
2229-
cacheOptByref &modulesByDemangledNameCache (fun () ->
2242+
let version = System.Threading.Volatile.Read(&entitiesVersion)
2243+
cacheOptByrefByVersion version &modulesByDemangledNameCache (fun () ->
22302244
QueueList.foldBack add entities Map.empty)
22312245

22322246
[<DebuggerBrowsable(DebuggerBrowsableState.Never)>]
@@ -3615,13 +3629,15 @@ type NonLocalEntityRef =
36153629
path[j],
36163630
(fun () -> Construct.NewProvidedTycon(resolutionEnvironment, st, ccu.ImportProvidedType, false, m)))
36173631
else
3618-
let cpath = entity.CompilationPath.NestedCompPath entity.LogicalName (ModuleOrNamespaceKind.Namespace false)
3619-
let newEntity =
3620-
Construct.NewModuleOrNamespace
3621-
(Some cpath)
3622-
(TAccess []) (ident(path[k], m)) XmlDoc.Empty []
3623-
(MaybeLazy.Strict (Construct.NewEmptyModuleOrNamespaceType (Namespace true)))
3624-
entity.ModuleOrNamespaceType.AddModuleOrNamespaceByMutation newEntity
3632+
let newEntity =
3633+
entity.ModuleOrNamespaceType.GetOrInternNamespaceEntity(
3634+
path[k],
3635+
(fun () ->
3636+
let cpath = entity.CompilationPath.NestedCompPath entity.LogicalName (ModuleOrNamespaceKind.Namespace false)
3637+
Construct.NewModuleOrNamespace
3638+
(Some cpath)
3639+
(TAccess []) (ident(path[k], m)) XmlDoc.Empty []
3640+
(MaybeLazy.Strict (Construct.NewEmptyModuleOrNamespaceType (Namespace true)))))
36253641
injectNamespacesFromIToJ newEntity (k+1)
36263642
let newEntity = injectNamespacesFromIToJ entity i
36273643

src/Compiler/TypedTree/TypedTree.fsi

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1393,6 +1393,10 @@ type ModuleOrNamespaceType =
13931393
/// Interns a provided-type entity by mangled name so concurrent linking from multiple files yields one
13941394
/// Entity. The first caller's 'create' wins; callers must use the returned entity.
13951395
member GetOrInternProvidedEntity: mangledName: string * create: (unit -> Entity) -> Entity
1396+
1397+
/// Interns a provided-namespace entity by mangled name, reusing any existing entity of that name so concurrent
1398+
/// linking yields one Entity. Callers must use the returned entity.
1399+
member GetOrInternNamespaceEntity: mangledName: string * create: (unit -> Entity) -> Entity
13961400
#endif
13971401

13981402
/// Return a new module or namespace type with a value added.

tests/FSharp.Compiler.Service.Tests/ManglingNameOfProvidedTypes.fs

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -231,3 +231,80 @@ module ProvidedTypeHostingTests =
231231
let table = mtyp.AllEntitiesByCompiledAndLogicalMangledNames
232232
for name in names do
233233
Assert.True(table.ContainsKey name, $"Lookup cache dropped interned entity '{name}'.")
234+
235+
let private newNamedEntity (name: string) =
236+
Construct.NewModuleOrNamespace
237+
(Some(CompPath(ILScopeRef.Local, SyntaxAccess.Unknown, [])))
238+
taccessPublic
239+
(ident (name, Range.range0))
240+
XmlDoc.Empty
241+
[]
242+
(MaybeLazy.Strict(Construct.NewEmptyModuleOrNamespaceType(Namespace true)))
243+
244+
// #20020: interned provided namespaces must be unique per name, and types interned under a namespace built by racing threads stay reachable.
245+
[<Fact>]
246+
let ``GetOrInternNamespaceEntity yields one namespace per name and keeps interned types reachable`` () =
247+
let root = Construct.NewEmptyModuleOrNamespaceType(Namespace true)
248+
let namespaceNames = [| "NsA"; "NsB"; "NsC" |]
249+
let workerCount = 60
250+
use barrier = new System.Threading.Barrier(workerCount)
251+
252+
let workers =
253+
[ for w in 0 .. workerCount - 1 ->
254+
let ns = namespaceNames[w % namespaceNames.Length]
255+
let typeName = $"Type{w}"
256+
257+
System.Threading.Thread(fun () ->
258+
barrier.SignalAndWait()
259+
let nsEntity = root.GetOrInternNamespaceEntity(ns, fun () -> newNamedEntity ns)
260+
261+
nsEntity.ModuleOrNamespaceType.GetOrInternProvidedEntity(typeName, fun () -> newNamedEntity typeName)
262+
|> ignore) ]
263+
264+
workers |> List.iter (fun t -> t.Start())
265+
workers |> List.iter (fun t -> t.Join())
266+
267+
let namespaceEntities = root.AllEntities |> Seq.toList
268+
Assert.Equal(namespaceNames.Length, namespaceEntities.Length)
269+
270+
Assert.Equal(
271+
namespaceNames.Length,
272+
namespaceEntities |> List.map (fun e -> e.LogicalName) |> List.distinct |> List.length)
273+
274+
for i in 0 .. namespaceNames.Length - 1 do
275+
let ns = namespaceNames[i]
276+
let expectedTypes = [ for w in 0 .. workerCount - 1 do if w % namespaceNames.Length = i then $"Type{w}" ]
277+
let nsEntity = root.GetOrInternNamespaceEntity(ns, fun () -> failwith "namespace should already be interned")
278+
let table = nsEntity.ModuleOrNamespaceType.AllEntitiesByCompiledAndLogicalMangledNames
279+
280+
for typeName in expectedTypes do
281+
Assert.True(
282+
table.ContainsKey typeName,
283+
$"Provided type '{typeName}' under concurrently-created namespace '{ns}' was stranded.")
284+
285+
Assert.Equal(expectedTypes.Length, nsEntity.ModuleOrNamespaceType.AllEntities |> Seq.length)
286+
287+
// #20020: AddModuleOrNamespaceByMutation and AddProvidedTypeEntity share the 'entities' field; concurrent appends must not drop any entity.
288+
[<Fact>]
289+
let ``Concurrent AddModuleOrNamespaceByMutation and AddProvidedTypeEntity never lose an append`` () =
290+
for _ in 1..5 do
291+
let mtyp = Construct.NewEmptyModuleOrNamespaceType(Namespace true)
292+
let appendCount = 80
293+
use barrier = new System.Threading.Barrier(appendCount)
294+
295+
let threads =
296+
[ for i in 0 .. appendCount - 1 ->
297+
let entity = newNamedEntity $"E{i}"
298+
299+
System.Threading.Thread(fun () ->
300+
barrier.SignalAndWait()
301+
302+
if i % 2 = 0 then
303+
mtyp.AddModuleOrNamespaceByMutation entity
304+
else
305+
mtyp.AddProvidedTypeEntity entity) ]
306+
307+
threads |> List.iter (fun t -> t.Start())
308+
threads |> List.iter (fun t -> t.Join())
309+
310+
Assert.Equal(appendCount, mtyp.AllEntities |> Seq.length)

0 commit comments

Comments
 (0)