sipha_error/
error.rs

1//! Parse error types.
2
3use sipha_core::{span::Span, traits::RuleId, traits::TokenKind};
4
5use crate::expected::Expected;
6
7/// Type alias for error context stack.
8type ErrorContextVec<R> = Vec<ErrorContext<R>>;
9
10/// Context information for error reporting, tracking where an error occurred.
11#[derive(Clone, Debug)]
12pub struct ErrorContext<R: RuleId> {
13    /// Rule ID where the error occurred.
14    pub rule_id: R,
15    /// Span where the error occurred.
16    pub span: Span,
17    /// Contextual message describing what was being parsed.
18    pub message: String,
19}
20
21/// Parse error emitted by the grammar and Pratt parser.
22#[derive(Clone, Debug)]
23pub enum ParseError<K: TokenKind, R: RuleId = ()> {
24    /// Unexpected token encountered.
25    UnexpectedToken {
26        kind: K,
27        span: Span,
28        rule_context: Option<R>,
29        #[allow(clippy::type_complexity)]
30        context_stack: ErrorContextVec<R>,
31    },
32    /// Specific token kind was expected.
33    Expected {
34        expected: Expected<K>,
35        found: Option<K>,
36        span: Span,
37        rule_context: Option<R>,
38        #[allow(clippy::type_complexity)]
39        context_stack: ErrorContextVec<R>,
40    },
41    /// Tried to look up a Pratt rule that does not exist.
42    NoRule {
43        kind: K,
44        span: Span,
45        rule_context: Option<R>,
46        #[allow(clippy::type_complexity)]
47        context_stack: ErrorContextVec<R>,
48    },
49    /// Operator declared as non-associative but used twice in a row.
50    NonAssociative {
51        kind: K,
52        span: Span,
53        rule_context: Option<R>,
54        #[allow(clippy::type_complexity)]
55        context_stack: ErrorContextVec<R>,
56    },
57    /// Conditional rule evaluated to false.
58    InvalidContext {
59        rule: &'static str,
60        span: Span,
61        rule_context: Option<R>,
62        #[allow(clippy::type_complexity)]
63        context_stack: ErrorContextVec<R>,
64    },
65    /// Ran out of tokens while parsing.
66    UnexpectedEof {
67        span: Span,
68        rule_context: Option<R>,
69        #[allow(clippy::type_complexity)]
70        context_stack: ErrorContextVec<R>,
71    },
72    /// Repetition exceeded its declared bounds.
73    RangeError {
74        min: usize,
75        max: usize,
76        found: usize,
77        span: Span,
78        rule_context: Option<R>,
79        #[allow(clippy::type_complexity)]
80        context_stack: ErrorContextVec<R>,
81    },
82    /// Multiple alternatives failed in a choice rule.
83    MultipleAlternatives {
84        #[allow(clippy::type_complexity)]
85        alternatives: Vec<ParseError<K, R>>,
86        span: Span,
87        rule_context: Option<R>,
88        #[allow(clippy::type_complexity)]
89        context_stack: ErrorContextVec<R>,
90    },
91}
92
93impl<K: TokenKind, R: RuleId> ParseError<K, R> {
94    /// Convert the parse error into a lightweight diagnostic.
95    ///
96    /// Requires the `diagnostics` feature to be enabled.
97    #[cfg(feature = "diagnostics")]
98    #[must_use]
99    pub fn to_diagnostic(&self) -> crate::diagnostic::Diagnostic {
100        use crate::diagnostic::{Diagnostic, Level};
101
102        match self {
103            ParseError::UnexpectedToken {
104                kind,
105                span,
106                rule_context,
107                context_stack,
108            } => {
109                let message =
110                    Self::add_rule_context(format!("Unexpected token {:?}", kind), *rule_context);
111                let hints = vec![
112                    "Check if you're missing a token before this position".to_string(),
113                    "Verify the syntax matches the expected grammar".to_string(),
114                ];
115
116                Diagnostic {
117                    level: Level::Error,
118                    message,
119                    spans: vec![span.clone()],
120                    notes: Self::build_notes_from_context(context_stack),
121                    hints,
122                    suggestions: vec![],
123                }
124            }
125            ParseError::Expected {
126                expected,
127                found,
128                span,
129                rule_context,
130                context_stack,
131            } => {
132                let expected_str = match expected {
133                    Expected::Single(k) => format!("{:?}", k),
134                    Expected::Set(ks) => ks
135                        .iter()
136                        .map(|k| format!("{:?}", k))
137                        .collect::<Vec<_>>()
138                        .join(", "),
139                    Expected::Dynamic(v) => v
140                        .iter()
141                        .map(|k| format!("{:?}", k))
142                        .collect::<Vec<_>>()
143                        .join(", "),
144                    Expected::Multiple(alternatives) => {
145                        let mut parts = Vec::new();
146                        for alt in alternatives {
147                            match alt {
148                                Expected::Single(k) => parts.push(format!("{:?}", k)),
149                                Expected::Set(ks) => {
150                                    parts.extend(ks.iter().map(|k| format!("{:?}", k)));
151                                }
152                                Expected::Dynamic(v) => {
153                                    parts.extend(v.iter().map(|k| format!("{:?}", k)));
154                                }
155                                Expected::Multiple(_) => {
156                                    parts.push("...".to_string());
157                                }
158                            }
159                        }
160                        parts.join(", ")
161                    }
162                };
163                let message = Self::add_rule_context(
164                    format!("Expected {}, found {:?}", expected_str, found),
165                    *rule_context,
166                );
167                let mut suggestions = Vec::new();
168
169                if let Some(found_token) = found {
170                    if let Expected::Single(expected_token) = expected {
171                        let found_str = format!("{:?}", found_token);
172                        let expected_str = format!("{:?}", expected_token);
173
174                        if Self::simple_similarity(&found_str, &expected_str) > 0.7 {
175                            suggestions.push(format!("Did you mean `{}`?", expected_str));
176                        }
177                    } else if let Expected::Set(expected_tokens) = expected {
178                        let found_str = format!("{:?}", found_token);
179                        let mut best_match: Option<String> = None;
180                        let mut best_score = 0.0;
181
182                        for expected_token in expected_tokens.iter() {
183                            let expected_str = format!("{:?}", expected_token);
184                            let score = Self::simple_similarity(&found_str, &expected_str);
185                            if score > best_score && score > 0.7 {
186                                best_score = score;
187                                best_match = Some(expected_str);
188                            }
189                        }
190
191                        if let Some(matched) = best_match {
192                            suggestions.push(format!("Did you mean `{}`?", matched));
193                        }
194                    }
195                }
196
197                Diagnostic {
198                    level: Level::Error,
199                    message,
200                    spans: vec![span.clone()],
201                    notes: Self::build_notes_from_context(context_stack),
202                    hints: vec![],
203                    suggestions,
204                }
205            }
206            ParseError::MultipleAlternatives {
207                alternatives,
208                span,
209                rule_context,
210                context_stack,
211            } => {
212                let mut messages = Vec::new();
213                let mut all_expected = Vec::new();
214
215                for err in alternatives {
216                    #[cfg(feature = "diagnostics")]
217                    {
218                        let diag = err.to_diagnostic();
219                        messages.push(diag.message);
220                    }
221                    if let ParseError::Expected { expected, .. } = err {
222                        match expected {
223                            Expected::Single(k) => all_expected.push(format!("{:?}", k)),
224                            Expected::Set(ks) => {
225                                all_expected.extend(ks.iter().map(|k| format!("{:?}", k)));
226                            }
227                            Expected::Dynamic(v) => {
228                                all_expected.extend(v.iter().map(|k| format!("{:?}", k)));
229                            }
230                            Expected::Multiple(_) => {}
231                        }
232                    }
233                }
234
235                let expected_str = if all_expected.is_empty() {
236                    "one of the alternatives".to_string()
237                } else {
238                    format!("one of: {}", all_expected.join(", "))
239                };
240
241                let message = Self::add_rule_context(
242                    format!("Expected {}, but all alternatives failed", expected_str),
243                    *rule_context,
244                );
245
246                let mut all_notes = messages;
247                all_notes.extend(context_stack.iter().rev().map(|ctx| ctx.message.clone()));
248
249                Diagnostic {
250                    level: Level::Error,
251                    message,
252                    spans: vec![span.clone()],
253                    notes: all_notes,
254                    hints: vec![],
255                    suggestions: vec![],
256                }
257            }
258            ParseError::NoRule {
259                kind,
260                span,
261                rule_context,
262                context_stack,
263            } => {
264                let message = Self::add_rule_context(
265                    format!("No Pratt rule registered for {:?}", kind),
266                    *rule_context,
267                );
268                Diagnostic {
269                    level: Level::Error,
270                    message,
271                    spans: vec![span.clone()],
272                    notes: Self::build_notes_from_context(context_stack),
273                    hints: vec![],
274                    suggestions: vec![],
275                }
276            }
277            ParseError::NonAssociative {
278                kind,
279                span,
280                rule_context,
281                context_stack,
282            } => {
283                let message = Self::add_rule_context(
284                    format!("Operator {:?} is non-associative", kind),
285                    *rule_context,
286                );
287                Diagnostic {
288                    level: Level::Error,
289                    message,
290                    spans: vec![span.clone()],
291                    notes: Self::build_notes_from_context(context_stack),
292                    hints: vec!["Non-associative operators cannot be chained".to_string()],
293                    suggestions: vec!["Add parentheses to clarify precedence".to_string()],
294                }
295            }
296            ParseError::InvalidContext {
297                rule,
298                span,
299                rule_context,
300                context_stack,
301            } => {
302                let message = Self::add_rule_context(
303                    format!("Rule {} is disabled in this context", rule),
304                    *rule_context,
305                );
306                Diagnostic {
307                    level: Level::Error,
308                    message,
309                    spans: vec![span.clone()],
310                    notes: Self::build_notes_from_context(context_stack),
311                    hints: vec![],
312                    suggestions: vec![],
313                }
314            }
315            ParseError::UnexpectedEof {
316                span,
317                rule_context,
318                context_stack,
319            } => {
320                let message =
321                    Self::add_rule_context("Unexpected end of input".to_string(), *rule_context);
322                let hints = vec![
323                    "The input ended before the parser could complete".to_string(),
324                    "Check if you're missing closing brackets, parentheses, or other delimiters"
325                        .to_string(),
326                ];
327
328                let suggestions = vec!["Add the missing tokens to complete the parse".to_string()];
329
330                Diagnostic {
331                    level: Level::Error,
332                    message,
333                    spans: vec![span.clone()],
334                    notes: Self::build_notes_from_context(context_stack),
335                    hints,
336                    suggestions,
337                }
338            }
339            ParseError::RangeError {
340                min,
341                max,
342                found,
343                span,
344                rule_context,
345                context_stack,
346            } => {
347                let message = Self::add_rule_context(
348                    format!(
349                        "Expected between {} and {} repetitions, found {}",
350                        min, max, found
351                    ),
352                    *rule_context,
353                );
354                Diagnostic {
355                    level: Level::Error,
356                    message,
357                    spans: vec![span.clone()],
358                    notes: Self::build_notes_from_context(context_stack),
359                    hints: vec![],
360                    suggestions: vec![],
361                }
362            }
363        }
364    }
365
366    /// Build notes from a context stack.
367    #[allow(dead_code)]
368    fn build_notes_from_context(context_stack: &[ErrorContext<R>]) -> Vec<String> {
369        context_stack
370            .iter()
371            .rev()
372            .map(|ctx| ctx.message.clone())
373            .collect()
374    }
375
376    /// Add rule context to a message if a rule is present.
377    #[allow(dead_code)]
378    fn add_rule_context(message: String, rule_context: Option<R>) -> String {
379        if let Some(rule) = rule_context {
380            format!("While parsing {:?}, {}", rule, message)
381        } else {
382            message
383        }
384    }
385
386    /// Simple similarity metric between two strings (Levenshtein distance normalized).
387    /// Returns a value between 0.0 and 1.0, where 1.0 is identical.
388    #[cfg(feature = "diagnostics")]
389    fn simple_similarity(s1: &str, s2: &str) -> f64 {
390        if s1 == s2 {
391            return 1.0;
392        }
393        if s1.is_empty() || s2.is_empty() {
394            return 0.0;
395        }
396
397        let s1_lower = s1.to_lowercase();
398        let s2_lower = s2.to_lowercase();
399
400        if s1_lower == s2_lower {
401            return 0.95;
402        }
403
404        if s1_lower.starts_with(&s2_lower) || s2_lower.starts_with(&s1_lower) {
405            return 0.8;
406        }
407
408        if s1_lower.contains(&s2_lower) || s2_lower.contains(&s1_lower) {
409            return 0.7;
410        }
411
412        let distance = Self::levenshtein_distance(&s1_lower, &s2_lower);
413        let max_len = s1.len().max(s2.len());
414        if max_len == 0 {
415            return 0.0;
416        }
417        1.0 - (distance as f64 / max_len as f64)
418    }
419
420    /// Calculate Levenshtein distance between two strings.
421    #[cfg(feature = "diagnostics")]
422    fn levenshtein_distance(s1: &str, s2: &str) -> usize {
423        let s1_chars: Vec<char> = s1.chars().collect();
424        let s2_chars: Vec<char> = s2.chars().collect();
425        let s1_len = s1_chars.len();
426        let s2_len = s2_chars.len();
427
428        if s1_len == 0 {
429            return s2_len;
430        }
431        if s2_len == 0 {
432            return s1_len;
433        }
434
435        let mut matrix = vec![vec![0; s2_len + 1]; s1_len + 1];
436
437        for (i, row) in matrix.iter_mut().enumerate().take(s1_len + 1) {
438            row[0] = i;
439        }
440        for (j, val) in matrix[0].iter_mut().enumerate().take(s2_len + 1) {
441            *val = j;
442        }
443
444        for i in 1..=s1_len {
445            for j in 1..=s2_len {
446                let cost = if s1_chars[i - 1] == s2_chars[j - 1] {
447                    0
448                } else {
449                    1
450                };
451                matrix[i][j] = (matrix[i - 1][j] + 1)
452                    .min(matrix[i][j - 1] + 1)
453                    .min(matrix[i - 1][j - 1] + cost);
454            }
455        }
456
457        matrix[s1_len][s2_len]
458    }
459}