[plt-scheme] Macro techniques for implementing lexical scoped binding constructs

From: Jens Axel Søgaard (jensaxel at soegaard.net)
Date: Mon Nov 13 17:58:44 EST 2006

Ryan Culpepper wrote:
 > Jens Axel Søgaard wrote:
>> Thanks! That made things fell into place. With syntax-parameters
>> the environment is embedded in the syntax objects.
> 
> More precisely, the letn macro makes the extension of the environment
> explicit in the *code that it produces*, rather than by doing
> something in the transformer procedure itself.
> 
> (Aside:
> This theme comes up over and over in macro programming. Recall the
> discussion from a couple weeks ago about attaching information to
> identifiers when you cited Matthew's ICFP paper. In order make
> side-effects work right, you've got to make them part of the program,
> not just something that happens inside a macro transformer.)

>>> The first time I tried this, I wrote the syntax-parameterize like
>>> this:
>>>
>>>   (syntax-parameterize 
>>>         ((the-env (append --- (syntax-parameter-value
>> #'the-env))))
>>>     ---)
>>>
>>> There seems to be a bug in the implementation of
>>> syntax-parameterize that causes that to raise an "identifier 
 >>> out of context" error. So I changed the code to retrieve the old
 >>> value of the parameter outside
>>> of the parameterization, and it worked.

I tried to see what the solution looked like, without the
(let-syntax ([old-env ...]) trick, and it becomes prettier
right away.

(require (lib "stxparam.ss"))
(require-for-syntax (lib "stxparam.ss")
                     (prefix srfi: (lib "1.ss" "srfi")))

(define-syntax-parameter env '())

(begin-for-syntax
   (define (get-env)
     (syntax-local-value
      (syntax-parameter-value #'env))))

(define-syntax (letn stx)
   (syntax-case stx ()
     [(_ ((name expr) ...) body ...)
      #'(syntax-parameterize
         ([env (append (list (cons #'name #'expr) ...)
                       (get-env))])
         body ...)]))

(define-syntax (ref stx)
   (syntax-case stx ()
     [(_ name)
      (cond
        [(srfi:member #'name (get-env) module-identifier=?)
         => (lambda (expr) expr)])]
     [else
      (raise-syntax-error 'ref "unbound variable" #'name)]))

As you say, one gets a "compile: variable used out of context in env636"
error. You and Flatt are fast! - the bug is fixed in SVN already.

[snip]

>> (define-expansion-parameter the-env null)
>>
>> (define-syntax (letn stx)
>>    (syntax-case stx ()
>>      [(letn ([name value] ...) body ...)
>>       (with-syntax ([((old-name . old-value) ...)
>>                      (expansion-value the-env)])
>>         #'(expansion-parameterize
>>            ([the-env (list (cons #'name #'value) ...
>>                            (cons #'old-name #'old-value) ...)])
>>            (begin body ...)))]))
> 
> What you're actually doing here is fetching the old environment and
> serializing it into the code the macro produces. That's another way
> to solve the problem, and it works fine because alists are simple to
> serialize.

Yes - but it wasn't what I intended :-)

>> The above works. However, I tried to implement an
>> expansion-parameter that allow me to write letn like this:
>>
>> (define-syntax (letn stx)
>>    (syntax-case stx ()
>>      [(letn ([name value] ...) body ...)
>>       (let ([old-env (expansion-value the-env)])
>>          (expansion-parameterize
>>            ([the-env (list (cons #'name #'value) ...
>>                            old-env])
>>            #'(begin body ...)))]))
>>
>> The philosophy being that since the environment isn't part
>> of the expanded syntax, it should be "hidden" behind the scenes.
> 
> The superficial problems (the "no #%app transformer" error) can be
> fixed with a few require-for-templates for mzscheme and stxparam.ss
> and some care with #' and #`.
> 
> The deeper problem is that expand-parameterize is the wrong
> abstraction. It looks like it's introducing bindings in the
> transformer procedure, but it's not; it's generating code that
> produces bindings in the program itself. 

Can't it do both?

Introduce the variables right away, use then, and then
when returning the syntax object, wrap it in a, say,
syntax-parameter expression that preserves the values.

> It makes you think you can
> refer to local variables (old-env) in the right-hand side; you can't.
> Once the macro transformer returns, old-env is gone, meaningless, and
> you'll get an "identifier used out of context" error from the
> expander.

Oooh - sorry. The example should have been:

   (define-syntax (letn stx)
     (syntax-case stx ()
       [(letn ([name value] ...) body ...)
        (expansion-parameterize
           ([the-env (list (cons #'name #'value) ...
                            the-env])
             #'(begin body ...)))]))

The old-env was left over from the work-around.


Anyways, now the (let-syntax ([old-env ...))) work-around isn't
necessary, I like your syntax-parameterize solution better.

For me, the tricky point is to get from "I want to change the
value of env in the syntax environment" to "write a form, that
changes env when it is expanded".

That is, instead of writing the correct:

(define-syntax (letn stx)
   (syntax-case stx ()
     [(_ ((name expr) ...) body ...)
      #'(syntax-parameterize
         ([env (append (list (cons #'name #'expr) ...)
                       (get-env))])
         body ...)]))

the impulse is to write:

(define-syntax (letn stx)
   (syntax-case stx ()
     [(_ ((name expr) ...) body ...)
      (a-parameterize-variant
        ([env (append (list (cons #'name #'expr) ...)
                       (get-env))])
         #'(body ...))]))

Hmm - what happens if we introduce the arbitrary restriction that we
can have at most 5 active variables at a time?

A check must be added, but since begin-for-syntax is allowed only at
top-level or module-level his won't work:

(define-syntax (letn stx)
   (syntax-case stx ()
     [(_ ((name expr) ...) body ...)
      #'(syntax-parameterize
         ([env (append (list (cons #'name #'expr) ...)
                       (get-env))])
         (begin-for-syntax
           (if (> (length (get-env)) 5)
               (raise-syntax-error #f
                   "pay up to have more than 5 variables")))
         body ...)]))

So we need to write

(define-syntax (letn stx)
   (syntax-case stx ()
     [(_ ((name expr) ...) body ...)
      #'(syntax-parameterize
         ([env (append (list (cons #'name #'expr) ...)
                       (get-env))])
         (let-syntax
            ([my-begin-for-syntax
              (if (> (length (get-env)) 5)
                (raise-syntax-error #f
                     "pay up to have more than 5 variables")))])
           body ...)])))

I would have preferred to write:

(define-syntax (letn stx)
   (syntax-case stx ()
     [(_ ((name expr) ...) body ...)
      (a-parameterize-variant
        ([env (append (list (cons #'name #'expr) ...)
                       env)])
         (if (> (length env) 5)
             (raise-syntax-error #f
                  "pay up to have more than 5 variables"))
         #'(body ...))]))

But since I can't figure out what it should expand to, odds
are there is a problem with the semantics :-)

-- 
Jens Axel Søgaard



Posted on the users mailing list.