1use crate::parsing::source::Source;
2use crate::planning::semantics::{FactPath, RulePath};
3use crate::registry::RegistryErrorKind;
4use std::fmt;
5
6#[derive(Debug, Clone)]
8pub struct ErrorDetails {
9 pub message: String,
10 pub source: Option<Source>,
11 pub suggestion: Option<String>,
12}
13
14#[derive(Debug, Clone)]
16pub enum LemmaError {
17 Parse(Box<ErrorDetails>),
19
20 Semantic(Box<ErrorDetails>),
22
23 Inversion(Box<ErrorDetails>),
25
26 Runtime(Box<ErrorDetails>),
28
29 Engine(Box<ErrorDetails>),
31
32 Registry {
38 details: Box<ErrorDetails>,
39 identifier: String,
41 kind: RegistryErrorKind,
43 },
44
45 MissingFact(Box<ErrorDetails>),
47
48 CircularDependency {
50 details: Box<ErrorDetails>,
51 cycle: Vec<Source>,
52 },
53
54 ResourceLimitExceeded {
56 limit_name: String,
57 limit_value: String,
58 actual_value: String,
59 suggestion: String,
60 },
61
62 MultipleErrors(Vec<LemmaError>),
64}
65
66impl LemmaError {
67 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 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 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 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 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 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 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 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 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 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 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 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 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 pub fn source_text(&self) -> Option<&str> {
355 self.location().map(|s| &*s.source_text)
356 }
357
358 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}