squint

Chris McCormick 2025-09-22T06:32:35.648239Z

I am sure this has been discussed a bunch, but what's the status and/or plan with keywords? I naively assume it would be possible to make something more keyword-like which still looks somewhat like a string in JS land, so that we could have name and keyword? work properly. Or is this a pipe dream? I also assume a mapping directly to Symbol() was considered already.

Chris McCormick 2025-09-22T06:50:24.013709Z

String-like test.

borkdude 2025-09-23T07:58:01.581669Z

What does toValue mean/do?

Chris McCormick 2025-09-23T08:26:00.251059Z

It appears to be a hallucination slop.

borkdude 2025-09-23T09:41:37.345839Z

I guess we could go with this kind of Keyword thing, but since JS objects are lossy with respect to keys I wonder how much it would buy us

borkdude 2025-09-23T09:41:59.946789Z

it would also bloat the generated code too probably

borkdude 2025-09-23T09:42:09.339869Z

and possibly an effect on performance

👍 1
Chris McCormick 2025-09-23T09:42:10.567699Z

And performance could be impacted.

Chris McCormick 2025-09-23T09:43:46.768609Z

Not a thing to do lightly. If I get some more time to hack on it properly I'll try to generate some test results to get an idea of impact and also see if it actually solves the issue of "easier to port from clojurescript because keyword? and name are meaningful". If only JS supported properties on strings that would be much simpler.

borkdude 2025-09-23T09:45:02.300939Z

properties on strings?

borkdude 2025-09-23T09:46:10.322849Z

like this?

> const obj = new String("dude")
undefined
> obj["hello"] = true
true
> obj
[String: 'dude'] { hello: true }

Chris McCormick 2025-09-23T09:49:54.462799Z

Yes like that. 🙂

Chris McCormick 2025-09-23T09:51:14.574419Z

If you could do that then you could do ._keyword = true and if a string has that set then keyword? and name could do something different to that string. Again that requires emit-keyword to decorate the string with an object though.

borkdude 2025-09-23T09:55:44.926229Z

but what would that buy us over KeywordThing?

Chris McCormick 2025-09-23T09:57:50.731049Z

Yeah good question. It has less features.

Chris McCormick 2025-09-23T09:59:28.863199Z

I guess I mean if you could do:

const x = "hello";
x._keyword = true;
That would mean you could keep things nearly exactly as they are, have Map().set() work, and also know that this thing is a keyword. Or maybe String("dude") already works with Map().set() as a unique key?

borkdude 2025-09-23T09:59:47.936729Z

yes it does

borkdude 2025-09-23T09:59:57.726279Z

Maps just work with object identity

Chris McCormick 2025-09-23T10:00:19.953019Z

Oh ok so that's the same reason KeyThing works.

borkdude 2025-09-23T10:00:43.803389Z

> new Map([[new String("dude"), 1], [new String("dude"), 2]])
Map(2) { [String: 'dude'] => 1, [String: 'dude'] => 2 }

borkdude 2025-09-23T10:00:45.611219Z

yes

Chris McCormick 2025-09-23T10:01:21.468339Z

Wait no, that's different to KeyThing.

borkdude 2025-09-23T10:01:39.095959Z

it's the same, but KeyThing has a registry so created Keythings are interned

Chris McCormick 2025-09-23T10:02:38.129999Z

Oh ok, what I mean by "String("dude") already works with Map().set()" was String("dude") behaving just like "dude".

borkdude 2025-09-23T10:03:12.982779Z

except that it doesn't?

> new Map([[new String("dude"), 1], ["dude", 2]])
Map(2) { [String: 'dude'] => 1, 'dude' => 2 }

Chris McCormick 2025-09-23T10:03:21.583389Z

Yes exactly.

Chris McCormick 2025-09-23T10:03:38.622409Z

You said: > yes it does So I got confused.

Chris McCormick 2025-09-23T10:04:01.358739Z

Lol facepalm

borkdude 2025-09-23T10:04:06.461949Z

I replied to this: > Or maybe String("dude") already works with Map().set() as a unique key?

Chris McCormick 2025-09-23T10:04:19.909929Z

Yes when I said "unique key" I should have said the inverse of that.

borkdude 2025-09-23T10:04:29.961099Z

:)

borkdude 2025-09-23T10:04:58.266119Z

anyway, I think objects squashing keys to strings it the big issue

borkdude 2025-09-23T10:05:33.495719Z

and you can now add another thing called Symbols to it, but those can't replace the normal keys, they are invisible stuff that doesn't work with interop

borkdude 2025-09-23T10:09:16.667829Z

squint could swap to js/Map by default but this would break a plethora of other stuff :)

Chris McCormick 2025-09-23T10:10:52.126639Z

Yeah it's nice that it's just a plain object.

Chris McCormick 2025-09-23T10:11:28.060429Z

Ah I get it, so if keyword was a more complicated object then when you set the key on an object it would smash it into a string?

Chris McCormick 2025-09-23T10:11:36.917719Z

And that's why you say it's lossy.

borkdude 2025-09-23T10:13:35.667309Z

yes. so like this:

const obj = {}
obj[new KeyThing("dude")] = 10
console.log(obj["dude"])
console.log(obj.dude)
you would think that there still is a keyword named "dude" in memory but it can be garbage collected now since it no longer exists

👍 1
borkdude 2025-09-23T10:17:31.599049Z

well, since this doesn't break any interop, that would still be ok probably

borkdude 2025-09-22T07:01:10.238279Z

Keywords are just strings in squint

Chris McCormick 2025-09-22T07:04:48.486099Z

Yes. That means you can't have name or keyword?. What I'm asking is if they should be more than just strings to bring Squint closer to ClojureScript.

Chris McCormick 2025-09-22T07:05:05.934049Z

It would make porting code from cljs to squint a simpler task.

Chris McCormick 2025-09-22T07:06:44.083289Z

If you made a string-like thing (as in my demo above) then you could still have keywords behave like strings in squint, but also have a proper answer to keyword? and be able to correctly do name on keywords with slashes in them as well as strings.

borkdude 2025-09-22T07:11:58.219859Z

I’ll take a look but I’ve tried something like this recently and didn’t work because some edge cases. Search for keyword to find that thread in this channel

👍 1
🙏 1
borkdude 2025-09-22T07:15:22.593969Z

Or maybe it was a GitHub issue.I’ll check when I’m at the kbd

Chris McCormick 2025-09-22T07:39:55.701749Z

I searched the history of this Slack and also the GitHub issues. Hopefully I didn't miss anything: https://github.com/squint-cljs/squint/issues/711

Chris McCormick 2025-09-22T07:56:43.514219Z

https://clojurians.slack.com/archives/C03U8L2NXNC/p1746436057975639?thread_ts=1746381909.586379&cid=C03U8L2NXNC The solution I propose wouldn't fix this Map key issue you mentioned.

borkdude 2025-09-22T08:11:49.127569Z

How about this problem?

const m = new Map([[new Keyword("dude1"), 1], [new Keyword("dude1"), 2]])
console.log('Test 9: identity behavior')
console.log(m.get(new Keyword("dude1"))) // undefined
I don't think there is a good solution for this.

😬 1
Chris McCormick 2025-09-22T08:21:00.321249Z

I mean this is similar to cljs behaviour:

(def m (js/Map.))
(.set m "Hello" 1)
(.set m :Hello 2)
(js/console.log m)
Yields:
Map(2) {
  'Hello' => 1,
  {
    cc: null,
    name: 'Hello',
    ga: 'Hello',
    se: null,
    G: 2153775105,
    O: 4096
  } => 2
}

Chris McCormick 2025-09-22T08:21:56.539919Z

> The most significant difference with CLJS is that squint uses only built-in JS data structures. I would argue that a new js Keyword type still fulfils this promise.

Chris McCormick 2025-09-22T08:22:23.560739Z

Oh wait yes you've pointed out a deeper problem.

Chris McCormick 2025-09-22T08:37:18.526789Z

LLM suggests something like this, which I saw you discussed before:

const keywordRegistry = new Map();                                                                                                                                            
                                                                                                                                                                              
class InternedKeyword {                                                                                                                                                       
    constructor(value) {                                                                                                                                                      
        this.value = value;                                                                                                                                                   
    }                                                                                                                                                                         
    toString() {                                                                                                                                                              
        return this.value;                                                                                                                                                    
    }                                                                                                                                                                         
                                                                                                                                                                              
    static for(value) {                                                                                                                                                       
        if (!keywordRegistry.has(value)) {                                                                                                                                    
            keywordRegistry.set(value, new InternedKeyword(value));                                                                                                           
        }                                                                                                                                                                     
        return keywordRegistry.get(value);                                                                                                                                    
    }                                                                                                                                                                         
}                                                                                                                                                                             
                                                                                                                                                                              
const m3 = new Map();                                                                                                                                                         
const ik1 = InternedKeyword.for("foo");                                                                                                                                       
const ik2 = InternedKeyword.for("foo");                                                                                                                                       
You could get around the squint issue of separate files with something like this:
globalThis._squintKeywordRegistry |= new Map();

borkdude 2025-09-22T08:50:58.314809Z

a global registry would work but this will cause memory leaks

borkdude 2025-09-22T08:51:25.481879Z

this is why clojure uses a weak map for these things

Chris McCormick 2025-09-22T08:53:38.001149Z

So the registry could be a WeakMap hypothetically.

borkdude 2025-09-22T09:01:14.249879Z

One other problem may be that you still can't do stuff like:

{:foo 1 "foo" 2}
in Squint since JS objects always stringifies their keys. This was the main reason to go with just strings for keywords, since in JS, object keys are always strings. If we chose Symbols for keywords, those would work in Objects, but then interop would break. And squint is designed for good interop.

👍 1
borkdude 2025-09-22T09:02:56.423719Z

For your use case, what would be the main benefit of having distinct keywords? My guess would be keywords as values, like {:option :my-keyword}?

Chris McCormick 2025-09-22T09:03:37.000579Z

It's not that, it's that I was porting some cljs code over and it had keyword? in it. I guess I could alias that to string? or something. 🤔

borkdude 2025-09-22T09:03:46.098989Z

yes

Chris McCormick 2025-09-22T09:04:13.765279Z

Also name was a problem. Again I could alias that to str. laughcry

borkdude 2025-09-22T09:04:29.990259Z

we can fix name for namespaced keywords, if you need the name part of :foo/bar I guess.

borkdude 2025-09-22T09:04:37.248419Z

but not sure if you hit that problem

Chris McCormick 2025-09-22T09:04:41.540299Z

I don't even use namespaces in keywords ever lol.

borkdude 2025-09-22T09:04:50.603249Z

ok then it's just str indeed

Chris McCormick 2025-09-22T09:05:33.460349Z

Would it make sense to have something like squint-shims.mjs that people can include in their build? 🤔

borkdude 2025-09-22T09:06:54.857859Z

I don;'t know if there is a general shim that would work well in all cases, this is why I haven't included such a shim

borkdude 2025-09-22T09:07:10.416909Z

so just handle your case in the way that makes sense for your program

Chris McCormick 2025-09-22T09:07:25.182099Z

Yep understood, thanks!

borkdude 2025-09-22T09:50:26.615809Z

I've thought about this before, of using a proxy object to support both Symbol and string object keys but it complicates stuff and also affects performance.

const target = {
};

const handler = {
  get(target, prop, receiver) {
    if (typeof prop !== 'symbol') prop = Symbol.for(prop);
    return target[prop];
  },
  set(target, prop, value) {
    if (typeof prop !== 'symbol' && prop !== 'toString') prop = Symbol.for(prop);
    target[prop] = value;
  }
};

const proxy = new Proxy(target, handler);
proxy.toString = () => '{}';

proxy[Symbol.for("dude")] = () => "kwd dude;"
proxy["dude"] = () => "string dude";

console.log(proxy.dude()); // world

console.log(target+'');

console.log(target);

Chris McCormick 2025-09-22T09:52:45.982929Z

Ah interesting.

borkdude 2025-09-22T09:54:58.573839Z

I think you still can't distuingish between symbols/kwds or strings here for interop so it defeats the purpose

Chris McCormick 2025-09-23T05:07:20.325889Z

Sorry to bang on about this. Here's an implementation with the following properties: Identity: new KeyThing("hello") === new KeyThing("hello")true Map Override: Second m.set(KeyThing("hello"), 54) overrides the first value String Equality: KeyThing("hello") == "hello"true Clean Output: keyThing.toString() returns "hello" Memory Management: Cache uses WeakRef for automatic cleanup when objects are garbage collected

Chris McCormick 2025-09-23T05:07:43.785099Z

console.log("=== KEYTHING TEST SUITE ===\n");

// Identity and Basic Functionality
console.log("=== Identity and Equality Tests ===");
const key1 = new KeyThing("hello");
console.log("key1 = new KeyThing('hello');");
const key2 = new KeyThing("hello");
console.log("key2 = new KeyThing('hello');");

console.log("Are they the same object?", key1 === key2);
console.log("key1.toValue():", key1.toValue());
console.log("key1.toString():", key1.toString());
console.log("String(key1):", String(key1));

// String Equality Tests
console.log("\n=== String Equality Tests ===");
console.log("key1 == 'hello':", key1 == "hello");
console.log("key1 === 'hello':", key1 === "hello");
console.log("key1 == 'world':", key1 == "world");
console.log("key1 == key2:", key1 == key2);
console.log("key1.value === 'hello':", key1.value === "hello");
console.log("key1.toString() === 'hello':", key1.toString() === "hello");

// Map Functionality
console.log("\n=== Map Functionality Tests ===");
const m = new Map();

m.set(key1, 42);
console.log("After setting key1 to 42:", m.get(key1));

m.set(key2, 54);  // Should override since key1 === key2
console.log("After setting key2 to 54:", m.get(key1));
console.log("Map size:", m.size);

// Test with different values
const key3 = new KeyThing("world");
m.set(key3, 100);
console.log("Map size after adding 'world':", m.size);

console.log("All entries:");
for (const [k, v] of m.entries()) {
  console.log(`  "${k.toValue()}" => ${v}`);
}

// Retrieval with new instances
console.log("\n=== Retrieval with New Instances ===");
console.log("m.get(new KeyThing('hello')):", m.get(new KeyThing("hello")));
console.log("m.get(new KeyThing('world')):", m.get(new KeyThing("world")));
console.log("m.get(new KeyThing('nonexistent')):", m.get(new KeyThing("nonexistent")));

// Identity verification
console.log("\n=== New Instance Identity Tests ===");
const key4 = new KeyThing("hello");
console.log("key1 === new KeyThing('hello'):", key1 === key4);
console.log("All 'hello' instances are identical:", key1 === key2 && key2 === key4);

// Memory Management
console.log("\n=== Memory Management Tests ===");
console.log("Cache size:", KeyThing.getCacheSize());

function createTempKeys() {
  const temp1 = new KeyThing("temp1");
  const temp2 = new KeyThing("temp2");
  console.log("Created temp keys, cache size:", KeyThing.getCacheSize());
}

createTempKeys();
console.log("After function exit, cache size:", KeyThing.getCacheSize());

// Cache management
console.log("\n=== Cache Management ===");
console.log("Cache size before clear:", KeyThing.getCacheSize());
KeyThing.clearCache();
console.log("Cache size after clear:", KeyThing.getCacheSize());

// Verify existing objects still work after cache clear
console.log("key1.toValue() after cache clear:", key1.toValue());
console.log("m.get(key1) after cache clear:", m.get(key1));

// New instance after clear creates new object
const newHello = new KeyThing("hello");
console.log("New 'hello' after cache clear === old key1:", newHello === key1);

// Edge cases
console.log("\n=== Edge Cases ===");
const numKey = new KeyThing("123");
const strKey = new KeyThing("123");
console.log("String '123' instances are identical:", numKey === strKey);

const empty1 = new KeyThing("");
const empty2 = new KeyThing("");
console.log("Empty string instances are identical:", empty1 === empty2);
console.log("Empty string comparison with '':", empty1 == "");

const special = new KeyThing("hello\nworld!");
console.log("Special chars equals test:", special == "hello\nworld!");

Chris McCormick 2025-09-23T05:46:28.405199Z

This is mostly just me being curious so feel free to ignore.

Chris McCormick 2025-09-22T13:39:22.675959Z

TIL, you can do this in Squint + Vite:

(ns index
  (:require
    ["../README.md?raw" :as readme]))

(js/console.log (aget readme "default"))
; contents of README.md inlined into the build and printed

liebs 2025-09-22T14:55:58.593339Z

also doable with images I think? though I believe the suffix is url or something

liebs 2025-09-22T14:56:18.911059Z

I've never tried it with squint but I've done something like that in a Vite project before

borkdude 2025-09-22T15:03:15.025909Z

There is also import with which squint now supports

borkdude 2025-09-22T15:03:56.242299Z

import data from "" with { type: "json" };

👍 1
👀 2
Chris McCormick 2025-09-23T01:03:39.266949Z

I see from the code it's used like this:

["./test.json$default" :as json :with {:type :json}]
Should I add a section to the README like "Asset imports" documenting this?

👍 1