Documentation Index
11 min readChapter 21

21. Compiler Internals: Bytecode Generation

This chapter details the internal architecture of the Quorlin compiler pipeline and the bytecode generation subsystem for the Kortana Virtual Machine (KVM). It explains how human-readable Quorlin source code (.ql) is converted into execution-ready KVM bytecode and standard Ethereum-compatible ABI metadata.


21.1 Overview of the Quorlin Compilation Pipeline

The Quorlin compiler processes source text through a strictly ordered, four-stage pipeline. Each stage isolates specific responsibilities to ensure that invalid or malformed contracts are rejected before bytecode is produced.

+-------------------------------------------------------------------+
|                        Source Code (.ql)                          |
+-------------------------------------------------------------------+
                                  |
                                  v
+-------------------------------------------------------------------+
| 1. Lexical Analysis (Lexer)                                       |
|    - Converts source characters into tokens                       |
|    - Enforces text length limits and keyword identification       |
+-------------------------------------------------------------------+
                                  |
                                  v
+-------------------------------------------------------------------+
| 2. Syntactic Analysis (Parser)                                    |
|    - Builds Abstract Syntax Tree (SourceUnit, ContractDecl)       |
|    - Verifies context-free grammar constraints                    |
+-------------------------------------------------------------------+
                                  |
                                  v
+-------------------------------------------------------------------+
| 3. Semantic Analysis (Analyzer / Sema)                            |
|    - Name resolution and type checking                            |
|    - Evaluates mutability rules (reads vs. writes)                |
|    - Maps standard interfaces and binary operator semantics       |
+-------------------------------------------------------------------+
                                  |
                                  v
+-------------------------------------------------------------------+
| 4. Code Generation & ABI Emission (CodeGenerator & ABI Emitter)   |
|    - Emits fixed-width 32-bit KVM ISA instructions                |
|    - Performs jump label fixups and control-flow resolution        |
|    - Outputs binary KVM executable module and JSON ABI            |
+-------------------------------------------------------------------+

The orchestration function kortana::quorlin::compile() controls this pipeline:

CompilationResult compile(std::string_view source) { CompilationResult result; // Stage 1: Lexical Analysis Lexer lexer{source, result.diagnostics}; std::vector<Token> tokens = lexer.tokenize(); if (result.diagnostics.has_errors()) return result; // Stage 2: Syntactic Parsing Parser parser{std::move(tokens), result.diagnostics}; SourceUnit unit = parser.parse(); if (result.diagnostics.has_errors()) return result; if (!unit.contract) { result.diagnostics.error({}, "no contract found in this file"); return result; } result.contract_name = unit.contract->name; // Stage 3: Semantic Analysis Analyzer analyzer{result.diagnostics}; const AnalysisResult analysis = analyzer.analyze(unit); if (result.diagnostics.has_errors()) return result; // Stage 4: Code Generation & Module Assembly CodeGenerator codegen{analysis, result.diagnostics}; result.module = codegen.generate(*unit.contract); result.abi = abi_json(*unit.contract, analysis); return result; }

Semantic integrity acts as a strict barrier. Code generation performs no type checking or symbol lookup of its own; it trusts that all identifiers and types have been fully resolved by the Analyzer. If semantic analysis produces any diagnostic errors, compilation aborts immediately to prevent the creation of corrupt or un-verifiable KVM bytecode modules.


21.2 KVM Instruction Set Architecture (ISA) & Encoding

The Kortana Virtual Machine (KVM) uses a 32-bit fixed-length register ISA. Unlike stack-based virtual machines (such as the standard Ethereum Virtual Machine), the KVM operates on 32 general-purpose registers (r0 through r31), reducing the memory overhead and stack-manipulation instructions (PUSH, DUP, SWAP) required to compute complex expressions.

21.2.1 Instruction Encoding Formats

Every KVM instruction is encoded into a single 32-bit unsigned integer (uint32_t). The interpreter decodes opcodes and operands according to one of four instruction formats defined in kvm/isa.cpp:

Format R (Register-Register):
+---------------+-----------+-----------+-----------+-------------------+
| Opcode (8b)   |  rd (5b)  | rs1 (5b)  | rs2 (5b)  |   funct (9b)      |
+---------------+-----------+-----------+-----------+-------------------+
 31           24 23       19 18       14 13        9 8                 0

Format I (Immediate):
+---------------+-----------+-----------+-------------------------------+
| Opcode (8b)   |  rd (5b)  | rs1 (5b)  |        Immediate (14b)        |
+---------------+-----------+-----------+-------------------------------+
 31           24 23       19 18       14 13                            0

Format J (Jump / Direct Immediate):
+---------------+-------------------------------------------------------+
| Opcode (8b)   |                    Immediate (24b)                    |
+---------------+-------------------------------------------------------+
 31           24 23                                                    0

Format None:
+---------------+-------------------------------------------------------+
| Opcode (8b)   |                    Unused (24b)                       |
+---------------+-------------------------------------------------------+
 31           24 23                                                    0

21.2.2 ISA Bitwise Parameters

Bit positions, shift distances, and bitmasks are defined deterministically in kvm/isa.cpp to ensure bit-level compatibility across compiler and runtime components:

ParameterBit Shift / ValueBit WidthDescription
kOpcodeShift248 bitsHigh-byte position containing the opcode byte
kRdShift195 bitsDestination register index ($r_0 - r_{31}$)
kRs1Shift145 bitsSource register 1 index ($r_0 - r_{31}$)
kRs2Shift95 bitsSource register 2 index ($r_0 - r_{31}$)
kRegisterMask0x1F5 bitsMask for 5-bit register numbers
kFunctMask0x1FF9 bitsExtended function modifier space for R-type instructions
kImm14Mask0x3FFF14 bitsSigned/Unsigned 14-bit immediate offset
kImm24Mask0xFFFFFF24 bitsDirect 24-bit jump address offset

21.3 The Code Generation Subsystem

The CodeGenerator class processes the semantic AST nodes into a flat stream of instructions, resolving branch targets and managing memory offsets.

21.3.1 Emission Helpers and Form Construction

The code generator uses internal helper functions to assemble instruction formats before adding them to the instruction vector:

// Form an R-type instruction: rd = rs1 op rs2 Instruction r_form(Opcode op, uint8_t rd, uint8_t rs1, uint8_t rs2 = 0, uint16_t funct = 0) { Instruction out; out.opcode = op; out.rd = rd; out.rs1 = rs1; out.rs2 = rs2; out.funct = funct; return out; } // Form an I-type instruction: rd = rs1 op imm14 Instruction i_form(Opcode op, uint8_t rd, uint8_t rs1, uint32_t imm) { Instruction out; out.opcode = op; out.rd = rd; out.rs1 = rs1; out.imm = imm; return out; } // Form a J-type instruction: jump imm24 Instruction j_form(Opcode op, uint32_t imm) { Instruction out; out.opcode = op; out.imm = imm; return out; }

21.3.2 Control Flow and Label Resolution

Because control-flow targets (such as if/else branches or loop bodies) cannot be known until child blocks are compiled, CodeGenerator provides a resolution mechanism:

size_t CodeGenerator::make_label() { // Labels start with SIZE_MAX as a placeholder sentinel labels_.push_back(SIZE_MAX); return labels_.size() - 1; } void CodeGenerator::place(size_t label) { if (label < labels_.size()) { labels_[label] = code_.size(); // Bind label to the current instruction offset } }

During post-processing via resolve_fixups(), jump instructions holding unresolved label indices are patched with final instruction offset targets.


21.4 ABI Generation and Type Mapping

Quorlin contracts interface with the outside world—including standard EVM tools, wallets, and standard Solidity contracts—by emitting standard Ethereum JSON ABI metadata.

21.4.1 Internal Types vs. External ABI Types

Quorlin uses natural, developer-friendly keywords for primitive types within the source language. During ABI emission (quorlin/abi.cpp and quorlin/parser.cpp), internal compiler types map directly to standard Ethereum ABI primitives:

Quorlin Internal Type (Type)Developer Type Keyword (type_name)Output Standard ABI Type (abi_type_name)EVM Storage Representation
Type::U256number"uint256"256-bit unsigned word
Type::Booltruth"bool"1-byte value (0x00 or 0x01)
Type::Addressaddress"address"20-byte address, left-zero-padded
Type::Texttext"string"UTF-8 dynamic byte array
Type::Voidnothing"void"Return value omission

21.4.2 JSON Serialization Engine

The ABI generator emits standard JSON representations for contract functions, constructors, and events.

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 + "}"; }

When compiling a contract method like reads number balanceOf(address owner), abi_json outputs the following JSON ABI fragment:

{ "type": "function", "name": "balanceOf", "inputs": [ { "name": "owner", "type": "address", "internalType": "address" } ], "outputs": [ { "name": "", "type": "uint256", "internalType": "number" } ], "stateMutability": "view" }

21.5 Binary Module Layout and Container Format

Once instructions are emitted and jump targets are resolved, the compiler packages the instruction stream, constant pool, and binary metadata into a .kvm module payload (kvm/module.cpp).

21.5.1 KVM Binary Container Structure

The byte order across all KVM binaries is Big-Endian. Header fields use fixed-width integer fields to guarantee unambiguous parsing across architectures:

        +-------------------------------------------------------+
Byte 00 | Magic Marker: "KVM\0" (4 bytes)                       |
        +-------------------------------------------------------+
Byte 04 | Version: uint16_t Big-Endian (2 bytes)               |
        +-------------------------------------------------------+
Byte 06 | Constant Pool Count: uint32_t Big-Endian (4 bytes)   |
        +-------------------------------------------------------+
Byte 10 | Instruction Count: uint32_t Big-Endian (4 bytes)    |
        +-------------------------------------------------------+
Byte 14 | Entry Point Index: uint32_t Big-Endian (4 bytes)     |
        +-------------------------------------------------------+
Byte 18 | Constant Pool (N * 32 bytes each)                    |
        | ...                                                   |
        +-------------------------------------------------------+
Offset  | Encoded Instructions (M * 4 bytes each)               |
        | ...                                                   |
        +-------------------------------------------------------+

21.5.2 Constants and Binary Encoding Logic

Constants are stored in a dedicated 256-bit word table to optimize instruction size. Large numeric literals or address constants are loaded by referencing their constant pool index using the LoadK opcode.

// Header writing logic from kvm/module.cpp constexpr size_t kHeaderSize = 18; constexpr size_t kConstantSize = 32; Bytes Module::serialize() const { Bytes out; out.reserve(kHeaderSize + constants.size() * kConstantSize + code.size() * sizeof(uint32_t)); // Magic bytes "KVM\0" out.push_back('K'); out.push_back('V'); out.push_back('M'); out.push_back('\0'); // Header fields written in Big-Endian format write_u16(out, version); write_u32(out, static_cast<uint32_t>(constants.size())); write_u32(out, static_cast<uint32_t>(code.size())); write_u32(out, entry_point); // Write 256-bit constants (32 bytes per word) for (const auto& c : constants) { const auto bytes = c.to_be_bytes(); out.insert(out.end(), bytes.begin(), bytes.end()); } // Write encoded 32-bit instructions for (const Instruction& inst : code) { write_u32(out, encode(inst)); } return out; }

21.6 End-to-End Walkthrough: Quorlin to KVM Bytecode

To observe how source constructs translate to target assembly and module layout, consider a standard token state transition implemented in Quorlin.

21.6.1 Source Code (TokenVault.ql)

contract TokenVault { number totalSupply; map<address, number> balances; event Transfer(address indexed from, address indexed to, number amount); constructor { totalSupply = 1000000; balances[caller] = 1000000; } reads number balanceOf(address account) { return balances[account]; } 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; emit Transfer(caller, recipient, amount); return yes; } }

21.6.2 Compilation Stage Mapping

  1. Lexing & Parsing:

    • number is recognized as standard compiler primitive Type::U256.
    • truth is recognized as standard compiler primitive Type::Bool.
    • yes is recognized as TokenKind::Yes (evaluates to constant boolean true).
    • caller maps to builtin instruction source Builtin::Caller.
  2. Semantic Verification:

    • reads enforces that balanceOf contains no state-modifying opcodes (SStore, Log).
    • writes permits state modifications (SStore, Log).
    • Binary operator - in senderBalance - amount is matched against is_arithmetic(), verifying both operands are Type::U256.
  3. Code Generation Pipeline Trace:

[Quorlin AST Element]           [Generated KVM Opcode Stream]
----------------------------------------------------------------------
number senderBalance = ...  =>  LoadK   r1, [Constant_0]  ; Read caller context
                                MStore  r2, r1            ; Stage balance key
                                SLoad   r3, r2            ; Load balance from storage

require senderBalance >= ...=>  Gte     r4, r3, r0        ; Compare amount in r0
                                JmpTrue r4, label_ok      ; Branch if valid
                                Revert  "insufficient balance"
label_ok:
balances[caller] = ...      =>  Sub     r5, r3, r0        ; Subtract amount
                                SStore  r2, r5            ; Save updated balance

return yes                  =>  LoadI   r0, 1             ; Load boolean true (yes)
                                Return  r0                ; Return to call context
  1. Module Container Packaging:
    • The constant pool receives numeric literal 1000000 (stored as a 32-byte Big-Endian 256-bit word).
    • Instructions are converted into 32-bit big-endian words via encode().
    • Header magic "KVM\0" (bytes 0x4B 0x56 0x4D 0x00) is prepended to output binary.

21.6.3 Generated KVM Register Map Example

During execution of the transfer method, the KVM allocates register frames according to function scope:

Register IndexVariable / Execution ValueLifecycle / Purpose
r0Method parameter amountArgument register
r1Method parameter recipientArgument register
r2Context built-in callerAddress of call sender
r3Local variable senderBalanceLoaded via SLoad slot
r4Relational truth conditionTransient check evaluated by require
r5Computed balance subtractionIntermediate register for SStore value

This design allows the Kortana Virtual Machine to achieve high execution efficiency, clear register-file traceability, and complete safety guarantees across the runtime stack.