From 38e2a17af59e03112e20424a150cf942b3de542e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 21 Jul 2025 14:43:51 +0000 Subject: [PATCH 1/8] Initial plan From 281f22911f1cb550be6ba2a543fea79f431fb8fd Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 21 Jul 2025 15:20:32 +0000 Subject: [PATCH 2/8] Implement IEquatable nullness contravariance fix Co-authored-by: T-Gro <46543583+T-Gro@users.noreply.github.com> --- src/Compiler/Checking/ConstraintSolver.fs | 24 +++++++++++++------ .../Nullness/NullableReferenceTypesTests.fs | 13 ++++++++++ 2 files changed, 30 insertions(+), 7 deletions(-) diff --git a/src/Compiler/Checking/ConstraintSolver.fs b/src/Compiler/Checking/ConstraintSolver.fs index 02829f564ad..dbf5f850c1f 100644 --- a/src/Compiler/Checking/ConstraintSolver.fs +++ b/src/Compiler/Checking/ConstraintSolver.fs @@ -1407,17 +1407,26 @@ 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: treat its type parameter as contravariant for nullness purposes + // This matches C# behavior where IEquatable 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 (tycon:TyconRef option) (idx:int) = + match tycon with + | Some tc when tyconRefEq csenv.g tc csenv.g.system_GenericIEquatable_tcref && idx = 0 -> true + | _ -> false match origl1, origl2, typars with | [], [], [] -> CompleteD | _ -> // We unwind Iterate2D by hand here for performance reasons. - let rec loop l1 l2 tps = + let rec loop l1 l2 tps idx = match l1, l2, tps with | [], [], [] -> CompleteD | h1 :: t1, h2 :: t2, hTp :: tTps when t1.Length = t2.Length && t1.Length = tTps.Length -> @@ -1425,17 +1434,18 @@ and SolveTypeEqualsTypeWithContravarianceEqns (csenv:ConstraintSolverEnv) ndeep let h1 = // For contravariant typars (` 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 is treated as contravariant for nullness purposes to match C# behavior + if (isContravariant hTp || isIEquatableContravariantForNullness tyconRef idx) && (nullnessOfTy csenv.g h2).TryEvaluate() = ValueSome NullnessInfo.WithNull then replaceNullnessOfTy csenv.g.knownWithNull h1 else h1 do! SolveTypeEqualsTypeKeepAbbrevsWithCxsln csenv ndeep m2 trace cxsln h1 h2 - do! loop t1 t2 tTps + do! loop t1 t2 tTps (idx + 1) } | _ -> ErrorD(ConstraintSolverTupleDiffLengths(csenv.DisplayEnv, csenv.eContextInfo, origl1, origl2, csenv.m, m2)) - loop origl1 origl2 typars + loop origl1 origl2 typars 0 and SolveFunTypeEqn csenv ndeep m2 trace cxsln domainTy1 domainTy2 rangeTy1 rangeTy2 = trackErrors { @@ -1534,11 +1544,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 (Some 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 (Some tc1) do! SolveNullnessSubsumesNullness csenv m2 trace ty1 ty2 (nullnessOfTy g sty1) (nullnessOfTy g sty2) } diff --git a/tests/FSharp.Compiler.ComponentTests/Language/Nullness/NullableReferenceTypesTests.fs b/tests/FSharp.Compiler.ComponentTests/Language/Nullness/NullableReferenceTypesTests.fs index ced7e02fdca..6e94654ab48 100644 --- a/tests/FSharp.Compiler.ComponentTests/Language/Nullness/NullableReferenceTypesTests.fs +++ b/tests/FSharp.Compiler.ComponentTests/Language/Nullness/NullableReferenceTypesTests.fs @@ -1540,4 +1540,17 @@ let main _ = |> compile //|> verifyIL ["abc"] |> run + +[] +let ``No nullness warning when casting non-nullable to IEquatable`` () = + FSharp """module Test + +open System + +let x = "" +let y = x :> IEquatable // Should not warn about nullness + """ + |> asLibrary + |> typeCheckWithStrictNullness + |> shouldSucceed |> 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"|] \ No newline at end of file From f97affdfc9fd746a542b68035d2e9bc8d13f4c96 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 21 Jul 2025 15:26:25 +0000 Subject: [PATCH 3/8] Final validation of IEquatable nullness contravariance fix Co-authored-by: T-Gro <46543583+T-Gro@users.noreply.github.com> --- StandardError.txt | 0 StandardOutput.txt | 1 + 2 files changed, 1 insertion(+) create mode 100644 StandardError.txt create mode 100644 StandardOutput.txt diff --git a/StandardError.txt b/StandardError.txt new file mode 100644 index 00000000000..e69de29bb2d diff --git a/StandardOutput.txt b/StandardOutput.txt new file mode 100644 index 00000000000..72030ac8371 --- /dev/null +++ b/StandardOutput.txt @@ -0,0 +1 @@ +10.0.100-preview.6.25315.102 From 89b26abb59e51a7d4903563f2211f3e5fd196a4a Mon Sep 17 00:00:00 2001 From: Tomas Grosup Date: Tue, 22 Jul 2025 09:07:25 +0200 Subject: [PATCH 4/8] Update tests/FSharp.Compiler.ComponentTests/Language/Nullness/NullableReferenceTypesTests.fs --- .../Language/Nullness/NullableReferenceTypesTests.fs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/FSharp.Compiler.ComponentTests/Language/Nullness/NullableReferenceTypesTests.fs b/tests/FSharp.Compiler.ComponentTests/Language/Nullness/NullableReferenceTypesTests.fs index 6e94654ab48..4d0710ba899 100644 --- a/tests/FSharp.Compiler.ComponentTests/Language/Nullness/NullableReferenceTypesTests.fs +++ b/tests/FSharp.Compiler.ComponentTests/Language/Nullness/NullableReferenceTypesTests.fs @@ -1540,6 +1540,7 @@ 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"|] [] let ``No nullness warning when casting non-nullable to IEquatable`` () = @@ -1553,4 +1554,3 @@ let y = x :> IEquatable // Should not warn about nullness |> asLibrary |> typeCheckWithStrictNullness |> shouldSucceed - |> 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"|] \ No newline at end of file From ad2a833fb651f2bfe2c6debaa248fc6603dfe11c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 22 Jul 2025 07:33:36 +0000 Subject: [PATCH 5/8] Simplify IEquatable contravariance logic per code review feedback Co-authored-by: T-Gro <46543583+T-Gro@users.noreply.github.com> --- src/Compiler/Checking/ConstraintSolver.fs | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/src/Compiler/Checking/ConstraintSolver.fs b/src/Compiler/Checking/ConstraintSolver.fs index dbf5f850c1f..1132eefa448 100644 --- a/src/Compiler/Checking/ConstraintSolver.fs +++ b/src/Compiler/Checking/ConstraintSolver.fs @@ -1417,16 +1417,13 @@ and SolveTypeEqualsTypeWithContravarianceEqns (csenv:ConstraintSolverEnv) ndeep // This matches C# behavior where IEquatable 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 (tycon:TyconRef option) (idx:int) = - match tycon with - | Some tc when tyconRefEq csenv.g tc csenv.g.system_GenericIEquatable_tcref && idx = 0 -> true - | _ -> false + let isIEquatableContravariantForNullness = tyconRefEq csenv.g tyconRef csenv.g.system_GenericIEquatable_tcref match origl1, origl2, typars with | [], [], [] -> CompleteD | _ -> // We unwind Iterate2D by hand here for performance reasons. - let rec loop l1 l2 tps idx = + let rec loop l1 l2 tps = match l1, l2, tps with | [], [], [] -> CompleteD | h1 :: t1, h2 :: t2, hTp :: tTps when t1.Length = t2.Length && t1.Length = tTps.Length -> @@ -1435,17 +1432,17 @@ and SolveTypeEqualsTypeWithContravarianceEqns (csenv:ConstraintSolverEnv) ndeep // For contravariant typars (` 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. // Special case: IEquatable is treated as contravariant for nullness purposes to match C# behavior - if (isContravariant hTp || isIEquatableContravariantForNullness tyconRef idx) && (nullnessOfTy csenv.g h2).TryEvaluate() = ValueSome NullnessInfo.WithNull then + if (isContravariant hTp || isIEquatableContravariantForNullness) && (nullnessOfTy csenv.g h2).TryEvaluate() = ValueSome NullnessInfo.WithNull then replaceNullnessOfTy csenv.g.knownWithNull h1 else h1 do! SolveTypeEqualsTypeKeepAbbrevsWithCxsln csenv ndeep m2 trace cxsln h1 h2 - do! loop t1 t2 tTps (idx + 1) + do! loop t1 t2 tTps } | _ -> ErrorD(ConstraintSolverTupleDiffLengths(csenv.DisplayEnv, csenv.eContextInfo, origl1, origl2, csenv.m, m2)) - loop origl1 origl2 typars 0 + loop origl1 origl2 typars and SolveFunTypeEqn csenv ndeep m2 trace cxsln domainTy1 domainTy2 rangeTy1 rangeTy2 = trackErrors { @@ -1544,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 (Some tc1) + | _ -> 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 (Some tc1) + 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) } From 5c8ea05609eb6daf95cda0208a1eed83df5166a8 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 22 Jul 2025 08:29:45 +0000 Subject: [PATCH 6/8] Remove StandardOutput.txt and StandardError.txt files and add them to .gitignore Co-authored-by: T-Gro <46543583+T-Gro@users.noreply.github.com> --- .gitignore | 4 ++++ StandardError.txt | 0 StandardOutput.txt | 1 - 3 files changed, 4 insertions(+), 1 deletion(-) delete mode 100644 StandardError.txt delete mode 100644 StandardOutput.txt diff --git a/.gitignore b/.gitignore index 6ca107a5779..a5b80d11cc0 100644 --- a/.gitignore +++ b/.gitignore @@ -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 diff --git a/StandardError.txt b/StandardError.txt deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/StandardOutput.txt b/StandardOutput.txt deleted file mode 100644 index 72030ac8371..00000000000 --- a/StandardOutput.txt +++ /dev/null @@ -1 +0,0 @@ -10.0.100-preview.6.25315.102 From 2d07f3dc04a065cfbbfab813e6ebd3c405c06d79 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 22 Jul 2025 10:39:02 +0000 Subject: [PATCH 7/8] Add release notes entry for IEquatable nullness fix Co-authored-by: T-Gro <46543583+T-Gro@users.noreply.github.com> --- docs/release-notes/.Language/preview.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes/.Language/preview.md b/docs/release-notes/.Language/preview.md index 687402597c5..3d1a991e596 100644 --- a/docs/release-notes/.Language/preview.md +++ b/docs/release-notes/.Language/preview.md @@ -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` to match C# behavior. ([Issue #18759](https://github.com/dotnet/fsharp/issues/18759)) ### Changed From 369234f34e6013a6197cd4e0743873ac444bf020 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 25 Jul 2025 09:31:20 +0000 Subject: [PATCH 8/8] Move IEquatable nullness fix release note to FSharp.Compiler.Service Co-authored-by: T-Gro <46543583+T-Gro@users.noreply.github.com> --- docs/release-notes/.FSharp.Compiler.Service/10.0.100.md | 1 + docs/release-notes/.Language/preview.md | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/release-notes/.FSharp.Compiler.Service/10.0.100.md b/docs/release-notes/.FSharp.Compiler.Service/10.0.100.md index c7f68777166..8276598fab9 100644 --- a/docs/release-notes/.FSharp.Compiler.Service/10.0.100.md +++ b/docs/release-notes/.FSharp.Compiler.Service/10.0.100.md @@ -13,6 +13,7 @@ * Shorthand lambda: fix completion for chained calls and analysis for unfinished expression ([PR #18560](https://github.com/dotnet/fsharp/pull/18560)) * Completion: fix previous namespace considered opened [PR #18609](https://github.com/dotnet/fsharp/pull/18609) * Fix active pattern typechecking regression. ([Issue #18638](https://github.com/dotnet/fsharp/issues/18638), [PR #18642](https://github.com/dotnet/fsharp/pull/18642)) +* Fix nullness warnings when casting non-nullable values to `IEquatable` to match C# behavior. ([Issue #18759](https://github.com/dotnet/fsharp/issues/18759)) ### Changed * Use `errorR` instead of `error` in `CheckDeclarations.fs` when possible. ([PR #18645](https://github.com/dotnet/fsharp/pull/18645)) diff --git a/docs/release-notes/.Language/preview.md b/docs/release-notes/.Language/preview.md index 3d1a991e596..687402597c5 100644 --- a/docs/release-notes/.Language/preview.md +++ b/docs/release-notes/.Language/preview.md @@ -13,6 +13,5 @@ ### 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` to match C# behavior. ([Issue #18759](https://github.com/dotnet/fsharp/issues/18759)) ### Changed