diff --git a/current-pvars.rkt b/current-pvars.rkt index fe04a11..1507007 100644 --- a/current-pvars.rkt +++ b/current-pvars.rkt @@ -8,120 +8,43 @@ (for-syntax '#%kernel racket/private/qq-and-or racket/private/stx)) - - ;; This is a poor man's syntax parameter. Since the implementation of - ;; racket/stxparam depends on syntax-case, and we want to add current-pvars to - ;; syntax-case, we cannot use syntax parameters, lest we create a cyclic - ;; dependency. Instead, we implement here a simplified "syntax parameter". - ; Like racket/stxparam, it relies on nested bindings of the same identifier, - ;; and on syntax-local-get-shadower to access the most nested binding. - - ;; Since define/with-syntax and define/syntax-parse need to add new ids to - ;; the list, they redefine current-pvars-param, shadowing the outer binding. - ;; Unfortunately, if a let form contains two uses of define/with-syntax, this - ;; would result in two redefinitions of current-pvars-param, which would cause - ;; a "duplicate definition" error. Instead of shadowing the outer bindings, we - ;; therefore store the list of bound syntax pattern variables in a new, fresh - ;; identifier. When accessing the list, (current-pvars) then checks all such - ;; identifiers. The identifiers have the form current-pvars-paramNNN and are - ;; numbered sequentially, each new "shadowing" identifier using the number - ;; following the latest visible identifier. - ;; When it is safe to shadow identifiers (i.e. for with-pvars, but not for - ;; define-pvars), current-pvars-index-lower-bound is also shadowed. - ;; When current-pvars-index-lower-bound is bound, it contains the index of the - ;; latest current-pvars-paramNNN at that point. - ;; When accessing the latest current-pvars-paramNNN, a dichotomy search is - ;; performed between current-pvars-index-lower-bound and an upper bound - ;; computed by trying to access lower-bound + 2ᵏ, with increasing values of k, - ;; until an unbound identifier is found. - - ;; (poor-man-parameterof exact-nonnegative-integer?) - (define-syntaxes (current-pvars-index-lower-bound) 0) - ;; (poor-man-parameterof (listof identifier?)) - (define-syntaxes (current-pvars-param0) '()) - (begin-for-syntax - ;; (-> any/c (or/c (listof syntax?) #f)) + (define-values (current-pvars-param-guard) + (lambda (x) + ;; TODO: add condition: elements should be pairs of identifiers? + ;; Skip the guard, otherwise each operation is O(n). TODO: use a + ;; push/pop API which does the check on the head of the list instead. + #;(if (list? x) + x + (error "current-pvars-param should be a list")) + x)) + + (define-values (current-pvars-param) + (make-parameter '() current-pvars-param-guard)) + + (define-values (current-pvars) + (lambda () + (pop-unreachable-pvars) + (map car (current-pvars-param)))) + + (define-values (current-pvars+unique) + (lambda () + (pop-unreachable-pvars) + (current-pvars-param))) + (define-values (syntax*->list) (λ (stxlist) (syntax->list (datum->syntax #f stxlist)))) - - ;; (-> identifier? (or/c #f (listof identifier?))) - (define-values (try-current-pvars) - (λ (id) - (syntax-local-value - (syntax-local-get-shadower id - #t) - ;; Default value if we are outside of any with-pvars. - (λ () #f)))) - ;; (-> exact-nonnegative-integer? identifier?) - (define-values (nth-current-pvars-id) - (λ (n) - (syntax-local-introduce - (datum->syntax (quote-syntax here) - (string->symbol - (format "current-pvars-param~a" n)))))) - - ;; (-> exact-nonnegative-integer? (or/c #f (listof identifier?))) - (define-values (try-nth-current-pvars) - (λ (n) - (try-current-pvars (nth-current-pvars-id n)))) - - ;; (-> exact-nonnegative-integer? exact-nonnegative-integer? - ;; exact-nonnegative-integer?) - ;; Doubles the value of n until (+ start n) is not a valid index - ;; in the current-pvars-param pseudo-array - (define-values (double-max) - (λ (start n) - (if (try-nth-current-pvars (+ start n)) - (double-max start (* n 2)) - (+ start n)))) - - - ;; (-> exact-nonnegative-integer? exact-nonnegative-integer? - ;; exact-nonnegative-integer?) - ;; Preconditions: upper > lower ∧ upper - lower = 2ᵏ ∧ k ∈ ℕ - ;; Returns the last valid index in the current-pvars-param pseudo-array, - ;; by dichotomy between - (define-values (dichotomy) - (λ (lower upper) - (if (= (- upper lower) 1) - (if (try-nth-current-pvars upper) - upper ;; Technically not possible, still included for safety. - lower) - (let ([mid (/ (+ upper lower) 2)]) - (if (try-nth-current-pvars mid) - (dichotomy mid upper) - (dichotomy lower mid)))))) - - ;; (-> exact-nonnegative-integer?) - (define-values (find-last-current-pvars) - (λ () - (let ([lower-bound (syntax-local-value - (syntax-local-get-shadower - (syntax-local-introduce - (quote-syntax current-pvars-index-lower-bound)) - #t))]) - (if (not (try-nth-current-pvars (+ lower-bound 1))) - ;; Short path for the common case where there are no uses - ;; of define/with-syntax or define/syntax-parse in the most nested - ;; syntax-case, with-syntax or syntax-parse - lower-bound - ;; Find an upper bound by repeatedly doubling an offset (starting - ;; with 1) from the lower bound, then perform a dichotomy between - ;; these two bounds. - (dichotomy lower-bound - (double-max lower-bound 1)))))) - - ;; (-> (listof identifier?)) - (define-values (current-pvars) - (λ () - (map car (try-nth-current-pvars (find-last-current-pvars))))) - - (define-values (current-pvars+unique) - (λ () - (try-nth-current-pvars (find-last-current-pvars))))) + (define-values (pop-unreachable-pvars) + (lambda () + (if (or (null? (current-pvars-param)) + (syntax-local-value (caar (current-pvars-param)) + (λ () #f))) + (void) + (begin + (current-pvars-param (cdr (current-pvars-param))) + (pop-unreachable-pvars)))))) ;; (with-pvars [pvar ...] . body) (define-syntaxes (with-pvars) @@ -134,36 +57,55 @@ (syntax*->list (stx-car (stx-cdr stx)))))) (raise-syntax-error 'with-pvars "bad syntax" stx) (void)) - (let* ([pvars (reverse (syntax*->list (stx-car (stx-cdr stx))))] - [unique-at-runtime (map gensym (map syntax-e pvars))] - [stxquoted-pvars+unique (map (λ (v unique) - `(cons (quote-syntax ,v) - (quote-syntax ,unique))) - pvars - unique-at-runtime)] - [body (stx-cdr (stx-cdr stx))] - [old-pvars-index (find-last-current-pvars)] - [old-pvars (try-nth-current-pvars old-pvars-index)] - [binding (syntax-local-identifier-as-binding - (nth-current-pvars-id (+ old-pvars-index 1)))] - [lower-bound-binding - (syntax-local-identifier-as-binding - (syntax-local-introduce - (quote-syntax current-pvars-index-lower-bound)))] - [do-unique-at-runtime (map (λ (id pvar) - `[(,id) (gensym (quote ,pvar))]) - unique-at-runtime - pvars)]) + (let* ([pvars (syntax*->list (stx-car (stx-cdr stx)))] + [body (stx-cdr (stx-cdr stx))]) (datum->syntax (quote-syntax here) - `(let-values (,@do-unique-at-runtime) - (letrec-syntaxes+values - ([(,binding) (list* ,@stxquoted-pvars+unique - (try-nth-current-pvars ,old-pvars-index))] - [(,lower-bound-binding) ,(+ old-pvars-index 1)]) - () - . ,body)))))) + `(let-values () + (define-pvars ,@pvars) + ,@body)))) + #;(lambda (stx) + (if (not (and (stx-pair? stx) + (identifier? (stx-car stx)) + (stx-pair? (stx-cdr stx)) + (syntax*->list (stx-car (stx-cdr stx))) + (andmap identifier? + (syntax*->list (stx-car (stx-cdr stx)))))) + (raise-syntax-error 'with-pvars "bad syntax" stx) + (void)) + (let* ([pvars (reverse (syntax*->list (stx-car (stx-cdr stx))))] + [unique-at-runtime (map gensym (map syntax-e pvars))] + [pvars+unique (map cons pvars unique-at-runtime)] + [body (stx-cdr (stx-cdr stx))] + [do-unique-at-runtime (map (λ (id pvar) + `[(,id) (gensym (quote ,pvar))]) + unique-at-runtime + pvars)] + [wrapped-body (datum->syntax + (quote-syntax here) + `(let-values (,@do-unique-at-runtime) + ,@body))]) + (pop-unreachable-pvars) + + (with-continuation-mark + parameterization-key + (extend-parameterization + (continuation-mark-set-first #f parameterization-key) + current-pvars-param + (append pvars+unique + (current-pvars-param))) + (let-values ([(stx opaque) + (syntax-local-expand-expression wrapped-body #t)]) + opaque)) + + ;; above is the manual expansion of: + #;(parameterize ([current-pvars-param + (list* stxquoted-pvars+unique + (current-pvars-param))]) + … syntax-local-expand-expression …)))) + + ;; (define-pvars pv1 … pvn) (define-syntaxes (define-pvars) (lambda (stx) (if (not (and (stx-pair? stx) @@ -179,16 +121,16 @@ `(cons (quote-syntax ,v) (quote-syntax ,unique))) pvars - unique-at-runtime)] - [old-pvars-index (find-last-current-pvars)] - [old-pvars (try-nth-current-pvars old-pvars-index)] - [binding (syntax-local-identifier-as-binding - (nth-current-pvars-id (+ old-pvars-index 1)))]) + unique-at-runtime)]) (datum->syntax (quote-syntax here) `(begin (define-values (,@unique-at-runtime) (values ,@(map (λ (pvar) `(gensym (quote ,pvar))) pvars))) - (define-syntaxes (,binding) - (list* ,@stxquoted-pvars+unique - (try-nth-current-pvars ,old-pvars-index))))))))) \ No newline at end of file + (define-syntaxes () + (begin + (pop-unreachable-pvars) + (current-pvars-param + (list* ,@stxquoted-pvars+unique + (current-pvars-param))) + (values))))))))) \ No newline at end of file diff --git a/test/test-check-variable-visible.rkt b/test/test-check-variable-visible.rkt new file mode 100644 index 0000000..392c42d --- /dev/null +++ b/test/test-check-variable-visible.rkt @@ -0,0 +1,95 @@ +#lang racket + +;; This is a quick experiment to check that a set of identifiers (syntax +;; transformers) can be maintained using a stack, while ensuring that when +;; the set is queried at compile-time, only those identifiers which are within +;; scope are returned. +;; +;; It is necessary to understand this in order to internally build a stack of +;; definitions of pattern variables, and correctly pop pvars from the stack when +;; they go out of scope. + +(require rackunit) + +(define-for-syntax order '()) +(define-for-syntax (record-order x) + (set! order (cons x order))) + + +(define-for-syntax stack '()) +(define-for-syntax (push! e) + (set! stack (cons e stack))) +(define-for-syntax (peek) + (car stack)) +(define-for-syntax (pop!) + (set! stack (cdr stack))) +(define-for-syntax (pop…!) + (when (not (null? stack)) + (unless (syntax-local-value (car stack) (λ () #f)) + ;(displayln (syntax->datum #`(pop #,(car stack)))) + (pop!) + (pop…!)))) + +(define-syntax (def stx) + (syntax-case stx () + [(_ var) + (begin + (pop…!) + (push! #'var) + #'(define-syntax var 42))])) + +(define-syntax (query stx) + (syntax-case stx () + [(_ msg) + (begin + (pop…!) + (record-order (syntax->datum #`(msg . #,stack))) + #'(void))])) + +(define (expr x) (void)) + + +(define-syntax (macro stx) + #'(def v2)) + +(def v1) +(macro) +(let () + (def v6.1) + (query q6.2) + (expr (query q6.4)) + (let () + (def v6.5.1) + (void)) + (let () + (def v6.6.1) + ;; These queries must *not* contain v6.5.1. + (query q6.6.2) + (expr (query q6.6.3)) + (void)) + (let () + (def v6.7.1) + ;; These queries must *not* contain v6.5.1 nor v6.6.1. + (query q6.7.2) + (expr (query q6.7.3)) + (void)) + (def v6.3) + (void)) +(query q3) +(expr (query q7)) +(def v4) +(query q5) +(expr (query q8)) + +(check-equal? (let-syntax ([get (λ (stx) #`'#,(reverse order))]) + get) + '((q3 v2 v1) + (q5 v4 v2 v1) + (q6.2 v6.1 v4 v2 v1) + (q6.4 v6.3 v6.1 v4 v2 v1) + (q6.6.2 v6.6.1 v6.3 v6.1 v4 v2 v1) + (q6.6.3 v6.6.1 v6.3 v6.1 v4 v2 v1) + (q6.7.2 v6.7.1 v6.3 v6.1 v4 v2 v1) + (q6.7.3 v6.7.1 v6.3 v6.1 v4 v2 v1) + (q7 v4 v2 v1) + (q8 v4 v2 v1))) \ No newline at end of file diff --git a/test/test-current-pvars.rkt b/test/test-current-pvars.rkt index aee4071..593beb5 100644 --- a/test/test-current-pvars.rkt +++ b/test/test-current-pvars.rkt @@ -28,19 +28,61 @@ (list-ref (current-pvars) (syntax-e #'n)))]) (datum->syntax pvar (syntax-e pvar) stx))])) - ;; First check that (current-pvars) returns the empty list before anything ;; is done: (check-equal? (list-pvars) '()) +(let () + (define/with-syntax x #'1) + (void)) + +(check-equal? (list-pvars) + '()) + +;; test that the x is correctly removed, even if no querry was made +;; between its creation and the creation of the y. +(let () (define/with-syntax x #'1) (void)) +(let () + (define/with-syntax y #'2) + (check-equal? (list-pvars) + '(y)) + (void)) + +(check-equal? (list (list-pvars) + (syntax-case #'() () + [() (list (list-pvars) + (syntax-case #'(1 2 3 a b c) () + [(x y ...) + (list-pvars)]) + (list-pvars))]) + (list-pvars)) + '(() (() (y x) ()) ())) + +(check-equal? (list (list-pvars) + (syntax-case #'(-1 -2) () + [(k l) (list (list-pvars) + (syntax-case #'(1 2 3 a b c) () + [(z t ...) + (list-pvars)]) + (list-pvars))]) + (list-pvars)) + '(() ((l k) (t z l k) (l k)) ())) + ;; Simple case: (check-equal? (syntax-parse #'(1 2 3 a b c) [(x y ...) (list-pvars)]) '(y x)) +;; Simple case: +(check-equal? (syntax-case #'() () + [() (syntax-parse #'(1 2 3 a b c) + [(x y ...) + (list-pvars)])]) + '(y x)) + ;; Mixed definitions from user code and from a macro (begin (define-syntax (mixed stx) @@ -64,6 +106,9 @@ (syntax->datum (ref-nth-pvar 3))))) '(4 3 2 1))) +(check-equal? (list-pvars) + '()) + ;; Tests for syntax-parse (begin (check-equal? (syntax-parse #'(1 2 3 a b c) @@ -369,6 +414,16 @@ [_ #false])) +(let () + (define/with-syntax (x ... y) #'(1 2 3)) + (check-true (match (list-pvars+unique-val) + [(list (cons 'y (? symbol?)) + (cons 'x (? symbol?))) + #true] + [v + (displayln v) + #false]))) + (check-true (match (syntax-case #'(1 2 3) () [(x ... y) (list-pvars+unique-val)]) @@ -574,4 +629,7 @@ (check-equal? (expected-defs3 a b c d e) '(() (a) (a b) (a b c) (a b c d) (a b c d e))) -(check-defs3* 6 65) ;; continue tests with 6 till 65 pvars \ No newline at end of file +(check-defs3* 6 65) ;; continue tests with 6 till 65 pvars + +(check-equal? (list-pvars) + '()) \ No newline at end of file diff --git a/test/test-expansion-order.rkt b/test/test-expansion-order.rkt new file mode 100644 index 0000000..e99b031 --- /dev/null +++ b/test/test-expansion-order.rkt @@ -0,0 +1,83 @@ +#lang racket + +;; This is a quick experiment to see in what order are macros expanded. +;; +;; It is necessary to understand this in order to internally build a stack of +;; definitions of pattern variables, and correctly pop pvars from the stack when +;; they go out of scope. + +;; Macros in definition contexts are expanded in a breadth-first order +;; Macros in expression contexts are expanded in a breadth-first order +;; +;; Within a scope (let or top-level), all definitions are expanded, +;; then all the expressions + +(require rackunit) + +(define-for-syntax order '()) +(define-for-syntax (record-order x) + (set! order (cons x order))) + +(define-syntax (d stx) + (syntax-case stx () + [(_ a) + (begin (record-order `(d . ,(syntax-e #'a))) + #'(define x 42))])) + +(define-syntax (e stx) + (syntax-case stx () + [(_ a) + (begin (record-order `(e . ,(syntax-e #'a))) + #'42)])) + +(define (expr x) (void)) + +(d "+0 012") +(expr (e "?3 012")) + +(let () + (d "+4 012_45") + (expr (e "?6 012_45")) + ;; here, we're evaluating an "e" in a definition context, + ;; therefore it does not know that 5 will exist. + (e "¿5 012_4X\"") + ;; wrapping it with #%expression ensures that it runs after all definitions + ;; in the current scope (of course it then cannot introduce new definitions). + (#%expression (e "e6 012_46\"'")) + (let () + (d "+7 012_45_7") + (expr (e "?8 012_45_7")) + (d "+7 012_45_7'") + (expr (e "?8 012_45_7'"))) + (d "+5 012_45") + (expr (e "?9 012_45"))) + +(d "+1 012") +(expr (e "?A 012")) + +(let () + (d "+B 012_B") + (expr (e "?C 012_B"))) + +(d "+2 012") +(expr (e "?D 012")) +(check-equal? (let-syntax ([get (λ (stx) #`'#,(reverse order))]) + get) + '((d . "+0 012") + (d . "+1 012") + (d . "+2 012") + (e . "?3 012") + (d . "+4 012_45") + (e . "¿5 012_4X\"") + (d . "+5 012_45") + (e . "?6 012_45") + (e . "e6 012_46\"'") + (d . "+7 012_45_7") + (d . "+7 012_45_7'") + (e . "?8 012_45_7") + (e . "?8 012_45_7'") + (e . "?9 012_45") + (e . "?A 012") + (d . "+B 012_B") + (e . "?C 012_B") + (e . "?D 012"))) \ No newline at end of file