I noticed squint is built on scriptjure. If I'm looking for a simple s-exp to js compiler on the server (clojure server) without any runtime client side dependencies I take it it's a better approach than squint for my use case? squint.core is quite a big dependency in my context. Were there any big problems you ran into with scriptjure?
btw, we do support the IIFE case as much as we can now. the issue is we don't have access to the AST like we'd like. having a PEG or recursive descent would bloat the size tremendously. If only Eich built the LISP he intended. @borkdude your squint work is really inspiring!
Following up on this (I'm interested in the same as anders).. I've tried to run a bit with anders' idea and squint. There's some unique constraints to the context in which I want to use this.. we are generating little snippets of js that sometimes will be embedded in other pre-existing js.
The challenge boils down to squint generating statements where I need expressions. For example (and ...) with multiple sub forms expands into assignment + if/else statements, where I need an expression (`x && ...`).
I think this is because and is implemented https://github.com/squint-cljs/squint/blob/f008c6d898a5c0fb04c7a6a2e7ddcdd8bfc2d498/src/squint/internal/macros.cljc#L493-L503 in terms of let +`if`.
I managed to implement my own and macro:
(defn expr-and
([_ _]
true)
([_ _ x]
x)
([_ _ x & next]
(let [js (str/join " && " (repeat (inc (count next)) "(~{})"))]
(bool-expr
(concat (list 'js* js) (cons x next))))))
Which I can get squint to use by passing :macros {'expr {'and expr-and}} in the opts.
(testc '(expr/and (= "foo" "foo")
(= nil nil)
(= 42 (+ 41 1))))
;; => "((\"foo\") === (\"foo\")) && ((null) === (null)) && ((42) === ((41) + (1)));\n"
That's great, expect for the ; terminator. What I really want is an expression, not a statement. Is there a way to tell squint I want to compile an expression, not a statement? Passing :context :expr to transpile-string* didn't work (I naively hoped it would).:context :expr should work, but you should probably be using compile-string
(defn testc [form]
(squint.compiler/compile-string (str form)
{:elide-imports true :elide-exports true :top-level false :macros {'expr {'and expr-and}}
:context :expr}))
(testc '(expr/and (= "foo" "foo")
(= nil nil)
(= 42 (+ 41 1))))
;; => "((\"foo\") === (\"foo\")) && ((null) === (null)) && ((42) === ((41) + (1)));\n"Neither produces expressions unfortunately
hmm, maybe a bug. it's intended to work. I'll have a look
many thanks
@ramblurr Does this look correct to you now?
user=> (println (sq/compile-string "(and 1 2 3)" {:context :expr :elide-imports true}))
(() => {
const and__1777__auto__1 = 1;
if (squint_core.truth_(and__1777__auto__1)) {
const and__1777__auto__2 = 2;
if (squint_core.truth_(and__1777__auto__2)) {
return 3} else {
return and__1777__auto__2};} else {
return and__1777__auto__1};
})()(this is locally on my machine with some changes)
note that this is an expression, it's a self-calling anonymous function
if you override and with what you made it do, it won't behave correct anymore with respect to truthiness
Yes, I think that's correct. And indeed, if you use my and implementation it would become:
(() => {
((1) === (2)) && ((2) === (3));
})()
which is not correct.But what I'm looking for is just ((1) === (2)) && ((2) === (3)) (without the IIFE and without the statement terminator)
let me first fix this issue, then we'll look further https://github.com/squint-cljs/squint/issues/653
This seems to work already but it's a bit undocumented ;)
(println (sq/compile-string "(&& (= 1 2) (= 2 3))" {:context :expr :elide-imports true}))
((1) === (2)) && ((2) === (3))I'll check if I can get rid of the IIFE with normal and
first, thanks for looking into this! please feel no obligation to change anything.
this isn't just about and, it's about generating statements vs expressions in general
generating IIFEs is probably a good thing in many cases (though not mine)
Yes itβs necessary even because Js isnβt a value oriented language
exactly
I pushed the fix for expr to main
Ok great let me test that.
You can use && as a workaround
So the reason why and expands into an IIFE is that and in squint (and CLJS) is implemented using a macro. but we might as well make it a special form which kinda does what you wrote above
similar with or. I like the idea from a "produce less code" standpoint although in normal projects, esbuild will optimize it
hm no I'm wrong about esbuild here I guess: https://esbuild.github.io/try/#dAAwLjI1LjIALS1taW5pZnkAdmFyIG9yX18xNzkwN19fYXV0b19fXzI4ID0gKDEpOwppZihjbGpzLmNvcmUudHJ1dGhfKG9yX18xNzkwN19fYXV0b19fXzI4KSl7Cn0gZWxzZSB7CiAgdmFyIG9yX18xNzkwN19fYXV0b19fXzI5X18kMSA9ICgyKTsKICBpZihjbGpzLmNvcmUudHJ1dGhfKG9yX18xNzkwN19fYXV0b19fXzI5X18kMSkpewogIH0gZWxzZSB7CiAgfQp9Cg
Made an issue here: https://github.com/squint-cljs/squint/issues/655
"in general" things are hard to state, this must be looked at from a case by case basis
Indeed. So in my case, I need to produce the simplest/least amount of code, so I'm trying to get little/no IIFEs. Thankfully squint is pretty extensible since I can provide my own macros.
what are you using this for if I may ask?
I'm using it to compile datastar expressions, which are almost, but not quite, javascript expressions https://data-star.dev/guide/datastar_expressions
I should try datastar one of these days, hearing a lot of stuff about it
So, taking an example from that page, instead of writing:
<button data-on-click="$landingGearRetracted && @post('/launch')"></div
one can write:
[:button
{:data-on-click (d*/expr
(when $landingGearRetracted
(@post "/launch")))}]where d*/expr is a macro compiling the forms with squint + some pre/post processing:
(d*/expr
(when $landingGearRetracted
(@post "/launch")))
;; => "((!!$landingGearRetracted) ? ((@post(\"/launch\"))) : (null))"so I guess datastar has a little parser for its JS-like DSL which it then transforms to some JS under the hood?
correct yea, though its parser is just regex and has some corner cases that they aren't interested in fixing at the moment because it's code that no human would write
for example it chokes on IIFEs most of the time, which is fine for humans writing code because you wouldn't stuff an IIFE inside an html attribute.
For example in my clj-macro I walk the forms and replace the (do form1 form2 form3) special form with squint-macro that produces (form1, form2, form3)
ok
I am probably abusing squint here, so aside from actual bugs, I'm not asking for any changes to accommodate this use case. (thanks for your time!)
right, sounds like a hack but you did inspire me to some improvements, so thanks anyway :)
re-writing and using && is a bit complicated due to truthiness. you'll end up with something like:
$ ./node_cli.js --repl --show -e '(and 1 2 3)'
(async function() { var squint_core = await import('squint-cljs/core.js');
globalThis.user = globalThis.user || {};
return (squint_core.truth_(1)) && (((squint_core.truth_(squint_core.truth_(2))) ? (2) : (false))) && (((squint_core.truth_(squint_core.truth_(3))) ? (3) : (false))) })()
which isn't really an improvement in readability imo(the first clause should look like the others which is a bug I didn't fix)
(squint.compiler/compile-string "(set! foo \"bar\")"
{:elide-imports true :context :expr})
;; => "foo = \"bar\";\n"
Here is another expression bug(when true (set! foo "bar")) produces ((squint_core.truth_(true)) ? (foo = "bar";\n) : (null)) which is invalid js
thanks, invalid assumption that set! is always in statement mode apparently. can you file a github issue?
fixed
Amazing thanks!
Here is an example of server side squint: https://github.com/squint-cljs/squint/blob/main/examples/babashka/index.clj scriptjure is fine, it was the starting point for squint, but it wasn't compatible with CLJS at all, so you kinda need to learn a new DSL
How big is squint.core actually? Could you tree shake?
Cool that what I thought. That it was more to do with scriptjure not being cljs. Last I checked squint core is 100kb without tree shaking. Which in my context is too big. I'm also exploring compiling scriptjure to js using cljs curious how big it would be. So I can access a lisp JS DSL at runtime. These are mostly just ideas at the moment.
You could also use squint and not use any of the core functions
Is there an easy way to do that? Are core functions explicit when called? I did use squint a few years ago on the server but bundle size wasn't a concern in that context.
So wasn't aware you could just not ship squint.core.
@andersmurphy this is more or less it:
user=> (sq/compile-string "(defn foo [] (let [x 1] (inc x)))" {:elide-imports true :elide-exports true})
"var foo = function () {\nconst x1 = 1;\nreturn (x1 + 1);\n};\n"
As long as you don't touch core vars like assoc etc you should be fine.There's also a minified umd build of the core library which is 28k
$ ls -la node_modules/squint-cljs/lib/squint.core.umd.js
-rw-r--r-- 1 borkdude wheel 28971 2 mrt 15:29 node_modules/squint-cljs/lib/squint.core.umd.js
the original ES6 version is 55kb unminified
All of this is fantastic thank you for taking the time to write the example. 28k might be fine. Even more so if that's pre gzip.
yes of course pre gzip. if you want to use the umd build you can use the core-alias features:
user=> (sq/compile-string "(defn foo [] (assoc nil 1 2))" {:elide-imports true :elide-exports true :core-alias "sq"})
"var foo = function () {\nreturn sq.assoc(null, 1, 2);\n};\n"
So you just define the umd library as an sq global and it should workPerfect thanks!