Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 9 additions & 5 deletions clj/src/cljd/compiler.cljc
Original file line number Diff line number Diff line change
Expand Up @@ -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."
Expand All @@ -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]
Expand Down
26 changes: 23 additions & 3 deletions clj/src/cljd/core.cljd
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down Expand Up @@ -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}}
Expand Down Expand Up @@ -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]
Expand All @@ -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]
Expand Down
40 changes: 40 additions & 0 deletions clj/test/cljd/test_clojure/core_test_cljd.cljd
Original file line number Diff line number Diff line change
Expand Up @@ -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})))))