Functional Weave
Code in Rust

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

  • calculate_big_integer(123456789012345678901234567890, add, 987654321098765432109876543210) → 1111111110111111111011111111100 adds past 64 bits
  • calculate_big_integer(9223372036854775807, add, 1) → 9223372036854775808 i64 max plus one does not wrap
  • calculate_big_integer(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.

pub fn calculate_big_integer(a: &str, op: &str, b: &str) -> String
astringa whole number in decimal: optional "-", digits, no leading zeros
opBigIntegerOpadd, subtract, multiply, divide (towards zero), remainder (sign of a) or power
bstringa whole number in decimal; for power, 0 or more
returnsstringthe exact result in the same decimal form

The type it declares, generated into your project

// BigIntegerOp is a string in Rust, one of: "add", "subtract", "multiply", "divide", "remainder", "power".
// Parameters take it as &str and results hold it as String.

Your code names it in one line, in the file that uses it

fune!(math.big-integer@^1);  // then call calculate_big_integer(…)
impl/rust.rs · 455 lines · open · raw

Imports name this capability’s declared dependencies, which fune builds next to it in your project; each one links to its page.

use std::cmp::Ordering;
use std::fmt;

use super::funejson::Value;  ← the fune runtime: the JSON value the test vectors use; fune build keeps it only where a signature takes one

/// The largest result, in bits, that power will build: about 19,700 decimal digits.
pub const MAX_POWER_BITS: u64 = 65536;

/// An integer of any size: sign and magnitude, the magnitude in 32-bit limbs,
/// least significant first, with no high zero limbs. Zero is never negative.
///
/// TypeScript and Python have this built in (`bigint`, `int`); Rust's standard
/// library does not, and the registry takes no crates, so this is the Rust
/// side of the same arithmetic. Schoolbook multiplication and shift-subtract
/// division: quadratic, and fast enough for the few-thousand-digit numbers
/// money and rate calculations reach.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct BigInt {
    negative: bool,
    mag: Vec<u32>,
}

fn trim(v: &mut Vec<u32>) {
    while let Some(&0) = v.last() {
        v.pop();
    }
}

fn mag_cmp(a: &[u32], b: &[u32]) -> Ordering {
    if a.len() != b.len() {
        return a.len().cmp(&b.len());
    }
    for i in (0..a.len()).rev() {
        if a[i] != b[i] {
            return a[i].cmp(&b[i]);
        }
    }
    Ordering::Equal
}

fn mag_add(a: &[u32], b: &[u32]) -> Vec<u32> {
    let n = a.len().max(b.len());
    let mut out = Vec::with_capacity(n + 1);
    let mut carry = 0u64;
    for i in 0..n {
        let x = *a.get(i).unwrap_or(&0) as u64;
        let y = *b.get(i).unwrap_or(&0) as u64;
        let t = x + y + carry;
        out.push(t as u32);
        carry = t >> 32;
    }
    if carry > 0 {
        out.push(carry as u32);
    }
    trim(&mut out);
    out
}

/// a - b for a >= b.
fn mag_sub(a: &[u32], b: &[u32]) -> Vec<u32> {
    let mut out = Vec::with_capacity(a.len());
    let mut borrow = 0i64;
    for i in 0..a.len() {
        let mut t = a[i] as i64 - *b.get(i).unwrap_or(&0) as i64 - borrow;
        if t < 0 {
            t += 1i64 << 32;
            borrow = 1;
        } else {
            borrow = 0;
        }
        out.push(t as u32);
    }
    trim(&mut out);
    out
}

fn mag_mul(a: &[u32], b: &[u32]) -> Vec<u32> {
    if a.is_empty() || b.is_empty() {
        return Vec::new();
    }
    let mut out = vec![0u32; a.len() + b.len()];
    for i in 0..a.len() {
        let mut carry = 0u64;
        let x = a[i] as u64;
        for j in 0..b.len() {
            let t = out[i + j] as u64 + x * b[j] as u64 + carry;
            out[i + j] = t as u32;
            carry = t >> 32;
        }
        out[i + b.len()] = carry as u32;
    }
    trim(&mut out);
    out
}

fn mag_bits(a: &[u32]) -> u64 {
    match a.last() {
        None => 0,
        Some(&top) => (a.len() as u64 - 1) * 32 + (32 - top.leading_zeros() as u64),
    }
}

fn mag_shl(a: &[u32], bits: u64) -> Vec<u32> {
    if a.is_empty() {
        return Vec::new();
    }
    let limbs = (bits / 32) as usize;
    let rem = (bits % 32) as u32;
    let mut out = vec![0u32; limbs];
    if rem == 0 {
        out.extend_from_slice(a);
    } else {
        let mut carry = 0u32;
        for &x in a {
            out.push((x << rem) | carry);
            carry = x >> (32 - rem);
        }
        if carry > 0 {
            out.push(carry);
        }
    }
    trim(&mut out);
    out
}

/// Quotient and remainder of magnitudes, by shift and subtract.
fn mag_divrem(a: &[u32], b: &[u32]) -> (Vec<u32>, Vec<u32>) {
    if b.is_empty() {
        panic!("division by zero");
    }
    if mag_cmp(a, b) == Ordering::Less {
        return (Vec::new(), a.to_vec());
    }
    let shift = mag_bits(a) - mag_bits(b);
    let mut quotient = vec![0u32; (shift / 32 + 1) as usize];
    let mut rest = a.to_vec();
    let mut s = shift as i64;
    while s >= 0 {
        let candidate = mag_shl(b, s as u64);
        if mag_cmp(&rest, &candidate) != Ordering::Less {
            rest = mag_sub(&rest, &candidate);
            quotient[(s / 32) as usize] |= 1u32 << (s % 32);
        }
        s -= 1;
    }
    trim(&mut quotient);
    (quotient, rest)
}

fn mag_divrem_small(a: &[u32], d: u32) -> (Vec<u32>, u32) {
    let mut out = vec![0u32; a.len()];
    let mut rem = 0u64;
    for i in (0..a.len()).rev() {
        let cur = (rem << 32) | a[i] as u64;
        out[i] = (cur / d as u64) as u32;
        rem = cur % d as u64;
    }
    trim(&mut out);
    (out, rem as u32)
}

impl BigInt {
    fn make(negative: bool, mut mag: Vec<u32>) -> BigInt {
        trim(&mut mag);
        let negative = negative && !mag.is_empty();
        BigInt { negative, mag }
    }

    pub fn zero() -> BigInt {
        BigInt { negative: false, mag: Vec::new() }
    }

    pub fn from_i128(value: i128) -> BigInt {
        let mut m = value.unsigned_abs();
        let mut mag = Vec::new();
        while m > 0 {
            mag.push(m as u32);
            m >>= 32;
        }
        BigInt::make(value < 0, mag)
    }

    pub fn from_i64(value: i64) -> BigInt {
        BigInt::from_i128(value as i128)
    }

    pub fn is_zero(&self) -> bool {
        self.mag.is_empty()
    }

    pub fn is_negative(&self) -> bool {
        self.negative
    }

    /// Bits in the magnitude; 0 for zero.
    pub fn bit_length(&self) -> u64 {
        mag_bits(&self.mag)
    }

    pub fn neg(&self) -> BigInt {
        BigInt::make(!self.negative, self.mag.clone())
    }

    pub fn abs(&self) -> BigInt {
        BigInt::make(false, self.mag.clone())
    }

    pub fn add(&self, other: &BigInt) -> BigInt {
        if self.negative == other.negative {
            return BigInt::make(self.negative, mag_add(&self.mag, &other.mag));
        }
        match mag_cmp(&self.mag, &other.mag) {
            Ordering::Equal => BigInt::zero(),
            Ordering::Greater => BigInt::make(self.negative, mag_sub(&self.mag, &other.mag)),
            Ordering::Less => BigInt::make(other.negative, mag_sub(&other.mag, &self.mag)),
        }
    }

    pub fn sub(&self, other: &BigInt) -> BigInt {
        self.add(&other.neg())
    }

    pub fn mul(&self, other: &BigInt) -> BigInt {
        BigInt::make(self.negative != other.negative, mag_mul(&self.mag, &other.mag))
    }

    /// Quotient truncated towards zero, and the remainder with the sign of self.
    ///
    /// # Panics
    /// Panics if `other` is zero.
    pub fn div_rem(&self, other: &BigInt) -> (BigInt, BigInt) {
        let (q, r) = mag_divrem(&self.mag, &other.mag);
        (
            BigInt::make(self.negative != other.negative, q),
            BigInt::make(self.negative, r),
        )
    }

    /// Quotient truncated towards zero.
    pub fn div(&self, other: &BigInt) -> BigInt {
        self.div_rem(other).0
    }

    /// Quotient truncated towards zero by a small positive divisor, and the
    /// remainder of the magnitude. Much faster than `div_rem` for scaling by
    /// powers of ten.
    ///
    /// # Panics
    /// Panics if `divisor` is zero.
    pub fn div_rem_small(&self, divisor: u32) -> (BigInt, u32) {
        if divisor == 0 {
            panic!("division by zero");
        }
        let (q, r) = mag_divrem_small(&self.mag, divisor);
        (BigInt::make(self.negative, q), r)
    }

    /// Remainder with the sign of self.
    pub fn rem(&self, other: &BigInt) -> BigInt {
        self.div_rem(other).1
    }

    /// # Panics
    /// Panics beyond MAX_POWER_BITS, as the other languages do.
    pub fn pow(&self, exponent: u64) -> BigInt {
        if mag_cmp(&self.mag, &[1]) != Ordering::Greater {
            if exponent == 0 {
                return BigInt::from_i64(1);
            }
            if self.negative {
                return BigInt::from_i64(if exponent % 2 == 0 { 1 } else { -1 });
            }
            return self.clone();
        }
        if (self.bit_length() as u128) * (exponent as u128) > MAX_POWER_BITS as u128 {
            panic!("result too large: power is limited to {} bits", MAX_POWER_BITS);
        }
        let mut result = BigInt::from_i64(1);
        let mut base = self.clone();
        let mut e = exponent;
        while e > 0 {
            if e & 1 == 1 {
                result = result.mul(&base);
            }
            e >>= 1;
            if e > 0 {
                base = base.mul(&base);
            }
        }
        result
    }

    /// # Panics
    /// Panics if the value does not fit in an i128.
    pub fn to_i128(&self) -> i128 {
        if self.bit_length() > 127 {
            panic!("value does not fit in 128 bits");
        }
        let mut m: u128 = 0;
        for &limb in self.mag.iter().rev() {
            m = (m << 32) | limb as u128;
        }
        if self.negative {
            -(m as i128)
        } else {
            m as i128
        }
    }

    /// # Panics
    /// Panics if the value does not fit in an i64.
    pub fn to_i64(&self) -> i64 {
        let v = self.to_i128();
        if v > i64::MAX as i128 || v < i64::MIN as i128 {
            panic!("value does not fit in 64 bits");
        }
        v as i64
    }
}

impl Ord for BigInt {
    fn cmp(&self, other: &BigInt) -> Ordering {
        match (self.negative, other.negative) {
            (false, true) => Ordering::Greater,
            (true, false) => Ordering::Less,
            (false, false) => mag_cmp(&self.mag, &other.mag),
            (true, true) => mag_cmp(&other.mag, &self.mag),
        }
    }
}

impl PartialOrd for BigInt {
    fn partial_cmp(&self, other: &BigInt) -> Option<Ordering> {
        Some(self.cmp(other))
    }
}

impl fmt::Display for BigInt {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        if self.mag.is_empty() {
            return write!(f, "0");
        }
        let mut chunks: Vec<u32> = Vec::new();
        let mut rest = self.mag.clone();
        while !rest.is_empty() {
            let (q, r) = mag_divrem_small(&rest, 1_000_000_000);
            chunks.push(r);
            rest = q;
        }
        let mut text = String::new();
        if self.negative {
            text.push('-');
        }
        text.push_str(&chunks.last().unwrap().to_string());
        for chunk in chunks.iter().rev().skip(1) {
            text.push_str(&format!("{:09}", chunk));
        }
        write!(f, "{}", text)
    }
}

/// Parse the registry's decimal integer form. "-0", "+1", "01" and " 1" are refused.
///
/// # Panics
/// Panics on anything else.
pub fn parse_big_integer(text: &str) -> BigInt {
    let (negative, digits) = match text.strip_prefix('-') {
        Some(rest) => (true, rest),
        None => (false, text),
    };
    let valid = !digits.is_empty()
        && digits.bytes().all(|b| b.is_ascii_digit())
        && (digits == "0" || !digits.starts_with('0'))
        && !(negative && digits == "0");
    if !valid {
        panic!("not a whole number in decimal: \"{}\"", text);
    }
    let mut mag: Vec<u32> = Vec::new();
    for b in digits.bytes() {
        let mut carry = (b - b'0') as u64;
        for limb in mag.iter_mut() {
            let t = *limb as u64 * 10 + carry;
            *limb = t as u32;
            carry = t >> 32;
        }
        if carry > 0 {
            mag.push(carry as u32);
        }
    }
    BigInt::make(negative, mag)
}

/// Raise to a power, refusing results beyond MAX_POWER_BITS before building them.
///
/// # Panics
/// Panics on a negative exponent or a result that would be too large.
pub fn power_big_integer(base: &BigInt, exponent: &BigInt) -> BigInt {
    if exponent.is_negative() {
        panic!("exponent must not be negative");
    }
    if base.abs() <= BigInt::from_i64(1) {
        if exponent.is_zero() {
            return BigInt::from_i64(1);
        }
        if base.is_negative() {
            let even = exponent.rem(&BigInt::from_i64(2)).is_zero();
            return BigInt::from_i64(if even { 1 } else { -1 });
        }
        return base.clone();
    }
    if exponent.bit_length() > 32
        || (base.bit_length() as u128) * (exponent.to_i128() as u128) > MAX_POWER_BITS as u128
    {
        panic!("result too large: power is limited to {} bits", MAX_POWER_BITS);
    }
    base.pow(exponent.to_i64() as u64)
}

/// Exact integer arithmetic on decimal strings. Division truncates towards
/// zero and the remainder takes the sign of the dividend, as in TypeScript.
///
/// # Panics
/// Panics on malformed numbers, division by zero, a negative or oversized
/// power, or an unknown operation.
pub fn calculate_big_integer(a: &str, op: &str, b: &str) -> String {
    let x = parse_big_integer(a);
    let y = parse_big_integer(b);
    match op {
        "add" => x.add(&y).to_string(),
        "subtract" => x.sub(&y).to_string(),
        "multiply" => x.mul(&y).to_string(),
        "divide" => {
            if y.is_zero() {
                panic!("division by zero");
            }
            x.div(&y).to_string()
        }
        "remainder" => {
            if y.is_zero() {
                panic!("division by zero");
            }
            x.rem(&y).to_string()
        }
        "power" => power_big_integer(&x, &y).to_string(),
        other => panic!("unknown operation \"{}\"", other),
    }
}

pub fn fune_vector(args: &[Value]) -> Value {
    Value::str(&calculate_big_integer(
        args[0].as_str(),
        args[1].as_str(),
        args[2].as_str(),
    ))
}

Install

fune build

With that line in your source, in a Rust project (language rust in fune.project), fune build resolves it and nothing else, pins them in fune.lock, downloads only the Rust 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. A crate’s build.rs runs it before every compile. Or pin a range in fune.project and build in one step:

fune add math.big-integer
Download for Rust math.big-integer-1.0.0-rust.fune · 22,355 bytes sha256 162556478d60a1768c2ee195328cebaaeb3145a6c3c1bc7e7387cdba235c6ea0

The manifest, vectors and README with only the Rust implementation. Install it without the registry with fune add ./math.big-integer-1.0.0-rust.fune, or fetch it from a terminal with fune pull math.big-integer@1.0.0:rust.

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.

CaseArgumentsExpected
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
CaseArgumentsExpected
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

PathBytes
README.md2,100
impl/python.py2,180
impl/rust.rs13,187
impl/typescript.ts2,388
vectors.json4,026