diff --git a/src/main/clojure/cljs/core.cljc b/src/main/clojure/cljs/core.cljc index 18fef7f68..c735659bf 100644 --- a/src/main/clojure/cljs/core.cljc +++ b/src/main/clojure/cljs/core.cljc @@ -626,108 +626,192 @@ `(when-not (exists? ~qualified) (def ~x ~init)))) +(core/defn ^:private destvec* + [pb bvec b val] + (core/let [gvec (gensym "vec__") + gseq (gensym "seq__") + gfirst (gensym "first__") + has-rest (some #{'&} b)] + (core/loop [ret (core/let [ret (conj bvec gvec val)] + (if has-rest + (conj ret gseq (core/list `seq gvec)) + ret)) + n 0 + bs b + seen-rest? false] + (if (seq bs) + (core/let [firstb (first bs)] + (core/cond + (= firstb '&) (recur (pb ret (second bs) gseq) + n + (nnext bs) + true) + (= firstb :as) (pb ret (second bs) gvec) + :else (if seen-rest? + (throw #?(:clj (new Exception "Unsupported binding form, only :as can follow & parameter") + :cljs (new js/Error "Unsupported binding form, only :as can follow & parameter"))) + (recur (pb (if has-rest + (conj ret + gfirst `(first ~gseq) + gseq `(next ~gseq)) + ret) + firstb + (if has-rest + gfirst + (core/list `nth gvec n nil))) + (core/inc n) + (next bs) + seen-rest?)))) + ret)))) + +(core/defn ^:private destmap* + [pb bvec b v] + (core/let [gmap (gensym "map__") + gignore (gensym "ignore__") + defaults (:or b) + defaults-as (:defaults b) + _ (core/when (core/and defaults-as (core/not defaults)) + #?(:clj (throw (new IllegalArgumentException "Can't specify :defaults without :or")) + :cljs (throw (new js/Error "Can't specify :defaults without :or")))) + b (dissoc b :defaults) + gdefaults (core/when defaults (zipmap (keys defaults) (repeatedly #(gensym "default__")))) + select (:select b) + all (:all b) + xf (core/fn [mk] + (core/let [mkns (namespace mk) + mkn (name mk)] + (core/cond + (.startsWith mkn "keys") #(keyword (core/or mkns (namespace %)) (name %)) + (.startsWith mkn "syms") #(core/list `quote (core/symbol (core/or mkns (namespace %)) (name %))) + (.startsWith mkn "strs") core/str + :else (throw #?(:clj (new Exception (core/str "Unsupported map directive: " mk)) + :cljs (new js/Error (core/str "Unsupported map directive: " mk))))))) + ret (reduce (core/fn [ret e] + (conj ret (val e) (defaults (key e)))) + bvec gdefaults) + ret (core/-> ret (conj gmap) (conj v) + (conj gmap) + (conj `(--destructure-map ~gmap)) + ((core/fn [ret] + (if (:as b) + (conj ret (:as b) gmap) + ret)))) + bes (dissoc b :as :or :select :all) + localize (core/fn [bb] + (if #?(:clj (core/instance? clojure.lang.Named bb) + :cljs (cljs.core/implements? INamed bb)) + (with-meta (core/symbol nil (name bb)) (meta bb)) bb)) + push1 (core/fn [ret bb bk req?] + (core/let [getter (if req? `cljs.core/req! `cljs.core/get) + local (localize bb) + local-default? (contains? defaults local) + key-default? (contains? defaults bk) + bv (if (core/or local-default? key-default?) + (if (core/and local-default? key-default?) + #?(:clj (throw (new Exception + (core/str "Multiple :or defaults for same key: " bk " '" local "'"))) + :cljs (throw (new js/Error + (core/str "Multiple :or defaults for same key: " bk " '" local "'")))) + (if req? + #?(:clj (throw (new Exception + (core/str "Can't supply default value for required key: " bk))) + :cljs (throw (new js/Error + (core/str "Can't supply default value for required key: " bk)))) + (core/list `cljs.core/get gmap bk (if local-default? (gdefaults local) (gdefaults bk))))) + (core/list getter gmap bk))] + (if (ident? bb) + (core/-> ret (conj local bv)) + (pb ret bb bv)))) + retsel + (core/loop [ret ret, sel #{}, bes bes, b->k {}, subs nil, suba nil] + (if (seq bes) + (core/let [be (first bes), bb (key be), bk (val be)] + (if (core/keyword? bb) + (core/let [dir bb + tr (xf bb) + req? (.endsWith (name bb) "!") + retsel + (core/loop [ret ret, sel sel, bbs (seq bk), preamp? true, b->k b->k] + (if (seq bbs) + (core/let [bb (first bbs)] + (if (= bb '&) + (if preamp? + (recur ret sel (next bbs) false b->k) + #?(:clj (throw (new IllegalArgumentException (core/str "& can only appear once in " dir))) + :cljs (throw (new js/Error (core/str "& can only appear once in " dir))))) + (core/let [_ + (core/when (core/and (not preamp?) (core/symbol? bb)) + #?(:clj (throw + (new IllegalArgumentException + (core/str "'" bb + "' - binding symbols can only appear before '&', use keys after"))) + :cljs (throw + (new js/Error + (core/str "'" bb + "' - binding symbols can only appear before '&', use keys after"))))) + bk (if preamp? (tr bb) bb)] + (recur (if (core/or preamp? req?) + (push1 ret (if preamp? bb gignore) bk req?) + ret) + (conj sel bk) + (next bbs) preamp? + (if preamp? (assoc b->k (localize bb) bk) b->k))))) + {:ret ret, :sel sel, :b->k b->k}))] + (recur (:ret retsel) (:sel retsel) (next bes) (:b->k retsel) subs suba)) + (core/let [subsel? (core/and select (map? bb)) + bb (if (core/or (core/not subsel?) (:select bb)) + bb + (assoc bb :select (gensym "select__"))) + subs (if subsel? (assoc subs bk (:select bb)) subs) + suball? (core/and all (map? bb)) + bb (if (core/or (core/not suball?) (:all bb)) + bb + (assoc bb :all (gensym "all__"))) + suba (if suball? (assoc suba bk (:all bb)) suba) + b->k (if (core/symbol? bb) (assoc b->k bb bk) b->k)] + (recur (push1 ret bb bk false) (conj sel bk) (next bes) b->k subs suba)))) + {:ret ret, :sel sel, :b->k b->k :subs subs :suba suba})) + ret (:ret retsel), sel (:sel retsel), b->k (:b->k retsel) + new-or-code (core/and defaults (core/or defaults-as select all)) + bk #(if (core/symbol? %) + (core/let [bk (b->k %)] + (core/when (core/and new-or-code (core/not bk)) + #?(:clj (throw (new IllegalArgumentException (core/str "symbol " % " in :or does not refer to a binding"))) + :cljs (throw (new js/Error (core/str "symbol " % " in :or does not refer to a binding"))))) + bk) + %) + dm (core/when defaults (dissoc (zipmap (map bk (keys gdefaults)) (vals gdefaults)) nil)) + _ (core/and new-or-code (core/not= (count (select-keys dm sel)) (count defaults)) + #?(:clj (throw (new IllegalArgumentException (core/str "keys " + (apply disj (set (keys dm)) sel) + " appear only in :or"))) + :cljs (throw (new js/Error (core/str "keys " + (apply disj (set (keys dm)) sel) + " appear only in :or"))))) + ret (if select + (conj ret select `(when-let [mm# (merge (some-vals ~dm) ~gmap (some-vals ~(:subs retsel)))] + (select-keys mm# ~sel))) + ret) + ret (if all + (conj ret all `(merge (some-vals ~dm) ~gmap (some-vals ~(:suba retsel)))) + ret) + ret (if defaults-as (conj ret defaults-as dm) ret)] + ret)) + (core/defn destructure [bindings] (core/let [bents (partition 2 bindings) pb (core/fn pb [bvec b v] - (core/let [pvec - (core/fn [bvec b val] - (core/let [gvec (gensym "vec__") - gseq (gensym "seq__") - gfirst (gensym "first__") - has-rest (some #{'&} b)] - (core/loop [ret (core/let [ret (conj bvec gvec val)] - (if has-rest - (conj ret gseq (core/list `seq gvec)) - ret)) - n 0 - bs b - seen-rest? false] - (if (seq bs) - (core/let [firstb (first bs)] - (core/cond - (= firstb '&) (recur (pb ret (second bs) gseq) - n - (nnext bs) - true) - (= firstb :as) (pb ret (second bs) gvec) - :else (if seen-rest? - (throw #?(:clj (new Exception "Unsupported binding form, only :as can follow & parameter") - :cljs (new js/Error "Unsupported binding form, only :as can follow & parameter"))) - (recur (pb (if has-rest - (conj ret - gfirst `(first ~gseq) - gseq `(next ~gseq)) - ret) - firstb - (if has-rest - gfirst - (core/list `nth gvec n nil))) - (core/inc n) - (next bs) - seen-rest?)))) - ret)))) - pmap - (core/fn [bvec b v] - (core/let [gmap (gensym "map__") - defaults (:or b)] - (core/loop [ret (core/-> bvec (conj gmap) (conj v) - (conj gmap) (conj `(--destructure-map ~gmap)) - ((core/fn [ret] - (if (:as b) - (conj ret (:as b) gmap) - ret)))) - bes (core/let [transforms - (reduce - (core/fn [transforms mk] - (if (core/keyword? mk) - (core/let [mkns (namespace mk) - mkn (name mk)] - (core/cond (= mkn "keys") (assoc transforms mk #(keyword (core/or mkns (namespace %)) (name %))) - (= mkn "syms") (assoc transforms mk #(core/list `quote (symbol (core/or mkns (namespace %)) (name %)))) - (= mkn "strs") (assoc transforms mk core/str) - :else transforms)) - transforms)) - {} - (keys b))] - (reduce - (core/fn [bes entry] - (reduce #(assoc %1 %2 ((val entry) %2)) - (dissoc bes (key entry)) - ((key entry) bes))) - (dissoc b :as :or) - transforms))] - (if (seq bes) - (core/let [bb (key (first bes)) - bk (val (first bes)) - local (if #?(:clj (core/instance? clojure.lang.Named bb) - :cljs (cljs.core/implements? INamed bb)) - (with-meta (symbol nil (name bb)) (meta bb)) - bb) - bv (if (contains? defaults local) - (core/list 'cljs.core/get gmap bk (defaults local)) - (core/list 'cljs.core/get gmap bk))] - (recur - (if (core/or (core/keyword? bb) (core/symbol? bb)) ;(ident? bb) - (core/-> ret (conj local bv)) - (pb ret bb bv)) - (next bes))) - ret))))] - (core/cond - (core/symbol? b) (core/-> bvec (conj (if (namespace b) (symbol (name b)) b)) (conj v)) - (core/keyword? b) (core/-> bvec (conj (symbol (name b))) (conj v)) - (vector? b) (pvec bvec b v) - (map? b) (pmap bvec b v) - :else (throw - #?(:clj (new Exception (core/str "Unsupported binding form: " b)) - :cljs (new js/Error (core/str "Unsupported binding form: " b))))))) + (core/cond + (core/symbol? b) (core/-> bvec (conj b) (conj v)) + (core/vector? b) (destvec* pb bvec b v) + (core/map? b) (destmap* pb bvec b v) + :else (throw + #?(:clj (new Exception (core/str "Unsupported binding form: " b)) + :cljs (new js/Error (core/str "Unsupported binding form: " b)))))) process-entry (core/fn [bvec b] (pb bvec (first b) (second b)))] (if (every? core/symbol? (map first bents)) bindings - (core/if-let [kwbs (seq (filter #(core/keyword? (first %)) bents))] - (throw - #?(:clj (new Exception (core/str "Unsupported binding key: " (ffirst kwbs))) - :cljs (new js/Error (core/str "Unsupported binding key: " (ffirst kwbs))))) - (reduce process-entry [] bents))))) + (reduce process-entry [] bents)))) (core/defmacro ^:private return-first [& body] diff --git a/src/test/cljs/cljs/destructuring_test.cljs b/src/test/cljs/cljs/destructuring_test.cljs index d8caf7387..7837b9af5 100644 --- a/src/test/cljs/cljs/destructuring_test.cljs +++ b/src/test/cljs/cljs/destructuring_test.cljs @@ -8,7 +8,7 @@ (ns cljs.destructuring-test (:refer-clojure :exclude [iter]) - (:require [cljs.test :refer-macros [deftest testing is]] + (:require [cljs.test :refer-macros [deftest testing is are]] [clojure.string :as s] [clojure.set :as set])) @@ -224,6 +224,225 @@ (= m3 (seq-to-map-for-destructuring (list :a 1 :b 2 {:a 0}))) (= a4 nil))))) +(deftest keys-bang + (let [sample-map {:a 1 :b 2}] + (testing ":keys! happy path, binds and throws when key missing" + (is (= 1 (let [{:keys! [a b]} sample-map] a))) + (is (thrown? js/Error (let [{:keys! [a b]} {:a 1}] a)))) + (testing ":keys! with & bind and don't bind" + (is (= 1 (let [{:keys! [a & :b]} sample-map] a))) + (is (thrown? js/Error (let [{:keys! [a & :b]} {:a 1}] a)))) + (testing "nested maps with :keys! &" + (let [sample-map {:a 1 :b {:a 2 :b 3 :c 4 :d 42}} + {a :a {aa :a :as m :keys! [b c & :d]} :b} sample-map] + (is (= m (:b sample-map))) + (is (= a 1)) + (is (= aa 2)) + (is (= b 3)) + (is (= c 4)) + (is (thrown? js/Error (let [{a :a {aa :a :as m :keys! [b c & :d :e]} :b} sample-map] a))) + (is (thrown? js/Error (let [{a :a {aa :a :as m :keys! [b c & :d]} :b} (update sample-map :b dissoc :c)] a))))) + (testing "a broad range of qualified names/declarators with :keys! &" + (let [sample-map {:foo/a 1 :b 2 :foo/c 3} + {:keys! [foo/a & :b]} sample-map + {:keys! [b & :foo/c]} sample-map + sample-map2 {:foo/aa 1 :bb 2 :foo/cc 3} + {:foo/keys! [aa & :foo/cc]} sample-map2] + (is (= a 1)) + (is (= b 2)) + (is (thrown? js/Error (let [{:keys! [b & :foo/c]} (dissoc sample-map :b)] b))) + (is (thrown? js/Error (let [{:keys! [b & :foo/c]} (dissoc sample-map :foo/c)] b))) + (is (= 1 (let [{:foo/keys! [aa & :bb]} sample-map2] aa))) + (is (= aa 1)) + (is (= 1 (let [{:keys! [::a & ::b]} {::a 1 , ::b 2}] a))))) + #_(testing "that right of & is unbound (compile-time errors)" + (is (thrown? js/Error (eval '(let [{:keys! [a & :b]} sample-map] b)))) + (is (thrown? js/Error (eval '(let [{a :a {aa :a :as m :keys [b c & :e]} :b} sample-map] e)))) + (is (thrown? js/Error (eval '(let [{:keys! [foo/a & :foo/c]} sample-map] c)))) + (let [sample-map2 {:foo/aa 1 :bb 2 :foo/cc 3}] + (is (thrown? js/Error (eval '(let [{:foo/keys! [foo/aa & :foo/cc]} sample-map2] cc)))))))) + +(deftest syms-bang + (let [sample-map '{a 1 b 2}] + (testing ":syms! happy path, binds and throws when key missing" + (is (= 1 (let [{:syms! [a b]} sample-map] a))) + (is (thrown? js/Error (let [{:syms! [a b]} {:a 1}] a)))) + (testing ":syms! with & bind and don't bind" + (is (= 1 (let [{:syms! [a & 'b]} sample-map] a))) + (is (thrown? js/Error (let [{:syms! [a & 'b]} {:a 1}] a)))) + (testing "nested maps with :syms! &" + (let [sample-map '{a 1 b {a 2 b 3 c 4 d 42}} + {a 'a {aa 'a :as m :syms! [b c & 'd]} 'b} sample-map] + (is (= m ('b sample-map))) + (is (= a 1)) + (is (= aa 2)) + (is (= b 3)) + (is (= c 4)) + (is (thrown? js/Error (let [{a 'a {aa :a :as m :syms! [b c & 'd 'e]} 'b} sample-map] a))) + (is (thrown? js/Error (let [{a 'a {aa :a :as m :syms! [b c & 'd]} 'b} (update sample-map 'b dissoc 'c)] a))))) + (testing "a broad range of qualified names/declarators with :syms! &" + (let [sample-map '{foo/a 1 b 2 foo/c 3} + {:syms! [foo/a & 'b]} sample-map + {:syms! [b & 'foo/c]} sample-map + sample-map2 '{foo/aa 1 bb 2 foo/cc 3} + {:foo/syms! [aa & 'foo/cc]} sample-map2] + (is (= a 1)) + (is (= b 2)) + (is (thrown? js/Error (let [{:syms! [b & 'foo/c]} (dissoc sample-map 'b)] b))) + (is (thrown? js/Error (let [{:syms! [b & 'foo/c]} (dissoc sample-map 'foo/c)] b))) + (is (= aa 1)) + (is (= 1 (let [{:foo/syms! [aa & 'bb]} sample-map2] aa))))) + #_(testing "that right of & is unbound (compile-time errors)" + (is (thrown? js/Error (eval '(let [{:syms! [a & 'b]} sample-map] b)))) + (is (thrown? js/Error (eval '(let [{a a {aa a :as m :syms [b c & 'e]} :b} sample-map] e)))) + (is (thrown? js/Error (eval '(let [{:syms! [foo/a & 'foo/c]} sample-map] c)))) + (let [sample-map2 '{foo/aa 1 bb 2 foo/cc 3}] + (is (thrown? js/Error (eval '(let [{:foo/syms! [foo/aa & 'foo/cc]} sample-map2] cc)))))))) + +(deftest strs-bang + (let [sample-map {"a" 1 "b" 2}] + (testing ":strs! happy path, binds and throws when key missing" + (is (= 1 (let [{:strs! [a b]} sample-map] a))) + (is (thrown? js/Error (let [{:strs! [a b]} {:a 1}] a)))) + (testing ":strs! with & bind and don't bind" + (is (= 1 (let [{:strs! [a & "b"]} sample-map] a))) + (is (thrown? js/Error (let [{:strs! [a & "b"]} {:a 1}] a)))) + (testing "nested maps with :strs! &" + (let [sample-map {"a" 1 "b" {"a" 2 "b" 3 "c" 4 "d" 42}} + {a "a" {aa "a" :as m :strs! [b c & "d"]} "b"} sample-map] + (is (= m (get sample-map "b"))) + (is (= a 1)) + (is (= aa 2)) + (is (= b 3)) + (is (= c 4)) + (is (thrown? js/Error (let [{a "a" {aa "a" :as m :strs! [b c & "d" "e"]} "b"} sample-map] a))) + (is (thrown? js/Error (let [{a "a" {aa "a" :as m :strs! [b c & "d"]} "b"} (update sample-map "b" dissoc "c")] a))))) + #_(testing "that right of & is unbound (compile-time errors)" + (is (thrown? js/Error (eval '(let [{:strs! [a & "b"]} sample-map] b)))) + (is (thrown? js/Error (eval '(let [{a "a" {aa "a" :as m :keys [b c & "e"]} "b"} sample-map] e))))))) + +(deftest select-directive + (let [m {:a 1 :b 2 :c 3 :d 4 + 'sa 10 'sb 20 'sc 30 'sd 40 + "stra" 100 "strb" 200 "strc" 300 "strd" 400 + :foo/x 1000 :foo/y 2000 :foo/z 3000 + ::x 10000 ::y 20000 ::z 30000 + :nested {:aa 1 'saa 10 "straa" 100}} + + {:keys [a b & :c :z] + :keys! [d] + :select keys-sel} m + + {:syms [sa sb & 'sc 'sz] + :syms! [sd] + :select syms-sel} m + + {:strs [stra strb & "strc" "strz"] + :strs! [strd] + :select strs-sel} m + + {:foo/keys [x & :y :zz] + :foo/keys! [z] + :select qkeys-sel} m + + {::keys [x & :y :zz] + ::keys! [z] + :select aqkeys-sel} m + + {{aa :aa saa 'saa + :select nest-sel} :nested + aqx ::x + :select tl-sel} m + + {:keys! [a b & :c] + :keys [d & :z] + :or {} + :select or-sel} m + + {:keys [a b c d] + :syms [sa sb sc sd] + :strs [stra strb strc strd] + :foo/keys! [x y z] + ::keys [x y z] + nest :nested + :as mm + :select sel-mm} m] + (are [expected result] (= expected result) + keys-sel {:a 1 :b 2 :c 3 :d 4} + syms-sel '{sa 10 sb 20 sc 30 sd 40} + strs-sel {"stra" 100 "strb" 200 "strc" 300 "strd" 400} + qkeys-sel {:foo/x 1000 :foo/z 3000} + aqkeys-sel {::x 10000 ::z 30000} + nest-sel '{:aa 1, saa 10} + tl-sel '{:nested {:aa 1, saa 10} ::x 10000} + or-sel {:a 1 :b 2 :c 3 :d 4} + sel-mm mm)) + (testing "base cases" + (is (nil? (let [{{a :a} :n :select s} nil] s)) + "if you haven't supplied a map, select won't make one for no reason") + + (testing "you get what you supplied if nothing else" + (is (= {} (let [{{a :a} :n :select s} {}] s))) + (is (= {:n nil} (let [{{a :a} :n :select s} {:n nil}] s))) + (is (= {:n {}} (let [{{a :a} :n :select s} {:n {}}] s)))) + + (testing "defaults can turn nothing into something" + (is (= {:n {:a 42}} (let [{{a :a :or {a 42}} :n :select s} nil] s))) + (is (= {:n {:a 42}} (let [{{a :a :or {a 42}} :n :select s} {:n nil}] s)))))) + +(deftest select-or-defaults + (let [sample-map {:a 1, :b 2, :c {:aa 10 :bb 20}, + 'd 4 'e 5 'f {'dd 40 'ee 50}, + "g" 6 "h" 7 "i" {"gg" 60 "hh" 70},}] + (testing "happy path" + (testing ":defaults" + (is (empty? (let [{:defaults d :or {}} {}] d))) + #_(is (thrown? js/Error (eval '(let [{:defaults d :or {:a 1}} {}] d)))) + (is (= {:a 1} (let [{:keys [a] :defaults d :or {:a 1}} {}] d))) + (is (= {:a 1} (let [{:keys [a] :defaults d :or {a 1}} {}] d))) + (is (= {:a 1, 'b 2, "c" 3} (let [{b 'b, c "c", :keys [a] :defaults d :or {:a 1, 'b 2, "c" 3}} {}] d)))) + + (testing ":keys + :select + :or + defaults" + (let [{:keys [a b z & :c :d] {:keys! [aa & :bb]} :c + :or {:d 42, z :or-z} + :select m + :defaults dfs} sample-map] + (is (= 1 a)) + (is (= 2 b)) + (is (= 10 aa)) + (is (= {:z :or-z, :c {:aa 10, :bb 20}, :b 2, :d 42, :a 1} m)) + (is (= {:d 42, :z :or-z} dfs)))) + + (testing ":syms + :select + :or + defaults" + (let [{:syms [d e z & 'd 'f] {:syms! [dd & 'ee]} 'f + :or {'d 42, z :or-z} + :select m + :defaults dfs} sample-map] + (is (= 4 d)) + (is (= 5 e)) + (is (= 40 dd)) + (is (= '{f {dd 40, ee 50}, e 5, d 4, z :or-z} m)) + (is (= '{d 42, z :or-z} dfs)))) + + (testing ":strs + :select + :or + defaults" + (let [{:strs [g h z & "d" "i"] {:strs! [gg & "hh"]} "i" + :or {"d" 42, z :or-z} + :select m + :defaults dfs} sample-map] + (is (= 6 g)) + (is (= 7 h)) + (is (= 60 gg)) + (is (= {"d" 42, "z" :or-z, "i" {"gg" 60, "hh" 70}, "g" 6, "h" 7} m)) + (is (= {"d" 42, "z" :or-z} dfs)))) + + (testing "mixed things after &" + (is (= 1 (let [{:keys [a & 'b]} {:a 1}] a))) + (is (= 1 (let [{:keys! [a & 'b "c"]} {:a 1, 'b 2, "c" 3}] a)))) + + #_(testing "known compile-time errors" + (is (thrown? js/Error (eval '(let [{:keys [a] :defaults d :or {:a 1, a 1}} {}] d)))) + (is (thrown? js/Error (eval '(let [{:defaults d} {}] d)))))))) + (comment (cljs.test/run-tests)