← Lambda Land · Blog


Writing Racket Macros: define-syntax and phases

19 May 2023

There are a bunch of different ways of writing a macro in Racket. There are also some tricky things around phases to keep in mind. This is to help me keep them all straight.

3+1 ways to make a macro

This form:

(define-syntax-rule (foo args ...) (use args ...))

is equivalent to:

(define-syntax foo
  (syntax-rules ()
    ([foo args ...] (use args ...))))

Which, is in turn equivalent to:

(define-syntax foo
  (λ (stx)
    (syntax-case stx ()
      [(gensymed-foo args ...) #'(use args ...)])))  ; gensymed-foo is like foo but doesn't match in the template

because syntax-rules expands to syntax-case with some fancy wrapping around it.

This makes syntax-case the most powerful of them all, and it’s here that we’re treating syntax as data comes to the forefront: you can bind the syntax object directly (in our example, with the (λ (stx) ...) part), pattern match on it, and finally return new syntax with the #' notation.

define-syntax-rule is the weakest of the three, but handles a common case: just a single form that you’d like to transform a little bit. This version doesn’t allow for writing multiple clauses.

define-syntax with syntax-rules is in the middle: the bodies of each of the rule match arms ((use args ...)) are assumed to be syntax objects. This works well for the majority of cases I think. It’s only when you need to do really hairy stuff and manually generate code that can’t be put together with repeats (...) that you need the full power of syntax-case at your disposal.

Note that there are two forms of define-syntax: (define-syntax (id stx) body ...) is shorthand for (define-syntax id (λ (stx) body ...)) much like the shorthand for building functions with define.

Bonus: more power

A cousin of syntax-case is syntax-parse. I was confused about this one for a bit because the names are so close, and they share a lot of similarities. syntax-case’s documentation is in the Racket Reference proper, while syntax-parse’s documentation lives with the syntax module documentation.

Our previous example would be written like this:

(define-syntax (foo stx)
  (syntax-parse stx
    [(_ args ...) #'(use args ...)]))

or equivalently:

(define-syntax foo
  (λ (stx)
    (syntax-parse stx
      [(_ args ...) #'(use args ...)])))

syntax-parse supports keyword arguments like #:with and #:when to do some pattern matching and predicate checking. syntax-parse will backtrack through match arms until it finds a matching and satisfying clause.

As far as I can tell, syntax-parse is strictly the most powerful of the syntax manipulating forms that I’ve outlined here.

Phases

It doesn’t seem like macros can use the functions in their current module by default. However, if you wrap your function definitions in begin-for-syntax, this shifts the function definitions “up” a phase, and they can be used at the same level as functions.

(begin-for-syntax
  (define foo (stx) (add-stx-prop stx 'bar 'baz)))

(define-syntax my-macro
  (syntax-parse stx
    #:with foo-ed (foo stx)
    #'foo-ed))

You can also require a module with the for-syntax keyword:

(require (for-syntax "util.rkt"))

For more information on phases, see the Racket Docs on phase levels.