Skip to content

Archive Survey Browser Math Libraries

enumeratio edited this page Sep 29, 2026 · 1 revision

Survey: browser/JS math libraries + off-thread compute patterns

Carried over from the earlier enumeratio. Written to feed a provideEngine/SQL client abstraction that no longer exists — the current engine is compute-engine, which has its own arbitrary-precision numeric story (see the BigDecimal ζ kernel and the arbitrary-precision map across heads). The library survey itself (big.js, bignumber.js, decimal.js, BigInt) is still accurate prior art and hasn't been superseded by anything in design/.

Prior-art survey for the provideEngine model (Async-Calc-Engines, #269) — specifically feeding the ts-engine (precision story), the future wasm-engine (#280), and the queue-based calc worker (#279, bounded/cancelable calc). Doc-verified against project homepages/READMEs, not run against an oracle — nothing here is wired into the codebase yet.

1. Arbitrary-precision numerics in JS

pg gives us arbitrary-precision numeric for free; JS has no built-in decimal type and BigInt only covers exact integers. Three long-standing sibling libraries (same original author for two of them) cover most of the gap:

Library Model Precision Size (min+gz) Fits
big.js decimal, base-10 only arbitrary, decimal-places-based ~6 KB, half the API of bignumber.js simplest case — money-style decimal add/sub/mul/div, no trig
bignumber.js decimal + non-decimal bases arbitrary, decimal-places-based ~8 KB general arbitrary-precision decimal, binary/octal/hex conversions
decimal.js decimal arbitrary, significant-digits-based (vs. decimal-places) largest of the three — adds trig/log/pow for non-integers scientific work; also the basis for the emerging Decimal128 type
native BigInt integer only arbitrary (integers), no fractions/decimals built-in, zero cost exact integer math (factorials, binomial coefficients, cardinalities) — enumeratio's most common case

None of the decimal libs do rationals — for exact fractions, the ecosystem reaches for a different family: Fraction.js (BigInt-backed numerator/denominator, auto-reduces to lowest terms), BigRational.js, and the narrower bigint-rational (thin BigInt-pair wrapper, less overhead than a full rational-number ADT). None of these have pg's single unified arbitrary-precision numeric type spanning integer/decimal/rational uniformly — JS forces a choice per value kind.

Relevance to enumeratio: ts-engine's primitive subset is overwhelmingly exact-integer (cardinalities, ranks, binomial-type counts) — plain BigInt covers that with zero dependency weight. Decimal/rational needs (stat aggregates, probabilities) are the minority case and would pull in Fraction.js (rationals) or decimal.js (arbitrary decimal) only where pg's numeric/rational types are actually exercised — no single JS lib replaces pg's unified numeric tower, so ts-engine's capability declaration should be honest about which value kinds it covers before falling back to pg-engine.

2. General math libraries

math.js (mathjs) is the dominant general-purpose JS math library: an extensible expression parser, a full type system (numbers, BigNumbers, Fractions, complex, units, matrices), and a plugin/import mechanism for adding custom functions and types. It is synchronous only — no built-in worker or async compute story. A long-standing GitHub issue about running math.js inside a Web Worker turned out to be an unrelated console polyfill problem, not evidence of any worker-aware design in the library itself; math.js has no queue, no cancellation, no progress channel — any off-thread use is entirely the caller's responsibility (wrap the whole call in a worker + Comlink, see §4). Its extensibility model (typed functions dispatched by argument type, import()-able custom types) is the closest JS analogue to pg's extendDb.

Relevance to enumeratio: math.js is a plausible model for ts-engine's API shape (typed dispatch, extensible function registry) but not for its execution model — mathjs proves that "general math library" and "off-thread/cancelable" are orthogonal concerns in this ecosystem; enumeratio's queue-worker layer has to be built separately regardless of which numeric core it wraps.

3. WASM math

For the primitives above, compiling C libraries to WASM buys real arbitrary-precision performance:

  • gmp-wasm — GMP (integers, Integer/MPZ) + MPFR (arbitrary-precision floats, Float) + rationals (Rational/MPQ), wrapped in a high-level JS API with the raw C functions also exposed for low-level control. Runs in browsers, web workers, Node, and Deno; bundled as a base64 WASM blob with zero external dependencies; a "mini" build drops Float/MPFR for a smaller bundle when only integer/rational is needed. In some benchmarks it beats native BigInt on heavy integer workloads — the interop cost of crossing the JS↔WASM boundary is paid once per call, not per digit.
  • mpdecimal — correctly-rounded arbitrary-precision decimal arithmetic (the reference implementation behind Python's decimal module), with complete WASM test coverage and Emscripten support for C/C++ → WASM builds; no turnkey npm wrapper as widely used as gmp-wasm's, so adopting it means building/wrapping the WASM binary directly.
  • Number-theory/combinatorics-specific WASM: no turnkey npm package. FLINT (the library SageMath itself uses for number theory — integers, rationals, finite fields, polynomials) has no documented WASM build. PARI/GP does — wasm-pari and a GMP-backed variant wasm-pari-gmp (from the CoCalc/SageMathCloud team) compile the full PARI/GP computer-algebra system to WASM, usable without GMP (smaller, slower) or with it (faster, bigger). These are much heavier than gmp-wasm (a whole CAS, not a numeric kernel) and aimed at running PARI/GP interactively in a browser, not at exposing a narrow primitive API — a real option for a future wasm-engine, but the wrong shape for a lean, capability-gated primitive layer.

Relevance to enumeratio: gmp-wasm is the standout candidate for a future wasm-engine's numeric core — integer/rational/float in one package, worker-and-Node-portable, zero-dependency bundling, and precedent for beating native BigInt on heavy workloads. wasm-pari is worth a second look only if the wasm-engine's ambition grows past primitives into general number-theoretic computation (factorization, class groups) that would otherwise mean hand-porting FLINT/PARI algorithms into TS.

4. Off-thread / worker orchestration

The Layer-3 queue-based calc worker (#279) doesn't need to be invented from scratch — the patterns are well-established:

  • Comlink (1.1 KB) — turns postMessage into an RPC: expose() a value on the worker side, wrap() it on the caller side, and every property/method access becomes an async proxy call. Works with both browser Web Workers and Node's worker_threads. This is the right layer for ergonomics (making the worker boundary invisible to callers) but it has no built-in queue, pool, or cancellation semantics of its own — those come from a pool library on top.
  • Worker pools: workerpool (same author as math.js; browser + Node, dynamic task offloading, abort-listener-based graceful termination), threads.js (pool.queue() + task.cancel() for queued-but-not-yet-running tasks, pool.terminate() for hard shutdown), and Node-specific Piscina (accepts a real AbortSignal per submitted task — aborting mid-execution tears down and restarts the specific worker thread, rejecting that task's promise without disturbing the rest of the pool). Piscina's AbortSignal-per-task model is the cleanest cancellation story of the three — it maps directly onto the standard AbortController/AbortSignal pattern already idiomatic elsewhere in JS.
  • Progress reporting: the two options are (a) plain postMessage progress events from worker to caller — simplest, fine for coarse-grained progress, is exactly the "message queue" shape #269 already describes — or (b) a shared SharedArrayBuffer + Atomics.add/Atomics.load for continuous, no-copy progress counters when updates are very frequent. (b) requires COOP/COEP cross-origin isolation headers to enable SharedArrayBuffer in browsers at all — a real deployment constraint, not just a code choice.
  • Transferables: ArrayBuffer-backed data (e.g. a large result set) can be postMessage'd as a transferable — ownership moves, zero-copy — but a plain object graph (the shape of most enumeratio row/element payloads) still gets structured-cloned, not transferred.

Relevance to enumeratio: the shape #269 already sketches ({op, args, progress, continuation} over a message queue) matches this ecosystem closely — Comlink for the RPC ergonomics, an AbortSignal-per-task model (Piscina's pattern, even if not the library itself) for the "nothing hangs" cancellation requirement, and plain postMessage progress events as the default (SharedArrayBuffer/Atomics only if per-element progress granularity ever becomes a real bottleneck — likely premature for enumeration workloads).

5. Resumable / continuation computation

JS generators (function* / yield) are the native building block for pause/resume: a generator suspends at a yield and resumes exactly where it left off on the next .next() call, which is precisely the "long-running enumeration that can be paused and continued later" shape #273 (adaptive enumeration windows) needs. The practical gap is durability — a generator's suspended state lives only in-process; it does not survive a worker restart, a page reload, or a network round-trip. True checkpoint/resume across those boundaries means serializing whatever the generator needs to resume (typically just a rank/cursor + the parameters that produced it) rather than trying to serialize the generator object itself, which isn't portable. This is a much lighter requirement for enumeratio than for a general workflow engine: because every collection already carries a (fiber, rank) address, the "continuation" is just that address — no separate continuation-serialization machinery is needed, generators only need to supply the pausable iteration, not the resumable state.

Relevance to enumeratio: don't reach for a coroutine/continuation library — the (fiber, rank) model already is the checkpoint; a function* unranker/enumerator that yields per-batch, paired with the router handing back the last-seen rank as the resume token, is the whole pattern. This is closer to "use what pg-engine already gives us" than "import a new resumability abstraction."

Shortlist

Layer Reach for Avoid
ts-engine precision Native BigInt for the exact-integer majority (cardinalities, ranks); Fraction.js only where pg's rational is genuinely exercised; decimal.js only if/when a decimal (non-integer, non-rational) capability is added Don't adopt a single "one true numeric type" — pg's unified numeric has no one-to-one JS analogue; picking one lib for everything (e.g. decimal.js for integers) misrepresents the capability declaration
wasm-engine (future) gmp-wasm as the default numeric core (integer/rational/float, worker+Node portable, zero deps); wasm-pari only if scope grows into general number-theoretic computation Don't hand-roll a GMP/MPFR WASM build — gmp-wasm already ships one; don't reach for a full CAS (PARI/GP) just for primitives
queue-worker (#279) Comlink for the RPC boundary; an AbortSignal-per-task cancellation model (Piscina's pattern); plain postMessage progress events; the existing (fiber, rank) address as the continuation token Don't reach for SharedArrayBuffer/Atomics until per-element progress granularity is a proven bottleneck (COOP/COEP header cost isn't free); don't build a generic coroutine/continuation-serialization layer — the rank is the checkpoint
math.js / general libs Borrow the API shape (typed-dispatch, extensible function registry) as a model for ts-engine's own registry Don't expect any async/worker/cancellation story from math.js itself — it has none

Couldn't fully verify: whether Decimal128 (the emerging decimal.js-derived standard, referenced by MDN/TC39 proposals) has shipped in any JS engine as of this writing — flagging as unconfirmed rather than asserting a ship date. FLINT's WASM story is confirmed absent (no build found), not merely unresearched. mpdecimal's WASM packaging is confirmed to exist (project's own test suite) but no widely-adopted npm wrapper was found — unlike gmp-wasm, adopting it would mean building/wrapping the binary directly.

Home

Project

Design: every design page, settled, speculative and archived

Clone this wiki locally