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:
| Parameter | Bit Shift / Value | Bit Width | Description |
|---|---|---|---|
kOpcodeShift | 24 | 8 bits | High-byte position containing the opcode byte |
kRdShift | 19 | 5 bits | Destination register index ($r_0 - r_{31}$) |
kRs1Shift | 14 | 5 bits | Source register 1 index ($r_0 - r_{31}$) |
kRs2Shift | 9 | 5 bits | Source register 2 index ($r_0 - r_{31}$) |
kRegisterMask | 0x1F | 5 bits | Mask for 5-bit register numbers |
kFunctMask | 0x1FF | 9 bits | Extended function modifier space for R-type instructions |
kImm14Mask | 0x3FFF | 14 bits | Signed/Unsigned 14-bit immediate offset |
kImm24Mask | 0xFFFFFF | 24 bits | Direct 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::U256 | number | "uint256" | 256-bit unsigned word |
Type::Bool | truth | "bool" | 1-byte value (0x00 or 0x01) |
Type::Address | address | "address" | 20-byte address, left-zero-padded |
Type::Text | text | "string" | UTF-8 dynamic byte array |
Type::Void | nothing | "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
-
Lexing & Parsing:
numberis recognized as standard compiler primitiveType::U256.truthis recognized as standard compiler primitiveType::Bool.yesis recognized asTokenKind::Yes(evaluates to constant booleantrue).callermaps to builtin instruction sourceBuiltin::Caller.
-
Semantic Verification:
readsenforces thatbalanceOfcontains no state-modifying opcodes (SStore,Log).writespermits state modifications (SStore,Log).- Binary operator
-insenderBalance - amountis matched againstis_arithmetic(), verifying both operands areType::U256.
-
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
- 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"(bytes0x4B 0x56 0x4D 0x00) is prepended to output binary.
- The constant pool receives numeric literal
21.6.3 Generated KVM Register Map Example
During execution of the transfer method, the KVM allocates register frames according to function scope:
| Register Index | Variable / Execution Value | Lifecycle / Purpose |
|---|---|---|
r0 | Method parameter amount | Argument register |
r1 | Method parameter recipient | Argument register |
r2 | Context built-in caller | Address of call sender |
r3 | Local variable senderBalance | Loaded via SLoad slot |
r4 | Relational truth condition | Transient check evaluated by require |
r5 | Computed balance subtraction | Intermediate 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.