racket/collects/redex/examples/delim-cont/randomized-tests.rkt
Eli Barzilay 4c2940a6d9 Simplify away uses of "-t" in `drdr:command-line'.
In some cases use `module+' to make the code run nicely without a "-m".

(And some other minor tweaks.)
2012-06-19 12:52:22 -04:00

548 lines
18 KiB
Racket
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#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 <non-procedure>") _)
(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))