Skip to main content

logicaffeine_web/ui/pages/
register_alloc_viz.rs

1//! Studio easter egg — certified linear-scan register allocation, visualised.
2//!
3//! Pure data → SVG (no Z3, no JS): a basic block's variable live ranges are laid on an instruction
4//! timeline as bars, coloured by the physical register they're assigned. When the block is
5//! over-pressure, the bars that provably must spill are flagged red and the certified Hall/clique
6//! witness is reported. The allocation comes straight from the certified `register_alloc` engine,
7//! so the picture is a faithful view of a re-checkable result — the same engine that crushes Z3 on
8//! the colouring encoding, here rendered for a one-glance "watch the allocator decide" demo.
9
10use logicaffeine_proof::register_alloc::{allocate, register_pressure, Allocation, LiveRange};
11
12/// A parsed register-allocation spec: named variable live ranges + the physical register budget.
13pub struct RegSpec {
14    /// Display name per variable index.
15    pub names: Vec<String>,
16    /// Live ranges (variable index matches `names`).
17    pub ranges: Vec<LiveRange>,
18    /// Physical register budget.
19    pub registers: usize,
20}
21
22/// Parse a spec of the form (one `name: start-end` per line, plus a `registers: N` budget):
23/// ```text
24/// ## Register Allocation
25/// registers: 3
26/// a: 0-5
27/// b: 1-6
28/// ```
29/// Returns `None` if there is no register budget or no live ranges.
30pub fn parse_register_spec(spec: &str) -> Option<RegSpec> {
31    let mut registers: Option<usize> = None;
32    let mut names = Vec::new();
33    let mut ranges = Vec::new();
34    for raw in spec.lines() {
35        let line = raw.trim();
36        if line.is_empty() || line.starts_with('#') {
37            continue;
38        }
39        if let Some(rest) = line
40            .strip_prefix("registers:")
41            .or_else(|| line.strip_prefix("Registers:"))
42        {
43            // Only accept a well-formed budget; never clobber a valid one with a malformed line.
44            if let Ok(r) = rest.trim().parse::<usize>() {
45                registers = Some(r);
46            }
47            continue;
48        }
49        if let Some((name, rng)) = line.split_once(':') {
50            if let Some((s, e)) = rng.trim().split_once('-') {
51                if let (Ok(start), Ok(end)) = (s.trim().parse::<i64>(), e.trim().parse::<i64>()) {
52                    if start < end {
53                        let var = names.len();
54                        names.push(name.trim().to_string());
55                        ranges.push(LiveRange::new(var, start, end));
56                    }
57                }
58            }
59        }
60    }
61    let registers = registers?;
62    if ranges.is_empty() {
63        return None;
64    }
65    Some(RegSpec { names, ranges, registers })
66}
67
68/// Whether `spec` is a register-allocation spec (a `registers:` budget plus at least one live range).
69/// This is the single source of truth the Studio uses to route Hardware-mode input to the allocator
70/// easter egg, so the wiring (panel visibility, output panel, viz panel) can never drift from what
71/// the parser actually accepts.
72pub fn is_register_alloc_spec(spec: &str) -> bool {
73    parse_register_spec(spec).is_some()
74}
75
76const REG_COLORS: &[&str] = &[
77    "#61afef", "#98c379", "#e5c07b", "#c678dd", "#56b6c2", "#d19a66", "#56c2a8", "#b3a0ff",
78];
79
80/// A deterministic linear-scan assignment: variable index `i` → the physical register it is given, or
81/// `None` if linear-scan must spill it (no register was free when it became live). This is the data
82/// the animated view replays — each variable becomes one bar that lights up in its register's lane
83/// while it is live. Re-checkable via [`is_valid_linear_scan`]. It spills *nothing* exactly when the
84/// block fits the budget (peak pressure ≤ `registers`), so it agrees with the certified [`allocate`]
85/// decision while also showing *which* value gets evicted when it does not.
86pub fn linear_scan_assignment(spec: &RegSpec) -> Vec<Option<usize>> {
87    let n = spec.ranges.len();
88    let mut assignment = vec![None; n];
89    // Visit variables in the order they become live (ties: the one that dies first).
90    let mut order: Vec<usize> = (0..n).collect();
91    order.sort_by_key(|&i| (spec.ranges[i].start, spec.ranges[i].end));
92    // The register file: slot `r` currently holds `Some(variable)`, or `None` when free.
93    let mut slots: Vec<Option<usize>> = vec![None; spec.registers];
94    for &i in &order {
95        let start = spec.ranges[i].start;
96        // Free every register whose occupant has already died — `[start, end)` is half-open, so an
97        // occupant with `end <= start` no longer overlaps the arriving variable.
98        for slot in slots.iter_mut() {
99            if let Some(occ) = *slot {
100                if spec.ranges[occ].end <= start {
101                    *slot = None;
102                }
103            }
104        }
105        // Take the lowest-numbered free register; if every slot is held, this variable spills.
106        if let Some(free) = slots.iter().position(Option::is_none) {
107            slots[free] = Some(i);
108            assignment[i] = Some(free);
109        }
110    }
111    assignment
112}
113
114/// Re-check a linear-scan assignment: every assigned register is within budget, and no two variables
115/// that are live at the same time are given the same register — the soundness property any register
116/// allocation must satisfy. (Spilled variables, `None`, impose no constraint: they live in memory.)
117pub fn is_valid_linear_scan(spec: &RegSpec, assignment: &[Option<usize>]) -> bool {
118    if assignment.len() != spec.ranges.len() {
119        return false;
120    }
121    if assignment.iter().flatten().any(|&r| r >= spec.registers) {
122        return false;
123    }
124    for i in 0..spec.ranges.len() {
125        for j in (i + 1)..spec.ranges.len() {
126            let (a, b) = (spec.ranges[i], spec.ranges[j]);
127            let overlap = a.start < b.end && b.start < a.end;
128            if overlap && assignment[i].is_some() && assignment[i] == assignment[j] {
129                return false;
130            }
131        }
132    }
133    true
134}
135
136/// Register pressure sampled at each instruction in `tmin..tmax`: `profile[i]` is how many variables
137/// are live at instruction `tmin + i`. Its maximum is the register pressure (the fewest registers the
138/// block could ever need), which is what the budget line is drawn against — wherever the profile rises
139/// above the budget, spilling is unavoidable.
140pub fn pressure_profile(spec: &RegSpec) -> Vec<usize> {
141    let tmin = spec.ranges.iter().map(|r| r.start).min().unwrap_or(0);
142    let tmax = spec.ranges.iter().map(|r| r.end).max().unwrap_or(0);
143    (tmin..tmax)
144        .map(|t| spec.ranges.iter().filter(|r| r.start <= t && t < r.end).count())
145        .collect()
146}
147
148/// The interference graph's edges: every pair of variables `(i, j)` (`i < j`) whose live ranges
149/// overlap, so they cannot share a register. Register allocation is exactly colouring this graph; for
150/// straight-line code it is an interval graph (perfect), so its chromatic number equals the largest
151/// clique equals the register pressure.
152pub fn interference_edges(spec: &RegSpec) -> Vec<(usize, usize)> {
153    let mut edges = Vec::new();
154    for i in 0..spec.ranges.len() {
155        for j in (i + 1)..spec.ranges.len() {
156            let (a, b) = (spec.ranges[i], spec.ranges[j]);
157            if a.start < b.end && b.start < a.end {
158                edges.push((i, j));
159            }
160        }
161    }
162    edges
163}
164
165/// A looping SMIL opacity pulse: dim outside the live window `[t0, t1]` (both normalised to `0..1` of
166/// the animation cycle), bright inside it, with a fast snap at each edge — this is what makes a value
167/// "light up" in its register lane exactly while the sweep line is over it.
168fn pulse(t0: f64, t1: f64, dur: f64, dim: f64, bright: f64) -> String {
169    let a1 = t0.clamp(0.006, 0.99);
170    let a0 = (a1 - 0.004).max(0.002);
171    let b0 = t1.clamp(0.006, 0.99).max(a1 + 0.002);
172    let b1 = (b0 + 0.004).min(0.996);
173    format!(
174        r#"<animate attributeName="opacity" dur="{dur:.2}s" repeatCount="indefinite" keyTimes="0;{a0:.4};{a1:.4};{b0:.4};{b1:.4};1" values="{dim};{dim};{bright};{bright};{dim};{dim}"/>"#
175    )
176}
177
178/// Render the certified allocation of `spec` as `(svg, verdict)`. The SVG is an animated linear-scan:
179/// a live-range timeline above an animated register file, with a sweep line crossing both so you can
180/// watch each value claim and release its register (and, when over capacity, drop to a spill lane).
181/// The verdict is the plain-language, certified summary.
182pub fn render(spec: &RegSpec) -> (String, String) {
183    use std::fmt::Write as _;
184    let alloc = allocate(&spec.ranges, spec.registers);
185    let pressure = register_pressure(&spec.ranges);
186    let assignment = linear_scan_assignment(spec);
187    let spilled_any = assignment.iter().any(Option::is_none);
188    let n = spec.ranges.len();
189    let tmin = spec.ranges.iter().map(|r| r.start).min().unwrap_or(0);
190    let tmax = spec.ranges.iter().map(|r| r.end).max().unwrap_or(1);
191    let span = (tmax - tmin).max(1) as f64;
192
193    // Geometry: a variable timeline stacked above a register-file panel, sharing the instruction axis.
194    let left = 86.0_f64;
195    let plot_w = 462.0_f64;
196    let x = |t: i64| left + (t - tmin) as f64 / span * plot_w;
197    let nt = |t: i64| (t - tmin) as f64 / span; // normalised 0..1 for animation timing
198
199    let top1 = 46.0_f64;
200    let trow_h = 22.0_f64;
201    let tl_bottom = top1 + n as f64 * trow_h;
202    let rf_label_y = tl_bottom + 20.0;
203    let top_rf = tl_bottom + 30.0;
204    let lane_h = 24.0_f64;
205    let lanes = spec.registers + usize::from(spilled_any);
206    let rf_bottom = top_rf + lanes as f64 * lane_h;
207
208    // Pressure histogram panel: how many values are live at each instruction, against the budget line.
209    let profile = pressure_profile(spec);
210    let max_p = profile.iter().copied().max().unwrap_or(0).max(spec.registers).max(1);
211    let pr_label_y = rf_bottom + 20.0;
212    let pr_top = rf_bottom + 28.0;
213    let pr_h = 56.0_f64;
214    let pr_bottom = pr_top + pr_h;
215
216    // Interference-graph panel: the graph register allocation is really colouring.
217    let graph_label_y = pr_bottom + 36.0;
218    let graph_top = pr_bottom + 44.0;
219    let graph_h = 188.0_f64;
220    let gcx = 300.0_f64;
221    let gcy = graph_top + graph_h / 2.0;
222    let gradius = (graph_h / 2.0 - 30.0).min(120.0);
223    let legend_y = graph_top + graph_h + 2.0;
224
225    let height = legend_y + 16.0;
226    // One animation loop sweeps the whole block; tie its length to the program length, within reason.
227    let dur = (span * 0.6).clamp(4.0, 12.0);
228
229    let mut s = String::new();
230    let _ = write!(
231        s,
232        r#"<svg viewBox="0 0 600 {height:.0}" xmlns="http://www.w3.org/2000/svg" font-family="ui-monospace, monospace" font-size="12">"#
233    );
234    let _ = write!(s, r##"<rect x="0" y="0" width="600" height="{height:.0}" fill="#0f1117"/>"##);
235    let _ = write!(
236        s,
237        r##"<text x="12" y="24" fill="#e5e7eb" font-size="14">Register allocation · {} registers · pressure {pressure}{}</text>"##,
238        spec.registers,
239        if spilled_any { "  \u{26A0} over capacity" } else { "" }
240    );
241
242    // Faint instruction gridlines + tick labels spanning every panel.
243    let grid_top = top1 - 6.0;
244    let grid_bot = pr_bottom + 2.0;
245    let mut t = tmin;
246    while t <= tmax {
247        let gx = x(t);
248        let _ = write!(s, r##"<line x1="{gx:.1}" y1="{grid_top:.0}" x2="{gx:.1}" y2="{grid_bot:.0}" stroke="#1e2230" stroke-width="1"/>"##);
249        let _ = write!(s, r##"<text x="{gx:.1}" y="{:.0}" fill="#3b4252" font-size="9" text-anchor="middle">{t}</text>"##, grid_bot + 12.0);
250        t += 1;
251    }
252
253    // ---- Timeline panel: one solid bar per variable, coloured by the register it is given. ----
254    for i in 0..n {
255        let r = spec.ranges[i];
256        let y = top1 + i as f64 * trow_h;
257        let _ = write!(s, r##"<text x="10" y="{:.0}" fill="#abb2bf">{}</text>"##, y + 14.0, spec.names[i]);
258        let bx = x(r.start);
259        let bw = (x(r.end) - bx).max(5.0);
260        let (fill, tag, stroke) = match assignment[i] {
261            Some(reg) => (REG_COLORS[reg % REG_COLORS.len()], format!("r{reg}"), ""),
262            None => ("#e06c75", "SPILL".to_string(), r##" stroke="#ff5c66" stroke-width="1.5""##),
263        };
264        let _ = write!(s, r#"<rect x="{bx:.1}" y="{:.0}" width="{bw:.1}" height="16" rx="4" fill="{fill}"{stroke}/>"#, y + 2.0);
265        let _ = write!(s, r##"<text x="{:.1}" y="{:.0}" fill="#10131a" font-size="11">{tag}</text>"##, bx + 4.0, y + 14.0);
266    }
267
268    // ---- Register-file panel: a lane per register (plus MEM for spills); occupants pulse bright as
269    //       the sweep crosses their live range, so you watch each register fill and free. ----
270    let _ = write!(s, r##"<text x="12" y="{rf_label_y:.0}" fill="#7a8290" font-size="11" letter-spacing="0.08em">REGISTER FILE</text>"##);
271    for reg in 0..spec.registers {
272        let y = top_rf + reg as f64 * lane_h;
273        let colour = REG_COLORS[reg % REG_COLORS.len()];
274        let _ = write!(s, r##"<rect x="{left:.0}" y="{:.1}" width="{plot_w:.0}" height="{:.1}" rx="4" fill="#161a24"/>"##, y + 2.0, lane_h - 6.0);
275        let _ = write!(s, r##"<text x="12" y="{:.0}" fill="{colour}">r{reg}</text>"##, y + 16.0);
276        for i in 0..n {
277            if assignment[i] == Some(reg) {
278                let r = spec.ranges[i];
279                let bx = x(r.start);
280                let bw = (x(r.end) - bx).max(5.0);
281                let bar = pulse(nt(r.start), nt(r.end), dur, 0.16, 1.0);
282                let txt = pulse(nt(r.start), nt(r.end), dur, 0.25, 1.0);
283                let _ = write!(s, r#"<rect x="{bx:.1}" y="{:.1}" width="{bw:.1}" height="{:.1}" rx="4" fill="{colour}" opacity="0.16">{bar}</rect>"#, y + 3.0, lane_h - 8.0);
284                let _ = write!(s, r##"<text x="{:.1}" y="{:.0}" fill="#10131a" font-size="11" opacity="0.25">{}{txt}</text>"##, bx + 5.0, y + 16.0, spec.names[i]);
285            }
286        }
287    }
288    if spilled_any {
289        let y = top_rf + spec.registers as f64 * lane_h;
290        let _ = write!(s, r##"<rect x="{left:.0}" y="{:.1}" width="{plot_w:.0}" height="{:.1}" rx="4" fill="#2a1417"/>"##, y + 2.0, lane_h - 6.0);
291        let _ = write!(s, r##"<text x="12" y="{:.0}" fill="#e06c75">MEM</text>"##, y + 16.0);
292        for i in 0..n {
293            if assignment[i].is_none() {
294                let r = spec.ranges[i];
295                let bx = x(r.start);
296                let bw = (x(r.end) - bx).max(5.0);
297                let bar = pulse(nt(r.start), nt(r.end), dur, 0.3, 1.0);
298                let _ = write!(s, r##"<rect x="{bx:.1}" y="{:.1}" width="{bw:.1}" height="{:.1}" rx="4" fill="#e06c75" stroke="#ff5c66" stroke-width="1.2" opacity="0.3">{bar}</rect>"##, y + 3.0, lane_h - 8.0);
299                let _ = write!(s, r##"<text x="{:.1}" y="{:.0}" fill="#2a1417" font-size="11">{} SPILL</text>"##, bx + 5.0, y + 16.0, spec.names[i]);
300            }
301        }
302    }
303
304    // ---- Pressure panel: a per-instruction live-count histogram against the budget line. Bars that
305    //       rise above the budget are red — exactly the instructions at which spilling is forced. ----
306    let _ = write!(s, r##"<text x="12" y="{pr_label_y:.0}" fill="#7a8290" font-size="11" letter-spacing="0.08em">PRESSURE</text>"##);
307    let bar_y = |p: usize| pr_bottom - (p as f64 / max_p as f64) * pr_h;
308    for (i, &p) in profile.iter().enumerate() {
309        let t = tmin + i as i64;
310        let bx = x(t);
311        let bw = (x(t + 1) - bx - 1.0).max(2.0);
312        let y = bar_y(p);
313        let colour = if p > spec.registers { "#ff5c66" } else { "#5aa0e0" };
314        let _ = write!(s, r##"<rect x="{bx:.1}" y="{y:.1}" width="{bw:.1}" height="{:.1}" rx="1.5" fill="{colour}" opacity="0.85"/>"##, pr_bottom - y);
315    }
316    let by = bar_y(spec.registers);
317    let _ = write!(s, r##"<line x1="{left:.0}" y1="{by:.1}" x2="{:.0}" y2="{by:.1}" stroke="#e5c07b" stroke-width="1.2" stroke-dasharray="5 4"/>"##, left + plot_w);
318    let _ = write!(s, r##"<text x="8" y="{:.1}" fill="#e5c07b" font-size="9">budget {}</text>"##, by + 3.0, spec.registers);
319
320    // ---- The sweep line: a translating beam crossing every panel, looping with the occupancy pulses.
321    let beam_top = top1 - 6.0;
322    let beam_bot = pr_bottom + 2.0;
323    let _ = write!(
324        s,
325        r##"<g><rect x="{left:.0}" y="{beam_top:.0}" width="7" height="{:.0}" fill="#f8fafc" opacity="0.10"/><line x1="{left:.0}" y1="{beam_top:.0}" x2="{left:.0}" y2="{beam_bot:.0}" stroke="#f8fafc" stroke-width="2" opacity="0.85"/><animateTransform attributeName="transform" type="translate" from="0 0" to="{plot_w:.0} 0" dur="{dur:.2}s" repeatCount="indefinite"/></g>"##,
326        beam_bot - beam_top
327    );
328
329    // ---- Interference graph: nodes = variables on a ring, edges = overlapping lifetimes; nodes are
330    //       coloured by the register they get, and the certified spill clique (if any) is ringed red.
331    //       A node glows in sync with the sweep while its value is live. ----
332    let clique: std::collections::HashSet<usize> = match &alloc {
333        Allocation::Spill { must_spill, .. } => must_spill.iter().copied().collect(),
334        Allocation::Allocated(_) => Default::default(),
335    };
336    let _ = write!(s, r##"<text x="12" y="{graph_label_y:.0}" fill="#7a8290" font-size="11" letter-spacing="0.08em">INTERFERENCE GRAPH · χ = {pressure} (registers needed)</text>"##);
337    let mut pos: Vec<(f64, f64)> = Vec::with_capacity(n);
338    for i in 0..n {
339        if n <= 1 {
340            pos.push((gcx, gcy));
341        } else {
342            let ang = -std::f64::consts::FRAC_PI_2 + 2.0 * std::f64::consts::PI * i as f64 / n as f64;
343            pos.push((gcx + gradius * ang.cos(), gcy + gradius * ang.sin()));
344        }
345    }
346    for &(i, j) in &interference_edges(spec) {
347        let (x1, y1) = pos[i];
348        let (x2, y2) = pos[j];
349        let (stroke, w, op) = if clique.contains(&i) && clique.contains(&j) {
350            ("#ff5c66", 2.5, 0.9)
351        } else {
352            ("#3a4150", 1.2, 0.55)
353        };
354        let _ = write!(s, r##"<line x1="{x1:.1}" y1="{y1:.1}" x2="{x2:.1}" y2="{y2:.1}" stroke="{stroke}" stroke-width="{w}" opacity="{op}"/>"##);
355    }
356    for i in 0..n {
357        let (px, py) = pos[i];
358        let fill = match assignment[i] {
359            Some(reg) => REG_COLORS[reg % REG_COLORS.len()],
360            None => "#e06c75",
361        };
362        if clique.contains(&i) {
363            let _ = write!(s, r##"<circle cx="{px:.1}" cy="{py:.1}" r="17" fill="none" stroke="#ff5c66" stroke-width="3"/>"##);
364        }
365        let glow = pulse(nt(spec.ranges[i].start), nt(spec.ranges[i].end), dur, 0.5, 1.0);
366        let _ = write!(s, r##"<circle cx="{px:.1}" cy="{py:.1}" r="14" fill="{fill}" stroke="#0f1117" stroke-width="1.5" opacity="0.5">{glow}</circle>"##);
367        let _ = write!(s, r##"<text x="{px:.1}" y="{:.1}" fill="#10131a" font-size="11" text-anchor="middle">{}</text>"##, py + 4.0, spec.names[i]);
368    }
369
370    // ---- Legend ----
371    let mut lx = 12.0_f64;
372    for reg in 0..spec.registers.min(REG_COLORS.len()) {
373        let _ = write!(s, r##"<rect x="{lx:.0}" y="{:.0}" width="10" height="10" rx="2" fill="{}"/>"##, legend_y - 9.0, REG_COLORS[reg]);
374        let _ = write!(s, r##"<text x="{:.0}" y="{legend_y:.0}" fill="#9aa3b2" font-size="10">r{reg}</text>"##, lx + 14.0);
375        lx += 42.0;
376    }
377    if spilled_any {
378        let _ = write!(s, r##"<rect x="{lx:.0}" y="{:.0}" width="10" height="10" rx="2" fill="#e06c75"/>"##, legend_y - 9.0);
379        let _ = write!(s, r##"<text x="{:.0}" y="{legend_y:.0}" fill="#9aa3b2" font-size="10">spill</text>"##, lx + 14.0);
380        lx += 50.0;
381    }
382    let _ = write!(s, r##"<text x="{lx:.0}" y="{legend_y:.0}" fill="#9aa3b2" font-size="10">red clique = cannot share</text>"##);
383
384    let _ = write!(s, "</svg>");
385
386    let verdict = match &alloc {
387        Allocation::Allocated(_) => format!(
388            "\u{2713} Allocated with {} registers — certified: no two simultaneously-live variables share a register.",
389            spec.registers
390        ),
391        Allocation::Spill { pressure, must_spill } => {
392            let names: Vec<&str> = must_spill
393                .iter()
394                .filter_map(|v| spec.names.get(*v).map(String::as_str))
395                .collect();
396            format!(
397                "\u{26A0} Must spill — {pressure} variables are live at once but only {} registers exist. {{{}}} all mutually interfere, so they cannot share {} registers (certified clique).",
398                spec.registers,
399                names.join(", "),
400                spec.registers
401            )
402        }
403    };
404    (s, verdict)
405}
406
407/// A textual allocation report for the Studio output panel: the physical register assigned to each
408/// variable, or — when the block is over-pressure — the spill verdict naming its mutually-interfering
409/// clique. This is the "compiler back-end output" view that pairs with the [`render`] timeline; the
410/// per-variable registers come from the same [`linear_scan_assignment`] the animation replays, and
411/// the feasibility/clique verdict from the certified [`allocate`] result, so the two views agree.
412pub fn allocation_report(spec: &RegSpec) -> String {
413    use std::fmt::Write as _;
414    let mut out = String::new();
415    match allocate(&spec.ranges, spec.registers) {
416        Allocation::Allocated(_) => {
417            let assignment = linear_scan_assignment(spec);
418            let _ = writeln!(
419                out,
420                "Allocated {} variable{} into {} register{} — certified: no two simultaneously-live variables share a register.",
421                spec.names.len(),
422                if spec.names.len() == 1 { "" } else { "s" },
423                spec.registers,
424                if spec.registers == 1 { "" } else { "s" },
425            );
426            for (i, name) in spec.names.iter().enumerate() {
427                if let Some(reg) = assignment.get(i).copied().flatten() {
428                    let _ = writeln!(out, "  {name} \u{2192} r{reg}");
429                }
430            }
431        }
432        Allocation::Spill { pressure, must_spill } => {
433            let clique: Vec<&str> = must_spill
434                .iter()
435                .filter_map(|v| spec.names.get(*v).map(String::as_str))
436                .collect();
437            let _ = writeln!(
438                out,
439                "Must spill — {pressure} variables are live at once but only {} register{} exist.",
440                spec.registers,
441                if spec.registers == 1 { "" } else { "s" },
442            );
443            let _ = write!(
444                out,
445                "Certified clique (all mutually interfere): {}",
446                clique.join(", ")
447            );
448        }
449    }
450    out
451}
452
453#[cfg(test)]
454mod tests {
455    use super::*;
456
457    const FITS: &str = "## Register Allocation\nregisters: 2\na: 0-2\nb: 1-3\nc: 3-5\n";
458    const SPILLS: &str = "registers: 3\nv0: 0-5\nv1: 1-6\nv2: 2-7\nv3: 2-8\n";
459
460    #[test]
461    fn parses_a_spec() {
462        let s = parse_register_spec(FITS).expect("parses");
463        assert_eq!(s.registers, 2);
464        assert_eq!(s.names, vec!["a", "b", "c"]);
465        assert_eq!(s.ranges.len(), 3);
466        assert_eq!((s.ranges[0].start, s.ranges[0].end), (0, 2));
467    }
468
469    #[test]
470    fn rejects_specs_without_a_budget_or_ranges() {
471        assert!(parse_register_spec("a: 0-3\nb: 1-4").is_none(), "no register budget");
472        assert!(parse_register_spec("registers: 2").is_none(), "no ranges");
473    }
474
475    #[test]
476    fn renders_a_feasible_allocation() {
477        let spec = parse_register_spec(FITS).unwrap();
478        let (svg, verdict) = render(&spec);
479        assert!(svg.starts_with("<svg"), "valid SVG");
480        assert!(svg.contains("</svg>"));
481        assert!(svg.contains(">a<") && svg.contains(">c<"), "variable labels present");
482        assert!(verdict.contains("Allocated") && verdict.contains("certified"), "{verdict}");
483        assert!(!verdict.contains("spill") && !verdict.contains("Must spill"));
484    }
485
486    #[test]
487    fn renders_a_spill_with_a_certified_clique() {
488        // 4 variables live at once, 3 registers ⇒ spill, and the verdict names the clique.
489        let spec = parse_register_spec(SPILLS).unwrap();
490        let (svg, verdict) = render(&spec);
491        assert!(svg.contains("SPILL"), "spill bars are flagged");
492        assert!(verdict.contains("Must spill") && verdict.contains("certified clique"), "{verdict}");
493        assert!(verdict.contains("4 variables are live at once"), "reports pressure: {verdict}");
494    }
495
496    #[test]
497    fn parser_is_robust_to_messy_input() {
498        // Comments, blank lines, stray whitespace, out-of-order lines, and a malformed second
499        // `registers:` line that must NOT clobber the valid budget.
500        let messy = "# a basic block\n\n  registers:  2  \n\n a :  0-3 \n  registers: oops\n b: 1 - 4 \n";
501        let spec = parse_register_spec(messy).expect("messy spec still parses");
502        assert_eq!(spec.registers, 2, "valid budget kept despite a later malformed line");
503        assert_eq!(spec.names, vec!["a", "b"]);
504        assert_eq!((spec.ranges[1].start, spec.ranges[1].end), (1, 4));
505        // Degenerate ranges (start >= end) are dropped, not mis-parsed.
506        let with_empty = "registers: 1\nx: 5-5\ny: 0-2\n";
507        let s2 = parse_register_spec(with_empty).expect("parses");
508        assert_eq!(s2.ranges.len(), 1, "empty range x:5-5 dropped");
509        assert_eq!(s2.names, vec!["y"]);
510    }
511
512    #[test]
513    fn recognizes_only_register_alloc_specs() {
514        // Real register-alloc specs route to the easter egg.
515        assert!(is_register_alloc_spec(FITS));
516        assert!(is_register_alloc_spec(SPILLS));
517        // The OTHER Hardware-mode inputs must NOT be hijacked by the allocator:
518        assert!(
519            !is_register_alloc_spec("Always, if request is high, then acknowledge is high."),
520            "an SVA English spec is not a register-alloc spec"
521        );
522        assert!(
523            !is_register_alloc_spec(
524                "module m(input clk); reg a; always @(posedge clk) a <= ~a; assert property (a); endmodule"
525            ),
526            "a Verilog module is not a register-alloc spec"
527        );
528        assert!(
529            !is_register_alloc_spec("ns-through conflicts with ew-through and ew-left."),
530            "a signal-design spec is not a register-alloc spec"
531        );
532        assert!(!is_register_alloc_spec(""), "empty input is not a spec");
533        assert!(
534            !is_register_alloc_spec("registers: 3"),
535            "a budget with no live ranges has nothing to allocate"
536        );
537        assert!(
538            !is_register_alloc_spec("a: 0-4\nb: 1-3"),
539            "live ranges with no budget cannot be allocated"
540        );
541    }
542
543    #[test]
544    fn allocation_report_lists_a_register_per_variable_when_it_fits() {
545        let spec = parse_register_spec(FITS).unwrap();
546        let report = allocation_report(&spec);
547        assert!(report.contains("Allocated"), "{report}");
548        assert!(report.contains("certified"), "{report}");
549        // Every variable is reported with a concrete physical register.
550        for name in &spec.names {
551            assert!(
552                report.contains(&format!("{name} \u{2192} r")),
553                "variable {name} missing its register: {report}"
554            );
555        }
556        assert!(!report.contains("spill") && !report.contains("Must spill"), "{report}");
557    }
558
559    #[test]
560    fn allocation_report_names_the_spill_clique_when_over_pressure() {
561        let spec = parse_register_spec(SPILLS).unwrap();
562        let report = allocation_report(&spec);
563        assert!(report.contains("Must spill"), "{report}");
564        assert!(report.contains("Certified clique"), "{report}");
565        assert!(report.contains("live at once"), "{report}");
566        // Every member of the over-pressure clique is named.
567        for name in &spec.names {
568            assert!(report.contains(name.as_str()), "clique missing {name}: {report}");
569        }
570    }
571
572    #[test]
573    fn report_agrees_with_the_rendered_verdict() {
574        // The textual report and the SVG verdict are two views of the SAME certified result, so
575        // their feasibility must always agree.
576        for text in [FITS, SPILLS] {
577            let spec = parse_register_spec(text).unwrap();
578            let report = allocation_report(&spec);
579            let (_, verdict) = render(&spec);
580            assert_eq!(
581                report.contains("Allocated"),
582                verdict.contains("Allocated"),
583                "report/verdict disagree:\n{report}\n{verdict}"
584            );
585            assert_eq!(report.contains("Must spill"), verdict.contains("Must spill"));
586        }
587    }
588
589    #[test]
590    fn linear_scan_is_valid_and_consistent_with_the_engine() {
591        for text in [FITS, SPILLS] {
592            let spec = parse_register_spec(text).unwrap();
593            let assignment = linear_scan_assignment(&spec);
594            assert!(is_valid_linear_scan(&spec, &assignment), "{assignment:?}");
595            // Linear-scan spills nothing iff the block fits the budget (peak pressure ≤ registers).
596            let fits = register_pressure(&spec.ranges) <= spec.registers;
597            assert_eq!(
598                assignment.iter().all(Option::is_some),
599                fits,
600                "spill-set should be empty iff feasible: {assignment:?}"
601            );
602        }
603    }
604
605    #[test]
606    fn linear_scan_matches_the_engine_on_random_blocks() {
607        let mut s: u64 = 0x9E3779B97F4A7C15;
608        let mut next = || {
609            s ^= s << 13;
610            s ^= s >> 7;
611            s ^= s << 17;
612            s
613        };
614        for _ in 0..500 {
615            let n = (next() % 9) as usize + 1;
616            let registers = (next() % 5) as usize + 1;
617            let names: Vec<String> = (0..n).map(|i| format!("v{i}")).collect();
618            let ranges: Vec<LiveRange> = (0..n)
619                .map(|v| {
620                    let a = (next() % 12) as i64;
621                    let len = (next() % 6) as i64 + 1;
622                    LiveRange::new(v, a, a + len)
623                })
624                .collect();
625            let spec = RegSpec { names, ranges, registers };
626            let assignment = linear_scan_assignment(&spec);
627            // It is ALWAYS a sound, re-checkable allocation...
628            assert!(is_valid_linear_scan(&spec, &assignment), "invalid: {assignment:?}");
629            // ...and it spills exactly when the certified engine says spilling is unavoidable.
630            let engine_fits = matches!(allocate(&spec.ranges, registers), Allocation::Allocated(_));
631            assert_eq!(
632                assignment.iter().all(Option::is_some),
633                engine_fits,
634                "linear-scan/engine feasibility disagree: {assignment:?}"
635            );
636        }
637    }
638
639    #[test]
640    fn is_valid_linear_scan_rejects_bad_assignments() {
641        let spec = parse_register_spec(SPILLS).unwrap(); // v0..v3 all live together at instr 2
642        // Two simultaneously-live variables in the SAME register is unsound.
643        assert!(!is_valid_linear_scan(&spec, &[Some(0), Some(0), Some(1), None]));
644        // A register outside the budget is rejected.
645        assert!(!is_valid_linear_scan(&spec, &[Some(99), Some(1), Some(2), None]));
646        // A wrong-length assignment is rejected.
647        assert!(!is_valid_linear_scan(&spec, &[Some(0)]));
648        // The honest linear-scan result always re-checks as valid.
649        assert!(is_valid_linear_scan(&spec, &linear_scan_assignment(&spec)));
650    }
651
652    #[test]
653    fn animated_render_has_a_moving_sweep_and_register_lanes() {
654        let spec = parse_register_spec(FITS).unwrap();
655        let (svg, _) = render(&spec);
656        assert!(svg.contains("<animate"), "no SMIL animation in the SVG");
657        assert!(
658            svg.contains("animateTransform") && svg.contains("translate"),
659            "no moving sweep line"
660        );
661        assert!(svg.contains("REGISTER FILE"), "no register-file panel");
662        for reg in 0..spec.registers {
663            assert!(svg.contains(&format!(">r{reg}<")), "missing lane r{reg}: {svg}");
664        }
665        assert!(!svg.contains("MEM"), "a feasible block must not show a spill lane");
666    }
667
668    #[test]
669    fn animated_render_flags_a_spill_in_a_memory_lane() {
670        let spec = parse_register_spec(SPILLS).unwrap();
671        let (svg, _) = render(&spec);
672        assert!(svg.contains("<animate"), "the spill view is still animated");
673        assert!(svg.contains(">MEM<"), "no spill (MEM) lane");
674        assert!(svg.contains("SPILL"), "spilled value not flagged");
675    }
676
677    #[test]
678    fn pressure_profile_peaks_at_the_register_pressure() {
679        // The histogram's tallest bar IS the certified register pressure, on the examples...
680        for text in [FITS, SPILLS] {
681            let spec = parse_register_spec(text).unwrap();
682            let profile = pressure_profile(&spec);
683            assert_eq!(
684                profile.iter().copied().max().unwrap_or(0),
685                register_pressure(&spec.ranges),
686                "profile peak must equal register pressure: {profile:?}"
687            );
688        }
689        // ...a hand-checked instance: a[0,2) b[1,3) overlap at instr 1, c[3,5) disjoint.
690        let spec = parse_register_spec("registers: 2\na: 0-2\nb: 1-3\nc: 3-5").unwrap();
691        // a live at {0,1}, b live at {1,2}, c live at {3,4} (half-open [start,end)).
692        assert_eq!(pressure_profile(&spec), vec![1, 2, 1, 1, 1]);
693    }
694
695    #[test]
696    fn pressure_profile_peak_matches_engine_on_random_blocks() {
697        let mut s: u64 = 0xD1B54A32D192ED03;
698        let mut next = || {
699            s ^= s << 13;
700            s ^= s >> 7;
701            s ^= s << 17;
702            s
703        };
704        for _ in 0..500 {
705            let n = (next() % 9) as usize + 1;
706            let names: Vec<String> = (0..n).map(|i| format!("v{i}")).collect();
707            let ranges: Vec<LiveRange> = (0..n)
708                .map(|v| {
709                    let a = (next() % 12) as i64;
710                    let len = (next() % 6) as i64 + 1;
711                    LiveRange::new(v, a, a + len)
712                })
713                .collect();
714            let spec = RegSpec { names, ranges, registers: 3 };
715            assert_eq!(
716                pressure_profile(&spec).iter().copied().max().unwrap_or(0),
717                register_pressure(&spec.ranges),
718            );
719        }
720    }
721
722    #[test]
723    fn render_has_a_pressure_panel_with_a_budget_line() {
724        let spec = parse_register_spec(FITS).unwrap();
725        let (svg, _) = render(&spec);
726        assert!(svg.contains("PRESSURE"), "no pressure panel");
727        assert!(svg.contains("budget"), "no budget label");
728        assert!(svg.contains("stroke-dasharray"), "budget line is not dashed");
729        // A feasible block never exceeds its budget, so nothing is painted over-capacity red.
730        assert!(!svg.contains("#ff5c66"), "feasible block must not show an over-capacity marker");
731    }
732
733    #[test]
734    fn render_paints_over_capacity_pressure_red_when_spilling() {
735        let spec = parse_register_spec(SPILLS).unwrap();
736        let (svg, _) = render(&spec);
737        assert!(svg.contains("PRESSURE"), "no pressure panel");
738        // Over-pressure instructions are flagged red.
739        assert!(svg.contains("#ff5c66"), "spill block must show an over-capacity marker");
740    }
741
742    #[test]
743    fn interference_edges_finds_overlapping_pairs() {
744        // a[0,2) overlaps b[1,3); c[3,5) overlaps neither — one edge.
745        let fits = parse_register_spec(FITS).unwrap();
746        assert_eq!(interference_edges(&fits), vec![(0, 1)]);
747        // v0..v3 all share instruction 2 — a complete graph K4 (6 edges).
748        let spills = parse_register_spec(SPILLS).unwrap();
749        assert_eq!(interference_edges(&spills).len(), 6);
750    }
751
752    #[test]
753    fn interference_edges_are_symmetric_with_overlap_on_random_blocks() {
754        let mut s: u64 = 0x2545F4914F6CDD1D;
755        let mut next = || {
756            s ^= s << 13;
757            s ^= s >> 7;
758            s ^= s << 17;
759            s
760        };
761        for _ in 0..300 {
762            let n = (next() % 7) as usize + 1;
763            let names: Vec<String> = (0..n).map(|i| format!("v{i}")).collect();
764            let ranges: Vec<LiveRange> = (0..n)
765                .map(|v| {
766                    let a = (next() % 10) as i64;
767                    let len = (next() % 5) as i64 + 1;
768                    LiveRange::new(v, a, a + len)
769                })
770                .collect();
771            let spec = RegSpec { names, ranges, registers: 3 };
772            let edges = interference_edges(&spec);
773            // Every edge is a genuine overlap and i<j; and a clique cannot exceed the pressure.
774            for &(i, j) in &edges {
775                assert!(i < j);
776                let (a, b) = (spec.ranges[i], spec.ranges[j]);
777                assert!(a.start < b.end && b.start < a.end, "non-overlap edge {i}-{j}");
778            }
779        }
780    }
781
782    #[test]
783    fn render_draws_the_interference_graph() {
784        let spec = parse_register_spec(FITS).unwrap();
785        let (svg, _) = render(&spec);
786        assert!(svg.contains("INTERFERENCE GRAPH"), "no interference-graph panel");
787        assert!(svg.contains("χ ="), "no chromatic-number label");
788        assert!(svg.contains("<circle"), "no graph nodes");
789        // A feasible block has no certified clique, so no clique ring is drawn.
790        assert!(!svg.contains("stroke-width=\"3\""), "feasible block must not ring a clique");
791    }
792
793    #[test]
794    fn render_rings_the_spill_clique_in_the_graph() {
795        let spec = parse_register_spec(SPILLS).unwrap();
796        let (svg, _) = render(&spec);
797        assert!(svg.contains("INTERFERENCE GRAPH"));
798        // The certified clique nodes are ringed (unique stroke-width="3").
799        assert!(svg.contains("stroke-width=\"3\""), "spill clique not ringed in the graph");
800    }
801
802    #[test]
803    fn pulse_keytimes_are_strictly_increasing_for_every_window() {
804        // The SMIL keyTimes MUST be strictly monotonic in [0,1] or the browser drops the animation;
805        // sweep every possible normalised live window and parse the emitted keyTimes back out.
806        for a in 0..=20 {
807            for b in (a + 1)..=21 {
808                let (t0, t1) = (a as f64 / 21.0, b as f64 / 21.0);
809                let anim = pulse(t0, t1, 6.0, 0.16, 1.0);
810                let kt = anim
811                    .split("keyTimes=\"")
812                    .nth(1)
813                    .and_then(|s| s.split('"').next())
814                    .expect("has keyTimes");
815                let times: Vec<f64> = kt.split(';').map(|x| x.parse().unwrap()).collect();
816                assert_eq!(times.len(), 6, "{anim}");
817                assert_eq!(times[0], 0.0);
818                assert_eq!(*times.last().unwrap(), 1.0);
819                for w in times.windows(2) {
820                    assert!(w[0] < w[1], "keyTimes not increasing at {t0}..{t1}: {kt}");
821                }
822            }
823        }
824    }
825
826    #[test]
827    fn verdict_tracks_the_certified_engine() {
828        // Feasibility in the rendered verdict must agree with the engine's pressure check.
829        for spec_text in [FITS, SPILLS] {
830            let spec = parse_register_spec(spec_text).unwrap();
831            let fits = register_pressure(&spec.ranges) <= spec.registers;
832            let (_, verdict) = render(&spec);
833            assert_eq!(verdict.contains("Allocated"), fits, "{verdict}");
834        }
835    }
836}