squint icon indicating copy to clipboard operation
squint copied to clipboard

Allow specifying macros as argument to `compileString`

Open martinklepsch opened this issue 1 year ago • 6 comments

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.

martinklepsch avatar Mar 31 '24 12:03 martinklepsch

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?

borkdude avatar Mar 31 '24 13:03 borkdude

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!

martinklepsch avatar Mar 31 '24 14:03 martinklepsch

I don't think it will be very convenient if users would have to add library macros manually in the plugin configuration though

borkdude avatar Mar 31 '24 18:03 borkdude

That's not what I'm suggesting. compileString is called by the plugin and the plugin could construct the macros input.

martinklepsch avatar Mar 31 '24 19:03 martinklepsch

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.

borkdude avatar Mar 31 '24 20:03 borkdude

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.

martinklepsch avatar Apr 01 '24 07:04 martinklepsch

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 avatar Jun 26 '24 21:06 nasser

@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/

borkdude avatar Jun 26 '24 21:06 borkdude

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.

nasser avatar Jun 26 '24 21:06 nasser

yes: https://github.com/squint-cljs/squint/blob/1aa8a675ffcddb669aee2ed2d2b40445a8dd64f1/src/squint/compiler.cljc#L550-L557

borkdude avatar Jun 26 '24 21:06 borkdude

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 avatar Jun 26 '24 21:06 nasser

@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.

borkdude avatar Jun 27 '24 09:06 borkdude

Cherry does have runtime keywords and symbols btw.

borkdude avatar Jun 27 '24 09:06 borkdude

thats a good question. is there a way i could use cherry to compile or run the macros?

nasser avatar Jun 27 '24 14:06 nasser

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.

borkdude avatar Jun 27 '24 14:06 borkdude

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.

nasser avatar Jun 27 '24 15:06 nasser

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.

borkdude avatar Jun 27 '24 15:06 borkdude

Btw, @nasser, I'm curious what you're creating :)

borkdude avatar Jun 27 '24 15:06 borkdude

porting https://8fl.live/ to the browser :eyes:

nasser avatar Jun 27 '24 16:06 nasser

nice!

borkdude avatar Jun 27 '24 16:06 borkdude