Rust · from scratch · no parser generator
Alchemist is a compiler in Rust for a small language: a hand-written lexer, a recursive-descent parser, a code generator that lowers the AST to stack bytecode, and a bundled virtual machine that executes it. This is the real pipeline, ported to JavaScript, running below.
Alchemist is a compiler you can watch work: edit the program in the box, press Compile and run, and the panes below show its output and the stack bytecode it was lowered to.
let, if/else, while, functions, and print.Every stage is written by hand, no parser-generator or compiler crate anywhere in the pipeline.
A hand-written scanner turns source text into a flat token stream, tracking line and column for every error.
Recursive descent with precedence climbing builds an AST, guarded against runaway nesting so a bad file cannot overflow the stack.
The AST is walked once and lowered into a compact stack bytecode, with variables resolved to numbered local slots.
A small instruction set: pushes, arithmetic, comparisons, locals, jumps, and calls, readable in a disassembly.
A stack machine with call frames executes the bytecode directly, catching underflow and division by zero cleanly.
Compile and run a program, then lower it to bytecode and read the exact instructions the VM executes. This is real output captured from the CLI.
Alchemist is a teaching-grade compiler with a real backend. The design choices below are what separate it from a tree-walking interpreter.
The AST is lowered to a compact stack bytecode, and a separate stack machine with call frames executes that. You can read the instructions in a disassembly instead of guessing what the interpreter did.
The lexer and the recursive-descent parser with precedence climbing are written by hand. No parser-generator and no compiler crate anywhere in the pipeline.
Compile errors are typed and reported before anything runs: undefined variable, arity mismatch, and parse errors. The VM catches stack underflow, division by zero, and type errors cleanly.
Expression nesting is capped, so a malformed or adversarial file returns a parse error rather than overflowing the stack.
Every command below runs the same four stages you just watched in the browser.
# compile and run a program alchemist run factorial.al # emit bytecode to a file alchemist build factorial.al -o factorial.abc # show the instructions inside a bytecode file alchemist disasm factorial.abc
Three subcommands: run compiles and executes a source file, build emits bytecode to a file, and disasm prints the instructions inside a compiled bytecode file.
The pipeline ships as a Rust library crate (alchemist, src/lib.rs): lex, parse, compile, and run are callable directly, so you can embed the compiler or test a single stage.
An integration suite (tests/integration.rs) drives whole programs through the full lexer, parser, codegen, and VM path and checks the output.