shrimple_parser/
error.rs

1extern crate alloc;
2
3use {
4    crate::{utils::PathLike, FullLocation, Input, Location},
5    alloc::borrow::Cow,
6    core::{
7        convert::Infallible,
8        error::Error,
9        fmt::{Debug, Display, Formatter},
10        ops::Not,
11    },
12    std::{fs::read_to_string, io},
13};
14
15/// Error returned by a parser.
16///
17/// A parsing error may be either recoverable or fatal, parser methods such as [`Parser::or`] allow
18/// trying different paths if a recoverable error occurs, whereas a fatal error is not intended to
19/// be recovered from and should just be propagated.
20///
21/// To make the error more useful, consider the following options:
22/// - [`ParsingError::with_src_loc`]
23/// - [`Parser::with_full_error`]
24#[derive(Debug, PartialEq, Eq, Clone, Copy)]
25pub struct ParsingError<In, Reason = Infallible> {
26    /// The rest of the input that could not be processed.
27    pub rest: In,
28    /// What the parser expected, the reason for the error.
29    /// `None` means that the error is recoverable.
30    pub reason: Option<Reason>,
31}
32
33impl<In: Input, Reason: Display> Display for ParsingError<In, Reason> {
34    fn fmt(&self, f: &mut Formatter) -> core::fmt::Result {
35        if let Some(reason) = &self.reason {
36            writeln!(f, "{reason}")?;
37        }
38        write!(
39            f,
40            "error source: {}{}",
41            self.rest[..self.rest.len().min(16)].escape_debug(),
42            if self.rest.len() > 16 { "..." } else { "" }
43        )?;
44        Ok(())
45    }
46}
47
48impl<In: Input, Reason: Error> Error for ParsingError<In, Reason> {}
49
50impl<In, Reason> ParsingError<In, Reason> {
51    /// Create a new fatal parsing error.
52    pub const fn new(rest: In, reason: Reason) -> Self {
53        Self {
54            rest,
55            reason: Some(reason),
56        }
57    }
58
59    /// Create a new recoverable parsing error.
60    pub const fn new_recoverable(rest: In) -> Self {
61        Self { rest, reason: None }
62    }
63
64    /// Returns a boolean indicating whether the error is recoverable.
65    pub const fn is_recoverable(&self) -> bool {
66        self.reason.is_none()
67    }
68
69    /// Changes the reason associated with the error, making the error fatal.
70    pub fn reason<NewReason>(self, reason: NewReason) -> ParsingError<In, NewReason> {
71        ParsingError {
72            reason: Some(reason),
73            rest: self.rest,
74        }
75    }
76
77    /// Makes a recoverable error fatal by giving it a reason, if it's already fatal, does nothing
78    #[must_use]
79    pub fn or_reason(self, reason: Reason) -> Self {
80        Self {
81            reason: self.reason.or(Some(reason)),
82            rest: self.rest,
83        }
84    }
85
86    /// Like [`ParsingError::or_reason`] but does nothing if the rest of the input is empty
87    #[must_use]
88    pub fn or_reason_if_nonempty(self, reason: Reason) -> Self
89    where
90        In: Input,
91    {
92        Self {
93            reason: self
94                .reason
95                .or_else(|| self.rest.is_empty().not().then_some(reason)),
96            rest: self.rest,
97        }
98    }
99
100    /// Transforms the reason by calling `f`, except if it's a recoverable error,
101    /// in which case it remains recoverable.
102    pub fn map_reason<NewReason>(
103        self,
104        f: impl FnOnce(Reason) -> NewReason,
105    ) -> ParsingError<In, NewReason> {
106        ParsingError {
107            reason: self.reason.map(f),
108            rest: self.rest,
109        }
110    }
111
112    /// Convert the reason of an always recoverable error to another type. This will be a no-op
113    /// since it's statically guaranteed that the reason doesn't exist.
114    #[allow(unreachable_code)]
115    pub fn adapt_reason<NewReason>(self) -> ParsingError<In, NewReason>
116    where
117        Infallible: From<Reason>,
118    {
119        ParsingError {
120            reason: self.reason.map(|x| match Infallible::from(x) {}),
121            rest: self.rest,
122        }
123    }
124
125    /// Turns the error into a [`FullParsingError`] for a more informative report.
126    ///
127    /// The error will point to the provided source code.
128    /// The provided path will only be used for display purposes, this method won't access the file
129    /// system.
130    pub fn with_src_loc<'a>(
131        self,
132        path: impl PathLike<'a>,
133        src: &'a str,
134    ) -> FullParsingError<'a, Reason>
135    where
136        In: Input,
137    {
138        FullParsingError {
139            loc: Location::find_saturating(self.rest.as_ptr(), src).with_path(path),
140            reason: self.reason,
141            src: src.into(),
142        }
143    }
144
145    /// Turns this error into a [`FullParsingError`] that points to a file on the machine, for a more informative report.
146    ///
147    /// # Errors
148    /// Returns an error if [`std::fs::read_to_string`] does.
149    #[cfg(feature = "std")]
150    pub fn with_file_loc<'a>(
151        self,
152        path: impl PathLike<'a>,
153    ) -> io::Result<FullParsingError<'a, Reason>>
154    where
155        In: Input,
156    {
157        let path = path.into_path();
158        let src = read_to_string(&path)?;
159        Ok(FullParsingError {
160            loc: Location::find_saturating(self.rest.as_ptr(), &src).with_path(path),
161            reason: self.reason,
162            src: src.into(),
163        })
164    }
165}
166
167/// A final error with information about where in the source did the error occur.
168///
169/// This should be constructed at the top-level of a parser as the final action before returning
170/// the result. Main ways to construct this are [`ParsingError::with_src_loc`] and
171/// [`Parser::with_full_error`]
172///
173/// To print the source line of the error along with the reason & location, use the value returned
174/// by its method [`with_source_line`](Self::with_source_line),
175/// this will alter its [`Display`] implementation.
176#[derive(Debug, Clone)]
177pub struct FullParsingError<'a, Reason> {
178    /// Where the error occured.
179    pub loc: FullLocation<'a>,
180    /// What the parser expected to see at the location of the error.
181    /// If `None`, then the error was recoverable and the parser didn't have any particular
182    /// reason.
183    pub reason: Option<Reason>,
184    /// The source code to which the error points.
185    pub src: Cow<'a, str>,
186}
187
188impl<Reason: Display> Display for FullParsingError<'_, Reason> {
189    fn fmt(&self, f: &mut Formatter) -> core::fmt::Result {
190        if let Some(reason) = &self.reason {
191            writeln!(f, "{reason}")?;
192        }
193        writeln!(f, "--> {}", self.loc)?;
194        let line = self
195            .src
196            .lines()
197            .nth(self.loc.loc.line.get() as usize - 1)
198            .ok_or(core::fmt::Error)?;
199        let line_col_off = self.loc.loc.line.ilog10() as usize + 1;
200        writeln!(f, "{:line_col_off$} |", "")?;
201        writeln!(f, "{:line_col_off$} | {line}", self.loc.loc.line)?;
202        write!(
203            f,
204            "{:line_col_off$} | {:>2$}",
205            "",
206            '^',
207            self.loc.loc.col as usize + 1
208        )?;
209        Ok(())
210    }
211}
212
213impl<Reason: Error> Error for FullParsingError<'_, Reason> {}
214
215impl<Reason> FullParsingError<'_, Reason> {
216    /// Unbind the error from the lifetimes by allocating the file path if it hasn't been already.
217    pub fn own(self) -> FullParsingError<'static, Reason> {
218        FullParsingError {
219            loc: self.loc.own(),
220            src: self.src.into_owned().into(),
221            ..self
222        }
223    }
224}
225
226/// The result of a parser.
227pub type ParsingResult<In, T, Reason = Infallible> =
228    core::result::Result<(In, T), ParsingError<In, Reason>>;