Skip to main content

solar_codegen/analysis/
liveness.rs

1//! Liveness analysis for MIR.
2//!
3//! Computes which values are live at each program point using backward dataflow analysis.
4//! A value is live at a point if there exists a path from that point to a use of the value
5//! that doesn't pass through a definition of that value.
6//!
7//! The analysis uses dense bitsets indexed by `ValueId` for efficiency.
8//!
9//! Phi nodes are ordinary instructions (`InstKind::Phi`): their incoming operands are
10//! treated as uses at the phi instruction in the merge block, and the phi result is
11//! defined like any other instruction result.
12
13use crate::mir::{BlockId, Function, InstId, Terminator, Value, ValueId};
14use smallvec::SmallVec;
15use solar_data_structures::{bit_set::GrowableBitSet, map::FxHashMap};
16use std::collections::VecDeque;
17
18/// A dense bitset for tracking live values.
19pub type LiveSet = GrowableBitSet<ValueId>;
20
21/// Per-instruction liveness information.
22#[derive(Clone, Debug)]
23pub struct LivenessInfo {
24    /// Values that are live before this instruction.
25    pub live_before: LiveSet,
26    /// Values that are live after this instruction.
27    pub live_after: LiveSet,
28}
29
30/// Per-block liveness results.
31#[derive(Clone, Debug)]
32struct BlockLiveness {
33    /// Values live at block entry (live_in).
34    live_in: LiveSet,
35    /// Values live at block exit (live_out).
36    live_out: LiveSet,
37}
38
39/// Liveness analysis results for a function.
40#[derive(Debug)]
41pub struct Liveness {
42    /// Per-block liveness information (indexed by block index).
43    block_liveness: Vec<BlockLiveness>,
44    /// The last use location of each value within each block: (block, instruction index).
45    /// The key is (ValueId, BlockId), and value is the instruction index (None = terminator).
46    /// This tracks the last use of a value *within* each block where it's used.
47    last_use_in_block: FxHashMap<(ValueId, BlockId), Option<usize>>,
48    /// Precomputed map from InstId to the ValueId it defines.
49    /// Built once during compute() to avoid O(n) scans.
50    pub(crate) inst_to_value: FxHashMap<InstId, ValueId>,
51    /// Number of values in the function.
52    #[allow(dead_code)]
53    num_values: usize,
54}
55
56impl Liveness {
57    /// Computes liveness for a function.
58    #[must_use]
59    pub fn compute(func: &Function) -> Self {
60        let num_values = func.values.len();
61        let num_blocks = func.blocks.len();
62
63        // Precompute InstId → ValueId mapping.
64        // This replaces the O(n) linear scans that were previously done per-instruction.
65        let mut inst_to_value: FxHashMap<InstId, ValueId> = FxHashMap::default();
66        for (val_id, val) in func.values.iter_enumerated() {
67            if let Value::Inst(inst_id) = val {
68                inst_to_value.insert(*inst_id, val_id);
69            }
70        }
71
72        // Initialize per-block liveness
73        let mut block_liveness: Vec<BlockLiveness> = (0..num_blocks)
74            .map(|_| BlockLiveness {
75                live_in: LiveSet::with_capacity(num_values),
76                live_out: LiveSet::with_capacity(num_values),
77            })
78            .collect();
79
80        // Compute local def/use sets for each block
81        let mut block_defs: Vec<LiveSet> =
82            (0..num_blocks).map(|_| LiveSet::with_capacity(num_values)).collect();
83        let mut block_uses: Vec<LiveSet> =
84            (0..num_blocks).map(|_| LiveSet::with_capacity(num_values)).collect();
85
86        let mut operand_buf = SmallVec::<[ValueId; 8]>::new();
87
88        for (block_id, block) in func.blocks.iter_enumerated() {
89            let bidx = block_id.index();
90            // Process instructions in forward order to compute upward-exposed uses and defs
91            for &inst_id in &block.instructions {
92                let inst = func.instruction(inst_id);
93
94                // Collect uses (upward-exposed uses - used before defined in this block)
95                operand_buf.clear();
96                inst.kind.collect_operands(&mut operand_buf);
97                for &operand in &operand_buf {
98                    if !block_defs[bidx].contains(operand) {
99                        block_uses[bidx].insert(operand);
100                    }
101                }
102
103                // Record definition using precomputed map (O(1) instead of O(n)).
104                if let Some(&val_id) = inst_to_value.get(&inst_id) {
105                    block_defs[bidx].insert(val_id);
106                }
107            }
108
109            // Process terminator uses
110            if let Some(term) = &block.terminator {
111                operand_buf.clear();
112                collect_terminator_uses(term, &mut operand_buf);
113                for &operand in &operand_buf {
114                    if !block_defs[bidx].contains(operand) {
115                        block_uses[bidx].insert(operand);
116                    }
117                }
118            }
119        }
120
121        // Worklist algorithm for computing live_in/live_out.
122        //
123        // live_out(B) = union over S in succ(B) of live_in(S)
124        // live_in(B) = block_uses(B) | (live_out(B) - block_defs(B))
125        let mut worklist: VecDeque<BlockId> = func.blocks.indices().collect();
126
127        while let Some(block_id) = worklist.pop_front() {
128            let bidx = block_id.index();
129            let block = &func.blocks[block_id];
130
131            let mut new_live_out = LiveSet::with_capacity(num_values);
132            let successors =
133                block.terminator.as_ref().map(Terminator::successors).unwrap_or_default();
134            for succ in successors {
135                new_live_out.union(&block_liveness[succ.index()].live_in);
136            }
137
138            // live_in = use ∪ (live_out - def)
139            let mut new_live_in = block_uses[bidx].clone();
140            for val in new_live_out.iter() {
141                if !block_defs[bidx].contains(val) {
142                    new_live_in.insert(val);
143                }
144            }
145
146            if new_live_out != block_liveness[bidx].live_out
147                || new_live_in != block_liveness[bidx].live_in
148            {
149                block_liveness[bidx].live_out = new_live_out;
150                block_liveness[bidx].live_in = new_live_in;
151
152                // Add predecessors to worklist
153                for &pred in &block.predecessors {
154                    worklist.push_back(pred);
155                }
156            }
157        }
158
159        // Compute last use locations per block
160        // For each value, track the last instruction index where it's used within each block.
161        let mut last_use_in_block: FxHashMap<(ValueId, BlockId), Option<usize>> =
162            FxHashMap::default();
163        for (block_id, block) in func.blocks.iter_enumerated() {
164            // Check terminator uses - these are the last use in this block
165            if let Some(term) = &block.terminator {
166                operand_buf.clear();
167                collect_terminator_uses(term, &mut operand_buf);
168                for &operand in &operand_buf {
169                    // Terminator is represented by None for inst_idx
170                    last_use_in_block.entry((operand, block_id)).or_insert(None);
171                }
172            }
173
174            // Check instruction uses in reverse order
175            // The first occurrence in reverse order is the last use in forward order
176            for (inst_idx, &inst_id) in block.instructions.iter().enumerate().rev() {
177                let inst = func.instruction(inst_id);
178                operand_buf.clear();
179                inst.kind.collect_operands(&mut operand_buf);
180                for &operand in &operand_buf {
181                    last_use_in_block.entry((operand, block_id)).or_insert(Some(inst_idx));
182                }
183            }
184        }
185
186        Self { block_liveness, last_use_in_block, inst_to_value, num_values }
187    }
188
189    /// Returns the values live at the entry of a block.
190    #[must_use]
191    pub fn live_in(&self, block: BlockId) -> &LiveSet {
192        &self.block_liveness[block.index()].live_in
193    }
194
195    /// Returns the values live at the exit of a block.
196    #[must_use]
197    pub fn live_out(&self, block: BlockId) -> &LiveSet {
198        &self.block_liveness[block.index()].live_out
199    }
200
201    /// Computes liveness at a specific instruction within a block.
202    /// Returns values live before and after the instruction.
203    #[must_use]
204    pub fn live_at_inst(
205        &self,
206        func: &Function,
207        block_id: BlockId,
208        inst_idx: usize,
209    ) -> LivenessInfo {
210        let bidx = block_id.index();
211        let block = &func.blocks[block_id];
212
213        // Start with live_out of the block
214        let mut live = self.block_liveness[bidx].live_out.clone();
215
216        // Process terminator (add its uses — they must be live before the terminator)
217        if let Some(term) = &block.terminator {
218            let mut term_uses = SmallVec::<[ValueId; 8]>::new();
219            collect_terminator_uses(term, &mut term_uses);
220            for operand in term_uses {
221                live.insert(operand);
222            }
223        }
224
225        // Process instructions in reverse order from end to inst_idx
226        let mut operand_buf = SmallVec::<[ValueId; 8]>::new();
227        let mut live_after = None;
228
229        for (idx, &inst_id) in block.instructions.iter().enumerate().rev() {
230            if idx == inst_idx {
231                live_after = Some(live.clone());
232            }
233
234            // Remove definition using precomputed map (O(1) instead of O(n)).
235            if let Some(&val_id) = self.inst_to_value.get(&inst_id) {
236                live.remove(val_id);
237            }
238
239            // Add uses (values become live before this instruction)
240            operand_buf.clear();
241            func.instruction(inst_id).kind.collect_operands(&mut operand_buf);
242            for &operand in &operand_buf {
243                live.insert(operand);
244            }
245
246            if idx == inst_idx {
247                return LivenessInfo { live_before: live, live_after: live_after.unwrap() };
248            }
249        }
250
251        // Should not reach here
252        LivenessInfo { live_before: live.clone(), live_after: live }
253    }
254
255    /// Returns the last use location of a value within a specific block, if any.
256    /// Returns `Some(Some(inst_idx))` if last used at an instruction,
257    /// `Some(None)` if last used in a terminator,
258    /// `None` if the value is not used in this block.
259    #[must_use]
260    pub fn last_use_in_block(&self, val: ValueId, block: BlockId) -> Option<Option<usize>> {
261        self.last_use_in_block.get(&(val, block)).copied()
262    }
263
264    /// Returns true if the value is dead after the given instruction in the given block.
265    ///
266    /// A value is dead after an instruction if:
267    /// 1. The instruction is the last use of the value within this block, AND
268    /// 2. The value is NOT in live_out (meaning no successor blocks use it)
269    #[must_use]
270    pub fn is_dead_after(&self, val: ValueId, block: BlockId, inst_idx: usize) -> bool {
271        // If the value is in live_out, it's used by successor blocks, so it's not dead
272        if self.block_liveness[block.index()].live_out.contains(val) {
273            return false;
274        }
275
276        // Check if this instruction is the last use within this block
277        match self.last_use_in_block.get(&(val, block)) {
278            Some(&Some(last_idx)) => last_idx == inst_idx,
279            // Last use is in terminator - not dead after any instruction
280            Some(&None) => false,
281            // Value not used in this block at all - should not happen if we're asking
282            // but conservatively say it's dead
283            None => true,
284        }
285    }
286}
287
288/// Collects all value uses from a terminator.
289fn collect_terminator_uses(term: &Terminator, out: &mut SmallVec<[ValueId; 8]>) {
290    match term {
291        Terminator::Jump(_) => {}
292        Terminator::Branch { condition, .. } => {
293            out.push(*condition);
294        }
295        Terminator::Switch { value, cases, .. } => {
296            out.push(*value);
297            for (case_val, _) in cases {
298                out.push(*case_val);
299            }
300        }
301        Terminator::Return { values } => {
302            out.extend(values.iter().copied());
303        }
304        Terminator::Revert { offset, size } | Terminator::ReturnData { offset, size } => {
305            out.push(*offset);
306            out.push(*size);
307        }
308        Terminator::Stop | Terminator::Invalid => {}
309        Terminator::SelfDestruct { recipient } => {
310            out.push(*recipient);
311        }
312    }
313}
314
315#[cfg(test)]
316mod tests {
317    use super::*;
318
319    #[test]
320    fn test_liveset_basic() {
321        let mut set = LiveSet::with_capacity(100);
322        let v0 = ValueId::from_usize(0);
323        let v42 = ValueId::from_usize(42);
324        let v99 = ValueId::from_usize(99);
325
326        assert!(!set.contains(v0));
327        assert!(!set.contains(v42));
328
329        assert!(set.insert(v0));
330        assert!(set.contains(v0));
331        assert!(!set.insert(v0)); // Already present
332
333        assert!(set.insert(v42));
334        assert!(set.contains(v42));
335
336        assert!(set.insert(v99));
337        assert_eq!(set.count(), 3);
338
339        set.remove(v42);
340        assert!(!set.contains(v42));
341        assert_eq!(set.count(), 2);
342    }
343
344    #[test]
345    fn test_liveset_union() {
346        let mut set1 = LiveSet::with_capacity(64);
347        let mut set2 = LiveSet::with_capacity(64);
348
349        set1.insert(ValueId::from_usize(1));
350        set1.insert(ValueId::from_usize(3));
351        set2.insert(ValueId::from_usize(2));
352        set2.insert(ValueId::from_usize(3));
353
354        assert!(set1.union(&set2));
355        assert!(set1.contains(ValueId::from_usize(1)));
356        assert!(set1.contains(ValueId::from_usize(2)));
357        assert!(set1.contains(ValueId::from_usize(3)));
358        assert_eq!(set1.count(), 3);
359
360        // Union again should not change
361        assert!(!set1.union(&set2));
362    }
363
364    #[test]
365    fn test_liveset_boundary() {
366        let mut set = LiveSet::with_capacity(200);
367        for i in [0, 1, 62, 63, 64, 65, 126, 127, 128, 129, 199] {
368            assert!(set.insert(ValueId::from_usize(i)));
369            assert!(set.contains(ValueId::from_usize(i)));
370        }
371        assert_eq!(set.count(), 11);
372    }
373
374    #[test]
375    fn test_liveset_clear() {
376        let mut set = LiveSet::with_capacity(128);
377        set.insert(ValueId::from_usize(0));
378        set.insert(ValueId::from_usize(63));
379        set.insert(ValueId::from_usize(64));
380        set.insert(ValueId::from_usize(127));
381        assert_eq!(set.count(), 4);
382        set.clear();
383        assert_eq!(set.count(), 0);
384        assert!(!set.contains(ValueId::from_usize(0)));
385    }
386
387    #[test]
388    fn test_liveset_iter() {
389        let mut set = LiveSet::with_capacity(200);
390        let indices = [0, 5, 63, 64, 127, 128, 199];
391        for &i in &indices {
392            set.insert(ValueId::from_usize(i));
393        }
394        let collected: Vec<usize> = set.iter().map(|v| v.index()).collect();
395        assert_eq!(collected, indices);
396    }
397
398    // === Liveness algorithm tests ===
399
400    use crate::mir::{Function, FunctionBuilder, MirType};
401    use solar_interface::Ident;
402
403    fn make_func() -> Function {
404        Function::new(Ident::DUMMY)
405    }
406
407    #[test]
408    fn test_linear_code() {
409        // bb0: v2 = add v0, v1; ret v2
410        let mut func = make_func();
411        let mut b = FunctionBuilder::new(&mut func);
412        let x = b.add_param(MirType::uint256());
413        let one = b.imm_u64(1);
414        let sum = b.add(x, one);
415        b.ret([sum]);
416
417        let liveness = Liveness::compute(&func);
418        let entry = func.entry_block;
419
420        // x and one are used by add, so live-in to entry.
421        assert!(liveness.live_in(entry).contains(x));
422        assert!(liveness.live_in(entry).contains(one));
423        // sum is defined in entry and consumed by ret; not live-out.
424        assert!(!liveness.live_out(entry).contains(sum));
425        // Nothing escapes a single return block.
426        assert_eq!(liveness.live_out(entry).count(), 0);
427    }
428
429    #[test]
430    fn test_diamond_cfg() {
431        // entry: branch cond, then_bb, else_bb
432        // then_bb: v_then = add x, c1; jump merge
433        // else_bb: v_else = sub x, c2; jump merge
434        // merge: ret x  (x used across branches)
435        let mut func = make_func();
436        let mut b = FunctionBuilder::new(&mut func);
437        let x = b.add_param(MirType::uint256());
438        let cond = b.add_param(MirType::Bool);
439
440        let then_bb = b.create_block();
441        let else_bb = b.create_block();
442        let merge = b.create_block();
443
444        b.branch(cond, then_bb, else_bb);
445
446        b.switch_to_block(then_bb);
447        let c1 = b.imm_u64(1);
448        let v_then = b.add(x, c1);
449        b.jump(merge);
450
451        b.switch_to_block(else_bb);
452        let c2 = b.imm_u64(1);
453        let v_else = b.sub(x, c2);
454        b.jump(merge);
455
456        // merge: return both values via x (simplified: just ret x)
457        b.switch_to_block(merge);
458        b.ret([v_then]);
459
460        let liveness = Liveness::compute(&func);
461
462        // x must be live-in to then_bb and else_bb (used in add/sub).
463        assert!(liveness.live_in(then_bb).contains(x));
464        assert!(liveness.live_in(else_bb).contains(x));
465        // x must be live-out of entry (flows to successors).
466        assert!(liveness.live_out(func.entry_block).contains(x));
467        // v_then must be live-out of then_bb (used in merge's ret).
468        assert!(liveness.live_out(then_bb).contains(v_then));
469        // v_else should NOT be live-out of else_bb (merge returns v_then, not v_else).
470        assert!(!liveness.live_out(else_bb).contains(v_else));
471    }
472
473    #[test]
474    fn test_simple_loop() {
475        // entry: jump header
476        // header: cond = lt(i, limit); branch cond, body, exit
477        // body: i_next = add(i, step); jump header
478        // exit: ret i
479        //
480        // Note: without phis, i is the param. This tests cross-block liveness.
481        let mut func = make_func();
482        let mut b = FunctionBuilder::new(&mut func);
483        let i = b.add_param(MirType::uint256());
484        let limit = b.imm_u64(10);
485
486        let header = b.create_block();
487        let body = b.create_block();
488        let exit = b.create_block();
489
490        b.jump(header);
491
492        b.switch_to_block(header);
493        let cond = b.lt(i, limit);
494        b.branch(cond, body, exit);
495
496        b.switch_to_block(body);
497        let step = b.imm_u64(1);
498        let _i_next = b.add(i, step);
499        b.jump(header);
500
501        b.switch_to_block(exit);
502        b.ret([i]);
503
504        let liveness = Liveness::compute(&func);
505
506        // i must be live through the entire loop.
507        assert!(liveness.live_in(header).contains(i), "i live-in to header");
508        assert!(liveness.live_in(body).contains(i), "i live-in to body");
509        assert!(liveness.live_in(exit).contains(i), "i live-in to exit");
510        assert!(liveness.live_out(header).contains(i), "i live-out of header");
511        assert!(liveness.live_out(body).contains(i), "i live-out of body");
512    }
513
514    #[test]
515    fn test_dead_instruction_result() {
516        // A dead *instruction result* (not just an immediate).
517        // The add result is never used — liveness should not track it
518        // beyond its definition point.
519        let mut func = make_func();
520        let mut b = FunctionBuilder::new(&mut func);
521        let x = b.add_param(MirType::uint256());
522        let y = b.add_param(MirType::uint256());
523        let dead = b.add(x, y); // result never used
524        b.ret([x]);
525
526        let liveness = Liveness::compute(&func);
527        let entry = func.entry_block;
528
529        assert!(liveness.live_in(entry).contains(x));
530        // dead instruction result must not be live-out.
531        assert!(!liveness.live_out(entry).contains(dead));
532        // dead after its own instruction.
533        assert!(liveness.is_dead_after(dead, entry, 0), "dead inst result should be dead");
534    }
535
536    #[test]
537    fn test_unused_param() {
538        let mut func = make_func();
539        let mut b = FunctionBuilder::new(&mut func);
540        let _x = b.add_param(MirType::uint256()); // Unused.
541        let y = b.add_param(MirType::uint256());
542        b.ret([y]);
543
544        let liveness = Liveness::compute(&func);
545        let entry = func.entry_block;
546
547        assert!(!liveness.live_in(entry).contains(_x), "unused param not live");
548        assert!(liveness.live_in(entry).contains(y), "used param is live");
549    }
550
551    #[test]
552    fn test_value_used_in_two_successors() {
553        // entry: branch cond, left, right
554        // left: ret x
555        // right: ret x
556        let mut func = make_func();
557        let mut b = FunctionBuilder::new(&mut func);
558        let x = b.add_param(MirType::uint256());
559        let cond = b.add_param(MirType::Bool);
560
561        let left = b.create_block();
562        let right = b.create_block();
563
564        b.branch(cond, left, right);
565
566        b.switch_to_block(left);
567        b.ret([x]);
568
569        b.switch_to_block(right);
570        b.ret([x]);
571
572        let liveness = Liveness::compute(&func);
573
574        assert!(liveness.live_in(left).contains(x));
575        assert!(liveness.live_in(right).contains(x));
576        assert!(liveness.live_out(func.entry_block).contains(x));
577    }
578
579    #[test]
580    fn test_empty_function() {
581        let mut func = make_func();
582        let mut b = FunctionBuilder::new(&mut func);
583        b.stop();
584
585        let liveness = Liveness::compute(&func);
586        assert_eq!(liveness.live_in(func.entry_block).count(), 0);
587        assert_eq!(liveness.live_out(func.entry_block).count(), 0);
588    }
589
590    #[test]
591    fn test_side_effect_op_keeps_operands_live() {
592        // sstore(slot, val); loaded = sload(slot); ret loaded
593        let mut func = make_func();
594        let mut b = FunctionBuilder::new(&mut func);
595        let slot = b.add_param(MirType::uint256());
596        let val = b.add_param(MirType::uint256());
597        b.sstore(slot, val);
598        let loaded = b.sload(slot);
599        b.ret([loaded]);
600
601        let liveness = Liveness::compute(&func);
602        let entry = func.entry_block;
603
604        // Before sstore (inst 0): slot and val must be live.
605        let info_0 = liveness.live_at_inst(&func, entry, 0);
606        assert!(info_0.live_before.contains(slot), "slot live before sstore");
607        assert!(info_0.live_before.contains(val), "val live before sstore");
608
609        // Before sload (inst 1): slot must be live, val no longer needed.
610        let info_1 = liveness.live_at_inst(&func, entry, 1);
611        assert!(info_1.live_before.contains(slot), "slot live before sload");
612        assert!(!info_1.live_before.contains(val), "val not live before sload");
613    }
614
615    #[test]
616    fn test_live_at_inst() {
617        // bb0: v2 = add v0, v1; v3 = mul v2, v0; ret v3
618        let mut func = make_func();
619        let mut b = FunctionBuilder::new(&mut func);
620        let v0 = b.add_param(MirType::uint256());
621        let v1 = b.add_param(MirType::uint256());
622        let v2 = b.add(v0, v1);
623        let v3 = b.mul(v2, v0);
624        b.ret([v3]);
625
626        let liveness = Liveness::compute(&func);
627        let entry = func.entry_block;
628
629        // Before add (inst 0): v0 and v1 are live.
630        let info_0 = liveness.live_at_inst(&func, entry, 0);
631        assert!(info_0.live_before.contains(v0));
632        assert!(info_0.live_before.contains(v1));
633        assert!(!info_0.live_before.contains(v2));
634        assert!(!info_0.live_before.contains(v3));
635
636        // After add (inst 0): v0 and v2 are live (v0 used again by mul).
637        assert!(info_0.live_after.contains(v0));
638        assert!(info_0.live_after.contains(v2));
639        assert!(!info_0.live_after.contains(v1), "v1 dead after add");
640
641        // Before mul (inst 1): v0 and v2 are live.
642        let info_1 = liveness.live_at_inst(&func, entry, 1);
643        assert!(info_1.live_before.contains(v0));
644        assert!(info_1.live_before.contains(v2));
645
646        // After mul (inst 1): only v3 is live (used by ret).
647        assert!(info_1.live_after.contains(v3));
648        assert!(!info_1.live_after.contains(v0));
649        assert!(!info_1.live_after.contains(v2));
650    }
651
652    #[test]
653    fn test_is_dead_after() {
654        // bb0: v2 = add v0, v1; ret v2
655        let mut func = make_func();
656        let mut b = FunctionBuilder::new(&mut func);
657        let v0 = b.add_param(MirType::uint256());
658        let v1 = b.add_param(MirType::uint256());
659        let v2 = b.add(v0, v1);
660        b.ret([v2]);
661
662        let liveness = Liveness::compute(&func);
663        let entry = func.entry_block;
664
665        // v0 and v1 are dead after the add (inst 0) — their last use is at inst 0.
666        assert!(liveness.is_dead_after(v0, entry, 0), "v0 dead after add");
667        assert!(liveness.is_dead_after(v1, entry, 0), "v1 dead after add");
668        // v2 is NOT dead after add — it's used by ret (terminator).
669        assert!(!liveness.is_dead_after(v2, entry, 0), "v2 alive after add");
670    }
671
672    #[test]
673    fn test_last_use_in_block() {
674        // bb0: v2 = add v0, v1; v3 = mul v2, v0; ret v3
675        let mut func = make_func();
676        let mut b = FunctionBuilder::new(&mut func);
677        let v0 = b.add_param(MirType::uint256());
678        let v1 = b.add_param(MirType::uint256());
679        let _v2 = b.add(v0, v1);
680        let v3 = b.mul(_v2, v0);
681        b.ret([v3]);
682
683        let liveness = Liveness::compute(&func);
684        let entry = func.entry_block;
685
686        // v0 last used at inst 1 (mul).
687        assert_eq!(liveness.last_use_in_block(v0, entry), Some(Some(1)));
688        // v1 last used at inst 0 (add).
689        assert_eq!(liveness.last_use_in_block(v1, entry), Some(Some(0)));
690        // v3 last used at terminator (ret).
691        assert_eq!(liveness.last_use_in_block(v3, entry), Some(None));
692    }
693
694    #[test]
695    fn test_value_live_across_multiple_blocks() {
696        // entry: jump bb1
697        // bb1: v2 = add(v0, v1); jump bb2
698        // bb2: ret v0  (v0 must be live through bb1 and into bb2)
699        let mut func = make_func();
700        let mut b = FunctionBuilder::new(&mut func);
701        let v0 = b.add_param(MirType::uint256());
702        let v1 = b.add_param(MirType::uint256());
703
704        let bb1 = b.create_block();
705        let bb2 = b.create_block();
706
707        b.jump(bb1);
708
709        b.switch_to_block(bb1);
710        let _v2 = b.add(v0, v1);
711        b.jump(bb2);
712
713        b.switch_to_block(bb2);
714        b.ret([v0]);
715
716        let liveness = Liveness::compute(&func);
717
718        // v0 must be live-out of bb1 (used in bb2).
719        assert!(liveness.live_out(bb1).contains(v0));
720        // v1 is NOT live-out of bb1 (only used locally in bb1's add).
721        assert!(!liveness.live_out(bb1).contains(v1));
722        // v0 must be live-in to bb2.
723        assert!(liveness.live_in(bb2).contains(v0));
724    }
725
726    #[test]
727    fn test_multiple_returns() {
728        // entry: branch cond, left, right
729        // left: ret v0
730        // right: ret v1
731        let mut func = make_func();
732        let mut b = FunctionBuilder::new(&mut func);
733        let v0 = b.add_param(MirType::uint256());
734        let v1 = b.add_param(MirType::uint256());
735        let cond = b.add_param(MirType::Bool);
736
737        let left = b.create_block();
738        let right = b.create_block();
739
740        b.branch(cond, left, right);
741
742        b.switch_to_block(left);
743        b.ret([v0]);
744
745        b.switch_to_block(right);
746        b.ret([v1]);
747
748        let liveness = Liveness::compute(&func);
749
750        // v0 is live-in to left, NOT to right.
751        assert!(liveness.live_in(left).contains(v0));
752        assert!(!liveness.live_in(right).contains(v0));
753        // v1 is live-in to right, NOT to left.
754        assert!(liveness.live_in(right).contains(v1));
755        assert!(!liveness.live_in(left).contains(v1));
756    }
757
758    #[test]
759    fn test_phi_liveness() {
760        // Phi nodes are ordinary instructions (`InstKind::Phi`): their incoming
761        // operands are uses at the phi instruction in the merge block, and the
762        // phi result is defined like any other instruction result.
763        //
764        // entry: jump header
765        // header: phi_val = phi [(entry, init), (body, updated)]
766        //         branch cond, body, exit
767        // body:   updated = add phi_val, step; jump header
768        // exit:   ret phi_val
769        let mut func = make_func();
770
771        // Phase 1: build the CFG skeleton without the phi (to avoid borrow conflicts).
772        let entry;
773        let header;
774        let body;
775        let exit;
776        let init;
777        let updated;
778        let phi_placeholder; // ValueId slot we'll point at the phi instruction.
779        {
780            let mut b = FunctionBuilder::new(&mut func);
781            init = b.imm_u64(0);
782            entry = b.current_block();
783            header = b.create_block();
784            body = b.create_block();
785            exit = b.create_block();
786
787            b.jump(header);
788
789            b.switch_to_block(header);
790            // Allocate a placeholder for the phi result (an undef that we'll replace).
791            phi_placeholder = b.undef(MirType::uint256());
792            let limit = b.imm_u64(10);
793            let cond = b.lt(phi_placeholder, limit);
794            b.branch(cond, body, exit);
795
796            b.switch_to_block(body);
797            let step = b.imm_u64(1);
798            updated = b.add(phi_placeholder, step);
799            b.jump(header);
800
801            b.switch_to_block(exit);
802            b.ret([phi_placeholder]);
803        }
804
805        // Phase 2: insert the phi instruction at the head of `header` and point
806        // the placeholder value at its result.
807        let phi_val = phi_placeholder;
808        let phi_inst = func.alloc_inst(crate::mir::Instruction::new(
809            crate::mir::InstKind::Phi(vec![(entry, init), (body, updated)]),
810            Some(MirType::uint256()),
811        ));
812        func.blocks[header].instructions.insert(0, phi_inst);
813        func.values[phi_val] = crate::mir::Value::Inst(phi_inst);
814
815        let liveness = Liveness::compute(&func);
816
817        // init must be live-out of entry (used by the phi in header).
818        assert!(liveness.live_out(entry).contains(init), "init live-out of entry");
819        // updated must be live-out of body (flows back to the phi via the back-edge).
820        assert!(liveness.live_out(body).contains(updated), "updated live-out of body");
821        // phi_val must be live-in to body and exit (used by add and ret).
822        assert!(liveness.live_in(body).contains(phi_val), "phi_val live-in to body");
823        assert!(liveness.live_in(exit).contains(phi_val), "phi_val live-in to exit");
824        // phi_val should NOT be live-in to entry (it's defined in header).
825        assert!(!liveness.live_in(entry).contains(phi_val), "phi_val not live-in to entry");
826    }
827
828    #[test]
829    fn test_inst_to_value_map_consistency() {
830        // The precomputed inst_to_value map must agree with a linear scan
831        // of func.values. This validates the O(1) lookup matches O(n) truth.
832        let mut func = make_func();
833        {
834            let mut b = FunctionBuilder::new(&mut func);
835            let x = b.add_param(MirType::uint256());
836            let y = b.add_param(MirType::uint256());
837            let sum = b.add(x, y);
838            let product = b.mul(sum, x);
839            b.sstore(x, product); // no result
840            let loaded = b.sload(y);
841            b.ret([loaded]);
842        }
843        let liveness = Liveness::compute(&func);
844
845        // Rebuild the map the slow way and compare.
846        use solar_data_structures::map::FxHashMap;
847        let mut expected: FxHashMap<crate::mir::InstId, ValueId> = FxHashMap::default();
848        for (val_id, val) in func.values.iter_enumerated() {
849            if let crate::mir::Value::Inst(inst_id) = val {
850                expected.insert(*inst_id, val_id);
851            }
852        }
853        assert_eq!(liveness.inst_to_value, expected, "inst_to_value map diverged from linear scan");
854    }
855}