RSS Feed

Lisp Project of the Day

asdf-finalizers

You can support this project by donating at:

Donate using PatreonDonate using Liberapay

asdf-finalizersasdfasdf-extension

This is a library written by François-René Rideau in days when he was the maintainer of the ASDF. This library allows you to move a piece of code, generated by a macro to the top-level of the lisp component.

This transformation happens during macro-expansion step.

As an example of this technique, "asdf-finalizers" contains a system "list-of" which defines a custom type "list-of":

POFTHEDAY> (asdf:load-system :list-of)
T
POFTHEDAY> (typep '(1 2 3 4 5)
                  '(list-of:list-of integer))
T
POFTHEDAY> (typep '(1 2 3 4 "Hello Lisp World")
                  '(list-of:list-of integer))
NIL
POFTHEDAY> (typep '((1 2 3))
                  '(list-of:list-of integer))
NIL
POFTHEDAY> (typep '((1 2 3)
                  (4 5 6))
                  '(list-of:list-of
                    (list-of:list-of integer)))
T
POFTHEDAY> (typep '((1 2 "Not an integer")
                  (4 5 6))
                  '(list-of:list-of
                    (list-of:list-of integer)))
NIL

It does this by creating a predicate function on the fly first time when you are using the type specifier. Type definition binds a function to the symbol and returns a type specifier (and list (satisfies list-of-integer-p)).

Of cause for different element types it uses their own predicate names.

To reproduce this type definition we could write such code:

POFTHEDAY> (deftype my-list-of (type)
             (let* ((name (format nil "LIST-OF-~S-P" type))
                    (predicate (intern name)))
               (format t "Creating predicate ~A for ~A~%" name type)
               (setf (symbol-function predicate)
                     (lambda (x)
                       (loop for c = x
                               then (cdr c)
                             while (consp c)
                             always (typep (car c)
                                           type)
                             finally (return (null c)))))
               `(and list
                     (satisfies ,predicate))))
MY-LIST-OF
POFTHEDAY> (typep '(1 2 3)
                  '(my-list-of string))
Creating predicate LIST-OF-STRING-P for STRING
NIL
POFTHEDAY> (typep '("foo" "bar")
                  '(my-list-of string))
T
POFTHEDAY> (typep '(1 2 3)
                  '(my-list-of integer))
Creating predicate LIST-OF-INTEGER-P for INTEGER
T
POFTHEDAY> (typep '(1 2 3)
                  '(my-list-of integer))
T

But if we'll put this naive type definition into a library "my-list-of", then there will be problems.

During the first loading a system, which uses "my-list-of", everything will be ok. But when you'll try to load it into a fresh Lisp image, this UNDEFINED-FUNCTION error will be signalled:

The function MYLIST::LIST-OF-INTEGER-P is undefined.

This is because of SBCL processes "(my-list-of integer)" definitions during macro expansion step, before compilation. But does not do this when loading a compiled FASL.

In other words, using "(my-list-of integer)" in the code causes side-effects during macro expansion step. And when you are loading a precompiled code into a fresh Lisp image, these side-effects are not available anymore.

"asdf-finalizer" solves this problem, by placing additional code into the (eval-when (:compile-toplevel :load-toplevel :execute)) block. This code may recreate a side-effect during the :load-toplevel phase.

Here is the original "list-of" type definition:

(deftype list-of (type)
  (case type
    ((t) 'list) ;; a (list-of t) is the same as a regular list.
    ((nil) 'null) ;; a (list-of nil) can have no elements, it's null.
    (otherwise
     (let ((predicate (list-of-predicate-for type)))
       (eval-at-toplevel ;; now, and amongst final-forms if enabled
        `(ensure-list-of-predicate ',type ',predicate)
        `(fboundp ',predicate) ;; hush unnecessary eval-at-toplevel warnings
        "Defining ~S outside of finalized Lisp code" `(list-of ,type))
       
       `(and list (satisfies ,predicate))))))

It uses a function "eval-at-toplevel", which helps "asdf-finalizer" to collect "(ensure-list-of-predicate 'integer 'list-of-integer-p)" forms into a special dynamic variables.

Later, these collected forms should be injected to the top-level with the call to "asdf-finalizers:final-forms":

(defpackage :test-list-of
  (:use :cl))
(in-package :test-list-of)

(defun check-list-of ()
  (typep '(1 2 3)
         '(list-of:list-of string)))

(final-forms)

Also, you'll need to replace :file with :finalized-cl-source-file in the components section of your ASDF system definition. This will turn on "top-level forms" collection mechanism for this file.

This call to final-forms will be expanded into the forms, injected into a special variable during the macro expansion stage:

(eval-when (:compile-toplevel :load-toplevel :execute)
  (ensure-list-of-predicate 'integer
                            'list-of-integer-p)
  (ensure-list-of-predicate 'string
                            'list-of-string-p)
  ...)

Each call to "ensure-list-of-predicate" will recreate a predicate function during when the compiled code will be loaded into the fresh Lisp image.

You can also use this technic to inject any code from macroses into the top-level. Just call "asdf-finalizers:eval-at-toplevel" or "asdf-finalizers:register-final-form" from the macro's code and don't forget to insert "(final-forms)" to the end of files where these macroses will be used.

As a bonus for everybody who is interested to learn how does code processing work in Common Lisp, there is a great @ngnghm's article about Common Lisp code processing stages and eval-when usage:

https://fare.livejournal.com/146698.html

Update

Fare shared a piece of history explaining why "asdf-finalizers" were created:


Brought to you by 40Ants under Creative Commons License