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
52impl fmt::Display for RuntimeError {
53    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
54        // If we have both span and source, use ariadne for beautiful formatting
55        if let (Some(span), Some(source)) = (&self.span, &self.source) {
56            let mut output = Vec::new();
57
58            // Disable colors when compiling for WASM to avoid ANSI escape sequences
59            // in browser/Node.js environments where they won't be interpreted
60            #[cfg(target_arch = "wasm32")]
61            let config = Config::default().with_color(false);
62
63            #[cfg(not(target_arch = "wasm32"))]
64            let config = Config::default();
65
66            // Build label - don't set color in WASM builds as it may override config
67            #[cfg(target_arch = "wasm32")]
68            let label = Label::new(span.start_byte..span.end_byte).with_message(&self.message);
69
70            #[cfg(not(target_arch = "wasm32"))]
71            let label = Label::new(span.start_byte..span.end_byte)
72                .with_message(&self.message)
73                .with_color(Color::Red);
74
75            Report::build(ReportKind::Error, (), span.start_byte)
76                .with_message(&self.message)
77                .with_label(label)
78                .with_config(config)
79                .finish()
80                .write(Source::from(&**source), &mut output)
81                .map_err(|_| fmt::Error)?;
82
83            let output_str = String::from_utf8(output).map_err(|_| fmt::Error)?;
84            write!(f, "{}", output_str)
85        } else {
86            // Fallback to simple error message
87            write!(f, "[evaluation error] {}", self.message)
88        }
89    }
90}
91
92impl std::error::Error for RuntimeError {}
93
94// Allow conversion from anyhow::Error to RuntimeError (for backwards compatibility)
95impl From<anyhow::Error> for RuntimeError {
96    fn from(err: anyhow::Error) -> Self {
97        RuntimeError::new(err.to_string())
98    }
99}
100
101// Allow conversion from &str for convenience
102impl From<&str> for RuntimeError {
103    fn from(s: &str) -> Self {
104        RuntimeError::new(s.to_string())
105    }
106}
107
108// Allow conversion from String for convenience
109impl From<String> for RuntimeError {
110    fn from(s: String) -> Self {
111        RuntimeError::new(s)
112    }
113}
114
115#[cfg(test)]
116mod tests {
117    use super::*;
118
119    #[test]
120    fn test_runtime_error_with_span() {
121        let source = "output x = undefined_variable + 5".to_string();
122
123        // Simulate an error at the location of "undefined_variable"
124        let span = Span::new(11, 29, 1, 12); // byte positions 11-29, line 1, col 12
125
126        let error = RuntimeError::with_span(
127            "unknown identifier: undefined_variable".to_string(),
128            span,
129            source.into(),
130        );
131
132        let error_msg = format!("{}", error);
133
134        // The error message should contain the source line and point to the error location
135        assert!(error_msg.contains("undefined_variable"));
136        println!("\n=== DEMO: Improved Error Message ===");
137        println!("{}", error);
138        println!("=====================================\n");
139    }
140
141    #[test]
142    fn test_runtime_error_without_span() {
143        let error = RuntimeError::new("simple error".to_string());
144        let error_msg = format!("{}", error);
145        assert_eq!(error_msg, "[evaluation error] simple error");
146    }
147}