Skip to content

[WIP] Add downcast_trait and downcast_trait_mut #144363

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 23 commits into
base: master
Choose a base branch
from

Conversation

ivarflakstad
Copy link

@ivarflakstad ivarflakstad commented Jul 23, 2025

Tracking issue: #144361

@rustbot rustbot added S-waiting-on-author Status: This is awaiting some action (such as code changes or more information) from the author. T-compiler Relevant to the compiler team, which will review and decide on the PR/issue. T-libs Relevant to the library team, which will review and decide on the PR/issue. labels Jul 23, 2025
@oli-obk oli-obk self-assigned this Jul 23, 2025
@rust-log-analyzer

This comment has been minimized.

#[must_use]
#[unstable(feature = "downcast_trait", issue = "144361")]
pub const fn downcast_trait_mut<
T: Any + 'static,
Copy link
Contributor

@oli-obk oli-obk Jul 23, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

does the Any bound actually prevent anything?

We'll need some ui tests that actually error when downcast_trait gets mis-used.

Some general test ideas:

  • passing a dyn Trait for the T where U is also dyn Trait
  • passing a dyn UnrelatedTrait for T when U is dyn Trait
  • using a not dyn-safe trait for U
  • using a dyn Trait<AssocTy = u32> when the type only implements Trait<AssocTy = bool>.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also: Test out:

  • downcasting from two disjoint supertrait impls (trait Foo<T, U>: Super<T> + Super<U>).
  • Downcasting when the type doesn't implement an Auto trait.

This is also I think still unsound because we don't (and cannot) enforce that the lifetime in the input and output are equal. This should allow you to unsoundly cast dyn Foo + 'a to dyn Foo + 'static, right?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The 'static bound maybe prevents potential unsoundness.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, then that really limits the usefulness of this intrinsic as an alternative to specialization as noted in the tracking issue 🤔

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As @oli-obk noted in another comment the intrinsic doesn't necessarily need 'static, but downcast_trait* does.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm, yeah, that is kind of similar... but the existing downcast is not, not at all.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doesn't really warrant a new module either, right? Even with type_name

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just for clarity @RalfJung I just plopped it in any so I could keep going. Was the best candidate I saw at a glance, I'm very open to moving it :)

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adding it to unresolved questions in the tracking issue

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about std::convert?

}
}

#[allow(missing_docs)]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Need some docs before merging

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Absolutely. Just appeasing the commit hook ☺️

.build_with_typing_env(ty::TypingEnv::fully_monomorphized());

let type_impls_trait = preds.iter().all(|pred| {
let trait_ref = ty::TraitRef::new(tcx, pred.def_id(), [tp_ty]);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You should definitely be matching on the ExistentialPredicate and using .with_self_ty instead of just constructing a TraitRef here.

As far as I can tell, right now if I try to downcast to dyn Iterator<Item = ()>, I'm only checking that the type implements Iterator, not that it implements the projection predicate of <Self as Iterator>::Item == ().

That should also allow you to remove that def_id function you added to ExistentialPredicate too.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's great!
This is the first time I've even seen these concepts so I'm glad you can detect mistakes like these hehe

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I noticed that when dealing with TraitRefs and .with_self_ty there is often a call to tcx.erase_regions(trait_ref) following.
Inspected the code. Had a look at the documentation. Still not fully certain but it seems to be simplifying the definition you pass in without changing it's actual meaning - is that correct?
I guess it would help if I knew exactly what a region was in this context hehe

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, you can erase regions as we already checked that all regions are 'static via the bounds.

Copy link
Member

@compiler-errors compiler-errors left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is an interesting idea, but I think before we commit to this experiment or anything, we should definitely do a vibe check about whether this is ever possible to implement in our current codegen backends.

If not, then this doesn't really serve as an alternative to specialization, right?


edit: Oh wait, I understand. This is a constant intrinsic, so we should never need to impl the intrinsic at runtime. I think I see. This whole scheme should be documented regardless, since that's a subtle difference from the way that upcasting is implemented today, which sometimes happens at runtime and thus has backend support.

ivarflakstad and others added 6 commits July 23, 2025 17:54
Co-authored-by: Oli Scherer <github35764891676564198441@oli-obk.de>
Co-authored-by: Oli Scherer <github35764891676564198441@oli-obk.de>
Co-authored-by: Oli Scherer <github35764891676564198441@oli-obk.de>
Co-authored-by: Oli Scherer <github35764891676564198441@oli-obk.de>
Co-authored-by: Oli Scherer <github35764891676564198441@oli-obk.de>
@RalfJung
Copy link
Member

RalfJung commented Jul 23, 2025

edit: Oh wait, I understand. This is a constant intrinsic, so we should never need to impl the intrinsic at runtime. I think I see. This whole scheme should be documented regardless, since that's a subtle difference from the way that upcasting is implemented today, which sometimes happens at runtime and thus has backend support.

Yeah, the key point is that everything is static. You can't take an &dyn X and ask "does the underlying type implement some trait". You can only do this for a concrete, statically-known type.

That also makes it extremely confusing to put this near the existing downcast methods which work on the actual underlying runtime type.

@rust-log-analyzer

This comment has been minimized.

@theemathas
Copy link
Contributor

Does overflow in the trait solver cause a post-mono error?

@theemathas

This comment was marked as resolved.

@theemathas

This comment was marked as resolved.

@theemathas
Copy link
Contributor

theemathas commented Jul 24, 2025

Edit: Same behavior in 8e9b17b

At commit 0da23e7 (the last commit that builds on my machine), this PR has weird behavior with overflows in the trait solver, seemingly causing an error only if a function makes it to codegen.

Code and error
#![feature(downcast_trait)]
#![recursion_limit = "8"]

use std::any::downcast_trait;

struct Wrap<T>(T);
struct Indirect<T>(T);

trait Trait {}

impl<T> Trait for Wrap<T> where Indirect<Indirect<T>>: Trait {}
impl<T> Trait for Indirect<T> where T: Trait {}
impl Trait for i32 {}

// Uncommenting the below line makes the code compile
// #[inline]
pub fn weird() {
    let x = Wrap(Wrap(Wrap(Wrap(1i32))));
    let b = downcast_trait::<Wrap<Wrap<Wrap<Wrap<i32>>>>, dyn Trait>(&x).is_some();
    println!("{b}");
}
error[E0275]: overflow evaluating the requirement `Indirect<Indirect<Wrap<i32>>>: Trait`
   |
   = help: consider increasing the recursion limit by adding a `#![recursion_limit = "16"]` attribute to your crate (`foo`)
note: required for `Wrap<Wrap<i32>>` to implement `Trait`
  --> src/lib.rs:11:9
   |
11 | impl<T> Trait for Wrap<T> where Indirect<Indirect<T>>: Trait {}
   |         ^^^^^     ^^^^^^^                              ----- unsatisfied trait bound introduced here
   = note: 6 redundant requirements hidden
   = note: required for `Wrap<Wrap<Wrap<Wrap<i32>>>>` to implement `Trait`

For more information about this error, try `rustc --explain E0275`.

@ivarflakstad
Copy link
Author

@theemathas it's not compiling because I used the gh commit
suggestion functionality, which I am ok with because it's still very much WIP:)

Great catch wrt the trait solver overflow. I'm hoping the suggestions pointed out by @compiler-errors and @oli-obk will solve that.

@bors
Copy link
Collaborator

bors commented Jul 24, 2025

☔ The latest upstream changes (presumably #116707) made this pull request unmergeable. Please resolve the merge conflicts.

@theemathas

This comment was marked as resolved.

@ivarflakstad
Copy link
Author

ivarflakstad commented Jul 24, 2025

I think the recent changes have addressed the unsoundness pointed out by @theemathas, but I still need to address the recursion issue that causes trait solver overflow.

#[rustc_intrinsic]
pub const fn vtable_for<T, U: ptr::Pointee<Metadata = ptr::DynMetadata<U>> + ?Sized>()
-> Option<ptr::DynMetadata<U>>;

Copy link
Contributor

@theemathas theemathas Jul 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since you replaced the erased lifetimes with 'static, I believe that this function needs a T: 'static and a U: 'static bound.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could make it unsafe to allow for best-effort checks for optimizations like the Fuse specializations

@theemathas
Copy link
Contributor

I just realized that the weirdness with #[inline] isn't specific to this PR, so I've filed issue #144460

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
S-waiting-on-author Status: This is awaiting some action (such as code changes or more information) from the author. T-compiler Relevant to the compiler team, which will review and decide on the PR/issue. T-libs Relevant to the library team, which will review and decide on the PR/issue.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

9 participants