From 3d84b072f373b378f4da528e590b6490082f9c07 Mon Sep 17 00:00:00 2001 From: Michiel Borkent Date: Wed, 7 Jan 2026 18:13:35 +0100 Subject: [PATCH] CLJS-3470: add async/await support --- src/main/cljs/cljs/test.cljc | 2 +- src/main/clojure/cljs/analyzer.cljc | 6 + src/main/clojure/cljs/compiler.cljc | 86 ++++++----- src/main/clojure/cljs/core.cljc | 118 +++++++-------- src/test/cljs/cljs/async_await_test.cljs | 179 +++++++++++++++++++++++ 5 files changed, 297 insertions(+), 94 deletions(-) create mode 100644 src/test/cljs/cljs/async_await_test.cljs diff --git a/src/main/cljs/cljs/test.cljc b/src/main/cljs/cljs/test.cljc index 5afb03ca2..27776ad4b 100644 --- a/src/main/cljs/cljs/test.cljc +++ b/src/main/cljs/cljs/test.cljc @@ -262,7 +262,7 @@ cljs.test/IAsyncTest cljs.core/IFn (~'-invoke [_# ~done] - ~@body))) + ((^:async fn [] ~@body))))) ;; ============================================================================= ;; Running Tests diff --git a/src/main/clojure/cljs/analyzer.cljc b/src/main/clojure/cljs/analyzer.cljc index daa4872a0..c63b857f2 100644 --- a/src/main/clojure/cljs/analyzer.cljc +++ b/src/main/clojure/cljs/analyzer.cljc @@ -2314,6 +2314,12 @@ x (not (contains? ret :info))) meths) locals (:locals env) name-var (fn-name-var env locals name) + async (or + ;; NOTE: adding async on fn form turns it into a MetaFn which isn't great for interop, let's discourage it - Michiel Borkent + #_(:async (meta form)) + (:async (meta name)) + (:async (meta (first form)))) + env (assoc env :async async) env (if (some? name) (update-in env [:fn-scope] conj name-var) env) diff --git a/src/main/clojure/cljs/compiler.cljc b/src/main/clojure/cljs/compiler.cljc index 6e9d152ff..1fbf54ec2 100644 --- a/src/main/clojure/cljs/compiler.cljc +++ b/src/main/clojure/cljs/compiler.cljc @@ -701,10 +701,16 @@ (emitln then "} else {") (emitln else "}")))))) +(defn iife-open [{:keys [async]}] + (str (when async "(await ") "(" (when async "async ") "function (){")) + +(defn iife-close [{:keys [async]}] + (str "})()" (when async ")"))) + (defmethod emit* :case [{v :test :keys [nodes default env]}] (when (= (:context env) :expr) - (emitln "(function(){")) + (emitln (iife-open env))) (let [gs (gensym "caseval__")] (when (= :expr (:context env)) (emitln "var " gs ";")) @@ -723,12 +729,13 @@ (emitln default))) (emitln "}") (when (= :expr (:context env)) - (emitln "return " gs ";})()")))) + (emitln "return " gs ";" + (iife-close env))))) (defmethod emit* :throw [{throw :exception :keys [env]}] (if (= :expr (:context env)) - (emits "(function(){throw " throw "})()") + (emits (iife-open env) "throw " throw (iife-close env)) (emitln "throw " throw ";"))) (def base-types @@ -865,7 +872,7 @@ (when (= :return (:context env)) (emitln "return (")) (when (:def-emits-var env) - (emitln "(function (){")) + (emitln (iife-open env))) (emits var) (when init (emits " = " @@ -878,7 +885,8 @@ {:op :the-var :env (assoc env :context :expr)} var-ast)) - (emitln ");})()")) + (emitln ");" + (iife-close env))) (when (= :return (:context env)) (emitln ")")) ;; NOTE: JavaScriptCore does not like this under advanced compilation @@ -936,18 +944,19 @@ (defn emit-fn-method [{expr :body :keys [type name params env recurs]}] - (emit-wrap env - (emits "(function " (munge name) "(") - (emit-fn-params params) - (emitln "){") - (when type - (emitln "var self__ = this;")) - (when recurs (emitln "while(true){")) - (emits expr) - (when recurs - (emitln "break;") - (emitln "}")) - (emits "})"))) + (let [async (:async env)] + (emit-wrap env + (emits "(" (when async "async ") "function " (munge name) "(") + (emit-fn-params params) + (emitln "){") + (when type + (emitln "var self__ = this;")) + (when recurs (emitln "while(true){")) + (emits expr) + (when recurs + (emitln "break;") + (emitln "}")) + (emits "})")))) (defn emit-arguments-to-array "Emit code that copies function arguments into an array starting at an index. @@ -968,9 +977,10 @@ (emit-wrap env (let [name (or name (gensym)) mname (munge name) - delegate-name (str mname "__delegate")] + delegate-name (str mname "__delegate") + async (:async env)] (emitln "(function() { ") - (emits "var " delegate-name " = function (") + (emits "var " delegate-name " = " (when async "async ") "function (") (doseq [param params] (emit param) (when-not (= param (last params)) (emits ","))) @@ -984,10 +994,11 @@ (emitln "}")) (emitln "};") - (emitln "var " mname " = function (" (comma-sep - (if variadic - (concat (butlast params) ['var_args]) - params)) "){") + (emitln "var " mname " = " (when async "async ") "function (" + (comma-sep + (if variadic + (concat (butlast params) ['var_args]) + params)) "){") (when type (emitln "var self__ = this;")) (when variadic @@ -1024,13 +1035,14 @@ (when (or in-loop (seq recur-params)) (mapcat :params loop-lets))) (map munge) - seq)] + seq) + async (:async env)] (when loop-locals (when (= :return (:context env)) (emits "return ")) (emitln "((function (" (comma-sep (map munge loop-locals)) "){") (when-not (= :return (:context env)) - (emits "return "))) + (emits "return "))) (if (= 1 (count methods)) (if variadic (emit-variadic-fn-method (assoc (first methods) :name name)) @@ -1054,9 +1066,10 @@ (emit-variadic-fn-method meth) (emit-fn-method meth)) (emitln ";")) - (emitln mname " = function(" (comma-sep (if variadic - (concat (butlast maxparams) ['var_args]) - maxparams)) "){") + (emitln mname " = " (when async "async ") "function(" + (comma-sep (if variadic + (concat (butlast maxparams) ['var_args]) + maxparams)) "){") (when variadic (emits "var ") (emit (last maxparams)) @@ -1101,10 +1114,10 @@ (defmethod emit* :do [{:keys [statements ret env]}] (let [context (:context env)] - (when (and (seq statements) (= :expr context)) (emitln "(function (){")) + (when (and (seq statements) (= :expr context)) (emitln (iife-open env))) (doseq [s statements] (emitln s)) (emit ret) - (when (and (seq statements) (= :expr context)) (emitln "})()")))) + (when (and (seq statements) (= :expr context)) (emitln (iife-close env))))) (defmethod emit* :try [{try :body :keys [env catch name finally]}] @@ -1112,7 +1125,7 @@ (if (or name finally) (do (when (= :expr context) - (emits "(function (){")) + (emits (iife-open env))) (emits "try{" try "}") (when name (emits "catch (" (munge name) "){" catch "}")) @@ -1120,13 +1133,14 @@ (assert (not= :const (:op (ana/unwrap-quote finally))) "finally block cannot contain constant") (emits "finally {" finally "}")) (when (= :expr context) - (emits "})()"))) + (emits (iife-close env)))) (emits try)))) (defn emit-let [{expr :body :keys [bindings env]} is-loop] (let [context (:context env)] - (when (= :expr context) (emits "(function (){")) + (when (= :expr context) + (emits (iife-open env))) (binding [*lexical-renames* (into *lexical-renames* (when (= :statement context) @@ -1145,7 +1159,7 @@ (when is-loop (emitln "break;") (emitln "}"))) - (when (= :expr context) (emits "})()")))) + (when (= :expr context) (emits (iife-close env))))) (defmethod emit* :let [ast] (emit-let ast false)) @@ -1166,11 +1180,11 @@ (defmethod emit* :letfn [{expr :body :keys [bindings env]}] (let [context (:context env)] - (when (= :expr context) (emits "(function (){")) + (when (= :expr context) (emits (iife-open env))) (doseq [{:keys [init] :as binding} bindings] (emitln "var " (munge binding) " = " init ";")) (emits expr) - (when (= :expr context) (emits "})()")))) + (when (= :expr context) (emits (iife-close env))))) (defn protocol-prefix [psym] (symbol (str (-> (str psym) diff --git a/src/main/clojure/cljs/core.cljc b/src/main/clojure/cljs/core.cljc index adde92ad0..5ab268e65 100644 --- a/src/main/clojure/cljs/core.cljc +++ b/src/main/clojure/cljs/core.cljc @@ -7,7 +7,7 @@ ; You must not remove this notice, or any other, from this software. (ns cljs.core - (:refer-clojure :exclude [-> ->> .. amap and areduce alength aclone assert assert-args binding bound-fn case comment + (:refer-clojure :exclude [-> ->> .. amap and areduce alength aclone assert await binding bound-fn case comment cond condp declare definline definterface defmethod defmulti defn defn- defonce defprotocol defrecord defstruct deftype delay destructure doseq dosync dotimes doto extend-protocol extend-type fn for future gen-class gen-interface @@ -246,28 +246,26 @@ [p & specs] (emit-extend-protocol p specs))) -#?(:cljs - (core/defn ^{:private true} - maybe-destructured - [params body] - (if (every? core/symbol? params) - (cons params body) - (core/loop [params params - new-params (with-meta [] (meta params)) - lets []] - (if params - (if (core/symbol? (first params)) - (recur (next params) (conj new-params (first params)) lets) - (core/let [gparam (gensym "p__")] - (recur (next params) (conj new-params gparam) - (core/-> lets (conj (first params)) (conj gparam))))) - `(~new-params - (let ~lets - ~@body))))))) - -#?(:cljs - (core/defmacro fn - "params => positional-params* , or positional-params* & rest-param +(core/defn ^{:private true} + maybe-destructured + [params body] + (if (every? core/symbol? params) + (cons params body) + (core/loop [params params + new-params (with-meta [] (meta params)) + lets []] + (if params + (if (core/symbol? (first params)) + (recur (next params) (conj new-params (first params)) lets) + (core/let [gparam (gensym "p__")] + (recur (next params) (conj new-params gparam) + (core/-> lets (conj (first params)) (conj gparam))))) + `(~new-params + (let ~lets + ~@body)))))) + +(core/defmacro fn + "params => positional-params* , or positional-params* & rest-param positional-param => binding-form rest-param => binding-form binding-form => name, or destructuring-form @@ -275,35 +273,35 @@ Defines a function See https://clojure.org/reference/special_forms#fn for more information" - {:forms '[(fn name? [params*] exprs*) (fn name? ([params*] exprs*) +)]} - [& sigs] - (core/let [name (if (core/symbol? (first sigs)) (first sigs) nil) - sigs (if name (next sigs) sigs) - sigs (if (vector? (first sigs)) - (core/list sigs) - (if (seq? (first sigs)) - sigs - ;; Assume single arity syntax - (throw (js/Error. - (if (seq sigs) - (core/str "Parameter declaration " - (core/first sigs) - " should be a vector") - (core/str "Parameter declaration missing")))))) - psig (fn* [sig] + {:forms '[(fn name? [params*] exprs*) (fn name? ([params*] exprs*) +)]} + [& sigs] + (core/let [name (if (core/symbol? (first sigs)) (first sigs) nil) + sigs (if name (next sigs) sigs) + sigs (if (vector? (first sigs)) + (core/list sigs) + (if (seq? (first sigs)) + sigs + ;; Assume single arity syntax + (throw (#?(:clj Exception. :cljs js/Error.) + (if (seq sigs) + (core/str "Parameter declaration " + (core/first sigs) + " should be a vector") + (core/str "Parameter declaration missing")))))) + psig (fn* [sig] ;; Ensure correct type before destructuring sig (core/when (not (seq? sig)) - (throw (js/Error. - (core/str "Invalid signature " sig - " should be a list")))) + (throw (#?(:clj Exception. :cljs js/Error.) + (core/str "Invalid signature " sig + " should be a list")))) (core/let [[params & body] sig _ (core/when (not (vector? params)) - (throw (js/Error. - (if (seq? (first sigs)) - (core/str "Parameter declaration " params - " should be a vector") - (core/str "Invalid signature " sig - " should be a list"))))) + (throw (#?(:clj Exception. :cljs js/Error.) + (if (seq? (first sigs)) + (core/str "Parameter declaration " params + " should be a vector") + (core/str "Invalid signature " sig + " should be a list"))))) conds (core/when (core/and (next body) (map? (first body))) (first body)) body (if conds (next body) body) @@ -319,15 +317,17 @@ body) body (if pre (concat (map (fn* [c] `(assert ~c)) pre) - body) + body) body)] (maybe-destructured params body))) - new-sigs (map psig sigs)] - (with-meta - (if name - (list* 'fn* name new-sigs) - (cons 'fn* new-sigs)) - (meta &form))))) + new-sigs (map psig sigs) + fn-sym-meta (meta (first &form)) + fn*-sym (with-meta 'fn* fn-sym-meta)] + (with-meta + (if name + (list* fn*-sym name new-sigs) + (cons fn*-sym new-sigs)) + (meta &form)))) #?(:cljs (core/defmacro defn- @@ -974,6 +974,10 @@ (reduce core/str "")) " */\n")))) +(core/defmacro await [expr] + (core/assert (:async &env) "await can only be used in async contexts") + (core/list 'js* "(await ~{})" expr)) + (core/defmacro unsafe-cast "EXPERIMENTAL: Subject to change. Unsafely cast a value to a different type." [t x] @@ -3211,7 +3215,7 @@ (. self# (~(get-delegate) (seq ~restarg))))))))] `(do (set! (. ~sym ~(get-delegate-prop)) - (fn (~(vec sig) ~@body))) + (~(with-meta `fn (meta sym)) (~(vec sig) ~@body))) ~@(core/when solo `[(set! (. ~sym ~'-cljs$lang$maxFixedArity) ~(core/dec (count sig)))]) @@ -3292,7 +3296,7 @@ {:variadic? false :fixed-arity (count sig)}) ~(symbol (core/str "-cljs$core$IFn$_invoke$arity$" (count sig)))) - (fn ~method))))] + (~(with-meta `fn (core/meta name)) ~method))))] (core/let [rname (symbol (core/str ana/*cljs-ns*) (core/str name)) arglists (map first fdecl) macro? (:macro meta) diff --git a/src/test/cljs/cljs/async_await_test.cljs b/src/test/cljs/cljs/async_await_test.cljs new file mode 100644 index 000000000..56d3f96e3 --- /dev/null +++ b/src/test/cljs/cljs/async_await_test.cljs @@ -0,0 +1,179 @@ +(ns cljs.async-await-test + (:require [clojure.test :refer [deftest is async]])) + +(defn ^:async foo [n] + (let [x (await (js/Promise.resolve 10)) + y (let [y (await (js/Promise.resolve 20))] + (inc y)) + ;; not async + f (fn [] 20)] + (+ n x y (f)))) + +(deftest defn-test + (async done + (try + (let [v (await (foo 10))] + (is (= 61 v))) + (let [v (await (apply foo [10]))] + (is (= 61 v))) + (catch :default e (prn :should-not-reach-here e)) + (finally (done))))) + +(defn ^:async variadic-foo [n & ns] + (let [x (await (js/Promise.resolve n)) + y (let [y (await (js/Promise.resolve (apply + ns)))] + (inc y)) + ;; not async + f (fn [] 20)] + (+ n x y (f)))) + +(deftest variadic-defn-test + (async done + (try + (let [v (await (variadic-foo 10))] + (is (= 41 v))) + (let [v (await (variadic-foo 10 1 2 3))] + (is (= 47 v))) + (let [v (await (apply variadic-foo [10 1 2 3]))] + (is (= 47 v))) + (catch :default e (prn :should-not-reach-here e)) + (finally (done))))) + +(defn ^:async multi-arity-foo + ([n] (await n)) + ([n x] (+ (await n) x))) + +(deftest multi-arity-defn-test + (async done + (try + (let [v (await (multi-arity-foo 10))] + (is (= 10 v))) + (let [v (await (multi-arity-foo 10 20))] + (is (= 30 v))) + (let [v (await (apply multi-arity-foo [10]))] + (is (= 10 v))) + (let [v (await (apply multi-arity-foo [10 20]))] + (is (= 30 v))) + (catch :default e (prn :should-not-reach-here e)) + (finally (done))))) + +(defn ^:async multi-arity-variadic-foo + ([n] (await n)) + ([n & xs] (apply + (await n) xs))) + +(deftest multi-arity-variadic-test + (async done + (try + (let [v (await (multi-arity-variadic-foo 10))] + (is (= 10 v))) + (let [v (await (multi-arity-variadic-foo 10 20))] + (is (= 30 v))) + (let [v (await (apply multi-arity-variadic-foo [10]))] + (is (= 10 v))) + (let [v (await (apply multi-arity-variadic-foo [10 20]))] + (is (= 30 v))) + (catch :default e (prn :should-not-reach-here e)) + (finally (done))))) + +(deftest fn-test + (async done + (try + (let [f (^:async fn [x] (+ x (await (js/Promise.resolve 20)))) + v (await (f 10)) + v2 (await (apply f [10]))] + (is (= 30 v v2))) + (catch :default e (prn :should-not-reach-here e)) + (finally (done))))) + +(deftest varargs-fn-test + (async done + (try + (let [f (^:async fn [x & xs] (apply + x (await (js/Promise.resolve 20)) xs)) + v (await (f 10)) + v2 (await (apply f [10])) + v3 (await (f 5 5)) + v4 (await (apply f [5 5]))] + (is (= 30 v v2 v3 v4))) + (catch :default e (prn :should-not-reach-here e)) + (finally (done))))) + +(deftest variadic-fn-test + (async done + (try (let [f (^:async fn + ([x] (await (js/Promise.resolve x))) + ([x y] (cons (await (js/Promise.resolve x)) [y])))] + (is (= [1 1 [1 2] [1 2]] + [(await (f 1)) + (await (apply f [1])) + (await (f 1 2)) + (await (apply f [1 2]))]))) + (catch :default e (prn :should-not-reach-here e)) + (finally (done))))) + +(deftest variadic-varargs-fn-test + (async done + (try (let [f (^:async fn + ([x] (await (js/Promise.resolve x))) + ([x & xs] (cons (await (js/Promise.resolve x)) xs)))] + (is (= [1 1 [1 2 3] [1 2 3]] + [(await (f 1)) + (await (apply f [1])) + (await (f 1 2 3)) + (await (apply f [1 2 3]))]))) + (catch :default e (prn :should-not-reach-here e)) + (finally (done))))) + +(deftest await-in-throw-test + (async done + (let [f (^:async fn [x] (inc (if (odd? x) (throw (await (js/Promise.resolve "dude"))) x)))] + (try + (let [x (await (f 2))] + (is (= 3 x))) + (let [x (try (await (f 1)) + (catch :default e e))] + (is (= "dude" x))) + (catch :default e (prn :should-not-reach-here e)) + (finally (done)))))) + +(deftest await-in-do-test + (async done + (try + (let [a (atom 0) + f (^:async fn [] (let [_ (do (swap! a inc) + (swap! a + (await (js/Promise.resolve 2))))] + @a)) + v (await (f))] + (is (= 3 v))) + (catch :default e (prn :should-not-reach-here e)) + (finally (done))))) + +(deftest await-let-fn-test + (async done + (try + (let [f (^:async fn [] (let [v + ;; force letfn in expr position + (letfn [(^:async f [] (inc (await (js/Promise.resolve 10))))] + (inc (await (f))))] + (identity v))) + v (await (f))] + (is (= 12 v))) + (catch :default e (prn :should-not-reach-here e)) + (finally (done))))) + +(deftest await-in-loop-test + (async done + (try + (let [f (^:async fn [] (let [x + ;; force loop in expr position + (loop [xs (map #(js/Promise.resolve %) [1 2 3]) + ys []] + (if (seq xs) + (let [x (first xs) + v (await x)] + (recur (rest xs) (conj ys v))) + ys))] + (identity x))) + v (await (f))] + (is (= [1 2 3] v))) + (catch :default e (prn :should-not-reach-here e)) + (finally (done)))))