Skip to content

Instantly share code, notes, and snippets.

@zenparsing
Last active July 1, 2019 15:05
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • 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 };
}
@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