1use indexmap::IndexMap;
2pub use kcl_error::{CompilationError, Severity, Suggestion, Tag};
3use serde::{Deserialize, Serialize};
4use thiserror::Error;
5use tower_lsp::lsp_types::{Diagnostic, DiagnosticSeverity};
6
7#[cfg(feature = "artifact-graph")]
8use crate::execution::{ArtifactCommand, ArtifactGraph, Operation};
9use crate::{
10 ModuleId, SourceRange,
11 exec::KclValue,
12 execution::DefaultPlanes,
13 lsp::{IntoDiagnostic, ToLspRange},
14 modules::{ModulePath, ModuleSource},
15};
16
17mod details;
18
19pub use details::KclErrorDetails;
20
21#[derive(thiserror::Error, Debug)]
23pub enum ExecError {
24 #[error("{0}")]
25 Kcl(#[from] Box<crate::KclErrorWithOutputs>),
26 #[error("Could not connect to engine: {0}")]
27 Connection(#[from] ConnectionError),
28 #[error("PNG snapshot could not be decoded: {0}")]
29 BadPng(String),
30 #[error("Bad export: {0}")]
31 BadExport(String),
32}
33
34impl From<KclErrorWithOutputs> for ExecError {
35 fn from(error: KclErrorWithOutputs) -> Self {
36 ExecError::Kcl(Box::new(error))
37 }
38}
39
40#[cfg_attr(target_arch = "wasm32", expect(dead_code))]
42#[derive(Debug)]
43pub struct ExecErrorWithState {
44 pub error: ExecError,
45 pub exec_state: Option<crate::execution::ExecState>,
46}
47
48impl ExecErrorWithState {
49 #[cfg_attr(target_arch = "wasm32", expect(dead_code))]
50 pub fn new(error: ExecError, exec_state: crate::execution::ExecState) -> Self {
51 Self {
52 error,
53 exec_state: Some(exec_state),
54 }
55 }
56}
57
58impl ExecError {
59 pub fn as_kcl_error(&self) -> Option<&crate::KclError> {
60 let ExecError::Kcl(k) = &self else {
61 return None;
62 };
63 Some(&k.error)
64 }
65}
66
67impl From<ExecError> for ExecErrorWithState {
68 fn from(error: ExecError) -> Self {
69 Self {
70 error,
71 exec_state: None,
72 }
73 }
74}
75
76impl From<ConnectionError> for ExecErrorWithState {
77 fn from(error: ConnectionError) -> Self {
78 Self {
79 error: error.into(),
80 exec_state: None,
81 }
82 }
83}
84
85#[derive(thiserror::Error, Debug)]
87pub enum ConnectionError {
88 #[error("Could not create a Zoo client: {0}")]
89 CouldNotMakeClient(anyhow::Error),
90 #[error("Could not establish connection to engine: {0}")]
91 Establishing(anyhow::Error),
92}
93
94#[derive(Error, Debug, Serialize, Deserialize, ts_rs::TS, Clone, PartialEq, Eq)]
95#[ts(export)]
96#[serde(tag = "kind", rename_all = "snake_case")]
97pub enum KclError {
98 #[error("lexical: {details:?}")]
99 Lexical { details: KclErrorDetails },
100 #[error("syntax: {details:?}")]
101 Syntax { details: KclErrorDetails },
102 #[error("semantic: {details:?}")]
103 Semantic { details: KclErrorDetails },
104 #[error("import cycle: {details:?}")]
105 ImportCycle { details: KclErrorDetails },
106 #[error("argument: {details:?}")]
107 Argument { details: KclErrorDetails },
108 #[error("type: {details:?}")]
109 Type { details: KclErrorDetails },
110 #[error("i/o: {details:?}")]
111 Io { details: KclErrorDetails },
112 #[error("unexpected: {details:?}")]
113 Unexpected { details: KclErrorDetails },
114 #[error("value already defined: {details:?}")]
115 ValueAlreadyDefined { details: KclErrorDetails },
116 #[error("undefined value: {details:?}")]
117 UndefinedValue {
118 details: KclErrorDetails,
119 name: Option<String>,
120 },
121 #[error("invalid expression: {details:?}")]
122 InvalidExpression { details: KclErrorDetails },
123 #[error("engine: {details:?}")]
124 Engine { details: KclErrorDetails },
125 #[error("internal error, please report to KittyCAD team: {details:?}")]
126 Internal { details: KclErrorDetails },
127}
128
129impl From<KclErrorWithOutputs> for KclError {
130 fn from(error: KclErrorWithOutputs) -> Self {
131 error.error
132 }
133}
134
135#[derive(Error, Debug, Serialize, ts_rs::TS, Clone, PartialEq)]
136#[error("{error}")]
137#[ts(export)]
138#[serde(rename_all = "camelCase")]
139pub struct KclErrorWithOutputs {
140 pub error: KclError,
141 pub non_fatal: Vec<CompilationError>,
142 pub variables: IndexMap<String, KclValue>,
145 #[cfg(feature = "artifact-graph")]
146 pub operations: Vec<Operation>,
147 #[cfg(feature = "artifact-graph")]
150 pub _artifact_commands: Vec<ArtifactCommand>,
151 #[cfg(feature = "artifact-graph")]
152 pub artifact_graph: ArtifactGraph,
153 pub filenames: IndexMap<ModuleId, ModulePath>,
154 pub source_files: IndexMap<ModuleId, ModuleSource>,
155 pub default_planes: Option<DefaultPlanes>,
156}
157
158impl KclErrorWithOutputs {
159 #[allow(clippy::too_many_arguments)]
160 pub fn new(
161 error: KclError,
162 non_fatal: Vec<CompilationError>,
163 variables: IndexMap<String, KclValue>,
164 #[cfg(feature = "artifact-graph")] operations: Vec<Operation>,
165 #[cfg(feature = "artifact-graph")] artifact_commands: Vec<ArtifactCommand>,
166 #[cfg(feature = "artifact-graph")] artifact_graph: ArtifactGraph,
167 filenames: IndexMap<ModuleId, ModulePath>,
168 source_files: IndexMap<ModuleId, ModuleSource>,
169 default_planes: Option<DefaultPlanes>,
170 ) -> Self {
171 Self {
172 error,
173 non_fatal,
174 variables,
175 #[cfg(feature = "artifact-graph")]
176 operations,
177 #[cfg(feature = "artifact-graph")]
178 _artifact_commands: artifact_commands,
179 #[cfg(feature = "artifact-graph")]
180 artifact_graph,
181 filenames,
182 source_files,
183 default_planes,
184 }
185 }
186 pub fn no_outputs(error: KclError) -> Self {
187 Self {
188 error,
189 non_fatal: Default::default(),
190 variables: Default::default(),
191 #[cfg(feature = "artifact-graph")]
192 operations: Default::default(),
193 #[cfg(feature = "artifact-graph")]
194 _artifact_commands: Default::default(),
195 #[cfg(feature = "artifact-graph")]
196 artifact_graph: Default::default(),
197 filenames: Default::default(),
198 source_files: Default::default(),
199 default_planes: Default::default(),
200 }
201 }
202 pub fn into_miette_report_with_outputs(self, code: &str) -> anyhow::Result<ReportWithOutputs> {
203 let mut source_ranges = self.error.source_ranges();
204
205 let first_source_range = source_ranges
207 .pop()
208 .ok_or_else(|| anyhow::anyhow!("No source ranges found"))?;
209
210 let source = self
211 .source_files
212 .get(&first_source_range.module_id())
213 .cloned()
214 .unwrap_or(ModuleSource {
215 source: code.to_string(),
216 path: self
217 .filenames
218 .get(&first_source_range.module_id())
219 .cloned()
220 .unwrap_or(ModulePath::Main),
221 });
222 let filename = source.path.to_string();
223 let kcl_source = source.source;
224
225 let mut related = Vec::new();
226 for source_range in source_ranges {
227 let module_id = source_range.module_id();
228 let source = self.source_files.get(&module_id).cloned().unwrap_or(ModuleSource {
229 source: code.to_string(),
230 path: self.filenames.get(&module_id).cloned().unwrap_or(ModulePath::Main),
231 });
232 let error = self.error.override_source_ranges(vec![source_range]);
233 let report = Report {
234 error,
235 kcl_source: source.source.to_string(),
236 filename: source.path.to_string(),
237 };
238 related.push(report);
239 }
240
241 Ok(ReportWithOutputs {
242 error: self,
243 kcl_source,
244 filename,
245 related,
246 })
247 }
248}
249
250impl IntoDiagnostic for KclErrorWithOutputs {
251 fn to_lsp_diagnostics(&self, code: &str) -> Vec<Diagnostic> {
252 let message = self.error.get_message();
253 let source_ranges = self.error.source_ranges();
254
255 source_ranges
256 .into_iter()
257 .map(|source_range| {
258 let source = self
259 .source_files
260 .get(&source_range.module_id())
261 .cloned()
262 .unwrap_or(ModuleSource {
263 source: code.to_string(),
264 path: self.filenames.get(&source_range.module_id()).unwrap().clone(),
265 });
266 let mut filename = source.path.to_string();
267 if !filename.starts_with("file://") {
268 filename = format!("file:///{}", filename.trim_start_matches("/"));
269 }
270
271 let related_information = if let Ok(uri) = url::Url::parse(&filename) {
272 Some(vec![tower_lsp::lsp_types::DiagnosticRelatedInformation {
273 location: tower_lsp::lsp_types::Location {
274 uri,
275 range: source_range.to_lsp_range(&source.source),
276 },
277 message: message.to_string(),
278 }])
279 } else {
280 None
281 };
282
283 Diagnostic {
284 range: source_range.to_lsp_range(code),
285 severity: Some(self.severity()),
286 code: None,
287 code_description: None,
289 source: Some("kcl".to_string()),
290 related_information,
291 message: message.clone(),
292 tags: None,
293 data: None,
294 }
295 })
296 .collect()
297 }
298
299 fn severity(&self) -> DiagnosticSeverity {
300 DiagnosticSeverity::ERROR
301 }
302}
303
304#[derive(thiserror::Error, Debug)]
305#[error("{}", self.error.error.get_message())]
306pub struct ReportWithOutputs {
307 pub error: KclErrorWithOutputs,
308 pub kcl_source: String,
309 pub filename: String,
310 pub related: Vec<Report>,
311}
312
313impl miette::Diagnostic for ReportWithOutputs {
314 fn code<'a>(&'a self) -> Option<Box<dyn std::fmt::Display + 'a>> {
315 let family = match self.error.error {
316 KclError::Lexical { .. } => "Lexical",
317 KclError::Syntax { .. } => "Syntax",
318 KclError::Semantic { .. } => "Semantic",
319 KclError::ImportCycle { .. } => "ImportCycle",
320 KclError::Argument { .. } => "Argument",
321 KclError::Type { .. } => "Type",
322 KclError::Io { .. } => "I/O",
323 KclError::Unexpected { .. } => "Unexpected",
324 KclError::ValueAlreadyDefined { .. } => "ValueAlreadyDefined",
325 KclError::UndefinedValue { .. } => "UndefinedValue",
326 KclError::InvalidExpression { .. } => "InvalidExpression",
327 KclError::Engine { .. } => "Engine",
328 KclError::Internal { .. } => "Internal",
329 };
330 let error_string = format!("KCL {family} error");
331 Some(Box::new(error_string))
332 }
333
334 fn source_code(&self) -> Option<&dyn miette::SourceCode> {
335 Some(&self.kcl_source)
336 }
337
338 fn labels(&self) -> Option<Box<dyn Iterator<Item = miette::LabeledSpan> + '_>> {
339 let iter = self
340 .error
341 .error
342 .source_ranges()
343 .into_iter()
344 .map(miette::SourceSpan::from)
345 .map(|span| miette::LabeledSpan::new_with_span(Some(self.filename.to_string()), span));
346 Some(Box::new(iter))
347 }
348
349 fn related<'a>(&'a self) -> Option<Box<dyn Iterator<Item = &'a dyn miette::Diagnostic> + 'a>> {
350 let iter = self.related.iter().map(|r| r as &dyn miette::Diagnostic);
351 Some(Box::new(iter))
352 }
353}
354
355#[derive(thiserror::Error, Debug)]
356#[error("{}", self.error.get_message())]
357pub struct Report {
358 pub error: KclError,
359 pub kcl_source: String,
360 pub filename: String,
361}
362
363impl miette::Diagnostic for Report {
364 fn code<'a>(&'a self) -> Option<Box<dyn std::fmt::Display + 'a>> {
365 let family = match self.error {
366 KclError::Lexical { .. } => "Lexical",
367 KclError::Syntax { .. } => "Syntax",
368 KclError::Semantic { .. } => "Semantic",
369 KclError::ImportCycle { .. } => "ImportCycle",
370 KclError::Argument { .. } => "Argument",
371 KclError::Type { .. } => "Type",
372 KclError::Io { .. } => "I/O",
373 KclError::Unexpected { .. } => "Unexpected",
374 KclError::ValueAlreadyDefined { .. } => "ValueAlreadyDefined",
375 KclError::UndefinedValue { .. } => "UndefinedValue",
376 KclError::InvalidExpression { .. } => "InvalidExpression",
377 KclError::Engine { .. } => "Engine",
378 KclError::Internal { .. } => "Internal",
379 };
380 let error_string = format!("KCL {family} error");
381 Some(Box::new(error_string))
382 }
383
384 fn source_code(&self) -> Option<&dyn miette::SourceCode> {
385 Some(&self.kcl_source)
386 }
387
388 fn labels(&self) -> Option<Box<dyn Iterator<Item = miette::LabeledSpan> + '_>> {
389 let iter = self
390 .error
391 .source_ranges()
392 .into_iter()
393 .map(miette::SourceSpan::from)
394 .map(|span| miette::LabeledSpan::new_with_span(Some(self.filename.to_string()), span));
395 Some(Box::new(iter))
396 }
397}
398
399impl KclErrorDetails {
400 pub fn new(message: String, source_ranges: Vec<SourceRange>) -> KclErrorDetails {
401 let backtrace = source_ranges
402 .iter()
403 .map(|s| BacktraceItem {
404 source_range: *s,
405 fn_name: None,
406 })
407 .collect();
408 KclErrorDetails {
409 source_ranges,
410 backtrace,
411 message,
412 }
413 }
414}
415
416impl KclError {
417 pub fn internal(message: String) -> KclError {
418 KclError::Internal {
419 details: KclErrorDetails {
420 source_ranges: Default::default(),
421 backtrace: Default::default(),
422 message,
423 },
424 }
425 }
426
427 pub fn new_internal(details: KclErrorDetails) -> KclError {
428 KclError::Internal { details }
429 }
430
431 pub fn new_import_cycle(details: KclErrorDetails) -> KclError {
432 KclError::ImportCycle { details }
433 }
434
435 pub fn new_argument(details: KclErrorDetails) -> KclError {
436 KclError::Argument { details }
437 }
438
439 pub fn new_semantic(details: KclErrorDetails) -> KclError {
440 KclError::Semantic { details }
441 }
442
443 pub fn new_value_already_defined(details: KclErrorDetails) -> KclError {
444 KclError::ValueAlreadyDefined { details }
445 }
446
447 pub fn new_syntax(details: KclErrorDetails) -> KclError {
448 KclError::Syntax { details }
449 }
450
451 pub fn new_io(details: KclErrorDetails) -> KclError {
452 KclError::Io { details }
453 }
454
455 pub fn new_invalid_expression(details: KclErrorDetails) -> KclError {
456 KclError::InvalidExpression { details }
457 }
458
459 pub fn new_engine(details: KclErrorDetails) -> KclError {
460 KclError::Engine { details }
461 }
462
463 pub fn new_lexical(details: KclErrorDetails) -> KclError {
464 KclError::Lexical { details }
465 }
466
467 pub fn new_undefined_value(details: KclErrorDetails, name: Option<String>) -> KclError {
468 KclError::UndefinedValue { details, name }
469 }
470
471 pub fn new_type(details: KclErrorDetails) -> KclError {
472 KclError::Type { details }
473 }
474
475 pub fn get_message(&self) -> String {
477 format!("{}: {}", self.error_type(), self.message())
478 }
479
480 pub fn error_type(&self) -> &'static str {
481 match self {
482 KclError::Lexical { .. } => "lexical",
483 KclError::Syntax { .. } => "syntax",
484 KclError::Semantic { .. } => "semantic",
485 KclError::ImportCycle { .. } => "import cycle",
486 KclError::Argument { .. } => "argument",
487 KclError::Type { .. } => "type",
488 KclError::Io { .. } => "i/o",
489 KclError::Unexpected { .. } => "unexpected",
490 KclError::ValueAlreadyDefined { .. } => "value already defined",
491 KclError::UndefinedValue { .. } => "undefined value",
492 KclError::InvalidExpression { .. } => "invalid expression",
493 KclError::Engine { .. } => "engine",
494 KclError::Internal { .. } => "internal",
495 }
496 }
497
498 pub fn source_ranges(&self) -> Vec<SourceRange> {
499 match &self {
500 KclError::Lexical { details: e } => e.source_ranges.clone(),
501 KclError::Syntax { details: e } => e.source_ranges.clone(),
502 KclError::Semantic { details: e } => e.source_ranges.clone(),
503 KclError::ImportCycle { details: e } => e.source_ranges.clone(),
504 KclError::Argument { details: e } => e.source_ranges.clone(),
505 KclError::Type { details: e } => e.source_ranges.clone(),
506 KclError::Io { details: e } => e.source_ranges.clone(),
507 KclError::Unexpected { details: e } => e.source_ranges.clone(),
508 KclError::ValueAlreadyDefined { details: e } => e.source_ranges.clone(),
509 KclError::UndefinedValue { details: e, .. } => e.source_ranges.clone(),
510 KclError::InvalidExpression { details: e } => e.source_ranges.clone(),
511 KclError::Engine { details: e } => e.source_ranges.clone(),
512 KclError::Internal { details: e } => e.source_ranges.clone(),
513 }
514 }
515
516 pub fn message(&self) -> &str {
518 match &self {
519 KclError::Lexical { details: e } => &e.message,
520 KclError::Syntax { details: e } => &e.message,
521 KclError::Semantic { details: e } => &e.message,
522 KclError::ImportCycle { details: e } => &e.message,
523 KclError::Argument { details: e } => &e.message,
524 KclError::Type { details: e } => &e.message,
525 KclError::Io { details: e } => &e.message,
526 KclError::Unexpected { details: e } => &e.message,
527 KclError::ValueAlreadyDefined { details: e } => &e.message,
528 KclError::UndefinedValue { details: e, .. } => &e.message,
529 KclError::InvalidExpression { details: e } => &e.message,
530 KclError::Engine { details: e } => &e.message,
531 KclError::Internal { details: e } => &e.message,
532 }
533 }
534
535 pub fn backtrace(&self) -> Vec<BacktraceItem> {
536 match self {
537 KclError::Lexical { details: e }
538 | KclError::Syntax { details: e }
539 | KclError::Semantic { details: e }
540 | KclError::ImportCycle { details: e }
541 | KclError::Argument { details: e }
542 | KclError::Type { details: e }
543 | KclError::Io { details: e }
544 | KclError::Unexpected { details: e }
545 | KclError::ValueAlreadyDefined { details: e }
546 | KclError::UndefinedValue { details: e, .. }
547 | KclError::InvalidExpression { details: e }
548 | KclError::Engine { details: e }
549 | KclError::Internal { details: e } => e.backtrace.clone(),
550 }
551 }
552
553 pub(crate) fn override_source_ranges(&self, source_ranges: Vec<SourceRange>) -> Self {
554 let mut new = self.clone();
555 match &mut new {
556 KclError::Lexical { details: e }
557 | KclError::Syntax { details: e }
558 | KclError::Semantic { details: e }
559 | KclError::ImportCycle { details: e }
560 | KclError::Argument { details: e }
561 | KclError::Type { details: e }
562 | KclError::Io { details: e }
563 | KclError::Unexpected { details: e }
564 | KclError::ValueAlreadyDefined { details: e }
565 | KclError::UndefinedValue { details: e, .. }
566 | KclError::InvalidExpression { details: e }
567 | KclError::Engine { details: e }
568 | KclError::Internal { details: e } => {
569 e.backtrace = source_ranges
570 .iter()
571 .map(|s| BacktraceItem {
572 source_range: *s,
573 fn_name: None,
574 })
575 .collect();
576 e.source_ranges = source_ranges;
577 }
578 }
579
580 new
581 }
582
583 pub(crate) fn add_unwind_location(&self, last_fn_name: Option<String>, source_range: SourceRange) -> Self {
584 let mut new = self.clone();
585 match &mut new {
586 KclError::Lexical { details: e }
587 | KclError::Syntax { details: e }
588 | KclError::Semantic { details: e }
589 | KclError::ImportCycle { details: e }
590 | KclError::Argument { details: e }
591 | KclError::Type { details: e }
592 | KclError::Io { details: e }
593 | KclError::Unexpected { details: e }
594 | KclError::ValueAlreadyDefined { details: e }
595 | KclError::UndefinedValue { details: e, .. }
596 | KclError::InvalidExpression { details: e }
597 | KclError::Engine { details: e }
598 | KclError::Internal { details: e } => {
599 if let Some(item) = e.backtrace.last_mut() {
600 item.fn_name = last_fn_name;
601 }
602 e.backtrace.push(BacktraceItem {
603 source_range,
604 fn_name: None,
605 });
606 e.source_ranges.push(source_range);
607 }
608 }
609
610 new
611 }
612}
613
614#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, ts_rs::TS, thiserror::Error, miette::Diagnostic)]
615#[serde(rename_all = "camelCase")]
616#[ts(export)]
617pub struct BacktraceItem {
618 pub source_range: SourceRange,
619 pub fn_name: Option<String>,
620}
621
622impl std::fmt::Display for BacktraceItem {
623 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
624 if let Some(fn_name) = &self.fn_name {
625 write!(f, "{fn_name}: {:?}", self.source_range)
626 } else {
627 write!(f, "(fn): {:?}", self.source_range)
628 }
629 }
630}
631
632impl IntoDiagnostic for KclError {
633 fn to_lsp_diagnostics(&self, code: &str) -> Vec<Diagnostic> {
634 let message = self.get_message();
635 let source_ranges = self.source_ranges();
636
637 let module_id = ModuleId::default();
639 let source_ranges = source_ranges
640 .iter()
641 .filter(|r| r.module_id() == module_id)
642 .collect::<Vec<_>>();
643
644 let mut diagnostics = Vec::new();
645 for source_range in &source_ranges {
646 diagnostics.push(Diagnostic {
647 range: source_range.to_lsp_range(code),
648 severity: Some(self.severity()),
649 code: None,
650 code_description: None,
652 source: Some("kcl".to_string()),
653 related_information: None,
654 message: message.clone(),
655 tags: None,
656 data: None,
657 });
658 }
659
660 diagnostics
661 }
662
663 fn severity(&self) -> DiagnosticSeverity {
664 DiagnosticSeverity::ERROR
665 }
666}
667
668impl From<KclError> for String {
671 fn from(error: KclError) -> Self {
672 serde_json::to_string(&error).unwrap()
673 }
674}
675
676impl From<String> for KclError {
677 fn from(error: String) -> Self {
678 serde_json::from_str(&error).unwrap()
679 }
680}
681
682#[cfg(feature = "pyo3")]
683impl From<pyo3::PyErr> for KclError {
684 fn from(error: pyo3::PyErr) -> Self {
685 KclError::new_internal(KclErrorDetails {
686 source_ranges: vec![],
687 backtrace: Default::default(),
688 message: error.to_string(),
689 })
690 }
691}
692
693#[cfg(feature = "pyo3")]
694impl From<KclError> for pyo3::PyErr {
695 fn from(error: KclError) -> Self {
696 pyo3::exceptions::PyException::new_err(error.to_string())
697 }
698}
699
700impl From<CompilationError> for KclErrorDetails {
701 fn from(err: CompilationError) -> Self {
702 let backtrace = vec![BacktraceItem {
703 source_range: err.source_range,
704 fn_name: None,
705 }];
706 KclErrorDetails {
707 source_ranges: vec![err.source_range],
708 backtrace,
709 message: err.message,
710 }
711 }
712}