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
118/// VMError with optional source location for better error messages
119#[derive(Debug, Clone)]
120pub struct LocatedVMError {
121    pub error: VMError,
122    pub location: Option<ErrorLocation>,
123}
124
125impl LocatedVMError {
126    pub fn new(error: VMError) -> Self {
127        Self {
128            error,
129            location: None,
130        }
131    }
132
133    pub fn with_location(error: VMError, location: ErrorLocation) -> Self {
134        Self {
135            error,
136            location: Some(location),
137        }
138    }
139}
140
141impl std::fmt::Display for LocatedVMError {
142    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
143        // Format with location if available
144        if let Some(loc) = &self.location {
145            // File and line header
146            if let Some(file) = &loc.file {
147                writeln!(f, "error: {}", self.error)?;
148                writeln!(f, "  --> {}:{}:{}", file, loc.line, loc.column)?;
149            } else {
150                writeln!(f, "error: {}", self.error)?;
151                writeln!(f, "  --> line {}:{}", loc.line, loc.column)?;
152            }
153
154            // Source context if available
155            if let Some(source) = &loc.source_line {
156                writeln!(f, "   |")?;
157                writeln!(f, "{:>3} | {}", loc.line, source)?;
158                // Underline the error position
159                let padding = " ".repeat(loc.column.saturating_sub(1));
160                writeln!(f, "   | {}^", padding)?;
161            }
162            Ok(())
163        } else {
164            write!(f, "error: {}", self.error)
165        }
166    }
167}
168
169impl std::error::Error for LocatedVMError {}
170
171impl From<shape_ast::error::ShapeError> for VMError {
172    fn from(err: shape_ast::error::ShapeError) -> Self {
173        VMError::RuntimeError(err.to_string())
174    }
175}
176
177#[cfg(test)]
178mod tests {
179    use super::*;
180
181    #[test]
182    fn test_runtime_error_no_double_prefix() {
183        let err = VMError::RuntimeError("something went wrong".to_string());
184        let display = format!("{}", err);
185        // Should NOT contain "Runtime error:" — that prefix is added by ShapeError
186        assert_eq!(display, "something went wrong");
187        assert!(!display.contains("Runtime error:"));
188    }
189
190    #[test]
191    fn test_located_error_formatting() {
192        let err = LocatedVMError::with_location(
193            VMError::RuntimeError("bad op".to_string()),
194            ErrorLocation::new(5, 3).with_source_line("let x = 1 + \"a\""),
195        );
196        let display = format!("{}", err);
197        assert!(display.contains("bad op"));
198        assert!(display.contains("line 5"));
199        assert!(display.contains("let x = 1 + \"a\""));
200    }
201}