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>;