cljs-dev

borkdude 2026-01-15T14:55:29.066819Z

More async/await adventures. I rewrote the go macro to just use async/await. All tests are passing, except those that need macro expansion in the go body, like alt! and stuff. But conceptually it works... https://github.com/borkdude/core.async/commit/afb51951260e8f2b4b7e741c57ccb37d020b861b So next step: macro-expansion. That's probably all that's needed in the go macro. (famous last words...)

Ran 45 tests containing 210 assertions.
0 failures, 0 errors.

👀 5
6
borkdude 2026-01-15T20:30:28.190549Z

Got most tests working now, with macroexpansion. Some things to figure out, but it's going to work. Spent 1 day on it now. 1 more day should do it... Ran 56 tests containing 192 assertions. 3 failures, 0 errors. cc @dnolen

borkdude 2026-01-15T20:34:31.491769Z

you can view (WIP) changes here. https://github.com/borkdude/core.async/compare/main...async-await

dnolen 2026-01-15T21:31:45.158239Z

wow! 🙂

borkdude 2026-01-15T21:56:57.116829Z

Ran 56 tests containing 228 assertions.
0 failures, 0 errors.

borkdude 2026-01-15T22:00:19.474049Z

works now. I have a few special forms to implement but this is trivial. probably untested in the test suite.

borkdude 2026-01-15T22:06:35.004689Z

If you want to try out my CLJS with async/await support + core.async based on async/await instead of state machine (probably better perf and less bundle size):

org.clojure/clojurescript {:git/url ""
                           :git/sha "2ecd2ebd8b79a5ad04c568bc348b4881ddedb4d7"}

org.clojure/core.async {:git/url ""
                        :git/sha "dc9650c0205dca6c0af1b41f198c0fd3226e6f15"}
Please give it a spin! :)

🎉 3
borkdude 2026-01-15T22:21:20.936889Z

Successful tests on CI: https://github.com/borkdude/core.async/actions/runs/21048351978/job/60528470570

borkdude 2026-01-16T12:10:59.910259Z

@dnolen it seems to go macro in CLJS didn't have support for letfn analysis. Is that something you would want? Easy to do, but since the old one didn't have it, I might just leave it out

borkdude 2026-01-16T12:11:45.374299Z

oh lol... https://clojure.atlassian.net/browse/ASYNC-221

borkdude 2026-01-16T12:11:50.777799Z

I think I'll fix it then

👍🏽 1
thheller 2026-01-16T13:41:35.733649Z

why keep any of the IOC stuff at all?

borkdude 2026-01-16T13:42:58.461949Z

what exactly do you mean?

borkdude 2026-01-16T13:44:19.835569Z

you can view the diff here: https://github.com/borkdude/core.async/compare/main...async-await I removed a lot of IOC stuff. Now it's just a pretty basic rewrite of the body to await + some helper fn

thheller 2026-01-16T13:48:56.670719Z

I don't think anything needs to be rewritten anymore? you just turn ( into an alias for (await (take-from-chan-and-returns-promise c)). so and >! and alts! just become a macro that emits an await

thheller 2026-01-16T13:49:37.365279Z

at least my instinct says thats enough? I actually think generators would be better than async/await, but haven't played with either

borkdude 2026-01-16T13:50:58.772569Z

That is basically what happens but you need to • keep track of locals while rewriting this. • expand macros since they can expand into <! etc.

borkdude 2026-01-16T13:52:58.108379Z

One thing I did that I'm not 100% sure of. If take! returns a value immediately I skip await. I think that conforms to how go worked previously: no task switching when the value is immediately available. That was just a small change I did the simple approach you like suggested before with a clojure.walk, and 90% of the tests passed, but it's not enough.

thheller 2026-01-16T14:04:37.380749Z

I really need some time to work on test support. I never run tests in for cljs code but the experience is horrible 😛

borkdude 2026-01-16T14:05:18.585789Z

incomplete sentence?

borkdude 2026-01-16T14:05:46.305609Z

are you asking how to run the tests for this branch? not sure what you meant

thheller 2026-01-16T14:07:52.372239Z

just a comment, not related to anything you are doing. just ran the tests via shadow-cljs :browser-test and its real bad 😛

borkdude 2026-01-16T14:09:39.556029Z

this is how I run the tests:

rm -rf out-simp-node/; clj -M:cljs-test:simp && node out-simp-node/tests.js

thheller 2026-01-16T14:19:08.950949Z

ok super rough quick and dirty

thheller 2026-01-16T14:19:11.848739Z

(defmacro go
  "Asynchronously executes the body, returning immediately to the
  calling thread. Additionally, any visible calls to <!, >! and alt!/alts!
  channel operations within the body will block (if necessary) by
  'parking' the calling thread rather than tying up an OS thread (or
  the only JS thread when in ClojureScript). Upon completion of the
  operation, the body will be resumed.

  Returns a channel which will receive the result of the body when
  completed"
  [& body]
  `(run-async-go
     (^:async fn []
       ~@body)))

(defmacro <! [chan]
  `(await (cljs.core.async/promise-<! ~chan)))

(defmacro >! [chan val]
  `(await (cljs.core.async/promise->! ~chan ~val)))

(defmacro alts! [chans & opts]
  `(await (cljs.core.async/promise-alts! ~chans ~@opts)))

thheller 2026-01-16T14:19:24.610729Z

thats the entire macro side, none of the ioc stuff

thheller 2026-01-16T14:19:33.879399Z

Ran 56 tests containing 228 assertions.
0 failures, 0 errors.

thheller 2026-01-16T14:19:36.753209Z

seems to work?

thheller 2026-01-16T14:23:18.088289Z

https://github.com/thheller/core.async/commit/71f892460298b79a22cc9ffbddfd547af5b20fcf pressed reformat code, so the diff is way larger than it needs to be

thheller 2026-01-16T14:25:23.679909Z

but again, I think generators would be better. Requires a basically full rewrite though, not as easy as just async/await

dnolen 2026-01-16T14:29:03.392859Z

I'm assuming w/ async/await you have to through promises - wondering if there's any perf implications here for the streaming style patterns

thheller 2026-01-16T14:30:17.433049Z

yeah wondering that too. I don't think I have ever actually used generators anywhere, so no clue how it compares

borkdude 2026-01-16T14:57:44.252389Z

@thheller That's a great alternative. I didn't think of that because <! were functions but this is even better :)

thheller 2026-01-16T15:34:45.881179Z

nice part is that then just also works inside regular async functions. so interop between channels/promises should be easier.

thheller 2026-01-16T15:41:47.450189Z

could also just make <! smarter to detect promises, so it just works with channels and promises seamlessly

thheller 2026-01-16T15:47:41.517199Z

@borkdude why did you remove the run-async-go part of the go macro? your variant generates "a lot more code" that is just repetitive and could be handled by a helper function?

borkdude 2026-01-16T15:48:28.956149Z

I didn't remove, I preserved. But good point, I can make it a helper functionL on it

borkdude 2026-01-16T15:57:54.377859Z

@thheller one benefit of keeping the current is approach is that it doesn't do any task switching when the body contains sync parts. e.g.

(go (do-something) 
    (
(do-something) will still be in the same task Do you think this matters? This is also why my previous expansion was like this:
(let [ret (go-take! ..)]
  (if (instance? js/Promise ret) (await ret) ret))
This helps not switching to another task in between if the value is available immediately

thheller 2026-01-16T16:00:07.770819Z

no clue if that matters, but thats the part that gets much easier with generators I'd assume

borkdude 2026-01-16T16:00:55.298879Z

it's not like this is not easy, it's just one line more

thheller 2026-01-16T16:01:34.304469Z

true, just seems dirty 😉

borkdude 2026-01-16T16:01:38.355349Z

generator functions is something that's pretty easy to add after the async support is merged

thheller 2026-01-16T16:02:03.750359Z

yes, thats not the hard part. rewriting core.async to make use of it properly is 😉

borkdude 2026-01-16T16:03:18.917639Z

how would you roughly sketch out how it would work with gen fns? (pseudo-code ok)

thheller 2026-01-16T16:03:20.070789Z

although probably also not that bad, given that there will still be no ioc macro rewrite that made everything go related to complicated

thheller 2026-01-16T16:04:45.638619Z

well basically you flip back to an inversion-of-control style implementation. so instead of <! doing a await it does a yield and the return value is a "placeholder" saying "I want to take from X"

thheller 2026-01-16T16:05:21.045279Z

the "go" implementation loop then works similar to the current IOC thing. so a state machine that calls .next() and does whatever depending on the return value

borkdude 2026-01-16T16:09:52.213889Z

and what would be the benefit of that?

thheller 2026-01-16T16:11:18.691149Z

in theory "less" async stuff. so almost no promises need to be created and stuff like that. not sure thats actually a benefit though.

thheller 2026-01-16T16:13:31.911459Z

I like the idea of core.async being able to decide when and how to go async. instead of leaving that control to async/await of the runtime.

thheller 2026-01-16T16:14:19.219439Z

stacktraces are probably more useful with async/await though

👍 1
borkdude 2026-01-16T16:14:44.162019Z

yeah that's definitely a benefit of this async/await approach

dnolen 2026-01-16T16:18:55.519489Z

Even after review I think this one will need to stew so we can collect information about the implications. I definitely think this is the right direction, but some due diligence required

👍 1
thheller 2026-01-16T18:53:03.888919Z

FWIW maybe its best to just do all this in a new dedicated namespace, not touching the original stuff at all. could also just exist as a separate library. there is enough API exposed so that no modification to core.async are needed for this to work. would allow extensive testing without breaking any existing stuff.

borkdude 2026-01-16T18:53:56.175749Z

Right

borkdude 2026-01-16T18:56:44.392059Z

Someone gave me this example that he used to test comparing missionary with core.async.

(let [n 1000
      cs (repeatedly n a/chan)
      begin (js/performance.now)]
  (doseq [c cs] (go (a/>! c "hi")))
  (go 
    (dotimes [i n]
      (let [[v c] (a/alts! cs)]
        (assert (= "hi" v))))
    (println "Read" n "msgs in" (- (js/performance.now) begin) "ms")))
With the original core.async, I get around 120ms in a REPL. With my non-simple branch I get around the same... but sometimes 200ms. With the simple branch I get around 200ms. That could be the async boundary prevention in my non-simple branch, which is easy to port to the simple branch. I should probably make an advanced compiled version of that and run it with bigger numbers. So just FWIW. JS benchmarking is hard...

dnolen 2026-01-16T18:57:50.529509Z

Oh yeah