Note: the following is NOT an RFC. I am explicitly not proposing any changes to any aspect of Rust. I don't need the functionality described herein, and I wouldn't have the motivation for it, either. I am just genuinely curious about a couple of technical details that I think are still somewhat unclear to me.
So, while attempting to answer this question on URLO, I realized that I don't have a completely satisfactory and compelling reason to back up and motivate my claim that TypeId
essentially has to impose a 'static
lifetime bound.
When I first learned about this issue, it seemed instinctively obvious. Lifetimes are just not a thing at runtime, they are context-dependent, they have subtyping relationships, and there is the issue of recursive functions, with some of their lifetimes only valid within the same function call.
However, while I'm clueless as to how I would even go about implementing the TypeId
intrinsic into the compiler for non-'static
lifetimes, I still couldn't convince myself 100% that context dependence and subtyping do indeed make lifetimes infeasible to encode dynamically. Why is it so? The compiler does erase lifetime information before codegen, but maybe it doesn't have to erase them. So, my best guesses so far were:
-
A lifetime-dependent
TypeId
would require global (interprocedural or even cross-crate) analysis of the entire code. The context-dependence of lifetimes means that named lifetimes (except for'static
) are always generic parameters, so they may change depending on the caller. Therefore, it's impossible to assign a single, concrete, globally unique ID to a given lifetime – just like the compiler isn't allowed to assign the same value to a function argument across different invocations of the same function. -
Perhaps this is also where recursion comes into play? Maybe, just maybe, recursion means that even if the compiler performed a complete global analysis of the code, type IDs of lifetimes would need to change dynamically within a recursive function, with every new level of the call.
This seems contradictory to me, because it would break the "lifetimes are a purely compile-time construct" assumption. But what if someone were to put addresses of local variables in a global
Vec<Box<dyn Any>>
? Surely a properly implemented non-'static
Any
would allow that, while ensuring that downcasting fails outside the scope of the local variable being referenced. -
It can be hard to encode subtyping relationships efficiently. An implementation I could think of would be to construct a tree of globally-available
TypeId
s, where every subtype would point to its supertype.But this might mean generating a prohibitively large tree. I remember that someone once proposed on IRLO that multi-trait objects be implemented by generating vtables for all possible combinations of all object-safe traits. Unsurprisingly, this was deemed unacceptable as there would then be a combinatorial explosion in compile time and binary sizes. Would the same problem arise with properly subtyped
TypeId
s?
Are any of these guesses of mine correct or relevant? Could someone more familiar with the internals of type lowering shed some light on the most important concrete issues with lifetime-aware TypeId
?