← Selected systems

Case study

Flip compiler and virtual machine

Built a compiler and virtual machine in Rust for a small programming language, covering the full path from source code to bytecode execution.

Problem

I wanted to understand what actually happens between writing source code and seeing it execute.

Rather than only studying compiler architecture, I built a small language and implemented the full pipeline myself: lexical analysis, parsing, syntax trees, semantic checks, bytecode generation and execution inside a custom virtual machine.

Decisions

  • Split the project into flipc, which handles compilation and flipvm, which executes the generated bytecode.
  • Built the compiler as a series of distinct stages, turning source text into tokens, tokens into an abstract syntax tree and the checked tree into bytecode.
  • Kept the language intentionally small so I could reason about its behaviour from end to end.
  • Added arithmetic, comparisons, variables, assignments, control flow, nested blocks and strict lexical scoping without variable shadowing.
  • Designed a 16-bit instruction set and compiled programs for a stack-based virtual machine rather than stopping after parsing.

Outcome

Flip provides a working path from source code to execution for a small programming language.

The compiler produces bytecode that can be run by the virtual machine, while the REPL makes it possible to compile and execute smaller programs interactively.

Tests cover the compiler and VM independently, including arithmetic, operator precedence, branching, loops, memory access, stack behaviour, nested scopes and complete example programs.

Motivation

Flip began as a systems learning project.

Compiler concepts can feel straightforward when shown as a diagram, but the details become much more concrete when every stage has to agree with the next. A token must contain enough information for the parser. The syntax tree must represent the language clearly. Semantic checks must catch invalid programs before code generation. The bytecode format must then give the virtual machine enough information to execute the program predictably.

Building each of those stages gave me a much stronger understanding of the decisions and trade-offs involved.

Compiler and VM

The repository is organized as a Rust workspace containing two main crates:

flipc implements lexical analysis, parsing, AST processing, semantic checks and bytecode generation. flipvm implements the instruction set, stack, memory model and execution loop.

The language supports arithmetic and comparison expressions, variable declarations and assignments, if statements, while loops, nested blocks and strict lexical scoping.

Programs are compiled into a custom 16-bit bytecode format before being executed by the virtual machine.

What I Learned

The most valuable part of Flip was learning to maintain a clear model of a program as it moved through several different representations.

Source text became tokens. Tokens became syntax. Syntax became a checked program structure. That structure became bytecode and the bytecode became changes to the state of the virtual machine.

Debugging across those boundaries required more than familiarity with Rust syntax. It required carefully defining the responsibility of each stage, designing interfaces between them and testing the same behaviour at multiple levels.

Flip gave me practical experience with those boundaries and a much better understanding of how language implementations fit together.