A tree-walking interpreter for a small Python-like language, written from scratch in pure C.
MiniPython is an interpreter for a small imperative subset of Python, implemented from scratch in C with zero external dependencies. It reads source text and executes it immediately through the three canonical phases of a real language runtime: a hand-written lexer tokenizes the input, a recursive-descent parser builds an abstract syntax tree (AST), and a tree-walking evaluator executes that tree. An interactive REPL ties everything together, accumulating lines and running them when the user submits a blank line.
The project is deliberately small enough to read end to end, yet its architecture mirrors how production interpreters are actually structured. It is a compact, honest demonstration of the core ideas behind language implementation — lexing, parsing, AST construction, operator precedence, and evaluation.
This was a university team project for a Programming Languages course. See Team & Credits.
Everything below is implemented and working in the current codebase:
- Integer variables & assignment —
x = 10 - Arithmetic operators —
+ - * / %, with division- and modulo-by-zero guarded at evaluation time - The six comparison operators —
== != < > <= >= print(...)for output- Control flow —
if/else,while, and C-stylefor(init; cond; update) - Unary minus — modeled elegantly as
0 - x, so no dedicated negation node is needed - Parentheses and brace-delimited blocks
{ ... } - Correct operator precedence and left-associativity, achieved structurally through the parser's call hierarchy rather than a precedence table
- Interactive REPL — write several lines, press ENTER on a blank line to run, type
exitto quit - Full memory hygiene — every
malloc'd AST node is reclaimed by a recursivefreeAST()after each execution
MiniPython is a classic three-stage tree-walking interpreter, with one concern per translation unit and no shared responsibilities between phases. Data flows in one direction: source text → tokens → AST → execution.
flowchart LR
SRC["Source text<br/>(x = 10 * 2)"] --> LEX
subgraph LEX["Lexer — lexer.c"]
L["getNextToken()<br/>scans characters"]
end
LEX -->|Tokens| PAR
subgraph PAR["Parser — parser.c"]
P["recursive descent<br/>comparison → expr → term → factor"]
end
PAR -->|AST| EVAL
subgraph EVAL["Evaluator — eval.c"]
E["evaluate()<br/>walks the tree"]
end
EVAL --> OUT["Result / output"]
REPL["main.c — REPL<br/>parse() → evaluate() → freeAST()"] -.orchestrates.- LEX
A hand-written scanner over the source string with an integer cursor. getNextToken() skips whitespace, then recognizes integer literals, identifiers and keywords (print, if, else, while, for), two-character operators (==, !=, <=, >=) matched before their single-character prefixes (a common lexer pitfall handled correctly), and single-character operators and punctuation. token.h defines the TokenType enum and a fixed Token struct (type + lexeme).
A single generic AST node struct is reused for every node type, tagged by a NodeType and sharing fields (value, name, op, child pointers left / right / third / fourth, plus a next pointer that links statements into a list). Constructor functions build nodes via malloc and a common zeroing helper; a recursive freeAST() reclaims every node and also walks the statement linked list.
Recursive descent with a single-token lookahead and an eat() helper that asserts the expected token type and advances. Operator precedence and associativity are encoded purely by the call chain — comparison → expr → term → factor — instead of a precedence table. factor handles literals, variables, unary minus, and parenthesized sub-expressions. Statement functions cover assignment, print, if/else, while, C-style for, and brace blocks; program() and block() build next-linked statement lists appended in O(1) via first/last cursors.
A tree-walking evaluator with a fixed-size symbol table (an array of name/value pairs, linear scan) for variables. A single evaluate() dispatch handles arithmetic, comparisons, assignment, print, blocks, and control flow directly through native C recursion and loops.
| Layer | Choice |
|---|---|
| Language | C (C99) |
| Build | gcc (single command; MinGW/GCC/Clang compatible) |
| Dependencies | None — standard library only |
| Interface | Terminal REPL |
| Lines of code | ~1,200 across 5 .c files + headers |
- A C compiler (
gcc,clang, or MinGW on Windows)
From inside the source directory:
gcc main.c lexer.c parser.c ast.c eval.c -o minipython./minipython # Linux / macOS
minipython.exe # WindowsType code, then press ENTER on a blank line to execute the whole block. Type exit (or send EOF with Ctrl+Z on Windows / Ctrl+D on Unix) to quit.
MINIPYTHON
>>> x = 10
>>> y = 20
>>> print(x + y * 2)
>>>
50>>> for (i = 0; i < 3; i = i + 1) {
>>> print(i)
>>> }
>>>
0
1
2minipython-interpreter/
├── main.c # Entry point + REPL: parse() -> evaluate() -> freeAST()
├── lexer.c/.h # Hand-written scanner: source text -> tokens
├── token.h # TokenType enum + Token struct
├── ast.c/.h # Generic AST node, constructors, recursive freeAST()
├── parser.c/.h # Recursive-descent parser with single-token lookahead
├── eval.c/.h # Tree-walking evaluator + symbol table
├── LICENSE
└── README.md
This is a course project and does not currently ship an automated test suite. Verification was done interactively through the REPL. Adding a small set of example .mpy programs with expected outputs and a lightweight test harness is a natural next step, along with a Makefile and CI.
A few implementation details worth calling out:
- Precedence without a table. Correct math (
*before+) and left-associativity fall out of the order in which parser functions call each other — a clean, idiomatic recursive-descent technique. - Unary minus for free.
-xis desugared to0 - x, avoiding a dedicated AST node and evaluator case entirely. - Two-char operators handled correctly.
==,!=,<=,>=are matched before their single-character prefixes, avoiding a classic tokenizer bug. - No memory leaks. Every allocated node is freed by a recursive
freeAST()that also traverses the intrusive statement linked list, called after each REPL run. - Strict separation of concerns. Lexer, AST, parser, and evaluator each live in their own translation unit behind minimal headers.
In the spirit of transparency, this is an educational interpreter with a deliberately small surface:
- Values are a single type: integers only (no strings, floats, or booleans as distinct types).
- No functions, arrays/lists, or logical operators (
and/or/not). - Error handling is coarse: syntax and runtime errors print a generic message and exit rather than recovering.
- Capacities are fixed (bounded symbol table, fixed-size buffers); undefined variables read as
0. - The interpreter relies on module-global state, so it is intentionally single-run rather than reentrant.
These are honest trade-offs for a course project — the goal was a clear, correct, readable demonstration of interpreter fundamentals, and that goal is met.
This was a team project for a university Programming Languages course. The original repository was created and hosted by a teammate:
- Original repository:
erickson-arestigue/PROYECTO_LP - Erickson Arestigue — original repository owner and collaborator
- Marcelo Jáuregui (@mazk36) — collaborator; studied the codebase line by line, documented the design, and prepared this English portfolio edition
Full credit to all team members who contributed to the original coursework. The source code was originally documented in Spanish (comments and the two .txt design notes); this README presents the work in English for an international audience.
Part of a portfolio of systems and language work:
- minipython-interpreter — this project
- os-simulator — an operating-system simulator
- advanced-algorithms — algorithms and data structures
- Pinguino — a full-stack monitoring dashboard
Released under the MIT License.
Built by Marcelo Jáuregui · github.com/mazk36