A bytecode virtual machine and compiler for the Lox language, written in modern C++ (C++23).
This project follows the second half of Crafting Interpreters by Robert Nystrom. The original C implementation (clox) lives in the author's repository. This version re-implements the VM in C++ using the standard library where the book hand-rolls its own data structures.
class Counter {
init(start) { this.n = start; }
next() {
this.n = this.n + 1;
return this.n;
}
}
var c = Counter(10);
print c.next(); // 11
print c.next(); // 12
fun makeAdder(a) {
fun add(b) { return a + b; }
return add;
}
print makeAdder(1)(2); // 3
class Animal {
init(name) { this.name = name; }
speak() { print this.name + " makes a sound"; }
}
class Dog < Animal {
speak() {
super.speak();
print this.name + " barks";
}
}
Dog("Rex").speak();
// Rex makes a sound
// Rex barks
From the book
- Single-pass compiler (Pratt parser) that emits bytecode
- Stack-based VM with call frames
- Global and local variables, block scoping
if/while/for, logicaland/or- Functions, recursion, and native functions
- Closures with open and closed upvalues
- Mark-and-sweep garbage collector with a weak string-interning table
- Classes, instances, bound methods,
this, and initializers - Single inheritance with
super - NaN-boxed values: every
Valuefits in 64 bits (switchable at build time)
Additions of my own
constvariables, plus const instances (aconstinstance can't gain new fields)breakandcontinue- Comma operator and
switchstatements - String concatenation with numbers (
"n=" + 5) - Extra native functions (below)
- Long constants (
OP_CONSTANT_LONG) and run-length-encoded line information
| Function | Description |
|---|---|
input() |
Reads a line from stdin and returns it as a string |
clock() |
CPU time in seconds |
sqrt(n) / abs(n) |
Square root and absolute value |
type(value) |
Returns "bool", "nil", "number", or "obj" |
sleep(ms) |
Sleeps for the given number of milliseconds |
exit(code) |
Exits the interpreter |
has_attribute(instance, "name") |
true if the instance has that field (a field set to nil still counts) |
remove_attribute(instance, "name") |
Removes a field from an instance |
Requirements
- A C++23 compiler with
<print>support (GCC 14+, or a recent Clang with libc++) - CMake 3.20 or newer
- Network access on the first configure, which downloads GoogleTest. You can skip it with
-DLOX_BUILD_TESTS=OFF.
cmake -S . -B build -DCMAKE_BUILD_TYPE=Release
cmake --build buildRunning
./build/efkenlang # start the REPL
./build/efkenlang script.lox # run a fileCMake options
| Option | Default | Effect |
|---|---|---|
LOX_NAN_BOXING |
ON |
Store values as 64-bit NaN-boxed words instead of a 16-byte tagged union |
LOX_TRACE |
OFF |
Print the compiled bytecode and trace every instruction and the stack |
LOX_LOG_GC |
OFF |
Log every GC allocation, mark, and free |
LOX_STRESS_GC |
OFF |
Collect garbage on every allocation, which is good for shaking out missing roots |
LOX_ASAN |
OFF |
Build with AddressSanitizer |
LOX_BUILD_TESTS |
ON |
Build the test suite |
A handy debugging build:
cmake -S . -B build-debug -DLOX_TRACE=ON -DLOX_BUILD_TESTS=OFFAnd a harsh GC check (run the tests with this):
cmake -S . -B build-gc -DLOX_STRESS_GC=ON -DLOX_ASAN=ONThe suite uses GoogleTest. Each test runs a Lox program in a fresh VM, captures what it prints, and checks the output or the error status. It covers expressions, control flow, functions, closures, classes, methods, initializers, inheritance and super, const rules, and garbage collection under load.
cmake -S . -B build-test
cmake --build build-test
ctest --test-dir build-test --output-on-failureThe tests compare printed output exactly, so build them with LOX_TRACE and LOX_LOG_GC turned off. Both value representations should pass the same suite, so it's worth running it with -DLOX_NAN_BOXING=ON and OFF.
All sources live in src/.
| File | Role |
|---|---|
main.cpp |
Entry point: the REPL and the script runner |
scanner.cpp |
Turns source text into tokens |
compiler.cpp / .hpp |
Single-pass Pratt parser that emits bytecode |
chunk.cpp / .hpp |
Bytecode, constants, and line information |
value.cpp / .hpp |
The Value type (tagged union or NaN-boxed) |
obj.cpp / .hpp |
Heap objects: strings, functions, closures, upvalues, classes, instances |
vm.cpp / .hpp |
The interpreter loop, calls, natives, and the garbage collector |
debug.cpp |
The disassembler |
The GoogleTest suite is in tests/.
All the credit for the design of Lox and the architecture of this VM goes to Robert Nystrom and Crafting Interpreters.