Implement more operators and add debugger

This commit is contained in:
Jordan Eldredge 2019-07-11 20:57:18 -07:00
parent c02b59219c
commit dcbcd20a65
10 changed files with 392 additions and 20 deletions

View file

@ -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"

View file

@ -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 = (
<span>
Variable(
<Variable variable={variables[arg]} />)
</span>
);
break;
default:
foo = null;
}
return (
<>
({command.opcode}) {command.command.name.toUpperCase()} {foo}
</>
);
}

View file

@ -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(
<Variable variable={value} />)
</>
);
}
return value ? value.toString() : null;
}

View file

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

View file

@ -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 (
<div
{...getRootProps()}
style={{
height: "100vh",
width: "100vh",
backgroundColor: isDragActive ? "yellow" : "white",
}}
>
{maki == null ? (
<>
<h1 style={{ textAlign: "center" }}>
Drop a .maki file here to debug
</h1>
<input {...getInputProps()} type="file" />
</>
) : (
<Debugger maki={maki} />
)}
</div>
);
}
/*
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 (
<div
style={{
display: "grid",
gridTemplateColumns: "70% 30%",
gridTemplateRows: "30% 70%",
height: "100vh",
gridTemplateAreas: `
"commands stack"
"commands variables"
`,
}}
>
<div style={{ gridArea: "commands", overflow: "scroll" }}>
<button onClick={next}>Next</button>
<h2>Commands</h2>
<table>
<tbody>
{commands != null &&
commands.map((command, i) => (
<tr
style={{
backgroundColor: i === commandOffset ? "pink" : "white",
}}
key={i}
>
<td>{i}</td>
<td>
<Command command={command} variables={variables} />
</td>
</tr>
))}
</tbody>
</table>
</div>
<div style={{ gridArea: "stack", overflow: "scroll" }}>
<h2>Stack</h2>
<table>
<tbody>
{stack != null &&
stack.map((value, i) => (
<tr key={i}>
<td>{i}</td>
<td>
<Value value={value} />
</td>
</tr>
))}
</tbody>
</table>
</div>
<div style={{ gridArea: "variables", overflow: "scroll" }}>
<h2>Variables</h2>
<table>
<tbody>
{variables != null &&
variables.map((variable, i) => (
<tr key={i}>
<td>{i}</td>
<td>
<Variable variable={variable} />
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
);
}
export default Wrapper;

View file

@ -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(<App />, document.getElementById('root'));
ReactDOM.render(
window.location.pathname === "/debugger" ? <Debugger /> : <App />,
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.

View file

@ -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, () => {

View file

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

View file

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

View file

@ -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"