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

3

7

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?

Polygyny answered 31/12, 2015 at 20:12 Comment(0)
L
2

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.

Lippold answered 4/1, 2016 at 16:2 Comment(0)
R
0

I'm not (yet) sure of a way you can do this with something like ~group, but there is a way you can make your existing (working) solution that uses #:with look a lot nicer. Maybe it will work for your case, maybe not.

~optional takes in a default argument #:defaults, which you can set to be the empty syntax list, #'#f, or some other sentinel value, removing your requirement to have an if statement in your #:with clause. It would look something like this:

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

Hope that helps.

Rappel answered 2/1, 2016 at 18:1 Comment(1)
This doesn’t work for me because I don’t have a default value to use—I need the keyword arguments that aren’t provided to not show up in the expansion.Polygyny
F
0

I think using ~and leads to the most straightforward macro, but the head pattern version of ~and is more restrictive and doesn't quite work so I would separate the head-pattern part out.

Does the code below accomplish what you want?

Without head patterns you lose ~optional so I manually check for duplicates.

Also, first-opts and second-opts are not flattened, but I suspect that is ok?

#lang racket
(require (for-syntax syntax/parse racket/list))

(define-for-syntax (check-duplicate-kws kws-stx)
  (check-duplicates (syntax->list kws-stx) #:key syntax->datum))

(define-syntax test
  (syntax-parser
    [(_ (~seq k:keyword v) ...)
     #:fail-when (check-duplicate-kws #'(k ...)) "duplicate keyword"
     #:with ((~or (~and first-opts (~or (#:a _) (#:b _)))
                  (~and second-opts (~or (#:c _) (#:d _)))) ...)
            #'((k v) ...)
     #'(void)]))
Fishman answered 4/1, 2016 at 21:57 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.