From a400d4865dc27818f3993f600da1689b3e773c2c Mon Sep 17 00:00:00 2001 From: maximilliangrand <214999687+maximilliangrand@users.noreply.github.com> Date: Sat, 15 Aug 2026 12:10:58 +0200 Subject: [PATCH] =?UTF-8?q?Fix=20bisector=20non-termination=20on=20arrays?= =?UTF-8?q?=20larger=20than=202=C2=B3=C2=B9=20elements?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The midpoint (lo + hi) >>> 1 coerces its operand to a Uint32, so once lo + hi reaches 2³², the sum wraps and the computed midpoint falls outside [lo, hi). The search then stops converging and loops forever (freezes the tab in Safari, which allows arrays this large). Use Math.trunc((lo + hi) / 2), the form proposed by mbostock in #261: it only loses precision once lo + hi reaches 2⁵³, far beyond any array a browser can allocate. Same integer arithmetic, no measurable cost. Closes #261. Co-Authored-By: Claude Opus 4.8 --- src/bisector.js | 4 ++-- test/bisect-test.js | 18 ++++++++++++++++++ 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/src/bisector.js b/src/bisector.js index 07e4c342..d9b764e6 100644 --- a/src/bisector.js +++ b/src/bisector.js @@ -23,7 +23,7 @@ export default function bisector(f) { if (lo < hi) { if (compare1(x, x) !== 0) return hi; do { - const mid = (lo + hi) >>> 1; + const mid = Math.trunc((lo + hi) / 2); if (compare2(a[mid], x) < 0) lo = mid + 1; else hi = mid; } while (lo < hi); @@ -35,7 +35,7 @@ export default function bisector(f) { if (lo < hi) { if (compare1(x, x) !== 0) return hi; do { - const mid = (lo + hi) >>> 1; + const mid = Math.trunc((lo + hi) / 2); if (compare2(a[mid], x) <= 0) lo = mid + 1; else hi = mid; } while (lo < hi); diff --git a/test/bisect-test.js b/test/bisect-test.js index cf45842b..bb0e4523 100644 --- a/test/bisect-test.js +++ b/test/bisect-test.js @@ -165,3 +165,21 @@ it("bisectRight(array, value, lo, hi) keeps non-comparable values to the right", assert.strictEqual(bisectRight(values, undefined), 4); assert.strictEqual(bisectRight(values, NaN), 4); }); + +it("bisectRight(array, value) terminates for arrays larger than 2³¹ elements (#261)", () => { + // A virtual sorted array of length > 2³¹, without allocating it. Reading past + // ~log2(length) elements means the midpoint overflowed and the search stalls. + const length = 2 ** 32 - 1; + const threshold = 3e9; // a[i] = i < threshold ? 0 : 1; boundary is above 2³¹ + let reads = 0; + const a = new Proxy({}, {get(_, k) { + if (k === "length") return length; + const i = Number(k); + if (Number.isInteger(i)) { + if (++reads > 64) throw new Error("bisect did not converge"); + return i < threshold ? 0 : 1; + } + }}); + assert.strictEqual(bisectRight(a, 0), threshold); + assert.strictEqual(bisectLeft(a, 1), threshold); +});