rust-lang / rust

Empowering everyone to build reliable and efficient software.
https://www.rust-lang.org
Other
98.86k stars 12.77k forks source link

`if` and `else` have incompatible types in a `let` statement, where `else` block's evaluation will never be assigned #133316

Open shanebishop opened 4 days ago

shanebishop commented 4 days ago

I apologize if this was already reported in a separate issue, or if this is a known issue - I wasn't sure how to best search for previous issues like this.

I also realize this might not be a "bug" per se, but the other issue templates didn't seem to quite fit either.

I tried this code:

enum Cause { Cause1, Cause2 }
struct MyErr { x: Cause }

fn main() {
    _ = f();
}

fn f() -> Result<i32, MyErr> {
    let res = could_fail();
    let x = if let Ok(x) = res {
        x
    } else if let Err(e) = res {
        cleanup();
        return Err(e);
    };
    Ok(x)
}

fn could_fail() -> Result<i32, MyErr> {
    // ... code that could fail and return an Err ...
    Ok(0)
}

fn cleanup() {
    // ... cleanup code ...
}

Playground: https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=60acf4e59e3c6403104e01aa409aa395

I expected the code to compile successfully, since the else if branch unconditionally returns. Because the else if branch always returns, x will always be an i32.

Instead, I get this compiler error:

error[E0308]: `if` and `else` have incompatible types
  --> src/main.rs:12:12
   |
10 |        let x = if let Ok(x) = res {
   |  ______________-
11 | |          x
   | |          - expected because of this
12 | |      } else if let Err(e) = res {
   | | ____________^
13 | ||         cleanup();
14 | ||         return Err(e);
15 | ||     };
   | ||     ^
   | ||_____|
   |  |_____`if` and `else` have incompatible types
   |        expected `i32`, found `()`

Meta

rustc --version --verbose:

rustc 1.82.0 (f6e511eec 2024-10-15)
binary: rustc
commit-hash: f6e511eec7342f59a25f7c0534f1dbea00d01b14
commit-date: 2024-10-15
host: x86_64-unknown-linux-gnu
release: 1.82.0
LLVM version: 19.1.1
fmease commented 3 days ago

Unfortunately the diagnostic doesn't make this clear (we should fix that) but the culprit isn't the type of the else if branch, it's the type of the implicit else branch: There's an invisible else {} / else { () } at the end of the if-expression. As a result of that the type checker tries to unify three types:

  1. i32,
  2. /*unconstrained*/
  3. ()

which leads to an error because i32 and () aren't compatible.

fmease commented 3 days ago

You might wonder why there's an implicit else {} / else { () } at all if the two branches seemingly cover the whole input space (Ok(_) and Err(_) for Result<_, _>). Well, in this case the checks are two separate pattern matches from the perspective of the exhaustiveness checker and therefore the match on Err(_) doesn't count towards the exhaustiveness of the first match that contains the Ok(_)! Rust doesn't have flow-sensitive typing.

Consider turning the two 'matches' into a single one for the code to pass compilation:

let x = match res {
    Ok(x) => x,
    Err(e) =>{
        cleanup();
        return Err(e);
    }
};

You can even write it pretty concisely as:

let x = res.inspect_err(|_| cleanup())?;
fmease commented 3 days ago

For the sake of completeness, I will mention that you could theoretically adjust your if let … else if let the following way to make your code compile but that's ill-advised:

// Please DON'T! Harness the power of exhaustiveness checking instead!
let x = if let Ok(x) = res {
    x              // <-- i32
} else if let Err(e) = res {
    cleanup();
    return Err(e); // <-- /*unconstrained*/
} else {
    // We "suppress" the implicit `else {}` / `else { () }` branch by providing an explicit branch
    unreachable!() // <-- /*unconstrained*/
};