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}