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, Clone)]
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        Rvalue::EnumTest { operand, .. }
179        | Rvalue::EnumPayload { operand, .. }
180        | Rvalue::TypePatternTest { operand, .. }
181        | Rvalue::EnumDiscriminantTest { operand, .. } => {
182            add_operand_uses(live, operand);
183        }
184    }
185}
186
187/// Add uses from an operand to the live set.
188fn add_operand_uses(live: &mut HashSet<SlotId>, op: &Operand) {
189    match op {
190        Operand::Copy(place) | Operand::Move(place) | Operand::MoveExplicit(place) => {
191            live.insert(place.root_local());
192        }
193        Operand::Constant(_) => {}
194    }
195}
196
197/// Add uses from a terminator to the live set.
198fn add_terminator_uses(live: &mut HashSet<SlotId>, kind: &TerminatorKind) {
199    match kind {
200        TerminatorKind::SwitchBool { operand, .. } => {
201            add_operand_uses(live, operand);
202        }
203        TerminatorKind::Call { func, args, .. } => {
204            add_operand_uses(live, func);
205            for arg in args {
206                add_operand_uses(live, arg);
207            }
208        }
209        TerminatorKind::Goto(_) | TerminatorKind::Return | TerminatorKind::Unreachable => {}
210    }
211}
212
213#[cfg(test)]
214mod tests {
215    use super::*;
216
217    fn span() -> shape_ast::ast::Span {
218        shape_ast::ast::Span { start: 0, end: 1 }
219    }
220
221    fn make_stmt(kind: StatementKind, point: u32) -> MirStatement {
222        MirStatement {
223            kind,
224            span: span(),
225            point: Point(point),
226        }
227    }
228
229    fn make_terminator(kind: TerminatorKind) -> Terminator {
230        Terminator { kind, span: span() }
231    }
232
233    #[test]
234    fn test_simple_liveness() {
235        // bb0: x = 1; y = x; return
236        let mir = MirFunction {
237            name: "test".to_string(),
238            blocks: vec![BasicBlock {
239                id: BasicBlockId(0),
240                statements: vec![
241                    make_stmt(
242                        StatementKind::Assign(
243                            Place::Local(SlotId(0)),
244                            Rvalue::Use(Operand::Constant(MirConstant::Int(1))),
245                        ),
246                        0,
247                    ),
248                    make_stmt(
249                        StatementKind::Assign(
250                            Place::Local(SlotId(1)),
251                            Rvalue::Use(Operand::Copy(Place::Local(SlotId(0)))),
252                        ),
253                        1,
254                    ),
255                ],
256                terminator: make_terminator(TerminatorKind::Return),
257            }],
258            num_locals: 2,
259            param_slots: vec![],
260            param_reference_kinds: vec![],
261            local_types: vec![LocalTypeInfo::Copy, LocalTypeInfo::Copy],
262            span: span(),
263            field_name_table: std::collections::HashMap::new(),
264            local_struct_type_names: std::collections::HashMap::new(),
265            local_typed_array_element_types: std::collections::HashMap::new(),
266            local_declared_scalar_types: std::collections::HashMap::new(),
267        };
268
269        let cfg = ControlFlowGraph::build(&mir);
270        let liveness = compute_liveness(&mir, &cfg);
271
272        // x (slot 0) should be live after stmt 0 (used in stmt 1)
273        assert!(liveness.is_live_after(BasicBlockId(0), 0, SlotId(0), &mir));
274        // x (slot 0) should NOT be live after stmt 1 (never used again)
275        assert!(!liveness.is_live_after(BasicBlockId(0), 1, SlotId(0), &mir));
276    }
277
278    #[test]
279    fn test_branch_liveness() {
280        // bb0: x = 1; if cond goto bb1 else bb2
281        // bb1: y = x; goto bb3
282        // bb2: goto bb3
283        // bb3: return
284        let mir = MirFunction {
285            name: "test".to_string(),
286            blocks: vec![
287                BasicBlock {
288                    id: BasicBlockId(0),
289                    statements: vec![make_stmt(
290                        StatementKind::Assign(
291                            Place::Local(SlotId(0)),
292                            Rvalue::Use(Operand::Constant(MirConstant::Int(1))),
293                        ),
294                        0,
295                    )],
296                    terminator: make_terminator(TerminatorKind::SwitchBool {
297                        operand: Operand::Copy(Place::Local(SlotId(2))),
298                        true_bb: BasicBlockId(1),
299                        false_bb: BasicBlockId(2),
300                    }),
301                },
302                BasicBlock {
303                    id: BasicBlockId(1),
304                    statements: vec![make_stmt(
305                        StatementKind::Assign(
306                            Place::Local(SlotId(1)),
307                            Rvalue::Use(Operand::Copy(Place::Local(SlotId(0)))),
308                        ),
309                        1,
310                    )],
311                    terminator: make_terminator(TerminatorKind::Goto(BasicBlockId(3))),
312                },
313                BasicBlock {
314                    id: BasicBlockId(2),
315                    statements: vec![],
316                    terminator: make_terminator(TerminatorKind::Goto(BasicBlockId(3))),
317                },
318                BasicBlock {
319                    id: BasicBlockId(3),
320                    statements: vec![],
321                    terminator: make_terminator(TerminatorKind::Return),
322                },
323            ],
324            num_locals: 3,
325            param_slots: vec![],
326            param_reference_kinds: vec![],
327            local_types: vec![
328                LocalTypeInfo::Copy,
329                LocalTypeInfo::Copy,
330                LocalTypeInfo::Copy,
331            ],
332            span: span(),
333            field_name_table: std::collections::HashMap::new(),
334            local_struct_type_names: std::collections::HashMap::new(),
335            local_typed_array_element_types: std::collections::HashMap::new(),
336            local_declared_scalar_types: std::collections::HashMap::new(),
337        };
338
339        let cfg = ControlFlowGraph::build(&mir);
340        let liveness = compute_liveness(&mir, &cfg);
341
342        // x (slot 0) should be live at entry of bb0 (used in bb1 via some path)
343        // Actually, x is defined in bb0, so it's live at exit of bb0
344        assert!(
345            liveness
346                .live_out
347                .get(&BasicBlockId(0))
348                .map_or(false, |s| s.contains(&SlotId(0)))
349        );
350    }
351}