Internals & embedding¶
The whole language lives in three files inside the lantern repo — no dependencies beyond the C++ standard library:
wick/wick.hpp the embed API (~120 lines)
wick/wick_front.cpp lexer + one-pass typed compiler → bytecode
wick/wick_vm.cpp stack VM, GC, built-ins, signature parser
Pipeline¶
No AST. The compiler is single-pass, clox-style: the Pratt expression
parser emits bytecode as it goes while carrying a static Type for every
expression — the type checker and the code generator are the same walk.
This is why declare-before-use exists, and why the implementation stays
around 2k lines.
Because types are known at emit time, opcodes are specialized: +
compiles to numeric ADD or string CONCAT (never a runtime tag check),
comparisons are numeric ops, and locals resolve to frame slots — there is
no per-access name lookup at run time. Engine natives resolve to integer
ids at compile time; the VM's NCALL is an array index, not a table walk.
The VM¶
A classic stack machine: constants pool, call frames with slot-addressed locals, ~40 opcodes. Runtime errors carry a bytecode→line table so the error screen can point at source. There is no JIT — deliberately: the interpreter is fast enough for game logic at 400×240, and the engine targets platforms where JITs are forbidden.
Garbage collection¶
Mark & sweep over strings, lists, maps, and records — but the
collector only runs when the host calls wick::collect(), which
lantern does exactly once per frame, after present. A collection can
never land mid-draw; the worst case is bounded by how much garbage one
frame creates. No closures/upvalues keeps the object graph shallow: roots are
globals, constants, and the (empty-between-frames) VM stack.
Embedding wick yourself¶
#include "wick.hpp"
wick::VM* vm = wick::create();
std::string err;
// typed natives: a signature DSL the compiler enforces
wick::addNative(vm, "game", "spawn(num, num, str): num",
[](wick::VM& vm, const wick::Value* a, int) {
int id = mySpawn(a[0].d, a[1].d, wick::getStr(a[2]));
return wick::Value::num(id);
}, err);
wick::addConst(vm, "game", "MAX", 64);
wick::load(vm, source, "main.wick", err); // compile + run top level
wick::call(vm, "update", dt, true, err); // each frame
wick::collect(vm); // at YOUR safe point
wick::reset(vm); // hot-reload support
Signature grammar: name(type[, type][, type = default]) [: type] with
types num bool str list and ? for optionals. Defaults make trailing
parameters optional at call sites; the compiler fills them in, so natives
always receive full arity. A native reports failure with
wick::setError(vm, msg) — the VM turns it into a file:line runtime
error at the call site.