Skip to main content

shape_value/
context.rs

1//! VM execution context and error types
2
3use super::ValueWord;
4
5/// VM execution context passed to module functions
6pub struct VMContext<'vm> {
7    /// Reference to the VM's stack
8    pub stack: &'vm mut Vec<ValueWord>,
9    /// Reference to local variables
10    pub locals: &'vm mut Vec<ValueWord>,
11    /// Reference to global variables
12    pub globals: &'vm mut Vec<ValueWord>,
13}
14
15/// Source location for error reporting
16#[derive(Debug, Clone, PartialEq, Default)]
17pub struct ErrorLocation {
18    /// Line number (1-indexed)
19    pub line: usize,
20    /// Column number (1-indexed)
21    pub column: usize,
22    /// Source file name (if available)
23    pub file: Option<String>,
24    /// The source line content (if available)
25    pub source_line: Option<String>,
26}
27
28impl ErrorLocation {
29    pub fn new(line: usize, column: usize) -> Self {
30        Self {
31            line,
32            column,
33            file: None,
34            source_line: None,
35        }
36    }
37
38    pub fn with_file(mut self, file: impl Into<String>) -> Self {
39        self.file = Some(file.into());
40        self
41    }
42
43    pub fn with_source_line(mut self, source: impl Into<String>) -> Self {
44        self.source_line = Some(source.into());
45        self
46    }
47}
48
49/// VM runtime errors
50#[derive(Debug, Clone, PartialEq, thiserror::Error)]
51pub enum VMError {
52    /// Stack underflow
53    #[error("Stack underflow")]
54    StackUnderflow,
55    /// Stack overflow
56    #[error("Stack overflow")]
57    StackOverflow,
58    /// Type mismatch
59    #[error("Type error: expected {expected}, got {got}")]
60    TypeError {
61        expected: &'static str,
62        got: &'static str,
63    },
64    /// Division by zero
65    #[error("Division by zero")]
66    DivisionByZero,
67    /// Variable not found
68    #[error("Undefined variable: {0}")]
69    UndefinedVariable(String),
70    /// Property not found
71    #[error("Undefined property: {0}")]
72    UndefinedProperty(String),
73    /// Invalid function call
74    #[error("Invalid function call")]
75    InvalidCall,
76    /// Invalid array index
77    #[error("Index out of bounds: {index} (length: {length})")]
78    IndexOutOfBounds { index: i32, length: usize },
79    /// Invalid operand
80    #[error("Invalid operand")]
81    InvalidOperand,
82    /// Wrong number of arguments passed to a function
83    #[error("{function}() expects {expected} argument(s), got {got}")]
84    ArityMismatch {
85        function: String,
86        expected: usize,
87        got: usize,
88    },
89    /// Invalid argument value (correct type, wrong value)
90    #[error("{function}(): {message}")]
91    InvalidArgument { function: String, message: String },
92    /// Feature not yet implemented
93    #[error("Not implemented: {0}")]
94    NotImplemented(String),
95    /// Runtime error with message
96    #[error("{0}")]
97    RuntimeError(String),
98    /// VM suspended on await — not a real error, used to propagate suspension up the Rust call stack
99    #[error("Suspended on future {future_id}")]
100    Suspended { future_id: u64, resume_ip: usize },
101    /// Execution interrupted by Ctrl+C signal
102    #[error("Execution interrupted")]
103    Interrupted,
104    /// Internal: state.resume() requested VM state restoration.
105    /// Not a real error — intercepted by the dispatch loop.
106    #[error("Resume requested")]
107    ResumeRequested,
108}
109
110impl VMError {
111    /// Convenience constructor for `TypeError { expected, got }`.
112    #[inline]
113    pub fn type_mismatch(expected: &'static str, got: &'static str) -> Self {
114        Self::TypeError { expected, got }
115    }
116
117    /// Convenience constructor for argument-count errors.
118    ///
119    /// Produces `ArityMismatch { function, expected, got }` with a consistent
120    /// message format: `"fn_name() expects N argument(s), got M"`.
121    ///
122    /// Prefer this over hand-writing `VMError::RuntimeError(format!(...))` for
123    /// arity mismatches — it uses the structured `ArityMismatch` variant which
124    /// tools can match on programmatically.
125    #[inline]
126    pub fn argument_count_error(fn_name: impl Into<String>, expected: usize, got: usize) -> Self {
127        Self::ArityMismatch {
128            function: fn_name.into(),
129            expected,
130            got,
131        }
132    }
133
134    /// Convenience constructor for type errors in builtin/stdlib functions.
135    ///
136    /// Produces a `RuntimeError` with the format:
137    /// `"fn_name(): expected <expected_type>, got <got_value>"`.
138    ///
139    /// Use this when a function receives a value of the wrong type. For the
140    /// lower-level `TypeError { expected, got }` variant (which requires
141    /// `&'static str`), use `VMError::type_mismatch()` instead.
142    #[inline]
143    pub fn type_error(fn_name: &str, expected_type: &str, got_value: &str) -> Self {
144        Self::RuntimeError(format!(
145            "{}(): expected {}, got {}",
146            fn_name, expected_type, got_value
147        ))
148    }
149}
150
151/// VMError with optional source location for better error messages
152#[derive(Debug, Clone)]
153pub struct LocatedVMError {
154    pub error: VMError,
155    pub location: Option<ErrorLocation>,
156}
157
158impl LocatedVMError {
159    pub fn new(error: VMError) -> Self {
160        Self {
161            error,
162            location: None,
163        }
164    }
165
166    pub fn with_location(error: VMError, location: ErrorLocation) -> Self {
167        Self {
168            error,
169            location: Some(location),
170        }
171    }
172}
173
174impl std::fmt::Display for LocatedVMError {
175    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
176        // Format with location if available
177        if let Some(loc) = &self.location {
178            // File and line header
179            if let Some(file) = &loc.file {
180                writeln!(f, "error: {}", self.error)?;
181                writeln!(f, "  --> {}:{}:{}", file, loc.line, loc.column)?;
182            } else {
183                writeln!(f, "error: {}", self.error)?;
184                writeln!(f, "  --> line {}:{}", loc.line, loc.column)?;
185            }
186
187            // Source context if available
188            if let Some(source) = &loc.source_line {
189                writeln!(f, "   |")?;
190                writeln!(f, "{:>3} | {}", loc.line, source)?;
191                // Underline the error position
192                let padding = " ".repeat(loc.column.saturating_sub(1));
193                writeln!(f, "   | {}^", padding)?;
194            }
195            Ok(())
196        } else {
197            write!(f, "error: {}", self.error)
198        }
199    }
200}
201
202impl std::error::Error for LocatedVMError {}
203
204impl From<shape_ast::error::ShapeError> for VMError {
205    fn from(err: shape_ast::error::ShapeError) -> Self {
206        VMError::RuntimeError(err.to_string())
207    }
208}
209
210// ─── Location type conversions ──────────────────────────────────────
211//
212// `ErrorLocation` (shape-value, 4 fields) is a lightweight VM-oriented
213// subset of `SourceLocation` (shape-ast, 8 fields). The AST type carries
214// richer information (hints, notes, length, is_synthetic) that the VM
215// location intentionally omits. These conversions let code pass locations
216// between the two layers without manual field mapping.
217
218impl From<shape_ast::error::SourceLocation> for ErrorLocation {
219    /// Lossily convert from `SourceLocation` (AST) to `ErrorLocation` (VM).
220    ///
221    /// Drops `length`, `hints`, `notes`, and `is_synthetic` since the VM
222    /// error renderer doesn't use them. This is the natural direction: rich
223    /// compiler info flows toward a simpler runtime representation.
224    fn from(src: shape_ast::error::SourceLocation) -> Self {
225        ErrorLocation {
226            line: src.line,
227            column: src.column,
228            file: src.file,
229            source_line: src.source_line,
230        }
231    }
232}
233
234impl From<ErrorLocation> for shape_ast::error::SourceLocation {
235    /// Widen an `ErrorLocation` (VM) into a `SourceLocation` (AST).
236    ///
237    /// Extended fields (`length`, `hints`, `notes`, `is_synthetic`) are
238    /// filled with defaults. This direction is less common — mainly useful
239    /// when VM errors need to be reported through the AST error renderer.
240    fn from(loc: ErrorLocation) -> Self {
241        shape_ast::error::SourceLocation {
242            file: loc.file,
243            line: loc.line,
244            column: loc.column,
245            length: None,
246            source_line: loc.source_line,
247            hints: Vec::new(),
248            notes: Vec::new(),
249            is_synthetic: false,
250        }
251    }
252}
253
254#[cfg(test)]
255mod tests {
256    use super::*;
257
258    #[test]
259    fn test_runtime_error_no_double_prefix() {
260        let err = VMError::RuntimeError("something went wrong".to_string());
261        let display = format!("{}", err);
262        // Should NOT contain "Runtime error:" — that prefix is added by ShapeError
263        assert_eq!(display, "something went wrong");
264        assert!(!display.contains("Runtime error:"));
265    }
266
267    #[test]
268    fn test_located_error_formatting() {
269        let err = LocatedVMError::with_location(
270            VMError::RuntimeError("bad op".to_string()),
271            ErrorLocation::new(5, 3).with_source_line("let x = 1 + \"a\""),
272        );
273        let display = format!("{}", err);
274        assert!(display.contains("bad op"));
275        assert!(display.contains("line 5"));
276        assert!(display.contains("let x = 1 + \"a\""));
277    }
278
279    #[test]
280    fn test_argument_count_error() {
281        let err = VMError::argument_count_error("foo", 2, 3);
282        match &err {
283            VMError::ArityMismatch {
284                function,
285                expected,
286                got,
287            } => {
288                assert_eq!(function, "foo");
289                assert_eq!(*expected, 2);
290                assert_eq!(*got, 3);
291            }
292            _ => panic!("expected ArityMismatch"),
293        }
294        let display = format!("{}", err);
295        assert!(display.contains("foo()"));
296        assert!(display.contains("2"));
297        assert!(display.contains("3"));
298    }
299
300    #[test]
301    fn test_type_error_helper() {
302        let err = VMError::type_error("parse_int", "string", "bool");
303        let display = format!("{}", err);
304        assert_eq!(display, "parse_int(): expected string, got bool");
305    }
306
307    #[test]
308    fn test_source_location_to_error_location() {
309        let src = shape_ast::error::SourceLocation {
310            file: Some("test.shape".to_string()),
311            line: 10,
312            column: 5,
313            length: Some(3),
314            source_line: Some("let x = 1".to_string()),
315            hints: vec!["try this".to_string()],
316            notes: vec![],
317            is_synthetic: true,
318        };
319        let loc: ErrorLocation = src.into();
320        assert_eq!(loc.line, 10);
321        assert_eq!(loc.column, 5);
322        assert_eq!(loc.file, Some("test.shape".to_string()));
323        assert_eq!(loc.source_line, Some("let x = 1".to_string()));
324    }
325
326    #[test]
327    fn test_error_location_to_source_location() {
328        let loc = ErrorLocation::new(7, 12)
329            .with_file("main.shape")
330            .with_source_line("fn main() {}");
331        let src: shape_ast::error::SourceLocation = loc.into();
332        assert_eq!(src.line, 7);
333        assert_eq!(src.column, 12);
334        assert_eq!(src.file, Some("main.shape".to_string()));
335        assert_eq!(src.source_line, Some("fn main() {}".to_string()));
336        assert_eq!(src.length, None);
337        assert!(src.hints.is_empty());
338        assert!(src.notes.is_empty());
339        assert!(!src.is_synthetic);
340    }
341}