Skip to content

Publish NavigableContainer's cases - #20529

Open
xperiandri wants to merge 3 commits into
dotnet:mainfrom
xperiandri:feature/navigable-container-cases
Open

xperiandri wants to merge 3 commits into
dotnet:mainfrom
xperiandri:feature/navigable-container-cases

Conversation

@xperiandri

Copy link
Copy Markdown
Contributor

NavigateTo.GetNavigableItems returns items a consumer can read but not rebuild: the signature seals
NavigableContainer and hides its File and Container cases. That is enough to display a result, and not
enough to carry one out of the process that produced it. Visual Studio's Navigate To is to keep each file's
navigable items on disk between sessions, as Roslyn keeps its syntax index, and reading them back means
constructing containers.

The change

  • NavigableContainer publishes File of fileName: string and Container of info: NavigableContainerInfo. Its
    members Type, FullName and Name are unchanged.

  • Container carried a three-element tuple, containerType * nameParts * parent. Published as it was, the tuple
    would be fixed into the API, so it becomes a record with named fields first:
    NavigableContainerInfo { ContainerType; NameParts; Parent }.

  • The record is a [<Struct>]. A struct record lies inside the case exactly as the tuple's fields did — one
    allocation per container — where a reference record adds an object per container and an indirection on every
    access. Building a file and three nested containers a million times (x64, .NET 11, fsi --optimize+):

    Container payload bytes per chain
    tuple (before) 144
    [<Struct>] record 144
    reference record 216

    A struct union is not an option: Parent is recursive.

The change is additive: the surface-area baseline only gains lines. NavigableContainer keeps its structural
equality and comparison, which the struct record derives as well.

SurfaceAreaTest passes against the updated FSharp.Compiler.Service.SurfaceArea.netstandard20.bsl. The
consumer, a persistent cache for Navigate To in FSharp.Editor, is a separate PR that builds on this one.

🤖 Generated with Claude Code

@github-actions

github-actions Bot commented Sep 11, 2026 •

Copy link
Copy Markdown
Contributor

✅ Release notes checked


✅ Found changes and release notes in following paths:

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

xperiandri added a commit to xperiandri/fsharp that referenced this pull request Sep 11, 2026
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@github-actions github-actions Bot added the ⚠️ Affects-Design-Time Tooling check: PR touches type providers or dependency manager label Sep 11, 2026
@github-actions

This comment has been minimized.

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

🤖 🕵️ AI review — verify independently.

and NavigableContainer =
| File of fileName: string
| Container of containerType: NavigableContainerType * nameParts: string list * parent: NavigableContainer
| Container of info: NavigableContainerInfo

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.

🤖 🕵️ [P2] Ordered-container comparisons now box each NavigableContainerInfo — 120 bytes per comparison at depth three, versus 0 with the previous representation.

open System
open System.Collections.Generic
open FSharp.Compiler.EditorServices

let chain file =
    [1..3] |> List.fold (fun parent _ ->
        NavigableContainer.Container {
            ContainerType = NavigableContainerType.Module
            NameParts = ["M"]
            Parent = parent
        }) (NavigableContainer.File file)

let a, b = chain "a.fs", chain "b.fs"
let comparer = Comparer<NavigableContainer>.Default
for _ in 1..10000 do comparer.Compare(a, b) |> ignore

let allocated =
    let before = GC.GetAllocatedBytesForCurrentThread()
    for _ in 1..100000 do comparer.Compare(a, b) |> ignore
    GC.GetAllocatedBytesForCurrentThread() - before
printfn "%d bytes" allocated // 12000000 bytes

@xperiandri xperiandri Sep 26, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

The allocation is real and the cause is not the struct record itself: F#'s structural comparison for the union reaches its field through generic comparison, which boxes a struct payload. Implementing IComparable<NavigableContainerInfo> would not remove it either, since generic comparison boxes to IComparable before it ever sees a typed implementation.

What it costs in practice is nothing, because nothing orders these. NavigateTo.GetNavigableItems returns an array in traversal order; the editor reads a container only for FullName and Name, and the ordering Navigate To does is over the strings ComputeSecondarySort builds, never over items or containers. The 12 MB in the repro comes from calling Comparer<NavigableContainer>.Default directly, which no caller does — the comparison exists only because the type is structurally comparable by default.

That leaves two honest ways to make it zero again, and they trade against what this PR is for:

  1. [<NoComparison>] on the container. Ordering containers is meaningless anyway, and this is the change that removes the cost outright — but CompareTo is in the surface-area baseline today, so it is a breaking change for anyone who took the default.
  2. Fields back on the case - Container of containerType: NavigableContainerType * nameParts: string list * parent: NavigableContainer. One allocation, no boxing, fields still named in a match. What goes is info.NameParts outside a match, and NavigableContainerInfo as a type to pass around.

I would rather not leave a documented allocation in a published API on the strength of "no one calls it", so I will take one of the two. Leaning to 2, since it keeps the comparison the baseline already promises.

@T-Gro
T-Gro self-requested a review September 14, 2026 14:20
@T-Gro T-Gro added the AI-reviewed PR reviewed by AI review council label Sep 14, 2026
xperiandri added a commit to xperiandri/fsharp that referenced this pull request Sep 22, 2026
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@xperiandri
xperiandri force-pushed the feature/navigable-container-cases branch from bd019a9 to 2e4c723 Compare September 22, 2026 11:34
@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

@github-actions

Copy link
Copy Markdown
Contributor

🔍 Tooling Safety Check — Affects-Design-Time
Affects-Design-Time: Changes FCS or Visual Studio editor behavior.

Generated by PR Tooling Safety Check · gpt56 2.5M · ◷

xperiandri and others added 2 commits September 26, 2026 03:00
`NavigateTo.GetNavigableItems` hands back items whose `Container` can be read
but not constructed: the signature seals the type and hides the `File` and
`Container` cases. That is enough to display a result and not enough to carry
one across a process boundary, which is what caching navigable items on disk
needs — the reader has to rebuild the container it deserialized.

Publish both cases. While the shape is still private, replace `Container`'s
three-element tuple with a named record so the parts have names at the point of
use, and make it a struct: a struct record is laid out inside the case exactly
as the tuple was, so this costs nothing. Building a file plus three nested
containers a million times allocates 144 bytes per chain either way, where a
reference record would take 216.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Copilot AI lite review requested due to automatic review settings September 26, 2026 01:00
@xperiandri
xperiandri force-pushed the feature/navigable-container-cases branch from 2e4c723 to fe5c029 Compare September 26, 2026 01:00

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Copilot review overview

🟡 Changes recommended

Remove the duplicate adjacent release-note entry.

Review effort: Lite
Findings: 1 Low severity

Open (1)
What changed in this PR

Publishes NavigableContainer cases and introduces a named struct payload for reconstructing persisted Navigate To results.

Changes:

  • Exposes File and Container cases.
  • Adds NavigableContainerInfo.
  • Updates implementation, API baseline, and release notes.
File Description
tests/​FSharp.Compiler.Service.Tests/​FSharp.Compiler.Service.SurfaceArea.netstandard20.bsl Updates the API baseline.
src/​Compiler/​Service/​ServiceNavigation.fsi Publishes the new API declarations.
src/​Compiler/​Service/​ServiceNavigation.fs Updates container representation and construction.
docs/​release-notes/​.FSharp.Compiler.Service/​11.0.100.md Documents the API change.

Comment on lines +212 to +213
* `NavigableContainer` publishes its `File` and `Container` cases, so a consumer of `NavigateTo.GetNavigableItems` can take a container apart and rebuild one — needed to cache navigable items outside the process that produced them. `Container` now carries a `[<Struct>]` record, `NavigableContainerInfo`, in place of its three-element tuple: the fields gain names at no cost, since a struct record is laid out inside the case exactly as the tuple was.
* `NavigableContainer` publishes its `File` and `Container` cases, so a consumer of `NavigateTo.GetNavigableItems` can take a container apart and rebuild one — needed to cache navigable items outside the process that produced them. `Container` now carries a `[<Struct>]` record, `NavigableContainerInfo`, in place of its three-element tuple: the fields gain names at no cost, since a struct record is laid out inside the case exactly as the tuple was. ([PR #20529](https://github.com/dotnet/fsharp/pull/20529))

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 5aaa231309: the unlinked copy is gone. Linking the note added a second line instead of editing the first.

The surviving entry also moved out of 11.0.100, which shipped while this PR was in review, into 11.0.200 — the version main now collects notes in.

Linking the note added a copy of it instead of editing the first, and `main` has since
opened 11.0.200 for SDK 11.0.200, leaving 11.0.100 shipped.

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

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

⚠️ Affects-Design-Time Tooling check: PR touches type providers or dependency manager AI-reviewed PR reviewed by AI review council

Projects

Status: New

Development

Successfully merging this pull request may close these issues.

3 participants