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:
- 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_storageandset_storage). Writes to storage are expensive in gas. - 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.
- 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 Region | Persistence | Access Unit | Low-Level Operations / Opcodes | Gas Impact |
|---|---|---|---|---|
| Registers | Frame-scoped | 256-bit words | Register-Form (r_form), Mov | Minimal (Register operations) |
| Linear Memory | Transaction Call | 8-bit bytes / 32-byte words | MLoad, MStore, MStore8, CallDataLoad | kGasVeryLow base cost + expansion |
| Storage Trie | Block State | 32-byte keys/values | get_storage, set_storage | High cost (differs for cold vs. overwrite) |
| Constant Pool | Immutable Bytecode | 32-byte words | LoadK | kGasVeryLow |
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:
MLoad(rd,rs1): Reads a 32-byte (256-bit) big-endian word from linear memory starting at the offset specified in registerrs1, storing the resulting word into destination registerrd.MStore(rs1,rs2): Writes the 32-byte word contained in registerrs2into linear memory at the byte offset specified in registerrs1.MStore8(rs1,rs2): Writes a single byte (the lowest 8 bits of registerrs2) into linear memory at the offset defined by registerrs1.CallDataLoad(rd,rs1): Reads a 32-byte word from the transaction call input payload at the offset given byrs1into destination registerrd.
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 toType::U256/ ABIuint256): 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 toType::Bool/ ABIbool): Stored inside a 32-byte register or memory slot as0x0000...0000(no) or0x0000...0001(yes).address(mapped toType::Address/ ABIaddress): 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.
- Size Limits: Text literals and stored string fields are bounded by
kMaxTextBytes. - Memory Layout: When
textis 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:
- Field ordering matches the exact sequence specified in the source declaration (
record.order). - Fields are contiguous in memory. Each primitive field (
address,number,truth) occupies a 32-byte offset window within linear memory. - Accessing a field (e.g.,
recordInfo.balance) resolves at compile-time to an offset equal tofield_index * 32bytes 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
- The first 4 bytes are loaded to match the method selector.
- Parameter 0 (
recipient) is loaded from offset0x04viaCallDataLoad. - Parameter 1 (
amount) is loaded from offset0x24viaCallDataLoad.
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:
- The result value (
balances[account]) is calculated into a register. - The compiler emits instructions to write the register contents to transient memory location
0x00viaMStore. - The compiler issues the KVM instruction
Ret(orReturn), passing offset0x00and size32as return bounds. - If an assertion or
requirecondition fails, the compiler emits instructions staging the revert error payload and executesRevert.
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_storageandget_storageincur 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) |
+------------------+ +------------------+ +------------------+ +------------------+
- Lexical Analysis: Tokenizes words (
writes,truth,number,=,+). - Parsing: Constructs an Abstract Syntax Tree (AST) confirming syntax rules.
- Semantic Analysis (
sema.cpp): Validates types. Guarantees thatvalueis aType::U256(number),10is an integer literal, and addition is valid. - 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:
-
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. -
Bound Dynamic Text Input: Ensure string inputs do not exceed
kMaxTextBytes. Large text dynamic payloads increase memory expansion costs rapidly. -
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.