1use schemars::JsonSchema;
2use serde::Deserialize;
3use serde::Serialize;
4use thiserror::Error;
5
6use crate::CompilationIssue;
7use crate::SourceRange;
8
9const RETRYABLE_ENGINE_MESSAGE_MARKER_SETS: &[&[&str]] = &[
10 &["modeling connection", "interrupted", "please reconnect"],
11 &["modeling connection", "heartbeats", "please reconnect"],
12];
13
14pub trait IsRetryable {
15 fn is_retryable(&self) -> bool;
18}
19
20#[derive(Error, Debug, Serialize, Deserialize, ts_rs::TS, Clone, PartialEq, Eq, JsonSchema)]
21#[ts(export)]
22#[serde(tag = "kind", rename_all = "snake_case")]
23pub enum KclError {
24 #[error("lexical: {details:?}")]
25 Lexical { details: KclErrorDetails },
26 #[error("syntax: {details:?}")]
27 Syntax { details: KclErrorDetails },
28 #[error("semantic: {details:?}")]
29 Semantic { details: KclErrorDetails },
30 #[error("import cycle: {details:?}")]
31 ImportCycle { details: KclErrorDetails },
32 #[error("argument: {details:?}")]
33 Argument { details: KclErrorDetails },
34 #[error("type: {details:?}")]
35 Type { details: KclErrorDetails },
36 #[error("user-defined: {details:?}")]
37 UserDefined { details: KclErrorDetails },
38 #[error("i/o: {details:?}")]
39 Io { details: KclErrorDetails },
40 #[error("unexpected: {details:?}")]
41 Unexpected { details: KclErrorDetails },
42 #[error("value already defined: {details:?}")]
43 ValueAlreadyDefined { details: KclErrorDetails },
44 #[error("undefined value: {details:?}")]
45 UndefinedValue {
46 details: KclErrorDetails,
47 name: Option<String>,
48 },
49 #[error("invalid expression: {details:?}")]
50 InvalidExpression { details: KclErrorDetails },
51 #[error("max call stack size exceeded: {details:?}")]
52 MaxCallStack { details: KclErrorDetails },
53 #[error("refactor: {details:?}")]
54 Refactor { details: KclErrorDetails },
55 #[error("engine: {details:?}")]
56 Engine { details: KclErrorDetails },
57 #[error("engine hangup: {details:?}")]
58 EngineHangup {
59 details: KclErrorDetails,
60 api_call_id: Option<String>,
61 },
62 #[error("engine internal: {details:?}")]
63 EngineInternal { details: KclErrorDetails },
64 #[error("internal error, please report to KittyCAD team: {details:?}")]
65 Internal { details: KclErrorDetails },
66}
67
68impl IsRetryable for KclError {
69 fn is_retryable(&self) -> bool {
70 matches!(self, KclError::EngineHangup { .. } | KclError::EngineInternal { .. })
71 }
72}
73#[derive(
74 Debug, Serialize, Deserialize, ts_rs::TS, Clone, PartialEq, Eq, thiserror::Error, miette::Diagnostic, JsonSchema,
75)]
76#[serde(rename_all = "camelCase")]
77#[error("{message}")]
78#[ts(export)]
79pub struct KclErrorDetails {
80 #[label(collection, "Errors")]
81 pub source_ranges: Vec<SourceRange>,
82 pub backtrace: Vec<BacktraceItem>,
83 #[serde(rename = "msg")]
84 pub message: String,
85}
86
87impl KclErrorDetails {
88 pub fn new(message: String, source_ranges: Vec<SourceRange>) -> KclErrorDetails {
89 let backtrace = source_ranges
90 .iter()
91 .map(|s| BacktraceItem {
92 source_range: *s,
93 fn_name: None,
94 kind: BacktraceItemKind::Call,
95 })
96 .collect();
97 KclErrorDetails {
98 source_ranges,
99 backtrace,
100 message,
101 }
102 }
103}
104
105impl KclError {
106 pub fn internal(message: String) -> KclError {
107 KclError::Internal {
108 details: KclErrorDetails {
109 source_ranges: Default::default(),
110 backtrace: Default::default(),
111 message,
112 },
113 }
114 }
115
116 pub fn new_internal(details: KclErrorDetails) -> KclError {
117 KclError::Internal { details }
118 }
119
120 pub fn new_import_cycle(details: KclErrorDetails) -> KclError {
121 KclError::ImportCycle { details }
122 }
123
124 pub fn new_argument(details: KclErrorDetails) -> KclError {
125 KclError::Argument { details }
126 }
127
128 pub fn new_semantic(details: KclErrorDetails) -> KclError {
129 KclError::Semantic { details }
130 }
131
132 pub fn new_value_already_defined(details: KclErrorDetails) -> KclError {
133 KclError::ValueAlreadyDefined { details }
134 }
135
136 pub fn new_syntax(details: KclErrorDetails) -> KclError {
137 KclError::Syntax { details }
138 }
139
140 pub fn new_io(details: KclErrorDetails) -> KclError {
141 KclError::Io { details }
142 }
143
144 pub fn new_invalid_expression(details: KclErrorDetails) -> KclError {
145 KclError::InvalidExpression { details }
146 }
147
148 pub fn new_max_call_stack(details: KclErrorDetails) -> KclError {
149 KclError::MaxCallStack { details }
150 }
151
152 pub fn refactor(message: String) -> KclError {
153 KclError::Refactor {
154 details: KclErrorDetails {
155 source_ranges: Default::default(),
156 backtrace: Default::default(),
157 message,
158 },
159 }
160 }
161
162 pub fn new_engine(details: KclErrorDetails) -> KclError {
163 if details.message.eq_ignore_ascii_case("internal error") {
164 KclError::EngineInternal { details }
165 } else if is_retryable_engine_message(&details.message) {
166 KclError::EngineHangup {
167 details,
168 api_call_id: None,
169 }
170 } else {
171 KclError::Engine { details }
172 }
173 }
174
175 pub fn new_engine_hangup(details: KclErrorDetails, api_call_id: Option<String>) -> KclError {
176 KclError::EngineHangup { details, api_call_id }
177 }
178
179 pub fn new_lexical(details: KclErrorDetails) -> KclError {
180 KclError::Lexical { details }
181 }
182
183 pub fn new_undefined_value(details: KclErrorDetails, name: Option<String>) -> KclError {
184 KclError::UndefinedValue { details, name }
185 }
186
187 pub fn new_type(details: KclErrorDetails) -> KclError {
188 KclError::Type { details }
189 }
190
191 pub fn new_user_defined(details: KclErrorDetails) -> KclError {
192 KclError::UserDefined { details }
193 }
194
195 pub fn is_undefined_value(&self) -> bool {
196 matches!(self, KclError::UndefinedValue { .. })
197 }
198
199 pub fn get_message(&self) -> String {
201 format!("{}: {}", self.error_type(), self.message())
202 }
203
204 pub fn error_type(&self) -> &'static str {
205 match self {
206 KclError::Lexical { .. } => "lexical",
207 KclError::Syntax { .. } => "syntax",
208 KclError::Semantic { .. } => "semantic",
209 KclError::ImportCycle { .. } => "import cycle",
210 KclError::Argument { .. } => "argument",
211 KclError::Type { .. } => "type",
212 KclError::UserDefined { .. } => "user-defined",
213 KclError::Io { .. } => "i/o",
214 KclError::Unexpected { .. } => "unexpected",
215 KclError::ValueAlreadyDefined { .. } => "value already defined",
216 KclError::UndefinedValue { .. } => "undefined value",
217 KclError::InvalidExpression { .. } => "invalid expression",
218 KclError::MaxCallStack { .. } => "max call stack",
219 KclError::Refactor { .. } => "refactor",
220 KclError::Engine { .. } => "engine",
221 KclError::EngineHangup { .. } => "engine hangup",
222 KclError::EngineInternal { .. } => "engine internal",
223 KclError::Internal { .. } => "internal",
224 }
225 }
226
227 pub fn details(&self) -> &KclErrorDetails {
229 match self {
230 KclError::Lexical { details: e }
231 | KclError::Syntax { details: e }
232 | KclError::Semantic { details: e }
233 | KclError::ImportCycle { details: e }
234 | KclError::Argument { details: e }
235 | KclError::Type { details: e }
236 | KclError::UserDefined { details: e }
237 | KclError::Io { details: e }
238 | KclError::Unexpected { details: e }
239 | KclError::ValueAlreadyDefined { details: e }
240 | KclError::UndefinedValue { details: e, .. }
241 | KclError::InvalidExpression { details: e }
242 | KclError::MaxCallStack { details: e }
243 | KclError::Refactor { details: e }
244 | KclError::Engine { details: e }
245 | KclError::EngineHangup { details: e, .. }
246 | KclError::EngineInternal { details: e }
247 | KclError::Internal { details: e } => e,
248 }
249 }
250
251 pub fn details_mut(&mut self) -> &mut KclErrorDetails {
253 match self {
254 KclError::Lexical { details: e }
255 | KclError::Syntax { details: e }
256 | KclError::Semantic { details: e }
257 | KclError::ImportCycle { details: e }
258 | KclError::Argument { details: e }
259 | KclError::Type { details: e }
260 | KclError::UserDefined { details: e }
261 | KclError::Io { details: e }
262 | KclError::Unexpected { details: e }
263 | KclError::ValueAlreadyDefined { details: e }
264 | KclError::UndefinedValue { details: e, .. }
265 | KclError::InvalidExpression { details: e }
266 | KclError::MaxCallStack { details: e }
267 | KclError::Refactor { details: e }
268 | KclError::Engine { details: e }
269 | KclError::EngineHangup { details: e, .. }
270 | KclError::EngineInternal { details: e }
271 | KclError::Internal { details: e } => e,
272 }
273 }
274
275 pub fn source_ranges(&self) -> Vec<SourceRange> {
276 self.details().source_ranges.clone()
277 }
278
279 pub fn message(&self) -> &str {
281 &self.details().message
282 }
283
284 pub fn backtrace(&self) -> Vec<BacktraceItem> {
285 self.details().backtrace.clone()
286 }
287
288 pub fn override_source_ranges(&self, source_ranges: Vec<SourceRange>) -> Self {
289 let mut new = self.clone();
290 let e = new.details_mut();
291 e.backtrace = source_ranges
292 .iter()
293 .map(|s| BacktraceItem {
294 source_range: *s,
295 fn_name: None,
296 kind: BacktraceItemKind::Call,
297 })
298 .collect();
299 e.source_ranges = source_ranges;
300
301 new
302 }
303
304 pub fn add_unwind_location(&self, last_fn_name: Option<String>, source_range: SourceRange) -> Self {
305 let mut new = self.clone();
306 let e = new.details_mut();
307 if let Some(item) = e.backtrace.last_mut() {
308 item.fn_name = last_fn_name;
309 }
310 e.backtrace.push(BacktraceItem {
311 source_range,
312 fn_name: None,
313 kind: BacktraceItemKind::Call,
314 });
315 e.source_ranges.push(source_range);
316
317 new
318 }
319
320 pub fn add_import_location(&self, import_path: &str, source_range: SourceRange) -> Self {
329 let mut new = self.clone();
330 let e = new.details_mut();
331 if let Some(item) = e.backtrace.last_mut() {
332 item.fn_name = Some(format!("import {import_path}"));
333 item.kind = BacktraceItemKind::Import;
334 }
335 e.backtrace.push(BacktraceItem {
336 source_range,
337 fn_name: None,
338 kind: BacktraceItemKind::Call,
339 });
340 e.source_ranges.push(source_range);
341
342 new
343 }
344}
345
346#[derive(
347 Debug, Clone, PartialEq, Eq, Serialize, Deserialize, ts_rs::TS, thiserror::Error, miette::Diagnostic, JsonSchema,
348)]
349#[serde(rename_all = "camelCase")]
350#[ts(export)]
351pub struct BacktraceItem {
352 pub source_range: SourceRange,
353 pub fn_name: Option<String>,
354 #[serde(default)]
355 pub kind: BacktraceItemKind,
356}
357
358#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, ts_rs::TS, JsonSchema)]
360#[serde(rename_all = "camelCase")]
361#[ts(export)]
362pub enum BacktraceItemKind {
363 #[default]
365 Call,
366 Import,
368}
369
370impl std::fmt::Display for BacktraceItem {
371 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
372 if let Some(fn_name) = &self.fn_name {
373 write!(f, "{fn_name}: {:?}", self.source_range)
374 } else {
375 write!(f, "(fn): {:?}", self.source_range)
376 }
377 }
378}
379
380fn is_retryable_engine_message(message: &str) -> bool {
381 let message = message.to_ascii_lowercase();
383 RETRYABLE_ENGINE_MESSAGE_MARKER_SETS
384 .iter()
385 .any(|markers| markers.iter().all(|marker| message.contains(marker)))
386}
387
388impl From<KclError> for String {
391 fn from(error: KclError) -> Self {
392 serde_json::to_string(&error).unwrap()
393 }
394}
395
396impl From<CompilationIssue> for KclErrorDetails {
397 fn from(err: CompilationIssue) -> Self {
398 let backtrace = vec![BacktraceItem {
399 source_range: err.source_range,
400 fn_name: None,
401 kind: BacktraceItemKind::Call,
402 }];
403 KclErrorDetails {
404 source_ranges: vec![err.source_range],
405 backtrace,
406 message: err.message,
407 }
408 }
409}
410
411#[cfg(feature = "pyo3")]
412impl From<pyo3::PyErr> for KclError {
413 fn from(error: pyo3::PyErr) -> Self {
414 KclError::new_internal(KclErrorDetails {
415 source_ranges: vec![],
416 backtrace: Default::default(),
417 message: error.to_string(),
418 })
419 }
420}
421
422#[cfg(feature = "pyo3")]
423impl From<KclError> for pyo3::PyErr {
424 fn from(error: KclError) -> Self {
425 pyo3::exceptions::PyException::new_err(error.to_string())
426 }
427}
428
429#[cfg(test)]
430mod tests {
431 use super::*;
432 use crate::ModuleId;
433
434 #[test]
435 fn add_import_location_marks_the_imported_frame() {
436 let inner = SourceRange::new(0, 1, ModuleId::from_usize(1));
437 let import_site = SourceRange::new(5, 9, ModuleId::default());
438 let error = KclError::new_semantic(KclErrorDetails::new("boom".to_owned(), vec![inner]))
439 .add_import_location("part.kcl", import_site);
440
441 let backtrace = error.backtrace();
442 assert_eq!(backtrace.len(), 2);
443 assert_eq!(backtrace[0].fn_name.as_deref(), Some("import part.kcl"));
444 assert_eq!(backtrace[0].kind, BacktraceItemKind::Import);
445 assert_eq!(backtrace[0].source_range, inner);
446 assert_eq!(backtrace[1].fn_name, None);
447 assert_eq!(backtrace[1].kind, BacktraceItemKind::Call);
448 assert_eq!(backtrace[1].source_range, import_site);
449 assert_eq!(error.source_ranges(), vec![inner, import_site]);
450 }
451
452 #[test]
453 fn add_unwind_location_keeps_call_kind() {
454 let inner = SourceRange::new(0, 1, ModuleId::default());
455 let call_site = SourceRange::new(5, 9, ModuleId::default());
456 let error = KclError::new_semantic(KclErrorDetails::new("boom".to_owned(), vec![inner]))
457 .add_unwind_location(Some("f".to_owned()), call_site);
458
459 let backtrace = error.backtrace();
460 assert_eq!(backtrace.len(), 2);
461 assert_eq!(backtrace[0].fn_name.as_deref(), Some("f"));
462 assert_eq!(backtrace[0].kind, BacktraceItemKind::Call);
463 assert_eq!(backtrace[1].kind, BacktraceItemKind::Call);
464 }
465
466 #[test]
467 fn backtrace_item_kind_defaults_to_call_in_serde() {
468 let item: BacktraceItem = serde_json::from_str(r#"{"sourceRange":[0,1,0],"fnName":null}"#).unwrap();
470 assert_eq!(item.kind, BacktraceItemKind::Call);
471 }
472}