Skip to content

Calculate Entity.PublicPath instead of storing - #20285

Merged
T-Gro merged 7 commits into
dotnet:mainfrom
auduchinok:il-entityPubPath
Aug 26, 2026
Merged

Calculate Entity.PublicPath instead of storing#20285
T-Gro merged 7 commits into
dotnet:mainfrom
auduchinok:il-entityPubPath

Conversation

@auduchinok

Copy link
Copy Markdown
Member

PublicPath becomes a struct over the enclosing CompilationPath and the name, so Entity.PublicPath
derives one without allocating and the field goes away. Two consequences:

  • Custom equality. Derived equality would also compare the enclosing path's ILScopeRef and
    SyntaxAccess — stricter than the flat string[] comparison it replaces. It compares the mangled path
    and name only, so pubPathEq keeps its meaning; EqualsFullPath compares against the flat form without
    building it.
  • voption accessors, so a struct payload does not allocate per lookup.

Pickle format unchanged: writers derive the flat path, readers consume the index and discard it.

One project held open (mean of 3 fresh processes)

Project Before After Retained
consoleapp 33.84 MB 33.64 MB -0.20 MB (-0.58%)
Oxpecker 72.83 MB 72.20 MB -0.63 MB (-0.86%)
IcedTasks 50.14 MB 49.89 MB -0.26 MB (-0.51%)
FsToolkit 75.39 MB 75.07 MB -0.32 MB (-0.42%)
Fantomas.Benchmarks 64.77 MB 64.52 MB -0.25 MB (-0.39%)
Prime 108.37 MB 108.15 MB -0.22 MB (-0.20%)
Fantomas.Core 109.50 MB 109.06 MB -0.45 MB (-0.41%)
Fantomas.Core.Tests 141.95 MB 141.49 MB -0.46 MB (-0.32%)
FSharp.Common 297.67 MB 295.00 MB -2.68 MB (-0.90%)
fcs 1273.30 MB 1273.14 MB -0.16 MB (not established)

A whole solution held open at once (mean of 2 passes)

Solution Before After Retained
resharper 1746.27 MB 1725.58 MB -20.69 MB (-1.18%)
fcsrepo 2557.91 MB 2552.92 MB -4.99 MB (-0.20%)
fantomas 690.18 MB 688.21 MB -1.97 MB (-0.29%)
oxpecker 254.06 MB 249.75 MB -4.32 MB (-1.70%)
fstoolkit 238.00 MB 237.43 MB -0.57 MB (not established)
icedtasks 212.48 MB 211.33 MB -1.15 MB (-0.54%)
prime 231.86 MB 231.56 MB -0.31 MB (-0.13%)
consoleapp 33.82 MB 33.62 MB -0.20 MB (-0.58%)

The saving is per entity, so it multiplies with the number of projects each importing their own copy.

…of storing it

entity_pubpath duplicated data the entity already had: every construction site set
it to exactly the enclosing compilation path plus the entity's own logical name, as
the REVIEW comment on the field had noted. Storing it cost an option, a PubPath and
a fresh string[] per entity — about 100 bytes each, 2.6 MB on a 489-reference
project — and the pickled form was read back into a field nothing needed.

PublicPath becomes a struct over the enclosing CompilationPath and the name, so
Entity.PublicPath can produce one without allocating, and the field is gone. The
struct carries custom equality over the mangled path and the name only: derived
equality would also compare the enclosing path's ILScopeRef and SyntaxAccess,
which is stricter than the flat string[] comparison it replaces. Accessors return
voption so a struct payload does not force an allocation per lookup.

The pickle format is unchanged: writers derive the flat path, readers consume the
index and discard it, so the pubpath table no longer builds PublicPath values.

Derivation is exact at all four construction sites, and the only writes to
entity_cpath and entity_logical_name after construction are in Link, which copies
both from one source entity, so a derived path cannot drift from a stored one.

Retained memory after ParseAndCheckProject drops 0.12-2.62 MB per project
(-0.18% to -0.84%) across the measurement suite.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@github-actions

github-actions Bot commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

❗ Release notes required

You can open this PR in browser to add release notes: open in github.dev


✅ Found changes and release notes in following paths:

Change path Release notes path Description
`src/Compiler` docs/release-notes/.FSharp.Compiler.Service/11.0.100.md

@github-actions github-actions Bot added the AI-Tooling-Check-Scanned-Clean Tooling check: diff analyzed, no interesting infrastructure files label Aug 18, 2026
T-Gro

This comment was marked as outdated.

@T-Gro T-Gro added the AI-reviewed PR reviewed by AI review council label Aug 19, 2026
@T-Gro
T-Gro self-requested a review August 19, 2026 09:27

@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.

🤖🕵️ LGTM ✅

Dimensions covered (expand)
  • Invariant equivalence: computed PublicPath ⇔ former stored entity_pubpath
  • Custom equality / hashing semantics on the new struct
  • Pickle format stability (read + write, both directions)
  • Allocation trade-off (retained ↓ vs transient ↑)
  • Self-host / build validation

Comment thread src/Compiler/TypedTree/TypedTree.fs Outdated
@github-project-automation github-project-automation Bot moved this from New to In Progress in F# Compiler and Tooling Aug 20, 2026
Comment thread src/Compiler/TypedTree/TypedTree.fs Outdated
pubPathEq ended in a call to PublicPath.Equals, and fslibRefEq in one to EqualsFullPath. A body
ending in a struct member call is not inferred to make no critical tailcalls, and that inference
propagates to callers: primEntityRefEq gained a .tail prefix, and from there so did tyconRefEq,
tcrefAEquiv, HasHeadType and the TypeTesters predicates. A tail-prefixed call is never inlined,
which cost 12% checking a two-project graph. Comparing the access paths in a plain function keeps
the inference, and builds neither a MangledPath list nor a FullPath array.

PublicPath no longer carries equality at all. Its only comparison was an implicit = on a
PublicPath voption in Exprs.fs, which boxed both sides; that now calls pubPathEq, so structural
equality on a PublicPath is a compile error rather than a silently boxing comparison.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SSWUAYfpj82BHvzpkUCy8Z
Comment thread src/Compiler/TypedTree/TypedTreeBasics.fs
@T-Gro
T-Gro enabled auto-merge (squash) August 21, 2026 11:30
@github-actions

Copy link
Copy Markdown
Contributor

🔍 Tooling Safety Check — Affects-Bootstrap, Affects-Compiler-Output
Affects-Bootstrap: Modifies TypedTree core data structures used by bootstrap compiler
Affects-Compiler-Output: Changes TypedTreePickle serialization (PublicPath encoding)

Generated by PR Tooling Safety Check · opus46 3.6M ·

@github-actions github-actions Bot added ⚠️ Affects-Compiler-Output Tooling check: PR touches IL emission or codegen ⚠️ Affects-Bootstrap Tooling check: PR touches compiler bootstrap chain labels Aug 21, 2026
auto-merge was automatically disabled August 26, 2026 11:00

Head branch was pushed to by a user without write access

@T-Gro
T-Gro merged commit a04f7d6 into dotnet:main Aug 26, 2026
52 checks passed
@github-project-automation github-project-automation Bot moved this from In Progress to Done in F# Compiler and Tooling Aug 26, 2026
@auduchinok
auduchinok deleted the il-entityPubPath branch August 26, 2026 14:22
nojaf pushed a commit to nojaf/fsharp that referenced this pull request Aug 26, 2026
* IL: derive an entity's public path from its compilation path instead of storing it

entity_pubpath duplicated data the entity already had: every construction site set
it to exactly the enclosing compilation path plus the entity's own logical name, as
the REVIEW comment on the field had noted. Storing it cost an option, a PubPath and
a fresh string[] per entity — about 100 bytes each, 2.6 MB on a 489-reference
project — and the pickled form was read back into a field nothing needed.

PublicPath becomes a struct over the enclosing CompilationPath and the name, so
Entity.PublicPath can produce one without allocating, and the field is gone. The
struct carries custom equality over the mangled path and the name only: derived
equality would also compare the enclosing path's ILScopeRef and SyntaxAccess,
which is stricter than the flat string[] comparison it replaces. Accessors return
voption so a struct payload does not force an allocation per lookup.

The pickle format is unchanged: writers derive the flat path, readers consume the
index and discard it, so the pubpath table no longer builds PublicPath values.

Derivation is exact at all four construction sites, and the only writes to
entity_cpath and entity_logical_name after construction are in Link, which copies
both from one source entity, so a derived path cannot drift from a stored one.

Retained memory after ParseAndCheckProject drops 0.12-2.62 MB per project
(-0.18% to -0.84%) across the measurement suite.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Release notes

* Release notes

* Review

* Compare public paths with a function, not a struct member

pubPathEq ended in a call to PublicPath.Equals, and fslibRefEq in one to EqualsFullPath. A body
ending in a struct member call is not inferred to make no critical tailcalls, and that inference
propagates to callers: primEntityRefEq gained a .tail prefix, and from there so did tyconRefEq,
tcrefAEquiv, HasHeadType and the TypeTesters predicates. A tail-prefixed call is never inlined,
which cost 12% checking a two-project graph. Comparing the access paths in a plain function keeps
the inference, and builds neither a MangledPath list nor a FullPath array.

PublicPath no longer carries equality at all. Its only comparison was an implicit = on a
PublicPath voption in Exprs.fs, which boxed both sides; that now calls pubPathEq, so structural
equality on a PublicPath is a compile error rather than a silently boxing comparison.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SSWUAYfpj82BHvzpkUCy8Z

* Fantomas

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: Tomas Grosup <Tomas.Grosup@gmail.com>
T-Gro pushed a commit that referenced this pull request Aug 27, 2026
fslibRefEq compared nlr1.Path against path2.FullPath, but PublicPath.FullPath
(after #20285 derives paths instead of storing them) builds a MangledPath list
plus a string[] on every call. This runs in the fslib-compile entity/val-ref
equality hot path. Walk the enclosing AccessPath as a list while indexing
nlr1.Path as an array instead: same O(N), zero allocation, mirroring the
pubPathEq treatment #20285 already applied to its sibling.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
T-Gro added a commit that referenced this pull request Aug 27, 2026
* Rotate [<return: X>] attributes during binding normalization

Since #19738 the parser moved attributes written as [<return: X>] in
front of a binding out of SynBinding.attributes and into
SynValInfo.retInfo. The untyped tree then reported no attributes for
source that visibly has one, which every consumer of the parse tree
sees: formatters, analyzers, source generators and refactoring tools.

The move was also lossy. The attribute list range narrowed from the
[< >] span to the attribute alone, and all return attributes were
collected into a single synthesized list, so [<return: A; return: B>]
and [<return: A>][<return: B>] produced identical trees. Neither can be
printed back to its original form.

Do the rotation in BindingNormalization.NormalizeBinding instead, the
single funnel from SynBinding to NormalizedBinding and already a
lowering step. Everything downstream still reads retInfo as the single
source of truth, so the fixes for #17904 and #19020 are unchanged, but
the parse tree again says what was written.

Add parse baselines for both the common [<return: Struct>] partial
active pattern and for attribute grouping, neither of which had any
coverage before.

* Add release note

* Calculate Entity.PublicPath instead of storing (#20285)

* IL: derive an entity's public path from its compilation path instead of storing it

entity_pubpath duplicated data the entity already had: every construction site set
it to exactly the enclosing compilation path plus the entity's own logical name, as
the REVIEW comment on the field had noted. Storing it cost an option, a PubPath and
a fresh string[] per entity — about 100 bytes each, 2.6 MB on a 489-reference
project — and the pickled form was read back into a field nothing needed.

PublicPath becomes a struct over the enclosing CompilationPath and the name, so
Entity.PublicPath can produce one without allocating, and the field is gone. The
struct carries custom equality over the mangled path and the name only: derived
equality would also compare the enclosing path's ILScopeRef and SyntaxAccess,
which is stricter than the flat string[] comparison it replaces. Accessors return
voption so a struct payload does not force an allocation per lookup.

The pickle format is unchanged: writers derive the flat path, readers consume the
index and discard it, so the pubpath table no longer builds PublicPath values.

Derivation is exact at all four construction sites, and the only writes to
entity_cpath and entity_logical_name after construction are in Link, which copies
both from one source entity, so a derived path cannot drift from a stored one.

Retained memory after ParseAndCheckProject drops 0.12-2.62 MB per project
(-0.18% to -0.84%) across the measurement suite.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Release notes

* Release notes

* Review

* Compare public paths with a function, not a struct member

pubPathEq ended in a call to PublicPath.Equals, and fslibRefEq in one to EqualsFullPath. A body
ending in a struct member call is not inferred to make no critical tailcalls, and that inference
propagates to callers: primEntityRefEq gained a .tail prefix, and from there so did tyconRefEq,
tcrefAEquiv, HasHeadType and the TypeTesters predicates. A tail-prefixed call is never inlined,
which cost 12% checking a two-project graph. Comparing the access paths in a plain function keeps
the inference, and builds neither a MangledPath list nor a FullPath array.

PublicPath no longer carries equality at all. Its only comparison was an implicit = on a
PublicPath voption in Exprs.fs, which boxed both sides; that now calls pubPathEq, so structural
equality on a PublicPath is a compile error rather than a silently boxing comparison.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SSWUAYfpj82BHvzpkUCy8Z

* Fantomas

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: Tomas Grosup <Tomas.Grosup@gmail.com>

* Cleanup: use `FindByNameAndArity` guard in `CheckILBaseCall` to avoid exception-based control flow for inherited IL methods (#20272)

* Fix inherited IL base-call crash and add release notes

* Address review: inherit the IL method in the 20264 test, resolve via FindByNameAndArity, and keep a try/with around signature matching.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

* PR review fix

* Clarify IL base-call check notes, split #20264 test

Release notes now specify skipping `resolveILMethodRefWithRescope` only when the method isn't on the immediate IL type, not via exception flow. The `SimpleInteropTests.fs` test for #20264 is split: one test checks inherited non-abstract IL base method calls on generics succeed, the other checks abstract base method calls on generics fail with FS1201. Test names and code are now clearer and more targeted.

---------

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

* Reject bitwise operators on char-backed enums (#11785) (#20322)

* Remove always-on IndexerNotationWithoutDot language feature flag (#20319)

* Flatten always-on IndexerNotationWithoutDot: remove flag and collapse enforcement sites

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Remove orphaned IndexerNotationWithoutDot diagnostic strings and regenerate xlf

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Sync xlf files: remove trans-units for FSComp keys deleted in this PR

---------

Co-authored-by: Copilot <copilot@github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: T-Gro <t-gro@users.noreply.github.com>

* Replace the stringified pattern-match memo key with a typed one (#20337)

* Replace the stringified pattern-match memo key with a typed one

The memo added in #20244 keyed residual match states by concatenating
paths, pattern node ids and bound expressions into a string, then
compared those strings. Replace it with structural keys:

- PathKey / BoundExprKey / FrontierKey / MemoKey instead of string
  concatenation, so equality is structural rather than textual.
- Record field and union case keys now carry the declaring tycon stamp,
  so same-named fields of different types can no longer fuse.
- MemoEntry replaces the (int ref * Lazy<bool> * Lazy<_>) tuple.
- The diagnostics/codegen distinction is a JoinPromotion argument rather
  than being inferred from warnOnIncomplete, which happened to coincide.

Behaviour preserving: emitted assembly size and full IL fingerprint are
identical to main for the issue #18425 repro at N=8..40.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 86c47998-fe39-4b68-8b80-c72b72fc9efb

* Add release note

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 86c47998-fe39-4b68-8b80-c72b72fc9efb

* Keep the memo machinery private and drop a redundant pass

CompilePatternBasic is only called from CompilePattern in this file and
is not in the signature file, so it and JoinPromotion can be private
like the key types already are.

Also name the pattern node id in FrontierKey (matching the existing
ClauseNumber alias) and build the bound-expression key list in one pass
instead of Map.toList followed by List.map. Map enumerates in ascending
key order, so the key is unchanged; verified IL-identical.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 86c47998-fe39-4b68-8b80-c72b72fc9efb

* Document the key types

State the safety invariant on the path key directly (equal keys imply
pathEq, so being finer only costs memo misses), and give FrontierKey and
MemoEntry the same brief purpose comments the neighbouring types have.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 86c47998-fe39-4b68-8b80-c72b72fc9efb

* Record what the size guard actually catches

Red-green checked by disabling promotion: the N=32 input then runs for
383s and is OOM-killed rather than emitting something slightly over the
bound, so the exact constant does not matter.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 86c47998-fe39-4b68-8b80-c72b72fc9efb

* Drop the release note

This is an internal refactoring with no observable behaviour change, and
the latent over-fusion it fixes was introduced by #20244 in this same
release, so no shipped compiler could exhibit it. Labelled NO_RELEASE_NOTES.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 86c47998-fe39-4b68-8b80-c72b72fc9efb

---------

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 86c47998-fe39-4b68-8b80-c72b72fc9efb

* Add postmortem

---------

Co-authored-by: Eugene Auduchinok <eugene.auduchinok@jetbrains.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: Tomas Grosup <Tomas.Grosup@gmail.com>
Co-authored-by: Andrii Chebukin <XperiAndri@Outlook.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Edgar Gonzalez <edgargonzalez.info@gmail.com>
Co-authored-by: Copilot <copilot@github.com>
Co-authored-by: T-Gro <t-gro@users.noreply.github.com>
Copilot-Session: 86c47998-fe39-4b68-8b80-c72b72fc9efb
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

⚠️ Affects-Bootstrap Tooling check: PR touches compiler bootstrap chain ⚠️ 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

Archived in project

Development

Successfully merging this pull request may close these issues.

2 participants