lemma/
error.rs

1use crate::parsing::ast::Span;
2use crate::Source;
3use std::fmt;
4use std::sync::Arc;
5
6/// Detailed error information with source location
7#[derive(Debug, Clone)]
8pub struct ErrorDetails {
9    pub message: String,
10    pub source_location: Source,
11    pub source_text: Arc<str>,
12    pub doc_start_line: usize,
13    pub suggestion: Option<String>,
14}
15
16/// Error types for the Lemma system with source location tracking
17#[derive(Debug, Clone)]
18pub enum LemmaError {
19    /// Parse error with source location
20    Parse(Box<ErrorDetails>),
21
22    /// Semantic validation error with source location
23    Semantic(Box<ErrorDetails>),
24
25    /// Runtime error during evaluation with source location
26    Runtime(Box<ErrorDetails>),
27
28    /// Engine error with source location
29    Engine(Box<ErrorDetails>),
30
31    /// Missing fact error during evaluation with source location
32    MissingFact(Box<ErrorDetails>),
33
34    /// Circular dependency error with source location and cycle information
35    CircularDependency {
36        details: Box<ErrorDetails>,
37        cycle: Vec<Source>,
38    },
39
40    /// Resource limit exceeded
41    ResourceLimitExceeded {
42        limit_name: String,
43        limit_value: String,
44        actual_value: String,
45        suggestion: String,
46    },
47
48    /// Multiple errors collected together
49    MultipleErrors(Vec<LemmaError>),
50}
51
52impl LemmaError {
53    /// Create a parse error with source information
54    pub fn parse(
55        message: impl Into<String>,
56        span: Span,
57        attribute: impl Into<String>,
58        source_text: Arc<str>,
59        doc_name: impl Into<String>,
60        doc_start_line: usize,
61        suggestion: Option<impl Into<String>>,
62    ) -> Self {
63        Self::Parse(Box::new(ErrorDetails {
64            message: message.into(),
65            source_location: Source::new(attribute, span, doc_name),
66            source_text,
67            doc_start_line,
68            suggestion: suggestion.map(Into::into),
69        }))
70    }
71
72    /// Create a parse error with suggestion
73    pub fn parse_with_suggestion(
74        message: impl Into<String>,
75        span: Span,
76        attribute: impl Into<String>,
77        source_text: Arc<str>,
78        doc_name: impl Into<String>,
79        doc_start_line: usize,
80        suggestion: impl Into<String>,
81    ) -> Self {
82        Self::parse(
83            message,
84            span,
85            attribute,
86            source_text,
87            doc_name,
88            doc_start_line,
89            Some(suggestion),
90        )
91    }
92
93    /// Create a semantic error with source information
94    pub fn semantic(
95        message: impl Into<String>,
96        span: Span,
97        attribute: impl Into<String>,
98        source_text: Arc<str>,
99        doc_name: impl Into<String>,
100        doc_start_line: usize,
101        suggestion: Option<impl Into<String>>,
102    ) -> Self {
103        Self::Semantic(Box::new(ErrorDetails {
104            message: message.into(),
105            source_location: Source::new(attribute, span, doc_name),
106            source_text,
107            doc_start_line,
108            suggestion: suggestion.map(Into::into),
109        }))
110    }
111
112    /// Create a semantic error with suggestion
113    pub fn semantic_with_suggestion(
114        message: impl Into<String>,
115        span: Span,
116        attribute: impl Into<String>,
117        source_text: Arc<str>,
118        doc_name: impl Into<String>,
119        doc_start_line: usize,
120        suggestion: impl Into<String>,
121    ) -> Self {
122        Self::semantic(
123            message,
124            span,
125            attribute,
126            source_text,
127            doc_name,
128            doc_start_line,
129            Some(suggestion),
130        )
131    }
132
133    /// Create an engine error with source information
134    pub fn engine(
135        message: impl Into<String>,
136        span: Span,
137        attribute: impl Into<String>,
138        source_text: Arc<str>,
139        doc_name: impl Into<String>,
140        doc_start_line: usize,
141        suggestion: Option<impl Into<String>>,
142    ) -> Self {
143        Self::Engine(Box::new(ErrorDetails {
144            message: message.into(),
145            source_location: Source::new(attribute, span, doc_name),
146            source_text,
147            doc_start_line,
148            suggestion: suggestion.map(Into::into),
149        }))
150    }
151
152    /// Create a missing fact error with source information
153    pub fn missing_fact(
154        fact_path: crate::FactPath,
155        span: Span,
156        attribute: impl Into<String>,
157        source_text: Arc<str>,
158        doc_name: impl Into<String>,
159        doc_start_line: usize,
160        suggestion: Option<impl Into<String>>,
161    ) -> Self {
162        Self::MissingFact(Box::new(ErrorDetails {
163            message: format!("Missing fact: {}", fact_path),
164            source_location: Source::new(attribute, span, doc_name),
165            source_text,
166            doc_start_line,
167            suggestion: suggestion.map(Into::into),
168        }))
169    }
170
171    /// Create a missing rule error with source information
172    pub fn missing_rule(
173        rule_path: crate::RulePath,
174        span: Span,
175        attribute: impl Into<String>,
176        source_text: Arc<str>,
177        doc_name: impl Into<String>,
178        doc_start_line: usize,
179        suggestion: Option<impl Into<String>>,
180    ) -> Self {
181        Self::Engine(Box::new(ErrorDetails {
182            message: format!("Missing rule: {}", rule_path),
183            source_location: Source::new(attribute, span, doc_name),
184            source_text,
185            doc_start_line,
186            suggestion: suggestion.map(Into::into),
187        }))
188    }
189
190    /// Create a missing type error with source information
191    pub fn missing_type(
192        type_name: impl Into<String>,
193        span: Span,
194        attribute: impl Into<String>,
195        source_text: Arc<str>,
196        doc_name: impl Into<String>,
197        doc_start_line: usize,
198        suggestion: Option<impl Into<String>>,
199    ) -> Self {
200        Self::Engine(Box::new(ErrorDetails {
201            message: format!("Missing type: {}", type_name.into()),
202            source_location: Source::new(attribute, span, doc_name),
203            source_text,
204            doc_start_line,
205            suggestion: suggestion.map(Into::into),
206        }))
207    }
208
209    /// Create a missing document error with source information
210    pub fn missing_doc(
211        doc_name: impl Into<String>,
212        span: Span,
213        attribute: impl Into<String>,
214        source_text: Arc<str>,
215        current_doc_name: impl Into<String>,
216        doc_start_line: usize,
217        suggestion: Option<impl Into<String>>,
218    ) -> Self {
219        Self::Engine(Box::new(ErrorDetails {
220            message: format!("Missing document: {}", doc_name.into()),
221            source_location: Source::new(attribute, span, current_doc_name),
222            source_text,
223            doc_start_line,
224            suggestion: suggestion.map(Into::into),
225        }))
226    }
227
228    /// Create a circular dependency error with source information
229    #[allow(clippy::too_many_arguments)]
230    pub fn circular_dependency(
231        message: impl Into<String>,
232        span: Span,
233        attribute: impl Into<String>,
234        source_text: Arc<str>,
235        doc_name: impl Into<String>,
236        doc_start_line: usize,
237        cycle: Vec<Source>,
238        suggestion: Option<impl Into<String>>,
239    ) -> Self {
240        Self::CircularDependency {
241            details: Box::new(ErrorDetails {
242                message: message.into(),
243                source_location: Source::new(attribute, span, doc_name),
244                source_text,
245                doc_start_line,
246                suggestion: suggestion.map(Into::into),
247            }),
248            cycle,
249        }
250    }
251}
252
253impl fmt::Display for LemmaError {
254    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
255        match self {
256            LemmaError::Parse(details) => {
257                write!(f, "Parse error: {}", details.message)?;
258                if let Some(suggestion) = &details.suggestion {
259                    write!(f, " (suggestion: {suggestion})")?;
260                }
261                write!(
262                    f,
263                    " at {}:{}:{}",
264                    details.source_location.attribute,
265                    details.source_location.span.line,
266                    details.source_location.span.col
267                )
268            }
269            LemmaError::Semantic(details) => {
270                write!(f, "Semantic error: {}", details.message)?;
271                if let Some(suggestion) = &details.suggestion {
272                    write!(f, " (suggestion: {suggestion})")?;
273                }
274                write!(
275                    f,
276                    " at {}:{}:{}",
277                    details.source_location.attribute,
278                    details.source_location.span.line,
279                    details.source_location.span.col
280                )
281            }
282            LemmaError::Runtime(details) => {
283                write!(f, "Runtime error: {}", details.message)?;
284                if let Some(suggestion) = &details.suggestion {
285                    write!(f, " (suggestion: {suggestion})")?;
286                }
287                write!(
288                    f,
289                    " at {}:{}:{}",
290                    details.source_location.attribute,
291                    details.source_location.span.line,
292                    details.source_location.span.col
293                )
294            }
295            LemmaError::Engine(details) => {
296                write!(f, "Engine error: {}", details.message)?;
297                if let Some(suggestion) = &details.suggestion {
298                    write!(f, " (suggestion: {suggestion})")?;
299                }
300                write!(
301                    f,
302                    " at {}:{}:{}",
303                    details.source_location.attribute,
304                    details.source_location.span.line,
305                    details.source_location.span.col
306                )
307            }
308            LemmaError::MissingFact(details) => {
309                write!(f, "Missing fact: {}", details.message)?;
310                if let Some(suggestion) = &details.suggestion {
311                    write!(f, " (suggestion: {suggestion})")?;
312                }
313                write!(
314                    f,
315                    " at {}:{}:{}",
316                    details.source_location.attribute,
317                    details.source_location.span.line,
318                    details.source_location.span.col
319                )
320            }
321            LemmaError::CircularDependency { details, .. } => {
322                write!(f, "Circular dependency: {}", details.message)?;
323                if let Some(suggestion) = &details.suggestion {
324                    write!(f, " (suggestion: {suggestion})")?;
325                }
326                write!(
327                    f,
328                    " at {}:{}:{}",
329                    details.source_location.attribute,
330                    details.source_location.span.line,
331                    details.source_location.span.col
332                )
333            }
334            LemmaError::ResourceLimitExceeded {
335                limit_name,
336                limit_value,
337                actual_value,
338                suggestion,
339            } => {
340                write!(
341                    f,
342                    "Resource limit exceeded: {limit_name} (limit: {limit_value}, actual: {actual_value}). {suggestion}"
343                )
344            }
345            LemmaError::MultipleErrors(errors) => {
346                writeln!(f, "Multiple errors:")?;
347                for (i, error) in errors.iter().enumerate() {
348                    write!(f, "  {}. {error}", i + 1)?;
349                    if i < errors.len() - 1 {
350                        writeln!(f)?;
351                    }
352                }
353                Ok(())
354            }
355        }
356    }
357}
358
359impl std::error::Error for LemmaError {}
360
361impl From<std::fmt::Error> for LemmaError {
362    fn from(err: std::fmt::Error) -> Self {
363        use crate::parsing::ast::Span;
364        LemmaError::engine(
365            format!("Format error: {err}"),
366            Span {
367                start: 0,
368                end: 0,
369                line: 1,
370                col: 0,
371            },
372            "<internal>",
373            Arc::from(""),
374            "<unknown>",
375            1,
376            None::<String>,
377        )
378    }
379}
380
381impl LemmaError {
382    /// Get the error message
383    pub fn message(&self) -> &str {
384        match self {
385            LemmaError::Parse(details)
386            | LemmaError::Semantic(details)
387            | LemmaError::Runtime(details)
388            | LemmaError::Engine(details)
389            | LemmaError::MissingFact(details) => &details.message,
390            LemmaError::CircularDependency { details, .. } => &details.message,
391            LemmaError::ResourceLimitExceeded { limit_name, .. } => limit_name,
392            LemmaError::MultipleErrors(_) => "Multiple errors occurred",
393        }
394    }
395
396    /// Get the source location if available
397    pub fn location(&self) -> Option<&Source> {
398        match self {
399            LemmaError::Parse(details)
400            | LemmaError::Semantic(details)
401            | LemmaError::Runtime(details)
402            | LemmaError::Engine(details)
403            | LemmaError::MissingFact(details) => Some(&details.source_location),
404            LemmaError::CircularDependency { details, .. } => Some(&details.source_location),
405            LemmaError::ResourceLimitExceeded { .. } | LemmaError::MultipleErrors(_) => None,
406        }
407    }
408
409    /// Get the source text if available
410    pub fn source_text(&self) -> Option<&str> {
411        match self {
412            LemmaError::Parse(details)
413            | LemmaError::Semantic(details)
414            | LemmaError::Runtime(details)
415            | LemmaError::Engine(details)
416            | LemmaError::MissingFact(details) => Some(&details.source_text),
417            LemmaError::CircularDependency { details, .. } => Some(&details.source_text),
418            LemmaError::ResourceLimitExceeded { .. } | LemmaError::MultipleErrors(_) => None,
419        }
420    }
421
422    /// Get the suggestion if available
423    pub fn suggestion(&self) -> Option<&str> {
424        match self {
425            LemmaError::Parse(details)
426            | LemmaError::Semantic(details)
427            | LemmaError::Runtime(details)
428            | LemmaError::Engine(details)
429            | LemmaError::MissingFact(details) => details.suggestion.as_deref(),
430            LemmaError::CircularDependency { details, .. } => details.suggestion.as_deref(),
431            LemmaError::ResourceLimitExceeded { suggestion, .. } => Some(suggestion),
432            LemmaError::MultipleErrors(_) => None,
433        }
434    }
435}
436
437#[cfg(test)]
438mod tests {
439    use super::*;
440    use crate::parsing::ast::Span;
441    use std::sync::Arc;
442
443    type ErrorVariant =
444        fn(String, Span, String, Arc<str>, String, usize, Option<String>) -> LemmaError;
445
446    #[allow(clippy::type_complexity)]
447    fn create_test_error(variant: ErrorVariant) -> LemmaError {
448        let source_text = "fact amount = 100";
449        let span = Span {
450            start: 14,
451            end: 21,
452            line: 1,
453            col: 15,
454        };
455        variant(
456            "Invalid currency".to_string(),
457            span,
458            "test.lemma".to_string(),
459            Arc::from(source_text),
460            "test_doc".to_string(),
461            1,
462            None,
463        )
464    }
465
466    #[test]
467    fn test_error_creation_and_display() {
468        let parse_error = create_test_error(LemmaError::parse);
469        let parse_error_display = format!("{parse_error}");
470        assert!(parse_error_display.contains("Parse error: Invalid currency"));
471        assert!(parse_error_display.contains("test.lemma:1:15"));
472
473        let semantic_error = create_test_error(LemmaError::semantic);
474        let semantic_error_display = format!("{semantic_error}");
475        assert!(semantic_error_display.contains("Semantic error: Invalid currency"));
476        assert!(semantic_error_display.contains("test.lemma:1:15"));
477
478        let source_text = "fact amont = 100";
479        let span = Span {
480            start: 5,
481            end: 10,
482            line: 1,
483            col: 6,
484        };
485        let parse_error_with_suggestion = LemmaError::parse_with_suggestion(
486            "Typo in fact name",
487            span.clone(),
488            "suggestion.lemma",
489            Arc::from(source_text),
490            "suggestion_doc",
491            1,
492            "Did you mean 'amount'?",
493        );
494        let parse_error_with_suggestion_display = format!("{parse_error_with_suggestion}");
495        assert!(parse_error_with_suggestion_display.contains("Typo in fact name"));
496        assert!(parse_error_with_suggestion_display.contains("Did you mean 'amount'?"));
497
498        let semantic_error_with_suggestion = LemmaError::semantic_with_suggestion(
499            "Incompatible types",
500            span.clone(),
501            "suggestion.lemma",
502            Arc::from(source_text),
503            "suggestion_doc",
504            1,
505            "Try converting one of the types.",
506        );
507        let semantic_error_with_suggestion_display = format!("{semantic_error_with_suggestion}");
508        assert!(semantic_error_with_suggestion_display.contains("Incompatible types"));
509        assert!(semantic_error_with_suggestion_display.contains("Try converting one of the types."));
510
511        let engine_error = LemmaError::engine(
512            "Something went wrong",
513            Span {
514                start: 0,
515                end: 0,
516                line: 1,
517                col: 0,
518            },
519            "<test>",
520            Arc::from(""),
521            "<test>",
522            1,
523            None::<String>,
524        );
525        assert!(format!("{engine_error}").contains("Engine error: Something went wrong"));
526
527        let circular_dependency_error = LemmaError::circular_dependency(
528            "a -> b -> a",
529            Span {
530                start: 0,
531                end: 0,
532                line: 1,
533                col: 0,
534            },
535            "<test>",
536            Arc::from(""),
537            "<test>",
538            1,
539            vec![],
540            None::<String>,
541        );
542        assert!(format!("{circular_dependency_error}").contains("Circular dependency: a -> b -> a"));
543
544        let multiple_errors =
545            LemmaError::MultipleErrors(vec![parse_error, semantic_error, engine_error]);
546        let multiple_errors_display = format!("{multiple_errors}");
547        assert!(multiple_errors_display.contains("Multiple errors:"));
548        assert!(multiple_errors_display.contains("Parse error: Invalid currency"));
549        assert!(multiple_errors_display.contains("Semantic error: Invalid currency"));
550        assert!(multiple_errors_display.contains("Engine error: Something went wrong"));
551    }
552}