Skip to main content

abnf_oracle/
error.rs

1//! The five error types, kept deliberately separate (SCOPE.md 6.6).
2//!
3//! * [`ParseError`] — the grammar text is not valid ABNF.
4//! * [`CheckError`] — structural: the grammar cannot be used at all.
5//! * [`LintWarning`] — advisory only; never fails `check`.
6//! * [`MatchError`] / [`GenError`] — a *start rule* cannot be used, or a resource limit was hit.
7//!
8//! The middle category is why a limit is never reported as a rejection: "does not match" and
9//! "could not decide" are different answers, and an oracle that conflates them is worthless
10//! (D14, D16, D29).
11//!
12//! All five are `#[non_exhaustive]`: the v2 candidates in SCOPE.md 14 will add variants, and
13//! that should not be a breaking change.
14
15use core::fmt;
16
17use crate::ast::Span;
18
19/// The grammar text could not be parsed.
20#[derive(Clone, Debug, PartialEq, Eq)]
21#[non_exhaustive]
22pub enum ParseError {
23    /// A non-ASCII byte appeared in the grammar text.
24    ///
25    /// Checked before tokenization. RFC 5234 restricts comments to `WSP / VCHAR` and prose to
26    /// `%x20-3D / %x3F-7E`, both ASCII, and those are the only places a non-ASCII byte could
27    /// otherwise have been mistaken for content (SCOPE.md 4.2, D35).
28    NonAscii {
29        /// Where the offending byte is.
30        span: Span,
31    },
32    /// A bare LF or CR appeared while `strict_crlf` was set.
33    ExpectedCrlf {
34        /// Where the line ending is.
35        span: Span,
36    },
37    /// A numeric literal or repetition bound does not fit in 64 bits (D17).
38    NumberTooLarge {
39        /// Where the literal is.
40        span: Span,
41    },
42    /// Something else was required here.
43    Expected {
44        /// What the parser required, e.g. `"="` or `"rule name"`.
45        what: &'static str,
46        /// Where it was required.
47        span: Span,
48    },
49    /// The grammar text ended in the middle of a construct.
50    UnexpectedEof {
51        /// The end of the input.
52        span: Span,
53    },
54}
55
56impl ParseError {
57    /// Where in the source text the error is.
58    #[must_use]
59    pub fn span(&self) -> Span {
60        match self {
61            Self::NonAscii { span }
62            | Self::ExpectedCrlf { span }
63            | Self::NumberTooLarge { span }
64            | Self::Expected { span, .. }
65            | Self::UnexpectedEof { span } => *span,
66        }
67    }
68}
69
70impl fmt::Display for ParseError {
71    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
72        match self {
73            Self::NonAscii { .. } => f.write_str(
74                "non-ASCII byte in grammar text; ABNF comments and prose values are ASCII-only",
75            ),
76            Self::ExpectedCrlf { .. } => f.write_str("expected CRLF line ending"),
77            Self::NumberTooLarge { .. } => f.write_str("numeric value does not fit in 64 bits"),
78            Self::Expected { what, .. } => write!(f, "expected {what}"),
79            Self::UnexpectedEof { .. } => f.write_str("unexpected end of grammar text"),
80        }
81    }
82}
83
84impl ParseError {
85    /// Renders the error against the grammar text it came from.
86    ///
87    /// Grammar authors are the ones who fix these, so the location matters more than the
88    /// wording (SCOPE.md 3).
89    #[must_use]
90    pub fn render(&self, src: &str) -> String {
91        render_at(src, self.span(), &self.to_string())
92    }
93}
94
95impl std::error::Error for ParseError {}
96
97/// A structural problem: the grammar cannot be used at all.
98#[derive(Clone, Debug, PartialEq, Eq)]
99#[non_exhaustive]
100pub enum CheckError {
101    /// A rule was defined twice with `=`.
102    DuplicateDefinition {
103        /// The rule name.
104        name: String,
105        /// Where the second definition is.
106        span: Span,
107    },
108    /// `name =/ …` with no prior `name = …`.
109    IncrementalWithoutBase {
110        /// The rule name.
111        name: String,
112        /// Whether the name is one of the implicit core rules (D34).
113        shadows_core: bool,
114        /// Where the incremental definition is.
115        span: Span,
116    },
117    /// A rule reference names no user rule and no core rule.
118    UndefinedRule {
119        /// The referenced name.
120        name: String,
121        /// Where the reference is.
122        span: Span,
123    },
124    /// A repetition whose lower bound exceeds its upper bound (D18).
125    InvalidRepeatRange {
126        /// Lower bound.
127        min: u64,
128        /// Upper bound.
129        max: u64,
130        /// Where the repetition is.
131        span: Span,
132    },
133    /// A numeric range whose lower endpoint exceeds its upper endpoint (D18).
134    InvalidNumericRange {
135        /// Lower endpoint.
136        lo: u64,
137        /// Upper endpoint.
138        hi: u64,
139        /// Where the range is.
140        span: Span,
141    },
142    /// A cycle in the first-graph: the recognizer could not terminate on it.
143    ///
144    /// Global, not per start rule — an unreachable left-recursive rule still fails `check`.
145    /// Left recursion is always a grammar bug for an ABNF recognizer, unlike prose values and
146    /// unrepresentable terminals, which are legitimate ABNF this crate does not support
147    /// (SCOPE.md 6.4).
148    LeftRecursion {
149        /// The cycle, in definition order.
150        cycle: Vec<String>,
151    },
152}
153
154impl fmt::Display for CheckError {
155    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
156        match self {
157            Self::DuplicateDefinition { name, .. } => {
158                write!(f, "rule `{name}` is defined more than once")
159            }
160            Self::IncrementalWithoutBase {
161                name,
162                shadows_core: false,
163                ..
164            } => {
165                write!(f, "`{name} =/ …` has no prior `{name} = …`")
166            }
167            Self::IncrementalWithoutBase {
168                name,
169                shadows_core: true,
170                ..
171            } => write!(
172                f,
173                "`{name} =/ …` has no prior `{name} = …`; `{name}` is an implicit core rule and \
174                 cannot be extended in place — write `{name} = <core definition> / …` to shadow \
175                 and extend it"
176            ),
177            Self::UndefinedRule { name, .. } => write!(f, "undefined rule `{name}`"),
178            Self::InvalidRepeatRange { min, max, .. } => {
179                write!(f, "repetition lower bound {min} exceeds upper bound {max}")
180            }
181            Self::InvalidNumericRange { lo, hi, .. } => {
182                write!(
183                    f,
184                    "numeric range starts at {lo:#X} and ends below it at {hi:#X}"
185                )
186            }
187            Self::LeftRecursion { cycle } => {
188                write!(f, "left recursion: {}", cycle.join(" -> "))
189            }
190        }
191    }
192}
193
194impl CheckError {
195    /// Where in the source this error is, if it has a single location.
196    ///
197    /// `LeftRecursion` has none: a cycle is a property of several rules at once, and pointing
198    /// at any one of them would be arbitrary.
199    #[must_use]
200    pub fn span(&self) -> Option<Span> {
201        match self {
202            Self::DuplicateDefinition { span, .. }
203            | Self::IncrementalWithoutBase { span, .. }
204            | Self::UndefinedRule { span, .. }
205            | Self::InvalidRepeatRange { span, .. }
206            | Self::InvalidNumericRange { span, .. } => Some(*span),
207            Self::LeftRecursion { .. } => None,
208        }
209    }
210
211    /// Renders the error against the grammar text it came from.
212    #[must_use]
213    pub fn render(&self, src: &str) -> String {
214        match self.span() {
215            Some(span) => render_at(src, span, &self.to_string()),
216            None => self.to_string(),
217        }
218    }
219}
220
221impl std::error::Error for CheckError {}
222
223/// Every structural problem found by [`crate::Grammar::check`], in the order found.
224///
225/// A newtype rather than a bare `Vec<CheckError>` so that it can implement [`std::error::Error`]
226/// — `Vec` is a foreign type, so it never could, and callers were forced to handle this one
227/// error specially instead of writing `?` like everywhere else.
228///
229/// It behaves like a slice of [`CheckError`] for every purpose except that:
230///
231/// ```
232/// use abnf_oracle::Grammar;
233///
234/// fn load(source: &str) -> Result<(), Box<dyn std::error::Error>> {
235///     let grammar = Grammar::parse(source)?.check()?;
236///     let _ = grammar;
237///     Ok(())
238/// }
239///
240/// // Four undefined references are four errors, not the first one four times.
241/// let broken = "start = a b c d\r\n";
242/// let errors = Grammar::parse(broken).unwrap().check().unwrap_err();
243/// assert_eq!(errors.len(), 4);
244/// for error in &errors {
245///     assert!(error.span().is_some());
246/// }
247/// ```
248#[derive(Clone, Debug, PartialEq, Eq)]
249pub struct CheckErrors(Vec<CheckError>);
250
251impl CheckErrors {
252    /// The errors, as a slice.
253    #[must_use]
254    pub fn as_slice(&self) -> &[CheckError] {
255        &self.0
256    }
257
258    /// Takes the errors out.
259    #[must_use]
260    pub fn into_vec(self) -> Vec<CheckError> {
261        self.0
262    }
263
264    /// Renders every error against the grammar text, one per line, with a closing count.
265    ///
266    /// This is what a grammar author wants to read: all of them at once, so a grammar with four
267    /// undefined references takes one round of fixing rather than four.
268    #[must_use]
269    pub fn render(&self, src: &str) -> String {
270        let mut out = String::new();
271        for error in &self.0 {
272            out.push_str(&error.render(src));
273            out.push('\n');
274        }
275        let count = self.0.len();
276        out.push_str(&format!(
277            "{count} structural {}",
278            if count == 1 { "error" } else { "errors" }
279        ));
280        out
281    }
282}
283
284impl core::ops::Deref for CheckErrors {
285    type Target = [CheckError];
286
287    fn deref(&self) -> &Self::Target {
288        &self.0
289    }
290}
291
292impl From<Vec<CheckError>> for CheckErrors {
293    fn from(errors: Vec<CheckError>) -> Self {
294        Self(errors)
295    }
296}
297
298impl From<CheckErrors> for Vec<CheckError> {
299    fn from(errors: CheckErrors) -> Self {
300        errors.0
301    }
302}
303
304impl IntoIterator for CheckErrors {
305    type Item = CheckError;
306    type IntoIter = std::vec::IntoIter<CheckError>;
307
308    fn into_iter(self) -> Self::IntoIter {
309        self.0.into_iter()
310    }
311}
312
313impl<'a> IntoIterator for &'a CheckErrors {
314    type Item = &'a CheckError;
315    type IntoIter = core::slice::Iter<'a, CheckError>;
316
317    fn into_iter(self) -> Self::IntoIter {
318        self.0.iter()
319    }
320}
321
322impl fmt::Display for CheckErrors {
323    /// One error reads as itself; several are counted and then listed.
324    ///
325    /// No source text is available here, so there are no carets — [`CheckErrors::render`] is
326    /// the one to use when the grammar text is at hand.
327    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
328        match self.0.as_slice() {
329            [] => f.write_str("no structural errors"),
330            [only] => write!(f, "{only}"),
331            many => {
332                write!(f, "{} structural errors:", many.len())?;
333                for error in many {
334                    write!(f, "\n  {error}")?;
335                }
336                Ok(())
337            }
338        }
339    }
340}
341
342impl std::error::Error for CheckErrors {}
343
344/// An advisory warning. Never fails `check` (D5).
345#[derive(Clone, Debug, PartialEq, Eq)]
346#[non_exhaustive]
347pub enum LintWarning {
348    /// No rule body mentions this rule.
349    ///
350    /// Textual, and deliberately so: a name written inside a repetition that can never run is
351    /// still written. That case is [`LintWarning::UnreachableRule`], which asks the other
352    /// question, and a grammar can trip one without the other (D47).
353    UnreferencedRule {
354        /// The rule name.
355        name: String,
356    },
357    /// Not reachable from the start rules given to `lint_from`.
358    ///
359    /// Semantic: reachability stops where the recognizer and generator stop, so a `max == 0`
360    /// repetition hides whatever it contains (D36). Contrast
361    /// [`LintWarning::UnreferencedRule`], which only asks whether the name appears.
362    UnreachableRule {
363        /// The rule name.
364        name: String,
365    },
366    /// The rule matches nothing: its shortest match is infinite.
367    UnproductiveRule {
368        /// The rule name.
369        name: String,
370    },
371    /// One branch of an alternation matches nothing, inside an otherwise productive rule.
372    UnproductiveAlternative {
373        /// The rule the alternation is in.
374        rule: String,
375        /// The branch index within the alternation.
376        branch: u32,
377    },
378    /// An explicit definition shadows an implicit core rule.
379    ///
380    /// Never an error: RFC 3986 and RFC 9110 restate core rules verbatim (SCOPE.md 4.1, D6).
381    ShadowsCoreRule {
382        /// The rule name.
383        name: String,
384    },
385}
386
387impl fmt::Display for LintWarning {
388    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
389        match self {
390            Self::UnreferencedRule { name } => {
391                write!(f, "rule `{name}` is never referenced")
392            }
393            Self::UnreachableRule { name } => {
394                write!(
395                    f,
396                    "rule `{name}` is not reachable from the given start rules"
397                )
398            }
399            Self::UnproductiveRule { name } => {
400                write!(
401                    f,
402                    "rule `{name}` can never match: it has no finite expansion"
403                )
404            }
405            Self::UnproductiveAlternative { rule, branch } => write!(
406                f,
407                "branch {branch} of an alternation in rule `{rule}` can never match"
408            ),
409            Self::ShadowsCoreRule { name } => {
410                write!(f, "rule `{name}` shadows the core rule of the same name")
411            }
412        }
413    }
414}
415
416/// Recognition could not be attempted, or could not be finished.
417///
418/// Never returned to mean "the input does not match" — that is `Ok(false)`.
419#[derive(Clone, Debug, PartialEq, Eq)]
420#[non_exhaustive]
421pub enum MatchError {
422    /// The start rule can reach a prose value, which has no matching semantics (SCOPE.md 6.5).
423    ProseValueReachable {
424        /// The start rule.
425        rule: String,
426        /// The prose text reached.
427        prose: String,
428    },
429    /// The start rule can reach a terminal that no Unicode scalar value can match (SCOPE.md 6.1).
430    UnrepresentableTerminal {
431        /// The start rule.
432        rule: String,
433        /// The terminal, in canonical spelling.
434        terminal: String,
435    },
436    /// No such rule in the grammar or the core environment.
437    UnknownRule(
438        /// The requested name.
439        String,
440    ),
441    /// The in-progress guard fired. Unreachable after `check`, which rejects left recursion
442    /// outright; kept as belt and braces (SCOPE.md 6.2).
443    LeftRecursionDetected {
444        /// The rule the guard fired on.
445        rule: String,
446    },
447    /// `MatchOptions::max_steps` was exceeded. Never a silent reject (D16).
448    StepLimit,
449    /// `MatchOptions::max_depth` was exceeded.
450    ///
451    /// The recognizer descends recursively, so nesting in the *input* becomes depth on the
452    /// call stack. Without this limit a deeply nested input overflows the stack, and a stack
453    /// overflow aborts the process — it cannot be caught and reported as "could not decide",
454    /// which is what every other limit here does.
455    DepthLimit,
456}
457
458impl fmt::Display for MatchError {
459    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
460        match self {
461            Self::ProseValueReachable { rule, prose } => write!(
462                f,
463                "rule `{rule}` can reach the prose value <{prose}>, which has no matching semantics"
464            ),
465            Self::UnrepresentableTerminal { rule, terminal } => write!(
466                f,
467                "rule `{rule}` can reach terminal {terminal}, which matches no Unicode scalar value"
468            ),
469            Self::UnknownRule(name) => write!(f, "unknown rule `{name}`"),
470            Self::LeftRecursionDetected { rule } => {
471                write!(f, "left recursion detected at rule `{rule}`")
472            }
473            Self::StepLimit => f.write_str("step limit exceeded"),
474            Self::DepthLimit => f.write_str(
475                "recursion depth limit exceeded: the input nests deeper than `MatchOptions::max_depth` allows",
476            ),
477        }
478    }
479}
480
481impl std::error::Error for MatchError {}
482
483/// Generation could not be attempted, or could not be finished.
484#[derive(Clone, Debug, PartialEq, Eq)]
485#[non_exhaustive]
486pub enum GenError {
487    /// The start rule can reach a prose value, which cannot be generated (SCOPE.md 6.5).
488    ProseValueReachable {
489        /// The start rule.
490        rule: String,
491        /// The prose text reached.
492        prose: String,
493    },
494    /// The start rule can reach a terminal with no representable scalar value (SCOPE.md 6.1).
495    UnrepresentableTerminal {
496        /// The start rule.
497        rule: String,
498        /// The terminal, in canonical spelling.
499        terminal: String,
500    },
501    /// No such rule in the grammar or the core environment.
502    UnknownRule(
503        /// The requested name.
504        String,
505    ),
506    /// The start rule is unproductive: it has no finite expansion (D9).
507    NoFiniteExpansion {
508        /// The start rule.
509        rule: String,
510    },
511    /// `GenOptions::max_output_len` was exceeded.
512    ///
513    /// Mandatory work is not bounded by depth — `start = 1000000000*"a"` is productive and has
514    /// no shorter expansion — so this bound, not termination, is what makes generation
515    /// practical (SCOPE.md 6.8, D29).
516    OutputLimit,
517    /// `GenOptions::max_steps` was exceeded.
518    StepLimit,
519}
520
521impl fmt::Display for GenError {
522    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
523        match self {
524            Self::ProseValueReachable { rule, prose } => write!(
525                f,
526                "rule `{rule}` can reach the prose value <{prose}>, which cannot be generated"
527            ),
528            Self::UnrepresentableTerminal { rule, terminal } => write!(
529                f,
530                "rule `{rule}` can reach terminal {terminal}, which has no representable value"
531            ),
532            Self::UnknownRule(name) => write!(f, "unknown rule `{name}`"),
533            Self::NoFiniteExpansion { rule } => {
534                write!(f, "rule `{rule}` has no finite expansion")
535            }
536            Self::OutputLimit => f.write_str("output length limit exceeded"),
537            Self::StepLimit => f.write_str("step limit exceeded"),
538        }
539    }
540}
541
542impl std::error::Error for GenError {}
543
544/// Renders `message` against the source, with a caret under `span`.
545///
546/// Grammar text is ASCII (D35), so byte offsets are column numbers. The exception is
547/// [`ParseError::NonAscii`], whose whole point is a byte that is not — there the column counts
548/// bytes, which is still where the reader should look.
549fn render_at(src: &str, span: Span, message: &str) -> String {
550    let offset = (span.start as usize).min(src.len());
551    let before = &src[..offset];
552    let line_number = before.matches('\n').count() + 1;
553    let line_start = before.rfind('\n').map_or(0, |index| index + 1);
554    // `lines` strips the trailing CR, so CRLF sources do not print a stray carriage return.
555    let line = src[line_start..].lines().next().unwrap_or_default();
556    let column = offset - line_start;
557    // A span can cover a whole definition, line terminator included, so clamp the caret to
558    // what is actually on this line rather than running past its end.
559    let width = (span.end.saturating_sub(span.start) as usize)
560        .min(line.len().saturating_sub(column))
561        .max(1);
562    format!(
563        "line {line_number}, column {}: {message}\n  {line}\n  {}{}",
564        column + 1,
565        " ".repeat(column),
566        "^".repeat(width)
567    )
568}
569
570#[cfg(test)]
571mod tests {
572    use super::*;
573
574    #[test]
575    fn incremental_without_base_suggests_the_workaround_for_core_rules() {
576        let plain = CheckError::IncrementalWithoutBase {
577            name: "foo".into(),
578            shadows_core: false,
579            span: Span::default(),
580        };
581        let core = CheckError::IncrementalWithoutBase {
582            name: "DIGIT".into(),
583            shadows_core: true,
584            span: Span::default(),
585        };
586        assert!(!plain.to_string().contains("shadow"));
587        assert!(core.to_string().contains("DIGIT = <core definition> / …"));
588    }
589
590    #[test]
591    fn parse_error_span_is_reported_for_every_variant() {
592        let span = Span::new(3, 7);
593        for error in [
594            ParseError::NonAscii { span },
595            ParseError::ExpectedCrlf { span },
596            ParseError::NumberTooLarge { span },
597            ParseError::Expected { what: "=", span },
598            ParseError::UnexpectedEof { span },
599        ] {
600            assert_eq!(error.span(), span);
601            assert!(!error.to_string().is_empty());
602        }
603    }
604
605    // -- CheckErrors ---------------------------------------------------------------------
606
607    fn check_errors(src: &str) -> CheckErrors {
608        crate::ast::Grammar::parse(src)
609            .expect("parses")
610            .check()
611            .expect_err("should not check")
612    }
613
614    #[test]
615    fn check_errors_is_a_std_error() {
616        // The reason the newtype exists: `Vec` is foreign, so it can never implement `Error`,
617        // and every caller had to handle this one error specially instead of writing `?`.
618        fn takes_std_error(_: &dyn std::error::Error) {}
619        takes_std_error(&check_errors("start = missing\r\n"));
620    }
621
622    #[test]
623    fn check_errors_behaves_like_a_slice() {
624        let errors = check_errors("start = a b c\r\n");
625        assert_eq!(errors.len(), 3);
626        assert!(!errors.is_empty());
627        assert_eq!(errors.iter().count(), 3);
628        assert!(matches!(errors[0], CheckError::UndefinedRule { .. }));
629        assert_eq!(errors.as_slice().len(), 3);
630    }
631
632    #[test]
633    fn check_errors_iterates_by_value_and_by_reference() {
634        let errors = check_errors("start = a b\r\n");
635        assert_eq!((&errors).into_iter().count(), 2);
636        assert_eq!(errors.into_iter().count(), 2);
637    }
638
639    #[test]
640    fn check_errors_converts_both_ways() {
641        let errors = check_errors("start = a b\r\n");
642        let as_vec: Vec<CheckError> = errors.clone().into();
643        assert_eq!(as_vec.len(), 2);
644        assert_eq!(CheckErrors::from(as_vec.clone()), errors);
645        assert_eq!(errors.into_vec(), as_vec);
646    }
647
648    #[test]
649    fn one_error_displays_as_itself() {
650        let errors = check_errors("start = missing\r\n");
651        assert_eq!(errors.to_string(), errors[0].to_string());
652        assert!(!errors.to_string().contains("structural errors"));
653    }
654
655    #[test]
656    fn several_errors_are_counted_then_listed() {
657        let errors = check_errors("start = a b c\r\n");
658        let shown = errors.to_string();
659        assert!(shown.starts_with("3 structural errors:"), "{shown}");
660        for error in &errors {
661            assert!(shown.contains(&error.to_string()), "{shown}");
662        }
663    }
664
665    #[test]
666    fn render_shows_every_error_against_the_source() {
667        // A grammar with four undefined references should take one round of fixing, not four.
668        let src = "start = a b c d\r\n";
669        let rendered = check_errors(src).render(src);
670
671        assert_eq!(rendered.matches("undefined rule").count(), 4);
672        assert!(rendered.contains('^'), "with carets: {rendered}");
673        assert!(rendered.ends_with("4 structural errors"), "{rendered}");
674    }
675
676    #[test]
677    fn render_says_error_in_the_singular() {
678        let src = "start = missing\r\n";
679        assert!(
680            check_errors(src)
681                .render(src)
682                .ends_with("1 structural error")
683        );
684    }
685}