refactor interpreter to new file

This commit is contained in:
Ole Morud
2022-08-25 19:31:40 +02:00
parent d85f6bf80b
commit 2cf821729b
2 changed files with 296 additions and 192 deletions

View File

@@ -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<Point>(Direction.Right)
const [programCounter, setProgramCounter] = useState<Point>({ x: -1, y: 0 })
const [board, setBoard] = useState<Instruction[][]>(
new Array(BOARD_HEIGHT)
.fill(0)
.map(() => new Array(BOARD_WIDTH).fill(0).map(() => structuredClone(instructions[0])))
)
let stringmode = false
const [stack, setStack] = useState<number[]>([])
const [program, setProgram] = useState<ProgramState>(newProgram())
const [playing, setPlaying] = useState<boolean>(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<HTMLDivElement> | 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<HTMLDivElement> | 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<HTMLInputElement | HTMLTextAreaElement>
) => {
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 (
<div style={{ marginLeft: `${SIDEBAR_WIDTH + 1}rem` }}>
{/* BUTTON PANEL */}
<div style={{ display: "flex" }}>
<IconButton onClick={() => iterate()}>
<Grid container>
<Grid item xs={4}>
<IconButton onClick={() => setPlaying(!playing)}>
{playing ? <StopIcon /> : <PlayArrowIcon />}
</IconButton>
<IconButton onClick={() => step()}>
<KeyboardDoubleArrowRightIcon />
</IconButton>
<Typography>stack: {JSON.stringify(stack)}</Typography>
</Grid>
<Grid item xs={4}>
<Typography>output: {program.output}</Typography>
</Grid>
<Grid item xs={4}>
<Typography>stack: {JSON.stringify(program.stack)}</Typography>
</Grid>
</Grid>
</div>
{/* BOARD */}
{board.map((row, rowNo): JSX.Element => {
{program.board.map((row, rowNo): JSX.Element => {
return (
<Grid key={key++} container columns={BOARD_WIDTH} sx={{ width: `${5 * 12}rem` }}>
{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() {
<TextField
sx={{ mt: "10%" }}
variant="standard"
inputProps={{ maxLength: 1 }}
onChange={(event) => handleChange(colNo, rowNo, event)}
inputProps={{ maxLength: 1, style: { textAlign: "center" } }}
onChange={(event) => handleTextFieldChange(colNo, rowNo, event)}
onDragOver={handleOnDragOver}
onDrop={(event) => {
handleDrop(colNo, rowNo, event)

247
src/interpreter.tsx Normal file
View File

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