[racket] Flatten syntax and runtime phases?

From: Ryan Culpepper (ryan at cs.utah.edu)
Date: Sat Jun 16 01:54:05 EDT 2012

On 06/15/2012 10:41 PM, Nick Sivo wrote:
> Hi,
>
> I have the following code:
>
> #lang racket
> (require mzlib/defmacro)
>
> (defmacro w/uniq (name . body)
>    `(let ([,name (gensym)])
>       , at body))
>
> (defmacro in (x . choices)
>    (w/uniq g
>       `(let ([,g ,x])
>          (or ,@(map (lambda (c) `(eqv? ,g ,c)) choices)))))
>
> Naturally, it fails:
>
> expand: unbound identifier in module (in phase 1, transformer environment) in: g
>
> Additionally defining w/uniq for-syntax fixes it:
>
> #lang racket
> (require mzlib/defmacro
>           (for-syntax racket
>                       mzlib/defmacro))
>
> (begin-for-syntax
>    (defmacro w/uniq (name . body)
>      `(let ([,name (gensym)])
>         , at body)))
>
> (defmacro w/uniq (name . body)
>      `(let ([,name (gensym)])
>         , at body))
>
> (defmacro in (x . choices)
>    (w/uniq g
>       `(let ([,g ,x])
>          (or ,@(map (lambda (c) `(eqv? ,g ,c)) choices)))))
>
> I know this is Racket's phase separation working as intended.  Now, I
> appreciate why Racket separates phases as well as the advantages of
> hygienic macros, but I'm not writing new code.  I'm trying to enable
> existing code* to run natively in Racket and take advantage of its
> great JIT and some module optimizations (like inlining).
>
> Is there any way to hack the first version to work? One option is to
> build a deeper nest of duplicate macro definitions each time I
> encounter a new one.  It seems possible, and it might even work, but
> it sounds awful.  I've already written a custom reader and have an
> extensive library of language syntax/parse macros, so I'm open to
> anything.

I don't know of a way of doing this within Racket other than running 
each definition at every phase that it might be used at. That might not 
require duplicating code, though.

If you could rely on let*-like scoping for Arc module bodies, then you 
could do it entirely with submodules (new feature since 5.2.1; see 
http://blog.racket-lang.org/2012/06/submodules.html for an introduction).

The idea would be to use the arc language's #%module-begin macro to 
transform

(module M arc
   defn1
   defn2
   defn3))

into

(module M arc
  (#%plain-module-begin

   (module S0 arc)

   (module S1 arc
     (require (submod ".." S0)
              (for-syntax (submod ".." S0)))
     defn1
     (provide (all-from-out (submod ".." S0))
              (all-defined-out)))

   (module S2 arc
     (require (submod ".." S1)
              (for-syntax (submod ".." S1)))
     defn2
     (provide (all-from-out (submod ".." S1))
              (all-defined-out)))

   (module S3 arc
     (require (submod ".." S2)
              (for-syntax (submod ".." S2)))
     defn3
     (provide (all-from-out (submod ".." S2))
              (all-defined-out)))

   (require (submod "." S3))
   (provide (all-from-out (submod "." S3)))))

Each S<N> submodule provides (at phase 0 only) the names defined by all 
definitions defn1 through defn<N>. (For S0, that means zero definitions.)

Each S<N+1> submodule requires S<N> at phases 0 and 1, thus it gets all 
definitions defn1 though defn<N> for both run time and compile-time.

That's if you have strict let*-like scoping. If you want to support 
mutually recursive modules, you could try to group contiguous run-time 
definitions together by head-expanding (ie, calling 'local-expand' with 
a stop list) and accumulating forms as long as they produce 
'define-values' forms. I don't see any way to support mutually recursive 
functions that have macro definitions between them, though.

I also don't see any way to support references from a macro body to 
functions defined later in the module.

This will break 'set!', because variables imported from another module 
cannot be mutated. But you can fix that by defining auxiliary setter 
procedures and providing proxy set-transformer macros in place of the 
variables.

Mutating a variable (or data structure) in one phase still cannot change 
the value in other phases, no matter what you do. In fact, compile-time 
mutation won't work very well at all, since each definition is compiled 
in a new submodule, which should get its own fresh compile-time state.

If you have 'let-syntax' or its equivalent (eg, local 'define-syntax'), 
then phases 0 and 1 might not be enough, since you can have expressions 
at phase 2 (and higher):

   (let-syntax ([m0
                 (let-syntax ([m1 <phase-2-expr>]) <phase-1-expr>)])
     <phase-0-expr>)

If you want to be really *really* crafty, you could try defining your 
own version of 'let-syntax', 'define-syntax', etc to create requires for 
higher phases on demand by calling 'syntax-local-lift-require'. That is,

   (arc-define-syntax id expr)

   == expands to the result of calling ==>

   (syntax-local-lift-require
    #`(for-meta #,(add1 (syntax-local-phase-level)) S<N>)
    #'(racket-define-syntax id expr))

I hope that helps. (I haven't tried any of this, so there are surely 
bugs that need ironing out.)

Ryan


Posted on the users mailing list.