Make interpreter a class

This will make it easier to build a debugger.
This commit is contained in:
Jordan Eldredge 2021-06-05 22:42:30 -07:00
parent 40fb24903b
commit 89cd90a2fa

View file

@ -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++;
}
}