# Specification: macros, quoting and syntax-quote.
(use ../support/harness)

(defspec "macros / quoting"
  ["quote symbol"       "(quote a)"         "(quote a)"]
  ["quote list"         "[1 2 3]"     "(quote (1 2 3))"]
  ["quote nested"       "[1 [2 3]]"   "(quote (1 (2 3)))"]
  ["quote sugar"        "'a"          "'a"]
  ["syntax-quote literal" "[1 2]"     "`[1 2]"]
  ["unquote"            "[1 2 3]"     "(let [x 2] `[1 ~x 3])"]
  ["unquote-splicing"   "[1 2 3 4]"   "(let [xs [2 3]] `[1 ~@xs 4])"]
  ["unquote in list"    "[3]"         "(let [x 3] `(~x))"]
  ["syntax-quote symbol qualifies" "true" "(symbol? `foo)"])

(defspec "macros / defmacro"
  ["simple macro"       "3"
   "(do (defmacro m [a b] `(+ ~a ~b)) (m 1 2))"]
  ["macro unless"       "1"
   "(do (defmacro unless [c body] `(if ~c nil ~body)) (unless false 1))"]
  ["macro with body splice" "6"
   "(do (defmacro msum [& xs] `(+ ~@xs)) (msum 1 2 3))"]
  ["macroexpand-1"      "true"
   "(do (defmacro m [x] `(inc ~x)) (list? (macroexpand-1 '(m 5))))"]
  ["gensym unique"      "false"
   "(= (gensym) (gensym))"]
  ["gensym# in template" "true"
   "(do (defmacro m [] `(let [x# 1] x#)) (= 1 (m)))"]
  # An #() lambda written inside a syntax-quote: its generated params must not be
  # qualified to the ns (a qualified symbol is not a valid param). hiccup's
  # compiler emits `(let [sb# ..] (run! #(.append sb# %) ..)) (jolt-nkx).
  ["#() inside syntax-quote" "[2 4 6]"
   "(do (defmacro m [] `(mapv #(* % 2) [1 2 3])) (m))"]
  ["#() + auto-gensym share in template" "\"ab\""
   "(do (defmacro m [] `(let [sb# (StringBuilder.)] (mapv #(.append sb# %) [\"a\" \"b\"]) (.toString sb#))) (m))"])

# Core macros ported from Janet to the Clojure overlay (jolt-1j0 phase 3,
# jolt-core/clojure/core/30-macros.clj).
(defspec "macros / core-overlay"
  ["if-not true branch"  ":then"  "(if-not false :then :else)"]
  ["if-not else branch"  ":else"  "(if-not true :then :else)"]
  ["if-not no else"      "nil"    "(if-not true :then)"]
  ["if-not no else hit"  ":then"  "(if-not false :then)"]
  ["comment -> nil"      "nil"    "(comment a b c)"]
  ["comment in do"       "42"     "(do (comment ignored) 42)"]
  ["if-let then"         "6"      "(if-let [x 5] (inc x) :none)"]
  ["if-let else"         ":none"  "(if-let [x nil] (inc x) :none)"]
  ["if-let else scope"   "9"      "(let [x 9] (if-let [x nil] :t x))"]
  ["if-some zero"        "1"      "(if-some [x 0] (inc x) :none)"]
  ["if-some nil"         ":none"  "(if-some [x nil] x :none)"]
  ["when-some multi"     "14"     "(when-some [x 7] (inc x) (* x 2))"]
  ["when-some nil"       "nil"    "(when-some [x nil] x)"]
  ["while loop"          "3"      "(let [a (atom 0)] (while (< @a 3) (swap! a inc)) @a)"]
  ["dotimes sum"         "10"     "(let [a (atom 0)] (dotimes [i 5] (swap! a + i)) @a)"]
  ["as-> threads"        "12"     "(as-> 5 x (+ x 1) (* x 2))"]
  ["as-> no forms"       "5"      "(as-> 5 x)"]
  ["some-> through"      "6"      "(some-> {:a {:b 5}} :a :b inc)"]
  ["some-> short-circuit" "nil"   "(some-> {:a nil} :a :b)"]
  ["some->> through"     "9"      "(some->> [1 2 3] (map inc) (reduce +))"]
  ["some->> nil"         "nil"    "(some->> nil (map inc))"]
  ["doto returns obj"    "[1 2]"  "(deref (doto (atom []) (swap! conj 1) (swap! conj 2)))"]
  ["when-first"          "20"     "(when-first [x [10 20 30]] (* x 2))"]
  ["when-first empty"    "nil"    "(when-first [x []] :body)"]
  ["when-first nil coll" "nil"    "(when-first [x nil] :body)"]
  ["when-first range"    "0"      "(when-first [x (range 5)] x)"]
  ["cond->> threads"     "12"     "(cond->> 5 true (+ 1) false (* 100) true (* 2))"]
  ["cond->> skip"        "10"     "(cond->> 10 false (+ 1))"]
  ["assert pass"         ":ok"    "(do (assert (= 1 1)) :ok)"]
  ["assert throws"       ":threw" "(try (assert (= 1 2)) (catch :default e :threw))"]
  ["assert message"      "\"nope\"" "(try (assert false \"nope\") (catch :default e (ex-message e)))"]
  ["delay value"         "42"     "(deref (delay 42))"]
  ["delay forces once"   "1"      "(let [c (atom 0) d (delay (swap! c inc))] @d @d @c)"]
  ["future deref"        "9"      "(deref (future (* 3 3)))"]
  ["letfn simple"        "25"     "(letfn [(sq [x] (* x x))] (sq 5))"]
  ["letfn mutual"        "true"   "(letfn [(ev? [n] (if (zero? n) true (od? (dec n)))) (od? [n] (if (zero? n) false (ev? (dec n))))] (ev? 8))"]
  ["condp match"         ":two"   "(condp = 2 1 :one 2 :two 3 :three)"]
  ["condp default"       ":else"  "(condp = 9 1 :one 2 :two :else)"]
  ["condp :>> form"      "\"got 2\"" "(condp some [1 2 3] #{0 9} :>> (fn [x] (str \"got \" x)) #{2 6} :>> (fn [x] (str \"got \" x)))"]
  ["condp no match"      ":threw" "(try (condp = 9 1 :one) (catch :default e :threw))"]
  ["binding rebinds"     "99"     "(do (def ^:dynamic *bx* 10) (binding [*bx* 99] *bx*))"]
  ["binding restores"    "10"     "(do (def ^:dynamic *by* 10) (binding [*by* 99] *by*) *by*)"]
  ["binding seen by fn"  "7"      "(do (def ^:dynamic *bz* 0) (defn rdz [] *bz*) (binding [*bz* 7] (rdz)))"])

# time returns the body value (the timing line goes to *out*); with-redefs
# temporarily rebinds var roots and restores on exit (even on throw);
# macroexpand expands repeatedly until the head is no longer a macro.
(defspec "macros / time, with-redefs, macroexpand"
  ["time returns value"  "3"      "(time (+ 1 2))"]
  ["with-redefs rebinds" "42"     "(do (defn wr-f [] 1) (with-redefs [wr-f (fn [] 42)] (wr-f)))"]
  ["with-redefs restores" "1"     "(do (defn wr-g [] 1) (with-redefs [wr-g (fn [] 42)]) (wr-g))"]
  ["with-redefs restores on throw" "1"
   "(do (defn wr-h [] 1) (try (with-redefs [wr-h (fn [] 42)] (throw (ex-info \"x\" {}))) (catch :default e nil)) (wr-h))"]
  ["with-redefs-fn"      "42"     "(do (defn wr-i [] 1) (with-redefs-fn {(var wr-i) (fn [] 42)} (fn [] (wr-i))))"]
  ["macroexpand full"    "true"   "(let [e (macroexpand (quote (when-not false 1)))] (= (quote if) (first e)))"]
  ["macroexpand non-macro" "[1 2]" "(macroexpand (quote [1 2]))"])

# defmacro accepts the arity-clause form (defmacro name ([params] body...)), a
# leading docstring, and ^{:map} metadata on the name (jolt-whp, jolt-8w2).
(defspec "macros / defmacro arity-clause & name metadata"
  ["arity-clause form"    "10"  "(do (defmacro tw ([x] (list (quote *) x 2))) (tw 5))"]
  ["docstring + arity"    "15"  "(do (defmacro th \"triple\" ([x] (list (quote *) x 3))) (th 5))"]
  ["^{:map} name meta"    "7"   "(do (defmacro ^{:private true} pm [] 7) (pm))"]
  ["multi-form body"      "6"   "(do (defmacro mb ([a b] (list (quote +) a b))) (mb 2 4))"])

# Multi-arity defmacro (dispatch on arg count) and the docstring + attr-map +
# params form — both needed to run real Clojure macros, e.g.
# clojure.tools.logging/log (4 arities) and its level macros (jolt-q8l, jolt-qnr).
(defspec "macros / defmacro multi-arity & attr-map"
  ["multi-arity 1"   "6"  "(do (defmacro ma ([a] (list (quote +) a 1)) ([a b] (list (quote +) a b))) (ma 5))"]
  ["multi-arity 2"   "5"  "(do (defmacro ma ([a] (list (quote +) a 1)) ([a b] (list (quote +) a b))) (ma 2 3))"]
  ["arity delegates" "[:d nil 9]"
   "(do (defmacro lg ([m] `(lg :d nil ~m)) ([l t m] (list (quote vector) l t m))) (lg 9))"]
  ["doc + attr-map + params" "10"
   "(do (defmacro am \"doc\" {:arglists (quote ([x]))} [x] (list (quote inc) x)) (am 9))"]
  ["doc + attr-map + variadic" "6"
   "(do (defmacro vg \"d\" {:arglists (quote ([& a]))} [& xs] `(+ ~@xs)) (vg 1 2 3))"])
