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.
Native callbacks
Some standard-library modules accept a Langoost function that a native Go
library calls back from its own goroutine — for example webrtc connection
events, raft’s applyLog, io.watch filesystem events, signal handlers,
and timer callbacks. To fire those safely without cross-thread races, the
callback is snapshotted when it’s registered (synchronously, while the VM is
in a known state) and then spawned at fire time into an isolated child VM
with a copy of the globals. So a native callback behaves like a spawned task:
it runs concurrently and does not share mutable state with the caller.
Roadmap
A DAP debugger (Debug Adapter Protocol — breakpoints, stepping, and variable
inspection in editors) has shipped; see the debugging guide.
The VM also produces source-mapped stack traces and can expose Go’s pprof
profiler with run --pprof (see the CLI reference). Still on the
roadmap: a tracing JIT that compiles hot bytecode paths.