Newer
Older
Francois-Rene Rideau
committed
;;; -*- Mode: Lisp ; Base: 10 ; Syntax: ANSI-Common-Lisp -*-
;;; pattern-matching friendly implementation of Quasiquote
;;; Copyright (c) 2002-2014 Fahree Reedaw <fare@tunes.org>
;;; See README
Francois-Rene Rideau
committed
#+xcvb (module (:depends-on ("packages")))
(in-package :fare-quasiquote)
(declaim (optimize (speed 1) (safety 3) (debug 3)))
(eval-when (:compile-toplevel :load-toplevel :execute)
;;;; uncomment some of the lines below to disable according simplifications:
Francois-Rene Rideau
committed
;;(pushnew :quasiquote-strict-append *features*)
;;(pushnew :quasiquote-passes-literals *features*)
Francois-Rene Rideau
committed
;;(pushnew :quasiquote-at-macro-expansion-time *features*)
Francois-Rene Rideau
committed
(eval-when (:compile-toplevel :load-toplevel :execute)
;;; Functions that actually build data structures.
;; Note that we want our own tokens for decompilation reasons,
;; but as functions they must evaluate the usual way.
(defsubst list (&rest r) r) ;; (apply #'cl:list r)
(defsubst list* (&rest r) (apply #'cl:list* r))
(defsubst cons (x y) (cl:cons x y))
(defsubst append (&rest r) (apply #'cl:append r))
(defsubst nconc (&rest r) (apply #'cl:nconc r))
;; These supporting functions don't have a standard name
(defsubst make-vector (l) (coerce l 'simple-vector))
(defun n-vector (n contents)
(if (null n) (make-vector contents)
(let ((a (make-array n :element-type t)))
(when (and (null contents) (> n 0))
(error "non-zero length vector with empty contents"))
(loop for i below n with x
do (unless (null contents) (setq x (pop contents)))
do (setf (aref a i) x))
(when contents
(error "provided contents larger than declared vector length"))
a)))
Francois-Rene Rideau
committed
;;; These functions build the forms that build the data structures.
Francois-Rene Rideau
committed
(make-single-arg-form quote kwote)
(make-single-arg-form quasiquote)
(make-single-arg-form unquote)
(make-single-arg-form unquote-splicing)
(make-single-arg-form unquote-nsplicing)
(defun k-list (&rest r) (cons 'list r))
Francois-Rene Rideau
committed
(defun k-list-p (x) (and (consp x) (eq (car x) 'list)))
(defun k-list* (&rest r) (cons 'list* r))
Francois-Rene Rideau
committed
(defun k-list*-p (x) (and (consp x) (eq (car x) 'list*)))
(defun k-cons (x y) (list 'cons x y))
Francois-Rene Rideau
committed
(defun k-cons-p (x) (and (consp x) (eq (car x) 'cons)))
(defun k-append (&rest r) (cons 'append r))
Francois-Rene Rideau
committed
(defun k-append-p (x) (and (consp x) (eq (car x) 'append)))
(defun k-nconc (&rest r) (cons 'nconc r))
Francois-Rene Rideau
committed
(defun k-nconc-p (x) (and (consp x) (eq (car x) 'nconc)))
(defun k-literal (literal)
#+quasiquote-passes-literals literal
#-quasiquote-passes-literals (kwote literal))
;;; These macros expand into suitable forms
Francois-Rene Rideau
committed
(defmacro quote (x) (list 'cl:quote x))
(defmacro quasiquote (x) (quasiquote-expand x))
(defmacro unquote (x)
(declare (ignore x))
(error "unquote only allowed within quasiquote"))
(defmacro unquote-splicing (x)
(declare (ignore x))
(error "unquote-splicing disallowed outside quasiquote"))
(defmacro unquote-nsplicing (x)
(declare (ignore x))
(error "unquote-nsplicing disallowed outside quasiquote"))
Francois-Rene Rideau
committed
(defun quasiquote-form-p (x)
(or (quotep x) (k-list-p x) (k-list*-p x) (k-cons-p x) (k-append-p x) (k-nconc-p x) (k-n-vector-p x)))
(defun k-n-vector (n l)
(cond
((null l)
(k-literal (vector)))
((quotep l)
(k-literal (n-vector n (single-arg l))))
(n
(list 'n-vector n l))
(t
(list 'make-vector l))))
(defun k-n-vector-p (x) (and (consp x) (member (first x) '(make-vector n-vector))))
(defun valid-k-n-vector-p (x)
(or (and (length=n-p x 3) (eq (first x) 'n-vector)
(typep (second x) `(or null (integer 0 ,array-rank-limit)))
Francois-Rene Rideau
committed
#+quasiquote-strict-append
Francois-Rene Rideau
committed
(quasiquote-form-p (third x)))
(and (length=n-p x 2) (eq (first x) 'make-vector)
Francois-Rene Rideau
committed
#+quasiquote-strict-append
Francois-Rene Rideau
committed
(quasiquote-form-p (second x)))))
(defun k-n-vector-n (x)
(and (valid-k-n-vector-p x) (eq (first x) 'n-vector) (second x)))
(defun k-n-vector-contents (x)
(and (valid-k-n-vector-p x)
(ecase (first x) ((make-vector) (second x)) ((n-vector) (third x)))))
(defun properly-ended-list-p (x)
(and (listp x) (null (cdr (last x)))))
Francois-Rene Rideau
committed
(defparameter *quasiquote-level* 0
"current depth of quasiquote nesting")
(defun unquote-xsplicing-p (x)
(or (unquote-splicing-p x) (unquote-nsplicing-p x)))
(defun quasiquote-expand (x)
(let ((*quasiquote-level* 0))
(multiple-value-bind (top arg)
(quasiquote-expand-0 x)
(when (eq top 'unquote-splicing)
(error ",@ after backquote in ~S" x))
(when (eq top 'unquote-nsplicing)
(error ",. after backquote in ~S" x))
(quasiquote-expand-1 top arg))))
(defun quasiquote-expand-0 (x)
"Given an expression x under a backquote, return two values:
1- a token identifying the context: nil quote :literal list list* append nconc
2- a form
Francois-Rene Rideau
committed
When combining backquoted expressions, tokens are used for simplifications."
(cond
((null x)
(values nil nil))
((literalp x)
(values #+quasiquote-passes-literals :literal #-quasiquote-passes-literals 'quote x))
Francois-Rene Rideau
committed
((or (symbolp x) (quotep x))
(values 'quote x))
((unquote-splicing-p x)
(values 'unquote-splicing (single-arg x)))
((unquote-nsplicing-p x)
(values 'unquote-nsplicing (single-arg x)))
((unquotep x)
(values 'unquote (single-arg x)))
((quasiquotep x)
(quasiquote-expand-0 (quasiquote-expand (single-arg x))))
Francois-Rene Rideau
committed
((k-n-vector-p x)
Francois-Rene Rideau
committed
(values (car x) (cdr x)))
Francois-Rene Rideau
committed
((consp x)
(multiple-value-bind (atop a) (quasiquote-expand-0 (car x))
(multiple-value-bind (dtop d) (quasiquote-expand-0 (cdr x))
(when (eq dtop 'unquote-splicing)
(error ",@ after dot"))
(when (eq dtop 'unquote-nsplicing)
(error ",. after dot"))
(cond
((eq atop 'unquote-splicing)
Francois-Rene Rideau
committed
(cond
Francois-Rene Rideau
committed
#-quasiquote-strict-append
Francois-Rene Rideau
committed
((null dtop)
(if (unquote-xsplicing-p a)
(values 'append (list a))
(expand-unquote a)))
(t
(values 'append
(cond ((eq dtop 'append)
(cons a d))
(t (list a (quasiquote-expand-1 dtop d))))))))
Francois-Rene Rideau
committed
((eq atop 'unquote-nsplicing)
Francois-Rene Rideau
committed
(cond
Francois-Rene Rideau
committed
#-quasiquote-strict-append
Francois-Rene Rideau
committed
((null dtop)
(if (unquote-nsplicing-p a)
(values 'nconc (list a))
(expand-unquote a)))
(t
(values 'nconc
(cond ((eq dtop 'nconc)
(cons a d))
(t (list a (quasiquote-expand-1 dtop d))))))))
Francois-Rene Rideau
committed
((null dtop)
(if (member atop '(quote :literal nil))
(values 'quote (list a))
(values 'list (list (quasiquote-expand-1 atop a)))))
((member dtop '(quote :literal))
Francois-Rene Rideau
committed
(cond
((member atop '(quote :literal nil))
(values 'quote (cons a d)))
Francois-Rene Rideau
committed
;; This should be done more cautiously.
;; Can we detect the case "has no (recursive) quasiquote escapes"?
;; Or is 'quote already that?
#|
((and (consp d) (null (cdr (last d))))
Francois-Rene Rideau
committed
(values 'list (list* (quasiquote-expand-1 atop a)
Francois-Rene Rideau
committed
(mapcar 'kwote d)))) |#
Francois-Rene Rideau
committed
(t
(values 'list* (list (quasiquote-expand-1 atop a)
(quasiquote-expand-1 dtop d))))))
Francois-Rene Rideau
committed
(t (let ((qa (quasiquote-expand-1 atop a)))
(if (member dtop '(list list*))
(values dtop (cons qa d))
(values 'list*
(list qa (quasiquote-expand-1 dtop d))))))))))
(t
(error "unrecognized object in quasiquote"))))
(defun expand-unquote (x)
(cond
((null x)
(values nil nil))
((literalp x)
(values #+quasiquote-passes-literals :literal #-quasiquote-passes-literals 'quote x))
Francois-Rene Rideau
committed
((symbolp x)
(values 'unquote x))
((not (consp x))
(error "unrecognized object in unquote"))
((and (quotep x)
(not (unquote-xsplicing-p (single-arg x))))
(values 'quote (single-arg x)))
((member (car x) '(list list* append nconc))
Francois-Rene Rideau
committed
(values (car x) (cdr x)))
((eq (car x) 'cons)
(values 'list* (cdr x)))
(t (values 'unquote x))))
(defun quasiquote-expand-1 (top x)
"Given a top token and an expression, give the quasiquoting
of the result of the top operation applied to the expression"
(cond
((member top '(unquote :literal nil))
x)
((eq top 'quote)
(kwote x))
Francois-Rene Rideau
committed
((member top '(cons list*))
(cond
Francois-Rene Rideau
committed
#-quasiquote-strict-append
Francois-Rene Rideau
committed
((length=n-p x 1) x)
((let ((last (last x)))
(when (or (null last) (and (consp last) (quotep (car last))
(properly-ended-list-p (single-arg (car last)))))
(quasiquote-expand-1 'list (append (butlast x)
(mapcar 'kwote (and last (single-arg (car last)))))))))
((length=n-p x 2)
(apply 'k-cons x))
((unquote-xsplicing-p (car (last x)))
(k-append
(quasiquote-expand-1 'list (butlast x))
(car (last x))))
(t
(apply 'k-list* x))))
Francois-Rene Rideau
committed
(t
(cons (ecase top
Francois-Rene Rideau
committed
((list cons append nconc make-vector n-vector) top))
Francois-Rene Rideau
committed
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
x))))
;; Note: it would be a *very bad* idea to use quasiquote:quote
;; in the expansion of the macro-character #\'
(defun call-with-quasiquote-reader (thunk)
(let ((*quasiquote-level* (1+ *quasiquote-level*)))
(make-quasiquote (funcall thunk))))
(defun call-with-unquote-reader (thunk)
(let ((*quasiquote-level* (1- *quasiquote-level*)))
(unless (>= *quasiquote-level* 0) (error "unquote outside quasiquote"))
(make-unquote (funcall thunk))))
(defun call-with-unquote-splicing-reader (thunk)
(let ((*quasiquote-level* (1- *quasiquote-level*)))
(unless (>= *quasiquote-level* 0) (error "unquote-splicing outside quasiquote"))
(make-unquote-splicing (funcall thunk))))
(defun call-with-unquote-nsplicing-reader (thunk)
(let ((*quasiquote-level* (1- *quasiquote-level*)))
(unless (>= *quasiquote-level* 0) (error "unquote-nsplicing outside quasiquote"))
(make-unquote-nsplicing (funcall thunk))))
(defun read-quasiquote (stream)
(call-with-quasiquote-reader (lambda () (read stream t nil t))))
(defun read-unquote (stream)
(call-with-unquote-reader (lambda () (read stream t nil t))))
(defun read-unquote-splicing (stream)
(call-with-unquote-splicing-reader (lambda () (read stream t nil t))))
(defun read-unquote-nsplicing (stream)
(call-with-unquote-nsplicing-reader (lambda () (read stream t nil t))))
(defun read-vector (stream n)
;; http://www.lisp.org/HyperSpec/Body/sec_2-4-8-3.html
Francois-Rene Rideau
committed
(if (= *quasiquote-level* 0)
(n-vector n (read-delimited-list #\) stream t))
(make-unquote
(k-n-vector n (quasiquote-expand
(progn (unread-char #\( stream)
(read-preserving-whitespace stream t nil t)))))))
Francois-Rene Rideau
committed
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
(defun read-read-time-backquote (stream char)
(declare (ignore char))
(values (macroexpand-1 (read-quasiquote stream))))
(defun read-macroexpand-time-backquote (stream char)
(declare (ignore char))
(read-quasiquote stream))
(defun read-backquote (stream char)
#-quasiquote-at-macro-expansion-time (read-read-time-backquote stream char)
#+quasiquote-at-macro-expansion-time (read-macroexpand-time-backquote stream char))
(defun backquote-reader (expansion-time)
(ecase expansion-time
((read) #'read-read-time-backquote)
((macroexpand) #'read-macroexpand-time-backquote)
((nil) #'read-backquote)))
(defun read-comma (stream char)
(declare (ignore char))
(case (peek-char nil stream t nil t)
((#\@)
(read-char stream t nil t)
(read-unquote-splicing stream))
((#\.)
(read-char stream t nil t)
(read-unquote-nsplicing stream))
(otherwise (read-unquote stream))))
(defun read-hash-paren (stream subchar arg)
(declare (ignore subchar))
(read-vector stream arg))
(defvar *hash-dot-reader* (get-dispatch-macro-character #\# #\.))
(defun read-hash-dot (stream subchar arg)
(let ((*quasiquote-level* 0))
(funcall *hash-dot-reader* stream subchar arg)))
(defun enable-quasiquote (&key expansion-time (table *readtable*))
Francois-Rene Rideau
committed
;; Note that it is *NOT* OK to enable-quasiquote in the initial readtable,
;; as this violates the build contract (see ASDF 3.1 documentation about readtables).
;; Please only use it in your own private readtable,
;; and/or use system fare-quasiquote-readtable and use
;; (named-readtables:in-readtable :fare-quasiquote)
(set-macro-character #\` (backquote-reader expansion-time) nil table)
(set-macro-character #\, #'read-comma nil table)
Francois-Rene Rideau
committed
(set-dispatch-macro-character #\# #\( #'read-hash-paren table)
(set-dispatch-macro-character #\# #\. #'read-hash-dot table)
Francois-Rene Rideau
committed
t)
(defvar *fq-readtable* (let ((x (copy-readtable nil))) (enable-quasiquote :table x) x))