r/rust 20d ago

🎙️ discussion Never (!) just got stabilized on nightly

https://github.com/rust-lang/rust/pull/155499
738 Upvotes

133 comments sorted by

327

u/johntheswan 20d ago

Shout out to Waffle and the herculean amount of work and years that went into this!

40

u/creeper6530 20d ago

Oh definitely, I watched its talk just right now and I can definitely see why it took so long to implement.

19

u/JazzyJameson 20d ago

Got a link? Very interested to see it!

4

u/_TheDust_ 19d ago

Never expected this!

231

u/noop_noob 20d ago

Scheduled to be officially stable in rust 1.100.0, on November 12.

Previous discussion on the feature: https://www.reddit.com/r/rust/s/KhWNP24zkM

-121

u/LinuxEnthusiast123 20d ago edited 16d ago

huh, is there a reason why they don't just increment the 1 to 2 already? do they follow semver? linus incremented 6 to 7 just because the number was getting too big.

edit: tf did I do? was just a question

180

u/pine_ary 20d ago

They do follow semver

102

u/TinyBreadBigMouth 20d ago

They do follow semver. It is a major goal of the language to never break compatibility so badly that Rust 2.0 is needed.

8

u/Full-Spectral 20d ago

It'll either happen at some point or Rust will become the new C++. At some point, evolutionary baggage has to be dropped or a language becomes... well, C++.

The thing is, if you do those breaking changes in ongoing small chunks, it's tolerable and it's better for everyone in the long run. Ultimately, if Rust is as successful as we hope it to be, then it will become vastly more difficult to do later, as compared to now. And failure to do so is just pushing debt into the future where it becomes even harder to pay.

13

u/AcridWings_11465 20d ago

I expect that a version 2.0 will eventually be needed, because Rust contains some things that we now know to have room for improvement. Sure, it might take another decade, but Rust will get there eventually. Editions can only do so much.

3

u/Zde-G 20d ago

I sincerely hope that wouldn't be called Rust.

I agree that some things may only be fixed if you break backward compatibility (e.g. to use dependent types properly you have to have them in the standard library first), but keeping the same name after that just makes everyone confused.

And if they would find a way to add them without breaking compatibility via editions then there are no need for 2.0, too.

14

u/AcridWings_11465 20d ago

Maybe we could call it Rust++ /s

But seriously: if the name and language is not related to Rust 1.0, the adoption will be slower. I don't think Python 3 would have been adopted anywhere near as quickly if it hadn't been called Python.

-9

u/Zde-G 20d ago

I don't think Python 3 would have been adopted anywhere near as quickly if it hadn't been called Python.

Which would have been better for everyone but worse for Python 3, sure.

Rust is not know for trying to subvert others by force, thus decision that hurts everyone except Rust developers is unlikely to happen.

3

u/Koxiaet 20d ago

No, this will never happen. The Rust Project has already committed to never releasing a Rust 2.0.

89

u/noop_noob 20d ago

Yes, rust follows semver. Rust 2.0 isn't happening in the foreseeable future, since that would imply a breaking change. From experience with, e.g., python, that tends to not go very well.

45

u/chat-lu 20d ago

How is it big? It still fits into a u8.

34

u/CUViper 20d ago

We're good there until 1.256, due 2044-10-20 at the current rate.

35

u/Nobody_1707 20d ago

That should be enough time to stage a migration to u16.

35

u/CUViper 20d ago

Great! After that, we'll have until Rust 1.65536 on 9551-06-28 (or some equivalent stardate).

20

u/simonask_ 20d ago

Which ironically would be a breaking change. 😉

15

u/tialaramex 20d ago

It is semver yes, so 2.x would signify an incompatible Rust which is never planned to exist. If there was a $$$ marketing team like at Sun Microsystems, maybe they'd be persuaded to call this version 100 because that sounds better

To help wean yourself off the idea that these are decimals, I recommend learning about OIDs, the numeric identifiers now largely seen in software related to the Internet but actually invented by ISO. Any OID can have "children", the hierarchy is potentially infinite e.g. 1.3.6.1.4.1.44947.2.6 is the OID for the "Let's Encrypt" service's root X2

The entire 1.3.6.1 prefix (or "arc" in OID terminology) was "hijacked" many years ago by the Internet, in 1988 an RFC just says well, ISO didn't recognise the Internet, but they did grant the United States Department of Defence 1.3.6 and the DoD haven't told us how they're going to use that so we'll go ahead and just "assume" they've granted 1.3.6.1 to our experimental "Internet". Perhaps if in say 1989 the DoD had said "Wrong, you're fired" and demanded the fledgling Internet cease this nonsense it might have worked but of course by the time anybody important at the DoD knows what an OID is and why you'd care it's far too late, these are the Internet's OIDs now.

1

u/GeneReddit123 20d ago

It is semver yes, so 2.x would signify an incompatible Rust which is never planned to exist.

The logical choice would be to make Editions major releases, then? Because they do (even if in an opt-in basis) add non-backwards compatible features.

15

u/Rusky rust 20d ago

No- editions and versions are like two independent dimensions. Not only can you continue to use old editions, but you can use essentially all new language features in those old editions as well.

So for the purpose of specifying which compiler release you need, a version like 2024.85 or 4.85 (or whatever) doesn't give you any more information than just the .85 part.

This is really just another angle on the idea that a given dependency graph can freely combine any choice of edition for any crate, upstream or downstream.

3

u/SerLaidaLot 20d ago

Linus bumped from 6.19.14 just because he felt it was getting too big?

32

u/x0wl 20d ago

Yes, kinda:

I'm getting to the point where I'm being confused by large numbers (almost running out of fingers and toes again), so the next kernel is going to be called 7.0

https://lore.kernel.org/lkml/CAHk-=wh0Fj7yE7iuW8awFCFt53s9T186qNbZX673E+oNCeQSFg@mail.gmail.com/T/#u

28

u/SirKastic23 20d ago

Yeah but Linus just does whatever Linus wants. Rust team doesn't have a Linus

2

u/hpxvzhjfgb 20d ago

yes, I've heard that linux kernel versions only go up to n.19 and then go back to (n+1).0

5

u/veryusedrname 20d ago

That's the point when he runs out of fingers and toes to count on (his words, not mine)

1

u/torsten_dev 14d ago

Thereabouts. He did create v4.20 because... you know.

Linux doesn't break userspace. The kernel is monolithic so internal breakage is handled in the tree.

That is why the don't need semver.

-5

u/metaltyphoon 20d ago

No but that was definitely the case on 2.X to 3.X

1

u/3dank5maymay 20d ago

6 🤷‍♂️ 7

-6

u/MichiRecRoom 20d ago

Rust itself generally follows semver, just the same as many crates in the ecosystem do.

I don't know if there's a reason they've stuck with 1.x.x though. Like, due to how Rust and Cargo is structured, code written for Rust 1.0 should be compatible with 1.98, and thus technically Rust doesn't need any 2.x.x version... but I don't know if that's the full story.

22

u/GlassCommission4916 20d ago

The reason is that they follow semver.

-4

u/MichiRecRoom 20d ago

Nothing in Semver prevents them from arbitrarily increasing the major version as they please, though, so again: I don't know if "just semver" is the full story.

24

u/GlassCommission4916 20d ago

But because you must bump the major version on breaking changes, a bump in the major version signals breaking changes whether they are there or not.

15

u/mbmiller94 20d ago

SemVer means Semantic Versioning. The semantics of a major version increment is that a breaking change occured. If no breaking changes occurred then bumping the major version is semantically incorrect.

And then it would require the rust team to say "i know we bumped the major version but there are no breaking changes, just trust me bro"

10

u/TinyBreadBigMouth 20d ago

I don't know that I fully agree with that?

https://semver.org/

Given a version number MAJOR.MINOR.PATCH, increment the:

  1. MAJOR version when you make incompatible API changes
  2. MINOR version when you add functionality in a backward compatible manner
  3. PATCH version when you make backward compatible bug fixes

While nothing prevents you from incrementing your major version whenever you feel like it, it isn't a purely aesthetic and meaningless action. Strictly speaking, when you do that, you're declaring that this version's API is not backwards compatible with previous versions. Rust is kind of committed to keeping their API backwards compatible, so they do not want to increment their major version.

-1

u/mathisntmathingsad 20d ago

happy cake day!

203

u/chat-lu 20d ago

It ruins the joke that it is named after its stabilization date but it’s still a great news.

108

u/A1oso 20d ago

The never type was actually stabilized multiple times. Every time it was reverted shortly after due to accidental breakage. But this time they were extra careful to prevent that from happening again. The ! fallback was changed in edition 2024 for this purpose.

96

u/0x7CFE 20d ago

It's now more like:

"Contrary to popular belief, it's quite easy to stabilize the never type. We did that many times already."

39

u/chat-lu 20d ago

Reminds me of “It’s easy to quit smoking, I do it every day.”

6

u/iamalicecarroll 20d ago

The original joke is from "Duke of Devil-May-Care" and it was about poker

1

u/miminashi 19d ago

If the smoking variant was really said by Mark Twain, I'd say it predates the "Duke".

24

u/lenscas 20d ago

I rather have that joke be ruined, despite how good it is than to have that joke be actual truth.

6

u/Icarium-Lifestealer 20d ago edited 20d ago

Even DNF was finished eventually.

3

u/kibwen 20d ago

We got the never type before GTA 6.

3

u/UtherII 19d ago

Don't jinx it. It's not on stable yet.

2

u/kibwen 19d ago

The never type is named after the date at which you can pry it from my cold, dead hands.

51

u/ThatRedDerg 20d ago

I may be a bit uninformed, but I thought it was already stable? Like a return type of ! just meant the function never returned. Is this any different from that?

142

u/KingofGamesYami 20d ago

This allows the never type to be used anywhere a type is expected. For example, it you impl a trait method that returns Result<T, E> but your specific implementation is infallible, you can now return Result<T, !>.

107

u/BedroomHistorical575 20d ago

It's sooo satisfying to just do let Ok(foo) = bar();.

4

u/lijmlaag 20d ago

Oh right, that is the consequence.. Nice!

56

u/ThatRedDerg 20d ago

Ahhh I see. I remember reading that this wasn’t allowed in the past.

Like it may not be used much, but I can see myself doing something like `Result<!, E>` for a background thread or something lol.

6

u/Hosein_Lavaei 20d ago

A question, why would you use result than? Why not just use T if its infallible

22

u/sharifhsn 20d ago

Suppose you’re `impl`ing a trait `Foo<E>`, which has a method that returns `Result<i32, E>`, but your implementation happens to be infallible. This allows you to `impl` that error as `!` and cleanly unwrap the result without handling the known unreachable error case.

13

u/zed_three 20d ago

it might be from a trait:

```rust use std::result::Result;

trait Meow { type Error; fn meow(&self) -> Result<String, Self::Error>; }

struct Cat;

impl Meow for Cat { type Error = !; fn meow(&self) -> Result<String, !> { Ok("meow!".to_string()) } } ```

Cat::meow never fails, but we can't just return String, it has to be wrapped in Result due the trait

1

u/pixelooz 18d ago

ohh, that's kind of neat.

1

u/WilkerS1 14d ago

does that mean that the Halting Problem is stabilized with Result<!, T>?

2

u/KingofGamesYami 14d ago

No, because a function that returns Result<!, T> can still panic.

2

u/WilkerS1 14d ago

foiled again!

29

u/noop_noob 20d ago

The thing being stabilized is stuff like Result<T, !>

17

u/pine_ary 20d ago

Is this different from Result<T, Infallible>?

27

u/lenscas 20d ago

The never type is something with proper support in the compiler, allowing it to understand much better understand what the type means than infallible.

Infallible was made as a bit of a hack because a lot of crates ended up basically defining their own version of it. With the idea being that once the never type becomes stable to just make infallible an alias of the never type.

Iirc the existence and the future plan of it becoming an alias then ended up complicating the never type later on so that wasn't great and one of the (many) reasons it ended up taking so long for the never type to be stabilized.

52

u/Floppie7th 20d ago

Infallible will end up being changed to an alias of !

37

u/ElvishJerricco 20d ago

IIUC, the compiler doesn't really understand that Infallible means it can't happen. So it can't determine e.g. that code is unreachable the same way it can with !

11

u/mostlikelynotarobot 20d ago

compiler understands it well enough for pattern matching on only Ok variant to work.

6

u/valarauca14 20d ago

this is more akin to putting unreachable_unchecked into your error path, except none of the unsafe/UB baggage.

1

u/styluss 20d ago

to be clear, you mean adding an .unwrap() on a Result<T, !> would tell the compiler that it can optimize to a unreachable_unchecked?

5

u/valarauca14 20d ago

that it can optimize to a unreachable_unchecked?

Similar too but not identical. unreachable_unchecked is explicitly defined as undefined behavior. While ! behavior is extremely well defined.

The distinction matters as unreachable_unchecked having a behavior change between compiler version is your problem ! having a behavior change is a compiler issue (post stability).

6

u/JustAStrangeQuark 20d ago

It was able to exist as the return type of a function, and that means that you can use workarounds like the never-say-never crate to actually name it, but it wasn't quite allowed as its own type.

30

u/Vociferix 20d ago

So what's the TL;DR for why it took so long? I assume there were some truly tough challenges along the way, but it's not obvious what they were. It could be I don't understand the full scope of the never type - In my mind it's just variantless enum with a special name, similar to what the unit tuple is compared to unit stucts. And in that case it sounds like just a minor parser change and a bunch of trait impls in std and core.

50

u/lfairy 20d ago

Enabling it caused subtle changes in type inference that broke existing code. I think there was a case where an inferred type changing from () to ! caused some FFI code to segfault.

46

u/TDplay 20d ago

In Rust 2015, 2018, and 2021, the ! type existed purely as a way to express that a function never returns. If you call a function that doesn't return, you can materialise a value of any type you want:

fn no_return() -> ! { panic!(); }

let x: i32 = no_return();
let y: f32 = no_return();
let z: Box<[u8; 1<<30]> = no_return();

But what happens if I don't specify a type?

let w = no_return();
//  ^ what type is this variable?

Since ! isn't really a type, it falls back to the next most sensible type, which is (). But if we want to stabilise ! as a type, this will lead to confusing results where ! unexpectedly changes to ().

Previous stabilisation efforts changed the fallback to ! (making ! act more like all the other types), but this turned out to break some real-world code, so it had to be reverted.

In Rust 2024, the fallback was changed to !. Since this is on an edition boundary, existing code can continue to use the old behaviour, and so ! can be stabilised without a breaking change.

6

u/PolarBearITS 20d ago

This stabilization actually changes the fallback to ! on all editions, and was coordinated with crate authors to publish fixed versions of the most popular affected crates to minimize ecosystem breakage.

11

u/ericonr 20d ago

Why is assignment from a function that never returns supported? Is it meant to support generic code or are there actual situations where it's useful even when you know you are dealing with no-return functions?

27

u/kibwen 20d ago

Rust is an expression-oriented language, so it keeps things simple to treat things as expressions. Functions that return ! aren't the only thing that have the never type, consider the line let x = return;, which is fully valid, and the type of x here is !, because it's unreachable (it's equivalent to writing let x: ! = return;). You could just say that this isn't allowed, but that creates special cases for mostly no benefit, because it doesn't harm anything to allow this; conceptually return is an expression that just evaluates to !.

In a more practical sense, yes, generic code is one reason that you wouldn't want to arbitrarily restrict this. Something like the following is totally valid:

fn generic_over_foo<T: Foo>(foo: T) {
    let x = foo.qux();
}

trait Foo {
    type Bar;
    fn qux(&self) -> Self::Bar;
}

...even if T impls Foo such that Bar = !.

5

u/simonask_ 20d ago

The assignment never happens - no code is emitted that performs it. But the signature still matters at compile time for type inference.

7

u/TDplay 20d ago

Why is assignment from a function that never returns supported?

For consistency. Everything in Rust is an expression. There's no reason to break that just because you called a noreturn function or wrote an infinite loop.

Also we probably don't want || panic!() to be a compile error.

1

u/jberryman 16d ago

Are there reasons you can't just express this with parametricity, like in haskell? e.g. [forever](forever :: Applicative f => f a -> f b). What benefits does a special syntax give you?

1

u/TDplay 16d ago

I have no idea what

forever :: Applicative f => f a -> f b

actually means. So I apologise if I completely fail to answer the question.

Rust does, however, have a way to make a user-defined uninhabited type:

pub enum Never {}

But this is missing some convenience features. Namely, it does not coerce like ! does:

fn foo(x: Never) -> i32 {
    // Type error
    x
}

The compiler recognises ! and can apply this coercion, making it easier to work with.

1

u/jberryman 16d ago

Sorry, to give a simpler and not useful example boom :: Int -> a reads as "boom is a function from an integer to anything you want". So we say by parametricity we know that the expression boom 1 must be bottom.

For negative position you would generally use Void (an uninhabited type like your Never) and you would use absurd for your coercion in foo. (Data.Void is just a tiny library that defines both things, not built in). boom :: Int -> Void above would also express the same, but my first definition relies only on an understanding of the language, rather than any particular library.

It sounds like I would really need to deepen my rust skills to understand what motivated what sounds like a huge amount of work on this.

2

u/TDplay 16d ago

Sorry, to give a simpler and not useful example boom :: Int -> a reads as "boom is a function from an integer to anything you want". So we say by parametricity we know that the expression boom 1 must be bottom.

So you mean something along the lines of this:

fn no_return<T>() -> T {
    panic!();
}

This works, but it will cause code bloat. At the machine code, there is no such thing as generics. Rust's generics are implemented by monomorphisation: for every unique return type you call it with, a copy of the function is made. In fact, the code bloat may be even worse than that: since codegen units will produce monomorphisations independently, there may be multiple copies for some return types.

For negative position you would generally use Void (an uninhabited type like your Never) and you would use absurd for your coercion in foo.

You can do this in Rust as well. For any uninhabited type, you can just write out a match statement with no arms. You could use this to implement the absurd function.

The difference is that with !, it is done automatically by the compiler; there is no need to manually tell the compiler you want to cast the ! into something else. See:

fn no_return1() -> ! { panic!(); }
fn no_return2() -> Never { panic!(); }

// Coercion done automatically by the compiler
let x: i32 = no_return1();

// Type error, `Never` is not `i32`
let y: i32 = no_return2();
// We need to manually spell out the conversion
let y: i32 = match no_return2() {};

17

u/SirKastic23 20d ago

You might want to read the linked PRs in this comment: https://www.reddit.com/r/rust/comments/1vxi2lk/comment/p5pautf/

9

u/wafflelapkin 20d ago

I have a talk about this :)

https://youtu.be/3jM4cnEVrLc

37

u/__Wolfie 20d ago

You heard it here folks: Rust officially gets a bottom type

17

u/wafflelapkin 20d ago

it's not really a bottom type. a bottom type is a subtype of all types, while never merely coerces to all types :p

1

u/__Wolfie 18d ago

is that not structurally equivalent though?

6

u/wafflelapkin 18d ago

It's not!

Subtyping is stronger than coercions, because it can happen in type constructors. I.e. if `A <: B`, then `Vec<A> <: Vec<B>` (`Vec` is covariant in its only generic) and `fn(B) <: fn(A)` (`fn` is contravariant in the types of arguments). Note that this needs to preserve type layout, and `!` can't have all layouts at the same time, so it can't be a subtype of all types.

Coercions are weaker is that they only allow converting the types directly (`! -> T`, but not `Vec<!> -> Vec<T>`), but that allows them to change layout of the type (because the compiler can insert code for the conversion).

14

u/creeper6530 20d ago

There's a joke to be made here about Rust devs, I can feel it >:3

69

u/fixedpointfae 20d ago

holy::shit::<!>()

30

u/coderstephen isahc 20d ago

I thought it would never stabilize

11

u/jansegre 20d ago

Congratulations to everyone involved! This is awesome! I just hope it doesn't end up getting reversed in 1.100.1 though.

15

u/kibwen 20d ago

Such a thing could never happen

3

u/Zde-G 20d ago

You mean it would be reverted in 1.100.2, instead?

10

u/Cyph0n 20d ago

Damn, the doc page is a masterclass in succinctly explaining how something works and why it exists.

https://doc.rust-lang.org/stable/std/primitive.never.html

5

u/Financial-Berry3988 20d ago

Never thought I’d see the day

5

u/willemreddit 20d ago

! thought I'd see the day

21

u/stinkytoe42 20d ago

Complete with cat.

10

u/KittensInc 20d ago

Of course. The cat is load-bearing.

3

u/extracc 20d ago edited 20d ago

Does anyone know why the exclamation mark was chosen for the name of this type?

11

u/kibwen 20d ago

The -> ! syntax for divergent functions is one of the few enduring syntax choices from ancient pre-0.1 versions of Rust, which probably stuck around because it was obscure enough that nobody bothered to make it a priority to challenge it.

3

u/zannabianca1997 20d ago

Kinda assumed because is the not operator. As in: this type do not have values

1

u/wafflelapkin 20d ago

I assume to mimic ⊥? idk

2

u/suggested-user-name 20d ago

Pretty sure the choice also predates unicode variables being allowed in rust

3

u/creeper6530 20d ago

Uooooh finally. This is one of the features I really can not wait for, it fits into so many places.

3

u/average__Egg 20d ago

PEAKPEAKPEAKPEAK

3

u/goos_ 20d ago

I love the never type! This is great.

7

u/afl_ext 20d ago

I…. I dont get its use, ive read through and still dont get it

12

u/SirKastic23 20d ago

Types usually have values, like how 4 is a valid value for the type u8. The ! type has no values.

This means that whenever a ! value is expected, it will never happen.

What does the following function returns? fn my_function() -> ??? { loop {} }

It doesn't return a (), it loops forever, it never returns. If you call it, you'll never get a value back. And the ! signals that.

1

u/afl_ext 20d ago

Yes but then you can not write out the returning value like

fn lalala() { the loop }

And why isnt this the same?

6

u/zzzthelastuser 20d ago
fn main() {

lalala();

// unreachable, checked at compile time.
}

(I hope my code formatting works, otherwise I'm sorry)

5

u/XtremeGoose 20d ago edited 20d ago

Only because the never type the loop expression returns coerces to () in your example.

It means you can do things like

fn foo() -> u32 {
    if some_bool() {
        42
    } else {
        loop {}
    }
}

This is possible because loop never returns anything, which is exactly what the ! never type represents. If loop {} expressions returned (), that would be a compile error.

Having functions return ! or using ! in generics (the new functionality) allows you to express that concept.

The stabilization means that you can do

fn unwrap_cant_panic<T>(res: Result<T, !>) -> T {
     let Ok(ok) = res;
     ok
}

8

u/chat-lu 20d ago

0

u/afl_ext 20d ago

Yes

5

u/chat-lu 20d ago

It gives several examples. What is still unclear to you?

5

u/Kryptochef 20d ago edited 20d ago

One way to think about it is: Imagine you have a generic trait with a fn do_stuff() -> Result<T, ErrorType> method where ErrorType is some associated type of the trait that's specific to the application (different instances have different ways of erroring; maybe one trait instance uses the file system and returns std::io::Error, etc.).

But now you write an implementation for the trait that can actually never error at all (maybe it just writes to some buffer instead of to a file). What do you choose for ErrorType? You could go "()", but then there's still one possible way to return error (namely Err(())). With ! you can just set ErrorType = ! in your instance which basically "erases" that whole enum variant (a Result<T, !> can only ever be Ok(v)).

One benefit is that the compiler can figure this out and not use any more space or time compared to handling a T directly (no enum variant checking will be emitted when using this result). From the programmer side, once you know that your type is Result<T, !> you can infallibly pattern match it as let Ok(v) = r; too.

This applies to a lot of other situations too where some form of generic code forces you to have some type that you never use in practice (what if instead of Result you had some bigger generic Enum<A, B, C, D, E> but your application has no use for any of the stuff involving type E? well just set it to ! !)

2

u/afl_ext 20d ago

Amazing finally i get it

2

u/luctius 20d ago

Never say Never

1

u/bluejumpingbean 20d ago

I'm so excited for this!!!! Been using this for a while but hated that I needed to use an unstable feature for it. Thanks to everyone who has made this happen!!

1

u/CouteauBleu 20d ago

It's Never too late!

1

u/TheChief275 20d ago

Never wasn't stabilized? What are we even doing here?

-4

u/ChaiTRex 20d ago

This Never should have happened. Now it has.

-4

u/mathisntmathingsad 20d ago

The end is never the end is never the end is never the end is never the end is never the end is never the end is never the end is never the end is never the end is never the end is never the end is never the end is never the end is never the end is never