use std::borrow::Cow;
use std::fmt;
use squonk_ast::Span;
use crate::tokenizer::LexErrorKind;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ParseError {
pub span: Span,
pub kind: ParseErrorKind,
pub expected: Expected,
pub found: Found,
hint: Option<Box<Cow<'static, str>>>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum ParseErrorKind {
Syntax,
RecursionLimitExceeded,
Lexical(LexErrorKind),
}
impl ParseError {
pub fn new(span: Span, expected: impl Into<Expected>, found: impl Into<Found>) -> Self {
Self {
span,
kind: ParseErrorKind::Syntax,
expected: expected.into(),
found: found.into(),
hint: None,
}
}
pub fn recursion_limit_exceeded(span: Span) -> Self {
Self {
span,
kind: ParseErrorKind::RecursionLimitExceeded,
expected: Expected::from("input within the configured recursion-depth limit"),
found: Found::from("input nested past the recursion-depth limit"),
hint: None,
}
}
pub fn lexical(span: Span, kind: LexErrorKind) -> Self {
Self {
span,
kind: ParseErrorKind::Lexical(kind),
expected: Expected::from("a well-formed SQL token"),
found: Found::from(kind.message()),
hint: None,
}
}
pub fn with_hint(mut self, hint: impl Into<Cow<'static, str>>) -> Self {
self.hint = Some(Box::new(hint.into()));
self
}
pub fn hint(&self) -> Option<&str> {
self.hint.as_deref().map(|hint| hint.as_ref())
}
}
impl fmt::Display for ParseError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self.kind {
ParseErrorKind::RecursionLimitExceeded => {
f.write_str("input nested too deeply: parser recursion limit exceeded")?;
}
ParseErrorKind::Syntax | ParseErrorKind::Lexical(_) => {
let expected = &self.expected;
let found = &self.found;
write!(f, "expected {expected}, found {found}")?;
}
}
if self.span.is_synthetic() {
f.write_str(" at an unknown position")
} else {
let start = self.span.start();
let end = self.span.end();
write!(f, " at bytes {start}..{end}")
}
}
}
impl std::error::Error for ParseError {}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Expected(Cow<'static, str>);
impl Expected {
pub fn as_str(&self) -> &str {
&self.0
}
}
impl fmt::Display for Expected {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.0.fmt(f)
}
}
impl From<&'static str> for Expected {
fn from(description: &'static str) -> Self {
Self(Cow::Borrowed(description))
}
}
impl From<String> for Expected {
fn from(description: String) -> Self {
Self(Cow::Owned(description))
}
}
impl From<Cow<'static, str>> for Expected {
fn from(description: Cow<'static, str>) -> Self {
Self(description)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Found {
Text(Cow<'static, str>),
EndOfInput,
}
impl fmt::Display for Found {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Text(text) => write!(f, "{text}"),
Self::EndOfInput => f.write_str("end of input"),
}
}
}
impl From<&'static str> for Found {
fn from(text: &'static str) -> Self {
Self::Text(Cow::Borrowed(text))
}
}
impl From<String> for Found {
fn from(text: String) -> Self {
Self::Text(Cow::Owned(text))
}
}
impl From<Cow<'static, str>> for Found {
fn from(text: Cow<'static, str>) -> Self {
Self::Text(text)
}
}
pub type ParseResult<T> = Result<T, ParseError>;
pub trait ErrorSink {
fn report(&mut self, error: ParseError);
}
impl<T: ErrorSink + ?Sized> ErrorSink for &mut T {
fn report(&mut self, error: ParseError) {
(**self).report(error);
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct FailFastSink {
first: Option<ParseError>,
}
impl FailFastSink {
pub const fn new() -> Self {
Self { first: None }
}
}
impl ErrorSink for FailFastSink {
fn report(&mut self, error: ParseError) {
if self.first.is_none() {
self.first = Some(error);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_error_preserves_its_span() {
let span = Span::new(7, 11);
let err = ParseError::new(span, "expression", "keyword `FROM`");
assert_eq!(err.span, span);
assert_eq!(err.span.start(), 7);
assert_eq!(err.span.end(), 11);
}
#[test]
fn display_is_readable_and_includes_position() {
let err = ParseError::new(Span::new(7, 11), "expression", "keyword `FROM`");
let msg = err.to_string();
assert!(msg.contains("expected expression"), "{msg}");
assert!(msg.contains("found keyword `FROM`"), "{msg}");
assert!(msg.contains("7..11"), "{msg}");
}
#[test]
fn found_end_of_input_renders_readably() {
let err = ParseError::new(Span::new(12, 12), "`)`", Found::EndOfInput);
assert!(err.to_string().contains("found end of input"), "{err}");
}
#[test]
fn synthetic_span_does_not_leak_the_sentinel() {
let err = ParseError::new(Span::SYNTHETIC, "expression", "`,`");
let msg = err.to_string();
assert!(msg.contains("unknown position"), "{msg}");
assert!(!msg.contains(&u32::MAX.to_string()), "{msg}");
}
#[test]
fn parse_error_is_a_std_error() {
let err = ParseError::new(Span::new(0, 1), "expression", "`,`");
let boxed: Box<dyn std::error::Error> = Box::new(err.clone());
assert_eq!(boxed.to_string(), err.to_string());
}
#[test]
fn hint_seam_round_trips_and_defaults_empty() {
let plain = ParseError::new(Span::new(0, 1), "expression", "`,`");
assert_eq!(
plain.hint(),
None,
"constructors leave the hint channel empty"
);
let hinted = plain.with_hint("did you mean `SELECT`?");
assert_eq!(hinted.hint(), Some("did you mean `SELECT`?"));
assert_eq!(hinted.kind, ParseErrorKind::Syntax);
assert_eq!(hinted.span, Span::new(0, 1));
}
#[test]
fn lexical_constructor_carries_the_lex_kind_and_stays_readable() {
let err = ParseError::lexical(Span::new(3, 8), LexErrorKind::UnterminatedString);
assert_eq!(
err.kind,
ParseErrorKind::Lexical(LexErrorKind::UnterminatedString)
);
let msg = err.to_string();
assert!(msg.contains("expected a well-formed SQL token"), "{msg}");
assert!(msg.contains("unterminated string literal"), "{msg}");
assert!(msg.contains("3..8"), "{msg}");
}
#[test]
fn fail_fast_sink_captures_the_first_error() {
let mut sink = FailFastSink::new();
assert!(sink.first.is_none());
let first = ParseError::new(Span::new(0, 3), "`SELECT`", "`slect`");
let second = ParseError::new(Span::new(10, 14), "`)`", Found::EndOfInput);
sink.report(first.clone());
sink.report(second);
assert_eq!(sink.first, Some(first));
}
#[test]
fn error_sink_is_implemented_for_mutable_references() {
fn report_once(mut sink: impl ErrorSink, error: ParseError) {
sink.report(error);
}
let mut sink = FailFastSink::new();
report_once(&mut sink, ParseError::new(Span::new(0, 1), "x", "y"));
assert!(sink.first.is_some());
}
}