Introduction
What is the MoveVM?
In latest Sui, MoveRuntime owns the package cache, native registry, and VM configuration,
and builds a linkage-specific MoveVM for execution. That MoveVM executes compiled
Move bytecode, mutates stack/frame/value state, invokes native functions, and returns either success values
or a typed failure status.
What is a package and its modules?
A package is an on-chain unit containing compiled modules plus linkage metadata. Each module has function
definitions and module tables (handles, signatures, constants, and definitions). In latest Sui,
cross-package execution is resolved against a LinkageContext and per-linkage
VMDispatchTables, so dispatch is package- and linkage-aware rather than only a raw handle lookup.
Execution Environment
Execution Stages
Sui starts from a programmable transaction command stream. The adapter prepares input objects, gas state,
linkage, and native extensions, then asks MoveRuntime to build or reuse a linkage-specific
MoveVM. A MoveCall executes bytecode opcode-by-opcode; the serialized call opcodes
stay CALL and CALL_GENERIC, but the latest runtime lowers them internally into
direct, virtual, or generic dispatch forms. Native boundaries handle object runtime, transfers, and
TxContext operations before Sui finalizes effects and gas.
Data Regions
| Region | Scope / Lifetime | Main Access Path |
|---|---|---|
| Function opcode stream | Frame-local (current function) | Program counter + branch/call/return opcodes |
| Function jump tables | Frame-local (enum-capable functions) | VARIANT_SWITCH targets function-local jump tables |
| Module tables | Module/package metadata | Indices in opcodes such as CALL, LD_CONST, PACK, FIELD ops |
| Linkage / dispatch tables | VM-instance shared for one linkage context | LinkageContext + VMDispatchTables resolve packages, functions, and datatypes |
| Program counter | Frame-local | Advanced or redirected by control-flow opcodes |
| Operand stack | Interpreter-shared across active frames | Most opcodes consume/produce values here |
| Call stack | Interpreter-shared | CALL/CALL_GENERIC push, RET pops |
| Machine heap / locals | Interpreter-managed frame storage | Frame allocation plus local-register access through COPY_LOC, MOVE_LOC, ST_LOC, *_BORROW_LOC |
| Base heap | Invocation-level external value storage | Holds PTB / external arguments so references into those values can exist |
| Type arguments | Frame-local metadata | Generic instantiation resolution, not runtime stack data |
| Sui native state | Transaction-level shared object/package state | Native extensions such as ObjectRuntime, TransactionContext, and native cost tables |
Latest runtime limits are 1024 operand-stack entries, 1024 call frames, 2048 locals per frame, type depth 256, and value depth 128.
Runtime values are represented as primitives, vectors or primitive vectors, structs, variants, and references. References are either direct references to a value cell or indexed references into an aggregate value.
Function Opcode Stream and Program Counter
Each frame executes one function bytecode stream. The program counter (PC) points to opcode position, not a raw hex byte
offset. BRANCH, BR_TRUE, and BR_FALSE set PC to target opcode position.
VARIANT_SWITCH reads its branch destinations from the function's local jump-table vector. If PC moves
beyond available opcode positions, execution fails with PC_OVERFLOW.
Module Tables Context
Tables are immutable metadata for the active module context. Serialized opcodes still read table entries by index:
LD_CONST resolves constants, and type/field/struct/variant opcodes resolve layouts through signature
tokens and handle tables. In the latest runtime, many of those indices are translated up front into cached pointers
or vtable keys. In particular, serialized CALL can become internal DirectCall or
VirtualCall, while serialized CALL_GENERIC becomes internal generic-call dispatch.
Frame Lifecycle
On function entry, the VM allocates a stack frame on the runtime machine heap, sizes locals to the function local
count, and initializes argument locals from stack values. For calls, argument values are popped from the shared
operand stack, written into callee locals, and a new frame starts at pc = 0. Invocation/PTB values
that need borrowable storage live in the base heap. On return, the frame is freed and control resumes in the caller
at the instruction after the originating call site.
Operand Stack Semantics
The operand stack is shared across active call frames. Arithmetic, loads, comparisons, casts, pack/unpack, and reference operations all read/write this stack. CALL consumes callee arguments from this shared stack. Return values are left on the shared stack for the caller. The latest runtime caps this stack at 1024 values.
RET Semantics
RET ends the current frame. The number of return values is inferred from the callee function signature, not by
“top-most only” convention. Those values remain on the shared operand stack where the caller continues execution,
regardless of whether the runtime lowered the original call to direct or virtual dispatch.
Locals Lifecycle
Locals live inside stack frames allocated on the machine heap. Arguments are loaded first into
loc[0..arg_count-1] in parameter order. Remaining locals are initialized as invalid until set.
Reading/moving invalid locals or violating local access constraints causes runtime failure statuses. The latest
runtime caps locals at 2048 per frame.
Type Arguments
Type arguments are frame metadata, not runtime values in locals or operand stack. Generic opcodes and handle resolution read this metadata to instantiate concrete types for signatures, datatype layouts, field accesses, and function instantiations. In latest Sui, that instantiation happens against the active linkage context and dispatch tables.
Move Types and SignatureToken Encoding
Move type information is encoded through signature vectors. A signature encodes ULEB128 length followed by
SignatureToken bytes. Composite tokens recursively append nested token streams, and datatype-based
tokens resolve through table indices rather than inline type names. In the latest binary-format source the
underlying token names are Datatype and DatatypeInstantiation; older docs often call
them STRUCT and STRUCT_INST.
| Byte | Token | Encoding Detail |
|---|---|---|
| 0x1 | BOOL | Boolean primitive |
| 0x2 | U8 | 8-bit unsigned integer |
| 0x3 | U64 | 64-bit unsigned integer |
| 0x4 | U128 | 128-bit unsigned integer |
| 0x5 | ADDRESS | Account address type |
| 0x6 | REFERENCE | Followed by nested SignatureToken |
| 0x7 | MUTABLE_REFERENCE | Followed by nested SignatureToken |
| 0x8 | DATATYPE (historically STRUCT) | Followed by ULEB128 index into STRUCT_HANDLES; in latest source this handle space is named DATATYPE_HANDLES |
| 0x9 | TYPE_PARAMETER | Followed by ULEB128 type parameter index |
| 0xA | VECTOR | Followed by nested SignatureToken element type |
| 0xB | DATATYPE_INST (historically STRUCT_INST) | Datatype handle index + type substitution signature vector |
| 0xC | SIGNER | Signer special type |
| 0xD | U16 | 16-bit unsigned integer |
| 0xE | U32 | 32-bit unsigned integer |
| 0xF | U256 | 256-bit unsigned integer |
Example A: (u8, u128) -> 0x02 0x02 0x04
Example B: (vector<address>) -> 0x01 0x0A 0x05
Example C: (&Datatype(handle=0x10)) -> 0x01 0x06 0x08 0x10
Example D: (Datatype<T0>) via DATATYPE_INST
-> 0x01 0x0B 0x10 0x01 0x09 0x00
Latest source defines bytecode versions 1 through 7, while the current serializer emits versions 5 through 7.
Version 4 adds vector opcodes, version 6 adds U16/U32/U256 tokens
and opcodes, and version 7 adds enums plus function-local jump tables. Version 7 and above must carry the
Sui binary flavor.
Verifier vs Runtime Checks
Before execution, deserialization and bytecode verification enforce binary-version gates, table/opcode validity, well-formed signatures, control flow, stack usage consistency, reference safety constraints, and dependency compatibility. Sui then applies extra verifier rules, including rejecting Move global-storage opcodes because object storage is handled by the adapter. Runtime still enforces dynamic checks during execution: stack underflow/overflow, type and layout validity, arithmetic failures, bounds checks, depth limits, and invariant checks.
Runtime Error Taxonomy
| Status | Typical Trigger |
|---|---|
| ABORTED | ABORT opcode with u64 abort code |
| EMPTY_VALUE_STACK | Pop/read when stack has insufficient values |
| EXECUTION_STACK_OVERFLOW | Pushing past the operand-stack limit |
| CALL_STACK_OVERFLOW | Pushing past the call-stack limit |
| PC_OVERFLOW | Program counter moved beyond opcode range |
| INTERNAL_TYPE_ERROR | Type mismatch for opcode operand expectations |
| VECTOR_OPERATION_ERROR | Vector index, length, or parity violations |
| VARIANT_TAG_MISMATCH | Unpack/switch expects a different enum variant tag |
| VM_MAX_VALUE_DEPTH_REACHED | Constructed/runtime value exceeds depth limit |
| UNKNOWN_INVARIANT_VIOLATION_ERROR | Internal invariant failure |
Sui Native Boundary
Move bytecode execution can invoke native functions that bridge into Sui runtime behavior. Latest Sui installs native
extensions such as ObjectRuntime, TransactionContext, and the native cost table. The
adapter uses that boundary for object operations, transfer flows, fresh IDs, event/object effect materialization,
and final transaction results.
Gas Costs
VM Instruction Metering
During execution the gas meter charges instruction costs, reference/value-size related costs, and native-call charging. Metering is continuous in the interpreter loop and can halt execution on out-of-gas.
Adapter-Level Charging
Sui adapter gas charging performs transaction-level accounting outside raw bytecode stepping: publish/upgrade charging, storage read/write accounting, gas smashing handling, and final net gas deduction against gas payment state.
Gas Refunds
Storage Rebate Timing
Storage rebate is computed after command execution from concrete object-state deltas and storage accounting. Rebate is not known a priori during bytecode stepping; it is applied in final charging/finalization.
Net Gas Finalization
Final net gas is derived from computation + storage costs minus rebates. On storage charging failures/out-of-gas branches, adapter logic applies dedicated fallback handling before producing final transaction effects.