lumbda/c/bignum.c
russell@unturf.com 2f342c3be2
c-tier bignum — arbitrary-precision integers unblock secp256k1 widths
Adds tagged bignum support alongside the existing 48-bit fixnum on the C
tier. Tag 6 = bignum, heap struct sign-magnitude with u64 little-endian
limbs. Reader emits bignums for any literal past the fixnum range; +, -,
*, quotient, remainder, modulo, expt, =, <, >, abs, odd?, even?,
integer?, exact?, number->string, string->number all promote fixnum →
bignum on overflow & demote back when results fit. Boehm GC owns every
allocation. Schoolbook O(n²) mul + shift-subtract divmod is sufficient
at our 4-limb / 256-bit scale.

Before: (expt 2 48) = 0, (expt 2 256) = 0, secp256k1-p = -4294968273.
After: all three return their exact arbitrary-precision values, matching
Python tier byte-for-byte.

Validated:
- c/test.c — 85/85 pass (+2 new bignum unit tests).
- tests/functional.lsp — 205/205 pass on both C & Python tiers.
- tests/bignum-cross-tier.lsp — 33/33 pass byte-identical on both tiers
  (diff produces no output).
- ecdsa/runs/lumbda-sweep-003/c-tier-bignum-probe.lsp — all four
  assertions now match the Python oracle.
- ecdsa Phase B byte-identity sweep inside QEMU guest:
  n+1=9  p=251           sha256 c668bbe3... — matches Python oracle.
  n+1=18 p=131071        sha256 8a031f96... — matches Python oracle.
  n+1=33 p=2³²-5         sha256 0bc56905... — matches Python oracle.
  Previously the n+1=33 C tier emitted sha256 b024d6d9... (26,078 fewer
  Toffolis due to silent fixnum wrap). Bignums close that gate.

secp256k1 production-width emit (n+1=257) is now structurally unblocked
on C tier; downstream agent (#55) drives that next-step on the ecdsa
side. Asm tier inherits in a follow-up port.
2026-06-06 20:23:37 -04:00

496 lines
19 KiB
C

/*
* bignum.c — Arbitrary-precision integer arithmetic for lumbda C tier.
*
* Design (full notes in lumbda.h after the Rational block):
* - Sign-magnitude representation: int32_t sign (-1, 0, +1),
* uint32_t n_limbs, uint64_t *limbs (little-endian magnitude).
* - bignum_normalize() trims leading-zero limbs & demotes to fixnum
* when |value| <= FIXNUM_MAX so callers never branch on tag.
* - All arithmetic uses 64-bit limbs with 128-bit products (__int128)
* for schoolbook multiply. Division uses long-division by single
* limb when divisor fits one limb, otherwise schoolbook with
* normalization (sufficient for our scale: secp256k1 needs at most
* 4 limbs / 256 bits, far below where Karatsuba helps).
* - Boehm GC owns the Bignum struct & its limbs array.
*
* Cross-tier oracle: Python tier (native arbitrary int) is the byte-
* identity standard. Every operation here must agree with Python on
* matching inputs — see tests/bignum-cross-tier.lsp.
*/
#include "lumbda.h"
/* ═══════════════════════════════════════════════════════════════════════════
* Allocation helpers
* ═══════════════════════════════════════════════════════════════════════════ */
static Bignum *alloc_bignum(uint32_t n_limbs) {
Bignum *b = (Bignum *)ul_malloc(sizeof(Bignum));
b->hdr.type = OBJ_BIGNUM;
b->sign = 0;
b->n_limbs = n_limbs;
if (n_limbs == 0) {
b->limbs = NULL;
} else {
b->limbs = (uint64_t *)ul_malloc(sizeof(uint64_t) * n_limbs);
memset(b->limbs, 0, sizeof(uint64_t) * n_limbs);
}
return b;
}
static Value wrap_bignum(Bignum *b) {
return NANBOX(TAG_BIGNUM, (uintptr_t)b);
}
/* Trim leading zero limbs in place. Zero magnitude => sign 0. */
static void trim(Bignum *b) {
while (b->n_limbs > 0 && b->limbs[b->n_limbs - 1] == 0)
b->n_limbs--;
if (b->n_limbs == 0) b->sign = 0;
}
/* ═══════════════════════════════════════════════════════════════════════════
* Constructors
* ═══════════════════════════════════════════════════════════════════════════ */
Value make_bignum_from_i64(int64_t v) {
if (FITS_FIXNUM(v)) return VAL_INT(v);
Bignum *b = alloc_bignum(1);
if (v < 0) {
b->sign = -1;
/* Avoid -INT64_MIN UB: cast then negate via unsigned. */
b->limbs[0] = (uint64_t)(-(v + 1)) + 1ULL;
} else if (v > 0) {
b->sign = 1;
b->limbs[0] = (uint64_t)v;
} else {
b->sign = 0;
b->n_limbs = 0;
}
return wrap_bignum(b);
}
Value bignum_from_limbs(int32_t sign, uint64_t *limbs, uint32_t n) {
Bignum *b = alloc_bignum(n);
if (n > 0) memcpy(b->limbs, limbs, sizeof(uint64_t) * n);
b->sign = sign;
trim(b);
return wrap_bignum(b);
}
/* Demote bignum-shape to fixnum when |value| fits 48 signed bits.
* On demote we return VAL_INT(...); otherwise allocate a fresh Bignum. */
Value bignum_normalize(int32_t sign, uint64_t *limbs, uint32_t n) {
/* Strip top zeros (compute effective limb count). */
while (n > 0 && limbs[n - 1] == 0) n--;
if (n == 0) return VAL_INT(0);
if (n == 1) {
uint64_t lo = limbs[0];
/* Signed 48-bit range: -2^47 .. 2^47 - 1. */
if (sign >= 0 && lo <= (uint64_t)FIXNUM_MAX)
return VAL_INT((int64_t)lo);
if (sign < 0 && lo <= (uint64_t)(1ULL << 47))
return VAL_INT(-(int64_t)lo);
}
Bignum *b = alloc_bignum(n);
memcpy(b->limbs, limbs, sizeof(uint64_t) * n);
b->sign = sign;
return wrap_bignum(b);
}
/* Promote a fixnum to a Bignum struct (caller-owned). Bignum input passes
* through (the same struct is returned — Bignum structs are immutable from
* the perspective of arithmetic so sharing is safe). */
Bignum *as_bignum(Value v) {
if (IS_BIGNUM(v)) return AS_BIGNUM(v);
if (IS_INT(v)) {
int64_t x = as_int(v);
Bignum *b = alloc_bignum(x == 0 ? 0 : 1);
if (x > 0) { b->sign = 1; b->limbs[0] = (uint64_t)x; }
else if (x < 0) {
b->sign = -1;
b->limbs[0] = (uint64_t)(-(x + 1)) + 1ULL;
}
return b;
}
if (IS_DOUBLE(v)) {
double d = as_double(v);
return AS_BIGNUM(make_bignum_from_i64((int64_t)d));
}
lisp_error("not an integer");
return NULL;
}
Value to_bignum(Value v) {
if (IS_BIGNUM(v)) return v;
return wrap_bignum(as_bignum(v));
}
/* ═══════════════════════════════════════════════════════════════════════════
* Parsing (string → bignum)
* ═══════════════════════════════════════════════════════════════════════════ */
/* Multiply magnitude by single limb; add another limb. Magnitude grows. */
static void mag_mul_add(uint64_t *limbs, uint32_t *n, uint64_t mul, uint64_t add) {
__uint128_t carry = add;
for (uint32_t i = 0; i < *n; i++) {
__uint128_t prod = (__uint128_t)limbs[i] * mul + carry;
limbs[i] = (uint64_t)prod;
carry = prod >> 64;
}
while (carry) {
limbs[(*n)++] = (uint64_t)carry;
carry >>= 64;
}
}
Value make_bignum_from_str(const char *s, int base) {
int32_t sign = 1;
if (*s == '-') { sign = -1; s++; }
else if (*s == '+') s++;
/* Conservative limb count: log2(10) ≈ 3.33 bits per digit → roughly
* len/19 limbs for base 10; len/16 limbs for base 16. Round up + 1. */
size_t len = strlen(s);
uint32_t cap = (uint32_t)((len * 4) / 19) + 2;
uint64_t *limbs = (uint64_t *)ul_malloc(sizeof(uint64_t) * cap);
memset(limbs, 0, sizeof(uint64_t) * cap);
uint32_t n = 0;
for (; *s; s++) {
int d;
if (*s >= '0' && *s <= '9') d = *s - '0';
else if (*s >= 'a' && *s <= 'f') d = *s - 'a' + 10;
else if (*s >= 'A' && *s <= 'F') d = *s - 'A' + 10;
else { ul_free(limbs); lisp_error("bad digit '%c' in number", *s); }
if (d >= base) { ul_free(limbs); lisp_error("digit out of base"); }
mag_mul_add(limbs, &n, (uint64_t)base, (uint64_t)d);
if (n >= cap - 1) {
cap *= 2;
limbs = (uint64_t *)ul_realloc(limbs, sizeof(uint64_t) * cap);
memset(limbs + n, 0, sizeof(uint64_t) * (cap - n));
}
}
Value v = bignum_normalize(sign, limbs, n);
ul_free(limbs);
return v;
}
/* ═══════════════════════════════════════════════════════════════════════════
* Magnitude-level primitives
* ═══════════════════════════════════════════════════════════════════════════ */
/* Compare magnitudes. Returns -1, 0, +1. */
static int mag_cmp(const uint64_t *a, uint32_t na, const uint64_t *b, uint32_t nb) {
if (na != nb) return na < nb ? -1 : 1;
for (int32_t i = (int32_t)na - 1; i >= 0; i--) {
if (a[i] != b[i]) return a[i] < b[i] ? -1 : 1;
}
return 0;
}
/* out = a + b magnitudes. out_cap must be at least max(na, nb) + 1. */
static uint32_t mag_add(const uint64_t *a, uint32_t na,
const uint64_t *b, uint32_t nb,
uint64_t *out) {
uint32_t n = na > nb ? na : nb;
__uint128_t carry = 0;
for (uint32_t i = 0; i < n; i++) {
__uint128_t s = carry;
if (i < na) s += a[i];
if (i < nb) s += b[i];
out[i] = (uint64_t)s;
carry = s >> 64;
}
if (carry) out[n++] = (uint64_t)carry;
return n;
}
/* out = a - b magnitudes, assuming a >= b. out_cap >= na. */
static uint32_t mag_sub(const uint64_t *a, uint32_t na,
const uint64_t *b, uint32_t nb,
uint64_t *out) {
int64_t borrow = 0;
for (uint32_t i = 0; i < na; i++) {
__int128 s = (__int128)a[i] - borrow;
if (i < nb) s -= b[i];
if (s < 0) { s += ((__int128)1 << 64); borrow = 1; }
else borrow = 0;
out[i] = (uint64_t)s;
}
while (na > 0 && out[na - 1] == 0) na--;
return na;
}
/* out = a * b magnitudes. out_cap >= na + nb. */
static uint32_t mag_mul(const uint64_t *a, uint32_t na,
const uint64_t *b, uint32_t nb,
uint64_t *out) {
memset(out, 0, sizeof(uint64_t) * (na + nb));
for (uint32_t i = 0; i < na; i++) {
__uint128_t carry = 0;
for (uint32_t j = 0; j < nb; j++) {
__uint128_t prod = (__uint128_t)a[i] * b[j] + out[i + j] + carry;
out[i + j] = (uint64_t)prod;
carry = prod >> 64;
}
out[i + nb] += (uint64_t)carry;
}
uint32_t n = na + nb;
while (n > 0 && out[n - 1] == 0) n--;
return n;
}
/* Divide magnitude a by single 64-bit limb d. Quotient into q (cap >= na),
* remainder returned. */
static uint64_t mag_divmod_u64(const uint64_t *a, uint32_t na,
uint64_t d, uint64_t *q, uint32_t *nq) {
__uint128_t r = 0;
for (int32_t i = (int32_t)na - 1; i >= 0; i--) {
__uint128_t cur = (r << 64) | a[i];
q[i] = (uint64_t)(cur / d);
r = cur % d;
}
*nq = na;
while (*nq > 0 && q[*nq - 1] == 0) (*nq)--;
return (uint64_t)r;
}
/* Schoolbook division: a / b → (q, r) where a = q*b + r, 0 <= r < b.
* For our scale (operands <= 4 limbs commonly, up to a few dozen for
* intermediates in expt), a basic Knuth Algorithm D is overkill — we use
* shift-subtract on bits. O(na * nb * 64). */
static void mag_divmod(const uint64_t *a, uint32_t na,
const uint64_t *b, uint32_t nb,
uint64_t *q, uint32_t *nq,
uint64_t *r, uint32_t *nr) {
if (nb == 0) lisp_error("division by zero");
if (mag_cmp(a, na, b, nb) < 0) {
/* a < b: q=0, r=a */
*nq = 0;
memcpy(r, a, sizeof(uint64_t) * na);
*nr = na;
return;
}
if (nb == 1) {
uint64_t rem = mag_divmod_u64(a, na, b[0], q, nq);
if (rem) { r[0] = rem; *nr = 1; } else { *nr = 0; }
return;
}
/* Shift-subtract long division: walk bits MSB → LSB of dividend. */
memset(q, 0, sizeof(uint64_t) * na);
memset(r, 0, sizeof(uint64_t) * (nb + 1));
uint32_t rn = 0;
for (int64_t bit = (int64_t)na * 64 - 1; bit >= 0; bit--) {
/* r <<= 1 */
uint64_t carry = 0;
for (uint32_t i = 0; i <= nb; i++) {
uint64_t new_carry = r[i] >> 63;
r[i] = (r[i] << 1) | carry;
carry = new_carry;
}
if (carry || r[nb]) rn = nb + 1;
else { while (rn > 0 && r[rn - 1] == 0) rn--; }
/* r |= bit of a */
uint64_t bit_val = (a[bit / 64] >> (bit % 64)) & 1ULL;
r[0] |= bit_val;
if (bit_val && rn == 0) rn = 1;
/* if r >= b: r -= b; set bit in q */
if (mag_cmp(r, rn, b, nb) >= 0) {
uint32_t new_rn = mag_sub(r, rn, b, nb, r);
rn = new_rn;
q[bit / 64] |= 1ULL << (bit % 64);
}
}
*nq = na;
while (*nq > 0 && q[*nq - 1] == 0) (*nq)--;
*nr = rn;
}
/* ═══════════════════════════════════════════════════════════════════════════
* Signed arithmetic (sign-magnitude wrappers)
* ═══════════════════════════════════════════════════════════════════════════ */
Value big_add(Value a, Value b) {
Bignum *ba = as_bignum(a), *bb = as_bignum(b);
if (ba->sign == 0) return bignum_normalize(bb->sign, bb->limbs, bb->n_limbs);
if (bb->sign == 0) return bignum_normalize(ba->sign, ba->limbs, ba->n_limbs);
uint32_t cap = (ba->n_limbs > bb->n_limbs ? ba->n_limbs : bb->n_limbs) + 1;
uint64_t *out = (uint64_t *)ul_malloc(sizeof(uint64_t) * cap);
Value res;
if (ba->sign == bb->sign) {
uint32_t n = mag_add(ba->limbs, ba->n_limbs, bb->limbs, bb->n_limbs, out);
res = bignum_normalize(ba->sign, out, n);
} else {
int cmp = mag_cmp(ba->limbs, ba->n_limbs, bb->limbs, bb->n_limbs);
if (cmp == 0) { res = VAL_INT(0); }
else if (cmp > 0) {
uint32_t n = mag_sub(ba->limbs, ba->n_limbs, bb->limbs, bb->n_limbs, out);
res = bignum_normalize(ba->sign, out, n);
} else {
uint32_t n = mag_sub(bb->limbs, bb->n_limbs, ba->limbs, ba->n_limbs, out);
res = bignum_normalize(bb->sign, out, n);
}
}
ul_free(out);
return res;
}
Value big_neg(Value a) {
if (IS_INT(a)) {
int64_t x = as_int(a);
return make_bignum_from_i64(-x);
}
Bignum *ba = as_bignum(a);
if (ba->sign == 0) return VAL_INT(0);
return bignum_normalize(-ba->sign, ba->limbs, ba->n_limbs);
}
Value big_sub(Value a, Value b) {
return big_add(a, big_neg(b));
}
Value big_mul(Value a, Value b) {
Bignum *ba = as_bignum(a), *bb = as_bignum(b);
if (ba->sign == 0 || bb->sign == 0) return VAL_INT(0);
uint32_t cap = ba->n_limbs + bb->n_limbs;
uint64_t *out = (uint64_t *)ul_malloc(sizeof(uint64_t) * cap);
uint32_t n = mag_mul(ba->limbs, ba->n_limbs, bb->limbs, bb->n_limbs, out);
Value res = bignum_normalize(ba->sign * bb->sign, out, n);
ul_free(out);
return res;
}
bool big_is_zero(Value v) {
if (IS_INT(v)) return as_int(v) == 0;
if (IS_BIGNUM(v)) return AS_BIGNUM(v)->sign == 0;
return false;
}
bool big_is_odd(Value v) {
if (IS_INT(v)) return (as_int(v) & 1) != 0;
if (IS_BIGNUM(v)) {
Bignum *b = AS_BIGNUM(v);
return b->n_limbs > 0 && (b->limbs[0] & 1ULL) != 0;
}
return false;
}
Value big_abs(Value v) {
if (IS_INT(v)) { int64_t x = as_int(v); return make_bignum_from_i64(x < 0 ? -x : x); }
Bignum *b = as_bignum(v);
if (b->sign >= 0) return v;
return bignum_normalize(1, b->limbs, b->n_limbs);
}
int big_cmp(Value a, Value b) {
Bignum *ba = as_bignum(a), *bb = as_bignum(b);
if (ba->sign != bb->sign) return ba->sign < bb->sign ? -1 : 1;
if (ba->sign == 0) return 0;
int m = mag_cmp(ba->limbs, ba->n_limbs, bb->limbs, bb->n_limbs);
return ba->sign > 0 ? m : -m;
}
/* Quotient: truncated toward zero (matches C / Python's // for non-negatives;
* Scheme's `quotient` is also truncate-toward-zero). */
Value big_quotient(Value a, Value b) {
Bignum *ba = as_bignum(a), *bb = as_bignum(b);
if (bb->sign == 0) lisp_error("division by zero");
if (ba->sign == 0) return VAL_INT(0);
uint32_t qcap = ba->n_limbs > 0 ? ba->n_limbs : 1;
uint32_t rcap = bb->n_limbs + 1;
uint64_t *q = (uint64_t *)ul_malloc(sizeof(uint64_t) * qcap);
uint64_t *r = (uint64_t *)ul_malloc(sizeof(uint64_t) * rcap);
uint32_t nq, nr;
mag_divmod(ba->limbs, ba->n_limbs, bb->limbs, bb->n_limbs, q, &nq, r, &nr);
int32_t sign = ba->sign * bb->sign;
Value res = bignum_normalize(nq == 0 ? 0 : sign, q, nq);
ul_free(q); ul_free(r);
return res;
}
/* Remainder: result has sign of dividend (Scheme `remainder`). */
Value big_remainder(Value a, Value b) {
Bignum *ba = as_bignum(a), *bb = as_bignum(b);
if (bb->sign == 0) lisp_error("division by zero");
if (ba->sign == 0) return VAL_INT(0);
uint32_t qcap = ba->n_limbs > 0 ? ba->n_limbs : 1;
uint32_t rcap = bb->n_limbs + 1;
uint64_t *q = (uint64_t *)ul_malloc(sizeof(uint64_t) * qcap);
uint64_t *r = (uint64_t *)ul_malloc(sizeof(uint64_t) * rcap);
uint32_t nq, nr;
mag_divmod(ba->limbs, ba->n_limbs, bb->limbs, bb->n_limbs, q, &nq, r, &nr);
Value res = bignum_normalize(nr == 0 ? 0 : ba->sign, r, nr);
ul_free(q); ul_free(r);
return res;
}
/* Modulo: result has sign of divisor (Scheme `modulo`). */
Value big_modulo(Value a, Value b) {
Value r = big_remainder(a, b);
/* If r != 0 and sign(r) != sign(b), add b. */
if (big_is_zero(r)) return r;
Bignum *br = as_bignum(r), *bb = as_bignum(b);
if (br->sign != bb->sign) return big_add(r, b);
return r;
}
/* Binary exponentiation. exp must be a non-negative integer. */
Value big_expt(Value base, Value exp) {
/* exp must be an integer. */
if (!IS_INTEGER(exp)) lisp_error("expt: integer exponent required");
/* Negative exponent → not handled here (caller falls back to double). */
if (IS_INT(exp) && as_int(exp) < 0) lisp_error("expt: negative exponent");
if (IS_BIGNUM(exp) && AS_BIGNUM(exp)->sign < 0) lisp_error("expt: negative exponent");
/* Read exp as u64 — bignum exponents larger than 2^63 would never finish. */
uint64_t e;
if (IS_INT(exp)) e = (uint64_t)as_int(exp);
else {
Bignum *be = AS_BIGNUM(exp);
if (be->n_limbs > 1) lisp_error("expt: exponent too large");
e = be->n_limbs == 0 ? 0 : be->limbs[0];
}
Value result = VAL_INT(1);
Value b = base;
while (e > 0) {
if (e & 1) result = big_mul(result, b);
e >>= 1;
if (e) b = big_mul(b, b);
}
return result;
}
/* ═══════════════════════════════════════════════════════════════════════════
* Decimal rendering
* ═══════════════════════════════════════════════════════════════════════════ */
char *bignum_to_str(Bignum *b) {
if (b->sign == 0) return ul_strdup("0");
/* Divide repeatedly by 10^18 → 18 decimal digits per chunk. */
uint32_t n = b->n_limbs;
uint64_t *mag = (uint64_t *)ul_malloc(sizeof(uint64_t) * n);
memcpy(mag, b->limbs, sizeof(uint64_t) * n);
/* Generous size: log10(2) ≈ 0.30103 → n*64*0.31 + sign + null. */
size_t cap = (size_t)n * 21 + 4;
char *buf = (char *)ul_malloc(cap);
size_t pos = cap;
buf[--pos] = '\0';
const uint64_t CHUNK = 1000000000000000000ULL; /* 10^18 fits in u64. */
while (n > 0) {
uint64_t rem = mag_divmod_u64(mag, n, CHUNK, mag, &n);
if (n > 0) {
/* Full 18-digit chunk (zero-pad). */
for (int i = 0; i < 18; i++) {
buf[--pos] = '0' + (int)(rem % 10);
rem /= 10;
}
} else {
/* Top chunk: no leading zeros. */
if (rem == 0) buf[--pos] = '0';
else while (rem) { buf[--pos] = '0' + (int)(rem % 10); rem /= 10; }
}
}
if (b->sign < 0) buf[--pos] = '-';
/* Move string to start. */
size_t len = cap - pos;
memmove(buf, buf + pos, len);
ul_free(mag);
return buf;
}