diff --git a/collects/syntax/parse/private/parse.rkt b/collects/syntax/parse/private/parse.rkt index 5cb4cd6f0a..47855ce558 100644 --- a/collects/syntax/parse/private/parse.rkt +++ b/collects/syntax/parse/private/parse.rkt @@ -696,7 +696,7 @@ Conventions: (for/fold ([k #'k]) ([side (in-list (reverse (syntax->list #'(side ...))))]) (syntax-case side () [#s(clause:attr a expr) - #`(let-attributes ([a (wrap-user-code (check-list^depth a expr))]) + #`(let-attributes ([a (wrap-user-code expr)]) #,k)]))])) (begin-for-syntax diff --git a/collects/syntax/parse/private/residual.rkt b/collects/syntax/parse/private/residual.rkt index 10fcb522b6..685fa236a8 100644 --- a/collects/syntax/parse/private/residual.rkt +++ b/collects/syntax/parse/private/residual.rkt @@ -1,7 +1,8 @@ #lang racket/base (require (for-syntax racket/base) racket/stxparam - racket/lazy-require) + racket/lazy-require + racket/private/promise) ;; ============================================================ ;; Compile-time @@ -33,10 +34,11 @@ [else #f])) (attribute-mapping-name self))]) #`(let ([value #,(attribute-mapping-var self)]) - (check-attr-value-is-syntax '#,(attribute-mapping-depth self) - value - (quote-syntax #,source-name)) - value))))) + (if (syntax-list^depth? '#,(attribute-mapping-depth self) value) + value + (check/force-syntax-list^depth '#,(attribute-mapping-depth self) + value + (quote-syntax #,source-name)))))))) ) ;; ============================================================ @@ -54,7 +56,6 @@ attribute-binding stx-list-take stx-list-drop/cx - check-list^depth* check-literal* begin-for-syntax/once @@ -131,30 +132,45 @@ (if (syntax? x) x cx) (sub1 n))))) -;; check-attr-value-is-syntax : nat any id -> boolean -;; returns #t if value is a (listof^depth syntax) -;; used by attribute-mapping code above -(define (check-attr-value-is-syntax depth value source-id) - (define (check-syntax depth value) - (if (zero? depth) - (syntax? value) - (and (list? value) - (for/and ([part (in-list value)]) - (check-syntax (sub1 depth) part))))) - (unless (check-syntax depth value) +;; check/force-syntax-list^depth : nat any id -> (listof^depth syntax) +;; Checks that value is (listof^depth syntax); forces promises. +;; Slow path for attribute-mapping code, assumes value is not syntax-list^depth? already. +(define (check/force-syntax-list^depth depth value0 source-id) + (define (bad) (raise-syntax-error #f - (format "attribute is bound to non-syntax value: ~e" value) - source-id))) + (format "attribute is bound to non-syntax value: ~e" value0) + source-id)) + (define (loop depth value) + (cond [(promise? value) + (loop depth (force value))] + [(zero? depth) + (if (syntax? value) value (bad))] + [else (loop-list depth value)])) + (define (loop-list depth value) + (cond [(promise? value) + (loop-list depth (force value))] + [(pair? value) + (let ([new-car (loop (sub1 depth) (car value))] + [new-cdr (loop-list depth (cdr value))]) + ;; Don't copy unless necessary + (if (and (eq? new-car (car value)) + (eq? new-cdr (cdr value))) + value + (cons new-car new-cdr)))] + [(null? value) + null] + [else + (bad)])) + (loop depth value0)) -;; check-list^depth* : symbol nat any -> list^depth -(define (check-list^depth* aname n0 v0) - (define (loop n v) - (when (positive? n) - (unless (list? v) - (raise-type-error aname (format "lists nested ~s deep" n0) v)) - (for ([x (in-list v)]) (loop (sub1 n) x)))) - (loop n0 v0) - v0) +;; syntax-list^depth? : nat any -> boolean +;; Returns true iff value is (listof^depth syntax). +(define (syntax-list^depth? depth value) + (if (zero? depth) + (syntax? value) + (and (list? value) + (for/and ([part (in-list value)]) + (syntax-list^depth? (sub1 depth) part))))) ;; check-literal* : id phase phase (listof phase) stx -> void (define (check-literal* id used-phase mod-phase ok-phases/ct-rel ctx) diff --git a/collects/syntax/parse/private/runtime.rkt b/collects/syntax/parse/private/runtime.rkt index 45743813ac..f07ba0d8f2 100644 --- a/collects/syntax/parse/private/runtime.rkt +++ b/collects/syntax/parse/private/runtime.rkt @@ -24,7 +24,6 @@ let/unpack defattrs/unpack - check-list^depth check-literal no-shadow @@ -142,14 +141,6 @@ residual.rkt. (define-syntax name (make-syntax-mapping 'depth (quote-syntax stmp))) ...)))])) -;; (check-list^depth attr expr) -(define-syntax (check-list^depth stx) - (syntax-case stx () - [(_ a expr) - (with-syntax ([#s(attr name depth syntax?) #'a]) - (quasisyntax/loc #'expr - (check-list^depth* 'name 'depth expr)))])) - ;; (check-literal id phase-level-expr ctx) -> void (define-syntax (check-literal stx) (syntax-case stx () diff --git a/collects/syntax/scribblings/parse/parse-common.rkt b/collects/syntax/scribblings/parse/parse-common.rkt index 7ffb29a523..b113e941de 100644 --- a/collects/syntax/scribblings/parse/parse-common.rkt +++ b/collects/syntax/scribblings/parse/parse-common.rkt @@ -31,7 +31,8 @@ (lambda (e) (raise (fixup e)))]) (thunk))))]) (make-evaluator 'racket/base - #:requires (let ([mods '(syntax/parse + #:requires (let ([mods '(racket/promise + syntax/parse syntax/parse/debug syntax/parse/experimental/splicing syntax/parse/experimental/contract diff --git a/collects/syntax/scribblings/parse/stxclasses.scrbl b/collects/syntax/scribblings/parse/stxclasses.scrbl index ed2189aa4b..d672545adc 100644 --- a/collects/syntax/scribblings/parse/stxclasses.scrbl +++ b/collects/syntax/scribblings/parse/stxclasses.scrbl @@ -215,7 +215,8 @@ clause to be tried multiple times before the next clause is reached. Evaluates the @racket[expr] in the context of all previous attribute bindings and binds it to the given attribute. The value of -@racket[expr] need not be syntax. +@racket[expr] need not be, or even contain, syntax---see +@racket[attribute] for details. } @specsubform[(code:line #:fail-when condition-expr message-expr) @@ -261,7 +262,7 @@ in a @racket[#:do] block. @;{----------} -@section{Pattern Variables and Attributes} +@section[#:tag "stxparse-attrs"]{Pattern Variables and Attributes} An @deftech{attribute} is a name bound by a syntax pattern. An attribute can be a @tech{pattern variable} itself, or it can be a @@ -270,58 +271,92 @@ variable}. The name of a nested attribute is computed by concatenating the pattern variable name with the syntax class's exported attribute's name, separated by a dot (see the example below). -Attribute names cannot be used directly as expressions; that is, -attributes are not variables. Instead, an attribute's value can be -gotten using the @racket[attribute] special form. +Attributes can be used in two ways: with the @racket[attribute] form, +and inside syntax templates via @racket[syntax], @racket[quasisyntax], +etc. Attribute names cannot be used directly as expressions; that is, +attributes are not variables. -@defform[(attribute attr-id)]{ +A @deftech{syntax-valued attribute} is an attribute whose value is a +syntax object, a syntax list of the appropriate @tech{ellipsis depth}, +or a tree containing @tech[#:doc '(lib +"scribblings/reference/reference.scrbl")]{promises} that when +completely forced produces a suitable syntax object or syntax +list. Syntax-valued attributes can be used within @racket[syntax], +@racket[quasisyntax], etc as part of a syntax template. If an +attribute is used inside a syntax template but it is not +syntax-valued, an error is signaled. -Returns the value associated with the attribute named -@racket[attr-id]. If @racket[attr-id] is not bound as an attribute, an -error is raised. -} - -The value of an attribute need not be syntax. Non-syntax-valued -attributes can be used to return a parsed representation of a subterm -or the results of an analysis on the subterm. A non-syntax-valued -attribute should be bound using the @racket[#:attr] directive or a -@racket[~bind] pattern. +The value of an attribute is not required to be syntax. +Non-syntax-valued attributes can be used to return a parsed +representation of a subterm or the results of an analysis on the +subterm. A non-syntax-valued attribute should be bound using the +@racket[#:attr] directive or a @racket[~bind] pattern; @racket[#:with] +and @racket[~parse] will convert the right-hand side to a (possibly +3D) syntax object. @myexamples[ (define-syntax-class table (pattern ((key value) ...) - #:attr hash + #:attr hashtable (for/hash ([k (syntax->datum #'(key ...))] [v (syntax->datum #'(value ...))]) - (values k v)))) -(syntax-parse #'((a 1) (b 2) (c 3)) - [t:table - (attribute t.hash)]) + (values k v)) + #:attr [sorted-kv 1] + (delay + (printf "sorting!\n") + (sort (syntax->list #'((key value) ...)) + < + #:key (lambda (kv) (cadr (syntax->datum kv))))))) ] -A syntax-valued attribute is an attribute whose value is a syntax -object or a syntax list of the appropriate @tech{ellipsis -depth}. Syntax-valued attributes can be used within @racket[syntax], -@racket[quasisyntax], etc as part of a syntax template. If a -non-syntax-valued attribute is used in a syntax template, a runtime -error is signaled. +The @racket[table] syntax class provides four attributes: +@racket[key], @racket[value], @racket[hashtable], and +@racket[sorted-kv]. The @racket[hashtable] attribute has +@tech{ellipsis depth} 0 and the rest have depth 1; all but +@racket[hashtable] are syntax-valued. The @racket[sorted-kv] +attribute's value is a promise; it will be automatically forced if +used in a syntax template. -@myexamples[ -(syntax-parse #'((a 1) (b 2) (c 3)) +Syntax-valued attributes can be used in syntax templates: + +@myinteraction[ +(syntax-parse #'((a 3) (b 2) (c 1)) [t:table #'(t.key ...)]) +(syntax-parse #'((a 3) (b 2) (c 1)) + [t:table + #'(t.sorted-kv ...)])] + +But non-syntax-valued attributes cannot: + +@myinteraction[ +(syntax-parse #'((a 3) (b 2) (c 1)) + [t:table + #'t.hashtable]) +] + +Use the @racket[attribute] form to get the value of an attribute +(syntax-valued or not). + +@myinteraction[ (syntax-parse #'((a 1) (b 2) (c 3)) [t:table - #'t.hash]) + (attribute t.hashtable)]) +(syntax-parse #'((a 3) (b 2) (c 1)) + [t:table + (attribute t.sorted-kv)]) ] Every attribute has an associated @deftech{ellipsis depth} that determines how it can be used in a syntax template (see the discussion of ellipses in @racket[syntax]). For a pattern variable, the ellipsis depth is the number of ellipses the pattern variable ``occurs under'' -in the pattern. For a nested attribute the depth is the sum of the -pattern variable's depth and the depth of the attribute in the syntax -class. Consider the following code: +in the pattern. An attribute bound by @racket[#:attr] has depth 0 +unless declared otherwise. For a nested attribute the depth is the sum +of the annotated pattern variable's depth and the depth of the +attribute exported by the syntax class. + +Consider the following code: @racketblock[ (define-syntax-class quark @@ -341,11 +376,20 @@ binds the following nested attributes: @racket[y.a] at depth 2, depth 1. An attribute's ellipsis nesting depth is @emph{not} a guarantee that -its value has that level of list nesting. In particular, @racket[~or] -and @racket[~optional] patterns may result in attributes with fewer -than expected levels of list nesting. +it is syntax-valued. In particular, @racket[~or] and +@racket[~optional] patterns may result in attributes with fewer than +expected levels of list nesting, and @racket[#:attr] and +@racket[~bind] can be used to bind attributes to arbitrary values. -@(myexamples - (syntax-parse #'(1 2 3) - [(~or (x:id ...) _) - (attribute x)])) +@myexamples[ +(syntax-parse #'(a b 3) + [(~or (x:id ...) _) + (attribute x)]) +] + +@defform[(attribute attr-id)]{ + +Returns the value associated with the @tech{attribute} named +@racket[attr-id]. If @racket[attr-id] is not bound as an attribute, an +error is raised. +} diff --git a/collects/tests/stxparse/test.rkt b/collects/tests/stxparse/test.rkt index 161da86ad7..e28095e9c5 100644 --- a/collects/tests/stxparse/test.rkt +++ b/collects/tests/stxparse/test.rkt @@ -349,6 +349,57 @@ [ns (void)]))) (void)) +;; lazy attributes + +(test-case "lazy syntax-valued attributes" + (let ([counter 0]) + (define-syntax-class foo + (pattern n:nat + #:attr 2n + (delay + (set! counter (add1 counter)) + (datum->syntax #'n (* 2 (syntax-e #'n)))))) + (syntax-parse #'45 + [x:foo + (check-equal? counter 0) ;; hasn't run yet + (attribute x.2n) + (check-pred promise? (attribute x.2n)) + (check-equal? counter 0) ;; still hasn't run yet + #'(lambda (q) x.2n) + (check-equal? counter 1) ;; run + #'(lambda (q) x.2n) + (force (attribute x.2n)) + (check-equal? counter 1) ;; still only run once + (void)]))) + +(test-case "lazy syntax-valued attributes, lists" + ;; check both (promiseof (listof syntax)) and (listof (promiseof syntax)) work + (let ([counter 0]) + (define-syntax-class foo + (pattern (x:id ...) + #:attr [alpha 1] + (delay (set! counter (add1 counter)) + (filter (lambda (x) + (regexp-match #rx"^[a-z]+$" (symbol->string (syntax-e x)))) + (syntax->list #'(x ...)))) + #:attr [alpha-part 1] + (map (lambda (x) + (delay + (set! counter (add1 counter)) + (datum->syntax #f + (car (regexp-match #rx"^[a-z]+" (symbol->string (syntax-e x))))))) + (syntax->list #'(x ...))))) + (syntax-parse #'(abc g64 xyz c%) + [f:foo + (check-equal? counter 0) + (check-pred syntax? #'(f.alpha ...)) + (check-equal? (syntax->datum #'(f.alpha ...)) '(abc xyz)) + (check-equal? counter 1) + (check-pred syntax? #'(f.alpha-part ...)) + (check-equal? (syntax->datum #'(f.alpha-part ...)) '("abc" "g" "xyz" "c")) + (check-equal? counter 5) + (void)]))) + ;; == Lib tests ;; static