Documentation Index
11 min readChapter 16

16. Memory Management in Quorlin

Memory management in the Quorlin language and the Kortana Virtual Machine (KVM) provides a safe, high-level developer experience while maintaining precise, low-level execution semantics. Unlike systems programming languages where memory must be manually allocated and freed using raw pointers, Quorlin abstracts memory management through strong typing, automatic scope management, and automated instruction generation. Underneath this high-level surface, the KVM operates a register-based architecture with explicit, word-aligned linear memory operations.

This chapter details how memory is structured, allocated, utilized, and cleaned up across the lifecycle of a Quorlin smart contract execution.


16.1 Overview: Memory vs. Storage vs. Constant Pool

Understanding memory in Quorlin requires distinguishing between the three distinct state regions available during execution:

  1. Persistent Storage (Unified State Trie): Long-term data held on the blockchain state trie. Storage slots are 256-bit keys mapped to 256-bit values, read and written via state-host interfaces (get_storage and set_storage). Writes to storage are expensive in gas.
  2. Transient Linear Memory: Byte-addressable scratchpad memory initialized to zero for each execution call. Transient memory is volatile and discarded once a transaction execution frame completes. It is used for complex dynamic values, such as raw text strings, external call payloads, and ABI encoding/decoding buffers.
  3. The Constant Pool: A contiguous section contained within the KVM bytecode module header. The module header stores 32-byte constants loaded at bytecode compilation time, accessible via register loading instructions (LoadK).
Execution RegionPersistenceAccess UnitLow-Level Operations / OpcodesGas Impact
RegistersFrame-scoped256-bit wordsRegister-Form (r_form), MovMinimal (Register operations)
Linear MemoryTransaction Call8-bit bytes / 32-byte wordsMLoad, MStore, MStore8, CallDataLoadkGasVeryLow base cost + expansion
Storage TrieBlock State32-byte keys/valuesget_storage, set_storageHigh cost (differs for cold vs. overwrite)
Constant PoolImmutable Bytecode32-byte wordsLoadKkGasVeryLow

16.2 The KVM Memory Architecture

The Kortana Virtual Machine uses a 32-byte (256-bit) word size as its primary data unit, aligning with standard cryptographic primitives and EVM/KEVM compatibility guarantees.

Register-Based Data Storage

Unlike stack-based virtual machines, the KVM ISA is fundamentally register-based. An instruction in KVM is encoded as a 32-bit big-endian integer, utilizing designated bitfields for register selection:

  • Opcode: Bits 24–31 (8 bits)
  • Destination Register (rd): Bits 19–23 (5 bits, supporting 32 registers)
  • Source Register 1 (rs1): Bits 14–18 (5 bits)
  • Source Register 2 (rs2): Bits 9–13 (5 bits)
  • Function / Immediate (funct/imm): Remaining low-order bits

When scalar types like number (u256), truth (bool), or address are processed inside local function scopes, the Quorlin code generator (CodeGenerator) binds them primarily to KVM registers rather than staging them in linear memory.

Linear Memory Alignment and Opcodes

When working with byte arrays, call payloads, or string manipulation, register capacity is exceeded, and transient linear memory is utilized. KVM provides four primary instructions for managing linear memory:

  1. MLoad (rd, rs1): Reads a 32-byte (256-bit) big-endian word from linear memory starting at the offset specified in register rs1, storing the resulting word into destination register rd.
  2. MStore (rs1, rs2): Writes the 32-byte word contained in register rs2 into linear memory at the byte offset specified in register rs1.
  3. MStore8 (rs1, rs2): Writes a single byte (the lowest 8 bits of register rs2) into linear memory at the offset defined by register rs1.
  4. CallDataLoad (rd, rs1): Reads a 32-byte word from the transaction call input payload at the offset given by rs1 into destination register rd.
          KVM Linear Memory (Byte-Addressable Transient Space)
Offset:  0x00      0x04      0x08      0x0C      0x10      0x14      0x18      0x1C      0x20
        +---------+---------+---------+---------+---------+---------+---------+---------+
Data:   | 00 ...  | 00 ...  |   256-bit Word Data / ABI Enconding Payload   | ... 00  |
        +---------+---------+---------+---------+---------+---------+---------+---------+
        ^                                                                           ^
        |----------- MLoad / MStore 32-Byte Block Operations Window ----------------|

Offset Narrowing and Bounds Protection

All memory offsets passed to memory opcodes inside the KVM interpreter are represented as 256-bit integer values (uint256_t). However, host system memory architecture cannot allocate $2^{256}$ bytes. The KVM interpreter enforces offset bounds safety by inspecting memory offsets before executing any operation.

If a contract attempts to pass an offset or length that exceeds execution boundaries (e.g., an address wrapped around $2^{64}$), the interpreter catches the invalid range and halts execution safely, avoiding memory truncation or out-of-bounds pointer vulnerabilities.


16.3 Representation of Quorlin Types in Memory

The Quorlin compiler (quorlin::Analyzer and quorlin::CodeGenerator) maps language-level data types into standardized byte layouts.

contract TypeLayoutDemo { number totalAmount; address owner; text symbol; reads number getAmount() { return totalAmount; } }

Primitive Types (number, truth, address)

  • number (mapped to Type::U256 / ABI uint256): Occupies a full 32-byte (256-bit) word. Unsigned, big-endian byte layout. Arithmetic operations check boundaries or explicitly use wrapping variants (AddWrap, SubWrap, MulWrap).
  • truth (mapped to Type::Bool / ABI bool): Stored inside a 32-byte register or memory slot as 0x0000...0000 (no) or 0x0000...0001 (yes).
  • address (mapped to Type::Address / ABI address): A 160-bit (20-byte) raw identifier. When loaded into a 256-bit register or memory word, the address is left-padded with 12 zero bytes (0x00), placing the 20 address bytes in the lowest-significant position.
Address Word Layout (32 Bytes / 256 Bits):
+------------------------------------------+------------------------------------------+
| 12 Bytes Zero Padding (0x00...00)        | 20 Bytes Active Address Data             |
+------------------------------------------+------------------------------------------+
Byte 0                                     Byte 12                                    Byte 31

Text Data (text)

Strings in Quorlin are represented by the text type (which maps to ABI type string). Unlike primitive numbers, text dynamic payloads cannot fit into a single scalar register.

  1. Size Limits: Text literals and stored string fields are bounded by kMaxTextBytes.
  2. Memory Layout: When text is encoded for event emissions, return values, or calls, it is written to transient linear memory as a length-prefixed payload:
    • Offset + 0x00: 32-byte word holding the byte length of the text.
    • Offset + 0x20: Raw UTF-8 encoded string bytes.
    • Padding: Zero-padded up to the nearest 32-byte word boundary.

16.4 Structs and Dynamic Records in Memory

Quorlin supports user-defined data structures via the record keyword. A record groups multiple explicit fields into a structured layout.

record TokenHolder { address holder; number balance; truth isActive; } contract StorageRecordDemo { map<address, TokenHolder> holders; writes truth registerHolder(address user, number initialBalance) { TokenHolder recordInfo = TokenHolder(user, initialBalance, yes); holders[user] = recordInfo; return yes; } }

When a record is instantiated or passed in transient memory:

  1. Field ordering matches the exact sequence specified in the source declaration (record.order).
  2. Fields are contiguous in memory. Each primitive field (address, number, truth) occupies a 32-byte offset window within linear memory.
  3. Accessing a field (e.g., recordInfo.balance) resolves at compile-time to an offset equal to field_index * 32 bytes relative to the record's base memory pointer.

16.5 ABI Encoding and Call Data Deserialization

When an external contract or user calls a Quorlin method, interaction occurs through the Application Binary Interface (ABI). The ABI emitter (quorlin/abi.cpp) outputs an Ethereum-compatible JSON description, mapping Quorlin types to standard standard ABI primitives:

[ { "type": "function", "name": "transfer", "inputs": [ {"name": "recipient", "type": "address", "internalType": "address"}, {"name": "amount", "type": "uint256", "internalType": "number"} ], "outputs": [ {"name": "", "type": "bool", "internalType": "truth"} ] } ]

Reading Input Data (CallDataLoad)

When an incoming invocation hits a contract, input data lies in the transaction payload. The generated entrypoint inspects the function selector derived from the Keccak-256 hash of the signature (e.g., transfer(address,uint256)).

The compiler generates KVM code using CallDataLoad to pull arguments out of calldata and place them into registers or local memory:

Calldata Memory Buffer:
+-------------------+-----------------------------------+-----------------------------------+
| Selector (4 Bytes)| Arg 0: Recipient Address (32 B)   | Arg 1: Amount uint256 (32 B)      |
+-------------------+-----------------------------------+-----------------------------------+
Offset 0x00         Offset 0x04                         Offset 0x24
  1. The first 4 bytes are loaded to match the method selector.
  2. Parameter 0 (recipient) is loaded from offset 0x04 via CallDataLoad.
  3. Parameter 1 (amount) is loaded from offset 0x24 via CallDataLoad.

Output Encoding and Execution Termination

Returning data from a function triggers linear memory staging followed by an execution termination opcode:

reads number queryBalance(address account) { return balances[account]; }

During the execution of return:

  1. The result value (balances[account]) is calculated into a register.
  2. The compiler emits instructions to write the register contents to transient memory location 0x00 via MStore.
  3. The compiler issues the KVM instruction Ret (or Return), passing offset 0x00 and size 32 as return bounds.
  4. If an assertion or require condition fails, the compiler emits instructions staging the revert error payload and executes Revert.

16.6 Gas Accounting for Memory Operations

Memory expansion costs gas. KVM calculates gas systematically based on operation types defined in kvm/gas.hpp:

  • Free Operations (kGasZero): Stop, Return, Revert. (Halting the machine does not penalize remaining gas).
  • Very Low Operations (kGasVeryLow): MLoad, MStore, MStore8, CallDataLoad, LoadK, LoadI, Mov, arithmetic logic.
  • Storage Operations: State modifications via set_storage and get_storage incur significantly higher gas costs than memory operations.
Gas Cost Spectrum:
[ Register Ops / Mov ] < [ MLoad / MStore (Very Low) ] << [ Storage Read / Write ]

Because transient memory accesses operate at kGasVeryLow cost, utilizing temporary local variables in Quorlin functions is highly efficient compared to updating contract storage fields.


16.7 Concrete Compiler Code Generation Example

To visualize how high-level Quorlin statements translate into low-level KVM memory operations, consider the following contract method:

contract MemoryAllocationExample { number storedData; writes truth processData(number value) { number temp = value + 10; storedData = temp; return yes; } }

During compilation (quorlin/compiler.cpp), this source code passes through four distinct phases:

+------------------+     +------------------+     +------------------+     +------------------+
| 1. Lexer         | --> | 2. Parser        | --> | 3. Analyzer      | --> | 4. CodeGenerator |
| (Token Stream)   |     | (SourceUnit AST) |     | (Type Validation)|     | (KVM Bytecode)   |
+------------------+     +------------------+     +------------------+     +------------------+
  1. Lexical Analysis: Tokenizes words (writes, truth, number, =, +).
  2. Parsing: Constructs an Abstract Syntax Tree (AST) confirming syntax rules.
  3. Semantic Analysis (sema.cpp): Validates types. Guarantees that value is a Type::U256 (number), 10 is an integer literal, and addition is valid.
  4. Code Generation (codegen.cpp): Converts semantic trees to KVM ISA instructions:
# Generated KVM Assembly Conceptual Stream:
CallDataLoad r1, 0x04       # Load 'value' parameter from calldata into register r1
LoadI        r2, 10         # Load immediate literal 10 into register r2
Add          r3, r1, r2     # Perform unsigned 256-bit addition: r3 = r1 + r2
SetStorage   slot(0), r3    # Persist r3 to storage slot 0 ('storedData')
LoadI        r4, 1          # Load boolean truth (1) into register r4
MStore       0x00, r4       # Store return value (yes) to memory address 0x00
Ret          0x00, 0x20     # Return 32 bytes from memory address 0x00

16.8 Memory Safety Best Practices for Developers

When writing smart contracts in Quorlin, following these guidelines ensures optimal memory efficiency and safety:

  1. Prefer Register Allocation for Primitive Operations: Keep intermediate computations local inside functions using local scalar variables (number, truth, address). The Quorlin compiler prioritizes register storage for primitive local variables, avoiding unnecessary linear memory access.

  2. Bound Dynamic Text Input: Ensure string inputs do not exceed kMaxTextBytes. Large text dynamic payloads increase memory expansion costs rapidly.

  3. Minimize Storage Operations inside Loops: Because transient linear memory access (MLoad/MStore) is significantly cheaper than persistent storage updates (set_storage), stage calculations inside local variables within loops and commit the final result to storage once execution finishes.

// Efficient Memory Management Example: contract EfficientLoop { number totalSum; writes truth accumulate(number count) { number localAccumulator = 0; // Staging in register/memory number i = 0; if i < count { localAccumulator = localAccumulator + 5; i = i + 1; } totalSum = localAccumulator; // Single write to persistent storage return yes; } }

Through this layered design—combining high-level developer abstractions with strong static type checking and low-level register and linear memory instructions—Quorlin guarantees both memory safety and high-performance execution on the Kortana Virtual Machine.