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.
String-like test.
What does toValue mean/do?
It appears to be a hallucination slop.
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
it would also bloat the generated code too probably
and possibly an effect on performance
And performance could be impacted.
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.
properties on strings?
like this?
> const obj = new String("dude")
undefined
> obj["hello"] = true
true
> obj
[String: 'dude'] { hello: true }Yes like that. 🙂
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.
but what would that buy us over KeywordThing?
Yeah good question. It has less features.
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?yes it does
Maps just work with object identity
Oh ok so that's the same reason KeyThing works.
> new Map([[new String("dude"), 1], [new String("dude"), 2]])
Map(2) { [String: 'dude'] => 1, [String: 'dude'] => 2 }yes
Wait no, that's different to KeyThing.
it's the same, but KeyThing has a registry so created Keythings are interned
Oh ok, what I mean by "String("dude") already works with Map().set()" was String("dude") behaving just like "dude".
except that it doesn't?
> new Map([[new String("dude"), 1], ["dude", 2]])
Map(2) { [String: 'dude'] => 1, 'dude' => 2 }Yes exactly.
You said: > yes it does So I got confused.
Lol facepalm
I replied to this: > Or maybe String("dude") already works with Map().set() as a unique key?
Yes when I said "unique key" I should have said the inverse of that.
:)
anyway, I think objects squashing keys to strings it the big issue
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
squint could swap to js/Map by default but this would break a plethora of other stuff :)
Yeah it's nice that it's just a plain object.
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?
And that's why you say it's lossy.
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 existswell, since this doesn't break any interop, that would still be ok probably
Keywords are just strings in squint
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.
It would make porting code from cljs to squint a simpler task.
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.
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
Or maybe it was a GitHub issue.I’ll check when I’m at the kbd
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
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.
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.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
}
> 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.
Oh wait yes you've pointed out a deeper problem.
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();
a global registry would work but this will cause memory leaks
this is why clojure uses a weak map for these things
So the registry could be a WeakMap hypothetically.
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.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}?
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. 🤔
yes
Also name was a problem. Again I could alias that to str. laughcry
we can fix name for namespaced keywords, if you need the name part of :foo/bar I guess.
but not sure if you hit that problem
I don't even use namespaces in keywords ever lol.
ok then it's just str indeed
Would it make sense to have something like squint-shims.mjs that people can include in their build? 🤔
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
so just handle your case in the way that makes sense for your program
Yep understood, thanks!
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);Ah interesting.
I think you still can't distuingish between symbols/kwds or strings here for interop so it defeats the purpose
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
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!");This is mostly just me being curious so feel free to ignore.
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 printedalso doable with images I think? though I believe the suffix is url or something
I've never tried it with squint but I've done something like that in a Vite project before
There is also import with which squint now supports
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/import/with
import data from "" with { type: "json" }; 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?