import assert from "assert";
import * as leap from "./leap";
import * as meta from "./meta";
import * as util from "./util";
let hasOwn = Object.prototype.hasOwnProperty;
function Emitter(contextId) {
assert.ok(this instanceof Emitter);
util.getTypes().assertIdentifier(contextId);
this.nextTempId = 0;
this.contextId = contextId;
this.listing = [];
this.marked = [true];
this.insertedLocs = new Set();
this.finalLoc = this.loc();
this.tryEntries = [];
this.leapManager = new leap.LeapManager(this);
}
let Ep = Emitter.prototype;
exports.Emitter = Emitter;
const PENDING_LOCATION = Number.MAX_VALUE;
Ep.loc = function() {
const l = util.getTypes().numericLiteral(PENDING_LOCATION)
this.insertedLocs.add(l);
return l;
}
Ep.getInsertedLocs = function() {
return this.insertedLocs;
}
Ep.getContextId = function() {
return util.getTypes().clone(this.contextId);
}
Ep.mark = function(loc) {
util.getTypes().assertLiteral(loc);
let index = this.listing.length;
if (loc.value === PENDING_LOCATION) {
loc.value = index;
} else {
assert.strictEqual(loc.value, index);
}
this.marked[index] = true;
return loc;
};
Ep.emit = function(node) {
const t = util.getTypes();
if (t.isExpression(node)) {
node = t.expressionStatement(node);
}
t.assertStatement(node);
this.listing.push(node);
};
Ep.emitAssign = function(lhs, rhs) {
this.emit(this.assign(lhs, rhs));
return lhs;
};
Ep.assign = function(lhs, rhs) {
const t = util.getTypes();
return t.expressionStatement(
t.assignmentExpression("=", t.cloneDeep(lhs), rhs));
};
Ep.contextProperty = function(name, computed) {
const t = util.getTypes();
return t.memberExpression(
this.getContextId(),
computed ? t.stringLiteral(name) : t.identifier(name),
!!computed
);
};
Ep.stop = function(rval) {
if (rval) {
this.setReturnValue(rval);
}
this.jump(this.finalLoc);
};
Ep.setReturnValue = function(valuePath) {
util.getTypes().assertExpression(valuePath.value);
this.emitAssign(
this.contextProperty("rval"),
this.explodeExpression(valuePath)
);
};
Ep.clearPendingException = function(tryLoc, assignee) {
const t = util.getTypes();
t.assertLiteral(tryLoc);
let catchCall = t.callExpression(
this.contextProperty("catch", true),
[t.clone(tryLoc)]
);
if (assignee) {
this.emitAssign(assignee, catchCall);
} else {
this.emit(catchCall);
}
};
Ep.jump = function(toLoc) {
this.emitAssign(this.contextProperty("next"), toLoc);
this.emit(util.getTypes().breakStatement());
};
Ep.jumpIf = function(test, toLoc) {
const t = util.getTypes();
t.assertExpression(test);
t.assertLiteral(toLoc);
this.emit(t.ifStatement(
test,
t.blockStatement([
this.assign(this.contextProperty("next"), toLoc),
t.breakStatement()
])
));
};
Ep.jumpIfNot = function(test, toLoc) {
const t = util.getTypes();
t.assertExpression(test);
t.assertLiteral(toLoc);
let negatedTest;
if (t.isUnaryExpression(test) &&
test.operator === "!") {
negatedTest = test.argument;
} else {
negatedTest = t.unaryExpression("!", test);
}
this.emit(t.ifStatement(
negatedTest,
t.blockStatement([
this.assign(this.contextProperty("next"), toLoc),
t.breakStatement()
])
));
};
Ep.makeTempVar = function() {
return this.contextProperty("t" + this.nextTempId++);
};
Ep.getContextFunction = function(id) {
const t = util.getTypes();
return t.functionExpression(
id || null,
[this.getContextId()],
t.blockStatement([this.getDispatchLoop()]),
false, false );
};
Ep.getDispatchLoop = function() {
const self = this;
const t = util.getTypes();
let cases = [];
let current;
let alreadyEnded = false;
self.listing.forEach(function(stmt, i) {
if (self.marked.hasOwnProperty(i)) {
cases.push(t.switchCase(
t.numericLiteral(i),
current = []));
alreadyEnded = false;
}
if (!alreadyEnded) {
current.push(stmt);
if (t.isCompletionStatement(stmt))
alreadyEnded = true;
}
});
this.finalLoc.value = this.listing.length;
cases.push(
t.switchCase(this.finalLoc, [
]),
t.switchCase(t.stringLiteral("end"), [
t.returnStatement(
t.callExpression(this.contextProperty("stop"), [])
)
])
);
return t.whileStatement(
t.numericLiteral(1),
t.switchStatement(
t.assignmentExpression(
"=",
this.contextProperty("prev"),
this.contextProperty("next")
),
cases
)
);
};
Ep.getTryLocsList = function() {
if (this.tryEntries.length === 0) {
return null;
}
const t = util.getTypes();
let lastLocValue = 0;
return t.arrayExpression(
this.tryEntries.map(function(tryEntry) {
let thisLocValue = tryEntry.firstLoc.value;
assert.ok(thisLocValue >= lastLocValue, "try entries out of order");
lastLocValue = thisLocValue;
let ce = tryEntry.catchEntry;
let fe = tryEntry.finallyEntry;
let locs = [
tryEntry.firstLoc,
ce ? ce.firstLoc : null
];
if (fe) {
locs[2] = fe.firstLoc;
locs[3] = fe.afterLoc;
}
return t.arrayExpression(locs.map(loc => loc && t.clone(loc)));
})
);
};
Ep.explode = function(path, ignoreResult) {
const t = util.getTypes();
let node = path.node;
let self = this;
t.assertNode(node);
if (t.isDeclaration(node))
throw getDeclError(node);
if (t.isStatement(node))
return self.explodeStatement(path);
if (t.isExpression(node))
return self.explodeExpression(path, ignoreResult);
switch (node.type) {
case "Program":
return path.get("body").map(
self.explodeStatement,
self
);
case "VariableDeclarator":
throw getDeclError(node);
case "Property":
case "SwitchCase":
case "CatchClause":
throw new Error(
node.type + " nodes should be handled by their parents");
default:
throw new Error(
"unknown Node of type " +
JSON.stringify(node.type));
}
};
function getDeclError(node) {
return new Error(
"all declarations should have been transformed into " +
"assignments before the Exploder began its work: " +
JSON.stringify(node));
}
Ep.explodeStatement = function(path, labelId) {
const t = util.getTypes();
let stmt = path.node;
let self = this;
let before, after, head;
t.assertStatement(stmt);
if (labelId) {
t.assertIdentifier(labelId);
} else {
labelId = null;
}
if (t.isBlockStatement(stmt)) {
path.get("body").forEach(function (path) {
self.explodeStatement(path);
});
return;
}
if (!meta.containsLeap(stmt)) {
self.emit(stmt);
return;
}
switch (stmt.type) {
case "ExpressionStatement":
self.explodeExpression(path.get("expression"), true);
break;
case "LabeledStatement":
after = this.loc();
self.leapManager.withEntry(
new leap.LabeledEntry(after, stmt.label),
function() {
self.explodeStatement(path.get("body"), stmt.label);
}
);
self.mark(after);
break;
case "WhileStatement":
before = this.loc();
after = this.loc();
self.mark(before);
self.jumpIfNot(self.explodeExpression(path.get("test")), after);
self.leapManager.withEntry(
new leap.LoopEntry(after, before, labelId),
function() { self.explodeStatement(path.get("body")); }
);
self.jump(before);
self.mark(after);
break;
case "DoWhileStatement":
let first = this.loc();
let test = this.loc();
after = this.loc();
self.mark(first);
self.leapManager.withEntry(
new leap.LoopEntry(after, test, labelId),
function() { self.explode(path.get("body")); }
);
self.mark(test);
self.jumpIf(self.explodeExpression(path.get("test")), first);
self.mark(after);
break;
case "ForStatement":
head = this.loc();
let update = this.loc();
after = this.loc();
if (stmt.init) {
self.explode(path.get("init"), true);
}
self.mark(head);
if (stmt.test) {
self.jumpIfNot(self.explodeExpression(path.get("test")), after);
} else {
}
self.leapManager.withEntry(
new leap.LoopEntry(after, update, labelId),
function() { self.explodeStatement(path.get("body")); }
);
self.mark(update);
if (stmt.update) {
self.explode(path.get("update"), true);
}
self.jump(head);
self.mark(after);
break;
case "TypeCastExpression":
return self.explodeExpression(path.get("expression"));
case "ForInStatement":
head = this.loc();
after = this.loc();
let keyIterNextFn = self.makeTempVar();
self.emitAssign(
keyIterNextFn,
t.callExpression(
util.runtimeProperty("keys"),
[self.explodeExpression(path.get("right"))]
)
);
self.mark(head);
let keyInfoTmpVar = self.makeTempVar();
self.jumpIf(
t.memberExpression(
t.assignmentExpression(
"=",
keyInfoTmpVar,
t.callExpression(t.cloneDeep(keyIterNextFn), [])
),
t.identifier("done"),
false
),
after
);
self.emitAssign(
stmt.left,
t.memberExpression(
t.cloneDeep(keyInfoTmpVar),
t.identifier("value"),
false
)
);
self.leapManager.withEntry(
new leap.LoopEntry(after, head, labelId),
function() { self.explodeStatement(path.get("body")); }
);
self.jump(head);
self.mark(after);
break;
case "BreakStatement":
self.emitAbruptCompletion({
type: "break",
target: self.leapManager.getBreakLoc(stmt.label)
});
break;
case "ContinueStatement":
self.emitAbruptCompletion({
type: "continue",
target: self.leapManager.getContinueLoc(stmt.label)
});
break;
case "SwitchStatement":
let disc = self.emitAssign(
self.makeTempVar(),
self.explodeExpression(path.get("discriminant"))
);
after = this.loc();
let defaultLoc = this.loc();
let condition = defaultLoc;
let caseLocs = [];
let cases = stmt.cases || [];
for (let i = cases.length - 1; i >= 0; --i) {
let c = cases[i];
t.assertSwitchCase(c);
if (c.test) {
condition = t.conditionalExpression(
t.binaryExpression("===", t.cloneDeep(disc), c.test),
caseLocs[i] = this.loc(),
condition
);
} else {
caseLocs[i] = defaultLoc;
}
}
let discriminant = path.get("discriminant");
util.replaceWithOrRemove(discriminant, condition);
self.jump(self.explodeExpression(discriminant));
self.leapManager.withEntry(
new leap.SwitchEntry(after),
function() {
path.get("cases").forEach(function(casePath) {
let i = casePath.key;
self.mark(caseLocs[i]);
casePath.get("consequent").forEach(function (path) {
self.explodeStatement(path);
});
});
}
);
self.mark(after);
if (defaultLoc.value === PENDING_LOCATION) {
self.mark(defaultLoc);
assert.strictEqual(after.value, defaultLoc.value);
}
break;
case "IfStatement":
let elseLoc = stmt.alternate && this.loc();
after = this.loc();
self.jumpIfNot(
self.explodeExpression(path.get("test")),
elseLoc || after
);
self.explodeStatement(path.get("consequent"));
if (elseLoc) {
self.jump(after);
self.mark(elseLoc);
self.explodeStatement(path.get("alternate"));
}
self.mark(after);
break;
case "ReturnStatement":
self.emitAbruptCompletion({
type: "return",
value: self.explodeExpression(path.get("argument"))
});
break;
case "WithStatement":
throw new Error("WithStatement not supported in generator functions.");
case "TryStatement":
after = this.loc();
let handler = stmt.handler;
let catchLoc = handler && this.loc();
let catchEntry = catchLoc && new leap.CatchEntry(
catchLoc,
handler.param
);
let finallyLoc = stmt.finalizer && this.loc();
let finallyEntry = finallyLoc &&
new leap.FinallyEntry(finallyLoc, after);
let tryEntry = new leap.TryEntry(
self.getUnmarkedCurrentLoc(),
catchEntry,
finallyEntry
);
self.tryEntries.push(tryEntry);
self.updateContextPrevLoc(tryEntry.firstLoc);
self.leapManager.withEntry(tryEntry, function() {
self.explodeStatement(path.get("block"));
if (catchLoc) {
if (finallyLoc) {
self.jump(finallyLoc);
} else {
self.jump(after);
}
self.updateContextPrevLoc(self.mark(catchLoc));
let bodyPath = path.get("handler.body");
let safeParam = self.makeTempVar();
self.clearPendingException(tryEntry.firstLoc, safeParam);
bodyPath.traverse(catchParamVisitor, {
getSafeParam: () => t.cloneDeep(safeParam),
catchParamName: handler.param.name
});
self.leapManager.withEntry(catchEntry, function() {
self.explodeStatement(bodyPath);
});
}
if (finallyLoc) {
self.updateContextPrevLoc(self.mark(finallyLoc));
self.leapManager.withEntry(finallyEntry, function() {
self.explodeStatement(path.get("finalizer"));
});
self.emit(t.returnStatement(t.callExpression(
self.contextProperty("finish"),
[finallyEntry.firstLoc]
)));
}
});
self.mark(after);
break;
case "ThrowStatement":
self.emit(t.throwStatement(
self.explodeExpression(path.get("argument"))
));
break;
case "ClassDeclaration":
self.emit(self.explodeClass(path));
break;
default:
throw new Error(
"unknown Statement of type " +
JSON.stringify(stmt.type));
}
};
let catchParamVisitor = {
Identifier: function(path, state) {
if (path.node.name === state.catchParamName && util.isReference(path)) {
util.replaceWithOrRemove(path, state.getSafeParam());
}
},
Scope: function(path, state) {
if (path.scope.hasOwnBinding(state.catchParamName)) {
path.skip();
}
}
};
Ep.emitAbruptCompletion = function(record) {
if (!isValidCompletion(record)) {
assert.ok(
false,
"invalid completion record: " +
JSON.stringify(record)
);
}
assert.notStrictEqual(
record.type, "normal",
"normal completions are not abrupt"
);
const t = util.getTypes();
let abruptArgs = [t.stringLiteral(record.type)];
if (record.type === "break" ||
record.type === "continue") {
t.assertLiteral(record.target);
abruptArgs[1] = this.insertedLocs.has(record.target)
? record.target
: t.cloneDeep(record.target);
} else if (record.type === "return" ||
record.type === "throw") {
if (record.value) {
t.assertExpression(record.value);
abruptArgs[1] = this.insertedLocs.has(record.value)
? record.value
: t.cloneDeep(record.value);
}
}
this.emit(
t.returnStatement(
t.callExpression(
this.contextProperty("abrupt"),
abruptArgs
)
)
);
};
function isValidCompletion(record) {
let type = record.type;
if (type === "normal") {
return !hasOwn.call(record, "target");
}
if (type === "break" ||
type === "continue") {
return !hasOwn.call(record, "value")
&& util.getTypes().isLiteral(record.target);
}
if (type === "return" ||
type === "throw") {
return hasOwn.call(record, "value")
&& !hasOwn.call(record, "target");
}
return false;
}
Ep.getUnmarkedCurrentLoc = function() {
return util.getTypes().numericLiteral(this.listing.length);
};
Ep.updateContextPrevLoc = function(loc) {
const t = util.getTypes();
if (loc) {
t.assertLiteral(loc);
if (loc.value === PENDING_LOCATION) {
loc.value = this.listing.length;
} else {
assert.strictEqual(loc.value, this.listing.length);
}
} else {
loc = this.getUnmarkedCurrentLoc();
}
this.emitAssign(this.contextProperty("prev"), loc);
};
Ep.explodeViaTempVar = function(tempVar, childPath, hasLeapingChildren, ignoreChildResult) {
assert.ok(
!ignoreChildResult || !tempVar,
"Ignoring the result of a child expression but forcing it to " +
"be assigned to a temporary variable?"
);
const t = util.getTypes();
let result = this.explodeExpression(childPath, ignoreChildResult);
if (ignoreChildResult) {
} else if (tempVar || (hasLeapingChildren &&
!t.isLiteral(result))) {
result = this.emitAssign(
tempVar || this.makeTempVar(),
result
);
}
return result;
};
Ep.explodeExpression = function(path, ignoreResult) {
const t = util.getTypes();
let expr = path.node;
if (expr) {
t.assertExpression(expr);
} else {
return expr;
}
let self = this;
let result; let after;
function finish(expr) {
t.assertExpression(expr);
if (ignoreResult) {
self.emit(expr);
}
return expr;
}
if (!meta.containsLeap(expr)) {
return finish(expr);
}
let hasLeapingChildren = meta.containsLeap.onlyChildren(expr);
switch (expr.type) {
case "MemberExpression":
return finish(t.memberExpression(
self.explodeExpression(path.get("object")),
expr.computed
? self.explodeViaTempVar(null, path.get("property"), hasLeapingChildren)
: expr.property,
expr.computed
));
case "CallExpression":
let calleePath = path.get("callee");
let argsPath = path.get("arguments");
let newCallee;
let newArgs;
let hasLeapingArgs = argsPath.some(
argPath => meta.containsLeap(argPath.node)
);
let injectFirstArg = null;
if (t.isMemberExpression(calleePath.node)) {
if (hasLeapingArgs) {
let newObject = self.explodeViaTempVar(
self.makeTempVar(),
calleePath.get("object"),
hasLeapingChildren
);
let newProperty = calleePath.node.computed
? self.explodeViaTempVar(null, calleePath.get("property"), hasLeapingChildren)
: calleePath.node.property;
injectFirstArg = newObject;
newCallee = t.memberExpression(
t.memberExpression(
t.cloneDeep(newObject),
newProperty,
calleePath.node.computed
),
t.identifier("call"),
false
);
} else {
newCallee = self.explodeExpression(calleePath);
}
} else {
newCallee = self.explodeViaTempVar(null, calleePath, hasLeapingChildren);
if (t.isMemberExpression(newCallee)) {
newCallee = t.sequenceExpression([
t.numericLiteral(0),
t.cloneDeep(newCallee)
]);
}
}
if (hasLeapingArgs) {
newArgs = argsPath.map(argPath => self.explodeViaTempVar(null, argPath, hasLeapingChildren));
if (injectFirstArg) newArgs.unshift(injectFirstArg);
newArgs = newArgs.map(arg => t.cloneDeep(arg));
} else {
newArgs = path.node.arguments;
}
return finish(t.callExpression(newCallee, newArgs));
case "NewExpression":
return finish(t.newExpression(
self.explodeViaTempVar(null, path.get("callee"), hasLeapingChildren),
path.get("arguments").map(function(argPath) {
return self.explodeViaTempVar(null, argPath, hasLeapingChildren);
})
));
case "ObjectExpression":
return finish(t.objectExpression(
path.get("properties").map(function(propPath) {
if (propPath.isObjectProperty()) {
return t.objectProperty(
propPath.node.key,
self.explodeViaTempVar(null, propPath.get("value"), hasLeapingChildren),
propPath.node.computed
);
} else {
return propPath.node;
}
})
));
case "ArrayExpression":
return finish(t.arrayExpression(
path.get("elements").map(function(elemPath) {
if (!elemPath.node) {
return null;
} if (elemPath.isSpreadElement()) {
return t.spreadElement(
self.explodeViaTempVar(null, elemPath.get("argument"), hasLeapingChildren)
);
} else {
return self.explodeViaTempVar(null, elemPath, hasLeapingChildren);
}
})
));
case "SequenceExpression":
let lastIndex = expr.expressions.length - 1;
path.get("expressions").forEach(function(exprPath) {
if (exprPath.key === lastIndex) {
result = self.explodeExpression(exprPath, ignoreResult);
} else {
self.explodeExpression(exprPath, true);
}
});
return result;
case "LogicalExpression":
after = this.loc();
if (!ignoreResult) {
result = self.makeTempVar();
}
let left = self.explodeViaTempVar(result, path.get("left"), hasLeapingChildren);
if (expr.operator === "&&") {
self.jumpIfNot(left, after);
} else {
assert.strictEqual(expr.operator, "||");
self.jumpIf(left, after);
}
self.explodeViaTempVar(result, path.get("right"), hasLeapingChildren, ignoreResult);
self.mark(after);
return result;
case "ConditionalExpression":
let elseLoc = this.loc();
after = this.loc();
let test = self.explodeExpression(path.get("test"));
self.jumpIfNot(test, elseLoc);
if (!ignoreResult) {
result = self.makeTempVar();
}
self.explodeViaTempVar(result, path.get("consequent"), hasLeapingChildren, ignoreResult);
self.jump(after);
self.mark(elseLoc);
self.explodeViaTempVar(result, path.get("alternate"), hasLeapingChildren, ignoreResult);
self.mark(after);
return result;
case "UnaryExpression":
return finish(t.unaryExpression(
expr.operator,
self.explodeExpression(path.get("argument")),
!!expr.prefix
));
case "BinaryExpression":
return finish(t.binaryExpression(
expr.operator,
self.explodeViaTempVar(null, path.get("left"), hasLeapingChildren),
self.explodeViaTempVar(null, path.get("right"), hasLeapingChildren)
));
case "AssignmentExpression":
if (expr.operator === "=") {
return finish(t.assignmentExpression(
expr.operator,
self.explodeExpression(path.get("left")),
self.explodeExpression(path.get("right"))
));
}
const lhs = self.explodeExpression(path.get("left"));
const temp = self.emitAssign(self.makeTempVar(), lhs);
return finish(t.assignmentExpression(
"=",
t.cloneDeep(lhs),
t.assignmentExpression(
expr.operator,
t.cloneDeep(temp),
self.explodeExpression(path.get("right"))
)
));
case "UpdateExpression":
return finish(t.updateExpression(
expr.operator,
self.explodeExpression(path.get("argument")),
expr.prefix
));
case "YieldExpression":
after = this.loc();
let arg = expr.argument && self.explodeExpression(path.get("argument"));
if (arg && expr.delegate) {
let result = self.makeTempVar();
let ret = t.returnStatement(t.callExpression(
self.contextProperty("delegateYield"),
[
arg,
t.stringLiteral(result.property.name),
after
]
));
ret.loc = expr.loc;
self.emit(ret);
self.mark(after);
return result;
}
self.emitAssign(self.contextProperty("next"), after);
let ret = t.returnStatement(t.cloneDeep(arg) || null);
ret.loc = expr.loc;
self.emit(ret);
self.mark(after);
return self.contextProperty("sent");
case "ClassExpression":
return finish(self.explodeClass(path));
default:
throw new Error(
"unknown Expression of type " +
JSON.stringify(expr.type));
}
};
Ep.explodeClass = function(path) {
const explodingChildren = [];
if (path.node.superClass) {
explodingChildren.push(path.get("superClass"));
}
path.get("body.body").forEach(member => {
if (member.node.computed) {
explodingChildren.push(member.get("key"));
}
});
const hasLeapingChildren = explodingChildren.some(
child => meta.containsLeap(child));
for (let i = 0; i < explodingChildren.length; i++) {
const child = explodingChildren[i];
const isLast = i === explodingChildren.length - 1;
if (isLast) {
child.replaceWith(this.explodeExpression(child));
} else {
child.replaceWith(this.explodeViaTempVar(null, child, hasLeapingChildren));
}
}
return path.node;
};