Parser generators vs. handwritten parsers: surveying major languages in 2021
notes.eatonphil.com
notes.eatonphil.com
I think that with enough work, we can adapt and evolve parser generators to mesh well with better error reporting, and give the programmer more options and control, but it’ll take some elbow grease and probably breaking backwards compatibility with the production syntax of these parser generator systems. There’s also a risk, of course, that if there’s too much customization, you sort of lose the value of having a parser generator DSL in the first place.
I implemented an earley parser, because from what I read on wikipedia, it seems to be more advanced.
"Earley parsers are appealing because they can parse all context-free languages, unlike LR parsers and LL parsers, which are more typically used in compilers but which can only handle restricted classes of languages."
however I seldom see languages use Earley parser, there must be a reason, but I've never seen anybody explaining why choosing one algorithm over another.
After all, when you're writing software, you pretend to be the compiler to some extent.
Natural languages, like English, are a good example of something which humans struggle with because they are complex, ambiguous, and often require context. Sure, it's extremely expressive, but that is the sharpest double-edged sword in programming languages.
Consider the problem of parsing a C program that has NOT been put through the preprocessor. You want to be able to do this for structure editors and source-to-source transformation systems.
My favourite is Bourne's longing for Algol: https://minnie.tuhs.org/cgi-bin/utree.pl?file=V7/usr/src/cmd...
if the shit i'm parsing isn't in the language there is no reason to continue. its invalid.
You gain error messages for subsequent errors, so you can fix them all before recompiling.
"Consider the problem of parsing a C program that has NOT been put through the preprocessor. You want to be able to do this for structure editors and source-to-source transformation systems."
One cannot in general parse un-cpped C/C++. But that doesn't mean it's useless to do as well as one can, even if it cannot be parsed exactly. Parsing is for more than just compilers.
As the above commenter mentioned, most language designers make hand-rolled parsers. Making a hand-rolled LL or LR parser is easier too.
In general most people think advanced = bad, they want the easiest solution which gets the job done well.
Earley's algorithm should be able to be trivially parallelized which might make it a contender for languages which are ambiguous (like most real world languages) where they are hand-rolling parsers. I haven't tried since I have no real need but looking at the steps I can't see a reason it couldn't do its work across multiple threads.
Honestly, other than JavaScript 1.1 I can't think of a popular language which has an unambiguous syntax and I really like playing with language grammars for some odd reason -- probably wrong though...
This is certainly slower than a deterministic LR(1) parser, but maybe if you're clever about the implementation you can efficiently handle local ambiguities in a grammar. Large-scale ambiguity is bad since you don't want small changes to have action-at-a-distance, but you could in principle have a strict LR(1) grammar that contains Earley sub-grammars for expressions.
By 1991, "Most researchers see the Parsing Problem as "solved" -- a closed issue. Earley parsing is almost forgotten, and Leo's discovery is ignored. Two decades will pass before anyone attempts a practical implementation of Leo 1991."
It takes Aycock and Horspool's work in 2002 and Kegler's work in 2010 in Marpa to have a "practical implementation" (quoting that link).
(I quote that also because Aycock distributed SPARK, an Earley parser, which was included as part of the Python distribution, in the Parser/ subdirectory, and a couple of people here on HN report having used it.)
That one is really the only Earley parser I've found used in the wild (don't know what marpa is used for) and unfortunately it is mostly unhackable because they did some serious optimization voodoo on it so it was replaced by a hand-written recursive decent parser a while back because nobody in the world could figure how it works[0] -- which is kind of strange since ASDL is super simple to parse and the generator which used spark was meant to check files into source control but, whatever.
Its easy to play around with but not a great source if you want to see how an Earley parser is put together. There are also some bugs with parser action on duplicate rules not working properly that were pretty easy to fix but python pulled it out of the source tree so no upstream to send patches to?
[0] might be making that part up, dunno?
I know SPARK's docstring use influenced PLY.
PLY doesn't use Earley, but "Earley" does come up in the show notes of an interview with Beazley, PLY's author, at https://www.pythonpodcast.com/episode-95-parsing-and-parsers... . No transcript, and I'm not going to listen to it just to figure out the context.
https://github.com/lark-parser/lark "implements both Earley(SPPF) and LALR(1)".
Kegler, the author of that timeline I linked to, is the author of Marpa. Home page is http://savage.net.au/Marpa.html . The most recent HN comments about it are from a year ago, at https://news.ycombinator.com/item?id=24321395 .
Either way, I have never used a better parser generator. It has the best usability and incredible performance when you consider it is written in pure Python.
So looking at the list I'm dumbfounded that CPython and Ruby use generators.
It's also potentially quite slow.
Hand-written recursive descent are simple to write, debug and maintain and extend, while with generators I always had to fight the tool at some point, and that always ended in frustration.
As for fighting the tool and simplicity, sure, that's beside my point.
- turning it into a state machine (required to deal with nested comments and C++ template syntax)
- building the symbol table while parsing and querying for disambiguation
- treating expressions as a stream of atoms and using a specialised precedence parser (probably required to deal with operator overloading)
Parser generators usually employ these as well.
That issue is the primary reason why postgres continues to use bison. The SQL standard introduced potential parsing ambiguities frequently and leaves a large portion of the things necessary for a functioning RDBMS unspecced.
I agree that I think generators could address this issue, they simply haven't.
Also I'm particularly fond of PEGs, because they match the intuitiveness of recursive descent parsers with a generator suitable formalism (though they have their rough edges as well).
But yeah, it makes the compiler output way easier to work through when you have everything at once.
I then went to work at a private company, and an older guy who had gone to a state school that taught recursive descent (his was the last class to teach it) taught me how to do it. In a month or so I had learned more about how grammars actually work, what ambiguity is, and so forth, than in my whole class at Stanford.
I now teach compilers at a university, and I teach recursive descent.
There's some ugly stuff in there, but it's fun, all because we wrote it in recursive descent and can do whatever we darn well please.
Using recursive descent for everything is like using Python (or Go or pick your favorite easy language to learn) for everything. Yeah you can totally do it, and a heck of a lot of people do precisely because of how nice and convenient it is, but it's good to have a sense what else is out there and how you can think about a problem differently IMO, even if you rarely use other languages in practice.
i think the more complex parsing algorithms were good for producing papers, which explains their success in academia but relative lack of success in industry
The lovely thing about a nicely written recursive descent parser is that the code pretty much is the grammar (topologically), so any grammar you can actually imagine that can be parsed by recursive descent doesn't take much thought to make work.
C/C++ have plenty of difficult syntax edge cases. For example, >> can represent a single operator in an expression or two successive > in a template declaration.
https://docs.python.org/3/reference/lexical_analysis.html#in...
The main difference is that LR (bottom up) family grammars can describe more languages than LL (top down) ones can largely because bottom up has slightly more contextual information available when it puts stuff together (often small treelets rather than just the next token)
I had my compiler course at the University of Utrecht in the mid nineties. The local faculty there had some functional programming pioneers and did a lot of work on parse generators. So, I learned how to write a parser generator in Gopher (a Haskell predecessor). Very nice course and definitely a bit of a mind fuck since I also had to wrap my head around monads at the same time. And of course not really that useful for real world stuff and I've since forgotten most of what I learned as I have not used it in over 25 years.
A few years ago, I had to sit down and write a parser for a little query language that we came up with. I spend some time looking at all sorts of convoluted tools for this and refreshing some decades old knowledge on grammars to actually try to understand what the tools were trying to do. I then found a useful example of a lexer that simply used regular expressions and suddenly realized I was wasting time and that this stuff was stupidly simple. I then knocked out this little project out in a few hours using that. Once you have a lexer, things get easy and they are not that hard to write. I think this is why people end up doing these things manually. It's just not that hard or worth automating and it's rare enough that it's not worth investing time in learning new tools.
Also, modern languages are a lot better at this stuff too. String handling in C was always a bit tedious. But modern languages come with so many built in stuff for that that it's not really a burden. So, in short, parser generators solve a problem that's not really that big of a problem anymore.
They're far from the only way, handwritten parsers are often mentionned as having way better error recovery. But if you're making a new configuration language, programming language, or something like that, ensuring that it has LR-compliant grammar and that this grammar is the source of truth can avoid a lot of pain later down the road.
I had to write an interpreter, optimizer and engine for a declarative language plus bottom up knowledge base in Haskell as part of an assignment, and an exam in a graduate course on advanced programming. Haskell made the problem significantly easier compared to languages I am much more comfortable with, like Python or C.
[1] www.cs.nott.ac.uk/~pszgmh/pearl.pdf
[2] https://www.amazon.com/Programming-Haskell-Graham-Hutton/dp/...
[3] https://www.youtube.com/channel/UC9-y-6csu5WGm29I7JiwpnA
Part of the course is understanding exactly when Regexps are good enough and when you need something more powerful.
Just would like to add a link: https://craftinginterpreters.com/contents.html
It explains parsing and other topics in a really clear and accessible way.
The lexer that yacc implements is indeed regex based and as I also recently discovered was originally written by a little known developer by the name of Eric Schmidt [1]
Would it be better with hand rolled and they could have abstracted and organized somethings or does it all make sense in its current format if you are familiar with it?
I suspect not, because Scala is super flexible and probably more difficult to parse, but has a better organized parser.
Last but not least, what you actually end up wanting to build is an ast, as at some point you want to do something with the input, for most parsers you then have to implement even more code to build up the ast.
It is much easier to hand write it. In the end its faster to write and usually faster to run.
Every few years I evaluate the new options for the languages I use (c#, pascal), every time so far I am disappointed with the tooling. Maybe one year.
For example I work with a parser definition that's supposedly shared between C and Java, but it's a massive nightmare because it's 50% imperative actions.
Yes... but most practical languages do not have simple LL or LR grammars. Hence this blog post and discussion and these problems.
I said ‘most’ not ‘all’, and nobody here said the only reason to hand-write a parser was context sensitivity.
Java hasn't ever been able to be parsed with one token look ahead, the earlier versions had a section on how to modify the grammar to be LALR(1)[0] but it was dropped in later versions of the specification -- probably due to added features which made it unfeasible like generic classes.
It's actually quite a good resource since they explain the reasons why the parser needs to have more than one token to figure out what's going on.
[0] http://titanium.cs.berkeley.edu/doc/java-langspec-1.0/19.doc...
And it's under 3000 lines!
https://github.com/llvm/llvm-project/blob/llvmorg-12.0.1/cla...
Could you link me to where you see GHC's parser using parser combinators?
[0] https://gitlab.haskell.org/ghc/ghc/-/wikis/commentary/compil...
[1] https://www.haskell.org/happy/
[2] https://gitlab.haskell.org/ghc/ghc/-/blob/master/compiler/GH...
Scala has them as well, e.g.: https://com-lihaoyi.github.io/fastparse/
And the good thing is, you don't have to learn a completely new language/syntax, you can use the host language's syntax and you have full IDE support as well.
Recursive descent might be a tiny bit more code, but it will never stand in your way.
And you don't have to write absolutely everything by hand; it's perfectly possible to simplify the process somewhat using the full power of the host language to get incremental parsing, look ahead etc:
https://github.com/codr7/swifties/blob/main/Sources/Swifties...
I've written a parser for CSV which I think works very well but I'm self taught and have no idea if it's any good.
Any 'parsers for dummies' guides?
The Dragon book is the classic reference on the subject, but I don't think it's very good pedagogically, and most printings I have seen recently have been awful.
[0] https://drewdevault.com/2021/04/22/Our-self-hosted-parser-de...
A killer feature for a parser generator would be the ability to auto-generate a pretty printer which requires stuffing comments into the tree as a "meta token".
https://www.cambridge.org/core/books/specifying-software/FC5...
has a tutorial introduction to recursive descent.
If you ask developers who have no compiler experience to come up with a solution for e.g. evaluating maths expressions, chances are that they will effectively derive recursive descent in writing a solution, because of its simplicity and ease of understanding.
PUB FN ID(main) OPAREN CPAREN COLON ID(i32) OBRACE NL RETURN NUMBER(0) CBRACE
Look familiar? How might you parse that into something like
{ kind: "function", name: "main", args: [], public: true, return_type: { kind: "integer_type", width: 32 }, body: [ { kind: "return", expression: { kind: "number_literal", value: 0 } } ] }
A recursive descent parser is just a parser that starts at a root node type and recursively traverses a token stream and calls handler functions based on what's next. Those functions might also call other handlers (e.g. for nested expression parsing, etc.) which is why it's called recursive.
It is sort of a walk-through of a small programming langauge written in go, using a recursive descent parser.
Crafting Interpreters is good as well, though I only read part of it, because it wasn't done at the time I read it. https://craftinginterpreters.com/
I've also interviewed both of them, if you like an audio intro: https://corecursive.com/032-bob-nystrom-on-building-an-inter...
https://github.com/dlang/dmd/blob/master/src/dmd/cparse.d
which is a C parser. It's not hard to follow.
I imagine that the same idea applies for more general languages, so, I expect the derivative of a language with respect to a character, is the language of strings such that prepending one by the character results in a string of the original language.
So, if R_1 and R_2 are two different regexes, then the derivative with respect to c of the regex R_1 | R_2 , i.e. \partial_c (R_1 | R_2) , would be (\partial_c R_1) | (\partial_c R_2),
and, if + is used for concatenation of two regexes, the derivative of R_1 + R_2 would be, ((\partial_c R_1) + R_2) | ((if R_1 accepts the empty string, then the language which accepts only the empty string, otherwise, the language that does not accept any string) + (\partial_c R_2))
etc.
... I think.
Actually it's not just the parser but most of the compiler frontend which is written in femtolisp. It would be nice to make the frontend more accessible by replacing it with Julia code at some stage. Bootstrapping is tricky though until someone gets separate compilation working.
All lisps use handwritten parsers, in under 100 lines.
On the other hand, I certain agree with your idea about teach/using handwritten recursive-descent parsers. Here's an old book that presents it pretty clearly, along with a nice approach for error handling. Maybe you can find it in a library. https://www.springer.com/gp/book/9783540082408
Other well-known languages that have hand-written parsers are Rust and D.
And here are a couple quotes from compiler developers explaining their reasons for going with hand-crafted parsers.
Someone from the C# compiler’s team gave the following reasons [1]:
>Hello, I work on the C# compiler and we use a handwritten recursive-descent parser. Here are a few of the more important reasons for doing so:
>Incremental re-parsing. If a user in the IDE changes the document, we need to reparse the file, but we want to do this while using as little memory as possible. To this end, we re-use AST nodes from previous parses.
>Better error reporting. Parser generators are known for producing terrible errors. While you can hack around this, by using recursive-descent, you can get information from further "up" the tree to make it more relevant to the context in which the error occurred.
>Resilient parsing. This is the big one! If you give our parser a string that is illegal according to the grammar, our parser will still give you a syntax tree! (We'll also spit errors out). But getting a syntax tree regardless of the actual validity of the program being passed in means that the IDE can give autocomplete and report type-checking error messages. As an example, the code ` var x = velocity.` is invalid C#. However, in order to give autocomplete on `velocity. `, that code needs to be parsed into an AST, and then typechecked, and then we can extract the members on the type in order to provide a good user experience.
GCC actually used Bison for a long time but eventually switched to a hand-written parser, the team gives some reasons in the changelog [2]
>A hand-written recursive-descent C++ parser has replaced the YACC-derived C++ parser from previous GCC releases. The new parser contains much improved infrastructure needed for better parsing of C++ source codes, handling of extensions, and clean separation (where possible) between proper semantics analysis and parsing.
Some people argue coding parsers by hand is prone to errors. This line of reasoning certainly makes a lot of sense to me, but it's not that simple in practice for non-trivial grammars. For instance, ANTLR 4 emits a so called parse tree which you would like to convert to AST, if you have a somewhat complex grammar. This conversion takes quite a bit of code written manually, see AstBuilder.java [3] to get an idea (I'm not affiliated with the project). So, there's still plenty of opportunities for having errors in that amount of code. To be fair, creating AST-s is an explicit non-goal for ANTLR 4 [4].
[1]: https://news.ycombinator.com/item?id=13915150
[2]: https://gcc.gnu.org/gcc-3.4/changes.html
[3]: https://github.com/crate/crate/blob/5173b655a9fbf72028876ae7...
[4]: https://theantlrguy.atlassian.net/wiki/spaces/~admin/blog/20...