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)
69                .with_message(&self.message);
70
71            #[cfg(not(target_arch = "wasm32"))]
72            let label = Label::new(span.start_byte..span.end_byte)
73                .with_message(&self.message)
74                .with_color(Color::Red);
75
76            Report::build(ReportKind::Error, (), span.start_byte)
77                .with_message(&self.message)
78                .with_label(label)
79                .with_config(config)
80                .finish()
81                .write(Source::from(&**source), &mut output)
82                .map_err(|_| fmt::Error)?;
83
84            let output_str = String::from_utf8(output).map_err(|_| fmt::Error)?;
85            write!(f, "{}", output_str)
86        } else {
87            // Fallback to simple error message
88            write!(f, "[evaluation error] {}", self.message)
89        }
90    }
91}
92
93impl std::error::Error for RuntimeError {}
94
95// Allow conversion from anyhow::Error to RuntimeError (for backwards compatibility)
96impl From<anyhow::Error> for RuntimeError {
97    fn from(err: anyhow::Error) -> Self {
98        RuntimeError::new(err.to_string())
99    }
100}
101
102// Allow conversion from &str for convenience
103impl From<&str> for RuntimeError {
104    fn from(s: &str) -> Self {
105        RuntimeError::new(s.to_string())
106    }
107}
108
109// Allow conversion from String for convenience
110impl From<String> for RuntimeError {
111    fn from(s: String) -> Self {
112        RuntimeError::new(s)
113    }
114}
115
116#[cfg(test)]
117mod tests {
118    use super::*;
119
120    #[test]
121    fn test_runtime_error_with_span() {
122        let source = "output x = undefined_variable + 5".to_string();
123
124        // Simulate an error at the location of "undefined_variable"
125        let span = Span::new(11, 29, 1, 12); // byte positions 11-29, line 1, col 12
126
127        let error = RuntimeError::with_span(
128            "unknown identifier: undefined_variable".to_string(),
129            span,
130            source.into(),
131        );
132
133        let error_msg = format!("{}", error);
134
135        // The error message should contain the source line and point to the error location
136        assert!(error_msg.contains("undefined_variable"));
137        println!("\n=== DEMO: Improved Error Message ===");
138        println!("{}", error);
139        println!("=====================================\n");
140    }
141
142    #[test]
143    fn test_runtime_error_without_span() {
144        let error = RuntimeError::new("simple error".to_string());
145        let error_msg = format!("{}", error);
146        assert_eq!(error_msg, "[evaluation error] simple error");
147    }
148}