Implemented and documented: pre operations, ~lift-rest.

This commit is contained in:
Georges Dupéron 2016-09-22 22:33:31 +02:00
parent f030c3ffbe
commit 01c9b553a5
20 changed files with 1164 additions and 579 deletions

View File

@ -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

View File

@ -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))
(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))])))

10
private/nop.rkt Normal file
View File

@ -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))))

View File

@ -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))))))
(string->symbol (format " -clause-~a " ((clause-counter))))))

View File

@ -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)

72
private/pre.rkt Normal file
View File

@ -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 (... ...)}}})])))

View File

@ -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).}

View File

@ -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}

View File

@ -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)]}

97
scribblings/global.scrbl Normal file
View File

@ -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)]

18
scribblings/misc.scrbl Normal file
View File

@ -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}].
}

192
scribblings/no-order.scrbl Normal file
View File

@ -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.}

40
scribblings/post.scrbl Normal file
View File

@ -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").}

View File

@ -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.}

View File

@ -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}

75
scribblings/pre.scrbl Normal file
View File

@ -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].}

67
scribblings/rest.scrbl Normal file
View File

@ -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.}]}

26
scribblings/utils.rkt Normal file
View File

@ -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)))

View File

@ -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")
(check-fail-abc #'(#:a #:a) #px"expected abc-order")
(check-fail-abc #'(#:c #:c) #px"expected abc-order")

88
test/test-rest.rkt Normal file
View File

@ -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))))