Skip to main content

celox_backend_common/regalloc/
live_interval.rs

1//! Exact sparse live intervals over opcode-free allocation facts.
2//!
3//! Backends retain their own machine IR. This module only sees the normalized
4//! control-flow, SSA definitions, uses, and phi edges exported at the
5//! allocation boundary.
6
7use std::collections::{BTreeMap, BTreeSet, VecDeque};
8use std::fmt;
9use std::hash::Hash;
10
11use super::FunctionAllocationFacts;
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
14struct DefinitionSite {
15    block: usize,
16    instruction: Option<usize>,
17    slot: u64,
18}
19
20#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
21struct UseSite {
22    block: usize,
23    instruction: Option<usize>,
24    slot: u64,
25}
26
27/// One half-open live segment within a normalized basic block.
28#[derive(Debug, Clone, Copy, PartialEq, Eq)]
29pub struct LiveSegment {
30    pub block: usize,
31    pub start: u64,
32    pub end: u64,
33}
34
35impl LiveSegment {
36    pub fn contains(self, point: u64) -> bool {
37        self.start <= point && point < self.end
38    }
39
40    pub fn overlaps(self, other: Self) -> bool {
41        self.block == other.block && self.start < other.end && other.start < self.end
42    }
43}
44
45/// Sparse per-block live interval for one target-owned virtual register.
46#[derive(Debug, Clone, PartialEq, Eq)]
47pub struct LiveInterval<V> {
48    pub value: V,
49    pub segments: Vec<LiveSegment>,
50}
51
52impl<V> LiveInterval<V> {
53    pub fn segment_in_block(&self, block: usize) -> Option<LiveSegment> {
54        self.segments
55            .binary_search_by_key(&block, |segment| segment.block)
56            .ok()
57            .map(|index| self.segments[index])
58    }
59
60    pub fn interferes(&self, other: &Self) -> bool {
61        let mut left = 0;
62        let mut right = 0;
63        while left < self.segments.len() && right < other.segments.len() {
64            let a = self.segments[left];
65            let b = other.segments[right];
66            if a.overlaps(b) {
67                return true;
68            }
69            if (a.block, a.end) <= (b.block, b.end) {
70                left += 1;
71            } else {
72                right += 1;
73            }
74        }
75        false
76    }
77}
78
79/// Exact SSA liveness reconstructed from allocation facts.
80#[derive(Debug, Clone, PartialEq, Eq)]
81pub struct LiveIntervals<V> {
82    intervals: BTreeMap<V, LiveInterval<V>>,
83    live_in: Vec<BTreeSet<V>>,
84    live_out: Vec<BTreeSet<V>>,
85}
86
87impl<V: Ord> LiveIntervals<V> {
88    pub fn get(&self, value: &V) -> Option<&LiveInterval<V>> {
89        self.intervals.get(value)
90    }
91
92    pub fn iter(&self) -> impl Iterator<Item = (&V, &LiveInterval<V>)> {
93        self.intervals.iter()
94    }
95
96    pub fn live_in(&self, block: usize) -> Option<&BTreeSet<V>> {
97        self.live_in.get(block)
98    }
99
100    pub fn live_out(&self, block: usize) -> Option<&BTreeSet<V>> {
101        self.live_out.get(block)
102    }
103}
104
105/// Failure while constructing strict-SSA live intervals.
106#[derive(Debug, Clone, PartialEq, Eq)]
107pub struct LiveIntervalError<V> {
108    pub rule: &'static str,
109    pub block: Option<usize>,
110    pub instruction: Option<usize>,
111    pub values: Vec<V>,
112    pub message: String,
113}
114
115impl<V> LiveIntervalError<V> {
116    fn new(
117        rule: &'static str,
118        block: Option<usize>,
119        instruction: Option<usize>,
120        values: Vec<V>,
121        message: impl Into<String>,
122    ) -> Self {
123        Self {
124            rule,
125            block,
126            instruction,
127            values,
128            message: message.into(),
129        }
130    }
131}
132
133impl<V: fmt::Debug> fmt::Display for LiveIntervalError<V> {
134    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
135        write!(formatter, "{}", self.rule)?;
136        if let Some(block) = self.block {
137            write!(formatter, " at block {block}")?;
138        }
139        if let Some(instruction) = self.instruction {
140            write!(formatter, "/i{instruction}")?;
141        }
142        if !self.values.is_empty() {
143            write!(formatter, " values={:?}", self.values)?;
144        }
145        write!(formatter, ": {}", self.message)
146    }
147}
148
149impl<V: fmt::Debug> std::error::Error for LiveIntervalError<V> {}
150
151struct BlockFacts<V> {
152    definitions: BTreeSet<V>,
153    upward_uses: BTreeSet<V>,
154    last_use: BTreeMap<V, u64>,
155}
156
157impl<V> Default for BlockFacts<V> {
158    fn default() -> Self {
159        Self {
160            definitions: BTreeSet::new(),
161            upward_uses: BTreeSet::new(),
162            last_use: BTreeMap::new(),
163        }
164    }
165}
166
167struct ModelFacts<V> {
168    definitions: BTreeMap<V, DefinitionSite>,
169    uses: BTreeMap<V, Vec<UseSite>>,
170    blocks: Vec<BlockFacts<V>>,
171    edge_uses: BTreeMap<(usize, usize), BTreeSet<V>>,
172}
173
174#[derive(Debug, Clone, Copy, PartialEq, Eq)]
175struct BlockSlots {
176    phi_def: u64,
177    exit: u64,
178}
179
180fn instruction_use_slot(instruction: usize) -> Option<u64> {
181    u64::try_from(instruction)
182        .ok()?
183        .checked_mul(3)?
184        .checked_add(2)
185}
186
187fn instruction_def_slot(instruction: usize) -> Option<u64> {
188    instruction_use_slot(instruction)?.checked_add(2)
189}
190
191fn block_slots<V, R>(
192    facts: &FunctionAllocationFacts<V, R>,
193) -> Result<Vec<BlockSlots>, LiveIntervalError<V>> {
194    facts
195        .blocks
196        .iter()
197        .enumerate()
198        .map(|(block, facts)| {
199            let exit = u64::try_from(facts.instructions.len())
200                .ok()
201                .and_then(|count| count.checked_mul(3))
202                .and_then(|slot| slot.checked_add(2))
203                .ok_or_else(|| {
204                    LiveIntervalError::new(
205                        "LIVE_INTERVAL.SLOT_RANGE",
206                        Some(block),
207                        None,
208                        Vec::new(),
209                        "block exit is outside the program-point domain",
210                    )
211                })?;
212            Ok(BlockSlots { phi_def: 1, exit })
213        })
214        .collect()
215}
216
217fn record_definition<V: Copy + Ord>(
218    definitions: &mut BTreeMap<V, DefinitionSite>,
219    value: V,
220    site: DefinitionSite,
221) -> Result<(), LiveIntervalError<V>> {
222    if let Some(previous) = definitions.insert(value, site) {
223        return Err(LiveIntervalError::new(
224            "LIVE_INTERVAL.MULTIPLE_DEFINITIONS",
225            Some(site.block),
226            site.instruction,
227            vec![value],
228            format!("value was already defined at {previous:?}"),
229        ));
230    }
231    Ok(())
232}
233
234fn collect_model<V, R>(
235    facts: &FunctionAllocationFacts<V, R>,
236    predecessors: &[Vec<usize>],
237    slots: &[BlockSlots],
238) -> Result<ModelFacts<V>, LiveIntervalError<V>>
239where
240    V: Copy + Ord,
241{
242    let mut definitions = BTreeMap::new();
243    let mut uses = BTreeMap::<V, Vec<UseSite>>::new();
244    let mut blocks = (0..facts.blocks.len())
245        .map(|_| BlockFacts::default())
246        .collect::<Vec<_>>();
247
248    for (block_index, block) in facts.blocks.iter().enumerate() {
249        for phi in &block.phis {
250            let site = DefinitionSite {
251                block: block_index,
252                instruction: None,
253                slot: slots[block_index].phi_def,
254            };
255            record_definition(&mut definitions, phi.destination, site)?;
256            blocks[block_index].definitions.insert(phi.destination);
257        }
258
259        let mut seen_definitions = blocks[block_index].definitions.clone();
260        for (instruction_index, instruction) in block.instructions.iter().enumerate() {
261            let use_slot = instruction_use_slot(instruction_index).ok_or_else(|| {
262                LiveIntervalError::new(
263                    "LIVE_INTERVAL.SLOT_RANGE",
264                    Some(block_index),
265                    Some(instruction_index),
266                    Vec::new(),
267                    "instruction use is outside the program-point domain",
268                )
269            })?;
270            let mut instruction_uses = instruction.uses.clone();
271            instruction_uses.sort_unstable();
272            instruction_uses.dedup();
273            for value in instruction_uses {
274                let site = UseSite {
275                    block: block_index,
276                    instruction: Some(instruction_index),
277                    slot: use_slot,
278                };
279                uses.entry(value).or_default().push(site);
280                if !seen_definitions.contains(&value) {
281                    blocks[block_index].upward_uses.insert(value);
282                }
283                blocks[block_index]
284                    .last_use
285                    .entry(value)
286                    .and_modify(|current| *current = (*current).max(use_slot))
287                    .or_insert(use_slot);
288            }
289            let def_slot = instruction_def_slot(instruction_index).ok_or_else(|| {
290                LiveIntervalError::new(
291                    "LIVE_INTERVAL.SLOT_RANGE",
292                    Some(block_index),
293                    Some(instruction_index),
294                    Vec::new(),
295                    "instruction definition is outside the program-point domain",
296                )
297            })?;
298            for &value in &instruction.defs {
299                let site = DefinitionSite {
300                    block: block_index,
301                    instruction: Some(instruction_index),
302                    slot: def_slot,
303                };
304                record_definition(&mut definitions, value, site)?;
305                blocks[block_index].definitions.insert(value);
306                seen_definitions.insert(value);
307            }
308        }
309    }
310
311    let mut edge_uses = BTreeMap::<(usize, usize), BTreeSet<V>>::new();
312    for (successor, block) in facts.blocks.iter().enumerate() {
313        for phi in &block.phis {
314            let mut seen_predecessors = BTreeSet::new();
315            for source in &phi.sources {
316                if !seen_predecessors.insert(source.predecessor) {
317                    return Err(LiveIntervalError::new(
318                        "LIVE_INTERVAL.PHI_PREDECESSOR",
319                        Some(successor),
320                        None,
321                        vec![source.value],
322                        "phi predecessor appears more than once",
323                    ));
324                }
325                let site = UseSite {
326                    block: source.predecessor,
327                    instruction: None,
328                    slot: slots[source.predecessor].exit,
329                };
330                uses.entry(source.value).or_default().push(site);
331                edge_uses
332                    .entry((source.predecessor, successor))
333                    .or_default()
334                    .insert(source.value);
335                blocks[source.predecessor]
336                    .last_use
337                    .entry(source.value)
338                    .and_modify(|current| *current = (*current).max(site.slot))
339                    .or_insert(site.slot);
340            }
341            if seen_predecessors.len() != predecessors[successor].len() {
342                return Err(LiveIntervalError::new(
343                    "LIVE_INTERVAL.PHI_PREDECESSOR",
344                    Some(successor),
345                    None,
346                    vec![phi.destination],
347                    "phi does not provide exactly one source for every predecessor",
348                ));
349            }
350        }
351    }
352    for sites in uses.values_mut() {
353        sites.sort_unstable();
354        sites.dedup();
355    }
356
357    Ok(ModelFacts {
358        definitions,
359        uses,
360        blocks,
361        edge_uses,
362    })
363}
364
365fn solve_liveness<V: Copy + Ord, R>(
366    facts: &FunctionAllocationFacts<V, R>,
367    predecessors: &[Vec<usize>],
368    model: &ModelFacts<V>,
369) -> (Vec<BTreeSet<V>>, Vec<BTreeSet<V>>) {
370    let mut live_in = vec![BTreeSet::new(); facts.blocks.len()];
371    let mut live_out = live_in.clone();
372    let mut queue = (0..facts.blocks.len()).rev().collect::<VecDeque<_>>();
373    let mut queued = vec![true; facts.blocks.len()];
374    while let Some(block) = queue.pop_front() {
375        queued[block] = false;
376        let mut next_out = BTreeSet::new();
377        for &successor in &facts.blocks[block].successors {
378            next_out.extend(live_in[successor].iter().copied());
379            if let Some(edge) = model.edge_uses.get(&(block, successor)) {
380                next_out.extend(edge.iter().copied());
381            }
382        }
383        let mut next_in = model.blocks[block].upward_uses.clone();
384        next_in.extend(
385            next_out
386                .iter()
387                .copied()
388                .filter(|value| !model.blocks[block].definitions.contains(value)),
389        );
390        if next_in != live_in[block] || next_out != live_out[block] {
391            live_in[block] = next_in;
392            live_out[block] = next_out;
393            for &predecessor in &predecessors[block] {
394                if !queued[predecessor] {
395                    queued[predecessor] = true;
396                    queue.push_back(predecessor);
397                }
398            }
399        }
400    }
401    (live_in, live_out)
402}
403
404struct DominatorTree {
405    enter: Vec<usize>,
406    exit: Vec<usize>,
407}
408
409impl DominatorTree {
410    fn dominates(&self, dominator: usize, block: usize) -> bool {
411        self.enter[dominator] <= self.enter[block] && self.exit[block] <= self.exit[dominator]
412    }
413}
414
415fn compute_dominators<V>(
416    entry: usize,
417    successors: &[Vec<usize>],
418    predecessors: &[Vec<usize>],
419) -> Result<DominatorTree, LiveIntervalError<V>> {
420    let mut reachable = vec![false; successors.len()];
421    let mut postorder = Vec::with_capacity(successors.len());
422    let mut stack = vec![(entry, 0usize)];
423    reachable[entry] = true;
424    while let Some((block, next_successor)) = stack.last_mut() {
425        if *next_successor == successors[*block].len() {
426            postorder.push(*block);
427            stack.pop();
428        } else {
429            let successor = successors[*block][*next_successor];
430            *next_successor += 1;
431            if !reachable[successor] {
432                reachable[successor] = true;
433                stack.push((successor, 0));
434            }
435        }
436    }
437    if let Some(block) = reachable.iter().position(|reachable| !reachable) {
438        return Err(LiveIntervalError::new(
439            "LIVE_INTERVAL.UNREACHABLE_BLOCK",
440            Some(block),
441            None,
442            Vec::new(),
443            "allocation facts contain a block unreachable from the entry",
444        ));
445    }
446
447    postorder.reverse();
448    let mut rpo_position = vec![0; successors.len()];
449    for (position, &block) in postorder.iter().enumerate() {
450        rpo_position[block] = position;
451    }
452    let mut idom = vec![None; successors.len()];
453    idom[entry] = Some(entry);
454    let mut changed = true;
455    while changed {
456        changed = false;
457        for &block in postorder.iter().skip(1) {
458            let mut processed = predecessors[block]
459                .iter()
460                .copied()
461                .filter(|predecessor| idom[*predecessor].is_some());
462            let mut next = processed.next().ok_or_else(|| {
463                LiveIntervalError::new(
464                    "LIVE_INTERVAL.DOMINATOR_TREE",
465                    Some(block),
466                    None,
467                    Vec::new(),
468                    "reachable block has no processed predecessor",
469                )
470            })?;
471            for predecessor in processed {
472                next = intersect_dominators(next, predecessor, &idom, &rpo_position);
473            }
474            if idom[block] != Some(next) {
475                idom[block] = Some(next);
476                changed = true;
477            }
478        }
479    }
480    idom[entry] = None;
481
482    let mut children = vec![Vec::new(); successors.len()];
483    for (block, parent) in idom.iter().copied().enumerate() {
484        if let Some(parent) = parent {
485            children[parent].push(block);
486        }
487    }
488    let mut enter = vec![0; successors.len()];
489    let mut exit = vec![0; successors.len()];
490    let mut clock = 0usize;
491    let mut stack = vec![(entry, false)];
492    while let Some((block, leaving)) = stack.pop() {
493        if leaving {
494            exit[block] = clock;
495            clock += 1;
496        } else {
497            enter[block] = clock;
498            clock += 1;
499            stack.push((block, true));
500            stack.extend(children[block].iter().rev().map(|&child| (child, false)));
501        }
502    }
503    Ok(DominatorTree { enter, exit })
504}
505
506fn intersect_dominators(
507    mut left: usize,
508    mut right: usize,
509    idom: &[Option<usize>],
510    rpo_position: &[usize],
511) -> usize {
512    while left != right {
513        while rpo_position[left] > rpo_position[right] {
514            left = idom[left].expect("processed dominator must have a parent");
515        }
516        while rpo_position[right] > rpo_position[left] {
517            right = idom[right].expect("processed dominator must have a parent");
518        }
519    }
520    left
521}
522
523/// Build exact block-sparse live intervals for a strict-SSA target program.
524pub fn analyze_live_intervals<V, R>(
525    facts: &FunctionAllocationFacts<V, R>,
526) -> Result<LiveIntervals<V>, LiveIntervalError<V>>
527where
528    V: Copy + Eq + Hash + Ord + fmt::Debug,
529{
530    facts.verify().map_err(|error| {
531        LiveIntervalError::new(
532            "LIVE_INTERVAL.ALLOCATION_FACTS",
533            None,
534            None,
535            Vec::new(),
536            error.to_string(),
537        )
538    })?;
539    let mut predecessors = vec![Vec::new(); facts.blocks.len()];
540    for (block, facts) in facts.blocks.iter().enumerate() {
541        for &successor in &facts.successors {
542            predecessors[successor].push(block);
543        }
544    }
545    let successors = facts
546        .blocks
547        .iter()
548        .map(|block| block.successors.clone())
549        .collect::<Vec<_>>();
550    let dominators = compute_dominators(facts.entry, &successors, &predecessors)?;
551    let slots = block_slots(facts)?;
552    let model = collect_model(facts, &predecessors, &slots)?;
553    let (live_in, live_out) = solve_liveness(facts, &predecessors, &model);
554    let mut segments = BTreeMap::<V, Vec<LiveSegment>>::new();
555
556    for block in 0..facts.blocks.len() {
557        let mut values = BTreeSet::new();
558        values.extend(live_in[block].iter().copied());
559        values.extend(live_out[block].iter().copied());
560        values.extend(model.blocks[block].definitions.iter().copied());
561        values.extend(model.blocks[block].last_use.keys().copied());
562        for value in values {
563            let Some(definition) = model.definitions.get(&value).copied() else {
564                return Err(LiveIntervalError::new(
565                    "LIVE_INTERVAL.MISSING_DEFINITION",
566                    Some(block),
567                    None,
568                    vec![value],
569                    "live or used value has no target-MIR definition",
570                ));
571            };
572            if definition.block == block && live_in[block].contains(&value) {
573                return Err(LiveIntervalError::new(
574                    "LIVE_INTERVAL.USE_BEFORE_DEFINITION",
575                    Some(block),
576                    definition.instruction,
577                    vec![value],
578                    "value is live at entry of its defining block",
579                ));
580            }
581            let start = if definition.block == block {
582                definition.slot
583            } else {
584                0
585            };
586            let end = if live_out[block].contains(&value) {
587                slots[block].exit.checked_add(1)
588            } else if let Some(last_use) = model.blocks[block].last_use.get(&value) {
589                last_use.checked_add(1)
590            } else if definition.block == block {
591                definition.slot.checked_add(1)
592            } else {
593                None
594            }
595            .ok_or_else(|| {
596                LiveIntervalError::new(
597                    "LIVE_INTERVAL.SLOT_RANGE",
598                    Some(block),
599                    None,
600                    vec![value],
601                    "live segment end overflows or has no local reason to exist",
602                )
603            })?;
604            if start >= end {
605                return Err(LiveIntervalError::new(
606                    "LIVE_INTERVAL.EMPTY_SEGMENT",
607                    Some(block),
608                    None,
609                    vec![value],
610                    format!("segment {start}..{end} is empty or reversed"),
611                ));
612            }
613            segments
614                .entry(value)
615                .or_default()
616                .push(LiveSegment { block, start, end });
617        }
618    }
619
620    let mut intervals = BTreeMap::new();
621    for (&value, &definition) in &model.definitions {
622        let mut value_segments = segments.remove(&value).unwrap_or_default();
623        value_segments.sort_unstable_by_key(|segment| (segment.block, segment.start));
624        let interval = LiveInterval {
625            value,
626            segments: value_segments,
627        };
628        if !interval
629            .segment_in_block(definition.block)
630            .is_some_and(|segment| segment.contains(definition.slot))
631        {
632            return Err(LiveIntervalError::new(
633                "LIVE_INTERVAL.DEFINITION_COVERAGE",
634                Some(definition.block),
635                definition.instruction,
636                vec![value],
637                "definition is not covered by its live interval",
638            ));
639        }
640        for site in model.uses.get(&value).into_iter().flatten() {
641            let covered = interval
642                .segment_in_block(site.block)
643                .is_some_and(|segment| segment.contains(site.slot));
644            let dominated = if definition.block == site.block {
645                definition.slot < site.slot
646            } else {
647                dominators.dominates(definition.block, site.block)
648            };
649            if !covered || !dominated {
650                return Err(LiveIntervalError::new(
651                    "LIVE_INTERVAL.DEFINITION_DOMINANCE",
652                    Some(site.block),
653                    site.instruction,
654                    vec![value],
655                    "definition does not dominate the target-MIR use",
656                ));
657            }
658        }
659        intervals.insert(value, interval);
660    }
661    if let Some((&value, sites)) = model
662        .uses
663        .iter()
664        .find(|(value, _)| !model.definitions.contains_key(value))
665    {
666        return Err(LiveIntervalError::new(
667            "LIVE_INTERVAL.MISSING_DEFINITION",
668            sites.first().map(|site| site.block),
669            sites.first().and_then(|site| site.instruction),
670            vec![value],
671            "used value has no target-MIR definition",
672        ));
673    }
674
675    Ok(LiveIntervals {
676        intervals,
677        live_in,
678        live_out,
679    })
680}
681
682#[cfg(test)]
683mod tests {
684    use super::*;
685    use crate::regalloc::{
686        BlockAllocationFacts, InstructionAllocationFacts, InstructionConstraints,
687        PhiAllocationFacts, PhiSource,
688    };
689
690    fn instruction(uses: Vec<u32>, defs: Vec<u32>) -> InstructionAllocationFacts<u32, ()> {
691        InstructionAllocationFacts {
692            uses,
693            defs,
694            constraints: InstructionConstraints::default(),
695            is_copy: false,
696        }
697    }
698
699    #[test]
700    fn diamond_arms_remain_non_interfering() {
701        let facts = FunctionAllocationFacts {
702            entry: 0,
703            blocks: vec![
704                BlockAllocationFacts {
705                    successors: vec![1, 2],
706                    phis: Vec::new(),
707                    instructions: vec![instruction(Vec::new(), vec![0])],
708                },
709                BlockAllocationFacts {
710                    successors: vec![3],
711                    phis: Vec::new(),
712                    instructions: vec![instruction(vec![0], vec![1])],
713                },
714                BlockAllocationFacts {
715                    successors: vec![3],
716                    phis: Vec::new(),
717                    instructions: vec![instruction(vec![0], vec![2])],
718                },
719                BlockAllocationFacts {
720                    successors: Vec::new(),
721                    phis: vec![PhiAllocationFacts {
722                        destination: 3,
723                        sources: vec![
724                            PhiSource {
725                                predecessor: 1,
726                                value: 1,
727                            },
728                            PhiSource {
729                                predecessor: 2,
730                                value: 2,
731                            },
732                        ],
733                    }],
734                    instructions: vec![instruction(vec![3], Vec::new())],
735                },
736            ],
737        };
738
739        let intervals = analyze_live_intervals(&facts).unwrap();
740        assert!(
741            !intervals
742                .get(&1)
743                .unwrap()
744                .interferes(intervals.get(&2).unwrap())
745        );
746        assert!(intervals.live_out(1).unwrap().contains(&1));
747        assert!(intervals.live_out(2).unwrap().contains(&2));
748        assert!(!intervals.live_in(3).unwrap().contains(&1));
749    }
750
751    #[test]
752    fn rejects_missing_phi_source() {
753        let facts = FunctionAllocationFacts::<u32, ()> {
754            entry: 0,
755            blocks: vec![
756                BlockAllocationFacts {
757                    successors: vec![1, 2],
758                    phis: Vec::new(),
759                    instructions: vec![instruction(Vec::new(), vec![0])],
760                },
761                BlockAllocationFacts {
762                    successors: vec![2],
763                    phis: Vec::new(),
764                    instructions: vec![instruction(Vec::new(), vec![1])],
765                },
766                BlockAllocationFacts {
767                    successors: Vec::new(),
768                    phis: vec![PhiAllocationFacts {
769                        destination: 2,
770                        sources: vec![PhiSource {
771                            predecessor: 0,
772                            value: 0,
773                        }],
774                    }],
775                    instructions: Vec::new(),
776                },
777            ],
778        };
779
780        assert_eq!(
781            analyze_live_intervals(&facts).unwrap_err().rule,
782            "LIVE_INTERVAL.PHI_PREDECESSOR"
783        );
784    }
785
786    #[test]
787    fn rejects_use_before_definition() {
788        let facts = FunctionAllocationFacts::<u32, ()> {
789            entry: 0,
790            blocks: vec![BlockAllocationFacts {
791                successors: Vec::new(),
792                phis: Vec::new(),
793                instructions: vec![
794                    instruction(vec![0], Vec::new()),
795                    instruction(Vec::new(), vec![0]),
796                ],
797            }],
798        };
799
800        assert_eq!(
801            analyze_live_intervals(&facts).unwrap_err().rule,
802            "LIVE_INTERVAL.USE_BEFORE_DEFINITION"
803        );
804    }
805}