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