Skip to content

Commit 71bfe3e

Browse files
Initial implementation of bounded intertrait casting (RFC rust-lang/rfcs#3952)
Tracking issue: TBD r? @ghost (draft) ## 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. Stabilization is not proposed here; everything is gated behind `#![feature(trait_cast)]` and the new items are `#[unstable]`. The feature is large (~16k LoC across ~200 files) and intentionally landed as one commit so the graph/layout/augmentation passes stay coherent; I'd like reviewer guidance on whether to split before further review, and where the natural seams are. ## 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. A "not part of any global crate" diagnostic was considered but is not feasible — the detection info is categorically unavailable at compile time. ## 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. ## Tests - `tests/ui/trait-cast/` — 23 files: basic/lifetime-bounded downcasts, erasure-safety (chain-walk, projections, structural, outlives), cross-crate casts, invalid targets, non-dyn-compat targets, missing root bound, TMT arg mismatch, lifetime-in-generics (565 lines), torture-tests (306 lines), runtime cast failures. - `tests/run-make/` — 11 rmake tests: `trait-cast-condense-*` (baseline, param aliasing, static-in-impl, same-class-different-impls), `trait-cast-table-layout`, `cross-global-crate-casts`, `print-trait-cast-stats`, `dump-trait-*`. ## Known caveats for review - The `call_id` chain is threaded through every `TerminatorKind::Call` construction site in the compiler and in test mocks (which use `ty::List::empty()`). If there's a cleaner place to stash this — e.g. a side table keyed on basic-block / statement index — I'd take that feedback. - `OutlivesArg` lands as a first-class `GenericArgKind` variant with pack/unpack. Whether this belongs in `GenericArg` or should live as a separate field on `Instance` is a legitimate design question; it's in `GenericArgKind` today so mangling/encoding come along for free. - `library/alloc/*` and a few other paths carry pre-existing churn from earlier iterations; I'll rebase/squash those out before this is reviewable outside of a draft. - 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; guidance on a representative workload would be welcome. ## Not in this PR - Stabilization / `rustc_deny_explicit_impl` on `TraitMetadataTable` (the RFC discussion around a `pub root trait` keyword is unresolved). - `cast!` on `Pin<P>` or user smart pointers. - `rustdoc` surfacing of cast graphs.
1 parent c28e303 commit 71bfe3e

223 files changed

Lines changed: 16056 additions & 272 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

Cargo.lock

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4372,13 +4372,15 @@ dependencies = [
43724372
"rustc_errors",
43734373
"rustc_hir",
43744374
"rustc_index",
4375+
"rustc_lint_defs",
43754376
"rustc_macros",
43764377
"rustc_middle",
43774378
"rustc_session",
43784379
"rustc_span",
43794380
"rustc_target",
43804381
"serde",
43814382
"serde_json",
4383+
"smallvec",
43824384
"tracing",
43834385
]
43844386

compiler/rustc_borrowck/src/diagnostics/region_name.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -730,6 +730,10 @@ impl<'tcx> MirBorrowckCtxt<'_, '_, 'tcx> {
730730
// to search anything here.
731731
}
732732

733+
(GenericArgKind::Outlives(_), _) => {
734+
// Outlives args are metadata-only; no lifetime to find here.
735+
}
736+
733737
(
734738
GenericArgKind::Lifetime(_)
735739
| GenericArgKind::Type(_)

compiler/rustc_borrowck/src/lib.rs

Lines changed: 76 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ mod places_conflict;
8686
mod polonius;
8787
mod prefixes;
8888
mod region_infer;
89+
mod region_summary;
8990
mod renumber;
9091
mod root_cx;
9192
mod session_diagnostics;
@@ -104,38 +105,88 @@ impl<'tcx> TyCtxtConsts<'tcx> {
104105
}
105106

106107
pub fn provide(providers: &mut Providers) {
107-
*providers = Providers { mir_borrowck, ..*providers };
108+
*providers = Providers { mir_borrowck, borrowck_result, borrowck_region_summary, ..*providers };
108109
}
109110

110-
/// Provider for `query mir_borrowck`. Unlike `typeck`, this must
111-
/// only be called for typeck roots which *similar* to `typeck` will
112-
/// then borrowck all nested bodies as well.
113-
fn mir_borrowck(
114-
tcx: TyCtxt<'_>,
115-
def: LocalDefId,
116-
) -> Result<&FxIndexMap<LocalDefId, ty::DefinitionSiteHiddenType<'_>>, ErrorGuaranteed> {
111+
/// Shared core query: runs borrowck and collects both hidden types
112+
/// and region summaries into a single `BorrowckResult`.
113+
fn borrowck_result<'tcx>(tcx: TyCtxt<'tcx>, def: LocalDefId) -> &'tcx BorrowckResult<'tcx> {
117114
assert!(!tcx.is_typeck_child(def.to_def_id()));
118115
let (input_body, _) = tcx.mir_promoted(def);
119-
debug!("run query mir_borrowck: {}", tcx.def_path_str(def));
116+
debug!("run query borrowck_result: {}", tcx.def_path_str(def));
120117

121-
// We should eagerly check stalled coroutine obligations from HIR typeck.
118+
// Eagerly check stalled coroutine obligations from HIR typeck.
122119
// Not doing so leads to silent normalization failures later, which will
123120
// fail to register opaque types in the next solver.
124-
tcx.ensure_result().check_coroutine_obligations(def)?;
121+
//
122+
// This must come after `mir_promoted` so that query-cycle detection
123+
// follows the same dependency path as the original `mir_borrowck`.
124+
if let Err(guar) = tcx.ensure_result().check_coroutine_obligations(def) {
125+
return tcx.arena.alloc(BorrowckResult {
126+
hidden_types: Err(guar),
127+
region_summaries: Default::default(),
128+
});
129+
}
125130

126131
let input_body: &Body<'_> = &input_body.borrow();
127132
if let Some(guar) = input_body.tainted_by_errors {
128133
debug!("Skipping borrowck because of tainted body");
129-
Err(guar)
130-
} else if input_body.should_skip() {
134+
return tcx.arena.alloc(BorrowckResult {
135+
hidden_types: Err(guar),
136+
region_summaries: Default::default(),
137+
});
138+
}
139+
if input_body.should_skip() {
131140
debug!("Skipping borrowck because of injected body");
132-
let opaque_types = Default::default();
133-
Ok(tcx.arena.alloc(opaque_types))
134-
} else {
135-
let mut root_cx = BorrowCheckRootCtxt::new(tcx, def, None);
136-
root_cx.do_mir_borrowck();
137-
root_cx.finalize()
141+
return tcx.arena.alloc(BorrowckResult {
142+
hidden_types: Ok(Default::default()),
143+
region_summaries: Default::default(),
144+
});
145+
}
146+
147+
let mut root_cx = BorrowCheckRootCtxt::new(tcx, def, None);
148+
root_cx.do_mir_borrowck();
149+
root_cx.finalize()
150+
}
151+
152+
/// Provider for `query mir_borrowck`. Extracts hidden types from
153+
/// the shared `borrowck_result`.
154+
fn mir_borrowck(
155+
tcx: TyCtxt<'_>,
156+
def: LocalDefId,
157+
) -> Result<&FxIndexMap<LocalDefId, ty::DefinitionSiteHiddenType<'_>>, ErrorGuaranteed> {
158+
assert!(!tcx.is_typeck_child(def.to_def_id()));
159+
debug!("run query mir_borrowck: {}", tcx.def_path_str(def));
160+
161+
let result = tcx.borrowck_result(def);
162+
match &result.hidden_types {
163+
Ok(t) => Ok(t),
164+
Err(g) => Err(*g),
165+
}
166+
}
167+
168+
/// Provider for `query borrowck_region_summary`. Extracts region summary
169+
/// for a specific def_id from the shared `borrowck_result`.
170+
///
171+
/// Returns a default (empty) summary for items without borrowck (shims,
172+
/// constructors, trait method declarations). For shims, the collector
173+
/// handles the transparent-forwarder case in `compose_all_through_chain`.
174+
fn borrowck_region_summary<'tcx>(tcx: TyCtxt<'tcx>, def_id: LocalDefId) -> BorrowckRegionSummary {
175+
let root = tcx.typeck_root_def_id(def_id.to_def_id()).expect_local();
176+
// Constructors have synthetic MIR, trivial consts have no MIR — skip them.
177+
// Also bail for items without bodies (e.g. trait method declarations like
178+
// FnOnce::call_once) which cannot be borrowck'd.
179+
if matches!(tcx.def_kind(root), rustc_hir::def::DefKind::Ctor(..)) || tcx.is_trivial_const(root)
180+
{
181+
return BorrowckRegionSummary::default();
138182
}
183+
184+
if tcx.hir_node(tcx.local_def_id_to_hir_id(root)).body_id().is_none() {
185+
return BorrowckRegionSummary::default();
186+
}
187+
188+
let result = tcx.borrowck_result(root);
189+
result.region_summaries.get(&def_id).cloned().unwrap_or_default()
139190
}
140191

141192
/// Data propagated to the typeck parent by nested items.
@@ -436,6 +487,10 @@ fn borrowck_check_region_constraints<'tcx>(
436487
polonius_context,
437488
);
438489

490+
// Compute region summary for this function body.
491+
let region_summary = region_summary::compute_region_summary(&regioncx, body, tcx);
492+
root_cx.add_region_summary(def, region_summary);
493+
439494
// Dump MIR results into a file, if that is enabled. This lets us
440495
// write unit-tests, as well as helping with debugging.
441496
nll::dump_nll_mir(&infcx, body, &regioncx, &opt_closure_req, &borrow_set);
@@ -908,14 +963,15 @@ impl<'a, 'tcx> ResultsVisitor<'tcx, Borrowck<'a, 'tcx>> for MirBorrowckCtxt<'a,
908963
unwind: _,
909964
call_source: _,
910965
fn_span: _,
966+
call_id: _,
911967
} => {
912968
self.consume_operand(loc, (func, span), state);
913969
for arg in args {
914970
self.consume_operand(loc, (&arg.node, arg.span), state);
915971
}
916972
self.mutate_place(loc, (*destination, span), Deep, state);
917973
}
918-
TerminatorKind::TailCall { func, args, fn_span: _ } => {
974+
TerminatorKind::TailCall { func, args, fn_span: _, call_id: _ } => {
919975
self.consume_operand(loc, (func, span), state);
920976
for arg in args {
921977
self.consume_operand(loc, (&arg.node, arg.span), state);

compiler/rustc_borrowck/src/polonius/legacy/loan_invalidations.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,7 @@ impl<'a, 'tcx> Visitor<'tcx> for LoanInvalidationsGenerator<'a, 'tcx> {
125125
unwind: _,
126126
call_source: _,
127127
fn_span: _,
128+
call_id: _,
128129
} => {
129130
self.consume_operand(location, func);
130131
for arg in args {

0 commit comments

Comments
 (0)