blots_core/
error.rs

1use crate::ast::Span;
2use ariadne::{Config, Label, Report, ReportKind, Source};
3use std::{fmt, rc::Rc};
4
5// Only import Color when not building for WASM (since we don't use colors there)
6#[cfg(not(target_arch = "wasm32"))]
7use ariadne::Color;
8
9/// Runtime error with source location information for beautiful error reporting
10#[derive(Debug)]
11pub struct RuntimeError {
12    pub message: String,
13    pub span: Option<Span>,
14    pub source: Option<Rc<str>>,
15}
16
17impl RuntimeError {
18    /// Create a new runtime error without source location
19    pub fn new(message: String) -> Self {
20        Self {
21            message,
22            span: None,
23            source: None,
24        }
25    }
26
27    /// Create a runtime error with source location
28    pub fn with_span(message: String, span: Span, source: Rc<str>) -> Self {
29        Self {
30            message,
31            span: Some(span),
32            source: Some(source),
33        }
34    }
35
36    /// Add span information to an existing error (useful for wrapping function call errors)
37    pub fn with_call_site(self, span: Span, source: Rc<str>) -> Self {
38        // If the error already has span info, keep it (it's more specific)
39        // Otherwise, add the call site span
40        if self.span.is_some() {
41            self
42        } else {
43            Self {
44                message: self.message,
45                span: Some(span),
46                source: Some(source),
47            }
48        }
49    }
50
51    /// Prepend function name context to the error message while preserving span information
52    pub fn with_function_context(self, function_name: &str) -> Self {
53        Self {
54            message: format!("in {}: {}", function_name, self.message),
55            span: self.span,
56            source: self.source,
57        }
58    }
59}
60
61impl fmt::Display for RuntimeError {
62    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
63        // If we have both span and source, use ariadne for beautiful formatting
64        if let (Some(span), Some(source)) = (&self.span, &self.source) {
65            let mut output = Vec::new();
66
67            // Disable colors when compiling for WASM to avoid ANSI escape sequences
68            // in browser/Node.js environments where they won't be interpreted
69            #[cfg(target_arch = "wasm32")]
70            let config = Config::default().with_color(false);
71
72            #[cfg(not(target_arch = "wasm32"))]
73            let config = Config::default();
74
75            // Build label - don't set color in WASM builds as it may override config
76            #[cfg(target_arch = "wasm32")]
77            let label = Label::new(span.start_byte..span.end_byte).with_message(&self.message);
78
79            #[cfg(not(target_arch = "wasm32"))]
80            let label = Label::new(span.start_byte..span.end_byte)
81                .with_message(&self.message)
82                .with_color(Color::Red);
83
84            Report::build(ReportKind::Error, (), span.start_byte)
85                .with_message(&self.message)
86                .with_label(label)
87                .with_config(config)
88                .finish()
89                .write(Source::from(&**source), &mut output)
90                .map_err(|_| fmt::Error)?;
91
92            let output_str = String::from_utf8(output).map_err(|_| fmt::Error)?;
93            write!(f, "{}", output_str)
94        } else {
95            // Fallback to simple error message
96            write!(f, "[evaluation error] {}", self.message)
97        }
98    }
99}
100
101impl std::error::Error for RuntimeError {}
102
103// Allow conversion from anyhow::Error to RuntimeError (for backwards compatibility)
104impl From<anyhow::Error> for RuntimeError {
105    fn from(err: anyhow::Error) -> Self {
106        RuntimeError::new(err.to_string())
107    }
108}
109
110// Allow conversion from &str for convenience
111impl From<&str> for RuntimeError {
112    fn from(s: &str) -> Self {
113        RuntimeError::new(s.to_string())
114    }
115}
116
117// Allow conversion from String for convenience
118impl From<String> for RuntimeError {
119    fn from(s: String) -> Self {
120        RuntimeError::new(s)
121    }
122}
123
124#[cfg(test)]
125mod tests {
126    use super::*;
127
128    #[test]
129    fn test_runtime_error_with_span() {
130        let source = "output x = undefined_variable + 5".to_string();
131
132        // Simulate an error at the location of "undefined_variable"
133        let span = Span::new(11, 29, 1, 12); // byte positions 11-29, line 1, col 12
134
135        let error = RuntimeError::with_span(
136            "unknown identifier: undefined_variable".to_string(),
137            span,
138            source.into(),
139        );
140
141        let error_msg = format!("{}", error);
142
143        // The error message should contain the source line and point to the error location
144        assert!(error_msg.contains("undefined_variable"));
145        println!("\n=== DEMO: Improved Error Message ===");
146        println!("{}", error);
147        println!("=====================================\n");
148    }
149
150    #[test]
151    fn test_runtime_error_without_span() {
152        let error = RuntimeError::new("simple error".to_string());
153        let error_msg = format!("{}", error);
154        assert_eq!(error_msg, "[evaluation error] simple error");
155    }
156}