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.to_string();
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            .clone()
340            .into_iter()
341            .map(miette::SourceSpan::from)
342            .map(|span| miette::LabeledSpan::new_with_span(Some(self.filename.to_string()), span));
343        Some(Box::new(iter))
344    }
345
346    fn related<'a>(&'a self) -> Option<Box<dyn Iterator<Item = &'a dyn miette::Diagnostic> + 'a>> {
347        let iter = self.related.iter().map(|r| r as &dyn miette::Diagnostic);
348        Some(Box::new(iter))
349    }
350}
351
352#[derive(thiserror::Error, Debug)]
353#[error("{}", self.error.get_message())]
354pub struct Report {
355    pub error: KclError,
356    pub kcl_source: String,
357    pub filename: String,
358}
359
360impl miette::Diagnostic for Report {
361    fn code<'a>(&'a self) -> Option<Box<dyn std::fmt::Display + 'a>> {
362        let family = match self.error {
363            KclError::Lexical { .. } => "Lexical",
364            KclError::Syntax { .. } => "Syntax",
365            KclError::Semantic { .. } => "Semantic",
366            KclError::ImportCycle { .. } => "ImportCycle",
367            KclError::Argument { .. } => "Argument",
368            KclError::Type { .. } => "Type",
369            KclError::Io { .. } => "I/O",
370            KclError::Unexpected { .. } => "Unexpected",
371            KclError::ValueAlreadyDefined { .. } => "ValueAlreadyDefined",
372            KclError::UndefinedValue { .. } => "UndefinedValue",
373            KclError::InvalidExpression { .. } => "InvalidExpression",
374            KclError::Engine { .. } => "Engine",
375            KclError::Internal { .. } => "Internal",
376        };
377        let error_string = format!("KCL {family} error");
378        Some(Box::new(error_string))
379    }
380
381    fn source_code(&self) -> Option<&dyn miette::SourceCode> {
382        Some(&self.kcl_source)
383    }
384
385    fn labels(&self) -> Option<Box<dyn Iterator<Item = miette::LabeledSpan> + '_>> {
386        let iter = self
387            .error
388            .source_ranges()
389            .clone()
390            .into_iter()
391            .map(miette::SourceSpan::from)
392            .map(|span| miette::LabeledSpan::new_with_span(Some(self.filename.to_string()), span));
393        Some(Box::new(iter))
394    }
395}
396
397#[derive(Debug, Serialize, Deserialize, ts_rs::TS, Clone, PartialEq, Eq, thiserror::Error, miette::Diagnostic)]
398#[serde(rename_all = "camelCase")]
399#[error("{message}")]
400#[ts(export)]
401pub struct KclErrorDetails {
402    #[label(collection, "Errors")]
403    pub source_ranges: Vec<SourceRange>,
404    pub backtrace: Vec<BacktraceItem>,
405    #[serde(rename = "msg")]
406    pub message: String,
407}
408
409impl KclErrorDetails {
410    pub fn new(message: String, source_ranges: Vec<SourceRange>) -> KclErrorDetails {
411        let backtrace = source_ranges
412            .iter()
413            .map(|s| BacktraceItem {
414                source_range: *s,
415                fn_name: None,
416            })
417            .collect();
418        KclErrorDetails {
419            source_ranges,
420            backtrace,
421            message,
422        }
423    }
424}
425
426impl KclError {
427    pub fn internal(message: String) -> KclError {
428        KclError::Internal {
429            details: KclErrorDetails {
430                source_ranges: Default::default(),
431                backtrace: Default::default(),
432                message,
433            },
434        }
435    }
436
437    pub fn new_internal(details: KclErrorDetails) -> KclError {
438        KclError::Internal { details }
439    }
440
441    pub fn new_import_cycle(details: KclErrorDetails) -> KclError {
442        KclError::ImportCycle { details }
443    }
444
445    pub fn new_argument(details: KclErrorDetails) -> KclError {
446        KclError::Argument { details }
447    }
448
449    pub fn new_semantic(details: KclErrorDetails) -> KclError {
450        KclError::Semantic { details }
451    }
452
453    pub fn new_value_already_defined(details: KclErrorDetails) -> KclError {
454        KclError::ValueAlreadyDefined { details }
455    }
456
457    pub fn new_syntax(details: KclErrorDetails) -> KclError {
458        KclError::Syntax { details }
459    }
460
461    pub fn new_io(details: KclErrorDetails) -> KclError {
462        KclError::Io { details }
463    }
464
465    pub fn new_engine(details: KclErrorDetails) -> KclError {
466        KclError::Engine { details }
467    }
468
469    pub fn new_lexical(details: KclErrorDetails) -> KclError {
470        KclError::Lexical { details }
471    }
472
473    pub fn new_undefined_value(details: KclErrorDetails, name: Option<String>) -> KclError {
474        KclError::UndefinedValue { details, name }
475    }
476
477    pub fn new_type(details: KclErrorDetails) -> KclError {
478        KclError::Type { details }
479    }
480
481    /// Get the error message.
482    pub fn get_message(&self) -> String {
483        format!("{}: {}", self.error_type(), self.message())
484    }
485
486    pub fn error_type(&self) -> &'static str {
487        match self {
488            KclError::Lexical { .. } => "lexical",
489            KclError::Syntax { .. } => "syntax",
490            KclError::Semantic { .. } => "semantic",
491            KclError::ImportCycle { .. } => "import cycle",
492            KclError::Argument { .. } => "argument",
493            KclError::Type { .. } => "type",
494            KclError::Io { .. } => "i/o",
495            KclError::Unexpected { .. } => "unexpected",
496            KclError::ValueAlreadyDefined { .. } => "value already defined",
497            KclError::UndefinedValue { .. } => "undefined value",
498            KclError::InvalidExpression { .. } => "invalid expression",
499            KclError::Engine { .. } => "engine",
500            KclError::Internal { .. } => "internal",
501        }
502    }
503
504    pub fn source_ranges(&self) -> Vec<SourceRange> {
505        match &self {
506            KclError::Lexical { details: e } => e.source_ranges.clone(),
507            KclError::Syntax { details: e } => e.source_ranges.clone(),
508            KclError::Semantic { details: e } => e.source_ranges.clone(),
509            KclError::ImportCycle { details: e } => e.source_ranges.clone(),
510            KclError::Argument { details: e } => e.source_ranges.clone(),
511            KclError::Type { details: e } => e.source_ranges.clone(),
512            KclError::Io { details: e } => e.source_ranges.clone(),
513            KclError::Unexpected { details: e } => e.source_ranges.clone(),
514            KclError::ValueAlreadyDefined { details: e } => e.source_ranges.clone(),
515            KclError::UndefinedValue { details: e, .. } => e.source_ranges.clone(),
516            KclError::InvalidExpression { details: e } => e.source_ranges.clone(),
517            KclError::Engine { details: e } => e.source_ranges.clone(),
518            KclError::Internal { details: e } => e.source_ranges.clone(),
519        }
520    }
521
522    /// Get the inner error message.
523    pub fn message(&self) -> &str {
524        match &self {
525            KclError::Lexical { details: e } => &e.message,
526            KclError::Syntax { details: e } => &e.message,
527            KclError::Semantic { details: e } => &e.message,
528            KclError::ImportCycle { details: e } => &e.message,
529            KclError::Argument { details: e } => &e.message,
530            KclError::Type { details: e } => &e.message,
531            KclError::Io { details: e } => &e.message,
532            KclError::Unexpected { details: e } => &e.message,
533            KclError::ValueAlreadyDefined { details: e } => &e.message,
534            KclError::UndefinedValue { details: e, .. } => &e.message,
535            KclError::InvalidExpression { details: e } => &e.message,
536            KclError::Engine { details: e } => &e.message,
537            KclError::Internal { details: e } => &e.message,
538        }
539    }
540
541    pub fn backtrace(&self) -> Vec<BacktraceItem> {
542        match self {
543            KclError::Lexical { details: e }
544            | KclError::Syntax { details: e }
545            | KclError::Semantic { details: e }
546            | KclError::ImportCycle { details: e }
547            | KclError::Argument { details: e }
548            | KclError::Type { details: e }
549            | KclError::Io { details: e }
550            | KclError::Unexpected { details: e }
551            | KclError::ValueAlreadyDefined { details: e }
552            | KclError::UndefinedValue { details: e, .. }
553            | KclError::InvalidExpression { details: e }
554            | KclError::Engine { details: e }
555            | KclError::Internal { details: e } => e.backtrace.clone(),
556        }
557    }
558
559    pub(crate) fn override_source_ranges(&self, source_ranges: Vec<SourceRange>) -> Self {
560        let mut new = self.clone();
561        match &mut new {
562            KclError::Lexical { details: e }
563            | KclError::Syntax { details: e }
564            | KclError::Semantic { details: e }
565            | KclError::ImportCycle { details: e }
566            | KclError::Argument { details: e }
567            | KclError::Type { details: e }
568            | KclError::Io { details: e }
569            | KclError::Unexpected { details: e }
570            | KclError::ValueAlreadyDefined { details: e }
571            | KclError::UndefinedValue { details: e, .. }
572            | KclError::InvalidExpression { details: e }
573            | KclError::Engine { details: e }
574            | KclError::Internal { details: e } => {
575                e.backtrace = source_ranges
576                    .iter()
577                    .map(|s| BacktraceItem {
578                        source_range: *s,
579                        fn_name: None,
580                    })
581                    .collect();
582                e.source_ranges = source_ranges;
583            }
584        }
585
586        new
587    }
588
589    pub(crate) fn add_unwind_location(&self, last_fn_name: Option<String>, source_range: SourceRange) -> Self {
590        let mut new = self.clone();
591        match &mut new {
592            KclError::Lexical { details: e }
593            | KclError::Syntax { details: e }
594            | KclError::Semantic { details: e }
595            | KclError::ImportCycle { details: e }
596            | KclError::Argument { details: e }
597            | KclError::Type { details: e }
598            | KclError::Io { details: e }
599            | KclError::Unexpected { details: e }
600            | KclError::ValueAlreadyDefined { details: e }
601            | KclError::UndefinedValue { details: e, .. }
602            | KclError::InvalidExpression { details: e }
603            | KclError::Engine { details: e }
604            | KclError::Internal { details: e } => {
605                if let Some(item) = e.backtrace.last_mut() {
606                    item.fn_name = last_fn_name;
607                }
608                e.backtrace.push(BacktraceItem {
609                    source_range,
610                    fn_name: None,
611                });
612                e.source_ranges.push(source_range);
613            }
614        }
615
616        new
617    }
618}
619
620#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, ts_rs::TS, thiserror::Error, miette::Diagnostic)]
621#[serde(rename_all = "camelCase")]
622#[ts(export)]
623pub struct BacktraceItem {
624    pub source_range: SourceRange,
625    pub fn_name: Option<String>,
626}
627
628impl std::fmt::Display for BacktraceItem {
629    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
630        if let Some(fn_name) = &self.fn_name {
631            write!(f, "{fn_name}: {:?}", self.source_range)
632        } else {
633            write!(f, "(fn): {:?}", self.source_range)
634        }
635    }
636}
637
638impl IntoDiagnostic for KclError {
639    fn to_lsp_diagnostics(&self, code: &str) -> Vec<Diagnostic> {
640        let message = self.get_message();
641        let source_ranges = self.source_ranges();
642
643        // Limit to only errors in the top-level file.
644        let module_id = ModuleId::default();
645        let source_ranges = source_ranges
646            .iter()
647            .filter(|r| r.module_id() == module_id)
648            .collect::<Vec<_>>();
649
650        let mut diagnostics = Vec::new();
651        for source_range in &source_ranges {
652            diagnostics.push(Diagnostic {
653                range: source_range.to_lsp_range(code),
654                severity: Some(self.severity()),
655                code: None,
656                // TODO: this is neat we can pass a URL to a help page here for this specific error.
657                code_description: None,
658                source: Some("kcl".to_string()),
659                related_information: None,
660                message: message.clone(),
661                tags: None,
662                data: None,
663            });
664        }
665
666        diagnostics
667    }
668
669    fn severity(&self) -> DiagnosticSeverity {
670        DiagnosticSeverity::ERROR
671    }
672}
673
674/// This is different than to_string() in that it will serialize the Error
675/// the struct as JSON so we can deserialize it on the js side.
676impl From<KclError> for String {
677    fn from(error: KclError) -> Self {
678        serde_json::to_string(&error).unwrap()
679    }
680}
681
682impl From<String> for KclError {
683    fn from(error: String) -> Self {
684        serde_json::from_str(&error).unwrap()
685    }
686}
687
688#[cfg(feature = "pyo3")]
689impl From<pyo3::PyErr> for KclError {
690    fn from(error: pyo3::PyErr) -> Self {
691        KclError::new_internal(KclErrorDetails {
692            source_ranges: vec![],
693            backtrace: Default::default(),
694            message: error.to_string(),
695        })
696    }
697}
698
699#[cfg(feature = "pyo3")]
700impl From<KclError> for pyo3::PyErr {
701    fn from(error: KclError) -> Self {
702        pyo3::exceptions::PyException::new_err(error.to_string())
703    }
704}
705
706impl From<CompilationError> for KclErrorDetails {
707    fn from(err: CompilationError) -> Self {
708        let backtrace = vec![BacktraceItem {
709            source_range: err.source_range,
710            fn_name: None,
711        }];
712        KclErrorDetails {
713            source_ranges: vec![err.source_range],
714            backtrace,
715            message: err.message,
716        }
717    }
718}