- Notifications
You must be signed in to change notification settings - Fork 153
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:
-
farg0throughfarg4retrieve the 1st-5th argument and store it in theARGregister. -
argn nretrieves the nth argument and stores it in theARGregister. -
tag-test fixnum cons single char generaldispatches 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-readsets theSTAMPregister to the value ofARG's stamp. -
lt-branch pivoti leftbranches toleftifSTAMPis less than the pivot, which is the integer stored in the literal table at indexpivoti. -
eq-check pivotichecks ifSTAMPequals the integer in the literal table atpivoti; if it doesn't, dispatch miss. -
range-check low highchecks ifSTAMPis between (inclusive) the low and high values in the literal table; if it's not, dispatch miss. -
eql obji falsebchecks ifARGiseqlto the object in the literal table, and if it is, branches tofalseb. -
slot-read index slot-namereads theindex-th slot of the first parameter to the generic function. If the slot is unbound,slot-unboundis called using theslot-nameargument. This is an outcome, i.e. interpretation stops after this. -
slot-write indexwrites the first parameter into theindexth slot of the second parameter. Also an outcome. -
car cons slot-namereads the value fromcons, a literal cons. If it's unbound,slot-unboundis called with theslot-name. This is used to implement slots with:classallocation. Outcome. -
rplaca conswrites the first argument into the car ofcons. Also used for class slots. Outcome. -
effective-method-outcome emfcalls the effective method function with the arguments passed to the generic function. Outcome.