147 lines
6.0 KiB
Common Lisp
147 lines
6.0 KiB
Common Lisp
;;; Copyright 2013 Google Inc.
|
|
;;;
|
|
;;; Licensed under the Apache License, Version 2.0 (the "License");
|
|
;;; you may not use this file except in compliance with the License.
|
|
;;; You may obtain a copy of the License at
|
|
;;;
|
|
;;; http://www.apache.org/licenses/LICENSE-2.0
|
|
;;;
|
|
;;; Unless required by applicable law or agreed to in writing, software
|
|
;;; distributed under the License is distributed on an "AS IS" BASIS,
|
|
;;; WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
;;; See the License for the specific language governing permissions and
|
|
;;; limitations under the License.
|
|
|
|
;;; A Lisp macro is a function that accepts Lisp data and produces a Lisp form.
|
|
;;; When the macro is called, its macro function receives unevaluated arguments
|
|
;;; and may use them to produce a new Lisp form. This form is then spliced in
|
|
;;; place of the original macro call and is then evaluated.
|
|
|
|
(defmacro my-and (&rest forms)
|
|
;; We use a LABELS local function to allow for recursive expansion.
|
|
(labels ((generate (forms)
|
|
(cond ((null forms) 'nil)
|
|
((null (rest forms)) (first forms))
|
|
(t `(when ,(first forms)
|
|
,(generate (rest forms))))))) ; wowy
|
|
(generate forms)))
|
|
|
|
(when (= 2 3) "hello")
|
|
|
|
(define-test my-and
|
|
;; ASSERT-EXPANDS macroexpands the first form once and checks if it is equal
|
|
;; to the second form.
|
|
(assert-expands (my-and (= 0 (random 6)) (error "Bang!"))
|
|
'(when (= 0 (random 6)) (error "Bang!")))
|
|
(assert-expands (my-and (= 0 (random 6))
|
|
(= 0 (random 6))
|
|
(= 0 (random 6))
|
|
(error "Bang!"))
|
|
'(when (= 0 (random 6))
|
|
(when (= 0 (random 6)) (when (= 0 (random 6)) (error "Bang!"))))))
|
|
|
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
|
|
;;; A common macro pitfall is capturing a variable defined by the user.
|
|
|
|
(define-test variable-capture
|
|
(macrolet ((for ((var start stop) &body body)
|
|
`(do ((,var ,start (1+ ,var))
|
|
(limit ,stop))
|
|
((> ,var limit))
|
|
,@body)))
|
|
(let ((limit 10)
|
|
(result '()))
|
|
(for (i 0 3)
|
|
(push i result)
|
|
(assert-equal 3 limit))
|
|
(assert-equal '(0 1 2 3) (nreverse result))))) ; didn't get it on first tries, ugh
|
|
;; oh, ok - then try to use names that wouldn't happen in outside condext
|
|
;; so that explicitly defined things in outside context get overshadowed
|
|
;; ok
|
|
|
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
|
|
;;; Another pitfall is evaluating some forms multiple times where they are only
|
|
;;; meant to be evaluated once.
|
|
|
|
(define-test multiple-evaluation
|
|
;; We use MACROLET for defining a local macro.
|
|
(macrolet ((for ((var start stop) &body body)
|
|
`(do ((,var ,start (1+ ,var)))
|
|
((> ,var ,stop))
|
|
,@body)))
|
|
(let ((side-effects '())
|
|
(result '()))
|
|
;; Our functions RETURN-0 and RETURN-3 have side effects.
|
|
(flet ((return-0 () (push 0 side-effects) 0)
|
|
(return-3 () (push 3 side-effects) 3))
|
|
(for (i (return-0) (return-3))
|
|
(push i result)))
|
|
(assert-equal '(0 1 2 3) (nreverse result))
|
|
(assert-equal '(0 3 3 3 3 3) (nreverse side-effects)))))
|
|
; omg, fuck this guessing
|
|
; ok, the ,stop was evaluated on each iteraction
|
|
; to check "whether to stop"
|
|
|
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
|
|
;;; Yet another pitfall is not respecting the evaluation order of the macro
|
|
;;; subforms.
|
|
|
|
(define-test wrong-evaluation-order
|
|
(macrolet ((for ((var start stop) &body body)
|
|
;; The function GENSYM creates GENerated SYMbols, guaranteed to
|
|
;; be unique in the whole Lisp system. Because of that, they
|
|
;; cannot capture other symbols, preventing variable capture.
|
|
(let ((limit (gensym "LIMIT")))
|
|
`(do ((,limit ,stop)
|
|
(,var ,start (1+ ,var)))
|
|
((> ,var ,limit))
|
|
,@body))))
|
|
(let ((side-effects '())
|
|
(result '()))
|
|
(flet ((return-0 () (push 0 side-effects) 0)
|
|
(return-3 () (push 3 side-effects) 3))
|
|
(for (i (return-0) (return-3))
|
|
(push i result)))
|
|
(assert-equal '(0 1 2 3) (nreverse result))
|
|
(assert-equal '(3 0) (nreverse side-effects)))))
|
|
;; didn't got on first try,
|
|
;; but yes, for gensym limit ,stop is evaluated first
|
|
|
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
;; advice on checking intermediate things inside of let / flet / etc
|
|
;; (*print-gensym*) ; so that gensyms could be copied into repl
|
|
(declaim (optimize 'debug))
|
|
|
|
(define-test for
|
|
(macrolet ((for ((var start stop) &body body)
|
|
;; Fill in the blank with a correct FOR macroexpansion that is
|
|
;; not affected by the three macro pitfalls mentioned above.
|
|
(let ((initial (gensym "INITIAL"))
|
|
(limit (gensym "LIMIT")))
|
|
`(do* ((,initial ,start)
|
|
(,limit ,stop)
|
|
(,var ,initial (1+ ,var)))
|
|
((> ,var ,limit))
|
|
,@body))))
|
|
(let ((side-effects '())
|
|
(result '()))
|
|
(flet ((return-0 () (push 0 side-effects) 0)
|
|
(return-3 () (push 3 side-effects) 3))
|
|
(for (i (return-0) (return-3))
|
|
(push i result)))
|
|
(break)
|
|
(assert-equal '(0 1 2 3) (nreverse result))
|
|
(assert-equal '(0 3) (nreverse side-effects)))))
|
|
|
|
;; (do* ((a (return-0))
|
|
;; (b (return-3))
|
|
;; (i a (1+ i)))
|
|
;; ((> i b))
|
|
;; (push i result))
|
|
;;
|
|
;; so, my mistake was: using DO and trying to cross reference temp vars
|
|
;; and mistyping 1+ as 1_
|