Skip to main content

kcl_lib/
errors.rs

1use std::collections::BTreeMap;
2
3use indexmap::IndexMap;
4pub use kcl_error::CompilationIssue;
5pub use kcl_error::Severity;
6pub use kcl_error::Suggestion;
7pub use kcl_error::Tag;
8use serde::Deserialize;
9use serde::Serialize;
10use thiserror::Error;
11use tower_lsp::lsp_types::Diagnostic;
12use tower_lsp::lsp_types::DiagnosticSeverity;
13use uuid::Uuid;
14
15use crate::ExecOutcome;
16use crate::ModuleId;
17use crate::NodePath;
18use crate::SourceRange;
19use crate::exec::KclValue;
20use crate::execution::ArtifactCommand;
21use crate::execution::ArtifactGraph;
22use crate::execution::DefaultPlanes;
23use crate::execution::OperationsByModule;
24use crate::front::Number;
25use crate::front::Object;
26use crate::front::ObjectId;
27use crate::lsp::IntoDiagnostic;
28use crate::lsp::ToLspRange;
29use crate::modules::ModulePath;
30use crate::modules::ModuleSource;
31
32pub trait IsRetryable {
33    /// Returns true if the error is transient and the operation that caused it
34    /// should be retried.
35    fn is_retryable(&self) -> bool;
36}
37
38/// How did the KCL execution fail
39#[derive(thiserror::Error, Debug)]
40pub enum ExecError {
41    #[error("{0}")]
42    Kcl(#[from] Box<crate::KclErrorWithOutputs>),
43    #[error("Could not connect to engine: {0}")]
44    Connection(#[from] ConnectionError),
45    #[error("PNG snapshot could not be decoded: {0}")]
46    BadPng(String),
47    #[error("Bad export: {0}")]
48    BadExport(String),
49}
50
51impl From<KclErrorWithOutputs> for ExecError {
52    fn from(error: KclErrorWithOutputs) -> Self {
53        ExecError::Kcl(Box::new(error))
54    }
55}
56
57/// How did the KCL execution fail, with extra state.
58#[derive(Debug, thiserror::Error)]
59#[error("{error}")]
60pub struct ExecErrorWithState {
61    pub error: ExecError,
62    pub exec_state: Option<crate::execution::ExecState>,
63    #[cfg(feature = "snapshot-engine-responses")]
64    pub responses: Option<IndexMap<Uuid, kittycad_modeling_cmds::websocket::WebSocketResponse>>,
65}
66
67impl ExecErrorWithState {
68    #[cfg_attr(target_arch = "wasm32", expect(dead_code))]
69    pub fn new(
70        error: ExecError,
71        exec_state: crate::execution::ExecState,
72        #[cfg_attr(not(feature = "snapshot-engine-responses"), expect(unused_variables))] responses: Option<
73            IndexMap<Uuid, kittycad_modeling_cmds::websocket::WebSocketResponse>,
74        >,
75    ) -> Self {
76        Self {
77            error,
78            exec_state: Some(exec_state),
79            #[cfg(feature = "snapshot-engine-responses")]
80            responses,
81        }
82    }
83}
84
85impl IsRetryable for ExecErrorWithState {
86    fn is_retryable(&self) -> bool {
87        self.error.is_retryable()
88    }
89}
90
91impl ExecError {
92    pub fn as_kcl_error(&self) -> Option<&crate::KclError> {
93        let ExecError::Kcl(k) = &self else {
94            return None;
95        };
96        Some(&k.error)
97    }
98}
99
100impl IsRetryable for ExecError {
101    fn is_retryable(&self) -> bool {
102        matches!(self, ExecError::Kcl(kcl_error) if kcl_error.is_retryable())
103    }
104}
105
106impl From<ExecError> for ExecErrorWithState {
107    fn from(error: ExecError) -> Self {
108        Self {
109            error,
110            exec_state: None,
111            #[cfg(feature = "snapshot-engine-responses")]
112            responses: None,
113        }
114    }
115}
116
117impl From<ConnectionError> for ExecErrorWithState {
118    fn from(error: ConnectionError) -> Self {
119        Self {
120            error: error.into(),
121            exec_state: None,
122            #[cfg(feature = "snapshot-engine-responses")]
123            responses: None,
124        }
125    }
126}
127
128/// How did KCL client fail to connect to the engine
129#[derive(thiserror::Error, Debug)]
130pub enum ConnectionError {
131    #[error("Could not create a Zoo client: {0}")]
132    CouldNotMakeClient(anyhow::Error),
133    #[error("Could not establish connection to engine: {0}")]
134    Establishing(anyhow::Error),
135}
136
137#[derive(Error, Debug, Serialize, Deserialize, ts_rs::TS, Clone, PartialEq, Eq)]
138#[ts(export)]
139#[serde(tag = "kind", rename_all = "snake_case")]
140pub enum KclError {
141    #[error("lexical: {details:?}")]
142    Lexical { details: KclErrorDetails },
143    #[error("syntax: {details:?}")]
144    Syntax { details: KclErrorDetails },
145    #[error("semantic: {details:?}")]
146    Semantic { details: KclErrorDetails },
147    #[error("import cycle: {details:?}")]
148    ImportCycle { details: KclErrorDetails },
149    #[error("argument: {details:?}")]
150    Argument { details: KclErrorDetails },
151    #[error("type: {details:?}")]
152    Type { details: KclErrorDetails },
153    #[error("i/o: {details:?}")]
154    Io { details: KclErrorDetails },
155    #[error("unexpected: {details:?}")]
156    Unexpected { details: KclErrorDetails },
157    #[error("value already defined: {details:?}")]
158    ValueAlreadyDefined { details: KclErrorDetails },
159    #[error("undefined value: {details:?}")]
160    UndefinedValue {
161        details: KclErrorDetails,
162        name: Option<String>,
163    },
164    #[error("invalid expression: {details:?}")]
165    InvalidExpression { details: KclErrorDetails },
166    #[error("max call stack size exceeded: {details:?}")]
167    MaxCallStack { details: KclErrorDetails },
168    #[error("refactor: {details:?}")]
169    Refactor { details: KclErrorDetails },
170    #[error("engine: {details:?}")]
171    Engine { details: KclErrorDetails },
172    #[error("engine hangup: {details:?}")]
173    EngineHangup {
174        details: KclErrorDetails,
175        api_call_id: Option<String>,
176    },
177    #[error("engine internal: {details:?}")]
178    EngineInternal { details: KclErrorDetails },
179    #[error("internal error, please report to KittyCAD team: {details:?}")]
180    Internal { details: KclErrorDetails },
181}
182
183impl From<KclErrorWithOutputs> for KclError {
184    fn from(error: KclErrorWithOutputs) -> Self {
185        error.error
186    }
187}
188
189impl IsRetryable for KclError {
190    fn is_retryable(&self) -> bool {
191        matches!(self, KclError::EngineHangup { .. } | KclError::EngineInternal { .. })
192    }
193}
194
195const RETRYABLE_ENGINE_MESSAGE_MARKER_SETS: &[&[&str]] = &[
196    &["modeling connection", "interrupted", "please reconnect"],
197    &["modeling connection", "heartbeats", "please reconnect"],
198];
199
200fn is_retryable_engine_message(message: &str) -> bool {
201    // TODO: Replace string matching with structured engine/API retry metadata once it is available.
202    let message = message.to_ascii_lowercase();
203    RETRYABLE_ENGINE_MESSAGE_MARKER_SETS
204        .iter()
205        .any(|markers| markers.iter().all(|marker| message.contains(marker)))
206}
207
208#[derive(Error, Debug, Serialize, ts_rs::TS, Clone, PartialEq)]
209#[error("{error}")]
210#[ts(export)]
211#[serde(rename_all = "camelCase")]
212pub struct KclErrorWithOutputs {
213    pub error: KclError,
214    pub non_fatal: Vec<CompilationIssue>,
215    /// Variables in the top-level of the root module. Note that functions will
216    /// have an invalid env ref.
217    pub variables: IndexMap<String, KclValue>,
218    pub operations: OperationsByModule,
219    // TODO: Remove this field.  Doing so breaks the ts-rs output for some
220    // reason.
221    pub _artifact_commands: Vec<ArtifactCommand>,
222    pub artifact_graph: ArtifactGraph,
223    #[serde(skip)]
224    pub scene_objects: Vec<Object>,
225    #[serde(skip)]
226    pub source_range_to_object: BTreeMap<SourceRange, ObjectId>,
227    #[serde(skip)]
228    pub var_solutions: Vec<(SourceRange, Option<NodePath>, Number)>,
229    pub scene_graph: Option<crate::front::SceneGraph>,
230    pub filenames: IndexMap<ModuleId, ModulePath>,
231    pub source_files: IndexMap<ModuleId, ModuleSource>,
232    pub default_planes: Option<DefaultPlanes>,
233}
234
235impl KclErrorWithOutputs {
236    #[allow(clippy::too_many_arguments)]
237    pub fn new(
238        error: KclError,
239        non_fatal: Vec<CompilationIssue>,
240        variables: IndexMap<String, KclValue>,
241        operations: OperationsByModule,
242        artifact_commands: Vec<ArtifactCommand>,
243        artifact_graph: ArtifactGraph,
244        scene_objects: Vec<Object>,
245        source_range_to_object: BTreeMap<SourceRange, ObjectId>,
246        var_solutions: Vec<(SourceRange, Option<NodePath>, Number)>,
247        filenames: IndexMap<ModuleId, ModulePath>,
248        source_files: IndexMap<ModuleId, ModuleSource>,
249        default_planes: Option<DefaultPlanes>,
250    ) -> Self {
251        Self {
252            error,
253            non_fatal,
254            variables,
255            operations,
256            _artifact_commands: artifact_commands,
257            artifact_graph,
258            scene_objects,
259            source_range_to_object,
260            var_solutions,
261            scene_graph: Default::default(),
262            filenames,
263            source_files,
264            default_planes,
265        }
266    }
267
268    pub fn no_outputs(error: KclError) -> Self {
269        Self {
270            error,
271            non_fatal: Default::default(),
272            variables: Default::default(),
273            operations: Default::default(),
274            _artifact_commands: Default::default(),
275            artifact_graph: Default::default(),
276            scene_objects: Default::default(),
277            source_range_to_object: Default::default(),
278            var_solutions: Default::default(),
279            scene_graph: Default::default(),
280            filenames: Default::default(),
281            source_files: Default::default(),
282            default_planes: Default::default(),
283        }
284    }
285
286    /// This is for when the error is generated after a successful execution.
287    pub fn from_error_outcome(error: KclError, outcome: ExecOutcome) -> Self {
288        KclErrorWithOutputs {
289            error,
290            non_fatal: outcome.issues,
291            variables: outcome.variables,
292            operations: outcome.operations,
293            _artifact_commands: Default::default(),
294            artifact_graph: outcome.artifact_graph,
295            scene_objects: outcome.scene_objects,
296            source_range_to_object: outcome.source_range_to_object,
297            var_solutions: outcome.var_solutions,
298            scene_graph: Default::default(),
299            filenames: outcome.filenames,
300            source_files: Default::default(),
301            default_planes: outcome.default_planes,
302        }
303    }
304
305    pub fn sketch_constraint_report(&self) -> crate::SketchConstraintReport {
306        crate::execution::sketch_constraint_report_from_scene_objects(&self.scene_objects)
307    }
308
309    pub fn into_miette_report_with_outputs(self, code: &str) -> anyhow::Result<ReportWithOutputs> {
310        let mut source_ranges = self.error.source_ranges();
311
312        // Pop off the first source range to get the filename.
313        let first_source_range = source_ranges
314            .pop()
315            .ok_or_else(|| anyhow::anyhow!("No source ranges found"))?;
316
317        let source = self
318            .source_files
319            .get(&first_source_range.module_id())
320            .cloned()
321            .unwrap_or(ModuleSource {
322                source: code.to_string(),
323                path: self
324                    .filenames
325                    .get(&first_source_range.module_id())
326                    .cloned()
327                    .unwrap_or(ModulePath::Main),
328            });
329        let filename = source.path.to_string();
330        let kcl_source = source.source;
331
332        let mut related = Vec::new();
333        for source_range in source_ranges {
334            let module_id = source_range.module_id();
335            let source = self.source_files.get(&module_id).cloned().unwrap_or(ModuleSource {
336                source: code.to_string(),
337                path: self.filenames.get(&module_id).cloned().unwrap_or(ModulePath::Main),
338            });
339            let error = self.error.override_source_ranges(vec![source_range]);
340            let report = Report {
341                error,
342                kcl_source: source.source.to_string(),
343                filename: source.path.to_string(),
344            };
345            related.push(report);
346        }
347
348        Ok(ReportWithOutputs {
349            error: self,
350            kcl_source,
351            filename,
352            related,
353        })
354    }
355}
356
357impl IsRetryable for KclErrorWithOutputs {
358    fn is_retryable(&self) -> bool {
359        matches!(
360            self.error,
361            KclError::EngineHangup { .. } | KclError::EngineInternal { .. }
362        )
363    }
364}
365
366impl IntoDiagnostic for KclErrorWithOutputs {
367    fn to_lsp_diagnostics(&self, code: &str) -> Vec<Diagnostic> {
368        let message = self.error.get_message();
369        let source_ranges = self.error.source_ranges();
370
371        source_ranges
372            .into_iter()
373            .map(|source_range| {
374                let source = self.source_files.get(&source_range.module_id()).cloned().or_else(|| {
375                    self.filenames
376                        .get(&source_range.module_id())
377                        .cloned()
378                        .map(|path| ModuleSource {
379                            source: code.to_string(),
380                            path,
381                        })
382                });
383
384                let related_information = source.and_then(|source| {
385                    let mut filename = source.path.to_string();
386                    if !filename.starts_with("file://") {
387                        filename = format!("file:///{}", filename.trim_start_matches("/"));
388                    }
389
390                    url::Url::parse(&filename).ok().map(|uri| {
391                        vec![tower_lsp::lsp_types::DiagnosticRelatedInformation {
392                            location: tower_lsp::lsp_types::Location {
393                                uri,
394                                range: source_range.to_lsp_range(&source.source),
395                            },
396                            message: message.to_string(),
397                        }]
398                    })
399                });
400
401                Diagnostic {
402                    range: source_range.to_lsp_range(code),
403                    severity: Some(self.severity()),
404                    code: None,
405                    // TODO: this is neat we can pass a URL to a help page here for this specific error.
406                    code_description: None,
407                    source: Some("kcl".to_string()),
408                    related_information,
409                    message: message.clone(),
410                    tags: None,
411                    data: None,
412                }
413            })
414            .collect()
415    }
416
417    fn severity(&self) -> DiagnosticSeverity {
418        DiagnosticSeverity::ERROR
419    }
420}
421
422#[derive(thiserror::Error, Debug)]
423#[error("{}", self.error.error.get_message())]
424pub struct ReportWithOutputs {
425    pub error: KclErrorWithOutputs,
426    pub kcl_source: String,
427    pub filename: String,
428    pub related: Vec<Report>,
429}
430
431impl miette::Diagnostic for ReportWithOutputs {
432    fn code<'a>(&'a self) -> Option<Box<dyn std::fmt::Display + 'a>> {
433        let family = match self.error.error {
434            KclError::Lexical { .. } => "Lexical",
435            KclError::Syntax { .. } => "Syntax",
436            KclError::Semantic { .. } => "Semantic",
437            KclError::ImportCycle { .. } => "ImportCycle",
438            KclError::Argument { .. } => "Argument",
439            KclError::Type { .. } => "Type",
440            KclError::Io { .. } => "I/O",
441            KclError::Unexpected { .. } => "Unexpected",
442            KclError::ValueAlreadyDefined { .. } => "ValueAlreadyDefined",
443            KclError::UndefinedValue { .. } => "UndefinedValue",
444            KclError::InvalidExpression { .. } => "InvalidExpression",
445            KclError::MaxCallStack { .. } => "MaxCallStack",
446            KclError::Refactor { .. } => "Refactor",
447            KclError::Engine { .. } => "Engine",
448            KclError::EngineHangup { .. } => "EngineHangup",
449            KclError::EngineInternal { .. } => "EngineInternal",
450            KclError::Internal { .. } => "Internal",
451        };
452        let error_string = format!("KCL {family} error");
453        Some(Box::new(error_string))
454    }
455
456    fn source_code(&self) -> Option<&dyn miette::SourceCode> {
457        Some(&self.kcl_source)
458    }
459
460    fn labels(&self) -> Option<Box<dyn Iterator<Item = miette::LabeledSpan> + '_>> {
461        let iter = self
462            .error
463            .error
464            .source_ranges()
465            .into_iter()
466            .map(miette::SourceSpan::from)
467            .map(|span| miette::LabeledSpan::new_with_span(Some(self.filename.to_string()), span));
468        Some(Box::new(iter))
469    }
470
471    fn related<'a>(&'a self) -> Option<Box<dyn Iterator<Item = &'a dyn miette::Diagnostic> + 'a>> {
472        let iter = self.related.iter().map(|r| r as &dyn miette::Diagnostic);
473        Some(Box::new(iter))
474    }
475}
476
477#[derive(thiserror::Error, Debug)]
478#[error("{}", self.error.get_message())]
479pub struct Report {
480    pub error: KclError,
481    pub kcl_source: String,
482    pub filename: String,
483}
484
485impl miette::Diagnostic for Report {
486    fn code<'a>(&'a self) -> Option<Box<dyn std::fmt::Display + 'a>> {
487        let family = match self.error {
488            KclError::Lexical { .. } => "Lexical",
489            KclError::Syntax { .. } => "Syntax",
490            KclError::Semantic { .. } => "Semantic",
491            KclError::ImportCycle { .. } => "ImportCycle",
492            KclError::Argument { .. } => "Argument",
493            KclError::Type { .. } => "Type",
494            KclError::Io { .. } => "I/O",
495            KclError::Unexpected { .. } => "Unexpected",
496            KclError::ValueAlreadyDefined { .. } => "ValueAlreadyDefined",
497            KclError::UndefinedValue { .. } => "UndefinedValue",
498            KclError::InvalidExpression { .. } => "InvalidExpression",
499            KclError::MaxCallStack { .. } => "MaxCallStack",
500            KclError::Refactor { .. } => "Refactor",
501            KclError::Engine { .. } => "Engine",
502            KclError::EngineHangup { .. } => "EngineHangup",
503            KclError::EngineInternal { .. } => "EngineInternal",
504            KclError::Internal { .. } => "Internal",
505        };
506        let error_string = format!("KCL {family} error");
507        Some(Box::new(error_string))
508    }
509
510    fn source_code(&self) -> Option<&dyn miette::SourceCode> {
511        Some(&self.kcl_source)
512    }
513
514    fn labels(&self) -> Option<Box<dyn Iterator<Item = miette::LabeledSpan> + '_>> {
515        let iter = self
516            .error
517            .source_ranges()
518            .into_iter()
519            .map(miette::SourceSpan::from)
520            .map(|span| miette::LabeledSpan::new_with_span(Some(self.filename.to_string()), span));
521        Some(Box::new(iter))
522    }
523}
524
525#[derive(thiserror::Error, Debug)]
526#[error("{}", self.issue.message)]
527pub struct CompilationIssueReport {
528    pub issue: CompilationIssue,
529    pub kcl_source: String,
530    pub filename: String,
531}
532
533impl miette::Diagnostic for CompilationIssueReport {
534    fn code<'a>(&'a self) -> Option<Box<dyn std::fmt::Display + 'a>> {
535        let tag = match self.issue.tag {
536            Tag::Deprecated => "deprecated",
537            Tag::Unnecessary => "unnecessary",
538            Tag::UnknownNumericUnits => "unknown-numeric-units",
539            Tag::None => return None,
540        };
541        Some(Box::new(format!("KCL {tag}")))
542    }
543
544    fn severity(&self) -> Option<miette::Severity> {
545        Some(match self.issue.severity {
546            Severity::Warning => miette::Severity::Warning,
547            Severity::Error | Severity::Fatal => miette::Severity::Error,
548        })
549    }
550
551    fn help<'a>(&'a self) -> Option<Box<dyn std::fmt::Display + 'a>> {
552        self.issue
553            .suggestion
554            .as_ref()
555            .map(|s| Box::new(s.title.clone()) as Box<dyn std::fmt::Display>)
556    }
557
558    fn source_code(&self) -> Option<&dyn miette::SourceCode> {
559        Some(&self.kcl_source)
560    }
561
562    fn labels(&self) -> Option<Box<dyn Iterator<Item = miette::LabeledSpan> + '_>> {
563        let span = miette::SourceSpan::from(self.issue.source_range);
564        let label = miette::LabeledSpan::new_with_span(Some(self.filename.to_string()), span);
565        Some(Box::new(std::iter::once(label)))
566    }
567}
568
569/// Render a [`CompilationIssue`] as a miette report string, mirroring the
570/// formatting used for [`Report`].
571pub fn render_compilation_issue_miette(filename: &str, source: &str, issue: CompilationIssue) -> String {
572    let report = CompilationIssueReport {
573        issue,
574        kcl_source: source.to_owned(),
575        filename: filename.to_owned(),
576    };
577    let report = miette::Report::new(report);
578    format!("{report:?}")
579}
580
581#[derive(Debug, Serialize, Deserialize, ts_rs::TS, Clone, PartialEq, Eq, thiserror::Error, miette::Diagnostic)]
582#[serde(rename_all = "camelCase")]
583#[error("{message}")]
584#[ts(export)]
585pub struct KclErrorDetails {
586    #[label(collection, "Errors")]
587    pub source_ranges: Vec<SourceRange>,
588    pub backtrace: Vec<super::BacktraceItem>,
589    #[serde(rename = "msg")]
590    pub message: String,
591}
592
593impl KclErrorDetails {
594    pub fn new(message: String, source_ranges: Vec<SourceRange>) -> KclErrorDetails {
595        let backtrace = source_ranges
596            .iter()
597            .map(|s| BacktraceItem {
598                source_range: *s,
599                fn_name: None,
600            })
601            .collect();
602        KclErrorDetails {
603            source_ranges,
604            backtrace,
605            message,
606        }
607    }
608}
609
610impl KclError {
611    pub fn internal(message: String) -> KclError {
612        KclError::Internal {
613            details: KclErrorDetails {
614                source_ranges: Default::default(),
615                backtrace: Default::default(),
616                message,
617            },
618        }
619    }
620
621    pub fn new_internal(details: KclErrorDetails) -> KclError {
622        KclError::Internal { details }
623    }
624
625    pub fn new_import_cycle(details: KclErrorDetails) -> KclError {
626        KclError::ImportCycle { details }
627    }
628
629    pub fn new_argument(details: KclErrorDetails) -> KclError {
630        KclError::Argument { details }
631    }
632
633    pub fn new_semantic(details: KclErrorDetails) -> KclError {
634        KclError::Semantic { details }
635    }
636
637    pub fn new_value_already_defined(details: KclErrorDetails) -> KclError {
638        KclError::ValueAlreadyDefined { details }
639    }
640
641    pub fn new_syntax(details: KclErrorDetails) -> KclError {
642        KclError::Syntax { details }
643    }
644
645    pub fn new_io(details: KclErrorDetails) -> KclError {
646        KclError::Io { details }
647    }
648
649    pub fn new_invalid_expression(details: KclErrorDetails) -> KclError {
650        KclError::InvalidExpression { details }
651    }
652
653    pub fn refactor(message: String) -> KclError {
654        KclError::Refactor {
655            details: KclErrorDetails {
656                source_ranges: Default::default(),
657                backtrace: Default::default(),
658                message,
659            },
660        }
661    }
662
663    pub fn new_engine(details: KclErrorDetails) -> KclError {
664        if details.message.eq_ignore_ascii_case("internal error") {
665            KclError::EngineInternal { details }
666        } else if is_retryable_engine_message(&details.message) {
667            KclError::EngineHangup {
668                details,
669                api_call_id: None,
670            }
671        } else {
672            KclError::Engine { details }
673        }
674    }
675
676    pub fn new_engine_hangup(details: KclErrorDetails, api_call_id: Option<String>) -> KclError {
677        KclError::EngineHangup { details, api_call_id }
678    }
679
680    pub fn new_lexical(details: KclErrorDetails) -> KclError {
681        KclError::Lexical { details }
682    }
683
684    pub fn new_undefined_value(details: KclErrorDetails, name: Option<String>) -> KclError {
685        KclError::UndefinedValue { details, name }
686    }
687
688    pub fn new_type(details: KclErrorDetails) -> KclError {
689        KclError::Type { details }
690    }
691
692    pub fn is_undefined_value(&self) -> bool {
693        matches!(self, KclError::UndefinedValue { .. })
694    }
695
696    /// Get the error message.
697    pub fn get_message(&self) -> String {
698        format!("{}: {}", self.error_type(), self.message())
699    }
700
701    pub fn error_type(&self) -> &'static str {
702        match self {
703            KclError::Lexical { .. } => "lexical",
704            KclError::Syntax { .. } => "syntax",
705            KclError::Semantic { .. } => "semantic",
706            KclError::ImportCycle { .. } => "import cycle",
707            KclError::Argument { .. } => "argument",
708            KclError::Type { .. } => "type",
709            KclError::Io { .. } => "i/o",
710            KclError::Unexpected { .. } => "unexpected",
711            KclError::ValueAlreadyDefined { .. } => "value already defined",
712            KclError::UndefinedValue { .. } => "undefined value",
713            KclError::InvalidExpression { .. } => "invalid expression",
714            KclError::MaxCallStack { .. } => "max call stack",
715            KclError::Refactor { .. } => "refactor",
716            KclError::Engine { .. } => "engine",
717            KclError::EngineHangup { .. } => "engine hangup",
718            KclError::EngineInternal { .. } => "engine internal",
719            KclError::Internal { .. } => "internal",
720        }
721    }
722
723    pub fn source_ranges(&self) -> Vec<SourceRange> {
724        match &self {
725            KclError::Lexical { details: e } => e.source_ranges.clone(),
726            KclError::Syntax { details: e } => e.source_ranges.clone(),
727            KclError::Semantic { details: e } => e.source_ranges.clone(),
728            KclError::ImportCycle { details: e } => e.source_ranges.clone(),
729            KclError::Argument { details: e } => e.source_ranges.clone(),
730            KclError::Type { details: e } => e.source_ranges.clone(),
731            KclError::Io { details: e } => e.source_ranges.clone(),
732            KclError::Unexpected { details: e } => e.source_ranges.clone(),
733            KclError::ValueAlreadyDefined { details: e } => e.source_ranges.clone(),
734            KclError::UndefinedValue { details: e, .. } => e.source_ranges.clone(),
735            KclError::InvalidExpression { details: e } => e.source_ranges.clone(),
736            KclError::MaxCallStack { details: e } => e.source_ranges.clone(),
737            KclError::Refactor { details: e } => e.source_ranges.clone(),
738            KclError::Engine { details: e } => e.source_ranges.clone(),
739            KclError::EngineHangup { details: e, .. } => e.source_ranges.clone(),
740            KclError::EngineInternal { details: e } => e.source_ranges.clone(),
741            KclError::Internal { details: e } => e.source_ranges.clone(),
742        }
743    }
744
745    /// Get the inner error message.
746    pub fn message(&self) -> &str {
747        match &self {
748            KclError::Lexical { details: e } => &e.message,
749            KclError::Syntax { details: e } => &e.message,
750            KclError::Semantic { details: e } => &e.message,
751            KclError::ImportCycle { details: e } => &e.message,
752            KclError::Argument { details: e } => &e.message,
753            KclError::Type { details: e } => &e.message,
754            KclError::Io { details: e } => &e.message,
755            KclError::Unexpected { details: e } => &e.message,
756            KclError::ValueAlreadyDefined { details: e } => &e.message,
757            KclError::UndefinedValue { details: e, .. } => &e.message,
758            KclError::InvalidExpression { details: e } => &e.message,
759            KclError::MaxCallStack { details: e } => &e.message,
760            KclError::Refactor { details: e } => &e.message,
761            KclError::Engine { details: e } => &e.message,
762            KclError::EngineHangup { details: e, .. } => &e.message,
763            KclError::EngineInternal { details: e } => &e.message,
764            KclError::Internal { details: e } => &e.message,
765        }
766    }
767
768    pub fn backtrace(&self) -> Vec<BacktraceItem> {
769        match self {
770            KclError::Lexical { details: e }
771            | KclError::Syntax { details: e }
772            | KclError::Semantic { details: e }
773            | KclError::ImportCycle { details: e }
774            | KclError::Argument { details: e }
775            | KclError::Type { details: e }
776            | KclError::Io { details: e }
777            | KclError::Unexpected { details: e }
778            | KclError::ValueAlreadyDefined { details: e }
779            | KclError::UndefinedValue { details: e, .. }
780            | KclError::InvalidExpression { details: e }
781            | KclError::MaxCallStack { details: e }
782            | KclError::Refactor { details: e }
783            | KclError::Engine { details: e }
784            | KclError::EngineHangup { details: e, .. }
785            | KclError::EngineInternal { details: e }
786            | KclError::Internal { details: e } => e.backtrace.clone(),
787        }
788    }
789
790    pub(crate) fn override_source_ranges(&self, source_ranges: Vec<SourceRange>) -> Self {
791        let mut new = self.clone();
792        match &mut new {
793            KclError::Lexical { details: e }
794            | KclError::Syntax { details: e }
795            | KclError::Semantic { details: e }
796            | KclError::ImportCycle { details: e }
797            | KclError::Argument { details: e }
798            | KclError::Type { details: e }
799            | KclError::Io { details: e }
800            | KclError::Unexpected { details: e }
801            | KclError::ValueAlreadyDefined { details: e }
802            | KclError::UndefinedValue { details: e, .. }
803            | KclError::InvalidExpression { details: e }
804            | KclError::MaxCallStack { details: e }
805            | KclError::Refactor { details: e }
806            | KclError::Engine { details: e }
807            | KclError::EngineHangup { details: e, .. }
808            | KclError::EngineInternal { details: e }
809            | KclError::Internal { details: e } => {
810                e.backtrace = source_ranges
811                    .iter()
812                    .map(|s| BacktraceItem {
813                        source_range: *s,
814                        fn_name: None,
815                    })
816                    .collect();
817                e.source_ranges = source_ranges;
818            }
819        }
820
821        new
822    }
823
824    pub(crate) fn add_unwind_location(&self, last_fn_name: Option<String>, source_range: SourceRange) -> Self {
825        let mut new = self.clone();
826        match &mut new {
827            KclError::Lexical { details: e }
828            | KclError::Syntax { details: e }
829            | KclError::Semantic { details: e }
830            | KclError::ImportCycle { details: e }
831            | KclError::Argument { details: e }
832            | KclError::Type { details: e }
833            | KclError::Io { details: e }
834            | KclError::Unexpected { details: e }
835            | KclError::ValueAlreadyDefined { details: e }
836            | KclError::UndefinedValue { details: e, .. }
837            | KclError::InvalidExpression { details: e }
838            | KclError::MaxCallStack { details: e }
839            | KclError::Refactor { details: e }
840            | KclError::Engine { details: e }
841            | KclError::EngineHangup { details: e, .. }
842            | KclError::EngineInternal { details: e }
843            | KclError::Internal { details: e } => {
844                if let Some(item) = e.backtrace.last_mut() {
845                    item.fn_name = last_fn_name;
846                }
847                e.backtrace.push(BacktraceItem {
848                    source_range,
849                    fn_name: None,
850                });
851                e.source_ranges.push(source_range);
852            }
853        }
854
855        new
856    }
857}
858
859#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, ts_rs::TS, thiserror::Error, miette::Diagnostic)]
860#[serde(rename_all = "camelCase")]
861#[ts(export)]
862pub struct BacktraceItem {
863    pub source_range: SourceRange,
864    pub fn_name: Option<String>,
865}
866
867impl std::fmt::Display for BacktraceItem {
868    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
869        if let Some(fn_name) = &self.fn_name {
870            write!(f, "{fn_name}: {:?}", self.source_range)
871        } else {
872            write!(f, "(fn): {:?}", self.source_range)
873        }
874    }
875}
876
877impl IntoDiagnostic for KclError {
878    fn to_lsp_diagnostics(&self, code: &str) -> Vec<Diagnostic> {
879        let message = self.get_message();
880        let source_ranges = self.source_ranges();
881
882        // Limit to only errors in the top-level file.
883        let module_id = ModuleId::default();
884        let source_ranges = source_ranges
885            .iter()
886            .filter(|r| r.module_id() == module_id)
887            .collect::<Vec<_>>();
888
889        let mut diagnostics = Vec::new();
890        for source_range in &source_ranges {
891            diagnostics.push(Diagnostic {
892                range: source_range.to_lsp_range(code),
893                severity: Some(self.severity()),
894                code: None,
895                // TODO: this is neat we can pass a URL to a help page here for this specific error.
896                code_description: None,
897                source: Some("kcl".to_string()),
898                related_information: None,
899                message: message.clone(),
900                tags: None,
901                data: None,
902            });
903        }
904
905        diagnostics
906    }
907
908    fn severity(&self) -> DiagnosticSeverity {
909        DiagnosticSeverity::ERROR
910    }
911}
912
913/// This is different than to_string() in that it will serialize the Error
914/// the struct as JSON so we can deserialize it on the js side.
915impl From<KclError> for String {
916    fn from(error: KclError) -> Self {
917        serde_json::to_string(&error).unwrap()
918    }
919}
920
921impl From<String> for KclError {
922    fn from(error: String) -> Self {
923        serde_json::from_str(&error).unwrap()
924    }
925}
926
927#[cfg(feature = "pyo3")]
928impl From<pyo3::PyErr> for KclError {
929    fn from(error: pyo3::PyErr) -> Self {
930        KclError::new_internal(KclErrorDetails {
931            source_ranges: vec![],
932            backtrace: Default::default(),
933            message: error.to_string(),
934        })
935    }
936}
937
938#[cfg(feature = "pyo3")]
939impl From<KclError> for pyo3::PyErr {
940    fn from(error: KclError) -> Self {
941        pyo3::exceptions::PyException::new_err(error.to_string())
942    }
943}
944
945impl From<CompilationIssue> for KclErrorDetails {
946    fn from(err: CompilationIssue) -> Self {
947        let backtrace = vec![BacktraceItem {
948            source_range: err.source_range,
949            fn_name: None,
950        }];
951        KclErrorDetails {
952            source_ranges: vec![err.source_range],
953            backtrace,
954            message: err.message,
955        }
956    }
957}
958
959#[cfg(test)]
960mod tests {
961    use super::*;
962
963    #[test]
964    fn missing_filename_mapping_does_not_panic_when_building_diagnostics() {
965        let error = KclErrorWithOutputs::no_outputs(KclError::new_semantic(KclErrorDetails::new(
966            "boom".to_owned(),
967            vec![SourceRange::new(0, 1, ModuleId::from_usize(9))],
968        )));
969
970        let diagnostics = error.to_lsp_diagnostics("x");
971
972        assert_eq!(diagnostics.len(), 1);
973        assert_eq!(diagnostics[0].message, "semantic: boom");
974        assert_eq!(diagnostics[0].related_information, None);
975    }
976}