Search code examples
macrosschemeguile

In guile scheme, how to prevent renaming when calling macro from another macro?


; Having this definition that creates identifier `self'
(define-syntax alambda
  (lambda (stx)
    (syntax-case stx ()
      [(alambda lambda-list . body)
       (with-syntax ([name (datum->syntax #'alambda 'self)])
         #'(letrec ([name (lambda lambda-list . body)])
             name))])))

; I want to "compose" it with another macro
(define-syntax-rule [apply-alambda args argv . body]
  ((alambda args . body) . argv))

; But then it doesn't work (while alambda itself does)
(apply-alambda [x] [5] (if (= 0 x) 1 (* x (self (- x 1)))))
; => Unbound variable: self
; (expected 120)

How to prevent apply-alambda from renaming self?


I tried to use define-macro which also didn't work but for a different reason:

(defmacro apply-alambda [args argv . body]
  ((alambda args . body) . argv))
; => lambda: bad lambda in form (lambda args . body)

Here, I don't even know what went wrong


Solution

  • Your alambda macro is unhygienic, and unhygienic macros don't compose well. Unhygienic macros that create identifiers based on subterms already used for another purpose compose especially poorly. One solution is to create a helper macro that takes the "lexical context" for the new identifiers as a separate argument. Then create the derived macros from that.

    (define-syntax alambda/lctx
      (lambda (stx)
        (syntax-case stx ()
          [(alambda lctx formals . body)
           (with-syntax ([name (datum->syntax #'lctx 'self)])
             #'(letrec ([name (lambda formals . body)])
                 name))])))
    
    (define-syntax alambda
      (lambda (stx)
        (syntax-case stx ()
          [(alambda formals . body)
           #'(alambda/lctx alambda formals . body)])))
    
    (define-syntax apply-alambda
      (lambda (stx)
        (syntax-case stx ()
          [(apply-alambda formals argv . body)
           #'((alambda/lctx apply-alambda formals . body) . argv)])))
    
    (apply-alambda [x] [5] (if (= 0 x) 1 (* x (self (- x 1)))))
    

    In the alambda macro, the lexical context used to create the self binder is taken from the reference to the macro itself. The macro makes that argument explicit in the call to alambda/lctx. Likewise with apply-alambda---and if you want to create another macro that expands into apply-alambda, then you should likewise create an apply-alambda/lctx helper.

    (In Racket, lexical context is attached not only to identifiers but also to the list structure (the "parentheses"), and it is common for unhygienic macros to use the whole syntax object, as in (datum->syntax stx 'self). This avoids the need for a separate helper macro.)

    Note: using define-syntax-rule to define alambda and apply-alambda doesn't work, because it doesn't actually bind the identifier in operator position.


    You might be tempted to make apply-alambda call alambda with an alambda identifier with the lexical context corresponding to the use of the apply-alambda form, like this:

    (define-syntax bad-apply-alambda
      (lambda (stx)
        (syntax-case stx ()
          [(apply-alambda formals argv . body)
           (with-syntax ([alambda (datum->syntax #'apply-alambda 'alambda)])
             #'((alambda formals . body) . argv))])))
    

    This version is wrong. It behaves incorrectly if alambda is not bound (or is bound to the wrong thing) in the scope where bad-apply-alambda is used. For example:

    (let ([alambda 5])
      (bad-apply-alambda [x] [5] (if (= 0 x) 1 (* x (self (- x 1))))))