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