Compiler Design – Writing a Compiler for Languages with Runtime Code Rewriting

compilerlispmacros

There are some programming languages, like the many dialects of Lisp, that allow for macro-metaprogramming: rewriting and altering sections of code before the code is run.

It is relatively trivial to write a simple interpreter for Lisp (mostly because there is only very little special syntax). However, I cannot understand how it would be possible to write a compiler for a language that allows you to rewrite code at-runtime (and then execute that code).

How is this done? Is the compiler itself basically included in the generated compiled program, such that it can compile new sections of code? Or is there another way?

Best Answer

Macros have the advantage to be expanded at compile time

The idea of Lisp macros is to be able to fully expand them at compile time. Then no compiler is needed at runtime. Most Lisp systems allow you to fully compile code. The compilation step includes the macro expansion phase. There is no expansion needed at runtime.

Often Lisp systems include a compiler, but this is needed when code is generated at runtime and this code would need to be compiled. But this is independent of macro expansion.

You will even find Lisp systems which don't include a compiler and even no full interpreter at runtime. All code will be compiled before runtime.

FEXPRs were code modifying functions, but were mostly replaced by Macros

In earlier times in the 60s/70s many Lisp systems included so-called FEXPR functions, which could translate code at runtime. But they could not be compiled before runtime. Macros replaced them mostly, since they enable full compilation.

An example of a macro interpreted and compiled

Let's look at LispWorks, which has both an interpreter and a compiler. It allows to mix interpreted and compiled code freely. The Read-Eval-Print-Loop uses the Interpreter to execute code.

Let's define a trivial macro. But the macro prints the code it gets called with, every time the macro runs.

CL-USER 45 > (defmacro my-if (test yes no)
               (format t "~%Expanding (my-if ~a ~a ~a)" test yes no)
               `(if ,test ,yes ,no))
MY-IF

Let's define a function which uses the macro from above. Remember: here in LispWorks the function will be interpreted.

CL-USER 46 > (defun test (x y)
               (my-if (> x y) 'larger 'not-larger))
TEST

If you look above, the Lisp system only printed the function name. The macro did not run - otherwise the macro would have printed something. So the code is not expanded.

Let's run the TEST function using the Interpreter:

CL-USER 47 > (loop for i below 5 collect (test i 3))

Expanding (my-if (> X Y) (QUOTE LARGER) (QUOTE NOT-LARGER))
Expanding (my-if (> X Y) (QUOTE LARGER) (QUOTE NOT-LARGER))
Expanding (my-if (> X Y) (QUOTE LARGER) (QUOTE NOT-LARGER))
Expanding (my-if (> X Y) (QUOTE LARGER) (QUOTE NOT-LARGER))
Expanding (my-if (> X Y) (QUOTE LARGER) (QUOTE NOT-LARGER))
Expanding (my-if (> X Y) (QUOTE LARGER) (QUOTE NOT-LARGER))
Expanding (my-if (> X Y) (QUOTE LARGER) (QUOTE NOT-LARGER))
Expanding (my-if (> X Y) (QUOTE LARGER) (QUOTE NOT-LARGER))
Expanding (my-if (> X Y) (QUOTE LARGER) (QUOTE NOT-LARGER))
Expanding (my-if (> X Y) (QUOTE LARGER) (QUOTE NOT-LARGER))
(NOT-LARGER NOT-LARGER NOT-LARGER NOT-LARGER LARGER)

So you see that for some reason the macro expansion is run twice for each of the five calls to test. The macro is expanded by the interpreter every time the function TEST is called.

Now let's compile the function TEST:

CL-USER 48 > (compile 'test)

Expanding (my-if (> X Y) (QUOTE LARGER) (QUOTE NOT-LARGER))
TEST
NIL
NIL

You can see above that the compiler runs the macro once.

If we now run the function TEST, no macro expansion will happen. The macro form (MY-IF ...) has already been expanded by the compiler:

CL-USER 49 > (loop for i below 5 collect (test i 3))
(NOT-LARGER NOT-LARGER NOT-LARGER NOT-LARGER LARGER)

If you used some other Lisps like SBCL or CCL, they will compile everything by default. SBCL has in new versions also an interpreter. Let's do the example from above in a recent SBCL:

Let's use the new SBCL interpreter:

CL-USER> (setf sb-ext:*evaluator-mode* :interpret)
:INTERPRET

CL-USER> (defmacro my-if (test yes no)
           (format t "~%Expanding (my-if ~a ~a ~a)" test yes no)
           `(if ,test ,yes ,no))
MY-IF
CL-USER> (defun test (x y)
           (my-if (> x y) 'larger 'not-larger))
TEST
CL-USER> (loop for i below 5 collect (test i 3))

Expanding (my-if (> X Y) 'LARGER 'NOT-LARGER)
Expanding (my-if (> X Y) 'LARGER 'NOT-LARGER)
Expanding (my-if (> X Y) 'LARGER 'NOT-LARGER)
Expanding (my-if (> X Y) 'LARGER 'NOT-LARGER)
Expanding (my-if (> X Y) 'LARGER 'NOT-LARGER)
(NOT-LARGER NOT-LARGER NOT-LARGER NOT-LARGER LARGER)
CL-USER> (compile 'test)

Expanding (my-if (> X Y) 'LARGER 'NOT-LARGER)
TEST
NIL
NIL
CL-USER> (loop for i below 5 collect (test i 3))
(NOT-LARGER NOT-LARGER NOT-LARGER NOT-LARGER LARGER)
CL-USER> 
Related Topic