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        // Get the error location from the top frame (line, column)
18        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                // Show source line with caret
49                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                    // Render caret line
57                    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                    // Calculate how many carets to show
63                    let caret_len = if col > 0 {
64                        // Try to find a reasonable token length at this column
65                        Self::token_len_at(source_line, col)
66                    } else {
67                        // No column info: underline the trimmed content
68                        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        // Show call stack (bottom-up, skipping top frame which is already shown)
86        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    /// Estimate the length of the token at the given 1-based column position
101    /// in a source line. Scans forward from that position to find a word/operator
102    /// boundary.
103    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            // Scan forward through identifier/number chars
112            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            // Operator or punctuation: just one caret
119            1
120        }
121    }
122}