lemma/
error.rs

1use crate::ast::Span;
2use ariadne::{Color, Label, Report, ReportKind, Source};
3use std::fmt;
4use std::sync::Arc;
5
6/// Detailed error information with source location
7#[derive(Debug, Clone)]
8pub struct ErrorDetails {
9    pub message: String,
10    pub span: Span,
11    pub source_id: String,
12    pub source_text: Arc<str>,
13    pub doc_name: String,
14    pub doc_start_line: usize,
15    pub suggestion: Option<String>,
16}
17
18/// Error types for the Lemma system with source location tracking
19#[derive(Debug, Clone)]
20pub enum LemmaError {
21    /// Parse error with source location
22    Parse(Box<ErrorDetails>),
23
24    /// Semantic validation error with source location
25    Semantic(Box<ErrorDetails>),
26
27    /// Runtime error during evaluation with source location
28    Runtime(Box<ErrorDetails>),
29
30    /// Engine error without specific source location
31    Engine(String),
32
33    /// Circular dependency error
34    CircularDependency(String),
35
36    /// Multiple errors collected together
37    MultipleErrors(Vec<LemmaError>),
38}
39
40impl LemmaError {
41    /// Create a parse error with source information
42    pub fn parse(
43        message: impl Into<String>,
44        span: Span,
45        source_id: impl Into<String>,
46        source_text: Arc<str>,
47        doc_name: impl Into<String>,
48        doc_start_line: usize,
49    ) -> Self {
50        Self::Parse(Box::new(ErrorDetails {
51            message: message.into(),
52            span,
53            source_id: source_id.into(),
54            source_text,
55            doc_name: doc_name.into(),
56            doc_start_line,
57            suggestion: None,
58        }))
59    }
60
61    /// Create a parse error with suggestion
62    pub fn parse_with_suggestion(
63        message: impl Into<String>,
64        span: Span,
65        source_id: impl Into<String>,
66        source_text: Arc<str>,
67        doc_name: impl Into<String>,
68        doc_start_line: usize,
69        suggestion: impl Into<String>,
70    ) -> Self {
71        Self::Parse(Box::new(ErrorDetails {
72            message: message.into(),
73            span,
74            source_id: source_id.into(),
75            source_text,
76            doc_name: doc_name.into(),
77            doc_start_line,
78            suggestion: Some(suggestion.into()),
79        }))
80    }
81
82    /// Create a semantic error with source information
83    pub fn semantic(
84        message: impl Into<String>,
85        span: Span,
86        source_id: impl Into<String>,
87        source_text: Arc<str>,
88        doc_name: impl Into<String>,
89        doc_start_line: usize,
90    ) -> Self {
91        Self::Semantic(Box::new(ErrorDetails {
92            message: message.into(),
93            span,
94            source_id: source_id.into(),
95            source_text,
96            doc_name: doc_name.into(),
97            doc_start_line,
98            suggestion: None,
99        }))
100    }
101
102    /// Create a semantic error with suggestion
103    pub fn semantic_with_suggestion(
104        message: impl Into<String>,
105        span: Span,
106        source_id: impl Into<String>,
107        source_text: Arc<str>,
108        doc_name: impl Into<String>,
109        doc_start_line: usize,
110        suggestion: impl Into<String>,
111    ) -> Self {
112        Self::Semantic(Box::new(ErrorDetails {
113            message: message.into(),
114            span,
115            source_id: source_id.into(),
116            source_text,
117            doc_name: doc_name.into(),
118            doc_start_line,
119            suggestion: Some(suggestion.into()),
120        }))
121    }
122}
123
124impl fmt::Display for LemmaError {
125    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
126        match self {
127            LemmaError::Parse(details) => {
128                let context = ErrorContext {
129                    error_type: "Parse error",
130                    message: &details.message,
131                    span: &details.span,
132                    source_id: &details.source_id,
133                    source_text: &details.source_text,
134                    doc_name: &details.doc_name,
135                    doc_start_line: details.doc_start_line,
136                    suggestion: details.suggestion.as_deref(),
137                    note: None,
138                };
139                format_error_with_ariadne(f, &context)
140            }
141            LemmaError::Semantic(details) => {
142                let context = ErrorContext {
143                    error_type: "Semantic error",
144                    message: &details.message,
145                    span: &details.span,
146                    source_id: &details.source_id,
147                    source_text: &details.source_text,
148                    doc_name: &details.doc_name,
149                    doc_start_line: details.doc_start_line,
150                    suggestion: details.suggestion.as_deref(),
151                    note: None,
152                };
153                format_error_with_ariadne(f, &context)
154            }
155            LemmaError::Runtime(details) => {
156                let context = ErrorContext {
157                    error_type: "Runtime error",
158                    message: &details.message,
159                    span: &details.span,
160                    source_id: &details.source_id,
161                    source_text: &details.source_text,
162                    doc_name: &details.doc_name,
163                    doc_start_line: details.doc_start_line,
164                    suggestion: details.suggestion.as_deref(),
165                    note: None,
166                };
167                format_error_with_ariadne(f, &context)
168            }
169            LemmaError::Engine(msg) => write!(f, "Engine error: {}", msg),
170            LemmaError::CircularDependency(msg) => write!(f, "Circular dependency: {}", msg),
171            LemmaError::MultipleErrors(errors) => {
172                writeln!(f, "Multiple errors occurred:")?;
173                for error in errors {
174                    writeln!(f, "\n{}", error)?;
175                }
176                Ok(())
177            }
178        }
179    }
180}
181
182struct ErrorContext<'a> {
183    error_type: &'a str,
184    message: &'a str,
185    span: &'a Span,
186    source_id: &'a str,
187    source_text: &'a str,
188    doc_name: &'a str,
189    doc_start_line: usize,
190    suggestion: Option<&'a str>,
191    note: Option<&'a str>,
192}
193
194fn format_error_with_ariadne(f: &mut fmt::Formatter<'_>, context: &ErrorContext) -> fmt::Result {
195    let mut output = Vec::new();
196
197    let doc_line = if context.span.line >= context.doc_start_line {
198        context.span.line - context.doc_start_line + 1
199    } else {
200        context.span.line // Fallback if something is off
201    };
202
203    // Enhanced error message showing both doc and file context
204    let enhanced_message = format!(
205        "{}: {} (in doc '{}' at line {}, file {}:{})",
206        context.error_type,
207        context.message,
208        context.doc_name,
209        doc_line,
210        context.source_id,
211        context.span.line
212    );
213
214    let mut report = Report::build(ReportKind::Error, context.source_id, context.span.start)
215        .with_message(enhanced_message)
216        .with_label(
217            Label::new((context.source_id, context.span.start..context.span.end))
218                .with_message("")
219                .with_color(Color::Red),
220        );
221
222    if let Some(help) = context.suggestion {
223        report = report.with_help(help);
224    }
225
226    if let Some(note_text) = context.note {
227        report = report.with_note(note_text);
228    }
229
230    match report.finish().write(
231        (context.source_id, Source::from(context.source_text)),
232        &mut output,
233    ) {
234        Ok(_) => write!(f, "{}", String::from_utf8_lossy(&output)),
235        Err(_) => {
236            // Fallback if ariadne fails
237            write!(
238                f,
239                "{}: {} at {}:{}:{}",
240                context.error_type,
241                context.message,
242                context.source_id,
243                context.span.line,
244                context.span.col
245            )
246        }
247    }
248}
249
250impl std::error::Error for LemmaError {}
251
252impl From<std::fmt::Error> for LemmaError {
253    fn from(err: std::fmt::Error) -> Self {
254        LemmaError::Engine(format!("Format error: {}", err))
255    }
256}