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