Another approach to the nonzero macro

Initialization of a nonzero number, where assertion happens at compile time:

// Using macro_rules for performant compilation, tested to work in stable Rust
#[macro_export]
macro_rules! nonzero {
    ($n:expr) => {
        // This is in a const context
        const {
            // Constant assertion that works even when running `cargo check`
            const _: () = {
                if $n == 0 { ::core::panic!("Cannot be zero!") }
            };

            // SAFETY: Checked that $n is not zero
            unsafe { ::core::num::NonZero::new_unchecked($n) }
        }
    };
}

This macro accepts all numbers that can be wrapped inside a nonzero, and returns it wrapped in a nonzero struct, as long as that number is not zero. Else, an evaluation panic will happen at compile time.

This API allows following patterns:

const MY_NUM: i32 = 1234; // Some constant defined in an external crate
    
const MY_NUM_AS_NONZERO: core::num::NonZero<i32> = nonzero!(MY_NUM);
// This code panics at compile time
let zero = nonzero!(1u16 - 1);

// But this compiles
let non_zero = nonzero!(1u16 + 1);

Is this something that could be implemented into the standard library?

Note that you can already write the following:

const MY_NUM_AS_NONZERO: NonZero<i32> = match NonZero::new(MY_NUM) {
    Some(nz) => nz,
    None => panic!("Cannot be zero!"),
};

Since this is totally safe there's less of a reason to do this in the stdlib.

On the other hand I think it might be more interesting to have dedicated literals for NonZero, or if we ever get some kind of custom literal or literal macros we could use those.

4 Likes

This is mostly an answer to how you can create a Vec using the vec! macro, but for NonZero numbers instead: By being short and concise. Though the vec! may only exist because it is not possible to do Vec::from([1, 2, 3, 4]) in a constant context.

It is mostly a way to act like a literal without being one: It is much easier to implement this macro to the standard library than to make an enhanced literal system.

And this macro depends on stable NonZero anyway, so if said literal system where to be stabilized, it will conflict with it the same way as the NonZero struct does.

Also this macro will never panic in a run-time context.

Option::unwrap and expect both work in const too.

5 Likes

You can do const { NonZero::new(123).unwrap() }, and while making it a macro will make that nicer, it'll need a strong justification to be in std.

3 Likes

I don't think so. Instead, takes_nonzero(2) should just work, same as how literals are already type-overloaded so that takes_u8(2) and takes_i128(2) both work.

9 Likes

Optimally you'd just write this:

const X: NonZero<i32> = 1234;

and similarly:

const X: BigInteger = 123123123123123123123123123123123123123123131;

This could work if there was a trait for integer literals and we had const traits.

12 Likes

I agree with both scott and tczajka. This is much better than using a macro.

Why does your macro nest const blocks inside each other? I don't see how that helps. I think your macro is just unnecessarily complicated.

The same is true for const { NonZero::new(123).unwrap() }. So you could replace your entire macro by

macro_rules! nonzero {
    ($n:expr) => {
        const {
            ::core::num::NonZero::new($n).unwrap()
        }
    };
}

That is so simple that it doesn't really need a macro.

2 Likes

I wouldn't quite say it doesn't need a macro. For literals, occupying space in a larger call or construction exception, it's a lot of verbiage over a plain integer, and I'd rather have a macro for it until the plain integer works. Whether or not that macro needs to live in the stdlib is an additional question though.

3 Likes

FWIW .unwrap()/.expect() do raise "syntactical alarm" bells which need to be soothed after the fact by spotting/pointing out the surrounding const context, which entails the error would be at compile-time, rather than at runtime (where tests could potentially miss the panic path).

Granted, to palliate that the macro can very much be defined by the user as part of some utils battery; but until we get something in the language or stdlib, be it a macro, or some "magical from-literal through const traits" construction, these NonZero<…> types do feel rather second-class citizens, with higher usability hindrance than bare integers. I won't say which one would be the best solution here (between macros, some const-generic ctor[1], or whatnot), but only that we acknowledge that there is some (mild) problem at play, here

  • Let's however note that an argument for a macro or built-in sugar for NonZero literals (i.e., not allowing the OP's use case with MY_NUM) could have the advantage of, by virtue of being a syntactical check, being able to be a true compile-check-time assertion, so cargo check/clippy would catch these, rather than a post-mono error with all the limitations it has (notably, a post-mono-error based API does not seem suitable for the stdlib standard of quality, wherein cargo check and cargo build might disagree w.r.t. the success of compilation).

Though perhaps this overarching question would be more generally handled when dealing with pattern types, I don't know :thinking:


  1. impl NonZeroUsize {
       // "Compile-error" (codegen error) if `N == 0`.
       const fn new_const<const N: usize>() -> Self {
           const { Self::new(N).expect("expected the parameter to be non-zero") }
       }
    }
    
    // example:
    let two = NonZeroUsize::new_const::<2>();
    
    ↩︎