optimise rest parameters in spread element position and allocate rest array at the earliest common ancestor of all references - fixes #1768

This commit is contained in:
Sebastian McKenzie
2015-06-17 01:57:14 +01:00
parent 574d47a571
commit b57a80ecae
22 changed files with 347 additions and 99 deletions

View File

@@ -9,41 +9,53 @@ var memberExpressionOptimisationVisitor = {
}
},
enter(node, parent, scope, state) {
var stop = () => {
state.canOptimise = false;
this.stop();
};
if (this.isArrowFunctionExpression()) return stop();
Function(node, parent, scope, state) {
// skip over functions as whatever `arguments` we reference inside will refer
// to the wrong function
if (this.isFunctionDeclaration() || this.isFunctionExpression()) {
state.noOptimise = true;
this.traverse(memberExpressionOptimisationVisitor, state);
state.noOptimise = false;
return this.skip();
state.noOptimise = true;
this.traverse(memberExpressionOptimisationVisitor, state);
state.noOptimise = false;
this.skip();
},
ReferencedIdentifier(node, parent, scope, state) {
// we can't guarantee the purity of arguments
if (node.name === "arguments") {
state.deopted = true;
}
// is this a referenced identifier and is it referencing the rest parameter?
if (!this.isReferencedIdentifier({ name: state.name })) return;
if (node.name !== state.name) return;
if (!state.noOptimise && t.isMemberExpression(parent) && parent.computed) {
// if we know that this member expression is referencing a number then we can safely
// optimise it
var prop = this.parentPath.get("property");
if (prop.isBaseType("number")) {
state.candidates.push(this);
return;
if (!state.noOptimise) {
if (this.parentPath.isMemberExpression({ computed: true, object: node })) {
// if we know that this member expression is referencing a number then we can safely
// optimise it
var prop = this.parentPath.get("property");
if (prop.isBaseType("number")) {
state.candidates.push(this);
return;
}
}
// optimise single spread args in calls
if (this.parentPath.isSpreadElement() && state.offset === 0) {
var call = this.parentPath.parentPath;
if (call.isCallExpression() && call.node.arguments.length === 1) {
return state.argumentsNode;
}
}
}
stop();
if (state.noOptimise) {
state.deopted = true;
} else {
state.references.push(this);
}
}
};
function optimizeMemberExpression(parent, offset) {
function optimiseMemberExpression(parent, offset) {
if (offset === 0) return;
var newExpr;
@@ -86,25 +98,38 @@ export var visitor = {
node.body.body.unshift(declar);
}
// check if rest is used in member expressions and optimise for those cases
// check and optimise for extremely common cases
var state = {
outerBinding: scope.getBindingIdentifier(rest.name),
canOptimise: true,
candidates: [],
method: node,
name: rest.name
references: [],
offset: node.params.length,
argumentsNode: argsId,
outerBinding: scope.getBindingIdentifier(rest.name),
// candidate member expressions we could optimise if there are no other references
candidates: [],
// local rest binding name
name: rest.name,
// whether any references to the rest parameter were made in a function
deopted: false
};
this.traverse(memberExpressionOptimisationVisitor, state);
// we only have shorthands and there's no other references
if (state.canOptimise && state.candidates.length) {
for (var candidate of (state.candidates: Array)) {
candidate.replaceWith(argsId);
optimizeMemberExpression(candidate.parent, node.params.length);
if (!state.deopted && !state.references.length) {
// we only have shorthands and there are no other references
if (state.candidates.length) {
for (var candidate of (state.candidates: Array)) {
candidate.replaceWith(argsId);
optimiseMemberExpression(candidate.parent, state.offset);
}
}
return;
} else {
state.references = state.references.concat(state.candidates);
}
//
@@ -144,6 +169,13 @@ export var visitor = {
KEY: key,
LEN: len
});
if (!state.deopted) {
// perform allocation at the lowest common denominator of all references
this.getEarliestCommonAncestorFrom(state.references).getStatementParent().insertBefore(loop);
return;
}
loop._blockHoist = node.params.length + 1;
node.body.body.unshift(loop);
}

View File

@@ -38,9 +38,9 @@ function convertNodePath(path) {
keysAlongPath.push(path.key);
if (parentNode !== path.container) {
var found = Object.keys(parentNode).some(containerKey => {
if (parentNode[containerKey] === path.container) {
keysAlongPath.push(containerKey);
var found = Object.keys(parentNode).some(listKey => {
if (parentNode[listKey] === path.container) {
keysAlongPath.push(listKey);
return true;
}
});

View File

@@ -26,19 +26,19 @@ export default class TraversalContext {
return false;
}
create(node, obj, key, containerKey) {
create(node, obj, key, listKey) {
var path = NodePath.get({
parentPath: this.parentPath,
parent: node,
container: obj,
key: key,
containerKey: containerKey
listKey
});
path.unshiftContext(this);
return path;
}
visitMultiple(container, parent, containerKey) {
visitMultiple(container, parent, listKey) {
// nothing to traverse!
if (container.length === 0) return false;
@@ -51,7 +51,7 @@ export default class TraversalContext {
for (let key = 0; key < container.length; key++) {
var self = container[key];
if (self && this.shouldVisit(self)) {
queue.push(this.create(parent, container, key, containerKey));
queue.push(this.create(parent, container, key, listKey));
}
}

View File

@@ -1,3 +1,6 @@
import * as t from "../../types";
import NodePath from "./index";
/**
* Description
*/
@@ -28,16 +31,118 @@ export function getStatementParent() {
* Description
*/
export function getAncestry() {
var ancestry = [];
export function getEarliestCommonAncestorFrom(paths: Array<NodePath>): NodePath {
return this.getDeepestCommonAncestorFrom(paths, function (deepest, i, ancestries) {
var earliest;
var keys = t.VISITOR_KEYS[deepest.type];
var path = this.parentPath;
while (path) {
ancestry.push(path.node);
path = path.parentPath;
for (var ancestry of (ancestries: Array)) {
var path = ancestry[i - 0];
if (!earliest) {
earliest = path;
continue;
}
// handle containers
if (path.listKey && earliest.listKey === path.listKey) {
// we're in the same container so check
if (path.key < earliest.key) {
earliest = path;
continue;
}
}
// handle keys
var earliestKeyIndex = keys.indexOf(earliest.parentKey);
var currentKeyIndex = keys.indexOf(path.parentKey);
if (earliestKeyIndex > currentKeyIndex) {
// key appears after
earliest = path;
}
}
return earliest;
});
}
/**
* Get the earliest path in the tree where the provided `paths` intersect.
*
* TODO: Possible optimisation target.
*/
export function getDeepestCommonAncestorFrom(paths: Array<NodePath>, filter?: Function): NodePath {
if (!paths.length) {
return this;
}
return ancestry;
if (paths.length === 1) {
return paths[0];
}
// minimum depth of the tree so we know the highest node
var minDepth = Infinity;
// last common ancestor
var lastCommonIndex, lastCommon;
// get the ancestors of the path, breaking when the parent exceeds ourselves
var ancestries = paths.map((path) => {
var ancestry = [];
do {
ancestry.unshift(path);
} while((path = path.parentPath) && path !== this);
// save min depth to avoid going too far in
if (ancestry.length < minDepth) {
minDepth = ancestry.length;
}
return ancestry;
});
// get the first ancestry so we have a seed to assess all other ancestries with
var first = ancestries[0];
// check ancestor equality
depthLoop: for (var i = 0; i < minDepth; i++) {
var shouldMatch = first[i];
for (var ancestry of (ancestries: Array)) {
if (ancestry[i] !== shouldMatch) {
// we've hit a snag
break depthLoop;
}
}
lastCommonIndex = i;
lastCommon = shouldMatch;
}
if (lastCommon) {
if (filter) {
return filter(lastCommon, lastCommonIndex, ancestries);
} else {
return lastCommon;
}
} else {
throw new Error("Couldn't find intersection");
}
}
/**
* Description
*/
export function getAncestry() {
var path = this;
var paths = [];
do {
paths.push(path);
} while(path = path.parentPath);
return paths;
}
/**

View File

@@ -148,7 +148,7 @@ export function resync() {
if (this.removed) return;
this._resyncParent();
this._resyncContainer();
this._resyncList();
this._resyncKey();
//this._resyncRemoved();
}
@@ -184,12 +184,12 @@ export function _resyncKey() {
this.key = null;
}
export function _resyncContainer() {
var containerKey = this.containerKey;
var parentPath = this.parentPath;
if (!containerKey || !parentPath) return;
export function _resyncList() {
var listKey = this.listKey;
var parentPath = this.parentPath;
if (!listKey || !parentPath) return;
var newContainer = parentPath.node[containerKey];
var newContainer = parentPath.node[listKey];
if (this.container === newContainer) return;
// container is out of sync. this is likely the result of it being reassigned
@@ -229,9 +229,10 @@ export function unshiftContext(context) {
* Description
*/
export function setup(parentPath, container, containerKey, key) {
this.containerKey = containerKey;
this.container = container;
export function setup(parentPath, container, listKey, key) {
this.listKey = listKey;
this.parentKey = listKey || key;
this.container = container;
this.parentPath = parentPath || this.parentPath;
this.setKey(key);

View File

@@ -71,7 +71,7 @@ export function getSibling(key) {
parentPath: this.parentPath,
parent: this.parent,
container: this.container,
containerKey: this.containerKey,
listKey: this.listKey,
key: key
});
}
@@ -101,7 +101,7 @@ export function _getKey(key) {
// requested a container so give them all the paths
return container.map((_, i) => {
return NodePath.get({
containerKey: key,
listKey: key,
parentPath: this,
parent: node,
container: container,

View File

@@ -20,11 +20,11 @@ export default class NodePath {
this.parentPath = null;
this.context = null;
this.container = null;
this.containerKey = null;
this.listKey = null;
this.parentKey = null;
this.key = null;
this.node = null;
this.scope = null;
this.breakOnScopePaths = null;
this.type = null;
this.typeAnnotation = null;
}
@@ -33,7 +33,7 @@ export default class NodePath {
* Description
*/
static get({ hub, parentPath, parent, container, containerKey, key }) {
static get({ hub, parentPath, parent, container, listKey, key }) {
if (!hub && parentPath) {
hub = parentPath.hub;
}
@@ -55,7 +55,7 @@ export default class NodePath {
paths.push(path);
}
path.setup(parentPath, container, containerKey, key);
path.setup(parentPath, container, listKey, key);
return path;
}

View File

@@ -260,10 +260,10 @@ export function _guessExecutionStatusRelativeTo(target) {
return "function";
}
var targetPaths = getAncestry(target);
var targetPaths = target.getAncestry();
//if (targetPaths.indexOf(this) >= 0) return "after";
var selfPaths = getAncestry(this);
var selfPaths = this.getAncestry();
// get ancestor where the branches intersect
var commonPath;
@@ -289,7 +289,7 @@ export function _guessExecutionStatusRelativeTo(target) {
}
// container list so let's see which one is after the other
if (targetRelationship.containerKey && targetRelationship.container === selfRelationship.container) {
if (targetRelationship.listKey && targetRelationship.container === selfRelationship.container) {
return targetRelationship.key > selfRelationship.key ? "before" : "after";
}
@@ -299,14 +299,6 @@ export function _guessExecutionStatusRelativeTo(target) {
return targetKeyPosition > selfKeyPosition ? "before" : "after";
}
function getAncestry(path) {
var paths = [];
do {
paths.push(path);
} while(path = path.parentPath);
return paths;
}
/**
* Resolve a "pointer" `NodePath` to it's absolute path.
*/

View File

@@ -53,7 +53,7 @@ export var post = [
// var NODE;
// remove an entire declaration if there are no declarators left
removeParent = removeParent || (self.containerKey === "declarations" && parent.isVariableDeclaration() && parent.node.declarations.length === 0);
removeParent = removeParent || (self.listKey === "declarations" && parent.isVariableDeclaration() && parent.node.declarations.length === 0);
// NODE;
// remove the entire expression statement if there's no expression

View File

@@ -42,7 +42,7 @@ export function _containerInsert(from, nodes) {
this.container.splice(to, 0, node);
if (this.context) {
var path = this.context.create(this.parent, this.container, to, this.containerKey);
var path = this.context.create(this.parent, this.container, to, this.listKey);
paths.push(path);
this.queueNode(path);
} else {
@@ -50,7 +50,7 @@ export function _containerInsert(from, nodes) {
parentPath: this,
parent: node,
container: this.container,
containerKey: this.containerKey,
listKey: this.listKey,
key: to
}));
}
@@ -154,7 +154,7 @@ export function _verifyNodeList(nodes) {
* Description
*/
export function unshiftContainer(containerKey, nodes) {
export function unshiftContainer(listKey, nodes) {
this._assertUnremoved();
nodes = this._verifyNodeList(nodes);
@@ -162,12 +162,12 @@ export function unshiftContainer(containerKey, nodes) {
// get the first path and insert our nodes before it, if it doesn't exist then it
// doesn't matter, our nodes will be inserted anyway
var container = this.node[containerKey];
var container = this.node[listKey];
var path = NodePath.get({
parentPath: this,
parent: this.node,
container: container,
containerKey,
listKey,
key: 0
});
@@ -178,7 +178,7 @@ export function unshiftContainer(containerKey, nodes) {
* Description
*/
export function pushContainer(containerKey, nodes) {
export function pushContainer(listKey, nodes) {
this._assertUnremoved();
nodes = this._verifyNodeList(nodes);
@@ -186,13 +186,13 @@ export function pushContainer(containerKey, nodes) {
// get an invisible path that represents the last node + 1 and replace it with our
// nodes, effectively inlining it
var container = this.node[containerKey];
var container = this.node[listKey];
var i = container.length;
var path = NodePath.get({
parentPath: this,
parent: this.node,
container: container,
containerKey,
listKey,
key: i
});

View File

@@ -390,7 +390,8 @@ export default class Scope {
var binding = scope.bindings[name];
console.log(" -", name, {
constant: binding.constant,
references: binding.references
references: binding.references,
kind: binding.kind
});
}
} while(scope = scope.parent);