math.big-integer
Exact integer arithmetic of any size, on decimal strings, for powers and products that overflow 64 bits.
1.0.0 · published 2026-10-03 by charlie · Anterra
Pinned by 24 tests, run in TypeScript, Python and Rust.
What it does
Exact whole-number arithmetic with no size limit worth mentioning, for the calculations where 64 bits run out: a monthly loan compounds (1 + r) over 300 periods, and the exact fraction behind the payment has numerators thousands of digits long. Money and rates in this registry are integers, so exactness is available if the integers are allowed to be big.
Numbers travel as decimal strings ("-123", "0", "98765..."), because that is the one form all three languages read and write identically: a JavaScript number stops being exact at 2^53.
For example
calculateBigInteger(123456789012345678901234567890, add, 987654321098765432109876543210)→ 1111111110111111111011111111100 adds past 64 bitscalculateBigInteger(9223372036854775807, add, 1)→ 9223372036854775808 i64 max plus one does not wrapcalculateBigInteger(123456789012345678901234567890, subtract, 987654321098765432109876543210)→ -864197532086419753208641975320 subtracting a larger number goes negative
The function
The same function in TypeScript, Python and Rust, pinned by the same tests. Pick your language; the choice follows you around the registry.
export function calculateBigInteger(a: string, op: BigIntegerOp, b: string): string
| a | string | a whole number in decimal: optional "-", digits, no leading zeros |
| op | BigIntegerOp | add, subtract, multiply, divide (towards zero), remainder (sign of a) or power |
| b | string | a whole number in decimal; for power, 0 or more |
| returns | string | the exact result in the same decimal form |
The type it declares, generated into your project
export type BigIntegerOp = "add" | "subtract" | "multiply" | "divide" | "remainder" | "power";
Your code names it in one line, in the file that uses it
import { calculateBigInteger } from "#fune/math.big-integer@^1";
import { type BigIntegerOp } from "./math_big_integer_types.ts";
const INTEGER = /^-?(0|[1-9][0-9]*)$/;
/** The largest result, in bits, that power will build: about 19,700 decimal digits. */
export const MAX_POWER_BITS = 65536;
/** Parse the registry's decimal integer form. "-0", "+1", "01" and " 1" are refused. */
export function parseBigInteger(text: string): bigint {
if (typeof text !== "string" || !INTEGER.test(text) || text === "-0") {
throw new RangeError(`not a whole number in decimal: "${text}"`);
}
return BigInt(text);
}
function bitLength(value: bigint): number {
return value === 0n ? 0 : (value < 0n ? -value : value).toString(2).length;
}
/**
* Raise to a power, refusing results beyond MAX_POWER_BITS before building
* them, so a typo cannot hang the process on a million-digit number.
*/
export function powerBigInteger(base: bigint, exponent: bigint): bigint {
if (exponent < 0n) {
throw new RangeError("exponent must not be negative");
}
const magnitude = base < 0n ? -base : base;
if (magnitude >= 2n && BigInt(bitLength(magnitude)) * exponent > BigInt(MAX_POWER_BITS)) {
throw new RangeError(`result too large: power is limited to ${MAX_POWER_BITS} bits`);
}
if (magnitude <= 1n) {
// 0^0 is 1, as in every language's integer power; (-1)^n alternates.
if (exponent === 0n) return 1n;
if (base === -1n) return exponent % 2n === 0n ? 1n : -1n;
return base;
}
return base ** exponent;
}
/**
* Exact integer arithmetic on decimal strings. Division truncates towards
* zero and the remainder takes the sign of the dividend, as in TypeScript,
* Rust, C and Java (Python floors, and is adjusted to agree).
*/
export function calculateBigInteger(a: string, op: BigIntegerOp, b: string): string {
const x = parseBigInteger(a);
const y = parseBigInteger(b);
switch (op) {
case "add":
return (x + y).toString();
case "subtract":
return (x - y).toString();
case "multiply":
return (x * y).toString();
case "divide":
if (y === 0n) throw new RangeError("division by zero");
return (x / y).toString();
case "remainder":
if (y === 0n) throw new RangeError("division by zero");
return (x % y).toString();
case "power":
return powerBigInteger(x, y).toString();
default:
throw new RangeError(`unknown operation "${op}"`);
}
}Install
fune build
With that line in your source, in a TypeScript project (language typescript in fune.project), fune build resolves it and nothing else, pins them in fune.lock, downloads only the TypeScript package of each, and builds the code above into your project’s .fune/build, one readable file per capability with a header linking back here. Or pin a range in fune.project and build in one step:
fune add math.big-integer
The manifest, vectors and README with only the TypeScript implementation. Install it without the registry with fune add ./math.big-integer-1.0.0-typescript.fune, or fetch it from a terminal with fune pull math.big-integer@1.0.0:typescript.
The whole function, every language, is one file too: math.big-integer-1.0.0.fune, 27,178 bytes, sha256 242eb37623145ad5d7b0d7b9063ea4d93bac3ec93828ccab3e7149d5c7ed9af3. It installs into a project of any language.
Customise it in your app
The seams this capability offers. Put a marker directly above a function of your own and fune build wires it into the built code; the package on the registry is not changed, the built file’s header lists it under CUSTOMISED, and fune hooks lists every hook in the project. How hooks work.
before — your function gets the arguments and returns them, changed or not, or throws to refuse the call.
// fune: before math.big-integer
after — your function gets the result and the arguments, and returns the final result.
// fune: after math.big-integer
replace — it requires no other capability, so there is no dependency to replace.
step — your function runs at a numbered point inside the function’s body, receives the in-scope values it names as parameters, and may return replacements. List the points with fune show math.big-integer --steps.
// fune: step math.big-integer after <n|label>
Tests
A version published now needs at least 8 tests for every function, and one that expects the error for each function that throws; the registry refuses it otherwise. fune verify --all runs each case in TypeScript, Python and Rust, and a project runs them again with fune verify. This page lists the cases; it does not run them. The exact JSON is vectors.json.
| Case | Arguments | Expected | |
|---|---|---|---|
| adds past 64 bits | 123456789012345678901234567890, add, 987654321098765432109876543210 | → | 1111111110111111111011111111100 |
| i64 max plus one does not wrap | 9223372036854775807, add, 1 | → | 9223372036854775808 |
| subtracting a larger number goes negative | 123456789012345678901234567890, subtract, 987654321098765432109876543210 | → | -864197532086419753208641975320 |
| 2^64 squared is 2^128 | 18446744073709551616, multiply, 18446744073709551616 | → | 340282366920938463463374607431768211456 |
| negative times negative is positive | -99999999999999999999, multiply, -3 | → | 299999999999999999997 |
| anything times zero is zero | -123456789012345678901234567890, multiply, 0 | → | 0 |
| divide truncates towards zero | -7, divide, 2 | → | -3 |
| remainder takes the sign of the dividend | -7, remainder, 2 | → | -1 |
| positive by negative divisor | 7, remainder, -2 | → | 1 |
| long division of large numbers | 1606938044258990275541962092341162602522202993782792835313721, divide, 717897987691852588770249 | → | 2238393297946874000179418290327143433 |
Show the other 14 tests
| Case | Arguments | Expected | |
|---|---|---|---|
| remainder of large numbers | 1606938044258990275541962092341162602522202993782792835313721, remainder, 717897987691852588770249 | → | 249667313308346329188904 |
| power of two | 2, power, 100 | → | 1267650600228229401496703205376 |
| a negative base to an odd power | -3, power, 41 | → | -36472996377170786403 |
| zero to the zero is one | 0, power, 0 | → | 1 |
| minus one to a huge power | -1, power, 1000000001 | → | -1 |
| compound growth factor 1.0045^12, scaled | 10045, power, 12 | → | 1055356751950102607459752843054131873164306640625 |
| a malformed number is refused | 01, add, 1 | → | error: not a whole number in decimal |
| minus zero is refused | -0, add, 1 | → | error: not a whole number in decimal |
| a decimal point is refused | 1.5, add, 1 | → | error: not a whole number in decimal |
| division by zero | 1, divide, 0 | → | error: division by zero |
| remainder by zero | 1, remainder, 0 | → | error: division by zero |
| a negative exponent is refused | 2, power, -1 | → | error: exponent must not be negative |
| a power beyond the size limit is refused before it is built | 3, power, 50000 | → | error: result too large |
| an unknown operation | 1, modulo, 2 | → | error: unknown operation |
More from the author
## Operations
- `add`, `subtract`, `multiply`: exact. - `divide`: the quotient truncated towards zero, so -7 / 2 is -3. - `remainder`: takes the sign of the dividend, so -7 rem 2 is -1 and a = (a / b) × b + (a rem b) always holds. This is TypeScript's and Rust's rule; Python floors by default and is adjusted to agree. - `power`: the exponent is 0 or more; 0^0 is 1.
## Limits
`power` refuses a result that would exceed 65,536 bits (about 19,700 decimal digits) before building it, estimated as bits(base) × exponent, the same way in every language, so a slip of the exponent fails at once instead of hanging. The other operations have no limit beyond memory and time; the Rust side is schoolbook multiplication and shift-subtract division, which is quadratic.
## Why a capability, and what it exports
TypeScript (`bigint`) and Python (`int`) have arbitrary-precision integers built in. Rust's standard library does not, and the registry takes no crates, so the Rust module is a real implementation: it exports a `BigInt` type (`from_i64`, `from_i128`, `add`, `sub`, `mul`, `div_rem`, `pow`, `to_i64`, `to_i128`, `div_rem_small`, ordering and `Display`) that other capabilities build on, plus `parse_big_integer` and `power_big_integer`. TypeScript and Python export `parseBigInteger` / `parse_big_integer` and `powerBigInteger` / `power_big_integer` over their native integers.
Numbers must be in canonical form: an optional "-", then digits with no leading zero. "-0", "+1", "01", " 1" and "1.0" are errors, not guesses.
Files
| Path | Bytes |
|---|---|
| README.md | 2,100 |
| impl/python.py | 2,180 |
| impl/rust.rs | 13,187 |
| impl/typescript.ts | 2,388 |
| vectors.json | 4,026 |