generic method substitutions and unification

It's been a while since I've last written - my apologies. We've been hard at work figuring out what the next release of C# will look like, and I'm happy to say that I'm very excited about what we're working on. Great minds are at work figuring out things like integration of C# with the DLR, dynamic late binding, and better debugging experiences as we speak. Don't worry, I'll be sure to report to you about what they determine. :)

I'm always fascinated by discovering subtle details in the language that baffle me. Today I came across such a case. Consider the following code:

 using System;

abstract class A<T>
{
    public abstract void Foo(T x);
}

abstract class B<T, S> : A<T>
{
    public virtual void Foo(S x){}
}

abstract class C<T, S> : B<T, S>
{
    public abstract override void Foo(T x);
}

class D : C<int, int>
{
    public override void Foo(int x) {}

    static void Main()
    {
        new D().Foo(1);
    }
}

What happens here?

Well, on the surface, this looks like it should be correct right? A's Foo and B's Foo are different, since they have different generic type parameters, and C clearly overrides A's Foo. But the weird part is D's Foo. Does it override A::Foo? Or B::Foo?

Well, it turns out that the specification outlines that when we look up members to override, we look from our current parent up - that is, when we find a method in one base class, we use that, regardless of other matches in higher ancestral classes. So since C is D's direct base class, D::Foo overrides C::Foo, which overrides the abstract A::Foo.

So we should be good here right? A::Foo has an implementation in D::Foo, and B::Foo has its own implementation as well. But we're not. We get a runtime type load exception, which complains that D has a method Foo with no body.

Well that's odd! I thought we just said that everything has an implementation!

So, being the inquisitive minded person that I am, (and of course, having a bug open to fix this issue), I went and poked around, and discovered that we could fix this problem by having the compiler emit the .override instruction, so that the CLR will know exactly which method we're overriding.

Well neat, that fixes that problem. Or so I thought!

Bug number two looks a little different, but very similar.

 using System;

class A<T>
{
    protected virtual void Foo(T x){}
}

class B<T, S> : A<T>
{
    public virtual void Foo(S x) {}
}

class C<T, S> : B<T, S>
{
    protected override void Foo(T x) { }
}

class D : C<int, int>
{
    protected override void Foo(int x) {}
    
    static void Main()
    {
        new D();
    }
}

In this scenario, the compiler again allows us to compile the code without problems, and once again we get a runtime exception. This time, we get an exception saying that we're attempting to lower the access of B::Foo. Interesting. This looks like my first bug! So what do I do? I apply the fix to my first bug and see what happens.

No luck.

"But why not?", you ask? Well, after more digging (and finding the right contacts on the CLR team to chat with), we discovered that the initial fix was wrong! It turns out that by specifying the .override command, we tell the CLR to override A::Foo, but the CLR also finds B::Foo and tells D:Foo to override that as well.

At the end of it all, we discovered that the entire scenario is an error. Why? The CLI spec, chapter 11.9.9 "Inheritance and overriding" outlines:

Type definitions are invalid if, after substituting base class generic arguments, two methods result in the same name and signature (including return type). The following illustrates this point:
[Example:
.class B`1<T>
{ .method public virtual void V(!0 t) { … }
.method public virtual void V(string x) { … }
}
.class D extends B`1<string> { } // Invalid
Class D is invalid, because it will inherit from B<string> two methods with identical signatures:
void V(string)

At the end of it all, we discovered that we have a disconnect between the CLR and the C# compiler here. We're currently working on determining what C# spec changes we'll need to make to clarify this case, and from there we'll put a fix together.

So what did I learn today? Two things:

1) Our customer that found this bug (I only know them as nikov) is great at finding an exploit, and then massaging it in all possible ways to find other problems (which I might add, is phenomenal).

2) People should just not code like this cause its confusing and doesn't work :)