Compile-time const unwrapping

Now that e.g. NonZeroU8::new is const, how far away would it be to actually check / unwrapp the result (well, Option, in this case) in compilation?

It would be really cool (and a great stepping-stone to actually parsing stuff compile-time) if it would be possible to write somteing like:

const FOO: NonZeroU8 = NonZeroU8::new(17).unwrap();

... and also, then, have this code result in a compile-time "unwrapped None" error:

const FOO: NonZeroU8 = NonZeroU8::new(0).unwrap();

There’s already #![feature(const_panic)]. Once that gets stabilized, the only other potential blocker would be const pattern matching (I don’t know if this is currently possible or not).

2 Likes

Cool! Pattern matching in const fn is already stable, so this actually gives a nice compilation message (with a nightly compiler), and changing the 0 to something non-zero gives a working program.

#![feature(const_panic)]
use std::num::NonZeroU8;

const FOO: NonZeroU8 = match NonZeroU8::new(0) {
    Some(v) => v,
    None => panic!("Bad value"),
};


fn main() {
    println!("Hello, {}!", FOO);
}

Edit: Here's the tracking issue for const_panic.

Note that if you do this:

const FOO: NonZeroU8 = unsafe { NonZeroU8::new_unchecked(0) };

the compiler will catch zeros with an error at compile time.

error[E0080]: it is undefined behavior to use this value
 --> src/lib.rs:4:1
  |
4 | const FOO: NonZeroU8 = unsafe { NonZeroU8::new_unchecked(0) };
  | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ type validation failed: encountered 0, but expected something greater or equal to 1
  |
  = note: The rules on what exactly is undefined behavior aren't clear, so this check might be overzealous. Please open an issue on the rustc repository if you believe it should not be considered undefined behavior.
3 Likes

What I really want to do is things like:

const FOO = NonZeroU8::new(env!("FOO").parse()).unwrap();

and of course the holy grail:

static PATTERN = Regex::new("patt[er]*n").unwrap();

The second of these require allocation in a static context, so that is very far from what can currently be done. The first can be done without allocation, but having a const parse function would require a const FromStr trait, which would make allocation impossible in other FromStr impls. So that is kind of impossible to. Unless there was to be a separate const_parse method and a separate ConstFromStr trait. Which may or may not be worth having in std, but could start of in a separate crate.

Addendum: One such separate crate seem to be https://crates.io/crates/const_env (it does its "parsing" by just dumping the value in the source, so even expressions work, including use of other const values in the program, which may be awessome or ... "surprising" ... depending on how you look at it).

Here is one hack I figured out to unwrap an Option:
https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=640e54836349c1eb1bb55ad2d802069a

use std::num::NonZeroU8;

macro_rules! unwrap {
    ($e:expr $(,)*) => {
        match $e {
            ::core::option::Option::Some(x) => x,
            ::core::option::Option::None => {
                ["You tried to unwrap a None!"][10];
                loop{}
            },
        }
    };
}

pub const FOO: NonZeroU8 = unwrap!(NonZeroU8::new(0));

which produces this error

error: any use of this value will cause an error
  --> src/lib.rs:8:17
   |
8  |                 ["You tried to unwrap a None!"][10];
   |                 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ index out of bounds: the length is 1 but the index is 10
...
15 | pub const FOO: NonZeroU8 = unwrap!(NonZeroU8::new(0));
   | ------------------------------------------------------
   |                            |
   |                            in this macro invocation
   |
   = note: `#[deny(const_err)]` on by default
   = note: this error originates in a macro (in Nightly builds, run with -Z macro-backtrace for more info)

This works from Rust 1.46.0 onwards, which stabilized branching and looping in const contexts.

5 Likes

You can almost replace that macro with a const fn. Writing it specifically for Option<NonZeroU8> works:

const fn unwrap(opt: Option<NonZeroU8>) -> NonZeroU8 {
    match opt {
        Some(x) => x,
        None => {
            #[allow(unconditional_panic)]
            ["You tried to unwrap a None!"][10];
            loop {}
        }
    }
}

But replacing the parameter with a generic makes the compiler complain about running destructors at compile time. This feels like a bug to me: the match always moves out all of the enum’s component values, so there’s nothing left to destruct at the end of the function.

   Compiling playground v0.0.1 (/playground)
error[E0493]: destructors cannot be evaluated at compile-time
  --> src/lib.rs:3:20
   |
3  | const fn unwrap<T>(opt: Option<T>) -> T {
   |                    ^^^ constant functions cannot evaluate destructors
...
12 | }
   | - value is dropped here

(Playground)

1 Like

This is because Rust is currently imprecise about whether values are dropped in a const context.

I reported this exact problem a year ago in https://github.com/rust-lang/rust/issues/66753 ,and it was fixed with the const_precise_live_drops unstable feature(tracking issue):

https://play.rust-lang.org/?version=nightly&mode=debug&edition=2018&gist=fbfdaa9988acb76dd7cf6e386f2cbc76

#![feature(const_precise_live_drops)]
#![feature(const_panic)]

const fn unwrap<T>(opt: Option<T>) -> T {
    match opt {
        Some(x) => x,
        None => panic!("Trying to unwrap a None"),
    }
}

1 Like

Wow, that's really neat.

It is perhaps worth noting that this doesn't catch some cases like enum using as on an repr, without const_panic, in cases it could in theory catch statically.