[racket] tutorial: exploring the boundaries of outer space

From: Danny Yoo (dyoo at cs.wpi.edu)
Date: Wed Apr 11 08:45:10 EDT 2012

> Is there a way to make (outer (outer x)) do the right thing while still
> using syntax parameters?
> Do you consider this behavior to be surprising? Would you expect as the
> author of `m' that your macro affects the behavior of `outer'?
> (define-syntax m
>  (syntax-rules ()
>    ((_ val)
>     ;; Helper function
>     (let ()
>       (def (h i) (* i val))
>       (h 2)))))
> (def (g x)
>  (def (h x)
>    (m (outer x)))
>  (h 3))
> -> "expand: unbound identifier in module in: x"

Hi Brian and everyone,

Yikes.  Yes, that's unexpected.  Nice catch!

Ok, the following code is an extension to the first version that
should resolve the problem.  I'll have to add this as an epilogue in
the tutorial, and include test cases to be more confident that it is
doing the right thing.

The main changes include holding a stack of scopes, rather than just
the innermost, and being able to skip k scopes by providing a number
to outer.

#lang racket

(require racket/stxparam

(require (for-syntax racket/list))

;; We represent both the inside and outside of a scope
;; by holding both respectively in these two parameters.
(define-syntax-parameter current-insides '())
(define-syntax-parameter current-outsides '())
;; Unlike the first version, we keep a stack of these scopes
;; around for inspection.

;; We revise def to maintain both.
(define-syntax (def stx)
  (syntax-case stx ()
    [(_ (name args ...) body ...)
     (with-syntax ([fun-stx stx])
            (cons #'fun-stx
                  (syntax-parameter-value #'current-outsides))])
          (define (name args ...)
             ([current-insides (cons #'fun-stx
             body ...))))]))

  ;; find-scope: identifier (listof syntax) (listof syntax)
positive-integer -> (U syntax #f)

  ;; Given an identifier, we now need a function to help
  ;; us find the innermost scope that still can refer to id.
  ;; Once we do so, we can then provide the lexical information
  ;; outside of that scope.
  (define (find-scope id outsides insides n)
      [(empty? insides)
       ;; FIXME: we really should raise a good syntax error at this point.
      ;; If an instance of an an identifier in the inside of a scope
      ;; matches the id we're searching, then we just found an appropriate
      ;; scope.
      [(free-identifier=? id
                          (datum->syntax (first insides) (syntax-e id)))
         [(= n 1)
          (first outsides)]
          ;; If we need to skip more than one scope, we restructure the
          ;; searched id so that it's as if we're looking for it from
          ;; the perspective of the outside.
          (find-scope (datum->syntax (first outsides) (syntax-e id))
                      (rest outsides)
                      (rest insides)
                      (sub1 n))])]
       (find-scope id (rest outsides) (rest insides) n)])))

(define-syntax (outer stx)
  (syntax-case stx ()
    [(_ n id)
     (and (exact-positive-integer? (syntax-e #'n))
          (identifier? #'id))
     (let ()
       (define found-binding
         (find-scope #'id
                     (syntax-parameter-value #'current-outsides)
                     (syntax-parameter-value #'current-insides)
                     (syntax-e #'n)))
       (datum->syntax found-binding
                      (syntax-e #'id)
    [(_ id)
     (identifier? #'id)
     #'(outer 1 id)]))

(define-syntax m
  (syntax-rules ()
    ((_ val)
     (let ()
       (def (h x) (* x val))   ;; slight change to make even more devious
       (h 2)))))

(define x 42)

(def (g x)
  (def (h x)
    (m (outer 1 x)))
  (h 3))

Posted on the users mailing list.