Skip to content

IL: add ILPreNamespace, make ILPreTypeDef creation lazy - #20092

Open
auduchinok wants to merge 8 commits into
dotnet:mainfrom
auduchinok:il-pre-namespace
Open

IL: add ILPreNamespace, make ILPreTypeDef creation lazy#20092
auduchinok wants to merge 8 commits into
dotnet:mainfrom
auduchinok:il-pre-namespace

Conversation

@auduchinok

@auduchinok auduchinok commented Jul 28, 2026

Copy link
Copy Markdown
Member

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):

Project main 8c0e444de branch + other PRs vs main
Console — 168 refs / 3 src 77.7 MB 59.9 MB −17.8 (−22.9 %)
FSharp.Common — 486 refs / 58 src 1319.2 MB 766.0 MB −553.2 (−41.9 %)
FCS — 124 refs / 397 src 2308.7 MB 2289.2 MB noise

Type defs actually read out of the references:

Project top-level TypeDef rows read before constraint fix after namespace levels realised
Console — 168 refs 3 905 3 905 / 3 905 (100 %) 2 280 / 3 905 (58 %) 428 → 300
FSharp.Common — 486 refs 106 980 107 356 (≈100 %) † 34 877 / 106 980 (33 %) 11 262 → 3 964
FCS — 124 refs not instrumented

† 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):

Project main: time (min / median) branch: time (min / median) main: allocated branch: allocated
Console — 168 refs / 3 src 109 / 122 ms 95 / 110 ms (−13 % / −10 %) 110.2 MB 96.4 MB (−12.5 %)
FSharp.Common — 486 refs / 58 src 1832 / 2027 ms 1699 / 1968 ms (−7 % / −3 %) 3167 MB 2670 MB (−15.7 %)
FCS — 124 refs / 397 src 3060 ms (min) 2906 ms (min) 14 347 MB 14 340 MB (−0 %)

@auduchinok
auduchinok requested a review from a team as a code owner July 28, 2026 20:09
@github-actions github-actions Bot added the AI-Tooling-Check-Scanned-Clean Tooling check: diff analyzed, no interesting infrastructure files label Jul 28, 2026
@github-actions github-actions Bot added the ⚠️ Affects-Compiler-Output Tooling check: PR touches IL emission or codegen label Jul 30, 2026
@github-actions

Copy link
Copy Markdown
Contributor

🔍 Tooling Safety Check — Affects-Compiler-Output
Affects-Compiler-Output: modifies IL reader (il.fs, ilread.fs), import, and static linking

Generated by PR Tooling Safety Check · opus46 3M ·

@auduchinok

Copy link
Copy Markdown
Member Author

This is ready.

auduchinok and others added 8 commits July 30, 2026 15:16
Hold 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>

@T-Gro T-Gro left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 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: DelayInitValue and ILTypeDefs.RealiseNamespaces are correct double-checked locking (volatile stores, non-cached exceptions). They lock this while DelayInitArrayMap locks a private syncObj, so the two paths don't interfere.
  • Enumeration order: AllPreTypeDefs regroups by namespace only for tables built via mkILTypeDefsGroupedComputed (the metadata reader). Flat tables (mkILTypeDefsFromArray, as used by FSI/ilreflect for generated modules) have no child pre-namespaces, so GetArray() order is preserved. The one read-order-sensitive consumer, StaticLinking, now sorts by MetadataIndex. ✓
  • ImportILAssemblyExportedType: moving FindByName inside the try that maps KeyNotFoundException to the friendly impReferenceToDllRequiredByAssembly error is a slight improvement over the old path (which let KeyNotFoundException escape), not a regression.
  • TypedTree.DemangledModuleOrNamespaceName and NameResolution reorderings are semantically equivalent to the originals while avoiding a forced read.
  • noTypars singleton: 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.

// namespaces lazily - to find nothing.
let addConstraintSources (ia: ImportedAssembly) =
if ia.FSharpViewOfMetadata.IsFSharp then
addConstraintSources ia

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Non-blocking: this inner addConstraintSources (line 2347) is a non-rec binding, so this call resolves to the module-level addConstraintSources (line 495) — which is the intent, and correct as written. It's a mild footgun though: reusing the exact same name means a future refactor that adds rec, or that hoists/renames the outer function, would silently turn this into unbounded self-recursion. A distinct name (e.g. addConstraintSourcesIfFSharp) would make the delegation explicit and remove the trap.

@T-Gro T-Gro added the AI-reviewed PR reviewed by AI review council label Aug 3, 2026
@T-Gro
T-Gro self-requested a review August 3, 2026 19:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

⚠️ Affects-Compiler-Output Tooling check: PR touches IL emission or codegen AI-reviewed PR reviewed by AI review council AI-Tooling-Check-Scanned-Clean Tooling check: diff analyzed, no interesting infrastructure files

Projects

Status: New

Development

Successfully merging this pull request may close these issues.

2 participants