Scheme macro expansion: Nesting let-syntax inside define-syntax

1.3k views Asked by At

I wish to expand

(foo x (f n) (f n) (arbitrary) (f n) ...)

into

(begin (x 'f n) (x 'f n) (arbitrary) (x 'f n) ...)

my attempt is:

(define-syntax foo
  (syntax-rules ()
    ((_ l a ...)
     (let-syntax ((f (syntax-rules ()
                       ((_ n) (l (quote f) n)))))
       (begin a ...)))))

(define (x t1 t2) (cons t1 t2))   ;; for example only
(define (arbitrary) (cons 'a 'b)) ;; for example only
(foo x (f 1) (f 2) (arbitrary) (f 3))

Using a macro stepper I can see that the first stage of the macro expands to

(let-syntax ((f (syntax-rules () ((_ n) (x 'f n)))))
  (begin (f 1) (f 2) (arbitrary) (f 3)))

Which, when evaluated in isolation works perfectly, but when executed as a whole I get an error about f being an undefined identifier. I assume this is an issue in scoping, is this type of macro expansion possible?

1

There are 1 answers

3
Eli Barzilay On BEST ANSWER

Yeah, you need to get f from somewhere -- your macro just makes it up, and therefore it is not visible to users of foo. When you do consider that you need to get it from somewhere, the question is where would you get it from? Here's a fixed version of your code that assumes that it is the first thing in the second subform of foo:

(define-syntax foo
  (syntax-rules ()
    [(_ l (f a) more ...)
     (let-syntax ([f (syntax-rules ()
                       [(_ n) (l 'f n)])])
       (list (f a) more ...))]))

(define (x t1 t2) (cons t1 t2))
(define (arbitrary) (cons 'a 'b))
(foo x (f 1) (f 2) (arbitrary) (f 3))

(I also made it expand into a list to see that all forms are transformed.)

However, if you want a global kind of f to be used inside foo, then you really have to do just that: define a global f. Here's a limited way to do that:

;; no body => using `f' is always an error
(define-syntax f (syntax-rules ()))

(define-syntax foo
  (syntax-rules ()
    [(_ l a ...) (list (foo-helper l a) ...)]))
(define-syntax foo-helper
  (syntax-rules (f) ; match on f and transform it
    [(_ l (f n)) (l 'f n)]
    [(_ l a)     a]))

(define (x t1 t2) (cons t1 t2))
(define (arbitrary) (cons 'a 'b))
(foo x (f 1) (f 2) (arbitrary) (f 3))

The main limitation in this is that it will only work if one of the a forms is using f -- but it won't work if it is nested in an expression. For example, this will throw a syntax error:

(foo x (f 1) (f 2) (arbitrary)
       (let ([n 3]) (f n)))

You can imagine complicating foo-helper and make it scan its input recursively, but that's a slippery slope you don't want to get into. (You'll need to make special cases for places like inside a quote, in a binding, etc.)

The way to solve that in Racket (and recently in Guile too) is to use a syntax parameter. Think about this as binding f to the same useless macro using define-syntax-parameter, and then use syntax-parameterize to "adjust" its meaning inside a foo to a macro that does the transformation that you want. Here's how this looks like:

;; needed to get syntax parameters
(require racket/stxparam)

;; same useless definition, but as a syntax parameter
(define-syntax-parameter f (syntax-rules ()))

(define-syntax foo
  (syntax-rules ()
    [(_ l a ...)
     ;; adjust it inside these forms
     (syntax-parameterize ([f (syntax-rules ()
                                [(_ n) (l 'f n)])])
       (list a ...))]))

(define (x t1 t2) (cons t1 t2))
(define (arbitrary) (cons 'a 'b))
(foo x (f 1) (f 2) (arbitrary)
       (let ([n 3]) (f n)))