squint 2025-03-02

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!

Or you could just support a lisp directly :)

😁 1

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

@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

πŸ‘ 1
❀️ 1

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

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

"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)

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

πŸ‘ 1

(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?

oh you did, I see it now :)

πŸ™Œ 1

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.

Yes, I’ll make an example when I’m back at home

πŸ™ 1

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

πŸš€ 1