Alchemist logo

Rust · from scratch · no parser generator

Source becomes instructions,
then the instructions run.

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.

Get the source See how it works
ready

Program output


        

Bytecode disassembly


        
How to use this playground

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.

  • Write a small program using let, if/else, while, functions, and print.
  • Press Compile and run to lex, parse, generate bytecode, and execute it.
  • Read the Bytecode disassembly pane to see the exact instructions the VM runs.

Five stages, source to result.

Every stage is written by hand, no parser-generator or compiler crate anywhere in the pipeline.

1

Lex

A hand-written scanner turns source text into a flat token stream, tracking line and column for every error.

2

Parse

Recursive descent with precedence climbing builds an AST, guarded against runaway nesting so a bad file cannot overflow the stack.

3

Codegen

The AST is walked once and lowered into a compact stack bytecode, with variables resolved to numbered local slots.

4

Bytecode

A small instruction set: pushes, arithmetic, comparisons, locals, jumps, and calls, readable in a disassembly.

5

VM

A stack machine with call frames executes the bytecode directly, catching underflow and division by zero cleanly.

The same run, on the command line.

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.

# compile and run the factorial example $ alchemist run examples/factorial.al 120 # lower it to bytecode, then read the instructions $ alchemist build examples/factorial.al -o factorial.abc wrote factorial.abc $ alchemist disasm factorial.abc MAIN_LOCALS 0 FUNCS 1 FUNC fact 1 1 4 CODE 20 0: PUSH_INT 5 1: CALL 0 1 2: PRINT 3: HALT 4: LOAD 0 5: PUSH_INT 1 6: LE 7: JUMP_IF_FALSE 11 8: PUSH_INT 1 9: RET 10: JUMP 18 11: LOAD 0 12: LOAD 0 13: PUSH_INT 1 14: SUB 15: CALL 0 1 16: MUL 17: RET 18: PUSH_INT 0 19: RET

What makes it different.

Alchemist is a teaching-grade compiler with a real backend. The design choices below are what separate it from a tree-walking interpreter.

Bytecode and a VM, not a tree-walker

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.

Hand-written, no generators

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.

Errors instead of panics

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.

Bounded parsing

Expression nesting is capped, so a malformed or adversarial file returns a parse error rather than overflowing the stack.

Read the pipeline, not just the output.

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 ways in.

CLI

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.

Library

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.

Tests

An integration suite (tests/integration.rs) drives whole programs through the full lexer, parser, codegen, and VM path and checks the output.