mirror of
https://github.com/captbaritone/webamp.git
synced 2026-07-28 12:36:35 +00:00
Swap the name of the VM and the interpreter (#843)
This commit is contained in:
parent
d8b33e4795
commit
46042d0748
5 changed files with 362 additions and 529 deletions
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -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++;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue