-
Notifications
You must be signed in to change notification settings - Fork 833
Type subsumption cache: handle unsolved type vars #19040
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 10 commits
e3d1871
aec47e7
b4b951c
0939fba
ad681fa
5f86502
068b3b5
25357f8
73c80ff
e22f124
5065dc7
105e9aa
cb423e5
9c5037a
95711be
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,15 +1,13 @@ | ||
| module internal Internal.Utilities.TypeHashing | ||
|
|
||
| open Internal.Utilities.Rational | ||
| open Internal.Utilities.Library | ||
| open FSharp.Compiler.AbstractIL.IL | ||
| open FSharp.Compiler.Syntax | ||
| open FSharp.Compiler.TcGlobals | ||
| open FSharp.Compiler.Text | ||
| open FSharp.Compiler.TypedTree | ||
| open FSharp.Compiler.TypedTreeBasics | ||
| open FSharp.Compiler.TypedTreeOps | ||
| open System.Collections.Immutable | ||
|
|
||
| type ObserverVisibility = | ||
| | PublicOnly | ||
|
|
@@ -126,7 +124,6 @@ module HashAccessibility = | |
| | _ -> true | ||
|
|
||
| module rec HashTypes = | ||
| open Microsoft.FSharp.Core.LanguagePrimitives | ||
|
|
||
| /// Hash a reference to a type | ||
| let hashTyconRef tcref = hashTyconRefImpl tcref | ||
|
|
@@ -380,110 +377,154 @@ module HashTastMemberOrVals = | |
| /// * Uses per-compilation stamps (entities, typars, anon records, measures). | ||
| /// * Emits shape for union cases (declaring type stamp + case name), tuple structness, | ||
| /// function arrows, forall binders, nullness, measures, generic arguments. | ||
| /// * Unknown/variable nullness => NeverEqual token to force inequality (avoid unsound hits). | ||
| /// * Does not include type constraints. | ||
| /// | ||
| /// Non-goals: | ||
| /// * Cross-compilation stability. | ||
| /// * Perfect canonicalisation or alpha-equivalence collapsing. | ||
| /// | ||
| /// </summary> | ||
| module StructuralUtilities = | ||
| [<Struct; CustomEquality; NoComparison>] | ||
| type NeverEqual = | ||
| struct | ||
| interface System.IEquatable<NeverEqual> with | ||
| member _.Equals _ = false | ||
|
|
||
| override _.Equals _ = false | ||
| override _.GetHashCode() = 0 | ||
| end | ||
|
|
||
| static member Singleton = NeverEqual() | ||
| open Internal.Utilities.Library.Extras | ||
|
|
||
| [<Struct; NoComparison; RequireQualifiedAccess>] | ||
| type TypeToken = | ||
| | Stamp of stamp: Stamp | ||
| | UCase of name: string | ||
| | Nullness of nullness: NullnessInfo | ||
| | NullnessUnsolved | ||
| | TupInfo of b: bool | ||
| | Forall of int | ||
| | MeasureOne | ||
| | MeasureRational of int * int | ||
| | NeverEqual of never: NeverEqual | ||
| | Unsolved of int | ||
| | Rigid of int | ||
|
|
||
| type TypeStructure = | ||
| | TypeStructure of TypeToken[] | ||
| | PossiblyInfinite of never: NeverEqual | ||
| | Stable of TypeToken[] | ||
| | Unstable of TypeToken[] | ||
| | PossiblyInfinite | ||
|
|
||
| type private EmitContext = | ||
| { | ||
| typarMap: System.Collections.Generic.Dictionary<Stamp, int> | ||
| mutable stable: bool | ||
| } | ||
|
|
||
| let inline toNullnessToken (n: Nullness) = | ||
| let private emitNullness env (n: Nullness) = | ||
| match n.TryEvaluate() with | ||
| | ValueSome k -> TypeToken.Nullness k | ||
| | _ -> TypeToken.NeverEqual NeverEqual.Singleton | ||
| | ValueNone -> | ||
| env.stable <- false | ||
| TypeToken.NullnessUnsolved | ||
|
|
||
| let rec private accumulateMeasure (m: Measure) = | ||
| let rec private emitMeasure (m: Measure) = | ||
| seq { | ||
| match m with | ||
| | Measure.Var mv -> TypeToken.Stamp mv.Stamp | ||
| | Measure.Const(tcref, _) -> TypeToken.Stamp tcref.Stamp | ||
| | Measure.Prod(m1, m2, _) -> | ||
| yield! accumulateMeasure m1 | ||
| yield! accumulateMeasure m2 | ||
| | Measure.Inv m1 -> yield! accumulateMeasure m1 | ||
| yield! emitMeasure m1 | ||
| yield! emitMeasure m2 | ||
| | Measure.Inv m1 -> yield! emitMeasure m1 | ||
| | Measure.One _ -> TypeToken.MeasureOne | ||
| | Measure.RationalPower(m1, r) -> | ||
| yield! accumulateMeasure m1 | ||
| yield! emitMeasure m1 | ||
| TypeToken.MeasureRational(GetNumerator r, GetDenominator r) | ||
| } | ||
|
|
||
| let rec private accumulateTType (ty: TType) = | ||
| and private emitTType (env: EmitContext) (ty: TType) = | ||
| seq { | ||
| match ty with | ||
| | TType_ucase(u, tinst) -> | ||
| TypeToken.Stamp u.TyconRef.Stamp | ||
| TypeToken.UCase u.CaseName | ||
|
|
||
| for arg in tinst do | ||
| yield! accumulateTType arg | ||
| yield! emitTType env arg | ||
|
|
||
| | TType_app(tcref, tinst, n) -> | ||
| TypeToken.Stamp tcref.Stamp | ||
| toNullnessToken n | ||
| emitNullness env n | ||
|
|
||
| for arg in tinst do | ||
| yield! accumulateTType arg | ||
| yield! emitTType env arg | ||
|
|
||
| | TType_anon(info, tys) -> | ||
| TypeToken.Stamp info.Stamp | ||
|
|
||
| for arg in tys do | ||
| yield! accumulateTType arg | ||
| yield! emitTType env arg | ||
|
|
||
| | TType_tuple(tupInfo, tys) -> | ||
| TypeToken.TupInfo(evalTupInfoIsStruct tupInfo) | ||
|
|
||
| for arg in tys do | ||
| yield! accumulateTType arg | ||
| yield! emitTType env arg | ||
|
|
||
| | TType_forall(tps, tau) -> | ||
| for tp in tps do | ||
| TypeToken.Stamp tp.Stamp | ||
| env.typarMap.[tp.Stamp] <- env.typarMap.Count | ||
|
|
||
| TypeToken.Forall tps.Length | ||
|
|
||
| yield! emitTType env tau | ||
|
|
||
| yield! accumulateTType tau | ||
| | TType_fun(d, r, n) -> | ||
| yield! accumulateTType d | ||
| yield! accumulateTType r | ||
| toNullnessToken n | ||
| yield! emitTType env d | ||
| yield! emitTType env r | ||
| emitNullness env n | ||
|
|
||
| | TType_var(r, n) -> | ||
| TypeToken.Stamp r.Stamp | ||
T-Gro marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| toNullnessToken n | ||
| | TType_measure m -> yield! accumulateMeasure m | ||
| emitNullness env n | ||
|
|
||
| let typarId = | ||
| match env.typarMap.TryGetValue r.Stamp with | ||
| | true, idx -> idx | ||
| | _ -> | ||
| let idx = env.typarMap.Count | ||
| env.typarMap.[r.Stamp] <- idx | ||
T-Gro marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| idx | ||
|
|
||
| match r.Solution with | ||
| | Some ty -> yield! emitTType env ty | ||
| | None -> | ||
| if r.Rigidity = TyparRigidity.Rigid then | ||
| TypeToken.Rigid typarId | ||
| else | ||
| env.stable <- false | ||
| TypeToken.Unsolved typarId | ||
|
Comment on lines
493
to
503
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. A basic checklist, please check each:
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
This one I am not entirely sure. I'm trying to informally test by building FCS net10.0 and also OpenTK 5.0, both of which have quite different profiles of cache use, OpenTK now does not benefit from weak memoization. I left it in with the editor use in mind, but now that the cache works only on TType_apps, I wonder if it still helps. |
||
| | TType_measure m -> yield! emitMeasure m | ||
| } | ||
|
|
||
| // If the sequence got too long, just drop it, we could be dealing with an infinite type. | ||
| let private toTypeStructure tokens = | ||
| let tokens = tokens |> Seq.truncate 256 |> Array.ofSeq | ||
|
|
||
| if tokens.Length = 256 then | ||
| PossiblyInfinite NeverEqual.Singleton | ||
| else | ||
| TypeStructure tokens | ||
|
|
||
| /// Get the full structure of a type as a sequence of tokens, suitable for equality | ||
| let getTypeStructure = | ||
| Extras.WeakMap.getOrCreate (fun ty -> accumulateTType ty |> toTypeStructure) | ||
| let private getTypeStructureOfStrippedType (ty: TType) = | ||
|
|
||
| let env = | ||
| { | ||
| typarMap = System.Collections.Generic.Dictionary<Stamp, int>() | ||
| stable = true | ||
| } | ||
|
|
||
| let tokens = | ||
| emitTType env ty | ||
| |> Seq.filter (fun t -> t <> TypeToken.Nullness NullnessInfo.WithoutNull) | ||
|
||
| |> Seq.truncate 256 | ||
| |> Seq.toArray | ||
|
|
||
| // If the sequence got too long, just drop it, we could be dealing with an infinite type. | ||
| if tokens.Length = 256 then PossiblyInfinite | ||
| elif not env.stable then Unstable tokens | ||
| else Stable tokens | ||
|
|
||
| let tryGetTypeStructureOfStrippedType ty = | ||
| // Speed up repeated calls by memoizing results for types that yield a stable structure. | ||
| let memoize = | ||
majocha marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| WeakMap.cacheConditionally | ||
| (function | ||
| | Stable _ -> true | ||
| | _ -> false) | ||
| getTypeStructureOfStrippedType | ||
|
|
||
| match memoize ty with | ||
| | PossiblyInfinite -> ValueNone | ||
| | ts -> ValueSome ts | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,34 @@ | ||
| module MyModule | ||
|
|
||
| type IFoo<'T when 'T :> IFoo<'T>> = | ||
| abstract member Bar: other:'T -> unit | ||
|
|
||
| [<AbstractClass>] | ||
| type FooBase() = | ||
|
|
||
| interface IFoo<FooBase> with | ||
| member this.Bar (other: FooBase) = () | ||
|
|
||
| [<Sealed>] | ||
| type FooDerived<'T>() = | ||
| inherit FooBase() | ||
|
|
||
| interface IFoo<FooDerived<'T>> with | ||
| member this.Bar other = () | ||
|
|
||
| type IFooContainer<'T> = | ||
| abstract member Foo: FooDerived<'T> | ||
|
|
||
| let inline bar<'a when 'a :> IFoo<'a>> (x: 'a) (y: 'a) = x.Bar y | ||
| let inline takeSame<'a> (x: 'a) (y: 'a) = () | ||
|
|
||
| // Successfully compiles under .NET 9 + F# 9 | ||
| // Error under .NET 10 + F# 10: Program.fs(26,13): Error FS0193 : The type 'FooDerived<'TId>' does not match the type 'FooBase' | ||
| let callBar_NewlyBroken (foo1: IFooContainer<'TId>) (foo2: IFooContainer<'TId>) = | ||
| bar foo1.Foo foo2.Foo | ||
|
|
||
| // Successfully compiles under both versions | ||
| let callBar (foo1: IFooContainer<'TId>) (foo2: IFooContainer<'TId>) = | ||
| let id1 = foo1.Foo | ||
| let id2 = foo2.Foo | ||
| bar id1 id2 |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| module TypeChecks.TypeRelations | ||
|
|
||
| open Xunit | ||
| open FSharp.Test.Compiler | ||
| open FSharp.Test | ||
|
|
||
| [<Theory; FileInlineData("CrgpLibrary.fs")>] | ||
| let ``Unsolved type variables are not cached`` compilation = | ||
| compilation | ||
| |> getCompilation | ||
| |> typecheck | ||
| |> shouldSucceed |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The
tracebased mutation+rollback might be a problem for the weakcache, see e.g. herefsharp/src/Compiler/Checking/ConstraintSolver.fs
Lines 1045 to 1050 in 3828c87
Or another one here:
fsharp/src/Compiler/Checking/ConstraintSolver.fs
Line 961 in 3828c87
The typestructure has this covered, but weakcache has not.
But fully clearing the weakcache when a trace's undo is called is way too defensive and would likely sacrifice a lot of perf potential :-(
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think the weakcache is not as crucial now, when we limit only to TType_app. Is nullness taken into account for
TypeFeasiblySubsumesType? Maybe it's possible to not emit it at all.(In the long run type structure generation could be made more configurable if needed, too.)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, this happens not just for nullness.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, happens for typar solutions - e.g. method overloading, also in context of picking SRTP overloads.
If we are in a
Traceenvironment within constraint solving, it would be safest to never add things to the WeakCache.(alternatively add it there, but then remove at
Undo- but there might be overhead in keeping track what to remove, I guess safer not to add...)There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, as first approximation we can just not memoize when any typars come into play, solved or not. Good thing the performance is still way better than before.