Skip to main content

harn_vm/vm/
format.rs

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        // Prefer captured stack trace (taken before unwinding); fall back to live frames.
14        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            // Read the frame's own source so the caret line is meaningful;
43            // fall back to entry-point source (e.g. for stdlib modules).
44            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        // Call stack, bottom-up, skipping the top frame (already shown).
96        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    /// Estimate the length of the token at the given 1-based column position
114    /// in a source line. Scans forward from that position to find a word/operator
115    /// boundary.
116    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}