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