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.