llms.txt
Skip to main content

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.

TypeSize (bits)Maximum Value
u88255
u161665_535
u32324_294_967_295
u646418_446_744_073_709_551_615
u1281282128 − 1
u2562562256 − 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:

SyntaxOperationAborts If
+additionResult is too large for the integer type
-subtractionResult is less than zero
*multiplicationResult is too large for the integer type
%modulus (remainder)The divisor is 0
/truncating divisionThe 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.

Further Reading​

  • Bool in the Move Reference.
  • Integer in the Move Reference.
  • std::u64 module documentation - every integer type has a helper module (std::u8 through std::u256) with functions like min, max, sqrt, and more.
llms.txt