Completed extensions:
- Parsing Expressions
- Evaluating Expressions
- Statements & State
- Control Flow
- Functions
- Resolving
- Classes
- Inheritance
This my Python solutions to the "Build your own Interpreter" Challenge. It follows the book Crafting Interpreters by Robert Nystrom.
In this challenge you'll build an interpreter for Lox, a simple scripting language. Along the way, you'll learn about tokenization, ASTs, tree-walk interpreters and more. There is a Java implementation on the book's repo (and C too!) which are easy to run locally if you want to compare behavior or run test suite against your own implementation (see below).
- Ensure you have
python (3.12)installed locally - Run
./your_program.shto run your program, which is implemented inapp/main.py. - Run tests with
pipenv run tests - Assert 100% coverage with
pipenv run cov - Format and lint code using
ruffusingpipenv run fmt - Type check with
pyrightusingpipenv run check
- Pretty decent unit tests for scanner, parser, interpreter, and main modules
AstPrintercan print debug versions of all syntax, helps with dangling-else testExprhas pretty minimal boilerplate, didn't need to write a source code generator!- Creating e.g.
Assignrecord class makes great use ofdataclass - Instead of repeated static definitions:
- e.g.
class Assign: def accept(self, v): return v.visit_assign(self) - base class
accept()uses dynamic dispatch to invokev["visit_{name}](self)
- e.g.
- But, kept the generic
Visitor[T]static definitionsdef visit_assign(self, assign: Assign) -> T:for IDE support (I can't image defining these dynamically would play well with IDE type inference
- Creating e.g.
ScannerandParser- Makes them easy to unit test
Interpretertakes anIOobject toprintto, making it pretty easy to test
ScannerusesIntEnum- to determine which range are keywords, using the enum name as string to match. i.e.
/print/appears one time in file. - Also, uses the fact that e.g.
TokenType.BANG + 1 == TokenType.BANG_EQUALin a clever way
- to determine which range are keywords, using the enum name as string to match. i.e.
Parseruses a betterprivate Token match(...)pattern to combing predicate andprevious()take_binarymake short work forlogic_and -> equality -> comparison -> ...
Resolvertakes multiple passes of the syntax tree to find problems, which is much simpler than a do-everything class.mainuses awith step("parse") as out: ...context manager- The CLI options
tokenize|parse|evaluate|runkind of follow a linear flow, so would takeO(N^2)steps to represent each in their own function. - that exits if there were errors or
parsewas requested as the CLI result. - Use
print(..., file=out)to write tostderrunless this was the requested text.
- The CLI options
test_environmentis an Environment wrapper allowing forwrap.a = 1to set"a"in the env.- Also context manager:
with self.parent(a=2).child() as (p, c): p.a = 1; c.assign("a", 2) - Use
**kw_argsfor expected final parent/child env state instead of literal dicts.
- Also context manager:
- Book and CodeCrafters had some subtle differences in behavior; ust search code for
CRAFTING_INTERPRETERS()
funcmodels a Lox function as a normal python function. Removed LoxFunction entirely in d70adfb.- In order to support
this, we need to compose the function's environment with an outer environment withthis. - Instead of having a function, now we need to put captured variables in an format that can be accessed (a class instance with fields).
- In order to support
- Tried to compose
LoxFunctionforinit()returningthisbut ended up with a subclass as the "simplest" solution
- main script errors out if there are compiler/runtime 9000 errors.
- Use my JustAnotherYamlParser BNF evaluator
- I think it would produce the exact Parse Tree so we would need a light tree-transform into a workaround AST
- Could probably delete much of the Token / TokenType / Scanner / Parser modules?
- BUT, could you get most of the same errors?
- Would want to check test_parser coverage.
- Could this take the AST and generate the datatypes and visitor interface?
- How hard would it be to "just" enable FFI in a similar way to c# with decorators?
[DllImport("libc.so")]
private static extern int getpid();
[DllImport("user32.dll", SetLastError=true)]
static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint processId);implementation in python might look like what ChatGPT generated:
import ctypes
import ctypes.wintypes
libc = ctypes.CDLL("libc.so")
getpid = libc.getpid
getpid.restype = ctypes.c_int # Return type is int (ctypes default)
user32 = ctypes.WinDLL("user32.dll", use_last_error=True)
GetWindowThreadProcessId = user32.GetWindowThreadProcessId
GetWindowThreadProcessId.argtypes = [ctypes.wintypes.HWND, ctypes.POINTER(ctypes.wintypes.DWORD)]
GetWindowThreadProcessId.restype = ctypes.wintypes.UINTSee branch https://github.com/darthwalsh/codecrafters-interpreter-python/tree/wip-test-codecrafters-course
- [-] Parse the course definition locally, and make script to run the input/output test cases?? ❌ 2025-02-17
Running all test cases can be kind of slow. Compare running a trivial program from
- E2E tests that
imports main; 50 microseconds python3.12 -m app.main run: 400 millisecondspipenv run python3.12 -m app.main: 800 milliseconds = 10,000x slower to add pipenv and python overheads!
Also writing tests by hand can be a drag, so I had the idea to run smoke tests from github.com/codecrafters-io/build-your-own-interpreter course-definition.yml (MIT License)
Instead of invoking their test runner which takes an entire second per test case, I could have a python in-process loop that executes all tests "instantly" like this:
- Add requests and pyyaml to Pipfile
dev-packageswhich seems not to break the official test runner - Test runner downloads and caches
course-definition.yml - Parse YAML file for
.description_md - Parse the markdown (see branch TODO comments, got stuck making this robust)
- Load state from disk of course N expected to pass
- Execute test cases that are known to pass: course 1 through course N
- Attempt to pass course N+1, N+2, etc. which affects disk-state but not test-case result status
- Would be great to have these tests results from a file-tree-watcher
Other sources for test cases:
- figure out how to use https://github.com/codecrafters-io/interpreter-tester repo (not OSS) for testing: it has this template or this ANSI output, which also has a golang Lox golden implementation...
- https://github.com/codecrafters-io/course-sdk might be useful for running a course locally
Based on munificent/craftinginterpreters#1122 install dart 2:
brew install dart@2.12
brew unlink dart && brew link dart@2.12
Run git clone github.com:munificent/craftinginterpreters.git
Create an executable in craftinginterpreters folder
#!/bin/bash
arg=$(realpath $1)
cd /Users/walshca/code/codecrafters-interpreter-python
export CRAFTING_INTERPRETERS_COMPAT=1
exec python3 -m app.main run $argRun tests through implemented chapter:
dart tool/bin/test.dart chap12_classes --interpreter run.sh