Checked arithmetic
Ordinary +, - and * never complain. A result too big for its type wraps around, and the program carries on with a wrong number: uint8 255 + 1 is 0. Often that is harmless — the values are known to be small. But in a size, a price, a count or an index, a silently wrong number is a bug waiting to happen.
When it would matter, ask Core to check instead. AddChecked, SubChecked and MulChecked do the same arithmetic and also tell you whether it overflowed. They work at every integer width, signed or unsigned.
Two answers from one call
A checked operation has two things to hand back: the result, and whether it is trustworthy. So it uses an out-parameter. The result is written through a pointer, and the return value is a bool:
var sum: uint8 = 0;
let first = AddChecked(uint8::Max - 5, 5, @sum);
@sum is a pointer to sum, and the call writes 255 into it. The returned bool answers the question "did it overflow?" — so here it is false.
That reads backwards the first time, since true means failure. Name the flag after the problem rather than the success — overflowed, short, deep — and the if that follows reads correctly.
flowchart LR
call["AddChecked(a, b, @result)"] --> w["writes a + b<br/>through @result"]
call --> q{"returns:<br/>did it overflow?"}
q -- "false" --> ok["result is the true answer"]
q -- "true" --> bad["result is the wrapped value:<br/>do not use it"]When it overflows
One step too far, and the flag is raised:
let second = AddChecked(uint8::Max, 1, @sum);
second is true. The pointer is written either way, with the value + would have wrapped to — here 0 — so check the flag before you use the result.
An unsigned type has nothing below zero, so subtraction overflows downwards:
var stock: uint32 = 0;
let short = SubChecked(3, 5, @stock);
short is true and stock holds the wrapped 4294967294. Signed types overflow at both ends — one below the minimum wraps to the maximum:
var level: int32 = 0;
let deep = SubChecked(int32::Min, 1, @level);
| Call | Overflow | Written to the pointer |
|---|---|---|
AddChecked(250u8, 5, …) | false | 255 |
AddChecked(255u8, 1, …) | true | 0 |
SubChecked(3u32, 5, …) | true | 4294967294 |
SubChecked(int32::Min, 1, …) | true | 2147483647 |
All three arguments share one type, which the compiler works out from the call — from a typed operand, or from the pointer when both operands are plain literals, as in SubChecked(3, 5, @stock). Every argument has to agree with it.
Reporting overflow in a type
A bool and an out-parameter are awkward to pass around. The usual move is to hide them inside a function whose return type says what can go wrong — here an optional:
func Area(width: uint32, height: uint32) -> uint32? {
var area: uint32 = 0;
let overflowed = MulChecked(width, height, @area);
if overflowed {
return none;
}
return area;
}
A caller can no longer use a wrapped area by accident. It gets none, and has to decide — with ??, or with a match:
PrintLine("area {}", Area(1920, 1080) ?? 0);
match Area(100000, 100000) {
area? => PrintLine("area {}", area),
none => PrintLine("area too large")
}
100 000 × 100 000 is ten billion, more than a uint32 holds, so the second call prints too large.
The program
The whole lesson is one package in the Examples repository. Its comments explain every step.
// Ordinary `+`, `-` and `*` never complain: a result too big for its type wraps around and the
// program carries on with a wrong number. When a wrong number would matter, as in a size, a price
// or a count, ask `Core` to check instead. `AddChecked`, `SubChecked` and `MulChecked` work at
// every integer width, signed or unsigned.
//
// Each one hands back two things, so it uses an out-parameter: the result is written through the
// pointer, and the return value is a `bool` that answers "did it overflow?". That reads backwards
// the first time, since `true` means failure, so name the flag after the problem rather than the
// success. The pointer is written either way, with the wrapped value when the answer is `true`.
//
// A function that does the checking can then report overflow in a type, here as an optional.
import Core::{ AddChecked, MulChecked, SubChecked, int32, uint8 };
import Io::PrintLine;
// The area of a rectangle, or `none` when it does not fit in 32 bits.
func Area(width: uint32, height: uint32) -> uint32? {
var area: uint32 = 0;
let overflowed = MulChecked(width, height, @area);
if overflowed {
return none;
}
return area;
}
func Main() -> int {
var sum: uint8 = 0;
// Within range: no overflow, and the result is the true sum.
let first = AddChecked(uint8::Max - 5, 5, @sum);
PrintLine("250 + 5 overflow {:5} result {}", first, sum);
// One step too far: the flag is raised, and the result is what `+` would have wrapped to.
let second = AddChecked(uint8::Max, 1, @sum);
PrintLine("255 + 1 overflow {:5} result {}", second, sum);
// An unsigned type has nothing below zero.
var stock: uint32 = 0;
let short = SubChecked(3, 5, @stock);
PrintLine("3 - 5 overflow {:5} result {}", short, stock);
// Signed types overflow at both ends.
var level: int32 = 0;
let deep = SubChecked(int32::Min, 1, @level);
PrintLine("Min - 1 overflow {:5} result {}", deep, level);
// The check, hidden behind a function that says what went wrong in its type.
PrintLine("area {}", Area(1920, 1080) ?? 0);
match Area(100000, 100000) {
area? => PrintLine("area {}", area),
none => PrintLine("area too large")
}
return 0;
}
Besides Io, its Rux.toml lists Core under [Dependencies].
Run it
cd Examples/Numbers/CheckedArithmetic
rux run
250 + 5 overflow false result 255
255 + 1 overflow true result 0
3 - 5 overflow true result 4294967294
Min - 1 overflow true result 2147483647
area 2073600
area too large
Common mistakes
The
bool means "overflowed", so true is the bad case. if AddChecked(a, b, @sum) { use(sum) } uses exactly the results it should reject. Name the flag after the problem — let overflowed = … — and test that.AddChecked(a, 5, @sum); on its own compiles: the bool may be dropped. But then the result is no safer than a + 5. Keep the flag and act on it.Both operands must have one type. With a
uint8 called a and an int32 called b, AddChecked(a, b, @sum) fails with error: argument 2 to 'AddChecked' has type 'int32', but parameter 'right' requires 'uint8'. Convert one of them first — checking that it fits.AddChecked(a, 5, @sum) with a uint8 called a and an int32 called sum fails with error: argument 1 to 'AddChecked' has type 'uint8', but parameter 'left' requires 'T'. The message names the first argument, but the fix is the third: the result must have the operands' type.Try it yourself
- Write
func Total(prices: uint32[..]) -> uint32?that adds the prices withAddCheckedand returnsnoneat the first overflow. - Use
MulCheckedon twoint8values, −128 and −1. Does it overflow? Why? - Change
Areato returnuint32 ! AreaError, with an error variantTooLarge, and handle it inMainwithcatch.
Learn more
- Arithmetic operations in the Rux Reference
- Wrapping arithmetic — when overflow is the plan, not the problem
- Checked convert — the same idea for conversions between types
- Out-parameter — returning a second answer through a pointer