1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
/// A script runtime abort. What a compiled binary would report as a panic,
/// carried as a typed error so `main` can print the panic header and exit
/// with the panic status 101, matching real Rust.
#[derive(Debug)]
pub struct ScriptPanic {
/// File and line of the innermost script frame, for the panic header.
pub file: String,
pub line: u32,
/// The message with the script backtrace lines appended.
pub rendered: String,
}
impl std::fmt::Display for ScriptPanic {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.rendered)
}
}
impl std::error::Error for ScriptPanic {}
/// `main` returned a `Result::Err` value. A compiled binary prints it as
/// `Error: ...` and exits 1, so this marks that outcome apart from a panic.
#[derive(Debug)]
pub struct ErrReturn(pub String);
impl std::fmt::Display for ErrReturn {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "Error: {}", self.0)
}
}
impl std::error::Error for ErrReturn {}
/// Wrap a runtime error as a `ScriptPanic` carrying the script backtrace.
/// Frames arrive innermost first as (function, file, line), line 0 meaning
/// unknown. Deep chains cap at a fixed count so runaway recursion stays
/// readable.
pub(super) fn trace_error(
e: anyhow::Error,
frames: impl Iterator<Item = (String, String, u32)>,
) -> anyhow::Error {
const SHOWN: usize = 15;
let mut msg = format!("{e:#}");
// A closure called from inside a bridge runs its own exec and wraps
// first; the panic origin must stay that innermost site, not this
// outer exec's current frame.
let mut origin: Option<(String, u32)> = e
.downcast_ref::<ScriptPanic>()
.map(|p| (p.file.clone(), p.line));
let mut hidden = 0usize;
for (i, (func, file, line)) in frames.enumerate() {
if origin.is_none() {
origin = Some((file.clone(), line));
}
if i >= SHOWN {
hidden += 1;
continue;
}
if file.is_empty() {
msg.push_str(&format!("\n at {func}"));
} else if line == 0 {
msg.push_str(&format!("\n at {func} ({file})"));
} else {
msg.push_str(&format!("\n at {func} ({file}:{line})"));
}
}
if hidden > 0 {
msg.push_str(&format!("\n ... {hidden} more frames"));
}
let (file, line) = origin.unwrap_or_default();
anyhow::Error::new(ScriptPanic {
file,
line,
rendered: msg,
})
}