Skip to content
Draft
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/release-notes/.FSharp.Compiler.Service/11.0.0.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
* Fix: warn FS0049 on upper union case label. ([PR #19003](https://github.com/dotnet/fsharp/pull/19003))
* Type relations cache: handle potentially "infinite" types ([PR #19010](https://github.com/dotnet/fsharp/pull/19010))
* Disallow recursive structs with lifted type parameters ([Issue #18993](https://github.com/dotnet/fsharp/issues/18993), [PR #19031](https://github.com/dotnet/fsharp/pull/19031))
* Type relations cache: handle unsolved type variables ([Issue #19037](https://github.com/dotnet/fsharp/issues/19037)) ([PR #19040](https://github.com/dotnet/fsharp/pull/19040))

### Added

Expand Down
56 changes: 34 additions & 22 deletions src/Compiler/Utilities/TypeHashing.fs
Original file line number Diff line number Diff line change
Expand Up @@ -379,17 +379,6 @@ module HashTastMemberOrVals =
///
/// </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()

[<Struct; NoComparison; RequireQualifiedAccess>]
type TypeToken =
Expand All @@ -399,16 +388,17 @@ module StructuralUtilities =
| TupInfo of b: bool
| MeasureOne
| MeasureRational of int * int
| NeverEqual of never: NeverEqual
| Unsolved

type TypeStructure =
| TypeStructure of TypeToken[]
| PossiblyInfinite of never: NeverEqual
| UnsolvedTypeStructure of TypeToken[]
| PossiblyInfinite

let inline toNullnessToken (n: Nullness) =
match n.TryEvaluate() with
| ValueSome k -> TypeToken.Nullness k
| _ -> TypeToken.NeverEqual NeverEqual.Singleton
| _ -> TypeToken.Unsolved

let rec private accumulateMeasure (m: Measure) =
seq {
Expand All @@ -425,7 +415,14 @@ module StructuralUtilities =
TypeToken.MeasureRational(GetNumerator r, GetDenominator r)
}

let rec private accumulateTType (ty: TType) =
let rec private accumulateTypar (typar: Typar) =
seq {
match typar.Solution with
| Some ty -> yield! accumulateTType ty
Copy link
Member

Choose a reason for hiding this comment

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

In general, the input going into the cache is already stripped, right?

i.e. we should not be getting long chains of solution pointers for something which is solved.

Copy link
Contributor Author

@majocha majocha Nov 4, 2025

Choose a reason for hiding this comment

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

Yeah since this is operating solely on stripped types, if we encounter a type var, it should never be a solved one. In theory.

| None -> TypeToken.Unsolved
}

and private accumulateTType (ty: TType) =
seq {
match ty with
| TType_ucase(u, tinst) ->
Expand All @@ -441,40 +438,55 @@ module StructuralUtilities =

for arg in tinst do
yield! accumulateTType arg

| TType_anon(info, tys) ->
TypeToken.Stamp info.Stamp

for arg in tys do
yield! accumulateTType arg

| TType_tuple(tupInfo, tys) ->
TypeToken.TupInfo(evalTupInfoIsStruct tupInfo)

for arg in tys do
yield! accumulateTType arg

| TType_forall(tps, tau) ->
for tp in tps do
TypeToken.Stamp tp.Stamp
yield! accumulateTypar tp

yield! accumulateTType tau

| TType_fun(d, r, n) ->
yield! accumulateTType d
yield! accumulateTType r
toNullnessToken n

| TType_var(r, n) ->
TypeToken.Stamp r.Stamp
Copy link
Member

Choose a reason for hiding this comment

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

Now that the stamp is gone, I feel we are missing something to differentiate constraints.

Most likely types where constraints matter will be unsolved, so this will bypass the cache (via shouldCache=false) anyway. Right now cannot come up with a specific example of solved types where also constraints would matter for subsumption.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

shouldCache here only affects the memoization of TType -> TypeStructure, it will not prevent the caching of checkSubsumes result for unsolved.

Yeah this is missing constraints. I feel encoding them could cause this to explode in size.
What I completely missed also is the notion of rigidity. As I understand not all variables end up solved.

toNullnessToken n
yield! accumulateTypar r

| TType_measure m -> yield! accumulateMeasure 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
let private toTypeStructure (tokens: TypeToken seq) =
let tokens = tokens |> Seq.truncate 256 |> Seq.toArray

if tokens.Length = 256 then
PossiblyInfinite NeverEqual.Singleton
if Array.length tokens = 256 then
PossiblyInfinite
elif tokens |> Array.exists _.IsUnsolved then
UnsolvedTypeStructure tokens
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 shouldCache =
function
| PossiblyInfinite
| UnsolvedTypeStructure _ -> false
| _ -> true

// Speed up repeated calls by caching results for types that yield a stable structure.
Extras.WeakMap.cacheConditionally shouldCache (fun ty -> accumulateTType ty |> toTypeStructure)
12 changes: 12 additions & 0 deletions src/Compiler/Utilities/lib.fs
Original file line number Diff line number Diff line change
Expand Up @@ -473,3 +473,15 @@ module WeakMap =
// Cached factory to avoid allocating a new lambda per lookup.
let factory = ConditionalWeakTable.CreateValueCallback(fun k -> valueFactory k)
fun (key: 'Key when 'Key: not null) -> table.GetValue(key, factory)

/// Like getOrCreate, but only cache the value if it satisfies the given predicate.
let cacheConditionally shouldCache valueFactory =
let table = ConditionalWeakTable<_, _>()
fun (key: 'Key when 'Key: not null) ->
match table.TryGetValue key with
| true, value -> value
| false, _ ->
let value = valueFactory key
if shouldCache value then
try table.Add(key, value) with _ -> ()
value
5 changes: 5 additions & 0 deletions src/Compiler/Utilities/lib.fsi
Original file line number Diff line number Diff line change
Expand Up @@ -307,3 +307,8 @@ module internal WeakMap =
val internal getOrCreate:
valueFactory: ('Key -> 'Value) -> ('Key -> 'Value)
when 'Key: not struct and 'Key: not null and 'Value: not struct

/// Like getOrCreate, but only cache the value if it satisfies the given predicate.
val cacheConditionally:
shouldCache: ('Value -> bool) -> valueFactory: ('Key -> 'Value) -> ('Key -> 'Value)
when 'Key: not struct and 'Key: not null and 'Value: not struct
Original file line number Diff line number Diff line change
Expand Up @@ -277,6 +277,7 @@
<Compile Include="Interop\Literals.fs" />
<Compile Include="Scripting\Interactive.fs" />
<Compile Include="Scripting\TypeCheckOnlyTests.fs" />
<Compile Include="TypeChecks\TypeRelations.fs" />
<Compile Include="TypeChecks\SeqTypeCheckTests.fs" />
<Compile Include="TypeChecks\CheckDeclarationsTests.fs" />
<Compile Include="TypeChecks\Graph\Utils.fs" />
Expand Down
34 changes: 34 additions & 0 deletions tests/FSharp.Compiler.ComponentTests/TypeChecks/CrgpLibrary.fs
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
Loading