1use super::*;
2
3impl super::Vm {
4 pub fn format_runtime_error(&self, error: &VmError) -> String {
5 let source = match &self.source_text {
6 Some(s) => s.as_str(),
7 None => return format!("error: {error}"),
8 };
9 let filename = self.source_file.as_deref().unwrap_or("<unknown>");
10
11 let error_msg = format!("{error}");
12 let mut out = String::new();
13
14 out.push_str(&format!("error: {error_msg}\n"));
16
17 let frames: Vec<(&str, usize, usize)> = if !self.error_stack_trace.is_empty() {
19 self.error_stack_trace
20 .iter()
21 .map(|(name, line, col)| (name.as_str(), *line, *col))
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.as_str(), line, col)
31 })
32 .collect()
33 };
34
35 if let Some((_name, line, col)) = frames.last() {
36 let line = *line;
37 let col = *col;
38 if line > 0 {
39 let display_col = if col > 0 { col } else { 1 };
40 let gutter_width = line.to_string().len();
41 out.push_str(&format!(
42 "{:>width$}--> {filename}:{line}:{display_col}\n",
43 " ",
44 width = gutter_width + 1,
45 ));
46 if let Some(source_line) = source.lines().nth(line.saturating_sub(1)) {
48 out.push_str(&format!("{:>width$} |\n", " ", width = gutter_width + 1));
49 out.push_str(&format!(
50 "{:>width$} | {source_line}\n",
51 line,
52 width = gutter_width + 1,
53 ));
54 let caret_col = if col > 0 { col } else { 1 };
56 let trimmed = source_line.trim();
57 let leading = source_line
58 .len()
59 .saturating_sub(source_line.trim_start().len());
60 let caret_len = if col > 0 {
62 Self::token_len_at(source_line, col)
64 } else {
65 trimmed.len().max(1)
67 };
68 let padding = if col > 0 {
69 " ".repeat(caret_col.saturating_sub(1))
70 } else {
71 " ".repeat(leading)
72 };
73 let carets = "^".repeat(caret_len);
74 out.push_str(&format!(
75 "{:>width$} | {padding}{carets}\n",
76 " ",
77 width = gutter_width + 1,
78 ));
79 }
80 }
81 }
82
83 if frames.len() > 1 {
85 for (name, line, _col) in frames.iter().rev().skip(1) {
86 let display_name = if name.is_empty() { "pipeline" } else { name };
87 if *line > 0 {
88 out.push_str(&format!(
89 " = note: called from {display_name} at {filename}:{line}\n"
90 ));
91 }
92 }
93 }
94
95 out
96 }
97
98 fn token_len_at(source_line: &str, col: usize) -> usize {
102 let chars: Vec<char> = source_line.chars().collect();
103 let start = col.saturating_sub(1);
104 if start >= chars.len() {
105 return 1;
106 }
107 let first = chars[start];
108 if first.is_alphanumeric() || first == '_' {
109 let mut end = start + 1;
111 while end < chars.len() && (chars[end].is_alphanumeric() || chars[end] == '_') {
112 end += 1;
113 }
114 end - start
115 } else {
116 1
118 }
119 }
120}