Architecture
tsrun uses a register-based bytecode VM for efficient execution. This page describes the key architectural components.
Execution Pipeline
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:
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:
// 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
Guardedto 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:
- Parser encounters
importstatement - Compiler records import request
- VM returns
NeedImportswith list of paths - Host provides module sources via
provide_module() - VM parses, compiles, and links modules
- 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:
// 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 |