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