Challenging LR Parsing
rust-analyzer.github.io
rust-analyzer.github.io
The parsing literature can be a bit difficult to navigate because the field is effectively dead for research purposes. However, other models of errors have been considered. Tim Wagner's PhD thesis is the best discussion of the application of LR parsing techniques in IDEs, and this paper extracted from it discusses error recovery in particular:
http://harmonia.cs.berkeley.edu/papers/twagner-er.pdf
In the non-interactive context, Richter's work on error recovery by substring parsing is also very interesting, in that it doesn't cause spurious errors when attempting to recover from errors, and it only depends on the language and not the grammar or parsing technique used:
https://dl.acm.org/doi/10.1145/3916.4019
For LR grammars (and many other grammars), the necessary substring parsing can be done in linear time using variations of the GLR algorithm.
I think these projects show that GLR is viable (there's a good set of grammars written for Tree-sitter), but I am also very willing to believe that the parser in rust-analyzer is more finely tuned for the specific needs of a Rust IDE.
The problem with structured approaches to incremental parsing is that you rarely want incremental parsing alone. You’re going to want to do something else with the resulting parse tree (name resolution, type checking, compilation, etc.) and no structured approach to incremental parsing will help you there. Once you’re stuck writing an incremental type checker by hand, you probably have the right framework for also writing an incremental parser.
Rust has additional complications, e.g. the macro system and the fact that the language is defined by a single implementation.
For error recovery, Tree-sitter is currently optimized for some goals that are a bit different from the goals of a single-language IDE like Rust-Analyzer:
* Like RA, we want to minimize the impact of syntax errors on basic features like syntax highlighting and symbol name extraction (which are important to GitHub, and in lightweight text editors like Neovim and Atom).
* Unlike RA, I wanted to avoid requiring grammar authors to think about error recovery at all.
* We needed to tightly control the performance cost of pathological error recovery cases, like parsing a large Ruby file as Go (either accidentally or adversarially).
My understanding is that Wagner's thesis is mostly concerned with "history-sensitive" error recovery in an IDE: using earlier syntax trees as an additional input to the error recovery process in order to better understand the user's intent when editing.I think this idea makes a lot of sense, but it doesn't address cases like Matklad has prevented here, where the IDE is presented with already-invalid file.
That matches my understanding (note that Wagner's error recovery algorithm is probably best supplemented with Lukas Diekmann's thesis, which corrects and expands it a bit https://diekmann.co.uk/diekmann_phd.pdf).
Because Wagner's algorithm relies on history and context, it does sometimes do some unexpected (and not always useful things). However, I don't see any fundamental reason why it couldn't be paired with a "traditional" error recovery approach (I have one to sell!) to try and get the best of both worlds.
At the time that I was implementing error recovery for Tree-sitter, that felt beyond my "complexity budget", since batch error recovery was already not trivial, and the batch approach was good enough for the features I was building on GitHub/Atom. Someday, I could imagine augmenting it with history-sensitive behavior. I am thankful that Lukas has helped to "pave that path" so thoroughly.
For example, in an actively edited C++ file, braces are more likely correct than angle brackets which are overloaded. So it's reasonable to auto-insert closing > if you see a } with an open <, but not reasonable to insert a } if you see a > with an open <.
lrpar has a %avoid_insert declaration which allows users to say "these token types are better avoided if there are other equivalently good repair sequences". It's simple, but it works well https://softdevteam.github.io/grmtools/master/book/errorreco...
If you had a large dataset of keystroke-by-keystroke edit streams, you'd be able to match up erroneous code with the eventual fix. Google or Facebook could absolutely capture these datasets for their internal work if they wanted, and I suspect the results could be scary-good. They wouldn't be able to release the models, though, even for their open source work (Chromium and Android), because it would have such a high chance of revealing internal secrets.
It also offers sharing refined predictions within teams.
I think we will see some interesting developments in this domain over the next few years.
[1] https://visualstudio.microsoft.com/services/intellicode/
[2] https://devblogs.microsoft.com/visualstudio/ai-assisted-inte...
But methinks it should be called a Wirth parser. Nicholas Wirth invented the same thing in the 1970's. He initially used it to parse his first language, Pascal. He drew them as Syntax diagrams: https://en.wikipedia.org/wiki/Syntax_diagram The loops that characterise Pratt are evident in those diagrams.
You can read his book on Pascal here: https://www.research-collection.ethz.ch/bitstream/handle/20.... At the end you will find the syntax diagrams for the entire language. The compiler was derived from those diagrams in the straight forward way described in the article.
And everything the article says is true, if you are going to manually write a parser, they are by far the easiest starting point. And I don't doubt error messages and error recovery are much easier too. Parser generators don't give you the same flexibility to play with a parsers guts.
But you do lose some things. A parser generator checks for ambiguities, something you can easily miss in a hand written parser. A parser generator also enforces a clean split between the parser and whatever you are doing with the syntax tree, whereas hand written parsers lead you toward a ball of mud where one lump of code has all the tasks intermixed.
And there are LR parsers out there that support the loops and priorities that raw BNF productions make so hard. Like mine :D http://lrparsing.sourceforge.net/doc/html/
Rather, just go L to R except where superceded by parens. Explicit and easy to understand.
(Though I say that not having lived it in production, lol - so take with a grain of salt)
Over time, readability/grokability tends to trump other factors...
(This does assume that op= is an expression in your language.)
Intuitive to who? :) Trivial programs are trivial. We might be stuck in the BCPL derived syntax that trade a false notion of intuitive for complexity of implentation and maintenance. (And I do mean might --- it's not hard not to think incrementally from the languages we know) --- but "inituitive" is EXACTLY the problem: I guess I was arguing we might consider searching for simple and predictable (even if that comes at the cost of slightly higher initial learning?)
Precedence and associativity is a painful aspect of parsing JavaScript… and probably C, Java and many programming languages that look like C. It took me a non negligible part of the time to think about how to handle this. With the presence of infix, suffix and prefix operators, and weird operators like new…(), ? … : …, and a lot of rules and exceptions, it's a lot of fun (for instance, yield is an operator in JavaScript. It takes an optional expression. However, 1 + yield is forbidden…). The grammar of JavaScript expressions is… not simple.
And then, you have Automatic Semicolon Insertion, which forces you to backtrack and snapshot / restore the lexer state when consuming white characters EVERYWHERE (well, not literally everywhere, but still) because things may behave differently if there is a new line. And of course, new lines cannot happen in some constructs (there are a few exceptions to the general behavior of ASI), so you need to take care of this too.
And then you have the comma operator that is allowed in some places taking an expression (especially in older constructs, and disallowed in other places (especially in newer constructs). So you need to handle this too.
Parsing the rest is easy. It's just that there are a lot of things in the language so it's a bit long to write.
In practice, C compilers warn you if you try to take advantage of && having higher precedence than ||, so other than a few very simple rules wise programmers use extra parens anyway.
The connection between APL syntax and s-expressions is surprisingly close. Start with prefix notation and right-associativity, add grouping parens, and you have something that looks like lisp. The Nial language is an outright fusion of the two that's fun and enlightening to play with.
https://en.wikipedia.org/wiki/Nial https://github.com/danlm/QNial7
As with most things, switching from too much to none is generally not an improvement.
Perhaps languages should have 8 levels of precedence instead of 18.
https://github.com/fsprojects/FsLexYacc
https://github.com/dotnet/fsharp/blob/main/src/fsharp/lex.fs...
As a sidenote, if like myself you like to dabble first before jumping in to theory, I would recommend starting with Jisson -- it's so much fun and you'll learn a lot quickly!:
There's a paper [1] about efficient parallel implementation possible.
[1] https://jyp.github.io/pdf/PP.pdf
Enjoy. ;)
parser recovery in codemirror6 (inspired by tree-sitter but adapted for IDEs):
https://marijnhaverbeke.nl/blog/lezer.html
a standalone incremental JS parser with error recovery:
Most of my poor attempts at parsing have been in the context of an offline tool. I find error handling hard enough using Yacc or Bison.
I missed the post that this one responds to, so I'm going back and reading that discussion too:
That's the hidden benefit of parsers that use DFAs: they generally don't use recursion. They will parse something until you run out of memory, not stack space (which is at least 3 orders of magnitude smaller than the heap).
Also, a significant number of successful parsers have used recursion. As long as you successfully handle stack overflow and only hit it on pathological inputs, it works reasonably well.
Interesting. Do you have any examples of that, reading the article actually had me thinking how that would be achieved.