(ns com.tailrecursion.blacktide.invocation-test
"Direct protocol calls select interface dispatch before falling back to the
method Var. Compare values, failures and evaluation traces with Clojure."
(:require [clojure.test :refer [deftest is]]
[clojure.core.protocols :as p]
[com.tailrecursion.blacktide.backend-fixture :as backend]
[com.tailrecursion.blacktide.jvm-fixture]))
(deftype DirectReduction [events]
p/CollReduce
(coll-reduce [_ f] (swap! events conj :interface) (f :direct))
(coll-reduce [_ f init] (swap! events conj :interface) (f init :direct)))
(defn observe [form mode direct? target-kind replacement]
(binding [*compiler-options* {:direct-linking direct?}]
(let [events (atom []) f ((backend/compile-program form mode) nil)
target (case target-kind :direct (DirectReduction. events) :nil nil :vector [1 2])
replacement (case replacement :function (fn [& xs] (swap! events conj :fallback) (vec (rest xs)))
:nil nil :invalid 42)]
(with-redefs [p/coll-reduce replacement]
(let [out (try {:value (f target events)} (catch Throwable e {:throw (class e)}))]
(assoc out :trace @events))))))
(defn record-result [events tag & _] (swap! events conj tag) tag)
(defn wrapped [body location]
(case location
:region (list 'fn '[target events] (list 'loophole [] body))
:ordinary (list 'fn '[target events] body)))
(deftest direct-protocol-and-first-class-calls-stay-distinct
(doseq [body '[(clojure.core.protocols/coll-reduce target vector)
(clojure.core.protocols/coll-reduce target vector 7)
((do clojure.core.protocols/coll-reduce) target vector 7)
(let [f clojure.core.protocols/coll-reduce] (f target vector 7))]
target [:direct :nil :vector] direct? [false true]
mode [:default :dispatcher]]
(is (= (observe (wrapped body :ordinary) :default direct? target :function)
(observe (wrapped body :region) mode direct? target :function))
(pr-str [body target direct? mode]))))
(deftest fallback-root-is-read-after-the-target-before-later-arguments
(let [body '(clojure.core.protocols/coll-reduce
(do (swap! events conj :target)
(alter-var-root #'clojure.core.protocols/coll-reduce
(constantly (partial com.tailrecursion.blacktide.invocation-test/record-result events :early))) target)
(do (swap! events conj :argument)
(alter-var-root #'clojure.core.protocols/coll-reduce
(constantly (partial com.tailrecursion.blacktide.invocation-test/record-result events :late))) vector)
(do (swap! events conj :initial) 7))]
(doseq [target [:direct :nil :vector] mode [:default :dispatcher]]
(is (= (observe (wrapped body :ordinary) :default false target :function)
(observe (wrapped body :region) mode false target :function))
(pr-str [target mode])))))
(deftest invalid-fallback-roots-fail-after-arguments
(doseq [body '[(clojure.core.protocols/coll-reduce
(do (swap! events conj :target) target)
(do (swap! events conj :argument) vector)
(do (swap! events conj :initial) 7))
(clojure.core.protocols/coll-reduce target vector
(do (swap! events conj :throw) (throw (ex-info "argument" {}))))]
target [:direct :nil] replacement [:nil :invalid]
mode [:default :dispatcher]]
(is (= (observe (wrapped body :ordinary) :default false target replacement)
(observe (wrapped body :region) mode false target replacement))
(pr-str [body target replacement mode]))))
(deftest protocol-operands-may-change-region-state
(let [body '(clojure.core.protocols/coll-reduce
(do (swap! events conj :target) target)
(do (swap! events conj :argument) vector)
(do (swap! events conj :initial) 7))
source '(fn [target events]
(loophole [x nil]
(clojure.core.protocols/coll-reduce
(do (swap! events conj :target) (setq x target))
(do (swap! events conj :argument) (setq x vector))
(do (swap! events conj :initial) (setq x 7)))))]
(doseq [target [:direct :nil :vector] mode [:default :dispatcher]]
(is (= (observe (wrapped body :ordinary) :default false target :function)
(observe source mode false target :function))))))
(defmacro remove-protocol [value]
(alter-meta! #'p/coll-reduce dissoc :protocol)
value)
(deftest protocol-facts-are-captured-after-arguments-before-following-forms
(let [metadata (meta #'p/coll-reduce)
observe (fn [body mode location]
(try (reset-meta! #'p/coll-reduce metadata)
(observe (wrapped body location) mode false :direct :function)
(finally (reset-meta! #'p/coll-reduce metadata))))]
(doseq [body '[(do (clojure.core.protocols/coll-reduce target vector 7)
(com.tailrecursion.blacktide.invocation-test/remove-protocol :done))
(clojure.core.protocols/coll-reduce
(com.tailrecursion.blacktide.invocation-test/remove-protocol target) vector 7)]
mode [:default :dispatcher]]
(is (= (observe body :default :ordinary) (observe body mode :region))
(pr-str [body mode])))))
(deftest invalid-direct-protocol-arity-is-rejected-at-compilation
(doseq [body '[(clojure.core.protocols/coll-reduce)
(clojure.core.protocols/coll-reduce target)
(clojure.core.protocols/coll-reduce target vector 7 8)]
location [:ordinary :region] mode [:default :dispatcher]]
(is (thrown? clojure.lang.Compiler$CompilerException
(backend/compile-program (wrapped body location) mode))
(pr-str [body location mode]))))
;; Existing Java interfaces can also be protocol dispatch targets. They expose
;; conversions that the Object-only interfaces generated by defprotocol cannot.
(def java-list {:on 'java.util.List :method-map {:list-set :set}})
(defn list-set [& _] :fallback)
(alter-meta! #'list-set assoc :protocol #'java-list)
(deftest interface-conversions-are-conditional-and-ordered
(let [observe (fn [body mode location direct?]
(let [events (atom []) f ((backend/compile-program (wrapped body location) mode) nil)
target (when direct? (java.util.ArrayList. [1 2]))]
[(try [:value (f target events)] (catch Throwable e [:throw (class e)]))
(when target (vec target)) @events]))]
(doseq [index [0 2147483648 nil] direct? [false true]
mode [:default :dispatcher]
:let [body (list 'com.tailrecursion.blacktide.invocation-test/list-set 'target
(list 'do '(swap! events conj :index) index)
'(do (swap! events conj :value) :new))]]
(is (= (observe body :default :ordinary direct?) (observe body mode :region direct?))
(pr-str [index direct? mode])))))
(deftest interface-functional-arguments
(let [body '(com.tailrecursion.blacktide.jvm-fixture/map-compute target :key
(do (swap! events conj :argument) vector))
observe (fn [mode location direct?]
(let [events (atom []) f ((backend/compile-program (wrapped body location) mode) nil)
target (when direct? (java.util.HashMap. {:key :old}))]
[(f target events) (when target (into {} target)) @events]))]
(doseq [direct? [false true] mode [:default :dispatcher]]
(is (= (observe :default :ordinary direct?) (observe mode :region direct?))
(pr-str [direct? mode])))))