Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Checked arithmetic blocks

Status: Implemented (issue #557). The numbers reference defines current behavior; this page records the decisions and rationale.

Problem

Integer overflow and division by zero trap. That is the right default when they mean a bug, but code that takes sizes, counts or prices from input needs to report them instead. Before this change the only recoverable options were try_from after widening, or std.math helpers, which do not compose across an expression.

Rejected: a Result from every operator

Local PR #678 made every integer + - * // % return Result and changed existing code to .unwrap() or ? each one. It was abandoned unmerged:

  • ordinary math became noisy, and most call sites only unwrapped;
  • every operation carried a payload and a branch on the result, which was slower than the existing inline trap check;
  • the tutorial had to teach Result before the first addition.

Its error types, float div_checked, T.MIN % -1 == 0, the overflow code generation and constant-divisor branch elimination were kept.

Decision: an expression block

r = checked:
    a * b + c

checked: opens an indented suite whose last expression is the value. The block’s type is Result[T, E], where E is the anonymous sum of OverflowError[T] per operand width and ZeroDivisionError that its operators can produce. The first failure exits the block with Err.

Outside any block, arithmetic compiles exactly as before, so the cost is paid only where it is asked for.

Lexical scope

Only arithmetic written in the block’s suite is checked. Calls, closures (including closures defined in the block) and other functions keep their own behavior. A dynamic rule, where a callee’s arithmetic depends on its caller, would need either two compilations of every function or a run-time mode flag on every operation, and would make a function’s failure behavior depend on code it cannot see. Comprehensions and compound assignment are lowered inline, so they are checked when written in the block.

Control flow

?, return, break and continue keep their meaning: the block catches only arithmetic failures. A failure exits like ?: it releases the locals the block declared and the temporaries on the operand stack, then produces Err. There is no rollback. An outer variable assigned before the failure keeps the new value, and a compound assignment whose operation failed does not write. Rollback would need copies of every mutated value and would hide allocation.

Settled questions

QuestionDecisionReason
Where a block may appearBinding or assignment value, after return, or a function’s or case’s final expressionThe positions where a match block produces a value; no new expression grammar
checked as a keywordContextual: checked followed by : and a newlineExisting names such as checked: i64 = 0 keep working
NestingThe innermost block catchesEach block’s error type stays local and predictable
A block with no failing operationCompile-time errorResult with an empty error set would be misleading, and the block is likely misplaced
Every path exitsCompile-time errorThe block has no value
Discarded blockRejected as an unused ResultSame rule as any Result
Reference resultRejectedLocals the block owns are released on failure
x += 1, items[i] += 1CheckedLexically arithmetic; the earlier rejection of compound assignment is not needed
@newtype operators (issue #555)Forwarded integer operators are checked and report OverflowError of the field typeForwarding is inline lowering of the field’s operator; user-defined hooks are calls and keep trapping
Constant operandsEvaluated at run time inside a block; module constants still reject overflow at compile timeA block’s purpose is to produce the Err value
Constant divisorError type unchanged; impossible branches omittedTypes follow operators, not values, so editing a constant does not change a signature
Shifts and castsUnchangedOut of scope; try_from covers casts
Negative pow exponentStill trapsIt has no integer result to wrap
Declared wider errorKept when it accepts every possible failureLets a block be a function body returning a larger error set

Code generation

Op::CheckedBlock holds the body and the result type; each checked operator is Op::CheckedArith, carrying the operation, operand type and cleanup for the block’s locals. Code generation computes the wrapped result and an overflow flag with Cranelift’s *_overflow instructions, tests the divisor for zero and the signed MIN // -1 case, and branches to a cold block that builds the error, releases stack values above the block’s base, runs the cleanup and jumps to the block’s exit. The success path wraps the value in Ok. A flag known to be zero, such as the zero test for a nonzero constant divisor, emits no branch.

Integer pow in a block calls plenty_int_pow_checked, which returns the wrapped result and an overflow flag. Float div_checked is a one-operation block.

Programs without checked: produce the same objects as before; the comparison is recorded in the PR for issue #557.

Follow-up

  • Class-destructuring patterns for OverflowError landed with issue #558: case Err(OverflowError(wrapped)); see class patterns.
  • Checked shifts and casts, if a use case needs them.