Skip to content

Instantly share code, notes, and snippets.

@zenparsing
Last active July 1, 2019 15:05
Show Gist options
  • Save zenparsing/75381b450adb6792b892eeb15822b4d4 to your computer and use it in GitHub Desktop.
Save zenparsing/75381b450adb6792b892eeb15822b4d4 to your computer and use it in GitHub Desktop.
Symbol names

Symbol Literals

The form @identifierName is a symbol literal. Within any single module or script, two identitical symbol literals refer to the same symbol. Two identical symbol literals in separate modules or scripts refer to different symbols. A symbol literal may appear as a propery name or as a primary expression.

When a symbol literal appears as a primary expression, it is shorthand for this.@identifierName.

When a symbol literal appears as a property name, the corresponding symbol is used as the property key during evaluation.

import { AST, resolveScopes } from 'esparse';
export class Path {
constructor(node, parent = null, location = null) {
@node = node;
@location = location;
@parent = parent;
@scopeInfo = parent ? parent.@scopeInfo : null;
}
get node() {
return @node;
}
get parent() {
return @parent;
}
get parentNode() {
return @parent ? @parent.@node : null;
}
forEachChild(fn) {
if (!@node) {
return;
}
let paths = [];
AST.forEachChild(@node, (child, key, index) => {
let path = new Path(child, this, { key, index });
paths.push(path);
fn(path);
});
for (let path of paths) {
path.applyChanges();
}
}
applyChanges() {
let list = @changeList;
@changeList = [];
for (let record of list) {
if (!@node) {
break;
}
record.apply();
}
}
removeNode() {
@changeList.push(new ChangeRecord(this, 'replaceNode', [null]));
}
replaceNode(newNode) {
@changeList.push(new ChangeRecord(this, 'replaceNode', [newNode]));
}
insertNodesBefore(...nodes) {
@changeList.push(new ChangeRecord(this, 'insertNodesBefore', nodes));
}
insertNodesAfter(...nodes) {
@changeList.push(new ChangeRecord(this, 'insertNodesAfter', nodes));
}
visitChildren(visitor) {
this.forEachChild(childPath => childPath.visit(visitor));
}
visit(visitor) {
// TODO: applyChanges will not be run if called from top-level. Is this a problem?
if (!@node) {
return;
}
let method = visitor[@node.type];
if (typeof method === 'function') {
method.call(visitor, this);
}
if (!@node) {
return;
}
let { after } = visitor;
if (typeof after === 'function') {
after.call(visitor, this);
}
if (!method) {
this.visitChildren(visitor);
}
}
uniqueIdentifier(baseName, options = {}) {
let scopeInfo = @scopeInfo;
let ident = null;
for (let i = 0; true; ++i) {
let value = baseName;
if (i > 0) value += '_' + i;
if (!scopeInfo.names.has(value)) {
ident = value;
break;
}
}
scopeInfo.names.add(ident);
if (options.kind) {
@changeList.push(new ChangeRecord(this, 'insertDeclaration', [ident, options]));
}
return ident;
}
static fromParseResult(result) {
let path = new Path(result.ast);
path.@scopeInfo = getScopeInfo(result);
return path;
}
@getLocation(fn) {
if (!@parent) {
throw new Error('Node does not have a parent');
}
let { key, index } = @location;
let node = @node;
let parent = @parent.@node;
let valid = typeof index === 'number' ?
parent[key][index] === node :
parent[key] === node;
if (!valid) {
AST.forEachChild(parent, (child, k, i, stop) => {
if (child === node) {
valid = true;
@location = { key: (key = k), index: (index = i) };
return stop;
}
});
}
if (!valid) {
throw new Error('Unable to determine node location');
}
fn(parent, key, index);
}
@getBlock() {
let path = this;
while (path) {
switch (path.node.type) {
case 'Script':
case 'Module':
case 'Block':
case 'FunctionBody':
return path;
}
path = path.parent;
}
return null;
}
}
class ChangeRecord {
constructor(path, name, args) {
@path = path;
@name = name;
@args = args;
}
apply() {
switch (@name) {
case 'replaceNode': return @replaceNode(@args[0]);
case 'insertNodesAfter': return @insertNodesAfter(@args);
case 'insertNodesBefore': return @insertNodesBefore(@args);
case 'insertDeclaration': return @insertDeclaration(...@args);
default: throw new Error('Invalid change record type');
}
}
@replaceNode(newNode) {
if (@path.@parent) {
@path.@getLocation((parent, key, index) => {
if (typeof index !== 'number') {
parent[key] = newNode;
} else if (newNode) {
parent[key].splice(index, 1, newNode);
} else {
parent[key].splice(index, 1);
}
});
}
@path.@node = newNode;
}
@insertNodesAfter(nodes) {
@path.@getLocation((parent, key, index) => {
if (typeof index !== 'number') {
throw new Error('Node is not contained within a node list');
}
parent[key].splice(index + 1, 0, ...nodes);
});
}
@insertNodesBefore(nodes) {
@path.@getLocation((parent, key, index) => {
if (typeof index !== 'number') {
throw new Error('Node is not contained within a node list');
}
parent[key].splice(index, 0, ...nodes);
});
}
@insertDeclaration(ident, options) {
let { statements } = @path.@getBlock().node;
let i = 0;
while (i < statements.length) {
if (statements[i].type !== 'VariableDeclaration') break;
i += 1;
}
statements.splice(i, 0, {
type: 'VariableDeclaration',
kind: options.kind,
declarations: [{
type: 'VariableDeclarator',
pattern: { type: 'Identifier', value: ident },
initializer: options.initializer || null,
}],
});
}
}
function getScopeInfo(parseResult) {
let scopeTree = resolveScopes(parseResult.ast, { lineMap: parseResult.lineMap });
let names = new Set();
function visit(scope) {
scope.names.forEach((value, key) => names.add(key));
scope.free.forEach(ident => names.add(ident.value));
scope.children.forEach(visit);
}
visit(scopeTree);
return { names };
}
@zenparsing
Copy link
Author

We could also consider providing a way for users to declare that certain symbol literals are private symbols.

private @val; // This symbol literal is declared private

let obj = {
  @val: 1,
};

Reflect.ownKeys(obj); // []

@thysultan
Copy link

Would @val in the OP emulate Symbol() or Symbol.for()?

Possibly one of the two could describe private vs public @@val , @val. as long as as it is not "just the syntax version" i.e comes with Symbol.private().

@zenparsing
Copy link
Author

@thysultan

@val would emulate Symbol(), but any two @vals in the same module or script would refer to the same symbol.

Yes, you could definitely distinguish with syntax. I'm just not certain that the language needs syntactic sugar for "hard private". Hard privacy should be a really advanced feature, because it has non-obvious implications (e.g. doesn't work with Object.assign, doesn't work with Proxy). For an advanced feature, I think the WeakMap API is enough. (Although WM performance could be improved for that use case.)

@thysultan
Copy link

@zenparsing It would sound like Symbol.for if any two @val's are equal across different realms i.e is this.@val === this.@val, which it should since this.a === this.a.

Apart from property access if (objectFromOtherRealm.@val) {} can you use them as values i.e if (a === @val) {}.

If so then wouldn't that make them emulate more of Symbol.for than Symbol with the aspect that you can't reasonably craft your own with Symbol.for('val') given the exact interned name/id is hidden from the author.

@zenparsing
Copy link
Author

@thysultan Two identical symbol names are not equivalent accross realms - only within a given script or module. In some ways this is similar to Symbol.for.

@ljharb
Copy link

ljharb commented Jan 5, 2019

Would i be unable to use this syntax in two modules for a Symbol shared across both? I think shared Symbol protocols is a common case, and it seems unfortunate to privilege single-module usage with syntax without providing any benefits for that use case.

@Igmat
Copy link

Igmat commented Jan 8, 2019

@zenparsing, @ljharb, @thysultan I have arrived to something very close to this particular Symbol literal proposal and shred my thoughts in this issue and I love to hear your feedback about such approach.

The core differences comparing with this suggestion are:

  1. @x = 1 syntax is restricted, I found it too magical and confusing;
  2. Symbol declaration requires keyword as well as variable/constant declaration does;
  3. Symbol is lexically scoped and not file/module/script scoped, since second could be a little bit harder to reason about, especially if bundling and minifying comes in play. Other viable option IMO is closure scoped
  4. obj.@x syntax is restricted to preserve simpler mental model as in case of existing symbols usage, which works only with [] access syntax.

All of these differences are discussable, but it would be great if you, @zenparsing, clarify your choices here.

@Igmat
Copy link

Igmat commented Jan 9, 2019

Discussion for keyword-based shorthand syntax was moved to @jridgewell's fork.

@hax
Copy link

hax commented Jan 10, 2019

A small question: it seems @foo syntax contradicts @deco usage?

@mbrowne
Copy link

mbrowne commented Jul 1, 2019

@zenparsing Are you still interested in pursuing this idea? I know you're not a big fan of private class fields, but they're presumably going to stage 4, and I think this could still be a nice complement—with different syntax of course (given how much traction @ has gained for decorators). I would love to help support a shorthand symbol literals proposal, but I'm not sure there's much use unless someone on the committee is also interested.

CC @ljharb in case you or someone you know on the committee might be interested in working with the community on this.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment