squint

yuhan 2025-10-11T12:09:47.216959Z

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)

borkdude 2025-10-13T12:15:18.415869Z

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.

borkdude 2025-10-13T12:16:01.594669Z

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

borkdude 2025-10-11T12:10:42.704119Z

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

borkdude 2025-10-11T12:10:50.858359Z

I think I had support for nullish somewhere

borkdude 2025-10-11T12:18:53.925199Z

Ah this is what squint has, support for ?? : https://squint-cljs.github.io/squint/?src=KGpzLT8%2FIG5pbCBqcy91bmRlZmluZWQgMSk%3D

yuhan 2025-10-11T12:18:55.527819Z

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

borkdude 2025-10-11T12:20:43.080809Z

I guess we could add something like:

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

yuhan 2025-10-11T12:22:19.812099Z

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

borkdude 2025-10-11T12:22:41.068939Z

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

borkdude 2025-10-11T12:22:51.758379Z

or methods

yuhan 2025-10-11T12:22:52.776069Z

yeah but that would be .-?foo

yuhan 2025-10-11T12:23:49.643979Z

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

yuhan 2025-10-11T12:31:27.279759Z

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

borkdude 2025-10-11T12:33:02.493759Z

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

yuhan 2025-10-11T12:33:32.655109Z

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

borkdude 2025-10-11T12:34:23.132129Z

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

yuhan 2025-10-11T12:35:50.945069Z

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

borkdude 2025-10-11T12:36:58.629489Z

it uses two = signs:

> undefined == null
true

yuhan 2025-10-11T12:37:36.739029Z

ahh yes just confirmed that in my browser console 🤦

borkdude 2025-10-11T12:37:56.532039Z

I think I just copied this macro over from CLJS

yuhan 2025-10-11T12:42:57.200339Z

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

borkdude 2025-10-11T12:44:20.973239Z

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

borkdude 2025-10-11T12:44:51.338919Z

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

borkdude 2025-10-11T12:45:02.885119Z

just ?? would be better imo

borkdude 2025-10-11T12:45:17.718299Z

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

yuhan 2025-10-11T12:45:54.120709Z

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

borkdude 2025-10-11T12:46:08.722229Z

yep

borkdude 2025-10-11T12:46:27.374469Z

js/ is used for objects globally defined

yuhan 2025-10-11T12:49:20.284369Z

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"

borkdude 2025-10-11T12:50:24.750989Z

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

1
yuhan 2025-10-11T12:56:04.109579Z

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

borkdude 2025-10-11T12:58:42.221629Z

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

yuhan 2025-10-11T12:58:43.946049Z

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

borkdude 2025-10-11T13:00:07.431219Z

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);
}

borkdude 2025-10-11T13:01:45.420909Z

nice idea to inline str as template strings

borkdude 2025-10-11T13:01:53.520929Z

might be a trick I want to adopt in squint

yuhan 2025-10-11T13:05:09.026309Z

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
borkdude 2025-10-11T13:07:33.377199Z

made an issue for str here: https://github.com/squint-cljs/squint/issues/723

borkdude 2025-10-11T13:07:57.909489Z

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

borkdude 2025-10-11T13:08:43.119579Z

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

borkdude 2025-10-11T13:09:34.748809Z

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

👍 1
yuhan 2025-10-11T13:19:38.435489Z

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

borkdude 2025-10-11T13:22:23.816229Z

I don't think this will cause stackoverflows

yuhan 2025-10-11T13:23:15.150739Z

*SO the website

borkdude 2025-10-11T14:48:21.206139Z

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

borkdude 2025-10-11T14:50:33.680109Z

I could expand to

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

borkdude 2025-10-11T14:59:18.097149Z

Maybe this is better:

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

borkdude 2025-10-11T15:02:12.870749Z

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

borkdude 2025-10-11T15:09:09.071929Z

or maybe:

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

borkdude 2025-10-11T15:12:37.019049Z

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

borkdude 2025-10-11T12:42:19.610879Z

eucalypt video: https://clojurians.slack.com/archives/C8NUSGWG6/p1760185407840259