use core::fmt;
use crate::ast::Span;
#[derive(Clone, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub enum ParseError {
NonAscii {
span: Span,
},
ExpectedCrlf {
span: Span,
},
NumberTooLarge {
span: Span,
},
Expected {
what: &'static str,
span: Span,
},
UnexpectedEof {
span: Span,
},
}
impl ParseError {
#[must_use]
pub fn span(&self) -> Span {
match self {
Self::NonAscii { span }
| Self::ExpectedCrlf { span }
| Self::NumberTooLarge { span }
| Self::Expected { span, .. }
| Self::UnexpectedEof { span } => *span,
}
}
}
impl fmt::Display for ParseError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::NonAscii { .. } => f.write_str(
"non-ASCII byte in grammar text; ABNF comments and prose values are ASCII-only",
),
Self::ExpectedCrlf { .. } => f.write_str("expected CRLF line ending"),
Self::NumberTooLarge { .. } => f.write_str("numeric value does not fit in 64 bits"),
Self::Expected { what, .. } => write!(f, "expected {what}"),
Self::UnexpectedEof { .. } => f.write_str("unexpected end of grammar text"),
}
}
}
impl ParseError {
#[must_use]
pub fn render(&self, src: &str) -> String {
render_at(src, self.span(), &self.to_string())
}
}
impl std::error::Error for ParseError {}
#[derive(Clone, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub enum CheckError {
DuplicateDefinition {
name: String,
span: Span,
},
IncrementalWithoutBase {
name: String,
shadows_core: bool,
span: Span,
},
UndefinedRule {
name: String,
span: Span,
},
InvalidRepeatRange {
min: u64,
max: u64,
span: Span,
},
InvalidNumericRange {
lo: u64,
hi: u64,
span: Span,
},
LeftRecursion {
cycle: Vec<String>,
},
}
impl fmt::Display for CheckError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::DuplicateDefinition { name, .. } => {
write!(f, "rule `{name}` is defined more than once")
}
Self::IncrementalWithoutBase {
name,
shadows_core: false,
..
} => {
write!(f, "`{name} =/ …` has no prior `{name} = …`")
}
Self::IncrementalWithoutBase {
name,
shadows_core: true,
..
} => write!(
f,
"`{name} =/ …` has no prior `{name} = …`; `{name}` is an implicit core rule and \
cannot be extended in place — write `{name} = <core definition> / …` to shadow \
and extend it"
),
Self::UndefinedRule { name, .. } => write!(f, "undefined rule `{name}`"),
Self::InvalidRepeatRange { min, max, .. } => {
write!(f, "repetition lower bound {min} exceeds upper bound {max}")
}
Self::InvalidNumericRange { lo, hi, .. } => {
write!(
f,
"numeric range starts at {lo:#X} and ends below it at {hi:#X}"
)
}
Self::LeftRecursion { cycle } => {
write!(f, "left recursion: {}", cycle.join(" -> "))
}
}
}
}
impl CheckError {
#[must_use]
pub fn span(&self) -> Option<Span> {
match self {
Self::DuplicateDefinition { span, .. }
| Self::IncrementalWithoutBase { span, .. }
| Self::UndefinedRule { span, .. }
| Self::InvalidRepeatRange { span, .. }
| Self::InvalidNumericRange { span, .. } => Some(*span),
Self::LeftRecursion { .. } => None,
}
}
#[must_use]
pub fn render(&self, src: &str) -> String {
match self.span() {
Some(span) => render_at(src, span, &self.to_string()),
None => self.to_string(),
}
}
}
impl std::error::Error for CheckError {}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct CheckErrors(Vec<CheckError>);
impl CheckErrors {
#[must_use]
pub fn as_slice(&self) -> &[CheckError] {
&self.0
}
#[must_use]
pub fn into_vec(self) -> Vec<CheckError> {
self.0
}
#[must_use]
pub fn render(&self, src: &str) -> String {
let mut out = String::new();
for error in &self.0 {
out.push_str(&error.render(src));
out.push('\n');
}
let count = self.0.len();
out.push_str(&format!(
"{count} structural {}",
if count == 1 { "error" } else { "errors" }
));
out
}
}
impl core::ops::Deref for CheckErrors {
type Target = [CheckError];
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl From<Vec<CheckError>> for CheckErrors {
fn from(errors: Vec<CheckError>) -> Self {
Self(errors)
}
}
impl From<CheckErrors> for Vec<CheckError> {
fn from(errors: CheckErrors) -> Self {
errors.0
}
}
impl IntoIterator for CheckErrors {
type Item = CheckError;
type IntoIter = std::vec::IntoIter<CheckError>;
fn into_iter(self) -> Self::IntoIter {
self.0.into_iter()
}
}
impl<'a> IntoIterator for &'a CheckErrors {
type Item = &'a CheckError;
type IntoIter = core::slice::Iter<'a, CheckError>;
fn into_iter(self) -> Self::IntoIter {
self.0.iter()
}
}
impl fmt::Display for CheckErrors {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self.0.as_slice() {
[] => f.write_str("no structural errors"),
[only] => write!(f, "{only}"),
many => {
write!(f, "{} structural errors:", many.len())?;
for error in many {
write!(f, "\n {error}")?;
}
Ok(())
}
}
}
}
impl std::error::Error for CheckErrors {}
#[derive(Clone, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub enum LintWarning {
UnreferencedRule {
name: String,
},
UnreachableRule {
name: String,
},
UnproductiveRule {
name: String,
},
UnproductiveAlternative {
rule: String,
branch: u32,
},
ShadowsCoreRule {
name: String,
},
}
impl fmt::Display for LintWarning {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::UnreferencedRule { name } => {
write!(f, "rule `{name}` is never referenced")
}
Self::UnreachableRule { name } => {
write!(
f,
"rule `{name}` is not reachable from the given start rules"
)
}
Self::UnproductiveRule { name } => {
write!(
f,
"rule `{name}` can never match: it has no finite expansion"
)
}
Self::UnproductiveAlternative { rule, branch } => write!(
f,
"branch {branch} of an alternation in rule `{rule}` can never match"
),
Self::ShadowsCoreRule { name } => {
write!(f, "rule `{name}` shadows the core rule of the same name")
}
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub enum MatchError {
ProseValueReachable {
rule: String,
prose: String,
},
UnrepresentableTerminal {
rule: String,
terminal: String,
},
UnknownRule(
String,
),
LeftRecursionDetected {
rule: String,
},
StepLimit,
DepthLimit,
}
impl fmt::Display for MatchError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::ProseValueReachable { rule, prose } => write!(
f,
"rule `{rule}` can reach the prose value <{prose}>, which has no matching semantics"
),
Self::UnrepresentableTerminal { rule, terminal } => write!(
f,
"rule `{rule}` can reach terminal {terminal}, which matches no Unicode scalar value"
),
Self::UnknownRule(name) => write!(f, "unknown rule `{name}`"),
Self::LeftRecursionDetected { rule } => {
write!(f, "left recursion detected at rule `{rule}`")
}
Self::StepLimit => f.write_str("step limit exceeded"),
Self::DepthLimit => f.write_str(
"recursion depth limit exceeded: the input nests deeper than `MatchOptions::max_depth` allows",
),
}
}
}
impl std::error::Error for MatchError {}
#[derive(Clone, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub enum GenError {
ProseValueReachable {
rule: String,
prose: String,
},
UnrepresentableTerminal {
rule: String,
terminal: String,
},
UnknownRule(
String,
),
NoFiniteExpansion {
rule: String,
},
OutputLimit,
StepLimit,
}
impl fmt::Display for GenError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::ProseValueReachable { rule, prose } => write!(
f,
"rule `{rule}` can reach the prose value <{prose}>, which cannot be generated"
),
Self::UnrepresentableTerminal { rule, terminal } => write!(
f,
"rule `{rule}` can reach terminal {terminal}, which has no representable value"
),
Self::UnknownRule(name) => write!(f, "unknown rule `{name}`"),
Self::NoFiniteExpansion { rule } => {
write!(f, "rule `{rule}` has no finite expansion")
}
Self::OutputLimit => f.write_str("output length limit exceeded"),
Self::StepLimit => f.write_str("step limit exceeded"),
}
}
}
impl std::error::Error for GenError {}
fn render_at(src: &str, span: Span, message: &str) -> String {
let offset = (span.start as usize).min(src.len());
let before = &src[..offset];
let line_number = before.matches('\n').count() + 1;
let line_start = before.rfind('\n').map_or(0, |index| index + 1);
let line = src[line_start..].lines().next().unwrap_or_default();
let column = offset - line_start;
let width = (span.end.saturating_sub(span.start) as usize)
.min(line.len().saturating_sub(column))
.max(1);
format!(
"line {line_number}, column {}: {message}\n {line}\n {}{}",
column + 1,
" ".repeat(column),
"^".repeat(width)
)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn incremental_without_base_suggests_the_workaround_for_core_rules() {
let plain = CheckError::IncrementalWithoutBase {
name: "foo".into(),
shadows_core: false,
span: Span::default(),
};
let core = CheckError::IncrementalWithoutBase {
name: "DIGIT".into(),
shadows_core: true,
span: Span::default(),
};
assert!(!plain.to_string().contains("shadow"));
assert!(core.to_string().contains("DIGIT = <core definition> / …"));
}
#[test]
fn parse_error_span_is_reported_for_every_variant() {
let span = Span::new(3, 7);
for error in [
ParseError::NonAscii { span },
ParseError::ExpectedCrlf { span },
ParseError::NumberTooLarge { span },
ParseError::Expected { what: "=", span },
ParseError::UnexpectedEof { span },
] {
assert_eq!(error.span(), span);
assert!(!error.to_string().is_empty());
}
}
fn check_errors(src: &str) -> CheckErrors {
crate::ast::Grammar::parse(src)
.expect("parses")
.check()
.expect_err("should not check")
}
#[test]
fn check_errors_is_a_std_error() {
fn takes_std_error(_: &dyn std::error::Error) {}
takes_std_error(&check_errors("start = missing\r\n"));
}
#[test]
fn check_errors_behaves_like_a_slice() {
let errors = check_errors("start = a b c\r\n");
assert_eq!(errors.len(), 3);
assert!(!errors.is_empty());
assert_eq!(errors.iter().count(), 3);
assert!(matches!(errors[0], CheckError::UndefinedRule { .. }));
assert_eq!(errors.as_slice().len(), 3);
}
#[test]
fn check_errors_iterates_by_value_and_by_reference() {
let errors = check_errors("start = a b\r\n");
assert_eq!((&errors).into_iter().count(), 2);
assert_eq!(errors.into_iter().count(), 2);
}
#[test]
fn check_errors_converts_both_ways() {
let errors = check_errors("start = a b\r\n");
let as_vec: Vec<CheckError> = errors.clone().into();
assert_eq!(as_vec.len(), 2);
assert_eq!(CheckErrors::from(as_vec.clone()), errors);
assert_eq!(errors.into_vec(), as_vec);
}
#[test]
fn one_error_displays_as_itself() {
let errors = check_errors("start = missing\r\n");
assert_eq!(errors.to_string(), errors[0].to_string());
assert!(!errors.to_string().contains("structural errors"));
}
#[test]
fn several_errors_are_counted_then_listed() {
let errors = check_errors("start = a b c\r\n");
let shown = errors.to_string();
assert!(shown.starts_with("3 structural errors:"), "{shown}");
for error in &errors {
assert!(shown.contains(&error.to_string()), "{shown}");
}
}
#[test]
fn render_shows_every_error_against_the_source() {
let src = "start = a b c d\r\n";
let rendered = check_errors(src).render(src);
assert_eq!(rendered.matches("undefined rule").count(), 4);
assert!(rendered.contains('^'), "with carets: {rendered}");
assert!(rendered.ends_with("4 structural errors"), "{rendered}");
}
#[test]
fn render_says_error_in_the_singular() {
let src = "start = missing\r\n";
assert!(
check_errors(src)
.render(src)
.ends_with("1 structural error")
);
}
}