Parsing function arguments with a combination of positional and keyword arguments #908
-
|
I'm trying to parse a grammar with functions that take zero or more positional arguments followed by zero or more keyword arguments. Examples: I feel like I've figured out much more difficult parsers than this, but for some reason everything I've tried has ended up with the Here's a minimal reproducible example. This has two parsers that I've tried, one where the two types of arguments are chained with What am I overlooking? #![allow(dead_code)]
use chumsky::extra::Err;
use chumsky::prelude::*;
fn simple_parser<'a>()
-> impl Parser<'a, &'a str, (&'a str, Vec<&'a str>, Vec<(&'a str, &'a str)>), Err<Rich<'a, char>>> {
just("fn")
.padded()
.ignore_then(text::ascii::ident())
.then_ignore(just('('))
.then(
text::ascii::ident()
.separated_by(just(", "))
.collect::<Vec<_>>(),
)
.then_ignore(just(',').or_not())
.then(
text::ascii::ident()
.then_ignore(just(": "))
.then(text::ascii::ident())
.separated_by(just(", "))
.collect::<Vec<_>>(),
)
.then_ignore(just(')'))
.map(|((name, args), kwargs)| (name, args, kwargs))
}
#[derive(Debug, PartialEq)]
enum Args<'a> {
Pos(Vec<&'a str>),
Kw(Vec<(&'a str, &'a str)>),
Both(Vec<&'a str>, Vec<(&'a str, &'a str)>),
}
fn choice_parser<'a>()
-> impl Parser<'a, &'a str, (&'a str, Args<'a>), Err<Rich<'a, char>>> {
let pos = text::ascii::ident()
.separated_by(just(", "))
.collect::<Vec<_>>();
let kw = text::ascii::ident()
.then_ignore(just(": "))
.then(text::ascii::ident())
.separated_by(just(", "))
.collect::<Vec<_>>();
just("fn")
.padded()
.ignore_then(text::ascii::ident())
.then_ignore(just('('))
.then(choice((
pos.clone().map(Args::Pos),
kw.clone().map(Args::Kw),
pos.then_ignore(just(", ")).then(kw).map(|(pos, kw)| Args::Both(pos, kw)),
)))
.then_ignore(just(')'))
}
#[cfg(test)]
mod simple_parser_tests {
use super::*;
#[test]
fn no_args() {
assert_eq!(
simple_parser().parse("fn a()").into_result(),
Ok(("a", vec![], vec![]))
);
}
#[test]
fn one_arg() {
assert_eq!(
simple_parser().parse("fn a(b)").into_result(),
Ok(("a", vec!["b"], vec![]))
);
}
#[test]
fn two_args() {
assert_eq!(
simple_parser().parse("fn a(b, c)").into_result(),
Ok(("a", vec!["b", "c"], vec![]))
);
}
#[test]
fn one_kwarg() {
assert_eq!(
simple_parser().parse("fn a(b: c)").into_result(),
Ok(("a", vec![], vec![("b", "c")]))
);
}
#[test]
fn two_kwargs() {
assert_eq!(
simple_parser().parse("fn a(b: c, d: e)").into_result(),
Ok(("a", vec![], vec![("b", "c"), ("d", "e")]))
);
}
#[test]
fn both_arg_types() {
assert_eq!(
simple_parser().parse("fn a(b, c, d: e, f: g)").into_result(),
Ok(("a", vec!["b", "c"], vec![("d", "e"), ("f", "g")]))
);
}
}
#[cfg(test)]
mod choice_parser_tests {
use super::*;
#[test]
fn no_args() {
assert_eq!(
choice_parser().parse("fn a()").into_result(),
Ok(("a", Args::Pos(vec![])))
);
}
#[test]
fn one_arg() {
assert_eq!(
choice_parser().parse("fn a(b)").into_result(),
Ok(("a", Args::Pos(vec!["b"])))
);
}
#[test]
fn two_args() {
assert_eq!(
choice_parser().parse("fn a(b, c)").into_result(),
Ok(("a", Args::Pos(vec!["b", "c"])))
);
}
#[test]
fn one_kwarg() {
assert_eq!(
choice_parser().parse("fn a(b: c)").into_result(),
Ok(("a", Args::Kw(vec![("b", "c")])))
);
}
#[test]
fn two_kwargs() {
assert_eq!(
choice_parser().parse("fn a(b: c, d: e)").into_result(),
Ok(("a", Args::Kw(vec![("b", "c"), ("d", "e")])))
);
}
#[test]
fn both_arg_types() {
assert_eq!(
choice_parser().parse("fn a(b, c, d: e, f: g)").into_result(),
Ok(("a", Args::Both(vec!["b", "c"], vec![("d", "e"), ("f", "g")])))
);
}
} |
Beta Was this translation helpful? Give feedback.
Replies: 1 comment 12 replies
-
|
I'm a bit confused as to why you're bifurcating your argument parser when you want to accept both positional and keyword args in the same set of arguments. Perhaps something like this might suit you better? // ---- Types
enum Arg<'src> {
Pos(&'src str),
Kw(&'src str, &'src str),
}
struct Args<'src>(Vec<Arg<'src>);
// ---- In the parser
let pos = text::ascii::ident().map(Arg::Pos);
let kw = text::ascii::ident()
.then_ignore(just(": "))
.then(text::ascii::ident())
.map(|(lhs, rhs)| Arg::Kw(lhs, rhs));
just("fn")
.padded()
.ignore_then(text::ascii::ident())
.then_ignore(just('('))
.then(choice((kw, pos))
.separated_by(", ")
.collect()
.map(Args))
.then_ignore(just(')'))Just as an additional side note: you might want to start making use of |
Beta Was this translation helpful? Give feedback.
It can still be done with what you are after, though you lose the benefit of
.validate(..)allowing parsing to continue cleanly if an error occurs.Description
While parsing the args, chumsky has 3 choices:
I'm going to try and explain how this happens in chumsky. If something is unclear in any or the steps, do let me know.
With that said, let's try each option:
both
So, it begins to parse
bothby parsing the list of positional args. However, due to the shared prefix chumsky parses the first identifier ofkwas apos. It attempts again to parse apos. But because the next token:is neither anident()nor a,it stops parsing the list ofpos. It then attempts to parse a,wh…