Skip to main content

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