1#[derive(Debug, Clone, PartialEq, Default)]
10pub struct ErrorLocation {
11 pub line: usize,
13 pub column: usize,
15 pub file: Option<String>,
17 pub source_line: Option<String>,
19}
20
21impl ErrorLocation {
22 pub fn new(line: usize, column: usize) -> Self {
23 Self {
24 line,
25 column,
26 file: None,
27 source_line: None,
28 }
29 }
30
31 pub fn with_file(mut self, file: impl Into<String>) -> Self {
32 self.file = Some(file.into());
33 self
34 }
35
36 pub fn with_source_line(mut self, source: impl Into<String>) -> Self {
37 self.source_line = Some(source.into());
38 self
39 }
40}
41
42#[derive(Debug, Clone, PartialEq, thiserror::Error)]
44pub enum VMError {
45 #[error("Stack underflow")]
47 StackUnderflow,
48 #[error("Stack overflow")]
50 StackOverflow,
51 #[error("Type error: expected {expected}, got {got}")]
53 TypeError {
54 expected: &'static str,
55 got: &'static str,
56 },
57 #[error("Division by zero")]
59 DivisionByZero,
60 #[error("Undefined variable: {0}")]
62 UndefinedVariable(String),
63 #[error("Undefined property: {0}")]
65 UndefinedProperty(String),
66 #[error("Invalid function call")]
68 InvalidCall,
69 #[error("Index out of bounds: {index} (length: {length})")]
71 IndexOutOfBounds { index: i32, length: usize },
72 #[error("Invalid operand")]
74 InvalidOperand,
75 #[error("{function}() expects {expected} argument(s), got {got}")]
77 ArityMismatch {
78 function: String,
79 expected: usize,
80 got: usize,
81 },
82 #[error("{function}(): {message}")]
84 InvalidArgument { function: String, message: String },
85 #[error("Not implemented: {0}")]
87 NotImplemented(String),
88 #[error("{0}")]
90 RuntimeError(String),
91 #[error("Suspended on future {future_id}")]
93 Suspended { future_id: u64, resume_ip: usize },
94 #[error("Execution interrupted")]
96 Interrupted,
97 #[error("Resume requested")]
100 ResumeRequested,
101}
102
103impl VMError {
104 #[inline]
106 pub fn type_mismatch(expected: &'static str, got: &'static str) -> Self {
107 Self::TypeError { expected, got }
108 }
109
110 #[inline]
119 pub fn argument_count_error(fn_name: impl Into<String>, expected: usize, got: usize) -> Self {
120 Self::ArityMismatch {
121 function: fn_name.into(),
122 expected,
123 got,
124 }
125 }
126
127 #[inline]
136 pub fn type_error(fn_name: &str, expected_type: &str, got_value: &str) -> Self {
137 Self::RuntimeError(format!(
138 "{}(): expected {}, got {}",
139 fn_name, expected_type, got_value
140 ))
141 }
142}
143
144#[derive(Debug, Clone)]
146pub struct LocatedVMError {
147 pub error: VMError,
148 pub location: Option<ErrorLocation>,
149}
150
151impl LocatedVMError {
152 pub fn new(error: VMError) -> Self {
153 Self {
154 error,
155 location: None,
156 }
157 }
158
159 pub fn with_location(error: VMError, location: ErrorLocation) -> Self {
160 Self {
161 error,
162 location: Some(location),
163 }
164 }
165}
166
167impl std::fmt::Display for LocatedVMError {
168 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
169 if let Some(loc) = &self.location {
171 if let Some(file) = &loc.file {
173 writeln!(f, "error: {}", self.error)?;
174 writeln!(f, " --> {}:{}:{}", file, loc.line, loc.column)?;
175 } else {
176 writeln!(f, "error: {}", self.error)?;
177 writeln!(f, " --> line {}:{}", loc.line, loc.column)?;
178 }
179
180 if let Some(source) = &loc.source_line {
182 writeln!(f, " |")?;
183 writeln!(f, "{:>3} | {}", loc.line, source)?;
184 let padding = " ".repeat(loc.column.saturating_sub(1));
186 writeln!(f, " | {}^", padding)?;
187 }
188 Ok(())
189 } else {
190 write!(f, "error: {}", self.error)
191 }
192 }
193}
194
195impl std::error::Error for LocatedVMError {}
196
197impl From<shape_ast::error::ShapeError> for VMError {
198 fn from(err: shape_ast::error::ShapeError) -> Self {
199 VMError::RuntimeError(err.to_string())
200 }
201}
202
203impl From<shape_ast::error::SourceLocation> for ErrorLocation {
212 fn from(src: shape_ast::error::SourceLocation) -> Self {
218 ErrorLocation {
219 line: src.line,
220 column: src.column,
221 file: src.file,
222 source_line: src.source_line,
223 }
224 }
225}
226
227impl From<ErrorLocation> for shape_ast::error::SourceLocation {
228 fn from(loc: ErrorLocation) -> Self {
234 shape_ast::error::SourceLocation {
235 file: loc.file,
236 line: loc.line,
237 column: loc.column,
238 length: None,
239 source_line: loc.source_line,
240 hints: Vec::new(),
241 notes: Vec::new(),
242 is_synthetic: false,
243 }
244 }
245}
246
247#[cfg(test)]
248mod tests {
249 use super::*;
250
251 #[test]
252 fn test_runtime_error_no_double_prefix() {
253 let err = VMError::RuntimeError("something went wrong".to_string());
254 let display = format!("{}", err);
255 assert_eq!(display, "something went wrong");
257 assert!(!display.contains("Runtime error:"));
258 }
259
260 #[test]
261 fn test_located_error_formatting() {
262 let err = LocatedVMError::with_location(
263 VMError::RuntimeError("bad op".to_string()),
264 ErrorLocation::new(5, 3).with_source_line("let x = 1 + \"a\""),
265 );
266 let display = format!("{}", err);
267 assert!(display.contains("bad op"));
268 assert!(display.contains("line 5"));
269 assert!(display.contains("let x = 1 + \"a\""));
270 }
271
272 #[test]
273 fn test_argument_count_error() {
274 let err = VMError::argument_count_error("foo", 2, 3);
275 match &err {
276 VMError::ArityMismatch {
277 function,
278 expected,
279 got,
280 } => {
281 assert_eq!(function, "foo");
282 assert_eq!(*expected, 2);
283 assert_eq!(*got, 3);
284 }
285 _ => panic!("expected ArityMismatch"),
286 }
287 let display = format!("{}", err);
288 assert!(display.contains("foo()"));
289 assert!(display.contains("2"));
290 assert!(display.contains("3"));
291 }
292
293 #[test]
294 fn test_type_error_helper() {
295 let err = VMError::type_error("parse_int", "string", "bool");
296 let display = format!("{}", err);
297 assert_eq!(display, "parse_int(): expected string, got bool");
298 }
299
300 #[test]
301 fn test_source_location_to_error_location() {
302 let src = shape_ast::error::SourceLocation {
303 file: Some("test.shape".to_string()),
304 line: 10,
305 column: 5,
306 length: Some(3),
307 source_line: Some("let x = 1".to_string()),
308 hints: vec!["try this".to_string()],
309 notes: vec![],
310 is_synthetic: true,
311 };
312 let loc: ErrorLocation = src.into();
313 assert_eq!(loc.line, 10);
314 assert_eq!(loc.column, 5);
315 assert_eq!(loc.file, Some("test.shape".to_string()));
316 assert_eq!(loc.source_line, Some("let x = 1".to_string()));
317 }
318
319 #[test]
320 fn test_error_location_to_source_location() {
321 let loc = ErrorLocation::new(7, 12)
322 .with_file("main.shape")
323 .with_source_line("fn main() {}");
324 let src: shape_ast::error::SourceLocation = loc.into();
325 assert_eq!(src.line, 7);
326 assert_eq!(src.column, 12);
327 assert_eq!(src.file, Some("main.shape".to_string()));
328 assert_eq!(src.source_line, Some("fn main() {}".to_string()));
329 assert_eq!(src.length, None);
330 assert!(src.hints.is_empty());
331 assert!(src.notes.is_empty());
332 assert!(!src.is_synthetic);
333 }
334}