ee-lib: Library Support for DSL Macro Expanders
Michael Ballantyne <michael.ballantyne@gmail.com>
This library provides a higher-level API to Racket’s syntax system, designed for implementing macro expanders for DSLs. The paper "Macros for Domain-Specific Languages" serves as the guide-level explanation of the library and associated programming patterns. This page provides reference documentation.
1 Scope, binding, and hygiene operations for DSL expanders
(require ee-lib) | package: ee-lib |
Import at phase 1.
1.1 Scope
Intuitively, a scope is a region of program text in which certain bindings are available, determined by scoping forms such as let or block. In the context of macro expansion, syntax may be moved in and out of scopes during the process of expansion, and some scopes aren’t immediately evident in the source program text. The Racket expander uses several kinds of scope values to represent regions of partially expanded programs to implement macro hygiene. This documentation refers to these scope values as scope tags, and reserves the word "scope" for the intuitive notion.
syntax
(with-scope id body ...)
The two scope tags are encapsulated in a scope tagger value accessible via id. The new binding context segment is added to the library local binding context for the dynamic extent of the evaluation of the body forms.
procedure
(splice-from-scope id tagger) → syntax?
id : identifier? tagger : scope-tagger?
Useful for implementing splicing forms like splicing-let where certain bindings that initially appear to be within a scope in fact splice outside of it.
procedure
(syntax-local-introduce-splice stx) → syntax?
stx : syntax?
Useful when moving syntax out of the context of a given macro expansion, as when lifting a definition to a surrounding context.
1.2 Binding
This library implicitly maintains a library local binding context with entries that map bindings to values, similar to the core expander’s local binding context. See current-def-ctx for a way to access the library local binding context as a first-class definition context that can be used with the core expander’s API.
The library local binding context consists of nested binding context segments corresponding to the nested dynamic extents of with-scope uses. The bind! operation adds new entries to the innermost segment. The binding context may be sealed, preventing further bindings within the context until a new segment is added. All operations that use syntax to create or lookup bindings in the binding context first annotate the syntax with the inside-edge scope tag for the scope corresponding to the innermost binding context segment.
procedure
(bind! id v) → identifier?
id : identifier? v : any/c (bind! ids vs) → (listof identifier?) ids : (listof identifier?) vs : (listof any/c)
This operation is legal only in a library local binding context with an unsealed segment.
The second form works like the first, but for lists of corresponding ids and vs.
procedure
id : identifier? predicate : (-> any/c (or/c #f any/c))
1.3 Hygiene for expander definitions
As an expander traverses syntax, it needs to enter new expansion contexts and adjust the library local binding context. And hygiene should treat syntax constructed introduced by templates in the expander similarly to syntax introduced by a macro. The following forms define expand functions with these behaviors.
syntax
(define/hygienic (id arg ...) ctx-type body ...)
arg = id ctx-type = #:expression | #:definition
Invocations of the function are hygienic in the same way macro applications are hygienic: for syntax-valued arguments and returns, arguments are tagged by a fresh use-site scope tag, and syntax returned from the function that was not part of one of the arguments is tagged with a fresh macro-introduction scope tag.
The expansion context type determines the treatment of use-site scope tags at uses of bind, syntax-local-identfier-as-binding, and syntax-local-introduce-splice within. During expansion in an internal-definition context, the expansion context tracks a set of use-site scopes created during expansion of the context. The operations just mentioned remove use-site scopes present in that set. Entering an expression context resets the set to empty.
syntax
(define/hygienic-metafunction (id id ...) ctx-type body ...)
1.4 Hygiene for macro application
Expanders also need to apply macro hygiene when invoking macros, via apply-as-transformer. Macros should expand to forms such as define rather than directly extending the binding context with bind!, so apply-as-transformer seals the binding context.
procedure
(apply-as-transformer proc binding-id ctx-type arg ...) → any proc : procedure? binding-id : (or/c identifier? #f) ctx-type : (or/c 'expression 'definition) arg : any/c
The binding-id argument specifies a binding associated with the proc, which the expander uses to determine whether to add use-site scopes and which code inspector to use during expansion.
Changed in version 1.0 of package ee-lib: Added the binding-id argument.
1.5 Transformer evaluation
procedure
(eval-transformer stx) → any/c
stx : syntax?
Useful for implementing forms like let-syntax for a DSL.
1.6 Integrating with Racket’s expander
When a DSL’s syntax has Racket subexpression positions, the DSL expander needs to call the Racket expander via local-expand. The following operations help connect the Racket expansion with the library-managed binding context.
procedure
procedure
procedure
procedure
(racket-var? v) → boolean?
v : any/c
1.7 Disappeared uses and bindings
For DrRacket to provide behaviors such as binding arrows, it needs to know which identifiers act as bindings and references. In the case of DSL code these identifiers may not be present as bindings and references in the program’s compilation to Racket. The bind! and lookup operations automatically record such disappeared uses and disappeared bindings.
DrRacket looks for information about disappeared uses and bindings on syntax in fully-expanded modules (see Syntax Properties that Check Syntax Looks For). This library inserts extra syntax without runtime meaning into the expanded module to carry this information.
2 Defining literals
(require ee-lib/define) | package: ee-lib |
Import at phase 0.
syntax
(define-literal-forms literal-set-id message (form-id ...))