mattmower

mattmower

Parser Combinators how to know when many should return an error

I’ve rolled my own parser combinator library, Ergo, following the work of particularly Saśa Jurić, and it’s got to the point where it’s good enough to do real work. There are still corners and at least a couple of performance issues but the thing is solid enough.

Now I’ve hit a snag that I hope someone more familiar with parser combinators has dealt with before and can shine a light on the path ahead.

What it boils down to is this:

The many(p) combinator matches p zero or more times.p returning an error is the signal to many that its work is done, so many always return success to its parent parser.

I’ve identified two different situations in which this typically occurs:

  1. The legitimate end of a sequence: p should not match
  2. Early termination due to an invalid input: p should have matched

In the second case when many rewinds the input its leaving it in a state that the following parsers will not expect and parsing will likely fail but not at the real source of the problem.

I’ll try and clarify. If the input was “abcbcbcd” to a parser:

sequence([
  char(?a),
  many(
    sequence([
      char(?b),
      char(?c),
    ])
  ),
  char(?d)
])

The many would consume the “bc” pairs until it reached the “d”, i.e. char(?b) returns an error. At which point sequence parsing would fail and the many would return ok with the “bc” pairs it had matched as an AST. The input would be rewound to “d” and parsing would continue with char(?d) which then matches.

Now let’s imagine the user erroneously writes “abcbbcd” as their input, they forgot one of those "c"s. Now the sequence parsing “bc” would fail when it hit a “b” while expecting a “c”. The many now fails and rewinds the input to “b”.

Now char(?d) attempts to match “b” and fails.

Of course, we expect a failure but the parser appears to be failing at char(?d) with “unexpected character: b expecting d” whereas the real problem is back inside the many and should have failed with “unexpected character: b expecting c”. This makes debugging or reporting errors to the user difficult.

The problem is that many is unable to disambiguate its child parser p failing due to “end of sequence” vs. p failing on an input that should be parsed there.

I’ve written a longer post about this that has a more realistic example.

My current thinking is to add a meta-parser called something like commit that says “when we reach this point we’ve got something we should be able to parse. If this fails it’s not an end of sequence but a real error” and somehow many then knows to return the right error at the right point.

At this point, it’s probably worth mentioning that Ergo passes around a %Context{} structure that holds the remaining input and maintains line, column, and parsing status. A hypothetical commit parser could be setting a flag in the context.

I’d welcome any insight into this situation, thank you.

Matt

Most Liked

sasajuric

sasajuric

Author of Elixir In Action

We have encountered the same issue when developing an SQL parser. Basically, my takeaway was that parsers such as many and choice are mostly useless if we want to produce a precise error report. Perhaps there is a way to make it work, I’m not super familiar with various parsing techniques, so I’ll explain what we did instead.

We had multiple variations of the problem you describe here, e.g. when parsing arithmetic expressions, or handling joins and subqueries. Our strategy was to abandon many and choice. Instead, we would “commit” to a parsing path based on the next token. In your example grammar, if the next token is b, we’ll commit to parsing the pair bc. If that fails, we’ll emit an error. If that succeeds, we’ll try to parse another bc if the next token is b. I believe that this technique is called “lookahead”.

To make it work, we introduced two additional parsers. The most important one is called switch. Let’s see an example:

switch([
  {char(?b), char(?c)},
  {char(?d), char(?e)},
])

This will parse bc or de, returning a proper error if the second char is invalid. Basically the switch parser commits to the given “branch” if the first parser in some tuple succeeds. If the rest of that branch errors, that error is returned by the switch parser. If no branch can be selected, the parser will return a generic error (similar to choice). If the parser succeeds, it will emit {result_of_the_first_parser, result_of_the_second_parser}

Now in your example we don’t want to return an error if no branch can be selected. Instead, we want to just stop parsing. To make this happen, we added the support for the :else clause, and introduced another parser called noop which always succeeds consuming nothing from the input. So parsing bc or nothing can be now expressed as:

switch([
  {char(?b), char(?c)},
  {:else, noop()}
])

And now, we can parse zero or more bc pairs as:

defp bc_pairs do
  switch([
    {char(?b), sequence([char(?c), lazy(&bc_pairs/0)])},
    {:else, noop()}
  ])
end

The emitted term will be a bit weird, something like:

{?b, [?c, {?b, [?c, nil]}]}

The final nil is emitted by the noop parser. This can be transformed into a list (e.g. ["bc", "bc", ...]) with the map parser.

I’m not sure if there’s a more elegant way, but this is what we did, and it worked fine. The code was definitely more complicated than the naive approach with many, but it was still understandable. Most importantly, our error reports were much more precise and informative.

For completness, we also wrote a parser called choice_deepest_error, which would work like a choice (aka one_of):

choice_deepest_error([
  parser1(),
  parser2(),
  ...
])

This parser emits either the result of parser1, parser2, etc. If no parser succeeds, it returns the error from the parser that consumed most of the input. I personally wasn’t a fan of this approach, because I found it hard to reason about. The “deepest error” isn’t necessarily the correct error. So personally I prefer the lookahead technique.

LostKobrakai

LostKobrakai

So many() being “zero or more” essentially means this is an optional match. Your goal seems to be making it clear that either parsing ?d was the issue if many() was indeed meant to be empty, or parsing the many() step failed for some reason. So how about:

either(
  sequence([
    many(
      sequence([
        char(?b),
        char(?c),
      ])
    ),
    char(?d)
  ]),
  char(?d)
)

Which will communicate the two different states explicitly to the parser. If both branches fail the error can communicate both expectations instead of just one.

mattmower

mattmower

Thanks for thinking it through.

It’s possible that I am missing something in what you’re saying — I feel I am quite close to the problem and that has its own challenges but it seems to me that the focus on the zero-or-more semantics of many here is possibly a misdirect.

If we imagine a parser like:

program = many(elixir_statement)
elxir_statement = choice([case_expr, for_expr, def_expr, with_expr, …])
case_expr = sequence([literal("case"), expr, literal("do"), …, literal("end")])
# and so on

if the input is

case x# do … end

then the case_expr will fail but it’s not the case that for_expr, with_expr, def_expr or anything could match. From the moment it parsed "case " there was no other valid option and something is wrong with the input.

So the case_expr fails, causing the choice to fail and that rolls up into the many which expects its child parser to fail at some point because it treats that as “end of sequence” and then it returns success.

My problem is how to differentiate between the child parser of many failing in the expected “end of sequence” case or the unexpected “malformed choice” case because the error is different in each.

It seems Saša & co. didn’t attempt to solve this but rather worked around it through writing parsers differently.

Looking at your suggestion it seems to me we don’t want to attempt char(?d) because the input is invalid and char(?d) cannot match. It might be that some of these errors could be recovered to continue parsing (in my example of a # in an expression we could clearly attempt to continue parsing since the structure is sound but in other cases, it may not be). What I am trying to do is report the right error (i.e. we need to bubble an error out of many when that’s not its typical behaviour).

Thanks.

Matt

Where Next?

Popular in Questions Top

aadeshere1
I have a another noob question about loop. Since elixir is immutable, while loop is not directly possible. total = 10 while total != 0 ...
New
vertexbuffer
Hello, can anybody help here..? I have a list of players and I what to delete an element, but every for loop the list is reverting to ori...
New
greenz1
I have a phoenix application from which a user can download multiple(5-6) files of size 1MB. I couldn’t find anything related to sending ...
New
fireproofsocks
I’m working on defining a simple Ecto schema for a table (in PostGres), but I don’t see where I can define a column as NOT NULL. Conside...
New
ovidiubadita
Hey all, I discovered Elixir and I love it. I always wanted to learn a functional programming and I intended to go for Haskell, but afte...
New
LegitStack
I’m trying to make a websocket server in Phoenix or raw Elixir. I heard about gun, I think I could use cowboy, but since I’m not that sma...
New
Lily
In templates/appointment/index.html.eex: <%= for appointment <- @appointments do %> <tr> <td><%= appoi...
New
vegabook
I’m brand new to Phoenix and I have stripped one of the demo applications to the bone. I just want to get an svg up on the screen. Here i...
New
script
If I have a string “1000 cfu/ml” . I want to remove the characters and / and space . So the string is like this "1000" What is the ...
New
jononomo
For some reason my phoenix channels are working for me in my local dev environment, but as soon as I deploy via Docker, I get a 403 error...
New

Other popular topics Top

JakeBecker
TL;DR: I’ve just released an implementation of Microsoft’s IDE-independent Language Server Protocol for Elixir. It adds language support ...
1144 53690 245
New
hariharasudhan94
lets say i have a sample like a = 20; b = 10; if (a > b) do {:ok, "a"} end if (a < b) do {:ok, b} end if (a == b) do {:ok, "equa...
New
AngeloChecked
What learn first? Rust or Elixir Hi Elixir community! I’m here because i want learn a new language. I’m a junior developer and mainly i ...
New
baxterw3b
Hi guys, i’m new in the Elixir world, and i have to say, that i love it! i’m having some problem to understand anonymous functions with ...
New
SoCreat
i’m a new one to elixir which editor can i use vs code? or atom? Thanks! :smiley:
New
ashish173
I am using Ecto timestamps with postgres, I can see the timestamps() use the :naive_dateime but for my use case I wanted to store the ti...
New
romenigld
I am trying to run a deploy with docker and I successfully runned with this command: docker build -t romenigld/blog-prod . but when I t...
New
hariharasudhan94
I would like to know what is the best IDE for elixir development?
New
jononomo
For some reason my phoenix channels are working for me in my local dev environment, but as soon as I deploy via Docker, I get a 403 error...
New
svb
Hi! Currently I want to submit a form by pressing the Enter key. However, since my input field is of type “textarea” this is just adds a...
New

We're in Beta

About us Mission Statement