Skip to content

Numeric semantics (reference)

This page specifies the numeric types and their aliases, numeric literals, decimal types, assignment between numeric types, resizing methods, numeric arguments to Rust functions, numeric operators, and compound assignment.

Types

Type Family Rust representation Values
i8 Signed integer i8 -128 to 127
i16 Signed integer i16 -32,768 to 32,767
i32 Signed integer i32 -2,147,483,648 to 2,147,483,647
i64 Signed integer i64 -9,223,372,036,854,775,808 to 9,223,372,036,854,775,807
i128 Signed integer i128 -2^127 to 2^127 - 1
isize Signed integer isize The platform's pointer-sized signed range
u8 Unsigned integer u8 0 to 255
u16 Unsigned integer u16 0 to 65,535
u32 Unsigned integer u32 0 to 4,294,967,295
u64 Unsigned integer u64 0 to 18,446,744,073,709,551,615
u128 Unsigned integer u128 0 to 2^128 - 1
usize Unsigned integer usize The platform's pointer-sized unsigned range
f32 Binary float f32 Finite IEEE 754 binary32 values
f64 Binary float f64 IEEE 754 binary64 values, NaN and infinity included
decimal[p, s] Decimal incan_std_core::num::Decimal128 Base-10 values with precision p and scale s (see Decimal types)

Aliases

An alias is the same type as its canonical type.

Alias Canonical type
byte u8
short, smallint i16
integer i32
int, bigint, long i64
hugeint i128
real, fp32 f32
float, double, fp64 f64
numeric[p, s], decimal128[p, s] decimal[p, s]
def main() -> None:
    x: integer = 10
    y: i32 = x                                   # accepted
    z: i16 = x                                   # refused: i32 is not assignable to i16
    ratio: float = 0.5
    exact: f64 = ratio                           # accepted
    back: double = exact                         # accepted

Literals

Literal Type without a destination Type at a destination
42, -42, 1_000_000 int The numeric type the destination expects. At a float or f32 destination, the float of the same value.
0.5, 1e3 float The binary float type the destination expects: f32 or float.
42u16, 7i8, 3.14f32 The type its suffix names The type its suffix names.
19.99d, 12345d A decimal literal requires a decimal destination The destination's decimal[p, s] type (see Decimal types).
  • An integer literal outside the range of its integer type is refused (INCAN-T0001), as a declared value, an argument and a returned value alike. A suffixed literal outside the range of its suffix's type is refused (INCAN-T0001).
  • A literal whose value is not finite in f32 is refused at an f32 destination (INCAN-T0001). At a float destination, a float literal may be infinite (1e309), and so may an f64-suffixed literal (1e309f64).
  • Numeric literals are written in decimal digits. A float literal has a . followed by a digit (0.5), an exponent (1e3), or both.
  • _ separators do not change a literal's value.
  • A negated integer literal takes its destination's type as the literal does.
  • At an Option or union destination that holds one numeric type other than int, an integer literal takes that type: 1 is the float 1.0 at an Option[float] or float | str destination. At a destination that holds int, or two other numeric types, it is an int.
  • An integer literal argument of a generic function takes its parameter's type in the call: with def pick[T](a: T, b: T) -> T, pick(2.5, 1) and pick(1, 2.5) bind T to float, and 1 is 1.0.
  • An int value is not assignable to a float type (see Assignment between numeric types).
  • A float literal at an integer destination is refused (INCAN-T0001).
  • An unsuffixed integer literal operand of +, -, *, //, %, &, | or ^ takes the exact-width integer type of the other operand (see Operators).
def main() -> None:
    scale: float = 2                             # accepted
    mut total: float = 0.5
    total = -1                                   # accepted: -1.0
    pair: tuple[float, int] = (3, 4)             # accepted: (3.0, 4)
    weights: dict[str, float] = {"a": 1}         # accepted: {"a": 1.0}
    maybe: Option[float] = 1                     # accepted
    ok: u8 = 255                                 # accepted
    bad: u8 = 256                                # refused: 256 is outside u8
    ratio: f32 = 1e39                            # refused: not finite in f32
    count = 5
    total = count                                # refused: count is an int value

Decimal types

  • decimal[p, s] takes exactly two integer arguments: a precision p from 1 to 38 and a scale s from 0 to p. Any other argument list is refused (INCAN-T0001).
  • Bare decimal and bare numeric are not types (INCAN-T0001).
  • A decimal literal is a plain decimal number followed by a lowercase d. A literal in exponent notation (1e2d) is refused (INCAN-T0001).
  • A decimal literal at a decimal[p, s] destination has at most s fractional digits, and its integer part has at most p - s digits (an integer part of 0 has none). A literal outside these limits is refused (INCAN-T0001). A literal with fewer than s fractional digits is accepted.
  • An int value is not assignable to a decimal type (INCAN-T0001).
  • A decimal value is assignable to a decimal type with at least its scale and at least its number of integer digits (p - s). A conversion that loses precision or scale is not implicit.
  • The language defines no arithmetic on decimal values: an arithmetic operator with a decimal operand, unary - included, is refused (INCAN-T0001).
  • Decimal values compare by value with ==, !=, <, <=, > and >= against decimal values of any precision and scale: 1.5d equals 1.50d, and 1.49d is less than 1.5d. Equal decimal values are the same set element and the same dict key.
  • A decimal value displays the fractional digits it was written with, whatever the scale of its type: 1.5d displays as 1.5 and 1.50d as 1.50.
def main() -> None:
    price: decimal[10, 2] = 19.99d               # accepted
    amount: numeric[12, 4] = 1000.2500d          # accepted
    whole: decimal[5, 0] = 12345d                # accepted
    wider: decimal[12, 4] = price                # accepted
    narrower: decimal[5, 2] = price              # refused: 3 integer digits, not 8
    too_precise: decimal[10, 2] = 1.234d         # refused: 3 fractional digits
    too_large: decimal[7, 2] = 123456.78d        # refused: 6 integer digits
    missing_shape: decimal = 1.00d               # refused: bare decimal
    total = price + price                        # refused: no decimal arithmetic
    cheaper = amount < price                     # accepted

Assignment between numeric types

A value of one numeric type is assignable to another numeric type only as the table lists; every other assignment between different numeric types is refused (INCAN-T0001). Arguments and returned values follow the same rule.

Source Assignable to
i8 i16, i32, i64, i128
i16 i32, i64, i128
i32 i64, i128
i64 i128
u8 u16, u32, u64, u128, i16, i32, i64, i128
u16 u32, u64, u128, i32, i64, i128
u32 u64, u128, i64, i128
u64 u128, i128
f32 f64
  • A type spelled with an alias is its canonical type: float is f64 and int is i64, so each is assignable to the other.
  • i128, u128, isize and usize are assignable to no other numeric type.
  • No integer type is assignable to a float type, and no float type to an integer type.
  • The table applies to a value. A value also widens to the numeric type of an Option payload and to the first numeric member of a union it widens to, and a lookup probe (in, a dict key, count, index, contains, get) widens to the element or key type.
  • Inside a collection, tuple, Option or function type, a numeric type matches only its own type: list[i8] is not assignable to list[int] (INCAN-T0001).
def main() -> None:
    small: i8 = 120
    wide: int = small                            # accepted
    huge: i128 = wide                            # accepted
    bits: u8 = 200
    more_bits: u16 = bits                        # accepted
    single: f32 = 1.25
    double: float = single                       # accepted
    exact: f64 = double                          # accepted
    maybe: Option[int] = small                   # accepted
    back: i8 = wide                              # refused: int is not assignable to i8
    narrow_values: list[i8] = [1, 2]
    wide_values: list[int] = narrow_values       # refused: list[i8] is not list[int]

Resizing methods

A resizing method converts a numeric value to its destination type T. The destination may come from the expected type of the call, such as a binding's declared type. try_resize[T](), wrapping_resize[T]() and saturating_resize[T]() may instead name it explicitly. When both forms supply a destination, they must name the same type (try_resize compares T with the payload of the expected Option[T]).

Method Destination Receiver and T Result
resize() T The receiver's type is T or assignable to T The same value
try_resize() or try_resize[T]() Option[T] Integer types Some(value) when T holds the value, otherwise None
wrapping_resize() or wrapping_resize[T]() T Integer types The value modulo 2^N, in the range of T, where N is the bit width of T
saturating_resize() or saturating_resize[T]() T Integer types The value clamped to the minimum and maximum of T
  • A call without a contextual or explicit destination type, with value arguments, with more than one type argument, or outside the table's receiver and destination types is refused (INCAN-T0001). resize() does not accept an explicit type argument.
def main() -> None:
    small: i8 = 120
    wide: int = small.resize()                   # accepted
    incoming: i16 = 240
    maybe: Option[i8] = incoming.try_resize()    # accepted
    wrapped = incoming.wrapping_resize[i8]()     # accepted: explicit destination
    capped: i8 = incoming.saturating_resize[i8]() # accepted: matching context and explicit destination
    conflict: u8 = incoming.saturating_resize[i8]() # refused: conflicting destinations
    narrow: i8 = wide.resize()                   # refused: int is not assignable to i8
    loose = small.resize()                       # refused: no destination type

Integer overflow helpers

All signed and unsigned integer types, including int, provide the following helpers. T is the receiver's type. The operand to add, sub and mul must also be T; mixed-width values require an explicit resize. A pow exponent is u32, matching the underlying integer operation. Float receivers are refused (INCAN-T0001).

Policy Methods Result
Checked checked_add(other: T), checked_sub(other: T), checked_mul(other: T), checked_pow(exponent: u32) Option[T]: Some(result) when representable, otherwise None
Wrapping wrapping_add(other: T), wrapping_sub(other: T), wrapping_mul(other: T), wrapping_pow(exponent: u32) T, wrapped at the bounds of T
Saturating saturating_add(other: T), saturating_sub(other: T), saturating_mul(other: T), saturating_pow(exponent: u32) T, clamped at the bounds of T

Ordinary sized-integer arithmetic traps on overflow in debug builds and wraps in release builds. The helper methods select checked, wrapping or saturating behavior explicitly in every build profile.

def main() -> None:
    maximum: u8 = 255
    maybe: Option[u8] = maximum.checked_add(1u8)
    wrapped: u8 = maximum.wrapping_add(1u8)
    clamped: u8 = maximum.saturating_add(1u8)
    power: u8 = 3u8.saturating_pow(6u32)

Rust interop numeric arguments

A numeric argument to a Rust parameter of a primitive numeric type is accepted when the types match or when the Incan type widens without loss: a signed type to a signed type at least as wide, an unsigned type to an unsigned type at least as wide or to a wider signed type, and f32 to f64. int matches i64 and float matches f64. isize and usize match only themselves. Every other pairing is refused (INCAN-T0001).

Incan argument Rust parameter Accepted
i32 i32 Yes
i32 i64 Yes
int i32 No
u8 i16 Yes
i16 u16 No
f32 f64 Yes
float, f64 f32 No

Operators

Operator Operands Result
+, -, * Two operands of one integer type That integer type
Two f32 f32
Any other pair with a float operand float
/ Two f32 f32
Any other numeric pair float
//, % Two operands of one integer type That integer type
Two f32 f32
Any other pair with a float operand float
** An integer left operand and a non-negative integer literal exponent The left operand's type
Two f32 f32
Any other numeric pair float
&, |, ^ Two operands of one integer type That integer type
<<, >> Two integer operands The left operand's type
==, !=, <, <=, >, >= Two numeric operands bool
  • +, -, *, //, %, &, | and ^ between two different integer types are refused (INCAN-T0001), with a hint naming resize() and try_resize(). int and i64 are one type.
  • An unsuffixed integer literal operand of +, -, *, //, %, &, | or ^ takes the type of an exact-width integer operand on the other side. A literal outside that type's range is refused (INCAN-T0001).
  • Two different integer types compare in the narrowest integer type holding both: the wider of the two, or the narrowest signed type holding both (i16 for i8 and u8, i128 for i64 and u64). A pair no integer type holds, u128 with a signed type or isize or usize with another integer type, is refused (INCAN-T0001). An unsuffixed integer literal operand of a comparison takes the other operand's integer type when it holds the literal's value.
  • A prefix - keeps the type of its signed integer or float operand. A prefix - on an unsigned integer operand is refused (INCAN-T0001).
  • A prefix ~ keeps the type of its integer operand.
  • &, |, ^, << and >> with a float operand are refused (INCAN-T0001).
  • / is true division. // rounds the quotient toward negative infinity. The result of % has the sign of the divisor, and a == (a // b) * b + (a % b).
  • /, // and % with a zero divisor raise ZeroDivisionError, for signed integer, unsigned integer and float operands alike.
  • A signed integer // whose quotient is outside its type's range, the type's minimum divided by -1, raises ValueError. The minimum of a signed integer type % -1 is 0.
  • ** binds tighter than a prefix - or ~ on its left and looser than one on its right: -x ** 2 is -(x ** 2), ~x ** 2 is ~(x ** 2), and 2 ** -1 is 2 ** (-1). The operator table gives the precedence of every operator.
  • +, - and * between two values of one type parameter are accepted. /, //, % and ** between two values of one type parameter are refused (INCAN-T0109), unless the parameter is bounded by a trait that defines the operator's method (__div__, __floordiv__, __mod__ or __pow__); the operator then resolves through that trait.
trait Remainder:
    def __mod__(self, other: Self) -> Self: ...

def modulo[T with Remainder](a: T, b: T) -> T:
    return a % b                                 # accepted

def divide[T](a: T, b: T) -> T:
    return a / b                                 # refused: INCAN-T0109

def main() -> None:
    half = 1 / 2                                 # accepted
    floor = -7 // 2                              # accepted
    rest = -7 % 3                                # accepted
    cube = 2 ** 3                                # accepted
    inverse = 2 ** -1                            # accepted
    exp = 3
    power: int = 2 ** exp                        # refused: 2 ** exp is a float
    size: u16 = 10
    part: u16 = size % 3                         # accepted
    other: u8 = 3
    mixed = size % other                         # refused: u16 and u8
    small: i8 = 10
    bumped: i8 = small + 1                       # accepted
    square: i8 = small ** 2                      # accepted
    over = small + 300                           # refused: 300 is outside i8
    count: int = 2
    scaled = small * count                       # refused: i8 and int
    flipped = -size                              # refused: u16 is unsigned
    wide: i64 = 5
    total: int = count + wide                    # accepted
    smaller = small < count                      # accepted
    huge: u128 = 1
    unsure = huge == count                       # refused: no integer type holds u128 and int
    mask: u8 = 12
    low: u8 = mask & 3                           # accepted
    moved: u8 = mask << count                    # accepted
    flipped: u8 = ~mask                          # accepted
    either = mask | size                         # refused: u8 and u16

Compound assignment

This section states compound assignment with numeric operands.

  • x op= y, with op one of +, -, *, /, //, %, &, |, ^, << and >>, reassigns x, which is a mut binding or a static of its module (INCAN-T0001 otherwise). **= is not an operator (INCAN-P0001).
  • A mut parameter that is marked (see mut parameters), such as mut n: i8, is not reassigned: n += 1 is refused (INCAN-T0001). A mut parameter of type int, float or bool is reassigned.
  • target.field op= y writes target.field op y to the field, and target[index] op= y writes target[index] op y to the element. Inside a method, a target reached through self requires a mut self receiver (INCAN-T0102).
  • x op= y has the assignability requirement of x = x op y: the result of x op y (see Operators) is assignable to the type of x, or the statement is refused (INCAN-T0001).
  • The same requirement holds for a field or element target: the result of target op y is assignable to the field's or element's type (INCAN-T0001).
  • x @= y with a numeric x is refused (INCAN-T0001).
model Counter:
    n: i8

    def bump(mut self) -> None:
        self.n += 1                              # accepted

def shrink(mut n: i8) -> i8:
    n -= 1                                       # refused: n is a marked mut parameter
    return n

def main() -> None:
    mut x: int = 10
    x += 2                                       # accepted
    x /= 2                                       # refused: x / 2 is a float
    mut y: float = 10.0
    y /= 2                                       # accepted
    y %= 7                                       # accepted
    mut n: i8 = 10
    n += 1                                       # accepted: n + 1 is an i8
    step: int = 1
    n += step                                    # refused: i8 and int
    mut s: f32 = 1.5
    s *= s                                       # accepted
    s *= 2.0                                     # refused: s * 2.0 is a float
    mut b: u8 = 7
    b //= 2                                      # accepted
    b |= 1                                       # accepted: b | 1 is a u8
    mut counter = Counter(n=1)
    counter.n += 1                               # accepted
    mut values: list[int] = [4, 8]
    values[0] += 1                               # accepted
    values[1] /= 2                               # refused: values[1] / 2 is a float
    x @= 2                                       # refused: x is numeric

NaN and infinity

  • A float (f64) value may be NaN or infinite.
  • An f32 value is finite. A non-finite value that reaches an f32 at run time, from a Rust value or as the result of an operation, raises ValueError.

See also