Big Numbers
Navigation: Guide | Documentation Root
The V0.2 numeric overflow construct binds the high and low halves of an i128 intermediate result on every checked arithmetic operation. This is the load-bearing mechanism for multi-digit arithmetic against the bundled Word type, which is a 64-bit signed integer. This guide walks through the pattern with the worked example in examples/scripts/09_big_numbers.kel.
What the construct exposes
The construct’s surface form is
op_expr {
ok(v) => arm_body,
overflow(h, l) => arm_body,
underflow(h, l) => arm_body,
}
The runtime computes the true result of op_expr in i128, splits it into a high and a low half, and pushes (high, low, flag) on the operand stack. The compiler dispatches on flag to one of three outcome classes (ok, overflow, underflow) and binds the pattern variables in that arm’s body to the corresponding slot values. The ok arm binds a single Word against the in-range result; the overflow and underflow arms bind two Word values against the high and low halves.
The high half is the carry-out for additive operations and the upper 64 bits of the true product for multiplication. Together with the low half (the wrapped i64 result) this is sufficient to express chained multi-digit arithmetic.
Pattern: full 64x64 -> 128-bit multiplication
fn mul_full(a: Word, b: Word) -> (Word, Word) {
a * b {
ok(v) => (0, v),
overflow(h, l) => (h, l),
underflow(h, l) => (h, l),
}
}
When the true product fits in Word the ok arm fires and the high half is zero by definition. When the product needs more than 64 bits the construct routes to the overflow arm and binds the upper 64 bits of the true product to h.
Worked example: 2^32 * 2^32 = 2^64. The true product is the bit pattern 0x0000000000000001_0000000000000000 interpreted as a 128-bit value. The high half is 1, the low half is 0. The script’s main returns 1 confirming this decomposition.
Pattern: addition with carry-out
fn add_with_carry(a: Word, b: Word) -> (Word, Word) {
a + b {
ok(v) => (0, v),
overflow(_, l) => (1, l),
underflow(_, l) => (1, l),
}
}
The carry-out is derived from the overflow class rather than from the high half directly. For signed Word addition the high half of the i128 intermediate is the sign extension of the i64 wrap, not the unsigned carry; the cleaner abstraction is to read the carry from the outcome class. The wrapped result remains in the low slot.
A chained two-digit add propagates the carry to the next-higher position:
fn add_two_digits(a_hi: Word, a_lo: Word, b_hi: Word, b_lo: Word) -> (Word, Word) {
let (carry_lo, sum_lo) = add_with_carry(a_lo, b_lo);
let (_, partial_hi) = add_with_carry(a_hi, b_hi);
let (_, sum_hi) = add_with_carry(partial_hi, carry_lo);
(sum_hi, sum_lo)
}
For a full 256-bit add, repeat the same step over four Word positions, threading the carry through each.
Caveats
The Word type is signed i64. Treating it as an unsigned u64 digit in multi-digit arithmetic requires care:
-
The i128 intermediate’s high half reflects signed arithmetic. For two non-negative operands whose sum exceeds
i64::MAX, the high half is0and the low half is the wrap (a negativei64whose bit pattern matches the high bit of the unsigned sum). The carry-out is one regardless, derivable from the overflow class. -
For two operands whose sum needs more than 65 bits (impossible for
i64addition but reachable through multiplication), the high half carries genuine bits 64-127. The multiplication example demonstrates this case directly. -
Division and modulo route through dedicated
Op::CheckedDivandOp::CheckedModopcodes that compute the true result ini128and dispatch to the same(h, l, flag)shape as the other checked operations. Thei64::MIN / -1corner (true result2^63, decomposed ashigh=0, low=i64::MIN, flag=1) routes to the overflow arm; thei64::MIN % -1corner (true result0, division step overflows) likewise flags through the overflow arm withhigh=0, low=0. Division by zero continues to trap withVmError::DivisionByZerobecause the opcode fails before arm dispatch runs.
Where the pattern is and is not appropriate
The construct is appropriate when:
- The arithmetic needs to detect or recover from
Word-range overflow at well-defined points in the program. - The high half of a multiplication carries useful information (the load-bearing case for true 64x64 -> 128 products).
- The carry-out of an addition needs to thread into a higher-order digit.
The construct is not a substitute for an arbitrary-precision BigInt type. Multi-digit arithmetic at runtime through the construct works but is not ergonomically zero-cost; a future iteration may introduce a dedicated BigInt standard-library type with native arithmetic operators that compile to the chained checked operations under the hood.
Cross-references
- GRAMMAR.md, Section 7.5 — the formal grammar of the numeric overflow construct.
- LANGUAGE_DESIGN.md, “Partial Operation Handling” — the design rationale, of which checked arithmetic is one member of the construct family.
examples/scripts/09_big_numbers.kel— the worked example this guide walks through.tests/big_number_arithmetic.rs— the integration test that compiles the example and verifies the result.