Documentation Index
8 min readChapter 7

7. Functions and Closures

Functions are the executable building blocks of Quorlin smart contracts. Designed to look like Java and read like plain English, Quorlin function declarations enforce clear boundaries between state-modifying logic and read-only operations.

This chapter details the syntax, semantic categorization, ABI mapping, bytecode compilation, and low-level Kortana Virtual Machine (KVM) execution model for Quorlin functions. It also explores why dynamic closures are intentionally omitted from the language in favor of deterministic, statically verifiable execution.


7.1 Function Declarations and Mutability Modifiers

In Quorlin, every function must explicitly declare its side-effect behavior using one of two mutability keywords: reads or writes. This syntax eliminates ambiguity regarding whether a call modifies state on the Kortana blockchain.

contract TokenVault { number totalSupply; map<address, number> balances; constructor { totalSupply = 1000; balances[caller] = 1000; } // Read-only function: inspects contract state without mutating storage reads number balanceOf(address account) { return balances[account]; } // Mutating function: alters persistent storage or emits events writes truth transfer(address recipient, number amount) { number senderBalance = balances[caller]; require senderBalance >= amount, "insufficient balance"; balances[caller] = senderBalance - amount; balances[recipient] = balances[recipient] + amount; return yes; } }

7.1.1 reads Functions

A reads function indicates that the execution context only reads contract state (storage variables, maps, built-ins) or performs pure computation.

  • EVM Mapping: Maps to view or pure mutability (Mutability::View in quorlin/standard.hpp).
  • Gas & State Guarantees: A reads function cannot perform storage writes (sstore), emit events, or instantiate new contracts. Any internal attempt to mutate state triggers a semantic analysis error during compilation (quorlin/sema.cpp).

7.1.2 writes Functions

A writes function permits state modifications, storage writes, event emissions (emit), and balance updates.

  • EVM Mapping: Maps to state-modifying contract functions (Mutability::Mut).
  • Return Requirements: A writes function can return native primitive types like truth (yes/no), number, address, or nothing.

7.1.3 The constructor

The constructor is a specialized lifecycle block executed exactly once during contract instantiation.

  • It does not take reads or writes keywords.
  • It has no explicit return type or return statement.
  • Even if a contract omits parameters in its constructor, the Quorlin ABI generator (quorlin/abi.cpp) automatically emits a constructor entry in the JSON contract interface to ensure standard deployment tooling compatibility.

7.2 Type System and Return Values

Quorlin maps user-facing human-readable types to lower-level Kortana Virtual Machine types and standard Ethereum ABI representations:

Quorlin Type (quorlin/parser.cpp)KVM Internal (Type)Standard ABI Mapping (abi_type_name)Description
numberType::U256uint256Unsigned 256-bit integer
truthType::BoolboolBoolean (yes or no)
addressType::Addressaddress20-byte Kortana/EVM account address
textType::TextstringUTF-8 encoded string
nothingType::VoidvoidRepresents no return value

Functions returning nothing omit the return value or simply exit execution at the end of the block:

contract Logger { event LogNotice(text message); writes nothing notify(text message) { emit LogNotice(message); } }

If a function defines a non-void return type (such as number or truth), the Quorlin semantic analyzer (Analyzer::analyze) verifies that all execution paths culminate in a valid return statement returning a matching type.


7.3 Canonical Signatures, Selectors, and ABI Generation

When Quorlin functions are compiled into bytecode, they are exposed to external callers through 4-byte function selectors generated via Keccak-256 hashing.

Signature Normalization

Although a developer writes Quorlin syntax using number and truth, the compiler's ABI emitter (quorlin/abi.cpp and quorlin/parser.cpp) normalizes signatures into standard Ethereum-compatible forms using abi_type_name:

Quorlin Code:      reads truth isOwner(address user, number minBalance)
Canonical Form:    isOwner(address,uint256)
Keccak256 Hash:    0x2f802100...
4-Byte Selector:   0x2f802100

The function abi_type_name ensures standard EVM tooling (such as web3 libraries and wallets) can invoke Quorlin contracts without specialized adapters:

std::string_view abi_type_name(Type type) noexcept { switch (type) { case Type::U256: return "uint256"; case Type::Bool: return "bool"; case Type::Address: return "address"; case Type::Text: return "string"; case Type::Void: return "void"; case Type::Invalid: return "<invalid>"; } return "<unknown>"; }

The resulting ABI entry emitted by abi_json formats the parameters using parameter_json:

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

7.4 Scope, Local Variables, and Parameter Passing

Functions establish isolated execution frames. Variables declared within a function body exist strictly within that function's scope and mask higher-level identifier names if shadowed.

contract Escrow { address owner; constructor { owner = caller; } reads truth checkPayer(address owner) { // The parameter `owner` shadows the contract state variable `owner` return owner == caller; } }

Parameter Evaluation and Built-ins

When a function is called, arguments are passed by value onto the KVM register or stack frame. Inside the function body, built-in contextual properties are available automatically:

  • caller: The address invoking the current function frame.
  • value: The number of native tokens sent with the invocation.
contract Receiver { number totalReceived; writes truth acceptPayment() { require value > 0, "must deposit funds"; totalReceived = totalReceived + value; return yes; } }

7.5 Lowering Functions to KVM Bytecode

During compilation (quorlin/compiler.cpp), high-level Quorlin source code is transformed through four discrete phases:

  1. Lexing (lexer.cpp): Converts function definitions into token streams (Reads, Writes, Identifier, LeftParen, etc.).
  2. Parsing (parser.cpp): Constructs an Abstract Syntax Tree (AST) rooted in SourceUnit and ContractDeclaration.
  3. Semantic Analysis (sema.cpp): Validates argument counts, parameter types, state mutability rules, and return types.
  4. Code Generation (codegen.cpp): Emits KVM 32-bit fixed-width instructions.

KVM Register Formats for Function Code

The CodeGenerator emits KVM instructions using one of three structural formats defined in kvm/isa.cpp:

  • R-Form (Register-to-Register): Used for arithmetic, logic, and comparison operations (Add, Sub, Eq, Lt).
  • I-Form (Immediate): Used for constant loads and offset offsets (LoadI, jumps).
  • J-Form (Jump/Branch): Used for unconditional target branches.
Instruction r_form(Opcode op, uint8_t rd, uint8_t rs1, uint8_t rs2 = 0, uint16_t funct = 0); Instruction i_form(Opcode op, uint8_t rd, uint8_t rs1, uint32_t imm); Instruction j_form(Opcode op, uint32_t imm);

Module Entry Points

The final bytecode output forms a KVM module (kvm/module.cpp), structured with a precise 18-byte header followed by 32-byte constants and fixed-size instruction vectors:

0       4     magic "KVM\0"
4       2     version (big-endian)
6       4     constant count
10      4     instruction count
14      4     entry point (instruction index)
18      ...   constant table (32 bytes per entry)
...     ...   code section (4 bytes per instruction)

When an external call enters the contract, the KVM interpreter (kvm/interpreter.cpp) executes the dispatch table starting at the designated entry point, matching the 4-byte selector derived from the call payload against the contract's function signatures.


7.6 The Closure Paradox: Why Quorlin Omits Dynamic Closures

In traditional general-purpose languages (such as JavaScript, Python, or Rust), functions can form closures—anonymous dynamic functions that capture surrounding variables from their enclosing lexical scope at runtime.

Quorlin strictly prohibits higher-order functions, dynamic lambda definitions, and variable-capturing closures.

This design decision is governed by the structural constraints of the Kortana Virtual Machine and smart contract execution primitives:

1. Deterministic Gas Accounting and Static Verification

In the KVM gas model (kvm/gas.cpp), every instruction carries a deterministic gas cost (kGasVeryLow, kGasLow, etc.). Anonymous dynamic closures require heap-allocated heap frames (environment records) to retain captured variables. On-chain heap allocation introduces dynamic, non-deterministic memory consumption, creating unpredictable gas expansion costs during runtime execution.

2. Module Layout and Instruction Mechanics

KVM binary modules (kvm/module.cpp) are composed of fixed, statically size-checked instruction arrays indexed directly by an integer entry point:

constexpr size_t kHeaderSize = 18; constexpr size_t kConstantSize = 32;

Because instruction offsets are immutably baked into the binary header, function jump targets must be fully known at compile time. Dynamic anonymous dynamic closures would require executable code generation on the fly or heap function pointers—both of which violate KVM module verification rules (§32.1 resource bounds).

3. Reentrancy and State Security

Capturing dynamic references to contract storage or local frame buffers inside a runtime closure context creates potential hidden reentrancy vectors. By restricting functions strictly to explicit top-level signatures (reads and writes), the Quorlin static analyzer (sema.cpp) can enforce strict invariants on contract state access before code generation ever begins.


7.7 Comprehensive Example: Multi-Function Contract

The following complete contract demonstrates constructors, mutability modifiers (reads and writes), return types, explicit parameter handling, and event emissions:

contract StakingPool { address poolOwner; number rewardRate; map<address, number> stakedBalances; event Staked(address indexed user, number amount); event Withdrawn(address indexed user, number amount); constructor { poolOwner = caller; rewardRate = 5; } reads address getOwner() { return poolOwner; } reads number getStake(address account) { return stakedBalances[account]; } writes truth stake(number amount) { require amount > 0, "cannot stake zero"; stakedBalances[caller] = stakedBalances[caller] + amount; emit Staked(caller, amount); return yes; } writes truth unstake(number amount) { number currentStake = stakedBalances[caller]; require currentStake >= amount, "insufficient stake"; stakedBalances[caller] = currentStake - amount; emit Withdrawn(caller, amount); return yes; } }