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("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 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 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 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
379impl 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}