cljs-dev

p-himik 2025-09-01T11:38:22.441209Z

A real-life example where implements? can potentially fail in minified code on particular JS objects: https://clojurians.slack.com/archives/C03S1L9DN/p1756723252000659 Tried it in my end. The minimized version of thisfn in js->clj produces a bunch of conditions like g.j & 64, and sure enough if you feed {j: ...} that matches one of these conditions, the function throws. Perhaps msym in implements? should not be minified?

dnolen 2025-09-02T11:37:48.033549Z

it's too late to change anything here, looking at the history I think people found themselves converting JS values w/ CLJS values in them.

dnolen 2025-09-02T11:41:28.847319Z

my suggestion would be don't bother w/ js->clj at all - it's really hard for me to come w/ examples that I've seen in projects that I worked on that it was worth the trouble.

dnolen 2025-09-02T11:42:32.785779Z

it might be worth adding a warning to js->clj about advanced compilation

p-himik 2025-09-02T11:44:41.521219Z

> it's too late to change anything here But why? Surely it's not a breaking change to stop renaming an already existing field? > my suggestion would be don't bother w/ js->clj at all But the impact goes well beyond that, to any instance?, satisfies? and even, as you mentioned, some goog stuff.

dnolen 2025-09-02T11:45:44.176069Z

yes I would not consider this a bug, but a bad idea wrt. to js->clj

dnolen 2025-09-02T11:46:05.449589Z

but for a lot of cases people use js->clj w/o this problem

dnolen 2025-09-02T11:46:57.778929Z

I have been burned by this before myself, but I think I learned my lesson and stop using js->clj for JSON values I don't control

dnolen 2025-09-02T11:49:39.658809Z

there's more than just one property we can't rename - three it seems

dnolen 2025-09-02T11:51:32.090129Z

in general, I like the idea of discouraging people from using it (which has other repercussions) than preventing renaming of protocols which is a bad case of special casing

p-himik 2025-09-02T11:53:53.078359Z

> wrt. to js->clj What about all other usages of implements? and satisfies? though?

dnolen 2025-09-02T11:56:16.224589Z

this is a DCE issue for sure depending on what you're doing to not collapse these, not interested

dnolen 2025-09-02T11:56:27.633549Z

sorry not DCE, rather but code size

dnolen 2025-09-02T11:57:22.081899Z

anyways changing stuff because there's one bad API decision is undesirable

p-himik 2025-09-02T12:07:29.018079Z

Sorry, I still don't quite get it. Why would the change be undesirable if it has no significant repercussions to anyone but can potentially save hours if not days of work for multiple engineers that have randomly stumbled upon it? I've seen some weird issues that took me days and ended up seemingly going away on their own, so I'm personally just predisposed against leaving rare but pernicious footguns around. :) It's not too hard to imagine some code out there that uses walk on some data that can have JS object with the branch fn using implements? or something like that. Regarding code size - it's a factor of two things: • How the end result looks like - x.long_field = 1 vs. x[get_field_name()] = 1. If it's the latter, there will be no significant difference. Although I can see from defmethod emit* :deftype that it's the former, and changing this to the latter would probably have an undesirable performance impact • How well it compresses. AFAICT you yourself mostly care about compressed bundle size. And surely strings like cljs$lang$protocol_mask$partition would be compressed quite well? Could it be something worth experimenting with?

dnolen 2025-09-02T12:13:57.767459Z

code, maybe users are generating types and protocols, you have no idea what people might be doing

dnolen 2025-09-02T12:14:10.374149Z

if you can imagine it I guarantee it is being done

dnolen 2025-09-02T12:14:44.929989Z

js->clj is not good

dnolen 2025-09-02T12:14:54.576929Z

changing code size is extremely undesirable

dnolen 2025-09-02T12:15:12.099759Z

those are the 2 principles at play here

p-himik 2025-09-02T12:18:59.565269Z

> if you can imagine it I guarantee it is being done That's for sure, but I might be lacking imagination in this department. :) I'm a "boring" kinda guy with my code. What do you mean by "generating"? Using macros to spit out deftype so that there are thousands of types/protocols/whatever? If so, it would just mean that there are thousands of -cljs$lang$protocol_mask$partition17$ being around, and it is my assumption that the part before the number is very amenable to compression. > js->clj is not good Yeah, forget about js->clj, I'm not talking about it at all. Only about the impl details of implements? et al. > changing code size is extremely undesirable If code size post-compression remains the same, why would increasing the code size pre-compression be a negative? Are pre-compression sizes relevant at all?

dnolen 2025-09-02T12:20:11.603899Z

we cannot fix all the cases - this is just papering over a hard fact of advanced compilation.

dnolen 2025-09-02T12:20:28.950429Z

again js->clj is not good, it was extremely reluctantly added years ago

dnolen 2025-09-02T12:20:35.098879Z

and the bad idea was "improved" over time

dnolen 2025-09-02T12:21:00.923939Z

investing in bad ideas is just not a good use of time, thus the preference for warning to stop using this thing

p-himik 2025-09-02T12:22:43.392969Z

Were implements? and satsifies? bad ideas by themselves? I can come up with a case where they result in errors in advanced compilation just as easily, without js->clj whatsoever. Here a proper fix is possible. As long as people don't actually use -cljs$lang$protocol_mask$partition in their own objects.

dnolen 2025-09-02T12:35:03.493299Z

a proper fix is not possible - I only agree that you could make an assessment that's is all.

dnolen 2025-09-02T12:35:25.495959Z

but this is just not a good use of time if you think about it some more

dnolen 2025-09-02T12:35:44.701239Z

there are many, many other generated properties, i.e. for fn dispatch

p-himik 2025-09-02T12:37:38.670659Z

Is bundle size the only thing that makes not renaming generated property names an improper fix? Or are there other concerns?

dnolen 2025-09-02T12:37:43.810389Z

if you go from first principles on how CLJS is intended to be used (advanced compilation) it becomes obvious that in general you can not expect to put foreign obj / JSON values into a CLJS program and expect it to work w/o using the right apis (goog.object)

dnolen 2025-09-02T12:38:06.079219Z

solving the bundle size problem is ClojureScript

dnolen 2025-09-02T12:39:35.591999Z

there is absolutely nothing wrong w/ us generating properties or whatever

dnolen 2025-09-02T12:40:03.238699Z

the only thing thing that is wrong is that js->clj is being used for foreign object values which can't possible be advanced compilation aware

dnolen 2025-09-02T12:40:27.565349Z

fixing things downstream of a misconception does not lead to good place.

p-himik 2025-09-02T12:49:39.654109Z

> you can not expect to put foreign obj / JSON values into a CLJS program and expect it to work w/o using the right apis (goog.object) That's an interesting perspective. Definitely not something I'd call obvious or even intuitive. Especially given that CLJS has some affordances for consuming JS objects of all kinds from CLJS directly, without goog or JS APIs. Like being able to (seq js-iterable), which can also potentially explode if the iterable object has some j field or whatever.

dnolen 2025-09-02T12:51:53.513969Z

I'm not claiming it is obvious - because now you're talking about something that Closure knows about

dnolen 2025-09-02T12:52:31.305359Z

Closure knows about programmatic patterns w/ JS object values, it cannot possibly know about JSON data values from some random API

p-himik 2025-09-02T12:55:35.679919Z

Suppose I include a random JS library from NPM in my build. Does it get passed through GCC so it learns about each and every field in each and every object and avoids using those names for itself?

dnolen 2025-09-02T12:57:51.666139Z

no it is foreign, all the same issues as js->clj apply - but in this case you have a different tool to help you, externs

dnolen 2025-09-02T12:58:44.375839Z

(which also means you could probably extern this API that is breaking w/ js->clj to avoid a property collapse collision)

p-himik 2025-09-02T13:01:23.456269Z

But it's not a part of a public API of the library, it's an internal detail that's supposed to stay minified - why would there be externs for it? Even if I notice that some of the object prototypes in some library have a field j that conflicts with whatever GCC produces and I add extern for that field so that GCC doesn't use the field name anymore for itself (assuming that's how it works in this case - I don't actually know), that won't solve anything whatsoever. Library 0.0.1 works today, library 0.0.2 pushes a new mifinied dist and my externs become immediately obsolete, even though from the perspective of the library it wasn't a breaking change at all.

dnolen 2025-09-02T13:02:30.661349Z

this is not argument, this is just a fact

dnolen 2025-09-02T13:03:03.589909Z

any possible collision w/ advanced compilation you could have imagine is guaranteed to have occurred to someone

dnolen 2025-09-02T13:03:33.632949Z

what I mean is you are correct - those kinda of problems are a direct result of Google Closure not seeing something

dnolen 2025-09-02T13:03:42.261309Z

and they occur in practice

dnolen 2025-09-02T13:05:25.793649Z

this isn't new, and again in the general case, nothing you can really do about other than, care less about bundle size 😉

p-himik 2025-09-02T13:05:42.233069Z

But we can reduce the collisions - that's my whole point. Currently, we exchange a degree of correctness for some ephemeral bytes that will be compressed away anyway. That could be improved. Even if it doesn't make everything perfect. Seatbelts don't prevent incidents or even eliminate deaths. And they do cost money. But they do reduce the overall harm being done to people.

dnolen 2025-09-02T13:06:24.852399Z

I don't know, these kinda of reports were more prevalent in the early days

dnolen 2025-09-02T13:06:38.232859Z

you can go back through the history and hear me complain about js->clj for years

dnolen 2025-09-02T13:07:42.021419Z

I think externs inference, Transit etc helped so people don't talk about it as much

dnolen 2025-09-02T13:08:03.979199Z

but if you want to provide evidence that time is well spent here, I'm also not against it.

dnolen 2025-09-02T13:08:16.266399Z

a typical case I would be concerned about is code gen against a database.

dnolen 2025-09-02T13:08:25.203219Z

i.e. hundreds of generated types

dnolen 2025-09-02T13:11:18.487079Z

probably anything greater than 10% code size increase would be considered unnacceptable

dnolen 2025-09-02T13:11:35.637109Z

compressed against uncompressed would be weighed

dnolen 2025-09-02T13:12:05.866299Z

I'm just not interested in the effort myself but would seriously consider a good report

p-himik 2025-09-02T13:16:13.825639Z

We're all in a bubble, we never get full reports on anything. A random engineer would likely just slap ;; NOTE: Don't use this, use the other parameter instead. This one breaks for some reason onto some commented out code and be done with it. And that's closer to the best end of the best-worst case scenario. I would not be surprised if the vast majority of Clojure engineers never even touch any Clojure-related social outlets. It's my own bubble of course, but that was the picture in every single place I worked at while I was still an office worker. Maybe like 1 in 20 of people would be discussing things online or reporting issues. > probably anything greater than 10% code size increase would be considered unnacceptable > I'm just not interested in the effort myself but would seriously consider a good report Ah, OK. You've answered a question I've been typing out. > compressed against uncompressed would be weighed Wait, why? Shouldn't it be "vanilla+compressed vs. changed+compressed"?

dnolen 2025-09-02T13:20:21.571969Z

That’s what I meant.

👍 1
Pepijn de Vos 2025-09-08T10:52:29.787479Z

So let me see if I understand correctly: Arbitrary JS objects fundamentally cannot exist bug-free in a Closure universe because Closure objects have magical properties that may happen to collide with arbitrary JS objects. (Cljs objects are Google Cosure objects) The solution that p-himlk suggests is to reduce these conflicts, which it seems you'd be open to if it doesn't regress file size, but overall not optimistic about. Personally I'm the opposite, I don't care at all about any standard library functions working on arbitrary JS objects, but for a language that prides itself in good interop, I think it's essential to have a safe and reliable way to safely consume JS objects into the cljs universe. I'm sure that people are using js->clj for mixed JS/Cljs objects that are aiui fundamentally a bad idea, but what I would want is essentially the json->clj that p-himlk provided. It seems to be adding a :safe or :pure kwarg to that with a warning would be the right approach to let people work with interop return values in a safe way.

dnolen 2025-09-08T15:59:39.071169Z

that's not a accurate summary

dnolen 2025-09-08T16:00:05.711129Z

ClojureScript generates regular old JavaScript, Google Closure advanced compilation is whole program, it assumes it knows everything

dnolen 2025-09-08T16:00:26.213849Z

Anything it does not know about you must declare an extern

dnolen 2025-09-08T16:02:10.459199Z

the design of js->clj is flawed since it was designed to consume external JS stuff, but it does not take into account the extern problem

dnolen 2025-09-08T16:02:33.890959Z

all you need to do is make an extern for the CouchDB SPICE parameters

Pepijn de Vos 2025-09-08T16:08:41.251419Z

They are pretty much user generated content, so for now I've just switched to the json->clj implementation that converts them to clojure maps. Maybe this is a more obscure problem than I imagine since most structured APIs could indeed return externed data, and most actual json would better be parsed in clojurescript so the problem I'm having pretty much only occurs when you use a js library that parses arbitrary data

Pepijn de Vos 2025-09-01T11:47:15.333749Z

Could you explain what is going on here for someone not intimately familiar with cljs compilation?

Pepijn de Vos 2025-09-01T11:49:54.011479Z

I guess for now the workaround is "don's use single character keys in js objects"

Pepijn de Vos 2025-09-01T11:50:51.873319Z

Does cljs somehow encode type information as bit flag integer properties?

p-himik 2025-09-01T11:55:40.581519Z

I think so, yes.

dnolen 2025-09-01T15:54:19.299049Z

this problem isn't specific to implements? - you could encounter issues like this calling to Google Closure Library etc.

p-himik 2025-09-01T16:03:11.366609Z

Oh, right... Is it feasible to add all such fields to the externs? Or use whatever other way might exist to avoid renaming of such fields. I wouldn't expect for there to be many such fields (or field generation locations in the case of automatically generated fields).

dnolen 2025-09-01T16:09:12.122429Z

maybe - but this is just one of the hard facts about advanced compilation.

dnolen 2025-09-01T16:10:13.090579Z

I don't think people normally complain about this because you're not generally passing random JS objects in to ClojureScript standard library code.

dnolen 2025-09-01T16:11:05.546049Z

looking at the thread, I'm missing context as to why that's desirable in this case?

p-himik 2025-09-01T16:16:32.674159Z

#js {:m 10} is not too unlikely to not be a random JS object. I haven't encountered this issue myself yet, but just anecdotally - I noticed today that GET returns a bunch of JS objects with single-letter keys in them.

Pepijn de Vos 2025-09-01T18:44:03.918869Z

Yea the context is that I'm getting JSON from CouchDB that contains SPICE parameters that are often single letter fields, which I call js->clj on for further handling.

Pepijn de Vos 2025-09-01T18:45:09.885909Z

Why is js->clj checking for clj types? Maybe there are cases where you have clj inside js types, in which case maybe a feasible workaround is to add a pure? flag in case you're passing a pure JS type without any sequences or colls

p-himik 2025-09-01T18:46:23.446259Z

For simpler cases where you have plain JSON with zero potential of anything "special", it's trivial to implement your own js->clj that doesn't do any checks except for whether something is a plain JS object or array.

Pepijn de Vos 2025-09-01T18:46:33.177649Z

You'd imagine js->clj is the interface to random JS objects, and should be resilient to them in ways that random library code probably isn't

p-himik 2025-09-01T18:46:53.581689Z

And there are probably already CLJS-compatible libraries that read JSON strings and output CLJS data.

Pepijn de Vos 2025-09-01T18:48:00.633949Z

In my case the string to js parsing is happening in JS land (PouchDB)

p-himik 2025-09-01T19:25:13.406459Z

There might be a way to plug into it. But even if there isn't - as I said, it's trivial to implement on your own. Of course, it's not really a solution to the js->clj footgun.

(defn json->clj [data {:keys [keywordize?] :as opts}]
  (cond
    (nil? data)
    nil

    (js/Array.isArray data)
    (mapv #(json->clj % opts) data)

    (= (.-constructor data) js/Object)
    (into {}
          (map (fn [[k v]]
                 [(if keywordize? (keyword k) k)
                  (json->clj v opts)]))
          (js/Object.entries data))

    :else
    data))

Pepijn de Vos 2025-09-01T20:10:47.022079Z

I might honestly just use something like this yeah but fixing the footgun would be nice of course