← Back to Projects Compilers & Runtimes

YatsuScript

A bytecode interpreter built from absolute scratch. No dependencies, no existing code — just me and the Rust compiler.

Tech Stack
Rust Bytecode No deps

What it is

YatsuScript is a bytecode interpreter written entirely in Rust with zero external dependencies. It has its own instruction set, its own register allocation, its own memory mapping. Everything from the lexer to the VM is hand-written.

The goal wasn't to build something production-ready. It was to understand, at a deep level, what happens between typing if (x > 10) { ... } and the CPU actually branching.

Architecture

The interpreter has four layers:

  • Lexer + Parser — tokenizes source code, builds an AST. Nothing fancy, but it handles expressions, variable declarations, control flow, and functions.
  • Compiler — walks the AST and emits bytecode. Each node type maps to a sequence of opcodes. Variable lookups become load instructions, arithmetic becomes operation instructions.
  • VM / Interpreter — a register-based bytecode VM. Fetches instructions, decodes operands, executes them. The main loop is a match on opcodes, about as tight as you can make it in safe Rust.
  • Memory — custom memory mapping with direct register allocation. No heap allocation during execution after initial setup.

The instruction set is minimal by design: load, store, add, sub, jump, compare, call, return. About 20 opcodes total. Enough to be Turing-complete, few enough that each one has a clear purpose.

The hard parts

Designing an instruction set. You don't realize how many decisions go into something as "simple" as an instruction set until you have to make them all. Fixed-width vs variable-width instructions? How many registers? What addressing modes? Every choice constrains everything else.

Register allocation. Infinite registers in the IR is easy. Mapping them to a fixed set of real registers without spilling too much is where the fun starts. I went with a simple linear scan allocator — not optimal, but it works and I understand every line.

Zero dependencies. No nom for parsing, no logos for lexing, no thiserror for errors. Everything is hand-rolled. This means more code and more bugs, but the learning is incomparable. I know exactly how my lexer works because I wrote every character of it.

What I learned

This is the project I'm proudest of. Not because it's useful (it's not) but because it's the hardest thing I've built and I didn't cut corners.

I learned that compilers are not magic. They're just programs that transform trees into other trees. The mystique around them is undeserved — what's hard is not the concept but the thousands of small, correct decisions you need to make.

I learned that Rust is an incredible language for this kind of work. The exhaustiveness checking on the opcode enum means I can never forget to handle an instruction in the VM. The ownership model maps naturally to the compiler pipeline (parse → compile → execute, no shared mutable state).

And I learned that building something from nothing — zero dependencies, zero scaffolding — is terrifying and exhilarating in equal measure. Every time the VM executed its first correct instruction, it felt like a small miracle.

If you're into compilers, I'd love to talk.