Documentation Index
8 min readChapter 20

20. Compiler Internals: Semantic Analysis

Semantic analysis is the critical third phase of the Quorlin compilation pipeline. Operating directly between abstract syntax tree (AST) construction and low-level Kortana Virtual Machine (KVM) code generation, the semantic analyzer (kortana::quorlin::Analyzer) validates that a syntactically correct source unit conforms strictly to the type system, scoping rules, mutability constraints, and language invariants of Quorlin.

       +-------------------+
       |   Source Code     |
       +-------------------+
                 |
                 v
       +-------------------+
       |   1. Lexer        |
       +-------------------+
                 |  Token Stream
                 v
       +-------------------+
       |   2. Parser       |
       +-------------------+
                 |  SourceUnit (AST)
                 v
  +-----------------------------+
  |  3. Semantic Analyzer       |  <--- [Chapter 20 Focus]
  |     (kortana::quorlin::     |       Verifies types, symbols,
  |      Analyzer)              |       mutability, and invariants
  +-----------------------------+
                 |  AnalysisResult
                 v
       +-------------------+
       | 4. Code Generator |
       +-------------------+
                 |  KVM Bytecode
                 v
       +-------------------+
       |   KVM Executable  |
       +-------------------+

20.1 Semantic Analysis in the Pipeline

As implemented in quorlin/compiler.cpp, semantic analysis acts as the non-negotiable gatekeeper of the compiler. The AST generated by the Parser merely proves that input tokens satisfy Quorlin's grammar rules. It does not guarantee that variables exist, that types match across assignments and arithmetic expressions, or that storage mutability rules are respected.

// Excerpt from quorlin/compiler.cpp CompilationResult compile(std::string_view source) { CompilationResult result; // --- 1. Lex --- Lexer lexer{source, result.diagnostics}; std::vector<Token> tokens = lexer.tokenize(); if (result.diagnostics.has_errors()) return result; // --- 2. Parse --- 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. Analyse --- Analyzer analyzer{result.diagnostics}; const AnalysisResult analysis = analyzer.analyze(unit); // 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. if (result.diagnostics.has_errors()) return result; // --- 4. Codegen --- // ... }

The downstream CodeGenerator assumes that the input AST and its corresponding AnalysisResult are entirely free of structural, type, and identifier errors. If code generation were allowed to execute after a semantic failure, it would produce an invalid KVM bytecode module rather than helpful diagnostic messages.


20.2 Type System and Mapping

Quorlin's design goal is to present a clean surface syntax using English words, while compiling down to standard 256-bit EVM/KVM primitives and generating Ethereum-compatible ABIs. The semantic analyzer is responsible for bridging these representations.

Surface Types vs. Internal Types vs. ABI Types

The semantic analyzer processes five core surface types defined in quorlin/parser.cpp:

Quorlin KeywordInternal Representation (Type)ABI Type (abi_type_name)Description
numberType::U256uint256Unsigned 256-bit integer
truthType::BoolboolBoolean (yes or no)
addressType::Addressaddress20-byte account address
textType::TextstringDynamic string (up to kMaxTextBytes)
nothingType::VoidvoidReturn type for void methods
// Excerpt from quorlin/parser.cpp std::string_view type_name(Type type) noexcept { switch (type) { case Type::U256: return "number"; case Type::Bool: return "truth"; case Type::Address: return "address"; case Type::Text: return "text"; case Type::Void: return "nothing"; case Type::Invalid: return "<invalid>"; } return "<unknown>"; } 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>"; }

When generating human-readable diagnostic messages, the analyzer invokes type_name() to report errors using the programmer-facing Quorlin syntax (e.g., "expected number, found truth"). When binding function signatures for external interop or standard contract interfaces, it translates types using abi_type_name().


20.3 Symbol Resolution and Scope Management

During semantic analysis, symbol resolution validates every identifier reference against active variable declarations, storage layout, built-in context primitives, records, and events.

contract Vault { number totalBalance; map<address, number> userBalances; event Deposit(address indexed sender, number amount); writes truth deposit() { number amount = caller; // SEMANTIC ERROR: caller is an 'address', assigned to 'number' userBalances[caller] = userBalances[caller] + amount; emit Deposit(caller, amount); return yes; } }

Symbol Hierarchy

Resolution occurs in a strict scoping hierarchy:

  1. Local Variables & Parameters: Identifiers declared inside the current function block or parameter list.
  2. Contract Storage Fields: State variables declared at contract scope (e.g., totalBalance, userBalances).
  3. Built-in Symbols: Context variables provided directly by the Kortana execution environment (quorlin/parser.cpp):
    • caller -> Resolves to Type::Address (address of the immediate message sender).
    • yes / no -> Boolean literals resolving to Type::Bool.
  4. User-Defined Records & Interfaces: Custom structures and external contract interfaces declared globally or imported.

If an identifier cannot be resolved across any active scope, the analyzer records an error in the DiagnosticBag indicating an undeclared identifier.


20.4 Operator Semantics and Validation

quorlin/sema.cpp implements semantic rules for all binary and unary operations supported by the language. Operators are grouped into functional categories:

// Excerpt from quorlin/sema.cpp [[nodiscard]] bool is_arithmetic(BinaryOp op) noexcept { switch (op) { case BinaryOp::Add: case BinaryOp::Sub: case BinaryOp::Mul: case BinaryOp::Div: case BinaryOp::Mod: case BinaryOp::AddWrap: case BinaryOp::SubWrap: case BinaryOp::MulWrap: case BinaryOp::BitAnd: case BinaryOp::BitOr: case BinaryOp::BitXor: case BinaryOp::ShiftLeft: case BinaryOp::ShiftRight: return true; default: return false; } } [[nodiscard]] bool is_ordering(BinaryOp op) noexcept { return op == BinaryOp::Less || op == BinaryOp::LessEqual || op == BinaryOp::Greater || op == BinaryOp::GreaterEqual; } [[nodiscard]] bool is_logical(BinaryOp op) noexcept { return op == BinaryOp::LogicalAnd || op == BinaryOp::LogicalOr; }

Rule Rules by Operator Category

  1. Arithmetic Operators (+, -, *, /, %, +#, -#, *#, &, |, ^, <<, >>):
    • Both left and right operands must evaluate to Type::U256 (number).
    • Standard arithmetic operators (+, -, *) are checked for overflow safety at runtime.
    • Explicit wrapping operators (+#, -#, *#) explicitly perform modulo $2^{256}$ wrapping arithmetic.
  2. Ordering Operators (<, <=, >, >=):
    • Both operands must evaluate to Type::U256 (number).
    • Comparisons between address or truth values using magnitude ordering are rejected at compile time.
  3. Logical Operators (and, or):
    • Both operands must evaluate to Type::Bool (truth).
  4. Equality Operators (==, !=):
    • Both operands must share identical types (number vs number, address vs address, truth vs truth).

20.5 Field Resolution and Diagnostic Precision

When resolving field access on custom records, the semantic analyzer verifies that the target identifier represents a defined field within that record's declaration. If a field name is misspelled, the analyzer generates a precise context-aware error message listing all valid fields for that record.

// Excerpt from quorlin/sema.cpp [[nodiscard]] std::string field_list(const RecordInfo& record) { std::string listed; for (size_t i = 0; i < record.order.size(); ++i) { if (i > 0) listed += (i + 1 == record.order.size()) ? " and " : ", "; listed += "`" + record.order[i] + "`"; } return listed; }

Example: Misspelled Field Resolution

Consider a contract attempting to access an invalid field on a custom record:

record Listing { address seller; number price; truth active; } contract Marketplace { Listing currentListing; reads number getPrice() { return currentListing.cost; // ERROR: 'cost' is not a field of 'Listing' } }

During analysis, field lookup for cost fails against RecordInfo. Rather than reporting an unhelpful generic error, field_list constructs a human-readable message detailing the allowed properties:

error: 'Listing' has no field 'cost'; available fields are `seller`, `price` and `active`

20.6 Mutability Verification

Quorlin enforces state mutability at compile time using function modifiers:

  • reads: Declares a read-only (view) function.
  • writes: Declares a state-modifying function.

The semantic analyzer maintains mutability context flags while traversing function bodies:

contract Store { number data; reads number getData() { return data; // Permitted: reading state } reads nothing setData(number newValue) { data = newValue; // SEMANTIC ERROR: Cannot write to storage in a 'reads' function } }

When analyzing a reads function, the Analyzer prohibits:

  1. Writing or assigning to any contract storage state variable.
  2. Emitting events (emit).
  3. Calling writes functions on other external contracts.

20.7 Standard Interface Binding

As defined in quorlin/standard.cpp, Quorlin includes standard ERC/EIP interface definitions natively built into the compiler context. During semantic analysis, external contract calls or interface bindings are checked against these canonical definitions.

// Excerpt from quorlin/standard.cpp const std::vector<StandardInterface>& standard_interfaces() { static const std::vector<StandardInterface> interfaces = [] { std::vector<StandardInterface> built; // --- EIP-20 --- built.push_back(StandardInterface{ "IERC20", { {"totalSupply", "totalSupply()", {}, Type::U256, Mutability::View}, {"balanceOf", "balanceOf(address)", {Type::Address}, Type::U256, Mutability::View}, {"transfer", "transfer(address,uint256)", {Type::Address, Type::U256}, Type::Bool, Mutability::Mut}, {"allowance", "allowance(address,address)", {Type::Address, Type::Address}, Type::U256, Mutability::View}, } }); return built; }(); return interfaces; }

When a Quorlin contract interacts with an external token balance via an IERC20 interface reference, the analyzer verifies the function selector arguments and return types against the canonical signature strings ("transfer(address,uint256)").

contract PaymentProcessor { writes truth pay(address token, address recipient, number amount) { IERC20 tokenContract = IERC20(token); // Semantic Analyzer checks function call signature against IERC20 definition: // transfer(address, uint256) -> truth truth success = tokenContract.transfer(recipient, amount); require success, "transfer failed"; return yes; } }

By completing type checking, storage protection, operator enforcement, and symbol resolution during semantic analysis, the Quorlin compiler guarantees that downstream code generation (CodeGenerator) operates exclusively on validated, type-safe, and unambiguous program structures.