Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 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
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -137,3 +137,7 @@ positive.exe
*.bsl.actual
/src/FSharp.DependencyManager.Nuget/StandardError.txt
/src/FSharp.DependencyManager.Nuget/StandardOutput.txt

# Standard output/error files in root directory
StandardOutput.txt
StandardError.txt
1 change: 1 addition & 0 deletions docs/release-notes/.Language/preview.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,6 @@
### Fixed

* Warn on uppercase identifiers in patterns. ([PR #15816](https://github.com/dotnet/fsharp/pull/15816))
* Fix nullness warnings when casting non-nullable values to `IEquatable<T>` to match C# behavior. ([Issue #18759](https://github.com/dotnet/fsharp/issues/18759))

### Changed
15 changes: 11 additions & 4 deletions src/Compiler/Checking/ConstraintSolver.fs
Original file line number Diff line number Diff line change
Expand Up @@ -1407,11 +1407,17 @@ and SolveTypeEqualsTypeEqns csenv ndeep m2 trace cxsln origl1 origl2 =
ErrorD(ConstraintSolverTupleDiffLengths(csenv.DisplayEnv, csenv.eContextInfo, origl1, origl2, csenv.m, m2))
loop origl1 origl2

and SolveTypeEqualsTypeWithContravarianceEqns (csenv:ConstraintSolverEnv) ndeep m2 trace cxsln origl1 origl2 typars =
and SolveTypeEqualsTypeWithContravarianceEqns (csenv:ConstraintSolverEnv) ndeep m2 trace cxsln origl1 origl2 typars tyconRef =
let isContravariant (t:Typar) =
t.typar_opt_data
|> Option.map (fun d -> d.typar_is_contravariant)
|> Option.defaultValue(false)

// Special case for IEquatable<T>: treat its type parameter as contravariant for nullness purposes
// This matches C# behavior where IEquatable<T> is treated as contravariant for nullness, even though
// it's not formally marked as contravariant in IL.
// See: https://github.com/dotnet/fsharp/issues/18759 and https://github.com/dotnet/roslyn/issues/37187
let isIEquatableContravariantForNullness = tyconRefEq csenv.g tyconRef csenv.g.system_GenericIEquatable_tcref

match origl1, origl2, typars with
| [], [], [] -> CompleteD
Expand All @@ -1425,7 +1431,8 @@ and SolveTypeEqualsTypeWithContravarianceEqns (csenv:ConstraintSolverEnv) ndeep
let h1 =
// For contravariant typars (`<in T> in C#'), if the required type is WithNull, the actual type can have any nullness it wants
// Without this added logic, their nullness would be forced to be equal.
if isContravariant hTp && (nullnessOfTy csenv.g h2).TryEvaluate() = ValueSome NullnessInfo.WithNull then
// Special case: IEquatable<T> is treated as contravariant for nullness purposes to match C# behavior
if (isContravariant hTp || isIEquatableContravariantForNullness) && (nullnessOfTy csenv.g h2).TryEvaluate() = ValueSome NullnessInfo.WithNull then
replaceNullnessOfTy csenv.g.knownWithNull h1
else
h1
Expand Down Expand Up @@ -1534,11 +1541,11 @@ and SolveTypeSubsumesType (csenv: ConstraintSolverEnv) ndeep m2 (trace: Optional
(tyconRefEq g tagc1 g.byrefkind_In_tcr || tyconRefEq g tagc1 g.byrefkind_Out_tcr) ) -> ()
| _ -> return! SolveTypeEqualsType csenv ndeep m2 trace cxsln tag1 tag2
}
| _ -> SolveTypeEqualsTypeWithContravarianceEqns csenv ndeep m2 trace cxsln l1 l2 tc1.TyparsNoRange
| _ -> SolveTypeEqualsTypeWithContravarianceEqns csenv ndeep m2 trace cxsln l1 l2 tc1.TyparsNoRange tc1

| TType_app (tc1, l1, _) , TType_app (tc2, l2, _) when tyconRefEq g tc1 tc2 ->
trackErrors {
do! SolveTypeEqualsTypeWithContravarianceEqns csenv ndeep m2 trace cxsln l1 l2 tc1.TyparsNoRange
do! SolveTypeEqualsTypeWithContravarianceEqns csenv ndeep m2 trace cxsln l1 l2 tc1.TyparsNoRange tc1
do! SolveNullnessSubsumesNullness csenv m2 trace ty1 ty2 (nullnessOfTy g sty1) (nullnessOfTy g sty2)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1540,4 +1540,17 @@ let main _ =
|> compile
//|> verifyIL ["abc"]
|> run
|> verifyOutputContains [|"Test true;,1 true,2 true,3 true,4 true,5 true,6 false,7 true,8 false,9 false,10 false,11 false,12 true"|]
|> verifyOutputContains [|"Test true;,1 true,2 true,3 true,4 true,5 true,6 false,7 true,8 false,9 false,10 false,11 false,12 true"|]

[<Fact>]
let ``No nullness warning when casting non-nullable to IEquatable`` () =
FSharp """module Test

open System

let x = ""
let y = x :> IEquatable<string> // Should not warn about nullness
"""
|> asLibrary
|> typeCheckWithStrictNullness
|> shouldSucceed
Loading