execute = {
const executors = {};
function makeSummaryReporter () {
let i = 0;
return function (context, info) {
return `[${i++}] ${info.pre ? "pre-" : ""} ${info.summary}`;
};
}
function* exec(node, context = makeInitialContext(), options = { report: makeSummaryReporter() }) {
const node_executor = executors[node.type];
if (!node_executor) {
console.log(node);
throw new NotImplemented(`No executor for AST nodes of type: ${node.type}`);
}
return yield* node_executor(node, context, options);
}
function applyOperator(op, a, b) {
if (op === "+") return a + b;
if (op === "-") return a - b;
if (op === "*") return a * b;
if (op === "&&") return a && b;
if (op === "||") return a || b;
throw new NotImplemented(`operator not implemented: ${op}`);
}
executors.File = function* (node, context, { report }) {
yield* exec(node.program, context, { report });
};
executors.Program = function* (node, context, { report }) {
for (const statement of node.body) {
yield* exec(statement, context, { report });
}
};
executors.BlockStatement = function* (node, context, { report, skipScope }) {
// TODO make new scope instead if `skip_scope` set
for (const statement of node.body) {
yield* exec(statement, context, { report });
}
};
executors.ReturnStatement = function* (node, context, { report }) {
yield report(context, { node, pre: true, summary: `return`, detail: 1 });
const ret = new Return(yield* exec(node.argument, context, { report }));
yield report(context, { node, summary: `return`, detail: 1 });
throw ret;
};
function* invokeFunction ({ definingScope, params, body }, args, context, { report }) {
// Create an execution scope
const executionScope = {
parent: definingScope,
children: [],
variables: {}
};
const scope_ref = context.scopes.push(executionScope) - 1;
context.scopes[definingScope].children.push(scope_ref);
// Populate with given arguments
for (const [i, param] of params.entries()) {
if (param.type !== "Identifier")
throw new NotImplemented(`Sorry, can't deal with ${param.type} params yet :(`);
executionScope.variables[param.name] = {
name: param.name,
kind: "param",
value: args[i]
};
}
let result;
if (body.type === "BlockStatement") {
result = yield* Return.from(function* () {
return yield* exec(body, { ...context, currentScope: scope_ref }, { report, skipScope: true });
});
} else {
result = yield* exec(body, { ...context, currentScope: scope_ref }, { report });
}
executionScope.variables[Return.symbol] = { kind: "return", value: result };
return result;
};
executors.CallExpression = function* (node, context, { report }) {
const { callee, arguments: argnodes } = node;
yield report(context, { node, pre: true, summary: `evaluate call`, detail: 2 });
// Second, lookup the function
const fnValue = yield* exec(callee, context, { report });
if (!("object_ref" in fnValue))
throw new RuntimeError(`Cannot call non-function #1`);
const fn = context.objects[fnValue.object_ref];
if (!fn || fn.type !== "function")
throw new RuntimeError(`Cannot call non-function #2`);
// First, evaluate arguments
const args = [];
for (const argnode of argnodes) {
args.push(yield* exec(argnode, context, { report }));
}
// Finally, invoke the function
yield report(context, { node, pre: true, summary: `invoke function`, detail: 2 });
const ret = yield* invokeFunction(fn, args, context, { report });
yield report(context, { node, summary: `evaluated call`, detail: 2 });
return ret;
};
executors.FunctionDeclaration = function* (node, context, { report }) {
if (node.async || node.generator)
throw new NotSupported(`Async and generator functions are not supported`);
if (!node.id || node.id.type !== "Identifier")
throw new RuntimeError(`should not happen`);
yield report(context, { node, pre: true, summary: `declare function ${node.id.name}`, detail: 1 });
const scope = context.scopes[context.currentScope];
const fn = {
type: "function",
definingScope: context.currentScope,
params: node.params,
body: node.body
};
const object_ref = context.objects.push(fn) - 1;
scope.variables[node.id.name] = { name: node.id.name, kind: "function", value: { object_ref } };
yield report(context, { node, summary: `declared function ${node.id.name}`, detail: 1 });
};
executors.ObjectProperty = function* (node, context, { report, object_ref }) {
const { key } = node;
if (key.type !== "Identifier")
throw new NotImplemented(`Computed or otherwise property keys are sadly not implemented yet :(`);
const obj = context.objects[object_ref];
const value = yield* exec(node.value, context, { report });
obj.properties[key.name] = { name: key.name, kind: "property", value };
};
executors.ObjectExpression = function* (node, context, { report }) {
yield report(context, { node, pre: true, summary: `evaluate obj expression`, detail: 2 });
const obj = {
type: "object",
properties: {}
};
const object_ref = context.objects.push(obj) - 1;
for (const property of node.properties) {
yield* exec(property, context, { report, object_ref });
}
yield report(context, { node, summary: `evaluated obj expression`, detail: 2 });
return { object_ref };
};
executors.ArrowFunctionExpression = function* (node, context, { report }) {
if (node.async || node.generator)
throw new NotSupported(`Async and generator functions are not supported`);
const scope = context.scopes[context.currentScope];
const fn = {
type: "function",
definingScope: context.currentScope,
params: node.params,
body: node.body
};
const object_ref = context.objects.push(fn) - 1;
yield report(context, { node, summary: `evaluated arrow function`, detail: 2 });
return { object_ref };
};
executors.BinaryExpression = function* (node, context, { report }) {
const { left, right } = node;
yield report(context, { node, pre: true, summary: `evaluate binary expression`, detail: 2 });
const leftValue = yield* exec(left, context, { report });
if (node.operator === "&&" && !leftValue) return leftValue;
if (node.operator === "||" && leftValue) return leftValue;
const rightValue = yield* exec(right, context, { report });
yield report(context, { node, summary: `evaluated binary expression`, detail: 2 });
return applyOperator(node.operator, leftValue, rightValue);
};
// (a) lookup identifier
function* _LookupIdentifier (node, context, { report }) {
const defining_scope_ref = lookup(context, node.name, context.currentScope);
if (defining_scope_ref === null)
throw new RuntimeError(`variable ${node.name} is not defined [lookup identifier]`);
yield report(context, { node, summary: `looked up ${node.name}`, detail: 2 });
return {
scope_ref: defining_scope_ref,
name: node.name,
site: context.scopes[defining_scope_ref].variables[node.name]
};
};
// (b) evaluate identifier as value (more common)
executors.Identifier = function* (node, context, { report }) {
if (node.name === "undefined") {
yield report(context, { node, summary: `evaluated undefined`, detail: 2 });
return undefined;
} else {
const defining_scope_ref = lookup(context, node.name, context.currentScope);
if (defining_scope_ref === null) {
throw new RuntimeError(`variable ${node.name} is not defined [eval identifier]`);
}
yield report(context, { node, summary: `looked up ${node.name}`, detail: 2 });
return context.scopes[defining_scope_ref].variables[node.name].value;
}
};
executors.MemberExpression = function* (node, context, { report }) {
const { computed, object, property } = node;
const obj = yield* exec(object, context, { report });
let propertyName;
if (computed) {
propertyName = yield* exec();
} else {
if (property.type !== "Identifier") throw "should not happen";
propertyName = property.name;
}
const defining_object_ref = lookupProperty(context, propertyName);
if (defining_object_ref === null)
throw new RuntimeError(`property ${node.name} not found`);
yield report(context, { node, summary: `looked up property ${propertyName}`, detail: 2 });
return {
object_ref: defining_object_ref,
name: propertyName,
site: context.objects[defining_object_ref].properties[propertyName]
};
};
executors.AssignmentExpression = function* (node, context, { report }) {
const { left, right } = node;
// yield report(context, { node, pre: true, summary: `assign ${left.name}`, detail: 2 });
const { site } = (yield* _LookupIdentifier(left, context, { report }));
site.value = yield* exec(right, context, { report });
// yield report(context, { node, summary: `assigned ${left.name}`, detail: 2 });
};
executors.ExpressionStatement = function* (node, context, { report }) {
yield report(context, { node, pre: true, summary: `compute expression statement`, detail: 1 });
yield* exec(node.expression, context, { report });
yield report(context, { node, summary: `computed expression statement`, detail: 1 });
};
executors.VariableDeclaration = function* (node, context, { report }) {
const kind = node.kind;
yield report(context, { node, pre: true, summary: `${kind} variable declaration`, detail: 1 });
for (const declaration of node.declarations) {
yield* exec(declaration, context, { report, kind });
}
yield report(context, { node, summary: `completed ${kind} variable declaration`, detail: 1 });
};
executors.VariableDeclarator = function* (node, context, { report, kind }) {
if (kind !== "let" && kind !== "const")
throw new NotSupported(`only let/const supported, not ${kind}`);
if (node.id.type !== "Identifier")
throw new NotImplemented(`Not yet able to declare non-identifiers :(`);
const scope = context.scopes[context.currentScope];
const name = node.id.name;
scope.variables[name] = { name, kind };
yield report(context, { node: node.id, summary: `declared ${name}`, detail: 2 });
if (node.init) {
const value = yield* exec(node.init, context, { report });
scope.variables[name].value = value;
yield report(context, { node, summary: `initialized ${name}`, detail: 2 });
}
};
executors.NullLiteral = function* (node, context, { report }) {
yield report(context, { node, summary: `evaluated null`, detail: 2 });
return null;
};
executors.StringLiteral = function* (node, context, { report }) {
yield report(context, { node, summary: `evaluated "${node.value}"`, detail: 2});
return node.value;
};
executors.NumericLiteral = function* (node, context, { report }) {
yield report(context, { node, summary: `evaluated ${node.value}`, detail: 2 });
return node.value;
};
executors.BooleanLiteral = function* (node, context, { report }) {
yield report(context, { node, summary: `evaluated ${node.value}`, detail: 2 });
return node.value;
};
exec._executors = executors;
return exec;
}