On this page:
1.1.3.2Productions
8.18
top
up

1.1Specifying languagesπŸ”— i

This section describes the syntax of the syntax-spec metalanguage, used to describe the grammar, binding structure, and host interface of a DSL.

Language specifications are made via the subforms of syntax-spec , which must be used at module-level.

syntax

( syntax-spec spec-def...)

spec-def = binding-class
| extension-class
| nonterminal
| host-interface

The following subsections address each kind of declaration allowed within the syntax-spec form.

1.1.1Binding classesπŸ”— i

Binding classes distinguish types of binding. When a reference resolves to a binder, it is an error if the binding class declared for the reference position does not match the binding class of the binding position.

syntax

( binding-classidbinding-class-option...)

binding-class-option = #:descriptionstring-literal
| #:binding-spacespace-symbol
| #:reference-compilerreference-compiler-expr

The #:description option provides a user-friendly phrase describing the kind of binding. This description is used in error messages.

The #:binding-space option specifies a binding space to use for all bindings and references declared with this class. Operationally, the binding space declaration causes the syntax-spec expander to add the binding space scope to bindings and references. The scope is added to the scope sets of all binding occurrences. When parsing a reference position declared with a binding class that has an associated binding space, the name that is looked up is augmented with the binding class scope in order to give it access to bindings defined in the space.

See Compiling references to DSL bindings within Racket code for more information about reference compilers

The #:reference-compiler option specifies a reference compiler for controlling how references to variables of this binding class are treated in Racket code.

1.1.2Extension classesπŸ”— i

Extension classes distinguish types of extensions to languages. A syntax transformer is tagged with an extension class using define-dsl-syntax . Nonterminals can be declared extensible by a certain extension class using #:allow-extension. These extensions are expanded away into core DSL forms before compilation.

syntax

( extension-classidextension-class-option)

extension-class-option = #:descriptionstring-literal
| #:binding-spacespace-symbol

The #:description option provides a user-friendly phrase describing the kind of extension. This description is used in error messages.

The #:binding-space option specifies a binding space to use for all extensions with this class.

1.1.3NonterminalsπŸ”— i

syntax

( nonterminalidnonterminal-optionproduction...)

Defines a nonterminal supporting let -like binding structure.

Example:

(binding-classmy-var)
(nonterminalmy-expr
n:number
x:my-var
(my-let([x:my-vare:my-expr]... )body:my-expr)
#:binding(scope(bindx)... body)))

syntax

( nonterminal/nestingid(nested-id)nonterminal-optionproduction...)

Defines a nesting nonterminal supporting nested, let* -like binding structure. Nesting nonterminals may also be used to describe complex binding structures like for match .

Example:

(binding-classmy-var)
(nonterminalmy-expr
n:number
x:my-var
(my-let*(b:binding-pair... )body:my-expr)
#:binding(nestb... body))
(nonterminal/nestingbinding-pair(nested)
[x:my-vare:my-expr]
#:binding(scope(bindx)nested)))

syntax

( nonterminal/exportingidnonterminal-optionproduction...)

Defines an exporting nonterminal which can export bindings, like define and begin .

Example:

(binding-classmy-var)
(nonterminal/exportingmy-defn
(my-definex:my-vare:my-expr)
#:binding(export x)
(my-begind:my-defn... )
#:binding[(re-exportd)... ])
(nonterminalmy-expr
n:number))
1.1.3.1Nonterminal optionsπŸ”— i

nonterminal-option = #:descriptionstring-literal
| #:allow-extensionextension-class-spec
| #:binding-spacespace-symbol
extension-class-spec = extension-class-id
| (extension-class-id...)

The #:description option provides a user-friendly phrase describing the kind of nonterminal. This description is used in error messages.

The #:allow-extension option makes the nonterminal extensible by macros of the given extension class(es).

The #:binding-space option specifies a binding space to use for all bindings and references declared with this nonterminal.

1.1.3.2ProductionsπŸ”— i

production = rewrite-production
| form-production
| syntax-production
rewrite-production =
(~>syntax-pattern
pattern-directive...
body...+)
form-production = (form-id. syntax-spec)maybe-binding-spec
| form-id
syntax-production = syntax-specmaybe-binding-spec
maybe-binding-spec = #:bindingbinding-spec
|

A rewrite production allows certain terms to be re-written into other forms. For example, you might want to tag literals:

(nonterminalpeg
(~>(~or s:strings:chars:numbers:regexp)
#:with#%peg-datum(datum->syntax #'s'#%peg-datum)
#'(#%peg-datums))
... ))

Rewrite productions don’t have binding specs since they declare an expansion of surface syntax into a another DSL form. The don’t necessarily have to expand into a core form like one declared by a form production or a syntax production. A rewrite production can expand into a DSL macro usage or another rewrite production.

Form productions and syntax productions declare core forms in the nonterminal which may have binding specs. If a binding spec is not provided, one is implicitly created. In this case, or if any spec variable is excluded from a binding spec in general, it will be treated as a reference position and implicitly added to the binding spec.

A form production defines a form with the specified name. You may want to use a syntax production if you are re-interpreting racket syntax. For example, if you are implementing your own block macro using syntax-spec:

(nonterminal/exportingblock-form
#:allow-extensionracket-macro
((~literal define-values )(x:racket-var... )e:racket-expr)
#:binding[(export x)... ]
((~literal define-syntaxes )(x:racket-macro... )e:expr)
#:binding(export-syntaxesx... e)
e:racket-expr))

When a form production’s form is used outside of the context of a syntax-spec DSL, like being used directly in Racket, a syntax error is thrown.

1.1.4Syntax specsπŸ”— i

syntax-spec = ()
| keyword
| ...
| ...+
| (~literal idmaybe-space)
| (~datum id)
| (syntax-spec. syntax-spec)
| spec-variable-id:binding-class-id
| spec-variable-id:nonterminal-id
| spec-variable-id:extension-class-id
maybe-space = #:spacespace-name
|

Syntax specs declare the grammar of a DSL form.

  • () specifies an empty list.

  • keyword specifies a keyword like #:key .

  • ... specifies zero or more repetitions of the previous syntax spec.

  • ...+ specifies one or more repetitions of the previous syntax spec.

  • (~literal id maybe-space) specifies a literal identifier and optionally allows the specification of a binding space for the identifier.

  • (~datum id ) specifies a datum literal.

  • (syntax-spec . syntax-spec ) specifies a pair of syntax specs.

  • spec-variable-id:binding-class-id specifies an identifier sub-expression belonging to the specified binding class. For example: x:my-var specifies an identifier of the my-var binding class.

  • spec-variable-id:nonterminal-id specifies a sub-expression that conforms to the specified nonterminal’s grammar. For example: e:my-expr specifies a sub-expression that is a my-expr, where my-expr is a nonterminal name.

  • spec-variable-id:extension-class-id specifies a sub-expression that is treated as a phase-1 transformer expression. This is used when your language has macro definitions.

1.1.5Binding specsπŸ”— i

binding-spec = spec-variable-id
| (bindspec-variable-id)
| (bind-syntaxspec-variable-idspec-variable-id)
| (bind-syntaxesspec-variable-idooo...spec-variable-id)
| (scopespec-or-ooo...)
| [spec-or-ooo...]
| (nestspec-variable-idooo...binding-spec)
| (import spec-variable-id)
| (export spec-variable-id)
| (export-syntaxspec-variable-idspec-variable-id)
| (export-syntaxesspec-variable-idooo...spec-variable-id)
| (re-exportspec-variable-id)
ooo = ...
spec-or-ooo = binding-spec
| ooo

Binding specs declare the binding rules of a DSL’s forms. They allow us to control the scope of bound variables and to check that programs are well-bound before compilation. A binding spec is associated with a production and refers to spec variables from the production.

Similar to syntax patterns and templates, syntax specs and binding specs have a notion of ellipsis depth. However, all spec references in binding specs must have the exact same ellipsis depth as their syntax spec counterparts. Ellipses in binding specs are used to declare the scoping structure of syntax that includes sequences.

  • For a spec-variable-id binding spec, if the specified sub-expression is an identifier (the syntax spec would be something like x:my-var), then the identifier is treated as a reference. If the specified sub-expression is something else (the syntax spec would be something like x:my-expr), then the resulting binding structure depends on the content of the sub-expression. If a sub-expression of a production is excluded from its binding spec it is implicitly added as this type of binding spec.

  • (bindx) declares that the variable specified by x is bound in the current scope. bind must be used inside of a scope and x must be specified with a binding class in its syntax spec like x:my-var.

    Example:
    (binding-classmy-var)
    (nonterminalmy-expr
    n:number
    x:my-var
    (my-let([x:my-vare:my-expr]... )body:my-expr)
    #:binding(scope(bindx)... body)))

    Notice how there are ellipses after the (bindx) since x occurred inside of an ellipsized syntax spec.

  • (bind-syntaxxe) declares that the variable specified by x is bound to the transformer specified by e. bind-syntax must be used inside of a scope, x must be specified with a binding class in its syntax spec like x:my-var, and e must be specified with an extension class in its syntax spec like e:my-macro.

    Example:
    (binding-classmy-var)
    (extension-classmy-macro)
    (nonterminalmy-expr
    #:allow-extensionmy-macro
    n:number
    (my-let-syntax([x:my-vartrans:my-macro])body:my-expr)
    #:binding(scope(bind-syntaxxtrans)body)))

  • bind-syntaxes is similar to bind-syntax, except it binds multiple identifiers.

    Example:
    (binding-classmy-var)
    (extension-classmy-macro)
    (nonterminalmy-expr
    #:allow-extensionmy-macro
    n:number
    (my-let-syntaxes([(x:my-var... )trans:my-macro])body:my-expr)
    #:binding(scope(bind-syntaxesx... trans)body)))
    Here, trans should evaluate to multiple transformers using values .

    Note that the ellipses for x occur inside of the bind-syntaxes.

  • scope declares that bindings and sub-expressions in the sub-specs are in a particular scope. Local bindings binding specs like bind must occur directly in a scope binding spec.

  • [spec... ] is called a group, and it groups binding specs together. For example, when we write
    (my-let([x:my-vare:my-expr])body:my-expr)
    #:binding(scope(bindx)e)
    We could instead write
    (my-let([x:my-vare:my-expr])body:my-expr)
    #:binding[(scope(bindx)body)e]
    Which adds that e is a sub-expression outside of the scope of the let . All un-referenced syntax spec variables get implicitly added to a group with the provided binding spec, so the former example is equivalent to the latter.

    Ellipses can occur after a binding spec in a group.

  • nest is used with nesting nonterminals. In particular, the first argument to nest must be a spec variable associated with a nesting nonterminal. The second argument is treated as the "base case" of the "fold".

    Example:

    (binding-classmy-var)
    (nonterminalmy-expr
    n:number
    x:my-var
    (my-let*(b:binding-pair... )body:my-expr)
    #:binding(nestb... body))
    (nonterminal/nestingbinding-pair(nested)
    [x:my-vare:my-expr]
    #:binding(scope(bindx)nested)))

    The nest binding spec sort of folds over the binding pairs. In this example, it’ll produce binding structure like

    [e1(scope(bindx1)[e2(scope(bindx2)[... [en(scope(bindxn)body)]])])]

    nest does not necessarily have to be used with a sequence.

    Example:
    (binding-classpattern-var)
    (nonterminalclause
    [p:pate:racket-expr]
    #:binding(nestpe))
    (nonterminal/nestingpat(nested)
    x:pattern-var
    #:binding(scope(bindx)nested)
    ((~literal cons )car-pat:patcdr-pat:pat)
    #:binding(nestcar-pat(nestcdr-patnested))))

    However, the first arguemnt of nest cannot have ellipsis depth exceeding one.

  • (import d) imports the bindings exported from the sub-expression specified by d. import must be used inside of a scope and must refer to a syntax spec associated with an exporting nonterminal.

    Example:
    (binding-classmy-var)
    (nonterminal/exportingmy-defn
    (my-definex:my-vare:my-expr)
    #:binding(export x)
    (my-begind:my-defn... )
    #:binding[(re-exportd)... ])
    (nonterminalmy-expr
    n:number
    (my-local[d:my-defn... ]body:my-expr)
    #:binding(scope(import d)... body)))

    The argument to import cannot have ellipsis depth exceeding one.

  • (export x) exports the variable specified by x. x must refer to a syntax spec variable associated with a binding class and export can only be used in an exporting nonterminal. See import for an example of usage.

  • export-syntax is like bind-syntax, except it exports the binding instead of binding the identifier locally to the current scope. Like export , it can only be used in an exporting nonterminal.

  • export-syntaxes is like bind-syntaxes, except it exports the bindings instead of binding the identifiers locally to the current scope. Like export , it can only be used in an exporting nonterminal.

  • (re-exportd) export s all bindings that are exported by d. d must be associated with an exporting nonterminal and re-export can only be used in an exporting nonterminal. See import for an example of usage.

There are several other constraints on binding specs:

  • Specs of different categories cannot occur within the same ellipsis. The categories of specs are:
    • refs+subexps include references, nest, and scope.

    • binds include bind, bind-syntax, and bind-syntaxes.

    • imports include import .

    • exports include export , export-syntax, export-syntaxes, and re-export.

    For example, the spec (scope[(bindx)e]... ) is illegal since it mixes refs+subexps and binds in an ellipsis.

  • binds and imports can only occur within a scope

  • exports cannot occur within a scope.

  • Within a scope, there can be zero or more binds, followed by zero or more imports, followed by zero or more refs+subexps.

  • The second argument to nest must be refs+subexps.

  • Spec variables can be used at most once. For example, (scope(bindx)ee) is illegal.

1.1.6Host interface formsπŸ”— i

Host interface forms are the entry point to the DSL from the host language. They often invoke a compiler macro to translate the DSL forms into Racket expressions.

syntax

( host-interface/expression
(id. syntax-spec)
maybe-binding-spec
pattern-directive...
body...+)

Defines a host interface to be used in expression positions.

Can only be used inside of a syntax-spec block.

An example from the miniKanren DSL:

...
(host-interface/expression
(runn:exprq:term-variableg:goal)
#:binding(scope(bindq)g)
#`(let ([q(var'q)])
(map (reifyq)
(run-goaln(compile-goalg))))))

This defines run, which takes in a Racket expression representing a number, a term variable, and a goal, and invokes the compiler compile-goal to translate the DSL forms into Racket.

syntax

( host-interface/definition
(id. syntax-spec)
maybe-binding-spec
#:lhs
[pattern-directive...
body...+]
#:rhs
[pattern-directive...
body...+])

Defines a host interface to be used in a definition context.

#:lhs runs before the right-hand-sides of definitions in the current context expand and must produce the identifier being defined.

#:rhs runs after the left-hand-sides of definitions and must produce the Racket expression whose value will be bound to the identifier (don’t emit the definition syntax, just the syntax for producing the value).

Can only be used inside of a syntax-spec block.

An example from the miniKanren DSL:

...
(host-interface/definition
(defrel(name:relation-namex:term-variable... )g:goal)
#:binding[(export name)(scope(bindx)... g)]
#:lhs
relation-arity
#'name
(length (syntax->list #'(x... ))))
#'name]
#:rhs
[#`(lambda (x... )
(lambda (s)
(lambda ()
(#%app (compile-goalg)s))))]))

This defines defrel, which defines a relation. In the #:lhs, We record arity information about the identifier before producing it. Since the left-hand-sides all run before the right-hand-sides, even if there is mutual recursion, all arity information will be available before any goals are compiled. Note that the #:rhs produces a lambda expression, not a define .

syntax

( host-interface/definitions
(id. syntax-spec)
maybe-binding-spec
pattern-directive...
body...+)

Defines a host interface to be used in a definition context.

Can be used to produce multiple definitions.

Can only be used inside of a syntax-spec block.

An example from the PEG DSL:

(host-interface/definitions
(define-pegs[name:nontermp:peg]... )
#:binding[(export name)... ]
(run-leftrec-check!(attribute name)(attribute p))
#'(begin (define name(lambda (in)(compile-pegpin)))
... )))

Unlike host-interface/definition, the definitions are directly produced by the host interface.

1.1.7Defining macros for DSLsπŸ”— i

syntax

( define-dsl-syntax nameextension-class-idtransformer-expr)

Defines a macro of the specified extension class. The transformer expression can be any Racket expression that evaluates to a (-> syntax? syntax? ) procedure, so it is possible to use syntax-rules , syntax-case , syntax-parse , etc.

Example:

(define-dsl-syntax conjgoal-macro
[(_ g)#'g]
[(_ g1g2g*... )#'(conj(conj2g1g2)g*... )]))

This defines a macro conj that expands to a goal in miniKanren.

1.1.8Embedding Racket syntaxπŸ”— i

nonterminal

racket-expr

A nonterminal that allows arbitrary host language expressions. Such host expressions are wrapped with #%host-expression during DSL expansion. This nonterminal does not support definitions.

nonterminal

racket-body

A nonterminal that allows arbitrary host language expressions and definitions. This is an exporting nonterminal, so it must be explicitly mentioned in a binding spec, usually with import .

Example:

(nonterminalmy-expr
(my-let([x:racket-vare:racket-expr])body:racket-body...+ )
#:binding(scope(bindx)(import body)... )))

binding class

racket-var

A binding class for host language bindings.

extension class

racket-macro

A binding class for arbitrary host language transformers.

top
up

AltStyle γ«γ‚ˆγ£γ¦ε€‰ζ›γ•γ‚ŒγŸγƒšγƒΌγ‚Έ (->γ‚ͺγƒͺγ‚ΈγƒŠγƒ«) /