Skip to main content

shape_vm/mir/
liveness.rs

1//! Liveness analysis on MIR.
2//!
3//! Determines which variables are live (will be used later) at each program point.
4//! This is the foundation for smart move/clone inference:
5//! - If a variable is NOT live after an assignment, it can be moved (zero cost).
6//! - If a variable IS live after an assignment, it must be cloned (requires Clone).
7
8use super::cfg::ControlFlowGraph;
9use super::types::*;
10use std::collections::{HashMap, HashSet};
11
12/// Result of liveness analysis for a MIR function.
13#[derive(Debug)]
14pub struct LivenessResult {
15    /// Variables live at the entry of each block.
16    pub live_in: HashMap<BasicBlockId, HashSet<SlotId>>,
17    /// Variables live at the exit of each block.
18    pub live_out: HashMap<BasicBlockId, HashSet<SlotId>>,
19}
20
21impl LivenessResult {
22    /// Check if a variable is live at a given point within a block.
23    /// Walks backwards from the block exit to the statement index.
24    pub fn is_live_after(
25        &self,
26        block: BasicBlockId,
27        stmt_idx: usize,
28        slot: SlotId,
29        mir: &MirFunction,
30    ) -> bool {
31        let bb = mir.block(block);
32
33        // Start with live_out for the block
34        let mut live = self.live_out.get(&block).cloned().unwrap_or_default();
35
36        // Walk backwards from the end of the block to stmt_idx + 1
37        // (we want liveness AFTER stmt_idx, so we stop before processing stmt_idx)
38        for i in (stmt_idx + 1..bb.statements.len()).rev() {
39            let stmt = &bb.statements[i];
40            update_liveness_for_statement(&mut live, &stmt.kind);
41        }
42
43        // Also account for the terminator's uses
44        add_terminator_uses(&mut live, &bb.terminator.kind);
45
46        live.contains(&slot)
47    }
48
49    /// Check if a variable is live at the entry of a block.
50    pub fn is_live_at_entry(&self, block: BasicBlockId, slot: SlotId) -> bool {
51        self.live_in
52            .get(&block)
53            .map_or(false, |set| set.contains(&slot))
54    }
55}
56
57/// Run liveness analysis on a MIR function.
58/// Uses the standard backward dataflow algorithm:
59///   live_out[B] = ∪ live_in[S] for all successors S of B
60///   live_in[B] = (live_out[B] - def[B]) ∪ use[B]
61pub fn compute_liveness(mir: &MirFunction, cfg: &ControlFlowGraph) -> LivenessResult {
62    let mut live_in: HashMap<BasicBlockId, HashSet<SlotId>> = HashMap::new();
63    let mut live_out: HashMap<BasicBlockId, HashSet<SlotId>> = HashMap::new();
64
65    // Initialize all blocks with empty sets
66    for block in &mir.blocks {
67        live_in.insert(block.id, HashSet::new());
68        live_out.insert(block.id, HashSet::new());
69    }
70
71    // Iterate until fixpoint
72    let mut changed = true;
73    while changed {
74        changed = false;
75
76        // Process blocks in reverse postorder (for backward analysis,
77        // processing in reverse of the forward order is efficient)
78        let rpo = cfg.reverse_postorder();
79        for &block_id in rpo.iter().rev() {
80            let block = mir.block(block_id);
81
82            // live_out[B] = ∪ live_in[S] for successors S
83            let mut new_live_out = HashSet::new();
84            for &succ in cfg.successors(block_id) {
85                if let Some(succ_in) = live_in.get(&succ) {
86                    new_live_out.extend(succ_in);
87                }
88            }
89
90            // Compute live_in from live_out
91            let mut new_live_in = new_live_out.clone();
92
93            // Process terminator (uses)
94            add_terminator_uses(&mut new_live_in, &block.terminator.kind);
95
96            // Process statements in reverse order
97            for stmt in block.statements.iter().rev() {
98                update_liveness_for_statement(&mut new_live_in, &stmt.kind);
99            }
100
101            // Check for changes
102            if new_live_in != *live_in.get(&block_id).unwrap_or(&HashSet::new()) {
103                changed = true;
104                live_in.insert(block_id, new_live_in);
105            }
106            if new_live_out != *live_out.get(&block_id).unwrap_or(&HashSet::new()) {
107                changed = true;
108                live_out.insert(block_id, new_live_out);
109            }
110        }
111    }
112
113    LivenessResult { live_in, live_out }
114}
115
116/// Update liveness for a single statement (backward: remove defs, add uses).
117fn update_liveness_for_statement(live: &mut HashSet<SlotId>, kind: &StatementKind) {
118    match kind {
119        StatementKind::Assign(place, rvalue) => {
120            // Definition: remove the assigned-to slot
121            if let Place::Local(slot) = place {
122                live.remove(slot);
123            }
124            // Uses: add all used slots
125            add_rvalue_uses(live, rvalue);
126        }
127        StatementKind::Drop(place) => {
128            // Drop uses the place
129            live.insert(place.root_local());
130        }
131        StatementKind::TaskBoundary(operands, _kind) => {
132            for operand in operands {
133                add_operand_uses(live, operand);
134            }
135        }
136        StatementKind::ClosureCapture { operands, .. } => {
137            for operand in operands {
138                add_operand_uses(live, operand);
139            }
140        }
141        StatementKind::ArrayStore { operands, .. } => {
142            for operand in operands {
143                add_operand_uses(live, operand);
144            }
145        }
146        StatementKind::ObjectStore { operands, .. } => {
147            for operand in operands {
148                add_operand_uses(live, operand);
149            }
150        }
151        StatementKind::EnumStore { operands, .. } => {
152            for operand in operands {
153                add_operand_uses(live, operand);
154            }
155        }
156        StatementKind::Nop => {}
157    }
158}
159
160/// Add uses from an rvalue to the live set.
161fn add_rvalue_uses(live: &mut HashSet<SlotId>, rvalue: &Rvalue) {
162    match rvalue {
163        Rvalue::Use(op) | Rvalue::Clone(op) | Rvalue::UnaryOp(_, op) => {
164            add_operand_uses(live, op);
165        }
166        Rvalue::Borrow(_, place) => {
167            live.insert(place.root_local());
168        }
169        Rvalue::BinaryOp(_, lhs, rhs) => {
170            add_operand_uses(live, lhs);
171            add_operand_uses(live, rhs);
172        }
173        Rvalue::Aggregate(ops) => {
174            for op in ops {
175                add_operand_uses(live, op);
176            }
177        }
178    }
179}
180
181/// Add uses from an operand to the live set.
182fn add_operand_uses(live: &mut HashSet<SlotId>, op: &Operand) {
183    match op {
184        Operand::Copy(place) | Operand::Move(place) | Operand::MoveExplicit(place) => {
185            live.insert(place.root_local());
186        }
187        Operand::Constant(_) => {}
188    }
189}
190
191/// Add uses from a terminator to the live set.
192fn add_terminator_uses(live: &mut HashSet<SlotId>, kind: &TerminatorKind) {
193    match kind {
194        TerminatorKind::SwitchBool { operand, .. } => {
195            add_operand_uses(live, operand);
196        }
197        TerminatorKind::Call { func, args, .. } => {
198            add_operand_uses(live, func);
199            for arg in args {
200                add_operand_uses(live, arg);
201            }
202        }
203        TerminatorKind::Goto(_) | TerminatorKind::Return | TerminatorKind::Unreachable => {}
204    }
205}
206
207#[cfg(test)]
208mod tests {
209    use super::*;
210
211    fn span() -> shape_ast::ast::Span {
212        shape_ast::ast::Span { start: 0, end: 1 }
213    }
214
215    fn make_stmt(kind: StatementKind, point: u32) -> MirStatement {
216        MirStatement {
217            kind,
218            span: span(),
219            point: Point(point),
220        }
221    }
222
223    fn make_terminator(kind: TerminatorKind) -> Terminator {
224        Terminator { kind, span: span() }
225    }
226
227    #[test]
228    fn test_simple_liveness() {
229        // bb0: x = 1; y = x; return
230        let mir = MirFunction {
231            name: "test".to_string(),
232            blocks: vec![BasicBlock {
233                id: BasicBlockId(0),
234                statements: vec![
235                    make_stmt(
236                        StatementKind::Assign(
237                            Place::Local(SlotId(0)),
238                            Rvalue::Use(Operand::Constant(MirConstant::Int(1))),
239                        ),
240                        0,
241                    ),
242                    make_stmt(
243                        StatementKind::Assign(
244                            Place::Local(SlotId(1)),
245                            Rvalue::Use(Operand::Copy(Place::Local(SlotId(0)))),
246                        ),
247                        1,
248                    ),
249                ],
250                terminator: make_terminator(TerminatorKind::Return),
251            }],
252            num_locals: 2,
253            param_slots: vec![],
254            param_reference_kinds: vec![],
255            local_types: vec![LocalTypeInfo::Copy, LocalTypeInfo::Copy],
256            span: span(),
257        };
258
259        let cfg = ControlFlowGraph::build(&mir);
260        let liveness = compute_liveness(&mir, &cfg);
261
262        // x (slot 0) should be live after stmt 0 (used in stmt 1)
263        assert!(liveness.is_live_after(BasicBlockId(0), 0, SlotId(0), &mir));
264        // x (slot 0) should NOT be live after stmt 1 (never used again)
265        assert!(!liveness.is_live_after(BasicBlockId(0), 1, SlotId(0), &mir));
266    }
267
268    #[test]
269    fn test_branch_liveness() {
270        // bb0: x = 1; if cond goto bb1 else bb2
271        // bb1: y = x; goto bb3
272        // bb2: goto bb3
273        // bb3: return
274        let mir = MirFunction {
275            name: "test".to_string(),
276            blocks: vec![
277                BasicBlock {
278                    id: BasicBlockId(0),
279                    statements: vec![make_stmt(
280                        StatementKind::Assign(
281                            Place::Local(SlotId(0)),
282                            Rvalue::Use(Operand::Constant(MirConstant::Int(1))),
283                        ),
284                        0,
285                    )],
286                    terminator: make_terminator(TerminatorKind::SwitchBool {
287                        operand: Operand::Copy(Place::Local(SlotId(2))),
288                        true_bb: BasicBlockId(1),
289                        false_bb: BasicBlockId(2),
290                    }),
291                },
292                BasicBlock {
293                    id: BasicBlockId(1),
294                    statements: vec![make_stmt(
295                        StatementKind::Assign(
296                            Place::Local(SlotId(1)),
297                            Rvalue::Use(Operand::Copy(Place::Local(SlotId(0)))),
298                        ),
299                        1,
300                    )],
301                    terminator: make_terminator(TerminatorKind::Goto(BasicBlockId(3))),
302                },
303                BasicBlock {
304                    id: BasicBlockId(2),
305                    statements: vec![],
306                    terminator: make_terminator(TerminatorKind::Goto(BasicBlockId(3))),
307                },
308                BasicBlock {
309                    id: BasicBlockId(3),
310                    statements: vec![],
311                    terminator: make_terminator(TerminatorKind::Return),
312                },
313            ],
314            num_locals: 3,
315            param_slots: vec![],
316            param_reference_kinds: vec![],
317            local_types: vec![
318                LocalTypeInfo::Copy,
319                LocalTypeInfo::Copy,
320                LocalTypeInfo::Copy,
321            ],
322            span: span(),
323        };
324
325        let cfg = ControlFlowGraph::build(&mir);
326        let liveness = compute_liveness(&mir, &cfg);
327
328        // x (slot 0) should be live at entry of bb0 (used in bb1 via some path)
329        // Actually, x is defined in bb0, so it's live at exit of bb0
330        assert!(
331            liveness
332                .live_out
333                .get(&BasicBlockId(0))
334                .map_or(false, |s| s.contains(&SlotId(0)))
335        );
336    }
337}