Writing parsers like it is 2017
blog.acolyer.org
blog.acolyer.org
It's an LR(1) parser generator for OCaml and Coq with a lot of extremely interesting features, such as genuinely good debugging support for grammars and the ability of generating error messages by example.
What this means is that after you write down your grammar, Menhir will give you examples of all the possible syntax errors that could occur. You can then write error messages for each case and get a parser with built-in error reporting for syntax errors.
This works a lot better than you'd think and I really wonder why nobody else implements this feature. Or for that matter, why they're not advertising it on the webpage! If you want to know more, look in the manual, section 11.
I think I'm going to try and implement this in a parsing library I develop. :)
https://github.com/nikomatsakis/lalrpop http://smallcultfollowing.com/babysteps/blog/2016/03/02/nice...
I've never used Menhir so I can't compare how similar they are in practice, but I've enjoyed the times I played with LALRPOP much more than the many times I've battled various yacc derivatives.
[1] https://github.com/prakhar1989/JSJS/blob/master/src/parser.m...
Since writing recursive descent parsers is not that hard (at least if your grammar is LL(1), see https://github.com/YorickPeterse/inko/blob/a76a8c23f901c5b2a... for an example) I would personally go with this approach whenever possible.
Parser generators definitely have their use cases (and allow you to write a parser much faster), but error reporting is often tricky to get right.
In the best case scenario the specific parser combinator has good tracing support to enable debugging. However, imho this is still inferior to simple breakpoints and debugging "directly" in one's favorite IDE.
In programing languages which do not have true macros The combinator API normally creates a data structure representing the grammar which is than interpreted Which usually makes debugging harder and the parsing slower.
I learned and used bison+flex back in the 90s. More recently, I've enjoyed the PLY package for Python. It's essentially the same algorithms and abstractions, but it turns the usual bison grammar inside-out by embedding grammar fragments in your Python action code instead of embedding action code in a grammar file.
Your grammar is a Python module. You write Python function definitions which feel almost like you are writing the interesting subset of a recursive-descent parser, i.e. the important reduction steps, while hand-waving about all the input-testing and buffering you would have to do in a bespoke recursive descent parser. The code comment on each function attaches the grammatical rule which fires that action. The PLY parser-generator function you call at the end of your grammar module uses Python introspection to find all the production rules and stitch the parser together.
My only coding style complaint is how PLY abuses the function __doc__ strings to embed the grammar rules. I think using a decorator interface would have been a more "pythonic" interface to attach the grammatical rules to each action function...
My other problem was with error handling. I wanted to have my own `Error` enum to represent errors in my program. Nom supports a custom error type, but I now had to write out type annotations in many places with the fish syntax (e.g. foo::<&[u8], Ast, Error>()) and they are quite long annotations. In addition, some of the macros would not work—or at least, I couldn't get them to work—with my custom error type, and I had to revert to using functions in a way that was absolutely not compositional.
In the end, since the format I was parsing was simple and regular (Erlang's External Term Format), I wrote a parser by hand and removed the dependency on nom. The result is as just as fast and produces meaningful error messages. It's actually less tedious, because I am using simple functions that have the syntax and semantics that I already know; maybe if I had a more complex format to parse I would sing a different tune, but for the moment I prefer to avoid using nom.
Nom can be tricky to debug, but Rust can host tests in the same file as the code, so debugging by writing new tests is convenient.
The syntax takes some getting used to. The hardest part for me was learning to order the grammar alternatives correctly. The order matters for performance and correctness.
I think the main reason for hand-rolled parsers, at least in my experience, has been error reporting.
Most parsing frameworks make it difficult for you to tell the user simple things like misspelled variables, something does exist, but it isn't in-scope when its being called, and basically helpful messages like that.
Error management is only briefly mentioned, and sort of glossed over in Nom's [0] paper. Mostly because Nom was designed for efficiency... But efficiency is not why GCC rolled their own parsers.
I don't know of any parser combinator that can handle those very human mistakes, because they require partial evaluation to understand.
I might be mistaken, but if I'm not, parser combinators are great for saving time, but if you have the time to do it right, then a hand-rolled parser will give your users a much better experience.
These languages have constructions like OBJECT_NAME'ATTRIBUTE (object name can be anything with visible name in the scope and attributes depend on its kind) and RECORD_TYPE_NAME'SLICE where we create value of RECORD_TYPE_NAME using slice, which is collection of values of record fields.
There are also character literals, which have form like 'a', '1', '#', ''''. Just as you can expect here.
The combination of record value construction and lexer forms for character literals makes it hard to parse something like MY_RECORD'('1','0',false, 10).
Symbol table and/or dynamic parse construction allows you to parse these texts easily and specify the syntax straightforwardly. The other approach with construction of separate AST complicates things a lot.
I agree that generated parsers make error reporting difficult, but do not think these examples are relevent. If the problem is a misspelled or out of scope identifier, the parser should should still be able to parse the program, which would be syntactically valid.
Not necessarily.
Usually, we're used to seeing something like:
print("Something")
But there are statement based languages where instead you have: print "Something"
Still simple enough to parse.However, what about user function calls?
something 1 2
3 4
If a newline doesn't break a call, and you don't depend on indenting, but actually the arity of the original function definition, accidentally writing: someting 1, 2
3 4
Can be impossible to parse correctly, especially if said language also supports first-class functions. func apply with func:caller array:values
do
...code...
done
aply + list
do
10 10 10 10
done
The syntax in the above example is still correct, but because the arity of 'aply' can't be determined, the parser can't actually continue.The grammar might be a little insane, but I've had to work with similar grammars in several financial languages. And the parser does poop out, and when it does, you really want a nice error to result.
That's not necessarily true for all languages. For example, in C, "(e1)&e2" is parsed differently depending on how e1 is declared. If e1 is declared as
typedef int* e1;
then the "&" in "(e1)&e2" is parsed as the address-of operator, but if e1 is declared as int e1;
then "(e1)&e2" is parsed as a bitwise AND.I've had some success in the past with using GLR or another algorithm that can handle ambiguity, and then choosing among the possible parse trees in another pass that takes semantic information into account. How applicable that is really depends on the language, though; if things are so ambiguous that you're getting an exponential growth in possible parse trees, you may not want to use this approach.
Not only is there a high risk that you'll still be passing bad input through to your database, another program, or an unsafe library deep down in your application (YAML in Ruby anyone?), but you get what Meredith Patterson calls a "weird machine" that is programmable by an attacker.
There's a lot more of this at:
Stacks of papers, articles, videos, examples etc.
There are languages where you need to process some of what you've done before a later part of the source can be dis-ambiguated. How does that sit with what you've said here?
Another possible sweet spot would using something I call a "Parsing DSL" which is a sort of a cross between a parser combinator and a parser generator.
TLDR: See the JavaScript Parsing DSL library (Chevrotain) I've authored: https://github.com/SAP/chevrotain
Details: A Parsing DSL means using API similar to hand building a parser but without a-lot of the cruft associated with hand building while enjoying higher level abstractions from the Parsing DSL library such as: 1. automatic ambiguity detection. 2. lookahead calculation. 3. Grammar diagrams 4. auto-complete. 5. automatic error recovery 6. and more...
Under V8 (Chrome/Node) this is much faster than any other library tested and even substantially faster than a naive hand built parser. http://sap.github.io/chevrotain/performance/
(Benchmarked using a simple grammar [JSON])
[1] http://www.tinlizzie.org/ometa/
https://github.com/alexwarth/ometa-js
See also ohm:
Chevrotain shares two main ideas/concepts with Ohm.
1. Separation of grammar and semantics, but in a less opinionated manner as it does not enforce the separation as Ohm does (it is still possible to embed actions directly in the grammar).
2. Grammar Inheritance.
Although while those ideas are not common they are also not that rare (For example the same concepts exist in Antlr). I think there are three big conceptual differences.
1. In Chevrotain Performance is considered as a major feature. Which results in it being two orders of magnitude faster (in the benchmark linked above)
2. Chevrotain attempts to provides capabilities relevant for writing IDEs, for example automatic error recovery/tolerance and syntactic content assist.
3. Internal vs External DSL -
From an implementation perspective there is a vast difference as Ohm is an external DSL while Chevrotain is an internal DSL. In practical(user) terms this means that you can place a breakpoint directly in a Chevrotain grammar, but you cannot do so in Ohm. Or that you will need a separate editor to edit an Ohm grammar while you can use any JavaScript editor to create a Chevrotain grammar.
It also means that Ohm could be ported to different target runtimes (Like Antlr actually is) while Chevrotain can only run in an ECMAScript engine.
Just for fun, using the Bennu library[0] I wrote a JSON parser[1]. (Not intended for production use of course; well-optimized JSON parsers exist and browsers kinda ship with them now. If you're looking for a Bennu example or are thinking of experimenting with extending JSON in wacky ways for fun, then it's neat to mess with.) With that specific library, I seemed to create some messy parts when shuffling values through the library's stream abstraction, but I got the hang of it and the parts I thought were messy at least were straight-forward and didn't have issues like hidden edge cases. Something cool is that parsers made with the library and its stream abstraction automatically work incrementally too.
[1] https://github.com/AgentME/bennu-json/blob/701d17bc4872469dc..., right on my favorite part.
I agree that bison/yacc is a terrible user experience in a modern C++ or even modern C environment.
I wonder if I'm the only one...
I'd add that ANTLR has really good documentation.
But I really would want to read the ANTLR4 vs PCs article. I'm very happy with ANTLR4. But tools is tools.
There's a reason nearly every production parser is hand rolled, and its not simply for performance reasons.
(1) Often your grammar admits inputs which are not in your language. Less often, it forbids some inputs which are actually valid. This is usually because an accurate grammar would be extremely complex to write and inefficient to parse using a generic algorithm. Languages which combine a context-free grammar with parsing decisions based on semantic information fall into this category (think C++); the "true" grammar would usually be context sensitive and quite hard to work with.
(2) You are sometimes compiling a layered language. Again, consider C++. The program you're compiling is generally actually written in the C preprocessor language! Conceptually, it's translated to C++, and then the C++ code is compiled in a separate phase. In practice, that layering will make it difficult to produce good error messages, and a formal grammar for the combined language would be a mess to say the least.
(3) The structure of the grammar may just be awkward for producing good error messages, and refactoring it to eliminate the problem may not be possible, or it may only be possible by making problem (1) even worse.
In general, the decision of what goes in the lexer, what goes in the grammar, what is handled in semantic actions, and what is dealt with at a later semantic layer is always a trade off. Each layer has different characteristics in terms of computational power, performance, static analyzability, programming flexibility, composability, modularity, and maintainability. It doesn't make sense to insist that you solve all problems at one particular layer. You look at the problem you need to solve and decide how it'll be partitioned between these different layers to maximally benefit from their strengths and minimize their weaknesses.
That's engineering.
He had come there both to talk about ANTLR and to recruit students for his course at the University of San Francisco (IIRC).
Here's a photo I took of him with a possible future student:
https://www.flickr.com/photos/vram/31351410/
Nice chap.
Seriously, stop using CSV.
So? The parent claimed it understood everything. I was pointing out that it doesn't. There are plenty of CSV parsers out there that can pretty much handle anything you throw at them, and they work pretty well in practice.
> Seriously, stop using CSV.
I'm a consumer, not a producer. So I'll continue right on using it, thank you very much.
It would be amazing if someone wrote a gcc/clang wrapper that could detect rust files and "inline replace" them with C file equivalents
C++ would never had survived in AT&T if it wasn't zero friction compatible with C.
Cyclone, C+@ and Limbo are sadly three examples where things did not went that well at AT&T.
Ragel generates very fast code, is modular by design and has good error handling. It supports both compiled and scripted languages (e.g. can generate both Ruby and C) which is useful if you need a fallback.
I've really enjoyed using it to implement the following:
- A template language for Ruby: https://github.com/ioquatix/trenni/blob/master/parsers/trenn...
- A SGML parser for Ruby: https://github.com/ioquatix/trenni/blob/master/parsers/trenn...
- A HTTP V1 protocol parser: https://github.com/kurocha/async-http/blob/master/source/Asy...
- A URI parser: https://github.com/kurocha/uri/blob/master/source/URI/RFC398...
I'll admit, it does take a while to understand how to correctly handle ambiguity when dealing with callbacks/events during parsing, but generally speaking, once you get a bit of experience with how things work, it becomes a very powerful tool in your toolbox.
[0] https://blog.cloudflare.com/incident-report-on-memory-leak-c...
> The Ragel code we wrote contained a bug that caused the pointer to jump over the end of the buffer and past the ability of an equality check to spot the buffer overrun.
The author of this post wants uncompromising safety, ragel does not allow for that - programmer error can introduce memory unsafety. They even explicitly call out Cloudbleed.
Update: Checked it, he did:
No. There is no programming language that prevents the programmer from writing bogus code. Blaming software instability on the programming language would be like blaming unsafe building designs on the architect's drafting tools. Yes, shitty drafting tools can make certain kinds of mistakes easier, but the task of having to design something with careful thought and good engineering principles does not ever go away, no matter what kind of compass and ruler you're using.
Also, unsafe software IS dangerous, but putting it next to those actually life-threatening things actually undermines the message by the contrast.
https://jeffreykegler.github.io/Ocean-of-Awareness-blog/
Author then modernizes one of the better ones.
Is it still relevant if it has not been updated for several years?
Perhaps Ometa's younger's brother (Ohm) should have been referenced instead: https://github.com/harc/ohm Unfortunately while it seems to have some very nice features, particularly the separation of grammar and semantics.
Its performance is underwhelming. See a benchmark I've created of JSON parsers implemented using many parsing libraries.
http://sap.github.io/chevrotain/performance/
On V8 (Chrome 60) It is the slowest by far, in most cases by two orders of magnitude...
> TatSu (the successor to Grako) is a tool that takes grammars in a variation of EBNF as input, and outputs memoizing (Packrat) PEG parsers in Python.
(I'm familiar with Grako, haven't used Tatsu yet.)
Nooooo, we have to use rust, because we are not "2017" if we don't.
[0] https://en.wikipedia.org/wiki/Comparison_of_parser_generator...
PS I use C. I say this to align my post with the "Rust is the best and i use rust" mentality of the article.
EDIT: Also, this article says absolutely nothing about actually writing a parser.
The generated parsers should take the use case of untrusted input into account.
Even if they don't do it currently, it should be easier to extend a generator that will take into account while using the same grammar specification.
[0] Until they started selling the SDK as developer's edition addon
Pray tell, what are these reasons?
> otherwise we'd all be coding Ada by now
That's not why we're not all writing Ada now.
Because it takes longer, is more complicated and more difficult to design. You can't tell me that writing Rust is as easy as writing Go for example.
I'm not saying Rust's heavy typing and borrow checker don't have advantages. Of course they do. But I do think too many people pretend it is always an obvious choice to take them. In many situations a simpler, less 'perfect' language like Go, Python, or even C++ is better.
For example I don't think anyone is going to be writing AAA games in Rust any time soon, and not just because of language momentum.
So you mean it requires properly solving the hard problems implied by your application and your desired solution, instead of ignoring them and permitting latent safety and security violations.
> You can't tell me that writing Rust is as easy as writing Go for example.
GC will always be easier than lifetime checking. But that's not what you claimed: you said people have good reasons for not wanting to check all the (safety) boxes. Go also requires you to check all of its safety boxes too, so it isn't an example of what you claimed.
Only languages like C and C++ which permit violating type safety are easy are examples of being justified in not wanting to check the safety boxes. And you have unsafe Rust for when you really need it.
> In many situations a simpler, less 'perfect' language like Go, Python, or even C++ is better.
C++ is not simpler. It's hilarious that some people think "familiar" somehow means "simpler".
And Go has a runtime. The fact that it has green threads and runs a GC makes for a heavier runtime than C or Rust.
I'm not sure I get your argument about Go' boxes. The original post was clearly talking about the "extra" work a developer has to do to "satisfy" the compiler and run their code. It's a common complaint from those coming from less safe languages. I don't see how I could interpret the meaning as you suggested and still make sense of the post.
Sure I can! I've been writing Go and Rust daily for years. Both come pretty easy to me. But that's because I've been using both for quite a long time now. Do I sometimes stumble and wonder, "How do I represent my data in the cleanest way in Rust?" Sure! But the same happens in Go too.
Rust was definitely harder to learn, and it took me longer to reach proficiency. With Go, I can't recall much of a learning period. It was pretty much off to the races on day 1. With Rust, there were some growing pains for a few weeks before things clicked.
However, I don't actually disagree with your larger point! I am only one person, and what I find easy or hard might be completely different for another person. For example, I had written quite a bit of Haskell and C before coming to Rust, which turned out to be pretty good preparation. Not everyone has that background, which might make the learning experience worse (or better) than mine.
They're trying to establish that having both of those requires Rust's approach - and I think they're making a convincing argument.
It's not bug free mind you, but that class of bugs doesn't include things like buffer overruns, double frees or memory leaks (note: Rust does not guarantee absence of memory leaks, but RAII pretty much makes sure it doesn't happen in practice).
That's what I'm disagreeing with. I find Rust to be extremely pragmatic. There are other competing tools written in C++, and from where I'm standing, their maintenance story is quite a bit harder. Conversely, my tool works seamlessly on Windows, Mac and Linux.
> Still, the language I'm using interacts with the way I'm thinking; if that part isn't working out, all the features in the world isn't going to help.
Sure, and there was a lot of friction between me and Rust in the beginning. But my experience---and a lot others' experience as far as I'm aware---is that the friction settles down quite a bit. But yeah, experiences can vary there!
https://github.com/djc/askama/blob/master/askama_derive/src/...
https://github.com/djc/tokio-imap/blob/master/src/parser.rs
If you look at the parsing logic that makes up most of those files, I don't think you can reasonably argue there is a lot of ceremony going on.