syntax-objects / Summer2021

Syntax Parse Bee 2021
11 stars 3 forks source link

Abstracting Syntax Patterns With Parameterized Syntax Classes #23

Open shhyou opened 3 years ago

shhyou commented 3 years ago

Syntax classes offer a powerful mechanism for abstracting over classes of conceptually related patterns. Moreover, syntax classes can be parameterized, with expr/c being the most famous example.

In this example, we define a syntax class that roughly captures the grammar of formal parameters in function headers. It parses the name of the formal parameter, the default expressions of optional arguments and the keywords of keyworded arguments.

The syntax class argument is parameterized over two parameters: whether optional arguments are allowed and whether keyworded arguments are allowed.

#lang racket/base

(require (for-syntax racket/base
                     racket/syntax
                     syntax/parse))

(begin-for-syntax
  ;; We use _splicing_ syntax classes to parse a sequence of elements in the third clause.
  (define-splicing-syntax-class (argument [allow-optional? #t]
                                          #:keyword? [allow-keyword? #t])
    #:attributes (name fresh-var keyword default)
    #:commit
    (pattern name:id
             #:attr keyword #f
             #:attr default #f
             #:with fresh-var (generate-temporary #'name))
    (pattern [name:id default:expr]
             #:fail-unless allow-optional? "optional argument is not allowed"
             #:attr keyword #f
             #:with fresh-var (generate-temporary #'name))
    ;; The ~seq pattern describes a _sequence_ of elements in the enclosing list:
    ;; a keyword followed by an identifier
    (pattern (~seq keyword:keyword name:id)
             #:fail-unless allow-keyword? "keyword argument is not allowed"
             #:attr default #f
             #:with fresh-var (generate-temporary #'name))
    (pattern (~seq keyword:keyword [name:id default:expr])
             #:fail-unless allow-optional? "optional argument is not allowed"
             #:fail-unless allow-keyword? "keyword argument is not allowed"
             #:with fresh-var (generate-temporary #'name))))

Here is an example use of the argument syntax class. It defines a new function definition form that disallows mutation of the arguments.

(require (for-syntax racket/base
                     syntax/parse
                     syntax/transformer))

(define-syntax (define/immutable-parameter stx)
  (syntax-parse stx
    [(_ (name:id arg:argument ...) body:expr ...+)
     ;; The `~@` form _splices_ the keyword, if exists, into the function header.
     ;;
     ;; The `~?` form at (B) chooses the first form if both arg.fresh-var attribute and
     ;; the arg.default attribute have values. Otherwise, `~?` chooses the second form.
     #'(define (name (~@ (~? arg.keyword)                ;; (A)
                         (~? [arg.fresh-var arg.default] ;; (B)
                             arg.fresh-var))
                     ...)
         ;; Disable set! for arg
         (define-syntax arg.name
           (make-variable-like-transformer #'arg.fresh-var #f))
         ...
         body ...)]))

If we want to adjust the parameter of argument and disallow optional arguments or keyword arguments, the colon syntax for specifying the syntax class is not sufficient anymore. As an alternative, we can use either the pattern ~var or the directive #:declare:

(define-syntax (define/immutable-parameter stx)
  (syntax-parse stx
    ;; no optional arguments
    [(_ (name:id (~var arg (argument #f)) ...) body:expr ...+)
     ...

or:

(define-syntax (define/immutable-parameter stx)
  (syntax-parse stx
    [(_ (name:id arg ...) body:expr ...+)
     ;; no keyword arguments
     #:declare arg (argument #:keyword? #f)
     ...

Directly invoking argument with parameters in the colon syntax, unfortunately, results in confusing error messages. For example, this piece of program produces an error message about ellipsis:

;; Example A
(define-syntax (define/immutable-parameter stx)
  (syntax-parse stx
    [(_ (name:id arg:(argument #:keyword? #f) ...) body:expr ...+)
     ...

;; Error message:
stxclasses.rkt:43:21: syntax: no pattern variables before ellipsis in template
  at: (~@ (~? arg.keyword) ......

In another example, the #' part of the output of the macro is highlighted by the syntax error:

;; Example B
(define-syntax (define/immutable-parameter stx)
  (syntax-parse stx
    [(_ (name:id arg:(argument #:keyword? #'stxobj) ...) body:expr ...+)
     #'(define (name (~@ (~? arg.keyword) ;; <- the error points to this #'
         ...

;; Error message:
stxclasses.rkt:43:5: syntax: pattern variable cannot be used outside of a template
  at: syntax
  in: (syntax (define (name (~@ (~? arg.keyword) ......

In these two examples, the colon part and the syntax class "invocation" part are in fact being read as two consecutive patterns by the S-expression reader, instead of a variable pattern with syntax class specification:

;; Example A
(_ (name:id arg: (argument #:keyword? #f) ...) body:expr ...+)
;; Example B
(_ (name:id arg: (argument #:keyword? #'stxobj) ...) body:expr ...+)

Therefore, in example A the bound pattern variables are arg: (without ellipsis) and argument (with an ellipsis), hence the error message about missing pattern variables before ellipses.

Example B is conceptually the same but even more involved. The syntax object #'stxobj is a shorthand for (syntax stxobj). Therefore, the bound pattern variables are arg:, syntax and stxobj. Consequently, the #' in the syntax template is shadowed and highlighted in the error.


Here is an example usage of define/immutable-parameter for testing.

(define fib-cache (make-hash))
(define/immutable-parameter (fib n
                                 [verbose? #f]
                                 #:cache? [cache? #f])
  ;   (set! n 12345) ;=> syntax error
  (when verbose?
    (printf "(fib ~a)n" n))
  (cond
    [(<= n 1) n]
    [(and cache? (hash-has-key? fib-cache n))
     (hash-ref fib-cache n)]
    [else
     (define result
       (+ (fib (- n 1) verbose? #:cache? cache?)
          (fib (- n 2) verbose? #:cache? cache?)))
     (when cache?
       (hash-set! fib-cache n result))
     result]))

(fib 8)
(fib 2 #t)
(fib 12345 #:cache? #t)

Licence

I license the code in this issue under the same MIT License that the Racket language uses and the texts under the Creative Commons Attribution 4.0 International License