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