Skip to content

fix: correct wasm div of negative high-word dividend by -1#138

Open
spokodev wants to merge 1 commit into
dcodeIO:mainfrom
spokodev:fix/wasm-div-min-high-word
Open

fix: correct wasm div of negative high-word dividend by -1#138
spokodev wants to merge 1 commit into
dcodeIO:mainfrom
spokodev:fix/wasm-div-min-high-word

Conversation

@spokodev

Copy link
Copy Markdown

Problem

The wasm-backed signed division path returns the wrong value when dividing a negative dividend whose high 32-bit word is 0x80000000 but whose low word is non-zero, by -1.

Long.fromString("-9223372036854775807").div(Long.fromInt(-1)).toString();
// got:      "-9223372036854775807"   (the dividend, returned unchanged)
// expected: "9223372036854775807"

Long.fromString("-9223372036854775296").div(Long.fromInt(-1)).toString();
// got:      "-9223372036854775296"   (wrong)
// expected: "9223372036854775296"

Root cause

The overflow guard in LongPrototype.divide (wasm branch) is meant to catch the single true two's-complement overflow, MIN_VALUE / -1, where the result would be one larger than MAX_VALUE. It fires whenever this.high === -0x80000000 && divisor === -1 but never checks the low word:

if (
  !this.unsigned &&
  this.high === -0x80000000 &&
  divisor.low === -1 &&
  divisor.high === -1
) {
  return this;
}

MIN_VALUE is low = 0, high = 0x80000000. Any other negative value sharing that high word (for example -9223372036854775807 = low = 1, high = 0x80000000) also matches the guard and is short-circuited back unchanged, so it is never negated.

The non-wasm path does not have this issue: it guards on this.eq(MIN_VALUE), which checks both words, so it only special-cases the exact overflow. The wasm and non-wasm paths therefore disagree for these inputs.

Oracle

64-bit two's-complement signed division, BigInt.asIntN(64, a / b):

input wasm (before) BigInt.asIntN(64, a / -1n)
-9223372036854775807 / -1 -9223372036854775807 9223372036854775807
-9223372036854775296 / -1 -9223372036854775296 9223372036854775296
-9223372036854775808 / -1 (MIN_VALUE) -9223372036854775808 -9223372036854775808

Only the exact MIN_VALUE / -1 is a genuine overflow that must wrap to MIN_VALUE; that behavior is preserved.

Fix

Add this.low === 0 to the guard so only the exact MIN_VALUE is special-cased; every other value falls through to wasm.div_s. This brings the wasm path in line with the non-wasm path's this.eq(MIN_VALUE) check.

Tests

Added testSignedNegHighWordDivNegOne, which asserts the two negated results above and re-asserts that MIN_VALUE / -1 still returns MIN_VALUE. The test fails on main (returns the unchanged dividend) and passes with the fix. The full unit suite, including the closure-library tests, passes against both the ESM source and the UMD build. A 200k-case differential against the BigInt.asIntN(64, a / b) oracle, biased toward the 0x80000000 high-word edge, matches on every case.

The wasm-backed signed division short-circuits and returns the dividend
unchanged whenever this.high === 0x80000000 and the divisor is -1. That
guard is meant to catch only the true two's-complement overflow of
MIN_VALUE / -1, but it omits the low-word check, so any negative dividend
sharing that high word (for example -9223372036854775807) is wrongly
returned unchanged instead of being negated.

Add this.low === 0 so only the exact MIN_VALUE / -1 overflow is special
cased; every other value falls through to wasm.div_s. This matches the
non-wasm path, which guards on this.eq(MIN_VALUE).
@dcodeIO dcodeIO added the bug label Jun 30, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants