-
Notifications
You must be signed in to change notification settings - Fork 831
Description
Issue description
The interpretation of the null constraint has changed between F# 8 and 9 in a way that breaks overload resolution happening as a result of using the member constraint, whereby a non-nullable reference type is nonetheless considered nullable. This issue manifests only when an assembly compiled on pre-F# 9 is referenced in F# 9; if the assembly is updated to F# 9 (even without --checknulls), the issue goes away.
Choose one or more from the following categories of impact
- Unexpected nullness warning (false positive in nullness checking, code uses --checknulls and langversion:preview).
- Missing nullness warning in a case which can produce nulls (false negative, code uses --checknulls and langversion:preview).
- Breaking change related to older
nullconstructs in code not using the checknulls switch. - Breaking change related to generic code and explicit type constraints (
null,not null). - Type inference issue (i.e. code worked without type annotations before, and applying the --checknulls enforces type annotations).
- C#/F# interop issue related to nullness metadata.
- Other (none of the categories above apply).
Operating System
Windows (Default)
What .NET runtime/SDK kind are you seeing the issue on
.NET SDK (.NET Core, .NET 5+)
.NET Runtime/SDK version
.NET SDK 9.0.201
Reproducible code snippet and actual behavior
The following code (compiled with F# 8) defines a function Test.Method that has the static member M constraint. This member is satisfiable by two methods, one with x : NonNullType<_> and one with x : ^T when ^T : null:
namespace Library
open System
type NonNullType<'T> = class end
type C1 = C1
type C2 = C2
type C1 with
static member inline M(C1, C2, x : NonNullType<_>) =
true
static member inline M(C1, C2, x : ^T when ^T : null) =
false
type C2 with
// Impossible balancing overload
static member inline M(C1, C2, x : ^T when ^T :> Enum and ^T : not struct and ^T : (new : unit -> ^T)) =
()
module Test =
let inline Method x =
((^self1 or ^self2 or ^x) : (static member M: ^self1 * ^self2 * ^x -> _) C1, C2, x)In a different project (compiled with F# 9) the code above is referenced and used:
open Library
let a : NonNullType<int> = Unchecked.defaultof<_>
let b = Test.Method(a)
printf "%b" bThis results in an error:
error FS0043: A unique overload for method 'M' could not be determined based on type information prior to this program point. A type annotation may be needed.
Known return type: 'a
Known type parameters: < C1 , C2 , NonNullType >
Candidates:
- static member C1.M: C1: C1 * C2: C2 * x: NonNullType<'a> -> bool
- static member C1.M: C1: C1 * C2: C2 * x: ^T -> bool when ^T: null
The second candidate with x : ^T when ^T : null is picked even though NonNullType is definitely non-nullable (which can be attested by observing that let _ : NonNullType<int> = null results in an error).
The only way for this code to be successfully compiled and executed is either both projects being compiled on F# 8 (or older) or both projects being compiled on F# 9. In that case, true is printed.
The same issue can also be demonstrated by removing the x : NonNullType<_> overload. If compiled on F# 8, the code above instead prints false, and errors if compiled on F# 9. It should also be noted that this happens only through member constraints. Normal overload resolution does not seem to be affected (the type is correctly treated as non-nullable).
Attaching the solution (run dotnet run in Test):
FSharpNullOverloadResolutionExample.zip
Possible workarounds
I could not find a solution which did not involve updating the F# version. I did not find any difference in attributes between the two language versions for the assembly, so it must be something in the hidden resource.
Metadata
Metadata
Assignees
Labels
Type
Projects
Status