#lang racket (require "grammar.rkt" "reduce.rkt" (except-in redex/reduction-semantics plug) racket/runtime-path) (provide (all-defined-out)) (module+ main (apply main (vector->list (current-command-line-arguments)))) (define (main . args) (define from-grammar-tests #f) (define from-rules-tests #f) (define seed (add1 (random (sub1 (expt 2 31))))) (define size #f) (define attempt->size default-attempt-size) (define repetitions 1) (command-line #:argv args #:once-each ["--grammar" n "Perform n tests generated from the grammar for programs" (set! from-grammar-tests (string->number n))] ["--rules" n "Perform n tests generated from the reduction relation" (set! from-rules-tests (string->number n))] ["--seed" n "Generate tests using the PRG seed n" (set! seed (string->number n))] ["--size" n "Generate tests of size at most n" (set! size (string->number n)) (set! attempt->size (const size))] ["--log" p "Log generated tests to path p" (log-test (curryr pretty-write (open-output-file p #:exists 'truncate)))] ["--repetitions" n "Repeats the command n times" (set! repetitions (string->number n))]) (for ([_ (in-range 0 repetitions)]) (printf "Test seed: ~a (size: ~a)\n" seed (or size "variable")) (parameterize ([current-pseudo-random-generator test-prg]) (random-seed seed)) (parameterize ([redex-pseudo-random-generator test-prg]) (when from-grammar-tests (time (test #:attempts from-grammar-tests #:attempt-size attempt->size))) (when from-rules-tests (time (test #:source :-> #:attempts from-rules-tests #:attempt-size attempt->size)))))) (define log-test (make-parameter void)) (define-syntax-rule (test . kw-args) (redex-check grammar p (begin ((log-test) (term p)) (same-behavior? (term p))) #:prepare fix-prog . kw-args)) (define fix-prog (match-lambda [`(<> ,s ,_ ,e) (match-let ([`([,xs ,vs] ...) (remove-duplicates s #:key first)]) `(<> ,(map list xs (map (fix-expr xs) vs)) [] ,((fix-expr xs) e)))])) (define (fix-expr top-vars) (define rewrite (compose drop-duplicate-binders proper-wcms proper-conts consistent-dws (curry close top-vars '()))) ; Must call proper-wcm after proper-conts because the latter ; exposes opportunities to the former. ; ; (% 1 ; (cont 1 ; (wcm ([2 3]) ; (% 1 ; (wcm ([2 4]) ; hole) ; (λ (x) x)))) ; (λ (x) x)) ; ; But proper-conts sometimes cannot do its job until proper-wcms ; turns an arbitrary context into an evaluation context. ; ; (% 1 ; (cont 1 ; (wcm ([2 3]) ; (wcm ([2 4]) ; (% 1 hole (λ (x) x))))) ; (λ (x) x)) ; ; It might work to make proper-conts work in more contexts, ; but it's easier to iterate the rules to a fixed point (and ; there may be more dependencies that require iteration anyway). (λ (e) (let loop ([e e]) (define e’ (rewrite e)) (if (equal? e e’) e (loop e’))))) (struct error (cause) #:transparent) (struct answer (output result) #:transparent) (struct bad-test (reason) #:transparent) (struct timeout ()) (define (same-behavior? prog) (let ([impl-behavior (timeout-kill 15 (impl-eval (impl-program (transform-intermediate prog))))]) (or (bad-test? impl-behavior) (timeout? impl-behavior) (let ([model-behavior (timeout-warn 30 (model-eval prog) (pretty-write prog))]) (or (timeout? model-behavior) (if (error? impl-behavior) (error? model-behavior) (and (answer? model-behavior) (equal? impl-behavior model-behavior)))))))) (define impl-program (match-lambda [`(<> ,s [] ,e) `(let* ([previous-error #f] [result (with-handlers ([exn:fail? void]) (call-with-exception-handler (λ (exn) (when (and (exn:fail? exn) (not previous-error)) (set! previous-error exn)) exn) (λ () (letrec ,s ,e))))]) (if (exn:fail? previous-error) (raise previous-error) result))] [e e])) (define-runtime-module-path model-impl "model-impl.rkt") (define impl-eval (let ([ns (make-base-empty-namespace)]) (parameterize ([current-namespace ns]) (namespace-require 'racket) (namespace-require (resolved-module-path-name model-impl))) (define show (match-lambda [(? procedure?) 'procedure] [(? list? vs) (map show vs)] [v v])) (λ (test) (define output (open-output-string)) (define result (with-handlers ([exn:fail? (match-lambda [(exn:fail (regexp "%: expected argument of type ") _) (bad-test "procedure as tag")] [(exn:fail m _) (error m)])]) (parameterize ([current-output-port output]) (eval test ns)))) (if (or (error? result) (bad-test? result)) result (answer (get-output-string output) (show result)))))) (define model-eval-steps (make-parameter +inf.0)) (define (model-eval prog) (let/ec return (define show (match-lambda [(? number? n) n] [(? boolean? b) b] [`(list . ,vs) (map show vs)] [v 'procedure])) (define (eval prog steps) (define ns (set)) (let recur ([p prog] [d steps] [s (set)]) (define qs (apply-reduction-relation :-> p)) (if (empty? qs) (set! ns (set-add ns p)) (if (< d 0) (return (timeout)) (for ([q qs]) (if (set-member? s q) (return (timeout)) (recur q (sub1 d) (set-add s p))))))) (set-map ns values)) (match (eval prog (model-eval-steps)) [(list (and p `(<> ,_ ,output ,result))) (if (v? result) (answer (apply string-append (map (curry format "~v") output)) (show result)) (error p))]))) (define (with-timeout thunk timeout on-timeout) (let ([c (make-channel)]) (struct raised (value)) (let ([t (thread (λ () (channel-put c (with-handlers ([exn:fail? raised]) (thunk)))))]) (match (sync/timeout timeout c) [#f (on-timeout t c)] [(raised v) (raise v)] [x x])))) (define-syntax-rule (timeout-kill time expr) (with-timeout (λ () expr) time (λ (t _) (kill-thread t) (timeout)))) (define-syntax-rule (timeout-warn time expr warn) (with-timeout (λ () expr) time (λ (_ c) warn (sync c)))) (define (close top-vars loc-vars expr) (match expr [(? x? x) (let ([bound (append top-vars loc-vars)]) (cond [(memq x bound) x] [(not (empty? bound)) (random-member bound)] [else (random-literal)]))] [`(set! ,x ,e) (define e’ (close top-vars loc-vars e)) (cond [(memq x top-vars) `(set! ,x ,e’)] [(empty? top-vars) e’] [else `(set! ,(random-member top-vars) ,e’)])] [`(λ ,xs ,e) `(λ ,xs ,(close (filter (negate (curryr member xs)) top-vars) (append xs loc-vars) e))] [`(dw ,x ,e_1 ,e_2 ,e_3) ; Local variables are substituted away in realistic pre- ; and post-thunks. This invariant is important to ; `consistent-dws', which copies such thunks into different ; scopes. `(dw ,x ,(close top-vars '() e_1) ,(close top-vars loc-vars e_2) ,(close top-vars '() e_3))] ; substitution does not recur inside continuation values ; (not sure why it bothers to recur within dw expression) [`(cont ,v ,E) `(cont ,(close top-vars '() v) ,(close top-vars '() E))] [`(cont ,E) `(comp ,(close top-vars '() E))] [(? list?) (map (curry close top-vars loc-vars) expr)] [_ expr])) (define drop-duplicate-binders (match-lambda [`(λ ,xs ,e) `(λ ,(remove-duplicates xs) ,(drop-duplicate-binders e))] [(? list? es) (map drop-duplicate-binders es)] [e e])) (define (consistent-dws p) (define pre-post (let ([h (make-hash)]) (λ (id pre post) (match (hash-ref h id #f) [#f (hash-set! h id (list pre post)) (list pre post)] [x x])))) (let recur ([x p] [excluded '()]) (match x [`(dw ,x ,e1 ,e2 ,e3) (if (member x excluded) (recur e2 excluded) (match-let ([(list e1’ e3’) (pre-post x e1 e3)]) `(dw ,x ,(recur e1’ (cons x excluded)) ,(recur e2 excluded) ,(recur e3’ (cons x excluded)))))] [(? list?) (map (curryr recur excluded) x)] [_ x]))) (define (proper-wcms e) ; Performs two tasks: ; 1. drops duplicate cm keys, and ; 2. drops `wcm' frames when the reduction relation ; would not otherwise merge the marks (replacing them ; with `call/cm' requires more care, since the `wcm' ; body may contain a hole). (let fix ([ctxt 'wont-have-wcm] [e e]) (define tail (match-lambda [(or 'comp-top 'may-have-wcm) 'may-have-wcm] ['wont-have-wcm 'wont-have-wcm])) (match e [`(wcm ,w ,e) (match ctxt [(or 'comp-top 'wont-have-wcm) `(wcm ,(remove-duplicates (fix 'dont-care w) #:key first) ,(fix 'may-have-wcm e))] ['may-have-wcm (fix 'may-have-wcm e)])] [`(list . ,vs) ; context doesn't matter for values `(list . ,(map (curry fix 'dont-care) vs))] [`(λ ,xs ,e) ; caller's continuation may be marked `(λ ,xs ,(fix 'may-have-wcm e))] [`(cont ,v ,E) ; body will be wrapped in a prompt `(cont ,(fix 'dont-care v) ,(fix 'wont-have-wcm E))] [`(comp ,E) ; comp application merges only top-level marks `(comp ,(fix 'comp-top E))] [`(begin ,e1 ,e2) `(begin ,(fix 'wont-have-wcm e1) ; "begin-v" does not merge marks ,(fix (tail ctxt) e2))] [`(% ,e1 ,e2 ,e3) `(% ,(fix 'wont-have-wcm e1) ; prompt persists until e2 is a value ,(fix 'wont-have-wcm e2) ,(fix 'wont-have-wcm e3))] [`(dw ,x ,e1 ,e2 ,e3) `(dw ,x ,(fix 'wont-have-wcm e1) ; dw persists until e2 is a value ,(fix 'wont-have-wcm e2) ,(fix 'wont-have-wcm e3))] [`(if ,e1 ,e2 ,e3) `(if ,(fix 'wont-have-wcm e1) ; "ift" and "iff" do not merge marks ,(fix (tail ctxt) e2) ,(fix (tail ctxt) e3))] [`(set! ,x ,e) `(set! ,x ,(fix 'wont-have-wcm e))] [(? list?) (map (curry fix 'wont-have-wcm) e)] [_ e]))) (define proper-conts ; Turns (cont v_1 (in-hole E_1 (% v_1 E_2 v_2))) ; into (cont v_1 (in-hole E_1 E_2 )) ; since no real program can construct the former. ; ; It would be nice to perform this transformation ; by iteratively applying this rewrite rule ; ; (--> (in-hole (name C (cross e)) (cont v_1 (in-hole E_1 (% v_1 E_2 v_2)))) ; (in-hole C (cont v_1 (in-hole E_1 E_2)))) ; ; but a Redex bug (PR 11579) prevents that from working. (let ([none (gensym)]) (define-metafunction grammar [(fix (cont v E) any) (cont (fix v ,none) (fix E v))] [(fix (dw x e_1 E e_2) any) (dw x (fix e_1 ,none) (fix E any) (fix e_2 ,none))] [(fix (wcm w E) any) (wcm (fix w ,none) (fix E any))] [(fix (v ... E e ...) any) ((fix v ,none) ... (fix E any) (fix e ,none) ...)] [(fix (begin E e) any) (begin (fix E any) (fix e ,none))] [(fix (% E e_1 e_2) any) (% (fix E any) (fix e_1 ,none) (fix e_2 ,none))] [(fix (% v e E) any) (% (fix v ,none) (fix e ,none) (fix E any))] [(fix (% any E v) any) (fix E any)] [(fix (% v_1 E v_2) any) (% (fix v_1 ,none) (fix E any) (fix v_2 ,none))] [(fix (set! x E) any) (set! x (fix E any))] [(fix (if E e_1 e_2) any) (if (fix E any) (fix e_1 ,none) (fix e_2 ,none))] [(fix (any_1 ...) any_2) ((fix any_1 ,none) ...)] [(fix any_1 any_2) any_1]) (λ (expr) (term (fix ,expr ,none))))) (define transform-intermediate (match-lambda [(and p `(<> ,s ,o ,e)) (define fresh (make-fresh p)) (define allocated (map first s)) (define (alloc-cell prefix) (define cell (fresh prefix)) (set! allocated (cons cell allocated)) cell) (define capts (alloc-cell "active-cont-capts")) (define dw-frame-locs (let ([locs (make-hash)]) (λ (x) (hash-ref locs x (λ () (let ([ys (list (alloc-cell (format "~s-allocated?" x)) (alloc-cell (format "~s-skip-pre?" x)) (alloc-cell (format "~s-comp-cont" x)))]) (hash-set! locs x ys) ys)))))) (define transform (match-lambda [`(wcm () ,m) (transform m)] [`(wcm ([,k ,v] . ,w) ,m) `(call/cm ,(transform k) ,(transform v) (λ () ,(transform `(wcm ,w ,m))))] [(and e `(dw ,x ,e1 ,e2 ,e3)) (match-let ([(list a? s? c) (dw-frame-locs x)] [t (fresh "t")]) `((λ (,t) (if ,a? (begin (if (zero? ,capts) (set! ,s? #t) #f) (,c ,t)) (% 1 (dynamic-wind (λ () (if (zero? ,capts) (if ,a? (if ,s? (set! ,s? #f) ,(transform e1)) #f) #f)) (λ () ((call/comp (λ (k) (begin (set! ,c k) (abort 1 k))) 1))) (λ () (if (zero? ,capts) (if ,a? ,(transform e3) (set! ,a? #t)) (set! ,a? #t)))) (λ (k) (begin (if (zero? ,capts) (set! ,s? #t) #f) (k ,t)))))) (λ () ,(transform e2))))] [`(cont ,v ,E) (let ([x (fresh "v")]) `(begin (set! ,capts (+ ,capts 1)) ((λ (,x) (% ,x ,(transform (term (plug ,E (call/cc (λ (k) (abort ,x k)) ,x)))) (λ (x) (begin (set! ,capts (+ ,capts -1)) x)))) ,(transform v))))] [`(comp ,E) (define numbers (match-lambda [(? integer? n) (list n)] [(? list? l) (append-map numbers l)] [_ (list)])) (define t (add1 (apply max 0 (numbers E)))) `(begin (set! ,capts (+ ,capts 1)) (% ,t ,(transform (term (plug ,E (call/comp (λ (k) (abort ,t k)) ,t)))) (λ (x) (begin (set! ,capts (+ ,capts -1)) x))))] [`(list ,vs ...) `(list ,@(map transform-value vs))] [(? list? xs) (map transform xs)] [e e])) (define transform-value (match-lambda [(and e (or `(cont ,_ ,_) `(comp ,_))) `(λ (x) (,(transform e) x))] [e (transform e)])) (define e’ (transform e)) (define s’ (map (match-lambda [(list x v) (list x (transform-value v))]) s)) `(<> ,(map (λ (x) (match (assoc x s’) [#f (list x #f)] [(list _ v’) (list x v’)])) allocated) ,o (begin (set! ,capts 0) ,e’))])) ;; The built-in `plug' sometimes chooses the wrong hole. (define-metafunction grammar [(plug hole any) any] [(plug (in-hole W (dw x e_1 E e_2)) any) (in-hole W (dw x e_1 (plug E any) e_2))] [(plug (wcm w M) any) (wcm w (plug M any))] [(plug (v ... W e ...) any) (v ... (plug W any) e ...)] [(plug (begin W e) any) (begin (plug W any) e)] [(plug (% W e_1 e_2) any) (% (plug W any) e_1 e_2)] [(plug (% v e W) any) (% v e (plug W any))] [(plug (% v_1 W v_2) any) (% v_1 (plug W any) v_2)] [(plug (set! x W) any) (set! x (plug W any))] [(plug (if W e_1 e_2) any) (if (plug W any) e_1 e_2)]) (define (make-fresh p) (define suffix (let recur ([x p] [s 0]) (cond [(symbol? x) (match (regexp-match #rx"_(.+)$" (symbol->string x)) [(list _ n) (max s (add1 (string->number n)))] [#f s])] [(pair? x) (recur (cdr x) (recur (car x) s))] [else s]))) (λ (prefix) (begin0 (string->symbol (format "~a_~a" prefix suffix)) (set! suffix (add1 suffix))))) (define (random-literal) (random-member '(dynamic-wind abort current-marks cons -inf.0 +inf.0 -1 0 1 1/3 -1/4 .33 -.25 4-3i 3+4i call/cc call/comp call/cm #f #t zero? print + first rest))) (define (random-member xs) (parameterize ([current-pseudo-random-generator test-prg]) (list-ref xs (random (length xs))))) (define test-prg (make-pseudo-random-generator))