Unexpected location for compiler error reported

Referring to this SO question: why is the error reported on line 188 (the last match arm)? Why not already at the first match arm?

(This feels like a users question, but it's about compiler internals, so I guess it's also ok here?)

The reason the error is reported where it probably depends on subtle details of the compilation pipeline and queries, but can be understood as follows:

match is a control flow graph branch structure. While you read it with branches in order, the compiler sees it as an unordered set of control flow edges; effectively a lookup table. So which arm it processes (for the purpose of borrow and type checking and such) doesn't really matter, and is fairly arbitrary.

Trying to be nice, the compiler sees that it has already emitted a "used place before initialization" error and doesn't emit one for every arm. So you only get the error for whichever arm the compiler visited first, which is arbitrary.

If you clear the compilation data (cargo clean) and compile again, I would not be surprised to the error be produced by a different arm (though it would be unfortunate if the error diagnostics are nondeterministic, to be fair). More likely, though, is that the compiler has a stack of things it needs to check, which is populated from the top down, so barring other dependencies, gets checked bottom up. A stack is much easier to implement and use than a queue, and involves less work when growing, so that would explain why the compiler evaluation would use a stack over a queue (which would produce the error sooner in the file).

I went ahead and reported a bug for this. It definitely would be better to report the textually first violation in this case.

That's not quite right, though. The order of the match arms has semantic meaning; hence why you have to put the wilcard arm of your match last.

The reason I point it out, is that I'd expect the compiler to behave as if it were reading each match arm in lexical order. Any optimization that processes them in a different order or parallelizes them should guarantee that the observable result is the same as if they'd been processed sequentially.

I wonder if there are other places where you might get error messages in match arms ordered in a counter-intuitive way.

The order does matter, however only for the patterns and guards. As soon as you enter the arm expression, there’s no fallthrough or whatever, hence for analysis about initialization and moves, they are just parallel and error reporting order is arbitrary. Also we need to keep in mind that this analysis is non-trivial; working with loops, branches and even on individual fields of structs. Furthermore with move-semantics stuff can become uninitialized again. I wouldn’t be surprised if there was some kind of graph-traversal involved.

Regarding your question if other things consider match arms in a backwards order, too: I found it applies to moved-out values, too (probably the same code handling both initialization and move):

struct X;

fn main() {
    let x = X;
    drop(x);
    match 5 {
        1 => {x}
        2 => {x}
        3 => {x}
        4 => {x}
        _ => {X}
    };
}
   Compiling playground v0.0.1 (/playground)
error[E0382]: use of moved value: `x`
  --> src/main.rs:10:15
   |
4  |     let x = X;
   |         - move occurs because `x` has type `X`, which does not implement the `Copy` trait
5  |     drop(x);
   |          - value moved here
...
10 |         4 => {x}
   |               ^ value used here after move

Interestingly, when trying assigning twice, there is an error reported on each arm

struct X;

fn main() {
    let x;
    x=X;
    match 5 {
        1 => {x=X}
        2 => {x=X}
        3 => {x=X}
        4 => {x=X}
        _ => {}
    };
    drop(x);
}
   Compiling playground v0.0.1 (/playground)
error[E0384]: cannot assign twice to immutable variable `x`
 --> src/main.rs:7:15
  |
4 |     let x;
  |         - help: make this binding mutable: `mut x`
5 |     x=X;
  |     --- first assignment to `x`
6 |     match 5 {
7 |         1 => {x=X}
  |               ^^^ cannot assign twice to immutable variable

error[E0384]: cannot assign twice to immutable variable `x`
 --> src/main.rs:8:15
  |
4 |     let x;
  |         - help: make this binding mutable: `mut x`
5 |     x=X;
  |     --- first assignment to `x`
...
8 |         2 => {x=X}
  |               ^^^ cannot assign twice to immutable variable

error[E0384]: cannot assign twice to immutable variable `x`
 --> src/main.rs:9:15
  |
4 |     let x;
  |         - help: make this binding mutable: `mut x`
5 |     x=X;
  |     --- first assignment to `x`
...
9 |         3 => {x=X}
  |               ^^^ cannot assign twice to immutable variable

error[E0384]: cannot assign twice to immutable variable `x`
  --> src/main.rs:10:15
   |
4  |     let x;
   |         - help: make this binding mutable: `mut x`
5  |     x=X;
   |     --- first assignment to `x`
...
10 |         4 => {x=X}
   |               ^^^ cannot assign twice to immutable variable

error: aborting due to 4 previous errors
1 Like

Yeah, I get this is non-trivial.

Ideally, we'd want the compiler to make a list of all errors in the match arms, hold-off on printing them, and then remove duplicate errors in such a way that each error displayed is the closest one to its "source" among duplicates.

It'd probably be a pain and a half to implement, though.