use crate::span::{floor_char_boundary, Span};
use crate::stream::AtomStream;
use syan::error::ParseError;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ParseFailureKind {
Lex,
Syntax,
GaveUp,
}
#[derive(Debug, Clone, thiserror::Error)]
#[error("{span}: {}", self.render())]
pub struct ParseFileError {
pub span: Span,
pub message: String,
pub kind: ParseFailureKind,
}
impl ParseFileError {
pub fn from_lex(e: crate::lexer::LexError) -> Self {
ParseFileError {
span: e.span,
message: e.msg,
kind: ParseFailureKind::Lex,
}
}
pub fn render(&self) -> String {
match self.kind {
ParseFailureKind::Lex | ParseFailureKind::Syntax => {
format!("parse error: {}", self.message)
}
ParseFailureKind::GaveUp => format!("gave up: {}", self.message),
}
}
}
const GAVE_UP: &str = "this file needs more backtracking than the parser allows. \
The parse got this far and no further, which usually means a \
construct at or above this point is unfinished";
pub fn locate(source: &str, stream: &AtomStream, err: &ParseError<Span>) -> ParseFileError {
let furthest = stream.furthest();
let stalled = stream.furthest_span();
if stream.exhausted() {
return ParseFileError {
span: stalled.unwrap_or(*err.span()),
message: GAVE_UP.to_string(),
kind: ParseFailureKind::GaveUp,
};
}
let (span, message) = best_failure(err);
if span.end.byte >= furthest {
return ParseFileError {
span,
message,
kind: ParseFailureKind::Syntax,
};
}
let stalled = stalled.unwrap_or(span);
ParseFileError {
message: stalled_message(source, stalled),
span: stalled,
kind: ParseFailureKind::Syntax,
}
}
fn stalled_message(source: &str, span: Span) -> String {
const MAX: usize = 24;
let start = floor_char_boundary(source, span.start.byte);
let end = floor_char_boundary(source, span.end.byte.max(start));
let raw = source[start..end].trim();
if raw.is_empty() {
return match start >= source.trim_end().len() {
true => "unexpected end of input".to_string(),
false => "unexpected input here".to_string(),
};
}
let text: String = raw
.chars()
.take_while(|c| *c != '\n' && *c != '\r')
.collect();
let text = text.trim_end();
if text.chars().count() > MAX {
let cut: String = text.chars().take(MAX).collect();
return format!("unexpected `{cut}...`");
}
format!("unexpected `{text}`")
}
fn best_failure(err: &ParseError<Span>) -> (Span, String) {
let mut deepest: Option<Span> = None;
let mut reasons: Vec<String> = Vec::new();
visit_leaves(err, &mut |leaf| {
let span = *leaf.span();
let depth = span.end.byte;
let best = deepest.map(|s| s.end.byte);
if best.is_none_or(|b| depth > b) {
deepest = Some(span);
reasons.clear();
}
if deepest.map(|s| s.end.byte) == Some(depth) {
let reason = leaf_reason(leaf);
if !reasons.contains(&reason) {
reasons.push(reason);
}
}
});
let span = deepest.unwrap_or(*err.span());
(span, render_reasons(&reasons))
}
fn render_reasons(reasons: &[String]) -> String {
const MAX_REASONS: usize = 4;
const PREFIX: &str = "expected ";
if reasons.is_empty() {
return "the input does not parse here".to_string();
}
let (kept, extra) = match reasons.len() > MAX_REASONS {
true => (&reasons[..MAX_REASONS], reasons.len() - MAX_REASONS),
false => (reasons, 0),
};
let all_expected = kept.iter().all(|r| r.starts_with(PREFIX));
let body = if all_expected {
let stripped: Vec<String> = kept.iter().map(|r| r[PREFIX.len()..].to_string()).collect();
format!("expected {}", join_alternatives(&stripped))
} else {
join_alternatives(kept)
};
match extra {
0 => body,
n => format!("{body} (and {n} more)"),
}
}
fn visit_leaves(err: &ParseError<Span>, f: &mut impl FnMut(&ParseError<Span>)) {
let alts = err.alternatives();
if alts.is_empty() {
f(err);
return;
}
for alt in alts {
visit_leaves(alt, f);
}
}
fn leaf_reason(leaf: &ParseError<Span>) -> String {
let rendered = leaf.to_string();
match rendered.rfind(" at Span {") {
Some(cut) => rendered[..cut].to_string(),
None => rendered,
}
}
fn join_alternatives(reasons: &[String]) -> String {
match reasons {
[] => String::new(),
[one] => one.clone(),
[head @ .., last] => format!("{}, or {last}", head.join(", ")),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn join_alternatives_reads_as_english() {
assert_eq!(join_alternatives(&[]), "");
assert_eq!(join_alternatives(&["a".into()]), "a");
assert_eq!(join_alternatives(&["a".into(), "b".into()]), "a, or b");
assert_eq!(
join_alternatives(&["a".into(), "b".into(), "c".into()]),
"a, b, or c"
);
}
#[test]
fn render_reasons_factors_out_a_shared_expected() {
assert_eq!(render_reasons(&[]), "the input does not parse here");
assert_eq!(
render_reasons(&["expected 'let'".into(), "expected 'if'".into()]),
"expected 'let', or 'if'"
);
assert_eq!(
render_reasons(&["expected 'let'".into(), "unexpected end of input".into()]),
"expected 'let', or unexpected end of input"
);
}
#[test]
fn render_reasons_caps_a_long_alternative_list() {
let many: Vec<String> = (0..9).map(|i| format!("expected '{i}'")).collect();
assert_eq!(
render_reasons(&many),
"expected '0', '1', '2', or '3' (and 5 more)"
);
}
#[test]
fn leaf_reason_drops_syans_span_suffix() {
let span = Span::default();
let leaf = ParseError::expected(span, "end of input");
let rendered = leaf.to_string();
assert!(
rendered.contains("at Span {"),
"syan changed its Display: {rendered}"
);
assert_eq!(leaf_reason(&leaf), "expected end of input");
}
#[test]
fn the_two_kinds_are_framed_differently() {
let syntax = ParseFileError {
span: Span::default(),
message: "expected 'in'".to_string(),
kind: ParseFailureKind::Syntax,
};
assert_eq!(syntax.render(), "parse error: expected 'in'");
let gave_up = ParseFileError {
span: Span::default(),
message: GAVE_UP.to_string(),
kind: ParseFailureKind::GaveUp,
};
assert!(gave_up.render().starts_with("gave up: "), "{gave_up}");
assert!(!gave_up.render().contains("parse error"), "{gave_up}");
}
}