(ns com.tailrecursion.blacktide.jumps-test
"Statement transfers, outer labels, scope reentry and bounded control graphs."
(:require [clojure.test :refer [deftest is]]
[clojure.test.check.clojure-test :refer [defspec]]
[clojure.test.check.generators :as gen]
[clojure.test.check.properties :as prop]
[com.tailrecursion.blacktide :refer [loophole label goto setq psetq]]
[com.tailrecursion.blacktide.backend-fixture :as backend]))
(defn step [n] (dec n))
(defn more? [n] (pos? n))
(defn add [a b] (+' a b))
(defn odd-value? [n] (odd? n))
(defn record! [events tag value] (swap! events conj tag) value)
(deftest basic-and-conditional-transfers
(is (= 1 (loophole [x 1] (goto end) (setq x 9) (label end) x)))
(is (nil? (loophole [] (goto done) :unreachable (label done))))
(is (nil? (loophole [] (label first) (label last))))
(is (= 0 (loophole [n 3] (label again) (setq n (step n)) (when (more? n) (goto again)) n))))
(deftest scope-reentry
(let [events (atom [])]
(is (= 1 (loophole [n 3 result nil]
(label again)
(let [x (record! events n n)]
(setq n (step n))
(if (more? n) (goto again) (setq result x))) result)))
(is (= [3 2 1] @events))))
(deftest invalid-transfers-even-in-dead-source
(doseq [[body kind] [['(goto missing) :undefined-label]
['(goto end) :undefined-label]
['(do (label x) (label x)) :duplicate-label]
['(label :keyword) :invalid-marker]
['(goto qualified/name) :invalid-marker]
['(vector (label x)) :marker-position]
['(let [] (label x)) :marker-position]
['(if true (label x) nil) :marker-position]
['(case 0 0 (label x) nil) :marker-position]
['(vector (do (goto done) 1)) :marker-position]
['(let [x (do (goto done) 1)] x) :marker-position]
['(setq x (do (goto done) 1)) :marker-position]
['(loophole [] 1) :unsupported-form]]
prefix [[] ['(goto done)]]]
(is (= kind (:kind (backend/rejection (list* 'loophole '[x 0] (concat prefix [body '(label done)])))))
(pr-str [prefix body]))))
(defn sum-through [input]
(loophole [n input total 0]
(label again)
(when-not (more? n) (goto done))
(setq total (add total n) n (step n))
(goto again)
(label done) total))
(defn fibonacci [input]
(loophole [n input a 0 b 1]
(label again)
(when-not (more? n) (goto done))
(psetq a b b (add a b))
(setq n (step n))
(goto again)
(label done) a))
(defn reference-fibonacci [n]
(loop [n n a 0 b 1] (if (zero? n) a (recur (dec n) b (+' a b)))))
(defspec bounded-algorithm-differentials {:num-tests 300 :seed 424242}
(prop/for-all [n (gen/choose 0 49)]
(and (= (reduce +' 0 (range 1 (inc n))) (sum-through n))
(= (reference-fibonacci n) (fibonacci n)))))
(deftest large-sum
(is (= 5000050000 (sum-through 100000))))
(defn reference-machine [table budget value]
(loop [state 0 budget budget value value trace []]
(if (zero? budget) [value trace]
(let [{:keys [delta yes no]} (nth table state)
next-value (+ value delta)]
(recur (if (odd? next-value) yes no) (dec budget) next-value (conj trace [state value]))))))
(defn machine-form [table budget value labels wrapped?]
(let [blocks
(mapcat (fn [state {:keys [delta yes no]}]
(let [body (list 'let ['x (list 'record! 'events [state 'value] 'value)]
(list 'setq 'value (list 'add 'x delta) 'budget '(step budget))
(list 'if '(more? budget)
(list 'if '(odd-value? value) (list 'goto (nth labels yes)) (list 'goto (nth labels no)))
'(goto done)))]
[(list 'label (nth labels state)) (if wrapped? (list 'do (list 'do body)) body)]))
(range) table)]
(list 'let ['events '(atom [])]
(list* 'loophole ['budget budget 'value value]
(concat ['(when-not (more? budget) (goto done)) (list 'goto (first labels))]
blocks ['(label done) ['value '(deref events)]])))))
(defn execute-machine [table budget value labels wrapped?]
(binding [*ns* (the-ns 'com.tailrecursion.blacktide.jumps-test)]
(try (eval (machine-form table budget value labels wrapped?))
(catch Throwable e {:compiler-error (ex-message e)}))))
(defspec generated-bounded-control-graphs {:num-tests 60 :seed 8675309}
(prop/for-all [table (gen/vector (gen/hash-map :delta (gen/choose -4 4)
:yes (gen/choose 0 2) :no (gen/choose 0 2)) 3)
budget (gen/choose 0 13) value (gen/choose -20 20)]
(let [expected (reference-machine table budget value)]
(every? true?
(for [[labels wrapped?] [['[end b c] false] ['[renamed-a renamed-b renamed-c] true]]]
(= expected (execute-machine table budget value labels wrapped?)))))))