Welcome! Please see the About page for a little more info on how this works.

+1 vote
in Macros by
closed by

When case statements use hashing and have a hash collision, they embed a condp expression. However, this expression inserts the constants directly, without quoting. If any constants include a symbol, then these will lead to an error.

For example:

(case 'a #{a} 1 :foo 2 a 3)

Leads to: Unable to resolve symbol: a in this context

This can be seen when expanding the macro:

(macroexpand '(case 'a #{a} 1 :foo 2 a 3))
(let* [G__151 (quote a)] (case* G__151 0 1 (throw (java.lang.IllegalArgumentException. (clojure.core/str "No matching clause: " G__151))) {0 [-1640525200 (clojure.core/condp clojure.core/= G__151 #{a} 1 a 3 (throw (java.lang.IllegalArgumentException. (clojure.core/str "No matching clause: " G__151))))], 1 [:foo 2]} :compact :hash-equiv #{0}))

This shows a call to case*. The first and second arguments are 0, 1. When these are used with clojure.core/shift-mask both '#{a} and 'a will hash to 0.

The 5th argument contains a map of hash codes to values, which contains:

{0 [-1640525200 (clojure.core/condp clojure.core/= G__151
                  #{a} 1
                  a 3
                  (throw (java.lang.IllegalArgumentException. (clojure.core/str "No matching clause: " G__151))))],
 1 [:foo 2]}

This condp block contains unquoted values for #{a} and a, so they will incorrectly attempt to match on the value of the current binding for a (if such a binding exists).

Since case will interpret every test-constant as a literal (explicitly stating that they need not be quoted), these values should be quoted when they appear in an embedded Clojure expression like this this condp.

The following is a suggested patch to correct the generated code:

diff --git a/src/clj/clojure/core.clj b/src/clj/clojure/core.clj
index 0dba6fcd..b21d4556 100644
--- a/src/clj/clojure/core.clj
+++ b/src/clj/clojure/core.clj
@@ -6690,7 +6690,7 @@ fails, attempts to require sym's namespace and retries."
                       (next ks) (next vs))
         assoc-multi (fn [m h bucket]
-                      (let [testexprs (apply concat bucket)
+                      (let [testexprs (mapcat (fn [kv] [(list 'quote (first kv)) (second kv)]) bucket)
                             expr `(condp = ~expr-sym ~@testexprs ~default)]
                         (assoc m h expr)))
         hmap (reduce1
diff --git a/test/clojure/test_clojure/control.clj b/test/clojure/test_clojure/control.clj
index 92846ad3..f3fe436b 100644
--- a/test/clojure/test_clojure/control.clj
+++ b/test/clojure/test_clojure/control.clj
@@ -421,7 +421,14 @@
          :b 1
          :c -2
          :d 4294967296
-         :d 3))
+         :d 3)
+    (are [result input] (= result (case input
+                                    #{a} :set
+                                    :foo :keyword
+                                    a :symbol))
+         :symbol 'a
+         :keyword :foo
+         :set '#{a}))
   (testing "test warn for hash collision"
      #"Performance warning, .*:\d+ - hash collision of some case test constants; if selected, those entries will be tested sequentially..*\r?\n"
closed with the note: Released in 1.11.0-alpha4

1 Answer

0 votes
Thank you Fogus!
Does the patch look OK? (If not, then I'm interested in learning)