fsharp icon indicating copy to clipboard operation
fsharp copied to clipboard

No compile-time error when static abstract member accessed on type parameter whose resolved type has no concrete implementation for said member

Open brianrourkeboll opened this issue 2 months ago • 2 comments

There is no compile-time error when:

  1. You invoke a static abstract member on a type parameter that is constrained to be an interface with the given static abstract member (IWSAM).
  2. The type variable for that type parameter is automatically constrained to be the interface type itself (as opposed to a concrete type that has a concrete implementation of the member being invoked).

A System.Runtime.AmbiguousImplementationException is raised at runtime instead. This is raised even when the interface in question has only one static abstract member (or member of any kind).

Repro steps

#nowarn "3535"

type IFace =
    static abstract P1 : int

type T =
    interface IFace with
        static member P1 = 1

let inline p1<'T & #IFace> = 'T.P1

let _ = p1 // 'T is resolved to be IFace itself.

image

net8.0

Unhandled exception. System.Runtime.AmbiguousImplementationException: Could not call method 'Program+IFace.get_P1()' on interface 'Program+IFace' with type 'Program+IFace' from assembly '' because there are multiple incompatible interface methods overriding this method.
   at <StartupCode$ConsoleApp1>.$Program.main@() in D:\src\ConsoleApp1\ConsoleApp1\Program.fs:line 12

net9.0

Unhandled exception. System.Runtime.AmbiguousImplementationException: Ambiguous implementation found.
   at <StartupCode$ConsoleApp1>.$Program.main@() in D:\src\ConsoleApp1\ConsoleApp1\Program.fs:line 12

Expected behavior

Resolution of the type variable should fail at compile-time in the absence of further type information when the type variable has a constraint involving static abstract members — although ideally it would only fail when an abstract (as opposed to virtual) member was actually being accessed, which is not visible through a constraint like 'T & #IFace alone.

Actual behavior

A type variable like 'T & #IFace resolves to the interface type IFace itself, and a System.Runtime.AmbiguousImplementationException is raised at runtime.

Known workarounds

Always manually ensure that all type variables are resolved to types with concrete implementations for all static abstract methods that are invoked on them.

Related information

.NET 8/9 preview.

This problem is similar in spirit to #14012, #16299, etc., although it might be a bit trickier to fix.

brianrourkeboll avatar May 11 '24 21:05 brianrourkeboll

I honestly don't think we can resolve it easily for general case, since it's a constrained runtime call by design, and it would be hard to distinguish such cases in compile time.

We might though solve this specific one - for type functions, when type inferred is the interface.

vzarytovskii avatar May 12 '24 13:05 vzarytovskii

I honestly don't think we can resolve it easily for general case, since it's a constrained runtime call by design, and it would be hard to distinguish such cases in compile time.

Yeah I agree, especially since presumably the mechanism that resolves #IFaceIFace for an IWSAM is the same one that resolves, e.g., #('T seq)'T seq for a regular interface, which is definitely relied upon.

We might though solve this specific one - for type functions, when type inferred is the interface.

Unfortunately, the same thing happens when it's a regular function, too:

#nowarn "3535"

type IFace =
    static abstract P1 : int

type T =
    interface IFace with
        static member P1 = 1

let f<'T & #IFace> () = 'T.P1 + 'T.P1

f () // 'T is resolved to IFace.

On the other hand, this problem probably shouldn't affect most uses of IWSAMs in the wild, since a type parameter constrained by a (recursively) generic IWSAM (like anything from System.Numerics) will not silently resolve to the interface itself:

open System.Numerics

let g<'T & #INumber<'T>> () = 'T.Zero + 'T.One

let _ = g ()
// error FS0071: Type constraint mismatch when applying the default type 'INumber<'a>'
// for a type inference variable. The types ''a' and 'INumber<'a>' cannot be unified.
// Consider adding further type constraints

brianrourkeboll avatar May 12 '24 16:05 brianrourkeboll