Uh oh!
There was an error while loading. Please reload this page.
IL: add ILPreNamespace, make ILPreTypeDef creation lazy - #20092
Conversation
5ddd1ca to
28c10e2Compare🔍 Tooling Safety Check — Affects-Compiler-Output
|
28c10e2 to
2e7cbc0Compareauduchinok
commented
Jul 30, 2026
This is ready. |
2e7cbc0 to
8029661Compare
T-Gro
left a comment
There was a problem hiding this comment.
🤖 This review was generated by AI (@expert-reviewer agent). Findings may contain inaccuracies — please verify independently.
Reviewed the lazy-namespace refactoring end to end (il.fs/il.fsi, ilread.fs, import.fs, illib.fs, and the order-sensitive consumers). No correctness defects found — the change is well engineered and the tradeoffs are documented in-line. Notes on what I checked:
- Concurrency:
DelayInitValueandILTypeDefs.RealiseNamespacesare correct double-checked locking (volatile stores, non-cached exceptions). They lockthiswhileDelayInitArrayMaplocks a privatesyncObj, so the two paths don't interfere. - Enumeration order:
AllPreTypeDefsregroups by namespace only for tables built viamkILTypeDefsGroupedComputed(the metadata reader). Flat tables (mkILTypeDefsFromArray, as used by FSI/ilreflect for generated modules) have no child pre-namespaces, soGetArray()order is preserved. The one read-order-sensitive consumer,StaticLinking, now sorts byMetadataIndex. ✓ ImportILAssemblyExportedType: movingFindByNameinside thetrythat mapsKeyNotFoundExceptionto the friendlyimpReferenceToDllRequiredByAssemblyerror is a slight improvement over the old path (which letKeyNotFoundExceptionescape), not a regression.TypedTree.DemangledModuleOrNamespaceNameandNameResolutionreorderings are semantically equivalent to the originals while avoiding a forced read.noTyparssingleton:LazyWithContext.NotLazy []is an already-forced, never-mutated record, so sharing one instance across all no-typar entities is safe.
One non-blocking readability note inline.
Uh oh!
There was an error while loading. Please reload this page.
8029661 to
7a92a6dCompare❗ Release notes requiredYou can open this PR in browser to add release notes: open in github.dev
|
T-Gro
commented
Aug 12, 2026
This looks great. |
f6e4a9d to
0beabc7CompareHold the child namespaces + their by-name lookup in a single InterruptibleLazy<struct (ILPreNamespace[] * Dictionary<..>)> instead of two separate lazies, and share one pre-computed empty instance for namespace-less levels (every nested-type container - the vast majority of ILTypeDefs). On a single-file FCS check against a project with ~486 references this cut the namespace InterruptibleLazy wrappers from ~171,600 to ~8,800 and the retained IL-namespace machinery from ~16.5 MB to ~11.3 MB. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Follow-up cleanup on the ILPreNamespace work: - ImportILNamespaceLevel now buckets a level's flat entries and child pre-namespaces in one pass instead of via multisetDiscriminateAndMap - drop the now-unused readBlobHeapAsSplitTypeName/seekReadPreTypeDef and the mkILPreTypeDefEntry signature entry - TryFindPreTypeDef is a self-recursive member; simplify namespacesOf - flattening a grouped table (AsArray/AsList/the enumerator) restores the TypeDef row order, which static linking relies on to emit the types of a --standalone assembly in the reader's order - tests for laziness, lookup, hybrid levels, duplicate namespace nodes, imported entity order and metadata order of a read module Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Flattening a grouped table walks namespace by namespace, which is not the TypeDef row order when a namespace is split across the table. Doing the restoration inside ILTypeDefs cost a flag in the namespaces payload plus a sort in AllPreTypeDefs, in a memory-critical type; static linking is the only order-sensitive consumer, and one stable sort by MetadataIndex there covers it. Verified on a --standalone build: all 296 of FSharp.Core's top-level types are emitted in FSharp.Core's metadata order. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Temporary commit of the in-progress work on top of 9e331e8: - il.fs/il.fsi: one namespace-bucketing implementation (ILPreNamespaceOfEntries) reached both by the grouped reader and, via ilTypeDefsAsNamespaceLevel, by any flat table; DelayInitValue extracted into illib for the three hand-rolled lazies; fixed a race where AsArrayOfPreNamespaces could return an empty array. - import.fs: ImportILNamespaceLevel and its copy of the bucketing removed; ImportILTypeDefs walks the two arrays directly. Plus three unrelated memory tweaks (Nullness.GetFlags, shared noTypars, closure capture in ImportILTypeDef). - CompilerImports.fs: skip addConstraintSources for non-F# CCUs (also ported to its own branch off main, to be proposed and measured separately). - tests: namespace/cancellation tests updated, SurfaceArea baseline. - benchmarks: NamespaceImportBenchmarks with the retained-memory and retain-project probes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
AddModuleOrNamespaceRefsToNameEnv runs over the root namespaces of every referenced assembly, and two checks in it forced each one's contents - which for an imported namespace means importing every type in it: - Entity.DemangledModuleOrNamespaceName forced the ModuleOrNamespaceType only to read its ModuleOrNamespaceKind. Only FSharpModuleWithSuffix demangles, and only a suffixed name can be of that kind, so testing the name first avoids the read. - `modref.IsModule && EntityHasWellKnownAttribute ... AutoOpenAttribute`: IsModule forces the contents while the attribute check only reads entity_attribs, so the operands are swapped. Retained memory when checking ReSharper.FSharp's FSharp.Common (486 references): 874.7 -> 780.4 MB (-10.8%). Framework-only projects are unchanged, since their root namespaces get imported anyway. Also un-skips `Type defs 02 - assembly import`: no type of the reference is imported now, closing dotnet#16166. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The reader tests above them pin the API in isolation; these pin the guarantee it exists for. A synthetic reference assembly records every GetTypeDef, member, nested-type and attribute read, and each test checks a file against it and asserts what came out - so a walk introduced far from the reader (addConstraintSources did exactly that) fails here. What they pin: naming Ns1.A reads the root level and Ns1's own types and nothing else; members, nested types and the base type stay unread until something needs them; a member lookup is what forces the base type's namespace; attributes are read for the types that enter scope, so an open pays for all of a namespace's types and a qualified use does not. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A level is now one shape everywhere: the types declared in it and its child namespaces, each half realised once and independently. Importing a level's types must not read its children, nor the other way round, and a child level needs no ILTypeDefs table of its own. ILPreNamespace becomes an abstract class carrying the name and caching both halves, rather than an interface handing back an ILTypeDefs. A store that knows its namespaces inherits it; ILTypeDefs is left for the two places a level is the declared type - a module's own level and a type's nested types - and its map is keyed by simple name again. That leaves mkILTypeDefsComputed flat, drops mkILTypeDefsAndNamespacesComputed for mkILTypeDefsOfNamespace, and turns mkILTypeDefsGroupedComputed into the one entry point for a store with no namespace structure to hand: namespaced entries plus any namespaces supplied directly, merged so a name coming from both becomes one child. The grouped reader no longer needs a maker to defer building a pre-type-def, because the pre-type-def is now the thing that defers: it holds the name index and reads the string heap through the store's getName. Grouping a table therefore reads namespaces only, and an un-imported namespace pays for neither its types' names nor their defs. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
0beabc7 to
6d95690Compareauduchinok
commented
Aug 12, 2026
@T-Gro It's green 🙂 |
This PR groups lazy type definitions into lazy namespaces structure, allowing to make nested namespaces lazy and defer lazy type defs creation. It improves the custom module readers that we have in Rider.
In addition to that, the PR adds tests asserting the type defs are not eagerly read, which discovered several issues. This PR, #20088, and #20090 all fix these issues. The constraint fix has the most effect for the most users, since without it the type defs were effectively not lazy at all.
The following was measured with the other two PRs included.
Retained memory after ParseAndCheckProject (
retain-project, avg of 3):8c0e444deType defs actually read out of the references:
† Denominator is top-level rows only while the read counter also fires for nested type defs.
ParseAndCheckProject — warm (1 warm-up, then samples with the IL reader cache cleared each time):