|
23 | 23 | [form] |
24 | 24 | (and (seq? form) (= 'await (first form)))) |
25 | 25 |
|
| 26 | +(defn async-fn? |
| 27 | + "Check if form is ^:async fn or ^:async fn*" |
| 28 | + [form] |
| 29 | + (and (seq? form) |
| 30 | + (#{'fn 'fn*} (first form)) |
| 31 | + (:async (meta (first form))))) |
| 32 | + |
26 | 33 | (defn contains-await? |
27 | | - "Check if form contains any (await ...) calls" |
| 34 | + "Check if form contains any (await ...) calls at current scope. |
| 35 | + Skips all fn/fn* bodies - awaits there belong to the inner function's scope." |
28 | 36 | [form] |
29 | 37 | (cond |
30 | 38 | (await-call? form) true |
| 39 | + (and (seq? form) (#{'fn 'fn*} (first form))) false |
31 | 40 | (seq? form) (some contains-await? form) |
32 | 41 | (vector? form) (some contains-await? form) |
33 | 42 | (map? form) (some contains-await? (concat (keys form) (vals form))) |
34 | 43 | :else false)) |
35 | 44 |
|
| 45 | +(defn needs-async-transform? |
| 46 | + "Check if form needs async transformation - either contains await or ^:async fn. |
| 47 | + Skips non-async fn/fn* bodies." |
| 48 | + [form] |
| 49 | + (cond |
| 50 | + (await-call? form) true |
| 51 | + (async-fn? form) true ;; ^:async fn needs transformation |
| 52 | + (and (seq? form) (#{'fn 'fn*} (first form))) false |
| 53 | + (seq? form) (some needs-async-transform? form) |
| 54 | + (vector? form) (some needs-async-transform? form) |
| 55 | + (map? form) (some needs-async-transform? (concat (keys form) (vals form))) |
| 56 | + :else false)) |
| 57 | + |
36 | 58 | (defn wrap-promise |
37 | 59 | "Wrap value in js/Promise.resolve to handle non-Promise values" |
38 | 60 | [expr] |
39 | 61 | (list 'js/Promise.resolve expr)) |
40 | 62 |
|
| 63 | +(defn- promise-form? |
| 64 | + "Check if form is already a promise-producing expression" |
| 65 | + [form] |
| 66 | + (and (seq? form) |
| 67 | + (or (= '.then (first form)) |
| 68 | + (= '.catch (first form)) |
| 69 | + (= '.finally (first form)) |
| 70 | + (= 'js/Promise.resolve (first form))))) |
| 71 | + |
| 72 | +(defn- ensure-promise-result |
| 73 | + "Ensure async function body returns a promise. |
| 74 | + Wraps the last expression in js/Promise.resolve if not already a promise." |
| 75 | + [body-exprs] |
| 76 | + (if (empty? body-exprs) |
| 77 | + (list (wrap-promise nil)) |
| 78 | + (let [exprs (vec body-exprs) |
| 79 | + last-idx (dec (count exprs)) |
| 80 | + last-expr (nth exprs last-idx)] |
| 81 | + (if (promise-form? last-expr) |
| 82 | + body-exprs |
| 83 | + (assoc exprs last-idx (wrap-promise last-expr)))))) |
| 84 | + |
41 | 85 | (declare transform-async-body) |
42 | 86 |
|
43 | 87 | (defn transform-let* |
|
47 | 91 | acc-bindings [] |
48 | 92 | current-locals locals] |
49 | 93 | (if-let [[binding-name init] (first pairs)] |
50 | | - (if (or (await-call? init) (contains-await? init)) |
51 | | - ;; Emit .then, wrap remaining in continuation |
52 | | - (let [;; Transform the init expression and use it as the promise |
53 | | - transformed-init (if (await-call? init) |
54 | | - (wrap-promise (second init)) |
55 | | - (transform-async-body ctx current-locals init)) |
56 | | - rest-pairs (rest pairs) |
57 | | - ;; Add current binding to locals for rest of body |
58 | | - new-locals (conj current-locals binding-name) |
59 | | - ;; Recursively transform the rest body |
60 | | - rest-body (if (seq rest-pairs) |
61 | | - (transform-let* ctx new-locals (vec (mapcat identity rest-pairs)) body) |
62 | | - (let [transformed-body (map #(transform-async-body ctx new-locals %) body)] |
63 | | - (if (= 1 (count transformed-body)) |
64 | | - (first transformed-body) |
65 | | - (cons 'do transformed-body))))] |
66 | | - (if (seq acc-bindings) |
67 | | - ;; Wrap accumulated non-await bindings |
68 | | - (list 'let (vec acc-bindings) |
69 | | - (list '.then transformed-init |
70 | | - (list 'fn [binding-name] rest-body))) |
71 | | - (list '.then transformed-init |
72 | | - (list 'fn [binding-name] rest-body)))) |
73 | | - ;; Accumulate non-await binding, add to locals for subsequent bindings |
74 | | - (recur (rest pairs) |
75 | | - (conj acc-bindings binding-name init) |
76 | | - (conj current-locals binding-name))) |
| 94 | + (let [has-await? (or (await-call? init) (contains-await? init)) |
| 95 | + needs-transform? (needs-async-transform? init)] |
| 96 | + (if has-await? |
| 97 | + ;; Has await - emit .then, wrap remaining in continuation |
| 98 | + (let [transformed-init (if (await-call? init) |
| 99 | + (wrap-promise (second init)) |
| 100 | + (transform-async-body ctx current-locals init)) |
| 101 | + rest-pairs (rest pairs) |
| 102 | + new-locals (conj current-locals binding-name) |
| 103 | + rest-body (if (seq rest-pairs) |
| 104 | + (transform-let* ctx new-locals (vec (mapcat identity rest-pairs)) body) |
| 105 | + (let [transformed-body (map #(transform-async-body ctx new-locals %) body)] |
| 106 | + (if (= 1 (count transformed-body)) |
| 107 | + (first transformed-body) |
| 108 | + (cons 'do transformed-body))))] |
| 109 | + (if (seq acc-bindings) |
| 110 | + (list 'let (vec acc-bindings) |
| 111 | + (list '.then transformed-init |
| 112 | + (list 'fn [binding-name] rest-body))) |
| 113 | + (list '.then transformed-init |
| 114 | + (list 'fn [binding-name] rest-body)))) |
| 115 | + ;; No await - but might have ^:async fn that needs transformation |
| 116 | + (if needs-transform? |
| 117 | + ;; Transform init but don't wrap in .then (it's not a promise) |
| 118 | + (let [transformed-init (transform-async-body ctx current-locals init)] |
| 119 | + (recur (rest pairs) |
| 120 | + (conj acc-bindings binding-name transformed-init) |
| 121 | + (conj current-locals binding-name))) |
| 122 | + ;; Plain binding, accumulate as-is |
| 123 | + (recur (rest pairs) |
| 124 | + (conj acc-bindings binding-name init) |
| 125 | + (conj current-locals binding-name))))) |
77 | 126 | ;; No more pairs, emit remaining bindings + body (recursively transformed) |
78 | 127 | (let [transformed-body (map #(transform-async-body ctx current-locals %) body)] |
79 | 128 | (if (seq acc-bindings) |
|
205 | 254 | ([ctx body] |
206 | 255 | (transform-async-body ctx #{} body)) |
207 | 256 | ([ctx locals body] |
208 | | - (if-not (contains-await? body) |
209 | | - ;; No await, return as-is |
210 | | - body |
211 | | - ;; Has await - check if we need to expand macros first |
212 | | - (let [;; Try to expand macros if it's a seq starting with a symbol |
213 | | - ;; BUT not if the symbol is locally bound |
214 | | - op (when (seq? body) (first body)) |
215 | | - expanded (if (and (seq? body) |
216 | | - (symbol? op) |
217 | | - (not (contains? locals op)) ;; Don't expand if locally bound |
218 | | - (not (await-call? body)) |
219 | | - (not (#{'let 'let* 'do 'fn 'fn* 'if 'quote 'try} op))) |
220 | | - (macroexpand/macroexpand-1 ctx body) |
221 | | - body) |
222 | | - ;; If expansion changed the form, recursively transform |
223 | | - body (if (not= expanded body) |
224 | | - (transform-async-body ctx locals expanded) |
225 | | - body)] |
226 | | - (cond |
227 | | - (and (seq? body) (#{'let 'let*} (first body))) |
228 | | - (let [[_ bindings & exprs] body] |
229 | | - (transform-let* ctx locals bindings exprs)) |
| 257 | + ;; First check for ^:async fn - handle before expansion so we don't lose metadata |
| 258 | + (let [op (when (seq? body) (first body)) |
| 259 | + result |
| 260 | + (cond |
| 261 | + ;; Handle ^:async fn before expansion (metadata would be lost) |
| 262 | + (and (seq? body) |
| 263 | + (#{'fn 'fn*} op) |
| 264 | + (:async (meta op))) |
| 265 | + (let [expanded (if (= 'fn op) |
| 266 | + (macroexpand/macroexpand-1 ctx body) |
| 267 | + body) |
| 268 | + [fn*-sym & arities] expanded] |
| 269 | + (cons fn*-sym |
| 270 | + (map (fn [arity] |
| 271 | + (let [[args & fn-body] arity |
| 272 | + fn-locals (into locals (filter symbol? (flatten args)))] |
| 273 | + (cons args (map #(transform-async-body ctx fn-locals %) fn-body)))) |
| 274 | + arities))) |
| 275 | + |
| 276 | + ;; Try to expand macros (before checking needs-async-transform?) |
| 277 | + ;; This ensures we see awaits that are hidden inside macro calls |
| 278 | + :else |
| 279 | + (let [expanded (if (and (seq? body) |
| 280 | + (symbol? op) |
| 281 | + (not (contains? locals op)) ;; Don't expand if locally bound |
| 282 | + (not (await-call? body)) |
| 283 | + (not (#{'let 'let* 'do 'fn* 'if 'quote 'try} op))) |
| 284 | + (macroexpand/macroexpand-1 ctx body) |
| 285 | + body)] |
| 286 | + (if (not= expanded body) |
| 287 | + ;; Macro expanded, recurse with expanded form |
| 288 | + (transform-async-body ctx locals expanded) |
| 289 | + ;; No expansion, now check for await (safe to skip fn/fn* since macros expanded) |
| 290 | + (cond |
| 291 | + ;; No await or async fn in this form, return as-is |
| 292 | + (not (needs-async-transform? body)) |
| 293 | + body |
| 294 | + |
| 295 | + ;; Has await - transform based on form type |
| 296 | + (and (seq? body) (#{'let 'let*} (first body))) |
| 297 | + (let [[_ bindings & exprs] body] |
| 298 | + (transform-let* ctx locals bindings exprs)) |
230 | 299 |
|
231 | | - (and (seq? body) (= 'do (first body))) |
232 | | - (transform-do ctx locals (rest body)) |
| 300 | + (and (seq? body) (= 'do (first body))) |
| 301 | + (transform-do ctx locals (rest body)) |
233 | 302 |
|
234 | | - (and (seq? body) (= 'try (first body))) |
235 | | - (transform-try ctx locals (rest body)) |
| 303 | + (and (seq? body) (= 'try (first body))) |
| 304 | + (transform-try ctx locals (rest body)) |
236 | 305 |
|
237 | | - (and (seq? body) (= 'if (first body))) |
238 | | - (let [[_ test then else] body |
239 | | - transformed-test (if (contains-await? test) |
240 | | - (transform-async-body ctx locals test) |
241 | | - test) |
242 | | - ;; Transform branches independently - awaits stay in their branches |
243 | | - transformed-then (if (contains-await? then) |
244 | | - (transform-async-body ctx locals then) |
245 | | - then) |
246 | | - transformed-else (when else |
247 | | - (if (contains-await? else) |
248 | | - (transform-async-body ctx locals else) |
249 | | - else)) |
250 | | - ;; Check if test was transformed to a promise |
251 | | - test-is-promise? (and (seq? transformed-test) |
252 | | - (or (= '.then (first transformed-test)) |
253 | | - (= 'js/Promise.resolve (first transformed-test))))] |
254 | | - (if test-is-promise? |
255 | | - ;; Test has await - chain the if after the promise |
256 | | - (let [test-binding (gensym "test__")] |
257 | | - (list '.then transformed-test |
258 | | - (list 'fn [test-binding] |
259 | | - (if transformed-else |
260 | | - (list 'if test-binding transformed-then transformed-else) |
261 | | - (list 'if test-binding transformed-then))))) |
262 | | - ;; Test has no await, just rebuild if with transformed branches |
263 | | - (if transformed-else |
264 | | - (list 'if transformed-test transformed-then transformed-else) |
265 | | - (list 'if transformed-test transformed-then)))) |
| 306 | + (and (seq? body) (= 'if (first body))) |
| 307 | + (let [[_ test then else] body |
| 308 | + transformed-test (if (contains-await? test) |
| 309 | + (transform-async-body ctx locals test) |
| 310 | + test) |
| 311 | + ;; Transform branches independently - awaits stay in their branches |
| 312 | + transformed-then (if (contains-await? then) |
| 313 | + (transform-async-body ctx locals then) |
| 314 | + then) |
| 315 | + transformed-else (when else |
| 316 | + (if (contains-await? else) |
| 317 | + (transform-async-body ctx locals else) |
| 318 | + else)) |
| 319 | + ;; Check if test was transformed to a promise |
| 320 | + test-is-promise? (and (seq? transformed-test) |
| 321 | + (or (= '.then (first transformed-test)) |
| 322 | + (= 'js/Promise.resolve (first transformed-test))))] |
| 323 | + (if test-is-promise? |
| 324 | + ;; Test has await - chain the if after the promise |
| 325 | + (let [test-binding (gensym "test__")] |
| 326 | + (list '.then transformed-test |
| 327 | + (list 'fn [test-binding] |
| 328 | + (if transformed-else |
| 329 | + (list 'if test-binding transformed-then transformed-else) |
| 330 | + (list 'if test-binding transformed-then))))) |
| 331 | + ;; Test has no await, just rebuild if with transformed branches |
| 332 | + (if transformed-else |
| 333 | + (list 'if transformed-test transformed-then transformed-else) |
| 334 | + (list 'if transformed-test transformed-then)))) |
266 | 335 |
|
267 | | - ;; Handle any other expression containing await |
268 | | - (contains-await? body) |
269 | | - (transform-expr-with-await ctx locals body) |
| 336 | + ;; Handle any other expression containing await |
| 337 | + :else |
| 338 | + (transform-expr-with-await ctx locals body)))))] |
| 339 | + (when (not= body result) |
| 340 | + #?(:clj (binding [*out* *err*] |
| 341 | + (println "async transform:") |
| 342 | + (println " in: " (pr-str body)) |
| 343 | + (println " out:" (pr-str result))) |
| 344 | + :cljs (do |
| 345 | + (js/console.error "async transform:") |
| 346 | + (js/console.error " in: " (pr-str body)) |
| 347 | + (js/console.error " out:" (pr-str result))))) |
| 348 | + result))) |
270 | 349 |
|
271 | | - :else body))))) |
| 350 | +(defn transform-async-fn-body |
| 351 | + "Transform async function body expressions and ensure result is a promise. |
| 352 | + This is the main entry point for async function transformation." |
| 353 | + [ctx locals body-exprs] |
| 354 | + (->> body-exprs |
| 355 | + (map #(transform-async-body ctx locals %)) |
| 356 | + ensure-promise-result)) |
0 commit comments