Interpretation: Eval

Many people think of Scheme, Lisp, and ECMAScript as “interpreted” languages, though many of these languages have compilers. What these languages do have is eval - that is a command that at run-time takes a source program, and evaluates it. They may also have an interactive read-eval-print interface. For such uses a traditional interpreter is easiest and most responsive. Therefore, high-end Lisp systems traditionally provide both a compiler and an interpreter. Such duplication is expensive, in terms of size, development effort, and testing. If one has load-and-go capabilities, that is the abilility to efficiently load a compiled program into a running application, then one can simply implement eval as a compile followed by a load.

When we compile to Java bytecodes, we create one or more files in the .class format. The standard Java class java.lang.ClassLoader has a method defineClass that takes a byte array laid out in the format of a .class, and from it dynamically creates a new class in the existing Java run-time. (This facility is used for “applets” downloaded accross the Network.) Kawa uses this scheme to implement eval, and it works well. Because ClassLoader.defineClass takes an array, rather than a file, we can compile and load entirely inside the Kawa run-time, without having to go via the filesystem for temporary files, as a traditional compiler batch does. The result is near-instant response.

There is a tradeoff, though. Doing a compile+load is a very heavy-duty operation, compared to a simply interpreting an expression. It creates a lot of temporary objects. Worse, it also creates some temporary classes, and some Java implementations do not garbage collect unused classes.

Kawa uses a compromise strategy. If the Expression is “simple”, it is interpreted directly, using the Expression.eval. Otherwise, it is compiled. Simple expressions include literals, (global) variable access, assignment, and function application. Implementing eval in those cases is trivial. Expressions that define new local bindings (such as lambda expressions and let forms) do not implement eval. If the user types in such an expression, it is wrapped inside a dummy function, compiled to bytecodes, and immediately executed. This is to avoid dealing with lexical binding in the evaluator.

A ModuleExp represents a top-level form:

class ModuleExp extends LambdaExp
{ ...;
  public Object eval_module
      (Environment env) {
    if (body_is_simple) // Optimization
      return body.eval (env);
    Object v = eval (env);
    return ((ModuleBody) v).run (env);

ModuleExp is a sub-class of LambdaExp, since it is actually a dummy function created by wrapping the top-level forms in an implicit lambda. The eval_module method evaluates the top-level forms. If the body is not simple, it invokes the eval in LambdaExp (which invokes the compiler). The result of eval is a ModuleBody, which we can run.