Coersion of unsuffixed numeric literals to nonzero integers

Prior discussion regarding suffixed nonzero literals: Pre-RFC: ergonomics around NonZeroU* and literals

Would it be possible to permit coersion of unsuffixed numeric literals to nonzero integers? This would permit things like let _: NonZeroU8 = 50;. More notably, it would greatly reduce the friction when passing a NonZeroX as a parameter — right now the user has to explicitly construct it even for static cases, where the compiler could plausibly do this automatically.

Zero would be excluded for obvious reasons. I am not proposing a new suffix for nonzero literals, as was the case in the linked thread from a couple years ago.

10 Likes

What is wrong with let _ = NonZeroU8::from(50) or let _: NonZeroU8 = 50.into().

Also, in a function call:

fn foo( val : NonZeroU8 ) { ... }

foo( 50.into() )

Is not From and Into implemented for NonZeroU8?

It is not: Rust Playground

This isn't possible because you can't create a NonZero* from 0, but 0 is a valid value of integer types. You can implement TryInto instead, but it wouldn't be very different from the new method of NonZero* types, which returns Option<NonZero*>.

5 Likes

feature(generic_const_exprs) to the rescue!

#![feature(generic_const_exprs)]

use std::num::NonZeroU32;

pub trait True {}

pub struct If<const B: bool>;

impl True for If<true> {}

/// Builds a `NonZeroU32` from a constant expression.
/// Passing an expression that evaluates to zero to 
/// `nonzero` results in a compile-time error.
///
/// # Examples
///
/// ```
/// # use playground::nonzero;
/// let nz = nonzero::<42>();
/// assert_eq!(42, nz.get());
/// ```
///
/// ```compile_fail
/// # use playground::nonzero;
/// let nz = nonzero::<0>();
/// ```
///
pub fn nonzero<const N: u32>() -> NonZeroU32
where 
    If<{N > 0}>: True
{
    NonZeroU32::new(N).unwrap()
}
6 Likes

I'd still vastly prefer coercion in this situation, as it eliminates any need to be explicit when calling a function, for example.

1 Like

Framing this as cost vs benefit: the cost here is making the language harder to learn, because we add a range of new types to the language as potential (implicit) coercion targets; the benefit is less code needed to make use of the NonZero types. As such, I'm not sure this is such an easy sell.

Also, it would probably make sense to at least hold off until further constification is far enough along that creation of these types can be simpler/more concise due to const constraints, so we can see, at that time, whether we still think it makes sense to make this more ergonomic.

3 Likes

50.try_into().unwrap() compiles to nothing. So the question could be reframed whether this pattern should have a syntax sugar.

The same is useful for slice to array conversions, e.g. slice[..10].try_into().unwrap() as [_; 10]

2 Likes

I'd love to get some sort of custom literal thing, so that, say, let x: BigUInt = 4; could also just work. With let x: NonZeroU32 = 0; emitting the result's error or the panic message as a compilation error.

I think this is a different problem, because slice[..10] has its current type and it would probably be a breaking change to change it. slice.get::<..10>() seems more like a feasible direction for that scenario. Or slice.prefix(), I guess, which could infer the length. (Could even make a slice.try_prefix() that'd work today.)

1 Like

However, 0.try_into().unwrap() compiles to a runtime panic, which is suboptimal.

2 Likes

It's worth noting that it is possible to construct a nonzero value in a const context, which implicitly allows its static construction on any context:

let _ = {
    const X: NonZeroU8 = unsafe { NonZeroU8::new_unchecked(5) };
    X
};

This is a compilation error on zero. Inline const of course makes this simpler, but still not ideal.

I would eventually like to see custom literals to an extent, but I'm proposing this as a stepping stone. I want to write an RFC for ranged integers at some point, which would almost certainly need interaction with literals to be anywhere near sensible. One of the main reasons (imo) that nonzero numbers are not more widespread is the relative difficulty of using them compared to native integers. My hope was to get consensus on this clear, unambiguous, zero-cost numeric usage while remaining a small change overall.

1 Like

I don't think this is something on which it's acceptable to rely (emphasis added below):

When UB arises as part of CTFE, the result of this evaluation is an unspecified constant, i.e., it is arbitrary, and might not even be valid for the expected return type of this evaluation. The compiler might be able to detect that UB occurred and raise an error or a warning, but this is not mandated, and absence of lints does not imply absence of UB.

https://rust-lang.github.io/rfcs/3016-const-ub.html#reference-level-explanation

2 Likes

Correct. I should have clarified it's currently a compilation error. It's not guaranteed, but I'd be shocked if it ever regressed. Not that that's the point, admittedly.

In "official" rustc, I certainly agree.

That said, I wouldn't be surprised at all if an alternative implementation of rust implemented the non-zero types with just a library invariant, at least initially, instead of the detectable language invariant.

(And, as you also said, that's not the point of my statement.)

but what you can do is rely on cg:

use std::num::NonZeroI32;

unsafe trait IntoNonZero {
    type NonZero;

    unsafe fn into_nonzero(self) -> Self::NonZero;
}

unsafe impl IntoNonZero for i32 {
    type NonZero = NonZeroI32;

    unsafe fn into_nonzero(self) -> Self::NonZero {
         NonZeroI32::new_unchecked(self)
    }
}

struct Bool<const B: bool>;

macro_rules! nonzero {
    ( $E:expr ) => {{
        const  _:  $crate::Bool<true> =  $crate::Bool::<{ $E != 0 }>;

        unsafe { $crate::IntoNonZero::into_nonzero($E) }
    }}
}

fn main() {
    let _ = nonzero!(0); // ce
    let _ = nonzero!(45);
}
1 Like