www

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | README

commit 1f2a9eaebb1cf2e6ed4f533b6fee740c3997263b
parent f1ede1dd4db8481b8546bf25f41f36563371bfac
Author: Georges Dupéron <georges.duperon@gmail.com>
Date:   Thu, 26 Jan 2017 04:15:03 +0100

Changed subtemplate to use current-pvars+unique

Diffstat:
Mmain.rkt | 378+++++++++++++++++++++++++++++++++++++++----------------------------------------
Atest/assumption-free-identifier-equal.rkt | 21+++++++++++++++++++++
Atest/assumption-weak-hash.rkt | 25+++++++++++++++++++++++++
Mtest/test-subtemplate.rkt | 65+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--
Atest/wrong-assumption-with-syntax-eq.rkt | 19+++++++++++++++++++
5 files changed, 314 insertions(+), 194 deletions(-)

diff --git a/main.rkt b/main.rkt @@ -1,15 +1,15 @@ #lang racket + (require racket/require phc-toolkit/untyped racket/stxparam - syntax/parse - backport-template-pr1514/experimental/template - ;syntax/parse/experimental/template - ;syntax/parse/experimental/private/substitute + stxparse-info/parse + stxparse-info/current-pvars + stxparse-info/parse/experimental/template syntax/id-table racket/syntax - (for-syntax "patch-arrows.rkt" - syntax/parse + (for-syntax phc-graph/patch-arrows + stxparse-info/parse racket/private/sc racket/syntax racket/list @@ -21,82 +21,27 @@ syntax/contract racket/contract)) -(provide (rename-out [new-syntax-parse syntax-parse] - [new-syntax-parser syntax-parser] - [new-syntax-case syntax-case]) - ;define-unhygienic-template-metafunction - subtemplate +(provide subtemplate quasisubtemplate) -(begin-for-syntax (struct derived ())) -(define-syntax-parameter maybe-syntax-pattern-variable-ids '()) -(define empty-pvar-values '()) -(define-syntax-parameter pvar-values-id (make-rename-transformer - #'empty-pvar-values)) - -(begin-for-syntax - (define/contract (split-colon sym) - (-> symbol? (cons/c symbol? (listof symbol?))) - (cons sym - (map string->symbol - (string-split (symbol->string sym) - ":"))))) - -(define-for-syntax (new-scope rest lctx) - ;(wrap-expr/c - ;#'(listof (cons/c identifier? (listof symbol?))) - #`(cons (cons (quote-syntax #,(syntax-local-get-shadower - (datum->syntax lctx - 'outer-lctx)) - #:local) - '#,(~> (syntax->datum rest) - flatten - (filter symbol? _) - (append-map split-colon _) - (remove-duplicates))) - (syntax-parameter-value - #'maybe-syntax-pattern-variable-ids)));) +(define derived-valvar-cache (make-weak-hash)) (begin-for-syntax - (define/contract (wrap-with-parameterize lctx new-whole-form rest) - (-> identifier? syntax? syntax? syntax?) - (quasisyntax/top-loc lctx - (let () - #,(patch-arrows - ;; HERE insert a hash table, to cache the uses of derived pvars. - ;; Lifting the define-temp-ids is not likely to work, as they - ;; need to define syntax pattern variables so that other macros - ;; can recognize them. Instead, we only lift the values, but still - ;; do the bindings around the subtemplate. - #`(let ([the-pvar-values (cons (make-hash) pvar-values-id)]) - (syntax-parameterize ([maybe-syntax-pattern-variable-ids - #,(new-scope rest lctx)] - [pvar-values-id (make-rename-transformer - #'the-pvar-values)]) - #,new-whole-form))))))) + ;; Act like a syntax transformer, but which is recognizable via the + ;; derived-pattern-variable? predicate. + (struct derived-valvar (valvar) + #:property prop:procedure + (λ (self stx) + #`(#%expression #,(derived-valvar-valvar self)))) -(begin-for-syntax - (define/contract (simple-wrap-with-parameterize new-form-id) - (-> identifier? (-> syntax? syntax?)) - (λ/syntax-case (self . rest) () - (wrap-with-parameterize #'self #`(#,new-form-id . rest) #'rest)))) + (define (id-is-derived-valvar? id) + (define mapping (syntax-local-value id (thunk #f))) + (and mapping ;; … defined as syntax + (syntax-pattern-variable? mapping) ;; and is a syntax pattern variable + (derived-valvar? ;; and the pvar's valvar is derived + (syntax-local-value (syntax-mapping-valvar mapping) + (thunk #f))))) -(define-syntax new-syntax-parse - (simple-wrap-with-parameterize #'syntax-parse)) - -(define-syntax new-syntax-case - (simple-wrap-with-parameterize #'syntax-case)) - -(define-syntax (new-syntax-parser stx) - (syntax-case stx () - [(self . rest) - (quasisyntax/top-loc #'self - (λ (stx2) - #,(wrap-with-parameterize #'self - #'((syntax-parser . rest) stx2) - #'rest)))])) - -(begin-for-syntax (define/contract (string-suffix a b) (-> string? string? string?) (define suffix-length (string-suffix-length a b)) @@ -135,180 +80,223 @@ (string-length sub)))]) (datum->syntax id (string->symbol new-str) id id))) - (define/contract (find-subscript-binder2a lctx scopes bound scope-depth) - (-> identifier? - (listof (cons/c identifier? (listof symbol?))) - identifier? - exact-nonnegative-integer? - (listof (list/c identifier? exact-nonnegative-integer?))) - (if (null? scopes) - '() - (let () - (define outer-lctx (caar scopes)) - (define syms (cdar scopes)) - (define recur-found (find-subscript-binder2a outer-lctx - (cdr scopes) - bound - (add1 scope-depth))) - (define found-here - (for*/list ([binder-sym (in-list syms)] - [binder (in-value (datum->syntax lctx binder-sym #f))] - #:when (syntax-pattern-variable? - (syntax-local-value binder (thunk #f))) - #:when (not (derived? - (syntax-local-value - (format-id binder - " is-derived-~a " - binder) - (thunk #f)))) - [subscripts (in-value (subscript-equal? bound - binder))] - #:when subscripts) - (list binder scope-depth))) - (if (null? found-here) - recur-found - (append found-here recur-found))))) - - (define/contract (find-subscript-binder2 bound) - (-> identifier? - (or/c #f (list/c identifier? ;; bound - (syntax/c (listof identifier?)) ;; binders - (syntax/c (listof identifier?)) ;; max-binders - exact-nonnegative-integer? ;; ellipsis-depth - exact-nonnegative-integer? ;; scope-depth - syntax?))) ;; check-ellipsis-count - (define scopes (syntax-parameter-value #'maybe-syntax-pattern-variable-ids)) - (define/with-syntax ([binder scope-depth] …) - (find-subscript-binder2a bound ;; TODO: check this is okay (should be). - scopes - bound - 0)) - (if (stx-null? #'(binder …)) - #f - (let () - (define depths - (stx-map (∘ syntax-mapping-depth syntax-local-value) #'(binder …))) - (unless (or (< (length depths) 2) (apply = depths)) - (raise-syntax-error 'subtemplate - (format "inconsistent depths: ~a" - (syntax->list #'(binder …))) - bound)) - ;; generate code to check that the bindings have all the same - ;; ellipsis count - (define/with-syntax check-ellipsis-count-ddd - (nest-ellipses #'(binder …) (car depths))) - (define max-scope-depth (apply max (syntax->datum #'(scope-depth …)))) - (define max-binders - (sort (map car - (filter (λ (bs) (= (syntax-e (cdr bs)) max-scope-depth)) - (stx-map syntax-e #'([binder . scope-depth] …)))) - symbol<? - #:key syntax-e)) - (list bound - #'(binder …) - #`#,max-binders - (car depths) - max-scope-depth - #'check-ellipsis-count-ddd)))) - (define/contract (nest-ellipses stx n) (-> syntax? exact-nonnegative-integer? syntax?) (if (= n 0) stx #`(#,(nest-ellipses stx (sub1 n)) - (… …))))) + (… …)))) + + (define/contract (find-subscript-binder bound) + (-> identifier? + (or/c #f (list/c identifier? ; bound + (syntax/c (listof identifier?)) ; binders + (syntax/c (listof identifier?)) ; unique-at-runtime ids + exact-nonnegative-integer? ; ellipsis-depth + syntax?))) ; check-ellipsis-count + (let/cc return + ;; EARLY RETURN (already a pattern variable) + (when (syntax-pattern-variable? + (syntax-local-value bound (thunk #f))) + (return #f)) + + (define/with-syntax ([binder . unique-at-runtime-id] …) + (filter (compose (conjoin identifier? + (negate id-is-derived-valvar?) + (λ~> (syntax-local-value _ (thunk #f)) + syntax-pattern-variable?) + ;; force call syntax-local-value to prevent + ;; ambiguous bindings, as syntax-local-value + ;; triggers an error for those. + ;; Must be done before the free-identifier=? + ;; which just returns #false + (λ~> (datum->syntax _ (syntax-e bound)) + (syntax-local-value _ (thunk #f)) + (thunk* #t)) ;; ok if no error. + (λ~> (datum->syntax _ (syntax-e bound)) + (free-identifier=? _ bound)) + (λ~> (subscript-equal? bound _))) + car) + (current-pvars+unique))) + + ;; Or write it as: + + #;(define/with-syntax ([binder . unique-at-runtime] …) + (for/list ([binder (current-pvars)] + #:when (identifier? (car binder)) + #:unless (id-is-derived-pvar? (car binder)) + #:when (syntax-pattern-variable? + (syntax-local-value (car binder) (thunk #f))) + ;; force call syntax-local-value to prevent ambiguous + ;; bindings, as syntax-local-value triggers an error for + ;; those. + ;; Must be done before the free-identifier=? which just + ;; returns #false + #:when (begin + (syntax-local-value + (datum->syntax _ (syntax-e bound)) + (thunk #f)) + #t) ;; ok if no error. + #:when (free-identifier=? (datum->syntax (car binder) + (syntax-e bound)) + bound) + #:when (subscript-equal? bound (car binder))) + binder)) + + ;; EARLY RETURN (no candidate binders found) + (when (stx-null? #'(binder …)) + (return #f)) + + (define depths + (stx-map (∘ syntax-mapping-depth syntax-local-value) #'(binder …))) + + ;; EARLY ERROR (inconsistent depths) + (unless (or (< (length depths) 2) (apply = depths)) + (car depths) + (raise-syntax-error 'subtemplate + (format "inconsistent depths: ~a" + (syntax->list #'(binder …))) + bound)) + + ;; generate code to check that the bindings have all the same + ;; ellipsis count, by simply generating a dummy syntax object, with + ;; all the given binders nested under the right number of ellipses. + (define/with-syntax check-ellipsis-count-ddd + (nest-ellipses #'(binder …) (car depths))) + + ;; FINAL RETURN (list of same-depth binders + their depth) + (list bound + #'(binder …) + #'(unique-at-runtime-id …) + (car depths) + #'check-ellipsis-count-ddd)))) (define-for-syntax/case-args ((sub*template tmpl-form) (self . tmpl)) (define acc '()) + + ;; Finds identifiers of the form zᵢ, and return a list of existing xᵢ bindings (define (fold-process stx rec) (syntax-case stx () [(id . _) (and (identifier? #'id) (free-identifier=? #'id #'unsyntax)) stx] [id (identifier? #'id) - (let ([binders+info (find-subscript-binder2 #'id)]) + (let ([binders+info (find-subscript-binder #'id)]) (when binders+info (set! acc (cons binders+info acc))) #'id)] [other (rec #'other)])) - ;; process the syntax, extract the derived bindings into acc + ;; Process the syntax, extract the derived bindings into acc + ;; Does not take zᵢ identifiers generated by template metafunctions into + ;; account for now. (fold-syntax fold-process #'tmpl) + ;; define the result, which looks like (template . tmpl) or ;; like (quasitemplate . tmpl) (define result (quasisyntax/top-loc #'self - (#,tmpl-form - . tmpl))) + (#,tmpl-form . tmpl))) ;; Make sure that we remove duplicates, otherwise we'll get errors if we ;; define the same derived id twice. - (define/with-syntax ([bound binders - max-binders - depth - scope-depth - check-ellipsis-count] …) + (define/with-syntax ([bound + binders + unique-at-runtime-ids + ellipsis-depth + check-ellipsis-count] + …) (remove-duplicates acc #:key car)) #`(let () - (derive bound binders max-binders depth scope-depth) + (derive bound binders unique-at-runtime-ids ellipsis-depth) … (let () ;; no-op, just to raise an error when they are incompatible - #'(check-ellipsis-count …) + #'(check-ellipsis-count …) ;; TODO: allow #f values for ~optional in syntax/parse ;;;;;;;;;;;;;; ;; actually call template or quasitemplate #,result))) (define-syntax subtemplate (sub*template #'template)) (define-syntax quasisubtemplate (sub*template #'quasitemplate)) +(define/contract (multi-hash-ref! h keys to-set) + ;; This assumes that the hash does not get mutated during the execution of + ;; this function. + (-> (and/c (hash/c symbol? any/c #:immutable #f) hash-weak?) + (listof symbol?) + any/c + any/c) + (define val (or (for/or ([k (in-list keys)]) (hash-ref h k #f)) + to-set)) + (for ([k (in-list keys)]) (hash-ref! h k val)) + val) + +(define/contract ((stx-list*+stx/c depth) l) + (-> exact-nonnegative-integer? (-> any/c boolean?)) + (if (= depth 0) + (syntax? l) + (and (syntax? l) + (syntax->list l) + (andmap (λ (lᵢ) ((stx-list*+stx/c (sub1 depth)) lᵢ)) + (syntax->list l))))) +(define/contract ((list*+stx/c depth) l) + (-> exact-nonnegative-integer? (-> any/c boolean?)) + (if (= depth 0) + (syntax? l) + (and (list? l) + (andmap (λ (lᵢ) ((list*+stx/c (sub1 depth)) lᵢ)) + l)))) -(define-syntax/case (derive bound binders max-binders stx-depth stx-scope-depth) - () - ;; TODO: shouldn't it be called in the first place? - (if (syntax-pattern-variable? (syntax-local-value #'bound (thunk #f))) - #'(begin) - #'(derive2 bound binders max-binders stx-depth stx-scope-depth))) +(define/contract (destructure-stx-list* l depth) + (->i ([l (depth) (stx-list*+stx/c depth)] + [depth exact-nonnegative-integer?]) + [range (depth) (list*+stx/c depth)]) + (if (= depth 0) + l + (stx-map (λ (lᵢ) (destructure-stx-list* lᵢ (sub1 depth))) + l))) -(define-syntax/case (derive2 bound - binders - (max-binder0 . max-binders) - stx-depth - stx-scope-depth) () - (define depth (syntax-e #'stx-depth)) +(define-syntax/case (derive bound + (binder₀ binderᵢ …) + (unique-at-runtime-idᵢ …) + ellipsis-depth) () + (define depth (syntax-e #'ellipsis-depth)) (define/with-syntax bound-ddd (nest-ellipses #'bound depth)) (define/with-syntax tmp-id - (format-id #'here "~a/~a" #'max-binder0 (drop-subscripts #'bound))) + (format-id #'here "~a/~a" #'binder₀ (drop-subscripts #'bound))) (define/with-syntax tmp-str (datum->syntax #'tmp-id (symbol->string (syntax-e (format-id #'here "~~a/~a" (drop-subscripts #'bound)))))) (define/with-syntax tmp-ddd (nest-ellipses #'tmp-id depth)) - (define/with-syntax binder-ddd (nest-ellipses #'max-binder0 depth)) + (define/with-syntax binder-ddd (nest-ellipses #'binder₀ depth)) ;; Draw arrows in DrRacket. (with-arrows - (define subscripts (subscript-equal? #'bound #'max-binder0)) + (define subscripts (subscript-equal? #'bound #'binder₀)) (define bound-id-str (identifier->string #'bound)) - (for ([max-binder (in-list (syntax->list #'(max-binder0 . max-binders)))]) - (define binder-id-str (identifier->string max-binder)) + (for ([binder (in-list (syntax->list #'(binder₀ binderᵢ …)))]) + (define binder-id-str (identifier->string binder)) (record-sub-range-binders! (vector #'bound (- (string-length bound-id-str) (string-length subscripts)) (string-length subscripts) - max-binder + binder (- (string-length binder-id-str) (string-length subscripts)) (string-length subscripts)))) - #;(define binder0-id-str (identifier->string #'max-binder0)) + #;(define binder0-id-str (identifier->string #'binder0)) #;(record-sub-range-binders! (vector #'bound (- (string-length bound-id-str) (string-length subscripts)) (string-length subscripts) - #'max-binder0 + #'binder0 (- (string-length binder0-id-str) (string-length subscripts)) (string-length subscripts))) + (define/with-syntax temp-derived (generate-temporary #'bound)) + (define/with-syntax temp-cached (generate-temporary #'bound)) ;; HERE: cache the define-temp-ids in the free-id-table, and make sure ;; that we retrieve the cached ones, so that two subtemplate within the same ;; syntax-case or syntax-parse clause use the same derived ids. @@ -322,11 +310,16 @@ ;; zᵢ …))]) ;; the test above is not exactly right (zᵢ will still have the correct ;; binding), but it gives the general idea. - #`(begin (define-temp-ids tmp-str binder-ddd) - (define cached (hash-ref! (list-ref pvar-values-id - stx-scope-depth) - 'bound - #'tmp-ddd)) - (define/with-syntax bound-ddd cached) - (define-syntax #,(format-id #'bound " is-derived-~a " #'bound) - (derived))))) + #`(begin (define-temp-ids #:concise tmp-str binder-ddd) + (define temp-cached + (free-id-table-ref! (multi-hash-ref! derived-valvar-cache + '(unique-at-runtime-idᵢ …) + (make-free-id-table)) + (quote-syntax bound) + (destructure-stx-list* #'tmp-ddd + 'ellipsis-depth))) + (define-syntax temp-derived + (derived-valvar (quote-syntax temp-cached))) + (define-syntax bound + (make-syntax-mapping 'ellipsis-depth (quote-syntax temp-derived))) + (define-pvars bound)))) +\ No newline at end of file diff --git a/test/assumption-free-identifier-equal.rkt b/test/assumption-free-identifier-equal.rkt @@ -0,0 +1,20 @@ +#lang racket +(define-for-syntax outer #f) +(define-for-syntax inner #f) +(let ([x 1]) + (define-syntax (capture1 stx) + (set! outer #'x) + #'(void)) + (capture1) + (let ([x 2]) + (define-syntax (capture2 stx) + (set! inner #'x) + #'(void)) + (capture2) + (let ([y 3]) + (define-syntax (compare stx) + (define candidate (datum->syntax #'y 'x)) + (displayln (free-identifier=? candidate inner)) + (displayln (free-identifier=? candidate outer)) + #'(void)) + (compare)))) +\ No newline at end of file diff --git a/test/assumption-weak-hash.rkt b/test/assumption-weak-hash.rkt @@ -0,0 +1,24 @@ +#lang racket + +(require (for-syntax racket/private/sc)) + +(define h (make-weak-hasheq)) + +(define (all-eq? l) + (foldl (λ (x acc) + (and (eq? x acc) acc)) + (car l) + (cdr l))) + +(for/list ([range-a (in-range 100)]) + (with-syntax ([(xᵢ ...) #'(1 2 3)]) + (define-syntax (hh stx) + #`(hash-ref! h + #,(syntax-mapping-valvar (syntax-local-value #'xᵢ)) + (gensym))) + (displayln (hash->list h)) + (all-eq? (for/list ([range-b (in-range 5)]) + ;(collect-garbage) + ;(collect-garbage) + ;(collect-garbage) + (hh))))) +\ No newline at end of file diff --git a/test/test-subtemplate.rkt b/test/test-subtemplate.rkt @@ -1,5 +1,7 @@ #lang racket -(require "../subtemplate.rkt" +(require subtemplate + stxparse-info/parse + stxparse-info/case phc-toolkit/untyped rackunit) @@ -22,11 +24,70 @@ |# + (check-equal? (syntax->datum (syntax-parse #'(a b c d) [(_ xⱼ zᵢ …) (subtemplate foo)])) 'foo) +(check-equal? (syntax->datum (syntax-case #'(a b c d) () + [(_ xⱼ zᵢ …) + (subtemplate foo)])) + 'foo) + +(check-equal? (syntax->datum (syntax-parse #'(a b c d) + [(_ xⱼ zᵢ …) + (subtemplate xⱼ)])) + 'b) + +(check-equal? (syntax->datum (syntax-case #'(a b c d) () + [(_ xⱼ zᵢ …) + (subtemplate xⱼ)])) + 'b) + +(check-equal? (syntax->datum (syntax-parse #'(a b c d) + [(_ xⱼ zᵢ …) + (subtemplate (zᵢ …))])) + '(c d)) + +(check-equal? (syntax->datum (syntax-case #'(a b c d) () + [(_ xⱼ zᵢ …) + (subtemplate (zᵢ …))])) + '(c d)) + +(check-equal? (syntax->datum (syntax-parse #'(a b c d) + [(_ xⱼ zᵢ …) + (subtemplate (wᵢ …))])) + '(c/w d/w)) + +(check-equal? (syntax->datum (syntax-case #'(a b c d) () + [(_ xⱼ zᵢ …) + (subtemplate (wᵢ …))])) + '(c/w d/w)) + +(check-equal? (syntax->datum (syntax-parse #'(a b c d) + [(_ xⱼ zᵢ …) + (subtemplate (kⱼ wᵢ …))])) + '(b/k c/w d/w)) + +(check-equal? (syntax->datum (syntax-case #'(a b c d) () + [(_ xⱼ zᵢ …) + (subtemplate (kⱼ wᵢ …))])) + '(b/k c/w d/w)) + +(check-equal? (syntax->datum (syntax-parse #'(a b c d) + [(_ xⱼ zᵢ …) + (subtemplate (xⱼ kⱼ (zᵢ wᵢ) …))])) + '(b b/k (c c/w) (d d/w))) + +(check-equal? (syntax->datum (syntax-case #'(a b c d) () + [(_ xⱼ zᵢ …) + (subtemplate (xⱼ kⱼ (wᵢ zᵢ) …))])) + '(b b/k (c/w c) (d/w d))) + + + + #;(let () (syntax-parse #'a #;(syntax-parse #'(a b c d) [(_ xⱼ zᵢ …) @@ -427,7 +488,7 @@ (check free-identifier=? #'zz2 #'b) (check free-identifier=? #'x1 #'x2) - (check (∘ not free-identifier=?) #'w1 #'w2) ;; yes above, no here. + (check (∘ not bound-identifier=?) #'w1 #'w2) ;; yes above, no here. (check free-identifier=? #'foo1 #'foo2) (check free-identifier=? #'z1 #'z2) (check free-identifier=? #'p1 #'p2) diff --git a/test/wrong-assumption-with-syntax-eq.rkt b/test/wrong-assumption-with-syntax-eq.rkt @@ -0,0 +1,18 @@ +#lang racket +(require (for-syntax racket/private/sc)) + +(define old #f) + +(for/list ([range-a (in-range 100)]) + ;; The contents of the valvar is eq? when using a literal syntax object + ;; #'(1 2 3), but not with (datum->syntax #'here '(1 2 3)). + ;; I expected the result to always be different at each execution of the + ;; with-syntax, but it turns out the syntax object is kept as-is. + (with-syntax ([(xᵢ ...) #'(1 2 3) #;(datum->syntax #'here '(1 2 3))]) + (define-syntax (hh stx) + #`#,(syntax-mapping-valvar (syntax-local-value #'xᵢ))) + (unless old + (displayln "initial set!") + (set! old (hh))) + (andmap identity (for/list ([range-b (in-range 5)]) + (eq? old hh))))) +\ No newline at end of file