[racket] How to do binding analysis? Distinguishing identifiers: one that creates a binding, the other being bound to it

From: Ryan Culpepper (ryanc at ccs.neu.edu)
Date: Thu Apr 28 17:27:00 EDT 2011

On 04/25/2011 07:39 AM, Zack Brannigan wrote:
> Hi, all:
> What's the best way to analyze a fully-expanded syntax object so that I
> can work out identifier bindings and their dependencies? I have in mind
> what the syncheck tool does: it shows dependencies (through arrows) of a
> variable from where it is used to where it is bound.
> However, looking through collects/drracket/private/syncheck/gui.rkt
> reveals very little how it works out those dependencies. At the very
> least, I want to be able to distinguish when an identifier is doing the
> binding and when an identifier is being used.

The code that implements the analysis part of Check Syntax is in 

Whether an identifier is a binding occurrence or a reference is not 
something you can tell just by looking at the identifier itself. You 
have to check whether it's in a binding position.

> I also played around with syntax/id-table and syntax/boundmap, but they
> don't seem to reveal dependencies. (Unless I used them wrong.)

They're useful if you know how to use them. Here is a function that 
analyzes a fully-expanded expression and builds a table mapping 
references to their corresponding binding occurrences.

(require syntax/id-table)

;; compute-use=>def-table : syntax -> free-id-table
;; Result table maps reference to binding occurrence.
(define (compute-use=>def-table stx)
   (let ([binders (make-free-id-table)]
         [use=>def (make-hasheq)])

     ;; analyze : stx -> void
     (define (analyze stx)
       (syntax-case stx (#%expression
         [(#%expression expr)
          (analyze #'expr)]
         [(#%plain-lambda formals body-expr ...)
          (begin (for-each (lambda (id)
                             (free-id-table-set! binders id id))
                           (get-formals #'formals))
                 (for-each analyze (syntax->list #'(body-expr ...))))]
         [(#%plain-app op-expr arg-expr ...)
          (for-each analyze
                    (syntax->list #'(op-expr arg-expr ...)))]
         [(if test-expr then-expr else-expr)
          (for-each analyze
                    (syntax->list #'(test-expr then-expr else-expr)))]
         [(begin expr ...)
          (for-each analyze (syntax->list #'(expr ...)))]
         [(quote datum)
          (identifier? #'var)
          (let ([binding (identifier-binding #'var)])
            (cond [(eq? binding 'lexical)
                   (hash-set! use=>def #'var
                              (free-id-table-ref binders #'var))]
                  [(eq? binding #f)
                   ;; top-level variable
                  [(list? binding)
                   ;; module variable, imported via 'require'

     ;; get-formals : syntax -> (listof identifier)
     ;; Gets identifiers bound by lambda formals.
     (define (get-formals formals)
       (syntax-case formals ()
         [(id . formals-rest)
          (cons #'id (get-formals #'formals-rest))]
         [() null]
         [id (identifier? #'id) (list #'id)]))

     (analyze stx)

;; Example
 > (define s (expand #'(lambda (x) (if x (+ x 1) 0))))
 > (hash-map (compute-use=>def-table s) list)
'((#<syntax::66 x> #<syntax::54 x>) (#<syntax::61 x> #<syntax::54 x>))

In fully-expanded code, a reference is free-identifier=? to its 
corresponding binding occurrence. So by mapping every binding occurrence 
to itself in the 'binders' table, the table automatically maps any 
reference to its binding occurrence also.

But the 'binders' table doesn't let us enumerate all of the references. 
To do that, we have the 'analyze' function build a hash table; by using 
'eq?' to compare keys we ensure that distinct references are not 
collapsed into one entry, as a free-id-table would do. (We also could 
have the 'analyze' function build a list of the references and return 
that along with the 'binders' table.)

Note that the code above only handles fully-expanded *expressions*, and 
only a few of the expression syntactic forms. For the full grammar see


You don't have to type out the literals list for all of the core 
syntactic forms; see the docs for 'kernel-syntax-case'.

If you recur on the right-hand side of 'define-syntaxes', you need to 
change your comparisons from phase 0 (the default) to phase 1 (which 
means the "transformer environment"). See 'kernel-syntax-case/phase'. 
You'll also need a separate 'binders' table for phase-1 bindings; see 
the optional #:phase argument of 'make-free-id-table'.


Posted on the users mailing list.