C
C#14mo ago
Kenji

✅ Recursive type definition

This is valid (basically CRTP):
class Base<T> { }
class Derived<T> : Base<Derived<T>> { }
class Base<T> { }
class Derived<T> : Base<Derived<T>> { }
This is not, because if you instantiate anywhere a new Derived<SomeType>(), the program panics with TypeLoaderException because of "recursive generic definition"
class Base<T> { }
class Derived<T> : Base<Derived<Derived<T>>> { }
class Base<T> { }
class Derived<T> : Base<Derived<Derived<T>>> { }
I cannot understand the different between these two generic definitions. If I specify SomeType, I would imagine that the generics should expand as expected and resolve the types correctly, as kind-0 types, e.g.: Derived<int> : Base<Derived<Derived<int>>> The only thing I could find is the official explanation on the CLI Standard here on page 129 https://www.ecma-international.org/publications-and-standards/standards/ecma-335/, but I do not quite get it. 🙂 Any ideas?
Elisa Denis
Ecma International
ECMA-335 - Ecma International
Common Language Infrastructure (CLI) - Defines the infrastructure in which applications written in multiple high-level languages can be executed
16 Replies
reflectronic
reflectronic14mo ago
if it throws TypeLoadException it’s likely an implementation limitation i would open an issue on dotnet/runtime doesn’t fail on mono so, there’s a bug somewhere
Kenji
Kenji14mo ago
but mono is not working as intended according to the spec in some cases. I think you are hitting my problem, but maybe 50% of it: - The first part is, "should this fail?" ( as i understand your answer is "no") - If yes, why does it fail? If no, you are most likely right, I should open a ticket (why runtime and not roslyn repo?)
reflectronic
reflectronic14mo ago
mono has fewer problems in this area it’s not a roslyn issue because it’s not a problem with the C# compiler if it was, you would get a compile error, or a compiler crash or. let me put it a different way here something must be bugged here. the bug is either: - the C# compiler is too permissive and creates invalid metadata. (on top of this, Mono incorrectly accepts this metadata.) - CoreCLR is rejecting valid metadata if this is valid metadata, then roslyn is naturally in the clear. i am inclined to believe this, because CoreCLR has many known bugs with generics and recursion. see this issue for another example: https://github.com/dotnet/runtime/issues/6924. the type loader is over 20 years old, originally written before generics were even in the picture, so there are some architectural issues that make this a hard area for CoreCLR plus, the fact that the code is considered valid by Mono and Roslyn suggests that this really is the problem
Kenji
Kenji14mo ago
that's a very astute point.
Kenji
Kenji14mo ago
My only issue with your logic is the CIL spec, that explicitly says this is not allowed:
Kenji
Kenji14mo ago
CLI (common language infra), not CIL** It's possible that the definition here of "infinite instantiation closure" is wrong. The problem with this assumption, is that I am an idiot and I don't understand the "proof" 😅 (which was one of my questions) If you want to try to succeed where I failed and understand it, check the ECMA link in my original post (page 155 on the pdf, 129 on the actual bottom-right corner)
reflectronic
reflectronic14mo ago
hm. that does seem more authoritative than me
Kenji
Kenji14mo ago
But I also agree with your logic about the roslyn vs runtime point. Since it generates IL, the problem is on the runtime that tries to create a concrete type out of the generic. Now the question is, should the runtime succeed in this, or is roslyn erroneously nodding along and letting me generate IL based on this C# snippet? 🤔
reflectronic
reflectronic14mo ago
i will think about it because it’s not immediately obvious to me either i know for a fact that some infinite expansion is possible, so i’ll have to square that with this
Kenji
Kenji14mo ago
sure thanks for the brainstorming
reflectronic
reflectronic14mo ago
okay, i am pretty sure the standard is correct. yeah, there’s something deceptively different about A<B<T>> and A<B<B<T>>>. they aren’t very similar 😅 consider this example:
class A<T> { T t; }
class B<T> : A<B<B<T>>> { }
class A<T> { T t; }
class B<T> : A<B<B<T>>> { }
consider what happens when you instantiate B. it instantiates A with B<B<T>>, so that is the type of the field. now considering the type of the field, the outer B is instantiated with B<T> (here is where the recursion begins), which instantiates A with B<B<B<T>>>. now that’s the type of the next field, which instantiates A with B<B<B<B<T>>>>. as you can see, as you try to compute the closure of field types, the nestedness keeps increasing infinitely. now consider:
class A<T> { T t; }
class B<T> : A<B<T>> { }
class A<T> { T t; }
class B<T> : A<B<T>> { }
instantiate B again, which instantiates A with B<T>, which is the type of the field. that type instantiates A with T. the cycle ends here since T does not instantiate B again. so, in this case, the nestedness is decreasing i’m not exactly sure yet how this is connected to the description in the standard. i’m not in a position to think that through right now, i’m on my phone and in public which makes it difficult to think hard about stuff. but i trust that it’s right, based on this so, there are two bugs 😅. now, the interesting question is, does the C# standard have this bug or the C# compiler
Kenji
Kenji14mo ago
I think I get what you mean. One issue I had reading your idea was about the valid case (one order of recursion, CRTP/CRGP), you said: in this case, the nestedness is decreasing This is not true, is it? It is not decreasing, but it is stable:
class A<T> { T t; }
class B<T> : A<B<T>> { }
class A<T> { T t; }
class B<T> : A<B<T>> { }
Consider instantiating B, which instantiates A with B<T>, which is the type of the field. The field then instantiates an A, with a field of type B<T>. Now if you expand this B<T> you would instantiate an A again with a B<T>, which is the type of the second-order field. You did not lose a level of nestedness, but you did not gain one either. Maybe this is what the spec means by "instantiation closure is finite" versus "instantiation closure is infinite"
reflectronic
reflectronic14mo ago
ah, yeah, that’s right. there’s no A<T> at any point, there couldn’t be, the only use of A is A<B<T>> yeah, that’s what it means, i think. the set of all instantiations needed is finite because it remains stable here. it just repeats A<B<T>>. and, of course, it’s infinite in the doubly nested case as we saw i think the initially confusing thing was that, if there are no fields (as in the first example) this wouldn’t matter because you would not need to explore the entire closure. i imagine they deliberately decided that this would always be checked, though, because adding a field of type T (even a private one!) would otherwise be a binary breaking change. the way it is now, it always doesn’t work, so there’s nothing to break
Kenji
Kenji14mo ago
yeah, that makes sense. 🙂 I think I will open an issue in the roslyn repo then, since this should not even compile, to prevent issues as early as possible
reflectronic
reflectronic14mo ago
yes, it should not be difficult for roslyn to check this. and mono is busted too, apparently (no big surprise), so you should probably file an issue on dotnet/runtime for that i cannot find any text in the C# standard forbidding this, so it may be treated as a language feature instead of a bug fix
Accord
Accord14mo ago
Was this issue resolved? If so, run /close - otherwise I will mark this as stale and this post will be archived until there is new activity.