Thursday, April 21, 2005

You asked for it, Felix

Well, I'm not sure if you can quite call it a macro-defining-macro-defining macro, but it's still meta enough to be entertaining. Here's the rough idea. I found I wanted to use a lot of macro keywords when defining an embedded language: the various constructors of the terms of the language are keywords recognized by the macro, which get compiled down to core Scheme. It turned out my compilation would best proceed in a couple of passes, so I borrowed an idea from Sarkar et al's paper at last year's ICFP: I have a define-lang macro that can define either a stand-alone embedded language or an extension to an existing embedded language. The define-lang macro produces:
  1. a static registry of the keywords associated with the language as well as a link to its parent language
  2. a specialized version of syntax-case tailored to the new language -- essentially a wrapper around syntax-case that automatically adds the language's keywords to the literals list
So a user of define-lang can declare a language with its keywords:
(define-lang calc-lang calc-syntax-case (<+> <-> <*> </>))
And this declaration will generate a new calc-syntax-case macro, which can be used to define macros on the calc-lang language:
(define-syntax (compile-calc-program stx)
(calc-syntax-case stx ()
[(<+> e1 e2) ————]
[(<-> e1 e2) ————]
[(<*> e1 e2) ————]
[(</> e1 e2) ————]))
Notice that I didn't have to add the keywords to the literals list of calc-syntax-case, because it adds them already. (I left in the literals list argument to calc-syntax-case in case I need to add more keywords to a particular macro.) Now let's say I want to add an exponentiation operation to the calc-lang, which compiles down to the core calc-lang language. Then we would add the new operation with
(define-lang (extended-calc-lang calc-lang) extended-calc-syntax-case (<pow>))
So here's the approximate definition of define-lang:
(define-for-syntax (lang-keywords lang-name)
———— recursively collect all keywords from static registry ————)

(define-syntax (define-lang stx)
(syntax-case stx ()
[(_ (lang-name parent-name) case (kw ...))
(with-syntax ([(parent-kw ...) (lang-keywords #'parent-name)])
#'(begin
;; register the static information about the language
(define-syntax lang-name
(list #'parent-name
(list #'kw ...)))
;; define the language-specific syntax-case variant
(define-syntax case
(syntax-rules ()
[(_ stx-exp (extra-kw (... ...))
clause1 clause2 (... ...))
(syntax-case stx-exp
(parent-kw ... kw ... extra-kw (... ...))
clause1 clause2 (... ...))]))))]
[(_ lang-name case (kw ...))
#'(begin
;; register the static information about the language
(define-syntax lang-name
(list #f (list #'kw ...)))
;; define the new language-specific syntax-case variant
(define-syntax case
(syntax-rules ()
[(_ stx-exp (extra-kw (... ...))
clause1 clause2 (... ...))
(syntax-case stx-exp (kw ... extra-kw (... ...))
clause1 clause2 (... ...))])))]))
Incidentally, notice that since these macros produce a whole lots of keywords, it's important to do something to reserve the keywords to get decent module-level scoping behavior.

No comments: