Reputation: 547
I am attempting to answer the following exercise.
Write a macro function OUR-IF that translates the following macro calls.
(our-if a then b) translates to (cond (a b)) (our-if a then b else c) translates to (cond (a b) (t c))
My solution is the following:
(defmacro our-if (test then then-clause &optional else else-clause)
(if (equal 'else else)
`(cond (,test ,then-clause) (t ,else-clause))
`(cond (,test ,then-clause))))
That works fine, but I'm not totally satisfied by it. There's no syntax checking on the "then" and "else" arguments. Then could be anything. And if I gave the wrong symbol for "else", I'd get the wrong behaviour. The symbols aren't even checked to be symbols.
I could model them as keyword arguments and get close, but that's not exactly right either. It reminds me of the LOOP macro, which is a much more complicated example of the same thing. That made me wonder: how does LOOP do it? Maybe there's some pattern for "mini languages in macro arguments". But I couldn't find anything.
I found the hyperspec page for LOOP which confirms that the grammar is complicated. But I don't know if there's a nice way to implement it.
Upvotes: 1
Views: 83
Reputation: 139411
One can also signal specific errors. Such a syntax error is in Common Lisp a program error.
Example:
Let's define a general error condition, which can report errors with FORMAT
.
(define-condition simple-program-error (simple-error program-error) ())
An example macro expecting 42 as the argument.
(defmacro a (a)
(if (not (eql a 42))
(error 'simple-program-error
:format-control
"Wrong syntax for macro A. Argument is not 42, but is ~a"
:format-arguments (list a))
`',a))
testing it:
CL-USER 132 > (a 41)
Error: Wrong syntax for macro A. Argument is not 42, but is 41
1 (abort) Return to top loop level 0.
Type :b for backtrace or :c <option number> to proceed.
Type :bug-form "<subject>" for a bug report template or :? for other options.
CL-USER 133 : 1 > :c 1
CL-USER 134 > (a 42)
42
Upvotes: 1
Reputation: 9282
This is what pattern matching is for. There are a bunch of CL pattern matchers, dsm is one I am familar with which was designed for dealing with macros, albeit not quite this sort of 'let's just splice in some fortran' macro.
Using dsm in defmacro
:
(defmacro our-if (&body forms)
(destructuring-match forms
((test then value)
(:when (eq then 'then))
`(if ,test ,value))
((test then value else otherwise)
(:when (and (eq then 'then)
(eq else 'else)))
`(if ,test ,value ,otherwise))
(otherwise
(error "what?"))))
Or if you use the define-matching-macro
form it includes as an example:
(define-matching-macro our-if
((_ test then value)
(:when (eq then 'then))
`(if ,test ,value))
((_ test then value else otherwise)
(:when (and (eq then 'then)
(eq else 'else)))
`(if ,test ,value ,otherwise))
(otherwise
(error "what?")))
It may be that other matchers are better for the case where there are lots of literals, as here.
In the case of loop
the syntax keywords are compared by name, not symbol equality. Although it is definitely overkill in this simple case, you can do this pretty easily using spam:
(defun syntax-keyword (k)
;; return a predicate which matches a syntax keyword
(let ((uk (string-upcase k)))
(lambda (s)
(and (symbolp s)
(string= (symbol-name s) uk)))))
(defmacro our-if (&body forms)
(destructuring-match forms
((&whole form test _ value)
(:when (matchp form (list-matches (any) (syntax-keyword "then") (any))))
`(if ,test ,value))
((&whole form test _ value _ otherwise)
(:when (matchp form (list-matches (any) (syntax-keyword "then") (any)
(syntax-keyword "else") (any))))
`(if ,test ,value ,otherwise))
(otherwise
(error "what?"))))
This is pretty seriously over-the-top for this simple case, but for more general things it can help.
Upvotes: 1
Reputation: 38967
The LOOP macro accepts symbols that are compared by name and not by identity to the keywords defined in the grammar (see 6.1.1.2 Loop Keywords
). The section doesn't specify how the comparison by name is done, but in ECL and SBCL (I tried only two implementations), the expected case is upper-case (using |for|
doesn't work). All three invocations below are equivalent:
(loop :for x :in list)
(loop for x in list)
(loop my-package::for x their-package::in list)
For your simple macro it can be sufficient to add checks as follows:
(defmacro our-if (test then then-clause
&optional (else nil elsep) (else-clause nil ecp))
(unless (string= then "THEN")
(error "Keyword THEN expected, not ~s" then))
(when elsep
(unless (string= else "ELSE")
(error "Keyword ELSE expected, not ~s" else))
(unless ecp
(error "ELSE keyword without an else clause")))
(if elsep
`(cond (,test ,then-clause) (t ,else-clause))
`(cond (,test ,then-clause))))
Note the use of a elsep
and ecp
(if you prefer longer names: else-clause-p
) in the lambda list to know if the optional arguments are given or not (depending on their default value would not be enough to disambiguate the different cases).
Upvotes: 1