Rigorous binary64 interval arithmetic with proof-directed, correctly rounded bounds.
import { Interval } from 'interval64'
const step = 0.1
let naive = 0
let rigorous = Interval.of(0)
for (let i = 0; i < 10; i++) {
naive += step
rigorous = rigorous.add(Interval.of(step))
}
// naive = 0.9999999999999999; rigorous = [0.9999999999999998, 1.0000000000000007]The ordinary result silently picks one rounded path. The interval records every real result consistent with the binary64 inputs and operations. Each primitive endpoint is the correctly directed rounding of its exact endpoint expression; normal interval dependency can still widen a longer computation.
Zero runtime dependencies. ESM and TypeScript declarations are included.
JavaScript has no directed rounding mode. The incumbent
interval-arithmetic
works around that by moving every result one representable number outward.
That encloses the answer, but loses a ULP even when the operation was exact.
interval64 asks an error-free transform whether rounding crossed the true value and nudges only when the proof says it did. On 50,000 seeded interval pairs drawn by raw binary64 bit pattern across the full exponent range:
| operation | strictly tighter than interval-arithmetic@1.1.3 |
rate | invalid incumbent overflow results |
|---|---|---|---|
| add | 29,922 / 50,000 | 59.84% | 0 |
| sub | 29,813 / 50,000 | 59.63% | 0 |
| mul | 31,988 / 49,853 | 64.16% | 147 |
| div | 16,983 / 49,862 | 34.06% | 138 |
| sqrt | 21,941 / 37,563 | 58.41% | 0 |
“Invalid incumbent overflow” means its result collapsed a non-singleton real
range to [-Infinity, -Infinity] or [Infinity, Infinity]; those cases are
reported, but excluded from the subset denominator because the incumbent did
not return an enclosure. For every remaining defined result, interval64's
answer was a subset of the incumbent's.
A small worked receipt makes the cost of unconditional nudging concrete:
let x = Interval.of(0)
for (let i = 0; i < 1_000; i++) x = x.add(Interval.of(1))
x // [1000, 1000]
// interval-arithmetic@1.1.3: [999.9999999999252, 1000.0000000000749]Every addition there is exact. interval64 proves that and keeps all the digits; the incumbent discards roughly eleven decimal digits of interval width.
An Interval(lo, hi) denotes the closed set of real values between its bounds,
with infinite endpoints used for unbounded sets. Every operation is the natural
set extension of the corresponding real operation:
- Containment: if
x ∈ Xandy ∈ Y, then every definedx ∘ yis inX.op(Y). - Correctly rounded primitive bounds: for addition, subtraction,
multiplication, division, and square root, each endpoint is the closest
binary64 value on the required side of the exact endpoint — across the
full finite domain, including the overflow-adjacent windows near
±MAX_VALUE(an adversarial-review find; see DESIGN.md). - No false uncertainty: exact operations such as
1 + 2stay[3, 3]. - Explicit absence:
EMPTYis an empty set, not a NaN-bearing interval. NaN inputs throw.
The proof, overflow cases, and independent oracle architecture are in DESIGN.md.
Intervals are immutable. Operations return new intervals (or the shared empty value where specified).
const x = Interval.of(2) // [2, 2]
const y = Interval.of(-1, 4) // [-1, 4]
Interval.EMPTY // [+Infinity, -Infinity]
Interval.ENTIRE // [-Infinity, +Infinity]
x.add(y)
x.sub(y)
x.mul(y)
x.div(y) // hull if y contains zero
x.divSplit(y) // preserves the 0–2 connected components
x.neg()
x.abs()
x.sqrt() // intersects the input with [0, +Infinity]
x.powInt(7) // repeated squaring, parity-aware
x.min(y)
x.max(y)
x.intersect(y)
x.hull(y)
x.contains(2) // number or Interval
x.strictlyContains(2)
x.width()
x.rad() // outward upper bound on the radius
x.mid() // approximate convenience, not a bound
x.isEmpty()
x.isEntire()
x.eq(y)x / [0, 0] is EMPTY: there is no real quotient. A divisor that merely
contains zero excludes the zero point and retains the quotients for every
nonzero divisor value. divSplit returns the two disconnected rays when they
remain disconnected; div returns their hull. Thus [0, 0] / [-1, 1] is
[0, 0], while [1, 2] / [-1, 1] splits into two rays and has ENTIRE as its
hull. For an unbounded divisor, -0 and +0 bounds retain which side approaches
the excluded zero limit, allowing divSplit to keep the two components apart.
powInt(0) is [1, 1] for every nonempty interval; empty propagation takes
priority, so EMPTY.powInt(0) is EMPTY. Negative exponents use set-based
division. A non-safe-integer exponent throws.
width() and rad() return outward-rounded upper bounds and 0 for EMPTY;
rad() halves before subtracting so wide finite intervals do not overflow
prematurely. mid() is deliberately only a non-rigorous sampling convenience:
it uses an overflow-safe floating midpoint, returns 0 for ENTIRE, the
corresponding infinity for one-sided unbounded intervals, and NaN for
EMPTY.
The low-level exports are:
nextUp(x)
nextDown(x)
twoSum(a, b) // [rounded sum, exact residual]
twoProduct(a, b) // [product, residual/proxy, residualExact, signReliable]The last two flags expose the one remaining inexact case: a near-underflow residual may be too small to represent but still carries a proven sign. Both overflow windows resolve to exact residuals via power-of-two rescaling, so only non-finite inputs or products clear both flags.
Measured on Node 24.13.1, Apple M5 Max, 2026-07-11. The machine was under heavy
background load (load averages 24.12 / 24.21 / 26.84), so treat these as
reproducible order-of-magnitude receipts, not clean-room records. cyclebench
interleaves candidates to reduce drift. Each call processes arrays of 1,000
random intervals; the table divides the call time by 1,000.
| operation | interval64 ns/op | interval-arithmetic ns/op |
interval64 / incumbent |
|---|---|---|---|
| add | 101 | 289 | 0.35× |
| mul | 207 | 206 | 1.00× |
| div | 506 | 192 | 2.64× |
Addition benefits from an inlined bit-twiddling path; multiplication tied in
this run. Division is the tradeoff: proving the quotient side requires a
product transform and exact residual comparison, and cost 2.64× the incumbent.
The package targets tighter proof-backed bounds, not universal speed.
Run npm run bench for both the speed and tightness tables.
Dekker splitting multiplies each operand by 2^27 + 1, which overflows for
magnitudes above MAX_VALUE / (2^27 + 1) ≈ 2^997; and even with finite
splits, a partial product can overflow when |a·b| sits within ~2^-26
relative of 2^1024. Both windows are resolved exactly by rescaling one
operand by 2^-64 (a power of two neither rounds nor loses the residual),
so correctly rounded bounds hold across the entire finite domain. The second
window previously broke containment and is now regression-pinned. Near
underflow, the residual is recovered as a same-sign proxy after 2^54
scaling: bounds there are correctly rounded,
but a subnormal true residual has no binary64 representation, which is the
one place the machinery leans on a sign rather than an exact value.
As with every ordinary interval package, repeated variables introduce the
dependency problem: x.sub(x) generally encloses more than zero. This package
tightens floating-point rounding; it does not perform symbolic correlation.
exact-sum is the sibling package
for correctly rounded reductions. It shares the same Knuth/Dekker/Shewchuk
error-free-transform mathematics. interval64 is implemented standalone and
retains zero runtime dependencies.
The seeded suite performs two million containment checks with numbers generated
from raw binary64 bit patterns—not scaled Math.random()—covering subnormals,
signed zeros, the full exponent range, and near-overflow values. Independent
BigInt dyadic-rational oracles require bit-identical directed bounds for
addition, subtraction, and every product in the exact Dekker region. Division
and square root are checked by independently comparing exact BigInt products.
Dedicated batteries cover EMPTY/ENTIRE, zero-straddling div and
divSplit, negative and zero integer powers, signed zero, infinities, NaN
rejection, and explicit products near 2^1000. The incumbent cross-check runs
on the same full-exponent generator and prints strict-tightness counts.
CI runs test and build on Node 18, 22, and 24.
npm install interval64MIT © Xyra Sinclair