Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

10 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

A simple interpreter created using Python from first principles.

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.

Features

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

How It Works

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

Project Structure

interpreter-python/
├── pyproject.toml
├── uv.lock
├── README.md
└── src/
    └── interpreter_python/
        ├── __init__.py
        ├── main.py
        ├── interpreter.py
        ├── lexer.py
        └── parser.py

lexer.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.

parser.py

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.

interpreter.py

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

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.

Installation

This project uses uv for Python environment and dependency management.

Clone the repository and enter the project directory:

git clone <repository-url>
cd interpreter-python

Create/synchronize the project environment:

uv sync

Running the Interpreter

Run the installed command-line entry point with:

uv run interpreter-python

You 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

Development

Ruff

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 .

Current Architecture

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

Example Walkthrough

Consider:

10 + 2 * 3

1. Lexing

The lexer produces:

NUMBER(10)
PLUS
NUMBER(2)
MUL
NUMBER(3)

2. Parsing

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

Error Handling

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.

Goals

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.

Roadmap

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

Why Build This?

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.

License

See the LICENSE file for license information.

About

A simple interpreter created using Python from first principles.

Resources

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages