DEV Community

Evan Typanski
Evan Typanski

Posted on

Rust will `never` do that!

That is, the Rust never type (!).

Everything is an expression

Rust is kind of an "everything is an expression" type of language. An expression is something that produces a value. Not everything is an expression, but basically everything that is in a Rust body (executable code) is an expression. Actually, one thing that isn't are locals, like:

let x = 5;
Enter fullscreen mode Exit fullscreen mode

That is its own statement! But you can have an expression with a let statement, just take:


if let Some(num) = func_that_returns_option() {}
Enter fullscreen mode Exit fullscreen mode

The let statement in there isn't a local, it's a let expression.

Let's go over a few other things that are also expressions:

{ 1 }
Enter fullscreen mode Exit fullscreen mode
loop { break 1; }
Enter fullscreen mode Exit fullscreen mode
if true { 1 } else { return; }
Enter fullscreen mode Exit fullscreen mode

This last one is what we'll be interested in.

The never type

So if we look into the last example I gave, you know the if block evaluates to 1. But what about the else block? return; doesn't produce a value, it actually leaves the function. So what is its type?

That's right, the never type!

The point of the never type is to say "this computation will not complete." That way, if you assign something to that value:

let x = if true { 1 } else { return; }
Enter fullscreen mode Exit fullscreen mode

The type of x is based on the if block. But, that doesn't mean the never type is ignored. How the compiler figures that out is never can be coerced into any type. That means, when trying to infer the type for x, the compiler sees the if block has type i32. Then the else block has type never, so to make it consistent, it says "never can convert to i32" and it's all okay!

What does this mean for the user?

Well, not much. It's mostly a fun compiler intricacy. But, if you want to see it mentioned in your diagnostics, try the new let else syntax (currently only available with nightly). This syntax lets you use a pattern for a let binding that may not always be true, like:

let Ok(x) = returns_result() else { warn!("This is bad!"); return; };
Enter fullscreen mode Exit fullscreen mode

So, if returns_result returns Ok, then we assign x to the value in Ok. But, if it's not, we warn and return. Pretty easy!

The type of that else block has to be never, though.

So try making it not, with this minimal example:

#![feature(let_else)]

fn opt() -> Option<i32> {
    Some(1)
}

fn main() {
    let Some(x) = opt() else { 1 };
}
Enter fullscreen mode Exit fullscreen mode

This will error:

error[E0308]: `else` clause of `let...else` does not diverge
 --> letelse.rs:7:30
  |
7 |     let Some(x) = opt() else { 1 };
  |                              ^^^^^ expected `!`, found integer
  |
  = note: expected type `!`
             found type `{integer}`
  = help: try adding a diverging expression, such as `return` or `panic!(..)`
  = help: ...or use `match` instead of `let...else`

error: aborting due to previous error

For more information about this error, try `rustc --explain E0308`.
Enter fullscreen mode Exit fullscreen mode

We expect that else block to diverge, so it has to be the never type!

Cool.

Rust will never return from that.

Top comments (2)

Collapse
 
sfleroy profile image
Leroy

Really, this is allowed? I mean.. Any sensible compiler would normally just optimize the entire code it would never get to away. Why have code that never does anything in the binary? I seriously doubt that it doesn't. It'd probably also unroll the loop or branching statement out

Collapse
 
evantypanski profile image
Evan Typanski

Well it's not that it will never get executed. It's that an expression with the never type will never continue executing after that point. It's mainly used in cases where one branch will return a value, but another branch won't continue executing.

Though in a similar sense to what you're referring to, it can be used in a return type like exit. It's like a C++ [[no return]] annotation. When it gets to LLVM it passes that info along to do optimizations like you mention. But for the most part (with some exceptions) optimizations like that will happen during code gen, not during a phase that needs to be Rust specific (eg semantic analysis).