Writing my own interpreter for Lox, Part 9 - Inheritance
This is the next part in writing my own interpreter for Lox in Python. Previously, we implemented classes with properties, methods, and initializers. In this post, we add inheritance, allowing classes to extend and specialize existing classes. Here is the corresponding chapter from Crafting Interpreters.
Inheritance is a crucial aspect of object-oriented programming enabling:
- Code reuse through class hierarchies
- Specialization of behavior through method overriding
- Polymorphism where subclass instances can be used wherever superclass instances are expected
Lox's inheritance model is single inheritance with method overriding and explicit superclass method calls via the super keyword.
Updated Grammar
The grammar now includes superclass declarations and the super keyword. Here's the updated grammar:
program → declaration* EOF ;
declaration → classDecl
| funDecl
| varDecl
| statement ;
classDecl → "class" IDENTIFIER ( "<" IDENTIFIER )? "{" function* "}" ;
funDecl → "fun" function ;
function → IDENTIFIER "(" parameters? ")" block ;
statement → exprStmt
| ifStmt
| printStmt
| returnStmt
| whileStmt
| block ;
expression → assignment ;
assignment → ( call "." )? IDENTIFIER "=" assignment
| logic_or ;
call → primary ( "(" arguments? ")" | "." IDENTIFIER )* ;
primary → "true" | "false" | "nil" | "this" | "super"
| NUMBER | STRING | IDENTIFIER
| "(" expression ")" ;
Key additions:
- Superclass syntax: Optional
< IDENTIFIERafter class name superkeyword: Access to superclass methods
Superclasses
Classes in Lox can declare a superclass using the < operator:
class Animal {
eat() {
print "Nom nom nom...";
}
}
class Dog < Animal {
bark() {
print "Woof!";
}
}
var dog = Dog();
dog.eat(); // prints "Nom nom nom..."
dog.bark(); // prints "Woof!"
Class Statement Updates
I updated the ClassStmt in app/stmt.py to include an optional superclass:
class ClassStmt(Stmt):
def __init__(self, name: Token, superclass: Variable, methods: list[FunctionStmt]):
self.name = name
self.superclass = superclass
self.methods = methods
def accept(self, visitor: StmtVisitor):
return visitor.visit_class_stmt(self)
The superclass is stored as a Variable expression because it's evaluated at runtime, not just a name reference.
Parsing Superclass Declarations
In app/parser.py, I updated class parsing to handle superclass declarations:
def class_declaration(self):
name = self.consume(TokenType.IDENTIFIER, "Expect class name.")
superclass = None
if (self.match(TokenType.LESS)):
self.consume(TokenType.IDENTIFIER, "Expect superclass name.")
superclass = Variable(self.previous())
self.consume(TokenType.LEFT_BRACE, "Expect '{' before class body.")
methods = []
while not self.check(TokenType.RIGHT_BRACE) and not self.is_at_end():
methods.append(self.function_declaration("method"))
self.consume(TokenType.RIGHT_BRACE, "Expect '}' after class body.")
return ClassStmt(name, superclass, methods)
The parser:
- Checks for
<token after the class name - If present, consumes an identifier and wraps it in a
Variableexpression - Passes the superclass (or
None) to theClassStmtconstructor
Runtime Superclass Validation
In app/interpreter.py, class declarations now evaluate and validate the superclass:
def visit_class_stmt(self, stmt: ClassStmt):
superclass = None
if (stmt.superclass is not None):
superclass = self.evaluate(stmt.superclass)
if not isinstance(superclass, LoxClass):
raise LoxRuntimeError(stmt.superclass.name, "Superclass must be a class.")
self.environment.define(stmt.name.lexeme, None)
if stmt.superclass is not None:
self.environment = Environment(self.environment)
self.environment.define("super", superclass)
methods = {}
for method in stmt.methods:
is_initializer = method.name.lexeme == "init"
function = LoxFunction(method, self.environment, is_initializer)
methods[method.name.lexeme] = function
klass = LoxClass(stmt.name.lexeme, superclass, methods)
if stmt.superclass is not None:
self.environment = self.environment.enclosing
self.environment.assign(stmt.name, klass)
return None
Key steps:
- Evaluate superclass: The superclass expression is evaluated to get the actual class object
- Validate: Ensure the superclass is actually a
LoxClassinstance - Create super environment: If there's a superclass, create a new environment with
superbound to it - Define methods: Methods are defined with access to the
superbinding - Create class: Pass the superclass to the
LoxClassconstructor - Clean up: Exit the super environment before assigning the class
The environment dance ensures that super is available inside method bodies but not elsewhere.
Inheriting Methods
When you call a method on an instance, if the method isn't found in the class, the lookup continues up the inheritance chain.
Updated LoxClass
I updated LoxClass in app/lox_class.py to store and search the superclass:
class LoxClass(LoxCallable):
def __init__(self, name: str, superclass, methods):
self.name = name
self.superclass = superclass
self.methods = methods
def __repr__(self):
return self.name
def arity(self):
initializer = self.find_method("init")
if initializer is not None:
return initializer.arity()
return 0
def call(self, interpreter, arguments):
instance = LoxInstance(self)
initializer = self.find_method("init")
if initializer is not None:
initializer.bind(instance).call(interpreter, arguments)
return instance
def find_method(self, name):
if name in self.methods:
return self.methods[name]
elif self.superclass is not None:
return self.superclass.find_method(name)
return None
The find_method() implementation:
- First checks if the method exists in the current class
- If not found and there's a superclass, recursively searches the superclass
- Returns
Noneif the method isn't found anywhere in the hierarchy
This simple recursive search implements the entire inheritance chain lookup. If you have class C < B < A, calling a method on a C instance will search C first, then B, then A.
Calling Superclass Methods
Sometimes a subclass wants to override a method but still call the superclass version. The super keyword provides this capability:
class Animal {
move() {
print "Moving...";
}
}
class Bird < Animal {
move() {
super.move(); // Call superclass method
print "Flapping wings!";
}
}
var bird = Bird();
bird.move();
// Output:
// Moving...
// Flapping wings!
Super Expression
I added a Super expression to app/expr.py:
class Super(Expr):
def __init__(self, keyword: Token, method: Token):
self.keyword = keyword
self.method = method
def accept(self, visitor: ExprVisitor):
return visitor.visit_super(self)
The super expression holds:
keyword: Thesupertoken (for error reporting)method: The method name to call on the superclass
Parsing super
In app/parser.py, I added super to primary expressions:
def primary(self):
# ... other cases
if self.match(TokenType.SUPER):
keyword = self.previous()
self.consume(TokenType.DOT, "Expect '.' after 'super'.")
method = self.consume(TokenType.IDENTIFIER, "Expect superclass method name.")
return Super(keyword, method)
# ... rest of primary
The parser enforces that super must be followed by . and a method name. This means super by itself is invalid - you must specify which superclass method you want.
Evaluating super
In app/interpreter.py, super expressions require careful handling:
def visit_super(self, expr: Super):
distance = self.locals[expr]
superclass = self.environment.get_at(distance, "super")
obj = self.environment.get_at(distance - 1, "this")
method = superclass.find_method(expr.method.lexeme)
if method is None:
LoxRuntimeError(expr.method, f"Undefined property '{expr.method.lexeme}'.")
return method.bind(obj)
The evaluation logic:
- Get superclass: Looks up
superat the resolved distance (from static analysis) - Get instance: Gets
this(the instance) from one environment closer (this one's a thinker - think why the instance is atdistance-1) - Find method: Searches for the method starting from the superclass
- Bind method: Binds the method to the current instance (not a superclass instance)
The distance arithmetic is subtle but important:
superis bound in the environment surrounding the methodthisis bound in the environment inside the method (one level deeper)- We need the current instance (
this) but the superclass from the outer environment
This ensures that super.method() calls the superclass method but operates on the current instance.
Resolver Updates
The resolver needs to enforce several invariants around inheritance:
supercan only be used inside methods of a subclass- A class cannot inherit from itself
- Proper scoping for
super
Tracking Subclasses
I updated the ClassType enum in app/resolver.py:
ClassType = Enum(
'ClassType',
[
'NONE', 'CLASS', 'SUBCLASS'
]
)
The SUBCLASS type distinguishes classes with superclasses from those without, enabling validation of super usage.
Resolving Class Declarations
I updated class resolution in app/resolver.py:
def visit_class_stmt(self, stmt: ClassStmt):
enclosing_class = self.current_class
self.current_class = ClassType.CLASS
self.declare(stmt.name)
self.define(stmt.name)
if (stmt.superclass is not None):
if (stmt.name.lexeme == stmt.superclass.name.lexeme):
ResolveError.error(self, stmt.superclass.name, "A class can't inherit from itself.")
else:
self.current_class = ClassType.SUBCLASS
self.resolve(stmt.superclass)
if (stmt.superclass is not None):
self.begin_scope()
self.scopes[-1]["super"] = True
self.begin_scope()
self.scopes[-1]["this"] = True
for method in stmt.methods:
if method.name.lexeme == "init":
self.resolve_function(method, FunctionType.INITIALIZER)
else:
self.resolve_function(method, FunctionType.METHOD)
self.end_scope()
if stmt.superclass is not None:
self.end_scope()
self.current_class = enclosing_class
return None
Key validation and scoping:
- Self-inheritance check: Catches
class Foo < Foo - Mark as subclass: Sets
current_classtoSUBCLASSif there's a superclass - Resolve superclass: Resolves the superclass variable reference
- Super scope: Creates an outer scope with
superbound before thethisscope - This scope: Inner scope with
thisfor method bodies - Scope cleanup: Exits both scopes in the correct order
The two-scope structure mirrors the runtime environment structure, ensuring that super resolves correctly.
Resolving super
I added super resolution in app/resolver.py:
def visit_super(self, expr):
if self.current_class == ClassType.NONE:
ResolveError.error(self, expr.keyword, "Can't use 'super' outside of a class.")
elif self.current_class != ClassType.SUBCLASS:
ResolveError.error(self, expr.keyword, "Can't use 'super' in a class with no superclass.")
self.resolve_local(expr, expr.keyword)
The validator checks:
- Not outside class:
supercan't be used at the top level or in functions - Only in subclass:
superrequires a superclass to exist
These checks catch errors at compile time rather than runtime:
print super.foo; // Error: Can't use 'super' outside of a class.
class Alone {
method() {
super.something(); // Error: Can't use 'super' in a class with no superclass.
}
}
Conclusion
With inheritance, our interpreter now supports the complete Lox specification and we have implementated all of it from scratch! We've implemented:
- Superclass declarations: Classes can extend other classes using
<syntax - Method inheritance and overriding: Subclasses automatically inherit superclass methods and can provide specialized implementations
- Super calls: Access to superclass methods via the
superkeyword - Static validation: Compile-time checks for inheritance errors
The implementation demonstrates following OOP concepts:
- Single inheritance: Each class has at most one direct superclass
- Method lookup chain: Searches from subclass up to superclass recursively
- Lexical super resolution:
superis resolved statically, not dynamically - Proper method binding: Super methods are bound to the current instance, not a superclass instance