Macros expanding to definitions

Macros expanding to definitions

This post is about macros that expand into definitions.

The examples will use syntax/parse, so all snippets
work if they are preceded with:

#lang racket
(require (for-syntax racket/syntax syntax/parse))

Scope sets and bindings

The first example is the macro define-a.

1  (define-syntax (define-a stx)
2    (syntax-parse stx
3     [(_define-a expr)
4      #'(define a expr)]))
5
6  (define-a 42)
7  a               ; unbound identifier

The variable reference a on line 7 results in an unbound identifier error.
Why?

During macro expansion the expander attaches sets of scopes to each identifier.

When the macro application (define-a 42) is expanded a new scope s,
is created and attached to the variables bound in the syntax fragment
produced by the macro. The expansion becomes:

(define a_{s} 42)

The variable reference a on line 7, has no extra scopes attached,
so the reference is a_{}.

The scope sets are used to determine whether one variable can bind another.

A reference’s binding is found as one whose set of scopes is the
largest subset of the reference’s own scopes (in addition to having
the same symbolic name).

We now see, why a_{s} doesn't bind a_{}. The problem is that {s}
is not a subset of {}.

Copying the lexical context

If we want the definition to bind a_{} we must produce an identifier a,
that has no extra scopes. We can use format-id (or datum->syntax)
for this. A little simplified, a call to format-id looks like this:

(format-id lexical-context format-string fmt value ...)

The syntax transformer for define-a receives the syntax object
to expand (define-a 42) as the argument stx. Using stx as
the lexical context for a copies the scope set attached to stx
to our new identifier. Effectively giving us a an identifer a
with the same lexical context as (define-a 42). And that
context is the same as the reference a on line 9.

1  (define-syntax (define-a stx)
2    (syntax-parse stx
3      [(_define-a expr)
4       (define a (format-id stx "~a" 'a))
5       (with-syntax ([a a])
6         #'(define a expr))]))
7  
8  (define-a 42)
9  a                ;; evaluates to 42

Now our define-a introduces an a defined in the context where the macro was called.

Expansion to a macro, that expands into a definition

Let's attempt to define a macro that uses define-a in the expansion.

 1  (define-syntax (define-a stx)
 2    (syntax-parse stx
 3      [(_define-a expr)
 4       (define a (format-id stx "~a" 'a))
 5       (with-syntax ([a a])
 6         #'(define a expr))]))
 7 
 8  (define-syntax (define-a42 stx)
 9    (syntax-parse stx
10      [(_define-a42)
11       #'(define-a 42)]))
12       
13  (define-a42)
14  a            ;; unbound variable

The intention was that define-a42 where to define a variable a with
an initial value 42. However, we get an unbound identifier error again.

Can you see why?

Let's attempt to track the expansion of (define-a42).
The transformer returns #'(define a 42) but each macro call
introduces a new scope, so if we call the new scope t, it returns:

(define-a_{t} 42)

The expander then calls the transformer of define-a with stx
bound to (define-a_{t} 42). That macro call introduces a new scope s.
The return value will be [we are ignoring the scope set for define]:

(define a_{t} 42)

Since the scope set {t} is not a subset of {} a_{t} does not bind a_{}.

Fixing the example

Since define-a uses the context of stx for the variable a,
we can fix the example above like this:

(define-syntax (define-a42 stx)
  (syntax-parse stx
    [(_define-a42)
     (syntax-local-introduce
      #'(define-a 42))]))

The syntax-local-introduce removes the scope normally
added by each macro call.

3 Likes