-
Notifications
You must be signed in to change notification settings - Fork 149
dtrees
Clasp has two implementations of discriminating functions. The first generates Lisp code that can be compiled to produce a regular Lisp function to use as the discriminator. This is presently disabled due to metacircularity difficulties, and even aside from that, is slow enough that it's only useful for very frequently called functions. The other is the "dtree interpreter", described here. Most of the dtree code lives in src/lisp/clos/dtree.lisp
; the actual VM is defined in src/core/funcallableInstance.cc
.
A dtree (discrimination tree) is a simple tree structure representing the different combinations of arguments encountered by a generic function. For example, say we have a function
(defmethod foo ((a z) (b z)) ...)
(defmethod foo ((a z) (b y)) ...)
(defmethod foo ((a y) (b z)) ...)
(defmethod foo ((a y) (b y)) ...)
Then after all combinations of arguments have appeared, the dtree would basically look like this:
root - Z - Z - method 1
- Y - method 2
- Y - Z - method 3
- Y - method 4
At each level of the tree, we can see what possibilities there are for the corresponding argument. At the first level we see that the argument can be a Z or a Y, and the same for the second level in both branches. If we hadn't seen a call with Y Y before, the dtree under Y would only have a node for Z.
There is an additional wrinkle. If a parameter is not specialized by the generic function, the dtree will generate a special "skip" node corresponding to that parameter. E.g. given
(defmethod bar (a (b z)) ...)
(defmethod bar (a (b y)) ...)
the dtree might look like
root - skip - z - method 1
- y - method 2
The clos::basic-tree
function produces a dtree from a call history and specializer profile.
Each leaf of the tree is an "outcome". Outcomes are representations of the effective method to execute. Usually this is just an effective method function, but for standard reader or writer methods it will be a specialized structure, because standard reads and writes are done without using function calls (e.g. in the dtree VM described below).
Outcomes are used outside of the dtree mechanism as well.
The above sorts of dtrees are called "basic trees" internally, and they are very easily produced from call histories. When producing a discriminator, the basic tree will be "compiled". The result is another tree (or more generally, DAG) of nodes, but each node represents a much more concrete test. It is at this point that Clasp decides whether testing an object is of a class involves testing tags, testing stamps, or etc. In general it will produce a binary search tree using stamp values.
dtree compilation is performed by the clos::compile-tree-top
function.
The compiled tree is then further compiled into actual bytecode. This bytecode runs on a different virtual machine from the Common Lisp VM, because it is very specialized - it needs to perform direct type tests, and it does not need sophisticated control structures (e.g. there are no loops).
The VM is defined in src/lisp/kernel/cmp/bytecode-machines.lisp
and implemented in src/core/dtree-interpreter.cc
. Briefly, a bytecoded discriminating function consists of a vector of bytes (the code) and a vector of objects (used by the code, containing e.g. effective method functions). There are two registers, ARG
and STAMP
. Each instruction has zero or more operands, which are the successive bytes in the bytecode. If an instruction is preceded by the long
modifier (another byte), each operand is instead two bytes, interpreted as a big-endian integer.
In the below, a dispatch miss means that a novel combination of arguments has been used. This takes us to the slow path, where we add a new entry to the call history, recompute the discriminating function, and call the new function. A higher level description of how our CLOS works is available in src/lisp/kernel/clos/README
.
The instructions are as follows:
-
farg0
throughfarg4
retrieve the 1st-5th argument and store it in theARG
register. -
argn n
retrieves the nth argument and stores it in theARG
register. -
tag-test fixnum cons single char general
dispatches to one of five branches based on the tag ofARG
. The tested tags are for fixnums, conses, single floats, characters, and general objects. This is closely tied to the details of Clasp's object representation. -
stamp-read
sets theSTAMP
register to the value ofARG
's stamp. -
lt-branch pivoti left
branches toleft
ifSTAMP
is less than the pivot, which is the integer stored in the literal table at indexpivoti
. -
eq-check pivoti
checks ifSTAMP
equals the integer in the literal table atpivoti
; if it doesn't, dispatch miss. -
range-check low high
checks ifSTAMP
is between (inclusive) the low and high values in the literal table; if it's not, dispatch miss. -
eql obji falseb
checks ifARG
iseql
to the object in the literal table, and if it is, branches tofalseb
. -
slot-read index slot-name
reads theindex
-th slot of the first parameter to the generic function. If the slot is unbound,slot-unbound
is called using theslot-name
argument. This is an outcome, i.e. interpretation stops after this. -
slot-write index
writes the first parameter into theindex
th slot of the second parameter. Also an outcome. -
car cons slot-name
reads the value fromcons
, a literal cons. If it's unbound,slot-unbound
is called with theslot-name
. This is used to implement slots with:class
allocation. Outcome. -
rplaca cons
writes the first argument into the car ofcons
. Also used for class slots. Outcome. -
effective-method-outcome emf
calls the effective method function with the arguments passed to the generic function. Outcome.