Skip to content

Conversation

@izagawd
Copy link

@izagawd izagawd commented Nov 24, 2025

This RFC proposes #[exhaustive] traits to enable sound cross-trait casting for trait objects.

For any concrete type T, the set of #[exhaustive] traits it implements is finite and deterministic, allowing runtime checks like “if this dyn A also implements dyn B, cast and use it.”

The design adds a per-type exhaustive trait→vtable map and enforces four rules (type-crate ownership of implementation, trait arguments determined by Self, object safe, and 'static only) to keep the mapping coherent under separate compilation.

Use cases include capability-based game entities (e.g., Damageable, Walkable traits) and GUI widgets (e.g., Clickable, Scrollable),
avoiding manual registry/macro approaches such as bevy_reflect.

This enables patterns such as: "if dyn Character is dyn Flyable, then character.fly()"

Rendered

@ehuss ehuss added T-lang Relevant to the language team, which will review and decide on the RFC. T-types Relevant to the types team, which will review and decide on the RFC. labels Nov 25, 2025
@burdges
Copy link

burdges commented Nov 26, 2025

We could avoid Rule 1 by building the vtable lookup table externally in ./target, no?

As &'static [(TypeId, TraitVTable)] is a nasty lookup, https://internals.rust-lang.org/t/casting-families-for-fast-dynamic-trait-object-casts-and-multi-trait-objects/12195 proposed defining fresh indices for each "casting family", so that lookup takes place in &'static [TraitVTable]. In that, the families kept the vtable lookup tables small, but this could be avoided perhaps. And it could all be done via procmacros, without changing rustc.

@izagawd
Copy link
Author

izagawd commented Nov 26, 2025

I made another RFC: #3888.

If that lands, rather than having every trait object being forced to store metadata for casting, there could be a

trait Castable: 'static {
    const self EXHAUSTABLE_IMPLEMENTATIONS: &'static [(TypeId, TraitVTable)];
}

which could have a blanket implementation of:

impl<T: 'static> Castable for T {
   const self EXHAUSTABLE_IMPLEMENTATIONS: &'static [(TypeId, TraitVTable)] = std::intrinsics::exhausive_implementations::<T>();
}

Which would make the metadata for casting opt in

Though this will require a good chunk of changes to the exhaustive_traits RFC

It would be ideal to make this Castable trait use final for the EXHAUSTIBLE_IMPLEMENTATIONS const self field. final coming from this RFC: #3678, as making the EXHAUSTIBLE_IMPLEMENTATIONS final would be better than having a blanket implementation. The exhaustible_trait RFC doesnt allow exhaustible traits to have blanket implementations after all, so the final keyword would do wonders. It would be weird that the Castable trait itself was not considered as exhaustible.

Of course we can do compiler magic, and make the Castable trait the only exhaustible_trait that is blanket implemented, but that does not seem ideal to me if we can just do final const self EXHAUSTABLE_IMPLEMENTATIONS: &'static [(TypeId, TraitVTable)] = std::intrinsics::exhausive_implementations::<Self>();

@tmccombs
Copy link

To me "exhaustive" suggests that the trait can only be implemented within the defining crate (like sealed). I'm not really sure what a better name is though.

@izagawd
Copy link
Author

izagawd commented Nov 28, 2025

@tmccombs I would use a better name than exhaustive if I could, but I can't think of a better name either ¯_(ツ)_/¯

@burdges
Copy link

burdges commented Nov 29, 2025

Appears intertrait already provides a much cleaner solution, by using the linkme.

At present linkme needs linker support, but one could bypass the linker, using only #[no_mangle] and procmacros. Also, linkme cannot control ordering, which demands a hashmap like in this RFCs, so maybe best if all the procmacros compute trait indices too, thereby avoiding the hashmap entirely.

@izagawd
Copy link
Author

izagawd commented Nov 29, 2025

@burdges i tried intertrait and it doesnt... work, not even the example code in their crates.io page

I also tried out trait-cast. It does not support generic parameters at all, and requires that i must have the ptr_metadata feature gate available. And again, I have to do the extra step of registering the relationship between a type and interface, which i think should be implicit rather than explicit

cast_trait_object seems to support generic parameters, but it involves some boilerplate setup that registers relationships, which again, I feel should not be explicit.

@burdges
Copy link

burdges commented Nov 29, 2025

Interesting thanks. Appears 4 years since their last version, and the owning blockchain has no updates since May 2023, so bitrot. It nevertheless shows that rule 1 maybe excessively restrictive.

Anyways I definitely agree that generic statics or similar ala #3888 sound useful.

@izagawd
Copy link
Author

izagawd commented Nov 30, 2025

It nevertheless shows that rule 1 maybe excessively restrictive.

I agree, but I cannot find a way around this due to how separate compilation works. I would gladly remove that rule if it were soundly possible

@Scripter17
Copy link

Is it possible for sidecasting from trait A to trait B to make a subtrait of both, downcast to that subtrait, then upcast to B?

I don't think it's that farfetched but I only have vague surface level of the compiler so I don't really know. I just figured I should make sure it's considered.

@burdges
Copy link

burdges commented Nov 30, 2025

Yes, but only for types defined downstream of both traits, by Rule 1. If you have a &dyn A coming from a &u64 under the hood, then you cannot cast it to a &dyn B. And Box was not directly addressed.

how separate compilation works

There are degrees of separate compilation here: We've large projects using codegen-units=1 for various reasons, by far the worst failure of separate compilation, but really quite standard. By comparison, intertrait only needs some append only structure that's compiled late. Imho intertrait does not go far enough, and should reprocess those append only structures into efficently indexable families.

Around this, I wonder if intertrait failed because it depends upon lto or another linker flag?

Anyways, all these crates for casting dyn Trait track the vtables for the different pointer types seperately. As they check the underlying type, their reason for doing would be unstability of the Pointer layout, aka soundness bitrot. I'd suggest two preliminary changes:

First, Rust should commit to all smart pointer types using the same vtables. This is probably already the case. Second, Rust should exposes unsafe methods that manipulate the vtable pointer, optionally safe ones too. This would prevent soundness bitrot in external crates that cast dyn Traits, and allow wider adoption.

@arielb1
Copy link
Contributor

arielb1 commented Jan 10, 2026

So the current proposal does not involve linker tricks?

The non-linker-trick version would essentially allow doing error provides by adding subtraits of Error.

@arielb1
Copy link
Contributor

arielb1 commented Jan 12, 2026

@arielb1
Copy link
Contributor

arielb1 commented Jan 13, 2026

@rustbot label +I-lang-nominated

I am nominating this for lang team discussion.

Me and the libs team are trying to close our story for Error generic member access. It has come to our attention that this sort of trait downcasting essentially duplicates the functionality of Provide in a way that is easier to observe via rustdoc.

We would prefer not to have 2 ways of doing Provide, and for that we need to be aware whether this looks like a reasonable feature lang-wise.

@rustbot

This comment was marked as resolved.

@programmerjake programmerjake added the I-lang-nominated Indicates that an issue has been nominated for prioritizing at the next lang team meeting. label Jan 13, 2026
@traviscross traviscross added the P-lang-drag-2 Lang team prioritization drag level 2. label Jan 14, 2026
@jackh726
Copy link
Member

@traviscross @joshtriplett and I had a brief discussion about this RFC. We all seemed excited about this work, but all felt that it likely should start as a lang experiment rather than as an RFC.

On that note, we would need to find a lang champion for this. Probably the easiest/best path forward on that is to open a channel in #t-lang on zulip.

@Keith-Cancel
Copy link

Keith-Cancel commented Feb 2, 2026

@izagawd Regarding on of the Unresolved questions:

  • Could #[exhaustive] be a keyword rather than an attribute?

I was having a short conversation with @programmerjake on zulip about just kinda an idea I had initially unrelated to this RFC. I have quoted part of the conversion below. Anyways he brought up this RFC, and it seems like it could also be useful. Essentially the short of it is some special bound I am just assuming it was called crate.

trait MyExhaustive: crate {
    // items...
}

@Keith-Cancel

You seem like you might know, I did search around a bit. However, did not find anything or maybe it was called something else. However, do you know if any sort of crate level bound was ever thought about.

Just for example assume it was called crate

impl<T: MyTrait + crate> CrateExtrenalTrait for T {
   // impl...
}

Something like that blanket impl becomes possible at least for types defined in a crate. This keep the principles of the orphan rule since we are saying this applies only to types defined in the crate. The alternative is generally like a macro or manual repetition.

It was more of just a thought, but more was trying to see if anything like that had been discussed anywhere?

@programmerjake

i'm not aware of anything that restricts a blanket impl to crate-local types, but there is a RFC that proposes traits that error if they're implemented for types that aren't in the same crate as the impls: #3885

@Keith-Cancel

Hmm I had not run into that, but I suppose instead of attribute like they proposed, the bound idea I am talking about would work. Assuming crate again for the name, but this would allow similar a concept.

trait MyExhaustive: crate {
   // items...
}

Nothing can implement this but types defined in the same crate. While something similar can be achieved with a sealed trait. I do suppose something like this would allow the compiler to reason about the trait in ways like the author of that rfc is wanting compared to a sealed trait which is more just a visibility trick.

It also feels more in line with the language than attribute idea since it avoid caveats like no blanket impls. Since those would be allowed, but such impls would have to include crate as one of their bounds for the type.

@burdges
Copy link

burdges commented Feb 2, 2026

Imho, rust should seriously consider "mild" linker tricks, not only here, but reducing repeated codegen, etc. At least to me, Rule 1 seems way too heavy handed when seemingly simple alternatives exist.

Also, rust should remain open & friendly to linker tricks, so if one adds features like this then one should think about how they interact with better alternatives. At the same time, if this gives a way to resolve common situations before linker phase, then okay whatever.

@izagawd
Copy link
Author

izagawd commented Feb 2, 2026

@Keith-Cancel the blanket implementation, but only for within the defining crate sounds interesting, but how about this scenario?

impl<T: MyTrait + crate, U> CrateExtrenalTrait<U> for T {
   // impl...
}

this could lead to concrete types implementing an infinite amount of CrateExternalTrait<T> generic variants, which would cause issues for exhaustive_traits. Though it should be possible if done with traits with no generics, or their generic parameters are concrete ((ie Trait<i32> rather than Trait<T>)

as for using:

trait MyExhaustive: crate {
   // items...
}

this implies that if I make a trait say:

trait Foo: MyExhaustive{}

that Foo is also exhaustive, and gains the restrictions and benefits of being an exhaustive trait as well, which may not be as flexible, as there may be some traits that want an exhaustive trait to be their supertrait, but not be exhaustive traits themselves. Though it could be argued that it should be the desired behavior

@Keith-Cancel
Copy link

@Keith-Cancel the blanket implementation, but only for within the defining crate sounds interesting, but how about this scenario?

this could lead to concrete types implementing an infinite amount of CrateExternalTrait<T> generic variants, which would cause issues for exhaustive_traits.

@izagawd Like your already allowed to do that with an instance of a crate local type and an external trait with a generic parameter: Rust Playground

as for using:

trait MyExhaustive: crate {
   // items...
}

this implies that if I make a trait say:

trait Foo: MyExhaustive{}

that Foo is also exhaustive, and gains the restrictions and benefits of being an exhaustive trait as well, which may not be as flexible, as there may be some traits that want an exhaustive trait to be their supertrait, but not be exhaustive traits themselves. Though it could be argued that it should be the desired behavior

Like whether the crate local restriction is expressed as a bound or your attribute, if Foo has MyExhaustive as a super trait the only possible way to implement Foo is if the type is crate local, otherwise you can't implement MyExhaustive I would say that is expected.

Conceptually to me there are two parts to this RFC, basically a crate local trait bound, and the cross crate casting.

@izagawd
Copy link
Author

izagawd commented Feb 3, 2026

@izagawd Like your already allowed to do that with an instance of a crate local type and an external trait with a generic parameter: Rust Playground

I am aware that we are allowed to do that, but how exhaustive_traits works is that it stores all its implementations (as typeid vtable mappings) in it's metadata to allow cross trait casting. If it enabled infinite implementations, it will be impossible to do that

@Keith-Cancel
Copy link

Keith-Cancel commented Feb 3, 2026

@izagawd Like your already allowed to do that with an instance of a crate local type and an external trait with a generic parameter: Rust Playground

I am aware that we are allowed to do that, but how exhaustive_traits works is that it stores all its implementations (as typeid vtable mappings) in it's metadata to allow cross trait casting. If it enabled infinite implementations, it will be impossible to do that

@izagawd This can be worked around instead of treating the trait type ids as a flat id in your exhaustive map, if they composed of two parts. Basically one half identifies the trait, the other half the generic, and instead of just plain equality we change equality to: lhs == (lhs & rhs).

Might be easier to explain with just some code: Rust Play Ground

--EDIT-
Slight issue with that logic in general, but the idea still is the same.
Here it is updated with slightly different eq logic: Play Ground

@programmerjake
Copy link
Member

@izagawd This can be worked around instead of treating the trait type ids as a flat id in your exhaustive map, if they composed of two parts. Basically one half identifies the trait, the other half the generic, and instead of just plain equality we change equality to: lhs == (lhs & rhs).

Might be easier to explain with just some code: Rust Play Ground

that still doesn't work for:

impl<T: MyTrait + crate, U> SomeTrait<U> for T {
   // impl...
}

because U isn't bounded by crate so it can be any arbitrary type, e.g. for every N you can have MyType: SomeTrait<[(); N]> which is enough different N values that you can't possibly store all of them in memory assuming you need more than 1 byte for each trait impl.

@Keith-Cancel
Copy link

Keith-Cancel commented Feb 4, 2026

@izagawd This can be worked around instead of treating the trait type ids as a flat id in your exhaustive map, if they composed of two parts. Basically one half identifies the trait, the other half the generic, and instead of just plain equality we change equality to: lhs == (lhs & rhs).
Might be easier to explain with just some code: Rust Play Ground

that still doesn't work for:

impl<T: MyTrait + crate, U> SomeTrait<U> for T {
   // impl...
}

because U isn't bounded by crate so it can be any arbitrary type, e.g. for every N you can have MyType: SomeTrait<[(); N]> which is enough different N values that you can't possibly store all of them in memory assuming you need more than 1 byte for each trait impl.

@programmerjake I don't see what you mean though this non-flat id does not work for impl<T: MyTrait + crate, U> SomeTrait<U>? There will always be only on V-Table regardless of U for each T in the crate? So if there is a... I guess you could it like a wild card ID in the exhaustive match table it will match any other type ID for U to that vtable.

That aside, I was busy earlier today after posting that, but was still thinking about it. I realized a couple things first my eq logic would need changed these are not bit-field ids so that trick does not quite work. The BIGGER issue I noticed was deeper levels of generics:

impl<T: MyTrait + crate, U> SomeTrait<(u32, U)>  for T {}
impl<T: MyTrait + crate, U> SomeTrait<(U, u32)>  for T {}
impl<T: MyTrait + crate, U, V> SomeTrait<(U, V)> for T {}

This can get arbitrarily wide or deep. It also made wonder how we would even generate IDs deterministically in the CONCRETE case. Since even if every type was explicitly spelled the type expression of that generic could arbitrarily long or deep. So would need like IDs of unbounded length or a hard limit on length and nesting.

--Edit--
@programmerjake That aside the thing that interests me most here is just some form of crate level bound, since it makes writing certain types of trait impls much nicer or possible.

@programmerjake
Copy link
Member

programmerjake commented Feb 4, 2026

@programmerjake I don't see what you mean though this non-flat id does not work for impl<T: MyTrait + crate, U> SomeTrait<U>? There will always be only on V-Table regardless of U for each T in the crate? So if there is a... I guess you could it like a wild card ID in the exhaustive match table it will match any other type ID for U.

the problem is that to support trait casting there has to be a finite known set of possible dyn types that you can cast to, so the compiler can generate the table of vtables, but dyn SomeTrait<U> is an infinite set of dyn types, one for each different type U, so for any possible table of vtables that the compiler could generate, there will always be some type U that isn't in that table even though it should be. [(); N] is just an easy way to get a very large set of possible U types where there's at least one type for every byte of memory that could possibly exist in the compiled program.

@Keith-Cancel
Copy link

Keith-Cancel commented Feb 4, 2026

the problem is that to support trait casting there has to be a finite known set of possible dyn types that you can cast to, so the compiler can generate the table of vtables, but dyn SomeTrait<U> is an infinite set of dyn types, one for each different type U, so for any possible table of vtables that the compiler could generate, there will always be some type U that isn't in that table even though it should be. [(); N] is just an easy way to get a very large set of possible U types where there's at least one type for every byte of memory that could possibly exist in the compiled program.

@programmerjake What's the V-Table for this then leaving casting aside?

impl <T> MyTrait<T> for MyType { ... }

It's just same V-Table regardless of of T correct?

This would just be doing the same but for every type in the crate. Naively it could just thought of copying pasting basically the above.

impl<T: crate, U> MyTrait<U> for T { ... }

Now back to casting let just look at this case. In the exhaustive table all we care is that we match the trait, the generic part could be treated as a wild card/any ect... since we have an impl that applies to all T. Hence why I split it into two parts a trait_id and generic_id. If the generic_id is zero in the table it's basically a wild card otherwise it's a concrete type.

impl <T> MyTrait<T> for MyType { ... }

The bigger issue which I noted above is how to handle arbitrarily wide or nested types which I noted above.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

I-lang-nominated Indicates that an issue has been nominated for prioritizing at the next lang team meeting. P-lang-drag-2 Lang team prioritization drag level 2. T-lang Relevant to the language team, which will review and decide on the RFC. T-types Relevant to the types team, which will review and decide on the RFC.

Projects

None yet

Development

Successfully merging this pull request may close these issues.