Sunteți pe pagina 1din 13

D-Expressions: Lisp Power, Dylan Style

Jonathan Bachrach Keith Playford


Artificial Intelligence Laboratory Functional Objects, Inc.
Massachussetts Institute of Technology 86 Chandler Street
Cambridge, MA 02139 Somerville, MA 02144
jrb@ai.mit.edu keith@functionalobjects.com

1 Abstract in languages with more conventional syntaxes like those of


C or Pascal, but in comparison with what Lisp provides, the
This paper aims to demonstrate that it is possible for a lan- solutions have been restrictive, difficult to explain and use,
guage with a rich, conventional syntax to provide Lisp-style or both. None have been standardized. Further, the utility
macro power and simplicity. We describe a macro system of syntax manipulation tools independent of the macroex-
and syntax manipulation toolkit designed for the Dylan pro- pansion process is typically forgotten.
gramming language that meets, and in some areas exceeds,
this standard. The debt to Lisp is great, however, since 2.2 Lisp Power, Dylan Style
although Dylan has a conventional algebraic syntax, the ap-
proach taken to describe and represent that syntax is dis- This paper aims finally to demonstrate that it is possible for
tinctly Lisp-like in philosophy. a language with a richer, more conventional syntax to pro-
vide Lisp-style macro power and simplicity. We describe a
2 Introduction macro system and syntax manipulation toolkit designed for
the Dylan programming language (cf [13], [7]) that meets our
The ability to extend a programming language with new goals as well as, if not better than, those commonly found in
constructs is a valuable tool. With it, system designers can Lisps. The debt to Lisp is great, however, since although Dy-
grow a language towards their problem domain and enhance lan has a conventional algebraic syntax, the approach taken
productivity and ease of maintenance. A macro system pro- to describe and represent its syntax is distinctly Lisp-like in
vides this capability in a portable, high-level fashion by al- philosophy.
lowing new constructs to be implemented in terms of exist- Taking Dylan’s already capable rule-based pattern match-
ing ones via programmer-defined source-to-source transfor- ing and template substitution macro system as a starting
mations. point, we explore its underpinnings. Within a broader com-
Beyond the above, the ability to read, write, and eas- parison with other styles, we relate Dylan’s approach to the
ily manipulate the syntax of a language from within that Lisp model and show how their common basis can (and has,
language can be especially powerful. It can allow the full in other contexts) be applied to a family of languages whose
language to be brought to bear when implementing macros. syntax is based on what we term a skeleton syntax tree. We
It can provide a convenient means of saving and restoring go on to describe a library which provides a code representa-
structured data in text form. It can form the basis of code tion (d-expressions), source-level pattern matching and code
analysis tools and be the starting point for experiments with construction utilities, and source code IO. Some models of
new language processors or into modified language seman- compile-time evaluation are proposed that are suitable for
tics. Dylan, a language in which compiler and run-time are sep-
arated. Finally, putting the d-expressions library together
with a compile-time evaluation model leads us to a natu-
2.1 Successes and Failures ral procedural macro extension of Dylan’s rule-based macro
Lisp is the only language family (cf [9], [10]) that has suc- system, one comparable to those available in Lisp.
ceeded in providing integrated macro systems along with
simple and powerful syntax manipulation tools like these. 2.3 Requirements and Goals
They are considered one of Lisp’s unique strengths, perhaps
even the most important and distinctive feature of the lan- A macro facility provides a mechanism to extend a base
guage. But the key to their viability in Lisp is the simplicity syntax. Such a facility can be measured along a set of axes,
and regularity of its syntax. Recognizing their utility, at- covering such aspects as power, generality, reliability, and
tempts have been made to provide powerful macro facilities ease of use.
Macro facilities differ greatly in their ability to perform
syntactic transformations. Their power can be judged by
the complexity of their underlying syntactic representations
and the extent of their programmability. Macro systems
have used input representations such as characters, tokens,
syntax, and semantic information, with the latter leading to

1
more powerful transformations than the impoverished for- great help to macro writers.
mer. First, the underlying syntax representation affects the
ability of the macro system to maintain source locations. 3 An Overview of Syntax Representations
During debugging, a user should not have to step through
macro expanded cruft, no more than they should have to We split the space of syntax representations into four broad
step through code obfuscated by optimizations. Second, the categories: text based, token based, abstract syntax trees,
syntax representation determines the ease with which syntax and skeleton syntax trees. Each one in considered in turn
can be manipulated. Ultimately, in order to perform pow- in the following sections and evaluated with respect to the
erful transformations, the underlying representation needs requirements set out above.
to be at least rich enough to represent recursive structures
such as expressions.
3.1 Text-Based Representations
A macro facility’s programmability is a function of the
strength of its transformational engine, starting from a pure- The most basic representation of a piece of code is its origi-
ly substitution system (e.g., the C Preprocessor [11]) all the nal text. Such unstructured text is difficult to analyse and
way up to a full programming language. In between, some manipulate, and the simple string-based pattern matching
macro systems augment substitution with support for rep- tools often used are neither syntax aware nor context aware.
etition and conditionals. The more powerful the transfor- Even simple things like matching only code, as opposed to
mational engine, the more expressive the macro facility will text contained within strings or comments, can be problem-
be. A good test of a macro facility is the extent to which it atical.
can represent the base language on which it was built. This Repeated application of a full parser, if available, is ex-
helps determine whether a language can be seamlessly ex- pensive and may not be possible on subexpressions taken
tended or whether macro defined constructs will be confined out of context. Also, unless auxiliary data is maintained
to a limited syntactic space. somehow, source location information is lost by transforma-
A powerful macro facility should provide an ability to tions.
read and write syntactic elements. Input facilities are obvi- Because of these problem and others, a text-based rep-
ously crucial for writing a file compiler, while output facili- resentation clearly does not meet our requirements.
ties are useful for warnings and debugging. Ideally, a macro
facility could be provided as a separate library upon which 3.2 Token-Based Representations
to base compilers and code analysis tools.
Another important axis along which to measure a macro In a token-based representation, code is manipulated as a
facility is its reliability. Will it produce syntactically correct sequence of tokens as generated by a lexer. This has a num-
output and produce correct results in all cases? Can a macro ber of benefits over straight text. For example, the lexer
introduce a syntax error by way of an admissible usage? In typically strips out comments, leaving only code. Also, the
other words, users should only see syntax errors from the problem of accidental token merging when constructing new
actual code they write. Can syntactic constituents interfere phrases can be eliminated. The C preprocessor [11] and
with each other? In some systems (e.g., C Preprocessor [11]) Pop-11 [1] are examples of languages offering a token-based
extra parentheses are required to prevent unwanted operator macro system.
precedence bugs. If each token is represented by a structure rather than a
From an ease of use point of view as well as a reliabil- simple string, it is possible to record its classification (e.g.
ity perspective, it is important that a macro facility ensures literal string, identifier, punctuation) and source location
that variable references copied from a macro call and from along with it. This information can be maintained through
a macro definition mean the same thing in an expansion. In source-to-source transformations.
particular, the macro system should (1) avoid accidental col- Processing an unstructured token stream is not very con-
lisions of macro variables with program variables of the same venient, however. If a macro takes as its arguments a fixed
name regardless of the context in which a given macro is number of tokens following the macro name, it works cor-
used and (2) maintain consistency when macro variables re- rectly only when its arguments are single-token elements. If
fer to identically named variables introduced within a given a macro needs to work in terms of expressions, the number
macro definition. (1) is generally referred to as hygiene [12] of tokens it accepts is variable. Consider the following:
and (2) is generally referred to as referential transparency.
A violation of either is called a variable capture error. Vari- define token-macro increment!
(loc1, comma, loc2) => (replacement-tokens)
able capture errors create subtle bugs that are difficult to assert(comma = ",");
debug. Much has been written about how to avoid intro- concatenate(loc2, ":=", loc1, "+", delta)
ducing these sorts of errors [8], but we maintain that there end token-macro;
is no reason to introduce them in the first place.
// OK
Other aspects that improve usability are intuitiveness of increment! n, delta;
a macro’s definition and ease with which it’s debugged. We
assert that a macro facility should enable a programmer to // Assertion fails, comma gets bound to "["
write down what they want the input and output to look like increment! a[i], delta;
augmented only by substitution parameters. The alterna-
tive of constructing and manipulating representations man- C macros and one kind of Pop-11 macro actually con-
ually or specifying productions in terms of Abstract Syntax strain the shape of macro input to being of function call
Tree classes is too tedious and error prone requiring inti- form. This allows the parser to easily determine the extent
mate knowledge of an underlying syntactic representation. of a macro’s input and split it into comma-separated chunks
Debugging shift/reduce errors is a daunting task for even before finally passing these on to the macro.
the best programmers. Furthermore, some form of a trace At the other extreme, macros can be given full control
of the transformation engine and selected expansions is a over the token stream. While this is the ultimate in versa-
tility, it presents some practical problems. For example, the

2
extent of forms cannot be determined without performing Referring back to a previous section, another way to look
macro expansion. This can be a problem for development at an SST is as a lightly-structured, token-based representa-
environments and code analysis tools, but also for program- tion. SST’s have most of the power of general token-based
mers. The approach lacks modularity. Token macros be- approaches, but without many of the pitfalls discussed. The
come, in essence, part of an ad-hoc recursive descent parser challenge is to define a simple SST upon which it is possible
and programmers must be aware of the implications of that. to build a language with a conventional algebraic syntax.
They must always take care to respect any macro syntax It is worth noting that although Lisp is SST-based, the
contained within the subexpressions manipulated because SST used in most Lisps would not meet our requirements.
those subexpressios are not isolated within nested structure. Because standard run-time objects (numbers, symbols, lists)
One further problem familiar to most C programmers are reused to represent the Lisp SST, there is no reliable
is inadvertent expression merging. When bringing code to- way to record auxiliary information like source location or
gether within a macro or when returning replacement code hygiene context. Scheme macro systems [5] that implement
to a macro call point, care must be taken to isolate the automatic hygiene have to face this deficiency and work in
inserted code from its surroundings. Wrapping with paren- terms of special-purpose syntax objects rather than lists and
theses where appropriate is normally sufficient to avoid, typ- symbols.
ically, operator precedence related bugs. Failing to do so is
still a common source of bugs, however. 4 Dylan’s Rewrite-Rule Only Macro System
Token-based solutions can clearly be very powerful. How-
ever, we feel that the pitfalls and complexities that come Dylan macros provide a way to define new syntactic forms.
with that power make them an unlikely basis for a macro Their definitions are called macro definitions and their us-
system intended to be as simple and accessible as Lisp’s. ages are called macro calls. At compile-time, macro ex-
pansion replaces macro calls with other constructs per their
3.3 Abstract Syntax Tree Representations definition. This macro expansion process is repeated until
the resulting expansion contains no more macro calls.
Abstract syntax trees (AST’s) are highly structured repre-
sentations of source code. In an object-oriented implementa-
tion, there would typically be one class per definition, state- 4.1 Dylan’s Rewrite Rules
ment, or other language construct. Down at the expression The macro system defined in the Dylan Reference Manual
level, there might be classes corresponding to function calls, (DRM) [13] is a rewrite-rule only pattern matching template
array accesses, literal references, and so on. substitution system. Macros are named with module bind-
Typically with an AST, adding a new syntactic construct ings using the usual Dylan namespace mechanism. Each
requires extending the parser to understand it and adding macro is comprised of a sequence of rewrite rules where the
a new class to represent that construct. How difficult this left-hand side of a rule is a pattern that matches a fragment
is is related to the complexity of the parser because there is of code and the right-hand side of a rule is a template which
no intermediate form between tokens and full AST. Worst is substituted for the matched fragment. Pattern variables
case, there is no special support at all for modular macro in the left-hand side serve as macro arguments. Pattern vari-
extensions, and new language constructs must be defined as ables appearing on the right-hand side substitute arguments
a cooperating element of the full language grammar [3]. It into the expansion. The rules are attempted in order until
could require extensive knowledge of the parser’s intricacies a given rule’s pattern is found to match, at which time, the
to avoid or resolve problems like shift/reduce conflicts in an matched fragment is replaced with the corresponding tem-
LALR parser, for example. We don’t consider requiring such plate. If no patterns are found to match, then an error is
knowledge of programmers to be reasonable. raised.

3.4 Skeleton Syntax Tree Representations 4.2 Dylan’s Skeleton Syntax Tree
The distinguishing feature of a skeleton syntax tree (SST) is Before we delve into the intricacies of the rewrite-rule only
that it has few classes and corresponds to a very simple core macro system, it is important to explore Dylan’s SST. Dy-
grammar. That grammar describes “shapes” rather than lan’s SST is a richer version of Lisp’s s-expression based
particular language constructs. Many different constructs SST. Lisp’s SST can roughly be described as follows:
may fit within the same basic shape.
The most extreme example of a language based on an form: symbol
SST is, of course, Lisp. Lisp’s underlying grammar says form: literal
form: "(" elements ")"
nothing about cond or defun, it simply specifies the single
basic shape of all Lisp constructs. Although not a program- elements: form ...
ming language, XML has much the same property.
SST’s define the axioms to which all syntax must con- Dylan adds to this a few more basic shapes:
form. In Lisp, it is, essentially, that brackets match. In
XML, that tags must match. The simplicity of the axioms form: identifier
makes the overall structure of the language easy for people form: literal
form: "(" elements ")"
to understand and to remember and also for programs to form: "define" modifiers LIST-DEFINITION elements ";"
work with. form: "define" modifiers BODY-DEFINITION elements "end" ";"
Language-level constructs are defined modularly, above form: STATEMENT elements "end" ";"
the core grammar and without requiring any changes to it.
elements: [form | punctuation ] ...
User-defined constructs have the same status as standard
syntactic forms.

3
where modifiers are a sequence of adjective names. Consult with ?test bound to close? and ?body bound to close(stream).
the DRM [13] for the full grammar. Templates do not produce a parsed output but instead
Although, the Dylan rule-based macro system could be produce a well formed sequence of tokens whose parsing
made to work with more involved and stricter grammars, a is delayed, where well-formed means that all occurrences
skeleton syntax tree offers a number of advantages. First, of Dylan brackets match. Delaying their parsing permits
an SST modularizes parsing, that is, descending structures fewer inter-macro dependencies thereby allowing macros to
without doing an expansion is now possible, changes in a be compiled independent of each other and permitting re-
given expansion algorithm don’t require redoing a top-level cursive macros without forward declarations.
parse, and syntax errors can be kept localized. Second, an
SST gives the user a lot of latitude to choose an appropriate 4.5 Example Macros
grammar for a given macro, that is, the Dylan grammar
does not dictate the precise form of templates, but instead We present a number of Dylan macros in order to give a
very loosely constrains their basic shape. sense of the range of their uses. Graham’s book [8] is an
excellent source for macro motivation and examples. An
4.3 Source-level Pattern Matching example of a function macro is increment!:

Now that we have described Dylan’s SST we are in a position define macro increment!
to describe the constituents of rules. Patterns appearing on { increment!(?place:expression) }
=> { ?place := ?place + 1 }
the left hand side of rules provide a “what you see is what { increment!(?place:expression, ?amount:expression) }
you get” (WYSIWYG) mechanism for specifying the desired => { ?place := ?place + ?amount }
input of a macro in terms of concrete syntax. A pattern ba- end macro;
sically looks like the construct that it is to match augmented
increment!(x);
by pattern variables which match and bind to appropriate increment!(height(x), 10);
parts of the construct. Pattern variables are denoted with a
? prefixing their names. Pattern variables have constraints which is the Dylan equivalent of C’s ++.
that restrict the syntactic type of fragments (based on the An example statement macro is with-open-file:
Dylan grammar) that they match when appearing in a pat-
tern. Some of the various pattern constraints are token, define macro with-open-file
name, variable, expression, and body. Constraints are de- { with-open-file (?stream:name, ?options:*) ?:body end }
=> { let ?stream = #f;
noted with a : separated pattern variable name’s suffix. block ()
An example pattern (forming the basis of a complement ?stream := make(<file-stream>, ?options);
to Dylan’s unless macro) is the following: ?body
cleanup
{ when (?test:expression) ?body:body end } ?stream & close(?stream)
end block }
where the following would be a well-formed when macro call: end macro;
when (close?) close(stream) end with-open-file (stream, locator: "phonenumbers")
process-phone-numbers(stream);
Patterns match their input based on a simple matching end with-open-file;
procedure. In order to avoid easily constructed ambiguous
patterns, patterns are matched using left to right processing where the * constraint on the pattern variable ?options
combined with a match shortest first priority for matching means that options can match any parsed fragment. This
wildcard pattern variables. A pattern matches if and only macro is typical of a whole range of macros, called with-
if all of its sub-patterns match. For more details, please macros, where resources, datastructures, or special variables
consult the DRM [13] . must be managed (and cleanups performed) or more gener-
ally where context needs to be built.
4.4 Source-level Code Generation The following define macro:
Templates appearing on the right hand side of rules provide define macro functional-variable-definer
a WYSIWYG mechanism for specifying the desired output { define functional-variable ?var:name = ?init:expression }
=> { define variable ?var ## "-value" = ?init;
of a macro in terms of concrete syntax. A template basically define method ?var ()
looks like the construct that it produces augmented by pat- ?var ## "-value"
tern variables that stand in for parts of the construct bound end method;
to the same pattern variables in the corresponding left hand define method ?var ## "-setter" (new-value)
?var ## "-value" := new-value;
side pattern. For example, the full macro containing the end method }
example pattern from above would be: end macro;

define macro when ? define functional-variable time = 0;


{ when (?test:expression) ?body:body end } ? time();
=> { if (?test) ?body end if } > 0
end macro; ? time() := 25;
where > 25

when (close?) close(stream) end provides a functional interface to variables, defining acces-
would expand into sors for variables, and thus hiding implementation details.
The above example demonstrates how to create new identi-
if (close?) close(stream) end if fiers through the ## concatenation operator.

4
define macro defaulted-variable-definer
4.6 Hygiene { define defaulted-variable ?:name ?default }
=> { define variable ?name = ?default }
Dylan’s macro system is hygienic and maintains referen- default:
tial transparency such that variable references copied from { } => { #f }
a macro call and from a macro definition mean the same { = ?:expression } => { ?expression }
thing in an expansion. For an example of hygiene, consider end macro;
the following macro:
Instead of adding more main rules, only extra rules for the
define macro or varying fragments need to be written.
{ or(?x:expression, ?y:expression) It turns out though that local rewrite rule sets are partic-
=> { let tmp = ?x; ularly useful for macros involving iterating over a sequence
if (tmp) tmp else ?y end }
end define; of constituent fragments:
define macro properties-definer
where a temporary binding, called tmp, is introduced in or- { define properties ?kind:name ?properties end }
der to avoid multiple evaluations of the first argument, x. => { define variable ?kind = list(?properties) }
The macro system ensures that tmp introduced by the or properties:
{ } => { }
macro does not collide with visible bindings named tmp in { ?prop:name; ... } => { ?#"prop", ... }
the enclosing program. For example, the following end macro;

begin where, the substitution ?#"property" coerces the identifier


let tmp = 5;
or(1, 2); into a similarly named symbol. The ellipses ... are a short-
tmp hand for aux-rule recursion, that is,
end
properties:
returns 5. { } => { }
{ ?prop:name; ... } => { ?#"prop", ... }
For an example of referential transparency, consider the
following increment! macro from above. The + in the re- is a shorthand for
sulting template refers to + in the defining macro’s names-
properties:
pace regardless of where the macro is used. In other words, { } => { }
even in the face of + being renamed or even not imported { ?prop:name; ?properties } => { ?#"prop", ?properties }
at all into the namespace of a macro call, the macro system
ensures that increment!’s + is the same and is available.
Sometimes it is necessary to circumvent hygiene in order 5 The D-Expressions Library
to permit macros to introduce variables accessible by the
lexical context from which the macro was called. For exam- In order to write macros that require more complicated
ple, imagine writing a loop macro that provides a standard macro expansion processing, we introduce d-expressions and
named function, called break, for calling when terminating later a full procedural macro facility. The d-expressions li-
iteration: brary provides a collection of classes suitable for representing
fragments of source code in skeleton syntax tree form. If de-
define macro repeat
{ repeat ?:body end } sired, skeleton syntax trees may be constructed and pulled
=> { block (?=break) apart manually using the exported interface of these classes.
local method repeat () ?body end; In addition to the raw, skeleton syntax class hierarchy,
repeat(); source level tools are provided for easier parsing and con-
end block }
end macro; struction of source code fragments held in that form. These
tools are the moral equivalents of the destructuring-bind and
A user can then write the following: backquote macros familiar to Lisp programmers.
The final facility provided is source code IO. An input
repeat method is defined that is parameterizable with syntactic
if ((input := read(*standard-input*, eof: #f)) == #f) context (i.e. a map from identifiers to their syntactic cate-
break()
else gories) and also, optionally, with a home context (e.g. the
write(input, *standard-output*) code’s top level module context) and source location context.
end if An output method is also defined and writes a re-readable
end repeat; text form of any given skeleton tree. These methods are the
moral equivalents of Lisp’s read and write functions.
and have it terminate upon eof by calling the repeat macro’s
provided exit function, break. This mechanism can also be
used for writing anaphoric macros [8]. 5.1 Skeleton Syntax Tree Classes
The basic d-expression AST class hierarchy is agreeably sim-
4.7 Local Rewrite Rules ple:
Dylan provides a mechanism for defining local named rewrite <d-expression> [Abstract]
rule sets which act like local subroutines. These help in <d-leaf> [Abstract]
<d-identifier>
breaking a macro up into rewrite rule subproblems and per- <d-literal>
mit a form of modularization. They are invoked after an <d-punctuation>
initial pattern matching using the main rule set and before <d-compound> [Abstract]
template substitution. Instead of backtracking, a macro is <d-nested>
<d-macro-call>
deemed invalid if none of a given local rewrite rules match. <d-sequence>
Consider the following example:

5
As an example, given the input source: Pattern syntax and pattern matching behaviour in expression-case
are as specified for Dylan’s rewrite-rule only macros. To re-
f(x, y) + g(z) + h[0]; cap, pattern variables are introduced with ? and have an
associated constraint. The most liberal constraint is the
and a syntactic context in which none of the identifiers in- wildcard constraint, *, which matches anything, as used in
volved is associated with a macro category, the object tree the example.
is: To illustrate how expression-case relates to the un-
{<d-sequence> derlying skeleton syntax classes, consider this hypothetical
{<d-identifier> "f"} expansion of the first case in the above:
{<d-nested> "(" ")"
{<d-identifier> "x"} // Fail is bound to an exit procedure that skips and
{<d-punctuation> ","} // moves on to try the next pattern. If a match-xxx
{<d-identifier> "y"} } // function can’t match, it invokes its fail argument.
{<d-identifier> "+"} let elements = d-top-level-elements(d-expr);
{<d-identifier> "g"} let elements-after
{<d-nested> "(" ")" = match-identifier(#"always-assert", elements, fail);
{<d-identifier> "z"} } let ( elements-inside, elements-after)
{<d-identifier> "h"} = match-nested(#"(", #")", elements-after, fail);
{<d-nested> "[" "]" match-empty( elements-after, fail);
{<d-literal> "0"} } // Matched, so bind the pattern variables.
{<d-punctuation> ";"} } let test = elements-inside;
// The right hand side code. The values returned are
On the other hand, if f were associated with a function- // the values returned by expression-case.
generate-always-assertion(test);
form macro, this would be the result:
{<d-sequence> The utility functions match-xxx use the skeleton syntax ac-
{<d-macro-call> "f" cessor methods to do their work.
{<d-nested> "(" ")"
{<d-identifier> "x"}
{<d-punctuation> ","} 5.3 Source-level Tools for Code Generation
{<d-identifier> "y"} } }
{<d-identifier> "+"} A similarly intuitive and accessible way of generating param-
{<d-identifier> "g"} eterized code is with templates. These allow a programmer
{<d-nested> "(" ")" to write out a prototype of the form they want to generate,
{<d-identifier> "z"} } but with substitutions in place of the variable parts of the
{<d-identifier> "h"}
{<d-nested> "[" "]" code.
{<d-literal> "0"} } The d-expressions library implements the special syntax
{<d-punctuation> ";"} } { } for templates. This is a simple example:

Note how the macro call becomes a single syntactic entity at { if ( ?test) error("Assertion failed!") end }
top level, but containing nested structure. This is consistent
with our earlier-stated intuition that macros introduce “new The template is replaced with code that generates the cor-
kinds of bracket” to the syntax. responding d-expression. Subcomponents of that expres-
sion will be elements obtained by evaluating the substitu-
5.2 Source-level Tools for Parsing tion variables in the lexical environment in place where the
template appears.
Working with AST classes directly is tedious and error- Substitution syntax and insertion matching behaviour
prone. Where possible, it is desirable for a programmers in templates are as specified for Dylan’s rewrite-rule only
to be able to work in terms of the source code shapes they macros. To recap, substitutions are introduced with ? fol-
are already familiar with. lowed by a variable name. Here, however, the substituted
As shown earlier, an intuitive and accessible way of ex- variable name need not have been bound by pattern match-
pressing a parser is with patterns. This allows a programmer ing. Any variable bound to a d-expression, however its value
to write out the general shape of input expected, but with was computed, is allowable.
pattern bindings in place of the variable parts of the code. To illustrate how { } relates to the underlying skeleton
The source-level parsing tools provided in the d-expressions syntax classes, consider this hypothetical expansion of the
library take this approach. above:
The primary parsing tool offered is expression-case.
process-template
This is a simple example: (substitute-identifier(#"if"),
process-nested
expression-case (d-expr) (#"(", #")",
{ always-assert(?test:*) } substitute-identifier(#" "),
=> generate-always-assertion(test); test), // Reference to test
{ debug-assert(?test:*) } substitute-identifier(#"error"),
=> generate-debug-assertion(test); process-nested(#"(", #")",
end expression-case; substitute-literal("Assertion failed!")),
substitute-identifier(#"end"));
The input expression, d-expr in this case, must evaluate to a
valid d-expression. This d-expression is tested against each There are subtleties to be aware of to do with template
of the left hand side patterns in turn. If one of the patterns processing. Note that if and end in the above have been
matches, its pattern variables are bound to local variables code-generated as independent top level identifiers rather
of the same name and the corresponding right hand side than as components within a macro call. This is because
expression is run in the context of those bindings. the syntactic classification of identifiers is not known at code

6
generation time. This canonicalization is delayed to execu- 6 Models of Compile-time Evaluation
tion time when a syntactic context within which to analyse
the generated code must have been established. The preceding sections outline a utility library for reading,
In particular, the static checking of templates attempted writing, and manipulating code representable by our skele-
in [14] cannot be usefully applied here. On execution, tem- ton syntax tree form. As previously suggested, this library
plate identifiers are “read” within a syntactic context estab- is useful for a number of purposes, from simple storage and
lished at runtime by the program containing the template, retrieval of data in text form to parsing for a full-scale Dy-
not in the static syntactic context of the program’s source lan compiler (the Functional Objects Dylan compiler was in
code. For example, in the mapping of names to syntactic fact based on such a library), or indeed a compiler for any
classifications put in place at runtime, if may have no spe- language with a Dylan-like syntax.
cial association, or an association that differs from that of But in order to meet our goals, the task remaining is to
standard Dylan syntax. describe a Lisp-strength procedural macro system for Dylan.
Because it matches typical usage patterns well, we have The source-level code manipulation tools may be available
elected to allow syntactic template contexts to be estab- in the form of the d-expressions library as we have seen,
lished with dynamic scope. That is, a syntactic context may but some model of compile-time evaluation is required be-
be established with a single dynamic binding for all tem- fore they can be used to implement macros. In the following
plates executed within its scope. Similar dynamic binding sections we consider some approaches to compile-time eval-
forms can be used to establish new, shared hygiene contexts uation.
and source location annotations for template-generated code
run within their scope. 6.1 Compile-time is Run-time
It is not uncommon for languages originally or convention-
5.4 D-Expression IO
ally implemented using an interpreter, or where an inter-
A method is provided for reading d-expressions from an in- preter or dynamic compiler is available during program exe-
put stream. Reading requires parameterization with the fol- cution, to allow programs to construct and then execute new
lowing: code at runtime. A macro in such a language can be thought
of as a runtime function flagged such that it gets passed the
• A syntactic context is a mapping between identifier unevaluated source code of its arguments. The code of the
names and their syntactic category. The set of avail- macro function rewrites the input source and then, either
able categories is fixed and corresponds to the set of explicitly or implicitly, re-invokes the interpreter/compiler
broad “shape” categories offered by the skeleton syn- on the result.
tax, as described above. Older dialects of Lisp used to support this approach, call-
This mapping is all that is needed for the reader to be ing such functions fexpr ’s. However, more recent dialects
able to parse and return a single, complete d-expression. like Common Lisp no longer support them. Fexpr’s can
In particular, macroexpansion does not have to be per- have the semantic problem of “losing” the local binding en-
formed during parsing. vironment of their input expressions in lexically-scoped lan-
guages, but more importantly they defeat optimized com-
A read identifier remembers its syntactic category. This pilation and impose the overhead of some form of language
category is preserved through template substitutions processor having to be available at runtime along with any
so that it may be re-read correctly if inserted as part of of the code rewriting tools used.
a newly constructed piece of source code (e.g. through Dylan is very much designed with compilability and min-
macroexpansion). imal runtime overhead in mind, so this approach to arbi-
trary computation during macroexpansion is not acceptable.
Reading may be parameterized optionally with the follow-
These days, the fexpr approach tends to be limited to in-
ing:
terpreted or dynamically compiled scripting languages like
• A home context, if meaningful, can be specified for TCL.
the code read. This context object is remembered by
and can be recovered from the identifiers within the 6.2 Static Compiler Plugins
returned d-expression. The home context is intended
Probably the simplest approach to compile-time evaluation,
to allow a top level binding context or container to
for the language implementor at least, is to admit to the
be associated with code. If reading full Dylan code,
existence of the compiler and allow programmers to write
the home context would typically identify the module
compiler “plugins”. In this model, procedural macro code
in which the code lives and be used when resolving
cannot be interleaved with the runtime source - it must be
identifiers to module variables.
compiled separately and thought of as a compiler extension
• A source location context, if desired, can be specified that will later be applied to the runtime source.
for the code read. A context object together with a Compile time code and runtime code are therefore very
seed line/column source location are used to record clearly distinguished, being contained within entirely dif-
detailed source location information about each ele- ferent programs. Compile-time dependencies and run-time
ment of the d-expression read. The context object dependencies can’t easily be confused. A plugin model also
would typically identify a file or other physical source opens the door on other interesting compiler extensions and
container. customisations not necessarily related to macros.
Because a macro implemented in a plugin is defined in
A method is also provided for writing d-expressions to a context away from its logical home, extra annotation is
an output stream as re-readable text. The only parameter- required to identify the module where it should be bound.
ization available on output is involved with specifying the For example:
layout of the resulting text and has no semantic impact.

7
Module: parsergen-plugin
be annotated as to whether it is compile-time code, runtime
define macro parser-definer in parsergen, parsergen code, or both. The definition of the library itself must dis-
{ define parser ?:name ?rules:* } tinguish compile-time only dependencies from runtime de-
=> let compiled-grammar = compile-grammar(name, rules); pendencies.
let tables = generate-grammar-tables(compiled-form);
let driver = generate-parser-engine(compiled-form);
For simplicity, with the exception of macro bindings them-
{ ?tables; selves, we allow only runtime definitions to be exported from
?driver; } libraries. However, those runtime definitions may be im-
end macro; ported into a library for use either during compilation or
The macro is implemented in the parsergen-plugin mod- at runtime or both. For example, if a library defined pars-
ule, but through the addition of an “in” clause, parsergen- ing utilities intended for use during macro expansion, those
definer will be bound in the parsergen module of the parser- utilities would be defined and exported as ordinary runtime
gen library. The macro will not be bound or available within definitions. In order to make those utilities available to its
parsergen-plugin. macros and other compile-time code, a client library would
The compiler maintains a table of macros defined by plu- employ a compile-stage use. For example:
gins which it queries when compiling a module definition in
a program under development. If the module matches the define library parsergen
“in” clause of a macro, that macro is bound in the module’s use dylan, stage: both;
use io; // runtime only by default.
namespace and becomes available to all code within that use parsing-tools, stage: compile;
module and to the module’s clients (assuming the macro
binding is exported by the module). export parsergen;
A further implication of the “in” clause is that template end library;
code generated while expanding a call to the macro will be
resolved as if the code had appeared within the target mod- Code on the right hand side of rules within a macro def-
ule. This is important since the namespace and syntactic inition are implicitly compile-stage code and so have ac-
context of the target module is likely to be quite different cess only to bindings imported through a compile-stage use.
from that of the implementing module. Further, details of Other top level definitions can be made into compile-stage
the target module will not be known until after compilation definitions by wrapping them in a compile-stage form. For
of the plugin macro, and then may vary from compile to example:
compile of the program under development. The ability to
define macro parser-definer
delay interpretation of templates is crucial in enabling this { define parser ?:name ?rules:* }
compilation model. => let compiled-grammar = compile-grammar(name, rules);
Although managing a separate runtime program and a let tables = generate-grammar-tables(compiled-form);
parallel compiler plugin sounds like a headache, most of the let driver = generate-parser-engine(compiled-form);
{ ?tables;
details can be hidden by development environment support. ?driver; }
The source of the plugin library can be shown along with end macro;
the source of the target and the two compiled together as
a single action. The plugin is compiled first and the result- compile-stage
ing library dynamically loaded into the compiler to ensure define class <compiled-grammar> (<object>)
that the macros defined are available when compilation of // ...
the runtime program begins. Procedural macros that define end class;
other procedural macros are possible, with plugins being
define method compile-grammar (name, rules) => (grammar)
loaded in order to compile other plugins. // ... calls to parsing-tools ...
If cross-compiling, two compiler back-ends may need to end method;
be loaded simultaneously in order to compile plugins. How-
ever, in a situation where the runtime target platform is // etc.
resource constrained, running large or otherwise resource- end compile-stage;
hungry compile-time code only on the development host can
be a necessity. Compilation of a library containing both compile-stage
There are two main drawbacks to the plugin approach. and run-stage code can be thought of as proceeding as fol-
The first is that the transition from Dylan’s rewrite-rule only lows. Any procedure with the same effect is equally valid.
macro system to procedural macros cannot be seamless. As If the library definition has compile-stage uses, a compile-
soon as general compile-time evaluation is used, the macro stage version of the library in the form of an application is
must be moved out of the runtime source into the plugin needed in order to compile the run-stage portion of the li-
source. brary. The compiler processes each top level form of the li-
The second drawback is the danger involved in running brary in turn. As it encounters run-stage code, it processes it
arbitrary user code within the compiler/development envi- normally. As it encounters macros or explicit compile-stage
ronment process. Instability may result. code, that code is compiled and then immediately dynami-
cally loaded into the compile-stage app. If the compiler finds
6.3 Compile Time Loading and Eval-When a call to a procedural macro in run-stage code, a macro call
is packaged and sent to the macro function in the compile-
To achieve a seamless transition between Dylan’s rewrite- stage app where it is expanded before the results are sent
rule only macro system and full procedural macros it must back to the compiler. When compilation is complete, two
be possible to use procedural facilities while still interleaving binaries are saved: one for the run-stage code and one for
macros with runtime code. the compile-stage code. The compile-stage aspect will be
While such integration is desirable, maintaining as clear loaded when compiling client libraries if procedural macros
a distinction as possible between compile-time and runtime are exported.
dependencies is also important. Code within a library must

8
One likely variation on this is to dynamically load the For an example of where quoting is necessary, consider the
compile-stage code directly into the compiler process rather case of nested macros, that is, macros that define other
than start a separate application. Also, under certain cir- macros. Suppose we find that several macros have a similar
cumstances it may be possible to determine the compile- form. We can abstract this macro pattern into a macro-
stage code of a library by a pre-pass, avoiding the need for defining macro. For example, consider a macro that defines
incremental compilation of the compile-stage code. macros that pass a thunk to a given procedure [2]:
The problem with this approach is clearly its complex-
ity for implementors. In the general case, it requires either define macro caller-definer
{ define caller ?abbrev:name ?proc:expression end }
incremental compilation or interpretation of compile-stage => { define macro ?abbrev
code interleaved with another ongoing compilation; new and { ?abbrev (\?var:name) \?:body end }
challenging requirements for Dylan implementations, which => { ?proc(method (\?var) \?body end) }
currently can be as simple as straightforward batch compil- end macro }
end macro;
ers.
define caller collect call-with-new-collector end;
7 Status define caller catch call-with-current-continuation end;

Our current status is that we have implemented a d-expressions where \ is used to prevent evaluation of pattern variables in
library and have used it to write our compiler. We sup- the inner macro definition.
port the plugin model of compile-time evaluation, but don’t Unfortunately, several limitations restrict Lisp macros’
yet support the full strength compile-time evaluation model. ease of use. First, variable capture is a real problem and
Nevertheless, we are capable of it since we already support leads to difficult to debug macros. Second, macro calls are
incremental compilation and dynamic update. difficult to debug as Lisp macros do not offer a mechanism
The Dylan language is out there and the rewrite-rule only for correlating between macro expanded code and a user’s
macros are part of the Dylan culture just as much as they are original source code.
the Lisp culture. There are people out there really writing Several other researchers have reported on systems that
non-trivial macros in Dylan for themselves. So it has proved generalize the backquote mechanism to infix syntaxes. Weise
to be a very usable system. What’s more, there’s more than and Crew [14] are discussed below. Engler, Hsieh, and
one implementation. Kaashoek [6] employ a version of backquote to support a
form of partial evaluation in C.
8 Related Work
8.2 Scheme
In this section we survey the most related macro systems. Scheme’s macro system (i.e., syntax-case and syntax-rules)
comes the closest to offering the power and ease of use of
8.1 Lisp Macros Dylan’s macro system. We feel though that the combina-
tion of our rewrite-rule only and procedural macro systems
Dylan was very much inspired by Lisp’s destructuring and
provides a much more cohesive whole that gracefully pro-
backquote facilities. Their advent was a quantum leap in
gresses from a self-contained pattern language to the full
macro systems and played a big part in popularizing macros
power of procedural macros. Scheme’s system, on the other
and Lisp itself [2]. We maintain that our patterns and tem-
hand, uses a different notation for their pattern language,
plates are as natural to use as Lisp’s destructuring and back-
syntax-rules, than from their procedural macro system,
quote. In fact, we feel that our splicing operator is an easier
syntax-case. In Dylan, the same basic pattern match-
to use unification of backquote’s unquote (,) and splicing
ing language naturally incorporates the full Dylan language
(,@) operators. The reason this is possible in Dylan is be-
when writing procedural macros. In particular, Scheme’s
cause pattern variables can be bound to the actual elements
system requires a programmer to introduce local pattern
of a sequence and not the sequence itself. For example, in
variables using with-syntax whereas, in Dylan, a program-
Lisp, in order to splice in elements to the end of a parameter
mer merely introduces them with the usual local variable
list, one uses the splicing operator on a whole sequence as
(i.e., let) definition syntax. For example in Scheme one
follows:
must write the following:
(let ((more ’(c d e)))
‘(a b ,@more)) (lambda (f)
(with-syntax ((stuff f))
(syntax stuff)))
while in Dylan, one could do the same with the following:
begin where in Dylan one could do the equivalent with the follow-
let more = {c, d, e}; ing:
{(a, b, ?more)}
end method (f) { ?f } end

without the need for a different splicing operator. In order This stems from the fact that Scheme requires users to spec-
to better relate Dylan’s templates to Lisp’s backquote, we ify reserved intermediate words up front otherwise names
present the following table showing a loose correspondence occurring within a pattern or template are interpreted as
between the two: pattern variables. In Dylan, pattern variables have a spe-
cial notation and thus reserved intermediate words do not
Name Lisp Dylan need to be declared ahead of time, that is, they’re the names
backquote ‘() {} without the special notation.
unquote , ? Scheme’s system provides a nice solution to hygiene that
splicing ,@ ? automatically avoids most variable capture errors. Dylan
quote ’ \

9
improves on this in a couple ways. First, it handles the of macro produced code, and provides a template substi-
more general problem of referential transparency in the face tution mechanism based on Lisp’s backquote mechanism.
of modules (a.k.a., namespaces), ensuring that variables ref- Their system lacks support for hygiene, but instead requires
erenced within a macro definition mean the same thing in programmer intervention to avoid variable capture errors.
macro calls regardless of their namespace context. Second, Unfortunately, their system is restrictive. Their macro
Dylan provides a much more natural mechanism for intro- syntax is constrained to that describable by what is, essen-
ducing hygiene escapes using an intuitive notation, ?=. Con- tially, a weak regular expression. In contrast, in our system,
sider the Scheme the following version of the Dylan repeat within the liberal shapes of an SST, just about anything
macro defined above: goes, and further, the fragments can be parsed using any
(define-syntax repeat
appropriate technique. Finally, because templates are ea-
(lambda (x) gerly parsed, it’s not clear that forward references within
(syntax-case x () expanders (and so mutual recursion, say) works in their sys-
((k e ...) tem. It could be made to work, however (by forward declar-
(with-syntax
((break (datum->syntax-object (syntax k) ’break)))
ing the input syntax apart from the transformer), but this
(syntax would be awkward to use.
(call-with-current-continuation Their system requires knowledge of formal parsing intri-
(lambda (break) cacies. They say:
(let repeat () e ... (repeat))))))))))

Notice how Scheme must use a long-winded call and with-syntax The pattern parser used to parse macro invoca-
binding to produce the desired break variable. tions requires that detecting the end of a repe-
Although Scheme’s repeated pattern matching mecha- tition or the presence of an optional element re-
nism (i.e., ...) is cute, it is unfortunately brittle. Scheme’s quire only one token lookahead. It will report an
repeated patterns only win if every input in a given sequence error in the specification of a pattern if the end
has the same general shape; as soon as this isn’t the case, of a repetition cannot be uniquely determined by
one must resort to general traversal. As an example of why one token lookahead.
the Dylan approach is more general, consider the case of
It is not a reasonable requirement that programmers must
sequences of heterogeneous input. For example, imagine ex-
understand and solve static grammar ambiguities like this.
tending the common define-structure example [4] such
We believe that as far as possible, programmers should
that constant structure slots can be optionally defined, say,
only have to know the concrete syntax, not the abstract syn-
and then compare the code. That is, a Scheme macro call
tax. Unfortunately, their system requires programmers to
to define-structure would look like:
use syntax accessors to extract syntactic elements, whereas
(define-structure foo x y (const z)) our system allows access through pattern matching concrete
syntax.
whereas an equivalent Dylan macro call would look like: Their system requires templates to always be consistent.
define structure foo x, y, const z end; We feel that it’s often useful to be able to generate incom-
The usual Scheme define-structure involves the following plete templates containing macro parameters for instance,
pattern: that are spliced together at the end of macro processing to
form something recognizable. Weise and Crew’s insistence
( name field ...) that the result of evaluating a template should always be a
recognizable syntactic entity defeats that mechanism. Being
used in a template in the following fashion: able to work easily with intermediate part-expressions that
(syntax have no corresponding full-AST class (e.g. a disembodied
(begin pair of function arguments) is a win for the SST approach.
(define constructor
(lambda (field ...) (vector ’name field ...)))
;; ... 9 Summary
))
Most of the people involved in Dylan’s design were experi-
will not work for this case. The Scheme code has to emulate enced Lisp programmers. Anyone who has worked exten-
Dylan’s more general traversal over the input in order to sively in Lisp is well aware of the potential of Lisp macros,
handle this possibility. and despite the challenge presented by Dylan’s algebraic
One advantage Scheme macros have over Dylan macros syntax, we knew we still wanted full-power macros. As com-
is that one can limit a macro’s visibility to a lexically lo- piler writers, we also realised the value of source code tools
cal region of code using let-syntax and letrec-syntax. as a library. D-expressions are the result.
Dylan macros could be extended to provide local scoping We have brought together the elements of a Lisp-inspired
for limited macro shapes. In particular, because of the re- SST capable of representing Dylan’s rich algebraic syntax,
quirements of an initial skeletal parse, a local macro could best-of-breed source level code manipulation tools, and a
not be defined that introduced new end brackets, such as model of compile-time evaluation suitable for a language
statement macros. It would be possible though to introduce with strong compiler/runtime separation like Dylan, to pro-
local function macros to Dylan as these are unambiguously duce what we feel is the first macro system with Lisp’s power
parseable. and simplicity for a language with a conventional syntax.

8.3 Weise and Crew Acknowledgements


Weise and Crew [14] describe a macro system for infix syntax
languages such as C. Their macro system is programmable Much of the initial design work on Dylan macros was done
in an extended form of C, guarantees syntactic correctness at Apple. The syntax of macro definitions, patterns, con-

10
straints, and templates used in Dylan’s standard macro sys- [13] A. Shalit. The Dylan Reference Manual. Addison Wes-
tem was developed by Mike Kahl. Dylan’s very first ”loose ley, 1996.
grammar” was due to David Moon, who also designed a pat-
tern matching and constraint parsing model suitable for such [14] Daniel Weise and Roger Crew. Programmable syntax
a grammar. Earlier, Moon had proposed a model of compile- macros. In Proceedings of the SIGPLAN ’93 Conference
time evaluation for Dylan in the context of a prefix-syntax on Programming Language Design and Implementation,
macro system from which ours takes some terminology. pages 156–165, June 1993.
Generalizations of Dylan’s loose grammar enabling it to
describe all Dylan’s syntactic forms, along with the first im- A Auxiliary Macros
plementation of the macro system, were developed by the
authors while at Harlequin Ltd. The procedural macro sys- A number of limitations of local rewrite rules require resort-
tem was initially designed and implemented as a component ing to top-level auxiliary macros. The first problem is that
of Harlequin’s Dylan compiler, now owned by Functional local rewrite rules do not have access to pattern variables
Objects, Inc. defined in main rule sets. The usual solution is to employ
Many other ”Dylan Partners” made contributions to the an auxiliary macro which takes those needed variables as
design of Dylan macros, particularly the Gwydion team at extra macro arguments. For example, suppose we want to
CMU. add a prefix option to allow the creation of properties with
Dave Moon provided feedback on various drafts of this a common prefix. We need to introduce an auxiliary macro
paper. This paper also benefitted from helpful discussions so that the prefix variable can be in scope when iterating
with Alan Bawden and Tony Mann. Tony Mann gave many over the properties. For example, consider the following:
insights into the limits of the rewrite-rule only macro system
by pushing the macro writing envelope. define macro properties-definer
{ define properties ?kind:name
prefixed-by ?prefix:name ?properties:* end }
References => { define variable ?kind
= concatenate
(prefixed-props (?"prefix") ?properties end) }
[1] Barrett, R, Ramsay, A, and Sloman, A. POP-11: a { define properties ?kind:name ?properties:* end }
Practical Language for Artificial Intelligence. Ellis- => { define variable ?kind
Horwood, Chicester, 1985. = concatenate
(prefixed-props ("") ?properties end) }
[2] A. Bawden. Quasiquotation in lisp. Proceedings of the end macro;
ACM Conference on Lisp and Functional Programming, define macro prefixed-props
?(?), 1999. { prefixed-props (?prefix:name) end }
=> { #() }
[3] Luca Cardelli, Florian Matthes, and Martin Abadi. Ex- { prefixed-props (?prefix:name) ?prop:name; ?more:* end }
tensible syntax with lexical scoping. Technical Report => { list(?prefix ## ?prop),
121, DEC SRC, February 1994. prefixed-props (?prefix) ?more end) }
end macro;
[4] Kent R. Dybvig. The Scheme Programming Language.
Prentice-Hall, 1987. Auxiliary macros are also needed when pattern variables
must be walked in two different ways. Consider a macro,
[5] R.K. Dybvig, R. Hieb, and C. Bruggeman. Syntactic called iterate, for creating internally recursive procedures
abstraction in scheme. Lisp and Symbolic Computation, that are as convenient as loops. It has the following basic
?(?), 1993. form:
[6] E.R. Engler, W.C. Hsieh, and M.F. Kaashoek. ‘c: A iterate name (variable = init, ...)
language for fast, efficient, high-level dynamic code gen- body
eration. In Proceedings of Symposium on Principles of end iterate
Programming Languages, January 1996.
and has the semantics of evaluating the body with the vari-
[7] N. Feinberg, S. Keene, R. Mathews, and T. Withington. ables initially bound to the inits and such that any call to
Dylan Programming. Addison Wesley, 1997. name inside the body recursively calls the procedure with
the variables bound to the arguments of the call. In writ-
[8] P. Graham. On Lisp: Advanced Techniques for Com- ing a macro for iterate, the parenthesized fragments must
mon Lisp. Prentice-Hall, 1994. be walked once to extract variables and another time to ex-
[9] Guy Lewis Steele Jr. Common LISP: The Language, tract initial values. Unfortunately, with the current macro
Second Edition. Digital Press, Burlington, MA, 1990. system, there is no way to walk the same pattern variable
with more than one set of local rewrite rules. Instead, an
[10] R. Kelsey, W. Clinger, and J. Rees. Revised5 report extra copy of the pattern variable must be made and passed
on the algorithmic language scheme. Higher-Order and on to an auxiliary macro:
Symbolic Computation, 11(1):7–105, 1998.
[11] Brian W. Kernighan and Dennis M. Ritchie. The C pro-
gramming language. Prentice-Hall, Englewood Cliffs,
NJ 07632, USA, second edition, 1988.
[12] E. Kohlbecker, D. P. Friedman, M. Felleisen, and
B. Duba. Hygenic macro expansion. In Proceedings of
the 1986 ACM Conference on Lisp and Functional Pro-
gramming, pages 151–161. ACM, ACM, August 1986.

11
define macro iterate define macro iterate2
{ iterate ?loop:name (?args:*) ?:body end } { iterate2 ?:name (?bindings:*) ?:body end }
=> { iterate-aux ?loop (?args) (?args) => { local method ?name (?@vars{ ?bindings }) ?body end;
?body ?name(?@inits{ ?bindings }) }
end } vars:
end macro iterate; { }
=> { }
define macro iterate-aux { ?:variable = ?:expression, ... }
{ iterate-aux ?loop:name (?args) (?inits) ?:body end } => { ?variable, ... }
=> { local method ?loop (?args) ?body end; inits:
?loop(?inits) } { }
args: => { }
{ } { ?:variable = ?:expression, ... }
=> { } => { ?expression, ... }
{ ?:variable = ?:expression, ... } end macro;
=> { ?variable, ... }
inits:
{ }
We can also introduce a template macro call
=> { }
{ ?:variable = ?:expression, ... } ?@{ <arbitrary-template-stuff-that-forms-a-macro-call> }
=> { ?expression, ... }
end macro repeatable-aux; which acts as a kind of shorthand for the :macro constraint
and permits the definition of macros for use as shared rewrit-
ing tools. For example:
B Dylan Rewrite-Rule Only Macro Extensions
define traced macro mcreverse
{ mcreverse(?list:*) } => { ?list }
B.1 Template Calls list:
{ } => { }
Both of these reasons for needing auxiliary macros are some- { ?:expression } => { ?expression }
what artificial because in fact local rewrite rules are really { ?:expression, ... } => { ..., ?expression }
like local functions and should allow extra arguments and end macro;
should be directly callable on any pattern variable. The define traced macro user
problem lies in the fact that the local rewrite rule is artifi- { user(?stuff:*) } => { list(?@{ mcreverse(?stuff) }) }
cially tied to one pattern variable by virtue of its name. end macro;
In order to overcome this problem, we introduce a di-
rect template call, obviating the need for auxiliary macros where the traced modifier causes macro expansion to be
is many cases. This leads to a much more elegant solution to traced. For example, here’s the trace for user(1, 2, 3):
these more complicated macros. A template auxiliary rule
set call has the following form: { user } > user(1, 2, 3)
{ mcreverse } > mcreverse(1, 2, 3)
{ mcreverse } < 3 , 2 , 1
?@rule-name{ <arbitrary-template-stuff> }
{ user } < list (3, 2, 1)

where a new macro punctuation ?@ marks a template call. Like normal macro calls, a new hygiene context in cre-
For example, in the prefixed properties-definer macro, we ated for ?@\{ } calls, so you could define gensym thusly:
can now directly invoke a local rewrite rule set with extra
arguments: define macro gensym
{ gensym() } => { gensymed-name }
end macro;
define macro properties-definer
{ define properties ?kind:name
prefixed-by ?prefix:name ?properties:* end }
=> { define variable ?kind C Limits of Rewrite-Rule Only Macro System
= concatenate
(?@prefixed-properties{ ?"prefix"; ?properties }) }
{ define properties ?kind:name ?properties:* end }
Dylan’s rewrite-rule only macros are more powerful than
=> { define variable ?kind they first appear. Unlike a grand tradition in Lisp, replace-
= concatenate(?@prefixed-properties{ ""; ?properties } } ment phrases are constructed purely by template substitu-
prefixed-properties: tion and not arbitrary computation. A number of inherent
{ ?prefix:name }
=> { #() }
properties increase the power of this base macro system.
{ ?prefix:name; ?property:name; ?more:* } First, there is a powerful set of built-in atomic features
=> { list(?prefix ## ?property), such as hygiene, name concatenation, and pattern constraints
?@prefixed-properties{ ?prefix; ?more } } which often obviate the need for such procedural facilities.
end macro;
Other macro systems (e.g., Scheme’s syntax-case) must
rely on function calls for these facilities and must resort to
Similarly, iterate can now be written without auxiliary the full power of procedural macros.
macros using two template calls: Second, the rewrite-rule only system allows for powerful
control structures, not unlike a mini programming language.
Macros can expand into other macro calls including into
recursive calls. Although not discussed in this paper, macros
can have keyword and optional arguments that can be used
to collect a series of properties which can be spliced into the
resulting output in bulk. For example, another formulation
of the iterate macro making use of this facility would be:

12
define traced macro iterate3
{ iterate3 ?:name (?bindings:*) ?:body end }
=> { ?@generate{ ?name (?@parse{ ?bindings }) ?body } }
parse:
{ }
=> { }
{ ?:variable = ?:expression, ... }
=> { var: ?variable, init: ?expression, ... }
generate:
{ ?:name (#key ??var:variable, ??init:expression) ?:body }
=> { local method ?name (??var, ...) ?body end;
?name(??init, ...) }
end macro;

where, for example, ??var:variable matches all keywords


arguments with keyword var: and binds the pattern variable
??var to the sequence of their values.
Finally, expanding into calls to runtime functions can of-
ten defer calculations that would have otherwise needed to
be computed during macro expansion. Furthermore, con-
stant folding and more generally partial evaluation can of-
ten be relied on to collapse these residual calls. For example,
concatenation can be deferred to runtime as follows:
define macro catsyms
{ } => { }
{ catsyms(?items) } => { concatenate(?items) }
items:
{ ?item:name, ... } => { list(?#"name"), ... }
end macro;

? catsyms(a, b, c);
> #(#"a", #"b", #"c")

Similarly, counting a sequence of items can be performed


as follows:
define macro count
{ count() } => { 0 }
{ count(?item:expression, ...) } => { 1 + ... }
end macro;

? count(a, b, c);
> 3

Unfortunately, Dylan’s rewrite-rule only macros can only


count a given input sequence, but can’t count from say 0
to some given limit. Thus, it is awkward to construct a
macro, for example, that expands into a specified numbers
of functions with increasing number of parameters.

13

S-ar putea să vă placă și