analyssa/analysis/dataflow/liveness.rs
1//! Live variable analysis using the dataflow framework.
2//!
3//! A variable is *live* at a program point if there exists a path from that
4//! point to a use of the variable without passing through a definition of the
5//! variable. In SSA form, since each variable is defined exactly once, this
6//! simplifies to: a variable is live if it will be used on some future path.
7//!
8//! # Uses
9//!
10//! - **Dead code elimination**: If a definition's result is never live, it's dead
11//! - **Register allocation**: Variables live simultaneously need different registers
12//! - **Debugging**: Determine which variables can be inspected at a breakpoint
13//!
14//! # Algorithm
15//!
16//! This is a backward data flow analysis using the generic framework:
17//!
18//! ## Dataflow Equations
19//!
20//! | Set | Definition |
21//! |-----|------------|
22//! | `USE[B]` | Variables used in B before any definition |
23//! | `DEF[B]` | Variables defined in B (phi nodes + instruction defs) |
24//! | `OUT[B]` | `∪ { IN(s) for s in succ(B) }` |
25//! | `IN[B]` | `USE[B] ∪ (OUT[B] \\ DEF[B])` |
26//!
27//! ## PHI Operand Handling
28//!
29//! PHI operand uses are placed at the END of the predecessor block (ECMA-335 /
30//! standard SSA semantics: phi copies execute at the predecessor's outgoing edge).
31//! This ensures backward dataflow correctly propagates liveness from the predecessor
32//! back through all intermediate blocks to the definition.
33//!
34//! ## Lattice
35//!
36//! The `LivenessResult` lattice uses `MeetSemiLattice` with:
37//! - **Meet = union**: A variable is live if it's live on ANY successor path
38//! (may analysis — over-approximate liveness)
39//! - **Boundary**: At function exit, no variables are live
40//! - **Initial**: All blocks start with empty live sets
41
42use crate::{
43 analysis::dataflow::{
44 framework::{DataFlowAnalysis, Direction},
45 lattice::MeetSemiLattice,
46 },
47 bitset::BitSet,
48 ir::{block::SsaBlock, function::SsaFunction, variable::SsaVarId},
49 target::Target,
50};
51
52/// Live variable analysis.
53///
54/// Computes which variables are live at each program point.
55/// A variable is live if its value may be used on some path from
56/// that point forward.
57///
58/// # Example
59///
60/// ```rust
61/// use analyssa::{
62/// analysis::{
63/// dataflow::{DataFlowSolver, LiveVariables},
64/// SsaCfg,
65/// },
66/// ir::SsaVarId,
67/// testing,
68/// };
69///
70/// // Block 1 defines the loop counter phi; block 2 returns it.
71/// let ssa = testing::loop_counter_fixture();
72/// let graph = SsaCfg::from_ssa(&ssa);
73///
74/// let analysis = LiveVariables::new(&ssa);
75/// let solver = DataFlowSolver::new(analysis);
76/// let results = solver.solve(&ssa, &graph);
77///
78/// // The counter is live on entry to block 2, because block 2 returns it.
79/// let counter = SsaVarId::from_index(1);
80/// let live_in = results.in_state(2).unwrap();
81/// assert!(live_in.variables().any(|var| var == counter));
82///
83/// // Nothing is live after the return terminator.
84/// assert_eq!(results.out_state(2).unwrap().variables().count(), 0);
85/// ```
86pub struct LiveVariables {
87 /// Number of variables in the function.
88 num_vars: usize,
89 /// USE sets for each block (variables used before definition).
90 use_sets: Vec<BitSet>,
91 /// DEF sets for each block (variables defined).
92 def_sets: Vec<BitSet>,
93 /// Values consumed by a successor's phi on each block's outgoing edges.
94 ///
95 /// `phi_out[b]` holds every variable that some successor of `b` reads as a
96 /// phi operand on the edge from `b` — *including* ones `b` itself defines.
97 /// See [`LiveVariables::live_out`] for why this cannot be recovered from the
98 /// solver's `out_state`.
99 phi_out: Vec<BitSet>,
100}
101
102impl LiveVariables {
103 /// Creates a new live variables analysis for the given SSA function.
104 #[must_use]
105 pub fn new<T: Target>(ssa: &SsaFunction<T>) -> Self {
106 let num_vars = ssa.variable_count();
107 let num_blocks = ssa.block_count();
108
109 let mut use_sets = Vec::with_capacity(num_blocks);
110 let mut def_sets = Vec::with_capacity(num_blocks);
111
112 // Phase 1: Initialize use/def sets without PHI operands
113 for block in ssa.blocks() {
114 let mut uses = BitSet::new(num_vars);
115 let mut defs = BitSet::new(num_vars);
116
117 // Process phi nodes: they define variables
118 for phi in block.phi_nodes() {
119 if let Some(def_idx) = ssa.var_index(phi.result()) {
120 defs.insert(def_idx);
121 }
122 }
123
124 // Process instructions in forward order
125 for instr in block.instructions() {
126 // Uses first (before def, since this is the "USE before DEF" set)
127 instr.for_each_use(|use_var| {
128 if let Some(var_idx) = ssa.var_index(use_var)
129 && !defs.contains(var_idx)
130 {
131 uses.insert(var_idx);
132 }
133 });
134
135 // Then definition
136 for def in instr.defs() {
137 if let Some(def_idx) = ssa.var_index(def) {
138 defs.insert(def_idx);
139 }
140 }
141 }
142
143 use_sets.push(uses);
144 def_sets.push(defs);
145 }
146
147 // Phase 2: Add PHI operand uses to their PREDECESSOR blocks.
148 // A PHI operand `v<-B_pred` means variable v is used at the END
149 // of B_pred (ECMA-335 / SSA semantics: phi copies happen at the
150 // predecessor's outgoing edge). Placing the use in the predecessor
151 // ensures backward dataflow propagates liveness from the predecessor
152 // back through all intermediate blocks to the definition.
153 let mut phi_out = vec![BitSet::new(num_vars); num_blocks];
154 for block in ssa.blocks() {
155 for phi in block.phi_nodes() {
156 for op in phi.operands() {
157 let pred = op.predecessor();
158 if let Some(var_idx) = ssa.var_index(op.value()) {
159 // Recorded unconditionally, unlike the USE insert below:
160 // the value crosses the edge whether or not `pred`
161 // defines it, and a value `pred` defines is precisely the
162 // case the USE set must exclude but live-out must not.
163 if let Some(slot) = phi_out.get_mut(pred) {
164 slot.insert_checked(var_idx);
165 }
166 let already_def = def_sets.get(pred).is_some_and(|s| s.contains(var_idx));
167 if !already_def && let Some(slot) = use_sets.get_mut(pred) {
168 slot.insert(var_idx);
169 }
170 }
171 }
172 }
173 }
174
175 Self {
176 num_vars,
177 use_sets,
178 def_sets,
179 phi_out,
180 }
181 }
182
183 /// Returns the values live on `block`'s outgoing edges.
184 ///
185 /// **Use this rather than the solver's raw `out_state`.** `out_state` is the
186 /// meet of the successors' IN sets, and a value consumed *only* by a
187 /// successor's phi never appears there: phi-operand uses are relocated into
188 /// the predecessor's USE set, which feeds `IN[pred]`, not `OUT[pred]`. The
189 /// value is genuinely live across the edge — the phi copy reads it — so raw
190 /// `out_state` under-approximates liveness, which is the unsafe direction
191 /// for a may-analysis and would let a register allocator reuse a live
192 /// register.
193 ///
194 /// This computes `OUT[B] = out_state(B) ∪ PhiUses(B)`, where `PhiUses(B)` is
195 /// every value some successor reads as a phi operand on an edge from `B`.
196 #[must_use]
197 pub fn live_out(&self, block: usize, out_state: &LivenessResult) -> LivenessResult {
198 let mut live = out_state.live.clone();
199 if let Some(phi_uses) = self.phi_out.get(block) {
200 live.union_with(phi_uses);
201 }
202 LivenessResult { live }
203 }
204
205 /// Returns the values a successor's phi reads on `block`'s outgoing edges.
206 #[must_use]
207 pub fn phi_out_set(&self, block: usize) -> Option<&BitSet> {
208 self.phi_out.get(block)
209 }
210
211 /// Returns the number of variables being tracked.
212 #[must_use]
213 pub const fn num_variables(&self) -> usize {
214 self.num_vars
215 }
216
217 /// Returns the USE set for a block.
218 #[must_use]
219 pub fn use_set(&self, block: usize) -> Option<&BitSet> {
220 self.use_sets.get(block)
221 }
222
223 /// Returns the DEF set for a block.
224 #[must_use]
225 pub fn def_set(&self, block: usize) -> Option<&BitSet> {
226 self.def_sets.get(block)
227 }
228}
229
230impl<T: Target> DataFlowAnalysis<T> for LiveVariables {
231 type Lattice = LivenessResult;
232 const DIRECTION: Direction = Direction::Backward;
233
234 fn boundary(&self, _ssa: &SsaFunction<T>) -> Self::Lattice {
235 // At function exit, no variables are live
236 // (unless we're tracking return values, which we could add)
237 LivenessResult {
238 live: BitSet::new(self.num_vars),
239 }
240 }
241
242 fn initial(&self, _ssa: &SsaFunction<T>) -> Self::Lattice {
243 // Initially, no variables are live
244 LivenessResult {
245 live: BitSet::new(self.num_vars),
246 }
247 }
248
249 fn transfer(
250 &self,
251 block_id: usize,
252 _block: &SsaBlock<T>,
253 output: &Self::Lattice,
254 _ssa: &SsaFunction<T>,
255 ) -> Self::Lattice {
256 // For backward analysis: IN = USE ∪ (OUT - DEF)
257 let mut result = output.live.clone();
258
259 // Remove definitions (OUT - DEF)
260 if let Some(d) = self.def_sets.get(block_id) {
261 result.difference_with(d);
262 }
263
264 // Add uses (USE ∪ ...)
265 if let Some(u) = self.use_sets.get(block_id) {
266 result.union_with(u);
267 }
268
269 LivenessResult { live: result }
270 }
271}
272
273/// Result of live variable analysis for a single program point.
274#[derive(Debug, Clone, PartialEq)]
275pub struct LivenessResult {
276 /// Bit vector of live variables (indexed by `SsaVarId`).
277 live: BitSet,
278}
279
280impl LivenessResult {
281 /// Creates a new empty result.
282 #[must_use]
283 pub fn new(num_vars: usize) -> Self {
284 Self {
285 live: BitSet::new(num_vars),
286 }
287 }
288
289 /// Returns `true` if the given variable is live at this point.
290 #[must_use]
291 pub fn is_live(&self, var: SsaVarId) -> bool {
292 let idx = var.index();
293 idx < self.live.len() && self.live.contains(idx)
294 }
295
296 /// Returns an iterator over all live variables.
297 pub fn variables(&self) -> impl Iterator<Item = SsaVarId> + '_ {
298 self.live.iter().map(SsaVarId::from_index)
299 }
300
301 /// Returns the number of live variables.
302 #[must_use]
303 pub fn count(&self) -> usize {
304 self.live.count()
305 }
306
307 /// Returns `true` if no variables are live.
308 #[must_use]
309 pub fn is_empty(&self) -> bool {
310 self.live.is_empty()
311 }
312
313 /// Marks a variable as live.
314 pub fn add(&mut self, var: SsaVarId) {
315 let idx = var.index();
316 if idx < self.live.len() {
317 self.live.insert(idx);
318 }
319 }
320
321 /// Marks a variable as not live.
322 pub fn remove(&mut self, var: SsaVarId) {
323 let idx = var.index();
324 if idx < self.live.len() {
325 self.live.remove(idx);
326 }
327 }
328
329 /// Returns the underlying bit set.
330 #[must_use]
331 pub const fn as_bitset(&self) -> &BitSet {
332 &self.live
333 }
334}
335
336impl MeetSemiLattice for LivenessResult {
337 /// Meet is union (a variable is live if it's live on ANY successor path).
338 fn meet(&self, other: &Self) -> Self {
339 let mut result = self.live.clone();
340 result.union_with(&other.live);
341 Self { live: result }
342 }
343
344 /// Union in place — no temporary set per predecessor.
345 fn meet_into(&mut self, other: &Self) {
346 self.live.union_with(&other.live);
347 }
348
349 fn is_bottom(&self) -> bool {
350 // Bottom is when all variables are live (full set).
351 self.live.is_full()
352 }
353}
354
355#[cfg(test)]
356mod tests {
357 use super::*;
358
359 /// A value consumed *only* by a successor's phi is live across the edge, but
360 /// never appears in the solver's `out_state`: phi-operand uses are relocated
361 /// into the predecessor's USE set, which feeds `IN[pred]`, not `OUT[pred]`.
362 /// Under-approximating live-out is the unsafe direction — a register
363 /// allocator reading it would reuse a register that is still live.
364 #[test]
365 fn live_out_includes_values_a_successor_phi_consumes() {
366 use crate::{
367 ir::{
368 SsaBlock, SsaInstruction,
369 ops::SsaOp,
370 phi::{PhiNode, PhiOperand},
371 value::ConstValue,
372 variable::{DefSite, VariableOrigin},
373 },
374 testing::{MockTarget, MockType},
375 };
376
377 // B0 defines `v` and jumps to B1, whose phi is `v`'s only consumer.
378 let mut ssa: SsaFunction<MockTarget> = SsaFunction::new(0, 2);
379 let value = ssa.create_variable(
380 VariableOrigin::Local(0),
381 0,
382 DefSite::instruction(0, 0),
383 MockType::I32,
384 );
385 let merged =
386 ssa.create_variable(VariableOrigin::Local(1), 0, DefSite::phi(1), MockType::I32);
387
388 let mut b0 = SsaBlock::new(0);
389 b0.add_instruction(SsaInstruction::synthetic(SsaOp::Const {
390 dest: value,
391 value: ConstValue::I32(1),
392 }));
393 b0.add_instruction(SsaInstruction::synthetic(SsaOp::Jump { target: 1 }));
394 ssa.add_block(b0);
395
396 let mut b1 = SsaBlock::new(1);
397 let mut phi = PhiNode::new(merged, VariableOrigin::Local(1));
398 phi.add_operand(PhiOperand::new(value, 0));
399 b1.add_phi(phi);
400 b1.add_instruction(SsaInstruction::synthetic(SsaOp::Return {
401 value: Some(merged),
402 }));
403 ssa.add_block(b1);
404 ssa.recompute_uses();
405
406 let analysis = LiveVariables::new(&ssa);
407 let var_idx = ssa.var_index(value).expect("value is registered");
408
409 assert!(
410 analysis
411 .phi_out_set(0)
412 .is_some_and(|set| set.contains(var_idx)),
413 "the phi operand crosses the B0->B1 edge"
414 );
415
416 // The raw successor-meet does not see it: B1's IN excludes `value`
417 // because the phi *defines* rather than uses it there.
418 let bare = LivenessResult {
419 live: BitSet::new(analysis.num_variables()),
420 };
421 assert!(
422 !bare.live.contains(var_idx),
423 "precondition: the bare out_state does not contain it"
424 );
425
426 // `live_out` adds it back.
427 let live_out = analysis.live_out(0, &bare);
428 assert!(
429 live_out.is_live(value),
430 "live_out must report a value the successor's phi consumes"
431 );
432 }
433
434 #[test]
435 fn test_liveness_result() {
436 let mut result = LivenessResult::new(10);
437 assert!(result.is_empty());
438
439 result.add(SsaVarId::from_index(0));
440 result.add(SsaVarId::from_index(5));
441
442 assert!(!result.is_empty());
443 assert_eq!(result.count(), 2);
444 assert!(result.is_live(SsaVarId::from_index(0)));
445 assert!(result.is_live(SsaVarId::from_index(5)));
446 assert!(!result.is_live(SsaVarId::from_index(1)));
447
448 result.remove(SsaVarId::from_index(0));
449 assert!(!result.is_live(SsaVarId::from_index(0)));
450 assert_eq!(result.count(), 1);
451 }
452
453 #[test]
454 fn test_liveness_meet() {
455 let mut a = LivenessResult::new(10);
456 let mut b = LivenessResult::new(10);
457
458 a.add(SsaVarId::from_index(0));
459 a.add(SsaVarId::from_index(1));
460 b.add(SsaVarId::from_index(1));
461 b.add(SsaVarId::from_index(2));
462
463 let result = a.meet(&b);
464 assert!(result.is_live(SsaVarId::from_index(0)));
465 assert!(result.is_live(SsaVarId::from_index(1)));
466 assert!(result.is_live(SsaVarId::from_index(2)));
467 assert_eq!(result.count(), 3);
468 }
469
470 #[test]
471 fn test_liveness_iterator() {
472 let mut result = LivenessResult::new(100);
473 result.add(SsaVarId::from_index(5));
474 result.add(SsaVarId::from_index(42));
475 result.add(SsaVarId::from_index(99));
476
477 let vars: Vec<_> = result.variables().collect();
478 assert_eq!(vars.len(), 3);
479 assert!(vars.contains(&SsaVarId::from_index(5)));
480 assert!(vars.contains(&SsaVarId::from_index(42)));
481 assert!(vars.contains(&SsaVarId::from_index(99)));
482 }
483}