Primitive Types
Move is a statically typed language: every value has a type, known at compilation time. This section introduces the simplest of them - the built-in primitive types: booleans and unsigned integers. Together with addresses, covered in the next section, they are the material every other type is built from.
The code samples in this chapter are excerpts: expressions like the ones below live inside a function - usually a test function - in a module, which we omit for brevity. To try a sample yourself, place it inside a #[test] function of the package created in the Hello World chapter and run sui move test.
Variables and Assignment
Variables are declared with the let keyword, and they are immutable by default: once a value is assigned, it cannot be replaced. A variable that needs to change is declared with let mut, and only then can it be reassigned with the = operator:
// The type annotation is optional when it can be inferred.
let x: bool = true;
let y = 10u8;
// A `mut` variable can be reassigned with the `=` operator.
let mut z: u8 = 42;
z = 43;
The type annotation - the : u8 after the name - is optional wherever the compiler can infer the type from the value or from later use; writing it out is a matter of clarity, not necessity.
A variable name can also be reused by declaring it again, which is called shadowing. Unlike reassignment, shadowing creates a new variable, so it works on immutable variables and can change the type:
let x: u8 = 42;
// The new `x` replaces the previous one, and may
// even have a different type.
let x: u16 = (x as u16) + 1;
Booleans
The bool type has exactly two values - the keywords true and false - and the compiler always infers it, so a bool never needs a type annotation. Booleans combine with the logical operators && (and), || (or), and ! (not), where && and || short-circuit: the right-hand side is not evaluated if the left-hand side already decides the result.
// The type of a boolean is always inferred.
let is_ready = true;
let is_done = false;
// Logical operators: `&&` (and), `||` (or), and `!` (not).
let in_progress = is_ready && !is_done;
Booleans store flags and drive conditions - the if and while expressions covered in the Control Flow section.
Integer Types
Move has six integer types, differing only in size - and all of them unsigned: there are no negative integers in Move, and no dedicated signed types.
| Type | Size (bits) | Maximum Value |
|---|---|---|
| u8 | 8 | 255 |
| u16 | 16 | 65_535 |
| u32 | 32 | 4_294_967_295 |
| u64 | 64 | 18_446_744_073_709_551_615 |
| u128 | 128 | 2128 − 1 |
| u256 | 256 | 2256 − 1 |
The workhorse is u64 - token amounts, sizes, and indices all use it. Integer literals are written in decimal (42), with optional underscores for readability (1_000_000), or in hexadecimal with the 0x prefix (0x2A):
let small: u8 = 42;
let medium: u16 = 1_000; // underscores improve readability
let large: u256 = 100_000_000_000;
let hex: u64 = 0x2A; // hexadecimal literal, 42
While true and false are unambiguously booleans, a literal like 42 could be any of the six integer types. The compiler infers the type from how the value is used, defaulting to u64; when inference is not enough - or when being explicit reads better - the type can be given as an annotation or as a literal suffix:
// Both are equivalent.
let x: u8 = 42;
let x = 42u8;
Operations
Move supports the standard arithmetic operations for integers: addition, subtraction, multiplication, division, and modulus (remainder). None of them can produce a value outside the range of the type - instead of wrapping around, the operation aborts:
| Syntax | Operation | Aborts If |
|---|---|---|
| + | addition | Result is too large for the integer type |
| - | subtraction | Result is less than zero |
| * | multiplication | Result is too large for the integer type |
| % | modulus (remainder) | The divisor is 0 |
| / | truncating division | The divisor is 0 |
Division is truncating: there are no fractional values, and any remainder is discarded, so 7 / 2 is 3. Integers can also be compared with ==, !=, <, >, <=, and >=, producing a bool:
let a = 10u8;
let b = 20u8;
// Comparison produces a `bool`; the operands must be of the same type.
let is_less = a < b; // true
let is_equal = a == b; // false
In every operation and comparison, the types of the operands must match - there is no implicit conversion between integer types, and adding a u8 to a u64 is a compilation error. To operate on different types, one of the operands has to be explicitly cast.
For more operations, including bitwise operations, refer to the Move Reference.
Casting with as
The as operator converts an integer from one type to another. Note that an expression with a cast often needs parentheses around it to prevent ambiguity:
let x: u8 = 42;
let y: u16 = x as u16;
let z = 2 * (x as u16); // ambiguity requires parentheses
Casting up to a larger type always succeeds. Casting down must fit: unlike languages that silently truncate the value, Move aborts when it is out of range:
let x: u16 = 300;
let y = x as u8; // ABORTS! 300 does not fit into `u8`
A common use of upcasting is making room for an intermediate result that would not fit into the original type:
// The same values that would overflow `u8` arithmetic
// fit comfortably once upcast to `u16`.
let x: u8 = 255;
let y: u8 = 255;
let z: u16 = (x as u16) + ((y as u16) * 2);
Overflow and Underflow
As the operations table shows, arithmetic in Move never wraps around. An operation whose result does not fit into the type - too large, or below zero - aborts at runtime:
let x = 255u8;
let y = 1u8;
let z = x + y; // ABORTS! The result does not fit into `u8`
This is a deliberate safety feature. Silent overflow is a classic source of smart contract bugs - a balance that wraps around to zero, or a check that passes because a value quietly became small. Move turns every such case into a loud failure that reverts the transaction.