Architecture

Langoost is a bytecode-compiled interpreter written in Go. Each script runs through a small, predictable pipeline.

Source text
    ↓  Lexer      (token/lexer.go)
Token stream
    ↓  Parser     (parser/parser.go)
AST
    ↓  Compiler   (compiler/compiler.go)
Bytecode chunk
    ↓  VM         (vm/vm.go)
Result

Bytecode

Instructions are fixed-width 4-byte (uint32) words. The top byte is the opcode; the lower 3 bytes are the operand (up to ~16 million). A constant width means no alignment work and a simple decode loop.

[ opcode: 8 bits | operand: 24 bits ]

Inspect the compiled output of any script with disasm (see the CLI reference).

Stack model

Local variables live directly on the value stack at fixed slot indices (frame.base + slot), so LOAD_LOCAL is a single array read with no indirection. Temporaries — intermediate expression results — sit above the locals and are cleaned up as each expression completes.

Module cache

Modules are compiled once and cached, keyed by absolute path and file modification time. If a file hasn’t changed, importing it is just a map lookup. The cache is guarded by a sync.RWMutex, so concurrent requests read it without blocking one another.

Per-request isolation

In server mode, every HTTP request gets its own VM instance with a private stack and output buffer. The only shared state is the (read-mostly) module cache. Scripts handling different requests therefore never share mutable state — which is exactly what makes them safe to run concurrently. When sharing is needed, it’s explicit, via the runtime.core key-value store.

This design is the core trade-off behind Langoost: keep one warm process with shared compiled code, but isolate execution per request so there’s no cross-request interference and no cold start.