cint university / 201 / ced, the cint editor
201-01: numbers that hold
This is lesson 201-01 of CU-201.2, checked against cint 0.2.0.
You will run a program in your browser and with cint run, watch it stop at the one multiplication that does not fit and read the fault record.
the program
The program puts one grain of rice on the first square of a chessboard and twice as many on each square after it. The total after 64 squares is 264 - 1.
// One grain of rice on the first square of a chessboard,
// twice as many on each square after it.
void main() {
I64 grains = 1;
I64 total = 0;
for square in 1..=64 {
total += grains;
"square {square:>2}: {grains:>20} total {total:>20}\n";
grains *= 2;
}
}
run it
$ cint run chessboard.ci
square 1: 1 total 1
square 2: 2 total 3
square 3: 4 total 7
...
square 62: 2305843009213693952 total 4611686018427387903
square 63: 4611686018427387904 total 9223372036854775807
fault[E_OVERFLOW]: the exact result is outside the type's range
--> chessboard.ci:9:16
|
9 | grains *= 2;
| ^ mul.checked.i64
|
= operation: mul.checked.i64
= operand-count: 2
= operand: I64 4611686018427387904
= operand: I64 2
= exact: 9223372036854775808
= limit: I64 9223372036854775807
= revision: 20867626996cb61a47e788e74f82b7226550f13626a9d9494bb161d07f294382
= source-map: none
= address: none
= stack-depth: 0
= fuel-consumed: 64 (not part of the fault record)
= state: the program stopped; stdout holds what it wrote before the fault
After square 63 the total is 9223372036854775807, which is I64.max exactly. The doubling that follows has no I64 result, so the program stops at it. Every line it printed before the fault is correct; no wrong number reached the output.
elsewhere
The same loop was run in other languages on 2026-10-10:
| Language | What it printed |
|---|---|
| Python 3.13 | The exact total, 18446744073709551615: int has no fixed size. |
| JavaScript, Node 22.22.0 | At square 54, a total of 18014398509481984, one more than the exact 18014398509481983, with no error. Numbers are doubles, which hold every integer only up to 253. Square 64 printed 18446744073709552000. |
NumPy 2.5.3, int64 scalars |
At square 64, grains -9223372036854775808 and total -1, with RuntimeWarning: overflow encountered in scalar multiply. |
NumPy 2.5.3, int64 arrays |
(2 ** np.arange(64, dtype=np.int64)).sum() gives -1, with no warning. |
C, GCC 13.3, -O2 |
Signed overflow is undefined behavior. This build printed grains -9223372036854775808 and total -1. Built with -fsanitize=undefined, it also reports signed integer overflow: 4611686018427387904 * 2 cannot be represented in type 'long int' and keeps going. |
Python computes the exact total because its integers grow without a fixed size. JavaScript, NumPy and C give no correct result for square 64; only the NumPy scalar warning and the C sanitizer build show that something went wrong. cint keeps fixed-size integers and checks every plain operator instead.
walk-through
Types come first. I64 grains = 1; declares a signed 64-bit integer. Every variable has a type, and nothing changes it silently: 201-02 covers conversions.
for square in 1..=64 counts from 1 to 64 inclusive. 0..n excludes n, as Python's range does.
Printing happens when a string literal stands on its own as a statement. {grains:>20} prints grains right-aligned in 20 characters.
The multiplication is checked. For grains *= 2, cint computes the exact product, 9223372036854775808, compares it with the type's range and stops because it does not fit. 201-00 shows the call this becomes in the emitted C.
reading the fault record
| Field | Meaning |
|---|---|
fault[E_OVERFLOW] |
The fault code. E_OVERFLOW means an exact result outside the type's range. |
--> and the caret |
The source position of the operation, here line 9, character 16. |
operation |
The operation by name: a checked (checked) multiply (mul) on i64. Each kind of checked operation has its own name. |
operand-count, operand |
The inputs, each with its type. |
exact |
The mathematically exact result. |
limit |
The bound it crossed. |
revision |
A digest of the program's operations, without their source positions. It is the same on every compiler for the same program and cint release. It stays the same when you only add blank lines or comments and changes when an operation changes. |
source-map |
A separate digest of the source positions. cint 0.2.0 prints none. |
address |
For a fault inside a kernel, the element it was working on; none elsewhere. |
stack-depth |
How many calls deep the operation was. A fault inside a function adds a called from line for each caller. |
fuel-consumed |
How many counted steps the program ran before the fault. It is not part of the record, but it is the same on every compiler, so a change in it means the program took a different path. |
The record is the same on every compiler cint supports, field for field, and so is everything the program printed before it. That is what lets you compare two runs of a program, on two machines or before and after a change, and trust that any difference comes from the program. 201-07 builds a way of debugging on it.
fix it
There are two fixes, and they mean different things:
void main() {
U64 grains = 1;
U64 total = 0;
for square in 1..=64 {
total += grains;
if (square < 64) { grains *= 2; }
}
"64 squares: {total} grains\n";
}
64 squares: 18446744073709551615 grains
U64.max is exactly 264 - 1, so the total fits. It fits only if the program skips the doubling after the last square, which would need 264.
The second fix is a wider type. cint's integers go up to 1024 bits:
void main() {
I128 grains = 1;
I128 total = 0;
for square in 1..=64 {
total += grains;
grains *= 2;
}
"64 squares: {total} grains\n";
"next square would hold {grains}\n";
}
64 squares: 18446744073709551615 grains
next square would hold 18446744073709551616
try this
- Change both types to
I32. Which square's total is the last one that fits, and what is it? - Triple instead of double. Predict the last square an
I64board reaches.
answer to 1
The doubling after square 31 faults: = operation: mul.checked.i32
= operand-count: 2
= operand: I32 1073741824
= operand: I32 2
= exact: 2147483648
= limit: I32 2147483647
Square 31's total is 2147483647, `I32.max`.
reference
the fault record, checked arithmetic, integer types, ranges, printing