A from-scratch educational Rust compiler. The skeleton mirrors the architecture
of rustc itself, just dramatically smaller and in C++17.
source.rs
│
▼
┌──────────┐ ┌────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐
│ Lexer │──▶│ Parser │──▶│ Resolver │──▶│ TypeCk │──▶│ BorrowCk │
└──────────┘ └────────┘ └──────────┘ └──────────┘ └──────────┘
tokens AST name → DefId typed AST (validated)
│
▼
┌──────────────┐
│ MIR Lower │
└──────────────┘
│
▼
┌──────────────┐
│ x86-64 cg │──▶ .s
└──────────────┘
- Items:
fn - Types:
i64,bool, unit() - Statements:
let,let mut, expression-statements,return - Expressions: integer/bool literals, identifiers, binary
+ - * / % == != < <= > >= && ||, unary- !, function calls, blocks,if / else,while, assignment. - Control flow: recursion, mutation, nested ifs/whiles.
- A minimal C runtime providing
print_i64andprint_bool.
The parser accepts a much larger Rust surface so the AST is realistic. These constructs report a clean "unsupported in this stage" diagnostic from the resolver/typeck instead of being silently miscompiled:
struct,enum,impl,trait,use,mod- generics
<T, 'a, const N: usize>and trait bounds - patterns beyond plain identifiers (
match, tuple/struct patterns) - references
&T,&mut T, lifetimes,Box<T>,Vec<T> - closures,
?, ranges, method calls on user types
Each of those is one (often more) follow-up session of work. Hooks are in place.
cmake -S . -B build
cmake --build buildThis produces build/rustc (or rustc.exe).
# 1. compile a Rust source to assembly
build/rustc tests/samples/fib.rs -o fib.s
# 2. assemble + link with the tiny C runtime (mingw / gcc)
gcc fib.s runtime/runtime.c -o fib.exe
# 3. run
./fib.exeUse --emit=tokens|ast|mir|asm to inspect each stage.
include/rustc/ public headers, one folder per phase
src/ matching .cpp implementations
runtime/runtime.c print_i64 / print_bool, linked into compiled programs
tests/samples/*.rs Rust source the working subset covers
- No borrow checker beyond a stub. We parse
&/&mutbut don't enforce aliasing rules. Adding NLL would be a project of its own. - No monomorphisation. Generics parse and are stored in the AST but type-checking refuses them.
- No incremental / on-demand query system. Every phase runs once, top-down.
- No real
cargo— single-file input only. Module loading is a TODO. - No optimisation passes between MIR and codegen. Codegen consumes MIR
directly with a naïve stack-slot model. Every local lives on the stack;
every operation goes through
rax.
The point is to be a faithful shape of a real compiler, small enough to read.