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("Permission denied: {operation} on '{resource}'")]
143    PermissionDenied {
144        operation: String,
145        resource: String,
146        hint: String,
147    },
148
149    #[error("break")]
150    Break,
151
152    #[error("continue")]
153    Continue,
154
155    #[error("return")]
156    Return { value: Arc<crate::Value> },
157
158    #[error("exit with code {code}")]
159    Exit { code: i32 },
160
161    #[error("")]
162    Silent,
163
164    #[error("{error}")]
165    WithStack {
166        error: Box<BlueprintError>,
167        stack: StackTrace,
168        location: Option<SourceLocation>,
169    },
170}
171
172impl BlueprintError {
173    pub fn with_file(self, file: String) -> Self {
174        match self {
175            BlueprintError::ParseError { location, message } => BlueprintError::ParseError {
176                location: SourceLocation {
177                    file: Some(file),
178                    ..location
179                },
180                message,
181            },
182            other => other,
183        }
184    }
185
186    pub fn is_control_flow(&self) -> bool {
187        matches!(
188            self,
189            BlueprintError::Break
190                | BlueprintError::Continue
191                | BlueprintError::Return { .. }
192                | BlueprintError::Exit { .. }
193        )
194    }
195
196    pub fn with_stack_frame(self, frame: StackFrame) -> Self {
197        if self.is_control_flow() {
198            return self;
199        }
200
201        match self {
202            BlueprintError::WithStack {
203                error,
204                mut stack,
205                location,
206            } => {
207                stack.push(frame);
208                BlueprintError::WithStack {
209                    error,
210                    stack,
211                    location,
212                }
213            }
214            other => {
215                let mut stack = StackTrace::new();
216                stack.push(frame);
217                BlueprintError::WithStack {
218                    error: Box::new(other),
219                    stack,
220                    location: None,
221                }
222            }
223        }
224    }
225
226    pub fn with_location(self, loc: SourceLocation) -> Self {
227        if self.is_control_flow() {
228            return self;
229        }
230
231        match self {
232            BlueprintError::WithStack {
233                error,
234                stack,
235                location: _,
236            } => BlueprintError::WithStack {
237                error,
238                stack,
239                location: Some(loc),
240            },
241            other => BlueprintError::WithStack {
242                error: Box::new(other),
243                stack: StackTrace::new(),
244                location: Some(loc),
245            },
246        }
247    }
248
249    pub fn stack_trace(&self) -> Option<&StackTrace> {
250        match self {
251            BlueprintError::WithStack { stack, .. } => Some(stack),
252            _ => None,
253        }
254    }
255
256    pub fn error_location(&self) -> Option<&SourceLocation> {
257        match self {
258            BlueprintError::WithStack { location, .. } => location.as_ref(),
259            BlueprintError::ParseError { location, .. } => Some(location),
260            _ => None,
261        }
262    }
263
264    pub fn inner_error(&self) -> &BlueprintError {
265        match self {
266            BlueprintError::WithStack { error, .. } => error.inner_error(),
267            other => other,
268        }
269    }
270
271    pub fn format_with_stack(&self) -> String {
272        let mut result = String::new();
273
274        if let Some(loc) = self.error_location() {
275            result.push_str(&format!("Error at {}: ", loc));
276        } else {
277            result.push_str("Error: ");
278        }
279
280        result.push_str(&format!("{}", self.inner_error()));
281
282        if let Some(stack) = self.stack_trace() {
283            if !stack.is_empty() {
284                result.push_str("\n\n");
285                result.push_str(&format!("{}", stack));
286            }
287        }
288
289        result
290    }
291}
292
293pub type Result<T> = std::result::Result<T, BlueprintError>;