We now move on to complete the interpreter by adding support for classes. They will also be able to inherit from other classes (i.e., reuse their properties and methods) while also customising them through modifications and additions.
What I built: Commits bac3f3c, bfa82b9, 03b3af3, 6c70087
What I understood:
1) Class Declarations:
- The parser creates a
Stmt.Classnode for each class declaration, containing the name and list of methods. - The resolver declares the class name in the current scope to allow the class to refer to itself (inside its own methods), for recursion.
- When the interpreter visits
Stmt.Class, it converts the AST node into aLoxClassobject, which stores a map of methods (which are in turn converted intoLoxFunctionobjects).
2) Class Instances/Objects:
- Lox doesn’t use any
newkeyword. - Instead,
LoxClassimplements theLoxCallableinterface such that a class is instantiated whenever called. - When the class is “called,” a new
LoxInstanceobject is instantiated and returned. - This new object stores a reference to its class (
LoxClass) so it can access methods later.
3) Properties:
- Fields aren’t declared in the class, but are created on assignment through instances.
- When the interpreter evaluates that an object is a
LoxInstance, it first callsget(), which checks the instance’s field map. If the field exists, the value is returned. Else, it looks for a method. If neither are found, it throws a runtime error.
Also, the parser can’t easily distinguish between a set and get expression until it encounters a
=sign. Thus, it parses the left-hand side as a regular expression (aget), and on encountering the=it converts that node into asetnode.If
set()is invoked, thefieldsmap is updated to overwrite any existing key.
4) Methods:
- Lox methods don’t have the
funkeyword preceding them and are accessed through instances. - If we extract a method (assign it to a variable) and call it later,
thismust still refer to the instance the method was accessed/extracted from, and not where it was called. - To achieve this, when a method is accessed,
LoxClassinstead of returning the rawLoxFunction, callsbind(instance)on it. -
bind()creates a new environment nested inside the method’s original closure, definesthisin that new environment, and uses it to return a newLoxFunction - If
thisis used outside a class, the resolver (which tracks if it’s currently inside a class) reports an error. Else, it resolvesthisas a local variable. - The interpreter looks up
thisin the environment like any other variable. Because of the binding step, the correct instance is found.
5) Constructors:
- We implement support for user-defined constructors called
init, which if found (byLoxClass.call), is called immediately to set up the new instance. Also, arity is checked here to ensure the class’ argument count matches the initialiser’s. -
initalways returnsthis(even if the user tries to return any value from inside the function). The resolver ensures this by tracking through anisInitialiserflag inLoxFunction.
6) Inheritance:
- Allows a subclass to inherit methods from a superclass through a reference to the latter (stored by the subclass’
LoxClass) and a method lookup walking up the chain. - The parser creates a
Stmt.Classnode that now includes asuperclassvariable (of typeExpr.Variable) - The resolver checks if a class tries to inherit from itself and reports an error if yes.
- If a superclass exists, the resolver creates a new scope and defines
superin it, ensuringsuperis available to all methods in the subclass. - While creating a subclass, the interpreter creates a new environment, defines
superto point to the superclass, and then creates the methods. - The parser recursively looks up a method by first checking the instance’s fields, then the class’ methods, and then the superclass, which if null, causes the lookup to fail.
7) Super:
- This keyword allows subclasses to override (their own version of) a method and access the original implementation in a parent class.
- It ensures that even if a chain of subclasses overrides a method,
superwill always point to the correct parent class definition. - It is followed by a dot and an identifier (
super.method) and is parsed as anExpr.Supernode. - It looks up a method on the superclass but binds it to the current instance (
this), with both belonging to different environments. - The resolver treats
superas a local variable and calculates its distance (number of hops) from the environment in whichsuperis defined.
What's next: Some extensions to Lox. Support for lists and for-in loops.
Musings:
Blogging this project has been really good for me. It probably is because despite doing other projects at the moment, trying to explain it to myself by writing encourages me to remember what I did (months ago!) and why. In a way, the insights I gained earlier continue to influence how I approach my current work. I absolutely love documenting things I do, even if its for the must mundane of activities. In Indian philosophy, work is among the highest acts of devotion. Whatever we do, if we do it with intent, it leads us to competence and mastery. Because it's the process of learning and doing something that helps us become our best versions. Tagore put it well — "Where tireless striving stretches its arms towards perfection..."
Where else will God lie but in that perfection? Hence, I remind myself of all that I've learnt.




Top comments (0)