Implementing BETA Semantics in CLox
最近看了Crafting Interpreters 这本书, 准备记录一下其中一个challenge的实现过程。是challenge 3,关于在clox中实现BETA。
Overview
In BETA semantics, method calls start at the top of the class hierarchy and walks downwards. A superclass method takes precedence over a subclass method. The inner keyword is introduced to allow superclass methods to call the overridden method in the subclass. An example looks like this:
1 | class Doughnut { |
What could be challenging?
Before implementing BETA semantics, when we call a method, we just look up the method in the class’s method table and that’s it. However, for BETA, we need to climb up the class inheritance tree to the top to see if we could get the method in any superclass. And that would be our starting point for calling the method.
What makes the story worse is that we could not throw the inner method away. We need to store it somewhere in the stack, so that when the top-level method calls inner(), we will be able to find it.
Where to put the inner method?
When capturing a method, we also captured this using ObjBoundMethod. For inner, we could also store it in ObjBoundMethod, so that we have a way to store all the inner methods (till the innermost one) in the stack.
graph TD
A["top level method calls inner()"] --"look up inner in the stack"--> B["ObjBoundMethod A (inner's method)"] --"read A.inner, place that in the stack"--> C["ObjBoundMethod B (inner's inner)"]
The new ObjBoundMethod looks like this (the fourth line is added):
1 | typedef struct { |
If a method does not have inner method, we could set its inner to be ObjNative nil, which will return nil.
We need to have a place to put inner in the stack. It could be the same as how we put this in the stack. If say BostonCream inherits Doughnut, the call stack for Doughnut cook will looks like this:
graph LR
A["(Doughnut cook call frame) this"]--> B["inner(BostonCream's cook())"] -->C["arg 0"] --> D["arg 1..."]
Implementation
When calling invokeFromClass:
- Get method from class’s method table
- If class has super
- if current class has the method, build a
ObjBoundMethodand stored it in the stack - if current class does not have the method, which means
inneris nil, put a nil method in the stack - Note that if
inneris not nil method, we still need to build aObjBoundMethodin the stack, so that when GC is happening,inneris not lost (later, we need to recursively callinvokeFromClassto reach the top-level class of the inheritance tree. It is possible that the top-level class does not implement the method we want to call. In that case, we should make sure the most-top-level class that implement the method does not get lost)
- if current class has the method, build a
- call
invokeFromClassusing super- if call is successful, just return true
- Otherwise, reset the
innerto the originalinner(this is possible as we enforced 2b.i.)
- If in step 1. we get the method from current class method table, call that method
- otherwise, return false, indicating calling the method is not successful.
1 | // Stack: receiver, inner, args... |
Optimization
One possible optimization is to create ObjBoundMethod only if the method is calling inner. We could store whether method is calling inner during compile time, and store that info inside ObjFunction. If we find a method we want to call, but it does not use inner(), then we could safely throw the inner in the stack away, and let GC to collect it.