A Python-based tree-walking interpreter for a simplified subset of C — demonstrating lexical analysis, recursive descent parsing, and semantic evaluation.
A carefully chosen subset of C that covers the essential constructs needed to demonstrate a complete interpreter pipeline.
int (32-bit integer) and
float (double-precision). Type coercion follows C
semantics — floats assigned to int are truncated.
Full + - * / support with correct operator precedence
encoded structurally in the grammar. Parentheses override at any
depth.
Comparison operators > <
== return integer 0 or 1 — exactly as C does — usable
directly in conditional expressions.
Full if (expr) { } else { } with arbitrary nesting.
Each block creates its own lexical scope with a parent-pointer
chain.
Built-in printf(a, b, ...) accepts multiple
comma-separated expressions and prints space-separated values to
the output.
Both single-line // ... and multi-line
/* ... */ comments are silently consumed during
lexical analysis.
Variables declared inside if/else blocks
are destroyed on exit. Inner scopes can read and modify outer
variables.
Three distinct error types: LexerError,
ParseError, and RuntimeError_ — each
with line numbers and descriptive messages.
Four independent Python modules, each with a single responsibility, communicating through clean interfaces.
Hand-written character-by-character scanner. Classifies tokens into keywords, identifiers, literals, operators, and symbols in a single linear pass with one character of lookahead.
Pure data containers defined as Python dataclasses.
Immutable node types for every expression and statement kind in
the language.
Each EBNF non-terminal maps directly to a method. LL(1) lookahead selects productions. Operator precedence encoded structurally — no precedence table needed.
Walks the AST recursively. Scoped symbol table implemented as a parent-pointer chain. Type coercion and C-compatible integer division at assignment time.
The complete LL(1) grammar. Operator precedence is encoded through rule stratification — no disambiguation logic needed.
program = { statement } EOF statement = var_decl | assignment | if_stmt | printf_stmt var_decl = type IDENT ';' type = 'int' | 'float' assignment = IDENT '=' expr ';' if_stmt = 'if' '(' expr ')' block [ 'else' block ] block = '{' { statement } '}' printf_stmt = 'printf' '(' arg_list ')' ';' arg_list = expr { ',' expr }
expr = comparison comparison = addition { ( '==' | '<' | '>' ) addition } addition = multiplication { ( '+' | '-' ) multiplication } multiplication = unary { ( '*' | '/' ) unary } unary = '-' unary | primary primary = INT_LIT | FLOAT_LIT | IDENT | '(' expr ')'
| Level | Operators | Assoc. | Priority |
|---|---|---|---|
| 1 | == < > |
Left | Lowest |
| 2 | + - |
Left | |
| 3 | * / |
Left | |
| 4 | unary - |
Right | Highest |
INT_LIT = digit { digit } FLOAT_LIT = digit { digit } '.' digit { digit } IDENT = letter { letter | digit | '_' } // Keywords 'int' 'float' 'if' 'else' 'printf' // Comments (silently consumed) // single-line /* multi-line */
// float → int : truncated (C-style) int x; x = 3.9; // x = 3 // int → float : widened float f; f = 2; // f = 2.0 // int/int → floor division 7 / 2 // → 3 // float/float → true division 7.0 / 2.0 // → 3.5
Your C-Lite code runs entirely in the browser using Pyodide — the real Python interpreter compiled to WebAssembly. No server involved.
All tests pass. The built-in suite covers every language feature; the pytest suite adds unit-level coverage per module.
int type, declaration, assignment
float type, float literals
Operator precedence (* before +)
int / int = floor division
float / float = true division
if with true condition
if-else with false condition
== operator
Multi-level conditional branching
printf with comma-separated args
Grouping with parentheses
Negative literals via unary -
Semantic error detection
Semantic error detection
Mixed-type arithmetic expression