[plt-scheme] Question: Restoring lexical context in anaphoric macros

From: Ryan Culpepper (ryanc at ccs.neu.edu)
Date: Fri Feb 19 11:57:59 EST 2010

Remco Bras wrote:
> Hi,
> 
> I have a question regarding datum->syntax, specifically about a macro
> alambda given below.
> 
> (define-syntax alambda
>  (lambda (exp)
>    (syntax-case exp (alambda)
>      [(alambda args body ...)
>       (datum->syntax exp
>                      `(letrec ((self (lambda ,(syntax->datum #'args)
>                                        ,@(syntax->datum #'(body ...)))))
>                         self))])))

This is way more unhygienic than you probably intended. You're 
discarding *all* lexical context information that comes into the macro. 
That that virtually guarantees that it will break if other macros expand 
into uses of 'alambda'. It also means that it breaks if you use it 
inside of 'lambda' or 'letrec'. Try this:

   (let ([lambda 1]) (alambda (x) x))

The way one usually writes macros like this is to generate a 'self' 
identifier based on some syntax given to the macro. For example, if we 
want to give 'self' the same lexical context as the 'args', here's how 
we write the macro:

   (define-syntax alambda
     (lambda (exp)
       (syntax-case exp ()
         [(alambda args body ...)
          (with-syntax ([self (datum->syntax exp 'self)])
            #'(letrec ([self (lambda args body ...)])
                self))])))

Now everything is hygienic except for the one binding you intended to 
capture, 'self', and you can read off exactly *how* 'self' captures: 
'self' captures references that have the same lexical context as the 
whole 'alambda' expression.

You could also write it to use the lexical context of the 'alambda' keyword:
   (datum->syntax #'alambda 'self)
or the formals list:
   (datum->syntax #'args 'self)
or the first body expression:
   (datum->syntax (car (syntax->list #'(body ...))) 'self)

Each of these interacts differently with macros expanding into 'alambda' 
forms, depending on how the other macros put the 'alambda' expression 
together. Did they produce the argument list, or did they get it from 
the original program? Which code gets to see 'self'? And so on.

It's very hard to make unhygienic macros behave right when other macros 
expand into them.

--

In PLT Scheme there is a better way. Define 'self' once as a syntax 
parameter and update it within the body of an 'alambda'. Here's the code:

   (require scheme/stxparam)

   (define-syntax-parameter self
     (lambda (stx)
       (raise-syntax-error #f "self used out of context" stx)))

   (define-syntax alambda
     (syntax-rules ()
       [(alambda args body ...)
        (letrec ([me (lambda args
                       (syntax-parameterize
                           ([self (make-rename-transformer #'me)])
                         body ...))])
          me)]))

First we define 'self' as a syntax parameter. Its default behavior is to 
raise an error, because it's outside of any use of 'alambda'.

'alambda' binds the procedure to a fresh name, 'me', and then locally 
updates 'self' to expand into 'me'. Done.

Now you can add 'dlambda' and it just works.

You can read more about syntax parameters in my blog post here: 
http://macrologist.blogspot.com/2006/04/macros-parameters-binding-and.html

Ryan


> 
> This macro works fine in simple cases and things like the below
> definition of counter work.
> 
> (define counter (let ((x 1))
>                  (alambda (message . stuff)
>                    (cond ((eq? message ':get) x)
>                          ((eq? message ':set) (set! x (+ x (apply +
> stuff))) (self ':get))))))
> 
> However, if I define a macro dlambda using syntax-rules, like below,
> the counter2 (given below) fails to assign the proper context to x in
> the body of dlambda, expanding it to a top-level binding instead.
> 
> (define-syntax dlambda
>  (syntax-rules (dlambda)
>    [(dlambda (name args body ...) ...)
>     (alambda (message . stuff)
>              (cond ((eq? message (quote name)) (apply (lambda args
> body ...) stuff)) ...))]))
> 
> (define counter2 (let ((x 1))
>                   (dlambda (:get () x)
>                            (:set args
>                                  (set! x (+ x (apply + args)))
>                                  (self ':get)))))
> 
> The cause of this bug appears to be the datum->syntax call in
> alambda's definition.
> Is there a way to properly restore the lexical context of the
> arguments to alambda, while injecting the self binding?
> 
> Thanks for your time,
> 
> -Remco Bras
> _________________________________________________
>   For list-related administrative tasks:
>   http://list.cs.brown.edu/mailman/listinfo/plt-scheme



Posted on the users mailing list.