Skip to content

Partial-stabilize the basics from bigint_helper_methods #144494

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: master
Choose a base branch
from

Conversation

scottmcm
Copy link
Member

@scottmcm scottmcm commented Jul 26, 2025

A conversation on IRLO the other day pushed me to write this up 🙂

This PR proposes a partial stabilization of bigint_helper_methods (#85532), focusing on a basic set that hopefully can be non-controversial. Specifically:

Why these?

  • We should let people write Rust without needing to be backend experts to know what the magic incantation is to do this. Even carrying_add, which doesn't seem that complicated, actually broke in 1.82 (see Fix chaining carrying_adds #133674) so we should just offer something fit-for-purpose rather than making people keep up with whatever the secret sauce is today. We also get to do things that users cannot, like have the LLVM version emit operations on i256 in the implementation of u128::carrying_mul_add (https://rust.godbolt.org/z/cjG7eKcxd).
  • Unsigned only because the behaviour is much clearer than when signed is involved, as everything is just unsigned (vs questions like whether iN * iN should give (uN, iN)) and carries can only happen in one direction (vs questions about whether the carry from -128_u8 + -128_u8 should be considered -1).
  • carrying_add is the core full adder primitive for implementing addition.
  • carrying_mul_add is the core primitive for grade school multiplication (see the example in its docs for why both carries are needed).
  • widening_mul even though it's not strictly needed (its implementation is just carrying_mul_add(a, b, 0, 0) right now) as the simplest way for users to get to cranelift's umulhi, RISC-V's MULHU, Arm's UMULL, etc. (For example, I added an ISLE pattern bytecodealliance/wasmtime@d12e423#diff-2041f67049d5ac3d8f62ea91d3cb45cdb8608d5f5cdab988731ae2addf90ef01 so Cranelift can notice what's happening from the fallback, even if the intrinsics aren't overridden specifically. And on x86 this is one of the simplest possible non-trivial functions https://rust.godbolt.org/z/4oadWKTc1 because MUL puts the results in exactly the registers that the scalar pair result happens to want.)

(I did not const-stabilize them in this PR because the fallbacks are using #[const_trait] plus there's two new intrinsics involved, so I didn't want to also open those cans of worms here. Given that both intrinsics have fallbacks, and thus don't do anything that can't already be expressed in existing Rust, const-stabilizing these should be straight-forward once the underlying machinery is allowed on stable. But that doesn't need to keep these from being usable at runtime in the mean time.)

@scottmcm scottmcm added T-libs-api Relevant to the library API team, which will review and decide on the PR/issue. needs-fcp This change is insta-stable, or significant enough to need a team FCP to proceed. labels Jul 26, 2025
@rustbot
Copy link
Collaborator

rustbot commented Jul 26, 2025

r? @Mark-Simulacrum

rustbot has assigned @Mark-Simulacrum.
They will have a look at your PR within the next two weeks and either review your PR or reassign to another reviewer.

Use r? to explicitly pick a reviewer

@rustbot rustbot added the S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. label Jul 26, 2025
@scottmcm scottmcm added I-libs-api-nominated Nominated for discussion during a libs-api team meeting. and removed S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. labels Jul 26, 2025
@clarfonthey
Copy link
Contributor

clarfonthey commented Jul 26, 2025

The only issue I can see is that because carrying_mul isn't included here, there's some potential naming confusion depending on whether carrying_mul_add features both carries or only one. Effectively, I feel like this should either make the decision to permanently ditch the single-carry version or include it, so that it's clear whether the name might conflict with that version if it were added.

I also have no idea whether it'd ever be possible to have "more efficient" codegen for the single-carry version than the double-carry one, since I'm not 100% sure what versions of that operation exist across targets. For example, would it always be able to optimise the following three to the same code?

  • carrying_mul_add(x, y, z, 0)
  • carrying_mul_add(x, y, 0, z)
  • carrying_mul(x, y, z)

@TDecking
Copy link
Contributor

If borrowing_sub were to be stabilized aswell, then bignum implementations would have everything needed for the implemetation of arithmetic operations.

@scottmcm
Copy link
Member Author

scottmcm commented Jul 26, 2025

For example, would it always be able to optimise the following three to the same code?

With constant values like that I have every expectation they will, yes. They certainly do in LLVM already, since adding iconst(0) is trivially removable. Cranelift has the opt in ISLE too: https://github.com/bytecodealliance/wasmtime/blob/d2f51186ffb9fd2110d8658f215922b8398dd491/cranelift/codegen/src/opts/arithmetic.isle#L7-L11

Basically it's like how I don't think we need a dedicated a.mul_hi(b) method; a.wrapping_mul(b).1 is fine because it's easy to optimize out the unused low part on architectures that split it into two instructions -- after all, even if we did add such a method we'd implement it in LLVM by doing the double-wide multiplication and shifting anyway, since that's how it's represented there.

(And of course not adding carrying_mul now doesn't prohibit adding it later, if it turns out that it's common enough to be worth it. I just want to limit the min-stabilization as much as possible to help it land. The reason I removed the mentions of carrying_mul from the docs is that generally we don't have stable methods mention unstable ones.)

@scottmcm scottmcm force-pushed the min_bigint_helpers branch from d2dbda0 to 8ac4690 Compare July 26, 2025 18:33
@clarfonthey
Copy link
Contributor

I guess that my main point was that if we want the two-carry version to be the canonical one, and never want to add the single-carry one, it might be more reasonable to call it carrying_mul instead of carrying_mul_add, since the implication is that carrying_mul_add is carrying_mul with an extra add. I guess it's not the biggest deal, just, it feels like the name is affected by the existence of carrying_mul, which is why I bring up that it's not included.

@scottmcm
Copy link
Member Author

scottmcm commented Jul 26, 2025

since the implication is that carrying_mul_add is carrying_mul with an extra add

Hmm, my mental model was different: that it's mul_add (kinda like the f32 method) but with a carry.

(Like a wrapping_mul_add or checked_mul_add would be entirely reasonable methods to define, even if we're probably not going to be cause they're just a.wrapping_mul(b).wrapping_add(c) and try { a.checked_mul(b)?.checked_add(c)? }.)

@clarfonthey
Copy link
Contributor

From that perspective, would carrying_mul as it is right now be widening_mul_add? Since it's mul_add, but you're widening to include the full result of overflow.

I guess that would make sense.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
I-libs-api-nominated Nominated for discussion during a libs-api team meeting. needs-fcp This change is insta-stable, or significant enough to need a team FCP to proceed. T-libs-api Relevant to the library API team, which will review and decide on the PR/issue.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants