invokedynamic probably isn't useful for speeding up an interpretive system.
The time to consider invokedynamic is when the implementation uses
byte-compilation, and expects to perform calls in about 10 native instructions
(as opposed to hundreds or more).
Dynamism in Lisp (and Scheme) has a number of forms, and all of
the important ones (I think) can eventually benefit from JVM support:
1. Function calling can take a variable number of arguments,
and/or a final "spread" argument, and/or some keyworded arguments.
Making this fit together smoothly requires an interesting set of
calling sequences, with some on-the-fly argument transformation
when caller and callee do not exactly agree on number and type
of arguments. ("Type" usually just means restarg or not,
but it could mean, esp. in the JVM, more interesting things,
such as fixnum vs. 'any'.)
Example: (list 'a 'b 'c) might suppose it is passing two
positional arguments, but #'list probably expects a single
&rest parameter. A hard-compiled implementation, if
it cannot guess this when compiling the call site, might
issue an invokedynamic of call(Object,Object,Object)
to the object #'list, and rely on the signature mismatch
to be fixed by an adapter routine, generated on demand
as part of invokedynamic's signature mismatch processing.
The method which eventually implements 'list' needs a
signature like call$rest(List).
(Put another way, this is an important use case for giving
invokedynamic an efficient hook for managing signature
mismatches.)
In this example, 'list' is a well-known function, so compilers
will often special-case it. However, if you replace it by an
indirect call to a statically unknown function (a 'funcall')
then the problem of argument adaptation arises again
in full generality.
If you look at Neal G's Java closures proposal, you'll
see an open-ended set of interface types like 'call' above.
With invokedynamic, those interface types are not necessary.
That would be nice for Lisp. (I suppose current JVM implementors
avoid this problem by predefining 'call' interfaces for any
number of arguments up to some fixed small limit, and
use restargs for all other calls. This approach does not
scale well if you want to optimize a wider variety of
calling sequences, e.g., with unboxed numbers.)
2. Generic arithmetic, which requires some sort of
fast double dispatch. It should also be able to make
use of compile-time (and JIT-time) types.
Example: (+ A B) needs to compile to something
like A.op$plus(B) of signature Object(Object),
which is backed up by lots of secondary method
variants like Object(Integer). A simpler expression
(+ A 12) should ideally compile to something like
A.op$plus(12) of signature Object(int).
As above, with simple procedure types, it is possible
to get by without invokedynamic, but you have to cope
with a large number of interfaces to manage the
intermediate dispatches (or else one central
decision tree, which does not scale).
Another advantage of invokedynamic (properly
designed) for generic arithmetic is integration
with standard JVM classes like Integer and Double.
The current JVM invoke instructions cannot
invoke 'op$plus' directly on an Integer, forcing
implementors to compromises such as defining
their own MyInteger types.
3. Generic sequence operations, which want to
dispatch on the sequence type. They probably
want to use an interface which refines java.util.List
with Lisp-y additions, such as 'map', 'find', and 'concat'.
The value of invokedynamic here is that (as with
arithmetic) it can allow Lisp to use native String,
List, and array types as sequences, even if Lisp
has richer interfaces for its own language-specific
types (like multi-dimensional and/or resizable arrays).
Dynamic languages often treat strings, lists, and/or arrays
as multiple concrete implementations of a common sequence
type. If the JVM had a Lisp-y interface which all of those types
implemented, then it would be obvious how to make this work
for Lisp, but of course the JVM does not define Lisp-y interfaces,
and even if it did that might not help the next dynamic language.
The basic need is for something one might call "counterfeit traits".
If an object does not implement some desired interface (the 'trait'),
the language runtime system can fake up a set of methods for
the object, in terms of its natural capabilities. Example:
class FakeUpAdapter$123ABC implements List {
String original; ...
Object get(int n) { return original.charAt(n); } ...
}
I think invokedynamic should provide a way to do such fake-ups,
on a per-language basis, by trapping to a language-specific
error handler which can inspect types and provide a suitable
adapter method. (E.g., an adapter which connects Lisp's
'elt' operation to String.charAt, if the receiver is a String.)
The adapter method would then happily process any number
of similar calls in the future, without further visits to the error
handler.
(The simplest way to do this language-specific error
handler is to allow the bytecode compiler to emit a declaration,
for each compilation unit, of a class to which invocation
errors are directed. It does not work to put a catch-all
method on Object, as in the earliest proposals, since each
language has its own ideas about how to repair broken
calls. In addition, the error handler should report a method
back to the JVM which the JVM can then use for more than
one call.)
4, 5, .... Structs and CLOS. It's probably not time to talk about
that yet, but clean, scalable solutions to the previous cases
would provide a good start.
Cheers,
-- John