Architecture

tsrun uses a register-based bytecode VM for efficient execution. This page describes the key architectural components.

Execution Pipeline

Source Lexer Parser AST Compiler Bytecode VM Result
RuntimeResult | +-- Complete(value) // Execution finished | +-- NeedImports([...]) // Waiting for module sources | +-- Suspended { // Async operation pending pending: [...], cancelled: [...] }

Register-Based VM

The VM uses registers instead of a stack, providing:

  • Fewer instructions - No push/pop overhead
  • Better cache locality - Registers are contiguous in memory
  • Efficient state capture - Easy to suspend/resume for async and generators

Each function call frame has up to 256 registers (u8 index). The compiler allocates registers for:

  • Local variables
  • Temporary values
  • Function arguments
  • Return values

Bytecode Instructions

The VM has 100+ instruction types:

Category Examples
Load/Store LoadConst, LoadLocal, StoreLocal, Move
Arithmetic Add, Sub, Mul, Div, Mod
Comparison Eq, Ne, Lt, Le, Gt, Ge
Control Flow Jump, JumpIfTrue, JumpIfFalse
Functions Call, Return, TailCall
Objects GetProperty, SetProperty, CreateObject
Arrays CreateArray, GetIndex, SetIndex
Async Await, Yield, CreatePromise

Value Types

JavaScript values are represented by the JsValue enum:

rust
pub enum JsValue {
    Undefined,
    Null,
    Boolean(bool),
    Number(f64),
    String(JsString),      // Rc<str> - cheap clone
    Object(Gc<JsObject>),  // GC-managed object
    Symbol(JsSymbol),
}

Object Types

Objects have an "exotic" type that determines special behavior:

Type Description
Ordinary Regular objects with properties
Array { length } Array with tracked length
Function { ... } Callable with environment
NativeFunction Rust/C callback
BoundFunction Function with bound this/args
Generator Generator state machine
Promise Promise with pending/fulfilled/rejected
Proxy Proxy with handler traps
Map, Set Collection types
Date Date with timestamp
RegExp Compiled regex

Garbage Collection

tsrun uses a mark-and-sweep garbage collector with a guard system for safe object references.

Guard System

Guards keep objects alive during GC. The pattern is:

rust
// Create guard BEFORE allocating
let guard = heap.create_guard();

// Guard existing values that might be collected
heap.guard_value_with(&guard, &existing_value);

// Now safe to allocate - GC won't collect guarded values
let new_obj = create_object(&guard);

Key Rules

  • Guard before allocate - GC runs during allocation
  • Return Guarded - Functions returning objects return Guarded to keep values alive
  • Guard scope in loops - Keep guards alive for the entire loop when collecting values

Collection Timing

GC runs when heap size exceeds a threshold (configurable via GC_THRESHOLD env var). Collection is triggered during object allocation.

Module System

ES modules use step-based loading:

  1. Parser encounters import statement
  2. Compiler records import request
  3. VM returns NeedImports with list of paths
  4. Host provides module sources via provide_module()
  5. VM parses, compiles, and links modules
  6. Execution continues

This design allows the host to load modules from any source: filesystem, network, embedded resources, or generated code.

Module Resolution

Import specifiers are resolved relative to the importing module's path:

typescript
// In /app/main.ts
import { util } from "./lib/util.ts";  // Resolves to /app/lib/util.ts
import { core } from "../core.ts";     // Resolves to /core.ts

Source Structure

File/Directory Purpose
src/lib.rs Public API - Interpreter, InterpreterConfig
src/lexer.rs Tokenizer
src/parser.rs Recursive descent + Pratt parsing
src/ast.rs AST node types
src/value.rs Runtime values, object model
src/gc.rs Garbage collector, Guard system
src/compiler/ Bytecode compiler
src/interpreter/ VM and builtins
src/ffi/ C FFI module