Ints, Floats, Bignums, and Overflow: the number story
How eight languages answer one quiet question: what happens when a number gets too big?
Every language must decide what an integer is, and the decision leaks into everything. Add one to the largest integer you have and the language's whole philosophy falls out: it wraps, it panics, it promotes, or it simply grows. Here is how eight of them answer.
Two families
The first split is between languages whose default integer is a fixed-width machine word and those whose default is an arbitrary-precision "bignum". Fixed-width is fast and predictable in memory; bignums never overflow but cost an allocation and a branch on the hot path. Almost every other design choice follows from this fork.
Python sits firmly in the bignum camp. Its int is unbounded, so 2 ** 100 is just a number:
print(2 ** 100) # 1267650600228229401496703205376
print(10**20 * 10**20) # 40 digits, exact
import sys
print(sys.maxsize + 1) # 9223372036854775808, no overflow
sys.maxsize reflects the platform word, but it is a hint about container indices, not a ceiling on arithmetic. The cost is real - small-integer math in Python is boxed and slower than a C long - but the programmer never thinks about wrapping. Floor division // and a modulo that follows the sign of the divisor (-7 % 3 == 2) round out a set of choices aimed at "least surprise" for everyday math.
The machine-word camp, and what overflow means
Rust, Go, and OCaml default to fixed-width integers, and the interesting differences are entirely about overflow.
Rust draws the sharpest line. In debug builds, overflow panics; in release builds, it wraps (two's complement), and the language makes you say which one you mean when it matters:
let a: i32 = i32::MAX;
let b = a.wrapping_add(1); // -2147483648, defined wrap
let c = a.checked_add(1); // None
let d = a.saturating_add(1); // i32::MAX
let (e, of) = a.overflowing_add(1); // (-2147483648, true)
This family of methods - wrapping_, checked_, saturating_, overflowing_ - is Rust's real answer: overflow is not one behavior but four, and the type system makes you pick. For unbounded math you reach for the num-bigint crate; bignums are a library, not a default.
Go takes the C-like road: int and the sized types wrap silently on overflow with no panic, which is fast and occasionally dangerous. But Go has a quiet trick - untyped constants are arbitrary precision at compile time, so const big = 1 << 100 is legal as long as the final value fits its destination type. For runtime bignums, math/big provides big.Int and big.Rat. Go also gives you math.MaxInt64 and friends, but checking against them is on you.
OCaml's native int is a 63-bit integer (one bit is stolen for the runtime's tagging), and it wraps silently like Go. Distinct operators signal the float world: + is integer addition, +. is float addition, and OCaml refuses to mix them without an explicit float_of_int. Arbitrary precision lives in the Zarith library, whose Z.t is the standard answer for cryptography and exact arithmetic.
Haskell is the camp's diplomat. Its default Integer is an arbitrary-precision bignum, like Python, so product [1..100] is exact. But Int is a fixed-width machine word that wraps silently, and the type classes Num, Integral, and Fractional let the same literal 42 resolve to whichever type the context demands:
factorial :: Integer -> Integer
factorial n = product [1..n] -- factorial 100 is exact, 158 digits
Type-class defaulting picks Integer when nothing pins the type down, so the safe choice is the lazy default and the fast choice is opt-in. That inversion - safe-by-default, fast-on-request - is the opposite of Go and the same as Python, reached from a completely different direction.
The Perl and Raku approach: numbers that change shape
Perl is the great pragmatist here. A scalar is not typed as int or float; it holds whatever it needs and promotes on demand. Add one past the 64-bit ceiling and Perl quietly converts to a double:
print 9223372036854775807 + 1, "\n"; # 9223372036854775808 (now a float)
print 0.1 + 0.2, "\n"; # 0.3 (stringified to 15 sig digits)
use bigint;
print 2 ** 100, "\n"; # exact 31-digit integer
Two things to notice. First, 0.1 + 0.2 prints 0.3, not the famous 0.30000000000000004 - the value is the same IEEE 754 double, but Perl's default stringification rounds to roughly 15 significant figures and hides the noise. Second, use bigint (or bignum, Math::BigInt) flips the whole lexical scope into arbitrary precision. There is, as Larry Wall liked to remind us, more than one way to do it, and number representation is no exception.
Raku, Perl's sibling, goes further and bakes the bignum in. Its Int is arbitrary precision by default, so 2 ** 100 needs no pragma. More striking is Rat: a literal like 0.1 + 0.2 is stored as an exact rational 3/10, so it really does equal 0.3 with no floating error at all. You opt into Num (a raw double) only when you want speed or call into the float world. Raku is the one language here that makes exact decimal arithmetic the path of least resistance.
Guji: checked by default, no coercion
Guji (in-house, v0.1-alpha) lands in the machine-word camp but with a stance closer to debug-mode Rust than to Go. Its Int is a 64-bit signed integer, and overflow is a hard runtime panic in both the reference interpreter and the native AOT build - the two are kept in parity, so what aborts under interpretation also aborts compiled:
sub main() {
$big = 9223372036854775807
print($big) # 9223372036854775807
print($big + 1) # panic: integer overflow: 9223372036854775807 + 1
}
There is no silent wrap and no automatic promotion to a bignum; the program stops rather than produce a wrong number. The second pillar is strictness about the int/float boundary. Guji is statically typed and functional-first, and it refuses to mix the two families implicitly:
sub main() {
print(3 + 2.0) # type error: + requires matching operand types, got Int and Float
}
You write 2.0 + 1.0 or 3 + 2, never a quiet blend, which echoes OCaml's separation of concerns without needing separate +. operators - the types alone disambiguate. Floats themselves are ordinary IEEE 754 doubles, with all the usual texture:
sub main() {
print(0.1 + 0.2) # 0.30000000000000004
print(1.0 / 0.0) # +Inf
print(7 / 2) # 3 (integer division truncates toward zero)
}
Integer division truncates toward zero and modulo follows the dividend's sign (-7 % 3 is -1), matching Rust, Go, and C rather than Python's flooring rule. The net effect is a language that will never hand you a silently corrupted integer: the cost is that genuinely unbounded arithmetic is not yet expressible in v0.1-alpha, so a bignum type is a clear item for the roadmap.
The shape of the choices
Lay them out and a pattern appears. Python, Haskell, and Raku make correctness the default and let you opt into speed. Go and OCaml make speed the default and let you opt into correctness. Rust refuses to choose for you, forcing the decision at every call site. Perl shape-shifts to whatever fits. Guji takes the strictest available position - fixed width for speed, but a panic rather than a wrong answer when that width runs out. None is wrong; each is a bet about which mistake costs more, a silent wrong number or a slow correct one.