Documentation Index
11 min readChapter 34

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:

  1. Lexing: Lexer converts .ql source text into a sequential vector of Token instances. If a character sequence violates lexical rules, lexing fails immediately to prevent generating parser errors on malformed tokens.
  2. Parsing: Parser builds a SourceUnit AST from tokens. If parsing fails or yields no contract definition (no contract found in this file), execution halts.
  3. Semantic Analysis: Analyzer verifies 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.
  4. Code Generation: CodeGenerator receives 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 KeywordInternal Type EnumDiagnostic NameEthereum ABI Type
numberType::U256numberuint256
truthType::Booltruthbool
addressType::Addressaddressaddress
textType::Texttextstring
nothingType::Voidnothingvoid

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: Version 1.
  • 00 00 00 02: 2 constants in the constant pool.
  • 00 00 00 10: 16 instructions in the code section.
  • 00 00 00 00: Execution starts at instruction index 0.

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 register rd, selecting registers 0–31)
  • kRs1Shift = 14 (5-bit source register 1 rs1)
  • kRs2Shift = 9 (5-bit source register 2 rs2)
  • kRegisterMask = 0x1F
  • kFunctMask = 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):

  1. R-Form (Register-Register): Used for three-register arithmetic and bitwise operations (Add, Sub, Mul, Div, And, Or, Xor, Shl, Shr, SLt, SGt, Eq).
  2. I-Form (Register-Immediate): Used for conditional branching, memory loading with immediate offsets, and loading constants (LoadI, LoadK, branch instructions).
  3. J-Form (Jump-Immediate): Used for un-targeted or long range jumps containing a 24-bit target instruction offset.
  4. 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 (0x01 shifted left 24): Opcode::Add
  • Destination Register (rd): Register r1
  • Source Register 1 (rs1): Register r2
  • Source Register 2 (rs2): Register r0

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:

  1. Revert (Opcode::Revert):

    • Cost: 0 additional 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 require assertions 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; }
  2. 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.
  3. 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:

  1. 4-Byte Selector Calculation:

    • balanceOf(address) hashes to selector 0x70a08231.
    • Ensure the Quorlin compiler emits signature parameters as address, not address translated to non-standard types.
  2. Return Value Compatibility:

    • Quorlin expects Type::U256 (number) to return a full 32-byte (256-bit) left-padded word from KVM memory.
  3. Storage Slot Conflicts:

    • Quorlin reads and updates contract state directly through the host state interface (StateHost::get_storage and StateHost::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.

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.         |
                                               +-----------------------------------+