Skip to main content

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}