Maybe a C# compiler bug in Visual Studio 2015

First off, it is important when analyzing these issues to make a minimal reproducer, so that we can narrow down where the problem is. In the original code there are three red herrings: the readonly, the static and the Nullable<T>. None are necessary to repro the issue. Here’s a minimal repro:

struct N<T> {}
struct M { public N<M> E; }
class P { static void Main() { var x = default(M); } }

This compiles in the current version of VS, but throws a type load exception when run.

  • The exception is not triggered by use of E. It is triggered by any attempt to access the type M. (As one would expect in the case of a type load exception.)
  • The exception reproduces whether the field is static or instance, readonly or not; this has nothing to do with the nature of the field. (However it must be a field! The issue does not repro if it is, say, a method.)
  • The exception has nothing whatsoever to do with “invocation”; nothing is being “invoked” in the minimal repro.
  • The exception has nothing whatsoever to do with the member access operator “.”. It does not appear in the minimal repro.
  • The exception has nothing whatsoever to do with nullables; nothing is nullable in the minimal repro.

Now let’s do some more experiments. What if we make N and M classes? I will tell you the results:

  • The behaviour only reproduces when both are structs.

We could go on to discuss whether the issue reproduces only when M in some sense “directly” mentions itself, or whether an “indirect” cycle also reproduces the bug. (The latter is true.) And as Corey notes in his answer, we could also ask “do the types have to be generic?” No; there is a reproducer even more minimal than this one with no generics.

However I think we have enough to complete our discussion of the reproducer and move on to the question at hand, which is “is it a bug, and if so, in what?”

Plainly something is messed up here, and I lack the time today to sort out where the blame ought to fall. Here are some thoughts:

  • The rule against structs containing members of themselves plainly does not apply here. (See section 11.3.1 of the C# 5 specification, which is the one I have present at hand. I note that this section could benefit from a careful rewriting with generics in mind; some of the language here is a bit imprecise.) If E is static then that section does not apply; if it is not static then the layouts of N<M> and M can both be computed regardless.

  • I know of no other rule in the C# language that would prohibit this arrangement of types.

  • It might be the case that the CLR specification prohibits this arrangement of types, and the CLR is right to throw an exception here.

So now let us sum up the possibilities:

  • The CLR has a bug. This type topology should be legal, and it is wrong of the CLR to throw here.

  • The CLR behaviour is correct. This type topology is illegal, and it is correct of the CLR to throw here. (In this scenario it may be the case that the CLR has a spec bug, in that this fact may not be adequately explained in the specification. I don’t have time to do CLR spec diving today.)

Let us suppose for the sake of argument that the second is true. What can we now say about C#? Some possibilities:

  • The C# language specification prohibits this program, but the implementation allows it. The implementation has a bug. (I believe this scenario to be false.)

  • The C# language specification does not prohibit this program, but it could be made to do so at a reasonable implementation cost. In this scenario the C# specification is at fault, it should be fixed, and the implementation should be fixed to match.

  • The C# language specification does not prohibit the program, but detecting the problem at compile time cannot be done at reasonable cost. This is the case with pretty much any runtime crash; your program crashed at runtime because the compiler couldn’t stop you from writing a buggy program. This is just one more buggy program; unfortunately, you had no reason to know it was buggy.

Summing up, our possibilities are:

  • The CLR has a bug
  • The C# spec has a bug
  • The C# implementation has a bug
  • The program has a bug

One of these four must be true. I do not know which it is. Were I asked to guess, I’d pick the first one; I see no reason why the CLR type loader ought to balk on this one. But perhaps there is a good reason that I do not know; hopefully an expert on the CLR type loading semantics will chime in.


UPDATE:

This issue is tracked here:

https://github.com/dotnet/roslyn/issues/10126

To sum up the conclusions from the C# team in that issue:

  • The program is legal according to both the CLI and C# specifications.
  • The C# 6 compiler allows the program, but some implementations of the CLI throw a type load exception. This is a bug in those implementations.
  • The CLR team is aware of the bug, and apparently it is hard to fix on the buggy implementations.
  • The C# team is considering making the legal code produce a warning, since it will fail at runtime on some, but not all, versions of the CLI.

The C# and CLR teams are on this; follow up with them. If you have any more concerns with this issue please post to the tracking issue, not here.

Leave a Comment