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 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        // Error header
12        out.push_str(&format!("error: {error_msg}\n"));
13
14        // Prefer captured stack trace (taken before unwinding), else use live frames.
15        // Each frame now carries its own source file so errors attribute the correct
16        // path for imported-module frames instead of the entry-point pipeline.
17        let frames: Vec<(String, usize, usize, Option<String>)> =
18            if !self.error_stack_trace.is_empty() {
19                self.error_stack_trace
20                    .iter()
21                    .map(|(name, line, col, src)| (name.clone(), *line, *col, src.clone()))
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.clone(), line, col, f.chunk.source_file.clone())
31                    })
32                    .collect()
33            };
34
35        if let Some((_name, line, col, frame_file)) = frames.last() {
36            let line = *line;
37            let col = *col;
38            let filename = frame_file.as_deref().unwrap_or(entry_file);
39            // Prefer reading the frame's own source file from disk so the
40            // caret line is meaningful. Fall back to the entry-point source
41            // text if reading fails (e.g. stdlib modules).
42            let owned_source: Option<String> = frame_file
43                .as_deref()
44                .and_then(|p| std::fs::read_to_string(p).ok());
45            let source_for_line: Option<&str> =
46                owned_source.as_deref().or(if frame_file.is_none() {
47                    entry_source
48                } else {
49                    None
50                });
51            if line > 0 {
52                let display_col = if col > 0 { col } else { 1 };
53                let gutter_width = line.to_string().len();
54                out.push_str(&format!(
55                    "{:>width$}--> {filename}:{line}:{display_col}\n",
56                    " ",
57                    width = gutter_width + 1,
58                ));
59                if let Some(source_line) =
60                    source_for_line.and_then(|s| s.lines().nth(line.saturating_sub(1)))
61                {
62                    out.push_str(&format!("{:>width$} |\n", " ", width = gutter_width + 1));
63                    out.push_str(&format!(
64                        "{:>width$} | {source_line}\n",
65                        line,
66                        width = gutter_width + 1,
67                    ));
68                    let caret_col = if col > 0 { col } else { 1 };
69                    let trimmed = source_line.trim();
70                    let leading = source_line
71                        .len()
72                        .saturating_sub(source_line.trim_start().len());
73                    let caret_len = if col > 0 {
74                        Self::token_len_at(source_line, col)
75                    } else {
76                        trimmed.len().max(1)
77                    };
78                    let padding = if col > 0 {
79                        " ".repeat(caret_col.saturating_sub(1))
80                    } else {
81                        " ".repeat(leading)
82                    };
83                    let carets = "^".repeat(caret_len);
84                    out.push_str(&format!(
85                        "{:>width$} | {padding}{carets}\n",
86                        " ",
87                        width = gutter_width + 1,
88                    ));
89                }
90            }
91        }
92
93        // Show call stack (bottom-up, skipping top frame which is already shown).
94        if frames.len() > 1 {
95            for (name, line, _col, frame_file) in frames.iter().rev().skip(1) {
96                let display_name = if name.is_empty() { "pipeline" } else { name };
97                if *line > 0 {
98                    let filename = frame_file.as_deref().unwrap_or(entry_file);
99                    out.push_str(&format!(
100                        "  = note: called from {display_name} at {filename}:{line}\n"
101                    ));
102                }
103            }
104        }
105
106        out
107    }
108
109    /// Estimate the length of the token at the given 1-based column position
110    /// in a source line. Scans forward from that position to find a word/operator
111    /// boundary.
112    fn token_len_at(source_line: &str, col: usize) -> usize {
113        let chars: Vec<char> = source_line.chars().collect();
114        let start = col.saturating_sub(1);
115        if start >= chars.len() {
116            return 1;
117        }
118        let first = chars[start];
119        if first.is_alphanumeric() || first == '_' {
120            // Scan forward through identifier/number chars
121            let mut end = start + 1;
122            while end < chars.len() && (chars[end].is_alphanumeric() || chars[end] == '_') {
123                end += 1;
124            }
125            end - start
126        } else {
127            // Operator or punctuation: just one caret
128            1
129        }
130    }
131}