libs-team icon indicating copy to clipboard operation
libs-team copied to clipboard

ACP: `try_exact_div` method on `NonZero<{integer}>`

Open Kixunil opened this issue 10 months ago • 19 comments

Proposal

Problem statement

Given x: NonZero<{unsigned_integer}> and y: NonZero<{unsigned_integer}> if x % y is zero then x / y is guaranteed to not be zero. In some computations one needs to compute modulo and then divide if it's zero, so it'd be helpful to preserve this property. Today, the optimizer doesn't understand this property so unsafe might be needed to optimize it rather than relying on unwrap. It'd be helpful to share the burden of reviewing the correctness among all crates that need to do this.

Motivating examples or use cases

In decimal formatting, to display the number after decimal point it's useful to normalize it by removing the trailing zeros. This can be done by dividing by 10 in a loop while the remainder is 0. While NonZero doesn't need to be used at all in the code, it's still helpful to express this property because the number after decimal point being zero is special and needs to be handled differently (not adding the dot). And it's also nice to express that the non-zero number stayed non-zero.

Solution sketch

impl<T: Div<Output=T> + Mod<Output=T>> NonZero<T> {
    /// Divides the number if it's exactly divisible.
    ///
    /// In that case the result is guaranteed to be non-zero. In case of error, the remainder is returned.
    fn try_exact_div(self, rhs: Self) -> Result<Self, Self> {
        match NonZero::new(self % rhs) {
            None => unsafe { Ok(NonZero::new_unchecked(self / rhs)) },
            Some(remainder) => Err(remainder),
        }
    }
}

Alternatives

  • Ignore this - it may be legitimately too niche. (But the function is also very simple, is it worth it?)
  • Teach the optimizer about this pattern instead, so people could use unwrap without perf hit.

Links and related work

https://internals.rust-lang.org/t/div-mod-for-nonzero-t-is-it-worth-acp

What happens now?

This issue contains an API change proposal (or ACP) and is part of the libs-api team feature lifecycle. Once this issue is filed, the libs-api team will review open proposals as capability becomes available. Current response times do not have a clear estimate, but may be up to several months.

Possible responses

The libs team may respond in various different ways. First, the team will consider the problem (this doesn't require any concrete solution or alternatives to have been proposed):

  • We think this problem seems worth solving, and the standard library might be the right place to solve it.
  • We think that this probably doesn't belong in the standard library.

Second, if there's a concrete solution:

  • We think this specific solution looks roughly right, approved, you or someone else should implement this. (Further review will still happen on the subsequent implementation PR.)
  • We're not sure this is the right solution, and the alternatives or other materials don't give us enough information to be sure about that. Here are some questions we have that aren't answered, or rough ideas about alternatives we'd want to see discussed.

Kixunil avatar May 13 '25 20:05 Kixunil

i would call it checked_exact_div, see: https://github.com/rust-lang/rust/issues/139911

programmerjake avatar May 13 '25 21:05 programmerjake

also, you need to check for signed overflow when doing i32::MIN / -1

programmerjake avatar May 13 '25 21:05 programmerjake

Yeah, that could make sense. Signed is problematic since the remainder is always zero and then the method doesn't make sense. So it should be only for unsigned integers.

Kixunil avatar May 13 '25 21:05 Kixunil

Signed is problematic since the remainder is always zero

it's still plenty useful for signed integers, just that checked_exact_div would return None in cases of overflow too. so i32::MIN.checked_exact_div(-1) == None, but there are non-zero remainders in signed division: -23i32.checked_exact_div(-10) == None and -30i32.checked_exact_div(-10) == Some(3)

programmerjake avatar May 13 '25 22:05 programmerjake

The proposed API specifically returns Result so for Signed it'd have to be Result<NonZero<T>, Option<NonZero<T>>` or some kind of error type.

Kixunil avatar May 14 '25 12:05 Kixunil

I think this is way too niche for the standard library, even though it could in theory avoid a NonZero::unchecked_new. In particular the signature doesn't match the ones for the proposed exact-div methods proposed in #337, especially the Err return type where nothing in the method name indicates that this is the remainder.

Amanieu avatar May 20 '25 18:05 Amanieu

I'm fine with "this is too niche" (I even expected it) but signatures not matching is very usual and reasonable thing to do in Rust. E.g. Option::map and Iterator::map have different signatures because it makes sense. In the internals thread I proposed a separate enum which was more readable but it increased the API urface even more.

Kixunil avatar May 26 '25 10:05 Kixunil

We've discussed this in the @rust-lang/libs-api meeting on 2025-06-03. We have two requests:

We'd like this method to be called exact_div_or_rem instead of try_exact_div. That clarifies the what it does (especially that the remainder can be returned) and doesn't get into the potentially confusing signatures as discussed above.

Second, this method could be useful for more than just unsigned NonZero integers and we want to see the whole picture between the various integer types and make sure it's consistent.

Can you please put together a table showing the main integer types, NonZero and both signed and unsigned and what would the result of this method call be?

tomassedovic avatar Jun 04 '25 10:06 tomassedovic

Is this method supposed to panic or to return an Err when an overflow or division-by-zero occurs? It seems to me that neither is a particularly good option.

The original naming of try_exact_div would preclude panicking, and exact_div_or_rem doesn't quite cover all the possibilities. Whereas it might be acceptable to return the value of 0 for a remainder with overflow (e.g. i32::MIN / -1 mathematically has a remainder of 0), for division by zero the remainder is undefined.

If I had to choose a remainder, I would pick the original dividend a = any * 0 + r therefore r = a as the remainder, but it remains an undefined operation.

wmstack avatar Jul 18 '25 06:07 wmstack

~~The solution sketch panics on div-by-zero (via calling self % rhs).~~ With the name exact_div_or_rem it is more clear that it should panic, and the non-panicking variant could be named checked_exact_div_or_rem or try_exact_div_or_rem.

If the return type Option<Result<Self, Self>> is too strange one could introduce an explicit type enum DivOrRem<T> { Div(NonZero<T>), Rem(NonZero<T>) } and make the functions return DivOrRem<T> and Option<DivOrRem<T>> instead.

kennytm avatar Jul 19 '25 05:07 kennytm

The original proposal requires unsigned non-zero, so errors are impossible. But yes, the request to include the other combinations is a significant reason why I didn't have much time to respond to this yet as it complicates things.

@kennytm it doesn't because rhs is guaranteed to be non-zero.

Kixunil avatar Jul 19 '25 15:07 Kixunil

@kennytm I like the Option<Result<A,B>> for a non-panicking variant. The only drawback I can see is not being able to distinguish the error in the None case immediately, but testing for that should be simple e.g. whether RHS is 0 or -1.

// May not panic
match a.try_exact_div_or_rem(b) {
    Some(Ok(quot)) => ..,
    Some(Err(rem)) => ..,
    None => /* unknown cause.. */
}

As for the combination of types, I would suggest the following for fixed N.

Type Panicking Behaviour Return Type
uN / uN Division By Zero Result<uN, uN>
uN / NonZero<uN> N/A Result<uN, NonZero<uN>>
NonZero<uN> / uN Division By Zero Result<NonZero<uN>, NonZero<uN>>
NonZero<uN> / NonZero<uN> N/A Result<NonZero<uN>, NonZero<uN>>
iN / iN Division By Zero, Overflow Result<iN, iN>
iN / NonZero<iN> Overflow Result<iN, NonZero<iN>>
NonZero<iN> / iN Division By Zero, Overflow Result<NonZero<iN>, NonZero<iN>>
NonZero<iN> / NonZero<iN> Overflow Result<NonZero<iN>, NonZero<iN>>

Even though the remainder is never 0 for any of the combinations, I still suggest only returning a NonZero when either of the operands is NonZero.

wmstack avatar Jul 19 '25 23:07 wmstack

@wmstack thanks for the table! I guess I agree with it mostly.

I still suggest only returning a NonZero when either of the operands is NonZero.

Why? It's easy to do NonZero<T> -> T conversion but the reverse is more annoying. Although, one can try converting upfront, handling the error and pass it to the non-zero method instead, so maybe it's not a big deal.

Kixunil avatar Jul 20 '25 09:07 Kixunil

I am happy if the remainder is wrapped in a NonZero in all cases, but @tomassedovic do you think this table adequately describes the desired combinations? This is no longer waiting on author.

wmstack avatar Aug 07 '25 00:08 wmstack

@rustbot labels +I-libs-api-nominated

tomassedovic avatar Aug 07 '25 16:08 tomassedovic

@rustbot ready

tomassedovic avatar Aug 07 '25 16:08 tomassedovic

Error: The feature shortcut is not enabled in this repository. To enable it add its section in the triagebot.toml in the root of the repository.

Please file an issue on GitHub at triagebot if there's a problem with this bot, or reach out on #t-infra on Zulip.

rustbot avatar Aug 07 '25 16:08 rustbot

@wmstack thank you! I don't have the rights to remove waiting-on-author but I've nominated it for the next libs-api discussion.

tomassedovic avatar Aug 07 '25 16:08 tomassedovic

We talked about this in the libs-api call today. We wanted to compare what the code that's motivating this would look like both with and without this method, but we were struggling to simulate that in our heads during the call.

If you could please post playground-runnable examples of motivating code using this method, and comparable code not using this method, that would be helpful to us.

traviscross avatar Aug 12 '25 15:08 traviscross