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