From 04eda99cbf0b3ab5b67b2d460bd7c85be16477e8 Mon Sep 17 00:00:00 2001 From: Mangel Maxime Date: Wed, 21 Oct 2026 21:25:37 +0200 Subject: [PATCH 1/6] perf(symbols): share symbols per definition, cache queries --- .../.FSharp.Compiler.Service/11.0.200.md | 1 + src/Compiler/Symbols/Exprs.fs | 14 +- src/Compiler/Symbols/Symbols.fs | 267 +++++++++++++----- src/Compiler/Symbols/Symbols.fsi | 13 + 4 files changed, 215 insertions(+), 80 deletions(-) diff --git a/docs/release-notes/.FSharp.Compiler.Service/11.0.200.md b/docs/release-notes/.FSharp.Compiler.Service/11.0.200.md index 17bcd329342..2abc6eb2922 100644 --- a/docs/release-notes/.FSharp.Compiler.Service/11.0.200.md +++ b/docs/release-notes/.FSharp.Compiler.Service/11.0.200.md @@ -37,6 +37,7 @@ ### Changed * Debugger: emit a call for a self tail call in non-optimized builds ([PR #20782](https://github.com/dotnet/fsharp/pull/20782)) +* `FSharpMemberOrFunctionOrValue` and non-generic `FSharpEntity` symbols are shared per definition within a `SymbolEnv`, and cache their parameter groups, attributes, generic parameters, declaring entity and full type. `FSharpMemberOrFunctionOrValue.GetHashCode` uses the definition's stamp. ([PR #TODO](https://github.com/dotnet/fsharp/pull/TODO)) * Remove the unused internal `--test:NoErrorText` switch and its argument-only diagnostic formatting. ([PR #20742](https://github.com/dotnet/fsharp/pull/20742)) * Assign seven previously preview-only features to [F# 11.2](https://fsharp.github.io/fsharp-compiler-docs/release-notes/Language.html#11.2), now the default language version. `default`, `latest`, and `latestmajor` select F# 11.2; explicit `11` and `11.0` retain F# 11.0 behavior. Preview SDK projects use the same compiler default unless `LangVersion` is explicitly set. `FromEndSlicing` remains preview-only. ([PR #20617](https://github.com/dotnet/fsharp/pull/20617)) * Add `SynExceptionDefnReprTrivia` with the range of the `exception` keyword to `SynExceptionDefnRepr`. ([Fantomas Issue #3483](https://github.com/fsprojects/fantomas/issues/3483), [PR #20612](https://github.com/dotnet/fsharp/pull/20612)) diff --git a/src/Compiler/Symbols/Exprs.fs b/src/Compiler/Symbols/Exprs.fs index 443980eae49..b9e9163014a 100644 --- a/src/Compiler/Symbols/Exprs.fs +++ b/src/Compiler/Symbols/Exprs.fs @@ -499,9 +499,9 @@ module FSharpExprConvert = let enclTyArgs, methTyArgs = List.splitAfter numEnclTypeArgs tyargs let witnessArgsR = GetWitnessArgs cenv env vref m tps tyargs // tailcall - ConvObjectModelCallLinear cenv env (isNewObj, FSharpMemberOrFunctionOrValue(cenv, vref), enclTyArgs, methTyArgs, witnessArgsR, callArgs) contf2 + ConvObjectModelCallLinear cenv env (isNewObj, FSharpMemberOrFunctionOrValue.Shared(cenv, vref), enclTyArgs, methTyArgs, witnessArgsR, callArgs) contf2 else - let v = FSharpMemberOrFunctionOrValue(cenv, vref) + let v = FSharpMemberOrFunctionOrValue.Shared(cenv, vref) let witnessArgsR = GetWitnessArgs cenv env vref m vref.Typars tyargs // tailcall ConvObjectModelCallLinear cenv env (false, v, [], tyargs, witnessArgsR, List.concat untupledCurriedArgs) contf2 @@ -892,7 +892,7 @@ module FSharpExprConvert = E.AddressSet(ConvExpr cenv env (exprForValRef m vref), ConvExpr cenv env e) | TOp.LValueOp (LSet, vref), [], [e] -> - E.ValueSet(FSharpMemberOrFunctionOrValue(cenv, vref), ConvExpr cenv env e) + E.ValueSet(FSharpMemberOrFunctionOrValue.Shared(cenv, vref), ConvExpr cenv env e) | TOp.LValueOp (LByrefGet, vref), [], [] -> ConvValRef cenv env m vref @@ -1039,7 +1039,7 @@ module FSharpExprConvert = let minfo = MethInfo.FSMeth(g, enclosingTy, vr, None) FSharpMemberOrFunctionOrValue(cenv, minfo) else - FSharpMemberOrFunctionOrValue(cenv, vr) + FSharpMemberOrFunctionOrValue.Shared(cenv, vr) makeCall memOrVal // takes a possibly fake ValRef and tries to resolve it to an F# expression @@ -1272,11 +1272,11 @@ module FSharpExprConvert = elif v.IsBaseVal then E.BaseValue(ConvType cenv v.Type) else - E.Value(FSharpMemberOrFunctionOrValue(cenv, vref)) + E.Value(FSharpMemberOrFunctionOrValue.Shared(cenv, vref)) and ConvVal cenv (v: Val) : FSharpMemberOrFunctionOrValue = let vref = mkLocalValRef v - FSharpMemberOrFunctionOrValue(cenv, vref) + FSharpMemberOrFunctionOrValue.Shared(cenv, vref) and ConvConst cenv env m c ty = let g = cenv.g @@ -1401,7 +1401,7 @@ and FSharpImplementationFileContents(cenv, mimpl) = assert v.IsCompiledAsTopLevel let valReprInfo = InferValReprInfoOfBinding g AllowTypeDirectedDetupling.Yes v bind.Expr let tps, _ctorThisValOpt, _baseValOpt, vsl, body, _bodyty = IteratedAdjustLambdaToMatchValReprInfo g cenv.amap valReprInfo bind.Expr - let v = FSharpMemberOrFunctionOrValue(cenv, mkLocalValRef v) + let v = FSharpMemberOrFunctionOrValue.Shared(cenv, mkLocalValRef v) let gps = v.GenericParameters let vslR = List.mapSquared (FSharpExprConvert.ConvVal cenv) vsl let env = ExprTranslationEnv.Empty(g).BindTypars (Seq.zip tps gps |> Seq.toList) diff --git a/src/Compiler/Symbols/Symbols.fs b/src/Compiler/Symbols/Symbols.fs index 234d4e56edc..882406a2e2a 100644 --- a/src/Compiler/Symbols/Symbols.fs +++ b/src/Compiler/Symbols/Symbols.fs @@ -3,6 +3,7 @@ namespace rec FSharp.Compiler.Symbols open System +open System.Collections.Concurrent open System.Collections.Generic open FSharp.Compiler.DiagnosticsLogger open Internal.Utilities.Collections @@ -63,6 +64,8 @@ type SymbolEnv(g: TcGlobals, thisCcu: CcuThunk, thisCcuTyp: ModuleOrNamespaceTyp // traitCtxtNone: IDE symbol API — no TcEnv available, only SymbolEnv (audited for RFC FS-1043) let tcVal = LightweightTcValForUsingInBuildMethodCall g traitCtxtNone + let memberSymbols = ConcurrentDictionary() + let entitySymbols = ConcurrentDictionary() new(g: TcGlobals, thisCcu: CcuThunk, thisCcuTyp: ModuleOrNamespaceType option, tcImports: TcImports) = let amap = tcImports.GetImportMap() @@ -77,9 +80,15 @@ type SymbolEnv(g: TcGlobals, thisCcu: CcuThunk, thisCcuTyp: ModuleOrNamespaceTyp member _.tcImports = tcImports member _.tcValF = tcVal + /// Symbols for values and members, shared by stamp + member _.MemberSymbolCache = memberSymbols + + /// Symbols for non-generic entities, shared by stamp + member _.EntitySymbolCache = entitySymbols + [] module Impl = - let protect f = + let inline protect ([] f) = DiagnosticsLogger.protectAssemblyExplorationF (fun (asmName, path) -> invalidOp (sprintf "The entity or value '%s' does not exist or is in an unresolved assembly. You may need to add a reference to assembly '%s'" path asmName)) f @@ -764,10 +773,31 @@ type FSharpEntity(cenv: SymbolEnv, entity: EntityRef, tyargs: TType list) = | None -> false | Some ccu -> ccuEq ccu cenv.g.fslibCcu + let mutable attributes: IList | null = null + let mutable tryFullNameComputed = false + let mutable tryFullName: string option = None + new(cenv: SymbolEnv, tcref: TyconRef) = - // traitCtxtNone: IDE symbol API — type freshening for display, not constraint solving (audited for RFC FS-1043) - let _, _, tyargs = FreshenTypeInst cenv.g traitCtxtNone range0 (tcref.Typars) - FSharpEntity(cenv, tcref, tyargs) + match tcref.Typars with + | [] -> FSharpEntity(cenv, tcref, []) + | tps -> + // traitCtxtNone: IDE symbol API — type freshening for display, not constraint solving (audited for RFC FS-1043) + let _, _, tyargs = FreshenTypeInst cenv.g traitCtxtNone range0 tps + FSharpEntity(cenv, tcref, tyargs) + + /// The symbol for a non-generic entity, shared through the SymbolEnv. Generic entities get fresh + /// type arguments per symbol, so they are not shared. + static member Shared(cenv: SymbolEnv, tcref: TyconRef) : FSharpEntity = + match tcref.TryDeref with + | ValueSome tycon when isNil tycon.Typars -> + let cache = cenv.EntitySymbolCache + match cache.TryGetValue tycon.Stamp with + | true, cached -> !!cached :?> FSharpEntity + | _ -> + let ent = FSharpEntity(cenv, tcref, []) + cache.TryAdd(tycon.Stamp, (ent :> obj)) |> ignore + ent + | _ -> FSharpEntity(cenv, tcref) member _.Entity = entity @@ -843,17 +873,22 @@ type FSharpEntity(cenv: SymbolEnv, entity: EntityRef, tyargs: TType list) = | Some nm -> nm member _.TryFullName = - if isUnresolved() then None -#if !NO_TYPEPROVIDERS - elif entity.IsTypeAbbrev || entity.IsProvidedErasedTycon then None - #else - elif entity.IsTypeAbbrev then None -#endif - elif entity.IsNamespace then Some entity.DemangledModuleOrNamespaceName - else - match entity.CompiledRepresentation with - | CompiledTypeRepr.ILAsmNamed(tref, _, _) -> Some tref.FullName - | CompiledTypeRepr.ILAsmOpen _ -> None + if tryFullNameComputed then tryFullName else + let result = + if isUnresolved() then None + #if !NO_TYPEPROVIDERS + elif entity.IsTypeAbbrev || entity.IsProvidedErasedTycon then None + #else + elif entity.IsTypeAbbrev then None + #endif + elif entity.IsNamespace then Some entity.DemangledModuleOrNamespaceName + else + match entity.CompiledRepresentation with + | CompiledTypeRepr.ILAsmNamed(tref, _, _) -> Some tref.FullName + | CompiledTypeRepr.ILAsmOpen _ -> None + tryFullName <- result + tryFullNameComputed <- true + result member _.DeclarationLocation = checkIsResolved() @@ -1151,10 +1186,17 @@ type FSharpEntity(cenv: SymbolEnv, entity: EntityRef, tyargs: TType list) = FSharpType(cenv, ty) override _.Attributes = - if isUnresolved() then makeReadOnlyCollection [] else - GetAttribInfosOfEntity cenv.g cenv.amap range0 entity - |> List.map (fun a -> FSharpAttribute(cenv, a)) - |> makeReadOnlyCollection + let cached = attributes + if isNull cached then + let result = + if isUnresolved() then makeReadOnlyCollection [] else + GetAttribInfosOfEntity cenv.g cenv.amap range0 entity + |> List.map (fun a -> FSharpAttribute(cenv, a)) + |> makeReadOnlyCollection + attributes <- result + result + else + !!cached member _.AllCompilationPaths = checkIsResolved() @@ -2034,13 +2076,15 @@ type FSharpMemberOrFunctionOrValue(cenv, d:FSharpMemberOrValData, item) = //| V v -> AccessibilityLogic.IsValAccessible ad v ) - let fsharpInfo() = + let fsharpInfoValue = match d with | M m | C m -> m.ArbitraryValRef | P p -> p.ArbitraryValRef | E e -> e.ArbitraryValRef | V v -> Some v + let fsharpInfo() = fsharpInfoValue + let isUnresolved() = match fsharpInfo() with | None -> false @@ -2055,8 +2099,28 @@ type FSharpMemberOrFunctionOrValue(cenv, d:FSharpMemberOrValData, item) = let mkMethSym minfo = FSharpMemberOrFunctionOrValue(cenv, M minfo, Item.MethodGroup (minfo.DisplayName, [minfo], None)) let mkEventSym einfo = FSharpMemberOrFunctionOrValue(cenv, E einfo, Item.Event einfo) + let mutable curriedParameterGroups: IList> | null = null + let mutable attributes: IList | null = null + let mutable genericParameters: IList | null = null + let mutable declaringEntityComputed = false + let mutable declaringEntity: FSharpEntity option = None + let mutable fullType: FSharpType | null = null + new (cenv, vref) = FSharpMemberFunctionOrValue(cenv, V vref, Item.Value vref) + /// The symbol for a value or member, shared through the SymbolEnv + static member Shared(cenv: SymbolEnv, vref: ValRef) : FSharpMemberOrFunctionOrValue = + match vref.TryDeref with + | ValueSome v -> + let cache = cenv.MemberSymbolCache + match cache.TryGetValue v.Stamp with + | true, cached -> !!cached :?> FSharpMemberOrFunctionOrValue + | _ -> + let sym = FSharpMemberOrFunctionOrValue(cenv, vref) + cache.TryAdd(v.Stamp, (sym :> obj)) |> ignore + sym + | _ -> FSharpMemberOrFunctionOrValue(cenv, vref) + new (cenv, minfo: MethInfo) = if minfo.IsConstructor || minfo.IsClassConstructor then FSharpMemberFunctionOrValue(cenv, C minfo, Item.CtorGroup(minfo.LogicalName, [minfo])) @@ -2103,15 +2167,20 @@ type FSharpMemberOrFunctionOrValue(cenv, d:FSharpMemberOrValData, item) = | None -> failwith "DeclarationLocation property not available" member _.DeclaringEntity: FSharpEntity option = + if declaringEntityComputed then declaringEntity else checkIsResolved() - match d with - | E e -> FSharpEntity(cenv, e.DeclaringTyconRef) |> Some - | P p -> FSharpEntity(cenv, p.DeclaringTyconRef) |> Some - | M m | C m -> FSharpEntity(cenv, m.DeclaringTyconRef) |> Some - | V v -> - match v.TryDeclaringEntity with - | ParentNone -> None - | Parent p -> FSharpEntity(cenv, p) |> Some + let result = + match d with + | E e -> FSharpEntity.Shared(cenv, e.DeclaringTyconRef) |> Some + | P p -> FSharpEntity.Shared(cenv, p.DeclaringTyconRef) |> Some + | M m | C m -> FSharpEntity.Shared(cenv, m.DeclaringTyconRef) |> Some + | V v -> + match v.TryDeclaringEntity with + | ParentNone -> None + | Parent p -> FSharpEntity.Shared(cenv, p) |> Some + declaringEntity <- result + declaringEntityComputed <- true + result member _.ApparentEnclosingEntity: FSharpEntity option = let createEntity (ttype: TType) = @@ -2145,27 +2214,39 @@ type FSharpMemberOrFunctionOrValue(cenv, d:FSharpMemberOrValData, item) = | Parent p -> FSharpType(cenv, generalizedTyconRef cenv.g p) member _.GenericParameters = - checkIsResolved() - let tps = - match d with - | E _ -> [] - | P _ -> [] - | M m | C m -> m.FormalMethodTypars - | V v -> v.Typars - tps |> List.map (fun tp -> FSharpGenericParameter(cenv, tp)) |> makeReadOnlyCollection + let cached = genericParameters + if isNull cached then + checkIsResolved() + let tps = + match d with + | E _ -> [] + | P _ -> [] + | M m | C m -> m.FormalMethodTypars + | V v -> v.Typars + let result = tps |> List.map (fun tp -> FSharpGenericParameter(cenv, tp)) |> makeReadOnlyCollection + genericParameters <- result + result + else + !!cached member _.FullType = - checkIsResolved() - let ty = - match d with - | E e -> e.GetDelegateType(cenv.amap, range0) - | P p -> p.GetPropertyType(cenv.amap, range0) - | M m | C m -> - let retTy = m.GetFSharpReturnType(cenv.amap, range0, m.FormalMethodInst) - let argTysl = m.GetParamTypes(cenv.amap, range0, m.FormalMethodInst) - mkIteratedFunTy cenv.g (List.map (mkRefTupledTy cenv.g) argTysl) retTy - | V v -> v.TauType - FSharpType(cenv, ty) + let cached = fullType + if isNull cached then + checkIsResolved() + let ty = + match d with + | E e -> e.GetDelegateType(cenv.amap, range0) + | P p -> p.GetPropertyType(cenv.amap, range0) + | M m | C m -> + let retTy = m.GetFSharpReturnType(cenv.amap, range0, m.FormalMethodInst) + let argTysl = m.GetParamTypes(cenv.amap, range0, m.FormalMethodInst) + mkIteratedFunTy cenv.g (List.map (mkRefTupledTy cenv.g) argTysl) retTy + | V v -> v.TauType + let result = FSharpType(cenv, ty) + fullType <- result + result + else + !!cached member _.HasGetterMethod = if isUnresolved() then false @@ -2531,7 +2612,16 @@ type FSharpMemberOrFunctionOrValue(cenv, d:FSharpMemberOrValData, item) = | V v -> v.XmlDoc |> makeElaboratedXmlDoc member x.CurriedParameterGroups = - checkIsResolved() + let cached = curriedParameterGroups + if isNull cached then + checkIsResolved() + let result = x.BuildCurriedParameterGroups() + curriedParameterGroups <- result + result + else + !!cached + + member private x.BuildCurriedParameterGroups() : IList> = match d with | P p -> [ [ for ParamData(isParamArrayArg, isInArg, isOutArg, optArgInfo, _callerInfo, nmOpt, _reflArgInfo, pty) in p.GetParamDatas(cenv.amap, range0) do @@ -2644,18 +2734,25 @@ type FSharpMemberOrFunctionOrValue(cenv, d:FSharpMemberOrValData, item) = override _.Attributes = - if isUnresolved() then makeReadOnlyCollection [] else - let m = range0 - match d with - | E einfo -> - GetAttribInfosOfEvent cenv.amap m einfo |> List.map (fun a -> FSharpAttribute(cenv, a)) - | P pinfo -> - GetAttribInfosOfProp cenv.amap m pinfo |> List.map (fun a -> FSharpAttribute(cenv, a)) - | M minfo | C minfo -> - GetAttribInfosOfMethod cenv.amap m minfo |> List.map (fun a -> FSharpAttribute(cenv, a)) - | V v -> - v.Attribs |> List.map (fun a -> FSharpAttribute(cenv, AttribInfo.FSAttribInfo(cenv.g, a))) - |> makeReadOnlyCollection + let cached = attributes + if isNull cached then + let result = + if isUnresolved() then makeReadOnlyCollection [] else + let m = range0 + match d with + | E einfo -> + GetAttribInfosOfEvent cenv.amap m einfo |> List.map (fun a -> FSharpAttribute(cenv, a)) + | P pinfo -> + GetAttribInfosOfProp cenv.amap m pinfo |> List.map (fun a -> FSharpAttribute(cenv, a)) + | M minfo | C minfo -> + GetAttribInfosOfMethod cenv.amap m minfo |> List.map (fun a -> FSharpAttribute(cenv, a)) + | V v -> + v.Attribs |> List.map (fun a -> FSharpAttribute(cenv, AttribInfo.FSAttribInfo(cenv.g, a))) + |> makeReadOnlyCollection + attributes <- result + result + else + !!cached /// Is this "base" in "base.M(...)" member _.IsBaseValue = @@ -2774,7 +2871,21 @@ type FSharpMemberOrFunctionOrValue(cenv, d:FSharpMemberOrValData, item) = | _ -> false | _ -> false - override x.GetHashCode() = hash (box x.LogicalName) + override x.GetHashCode() = + let hashValRef (vref: ValRef) = + match vref.TryDeref with + | ValueSome v -> hash v.Stamp + | ValueNone -> hash (box x.LogicalName) + let rec hashMethInfo (minfo: MethInfo) = + match minfo with + | FSMeth(_, _, vref, _) -> hashValRef vref + | ILMeth(_, ilminfo, _) -> LanguagePrimitives.PhysicalHash ilminfo.RawMetadata + | MethInfoWithModifiedReturnType(minfo, _) -> hashMethInfo minfo + | _ -> hash (box x.LogicalName) + match d with + | V vref -> hashValRef vref + | M minfo | C minfo -> hashMethInfo minfo + | _ -> hash (box x.LogicalName) override x.ToString() = try let prefix = (if x.IsEvent then "event " elif x.IsProperty then "property " elif x.IsMember then "member " else "val ") @@ -2953,6 +3064,8 @@ type FSharpMemberOrFunctionOrValue(cenv, d:FSharpMemberOrValData, item) = type FSharpType(cenv, ty:TType) = + let mutable genericArguments: IList | null = null + let isUnresolved() = DiagnosticsLogger.protectAssemblyExploration true <| fun () -> match stripTyparEqns ty with @@ -3035,6 +3148,7 @@ type FSharpType(cenv, ty:TType) = member _.TypeDefinition = protect <| fun () -> match stripTyparEqns ty with + | TType_app (tcref, [], _) -> FSharpEntity.Shared(cenv, tcref) | TType_app (tcref, tyargs, _) -> FSharpEntity(cenv, tcref, tyargs) | TType_measure (Measure.Const(tyconRef= tcref)) -> FSharpEntity(cenv, tcref) | TType_measure (Measure.Prod _) -> FSharpEntity(cenv, cenv.g.measureproduct_tcr) @@ -3060,17 +3174,24 @@ type FSharpType(cenv, ty:TType) = | _ -> false member _.GenericArguments = - protect <| fun () -> - match stripTyparEqns ty with - | TType_anon (_, tyargs) - | TType_app (_, tyargs, _) - | TType_tuple (_, tyargs) -> (tyargs |> List.map (fun ty -> FSharpType(cenv, ty)) |> makeReadOnlyCollection) - | TType_fun(domainTy, rangeTy, _) -> [| FSharpType(cenv, domainTy); FSharpType(cenv, rangeTy) |] |> makeReadOnlyCollection - | TType_measure (Measure.Const _) -> [| |] |> makeReadOnlyCollection - | TType_measure (Measure.Prod(measure1= t1; measure2 = t2)) -> [| FSharpType(cenv, TType_measure t1); FSharpType(cenv, TType_measure t2) |] |> makeReadOnlyCollection - | TType_measure (Measure.One _) -> [| |] |> makeReadOnlyCollection - | TType_measure (Measure.Inv t1) -> [| FSharpType(cenv, TType_measure t1) |] |> makeReadOnlyCollection - | _ -> invalidOp "not a named type" + let cached = genericArguments + if isNull cached then + let result = + protect <| fun () -> + match stripTyparEqns ty with + | TType_anon (_, tyargs) + | TType_app (_, tyargs, _) + | TType_tuple (_, tyargs) -> (tyargs |> List.map (fun ty -> FSharpType(cenv, ty)) |> makeReadOnlyCollection) + | TType_fun(domainTy, rangeTy, _) -> [| FSharpType(cenv, domainTy); FSharpType(cenv, rangeTy) |] |> makeReadOnlyCollection + | TType_measure (Measure.Const _) -> [| |] |> makeReadOnlyCollection + | TType_measure (Measure.Prod(measure1= t1; measure2 = t2)) -> [| FSharpType(cenv, TType_measure t1); FSharpType(cenv, TType_measure t2) |] |> makeReadOnlyCollection + | TType_measure (Measure.One _) -> [| |] |> makeReadOnlyCollection + | TType_measure (Measure.Inv t1) -> [| FSharpType(cenv, TType_measure t1) |] |> makeReadOnlyCollection + | _ -> invalidOp "not a named type" + genericArguments <- result + result + else + !!cached (* member _.ProvidedArguments = @@ -3252,7 +3373,7 @@ type FSharpAttribute(cenv: SymbolEnv, attrib: AttribInfo) = | _ -> arg member _.AttributeType = - FSharpEntity(cenv, attrib.TyconRef) + FSharpEntity.Shared(cenv, attrib.TyconRef) member _.IsUnresolved = entityIsUnresolved(attrib.TyconRef) diff --git a/src/Compiler/Symbols/Symbols.fsi b/src/Compiler/Symbols/Symbols.fsi index c77417c9209..aac0c22934d 100644 --- a/src/Compiler/Symbols/Symbols.fsi +++ b/src/Compiler/Symbols/Symbols.fsi @@ -2,6 +2,7 @@ namespace rec FSharp.Compiler.Symbols +open System.Collections.Concurrent open System.Collections.Generic open FSharp.Compiler @@ -36,6 +37,12 @@ type internal SymbolEnv = member tcValF: ConstraintSolver.TcValF + /// Symbols for values and members, shared by stamp + member MemberSymbolCache: ConcurrentDictionary + + /// Symbols for non-generic entities, shared by stamp + member EntitySymbolCache: ConcurrentDictionary + /// Indicates the accessibility of a symbol, as seen by the F# language type FSharpAccessibility = internal new: Accessibility * ?isProtected: bool -> FSharpAccessibility @@ -214,6 +221,9 @@ type FSharpEntity = internal new: SymbolEnv * EntityRef -> FSharpEntity + /// The symbol for a non-generic entity, shared through the SymbolEnv + static member internal Shared: SymbolEnv * EntityRef -> FSharpEntity + /// Get the enclosing entity for the definition member DeclaringEntity: FSharpEntity option @@ -798,6 +808,9 @@ type FSharpMemberOrFunctionOrValue = internal new: SymbolEnv * ValRef -> FSharpMemberOrFunctionOrValue internal new: SymbolEnv * Infos.MethInfo -> FSharpMemberOrFunctionOrValue + /// The symbol for a value or member, shared through the SymbolEnv + static member internal Shared: SymbolEnv * ValRef -> FSharpMemberOrFunctionOrValue + /// Indicates if the member, function or value is in an unresolved assembly member IsUnresolved: bool From 6eb910c8356e1f1c42dee73915f6d5b33e653ebe Mon Sep 17 00:00:00 2001 From: Mangel Maxime Date: Wed, 21 Oct 2026 21:25:37 +0200 Subject: [PATCH 2/6] test(benchmarks): add symbol API FCS benchmark --- .../FSharp.Compiler.Benchmarks.fsproj | 1 + .../CompilerServiceBenchmarks/README.md | 10 ++ .../SymbolApiBenchmarks.fs | 122 ++++++++++++++++++ 3 files changed, 133 insertions(+) create mode 100644 tests/benchmarks/FCSBenchmarks/CompilerServiceBenchmarks/SymbolApiBenchmarks.fs diff --git a/tests/benchmarks/FCSBenchmarks/CompilerServiceBenchmarks/FSharp.Compiler.Benchmarks.fsproj b/tests/benchmarks/FCSBenchmarks/CompilerServiceBenchmarks/FSharp.Compiler.Benchmarks.fsproj index 915cf5d8ab0..4ffcb3a5b0a 100644 --- a/tests/benchmarks/FCSBenchmarks/CompilerServiceBenchmarks/FSharp.Compiler.Benchmarks.fsproj +++ b/tests/benchmarks/FCSBenchmarks/CompilerServiceBenchmarks/FSharp.Compiler.Benchmarks.fsproj @@ -16,6 +16,7 @@ + diff --git a/tests/benchmarks/FCSBenchmarks/CompilerServiceBenchmarks/README.md b/tests/benchmarks/FCSBenchmarks/CompilerServiceBenchmarks/README.md index 15e2d57422d..fdc1a387122 100644 --- a/tests/benchmarks/FCSBenchmarks/CompilerServiceBenchmarks/README.md +++ b/tests/benchmarks/FCSBenchmarks/CompilerServiceBenchmarks/README.md @@ -33,6 +33,16 @@ compare `Narrow` retained across `main` and this branch (and Narrow-vs-Wide with To compare branches: build + run on `main`, note the numbers, `git checkout il-pre-namespace`, rebuild and re-run, diff. (The `BenchmarkComparison/` project automates historical before/after runs if preferred.) +## Symbol API benchmarks + +`SymbolApiBenchmarks.fs` checks `decentlySizedStandAloneFile.fs` in the iteration setup, then walks every +declaration and expression of the typed tree through the symbol API, asking each member for its parameters, +attributes, declaring entity and full type, as a compiler back end or an analyzer does. + +```dotnet run -c Release --filter *SymbolApi*``` + +Only the walk is measured: `WalkSymbols` is one pass, `WalkSymbolsRepeated` five passes over the same checked file. Compare `Mean` and `Allocated` across branches. + ## Sample results | Method | Job | UnrollFactor | Mean | Error | StdDev | Median | Gen 0 | Gen 1 | Gen 2 | Allocated | diff --git a/tests/benchmarks/FCSBenchmarks/CompilerServiceBenchmarks/SymbolApiBenchmarks.fs b/tests/benchmarks/FCSBenchmarks/CompilerServiceBenchmarks/SymbolApiBenchmarks.fs new file mode 100644 index 00000000000..cfe45acd22e --- /dev/null +++ b/tests/benchmarks/FCSBenchmarks/CompilerServiceBenchmarks/SymbolApiBenchmarks.fs @@ -0,0 +1,122 @@ +namespace FSharp.Compiler.Benchmarks + +open System.IO +open FSharp.Compiler.CodeAnalysis +open FSharp.Compiler.Diagnostics +open FSharp.Compiler.Symbols +open FSharp.Compiler.Symbols.FSharpExprPatterns +open FSharp.Compiler.Text +open BenchmarkDotNet.Attributes +open FSharp.Benchmarks.Common.Categories + +// What a compiler back end or an analyzer does with a checked file: walk every declaration and +// expression and ask the symbols about parameters, attributes, declaring entities and types. +[] +module private SymbolApiHelpers = + + let filePath = Path.Combine(__SOURCE_DIRECTORY__, "../decentlySizedStandAloneFile.fs") + + let sourceText () = SourceText.ofString (File.ReadAllText filePath) + + let inline addTo (acc: byref) n = acc <- acc + n + + let visitType (acc: byref) (ty: FSharpType) = + if ty.HasTypeDefinition then + addTo &acc ty.TypeDefinition.Attributes.Count + match ty.TypeDefinition.TryFullName with + | Some name -> addTo &acc name.Length + | None -> () + if ty.HasTypeDefinition || ty.IsTupleType || ty.IsFunctionType || ty.IsAnonRecordType then + addTo &acc ty.GenericArguments.Count + + let visitMember (acc: byref) (memb: FSharpMemberOrFunctionOrValue) = + for group in memb.CurriedParameterGroups do + for param in group do + visitType &acc param.Type + addTo &acc memb.Attributes.Count + addTo &acc memb.GenericParameters.Count + match memb.DeclaringEntity with + | Some ent -> + addTo &acc ent.Attributes.Count + match ent.TryFullName with + | Some name -> addTo &acc name.Length + | None -> () + | None -> () + visitType &acc memb.FullType + if memb.IsMutable then addTo &acc 1 + addTo &acc memb.CompiledName.Length + + let rec visitExpr (acc: byref) (expr: FSharpExpr) = + match expr with + | Call(_, memb, _, _, _) + | NewObject(memb, _, _) -> visitMember &acc memb + | Value v -> visitMember &acc v + | Let((v, _, _), _) -> visitMember &acc v + | Lambda(v, _) -> visitMember &acc v + | _ -> () + visitType &acc expr.Type + for sub in expr.ImmediateSubExpressions do + visitExpr &acc sub + + let rec visitDeclaration (acc: byref) (decl: FSharpImplementationFileDeclaration) = + match decl with + | FSharpImplementationFileDeclaration.Entity(ent, subDecls) -> + addTo &acc ent.Attributes.Count + for sub in subDecls do + visitDeclaration &acc sub + | FSharpImplementationFileDeclaration.MemberOrFunctionOrValue(memb, args, body) -> + visitMember &acc memb + for group in args do + for arg in group do + visitMember &acc arg + visitExpr &acc body + | FSharpImplementationFileDeclaration.InitAction body -> visitExpr &acc body + +/// Walking the typed tree of a checked file through the symbol API. The check itself runs in the +/// iteration setup, so only the symbol queries are measured. +[] +[] +type SymbolApiBenchmarks() = + + let mutable checker = Unchecked.defaultof + let mutable options = Unchecked.defaultof + let mutable results = Unchecked.defaultof + + [] + member _.Setup() = + checker <- FSharpChecker.Create(projectCacheSize = 200, keepAssemblyContents = true) + let opts, _ = checker.GetProjectOptionsFromScript(filePath, sourceText (), assumeDotNetFramework = false, useSdkRefs = true) |> Async.RunSynchronously + options <- opts + + [] + member _.CheckFile() = + checker.InvalidateAll() + checker.ClearLanguageServiceRootCachesAndCollectAndFinalizeAllTransients() + let _, answer = checker.ParseAndCheckFileInProject(filePath, 0, sourceText (), options) |> Async.RunSynchronously + match answer with + | FSharpCheckFileAnswer.Aborted -> failwith "check aborted" + | FSharpCheckFileAnswer.Succeeded r -> + let errors = r.Diagnostics |> Array.filter (fun d -> d.Severity = FSharpDiagnosticSeverity.Error) + if errors.Length > 0 then failwithf "check had errors: %A" errors + results <- r + + member _.Walk() = + let mutable acc = 0 + match results.ImplementationFile with + | None -> failwith "no implementation file" + | Some implFile -> + for decl in implFile.Declarations do + visitDeclaration &acc decl + acc + + /// One pass over the file. + [] + member this.WalkSymbols() = this.Walk() + + /// Five passes over the same checked file, as a consumer that queries the same symbols repeatedly does. + [] + member this.WalkSymbolsRepeated() = + let mutable acc = 0 + for _ in 1 .. 5 do + acc <- acc + this.Walk() + acc From 40cb209778233eeb1637a254eeb98189a6415f66 Mon Sep 17 00:00:00 2001 From: Mangel Maxime Date: Wed, 21 Oct 2026 23:08:36 +0200 Subject: [PATCH 3/6] test(benchmarks): add project symbol walk probe --- .../CompilerServiceBenchmarks/Program.fs | 4 ++ .../CompilerServiceBenchmarks/README.md | 5 ++ .../SymbolApiBenchmarks.fs | 55 +++++++++++++++++++ 3 files changed, 64 insertions(+) diff --git a/tests/benchmarks/FCSBenchmarks/CompilerServiceBenchmarks/Program.fs b/tests/benchmarks/FCSBenchmarks/CompilerServiceBenchmarks/Program.fs index c075df7d81e..a1a87b97953 100644 --- a/tests/benchmarks/FCSBenchmarks/CompilerServiceBenchmarks/Program.fs +++ b/tests/benchmarks/FCSBenchmarks/CompilerServiceBenchmarks/Program.fs @@ -24,6 +24,10 @@ let main args = | [| "check-project"; iterations; responseFile; projectDir |] -> CheckProjectProbe.run (int iterations) responseFile projectDir 0 + // Project check followed by a symbol API walk of every file; see WalkProjectProbe. + | [| "walk-project"; iterations; responseFile; projectDir |] -> + WalkProjectProbe.run (int iterations) responseFile projectDir + 0 // Single-file check then hold alive for an external heap dump; see CheckFileProbe. | [| "check-file"; responseFile; projectDir; fileToCheck |] -> CheckFileProbe.run responseFile projectDir fileToCheck diff --git a/tests/benchmarks/FCSBenchmarks/CompilerServiceBenchmarks/README.md b/tests/benchmarks/FCSBenchmarks/CompilerServiceBenchmarks/README.md index fdc1a387122..45a84ee0140 100644 --- a/tests/benchmarks/FCSBenchmarks/CompilerServiceBenchmarks/README.md +++ b/tests/benchmarks/FCSBenchmarks/CompilerServiceBenchmarks/README.md @@ -43,6 +43,11 @@ attributes, declaring entity and full type, as a compiler back end or an analyze Only the walk is measured: `WalkSymbols` is one pass, `WalkSymbolsRepeated` five passes over the same checked file. Compare `Mean` and `Allocated` across branches. +For a real project, `walk-project` checks it from a captured fsc response file and prints, per sample, the time +and allocation of a declaration walk over every file and of a `GetAllUsesOfAllSymbols` pass with tooltip-style +queries on each use: +```dotnet run -c Release -- walk-project ``` + ## Sample results | Method | Job | UnrollFactor | Mean | Error | StdDev | Median | Gen 0 | Gen 1 | Gen 2 | Allocated | diff --git a/tests/benchmarks/FCSBenchmarks/CompilerServiceBenchmarks/SymbolApiBenchmarks.fs b/tests/benchmarks/FCSBenchmarks/CompilerServiceBenchmarks/SymbolApiBenchmarks.fs index cfe45acd22e..eaa8ade8c12 100644 --- a/tests/benchmarks/FCSBenchmarks/CompilerServiceBenchmarks/SymbolApiBenchmarks.fs +++ b/tests/benchmarks/FCSBenchmarks/CompilerServiceBenchmarks/SymbolApiBenchmarks.fs @@ -1,5 +1,7 @@ namespace FSharp.Compiler.Benchmarks +open System +open System.Diagnostics open System.IO open FSharp.Compiler.CodeAnalysis open FSharp.Compiler.Diagnostics @@ -120,3 +122,56 @@ type SymbolApiBenchmarks() = for _ in 1 .. 5 do acc <- acc + this.Walk() acc + +/// Checks a real project from a captured fsc response file, then walks every implementation file through +/// the symbol API, like a compiler back end does. The walk's time and allocation are printed separately +/// from the check's. +/// +/// Run from Program.fs: `walk-project `. +module WalkProjectProbe = + + let run iterations responseFile projectDir = + if iterations <= 0 then invalidArg (nameof iterations) "The sample count must be positive." + Environment.CurrentDirectory <- projectDir + let options = readProjectOptions responseFile projectDir + let checker = FSharpChecker.Create(projectCacheSize = 1, keepAssemblyContents = true) + for i in 1 .. iterations do + if i > 1 then checker.InvalidateConfiguration options + let results = checker.ParseAndCheckProject options |> Async.RunSynchronously + let errors = results.Diagnostics |> Array.filter (fun d -> d.Severity = FSharpDiagnosticSeverity.Error) + if errors.Length > 0 then failwith $"Project check failed: %A{errors}" + forceGC () + let before = GC.GetTotalAllocatedBytes true + let sw = Stopwatch.StartNew() + let mutable acc = 0 + let mutable skipped = 0 + for implFile in results.AssemblyContents.ImplementationFiles do + for decl in implFile.Declarations do + // The expression converter can fail on code the compiler accepted, for instance witness + // generation for some trait calls. Those declarations are skipped and counted. + try visitDeclaration &acc decl with _ -> skipped <- skipped + 1 + sw.Stop() + let allocated = GC.GetTotalAllocatedBytes true - before + printfn "sample %d: walk %.3f ms | allocated %d bytes | %d files | %d declarations skipped | checksum %d" + i sw.Elapsed.TotalMilliseconds allocated results.AssemblyContents.ImplementationFiles.Length skipped acc + // The IDE path: every symbol use in the project, with the queries a tooltip makes. + forceGC () + let before = GC.GetTotalAllocatedBytes true + let sw = Stopwatch.StartNew() + let mutable acc = 0 + let uses = results.GetAllUsesOfAllSymbols() + for su in uses do + match su.Symbol with + | :? FSharpMemberOrFunctionOrValue as memb -> visitMember &acc memb + | :? FSharpEntity as ent -> + addTo &acc ent.Attributes.Count + match ent.TryFullName with + | Some name -> addTo &acc name.Length + | None -> () + | _ -> () + sw.Stop() + let allocated = GC.GetTotalAllocatedBytes true - before + printfn "sample %d: uses %.3f ms | allocated %d bytes | %d uses | checksum %d" + i sw.Elapsed.TotalMilliseconds allocated uses.Length acc + GC.KeepAlive results + GC.KeepAlive checker From 8a39eb65e3ec7b80669e19e3a4dc09ee648f2649 Mon Sep 17 00:00:00 2001 From: Mangel Maxime Date: Thu, 22 Oct 2026 10:18:40 +0200 Subject: [PATCH 4/6] docs(release-notes): link PR 20845 --- docs/release-notes/.FSharp.Compiler.Service/11.0.200.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes/.FSharp.Compiler.Service/11.0.200.md b/docs/release-notes/.FSharp.Compiler.Service/11.0.200.md index 2abc6eb2922..6d46f62f93d 100644 --- a/docs/release-notes/.FSharp.Compiler.Service/11.0.200.md +++ b/docs/release-notes/.FSharp.Compiler.Service/11.0.200.md @@ -38,6 +38,7 @@ * Debugger: emit a call for a self tail call in non-optimized builds ([PR #20782](https://github.com/dotnet/fsharp/pull/20782)) * `FSharpMemberOrFunctionOrValue` and non-generic `FSharpEntity` symbols are shared per definition within a `SymbolEnv`, and cache their parameter groups, attributes, generic parameters, declaring entity and full type. `FSharpMemberOrFunctionOrValue.GetHashCode` uses the definition's stamp. ([PR #TODO](https://github.com/dotnet/fsharp/pull/TODO)) +* `FSharpMemberOrFunctionOrValue` and non-generic `FSharpEntity` symbols are shared per definition within a `SymbolEnv`, and cache their parameter groups, attributes, generic parameters, declaring entity and full type. `FSharpMemberOrFunctionOrValue.GetHashCode` uses the definition's stamp. ([PR #20845](https://github.com/dotnet/fsharp/pull/20845)) * Remove the unused internal `--test:NoErrorText` switch and its argument-only diagnostic formatting. ([PR #20742](https://github.com/dotnet/fsharp/pull/20742)) * Assign seven previously preview-only features to [F# 11.2](https://fsharp.github.io/fsharp-compiler-docs/release-notes/Language.html#11.2), now the default language version. `default`, `latest`, and `latestmajor` select F# 11.2; explicit `11` and `11.0` retain F# 11.0 behavior. Preview SDK projects use the same compiler default unless `LangVersion` is explicitly set. `FromEndSlicing` remains preview-only. ([PR #20617](https://github.com/dotnet/fsharp/pull/20617)) * Add `SynExceptionDefnReprTrivia` with the range of the `exception` keyword to `SynExceptionDefnRepr`. ([Fantomas Issue #3483](https://github.com/fsprojects/fantomas/issues/3483), [PR #20612](https://github.com/dotnet/fsharp/pull/20612)) From 09348aacfc95d2737dd9f0319713a761b45332bc Mon Sep 17 00:00:00 2001 From: Mangel Maxime Date: Thu, 22 Oct 2026 12:08:08 +0200 Subject: [PATCH 5/6] perf(symbols): lazy caches, atomic option caching --- src/Compiler/Symbols/Symbols.fs | 76 ++++++++++++++++----------------- 1 file changed, 38 insertions(+), 38 deletions(-) diff --git a/src/Compiler/Symbols/Symbols.fs b/src/Compiler/Symbols/Symbols.fs index 882406a2e2a..f2a84df2934 100644 --- a/src/Compiler/Symbols/Symbols.fs +++ b/src/Compiler/Symbols/Symbols.fs @@ -64,8 +64,8 @@ type SymbolEnv(g: TcGlobals, thisCcu: CcuThunk, thisCcuTyp: ModuleOrNamespaceTyp // traitCtxtNone: IDE symbol API — no TcEnv available, only SymbolEnv (audited for RFC FS-1043) let tcVal = LightweightTcValForUsingInBuildMethodCall g traitCtxtNone - let memberSymbols = ConcurrentDictionary() - let entitySymbols = ConcurrentDictionary() + let memberSymbols = lazy (ConcurrentDictionary()) + let entitySymbols = lazy (ConcurrentDictionary()) new(g: TcGlobals, thisCcu: CcuThunk, thisCcuTyp: ModuleOrNamespaceType option, tcImports: TcImports) = let amap = tcImports.GetImportMap() @@ -81,10 +81,10 @@ type SymbolEnv(g: TcGlobals, thisCcu: CcuThunk, thisCcuTyp: ModuleOrNamespaceTyp member _.tcValF = tcVal /// Symbols for values and members, shared by stamp - member _.MemberSymbolCache = memberSymbols + member _.MemberSymbolCache = memberSymbols.Value /// Symbols for non-generic entities, shared by stamp - member _.EntitySymbolCache = entitySymbols + member _.EntitySymbolCache = entitySymbols.Value [] module Impl = @@ -774,8 +774,7 @@ type FSharpEntity(cenv: SymbolEnv, entity: EntityRef, tyargs: TType list) = | Some ccu -> ccuEq ccu cenv.g.fslibCcu let mutable attributes: IList | null = null - let mutable tryFullNameComputed = false - let mutable tryFullName: string option = None + let mutable tryFullName: Ref | null = null new(cenv: SymbolEnv, tcref: TyconRef) = match tcref.Typars with @@ -873,22 +872,23 @@ type FSharpEntity(cenv: SymbolEnv, entity: EntityRef, tyargs: TType list) = | Some nm -> nm member _.TryFullName = - if tryFullNameComputed then tryFullName else - let result = - if isUnresolved() then None - #if !NO_TYPEPROVIDERS - elif entity.IsTypeAbbrev || entity.IsProvidedErasedTycon then None - #else - elif entity.IsTypeAbbrev then None - #endif - elif entity.IsNamespace then Some entity.DemangledModuleOrNamespaceName - else - match entity.CompiledRepresentation with - | CompiledTypeRepr.ILAsmNamed(tref, _, _) -> Some tref.FullName - | CompiledTypeRepr.ILAsmOpen _ -> None - tryFullName <- result - tryFullNameComputed <- true - result + match tryFullName with + | null -> + let result = + if isUnresolved() then None +#if !NO_TYPEPROVIDERS + elif entity.IsTypeAbbrev || entity.IsProvidedErasedTycon then None +#else + elif entity.IsTypeAbbrev then None +#endif + elif entity.IsNamespace then Some entity.DemangledModuleOrNamespaceName + else + match entity.CompiledRepresentation with + | CompiledTypeRepr.ILAsmNamed(tref, _, _) -> Some tref.FullName + | CompiledTypeRepr.ILAsmOpen _ -> None + tryFullName <- ref result + result + | cached -> cached.Value member _.DeclarationLocation = checkIsResolved() @@ -2102,8 +2102,7 @@ type FSharpMemberOrFunctionOrValue(cenv, d:FSharpMemberOrValData, item) = let mutable curriedParameterGroups: IList> | null = null let mutable attributes: IList | null = null let mutable genericParameters: IList | null = null - let mutable declaringEntityComputed = false - let mutable declaringEntity: FSharpEntity option = None + let mutable declaringEntity: Ref | null = null let mutable fullType: FSharpType | null = null new (cenv, vref) = FSharpMemberFunctionOrValue(cenv, V vref, Item.Value vref) @@ -2167,20 +2166,21 @@ type FSharpMemberOrFunctionOrValue(cenv, d:FSharpMemberOrValData, item) = | None -> failwith "DeclarationLocation property not available" member _.DeclaringEntity: FSharpEntity option = - if declaringEntityComputed then declaringEntity else - checkIsResolved() - let result = - match d with - | E e -> FSharpEntity.Shared(cenv, e.DeclaringTyconRef) |> Some - | P p -> FSharpEntity.Shared(cenv, p.DeclaringTyconRef) |> Some - | M m | C m -> FSharpEntity.Shared(cenv, m.DeclaringTyconRef) |> Some - | V v -> - match v.TryDeclaringEntity with - | ParentNone -> None - | Parent p -> FSharpEntity.Shared(cenv, p) |> Some - declaringEntity <- result - declaringEntityComputed <- true - result + match declaringEntity with + | null -> + checkIsResolved() + let result = + match d with + | E e -> FSharpEntity.Shared(cenv, e.DeclaringTyconRef) |> Some + | P p -> FSharpEntity.Shared(cenv, p.DeclaringTyconRef) |> Some + | M m | C m -> FSharpEntity.Shared(cenv, m.DeclaringTyconRef) |> Some + | V v -> + match v.TryDeclaringEntity with + | ParentNone -> None + | Parent p -> FSharpEntity.Shared(cenv, p) |> Some + declaringEntity <- ref result + result + | cached -> cached.Value member _.ApparentEnclosingEntity: FSharpEntity option = let createEntity (ttype: TType) = From 97f34c4971b63472475a84627373eeb61108591d Mon Sep 17 00:00:00 2001 From: Mangel Maxime Date: Mon, 26 Oct 2026 17:16:50 +0100 Subject: [PATCH 6/6] fix(symbols): no throw on unresolved entity in Shared --- src/Compiler/Symbols/Symbols.fs | 3 +- .../FSharpChecker/SymbolUse.fs | 37 +++++++++++++++++++ 2 files changed, 39 insertions(+), 1 deletion(-) diff --git a/src/Compiler/Symbols/Symbols.fs b/src/Compiler/Symbols/Symbols.fs index f2a84df2934..fb2666442d9 100644 --- a/src/Compiler/Symbols/Symbols.fs +++ b/src/Compiler/Symbols/Symbols.fs @@ -796,7 +796,8 @@ type FSharpEntity(cenv: SymbolEnv, entity: EntityRef, tyargs: TType list) = let ent = FSharpEntity(cenv, tcref, []) cache.TryAdd(tycon.Stamp, (ent :> obj)) |> ignore ent - | _ -> FSharpEntity(cenv, tcref) + | ValueSome _ -> FSharpEntity(cenv, tcref) + | ValueNone -> FSharpEntity(cenv, tcref, []) member _.Entity = entity diff --git a/tests/FSharp.Compiler.ComponentTests/FSharpChecker/SymbolUse.fs b/tests/FSharp.Compiler.ComponentTests/FSharpChecker/SymbolUse.fs index aa76f22af52..56377fce234 100644 --- a/tests/FSharp.Compiler.ComponentTests/FSharpChecker/SymbolUse.fs +++ b/tests/FSharp.Compiler.ComponentTests/FSharpChecker/SymbolUse.fs @@ -3,6 +3,7 @@ open FSharp.Compiler.CodeAnalysis open Xunit +open FSharp.Test.Compiler open FSharp.Test.ProjectGeneration open FSharp.Compiler.Symbols @@ -94,3 +95,39 @@ module IsPrivateToFile = let symbolUse = typeCheckResult.GetSymbolUseAtLocation(6, 14, "let private f3 x = x + 1", ["f3"]) |> Option.defaultWith (fun () -> failwith "no symbol use found") Assert.False(symbolUse.IsPrivateToFile)) } + +module UnresolvedReferences = + + let private compileLibrary name (cUnit: CompilationUnit) = + cUnit + |> withName name + |> withOutputDirectory (Some(TestFramework.createTemporaryDirectory ())) + |> asLibrary + |> compile + |> shouldSucceed + |> _.OutputPath.Value + + [] + let ``Type from a missing transitive reference is unresolved`` () = + let dependencyPath = + FSharp "module Dependency\ntype Gone() = class end" + |> compileLibrary "Dependency" + + let libraryPath = + FSharp "module ModuleApi\nlet missing = Dependency.Gone()" + |> withOptions [ $"-r:{dependencyPath}" ] + |> compileLibrary "Library" + + let checkResults = + FSharp "module Consumer" + |> withOptions [ $"-r:{libraryPath}" ] + |> typecheckResults + + let library = + checkResults.ProjectContext.GetReferencedAssemblies() + |> List.find (fun assembly -> assembly.SimpleName = "Library") + + let api = library.Contents.Entities |> Seq.find (fun entity -> entity.DisplayName = "ModuleApi") + let value = api.MembersFunctionsAndValues |> Seq.find (fun value -> value.DisplayName = "missing") + + Assert.True(value.FullType.TypeDefinition.IsUnresolved)