From d278c049f51997eb57aa932982afc4d2791b7f00 Mon Sep 17 00:00:00 2001 From: Andrii Chebukin Date: Thu, 10 Sep 2026 12:09:46 +0200 Subject: [PATCH 1/2] Let a consumer take a navigable container apart and rebuild one MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `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 --- .../.FSharp.Compiler.Service/11.0.100.md | 1 + src/Compiler/Service/ServiceNavigation.fs | 53 +++++++++++++++---- src/Compiler/Service/ServiceNavigation.fsi | 20 ++++++- ...iler.Service.SurfaceArea.netstandard20.bsl | 35 ++++++++++++ 4 files changed, 97 insertions(+), 12 deletions(-) diff --git a/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md b/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md index 27f2c0070d7..1b6f6a518be 100644 --- a/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md +++ b/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md @@ -180,6 +180,7 @@ * IL: hold custom attributes in fields rather than a union case ([PR #20287](https://github.com/dotnet/fsharp/pull/20287)) * IL: reuse the cached ILTypeRef in ILTypeInfo.FromType ([PR #20255](https://github.com/dotnet/fsharp/pull/20255)) * Name resolution: group C#-style extension members per `open` and extended type ([PR #20298](https://github.com/dotnet/fsharp/pull/20298)) +* `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 `[]` 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. ### Improved diff --git a/src/Compiler/Service/ServiceNavigation.fs b/src/Compiler/Service/ServiceNavigation.fs index 41d565ebed4..fc8ef0bd1ae 100755 --- a/src/Compiler/Service/ServiceNavigation.fs +++ b/src/Compiler/Service/ServiceNavigation.fs @@ -731,28 +731,36 @@ type NavigableContainerType = | Type | Exception -type NavigableContainer = +[] +type NavigableContainerInfo = + { + ContainerType: NavigableContainerType + NameParts: string list + Parent: NavigableContainer + } + +and NavigableContainer = | File of fileName: string - | Container of containerType: NavigableContainerType * nameParts: string list * parent: NavigableContainer + | Container of info: NavigableContainerInfo member x.FullName = let rec loop acc = function | File _ -> acc - | Container(_, nameParts, parent) -> loop (nameParts @ acc) parent + | Container info -> loop (info.NameParts @ acc) info.Parent loop [] x |> textOfPath member x.Type = match x with | File _ -> NavigableContainerType.File - | Container(t, _, _) -> t + | Container info -> info.ContainerType member x.Name = match x with | File name -> name - | Container(nameParts = []) -> "" - | Container(nameParts = ns) -> ns |> List.last + | Container { NameParts = [] } -> "" + | Container { NameParts = ns } -> ns |> List.last type NavigableItem = { @@ -813,13 +821,24 @@ module NavigateTo = let addExceptionRepr exnRepr isSig container = let (SynExceptionDefnRepr(_, SynUnionCase(ident = SynIdent(id, _)), _, _, _, _)) = exnRepr addIdent NavigableItemKind.Exception id isSig container - NavigableContainer.Container(NavigableContainerType.Exception, [ id.idText ], container) + + NavigableContainer.Container + { + ContainerType = NavigableContainerType.Exception + NameParts = [ id.idText ] + Parent = container + } let addComponentInfo containerType kind (info: SynComponentInfo) isSig container = let lid = info.LongIdent addLongIdent kind lid isSig container - NavigableContainer.Container(containerType, pathOfLid lid, container) + NavigableContainer.Container + { + ContainerType = containerType + NameParts = pathOfLid lid + Parent = container + } let addValSig kind synValSig isSig container = let (SynValSig(ident = SynIdent(id, _))) = synValSig @@ -897,7 +916,14 @@ module NavigateTo = NavigableContainerType.Namespace for decl in decls do - walkSynModuleSigDecl decl (NavigableContainer.Container(ctype, pathOfLid lid, container)) + walkSynModuleSigDecl + decl + (NavigableContainer.Container + { + ContainerType = ctype + NameParts = pathOfLid lid + Parent = container + }) and walkSynModuleSigDecl (decl: SynModuleSigDecl) container = match decl with @@ -956,7 +982,14 @@ module NavigateTo = NavigableContainerType.Namespace for decl in decls do - walkSynModuleDecl decl (NavigableContainer.Container(ctype, pathOfLid lid, container)) + walkSynModuleDecl + decl + (NavigableContainer.Container + { + ContainerType = ctype + NameParts = pathOfLid lid + Parent = container + }) and walkSynModuleDecl (decl: SynModuleDecl) container = match decl with diff --git a/src/Compiler/Service/ServiceNavigation.fsi b/src/Compiler/Service/ServiceNavigation.fsi index cfccd6ef20c..8063a107d91 100755 --- a/src/Compiler/Service/ServiceNavigation.fsi +++ b/src/Compiler/Service/ServiceNavigation.fsi @@ -99,8 +99,24 @@ type NavigableContainerType = | Type | Exception -[] -type NavigableContainer = +/// What a container declared inside a file is, and what encloses it. +[] +type NavigableContainerInfo = + { + /// The kind of container. + ContainerType: NavigableContainerType + + /// The name of the container, one element per dotted part. + NameParts: string list + + /// The container this one is declared in, ending at the file. + Parent: NavigableContainer + } + +and NavigableContainer = + | File of fileName: string + | Container of info: NavigableContainerInfo + /// The kind of container. member Type: NavigableContainerType diff --git a/tests/FSharp.Compiler.Service.Tests/FSharp.Compiler.Service.SurfaceArea.netstandard20.bsl b/tests/FSharp.Compiler.Service.Tests/FSharp.Compiler.Service.SurfaceArea.netstandard20.bsl index 5c9c346b613..ed4cdc088c4 100644 --- a/tests/FSharp.Compiler.Service.Tests/FSharp.Compiler.Service.SurfaceArea.netstandard20.bsl +++ b/tests/FSharp.Compiler.Service.Tests/FSharp.Compiler.Service.SurfaceArea.netstandard20.bsl @@ -3877,9 +3877,25 @@ FSharp.Compiler.EditorServices.ModuleKind: Int32 GetHashCode() FSharp.Compiler.EditorServices.ModuleKind: Int32 GetHashCode(System.Collections.IEqualityComparer) FSharp.Compiler.EditorServices.ModuleKind: System.String ToString() FSharp.Compiler.EditorServices.ModuleKind: Void .ctor(Boolean, Boolean) +FSharp.Compiler.EditorServices.NavigableContainer+Container: FSharp.Compiler.EditorServices.NavigableContainerInfo get_info() +FSharp.Compiler.EditorServices.NavigableContainer+Container: FSharp.Compiler.EditorServices.NavigableContainerInfo info +FSharp.Compiler.EditorServices.NavigableContainer+File: System.String fileName +FSharp.Compiler.EditorServices.NavigableContainer+File: System.String get_fileName() +FSharp.Compiler.EditorServices.NavigableContainer+Tags: Int32 Container +FSharp.Compiler.EditorServices.NavigableContainer+Tags: Int32 File FSharp.Compiler.EditorServices.NavigableContainer: Boolean Equals(FSharp.Compiler.EditorServices.NavigableContainer) +FSharp.Compiler.EditorServices.NavigableContainer: Boolean Equals(FSharp.Compiler.EditorServices.NavigableContainer, System.Collections.IEqualityComparer) FSharp.Compiler.EditorServices.NavigableContainer: Boolean Equals(System.Object) FSharp.Compiler.EditorServices.NavigableContainer: Boolean Equals(System.Object, System.Collections.IEqualityComparer) +FSharp.Compiler.EditorServices.NavigableContainer: Boolean IsContainer +FSharp.Compiler.EditorServices.NavigableContainer: Boolean IsFile +FSharp.Compiler.EditorServices.NavigableContainer: Boolean get_IsContainer() +FSharp.Compiler.EditorServices.NavigableContainer: Boolean get_IsFile() +FSharp.Compiler.EditorServices.NavigableContainer: FSharp.Compiler.EditorServices.NavigableContainer NewContainer(FSharp.Compiler.EditorServices.NavigableContainerInfo) +FSharp.Compiler.EditorServices.NavigableContainer: FSharp.Compiler.EditorServices.NavigableContainer NewFile(System.String) +FSharp.Compiler.EditorServices.NavigableContainer: FSharp.Compiler.EditorServices.NavigableContainer+Container +FSharp.Compiler.EditorServices.NavigableContainer: FSharp.Compiler.EditorServices.NavigableContainer+File +FSharp.Compiler.EditorServices.NavigableContainer: FSharp.Compiler.EditorServices.NavigableContainer+Tags FSharp.Compiler.EditorServices.NavigableContainer: FSharp.Compiler.EditorServices.NavigableContainerType Type FSharp.Compiler.EditorServices.NavigableContainer: FSharp.Compiler.EditorServices.NavigableContainerType get_Type() FSharp.Compiler.EditorServices.NavigableContainer: Int32 CompareTo(FSharp.Compiler.EditorServices.NavigableContainer) @@ -3887,11 +3903,30 @@ FSharp.Compiler.EditorServices.NavigableContainer: Int32 CompareTo(System.Object FSharp.Compiler.EditorServices.NavigableContainer: Int32 CompareTo(System.Object, System.Collections.IComparer) FSharp.Compiler.EditorServices.NavigableContainer: Int32 GetHashCode() FSharp.Compiler.EditorServices.NavigableContainer: Int32 GetHashCode(System.Collections.IEqualityComparer) +FSharp.Compiler.EditorServices.NavigableContainer: Int32 Tag +FSharp.Compiler.EditorServices.NavigableContainer: Int32 get_Tag() FSharp.Compiler.EditorServices.NavigableContainer: System.String FullName FSharp.Compiler.EditorServices.NavigableContainer: System.String Name FSharp.Compiler.EditorServices.NavigableContainer: System.String ToString() FSharp.Compiler.EditorServices.NavigableContainer: System.String get_FullName() FSharp.Compiler.EditorServices.NavigableContainer: System.String get_Name() +FSharp.Compiler.EditorServices.NavigableContainerInfo: Boolean Equals(FSharp.Compiler.EditorServices.NavigableContainerInfo) +FSharp.Compiler.EditorServices.NavigableContainerInfo: Boolean Equals(FSharp.Compiler.EditorServices.NavigableContainerInfo, System.Collections.IEqualityComparer) +FSharp.Compiler.EditorServices.NavigableContainerInfo: Boolean Equals(System.Object) +FSharp.Compiler.EditorServices.NavigableContainerInfo: Boolean Equals(System.Object, System.Collections.IEqualityComparer) +FSharp.Compiler.EditorServices.NavigableContainerInfo: FSharp.Compiler.EditorServices.NavigableContainer Parent +FSharp.Compiler.EditorServices.NavigableContainerInfo: FSharp.Compiler.EditorServices.NavigableContainer get_Parent() +FSharp.Compiler.EditorServices.NavigableContainerInfo: FSharp.Compiler.EditorServices.NavigableContainerType ContainerType +FSharp.Compiler.EditorServices.NavigableContainerInfo: FSharp.Compiler.EditorServices.NavigableContainerType get_ContainerType() +FSharp.Compiler.EditorServices.NavigableContainerInfo: Int32 CompareTo(FSharp.Compiler.EditorServices.NavigableContainerInfo) +FSharp.Compiler.EditorServices.NavigableContainerInfo: Int32 CompareTo(System.Object) +FSharp.Compiler.EditorServices.NavigableContainerInfo: Int32 CompareTo(System.Object, System.Collections.IComparer) +FSharp.Compiler.EditorServices.NavigableContainerInfo: Int32 GetHashCode() +FSharp.Compiler.EditorServices.NavigableContainerInfo: Int32 GetHashCode(System.Collections.IEqualityComparer) +FSharp.Compiler.EditorServices.NavigableContainerInfo: Microsoft.FSharp.Collections.FSharpList`1[System.String] NameParts +FSharp.Compiler.EditorServices.NavigableContainerInfo: Microsoft.FSharp.Collections.FSharpList`1[System.String] get_NameParts() +FSharp.Compiler.EditorServices.NavigableContainerInfo: System.String ToString() +FSharp.Compiler.EditorServices.NavigableContainerInfo: Void .ctor(FSharp.Compiler.EditorServices.NavigableContainerType, Microsoft.FSharp.Collections.FSharpList`1[System.String], FSharp.Compiler.EditorServices.NavigableContainer) FSharp.Compiler.EditorServices.NavigableContainerType+Tags: Int32 Exception FSharp.Compiler.EditorServices.NavigableContainerType+Tags: Int32 File FSharp.Compiler.EditorServices.NavigableContainerType+Tags: Int32 Module From bd019a9873b5a050f5db4058604926534f1d9160 Mon Sep 17 00:00:00 2001 From: Andrii Chebukin Date: Fri, 11 Sep 2026 20:21:00 +0200 Subject: [PATCH 2/2] Link the release note to PR #20529 Co-Authored-By: Claude Opus 5 --- docs/release-notes/.FSharp.Compiler.Service/11.0.100.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md b/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md index 1b6f6a518be..4ef6c375622 100644 --- a/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md +++ b/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md @@ -180,7 +180,7 @@ * IL: hold custom attributes in fields rather than a union case ([PR #20287](https://github.com/dotnet/fsharp/pull/20287)) * IL: reuse the cached ILTypeRef in ILTypeInfo.FromType ([PR #20255](https://github.com/dotnet/fsharp/pull/20255)) * Name resolution: group C#-style extension members per `open` and extended type ([PR #20298](https://github.com/dotnet/fsharp/pull/20298)) -* `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 `[]` 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 `[]` 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)) ### Improved