Beautiful Code [67]
The syntax-case system[] was developed to address the shortcomings of the original algorithm without the limitations of syntax-rules. The system supports local macro bindings and operates with constant overhead, yet allows macros to use the full expressive power of the Scheme language. It is upwardly compatible with syntax-rules, which can be expressed as a simple macro in terms of syntax-case, and it permits the same pattern language to be used even for "low-level" macros for which syntax-rules cannot be used. It also provides a mechanism for allowing intended captures—i.e., allowing hygiene to be "bent" or "broken" in a selective and straightforward manner. In addition, it handles several practical aspects of expansion that must be addressed in a real implementation, such as internal definitions and tracking of source information through macro expansion.
[] "Syntactic abstraction in Scheme," R. Kent Dybvig, Robert Hieb, and Carl Bruggeman, Lisp and Symbolic Computation, Vol. 5, No. 4, pp. 295–326, 1993.
This all comes at a price in terms of the complexity of the expansion algorithm and the size of the code required to implement it. A study of a complete implementation in all its glory is therefore beyond the scope of this chapter. Instead, we'll investigate a simplified version of the expander that illustrates the underlying algorithm and the most important aspects of its implementation.
25.1. Brief Introduction to syntax-case
We'll start with a few brief syntax-case examples, adapted from the Chez Scheme Version 7 User's Guide (R. Kent Dybvig, Cadence Research Systems, 2005). Additional examples and a more detailed description of syntax-case are given in that document and in The Scheme Programming Language, Third Edition.
The following definition of or illustrates the form of a syntax-case macro definition:
(define-syntax or
(lambda (x)
(syntax-case x ()
[(_ e1 e2)
(syntax (let ([t e1]) (if t t e2)))])))
The define-syntax form creates a keyword binding, associating the keyword or in this case with a transformation procedure, or transformer. The transformer is obtained by evaluating, at expansion time, the lambda expression on the righthand side of the define-syntax form. The syntax-case form is used to parse the input, and the syntax form is used to construct the output, via straightforward pattern matching. The pattern (_ e1 e2) specifies the shape of the input, with the underscore (_) marking where the keyword or appears, and the pattern variables e1 and e2 bound to the first and second subforms. The template (let([te1]) (iftte2)) specifies the output, with e1 and e2 inserted from the input.
The form (syntax template) may be abbreviated to #'template, so the previous definition may be rewritten as follows:
(define-syntax or
(lambda (x)
(syntax-case x ()
[(_ e1 e2)
(syntax (let ([t e1]) (if t t e2))])))
Macros may also be bound within a single expression via letrec-syntax.
(letrec-syntax ([or (lambda (x)
(syntax-case x ()
[(_ e1 e2)
#'(let ([t e1]) (if t t e2))]))])
(or a b))
Macros can be recursive (i.e., expand into occurrences of themselves), as illustrated by the following version of or that handles an arbitrary number of subforms. Multiple syntax-case clauses are required to handle the two base cases and the recursion case:
(define-syntax or
(lambda (x)
(syntax-case x ()
[(_) #'#f]
[(_ e) #'e]
[(_ e1 e2 e3 ...)
#'(let ([t e1]) (if t t (or e2 e3 ...)))])))
An input or output form followed by an ellipsis in the syntax-case pattern language matches or produces zero or more forms.
Hygiene is ensured for the definitions of or in this example, so that the introduced binding for t and the introduced references to let, if, and even or are scoped properly. If we want to bend or break hygiene, we do so with the procedure datum->syntax, which produces a syntax object