Would non-`'static` TypeId be at all possible?

Well, that's the problem. Lifetimes are such a purely compile-time construct that they're completely erased at runtime.

Consider this function signature:

// If 'b: 'a, then returns Some(x), else returns None
fn downcast_lifetime<'a, 'b>(x: &'a u32) -> Option<&'b u32>

If there was a version of Any that could dynamically test for lifetimes, you could use it to implement downcast_lifetime.

But how would that actually work? downcast_lifetime can be called with either 'a or 'b as the longer lifetime (or they could be equal). Suppose there is one code path that calls downcast where 'b: 'a holds, and another where it doesn't hold. Then no matter how sophisticated a global analysis the compiler performs, it obviously can't prove statically that 'b: 'a either holds or doesn't hold.

So there are two options: it could encode the lifetime at runtime as part of the reference, or it could try to monomorphize functions based on lifetime.

Encoding it at runtime would come with overhead; every reference would probably have to double in size, to store some integer representing the lifetime along with the actual pointer value. It might be possible for the compiler to optimize away the lifetime value in cases where it's known not to be used – sometimes. If you're using references as local variables or function arguments passed by value, and not doing any dynamic dispatch, it might be possible to elide most unneeded lifetimes... with a sophisticated (and slow) global analysis. But what if you have a Vec<&Foo>? The compiler is not going to magically compress the Vec elements in cases where they can be compressed. It's not that smart.

Monomorphizing functions based on lifetime would be difficult. Some might say it's impossible, since the existence of recursion means that there can be an unbounded number of nested lifetimes. But I think (could be wrong) that you'd 'only' need a separate copy of the function for every possible combination of outlives relationships. The number of possible combinations is finite, sure enough… but it grows exponentially in the number of lifetimes. Now, as long as you stick to static calls, you'd only have to monomorphize the combinations that were actually used. But what about dynamic dispatch? Rust allows lifetime generics in trait objects, precisely because lifetimes are erased. To make this work with monomorphized lifetimes, a trait object vtable would need one slot for every possible combination. That's not going to work.

(Also, even ignoring the exponential growth issue, having to compile functions even two or three times just because they were used with different lifetimes would be a massive compile time hit. Maybe you could reduce the impact by carefully keeping track of when lifetimes do and don't affect the end result.)

With all that said, I believe it is possible to make a version of Any that works with non-'static objects, but where all lifetimes involved are known at compile time. You could convert from Foo<'a> to AnyA<'a> and back, but you couldn't convert from Foo<'static> to AnyA<'a>. (You could convert from Foo<'static> to Foo<'a> first, if it happens to be a covariant lifetime parameter, but there would be no way to recover the 'static lifetime afterwards.)

And yes, AnyA is what Soni was proposing the other day. Except I'm pretty sure that, rather than needing a new higher-kinded types feature, you can implement at least a version of it with just GATs...

11 Likes