Skip to main content

depyler_hir/
error.rs

1use std::fmt;
2use thiserror::Error;
3
4/// Source location information for error reporting
5#[derive(Debug, Clone, PartialEq)]
6pub struct SourceLocation {
7    pub file: String,
8    pub line: usize,
9    pub column: usize,
10}
11
12impl fmt::Display for SourceLocation {
13    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
14        write!(f, "{}:{}:{}", self.file, self.line, self.column)
15    }
16}
17
18/// Types of transpilation errors
19#[derive(Debug, Error)]
20pub enum ErrorKind {
21    #[error("Python parse error")]
22    ParseError,
23
24    #[error("Unsupported Python feature")]
25    UnsupportedFeature(String),
26
27    #[error("Type inference error")]
28    TypeInferenceError(String),
29
30    #[error("Invalid type annotation")]
31    InvalidTypeAnnotation(String),
32
33    #[error("Type mismatch")]
34    TypeMismatch {
35        expected: String,
36        found: String,
37        context: String,
38    },
39
40    #[error("Code generation error")]
41    CodeGenerationError(String),
42
43    #[error("Verification failed")]
44    VerificationError(String),
45
46    #[error("Internal error")]
47    InternalError(String),
48}
49
50/// Context-aware transpilation error
51#[derive(Debug, Error)]
52pub struct TranspileError {
53    pub kind: ErrorKind,
54    pub location: Option<SourceLocation>,
55    pub context: Vec<String>,
56    pub source: Option<Box<dyn std::error::Error + Send + Sync>>,
57}
58
59impl TranspileError {
60    /// Create a new error with the given kind
61    pub fn new(kind: ErrorKind) -> Self {
62        Self {
63            kind,
64            location: None,
65            context: Vec::new(),
66            source: None,
67        }
68    }
69
70    /// Add location information to the error
71    pub fn with_location(mut self, location: SourceLocation) -> Self {
72        self.location = Some(location);
73        self
74    }
75
76    /// Add context to the error
77    pub fn with_context(mut self, ctx: impl Into<String>) -> Self {
78        self.context.push(ctx.into());
79        self
80    }
81
82    /// Add source error
83    pub fn with_source(mut self, source: impl std::error::Error + Send + Sync + 'static) -> Self {
84        self.source = Some(Box::new(source));
85        self
86    }
87}
88
89impl fmt::Display for TranspileError {
90    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
91        // Write main error message
92        write!(f, "{}", self.kind)?;
93
94        // Add location if available
95        self.format_location(f)?;
96
97        // Add context if available
98        self.format_context_list(f)?;
99
100        Ok(())
101    }
102}
103
104impl TranspileError {
105    /// Format location information if available
106    #[inline]
107    fn format_location(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
108        if let Some(loc) = &self.location {
109            write!(f, " at {loc}")?;
110        }
111        Ok(())
112    }
113
114    /// Format context list if not empty
115    #[inline]
116    fn format_context_list(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
117        if !self.context.is_empty() {
118            write!(f, "\n\nContext:")?;
119            for (i, ctx) in self.context.iter().enumerate() {
120                write!(f, "\n  {}. {}", i + 1, ctx)?;
121            }
122        }
123        Ok(())
124    }
125}
126
127impl TranspileError {
128    /// Create backend-specific error
129    pub fn backend_error(msg: impl Into<String>) -> Self {
130        Self::new(ErrorKind::CodeGenerationError(msg.into()))
131    }
132
133    /// Create transformation error
134    pub fn transform_error(msg: impl Into<String>) -> Self {
135        Self::new(ErrorKind::CodeGenerationError(format!(
136            "Transformation failed: {}",
137            msg.into()
138        )))
139    }
140
141    /// Create optimization error
142    pub fn optimization_error(msg: impl Into<String>) -> Self {
143        Self::new(ErrorKind::InternalError(format!(
144            "Optimization failed: {}",
145            msg.into()
146        )))
147    }
148}
149
150/// Result type alias for transpilation operations
151pub type TranspileResult<T> = Result<T, TranspileError>;
152
153/// Extension trait for adding context to Results
154pub trait ResultExt<T> {
155    #[allow(clippy::result_large_err)]
156    fn with_context(self, ctx: impl Into<String>) -> TranspileResult<T>;
157}
158
159impl<T, E> ResultExt<T> for Result<T, E>
160where
161    E: Into<TranspileError>,
162{
163    fn with_context(self, ctx: impl Into<String>) -> TranspileResult<T> {
164        self.map_err(|e| e.into().with_context(ctx))
165    }
166}
167
168/// Convert anyhow errors to TranspileError
169impl From<anyhow::Error> for TranspileError {
170    fn from(err: anyhow::Error) -> Self {
171        TranspileError::new(ErrorKind::InternalError(err.to_string()))
172    }
173}
174
175/// Helper macro for creating errors with context
176#[macro_export]
177macro_rules! transpile_error {
178    ($kind:expr) => {
179        $crate::error::TranspileError::new($kind)
180    };
181
182    ($kind:expr, $($ctx:expr),+) => {{
183        let mut err = $crate::error::TranspileError::new($kind);
184        $(
185            err = err.with_context($ctx);
186        )+
187        err
188    }};
189}
190
191/// Helper macro for bailing with a transpile error
192#[macro_export]
193macro_rules! transpile_bail {
194    ($kind:expr) => {
195        return Err($crate::transpile_error!($kind))
196    };
197
198    ($kind:expr, $($ctx:expr),+) => {
199        return Err($crate::transpile_error!($kind, $($ctx),+))
200    };
201}
202
203#[cfg(test)]
204mod tests {
205    use super::*;
206
207    // === SourceLocation tests ===
208
209    #[test]
210    fn test_source_location_new() {
211        let loc = SourceLocation {
212            file: "test.py".to_string(),
213            line: 10,
214            column: 5,
215        };
216        assert_eq!(loc.file, "test.py");
217        assert_eq!(loc.line, 10);
218        assert_eq!(loc.column, 5);
219    }
220
221    #[test]
222    fn test_source_location_display() {
223        let loc = SourceLocation {
224            file: "example.py".to_string(),
225            line: 42,
226            column: 8,
227        };
228        assert_eq!(format!("{}", loc), "example.py:42:8");
229    }
230
231    #[test]
232    fn test_source_location_display_edge_cases() {
233        let loc = SourceLocation {
234            file: "".to_string(),
235            line: 0,
236            column: 0,
237        };
238        assert_eq!(format!("{}", loc), ":0:0");
239    }
240
241    #[test]
242    fn test_source_location_clone() {
243        let loc = SourceLocation {
244            file: "test.py".to_string(),
245            line: 1,
246            column: 1,
247        };
248        let cloned = loc.clone();
249        assert_eq!(loc, cloned);
250    }
251
252    #[test]
253    fn test_source_location_partial_eq() {
254        let loc1 = SourceLocation {
255            file: "a.py".to_string(),
256            line: 1,
257            column: 1,
258        };
259        let loc2 = SourceLocation {
260            file: "a.py".to_string(),
261            line: 1,
262            column: 1,
263        };
264        let loc3 = SourceLocation {
265            file: "b.py".to_string(),
266            line: 1,
267            column: 1,
268        };
269        assert_eq!(loc1, loc2);
270        assert_ne!(loc1, loc3);
271    }
272
273    #[test]
274    fn test_source_location_debug() {
275        let loc = SourceLocation {
276            file: "test.py".to_string(),
277            line: 5,
278            column: 10,
279        };
280        let debug = format!("{:?}", loc);
281        assert!(debug.contains("SourceLocation"));
282        assert!(debug.contains("test.py"));
283    }
284
285    // === ErrorKind tests ===
286
287    #[test]
288    fn test_error_kind_parse_error() {
289        let err = ErrorKind::ParseError;
290        assert_eq!(format!("{}", err), "Python parse error");
291    }
292
293    #[test]
294    fn test_error_kind_unsupported_feature() {
295        let err = ErrorKind::UnsupportedFeature("async generators".to_string());
296        let display = format!("{}", err);
297        assert!(display.contains("Unsupported Python feature"));
298    }
299
300    #[test]
301    fn test_error_kind_type_inference_error() {
302        let err = ErrorKind::TypeInferenceError("cannot infer type".to_string());
303        let display = format!("{}", err);
304        assert!(display.contains("Type inference error"));
305    }
306
307    #[test]
308    fn test_error_kind_invalid_type_annotation() {
309        let err = ErrorKind::InvalidTypeAnnotation("List[Unknown]".to_string());
310        let display = format!("{}", err);
311        assert!(display.contains("Invalid type annotation"));
312    }
313
314    #[test]
315    fn test_error_kind_type_mismatch() {
316        let err = ErrorKind::TypeMismatch {
317            expected: "int".to_string(),
318            found: "str".to_string(),
319            context: "function return".to_string(),
320        };
321        let display = format!("{}", err);
322        assert!(display.contains("Type mismatch"));
323    }
324
325    #[test]
326    fn test_error_kind_code_generation_error() {
327        let err = ErrorKind::CodeGenerationError("failed to generate".to_string());
328        let display = format!("{}", err);
329        assert!(display.contains("Code generation error"));
330    }
331
332    #[test]
333    fn test_error_kind_verification_error() {
334        let err = ErrorKind::VerificationError("verification failed".to_string());
335        let display = format!("{}", err);
336        assert!(display.contains("Verification failed"));
337    }
338
339    #[test]
340    fn test_error_kind_internal_error() {
341        let err = ErrorKind::InternalError("unexpected state".to_string());
342        let display = format!("{}", err);
343        assert!(display.contains("Internal error"));
344    }
345
346    #[test]
347    fn test_error_kind_debug() {
348        let err = ErrorKind::ParseError;
349        let debug = format!("{:?}", err);
350        assert!(debug.contains("ParseError"));
351    }
352
353    // === TranspileError tests ===
354
355    #[test]
356    fn test_error_creation() {
357        let err = TranspileError::new(ErrorKind::UnsupportedFeature("async/await".to_string()));
358        assert!(matches!(err.kind, ErrorKind::UnsupportedFeature(_)));
359        assert!(err.location.is_none());
360        assert!(err.context.is_empty());
361    }
362
363    #[test]
364    fn test_error_with_location() {
365        let loc = SourceLocation {
366            file: "test.py".to_string(),
367            line: 10,
368            column: 5,
369        };
370
371        let err = TranspileError::new(ErrorKind::ParseError).with_location(loc.clone());
372
373        assert_eq!(err.location.unwrap(), loc);
374    }
375
376    #[test]
377    fn test_error_with_context() {
378        let err = TranspileError::new(ErrorKind::TypeInferenceError("unknown type".to_string()))
379            .with_context("in function 'add'")
380            .with_context("while processing parameter 'x'");
381
382        assert_eq!(err.context.len(), 2);
383        assert_eq!(err.context[0], "in function 'add'");
384        assert_eq!(err.context[1], "while processing parameter 'x'");
385    }
386
387    #[test]
388    fn test_error_with_source() {
389        let source_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
390        let err = TranspileError::new(ErrorKind::InternalError("io error".to_string()))
391            .with_source(source_err);
392        assert!(err.source.is_some());
393    }
394
395    #[test]
396    fn test_error_display() {
397        let loc = SourceLocation {
398            file: "example.py".to_string(),
399            line: 25,
400            column: 10,
401        };
402
403        let err = TranspileError::new(ErrorKind::UnsupportedFeature("decorators".to_string()))
404            .with_location(loc)
405            .with_context("in function 'my_func'")
406            .with_context("processing @decorator syntax");
407
408        let display = format!("{err}");
409        assert!(display.contains("Unsupported Python feature"));
410        assert!(display.contains("example.py:25:10"));
411        assert!(display.contains("in function 'my_func'"));
412    }
413
414    #[test]
415    fn test_error_display_no_location() {
416        let err = TranspileError::new(ErrorKind::ParseError);
417        let display = format!("{}", err);
418        assert!(display.contains("Python parse error"));
419        assert!(!display.contains("at "));
420    }
421
422    #[test]
423    fn test_error_display_no_context() {
424        let err = TranspileError::new(ErrorKind::ParseError);
425        let display = format!("{}", err);
426        assert!(!display.contains("Context:"));
427    }
428
429    #[test]
430    fn test_error_display_with_numbered_context() {
431        let err = TranspileError::new(ErrorKind::ParseError)
432            .with_context("first")
433            .with_context("second")
434            .with_context("third");
435        let display = format!("{}", err);
436        assert!(display.contains("1. first"));
437        assert!(display.contains("2. second"));
438        assert!(display.contains("3. third"));
439    }
440
441    #[test]
442    fn test_error_builder_chain() {
443        let loc = SourceLocation {
444            file: "chain.py".to_string(),
445            line: 1,
446            column: 1,
447        };
448        let source_err = std::io::Error::other("test");
449
450        let err = TranspileError::new(ErrorKind::InternalError("test".to_string()))
451            .with_location(loc.clone())
452            .with_context("ctx1")
453            .with_source(source_err);
454
455        assert_eq!(err.location, Some(loc));
456        assert_eq!(err.context.len(), 1);
457        assert!(err.source.is_some());
458    }
459
460    #[test]
461    fn test_error_debug() {
462        let err = TranspileError::new(ErrorKind::ParseError);
463        let debug = format!("{:?}", err);
464        assert!(debug.contains("TranspileError"));
465        assert!(debug.contains("ParseError"));
466    }
467
468    // === ResultExt tests ===
469
470    #[test]
471    fn test_result_ext_ok() {
472        let result: Result<i32, TranspileError> = Ok(42);
473        let with_ctx = result.with_context("extra context");
474        assert_eq!(with_ctx.unwrap(), 42);
475    }
476
477    #[test]
478    fn test_result_ext_err() {
479        let result: Result<i32, TranspileError> = Err(TranspileError::new(ErrorKind::ParseError));
480        let with_ctx = result.with_context("added context");
481        let err = with_ctx.unwrap_err();
482        assert_eq!(err.context.len(), 1);
483        assert_eq!(err.context[0], "added context");
484    }
485
486    // === From<anyhow::Error> tests ===
487
488    #[test]
489    fn test_from_anyhow_error() {
490        let anyhow_err = anyhow::anyhow!("something went wrong");
491        let err: TranspileError = anyhow_err.into();
492        assert!(matches!(err.kind, ErrorKind::InternalError(_)));
493        let display = format!("{}", err);
494        assert!(display.contains("Internal error"));
495    }
496
497    // === Macro tests ===
498
499    #[test]
500    fn test_transpile_error_macro() {
501        let err1 = transpile_error!(ErrorKind::ParseError);
502        assert!(matches!(err1.kind, ErrorKind::ParseError));
503
504        let err2 = transpile_error!(
505            ErrorKind::TypeInferenceError("test".to_string()),
506            "context 1",
507            "context 2"
508        );
509        assert_eq!(err2.context.len(), 2);
510    }
511
512    #[test]
513    fn test_transpile_error_macro_single_context() {
514        let err = transpile_error!(ErrorKind::ParseError, "single context");
515        assert_eq!(err.context.len(), 1);
516        assert_eq!(err.context[0], "single context");
517    }
518
519    #[test]
520    fn test_transpile_error_macro_many_contexts() {
521        let err = transpile_error!(
522            ErrorKind::InternalError("test".to_string()),
523            "ctx1",
524            "ctx2",
525            "ctx3",
526            "ctx4"
527        );
528        assert_eq!(err.context.len(), 4);
529    }
530
531    #[allow(clippy::result_large_err)]
532    fn bail_test_helper() -> TranspileResult<()> {
533        transpile_bail!(ErrorKind::ParseError);
534    }
535
536    #[test]
537    fn test_transpile_bail_macro() {
538        let result = bail_test_helper();
539        assert!(result.is_err());
540        let err = result.unwrap_err();
541        assert!(matches!(err.kind, ErrorKind::ParseError));
542    }
543
544    #[allow(clippy::result_large_err)]
545    fn bail_test_with_context() -> TranspileResult<()> {
546        transpile_bail!(ErrorKind::ParseError, "context1", "context2");
547    }
548
549    #[test]
550    fn test_transpile_bail_macro_with_context() {
551        let result = bail_test_with_context();
552        assert!(result.is_err());
553        let err = result.unwrap_err();
554        assert_eq!(err.context.len(), 2);
555    }
556
557    // === Type alias tests ===
558
559    #[test]
560    fn test_transpile_result_ok() {
561        let result: TranspileResult<i32> = Ok(42);
562        assert!(result.is_ok());
563    }
564
565    #[test]
566    fn test_transpile_result_err() {
567        let result: TranspileResult<i32> = Err(TranspileError::new(ErrorKind::ParseError));
568        assert!(result.is_err());
569    }
570
571    // === Edge cases ===
572
573    #[test]
574    fn test_empty_strings_in_error_kind() {
575        let err = ErrorKind::UnsupportedFeature("".to_string());
576        let display = format!("{}", err);
577        assert!(display.contains("Unsupported Python feature"));
578    }
579
580    #[test]
581    fn test_type_mismatch_all_empty() {
582        let err = ErrorKind::TypeMismatch {
583            expected: "".to_string(),
584            found: "".to_string(),
585            context: "".to_string(),
586        };
587        let display = format!("{}", err);
588        assert!(display.contains("Type mismatch"));
589    }
590
591    #[test]
592    fn test_context_with_special_chars() {
593        let err = TranspileError::new(ErrorKind::ParseError)
594            .with_context("context with 'quotes' and \"double quotes\"")
595            .with_context("context\nwith\nnewlines");
596        assert_eq!(err.context.len(), 2);
597    }
598
599    #[test]
600    fn test_long_context_chain() {
601        let mut err = TranspileError::new(ErrorKind::ParseError);
602        for i in 0..100 {
603            err = err.with_context(format!("context {}", i));
604        }
605        assert_eq!(err.context.len(), 100);
606    }
607}