Skip to content

Commit ffef2c5

Browse files
alexander-yakushevbbatsov
authored andcommitted
[macroexpand] Make macroexpand mw delegate the expansion to eval
1 parent 8ecdab4 commit ffef2c5

File tree

4 files changed

+97
-45
lines changed

4 files changed

+97
-45
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
## master (unreleased)
44

55
* [#893](https://github.com/clojure-emacs/cider-nrepl/pull/893): Replace `clojure.tools.trace` with `orchard.trace`.
6+
* [#894](https://github.com/clojure-emacs/cider-nrepl/pull/894): Delegate actual macroexpansion to "eval" command in middleware.macroexpand.
67
* Bump `orchard` to [0.26.3](https://github.com/clojure-emacs/orchard/blob/master/CHANGELOG.md#0263-2024-08-14).
78

89
## 0.49.3 (2024-08-13)

src/cider/nrepl.clj

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -529,11 +529,12 @@ if applicable, and re-render the updated value."
529529
(cljs/requires-piggieback
530530
{:doc "Macroexpansion middleware."
531531
:requires #{#'session}
532+
:expects #{"eval"}
532533
:handles {"macroexpand"
533534
{:doc "Produces macroexpansion of some form using the given expander."
534535
:requires {"code" "The form to macroexpand."}
535536
:optional {"ns" "The namespace in which to perform the macroexpansion. Defaults to 'user for Clojure and 'cljs.user for ClojureScript."
536-
"expander" "The macroexpansion function to use. Possible values are \"macroexpand-1\", \"macroexpand\", or \"macroexpand-all\". Defaults to \"macroexpand\"."
537+
"expander" "The macroexpansion function to use. Possible values are \"macroexpand-1\", \"macroexpand\", \"macroexpand-step\", or \"macroexpand-all\". Defaults to \"macroexpand\"."
537538
"display-namespaces" "How to print namespace-qualified symbols in the result. Possible values are \"qualified\" to leave all namespaces qualified, \"none\" to elide all namespaces, or \"tidy\" to replace namespaces with their aliases in the given namespace. Defaults to \"qualified\"."
538539
"print-meta" "If truthy, also print metadata of forms."}
539540
:returns {"expansion" "The macroexpanded form."}}}}))

src/cider/nrepl/middleware/macroexpand.clj

Lines changed: 79 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -3,35 +3,40 @@
33
{:author "Bozhidar Batsov"}
44
(:require
55
[cider.nrepl.middleware.util.cljs :as cljs]
6-
[cider.nrepl.middleware.util.error-handling :refer [with-safe-transport]]
6+
[cider.nrepl.middleware.util.error-handling
7+
:refer [base-error-response eval-interceptor-transport with-safe-transport]]
78
[orchard.cljs.analysis :as cljs-ana]
9+
[nrepl.misc :refer [response-for]]
10+
[nrepl.transport :as transport]
811
[clojure.pprint :as pp]
912
[clojure.tools.reader :as reader]
1013
[clojure.walk :as walk]
1114
[orchard.misc :as misc])
1215
(:import
1316
[clojure.lang Var]))
1417

18+
;; Macroexpansion for CLJ and CLJS follow two separate paths. In CLJ, a special
19+
;; macroexpanding code is formatted and passed down as `eval` message, then the
20+
;; result is intercepted, processed, and returned. In CLJS, the middleware
21+
;; itself performs the expansion as there is no way to macroexpand the
22+
;; expression with the evaluating part of CLJS.
23+
1524
;; Common helpers
1625

1726
(defn- var->namespace-qualified-name [^Var v]
1827
(symbol (str (.name (.ns v)))
1928
(str (.sym v))))
2029

21-
(defn- expandable?
22-
"Return true if form is macro-expandable."
23-
[form]
24-
(not= (macroexpand-1 form) form))
25-
2630
(defn macroexpand-step
2731
"Walk form, expanding the next subform."
2832
[form]
2933
(let [expanded? (atom false)]
3034
(walk/prewalk (fn [x]
31-
(if (and (not @expanded?)
32-
(expandable? x))
33-
(do (reset! expanded? true)
34-
(macroexpand-1 x))
35+
(if-not @expanded?
36+
(let [x' (macroexpand-1 x)]
37+
(when (not= x x')
38+
(reset! expanded? true))
39+
x')
3540
x))
3641
form)))
3742

@@ -68,17 +73,24 @@
6873
(and (symbol? x) (namespace x))
6974
(tidy-namespaced-sym ns aliases refers))))
7075

76+
(defn- macroexpansion-response-map
77+
[{:keys [print-meta] :as msg} expanded-form]
78+
(let [expansion (with-out-str
79+
(binding [*print-meta* (boolean print-meta)]
80+
(pp/write expanded-form :dispatch pp/code-dispatch)))]
81+
{:expansion expansion}))
82+
7183
;; Clojure impl
7284

7385
(defn- resolve-expander-clj
74-
"Returns the macroexpansion fn for macroexpanding Clojure code, corresponding
75-
to the given value of the :expander option."
86+
"Returns the qualified macroexpansion fn symbol for macroexpanding Clojure code,
87+
corresponding to the given value of the :expander option."
7688
[expander]
7789
(case expander
78-
"macroexpand-1" macroexpand-1
79-
"macroexpand" macroexpand
80-
"macroexpand-all" walk/macroexpand-all
81-
"macroexpand-step" macroexpand-step
90+
"macroexpand-1" `macroexpand-1
91+
"macroexpand" `macroexpand
92+
"macroexpand-all" `walk/macroexpand-all
93+
"macroexpand-step" `macroexpand-step
8294
(throw (IllegalArgumentException. (format "Unrecognized expander: %s" expander)))))
8395

8496
(defn- tidy-walker-clj
@@ -108,21 +120,44 @@
108120
"tidy" (tidy-walker-clj msg)
109121
(throw (IllegalArgumentException. (format "Unrecognized value for display-namespaces: %s" display-namespaces)))))
110122

111-
(defn- expand-clj
112-
"Returns the macroexpansion of the given Clojure form :code, performed in the
113-
context of the given :ns, using the provided :expander and :display-namespaces
114-
options."
115-
[{:keys [code expander ns] :as msg}]
116-
;; Bind LOADER, which can be unbound under certain code paths, particularly when using tools.deps:
117-
(with-bindings {clojure.lang.Compiler/LOADER
118-
(if (instance? clojure.lang.Var$Unbound
119-
@clojure.lang.Compiler/LOADER)
120-
(clojure.lang.DynamicClassLoader. (clojure.lang.RT/baseLoader))
121-
@clojure.lang.Compiler/LOADER)}
122-
(->> (let [expander-fn (resolve-expander-clj expander)]
123-
(binding [*ns* (find-ns ns)]
124-
(expander-fn (read-string code))))
125-
(walk/prewalk (post-expansion-walker-clj msg)))))
123+
(defn- send-middleware-error [msg ex]
124+
(transport/send (:transport msg)
125+
(base-error-response msg ex :done :macroexpand-error)))
126+
127+
(defn macroexpansion-reply-clj [{:keys [transport] :as msg}
128+
{:keys [value] :as resp}]
129+
(try (let [msg (update msg :ns #(or (misc/as-sym %) 'user))
130+
expansion (walk/prewalk (post-expansion-walker-clj msg) value)
131+
response-map (macroexpansion-response-map msg expansion)]
132+
(transport/send transport (response-for msg response-map)))
133+
(catch Exception ex
134+
(send-middleware-error msg ex))))
135+
136+
(defn handle-macroexpand-clj
137+
"Substitute the incoming `macroexpand` message with an `eval` message that will
138+
perform the expansion for us, intercept the result with
139+
`eval-interceptor-transport`, post-process it and return back to the client.
140+
141+
Delegating the actual expansion (and even reading the code string) to `eval`
142+
op is preferable because it ensures that the context (state of dynamic
143+
variables) for macroexpansion is identical to if the user called `(macroexpand
144+
...)` manually at the REPL."
145+
[handler {:keys [code expander ns] :as msg}]
146+
;; `try` is not around the handler but only around constructing the msg
147+
;; because we don't want to catch an error from some underlying middleware.
148+
(let [msg
149+
(try (let [expander-fn (resolve-expander-clj expander)
150+
;; Glueing strings may seem ugly, but this is the most
151+
;; reliable way to ensure the macroexpansion fully happens in
152+
;; a correct context.
153+
expander-code (format "(%s '%s)" expander-fn code)
154+
transport (eval-interceptor-transport
155+
msg macroexpansion-reply-clj :macroexpand-error)]
156+
(assoc msg :op "eval", :code expander-code, :transport transport))
157+
(catch Exception ex
158+
(send-middleware-error msg ex)
159+
nil))]
160+
(some-> msg handler)))
126161

127162
;; ClojureScript impl
128163

@@ -213,20 +248,20 @@
213248
(cljs/with-cljs-ns ns
214249
(expander-fn code))))))
215250

216-
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
251+
(defn macroexpansion-reply-cljs [msg]
252+
(let [msg (update msg :ns #(or (misc/as-sym %) 'cljs.user))]
253+
(macroexpansion-response-map msg (expand-cljs msg))))
217254

218-
(defn macroexpansion [{:keys [print-meta ns] :as msg}]
219-
(let [msg (merge {:expander "macroexpand" :display-namespaces "qualified"} msg)
220-
expansion (if (cljs/grab-cljs-env msg)
221-
(expand-cljs (assoc msg :ns (or (misc/as-sym ns) 'cljs.user)))
222-
(expand-clj (assoc msg :ns (or (misc/as-sym ns) 'user))))]
223-
(with-out-str
224-
(binding [*print-meta* (boolean print-meta)]
225-
(pp/write expansion :dispatch pp/code-dispatch)))))
255+
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
226256

227-
(defn macroexpansion-reply [msg]
228-
{:expansion (macroexpansion msg)})
257+
(defn- handle-macroexpand* [handler msg]
258+
(let [msg (merge {:expander "macroexpand" :display-namespaces "qualified"} msg)]
259+
(if (cljs/grab-cljs-env msg)
260+
(with-safe-transport handler msg
261+
"macroexpand" [macroexpansion-reply-cljs :macroexpand-error])
262+
(handle-macroexpand-clj handler msg))))
229263

230264
(defn handle-macroexpand [handler msg]
231-
(with-safe-transport handler msg
232-
"macroexpand" [macroexpansion-reply :macroexpand-error]))
265+
(if (= (:op msg) "macroexpand")
266+
(handle-macroexpand* handler msg)
267+
(handler msg)))

test/clj/cider/nrepl/middleware/macroexpand_test.clj

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,21 @@
7676
(is (= #{"done" "macroexpand-error"} status))
7777
(is pp-stacktrace))))
7878

79+
(defmacro ^:private lazy-test-macro []
80+
`(list {:a ~@(lazy-seq [(ns-name *ns*)])}))
81+
82+
(deftest lazy-expand-test
83+
(testing "lazy macroexpansion expands in the correct namespace"
84+
(let [{:keys [expansion status]}
85+
(session/message {:op "macroexpand"
86+
:expander "macroexpand"
87+
:ns "cider.nrepl.middleware.macroexpand-test"
88+
:code "(lazy-test-macro)"
89+
:display-namespaces "none"})]
90+
(is (= "(list {:a cider.nrepl.middleware.macroexpand-test})"
91+
expansion))
92+
(is (= #{"done"} status)))))
93+
7994
;; Tests for the three possible values of the display-namespaces option:
8095
;; "qualified", "none" and "tidy"
8196

0 commit comments

Comments
 (0)