Parsing / Recursive Descent Parser
huy.rocks
huy.rocks
I have a large-ish grammar that I spent a lot of time on, and I'll be damned if I don't still sometimes get "Invalid statement." with no useful location information. I even wrote my own parser generator; it's not just a shortcoming of existing tools, it's just really hard to write a generic parser generator that's capable of generating custom and useful errors.
If you or anyone here happens to write this blog post before I do please send me a link when you do. :)
If your parser is parsing something machine-generated where the expected case is success, and failure indicates data corruption/malicious input/a bug somewhere else, then parser generators can be useful.
Parsing combinator libraries are a good middle ground between a declarative grammar and useful error handling.
After having written parser dozens of times nowadays I don't bother with anything other than recursive descent. In practice, I don't think anything works better.
(Stacks are an implementation detail. Only some languages implement function calls with stacks.)
Programs compiled with GHC do use 'the stack', but for pattern matching, not for function calls. Whether 'stack frames' (ie the information associated with a function invocation) go on the heap or elsewhere is a bit complicated. Especially since the compiler does so much analysis and optimization.
Also eg, an implementation of Unlambda (based on SKI-calculus) wouldn't necessarily have a stack either.
Something like Datalog wouldn't really have a call stack either, but that's deliberately not a Turing complete language. (But neither is Agda a Turing complete language, but it's still general purpose.)
Recursive descent parsers can contain Pratt parsers but they don't need to. For example you can parse s-expressions with recursive descent but there's no need for Pratt parsing.
And Pratt parsing is not the same technique as general recursive descent.
So the article you linked is about a specific subset of parsing expressions, which is useful yes, but not the same category as OP's post.
And pratt parsers can be contained in recursive descent parsers but they don't need to either.
[1] https://reindeereffect.github.io/2018/12/08/index.html
[2] https://reindeereffect.github.io/2019/01/16/index.html
[3] https://github.com/reindeereffect/tools-from-blog/tree/maste...
My first idea was to turn the interpretter in a code generator that would generate C code. It produced a massive file, which took very long to compile and to my surprise, was slower than the interpretter. This might be due many missed cache hits.
Next, I decided to implement some caching, simply remembering at each point of the input which non-terminals were parsed or not. This gave a huge performance improvement.
I also added some mechanism to let it generate a Abstract Syntax Tree. Later, I wrote a C++ version that also included an unparser, based on the input grammar. Instant pretty-printer and 'unifier'.
Recently, I developed a JavaScript version (with a simple caching strategy), which turned out to be still fast enough to follow key strokes for small inputs. (Just parsing the whole input on each key stroke.) See: https://fransfaase.github.io/ParserWorkshop/Online_inter_par...
For people without a CS background (like me) it can be a bit intimidating to get started. Parsers tend to have their own vocabulary with terminals, productions, grammars, DFA, NFA, etc. I never took a compilers class and therefore feel like I was never admitted to the club.
Crenshaw’s articles are much more approachable and give a way for the rest of us to use this great technique.
2. Essentials of Compilation (Siek)
A very easy way in would be a venerable Wirth's "Compiler Contruction", which takes a simplistic approach, e.g. concentrating on getting the result out as fast as possible.
Somewhat similar, crisp in style and exposition, would be Nystrom's "Crafting Interpreters". It explains modern interpreter kind of language implementation (Python, JS, PHP and others), which includes compiling to bytecode and bytecode VM. There is a free online version of the book.
Now, compilers are one of the oldest area of computer-related software research. There are many good books, numerous approaches and schools. Any recommendation should take concrete student's background into account. Say, improving a massive modern compiler backend requires a very different kind of recommendation compared with a make-my-own-language project.
The book is very imperative and doesn't really know much about modern abstractions for data structures.
Something like https://en.wikibooks.org/wiki/Write_Yourself_a_Scheme_in_48_... is probably more fun for a beginner these days. (Languages in the ML family are really well suited to writing parsers, interpreters and compilers. That what that family of languages was designed for.)
Would you believe it doesn't discuss looping structures at all? No repeat, while, for loops.
Last time I looked, for the latest edition of the book, you had to create a timed online account to access the chapters they didn't include in the book anymore. Ridiculous. Use any other book, I'd say.
Like most of these, I’ve never actually seen one “in the wild,” but it was a great example of the power of recursion.
But, like I said, the idea behind it was quite useful, to me. I use recursion all the time, and RDPs are a pretty "pure" form of recursion.
Is there a reason for that? None that I can think of. SQL systems tend to have worse error messages but I don't believe parser generators require error messages to be as bad as SQL error messages (I'm talking about how they often don't give line number or column number info).
So my guess (and it sounds kind of absurd to say) is that most SQL systems just don't put much thought into parsing. And not that parser generators fit the domain better.
The original C++ compiler was (after horrible experiences with YACC, or similar) writen mostly as a recursive descent compiler.
Also, recursion can be very bad for parallel stuff, if not done carefully.
We used to have to do a lot of optimization, in my old job, involving use of GPUs, ISPs, multi-thread, etc.
In that kind of environment, things can get quite crazy, and seemingly innocuous stuff, can have huge knock-on effects.
For example, cache hits. If you want your code to stay in a shallow cache (which could result in 100X improvement in speed), you may do things like write something over and over, inline, instead of calling a method or subroutine.
fn parse_money(&mut self) -> ParseResult<MoneyNode> {
let currency = self.parse_currency_symbol()?;
let amount = self.parse_amount()?;
return Ok(MoneyNode {
currency,
amount
});
}