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