34. Debugging KVM Execution
Debugging a smart contract requires visibility across both high-level language abstractions and low-level runtime execution. In the Kortana ecosystem, Quorlin code compiles directly into binary modules executed by the Kortana Virtual Machine (KVM).
When execution fails—whether due to a compile-time semantic error, an out-of-gas exception, or a reverted transaction—you must trace the failure back through the multi-stage compilation pipeline or inspect the KVM state host directly.
34.1 The Quorlin-to-KVM Compilation Pipeline
Before an execution bug can be diagnosed in KVM bytecode, you must understand how Quorlin code transitions through the four-stage compilation process implemented in quorlin/compiler.cpp:
+------------------+ +------------------+ +-------------------+ +-------------------+
| 1. Lexer | --> | 2. Parser | --> | 3. Analyzer | --> | 4. Code Generator|
| Source -> Tokens| | Tokens -> AST | | Sema & Typecheck | | AST -> Bytecode |
+------------------+ +------------------+ +-------------------+ +-------------------+
The pipeline operates with absolute gating. If diagnostics record errors at any phase, downstream processing halts:
- Lexing:
Lexerconverts.qlsource text into a sequential vector ofTokeninstances. If a character sequence violates lexical rules, lexing fails immediately to prevent generating parser errors on malformed tokens. - Parsing:
Parserbuilds aSourceUnitAST from tokens. If parsing fails or yields no contract definition (no contract found in this file), execution halts. - Semantic Analysis:
Analyzerverifies scope, variable declarations, records, and binary operator rules. Analysis acts as the primary compilation gate. Code generation is strictly prevented from running on a failed analysis to guarantee that malformed ASTs never produce invalid binary modules. - Code Generation:
CodeGeneratorreceives a clean AST and emits KVM instructions, places labels, and resolves jump fixups.
When debugging a deployment failure, check which stage failed first. Diagnostic bags accumulate errors with precise source spans.
34.2 Compiler Diagnostic Pipeline & Gating
Semantic analysis failures represent the majority of compile-time bugs. The Quorlin analyzer evaluates variable mutability, valid type usage, and field access on custom records.
Record Field Mismatches
When accessing non-existent fields on a record, the semantic analyzer formats diagnostic output to list available fields clearly rather than merely repeating the missing identifier back to the developer.
Consider the following erroneous Quorlin code:
contract Escrow { record Trade { address seller; number price; truth active; } map<number, Trade> trades; reads number getTradeAmount(number tradeId) { Trade item = trades[tradeId]; // Error: 'amount' does not exist on record 'Trade' return item.amount; } }
During analysis (quorlin/sema.cpp), the analyzer formats a field list suggestion using natural phrasing:
Error: field `amount` does not exist on record `Trade`. Did you mean one of: `seller`, `price` and `active`?
Type Name Equivalence vs. ABI Equivalence
A common source of confusion during debugging is the distinction between Quorlin's user-facing type names and the standard ABI types generated for external interfaces (quorlin/parser.cpp, quorlin/abi.cpp):
| Quorlin Keyword | Internal Type Enum | Diagnostic Name | Ethereum ABI Type |
|---|---|---|---|
number | Type::U256 | number | uint256 |
truth | Type::Bool | truth | bool |
address | Type::Address | address | address |
text | Type::Text | text | string |
nothing | Type::Void | nothing | void |
If standard tools fail to call a function on your deployed Quorlin contract, verify the function signatures in the generated ABI JSON. Function selectors derived for EVM/KVM cross-calling compute hashes over external standard signatures (e.g., transfer(address,uint256) rather than transfer(address,number)).
34.3 Decoding the KVM Bytecode Module Layout
When a Quorlin contract compiles successfully, CodeGenerator produces a serialized binary .kvm module structured according to kvm/module.cpp.
Modules use strict big-endian byte ordering across all fixed-width fields. Decoding a raw KVM module binary header requires reading an initial 18-byte structure:
Offset (Bytes) Size (Bytes) Field Description
-----------------------------------------------------------------
0 4 Magic Identifier ("KVM\0")
4 2 Module Format Version (u16)
6 4 Constant Pool Count (u32)
10 4 Instruction Count (u32)
14 4 Entry Point Index (u32)
18 C * 32 Constant Pool Entries (32 bytes each)
18 + (C * 32) I * 4 Instruction Stream (4 bytes each)
Module Header Decoding Example
If you inspect a compiled .kvm file with a hex viewer:
4B 56 4D 00 00 01 00 00 00 02 00 00 00 10 00 00 00 00
Breaking down the 18-byte header (kHeaderSize = 18):
4B 56 4D 00: Magic bytes"KVM\0".00 01: Version1.00 00 00 02:2constants in the constant pool.00 00 00 10:16instructions in the code section.00 00 00 00: Execution starts at instruction index0.
Following the header, the decoder expects 2 * 32 = 64 bytes of constant pool data followed by 16 * 4 = 64 bytes of 32-bit instructions. Corrupted file sizes or invalid magic bytes cause the KVM module loader to immediately reject execution.
34.4 Instruction Formats and Register Decoding
Unlike stack-based virtual machines (like the Ethereum Virtual Machine), the Kortana Virtual Machine (KVM) operates on a register-based architecture (kvm/isa.cpp). All KVM instructions are encoded into fixed 32-bit (4-byte) words.
Register Bitfield Layout
Instructions are split into distinct bitfields mapped using shifts and masks defined in kvm/isa.cpp:
31 24 23 19 18 14 13 9 8 0 +----------------+------------+------------+------------+---------------------+ | Opcode (8-bit) | Rd (5-bit)| Rs1 (5-bit)| Rs2 (5-bit)| Funct (9-bit) | R-Form +----------------+------------+------------+------------+---------------------+ | Opcode (8-bit) | Rd (5-bit)| Rs1 (5-bit)| Immediate (14-bit) | I-Form +----------------+------------+-----------------------------------------------+ | Opcode (8-bit) | Immediate (24-bit) | J-Form +----------------+------------------------------------------------------------+
The bit shifts and masks governing this architecture are:
kOpcodeShift = 24(8-bit opcode)kRdShift = 19(5-bit destination registerrd, selecting registers 0–31)kRs1Shift = 14(5-bit source register 1rs1)kRs2Shift = 9(5-bit source register 2rs2)kRegisterMask = 0x1FkFunctMask = 0x1FF(9-bit function modifier)kImm14Mask = 0x3FFF(14-bit immediate field)kImm24Mask = 0xFFFFFF(24-bit jump offset field)
Instruction Formats
Depending on the opcode, the KVM instruction decoder interprets the remaining 24 bits in one of four formats (Format enum):
- R-Form (Register-Register): Used for three-register arithmetic and bitwise operations (
Add,Sub,Mul,Div,And,Or,Xor,Shl,Shr,SLt,SGt,Eq). - I-Form (Register-Immediate): Used for conditional branching, memory loading with immediate offsets, and loading constants (
LoadI,LoadK, branch instructions). - J-Form (Jump-Immediate): Used for un-targeted or long range jumps containing a 24-bit target instruction offset.
- None (Plain): Used for zero-operand control signals (
Stop,Ret,Invalid).
Decoding Example: R-Form Addition
Suppose a disassembly step exposes the instruction byte sequence 0x01 0x08 0x80 0x00.
Converting to binary:
- Opcode (
0x01shifted left 24):Opcode::Add - Destination Register (
rd): Registerr1 - Source Register 1 (
rs1): Registerr2 - Source Register 2 (
rs2): Registerr0
If execution produces unexpected output, verify register allocations generated in quorlin/codegen.cpp.
34.5 Gas Tracing and Runtime Halt Conditions
During KVM execution, the interpreter enforces resource limits via gas metering (kvm/gas.cpp). Every instruction consumes a specific base cost from the remaining gas allowance before execution proceeds.
Gas Schedule Hierarchy
+---------------------------------------------------------------------------+
| Free Opcodes (Base Cost: 0 Gas) |
| Opcode::Stop, Opcode::Return, Opcode::Revert |
+---------------------------------------------------------------------------+
| Very Low Cost Opcodes (kGasVeryLow = 3 Gas) |
| Add, Sub, Lt, Gt, SLt, SGt, Eq, IsZero, And, Or, Xor, Not, Byte, Shl, Shr |
| Mov, LoadK, LoadI, MLoad, MStore, MStore8, CallDataLoad |
+---------------------------------------------------------------------------+
| Low Cost Opcodes (kGasLow = 5 Gas) |
| Mul, Div, SDiv, Mod, SMod |
+---------------------------------------------------------------------------+
| Gas Drain Exceptions |
| Opcode::Invalid -> Base Cost 0, Drains ALL remaining gas |
+---------------------------------------------------------------------------+
Diagnosing Program Failure Modes
When an execution halts prematurely, analyze the resulting CallResult status:
-
Revert (
Opcode::Revert):- Cost:
0additional gas base cost beyond state/memory charges incurred up to the failure point. - Behavior: Unwinds state mutations made in the current call context. Unspent gas is refunded to the caller.
- Common Cause: Quorlin
requireassertions failing at runtime.
writes truth withdraw(number amount) { number balance = balances[caller]; // Reverts if balance < amount, preserving remaining gas require balance >= amount, "Insufficient balance"; balances[caller] = balance - amount; return yes; } - Cost:
-
Invalid Opcode / Illegal Execution (
Opcode::Invalid):- Cost: Drains 100% of remaining transaction gas.
- Behavior: Halts execution immediately, marks context as failed, and provides no gas refund.
- Common Cause: Executing unplaced labels, corrupted bytecode jumps, or invalid instruction encodings.
-
Out of Gas:
- Behavior: Triggers when the active gas pool falls below the opcode base cost + dynamic memory/storage expanding costs. Triggers an implicit context failure.
34.6 Edge Cases in Arithmetic and Memory Operations
Debugging KVM execution requires an understanding of how numeric operations and memory bounds are checked in kvm/arith.cpp and kvm/interpreter.cpp.
Signed 256-Bit Integer Overflow (INT_MIN / -1)
Quorlin relies on 256-bit signed arithmetic helpers (kvm/arith.cpp) for signed comparison and division operations (SDiv, SMod, SLt, SGt).
A critical arithmetic edge case occurs when dividing INT_MIN (the most negative signed 256-bit integer, 0x8000...0000) by -1 (0xFFFF...FFFF). In standard two's-complement arithmetic, the quotient ($2^{255}$) cannot be represented in a signed 256-bit slot.
Rather than panicking or crashing the underlying host node process, KVM explicitly catches this condition inside signed_div:
// Excerpt from kvm/arith.cpp handling INT_MIN / -1 const uint256_t minimum = int_min(); if (a == minimum && b == ~uint256_t::zero()) { return minimum; // Returns INT_MIN without faulting }
If your contract performs signed calculations, be aware that INT_MIN / -1 returns INT_MIN in compliance with EVM overflow specifications.
Memory Offset Narrowing and Bounds Safety
KVM handles memory addresses using 256-bit words, but physical node hardware addresses memory using 64-bit pointers. In kvm/interpreter.cpp, memory access parameters (like offsets and lengths supplied to MLoad or MStore) are safely narrowed from 256-bit values to host size types (size_t).
To prevent integer wraparound exploits (e.g., passing $2^{64} + 8$ to bypass bounds checks), the interpreter rejects offset values that exceed host hardware bounds before calculating memory growth gas fees.
If a contract transaction fails with an out-of-gas error during a memory operation, verify that array indices or offset calculations are not overflowing $2^{64}-1$.
34.7 ABI Selector Mismatches and Interoperability Debugging
Quorlin smart contracts interact with state and standard external interfaces via the StateHost abstraction (kvm/state_host.cpp). External contract calls execute via ICallDispatcher::dispatch.
When cross-calling between Solidity and Quorlin contracts, ensure interface method names and parameter types match standard definitions (quorlin/standard.cpp).
Interface Match Debugging Checklist
Consider this standard Quorlin contract calling an ERC-20 token interface:
interface IERC20 { reads number balanceOf(address account); writes truth transfer(address recipient, number amount); } contract TokenVault { address tokenAddress; constructor { tokenAddress = caller; } reads number checkVaultBalance() { IERC20 token = IERC20(tokenAddress); // Dispatching call through StateHost return token.balanceOf(caller); } }
If token.balanceOf(caller) returns zero unexpected or fails, trace the call through these failure vectors:
-
4-Byte Selector Calculation:
balanceOf(address)hashes to selector0x70a08231.- Ensure the Quorlin compiler emits signature parameters as
address, notaddresstranslated to non-standard types.
-
Return Value Compatibility:
- Quorlin expects
Type::U256(number) to return a full 32-byte (256-bit) left-padded word from KVM memory.
- Quorlin expects
-
Storage Slot Conflicts:
- Quorlin reads and updates contract state directly through the host state interface (
StateHost::get_storageandStateHost::set_storage). State Host maps contract storage to Kortana's unified state trie (kvm/state_host.cpp). - If storage values appear corrupted after execution, inspect whether slot index calculations match the record alignment expected by the compiler.
- Quorlin reads and updates contract state directly through the host state interface (
Summary Troubleshooting Flowchart
Use the following flowchart to isolate issues encountered during Quorlin development and KVM execution:
+-----------------------------+
| Execution Issue Observed |
+-----------------------------+
|
Is error at compile time?
/ \
YES NO
/ \
+------------------------------+ +-------------------------------+
| Inspect Diagnostics Bag | | Trace KVM Interpreter |
| 1. Lexer (Syntax) | | 1. Check Return/Revert status |
| 2. Parser (Grammar) | | 2. Verify Remaining Gas |
| 3. Analyzer (Types/Records) | | 3. Check Binary Header Offset |
+------------------------------+ +-------------------------------+
|
Did transaction revert?
/ \
YES NO (Invalid / Out of Gas)
/ \
+-----------------------------------+ +-----------------------------------+
| Trace `require` assertions | | Check for illegal jump labels, |
| and explicit state revert paths. | | memory bounds overflow (>2^64), |
+-----------------------------------+ | or exhausted gas budgets. |
+-----------------------------------+