Architecture

TinyLang is a complete language implementation featuring both a tree-walk interpreter and a bytecode compiler with virtual machine. This guide explains how each component works together.

Compilation Pipeline

TinyLang Architecture Diagram

TinyLang supports two execution paths:

  1. Interpreter path: Source → Lexer → Parser → AST → Tree-walk Interpreter → Output
  2. Compiler path: Source → Lexer → Parser → AST → Compiler → Optimizer → Bytecode → VM → Output

Lexer (Tokenizer)

The lexer (src/lexer/) converts raw source text into a stream of tokens. It handles:

  • Keywords: let, const, fn, class, if, else, while, for, in, return, match, when, import, from, new, this, extends, test, break, continue, and, or, not, true, false, null
  • Literals: Numbers (integers, floats), strings (single/double quoted)
  • Operators: +, -, *, /, %, **, ==, !=, <, >, <=, >=, =, +=, -=, *=, /=, =>, ..
  • Delimiters: (, ), {, }, [, ], ,, ., :, ;
  • Comments: Single-line (//)

Each token carries its source position (line and column), enabling precise error messages:

// Token structure
{
  type: TokenType,      // e.g. "NUMBER", "IDENTIFIER", "KEYWORD"
  value: string,       // raw text: "42", "let", "hello"
  line: number,         // 1-based line number
  column: number       // 1-based column number
}

Parser (Pratt Parsing)

The parser (src/parser/) uses a Pratt parser (top-down operator precedence) to convert tokens into an Abstract Syntax Tree (AST). This approach elegantly handles:

  • Operator precedence: 2 + 3 * 4 correctly parses as 2 + (3 * 4)
  • Associativity: a = b = c associates right-to-left
  • Prefix operators: -x, not y
  • Postfix/infix: Function calls, index access, property access
  • Mixed expressions: obj.method(args)[0]

Operator Precedence (low to high)

LevelOperatorsAssociativity
1=, +=, -=, *=, /=Right
2orLeft
3andLeft
4==, !=Left
5<, >, <=, >=Left
6.. (range)Left
7+, -Left
8*, /, %Left
9**Right
10not, - (unary)Prefix
11., [], ()Left

AST Node Types

The AST (src/types/ast.ts) represents every syntactic construct:

// Example AST for: let x = 2 + 3
{
  type: "VariableDeclaration",
  name: "x",
  mutable: true,
  initializer: {
    type: "BinaryExpression",
    operator: "+",
    left: { type: "NumberLiteral", value: 2 },
    right: { type: "NumberLiteral", value: 3 }
  }
}

Tree-Walk Interpreter

The interpreter (src/interpreter/) recursively visits each AST node and evaluates it directly. It uses an Environment class with parent chain for lexical scoping:

// Environment chain for closure support
class Environment {
  values: Map<string, RuntimeValue>
  parent: Environment | null

  get(name)    // Look up variable, walk parent chain
  set(name, v) // Set variable in current scope
  define(name) // Create new binding in current scope
}

Runtime Values

Values in the interpreter are represented as discriminated unions (src/types/values.ts):

Type TagValue Field
numbervalue: number
stringvalue: string
booleanvalue: boolean
null(no value field)
arrayelements: RuntimeValue[]
objectproperties: Map<string, RuntimeValue>
functionparams, body, closure (Environment)
native-functionfn: (args) => RuntimeValue
classname, methods, superClass
instanceclass, properties

Bytecode Compiler

The compiler (src/compiler/compiler.ts) translates AST nodes into a linear sequence of bytecode instructions stored in a Chunk.

Chunk Structure

// A compiled bytecode chunk
{
  code: Uint8Array,         // Bytecode instructions
  constants: RuntimeValue[], // Constant pool
  lines: number[]           // Source line mapping
}

Opcode Reference

TinyLang uses 63 bytecode instructions organized into categories:

Stack Operations

OpcodeHexDescription
CONST0x01Push a constant from the constant pool
POP0x02Pop the top of stack
DUP0x03Duplicate the top of stack
DUP20x04Duplicate the top two values, preserving their order
ROT0x05Move the deepest of the top N values to the top

Arithmetic Operations

OpcodeHexDescription
ADD0x10Pop two values, push their sum
SUB0x11Subtraction
MUL0x12Multiplication
DIV0x13Division
MOD0x14Modulo
POW0x15Exponentiation
NEGATE0x16Unary negation
COMPOUND0x17Apply a compound-assignment operator

Comparison Operations

OpcodeHexDescription
EQ0x20Equal
NEQ0x21Not equal
LT0x22Less than
LTE0x23Less than or equal
GT0x24Greater than
GTE0x25Greater than or equal

Logical Operations

OpcodeHexDescription
NOT0x30Logical NOT
AND0x31Logical AND
OR0x32Logical OR

Control Flow

OpcodeHexDescription
JMP0x40Unconditional jump
JMP_IF_FALSE0x41Jump if top of stack is falsy
JMP_IF_TRUE0x42Jump if top of stack is truthy
LOOP0x43Jump backwards (loop back)
JMP_IF_NULL0x44Jump if top of stack is null (used by ?. and ??)

Variable Access

OpcodeHexDescription
LOAD_LOCAL0x50Push local variable onto stack
STORE_LOCAL0x51Store top of stack in local
LOAD_GLOBAL0x52Push global variable onto stack
STORE_GLOBAL0x53Store top of stack in global
LOAD_UPVALUE0x54Load captured closure variable
STORE_UPVALUE0x55Store into captured closure variable
LOAD_ARGC0x56Push the number of arguments passed to this frame
DECLARE_GLOBAL0x57Bind a new global, rejecting a name already bound
DECLARE_CONST_GLOBAL0x58Declare a global that may never be reassigned

Functions

OpcodeHexDescription
CALL0x60Call function with N arguments
RETURN0x61Return from function
CLOSURE0x62Create closure capturing upvalues

Data Structures

OpcodeHexDescription
ARRAY0x70Create array from N stack elements
OBJECT0x71Create object from key-value pairs
INDEX0x72Read value at index/key
SET_INDEX0x73Set value at index/key
GET_PROP0x74Get object property
SET_PROP0x75Set object property
ARRAY_APPEND0x76Append top of stack to the array beneath it
ARRAY_SPREAD0x77Spread an array/string into the array beneath it
DESTRUCT_ELEM0x78Extract element N from the array on top (null if absent)
DESTRUCT_PROP0x79Extract a property from the object on top (null if absent)
INDEX_OPTIONAL0x7AIndex but yield null instead of throwing when out of range
GET_METHOD0x7BResolve a property as a callable method
CHECK_ITERABLE0x7CVerify the top of stack can be iterated by a for loop

Object-Oriented Programming

OpcodeHexDescription
CLASS0x80Define a class
METHOD0x81Define a method on a class
INHERIT0x82Set up inheritance
NEW_INSTANCE0x83Create new class instance
GET_THIS0x84Push this reference onto stack

Error Handling

OpcodeHexDescription
TRY_BEGIN0xA0Install a catch handler at the given address
TRY_END0xA1Uninstall the innermost catch handler
THROW0xA2Throw the value on top of the stack as an error
RAISE0xA3Raise a runtime error with the given message

I/O and Control

OpcodeHexDescription
PRINT0x90Print top of stack
HALT0xFFStop execution

Optimizer

The optimizer runs peephole optimizations on the bytecode before execution:

  • Constant folding: CONST 2; CONST 3; ADDCONST 5
  • Dead code elimination: Removes unreachable instructions after unconditional jumps
  • Redundant pop removal: Eliminates push-then-pop sequences
  • Jump threading: Collapses chains of jumps to their final target

Virtual Machine (VM)

The VM (src/vm/vm.ts) is a stack-based virtual machine that executes compiled bytecode:

VM Components

  • Value Stack: Holds operands and intermediate results
  • Call Frame Stack: Tracks function calls (return address, local variables offset)
  • Instruction Pointer (IP): Points to the next instruction to execute
  • Globals Table: Stores global variables (including stdlib functions)

Execution Example

// Source: print(2 + 3 * 4)
// Compiles to:
CONST 2         // Push 2        Stack: [2]
CONST 3         // Push 3        Stack: [2, 3]
CONST 4         // Push 4        Stack: [2, 3, 4]
MUL             // 3 * 4 = 12    Stack: [2, 12]
ADD             // 2 + 12 = 14   Stack: [14]
PRINT           // Output: 14    Stack: []
HALT            // Stop

Closure Implementation

Closures are implemented using upvalues. When a function captures a variable from an outer scope, the compiler generates LOAD_UPVALUE/STORE_UPVALUE instructions. The VM maintains upvalue references that can outlive their original stack frame.

Debugger

The debugger (src/debugger/) provides step-through execution with:

  • Breakpoints: Set at specific source lines, with optional conditions
  • Step/Step-into/Step-out: Fine-grained execution control
  • Variable inspection: View local variables at any point
  • Expression evaluation: Evaluate expressions in the current context
  • Source display: Shows current position in source code

Supporting Tools

Formatter

The formatter (src/formatter/) parses source code and re-emits it with consistent style. It handles indentation, spacing, line breaks, and trailing newlines based on configurable rules.

Linter

The linter (src/linter/) performs static analysis by walking the AST and checking for common issues:

  • prefer-const - Variables that could be const
  • no-unused-variables - Declared but never referenced variables
  • no-empty-blocks - Empty function/loop bodies
  • unreachable-code - Code after return/break
  • no-shadow - Variable shadowing in nested scopes

Test Runner

The test runner (src/testing/) executes test blocks and reports results. It provides assertion functions like expectToBe and tracks pass/fail status.

Module Loader

The module loader (src/modules/) resolves import paths, reads module files, parses them, and makes exported bindings available to the importing module.

Project Structure

src/
  lexer/        - Tokenization
  parser/       - Pratt parser, AST construction
  interpreter/  - Tree-walk evaluation
  compiler/     - Bytecode compilation & optimization
  vm/           - Stack-based virtual machine
  debugger/     - Interactive debugger
  formatter/    - Code formatter
  linter/       - Static analysis
  testing/      - Test runner
  modules/      - Module system & loader
  stdlib/       - Standard library (60+ functions)
  repl/         - Interactive REPL
  cli/          - Command-line interface
  types/        - AST nodes, tokens, runtime values
tests/          - Mirrors src/ structure
examples/       - 18 example programs
playground/     - Web IDE (CodeMirror + bundled TinyLang)
docs/           - Documentation site