Skip to content

Conversation

@Adityazzzzz
Copy link
Contributor

As requested by @rikkimax and @ibuclaw in Issue #22170.

This PR adds mul (signed) and umul (unsigned) overloads that accept an out bool overflow parameter.

Changes:

  • Added umul(Cent, Cent, out bool) for unsigned overflow detection.
  • Added mul(Cent, Cent, out bool) for signed overflow detection.
  • Implemented using the division check method (prod / a == b) to verify results.
  • Added unit tests covering signed and unsigned overflow edge cases (including Cent.min checks).

This allows callers to detect if a multiplication result has exceeded the 128-bit storage limit.

@dlang-bot
Copy link
Contributor

Thanks for your pull request and interest in making D better, @Adityazzzzz! We are looking forward to reviewing it, and you should be hearing from a maintainer soon.
Please verify that your PR follows this checklist:

  • My PR is fully covered with tests (you can see the coverage diff by visiting the details link of the codecov check)
  • My PR is as minimal as possible (smaller, focused PRs are easier to review than big ones)
  • I have provided a detailed rationale explaining my changes
  • New or modified functions have Ddoc comments (with Params: and Returns:)

Please see CONTRIBUTING.md for more information.


If you have addressed all reviews or aren't sure how to proceed, don't hesitate to ping us with a simple comment.

Bugzilla references

Your PR doesn't reference any Bugzilla issue.

If your PR contains non-trivial changes, please reference a Bugzilla issue or create a manual changelog.

Testing this PR locally

If you don't have a local development environment setup, you can use Digger to test this PR:

dub run digger -- build "master + dmd#22233"

@rikkimax
Copy link
Contributor

As I don't remember the math for integer ops, I am asking Gemini to help with the PR.

After looking at the op itself, that seems like it is ok, however it looks like you're missing some tests, please look over these and see if yours can be improved (don't copy them 1:1 confirm & cleanup first):

// --- Setup for additional test cases ---
Cent C1 = {lo: 1UL, hi: 0UL};

// Max ULONG for low part
Cent MAX_L = {lo: ulong.max, hi: 0UL};

// Represents 2^63 (0x8000...)
Cent C2_63 = {lo: 0UL, hi: 0x8000000000000000UL};

// Represents 2^126 (0x4000...)
Cent C2_126 = {lo: 0UL, hi: 0x4000000000000000UL}; 

// Represents 2^128 + 1 (for cross-check)
Cent Cross_A = {lo: 1UL, hi: 0UL};
Cent Cross_B = {lo: ulong.max, hi: 1UL}; // B = 2^64*1 + (2^64-1) = 2^128 - 1 + 2^64


// --- ADDITIONS TO UNIT TEST ---

// 1. Unsigned Corner Cases (Cross-Product Overflow)
// Cross_A (1) * Cross_B (~2^128) should overflow due to cross-product carries.
res = umul(Cross_A, Cross_B, overflow);
assert(overflow);

// Max Low * Max Low (2^64-1)^2 = 2^128 - 2^65 + 1. Should NOT overflow.
res = umul(MAX_L, MAX_L, overflow);
assert(!overflow);
assert(res.hi == 0UL); // Should only populate low part (r0, r1)

// 2. Signed Corner Cases (Magnitude and Zero/Identity)
// S-Max Mag: 2^126 * 2 = 2^127. Valid magnitude for MinS. Should NOT overflow.
res = smul(C2_126, C2, overflow);
assert(!overflow);
// Result should be exactly MinS (0x8000...)
assert(res.lo == minS.lo && res.hi == minS.hi); 

// Zero check (signed)
res = smul(minS, C2_63, overflow); // MinS * 2^63
assert(overflow); // (-2^127) * 2^63 = -2^190. Guaranteed signed overflow.

// Trivial Zero check
res = smul(maxU, Cent(0,0), overflow);
assert(!overflow);
assert(res.lo == 0UL && res.hi == 0UL);

@ibuclaw
Copy link
Member

ibuclaw commented Dec 13, 2025

Using div is way too expensive a check.

Think:

c256 = a128 * b128;
result = c256.lo;
excess = c256.hi;

When unsigned, overflow is checked by testing that the excess precision is zero

overflowed = excess != 0;

When signed, the sign of the top half of the widened result should agree with the bottom half.

if (long(a128.hi) < 0)
    excess += -b128;
if (long(b128.hi) < 0)
    excess += -a128;

if (long(result.hi) < 0)
    overflowed = ~excess != 0;
else
    overflowed = excess != 0;

@ibuclaw
Copy link
Member

ibuclaw commented Dec 13, 2025

The current implementation of mul ignores any carry in the excess precision bits. So maybe a slower implementation of mul should be added that computes and returns the excess precision via an out parameter? For example Cent mul_wide(Cent a, Cent b, out Cent excess)

@ibuclaw
Copy link
Member

ibuclaw commented Dec 14, 2025

// 1. Unsigned Corner Cases (Cross-Product Overflow)
// Cross_A (1) * Cross_B (~2^128) should overflow due to cross-product carries.
res = umul(Cross_A, Cross_B, overflow);
assert(overflow);

@rikkimax is this gemini coming up with this?

If you can point me to the proof where 1 * N is bigger than N, I'd love to see this. :-)

@ibuclaw
Copy link
Member

ibuclaw commented Dec 14, 2025

// Max Low * Max Low (2^64-1)^2 = 2^128 - 2^65 + 1. Should NOT overflow.
res = umul(MAX_L, MAX_L, overflow);
assert(!overflow);
assert(res.hi == 0UL); // Should only populate low part (r0, r1)

@rikkimax again, a simple C __int128 program test shows that the bit representation is {lo: 1, hi: ulong.max-1}

@ibuclaw
Copy link
Member

ibuclaw commented Dec 14, 2025

// 2. Signed Corner Cases (Magnitude and Zero/Identity)
// S-Max Mag: 2^126 * 2 = 2^127. Valid magnitude for MinS. Should NOT overflow.
res = smul(C2_126, C2, overflow);
assert(!overflow);
// Result should be exactly MinS (0x8000...)
assert(res.lo == minS.lo && res.hi == minS.hi);

@rikkimax and again. 2^126 is a positive signed 128-bit number. Signed Cent.min is a negative number. So overflow does occur.

At this point, I'd rather not go through the other examples, as they are up to this point all demonstrably false.

@rikkimax
Copy link
Contributor

Yes Iain Gemini came up with them, and no I don't trust them, hence why I said don't copy them and verify them.

The tests look less than full range of inputs. Hence, the suggestion to consider.

@Adityazzzzz
Copy link
Contributor Author

@WalterBright @ibuclaw @rikkimax I have updated the implementation based on your feedback to avoid the expensive division check.
Changes in this update:

  • Algorithm: Switched to a widening multiplication (calculating the full 128x128 -> 256-bit result) to detect overflow via the upper 128 bits ("excess"), as suggested by Walter.
  • Implementation: Added a private umulExtended helper that decomposes operands into 64-bit parts to handle carries manually without using div.
  • Signed Logic: Implemented the standard correction logic for signed multiplication (if (a < 0) excess -= b) to ensure the high bits are calculated correctly for signed inputs.
  • Tests: Removed the previous generated tests and replaced them with explicit boundary checks (e.g., Cent.max, Cent.min, and cross-product overflows).

Please review

@Adityazzzzz
Copy link
Contributor Author

@ibuclaw @rikkimax Thanks for the detailed review! I have addressed all your comments in the latest push:

  • Consistency: Replaced all instances of ulong / long with U / I across the entire file (including mul overloads and debug helpers).
  • Optimization: Implemented the res.hi < p.lo check to avoid the overhead of ult, as suggested.
  • Logic: Updated the signed overflow logic to use tst(com(excess)) and add(..., neg(...)) for better performance.
  • Syntax: Switched to using Cent(...) constructors consistently.
  • Tests: Added the specific edge cases you requested (including urem checks and negative multiplication).

Ready for re-review!


assert(mul(C9_3, C10, overflow) == C90_30);
assert(!overflow);

Copy link
Member

@ibuclaw ibuclaw Dec 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mind the trailing whitespace on some lines, there are a few other examples dotted around this changeset.

@Adityazzzzz
Copy link
Contributor Author

Thanks for the review! I've applied the suggestions in the latest push:

  • Syntax: Switched to named arguments (Cent(hi: ...) and Cent(lo: ...) for clearer intent.
  • Tests: Added the missing assert(overflow) check for the Cs_3 * C10 case.
  • Style: Cleaned up the trailing whitespace and removed temporary comments.

Ready for another look!

@Adityazzzzz
Copy link
Contributor Author

@ibuclaw Can you review this , if everything is alright in this PR, suitable for merging??

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants