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.