28. The Deployment Pipeline
The deployment pipeline in the Quorlin compiler toolchain transforms high-level source code written in Quorlin (.ql) into deterministic, binary-encoded bytecode executable on the Kortana Virtual Machine (KVM), along with an Ethereum-compatible JSON Application Binary Interface (ABI).
This pipeline guarantees that invalid contracts are rejected early during semantic analysis, protecting the execution environment from ill-formed instructions while maintaining complete inter-operability with existing Ethereum tooling.
+-----------------------------------------------------------------------+
| Quorlin Source (.ql) |
+-----------------------------------------------------------------------+
|
v
+-----------------------------------------------------------------------+
| 1. Lexical Analysis (Lexer) |
| Converts character stream into discrete Tokens. |
+-----------------------------------------------------------------------+
|
v
+-----------------------------------------------------------------------+
| 2. Parsing (Parser) |
| Transforms Token stream into an Abstract Syntax Tree (SourceUnit). |
+-----------------------------------------------------------------------+
|
v
+-----------------------------------------------------------------------+
| 3. Semantic Analysis (Analyzer) |
| Validates types, scope, builtins, and symbol bindings. |
+-----------------------------------------------------------------------+
|
+---------------------+---------------------+
| |
v v
+---------------------------+ +---------------------------+
| 4. Code Generation | | 5. ABI Emission |
| Generates KVM bytecode | | Generates standard |
| & resolves jumps. | | EVM JSON ABI. |
+---------------------------+ +---------------------------+
| |
v v
+-----------------------------------------------------------------------+
| 6. KVM Binary Serialization |
| Packs magic bytes, headers, constants, & instructions into payload. |
+-----------------------------------------------------------------------+
|
v
+-----------------------------------------------------------------------+
| 7. On-Chain Deployment & Execution |
| State Host instantiates contract & executes constructor. |
+-----------------------------------------------------------------------+
1. Overview of Compilation Phases
The compilation process is coordinated by the driver function compile(std::string_view source) defined in quorlin/compiler.cpp. The pipeline executes through distinct, strictly ordered phases. Failure in any phase instantly halts execution, reporting diagnostic errors before subsequent phases are executed.
// quorlin/compiler.cpp execution flow CompilationResult compile(std::string_view source) { CompilationResult result; // 1. Lexical Analysis Lexer lexer{source, result.diagnostics}; std::vector<Token> tokens = lexer.tokenize(); if (result.diagnostics.has_errors()) return result; // 2. 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; // 3. Semantic Analysis Analyzer analyzer{result.diagnostics}; const AnalysisResult analysis = analyzer.analyze(unit); if (result.diagnostics.has_errors()) return result; // 4. Code Generation & 5. ABI Generation // ... }
2. Phase 1: Lexical Analysis (Lexer)
The Lexer scans the raw Quorlin string source and breaks it into atomic standard tokens.
Identifier and Keyword Rules
Identifiers must begin with an ASCII letter (a-z, A-Z) or an underscore (_), followed by any combination of ASCII letters, digits (0-9), or underscores. The lexer categorizes reserved keywords, including:
- Declarations:
contract,record,interface,constructor,event - Function Modifiers:
reads(view/pure functions),writes(state-modifying functions) - Control Flow:
if,else,emit,indexed
Sample Source Code
Below is a typical Quorlin contract declaration processed by the pipeline:
contract TokenVault { number totalSupply; address owner; map<address, number> balances; event Deposit(address indexed sender, number amount); constructor { owner = caller; totalSupply = 1000; balances[caller] = 1000; } reads number getBalance(address account) { return balances[account]; } writes truth deposit(number amount) { require amount > 0, "invalid amount"; balances[caller] = balances[caller] + amount; totalSupply = totalSupply + amount; emit Deposit(caller, amount); return yes; } }
If the lexer encounters an unclosed string literal or an illegal character sequence, it appends an error to the DiagnosticBag and terminates compilation immediately.
3. Phase 2: Structural Parsing (Parser)
The Parser consumes the stream of Token instances generated by the Lexer and attempts to build an Abstract Syntax Tree (AST) rooted at a SourceUnit.
Source Unit Validation
The parser verifies that:
- Syntax grammar conforms to language specifications.
- The file contains a valid contract root (
unit.contract). - Variable declarations, storage declarations, and methods match required syntactic structures.
During parsing, language-native type identifiers are mapped to internal compiler constructs:
| Quorlin Keyword | Internal AST Type (Type) | Diagnostic Name |
|---|---|---|
number | Type::U256 | number |
truth | Type::Bool | truth |
address | Type::Address | address |
text | Type::Text | text |
| (Implicit) | Type::Void | nothing |
If structural errors occur (e.g., missing closing braces or malformed event declarations), diagnostic errors are generated, preventing further processing.
4. Phase 3: Semantic Analysis (Analyzer)
Semantic analysis is the main validation gate. As highlighted in quorlin/compiler.cpp:
"The important gate. The code generator does no checking of its own — it trusts that every identifier resolves and every type agrees, exactly as the KVM interpreter trusts module verification. Running it after a failed analysis would not produce a bad diagnostic; it would produce a bad module."
Analysis Tasks (sema.cpp)
- Symbol Resolution: Verifies that state variables, local variables, method parameters, and built-in contextual properties (such as
caller) are declared in scope. - Type Checking:
- Ensures arithmetic operations (
+,-,*,/,%) and ordering operations (<,<=,>,>=) operate strictly onnumber(Type::U256). - Ensures logical operations (
and,or) operate strictly ontruth(Type::Bool). - Ensures assignments match declared slot types.
- Ensures arithmetic operations (
- Record Field Checking: Validates struct/record field accesses. If a non-existent field is accessed, the analyzer inspects the record schema and presents available choices in diagnostic format (e.g.,
field `x` does not exist; did you mean `seller`, `price` and `active`?). - Interface and Signature Checking: Validates function mutability annotations (
readsvswrites) and verifies adherence to standard interfaces (such asIERC20).
5. Phase 4: Code Generation and Jump Resolution (CodeGenerator)
Once semantic integrity is guaranteed, the CodeGenerator transforms the analyzed AST into KVM machine instructions (kvm::Instruction).
KVM Instruction Formats
Instructions in KVM are represented using standard fixed-width 32-bit register formats:
R-Format: [ Opcode (8b) | Rd (5b) | Rs1 (5b) | Rs2 (5b) | Funct (9b) ]
I-Format: [ Opcode (8b) | Rd (5b) | Rs1 (5b) | Immediate (14b) ]
J-Format: [ Opcode (8b) | Immediate (24b) ]
Plain: [ Opcode (8b) | Unused (24b) ]
Emission Primitives (codegen.cpp)
The code generator relies on explicit builder functions to format instruction words:
// quorlin/codegen.cpp 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); Instruction plain(Opcode op);
Jump Label Resolution
Control structures (if/else conditions, loops) emit conditional jump instructions referencing unresolved labels. The code generator maintains a dynamic label fixup table:
size_t CodeGenerator::make_label() { labels_.push_back(SIZE_MAX); // Unplaced label sentinel return labels_.size() - 1; } void CodeGenerator::place(size_t label) { labels_[label] = code_.size(); // Resolve label to current instruction index }
Prior to final module construction, the fixup pass replaces sentinel label markers with resolved instruction address offsets.
6. Phase 5: ABI Export Generation (abi.cpp)
To enable seamless interactions from dApps, wallets, and standard Web3 client libraries (like ethers.js or web3.js), the deployment pipeline automatically generates a JSON ABI.
Type Translation Architecture
While diagnostics and language parsing display Quorlin human-readable syntax, the ABI generator maps all types directly to standard EVM ABI equivalents:
// quorlin/parser.cpp 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>"; }
JSON Construction (abi.cpp)
Function parameters and event fields are formatted using JSON escaping primitives:
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 + "}"; }
Example Output: Generated ABI Segment
For the Deposit event and deposit function in our TokenVault contract, the pipeline outputs:
[ { "type": "constructor", "inputs": [] }, { "type": "event", "name": "Deposit", "inputs": [ {"name": "sender", "type": "address", "internalType": "address", "indexed": true}, {"name": "amount", "type": "uint256", "internalType": "number", "indexed": false} ] }, { "type": "function", "name": "deposit", "inputs": [ {"name": "amount", "type": "uint256", "internalType": "number"} ], "outputs": [ {"name": "", "type": "bool", "internalType": "truth"} ], "stateMutability": "nonpayable" } ]
7. Phase 6: KVM Module Binary Layout (kvm/module.cpp)
Once instructions are generated and resolved, the executable program is packed into a binary payload called a KVM Module.
Binary Header Encoding
The module file uses big-endian encoding across all fixed-width binary fields:
| Byte Offset | Size (Bytes) | Field Description | Binary Type |
|---|---|---|---|
0x00 | 4 | Magic Signature (KVM\0) | 0x4B 0x56 0x4D 0x00 |
0x04 | 2 | Version Number | uint16_t (Big-Endian) |
0x06 | 4 | Constant Pool Entry Count | uint32_t (Big-Endian) |
0x0A | 4 | Instruction Vector Count | uint32_t (Big-Endian) |
0x0E | 4 | Code Entry Point Index | uint32_t (Big-Endian) |
0x12 | Variable | Constant Pool Table | 32-byte raw words |
| End Consts | Variable | Code Instruction Stream | 4-byte packed instruction words |
+-------------------------------------------------------------------------+
| MODULE HEADER |
+-------------------------------------------------------------------------+
| 0x00: Magic ("KVM\0") | 0x04: Version | 0x06: Consts | 0x0A: Instructions |
+-------------------------------------------------------------------------+
| 0x0E: Entry Point |
+-------------------------+
| CONSTANTS |
+-------------------------------------------------------------------------+
| 32-byte constant word 0 |
| 32-byte constant word 1 |
| ... |
+-------------------------------------------------------------------------+
| CODE |
+-------------------------------------------------------------------------+
| 4-byte encoded instruction 0 |
| 4-byte encoded instruction 1 |
| ... |
+-------------------------------------------------------------------------+
Low-Level Serialization Logic
The serialization mechanics in kvm/module.cpp write fields sequentially without dynamic padding:
void Module::serialize(Bytes& out) const { // Write 4-byte magic signature "KVM\0" out.push_back('K'); out.push_back('V'); out.push_back('M'); out.push_back('\0'); // Write metadata headers write_u16(out, version); write_u32(out, static_cast<uint32_t>(constants.size())); write_u32(out, static_cast<uint32_t>(instructions.size())); write_u32(out, entry_point); // Write 32-byte constant words for (const auto& word : constants) { const auto bytes = word.to_be_bytes(); out.insert(out.end(), bytes.begin(), bytes.end()); } // Write 4-byte instructions for (const auto& inst : instructions) { write_u32(out, encode_instruction(inst)); } }
8. Phase 7: On-Chain Deployment and State Execution
The compiled KVM Module binary is submitted within a contract creation transaction. On-chain instantiation is executed via kvm::StateHost interacting with Kortana's persistent WorldState trie.
Execution Initialization & Gas Scheduling
- Contract Initialization: The KVM interpreter loads the entry point specified in the module header and begins executing constructor logic.
- Gas Accounting (
kvm/gas.cpp): Every operation deducts gas according to predefined execution tiers:- Stop / Return / Revert:
kGasZero(0 gas) - ALU Operations (
Add,Sub,Eq,And,MLoad,MStore):kGasVeryLow - Multiplication / Division (
Mul,Div,Mod):kGasLow
- Stop / Return / Revert:
- State Operations (
kvm/state_host.cpp): During constructor execution, state writes use the state host wrapper:
Result<uint256_t> StateHost::set_storage(const Address& address, const uint256_t& key, const uint256_t& value) { // Obtains previous value to determine accurate gas pricing (e.g. fresh allocation vs overwrite) KORTANA_TRY_ASSIGN(const uint256_t previous, world_.get_storage(address, key)); KORTANA_TRY(world_.put_storage(address, key, value)); return previous; }
Once constructor execution completes cleanly without exceeding available gas limits or triggering a Revert instruction, the contract byte payload is committed to the node's unified global state trie at its deterministic deployed address.