Skip to content

Fix bisector non-termination on arrays larger than 2³¹ elements - #284

Open
maximilliangrand wants to merge 1 commit into
d3:mainfrom
maximilliangrand:fix/bisector-midpoint-overflow
Open

Fix bisector non-termination on arrays larger than 2³¹ elements#284
maximilliangrand wants to merge 1 commit into
d3:mainfrom
maximilliangrand:fix/bisector-midpoint-overflow

Conversation

@maximilliangrand

Copy link
Copy Markdown

Problem

bisector computes the midpoint as (lo + hi) >>> 1. >>> coerces its operand to a Uint32, so once lo + hi reaches 2³² the sum wraps and the midpoint lands outside [lo, hi). The interval stops narrowing and the loop never terminates — it freezes the tab in Safari, which allows arrays this large. This is #261, confirmed there with new Uint8Array(2147483649); d3.bisectRight(a, 0).

Repro (no giant allocation needed — a Proxy stands in for a sorted array of ~2³² zeros; a correct bisect reads ≤ log2(len) ≈ 32 elements):

let reads = 0;
const a = new Proxy({}, {get(_, k) {
  if (k === "length") return 2 ** 32 - 1;
  if (++reads > 64) throw new Error("did not converge");
  return 0;
}});
bisectRight(a, 0); // never converges before the fix

Fix

Use Math.trunc((lo + hi) / 2) — the form @mbostock proposed in #261 and @yurivish agreed to. It only loses precision once lo + hi reaches 2⁵³ (≈9 PB of indices), far beyond any array a browser can allocate, so it is strictly more robust than the lo + (hi - lo >>> 1) alternative. Same integer arithmetic, no measurable cost. This is the only >>> 1 midpoint in src (quickselect uses Math.floor).

The added regression test passes with the fix and fails without it (the loop reads past 64 elements and throws).

A note on scope

Fil raised an open question in the issue: whether d3 wants to officially support arrays this large, versus documenting a supported cap of ~2³⁰ elements. This PR is the minimal, zero-cost fix — it removes the non-termination without committing the project either way, and the call on documenting a limit is left to the maintainers.

Closes #261.

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 d3#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 d3#261.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

Incorrect results for binary search on large arrays due to miscomputation of midpoint

1 participant