Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How can I group optional attributes captured with syntax-parse?

When writing a macro that uses syntax/parse, I have created a splicing syntax class that captures options that may be provided to the macro. These options are all optional, and they may be provided in any order. Using the ~optional ellipsis head pattern makes this easy enough:

(define-splicing-syntax-class opts
  (pattern (~seq (~or (~optional (~seq #:a a))
                      (~optional (~seq #:b b))
                      (~optional (~seq #:x x))
                      (~optional (~seq #:y y)))
                 ...))

However, there is a catch: I want to be able to group these options into two groups: the group containing a and b, and the group containing x and y. However, the user may still specify the options in any order, so for this example input:

(foobar #:b 3 #:y 7 #:a 2)

I want to be able to produce the following attributes:

first-opts:  (#:a 2 #:b 3)
second-opts: (#:y 7)

So far, I’ve managed to do this manually using #:with, but it isn’t pretty:

(define-splicing-syntax-class opts
  #:attributes ([first-opts 1] [second-opts 1])
  (pattern (~seq (~or (~optional (~seq #:a a))
                      (~optional (~seq #:b b))
                      (~optional (~seq #:x x))
                      (~optional (~seq #:y y)))
                 ...)
           #:with (first-opts ...)
           #`(#,@(if (attribute a) #'(#:a a) #'())
              #,@(if (attribute b) #'(#:b b) #'()))
           #:with (second-opts ...)
           #`(#,@(if (attribute x) #'(#:x x) #'())
              #,@(if (attribute y) #'(#:y y) #'()))))

This can be simplified a little bit using template from syntax/parse/experimental/template:

(define-splicing-syntax-class opts
  #:attributes ([first-opts 1] [second-opts 1])
  (pattern (~seq (~or (~optional (~seq #:a a))
                      (~optional (~seq #:b b))
                      (~optional (~seq #:x x))
                      (~optional (~seq #:y y)))
                 ...)
           #:with (first-opts ...)
           (template ((?? (?@ #:a a))
                      (?? (?@ #:b b))))
           #:with (second-opts ...)
           (template ((?? (?@ #:a x))
                      (?? (?@ #:b y))))))

However, this is really just some sugar for the above, and it doesn’t actually address the problem of having to enumerate each option in each clause. If I, for example, added a #:c option, I would need to remember to add it to the first-opts group, otherwise it would be completely ignored.

What I really want is some declarative way to group these sets of optional values. For example, I’d like a syntax like this:

(define-splicing-syntax-class opts
  #:attributes ([first-opts 1] [second-opts 1])
  (pattern (~seq (~or (~group first-opts
                              (~optional (~seq #:a a))
                              (~optional (~seq #:b b)))
                      (~group second-opts
                              (~optional (~seq #:x x))
                              (~optional (~seq #:y y))))
                 ...)))

Or, even better, it would be nice if I could use existing primitives, something like this:

(define-splicing-syntax-class opts
  #:attributes ([first-opts 1] [second-opts 1])
  (pattern (~seq (~or (~and first-opts
                            (~seq (~optional (~seq #:a a))
                                  (~optional (~seq #:b b))))
                      (~and second-opts
                            (~seq (~optional (~seq #:x x))
                                  (~optional (~seq #:y y)))))
                 ...)))

However, neither of those work. Is there any way to do this using the builtins provided by syntax/parse? If not, is there any simple way to define something like ~group myself?

like image 478
Alexis King Avatar asked Dec 31 '15 20:12

Alexis King


1 Answers

There is a way to do that with a ~groups-no-order pattern expander like this:

(define-splicing-syntax-class opts
  #:attributes ([first-opts 1] [second-opts 1])
  [pattern (~groups-no-order
            [first-opts
             (~optional (~seq #:a a))
             (~optional (~seq #:b b))]
            [second-opts
             (~optional (~seq #:x x))
             (~optional (~seq #:y y))])])

(syntax-parse #'(foobar #:b 3 #:y 7 #:a 2)
  [(foobar opts:opts)
   (values #'(opts.first-opts ...)
           #'(opts.second-opts ...))])
; #<syntax (#:a 2 #:b 3)>
; #<syntax (#:y 7)>

Where ~groups-no-order can be defined like this:

#lang racket
(provide ~groups-no-order)

(require syntax/parse
         seq-no-order
         (for-syntax racket/syntax
                     syntax/stx))

(define-syntax ~groups-no-order
  (pattern-expander
   (lambda (stx)
     (syntax-case stx ()
       [(groups [group-name member-pat ...] ...)
        (with-syntax ([ooo (quote-syntax ...)])
          (define/with-syntax [[member-tmp ...] ...]
            (stx-map generate-temporaries #'[[member-pat ...] ...]))
          (define/with-syntax [group-tmp ...]
            (generate-temporaries #'[group-name ...]))
          #'(~and (~seq-no-order (~and (~seq (~var member-tmp) ooo)
                                       member-pat)
                                 ... ...)
                  (~parse [[(~var group-tmp) ooo] ooo] #'[[member-tmp ooo] ...])
                  ...
                  (~parse [group-name ooo] #'[group-tmp ooo ooo])
                  ...))]))))

This does the same thing as your first solution using #:with, but it abstracts that stuff out into a reusable pattern expander.

like image 62
Alex Knauth Avatar answered Nov 03 '22 04:11

Alex Knauth