1use wasmtime::{Error, FrameInfo, Trap, WasmBacktrace};
4
5use crate::diagnostics::write_source_block;
6use crate::rendering::{RenderError, RenderLimits, RenderedText, Writer};
7use crate::source::Sources;
8use crate::{FileId, Span};
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq)]
11pub enum BacktraceMode {
12 Full,
13 LlmTrimmed,
17}
18
19#[derive(Debug)]
24pub struct ThrownError {
25 pub message: String,
26 pub backtrace: Option<WasmBacktrace>,
27 pub denial: Option<crate::runtime::host::Denial>,
30}
31
32impl std::fmt::Display for ThrownError {
33 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
34 f.write_str(&self.message)
35 }
36}
37
38impl std::error::Error for ThrownError {}
39
40pub fn render(
44 error: &Error,
45 sources: &Sources,
46 file: FileId,
47 mode: BacktraceMode,
48) -> Option<String> {
49 match render_checked(error, sources, file, mode) {
50 Ok(rendered) => rendered.map(|text| text.text),
51 Err(failure) => Some(crate::rendering::failure_text(
52 &failure_message(error),
53 &failure,
54 )),
55 }
56}
57
58pub fn render_checked(
59 error: &Error,
60 sources: &Sources,
61 file: FileId,
62 mode: BacktraceMode,
63) -> Result<Option<RenderedText>, RenderError> {
64 let (trace, label) = if let Some(thrown) = error.downcast_ref::<ThrownError>() {
65 (thrown.backtrace.as_ref(), "thrown here")
66 } else {
67 (error.downcast_ref::<WasmBacktrace>(), trap_label_for(error))
68 };
69 let Some(trace) = trace else {
70 return Ok(None);
71 };
72 sources
73 .get(file)
74 .ok_or(crate::source::SourceError::UnknownFile { file })?;
75 let mut has_frame = false;
76 let rendered = Writer::render(RenderLimits::default(), |out| {
77 out.push("error: ")?;
78 write_failure(out, error)?;
79 out.push("\n")?;
80 let mut first = None;
81 let mut last = None;
82 for (i, frame) in trace.frames().iter().enumerate() {
83 out.step()?;
84 if frame.symbols().first().and_then(|s| s.name()).is_none() {
85 continue;
86 }
87 if !is_source_frame(frame, sources) {
88 first.get_or_insert(i);
89 last = Some(i);
90 }
91 }
92 let mut rendered_index = 0usize;
93 let mut last_kept = None;
95 for (i, frame) in trace.frames().iter().enumerate() {
96 out.step()?;
97 if frame.symbols().first().and_then(|s| s.name()).is_none() {
98 continue;
99 }
100 if mode == BacktraceMode::Full
101 || is_source_frame(frame, sources)
102 || Some(i) == first
103 || Some(i) == last
104 {
105 last_kept = Some(i);
106 }
107 }
108 for (i, frame) in trace.frames().iter().enumerate() {
109 out.step()?;
110 if frame.symbols().first().and_then(|s| s.name()).is_none() {
111 continue;
112 }
113 if mode == BacktraceMode::LlmTrimmed
114 && !is_source_frame(frame, sources)
115 && Some(i) != first
116 && Some(i) != last
117 {
118 continue;
119 }
120 let role = if rendered_index == 0 {
121 label
122 } else if Some(i) == last_kept {
123 "entry"
124 } else {
125 "caller"
126 };
127 write_frame(out, frame, sources, role)?;
128 rendered_index = rendered_index
129 .checked_add(1)
130 .ok_or(RenderError::Formatting)?;
131 has_frame = true;
132 }
133 Ok(())
134 })?;
135 Ok((has_frame || rendered.truncated).then_some(rendered))
136}
137
138pub fn failure_message_checked(error: &Error) -> Result<RenderedText, RenderError> {
139 Writer::render(RenderLimits::default(), |out| write_failure(out, error))
140}
141
142fn write_failure(out: &mut Writer, error: &Error) -> Result<(), RenderError> {
143 match error.downcast_ref::<Trap>() {
144 Some(Trap::Interrupt) => out.push("timeout exceeded"),
145 Some(Trap::OutOfFuel) => out.push("fuel exhausted"),
146 Some(Trap::UnreachableCodeReached) => out.push("unreachable code reached"),
147 _ => out.format(format_args!("{error}")),
148 }
149}
150
151pub fn failure_message(error: &Error) -> String {
164 failure_message_checked(error).map_or_else(
165 |failure| crate::rendering::failure_text("runtime execution failed", &failure),
166 |rendered| rendered.text,
167 )
168}
169
170fn trap_label_for(error: &Error) -> &'static str {
171 match error.downcast_ref::<Trap>() {
172 Some(Trap::UnreachableCodeReached) => "unreachable code reached",
173 Some(Trap::OutOfFuel) => "fuel exhausted",
174 Some(Trap::StackOverflow) => "stack overflow",
175 Some(Trap::Interrupt) => "timeout exceeded",
176 Some(_) | None => "trap raised here",
177 }
178}
179
180fn write_frame(
181 out: &mut Writer,
182 frame: &FrameInfo,
183 sources: &Sources,
184 role: &str,
185) -> Result<(), RenderError> {
186 let Some(sym) = frame.symbols().first() else {
187 return Ok(());
188 };
189 let Some(name) = sym.name() else {
190 return Ok(());
191 };
192 let path = sym.file().unwrap_or("?");
193 let line = sym.line().unwrap_or(0);
194 let col = sym.column().unwrap_or(0);
195 write_symbol(out, name, path, line, col, sources, role)
196}
197
198fn write_symbol(
199 out: &mut Writer,
200 name: &str,
201 path: &str,
202 line: u32,
203 col: u32,
204 sources: &Sources,
205 role: &str,
206) -> Result<(), RenderError> {
207 let context = if let Some((file, source)) = sources.find_path(path)
208 && line != 0
209 {
210 let index = source.line_index();
211 let offset = index.byte_offset(line, col.max(1))?;
212 Some((index, Span::new(file, offset, offset)?))
213 } else {
214 None
215 };
216 out.format(format_args!(
217 " at {name} ({path}:{line}:{col}) [{role}]\n"
218 ))?;
219 if let Some((index, span)) = context {
220 let width = index.line_count().max(1).to_string().len();
221 write_source_block(out, index, span, width)?;
222 }
223 Ok(())
224}
225
226fn is_source_frame(frame: &FrameInfo, sources: &Sources) -> bool {
227 frame
228 .symbols()
229 .first()
230 .and_then(|s| s.file())
231 .is_some_and(|p| sources.find_path(p).is_some())
232}
233
234pub fn failure_chain_checked(error: &Error) -> Result<RenderedText, RenderError> {
237 Writer::render(RenderLimits::default(), |out| {
238 out.format(format_args!("{error:#}"))
239 })
240}
241
242#[cfg(test)]
244fn kept_indices(is_user: &[bool]) -> Vec<usize> {
245 let first_non_user = is_user.iter().position(|u| !u);
246 let last_non_user = is_user.iter().rposition(|u| !u);
247 (0..is_user.len())
248 .filter(|i| is_user[*i] || Some(*i) == first_non_user || Some(*i) == last_non_user)
249 .collect()
250}
251
252#[cfg(test)]
253mod tests {
254 use super::*;
255
256 #[test]
257 fn malformed_symbol_context_fails_before_a_long_frame_can_truncate() {
258 let (sources, _) = Sources::single("test.ts", "x").unwrap();
259 let long_name = "frame".repeat(20_000);
260 let result = Writer::render(RenderLimits::default(), |out| {
261 write_symbol(out, &long_name, "test.ts", 99, 1, &sources, "caller")
262 });
263 assert!(matches!(result, Err(RenderError::Source(_))));
264 let valid = Writer::render(RenderLimits::default(), |out| {
265 write_symbol(out, &long_name, "test.ts", 1, 1, &sources, "caller")
266 })
267 .unwrap();
268 assert!(valid.truncated);
269 let unknown = Writer::render(RenderLimits::default(), |out| {
270 write_symbol(out, "frame", "unknown.ts", 99, 1, &sources, "caller")
271 })
272 .unwrap();
273 assert!(unknown.text.contains("unknown.ts:99:1"));
274 }
275
276 #[test]
277 fn oversized_failure_chain_keeps_its_primary_message() {
278 let error = Error::msg("primary failure ".repeat(20_000));
279 let result = failure_chain_checked(&error).unwrap();
280 assert!(result.truncated);
281 assert!(result.text.starts_with("primary failure"));
282 assert!(result.text.len() <= RenderLimits::default().bytes);
283 }
284
285 #[test]
286 fn curated_trap_messages_replace_the_engines_wording() {
287 for (trap, expected) in [
290 (Trap::Interrupt, "timeout exceeded"),
291 (Trap::OutOfFuel, "fuel exhausted"),
292 (Trap::UnreachableCodeReached, "unreachable code reached"),
293 ] {
294 assert_eq!(failure_message(&Error::new(trap)), expected);
295 }
296 }
297
298 #[test]
299 fn uncurated_traps_keep_the_engine_message() {
300 assert_eq!(
301 failure_message(&Error::new(Trap::NullReference)),
302 "null reference",
303 );
304 }
305
306 fn run(is_user: &[bool]) -> Vec<bool> {
307 kept_indices(is_user)
308 .into_iter()
309 .map(|i| is_user[i])
310 .collect()
311 }
312
313 #[test]
314 fn empty_in_empty_out() {
315 assert!(run(&[]).is_empty());
316 }
317
318 #[test]
319 fn all_user_unchanged() {
320 assert_eq!(run(&[true]), vec![true]);
321 assert_eq!(run(&[true, true, true]), vec![true, true, true]);
322 }
323
324 #[test]
325 fn single_non_user_kept() {
326 assert_eq!(run(&[false]), vec![false]);
327 }
328
329 #[test]
330 fn one_non_user_then_users() {
331 assert_eq!(run(&[false, true, true]), vec![false, true, true]);
332 }
333
334 #[test]
335 fn middle_non_user_dropped() {
336 assert_eq!(
337 run(&[false, false, false, true, true]),
338 vec![false, false, true, true],
339 );
340 }
341
342 #[test]
343 fn interleaved_non_user_keeps_anchors_only() {
344 assert_eq!(
345 run(&[false, true, false, true, false]),
346 vec![false, true, true, false],
347 );
348 }
349
350 #[test]
351 fn all_non_user_keeps_first_and_last() {
352 assert_eq!(run(&[false, false, false]), vec![false, false]);
353 assert_eq!(run(&[false, false, false, false]), vec![false, false],);
354 }
355}