Skip to content

Commit e818786

Browse files
zeichenreihezeichenreihe
andauthored
add Parser::filter_map (#925)
* add `Parser::filter_map` The code is mostly just the implementation of `Parser::filter`, but taking an `Option<U>` from the condition. * Implement `Filter::go` by using `FilterMap`, add `FilterMap` to parsers guide * format changes using `cargo fmt` --------- Co-authored-by: zeichenreihe <zeichenreihe@noreply.codeberg.org>
1 parent 291b876 commit e818786

File tree

3 files changed

+97
-13
lines changed

3 files changed

+97
-13
lines changed

guide/meet_the_parsers.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,11 +95,12 @@ Combinators that manipulate or emit errors, along with fallibly validating parse
9595
|---------------------------------|---------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
9696
| [`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. |
9797
| [`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). |
98-
| [`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. |
98+
| [`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. |
9999
| [`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. |
100100
| [`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. |
101101
| [`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. |
102102
| [`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. |
103+
| [`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. |
103104
| [`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"). |
104105

105106
### Text-oriented parsing

src/combinator.rs

Lines changed: 57 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -260,6 +260,48 @@ where
260260
debug::NodeInfo::Filter(Box::new(self.parser.node_info(scope)))
261261
}
262262

263+
#[inline(always)]
264+
fn go<M: Mode>(&self, inp: &mut InputRef<'src, '_, I, E>) -> PResult<M, O> {
265+
(&self.parser)
266+
.filter_map(|out| if (self.filter)(&out) { Some(out) } else { None })
267+
.go::<M>(inp)
268+
}
269+
270+
go_extra!(O);
271+
}
272+
273+
/// See [`Parser::filter_map`].
274+
pub struct FilterMap<A, OA, F> {
275+
pub(crate) parser: A,
276+
pub(crate) filter_mapper: F,
277+
#[allow(dead_code)]
278+
pub(crate) phantom: EmptyPhantom<OA>,
279+
}
280+
281+
impl<A: Copy, OA, F: Copy> Copy for FilterMap<A, OA, F> {}
282+
impl<A: Clone, OA, F: Clone> Clone for FilterMap<A, OA, F> {
283+
fn clone(&self) -> Self {
284+
Self {
285+
parser: self.parser.clone(),
286+
filter_mapper: self.filter_mapper.clone(),
287+
phantom: EmptyPhantom::new(),
288+
}
289+
}
290+
}
291+
292+
impl<'src, I, O, E, A, OA, F> Parser<'src, I, O, E> for FilterMap<A, OA, F>
293+
where
294+
I: Input<'src>,
295+
E: ParserExtra<'src, I>,
296+
A: Parser<'src, I, OA, E>,
297+
F: Fn(OA) -> Option<O>,
298+
{
299+
#[doc(hidden)]
300+
#[cfg(feature = "debug")]
301+
fn node_info(&self, scope: &mut debug::NodeScope) -> debug::NodeInfo {
302+
debug::NodeInfo::Filter(Box::new(self.parser.node_info(scope)))
303+
}
304+
263305
#[inline(always)]
264306
fn go<M: Mode>(&self, inp: &mut InputRef<'src, '_, I, E>) -> PResult<M, O> {
265307
let found = inp.peek_maybe();
@@ -274,19 +316,22 @@ where
274316
inp.errors.alt = old_alt;
275317
match res {
276318
Ok(out) => {
277-
if (self.filter)(&out) {
278-
// If successful, reinsert the original alt and then apply the new alt on top of it, since both are valid
279-
if let Some(new_alt) = new_alt {
280-
inp.add_alt_err(&new_alt.pos, new_alt.err);
319+
match (self.filter_mapper)(out) {
320+
Some(mapped) => {
321+
// If successful, reinsert the original alt and then apply the new alt on top of it, since both are valid
322+
if let Some(new_alt) = new_alt {
323+
inp.add_alt_err(&new_alt.pos, new_alt.err);
324+
}
325+
Ok(M::bind(|| mapped))
326+
}
327+
None => {
328+
// If unsuccessful, reinsert the original alt but replace the new alt with the "something else" error (since it overrides it)
329+
let expected = [DefaultExpected::SomethingElse];
330+
// TODO: Use something more detailed than the next token as the found
331+
let err = E::Error::expected_found(expected, found, span);
332+
inp.add_alt_err(&before.inner, err);
333+
Err(())
281334
}
282-
Ok(M::bind(|| out))
283-
} else {
284-
// If unsuccessful, reinsert the original alt but replace the new alt with the "something else" error (since it overrides it)
285-
let expected = [DefaultExpected::SomethingElse];
286-
// TODO: Use something more detailed than the next token as the found
287-
let err = E::Error::expected_found(expected, found, span);
288-
inp.add_alt_err(&before.inner, err);
289-
Err(())
290335
}
291336
}
292337

src/lib.rs

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -520,6 +520,44 @@ pub trait Parser<'src, I: Input<'src>, O, E: ParserExtra<'src, I> = extra::Defau
520520
}
521521
}
522522

523+
/// Filter and map the output of this parser, accepting only inputs that get mapped to `Some`.
524+
///
525+
/// The output type of this parser is `U`.
526+
///
527+
/// # Examples
528+
///
529+
/// ```
530+
/// # use chumsky::{prelude::*, error::Simple};
531+
/// #[derive(Debug, PartialEq)]
532+
/// enum Token {
533+
/// Digit(char), // invariant: .is_ascii_digit()
534+
/// Alpha(char), // invariant: .is_alphabetic()
535+
/// }
536+
///
537+
/// let token = any::<_, extra::Err<Simple<char>>>()
538+
/// .filter_map(|c: char| if c.is_ascii_digit() {
539+
/// Some(Token::Digit(c))
540+
/// } else if c.is_alphabetic() {
541+
/// Some(Token::Alpha(c))
542+
/// } else {
543+
/// None
544+
/// });
545+
///
546+
/// assert_eq!(token.parse("x").into_result(), Ok(Token::Alpha('x')));
547+
/// assert_eq!(token.parse("5").into_result(), Ok(Token::Digit('5')));
548+
/// assert!(token.parse("!").has_errors());
549+
/// ```
550+
fn filter_map<U, F: Fn(O) -> Option<U>>(self, f: F) -> FilterMap<Self, O, F>
551+
where
552+
Self: Sized,
553+
{
554+
FilterMap {
555+
parser: self,
556+
filter_mapper: f,
557+
phantom: EmptyPhantom::new(),
558+
}
559+
}
560+
523561
/// Map the output of this parser to another value.
524562
///
525563
/// The output type of this parser is `U`, the same as the function's output.

0 commit comments

Comments
 (0)