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)> = self
19 .frames
20 .iter()
21 .map(|f| {
22 let idx = if f.ip > 0 { f.ip - 1 } else { 0 };
23 let line = if idx < f.chunk.lines.len() {
24 f.chunk.lines[idx] as usize
25 } else {
26 0
27 };
28 let col = if idx < f.chunk.columns.len() {
29 f.chunk.columns[idx] as usize
30 } else {
31 0
32 };
33 (f.fn_name.as_str(), line, col)
34 })
35 .collect();
36
37 if let Some((_name, line, col)) = frames.last() {
38 let line = *line;
39 let col = *col;
40 if line > 0 {
41 let display_col = if col > 0 { col } else { 1 };
42 let gutter_width = line.to_string().len();
43 out.push_str(&format!(
44 "{:>width$}--> {filename}:{line}:{display_col}\n",
45 " ",
46 width = gutter_width + 1,
47 ));
48 if let Some(source_line) = source.lines().nth(line.saturating_sub(1)) {
50 out.push_str(&format!("{:>width$} |\n", " ", width = gutter_width + 1));
51 out.push_str(&format!(
52 "{:>width$} | {source_line}\n",
53 line,
54 width = gutter_width + 1,
55 ));
56 let caret_col = if col > 0 { col } else { 1 };
58 let trimmed = source_line.trim();
59 let leading = source_line
60 .len()
61 .saturating_sub(source_line.trim_start().len());
62 let caret_len = if col > 0 {
64 Self::token_len_at(source_line, col)
66 } else {
67 trimmed.len().max(1)
69 };
70 let padding = if col > 0 {
71 " ".repeat(caret_col.saturating_sub(1))
72 } else {
73 " ".repeat(leading)
74 };
75 let carets = "^".repeat(caret_len);
76 out.push_str(&format!(
77 "{:>width$} | {padding}{carets}\n",
78 " ",
79 width = gutter_width + 1,
80 ));
81 }
82 }
83 }
84
85 if frames.len() > 1 {
87 for (name, line, _col) in frames.iter().rev().skip(1) {
88 let display_name = if name.is_empty() { "pipeline" } else { name };
89 if *line > 0 {
90 out.push_str(&format!(
91 " = note: called from {display_name} at {filename}:{line}\n"
92 ));
93 }
94 }
95 }
96
97 out
98 }
99
100 fn token_len_at(source_line: &str, col: usize) -> usize {
104 let chars: Vec<char> = source_line.chars().collect();
105 let start = col.saturating_sub(1);
106 if start >= chars.len() {
107 return 1;
108 }
109 let first = chars[start];
110 if first.is_alphanumeric() || first == '_' {
111 let mut end = start + 1;
113 while end < chars.len() && (chars[end].is_alphanumeric() || chars[end] == '_') {
114 end += 1;
115 }
116 end - start
117 } else {
118 1
120 }
121 }
122}