There are times when being in control of the whole software stack is a mixed blessing.
While doing investigations related to my
previous post,
I found myself wondering what the arguments and return values of
make-method-lambda
were in practice, in SBCL. So I did what any
self-respecting Lisp programmer would do, and instead of following
that link and decoding the description, I simply ran
(trace sb-mop:make-method-lambda), and then ran my defmethod as
normal. I was half-expecting it to break instantly, because the
implementation of trace encapsulates named functions in a way that
changes the class of the function object (essentially, it wraps the
existing function in a new anonymous function; fine for ordinary
functions, not so good for generic-function objects), and I was
half-right: an odd error occurred, but after trace printed the
information I wanted.
What was the odd error? Well, after successfully calling and
returning from make-method-lambda, I got a no-applicable-method
error while trying to compute the applicable methods
for... make-method-lambda. Wait, what?
SBCL's CLOS has various optimizations in it; some of them have been
documented in the
SBCL Internals Manual, such as
the clever things done to make slot-value fast, and specialized
discriminating functions. There are plenty more that are more opaque
to the modern user, one of which is the “fast method call”
optimization. In that optimization, the normal calling convention for
methods within method combination, which involves calling the method's
method-function with two arguments – a list of the arguments passed
to the generic function, and a list of next methods – is bypassed,
with the fast-method-function instead being supplied with a
permutation vector (for fast slot access) and next method call (for
fast call-next-method) as the first two arguments and the generic
function's original arguments as the remainder, unrolled.
In order for this optimization to be valid, the call-method calling
convention must be the standard one – if the user is extending or
overriding the method invocation protocol, all the optimizations based
on assuming that the method invocation protocol might be invalid.
We have to be conservative, so we need to turn this optimization off
if we can't prove that it's valid – and the only case where we can
prove that it's valid is if only the system-provided method on
make-method-lambda has been called. But we can't communicate that
after the fact; although make-method-lambda returns initargs as well
as the lambda, an extending method could arbitrarily mess with the
lambda while returning the initargs the system-provided method
returns. So in order to find out whether the optimization is safe, we
have to check whether exactly our system-provided method on
make-method-lambda was the applicable one, so there's an explicit
call to compute-applicable-methods of make-method-lambda after
the method object has been created. And make-method-lambda being
traced and hence not a generic-function any more, it's normal that
there's an error. Hooray! Now we understand what is going on.
As for how to fix it, well, how about adding an encapsulations slot
to generic-function objects, and handling the encapsulations in
sb-mop:compute-discriminating-function?
The encapsulation implementation as it currently stands is fairly
horrible, abusing as it does special variables and chains of closures;
there's a fair chance that encapsulating generic functions in this way
will turn out a bit less horrible. So, modify
sb-debug::encapsulate, C-c C-c, and package locks strike. In
theory we are meant to be able to unlock and continue; in practice,
that seems to be true for some package locks but not others.
Specifically, the package lock from setting the fdefinition from a
non-approved package gives a continuable error, but the ones from
compiling special declarations of locked symbols have already taken
effect and converted themselves to run-time errors. Curses. So,
(mapcar #'unlock-package (list-all-packages)) and try again; then,
it all goes well until adding the slot to the generic-function class
(and I note in passing that many of the attributes that CL specifies
are generic-function SBCL only gives to standard-generic-function
objects), at which point my SLIME repl tells me that something has
gone wrong, but not what, because no generic function works any more,
including print-object. (This happens depressingly often while
working on CLOS).
That means it's time for an SBCL rebuild, which is fine because it
gives me time to write up this blog entry up to this point. Great,
that finishes, and now we go onwards: implementing the functionality
we need in compute-discriminating-function is a bit horrible, but
this is only a proof-of-concept so we wrap it all up in a labels and
stop worrying about 80-column conventions. Then we hit C-c C-c and
belatedly remember that redefining methods involves removing them from
their generic function and adding them again, and doing that to
compute-discriminating-function is likely to have bad consequences.
Sure enough:
There is no applicable method for the generic function
#<STANDARD-GENERIC-FUNCTION COMPUTE-DISCRIMINATING-FUNCTION (1)>
when called with arguments
(#<STANDARD-GENERIC-FUNCTION NO-APPLICABLE-METHOD (1)>).
Yes, well. One (shorter) rebuild of just CLOS later, and then a few more edit/build/test cycles, and we can trace generic functions without changing the identity of the fdefinition. Hooray! Wait, what was I intending to do with my evening?