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