Reputation: 43902
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?
Upvotes: 7
Views: 184
Reputation: 2550
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)]))
Upvotes: 0
Reputation: 8373
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.
Upvotes: 2
Reputation: 22342
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.
Upvotes: 0