Etch provides comprehensive compile-time overflow detection through its safety prover. This document explains how overflow checking works, why it’s important, and how to write code that works with the prover.
Integer overflow is a common source of bugs and security vulnerabilities in software. Etch’s prover analyzes your code before execution to ensure that all arithmetic operations are safe.
Key Features:
The prover uses a conservative approach: if overflow is mathematically possible given the range of input values, it’s an error.
“Possible overflow” means:
rand(), file I/O, function parameters, user input)Known constant values:
1000 + 2000 is safe, 9223372036854775807 + 1 overflowsConsider this function:
fn add(a: int, b: int) -> int {
return a + b; // ❌ ERROR: addition overflow
}
Even though you might call it with safe values like add(5, 10), the function could be called with any int64 values. The prover must assume the worst case: add(IMax, 1) would overflow.
The prover tracks ranges for every value in your program:
var x = rand(1, 100); // x ∈ [1, 100]
var y = rand(1, 50); // y ∈ [1, 50]
var z = x + y; // z ∈ [2, 150] ✅ Safe
The prover computes:
min(x) + min(y) = 1 + 1 = 2max(x) + max(y) = 100 + 50 = 150[2, 150] fit in int64? Yes! ✅When you create an array, the prover tracks the range of all elements:
var arr = [1, 2, 3, 4, 5]; // Elements ∈ [1, 5]
var sum = 0; // sum ∈ [0, 0]
for i in 0 ..< #arr {
sum = sum + arr[i]; // Loop iteration 1: sum ∈ [1, 5]
// Loop iteration 2: sum ∈ [2, 10]
// Loop iteration 3: sum ∈ [3, 15]
// ... etc
}
// Final: sum ∈ [15, 15] ✅ Safe
The prover:
[1, 5]The prover uses fixed-point iteration to analyze loops:
var x = 0;
for i in 0 ..< 10 {
x = x + 5; // Prover iterates to find: x ∈ [0, 50] ✅
}
For unbounded loops or very large iteration counts, the prover may reach a fixed point where ranges stabilize or hit maximum iterations.
✅ Good: Explicit bounds that the prover can verify
fn safe_add(a: int, b: int) -> int {
// Add runtime checks if you can't prove safety at compile time
if a > IMax - b {
return IMax; // Saturating arithmetic
}
return a + b;
}
✅ Better: Use bounded input types
fn bounded_add(a: int, b: int) -> int {
// Document preconditions
// Requires: 0 <= a <= 1000, 0 <= b <= 1000
return a + b; // Prover verifies at call sites
}
// Call with bounded values
var x = rand(0, 1000);
var y = rand(0, 1000);
var result = bounded_add(x, y); // ✅ Safe
✅ Good: Bounded element ranges
fn array_sum(arr: array[int]) -> int {
var sum = 0;
for i in 0 ..< #arr {
sum = sum + arr[i]; // ✅ Safe if elements are bounded
}
return sum;
}
// Create array with small values
var data = [rand(1, 10), rand(1, 10), rand(1, 10)];
var total = array_sum(data); // ✅ Safe: sum ∈ [3, 30]
❌ Bad: Unbounded accumulation
fn unsafe_sum(arr: array[int]) -> int {
var sum = 0;
for i in 0 ..< #arr {
sum = sum + arr[i]; // ❌ ERROR: arr[i] has unbounded range
}
return sum;
}
// Array elements could be anything
var data = [rand(IMax), rand(IMax), rand(IMax)];
var total = unsafe_sum(data); // Would overflow!
When a function takes int parameters without constraints, they have the full int64 range:
fn add(a: int, b: int) -> int {
return a + b; // ❌ ERROR: a ∈ [IMin, IMax], b ∈ [IMin, IMax]
}
Solutions:
fn add(a: i32, b: i32) -> i32 {
return a + b; // ✅ Safe: i32 + i32 won't overflow i32 bounds
}
fn add_checked(a: int, b: int) -> int {
if a > 0 and b > IMax - a {
panic("overflow");
}
if a < 0 and b < IMin - a {
panic("underflow");
}
return a + b;
}
fn add_saturating(a: int, b: int) -> int {
if a > 0 and b > IMax - a {
return IMax;
}
if a < 0 and b < IMin - a {
return IMin;
}
return a + b;
}
✅ Safe: When loop bounds and element ranges are known
fn safe_average() -> int {
var sum = 0;
for i in 0 ..< 100 {
sum = sum + rand(1, 10); // Each iteration: +[1, 10]
} // Final: sum ∈ [100, 1000]
return sum / 100; // ✅ Safe
}
✅ Safe: Use conditionals to narrow ranges
fn conditional_add(a: int, b: int) -> int {
if a < 0 or a > 1000 {
return 0;
}
if b < 0 or b > 1000 {
return 0;
}
// Prover knows: a ∈ [0, 1000], b ∈ [0, 1000]
return a + b; // ✅ Safe: result ∈ [0, 2000]
}
✅ Safe: Use modulo to keep values bounded
fn hash_combine(h1: int, h2: int) -> int {
// Use modulo to prevent overflow
return (h1 * 31 + h2) % 1000000007; // ✅ Result ∈ [0, 1000000006]
}
✅ Safe: Bound the recursion depth
fn fibonacci(n: int) -> int {
if n <= 1 {
return n;
}
if n > 46 { // fib(46) is max safe value for int64
return 0;
}
var fib1 = fibonacci(n - 1);
var fib2 = fibonacci(n - 2);
// Use modulo to prevent overflow
return (fib1 % 1000000) + (fib2 % 1000000);
}
examples/test.etch:10:15: error: addition overflow
9 | var sum = a + b;
10 | var z = x + y;
^
11 |
What this means:
x + y could overflowx and y, their sum might exceed IMax--verbose to see range information
./etch --verbose test.etch 2>&1 | grep "Variable x\|Variable y"
x and y are initialized
var x = rand(1000000000); // Large range!
var y = rand(1000000000); // Large range!
var z = x + y; // Could overflow
var x = rand(100); // Smaller range
var y = rand(100); // Smaller range
var z = x + y; // ✅ Safe: max is 200
❌ Mistake 1: Assuming small test values mean it’s safe
fn add(a: int, b: int) -> int {
return a + b; // ❌ ERROR even though you test with add(1, 2)
}
The prover checks ALL possible values, not just your test cases.
❌ Mistake 2: Large array accumulation
var sum = 0;
for i in 0 ..< 1000000 {
sum = sum + 1000; // ❌ ERROR: 10^9 iterations * 10^3 = 10^12
}
✅ Fix: Use bounded accumulation or check the math
// 1000 iterations * 100 per iteration = 100,000 ✅ Safe
var sum = 0;
for i in 0 ..< 1000 {
sum = sum + 100;
}
src/etch/prover/binary_operations.nim# Known constants: check if actual sum overflows
if a.known and b.known:
let s = a.cval + b.cval
if (b.cval > 0 and a.cval > IMax - b.cval) or
(b.cval < 0 and a.cval < IMin - b.cval):
raise newProverError(e.pos, "addition overflow on constants")
return infoConst(s)
# Unknown ranges: check if ranges could overflow
if (b.maxv > 0 and a.maxv > IMax - b.maxv) or
(b.maxv < 0 and a.maxv < IMin - b.maxv):
raise newProverError(e.pos, "addition overflow")
Similar logic for subtraction, checking both:
a.minv - b.maxv < IMina.maxv - b.minv > IMaxUses division test to detect overflow:
let product = a * b
if a != 0 and product / a != b:
raise newProverError(e.pos, "multiplication overflow")
For range multiplication, computes all corner products and verifies each is safe.
src/etch/prover/expression_analysis.nimWhen analyzing arr[i], returns the element range:
# If array has element range information
if arrayInfo.initialized:
return Info(
minv: arrayInfo.minv, # Min element value
maxv: arrayInfo.maxv # Max element value
)
This allows the prover to track element bounds through array operations.
See examples/overflow_*.etch for comprehensive test cases demonstrating:
Run tests with:
just test examples/overflow_safe_ranges.etch
just test examples/overflow_fail.etch