Swap the name of the VM and the interpreter (#843)

This commit is contained in:
Jordan Eldredge 2019-08-09 20:50:37 -07:00
parent d8b33e4795
commit 46042d0748
5 changed files with 362 additions and 529 deletions

View file

@ -5,7 +5,7 @@ import * as Utils from "./utils";
import initialize from "./initialize";
import System from "./runtime/System";
import runtime from "./runtime";
import interpret from "./maki-interpreter/interpreter";
import { run } from "./maki-interpreter/virtualMachine";
// import simpleSkin from "../skins/simple.wal";
import cornerSkin from "../skins/CornerAmp_Redux.wal";
@ -315,7 +315,7 @@ function App() {
React.useEffect(() => {
getSkin().then(async root => {
// Execute scripts
await Utils.asyncTreeFlatMap(root, async node => {
await Utils.asyncTreeFlatMap(root, node => {
switch (node.name) {
case "groupdef": {
// removes groupdefs from consideration (only run scripts when actually referenced by group)
@ -332,7 +332,7 @@ function App() {
"WasabiXML",
]);
const system = new System(scriptGroup);
await interpret({
run({
runtime,
data: node.js_annotations.script,
system,

View file

@ -1,5 +1,5 @@
import React from "react";
import run from "../maki-interpreter/interpreter";
import { run } from "../maki-interpreter/virtualMachine";
import System from "../runtime/System";
import runtime from "../runtime";
import Variable from "./Variable";

View file

@ -1,65 +1,308 @@
import parse from "./parser";
import { getClass, getFormattedId } from "./objects";
import interpret from "./virtualMachine";
import Variable from "./variable";
// 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) {
val = gen.next();
}
return val.value;
}
function main({ runtime, data, system, log, debugHandler }) {
const program = parse(data);
// Replace class hashes with actual JavaScript classes from the runtime
program.classes = program.classes.map(hash => {
const resolved = runtime[hash];
if (resolved == null && log) {
const klass = getClass(hash);
console.warn(
`Class missing from runtime: ${hash}`,
klass == null
? `(formatted ID: ${getFormattedId(hash)})`
: `expected ${klass.name}`
);
function coerceTypes(var1, var2, val1, val2) {
if (var2.type === "INT") {
if (var1.type === "FLOAT" || var1.type === "DOUBLE") {
return Math.floor(val1);
}
return resolved;
});
}
const handler = debugHandler || runGeneratorUntilReturn;
// Bind top level hooks.
program.bindings.forEach(binding => {
const { commandOffset, variableOffset, methodOffset } = binding;
const variable = program.variables[variableOffset];
const method = program.methods[methodOffset];
// const logger = log ? printCommand : logger;
// TODO: Handle disposing of this.
// TODO: Handle passing in variables.
variable.hook(method.name, (...args) => {
// 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, args.reverse()));
});
});
const { commands, variables } = program;
commands.forEach((command, i) => {
// printCommand({ i, command, stack: [], variables });
});
// Set the System global
// TODO: We could confirm that this variable has the "system" flag set.
program.variables[0].setValue(system);
system.js_start();
return val1;
}
export default main;
// 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()`.
export function* interpret(start, program, stack = []) {
const { commands, methods, variables, classes } = program;
function popStackValue() {
const v = stack.pop();
return v instanceof Variable ? v.getValue() : v;
}
function twoArgCoercingOperator(operator) {
const a = stack.pop();
const b = stack.pop();
let aValue = a instanceof Variable ? a.getValue() : a;
const bValue = b instanceof Variable ? b.getValue() : b;
aValue = coerceTypes(a, b, aValue, bValue);
stack.push(operator(bValue, aValue));
}
function twoArgOperator(operator) {
const aValue = popStackValue();
const bValue = popStackValue();
stack.push(operator(bValue, aValue));
}
// Instruction Pointer
let ip = start;
while (ip < commands.length) {
const command = commands[ip];
// 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: ip, command, stack, variables, commands };
switch (command.opcode) {
// push
case 1: {
const offsetIntoVariables = command.arg;
stack.push(variables[offsetIntoVariables]);
break;
}
// pop
case 2: {
stack.pop();
break;
}
// popTo
case 3: {
const aValue = popStackValue();
const offsetIntoVariables = command.arg;
const toVar = variables[offsetIntoVariables];
toVar.setValue(aValue);
break;
}
// ==
case 8: {
twoArgCoercingOperator((b, a) => b === a);
break;
}
// !=
case 9: {
twoArgCoercingOperator((b, a) => b !== a);
break;
}
// >
case 10: {
twoArgCoercingOperator((b, a) => b > a);
break;
}
// >=
case 11: {
twoArgCoercingOperator((b, a) => b >= a);
break;
}
// <
case 12: {
twoArgCoercingOperator((b, a) => b < a);
break;
}
// <=
case 13: {
twoArgCoercingOperator((b, a) => b <= a);
break;
}
// jumpIf
case 16: {
const value = popStackValue();
// This seems backwards. Seems like we're doing a "jump if not"
if (value) {
break;
}
ip = command.arg - 1;
break;
}
// jumpIfNot
case 17: {
const value = popStackValue();
// This seems backwards. Same as above
if (!value) {
break;
}
ip = command.arg - 1;
break;
}
// jump
case 18: {
ip = command.arg - 1;
break;
}
// call
// strangeCall (seems to behave just like regular call)
case 24:
case 112: {
const methodOffset = command.arg;
let { name: methodName, typeOffset: classesOffset } = methods[
methodOffset
];
methodName = methodName.toLowerCase();
const klass = classes[classesOffset];
if (!klass) {
throw new Error("Need to add a missing class to runtime");
}
// This is a bit awkward. Because the variables are stored on the stack
// before the object, we have to find the number of arguments without
// actually having access to the object instance.
let argCount = klass.prototype[methodName].length;
const methodArgs = [];
while (argCount--) {
const aValue = popStackValue();
methodArgs.push(aValue);
}
const obj = popStackValue();
stack.push(obj[methodName](...methodArgs));
break;
}
// 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;
}
// return
case 33: {
const aValue = popStackValue();
return aValue;
}
// mov
case 48: {
const a = stack.pop();
const b = stack.pop();
let aValue = a instanceof Variable ? a.getValue() : a;
if (b.type === "INT") {
aValue = Math.floor(aValue);
}
b.setValue(aValue);
stack.push(aValue);
break;
}
// postinc
case 56: {
const a = stack.pop();
const aValue = a.getValue();
a.setValue(aValue + 1);
stack.push(aValue);
break;
}
// postdec
case 57: {
const a = stack.pop();
const aValue = a.getValue();
a.setValue(aValue - 1);
stack.push(aValue);
break;
}
// preinc
case 58: {
const a = stack.pop();
const aValue = a.getValue() + 1;
a.setValue(aValue);
stack.push(aValue);
break;
}
// predec
case 59: {
const a = stack.pop();
const aValue = a.getValue() - 1;
a.setValue(aValue);
stack.push(aValue);
break;
}
// + (add)
case 64: {
twoArgOperator((b, a) => b + a);
break;
}
// - (subtract)
case 65: {
twoArgOperator((b, a) => b - a);
break;
}
// * (multiply)
case 66: {
twoArgOperator((b, a) => b * a);
break;
}
// / (divide)
case 67: {
twoArgOperator((b, a) => b / a);
break;
}
// % (mod)
case 68: {
const a = stack.pop();
const b = stack.pop();
const aValue = a instanceof Variable ? a.getValue() : a;
let bValue = b instanceof Variable ? b.getValue() : b;
// Need to coerce LHS if not int, RHS is always int (enforced by compiler)
if (b.type === "FLOAT" || b.type === "DOUBLE") {
bValue = Math.floor(bValue);
}
stack.push(bValue % aValue);
break;
}
// & (binary and)
case 72: {
twoArgOperator((b, a) => b & a);
break;
}
// | (binary or)
case 73: {
twoArgOperator((b, a) => b | a);
break;
}
// ! (not)
case 74: {
const aValue = popStackValue();
stack.push(aValue ? 0 : 1);
break;
}
// - (negative)
case 76: {
const aValue = popStackValue();
stack.push(-aValue);
break;
}
// logAnd (&&)
case 80: {
twoArgOperator((b, a) => b && a);
break;
}
// logOr ||
case 81: {
twoArgOperator((b, a) => b || a);
break;
}
// <<
case 88: {
twoArgOperator((b, a) => b << a);
break;
}
// >>
case 89: {
twoArgOperator((b, a) => b >> a);
break;
}
// new
case 96: {
const classesOffset = command.arg;
const Klass = classes[classesOffset];
const klassInst = new Klass();
stack.push(klassInst);
break;
}
default:
throw new Error(`Unhandled opcode ${command.opcode}`);
}
ip++;
}
}

View file

@ -1,163 +0,0 @@
import { readFileSync } from "fs";
import { join } from "path";
import System from "../runtime/System";
import runtime from "../runtime";
import interpret from "./interpreter";
import { VERSIONS } from "./testConstants";
function runFile(version, fileName) {
const relativePath = `../../resources/maki_compiler/${version}/${fileName}`;
const system = new System();
const data = readFileSync(join(__dirname, relativePath));
interpret({ runtime, data, system, log: false });
}
// The basicTest.m file that jberg prepared follows a convention for what a
// passing test looks like. This ultility function lets us just write the
// message, since the rest is common for all success messages.
function successOutputFromMessage(message) {
return [message, "Success", 0, ""];
}
let mockMessageBox;
beforeEach(() => {
mockMessageBox = jest.fn();
// The VM depends upon the arity of this function, so we can't use
// `mockMessageBox` directly.
System.prototype.messagebox = function(a, b, c, d) {
mockMessageBox(...arguments);
};
});
describe("can call messageBox with hello World", () => {
const versions = [
// VERSIONS.WINAMP_3_ALPHA,
VERSIONS.WINAMP_3_BETA,
VERSIONS.WINAMP_3_FULL,
VERSIONS.WINAMP_5_02,
VERSIONS.WINAMP_5_66,
];
versions.forEach(version => {
test(`with bytecode compiled by ${version}`, () => {
runFile(version, "hello_world.maki");
expect(mockMessageBox).toHaveBeenCalledTimes(1);
expect(mockMessageBox).toHaveBeenCalledWith(
"Hello World",
"Hello Title",
1,
null
);
});
});
});
describe("can use basic operators", () => {
const versions = [
// jberg could not get the script to compile on this version
// VERSIONS.WINAMP_3_ALPHA,
VERSIONS.WINAMP_3_BETA,
VERSIONS.WINAMP_3_FULL,
VERSIONS.WINAMP_5_02,
VERSIONS.WINAMP_5_66,
];
versions.forEach(version => {
test(`with basic test bytecode compiled by ${version}`, () => {
runFile(version, "basicTests.maki");
expect(mockMessageBox.mock.calls).toEqual(
[
"2 + 2 = 4",
"2.2 + 2.2 = 4.4",
"4 + 4.4 = 4.4 + 4 (not implict casting)",
"#t + #t = 2",
"3 - 2 = 1",
"3 - -2 = 5",
"3.5 - 2 = 1.5",
"2 * 3 = 6",
"2 * 1.5 = 3",
"#t * 3 = 3",
"#f * 3 = 0",
"#t * 0.25 = 0.25",
"0.25 * #t = 0.25",
"#f * 0.25 = 0",
"6 / 3 = 2",
"3 / 2 = 1.5",
"5 % 2 = 1",
"5.5 % 2 = 1 (implict casting)",
"3 & 2 = 2",
"3 | 2 = 3",
"2 << 1 = 4",
"4 >> 1 = 2",
"2.5 << 1 = 4 (implict casting)",
"4.5 >> 1 = 2 (implict casting)",
"1 != 2",
"1 < 2",
"2 > 1",
"[int] 4 = [float] 4.4 (autocasting types)",
"! [float] 4.4 = [int] 4 (not autocasting types)",
"[float] 4.4 != [int] 4 (not autocasting types)",
"! [int] 4 != [float] 4.4 (autocasting types)",
"[int] 4 <= [float] 4.4 (autocasting types)",
"[int] 4 >= [float] 4.4 (autocasting types)",
"! [float] 4.4 <= [int] 4 (not autocasting types)",
"[float] 4.4 >= [int] 4 (not autocasting types)",
"! [int] 4 < [float] 4.4 (autocasting types)",
"! [float] 4.4 < [int] 4 (not autocasting types)",
"! [int] 4 > [float] 4.4 (autocasting types)",
"[float] 4.4 > [int] 4 (not autocasting types)",
"1++ = 1",
"1++ (after incremeent) = 2",
"2-- = 2",
"2-- (after decrement) = 1",
"++1 = 2",
"!#f",
"!0",
"!1 == #f",
"1 == #t",
"0 == #f",
"#t && #t",
"!(#t && #f)",
"!(#f && #f)",
"#t || #t",
"#t || #f",
"#f || #t",
"!(#f || #f)",
"#t || ++n (doesn't short circuit)",
"!(#f && ++ n) (doesn't short circuit)",
].map(successOutputFromMessage)
);
});
});
});
describe("can use simple functions", () => {
const versions = [
// jberg could not get the script to compile on this version
// VERSIONS.WINAMP_3_ALPHA,
VERSIONS.WINAMP_3_BETA,
VERSIONS.WINAMP_3_FULL,
VERSIONS.WINAMP_5_02,
VERSIONS.WINAMP_5_66,
];
versions.forEach(version => {
test(`with bytecode compiled by ${version}`, () => {
runFile(version, "simpleFunctions.maki");
expect(mockMessageBox.mock.calls).toEqual(
[
"simple custom function",
"simple custom function with implicit cast",
"fib(2) == 0 (should be 1)",
"fib(3) == -3 (should be 2)",
"fib(9) == -63 (should be 34)",
"fib(20) == -360 (should be 6765)",
"global num == 0",
"shadow num == 5",
"localNum == 20",
"global num == 0",
"localNum == 20",
].map(successOutputFromMessage)
);
});
});
});

View file

@ -1,310 +1,63 @@
import Variable from "./variable";
import parse from "./parser";
import { getClass, getFormattedId } from "./objects";
import { interpret } from "./interpreter";
function coerceTypes(var1, var2, val1, val2) {
if (var2.type === "INT") {
if (var1.type === "FLOAT" || var1.type === "DOUBLE") {
return Math.floor(val1);
}
// 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) {
val = gen.next();
}
return val1;
return val.value;
}
// 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;
export function run({ runtime, data, system, log, debugHandler }) {
const program = parse(data);
function popStackValue() {
const v = stack.pop();
return v instanceof Variable ? v.getValue() : v;
}
function twoArgCoercingOperator(operator) {
const a = stack.pop();
const b = stack.pop();
let aValue = a instanceof Variable ? a.getValue() : a;
const bValue = b instanceof Variable ? b.getValue() : b;
aValue = coerceTypes(a, b, aValue, bValue);
stack.push(operator(bValue, aValue));
}
function twoArgOperator(operator) {
const aValue = popStackValue();
const bValue = popStackValue();
stack.push(operator(bValue, aValue));
}
// Instruction Pointer
let ip = start;
while (ip < commands.length) {
const command = commands[ip];
// 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: ip, command, stack, variables, commands };
switch (command.opcode) {
// push
case 1: {
const offsetIntoVariables = command.arg;
stack.push(variables[offsetIntoVariables]);
break;
}
// pop
case 2: {
stack.pop();
break;
}
// popTo
case 3: {
const aValue = popStackValue();
const offsetIntoVariables = command.arg;
const toVar = variables[offsetIntoVariables];
toVar.setValue(aValue);
break;
}
// ==
case 8: {
twoArgCoercingOperator((b, a) => b === a);
break;
}
// !=
case 9: {
twoArgCoercingOperator((b, a) => b !== a);
break;
}
// >
case 10: {
twoArgCoercingOperator((b, a) => b > a);
break;
}
// >=
case 11: {
twoArgCoercingOperator((b, a) => b >= a);
break;
}
// <
case 12: {
twoArgCoercingOperator((b, a) => b < a);
break;
}
// <=
case 13: {
twoArgCoercingOperator((b, a) => b <= a);
break;
}
// jumpIf
case 16: {
const value = popStackValue();
// This seems backwards. Seems like we're doing a "jump if not"
if (value) {
break;
}
ip = command.arg - 1;
break;
}
// jumpIfNot
case 17: {
const value = popStackValue();
// This seems backwards. Same as above
if (!value) {
break;
}
ip = command.arg - 1;
break;
}
// jump
case 18: {
ip = command.arg - 1;
break;
}
// call
// strangeCall (seems to behave just like regular call)
case 24:
case 112: {
const methodOffset = command.arg;
let { name: methodName, typeOffset: classesOffset } = methods[
methodOffset
];
methodName = methodName.toLowerCase();
const klass = classes[classesOffset];
if (!klass) {
throw new Error("Need to add a missing class to runtime");
}
// This is a bit awkward. Because the variables are stored on the stack
// before the object, we have to find the number of arguments without
// actually having access to the object instance.
let argCount = klass.prototype[methodName].length;
const methodArgs = [];
while (argCount--) {
const aValue = popStackValue();
methodArgs.push(aValue);
}
const obj = popStackValue();
stack.push(obj[methodName](...methodArgs));
break;
}
// 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;
}
// return
case 33: {
const aValue = popStackValue();
return aValue;
}
// mov
case 48: {
const a = stack.pop();
const b = stack.pop();
let aValue = a instanceof Variable ? a.getValue() : a;
if (b.type === "INT") {
aValue = Math.floor(aValue);
}
b.setValue(aValue);
stack.push(aValue);
break;
}
// postinc
case 56: {
const a = stack.pop();
const aValue = a.getValue();
a.setValue(aValue + 1);
stack.push(aValue);
break;
}
// postdec
case 57: {
const a = stack.pop();
const aValue = a.getValue();
a.setValue(aValue - 1);
stack.push(aValue);
break;
}
// preinc
case 58: {
const a = stack.pop();
const aValue = a.getValue() + 1;
a.setValue(aValue);
stack.push(aValue);
break;
}
// predec
case 59: {
const a = stack.pop();
const aValue = a.getValue() - 1;
a.setValue(aValue);
stack.push(aValue);
break;
}
// + (add)
case 64: {
twoArgOperator((b, a) => b + a);
break;
}
// - (subtract)
case 65: {
twoArgOperator((b, a) => b - a);
break;
}
// * (multiply)
case 66: {
twoArgOperator((b, a) => b * a);
break;
}
// / (divide)
case 67: {
twoArgOperator((b, a) => b / a);
break;
}
// % (mod)
case 68: {
const a = stack.pop();
const b = stack.pop();
const aValue = a instanceof Variable ? a.getValue() : a;
let bValue = b instanceof Variable ? b.getValue() : b;
// Need to coerce LHS if not int, RHS is always int (enforced by compiler)
if (b.type === "FLOAT" || b.type === "DOUBLE") {
bValue = Math.floor(bValue);
}
stack.push(bValue % aValue);
break;
}
// & (binary and)
case 72: {
twoArgOperator((b, a) => b & a);
break;
}
// | (binary or)
case 73: {
twoArgOperator((b, a) => b | a);
break;
}
// ! (not)
case 74: {
const aValue = popStackValue();
stack.push(aValue ? 0 : 1);
break;
}
// - (negative)
case 76: {
const aValue = popStackValue();
stack.push(-aValue);
break;
}
// logAnd (&&)
case 80: {
twoArgOperator((b, a) => b && a);
break;
}
// logOr ||
case 81: {
twoArgOperator((b, a) => b || a);
break;
}
// <<
case 88: {
twoArgOperator((b, a) => b << a);
break;
}
// >>
case 89: {
twoArgOperator((b, a) => b >> a);
break;
}
// new
case 96: {
const classesOffset = command.arg;
const Klass = classes[classesOffset];
const klassInst = new Klass();
stack.push(klassInst);
break;
}
default:
throw new Error(`Unhandled opcode ${command.opcode}`);
// Replace class hashes with actual JavaScript classes from the runtime
program.classes = program.classes.map(hash => {
const resolved = runtime[hash];
if (resolved == null && log) {
const klass = getClass(hash);
console.warn(
`Class missing from runtime: ${hash}`,
klass == null
? `(formatted ID: ${getFormattedId(hash)})`
: `expected ${klass.name}`
);
}
return resolved;
});
ip++;
}
const handler = debugHandler || runGeneratorUntilReturn;
// Bind top level hooks.
program.bindings.forEach(binding => {
const { commandOffset, variableOffset, methodOffset } = binding;
const variable = program.variables[variableOffset];
const method = program.methods[methodOffset];
// const logger = log ? printCommand : logger;
// 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, []));
});
});
const { commands, variables } = program;
commands.forEach((command, i) => {
// printCommand({ i, command, stack: [], variables });
});
// Set the System global
// TODO: We could confirm that this variable has the "system" flag set.
program.variables[0].setValue(system);
system.js_start();
}
export default interpret;