Allow specifying macros as argument to `compileString`
To upvote this issue, give it a thumbs up. See this list for the most upvoted issues.
Is your feature request related to a problem? Please describe.
Currently macros rely on a "classpath style" construct via squint.edn. For tools like Vite and general flexibility it would be nice to be able to provide macros to the compileString function.
Describe the solution you'd like
Document (and implement if not already) an extra input to compileString that takes macros. This could be plain JS functions.
It sounds like this is a solution proposal rather than a problem statement. The current solution that is proposed, how would that play with macros from libraries?
Fair, it's taking into account some things you already told me about the macro API.
As to how it considers dependencies: it wouldn't. Much like compileString doesn't really consider dependencies. I'm thinking of this as a low level extension mechanism through which other tools can provide macro implementations (from wherever).
Very open to other ideas as well, I guess what I'm suggesting is more about a "no assumptions" API to provide macros. Hope that makes sense!
I don't think it will be very convenient if users would have to add library macros manually in the plugin configuration though
That's not what I'm suggesting. compileString is called by the plugin and the plugin could construct the macros input.
That's only one piece of the puzzle though. The other maybe harder piece would be: where would the plugin get the information from?
I guess another approach could be to still have a squint.edn such that compileString resolves (:require-macros [...]) based on that configuration. Similarly (:require [foo.bar]) could also be resolved by compileString using the same logic that is used for squint.edn currently.
The plugin could also look in src or even have some built in logic to find namespaces in node_modules or even jars.
This is far out of course but I'm not seeing this API as something that end users would use but rather build tools that work on top of squint (like the Vite plugin).
squint.edn could of course still exist as an alternative or even work in conjunction with these tools.
I am trying to use squint to compile user code using compileString entirely in the browser, so I don't have access to squint.edn. At the very least I need to expose some of my own macros to the compiler so that user code has access to them, but ideally users could define their own macros as well.
@nasser compileString already accepts a map of :macros like this: {'namespace {'macro-name (fn [form env] ...)}}.
Currently it only works on the CLJ/CLJS side, but it could be made to work on the JS side as well.
Demo in JVM Clojure:
user=> (sq/compile-string "(dude/foo 1)" {:macros {'dude {'foo (fn [form env x] `(do (js/console.log ~x) ~x))}}})
"import * as squint_core from 'squint-cljs/core.js';\nconsole.log(1);\n1;\n"
Squint doesn't support "inline" defmacro, only macros defined in another file which is referred to with :require-macros and currently only the NodeJS squint command line tool pre-processes those, the lower level compile-string function just ignores those. I can provide more details about why that is, but maybe let's focus on the first use case for now.
Note that scittle (based on SCI = Clojure interpreter) does support inline macros without any problems since how it works is closer to how JVM Clojure works: https://babashka.org/scittle/
oh very cool! thanks for the background!
yeah i am less interested in the inline defmacro case and it is not lost on me how hard it is to get that stuff to work.
could you point me in the direction of what to change to expose the :macros key to the javascript api? happy to take out a PR to that effect if that's helpful.
yes: https://github.com/squint-cljs/squint/blob/1aa8a675ffcddb669aee2ed2d2b40445a8dd64f1/src/squint/compiler.cljc#L550-L557
awesome i am on it.
one thing -- do macros referenced this way always need to be fully qualified? i.e. in your example is there a way i could say (foo 1) instead if (dude/foo 1)?
@nasser Before we proceed, one more question. How would you write macros in pure JS or squint? Macros typically deal with CLJS data structures with symbols and keywords. Squint doesn't have the concept of keywords and/or symbols. This is why macros run in SCI when using squint with the command line.
Cherry does have runtime keywords and symbols btw.
thats a good question. is there a way i could use cherry to compile or run the macros?
No, since cherry is advanced compiled in a different build than squint and their symbol/keyword/data-structure stuff isn't interchangeable. It's way easier to use squint from ClojureScript, mix in your macros, then compile that to a final artifact and use that instead.
you're totally right, and given that i don't need macros beyond the few built-in ones in my system i think that's the path of least resistance. exposing my own compile-string like this
(ns eighth-floor.core
(:require [squint.compiler :as sq]))
(defn genstr [s]
(-> (js/Math.random) (.toString 32) (.replace "0." (str s "."))))
(aset js/globalThis "$CYCLES" #js {})
(def default-opts
{:macros
{'<> (fn [form env x]
(let [name (genstr "cycle")]
`(do
(when-not (aget js/globalThis.$CYCLES ~name)
(aset js/globalThis.$CYCLES ~name (cycle ~x)))
(aget js/globalThis.$CYCLES ~name))))}})
(defn compile-string
([s] (compile-string s nil))
([s opts]
(sq/compile-string s (-> opts
sq/clj-ize-opts
(merge default-opts)))))
means i can do this from JS
import { compileString } from './lib/eighth-floor.js';
const compiled = compileString("(fn [] (+ 1 (<> 10 (str [1 2 3 4]))))", { 'elide-imports': true, context: 'expr' });
console.log(compiled)
// (function () {
// return (1) + ((() => {
// if (squint_core.truth_(globalThis.$CYCLES["cycle.fggh95bnobo"])) {
// } else {
// squint_core.aset(globalThis.$CYCLES, "cycle.fggh95bnobo", squint_core.cycle(10))};
// return globalThis.$CYCLES["cycle.fggh95bnobo"];
// })());
// });
which is exactly what i need! thanks for your guidance @borkdude.
no PR coming from me in that case, especially given your point that it is not clear how to run the macros from the JS API side.
Excellent, thank you! And also thank you for letting me think through this issue more, in hindsight it doesn't make that much sense to allow this to be done from the JS side, so I'll close this.
Btw, @nasser, I'm curious what you're creating :)
porting https://8fl.live/ to the browser :eyes:
nice!