Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Abstracting Syntax Patterns With Parameterized Syntax Classes #23

Open
shhyou opened this issue Sep 2, 2021 · 0 comments
Open

Abstracting Syntax Patterns With Parameterized Syntax Classes #23

shhyou opened this issue Sep 2, 2021 · 0 comments

Comments

@shhyou
Copy link

shhyou commented Sep 2, 2021

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

@shhyou shhyou changed the title Syntax Errors in Parameterized Syntax Classes Abstracting Syntax Patterns With Parameterized Syntax Classes Sep 3, 2021
bennn added a commit to syntax-objects/syntax-parse-example that referenced this issue Sep 21, 2021
bennn added a commit to syntax-objects/syntax-parse-example that referenced this issue Oct 27, 2021
bennn added a commit to syntax-objects/syntax-parse-example that referenced this issue Oct 27, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant