Pre-RFC: Failable cast intrinsics

Hi,

I think it would be very useful to have a few intrinsics for doing specialisation as below. The idea is that during monomorphization any type conversion that can be statically verified will be cast to the target type or None otherwise. The functions below avoid lifetime changes, so I think they should avoid the soundness issues currently blocking specialisation. I believe this could just use the current type checking and casting under the hood.

The specific functions I have in mind are:

const fn try_cast<T: 'static + ?Sized, U: 'static + ?Sized>(that: T) -> Option<U>;
const fn try_cast_ref<T: 'static + ?Sized, U: 'static + ?Sized>(that: &T) -> Option<&U>;
const fn try_cast_mut<T: 'static + ?Sized, U: 'static + ?Sized>(that: &mut T) -> Option<&mut U>;

It could be useful to have a Result return type instead to be able to know why a cast failed.

These functions don't have to be intrinsics, that's just where they seemed to make sense to live.

There are many ways they could be used, but the specific case I have in mind is situations like this (contrived):

fn compute<T: Compute>(that: &T) -> usize {
    if let Some(cheaper) = try_cast_ref::<dyn CanComputeCheap>(that) {
        cheaper.compute_cheaply()
    } else if let Some(known) = try_cast_ref::<[Cheap]>(that) {
        known.map(Cheap::compute_cheaply).sum()
    } else {
        that.compute()
    }
}

After monomorphization and optimisation I expect that the code could become something like these:

fn compute(that: &Cheap) -> usize {
    that.compute_cheaply()
}
fn compute(that: &[Cheap]) -> usize {
    known.map(Cheap::compute_cheaply).sum()
}
fn compute<T: Compute>(that: &T) -> usize {
    that.compute()
}

You can already do this with std::any::Any casting.

— oh, you mean to include casts to dyn TraitItMightNotImplement. That would be new.

Yeah, dyn Trait and ?Sized aren’t supported by Any’s cast methods.

In theory it could compile down to using Any-like type checking in cases where it can’t be statically known. In that case I think we’d need a Result return type as the failure reasons could get more complicated. It might also get confusing when it doesn’t work in some dynamic cases like from dyn SomeTrait to dyn SomeOtherTrait. Possibly T needs to be constrained to not alllow traits somehow (or just document that it will always fail if it can’t be statically determined), but let U still allow it.

I'm curious as to how this could compile. I guess in theory, this could be implemented with some form of specialization?

Sorry, I missed the notification about your reply.

I'm not super familiar with the compiler internals, but I was imagining that somewere like FunctionCx::codegen_intrinsic_call, it would monomorphize so T and U are known, type check to see if it can statically determine if U is compatible with T, and then synthesize an appropriate implementation (Some or None).

In the case where it is compatible, then the synthesised code would be the ssa equivalent of implicit coercion that happens in parameters, for example:

fn coerce_to_concrete<T: 'static>(x: &T) -> Option<&U> {
   let x: &dyn Any = x;
   x.downcast_ref()
}
fn coerce_to_dyn<T: Unsize<U>>(x: &T) -> Option<&U> {
   Some(x)
}