Rework interpreter to use tagged union for variables

This commit is contained in:
Jordan Eldredge 2021-06-14 22:25:51 -07:00
parent a9466f878c
commit 1e86945f1e
17 changed files with 790 additions and 571 deletions

1
packages/webamp-modern-2/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
build/

View file

@ -1,11 +1,13 @@
# TODO Next
- [ ] SystemObject.getruntimeversion
- [ ] SystemObject.getskinname
- [ ] Ensure only wrapped variables go on the stack
- [ ] I suspect that this will require type-aware coersion
Meanding we don't just always convert to number, but match the type coersion of Maki?
- [ ] What is the root node?
- [ ] Where do Layers actually go?
- [ ] Where are scripts initialized?
- [ ]
---
- [ ] When parsing skins, where is the root state accumulated?
- [ ] How do includes work when parsing skins? Do they create new context?

View file

@ -10,6 +10,10 @@ async function main() {
await parser.parse();
for (const container of parser._containers) {
container.init();
}
for (const container of parser._containers) {
document.body.appendChild(container.getDebugDom());
}

View file

@ -0,0 +1,71 @@
// Holds a buffer and a pointer. Consumers can consume bytesoff the end of the
// file. When we want to run in the browser, we can refactor this class to use a
// typed array: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Typed_arrays
export default class MakiFile {
_arr: Uint8Array;
_i: number;
constructor(data: ArrayBuffer) {
this._arr = new Uint8Array(data);
this._i = 0;
}
readInt32LE(): number {
const offset = this._i >>> 0;
this._i += 4;
return (
this._arr[offset] |
(this._arr[offset + 1] << 8) |
(this._arr[offset + 2] << 16) |
(this._arr[offset + 3] << 24)
);
}
readUInt32LE(): number {
const int = this.peekUInt32LE();
this._i += 4;
return int;
}
peekUInt32LE(): number {
const offset = this._i >>> 0;
return (
(this._arr[offset] |
(this._arr[offset + 1] << 8) |
(this._arr[offset + 2] << 16)) +
this._arr[offset + 3] * 0x1000000
);
}
readUInt16LE(): number {
const offset = this._i >>> 0;
this._i += 2;
return this._arr[offset] | (this._arr[offset + 1] << 8);
}
readUInt8(): number {
const int = this._arr[this._i];
this._i++;
return int;
}
readStringOfLength(length: number): string {
let ret = "";
const end = Math.min(this._arr.length, this._i + length);
for (let i = this._i; i < end; ++i) {
ret += String.fromCharCode(this._arr[i]);
}
this._i += length;
return ret;
}
readString(): string {
return this.readStringOfLength(this.readUInt16LE());
}
getPosition(): number {
return this._i;
}
}

View file

@ -1,360 +0,0 @@
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: {
const a = this.stack.pop();
const b = this.stack.pop();
switch (a.type) {
case "INT":
case "FLOAT":
case "DOUBLE":
case "BOOLEAN": {
break;
}
case "STRING": {
break;
}
default:
throw new Error(`Unexpected type: ${a}`);
}
let aValue = this.getValue(a);
const bValue = this.getValue(b);
aValue = this.coerceTypes__DEPRECATED(a, b);
const result = Variable.newInt(bValue === aValue);
this.stack.push(result);
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++;
}
}
getValue(v) {
return v instanceof Variable ? v.getValue() : v;
}
popStackValue() {
const v = this.stack.pop();
return this.getValue(v);
}
twoArgCoercingOperator(operator) {
const a = this.stack.pop();
const b = this.stack.pop();
let aValue = this.getValue(a);
const bValue = this.getValue(b);
aValue = this.coerceTypes__DEPRECATED(a, b);
this.stack.push(operator(bValue, aValue));
}
twoArgOperator(operator) {
const aValue = this.popStackValue();
const bValue = this.popStackValue();
this.stack.push(operator(bValue, aValue));
}
coerceTypes__DEPRECATED(var1, var2) {
if (var2.type === "INT") {
if (var1.type === "FLOAT" || var1.type === "DOUBLE") {
return Math.floor(this.getValue(var1));
}
}
return this.getValue(var1);
}
}

View file

@ -0,0 +1,513 @@
import { V, Variable } from "./v";
import { assert, assume } from "../utils";
import { ParsedMaki, Command, Method } from "./parser";
export function interpret(
start: number,
program: ParsedMaki,
classResolver: (guid: string) => any
) {
const interpreter = new Interpreter(program, classResolver);
return interpreter.interpret(start);
}
class Interpreter {
stack: Variable[];
callStack: number[];
classes: string[];
variables: Variable[];
methods: Method[];
commands: Command[];
classResolver: (guid: string) => any;
constructor(program: ParsedMaki, classResolver: (guid: string) => any) {
const { commands, methods, variables, classes } = program;
this.classResolver = classResolver;
this.commands = commands;
this.methods = methods;
this.variables = variables;
this.classes = classes;
this.stack = [];
this.callStack = [];
}
interpret(start: number) {
// 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.stack.pop();
const offsetIntoVariables = command.arg;
const current = this.variables[offsetIntoVariables];
assume(
aValue.type === current.type,
"Assigned from one type to a different type."
);
current.value = aValue.value;
break;
}
// ==
case 8: {
const a = this.stack.pop();
const b = this.stack.pop();
assume(
a.type == b.type,
`Tried to compare a ${a.type} to a ${b.type}.`
);
const result = V.newInt(a.value === b.value);
this.stack.push(result);
break;
}
// !=
case 9: {
const a = this.stack.pop();
const b = this.stack.pop();
assume(
a.type == b.type,
`Tried to compare a ${a.type} to a ${b.type}.`
);
const result = V.newInt(a.value !== b.value);
this.stack.push(result);
break;
}
// >
case 10: {
const a = this.stack.pop();
const b = this.stack.pop();
switch (a.type) {
case "STRING":
case "OBJECT":
case "BOOL":
case "NULL":
throw new Error("Tried to add non-numbers.");
}
switch (b.type) {
case "STRING":
case "OBJECT":
case "BOOL":
case "NULL":
throw new Error("Tried to add non-numbers.");
}
this.stack.push(V.newInt(a.value > b.value));
break;
}
// >=
case 11: {
assume(false, "Unimplimented >= operator");
break;
}
// <
case 12: {
const a = this.stack.pop();
const b = this.stack.pop();
switch (a.type) {
case "STRING":
case "OBJECT":
case "BOOL":
case "NULL":
throw new Error("Tried to add non-numbers.");
}
switch (b.type) {
case "STRING":
case "OBJECT":
case "BOOL":
case "NULL":
throw new Error("Tried to add non-numbers.");
}
this.stack.push(V.newInt(a.value < b.value));
break;
}
// <=
case 13: {
assume(false, "Unimplimented <= operator");
break;
}
// jumpIf
case 16: {
const value = this.stack.pop();
// 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.stack.pop();
// 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 returnType = method.returnType;
const classesOffset = method.typeOffset;
methodName = methodName.toLowerCase();
const guid = this.classes[classesOffset];
const klass = this.classResolver(guid);
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 method "${methodName}" to ${klass.name}`
);
}
let argCount = klass.prototype[methodName].length;
const methodArgs = [];
while (argCount--) {
const a = this.stack.pop();
methodArgs.push(a.value);
}
const obj = this.stack.pop();
assert(
obj.type === "OBJECT",
"Tried to call a method on a primitive."
);
let value = obj.value[methodName](...methodArgs);
if (typeof value.then === "function") {
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({ type: returnType, value } as any);
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
assume(false, "OPCODE: complete");
break;
}
// mov
case 48: {
const a = this.stack.pop();
const b = this.stack.pop();
assume(
a.type === b.type,
`Type mismatch: ${a.type} != ${b.type} at ip: ${ip}`
);
b.value = a.value;
this.stack.push(a);
break;
}
// postinc
case 56: {
const a = this.stack.pop();
switch (a.type) {
case "STRING":
case "OBJECT":
case "BOOL":
case "NULL":
throw new Error("Tried to increment a non-number.");
}
const aValue = a.value;
a.value = aValue + 1;
this.stack.push({ type: a.type, value: aValue });
break;
}
// postdec
case 57: {
const a = this.stack.pop();
switch (a.type) {
case "STRING":
case "OBJECT":
case "BOOL":
case "NULL":
throw new Error("Tried to decrement a non-number.");
}
const aValue = a.value;
a.value = aValue - 1;
this.stack.push({ type: a.type, value: aValue });
break;
}
// preinc
case 58: {
const a = this.stack.pop();
switch (a.type) {
case "STRING":
case "OBJECT":
case "BOOL":
case "NULL":
throw new Error("Tried to increment a non-number.");
}
a.value++;
this.stack.push(a);
break;
}
// predec
case 59: {
const a = this.stack.pop();
switch (a.type) {
case "STRING":
case "OBJECT":
case "BOOL":
case "NULL":
throw new Error("Tried to increment a non-number.");
}
a.value--;
this.stack.push(a);
break;
}
// + (add)
case 64: {
const a = this.stack.pop();
const b = this.stack.pop();
switch (a.type) {
case "STRING":
case "OBJECT":
case "BOOL":
case "NULL":
throw new Error("Tried to add non-numbers.");
}
switch (b.type) {
case "STRING":
case "OBJECT":
case "BOOL":
throw new Error("Tried to add non-numbers.");
}
// TODO: Do we need to round the value if INT?
this.stack.push({ type: a.type, value: a.value + b.value });
break;
}
// - (subtract)
case 65: {
const a = this.stack.pop();
const b = this.stack.pop();
switch (a.type) {
case "STRING":
case "OBJECT":
case "BOOL":
case "NULL":
throw new Error("Tried to add non-numbers.");
}
switch (b.type) {
case "STRING":
case "OBJECT":
case "BOOL":
case "NULL":
throw new Error("Tried to add non-numbers.");
}
// TODO: Do we need to round the value if INT?
this.stack.push({ type: a.type, value: a.value - b.value });
break;
}
// * (multiply)
case 66: {
const a = this.stack.pop();
const b = this.stack.pop();
switch (a.type) {
case "STRING":
case "OBJECT":
case "BOOL":
case "NULL":
throw new Error("Tried to add non-numbers.");
}
switch (b.type) {
case "STRING":
case "OBJECT":
case "BOOL":
case "NULL":
throw new Error("Tried to add non-numbers.");
}
// TODO: Do we need to round the value if INT?
this.stack.push({ type: a.type, value: a.value * b.value });
break;
}
// / (divide)
case 67: {
const a = this.stack.pop();
const b = this.stack.pop();
switch (a.type) {
case "STRING":
case "OBJECT":
case "BOOL":
case "NULL":
throw new Error("Tried to add non-numbers.");
}
switch (b.type) {
case "STRING":
case "OBJECT":
case "BOOL":
throw new Error("Tried to add non-numbers.");
}
// TODO: Do we need to round the value if INT?
this.stack.push({ type: a.type, value: a.value / b.value });
break;
}
// % (mod)
case 68: {
const a = this.stack.pop();
const b = this.stack.pop();
switch (a.type) {
case "STRING":
case "OBJECT":
case "BOOL":
case "NULL":
throw new Error("Tried to add non-numbers.");
}
switch (b.type) {
case "STRING":
case "OBJECT":
case "BOOL":
throw new Error("Tried to add non-numbers.");
// Need to coerce LHS if not int, RHS is always int (enforced by compiler)
case "FLOAT":
case "DOUBLE":
const value = Math.floor(b.value) % a.value;
this.stack.push({ type: a.type, value });
break;
case "INT":
this.stack.push({ type: a.type, value: b.value % a.value });
break;
}
break;
}
// & (binary and)
case 72: {
assume(false, "Unimplimented & operator");
break;
}
// | (binary or)
case 73: {
assume(false, "Unimplimented | operator");
break;
}
// ! (not)
case 74: {
const a = this.stack.pop();
switch (a.type) {
case "STRING":
case "OBJECT":
throw new Error("Tried ! a string or object.");
}
this.stack.push(V.newInt(!a.value));
break;
}
// - (negative)
case 76: {
const a = this.stack.pop();
switch (a.type) {
case "STRING":
case "OBJECT":
case "BOOL":
case "NULL":
throw new Error("Tried to add non-numbers.");
}
this.stack.push({ type: a.type, value: -a.value });
break;
}
// logAnd (&&)
case 80: {
assume(false, "Unimplimented && operator");
break;
}
// logOr ||
case 81: {
const a = this.stack.pop();
const b = this.stack.pop();
// Some of these are probably valid, but we'll enable them once we see usage.
switch (a.type) {
case "STRING":
case "OBJECT":
case "BOOL":
case "NULL":
throw new Error("Tried to add non-numbers.");
}
switch (b.type) {
case "STRING":
case "OBJECT":
case "BOOL":
case "NULL":
throw new Error("Tried to add non-numbers.");
}
if (a.value) {
return a;
}
return b;
}
// <<
case 88: {
assume(false, "Unimplimented << operator");
break;
}
// >>
case 89: {
assume(false, "Unimplimented >> operator");
break;
}
// new
case 96: {
const classesOffset = command.arg;
const guid = this.classes[classesOffset];
const Klass = this.classResolver(guid);
const klassInst = new Klass();
this.stack.push(klassInst);
break;
}
// delete
case 97: {
const aValue = this.stack.pop();
// TODO: Cleanup the object?
break;
}
default:
throw new Error(`Unhandled opcode ${command.opcode}`);
}
ip++;
}
}
}

View file

@ -1,4 +1,4 @@
const std = require("./std.json");
import std from "./std.json";
const NAME_TO_DEF = {};
@ -60,4 +60,4 @@ Found 1 error.
*/
module.exports = std;
export default std;

View file

@ -1,13 +1,57 @@
const stdPatched = require("./objectData/stdPatched");
const pldir = require("./objectData/pldir.json");
const config = require("./objectData/config.json");
import stdPatched from "./objectData/stdPatched";
import pldir from "./objectData/pldir.json";
import config from "./objectData/config.json";
import { DataType } from "./v";
const objects = { ...stdPatched, ...pldir, ...config };
type MethodDefinition = {
name: string;
result: string;
parameters: string[][];
};
type ObjectDefinition = {
parent: string;
name: string;
functions: MethodDefinition[];
parentClass?: ObjectDefinition;
};
const objects: { [key: string]: ObjectDefinition } = {
...stdPatched,
...pldir,
...config,
};
export function getClass(id: String): ObjectDefinition {
return normalizedObjects[getFormattedId(id)];
}
export function getReturnType(classId: string, methodName: string): DataType {
const method = getMethod(classId, methodName);
const upper = method.result.toUpperCase();
switch (upper) {
case "INT":
case "DOUBLE":
case "STRING":
case "FLOAT":
case "BOOL":
return upper as any;
case "":
return "NULL" as any;
default:
return "OBJECT" as any;
}
}
function getMethod(classId: string, methodName: string): MethodDefinition {
const klass = getClass(classId);
return getObjectFunction(klass, methodName);
}
// TODO: We could probably just fix the keys used in this file to already be normalized
// We might even want to normalize the to match the formatting we get out the file. That could
// avoid the awkward regex inside `getClass()`.
const normalizedObjects = {};
const normalizedObjects: { [key: string]: ObjectDefinition } = {};
Object.keys(objects).forEach((key) => {
normalizedObjects[key.toLowerCase()] = objects[key];
});
@ -37,14 +81,14 @@ function getFormattedId(id) {
return formattedId.toLowerCase();
}
function getClass(id) {
return normalizedObjects[getFormattedId(id)];
}
function getObjectFunction(klass, functionName) {
function getObjectFunction(
klass: ObjectDefinition,
functionName: string
): MethodDefinition {
const lowerName = functionName.toLowerCase();
const method = klass.functions.find((func) => {
// TODO: This could probably be normalized at load time, or evern sooner.
return func.name.toLowerCase() === functionName.toLowerCase();
return func.name.toLowerCase() === lowerName;
});
if (method != null) {
return method;
@ -54,25 +98,3 @@ function getObjectFunction(klass, functionName) {
}
return getObjectFunction(klass.parentClass, functionName);
}
function getFunctionObject(klass, functionName) {
const method = klass.functions.find((func) => {
// TODO: This could probably be normalized at load time, or evern sooner.
return func.name.toLowerCase() === functionName.toLowerCase();
});
if (method != null) {
return klass;
}
if (klass.parentClass == null) {
throw new Error(`Could not find method ${functionName} on ${klass.name}.`);
}
return getFunctionObject(klass.parentClass, functionName);
}
module.exports = {
objects,
getFormattedId,
getClass,
getObjectFunction,
getFunctionObject,
};

View file

@ -1,5 +1,32 @@
import { assert, assume } from "../utils";
import { COMMANDS } from "./constants";
import Variable from "./variable";
import { DataType, Variable } from "./v";
import MakiFile from "./MakiFile";
import { getMethod, getReturnType } from "./objects";
export type Command = {
opcode: number;
arg: number;
};
export type Method = {
name: string;
typeOffset: number;
returnType: DataType;
};
export type ParsedMaki = {
commands: Command[];
methods: Method[];
variables: Variable[];
classes: string[];
bindings: Binding[];
version: number;
};
export type Binding = {
commandOffset: number;
};
const MAGIC = "FG";
@ -11,7 +38,7 @@ const PRIMITIVE_TYPES = {
6: "STRING",
};
export function parse(data) {
export function parse(data: ArrayBuffer): ParsedMaki {
const makiFile = new MakiFile(data);
const magic = readMagic(makiFile);
@ -22,7 +49,7 @@ export function parse(data) {
// Maybe it's additional version info?
const extraVersion = makiFile.readUInt32LE();
const classes = readClasses(makiFile);
const methods = readMethods(makiFile);
const methods = readMethods(makiFile, classes);
const variables = readVariables({ makiFile, classes });
readConstants({ makiFile, variables });
const bindings = readBindings(makiFile);
@ -42,33 +69,30 @@ export function parse(data) {
}
});
const resolvedBindings = bindings.map((binding) => {
const resolvedBindings = bindings.map((binding): Binding => {
return Object.assign({}, binding, {
commandOffset: offsetToCommand[binding.binaryOffset],
binaryOffset: undefined,
});
});
const resolvedCommands = commands.map((command) => {
const resolvedCommands = commands.map((command): Command => {
if (command.argType === "COMMAND_OFFSET") {
return Object.assign({}, command, { arg: offsetToCommand[command.arg] });
}
return command;
});
return {
magic,
classes,
methods,
variables,
bindings: resolvedBindings,
commands: resolvedCommands,
version,
extraVersion,
};
}
// TODO: Don't depend upon COMMANDS
function opcodeToArgType(opcode) {
function opcodeToArgType(opcode: number) {
const command = COMMANDS[opcode];
if (command == null) {
throw new Error(`Unknown opcode ${opcode}`);
@ -87,77 +111,7 @@ function opcodeToArgType(opcode) {
}
}
// Holds a buffer and a pointer. Consumers can consume bytesoff the end of the
// file. When we want to run in the browser, we can refactor this class to use a
// typed array: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Typed_arrays
class MakiFile {
constructor(data) {
this._arr = new Uint8Array(data);
this._i = 0;
}
readInt32LE() {
const offset = this._i >>> 0;
this._i += 4;
return (
this._arr[offset] |
(this._arr[offset + 1] << 8) |
(this._arr[offset + 2] << 16) |
(this._arr[offset + 3] << 24)
);
}
readUInt32LE() {
const int = this.peekUInt32LE();
this._i += 4;
return int;
}
peekUInt32LE() {
const offset = this._i >>> 0;
return (
(this._arr[offset] |
(this._arr[offset + 1] << 8) |
(this._arr[offset + 2] << 16)) +
this._arr[offset + 3] * 0x1000000
);
}
readUInt16LE() {
const offset = this._i >>> 0;
this._i += 2;
return this._arr[offset] | (this._arr[offset + 1] << 8);
}
readUInt8() {
const int = this._arr[this._i];
this._i++;
return int;
}
readStringOfLength(length) {
let ret = "";
const end = Math.min(this._arr.length, this._i + length);
for (let i = this._i; i < end; ++i) {
ret += String.fromCharCode(this._arr[i]);
}
this._i += length;
return ret;
}
readString() {
return this.readStringOfLength(this.readUInt16LE());
}
getPosition() {
return this._i;
}
}
function readMagic(makiFile) {
function readMagic(makiFile: MakiFile): string {
const magic = makiFile.readStringOfLength(MAGIC.length);
if (magic !== MAGIC) {
throw new Error(
@ -167,12 +121,12 @@ function readMagic(makiFile) {
return magic;
}
function readVersion(makiFile) {
function readVersion(makiFile: MakiFile): number {
// No idea what we're actually expecting here.
return makiFile.readUInt16LE();
}
function readClasses(makiFile) {
function readClasses(makiFile: MakiFile): string[] {
let count = makiFile.readUInt32LE();
const classes = [];
while (count--) {
@ -186,9 +140,9 @@ function readClasses(makiFile) {
return classes;
}
function readMethods(makiFile) {
function readMethods(makiFile: MakiFile, classes: string[]): Method[] {
let count = makiFile.readUInt32LE();
const methods = [];
const methods: Method[] = [];
while (count--) {
const classCode = makiFile.readUInt16LE();
// Offset into our parsed types
@ -196,7 +150,12 @@ function readMethods(makiFile) {
// This is probably the second half of a uint32
makiFile.readUInt16LE();
const name = makiFile.readString();
methods.push({ name, typeOffset });
const className = classes[typeOffset];
const returnType = getReturnType(className, name);
methods.push({ name, typeOffset, returnType });
}
return methods;
}
@ -220,17 +179,14 @@ function readVariables({ makiFile, classes }) {
if (variable == null) {
throw new Error("Invalid type");
}
variables.push(
new Variable({ type: variable, typeName: "SUBCLASS", global: !!global })
);
// assume(false, "Unimplemented subclass variable type");
variables.push({ type: "OBJECT", value: object });
} else if (object) {
const klass = classes[typeOffset];
if (klass == null) {
throw new Error("Invalid type");
}
variables.push(
new Variable({ type: klass, typeName: "OBJECT", global: !!global })
);
variables.push({ type: "OBJECT", value: object });
} else {
const typeName = PRIMITIVE_TYPES[typeOffset];
if (typeName == null) {
@ -259,12 +215,10 @@ function readVariables({ makiFile, classes }) {
default:
throw new Error("Invalid primitive type");
}
const variable = new Variable({
const variable = {
type: typeName,
typeName,
global: !!global,
});
variable.setValue(value);
value,
};
variables.push(variable);
}
}
@ -279,7 +233,7 @@ function readConstants({ makiFile, variables }) {
// TODO: Assert this is of type string.
const value = makiFile.readString();
// TODO: Don't mutate
variable.setValue(value);
variable.value = value;
}
}
@ -346,8 +300,7 @@ function parseComand({ start, makiFile, length }) {
makiFile.peekUInt32LE() >= 0xffff0000 &&
makiFile.peekUInt32LE() <= 0xffff000f
) {
command.foo = true;
command.stackProtection = makiFile.readUInt32LE();
// command.stackProtection = makiFile.readUInt32LE();
}
// TODO: What even is this?

View file

@ -1,55 +0,0 @@
import Variable from "./variable";
import runtime from "../runtime";
// Debug utility to pretty print a value/variable
function printValue(value) {
if (!(value instanceof Variable)) {
return value;
}
const variable = value;
let type = "UNKOWN";
switch (variable.typeName) {
case "OBJECT":
const obj = runtime[variable.type];
if (obj == null) {
type = "Unknown object";
} else {
type = obj.getclassname();
}
break;
case "STRING":
const str = variable.getValue();
type = `STRING(${str})`;
break;
case "INT":
type = "INT";
break;
case "FLOAT":
type = "FLOAT";
break;
case "DOUBLE":
type = "DOUBLE";
break;
case "BOOLEAN":
type = "BOOLEAN";
break;
default:
throw new Error(`Unknown variable type ${variable.typeName}`);
}
return `Variable(${type})`;
}
function printCommand({ i, command, stack, variables }) {
console.log(
`${i} (${command.start} + ${command.offset})`,
command.command.name.toUpperCase(),
command.opcode,
printValue(variables[command.arg])
);
stack.forEach((value, j) => {
const name = printValue(value, { runtime });
console.log(" ", j + 1, name);
});
}
export default { printCommand };

View file

@ -0,0 +1,32 @@
import GuiObj from "../skin/GuiObj";
export type Variable =
| {
type: "BOOL";
value: boolean;
}
| {
type: "INT" | "FLOAT" | "DOUBLE";
value: number;
}
| {
type: "STRING";
value: string;
}
| {
type: "OBJECT";
value: GuiObj;
}
| {
type: "NULL";
value: null;
};
export type DataType = Pick<Variable, "type">;
export const V = {
// TODO: Split boolean out into its own method
newInt(value: number | boolean): Variable {
return { type: "INT", value: Number(value) };
},
};

View file

@ -1,17 +0,0 @@
class Variable {
constructor({ type, typeName, global }) {
this.type = type;
this.typeName = typeName;
this.global = global;
}
getValue() {
return this._value;
}
setValue(value) {
this._value = value;
}
}
export default Variable;

View file

@ -31,6 +31,12 @@ export default class Container extends XmlObj {
return true;
}
init() {
for (const layout of this._layouts) {
layout.init();
}
}
addLayout(layout: Layout) {
layout.setParent(this);
this._layouts.push(layout);

View file

@ -43,6 +43,15 @@ export default class Group extends GuiObj {
return true;
}
init() {
for (const systemObject of this._systemObjects) {
systemObject.init();
}
for (const child of this._children) {
child.init();
}
}
addSystemObject(systemObj: SystemObject) {
systemObj.setParentGroup(this);
this._systemObjects.push(systemObj);

View file

@ -40,6 +40,10 @@ export default class GuiObj extends XmlObj {
return true;
}
init() {
// pass
}
getDebugDom(): HTMLDivElement {
const div = window.document.createElement("div");
div.style.display = this._visible ? "inline-block" : "none";

View file

@ -1,6 +1,8 @@
import Group from "./Group";
import { interpret } from "../maki/interpreter";
import { getClass } from "../maki/objects";
import { ParsedMaki } from "../maki/parser";
type ParsedMaki = Object; // Not typed yet.
import Group from "./Group";
export default class SystemObject {
_parentGroup: Group;
@ -10,7 +12,39 @@ export default class SystemObject {
this._parsedScript = parsedScript;
}
init() {
const initialVariable = this._parsedScript.variables[0];
if (initialVariable.type !== "OBJECT") {
throw new Error("First variable was not SystemObject.");
}
initialVariable.value = this;
// TODO: How should we setup bindings?
// console.log(this._parsedScript.bindings);
interpret(0, this._parsedScript, classResover);
}
setParentGroup(group: Group) {
this._parentGroup = group;
}
/* Required for Maki */
getruntimeversion(): number {
return 5.666;
}
getskinname() {
return "TODO: Get the Real skin name";
}
}
function classResover(guid: string): any {
switch (guid) {
case "d6f50f6449b793fa66baf193983eaeef":
return SystemObject;
default:
console.log();
throw new Error(
`Unresolvable class "${getClass(guid).name}" (guid: ${guid})`
);
}
}