A Compiler & Interpreter Deep Dive
An interactive exploration of every stage in the Rust compilation pipeline and Python execution model. Click, hover, and explore to understand how your code transforms from source text to running program.
Click any stage to jump to its detailed explanation.
The same Fibonacci algorithm in both languages. This is the starting point of each pipeline.
fn fib(n: u64) -> u64 { if n <= 1 { return n; } fib(n - 1) + fib(n - 2) } fn main() { let result = fib(10); println!("{}", result); }
def fib(n): if n <= 1: return n return fib(n - 1) + fib(n - 2) result = fib(10) print(result)
The lexer breaks source text into a stream of tokens — the smallest meaningful units.
INDENT, DEDENT,
and NEWLINE tokens because whitespace is syntactically significant.
Rust uses braces {} and semicolons, so whitespace is ignored.
| Category | Rust Examples | Python Examples |
|---|---|---|
| Keywords | fn, let, if, return, mut, pub | def, if, return, class, import |
| Identifiers | fib, n, result | fib, n, result |
| Literals | 1, 2, "hello" | 1, 2, "hello" |
| Operators | +, -, <=, -> | +, -, <=, ** |
| Delimiters | { } ( ) ; : | ( ) : INDENT DEDENT |
| Type Annotations | u64, String, Vec<T> | Optional hints: int, str |
The parser consumes tokens and builds a tree structure representing the program's syntax. Click nodes to expand/collapse.
Rust transforms code through four additional intermediate representations before producing machine code.
Desugaring, name resolution, and type checking happen here. Syntactic sugar like for loops become lower-level constructs.
for i in 0..10 { println!("{}", i); }
// for loop desugared to loop + match { let mut iter = IntoIterator::into_iter(0..10); loop { match Iterator::next(&mut iter) { Some(i) => { println!("{}", i); } None => break, } } }
MIR is a control-flow graph used for borrow checking, optimization, and code generation.
fib()// Simplified MIR representation bb0: { _2 = Le(_1, const 1_u64); switchInt(_2) -> [0: bb2, otherwise: bb1]; } bb1: { _0 = _1; return; } bb2: { _3 = Sub(_1, const 1_u64); _4 = fib(_3); _5 = Sub(_1, const 2_u64); _6 = fib(_5); _0 = Add(_4, _6); return; }
// This would FAIL borrow checking: let mut v = vec![1, 2, 3]; let first = &v[0]; // immutable borrow v.push(4); // mutable borrow ERROR! println!("{}", first); // Error: cannot borrow `v` as mutable // because it is also borrowed as immutable
MIR is lowered to LLVM's intermediate representation for platform-independent optimization.
; LLVM IR for fib function (simplified) define i64 @fib(i64 %n) { entry: %cmp = icmp ule i64 %n, 1 br i1 %cmp, label %base, label %recurse base: ret i64 %n recurse: %n1 = sub i64 %n, 1 %f1 = call i64 @fib(i64 %n1) %n2 = sub i64 %n, 2 %f2 = call i64 @fib(i64 %n2) %sum = add i64 %f1, %f2 ret i64 %sum }
LLVM's backend generates native assembly for the target architecture (e.g., x86-64).
; x86-64 assembly (simplified) fib: cmp rdi, 1 jbe .base push rbx mov rbx, rdi lea rdi, [rbx - 1] call fib mov rcx, rax lea rdi, [rbx - 2] call fib add rax, rcx pop rbx ret .base: mov rax, rdi ret
Python compiles to bytecode, then interprets it in the Python Virtual Machine.
CPython compiles the AST to bytecode instructions stored in .pyc files. Use dis.dis() to inspect.
# Output of dis.dis(fib) 2 0 LOAD_FAST 0 (n) 2 LOAD_CONST 1 (1) 4 COMPARE_OP 1 (<=) 6 POP_JUMP_IF_FALSE 12 3 8 LOAD_FAST 0 (n) 10 RETURN_VALUE 4 12 LOAD_GLOBAL 0 (fib) 14 LOAD_FAST 0 (n) 16 LOAD_CONST 1 (1) 18 BINARY_SUBTRACT 20 CALL_FUNCTION 1 22 LOAD_GLOBAL 0 (fib) 24 LOAD_FAST 0 (n) 26 LOAD_CONST 2 (2) 28 BINARY_SUBTRACT 30 CALL_FUNCTION 1 32 BINARY_ADD 34 RETURN_VALUE
The Python Virtual Machine is a stack-based interpreter. Click "Step" to execute bytecode instructions and watch the stack change.
Static typing (Rust) catches errors at compile time. Dynamic typing (Python) defers checks to runtime.
let x: i32 = 5; let y: String = "hello".to_string(); // Compile-time error: let z = x + y; // ERROR: cannot add `String` to `i32` // mismatched types // expected `i32`, found `String`
x = 5 y = "hello" # Runtime error: z = x + y # TypeError: unsupported operand type(s) # for +: 'int' and 'str' # But this is valid: x = "now I'm a string" # rebinding is fine
| Aspect | Rust (Static) | Python (Dynamic) |
|---|---|---|
| When types checked | Compile time | Runtime |
| Type annotations | Required (with inference) | Optional (hints only) |
| Variable rebinding type | Not allowed | Allowed freely |
| Generics | Monomorphized at compile time | Duck typing (no need) |
| Runtime overhead | Zero — types erased | Every value carries type info |
Rust uses ownership with zero-cost abstractions. Python uses reference counting + garbage collection.
fn main() { let s1 = String::from("hello"); let s2 = s1; // s1 is MOVED to s2 // println!("{}", s1); // ERROR: s1 no longer valid println!("{}", s2); // OK } // s2 dropped, memory freed
import sys a = [1, 2, 3] print(sys.getrefcount(a)) # 2 (a + getrefcount arg) b = a # b references same object print(sys.getrefcount(a)) # 3 del b # refcount decremented print(sys.getrefcount(a)) # 2
Rust enforces explicit error handling at compile time. Python uses exceptions at runtime.
use std::fs; fn read_config() -> Result<String, std::io::Error> { let content = fs::read_to_string("config.toml")?; Ok(content) } fn main() { match read_config() { Ok(cfg) => println!("Config: {}", cfg), Err(e) => eprintln!("Error: {}", e), } }
Result.
The ? operator propagates errors elegantly. Unhandled results produce compiler warnings.
def read_config(): with open("config.toml") as f: return f.read() try: cfg = read_config() print(f"Config: {cfg}") except FileNotFoundError as e: print(f"Error: {e}") # Forgetting try/except? Program crashes at runtime.
| Aspect | Rust | Python |
|---|---|---|
| Error mechanism | Result<T, E> / Option<T> | Exceptions (try/except) |
| When enforced | Compile time | Runtime |
| Can ignore errors? | Only explicitly (.unwrap()) | Yes, silently |
| Error propagation | ? operator | Automatic stack unwinding |
| Null handling | Option<T> — no null | None + AttributeError |
Approximate relative comparisons for typical workloads. Lower is better for all metrics except throughput.
Rust guarantees thread safety at compile time. Python has the GIL limiting true parallelism.
use std::thread; fn main() { let mut handles = vec![]; for i in 0..4 { handles.push(thread::spawn(move || { println!("Thread {} running", i); })); } for h in handles { h.join().unwrap(); } }
import threading def work(i): print(f"Thread {i} running") threads = [] for i in range(4): t = threading.Thread(target=work, args=(i,)) t.start() threads.append(t) for t in threads: t.join()
multiprocessing for CPU-bound tasks, or asyncio for I/O-bound tasks.| Feature | Rust | Python |
|---|---|---|
| Execution Model | AOT compiled to native code | Interpreted bytecode (PVM) |
| Type System | Static, strong, inferred | Dynamic, strong, duck-typed |
| Memory | Ownership + borrowing (no GC) | Reference counting + GC |
| Null Safety | Option<T> — no null | None + runtime errors |
| Error Handling | Result<T,E> — explicit | Exceptions — implicit |
| Concurrency | Fearless — compile-time safety | GIL limits parallelism |
| Performance | Near C/C++ speed | 10-100x slower (CPU-bound) |
| Compile Time | Slow (seconds to minutes) | Instant (interpreted) |
| Learning Curve | Steep (ownership, lifetimes) | Gentle (beginner-friendly) |
| Ecosystem | crates.io (~150k crates) | PyPI (~500k packages) |
| Best For | Systems, CLI, WebAssembly, embedded | Scripting, ML/AI, web backends, data |
Built by Dhruv Joshi. Inspired by Transformer Explainer.
Rust and Python logos and names are trademarks of their respective owners.