Skip to main content

qcode_passes/
dce.rs

1//! Dead-instruction elimination: removing pure instructions nothing reads.
2
3use rustc_hash::FxHashSet as HashSet;
4
5use qcode::{
6    context::Context,
7    value::{BlockId, FunctionBody, InstructionId, QCodeView, ValueId},
8};
9
10use crate::{PassCtx, with_body_mut};
11
12/// This host's users of `v` as body-local ids, without allocating.
13///
14/// Body-local because that is how they are stored: the qualified form exists
15/// only to be built, and this is read once per instruction.
16fn users_of_slice<'a, 'str: 'a>(
17    host: impl QCodeView<'a, 'str>,
18    v: ValueId,
19) -> &'a [qcode::value::insn::LocalInsnId] {
20    match v.owning_function() {
21        Some(f) => host.function_ref(f).local_users_of(v),
22        None => &[],
23    }
24}
25
26/// Whether this host records any user of `v`. A shared value with no owning
27/// function has none. Mirrors [`Context::has_users`].
28///
29/// Deliberately not `users_of(v).is_empty()`: this is asked once per
30/// instruction per round, and building a vector only to discard it was the
31/// largest single cost of lifting a block.
32pub fn has_users<'a, 'str: 'a>(host: impl QCodeView<'a, 'str>, v: ValueId) -> bool {
33    match v.owning_function() {
34        Some(f) => host.function_ref(f).has_users(v),
35        None => false,
36    }
37}
38
39/// This host's users of `v`. Allocates; prefer [`has_users`] to ask whether
40/// there are any, and `users_of_slice` to look at them in a hot loop.
41pub fn host_users<'a, 'str: 'a>(host: impl QCodeView<'a, 'str>, v: ValueId) -> Vec<InstructionId> {
42    match v.owning_function() {
43        Some(f) => host.function_ref(f).users_of(v),
44        None => Vec::new(),
45    }
46}
47
48/// Returns instructions in `block_id` that are pure and have no *live* users:
49/// no users at all, or none that this same answer does not already condemn.
50///
51/// Walked in reverse so that one pass reaches the fixed point. Within a block
52/// the IR is SSA, so a definition precedes its uses; going backwards means
53/// every user of an instruction has already been judged by the time the
54/// instruction itself is, and a whole dead chain falls in one sweep. Repeating
55/// a forward scan until nothing changes reaches the same answer, but rescans
56/// the entire block once per link in the longest chain — which on a lifted
57/// guest basic block was the dominant cost of translation.
58pub fn dead_insns<'a, 'str: 'a>(
59    host: impl QCodeView<'a, 'str>,
60    block_id: BlockId,
61) -> HashSet<InstructionId> {
62    let mut dead: HashSet<InstructionId> = HashSet::default();
63    let insn_ids: Vec<InstructionId> = host.block_ref(block_id).instruction_ids().to_vec();
64    let func = block_id.func;
65    for id in insn_ids.into_iter().rev() {
66        if host.instruction(id).mnemonic().has_side_effects() {
67            continue;
68        }
69        // A user in another block is never in `dead`, so it keeps this
70        // instruction alive — as it must.
71        let live_user = users_of_slice(host, ValueId::Instruction(id))
72            .iter()
73            .any(|&user| !dead.contains(&InstructionId::new(func, user)));
74        if !live_user {
75            dead.insert(id);
76        }
77    }
78    dead
79}
80
81/// Removes dead pure instructions from `block_id` iteratively until fixed point,
82/// updating the users reverse map after each round.
83pub fn remove_dead_insns(ctx: &mut Context, block_id: BlockId) -> bool {
84    with_body_mut(ctx, block_id.func, |body, cx| {
85        remove_dead_insns_body(body, cx, block_id)
86    })
87}
88
89/// Body-local core of [`remove_dead_insns`].
90pub fn remove_dead_insns_body<'a, 'str>(
91    body: &'a mut FunctionBody<'str>,
92    cx: PassCtx<'a, 'str>,
93    block_id: BlockId,
94) -> bool {
95    let mut changed = false;
96    loop {
97        let dead = dead_insns(cx.body_view(body), block_id);
98        if dead.is_empty() {
99            break;
100        }
101
102        changed = true;
103        // One pass over the block's instruction list however many go: removing
104        // them one at a time is quadratic in the size of the block, and lifted
105        // guest basic blocks are large.
106        let dead: HashSet<_> = dead
107            .into_iter()
108            .map(|id| id.localize(block_id.func))
109            .collect();
110        body.remove_block_instructions(block_id, &dead);
111    }
112
113    let params_changed = remove_unused_no_pred_block_params(body, cx, block_id);
114    changed || params_changed
115}
116
117/// Removes block params that have no users when the block has no incoming
118/// control-flow edges. This covers function-entry params introduced for
119/// load-before-store registers that later become dead, without touching join
120/// blocks whose predecessor terminators carry positional arguments.
121pub fn remove_unused_no_pred_block_params<'a, 'str>(
122    body: &'a mut FunctionBody<'str>,
123    cx: PassCtx<'a, 'str>,
124    block_id: BlockId,
125) -> bool {
126    if cx
127        .body_view(body)
128        .block_ref(block_id)
129        .predecessors()
130        .next()
131        .is_some()
132    {
133        return false;
134    }
135
136    // A `pure_reg` function's entry params are its canonical interface, aligned
137    // index-for-index with every caller's `Call.args`. Removing one is an
138    // interprocedural change that must drop the param and the matching argument at
139    // every caller in lockstep — that is the
140    // job of the `dead_signature` module pass (via `remove_entry_param`), not of
141    // this per-function sweep. A function pass must not reach across functions, so
142    // leave pure_reg entry params for `dead_signature`; the *local* fallback below
143    // would silently drop the param and break the interface alignment.
144    let is_reg_materialized_entry = cx
145        .body_view(body)
146        .block_ref(block_id)
147        .function()
148        .is_some_and(|f| f.is_reg_materialized() && f.root().map(|b| b.id) == Some(block_id));
149    if is_reg_materialized_entry {
150        return false;
151    }
152
153    let params: Vec<_> = cx.body_view(body).block(block_id).param_ids().to_vec();
154    let mut kept = Vec::with_capacity(params.len());
155    let mut changed = false;
156    for local in params {
157        let param = qcode::value::BlockParamId::new(block_id.func, local);
158        if !has_users(cx.body_view(body), ValueId::BlockParam(param)) {
159            body.remove_block_param(param);
160            changed = true;
161        } else {
162            body.block_param_mut(param).index = kept.len();
163            kept.push(local);
164        }
165    }
166
167    if changed {
168        body.block_mut(block_id).params = kept;
169    }
170    changed
171}