diff --git a/compiler/rustc_interface/src/passes.rs b/compiler/rustc_interface/src/passes.rs index 060083e3bb59b..b1bbd19dc7214 100644 --- a/compiler/rustc_interface/src/passes.rs +++ b/compiler/rustc_interface/src/passes.rs @@ -1262,6 +1262,10 @@ fn analysis(tcx: TyCtxt<'_>, (): ()) { }); }); } + + // Cross-crate dead-fn elimination: if requested, walk this crate's MIR now (before + // codegen, so it works on `--emit=metadata`) and write per-dependency used-set files. + rustc_monomorphize::emit_used_set_if_requested(tcx); } /// Runs the codegen backend, after which the AST and analysis can diff --git a/compiler/rustc_monomorphize/src/dead_fn_elim.rs b/compiler/rustc_monomorphize/src/dead_fn_elim.rs new file mode 100644 index 0000000000000..93bb95bdb29a3 --- /dev/null +++ b/compiler/rustc_monomorphize/src/dead_fn_elim.rs @@ -0,0 +1,603 @@ +//! Dead function elimination via BFS reachability — `-Z dead-fn-elimination`. +//! +//! Identifies functions unreachable from the binary entry point via a breadth-first +//! search over the call graph, then marks them so they can be removed from the set of +//! monomorphized items before codegen. +//! +//! This pass is scoped to binary crates only (executables and `bin` test harnesses). +//! Library crate types early-return at the top of [`run_analysis`] because their public +//! items are reachable by definition (downstream crates may call any `pub fn`) and there +//! is no entry point to seed the search from. + +use std::cell::RefCell; +use std::collections::VecDeque; +use std::panic; + +use rustc_data_structures::fx::{FxHashSet, FxIndexMap, FxIndexSet}; +use rustc_hir::def::DefKind; +use rustc_middle::middle::codegen_fn_attrs::CodegenFnAttrFlags; +use rustc_middle::mir::{Body, TerminatorKind}; +use rustc_middle::mono::MonoItem; +use rustc_middle::ty::{self, Ty, TyCtxt}; +use rustc_span::def_id::DefId; + +// `thread_local!` keeps the footprint of this experimental flag minimal. If the flag is +// stabilized, this state should move to a `GlobalCtxt` field or a dedicated query so the +// reachable set is computed and cached through the normal query machinery. +thread_local! { + /// Trait `DefId` indices seen in vtable constructions (`dyn Trait` casts). + static VTABLE_TRAITS: RefCell> = RefCell::new(FxHashSet::default()); + + /// Function `DefId`s whose address has been taken. Populated during MIR traversal by + /// [`scan_for_address_taken`]. These are unioned into the BFS seeds so they survive the + /// call graph: an indirect call site is not visible to direct-call BFS. + static ADDRESS_TAKEN: RefCell> = RefCell::new(FxHashSet::default()); + + /// Functions identified as unreachable and safe to eliminate. Populated by + /// [`run_analysis`]; read via [`is_eliminable`]. + static ELIMINABLE_DEF_IDS: RefCell> = RefCell::new(FxHashSet::default()); +} + +/// Encode a `DefId` as a `u64` key: high 32 bits = crate index, low 32 bits = item index. +#[inline] +fn def_id_key(def_id: DefId) -> u64 { + ((def_id.krate.as_u32() as u64) << 32) | (def_id.index.as_u32() as u64) +} + +/// Run BFS reachability analysis and populate `ELIMINABLE_DEF_IDS`. +/// +/// `mono_items` is the set of monomorphized items already collected by the caller; it is +/// used as the ground truth for [`is_safe_to_eliminate`], so this function never re-enters +/// the monomorphization collection query. +pub(crate) fn run_analysis(tcx: TyCtxt<'_>, mono_items: &[MonoItem<'_>]) { + // A used-set file (`-Zdead-fn-used-set`) carries the `DefPathHash`es a downstream binary + // reaches in *this* crate. When present, it seeds the BFS for a library crate: `pub` + // functions the binary never calls become eliminable. This is the cross-crate case. + // Resumable codegen: metadata is written *before* this point (at codegen start), which + // unblocks Cargo to compile the downstream binary via pipelining. If `-Zdead-fn-wait-used- + // set=N` is set, block here up to N seconds for that binary's frontend to produce the + // used-set — so this library's frontend runs once and only its (pruned) codegen is deferred. + if let (Some(path), Some(secs)) = ( + tcx.sess.opts.unstable_opts.dead_fn_used_set.as_deref(), + tcx.sess.opts.unstable_opts.dead_fn_wait_used_set, + ) && !path.exists() + { + let deadline = std::time::Instant::now() + std::time::Duration::from_secs(secs as u64); + while !path.exists() && std::time::Instant::now() < deadline { + std::thread::sleep(std::time::Duration::from_millis(20)); + } + } + + let used_set = tcx + .sess + .opts + .unstable_opts + .dead_fn_used_set + .as_deref() + .and_then(|p| crate::used_set::UsedSet::load(tcx, p)); + + // Without a used-set, the pass is binary-only: skip library crates entirely. A binary's + // collector is already exact, so there is nothing to eliminate there without a used-set, + // and a library without one has no seeds — every private fn would be dropped incorrectly. + if used_set.is_none() && tcx.entry_fn(()).is_none() { + return; + } + + // Build the set of `DefId`s that the collector kept. In the *binary* case dropping any of + // these would dangle a symbol, so they are never eligible. In the *cross-crate* case this + // guard must be empty: a library's collector eagerly emits its whole `pub` closure, so + // "the local collector kept it" is exactly the over-emission we are removing — authority + // is the used-set, not the local collector. Soundness is still enforced by + // `is_locally_safe_to_eliminate` (linker-visible / lang / vtable / drop-glue / generics), + // which `is_safe_to_eliminate` always applies. + let mono_def_ids: FxHashSet = if used_set.is_some() { + FxHashSet::default() + } else { + mono_items + .iter() + .filter_map(|item| match item { + MonoItem::Fn(instance) => Some(instance.def_id()), + MonoItem::Static(def_id) => Some(*def_id), + _ => None, + }) + .collect() + }; + + // Build the call graph (vtable constructions are scanned inline by `add_mir_edges`, + // which also records address-taken functions). + let call_graph = build_call_graph(tcx); + + // BFS from entry seeds. Vtable traits and address-taken functions were populated by + // `build_call_graph`; the latter are unioned into the seed set below so that functions + // reached only through indirect calls survive. + let mut seeds = collect_seeds(tcx, used_set.as_ref()); + // Iteration order over the `FxHashSet` does not affect the resulting seed set. + #[allow(rustc::potential_query_instability)] + ADDRESS_TAKEN.with(|a| { + for &key in a.borrow().iter() { + seeds.insert(key); + } + }); + let reachable = run_bfs(seeds, &call_graph); + + // In the binary-only case, every `reachable_set` item was seeded, so BFS cannot drop it. + // (In the cross-crate case this invariant is intentionally relaxed: unused `pub` fns are + // dropped by design, so only mandatory-keep items are guaranteed to survive.) + #[cfg(debug_assertions)] + if used_set.is_none() { + let reachable_set = tcx.reachable_set(()); + for &local_def_id in tcx.mir_keys(()) { + if reachable_set.contains(&local_def_id) { + let key = def_id_key(local_def_id.to_def_id()); + debug_assert!( + reachable.contains(&key), + "invariant violated: reachable_set item {key:?} missing from post-BFS set" + ); + } + } + } + + if let Some(used) = &used_set { + tcx.sess.dcx().note(format!( + "-Z dead-fn-elimination: cross-crate used-set with {} entries applied", + used.len() + )); + } + + // Mark unreachable and safe functions as eliminable. + let mut count = 0usize; + ELIMINABLE_DEF_IDS.with(|e| { + let mut eliminable = e.borrow_mut(); + for &local_def_id in tcx.mir_keys(()) { + let def_id = local_def_id.to_def_id(); + if !tcx.def_kind(def_id).is_fn_like() { + continue; + } + let key = def_id_key(def_id); + if !reachable.contains(&key) && is_safe_to_eliminate(tcx, def_id, &mono_def_ids) { + eliminable.insert(def_id.index.as_u32() as u64); + count += 1; + } + } + }); + + if count > 0 { + tcx.sess.dcx().note(format!( + "{count} unreachable functions excluded from codegen by -Z dead-fn-elimination" + )); + } +} + +/// Returns `true` if the given local `DefId` index was identified as eliminable. Callers +/// use this for per-item O(1) lookups, so it avoids cloning the set. +pub(crate) fn is_eliminable(idx: u64) -> bool { + ELIMINABLE_DEF_IDS.with(|e| e.borrow().contains(&idx)) +} + +/// Collect the BFS seed set: everything that must be kept regardless of the call graph. +/// +/// `used_set` is `Some` only in the cross-crate (library) case. When present, a public +/// function is seeded *only if* the downstream binary actually reaches it — this is what +/// lets an unused `pub fn` be eliminated. Items that must be kept for reasons other than +/// visibility (lang items, `#[used]`/`#[no_mangle]`, custom linkage) are still seeded +/// unconditionally, because dropping them would dangle a required symbol. +fn collect_seeds(tcx: TyCtxt<'_>, used_set: Option<&crate::used_set::UsedSet>) -> FxHashSet { + let mut seeds = FxHashSet::default(); + + // `reachable_set` is the non-eliminable lower bound. It already covers public items by + // effective visibility, lang items, trait impl items, custom-linkage symbols + // (`#[no_mangle]`/`#[used]`/`#[export_name]`), foreign-item-symbol aliases, and + // (transitively) const-initializer references — including the test-harness `TESTS` + // slice. See `compiler/rustc_passes/src/reachable.rs`. + let reachable_set = tcx.reachable_set(()); + for &local_def_id in tcx.mir_keys(()) { + if !reachable_set.contains(&local_def_id) { + continue; + } + let def_id = local_def_id.to_def_id(); + // Cross-crate: a free `pub fn` in `reachable_set` only by visibility becomes a + // *candidate* for elimination when the binary does not use it — so do not seed it. + // Soundness is still enforced downstream by `is_safe_to_eliminate` (keeps linker- + // visible / lang / vtable / drop-glue / generic items); the used-set only narrows the + // provably-droppable slice. + // + // Trait-impl associated methods are excluded from candidacy entirely: a downstream + // crate can invoke them via a trait bound, `dyn` dispatch, or a `fmt`/fn-pointer table + // built in *its* code — none of which appear in this crate's MIR, and which the + // used-set probe (a direct-call MIR walk) cannot fully observe. Keeping every `pub` + // trait-impl method is the conservative, sound floor for cross-crate elimination. + let is_trait_impl_method = tcx.def_kind(def_id) == DefKind::AssocFn + && tcx.associated_item(def_id).trait_item_def_id().is_some(); + if let Some(used) = used_set + && tcx.def_kind(def_id) == DefKind::Fn + && !is_trait_impl_method + && is_locally_safe_to_eliminate(tcx, def_id) + && !used.contains(tcx, def_id) + { + continue; + } + seeds.insert(def_id_key(def_id)); + } + + // The entry function is a binary-specific seed that `reachable_set` does not provide. + if let Some((entry_def_id, _)) = tcx.entry_fn(()) { + seeds.insert(def_id_key(entry_def_id)); + } + + // Vtable-constructed trait impl methods (local). Dynamic dispatch is invisible to the + // call-graph BFS, so methods of any trait used as `dyn Trait` must be seeded. + for &local_def_id in tcx.mir_keys(()) { + let def_id = local_def_id.to_def_id(); + if tcx.def_kind(def_id) != DefKind::AssocFn { + continue; + } + let assoc_item = tcx.associated_item(def_id); + if let Some(trait_method_def_id) = assoc_item.trait_item_def_id() { + let trait_def_id = tcx.parent(trait_method_def_id); + if tcx.is_dyn_compatible(trait_def_id) { + let trait_idx = trait_def_id.index.as_u32() as u64; + if VTABLE_TRAITS.with(|v| v.borrow().contains(&trait_idx)) { + seeds.insert(def_id_key(def_id)); + } + } + } + } + seeds +} + +/// Build call graph edges for all local (same-crate) fn-like items. +/// +/// Cross-crate edges are intentionally not added: `is_safe_to_eliminate` rejects any +/// non-local `DefId`, so extern-crate edges could never demote an item from the eliminable +/// set. Cross-crate effects are captured instead by `reachable_set` and address-taken +/// seeding. +fn build_call_graph(tcx: TyCtxt<'_>) -> FxIndexMap> { + let mut graph: FxIndexMap> = FxIndexMap::default(); + for &local_def_id in tcx.mir_keys(()) { + let def_id = local_def_id.to_def_id(); + let def_kind = tcx.def_kind(def_id); + if def_kind.is_fn_like() { + add_mir_edges(tcx, def_id, &mut graph); + } else if matches!( + def_kind, + DefKind::Const { .. } | DefKind::Static { .. } | DefKind::AssocConst { .. } + ) { + // A `const FLAGS: &[&dyn Flag] = &[..]` performs its `Unsize` coercions inside + // a const/static initializer rather than in function-body MIR. By the time we + // read it the coercions are const-evaluated into an allocation whose provenance + // points at `GlobalAlloc::VTable`/`GlobalAlloc::Function`, which + // `scan_for_address_taken` (it only walks fn MIR) misses. We must walk the + // evaluated allocation graph to seed `VTABLE_TRAITS` and `ADDRESS_TAKEN`, + // otherwise impl methods reachable only through such const vtables would be + // wrongly eliminated. + scan_const_static_allocs(tcx, def_id); + } + } + graph +} + +/// Read MIR for `def_id` and insert call edges into `graph`. Silently skips if MIR is +/// unavailable or if `optimized_mir` panics. +fn add_mir_edges(tcx: TyCtxt<'_>, def_id: DefId, graph: &mut FxIndexMap>) { + if !tcx.is_mir_available(def_id) { + return; + } + let Ok(body) = panic::catch_unwind(panic::AssertUnwindSafe(|| tcx.optimized_mir(def_id))) + else { + return; + }; + scan_for_address_taken(body); + let caller_key = def_id_key(def_id); + let edges = graph.entry(caller_key).or_default(); + for bb in body.basic_blocks.iter() { + // Direct call edges. + if let TerminatorKind::Call { func, .. } = &bb.terminator().kind { + if let rustc_middle::mir::Operand::Constant(c) = func { + if let ty::FnDef(callee_def_id, _) = c.const_.ty().kind() { + edges.insert(def_id_key(*callee_def_id)); + } + } + } + // A closure/coroutine is created via an `Aggregate` rvalue and invoked later + // through `Fn*`/`poll`, so there is no direct `Call` to it from its creator; + // without this edge the closure body (and everything it calls) would be wrongly + // unreachable. Likewise any `FnDef` mentioned as a value (passed to an adaptor such + // as `iter.map(f)`) must become an edge, not only direct call targets. + for stmt in &bb.statements { + use rustc_middle::mir::{AggregateKind, Rvalue, StatementKind}; + if let StatementKind::Assign(box (_, rvalue)) = &stmt.kind { + // Closure / coroutine / coroutine-closure construction. + if let Rvalue::Aggregate(box kind, _) = rvalue { + match kind { + AggregateKind::Closure(cdef, _) + | AggregateKind::Coroutine(cdef, _) + | AggregateKind::CoroutineClosure(cdef, _) => { + edges.insert(def_id_key(*cdef)); + } + _ => {} + } + } + // Any operand that names a function by value (a fn item passed around). + for op in rvalue_operands(rvalue) { + if let rustc_middle::mir::Operand::Constant(c) = op { + if let ty::FnDef(fdef, _) = c.const_.ty().kind() { + edges.insert(def_id_key(*fdef)); + } + } + } + } + } + } +} + +/// Best-effort iterator over the operands an `Rvalue` reads, so we can spot `FnDef` values +/// used as arguments to adaptors (`iter.map(some_fn)`), not only direct call targets. +fn rvalue_operands<'a, 'tcx>( + rvalue: &'a rustc_middle::mir::Rvalue<'tcx>, +) -> Vec<&'a rustc_middle::mir::Operand<'tcx>> { + use rustc_middle::mir::Rvalue; + let mut out = Vec::new(); + match rvalue { + Rvalue::Use(op, _) + | Rvalue::Repeat(op, _) + | Rvalue::Cast(_, op, _) + | Rvalue::UnaryOp(_, op) => out.push(op), + Rvalue::BinaryOp(_, box (a, b)) => { + out.push(a); + out.push(b); + } + Rvalue::Aggregate(_, ops) => out.extend(ops.iter()), + _ => {} + } + out +} + +/// Walk a const/static's evaluated allocation graph, recording any vtable traits +/// (`GlobalAlloc::VTable`) and address-taken functions (`GlobalAlloc::Function`) reachable +/// through its provenance. Silently skips on any evaluation failure. +fn scan_const_static_allocs(tcx: TyCtxt<'_>, def_id: DefId) { + use rustc_middle::mir::interpret::GlobalAlloc; + + let mut roots: Vec = Vec::new(); + + // Statics and consts use different queries: `const_eval_poly` asserts it is not called + // on a static (statics are places, not values). + if tcx.is_static(def_id) { + if let Ok(Ok(alloc)) = + panic::catch_unwind(panic::AssertUnwindSafe(|| tcx.eval_static_initializer(def_id))) + { + collect_alloc_ids(alloc, &mut roots); + } + } else if matches!(tcx.def_kind(def_id), DefKind::Const { .. } | DefKind::AssocConst { .. }) { + // Only non-generic consts are poly-evaluable; generic ones panic the query. + if !tcx.generics_of(def_id).requires_monomorphization(tcx) { + if let Ok(Ok(val)) = + panic::catch_unwind(panic::AssertUnwindSafe(|| tcx.const_eval_poly(def_id))) + { + push_const_value_allocs(val, &mut roots); + } + } + } + + // BFS over the allocation graph following provenance pointers. + let mut seen: FxHashSet = FxHashSet::default(); + while let Some(id) = roots.pop() { + if !seen.insert(id) { + continue; + } + match tcx.try_get_global_alloc(id) { + Some(GlobalAlloc::VTable(_ty, predicates)) => { + if let Some(principal) = predicates.principal() { + let trait_def_id = principal.def_id(); + VTABLE_TRAITS.with(|v| { + v.borrow_mut().insert(trait_def_id.index.as_u32() as u64); + }); + } + } + Some(GlobalAlloc::Function { instance }) => { + ADDRESS_TAKEN.with(|a| { + a.borrow_mut().insert(def_id_key(instance.def_id())); + }); + } + Some(GlobalAlloc::Memory(alloc)) => { + collect_alloc_ids(alloc, &mut roots); + } + // A reference to another static — its own scan covers it. + _ => {} + } + } +} + +/// Push every `AllocId` referenced by `alloc`'s provenance into `out`. +fn collect_alloc_ids( + alloc: rustc_middle::mir::interpret::ConstAllocation<'_>, + out: &mut Vec, +) { + for prov in alloc.inner().provenance().provenances() { + out.push(prov.alloc_id()); + } +} + +/// Extract any `AllocId`s carried by a `ConstValue` (`Indirect`/`Slice` carry one directly; +/// a thin pointer is `Scalar(Scalar::Ptr)`). +fn push_const_value_allocs( + val: rustc_middle::mir::ConstValue, + out: &mut Vec, +) { + use rustc_middle::mir::ConstValue; + use rustc_middle::mir::interpret::Scalar; + match val { + ConstValue::Indirect { alloc_id, .. } | ConstValue::Slice { alloc_id, .. } => { + out.push(alloc_id); + } + ConstValue::Scalar(Scalar::Ptr(ptr, _)) => { + out.push(ptr.provenance.alloc_id()); + } + _ => {} + } +} + +/// BFS over the call graph from the seed set, returning the set of reachable keys. +fn run_bfs(seeds: FxHashSet, graph: &FxIndexMap>) -> FxHashSet { + let mut marked: FxHashSet = FxHashSet::default(); + let mut queue = VecDeque::new(); + // Seeds are a membership-only set; sort them for deterministic traversal order. + #[allow(rustc::potential_query_instability)] + let mut sorted_seeds: Vec = seeds.into_iter().collect(); + sorted_seeds.sort_unstable(); + for seed in sorted_seeds { + if marked.insert(seed) { + queue.push_back(seed); + } + } + while let Some(current) = queue.pop_front() { + if let Some(callees) = graph.get(¤t) { + // `FxIndexSet` preserves insertion order; iterate directly. + for &callee in callees { + if marked.insert(callee) { + queue.push_back(callee); + } + } + } + } + marked +} + +/// Safety check combining the cheap local floor with the `mono_items` ground truth: if the +/// monomorphization collector kept this item, dropping it would dangle a symbol. +fn is_safe_to_eliminate(tcx: TyCtxt<'_>, def_id: DefId, mono_items: &FxHashSet) -> bool { + if mono_items.contains(&def_id) { + return false; + } + is_locally_safe_to_eliminate(tcx, def_id) +} + +/// Cheap, local-only safety floor (no monomorphization collection). Rejects items that are +/// unsound to drop regardless of reachability: non-local, non-fn-like, vtable-trait impl +/// methods, linker-visible symbols, `Drop` glue, the entry fn, generics, and async. +fn is_locally_safe_to_eliminate(tcx: TyCtxt<'_>, def_id: DefId) -> bool { + if !def_id.is_local() { + return false; + } + + let def_kind = tcx.def_kind(def_id); + match def_kind { + DefKind::Fn => {} + DefKind::AssocFn => { + // Don't eliminate trait impl methods for vtable-constructed traits. + let assoc_item = tcx.associated_item(def_id); + if let Some(trait_method_def_id) = assoc_item.trait_item_def_id() { + let trait_def_id = tcx.parent(trait_method_def_id); + if tcx.is_dyn_compatible(trait_def_id) { + let trait_idx = trait_def_id.index.as_u32() as u64; + if VTABLE_TRAITS.with(|v| v.borrow().contains(&trait_idx)) { + return false; + } + } + } + } + _ => return false, + } + + // Don't eliminate linker-visible symbols. + let attrs = tcx.codegen_fn_attrs(def_id); + if attrs.flags.intersects(CodegenFnAttrFlags::NO_MANGLE | CodegenFnAttrFlags::USED_LINKER) + || attrs.symbol_name.is_some() + || attrs.linkage.is_some() + { + return false; + } + + // Don't eliminate `Drop::drop` implementations. + if def_kind == DefKind::AssocFn { + if let Some(drop_trait) = tcx.lang_items().drop_trait() { + let assoc_item = tcx.associated_item(def_id); + if let Some(trait_item_id) = assoc_item.trait_item_def_id() { + if tcx.parent(trait_item_id) == drop_trait { + return false; + } + } + } + } + + // Don't eliminate the entry function. + if tcx.entry_fn(()).map(|(id, _)| id) == Some(def_id) { + return false; + } + + // Don't eliminate generic functions: they are not codegened until monomorphized, and + // `requires_monomorphization` correctly singles out type- and const-generic parameters + // (lifetime parameters do not require monomorphization). + if tcx.generics_of(def_id).requires_monomorphization(tcx) { + return false; + } + + // Don't eliminate async functions (the state-machine transform runs after MIR). + if tcx.asyncness(def_id).is_async() { + return false; + } + + true +} + +/// Scan a MIR body for address-taking operations. Records vtable constructions +/// (`PointerCoercion::Unsize` to `dyn Trait`) into `VTABLE_TRAITS`, and function-pointer +/// reifications / closure-to-fn-pointer coercions into `ADDRESS_TAKEN`. Inline-asm `sym fn` +/// operands are handled by [`scan_inline_asm`]. +fn scan_for_address_taken(body: &Body<'_>) { + use rustc_middle::mir::{CastKind, Operand, Rvalue, StatementKind}; + use rustc_middle::ty::adjustment::PointerCoercion; + for bb in body.basic_blocks.iter() { + for stmt in &bb.statements { + if let StatementKind::Assign(box (_, Rvalue::Cast(kind, op, target_ty))) = &stmt.kind { + match kind { + CastKind::PointerCoercion(PointerCoercion::Unsize, _) => { + record_dyn_traits(*target_ty); + } + CastKind::PointerCoercion(PointerCoercion::ReifyFnPointer(_), _) + | CastKind::PointerCoercion(PointerCoercion::ClosureFnPointer(_), _) => { + if let Operand::Constant(c) = op { + if let ty::FnDef(def_id, _) = c.const_.ty().kind() { + ADDRESS_TAKEN.with(|a| { + a.borrow_mut().insert(def_id_key(*def_id)); + }); + } + } + } + _ => {} + } + } + } + scan_inline_asm(&bb.terminator().kind); + } +} + +/// Inline-asm `sym fn` operands reference functions outside the call graph. +fn scan_inline_asm(kind: &TerminatorKind<'_>) { + if let TerminatorKind::InlineAsm { operands, .. } = kind { + for op in operands.iter() { + if let rustc_middle::mir::InlineAsmOperand::SymFn { value } = op { + if let ty::FnDef(def_id, _) = value.const_.ty().kind() { + ADDRESS_TAKEN.with(|a| { + a.borrow_mut().insert(def_id_key(*def_id)); + }); + } + } + } + } +} + +/// Record the principal trait of any `dyn Trait` type into `VTABLE_TRAITS`. +fn record_dyn_traits(ty: Ty<'_>) { + match ty.kind() { + ty::Dynamic(predicates, ..) => { + if let Some(def_id) = predicates.principal_def_id() { + VTABLE_TRAITS.with(|v| { + v.borrow_mut().insert(def_id.index.as_u32() as u64); + }); + } + } + ty::Ref(_, inner, _) | ty::RawPtr(inner, _) => record_dyn_traits(*inner), + _ => {} + } +} diff --git a/compiler/rustc_monomorphize/src/lib.rs b/compiler/rustc_monomorphize/src/lib.rs index 0b5a3d1f267da..8f83de13bcecb 100644 --- a/compiler/rustc_monomorphize/src/lib.rs +++ b/compiler/rustc_monomorphize/src/lib.rs @@ -1,4 +1,5 @@ // tidy-alphabetical-start +#![feature(box_patterns)] #![feature(file_buffered)] #![feature(impl_trait_in_assoc_type)] #![feature(once_cell_get_mut)] @@ -7,16 +8,18 @@ use rustc_hir::lang_items::LangItem; use rustc_middle::query::TyCtxtAt; use rustc_middle::ty::adjustment::CustomCoerceUnsized; -use rustc_middle::ty::{self, Ty}; +use rustc_middle::ty::{self, Ty, TyCtxt}; use rustc_middle::util::Providers; use rustc_middle::{bug, traits}; use rustc_span::ErrorGuaranteed; mod collector; +mod dead_fn_elim; mod diagnostics; mod graph_checks; mod mono_checks; mod partitioning; +mod used_set; mod util; fn custom_coerce_unsize_info<'tcx>( @@ -50,3 +53,12 @@ pub fn provide(providers: &mut Providers) { partitioning::provide(providers); mono_checks::provide(&mut providers.queries); } + +/// If `-Zdead-fn-emit-used-set=` is set, walk this crate's MIR (post-analysis, before +/// codegen) and write the per-dependency used-set files. Called from the analysis pass so it +/// works on `--emit=metadata` builds (no codegen required) — the first-build mechanism. +pub fn emit_used_set_if_requested(tcx: TyCtxt<'_>) { + if let Some(dir) = &tcx.sess.opts.unstable_opts.dead_fn_emit_used_set { + used_set::emit_used_sets(tcx, dir); + } +} diff --git a/compiler/rustc_monomorphize/src/partitioning.rs b/compiler/rustc_monomorphize/src/partitioning.rs index aee7153419883..f215932f0b3f0 100644 --- a/compiler/rustc_monomorphize/src/partitioning.rs +++ b/compiler/rustc_monomorphize/src/partitioning.rs @@ -1135,7 +1135,7 @@ fn collect_and_partition_mono_items(tcx: TyCtxt<'_>, (): ()) -> MonoItemPartitio MonoItemCollectionStrategy::Lazy }; - let (items, usage_map) = collector::collect_crate_mono_items(tcx, collection_strategy); + let (mut items, usage_map) = collector::collect_crate_mono_items(tcx, collection_strategy); // Perform checks that need to operate on the entire mono item graph target_specific_checks(tcx, &items, &usage_map); @@ -1144,6 +1144,20 @@ fn collect_and_partition_mono_items(tcx: TyCtxt<'_>, (): ()) -> MonoItemPartitio // (codegen relies on this and ICEs will happen if this is violated.) tcx.dcx().abort_if_errors(); + // Dead function elimination: drop local functions that are unreachable from the + // entry point before they reach codegen. The analysis uses the just-collected + // `items` as ground truth, so it does not re-enter this query. + if tcx.sess.opts.unstable_opts.dead_fn_elimination { + crate::dead_fn_elim::run_analysis(tcx, &items); + items.retain(|item| match item { + MonoItem::Fn(instance) => { + let did = instance.def_id(); + !(did.is_local() && crate::dead_fn_elim::is_eliminable(did.index.as_u32() as u64)) + } + _ => true, + }); + } + let (codegen_units, _) = tcx.sess.time("partition_and_assert_distinct_symbols", || { par_join( || { diff --git a/compiler/rustc_monomorphize/src/used_set.rs b/compiler/rustc_monomorphize/src/used_set.rs new file mode 100644 index 0000000000000..5dab9c98472bb --- /dev/null +++ b/compiler/rustc_monomorphize/src/used_set.rs @@ -0,0 +1,175 @@ +//! Cross-crate used-set: the producer ([`emit_used_sets`]) and consumer ([`UsedSet`]) for +//! `-Zdead-fn-emit-used-set` / `-Zdead-fn-used-set`. +//! +//! In its binary-only form (see [`super::dead_fn_elim`]) the pass is a no-op: a binary's +//! monomorphization collector is already exact, so nothing it emits is unreachable. The +//! *cross-crate* form addresses the real slack: a library is compiled before its callers and +//! must conservatively emit its whole `pub` closure, most of which no final binary calls. +//! +//! **Producer** ([`emit_used_sets`], run on the *binary* at `--emit=metadata`, before any +//! codegen): walk the binary's MIR and record every extern `fn` it references. This is the +//! set of a dependency's functions the binary actually reaches — computed without codegen, so +//! it can drive a *first-build* speedup. The binary's monomorphization collector cannot be +//! used here: it filters to local `DefId`s and never lists non-generic extern fns. +//! +//! **Consumer** ([`UsedSet`], run on each *library*): keep only the used-set functions (plus +//! what soundness requires) and drop the rest before `partition()`. +//! +//! **Identity** is the `local_hash` half of [`DefPathHash`] — the def-path fingerprint within +//! a crate. It is stable across compilations, unlike the mangled symbol name, whose crate +//! disambiguator differs between the probe compile and the dependency's own compile. +//! +//! File format (one 16-hex `local_hash` per line, `#`-comments and blanks ignored): +//! ```text +//! # used-set for `deplib` (MIR probe, DefPathHash local_hash) +//! 98912570aa6ef455 +//! ``` + +use std::collections::BTreeMap; +use std::panic; +use std::path::Path; + +use rustc_data_structures::fx::{FxHashSet, FxIndexSet}; +use rustc_middle::mir::TerminatorKind; +use rustc_middle::ty::{self, TyCtxt}; +use rustc_span::def_id::{DefId, LOCAL_CRATE}; + +/// A parsed used-set: the crate-local `DefPathHash` hashes a downstream binary reaches in +/// this crate. Keyed on the *local_hash* half of `DefPathHash`, which identifies an item by +/// its def-path within the crate and is stable across compilations (unlike the mangled +/// symbol name, whose crate-disambiguator changes between `cargo check` and `cargo build`). +pub(crate) struct UsedSet { + local_hashes: FxHashSet, +} + +impl UsedSet { + /// Parse a used-set file (one 16-hex local_hash per line). Returns `None` (and warns) if + /// the file cannot be read, so a missing/corrupt used-set degrades to the sound fallback + /// of keeping the full `pub` closure rather than miscompiling. + pub(crate) fn load(tcx: TyCtxt<'_>, path: &Path) -> Option { + let contents = match std::fs::read_to_string(path) { + Ok(c) => c, + Err(e) => { + tcx.sess.dcx().warn(format!( + "-Z dead-fn-elimination: cannot read used-set file {}: {e}; \ + keeping full public closure", + path.display() + )); + return None; + } + }; + let local_hashes: FxHashSet = contents + .lines() + .map(str::trim) + .filter(|l| !l.is_empty() && !l.starts_with('#')) + .filter_map(|l| u64::from_str_radix(l, 16).ok()) + .collect(); + Some(UsedSet { local_hashes }) + } + + /// Is this crate's function in the binary's used-set? Keyed on the def-path-stable + /// `local_hash`, so it matches regardless of the crate disambiguator differences between + /// the probe compile and this one. + pub(crate) fn contains(&self, tcx: TyCtxt<'_>, def_id: DefId) -> bool { + self.local_hashes.contains(&tcx.def_path_hash(def_id).local_hash().as_u64()) + } + + pub(crate) fn len(&self) -> usize { + self.local_hashes.len() + } +} + +/// The used-set **probe** (component 1). Walks this crate's MIR — available after analysis, +/// *before* codegen — and records every *extern* (non-local) function it references from a +/// call or as a function value. Each such reference is the exact non-generic dependency +/// function this crate reaches; grouped by crate and written as one `.usedset` file +/// per dependency into `dir`. +/// +/// This is the first-build mechanism: it runs on `--emit=metadata` (no codegen), so a later +/// dependency compile can be pruned against the used-set and codegen'd *once*, rather than +/// fully codegen'd and then discarded. It deliberately does **not** use the monomorphization +/// collector, which filters to local `DefId`s and so never lists non-generic extern fns. +pub(crate) fn emit_used_sets(tcx: TyCtxt<'_>, dir: &Path) { + if let Err(e) = std::fs::create_dir_all(dir) { + tcx.sess.dcx().warn(format!("-Z dead-fn-emit-used-set: cannot create {}: {e}", dir.display())); + return; + } + + // Collect extern fn DefIds referenced across all local MIR bodies. + let mut externs: FxIndexSet = FxIndexSet::default(); + for &local_def_id in tcx.mir_keys(()) { + let def_id = local_def_id.to_def_id(); + if !tcx.is_mir_available(def_id) { + continue; + } + let Ok(body) = + panic::catch_unwind(panic::AssertUnwindSafe(|| tcx.optimized_mir(def_id))) + else { + continue; + }; + for bb in body.basic_blocks.iter() { + if let TerminatorKind::Call { func, .. } = &bb.terminator().kind + && let rustc_middle::mir::Operand::Constant(c) = func + && let ty::FnDef(callee, _) = c.const_.ty().kind() + && !callee.is_local() + { + externs.insert(*callee); + } + for stmt in &bb.statements { + use rustc_middle::mir::{Rvalue, StatementKind}; + if let StatementKind::Assign(box (_, Rvalue::Use(op, _) | Rvalue::Cast(_, op, _))) = + &stmt.kind + && let rustc_middle::mir::Operand::Constant(c) = op + && let ty::FnDef(callee, _) = c.const_.ty().kind() + && !callee.is_local() + { + externs.insert(*callee); + } + } + } + } + + // Group by defining crate; key each entry by the extern fn's def-path-stable `local_hash` + // (the low half of its `DefPathHash`). This is stable across compilations — unlike the + // mangled symbol, whose crate disambiguator differs between the probe compile and the + // dependency's own compile — so the consumer matches it reliably. + let mut per_crate: BTreeMap> = BTreeMap::new(); + for &did in &externs { + if did.krate == LOCAL_CRATE || tcx.def_kind(did) != rustc_hir::def::DefKind::Fn { + continue; + } + // Only non-generic fns are eliminable candidates; generics are handled by the collector. + if tcx.generics_of(did).count() != 0 { + continue; + } + let name = tcx.crate_name(did.krate).to_string(); + let local_hash = tcx.def_path_hash(did).local_hash().as_u64(); + per_crate.entry(name).or_default().insert(local_hash); + } + + // The used-set of a dependency is the *union* over every crate that calls it: `Main`'s + // probe names `a::f`, but `b::used` is called from inside `a`'s body, so only `a`'s probe + // names it. Each crate therefore *appends* its references; duplicate lines are harmless + // (the consumer dedups into a set). Append is used rather than read-modify-write so the + // parallel crate compilations writing the same file do not race. + use std::io::Write; + let mut wrote = 0; + for (crate_name, hashes) in &per_crate { + let path = dir.join(format!("{crate_name}.usedset")); + let mut body = String::new(); + for h in hashes { + body.push_str(&format!("{h:016x}\n")); + } + if let Ok(mut f) = std::fs::OpenOptions::new().create(true).append(true).open(&path) + && f.write_all(body.as_bytes()).is_ok() + { + wrote += 1; + } + } + tcx.sess.dcx().note(format!( + "-Z dead-fn-emit-used-set: probed {} extern fns → {} used-set files in {}", + externs.len(), + wrote, + dir.display() + )); +} diff --git a/compiler/rustc_session/src/options.rs b/compiler/rustc_session/src/options.rs index 897aa91f7dbda..1024555e86439 100644 --- a/compiler/rustc_session/src/options.rs +++ b/compiler/rustc_session/src/options.rs @@ -2311,6 +2311,19 @@ options! { "inject the given attribute in the crate"), cross_crate_inline_threshold: InliningThreshold = (InliningThreshold::Sometimes(100), parse_inlining_threshold, [TRACKED], "threshold to allow cross crate inlining of functions"), + dead_fn_elimination: bool = (false, parse_bool, [UNTRACKED], + "eliminate functions unreachable from the entry point before codegen (experimental)"), + dead_fn_used_set: Option = (None, parse_opt_pathbuf, [UNTRACKED], + "path to a used-set file (mangled symbols a downstream binary reaches in this crate); \ + enables cross-crate dead-fn elimination for library crates (experimental)"), + dead_fn_wait_used_set: Option = (None, parse_opt_number, [UNTRACKED], + "block after metadata emission for up to N seconds waiting for the used-set file to \ + appear, so a downstream binary's frontend can produce it while this library's codegen \ + is deferred in the same process — no frontend re-run (experimental)"), + dead_fn_emit_used_set: Option = (None, parse_opt_pathbuf, [UNTRACKED], + "walk this crate's MIR (no codegen needed) and write per-dependency used-set files \ + into this directory: the extern fns it reaches, for cross-crate dead-fn elimination \ + (experimental)"), debug_info_type_line_numbers: bool = (false, parse_bool, [TRACKED], "emit type and line information for additional data types (default: no)"), debuginfo_compression: DebugInfoCompression = (DebugInfoCompression::None, parse_debuginfo_compression, [TRACKED],