Compile-time faillable expression

It is common to have a type that uphold invariants and have a faillable constructor that can be a const fn. An example of this is NonZero.

When you need to create a value from constant values you know are valid you end up to have to use .unwrap(), which even if the compiler will optimize out the failling branch, feel "wrong" since using unwrap is generally a bad practise. An alternative to this would be to wrap the expression in a const { } block, so that if unwrap would panic, it will panic at compile-time instead of runtime. But it makes the code more verbose.

let x = const { NonZero::new(5).unwrap() };

I propose to add a new syntactic sugar for this situation, something like the postfix operator ?. Let use ! (bikeshedding).

So that <expr>! becomes something like this:

const {
    match Try::branch(<expr>) {
        ControlFlow::Continue(x) => x,
        ControlFlow::Break(_) => panic!(),
    }
}

Or at least something equivalent as using trait in const context and the Try trait itself are currently unstable.

Our previous example becomes:

let x = NonZero::new(5)!;
7 Likes

I think this could be solved by pattern types. Essentially, it would work like this:

// Make NonZero field public
pub struct NonZero<T: ZeroablePrimitive>(pub T is ..0 | 1..);

// Then can use it like this 
let x = NonZero(1);

// Or redefine `NonZero::new` (less likely)
impl<T: ZeroablePrimitive> NonZero<T> {
    pub fn new(value: T)
    -> Option<Self> is (Some if value is (..0 | 1..));
}

// Then can use it like this
let Some(x) = NonZero::new(1); // infallible pattern matching

For now, the cleanest way is probably using a macro such as

macro_rules! nz {
    ($e:expr) => { 
        const { NonZero::new($e).expect("zero is not a valid NonZero value")
    }
}

Now that we have const blocks, using macros to emulate custom literals isn't actually too terrible.

4 Likes

Or just a const function:

impl<T: ZeroablePrimitive> NonZero<T> {
    fn new_const<const n: T>() -> NonZero<T> {
        const { NonZero::new(n).unwrap() }
    }
}

(this exact code would need a bit of compiler magic, but only for allowing a generic type in a const generic) Here's an already working version:

3 Likes

I’d really like to see the language offer something in this space. I’ve used a lot of restricted numeric types (NonZero, NotNan…) and it’d be nice to have a uniform, concise syntax for fallible construction of constants. Each numeric type providing its own macro is possible, but macro_rules! can’t be directly exported from a crate’s modules except as reexports, which makes the public API clunky unless the number library goes to the length of having a helper library that defines the macro.


For prototyping, we can write a macro today on stable which approximates @tguichaoua’s semantics, for Result and Option but not other types:

macro_rules! k {
    ($e:expr) => { 
        const { $e.unwrap() }
    }
}

(Obviously this isn’t very type-safe because it just invokes whatever the unwrap method is. But until we can use traits from const contexts, this is what we can reasonably do.) Then if this is combined with a plain function, it can be short if still rather punctuation-heavy:

const fn nz(x: u32) -> Option<NonZero<u32>> {
    NonZero::new(x)
}

fn main() {
    let x = k!(nz(3)).saturating_add(2);
    println!("{x}");
}

Playground


Regarding what the language could offer, here’s another idea: what if ? was usable directly in const blocks, and what it branches to is a compile-time error (which is unambiguous because there is no enclosing function to return from)? That way, writing

let three = const { nz(3)? };

would have the desired effect. It’s not as concise as a dedicated syntax, but it introduces no new syntax at all — it’s a reasonable extension of existing elements, and sort of fits the precedent of giving ? a particular branch destination inside const {} is very much like giving it a particular branch destination inside try {} (unstable).

10 Likes

I don’t see what’s wrong with unwrap (or expect) in the first place. Sometimes you have more information than the compiler; this is true at compile time and at run time. I agree it’s reasonable to have a shorthand for converting literals (like the notzero crate or the hex-literal crate), but once you’re writing expressions, you might as well keep writing expressions.

2 Likes

I actually quite like this. Basically:

macro_rules! eval {($($tt:tt)*) => { const {
    ::core::result::Result::unwrap(try { $($tt)* })
}}}

I might stick this in my toolbox of useful macros alongside yeet!, ix!, whoops!, and impl_op!.

It's far from perfect, but you can write

#[doc(hidden)]
#[macro_export]
macro_rules! __my_macro { … }

/// Documentation for my_macro!
#[doc(inline)]
pub use __my_macro as my_macro;

to document a module scope macro item without the crate root item today. Rustdoc changed how hidden and inline interact to make this work a while back.

5 Likes

I believe oli-obk is gonna be working on "ranged integer types" with something like type NonZeroInt = i64 is ..0 | 1..;, but that will likely not be ready "soon".

4 Likes

I really like this, and I think it's probably worth making into its own post, perhaps a Pre-RFC.

2 Likes

I may do just that. It’ll be pretty short!

Posted: Pre-RFC: `?` operator in constants

“Pretty short”, I said. Well, the definition is much shorter than the rationale.

6 Likes

I also think the const { <expr>? } syntax is preferrable, as <expr>! could lead to further abiguity:

const k: Option<[u8; 5]> = ...;
let x = k![3];

And more importantly, it establishes a hidden const block, which might confuse people, because just adding one symbol changes what you can do in the entire (previous) expression.

But great idea, would make things more ergonomic!

3 Likes