RSS Feed

Lisp Project of the Day

parse-declarations

You can support this project by donating at:

Donate using PatreonDonate using Liberapay

Or see the list of project sponsors.

parse-declarationsmacro

Documentation😀
Docstrings😀
Tests 😀
Examples😀
RepositoryActivity🥺
CI 🥺

This library provides advanced facilities to work with declaration forms and can be useful for writing macroses.

I wasn't able to imagine a good enough example, but luckily, this library has great documentation and I stole this example from it.

It is an analogue of the standard let* form which expands into multiple let forms. Maybe you know, let* can have declarations inside. But to process them correctly, you need to place a declaration related to each variable into its own let form.

Here is how this can be done with parse-declarations:

;; System and package names are different:
POFTHEDAY> (ql:quickload :parse-declarations-1.0)

POFTHEDAY> (import '(tcr.parse-declarations-1.0::parse-body
                     tcr.parse-declarations-1.0:filter-declaration-env
                     tcr.parse-declarations-1.0:build-declarations
                     tcr.parse-declarations-1.0:check-declaration-env
                     tcr.parse-declarations-1.0:parse-declarations))

POFTHEDAY> (defmacro custom-let* (bindings &body body &environment macro-env)
    (flet ((normalize-binding (binding)
             (cond ((symbolp binding)    `(,binding nil))
                   ((null (cdr binding)) `(,(car binding) nil))
                   (t binding))))
      (multiple-value-bind (real-body decls) (parse-body body :documentation nil)
        (let ((decl-env (parse-declarations decls macro-env)))
          (check-declaration-env decl-env :unknown-allowed nil :warn-only t)
          (labels ((generate-nested-lets (bindings &optional used-binding-names)
                     (if (null bindings)
                         `(locally
                              ,@(build-declarations 'declare
                                  (filter-declaration-env decl-env :include :free)
                                  (filter-declaration-env decl-env :include :bound
                                                          :not-affecting used-binding-names))
                            ,@real-body)
                         (destructuring-bind ((var value) . more-bindings) bindings
                           `(let ((,var ,value))
                              ,@(build-declarations 'declare
                                  (filter-declaration-env decl-env :affecting `(,var)))
                              ,(generate-nested-lets more-bindings (cons var used-binding-names)))))))
            (generate-nested-lets (mapcar #'normalize-binding bindings)))))))

Pay attention that parse-body function is intentionally is not exported to not conflict with alexandria:parse-body. Author considered that many lispers do import all alexandria's symbols into their packages.

By the way, Alexandria implements only a simple version of the parse-body function and does not support all advanced features of parse-declarations.

But let's see how our macro will expand!

POFTHEDAY> (custom-let* ((a 1)
                         (b (* a 0.01)))
             (declare (type fixnum a)
                      (type single-float b))
             (list a b))

;; It expands to ->
(let ((a 1))
  (declare (type fixnum a))
  (let ((b (* a 0.01)))
    (declare (type single-float b))
    (locally (list a b))))

As you can see, our single declaration form was torn apart and each part placed into the correct place inside nested lets. Great!

How does it work

There are three phases:

  • parse-declarations parses whole body passed to the custom-let* and extracts forms related to type declarations. It stores them in a special env object.
  • filter-declaration-env allows us to select only declarations related to the needed variable.
  • build-declarations transforms one or many env objects into the lists of symbols so that they can be used for the macro-expansion.

Brought to you by 40Ants under Creative Commons License