1use super::ValueWord;
4
5pub struct VMContext<'vm> {
7 pub stack: &'vm mut Vec<ValueWord>,
9 pub locals: &'vm mut Vec<ValueWord>,
11 pub globals: &'vm mut Vec<ValueWord>,
13}
14
15#[derive(Debug, Clone, PartialEq, Default)]
17pub struct ErrorLocation {
18 pub line: usize,
20 pub column: usize,
22 pub file: Option<String>,
24 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#[derive(Debug, Clone, PartialEq, thiserror::Error)]
51pub enum VMError {
52 #[error("Stack underflow")]
54 StackUnderflow,
55 #[error("Stack overflow")]
57 StackOverflow,
58 #[error("Type error: expected {expected}, got {got}")]
60 TypeError {
61 expected: &'static str,
62 got: &'static str,
63 },
64 #[error("Division by zero")]
66 DivisionByZero,
67 #[error("Undefined variable: {0}")]
69 UndefinedVariable(String),
70 #[error("Undefined property: {0}")]
72 UndefinedProperty(String),
73 #[error("Invalid function call")]
75 InvalidCall,
76 #[error("Index out of bounds: {index} (length: {length})")]
78 IndexOutOfBounds { index: i32, length: usize },
79 #[error("Invalid operand")]
81 InvalidOperand,
82 #[error("{function}() expects {expected} argument(s), got {got}")]
84 ArityMismatch {
85 function: String,
86 expected: usize,
87 got: usize,
88 },
89 #[error("{function}(): {message}")]
91 InvalidArgument { function: String, message: String },
92 #[error("Not implemented: {0}")]
94 NotImplemented(String),
95 #[error("{0}")]
97 RuntimeError(String),
98 #[error("Suspended on future {future_id}")]
100 Suspended { future_id: u64, resume_ip: usize },
101 #[error("Execution interrupted")]
103 Interrupted,
104 #[error("Resume requested")]
107 ResumeRequested,
108}
109
110impl VMError {
111 #[inline]
113 pub fn type_mismatch(expected: &'static str, got: &'static str) -> Self {
114 Self::TypeError { expected, got }
115 }
116
117 #[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 #[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#[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 if let Some(loc) = &self.location {
178 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 if let Some(source) = &loc.source_line {
189 writeln!(f, " |")?;
190 writeln!(f, "{:>3} | {}", loc.line, source)?;
191 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
210impl From<shape_ast::error::SourceLocation> for ErrorLocation {
219 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 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 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}