Skip to main content

harn_vm/vm/
format.rs

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        // Error header
15        out.push_str(&format!("error: {error_msg}\n"));
16
17        // Prefer captured stack trace (taken before unwinding), else use live frames
18        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                // Show source line with caret
47                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                    // Render caret line
55                    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                    // Calculate how many carets to show
61                    let caret_len = if col > 0 {
62                        // Try to find a reasonable token length at this column
63                        Self::token_len_at(source_line, col)
64                    } else {
65                        // No column info: underline the trimmed content
66                        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        // Show call stack (bottom-up, skipping top frame which is already shown)
84        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    /// Estimate the length of the token at the given 1-based column position
99    /// in a source line. Scans forward from that position to find a word/operator
100    /// boundary.
101    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            // Scan forward through identifier/number chars
110            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            // Operator or punctuation: just one caret
117            1
118        }
119    }
120}