This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
This is an experimental Python 3.9-compatible interpreter implementation in C++. Unlike CPython, this interpreter uses a register-based VM instead of a stack-based VM, implements Python objects as C++ classes, and includes MLIR integration for advanced optimizations.
- CMake 3.25+
- C++23 compiler
- LLVM 23+ with MLIR (required for MLIR backend)
- GMP (GNU Multiple Precision library)
- ICU (International Components for Unicode)
Install LLVM/MLIR on Ubuntu:
wget https://apt.llvm.org/llvm.sh
chmod +x llvm.sh
sudo ./llvm.sh 23 all
sudo apt install libmlir-23-dev mlir-23-toolsConfigure and build:
cmake --preset release
cmake --build --preset releaseRun tests:
# Run all tests (unit tests + integration tests)
ctest --preset release
# Run just integration tests
ctest --preset release -R integration-tests
# Run just unittests
ctest --preset release -E integration-testsRun the Python interpreter:
# The binary is named `python` and lives under the preset's build dir
./build/release/src/python <script.py>
# Stress the garbage collector while running (recommended when debugging
# object-lifetime issues); unit is number of allocations, default 10000
./build/release/src/python <script.py> --gc-frequency 1000000Useful diagnostic flags: -t/--tokenize (print tokens), -a/--ast (print AST),
-b/--bytecode (print generated bytecode), -d/--debug / --trace (logging).
Development builds with sanitizers:
# Address sanitizer
cmake -B build -DCMAKE_BUILD_TYPE=Debug -DENABLE_SANITIZER_ADDRESS=ON
cmake --build build
# Undefined behavior sanitizer
cmake -B build -DCMAKE_BUILD_TYPE=Debug -DENABLE_SANITIZER_UNDEFINED_BEHAVIOR=ON
cmake --build buildSource → Lexer → Parser → AST → Compiler → Program → VM → Runtime
- Lexer (
src/lexer/) tokenizes Python source using CPython-compatible tokens - Parser (
src/parser/) builds an AST using the same grammar spec as CPython - AST (
src/ast/) represents code with the same node types as CPython - Compiler has three backends (
compiler::Backendinsrc/executable/Program.cpp):- MLIR (current default): the
pythonbinary always compiles viaBackend::MLIR. Uses MLIR dialects for optimization, then lowers to bytecode - BytecodeGenerator: Register-based bytecode generated directly from the AST
- LLVM: JIT compilation (incomplete/experimental). Must be compiled in by configuring with
-DENABLE_LLVM_BACKEND=ON(which defines theUSE_LLVMmacro), then selected at runtime with--use-llvm
- MLIR (current default): the
- VM (
src/vm/) executes instructions with register-based architecture - Interpreter (
src/interpreter/) manages execution state, frames, modules - Runtime (
src/runtime/) implements Python objects as C++ classes
Unlike CPython's stack-based VM, this interpreter uses registers for intermediate values:
StackFrame structure:
registers: Vector ofpy::Valueacting like CPU registerslocals: Stack-allocated local variables (separate from registers)stack_pointer: For runtime stack management
Instructions specify register operands explicitly:
// Example: BINARY_OPERATION r5 r3 r4 means r5 = r3 + r4
const auto &lhs = vm.reg(m_lhs);
const auto &rhs = vm.reg(m_rhs);
vm.reg(m_destination) = result.unwrap();Benefits over stack-based:
- Fewer memory accesses
- More optimization opportunities
- Closer to actual CPU architectures
Trade-offs:
- Larger instruction encoding (includes register indices)
- Currently no register reuse optimization (allocated sequentially)
MLIR provides an optimization infrastructure and alternative compilation path.
Compilation flow:
AST → MLIR Python Dialect → Optimizations → MLIR PythonBytecode Dialect → Bytecode
Key components:
- Python Dialect (
src/executable/mlir/Dialect/Python/): High-level Python operations (py.add, py.call, etc.) defined in TableGen - MLIRGenerator (
src/executable/mlir/Dialect/Python/MLIRGenerator.hpp): Visitor over AST nodes that generates MLIR operations - PythonBytecode Dialect (
src/executable/mlir/Dialect/EmitPythonBytecode/): Lower-level operations closer to final bytecode - Conversion Pass (
src/executable/mlir/Conversion/PythonToPythonBytecode/): Lowers Python dialect → PythonBytecode dialect - Bytecode Emitter (
src/executable/mlir/Target/PythonBytecode/): Translates MLIR to BytecodeProgram
Why MLIR?
- Enables sophisticated optimizations (constant folding, DCE, inlining)
- Infrastructure for future JIT compilation
- Clean separation between frontend (Python semantics) and backend (codegen)
- Can leverage MLIR's ecosystem of transformation passes
All Python objects inherit from PyObject (src/runtime/PyObject.hpp):
class PyObject : public Cell { // Cell enables garbage collection
TypePrototype &m_type; // Type information
PyDict *m_attributes; // Instance __dict__
};TypePrototype pattern:
- Template-based compile-time introspection
- Slot functions for protocols (
__add__,__getitem__, etc.) - Supports both C++ lambdas and PyObject methods
Value representation (src/runtime/Value.hpp):
py::Valueis a discriminated union to avoid heap allocations for primitives- Can hold
PyObject*, inlineNumber,String, orBytes
Concrete types (src/runtime/):
- Each Python type is a C++ class: PyInteger, PyString, PyList, PyDict, PyTuple, etc.
- Implement Python protocols via methods
Interpreter (src/interpreter/Interpreter.hpp) manages:
- Current execution frame (
m_current_frame: PyFrame*) - Module registry and import machinery
- Global frame for module-level code
- Exception state
Runtime provides object implementations and delegates protocol operations:
// VM executes instruction, calls interpreter for object operations
PyResult<Value> execute(VirtualMachine &vm, Interpreter &interpreter) {
const auto &lhs = vm.reg(m_lhs);
return add(lhs, rhs, interpreter); // delegates to runtime
}Frame management:
PyFrame: Python execution context (locals, globals, builtins)StackFrame: VM state (registers, stack pointer)- Interpreter maintains frame chain for tracebacks
All runtime operations return PyResult<T> for error propagation:
template<typename T> class PyResult; // Either Ok(T) or Err(BaseException*)
PyResult<PyObject*> add(const PyObject*, const PyObject*);Never throw exceptions from runtime code - use PyResult.
Used extensively for:
- AST traversal:
ast::CodeGeneratorwithvisit()methods for each AST node type - Garbage collection:
Cell::Visitorfor graph traversal - Both use double-dispatch pattern
VariablesResolver (src/executable/bytecode/codegen/VariablesResolver.hpp):
- Pre-pass before bytecode generation
- Analyzes variable scope (local, global, free variables, cell variables)
- Critical for correct closure and nested function implementation
Name mangling (src/executable/Mangler.hpp):
- Implements Python's private name mangling for class attributes (e.g.,
__private→_ClassName__private) - Used during bytecode generation
- Uses
Labelobjects for jumps and branches - Two-pass compilation: generate code with labels, then relocate to instruction positions
- See
src/executable/Label.hpp
Garbage Collection (src/memory/):
- Mark-sweep collector
- All objects inherit from
Cellto participate in GC - Slab allocator for efficient small object allocation
Factory functions:
static PyObject* create(...); // Allocates via VirtualMachine::heap()Execution:
src/vm/- Register-based virtual machinesrc/interpreter/- Execution control, frame management, module systemsrc/executable/- Compiled program representations (BytecodeProgram, etc.)
Frontend (CPython-compatible):
src/lexer/- Tokenizationsrc/parser/- Recursive descent parsersrc/ast/- Abstract syntax tree nodes
Compilation:
src/executable/bytecode/codegen/- Register bytecode generatorsrc/executable/bytecode/instructions/- ~80 instruction typessrc/executable/mlir/- MLIR compilation pipelineDialect/Python/- High-level Python dialect (TableGen definitions)Dialect/EmitPythonBytecode/- Low-level bytecode dialectConversion/- Lowering passes between dialectsTarget/- Final bytecode emission from MLIR
Runtime:
src/runtime/- Python object implementations (PyInteger, PyList, PyDict, etc.)src/runtime/types/- Built-in type definitionssrc/runtime/modules/- Standard library modules (sys, builtins, math, etc.)
Memory:
src/memory/- Mark-sweep garbage collector, slab allocator
Other:
src/utilities/- Helper utilities and freeze toolsrc/repl/- Interactive shell (uses linenoise)src/testing/- Test infrastructure
Location: integration/
Run integration tests:
# Language-feature test suite
./integration/run_python_tests.sh ./build/release/src/python
# Full integration run (examples + run_python_tests.sh + LLVM backend)
./integration/run_integration_tests.sh ./build/release/src/pythonTest categories:
integration/tests/- Python scripts testing various language featuresintegration/aoc/- Advent of Code solutions used as larger programsintegration/fibonacci/- Fibonacci exampleintegration/mandelbrot/- Mandelbrot set computationintegration/llvm/- LLVM backend tests (experimental)
Test structure:
- Tests should assert using Python's
assertstatement - Scripts exit with code 0 on success, non-zero on failure
- Tests run with
--gc-frequencyflag to stress-test garbage collector
- Define instruction in
src/executable/bytecode/instructions/ - Add to instruction set enumeration
- Implement
execute()method that takes VM and Interpreter - Register in instruction decoder
- Update BytecodeGenerator to emit the instruction when visiting relevant AST nodes
- Define operation in TableGen:
src/executable/mlir/Dialect/Python/IR/PythonOps.td - Build to generate C++ code from TableGen
- Add emission in MLIRGenerator when visiting AST nodes
- Add lowering to PythonBytecode dialect in conversion pass
- Add bytecode emission in Target
- Create class inheriting from
PyObjectinsrc/runtime/ - Implement Python protocols as methods
- Create
TypePrototyperegistration - Add factory function using
VirtualMachine::heap() - Implement GC visitor if type contains references to other objects
- Add to builtins in
src/runtime/modules/BuiltinsModule.cpp
GC debugging:
- Use
--gc-frequency Nto trigger GC every N allocations - Useful for finding object lifetime bugs
Bytecode inspection:
- Run with
--bytecode(or-b) to print generated instructions;--ast/-aand--tokenize/-tdump the AST and token stream
MLIR pipeline debugging:
- Set
MLIR_PRINT_IR_AFTER_ALL=1when running thepythonbinary to dump the IR after every pass (e.g.MLIR_PRINT_IR_AFTER_ALL=1 ./build/release/src/python <script.py>). The interpreter parses its own args with cxxopts and does not expose MLIR's-mlir-print-*command-line flags directly. - The standalone
python-mlir-opttool (src/executable/mlir/tools/python-mlir-opt/) is a regularmlir-opt-style driver and does accept MLIR's CL flags.
What's the same:
- Token types from the lexer
- Grammar specification for the parser
- AST node types
- Python 3.9 language semantics
What's different:
- VM architecture (register-based vs stack-based)
- Runtime implementation (C++ classes vs C structs)
- Bytecode format (incompatible with CPython .pyc files)
- Performance characteristics (no JIT yet, but register VM may have different trade-offs)
The codebase maintains compatibility by keeping the frontend (lexer, parser, AST) identical to CPython while innovating in the backend (VM, runtime). Integration tests in integration/tests/ verify Python semantics are preserved.