squint-cljs / cherry

Experimental ClojureScript to ES6 module compiler
https://squint-cljs.github.io/cherry
558 stars 22 forks source link

Macros in user space #4

Closed borkdude closed 2 years ago

borkdude commented 2 years ago

Macros need to run in the same environment as the transpiler. Therefore it makes sense to force users to declare macros in separate .cljc files: .cljc so the cherry transpiler can run in both clojure/bb and JS environments.

Say we have foo.cljs which compiles to foo.mjs but requires a macro from bar.cljc, how would we declare that?

(ns foo (:require-macros ["./bar.cljc" :refer [my-macro]]))

(my-macro ...)

Or we can make the choice that macros are also transpiled functions and cherry only runs in JS. Then you could even write macros in JavaScript... I'm actually leaning towards that now.

Similar to what bun.js does: https://twitter.com/jarredsumner/status/1493204320984047619

We should explore both options.

Interpreter approach

The interpreter approach needs you to hold deps on the "classpath" when you want to use macros from another library, since macros would be elided in .mjs output.

Transpiled macros approach

...

alidcast commented 2 years ago

it'd be beneficial to have macro-expansion happen in same language as runtime so you can take full advantage of power of macros - e.g. using binding for contextual state

i've run into limitations of this when writing a custom Hiccup compiler / React wrapper. I was trying to compose macros inside a component, each with a bound state by parent macro (defc), but any unevaluated or quoted expressions would run in JS, so I had to manually eval/macroexpand the desired macro calls (e.g. css macro for compiling a component's styles)

borkdude commented 2 years ago

Macros in Clojure are ran inside the compiler. The JS environment of the compiler doesn't need to be the same environment as the JS target environment (but it can be). The macro function itself isn't visible in the output JS. If you need macros at runtime, this is imo a sign that you just needed to write a function in the first place.

alidcast commented 2 years ago

If you need macros at runtime, this is imo a sign that you just needed to write a function in the first place.

Now that I think about it.. I didn't run the logic at runtime since 1/ I wanted the transpiles to take place while compiling, not every time a component was rendered 2/ I did not want transpile logic included in bundle. So you may be right, it wasn't a CLJS-CLJ macro issue but just general dynamic of working with optimized browser code. Sorry for the detour here 😅

borkdude commented 2 years ago

So, this is what macros ran by the compiler in JS might look like. macros.cljs is first compiled to macros.mjs. Then macros.mjs is required with :require-macros from macro_usage.cljs. The compiler loads the (already compiled) macro code before it traverses the whole file, so it has the macros available for execution.

Screenshot 2022-08-01 at 13 04 53
borkdude commented 2 years ago

Possible later convention for inline macros / requiring macros:

By convention we could have

foo.mjs
foo$macros.mjs

and when you (:require ["./foo.mjs" :refer [bar]) then foo$macros would be loaded in the compiler and there would be a check if bar is macro.

Likewise, inline macros would be emitted to foo$macros.mjs instead of foo.mjs

borkdude commented 2 years ago

Part 1 is now complete with the transpiler approach. See examples/react.