I'm guessing that a lot of people's first impression of that will be "That's insane, are you seriously suggesting writing two grammars in one?" But that's exactly what you do when you build custom error reporting code into a recursive descent parser. It's just less obvious because the code is procedural instead of declarative.
A recursive descent parser does not have to be procedural. I have written one in a functional language that I designed that has no mutable variables or loops (recursive functions handle both of these cases).
Parser generators are great for iterating on language syntax design. Once you know the language syntax, handwriting the parser should be easy. If it isn't, then it is most likely that either your language design or parsing skills need work. Both require time and practice.
I don't see how this follows? There is no need to append an error node (or any node at all) to the AST when reducing an error production. You can just print an error message or push one onto a stack of error messages.
> Parser generators are great for iterating on language syntax design. Once you know the language syntax, handwriting the parser should be easy. If it isn't, then it is most likely that either your language design or parsing skills need work. Both require time and practice.
Parser generators aren't just for people who don't know how to write recursive descent parers; they are production ready tools. Python, Ruby, PHP, and Bash all use generated parsers.
I think the aversion to parser generators in general and LR parser generators in particular is a combination of not-invented-here-syndrome and cargo-culting the idea that LR parsers can't emit handcrafted error messages.
What would be useful is tooling that would check that one grammar is a sub-language of another grammar which is suitably connected (because in the general case that check is undecidable of course).
Wouldn’t the grammar of errors have to also be LR? Doesn’t that limit the kind of errors you can report?
I like `bison --xml` for the fact that you can write your own code but trust the table.
So, anything that makes code easier to read is a must. That includes the extra expressiveness of an LR grammar.
I wish that language designers (looking at you, Ruby - as much as I love using Ruby, the grammar is an abomination) would at least try to stick to LL as much as possible, and consciously, carefully and in a limited fashion "escape" into LR for as small subsets as possible.
Break rules if it actually helps developers, sure, but it'd be nice if people tried to make the weird bits as tiny as possible...
EDIT: Also, in practice this is often what we end up doing when we write recursive descent anyway, only in a less than formal way. It'd be nice if more tooling let us write grammars that default to LL w/checking, but let you insert clearly delineated exceptions.
A language should be LL as much as possible. And also depend on the parsed data as little as possible. Stuff like C's variable declaration must be avoided.
But I don't think either of those rules can be made universal. Some language for expressing exceptions would be great.
My general opinion is that any constructs that confuse a parser will also confuse a human.
And given the modern idea of "everything is either an identifier or an operator", precedence became very important.
(I went into more depth on this a few years ago: https://www.abubalay.com/blog/2021/12/31/lr-control-flow)
I would even call it a variant of recursive descent, which is top-down. In plain recursive descent, you often choose where to recurse based on the first token (e.g. if while for). In Pratt, you choose based on a table indexed by the second operater token.
You can have an an entire arbitrary length subexpression before the "second token", but it is still resolved "eagerly".
e.g. consulting the loop here - https://github.com/andychu/pratt-parsing-demo/blob/master/td...
I skimmed over your post, but I don't really see the bottom-up argument.
I guess I could concede that it's neither top down nor bottom up, but I don't see the argument for being bottom up.
And maybe this is just a taxonomy thing, which doesn't matter in practice. It could be understood both ways -- I don't think Vaughan Pratt was mistaken when he understood it a certain way :)
---
"where to recurse" is basically choosing the parent's production before its children -- that's most similar to top down
If you choose the child's production before its parent, then that's LR / bottom-up
(using Haberman's explanation - https://blog.reverberate.org/2013/07/ll-and-lr-parsing-demys...)
May also be relevant:
https://www.oilshell.org/blog/2017/03/31.html
https://www.oilshell.org/blog/2017/04/22.html
https://matklad.github.io/2020/04/15/from-pratt-to-dijkstra....
I believe some people claimed that Dijikstra is bottom-up and Pratt is top down. That has a certain nice symmetry to it, but I'm not sure it's true :) Dijikstra does use an explicit stack and Pratt uses the call stack -- maybe that is how I'd put it.
To make this more explicit:
The first thing a Pratt parser does before entering the loop is based on the lookahead token's "nud." This corresponds to an LR automaton in its initial state shifting that token and advancing into the leaf-most rule(s). The "nud" typically switches back to top-down internally, where pure bottom-up would not, but the choice of "nud" is necessarily made in a bottom-up way.
Once "nud" returns, or the LR automaton reduces back to the initial state, the next thing it does is based on the operator token's "led." This corresponds to the LR automaton shifting that token and advancing into the parent rule determined by the operator. This resolution is just as "eager" in LR, where it is represented by the new state containing only items from the chosen rule. This is the hallmark of bottom-up parsing: as you progress through the children, you narrow down the possibilities for the parent.
Finally, note that none of this has anything to do with the use of function calls vs a table of integers and explicit stack. Both top-down and bottom-up parsers can be implemented using either recursion or a table and stack.
1 + 1 * 42
1 + 1 + 1 * 42
1 + 1 + 1 + ... * 42
Or simply right associative operators.I'm not sure how much the classification matters, similar to how I've concluded that pratt parsing vs. shunting yard doesn't seem to "matter" (both work)
But I have heard the "complaint" that pratt parsing is hard to understand, so maybe explaining it as a bottom up could help.
The arbitrary length subexpression prefix is really the problem that LR solves compared to LL (historically Python used LL parsing, and param=value and lvalue=rvalue are "overparsed" for this reason)
Sometimes working through a particular problem, or conflict, or example is easier from one than the others, but knowing how they correspond (which is similar to how iteration and recursion correspond) means you can translate those ideas across.
I've never met a sane grammar that's not LALR(1). It's unfortunate that LALR(1) is such a leap of complexity beyond SLR(1), which is not practical for real languages.
I imagine that LL(1) "mostly" works after the expression hack - what's your particular motivation for LL(2)? A lot of weird grammars lead to code that's hard to parse for humans.
(Note also that, for languages (Python, Javascript, etc.) that lack an efficient `switch` statement, table-based is likely to beat function-call-based code)