cljs-dev

dnolen 2025-07-30T01:04:19.192229Z

:lite-mode is looking pretty interesting, 7K brotli / 8K gzip for println + vector

❓ 1
dnolen 2025-07-30T01:05:08.802269Z

but if we use the same strategy for map and vector and set - I don't think it will grow much beyond that

dnolen 2025-07-30T01:05:56.755169Z

that is just loading the original data structure implementations from 2011 w/ whatever fixes or modifications they might need

dnolen 2025-07-30T01:07:20.275249Z

low impact because it's a compiler flag, and adding the old implementation isn't really all that much code

dnolen 2025-07-30T01:08:10.449089Z

idea is to make the compiler just do something for literals - and then as people play around w/ it - could do compiler pass stuff to fix all cases.

dnolen 2025-07-30T03:28:43.202299Z

of course if you skip printing because you're just building something simple - initial size will be much smaller than 7K, more like 3K before compression for a vector, using hash maps and sets won't be adding much either

thheller 2025-07-30T09:24:29.711179Z

how do you feel about adding access to newer JS features such as spread/rest params? I mean a lot of code is "wasted" on juggling arguments?

💯 1
thheller 2025-07-30T09:25:01.521739Z

code like this is pretty common

zm = function (a) {
  switch (arguments.length) {
    case 6:
      return Am(
        arguments[0],
        arguments[1],
        arguments[2],
        arguments[3],
        arguments[4],
        arguments[5]
      );
    case 7:
      return ym(
        arguments[0],
        arguments[1],
        arguments[2],
        arguments[3],
        arguments[4],
        arguments[5],
        arguments[6]
      );
    default:
      throw Error(["Invalid arity: ", v.g(arguments.length)].join(""));
  }
};

thheller 2025-07-30T09:25:47.957739Z

zm = function (...a) {
  switch (a.length) {
    case 6:
      return Am(...a);
    case 7:
      return ym(...a);
    default:
      throw Error(["Invalid arity: ", v.g(a.length)].join(""));
  }
};

💯 1
thheller 2025-07-30T09:26:34.296189Z

dunno how that actually compares after gzip, but might be worth investigating

🤔 1
dnolen 2025-07-30T11:09:23.391649Z

I don't really love the idea of relying on newer JS features (we currently have a mistake in the codebase I need to address), it has to be a big win. I did play around w/ sharing the error strings but compression showed it made no difference at all. for repeated stuff like this probably compression fixes it for us.

dnolen 2025-07-30T11:09:51.922759Z

But, not against considering it if someone wants to do a code size assessment

borkdude 2025-08-08T21:22:11.593459Z

I implemented this for squint just now: https://clojurians.slack.com/archives/C07UQ678E/p1753867547957739?thread_ts=1753867469.711179&cid=C07UQ678E but I think it will break applying a function to an infinite sequence. e.g.: (apply (fn [x & xs] x) (range)) so I'll likely have to revert this change

thheller 2025-08-09T06:25:54.936589Z

should only use the spread thing for fixed multi-arity fns. variadic stuff must have special handling precisely because of apply and seqs

borkdude 2025-08-09T06:59:35.790429Z

👍

borkdude 2025-08-09T07:00:44.374069Z

this will complicate the code somewhat though. the outer function:

zm = function (...a) 
can only do this when all the arities are fixed but when there is one varargs, you must fall back to the old impl

thheller 2025-08-09T07:36:14.053049Z

hmm why? the variadic bit currently has to do this array copy to get convert arguments, which is then no longer necessary?

thheller 2025-08-09T07:36:17.490069Z

demo.browser.x = function demo$browser$x(...a) {
  switch (a.length) {
    case 1:
      return demo.browser.x.cljs$core$IFn$_invoke$arity$1(...a);
      break;
    case 2:
      return demo.browser.x.cljs$core$IFn$_invoke$arity$2(...a);
      break;
    default:
      var argseq__5775__auto__ =
        2 < a.length
          ? new cljs.core.IndexedSeq(a.slice(2), 0, null)
          : null;
      return demo.browser.x.cljs$core$IFn$_invoke$arity$variadic(
        a[0],
        a[1],
        argseq__5775__auto__
      );
  }
};

thheller 2025-08-09T07:37:23.028869Z

I mean this would need to be benchmarked to see if the ...a spread is even usefull perf wise. could just stick with a[0] etc, but since the rest thing creates a proper array it should make handling actually easier

thheller 2025-08-09T07:38:30.641619Z

demo.browser.x = function demo$browser$x(var_args) {
  var G__45643 = arguments.length;
  switch (G__45643) {
    case 1:
      return demo.browser.x.cljs$core$IFn$_invoke$arity$1(arguments[0]);

      break;
    case 2:
      return demo.browser.x.cljs$core$IFn$_invoke$arity$2(
        arguments[0],
        arguments[1]
      );

      break;
    default:
      var args_arr__5774__auto__ = [];
      var len__5749__auto___45666 = arguments.length;
      var i__5750__auto___45667 = 0;
      while (true) {
        if (i__5750__auto___45667 < len__5749__auto___45666) {
          args_arr__5774__auto__.push(arguments[i__5750__auto___45667]);

          var G__45668 = i__5750__auto___45667 + 1;
          i__5750__auto___45667 = G__45668;
          continue;
        } else {
        }
        break;
      }

      var argseq__5775__auto__ =
        2 < args_arr__5774__auto__.length
          ? new cljs.core.IndexedSeq(args_arr__5774__auto__.slice(2), 0, null)
          : null;
      return demo.browser.x.cljs$core$IFn$_invoke$arity$variadic(
        arguments[0],
        arguments[1],
        argseq__5775__auto__
      );
  }
};
compared to this currently I mean

thheller 2025-08-09T07:42:30.338999Z

heck, given that its a proper array we might even be able to change the variadic impl to something like

demo.browser.x = function demo$browser$x(...a) {
  switch (a.length) {
    case 1:
      return demo.browser.x.cljs$core$IFn$_invoke$arity$1(...a);
      break;
    case 2:
      return demo.browser.x.cljs$core$IFn$_invoke$arity$2(...a);
      break;
    default:
      return demo.browser.x.cljs$core$IFn$_invoke$arity$variadic(a, 2);
  }
};
I have spent no time on this, so definitely requires more thought, but I'm fairly confident that it simplifies a lot of code overall

borkdude 2025-08-09T07:43:34.522199Z

What I mean is, the outer function has:

demo$browser$x(...a) 
wouldn't this convert the infinite lazy seq to an array, which isn't possible in finite time and space?

thheller 2025-08-09T07:46:50.172319Z

no? I'm confused, not actually sure what you mean.

borkdude 2025-08-09T07:50:20.344639Z

let's just start with this example:

(apply (fn [x & xs] x) (range))
how would this be handled by the above? since a range doesn't have a length property, I guess it would hit the default case which then works properly with seqs? I think I got it then

thheller 2025-08-09T08:04:39.306299Z

yeah that hits the applyTo helper first, which extracts the first two args and calls the $variadic fn. so should be fine.

👍 1
dnolen 2025-07-30T11:45:10.410469Z

yeah there's still a way to go (not clear what bumps I'm gonna hit here), buuuuut .... in master which has a large number of changes for code size, 8K brotli for just a vector, 1K brotli in the :lite-mode branch

dnolen 2025-07-30T11:45:38.904529Z

so people building simple interactive stuff, the cost of CLJS just went waaaaaay down.

😍 2
dnolen 2025-07-30T12:54:47.841409Z

3K vs 88K for a simple hash-map in the :lite-mode branch

borkdude 2025-07-30T12:56:13.695539Z

nice work. does equality and hashing etc still work the same way?

dnolen 2025-07-30T12:57:11.323579Z

it's all protocols so if it's implemented yes

thheller 2025-07-30T12:57:34.822789Z

but how do you explain the difference? I mean PersistentHash/ArrayMap are not 85K of code?

dnolen 2025-07-30T12:58:02.016599Z

oh but actually they are

dnolen 2025-07-30T12:58:05.866499Z

PHM needs PAM needs Vector needs ChunkedSeq ...

dnolen 2025-07-30T12:59:01.065849Z

once you hit PHM you pull in everything

dnolen 2025-07-30T12:59:06.190869Z

PAM pulls in PHM

dnolen 2025-07-30T12:59:45.748589Z

and the Transient impls

dnolen 2025-07-30T12:59:57.771669Z

and MapEntry - anyways you get the idea

thheller 2025-07-30T13:00:21.077169Z

right ok

dnolen 2025-07-30T13:00:50.872109Z

so it's a mess, so using the old stuff means you just avoid all that

dnolen 2025-07-30T13:01:08.466309Z

so I see two goals w/ :lite-mode

dnolen 2025-07-30T13:01:50.261629Z

1. removing DCE issues remaining in the standard lib beyond data structures

dnolen 2025-07-30T13:02:50.437169Z

2. gauging what people actually need/tolerate in :lite-mode - like just not implementing toString

dnolen 2025-07-30T13:04:02.270349Z

2 implies giving up on full fidelity, perhaps even compatibility in some cases - maybe that's fine - not going to figure that on day 1 or even probably in a year

dnolen 2025-07-30T13:04:43.372939Z

so :lite-mode might be for people who just don't care about dependencies - they just want to write some Clojure, get a tiny artifact and go do something else

borkdude 2025-07-30T13:06:10.052989Z

@dnolen squint was also made to get very small artifacts, so you can actually publish your "CLJS" library to npm without bringing the whole data structure lib + part of the stdlib into it. one extra consideration with squint (and cherry) is that two or more libraries can share the same stdlib - this is done by compiling against a standard library which it itself a library on npm of which you get just one copy. I understand that this may not be a goal of lite-mode, since you want to have nothing to do with npm.

thheller 2025-07-30T13:06:27.318599Z

I mean some knobs to tweak the size would definitely be nice. I already have a few in shadow-cljs to shave a few pct here and there.

dnolen 2025-07-30T13:07:43.796419Z

definitely! this is a big knob for that - it might open the door for others.

dnolen 2025-07-30T13:08:24.767149Z

I think w/ some sweat thought we can get CLJS <10K compressed for lots of simple programs

borkdude 2025-07-30T13:08:43.414369Z

squint is also copy-on-write. you will definitely notice the difference e.g. when doing Advent of Code puzzles where in some cases where you're updating a map a million times

dnolen 2025-07-30T13:09:46.475619Z

Haha, I know, I literally copied this stuff from 2011 commit 🙂 I remember how "fast" CLJS was back then

dnolen 2025-07-30T12:55:29.511239Z

one of the big benefits here is going to be much like the last exercise - "why does the code size suddenly explode?"

dnolen 2025-07-30T12:55:50.952809Z

but now you don't have all these persistent data structures that refer to each other muddying the waters

dnolen 2025-07-30T12:57:55.024769Z

that said - I think getting :lite-mode to parity is going to be a long haul and a non-goal for shipping an experimental flag

dnolen 2025-07-30T21:39:10.520129Z

it's looking more and more promising

dnolen 2025-07-30T21:39:42.696179Z

just doing some trivial map, filter, range etc leads sub 10K brotli (like 5-8K)

dnolen 2025-07-30T21:40:37.075079Z

add the data structures and you're still around 10K

dnolen 2025-07-30T21:41:49.915979Z

needs a lot more testing, but overall I think this is really cool and overall compelling story

dnolen 2025-07-30T21:43:07.879199Z

you can just play around w/ :lite-mode and have fun - the cost is marginal if not better than JS alternatives and you don't have to think about depenendencies at all

dnolen 2025-07-30T21:46:24.364629Z

I was playing around goog.ui.Popup the other day and the positioning stuff, after having used some React stuff I was much more impressed w/ the decomplected nature of GCL

dnolen 2025-07-30T21:47:24.514769Z

stepping back even further, WebComponents sans Shadow DOM by tying together some GCL is look really cool to me - and diffing whatever is completely optional and can work.