Skip to main content

kcl_error/
error.rs

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    /// Returns true if the error is transient and the operation that caused it
16    /// should be retried.
17    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            })
95            .collect();
96        KclErrorDetails {
97            source_ranges,
98            backtrace,
99            message,
100        }
101    }
102}
103
104impl KclError {
105    pub fn internal(message: String) -> KclError {
106        KclError::Internal {
107            details: KclErrorDetails {
108                source_ranges: Default::default(),
109                backtrace: Default::default(),
110                message,
111            },
112        }
113    }
114
115    pub fn new_internal(details: KclErrorDetails) -> KclError {
116        KclError::Internal { details }
117    }
118
119    pub fn new_import_cycle(details: KclErrorDetails) -> KclError {
120        KclError::ImportCycle { details }
121    }
122
123    pub fn new_argument(details: KclErrorDetails) -> KclError {
124        KclError::Argument { details }
125    }
126
127    pub fn new_semantic(details: KclErrorDetails) -> KclError {
128        KclError::Semantic { details }
129    }
130
131    pub fn new_value_already_defined(details: KclErrorDetails) -> KclError {
132        KclError::ValueAlreadyDefined { details }
133    }
134
135    pub fn new_syntax(details: KclErrorDetails) -> KclError {
136        KclError::Syntax { details }
137    }
138
139    pub fn new_io(details: KclErrorDetails) -> KclError {
140        KclError::Io { details }
141    }
142
143    pub fn new_invalid_expression(details: KclErrorDetails) -> KclError {
144        KclError::InvalidExpression { details }
145    }
146
147    pub fn refactor(message: String) -> KclError {
148        KclError::Refactor {
149            details: KclErrorDetails {
150                source_ranges: Default::default(),
151                backtrace: Default::default(),
152                message,
153            },
154        }
155    }
156
157    pub fn new_engine(details: KclErrorDetails) -> KclError {
158        if details.message.eq_ignore_ascii_case("internal error") {
159            KclError::EngineInternal { details }
160        } else if is_retryable_engine_message(&details.message) {
161            KclError::EngineHangup {
162                details,
163                api_call_id: None,
164            }
165        } else {
166            KclError::Engine { details }
167        }
168    }
169
170    pub fn new_engine_hangup(details: KclErrorDetails, api_call_id: Option<String>) -> KclError {
171        KclError::EngineHangup { details, api_call_id }
172    }
173
174    pub fn new_lexical(details: KclErrorDetails) -> KclError {
175        KclError::Lexical { details }
176    }
177
178    pub fn new_undefined_value(details: KclErrorDetails, name: Option<String>) -> KclError {
179        KclError::UndefinedValue { details, name }
180    }
181
182    pub fn new_type(details: KclErrorDetails) -> KclError {
183        KclError::Type { details }
184    }
185
186    pub fn new_user_defined(details: KclErrorDetails) -> KclError {
187        KclError::UserDefined { details }
188    }
189
190    pub fn is_undefined_value(&self) -> bool {
191        matches!(self, KclError::UndefinedValue { .. })
192    }
193
194    /// Get the error message.
195    pub fn get_message(&self) -> String {
196        format!("{}: {}", self.error_type(), self.message())
197    }
198
199    pub fn error_type(&self) -> &'static str {
200        match self {
201            KclError::Lexical { .. } => "lexical",
202            KclError::Syntax { .. } => "syntax",
203            KclError::Semantic { .. } => "semantic",
204            KclError::ImportCycle { .. } => "import cycle",
205            KclError::Argument { .. } => "argument",
206            KclError::Type { .. } => "type",
207            KclError::UserDefined { .. } => "user-defined",
208            KclError::Io { .. } => "i/o",
209            KclError::Unexpected { .. } => "unexpected",
210            KclError::ValueAlreadyDefined { .. } => "value already defined",
211            KclError::UndefinedValue { .. } => "undefined value",
212            KclError::InvalidExpression { .. } => "invalid expression",
213            KclError::MaxCallStack { .. } => "max call stack",
214            KclError::Refactor { .. } => "refactor",
215            KclError::Engine { .. } => "engine",
216            KclError::EngineHangup { .. } => "engine hangup",
217            KclError::EngineInternal { .. } => "engine internal",
218            KclError::Internal { .. } => "internal",
219        }
220    }
221
222    pub fn source_ranges(&self) -> Vec<SourceRange> {
223        match &self {
224            KclError::Lexical { details: e } => e.source_ranges.clone(),
225            KclError::Syntax { details: e } => e.source_ranges.clone(),
226            KclError::Semantic { details: e } => e.source_ranges.clone(),
227            KclError::ImportCycle { details: e } => e.source_ranges.clone(),
228            KclError::Argument { details: e } => e.source_ranges.clone(),
229            KclError::Type { details: e } => e.source_ranges.clone(),
230            KclError::UserDefined { details: e } => e.source_ranges.clone(),
231            KclError::Io { details: e } => e.source_ranges.clone(),
232            KclError::Unexpected { details: e } => e.source_ranges.clone(),
233            KclError::ValueAlreadyDefined { details: e } => e.source_ranges.clone(),
234            KclError::UndefinedValue { details: e, .. } => e.source_ranges.clone(),
235            KclError::InvalidExpression { details: e } => e.source_ranges.clone(),
236            KclError::MaxCallStack { details: e } => e.source_ranges.clone(),
237            KclError::Refactor { details: e } => e.source_ranges.clone(),
238            KclError::Engine { details: e } => e.source_ranges.clone(),
239            KclError::EngineHangup { details: e, .. } => e.source_ranges.clone(),
240            KclError::EngineInternal { details: e } => e.source_ranges.clone(),
241            KclError::Internal { details: e } => e.source_ranges.clone(),
242        }
243    }
244
245    /// Get the inner error message.
246    pub fn message(&self) -> &str {
247        match &self {
248            KclError::Lexical { details: e } => &e.message,
249            KclError::Syntax { details: e } => &e.message,
250            KclError::Semantic { details: e } => &e.message,
251            KclError::ImportCycle { details: e } => &e.message,
252            KclError::Argument { details: e } => &e.message,
253            KclError::Type { details: e } => &e.message,
254            KclError::UserDefined { details: e } => &e.message,
255            KclError::Io { details: e } => &e.message,
256            KclError::Unexpected { details: e } => &e.message,
257            KclError::ValueAlreadyDefined { details: e } => &e.message,
258            KclError::UndefinedValue { details: e, .. } => &e.message,
259            KclError::InvalidExpression { details: e } => &e.message,
260            KclError::MaxCallStack { details: e } => &e.message,
261            KclError::Refactor { details: e } => &e.message,
262            KclError::Engine { details: e } => &e.message,
263            KclError::EngineHangup { details: e, .. } => &e.message,
264            KclError::EngineInternal { details: e } => &e.message,
265            KclError::Internal { details: e } => &e.message,
266        }
267    }
268
269    pub fn backtrace(&self) -> Vec<BacktraceItem> {
270        match self {
271            KclError::Lexical { details: e }
272            | KclError::Syntax { details: e }
273            | KclError::Semantic { details: e }
274            | KclError::ImportCycle { details: e }
275            | KclError::Argument { details: e }
276            | KclError::Type { details: e }
277            | KclError::UserDefined { details: e }
278            | KclError::Io { details: e }
279            | KclError::Unexpected { details: e }
280            | KclError::ValueAlreadyDefined { details: e }
281            | KclError::UndefinedValue { details: e, .. }
282            | KclError::InvalidExpression { details: e }
283            | KclError::MaxCallStack { details: e }
284            | KclError::Refactor { details: e }
285            | KclError::Engine { details: e }
286            | KclError::EngineHangup { details: e, .. }
287            | KclError::EngineInternal { details: e }
288            | KclError::Internal { details: e } => e.backtrace.clone(),
289        }
290    }
291
292    pub fn override_source_ranges(&self, source_ranges: Vec<SourceRange>) -> Self {
293        let mut new = self.clone();
294        match &mut new {
295            KclError::Lexical { details: e }
296            | KclError::Syntax { details: e }
297            | KclError::Semantic { details: e }
298            | KclError::ImportCycle { details: e }
299            | KclError::Argument { details: e }
300            | KclError::Type { details: e }
301            | KclError::UserDefined { details: e }
302            | KclError::Io { details: e }
303            | KclError::Unexpected { details: e }
304            | KclError::ValueAlreadyDefined { details: e }
305            | KclError::UndefinedValue { details: e, .. }
306            | KclError::InvalidExpression { details: e }
307            | KclError::MaxCallStack { details: e }
308            | KclError::Refactor { details: e }
309            | KclError::Engine { details: e }
310            | KclError::EngineHangup { details: e, .. }
311            | KclError::EngineInternal { details: e }
312            | KclError::Internal { details: e } => {
313                e.backtrace = source_ranges
314                    .iter()
315                    .map(|s| BacktraceItem {
316                        source_range: *s,
317                        fn_name: None,
318                    })
319                    .collect();
320                e.source_ranges = source_ranges;
321            }
322        }
323
324        new
325    }
326
327    pub fn add_unwind_location(&self, last_fn_name: Option<String>, source_range: SourceRange) -> Self {
328        let mut new = self.clone();
329        match &mut new {
330            KclError::Lexical { details: e }
331            | KclError::Syntax { details: e }
332            | KclError::Semantic { details: e }
333            | KclError::ImportCycle { details: e }
334            | KclError::Argument { details: e }
335            | KclError::Type { details: e }
336            | KclError::UserDefined { details: e }
337            | KclError::Io { details: e }
338            | KclError::Unexpected { details: e }
339            | KclError::ValueAlreadyDefined { details: e }
340            | KclError::UndefinedValue { details: e, .. }
341            | KclError::InvalidExpression { details: e }
342            | KclError::MaxCallStack { details: e }
343            | KclError::Refactor { details: e }
344            | KclError::Engine { details: e }
345            | KclError::EngineHangup { details: e, .. }
346            | KclError::EngineInternal { details: e }
347            | KclError::Internal { details: e } => {
348                if let Some(item) = e.backtrace.last_mut() {
349                    item.fn_name = last_fn_name;
350                }
351                e.backtrace.push(BacktraceItem {
352                    source_range,
353                    fn_name: None,
354                });
355                e.source_ranges.push(source_range);
356            }
357        }
358
359        new
360    }
361}
362
363#[derive(
364    Debug, Clone, PartialEq, Eq, Serialize, Deserialize, ts_rs::TS, thiserror::Error, miette::Diagnostic, JsonSchema,
365)]
366#[serde(rename_all = "camelCase")]
367#[ts(export)]
368pub struct BacktraceItem {
369    pub source_range: SourceRange,
370    pub fn_name: Option<String>,
371}
372
373impl std::fmt::Display for BacktraceItem {
374    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
375        if let Some(fn_name) = &self.fn_name {
376            write!(f, "{fn_name}: {:?}", self.source_range)
377        } else {
378            write!(f, "(fn): {:?}", self.source_range)
379        }
380    }
381}
382
383fn is_retryable_engine_message(message: &str) -> bool {
384    // TODO: Replace string matching with structured engine/API retry metadata once it is available.
385    let message = message.to_ascii_lowercase();
386    RETRYABLE_ENGINE_MESSAGE_MARKER_SETS
387        .iter()
388        .any(|markers| markers.iter().all(|marker| message.contains(marker)))
389}
390
391/// This is different than to_string() in that it will serialize the Error
392/// the struct as JSON so we can deserialize it on the js side.
393impl From<KclError> for String {
394    fn from(error: KclError) -> Self {
395        serde_json::to_string(&error).unwrap()
396    }
397}
398
399impl From<CompilationIssue> for KclErrorDetails {
400    fn from(err: CompilationIssue) -> Self {
401        let backtrace = vec![BacktraceItem {
402            source_range: err.source_range,
403            fn_name: None,
404        }];
405        KclErrorDetails {
406            source_ranges: vec![err.source_range],
407            backtrace,
408            message: err.message,
409        }
410    }
411}
412
413#[cfg(feature = "pyo3")]
414impl From<pyo3::PyErr> for KclError {
415    fn from(error: pyo3::PyErr) -> Self {
416        KclError::new_internal(KclErrorDetails {
417            source_ranges: vec![],
418            backtrace: Default::default(),
419            message: error.to_string(),
420        })
421    }
422}
423
424#[cfg(feature = "pyo3")]
425impl From<KclError> for pyo3::PyErr {
426    fn from(error: KclError) -> Self {
427        pyo3::exceptions::PyException::new_err(error.to_string())
428    }
429}