Crafting Interpreters (IV): Resolver Lays the Track, Interpreter Fills the Values
Crafting Interpreters (IV): Resolver Lays the Track, Interpreter Fills the Values
How jlox runs class, this, inheritance, and super without inventing a second runtime
TL;DR: jlox’s object system is not a separate OOP universe. A class becomes a LoxClass. An instance becomes a LoxInstance. A method is still a LoxFunction. this and super are still names in environments. The resolver records where those names will be; the interpreter later puts the real values there.
The previous post stopped at the moment when a tree could run: expressions became values, statements became effects, environments held names, calls created local worlds, and closures let those worlds survive.
At first, classes look like a new continent. Objects, fields, methods, constructors, inheritance, this, super—surely this needs a whole new machine?
The surprise in jlox is that it mostly does not.
The object system is built out of the old pieces:
source syntax
→ parser shape
→ resolver distances
→ interpreter values
→ environment lookup
→ closure capture
That is the main thread for this article:
Start from Lox class syntax, then watch resolver and interpreter cooperate to make class / instance / this / inheritance / super actually run.

Figure 1. The class feature looks large, but the implementation keeps returning to the same two verbs: resolver records where names live; interpreter creates the real runtime values.
1. What a Lox class looks like from the outside
Before implementation, look at the surface language:
class Person {
init(name) {
this.name = name;
}
sayHi() {
print "Hi, I am " + this.name;
}
}
var p = Person("Luna");
p.sayHi();
This looks familiar if you have used JavaScript, Python, Java, or Ruby, but Lox makes a few deliberately small choices.
First, there is no new keyword. A class is called directly:
Person("Luna")
At runtime, the class object behaves like a callable. Calling it creates an instance.
Second, fields are not declared in the class body. The field appears when code writes it:
this.name = name;
That line puts a name field on the current instance. The class declaration does not contain a field list.
Third, methods do not use fun inside the class body:
sayHi() {
print this.name;
}
The parser already has a function-declaration structure, so methods reuse that structure, but the syntax is lighter.
Fourth, the initializer is called init, not Person:
init(name) {
this.name = name;
}
That means every class uses the same initializer name, and the class name remains just the class name.
Fifth, inheritance uses <:
class B < A {
test() {
super.method();
}
}
The < reads as “B is below A” or “B derives from A”. When B cannot find a method, lookup continues in A. When B wants to intentionally start lookup in A, it uses super.method().
That is the full story from the user’s side. The interesting part is that each surface feature maps cleanly onto the runtime model we already have.
2. Parser: make the shape, but do not run it
The parser’s job is not to know who this is. It is not to know what class super points to. It only has to recognize the structure.
This source:
class B < A {
test() {
super.method();
}
}
becomes a statement-shaped AST node:
ClassStmt
name: B
superclass: Variable(A)
methods:
FunctionStmt test(...)
body:
Super(method)
A class declaration is a statement because it introduces a name into the current environment. The methods inside the class reuse the existing function statement node, because a method has the same basic pieces as a function: a name, parameters, and a body.
The parser also adds expression nodes for the object syntax:
this → This expression
super.method → Super expression
object.field → Get expression
object.field = value → Set expression
The important boundary is this:
Parser recognizes shape. It does not attach runtime meaning.
When the parser sees this.name, it can produce a Get(This, name) shape. But it does not know which object this will be. When it sees super.method, it can produce a Super(method) shape. But it does not know the actual superclass object yet.
That missing information is split between the next two phases.
The resolver will decide where the names should be looked up. The interpreter will later create the objects stored at those locations.

Figure 2. The parser and resolver prepare the map. The interpreter fills the map with LoxClass, LoxInstance, fields, methods, and environments.
3. Resolver: lay the track, but do not put trains on it
The resolver does not execute code. It does not create an instance. It does not assign this to p. It performs static analysis before interpretation.
For classes, the resolver’s work is mostly about legality and position:
1. Are we inside a class?
2. Is this allowed here?
3. Is super allowed here?
4. How many environment hops will this need?
5. How many environment hops will super need?
6. Are method bodies' variable references resolved?
A useful metaphor is that the resolver lays railroad tracks. It decides which direction lookup should travel later. But no train is on the track yet.
Take a simple method:
class Person {
sayHi() {
print this.name;
}
}
When the resolver enters sayHi, it creates a temporary scope containing a name:
scope:
this
This is not a real runtime environment. It does not contain a real object. It is only a scope used by static analysis.
Then, when the resolver sees this, it can record:
this is N environments away from the future call environment
The interpreter stores that number in its locals map, keyed by the expression node. Later, when that exact this expression is evaluated, the interpreter does not need to search dynamically. It can jump directly to the right environment distance.
The key sentence is:
Resolver records positions, not values.
That sentence also explains several class-related errors. this outside a class is illegal because the resolver would have no class scope to attach it to. super inside a class with no superclass is illegal because there will be no super environment to look up at runtime. A class inheriting from itself is rejected because the superclass reference would point back to the same class name.
The resolver is not being clever about objects. It is being disciplined about names.
4. Interpreter: turn class syntax into runtime objects
Now execution begins.
When jlox executes a class declaration like this:
class Person {
init(name) {
this.name = name;
}
sayHi() {
print this.name;
}
}
it creates a LoxClass object and stores it in the current environment:
global environment
Person → LoxClass("Person")
Inside that LoxClass, methods are stored by name:
LoxClass("Person")
methods:
init → LoxFunction
sayHi → LoxFunction
Then this line runs:
var p = Person("Luna");
Because a class implements the callable interface, calling Person creates a LoxInstance:
global environment
Person → LoxClass("Person")
p → Person instance
The instance has its own field map:
Person instance
fields:
name → "Luna"
This gives us the most important object-system split:
class stores methods
instance stores fields
That split is why methods can be shared across all instances, while each instance can still have its own state.
It also explains property access. When jlox evaluates:
p.name
it looks in the instance’s field map. But when it evaluates:
p.sayHi
there probably is no field named sayHi, so it asks the instance’s class to find a method with that name. If the method exists, the interpreter does not return the raw function unchanged. It binds the method to the instance first.
That is where this becomes real.
5. this: resolver wrote the address; interpreter puts the object there
Look again at the call:
var p = Person("Luna");
p.sayHi();
The interesting operation is not the final pair of parentheses. It is the property access before the call:
p.sayHi
When the interpreter gets that method from the class, it performs:
sayHi.bind(p)
Binding creates a new environment whose parent is the method’s original closure:
this environment
this → p
Then the bound function is called. The call creates the usual call environment for parameters and locals, and that call environment points back to the this environment.
In shape, the chain looks like this:
sayHi call environment
local variables...
↓ enclosing
this environment
this → p
↓ enclosing
method closure
...
That means this is not magic in the interpreter. It is a name in an environment.
Here is the essence of method binding:
LoxFunction bind(LoxInstance instance) {
Environment environment = new Environment(closure);
environment.define("this", instance);
return new LoxFunction(declaration, environment, isInitializer);
}
The resolver already recorded how far the this expression should look. The interpreter now ensures the expected environment exists and contains the current instance.
So this expression:
this.name
means:
1. Use the resolver distance to get the value of this.
2. That value is p.
3. Read the name field from p's field map.

Figure 3. this becomes a normal environment binding created by bind(instance). The resolver records where to look; the interpreter creates the environment that makes the lookup succeed.
This is the heart of Chapter 12. jlox does not implement this as a hidden global variable or a special slot on the interpreter. It implements it as a captured environment binding.
The method is still a function. The object is supplied by binding that function to an instance.
6. init: a constructor is a special method call made by the class
Now the initializer is easy to understand.
When this runs:
var p = Person("Luna");
jlox does roughly this:
1. Create a new Person instance.
2. Look for a method named init.
3. Bind init to the new instance.
4. Call init("Luna").
5. Return the new instance.
The initializer body:
init(name) {
this.name = name;
}
uses the same this mechanism as every other method. There is no separate constructor binding rule.
init is special in two places:
class call automatically looks for init
initializer returns the new instance
The second point matters. Even if an initializer body falls off the end, the class call returns the instance. Even if init is called directly later, jlox treats initializer methods as returning this. And to avoid confusing programs, the resolver rejects returning a value from an initializer.
But the main model stays the same:
init is a method
method binding creates this
class call chooses to invoke init automatically
7. Inheritance begins as method lookup
Now move from Chapter 12 into Chapter 13.
Do not start with super. Start with the simplest inherited method:
class A {
method() {
print "A method";
}
}
class B < A {}
B().method();
The runtime rule is small:
B.findMethod("method")
if B has method: return it
otherwise ask A.findMethod("method")
Inheritance first changes method lookup. A subclass keeps a reference to its superclass, and findMethod() walks that chain when the method is missing on the subclass.
The instance is still a B instance:
B() → B instance
If method is found in A, that does not turn the object into an A instance. The method is merely found in A. When called, it still binds to the original B instance.
That detail becomes crucial for super.
8. super: change where lookup starts, not what this means
Here is the example that reveals the whole trick:
class A {
method() {
print this.name;
}
}
class B < A {
test() {
super.method();
}
}
var b = B();
b.name = "B instance";
b.test();
It prints:
B instance
Why?
A tempting but wrong explanation is: “super is the parent object.”
Lox does not have a parent object here. There is no separate A instance hiding inside the B instance. There is only one receiver:
b → B instance
The correct explanation is:
super decides where method lookup starts.
this decides which object the method runs on.
In the example:
super → A class
this → b
So:
super.method();
is approximately:
superclass.findMethod("method").bind(this)()
That means:
1. Start lookup in A, not B.
2. Find A.method.
3. Bind A.method to b.
4. Run A.method with this → b.
5. Inside A.method, this.name reads b.name.
The lookup starts in the superclass, but the receiver remains the current instance.

Figure 4. super and this cooperate. super points to the class where method search begins. this points to the instance that receives the method.
This is the most important sentence in Chapter 13:
superis not a different object. It is a different lookup starting point.
Once that clicks, the rest of the implementation becomes a problem of environment layout.
9. How resolver and interpreter make super work together
super is more subtle than this because it needs two values at once:
superclass: where should lookup start?
object: what should this be bound to?
The resolver cannot store either actual value. It still only records positions.
When resolving a subclass:
class B < A {
test() {
super.method();
}
}
it creates two temporary scopes around the methods:
super scope
super
this scope
this
method local scope
...
From inside test, the distances look like this:
method local scope distance 0
this scope distance 1
super scope distance 2
So the resolver records the distance for the super expression. It also relies on a layout rule: the this environment is always one hop inside the super environment.
At runtime, the interpreter mirrors that fake scope layout with real environments.
When executing the class declaration class B < A, it evaluates A and creates a superclass environment:
super environment
super → A class
Then it creates the methods of B. Those LoxFunction objects capture the environment where super is stored.
Later, when b.test is accessed, method binding creates the this environment:
this environment
this → b
↓ enclosing
super environment
super → A class
Finally, when super.method() is evaluated, the interpreter uses the resolver’s distance:
int distance = locals.get(expr);
LoxClass superclass = (LoxClass) environment.getAt(distance, "super");
LoxInstance object = (LoxInstance) environment.getAt(distance - 1, "this");
LoxFunction method = superclass.findMethod(expr.method.lexeme);
return method.bind(object);
That little distance - 1 is the whole trick. The resolver recorded where super lives. Because jlox controls the environment layout, the current this is one environment closer.
So the full super.method() story is:
Resolver:
record where super will be
arrange this one hop inside super
Interpreter at class declaration:
create super → A class
let B's methods capture it
Interpreter at method access:
bind this → b
Interpreter at super expression:
get superclass using distance
get this using distance - 1
find method starting at superclass
bind found method to current this
No separate OOP runtime is needed.
10. The compact mental model
By the end, the class system can be summarized as a table.

Figure 5. Every object-oriented feature maps back to the same implementation vocabulary: environments, closures, and resolver distances.
The whole story fits into two lines:
Resolver: precompute where names will be found.
Interpreter: put the real values in those places at runtime.
For this, the resolver says where this will be. The interpreter later binds this to the current instance.
For super, the resolver says where super will be. The interpreter later stores the superclass there, then binds the found method to the current instance.
For methods, the class stores the reusable function. The instance supplies the receiver.
For fields, the instance stores the data. The class does not declare it in advance.
That is why the object system feels surprisingly small once the pieces are connected.
jlox did not stop using the environment model when classes arrived. It leaned harder on that model.
Closing thought
Classes often feel like a language feature with their own private rules. In jlox, they become less mysterious when viewed as a continuation of functions and closures.
A method is a function with a receiver-shaped environment.
An initializer is a method the class calls automatically.
Inheritance is recursive method lookup.
super is a captured superclass plus the current this.
The resolver lays the track. The interpreter fills the values. And the same small runtime machine carries the language from functions to objects.