petitparser / dart-petitparser

Dynamic parser combinators in Dart.
https://pub.dartlang.org/packages/petitparser
MIT License
457 stars 48 forks source link

Alternative to trim parser or how to discard #123

Closed DanielCardonaRojas closed 1 year ago

DanielCardonaRojas commented 2 years ago

First of all, thanks for this awesome package.

I've been playing around with this in a hobby project for a day or two and have been wanting to have a more concise way of expressing some parsers, I've been looking through most combinators but might be missing something please point me in the right direction in case that's true.

So lets use a simple example (not my case but just to explain) lets say I want to parse a number between parens here are some way to accomplish it:

    // Version 1
    final Parser<int> parserV1 = char('(') & digit() & char(')').map((values) => int.parse(values[1]));
    // Version 2
    final Parser<int> parserV2 = digit().trim(char('('), char(')')).map((value) => int.parse(value));

With version1 you have to keep track of the index to know which value to extract from values and to correctly map the parser, so not the best solution.

version 2 works perfectly but has a default parser for left and right using whitespace()

What I would like to accomplish is something like this:

    // Desired
    final Parser<int> parserV3 = char('(').dicard() & digit().map((value) => int.parse(value)) & char(')').dicard();

Someway of telling the parser consume input throwing it away completely, so that when composed in sequence it won't accumulate in the result list.

If that's not possible then would be great to have individual TrimLeft and TrimRight parsers.

Is there an extension I'm missing ?

renggli commented 2 years ago

Thank you for the kind words and the question with the great examples.

This is not something that is provided out of the box from this package. I don't think trim is a good stand-in here, because it consumes the opening and closing params an arbitrary amount of times. Furthermore, it also passes if the before and after characters are entirely absent.

Probably the easiest solution that comes closest to your version 2 would be to add a helper extension, like so:

extension SurroundedParserExtension<T> on Parser<T> {
  Parser<T> surroundedBy(Parser<void> left, [Parser<void>? right]) =>
      [left, this, right ?? left].toSequenceParser().pick(1).cast<T>();
}

final parserV2 = digit().map(int.parse).surroundedBy(char('('), char(')'));

You can copy that extension to your own code. We can also consider adding it to the package, if people think it is generally useful? While it obviously simplifies the creation of some quick parsers, I am afraid that in practice it might not be that useful (often exact positions of characters surrounding some other expression need to be known, i.e. for a syntax highlighter).

Something like version 3 could be implemented, but there are various complications that make it tricky: the typing in Dart doesn't work in your example, and you would still need to somehow unwrap the resulting element from a list and cast it to the right type. Also parser transformations (and optimizations) might become more complicated. Still could be worth to investigate.

DanielCardonaRojas commented 2 years ago

Hey @renggli thanks for the quick reply, playing around a bit more I found this alternative which is more declarative.

Sadly Dart doesn't seem to support generic params for operators and no way of specifying associativity and precedence for them either if I'm not mistaken.

extension SurroundParserExtension<T> on Parser<T> {
  Parser<dynamic> operator >>(Parser<dynamic> other) =>
      [this, other].toSequenceParser().pick(1);

  Parser<T> operator <<(Parser<void> other) =>
      [this, other].toSequenceParser().pick(0).cast<T>();
}

final Parser<int> num = digit().map((value) => int.parse(value));
final exampleParser = char('(') >> (num << char(')'));
final typedParser = exampleParser.cast<int>();