6.1 Static Information
Synopsis: Enables communication between macros by binding compile-time information to a name. The name is used as the communication channel.
Examples: define-struct and match; define-signature and unit; define-match-expander and match; define-require-syntax and require; define-provide-syntax and provide
This pattern of communication is useful when the relevant information can conceptually be attached to a particular name. It involves definition forms that bind static information to names and client forms that use the name to retrieve the static information to use in their transformations.
For example, the define-struct macro binds the struct name to static information containing identifiers for the struct’s super-struct name, constructor, predicate, accessors, mutators, and struct descriptor (see scheme/struct-info for more details). This data is consumed by other macros that offer special handling for structs, like match and the struct-out subform of provide.
The use of binding as the communication mechanism has some nice properties. The names that carry the static info are subect to normal scoping rules. They can be imported and exported from modules (with or without renaming). They can be shadowed; in that case, the information is properly hidden, not available through the shadowing binding. If the client macros are written to the correct protocols, identifiers carrying static information are correctly decorated with binding arrows in Check Syntax.
Static information is bound to a name using define-syntax and retrieved using syntax-local-value.
6.1.1 Example
A define-record-type form for defining new record types distinct from other record types. When we define a record type, we give it a fixed arity (number of fields).
A make form for creating record instances that statically checks the number of field arguments against the declared arity of the record type.
A match-record form for doing case analysis on records that statically checks the number of variables in a pattern against the declared arity of the record type.
Now let’s define the define-record-type macro. A record type definition includes a name and a literal number specifying the record type’s arity. The record type definition binds the name as a record type, and the binding is as real as a function binding, macro binding, structure name binding, etc. We use the phrases “bound as a record type” and “bound to static record type information” to describe an identifier that occurs in the binding position of a define-record-type form.
; syntax (define-record-type record-type-id field-count-number) |
(define-syntax (define-record-type stx) |
(syntax-case stx () |
[(define-record-type name field-count) |
#'(begin (define record-type (gensym)) |
(define-syntax name |
(list ((syntax-local-certifier) #'record-type) |
'field-count)))])) |
It is necessary to apply the local certifier (the result of (syntax-local-certifier) to the descriptor identifier because the identifier will be used as a reference in code produced by other macros and the variable it refers to is private (not provided) to the module where the record type definition occurs. See Certifying References in Static Information.
Why can we just use record-type for the name we bind to the record type descriptor? Won’t there be conflicts if we use define-record-type twice? Don’t we need to generate a fresh name? No: hygiene causes the record-type bindings created by different uses of define-record-type to be different. We could explicitly create distinct names using generate-temporaries if we wanted, but we don’t need to in this case. See Fresh names from hygiene.
Why quote the arity number? See Quote Literals Used as Expressions.
; syntax (make record-type field-expr ...) |
(define-syntax (make stx) |
(syntax-case stx () |
[(make record-type field-value ...) |
(let* ([record-type-info (syntax-local-value #'record-type)] |
[record-type-id (car record-type-info)] |
[field-count (cadr record-type-info)]) |
(unless (= (length (syntax->list #'(field-value ...))) |
field-count) |
(raise-syntax-error |
#f |
(format "wrong number of fields, expected ~s" |
field-count) |
stx)) |
#`(make-record #,record-type-id |
(list field-value ...)))])) |
; syntax (match-record record-expr clause ...) |
(define-syntax-rule (match-record r-expr . clauses) |
(let ([r r-expr]) |
(match-record* r . clauses))) |
(define-syntax (match-record* stx) |
(syntax-case stx (else) |
[(match-record* r) |
#'(error 'match-record "match failed for ~e" r)] |
[(match-record* r [else expr]) |
#'expr] |
[(match-record* r [(name var ...) expr] . clauses) |
(let* ([record-type-info (syntax-local-value #'name)] |
[type-id (car record-type-info)] |
[field-count (cadr record-type-info)]) |
(unless (= (length (syntax->list #'(var ...))) |
field-count) |
(raise-syntax-error |
#f |
(format "wrong number of fields, expected ~s" |
field-count) |
stx)) |
#`(if (eq? (record-type r) #,type-id) |
(apply (lambda (var ...) expr) (record-fields r)) |
(match-record* r . clauses)))])) |
Like make, the match-record* macro uses syntax-local-value to retrieve the static record type information. The rest of the macro is straightforward.
(define-record-type pair 2) |
(define-record-type triple 3) |
(make triple 1 2 3) |
(match-record (make pair 11 12) |
[(pair x y) (* x y)] |
[(triple x y z) 'no-thanks]) |
6.1.1.1 Example improvements
The code above represents static record information as a list of two elements, an identifier and a number. While representation is adequate for illustrating how static information works, it is a poor choice in practice for the same reason that using lists to represent structured datatypes is generally a poor idea.
In particular, this leads to bad syntax error behavior. Record information should be distinct from struct information and signature information and any other sort of static information that might occur in a program. A macro that expects one kind of static information should raise a syntax error when it receives another kind. It helps if each kind of static information is represented by a distinct structure type.
(begin-for-syntax |
(define-struct rectype (descriptor-var field-count) |
#:omit-define-syntaxes)) |
; syntax (define-record-type record-type-id field-count-number) |
(define-syntax (define-record-type stx) |
(syntax-case stx () |
[(define-record-type name field-count) |
#'(begin (define record-type (gensym)) |
(define-syntax name |
(make-rectype |
((syntax-local-certifier) #'record-type) |
'field-count)))])) |
; syntax (make record-type field-expr ...) |
(define-syntax (make stx) |
(syntax-case stx () |
[(make record-type field-value ...) |
(let ([fail |
(lambda () |
(raise-syntax-error |
#f |
"not a name bound as a record type" |
stx record-type-id))]) |
(unless (identifier? #'record-type) (fail)) |
(let ([record-type-info |
(syntax-local-value #'record-type fail)]) |
(unless (rectype? record-type-info) (fail)) |
(let ([record-type-id (car record-type-info)] |
[field-count (cadr record-type-info)]) |
__)))])) |
The rest of the macro is the same. The change in match-record* is similar.
See Static Information With Behavior for another improvement in error behavior.