420 lines
9.5 KiB
TypeScript
420 lines
9.5 KiB
TypeScript
/* eslint-disable eqeqeq */
|
|
import React, { useState } from "react";
|
|
import { useHash } from "react-use";
|
|
import TextArea from "./TextArea";
|
|
import "./App.css";
|
|
|
|
export default function App() {
|
|
let [hash, setHash] = useHash();
|
|
|
|
let input = decodeURI(hash.slice(1));
|
|
function setInput(s: string) {
|
|
setHash(encodeURI(s));
|
|
}
|
|
|
|
let [activeStateIdx, setActiveStateIdx] = useState(0);
|
|
let parsed = somes(input.split(/[^a-z0-9-]+/).map(parse_op));
|
|
let states = steps(INITIAL_STATE, parsed);
|
|
let state = states[activeStateIdx] ?? INITIAL_STATE;
|
|
|
|
return (
|
|
<>
|
|
<TextArea
|
|
onChange={(e: any) => setInput(e.target.value)}
|
|
value={input}
|
|
className="program"
|
|
title={`\
|
|
type Op =
|
|
| number
|
|
| "add"
|
|
| "sub"
|
|
| "mul"
|
|
| "div"
|
|
| "put"
|
|
| "jmp"
|
|
| "dup"
|
|
| "del"
|
|
| "cmp"
|
|
| "swp";`}
|
|
></TextArea>
|
|
<div className="info">
|
|
<Ops
|
|
parsed={parsed}
|
|
state={state}
|
|
// // hack
|
|
// top={document.getElementById("input")?.clientHeight ?? 0}
|
|
/>
|
|
<Stack state={state} just_popped={state.just_popped} />
|
|
<States
|
|
states={states}
|
|
activeStateIdx={activeStateIdx}
|
|
setActiveStateIdx={n => setActiveStateIdx(n)}
|
|
/>
|
|
</div>
|
|
</>
|
|
);
|
|
}
|
|
|
|
function Ops({
|
|
parsed,
|
|
state,
|
|
}: // top,
|
|
{
|
|
parsed: Op[];
|
|
state: State;
|
|
// top: number;
|
|
}): JSX.Element {
|
|
return (
|
|
<div
|
|
className="ops"
|
|
children={parsed.map((op, i) => (
|
|
<Op
|
|
op={op}
|
|
highlighted={i == state.ip}
|
|
jumped_from={i == state.jumped_from}
|
|
num={i}
|
|
key={i}
|
|
/>
|
|
))}
|
|
// style={{
|
|
// top: `${top + 5}px`,
|
|
// }}
|
|
/>
|
|
);
|
|
}
|
|
|
|
function Op({
|
|
op,
|
|
highlighted,
|
|
num,
|
|
jumped_from,
|
|
...props
|
|
}: {
|
|
op: Op;
|
|
num: number;
|
|
highlighted: boolean;
|
|
jumped_from: boolean;
|
|
}): JSX.Element {
|
|
return (
|
|
<div
|
|
className={`op ${highlighted ? "active" : ""} ${
|
|
jumped_from ? "jumped-from" : ""
|
|
}`}
|
|
key={num}
|
|
{...props}
|
|
>
|
|
<div className="op-num">{num}: </div>
|
|
{op}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function States({
|
|
states,
|
|
activeStateIdx,
|
|
setActiveStateIdx,
|
|
}: {
|
|
states: State[];
|
|
activeStateIdx: number;
|
|
setActiveStateIdx: (_: number) => any;
|
|
}): JSX.Element {
|
|
return (
|
|
<div
|
|
className="states"
|
|
children={states.map((state, i) => (
|
|
<State
|
|
state={state}
|
|
key={i}
|
|
active={activeStateIdx == i}
|
|
onMouseEnter={() => setActiveStateIdx(i)}
|
|
/>
|
|
))}
|
|
/>
|
|
);
|
|
}
|
|
|
|
function State({
|
|
state,
|
|
active,
|
|
onMouseEnter,
|
|
...props
|
|
}: {
|
|
state: State;
|
|
active: boolean;
|
|
onMouseEnter: () => any;
|
|
}): JSX.Element {
|
|
return (
|
|
<div
|
|
className={`state ${active ? "active" : ""}`}
|
|
onMouseEnter={onMouseEnter}
|
|
onFocus={onMouseEnter}
|
|
onClick={onMouseEnter}
|
|
{...props}
|
|
>
|
|
<div className="ip">Ip: {state.ip}</div>
|
|
{/* <span>Stack:</span> <Stack stack={state.stack} /> */}
|
|
<span>Output:</span> <Output output={state.output} />
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function Stack({
|
|
state,
|
|
just_popped,
|
|
...props
|
|
}: {
|
|
state: State;
|
|
just_popped?: number[];
|
|
}): JSX.Element {
|
|
let ns = [...state.stack];
|
|
ns.reverse();
|
|
console.log(just_popped);
|
|
let children = (just_popped ?? []).map((just_popped, i) => (
|
|
<StackItem
|
|
item={just_popped}
|
|
key={-(i + 1)}
|
|
highlighted={false}
|
|
just_popped={true}
|
|
/>
|
|
));
|
|
ns.map((item, i) => (
|
|
<StackItem
|
|
item={item}
|
|
key={i}
|
|
highlighted={
|
|
(state.just_pushed && i == 0) || (state.just_swapped && i < 2)
|
|
}
|
|
/>
|
|
)).forEach(child => children.push(child));
|
|
|
|
return <div className="stack" children={children} {...props} />;
|
|
}
|
|
|
|
function StackItem({
|
|
item,
|
|
highlighted = false,
|
|
just_popped = false,
|
|
...props
|
|
}: {
|
|
item: number;
|
|
highlighted: boolean;
|
|
just_popped?: boolean;
|
|
}): JSX.Element {
|
|
return (
|
|
<div
|
|
className={`stack-item ${highlighted ? "active" : ""} ${
|
|
just_popped ? "just-popped" : ""
|
|
}`}
|
|
{...props}
|
|
>
|
|
{item}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// bad
|
|
function Output({ output }: { output: number[] }): JSX.Element {
|
|
return (
|
|
<div
|
|
className="output"
|
|
children={output.map((item, i) => (
|
|
<span key={i}>{String.fromCharCode(item)}</span>
|
|
))}
|
|
></div>
|
|
);
|
|
}
|
|
|
|
type Option<T> = T | undefined;
|
|
|
|
function is_some<T>(a: Option<T>): a is T {
|
|
return typeof a != "undefined";
|
|
}
|
|
|
|
function is_none<T>(a: Option<T>): a is undefined {
|
|
return typeof a == "undefined";
|
|
}
|
|
|
|
function cons<T>(x: T, xs: T[]): T[] {
|
|
return [x, ...xs];
|
|
}
|
|
|
|
function somes<T>(os: Option<T>[]): T[] {
|
|
if (os.length == 0) {
|
|
return [];
|
|
} else {
|
|
let first = os[0];
|
|
if (is_some(first)) {
|
|
return cons(first, somes(os.slice(1)));
|
|
} else {
|
|
return somes(os.slice(1));
|
|
}
|
|
}
|
|
}
|
|
|
|
type Op =
|
|
| number
|
|
| "add"
|
|
| "sub"
|
|
| "mul"
|
|
| "div"
|
|
| "put"
|
|
| "jmp"
|
|
| "dup"
|
|
| "del"
|
|
| "cmp"
|
|
| "swp";
|
|
|
|
function parse_int(n: string, radix?: number): Option<number> {
|
|
let maybe_int = parseInt(n, radix);
|
|
// eslint-disable-next-line no-self-compare
|
|
if (maybe_int == maybe_int) {
|
|
return maybe_int;
|
|
}
|
|
}
|
|
|
|
function parse_op(op: string): Option<Op> {
|
|
let maybe_int = parse_int(op);
|
|
if (is_some(maybe_int)) {
|
|
return maybe_int;
|
|
}
|
|
switch (op) {
|
|
case "add":
|
|
case "sub":
|
|
case "mul":
|
|
case "div":
|
|
case "put":
|
|
case "jmp":
|
|
case "dup":
|
|
case "del":
|
|
case "cmp":
|
|
case "swp":
|
|
return op;
|
|
}
|
|
}
|
|
|
|
type State = {
|
|
ip: number;
|
|
stack: number[];
|
|
output: number[];
|
|
jumped_from: Option<number>;
|
|
just_pushed: boolean;
|
|
just_swapped: boolean;
|
|
just_popped?: number[];
|
|
};
|
|
|
|
const INITIAL_STATE: State = {
|
|
ip: 0,
|
|
stack: [],
|
|
output: [],
|
|
jumped_from: undefined,
|
|
just_pushed: false,
|
|
just_swapped: false,
|
|
};
|
|
|
|
function steps(state: State, ops: Op[], limit = 300, num_steps = 0): State[] {
|
|
if (!(state.ip in ops) || num_steps > limit) {
|
|
return [state];
|
|
}
|
|
return cons(
|
|
state,
|
|
steps(step(state, ops[state.ip]), ops, limit, num_steps + 1)
|
|
);
|
|
}
|
|
|
|
// all but jmp advance the ip by one
|
|
// cmp: lt => -1, eq => 0, gt => 1
|
|
function step(state: State, op: Op): State {
|
|
let ns = JSON.parse(JSON.stringify(state)) as State;
|
|
ns.jumped_from = ns.ip;
|
|
ns.just_pushed = false;
|
|
ns.just_swapped = false;
|
|
ns.just_popped = undefined;
|
|
ns.ip++;
|
|
|
|
function binary_op(
|
|
default_a: number,
|
|
default_b: number,
|
|
f: (a: number, b: number) => number
|
|
) {
|
|
let [b, a] = [ns.stack.pop() ?? default_b, ns.stack.pop() ?? default_a];
|
|
ns.stack.push(f(a, b));
|
|
}
|
|
|
|
if (typeof op == "number") {
|
|
ns.stack.push(op);
|
|
}
|
|
|
|
switch (op) {
|
|
case "add":
|
|
binary_op(0, 0, (a, b) => a + b);
|
|
break;
|
|
case "sub":
|
|
binary_op(0, 0, (a, b) => a - b);
|
|
break;
|
|
case "mul":
|
|
binary_op(0, 1, (a, b) => a * b);
|
|
break;
|
|
case "div":
|
|
binary_op(0, 1, (a, b) => a / b);
|
|
break;
|
|
case "put": {
|
|
let popped = ns.stack.pop();
|
|
if (typeof popped != "undefined") {
|
|
ns.output.push(popped);
|
|
}
|
|
break;
|
|
}
|
|
case "jmp":
|
|
ns.ip--;
|
|
// ns.jumped_from = ns.ip;
|
|
ns.ip += ns.stack.pop() ?? 1;
|
|
break;
|
|
case "dup": {
|
|
let popped = ns.stack.pop();
|
|
if (typeof popped != "undefined") {
|
|
ns.stack.push(popped, popped);
|
|
}
|
|
break;
|
|
}
|
|
case "del":
|
|
ns.stack.pop();
|
|
break;
|
|
case "cmp": {
|
|
let [a, b] = [ns.stack.pop() ?? 0, ns.stack.pop() ?? 0];
|
|
// matter of taste
|
|
ns.stack.push(cmp(b, a));
|
|
break;
|
|
}
|
|
case "swp": {
|
|
let [a, b] = [ns.stack.pop(), ns.stack.pop()];
|
|
if (typeof a == "undefined" || typeof b == "undefined") {
|
|
break;
|
|
}
|
|
ns.just_swapped = true;
|
|
ns.stack.push(a, b);
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (ns.stack.length > state.stack.length) {
|
|
ns.just_pushed = true;
|
|
} else if (ns.stack.length < state.stack.length) {
|
|
ns.just_popped = state.stack.slice(ns.stack.length - 1).reverse();
|
|
if (state.stack.length - ns.stack.length == 1) {
|
|
ns.just_pushed = true;
|
|
}
|
|
}
|
|
|
|
return ns;
|
|
}
|
|
|
|
function cmp(a: number, b: number): number {
|
|
if (a < b) {
|
|
return -1;
|
|
} else if (a == b) {
|
|
return 0;
|
|
} else {
|
|
return 1;
|
|
}
|
|
}
|