Repository navigation
Archive Survey Browser Math Libraries
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 indesign/.
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.
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.
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.
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 nativeBigInton 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
decimalmodule), 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.
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
postMessageinto 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'sworker_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 realAbortSignalper 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'sAbortSignal-per-task model is the cleanest cancellation story of the three — it maps directly onto the standardAbortController/AbortSignalpattern already idiomatic elsewhere in JS. -
Progress reporting: the two options are (a) plain
postMessageprogress events from worker to caller — simplest, fine for coarse-grained progress, is exactly the "message queue" shape #269 already describes — or (b) a sharedSharedArrayBuffer+Atomics.add/Atomics.loadfor continuous, no-copy progress counters when updates are very frequent. (b) requires COOP/COEP cross-origin isolation headers to enableSharedArrayBufferin browsers at all — a real deployment constraint, not just a code choice. -
Transferables:
ArrayBuffer-backed data (e.g. a large result set) can bepostMessage'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).
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."
| 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.