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

Calculate with numbers

Arithmetic requires matching types. Integer failures are recoverable by default: list their error types in the function’s Result return, and use Ok(...) for success. Operators themselves still produce ordinary numbers.

def main() -> Result[(), IoError]:
    small = 7u8
    print(small + 1i64)?
    Ok(())
cannot combine u8 with i64; convert with `i64(small)`

Choose a matching literal or explicitly convert a value:

def main() -> Result[(), IoError | OverflowError[u8] | OverflowError[i64]]:
    small = 7u8
    print(small + 1u8)?
    print(i64(small) + 1)?
    Ok(())
8
8

i64(small) is safe because every u8 value fits in an i64. When the target cannot hold every value of the source, use try_from. It checks the range and returns a Result: Ok with the converted value, or Err(RangeError.OutOfRange) when the value does not fit. RangeError has only this one variant; parsing text uses the separate ParseError type.

def main() -> Result[(), RangeError | IoError | OverflowError[u8]]:
    wide = 200
    small = u8.try_from(wide)?
    print(small + 1u8)?
    Ok(())
201

? takes the converted value or returns its error to the caller. The RangeError | IoError error type preserves either a conversion error or a printing error. Absence and failure explains propagation and how to recover from an error.

A cast to an integer type requires the value to fit. A literal that does not fit is rejected when the program is compiled:

def main() -> ():
    small = u8(300)
300 is out of range for u8; u8.wrapping_from keeps the low bits

A value known only when the program runs is checked then. With wide = 300, u8(wide) stops the program with error: integer cast out of range. Casts retain this trapping behavior; arithmetic errors use typed returns. The same applies to a negative value cast to an unsigned type, such as u64(-1), and to an unsigned value above the signed maximum, such as i8(200u8). Cast when the value is known to fit, and use try_from when it might not.

When discarding the high bits is the intent, say so with wrapping_from, which never fails:

def main() -> Result[(), IoError]:
    wide = 300
    negative = -1
    print(u8.wrapping_from(wide))?
    print(u64.wrapping_from(negative))?
    print(i8.wrapping_from(200u8))?
    Ok(())
44
18446744073709551615
-56

Integer overflow returns OverflowError[T], where T is the operand type. An unsigned type has no negative values, so unary minus requires a signed integer or a float:

def main() -> Result[(), IoError]:
    small = 7u8
    print(-small)?
    Ok(())
unary minus is not defined for unsigned type u8

Convert first: -i16(small) is -7.

The same rule applies to literals, including zero:

def main() -> ():
    zero: u8 = -0
unary minus is not defined for unsigned type u8

+, -, and * have their usual arithmetic precedence. // divides integers and rounds down, including for negative values:

def main() -> Result[(), IoError | OverflowError[i64] | ZeroDivisionError]:
    print(1 + 2 * 3)?
    print((1 + 2) * 3)?
    print(-7 // 3)?
    Ok(())
7
9
-3

Integer division by zero returns ZeroDivisionError. Signed division also requires OverflowError[T] for T.MIN // -1. The contract follows the operators and operand types, even with a known nonzero divisor such as 3. Use / for floating-point division.

Integers also support bitwise operations and shifts. A shift count must be nonnegative and smaller than the integer width. Signed right shifts preserve the sign; unsigned right shifts fill with zero. Left shifts discard high bits.

def main() -> Result[(), IoError]:
    bits = 5u8
    print(bits & 3)?
    print(bits | 2)?
    print(bits ^ 3)?
    print(~bits)?
    print(bits << 1)?
    print(bits >> 1)?
    print(255u8.wrapping_add(1))?
    print(0u8.wrapping_sub(1))?
    print(128u8.wrapping_mul(2))?
    Ok(())
1
7
6
250
10
2
0
255
0

Use wrapping_add, wrapping_sub, or wrapping_mul when a calculation deliberately wraps at the integer width, as in a hash or a random-number generator. Ordinary arithmetic returns a typed overflow error. Parenthesize mixed bitwise expressions when that makes the intended grouping easier to read.

rotate_left and rotate_right move bits around the ends of an integer instead of discarding them. The count has the integer’s own type. No count loses bits, so rotation never traps: a count of the full width leaves the value unchanged.

def main() -> Result[(), IoError]:
    bits = 129u8
    print(bits.rotate_left(1))?
    print(bits.rotate_right(1))?
    print(bits.rotate_left(8))?
    print(bits << 1)?
    Ok(())
3
192
129
2

pow raises a number to a power, and each integer type names its limits as MIN and MAX. As with the operators, the number and its exponent must have one type, and the result has that type:

def main() -> Result[(), IoError | OverflowError[i64] | OverflowError[u8]]:
    change = -7
    small = 200u8
    print(change.pow(2))?
    print((2).pow(10))?
    print(u8.MAX - small)?
    print(i64.MIN)?
    Ok(())
49
1024
55
-9223372036854775808

There is no ** operator. An integer literal receiver needs parentheses, as in (2).pow(10).

A result that does not fit returns the same typed overflow error as the operators. An integer power with a negative exponent still stops with negative integer exponent; convert to a float for that.

Recover from overflow

Declare the errors a calculation can produce and propagate them to its caller. An arithmetic failure returns Err immediately; otherwise explicitly return Ok with the successful value.

def average(total: u8, count: u8) -> Result[u8, OverflowError[u8] | ZeroDivisionError]:
    rounded = total + count // 2
    Ok(rounded // count)

def main() -> Result[(), OverflowError[u8] | ZeroDivisionError | IoError]:
    print(average(200, 4)?)?
    print(average(250, 20).is_err())?
    print(average(250, 0).is_err())?
    Ok(())
50
True
True

The error type lists what the function’s operators can produce: OverflowError[u8] for + on u8, and ZeroDivisionError for //. Unsigned // cannot overflow, so it adds only ZeroDivisionError. ? passes either error to the caller, as with try_from. An OverflowError holds the wrapped result in its wrapped field: 250 + 10 on a u8 wraps to 4. Keep several error types shows how to handle each error instead of passing it on.

For local recovery, use checked:. Its final expression becomes the successful value of the block’s Result; no explicit Ok is needed there:

def increment_or_zero(value: i64) -> i64:
    result = checked:
        value + 1
    match result:
        case Ok(incremented):
            incremented
        case Err(_):
            0

def main() -> Result[(), IoError]:
    print(increment_or_zero(4), increment_or_zero(i64.MAX))?
    Ok(())
5 0

Every named function and closure keeps its own declared error contract, including when defined inside checked:. Calls returning Result still need explicit handling.

Use unchecked: only where you can show the arithmetic cannot fail. It yields an ordinary value, not a Result. A failure inside it terminates the process by default, but --unchecked assume builds omit that check, so do not use the suite to reject bad input. In assume builds, list, slice-view and Bytes indexing inside the suite also requires nonnegative, in-range indices; normal negative indexing is unavailable there. Keep these suites small and justify every indexed access as well as the arithmetic. Write unchecked trap: when termination is the intent. See arithmetic modes. Integer std.math helpers return ordinary overflow results; their float variants remain infallible. Remaining std implementations and parallel reductions still need the coordinated adoption described in B14. Calls never inherit a caller’s mode.

Validate a divisor once

When a divisor comes from input, validate it with NonZero.try_from and pass the resulting value to repeated divisions. A plain integer accepts that divisor directly; the compiler omits its zero check.

def per_group(total: i64, groups: NonZero[i64]) -> Result[i64, OverflowError[i64]]:
    Ok(total // groups)

def main() -> Result[(), IoError | RangeError | OverflowError[i64]]:
    count = 3
    groups = NonZero.try_from(count)?
    print(per_group(14, groups)?, 14 % groups)?
    Ok(())
4 2

The quotient and remainder are ordinary integers and can be zero. Signed i64.MIN // -1 can still overflow, so the function’s error type includes OverflowError[i64]. A locally handled checked block can instead keep this error out of the function signature. See nonzero integers for construction and arithmetic rules.