From 2cf821729ba0e51199c1a1a5687f529879e1b314 Mon Sep 17 00:00:00 2001 From: Ole Morud Date: Thu, 25 Aug 2022 19:31:40 +0200 Subject: [PATCH] refactor interpreter to new file --- src/components/Board.tsx | 241 ++++++++------------------------------ src/interpreter.tsx | 247 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 296 insertions(+), 192 deletions(-) create mode 100644 src/interpreter.tsx diff --git a/src/components/Board.tsx b/src/components/Board.tsx index aed9f90..21c0ad9 100644 --- a/src/components/Board.tsx +++ b/src/components/Board.tsx @@ -1,7 +1,10 @@ -import React, { useEffect, useState } from "react" +import React, { useEffect, useRef, useState } from "react" import { Card, CardContent, Grid, IconButton, TextField, Typography } from "@mui/material" -import instructions, { Instruction } from "../instructions" +import { Instruction } from "../instructions" import KeyboardDoubleArrowRightIcon from "@mui/icons-material/KeyboardDoubleArrowRight" +import PlayArrowIcon from "@mui/icons-material/PlayArrow" +import StopIcon from "@mui/icons-material/Stop" + import { BOARD_HEIGHT, BOARD_WIDTH, @@ -10,40 +13,28 @@ import { SIDEBAR_WIDTH, TILE_SIZE } from "../const" - -// considering renaming this to vector -interface Point { - x: number - y: number -} - -const Direction = { - Up: { x: 0, y: -1 }, - Right: { x: 1, y: 0 }, - Left: { x: -1, y: 0 }, - Down: { x: 0, y: 1 } -} +import { iterate, newProgram, ProgramState, resetProgram } from "../interpreter" function Board() { let key = 0 - const [output, setOutput] = useState("") - const [done, setDone] = useState(true) - const [direction, setDirection] = useState(Direction.Right) - const [programCounter, setProgramCounter] = useState({ x: -1, y: 0 }) - const [board, setBoard] = useState( - new Array(BOARD_HEIGHT) - .fill(0) - .map(() => new Array(BOARD_WIDTH).fill(0).map(() => structuredClone(instructions[0]))) - ) - let stringmode = false - const [stack, setStack] = useState([]) + + const [program, setProgram] = useState(newProgram()) + const [playing, setPlaying] = useState(false) useEffect(() => { - if (done) { - setProgramCounter({ x: -1, y: 0 }) - setDirection(Direction.Right) + if (program.done) { + const x = structuredClone(program) + resetProgram(x) + x.done = false + setProgram(x) } - }, [done]) + }, [program]) + + const step = () => { + const x = structuredClone(program) + iterate(x) + setProgram(x) + } const handleDrop = ( col: number, @@ -51,16 +42,15 @@ function Board() { event: React.DragEvent | undefined ): void => { if (event === undefined) { - console.log("invalid drop target") return } const data = JSON.parse(event.dataTransfer.getData("instruction")) as Instruction - const x = [...board] - x[row][col] = data - setBoard(x) + const x = structuredClone(program) + x.board[row][col] = data + setProgram(x) const target = event.target as HTMLInputElement - target.value = data.symbol + target.value = data.emoji } const handleOnDragOver = (event: React.DragEvent | undefined) => { @@ -68,175 +58,41 @@ function Board() { return false } - const safepop = (): number => { - if (stack.length === 0) { - return 0 - } - - const x = [...stack] - let result = x.pop() - setStack(x) - - if (result === undefined) { - result = 0 - } - - return result - } - - const iterate = () => { - setDone(false) - stepProgramCounter() - // This is asynchronous, the next line is run before program counter - // is updated. That is bad - console.log(board[programCounter.y][programCounter.x]) - - const cmd = board[programCounter.y][programCounter.x].ascii - let a: number - let b: number - - if (stringmode && cmd !== '"') { - setStack([...stack, cmd.charCodeAt(0)]) - } else if ("0123456789".includes(cmd)) { - setStack([...stack, +cmd]) - } else { - switch (cmd) { - case "+": - stack.push(safepop() + safepop()) - break - case "-": - stack.push(-safepop() + safepop()) - break - case "*": - stack.push(safepop() * safepop()) - break - case "/": - stack.push((1 / safepop()) * safepop()) - break - case "%": - a = safepop() - b = safepop() - stack.push(b % a) - break - case "!": - stack.push(safepop() === 0 ? 1 : 0) - break - case "`": - stack.push(safepop() < safepop() ? 1 : 0) - break - case ">": - setDirection(Direction.Right) - break - case "<": - setDirection(Direction.Left) - break - case "^": - setDirection(Direction.Up) - break - case "v": - setDirection(Direction.Down) - break - case "?": - setDirection( - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - [Direction.Up, Direction.Down, Direction.Left, Direction.Right].at( - Math.floor(Math.random() * 4) - )! - ) - break - case "_": - setDirection(safepop() === 0 ? Direction.Right : Direction.Left) - break - case "|": - setDirection(safepop() === 0 ? Direction.Down : Direction.Up) - break - case '"': - stringmode = !stringmode - break - case ":": - setStack([...stack, stack[stack.length - 1]]) - break - case "\\": - swapTopStackValues() - break - case "$": - safepop() - break - case ".": - setOutput(output + safepop()) - break - case ",": - setOutput(output + String.fromCharCode(safepop())) - break - case "#": - stepProgramCounter() - break - case "g": - a = safepop() - b = safepop() - setStack([...stack, board[b][a].ascii.charCodeAt(0)]) - break - case "p": - // TODO - break - case "&": - // TODO - break - case "~": - // TODO - break - case "@": - setDone(true) - break - default: - console.log("unknown command:", cmd) - break - } - } - - return null - } - - const stepProgramCounter = () => { - const tmp: Point = { - x: programCounter.x + direction.x, - y: programCounter.y + direction.y - } - if (tmp.x >= BOARD_WIDTH || tmp.x < 0 || tmp.y >= BOARD_HEIGHT || tmp.y < 0) { - setDone(true) - return - } - setProgramCounter(tmp) - } - - const swapTopStackValues = () => { - const a = safepop() - const b = safepop() - setStack([...stack, a, b]) - } - - const handleChange = ( + const handleTextFieldChange = ( col: number, row: number, event: React.ChangeEvent ) => { - const x = [...board] - x[row][col].symbol = event.target.value - setBoard(x) + const x = structuredClone(program) + x.board[row][col].emoji = event.target.value + x.board[row][col].bytecode = event.target.value + setProgram(x) } return (
{/* BUTTON PANEL */}
- iterate()}> + + + setPlaying(!playing)}> + {playing ? : } + + step()}> - stack: {JSON.stringify(stack)} + + + output: {program.output} + + + stack: {JSON.stringify(program.stack)} + +
{/* BOARD */} - {board.map((row, rowNo): JSX.Element => { + {program.board.map((row, rowNo): JSX.Element => { return ( {row.map((tile, colNo): JSX.Element => { @@ -253,7 +109,8 @@ function Board() { width: `${TILE_SIZE}rem`, margin: 1, bgcolor: - rowNo === programCounter.y && colNo === programCounter.x + rowNo === program.instructionPointer.y && + colNo === program.instructionPointer.x ? SELECTED_TILE_COLOR : DEFAULT_TILE_COLOR }}> @@ -261,8 +118,8 @@ function Board() { handleChange(colNo, rowNo, event)} + inputProps={{ maxLength: 1, style: { textAlign: "center" } }} + onChange={(event) => handleTextFieldChange(colNo, rowNo, event)} onDragOver={handleOnDragOver} onDrop={(event) => { handleDrop(colNo, rowNo, event) diff --git a/src/interpreter.tsx b/src/interpreter.tsx new file mode 100644 index 0000000..28719ba --- /dev/null +++ b/src/interpreter.tsx @@ -0,0 +1,247 @@ +import { BOARD_HEIGHT, BOARD_WIDTH } from "./const" +import instructions, { Instruction } from "./instructions" + +export interface ProgramState { + instructionPointer: Point + direction: Point + stack: number[] + board: Instruction[][] + output: string + stringmode: boolean + done: boolean +} + +export interface Point { + x: number + y: number +} + +const Direction = { + Up: { x: 0, y: -1 }, + Right: { x: 1, y: 0 }, + Left: { x: -1, y: 0 }, + Down: { x: 0, y: 1 } +} + +/** + * Creates a new program + * @returns new program object with initialized attributes + */ +export const newProgram = () => { + const program: ProgramState = { + instructionPointer: { x: 0, y: 0 }, + direction: Direction.Right, + stack: [], + board: newBoard(), + output: "", + stringmode: false, + done: true + } + + return program +} + +/** + * Sets all program values to their default state + * @param program - program to be reset + */ +export const resetProgram = (program: ProgramState) => { + /*program.board.forEach((row) => { + row.forEach((tile) => { + tile.bytecode = instructions[0].bytecode + tile.description = instructions[0].description + tile.emoji = instructions[0].emoji + tile.name = instructions[0].name + tile.searchtags = instructions[0].searchtags + }) + })*/ + + program.direction = Direction.Right + program.done = false + program.instructionPointer.x = 0 + program.instructionPointer.y = 0 + program.output = "" + program.stack.length = 0 + program.stringmode = false +} + +/** + * Initializes a 2D array of instructions with all elements set to the NOOP instruction + * @returns Instruction[BOARD_HEIGHT][BOARD_WIDTH] + */ +export const newBoard = () => { + return new Array(BOARD_HEIGHT) + .fill(0) + .map(() => new Array(BOARD_WIDTH).fill(0).map(() => structuredClone(instructions[0]))) +} + +/** + * Removes the last element from an array and returns it. If the array is empty it returns 0 + * @param arr array to pop from + * @returns last element of array if array is non-empty, zero otherwise + */ +const safepop = (arr: number[]): number => { + const result = arr.pop() + + if (result !== undefined) { + return result + } else { + return 0 + } +} + +/** + * Increments the instructionPointer of a program according to the programs direction attribute + * @param program program to increment its instruction pointer + */ +const step = (program: ProgramState) => { + program.instructionPointer.x += program.direction.x + program.instructionPointer.y += program.direction.y + + if ( + program.instructionPointer.x >= BOARD_WIDTH || + program.instructionPointer.x < 0 || + program.instructionPointer.y >= BOARD_HEIGHT || + program.instructionPointer.y < 0 + ) { + console.log("pointer out of bounds, resetting program") + resetProgram(program) + } +} + +/** + * Reads the current instruction pointed at and executes it, then moves the instruction pointer + * according to the direction + * @param program program to iterate + * @see step() + * @see safepop() + */ +export const iterate = (program: ProgramState) => { + const cmd = program.board[program.instructionPointer.y][program.instructionPointer.x].bytecode + let a: number + let b: number + let maybeA: number | undefined + + program.done = false + + if (program.stringmode && cmd !== '"') { + program.stack.push(cmd.charCodeAt(0)) + } else if ("0123456789".includes(cmd)) { + program.stack.push(+cmd) + } else { + switch (cmd) { + case " ": + break + case "+": + a = safepop(program.stack) + b = safepop(program.stack) + program.stack.push(a + b) + break + case "-": + a = safepop(program.stack) + b = safepop(program.stack) + program.stack.push(b - a) + break + case "*": + a = safepop(program.stack) + b = safepop(program.stack) + program.stack.push(a * b) + break + case "/": + a = safepop(program.stack) + b = safepop(program.stack) + program.stack.push(b / a) + break + case "%": + a = safepop(program.stack) + b = safepop(program.stack) + program.stack.push(b % a) + break + case "!": + a = safepop(program.stack) + program.stack.push(a === 0 ? 1 : 0) + break + case "`": + a = safepop(program.stack) + b = safepop(program.stack) + program.stack.push(a < b ? 1 : 0) + break + case ">": + program.direction = Direction.Right + break + case "<": + program.direction = Direction.Left + break + case "^": + program.direction = Direction.Up + break + case "v": + program.direction = Direction.Down + break + case "?": + program.direction = + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + [Direction.Up, Direction.Down, Direction.Left, Direction.Right].at( + Math.floor(Math.random() * 4) + )! + break + case "_": + a = safepop(program.stack) + program.direction = a === 0 ? Direction.Right : Direction.Left + break + case "|": + a = safepop(program.stack) + program.direction = a === 0 ? Direction.Down : Direction.Up + break + case '"': + program.stringmode = !program.stringmode + break + case ":": + maybeA = program.stack.at(-1) + if (maybeA !== undefined) { + program.stack.push(maybeA) + } + break + case "\\": + a = safepop(program.stack) + b = safepop(program.stack) + program.stack.push(b, a) + break + case "$": + safepop(program.stack) + break + case ".": + a = safepop(program.stack) + program.output += a + break + case ",": + a = safepop(program.stack) + program.output += String.fromCharCode(a) + break + case "#": + step(program) + break + case "g": + a = safepop(program.stack) + b = safepop(program.stack) + program.stack.push(program.board[b][a].bytecode.charCodeAt(0)) + break + case "p": + // TODO + break + case "&": + // TODO + break + case "~": + // TODO + break + case "@": + program.done = true + break + default: + break + } + } + + step(program) +}