Syntax Parse Examples
Source code: https://github.com/bennn/syntax-parse-example
1 How to browse the examples
Scroll through this document, read the macros’ source code and look at the example uses of each macro.
The source code for each macro is in a top-level folder at https://github.com/bennn/syntax-parse-example. For example, the source for a macro named big-mac would be in the folder https://github.com/bennn/syntax-parse-example/big-mac.
2 How to use the examples in another project
Copy/paste the example code into a new file in your project, require that new file normally.
Install the syntax-parse-example package, then require the macro’s defining module. For example, the defining module for the first-class-or macro is "syntax-parse-example/first-class-or/first-class-or".
Clone the source code, then require the module path of the file that defines the macro.
3 How to contribute a new example
Clone this repository (link).
- Run raco syntax-parse-example --new EUGENE in the top-level folder of the cloned repository. This generates three new files:
EUGENE/EUGENE.rkt (source code)
EUGENE/EUGENE-test.rkt (tests)
EUGENE/EUGENE-doc.scrbl (Scribble documentation)
Fill the holes in the newly-generated files with an implementation, some unit tests, and documentation.
Run raco setup syntax-parse-example to generate the documentation.
4 A syntax-parse Crash Course
The syntax-parse form is a tool for un-packing data from a syntax object. It is similar to Racket’s match. Since the input to a macro is always a syntax object, syntax-parse is helpful for writing macros.
A syntax object is a Racket representation of source code. For example, #'(+ 1 2) is a syntax object that represents the sequence of characters (+ 1 2), along with the information that the + identifier is bound to a function in the racket/base library.
A macro is a compile-time function on syntax objects. In other words, a macro: (1) is a function, (2) expects a syntax object as input, (3) returns a new syntax object, and (4) runs at compile-time (see Expansion).
The name K is historic (link) and pretentious, enjoy.
> (require (for-syntax racket/base))
> (define-syntax (K args-stx) (define args (syntax-e args-stx)) (if (= (length args) 3) (cadr args) (raise-argument-error 'K "syntax object containing a list with 3 elements" args-stx))) > (K 1 2) 1
> (K 1) K: contract violation
expected: syntax object containing a list with 3 elements
given: #<syntax:eval:4:0 (K 1)>
> (K 1 2 3) K: contract violation
expected: syntax object containing a list with 3 elements
given: #<syntax:eval:5:0 (K 1 2 3)>
Here is the same macro, defined using syntax-parse instead of the low-level syntax-e and cadr functions:
> (require (for-syntax racket/base syntax/parse))
> (define-syntax (K args-stx) (syntax-parse args-stx [(_ ?arg0 ?arg1) #'?arg0])) > (K 1 2) 1
> (K 1) eval:4.0: K: expected more terms starting with any term
at: ()
within: (K 1)
in: (K 1)
> (K 1 2 3) eval:5.0: K: unexpected term
at: 3
in: (K 1 2 3)
I don’t expect that all this makes sense so far. Try running and modifying these examples. Try reading the documentation for define-syntax and syntax-e and syntax-parse and syntax (aka #').
the parentheses say this pattern matches a (special kind of) list,
the underscore (_) means the first element of the list can be anything,
the name ?arg0 means the second element of the list can be anything and gets bound to the pattern variable ?arg0,
the name ?arg1 binds the third element to another pattern variable,
and if the list has more or fewer elements the pattern does not match.
A pattern variable is a special kind of variable; it can only be referenced inside
a new syntax object.
The name ?arg0 starts with a ? as a style choice —
5 The Examples
5.1 first-class-or
(require syntax-parse-example/first-class-or/first-class-or) | |
package: syntax-parse-example |
syntax
(first-class-or expr ...)
See also Combining Tests: and and or
Identifier macros can be evaluated as identifiers.
expand like Racket’s or when called like a function, and
expand to a function definition when used like an identifier.
> (first-class-or #false #true) #t
> (apply first-class-or '(#false #true 0)) #t
> (first-class-or (+ 2 3) (let loop () (loop))) 5
> (map first-class-or '(9 #false 3) '(8 #false #false)) '(9 #f 3)
The macro:
#lang racket/base |
(provide first-class-or) |
(require (for-syntax racket/base syntax/parse)) |
(define-syntax (first-class-or stx) |
(syntax-parse stx |
[(_) |
#'#false] |
[(_ ?a . ?b) |
#'(let ([a-val ?a]) |
(if a-val a-val (first-class-or . ?b)))] |
[_:id |
#'(lambda arg* |
(let loop ([arg* arg*]) |
(cond |
[(null? arg*) |
#false] |
[(car arg*) |
(car arg*)] |
[else |
(loop (cdr arg*))])))])) |
The first two syntax/parse clauses define what happens when or is called like a function.
The pattern _:id matches any identifier.
The dot notation in (_ ?a . ?b) could be (_ ?a ?b ...) instead. See Pairs, Lists, and Racket Syntax for intuition about what the dot means, and Syntax Patterns for what it means in a syntax pattern.
5.2 optional-assert
(require syntax-parse-example/optional-assert/optional-assert) | |
package: syntax-parse-example |
syntax
(optional-assert expr ...)
a test that evaluates an expression and halts the program if the result is #f,
or nothing
#lang racket/base |
(provide optional-assert exn:fail:optional-assert?) |
(require (for-syntax racket/base syntax/parse)) |
(define-for-syntax no-asserts? (getenv "DISABLE_OPTIONAL_ASSERT")) |
(struct exn:fail:optional-assert exn:fail ()) |
(define (make-exn:fail:optional-assert datum) |
(exn:fail:optional-assert (format "assertion failure: ~a" datum) |
(current-continuation-marks))) |
(define-syntax (optional-assert stx) |
(syntax-parse stx |
[(_ e:expr) |
(if no-asserts? |
#'(void) |
#'(unless e |
(raise (make-exn:fail:optional-assert 'e))))])) |
procedure
x : any/c
5.3 Cross Macro Communication
(require syntax-parse-example/cross-macro-communication/cross-macro-communication) | |
package: syntax-parse-example |
syntax
(define-for-macros id expr)
syntax
(get-macroed id)
> (define-for-macros cake 42) > (get-macroed cake) 42
> cake eval:3:0: cake: illegal use of syntax
in: cake
value at phase 1: 42
This communication works even if the identifiers are defined and used in different files or modules:
> (module the-definition racket (require syntax-parse-example/cross-macro-communication/cross-macro-communication) (define-for-macros shake 54) (provide shake))
> (module the-use racket (require 'the-definition syntax-parse-example/cross-macro-communication/cross-macro-communication) (get-macroed shake)) > (require 'the-use) 54
The following is the source code for define-for-macros and get-macroed:
#lang racket/base |
(provide define-for-macros get-macroed) |
(require (for-syntax racket/base syntax/parse)) |
(define-syntax (define-for-macros stx) |
(syntax-parse stx |
[(_ name:id expr) |
#'(define-syntax name expr)])) |
(define-syntax (get-macroed stx) |
(syntax-parse stx |
[(_ name:id) |
#`(#%datum . #,(syntax-local-value #'name))])) |
In define-for-macros, the macro simply binds a new value at compile time using define-syntax. In this example define-for-macros is mostly synonymous with define-syntax, but it demonstrates that the name could be changed (to say add a question mark at the end), and the given expression can be changed. The get-macroed form simply takes the compile time value and puts it in the run time module. If name is used outside of a macro then a syntax error is raised.
The point of #%datum is to make it seem like a value was part of the source code. See Expansion Steps for details.
5.4 let-star
(require syntax-parse-example/let-star/let-star) | |
package: syntax-parse-example |
syntax
(let-star ((id expr) ...) expr)
(let* ([a 1] [b (+ a 1)]) b)
behaves the same as a nested let:
(let ([a 1]) (let ([b (+ a 1)]) b))
The let-star macro implements let* in terms of let.
#lang racket/base |
(provide let-star) |
(require (for-syntax racket/base syntax/parse)) |
(define-syntax (let-star stx) |
(syntax-parse stx |
[(_ ([x:id v:expr]) body* ...+) |
#'(let ([x v]) |
body* ...)] |
[(_ ([x:id v:expr] . bind*) body* ...+) |
#'(let ([x v]) |
(let-star bind* body* ...))] |
[(_ bad-binding body* ...+) |
(raise-syntax-error 'let-star |
"not a sequence of identifier--expression pairs" stx #'bad-binding)] |
[(_ (bind*)) |
(raise-syntax-error 'let-star |
"missing body" stx)])) |
> (let* 1) eval:1:0: let*: bad syntax (missing body)
in: (let* 1)
> (let* ([a 1])) eval:2:0: let*: bad syntax (missing body)
in: (let* ((a 1)))
> (let* ([a 1] [b (+ a 1)] [c (+ b 1)]) c) 3
5.5 def
(require syntax-parse-example/def/def) | |
package: syntax-parse-example |
syntax
(def (id arg-spec ...) doc-contract-tests ... expr ...)
> (module snoc racket/base (require syntax-parse-example/def/def) (def (snoc (x* : list?) x) "Append the value `x` to the end of the given list" #:test [ ((snoc '(1 2) 3) ==> '(1 2 3)) ((snoc '(a b) '(c)) ==> '(a b (c)))] (append x* (list x))) (provide snoc)) > (require 'snoc) > (snoc 1 '(2 3)) snoc: contract violation
expected: list?
given: 1
> (snoc '(1 2) 3) '(1 2 3)
requires a docstring
requires test cases;
optionally accepts contract annotations on its arguments; and
optionally accepts pre- and post- conditions.
> (module gcd racket/base (require syntax-parse-example/def/def) (def (gcd (x : integer?) (y : integer?)) "greatest common divisor" #:pre [ (>= "First argument must be greater-or-equal than second")] #:test [ ((gcd 10 3) ==> 1) ((gcd 12 3) ==> 3)] (cond [(zero? y) x] [else (gcd y (- x (* y (quotient x y))))])) (provide gcd)) > (require 'gcd) > (gcd 42 777) gcd: First argument must be greater-or-equal than second
> (gcd 777 42) 21
If the docstring or test cases are missing, def throws a syntax error.
> (def (f x) x) eval:9.0: def: expected string or expected one of these
literals: #:test, #:pre, or #:post
at: x
in: (def (f x) x)
> (def (f x) "identity" x) eval:10.0: def: expected string or expected one of these
literals: #:test, #:pre, or #:post
at: x
in: (def (f x) "identity" x)
> (def (f x) #:test [((f 1) ==> 1)] x) eval:11.0: def: expected string or expected one of these
literals: #:test, #:pre, or #:post
at: x
in: (def (f x) #:test (((f 1) ==> 1)) x)
The begin-for-syntax defines two syntax classes (see Syntax Classes). The first syntax class, arg-spec, captures arguments with an optional contract annotation. The second, doc-spec, captures docstrings.
The large ~or pattern captures the required-and-optional stuff that def accepts: the docstring, the #:test test cases, the #:pre pre-conditions, and the #:post post-conditions.
The four #:with clauses build syntax objects that run unit tests and/or checks.
The syntax object made from the #:test clause creates a post-submodule (module+ test ....) and uses parameterize to capture everything that the tests print to current-output-port.
The examples in the docs for the ~optional pattern help explain (1) why #'#f can be a useful #:default and (2) when it is necessary to specify the ellipses depth in a #:default, as in (check-pre* 1).
The def macro:
#lang racket/base |
(provide def) |
(require rackunit (for-syntax racket/base syntax/parse)) |
(begin-for-syntax |
(define-syntax-class arg-spec |
#:attributes (name type) |
#:datum-literals (:) |
(pattern |
(name:id : type:expr)) |
(pattern |
name:id |
#:with type #'#f)) |
(define-syntax-class doc-spec |
(pattern |
e:str)) |
) |
(define-syntax (def stx) |
(syntax-parse stx #:datum-literals (==>) |
[(_ (name:id arg*:arg-spec ...) |
(~or ;; using (~or (~once a) ...) to simulate an unordered (~seq a ...) |
(~once (~describe #:role "docstring" "docstring" doc:doc-spec)) |
(~once (~seq #:test ((in* ==> out* |
(~optional (~seq #:stdout expected-output*:str) |
#:defaults ([expected-output* #'#f]))) |
...))) |
(~once (~optional (~seq #:pre ([check-pre* pre-doc*:doc-spec] ...)) |
#:defaults ([(check-pre* 1) '()] [(pre-doc* 1) '()]))) |
(~once (~optional (~seq #:post ([check-post* post-doc*:doc-spec] ...)) |
#:defaults ([(check-post* 1) '()] [(post-doc* 1) '()])))) ... |
body) |
#:with check-types |
#'(for ([arg-name (in-list (list arg*.name ...))] |
[arg-type (in-list (list arg*.type ...))] |
[i (in-naturals)] |
#:when arg-type) |
(unless (arg-type arg-name) |
(raise-argument-error 'name (symbol->string (object-name arg-type)) i arg-name))) |
#:with check-pre |
#'(for ([pre-check (in-list (list check-pre* ...))] |
[pre-doc (in-list (list pre-doc* ...))]) |
(unless (pre-check arg*.name ...) |
(raise-user-error 'name pre-doc))) |
#:with check-post |
#'(lambda (result) |
(for ([post-check (in-list (list check-post* ...))] |
[post-doc (in-list (list post-doc* ...))]) |
(unless (post-check result) |
(error 'name post-doc)))) |
#:with test-cases |
#'(module+ test |
(let* ([p (open-output-string)] |
[result-val (parameterize ([current-output-port p]) in*)] |
[result-str (begin0 (get-output-string p) |
(close-output-port p))]) |
(check-equal? result-val out*) |
(when expected-output* |
(check-equal? result-str expected-output*))) |
...) |
#'(begin |
test-cases |
(define (name arg*.name ...) |
check-types |
check-pre |
(let ([result body]) |
(begin0 result |
(check-post result)))))])) |
This macro gives poor error messages when the docstring or test cases are missing.
The doc-spec syntax class could be extended to accept Scribble, or another kind of docstring syntax.
A #:test case may optionally use the #:stdout keyword. If given, the test will fail unless running the test prints the same string to current-output-port.
5.6 conditional-require
(require syntax-parse-example/conditional-require/conditional-require) | |
package: syntax-parse-example |
syntax
(conditional-require expr id id)
#lang racket/base |
(provide conditional-require) |
(require (for-syntax racket/base syntax/parse)) |
(begin-for-syntax |
(define-syntax-class mod-name |
(pattern _:id) |
(pattern _:str))) |
(define-syntax (conditional-require stx) |
(syntax-parse stx |
[(_ test:boolean r1:mod-name r2:mod-name) |
(if (syntax-e #'test) |
#'(require r1) |
#'(require r2))])) |
The syntax class mod-name matches syntactic strings or identifiers. This doesn’t guarantee that the second and third argument to conditional-require are valid module paths, but it rules out nonsense like (conditional-require #true (+ 2 2) 91).
The test could be more interesting. It could branch on the value of current-command-line-arguments, or do a case based on system-type.
5.7 multi-check-true
(require syntax-parse-example/multi-check-true/multi-check-true) | |
package: syntax-parse-example |
syntax
(multi-check-true expr ...)
(multi-check-true #true #false (even? 0))
expands to code that behaves the same as:
(check-true #true) (check-true #false) (check-true (even? 0))
The main difference between the macro and the example is that the macro uses with-check-info* to improve test failure messages. If part of a multi-check-true fails, the error message points to the bad expression (rather than the multi-check-true macro).
#lang racket/base |
(provide multi-check-true) |
(require rackunit (for-syntax racket/base syntax/srcloc syntax/parse)) |
(define-syntax (multi-check-true stx) |
(syntax-parse stx |
[(_ e* ...) |
#`(begin |
#,@(for/list ([e (in-list (syntax-e #'(e* ...)))]) |
(define loc (build-source-location-list e)) |
#`(with-check-info* (list (make-check-location '#,loc)) |
(λ () (check-true #,e)))))])) |
5.8 define-datum-literal-set
(require syntax-parse-example/define-datum-literal-set/define-datum-literal-set) | |
package: syntax-parse-example |
syntax
(define-datum-literal-set id (id ...))
Given a sequence of symbols, the define-datum-literal-set macro builds a syntax class that matches these symbols.
(define-datum-literal-set C-keyword (auto break case char const continue default do double else)) (define-syntax (is-C-keyword? stx) (syntax-parse stx [(_ x:C-keyword) #'#true] [(_ x) #'#false])) (is-C-keyword? else) (is-C-keyword? synchronized)
The macro works by defining a literal set and then a syntax class.
#lang racket/base |
(provide define-datum-literal-set) |
(require (for-syntax racket/base racket/syntax syntax/parse)) |
(define-syntax (define-datum-literal-set stx) |
(syntax-parse stx |
[(_ cls-name:id (lit*:id ...)) |
#:with set-name (format-id stx "~a-set" (syntax-e #'cls-name)) |
#'(begin-for-syntax |
(define-literal-set set-name |
#:datum-literals (lit* ...) |
()) |
(define-syntax-class cls-name |
#:literal-sets ([set-name]) |
(pattern (~or lit* ...)))) ])) |
5.9 rec/c
(require syntax-parse-example/rec-contract/rec-contract) | |
package: syntax-parse-example |
syntax
(rec/c id expr)
#lang racket/base |
(provide rec/c) |
(require racket/contract (for-syntax racket/base syntax/parse)) |
(define-syntax-rule (rec/c t ctc) |
(letrec ([rec-ctc |
(let-syntax ([t (syntax-parser (_:id #'(recursive-contract rec-ctc)))]) |
ctc)]) |
rec-ctc)) |
> (define/contract (deep n) (-> integer? (rec/c t (or/c integer? (list/c t)))) (if (zero? n) n (list (deep (- n 1))))) > (deep 4) '((((0))))
5.10 struct-list
(require syntax-parse-example/struct-list/struct-list) | |
package: syntax-parse-example |
syntax
(struct-list expr ...)
> (struct-list foo ([a : String] [b : String]) #:type-name Foo) > (define f (foo "hello" "world")) > (foo? f) - : Boolean
#t
> (string-append (foo-a f) " " (foo-b f)) - : String
"hello world"
> (ann f Foo) - : Foo
'(foo "hello" "world")
extracts the names and type names from the syntax,
creates an identifier for the predicate and a sequence of identifiers for the accessors (see the #:with clauses),
and defines a constructor and predicate and accessor(s).
#lang typed/racket/base |
(provide struct-list) |
(require (for-syntax racket/base racket/syntax syntax/parse)) |
(define-syntax (struct-list stx) |
(syntax-parse stx #:datum-literals (:) |
[(_ name:id ([f*:id : t*] ...) #:type-name Name:id) |
#:fail-when (free-identifier=? #'name #'Name) |
"struct name and #:type-name must be different" |
#:with name? |
(format-id stx "~a?" (syntax-e #'name)) |
#:with ((name-f* i*) ...) |
(for/list ([f (in-list (syntax-e #'(f* ...)))] |
[i (in-naturals 1)]) |
(list (format-id stx "~a-~a" (syntax-e #'name) (syntax-e f)) i)) |
(syntax/loc stx |
(begin |
(define-type Name (Pairof 'name (Listof Any))) |
(define (name (f* : t*) ...) : Name |
(list 'name f* ...)) |
(define (name? (v : Any)) : Boolean |
(and (list? v) (not (null? v)) (eq? 'name (car v)))) |
(define (name-f* (p : Name)) : t* |
(cast (list-ref p 'i*) t*)) |
...))])) |
6 Example-Formatting Tools
uses the reader from scribble/base; and
provides a few utility functions, documented below.
procedure
filename : path-string?