kcl_lib/
errors.rs

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