Skip to content

Commit 3f54ce2

Browse files
authored
Add basilisp.walk namespace with tree walkers (#434)
* Add tree walking functions in `basilisp.walk` * Ugh
1 parent c2017ab commit 3f54ce2

File tree

4 files changed

+253
-45
lines changed

4 files changed

+253
-45
lines changed

src/basilisp/lang/reader.py

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,13 +34,14 @@
3434
import basilisp.lang.symbol as symbol
3535
import basilisp.lang.util as langutil
3636
import basilisp.lang.vector as vector
37-
import basilisp.walker as walk
3837
from basilisp.lang.interfaces import (
3938
ILispObject,
4039
ILookup,
4140
IMeta,
4241
IPersistentList,
42+
IPersistentMap,
4343
IPersistentSet,
44+
IPersistentVector,
4445
IRecord,
4546
IType,
4647
)
@@ -808,6 +809,28 @@ def _read_meta(ctx: ReaderContext) -> IMeta:
808809
)
809810

810811

812+
def _walk(inner_f, outer_f, form):
813+
"""Walk an arbitrary, possibly nested data structure, applying inner_f to each
814+
element of form and then applying outer_f to the resulting form."""
815+
if isinstance(form, IPersistentList):
816+
return outer_f(llist.list(map(inner_f, form)))
817+
elif isinstance(form, IPersistentVector):
818+
return outer_f(vector.vector(map(inner_f, form)))
819+
elif isinstance(form, IPersistentMap):
820+
return outer_f(lmap.from_entries(map(inner_f, form)))
821+
elif isinstance(form, IPersistentSet):
822+
return outer_f(lset.set(map(inner_f, form)))
823+
else:
824+
return outer_f(form)
825+
826+
827+
def _postwalk(f, form):
828+
""""Walk form using depth-first, post-order traversal, applying f to each form
829+
and replacing form with its result."""
830+
inner_f = functools.partial(_postwalk, f)
831+
return _walk(inner_f, f, form)
832+
833+
811834
@_with_loc
812835
def _read_function(ctx: ReaderContext) -> llist.List:
813836
"""Read a function reader macro from the input stream."""
@@ -841,7 +864,7 @@ def identify_and_replace(f):
841864
return sym_replacement(arg_num)
842865
return f
843866

844-
body = walk.postwalk(identify_and_replace, form) if len(form) > 0 else None
867+
body = _postwalk(identify_and_replace, form) if len(form) > 0 else None
845868

846869
arg_list: List[symbol.Symbol] = []
847870
numbered_args = sorted(map(int, filter(lambda k: k != "rest", arg_set)))

src/basilisp/walk.lpy

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
(ns basilisp.walk)
2+
3+
(defn walk
4+
"Walk an arbitrary, possibly nested data structure, applying inner to each
5+
element of form and then applying outer to the resulting form.
6+
7+
All built in data structures are supported.
8+
9+
Lazy sequences will be completely consumed (and thus may not be infinite)."
10+
[inner outer form]
11+
(cond
12+
(list? form)
13+
(outer (apply list (map inner form)))
14+
15+
(map-entry? form)
16+
(outer (map-entry (inner (key form)) (inner (val form))))
17+
18+
(seq? form)
19+
(outer (doall (map inner form)))
20+
21+
(vector? form)
22+
(outer (apply vector (map inner form)))
23+
24+
(map? form)
25+
(outer (apply hash-map (mapcat inner form)))
26+
27+
(set? form)
28+
(outer (apply hash-set (map inner form)))
29+
30+
(record? form)
31+
(outer (reduce (fn [rec field]
32+
(conj rec (inner field)))
33+
form
34+
form))
35+
36+
:else
37+
(outer form)))
38+
39+
(defn postwalk
40+
"Walk form using depth-first, post-order traversal, applying f to each form
41+
and replacing form with its result.
42+
43+
All built in data structures are supported.
44+
45+
Lazy sequences will be completely consumed (and thus may not be infinite)."
46+
[f form]
47+
(walk (partial postwalk f) f form))
48+
49+
(defn prewalk
50+
"Walk form using depth-first, pre-order traversal, applying f to each form
51+
and replacing form with its result.
52+
53+
All built in data structures are supported.
54+
55+
Lazy sequences will be completely consumed (and thus may not be infinite)."
56+
[f form]
57+
(walk (partial prewalk f) identity (f form)))
58+
59+
(defn postwalk-replace
60+
"Recursively walk through form as by postwalk, replacing elements appearing
61+
as keys in replacements with the corresponding values."
62+
[replacements form]
63+
(postwalk #(if-let [newv (get replacements %)]
64+
newv
65+
%)
66+
form))
67+
68+
(defn prewalk-replace
69+
"Recursively walk through form as by prewalk, replacing elements appearing
70+
as keys in replacements with the corresponding values."
71+
[replacements form]
72+
(prewalk #(if-let [newv (get replacements %)]
73+
newv
74+
%)
75+
form))
76+
77+
(defn postwalk-demo
78+
"Print each element as it is walked as by postwalk."
79+
[form]
80+
(postwalk #(do (println (str "Walked: " %)) %) form))
81+
82+
(defn prewalk-demo
83+
"Print each element as it is walked as by postwalk."
84+
[form]
85+
(prewalk #(do (println (str "Walked: " %)) %) form))
86+
87+
(defn keywordize-keys
88+
"Recursively walk form, transforming string keys into keywords in any maps."
89+
[form]
90+
(postwalk (fn [v]
91+
(if (map? v)
92+
(->> v
93+
(mapcat (fn [[k v]] [(cond-> k (string? k) (keyword)) v]))
94+
(apply hash-map))
95+
v))
96+
form))
97+
98+
(defn stringify-keys
99+
"Recursively walk form, transforming keyword keys into strings in any maps."
100+
[form]
101+
(postwalk (fn [v]
102+
(if (map? v)
103+
(->> v
104+
(mapcat (fn [[k v]] [(cond-> k (keyword? k) (name)) v]))
105+
(apply hash-map))
106+
v))
107+
form))
108+
109+
(defn macroexpand-all
110+
"Recursively macroexpand all eligible forms contained in form."
111+
[form]
112+
(prewalk (fn [v]
113+
(if (seq? v)
114+
(macroexpand v)
115+
v))
116+
form))

src/basilisp/walker.py

Lines changed: 0 additions & 43 deletions
This file was deleted.

tests/basilisp/test_walk.lpy

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
(ns tests.basilisp.walk-test
2+
(:require
3+
[basilisp.test :refer [deftest is testing]]
4+
[basilisp.walk :as walk]))
5+
6+
(deftest postwalk-replace-test
7+
(is (= [:c :d] (walk/postwalk-replace {:a 1 :b 2} [:c :d])))
8+
(is (= [1 2] (walk/postwalk-replace {:a 1 :b 2} [:a :b])))
9+
(is (= [1 2 :c] (walk/postwalk-replace {:a 1 :b 2} [:a :b :c])))
10+
(is (= [1 2 [1 2] :c] (walk/postwalk-replace {:a 1 :b 2} [:a :b [:a :b] :c])))
11+
(is (= {:NIL 4, :a 1, :b :NIL, :c 3}
12+
(walk/postwalk-replace {nil :NIL} {:a 1, :b nil, :c 3, nil 4}))))
13+
14+
(deftest prewalk-replace-test
15+
(is (= [:c :d] (walk/prewalk-replace {:a 1 :b 2} [:c :d])))
16+
(is (= [1 2] (walk/prewalk-replace {:a 1 :b 2} [:a :b])))
17+
(is (= [1 2 :c] (walk/prewalk-replace {:a 1 :b 2} [:a :b :c])))
18+
(is (= [1 2 [1 2] :c] (walk/prewalk-replace {:a 1 :b 2} [:a :b [:a :b] :c])))
19+
(is (= {:NIL 4, :a 1, :b :NIL, :c 3}
20+
(walk/postwalk-replace {nil :NIL} {:a 1, :b nil, :c 3, nil 4}))))
21+
22+
(deftest keywordize-keys-test
23+
(testing "maps and nested maps"
24+
(is (= {} (walk/keywordize-keys {})))
25+
(is (= {:a 1 :b 2} (walk/keywordize-keys {"a" 1 "b" 2})))
26+
(is (= {:a 1 :b 2 :c 3} (walk/keywordize-keys {"a" 1 "b" 2 :c 3})))
27+
(is (= {:a 1 :c 3 4 :d}
28+
(walk/keywordize-keys {"a" 1 :c 3 4 :d})))
29+
(is (= {:a 1 :c 3 4 :d :nested {:e 5 'b 6}}
30+
(walk/keywordize-keys {"a" 1 :c 3 4 :d "nested" {"e" 5 'b 6}}))))
31+
32+
(testing "maps in lists and seqs"
33+
(is (= '({:a 1 :b 2}) (walk/keywordize-keys '({"a" 1 "b" 2}))))
34+
(is (= '({:a 1 :b 2 :c 3}) (walk/keywordize-keys '({"a" 1 "b" 2 :c 3}))))
35+
(is (= '({:a 1 :c 3 4 :d})
36+
(walk/keywordize-keys '({"a" 1 :c 3 4 :d}))))
37+
(is (= '({:a 1 :c 3 4 :d :nested {:e 5 'b 6}})
38+
(walk/keywordize-keys '({"a" 1 :c 3 4 :d "nested" {"e" 5 'b 6}}))))
39+
(is (= '({:a 1} {:a 2} {:a 3})
40+
(walk/keywordize-keys (map #(hash-map "a" %) (range 1 4))))))
41+
42+
(testing "maps in sets"
43+
(is (= #{{:a 1 :b 2}} (walk/keywordize-keys #{{"a" 1 "b" 2}})))
44+
(is (= #{{:a 1 :b 2 :c 3}} (walk/keywordize-keys #{{"a" 1 "b" 2 :c 3}})))
45+
(is (= #{{:a 1 :c 3 4 :d}}
46+
(walk/keywordize-keys #{{"a" 1 :c 3 4 :d}})))
47+
(is (= #{{:a 1 :c 3 4 :d :nested {:e 5 'b 6}}}
48+
(walk/keywordize-keys #{{"a" 1 :c 3 4 :d "nested" {"e" 5 'b 6}}}))))
49+
50+
(testing "maps in vectors"
51+
(is (= [{:a 1 :b 2}] (walk/keywordize-keys [{"a" 1 "b" 2}])))
52+
(is (= [{:a 1 :b 2 :c 3}] (walk/keywordize-keys [{"a" 1 "b" 2 :c 3}])))
53+
(is (= [{:a 1 :c 3 4 :d}]
54+
(walk/keywordize-keys [{"a" 1 :c 3 4 :d}])))
55+
(is (= [{:a 1 :c 3 4 :d :nested {:e 5 'b 6}}]
56+
(walk/keywordize-keys [{"a" 1 :c 3 4 :d "nested" {"e" 5 'b 6}}])))))
57+
58+
(deftest stringify-keys-test
59+
(testing "maps and nested maps"
60+
(is (= {} (walk/stringify-keys {})))
61+
(is (= {"a" 1 "b" 2} (walk/stringify-keys {:a 1 :b 2})))
62+
(is (= {"a" 1 "b" 2 "c" 3} (walk/stringify-keys {"a" 1 "b" 2 :c 3})))
63+
(is (= {"a" 1 "c" 3 4 :d}
64+
(walk/stringify-keys {"a" 1 :c 3 4 :d})))
65+
(is (= {"a" 1 "c" 3 4 :d "nested" {"e" 5 'b 6}}
66+
(walk/stringify-keys {"a" 1 :c 3 4 :d "nested" {:e 5 'b 6}}))))
67+
68+
(testing "maps in lists and seqs"
69+
(is (= '({"a" 1 "b" 2}) (walk/stringify-keys '({:a 1 :b 2}))))
70+
(is (= '({"a" 1 "b" 2 "c" 3}) (walk/stringify-keys '({"a" 1 "b" 2 :c 3}))))
71+
(is (= '({"a" 1 "c" 3 4 :d})
72+
(walk/stringify-keys '({"a" 1 :c 3 4 :d}))))
73+
(is (= '({"a" 1 "c" 3 4 :d "nested" {"e" 5 'b 6}})
74+
(walk/stringify-keys '({"a" 1 :c 3 4 :d "nested" {:e 5 'b 6}}))))
75+
(is (= '({"a" 1} {"a" 2} {"a" 3})
76+
(walk/stringify-keys (map #(hash-map :a %) (range 1 4))))))
77+
78+
(testing "maps in sets"
79+
(is (= #{{"a" 1 "b" 2}} (walk/stringify-keys #{{:a 1 :b 2}})))
80+
(is (= #{{"a" 1 "b" 2 "c" 3}} (walk/stringify-keys #{{"a" 1 "b" 2 :c 3}})))
81+
(is (= #{{"a" 1 "c" 3 4 :d}}
82+
(walk/stringify-keys #{{"a" 1 :c 3 4 :d}})))
83+
(is (= #{{"a" 1 "c" 3 4 :d "nested" {"e" 5 'b 6}}}
84+
(walk/stringify-keys #{{"a" 1 :c 3 4 :d "nested" {:e 5 'b 6}}}))))
85+
86+
(testing "maps in vectors"
87+
(is (= [{"a" 1 "b" 2}] (walk/stringify-keys [{:a 1 :b 2}])))
88+
(is (= [{"a" 1 "b" 2 "c" 3}] (walk/stringify-keys [{"a" 1 "b" 2 :c 3}])))
89+
(is (= [{"a" 1 "c" 3 4 :d}]
90+
(walk/stringify-keys [{"a" 1 :c 3 4 :d}])))
91+
(is (= [{"a" 1 "c" 3 4 :d "nested" {"e" 5 'b 6}}]
92+
(walk/stringify-keys [{"a" 1 :c 3 4 :d "nested" {:e 5 'b 6}}])))))
93+
94+
(defmacro plus [n1 n2]
95+
`(+ ~n1 ~n2))
96+
97+
(defmacro pl [p1 p2]
98+
`(plus ~p1 ~p2))
99+
100+
(defmacro minus [m1 m2]
101+
`(- ~m1 ~m2))
102+
103+
(defmacro calc [c1 c2]
104+
`(pl ~c1 (minus ~c1 ~c2)))
105+
106+
(deftest macroexpand-all-test
107+
(is (= '(tests.basilisp.walk-test/pl 20 (tests.basilisp.walk-test/minus 20 30))
108+
(macroexpand-1 '(calc 20 30))))
109+
(is (= '(basilisp.core/+ 20 (tests.basilisp.walk-test/minus 20 30))
110+
(macroexpand '(calc 20 30))))
111+
(is (= '(basilisp.core/+ 20 (basilisp.core/- 20 30))
112+
(walk/macroexpand-all '(calc 20 30)))))

0 commit comments

Comments
 (0)