hertzscript-compiler
Version:
Produces preemptible JavaScript coroutines which conform to the HertzScript specification.
519 lines • 15.9 kB
JavaScript
function Plugin(babel) {
const t = babel.types;
const tryStack = [];
// Traverses specific expression types and marks a CallExpression in tail position
function markTailCall(expr) {
if (expr.type === "CallExpression") {
expr.isTailCall = true;
} else if (expr.type === "SequenceExpression" && expr.expressions.length > 0) {
return markTailCall(expr.expressions[expr.expressions.length - 1]);
} else if (expr.type === "LogicalExpression") {
if (expr.operator === "&&" || expr.operator === "||")
return markTailCall(expr.right);
} else if (expr.type === "ConditionalExpression") {
markTailCall(expr.consequent);
return markTailCall(expr.alternate);
}
}
function addTailCallBool(seqExp) {
seqExp.expressions[0].argument.arguments.push(t.booleanLiteral(true));
}
function getParentNode(path) {
const parentPath = path.getFunctionParent();
if (parentPath === null) return path.scope.getProgramParent().block;
return parentPath.node;
}
// HzTokens are unique single-instance objects for wrapping user instructions and data.
// Type 1: Invocation Tokens,
// Wrap userland functors and any operands needed to invoke them.
// Type 2: Data Tokens,
// Wrap arbitrary userland datum when returning or yielding.
// Instruction Token: Function call without arguments
function hzCall(callee) {
return t.sequenceExpression([
t.yieldExpression(
t.callExpression(
t.memberExpression(
t.identifier("hzUserLib"),
t.identifier("call")
),
[
callee
]
)
)
]);
}
// Instruction Token: Function call with arguments
function hzCallArgs(name, argsArray) {
const seqExp = hzCall(name);
seqExp.expressions[0].argument.callee.property.name = "callArgs";
seqExp.expressions[0].argument.arguments.push(t.arrayExpression(argsArray));
return seqExp;
}
// Instruction Token: Method call without arguments
function hzCallMethod(object, prop) {
return t.sequenceExpression([
t.yieldExpression(
t.callExpression(
t.memberExpression(
t.identifier("hzUserLib"),
t.identifier("callMethod")
),
[
object,
t.stringLiteral(prop.name)
]
)
)
]);
}
// Instruction Token: Method call with arguments
function hzCallMethodArgs(object, prop, argsArray) {
const seqExp = hzCallMethod(object, prop);
seqExp.expressions[0].argument.callee.property.name = "callMethodArgs";
seqExp.expressions[0].argument.arguments.push(t.arrayExpression(argsArray));
return seqExp;
}
// Instruction Token: Constructor call without arguments
function hzNew(callee) {
const seqExp = hzCall(callee);
seqExp.expressions[0].argument.callee.property.name = "new";
return seqExp;
}
// Instruction Token: Constructor call with arguments
function hzNewArgs(name, argsArray) {
const seqExp = hzNew(name);
seqExp.expressions[0].argument.arguments.push(t.arrayExpression(argsArray));
seqExp.expressions[0].argument.callee.property.name = "newArgs";
return seqExp;
}
// Instruction Token: Method constructor call without arguments
function hzNewMethod(object, prop) {
const seqExp = hzCallMethod(object, prop);
seqExp.expressions[0].argument.callee.property.name = "newMethod";
return seqExp;
}
// Instruction Token: Method constructor call with arguments
function hzNewMethodArgs(object, prop, argsArray) {
const seqExp = hzNewMethod(object, prop);
seqExp.expressions[0].argument.arguments.push(t.arrayExpression(argsArray));
seqExp.expressions[0].argument.callee.property.name = "newMethodArgs";
return seqExp;
}
// Instruction Token: Return without argument
function hzReturn() {
return t.memberExpression(
t.identifier("hzUserLib"),
t.identifier("return")
);
}
// Instruction Token: Return with argument
function hzReturnArg(argExp) {
const memberExp = hzReturn();
const callExp = t.callExpression(memberExp, [argExp]);
memberExp.property.name = "returnValue";
return callExp;
}
// Instruction Token: Yield without argument
function hzYield() {
return t.callExpression(
t.memberExpression(
t.identifier("hzUserLib"),
t.identifier("yield")
),
[t.ObjectExpression([
t.ObjectProperty(t.identifier("value"), t.identifier("undefined")),
t.ObjectProperty(t.identifier("done"), t.BooleanLiteral(false))
])]
);
}
// Instruction Token: Yield with argument
function hzYieldArg(argExp, delegate = false) {
const callExp = hzYield();
callExp.callee.property.name = "yieldValue";
callExp.arguments[0].properties[0].value = argExp;
callExp.arguments[1] = t.BooleanLiteral(delegate);
return callExp;
}
// Instruction Token: Spawn without arguments
function hzSpawn(spawnExp) {
if (spawnExp.arguments[0].type === "CallExpression") {
spawnExp.arguments = [spawnExp.arguments[0].callee];
} else {
spawnExp.arguments = [spawnExp.arguments[0]];
}
return t.yieldExpression(
spawnExp
);
}
// Instruction Token: Spawn with arguments
function hzSpawnArgs(spawnExp) {
const args = spawnExp.arguments[0].arguments;
spawnExp = hzSpawn(spawnExp);
spawnExp.argument.arguments.push(t.arrayExpression(args));
spawnExp.argument.callee.property.name = "spawnArgs";
return spawnExp;
}
// Instruction Token: Spawn method without arguments
function hzSpawnMethod(spawnExp) {
spawnExp.arguments = [
spawnExp.arguments[0].callee.object,
t.stringLiteral(spawnExp.arguments[0].callee.property.name)
];
spawnExp.callee.property.name = "spawnMethod";
return t.yieldExpression(
spawnExp
);
}
// Instruction Token: Spawn method with arguments
function hzSpawnMethodArgs(spawnExp) {
const args = spawnExp.arguments[0].arguments;
spawnExp = hzSpawnMethod(spawnExp);
spawnExp.argument.arguments.push(t.arrayExpression(args));
spawnExp.argument.callee.property.name = "spawnMethodArgs";
return spawnExp;
}
// Instruction Token: Loop interruption token
function loopInterruptor(path) {
if (path.node.body.type !== "BlockStatement") {
if (path.node.body.type === "EmptyStatement") {
path.node.body = t.blockStatement([]);
} else {
if (Array.isArray(path.node.body)) {
path.node.body = t.blockStatement(path.node.body);
} else {
path.node.body = t.blockStatement([path.node.body]);
}
}
}
path.node.body.body.unshift(t.expressionStatement(
t.yieldExpression(t.memberExpression(
t.identifier("hzUserLib"),
t.identifier("loopYield")
))
));
}
// Function call, declaration, and expression detours enable dynamic call site interception.
// FunctionExpression detour
function hzCoroutine(funcExp) {
return t.callExpression(
t.memberExpression(
t.identifier("hzUserLib"),
t.identifier("hookCoroutine")
),
[funcExp]
);
}
// ArrowFunctionExpression detour
function hzArrowCoroutine(funcExp) {
funcExp.type = "FunctionExpression";
if (funcExp.body.type !== "BlockStatement") {
funcExp.body = t.blockStatement([
t.expressionStatement(
t.yieldExpression(hzReturnArg(funcExp.body)))
]);
}
return t.callExpression(
t.memberExpression(
t.identifier("hzUserLib"),
t.identifier("hookArrowCoroutine")
),
[
funcExp,
t.identifier("this")
]
);
}
// Generator FunctionExpression detour
function hzGenerator(funcExp) {
return t.callExpression(
t.memberExpression(
t.identifier("hzUserLib"),
t.identifier("hookGenerator")
),
[funcExp]
);
}
// FunctionDeclaration detour
function declareHzCoroutine(funcDec) {
return t.variableDeclaration("var", [
t.variableDeclarator(
funcDec.id,
hzCoroutine(t.functionExpression(
null,
funcDec.params,
funcDec.body,
true,
funcDec.async
))
)
]);
}
// Generator FunctionDeclaration detour
function declareHzGenerator(funcDec) {
return t.variableDeclaration("var", [
t.variableDeclarator(
funcDec.id,
hzGenerator(t.functionExpression(
null,
funcDec.params,
funcDec.body,
true,
funcDec.async
))
)
]);
}
const visitor = {
// These all nsert a yield & HzToken at the top of loops
// Useful for interrupting loops which make few function calls
"WhileStatement": {
exit: loopInterruptor
},
"DoWhileStatement": {
exit: loopInterruptor
},
"ForStatement": {
exit: loopInterruptor
},
"ForOfStatement": {
exit: loopInterruptor
},
"ForInStatement": {
exit: loopInterruptor
},
// Detours a FunctionExpression
"FunctionExpression": {
exit: function (path) {
if (path.node.generator) path.replaceWith(hzGenerator(path.node));
else path.replaceWith(hzCoroutine(path.node));
path.node.arguments[0].generator = true;
path.skip();
}
},
// Detours an ArrowFunctionExpression
"ArrowFunctionExpression": {
enter: function (path) {
if (path.node.body.type !== "BlockStatement") markTailCall(path.node.body);
},
exit: function (path) {
path.replaceWith(hzArrowCoroutine(path.node));
path.node.arguments[0].generator = true;
path.skip();
}
},
// Detours a FunctionDeclaration.
// Changes it to a FunctionExpression and assigns it to a variable, moving it tp the top of the block
"FunctionDeclaration": {
exit: function (path) {
if (path.node.generator) var varDec = declareHzGenerator(path.node);
else var varDec = declareHzCoroutine(path.node);
path.node.generator = true;
const parentNode = getParentNode(path);
if (Array.isArray(parentNode.body)) parentNode.body.unshift(varDec);
else parentNode.body.body.unshift(varDec);
path.remove();
}
},
// Transforms a NewExpression into an Instruction Token
"NewExpression": {
exit: function (path) {
if (path.node.callee.type === "MemberExpression") {
if (path.node.arguments.length === 0) {
path.replaceWith(hzNewMethod(
path.node.callee.object,
path.node.callee.property
));
} else {
path.replaceWith(hzNewMethodArgs(
path.node.callee.object,
path.node.callee.property,
path.node.arguments
));
}
} else {
if (path.node.arguments.length === 0) {
path.replaceWith(hzNew(
path.node.callee
));
} else {
path.replaceWith(hzNewArgs(
path.node.callee,
path.node.arguments
));
}
}
path.skip();
}
},
// Checks if the CallExpression is a partially transformed "spawn" HzToken from Acorn.
// If so, it completes the transformation of arguments and wraps the HzToken in a yield.
// If the argument is a FunctionExpression then it is detoured.
"CallExpression": {
enter: function (path) {
if (path.node.callee.type === "MemberExpression" &&
path.node.callee.object.type === "Identifier" &&
path.node.callee.object.name === "hzUserLib" &&
path.node.callee.property.type === "Identifier" &&
path.node.callee.property.name === "spawn"
) {
if (path.node.arguments[0].type === "CallExpression") {
if (path.node.arguments[0].callee.type === "MemberExpression") {
if (path.node.arguments[0].arguments.length > 0) {
path.replaceWith(hzSpawnMethodArgs(path.node));
} else {
path.replaceWith(hzSpawnMethod(path.node));
}
} else {
if (path.node.arguments[0].arguments.length > 0) {
path.replaceWith(hzSpawnArgs(path.node));
} else {
path.replaceWith(hzSpawn(path.node));
}
}
} else {
path.replaceWith(hzSpawn(path.node));
}
const callee = path.node.argument.arguments[0];
if (callee.type === "FunctionExpression"
|| callee.type === "ArrowFunctionExpression") {
if (callee.generator) {
path.node.argument.arguments[0] = hzGenerator(callee);
} else {
path.node.argument.arguments[0] = hzCoroutine(callee);
callee.generator = true;
}
}
path.skip();
}
},
// Transforms a CallExpression into an Instruction Token.
// Checks if the CallExpression is a proper tail call and marks the HzToken if so.
exit: function (path) {
const isTailCall = "isTailCall" in path.node;
if (path.node.callee.type === "MemberExpression") {
if (path.node.arguments.length === 0) {
path.replaceWith(hzCallMethod(
path.node.callee.object,
path.node.callee.property
));
} else {
path.replaceWith(hzCallMethodArgs(
path.node.callee.object,
path.node.callee.property,
path.node.arguments
));
}
} else {
if (path.node.arguments.length === 0) {
path.replaceWith(hzCall(
path.node.callee
));
} else {
path.replaceWith(hzCallArgs(
path.node.callee,
path.node.arguments
));
}
}
// Add Tail Call Optimization marker boolean to the HzToken
if (isTailCall) {
// Check for TCO validity if the call is within a TryStatement
if (tryStack.length > 0) {
const tryData = tryStack[tryStack.length - 1];
const parentNode = getParentNode(path);
if (parentNode === tryData.functionParent) {
if (
tryData.blockType === "finalizer"
|| tryData.blockType === "catch"
) {
addTailCallBool(path.node);
}
} else {
addTailCallBool(path.node);
}
} else {
addTailCallBool(path.node);
}
}
path.skip();
}
},
"ReturnStatement": {
// Finds and marks a CallExpression if it is in the tail position
enter: function (path) {
if (path.node.argument !== null) markTailCall(path.node.argument);
},
// Transforms a ReturnStatement into an Instruction Token
exit: function (path) {
if (getParentNode(path).generator) {
path.node.argument = hzReturnArg(t.ObjectExpression([
t.ObjectProperty(
t.identifier("value"),
path.node.argument === null ? t.identifier("undefined") : path.node.argument
),
t.ObjectProperty(
t.identifier("done"),
t.BooleanLiteral(true)
)
]));
} else {
if (path.node.argument === null) path.node.argument = hzReturn();
else path.node.argument = hzReturnArg(path.node.argument);
}
}
},
"BlockStatement": {
// Records entry into the "finalizer" block of a TryStatement
enter: function (path) {
if (tryStack.length > 0 && tryStack[tryStack.length - 1].blockType === null) {
const stmtParent = path.getStatementParent();
if (stmtParent.node.type === "TryStatement") {
if (stmtParent.node.finalizer === path.node)
tryStack[tryStack.length - 1].blockType = "finalizer";
else if (stmtParent.node.handler.body === path.node)
tryStack[tryStack.length - 1].blockType = "catch";
}
}
},
// Records exit out of the "finalizer" block of a TryStatement
exit: function (path) {
if (tryStack.length > 0 && tryStack[tryStack.length - 1].blockType !== null) {
const stmtParent = path.getStatementParent();
if (stmtParent.node.type === "TryStatement") {
if (
stmtParent.node.finalizer === path.node
|| stmtParent.node.handler.body === path.node
) {
tryStack[tryStack.length - 1].blockType = null;
}
}
}
}
},
"TryStatement": {
// Records entry into a TryStatement
enter: function (path) {
tryStack.push({
functionParent: getParentNode(path),
blockType: null
});
},
// Records exit out of a TryStatement
exit: function (path) {
if (tryStack.length > 0) tryStack.pop();
}
},
"YieldExpression": {
// Transforms a YieldExpression into an Instruction Token
exit: function (path) {
if (path.node.argument === null) path.node.argument = hzYield();
else path.node.argument = hzYieldArg(path.node.argument, path.node.delegate);
if (path.node.delegate) path.node.delegate = false;
}
}
};
return { visitor: visitor };
};
module.exports = Plugin;