From dcbcd20a65f766c7c1bdfbb474566d9b1d8635d5 Mon Sep 17 00:00:00 2001 From: Jordan Eldredge Date: Thu, 11 Jul 2019 20:57:18 -0700 Subject: [PATCH] Implement more operators and add debugger --- experiments/modern/package.json | 7 +- experiments/modern/src/debugger/Command.js | 23 +++ experiments/modern/src/debugger/Value.js | 15 ++ experiments/modern/src/debugger/Variable.js | 32 ++++ experiments/modern/src/debugger/index.js | 177 ++++++++++++++++++ experiments/modern/src/index.js | 16 +- .../src/maki-interpreter/interpreter.js | 2 +- .../src/maki-interpreter/interpreter.test.js | 20 +- .../src/maki-interpreter/virtualMachine.js | 93 ++++++++- experiments/modern/yarn.lock | 27 ++- 10 files changed, 392 insertions(+), 20 deletions(-) create mode 100644 experiments/modern/src/debugger/Command.js create mode 100644 experiments/modern/src/debugger/Value.js create mode 100644 experiments/modern/src/debugger/Variable.js create mode 100644 experiments/modern/src/debugger/index.js diff --git a/experiments/modern/package.json b/experiments/modern/package.json index f4a4288f..1a5d15a1 100644 --- a/experiments/modern/package.json +++ b/experiments/modern/package.json @@ -6,6 +6,7 @@ "jszip": "^3.2.1", "react": "^16.8.6", "react-dom": "^16.8.6", + "react-dropzone": "^10.1.5", "react-scripts": "3.0.1", "xml-js": "^1.6.11" }, @@ -14,9 +15,9 @@ "build": "react-scripts build", "test": "react-scripts test --no-watchman", "eject": "react-scripts eject", - "decompile": "cd src/maki-interpreter/reference/maki_decompiler_1.1 && perl mdc.pl ../../fixtures/standardframe.maki", - "interpret": "node ./src/maki-interpreter/cli.js ./fixtures/standardframe.maki", - "debug": "node --inspect-brk ./src/maki-interpreter/cli.js ./fixtures/standardframe.maki" + "decompile": "cd src/maki-interpreter/reference/maki_decompiler_1.1 && perl mdc.pl \"../../reference/maki_compiler/v1.1.1.b3 (Winamp 3.0 full)/basicTests.maki\"", + "interpret": "node ./src/maki-interpreter/cli.js \"./reference/maki_compiler/v1.1.13 (Winamp 5.02)/hello_world.maki\"", + "debug": "node --inspect-brk ./src/maki-interpreter/cli.js ./reference/maki_compiler/v1.1.13 (Winamp 5.02)/hello_world.maki" }, "eslintConfig": { "extends": "react-app" diff --git a/experiments/modern/src/debugger/Command.js b/experiments/modern/src/debugger/Command.js new file mode 100644 index 00000000..df422340 --- /dev/null +++ b/experiments/modern/src/debugger/Command.js @@ -0,0 +1,23 @@ +import React from "react"; +import Variable from "./Variable"; +export default function Command({ command, variables }) { + const arg = command.arguments[0]; + let foo = null; + switch (command.opcode) { + case 1: + foo = ( + + Variable( + ) + + ); + break; + default: + foo = null; + } + return ( + <> + ({command.opcode}) {command.command.name.toUpperCase()} {foo} + + ); +} diff --git a/experiments/modern/src/debugger/Value.js b/experiments/modern/src/debugger/Value.js new file mode 100644 index 00000000..78e46ddc --- /dev/null +++ b/experiments/modern/src/debugger/Value.js @@ -0,0 +1,15 @@ +import React from "react"; +import VariableClass from "../maki-interpreter/variable"; +import Variable from "./Variable"; + +export default function Value({ value }) { + if (value instanceof VariableClass) { + return ( + <> + Variable( + ) + + ); + } + return value ? value.toString() : null; +} diff --git a/experiments/modern/src/debugger/Variable.js b/experiments/modern/src/debugger/Variable.js new file mode 100644 index 00000000..8029335a --- /dev/null +++ b/experiments/modern/src/debugger/Variable.js @@ -0,0 +1,32 @@ +import runtime from "../maki-interpreter/runtime"; + +function quote(str) { + return `"${str}"`; +} + +export default function Variable({ variable }) { + 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 value = variable.getValue(); + return `${variable.typeName}(${value == null ? "NULL" : quote(value)})`; + } + case "INT": + case "FLOAT": + case "DOUBLE": + case "BOOLEAN": + const value = variable.getValue(); + return `${variable.typeName}(${value == null ? "NULL" : value})`; + default: + throw new Error(`Unknown variable type ${variable.typeName}`); + } + return type; +} diff --git a/experiments/modern/src/debugger/index.js b/experiments/modern/src/debugger/index.js new file mode 100644 index 00000000..a89bd7f5 --- /dev/null +++ b/experiments/modern/src/debugger/index.js @@ -0,0 +1,177 @@ +import React from "react"; +import run from "../maki-interpreter/interpreter"; +import System from "../maki-interpreter/runtime/System"; +import runtime from "../maki-interpreter/runtime"; +import Variable from "./Variable"; +import Value from "./Value"; +import Command from "./Command"; +import { useDropzone } from "react-dropzone"; + +function readFileAsArrayBuffer(file) { + return new Promise((resolve, reject) => { + const fileReader = new FileReader(); + fileReader.onload = () => { + resolve(fileReader.result); + }; + fileReader.onerror = reject; + fileReader.readAsArrayBuffer(file); + }); +} + +function Wrapper() { + const [maki, setMaki] = React.useState(null); + const onDrop = React.useCallback(async acceptedFiles => { + if (acceptedFiles.length !== 1) { + alert("Only drop one file at a time"); + return; + } + const file = acceptedFiles[0]; + const arrayBuffer = await readFileAsArrayBuffer(file); + setMaki(arrayBuffer); + // Do something with the files + }, []); + const { getRootProps, getInputProps, isDragActive } = useDropzone({ onDrop }); + return ( +
+ {maki == null ? ( + <> +

+ Drop a .maki file here to debug +

+ + + ) : ( + + )} +
+ ); +} + +/* +async function fetchTestScript() { + const response = await fetch(basicTestUrl); + return response.arrayBuffer(); +} +*/ + +function Debugger({ maki }) { + // This is all a huge disaster + const [commandOffset, setCommandOffset] = React.useState(0); + const [variables, setVariables] = React.useState(null); + const [stack, setStack] = React.useState(null); + const [commands, setCommands] = React.useState(null); + const [next, setNext] = React.useState(() => {}); + const system = new System(); + + React.useEffect(() => { + run({ + runtime, + data: maki, + system, + logger: ({ i, stack, program }) => { + const done = new Promise((resolve, reject) => { + // setState accepts a creator function, so we have to pass a function that returns our function. + setNext(() => resolve); + }); + setVariables([...program.variables]); + setStack([...stack]); + setCommands([...program.commands]); + setCommandOffset(i); + return done; + }, + }); + }, []); + + React.useEffect(() => { + function handler(e) { + switch (e.key) { + case ";": + next(); + break; + default: + console.log(e.key); + } + } + document.addEventListener("keydown", handler); + return () => document.removeEventListener("keydown", handler); + }); + + return ( +
+
+ +

Commands

+ + + {commands != null && + commands.map((command, i) => ( + + + + + ))} + +
{i} + +
+
+
+

Stack

+ + + {stack != null && + stack.map((value, i) => ( + + + + + ))} + +
{i} + +
+
+
+

Variables

+ + + {variables != null && + variables.map((variable, i) => ( + + + + + ))} + +
{i} + +
+
+
+ ); +} + +export default Wrapper; diff --git a/experiments/modern/src/index.js b/experiments/modern/src/index.js index 87d1be55..77f0ccce 100644 --- a/experiments/modern/src/index.js +++ b/experiments/modern/src/index.js @@ -1,10 +1,14 @@ -import React from 'react'; -import ReactDOM from 'react-dom'; -import './index.css'; -import App from './App'; -import * as serviceWorker from './serviceWorker'; +import React from "react"; +import ReactDOM from "react-dom"; +import "./index.css"; +import App from "./App"; +import * as serviceWorker from "./serviceWorker"; +import Debugger from "./debugger"; -ReactDOM.render(, document.getElementById('root')); +ReactDOM.render( + window.location.pathname === "/debugger" ? : , + document.getElementById("root") +); // If you want your app to work offline and load faster, you can change // unregister() to register() below. Note this comes with some pitfalls. diff --git a/experiments/modern/src/maki-interpreter/interpreter.js b/experiments/modern/src/maki-interpreter/interpreter.js index bb23b433..9e2e42d2 100644 --- a/experiments/modern/src/maki-interpreter/interpreter.js +++ b/experiments/modern/src/maki-interpreter/interpreter.js @@ -26,7 +26,7 @@ function main({ runtime, data, system, log, logger }) { const { commandOffset, variableOffset, methodOffset } = binding; const variable = program.variables[variableOffset]; const method = program.methods[methodOffset]; - const logger = log ? printCommand : null; + // const logger = log ? printCommand : logger; // TODO: Handle disposing of this. // TODO: Handle passing in variables. variable.hook(method.name, () => { diff --git a/experiments/modern/src/maki-interpreter/interpreter.test.js b/experiments/modern/src/maki-interpreter/interpreter.test.js index f26cd5b7..69171698 100644 --- a/experiments/modern/src/maki-interpreter/interpreter.test.js +++ b/experiments/modern/src/maki-interpreter/interpreter.test.js @@ -76,7 +76,25 @@ describe("can use basic operators", () => { console.error(e); } expect(mockMessageBox.mock.calls).toEqual( - ["2 + 2 = 4", "2.2 + 2.2 = 4.4"].map(successOutputFromMessage) + [ + "2 + 2 = 4", + "2.2 + 2.2 = 4.4", + "3 - 2 = 1", + "3 - -2 = 5", + "3.5 - 2 = 1.5", + "2 * 3 = 6", + "2 * 1.5 = 3", + "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)", + ].map(successOutputFromMessage) ); }); }); diff --git a/experiments/modern/src/maki-interpreter/virtualMachine.js b/experiments/modern/src/maki-interpreter/virtualMachine.js index 481b7fa9..d68abcbd 100644 --- a/experiments/modern/src/maki-interpreter/virtualMachine.js +++ b/experiments/modern/src/maki-interpreter/virtualMachine.js @@ -1,12 +1,13 @@ -function interpret(start, program, { logger = null }) { +const Variable = require("./variable"); + +async function interpret(start, program, { logger = null }) { const { commands, methods, variables, classes, offsetToCommand } = program; // Run all the commands that are safe to run. Increment this number to find // the next bug. const stack = []; - let i = start - 1; + let i = start; while (i < commands.length) { - i++; const command = commands[i]; switch (command.opcode) { @@ -25,7 +26,11 @@ function interpret(start, program, { logger = null }) { case 8: { const a = stack.pop(); const b = stack.pop(); - stack.push(a.getValue() === b.getValue()); + // I'm suspicious about this. Should we really be storing both values + // and variables on the stack. + const aValue = a instanceof Variable ? a.getValue() : a; + const bValue = b instanceof Variable ? b.getValue() : b; + stack.push(aValue === bValue); break; } // jumpIf @@ -88,20 +93,94 @@ function interpret(start, program, { logger = null }) { stack.push(a.getValue() + b.getValue()); break; } - // >> + // - (subtract) + case 65: { + const a = stack.pop(); + const b = stack.pop(); + const aValue = a instanceof Variable ? a.getValue() : a; + const bValue = b instanceof Variable ? b.getValue() : b; + stack.push(bValue - aValue); + break; + } + // * (multiply) + case 66: { + const a = stack.pop(); + const b = stack.pop(); + const aValue = a instanceof Variable ? a.getValue() : a; + const bValue = b instanceof Variable ? b.getValue() : b; + stack.push(bValue * aValue); + break; + } + // / (divide) + case 67: { + const a = stack.pop(); + const b = stack.pop(); + const aValue = a instanceof Variable ? a.getValue() : a; + const bValue = b instanceof Variable ? b.getValue() : b; + stack.push(bValue / aValue); + break; + } + // % (mod) + case 68: { + const a = stack.pop(); + const b = stack.pop(); + const aValue = a instanceof Variable ? a.getValue() : a; + const bValue = b instanceof Variable ? b.getValue() : b; + stack.push(bValue % aValue); + break; + } + // & (binary and) + case 72: { + const a = stack.pop(); + const b = stack.pop(); + const aValue = a instanceof Variable ? a.getValue() : a; + const bValue = b instanceof Variable ? b.getValue() : b; + stack.push(bValue & aValue); + break; + } + // | (binary or) + case 73: { + const a = stack.pop(); + const b = stack.pop(); + const aValue = a instanceof Variable ? a.getValue() : a; + const bValue = b instanceof Variable ? b.getValue() : b; + stack.push(bValue | aValue); + break; + } + // - (negative) + case 76: { + const a = stack.pop(); + stack.push(-a.getValue()); + break; + } + // << case 88: { const a = stack.pop(); const b = stack.pop(); - stack.push(a >> b); + const aValue = a instanceof Variable ? a.getValue() : a; + const bValue = b instanceof Variable ? b.getValue() : b; + stack.push(aValue << bValue); + break; + } + // >> + case 89: { + const a = stack.pop(); + const b = stack.pop(); + const aValue = a instanceof Variable ? a.getValue() : a; + const bValue = b instanceof Variable ? b.getValue() : b; + stack.push(bValue >> aValue); break; } default: throw new Error(`Unhandled opcode ${command.opcode}`); } + i++; // Print some debug info if (logger) { - logger({ i, command, stack, variables }); + const done = logger({ i, command, stack, variables, program }); + console.log(done); + await done; } } } diff --git a/experiments/modern/yarn.lock b/experiments/modern/yarn.lock index 00e388b6..bd4534a0 100644 --- a/experiments/modern/yarn.lock +++ b/experiments/modern/yarn.lock @@ -1791,6 +1791,13 @@ atob@^2.1.1: resolved "https://registry.yarnpkg.com/atob/-/atob-2.1.2.tgz#6d9517eb9e030d2436666651e86bd9f6f13533c9" integrity sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg== +attr-accept@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/attr-accept/-/attr-accept-1.1.3.tgz#48230c79f93790ef2775fcec4f0db0f5db41ca52" + integrity sha512-iT40nudw8zmCweivz6j58g+RT33I4KbaIvRUhjNmDwO2WmsQUxFEZZYZ5w3vXe5x5MX9D7mfvA/XaLOZYFR9EQ== + dependencies: + core-js "^2.5.0" + autoprefixer@^9.4.9: version "9.6.0" resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-9.6.0.tgz#0111c6bde2ad20c6f17995a33fad7cf6854b4c87" @@ -2710,7 +2717,7 @@ core-js@3.0.1: resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.0.1.tgz#1343182634298f7f38622f95e73f54e48ddf4738" integrity sha512-sco40rF+2KlE0ROMvydjkrVMMG1vYilP2ALoRXcYR4obqbYIuV3Bg+51GEDW+HF8n7NRA+iaA4qD0nD9lo9mew== -core-js@^2.4.0: +core-js@^2.4.0, core-js@^2.5.0: version "2.6.9" resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.6.9.tgz#6b4b214620c834152e179323727fc19741b084f2" integrity sha512-HOpZf6eXmnl7la+cUdMnLvUxKNqLUzJvgIziQ0DiF3JwSImNphIqdGqzj6hIKyX04MmV0poclQ7+wjWvxQyR2A== @@ -3910,6 +3917,13 @@ file-loader@3.0.1: loader-utils "^1.0.2" schema-utils "^1.0.0" +file-selector@^0.1.11: + version "0.1.12" + resolved "https://registry.yarnpkg.com/file-selector/-/file-selector-0.1.12.tgz#fe726547be219a787a9dcc640575a04a032b1fd0" + integrity sha512-Kx7RTzxyQipHuiqyZGf+Nz4vY9R1XGxuQl/hLoJwq+J4avk/9wxxgZyHKtbyIPJmbD4A66DWGYfyykWNpcYutQ== + dependencies: + tslib "^1.9.0" + filesize@3.6.1: version "3.6.1" resolved "https://registry.yarnpkg.com/filesize/-/filesize-3.6.1.tgz#090bb3ee01b6f801a8a8be99d31710b3422bb317" @@ -7683,7 +7697,7 @@ prompts@^2.0.1: kleur "^3.0.2" sisteransi "^1.0.0" -prop-types@^15.6.2: +prop-types@^15.6.2, prop-types@^15.7.2: version "15.7.2" resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.7.2.tgz#52c41e75b8c87e72b9d9360e0206b99dcbffa6c5" integrity sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ== @@ -7899,6 +7913,15 @@ react-dom@^16.8.6: prop-types "^15.6.2" scheduler "^0.13.6" +react-dropzone@^10.1.5: + version "10.1.5" + resolved "https://registry.yarnpkg.com/react-dropzone/-/react-dropzone-10.1.5.tgz#5f2817ab924b270010c7034fe01f49b6240b9c91" + integrity sha512-Hbe7soBc/i1fid7K3BU8T1oUJFnYO2AicS22F2MxcueMYDfKBWqZMr5ED3CoTAMs//9t2W30eyFiXm1h4wLCqA== + dependencies: + attr-accept "^1.1.3" + file-selector "^0.1.11" + prop-types "^15.7.2" + react-error-overlay@^5.1.6: version "5.1.6" resolved "https://registry.yarnpkg.com/react-error-overlay/-/react-error-overlay-5.1.6.tgz#0cd73407c5d141f9638ae1e0c63e7b2bf7e9929d"