How can I group optional attributes captured by parsing?

When writing a macro that uses syntax/parse , I created a splicing syntax class that captures parameters that can be provided to the macro. These parameters are optional and can be provided in any order. Using the ~optional head template with an ellipsis makes this simple enough:

 (define-splicing-syntax-class opts (pattern (~seq (~or (~optional (~seq #:aa)) (~optional (~seq #:bb)) (~optional (~seq #:xx)) (~optional (~seq #:yy))) ...)) 

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

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

I want to be able to create the following attributes:

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

So far, Ive managed to do this manually using #:with , but this is not the case:

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

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

 (define-splicing-syntax-class opts #:attributes ([first-opts 1] [second-opts 1]) (pattern (~seq (~or (~optional (~seq #:aa)) (~optional (~seq #:bb)) (~optional (~seq #:xx)) (~optional (~seq #:yy))) ...) #:with (first-opts ...) (template ((?? ( ?@ #:aa)) (?? ( ?@ #:bb)))) #:with (second-opts ...) (template ((?? ( ?@ #:ax)) (?? ( ?@ #:by)))))) 

However, this is actually just a little sugar for the above, and it does not actually address the problem of having to list each parameter in each section. If, for example, I added the #:c parameter, I won’t need to add it to the first-opts , otherwise it will be completely ignored.

What I really want is some declarative way of grouping these sets of optional values. For example, Id like syntax like this:

 (define-splicing-syntax-class opts #:attributes ([first-opts 1] [second-opts 1]) (pattern (~seq (~or (~group first-opts (~optional (~seq #:aa)) (~optional (~seq #:bb))) (~group second-opts (~optional (~seq #:xx)) (~optional (~seq #:yy)))) ...))) 

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 #:aa)) (~optional (~seq #:bb)))) (~and second-opts (~seq (~optional (~seq #:xx)) (~optional (~seq #:yy))))) ...))) 

However, none of them work. Is there a way to do this is by using built-in functions provided by the syntax/parse ? If not, is there an easy way to define something like ~group yourself?

+6
source share
3 answers

There is a way to do this with the ~groups-no-order template extender as follows:

 (define-splicing-syntax-class opts #:attributes ([first-opts 1] [second-opts 1]) [pattern (~groups-no-order [first-opts (~optional (~seq #:aa)) (~optional (~seq #:bb))] [second-opts (~optional (~seq #:xx)) (~optional (~seq #:yy))])]) (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 as follows:

 #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 as your first solution with #:with , but it abstracts this stuff into an extensible template expander.

+2
source

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

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

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

Hope this helps.

0
source

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

Does the code below do what you want?

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

In addition, first-opts and second-opts not smoothed, but I suspect that this is normal?

 #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 _)))) ...) #'((kv) ...) #'(void)])) 
0
source

All Articles