Skip to main content

lemma/
error.rs

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