From 01c9b553a5b35d5661548d59c928c727af7f966f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Georges=20Dup=C3=A9ron?= Date: Thu, 22 Sep 2016 22:33:31 +0200 Subject: [PATCH] Implemented and documented: pre operations, ~lift-rest. --- main.rkt | 6 + private/no-order.rkt | 291 ++++++++--- private/nop.rkt | 10 + private/parameters.rkt | 14 +- private/post.rkt | 29 +- private/pre.rkt | 72 +++ scribblings/defining-reusable-mixins.scrbl | 66 +++ .../extensible-parser-specifications.scrbl | 490 +----------------- scribblings/forward-attributes.scrbl | 53 ++ scribblings/global.scrbl | 97 ++++ scribblings/misc.scrbl | 18 + scribblings/no-order.scrbl | 192 +++++++ scribblings/post.scrbl | 40 ++ scribblings/pre-global-post-order.scrbl | 82 +++ scribblings/pre-global-post-section.scrbl | 23 + scribblings/pre.scrbl | 75 +++ scribblings/rest.scrbl | 67 +++ scribblings/utils.rkt | 26 + test/test-order-point.rkt | 4 +- test/test-rest.rkt | 88 ++++ 20 files changed, 1164 insertions(+), 579 deletions(-) create mode 100644 private/nop.rkt create mode 100644 private/pre.rkt create mode 100644 scribblings/defining-reusable-mixins.scrbl create mode 100644 scribblings/forward-attributes.scrbl create mode 100644 scribblings/global.scrbl create mode 100644 scribblings/misc.scrbl create mode 100644 scribblings/no-order.scrbl create mode 100644 scribblings/post.scrbl create mode 100644 scribblings/pre-global-post-order.scrbl create mode 100644 scribblings/pre-global-post-section.scrbl create mode 100644 scribblings/pre.scrbl create mode 100644 scribblings/rest.scrbl create mode 100644 scribblings/utils.rkt create mode 100644 test/test-rest.rkt diff --git a/main.rkt b/main.rkt index 7fbd212..97eedfc 100644 --- a/main.rkt +++ b/main.rkt @@ -3,11 +3,13 @@ (require generic-syntax-expanders "private/parameters.rkt" "private/no-order.rkt" + "private/pre.rkt" "private/post.rkt" "private/global.rkt" "private/optional.rkt" "private/mixin.rkt" "private/try-attribute.rkt" + "private/nop.rkt" (for-template "private/define-syntax+simple-api.rkt") syntax/parse) @@ -26,9 +28,13 @@ order-point> try-order-point< try-order-point> + ~before + ~after + ~lift-rest ~mixin ~post-check ~post-fail + ~maybe/empty ~named-seq ~nop ~optional/else diff --git a/private/no-order.rkt b/private/no-order.rkt index 7d6aa81..368464b 100644 --- a/private/no-order.rkt +++ b/private/no-order.rkt @@ -22,6 +22,8 @@ ;syntax/parse/experimental/eh generic-syntax-expanders phc-toolkit/untyped + racket/list + racket/function (for-syntax racket/base syntax/parse racket/syntax @@ -34,11 +36,15 @@ (provide define-eh-alternative-mixin ~seq-no-order ~no-order + ~before + ~after ~order-point order-point< order-point> try-order-point< try-order-point> + ~lift-rest + ~omitable-lifted-rest ;; Private (expander-out eh-mixin)) (define-expander-type eh-mixin) @@ -73,74 +79,166 @@ (record-disappeared-uses dis) #'(void))})) +;; TODO: this does not work when there is a pattern expander which expands to +;; an ~or^eh +(define-for-syntax (catch-omitable-lifted-rest stx) + (define caught '()) + (define (r stx) + ;(displayln (list r stx)) + (cond + [(syntax? stx) (datum->syntax stx (r (syntax-e stx)) stx stx)] + [(and (pair? stx) + (identifier? (car stx)) + (free-identifier=? (car stx) #'~or)) + (cons (car stx) (l (cdr stx)))] + [(and (pair? stx) + (identifier? (car stx)) + (free-identifier=? (car stx) #'~omitable-lifted-rest)) + (set! caught (cons stx caught)) + #'{~or}] ;; empty ~or with no eh alternatives + [else stx])) + (define (l stx) + ;(displayln (list l stx)) + (cond + [(syntax? stx) (datum->syntax stx (r (syntax-e stx)) stx stx)] + [(list? stx) (map r stx)] + [(pair? stx) (cons (r (car stx)) (l (cdr stx)))] + [else stx])) + (define cleaned (r stx)) + (values cleaned caught)) + ;; TODO: ~seq-no-order should also be a eh-mixin-expander, so that when there ;; are nested ~seq-no-order, the ~post-fail is caught by the nearest ;; ~seq-no-order. -(define-syntax ~seq-no-order - (pattern-expander - (λ (stx) - (syntax-case stx () - [(self pat ...) - (with-disappeared-uses* - (define counter 0) - (define (increment-counter) - (begin0 counter - (set! counter (add1 counter)))) - ;; post-acc gathers some a-patterns which will be added after the - ;; (~seq (~or ) ...) - (define post-acc '()) - (define (add-to-post! v) (set! post-acc (cons v post-acc))) - ;; post-groups-acc gathers some attributes that have to be grouped - (define post-groups-acc '()) - (define (add-to-post-groups! . v) - (set! post-groups-acc (cons v post-groups-acc))) - ;; expand EH alternatives: - (parameterize ([eh-post-accumulate add-to-post!] - [eh-post-group add-to-post-groups!] - [clause-counter increment-counter]) - (define alts - (expand-all-eh-mixin-expanders #'(~or pat ...))) - (define post-group-bindings - (for/list ([group (group-by car - (reverse post-groups-acc) - free-identifier=?)]) - ;; each item in `group` is a four-element list: - ;; (list result-id aggregate-function attribute) - (define/with-syntax name (first (car group)) - #;(syntax-local-introduce - (datum->syntax #'here - (first (car group))))) - (define/with-syntax f (second (car group))) - #`[name (f . #,(map (λ (i) #`(attribute #,(third i))) - group))])) - (define/with-syntax whole-clause (get-new-clause!)) - (define/with-syntax parse-seq-order-sym-id - (datum->syntax (parse-seq-order-sym-introducer - (syntax-local-introduce #'here)) - 'parse-seq-order-sym)) - #`(~delimit-cut - (~and #,(fix-disappeared-uses) - {~seq whole-clause (… …)} - {~do (define parse-seq-order-sym-id - (gensym 'parse-seq-order))} - {~parse ({~seq #,alts (… …)}) - #`#,(for/list - ([xi (in-syntax #'(whole-clause (… …)))] - [i (in-naturals)]) - ;; Add a syntax property before parsing, - ;; to track the position of matched elements - ;; using ~order-point - (syntax-property xi - parse-seq-order-sym-id - i))} - ~! - (~bind #,@post-group-bindings) - #,@(reverse post-acc)))))])))) -(define-syntax ~no-order - (pattern-expander - (λ/syntax-case (_ . rest) () - #'({~seq-no-order . rest})))) + +(define-for-syntax ((no-order-ish seq?) stx) + (syntax-case stx () + [(self pat ...) + (with-disappeared-uses* + (define counter 0) + (define (increment-counter!) + (begin0 counter + (set! counter (add1 counter)))) + ;; pre-acc and post-acc gather some a-patterns which will be added after + ;; the (~seq (~or ) ...), before and after the ~! cut respectively + (define pre-acc '()) + (define (add-to-pre! v) (set! pre-acc (cons v pre-acc))) + (define post-acc '()) + (define (add-to-post! v) (set! post-acc (cons v post-acc))) + ;; post-groups-acc gathers some attributes that have to be grouped + (define post-groups-acc '()) + (define (add-to-post-groups! . v) + (set! post-groups-acc (cons v post-groups-acc))) + (define lifted-rest '()) + (define (add-to-lift-rest! present-clause expanded-pat) + (define succeeded-clause (get-new-clause!)) + (set! lifted-rest (cons (list present-clause + expanded-pat + succeeded-clause) + lifted-rest))) + ;; expand EH alternatives: + (parameterize ([eh-pre-accumulate add-to-post!] + [eh-post-group add-to-post-groups!] + [eh-post-accumulate add-to-post!] + [clause-counter increment-counter!] + [lift-rest add-to-lift-rest!]) + (define alts + (expand-all-eh-mixin-expanders #'(~or pat ...))) + ;; NOTE: this works only because eh-mixin-expanders are NOT pattern + ;; expanders. If these are merged later on, then this needs to be + ;; adjusted + (define-values (cleaned-alts caught-omitable-lifted-rest) + (catch-omitable-lifted-rest alts)) + (define post-group-bindings + (for/list ([group (group-by car + (reverse post-groups-acc) + free-identifier=?)]) + ;; each item in `group` is a four-element list: + ;; (list result-id aggregate-function attribute) + (define/with-syntax name (first (car group)) + #;(syntax-local-introduce + (datum->syntax #'here + (first (car group))))) + (define/with-syntax f (second (car group))) + #`[name (f . #,(map (λ (i) #`(attribute #,(third i))) + group))])) + (set! lifted-rest (reverse lifted-rest)) + (define/with-syntax whole-clause (get-new-clause!)) + (define/with-syntax rest-clause (get-new-clause!)) + (define/with-syntax parse-seq-order-sym-id + (datum->syntax (parse-seq-order-sym-introducer + (syntax-local-introduce #'here)) + 'parse-seq-order-sym)) + (define/with-syntax whole-clause-pat + (if seq? + (begin + (when (not (null? lifted-rest)) + (raise-syntax-error + '~seq-no-order + (string-append "rest clause must be used within ~no-order," + " but was used within ~seq-no-order") + stx)) + #'{~seq whole-clause (… …) {~bind [(rest-clause 1) (list)]}}) + #'(whole-clause (… …) . {~and rest-clause {~not (_ . _)}}))) + (define rest-handlers + (if (null? lifted-rest) + #'() + (map (match-lambda + [(list present expanded-pat succeeded) + #`{~parse {~or {~and {~parse + #t + (ormap identity + (flatten + (attribute #,present)))} + #,expanded-pat + {~bind [#,succeeded #t]}} + _} + #'rest-clause}]) + lifted-rest))) + (define check-at-least-one-rest-handler + (if (null? lifted-rest) + #'() + (with-syntax ([([_ _ succeeded] …) lifted-rest]) + #'({~fail #:unless (or (attribute succeeded) …) + "expected one of the rest patterns to match"})))) + (define check-no-dup-rest-handlers + (if (null? lifted-rest) + #'() + (with-syntax ([([_ _ succeeded] …) lifted-rest]) + #'({~fail #:when (> (length + (filter (λ (x) x) + (list (attribute succeeded) …))) + 1) + (string-append "more than one of the lifted rest" + " patterns matched")})))) + ((λ (x) #;(pretty-write (syntax->datum x)) x) + #`(~delimit-cut + (~and #,(fix-disappeared-uses) + whole-clause-pat + {~do (define parse-seq-order-sym-id + (gensym 'parse-seq-order))} + {~parse ({~seq #,cleaned-alts (… …)}) + #`#,(for/list + ([xi (in-syntax #'(whole-clause (… …)))] + [i (in-naturals)]) + ;; Add a syntax property before parsing, + ;; to track the position of matched elements + ;; using ~order-point + (syntax-property xi + parse-seq-order-sym-id + i))} + #,@(reverse pre-acc) + #,@caught-omitable-lifted-rest + #,@rest-handlers + #,@check-at-least-one-rest-handler + ~! + #,@check-no-dup-rest-handlers + (~bind #,@post-group-bindings) + #,@(reverse post-acc))))))])) + +(define-syntax ~seq-no-order (pattern-expander (no-order-ish #t))) +(define-syntax ~no-order (pattern-expander (no-order-ish #f))) (define-eh-mixin-expander ~order-point (λ (stx) @@ -168,4 +266,67 @@ (if-attribute a (if-attribute b (order-point< a b) #f) #f)) (define-syntax-rule (try-order-point> a b) - (if-attribute a (if-attribute b (order-point> a b) #f) #f)) \ No newline at end of file + (if-attribute a (if-attribute b (order-point> a b) #f) #f)) + +(define-eh-mixin-expander ~before + (λ (stx) + (syntax-case stx () + [(_ other message pat …) + (and (identifier? #'other) + (string? (syntax-e #'message)) + #'{~order-point pt + {~seq pat …} + {~post-fail message #:when (order-point> pt other)}})]))) + +(define-eh-mixin-expander ~after + (λ (stx) + (syntax-case stx () + [(_ other message pat …) + (and (identifier? #'other) + (string? (syntax-e #'message)) + #'{~order-point pt + {~seq pat …} + {~post-fail message #:when (order-point< pt other)}})]))) + +(define-eh-mixin-expander ~try-before + (λ (stx) + (syntax-case stx () + [(_ other message pat …) + (and (identifier? #'other) + (string? (syntax-e #'message)) + #'{~order-point pt + {~seq pat …} + {~post-fail message #:when (try-order-point> pt other)}})]))) + +(define-eh-mixin-expander ~try-after + (λ (stx) + (syntax-case stx () + [(_ other message pat …) + (and (identifier? #'other) + (string? (syntax-e #'message)) + #'{~order-point pt + {~seq pat …} + {~post-fail message #:when (try-order-point< pt other)}})]))) + +(define-syntax ~omitable-lifted-rest + (pattern-expander + (λ (stx) + (syntax-case stx () + [(_ expanded-pats clause-present) + #'{~and + ;; TODO: copy the disappeared uses instead of this hack + {~do 'expanded-pats} + {~bind [clause-present #t]}}])))) + + +(define-eh-mixin-expander ~lift-rest + (λ (stx) + (syntax-case stx () + [(_ pat) + (let () + (define/with-syntax clause-present (get-new-clause!)) + (define/with-syntax expanded-pat + ;; let the ~post, ~global etc. within pat … be recognized + (expand-all-eh-mixin-expanders #'pat)) + (lift-rest! '~lift-rest #'clause-present #'expanded-pat) + #'(~omitable-lifted-rest expanded-pat clause-present))]))) \ No newline at end of file diff --git a/private/nop.rkt b/private/nop.rkt new file mode 100644 index 0000000..5e9bf3d --- /dev/null +++ b/private/nop.rkt @@ -0,0 +1,10 @@ +#lang racket +(require syntax/parse + (for-syntax syntax/parse + phc-toolkit/untyped)) + +(provide ~nop) + +(define-syntax ~nop + (pattern-expander + (λ/syntax-case (_) () #'(~do)))) diff --git a/private/parameters.rkt b/private/parameters.rkt index acce164..31ae5f8 100644 --- a/private/parameters.rkt +++ b/private/parameters.rkt @@ -2,13 +2,17 @@ (require (for-syntax racket/base)) -(provide (for-syntax eh-post-accumulate +(provide (for-syntax eh-pre-accumulate + eh-pre-accumulate! + eh-post-accumulate eh-post-accumulate! eh-post-group eh-post-group! clause-counter get-new-clause! - is-clause-id-sym?)) + is-clause-id-sym? + lift-rest + lift-rest!)) (define-syntax-rule (define-dynamic-accumulator-parameter parameter-name name!) (begin @@ -20,8 +24,10 @@ " used outside of ~seq-no-order"))) (apply (parameter-name) args)))) -(define-dynamic-accumulator-parameter eh-post-accumulate eh-post-accumulate!) +(define-dynamic-accumulator-parameter eh-pre-accumulate eh-pre-accumulate!) (define-dynamic-accumulator-parameter eh-post-group eh-post-group!) +(define-dynamic-accumulator-parameter eh-post-accumulate eh-post-accumulate!) +(define-dynamic-accumulator-parameter lift-rest lift-rest!) ;; This is a crude hack. (define-for-syntax (is-clause-id-sym? id-sym) @@ -34,4 +40,4 @@ (error "Use get-new-clause! within (parameterize ([clause-counter …]) …)")) (datum->syntax #'here ;; keep the spaces, they allow us to recognize clauses later. - (string->symbol (format " -clause-~a " ((clause-counter)))))) \ No newline at end of file + (string->symbol (format " -clause-~a " ((clause-counter)))))) diff --git a/private/post.rkt b/private/post.rkt index 2dc4517..f8ea102 100644 --- a/private/post.rkt +++ b/private/post.rkt @@ -6,16 +6,11 @@ racket/syntax phc-toolkit/untyped) "parameters.rkt" - "no-order.rkt") + "no-order.rkt" + "nop.rkt") -(provide ~nop - ~post-check - ~post-fail - ~named-seq) - -(define-syntax ~nop - (pattern-expander - (λ/syntax-case (_) () #'(~do)))) +(provide ~post-check + ~post-fail) (define-eh-mixin-expander ~post-check (λ (stx) @@ -38,22 +33,6 @@ ...)) #'(~and (~bind [clause-present #t]) . pats))]))) -(define-eh-mixin-expander ~named-seq - (λ (stx) - (syntax-case stx () - [(_ id . pats) - (identifier? #'id) - (let () - (define/with-syntax clause-present (get-new-clause!)) - (define/with-syntax clause (get-new-clause!)) - (eh-post-accumulate! '~named-seq - #'(~bind [(id 1) (if (attribute clause-present) - (attribute clause) - (list))])) - #'(~and (~bind [clause-present #t]) - (~seq clause (... ...)) - (~seq . pats)))]))) - (define-for-syntax (post-fail stx) (syntax-case stx () [(_ message #:when condition) diff --git a/private/pre.rkt b/private/pre.rkt new file mode 100644 index 0000000..69c20c2 --- /dev/null +++ b/private/pre.rkt @@ -0,0 +1,72 @@ +#lang racket/base + +(require syntax/parse + (for-syntax racket/base + syntax/parse + racket/syntax + phc-toolkit/untyped) + "parameters.rkt" + "no-order.rkt" + "nop.rkt") + +(provide ~pre-check + ~pre-fail + ~named-seq + ~maybe/empty) + +(define-eh-mixin-expander ~pre-check + (λ (stx) + (syntax-case stx () + [(_ pat post) + (begin (eh-pre-accumulate! '~pre-check #'post) + #'pat)] + [(_ post) + (begin (eh-pre-accumulate! '~pre-check #'post) + #'(~nop))]))) + +(define-for-syntax (pre-fail stx) + (syntax-case stx () + [(_ message #:when condition) + (let () + (define/with-syntax clause-present (get-new-clause!)) + (eh-pre-accumulate! '~pre-fail + #`(~fail #:when (and (attribute clause-present) + condition) + message)) + #'(~bind [clause-present #t]))] + [(self #:when condition message) + (pre-fail #'(self message #:when condition))] + [(self message #:unless unless-condition) + (pre-fail #'(self message #:when (not unless-condition)))] + [(self #:unless unless-condition message) + (pre-fail #'(self message #:when (not unless-condition)))])) + +(define-eh-mixin-expander ~pre-fail pre-fail) + +(define-eh-mixin-expander ~named-seq + (λ (stx) + (syntax-case stx () + [(_ id . pats) + (identifier? #'id) + (let () + (define/with-syntax clause-present (get-new-clause!)) + (define/with-syntax clause (get-new-clause!)) + (eh-pre-accumulate! '~named-seq + #'(~bind [(id 1) (if (attribute clause-present) + (attribute clause) + (list))])) + #'(~and (~bind [clause-present #t]) + (~seq clause (... ...)) + (~seq . pats)))]))) + +(define-eh-mixin-expander ~maybe/empty + (λ (stx) + (syntax-case stx () + [(_ . pats) + (let () + (define/with-syntax clause-present (get-new-clause!)) + (eh-pre-accumulate! '~maybe/empty + #'(~parse {~no-order {~seq . pats}} + #'(clause (... ...)))) + #'{~optional {~and {~bind [clause-present #t]} + {~seq clause (... ...)}}})]))) \ No newline at end of file diff --git a/scribblings/defining-reusable-mixins.scrbl b/scribblings/defining-reusable-mixins.scrbl new file mode 100644 index 0000000..e94cab6 --- /dev/null +++ b/scribblings/defining-reusable-mixins.scrbl @@ -0,0 +1,66 @@ +#lang scribble/manual +@require[scribble/example + "utils.rkt" + @for-label[phc-toolkit/untyped + extensible-parser-specifications + generic-syntax-expanders + racket/base + syntax/parse + (only-in racket/base [... …])]] + +@title{Defining reusable parser mixins} + +@defform[#:literals (pattern) + (define-eh-alternative-mixin name maybe-define-class + (pattern clause-or-mixin) ...) + #:grammar + [(maybe-define-class + (code:line #:define-splicing-syntax-class splicing-name)) + (clause-or-mixin #,ntax-pattern + (~mixin #,-alternative-mixin) + (~or clause-or-mixin ...) + derived-or)]]{ + Defines an @deftech{eh-alternative mixin}, which is implemented as an @tech{ + eh-mixin expander}. An eh-alternative mixin is like an + @tech[#:doc '(lib "syntax/scribblings/syntax.scrbl")]{ellipsis-head alternative + set}, except that it can only appear as part of a @racket[~no-order] (possibly + nested under other eh-alternative mixins), and can contain some global + constraints. The global constraints, detailed below, allow the parser to + perform checks across two or more mixins. For example, given a set of options + that can appear in any order, it is possible to specify that two of them are + mutually exclusive, or that two other must appear in a certain order, + regardless of the order of the other options. + + The @racket[derived-or] term covers any + @tech[#:doc '(lib "syntax/scribblings/syntax.scrbl")]{pattern expander} or + @tech{eh-mixin expander} application which expands to a + @racket[clause-or-mixin].} + +@deftogether[[@defthing[#:kind "for-syntax value" + eh-mixin-expander-type expander-type?] + @defproc[#:kind "for-syntax procedure" + (make-eh-mixin-expander) + (and/c expander? eh-mixin-expander?)] + @defproc[#:kind "for-syntax procedure" + (eh-mixin-expander? [v any/c]) + boolean?] + @defform[(define-eh-mixin-expander id transformer-procedure)] + @defproc[#:kind "for-syntax procedure" + (expand-all-eh-mixin-expanders [stx-tree syntax?]) + syntax?]]]{ + These functions and forms allow the creation and manipulation of @deftech{ + eh-mixin expanders}. These identifiers are generated by + @racket[define-expander-type]. For more information, see the documentation for + @racket[define-expander-type].} + +@section{Using mixins} + +@defform[(~mixin #,-alternative-mixin)]{ + Expands the @racket[#,-alternative-mixin], with no arguments. This is + equivalent to @racket[(_eh-alternative-mixin)], but @racket[~mixin] + additionally checks that the given @racket[_eh-alternative-mixin] is indeed an + @tech{eh-alternative mixin}. Otherwise, with the syntax, + @racket[(_eh-alternative-mixin)] the name @racket[_eh-alternative-mixin] would + be interpreted as a pattern variable by @racket[syntax-parse] if the expander + was not available for some reason (e.g. a missing import).} + diff --git a/scribblings/extensible-parser-specifications.scrbl b/scribblings/extensible-parser-specifications.scrbl index ae537af..091119a 100644 --- a/scribblings/extensible-parser-specifications.scrbl +++ b/scribblings/extensible-parser-specifications.scrbl @@ -1,5 +1,6 @@ #lang scribble/manual @require[scribble/example + "utils.rkt" @for-label[phc-toolkit/untyped extensible-parser-specifications generic-syntax-expanders @@ -7,494 +8,17 @@ syntax/parse (only-in racket/base [... …])]] -@(define make-evaluator - (make-eval-factory '(syntax/parse - extensible-parser-specifications))) - @title{extensible-parser-specifications} @author{@author+email["Georges Dupéron" "georges.duperon@gmail.com"]} -@(define ntax-pattern (tech #:doc '(lib "syntax/scribblings/syntax.scrbl") - #:key "syntax pattern" - "syntax-pattern")) - -@(define -alternative-mixin (tech #:key "eh-alternative mixin" - "eh-alternative-mixin")) - -@(define tribute-name (tech #:doc '(lib "syntax/scribblings/syntax.scrbl") - #:key "attribute" - "attribute-name")) - -@(define A-patte (tech #:doc '(lib "syntax/scribblings/syntax.scrbl") - #:key "action pattern" - "A-pattern")) - @defmodule[extensible-parser-specifications] -@defform[#:literals (pattern) - (define-eh-alternative-mixin name maybe-define-class - (pattern clause-or-mixin) ...) - #:grammar - [(maybe-define-class - (code:line #:define-splicing-syntax-class splicing-name)) - (clause-or-mixin #,ntax-pattern - (~mixin #,-alternative-mixin) - (~or clause-or-mixin ...) - derived-or)]]{ - Defines an @deftech{eh-alternative mixin}, which is implemented as an @tech{ - eh-mixin expander}. An eh-alternative mixin is like an - @tech[#:doc '(lib "syntax/scribblings/syntax.scrbl")]{ellipsis-head alternative - set}, except that it can only appear as part of a @racket[~no-order] (possibly - nested under other eh-alternative mixins), and can contain some global - constraints. The global constraints, detailed below, allow the parser to - perform checks across two or more mixins. For example, given a set of options - that can appear in any order, it is possible to specify that two of them are - mutually exclusive, or that two other must appear in a certain order, - regardless of the order of the other options. +@include-section{defining-reusable-mixins.scrbl} +@include-section{no-order.scrbl} +@include-section{rest.scrbl} - The @racket[derived-or] term covers any - @tech[#:doc '(lib "syntax/scribblings/syntax.scrbl")]{pattern expander} or - @tech{eh-mixin expander} application which expands to a - @racket[clause-or-mixin].} +@include-section{pre-global-post-section.scrbl} -@deftogether[[@defthing[#:kind "for-syntax value" - eh-mixin-expander-type expander-type?] - @defproc[#:kind "for-syntax procedure" - (make-eh-mixin-expander) - (and/c expander? eh-mixin-expander?)] - @defproc[#:kind "for-syntax procedure" - (eh-mixin-expander? [v any/c]) - boolean?] - @defform[(define-eh-mixin-expander id transformer-procedure)] - @defproc[#:kind "for-syntax procedure" - (expand-all-eh-mixin-expanders [stx-tree syntax?]) - syntax?]]]{ - These functions and forms allow the creation and manipulation of @deftech{ - eh-mixin expanders}. These identifiers are generated by - @racket[define-expander-type]. For more information, see the documentation for - @racket[define-expander-type].} - -@section{Pattern expanders and eh-mixin expanders} - -@defform[#:kind "pattern expander" - #:literals (~mixin ~or) - (~seq-no-order clause-or-mixin ...) - #:grammar - [(clause-or-mixin #,ntax-pattern - (~mixin #,-alternative-mixin) - (~or clause-or-mixin ...) - derived-or)]]{ - Splicing pattern which matches the given @racket[clause-or-mixin]s in any - order, enforcing the global constraints expressed within each. - - Nested @racket[~or] directly below @racket[~seq-no-order] are recursively - inlined. In other words, the @racket[~or] present directly below the - @racket[~seq-no-order] or below such an @racket[~or] clause do not behave as - "exclusive or", but instead contain clauses which can appear in any order. - These clauses are not grouped in any way by the @racket[~or], i.e. - @racket[(~no-order (~or (~or a b) (~or c d)))] is equivalent to - @racket[(~no-order a b c d)]. - - The @racket[derived-or] term covers any - @tech[#:doc '(lib "syntax/scribblings/syntax.scrbl")]{pattern expander} or - @tech{eh-mixin expander} application which expands to a - @racket[clause-or-mixin]. The expansion of pattern and eh-mixin expanders - happens before inlining the top @racket[~or] clauses.} - -@defform[#:kind "pattern expander" - #:literals (~mixin ~or) - (~no-order clause-or-mixin ...) - #:grammar - [(clause-or-mixin #,ntax-pattern - (~mixin #,-alternative-mixin) - (~or clause-or-mixin ...) - derived-or)]]{ - - Like @racket[~seq-no-order], except that it matches a syntax list, instead of - being spliced into the surrounding sequence of patterns. In other words, - - @racketblock[(~seq-no-order clause-or-mixin ...)] - - Equivalent to (notice the extra pair of braces): - - @racketblock[({~seq-no-order clause-or-mixin ...})]} - -@defform[#:kind "eh-mixin expander" - (~order-point point-name #,ntax-pattern ...)]{ - When parsing a sequence of elements, @racket[~seq-no-order] and - @racket[~no-order] associate an increasing number to each element starting from - zero. - - The number associated with the first element matched by - @racket[#,ntax-pattern ...] is memorised into the attribute - @racket[point-name]. - - This allows the position of elements matched by otherwise independent mixins to - be compared using @racket[order-point<] and @racket[order-point>]} - -@defform[(order-point< a b) - #:grammar - [(a #,tribute-name) - (b #,tribute-name)]]{ - Returns @racket[#t] when the first element matched by - @racket[(~order-point a #,ntax-pattern ...)] occurs before the first element - matched by @racket[(~order-point b #,ntax-pattern ...)]. Otherwise, returns - @racket[#f]. - - This operation does not fail if @racket[a] or @racket[b] are bound to - @racket[#f] (i.e. their corresponding @racket[_syntax-pattern ...] did not - match). Instead, in both cases, it returns @racket[#f].} - -@defform[(order-point> a b) - #:grammar - [(a #,tribute-name) - (b #,tribute-name)]]{ - Returns @racket[#t] when the first element matched by - @racket[(~order-point a #,ntax-pattern ...)] occurs after the first element - matched by @racket[(~order-point b #,ntax-pattern ...)]. Otherwise, returns - @racket[#f]. - - This operation does not fail if @racket[a] or @racket[b] are bound to - @racket[#f] (i.e. their corresponding @racket[_syntax-pattern ...] did not - match). Instead, in both cases, it returns @racket[#f].} - -@defform[(try-order-point< a b) - #:grammar - [(a #,tribute-name) - (b #,tribute-name)]]{ - - Like @racket[order-point<], except that it does not fail if @racket[a] or - @racket[b] are not attributes, or if they are bound to @racket[#f]. Instead, in - all those cases, it returns @racket[#f]. - - It can be used as follows: - - @racketblock[ - (~post-fail "a must appear after b" - #:when (try-order-point< a b))] - - The same caveats as for @racket[try-attribute] apply.} - -@defform[(try-order-point> a b) - #:grammar - [(a #,tribute-name) - (b #,tribute-name)]]{ - - Like @racket[order-point>], except that it does not fail if @racket[a] or - @racket[b] are not attributes, or if they are bound to @racket[#f]. Instead, in - all those cases, it returns @racket[#f]. - - It can be used as follows: - - @racketblock[ - (~post-fail "a must appear before b" - #:when (try-order-point> a b))] - - The same caveats as for @racket[try-attribute] apply.} - -@defform[(~mixin #,-alternative-mixin)]{ - - Expands the @racket[#,-alternative-mixin], with no arguments. This is - equivalent to @racket[(_eh-alternative-mixin)], but @racket[~mixin] - additionally checks that the given @racket[_eh-alternative-mixin] is indeed an - @tech{eh-alternative mixin}. Otherwise, with the syntax, - @racket[(_eh-alternative-mixin)] the name @racket[_eh-alternative-mixin] would - be interpreted as a pattern variable by @racket[syntax-parse] if the expander - was not available for some reason (e.g. a missing import).} - -@defform[#:kind "pattern expander" - {~nop}]{ - The @tech[#:doc '(lib "syntax/scribblings/syntax.scrbl") - #:key "action pattern"]{A-pattern} @racket[~nop] does not perform any - action. It simply expands to @racket[{~do}]. -} - -@section{Post operations and global operations} - -@subsection{Post operations} - -@defform*[[(~post-check #,ntax-pattern #,A-patte) - (~post-check #,A-patte)]]{ - Matches @racket[#,ntax-pattern], and executes the given @racket[#,A-patte] - after the whole @racket[~seq-no-order] or @racket[~no-order] finished matching - its contents. - - If unspecified, the @racket[_syntax-pattern] defaults to @racket[(~nop)].} - -@defform*[[(~post-fail message #:when condition) - (~post-fail #:when condition message) - (~post-fail message #:unless unless-condition) - (~post-fail #:unless unless-condition message)]]{ - - After the whole @racket[~seq-no-order] or @racket[~no-order] finished matching - its contents, checks whether @racket[condition] or @racket[unless-condition] is - true or false, respectively. If this is the case the whole - @racket[~seq-no-order] or @racket[~no-order] is rejected with the given - @racket[_message]. - - Note that there is an implicit cut (@racket[~!]) between the no-order patterns - and the "post" checks, so after a @racket[~post-fail] fails, - @racket[syntax-parse] does not backtrack and attempt different combinations of - patterns to match the sequence, nor does it backtrack and attempt to match a - shorter sequence. This is by design, as it allows for better error messages - (syntax-parse would otherwise attempt and possibly succeed in matching a - shorter sequence, then just treat the remaining terms as "unexpected terms").} - -@subsection{Global operations} - -The global patterns presented below match all of the given -@racket[#,ntax-pattern]s, like @racket[~and] does, and perform a global -aggregation over all the values corresponding to successful matches of a global -pattern using the same @racket[#,tribute-name]. - -After the whole @racket[~seq-no-order] or @racket[~no-order] finished matching -its contents, but before "post" operations are executed, the attribute -@racket[#,tribute-name] is bound to -@racket[(_aggrgate-function _value₁ ... _valueₙ)], where each @racket[valueᵢ] is -the value which was passed to an occurrence of @racket[~global-or] with the same -@racket[_attribute-name], and which successfully matched. The -@racket[_aggregate-function] will be @racket[or] for @racket[~global-or], -@racket[and] for @racket[~global-and] or @racket[+] for -@racket[~global-counter]. - -Each @racket[valueᵢ] is computed in the context in which it appears, after the -@racket[_syntax-pattern]s. This means that it can access: -@itemlist[ - @item{attributes already bound in the current alternative clause within the - current @racket[~no-order] or @racket[~seq-no-order]} - @item{attributes bound by the @racket[_syntax-patterns]s} - @item{attributes already bound outside of the @racket[~no-order] or - @racket[~seq-no-order]} - @item{but it cannot access attributes bound in other alternative clauses within - the current @racket[~no-order] or @racket[~seq-no-order].}] - -The @racket[valueᵢ] are aggregated with @racket[or], @racket[and] or @racket[+] -in the order in which they appear in the @racket[~no-order] or -@racket[~seq-no-order]. If a @racket[valueᵢ] appears under ellipses, or as part -of an alternative clause which can match more than once (i.e. not @racket[~once] -or @racket[~optional]), then each match within that @racket[valueᵢ] group is -aggregated in the order it appears. - -Since this notion of order is rather complex, it is possible that future -versions of this library will always return a boolean (@racket[#f] or -@racket[#t] for @racket[~global-or] and @racket[~global-and], which would make -the notion of order irrelevant. - -@defform[(~global-or attribute-name+value #,ntax-pattern ...) - #:grammar - [(attribute-name+value #,tribute-name - [#,tribute-name valueᵢ])]]{ - Matches all of the given @racket[#,ntax-pattern]s, like @racket[~and] does, and - perform a global @racket[or] over all the values corresponding to successful - matches of a global pattern using the same @racket[#,tribute-name]. See above - for a description of how global operations work. - - If the @racket[valueᵢ] is omitted, @racket[#t] is used as a default. - - The result is always transformed into a boolean, so @racket[_attribute-name] is - always bound to either @racket[#t] or @racket[#f].} - -@defform[(~global-and attribute-name+value #,ntax-pattern ...) - #:grammar - [(attribute-name+value [#,tribute-name valueᵢ])]]{ - Matches all of the given @racket[#,ntax-pattern]s, like @racket[~and] does, and - perform a global @racket[and] over all the values corresponding to successful - matches of a global pattern using the same @racket[#,tribute-name]. See above - for a description of how global operations work. - - If there is at least one occurrence of @racket[~global-and] for that - @racket[_attribute-name] which successfully matches, the result of the - @racket[(and valueᵢ ...)] is always coerced to a boolean, so - @racket[_attribute-name] is always bound to either @racket[#t] or @racket[#f]. - - If there are no matches at all, the special value @racket['none] is used - instead of @racket[#t] as would be produced by @racket[(and)].} - -@defform[(~global-counter attribute-name+value #,ntax-pattern ...) - #:grammar - [(attribute-name+value #,tribute-name - [#,tribute-name valueᵢ])]]{ - Matches all of the given @racket[#,ntax-pattern]s, like @racket[~and] does, and - perform a global @racket[+] over all the values corresponding to successful - matches of a global pattern using the same @racket[#,tribute-name]. See above - for a description of how global operations work. - - If the @racket[valueᵢ] is omitted, @racket[1] is used as a default.} - -@;@defform[(aggregate-global-or)] -@;@defform[(aggregate-global-and)] -@;@defform[(aggregate-global-counter)] - -@subsection{Order in which the attributes are bound for post operations and - global operations} - -Within the @racket[_A-pattern]s of post operations, the regular attributes bound -by all the clauses inside @racket[~seq-no-order] or @racket[~no-order] are -bound. The attributes defined as part of all "global" actions are bound too. The -attributes defined as part of "post" actions of other clauses are bound only if -the clause defining them appears before the current clause in the source code. -For example, the following code works because the clause containing -@racket[{~post-fail "2 is incompatible with 1" #:when (not (attribute a))}] -appears after the clause which binds @racket[a] with the "post" action -@racket[{~post-check {~bind ([a #'the-a])}}]. - -@racketblock[ - {~seq-no-order - {~post-check {~and the-a 1} {~bind ([a #'the-a])}} - {~and 2 {~post-fail "2 is incompatible with 1" #:when (not (attribute a))}}}] - -If the two clauses are swapped, then the following code would raise a syntax -error because @racket[a] is not bound as an attribute in the -@racket[~post-fail]: - -@racketblock[ - {~seq-no-order - {~and 2 {~post-fail "2 is incompatible with 1" #:when (not (attribute a))}} - {~post-check {~and the-a 1} {~bind ([a #'the-a])}}}] - -On the other hand, the following code, which does not bind @racket[a] as part -of a post operation, is valid: - -@racketblock[ - {~seq-no-order - {~and 2 {~post-fail "2 is incompatible with 1" #:when (not (attribute a))}} - {~and the-a 1 {~bind ([a #'the-a])}}}] - -Furthermore, the following code still works, as attributes are bound by the -"global" operations before the "post" operations are executed: - -@racketblock[ - {~seq-no-order - {~and 2 {~post-fail "2 is incompatible with 1" #:when (not (attribute a))}} - {~global-or a 1}}] - -Note that the order in which clauses appear within the @racket[~seq-no-order] -or @racket[~no-order] does not impact the order in which the elements must -appear in the matched syntax (aside from issues related to greediness). - -@defform[(try-attribute #,tribute-name)]{ - This macro expands to @racket[(attribute #,tribute-name)] if - @racket[#,tribute-name] is bound as a syntax pattern variable, and to - @racket[#f] otherwise. - - This macro can be used to check for mutual exclusion of an attribute which is - bound by other mixins that might or might not be present in the final - @racket[~no-order] or @racket[~seq-no-order]. - - Use this sparingly, as if an syntax pattern variable with that name is bound by - an outer scope, the @racket[try-attribute] macro will still access it, ignorant - of the fact that the current @racket[~seq-no-order] does not contain any mixin - which binds that attribute. - - Instead, it is better practice to use - @racket[{~global-or [_attribute-name #f]}] or - @racket[{~global-and [_attribute-name #t]}] to ensure that the attribute is - declared, while using the operation's neutral element to not alter the final - result.} - -@defform[(if-attribute #,tribute-name if-branch else-branch)]{ - This macro expands to @racket[if-branch] if @racket[#,tribute-name] is bound as - a syntax pattern variable, and to @racket[else-branch] otherwise. - - The same caveats as for @racket[try-attribute] apply.} - -@defform[(~named-seq #,tribute-name #,ntax-pattern ...)]{ - Equivalent to @racket[{~seq #,ntax-pattern ...}], but also binds the - @racket[#,tribute-name] to the whole sequence. If the sequence appears inside - an @racket[~optional] or @racket[~or] clause that fails, the - @racket[_attribute-name] is still bound to the empty sequence. - - Known issues: this may not behave as expected if @racket[~named-seq] appears - under ellipses. - - This probably should bind the sequence attribute @emph{before} the "global" - operations, instead of being a "post" operation, and may be changed in that way - the future.} - -@defform[(~optional/else #,ntax-pattern - maybe-defaults - else-post-fail ... - maybe-name) - #:grammar - [(maybe-defaults (code:line) - (code:line #:defaults (default-binding ...))) - (else-post-fail - (code:line #:else-post-fail message #:when condition) - (code:line #:else-post-fail #:when condition message) - (code:line #:else-post-fail message #:unless unless-condition) - (code:line #:else-post-fail #:unless unless-condition message)) - (maybe-name (code:line) - (code:line #:name #,tribute-name))]]{ - - Like @racket[~optional], but with conditional post-failures when the pattern is - not matched. An @racket[~optional/else] pattern can be matched zero or one time - as part of the @racket[~seq-no-order] or @racket[~no-order]. When it is not - matched (i.e. matched zero times): - @itemlist[ - @item{it uses the default values for the attributes as specified with - @racket[#:defaults].} - @item{for each @racket[#:else-post-fail] clause, it checks whether the - @racket[condition] or @racket[unless-condition] is true or false, - respectively. If this is the case the whole @racket[~seq-no-order] or - @racket[~no-order] is rejected with the given @racket[_message]. The - behaviour of @racket[#:else-post-fail] is the same as the behaviour of - @racket[~post-fail], except that the "post" conditional failure can only be - executed if the optional @racket[_syntax-pattern] was not matched. - - Note that there is an implicit cut (@racket[~!]) between the no-order - patterns and the "post" checks, so after a @racket[~post-fail] fails, - @racket[syntax-parse] does not backtrack and attempt different combinations - of patterns to match the sequence, nor does it backtrack and attempt to match - a shorter sequence. This is by design, as it allows for better error messages - (syntax-parse would otherwise attempt and possibly succeed in matching a - shorter sequence, then just treat the remaining terms as - "unexpected terms").}] - - The meaning of @racket[#:name #,tribute-name] option is the same as for - @racket[~optional].} - - -@section{Chaining macro calls without re-parsing everything} - -@defform[(define/syntax-parse+simple (name-or-curry . #,ntax-pattern) . body) - #:grammar - [(name-or-curry name - (name-or-curry arg ...)) - (maybe-define-class #:define-splicing-syntax-class class-id) - (name identifier?) - (class-id identifier?)]]{ - This macro works like @racket[define/syntax-parse] from @racket[phc-toolkit], - except that it also defines the function @racket[_name-forward-attributes], - which can be used by other macros to forward already parsed attributes to the - @racket[body], without the need to parse everything a second time. - - The syntax pattern for the @racket[name] macro's arguments can be saved in a - syntax class by specifying the @racket[#:define-splicing-syntax-class] option. - - If the caller macro which uses @racket[(_name-forward-attributes)] parsed its - own @racket[stx] argument using @racket[class-id], then - @racket[(_name-forward-attributes)] is equivalent to expanding - @racket[(name stx)]. - - The @racket[_name-forward-attributes] function is defined at the same meta - level as @racket[name], i.e. at the same meta-level where this library was - required. } - - -@defform[#:kind "for-template syntax" - (define-syntax/parse+simple (name . #,ntax-pattern) . body)]{ - This macro is provided for meta-level -1. - - This is the same as @racket[define/syntax-parse+simple], except that it - operates at level -1 relative to this library, and defines at that level a - transformer binding (which therefore executes at the same meta-level as this - library. In other words, - @racket[(define-syntax/parse+simple (name . pat) . body)] is roughly equivalent - to: - - @racketblock[ - (begin-for-syntax - (define/syntax-parse+simple (tmp . pat) . body) - (define name-forward-attributes tmp-forward-attributes)) - (define-syntax name tmp)]} +@include-section{misc.scrbl} +@include-section{forward-attributes.scrbl} diff --git a/scribblings/forward-attributes.scrbl b/scribblings/forward-attributes.scrbl new file mode 100644 index 0000000..e7e387b --- /dev/null +++ b/scribblings/forward-attributes.scrbl @@ -0,0 +1,53 @@ +#lang scribble/manual +@require[scribble/example + "utils.rkt" + @for-label[phc-toolkit/untyped + extensible-parser-specifications + generic-syntax-expanders + racket/base + syntax/parse + (only-in racket/base [... …])]] + +@title{Chaining macro calls without re-parsing everything} + +@defform[(define/syntax-parse+simple (name-or-curry . #,ntax-pattern) . body) + #:grammar + [(name-or-curry name + (name-or-curry arg ...)) + (maybe-define-class #:define-splicing-syntax-class class-id) + (name identifier?) + (class-id identifier?)]]{ + This macro works like @racket[define/syntax-parse] from @racket[phc-toolkit], + except that it also defines the function @racket[_name-forward-attributes], + which can be used by other macros to forward already parsed attributes to the + @racket[body], without the need to parse everything a second time. + + The syntax pattern for the @racket[name] macro's arguments can be saved in a + syntax class by specifying the @racket[#:define-splicing-syntax-class] option. + + If the caller macro which uses @racket[(_name-forward-attributes)] parsed its + own @racket[stx] argument using @racket[class-id], then + @racket[(_name-forward-attributes)] is equivalent to expanding + @racket[(name stx)]. + + The @racket[_name-forward-attributes] function is defined at the same meta + level as @racket[name], i.e. at the same meta-level where this library was + required. } + + +@defform[#:kind "for-template syntax" + (define-syntax/parse+simple (name . #,ntax-pattern) . body)]{ + This macro is provided for meta-level -1. + + This is the same as @racket[define/syntax-parse+simple], except that it + operates at level -1 relative to this library, and defines at that level a + transformer binding (which therefore executes at the same meta-level as this + library. In other words, + @racket[(define-syntax/parse+simple (name . pat) . body)] is roughly equivalent + to: + + @racketblock[ + (begin-for-syntax + (define/syntax-parse+simple (tmp . pat) . body) + (define name-forward-attributes tmp-forward-attributes)) + (define-syntax name tmp)]} diff --git a/scribblings/global.scrbl b/scribblings/global.scrbl new file mode 100644 index 0000000..9541585 --- /dev/null +++ b/scribblings/global.scrbl @@ -0,0 +1,97 @@ +#lang scribble/manual +@require[scribble/example + "utils.rkt" + @for-label[phc-toolkit/untyped + extensible-parser-specifications + generic-syntax-expanders + racket/base + syntax/parse + (only-in racket/base [... …])]] + +@title{Global operations} + +The global patterns presented below match all of the given +@racket[#,ntax-pattern]s, like @racket[~and] does, and perform a global +aggregation over all the values corresponding to successful matches of a global +pattern using the same @racket[#,tribute-name]. + +After the whole @racket[~seq-no-order] or @racket[~no-order] finished matching +its contents, but before "post" operations are executed, the attribute +@racket[#,tribute-name] is bound to +@racket[(_aggrgate-function _value₁ ... _valueₙ)], where each @racket[valueᵢ] is +the value which was passed to an occurrence of @racket[~global-or] with the same +@racket[_attribute-name], and which successfully matched. The +@racket[_aggregate-function] will be @racket[or] for @racket[~global-or], +@racket[and] for @racket[~global-and] or @racket[+] for +@racket[~global-counter]. + +Each @racket[valueᵢ] is computed in the context in which it appears, after the +@racket[_syntax-pattern]s. This means that it can access: +@itemlist[ + @item{attributes already bound in the current alternative clause within the + current @racket[~no-order] or @racket[~seq-no-order]} + @item{attributes bound by the @racket[_syntax-patterns]s} + @item{attributes already bound outside of the @racket[~no-order] or + @racket[~seq-no-order]} + @item{but it cannot access attributes bound in other alternative clauses within + the current @racket[~no-order] or @racket[~seq-no-order].}] + +The @racket[valueᵢ] are aggregated with @racket[or], @racket[and] or @racket[+] +in the order in which they appear in the @racket[~no-order] or +@racket[~seq-no-order]. If a @racket[valueᵢ] appears under ellipses, or as part +of an alternative clause which can match more than once (i.e. not @racket[~once] +or @racket[~optional]), then each match within that @racket[valueᵢ] group is +aggregated in the order it appears. + +Since this notion of order is rather complex, it is possible that future +versions of this library will always return a boolean (@racket[#f] or +@racket[#t] for @racket[~global-or] and @racket[~global-and], which would make +the notion of order irrelevant. + +@defform[#:kind "eh-mixin expander" + (~global-or attribute-name+value #,ntax-pattern ...) + #:grammar + [(attribute-name+value #,tribute-name + [#,tribute-name valueᵢ])]]{ + Matches all of the given @racket[#,ntax-pattern]s, like @racket[~and] does, and + perform a global @racket[or] over all the values corresponding to successful + matches of a global pattern using the same @racket[#,tribute-name]. See above + for a description of how global operations work. + + If the @racket[valueᵢ] is omitted, @racket[#t] is used as a default. + + The result is always transformed into a boolean, so @racket[_attribute-name] is + always bound to either @racket[#t] or @racket[#f].} + +@defform[#:kind "eh-mixin expander" + (~global-and attribute-name+value #,ntax-pattern ...) + #:grammar + [(attribute-name+value [#,tribute-name valueᵢ])]]{ + Matches all of the given @racket[#,ntax-pattern]s, like @racket[~and] does, and + perform a global @racket[and] over all the values corresponding to successful + matches of a global pattern using the same @racket[#,tribute-name]. See above + for a description of how global operations work. + + If there is at least one occurrence of @racket[~global-and] for that + @racket[_attribute-name] which successfully matches, the result of the + @racket[(and valueᵢ ...)] is always coerced to a boolean, so + @racket[_attribute-name] is always bound to either @racket[#t] or @racket[#f]. + + If there are no matches at all, the special value @racket['none] is used + instead of @racket[#t] as would be produced by @racket[(and)].} + +@defform[#:kind "eh-mixin expander" + (~global-counter attribute-name+value #,ntax-pattern ...) + #:grammar + [(attribute-name+value #,tribute-name + [#,tribute-name valueᵢ])]]{ + Matches all of the given @racket[#,ntax-pattern]s, like @racket[~and] does, and + perform a global @racket[+] over all the values corresponding to successful + matches of a global pattern using the same @racket[#,tribute-name]. See above + for a description of how global operations work. + + If the @racket[valueᵢ] is omitted, @racket[1] is used as a default.} + +@;@defform[(aggregate-global-or)] +@;@defform[(aggregate-global-and)] +@;@defform[(aggregate-global-counter)] diff --git a/scribblings/misc.scrbl b/scribblings/misc.scrbl new file mode 100644 index 0000000..21a524b --- /dev/null +++ b/scribblings/misc.scrbl @@ -0,0 +1,18 @@ +#lang scribble/manual +@require[scribble/example + "utils.rkt" + @for-label[phc-toolkit/untyped + extensible-parser-specifications + generic-syntax-expanders + racket/base + syntax/parse + (only-in racket/base [... …])]] + +@title{Miscellaneous pattern expanders} + +@defform[#:kind "pattern expander" + {~nop}]{ + The @tech[#:doc '(lib "syntax/scribblings/syntax.scrbl") + #:key "action pattern"]{A-pattern} @racket[~nop] does not perform any + action. It simply expands to @racket[{~do}]. +} diff --git a/scribblings/no-order.scrbl b/scribblings/no-order.scrbl new file mode 100644 index 0000000..66a4de8 --- /dev/null +++ b/scribblings/no-order.scrbl @@ -0,0 +1,192 @@ +#lang scribble/manual +@require[scribble/example + "utils.rkt" + @for-label[phc-toolkit/untyped + extensible-parser-specifications + generic-syntax-expanders + racket/base + syntax/parse + (only-in racket/base [... …])]] + +@title{Matching alternatives in any order} + +@defform[#:kind "pattern expander" + #:literals (~mixin ~or) + (~seq-no-order clause-or-mixin ...) + #:grammar + [(clause-or-mixin #,ntax-pattern + (~mixin #,-alternative-mixin) + (~or clause-or-mixin ...) + derived-or)]]{ + Splicing pattern which matches the given @racket[clause-or-mixin]s in any + order, enforcing the global constraints expressed within each. + + Nested @racket[~or] directly below @racket[~seq-no-order] are recursively + inlined. In other words, the @racket[~or] present directly below the + @racket[~seq-no-order] or below such an @racket[~or] clause do not behave as + "exclusive or", but instead contain clauses which can appear in any order. + These clauses are not grouped in any way by the @racket[~or], i.e. + @racket[(~no-order (~or (~or a b) (~or c d)))] is equivalent to + @racket[(~no-order a b c d)]. + + The @racket[derived-or] term covers any + @tech[#:doc '(lib "syntax/scribblings/syntax.scrbl")]{pattern expander} or + @tech{eh-mixin expander} application which expands to a + @racket[clause-or-mixin]. The expansion of pattern and eh-mixin expanders + happens before inlining the top @racket[~or] clauses.} + +@defform[#:kind "pattern expander" + #:literals (~mixin ~or) + (~no-order clause-or-mixin ...) + #:grammar + [(clause-or-mixin #,ntax-pattern + (~mixin #,-alternative-mixin) + (~or clause-or-mixin ...) + derived-or)]]{ + + Like @racket[~seq-no-order], except that it matches a syntax list, instead of + being spliced into the surrounding sequence of patterns. In other words, + + @racketblock[(~seq-no-order clause-or-mixin ...)] + + Equivalent to (notice the extra pair of braces): + + @racketblock[({~seq-no-order clause-or-mixin ...})]} + +@section{Enforcing a partial order on the alternatives} + +@defform[#:kind "eh-mixin expander" + (~order-point point-name #,ntax-pattern ...)]{ + When parsing a sequence of elements, @racket[~seq-no-order] and + @racket[~no-order] associate an increasing number to each element starting from + zero. + + The number associated with the first element matched by + @racket[#,ntax-pattern ...] is memorised into the attribute + @racket[point-name]. + + This allows the position of elements matched by otherwise independent mixins to + be compared using @racket[order-point<] and @racket[order-point>]} + +@defform[(order-point< a b) + #:grammar + [(a #,tribute-name) + (b #,tribute-name)]]{ + Returns @racket[#t] when the first element matched by + @racket[(~order-point a #,ntax-pattern ...)] occurs before the first element + matched by @racket[(~order-point b #,ntax-pattern ...)]. Otherwise, returns + @racket[#f]. + + This operation does not fail if @racket[a] or @racket[b] are bound to + @racket[#f] (i.e. their corresponding @racket[_syntax-pattern ...] did not + match). Instead, in both cases, it returns @racket[#f].} + +@defform[(order-point> a b) + #:grammar + [(a #,tribute-name) + (b #,tribute-name)]]{ + Returns @racket[#t] when the first element matched by + @racket[(~order-point a #,ntax-pattern ...)] occurs after the first element + matched by @racket[(~order-point b #,ntax-pattern ...)]. Otherwise, returns + @racket[#f]. + + This operation does not fail if @racket[a] or @racket[b] are bound to + @racket[#f] (i.e. their corresponding @racket[_syntax-pattern ...] did not + match). Instead, in both cases, it returns @racket[#f].} + +@defform[(try-order-point< a b) + #:grammar + [(a #,tribute-name) + (b #,tribute-name)]]{ + + Like @racket[order-point<], except that it does not fail if @racket[a] or + @racket[b] are not attributes, or if they are bound to @racket[#f]. Instead, in + all those cases, it returns @racket[#f]. + + It can be used as follows: + + @racketblock[ + (~post-fail "a must appear after b" + #:when (try-order-point< a b))] + + The same caveats as for @racket[try-attribute] apply.} + +@defform[(try-order-point> a b) + #:grammar + [(a #,tribute-name) + (b #,tribute-name)]]{ + + Like @racket[order-point>], except that it does not fail if @racket[a] or + @racket[b] are not attributes, or if they are bound to @racket[#f]. Instead, in + all those cases, it returns @racket[#f]. + + It can be used as follows: + + @racketblock[ + (~post-fail "a must appear before b" + #:when (try-order-point> a b))] + + The same caveats as for @racket[try-attribute] apply.} + +@defform[#:kind "eh-mixin-expander" + (~before other message pat ...)]{ + + Post-checks that the first element matched by @racket[pat ...] appears before + the @racket[other] order-point. This is a shorthand for: + + @racketblock[{~order-point pt + {~seq pat ...} + {~post-fail message #:when (order-point> pt other)}}] + + Note: Hopefully @racket[~before] will be modified in the future so that it + auto-detects if the @racket[other] order-point is not defined as part of the + current @racket[~no-order]. Do not rely on comparisons with order points + somehow defined outside the current @racket[~no-order], as that behaviour may + change in the future.} + +@defform[#:kind "eh-mixin-expander" + (~after other message pat ...)]{ + Post-checks that the first element matched by @racket[pat ...] appears after + the @racket[other] order-point. This is a shorthand for: + + @racketblock[{~order-point pt + {~seq pat ...} + {~post-fail message #:when (order-point< pt other)}}] + + Note: Hopefully @racket[~after] will be modified in the future so that it + auto-detects if the @racket[other] order-point is not defined as part of the + current @racket[~no-order]. Do not rely on comparisons with order points + somehow defined outside the current @racket[~no-order], as that behaviour may + change in the future.} + +@defform[#:kind "eh-mixin-expander" + (~try-before other message pat ...)]{ + + Post-checks that the first element matched by @racket[pat ...] appears before + the @racket[other] order-point. The @racket[try-] version does not cause an + error if the order-point @racket[other] is not define (e.g. it was part of + another mixin which was not included). This is a shorthand for: + + @racketblock[{~order-point pt + {~seq pat ...} + {~post-fail message #:when (try-order-point> pt other)}}] + + Note: Hopefully @racket[~before] will be modified in the future so that it + auto-detects if the @racket[other] order-point is missing. This form will then + be removed.} + +@defform[#:kind "eh-mixin-expander" + (~try-after other message pat ...)]{ + Post-checks that the first element matched by @racket[pat ...] appears after + the @racket[other] order-point. The @racket[try-] version does not cause an + error if the order-point @racket[other] is not define (e.g. it was part of + another mixin which was not included). This is a shorthand for: + + @racketblock[{~order-point pt + {~seq pat ...} + {~post-fail message #:when (try-order-point< pt other)}}] + + Note: Hopefully @racket[~after] will be modified in the future so that it + auto-detects if the @racket[other] order-point is missing. This form will then + be removed.} + diff --git a/scribblings/post.scrbl b/scribblings/post.scrbl new file mode 100644 index 0000000..6522ac3 --- /dev/null +++ b/scribblings/post.scrbl @@ -0,0 +1,40 @@ +#lang scribble/manual +@require[scribble/example + "utils.rkt" + @for-label[phc-toolkit/untyped + extensible-parser-specifications + generic-syntax-expanders + racket/base + syntax/parse + (only-in racket/base [... …])]] + +@title{Post operations} + +@defform*[#:kind "eh-mixin expander" + [(~post-check #,ntax-pattern #,A-patte) + (~post-check #,A-patte)]]{ + Matches @racket[#,ntax-pattern], and executes the given @racket[#,A-patte] + after the whole @racket[~seq-no-order] or @racket[~no-order] finished matching + its contents. + + If unspecified, the @racket[_syntax-pattern] defaults to @racket[(~nop)].} + +@defform*[#:kind "eh-mixin expander" + [(~post-fail message #:when condition) + (~post-fail #:when condition message) + (~post-fail message #:unless unless-condition) + (~post-fail #:unless unless-condition message)]]{ + + After the whole @racket[~seq-no-order] or @racket[~no-order] finished matching + its contents, checks whether @racket[condition] or @racket[unless-condition] is + true or false, respectively. If this is the case the whole + @racket[~seq-no-order] or @racket[~no-order] is rejected with the given + @racket[_message]. + + Note that there is an implicit cut (@racket[~!]) between the no-order patterns + and the "post" checks, so after a @racket[~post-fail] fails, + @racket[syntax-parse] does not backtrack and attempt different combinations of + patterns to match the sequence, nor does it backtrack and attempt to match a + shorter sequence. This is by design, as it allows for better error messages + (syntax-parse would otherwise attempt and possibly succeed in matching a + shorter sequence, then just treat the remaining terms as "unexpected terms").} diff --git a/scribblings/pre-global-post-order.scrbl b/scribblings/pre-global-post-order.scrbl new file mode 100644 index 0000000..3d1418e --- /dev/null +++ b/scribblings/pre-global-post-order.scrbl @@ -0,0 +1,82 @@ +#lang scribble/manual +@require[scribble/example + "utils.rkt" + @for-label[phc-toolkit/untyped + extensible-parser-specifications + generic-syntax-expanders + racket/base + syntax/parse + (only-in racket/base [... …])]] + +@title{Order in which the attributes are bound for post operations and + global operations} + +Within the @racket[_A-pattern]s of post operations, the regular attributes bound +by all the clauses inside @racket[~seq-no-order] or @racket[~no-order] are +bound. The attributes defined as part of all "global" actions are bound too. The +attributes defined as part of "post" actions of other clauses are bound only if +the clause defining them appears before the current clause in the source code. +For example, the following code works because the clause containing +@racket[{~post-fail "2 is incompatible with 1" #:when (not (attribute a))}] +appears after the clause which binds @racket[a] with the "post" action +@racket[{~post-check {~bind ([a #'the-a])}}]. + +@racketblock[ + {~seq-no-order + {~post-check {~and the-a 1} {~bind ([a #'the-a])}} + {~and 2 {~post-fail "2 is incompatible with 1" #:when (not (attribute a))}}}] + +If the two clauses are swapped, then the following code would raise a syntax +error because @racket[a] is not bound as an attribute in the +@racket[~post-fail]: + +@racketblock[ + {~seq-no-order + {~and 2 {~post-fail "2 is incompatible with 1" #:when (not (attribute a))}} + {~post-check {~and the-a 1} {~bind ([a #'the-a])}}}] + +On the other hand, the following code, which does not bind @racket[a] as part +of a post operation, is valid: + +@racketblock[ + {~seq-no-order + {~and 2 {~post-fail "2 is incompatible with 1" #:when (not (attribute a))}} + {~and the-a 1 {~bind ([a #'the-a])}}}] + +Furthermore, the following code still works, as attributes are bound by the +"global" operations before the "post" operations are executed: + +@racketblock[ + {~seq-no-order + {~and 2 {~post-fail "2 is incompatible with 1" #:when (not (attribute a))}} + {~global-or a 1}}] + +Note that the order in which clauses appear within the @racket[~seq-no-order] +or @racket[~no-order] does not impact the order in which the elements must +appear in the matched syntax (aside from issues related to greediness). + +@defform[(try-attribute #,tribute-name)]{ + This macro expands to @racket[(attribute #,tribute-name)] if + @racket[#,tribute-name] is bound as a syntax pattern variable, and to + @racket[#f] otherwise. + + This macro can be used to check for mutual exclusion of an attribute which is + bound by other mixins that might or might not be present in the final + @racket[~no-order] or @racket[~seq-no-order]. + + Use this sparingly, as if an syntax pattern variable with that name is bound by + an outer scope, the @racket[try-attribute] macro will still access it, ignorant + of the fact that the current @racket[~seq-no-order] does not contain any mixin + which binds that attribute. + + Instead, it is better practice to use + @racket[{~global-or [_attribute-name #f]}] or + @racket[{~global-and [_attribute-name #t]}] to ensure that the attribute is + declared, while using the operation's neutral element to not alter the final + result.} + +@defform[(if-attribute #,tribute-name if-branch else-branch)]{ + This macro expands to @racket[if-branch] if @racket[#,tribute-name] is bound as + a syntax pattern variable, and to @racket[else-branch] otherwise. + + The same caveats as for @racket[try-attribute] apply.} \ No newline at end of file diff --git a/scribblings/pre-global-post-section.scrbl b/scribblings/pre-global-post-section.scrbl new file mode 100644 index 0000000..b6c5d03 --- /dev/null +++ b/scribblings/pre-global-post-section.scrbl @@ -0,0 +1,23 @@ +#lang scribble/manual + +@title{Post operations and global operations} + +Pre operations happen before the @racket[~!] backtracking cut, so they can +affect what combination of alternative clauses the parser will choose. Post +operations happen after the @racket[~!] backtracking cut, and can only reject +the @racket[~no-order] or @racket[~seq-no-order] as a whole (i.e. different +orders will not be attempted after a @racket[~post-fail]. Global operations will +always succeed. + +Post operations can access the attributes defined by global and pre operations +as well as attributes defined by the alternative clauses. Global operations +cannot access the attributes of post operations, and pre operations cannot +access the attributes of global and post operations. See +@secref["Order_in_which_the_attributes_are_bound_for_post_operations_and_global_operations" + #:doc '(lib "extensible-parser-specifications/scribblings/extensible-parser-specifications.scrbl")] +for more details. + +@include-section{pre.scrbl} +@include-section{global.scrbl} +@include-section{post.scrbl} +@include-section{pre-global-post-order.scrbl} diff --git a/scribblings/pre.scrbl b/scribblings/pre.scrbl new file mode 100644 index 0000000..c9c686b --- /dev/null +++ b/scribblings/pre.scrbl @@ -0,0 +1,75 @@ +#lang scribble/manual +@require[scribble/example + "utils.rkt" + @for-label[phc-toolkit/untyped + extensible-parser-specifications + generic-syntax-expanders + racket/base + syntax/parse + (only-in racket/base [... …])]] + +@title{Pre operations} + +@defform[#:kind "eh-mixin expander" + (~named-seq #,tribute-name #,ntax-pattern ...)]{ + Equivalent to @racket[{~seq #,ntax-pattern ...}], but also binds the + @racket[#,tribute-name] to the whole sequence. If the sequence appears inside + an @racket[~optional] or @racket[~or] clause that fails, the + @racket[_attribute-name] is still bound to the empty sequence. + + Known issues: this may not behave as expected if @racket[~named-seq] appears + under ellipses. + + This probably should bind the sequence attribute @emph{before} the "global" + operations, instead of being a "post" operation, and may be changed in that way + the future.} + +@defform[#:kind "eh-mixin expander" + (~maybe/empty #,ntax-pattern ...)]{ + + Optionally matches @racket[{~seq #,ntax-pattern ...}]. If the match fails, it + matches these same sequence of patterns against the empty syntax list + @racket[#'()]. This form can be used in an ellipsis-head position. This is + implemented in both cases as a "pre" action.} + +@defform[#:kind "eh-mixin expander" + (~optional/else #,ntax-pattern + maybe-defaults + else-post-fail ... + maybe-name) + #:grammar + [(maybe-defaults (code:line) + (code:line #:defaults (default-binding ...))) + (else-post-fail + (code:line #:else-post-fail message #:when condition) + (code:line #:else-post-fail #:when condition message) + (code:line #:else-post-fail message #:unless unless-condition) + (code:line #:else-post-fail #:unless unless-condition message)) + (maybe-name (code:line) + (code:line #:name #,tribute-name))]]{ + Like @racket[~optional], but with conditional post-failures when the pattern is + not matched. An @racket[~optional/else] pattern can be matched zero or one time + as part of the @racket[~seq-no-order] or @racket[~no-order]. When it is not + matched (i.e. matched zero times): + @itemlist[ + @item{it uses the default values for the attributes as specified with + @racket[#:defaults].} + @item{for each @racket[#:else-post-fail] clause, it checks whether the + @racket[condition] or @racket[unless-condition] is true or false, + respectively. If this is the case the whole @racket[~seq-no-order] or + @racket[~no-order] is rejected with the given @racket[_message]. The + behaviour of @racket[#:else-post-fail] is the same as the behaviour of + @racket[~post-fail], except that the "post" conditional failure can only be + executed if the optional @racket[_syntax-pattern] was not matched. + + Note that there is an implicit cut (@racket[~!]) between the no-order + patterns and the "post" checks, so after a @racket[~post-fail] fails, + @racket[syntax-parse] does not backtrack and attempt different combinations + of patterns to match the sequence, nor does it backtrack and attempt to match + a shorter sequence. This is by design, as it allows for better error messages + (syntax-parse would otherwise attempt and possibly succeed in matching a + shorter sequence, then just treat the remaining terms as + "unexpected terms").}] + + The meaning of @racket[#:name #,tribute-name] option is the same as for + @racket[~optional].} diff --git a/scribblings/rest.scrbl b/scribblings/rest.scrbl new file mode 100644 index 0000000..15d5360 --- /dev/null +++ b/scribblings/rest.scrbl @@ -0,0 +1,67 @@ +#lang scribble/manual +@require[scribble/example + "utils.rkt" + @for-label[phc-toolkit/untyped + extensible-parser-specifications + generic-syntax-expanders + racket/base + syntax/parse + (only-in racket/base [... …])]] + +@title{Parsing the tail of improper lists} + +@defform[#:kind "eh-mixin expander" + {~lift-rest pat}]{ + + Lifts @racket[pat] out of the current mixin, so that it is used as a pattern to + match the tail of the improper list being matched. It is subject to the + following restrictions: + @itemlist[ + @item{@racket[~lift-rest] is allowed only within @racket[~no-order], but not + within @racket[~seq-no-order]. @racket[~seq-no-order] always matches against + a proper sequence of elements, while @racket[~no-order] may match a proper or + improper list.} + @item{The tail of the improper list must not be a pair, otherwise the + @racket[car] would have been included in the main part of the list.} + @item{The @racket[pat] is used to match the tail only if its surrounding + pattern successfully matched some elements of the main section of the list. + + If the @racket[{~lift-rest pat}] is the only pattern present within an + alternative, then it is always used. + + @examples[#:eval (make-evaluator) + (syntax-parse #'(x y z . 1) + [(~no-order {~lift-rest r:nat} i:id) + (syntax->datum #'(r i ...))])]} + @item{ + Among the lifted rest patterns which are considered (see the point + above), only one may successfully match. An error is raised if two or more + lifted rest patterns successfully match against the tail of the list. + + @examples[#:eval (make-evaluator) + (eval:no-prompt + (define p + (syntax-parser + [(~no-order {~and {~literal x} + {~lift-rest rn:nat} + {~lift-rest ri:id}} + {~and {~literal y} + {~lift-rest rs:str} + {~lift-rest rj:id}}) + 'match] + #;[_ + 'fail]))) + (code:line (p #'(x . 1)) (code:comment "rn and ri considered, rn matched")) + (code:line (p #'(x . z)) (code:comment "rn and ri considered, ri matched")) + (code:line (p #'(y . "a")) (code:comment "rs and rj considered, rs matched")) + (code:line (p #'(y . z)) (code:comment "rs and rj considered, rj matched")) + (code:line (p #'(x y . 1)) (code:comment "all four considered, rn matched")) + (eval:alts (code:line (p #'(x y . z)) (code:comment "all four considered, both ri and rj matched")) + (eval:error (p #'(x y . z))))] + + The rationale is that selecting the first lifted rest pattern that matches + would result in unclear behaviour, as the order of the alternative clauses + should not be significant.} + @item{Post and global operations can be used within the @racket[pat]. This + combination of features is not thoroughly tested, however. Please report any + issues you run into.}]} diff --git a/scribblings/utils.rkt b/scribblings/utils.rkt new file mode 100644 index 0000000..ed87d93 --- /dev/null +++ b/scribblings/utils.rkt @@ -0,0 +1,26 @@ +#lang racket + +(require scribble/manual + scribble/example) + +(provide (all-defined-out)) + +(define ntax-pattern (tech #:doc '(lib "syntax/scribblings/syntax.scrbl") + #:key "syntax pattern" + "syntax-pattern")) + +(define -alternative-mixin (tech #:key "eh-alternative mixin" + #:doc '(lib "extensible-parser-specifications/scribblings/extensible-parser-specifications.scrbl") + "eh-alternative-mixin")) + +(define tribute-name (tech #:doc '(lib "syntax/scribblings/syntax.scrbl") + #:key "attribute" + "attribute-name")) + +(define A-patte (tech #:doc '(lib "syntax/scribblings/syntax.scrbl") + #:key "action pattern" + "A-pattern")) + +(define make-evaluator + (make-eval-factory '(syntax/parse + extensible-parser-specifications))) diff --git a/test/test-order-point.rkt b/test/test-order-point.rkt index 52a33be..375a698 100644 --- a/test/test-order-point.rkt +++ b/test/test-order-point.rkt @@ -40,5 +40,5 @@ (check-fail-abc #'(#:c #:b #:a) #px"#:a must appear after #:b") (check-fail-abc #'(#:b #:c #:a) #px"#:a must appear after #:b") (check-fail-abc #'(#:b #:a #:c) #px"#:a must appear after #:b") -(check-fail-abc #'(#:a #:a) #px"unexpected term") -(check-fail-abc #'(#:c #:c) #px"unexpected term") \ No newline at end of file +(check-fail-abc #'(#:a #:a) #px"expected abc-order") +(check-fail-abc #'(#:c #:c) #px"expected abc-order") \ No newline at end of file diff --git a/test/test-rest.rkt b/test/test-rest.rkt new file mode 100644 index 0000000..367ef02 --- /dev/null +++ b/test/test-rest.rkt @@ -0,0 +1,88 @@ +#lang racket + +(require extensible-parser-specifications + extensible-parser-specifications/private/no-order + racket/require + syntax/parse + (subtract-in syntax/stx phc-toolkit/untyped) + rackunit + racket/format + phc-toolkit/untyped + (for-syntax syntax/parse + syntax/stx + racket/format)) + +(check-equal? + (syntax-parse #'(1 "ab" . #:kw) + [(~no-order {~or {~lift-rest {~and k #:kw}} + {~once n:nat}} + {~once s:str}) + (syntax->datum #'(k n s))]) + '(#:kw 1 "ab")) + + +(check-equal? + (syntax-parse #'(1 "ab" . #:kw) + [(~no-order {~lift-rest {~and k #:kw}} + {~once n:nat} + {~once s:str}) + (syntax->datum #'(k n s))]) + '(#:kw 1 "ab")) + +(check-equal? + (syntax-parse #'(1 "ab" . #:kw) + [(~no-order {~once {~and n:nat + {~lift-rest {~and k #:kw}}}} + {~once s:str}) + (syntax->datum #'(k n s))]) + '(#:kw 1 "ab")) + +(check-equal? + (syntax-parse #'(1 "ab" . #:kw) + [(~no-order {~once n:nat} + {~lift-rest {~and k #:kw}} + {~once s:str}) + (syntax->datum #'(k n s))] + [_ #f]) + '(#:kw 1 "ab")) + +(test-begin + "Exactly the same as above, but with the post-fail" + (check-false + (syntax-parse #'(1 "ab" . #:kw) + [(~no-order {~once n:nat} + {~lift-rest {~and k #:kw + {~post-fail "e" #:when (= (syntax-e #'n) 1)}}} + {~once s:str}) + (syntax->datum #'(k n s))] + [_ #f]))) + +(test-begin + "Exactly the same as above, but with a different value (2 instead of 1)" + (check-equal? + (syntax-parse #'(2 "ab" . #:kw) + [(~no-order {~once n:nat} + {~lift-rest {~and k #:kw + {~post-fail "e" #:when (= (syntax-e #'n) 1)}}} + {~once s:str}) + (syntax->datum #'(k n s))] + [_ #f]) + '(#:kw 2 "ab"))) + +(define p + (syntax-parser + [(~no-order {~and {~literal x} + {~lift-rest rn:nat} + {~lift-rest ri:id}} + {~and {~literal y} + {~lift-rest rs:str} + {~lift-rest rj:id}}) + 'match])) + +(check-equal? (p #'(x . 1)) 'match) +(check-equal? (p #'(x . z)) 'match) +(check-equal? (p #'(y . "a")) 'match) +(check-equal? (p #'(y . z)) 'match) +(check-equal? (p #'(x y . 1)) 'match) +(check-exn #px"more than one of the lifted rest patterns matched" + (λ () (p #'(x y . z)))) \ No newline at end of file