Skip to main content

lemma/
error.rs

1use crate::parsing::source::Source;
2use crate::planning::semantics::{FactPath, RulePath};
3use crate::registry::RegistryErrorKind;
4use std::fmt;
5
6/// Detailed error information with optional source location.
7#[derive(Debug, Clone)]
8pub struct ErrorDetails {
9    pub message: String,
10    pub source: Option<Source>,
11    pub suggestion: Option<String>,
12}
13
14/// Error types for the Lemma system with source location tracking
15#[derive(Debug, Clone)]
16pub enum LemmaError {
17    /// Parse error with source location
18    Parse(Box<ErrorDetails>),
19
20    /// Semantic validation error with source location
21    Semantic(Box<ErrorDetails>),
22
23    /// Inversion error (valid Lemma, but unsupported by inversion) with source location
24    Inversion(Box<ErrorDetails>),
25
26    /// Runtime error during evaluation with source location
27    Runtime(Box<ErrorDetails>),
28
29    /// Engine error with source location
30    Engine(Box<ErrorDetails>),
31
32    /// Registry resolution error with source location and structured error kind.
33    ///
34    /// Produced when an `@...` reference cannot be resolved by the configured Registry
35    /// (e.g. the document was not found, the request was unauthorized, or the network
36    /// is unreachable).
37    Registry {
38        details: Box<ErrorDetails>,
39        /// The `@...` identifier that failed to resolve (without the leading `@`).
40        identifier: String,
41        /// The category of failure.
42        kind: RegistryErrorKind,
43    },
44
45    /// Missing fact error during evaluation with source location
46    MissingFact(Box<ErrorDetails>),
47
48    /// Circular dependency error with source location and cycle information
49    CircularDependency {
50        details: Box<ErrorDetails>,
51        cycle: Vec<Source>,
52    },
53
54    /// Resource limit exceeded
55    ResourceLimitExceeded {
56        limit_name: String,
57        limit_value: String,
58        actual_value: String,
59        suggestion: String,
60    },
61
62    /// Multiple errors collected together
63    MultipleErrors(Vec<LemmaError>),
64}
65
66impl LemmaError {
67    /// Create a parse error with source information
68    pub fn parse(
69        message: impl Into<String>,
70        source: Option<Source>,
71        suggestion: Option<impl Into<String>>,
72    ) -> Self {
73        Self::Parse(Box::new(ErrorDetails {
74            message: message.into(),
75            source,
76            suggestion: suggestion.map(Into::into),
77        }))
78    }
79
80    /// Create a parse error with suggestion
81    pub fn parse_with_suggestion(
82        message: impl Into<String>,
83        source: Option<Source>,
84        suggestion: impl Into<String>,
85    ) -> Self {
86        Self::parse(message, source, Some(suggestion))
87    }
88
89    /// Create a semantic error with source information
90    pub fn semantic(
91        message: impl Into<String>,
92        source: Option<Source>,
93        suggestion: Option<impl Into<String>>,
94    ) -> Self {
95        Self::Semantic(Box::new(ErrorDetails {
96            message: message.into(),
97            source,
98            suggestion: suggestion.map(Into::into),
99        }))
100    }
101
102    /// Create a semantic error with suggestion
103    pub fn semantic_with_suggestion(
104        message: impl Into<String>,
105        source: Option<Source>,
106        suggestion: impl Into<String>,
107    ) -> Self {
108        Self::semantic(message, source, Some(suggestion))
109    }
110
111    /// Create an inversion error with source information
112    pub fn inversion(
113        message: impl Into<String>,
114        source: Option<Source>,
115        suggestion: Option<impl Into<String>>,
116    ) -> Self {
117        Self::Inversion(Box::new(ErrorDetails {
118            message: message.into(),
119            source,
120            suggestion: suggestion.map(Into::into),
121        }))
122    }
123
124    /// Create an inversion error with suggestion
125    pub fn inversion_with_suggestion(
126        message: impl Into<String>,
127        source: Option<Source>,
128        suggestion: impl Into<String>,
129    ) -> Self {
130        Self::inversion(message, source, Some(suggestion))
131    }
132
133    /// Create an engine error with source information
134    pub fn engine(
135        message: impl Into<String>,
136        source: Option<Source>,
137        suggestion: Option<impl Into<String>>,
138    ) -> Self {
139        Self::Engine(Box::new(ErrorDetails {
140            message: message.into(),
141            source,
142            suggestion: suggestion.map(Into::into),
143        }))
144    }
145
146    /// Create a registry error with source information and structured error kind.
147    pub fn registry(
148        message: impl Into<String>,
149        source: Option<Source>,
150        identifier: impl Into<String>,
151        kind: RegistryErrorKind,
152        suggestion: Option<impl Into<String>>,
153    ) -> Self {
154        Self::Registry {
155            details: Box::new(ErrorDetails {
156                message: message.into(),
157                source,
158                suggestion: suggestion.map(Into::into),
159            }),
160            identifier: identifier.into(),
161            kind,
162        }
163    }
164
165    /// Create a missing fact error with source information
166    pub fn missing_fact(
167        fact_path: FactPath,
168        source: Option<Source>,
169        suggestion: Option<impl Into<String>>,
170    ) -> Self {
171        Self::MissingFact(Box::new(ErrorDetails {
172            message: format!("Missing fact: {}", fact_path),
173            source,
174            suggestion: suggestion.map(Into::into),
175        }))
176    }
177
178    /// Create a missing rule error with source information
179    pub fn missing_rule(
180        rule_path: RulePath,
181        source: Option<Source>,
182        suggestion: Option<impl Into<String>>,
183    ) -> Self {
184        Self::Engine(Box::new(ErrorDetails {
185            message: format!("Missing rule: {}", rule_path),
186            source,
187            suggestion: suggestion.map(Into::into),
188        }))
189    }
190
191    /// Create a circular dependency error with source information
192    pub fn circular_dependency(
193        message: impl Into<String>,
194        source: Option<Source>,
195        cycle: Vec<Source>,
196        suggestion: Option<impl Into<String>>,
197    ) -> Self {
198        Self::CircularDependency {
199            details: Box::new(ErrorDetails {
200                message: message.into(),
201                source,
202                suggestion: suggestion.map(Into::into),
203            }),
204            cycle,
205        }
206    }
207}
208
209fn write_source_location(f: &mut fmt::Formatter<'_>, source: &Option<Source>) -> fmt::Result {
210    if let Some(src) = source {
211        write!(
212            f,
213            " at {}:{}:{}",
214            src.attribute, src.span.line, src.span.col
215        )
216    } else {
217        Ok(())
218    }
219}
220
221impl fmt::Display for LemmaError {
222    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
223        match self {
224            LemmaError::Parse(details) => {
225                write!(f, "Parse error: {}", details.message)?;
226                if let Some(suggestion) = &details.suggestion {
227                    write!(f, " (suggestion: {suggestion})")?;
228                }
229                write_source_location(f, &details.source)
230            }
231            LemmaError::Semantic(details) => {
232                write!(f, "Semantic error: {}", details.message)?;
233                if let Some(suggestion) = &details.suggestion {
234                    write!(f, " (suggestion: {suggestion})")?;
235                }
236                write_source_location(f, &details.source)
237            }
238            LemmaError::Inversion(details) => {
239                write!(f, "Inversion error: {}", details.message)?;
240                if let Some(suggestion) = &details.suggestion {
241                    write!(f, " (suggestion: {suggestion})")?;
242                }
243                write_source_location(f, &details.source)
244            }
245            LemmaError::Runtime(details) => {
246                write!(f, "Runtime error: {}", details.message)?;
247                if let Some(suggestion) = &details.suggestion {
248                    write!(f, " (suggestion: {suggestion})")?;
249                }
250                write_source_location(f, &details.source)
251            }
252            LemmaError::Engine(details) => {
253                write!(f, "Engine error: {}", details.message)?;
254                if let Some(suggestion) = &details.suggestion {
255                    write!(f, " (suggestion: {suggestion})")?;
256                }
257                write_source_location(f, &details.source)
258            }
259            LemmaError::Registry {
260                details,
261                identifier,
262                kind,
263            } => {
264                write!(
265                    f,
266                    "Registry error ({}): @{}: {}",
267                    kind, identifier, details.message
268                )?;
269                if let Some(suggestion) = &details.suggestion {
270                    write!(f, " (suggestion: {suggestion})")?;
271                }
272                write_source_location(f, &details.source)
273            }
274            LemmaError::MissingFact(details) => {
275                write!(f, "Missing fact: {}", details.message)?;
276                if let Some(suggestion) = &details.suggestion {
277                    write!(f, " (suggestion: {suggestion})")?;
278                }
279                write_source_location(f, &details.source)
280            }
281            LemmaError::CircularDependency { details, .. } => {
282                write!(f, "Circular dependency: {}", details.message)?;
283                if let Some(suggestion) = &details.suggestion {
284                    write!(f, " (suggestion: {suggestion})")?;
285                }
286                write_source_location(f, &details.source)
287            }
288            LemmaError::ResourceLimitExceeded {
289                limit_name,
290                limit_value,
291                actual_value,
292                suggestion,
293            } => {
294                write!(
295                    f,
296                    "Resource limit exceeded: {limit_name} (limit: {limit_value}, actual: {actual_value}). {suggestion}"
297                )
298            }
299            LemmaError::MultipleErrors(errors) => {
300                writeln!(f, "Multiple errors:")?;
301                for (i, error) in errors.iter().enumerate() {
302                    write!(f, "  {}. {error}", i + 1)?;
303                    if i < errors.len() - 1 {
304                        writeln!(f)?;
305                    }
306                }
307                Ok(())
308            }
309        }
310    }
311}
312
313impl std::error::Error for LemmaError {}
314
315impl From<std::fmt::Error> for LemmaError {
316    fn from(err: std::fmt::Error) -> Self {
317        LemmaError::engine(format!("Format error: {err}"), None, None::<String>)
318    }
319}
320
321impl LemmaError {
322    /// Get the error message
323    pub fn message(&self) -> &str {
324        match self {
325            LemmaError::Parse(details)
326            | LemmaError::Semantic(details)
327            | LemmaError::Inversion(details)
328            | LemmaError::Runtime(details)
329            | LemmaError::Engine(details)
330            | LemmaError::MissingFact(details) => &details.message,
331            LemmaError::Registry { details, .. } => &details.message,
332            LemmaError::CircularDependency { details, .. } => &details.message,
333            LemmaError::ResourceLimitExceeded { limit_name, .. } => limit_name,
334            LemmaError::MultipleErrors(_) => "Multiple errors occurred",
335        }
336    }
337
338    /// Get the source location if available
339    pub fn location(&self) -> Option<&Source> {
340        match self {
341            LemmaError::Parse(details)
342            | LemmaError::Semantic(details)
343            | LemmaError::Inversion(details)
344            | LemmaError::Runtime(details)
345            | LemmaError::Engine(details)
346            | LemmaError::MissingFact(details) => details.source.as_ref(),
347            LemmaError::Registry { details, .. } => details.source.as_ref(),
348            LemmaError::CircularDependency { details, .. } => details.source.as_ref(),
349            LemmaError::ResourceLimitExceeded { .. } | LemmaError::MultipleErrors(_) => None,
350        }
351    }
352
353    /// Get the source text if available
354    pub fn source_text(&self) -> Option<&str> {
355        self.location().map(|s| &*s.source_text)
356    }
357
358    /// Get the suggestion if available
359    pub fn suggestion(&self) -> Option<&str> {
360        match self {
361            LemmaError::Parse(details)
362            | LemmaError::Semantic(details)
363            | LemmaError::Inversion(details)
364            | LemmaError::Runtime(details)
365            | LemmaError::Engine(details)
366            | LemmaError::MissingFact(details) => details.suggestion.as_deref(),
367            LemmaError::Registry { details, .. } => details.suggestion.as_deref(),
368            LemmaError::CircularDependency { details, .. } => details.suggestion.as_deref(),
369            LemmaError::ResourceLimitExceeded { suggestion, .. } => Some(suggestion),
370            LemmaError::MultipleErrors(_) => None,
371        }
372    }
373}
374
375#[cfg(test)]
376mod tests {
377    use super::*;
378    use crate::parsing::ast::Span;
379    use std::sync::Arc;
380
381    fn test_source() -> Source {
382        Source::new(
383            "test.lemma",
384            Span {
385                start: 14,
386                end: 21,
387                line: 1,
388                col: 15,
389            },
390            "test_doc",
391            Arc::from("fact amount = 100"),
392        )
393    }
394
395    #[test]
396    fn test_error_creation_and_display() {
397        let parse_error =
398            LemmaError::parse("Invalid currency", Some(test_source()), None::<String>);
399        let parse_error_display = format!("{parse_error}");
400        assert!(parse_error_display.contains("Parse error: Invalid currency"));
401        assert!(parse_error_display.contains("test.lemma:1:15"));
402
403        let semantic_error =
404            LemmaError::semantic("Invalid currency", Some(test_source()), None::<String>);
405        let semantic_error_display = format!("{semantic_error}");
406        assert!(semantic_error_display.contains("Semantic error: Invalid currency"));
407        assert!(semantic_error_display.contains("test.lemma:1:15"));
408
409        let suggestion_source = Source::new(
410            "suggestion.lemma",
411            Span {
412                start: 5,
413                end: 10,
414                line: 1,
415                col: 6,
416            },
417            "suggestion_doc",
418            Arc::from("fact amont = 100"),
419        );
420
421        let parse_error_with_suggestion = LemmaError::parse_with_suggestion(
422            "Typo in fact name",
423            Some(suggestion_source.clone()),
424            "Did you mean 'amount'?",
425        );
426        let parse_error_with_suggestion_display = format!("{parse_error_with_suggestion}");
427        assert!(parse_error_with_suggestion_display.contains("Typo in fact name"));
428        assert!(parse_error_with_suggestion_display.contains("Did you mean 'amount'?"));
429
430        let semantic_error_with_suggestion = LemmaError::semantic_with_suggestion(
431            "Incompatible types",
432            Some(suggestion_source),
433            "Try converting one of the types.",
434        );
435        let semantic_error_with_suggestion_display = format!("{semantic_error_with_suggestion}");
436        assert!(semantic_error_with_suggestion_display.contains("Incompatible types"));
437        assert!(semantic_error_with_suggestion_display.contains("Try converting one of the types."));
438
439        let engine_error = LemmaError::engine("Something went wrong", None, None::<String>);
440        assert!(format!("{engine_error}").contains("Engine error: Something went wrong"));
441        assert!(!format!("{engine_error}").contains(" at "));
442
443        let circular_dependency_error =
444            LemmaError::circular_dependency("a -> b -> a", None, vec![], None::<String>);
445        assert!(format!("{circular_dependency_error}").contains("Circular dependency: a -> b -> a"));
446
447        let multiple_errors =
448            LemmaError::MultipleErrors(vec![parse_error, semantic_error, engine_error]);
449        let multiple_errors_display = format!("{multiple_errors}");
450        assert!(multiple_errors_display.contains("Multiple errors:"));
451        assert!(multiple_errors_display.contains("Parse error: Invalid currency"));
452        assert!(multiple_errors_display.contains("Semantic error: Invalid currency"));
453        assert!(multiple_errors_display.contains("Engine error: Something went wrong"));
454    }
455}