shrimple_parser/
error.rs

1extern crate alloc;
2
3use {
4    crate::{utils::PathLike, FullLocation, Input, Location},
5    core::{
6        convert::Infallible,
7        error::Error,
8        fmt::{Debug, Display, Formatter},
9        ops::Not,
10    },
11    std::{io, fs::read_to_string},
12    alloc::borrow::Cow,
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>(self, path: impl PathLike<'a>) -> io::Result<FullParsingError<'a, Reason>>
151    where
152        In: Input,
153    {
154        let path = path.into_path();
155        let src = read_to_string(&path)?;
156        Ok(FullParsingError {
157            loc: Location::find_saturating(self.rest.as_ptr(), &src).with_path(path),
158            reason: self.reason,
159            src: src.into(),
160        })
161    }
162}
163
164/// A final error with information about where in the source did the error occur.
165///
166/// This should be constructed at the top-level of a parser as the final action before returning
167/// the result. Main ways to construct this are [`ParsingError::with_src_loc`] and
168/// [`Parser::with_full_error`]
169///
170/// To print the source line of the error along with the reason & location, use the value returned
171/// by its method [`with_source_line`](Self::with_source_line),
172/// this will alter its [`Display`] implementation.
173#[derive(Debug, Clone)]
174pub struct FullParsingError<'a, Reason> {
175    /// Where the error occured.
176    pub loc: FullLocation<'a>,
177    /// What the parser expected to see at the location of the error.
178    /// If `None`, then the error was recoverable and the parser didn't have any particular
179    /// reason.
180    pub reason: Option<Reason>,
181    /// The source code to which the error points.
182    pub src: Cow<'a, str>,
183}
184
185impl<Reason: Display> Display for FullParsingError<'_, Reason> {
186    fn fmt(&self, f: &mut Formatter) -> core::fmt::Result {
187        if let Some(reason) = &self.reason {
188            writeln!(f, "{reason}")?;
189        }
190        writeln!(f, "--> {}", self.loc)?;
191        let line = self.src
192            .lines()
193            .nth(self.loc.loc.line.get() as usize - 1)
194            .ok_or(core::fmt::Error)?;
195        let line_col_off = self.loc.loc.line.ilog10() as usize + 1;
196        writeln!(f, "{:line_col_off$} |", "")?;
197        writeln!(f, "{:line_col_off$} | {line}", self.loc.loc.line)?;
198        write!(
199            f,
200            "{:line_col_off$} | {:>2$}",
201            "",
202            '^',
203            self.loc.loc.col as usize + 1
204        )?;
205        Ok(())
206    }
207}
208
209impl<Reason: Error> Error for FullParsingError<'_, Reason> {}
210
211impl<Reason> FullParsingError<'_, Reason> {
212    /// Unbind the error from the lifetimes by allocating the file path if it hasn't been already.
213    pub fn own(self) -> FullParsingError<'static, Reason> {
214        FullParsingError {
215            loc: self.loc.own(),
216            src: self.src.into_owned().into(),
217            ..self
218        }
219    }
220}
221
222/// The result of a parser.
223pub type ParsingResult<In, T, Reason = Infallible> =
224    core::result::Result<(In, T), ParsingError<In, Reason>>;