#lang racket/base
(require rackunit
         mzlib/serialize
         mzlib/match
         web-server/private/define-closure)
(provide define-closure-tests)

(define-closure id (x) () x)

(define-closure add-y (x) (y) (+ x y))
(define-closure even-p (n) (odd-p) (or (zero? n)
                                       (odd-p (sub1 n))))
(define-closure odd-p (n) (even-p) (or (= 1 n)
                                       (even-p (sub1 n))))

(define even-p (make-even-p (lambda () odd-p)))
(define odd-p (make-odd-p (lambda () even-p)))

;; an interpreter

(define-closure the-empty-env (var) ()
  (error "unbound symbol: " var))

(define-closure extended-env (v) (env var val)
  (if (eqv? v var) val
      (env v)))

(define-serializable-struct proc (var body env))

(define-closure clsr:evaluate (expr env) (evaluate eval-app)
  (match expr
    [('lambda (var) body) (make-proc var body env)]
    [(expr1 expr2)
     (let ([val1 (evaluate expr1 env)]
           [val2 (evaluate expr2 env)])
       (eval-app val1 val2))]
    [(? number? n) n]
    [var (env var)]))

(define-closure clsr:eval-app (val1 val2) (evaluate)
  (cond
    [(proc? val1)
     (evaluate (proc-body val1)
               (make-extended-env
                (lambda () (values (proc-env val1) (proc-var val1) val2))))]
    [else 
     (error "stuck term: " (list val1 val2))]))

(define evaluate (make-clsr:evaluate (lambda () (values evaluate eval-app))))
(define eval-app (make-clsr:eval-app (lambda () evaluate)))

(define define-closure-tests
  (test-suite
   "Define Closure"
   
   (test-case
    "serialize id procedure"
    (check = 7 ((deserialize (serialize (make-id))) 7)))
   
   (test-case
    "id procedure"
    (check = 7 ((make-id) 7)))
   
   (test-case
    "add-y procedure"
    (check = 2 ((make-add-y (lambda () 1)) 1)))
   
   (test-case
    "serialize the add-y procedure"
    (check = 2 ((deserialize (serialize (make-add-y (lambda () 1)))) 1)))
   
   (test-case
    "even-p procedure"
    (check-true (even-p 8)))
   
   (test-case
    "serialize the even-p procedure"
    (check-true ((deserialize (serialize even-p)) 64)))
   
   (test-case
    "simple interpreter case"
    (check = 3 (evaluate 3 (make-the-empty-env))))
   
   (test-case
    "serialize simple interpreter case"
    (check = 3 ((deserialize (serialize evaluate))
                3
                (deserialize (serialize (make-the-empty-env))))))
   
   (test-case
    "apply identity"
    (check = 3 (evaluate '((lambda (x) x) 3) (make-the-empty-env))))
   
   (test-case
    "serialize environments"
    (let* ([e0 (make-the-empty-env)]
           [e1 (make-extended-env (lambda () (values e0 'x 1)))]
           [e2 (make-extended-env (lambda () (values e1 'y 2)))]
           [e3 (make-extended-env (lambda () (values e2 'z 3)))]
           [e4 (make-extended-env (lambda () (values e3 'x 4)))]
           [e5 (make-extended-env (lambda () (values e4 'y 5)))]
           [e6 (make-extended-env (lambda () (values e5 'z 6)))]
           [env3 (deserialize (serialize e3))]
           [env5 (deserialize (serialize e5))]
           [env6 (deserialize (serialize e6))])
      (check = 1 (env3 'x))
      (check = 2 (env3 'y))
      (check = 3 (env3 'z))
      (check = 4 (env5 'x))
      (check = 5 (env5 'y))
      (check = 3 (env5 'z))
      (check = 4 (env6 'x))
      (check = 5 (env6 'y))
      (check = 6 (env6 'z))))))
