Compiler Overview
Compiler Overview
Section titled “Compiler Overview”Pipeline
Section titled “Pipeline” .nox source file │ │ Phase 1: Parsing (ANTLR 4) │ ├─ NoxLexer to Token stream │ └─ NoxParser to Parse tree (CST) │ │ Phase 2: AST Construction │ ├─ ASTBuilder (ANTLR Visitor) │ ├─ Desugaring (ParenExpr to inner, escape sequences) │ └─ SourceLocation attachment ▼ AST (Expr / Stmt / Decl hierarchy) │ │ Phase 2.5: Import Resolution │ ├─ Resolve import paths (relative to file) │ ├─ Validate namespaces (no Tier 0/1 collisions) │ ├─ Recursively parse + analyze imported files │ └─ Register import namespaces + assign global offsets │ │ Phase 3: Semantic Analysis │ ├─ Pass 1: Collect types and function signatures │ ├─ Pass 2: Resolve types in function bodies │ ├─ Pass 3: Validate expressions and control flow │ └─ Result: every Expr has .resolvedType set ▼ Annotated AST │ │ Phase 3.5: Constant Folding & Dead Branch Elimination │ ├─ Bottom-up expression folding (5+5 → 10) │ ├─ Unary folding (-5, !true) │ ├─ String concat folding ("a" + "b" → "ab") │ └─ Dead branch elimination (if(true) → inline then block) ▼ Optimized AST │ │ Phase 4: Code Generation │ ├─ Register allocation (liveness analysis) │ ├─ Bytecode emission (opcode selection) │ ├─ Constant pool construction │ ├─ Exception table generation │ ├─ Module metadata (per-module global offsets) │ └─ KILL_REF emission at scope exits ▼ CompiledProgram { bytecode: LongArray constantPool: Array<Any?> exceptionTable: Array<ExEntry> functions: Array<FuncMeta> modules: Array<ModuleMeta> }Phase Details
Section titled “Phase Details”Phase 1, 2: Parsing to AST
Section titled “Phase 1, 2: Parsing to AST”The ASTBuilder class implements ANTLR’s generated NoxParserVisitor<T> interface. Each visit* method converts a parse tree context into an AST node:
class ASTBuilder(private val fileName: String) : NoxParserBaseVisitor<Any>() {
override fun visitBinaryExpr(ctx: NoxParser.MulDivModExprContext): Expr { val left = visit(ctx.expression(0)) as Expr val right = visit(ctx.expression(1)) as Expr val op = mapBinaryOp(ctx) return BinaryExpr(left, op, right, locOf(ctx)) }
override fun visitIdentifierExpr(ctx: NoxParser.IdentifierExprContext): Expr = IdentifierExpr(ctx.Identifier().text, locOf(ctx))
// ParenExpr is NOT created, we just return the inner expression override fun visitParenExpr(ctx: NoxParser.ParenExprContext): Expr = visit(ctx.expression()) as Expr // Unwrap
private fun locOf(ctx: ParserRuleContext) = SourceLocation( file = fileName, line = ctx.start.line, column = ctx.start.charPositionInLine )}Phase 3: Semantic Analysis
Section titled “Phase 3: Semantic Analysis”Multiple passes over the AST, each using exhaustive when on sealed types:
Pass 1: Declaration Collection:
- Scan all
TypeDefs and build type registry (supports forward references for recursive structs) - Scan all
FuncDefs and build function registry - Scan all
GlobalVarDecls and register global variables
Pass 2: Type Resolution:
- Walk every expression in every function body
- Set
expr.resolvedTypeon everyExprnode - Resolve
IdentifierExpr.resolvedSymbolto variables/globals - Resolve
FuncCallExpr.resolvedFunctionto function definitions - Resolve
MethodCallExpr.resolution(UFCS vs type-bound vs namespace) - Validate struct literal fields match type definition
- Check null safety (null assigned only to nullable types)
- Validate
ascasts (target must be a struct type)
Pass 3: Control Flow Validation:
- Every code path in non-void functions returns a value
break/continueonly appear inside loops- Dead code detection (statements after
return/throw)
Phase 4: Code Generation
Section titled “Phase 4: Code Generation”Single pass over the annotated AST:
Register Allocation:
- Compute liveness intervals for all locals
- Assign registers (dual-bank:
pMemfor primitives,rMemfor references) - Reuse registers when lifetimes don’t overlap
- Record max register count per function frame size
Bytecode Emission:
- Exhaustive
whenonExprandStmtnodes - Opcode selection based on
resolvedType(e.g.,IADDvsDADD) - Super-instruction selection for json/struct property access
- Constant pool deduplication
- Forward-reference backpatching for jumps
Error Strategy
Section titled “Error Strategy”Principle: Collect errors, don’t fail fast.
Each phase collects as many errors as possible before stopping:
class CompilerErrors { private val errors = mutableListOf<CompilerError>()
fun report(loc: SourceLocation, message: String, suggestion: String? = null) { errors.add(CompilerError(loc, message, suggestion)) }
fun hasErrors(): Boolean = errors.isNotEmpty()}If Phase 3 finds errors, Phase 4 is never run. This gives the developer a full list of issues to fix in one pass.