Skip to content
This repository was archived by the owner on Apr 2, 2026. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions guide/meet_the_parsers.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,13 +94,14 @@ Combinators that manipulate or emit errors, along with fallibly validating parse
| Name | Example | Description |
|---------------------------------|---------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| [`Parser::map_err`] | `a.map_err(...)` | Parse a pattern. On failure, map the parser error to another value. Often used to customise error messages or add extra information to them. |
| [`Parser::map_err_with_state`] | `a.lazy()` | Like [`Parser::map_err`], but provides access to the parser state (see [`Parser::parse_with_state`] for more information). |
| [`Parser::map_err_with`] | `a.map_err_with(...)` | Like [`Parser::map_err`], but provides access to metadata associated with the output. |
| [`Parser::map_err_with_state`] | `a.map_err_with_state(...)` | Like [`Parser::map_err`], but provides access to the parser state (see [`Parser::parse_with_state`] for more information). |
| [`Parser::try_foldl`] | `a.try_foldl(...)` | Left-fold the output of the parser into a single value, possibly failing during the reduction. If the function produces an error, the parser fails with that error. |
| [`Parser::try_map`] | `a.try_map(...)` | Map the output of a parser using the given fallible mapping function. If the function produces an error, the parser fails with that error. |
| [`Parser::try_map_with`] | `a.try_map_with(...)` | Map the output of a parser using the given fallible mapping function, with access to output metadata. If the function produces an error, the parser fails with that error. |
| [`Parser::validate`] | `a.validate(...)` | Parse a pattern. On success, map the output to another value with the opportunity to emit extra secondary errors. Commonly used to check the validity of patterns in the parser. |
| [`Parser::filter`] | `any().filter(char::is_lowercase)` | Parse a pattern and apply the given filtering function to the output. If the filter function returns [`false`], the parser fails. |
| [`Parser::filter_map`] | `any().filter_map(...) | Parse a pattern and apply the given filter-map function to the output. If the filter-map function returns [`None`], the parser fails. |
| [`Parser::filter_map`] | `any().filter_map(...)` | Parse a pattern and apply the given filter-map function to the output. If the filter-map function returns [`None`], the parser fails. |
| [`Parser::labelled`] | `a.labelled("a")` | Parse a pattern, labelling it. What exactly this does depends on the error type, but it is generally used to give a pattern a more general name (for example, "expression"). |

### Text-oriented parsing
Expand Down
47 changes: 45 additions & 2 deletions src/combinator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3149,7 +3149,7 @@ where
Self: Sized,
{
(&self.parser)
.map_err_with_state(|e, _, _| (self.mapper)(e))
.map_err_with(|e, _| (self.mapper)(e))
.go::<M>(inp)
}

Expand Down Expand Up @@ -3191,7 +3191,6 @@ where
// go_extra!(O);
// }

// TODO: Remove combinator, replace with map_err_with
/// See [`Parser::map_err_with_state`].
#[derive(Copy, Clone)]
pub struct MapErrWithState<A, F> {
Expand Down Expand Up @@ -3237,6 +3236,50 @@ where
go_extra!(O);
}

/// See [`Parser::map_err_with`].
#[derive(Copy, Clone)]
pub struct MapErrWith<A, F> {
pub(crate) parser: A,
pub(crate) mapper: F,
}

impl<'src, I, O, E, A, F> Parser<'src, I, O, E> for MapErrWith<A, F>
where
I: Input<'src>,
E: ParserExtra<'src, I>,
A: Parser<'src, I, O, E>,
F: Fn(E::Error, &mut MapExtra<'src, '_, I, E>) -> E::Error,
{
#[inline(always)]
fn go<M: Mode>(&self, inp: &mut InputRef<'src, '_, I, E>) -> PResult<M, O>
where
Self: Sized,
{
let start = inp.cursor();
let old_alt = inp.take_alt();
let res = self.parser.go::<M>(inp);
let new_alt = inp.take_alt();

if res.is_ok() {
inp.errors.alt = old_alt;
if let Some(new_alt) = new_alt {
inp.add_alt_err(&new_alt.pos, new_alt.err);
}
} else {
// Can't fail!
let mut new_alt = new_alt.unwrap();
new_alt.err = (self.mapper)(new_alt.err, &mut MapExtra::new(&start, inp));

inp.errors.alt = old_alt;
inp.add_alt_err(&new_alt.pos, new_alt.err);
}

res
}

go_extra!(O);
}

/// See [`Parser::validate`]
pub struct Validate<A, OA, F> {
pub(crate) parser: A,
Expand Down
53 changes: 53 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1997,6 +1997,29 @@ pub trait Parser<'src, I: Input<'src>, O, E: ParserExtra<'src, I> = extra::Defau
}
}

/// Map the primary error of this parser to another value, making use of the parser state and
/// context.
///
/// This function is useful for augmenting errors to allow them to include context in non context-free
/// languages, or provide contextual notes on possible causes.
///
/// The output type of this parser is `O`, the same as the original parser.
///
Comment on lines +2000 to +2007

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While I think this change is a positive, I am a bit worried about users relying on things like MapExtra::slice, MapExtra::span, etc. Chumsky deliberately permits parsers to leave the input in an unspecified state after an error occurs and backtracking begins (it's up to calling parsers to explicitly rewind the input if they need recovery). As such, I think we should explicitly document that these functions may not produce the output a user might expect.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, while implementing this I was wondering what to provide to the closure. So I think you're saying that all of MapExtra is too much because some of it doesn't make much sense in case of an error?

Just span (map_error_with_state gets this at the moment though maybe computed differently), state and context then? Should it be a new struct or just passed individually to the closure? If it's the latter, this new function would be basically like the existing map_error_with_state but with one additional argument.

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm honestly not sure! For now, I think the easiest approach (and most consistent with the rest of the crate) is to stick with MapExtra, but document that the output of MapExtra::span and MapExtra::slice are unspecified. This certainly warrants further thought, but I don't think it should block this PR. I'll merge when the change is made to the docs, thanks.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added a warning. Let me know if I should change the wording.

Should I also add a deprecation note for map_err_with_state()?

/// Note: Chumsky permits parsers to leave the input in an unspecified state after an error
/// occurs and backtracking begins. As such, the output of [`MapExtra::span`] and
/// [`MapExtra::slice`] is unspecified within this function.
///
fn map_err_with<F>(self, f: F) -> MapErrWith<Self, F>
where
Self: Sized,
F: Fn(E::Error, &mut MapExtra<'src, '_, I, E>) -> E::Error,
{
MapErrWith {
parser: self,
mapper: f,
}
}

/// Validate an output, producing non-terminal errors if it does not fulfill certain criteria.
/// The errors will not immediately halt parsing on this path, but instead it will continue,
/// potentially emitting one or more other errors, only failing after the pattern has otherwise
Expand Down Expand Up @@ -4018,6 +4041,36 @@ mod tests {
);
}

#[test]
fn map_err_with() {
use crate::LabelError;

let parser = just::<char, &str, extra::Err<_>>('#')
.repeated()
.count()
.ignore_with_ctx(just('"').map_err_with(move |e: Rich<char>, extras| {
println!("Found = {:?}", e.found());
println!("Expected = {:?}", e.expected().collect::<Vec<_>>());
println!("Span = {:?}", e.span());
println!("Context = {:?}", extras.ctx());
LabelError::<&str, String>::expected_found(
[format!("after {} hashes", extras.ctx())],
e.found().copied().map(Into::into),
*e.span(),
)
}));

let mut err: Rich<_> =
LabelError::<&str, char>::expected_found(['#'], Some('H'.into()), (3..4).into());
err = LabelError::<&str, String>::merge_expected_found(
err,
["after 3 hashes".into()],
Some('H'.into()),
(3..4).into(),
);
assert_eq!(parser.parse("###H").into_result(), Err(vec![err]));
}

#[test]
fn try_map() {
use crate::{DefaultExpected, LabelError};
Expand Down