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,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)
}
}
```