feat(types): BigInt + Decimal exact economy numbers (#52)
All checks were successful
bootstrap / cfree-fixpoint (push) Successful in 19s
ci / build-and-test (push) Successful in 1m28s
commit-lint / conventional-commits (push) Successful in 5s
docs / build-and-deploy (push) Successful in 22s

Types phase 2 — the "money problem". A splice-on-demand bignum engine
(runtime/native/bignum.ludic, self-contained, only intrinsics) exposed as
two namespaces:

- BigInt.* — arbitrary-precision integer (sign-magnitude, base-1e9 limbs):
  from/parse, add/sub/mul/pow, div/mod (by int), cmp/eq/is_zero, to_int, str.
  For idle counters and exact huge currencies that overflow a 32/64-bit int.
- Decimal.* — exact base-10 fixed point (BigInt mantissa + decimal scale):
  from/parse, exact add/sub/mul, cmp/eq, scale/rescale (truncate), str.
  So 0.10 + 0.20 is exactly 0.30 — no binary rounding.

Both exact => deterministic; no f32/f64. Wired: parser splice trigger
(g_uses_bignum), emit_call dispatch, reseeded seed, a self-asserting example
(examples/library/bignum.ludic + feat_case), and per-symbol docs + inventory.
All suites green incl. golden renders byte-identical and the bootstrap fixpoint.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Orkun ÇAKILKAYA 2026-08-31 18:45:14 +03:00
parent 2c9f9ac549
commit bd6b12d2ea
35 changed files with 23274 additions and 20791 deletions

View file

@ -0,0 +1,13 @@
bump: minor
type: feat
**Exact numbers for game economies — `BigInt` + `Decimal` (#52, types phase 2).**
The thing a tycoon or idle game must never round wrong. `BigInt.*` is an
arbitrary-precision integer (sign-magnitude, base-1e9 limbs) with `from`/`parse`,
`add`/`sub`/`mul`/`pow`, integer `div`/`mod`, `cmp`/`eq`/`is_zero`, `to_int` and
`str` — for idle counters and exact huge currencies that overflow a 32/64-bit
int. `Decimal.*` is an exact base-10 fixed-point number (a `BigInt` mantissa
plus a decimal scale) with `from`/`parse`, exact `add`/`sub`/`mul`, `cmp`/`eq`,
`scale`/`rescale` and `str` — so money like `0.10 + 0.20` is exactly `0.30`, no
binary rounding. Both are exact and therefore deterministic; no `f32`/`f64`. The
engine (runtime/native/bignum.ludic) is spliced in on demand, so it costs
nothing when unused and works in plain tools as well as games.

View file

@ -0,0 +1,7 @@
---
id: bigint
title: BigInt
order: 10
---
Arbitrary-precision integers with no upper bound, for idle/incremental counters, exact huge currencies, and score arithmetic that a 32- or 64-bit <code>int</code> would overflow. A <code>BigInt</code> is a sign-magnitude number stored as base-1e9 limbs, so every operation is <strong>exact</strong> and therefore deterministic — bit-identical on every platform, with no binary floating point. Build one with <code>BigInt.from</code> (an <code>int</code>) or <code>BigInt.parse</code> (decimal text), combine with <code>add</code> / <code>sub</code> / <code>mul</code> / <code>pow</code> / <code>div</code> / <code>mod</code>, compare with <code>cmp</code> / <code>eq</code> / <code>is_zero</code>, and render with <code>str</code>. Arguments are positional. The runtime is spliced in only when a program mentions <code>BigInt.*</code>.

View file

@ -0,0 +1,22 @@
---
id: bigint-add
name: BigInt.add
category: bigint
kind: namespace-method
tokens: BigInt.add
sig: BigInt.add(a, b) -> BigInt
tip: Exact sum of two big integers.
order: 2
ns: BigInt
member: add
---
Returns the exact sum <code>a + b</code>. There is no overflow — the result grows as many digits as it needs.
```ludic
program Demo {
handler Step phase Update {
let total = BigInt.add(score, reward)
}
}
```

View file

@ -0,0 +1,22 @@
---
id: bigint-cmp
name: BigInt.cmp
category: bigint
kind: namespace-method
tokens: BigInt.cmp
sig: BigInt.cmp(a, b) -> int
tip: Compare two big integers: -1, 0, or 1.
order: 9
ns: BigInt
member: cmp
---
Compares two big integers, returning <code>-1</code>, <code>0</code>, or <code>1</code> as <code>a</code> is less than, equal to, or greater than <code>b</code>.
```ludic
program Demo {
handler Step phase Update {
if BigInt.cmp(score, best) > 0 { best = score }
}
}
```

View file

@ -0,0 +1,22 @@
---
id: bigint-div
name: BigInt.div
category: bigint
kind: namespace-method
tokens: BigInt.div
sig: BigInt.div(a, d) -> BigInt
tip: Divide a big integer by an int (toward zero).
order: 7
ns: BigInt
member: div
---
Divides a <code>BigInt</code> by an <code>int</code> divisor, truncating toward zero. Handy for splitting an exact total into equal shares.
```ludic
program Demo {
handler Step phase Update {
let each = BigInt.div(pot, players)
}
}
```

View file

@ -0,0 +1,22 @@
---
id: bigint-eq
name: BigInt.eq
category: bigint
kind: namespace-method
tokens: BigInt.eq
sig: BigInt.eq(a, b) -> bool
tip: True when two big integers are equal.
order: 10
ns: BigInt
member: eq
---
Returns true when two big integers have exactly the same value.
```ludic
program Demo {
handler Step phase Update {
if BigInt.eq(score, target) { win() }
}
}
```

View file

@ -0,0 +1,22 @@
---
id: bigint-from
name: BigInt.from
category: bigint
kind: namespace-method
tokens: BigInt.from
sig: BigInt.from(value) -> BigInt
tip: Turn a plain int into a BigInt.
order: 0
ns: BigInt
member: from
---
Lifts a plain <code>int</code> into a <code>BigInt</code>, the starting point for exact arbitrary-precision arithmetic that would overflow an ordinary integer.
```ludic
program Demo {
handler Step phase Update {
let n = BigInt.from(1000000)
}
}
```

View file

@ -0,0 +1,22 @@
---
id: bigint-is_zero
name: BigInt.is_zero
category: bigint
kind: namespace-method
tokens: BigInt.is_zero
sig: BigInt.is_zero(a) -> bool
tip: True when a big integer is zero.
order: 11
ns: BigInt
member: is_zero
---
Returns true when the value is exactly zero — cheaper and clearer than comparing against <code>BigInt.from(0)</code>.
```ludic
program Demo {
handler Step phase Update {
if BigInt.is_zero(balance) { gameOver() }
}
}
```

View file

@ -0,0 +1,22 @@
---
id: bigint-mod
name: BigInt.mod
category: bigint
kind: namespace-method
tokens: BigInt.mod
sig: BigInt.mod(a, d) -> int
tip: Remainder of a big integer divided by an int.
order: 8
ns: BigInt
member: mod
---
Returns the remainder of <code>a</code> divided by an <code>int</code> divisor, carrying the sign of <code>a</code>. Pairs with <code>BigInt.div</code>.
```ludic
program Demo {
handler Step phase Update {
let leftover = BigInt.mod(pot, players)
}
}
```

View file

@ -0,0 +1,22 @@
---
id: bigint-mul
name: BigInt.mul
category: bigint
kind: namespace-method
tokens: BigInt.mul
sig: BigInt.mul(a, b) -> BigInt
tip: Exact product of two big integers.
order: 4
ns: BigInt
member: mul
---
Returns the exact product <code>a * b</code>. Multiplying two large counters never loses precision — the classic idle-game growth step.
```ludic
program Demo {
handler Step phase Update {
let next = BigInt.mul(count, multiplier)
}
}
```

View file

@ -0,0 +1,22 @@
---
id: bigint-neg
name: BigInt.neg
category: bigint
kind: namespace-method
tokens: BigInt.neg
sig: BigInt.neg(a) -> BigInt
tip: Negate a big integer.
order: 5
ns: BigInt
member: neg
---
Returns <code>-a</code> — the same magnitude with the opposite sign.
```ludic
program Demo {
handler Step phase Update {
let debt = BigInt.neg(balance)
}
}
```

View file

@ -0,0 +1,22 @@
---
id: bigint-parse
name: BigInt.parse
category: bigint
kind: namespace-method
tokens: BigInt.parse
sig: BigInt.parse(text) -> BigInt
tip: Parse decimal text into a BigInt.
order: 1
ns: BigInt
member: parse
---
Parses decimal text (an optional leading sign then digits) into a <code>BigInt</code> — the way to enter a value too large for an <code>int</code> literal.
```ludic
program Demo {
handler Step phase Update {
let huge = BigInt.parse("123456789012345678901234567890")
}
}
```

View file

@ -0,0 +1,22 @@
---
id: bigint-pow
name: BigInt.pow
category: bigint
kind: namespace-method
tokens: BigInt.pow
sig: BigInt.pow(a, exp) -> BigInt
tip: Raise a big integer to an int power.
order: 6
ns: BigInt
member: pow
---
Raises <code>a</code> to the (non-negative) integer power <code>exp</code> by binary exponentiation — an exact way to reach astronomically large magnitudes.
```ludic
program Demo {
handler Step phase Update {
let googol = BigInt.pow(BigInt.from(10), 100)
}
}
```

View file

@ -0,0 +1,22 @@
---
id: bigint-str
name: BigInt.str
category: bigint
kind: namespace-method
tokens: BigInt.str
sig: BigInt.str(a) -> string
tip: Render a big integer as decimal text.
order: 13
ns: BigInt
member: str
---
Renders the full value as decimal text, with a leading <code>-</code> when negative. This is how you display or serialize a <code>BigInt</code>.
```ludic
program Demo {
handler Step phase Update {
Screen.draw_text(8, 8, BigInt.str(score), Color.White, 1)
}
}
```

View file

@ -0,0 +1,22 @@
---
id: bigint-sub
name: BigInt.sub
category: bigint
kind: namespace-method
tokens: BigInt.sub
sig: BigInt.sub(a, b) -> BigInt
tip: Exact difference of two big integers.
order: 3
ns: BigInt
member: sub
---
Returns the exact difference <code>a - b</code>; the result is negative when <code>b</code> is larger.
```ludic
program Demo {
handler Step phase Update {
let remaining = BigInt.sub(bank, cost)
}
}
```

View file

@ -0,0 +1,22 @@
---
id: bigint-to_int
name: BigInt.to_int
category: bigint
kind: namespace-method
tokens: BigInt.to_int
sig: BigInt.to_int(a) -> int
tip: Narrow a big integer to a plain int (clamped).
order: 12
ns: BigInt
member: to_int
---
Returns the value as a plain <code>int</code> when it fits in 32 bits, clamping to the int min/max otherwise. Use it to feed a small big-integer back into ordinary code.
```ludic
program Demo {
handler Step phase Update {
let n = BigInt.to_int(BigInt.from(2024))
}
}
```

View file

@ -0,0 +1,7 @@
---
id: decimal
title: Decimal
order: 11
---
Exact base-10 fixed-point numbers for game economies — prices, balances and taxes on values like <code>0.10</code> that binary floating point cannot represent, so they always add up exactly. A <code>Decimal</code> is a <code>BigInt</code> mantissa with a decimal <code>scale</code> (the number of digits after the point), giving unbounded range and exact <code>add</code> / <code>sub</code> / <code>mul</code>. Build one with <code>Decimal.from</code> (an <code>int</code>) or <code>Decimal.parse</code> (text like <code>"19.99"</code>), compare with <code>cmp</code> / <code>eq</code>, change precision with <code>rescale</code> (truncates toward zero), read the current precision with <code>scale</code>, and render with <code>str</code>. Every operation is exact and deterministic. The runtime is spliced in only when a program mentions <code>Decimal.*</code> (or <code>BigInt.*</code>).

View file

@ -0,0 +1,22 @@
---
id: decimal-add
name: Decimal.add
category: decimal
kind: namespace-method
tokens: Decimal.add
sig: Decimal.add(a, b) -> Decimal
tip: Exact sum of two decimals.
order: 2
ns: Decimal
member: add
---
Returns the exact sum, aligning the two scales first so nothing is rounded. <code>0.10 + 0.20</code> is exactly <code>0.30</code>.
```ludic
program Demo {
handler Step phase Update {
let total = Decimal.add(subtotal, tax)
}
}
```

View file

@ -0,0 +1,22 @@
---
id: decimal-cmp
name: Decimal.cmp
category: decimal
kind: namespace-method
tokens: Decimal.cmp
sig: Decimal.cmp(a, b) -> int
tip: Compare two decimals: -1, 0, or 1.
order: 6
ns: Decimal
member: cmp
---
Compares two decimals regardless of their scales, returning <code>-1</code>, <code>0</code>, or <code>1</code>.
```ludic
program Demo {
handler Step phase Update {
if Decimal.cmp(balance, price) < 0 { deny() }
}
}
```

View file

@ -0,0 +1,22 @@
---
id: decimal-eq
name: Decimal.eq
category: decimal
kind: namespace-method
tokens: Decimal.eq
sig: Decimal.eq(a, b) -> bool
tip: True when two decimals are equal in value.
order: 7
ns: Decimal
member: eq
---
Returns true when two decimals are equal in value even if written at different scales — <code>1.5</code> equals <code>1.50</code>.
```ludic
program Demo {
handler Step phase Update {
if Decimal.eq(paid, price) { accept() }
}
}
```

View file

@ -0,0 +1,22 @@
---
id: decimal-from
name: Decimal.from
category: decimal
kind: namespace-method
tokens: Decimal.from
sig: Decimal.from(value) -> Decimal
tip: Turn a whole int into a Decimal.
order: 0
ns: Decimal
member: from
---
Lifts a whole <code>int</code> into a <code>Decimal</code> with scale zero — a starting point for exact money arithmetic.
```ludic
program Demo {
handler Step phase Update {
let qty = Decimal.from(3)
}
}
```

View file

@ -0,0 +1,22 @@
---
id: decimal-mul
name: Decimal.mul
category: decimal
kind: namespace-method
tokens: Decimal.mul
sig: Decimal.mul(a, b) -> Decimal
tip: Exact product of two decimals.
order: 4
ns: Decimal
member: mul
---
Returns the exact product; the result's scale is the sum of the operands' scales, so no precision is lost.
```ludic
program Demo {
handler Step phase Update {
let line = Decimal.mul(price, quantity)
}
}
```

View file

@ -0,0 +1,22 @@
---
id: decimal-neg
name: Decimal.neg
category: decimal
kind: namespace-method
tokens: Decimal.neg
sig: Decimal.neg(a) -> Decimal
tip: Negate a decimal.
order: 5
ns: Decimal
member: neg
---
Returns <code>-a</code> at the same scale — the same magnitude with the opposite sign.
```ludic
program Demo {
handler Step phase Update {
let refund = Decimal.neg(charge)
}
}
```

View file

@ -0,0 +1,22 @@
---
id: decimal-parse
name: Decimal.parse
category: decimal
kind: namespace-method
tokens: Decimal.parse
sig: Decimal.parse(text) -> Decimal
tip: Parse decimal text like "19.99".
order: 1
ns: Decimal
member: parse
---
Parses text like <code>"19.99"</code> or <code>"0.10"</code> into an exact <code>Decimal</code>, remembering how many digits followed the point. This is how you enter a price the way binary floating point never could.
```ludic
program Demo {
handler Step phase Update {
let price = Decimal.parse("19.99")
}
}
```

View file

@ -0,0 +1,22 @@
---
id: decimal-rescale
name: Decimal.rescale
category: decimal
kind: namespace-method
tokens: Decimal.rescale
sig: Decimal.rescale(d, places) -> Decimal
tip: Change precision (truncates toward zero).
order: 9
ns: Decimal
member: rescale
---
Changes the number of fractional digits. Increasing precision is exact; decreasing it truncates toward zero — how you round a computed price down to whole cents.
```ludic
program Demo {
handler Step phase Update {
let cents = Decimal.rescale(raw, 2)
}
}
```

View file

@ -0,0 +1,22 @@
---
id: decimal-scale
name: Decimal.scale
category: decimal
kind: namespace-method
tokens: Decimal.scale
sig: Decimal.scale(d) -> int
tip: The number of digits after the point.
order: 8
ns: Decimal
member: scale
---
Returns the current scale — how many digits sit after the decimal point.
```ludic
program Demo {
handler Step phase Update {
let places = Decimal.scale(price)
}
}
```

View file

@ -0,0 +1,22 @@
---
id: decimal-str
name: Decimal.str
category: decimal
kind: namespace-method
tokens: Decimal.str
sig: Decimal.str(d) -> string
tip: Render a decimal as text.
order: 10
ns: Decimal
member: str
---
Renders the value as text with its decimal point in place (a leading <code>-</code> when negative). This is how you display or serialize money.
```ludic
program Demo {
handler Step phase Update {
Screen.draw_text(8, 8, Decimal.str(balance), Color.White, 1)
}
}
```

View file

@ -0,0 +1,22 @@
---
id: decimal-sub
name: Decimal.sub
category: decimal
kind: namespace-method
tokens: Decimal.sub
sig: Decimal.sub(a, b) -> Decimal
tip: Exact difference of two decimals.
order: 3
ns: Decimal
member: sub
---
Returns the exact difference <code>a - b</code>, aligning scales so balances stay penny-accurate.
```ludic
program Demo {
handler Step phase Update {
let change = Decimal.sub(paid, price)
}
}
```

View file

@ -0,0 +1,30 @@
# bignum.ludic — BigInt.* (arbitrary-precision integers) and Decimal.* (exact
# base-10 money). Each assertion that holds prints its number, so a full run
# prints:
# 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
# Both types are exact, so they are deterministic (see runtime/native/bignum.ludic).
program BigNum {
entry {
# --- BigInt: exact integers with no upper bound ---
let a = BigInt.parse("123456789012345678901234567890")
let b = BigInt.from(1000000)
if BigInt.str(BigInt.mul(a, b)) == "123456789012345678901234567890000000" { print(1) }
if BigInt.str(BigInt.pow(BigInt.from(2), 100)) == "1267650600228229401496703205376" { print(2) }
if BigInt.str(BigInt.add(a, a)) == "246913578024691357802469135780" { print(3) }
if BigInt.str(BigInt.sub(BigInt.from(5), BigInt.from(12))) == "-7" { print(4) }
if BigInt.cmp(a, b) == 1 { print(5) }
if BigInt.eq(BigInt.from(42), BigInt.parse("42")) { print(6) }
if BigInt.str(BigInt.div(BigInt.parse("1000000000000000000"), 7)) == "142857142857142857" { print(7) }
if BigInt.mod(BigInt.from(17), 5) == 2 { print(8) }
if BigInt.is_zero(BigInt.sub(a, a)) { print(9) }
if BigInt.to_int(BigInt.from(2024)) == 2024 { print(10) }
# --- Decimal: money that adds up exactly (binary floats can't) ---
if Decimal.str(Decimal.add(Decimal.parse("0.10"), Decimal.parse("0.20"))) == "0.30" { print(11) }
if Decimal.str(Decimal.mul(Decimal.parse("19.99"), Decimal.from(3))) == "59.97" { print(12) }
if Decimal.eq(Decimal.parse("1.5"), Decimal.parse("1.50")) { print(13) }
if Decimal.str(Decimal.rescale(Decimal.parse("3.14159"), 2)) == "3.14" { print(14) }
if Decimal.str(Decimal.sub(Decimal.parse("100.00"), Decimal.parse("0.01"))) == "99.99" { print(15) }
if Decimal.cmp(Decimal.parse("2.5"), Decimal.parse("2.49")) == 1 { print(16) }
}
}

380
runtime/native/bignum.ludic Normal file
View file

@ -0,0 +1,380 @@
# ============================================================================
# bignum.ludic — arbitrary-precision integers (`BigInt.*`) and exact base-10
# decimals (`Decimal.*`), written in Ludic.
#
# The thing a tycoon or idle game must never get wrong: money that adds up
# exactly, and counters that grow past what a 32- or 64-bit int can hold. Both
# types are EXACT, so they are deterministic — the same guarantee the rest of
# the runtime gives, with no binary floating point anywhere.
#
# BigInt — a sign-magnitude integer of unbounded size. Limbs are base 1e9
# (nine decimal digits each), little-endian, in a `words` buffer, so
# printing is just per-limb decimal with zero-padding. Multiplication
# accumulates in i64 (`long`) so a limb-by-limb product never
# overflows. add / sub / mul / pow / compare / divide-by-int.
# Decimal — a BigInt mantissa plus a decimal `scale` (digits after the point),
# so a value is mantissa x 10^-scale. add / sub align scales and stay
# exact; mul adds scales; `rescale` truncates toward zero. Perfect for
# prices, balances and taxes on values like 0.10 that binary can't
# represent.
#
# ludicc splices this file into any program that mentions `BigInt.*` or
# `Decimal.*` (like the regex/value runtimes); it is a self-contained fragment
# (only compiler intrinsics), so it works in a plain `program { entry }` tool as
# well as a game. The namespaces (emit_call.ludic) alias each method to the
# matching `bigint_*` / `decimal_*` function below.
# ============================================================================
const BN_BASE: int = 1000000000 # 1e9 — nine decimal digits per limb
# a sign-magnitude big integer. sign is -1 / 0 / +1 (0 only for the value zero);
# limbs are base-1e9, little-endian; n is the number of significant limbs.
property BigNum { sign: int = 0, n: int = 0, limbs: words }
# allocate a BigNum with room for `cap` limbs (at least one), all zeroed
function bn_alloc(cap: int) -> BigNum {
var c = cap
if c < 1 { c = 1 }
let b = new BigNum
b.sign = 0; b.n = 0; b.limbs = words(c)
var i = 0
while i < c { b.limbs[i] = 0; i = i + 1 }
return b
}
# drop high zero limbs; a magnitude of zero forces sign 0
function bn_norm(b: BigNum) -> BigNum {
var k = b.n
while k > 0 and b.limbs[k - 1] == 0 { k = k - 1 }
b.n = k
if k == 0 { b.sign = 0 }
return b
}
function bigint_zero() -> BigNum { return bn_alloc(1) }
# an int -> BigNum (widened through i64 so INT_MIN's magnitude is representable)
function bigint_from(value: int) -> BigNum {
if value == 0 { return bigint_zero() }
var sign = 1
var v: long = value
if v < 0 { sign = -1; v = 0 - v }
let b = bn_alloc(3)
var i = 0
while v > 0 {
let q: long = v / BN_BASE
let lo: int = v - q * BN_BASE
b.limbs[i] = lo
v = q
i = i + 1
}
b.n = i; b.sign = sign
return bn_norm(b)
}
# decimal text (optionally signed) -> BigNum. Assumes valid digits.
function bigint_from_str(text: pointer) -> BigNum {
let e = len(text)
var i = 0
var sign = 1
if e > 0 and text[0] == 45 { sign = -1; i = 1 } # '-'
if e > 0 and text[0] == 43 { i = 1 } # '+'
while i < e - 1 and text[i] == 48 { i = i + 1 } # skip leading zeros
let ndig = e - i
if ndig <= 0 { return bigint_zero() }
let nlimb = (ndig + 8) / 9
let b = bn_alloc(nlimb)
var li = 0
var pos = e
while pos > i {
var start = pos - 9
if start < i { start = i }
var v = 0
var k = start
while k < pos { v = v * 10 + (text[k] - 48); k = k + 1 }
b.limbs[li] = v; li = li + 1
pos = start
}
b.n = nlimb; b.sign = sign
return bn_norm(b)
}
# compare magnitudes: -1 / 0 / 1
function bn_ucmp(a: BigNum, b: BigNum) -> int {
if a.n != b.n { if a.n > b.n { return 1 }; return -1 }
var i = a.n - 1
while i >= 0 {
if a.limbs[i] != b.limbs[i] { if a.limbs[i] > b.limbs[i] { return 1 }; return -1 }
i = i - 1
}
return 0
}
# magnitude add (ignores signs)
function bn_uadd(a: BigNum, b: BigNum) -> BigNum {
var m = a.n
if b.n > m { m = b.n }
let r = bn_alloc(m + 1)
var carry = 0
var i = 0
while i < m {
var s = carry
if i < a.n { s = s + a.limbs[i] }
if i < b.n { s = s + b.limbs[i] }
if s >= BN_BASE { r.limbs[i] = s - BN_BASE; carry = 1 } else { r.limbs[i] = s; carry = 0 }
i = i + 1
}
r.limbs[m] = carry
r.n = m + 1
return bn_norm(r)
}
# magnitude subtract, assuming |a| >= |b|
function bn_usub(a: BigNum, b: BigNum) -> BigNum {
let r = bn_alloc(a.n)
var borrow = 0
var i = 0
while i < a.n {
var s = a.limbs[i] - borrow
if i < b.n { s = s - b.limbs[i] }
if s < 0 { s = s + BN_BASE; borrow = 1 } else { borrow = 0 }
r.limbs[i] = s
i = i + 1
}
r.n = a.n
return bn_norm(r)
}
function bigint_neg(a: BigNum) -> BigNum {
let r = bn_alloc(a.n)
var i = 0
while i < a.n { r.limbs[i] = a.limbs[i]; i = i + 1 }
r.n = a.n; r.sign = 0 - a.sign
return r
}
function bigint_add(a: BigNum, b: BigNum) -> BigNum {
if a.sign == 0 { return b }
if b.sign == 0 { return a }
if a.sign == b.sign {
let r = bn_uadd(a, b); r.sign = a.sign
return bn_norm(r)
}
let c = bn_ucmp(a, b)
if c == 0 { return bigint_zero() }
if c > 0 { let r = bn_usub(a, b); r.sign = a.sign; return bn_norm(r) }
let r = bn_usub(b, a); r.sign = b.sign
return bn_norm(r)
}
function bigint_sub(a: BigNum, b: BigNum) -> BigNum { return bigint_add(a, bigint_neg(b)) }
function bigint_mul(a: BigNum, b: BigNum) -> BigNum {
if a.sign == 0 or b.sign == 0 { return bigint_zero() }
let r = bn_alloc(a.n + b.n)
var i = 0
while i < a.n {
var carry: long = 0
let ai: long = a.limbs[i]
var j = 0
while j < b.n {
let bj: long = b.limbs[j]
let cur: long = r.limbs[i + j]
let t: long = cur + ai * bj + carry
let q: long = t / BN_BASE
let lo: int = t - q * BN_BASE
r.limbs[i + j] = lo
carry = q
j = j + 1
}
var k = i + b.n
while carry > 0 {
let cur2: long = r.limbs[k]
let t2: long = cur2 + carry
let q2: long = t2 / BN_BASE
let lo2: int = t2 - q2 * BN_BASE
r.limbs[k] = lo2
carry = q2
k = k + 1
}
i = i + 1
}
r.n = a.n + b.n; r.sign = a.sign * b.sign
return bn_norm(r)
}
# a^exp for exp >= 0, by binary exponentiation
function bigint_pow(a: BigNum, exp: int) -> BigNum {
var result = bigint_from(1)
var base = a
var e = exp
while e > 0 {
if e - (e / 2) * 2 == 1 { result = bigint_mul(result, base) }
base = bigint_mul(base, base)
e = e / 2
}
return result
}
# floor-toward-zero divide by a nonzero int; quotient is a BigNum
function bigint_div_int(a: BigNum, d: int) -> BigNum {
var dd = d
var dsign = 1
if d < 0 { dsign = -1; dd = 0 - d }
let r = bn_alloc(a.n)
var rem: long = 0
let ddl: long = dd
var i = a.n - 1
while i >= 0 {
let cur: long = rem * BN_BASE + a.limbs[i]
let q: long = cur / ddl
rem = cur - q * ddl
r.limbs[i] = q
i = i - 1
}
r.n = a.n; r.sign = a.sign * dsign
return bn_norm(r)
}
# remainder of dividing by a nonzero int; carries the sign of `a`
function bigint_mod_int(a: BigNum, d: int) -> int {
var dd = d
if d < 0 { dd = 0 - d }
let ddl: long = dd
var rem: long = 0
var i = a.n - 1
while i >= 0 {
let cur: long = rem * BN_BASE + a.limbs[i]
rem = cur - (cur / ddl) * ddl
i = i - 1
}
let r: int = rem
return r * a.sign
}
function bigint_cmp(a: BigNum, b: BigNum) -> int {
if a.sign != b.sign { if a.sign > b.sign { return 1 }; return -1 }
if a.sign == 0 { return 0 }
let c = bn_ucmp(a, b)
if a.sign > 0 { return c }
return 0 - c
}
function bigint_eq(a: BigNum, b: BigNum) -> bool { return bigint_cmp(a, b) == 0 }
function bigint_is_zero(a: BigNum) -> bool { return a.sign == 0 }
# the value as a plain int when it fits in 32 bits, else clamped to the extreme
function bigint_to_int(a: BigNum) -> int {
if a.sign == 0 { return 0 }
var v: long = 0
var i = a.n - 1
while i >= 0 { v = v * BN_BASE + a.limbs[i]; i = i - 1 }
if a.sign < 0 { v = 0 - v }
let hi: long = 2147483647
let lo: long = 0 - hi - 1
if v > hi { let r: int = hi; return r }
if v < lo { return 0 - 2147483647 - 1 }
let r: int = v
return r
}
# a limb (< 1e9) as exactly nine digits, left-padded with zeros
function bn_pad9(x: int) -> pointer {
var s = string(x)
var pad = 9 - len(s)
var out = ""
while pad > 0 { out = out + "0"; pad = pad - 1 }
return out + s
}
function bigint_str(a: BigNum) -> pointer {
if a.sign == 0 { return "0" }
var out = ""
if a.sign < 0 { out = "-" }
out = out + string(a.limbs[a.n - 1])
var i = a.n - 2
while i >= 0 { out = out + bn_pad9(a.limbs[i]); i = i - 1 }
return out
}
# ---- Decimal: a BigNum mantissa scaled by 10^-scale ------------------------
property Dec { m: BigNum, scale: int = 0 }
function dec_make(m: BigNum, scale: int) -> Dec {
let d = new Dec
d.m = m; d.scale = scale
return d
}
function decimal_from(value: int) -> Dec { return dec_make(bigint_from(value), 0) }
# "[-]int[.frac]" -> Dec, scale = number of fractional digits
function decimal_from_str(text: pointer) -> Dec {
let e = len(text)
var dot = 0 - 1
var i = 0
while i < e { if text[i] == 46 { dot = i }; i = i + 1 } # '.'
if dot < 0 { return dec_make(bigint_from_str(text), 0) }
let intp = text[0..dot]
let fracp = text[dot + 1 .. e]
let scale = len(fracp)
return dec_make(bigint_from_str(intp + fracp), scale)
}
# mantissa of `d` scaled up to `newscale` (newscale >= d.scale), exact
function dec_scaled(d: Dec, newscale: int) -> BigNum {
let diff = newscale - d.scale
if diff <= 0 { return d.m }
return bigint_mul(d.m, bigint_pow(bigint_from(10), diff))
}
function decimal_add(a: Dec, b: Dec) -> Dec {
var sc = a.scale
if b.scale > sc { sc = b.scale }
return dec_make(bigint_add(dec_scaled(a, sc), dec_scaled(b, sc)), sc)
}
function decimal_sub(a: Dec, b: Dec) -> Dec {
var sc = a.scale
if b.scale > sc { sc = b.scale }
return dec_make(bigint_sub(dec_scaled(a, sc), dec_scaled(b, sc)), sc)
}
function decimal_mul(a: Dec, b: Dec) -> Dec { return dec_make(bigint_mul(a.m, b.m), a.scale + b.scale) }
function decimal_neg(a: Dec) -> Dec { return dec_make(bigint_neg(a.m), a.scale) }
function decimal_cmp(a: Dec, b: Dec) -> int {
var sc = a.scale
if b.scale > sc { sc = b.scale }
return bigint_cmp(dec_scaled(a, sc), dec_scaled(b, sc))
}
function decimal_eq(a: Dec, b: Dec) -> bool { return decimal_cmp(a, b) == 0 }
function decimal_scale(d: Dec) -> int { return d.scale }
# change the number of fractional digits: scaling up is exact, scaling down
# truncates toward zero (drop the extra low digits)
function decimal_rescale(d: Dec, places: int) -> Dec {
if places >= d.scale { return dec_make(dec_scaled(d, places), places) }
var diff = d.scale - places
var m = d.m
var i = 0
while i < diff { m = bigint_div_int(m, 10); i = i + 1 }
return dec_make(m, places)
}
function decimal_str(d: Dec) -> pointer {
var neg = ""
var m = d.m
if m.sign < 0 { neg = "-"; m = bigint_neg(m) }
var digits = bigint_str(m)
if d.scale == 0 { return neg + digits }
var nd = len(digits)
var pad = d.scale + 1 - nd
while pad > 0 { digits = "0" + digits; pad = pad - 1; nd = nd + 1 }
let cut = nd - d.scale
let ip = digits[0..cut]
let fp = digits[cut..nd]
return neg + ip + "." + fp
}

View file

@ -332,6 +332,39 @@ function emit_ns_call(ns: pointer, meth: pointer, e: Node) -> Val {
# tile char, e.g. '#'. line/flood/a_star return []Cell slices. (Pathfinding
# lives under Grid rather than a `Path` namespace — that name is the filesystem
# paths library.)
# BigInt.* / Decimal.* -> the bignum engine (runtime/native/bignum.ludic,
# spliced on demand). These are ordinary Ludic functions, so the generic call
# path resolves them to @fn_bigint_* / @fn_decimal_* and keeps their return
# types (BigNum / Dec / int / bool / string).
if (ns == "BigInt") {
if (meth == "from") { bare = "bigint_from"; push(labels, "value") }
if (meth == "parse") { bare = "bigint_from_str"; push(labels, "text") }
if (meth == "add") { bare = "bigint_add"; push(labels, "a"); push(labels, "b") }
if (meth == "sub") { bare = "bigint_sub"; push(labels, "a"); push(labels, "b") }
if (meth == "mul") { bare = "bigint_mul"; push(labels, "a"); push(labels, "b") }
if (meth == "neg") { bare = "bigint_neg"; push(labels, "a") }
if (meth == "pow") { bare = "bigint_pow"; push(labels, "a"); push(labels, "exp") }
if (meth == "div") { bare = "bigint_div_int"; push(labels, "a"); push(labels, "d") }
if (meth == "mod") { bare = "bigint_mod_int"; push(labels, "a"); push(labels, "d") }
if (meth == "cmp") { bare = "bigint_cmp"; push(labels, "a"); push(labels, "b") }
if (meth == "eq") { bare = "bigint_eq"; push(labels, "a"); push(labels, "b") }
if (meth == "is_zero") { bare = "bigint_is_zero"; push(labels, "a") }
if (meth == "to_int") { bare = "bigint_to_int"; push(labels, "a") }
if (meth == "str") { bare = "bigint_str"; push(labels, "a") }
}
if (ns == "Decimal") {
if (meth == "from") { bare = "decimal_from"; push(labels, "value") }
if (meth == "parse") { bare = "decimal_from_str"; push(labels, "text") }
if (meth == "add") { bare = "decimal_add"; push(labels, "a"); push(labels, "b") }
if (meth == "sub") { bare = "decimal_sub"; push(labels, "a"); push(labels, "b") }
if (meth == "mul") { bare = "decimal_mul"; push(labels, "a"); push(labels, "b") }
if (meth == "neg") { bare = "decimal_neg"; push(labels, "a") }
if (meth == "cmp") { bare = "decimal_cmp"; push(labels, "a"); push(labels, "b") }
if (meth == "eq") { bare = "decimal_eq"; push(labels, "a"); push(labels, "b") }
if (meth == "scale") { bare = "decimal_scale"; push(labels, "d") }
if (meth == "rescale") { bare = "decimal_rescale"; push(labels, "d"); push(labels, "places") }
if (meth == "str") { bare = "decimal_str"; push(labels, "d") }
}
if (ns == "Grid") {
if (meth == "line") { bare = "grid_line"; push(labels, "x0"); push(labels, "y0"); push(labels, "x1"); push(labels, "y1") }
if (meth == "blocked") { bare = "grid_blocked"; push(labels, "x"); push(labels, "y"); push(labels, "wall") }

View file

@ -175,6 +175,7 @@ function p_postfix() -> Node {
while true {
if is_op(".") { pi = pi + 1; let m = node(E_MEMBER); m.a = e; m.s = eat_id(); e = m
if e.a.kind == E_ID and e.a.s == "Regex" { g_uses_regex = true } # splice the regex runtime on demand
if e.a.kind == E_ID and (e.a.s == "BigInt" or e.a.s == "Decimal") { g_uses_bignum = true } # splice the bignum runtime on demand
if e.a.kind == E_ID and e.a.s == "Query" { g_uses_query = true } # splice the ECS spatial-query runtime on demand
if e.a.kind == E_ID and e.a.s == "Reflect" { g_uses_reflect = true } # force-emit the reflection ABI (Reflect.* reads the world schema)
if e.a.kind == E_ID and e.a.s == "Light" { g_uses_light = true } # splice the 2D light-accumulation pass on demand
@ -421,6 +422,7 @@ function path_join(dir: pointer, rel: pointer) -> pointer {
var loaded_paths: []pointer
var cur_dir: pointer
var g_uses_regex: bool = false # a program mentioned Regex.* -> splice the regex runtime
var g_uses_bignum: bool = false # a program mentioned BigInt.*/Decimal.* -> splice the bignum runtime
var g_uses_query: bool = false # a program mentioned Query.* -> splice the query runtime + reflection ABI
var g_uses_reflect: bool = false # a program mentioned Reflect.* -> force-emit the reflection ABI
var g_uses_light: bool = false # a program mentioned Light.* -> splice the 2D light pass
@ -599,6 +601,13 @@ function maybe_splice_runtime() -> void {
do_import("runtime/native/regex_vm.ludic")
cur_dir = saved
}
# any program that uses BigInt.*/Decimal.* gets the bignum engine spliced in
# (self-contained — only compiler intrinsics — so a plain tool works too).
if g_uses_bignum {
cur_dir = ""
do_import("runtime/native/bignum.ludic")
cur_dir = saved
}
# any program that uses Query.* gets the ECS spatial-query helpers spliced in;
# they read entity state through the reflection ABI (emit_decl force-emits it
# for a Query program even when it declares no events).
@ -713,6 +722,7 @@ function parse_program() -> void {
g_onlisten = new []Node
g_toggled_layers = new []pointer
g_uses_regex = false
g_uses_bignum = false
g_uses_query = false
g_uses_reflect = false
g_uses_esys = false

File diff suppressed because it is too large Load diff

View file

@ -383,6 +383,35 @@
"rect-contains",
"rect-intersects"
],
"bigint": [
"bigint-from",
"bigint-parse",
"bigint-add",
"bigint-sub",
"bigint-mul",
"bigint-neg",
"bigint-pow",
"bigint-div",
"bigint-mod",
"bigint-cmp",
"bigint-eq",
"bigint-is_zero",
"bigint-to_int",
"bigint-str"
],
"decimal": [
"decimal-from",
"decimal-parse",
"decimal-add",
"decimal-sub",
"decimal-mul",
"decimal-neg",
"decimal-cmp",
"decimal-eq",
"decimal-scale",
"decimal-rescale",
"decimal-str"
],
"duration": [
"duration-seconds",
"duration-minutes",

View file

@ -193,6 +193,7 @@ function cmd_test() -> int {
feat_case("library/crypto", "", "1 2 3 4 5 6 7 8 9", "crypto.ludic (Crypto SHA-256/HMAC/base64 KAT + CSPRNG shape)")
feat_case("library/uuid", "", "1 2 3 4 5 6 7 8 9 10", "uuid.ludic (Uuid v4/v7 format, version/variant, parse/equals)")
feat_case("library/noise", "", "1 2 3 4 5 6 7 8 9 10 11", "noise.ludic (Noise value/perlin/simplex/fbm/cellular determinism + range)")
feat_case("library/bignum", "", "1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16", "bignum.ludic (BigInt arbitrary-precision + Decimal exact base-10 money)")
feat_case("library/regex", "", "1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18", "regex.ludic (Regex match/find/groups/classes/quantifiers/replace + linear-time safety)")
feat_case("library/grid", "", "1 2 3 4 5 6 7 8 9 10 11 12 13", "grid.ludic (Grid line/flood/line_of_sight + A* pathfinding over the tilemap)")
feat_case("library/anim", "", "1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34", "anim.ludic (Anim frame/once/pingpong/cell + Tween progress/loop/yoyo/ease/number/round/point/tint)")