blueprint_engine_core/
error.rs

1use std::sync::Arc;
2use thiserror::Error;
3
4#[derive(Debug, Clone)]
5pub struct Span {
6    pub start: usize,
7    pub end: usize,
8}
9
10#[derive(Debug, Clone)]
11pub struct SourceLocation {
12    pub file: Option<String>,
13    pub line: usize,
14    pub column: usize,
15    pub span: Option<Span>,
16}
17
18impl std::fmt::Display for SourceLocation {
19    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
20        match &self.file {
21            Some(file) => write!(f, "{}:{}:{}", file, self.line, self.column),
22            None => write!(f, "line {}:{}", self.line, self.column),
23        }
24    }
25}
26
27#[derive(Debug, Clone)]
28pub struct StackFrame {
29    pub function_name: String,
30    pub file: Option<String>,
31    pub line: usize,
32    pub column: usize,
33}
34
35impl std::fmt::Display for StackFrame {
36    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
37        let location = match &self.file {
38            Some(file) => format!("{}:{}:{}", file, self.line, self.column),
39            None => format!("line {}:{}", self.line, self.column),
40        };
41        write!(f, "  at {} ({})", self.function_name, location)
42    }
43}
44
45#[derive(Debug, Clone, Default)]
46pub struct StackTrace {
47    pub frames: Vec<StackFrame>,
48}
49
50impl StackTrace {
51    pub fn new() -> Self {
52        Self { frames: Vec::new() }
53    }
54
55    pub fn push(&mut self, frame: StackFrame) {
56        self.frames.push(frame);
57    }
58
59    pub fn is_empty(&self) -> bool {
60        self.frames.is_empty()
61    }
62}
63
64impl std::fmt::Display for StackTrace {
65    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
66        if self.frames.is_empty() {
67            return Ok(());
68        }
69        writeln!(f, "Stack trace (most recent call last):")?;
70        for frame in &self.frames {
71            writeln!(f, "{}", frame)?;
72        }
73        Ok(())
74    }
75}
76
77#[derive(Debug, Clone, Error)]
78pub enum BlueprintError {
79    #[error("Parse error at {location}: {message}")]
80    ParseError {
81        location: SourceLocation,
82        message: String,
83    },
84
85    #[error("Type error: expected {expected}, got {actual}")]
86    TypeError { expected: String, actual: String },
87
88    #[error("Name error: undefined variable '{name}'")]
89    NameError { name: String },
90
91    #[error("Import error: {message}")]
92    ImportError { message: String },
93
94    #[error("Attribute error: '{type_name}' has no attribute '{attr}'")]
95    AttributeError { type_name: String, attr: String },
96
97    #[error("Index error: {message}")]
98    IndexError { message: String },
99
100    #[error("Key error: key not found: {key}")]
101    KeyError { key: String },
102
103    #[error("Value error: {message}")]
104    ValueError { message: String },
105
106    #[error("Argument error: {message}")]
107    ArgumentError { message: String },
108
109    #[error("Division by zero")]
110    DivisionByZero,
111
112    #[error("I/O error: {path}: {message}")]
113    IoError { path: String, message: String },
114
115    #[error("HTTP error: {url}: {message}")]
116    HttpError { url: String, message: String },
117
118    #[error("Process error: {command}: {message}")]
119    ProcessError { command: String, message: String },
120
121    #[error("JSON error: {message}")]
122    JsonError { message: String },
123
124    #[error("Glob error: {message}")]
125    GlobError { message: String },
126
127    #[error("Assertion failed: {message}")]
128    AssertionError { message: String },
129
130    #[error("{message}")]
131    UserError { message: String },
132
133    #[error("Not callable: {type_name}")]
134    NotCallable { type_name: String },
135
136    #[error("Internal error: {message}")]
137    InternalError { message: String },
138
139    #[error("Unsupported: {message}")]
140    Unsupported { message: String },
141
142    #[error("break")]
143    Break,
144
145    #[error("continue")]
146    Continue,
147
148    #[error("return")]
149    Return { value: Arc<crate::Value> },
150
151    #[error("exit with code {code}")]
152    Exit { code: i32 },
153
154    #[error("")]
155    Silent,
156
157    #[error("{error}")]
158    WithStack {
159        error: Box<BlueprintError>,
160        stack: StackTrace,
161        location: Option<SourceLocation>,
162    },
163}
164
165impl BlueprintError {
166    pub fn with_file(self, file: String) -> Self {
167        match self {
168            BlueprintError::ParseError { location, message } => BlueprintError::ParseError {
169                location: SourceLocation {
170                    file: Some(file),
171                    ..location
172                },
173                message,
174            },
175            other => other,
176        }
177    }
178
179    pub fn is_control_flow(&self) -> bool {
180        matches!(
181            self,
182            BlueprintError::Break | BlueprintError::Continue | BlueprintError::Return { .. } | BlueprintError::Exit { .. }
183        )
184    }
185
186    pub fn with_stack_frame(self, frame: StackFrame) -> Self {
187        if self.is_control_flow() {
188            return self;
189        }
190
191        match self {
192            BlueprintError::WithStack { error, mut stack, location } => {
193                stack.push(frame);
194                BlueprintError::WithStack { error, stack, location }
195            }
196            other => {
197                let mut stack = StackTrace::new();
198                stack.push(frame);
199                BlueprintError::WithStack {
200                    error: Box::new(other),
201                    stack,
202                    location: None,
203                }
204            }
205        }
206    }
207
208    pub fn with_location(self, loc: SourceLocation) -> Self {
209        if self.is_control_flow() {
210            return self;
211        }
212
213        match self {
214            BlueprintError::WithStack { error, stack, location: _ } => {
215                BlueprintError::WithStack { error, stack, location: Some(loc) }
216            }
217            other => {
218                BlueprintError::WithStack {
219                    error: Box::new(other),
220                    stack: StackTrace::new(),
221                    location: Some(loc),
222                }
223            }
224        }
225    }
226
227    pub fn stack_trace(&self) -> Option<&StackTrace> {
228        match self {
229            BlueprintError::WithStack { stack, .. } => Some(stack),
230            _ => None,
231        }
232    }
233
234    pub fn error_location(&self) -> Option<&SourceLocation> {
235        match self {
236            BlueprintError::WithStack { location, .. } => location.as_ref(),
237            BlueprintError::ParseError { location, .. } => Some(location),
238            _ => None,
239        }
240    }
241
242    pub fn inner_error(&self) -> &BlueprintError {
243        match self {
244            BlueprintError::WithStack { error, .. } => error.inner_error(),
245            other => other,
246        }
247    }
248
249    pub fn format_with_stack(&self) -> String {
250        let mut result = String::new();
251
252        if let Some(loc) = self.error_location() {
253            result.push_str(&format!("Error at {}: ", loc));
254        } else {
255            result.push_str("Error: ");
256        }
257
258        result.push_str(&format!("{}", self.inner_error()));
259
260        if let Some(stack) = self.stack_trace() {
261            if !stack.is_empty() {
262                result.push_str("\n\n");
263                result.push_str(&format!("{}", stack));
264            }
265        }
266
267        result
268    }
269}
270
271pub type Result<T> = std::result::Result<T, BlueprintError>;