DEV Community

Lahari Tenneti
Lahari Tenneti

Posted on

Classes and Inheritance

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.Class node 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 a LoxClass object, which stores a map of methods (which are in turn converted into LoxFunction objects).


2) Class Instances/Objects:

  • Lox doesn’t use any new keyword.
  • Instead, LoxClass implements the LoxCallable interface such that a class is instantiated whenever called.
  • When the class is “called,” a new LoxInstance object 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 calls get(), 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 (a get), and on encountering the = it converts that node into a set node.

  • If set() is invoked, the fields map is updated to overwrite any existing key.

4) Methods:

  • Lox methods don’t have the fun keyword preceding them and are accessed through instances.
  • If we extract a method (assign it to a variable) and call it later, this must 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, LoxClass instead of returning the raw LoxFunction, calls bind(instance) on it.
  • bind() creates a new environment nested inside the method’s original closure, defines this in that new environment, and uses it to return a new LoxFunction
  • If this is used outside a class, the resolver (which tracks if it’s currently inside a class) reports an error. Else, it resolves this as a local variable.
  • The interpreter looks up this in 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 (by LoxClass.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.
  • init always returns this (even if the user tries to return any value from inside the function). The resolver ensures this by tracking through an isInitialiser flag in LoxFunction.

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.Class node that now includes a superclass variable (of type Expr.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 super in it, ensuring super is available to all methods in the subclass.
  • While creating a subclass, the interpreter creates a new environment, defines super to 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, super will always point to the correct parent class definition.
  • It is followed by a dot and an identifier (super.method) and is parsed as an Expr.Super node.
  • 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 super as a local variable and calculates its distance (number of hops) from the environment in which super is 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)