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
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
ca8e62d
Introduce vtable_for intrinsic. Passes hir analysis, far from functional
ivarflakstad Jul 13, 2025
4a00305
Merge branch 'master' into vtable-for-intrinsic
ivarflakstad Jul 19, 2025
17b5402
Add def_id() method to ty::Binder<.., ExistentialPredicate>
ivarflakstad Jul 22, 2025
d40408e
Verify that type implements trait in vtable_for. Return None if not.
ivarflakstad Jul 22, 2025
7a46b59
Add initial downcast_trait impl
ivarflakstad Jul 22, 2025
46224d5
Add downcast_trait ui tests
ivarflakstad Jul 22, 2025
b16ea15
Add initial downcast_trait_mut impl
ivarflakstad Jul 22, 2025
ff13405
Add downcast_trait_mut ui tests
ivarflakstad Jul 22, 2025
93aa4f2
Add actual tracking issue reference
ivarflakstad Jul 23, 2025
1249f38
Remove redundant #[allow(dead_code)]
ivarflakstad Jul 23, 2025
ad5fa57
update vtable_for U bounds
ivarflakstad Jul 23, 2025
0da23e7
Use existing typing env when building infer context
ivarflakstad Jul 23, 2025
d9b8269
Remove redundant upcast
ivarflakstad Jul 23, 2025
b08d57f
Document which discriminant we write
ivarflakstad Jul 23, 2025
db579d3
Document behaviour of writing Option<NonNull> pointer
ivarflakstad Jul 23, 2025
852c332
Safety comment
ivarflakstad Jul 23, 2025
8298c84
Make unreachable path trigger ICE with span
ivarflakstad Jul 23, 2025
d9cb965
Improve type impls trait check
ivarflakstad Jul 24, 2025
2919430
Add SAFETY comment to downcast_trait_mut
ivarflakstad Jul 24, 2025
8e9b17b
Merge branch 'master' into vtable-for-and-downcast-trait
ivarflakstad Jul 24, 2025
5b438f8
Turn predicate 'erased lifetimes into 'static
ivarflakstad Jul 24, 2025
11748bf
Add ui tests that were unsound previously and now fail on downcast_tr…
ivarflakstad Jul 24, 2025
c38ca5b
Restructure ui tests to highlight unsound path and how we no longer h…
ivarflakstad Jul 24, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions Cargo.lock
Original file line number Diff line number Diff line change
Expand Up @@ -1890,9 +1890,9 @@ dependencies = [

[[package]]
name = "ipc-channel"
version = "0.20.0"
version = "0.20.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b1c98b70019c830a1fc39cecfe1f60ff99c4122f0a189697c810c90ec545c14"
checksum = "1700f6b8b9f00cdd675f32fbb3a5be882213140dfe045805273221ca266c43f8"
dependencies = [
"bincode",
"crossbeam-channel",
Expand Down
52 changes: 49 additions & 3 deletions compiler/rustc_const_eval/src/interpret/intrinsics.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,17 @@

use std::assert_matches::assert_matches;

use rustc_abi::{FieldIdx, HasDataLayout, Size};
use rustc_abi::{FIRST_VARIANT, FieldIdx, HasDataLayout, Size};
use rustc_apfloat::ieee::{Double, Half, Quad, Single};
use rustc_hir::def_id::CRATE_DEF_ID;
use rustc_infer::infer::TyCtxtInferExt;
use rustc_middle::mir::interpret::{CTFE_ALLOC_SALT, read_target_uint, write_target_uint};
use rustc_middle::mir::{self, BinOp, ConstValue, NonDivergingIntrinsic};
use rustc_middle::ty::layout::TyAndLayout;
use rustc_middle::ty::{Ty, TyCtxt};
use rustc_middle::{bug, ty};
use rustc_middle::ty::{Ty, TyCtxt, TypeFoldable};
use rustc_middle::{bug, span_bug, ty};
use rustc_span::{Symbol, sym};
use rustc_trait_selection::traits::{Obligation, ObligationCause, ObligationCtxt};
use tracing::trace;

use super::memory::MemoryKind;
Expand Down Expand Up @@ -149,6 +152,49 @@ impl<'tcx, M: Machine<'tcx>> InterpCx<'tcx, M> {
let b_ty = self.read_type_id(&args[1])?;
self.write_scalar(Scalar::from_bool(a_ty == b_ty), dest)?;
}
sym::vtable_for => {
let tp_ty = instance.args.type_at(0);
let result_ty = instance.args.type_at(1);

ensure_monomorphic_enough(tcx, tp_ty)?;
ensure_monomorphic_enough(tcx, result_ty)?;
let ty::Dynamic(preds, _, ty::Dyn) = result_ty.kind() else {
span_bug!(
self.find_closest_untracked_caller_location(),
"Invalid type provided to vtable_for::<T, U>. U must be dyn Trait, got {result_ty}."
);
};

let (infcx, param_env) =
self.tcx.infer_ctxt().build_with_typing_env(self.typing_env);

let ocx = ObligationCtxt::new(&infcx);
ocx.register_obligations(preds.iter().map(|pred| {
let pred = pred.with_self_ty(tcx, tp_ty);
// Lifetimes can only be 'static because of the bound on T
let pred = pred.fold_with(&mut ty::BottomUpFolder {
tcx,
ty_op: |ty| ty,
lt_op: |lt| {
if lt == tcx.lifetimes.re_erased { tcx.lifetimes.re_static } else { lt }
},
ct_op: |ct| ct,
});
Obligation::new(tcx, ObligationCause::dummy(), param_env, pred)
}));
let type_impls_trait = ocx.select_all_or_error().is_empty();
// Since `assumed_wf_tys=[]` the choice of LocalDefId is irrelevant, so using the "default"
let regions_are_valid = ocx.resolve_regions(CRATE_DEF_ID, param_env, []).is_empty();

if regions_are_valid && type_impls_trait {
let vtable_ptr = self.get_vtable_ptr(tp_ty, preds)?;
// Writing a non-null pointer into an `Option<NonNull>` will automatically make it `Some`.
self.write_pointer(vtable_ptr, dest)?;
} else {
// Write `None`
self.write_discriminant(FIRST_VARIANT, dest)?;
}
}
sym::variant_count => {
let tp_ty = instance.args.type_at(0);
let ty = match tp_ty.kind() {
Expand Down
15 changes: 15 additions & 0 deletions compiler/rustc_hir_analysis/src/check/intrinsic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ fn intrinsic_operation_unsafety(tcx: TyCtxt<'_>, intrinsic_id: LocalDefId) -> hi
| sym::discriminant_value
| sym::type_id
| sym::type_id_eq
| sym::vtable_for
| sym::select_unpredictable
| sym::cold_path
| sym::ptr_guaranteed_cmp
Expand Down Expand Up @@ -553,6 +554,20 @@ pub(crate) fn check_intrinsic_type(
(0, 0, vec![Ty::new_imm_ptr(tcx, tcx.types.unit)], tcx.types.usize)
}

sym::vtable_for => {
let dyn_metadata = tcx.require_lang_item(LangItem::DynMetadata, span);
let dyn_metadata_adt_ref = tcx.adt_def(dyn_metadata);
let dyn_metadata_args = tcx.mk_args(&[param(1).into()]);
let dyn_ty = Ty::new_adt(tcx, dyn_metadata_adt_ref, dyn_metadata_args);

let option_did = tcx.require_lang_item(LangItem::Option, span);
let option_adt_ref = tcx.adt_def(option_did);
let option_args = tcx.mk_args(&[dyn_ty.into()]);
let ret_ty = Ty::new_adt(tcx, option_adt_ref, option_args);

(2, 0, vec![], ret_ty)
}

// This type check is not particularly useful, but the `where` bounds
// on the definition in `core` do the heavy lifting for checking it.
sym::aggregate_raw_ptr => (3, 0, vec![param(1), param(2)], param(0)),
Expand Down
1 change: 1 addition & 0 deletions compiler/rustc_span/src/symbol.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2350,6 +2350,7 @@ symbols! {
vreg_low16,
vsx,
vtable_align,
vtable_for,
vtable_size,
warn,
wasip2,
Expand Down
7 changes: 7 additions & 0 deletions compiler/rustc_type_ir/src/predicate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,13 @@ pub enum ExistentialPredicate<I: Interner> {
}

impl<I: Interner> ty::Binder<I, ExistentialPredicate<I>> {
pub fn def_id(&self) -> I::DefId {
match self.skip_binder() {
ExistentialPredicate::Trait(tr) => tr.def_id,
ExistentialPredicate::Projection(p) => p.def_id,
ExistentialPredicate::AutoTrait(did) => did,
}
}
/// Given an existential predicate like `?Self: PartialEq<u32>` (e.g., derived from `dyn PartialEq<u32>`),
/// and a concrete type `self_ty`, returns a full predicate where the existentially quantified variable `?Self`
/// has been replaced with `self_ty` (e.g., `self_ty: PartialEq<u32>`, in our example).
Expand Down
44 changes: 43 additions & 1 deletion library/core/src/any.rs
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@

#![stable(feature = "rust1", since = "1.0.0")]

use crate::{fmt, hash, intrinsics};
use crate::{fmt, hash, intrinsics, ptr};

///////////////////////////////////////////////////////////////////////////////
// Any trait
Expand Down Expand Up @@ -896,3 +896,45 @@ pub const fn type_name<T: ?Sized>() -> &'static str {
pub const fn type_name_of_val<T: ?Sized>(_val: &T) -> &'static str {
type_name::<T>()
}

#[allow(missing_docs)]
#[must_use]
#[unstable(feature = "downcast_trait", issue = "144361")]
pub const fn downcast_trait<
T: Any + 'static,
U: ptr::Pointee<Metadata = ptr::DynMetadata<U>> + ?Sized + 'static,
>(
t: &T,
) -> Option<&U> {
let vtable: Option<ptr::DynMetadata<U>> = const { intrinsics::vtable_for::<T, U>() };
match vtable {
Some(dyn_metadata) => {
let pointer = ptr::from_raw_parts(t, dyn_metadata);
// SAFETY: `t` is a reference to a type, so we know it is valid.
// `dyn_metadata` is a vtable for T, implementing the trait of `U`.
Some(unsafe { &*pointer })
}
None => None,
}
}

#[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 ☺️

#[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
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?

Copy link
Author

Choose a reason for hiding this comment

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

Conceptually yes, but in our case convert is really for conversion traits. The only fn in there is identity iirc. So I'm not sure it fits there either.

Copy link
Author

Choose a reason for hiding this comment

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

But perhaps the naming change we've been discussing in the tracking issue is enough.
If we drop downcast from the name (try_as_trait for example) does that make it less egregious to have in any? @RalfJung

U: ptr::Pointee<Metadata = ptr::DynMetadata<U>> + ?Sized + 'static,
>(
t: &mut T,
) -> Option<&mut U> {
let vtable: Option<ptr::DynMetadata<U>> = const { intrinsics::vtable_for::<T, U>() };
match vtable {
Some(dyn_metadata) => {
let pointer = ptr::from_raw_parts_mut(t, dyn_metadata);
// SAFETY: `t` is a reference to a type, so we know it is valid.
// `dyn_metadata` is a vtable for T, implementing the trait of `U`.
Some(unsafe { &mut *pointer })
}
None => None,
}
}
12 changes: 12 additions & 0 deletions library/core/src/intrinsics/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2657,6 +2657,18 @@ pub unsafe fn vtable_size(ptr: *const ()) -> usize;
#[rustc_intrinsic]
pub unsafe fn vtable_align(ptr: *const ()) -> usize;

/// FIXME: write actual docs (ivarflakstad)
/// The intrinsic will return the vtable of `t` through the lens of `U`.
///
/// # Safety
///
/// `ptr` must point to a vtable.
#[rustc_nounwind]
#[unstable(feature = "core_intrinsics", issue = "none")]
#[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

Copy link
Contributor

Choose a reason for hiding this comment

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

But I think it doesn't need to be as long as it's documented what returning Some means

/// The size of a type in bytes.
///
/// Note that, unlike most intrinsics, this is safe to call;
Expand Down
19 changes: 18 additions & 1 deletion library/coretests/tests/intrinsics.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
use core::any::TypeId;
use core::intrinsics::assume;
use core::intrinsics::{assume, vtable_for};
use std::fmt::Debug;
use std::option::Option;
use std::ptr::DynMetadata;

#[test]
fn test_typeid_sized_types() {
Expand Down Expand Up @@ -193,3 +196,17 @@ fn carrying_mul_add_fallback_i128() {
(u128::MAX - 1, -(i128::MIN / 2)),
);
}

#[test]
fn test_vtable_for() {
#[derive(Debug)]
struct A {}

struct B {}

const A_VTABLE: Option<DynMetadata<dyn Debug>> = vtable_for::<A, dyn Debug>();
assert!(A_VTABLE.is_some());

const B_VTABLE: Option<DynMetadata<dyn Debug>> = vtable_for::<B, dyn Debug>();
assert!(B_VTABLE.is_none());
}
29 changes: 29 additions & 0 deletions tests/ui/any/downcast_trait.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
//@ run-pass
#![feature(downcast_trait)]

use std::fmt::Debug;

// Look ma, no `T: Debug`
fn downcast_debug_format<T: 'static>(t: &T) -> String {
match std::any::downcast_trait::<_, dyn Debug>(t) {
Some(d) => format!("{d:?}"),
None => "default".to_string()
}
}

// Test that downcasting to a dyn trait works as expected
fn main() {
#[allow(dead_code)]
#[derive(Debug)]
struct A {
index: usize
}
let a = A { index: 42 };
let result = downcast_debug_format(&a);
assert_eq!("A { index: 42 }", result);

struct B {}
let b = B {};
let result = downcast_debug_format(&b);
assert_eq!("default", result);
}
34 changes: 34 additions & 0 deletions tests/ui/any/downcast_trait_err1.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
//@ run-pass
#![feature(downcast_trait)]

use std::{any::downcast_trait, sync::OnceLock};

trait Trait {
fn call(&self, x: &Box<i32>);
}

impl Trait for for<'a> fn(&'a Box<i32>) {
fn call(&self, x: &Box<i32>) {
self(x);
}
}

static STORAGE: OnceLock<&'static Box<i32>> = OnceLock::new();

fn store(x: &'static Box<i32>) {
STORAGE.set(x).unwrap();
}

fn main() {
let data = Box::new(Box::new(1i32));
let fn_ptr: fn(&'static Box<i32>) = store;
let dt = downcast_trait::<_, dyn Trait>(&fn_ptr);
if let Some(dt) = dt {
// unsound path
dt.call(&*data);
drop(data);
println!("{}", STORAGE.get().unwrap());
} else {
println!("success")
}
}
32 changes: 32 additions & 0 deletions tests/ui/any/downcast_trait_err2.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
//@ run-pass
#![feature(downcast_trait)]
use std::{any::downcast_trait, sync::OnceLock};

trait Trait<T> {
fn call(&self, t: T, x: &Box<i32>);
}

impl Trait<for<'a> fn(&'a Box<i32>)> for () {
fn call(&self, f: for<'a> fn(&'a Box<i32>), x: &Box<i32>) {
f(x);
}
}

static STORAGE: OnceLock<&'static Box<i32>> = OnceLock::new();

fn store(x: &'static Box<i32>) {
STORAGE.set(x).unwrap();
}

fn main() {
let data = Box::new(Box::new(1i32));
let dt = downcast_trait::<_, dyn Trait<fn(&'static Box<i32>)>>(&());
if let Some(dt) = dt {
// unsound path
dt.call(store, &*data);
drop(data);
println!("{}", STORAGE.get().unwrap());
} else {
println!("success")
}
}
20 changes: 20 additions & 0 deletions tests/ui/any/downcast_trait_mut.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
//@ run-pass
#![feature(downcast_trait)]

use std::fmt::{Error, Write};

// Look ma, no `T: Write`
fn downcast_mut_write<T: 'static>(t: &mut T, s: &str) -> Result<(), Error> {
match std::any::downcast_trait_mut::<_, dyn Write>(t) {
Some(w) => w.write_str(s),
None => Ok(())
}
}

// Test that downcasting to a mut dyn trait works as expected
fn main() {
let mut buf = "Hello".to_string();

downcast_mut_write(&mut buf, " world!").unwrap();
assert_eq!(buf, "Hello world!");
}
Loading