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;
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 .into_iter()
340 .map(miette::SourceSpan::from)
341 .map(|span| miette::LabeledSpan::new_with_span(Some(self.filename.to_string()), span));
342 Some(Box::new(iter))
343 }
344
345 fn related<'a>(&'a self) -> Option<Box<dyn Iterator<Item = &'a dyn miette::Diagnostic> + 'a>> {
346 let iter = self.related.iter().map(|r| r as &dyn miette::Diagnostic);
347 Some(Box::new(iter))
348 }
349}
350
351#[derive(thiserror::Error, Debug)]
352#[error("{}", self.error.get_message())]
353pub struct Report {
354 pub error: KclError,
355 pub kcl_source: String,
356 pub filename: String,
357}
358
359impl miette::Diagnostic for Report {
360 fn code<'a>(&'a self) -> Option<Box<dyn std::fmt::Display + 'a>> {
361 let family = match self.error {
362 KclError::Lexical { .. } => "Lexical",
363 KclError::Syntax { .. } => "Syntax",
364 KclError::Semantic { .. } => "Semantic",
365 KclError::ImportCycle { .. } => "ImportCycle",
366 KclError::Argument { .. } => "Argument",
367 KclError::Type { .. } => "Type",
368 KclError::Io { .. } => "I/O",
369 KclError::Unexpected { .. } => "Unexpected",
370 KclError::ValueAlreadyDefined { .. } => "ValueAlreadyDefined",
371 KclError::UndefinedValue { .. } => "UndefinedValue",
372 KclError::InvalidExpression { .. } => "InvalidExpression",
373 KclError::Engine { .. } => "Engine",
374 KclError::Internal { .. } => "Internal",
375 };
376 let error_string = format!("KCL {family} error");
377 Some(Box::new(error_string))
378 }
379
380 fn source_code(&self) -> Option<&dyn miette::SourceCode> {
381 Some(&self.kcl_source)
382 }
383
384 fn labels(&self) -> Option<Box<dyn Iterator<Item = miette::LabeledSpan> + '_>> {
385 let iter = self
386 .error
387 .source_ranges()
388 .into_iter()
389 .map(miette::SourceSpan::from)
390 .map(|span| miette::LabeledSpan::new_with_span(Some(self.filename.to_string()), span));
391 Some(Box::new(iter))
392 }
393}
394
395#[derive(Debug, Serialize, Deserialize, ts_rs::TS, Clone, PartialEq, Eq, thiserror::Error, miette::Diagnostic)]
396#[serde(rename_all = "camelCase")]
397#[error("{message}")]
398#[ts(export)]
399pub struct KclErrorDetails {
400 #[label(collection, "Errors")]
401 pub source_ranges: Vec<SourceRange>,
402 pub backtrace: Vec<BacktraceItem>,
403 #[serde(rename = "msg")]
404 pub message: String,
405}
406
407impl KclErrorDetails {
408 pub fn new(message: String, source_ranges: Vec<SourceRange>) -> KclErrorDetails {
409 let backtrace = source_ranges
410 .iter()
411 .map(|s| BacktraceItem {
412 source_range: *s,
413 fn_name: None,
414 })
415 .collect();
416 KclErrorDetails {
417 source_ranges,
418 backtrace,
419 message,
420 }
421 }
422}
423
424impl KclError {
425 pub fn internal(message: String) -> KclError {
426 KclError::Internal {
427 details: KclErrorDetails {
428 source_ranges: Default::default(),
429 backtrace: Default::default(),
430 message,
431 },
432 }
433 }
434
435 pub fn new_internal(details: KclErrorDetails) -> KclError {
436 KclError::Internal { details }
437 }
438
439 pub fn new_import_cycle(details: KclErrorDetails) -> KclError {
440 KclError::ImportCycle { details }
441 }
442
443 pub fn new_argument(details: KclErrorDetails) -> KclError {
444 KclError::Argument { details }
445 }
446
447 pub fn new_semantic(details: KclErrorDetails) -> KclError {
448 KclError::Semantic { details }
449 }
450
451 pub fn new_value_already_defined(details: KclErrorDetails) -> KclError {
452 KclError::ValueAlreadyDefined { details }
453 }
454
455 pub fn new_syntax(details: KclErrorDetails) -> KclError {
456 KclError::Syntax { details }
457 }
458
459 pub fn new_io(details: KclErrorDetails) -> KclError {
460 KclError::Io { details }
461 }
462
463 pub fn new_engine(details: KclErrorDetails) -> KclError {
464 KclError::Engine { details }
465 }
466
467 pub fn new_lexical(details: KclErrorDetails) -> KclError {
468 KclError::Lexical { details }
469 }
470
471 pub fn new_undefined_value(details: KclErrorDetails, name: Option<String>) -> KclError {
472 KclError::UndefinedValue { details, name }
473 }
474
475 pub fn new_type(details: KclErrorDetails) -> KclError {
476 KclError::Type { details }
477 }
478
479 pub fn get_message(&self) -> String {
481 format!("{}: {}", self.error_type(), self.message())
482 }
483
484 pub fn error_type(&self) -> &'static str {
485 match self {
486 KclError::Lexical { .. } => "lexical",
487 KclError::Syntax { .. } => "syntax",
488 KclError::Semantic { .. } => "semantic",
489 KclError::ImportCycle { .. } => "import cycle",
490 KclError::Argument { .. } => "argument",
491 KclError::Type { .. } => "type",
492 KclError::Io { .. } => "i/o",
493 KclError::Unexpected { .. } => "unexpected",
494 KclError::ValueAlreadyDefined { .. } => "value already defined",
495 KclError::UndefinedValue { .. } => "undefined value",
496 KclError::InvalidExpression { .. } => "invalid expression",
497 KclError::Engine { .. } => "engine",
498 KclError::Internal { .. } => "internal",
499 }
500 }
501
502 pub fn source_ranges(&self) -> Vec<SourceRange> {
503 match &self {
504 KclError::Lexical { details: e } => e.source_ranges.clone(),
505 KclError::Syntax { details: e } => e.source_ranges.clone(),
506 KclError::Semantic { details: e } => e.source_ranges.clone(),
507 KclError::ImportCycle { details: e } => e.source_ranges.clone(),
508 KclError::Argument { details: e } => e.source_ranges.clone(),
509 KclError::Type { details: e } => e.source_ranges.clone(),
510 KclError::Io { details: e } => e.source_ranges.clone(),
511 KclError::Unexpected { details: e } => e.source_ranges.clone(),
512 KclError::ValueAlreadyDefined { details: e } => e.source_ranges.clone(),
513 KclError::UndefinedValue { details: e, .. } => e.source_ranges.clone(),
514 KclError::InvalidExpression { details: e } => e.source_ranges.clone(),
515 KclError::Engine { details: e } => e.source_ranges.clone(),
516 KclError::Internal { details: e } => e.source_ranges.clone(),
517 }
518 }
519
520 pub fn message(&self) -> &str {
522 match &self {
523 KclError::Lexical { details: e } => &e.message,
524 KclError::Syntax { details: e } => &e.message,
525 KclError::Semantic { details: e } => &e.message,
526 KclError::ImportCycle { details: e } => &e.message,
527 KclError::Argument { details: e } => &e.message,
528 KclError::Type { details: e } => &e.message,
529 KclError::Io { details: e } => &e.message,
530 KclError::Unexpected { details: e } => &e.message,
531 KclError::ValueAlreadyDefined { details: e } => &e.message,
532 KclError::UndefinedValue { details: e, .. } => &e.message,
533 KclError::InvalidExpression { details: e } => &e.message,
534 KclError::Engine { details: e } => &e.message,
535 KclError::Internal { details: e } => &e.message,
536 }
537 }
538
539 pub fn backtrace(&self) -> Vec<BacktraceItem> {
540 match self {
541 KclError::Lexical { details: e }
542 | KclError::Syntax { details: e }
543 | KclError::Semantic { details: e }
544 | KclError::ImportCycle { details: e }
545 | KclError::Argument { details: e }
546 | KclError::Type { details: e }
547 | KclError::Io { details: e }
548 | KclError::Unexpected { details: e }
549 | KclError::ValueAlreadyDefined { details: e }
550 | KclError::UndefinedValue { details: e, .. }
551 | KclError::InvalidExpression { details: e }
552 | KclError::Engine { details: e }
553 | KclError::Internal { details: e } => e.backtrace.clone(),
554 }
555 }
556
557 pub(crate) fn override_source_ranges(&self, source_ranges: Vec<SourceRange>) -> Self {
558 let mut new = self.clone();
559 match &mut new {
560 KclError::Lexical { details: e }
561 | KclError::Syntax { details: e }
562 | KclError::Semantic { details: e }
563 | KclError::ImportCycle { details: e }
564 | KclError::Argument { details: e }
565 | KclError::Type { details: e }
566 | KclError::Io { details: e }
567 | KclError::Unexpected { details: e }
568 | KclError::ValueAlreadyDefined { details: e }
569 | KclError::UndefinedValue { details: e, .. }
570 | KclError::InvalidExpression { details: e }
571 | KclError::Engine { details: e }
572 | KclError::Internal { details: e } => {
573 e.backtrace = source_ranges
574 .iter()
575 .map(|s| BacktraceItem {
576 source_range: *s,
577 fn_name: None,
578 })
579 .collect();
580 e.source_ranges = source_ranges;
581 }
582 }
583
584 new
585 }
586
587 pub(crate) fn add_unwind_location(&self, last_fn_name: Option<String>, source_range: SourceRange) -> Self {
588 let mut new = self.clone();
589 match &mut new {
590 KclError::Lexical { details: e }
591 | KclError::Syntax { details: e }
592 | KclError::Semantic { details: e }
593 | KclError::ImportCycle { details: e }
594 | KclError::Argument { details: e }
595 | KclError::Type { details: e }
596 | KclError::Io { details: e }
597 | KclError::Unexpected { details: e }
598 | KclError::ValueAlreadyDefined { details: e }
599 | KclError::UndefinedValue { details: e, .. }
600 | KclError::InvalidExpression { details: e }
601 | KclError::Engine { details: e }
602 | KclError::Internal { details: e } => {
603 if let Some(item) = e.backtrace.last_mut() {
604 item.fn_name = last_fn_name;
605 }
606 e.backtrace.push(BacktraceItem {
607 source_range,
608 fn_name: None,
609 });
610 e.source_ranges.push(source_range);
611 }
612 }
613
614 new
615 }
616}
617
618#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, ts_rs::TS, thiserror::Error, miette::Diagnostic)]
619#[serde(rename_all = "camelCase")]
620#[ts(export)]
621pub struct BacktraceItem {
622 pub source_range: SourceRange,
623 pub fn_name: Option<String>,
624}
625
626impl std::fmt::Display for BacktraceItem {
627 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
628 if let Some(fn_name) = &self.fn_name {
629 write!(f, "{fn_name}: {:?}", self.source_range)
630 } else {
631 write!(f, "(fn): {:?}", self.source_range)
632 }
633 }
634}
635
636impl IntoDiagnostic for KclError {
637 fn to_lsp_diagnostics(&self, code: &str) -> Vec<Diagnostic> {
638 let message = self.get_message();
639 let source_ranges = self.source_ranges();
640
641 let module_id = ModuleId::default();
643 let source_ranges = source_ranges
644 .iter()
645 .filter(|r| r.module_id() == module_id)
646 .collect::<Vec<_>>();
647
648 let mut diagnostics = Vec::new();
649 for source_range in &source_ranges {
650 diagnostics.push(Diagnostic {
651 range: source_range.to_lsp_range(code),
652 severity: Some(self.severity()),
653 code: None,
654 code_description: None,
656 source: Some("kcl".to_string()),
657 related_information: None,
658 message: message.clone(),
659 tags: None,
660 data: None,
661 });
662 }
663
664 diagnostics
665 }
666
667 fn severity(&self) -> DiagnosticSeverity {
668 DiagnosticSeverity::ERROR
669 }
670}
671
672impl From<KclError> for String {
675 fn from(error: KclError) -> Self {
676 serde_json::to_string(&error).unwrap()
677 }
678}
679
680impl From<String> for KclError {
681 fn from(error: String) -> Self {
682 serde_json::from_str(&error).unwrap()
683 }
684}
685
686#[cfg(feature = "pyo3")]
687impl From<pyo3::PyErr> for KclError {
688 fn from(error: pyo3::PyErr) -> Self {
689 KclError::new_internal(KclErrorDetails {
690 source_ranges: vec![],
691 backtrace: Default::default(),
692 message: error.to_string(),
693 })
694 }
695}
696
697#[cfg(feature = "pyo3")]
698impl From<KclError> for pyo3::PyErr {
699 fn from(error: KclError) -> Self {
700 pyo3::exceptions::PyException::new_err(error.to_string())
701 }
702}
703
704impl From<CompilationError> for KclErrorDetails {
705 fn from(err: CompilationError) -> Self {
706 let backtrace = vec![BacktraceItem {
707 source_range: err.source_range,
708 fn_name: None,
709 }];
710 KclErrorDetails {
711 source_ranges: vec![err.source_range],
712 backtrace,
713 message: err.message,
714 }
715 }
716}