A Python-like interpreter written in C for learning how interpreters work.
Source Code → Lexer → Tokens → Parser → AST → Compiler → Bytecode → VM → Result
- Lexer - Tokenization with Python-style indentation (INDENT/DEDENT)
- Parser - Recursive descent building AST
- Two execution modes:
- Tree-walking interpreter (direct AST execution)
- Bytecode compiler + stack-based VM (~5x faster)
- Memory management - Reference counting + mark-and-sweep GC
- Pool allocator - Arena/pool/block hierarchy for small objects
- Classes - Simple classes with
__init__and methods - Generators -
yieldwith state preservation - Async/Await - Coroutines with
gather()for concurrency - Bytecode serialization - Save/load compiled
.pycfiles
make./minipy script.py # Tree-walking interpreter
./minipy --vm script.py # Bytecode VM (faster)
./minipy # Interactive REPL| Flag | Description |
|---|---|
--vm |
Use bytecode VM instead of tree-walking |
--disasm |
Show disassembled bytecode |
--bc |
Bytecode REPL (shows bytecode as you type) |
--profile |
Profile opcode execution |
--compile |
Compile to .pyc file |
x = 10
y = 20
print(x + y) # 30
if x > 5:
print("big")
else:
print("small")def factorial(n):
if n <= 1:
return 1
return n * factorial(n - 1)
print(factorial(5)) # 120nums = [1, 2, 3, 4, 5]
print(len(nums)) # 5
print(nums[2]) # 3
for n in nums:
print(n)class Counter:
def __init__(self, start):
self.value = start
def inc(self):
self.value = self.value + 1
return self.value
c = Counter(10)
print(c.inc()) # 11
print(c.inc()) # 12def countdown(n):
while n > 0:
yield n
n = n - 1
gen = countdown(3)
print(next(gen)) # 3
print(next(gen)) # 2
print(next(gen)) # 1async def task(name, count):
i = 0
while i < count:
print(name)
i = i + 1
yield
results = gather(task("A", 2), task("B", 2))
# Output: A, B, A, B (interleaved)Compile and inspect bytecode:
./minipy --compile script.py # Creates script.pyc
./minipy --disasm script.pyc # Show bytecode
./minipy script.pyc # Run compiled fileExample bytecode output:
== script.pyc ==
0000 0 BC_CONST 0 '10'
0002 | BC_STORE_NAME 0 (x)
0004 | BC_CONST 1 '20'
0006 | BC_STORE_NAME 1 (y)
0008 | BC_LOAD_NAME 2 (print)
0010 | BC_LOAD_NAME 0 (x)
0012 | BC_LOAD_NAME 1 (y)
0014 | BC_ADD
0015 | BC_CALL 1
0017 | BC_POP
| Opcode | Description |
|---|---|
BC_CONST |
Push constant onto stack |
BC_LOAD_NAME |
Load variable by name |
BC_STORE_NAME |
Store top of stack to variable |
BC_ADD/SUB/MUL/DIV |
Arithmetic operations |
BC_CALL |
Call function with N arguments |
BC_RETURN |
Return from function |
BC_JUMP |
Unconditional jump |
BC_JUMP_IF_FALSE |
Conditional jump |
BC_YIELD |
Yield from generator |
BC_AWAIT |
Await coroutine |
./minipy --profile script.pyOutput:
=== Profiler Report ===
Total opcodes: 150234
Execution time: 0.023s
Opcodes/second: 6.5M
Top opcodes:
BC_LOAD_FAST 45123 (30.0%)
BC_ADD 22456 (14.9%)
BC_JUMP_IF_FALSE 18234 (12.1%)
...
┌─────────────────────────────────────────────────────────────────┐
│ Source Code │
│ "x = 10 + 20\nprint(x)" │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ LEXER │
│ (lexer.c) │
│ Converts source text into tokens │
│ Handles Python-style INDENT/DEDENT │
└─────────────────────────────────────────────────────────────────┘
│
[TOK_NAME, TOK_EQ, TOK_INT, TOK_PLUS, TOK_INT, ...]
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ PARSER │
│ (parser.c) │
│ Recursive descent parser │
│ Builds Abstract Syntax Tree │
└─────────────────────────────────────────────────────────────────┘
│
AST_BLOCK
├── AST_ASSIGN(x, AST_BINOP(+, 10, 20))
└── AST_CALL(print, [AST_VAR(x)])
│
┌───────────────┴───────────────┐
▼ ▼
┌──────────────────────────┐ ┌──────────────────────────┐
│ TREE-WALK INTERP │ │ COMPILER │
│ (interp.c) │ │ (compiler.c) │
│ │ │ │
│ Directly executes AST │ │ AST → Bytecode │
│ Slower but simpler │ │ │
└──────────────────────────┘ └──────────────────────────┘
│ │
▼ ▼
Result ┌──────────────────────────┐
│ BYTECODE VM │
│ (vm.c) │
│ │
│ Stack-based execution │
│ ~5x faster │
└──────────────────────────┘
│
▼
Result
Every object tracks how many references point to it:
obj->refcount++; // When referenced
obj->refcount--; // When unreferenced
if (obj->refcount == 0) free(obj);Handles reference cycles:
1. MARK: Start from roots, mark all reachable objects
2. SWEEP: Free all unmarked objects
Efficient allocation for small objects:
Arena (256KB)
└── Pool (4KB, one size class)
└── Block (8-512 bytes)
┌────────────────────────────────────┐
│ Magic "MPC1" (4 bytes) │ ← File format identifier
├────────────────────────────────────┤
│ Code length (4 bytes) │
│ Bytecode (N bytes) │ ← Raw instructions
├────────────────────────────────────┤
│ Constant count (4 bytes) │
│ Constants (type + data each) │ ← Numbers, strings
├────────────────────────────────────┤
│ Name count (4 bytes) │
│ Names (length + string each) │ ← Variable names
└────────────────────────────────────┘
src/
├── main.c # Entry point, REPL, CLI handling
├── lexer.c/h # Tokenization
├── parser.c/h # AST construction
├── ast.c/h # AST node definitions
├── interp.c/h # Tree-walking interpreter
├── compiler.c/h # AST to bytecode compiler
├── bytecode.c/h # Bytecode chunk, disassembler, serialization
├── vm.c/h # Stack-based virtual machine
├── object.c/h # Object model (int, string, list, func, etc.)
├── pool.c/h # Pool allocator
examples/
├── test.py # Basic test
├── class_test.py
├── gen_test.py
├── async_test.py
└── ...
| Function | Description |
|---|---|
print(x) |
Print value |
len(x) |
Length of list/string |
range(n) |
Generate 0 to n-1 |
int(x) |
Convert to integer |
str(x) |
Convert to string |
type(x) |
Get type name |
next(gen) |
Get next value from generator |
run(coro) |
Run single coroutine |
gather(...) |
Run coroutines concurrently |
gc_collect() |
Force garbage collection |
gc_count() |
Count tracked objects |
This project demonstrates concepts from:
- Crafting Interpreters by Bob Nystrom
- CPython Internals
- Python's
dismodule for bytecode inspection
MIT