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
f32is refused at anf32destination (INCAN-T0001). At afloatdestination, a float literal may be infinite (1e309), and so may anf64-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
Optionor union destination that holds one numeric type other thanint, an integer literal takes that type:1is the float1.0at anOption[float]orfloat | strdestination. At a destination that holdsint, or two other numeric types, it is anint. - 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)andpick(1, 2.5)bindTtofloat, and1is1.0. - An
intvalue 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 precisionpfrom 1 to 38 and a scalesfrom 0 top. Any other argument list is refused (INCAN-T0001).- Bare
decimaland barenumericare 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 mostsfractional digits, and its integer part has at mostp - sdigits (an integer part of0has none). A literal outside these limits is refused (INCAN-T0001). A literal with fewer thansfractional digits is accepted. - An
intvalue 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.5dequals1.50d, and1.49dis less than1.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.5ddisplays as1.5and1.50das1.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:
floatisf64andintisi64, so each is assignable to the other. i128,u128,isizeandusizeare 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
Optionpayload and to the first numeric member of a union it widens to, and a lookup probe (in, adictkey,count,index,contains,get) widens to the element or key type. - Inside a collection, tuple,
Optionor function type, a numeric type matches only its own type:list[i8]is not assignable tolist[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 namingresize()andtry_resize().intandi64are 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 (
i16fori8andu8,i128fori64andu64). A pair no integer type holds,u128with a signed type orisizeorusizewith 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, anda == (a // b) * b + (a % b)./,//and%with a zero divisor raiseZeroDivisionError, 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, raisesValueError. The minimum of a signed integer type% -1is0. **binds tighter than a prefix-or~on its left and looser than one on its right:-x ** 2is-(x ** 2),~x ** 2is~(x ** 2), and2 ** -1is2 ** (-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, withopone of+,-,*,/,//,%,&,|,^,<<and>>, reassignsx, which is amutbinding or astaticof its module (INCAN-T0001otherwise).**=is not an operator (INCAN-P0001).- A
mutparameter that is marked (seemutparameters), such asmut n: i8, is not reassigned:n += 1is refused (INCAN-T0001). Amutparameter of typeint,floatorboolis reassigned. target.field op= ywritestarget.field op yto the field, andtarget[index] op= ywritestarget[index] op yto the element. Inside a method, a target reached throughselfrequires amut selfreceiver (INCAN-T0102).x op= yhas the assignability requirement ofx = x op y: the result ofx op y(see Operators) is assignable to the type ofx, or the statement is refused (INCAN-T0001).- The same requirement holds for a field or element target: the result of
target op yis assignable to the field's or element's type (INCAN-T0001). x @= ywith a numericxis 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
f32value is finite. A non-finite value that reaches anf32at run time, from a Rust value or as the result of an operation, raisesValueError.