Skip to main content

launchbound_report/
render.rs

1//! Text rendering. Legible to someone who has never used reconverge: rule
2//! IDs come with the source span and a plain-language reason.
3
4use crate::Report;
5use std::fmt::Write;
6
7/// Kept in the report crate so rendering cannot compile without it.
8pub fn launchbound_metal_notice() -> &'static str {
9    "NO convergence gate exists on the Metal path: the same bug class is NOT checked"
10}
11
12/// Render a report as plain text for a terminal.
13///
14/// The no-gate notice is emitted unconditionally when
15/// `convergence_gate` is `none`, and a test asserts it cannot be omitted:
16/// a Metal report that looked like a CUDA report would be the most
17/// dangerous output this tool could produce.
18pub fn render_text(report: &Report) -> String {
19    let mut out = String::new();
20    let device = report
21        .device
22        .as_ref()
23        .map(|d| format!("{} (cc {}, driver {})", d.name, d.cc, d.driver_version))
24        .unwrap_or_else(|| "no device — nothing measured yet".into());
25    let _ = writeln!(
26        out,
27        "launchbound report — {} · gate cc {} · {} · {}",
28        report.kernel, report.gate_cc, report.measurement_kind, device
29    );
30    // Unconditional whenever the gate did not run: the Metal asymmetry is
31    // published, never buried (docs/LIMITATIONS.md). Do not add a way to skip this.
32    if report.convergence_gate == "none" {
33        let _ = writeln!(out, "\n*** {} ***", launchbound_metal_notice());
34    }
35
36    match &report.chosen {
37        Some(chosen) => {
38            let s = &chosen.summary;
39            let _ = writeln!(
40                out,
41                "\nCHOSEN: {}  {}\n  {:.4} ms  [{:.4}, {:.4}]  (n={}, {} outliers rejected)",
42                chosen.id,
43                chosen.config,
44                s.median_ms,
45                s.ci95_lo_ms,
46                s.ci95_hi_ms,
47                s.n,
48                s.outliers_rejected
49            );
50            if !report.indistinguishable_from_chosen.is_empty() {
51                let _ = writeln!(
52                    out,
53                    "  statistically indistinguishable from: {}",
54                    report.indistinguishable_from_chosen.join(", ")
55                );
56            }
57        }
58        None => {
59            let _ = writeln!(
60                out,
61                "\nCHOSEN: none — no admitted candidate has a measurement"
62            );
63        }
64    }
65
66    if !report.rejected_faster.is_empty() {
67        let _ = writeln!(
68            out,
69            "\nREFUSED BUT FASTER — an autotuner without a convergence gate would have\nhanded you one of these:"
70        );
71        for r in &report.rejected_faster {
72            let s = &r.summary;
73            let _ = writeln!(
74                out,
75                "  {}  {}\n    {:.4} ms  [{:.4}, {:.4}]  — {:.2}x faster than the chosen config",
76                r.id, r.config, s.median_ms, s.ci95_lo_ms, s.ci95_hi_ms, r.speedup_vs_chosen
77            );
78            for rule in &r.rules {
79                let _ = writeln!(
80                    out,
81                    "    REFUSED {} at {}: {}",
82                    rule.rule,
83                    rule.span.as_deref().unwrap_or("<no span>"),
84                    rule.reason
85                );
86            }
87        }
88        if let Some(reason) = &report.allow_unsafe_reason {
89            let _ = writeln!(
90                out,
91                "    (measured under --allow-unsafe; recorded reason: {reason:?})"
92            );
93        }
94    }
95
96    let _ = writeln!(out, "\nALL CANDIDATES:");
97    for c in &report.candidates {
98        let timing = match (&c.summary, c.measurement_status.as_str()) {
99            (Some(s), "ok") => format!(
100                "{:.4} ms [{:.4}, {:.4}]",
101                s.median_ms, s.ci95_lo_ms, s.ci95_hi_ms
102            ),
103            (_, "timeout") => "TIMED OUT (presumed hung — the failure the gate predicts)".into(),
104            (_, "error") => format!("error: {}", c.measurement_error.as_deref().unwrap_or("?")),
105            _ => "unmeasured".into(),
106        };
107        let mark = match c.verdict.as_str() {
108            "clean" => " ",
109            "admitted_with_caveats" => "~",
110            "disqualified" => "x",
111            "ungated" => "u",
112            _ => "!",
113        };
114        let _ = writeln!(out, "  {mark} {}  {}  {timing}", c.id, c.config);
115        for rule in &c.rules {
116            let _ = writeln!(
117                out,
118                "      {} at {}: {}",
119                rule.rule,
120                rule.span.as_deref().unwrap_or("<no span>"),
121                rule.reason
122            );
123        }
124    }
125
126    let t = &report.totals;
127    let _ = writeln!(
128        out,
129        "\n{} candidates: {} admitted, {} refused; {} measured ok; {:.1} GPU-seconds consumed",
130        t.candidates, t.admitted, t.refused, t.measured_ok, t.gpu_seconds
131    );
132    out
133}