Skip to main content

mq_lang/
error.rs

1pub mod runtime;
2pub mod syntax;
3
4use miette::{Diagnostic, NamedSource, SourceOffset, SourceSpan};
5use std::borrow::Cow;
6
7use crate::{
8    ModuleLoader, ModuleResolver, Token, TokenKind,
9    error::{runtime::RuntimeError, syntax::SyntaxError},
10    module::{self, error::ModuleError},
11};
12
13#[allow(clippy::useless_conversion)]
14#[derive(Debug, thiserror::Error, PartialEq)]
15pub enum InnerError {
16    #[error(transparent)]
17    Runtime(#[from] RuntimeError),
18    #[error(transparent)]
19    Syntax(#[from] SyntaxError),
20    #[error(transparent)]
21    Module(#[from] ModuleError),
22}
23
24impl InnerError {
25    #[cold]
26    pub fn token(&self) -> Option<&Token> {
27        match self {
28            InnerError::Syntax(err) => err.token(),
29            InnerError::Runtime(err) => err.token(),
30            InnerError::Module(err) => err.token(),
31        }
32    }
33
34    /// Returns the opening delimiter token for errors that have one (e.g. unclosed brackets).
35    #[cold]
36    pub fn secondary_token(&self) -> Option<&Token> {
37        match self {
38            InnerError::Syntax(SyntaxError::ExpectedClosingParen(_, opening))
39            | InnerError::Syntax(SyntaxError::ExpectedClosingBrace(_, opening))
40            | InnerError::Syntax(SyntaxError::ExpectedClosingBracket(_, opening)) => {
41                opening.as_ref().map(|v| v.as_ref())
42            }
43            InnerError::Module(ModuleError::SyntaxError(SyntaxError::ExpectedClosingParen(_, opening)))
44            | InnerError::Module(ModuleError::SyntaxError(SyntaxError::ExpectedClosingBrace(_, opening)))
45            | InnerError::Module(ModuleError::SyntaxError(SyntaxError::ExpectedClosingBracket(_, opening))) => {
46                opening.as_ref().map(|v| v.as_ref())
47            }
48            _ => None,
49        }
50    }
51}
52
53/// Represents a high-level error with diagnostic information for the user.
54#[derive(PartialEq, Debug, thiserror::Error)]
55#[error("{cause}")]
56pub struct Error {
57    /// The underlying cause of the error.
58    pub cause: InnerError,
59    /// The named source code for diagnostics (includes the module filename when available).
60    pub source_code: NamedSource<String>,
61    /// The location in the source code for diagnostics.
62    pub location: SourceSpan,
63    /// The location of the opening delimiter, if applicable (e.g. unclosed `(`, `[`, `{`).
64    pub secondary_location: Option<SourceSpan>,
65}
66
67impl Error {
68    #[cold]
69    pub fn from_error(
70        top_level_source_code: impl Into<String>,
71        cause: InnerError,
72        module_loader: ModuleLoader<impl ModuleResolver>,
73    ) -> Self {
74        let source_code = top_level_source_code.into();
75        let token = cause.token();
76
77        match token {
78            Some(token) => {
79                let source_str = module_loader
80                    .get_source_code(token.module_id, source_code)
81                    .unwrap_or_default();
82                let source_name = module_loader.module_file_name(token.module_id);
83
84                let span_for = |t: &Token| {
85                    SourceSpan::new(
86                        SourceOffset::from_location(&source_str, t.range.start.line as usize, t.range.start.column),
87                        std::cmp::max(
88                            SourceOffset::from_location(&source_str, t.range.end.line as usize, t.range.end.column)
89                                .offset()
90                                .saturating_sub(
91                                    SourceOffset::from_location(
92                                        &source_str,
93                                        t.range.start.line as usize,
94                                        t.range.start.column,
95                                    )
96                                    .offset(),
97                                ),
98                            1,
99                        ),
100                    )
101                };
102
103                let location = span_for(token);
104                let secondary_location = cause.secondary_token().map(span_for);
105
106                Self {
107                    cause,
108                    source_code: NamedSource::new(source_name, source_str),
109                    location,
110                    secondary_location,
111                }
112            }
113            None => {
114                let (module_id, is_eof) = match &cause {
115                    InnerError::Syntax(SyntaxError::UnexpectedEOFDetected(module_id)) => (Some(module_id), true),
116                    InnerError::Runtime(_) => (None, false),
117                    InnerError::Module(ModuleError::SyntaxError(SyntaxError::UnexpectedEOFDetected(module_id))) => {
118                        (Some(module_id), true)
119                    }
120                    _ => (None, false),
121                };
122
123                let source_name = module_id
124                    .map(|id| module_loader.module_file_name(*id))
125                    .unwrap_or_default();
126
127                let source_str = module_id
128                    .map(|id| {
129                        module_loader
130                            .get_source_code(*id, source_code.clone())
131                            .unwrap_or_default()
132                    })
133                    .unwrap_or(source_code);
134
135                let location = if is_eof {
136                    let lines = source_str.lines();
137                    let loc_line = lines.clone().count().saturating_sub(1);
138                    let loc_col = lines.last().map(|lines| lines.len()).unwrap_or(0);
139                    SourceSpan::new(SourceOffset::from_location(&source_str, loc_line, loc_col), 1)
140                } else {
141                    SourceSpan::new(SourceOffset::from_location(&source_str, 0, 0), 1)
142                };
143
144                Self {
145                    cause,
146                    source_code: NamedSource::new(source_name, source_str),
147                    location,
148                    secondary_location: None,
149                }
150            }
151        }
152    }
153}
154
155impl Diagnostic for Error {
156    #[cold]
157    fn code<'a>(&'a self) -> Option<Box<dyn std::fmt::Display + 'a>> {
158        let code = match &self.cause {
159            InnerError::Runtime(_) => "mq::runtime",
160            InnerError::Syntax(_) => "mq::syntax",
161            InnerError::Module(_) => "mq::module",
162        };
163        Some(Box::new(code) as Box<dyn std::fmt::Display>)
164    }
165
166    #[cold]
167    fn url<'a>(&'a self) -> Option<Box<dyn std::fmt::Display + 'a>> {
168        match &self.cause {
169            InnerError::Runtime(RuntimeError::InvalidDefinition(_, _))
170            | InnerError::Runtime(RuntimeError::InvalidTypes { .. }) => {
171                Some(Box::new("https://mqlang.org/book") as Box<dyn std::fmt::Display>)
172            }
173            _ => None,
174        }
175    }
176
177    #[cold]
178    fn help<'a>(&'a self) -> Option<Box<dyn std::fmt::Display + 'a>> {
179        let msg: Option<Cow<'static, str>> = match &self.cause {
180            InnerError::Syntax(SyntaxError::EnvNotFound(_, env)) => Some(Cow::Owned(format!(
181                "Environment variable '{env}' not found. Did you forget to set it?"
182            ))),
183            InnerError::Syntax(SyntaxError::UnexpectedToken(token)) if token.kind == TokenKind::Eof => {
184                Some(Cow::Borrowed(
185                    "The source could not be fully parsed from this position. Check for unsupported escape sequences (use \\u{XXXX} for Unicode), invalid characters, or unterminated string literals.",
186                ))
187            }
188            InnerError::Syntax(SyntaxError::UnexpectedToken(_)) => Some(Cow::Borrowed(
189                "This token is not valid here. Check for typos, missing operators, or misplaced punctuation.",
190            )),
191            InnerError::Syntax(SyntaxError::UnexpectedEOFDetected(_)) => Some(Cow::Borrowed(
192                "Unexpected end of input. Check for missing closing brackets, parentheses, or incomplete expressions.",
193            )),
194            InnerError::Syntax(SyntaxError::InsufficientTokens(_)) => Some(Cow::Borrowed(
195                "Parsing could not continue here. Check for missing arguments, operators, or mismatched delimiters.",
196            )),
197            InnerError::Syntax(SyntaxError::UnknownSelector(_)) => Some(Cow::Borrowed(
198                "Unknown selector. Valid selectors include node types (e.g. .h1, .p, .code) and bracket access (e.g. .[0], .[n][m]).",
199            )),
200            InnerError::Syntax(SyntaxError::ExpectedClosingParen(_, _)) => Some(Cow::Borrowed(
201                "Expected a closing parenthesis ')'. Check your parentheses for balance.",
202            )),
203            InnerError::Syntax(SyntaxError::ExpectedClosingBrace(_, _)) => Some(Cow::Borrowed(
204                "Expected a closing brace '}'. Check your braces for balance.",
205            )),
206            InnerError::Syntax(SyntaxError::ExpectedClosingBracket(_, _)) => Some(Cow::Borrowed(
207                "Expected a closing bracket ']'. Check your brackets for balance.",
208            )),
209            InnerError::Syntax(SyntaxError::InvalidAssignmentTarget(_)) => Some(Cow::Borrowed(
210                "Invalid assignment target. Ensure you're assigning to a valid variable or property.",
211            )),
212            InnerError::Syntax(SyntaxError::ParameterWithoutDefaultAfterDefault(_)) => Some(Cow::Borrowed(
213                "Move this parameter before any parameters that have default values, or give it a default value.",
214            )),
215            InnerError::Syntax(SyntaxError::MacroParametersCannotHaveDefaults(_)) => {
216                Some(Cow::Borrowed("Macro parameters cannot have default values."))
217            }
218            InnerError::Syntax(SyntaxError::VariadicParameterMustBeLast(_)) => Some(Cow::Borrowed(
219                "Variadic parameter (*) must be the last parameter in the parameter list.",
220            )),
221            InnerError::Syntax(SyntaxError::MultipleVariadicParameters(_)) => Some(Cow::Borrowed(
222                "Only one variadic parameter (*) is allowed per function.",
223            )),
224            InnerError::Syntax(SyntaxError::MacroParametersCannotBeVariadic(_)) => {
225                Some(Cow::Borrowed("Macro parameters cannot be variadic."))
226            }
227            InnerError::Syntax(SyntaxError::UnexpectedEOFAfterToken(_)) => Some(Cow::Borrowed(
228                "An expression was expected here. Check for incomplete expressions after operators or keywords.",
229            )),
230            InnerError::Syntax(SyntaxError::UnmatchedEnd(_)) => Some(Cow::Borrowed(
231                "This `end` keyword does not match any open block. \
232                Note: single-line `if` expressions do not require `end`. \
233                Check that each `end` closes a `def`, `fn`, `do`, `while`, `loop`, or `foreach` block.",
234            )),
235            InnerError::Runtime(RuntimeError::UserDefined { .. }) => {
236                Some(Cow::Borrowed("A user-defined error occurred during evaluation."))
237            }
238            InnerError::Runtime(RuntimeError::InvalidBase64String(_, _)) => Some(Cow::Borrowed(
239                "The provided string is not valid Base64. Check your input.",
240            )),
241            InnerError::Runtime(RuntimeError::NotDefined(_, name)) => Some(Cow::Owned(format!(
242                "'{name}' is not defined. Did you forget to declare it?"
243            ))),
244            InnerError::Runtime(RuntimeError::DateTimeFormatError(_, _)) => Some(Cow::Borrowed(
245                "Invalid date/time format. Please check your format string.",
246            )),
247            InnerError::Runtime(RuntimeError::IndexOutOfBounds(_, _)) => Some(Cow::Borrowed(
248                "Index out of bounds. Check your array or string indices.",
249            )),
250            InnerError::Runtime(RuntimeError::InvalidDefinition(_, _)) => Some(Cow::Borrowed(
251                "Invalid definition. Please check your function or variable declaration.",
252            )),
253            InnerError::Runtime(RuntimeError::AssignToImmutable(_, name)) => Some(Cow::Owned(format!(
254                "Cannot assign to immutable variable '{name}'. Consider declaring it as mutable."
255            ))),
256            InnerError::Runtime(RuntimeError::UndefinedVariable(_, name)) => Some(Cow::Owned(format!(
257                "Variable '{name}' is undefined. Did you forget to declare it?"
258            ))),
259            InnerError::Runtime(RuntimeError::InvalidTypes { .. }) => {
260                Some(Cow::Borrowed("Type mismatch. Check the types of your operands."))
261            }
262            InnerError::Runtime(RuntimeError::InvalidNumberOfArguments {
263                token: _,
264                name: _,
265                expected,
266                actual,
267            }) => Some(Cow::Owned(format!(
268                "Invalid number of arguments: expected {expected}, got {actual}."
269            ))),
270            InnerError::Runtime(RuntimeError::InvalidRegularExpression(_, _)) => Some(Cow::Borrowed(
271                "Invalid regular expression. Please check your regex syntax.",
272            )),
273            InnerError::Runtime(RuntimeError::InternalError(_)) => Some(Cow::Borrowed(
274                "An internal error occurred. Please report this if it persists.",
275            )),
276            InnerError::Runtime(RuntimeError::Runtime(_, _)) => {
277                Some(Cow::Borrowed("A runtime error occurred during evaluation."))
278            }
279            InnerError::Runtime(RuntimeError::ZeroDivision(_)) => {
280                Some(Cow::Borrowed("Division by zero is not allowed."))
281            }
282            InnerError::Runtime(RuntimeError::RecursionError(_)) => {
283                Some(Cow::Borrowed("Maximum recursion depth exceeded."))
284            }
285            InnerError::Runtime(RuntimeError::ModuleLoadError(_)) => {
286                Some(Cow::Borrowed("Failed to load module. Check module paths and names."))
287            }
288            InnerError::Runtime(RuntimeError::UnexpectedBreak(_)) => {
289                Some(Cow::Borrowed("'break' can only be used inside a loop."))
290            }
291            InnerError::Runtime(RuntimeError::UnexpectedContinue(_)) => {
292                Some(Cow::Borrowed("'continue' can only be used inside a loop."))
293            }
294            InnerError::Runtime(RuntimeError::EnvNotFound(_, env)) => Some(Cow::Owned(format!(
295                "Environment variable '{env}' not found. Did you forget to set it?"
296            ))),
297            InnerError::Runtime(RuntimeError::QuoteNotAllowedInRuntimeContext(_)) => Some(Cow::Borrowed(
298                "quote() is not allowed in runtime context. It should only appear inside macros.",
299            )),
300            InnerError::Runtime(RuntimeError::UnquoteNotAllowedOutsideQuote(_)) => {
301                Some(Cow::Borrowed("unquote() can only be used inside quote()."))
302            }
303            InnerError::Runtime(RuntimeError::InvalidConvert(_, msg)) => Some(Cow::Owned(format!(
304                "Invalid conversion: {msg}. Check that the conversion is supported and value types match."
305            ))),
306            InnerError::Module(ModuleError::NotFound(name)) => Some(Cow::Owned(format!(
307                "Module '{name}' not found. Check the module name or path."
308            ))),
309            InnerError::Module(ModuleError::AlreadyLoaded(name)) => {
310                Some(Cow::Owned(format!("Module '{name}' is already loaded.")))
311            }
312            InnerError::Module(ModuleError::IOError(_)) => Some(Cow::Borrowed(
313                "An I/O error occurred while loading a module. Check file permissions and paths.",
314            )),
315            InnerError::Module(ModuleError::SyntaxError(SyntaxError::EnvNotFound(_, env))) => {
316                Some(Cow::Owned(format!("Environment variable '{env}' not found in module.")))
317            }
318            InnerError::Module(ModuleError::SyntaxError(SyntaxError::UnexpectedToken(token)))
319                if token.kind == TokenKind::Eof =>
320            {
321                Some(Cow::Borrowed(
322                    "The source could not be fully parsed from this position. Check for unsupported escape sequences (use \\u{XXXX} for Unicode), invalid characters, or unterminated string literals.",
323                ))
324            }
325            InnerError::Module(ModuleError::SyntaxError(SyntaxError::UnexpectedToken(_))) => Some(Cow::Borrowed(
326                "This token is not valid here. Check for typos, missing operators, or misplaced punctuation.",
327            )),
328            InnerError::Module(ModuleError::SyntaxError(SyntaxError::UnexpectedEOFDetected(_))) => Some(Cow::Borrowed(
329                "Unexpected end of input. Check for missing closing brackets, parentheses, or incomplete expressions.",
330            )),
331            InnerError::Module(ModuleError::SyntaxError(SyntaxError::InsufficientTokens(_))) => Some(Cow::Borrowed(
332                "Parsing could not continue here. Check for missing arguments, operators, or mismatched delimiters.",
333            )),
334            InnerError::Module(ModuleError::SyntaxError(SyntaxError::ExpectedClosingBracket(_, _))) => Some(
335                Cow::Borrowed("Expected a closing bracket ']'. Check your brackets for balance."),
336            ),
337            InnerError::Module(ModuleError::SyntaxError(SyntaxError::ExpectedClosingBrace(_, _))) => Some(
338                Cow::Borrowed("Expected a closing brace '}'. Check your braces for balance."),
339            ),
340            InnerError::Module(ModuleError::SyntaxError(SyntaxError::ExpectedClosingParen(_, _))) => Some(
341                Cow::Borrowed("Expected a closing parenthesis ')'. Check your parentheses for balance."),
342            ),
343            InnerError::Module(ModuleError::SyntaxError(SyntaxError::InvalidAssignmentTarget(_))) => Some(
344                Cow::Borrowed("Invalid assignment target. Ensure you're assigning to a valid variable or property."),
345            ),
346            InnerError::Module(ModuleError::SyntaxError(SyntaxError::UnknownSelector(_))) => Some(Cow::Borrowed(
347                "Unknown selector. Valid selectors include node types (e.g. .h1, .p, .code) and bracket access (e.g. .[0], .[n][m]).",
348            )),
349            InnerError::Module(ModuleError::InvalidModule) => Some(Cow::Borrowed("Invalid module format or content.")),
350            InnerError::Module(ModuleError::SyntaxError(SyntaxError::ParameterWithoutDefaultAfterDefault(_))) => {
351                Some(Cow::Borrowed(
352                    "Move this parameter before any parameters that have default values, or give it a default value.",
353                ))
354            }
355            InnerError::Module(ModuleError::SyntaxError(SyntaxError::MacroParametersCannotHaveDefaults(_))) => {
356                Some(Cow::Borrowed("Macro parameters cannot have default values."))
357            }
358            InnerError::Module(ModuleError::SyntaxError(SyntaxError::VariadicParameterMustBeLast(_))) => Some(
359                Cow::Borrowed("Variadic parameter (*) must be the last parameter in the parameter list."),
360            ),
361            InnerError::Module(ModuleError::SyntaxError(SyntaxError::MultipleVariadicParameters(_))) => Some(
362                Cow::Borrowed("Only one variadic parameter (*) is allowed per function."),
363            ),
364            InnerError::Module(ModuleError::SyntaxError(SyntaxError::MacroParametersCannotBeVariadic(_))) => {
365                Some(Cow::Borrowed("Macro parameters cannot be variadic."))
366            }
367            InnerError::Module(ModuleError::SyntaxError(SyntaxError::UnexpectedEOFAfterToken(_))) => {
368                Some(Cow::Borrowed(
369                    "An expression was expected here. Check for incomplete expressions after operators or keywords.",
370                ))
371            }
372            InnerError::Module(ModuleError::SyntaxError(SyntaxError::UnmatchedEnd(_))) => Some(Cow::Borrowed(
373                "This `end` keyword does not match any open block. \
374                Note: single-line `if` expressions do not require `end`. \
375                Check that each `end` closes a `def`, `fn`, `do`, `while`, `loop`, or `foreach` block.",
376            )),
377            InnerError::Runtime(RuntimeError::UndefinedMacro(_)) => {
378                Some(Cow::Borrowed("Macro expansion error: undefined macro used."))
379            }
380            InnerError::Runtime(RuntimeError::ArityMismatch { .. }) => {
381                Some(Cow::Borrowed("Macro expansion error: macro arity mismatch."))
382            }
383            InnerError::Runtime(RuntimeError::RecursionLimit) => {
384                Some(Cow::Borrowed("Macro expansion error: recursion limit exceeded."))
385            }
386            InnerError::Runtime(RuntimeError::InvalidMacroResultAst(_)) => {
387                Some(Cow::Borrowed("Invalid macro result AST during macro expansion."))
388            }
389            InnerError::Runtime(RuntimeError::InvalidMacroResult(_)) => Some(Cow::Borrowed(
390                "Invalid macro result: expected AST value during macro body evaluation.",
391            )),
392            InnerError::Runtime(RuntimeError::DestructuringFailed(_)) => Some(Cow::Borrowed(
393                "Destructuring pattern did not match the value. Check that the pattern structure matches the value.",
394            )),
395            #[cfg(feature = "http-import")]
396            InnerError::Module(ModuleError::HttpImportNotAllowed(_)) => Some(Cow::Borrowed(
397                "HTTP imports are only allowed at the top level. \
398                Move the HTTP import to the top-level script instead of inside an imported module.",
399            )),
400        };
401
402        msg.map(|m| Box::new(m) as Box<dyn std::fmt::Display>)
403    }
404
405    #[cold]
406    fn labels(&self) -> Option<Box<dyn Iterator<Item = miette::LabeledSpan> + '_>> {
407        let label = match &self.cause {
408            InnerError::Syntax(SyntaxError::UnexpectedToken(_)) => "unexpected token",
409            InnerError::Syntax(SyntaxError::InsufficientTokens(_)) => "expression incomplete here",
410            InnerError::Syntax(SyntaxError::ExpectedClosingParen(_, _)) => "expected `)` here",
411            InnerError::Syntax(SyntaxError::ExpectedClosingBrace(_, _)) => "expected `}` here",
412            InnerError::Syntax(SyntaxError::ExpectedClosingBracket(_, _)) => "expected `]` here",
413            InnerError::Syntax(SyntaxError::InvalidAssignmentTarget(_)) => "invalid assignment target",
414            InnerError::Syntax(SyntaxError::UnknownSelector(_)) => "unknown selector",
415            InnerError::Syntax(SyntaxError::EnvNotFound(_, _)) => "environment variable not found",
416            InnerError::Syntax(SyntaxError::ParameterWithoutDefaultAfterDefault(_)) => "parameter without default",
417            InnerError::Syntax(SyntaxError::MacroParametersCannotHaveDefaults(_)) => "parameter with default value",
418            InnerError::Syntax(SyntaxError::VariadicParameterMustBeLast(_)) => "misplaced variadic parameter",
419            InnerError::Syntax(SyntaxError::MultipleVariadicParameters(_)) => "duplicate variadic parameter",
420            InnerError::Syntax(SyntaxError::MacroParametersCannotBeVariadic(_)) => "variadic macro parameter",
421            InnerError::Syntax(SyntaxError::UnexpectedEOFDetected(_)) => "unexpected end of input",
422            InnerError::Syntax(SyntaxError::UnexpectedEOFAfterToken(_)) => "expected expression here",
423            InnerError::Syntax(SyntaxError::UnmatchedEnd(_)) => "unmatched `end` keyword",
424            InnerError::Runtime(_) => "error occurred here",
425            InnerError::Module(ModuleError::SyntaxError(SyntaxError::UnexpectedToken(_))) => "unexpected token",
426            InnerError::Module(ModuleError::SyntaxError(SyntaxError::InsufficientTokens(_))) => {
427                "expression incomplete here"
428            }
429            InnerError::Module(ModuleError::SyntaxError(SyntaxError::ExpectedClosingParen(_, _))) => {
430                "expected `)` here"
431            }
432            InnerError::Module(ModuleError::SyntaxError(SyntaxError::ExpectedClosingBrace(_, _))) => {
433                "expected `}` here"
434            }
435            InnerError::Module(ModuleError::SyntaxError(SyntaxError::ExpectedClosingBracket(_, _))) => {
436                "expected `]` here"
437            }
438            InnerError::Module(ModuleError::SyntaxError(SyntaxError::InvalidAssignmentTarget(_))) => {
439                "invalid assignment target"
440            }
441            InnerError::Module(ModuleError::SyntaxError(SyntaxError::UnknownSelector(_))) => "unknown selector",
442            InnerError::Module(ModuleError::SyntaxError(SyntaxError::EnvNotFound(_, _))) => {
443                "environment variable not found"
444            }
445            InnerError::Module(ModuleError::SyntaxError(SyntaxError::ParameterWithoutDefaultAfterDefault(_))) => {
446                "parameter without default"
447            }
448            InnerError::Module(ModuleError::SyntaxError(SyntaxError::MacroParametersCannotHaveDefaults(_))) => {
449                "parameter with default value"
450            }
451            InnerError::Module(ModuleError::SyntaxError(SyntaxError::VariadicParameterMustBeLast(_))) => {
452                "misplaced variadic parameter"
453            }
454            InnerError::Module(ModuleError::SyntaxError(SyntaxError::MultipleVariadicParameters(_))) => {
455                "duplicate variadic parameter"
456            }
457            InnerError::Module(ModuleError::SyntaxError(SyntaxError::MacroParametersCannotBeVariadic(_))) => {
458                "variadic macro parameter"
459            }
460            InnerError::Module(ModuleError::SyntaxError(SyntaxError::UnexpectedEOFDetected(_))) => {
461                "unexpected end of input"
462            }
463            InnerError::Module(ModuleError::SyntaxError(SyntaxError::UnexpectedEOFAfterToken(_))) => {
464                "expected expression here"
465            }
466            InnerError::Module(ModuleError::SyntaxError(SyntaxError::UnmatchedEnd(_))) => "unmatched `end` keyword",
467            InnerError::Module(_) => "module error here",
468        };
469
470        let primary = miette::LabeledSpan::new_with_span(Some(label.to_string()), self.location);
471
472        if let Some(secondary_span) = self.secondary_location {
473            Some(Box::new(
474                [
475                    miette::LabeledSpan::new_with_span(Some("opened here".to_string()), secondary_span),
476                    primary,
477                ]
478                .into_iter(),
479            ) as Box<dyn Iterator<Item = miette::LabeledSpan>>)
480        } else {
481            Some(Box::new(std::iter::once(primary)))
482        }
483    }
484
485    #[cold]
486    fn source_code(&self) -> Option<&dyn miette::SourceCode> {
487        Some(&self.source_code as &dyn miette::SourceCode)
488    }
489}
490
491#[cfg(test)]
492mod test {
493    use rstest::{fixture, rstest};
494    use scopeguard::defer;
495    use std::io::Write;
496    use std::{fs::File, path::PathBuf};
497
498    use super::*;
499    use crate::module::resolver::DefaultModuleResolver;
500    use crate::{Arena, Range, Shared, SharedCell, Token, TokenKind, arena::ArenaId};
501
502    type TempDir = PathBuf;
503    type TempFile = PathBuf;
504
505    fn create_file(name: &str, content: &str) -> (TempDir, TempFile) {
506        let temp_dir = std::env::temp_dir();
507        let temp_file_path = temp_dir.join(name);
508        let mut file = File::create(&temp_file_path).expect("Failed to create temp file");
509        file.write_all(content.as_bytes())
510            .expect("Failed to write to temp file");
511
512        (temp_dir, temp_file_path)
513    }
514
515    #[fixture]
516    fn module_loader() -> ModuleLoader {
517        ModuleLoader::default()
518    }
519
520    #[test]
521    fn test_from_error_with_eof_error() {
522        let cause = InnerError::Syntax(SyntaxError::UnexpectedEOFDetected(ArenaId::new(0)));
523        let module_loader: ModuleLoader = ModuleLoader::default();
524        let error = Error::from_error("line 1\nline 2", cause, module_loader);
525
526        assert_eq!(error.source_code.inner(), "line 1\nline 2");
527    }
528
529    #[rstest]
530    #[case::parse_unexpected_token(
531        InnerError::Syntax(SyntaxError::UnexpectedToken(Token {
532            range: Range::default(),
533            kind: TokenKind::Eof,
534            module_id: ArenaId::new(0),
535        })),
536        "source code"
537    )]
538    #[case::parse_unexpected_eof_detected(
539        InnerError::Syntax(SyntaxError::UnexpectedEOFDetected(ArenaId::new(0))),
540        "source code"
541    )]
542    #[case::parse_env_not_found(
543        InnerError::Syntax(SyntaxError::EnvNotFound(Token {
544            range: Range::default(),
545            kind: TokenKind::Eof,
546            module_id: ArenaId::new(0),
547        }, "ENV_VAR".into())),
548        "source code"
549    )]
550    #[case::parse_env_not_found(
551        InnerError::Syntax(SyntaxError::InsufficientTokens(Token {
552            range: Range::default(),
553            kind: TokenKind::Eof,
554            module_id: ArenaId::new(0),
555        })),
556        "source code"
557    )]
558    #[case::parse_env_not_found(
559        InnerError::Syntax(SyntaxError::InsufficientTokens(Token {
560            range: Range::default(),
561            kind: TokenKind::Eof,
562            module_id: ArenaId::new(0),
563        })),
564        "source code"
565    )]
566    #[case::eval_zero_division(
567        InnerError::Runtime(RuntimeError::ZeroDivision(Token {
568            range: Range::default(),
569            kind: TokenKind::Eof,
570            module_id: ArenaId::new(0),
571        })),
572        "source code"
573    )]
574    #[case::eval_invalid_base64_string(
575        InnerError::Runtime(RuntimeError::InvalidBase64String(Token {
576            range: Range::default(),
577            kind: TokenKind::Eof,
578            module_id: ArenaId::new(0),
579        }, "".to_string())),
580        "source code"
581    )]
582    #[case::eval_not_defined(
583        InnerError::Runtime(RuntimeError::NotDefined(Token {
584            range: Range::default(),
585            kind: TokenKind::Eof,
586            module_id: ArenaId::new(0),
587        }, "".to_string())),
588        "source code"
589    )]
590    #[case::eval_index_out_of_bounds(
591        InnerError::Runtime(RuntimeError::IndexOutOfBounds(Token {
592            range: Range::default(),
593            kind: TokenKind::Eof,
594            module_id: ArenaId::new(0),
595        }, 1.into())),
596        "source code"
597    )]
598    #[case::eval_invalid_definition(
599        InnerError::Runtime(RuntimeError::InvalidDefinition(Token {
600            range: Range::default(),
601            kind: TokenKind::Eof,
602            module_id: ArenaId::new(0),
603        }, "".to_string())),
604        "source code"
605    )]
606    #[case::eval_invalid_number_of_arguments(
607        InnerError::Runtime(RuntimeError::InvalidNumberOfArguments{token: Token {
608            range: Range::default(),
609            kind: TokenKind::Eof,
610            module_id: ArenaId::new(0),
611        }, name: "".to_string(), expected: 1, actual:1}),
612        "source code"
613    )]
614    #[case::eval_invalid_regular_expression(
615        InnerError::Runtime(RuntimeError::InvalidRegularExpression(Token {
616            range: Range::default(),
617            kind: TokenKind::Eof,
618            module_id: ArenaId::new(0),
619        }, "".to_string())),
620        "source code"
621    )]
622    #[case::eval_internal_error(
623        InnerError::Runtime(RuntimeError::InternalError(Token {
624            range: Range::default(),
625            kind: TokenKind::Eof,
626            module_id: ArenaId::new(0),
627        })),
628        "source code"
629    )]
630    #[case::eval_internal_error(
631        InnerError::Runtime(RuntimeError::Runtime(Token {
632            range: Range::default(),
633            kind: TokenKind::Eof,
634            module_id: ArenaId::new(0),
635        }, "".to_string())),
636        "source code"
637    )]
638    #[case::module_not_found(InnerError::Module(ModuleError::NotFound(Cow::Borrowed("test"))), "source code")]
639    #[case::module_io_error(InnerError::Module(ModuleError::IOError(Cow::Borrowed("test"))), "source code")]
640    #[case::module_parse_error(
641        InnerError::Module(ModuleError::SyntaxError(SyntaxError::EnvNotFound(Token {
642            range: Range::default(),
643            kind: TokenKind::Eof,
644            module_id: ArenaId::new(0),
645        }, "test".into()))),
646        "source code"
647    )]
648    #[case::module_parse_error(
649        InnerError::Module(ModuleError::SyntaxError(SyntaxError::UnexpectedToken(Token {
650            range: Range::default(),
651            kind: TokenKind::Eof,
652            module_id: ArenaId::new(0),
653        }))),
654        "source code"
655    )]
656    #[case::module_parse_error(
657        InnerError::Module(ModuleError::SyntaxError(SyntaxError::UnexpectedEOFDetected(ArenaId::new(0)))),
658        "source code"
659    )]
660    #[case::module_parse_error(
661        InnerError::Module(ModuleError::SyntaxError(SyntaxError::InsufficientTokens(Token {
662            range: Range::default(),
663            kind: TokenKind::Eof,
664            module_id: ArenaId::new(0),
665        }
666        ))),
667        "source code"
668    )]
669    fn test_from_error(
670        module_loader: module::ModuleLoader<impl ModuleResolver>,
671        #[case] cause: InnerError,
672        #[case] source_code: &str,
673    ) {
674        let error = Error::from_error(source_code, cause, module_loader);
675        assert_eq!(error.source_code.inner(), source_code);
676    }
677
678    #[test]
679    fn test_from_error_with_module_source() {
680        let (temp_dir, temp_file_path) = create_file(
681            "test_from_error_with_module_source.mq",
682            "def func1(): 42; | let val1 = 1",
683        );
684
685        defer! {
686            if temp_file_path.exists() {
687                std::fs::remove_file(&temp_file_path).expect("Failed to delete temp file");
688            }
689        }
690
691        let token_arena = Shared::new(SharedCell::new(Arena::new(10)));
692        let mut loader = ModuleLoader::new(DefaultModuleResolver::new(vec![temp_dir.clone()]));
693
694        loader
695            .load_from_file("test_from_error_with_module_source", token_arena)
696            .unwrap();
697
698        let token = Token {
699            range: Range::default(),
700            kind: TokenKind::Eof,
701            module_id: ArenaId::new(1),
702        };
703
704        let cause = InnerError::Runtime(RuntimeError::ZeroDivision(token));
705        let error = Error::from_error("top level source", cause, loader);
706
707        assert_eq!(error.source_code.inner(), "def func1(): 42; | let val1 = 1");
708    }
709
710    #[test]
711    fn test_from_error_with_builtin_module() {
712        let token_arena = Shared::new(SharedCell::new(Arena::new(10)));
713        let mut loader: ModuleLoader = ModuleLoader::default();
714        loader.load_builtin(token_arena).unwrap();
715        let token = Token {
716            range: Range::default(),
717            kind: TokenKind::Eof,
718            module_id: ArenaId::new(1),
719        };
720
721        let cause = InnerError::Runtime(RuntimeError::ZeroDivision(token));
722        let error = Error::from_error("top level source", cause, loader);
723
724        assert_eq!(error.source_code.inner(), module::BUILTIN_FILE);
725    }
726
727    #[rstest]
728    #[case::parse_env_not_found(
729        InnerError::Syntax(SyntaxError::EnvNotFound(Token {
730            range: Range::default(),
731            kind: TokenKind::Eof,
732            module_id: ArenaId::new(0),
733        }, "ENV_VAR".into()))
734    )]
735    #[case::parse_unexpected_token(
736        InnerError::Syntax(SyntaxError::UnexpectedToken(Token {
737            range: Range::default(),
738            kind: TokenKind::Eof,
739            module_id: ArenaId::new(0),
740        }))
741    )]
742    #[case::parse_unexpected_eof_detected(InnerError::Syntax(SyntaxError::UnexpectedEOFDetected(ArenaId::new(0))))]
743    #[case::parse_insufficient_tokens(
744        InnerError::Syntax(SyntaxError::InsufficientTokens(Token {
745            range: Range::default(),
746            kind: TokenKind::Eof,
747            module_id: ArenaId::new(0),
748        }))
749    )]
750    #[case::parse_expected_closing_paren(
751        InnerError::Syntax(SyntaxError::ExpectedClosingParen(Token {
752            range: Range::default(),
753            kind: TokenKind::Eof,
754            module_id: ArenaId::new(0),
755        }, None))
756    )]
757    #[case::parse_expected_closing_brace(
758        InnerError::Syntax(SyntaxError::ExpectedClosingBrace(Token {
759            range: Range::default(),
760            kind: TokenKind::Eof,
761            module_id: ArenaId::new(0),
762        }, None))
763    )]
764    #[case::parse_expected_closing_bracket(
765        InnerError::Syntax(SyntaxError::ExpectedClosingBracket(Token {
766            range: Range::default(),
767            kind: TokenKind::Eof,
768            module_id: ArenaId::new(0),
769        }, None))
770    )]
771    #[case::eval_recursion_error(InnerError::Runtime(RuntimeError::RecursionError(0)))]
772    #[case::eval_module_load_error(
773        InnerError::Runtime(RuntimeError::ModuleLoadError(ModuleError::NotFound("mod".into())))
774    )]
775    #[case::eval_user_defined(
776        InnerError::Runtime(RuntimeError::UserDefined {
777            token: Token {
778                range: Range::default(),
779                kind: TokenKind::Eof,
780                module_id: ArenaId::new(0),
781            },
782            message: "msg".to_string(),
783        })
784    )]
785    #[case::eval_invalid_base64_string(
786        InnerError::Runtime(RuntimeError::InvalidBase64String(Token {
787            range: Range::default(),
788            kind: TokenKind::Eof,
789            module_id: ArenaId::new(0),
790        }, "bad".to_string()))
791    )]
792    #[case::eval_not_defined(
793        InnerError::Runtime(RuntimeError::NotDefined(Token {
794            range: Range::default(),
795            kind: TokenKind::Eof,
796            module_id: ArenaId::new(0),
797        }, "name".to_string()))
798    )]
799    #[case::eval_datetime_format_error(
800        InnerError::Runtime(RuntimeError::DateTimeFormatError(Token {
801            range: Range::default(),
802            kind: TokenKind::Eof,
803            module_id: ArenaId::new(0),
804        }, "fmt".to_string()))
805    )]
806    #[case::eval_index_out_of_bounds(
807        InnerError::Runtime(RuntimeError::IndexOutOfBounds(Token {
808            range: Range::default(),
809            kind: TokenKind::Eof,
810            module_id: ArenaId::new(0),
811        }, 1.into()))
812    )]
813    #[case::eval_invalid_definition(
814        InnerError::Runtime(RuntimeError::InvalidDefinition(Token {
815            range: Range::default(),
816            kind: TokenKind::Eof,
817            module_id: ArenaId::new(0),
818        }, "bad".into()))
819    )]
820    #[case::eval_invalid_types(
821        InnerError::Runtime(RuntimeError::InvalidTypes {
822            token: Token {
823                range: Range::default(),
824                kind: TokenKind::Eof,
825                module_id: ArenaId::new(0),
826            },
827            name: "int".into(),
828            args: vec!["str".into()],
829        })
830    )]
831    #[case::eval_invalid_number_of_arguments(
832        InnerError::Runtime(RuntimeError::InvalidNumberOfArguments{token: Token {
833            range: Range::default(),
834            kind: TokenKind::Eof,
835            module_id: ArenaId::new(0),
836        }, name: "func".to_string(), expected: 2, actual: 1})
837    )]
838    #[case::eval_invalid_regular_expression(
839        InnerError::Runtime(RuntimeError::InvalidRegularExpression(Token {
840            range: Range::default(),
841            kind: TokenKind::Eof,
842            module_id: ArenaId::new(0),
843        }, "bad".to_string()))
844    )]
845    #[case::eval_internal_error(
846        InnerError::Runtime(RuntimeError::InternalError(Token {
847            range: Range::default(),
848            kind: TokenKind::Eof,
849            module_id: ArenaId::new(0),
850        }))
851    )]
852    #[case::eval_runtime_error(
853        InnerError::Runtime(RuntimeError::Runtime(Token {
854            range: Range::default(),
855            kind: TokenKind::Eof,
856            module_id: ArenaId::new(0),
857        }, "err".to_string()))
858    )]
859    #[case::eval_zero_division(
860        InnerError::Runtime(RuntimeError::ZeroDivision(Token {
861            range: Range::default(),
862            kind: TokenKind::Eof,
863            module_id: ArenaId::new(0),
864        }))
865    )]
866    #[case::eval_unexpected_break(InnerError::Runtime(RuntimeError::UnexpectedBreak(Token {
867        range: Range::default(),
868        kind: TokenKind::Eof,
869        module_id: ArenaId::new(0),
870    })))]
871    #[case::eval_unexpected_continue(InnerError::Runtime(RuntimeError::UnexpectedContinue(Token {
872        range: Range::default(),
873        kind: TokenKind::Eof,
874        module_id: ArenaId::new(0),
875    })))]
876    #[case::eval_env_not_found(
877        InnerError::Runtime(RuntimeError::EnvNotFound(Token {
878            range: Range::default(),
879            kind: TokenKind::Eof,
880            module_id: ArenaId::new(0),
881        }, "ENV".into()))
882    )]
883    #[case::module_not_found(InnerError::Module(ModuleError::NotFound(Cow::Borrowed("mod"))))]
884    #[case::module_io_error(InnerError::Module(ModuleError::IOError(Cow::Borrowed("io"))))]
885    #[case::module_parse_error_env_not_found(
886        InnerError::Module(ModuleError::SyntaxError(SyntaxError::EnvNotFound(Token {
887            range: Range::default(),
888            kind: TokenKind::Eof,
889            module_id: ArenaId::new(0),
890        }, "ENV".into())))
891    )]
892    #[case::module_parse_error_unexpected_token(
893        InnerError::Module(ModuleError::SyntaxError(SyntaxError::UnexpectedToken(Token {
894            range: Range::default(),
895            kind: TokenKind::Eof,
896            module_id: ArenaId::new(0),
897        })))
898    )]
899    #[case::module_parse_error_unexpected_eof(InnerError::Module(ModuleError::SyntaxError(
900        SyntaxError::UnexpectedEOFDetected(ArenaId::new(0))
901    )))]
902    #[case::module_parse_error_insufficient_tokens(
903        InnerError::Module(ModuleError::SyntaxError(SyntaxError::InsufficientTokens(Token {
904            range: Range::default(),
905            kind: TokenKind::Eof,
906            module_id: ArenaId::new(0),
907        })))
908    )]
909    #[case::module_invalid_module(InnerError::Module(ModuleError::InvalidModule))]
910    #[case::module_parse_error_expected_closing_paren(
911        InnerError::Module(ModuleError::SyntaxError(SyntaxError::ExpectedClosingParen(Token {
912            range: Range::default(),
913            kind: TokenKind::Eof,
914            module_id: ArenaId::new(0),
915        }, None)))
916    )]
917    #[case::module_parse_error_expected_closing_brace(
918        InnerError::Module(ModuleError::SyntaxError(SyntaxError::ExpectedClosingBrace(Token {
919            range: Range::default(),
920            kind: TokenKind::Eof,
921            module_id: ArenaId::new(0),
922        }, None)))
923    )]
924    #[case::module_parse_error_expected_closing_bracket(
925        InnerError::Module(ModuleError::SyntaxError(SyntaxError::ExpectedClosingBracket(Token {
926            range: Range::default(),
927            kind: TokenKind::Eof,
928            module_id: ArenaId::new(0),
929        }, None)))
930    )]
931    fn test_diagnostic_code_and_help(module_loader: ModuleLoader<impl ModuleResolver>, #[case] cause: InnerError) {
932        let error = Error::from_error("source code", cause, module_loader);
933        // code() and help() must not panic
934        let _ = error.code();
935        let _ = error.help();
936    }
937
938    #[test]
939    fn test_code_shows_category() {
940        let module_loader: ModuleLoader = ModuleLoader::default();
941        let runtime_error = Error::from_error(
942            "source code",
943            InnerError::Runtime(RuntimeError::ZeroDivision(Token {
944                range: Range::default(),
945                kind: TokenKind::Eof,
946                module_id: ArenaId::new(0),
947            })),
948            module_loader.clone(),
949        );
950        assert_eq!(
951            runtime_error.code().map(|c| c.to_string()),
952            Some("mq::runtime".to_string())
953        );
954
955        let syntax_error = Error::from_error(
956            "source code",
957            InnerError::Syntax(SyntaxError::UnexpectedToken(Token {
958                range: Range::default(),
959                kind: TokenKind::Eof,
960                module_id: ArenaId::new(0),
961            })),
962            module_loader,
963        );
964        assert_eq!(
965            syntax_error.code().map(|c| c.to_string()),
966            Some("mq::syntax".to_string())
967        );
968    }
969
970    #[test]
971    fn test_env_not_found_help_includes_name() {
972        let module_loader: ModuleLoader = ModuleLoader::default();
973        let cause = InnerError::Runtime(RuntimeError::EnvNotFound(
974            Token {
975                range: Range::default(),
976                kind: TokenKind::Eof,
977                module_id: ArenaId::new(0),
978            },
979            "MY_VAR".into(),
980        ));
981        let error = Error::from_error("source code", cause, module_loader);
982        let help = error.help().map(|h| h.to_string());
983        assert_eq!(
984            help,
985            Some("Environment variable 'MY_VAR' not found. Did you forget to set it?".to_string())
986        );
987    }
988
989    #[test]
990    fn test_invalid_convert_help_shows_message() {
991        let module_loader: ModuleLoader = ModuleLoader::default();
992        let cause = InnerError::Runtime(RuntimeError::InvalidConvert(
993            Token {
994                range: Range::default(),
995                kind: TokenKind::Eof,
996                module_id: ArenaId::new(0),
997            },
998            "cannot convert array to string".to_string(),
999        ));
1000        let error = Error::from_error("source code", cause, module_loader);
1001        let help = error.help().map(|h| h.to_string());
1002        assert!(
1003            help.as_deref().unwrap_or("").contains("cannot convert array to string"),
1004            "help text should contain the conversion message, got: {help:?}"
1005        );
1006    }
1007}