cint

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 this program in ced

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";
}

Run this program in ced

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";
}

Run this program in ced

64 squares: 18446744073709551615 grains
next square would hold 18446744073709551616

try this

  1. Change both types to I32. Which square's total is the last one that fits, and what is it?
  2. Triple instead of double. Predict the last square an I64 board 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

Next: 201-02, types and conversions.