Clean up and document the generator approach

This commit is contained in:
Jordan Eldredge 2019-07-23 23:44:21 -07:00
parent 2a3be1656b
commit a3b7527f71
2 changed files with 25 additions and 4 deletions

View file

@ -1,8 +1,10 @@
const parse = require("./parser");
const { getClass, getFormattedId } = require("./objects");
const interpret = require("./virtualMachine");
const { printCommand } = require("./prettyPrinter");
// Note: if this incurs a performance overhead, we could pass a flag into the VM
// to not yield in production. In that case, we would never even enter the
// `while` loop.
function runGeneratorUntilReturn(gen) {
let val = gen.next();
while (!val.done) {
@ -40,6 +42,11 @@ function main({ runtime, data, system, log, debugHandler }) {
// TODO: Handle disposing of this.
// TODO: Handle passing in variables.
variable.hook(method.name, () => {
// Interpret is a generator that yields before each command is exectued.
// `handler` is reponsible for `.next()`ing until the program execution is
// complete (the generator is "done"). In production this is done
// synchronously. In the debugger, if execution is paused, it's done
// async.
handler(interpret(commandOffset, program, []));
});
});

View file

@ -10,6 +10,15 @@ function coerceTypes(var1, var2, val1, val2) {
return val1;
}
// Note: We implement `interpret` as a generator in order to support pausing
// execution in the debugger. This allows the caller granular control over the
// execution timing of the virtual machine while also allowing the VM to divulge
// some of its internal state before each command is exectued.
//
// In "production" we syncrnously call `.next()` on the generator until it's
// empty, ignoring all yeilded values. In the debugger we use the yielded data
// to populate the UI, and -- when stepping -- pause execution by waiting
// between calls to `.next()`.
function* interpret(start, program, stack = []) {
const { commands, methods, variables, classes } = program;
@ -35,9 +44,11 @@ function* interpret(start, program, stack = []) {
let i = start;
while (i < commands.length) {
const command = commands[i];
{
yield { i, command, stack, variables, commands };
}
// This probably incurrs some perf cost. If it does, we can pass in a flag
// to enable it only when we are debugging. When we are not, (in prod) we
// can just jump right over it and we will execute straight through to the
// return value on the first call to `.next()`.
yield { i, command, stack, variables, commands };
switch (command.opcode) {
// push
@ -141,6 +152,9 @@ function* interpret(start, program, stack = []) {
// callGlobal
case 25: {
const offset = command.arg;
// Note: We proxy all yielded values from the child `interpret` out to
// the caller, while capturing the return value, (`value`) for use within the
// VM.
const value = yield* interpret(offset, program, stack);
stack.push(value);
break;