Documentation Index
9 min readChapter 8

8. Structs and Enums (Records and State Types)

Quorlin models custom composite types and state representations with clarity and type safety. While traditional EVM languages like Solidity utilize the struct and enum keywords, Quorlin adopts clean, English-first terminology.

In Quorlin:

  • Composite structures (structs) are declared using the record keyword.
  • Enumerations and discrete state sets are represented via typed state patterns and constant values, which map directly to KVM 256-bit words while preserving ABI compatibility with Solidity uint8 enums.

This chapter details the syntax, semantic analysis, KVM layout, and ABI generation rules for record declarations and state enumerations.


8.1 Defining Records (record)

A record in Quorlin is a custom user-defined data structure that groups related variable fields into a single logical unit. Records can be stored in persistent contract storage, instantiated in temporary memory during function execution, or passed across external call boundaries.

Syntax and Declaration

Records are defined inside a contract scope using the record keyword, followed by the record name and a block containing field declarations:

contract Escrow { record Trade { address buyer; address seller; number amount; truth fulfilled; } map<number, Trade> trades; number tradeCounter; constructor { tradeCounter = 0; } }

Supported Field Types

Every field inside a record must be a explicit Quorlin type:

Quorlin KeywordInternal Type (Type)ABI Type Name (abi_type_name)KVM Storage Size
numberType::U256uint25632 bytes (1 word)
truthType::Boolbool32 bytes (1 word)
addressType::Addressaddress32 bytes (20 bytes low-aligned)
textType::TextstringPointer / Offset word

8.2 Instantiating and Modifying Records

Quorlin allows records to be constructed in memory using positional constructor expressions or loaded directly from state storage maps.

Creating Record Instances

To create a record in memory, invoke the record name as a positional constructor passing expressions matching the field types in the order they were defined:

writes truth createTrade(address seller, number amount) { // Instantiating a Trade record in memory Trade newTrade = Trade(caller, seller, amount, no); // Storing the record in state storage map trades[tradeCounter] = newTrade; tradeCounter = tradeCounter + 1; return yes; }

Field Access and Modification

Record members are accessed and mutated using the dot (.) operator:

writes truth fulfillTrade(number tradeId) { Trade item = trades[tradeId]; require item.fulfilled == no, "already fulfilled"; require caller == item.buyer, "only buyer can fulfill"; // Modifying a record member variable item.fulfilled = yes; trades[tradeId] = item; return yes; }

8.3 Semantic Analysis and Field Validation

During Phase 3 Analysis (quorlin/sema.cpp), the Quorlin compiler strictly enforces structural type safety on record instantiations and field references.

Field Existence Checking

When an expression attempts to read or assign to a field on a record variable (e.g., item.active), the semantic analyzer verifies that the identifier exists within the target record's field symbol table (RecordInfo).

If a non-existent field is referenced, the analyzer emits a diagnostic listing all available fields formatted in plain English. The internal formatting routine field_list constructs human-friendly messages:

// quorlin/sema.cpp std::string field_list(const RecordInfo& record) { std::string listed; for (size_t i = 0; i < record.order.size(); ++i) { if (i > 0) listed += (i + 1 == record.order.size()) ? " and " : ", "; listed += "`" + record.order[i] + "`"; } return listed; }

For instance, accessing item.active on a Trade record containing buyer, seller, amount, and fulfilled will trigger a compiler diagnostic error:

error: field `active` does not exist on record `Trade`; available fields are `buyer`, `seller`, `amount` and `fulfilled`

Constructor Parameter Matching

The semantic analyzer verifies that positional construction of a record exact-matches both the arity (number of parameters) and the concrete types of the record fields. Passing mismatched types (e.g., passing a number to an address field) causes the analysis stage (Analyzer::analyze) to abort code generation before producing KVM bytecode.


8.4 State Modeling and Enumerations

Quorlin deliberately avoids legacy C-style implicitly-typed enums to eliminate silent overflow bugs and unexpected integer casting vulnerabilities. Instead, state modeling in Quorlin smart contracts is achieved through explicit numeric state constants or typed record state flags.

Explicit State Constants Pattern

To implement finite state machines (e.g., Pending, Active, Completed, Cancelled), state integers are defined alongside state validation rules:

contract Auction { // Discrete State Definitions: // 0 = Pending, 1 = Active, 2 = Ended number state; address highestBidder; number highestBid; constructor { state = 0; // Pending } writes truth startAuction() { require state == 0, "auction already started or ended"; state = 1; // Transition to Active return yes; } writes truth endAuction() { require state == 1, "auction not active"; state = 2; // Transition to Ended return yes; } reads number getState() { return state; } }

Record-Based Multistate Pattern

For complex workflows where state includes associated metadata, state is modeled cleanly by nesting a status number or explicit flags directly within a record:

contract OrderBook { record Order { address trader; number amount; number status; // 0: Open, 1: Filled, 2: Cancelled } map<number, Order> orders; reads truth isOpen(number orderId) { Order item = orders[orderId]; return item.status == 0; } }

When interacting with standard Solidity interfaces via EVM cross-calls, a status standard parameter typed as number in Quorlin automatically converts to an EVM uint8 or uint256 according to the target interface selector.


8.5 KVM Memory and Storage Architecture

At runtime, the Kortana Virtual Machine (KVM) executes smart contract logic using 256-bit native registers and memory words. The compiler's code generator (quorlin/codegen.cpp) maps record fields to concrete storage slots and memory offsets.

Storage Layout

When a record variable is stored in persistent world state via a map<K, V> or high-level variable, KVM calculates state slot locations using Keccak-256 derivation.

  1. Storage Slot Calculation: The root storage slot $S$ for key $K$ is computed via keccak256(K . slot_index).
  2. Field Offsets: Sequential record fields occupy sequential storage slot indices ($S + 0, S + 1, S + 2, \dots$).

For example, a Trade record stored at base slot $S$:

  • Slot $S + 0$: buyer (Address, padded to 32 bytes)
  • Slot $S + 1$: seller (Address, padded to 32 bytes)
  • Slot $S + 2$: amount (256-bit unsigned integer)
  • Slot $S + 3$: fulfilled (Boolean value: 1 for yes, 0 for no)

During state interaction, the KVM interpreter (kvm/interpreter.cpp) issues explicit host storage calls through StateHost:

// kvm/interpreter.cpp Result<uint256_t> StateHost::get_storage(const Address& address, const uint256_t& key) const { return world_.get_storage(address, key); } Result<uint256_t> StateHost::set_storage(const Address& address, const uint256_t& key, const uint256_t& value) { KORTANA_TRY_ASSIGN(const uint256_t previous, world_.get_storage(address, key)); KORTANA_TRY(world_.put_storage(address, key, value)); return previous; }

Address Word Conversions

Because KVM memory and registers operate exclusively on 32-byte 256-bit words (uint256_t), 20-byte address fields in records undergo conversion during field loads and stores:

// Conversion routines in kvm/interpreter.cpp [[nodiscard]] uint256_t address_to_word(const Address& address) noexcept { const auto result = uint256_t::from_be_bytes(ByteView{address.data(), Address::kSize}); return result.value_or(uint256_t::zero()); } [[nodiscard]] Address address_from_word(const uint256_t& word) noexcept { Address out; const auto bytes = word.to_be_bytes(); std::memcpy(out.data(), bytes.data() + (32 - Address::kSize), Address::kSize); return out; }

Memory Layout and Bytecode Instructions

When instantiated locally within a function, records reside in transient linear KVM memory. The compiler emits register load/store instructions (MLoad, MStore) generated in quorlin/codegen.cpp:

// Emission of memory write for a 32-byte record field slot Instruction inst = r_form(Opcode::MStore, rd_address, rs_val); emit(inst);

Each field occupies a 32-byte chunk starting at the allocated memory offset pointer.


8.6 ABI Generation for Records

When a Quorlin contract is compiled, the ABI emitter (quorlin/abi.cpp) exports structural metadata as standard Ethereum-compatible JSON ABI outputs, enabling seamless client interactions via web3.js, ethers.js, or standard wallets.

Tuple Representation

Records passed as parameters or returned from external contract interfaces are emitted as ABI tuple types.

Given the following Quorlin declaration:

contract PropertyRegistry { record Building { address owner; number value; } reads Building getBuilding(number id) { // ... } }

The ABI generator emits the corresponding ABI JSON representation:

// quorlin/abi.cpp parameter JSON formatting [[nodiscard]] std::string parameter_json(std::string_view name, Type type, bool indexed, bool with_indexed) { std::string out = "{\"name\":" + quoted(name) + ",\"type\":" + quoted(abi_type_name(type)) + ",\"internalType\":" + quoted(type_name(type)); if (with_indexed) out += ",\"indexed\":" + std::string{indexed ? "true" : "false"}; return out + "}"; }

The exported output maps type_name to Quorlin terms (number, truth, address) for developer tools, while mapping abi_type_name to standard EVM standard strings (uint256, bool, address, tuple):

{ "name": "getBuilding", "type": "function", "stateMutability": "view", "inputs": [ { "name": "id", "type": "uint256", "internalType": "number" } ], "outputs": [ { "name": "", "type": "tuple", "internalType": "record Building", "components": [ { "name": "owner", "type": "address", "internalType": "address" }, { "name": "value", "type": "uint256", "internalType": "number" } ] } ] }

This dual mapping guarantees that external tools recognize parameters correctly while internal Quorlin diagnostics maintain English readability.


8.7 Complete Example: Vault Management System

The following complete contract demonstrates record definitions, state map storage, state constant transitions, and access validations:

contract VaultManager { record Vault { address owner; number depositAmount; number unlockTimestamp; truth locked; } map<number, Vault> vaults; number totalVaults; event VaultCreated(number indexed vaultId, address indexed owner, number amount); event VaultUnlocked(number indexed vaultId, address indexed owner); constructor { totalVaults = 0; } writes number createVault(number lockDuration) { require lockDuration > 0, "duration must be positive"; number newId = totalVaults; Vault memoryVault = Vault(caller, 0, lockDuration, yes); vaults[newId] = memoryVault; totalVaults = totalVaults + 1; emit VaultCreated(newId, caller, 0); return newId; } writes truth deposit(number vaultId, number amount) { Vault item = vaults[vaultId]; require item.owner == caller, "not vault owner"; require item.locked == yes, "vault is closed"; item.depositAmount = item.depositAmount + amount; vaults[vaultId] = item; return yes; } writes truth unlock(number vaultId) { Vault item = vaults[vaultId]; require item.owner == caller, "not vault owner"; require item.locked == yes, "already unlocked"; item.locked = no; vaults[vaultId] = item; emit VaultUnlocked(vaultId, caller); return yes; } reads number getBalance(number vaultId) { Vault item = vaults[vaultId]; return item.depositAmount; } reads truth isLocked(number vaultId) { Vault item = vaults[vaultId]; return item.locked; } }