Classes
# Classes
Updating our grammar:
|
|
In plain English, a class declaration is the class keyword, followed by the class’s name, then a curly-braced body. Inside that body is a list of method declarations. Unlike function declarations, methods don’t have a leading fun keyword. Each method is a name, parameter list, and body.
Parser:
|
|
Interpreter:
|
|
# Instances
We create instances by calling a class name:
|
|
# Properties
Properties are accessed using a .
syntax. someObject.someProperty
Updating grammar:
|
|
After a primary expression, we allow a series of any mixture of parenthesized calls and dotted property accesses (i.e. get expressions).
# Get Expressions
A get expression stores the name and the expression: "Get: Expr object, Token name",
The get expression will call the get
method on a LoxInstance, returning named fields in the class:
|
|
# Set Expressions
Assignment now supports dotted identifiers on the left hand:
|
|
However, the reference to call
allows any high-precedence expression before the last dot, including any number of getters:
# Methods
For each method, we create a new LoxFunction and add that to the class via a hashmap.
|
|
# This
|
|
Does that last line print “Bill” because that’s the instance that we called the method through, or “Jane” because it’s the instance where we first grabbed the method?
Bound methods: if you take a reference to a method on some object so you can use it as a callback later, you want to remember the instance it belonged to, even if that callback happens to be stored in a field on some other object.
We need to take
this
at the point that the method is accessed and attach it to the function through a closure.
Put this into the current scope in resolver:
|
|
For each method, bind this into its closure environment:
|
|
# Constructors
Lox uses init
as a constructor.
Store whether a LoxFunction is an initializer or not
|
|
If it is, we get the bound instance in the function closure:
|
|
# Inheritance
Lox uses the <
to define an extends relationship:
|
|
The class expression must now capture the superclass relationship:
|
|
Look for the method in the current class before walking up the superclass chain:
|
|
# Super
With this
, the keyword works sort of like a magic variable, and the expression is that one lone token. But with super
, the subsequent .
and property name are inseparable parts of the super
expression. You can’t have a bare super
token all by itself.
|
|
The super expression contains the keyword and its method access:
|
|
a super expression starts the method lookup from “the superclass”, but which superclass? The naïve answer is the superclass of this, the object the surrounding method was called on. That coincidentally produces the right behavior in a lot of cases, but that’s not actually correct.
|
|
Instead, lookup should start on the superclass of the class containing the super
expression. In this case, since test()
is defined inside B, the super
expression inside it should start the lookup on B’s superclass—A.
One important difference is that we bound this
when the method was accessed. The same method can be called on different instances and each needs its own this
. With super
expressions, the superclass is a fixed property of the class declaration itself. Every time you evaluate some super
expression, the superclass is always the same.
That means we can create the environment for the superclass once, when the class definition is executed. Immediately before we define the methods, we make a new environment to bind the class’s superclass to the name super