Kindling logo Rust ยท bytecode language from scratch

Watch code
become bytecode.

Kindling is a small, dynamically typed language with a real bytecode compiler, a stack based virtual machine, and a mark and sweep garbage collector, written in pure Rust with zero dependencies. Type a program and watch the whole pipeline run in the page: source to tokens to a syntax tree to bytecode, then step the VM one instruction at a time while the value stack, call frames, and collector update live.

Open the playground View on GitHub
How to use this playground
  1. Type a program in the editor, or pick one of the sample buttons to load it.
  2. Press Run to lex, parse, compile, and execute the whole program at once.
  3. Read the Tokens panel to see the raw token stream from the lexer.
  4. Read the AST panel to see the parsed tree, then read the Bytecode panel to see the compiled instructions with offsets and operands.
  5. Check the Result panel for the program value and any printed output.
  6. Press Step to run the virtual machine one instruction at a time. Watch the value stack and the call frames update, and follow the highlighted current instruction.
  7. Press Run GC to allocate demo heap objects, mark everything reachable from the roots in green, then sweep the unreachable objects in red.

A quick tour. Kindling turns your text into tokens, then into a tree, then into a stack machine program. Run gives you the fast answer. Step is the slow motion view where you can see each push and pop as the machine works. Run GC shows how a mark and sweep collector keeps live objects and frees the rest.

Samples

Source editor

Tokens

AST

Bytecode

Run / Result

Not run yet.

Step: value stack

VM not started.

Step: call frames

Garbage collector: heap

Run a program, then press Run GC to mark and sweep.
Source in, bytecode out

Bytecode you can read.

The same compiler the playground runs also backs the CLI. Point kindling disasm at a file and it prints the constant pool and the instruction stream, offset by offset, in a text form that assembles straight back into the identical program.

# kindling disasm demo.kdl # let x = 6; let y = x * 7; print "answer:"; print y; return y; program main=0 funcs=1 func 0 name "main" arity 0 upvals 0 constants 5 0 str "x" 1 int 6 2 str "y" 3 int 7 4 str "answer:" code 0000 CONST 1 0003 DEF_GLOBAL 0 0006 GET_GLOBAL 0 0009 CONST 3 0012 MUL 0013 DEF_GLOBAL 2 0016 CONST 4 0019 PRINT 0020 GET_GLOBAL 2 0023 PRINT 0024 GET_GLOBAL 2 0027 RETURN

Two evaluators, checked against each other

Kindling's correctness rests on a differential gate. For hundreds of randomly generated programs, the bytecode VM and a completely independent tree walking interpreter must agree on the whole outcome, the same value on success or the same error on a trap such as division by zero or exceeding the call depth. Two independent evaluators agreeing is the oracle.

# round trip integrity, enforced on every build
assemble(disassemble(program))      == program
deserialize(serialize(program))     == program
run(deserialize(serialize(program))) == run(program)

# and the collector: a collection frees exactly the unreachable objects,
# keeps every reachable one, and never frees a live object mid execution
gc: mark from roots, sweep the rest, checked on a known reachability graph

How it differs

Kindling lives in the space between a teaching tree walker and a production runtime you cannot read.

Crafting Interpreters' clox

The C bytecode VM from the book. Kindling follows the same clox shape, a single pass compiler, a stack machine, closures with upvalues, and a mark and sweep collector, rewritten in safe Rust with zero dependencies.

Lua, Wren

Small, mature, embeddable bytecode languages written in C and tuned for speed. Kindling is smaller and slower, and spends that budget on staying readable end to end and on a machine checkable correctness gate instead.

Kindling

A genuine bytecode runtime kept small enough to read in an afternoon: two independent evaluators checked against each other, a lossless text and binary bytecode format, and a sandbox with no file, network, or system access from inside a program.

The parts that matter

A whole language runtime, in parts you can hold.

front end Lexer

A hand written scanner that turns source text into a token stream.

front end Parser

Recursive descent from tokens to an AST, with source nesting and expression depth capped so pathological input is a clean error, not a crash.

compiler Single pass compiler

Walks the AST once and emits Kindling's own opcodes, resolving globals, locals, and upvalues as it goes.

bytecode Opcode set

Thirty three opcodes covering constants, globals, locals, upvalues, jumps, calls, closures, and print.

runtime Stack VM

A clox style machine with call frames, globals, locals, and closures that capture the variable by reference, not a snapshot.

oracle Reference interpreter

A completely independent tree walker, run against the VM on every build so any disagreement fails the gate.

memory Mark and sweep GC

A precise collector: mark from the roots, sweep the rest, verified on a known reachability graph and under allocation stress.

format Text and binary bytecode

Disassemble to reassemblable text and serialize to a compact blob, both proven lossless round trips.

Use it

Three ways in: the command line tool, the crate API, and the correctness gate you can run yourself.

CLI

kindling run compiles and executes a program, kindling disasm prints the compiled bytecode, and kindling repl starts an interactive session.

Library API

run_source and compile_source drive the pipeline, run_program executes a compiled program, eval_reference runs the independent interpreter, and disassemble / assemble plus serialize / deserialize move bytecode to text or bytes and back.

Correctness gates

Differential testing, round trip integrity, and garbage collector correctness all run as tests, with the fuzzing workload scaled by KINDLING_FUZZ_OPS.

# build the runtime and CLI (pure Rust, zero dependencies)
cargo build --release

# run a program, disassemble it, or start a REPL
./target/release/kindling run examples/factorial.kdl
./target/release/kindling disasm examples/factorial.kdl
./target/release/kindling repl

# run the tests, including the differential correctness gate
cargo test
KINDLING_FUZZ_OPS=4000 cargo test --release