Skip to content

Commit f970656

Browse files
committed
wip
1 parent ea5dca0 commit f970656

File tree

3 files changed

+221
-98
lines changed

3 files changed

+221
-98
lines changed

src/sci/impl/analyzer.cljc

Lines changed: 7 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -262,7 +262,7 @@
262262

263263
(declare update-parents)
264264

265-
(defn expand-fn-args+body [{:keys [fn-expr] :as ctx} [binding-vector & body-exprs] _macro? fn-name fn-id]
265+
(defn expand-fn-args+body [{:keys [fn-expr] :as ctx} [binding-vector & body-exprs] _macro? fn-name fn-id async?]
266266
(when-not binding-vector
267267
(throw-error-with-location "Parameter declaration missing." fn-expr))
268268
(when-not (vector? binding-vector)
@@ -284,6 +284,11 @@
284284
ctx (update ctx :parents conj (or var-arg-name fixed-arity))
285285
_ (vswap! (:closure-bindings ctx) assoc-in (conj (:parents ctx) :syms) (zipmap param-idens (range)))
286286
self-ref-idx (when fn-name (update-parents ctx (:closure-bindings ctx) fn-id))
287+
;; Transform async bodies before analysis
288+
body-exprs (if async?
289+
(let [locals (set (keys (:bindings ctx)))]
290+
(async-macro/transform-async-fn-body ctx locals body-exprs))
291+
body-exprs)
287292
body (return-do (with-recur-target ctx true) fn-expr body-exprs)
288293
iden->invoke-idx (get-in @(:closure-bindings ctx) (conj (:parents ctx) :syms))]
289294
(cond-> (->FnBody binding-vector body fixed-arity var-arg-name self-ref-idx iden->invoke-idx)
@@ -353,15 +358,6 @@
353358
body
354359
[body])
355360
async? (:async fn-expr-m)
356-
;; Transform async function bodies: (await ...) -> .then chains
357-
bodies (if async?
358-
(map (fn [body]
359-
(let [arglist (first body)
360-
body-exprs (rest body)]
361-
(cons arglist
362-
(map #(async-macro/transform-async-body ctx %) body-exprs))))
363-
bodies)
364-
bodies)
365361
fn-id (gensym)
366362
parents ((fnil conj []) (:parents ctx) fn-id)
367363
ctx (assoc ctx :parents parents)
@@ -378,7 +374,7 @@
378374
(fn [{:keys [:max-fixed :min-varargs] :as acc} body]
379375
(let [orig-body body
380376
arglist (first body)
381-
body (expand-fn-args+body ctx body macro? fn-name fn-id)
377+
body (expand-fn-args+body ctx body macro? fn-name fn-id async?)
382378
;; body (assoc body :sci.impl/arglist arglist)
383379
var-arg-name (:var-arg-name body)
384380
fixed-arity (:fixed-arity body)

src/sci/impl/async_macro.cljc

Lines changed: 172 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -23,21 +23,65 @@
2323
[form]
2424
(and (seq? form) (= 'await (first form))))
2525

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+
2633
(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."
2836
[form]
2937
(cond
3038
(await-call? form) true
39+
(and (seq? form) (#{'fn 'fn*} (first form))) false
3140
(seq? form) (some contains-await? form)
3241
(vector? form) (some contains-await? form)
3342
(map? form) (some contains-await? (concat (keys form) (vals form)))
3443
:else false))
3544

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+
3658
(defn wrap-promise
3759
"Wrap value in js/Promise.resolve to handle non-Promise values"
3860
[expr]
3961
(list 'js/Promise.resolve expr))
4062

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+
4185
(declare transform-async-body)
4286

4387
(defn transform-let*
@@ -47,33 +91,38 @@
4791
acc-bindings []
4892
current-locals locals]
4993
(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)))))
77126
;; No more pairs, emit remaining bindings + body (recursively transformed)
78127
(let [transformed-body (map #(transform-async-body ctx current-locals %) body)]
79128
(if (seq acc-bindings)
@@ -205,67 +254,103 @@
205254
([ctx body]
206255
(transform-async-body ctx #{} body))
207256
([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))
230299

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))
233302

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))
236305

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))))
266335

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)))
270349

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))

test/sci/async_await_test.cljs

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,3 +210,45 @@
210210
(p/catch (fn [err]
211211
(is false (str err))))
212212
(p/finally done)))))
213+
214+
(deftest async-fn-nested-async-fn-test
215+
(testing "^:async fn with nested ^:async fn in let binding"
216+
(async done
217+
(-> (p/let [ctx (sci/init {:classes {'js js/globalThis :allow :all}})
218+
v (sci/eval-string* ctx
219+
"(defn ^:async foo []
220+
(let [add-async (^:async fn [x y] (+ (await x) y))]
221+
(add-async (js/Promise.resolve 1) 2)))
222+
(foo)")]
223+
(p/let [result v]
224+
(is (= 3 result))))
225+
(p/catch (fn [err]
226+
(is false (str err))))
227+
(p/finally done)))))
228+
229+
(deftest async-fn-param-shadowing-macro-test
230+
(testing "^:async fn with parameter shadowing macro"
231+
(async done
232+
(-> (p/let [ctx (sci/init {:classes {'js js/globalThis :allow :all}})
233+
v (sci/eval-string* ctx
234+
"(defn ^:async foo [->]
235+
(-> (await (js/Promise.resolve 1)) 1))
236+
(foo (fn [x y] (+ x y)))")]
237+
(p/let [result v]
238+
(is (= 2 result))))
239+
(p/catch (fn [err]
240+
(is false (str err))))
241+
(p/finally done)))))
242+
243+
(deftest async-fn-no-await-returns-promise-test
244+
(testing "^:async fn without await still returns a promise"
245+
(async done
246+
(-> (p/let [ctx (sci/init {:classes {'js js/globalThis :allow :all}})
247+
v (sci/eval-string* ctx
248+
"(defn ^:async foo [x] (+ x 1))
249+
(foo 41)")]
250+
(p/let [result v]
251+
(is (= 42 result))))
252+
(p/catch (fn [err]
253+
(is false (str err))))
254+
(p/finally done)))))

0 commit comments

Comments
 (0)