diff --git a/src/error.rs b/src/error.rs index fd7d94695..ca8f7e407 100644 --- a/src/error.rs +++ b/src/error.rs @@ -17,12 +17,14 @@ //! - [`ErrorKind`] //! - [`InputError`] (mostly for testing) //! - [`ContextError`] +//! - [`LocationContextError`] collect context from longest match //! - [`TreeError`] (mostly for testing) //! - [Custom errors][crate::_topic::error] #[cfg(feature = "alloc")] use crate::lib::std::borrow::ToOwned; use crate::lib::std::fmt; +use crate::stream::Location; use core::num::NonZeroUsize; use crate::stream::AsBStr; @@ -465,6 +467,8 @@ pub struct ContextError { context: core::marker::PhantomData, #[cfg(feature = "std")] cause: Option>, + #[cfg(not(feature = "std"))] + cause: (), } impl ContextError { @@ -635,6 +639,252 @@ impl crate::lib::std::fmt::Display for ContextError { } } +/// Location aware context error. +/// The context error tracks the rightmost position of the error. +/// In presence of backtracking new contexts in "lower" positions are ignored. +#[derive(Debug)] +pub struct LocationContextError { + pos: usize, + + #[cfg(feature = "alloc")] + context: crate::lib::std::vec::Vec, + #[cfg(not(feature = "alloc"))] + context: core::marker::PhantomData, + #[cfg(feature = "std")] + cause: Option>, + #[cfg(not(feature = "std"))] + cause: (), +} + +impl LocationContextError { + /// Create an empty error + #[inline] + pub fn new() -> Self { + Self::new_at(0) + } + + /// Create an empty error at the given position + #[inline] + pub fn new_at(pos: usize) -> Self { + Self { + pos, + context: Default::default(), + #[cfg(feature = "std")] + cause: None, + } + } + + /// Access the error position + pub fn pos(&self) -> usize { + self.pos + } + + /// Access context from [`Parser::context`] + #[inline] + #[cfg(feature = "alloc")] + pub fn context(&self) -> impl Iterator { + self.context.iter() + } + + /// Originating [`std::error::Error`] + #[inline] + #[cfg(feature = "std")] + pub fn cause(&self) -> Option<&(dyn std::error::Error + Send + Sync + 'static)> { + self.cause.as_deref() + } +} + +impl Clone for LocationContextError { + fn clone(&self) -> Self { + Self { + pos: self.pos.clone(), + context: self.context.clone(), + #[cfg(feature = "std")] + cause: self.cause.as_ref().map(|e| e.to_string().into()), + #[cfg(not(feature = "std"))] + cause: (), + } + } +} + +impl Default for LocationContextError { + #[inline] + fn default() -> Self { + Self::new() + } +} + +impl ParserError for LocationContextError { + #[inline] + fn from_error_kind(input: &I, _kind: ErrorKind) -> Self { + Self::new_at(input.location()) + } + + #[inline] + fn append(self, _input: &I, _kind: ErrorKind) -> Self { + self + } + + fn or(self, other: Self) -> Self { + // rightmost context wins + match self.pos.cmp(&other.pos) { + core::cmp::Ordering::Less => other, + core::cmp::Ordering::Greater => self, + core::cmp::Ordering::Equal => { + #[cfg(feature = "std")] + let cause = self.cause.or(other.cause); + #[cfg(not(feature = "std"))] + let cause = (); + + #[cfg(feature = "alloc")] + let context = { + let (mut context, other) = if self.context.capacity() > other.context.capacity() + { + (self.context, other.context) + } else { + (other.context, self.context) + }; + context.extend(other); + context + }; + #[cfg(not(feature = "alloc"))] + let context = self.context; + + Self { + pos: self.pos, + context, + cause, + } + } + } + } +} + +impl AddContext for LocationContextError { + #[inline] + fn add_context(mut self, input: &I, ctx: C) -> Self { + #[cfg(feature = "alloc")] + { + let pos = input.location(); + match pos.cmp(&self.pos) { + core::cmp::Ordering::Less => {} + core::cmp::Ordering::Greater => { + self.pos = pos; + self.context.clear(); + self.context.push(ctx); + } + core::cmp::Ordering::Equal => { + self.context.push(ctx); + } + } + } + self + } +} + +#[cfg(feature = "std")] +impl FromExternalError + for LocationContextError +{ + #[inline] + fn from_external_error(input: &I, _kind: ErrorKind, e: E) -> Self { + let mut err = Self::new_at(input.location()); + { + err.cause = Some(Box::new(e)); + } + err + } +} + +// HACK: This is more general than `std`, making the features non-additive +#[cfg(not(feature = "std"))] +impl FromExternalError + for LocationContextError +{ + #[inline] + fn from_external_error(input: &I, _kind: ErrorKind, _e: E) -> Self { + let err = Self::new_at(input.location()); + err + } +} + +// For tests +impl core::cmp::PartialEq for LocationContextError { + fn eq(&self, other: &Self) -> bool { + #[cfg(feature = "alloc")] + { + if self.context != other.context { + return false; + } + } + #[cfg(feature = "std")] + { + if self.cause.as_ref().map(ToString::to_string) + != other.cause.as_ref().map(ToString::to_string) + { + return false; + } + } + + true + } +} + +impl crate::lib::std::fmt::Display for LocationContextError { + fn fmt(&self, f: &mut crate::lib::std::fmt::Formatter<'_>) -> crate::lib::std::fmt::Result { + #[cfg(feature = "alloc")] + { + let expression = self.context().find_map(|c| match c { + StrContext::Label(c) => Some(c), + _ => None, + }); + let expected = self + .context() + .filter_map(|c| match c { + StrContext::Expected(c) => Some(c), + _ => None, + }) + .collect::>(); + + let mut newline = false; + + if let Some(expression) = expression { + newline = true; + + write!(f, "invalid {}", expression)?; + } + + if !expected.is_empty() { + if newline { + writeln!(f)?; + } + newline = true; + + write!(f, "expected ")?; + for (i, expected) in expected.iter().enumerate() { + if i != 0 { + write!(f, ", ")?; + } + write!(f, "{}", expected)?; + } + } + #[cfg(feature = "std")] + { + if let Some(cause) = self.cause() { + if newline { + writeln!(f)?; + } + write!(f, "{}", cause)?; + } + } + + write!(f, " at position {}", self.pos)?; + } + + Ok(()) + } +} + /// Additional parse context for [`ContextError`] added via [`Parser::context`] #[derive(Clone, Debug, PartialEq, Eq)] #[non_exhaustive]