Skip to main content

chumsky/
recovery.rs

1//! Types and functions that relate to error recovery.
2//!
3//! When chumsky encounters an erroneous input that it cannot parse, it can be told to attempt to recover from the
4//! error using a variety of strategies (you can also create your own strategies).
5//!
6//! There is no silver bullet strategy for error recovery. By definition, if the input to a parser is invalid then the
7//! parser can only make educated guesses as to the meaning of the input. Different recovery strategies will work
8//! better for different languages, and for different patterns within those languages.
9//!
10//! Chumsky provides a variety of recovery strategies (each implementing the `Strategy` trait), but it's important to
11//! understand that all of
12//!
13//! - which you apply
14//! - where you apply them
15//! - what order you apply them
16//!
17//! will greatly affect the quality of the errors that Chumsky is able to produce, along with the extent to which it
18//! is able to recover a useful AST. Where possible, you should attempt more 'specific' recovery strategies first
19//! rather than those that mindlessly skip large swathes of the input.
20//!
21//! It is recommended that you experiment with applying different strategies in different situations and at different
22//! levels of the parser to find a configuration that you are happy with. If none of the provided error recovery
23//! strategies cover the specific pattern you wish to catch, you can even create your own by digging into Chumsky's
24//! internals and implementing your own strategies! If you come up with a useful strategy, feel free to open a PR
25//! against the [main repository](https://codeberg.org/zesterer/chumsky/)!
26
27use super::*;
28
29/// A trait implemented by error recovery strategies. See [`Parser::recover_with`].
30///
31/// This trait is sealed and so cannot be implemented by other crates because it has an unstable API. This may
32/// eventually change. For now, if you wish to implement a new strategy, consider using [`via_parser`] or
33/// [opening an issue/PR](https://github.com/zesterer/chumsky/issues/new).
34pub trait Strategy<'src, I: Input<'src>, O, E: ParserExtra<'src, I> = extra::Default>:
35    Sealed
36{
37    // Attempt to recover from a parsing failure.
38    // The strategy should properly handle the alt error but is not required to handle rewinding.
39    #[doc(hidden)]
40    fn recover<M: Mode, P: Parser<'src, I, O, E>>(
41        &self,
42        inp: &mut InputRef<'src, '_, I, E>,
43        parser: &P,
44    ) -> PResult<M, O>;
45}
46
47/// See [`via_parser`].
48#[derive(Copy, Clone)]
49pub struct ViaParser<A>(A);
50
51/// Recover via the given recovery parser.
52pub fn via_parser<A>(parser: A) -> ViaParser<A> {
53    ViaParser(parser)
54}
55
56impl<A> Sealed for ViaParser<A> {}
57impl<'src, I, O, E, A> Strategy<'src, I, O, E> for ViaParser<A>
58where
59    I: Input<'src>,
60    A: Parser<'src, I, O, E>,
61    E: ParserExtra<'src, I>,
62{
63    fn recover<M: Mode, P: Parser<'src, I, O, E>>(
64        &self,
65        inp: &mut InputRef<'src, '_, I, E>,
66        _parser: &P,
67    ) -> PResult<M, O> {
68        let alt = inp.take_alt().unwrap(); // Can't fail!
69        let out = match self.0.go::<M>(inp) {
70            Ok(out) => out,
71            Err(()) => {
72                inp.errors.alt = Some(alt);
73                return Err(());
74            }
75        };
76        inp.emit(alt.err);
77        Ok(out)
78    }
79}
80
81/// See [`Parser::recover_with`].
82#[derive(Copy, Clone)]
83pub struct RecoverWith<A, S> {
84    pub(crate) parser: A,
85    pub(crate) strategy: S,
86}
87
88impl<'src, I, O, E, A, S> Parser<'src, I, O, E> for RecoverWith<A, S>
89where
90    I: Input<'src>,
91    E: ParserExtra<'src, I>,
92    A: Parser<'src, I, O, E>,
93    S: Strategy<'src, I, O, E>,
94{
95    #[doc(hidden)]
96    #[cfg(feature = "debug")]
97    fn node_info(&self, scope: &mut debug::NodeScope) -> debug::NodeInfo {
98        self.parser.node_info(scope)
99    }
100
101    fn go<M: Mode>(&self, inp: &mut InputRef<'src, '_, I, E>) -> PResult<M, O> {
102        let before = inp.save();
103        match self.parser.go::<M>(inp) {
104            Ok(out) => Ok(out),
105            Err(()) => {
106                inp.rewind(before.clone());
107                match self.strategy.recover::<M, _>(inp, &self.parser) {
108                    Ok(out) => Ok(out),
109                    Err(()) => {
110                        // Reset to before fallback attempt
111                        inp.rewind(before);
112                        Err(())
113                    }
114                }
115            }
116        }
117    }
118
119    go_extra!(O);
120}
121
122/// See [`skip_then_retry_until`].
123#[must_use]
124#[derive(Copy, Clone)]
125pub struct SkipThenRetryUntil<S, U> {
126    skip: S,
127    until: U,
128}
129
130impl<S, U> Sealed for SkipThenRetryUntil<S, U> {}
131impl<'src, I, O, E, S, U> Strategy<'src, I, O, E> for SkipThenRetryUntil<S, U>
132where
133    I: Input<'src>,
134    S: Parser<'src, I, (), E>,
135    U: Parser<'src, I, (), E>,
136    E: ParserExtra<'src, I>,
137{
138    fn recover<M: Mode, P: Parser<'src, I, O, E>>(
139        &self,
140        inp: &mut InputRef<'src, '_, I, E>,
141        parser: &P,
142    ) -> PResult<M, O> {
143        let alt = inp.take_alt().unwrap(); // Can't fail!
144        loop {
145            let before = inp.save();
146            if let Ok(()) = self.until.go::<Check>(inp) {
147                inp.errors.alt = Some(alt);
148                inp.rewind(before);
149                break Err(());
150            } else {
151                inp.rewind(before);
152            }
153
154            if let Err(()) = self.skip.go::<Check>(inp) {
155                inp.errors.alt = Some(alt);
156                break Err(());
157            }
158
159            let before = inp.save();
160            if let Some(out) = parser.go::<M>(inp).ok().filter(|_| {
161                inp.errors
162                    .secondary_errors_since(before.err_count)
163                    .is_empty()
164            }) {
165                inp.emit(alt.err);
166                break Ok(out);
167            } else {
168                inp.errors.alt.take();
169                inp.rewind(before);
170            }
171        }
172    }
173}
174
175/// TODO
176pub fn skip_then_retry_until<S, U>(skip: S, until: U) -> SkipThenRetryUntil<S, U> {
177    SkipThenRetryUntil { skip, until }
178}
179
180/// See [`skip_until`].
181#[must_use]
182#[derive(Copy, Clone)]
183pub struct SkipUntil<S, U, F> {
184    skip: S,
185    until: U,
186    fallback: F,
187}
188
189impl<S, U, F> Sealed for SkipUntil<S, U, F> {}
190impl<'src, I, O, E, S, U, F> Strategy<'src, I, O, E> for SkipUntil<S, U, F>
191where
192    I: Input<'src>,
193    S: Parser<'src, I, (), E>,
194    U: Parser<'src, I, (), E>,
195    F: Fn() -> O,
196    E: ParserExtra<'src, I>,
197{
198    fn recover<M: Mode, P: Parser<'src, I, O, E>>(
199        &self,
200        inp: &mut InputRef<'src, '_, I, E>,
201        _parser: &P,
202    ) -> PResult<M, O> {
203        let alt = inp.take_alt().unwrap(); // Can't fail!
204        loop {
205            let before = inp.save();
206            if let Ok(()) = self.until.go::<Check>(inp) {
207                inp.emit(alt.err);
208                break Ok(M::bind(|| (self.fallback)()));
209            }
210            inp.rewind(before);
211
212            if let Err(()) = self.skip.go::<Check>(inp) {
213                inp.errors.alt = Some(alt);
214                break Err(());
215            }
216        }
217    }
218}
219
220/// A recovery parser that skips input until one of several inputs is found.
221///
222/// This strategy is very 'stupid' and can result in very poor error generation in some languages. Place this strategy
223/// after others as a last resort, and be careful about over-using it.
224pub fn skip_until<S, U, F>(skip: S, until: U, fallback: F) -> SkipUntil<S, U, F> {
225    SkipUntil {
226        skip,
227        until,
228        fallback,
229    }
230}
231
232/// A recovery parser that searches for a start and end delimiter, respecting nesting.
233///
234/// It is possible to specify additional delimiter pairs that are valid in the pattern's context for better errors. For
235/// example, you might want to also specify `[('[', ']'), ('{', '}')]` when recovering a parenthesized expression as
236/// this can aid in detecting delimiter mismatches.
237///
238/// A function that generates a fallback output on recovery is also required.
239// TODO: Make this a strategy, add an unclosed_delimiter error
240pub fn nested_delimiters<'src, 'parse, I, O, E, F, const N: usize>(
241    start: I::Token,
242    end: I::Token,
243    others: [(I::Token, I::Token); N],
244    fallback: F,
245) -> impl Parser<'src, I, O, E> + Clone + 'parse
246where
247    I: ValueInput<'src>,
248    I::Token: PartialEq + Clone,
249    E: extra::ParserExtra<'src, I> + 'parse,
250    'src: 'parse,
251    F: Fn(I::Span) -> O + Clone + 'parse,
252{
253    // TODO: Does this actually work? TESTS!
254    #[allow(clippy::tuple_array_conversions)]
255    // Clippy is overly eager to fine pointless non-problems
256    recursive({
257        let (start, end) = (start.clone(), end.clone());
258        |block| {
259            let mut many_block = Parser::boxed(
260                block
261                    .clone()
262                    .delimited_by(just(start.clone()), just(end.clone())),
263            );
264            for (s, e) in &others {
265                many_block = Parser::boxed(
266                    many_block.or(block.clone().delimited_by(just(s.clone()), just(e.clone()))),
267                );
268            }
269
270            let skip = [start, end]
271                .into_iter()
272                .chain(IntoIterator::into_iter(others).flat_map(|(s, e)| [s, e]))
273                .collect::<Vec<_>>();
274
275            many_block
276                .or(any().and_is(none_of(skip)).ignored())
277                .repeated()
278        }
279    })
280    .delimited_by(just(start), just(end))
281    .map_with(move |_, e| fallback(e.span()))
282}