1use logicaffeine_proof::register_alloc::{allocate, register_pressure, Allocation, LiveRange};
11
12pub struct RegSpec {
14 pub names: Vec<String>,
16 pub ranges: Vec<LiveRange>,
18 pub registers: usize,
20}
21
22pub 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 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
68pub 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
80pub fn linear_scan_assignment(spec: &RegSpec) -> Vec<Option<usize>> {
87 let n = spec.ranges.len();
88 let mut assignment = vec![None; n];
89 let mut order: Vec<usize> = (0..n).collect();
91 order.sort_by_key(|&i| (spec.ranges[i].start, spec.ranges[i].end));
92 let mut slots: Vec<Option<usize>> = vec![None; spec.registers];
94 for &i in &order {
95 let start = spec.ranges[i].start;
96 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 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
114pub 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
136pub 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
148pub 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
165fn 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
178pub 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 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; 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 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 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 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 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 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 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 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 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 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 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
407pub 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 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 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 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 assert!(is_register_alloc_spec(FITS));
516 assert!(is_register_alloc_spec(SPILLS));
517 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 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 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 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 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 assert!(is_valid_linear_scan(&spec, &assignment), "invalid: {assignment:?}");
629 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(); assert!(!is_valid_linear_scan(&spec, &[Some(0), Some(0), Some(1), None]));
644 assert!(!is_valid_linear_scan(&spec, &[Some(99), Some(1), Some(2), None]));
646 assert!(!is_valid_linear_scan(&spec, &[Some(0)]));
648 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 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 let spec = parse_register_spec("registers: 2\na: 0-2\nb: 1-3\nc: 3-5").unwrap();
691 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 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 assert!(svg.contains("#ff5c66"), "spill block must show an over-capacity marker");
740 }
741
742 #[test]
743 fn interference_edges_finds_overlapping_pairs() {
744 let fits = parse_register_spec(FITS).unwrap();
746 assert_eq!(interference_edges(&fits), vec![(0, 1)]);
747 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 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 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 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 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 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}