diff --git a/clj/src/cljd/compiler.cljc b/clj/src/cljd/compiler.cljc index b734f511..e8cd06b9 100644 --- a/clj/src/cljd/compiler.cljc +++ b/clj/src/cljd/compiler.cljc @@ -2230,12 +2230,14 @@ (defn cljd-u32 [n] (bit-and 0xFFFFFFFF n)) +(defn cljd-i32 [n] (if (>= n 0x80000000) (- n 0x100000000) n)) + (defn cljd-hash-combine [seed hash] (cljd-u32 (bit-xor seed (+ hash 0x9e3779b9 (bit-shift-left seed 6) - (bit-shift-right seed 2))))) + (bit-shift-right (cljd-i32 seed) 2))))) (defn cljd-hash "Returns the hash for x in cljd." @@ -2249,11 +2251,13 @@ (char? x) (hash (str x)) (symbol? x) (cljd-hash-combine (cljd-u32 (clojure.lang.Murmur3/hashUnencodedChars (name x))) - (hash (namespace x))) + (if-let [ns (namespace x)] (.hashCode ^String ns) 0)) (keyword? x) - (cljd-hash-combine - (or (some-> x namespace cljd-hash) 0) - (cljd-hash (name x))) + (cljd-u32 + (+ (cljd-hash-combine + (cljd-u32 (clojure.lang.Murmur3/hashUnencodedChars (name x))) + (if-let [ns (namespace x)] (.hashCode ^String ns) 0)) + 0x9e3779b9)) :else (throw (ex-info (str "cljd-hash not implemented for " (class x)) {:x x}))))) (defn emit-case* [[op expr clauses default] env] diff --git a/clj/src/cljd/core.cljd b/clj/src/cljd/core.cljd index 51867f0a..ba9891f8 100644 --- a/clj/src/cljd/core.cljd +++ b/clj/src/cljd/core.cljd @@ -1870,7 +1870,10 @@ (zero? idx) (keyword "" (.substring s 1)) :else (keyword (.substring s 0 idx) (.substring s (inc idx))))))) ([ns name] - (Keyword. ns name (hash-combine (if ns (hash-string* ns) 0) (hash-string* name))))) + (Keyword. ns name + (u32 (+ (hash-combine (m3-hash-unencoded-chars name) + (if ns (string-hashcode* ns) 0)) + 0x9e3779b9))))) (defn ^bool keyword? [x] @@ -3034,6 +3037,13 @@ :inline-arities #{1}} [x] (.& 0xFFFFFFFF x)) +(defn ^int i32 + ;; Reinterpret the low 32 bits as a signed int32 (in 64-bit Dart int). + ;; Inverse of u32. Use for JVM-compatible signed-int32 arithmetic shifts. + {:inline (fn [n] `(let [n# ~n] (if (.>= n# 0x80000000) (.- n# 0x100000000) n#))) + :inline-arities #{1}} + [^int n] (if (>= n 0x80000000) (- n 0x100000000) n)) + (defn ^int u32-add {:inline (fn [x y] `(u32 (.+ ~(hint-as x `int) ~(hint-as y `int)))) :inline-arities #{2}} @@ -3137,7 +3147,7 @@ (u32 (bit-xor seed (+ hash 0x9e3779b9 (u32-bit-shift-left seed 6) - (u32-bit-shift-right seed 2))))) + (bit-shift-right (i32 seed) 2))))) ;;http://hg.openjdk.java.net/jdk7u/jdk7u6/jdk/file/8c2c5d63a17e/src/share/classes/java/lang/String.java (defn- ^int hash-string* [^String s] @@ -3149,10 +3159,20 @@ (m3-hash-u32 hash))) 0))) +;; raw String.hashCode polynomial — matches Util/hash (no Murmur finalizer) +(defn- ^int string-hashcode* [^String s] + (let [len (.-length s)] + (if (pos? len) + (loop [^int i 0 ^int h 0] + (if (< i len) + (recur (inc i) (u32 (+ (u32-mul 31 h) (.codeUnitAt s i)))) + h)) + 0))) + (defn- ^int hash-symbol [^Symbol sym] (hash-combine (m3-hash-unencoded-chars (.-name sym)) - (hash-string* (or (.-ns sym) "")))) + (if-let [ns (.-ns sym)] (string-hashcode* ns) 0))) (deftype HashCache [^:mutable ^#/(Map dynamic int) young ^:mutable ^#/(Map dynamic int) old] diff --git a/clj/test/cljd/test_clojure/core_test_cljd.cljd b/clj/test/cljd/test_clojure/core_test_cljd.cljd index 979ee10c..3e2949df 100644 --- a/clj/test/cljd/test_clojure/core_test_cljd.cljd +++ b/clj/test/cljd/test_clojure/core_test_cljd.cljd @@ -1209,3 +1209,43 @@ (is (true? @a)) (reset! $ a)) (is (false? @@$)))) + +(deftest testing-hash-jvm-compat + ;; reference values from JVM Clojure 1.12.0 (matches CLJS) + (testing "keyword" + (is (= -2123407586 (hash :a))) + (is (= 1482224470 (hash :b))) + (is (= -2146297393 (hash :k))) + (is (= -1386151538 (hash :foo/bar))) + (is (= 1268894036 (hash :foo)))) + (testing "symbol" + (is (= -482876059 (hash 'a))) + (is (= -1172211299 (hash 'b))) + (is (= -1172211204 (hash 'a/b))) + (is (= 254379989 (hash 'foo/bar)))) + (testing "non-ASCII names" + (is (= 1223535072 (hash :λ/Ω)))) + (testing "long names exercise the polynomial loop past int32 wraparound" + (let [s (apply str (repeat 100 "x"))] + (is (= 1441689385 (hash (keyword s)))))) + (testing "vector of ints unaffected (regression check)" + (is (= 736442005 (hash [1 2 3])))) + (testing "string hash unaffected (regression check)" + ;; (hash s) routes through hash-string which is hash-string* + cache; + ;; ensures the new sign-coercion in hash-combine didn't perturb that path. + (is (= 1715862179 (hash "hello"))) + (is (= 0 (hash ""))))) + +(deftest testing-hash-equality-contract + ;; = implies = hash. The defining contract; assert directly. + (testing "interning invariance" + (is (= (hash :a) (hash (keyword "a")))) + (is (= (hash :foo) (hash (keyword nil "foo")))) + (is (= (hash :foo/bar) (hash (keyword "foo" "bar")))) + (is (= (hash 'a) (hash (symbol "a")))) + (is (= (hash 'foo/bar) (hash (symbol "foo" "bar"))))) + (testing "keyword vs same-name symbol hash distinctly" + (is (not= (hash :foo) (hash 'foo))) + (is (not= (hash :foo/bar) (hash 'foo/bar)))) + (testing "keyword-keyed map (transitively asserts element parity)" + (is (= 161871944 (hash {:a 1 :b 2})))))