diff --git a/experiments/modern/src/maki-interpreter/interpreter.js b/experiments/modern/src/maki-interpreter/interpreter.js index f1d46757..31b2812c 100644 --- a/experiments/modern/src/maki-interpreter/interpreter.js +++ b/experiments/modern/src/maki-interpreter/interpreter.js @@ -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, [])); }); }); diff --git a/experiments/modern/src/maki-interpreter/virtualMachine.js b/experiments/modern/src/maki-interpreter/virtualMachine.js index 1623c709..5919d682 100644 --- a/experiments/modern/src/maki-interpreter/virtualMachine.js +++ b/experiments/modern/src/maki-interpreter/virtualMachine.js @@ -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;