squint 2025-10-11

any similar cheat to get native nullish coalescing operators 😬 Sure some-> exists but it compiles to a pretty heavy-looking iife (plus slightly different semantics around handling undefined)

I played around some more and made str go through a macro:

(core/defmacro stringify [& xs]
  (let [args (map (fn [expr]
                    (cond (constant? expr)
                          [(->str expr) nil]
                          (nil? expr)
                          ["" nil]
                          :else ["${(~{}) ?? ''}" expr])) xs)]
    `(~'js*
      ~(str "`" (str/join "" (map first args)) "`")
      ~@(keep second args))))
$ ./node_cli.js --show -e '(str 1 (+ 1 2 3) :foo nil 3 "dude")'
import * as squint_core from 'squint-cljs/core.js';
`1${((1) + (2) + (3)) ?? ''}foo3dude`;
This spits out a template macro (because strings in str can contain newlines I had to use the template) and optimizes for literal strings, keywords or numbers. It still adds ?? '' to make nil and undefined appear as empty strings.

This is still in a branch but performance is much better this way, so thanks for the idea

give me an example please, preferably a link to the playground

I think I had support for nullish somewhere

something like this: https://squint-cljs.github.io/squint/?src=KHNvbWUtPiBldnQgLi1rZXkgKC50b2xvd2VyQ2FzZSkp in an event handler where idiomatic js might be evt?.key?.toLowerCase()

I guess we could add something like:

(?.. {:foo :bar} -foo -bar (dude))

I was thinking along the lines of (.?-foo obj) instead

the problem here is that objects may have fields starting with a question mark

yeah but that would be .-?foo

or follow the js syntax and put the ? before the dot, although that has its own problems

fwiw I ended up adding that feature in my own ad-hoc transpiler for datastar expressions a while ago because of how often I found myself needing it, here's a snippet from it in use:

(dx/emit
  `(do
     (evt.preventDefault)
     (?.setPointerCapture el evt.pointerId)
     (let [m   (or (?.-matrix (.consolidate el.transform.baseVal))
                 (new js/DOMMatrix))
           startx (.-e m)
           starty (.-f m)
           ctm (.getScreenCTM $svg)
           p0  (.matrixTransform (new js/DOMPoint
                                   (.-clientX evt)
                                   (.-clientY evt))
                 (.inverse ctm))]
       ;; store offset + toggle dragging
       (set! $offdx (- p0.x startx))
       (set! $offdy (- p0.y starty))
       (set! $isDragging true))))
;; => "(evt.preventDefault(),el?.setPointerCapture(evt.pointerId),(m=>(startx=>(starty=>(ctm=>(inv=>(p0=>($offdx=(p0.x-startx),$offdy=(p0.y-starty),$isDragging=true))(new DOMPoint(evt.clientX,evt.clientY).matrixTransform(inv)))(ctm.inverse()))($svg.getScreenCTM()))(m.f))(m.e))(el.transform.baseVal.consolidate()?.matrix||(new DOMMatrix())))"

cool, so (?.fooo) means: call fooo if it exists?

yep, if it's non-null and non-undefined

you were saying something about some-> working differently with undefined or so - can you explain this?

from the compilation output it seems that some-> only checks against the === null case? I don't really know how much undefined crops up in js though / how important it is to guard against it in these checks

it uses two = signs:

> undefined == null
true

ahh yes just confirmed that in my browser console 🤦

I think I just copied this macro over from CLJS

hmm and wouldn't it make more sense to use the js/ pseudo-ns for these native escape hatches - ie js/?? rather than js-??

js-... is a CLJS convention for js keywords.

cljs.core/js-arguments
 cljs.core/js-comment
 cljs.core/js-debugger
 cljs.core/js-delete
 cljs.core/js-fn?
 cljs.core/js-in
 cljs.core/js-inline-comment
 cljs.core/js-invoke
 cljs.core/js-iterable?
 cljs.core/js-keys
 cljs.core/js-mod
 cljs.core/js-obj
 cljs.core/js-reserved
 cljs.core/js-str

but I have to admit that I don't love js-??

just ?? would be better imo

but then clj-kondo will complain about an unknown var :)

yeah and it would clash with any local or var named _QMARK__QMARK_

js/ is used for objects globally defined

hmm that's true, in my transpiler I just used it as a shorthand for "raw-string passthrough / don't bother with whatever this means"

that would be (js* "...") in CLJS and squint

1

I should get round to releasing that lib some time.. was planning to a couple months ago then discovered some nasty edge cases around let-binding closures / shadowing , decided to dogfood it a bit more first

sounds similar to squint. people have also been using squint for datastar

went with a slightly funky way of transpiling let-bindings into nested lambdas, apparently that's a pretty common technique used by other languages like purescript and browser engines are good at handling it (emit `(let [a 1 b (inc a) c (* a b)] (str a b c))) ;; => "(a=>(b=>(c=>`${a}${b}${c}`)(a*b))(a+1))(1)"

nice. yeah, squint tries to be a little more conservative here it seems:

() => {
  const a1 = 1;
  const b2 = a1 + 1;
  const c3 = a1 * b2;
  return squint_core.str(a1, b2, c3);
}

nice idea to inline str as template strings

might be a trick I want to adopt in squint

https://gist.github.com/yuhan0/a87796eedd902996c53f4f17b39067a5 here's an early version of it if you're interested - think I ended up overhauling many of the bits involving macroexpansions

👍 1

the other thing seems to perform somewhat worse than the nested lambda thing so I'm going to leave that

// Version 1
const fn1 = () => (a => (b => (c => `${a}${b}${c}`)(a * b))(a + 1))(1);

// Version 2
const squint_core = { str: (...args) => args.join("") };
const fn2 = () => {
  const a1 = 1;
  const b2 = a1 + 1;
  const c3 = a1 * b2;
  return `${a1}${b2}${c3}`;
};
$ node /tmp/foo.js
fn1: 1454.515 ms
fn2: 34.105 ms
const ITERATIONS = 1_000_000_00;

with the un-inlined str call, the nested lambda still wins. so inlining that str thing is a good optimization

👍 1

oof that's good to see an actual benchmark, I think I just found a stackoverflow post or other talking about the technique and took their word for it

I don't think this will cause stackoverflows

*SO the website

Hmm, I thought the inlining of str to templates was an easy no-brainer but for nil values it gets null into the string

I could expand to

`${1 ?? ""}${2 ?? ""}${3 ?? ""}${null ?? ""}${4 ?? ""}
`

Maybe this is better:

(core/defmacro stringify [& xs]
  `(.join [~@xs] ""))

but that's not any faster than what squint already does

or maybe:

(1 ?? "") +
  (2 ?? "") +
  (null ?? "") +
  (undefined ?? "") +
  (3 ?? "") +
  (4 ?? "")

so:

return squint_core.str(1, 2, null, undefined, 3, 4);
return ((1 ?? "") + (2 ?? "") + (null ?? "") + (undefined ?? "") + (3 ?? "") + (4 ?? ""));
hmm, probably the first is good enough