If you think PEG parsers are a good idea and we should use them, comment below which strings this PEG grammar matches:
Str = "a" Str "a" / "a"
If you get it right and you understand the logic, by all means use them. If you get it wrong or can't wrap your head around how it works despite knowing the answer, maybe reconsider.
EDIT: since my point was too subtle and I have been misunderstood and downvoted as a result of it, allow me to give a hint. If you believe the above grammar matches aaaaa you fall in the latter category. It does not. Yes, aaa matches, aaaaaaa matches, but aaaaa does not. Yes, this is mind-boggling.
The right mental model for PEGs is not whether or not it will parse the entire string.
PEGs will attempt to parse some prefix of the string and then stop and leave the cursor there.
Here is a list of strings and how far it will parse (hopefully correct):
Consider your example aaaaa. The PEG will parse the first a, and then the recursive call will parse from a|aaaa.
Now the next recursive call will eventually fail.
Therefore the first recursive call will only parse one a, and then return back to the initial call which will parse another a. So the final state is aaa|aa.
At a high level, the reason for this behaviour is that PEGs are greedy. The inner recursive calls will always parse as much as they can, without regard for the outer calls that still want to parse more a's.
> The PEG will parse the first a, and then the recursive call will parse from a|aaaa. Now the next recursive call will eventually fail.
Thanks for taking the time & effort to explain! I know you're right because I have reproduced this behaviour in 2 parser generators now (pegjs & grako).
Why does the next recursive call fail? (Don't answer, I think I've worked it out.)
Isn't it supposed to backtrack:
1. a|a a a a
2. a a|a a a
3. a a a a a| <- "succeeds" but doesn't leave any more input for the outer parses
3. a a a|a a <- doesn't it backtrack and try the other choice?
2. a a a a|a
1. a a a a a|
I guess it's because the backtracking doesn't happen at recursion level 3 (because it already thinks it succeeded) but at level 2:
1. a|a a a a
2. a a|a a a
3. a a a a a|
2. a a|a a a <- tries the other choice
1. a a a|a a
Let's see if my explanation holds for aaaaaaa:
1. a|a a a a a a
2. a a|a a a a a
3. a a a|a a a a
4. a a a a|a a a
5. a a a a a a a| <- "succeeds" but doesn't leave any more input for the outer parses
4. a a a a|a a a <- tries the other choice
3. a a a a a|a a
2. a a a a a a|a
1. a a a a a a a|
Geez.
Well, now I'm grateful I've never had to implement a parser for anything more complicated than a Lucene-style query syntax.
Yes I think that's right :) I think I have a way to visualize what's happening more clearly than the | notation I used above.
We have grammar: S -> 'a' S 'a' / 'a'
I'm going to put (aSa) around parses of the first alternative and <a> for the second alternative. So a successful parse would look like (a(a<a>a)a).
Here's what the PEG parser will do for aaaaa. It will first recurse all the way down, trying the first alternative at every point:
(a(a(a(a(a
^
The choice indicated by ^ will fail because it is expecting another 'a', so that choice will be backtracked, and it will replace the ( with < at that position:
(a(a(a(a<a>
^
Now it will backtrack over that ^ choice and replace the ( with < at that position:
(a(a(a<a>a)
^
Now comes the key point: the next thing it backtracks over is the second ( and NOT the third (, because the third ( actually succeeded. So it has skipped over one extra layer.
Now it will parse:
(a<a>a)aa
This one succeeded, and consumed 3 characters. The rest of the input is then still to be parsed by productions further up the stack, but since this S is the main production of the grammar, we declare failure because the whole input was not consumed.
In general, after backtracking, a bunch of ('s may initially seem to succeed, and then when it fails it will backtrack over all those, rather than over just one of them.
> Well, now I'm grateful I've never had to implement a parser for anything more complicated than a Lucene-style query syntax.
Interestingly, these problems don't seem to come up in practice. But things like this are still a good argument for LR/GLR parsers: LR parsers will report the conflict at grammar compilation time, and GLR parsers will backtrack with all possible choices, so they will parse the way you'd expect. They are certainly the more principled choice. But in practice it may not matter that much in practice because people don't seem to use these kind of grammars, at least not for programming languages.
Most languages allow writing tortured and hard to understand logic where simpler alternatives can also be written. I'm not sure why that should make people consider not using them.
Because that does not recognize the same set of strings. That matches aaaaa, but my example above does not. That is exactly the point I'm trying to make.
Well, if you wanted all odd-length strings, you'd write what GP wrote. If you wanted whatever it was your grammar accepted, you'd write what you wrote :)
> If you wanted whatever it was your grammar accepted
It is one-line, two-rule PEG grammar. What does it accept, and why? If we can't state this with confidence in such a tiny example, what makes us have any confidence in larger, more complex grammars? We should understand our tools. PEG pretends to be simple and understandable, but it's much less so than it might seem on the surface.
Simple rules can give surprising, complex behaviour.
That's a good point. Unfortunately, it's also true not only of PEGs and recursive-descent parsing, but also of essentially every parsing formalism out there. So it doesn't seem to disqualify PEGs in particular.
(It's also true of programming languages generally: For which inputs does Collatz halt, and why?)
I contest that. The parsers based on the formalism of CFG do not have that problem. The straightforward mental model of the user always matches their behaviour, no complexity, no surprises.
This is a good point in favour of context free grammar based parser generators. I think the trade-off is a bit more subtle though.
CFG semantics is nice and declarative when we specify which strings belong to the language, but a parser is a function from strings to parse results, and then the nice semantics stops working due to ambiguities.
The PEG semantics on the other hand naturally expresses such a function, and the function you get with PEG semantics is almost always the function you want.
When we look at character level grammars, the CFG formalism doesn't work well either. They fundamentally rely on greedy lexical token matching.
Extensions such as indentation sensitive parsing and user defined operators quite naturally fit into PEG style semantics, and are more difficult to integrate into CFGs.
CFG based parser generators are way more complicated internally than PEG based parser generators, which are extremely simple.
This is a good example of a theoretical problem with PEGs. It also applies (of course) to recursive descent parsers. I don't think this problem is a practical issue, though; I can't speak for others, but this kind of problem hasn't come up in practice for me. PEGs do cause other headaches from time to time, but are still head-and-shoulders easier to get right than other styles of working with grammars.
I work with PEGs every working day of my life. When I saw your example, it immediately gave me a chuckle, so thank you for that.
Does that mean I'd spot the same gotcha deep in some grammar I was working on? No of course not, that would never happen. It's going to be some other mistake that makes me feel like an idiot after staring at the screen for several hours.
Parsers are designed around data, not conundrums. For structured languages, PEGs are a sharp and simple tool for slicing them up. But they're not great at solving the Liar's Paradox.
This is a really nice example, but I feel that it comes to an unfair conclusion.
I'll concede that this appears counterintuitive, but I feel that cases like this most often just pop up because people have been "overexposed" to EBNF/CFGs before and write PEG grammars with wrong preconceptions (also often ending in "left recursion" confusion, but that at least gives early errors).
But I still think that it is much preferable to debug/rewrite a questionable PEG grammar than e.g. a cobbled-together mess of conditionals and regular expressions...
But now I'm really interested in what you're advocating for instead? CFG parser frameworks? Or learning how to write the parser by hand? Or learning about PEGs until your example is understood?
For general parsing of data I am currently advocating LR(1), and GLR or Earley for those that need ambiguity. The exception to this is programming languages, for which I think the #1 priority is error messages, thus recursive descent is quite logical. If whatever you are parsing is unambiguous isn't strictly LR(1) a small lexer can almost always fix that, but often scannerless works fine.
LR(1) gets a bad rap for impossible to understand conflicts while writing a grammar. I strongly believe this is due to most tools advertising LR(1) actually using LALR(1). LALR(1) is deservedly awful, and introduces mysterious conflicts. But modern parser generators can support minimal LR(1), which generates parse tables almost as small (or often exactly ad small) as LALR(1) without the mysterious conflicts.
Finally LR(1) gets a bad rap for screaming at you at all during parser writing, but I see this as a feature, like the type checker in a programming language, preventing you from writing ambiguous or expensive to parse constructs.
I think PEG let people think they can get away from the complexities of grammars because there are no ambiguities when you express them using PEGs. This is not true: the ambiguities are still there, and you still have to think about them, otherwise PEGs will generate "garden path" parses that make no sense.
I think the sweet spot for parsing medium and high complexity languages is autogenerated lexer + handwritten recursive descent.
PEGs are a compact notation for recursive descent and they compile to recursive descent parsers. What would be the advantage of writing the compiled code by hand?
Few parser generators do well on things like error reporting in a human friendly manner. In effect production parsers tends to end up being hand written or extensively hand-modified for that reason. I wish more work on parser generators would involve focusing on better diagnostics (for end users, not the parser developer) and reporting rather than making them easier to use for the parser writers, as "just" writing the parser is rarely the hard part.
EDIT: Even making it easier to annotate the parser specifications to improve error reporting would be an improvement for many parser generators. In the past I've experimented with parser generators that included a prolog-inspired "cut" like operator that stops further backtracking, and that'd take an error message to output if nothing further can be matched without backtracking. It'd prevent the parser from "escaping" out of a likely human error and end up reporting a failure to match productions much higher up. It worked fairly well, and mimics what human parser writers tends to do in recursive descent parsers - we'll often guess at the likely error based on context deep down and bail out.
> people think they can get away from the complexities of grammars because there are no ambiguities when you express them using PEGs. This is not true: the ambiguities are still there
It's literally impossible to write a PEG with an ambiguity. So I don't know what you mean?
Ambiguities in the intended language aren't literally "still there" in the PEG, but realistically they are "still there" as a source of problems: naive PEGs can remove ambiguities incorrectly in less than obvious ways, and figuring out what's wrong can be more difficult than hitting a brick wall with other parsing approaches.
Just because the PEG doesn't have ambiguities doesn't mean the grammar you meant to recognize doesn't have one. What I mean is: PEGs give you a false sense of confidence by making you feel like you solved all your grammar's issues. In reality, you need to think really carefully about what your PEG is doing, because it's not going to warn you. As you said, PEGs are never ambiguous.
Most other parser generator systems give you indication that what you're writing is probably wrong, by stating e.g. that a grammar is ambiguous.
S := A '+' A | A '*' A
A := ...
If you do that in most PEG systems I've tried, you're just going to get the wrong result with no indication of what might have gone wrong, unless you know where to look.
But that's the point - other parsing systems let you design an ambiguous language and then tell you about it. PEGs don't let you design an ambiguous language in the first place. Unambiguous by construction.
why would you recommend using a generated lexer? the machinery is so simple thats its generally not worth trying to deal with the plumbing, esp around types.
I love PEGs, but their error messages are usually vague, because it will backtrack out of a deep tree (where it should have discovered the actual error), and then presents an error much higher up ("computer says no").
Is there a mechanism that works well for improving errors in PEGs (i.e. something like a non-returnable node), and how does one practically implement that?
> Is there a mechanism that works well for improving errors in PEGs (i.e. something like a non-returnable node)
Yes, it's called the "cut operator".
> how does one practically implement that?
Pick a parser generator that supports it natively. ;-) If you're talking about implementing the parser generator yourself then you probably already know more about it than I do.
It's fascinating how many times the Prolog cut operator has been borrowed into parsing. It shouldn't be surprising, probably, given how easily lookahead leads people down the path of some backtracking mechanism or other for ease of implementation, and how ingrained the idea of a cut to stop backtracking is if you've ever been exposed to Prolog...
In an implementation I made ages ago, I created a different operator, the “naughty or”, which defines “invalid” syntax paths (for example, if the entire parse fails, allow going into the branch that does match identifiers that start with a number). This adds a language-level facility for a strategy I see most hand-rolled parsers end up in, which is to start parsing common mistakes to be able to provide better errors. It’s basically “free” because those paths are only explored if the parse is known to fail, so performance of successful parses isn’t affected, and allows the same level of “craft” in error messages as a hand-rolled one. It also allows people to easily submit patches to improve confusing errors they’ve gotten.
41 comments
[ 2.8 ms ] story [ 94.3 ms ] threadEDIT: since my point was too subtle and I have been misunderstood and downvoted as a result of it, allow me to give a hint. If you believe the above grammar matches aaaaa you fall in the latter category. It does not. Yes, aaa matches, aaaaaaa matches, but aaaaa does not. Yes, this is mind-boggling.
It parses this: a a aaa a a But not this: a aaa a
At a high level, the reason for this behaviour is that PEGs are greedy. The inner recursive calls will always parse as much as they can, without regard for the outer calls that still want to parse more a's.
Thanks for taking the time & effort to explain! I know you're right because I have reproduced this behaviour in 2 parser generators now (pegjs & grako).
Why does the next recursive call fail? (Don't answer, I think I've worked it out.)
Isn't it supposed to backtrack:
I guess it's because the backtracking doesn't happen at recursion level 3 (because it already thinks it succeeded) but at level 2: Let's see if my explanation holds for aaaaaaa: Geez.Well, now I'm grateful I've never had to implement a parser for anything more complicated than a Lucene-style query syntax.
We have grammar: S -> 'a' S 'a' / 'a'
I'm going to put (aSa) around parses of the first alternative and <a> for the second alternative. So a successful parse would look like (a(a<a>a)a).
Here's what the PEG parser will do for aaaaa. It will first recurse all the way down, trying the first alternative at every point:
The choice indicated by ^ will fail because it is expecting another 'a', so that choice will be backtracked, and it will replace the ( with < at that position: Now it will backtrack over that ^ choice and replace the ( with < at that position: Now comes the key point: the next thing it backtracks over is the second ( and NOT the third (, because the third ( actually succeeded. So it has skipped over one extra layer.Now it will parse:
This one succeeded, and consumed 3 characters. The rest of the input is then still to be parsed by productions further up the stack, but since this S is the main production of the grammar, we declare failure because the whole input was not consumed.In general, after backtracking, a bunch of ('s may initially seem to succeed, and then when it fails it will backtrack over all those, rather than over just one of them.
For your larger example:
So this one does parse the whole input.> Well, now I'm grateful I've never had to implement a parser for anything more complicated than a Lucene-style query syntax.
Interestingly, these problems don't seem to come up in practice. But things like this are still a good argument for LR/GLR parsers: LR parsers will report the conflict at grammar compilation time, and GLR parsers will backtrack with all possible choices, so they will parse the way you'd expect. They are certainly the more principled choice. But in practice it may not matter that much in practice because people don't seem to use these kind of grammars, at least not for programming languages.
It is one-line, two-rule PEG grammar. What does it accept, and why? If we can't state this with confidence in such a tiny example, what makes us have any confidence in larger, more complex grammars? We should understand our tools. PEG pretends to be simple and understandable, but it's much less so than it might seem on the surface.
That's a good point. Unfortunately, it's also true not only of PEGs and recursive-descent parsing, but also of essentially every parsing formalism out there. So it doesn't seem to disqualify PEGs in particular.
(It's also true of programming languages generally: For which inputs does Collatz halt, and why?)
CFG semantics is nice and declarative when we specify which strings belong to the language, but a parser is a function from strings to parse results, and then the nice semantics stops working due to ambiguities. The PEG semantics on the other hand naturally expresses such a function, and the function you get with PEG semantics is almost always the function you want.
When we look at character level grammars, the CFG formalism doesn't work well either. They fundamentally rely on greedy lexical token matching.
Extensions such as indentation sensitive parsing and user defined operators quite naturally fit into PEG style semantics, and are more difficult to integrate into CFGs.
CFG based parser generators are way more complicated internally than PEG based parser generators, which are extremely simple.
No, I think you might be misunderstanding my point. For example, if you think the grammar I posted matches aaaaa, you'd be wrong.
Does that mean I'd spot the same gotcha deep in some grammar I was working on? No of course not, that would never happen. It's going to be some other mistake that makes me feel like an idiot after staring at the screen for several hours.
Parsers are designed around data, not conundrums. For structured languages, PEGs are a sharp and simple tool for slicing them up. But they're not great at solving the Liar's Paradox.
I'll concede that this appears counterintuitive, but I feel that cases like this most often just pop up because people have been "overexposed" to EBNF/CFGs before and write PEG grammars with wrong preconceptions (also often ending in "left recursion" confusion, but that at least gives early errors).
But I still think that it is much preferable to debug/rewrite a questionable PEG grammar than e.g. a cobbled-together mess of conditionals and regular expressions...
But now I'm really interested in what you're advocating for instead? CFG parser frameworks? Or learning how to write the parser by hand? Or learning about PEGs until your example is understood?
LR(1) gets a bad rap for impossible to understand conflicts while writing a grammar. I strongly believe this is due to most tools advertising LR(1) actually using LALR(1). LALR(1) is deservedly awful, and introduces mysterious conflicts. But modern parser generators can support minimal LR(1), which generates parse tables almost as small (or often exactly ad small) as LALR(1) without the mysterious conflicts.
Finally LR(1) gets a bad rap for screaming at you at all during parser writing, but I see this as a feature, like the type checker in a programming language, preventing you from writing ambiguous or expensive to parse constructs.
EDIT: Even making it easier to annotate the parser specifications to improve error reporting would be an improvement for many parser generators. In the past I've experimented with parser generators that included a prolog-inspired "cut" like operator that stops further backtracking, and that'd take an error message to output if nothing further can be matched without backtracking. It'd prevent the parser from "escaping" out of a likely human error and end up reporting a failure to match productions much higher up. It worked fairly well, and mimics what human parser writers tends to do in recursive descent parsers - we'll often guess at the likely error based on context deep down and bail out.
It's literally impossible to write a PEG with an ambiguity. So I don't know what you mean?
Not sure how that's saying anything useful though? If you write the wrong grammar with any parsing system it's also not going to work.
S := A '+' A | A '*' A
A := ...
If you do that in most PEG systems I've tried, you're just going to get the wrong result with no indication of what might have gone wrong, unless you know where to look.
Is there a mechanism that works well for improving errors in PEGs (i.e. something like a non-returnable node), and how does one practically implement that?
Yes, it's called the "cut operator".
> how does one practically implement that?
Pick a parser generator that supports it natively. ;-) If you're talking about implementing the parser generator yourself then you probably already know more about it than I do.
https://github.com/tolmasky/language