This project is an attempt to understand how interpreters and compilers work by starting with the smallest useful implementation possible rather than immediately jumping into a full programming language.
The interpreter currently takes arithmetic expressions as source text, tokenizes them, parses them using a recursive-descent parser, and evaluates the result.
The interpreter currently supports:
- Integer literals
- Addition (
+) - Subtraction (
-) - Multiplication (
*) - Division (
/) - Operator precedence
- Parenthesized expressions
- Unary minus
- Invalid-character detection
- Basic syntax-error handling
- Division-by-zero handling
- Interactive command-line interface
For example:
calc> 2 + 3 * 4
14
calc> (2 + 3) * 4
20
calc> -10 + 5
-5
calc> 10 / 0
Error: Division by zero
The interpreter currently follows this pipeline:
Source Code
↓
Lexer
↓
Tokens
↓
Parser
↓
Evaluation
↓
Result
For example, given:
2 + 3 * 4
the lexer first converts the source text into tokens:
Token(NUMBER, 2)
Token(PLUS, +)
Token(NUMBER, 3)
Token(MUL, *)
Token(NUMBER, 4)
Those tokens are then passed to the parser.
The parser understands the grammatical structure of the expression and evaluates multiplication before addition, producing:
2 + (3 * 4)
which results in:
14
interpreter-python/
├── pyproject.toml
├── uv.lock
├── README.md
└── src/
└── interpreter_python/
├── __init__.py
├── main.py
├── interpreter.py
├── lexer.py
└── parser.py
The lexer is responsible for converting source text into tokens.
For example:
12 + 5
becomes approximately:
NUMBER(12)
PLUS
NUMBER(5)
The lexer currently recognizes:
NUMBER
PLUS
MINUS
MUL
DIV
LPAREN
RPAREN
Whitespace is ignored, while unrecognized characters result in a syntax error.
The parser receives the tokens produced by the lexer and determines how they should be interpreted.
It is implemented as a recursive-descent parser using the following grammar:
expr → term (("+" | "-") term)*
term → factor (("*" | "/") factor)*
factor → NUMBER
| "(" expr ")"
| "-" factor
This grammar also defines operator precedence.
factor handles the highest-precedence constructs, such as numbers, parentheses, and unary minus.
term handles multiplication and division.
expr handles addition and subtraction.
This means:
2 + 3 * 4
is naturally interpreted as:
2 + (3 * 4)
without assigning explicit numeric precedence values to the operators.
The interpreter connects the lexer and parser:
def interpret(text: str):
tokens = tokenize(text)
parser = Parser(tokens)
return parser.expr()It provides a simple interface where source text goes in and the evaluated result comes out.
main.py provides the interactive command-line interface.
It implements a small REPL:
Read
↓
Evaluate
↓
Print
↓
Loop
The interpreter continues accepting expressions until quit or exit is entered.
This project uses uv for Python environment and dependency management.
Clone the repository and enter the project directory:
git clone <repository-url>
cd interpreter-pythonCreate/synchronize the project environment:
uv syncRun the installed command-line entry point with:
uv run interpreter-pythonYou should see:
calc>
You can then enter expressions:
calc> 10 + 20
30
calc> 2 + 3 * 4
14
calc> (2 + 3) * 4
20
calc> -10 + 4
-6
To exit:
calc> exit
or:
calc> quit
Ruff is used for linting and formatting.
Run the linter:
uv run ruff check .Automatically fix supported lint issues:
uv run ruff check --fix .Format the project:
uv run ruff format .At the moment, the parser also performs evaluation.
That means the architecture is effectively:
Source
↓
Lexer
↓
Tokens
↓
Parser + Evaluator
↓
Result
For example, while parsing:
3 * 4
the parser directly calculates the result rather than creating an intermediate representation.
This keeps the first version deliberately small and makes it easier to understand the complete execution flow.
A more advanced version can separate those responsibilities:
Source
↓
Lexer
↓
Tokens
↓
Parser
↓
Abstract Syntax Tree
↓
Interpreter / Evaluator
↓
Result
Consider:
10 + 2 * 3
The lexer produces:
NUMBER(10)
PLUS
NUMBER(2)
MUL
NUMBER(3)
The parser begins with an expression:
expr
which contains terms:
expr
├── term → 10
├── PLUS
└── term
├── factor → 2
├── MUL
└── factor → 3
Because multiplication is handled by term, the parser evaluates:
2 * 3
first:
6
The outer expression then evaluates:
10 + 6
giving:
16
Invalid characters are rejected by the lexer.
For example:
calc> 2 + @
Error: Unexpected character: @
Invalid expressions are rejected by the parser.
Division by zero is also handled:
calc> 10 / 0
Error: Division by zero
Errors do not terminate the REPL, so another expression can be entered immediately afterward.
The goal of this project is not to build a production-ready programming language.
It is primarily a learning project for understanding concepts such as:
- Lexical analysis
- Tokens
- Grammars
- Recursive-descent parsing
- Operator precedence
- Unary and binary operators
- Syntax errors
- Interpreter architecture
- Abstract syntax trees
- Language evaluation
The project intentionally starts small so that every part of the interpreter can be understood before introducing more sophisticated language features.
Possible future additions include:
- Build an Abstract Syntax Tree (AST)
- Separate parsing from evaluation
- Add variables
- Add assignment expressions
- Add comparison operators
- Add boolean values and operators
- Add statements
- Add conditional expressions/statements
- Add better error messages with line and column information
- Add automated tests
- Explore bytecode or compilation after the interpreter becomes more capable
Resources such as Crafting Interpreters provide excellent introductions to implementing full programming languages.
This project takes a smaller first step.
Rather than beginning with a complete language implementation, the idea is to build the smallest interpreter possible, understand every stage of its execution, and then expand it incrementally.
The current arithmetic interpreter is the first step in that process.
See the LICENSE file for license information.