Skip to main content

kcl_lib/
errors.rs

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