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

Primitive Types

Integers

Move supports six unsigned integer types: u8, u16, u32, u64, u128, and u256. Values of these types range from 0 to a maximum that depends on the size of the type.

TypeValue Range
Unsigned 8-bit integer, u80 to 28 - 1
Unsigned 16-bit integer, u160 to 216 - 1
Unsigned 32-bit integer, u320 to 232 - 1
Unsigned 64-bit integer, u640 to 264 - 1
Unsigned 128-bit integer, u1280 to 2128 - 1
Unsigned 256-bit integer, u2560 to 2256 - 1

Since version 2.3, Move also supports signed integer types:

TypeValue Range
Signed 8-bit integer, i8-27 to 27 - 1
Signed 16-bit integer, i16-215 to 215 - 1
Signed 32-bit integer, i32-231 to 231 - 1
Signed 64-bit integer, i64-263 to 263 - 1
Signed 128-bit integer, i128-2127 to 2127 - 1
Signed 256-bit integer, i256-2255 to 2255 - 1

Literals

Literal values for these types are specified either as a sequence of digits (e.g., 112) or as hex literals, e.g., 0xFF. The type of the literal can optionally be added as a suffix, e.g., 112u8. If the type is not specified, the compiler will try to infer the type from the context where the literal is used. If the type cannot be inferred, it is assumed to be u64.

Number literals can be separated by underscores for grouping and readability (e.g., 1_234_5678, 1_000u128, 0xAB_CD_12_35).

To denote a negative number, prefix it with a - sign (e.g., -112).

If a literal is too large for its specified (or inferred) size range, an error is reported.

Examples

script {
  fun example() {
    // literals with explicit annotations;
    let explicit_u8 = 1u8;
    let explicit_u16 = 1u16;
    let explicit_u32 = 1u32;
    let explicit_u64 = 2u64;
    let explicit_u128 = 3u128;
    let explicit_u256 = 1u256;
    let explicit_u64_underscored = 154_322_973u64;
    let explicit_i8 = -1i8;
    let explicit_i64 = -2i64;

    // literals with simple inference
    let simple_u8: u8 = 1;
    let simple_u16: u16 = 1;
    let simple_u32: u32 = 1;
    let simple_u64: u64 = 2;
    let simple_u128: u128 = 3;
    let simple_u256: u256 = 1;

    // literals with more complex inference
    let complex_u8 = 1; // inferred: u8
    // right hand argument to shift must be u8
    let _unused = 10 << complex_u8;

    let x: u8 = 38;
    let complex_u8 = 2; // inferred: u8
    // arguments to `+` must have the same type
    let _unused = x + complex_u8;

    let complex_u128 = 133_876; // inferred: u128
    // inferred from function argument type
    function_that_takes_u128(complex_u128);

    // literals can be written in hex
    let hex_u8: u8 = 0x1;
    let hex_u16: u16 = 0x1BAE;
    let hex_u32: u32 = 0xDEAD80;
    let hex_u64: u64 = 0xCAFE;
    let hex_u128: u128 = 0xDEADBEEF;
    let hex_u256: u256 = 0x1123_456A_BCDE_F;
  }
}

Operations

Arithmetic

Each of these types supports the same set of checked arithmetic operations. For all of these operations, both arguments (the left and right side operands) must be of the same type. If you need to operate over values of different types, you will need to first perform a cast. Similarly, if you expect the result of the operation to be too large for the integer type, perform a cast to a larger size before performing the operation.

All arithmetic operations abort instead of behaving in a way that mathematical integers would not (e.g., overflow, underflow, divide-by-zero).

SyntaxOperationAborts If
a + badditionResult is too large/small for the integer type
a - bsubtractionResult is less than zero
a * bmultiplicationResult is too large/small for the integer type
a % bmodular divisionThe divisor is 0
a / btruncating divisionThe divisor is 0, or the result overflows
-anegationNegated result too large (e.g. -MIN_I64)

Bitwise

The unsigned integer types support the following bitwise operations that treat each number as a series of individual bits, either 0 or 1, instead of as numerical integer values.

Bitwise operations do not abort.

SyntaxOperationDescription
&bitwise andPerforms a boolean and for each bit pairwise
|bitwise orPerforms a boolean or for each bit pairwise
^bitwise xorPerforms a boolean exclusive or for each bit pairwise

Bit Shifts

Similar to the bitwise operations, each unsigned integer type supports bit shifts. But unlike the other operations, the right-hand side operand (how many bits to shift by) must always be a u8 and need not match the left side operand (the number you are shifting).

Bit shifts abort if the number of bits to shift by is greater than or equal to 8, 16, 32, 64, 128 or 256 for u8, u16, u32, u64, u128 and u256 respectively.

SyntaxOperationAborts if
<<shift leftNumber of bits to shift by is greater than or equal to the size of the integer type
>>shift rightNumber of bits to shift by is greater than or equal to the size of the integer type

Comparisons

All integer types support the “comparison” operations. Both arguments need to be of the same type. If you need to compare integers of different types, you will need to cast one of them first.

Comparison operations do not abort.

SyntaxOperation
<less than
>greater than
<=less than or equal to
>=greater than or equal to

Equality

Like all types with drop in Move, all integer types support the “equal” and “not equal” operations. Both arguments need to be of the same type. If you need to compare integers of different types, you will need to cast one of them first.

Equality operations do not abort.

SyntaxOperation
==equal
!=not equal

For more details see the section on equality

Casting

Integer types of one size can be cast to integer types of another size. Integers are the only types in Move that support casting.

Casts do not truncate. Casting will abort if the result is too large or too small for the specified type.

SyntaxOperationAborts if
(e as T)Cast integer expression e into an integer type Te is too large or too small to represent as a T

Any integer can be cast into any other integer type, including signed to unsigned and unsigned to signed, provided the target type is able to represent the source value.

For example:

  • (x as u8)
  • (y as u16)
  • (873u16 as u32)
  • (2u8 as u64)
  • (1 + 3 as u128)
  • (4/2 + 12345 as u256)

Notice that since language version 2.0, casts don’t always need to be in parentheses. Thus, x as u8 is a valid expression.

Ownership

As with the other scalar values built-in to the language, integer values are implicitly copyable, meaning they can be copied without an explicit instruction such as copy.

Bool

bool is Move’s primitive type for boolean true and false values.

Literals

Literals for bool are either true or false.

Operations

Logical

bool supports three logical operations:

SyntaxDescriptionEquivalent Expression
&&short-circuiting logical andp && q is equivalent to if (p) q else false
||short-circuiting logical orp || q is equivalent to if (p) true else q
!logical negation!p is equivalent to if (p) false else true

Control Flow

bool values are used in several of Move’s control-flow constructs:

Ownership

As with the other scalar values built into the language, boolean values are implicitly copyable, meaning they can be copied without an explicit instruction such as copy.

Address

address is a built-in type in Move that is used to represent locations (sometimes called accounts) in global storage. An address value is a 256-bit (32-byte) identifier. At a given address, two things can be stored: Modules and Resources.

Although an address is a 256-bit integer under the hood, Move addresses are intentionally opaque—they cannot be created from integers, they do not support arithmetic operations, and they cannot be modified. Even though there might be interesting programs that would use such a feature (e.g., pointer arithmetic in C fills a similar niche), Move does not allow this dynamic behavior because it has been designed from the ground up to support static verification.

You can use runtime address values (values of type address) to access resources at that address. You cannot access modules at runtime via address values.

Addresses and Their Syntax

Addresses come in two flavors, named or numerical. The syntax for a named address follows the same rules for any named identifier in Move. The syntax of a numerical address is not restricted to hex-encoded values, and any valid u256 numerical value can be used as an address value, e.g., 42, 0xCAFE, and 2021 are all valid numerical address literals.

To distinguish when an address is being used in an expression context or not, the syntax when using an address differs depending on the context where it’s used:

  • When an address is used as an expression the address must be prefixed by the @ character, i.e., @<numerical_value> or @<named_address_identifier>.
  • Outside of expression contexts, the address may be written without the leading @ character, i.e., <numerical_value> or <named_address_identifier>.

In general, you can think of @ as an operator that takes an address from being a namespace item to being an expression item.

Named Addresses

Named addresses are a feature that allows identifiers to be used in place of numerical values in any spot where addresses are used, and not just at the value level. Named addresses are declared and bound as top-level elements (outside of modules and scripts) in Move Packages, or passed as arguments to the Move compiler.

Named addresses only exist at the source language level and will be fully substituted for their value at the bytecode level. Because of this, modules and module members must be accessed through the module’s named address and not through the numerical value assigned to the named address during compilation, e.g., use my_addr::foo is not equivalent to use 0x2::foo even if the Move program is compiled with my_addr set to 0x2. This distinction is discussed in more detail in the section on Modules and Scripts.

Examples

script {
  fun example() {
    let a1: address = @0x1; // shorthand for 0x0000000000000000000000000000000000000000000000000000000000000001
    let a2: address = @0x42; // shorthand for 0x0000000000000000000000000000000000000000000000000000000000000042
    let a3: address = @0xDEADBEEF; // shorthand for 0x00000000000000000000000000000000000000000000000000000000DEADBEEF
    let a4: address = @0x000000000000000000000000000000000000000000000000000000000000000A;
    let a5: address = @std; // Assigns `a5` the value of the named address `std`
    let a6: address = @66;
    let a7: address = @0x42;
  }
}

module 66::some_module {   // Not in expression context, so no @ needed
    use 0x1::other_module; // Not in expression context so no @ needed
    use std::vector;       // Can use a named address as a namespace item when using other modules
    ...
}

module std::other_module {  // Can use a named address as a namespace item to declare a module
    ...
}

Global Storage Operations

The primary purpose of address values is to interact with the global storage operations.

address values are used with the exists, borrow_global, borrow_global_mut, and move_from operations.

The only global storage operation that does not use address is move_to, which uses signer.

Ownership

As with the other scalar values built-in to the language, address values are implicitly copyable, meaning they can be copied without an explicit instruction such as copy.

Signer

signer is a built-in Move resource type. A signer is a capability that allows the holder to act on behalf of a particular address. You can think of the native implementation as being:

module 0x1::signer {
  struct signer has drop { a: address }
}

A signer is somewhat similar to a Unix UID in that it represents a user authenticated by code outside of Move (e.g., by checking a cryptographic signature or password).

Comparison to address

A Move program can create any address value without special permission using address literals:

script {
  fun example() {
    let a1 = @0x1;
    let a2 = @0x2;
    // ... and so on for every other possible address
  }
}

However, signer values are special because they cannot be created via literals or instructions — only by the Move VM. Before the VM runs a script with parameters of type signer, it will automatically create signer values and pass them into the script:

script {
    use std::signer;
    fun main(s: signer) {
        assert!(signer::address_of(&s) == @0x42, 0);
    }
}

This script will abort with code 0 if the script is sent from any address other than 0x42.

A Move script can have an arbitrary number of signers as long as the signers are a prefix to any other arguments. In other words, all of the signer arguments must come first:

script {
    use std::signer;
    fun main(s1: signer, s2: signer, x: u64, y: u8) {
        // ...
    }
}

This is useful for implementing multi-signer scripts that atomically act with the authority of multiple parties. For example, an extension of the script above could perform an atomic currency swap between s1 and s2.

signer Operators

The std::signer standard library module provides two utility functions over signer values:

FunctionDescription
signer::address_of(&signer): addressReturn the address wrapped by this &signer.
signer::borrow_address(&signer): &addressReturn a reference to the address wrapped by this &signer.

In addition, the move_to<T>(&signer, T) global storage operator requires a &signer argument to publish a resource T under signer.address’s account. This ensures that only an authenticated user can elect to publish a resource under their address.

Ownership

Unlike simple scalar values, signer values are not copyable, meaning they cannot be copied from any operation whether it be through an explicit copy instruction or through a dereference *.