From 89cd90a2fad45278b047badb26dc43bc8c6199c2 Mon Sep 17 00:00:00 2001 From: Jordan Eldredge Date: Sat, 5 Jun 2021 22:42:30 -0700 Subject: [PATCH] Make interpreter a class This will make it easier to build a debugger. --- .../src/maki-interpreter/interpreter.js | 635 +++++++++--------- 1 file changed, 324 insertions(+), 311 deletions(-) diff --git a/packages/webamp-modern/src/maki-interpreter/interpreter.js b/packages/webamp-modern/src/maki-interpreter/interpreter.js index bc38bc45..6a6b8192 100644 --- a/packages/webamp-modern/src/maki-interpreter/interpreter.js +++ b/packages/webamp-modern/src/maki-interpreter/interpreter.js @@ -1,6 +1,330 @@ import Variable from "./variable"; import { isPromise, unimplementedWarning } from "../utils"; +export function interpret(start, program) { + const interpreter = new Interpreter(program); + return interpreter.interpret(start); +} + +class Interpreter { + constructor(program) { + const { commands, methods, variables, classes } = program; + this.commands = commands; + this.methods = methods; + this.variables = variables; + this.classes = classes; + + this.stack = []; + this.callStack = []; + } + + interpret(start) { + // Instruction Pointer + let ip = start; + while (ip < this.commands.length) { + const command = this.commands[ip]; + + switch (command.opcode) { + // push + case 1: { + const offsetIntoVariables = command.arg; + this.stack.push(this.variables[offsetIntoVariables]); + break; + } + // pop + case 2: { + this.stack.pop(); + break; + } + // popTo + case 3: { + const aValue = this.popStackValue(); + const offsetIntoVariables = command.arg; + const toVar = this.variables[offsetIntoVariables]; + toVar.setValue(aValue); + break; + } + // == + case 8: { + this.twoArgCoercingOperator((b, a) => b === a); + break; + } + // != + case 9: { + this.twoArgCoercingOperator((b, a) => b !== a); + break; + } + // > + case 10: { + this.twoArgCoercingOperator((b, a) => b > a); + break; + } + // >= + case 11: { + this.twoArgCoercingOperator((b, a) => b >= a); + break; + } + // < + case 12: { + this.twoArgCoercingOperator((b, a) => b < a); + break; + } + // <= + case 13: { + this.twoArgCoercingOperator((b, a) => b <= a); + break; + } + // jumpIf + case 16: { + const value = this.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 = this.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; + const method = this.methods[methodOffset]; + let methodName = method.name; + const classesOffset = method.typeOffset; + methodName = methodName.toLowerCase(); + + const klass = this.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. + if (!klass.prototype[methodName]) { + throw new Error( + `Need to add missing function (${methodName}) to ${klass.name}` + ); + } + let argCount = klass.prototype[methodName].length; + + const methodArgs = []; + while (argCount--) { + const aValue = this.popStackValue(); + methodArgs.push(aValue); + } + const obj = this.popStackValue(); + let value = obj[methodName](...methodArgs); + if (isPromise(value)) { + throw new Error("Did not expect maki method to return promise"); + } + if (value === null) { + // variables[1] holds global NULL value + value = this.variables[1]; + } + this.stack.push(value); + break; + } + // callGlobal + case 25: { + this.callStack.push(ip); + const offset = command.arg; + + ip = offset - 1; // -1 because we ++ after the switch + break; + } + // return + case 33: { + ip = this.callStack.pop(); + // TODO: Stack protection? + break; + } + // complete + case 40: { + // noop for now + unimplementedWarning("OPCODE: complete"); + break; + } + // mov + case 48: { + const a = this.stack.pop(); + const b = this.stack.pop(); + let aValue = a instanceof Variable ? a.getValue() : a; + if (b.type === "INT") { + aValue = Math.floor(aValue); + } + b.setValue(aValue); + this.stack.push(aValue); + break; + } + // postinc + case 56: { + const a = this.stack.pop(); + const aValue = a.getValue(); + a.setValue(aValue + 1); + this.stack.push(aValue); + break; + } + // postdec + case 57: { + const a = this.stack.pop(); + const aValue = a.getValue(); + a.setValue(aValue - 1); + this.stack.push(aValue); + break; + } + // preinc + case 58: { + const a = this.stack.pop(); + const aValue = a.getValue() + 1; + a.setValue(aValue); + this.stack.push(aValue); + break; + } + // predec + case 59: { + const a = this.stack.pop(); + const aValue = a.getValue() - 1; + a.setValue(aValue); + this.stack.push(aValue); + break; + } + // + (add) + case 64: { + this.twoArgOperator((b, a) => b + a); + break; + } + // - (subtract) + case 65: { + this.twoArgOperator((b, a) => b - a); + break; + } + // * (multiply) + case 66: { + this.twoArgOperator((b, a) => b * a); + break; + } + // / (divide) + case 67: { + this.twoArgOperator((b, a) => b / a); + break; + } + // % (mod) + case 68: { + const a = this.stack.pop(); + const b = this.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); + } + this.stack.push(bValue % aValue); + break; + } + // & (binary and) + case 72: { + this.twoArgOperator((b, a) => b & a); + break; + } + // | (binary or) + case 73: { + this.twoArgOperator((b, a) => b | a); + break; + } + // ! (not) + case 74: { + const aValue = this.popStackValue(); + this.stack.push(aValue ? 0 : 1); + break; + } + // - (negative) + case 76: { + const aValue = this.popStackValue(); + this.stack.push(-aValue); + break; + } + // logAnd (&&) + case 80: { + this.twoArgOperator((b, a) => b && a); + break; + } + // logOr || + case 81: { + this.twoArgOperator((b, a) => b || a); + break; + } + // << + case 88: { + this.twoArgOperator((b, a) => b << a); + break; + } + // >> + case 89: { + this.twoArgOperator((b, a) => b >> a); + break; + } + // new + case 96: { + const classesOffset = command.arg; + const Klass = this.classes[classesOffset]; + const system = this.variables[0].getValue(); + const klassInst = new Klass(null, system.getscriptgroup()); + this.stack.push(klassInst); + break; + } + // delete + case 97: { + const aValue = this.popStackValue(); + aValue.js_delete(); + break; + } + default: + throw new Error(`Unhandled opcode ${command.opcode}`); + } + + ip++; + } + } + + popStackValue() { + const v = this.stack.pop(); + return v instanceof Variable ? v.getValue() : v; + } + + twoArgCoercingOperator(operator) { + const a = this.stack.pop(); + const b = this.stack.pop(); + let aValue = a instanceof Variable ? a.getValue() : a; + const bValue = b instanceof Variable ? b.getValue() : b; + + aValue = coerceTypes(a, b, aValue, bValue); + this.stack.push(operator(bValue, aValue)); + } + + twoArgOperator(operator) { + const aValue = this.popStackValue(); + const bValue = this.popStackValue(); + + this.stack.push(operator(bValue, aValue)); + } +} + function coerceTypes(var1, var2, val1 /* val2 */) { if (var2.type === "INT") { if (var1.type === "FLOAT" || var1.type === "DOUBLE") { @@ -10,314 +334,3 @@ function coerceTypes(var1, var2, val1 /* val2 */) { return val1; } - -export function interpret(start, program) { - const { commands, methods, variables, classes } = program; - - const stack = []; - const callStack = []; - - 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]; - - 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; - const method = methods[methodOffset]; - let methodName = method.name; - const classesOffset = method.typeOffset; - 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. - if (!klass.prototype[methodName]) { - throw new Error( - `Need to add missing function (${methodName}) to ${klass.name}` - ); - } - let argCount = klass.prototype[methodName].length; - - const methodArgs = []; - while (argCount--) { - const aValue = popStackValue(); - methodArgs.push(aValue); - } - const obj = popStackValue(); - let value = obj[methodName](...methodArgs); - if (isPromise(value)) { - throw new Error("Did not expect maki method to return promise"); - } - if (value === null) { - // variables[1] holds global NULL value - value = variables[1]; - } - stack.push(value); - break; - } - // callGlobal - case 25: { - callStack.push(ip); - const offset = command.arg; - - ip = offset - 1; // -1 because we ++ after the switch - break; - } - // return - case 33: { - ip = callStack.pop(); - // TODO: Stack protection? - break; - } - // complete - case 40: { - // noop for now - unimplementedWarning("OPCODE: complete"); - break; - } - // 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 system = variables[0].getValue(); - const klassInst = new Klass(null, system.getscriptgroup()); - stack.push(klassInst); - break; - } - // delete - case 97: { - const aValue = popStackValue(); - aValue.js_delete(); - break; - } - default: - throw new Error(`Unhandled opcode ${command.opcode}`); - } - - ip++; - } -}