skint/misc/syntax-rules.md
2024-07-27 00:14:06 -04:00

10 KiB
Raw Blame History

Syntax-rules extensions

SKINT implements all standard features of R7RS syntax-rules, including custom ellipsis, non-final ellipsis patterns, non-binding underscore pattern, and (... tpl) template escapes. It also supports the following extensions:

Support for boxes

Boxes, as defined by SRFI-111 and the future (scheme box) library, are supported natively, and can be parts of both patterns and templates. See examples of their use below.

Simple pattern escape

A pattern of the form (<ellipsis> <pattern>), where <ellipsis> is the current ellipsis, is interpreted as if it were <pattern>, but ellipses and underscores in <pattern> lose their special meaning, e.g.:

(define-syntax underscored
  (syntax-rules ()
    [(_ (... _) (... ...)) (list (... ...) (... _))]))

(underscored 1 2)
; => (2 1) 

Note that R7RS prescribes special treatment of keyword identifier at the beginning of the pattern in a <syntax rule>: it is matched automatically with the head of the use form, but is not considered a pattern variable. SKINT's pattern escape extension drops this positional restriction, and matches its sub-pattern in a normal way, e.g.:

; in R7RS, x is not a pattern variable here due to its head position:
(let-syntax ([ttt (syntax-rules () [(x y) '(x y)])]) (ttt 123))
; => (x 123)

; x is a pattern variable here, even though it is in the head position:
(let-syntax ([ttt (syntax-rules () [((... x) y) '(x y)])]) (ttt 123))
; => (ttt 123)

; same thing, but with pattern template escaped via template escape to work properly:
((syntax-rules () ; NB: anonymous transformer positioned at the head of the use form
  [(_) (let-syntax ([ttt (syntax-rules () [(((... ...) x) y) '(x y)])]) (ttt 123))]))
; => (ttt 123)

The importance of this feature will be clear when we get to circumventing hygiene part below.

Named pattern escapes

A pattern of the form (<ellipsis> <predicate name> <pattern>) where <ellipsis> is the current ellipsis is interpreted as if it were <pattern>, with additional constraint that the S-expression it matches should also satisfy the constraint specified by <predicate name>. Predicate names are compared to predefined symbols according to free-identifier=? rules. The following named pattern escapes are supported:

  • (... number? <pattern>)
  • (... exact-integer? <pattern>)
  • (... boolean? <pattern>)
  • (... char? <pattern>)
  • (... string? <pattern>)
  • (... bytevector? <pattern>)
  • (... id? <pattern>)

All but the last predicate have the same meaning as the corresponding Scheme procedures. The id? predicate checks if the corresponding S-expression is either a symbol or a syntax object representing an identifier.

The rationale for adding these escapes is obvious: while syntax-rules-based macros can perform very complex calculations with structured S-expressions, they lack an ability to deal with atomic S-expressions (with the exception of identifiers they can be recognized, but the technique for that is quite complicated).

Example (also uses box templates):

(define-syntax wrap-by-type
  (syntax-rules ()
    [(_ (... string? x)) '#&x]
    [(_ (... number? x)) '#(x)]
    [(_ x) 'x]))

(list (wrap-by-type 42) (wrap-by-type "yes") (wrap-by-type #\c)) 
; => (#(42) #&"yes" #\c)

Named template escapes

A template of the form (<ellipsis> <converter name> <template+>) where <ellipsis> is the current ellipsis is interpreted as follows. First, <template+> (which can be any nonempty sequence of <template>s), is instantiated recursively, resulting in a list of S-expressions. These S-expressions become arguments to a converter specified by <converter name>. It is a syntax error to apply converters to a wrong type or number of arguments. Converter names are compared to predefined symbols according to free-identifier=? rules. The following named template escapes are supported:

  • (... number->string <template>)
  • (... string->number <template>)
  • (... list->string <template>)
  • (... string->list <template>)
  • (... list->bytevector <template>)
  • (... bytevector->list <template>)
  • (... length <template>)
  • (... make-list <template> <template>)
  • (... char<=? <template+>)
  • (... <= <template+>)
  • (... + <template+>)
  • (... - <template+>)
  • (... id->string <template>)
  • (... string->id <template> <id template>)

All but the last two converters have the same meaning as the corresponding Scheme procedures. The id->string converter expects either a symbol or a syntax object representing an identifier and produces a string containing a “quote name” of the identifier (the result of applying symbol->string to the original name supplied by the user after all substitutions).

The string->id converter allows one to produce identifiers having the same syntax properties as identifiers explicitly introduced as part of macro definitions or macro uses. The properies are copied from <id template>, which, after all substitutions are performed, should instantiate to an identifier serving as a prototype. The <template> argument should instantiate to a string, which is then converted to a symbol via string->symbol and then turned into an identifier syntax object as if it was introduced side-by-side with the prototype identifier (same expression, same expansion phase).

Please note that identifiers generated with string->id are not autorenamed with other “free” template identifiers; their syntactic identity is defined entirely by that of <id template> id, which might have already being renamed by the time string->id converter is applied.

Examples:

; generated and plain versions of pi and e are syntactically the same
(define-syntax pi-e-example 
  (syntax-rules () 
    [(_)  
     (let ([(... string->id "pi" e) 3.14] [e 2.72])
       (+ pi (... string->id "e" pi)))]))
 
(pi-e-example)
; => 5.86    
(let-syntax 
  ([define-math-constants
    (syntax-rules () 
      [((... ref-id))
       (begin (define (... string->id "pi" ref-id) 3.14)
              (define (... string->id "e" ref-id) 2.72))])])
  (define-math-constants)
  (+ pi e))
; => 5.86 

Note that in the last example the escaped keyword ref-id at the beginning of the pattern was used to bring in the define-math-constants from the macro use to serve as a prototype id for introduced pi and e identifiers, allowing them to capture the corresponding identifier names typed in by the user in (+ pi e). Without simple pattern escape, this keyword would not be treated as a pattern variable.

Here are some more examples:

(define-syntax loop-until-break
  (syntax-rules ()
    [((... ref-id) e ...)
     (call/cc 
       (lambda ((... string->id "break" ref-id))
         (let loop () e ... (loop))))]))

(let ([n 10] [steps 0] [break write])
  (loop-until-break
    ; break here refers to the escape continuation
    (when (= n 4) (break steps))
    (set! n (- n 1))
    (set! steps (+ steps 1))))
; => 6

To demonstrate combined use of different converters, here is a thin macro layer over tagged vectors:

(define-syntax define-variant
  (syntax-rules ()
    [((... ref-id) name () ([field0 index0] ...))
     (begin
       (define-syntax name
         (lambda (field0 ...)
           (vector 'name field0 ...)))
       (define-syntax 
         (... string->id 
           (... string-append (... id->string name) "?") ref-id)
         (lambda (object)
           (and (vector? object)
             (= (vector-length object) (... length (name field0 ...)))
             (eq? (vector-ref object 0) 'name))))
       (define-syntax 
         (... string->id 
           (... string-append (... id->string name) "->" 
             (... id->string field0)) ref-id)
         (lambda (object)
           (vector-ref object index0)))
       ...)]
    [((... ref-id) name (field0 field ...) (pair ...))
     (ref-id name (field ...)  
       (pair ... [field0 (... length (name pair ...))]))]
    [((... ref-id) name (field0 ...))
     (ref-id name (field0 ...) ())]))

(define-syntax variant-case
  (syntax-rules (else)
    [((... ref-id) (a . d) clause ...)
     (let ([var (a . d)]) (ref-id var clause ...))]
    [((... ref-id) var) (error "no variant-case clause matches" var)]
    [((... ref-id) var (else exp1 exp2 ...)) (body exp1 exp2 ...)]
    [((... ref-id) var [name (field ...) exp1 exp2 ...] clause ...)
     (if ((... string->id (... string-append (... id->string name) "?") ref-id) var)
         (let ([field ((... string->id (... string-append (... id->string name) "->" 
                         (... id->string field)) ref-id) 
                       var)] ...)
           exp1 exp2 ...)
         (ref-id var clause ...))]))

(let ()
  (define-variant point (x y))
  (define-variant kons (a d))
  (define-syntax pair->a car)
  (define-syntax pair->d cdr)
  (define (println v)
    (variant-case v ; nb: pair? is already defined
      [point (x y) (format #t "[point x=~s y=~s]~%" x y)]
      [kons (a d) (format #t "[kons a=~s d=~s]~%" a d)]
      [pair (a d) (format #t "[pair a=~s d=~s]~%" a d)]
      [else (format #t "[unknown ~s]~%" v)]))
  (define p (point 1 4))
  (define k (kons 'a 'd))
  (define c (cons 'a 'd))
  (format #t "p = ~s k = ~s c = ~s~%" p k c)
  (println p)
  (println k)
  (println c))

; prints:
; p = #(point 1 4) k = #(kons a d) c = (a . d)
; [point x=1 y=4]
; [kons a=a d=d]
; [pair a=a d=d]

Why stop here?

The above collection of named escapes is selected as almost minimal one. Its purpose is not to make syntax-rules-based macro programming more convenient, but just to extend its core abilities in dealing with non-structural S-expressions, so it is possible to recognize them and work with them via convertion to/from structural form if a need arises. Arithmetics is limited to what one can do using lists as Peano numbers; also, for numbers and chars, access to ordering is provided, to support simple ranges. One can imitate string-append without a dedicated converter, but this unnecessarily complicates generation of identifiers, which is a major use case.