Skip to content

Commit cdb57aa

Browse files
committed
Install print and hashp to all frames in dynamic var bindings #16
1 parent ed95350 commit cdb57aa

9 files changed

Lines changed: 114 additions & 77 deletions

File tree

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
# WIP
2+
3+
- Install print and hashp to all frames in dynamic var bindings #16
4+
15
# 1.7.1 - Jan 15, 2026
26

37
- Rename clj-kondo hooks namespace to avoid clashes #26 via @FelipeCortez

dev/user.clj

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
(ns user
22
(:require
33
[clj-reload.core :as reload]
4-
[clojure+.core :as core]
4+
[clojure+.util :as util]
55
[clojure.core.server :as server]
66
[clojure.java.io :as io]
77
[clojure.test :as test]))
@@ -44,7 +44,7 @@
4444
@test/*report-counters*))))
4545

4646
(def test-re
47-
(core/if-not-bb
47+
(util/if-not-bb
4848
#"clojure\+\.(?!test-test$|kondo-hooks$).*"
4949
#"clojure\+\.(?!test-test$|kondo-hooks$|print-test$|print$).*"))
5050

src/clojure+/core.clj

Lines changed: 1 addition & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,45 +1,6 @@
1-
(ns clojure+.core
2-
(:require
3-
[clojure.string :as str]))
1+
(ns clojure+.core)
42

53
(declare ^:private ^:dynamic *if+-syms)
6-
7-
(def ^:private runtime-version
8-
(let [v (System/getProperty "java.version")]
9-
(if (str/starts-with? v "1.")
10-
(-> (str/split v #"\.") second Long/parseLong)
11-
(-> (str/split v #"\.") first Long/parseLong))))
12-
13-
(defmacro if-java-version-gte
14-
([version if-branch]
15-
(when (<= version runtime-version)
16-
if-branch))
17-
([version if-branch else-branch]
18-
(if (<= version runtime-version)
19-
if-branch
20-
else-branch)))
21-
22-
(defmacro if-clojure-version-gte
23-
([version if-branch]
24-
`(if-clojure-version-gte ~version ~if-branch nil))
25-
([version if-branch else-branch]
26-
(if (>= (compare
27-
[(:major *clojure-version*) (:minor *clojure-version*) (:incremental *clojure-version* 0)]
28-
(->> (str/split version #"\.") (mapv #(Long/parseLong %))))
29-
0)
30-
if-branch
31-
else-branch)))
32-
33-
(def bb?
34-
(System/getProperty "babashka.version"))
35-
36-
(defmacro if-not-bb
37-
([if-branch]
38-
`(if-not-bb ~if-branch nil))
39-
([if-branch else-branch]
40-
(if (not bb?)
41-
if-branch
42-
else-branch)))
434

445
(defn- if+-rewrite-cond-impl [cond]
456
(clojure.core/cond

src/clojure+/hashp.clj

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -125,12 +125,8 @@
125125
(let [config (merge (default-config) opts)
126126
_ (alter-var-root #'config (constantly config))
127127
sym (:symbol config)]
128-
(alter-var-root #'*data-readers* assoc sym #'hashp)
129-
(when (thread-bound? #'*data-readers*)
130-
(set! *data-readers* (assoc *data-readers* sym #'hashp))))))
128+
(util/rebind-dynamic *data-readers* assoc sym #'hashp))))
131129

132130
(defn uninstall! []
133131
(let [sym (:symbol config)]
134-
(alter-var-root #'*data-readers* dissoc sym)
135-
(when (thread-bound? #'*data-readers*)
136-
(set! *data-readers* (dissoc *data-readers* sym)))))
132+
(util/rebind-dynamic *data-readers* dissoc sym)))

src/clojure+/print.clj

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@
33
[clojure.java.io :as io]
44
[clojure.pprint :as pprint]
55
[clojure.string :as str]
6-
[clojure+.core :as core])
6+
[clojure+.util :as util])
77
(:import
8-
[clojure.lang AFunction Agent Atom ATransientSet Compiler Delay IDeref IPending ISeq MultiFn Namespace PersistentQueue PersistentArrayMap$TransientArrayMap PersistentHashMap PersistentHashMap$TransientHashMap PersistentVector$TransientVector Reduced Ref Volatile]
8+
[clojure.lang AFunction Agent Atom ATransientSet Compiler Delay IDeref IPending ISeq MultiFn Namespace PersistentQueue PersistentArrayMap$TransientArrayMap PersistentHashMap PersistentHashMap$TransientHashMap PersistentVector$TransientVector Reduced Ref Var Volatile]
99
[java.io File Writer]
1010
[java.lang.ref SoftReference WeakReference]
1111
[java.lang.reflect Field]
@@ -157,7 +157,7 @@
157157
(swap! *catalogue conj {:class (Class/forName "[Ljava.lang.Object;") :print #'print-objects :tag 'objects :read #'object-array})
158158

159159

160-
(core/if-clojure-version-gte "1.12.0"
160+
(util/if-clojure-version-gte "1.12.0"
161161
(defn read-array [vals]
162162
(let [class (:tag (meta vals))
163163
class (cond-> class
@@ -172,7 +172,7 @@
172172
x)))
173173
arr)))
174174

175-
(core/if-clojure-version-gte "1.12.0"
175+
(util/if-clojure-version-gte "1.12.0"
176176
(swap! *catalogue conj {:class (Class/forName "[Ljava.lang.Object;") :print #'print-objects :tag 'array :read #'read-array}))
177177

178178

@@ -355,11 +355,11 @@
355355
;; java.lang
356356

357357
(defn print-thread [^Thread t ^Writer w]
358-
(core/if-java-version-gte 21
358+
(util/if-java-version-gte 21
359359
(when (.isVirtual t)
360360
(.write w "^:virtual ")))
361361
(.write w "#thread [")
362-
(pr-on w (core/if-java-version-gte 19 (.threadId t) (.getId t)))
362+
(pr-on w (util/if-java-version-gte 19 (.threadId t) (.getId t)))
363363
(.write w " ")
364364
(pr-on w (.getName t))
365365
(let [g (.getThreadGroup t)]
@@ -533,9 +533,7 @@
533533
(install-readers! {}))
534534
([opts]
535535
(let [readers (data-readers opts)]
536-
(alter-var-root #'*data-readers* merge readers)
537-
(when (thread-bound? #'*data-readers*)
538-
(set! *data-readers* (merge *data-readers* readers))))))
536+
(util/rebind-dynamic *data-readers* merge readers))))
539537

540538
(defn install!
541539
"Install both printers and readers for most of Clojure built-in data structures.

src/clojure+/util.clj

Lines changed: 79 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,82 @@
1-
(ns clojure+.util)
1+
(ns clojure+.util
2+
(:require
3+
[clojure.string :as str]))
4+
5+
(def runtime-version
6+
(let [v (System/getProperty "java.version")]
7+
(if (str/starts-with? v "1.")
8+
(-> (str/split v #"\.") second Long/parseLong)
9+
(-> (str/split v #"\.") first Long/parseLong))))
10+
11+
(defmacro if-java-version-gte
12+
([version if-branch]
13+
(when (<= version runtime-version)
14+
if-branch))
15+
([version if-branch else-branch]
16+
(if (<= version runtime-version)
17+
if-branch
18+
else-branch)))
19+
20+
(defmacro if-clojure-version-gte
21+
([version if-branch]
22+
`(if-clojure-version-gte ~version ~if-branch nil))
23+
([version if-branch else-branch]
24+
(if (>= (compare
25+
[(:major *clojure-version*) (:minor *clojure-version*) (:incremental *clojure-version* 0)]
26+
(->> (str/split version #"\.") (mapv #(Long/parseLong %))))
27+
0)
28+
if-branch
29+
else-branch)))
30+
31+
(def bb?
32+
(System/getProperty "babashka.version"))
33+
34+
(defmacro if-not-bb
35+
([if-branch]
36+
(when-not bb?
37+
if-branch))
38+
([if-branch else-branch]
39+
(if-not bb?
40+
if-branch
41+
else-branch)))
42+
43+
(defn rebind-dynamic-impl
44+
"Clojure creates a lot of layers of dynamic bindings for *data-readers*
45+
(e.g. clojure.main, require, Compiler/load etc). Some of them are not
46+
directly nested, but instead siblings. This causes issues even in simplest
47+
cases: e.g. loading namespace vs executing a function from that namespace,
48+
latter won't see changes made by former.
49+
50+
This might lead to very confusing behaviors, like:
51+
52+
(print/install!)
53+
54+
(defn -main [& args]
55+
(println *data-readers*))
56+
;; => {}
57+
58+
Modifying root binding is not enough, as the changes don't automatically
59+
propagate down the stack. Here we are abusing pop/pushBindings to add
60+
desired readers to every frame in dynamic vars stack."
61+
[var f args]
62+
(let [bindings (clojure.lang.Var/getThreadBindings)]
63+
(try
64+
(clojure.lang.Var/popThreadBindings)
65+
(try
66+
(rebind-dynamic-impl var f args)
67+
(finally
68+
(clojure.lang.Var/pushThreadBindings
69+
(apply update bindings var f args))))
70+
(catch IllegalStateException _
71+
nil))))
72+
73+
(defmacro rebind-dynamic [var f & args]
74+
`(do
75+
(alter-var-root (var ~var) ~f ~@args)
76+
~(if true #_bb?
77+
`(when (thread-bound? (var ~var))
78+
(set! ~var (~f ~var ~@args)))
79+
`(rebind-dynamic-impl (var ~var) ~f ~(vec args)))))
280

381
(defn color? []
482
(cond

test/clojure+/hashp_test.clj

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@
33
(:require
44
[clojure.string :as str]
55
[clojure.test :as test :refer [are deftest is testing use-fixtures]]
6-
[clojure+.core :as core]
7-
[clojure+.hashp :as hashp])
6+
[clojure+.hashp :as hashp]
7+
[clojure+.util :as util])
88
(:import
99
[java.io StringWriter]
1010
[java.util Collections]))
@@ -119,10 +119,10 @@
119119
(is (= {:res String :out "#p String [<pos>]\njava.lang.String\n"} (eval "#p String")))
120120
(is (= {:res String :out "#p java.lang.String [<pos>]\njava.lang.String\n"} (eval "#p java.lang.String")))
121121
(is (= {:res StringWriter :out "#p StringWriter [<pos>]\njava.io.StringWriter\n"} (eval "#p StringWriter")))
122-
(core/if-clojure-version-gte "1.12.0"
122+
(util/if-clojure-version-gte "1.12.0"
123123
(do
124124
(is (= "#p String/1 [<pos>]\njava.lang.String/1\n" (:out (eval "#p String/1"))))
125-
(core/if-not-bb
125+
(util/if-not-bb
126126
;; not in bb
127127
(is (= "#p StringWriter/1 [<pos>]\njava.io.StringWriter/1\n" (:out (eval "#p StringWriter/1"))))))))
128128

@@ -154,7 +154,7 @@
154154
(is (= {:res "b" :out "#p (. \"abc\" substring 1 2) [<pos>]\n\"b\"\n"} (eval "#p (. \"abc\" substring 1 2)")))
155155
(is (= {:res "b" :out "#p (. \"abc\" (substring 1 2)) [<pos>]\n\"b\"\n"} (eval "#p (. \"abc\" (substring 1 2))"))))
156156

157-
(core/if-not-bb
157+
(util/if-not-bb
158158
;; no built-in classes with instance fields in bb
159159
(testing "instance fields"
160160
(is (= {:res 1 :out "#p (.-x (java.awt.Point. 1 2)) [<pos>]\n1\n"} (eval "#p (.-x (java.awt.Point. 1 2))")))
@@ -165,12 +165,12 @@
165165
(testing "constructors"
166166
(is (= {:res 1 :out "#p (Long. 1) [<pos>]\n1\n"} (eval "#p (Long. 1)")))
167167
(is (= {:res 1 :out "#p (new Long 1) [<pos>]\n1\n"} (eval "#p (new Long 1)")))
168-
(core/if-clojure-version-gte "1.12.0"
168+
(util/if-clojure-version-gte "1.12.0"
169169
(is (= {:res 1 :out "#p (Long/new 1) [<pos>]\n1\n"} (eval "#p (Long/new 1)"))))
170170

171171
(is (= {:res 1 :out "#p (Long. \"1\") [<pos>]\n1\n"} (eval "#p (Long. \"1\")")))
172172
(is (= {:res 1 :out "#p (new Long \"1\") [<pos>]\n1\n"} (eval "#p (new Long \"1\")")))
173-
(core/if-clojure-version-gte "1.12.0"
173+
(util/if-clojure-version-gte "1.12.0"
174174
(is (= {:res 1 :out "#p (Long/new \"1\") [<pos>]\n1\n"} (eval "#p (Long/new \"1\")")))))
175175

176176
(testing "non-serializable"

test/clojure+/print_test.clj

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@
44
[clojure.pprint :as pprint]
55
[clojure.string :as str]
66
[clojure.test :as test :refer [are deftest is testing use-fixtures]]
7-
[clojure+.core :as core]
8-
[clojure+.print :as print])
7+
[clojure+.print :as print]
8+
[clojure+.util :as util])
99
(:import
1010
[clojure.lang Atom Agent ATransientSet Delay ExceptionInfo IDeref IPending ISeq Namespace PersistentQueue PersistentArrayMap$TransientArrayMap PersistentHashMap PersistentHashMap$TransientHashMap PersistentVector$TransientVector Reduced Ref Volatile]
1111
[java.io File]
@@ -163,7 +163,7 @@
163163
(is (= "#array ^java.io.File/1 [#file \"a\" #file \"b\" #file \"c\"]"
164164
(pr-str (into-array File [(io/file "a") (io/file "b") (io/file "c")]))))
165165

166-
(core/if-clojure-version-gte "1.12.0"
166+
(util/if-clojure-version-gte "1.12.0"
167167
(let [arr (read-string "#array ^java.io.File/1 [#file \"a\" #file \"b\" #file \"c\"]")]
168168
(is (= (Class/forName "[Ljava.io.File;") (class arr)))
169169
(is (= [(io/file "a") (io/file "b") (io/file "c")] (vec arr))))))
@@ -174,7 +174,7 @@
174174
[(into-array String ["a"])
175175
(into-array String ["b" "c"])]))))
176176

177-
(core/if-clojure-version-gte "1.12.0"
177+
(util/if-clojure-version-gte "1.12.0"
178178
(let [arr (read-string "#array ^String/2 [[\"a\"] [\"b\" \"c\"]]")]
179179
(is (= (Class/forName "[[Ljava.lang.String;") (class arr)))
180180
(is (= [["a"] ["b" "c"]] (mapv vec arr))))))
@@ -327,8 +327,8 @@
327327
_ (is (re-matches #"\#thread \[\d+ \"[^\"]+\"\]" (pr-str t)))
328328

329329
t (Thread. "the \"thread\"")
330-
_ (is (= (str "#thread [" (core/if-java-version-gte 19 (.threadId t) (.getId t)) " \"the \\\"thread\\\"\"]") (pr-str t)))]
331-
(core/if-java-version-gte 21
330+
_ (is (= (str "#thread [" (util/if-java-version-gte 19 (.threadId t) (.getId t)) " \"the \\\"thread\\\"\"]") (pr-str t)))]
331+
(util/if-java-version-gte 21
332332
(let [t (-> (Thread/ofVirtual)
333333
(.name "abc")
334334
(.start ^Runnable #(+ 1 2)))

test/clojure+/walk_test.cljc

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
(ns clojure+.walk-test
22
(:require
3-
[clojure+.core :as core]
43
[clojure+.walk :as walk]
4+
[clojure+.util :as util]
55
[clojure.test :refer [is are deftest]]))
66

77
(defn bump [form]
@@ -82,7 +82,7 @@
8282
4 5 [5] (list 4 [5])
8383
[1 2 {:a 3} (list 4 [5])]])))
8484

85-
(core/if-not-bb
85+
(util/if-not-bb
8686
;; https://github.com/babashka/babashka/issues/1868
8787
(defrecord Foo [a b c]))
8888

@@ -94,9 +94,9 @@
9494
(sorted-set-by > 1 2 3)
9595
{:a 1, :b 2, :c 3}
9696
(sorted-map-by > 1 10, 2 20, 3 30)
97-
(core/if-not-bb
97+
(util/if-not-bb
9898
(->Foo 1 2 3))
99-
(core/if-not-bb
99+
(util/if-not-bb
100100
(map->Foo {:a 1 :b 2 :c 3 :extra 4}))]]
101101
(doseq [c colls]
102102
(let [walked (walk/walk identity identity c)]
@@ -124,7 +124,7 @@
124124
(is (= (list 2 3 4 :a "b" nil 5 6 7) list'))
125125
(is (list? list'))))
126126

127-
(core/if-not-bb
127+
(util/if-not-bb
128128
(defrecord RM [a]))
129129

130130
(deftest retain-meta
@@ -135,6 +135,6 @@
135135
#{1 2}
136136
{1 2}
137137
(map inc (range 3))
138-
(core/if-not-bb
138+
(util/if-not-bb
139139
(->RM 1)
140140
{}))))

0 commit comments

Comments
 (0)