Conversation
## Summary This PR implements the compiler- and library-side plumbing for the **bounded intertrait casting** proposal in rust-lang/rfcs#3952. It adds a mechanism for casting between `dyn Trait` objects that share an explicitly-declared common root supertrait, resolved at runtime in `O(1)` via a per-root metadata table — no `'static` bound, no `TypeId`, and no global registry. ## Surface ```rust #![feature(trait_cast)] use core::marker::TraitMetadataTable; trait Animal: TraitMetadataTable<dyn Animal> {} // declares `Animal` as a cast root trait Dog: Animal { fn bark(&self); } fn maybe_bark(a: &dyn Animal) { if let Ok(d) = core::cast!(in dyn Animal, a => dyn Dog) { d.bark(); } } ``` A trait becomes a **cast root** by naming `TraitMetadataTable<dyn Self>` as a supertrait. Every subtrait of a root inherits the `TraitMetadataTable<dyn Root>` bound and is eligible as a cast target within that root's graph. `core::cast!`, `core::try_cast!`, and `core::unchecked_cast!` macros (in a new `core::trait_cast` module) dispatch through the `TraitCast<I, U>` trait implemented for `&T`, `&mut T`, `Box<T>`, `Rc<T>`, and `Arc<T>`. Runtime cost per cast: two loads and a branch against the table for the root's graph. ## Library additions (`core`/`alloc`) - `core::marker::TraitMetadataTable<SuperTrait>` — the marker/lang-item that declares a cast root; blanket impl for all `Sized` types (the actual root-supertrait obligation is enforced by the supertrait relationship itself, not the where-clauses, to break a cycle through `Unsize`). - `core::trait_cast` — `TraitCast`/`TraitCastError` and the `cast!` / `try_cast!` / `unchecked_cast!` macros. - `alloc::{boxed, rc, sync}` — owned-cast impls. - New intrinsics in `core::intrinsics`: - `trait_metadata_index<SuperTrait, Trait>() -> (&'static u8, usize)` - `trait_metadata_table<SuperTrait, ConcreteType>() -> (&'static u8, NonNull<Option<NonNull<()>>>)` - `trait_metadata_table_len<SuperTrait>() -> usize` - `trait_cast_is_lifetime_erasure_safe<SuperTrait, TargetTrait>() -> bool` The `&'static u8` returned alongside each index/table pointer is a per-global-crate sentinel used to detect the `ForeignTraitGraph` case when two independently-built artifacts are linked into one binary. ## Compiler additions **New passes / modules** (all under `rustc_monomorphize` unless noted): - `trait_graph.rs` — per-root `TraitGraph` built from gathered `trait_metadata_index` / `trait_metadata_table` requests. - `table_layout.rs` — assigns slots for `(sub_trait, outlives_class)` pairs with condensation (`BitMatrix` row-grouping) to collapse classes admitting identical impl sets. - `erasure_safe.rs` — resolves `trait_cast_is_lifetime_erasure_safe` by DFS-walking binder vars of the target dyn type and checking each is expressible through the root's binder. - `cast_sensitivity.rs` — SCC-based batch computation of per-`Instance` `CastRelevantLifetimes` (direct + transitive via call-graph). - `resolved_bodies.rs`, `trait_cast_requests.rs` — request gathering and delayed-codegen queue. - `partitioning.rs` — cascade-canonicalization of augmented callees so sensitive subgraphs are emitted once per signature group. **MIR**: `TerminatorKind::{Call, TailCall}` grows a `call_id: &'tcx List<(DefId, u32, GenericArgsRef<'tcx>)>` recording the full inlining chain. `TerminatorKind` size assertion goes from 80 → 88. Before inlining each list has length 1; the inliner prepends the caller's chain to each inlined callee's. **Borrowck**: new `region_summary.rs` publishes a `BorrowckRegionSummary` per fn (walk-position → `RegionVid`, call-site region mappings keyed on the `u32` counter) consumed by the sensitivity pass after typeck but before mono. **Generic args**: new `GenericArgKind::Outlives(OutlivesArg)` variant (tag `0b11`) carrying `(longer, shorter)` region-index pairs. Appended to an `Instance`'s args when a sensitive callee must be specialized for a given caller's outlives environment. Wired through interning, encode/decode, folding/visiting, symbol mangling, and all the usual suspects. **New lang item**: `TraitMetadataTable` (`sym::trait_metadata_table`). **HIR analysis** (`wfcheck.rs`, `dyn_trait.rs`): eagerly diagnoses at trait-definition time when a root-connected trait introduces a lifetime not expressible through the root (would be manufactured at downcast time — unsound). ## Diagnostics - `UNUSED_CAST_TARGET` lint — cast to a target no concrete type in the final binary implements (always `Err` at runtime). - `trait graph rooted at {root} is not downcast-safe` — erased-lifetime manufacturability check. - `TraitMetadataTable type argument must be a trait object` — non-`dyn T` arg. - `TraitMetadataTable type argument does not match a cast root` — `dyn X` where `X` isn't `Self` or a transitive cast-root supertrait. - `cast target not reachable in graph` / `non-dyn-compat target` / `tmt-arg-*` — various ill-formed roots and targets. ## Debugging / inspection flags All `-Z`, all dump to stderr: - `-Z dump-trait-graph[=FILTER]`, `-Z dump-trait-cast-sensitivity[=FILTER]`, `-Z dump-trait-cast-augmentation[=FILTER]`, `-Z dump-trait-cast-canonicalization`, `-Z dump-trait-cast-chain-composition[=FILTER]`, `-Z dump-trait-cast-erasure-safety[=FILTER]` - `-Z print-trait-cast-stats` Each has a matching `tests/run-make/dump-*` test. ## Known caveats for review - Perf was evaluated with rustc-perf and the impact on crates that do not use trait casting was found to be minimal. The SCC + Floyd-Warshall pass only runs over directly- and transitively-sensitive call graphs and stops at the ground-level caller, so crates with no cast graph pay effectively nothing. Heavy trait-casting usage has not yet been benched as no suitable public crates exist yet. ## Not in this PR - Stabilization / `rustc_deny_explicit_impl` on `TraitMetadataTable`. - `cast!` on `Pin<P>` or user smart pointers. - `rustdoc` surfacing of cast graphs.
|
Some changes occurred to the intrinsics. Make sure the CTFE / Miri interpreter cc @rust-lang/miri, @RalfJung, @oli-obk, @lcnr Some changes occurred in compiler/rustc_sanitizers cc @rcvalle Some changes occurred to the CTFE / Miri interpreter cc @rust-lang/miri Some changes occurred in compiler/rustc_codegen_cranelift cc @bjorn3 These commits modify the If this was unintentional then you should revert the changes before this PR is merged. Some changes occurred in src/tools/clippy cc @rust-lang/clippy Some changes occurred to the CTFE machinery Some changes occurred in need_type_info.rs cc @lcnr HIR ty lowering was modified cc @fmease Some changes occurred to the core trait solver cc @rust-lang/initiative-trait-system-refactor Some changes occurred to the CTFE / Miri interpreter cc @rust-lang/miri, @RalfJung, @oli-obk, @lcnr Some changes occurred in coverage instrumentation. cc @Zalathar This PR changes rustc_public cc @oli-obk, @celinval, @ouz-a, @makai410 Some changes occurred to MIR optimizations cc @rust-lang/wg-mir-opt Some changes occurred in compiler/rustc_codegen_gcc This PR changes MIR cc @oli-obk, @RalfJung, @JakobDegen, @vakaras Some changes occurred in match lowering cc @Nadrieril |
|
r? @jieyouxu rustbot has assigned @jieyouxu. Use Why was this reviewer chosen?The reviewer was selected based on:
|
|
|
The job Click to see the possible cause of the failure (guessed by this bot)For more information how to resolve CI failures of this job, visit this link. |
|
AFAIK this is a PoC for the RFC, removing my assignment. |
|
Hi, Two things before any code review happens on this:
A single 16k-lines PR is not a reviewable unit for changes that cross a lot of different boundaries Please, consider closing this PR (or convert to draft). Once the RFC is accepted, land the work as a stacked series of focused PRs -- each small enough that a single reviewer can handle at a time. A diff of this size landed as one commit is not something anyone, author included, can meaningfully (self-)review before submitting -- which means the entire burden of catching issues falls on reviewers from the first pass onward. This is exactly the situation our burdensome policy was written for. Splitting the work into reviewable pieces ins't just a process preference here, it's the minimum baseline for the time reviewers can reasonably be asked to commit. |
|
☔ The latest upstream changes (presumably #155392) made this pull request unmergeable. Please resolve the merge conflicts. |
|
@DiamondLovesYou Hi, this is the moderation team of the Rust project. As @Kivooeo says, please understand the burden you're giving on the Rust project by subtmitting a patch of more than 16K lines of code. Please take some time to get yourself familiar with our contribution guidelines, specifically our etiquette. If you need further support on how to contribute to the Rust project, feel free to either reach out to us on Zulip. |
|
@apiraino With my sincerest apologies, I must insist. Contribution guidelines notwithstanding, this PR is not a draft, and has been engineered in whole to support trait-casting. It is large, yes, and I'm fully aware of the gravity of reviewing such a large diff, but also: there is no rush. To be blunt: there is not much "wiggle room" left that I have not already explored - the RFC, while not technically accepted yet, is a desired feature and cannot practically change much in substance between now and acceptance. |
|
Hey @DiamondLovesYou Just wanted to chime here. First, I want to say: I appreciate your enthusiasm about this feature. The RFC seems quite comprehensive and having a reference implementation can sometimes be helpful for this type of feature. That being said, I want to give you a heads-up that this PR as-is will almost certainly never get assigned a reviewer. It's simply too large. Moreover, even the RFC likely is "too much" to get merged. If you want to get this moved forward, I would suggest chatting on Zulip. As you said, this intersects a lot of teams, so it's probably good to start building up from the foundations. If I were in your position, this is roughly the order that I would take it:
Again, I'm not trying to discourage you or say that you can't do these things. But I am saying that it's not just that there is "no rush" about getting this reviewed - it is simply not the process that we use to get features of this size into the compiler. Even RFCs for this size of feature today will likely not get merged without significant prior discussion and experimentation. |
Summary
This PR implements the compiler- and library-side plumbing for the bounded trait casting proposal in rust-lang/rfcs#3952. It adds a mechanism for casting between
dyn Traitobjects that share an explicitly-declared common root supertrait, resolved at runtime inO(1)via a per-root metadata table — no'staticbound, noTypeId, and no global registry.Surface
A trait becomes a cast root by naming
TraitMetadataTable<dyn Self>as a supertrait. Every subtrait of a root inherits theTraitMetadataTable<dyn Root>bound and is eligible as a cast target within that root's graph.core::cast!,core::try_cast!, andcore::unchecked_cast!macros (in a newcore::trait_castmodule) dispatch through theTraitCast<I, U>trait implemented for&T,&mut T,Box<T>,Rc<T>, andArc<T>.Runtime cost per cast: two loads and a branch against the table for the root's graph.
Library additions (
core/alloc)core::marker::TraitMetadataTable<SuperTrait>— the marker/lang-item that declares a cast root; blanket impl for allSizedtypes (the actual root-supertrait obligation is enforced by the supertrait relationship itself, not the where-clauses, to break a cycle throughUnsize).core::trait_cast—TraitCast/TraitCastErrorand thecast!/try_cast!/unchecked_cast!macros.alloc::{boxed, rc, sync}— owned-cast impls.core::intrinsics:trait_metadata_index<SuperTrait, Trait>() -> (&'static u8, usize)trait_metadata_table<SuperTrait, ConcreteType>() -> (&'static u8, NonNull<Option<NonNull<()>>>)trait_metadata_table_len<SuperTrait>() -> usizetrait_cast_is_lifetime_erasure_safe<SuperTrait, TargetTrait>() -> boolThe
&'static u8returned alongside each index/table pointer is a per-global-crate sentinel used to detect theForeignTraitGraphcase when two independently-built artifacts are linked into one binary.Compiler additions
New passes / modules (all under
rustc_monomorphizeunless noted):trait_graph.rs— per-rootTraitGraphbuilt from gatheredtrait_metadata_index/trait_metadata_tablerequests.table_layout.rs— assigns slots for(sub_trait, outlives_class)pairs with condensation (BitMatrixrow-grouping) to collapse classes admitting identical impl sets.erasure_safe.rs— resolvestrait_cast_is_lifetime_erasure_safeby DFS-walking binder vars of the target dyn type and checking each is expressible through the root's binder.cast_sensitivity.rs— SCC-based batch computation of per-InstanceCastRelevantLifetimes(direct + transitive via call-graph).resolved_bodies.rs,trait_cast_requests.rs— request gathering and delayed-codegen queue.partitioning.rs— cascade-canonicalization of augmented callees so sensitive subgraphs are emitted once per signature group.MIR:
TerminatorKind::{Call, TailCall}grows acall_id: &'tcx List<(DefId, u32, GenericArgsRef<'tcx>)>recording the full inlining chain.TerminatorKindsize assertion goes from 80 → 88. Before inlining each list has length 1; the inliner prepends the caller's chain to each inlined callee's.Borrowck: new
region_summary.rspublishes aBorrowckRegionSummaryper fn (walk-position →RegionVid, call-site region mappings keyed on theu32counter) consumed by the sensitivity pass after typeck but before mono.Generic args: new
GenericArgKind::Outlives(OutlivesArg)variant (tag0b11) carrying(longer, shorter)region-index pairs. Appended to anInstance's args when a sensitive callee must be specialized for a given caller's outlives environment. Wired through interning, encode/decode, folding/visiting, symbol mangling, and all the usual suspects.New lang item:
TraitMetadataTable(sym::trait_metadata_table).HIR analysis (
wfcheck.rs,dyn_trait.rs): eagerly diagnoses at trait-definition time when a root-connected trait introduces a lifetime not expressible through the root (would be manufactured at downcast time — unsound).Diagnostics
UNUSED_CAST_TARGETlint — cast to a target no concrete type in the final binary implements (alwaysErrat runtime).trait graph rooted at {root} is not downcast-safe— erased-lifetime manufacturability check.TraitMetadataTable type argument must be a trait object— non-dyn Targ.TraitMetadataTable type argument does not match a cast root—dyn XwhereXisn'tSelfor a transitive cast-root supertrait.cast target not reachable in graph/non-dyn-compat target/tmt-arg-*— various ill-formed roots and targets.Debugging / inspection flags
All
-Z, all dump to stderr:-Z dump-trait-graph[=FILTER],-Z dump-trait-cast-sensitivity[=FILTER],-Z dump-trait-cast-augmentation[=FILTER],-Z dump-trait-cast-canonicalization,-Z dump-trait-cast-chain-composition[=FILTER],-Z dump-trait-cast-erasure-safety[=FILTER]-Z print-trait-cast-statsEach has a matching
tests/run-make/dump-*test.Rustc Perf
Perf was evaluated with rustc-perf and the impact on crates that do not use trait casting (as-in, all of them) was found to be minimal. The mono-level SCC + Floyd-Warshall pass only runs over directly- and transitively-sensitive call graphs and stops at the ground-level caller, so crates with no cast graph pay effectively nothing. Perf is from slightly better to neutral on average across rustc-perf, with modest rmeta bloat at ~2.5% for typical crates.
Known caveats for review
Not in this PR
rustc_deny_explicit_implonTraitMetadataTable.cast!onPin<P>or user smart pointers.rustdocsurfacing of cast graphs.