furiosa_visa_std/
diag.rs

1//! Test-time panic hook that reads `build-log.jsonl` and renders kernel-compilation failures in rustc's
2//! aesthetic.
3//!
4//! The schema is owned by the plugin (`npu_opt::protocol::log`); this module defines its own minimal
5//! deserialization struct and only reads the fields needed for rendering. Field names must match the
6//! producer's schema.
7
8use std::fmt;
9use std::path::Path;
10use std::sync::Once;
11
12use anstyle::{AnsiColor, Style};
13use serde::Deserialize;
14
15const MARKER: &str = "__FURIOSA_KERNEL_LOAD_FAILURE__\n";
16
17/// Returns a panic payload the rendering hook will intercept, or `None` when no matching failure row exists
18/// (the caller falls back to a plain IO error).
19pub(crate) fn failure_payload(path: &str) -> Option<String> {
20    let row = log_row(path)?;
21    Some(format!("{MARKER}{row}"))
22}
23
24/// Installs the rendering panic hook, at most once per process.
25pub fn install_hook() {
26    static ONCE: Once = Once::new();
27    ONCE.call_once(|| {
28        let default = std::panic::take_hook();
29        std::panic::set_hook(Box::new(move |info| {
30            if let Some(rendered) = try_render(info) {
31                anstream::eprintln!("\n{rendered}\n");
32            } else {
33                default(info);
34            }
35        }));
36    });
37}
38
39fn try_render(info: &std::panic::PanicHookInfo<'_>) -> Option<String> {
40    let payload = info
41        .payload()
42        .downcast_ref::<String>()
43        .map(String::as_str)
44        .or_else(|| info.payload().downcast_ref::<&str>().copied())?;
45    let json = payload.strip_prefix(MARKER)?;
46    let row: LogRow = serde_json::from_str(json).ok()?;
47    Some(render(&row))
48}
49
50fn log_row(path: &str) -> Option<String> {
51    let path = Path::new(path);
52    let stem = path.file_stem()?.to_str()?;
53    let log = std::fs::read_to_string(path.with_file_name("build-log.jsonl")).ok()?;
54    log.lines()
55        .find(|line| match serde_json::from_str::<LogRow>(line) {
56            Ok(row) => row.stage != Stage::Edf && row.fn_name.replace("::", "__") == stem,
57            Err(_) => false,
58        })
59        .map(str::to_string)
60}
61
62/// Local view of the schema owned by `npu_opt::protocol::log`. Only the fields this renderer needs are
63/// deserialized; extras are ignored so the log format can grow without breaking the runtime.
64#[derive(Deserialize)]
65struct LogRow {
66    #[serde(rename = "fn")]
67    fn_name: String,
68    stage: Stage,
69    file: Option<String>,
70    line: Option<u32>,
71    col: Option<u32>,
72    reason: Option<String>,
73}
74
75#[derive(Deserialize, PartialEq, Eq)]
76#[serde(rename_all = "lowercase")]
77enum Stage {
78    Mir,
79    Visa,
80    Lir,
81    Edf,
82}
83
84impl fmt::Display for Stage {
85    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
86        f.write_str(match self {
87            Self::Mir => "MIR → VISA translation",
88            Self::Visa => "VISA → LIR translation",
89            Self::Lir => "LIR → kernel code generation",
90            Self::Edf => "edf",
91        })
92    }
93}
94
95fn render(row: &LogRow) -> String {
96    let err = Style::new().fg_color(Some(AnsiColor::Red.into())).bold();
97    let loc = Style::new().fg_color(Some(AnsiColor::Blue.into())).bold();
98    let bold = Style::new().bold();
99
100    let mut out = format!(
101        "{err}error{err:#}{bold}: kernel `{}` was not compiled ({}){bold:#}",
102        row.fn_name, row.stage
103    );
104    if let (Some(file), Some(line), Some(col)) = (&row.file, row.line, row.col) {
105        out.push_str(&format!("\n{loc}  -->{loc:#} {file}:{line}:{col}"));
106        append_snippet(&mut out, file, line, col, loc, err);
107    }
108    if let Some(reason) = &row.reason {
109        let mut lines = reason.lines();
110        if let Some(first) = lines.next() {
111            out.push_str(&format!("\n{loc}   ={loc:#} {bold}note{bold:#}: {first}"));
112            for tail in lines {
113                out.push_str(&format!("\n           {tail}"));
114            }
115        }
116    }
117    out.push_str(&format!(
118        "\n{loc}   ={loc:#} {bold}help{bold:#}: run `cargo furiosa-opt compiler build --device-function {}` to reproduce",
119        row.fn_name
120    ));
121    out
122}
123
124fn append_snippet(out: &mut String, file: &str, line: u32, col: u32, loc: Style, err: Style) {
125    let Ok(source) = std::fs::read_to_string(file) else {
126        return;
127    };
128    let Some(src_line) = source.lines().nth(line.saturating_sub(1) as usize) else {
129        return;
130    };
131    let no = line.to_string();
132    let pad = " ".repeat(no.len());
133    let caret_indent = " ".repeat(col.saturating_sub(1) as usize);
134    out.push_str(&format!(
135        "\n{pad} {loc}|{loc:#}\n{loc}{no} |{loc:#} {src_line}\n{pad} {loc}|{loc:#} {caret_indent}{err}^{err:#}\n{pad} {loc}|{loc:#}"
136    ));
137}