Symbol registration and weakmap keying

I was sitting here thinking about a couple aspects of the symbol/weakmap future and I realize that I want to lay out some of the incentives I see being created by these changes.

First things first: according to recent benchmarks, symbol comparison is still twice as fast as string comparison in a microbenchmarks. I have no reason to think this is an abberration, as it fits with my hypotheses about the fundamental limitations of JS perf. To be clear, the comparisons should always be between interned strings so falling back to character-by-character comparison should never be necessary, but this does not invalidate the result.

That strongly suggests to me that in the future the fastest code for handling trees, especially syntax trees, will look like a bit like this:

let types = {
  IfExpression: Symbol('IfExpression')
}

let node = {
  type: types.IfExpression,
  value,
}

What's interesting about that it is in some sense fundamentally equivalent to this:

class Node {
  value;
}

That's because the presence of a symbol-type type, when conjoined with the presence of weak maps, turns a symbol typed object into something which can have one (or arbitrarily many) prototypes associated with it like this:

let prototypes = new WeakMap();

// set up some prototypes

prototypes.get(node.type);

I think this is in ways the most spectacular single development in the JS language, ever. Just by existing this change completely unifies the object and class structures, and introduces new class-like patterning under which multiple inheritance and the diamond dependency problem are solved for.

Here's what I want to ask about though: if you don't wish to construct a de-facto class, you have two basic choices. Either you always create a new symbol every time (likely useless as it will then never === anything) or you can register the symbol somewhere in the global string namespace.

So I'm wondering: does the committee have any thoughts about the perverse incentives that might be created to register symbols in the global namespace merely to avoid turning symbol-typed plain objects into classes when that behavior is not desirable?

Conversely: if you do want to use the unified object/class model, you are free to register your classes absolutely anywhere except on the official Symbol.for registry. The official lookup method is the only one you will not ever be able to use.

Another reason people are going to want to do this: it solves (could potentially solve) the JS problem of not being able to extend a base class into multiple derivations without wrecking the monomorphism of accessing prototype methods in code which touches more than one derived class.

I think you would achieve this property when a prototype map essentially just stored a single method: something like prototypeMethodMaps.get('toString').get(node.type).call(node)

Well what do you want if not a de-facto class (with fancier or non-prototype inheritance)?

Not sure what you mean by that, or why it has to be a global namespace. What/who do you want to share the symbol with? And why do you say it can't be registered in the Symbol.for registry?

What I mean by a "global string namespace" is the namespace of strings that are keys to Symbol.for and Symbol.keyFor.

I just did a little more reading and I see on the weakmap key proposals that is considered an item of open debate whether registered symbols should make valid weak map keys. If registered symbols cannot be used then realm-lifespan classes would need to have an alternate, userland symbol registry.

That proposal is completed. Since es2023 the spec has been updated to reflect that registered symbols specifically are not considered valid WeakMap keys:

This is because they have the same GC semantics as strings.

I am aware of the reasoning behind the change, my comment is how it effectively completely changes the meaning of symbol registration. Now only registered symbols are like strings, and there are many situations where using a registered symbol would be dangerous and bad and there are other situations where using an unregistered symbol will be dangerous/bad/wrong.

No tool in existence can help avoid this kind of basic logic error either

So far tools can't help, but we don't even have language to talk about it ourselves either to keep track of the possible problems. I think I'll use "live symbol" and "dead symbol" when I talk about it.

My advice will probably (eventually) be: don't use dead symbols. Now that I have the words I can say why: If you are using weakmaps to track symbol keys, your code will crash if it receives a dead symbol when it is expecting a live one

See also my issue against TypeScript requesting to formally distinguish between the newly incompatible live and dead symbol types: Symbol = LiveSymbol | DeadSymbol · Issue #59574 · microsoft/TypeScript · GitHub

An empty array throws when doing .reduce without an initial value. Code needs to guard against cases like this.

If code intends to put 3rd party symbols in a WeakMap it can check if they are registered symbols and use a regular Map instead for those keys.

JS goes to almost any costs to not stop its thread of execution when there is any course by which it might continue logically, case in point:

[1].at(1/0); // or
''['3'];

Nowhere is this property more prevalent or important than with primitive types and their direct interactions with each other. From thence arises my shock that the position of the committee would be to let it crash and burn when there is nothing preventing the logic from being executed as written.

(For the record I was in the camp that wanted to see arr.at(NaN) crash and burn, or at least return undefined or do any of the many things that would have been less insane than returning the 0th element)

While JS has historically done that and can't be changed now. The recent discussions in the committee have been that newer APIs should, generally speaking, stop coercing things:

https://github.com/search?q=repo%3Atc39%2Fagendas%20"stop%20coercing%20things"&type=code