;; recon-test.scm
;;
;; Test the type reconstructor for Scheme/R
;;
;; You must load Scheme+ and recon.scm before running this code.
;;
;;
;; To run the test suite, execute
;;
;; (run-tests)
;;
;;

;; Modified from FL test suite by BJR

(define test-suite '())
(define test-counter 0)
(define test-recon-failed (list 'recon-failed))
(define halt-on-error #t)

(define-datatype test-case
  (test-case int sexp type-sexp))

(define (atest-case-n atest-case)
  (match atest-case
    ((test-case n _ _) n)))

(define (atest-case-exp atest-case)
  (match atest-case
    ((test-case _ exp _) exp)))

(define (atest-case-type-sexp atest-case)
  (match atest-case
    ((test-case _ _ tsexp) tsexp)))

(define (add-test! sexp result)
  (set! test-counter (1+ test-counter))
  (set! test-suite (cons (test-case test-counter sexp result)
			 test-suite))
  unspecific)

(define (run-tests)
  (let ((passed #t))
    (for-each 
     (lambda (atest-case)
       (newline)
       (for-each display
		 (list "Running test " (atest-case-n atest-case) " ..."))
       (run-test atest-case 
		 (lambda (passed? val)
		   (if passed?
		       (display " OK!")
		       (begin (set! passed #f)
			      (test-failed atest-case val))))))
     (reverse test-suite))
    (newline)
    (if passed
	(for-each display (list "Test Suite passed -- " 
				(length test-suite)
				" test cases.")))
    unspecific))


(define (test-failed atest-case val)
  (match atest-case
    ((test-case n sexp result)
     (let ((msg (apply error-string
		       (string-append 
			"\nTest Case " (number->string n) " Failed:")
		       ""
		       (list sexp result val))))
       (if halt-on-error 
	   (error msg)
	   (display msg))))
    ))

(define (run-test test return)
  ;; Returns two values: 
  ;; * A boolean that indicates whether the actual value matched the expected one.
  ;; * The actual value of the test.
  (if (eq? (atest-case-type-sexp test) test-recon-failed)
      (let ((bool&val
	     (call-with-current-continuation
	      (lambda (k)
		(fluid-let ((standard-error-hook 
			     (lambda (condition) 
			       (k (cons #t test-recon-failed)))))
		  (cons #f (check (atest-case-prog test))))))))
	(return (car bool&val) (cdr bool&val)))
      (let ((bool&val
	     (call-with-current-continuation
	      (lambda (k)
		(fluid-let ((standard-error-hook
			     (lambda (condition) 
			       (k (cons #f (with-output-to-string
					     (lambda ()
					       (write-condition-report
						condition
						(current-output-port)))))))))
		  (let* ((ignore (reset-tvariable-counter!))
			 (recon-type (reconstruct (parse (atest-case-exp test))
						  standard-type-environment)))
		    (cons (compare-types
			   recon-type 
			   (instantiate-schema
			    (parse-schema (atest-case-type-sexp test))))
			  recon-type)))))))
	(return (car bool&val) (cdr bool&val)))))


(define (compare-types t1 t2)
  (call-with-current-continuation
   (lambda (k)
     (fluid-let ((standard-error-hook
		  (lambda (condition) 
		    (k #f))))
       (begin (unify! t1 t2)
	      #t)))))



(add-test! '(let ((g (lambda (x) x)))
	      (if (g #t) (g 1) (g 2)))
	   'int)

(add-test! '(lambda (g)
	      (if (g #t) (g 1) (g 2)))
	   test-recon-failed)

(add-test! '(lambda (f)
	      (let ((g f))
		(if (g #t) (g 1) (g 2))))
	   test-recon-failed)

(add-test! '(lambda (f)
	      (let ((g (lambda (x) (f x))))
		(if (g #t) (g 1) (g 2))))
	   test-recon-failed)


(add-test! '(letrec ((fact (lambda (n)
			     (if (= n 0)
				 1
				 (* n (fact (- n 1)))))))
	      fact)
	   '(-> (int) int))

(add-test! 
 '(letrec ((map (lambda (p l)
		  (if (null? l)
		      (null)
		      (cons (p (car l))
			    (map p (cdr l)))))))
    map)
 '(generic (?t-17 ?result-16)
    (-> ((-> (?t-17) ?result-16) (list-of ?t-17)) (list-of ?result-16))))

(add-test! '(lambda (x y)
	      (letrec ((map (lambda (p l)
			      (if (null? l)
				  (null)
				  (cons (p (car l))
					(map p (cdr l)))))))
		(append x
			(map (lambda (y-elt) 
			       (if y-elt 1 0))
			     y))))
	   '(-> ((list-of int) (list-of bool)) (list-of int)))                

(add-test! '(letrec ((^ (lambda (p n)
			  (if (= n 0)
			      (lambda (x) x)
			      (lambda (x)
				(p ((^ p (- n 1)) x)))))))
	      ^)
	   '(generic (?x-11) (-> ((-> (?x-11) ?x-11) int) (-> (?x-11) ?x-11))))


; Functions defined by letrec can be used polymorphically in the body
; of the letrec.

(add-test! '(letrec ((g (lambda (x) x)))
	      (if (g #t) (g 1) (g 2)))
	   'int)

; ... but letrec definitions aren't polymorphic over themselves.

;Should fail
(add-test! '(letrec ((g (lambda (x) x))
		     (h (lambda () (if (g #t) (g 1) (g 2)))))
	      (if (g #t) (g 1) (g 2)))
	   test-recon-failed)

; Should fail
(add-test! '(letrec ((f (lambda (x) x))
		     (g (lambda () (f 1))))
	      (f #t))
	   test-recon-failed)

; A number of potential LETREC bugs are found by the following simple test,
; which should fail.
(add-test! '(letrec ((a (lambda () 3))
		     (b (lambda () (if (a) 1 2))))
	      (b))
	   test-recon-failed)

; Self-application should fail ...
(add-test! '(lambda (f) (f f))
	   test-recon-failed)

; ... unless we know what we're self-applying
;
(add-test! '(let ((twice (lambda (f) (lambda (x) (f (f x))))))
	      (twice twice))
	   '(generic (?result-6) 
	      (-> ((-> (?result-6) ?result-6)) (-> (?result-6) ?result-6))))

(add-test!
  '(let ((g (lambda (x) x)))
     (if ((g g) #t) ((g g) 1) ((g g) 2)))
  'int)

; Infinite loops match any type
;
(add-test! '(letrec ((loop (lambda () (loop))))
	      (lambda (x) 
		(if x 3 (loop))))
	   '(-> (bool) int))

(add-test! '(letrec ((loop (lambda () (loop))))
	      (lambda (x) 
		(if x "three" (loop))))
	   '(-> (bool) string))

; Type clash: string vs. int
(add-test! '(letrec ((loop (lambda (b) (if b 1 (loop b)))))
	      (lambda (x) 
		(if x "three" (loop x))))
	   test-recon-failed)


;;; Hairy examples with identity

; type clash (-> (bool) bool) (-> (int) bool)
(add-test! '(lambda (x) 
	      (let ((id (lambda (a) a)))
		(let ((id2 (if #t
			       id
			       x)))
		  (if (id2 #f) (id2 3) 4))))
	   test-recon-failed)

(add-test! '((lambda (x) 
	       (let ((id (lambda (a) a)))
		 (if #t
		     id
		     x)))
	     (lambda (z) z))
	   '(generic (?z-2) (-> (?z-2) ?z-2)))


(add-test! '(lambda (x)
	      (begin (set! x 3)
		     x))
	   '(-> (int) int))

(add-test! '((lambda (+ *)
	       (primop + 1 2))
	     (lambda (x) x)
	     (lambda (x) (* x x)))
	   'int)
