On this page:
6.1.1 Example
6.1.1.1 Example improvements
Version: 4.2.1

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

Related patterns:

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

Suppose we want a record system with the following special forms:
  • 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.

First, we’ll define a structure for representing record instances:
  (define-struct record (type fields))
The type field holds the record type descriptor, a value unique to a record type, and the fields field holds the field values.

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.

Here’s the definition of the define-record-type macro:
  ; 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)))]))
We bind two pattern variables, name and field-count. (For brevity, I’ve omitted the error checking code, but the macro should check that name is an identifier and field-count is a literal exact nonnegative integer.) The macro produces two definitions: one for the record type descriptor and one that binds name to the static record type information. The static information consists of a list containing the variable descriptor – the variable name, as an identifier, not the generated symbol that it contains at run time – and the arity (a number).

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.

Let’s write the make macro. We want make to do arity checking, and that information lies in the static record type information. We’ll also use that static information to get the right record type descriptor. Here’s the macro definition:
  ; 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 ...)))]))
The make macro takes as arguments record-type, a name bound as a record type, and the field-value expressions. It looks up the static record type information by calling the syntax-local-value procedure on the identifier bound as a record type. Then it extracts the descriptor variable and the arity from that value, checks the arity, and produces code that contains a reference to the descriptor variable.

The match-record form is another client of record types. Since we want the macro to evaluate the record expression exactly once regardless of how many clauses there are, we introduce a helper macro that does the recursive processing and call it from the main match-record macro on a temporary variable bound to the result of the record expression (see Unresolved pattern tag: "expr-avoid-duplicating"). Here are the macros:
  ; 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.

Here’s some code that uses our record facility:
  (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.

So let’s define a structure for representing static record type information. Since these structures are created and manipulated by macros, we place it in a begin-for-syntax form. (We could also put it in a separate module and require that module for-syntax.)
  (begin-for-syntax
    (define-struct rectype (descriptor-var field-count)
      #:omit-define-syntaxes))
The #:omit-define-syntaxes option is necessary because without it define-struct creates a syntax definition, which is illegal with in begin-for-syntax form.

Here is the change to define-record-type:
  ; 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)))]))
and here is the change to make:
  ; 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)])
             __)))]))
We have defined a local error-reporting procedure, fail, and used it in three levels of checking. First, we check that the syntax in the record-type position is an identifier; second, we extract its value, passing fail to syntax-local-value to call if the identifier is not bound as syntax; and third, we check that the static information bound to the identifier is actually a record type.

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.