Don't Panic! Better, Fewer, Syntax Errors for LR Parsers
soft-dev.org
soft-dev.org
This paper, like some others that I've seen, treats parsing as an academic/algorithmic topic. Given some token stream we'd like to minimize some error criteria. However, these papers ignore the fact that people don't write token streams: they write code that is formatted using whitespace. I don't know any programmer that writes their source code on a single line. On the contrary, pretty much every programmer formats their source code to some reasonable (if not complete) degree. In other words: parsing error recovery is not primarly an algorithmic problem: it's a usability problem.
Newlines and indentation are a source of extra information that we can use to infer what the programmer meant. Why throw all that away during tokenization? That's crazy! We can totally use it for error recovery/error message generation.
I decided to play around with this idea in the design my programming language Muon [1]. The language uses redundant significant whitespace for parser error recovery. This simple approach by itself has turned out to work surprisingly well! In my admittedly biased opinion, it frequently surpasses the error recovery quality in mature compilers such as the C# compiler, which has had tons of effort poured into error recovery.
Of course, a purely whitespace based recovery scheme is not perfect: there are rough edges, like having to deal with tabs vs spaces, and recovering inside a line is not usually possible. But the fact that such a simple approach has led to good results makes me think this would be a great area for future research, that can perhaps combine the best of both worlds here.
[1]: https://play.rust-lang.org/?version=stable&mode=debug&editio...
IMO extra redundancy makes Greek much easier to read than Latin. The upfront learning is a bit more effort, but then reading it is much easier. Unlike Latin it has articles (which are declined and agree with the nouns/adjectives), and the endings on verbs & nouns have way less ambiguity. I can certainly see how the same would apply to programming languages.
Oh sure you can save an object file if and ONLY if it didn't change and it's cached locally.
A perfect system if you edit a file and save it 90% of the compilers work would already done and saved as part of the file format. Topical is the parser would usually have access to the previously unbroken file.
The fastest way to compile code is to not constantly recompile it from scratch.
I recently saw an example of a language that aims to be AST-first rather than notation-first: it serialises to XML! http://mbeddr.com/
This recent link was pretty good, I think https://ollef.github.io/blog/posts/query-based-compilers.htm... and this discussion with Anders Hejlsberg https://youtu.be/wSdV1M7n4gQ
That said, I think whitespace-based error recovery is a good idea and would probably still help in my case.
All that said, it's surprisingly difficult to compare error recovery in an online parser (i.e. one that's parsing as you type) to a batch parser. In the worst case (e.g. load a file with a syntax error in), online parsers have exactly the same problems as a batch parser; however, once they've built up sufficient context they have different, sometimes more powerful, options available to them (but they also need to be cautious about rewriting the tree too much as that baffles users).
in the case of python, this is part of the language. :)
This one is easily fixable: just emit a parse error if you encounter a space used as an indentation character…
Project looks great! I see on your README you're working on an LLVM backend. I'm doing the same for an unrelated language, would love to contribute to Muon if you're looking for extra hands.
If instead you want to use the full set of Rust libraries, a good place to start is https://softdevteam.github.io/grmtools/master/book/quickstar....
I hope to study this paper well (which is by the author of the above post) and understand what their algorithm is. I feel that good error messages make or break a language.
Here is the arxiv link of the same paper: https://arxiv.org/pdf/1804.07133.pdf
So for any new language, say rust, you want (ideally) parsers for about 30 or more languages in real-world use cases. And that does not include language prototyping or consider the fact that recursive-descent parsers are usually not available as libraries (say for use in an editor).
In conclusion: Having good automatic parser generators readily available is very relevant. And "good" can obviosly include usability, IMO.
But this article takes it a step further and suggests deletions, too. That's actually really cool. My biggest gripe with people making DSLs is the crappy error messages you get when you have a syntax error (GCL, I'm looking at you). Hopefully research like this will improve the lives of those of us that have to use DSLs.
[1]: http://matt.might.net/articles/parsing-with-derivatives/
I feel most parsing APIs tend to expose a single method, something like:
parser.parse(source)
... which is kind of a limited API to do the things I ask about above.With a more flexible API, an structure aware editor [1] could keep different areas of the buffer separated, and know if the user is typing a function body, a variable definition, etc, and only parse that part w/o breaking the rest of the parser progress (is this a good idea? I think so but I don't think most editors work like this, so maybe not :-)
--
An area of parser APIs that I've recently seen trouble with is lack of metadata on the productions. I'd like to be able to do something like:
production.meta() => { :line 23, :column 10 }
... or something like that. This is very useful while error reporting, for instance. I was trying to find a parser for RDF Turtle that would do this, but couldn't find any!...and this is the beauty of recursive descent: you can call the appropriate method directly to parse that part of the grammar.
I haven't read the article, so maybe this is covered later, but why isn't "Delete x" included?
int single x;
and
int x y z;
The error for
int single x y z;
Would be to delete "y" and "z", but not to delete "single"?
That said, it could be useful again, for an IDE to get the most amount of information (for completion and whatnot) even in the face of a bit of bad syntax.
The technique opens up possibilities for the compiler to get into a bit of a loop generating a large quantity of error messages, most of which result from its repair attempts. If any of the recovery strategies insert tokens, there is a potential getting into an infinite loop, if the code isn't careful.
In the 1970's, Creative Computing magazine held a contest for producing the longest ream of error messages from a single error in a program, or something like that. That would have been fueled by error recovery.
Edit: it also follows the de-facto standard for the naming of cs-papers, which demands a pun followed by a quick explanation. Brilliant!
I also just saw that it might be too late for the thesis part. A pity.
I'm reminded of the following wordplay. (I'm ashamed to say I was rather slow to catch the pun when I first heard it.)
The aim of the semantic web is to save the world.
C.java:2: error: ’;’ expected
int x y;
^
C.java:2: error: <identifier> expected
int x y;
^
This is a common problem, but the solution here for the example shown is for the parser not to attempt to recover that statement and ignore everything until it reaches the "synchronization point" (I call them "landmarks"), which sounds like what their were arguing for at first. Doing that would make the AST be marked with a node along the lines of "some int named x with errors", and would completely skip everything between the `x` and the `;`. Because an error has been reported, there will be no output binary, but the second error will not be emitted.Beyond the parser, the AST needs to keep around a lot of extra information for error recovery that would otherwise not be needed. An example from rustc[1] (what I'm familiar with) is structs and struct variants that have had some parse error. We recover the parse and keep a node for the variant in the AST signaling the existence of a parse error, which means that later passes of the compiler can type and borrow check, but if it finds a use that is either missing or adding fields we do not emit an error for it[2][3] under the assumption that the parse error caused the field being used to not be captured in the AST node for the variant. This can have quite dramatic impact on the quantity of errors being emitted.
[1]: https://github.com/rust-lang/rust/blob/7e11379f3b4c376fbb9a6...
[2]: https://github.com/rust-lang/rust/blob/7e11379f3b4c376fbb9a6...
[3]: https://github.com/rust-lang/rust/blob/7e11379f3b4c376fbb9a6...