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

Numeric types and literals

For a fixed inclusive integer interval, use Bounded[T, lo, hi].

Unsuffixed integers default to i64; decimal or exponent literals default to f64. Width suffixes include 7u8, 1f32, 1.5f32, and 1e-3f64. Decimal forms such as .5 and 1. are accepted. Literals must fit their selected width.

Integer literals may also be hexadecimal (0x1F1E6), octal (0o755), or binary (0b1010). The prefix letter and hexadecimal digits may be either case. These forms are always integers: they take the integer suffixes and range checks of decimal literals, as in 0xFFu8 and -0x80i8, and have no fraction, exponent, or float suffix, so 0x1f32 is the integer 7986. Any literal may separate digits with single underscores, as in 1_000_000 and 0xffff_ffff; an underscore cannot lead, trail, repeat, or follow the prefix.

SourceDiagnostic
0x`0x` needs at least one hexadecimal digit
0b102invalid digit `2` in a binary literal
0b1f32invalid digit `f` in a binary literal
0x1u7invalid integer suffix
0x_FFinvalid integer separator
0x1FFu8integer literal out of range for u8

Annotations, parameters, returns, and typed arithmetic operands guide unsuffixed literals within an expression. Already typed values and suffixed literals must match exactly; no numeric widening occurs. Operands of different numeric types are rejected with the cast that reconciles them, for example cannot combine i64 with f64; convert with `f64(n)` . The suggested cast never fails: an unsigned and a signed integer convert to the narrowest signed type wider than the unsigned one, both operands if neither has that type (i8 with u16 suggests i32 for each). u64 with a signed type has no such type, so the message names no cast. Ordinary generic calls have argument inference rules that do not infer one argument’s literal width from another.

Explicit casts use numeric type names, for example u8(200). A cast to an integer type requires the target to represent the value. An integer keeps its mathematical value; one outside the target’s range traps, unlike the default recoverable integer arithmetic errors. This covers narrowing, a negative value cast to an unsigned type, and an unsigned value above a signed type’s maximum. A float is truncated toward zero first: u8(255.9) is 255 and u8(-0.9) is 0. A float whose truncated value is outside the target’s range, an infinity, or NaN is the same runtime error. The message is integer cast out of range.

A cast whose operand is a numeric literal is checked at compile time, so u8(300) and u8(300.7) are compile-time errors. A cast between integer types where the target represents every source value, such as i64(small_u8), emits no check. A failing check calls the runtime; a passing one does not, and no cast allocates. A cast whose operand compiles to a constant, such as i64(u16.MAX) or u8(i64(200)) in a function body, emits the converted constant with no conversion or check, as the literal 65535 would. Such an operand outside the target’s range, as in u8(i64(300)), is still the runtime error.

Casts to a float type never fail. Integer-to-float and narrowing float casts may round, and an f64 beyond f32’s range becomes an infinity. Widening f32 to f64 is exact.

Two other forms convert between integer types. T.try_from(value) returns a Result instead of stopping the program. T.wrapping_from(value) keeps the low bits and never fails.

All three conversions take a number, not a reference. Borrowed iteration such as for b in &buffer yields &u8, so i64(b) is a compile-time error that names the fix: numeric casts require a number, not a reference; read it with `*b` . A borrow written in the operand, as in i64(&n), is reported with remove the `&` instead.

Unary minus requires a signed integer or a float; an unsigned operand is a compile-time error, including a sign directly before a literal. Both -3u8 and -0u8 report unsigned negation, as does value: u8 = -0 when the literal gets its type from context. Signed minimum literals such as -128i8 remain valid, and floating-point negative zero is preserved.

Integer arithmetic failures return Err from the enclosing function by default. Its declared Result error type must include the possible errors; successful arithmetic still produces ordinary integers. Use an explicit checked: block for local recovery, or unchecked: where the programmer guarantees that the arithmetic cannot fail. // floors signed quotients, including negative operands; % is integer-only. / accepts matching-width floats. Float arithmetic follows IEEE behavior for rounding, signed zero, infinities, NaN, and underflow; arithmetic overflow may yield infinity.

Integers support bitwise &, |, ^, and unary ~, and shifts << and >>. Operands must have the same integer type; a typed left operand guides an unsuffixed count literal. Counts must be nonnegative and less than the integer width; invalid counts trap before shifting. Left shifts discard high bits. Signed right shifts extend the sign bit; unsigned right shifts fill with zero. From lower to higher precedence: |, ^, &, shifts, addition/subtraction, multiplication/division/modulo, unary operators. Comparisons bind below |. Prefix & and &mut still create borrows.

value.wrapping_add(other), wrapping_sub, and wrapping_mul require matching integer types and compute modulo two to the integer width. They allocate nothing. Ordinary +, -, and * keep their overflow checks.

value.rotate_left(count) and value.rotate_right(count) rotate the bits of an integer: bits leaving one end enter at the other, so none are lost. The count has the receiver’s type, and a typed receiver guides an unsuffixed count literal. Because no count loses bits, rotation never traps: the count is reduced modulo the integer width, so rotating by the width is the identity and a negative signed count rotates the other way. Signed values rotate their two’s-complement bit pattern. Rotation allocates nothing. These built-ins are selected for integer receivers; classes may define methods with the same names and their own signatures.

The numeric facilities provide the type constants T.MIN, T.MAX, T.PI, and T.E; pow on every numeric type; and the float methods sqrt, exp, log, sin, cos, floor, ceil, and fmod. A two-operand method requires one type, as the operators do, and an integer result outside its type is the same overflow error.

Comparisons require matching types; ordering accepts integers, floats, strings, and tuples of those. For scalar floats, NaN is unequal to every value, including itself, and all ordered comparisons with it are false. These rules also apply to structural equality and membership. Floats cannot be dictionary keys or set elements.

Function arithmetic contracts

def scale(value: i64, factor: i64) -> Result[i64, OverflowError[i64]]:
    Ok(value * factor)

def main() -> Result[(), IoError]:
    print(scale(3, 4).unwrap(), scale(i64.MAX, 2).is_err())?
    Ok(())
12 True

Each named or anonymous function has its own boundary, independent of its definition or call site. The declared error type must contain every escaping arithmetic error; a plain or Option return cannot implicitly propagate them. Explicit Ok, Err, returns and ? keep their ordinary meaning. Operators do not produce individual Result wrappers or implicitly unwrap calls.

The operation/error table under checked blocks also applies to the function default. Error contracts follow operators and operand types, not range analysis. Locally handled errors do not have to appear in the function’s error type. Generators cannot propagate function-level arithmetic errors: handle a local checked result or explicitly opt out.

Coordinated repository adoption under B14 remains in progress. Public contracts are explicit: do not assume that an arbitrary callback consumer accepts a Result-returning callback.

Unchecked blocks

unchecked: states that integer arithmetic written in its lexical suite does not overflow or divide by zero; that is the programmer’s precondition, not the compiler’s. It yields the last expression’s ordinary type, without wrapping or unwrapping a Result. It is also allowed as a statement suite, including an empty pass suite and suites whose paths exit. Locals remain block scoped and reference-valued suites are not supported.

def scale_in_bounds(value: i64, factor: i64) -> i64:
    unchecked:
        value * factor

The --unchecked MODE build setting selects what a plain unchecked: suite does with its preconditions. unchecked trap: always checks them:

Suite--unchecked trap (default)--unchecked assume
unchecked:Check; trap on a violationOmit the checks; a violation is undefined behavior
unchecked trap:Check; trap on a violationCheck; trap on a violation

A trap terminates the process; it never wraps or reaches an outer checked boundary. In assume mode, overflow is not wrapping: a violation may produce a silently wrong result or worse, and no crash is guaranteed. Use explicit wrapping operations for modular arithmetic, and unchecked trap: or ordinary validation when termination on bad input is part of the contract. The default does not change with optimization or profile.

def checked_index(row: i64, width: i64, column: i64) -> i64:
    unchecked trap:
        row * width + column

The preconditions are representable results of +, -, *, negation and pow, nonzero divisors and representable quotients for // and %, valid shift counts, and in-range integer conversions. i64.MIN % -1 stays 0, and a negative pow exponent still traps in either mode. Floating-point arithmetic, allocation and explicit wrapping, saturating and checked methods keep their contracts.

In assume mode, list, slice-view and Bytes indexing written inside a plain unchecked: suite also requires 0 <= index < len(collection). Reads, writes, augmented assignments and element borrows omit both bounds checks and negative-index normalization. A negative or out-of-range index is undefined behavior, even when the negative index would normally count from the end. Default/trap builds retain normal negative indexing and out-of-range traps. This does not change slices, collection methods such as swap, or indexing of strings, immutable bytes, dictionaries, tuples or ranges.

Modes apply lexically. Nested checked: restores local recoverability; a nested plain unchecked: follows the build setting, even inside unchecked trap:; leaving any suite restores the outer mode. Named functions and closures keep their own contracts wherever they are defined or called. Calls still return their declared results, and ?, return, break and continue keep their ordinary targets.

std always checks its unchecked: suites, so the setting cannot change library behavior before its preconditions are audited. The setting is part of the compile cache identity; there is no plenty.toml key yet. Cranelift has no no-overflow instruction flags, so assume mode removes checks but cannot give the backend further overflow facts.

unchecked and trap are contextual: only unchecked: or unchecked trap: followed by a newline opens a suite; unchecked: i64 = 0 remains a typed binding. Suites support binding, assignment, return, function-tail and match-arm-tail value positions.

Checked blocks

A checked: block turns the integer failures written inside it into a value. Its body is an indented suite whose last expression is the block’s value T. The block is an expression of type Result[T, E]:

def mean(total: i64, count: i64) -> Result[i64, OverflowError[i64] | ZeroDivisionError]:
    checked:
        total * 2 // count

If no operation fails, the block’s value is Ok(T). The first failing operation exits the block with Err, and the rest of the block does not run. E is the anonymous sum of the errors that the block’s operators can produce:

Operation inside the blockError
+, -, *, unary - and pow on TOverflowError[T]
// on a signed T with a T or NonZero[T] divisor (T.MIN // -1)OverflowError[T]
// and % with a plain integer divisor that may be zeroZeroDivisionError

OverflowError[T] has one field, wrapped: T, the exact result modulo 2N for an N-bit T. ZeroDivisionError has no fields. Both are builtin classes; their names cannot be redefined. T.MIN % -1 is 0, inside or outside a block. Shifts, casts, and float arithmetic are unchanged: they neither add an error nor exit the block. A negative pow exponent still traps, since it has no integer result. Float a.div_checked(b) returns Result[F, ZeroDivisionError], failing for either signed zero divisor; it behaves as a one-operation block and may appear anywhere.

A class pattern names each error and binds the wrapped result:

def halve_sum(a: i64, b: i64) -> i64:
    result = checked:
        (a + b) // 2
    match result:
        case Ok(half):
            half
        case Err(OverflowError(wrapped)):
            # A positive divisor rules out zero and MIN // -1.
            two: Positive[i64] = 2
            wrapped // two
        case Err(ZeroDivisionError()):
            0

The error type follows the operators, not their operand values. n // 4 inside a block still adds ZeroDivisionError and OverflowError[i64]; the code generator omits the branches a constant divisor rules out. A declared Result whose error includes every possible failure is kept, so a block can be the body of a function that returns Result[i64, OverflowError[i64] | ParseError].

The rule is lexical within one function. Compound assignment such as x += 1, comprehensions, and operators that @newtype classes forward to their field report the field’s arithmetic error types. The nearest explicit arithmetic mode controls failure routing.

Functions and closures defined inside the block keep their own declared contracts. There is no private callback error channel or checked-block lifetime restriction; ordinary ownership and borrow restrictions still apply. A callee’s Result requires explicit handling, and ? exits the function rather than being caught by the block.

Integer std.math reductions and serial pipeline sum() return public overflow results; float reductions stay infallible. sort_by_key accepts ordinary fallible key callbacks and returns their error. Other callback consumers require their own explicitly fallible contracts. Do not assume a call inherits local checking or that an infallible callback signature can accept recoverable arithmetic.

Control flow is otherwise unchanged. ? inside the block exits the function, and return, break and continue behave as they do outside it. A failure releases the values and locals the block owns, as ? does. It does not roll back assignments the block already made to outer variables or elements, and an assignment whose operation failed does not happen.

A block may appear wherever a match block can produce a value: as the value of a binding or assignment, after return, or as a function’s or case’s final expression. checked is not reserved: it starts a block only when : and a newline follow it, so checked: i64 = 0 still declares a variable. A block whose value is discarded is rejected like any unused Result. A block with no operation that can fail is a compile-time error, as is one whose every path exits. A block cannot produce a reference.

Module constants are evaluated at compile time, where overflow is a compile-time error. Arithmetic in a block is evaluated at run time even when its operands are constants, so checked: around i8.MAX + 1i8 produces Err(OverflowError[i8](wrapped=-128)).

Arithmetic uses plain values and a shared cold error exit at its nearest recoverable boundary. A trapping unchecked: operation uses an inline trap guard instead; assume mode emits the plain instruction. See the design note.

Nonzero integers

NonZero[T], for any integer type T, holds a T that is not zero. Where one is expected, so that the program cannot hold a zero, an integer literal converts at compile time, including a negative one; 0 is a compile-time error. Any other value is converted with NonZero.try_from(value), which returns Result[NonZero[T], RangeError] and fails with RangeError.OutOfRange for zero. T is the value’s type; NonZero[T].try_from(value) names it and requires a T. get() reads the integer back.

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

def main() -> Result[(), IoError | RangeError | OverflowError[i64]]:
    three: NonZero[i64] = 3
    print(per_group(12, three)?)?
    print(per_group(12, -4)?)?
    count = 5
    five = NonZero.try_from(count)?
    print(five * three, five + three, five > three)?
    match NonZero.try_from(count - 5):
        case Ok(n):
            print(n)?
        case Err(RangeError.OutOfRange):
            print("zero")?
    Ok(())
4
-3
15 8 True
zero

A value has the same representation as its integer and allocates nothing. The constructor is private: NonZero(3) is a compile-time error that names the two conversions. NonZero[T] copies, compares with ==, !=, <, <=, >, and >=, prints and formats as its integer, and is a valid dictionary key and set element.

Arithmetic keeps a result nonzero only where that follows from nonzero operands. Unary - and +, and * between two NonZero[T] values, return NonZero[T]; *= updates one. + and - between two values return T, since their result can be zero. Overflow returns Err at the nearest recoverable boundary, and violates the precondition of unchecked blocks, so a product cannot wrap to zero. Other operators between two NonZero values, including //, %, bitwise operators, and shifts, are compile-time errors that suggest .get(). An integer literal beside a NonZero[T] operand is a NonZero[T] too: step * 2 is a NonZero[i64], and step == 0 is a compile-time error. Operands must otherwise have the same type; mixing NonZero[T] with T is otherwise a type error whose hint shows try_from.

A plain T dividend accepts a NonZero[T] divisor: total // groups and total % groups return T, and //= and %= update a T destination. These forms omit the divide-by-zero check. Signed floor division still checks T.MIN // -1 for overflow; T.MIN % -1 returns zero. In a checked: block, signed division by NonZero[T] contributes only OverflowError[T]; modulo and unsigned division contribute no failures. A block containing only operations that cannot fail is rejected, as with other checked arithmetic.

A range step is NonZero[i64]. NonZero is a builtin name and cannot be redefined.

Positive and non-negative integers

Positive[T] holds an integer greater than zero; NonNegative[T] holds an integer greater than or equal to zero. T must be i8, i16, i32 or i64. For unsigned integers use NonZero[uN] or plain uN, respectively. Floats and other non-integer types are rejected without a replacement suggestion. Both are builtin names and cannot be redefined.

An unsuffixed integer literal converts where the domain type is expected, including parameters, defaults, fields, returns and collection elements. An out-of-domain literal is a compile-time error. A module constant may have a domain type; its value is checked the same way. A computed integer converts with Positive.try_from(value) or NonNegative.try_from(value), returning Result[Domain[T], RangeError]; invalid values give RangeError.OutOfRange. The qualified forms Positive[T].try_from and NonNegative[T].try_from require exactly T. Constructors and fields are private; .get() reads T. There is no implicit conversion between a domain and its integer or another domain.

def configured(value: i64) -> Result[Positive[i64], RangeError]:
    Positive.try_from(value)

def main() -> Result[(), IoError | RangeError | OverflowError[i64]]:
    size: Positive[i64] = 3
    limit: NonNegative[i64] = 0
    print(size + 2, size * 2, size - size, -size, limit)?
    stride = NonZero.from(size)
    print(list(range(0, 8, stride)).unwrap())?
    print(NonNegative.from(configured(4)?))?
    print(configured(0).is_err())?
    Ok(())
5 6 0 -3 0
[0, 3, 6]
4
True

+, *, += and *= preserve the domain; subtraction and unary - return T, and unary + preserves it. Overflow returns Err by default and violates the precondition of unchecked blocks, so the domain cannot be violated by wrapping. Comparisons accept the same domain type or a literal in its domain. Other operators between domain values, including division, modulo, bitwise operations and shifts, require .get().

A plain T dividend accepts a Positive[T] divisor directly with //, %, //= and %=. The result is T. These forms omit both the zero check and the signed MIN // -1 check, since a positive divisor can be neither zero nor -1. They contribute no failures inside a checked: block. A NonNegative[T] divisor still needs .get() or explicit conversion to a nonzero domain, since zero is valid for it.

NonZero.from(positive) and NonNegative.from(positive) explicitly widen a Positive[T]; they cannot fail or allocate. Qualified forms name T. Conversion from a NonNegative to a Positive or NonZero uses try_from(value.get()), since it may be zero.

Both domains have T’s representation, copy without allocation, print and format as the integer, and work as dictionary keys and set elements. len() continues to return i64; convert a computed length explicitly when a domain is needed.