I will provide some context from having done a lot of that work.
The Rust grammar is actually quite regular, that's why we have things like the turbofish for type parameters (`binding.method::()`): it makes the grammar unambiguous (a naïve parser would with a complicated grammar that accepts chained comparisons would have to deal with differentiating between `binding.method ()` and `binding.method()`). But that doesn't mean the rustc parser doesn't do the work of supporting some the more complex grammar in order to provide better diagnostics. I like to say that rustc actually knows about meta-Rust, a daughter language that goes crazier in its features. I also joke that rustc isn't done until you can paste code from another language and following the suggestions you end up with valid Rust code without loss of the user's intent.
Part of the problem is that the places where incorrect code can fail is in more places than the parser. The chained comparisons example is one that is easy for Rust (as it doesn't support them), so the parser itself can produce a "missing turbofish" suggestion with high certainty, but for truly ambiguous expressions, the errors will happen later, during name resolution ("expected a value and found a type") or when checking the number of arguments. A production compiler needs to account for not only the original error, but also silence every knock-down error too. The simplest strategies are to just stop if at the end of a given stage there are errors (which leads to the "wave of errors" experience of fixing the "last" error leading to a ton of new ones) or fully replacing entire blocks of code that had a parse error with an AST node that acts as a tombstone marking that that later stages need to ignore it. The first option leads to a bad experience, and the latter is insufficient. A recent example of looking at this is https://github.com/rust-lang/rust/pull/159689, where `Arc::new(RwLock::new(HashMap::default()));` currently produces
error[E0423]: expected value, found struct `HashMap`
--> $DIR/suggest-turbofish-parsed-as-comparisons.rs:11:34
|
LL | let _ = Arc::new(RwLock::new(HashMap::default()));
| ^^^^^^^
|
--> $SRC_DIR/std/src/collections/hash/map.rs:LL:COL
::: $SRC_DIR/std/src/collections/hash/map.rs:LL:COL
|
= note: `HashMap` defined here
error[E0423]: expected value, found builtin type `i32`
--> $DIR/suggest-turbofish-parsed-as-comparisons.rs:11:42
|
LL | let _ = Arc::new(RwLock::new(HashMap::default()));
| ^^^ not a value
error[E0423]: expected value, found builtin type `i64`
--> $DIR/suggest-turbofish-parsed-as-comparisons.rs:11:47
|
LL | let _ = Arc::new(RwLock::new(HashMap::default()));
| ^^^ not a value
error[E0425]: cannot find external crate `default` in the crate root
--> $DIR/suggest-turbofish-parsed-as-comparisons.rs:11:53
|
LL | let _ = Arc::new(RwLock::new(HashMap::default()));
| ^^^^^^^ not found in the crate root
error[E0061]: this function takes 1 argument but 2 arguments were supplied
--> $DIR/suggest-turbofish-parsed-as-comparisons.rs:11:22
|
LL | let _ = Arc::new(RwLock::new(HashMap::default()));
| ^^^^^^^^^^^ --------------- unexpected argument #2 of type `bool`
|
note: associated function defined here
--> $SRC_DIR/std/src/sync/poison/rwlock.rs:LL:COL
help: remove the extra argument
|
LL - let _ = Arc::new(RwLock::new(HashMap::default()));
LL + let _ = Arc::new(RwLock::new(HashMap
This is because the expression is syntactically correct as
RwLock::new( HashMap ::default() );
^^^^^^^^^^^^ ------- - ---^ --- - ----------- ^
| | | | | | | |
| | | | | | | a function call to `default` in the crate root
| | | | | | a more than binop
| | | | | a value to be compared
| | | | the separator of the second argument to `RwLock::new()`
| | | a value to be compared
| | a less than binop
| a value to be compared
an associated function call
but after that PR it would only be the following, even though the parser
hasn't changed:
error: can't compare two types
--> $DIR/suggest-turbofish-parsed-as-comparisons.rs:24:41
|
LL | let _ = Arc::new(RwLock::new(HashMap::default()));
| ^ ^ these are parsed as "less than" and "greater than"
|
help: you likely intended to write type `HashMap` with type parameters, but type parameters in expression contexts require the use of the "turbofish" `::`
|
LL | let _ = Arc::new(RwLock::new(HashMap::::default()));
| ++
I think that there's a lot of work needed in the parser itself to produce good diagnostics. There are other strategies, like performing multiple parses at a given point when you've reached a known bad state (you've seen a flag-post that shouldn't be there, but that is a signal for a handful of other known cases), or fully consuming the rest of a block when an unrecoverable parse occurred (we're half-way through parsing function arguments, but failed? consume the rest of the statement or of the parent block, accounting for sub-scopes). The latter can cause
the rest of the file to be consumed, but that's an edge-case that in practice is much better than a deluge of irrelevant errors.
Another added complexity is how some easy-to-hit errors occur during lexing, which means the compiler has barely any information about the user's code. Mismatched braces/parens is one of those. rustc tries to provide context by keeping a queue of seen open delimiters to point at, and explicitly checking for their indentation level as a heuristic to detect where the user's intent diverged from the code, but that's overly reliant on the code being sanely formatted (thanks to rustfmt-on-save, that's a good bet for many users). For an example of the things rustc can do even in the lexer, you can look at https://github.com/rust-lang/rust/pull/160592.