Skip to main content

logicaffeine_proof/
register_alloc.rs

1//! Certified linear-scan register allocation — the matching/Hall reasoner as a compiler back-end.
2//!
3//! In a basic block (straight-line code) each variable is *live* over a contiguous range of
4//! instructions, so the interference graph — variables that are live at the same time — is an
5//! **interval graph**, which is perfect. Hence the minimum number of registers needed is exactly the
6//! **register pressure**: the most variables simultaneously live (the largest clique). If that fits
7//! the physical register count, a one-sweep `interval_sched` colouring assigns registers; if not,
8//! the over-pressure point yields `R+1` mutually-live variables — a clique that provably cannot share
9//! `R` registers, so at least one *must* spill. The allocation is re-checkable, and the spill is
10//! certified by that clique (a Hall/pigeonhole witness) — no trusted solver, and far faster than
11//! throwing the colouring at a general SAT/SMT solver.
12
13use crate::interval_sched::{peak_concurrency, schedule_or_overflow, Interval, ScheduleOutcome};
14
15/// A variable's live range over instruction positions `[start, end)`.
16#[derive(Clone, Copy, Debug, PartialEq, Eq)]
17pub struct LiveRange {
18    /// The variable identifier.
19    pub var: usize,
20    /// First instruction the variable is live.
21    pub start: i64,
22    /// One past the last instruction the variable is live.
23    pub end: i64,
24}
25
26impl LiveRange {
27    /// Construct a live range for `var` over `[start, end)`.
28    pub fn new(var: usize, start: i64, end: i64) -> Self {
29        LiveRange { var, start, end }
30    }
31}
32
33/// The result of allocating physical registers to a basic block.
34#[derive(Clone, Debug, PartialEq, Eq)]
35pub enum Allocation {
36    /// Success: `(var, register)` pairs, every register in `0..registers`, no two
37    /// simultaneously-live variables sharing one (re-checkable via [`is_valid_allocation`]).
38    Allocated(Vec<(usize, usize)>),
39    /// Spilling is unavoidable: the block needs `pressure` registers, and `must_spill` is a set of
40    /// pairwise-live variables larger than the register count — a certified proof that they cannot
41    /// all be kept in registers (re-checkable via [`is_spill_certificate`]).
42    Spill {
43        /// Minimum registers the block requires (peak simultaneous liveness).
44        pressure: usize,
45        /// `> registers` mutually-live variables — the spill certificate.
46        must_spill: Vec<usize>,
47    },
48}
49
50/// The register pressure of a block: the most variables live at once (the fewest registers needed).
51pub fn register_pressure(ranges: &[LiveRange]) -> usize {
52    let tasks: Vec<Interval> = ranges.iter().map(|r| Interval::new(r.start, r.end)).collect();
53    peak_concurrency(&tasks)
54}
55
56/// Allocate `registers` physical registers to a basic block's `ranges`, or certify that spilling is
57/// unavoidable. O(n log n) via the interval sweep.
58pub fn allocate(ranges: &[LiveRange], registers: usize) -> Allocation {
59    let tasks: Vec<Interval> = ranges.iter().map(|r| Interval::new(r.start, r.end)).collect();
60    match schedule_or_overflow(&tasks, registers) {
61        ScheduleOutcome::Feasible(reg) => {
62            Allocation::Allocated(ranges.iter().zip(reg).map(|(r, m)| (r.var, m)).collect())
63        }
64        ScheduleOutcome::Infeasible(positions) => Allocation::Spill {
65            pressure: peak_concurrency(&tasks),
66            must_spill: positions.iter().map(|&i| ranges[i].var).collect(),
67        },
68    }
69}
70
71fn live_overlap(a: &LiveRange, b: &LiveRange) -> bool {
72    a.start < b.end && b.start < a.end
73}
74
75/// Re-check an allocation: every variable is assigned a register `< registers`, and no two
76/// simultaneously-live variables share one.
77pub fn is_valid_allocation(ranges: &[LiveRange], registers: usize, reg_of: &[(usize, usize)]) -> bool {
78    if reg_of.len() != ranges.len() {
79        return false;
80    }
81    let reg: std::collections::HashMap<usize, usize> = reg_of.iter().copied().collect();
82    if reg.len() != ranges.len() || reg.values().any(|&r| r >= registers) {
83        return false;
84    }
85    for i in 0..ranges.len() {
86        for j in (i + 1)..ranges.len() {
87            if live_overlap(&ranges[i], &ranges[j])
88                && reg.get(&ranges[i].var) == reg.get(&ranges[j].var)
89            {
90                return false;
91            }
92        }
93    }
94    true
95}
96
97/// Re-check a spill certificate: the listed variables pairwise interfere (are mutually live) and
98/// number more than `registers` — so they cannot all reside in registers at once.
99pub fn is_spill_certificate(ranges: &[LiveRange], registers: usize, must_spill: &[usize]) -> bool {
100    if must_spill.len() <= registers {
101        return false;
102    }
103    let by_var: std::collections::HashMap<usize, &LiveRange> =
104        ranges.iter().map(|r| (r.var, r)).collect();
105    must_spill.iter().enumerate().all(|(a, v)| {
106        by_var.get(v).is_some_and(|rv| {
107            must_spill
108                .iter()
109                .skip(a + 1)
110                .all(|u| by_var.get(u).is_some_and(|ru| live_overlap(rv, ru)))
111        })
112    })
113}
114
115#[cfg(test)]
116mod tests {
117    use super::*;
118
119    fn lr(var: usize, start: i64, end: i64) -> LiveRange {
120        LiveRange::new(var, start, end)
121    }
122
123    #[test]
124    fn low_pressure_block_allocates() {
125        //  v0:[0,2)  v1:[1,3)  v2:[3,5)  — peak pressure 2 (v0,v1 overlap), 2 registers fit.
126        let ranges = vec![lr(0, 0, 2), lr(1, 1, 3), lr(2, 3, 5)];
127        assert_eq!(register_pressure(&ranges), 2);
128        match allocate(&ranges, 2) {
129            Allocation::Allocated(reg_of) => {
130                assert!(is_valid_allocation(&ranges, 2, &reg_of), "{reg_of:?}")
131            }
132            o => panic!("expected Allocated, got {o:?}"),
133        }
134    }
135
136    #[test]
137    fn high_pressure_block_must_spill_with_a_clique() {
138        // Four variables all live across instruction 2; only 3 registers ⇒ one must spill.
139        let ranges = vec![lr(0, 0, 5), lr(1, 1, 6), lr(2, 2, 7), lr(3, 2, 8)];
140        assert_eq!(register_pressure(&ranges), 4);
141        match allocate(&ranges, 3) {
142            Allocation::Spill { pressure, must_spill } => {
143                assert_eq!(pressure, 4, "needs 4 registers");
144                assert!(must_spill.len() > 3, "clique must exceed register count");
145                assert!(is_spill_certificate(&ranges, 3, &must_spill), "{must_spill:?}");
146            }
147            o => panic!("expected Spill, got {o:?}"),
148        }
149    }
150
151    #[test]
152    fn exactly_at_pressure_fits() {
153        // Pressure 3, exactly 3 registers ⇒ allocates (no spill).
154        let ranges = vec![lr(0, 0, 5), lr(1, 1, 6), lr(2, 2, 7)];
155        assert_eq!(register_pressure(&ranges), 3);
156        assert!(matches!(allocate(&ranges, 3), Allocation::Allocated(_)));
157    }
158
159    #[test]
160    fn matches_pressure_oracle_on_random_blocks() {
161        let mut s: u64 = 0xC2B2AE3D27D4EB4F;
162        let mut next = || {
163            s ^= s << 13;
164            s ^= s >> 7;
165            s ^= s << 17;
166            s
167        };
168        for _ in 0..500 {
169            let n = (next() % 10) as usize + 1;
170            let registers = (next() % 5) as usize + 1;
171            let ranges: Vec<LiveRange> = (0..n)
172                .map(|v| {
173                    let a = (next() % 12) as i64;
174                    let len = (next() % 6) as i64 + 1;
175                    lr(v, a, a + len)
176                })
177                .collect();
178            let pressure = register_pressure(&ranges);
179            match allocate(&ranges, registers) {
180                Allocation::Allocated(reg_of) => {
181                    assert!(pressure <= registers, "Allocated but pressure {pressure} > {registers}");
182                    assert!(is_valid_allocation(&ranges, registers, &reg_of), "invalid: {reg_of:?}");
183                }
184                Allocation::Spill { pressure: p, must_spill } => {
185                    assert!(pressure > registers, "Spill but pressure {pressure} ≤ {registers}");
186                    assert_eq!(p, pressure);
187                    assert!(is_spill_certificate(&ranges, registers, &must_spill), "{must_spill:?}");
188                }
189            }
190        }
191    }
192
193    #[test]
194    fn robustness_edge_cases() {
195        // Single variable fits one register.
196        assert!(matches!(allocate(&[lr(0, 0, 5)], 1), Allocation::Allocated(_)));
197        // Disjoint live ranges all share one register.
198        let disjoint = vec![lr(0, 0, 2), lr(1, 2, 4), lr(2, 4, 6)];
199        assert_eq!(register_pressure(&disjoint), 1);
200        match allocate(&disjoint, 1) {
201            Allocation::Allocated(a) => assert!(is_valid_allocation(&disjoint, 1, &a), "{a:?}"),
202            o => panic!("disjoint ranges fit 1 register: {o:?}"),
203        }
204        // Six identical ranges all mutually interfere → spill over 4 registers, clique of 5.
205        let same: Vec<LiveRange> = (0..6).map(|v| lr(v, 0, 10)).collect();
206        assert_eq!(register_pressure(&same), 6);
207        match allocate(&same, 4) {
208            Allocation::Spill { pressure, must_spill } => {
209                assert_eq!(pressure, 6);
210                assert_eq!(must_spill.len(), 5, "the first overflow is registers+1 wide");
211                assert!(is_spill_certificate(&same, 4, &must_spill));
212            }
213            o => panic!("6 identical ranges over 4 registers must spill: {o:?}"),
214        }
215        // Huge coordinates do not overflow the sweep.
216        let big = vec![lr(0, 0, i64::MAX / 2), lr(1, 1, i64::MAX / 2)];
217        assert_eq!(register_pressure(&big), 2);
218        assert!(matches!(allocate(&big, 2), Allocation::Allocated(_)));
219        // Zero variables is trivially allocatable.
220        assert!(matches!(allocate(&[], 3), Allocation::Allocated(_)));
221    }
222
223    #[test]
224    fn a_bad_spill_certificate_is_rejected() {
225        let ranges = vec![lr(0, 0, 5), lr(1, 1, 6), lr(2, 2, 7), lr(3, 2, 8)];
226        assert!(!is_spill_certificate(&ranges, 3, &[0, 1]), "two vars don't exceed 3 registers");
227        // v2 and a far-future non-overlapping var would not pairwise interfere.
228        assert!(is_spill_certificate(&ranges, 3, &[0, 1, 2, 3]), "all four are mutually live");
229    }
230}