1use super::*;
2
3impl super::Vm {
4 pub fn format_runtime_error(&self, error: &VmError) -> String {
5 let entry_file = self.source_file.as_deref().unwrap_or("<unknown>");
6 let entry_source = self.source_text.as_deref();
7
8 let error_msg = format!("{error}");
9 let mut out = String::new();
10
11 out.push_str(&format!("error: {error_msg}\n"));
13
14 let frames: Vec<(String, usize, usize, Option<String>)> =
18 if !self.error_stack_trace.is_empty() {
19 self.error_stack_trace
20 .iter()
21 .map(|(name, line, col, src)| (name.clone(), *line, *col, src.clone()))
22 .collect()
23 } else {
24 self.frames
25 .iter()
26 .map(|f| {
27 let idx = if f.ip > 0 { f.ip - 1 } else { 0 };
28 let line = f.chunk.lines.get(idx).copied().unwrap_or(0) as usize;
29 let col = f.chunk.columns.get(idx).copied().unwrap_or(0) as usize;
30 (f.fn_name.clone(), line, col, f.chunk.source_file.clone())
31 })
32 .collect()
33 };
34
35 if let Some((_name, line, col, frame_file)) = frames.last() {
36 let line = *line;
37 let col = *col;
38 let filename = frame_file.as_deref().unwrap_or(entry_file);
39 let owned_source: Option<String> = frame_file
43 .as_deref()
44 .and_then(|p| std::fs::read_to_string(p).ok());
45 let source_for_line: Option<&str> =
46 owned_source.as_deref().or(if frame_file.is_none() {
47 entry_source
48 } else {
49 None
50 });
51 if line > 0 {
52 let display_col = if col > 0 { col } else { 1 };
53 let gutter_width = line.to_string().len();
54 out.push_str(&format!(
55 "{:>width$}--> {filename}:{line}:{display_col}\n",
56 " ",
57 width = gutter_width + 1,
58 ));
59 if let Some(source_line) =
60 source_for_line.and_then(|s| s.lines().nth(line.saturating_sub(1)))
61 {
62 out.push_str(&format!("{:>width$} |\n", " ", width = gutter_width + 1));
63 out.push_str(&format!(
64 "{:>width$} | {source_line}\n",
65 line,
66 width = gutter_width + 1,
67 ));
68 let caret_col = if col > 0 { col } else { 1 };
69 let trimmed = source_line.trim();
70 let leading = source_line
71 .len()
72 .saturating_sub(source_line.trim_start().len());
73 let caret_len = if col > 0 {
74 Self::token_len_at(source_line, col)
75 } else {
76 trimmed.len().max(1)
77 };
78 let padding = if col > 0 {
79 " ".repeat(caret_col.saturating_sub(1))
80 } else {
81 " ".repeat(leading)
82 };
83 let carets = "^".repeat(caret_len);
84 out.push_str(&format!(
85 "{:>width$} | {padding}{carets}\n",
86 " ",
87 width = gutter_width + 1,
88 ));
89 }
90 }
91 }
92
93 if frames.len() > 1 {
95 for (name, line, _col, frame_file) in frames.iter().rev().skip(1) {
96 let display_name = if name.is_empty() { "pipeline" } else { name };
97 if *line > 0 {
98 let filename = frame_file.as_deref().unwrap_or(entry_file);
99 out.push_str(&format!(
100 " = note: called from {display_name} at {filename}:{line}\n"
101 ));
102 }
103 }
104 }
105
106 out
107 }
108
109 fn token_len_at(source_line: &str, col: usize) -> usize {
113 let chars: Vec<char> = source_line.chars().collect();
114 let start = col.saturating_sub(1);
115 if start >= chars.len() {
116 return 1;
117 }
118 let first = chars[start];
119 if first.is_alphanumeric() || first == '_' {
120 let mut end = start + 1;
122 while end < chars.len() && (chars[end].is_alphanumeric() || chars[end] == '_') {
123 end += 1;
124 }
125 end - start
126 } else {
127 1
129 }
130 }
131}