This commit is contained in:
Alexander Bass 2024-02-14 22:54:23 -05:00
commit e53d40d5be
15 changed files with 3617 additions and 0 deletions

64
.eslintrc.json Executable file
View file

@ -0,0 +1,64 @@
{
"env": {
"browser": true,
"es2021": true
},
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/recommended"
],
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaVersion": "latest",
"sourceType": "module"
},
"plugins": [
"@typescript-eslint"
],
"rules": {
"indent": [
"warn",
"tab",
{"SwitchCase": 1}
],
"linebreak-style": [
"warn",
"unix"
],
"quotes": [
"warn",
"double"
],
"semi": [
"warn",
"always"
],
"space-before-blocks":["warn","always"],
"quote-props" : ["warn","as-needed"],
"dot-notation":"warn",
"one-var":["warn","never"],
"no-use-before-define":"warn",
"no-multi-assign":"warn",
"no-else-return":"warn",
"spaced-comment":"warn",
"prefer-destructuring":"warn",
"no-restricted-globals":"warn",
"prefer-template":"warn",
"class-methods-use-this":"warn",
"template-curly-spacing": ["warn", "never"],
"no-useless-rename":"warn",
"no-useless-escape":"warn",
"no-duplicate-imports":"warn",
"no-useless-constructor":"warn",
"no-loop-func":"warn",
"no-param-reassign":"warn",
"prefer-arrow-callback":"warn",
"no-array-constructor": "warn",
"object-shorthand": "warn",
"no-empty": "off",
"no-self-compare": "warn",
"eqeqeq": "warn",
"@typescript-eslint/no-unused-vars": "off",
"@typescript-eslint/explicit-function-return-type": "warn"
}
}

3
.gitignore vendored Executable file
View file

@ -0,0 +1,3 @@
node_modules
out/
dist/

9
.prettierrc.json Executable file
View file

@ -0,0 +1,9 @@
{
"trailingComma": "es5",
"useTabs": true,
"semi": true,
"endOfLine": "lf",
"singleQuote": false,
"printWidth": 100
}

26
ISA.txt Normal file
View file

@ -0,0 +1,26 @@
INSTRUCTIONS
--- Control Flow ---
0x00: NoOp - 0 Parameter
0x10: Goto - 1 Parameter - moves the program counter to (P1) in program memory
0x11: Goto if low bit - 2 Parameter - Moves the program counter to (P1) if register (P2)'s least significant bit is 1.
--- Memory and register management ---
0x20: Load to register - 2 Parameter - Loads into register (P1) the byte at processing memory location (P2)
0x21: Write register to memory - 2 Parameter - Writes the byte in register (P1) to the processing memory location (P2)
0x28: Copy Register -> Register - 2 Parameter - Copies byte from register (P1) to register (P2)
0x2F: Assign value to register - 2 Parameter - Assigns register (P1) to value (P2)
--- Operations ---
0x40: Add registers - 2 Parameter - Adds the contents of (P1) and (P2) and stores result to register (P1). (Overflow will be taken mod 256)
0x41: Reserved
--- Bit Operations ---
0x48: Bitwise and - 2 Parameter - Ands & each bit of register (P1) and register (P2) and stores result to register (P1)
0x49: Bitwise or - you get the point
0x4A: Bitwise not - 1 Parameter - Inverts each bit of register (P1)
0x4B: Left bit shift - 2 Parameter - Shifts bits in register (P1) to the left by (P2) and stores result to register (P1)
0x4C: right bit shift- 2 Parameter - same as left
--- Comparison Operations ---
0x50: Equals - 3 Parameter - If byte in register (P1) equals byte in register (P2), set byte in register (P3) to 0x01
0x51: Less than - 3 Parameter - If byte in register (P1) less than byte in register (P2), set byte in register (P3) to 0x01
0x52: Greater than - 3 Parameter - If byte in register (P1) greater than byte in register (P2), set byte in register (P3) to 0x01
--- Development ---
0xFE: Print byte as ASCII from register (invalid does nothing) - 1 Parameter - Prints the ASCII byte in register (P1) to console
0xFF: Print byte from register - 1 Parameter - Prints the byte in register (P1) to console

5
TODO Normal file
View file

@ -0,0 +1,5 @@
Highlight memory cells based on what they have been used for
add screen (VRAM?)
live memory and register editing
draw lines between registers and memory when used
add description for the current instruction

19
index.html Normal file
View file

@ -0,0 +1,19 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<script src="./dist/main.js"></script>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<div id="main">
<div id="container"></div>
<div id="printout"></div>
</div>
<button type="button" id="pause_play_button">Start</button>
<button type="button" id="step_button">step</button>
<input type="file" name="binary_upload" id="binary_upload" />
</body>
</html>

2852
package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

16
package.json Normal file
View file

@ -0,0 +1,16 @@
{
"private": true,
"devDependencies": {
"@typescript-eslint/eslint-plugin": "^6.0.0",
"@typescript-eslint/parser": "^6.0.0",
"eslint": "^8.44.0",
"ts-loader": "^9.4.4",
"typescript": "^5.1.6",
"webpack": "^5.88.2",
"webpack-cli": "^5.1.4"
},
"scripts": {
"build": "webpack --mode=production",
"watch": "webpack --mode=development --watch"
}
}

291
src/computer.ts Normal file
View file

@ -0,0 +1,291 @@
import { u8, $ } from "./etc";
// Set R1 to 255, then print R1, then go back to beginning
export enum Instr {
NoOp,
Goto,
GotoIfLowBit,
LoadToRegister,
WriteToMem,
CopyRegReg,
AssignRegister,
Add,
And,
Or,
Not,
LeftBitShift,
RightBitShift,
Equals,
LessThan,
GreaterThan,
Print,
PrintASCII,
}
const InstParamCount = new Map();
InstParamCount.set(Instr.NoOp, 0);
InstParamCount.set(Instr.Goto, 1);
InstParamCount.set(Instr.GotoIfLowBit, 2);
InstParamCount.set(Instr.LoadToRegister, 2);
InstParamCount.set(Instr.WriteToMem, 2);
InstParamCount.set(Instr.CopyRegReg, 2);
InstParamCount.set(Instr.AssignRegister, 2);
InstParamCount.set(Instr.Add, 2);
InstParamCount.set(Instr.And, 2);
InstParamCount.set(Instr.Or, 2);
InstParamCount.set(Instr.Not, 1);
InstParamCount.set(Instr.LeftBitShift, 2);
InstParamCount.set(Instr.RightBitShift, 2);
InstParamCount.set(Instr.Equals, 3);
InstParamCount.set(Instr.LessThan, 3);
InstParamCount.set(Instr.GreaterThan, 3);
InstParamCount.set(Instr.Print, 1);
InstParamCount.set(Instr.PrintASCII, 1);
export type TempInstrState = {
pos: u8;
params_found: number;
instr: Instr;
params: Uint8Array;
};
export type ComputerState = {
memory: Uint8Array;
program_counter: u8;
registers: Uint8Array;
current_instruction: TempInstrState | null;
};
export class Computer {
private memory: Uint8Array;
private program_counter: u8;
private registers: Uint8Array;
private current_instr: TempInstrState | null;
private state_change_callback: (c: ComputerState) => void;
constructor(callback: (c: ComputerState) => void) {
// 256 bytes for both program and general purpose memory.
this.memory = new Uint8Array(256);
this.registers = new Uint8Array(8);
this.program_counter = 0;
this.current_instr = null;
this.state_change_callback = callback;
}
cycle(): void {
const current_byte = this.memory[this.program_counter];
if (this.current_instr === null) {
const parsed_instruction = Computer.parse_instruction(current_byte);
if (parsed_instruction === null) {
console.log("invalid instruction");
this.step_forward();
return;
}
const instr_param_count = InstParamCount.get(parsed_instruction);
this.current_instr = {
pos: this.program_counter,
instr: parsed_instruction,
params_found: 0,
params: new Uint8Array(instr_param_count),
};
}
if (this.current_instr.pos === this.program_counter) {
this.step_forward();
return;
}
if (this.current_instr.params.length !== this.current_instr.params_found) {
// console.log(`Parameter count not fulfilled. Found new parameter ${current_byte}`);
this.current_instr.params[this.current_instr.params_found] = current_byte;
this.current_instr.params_found += 1;
}
if (this.current_instr.params.length !== this.current_instr.params_found) {
this.step_forward();
return;
}
const should_step = this.execute_instruction(this.current_instr);
this.current_instr = null;
if (should_step) {
this.step_forward();
} else {
this.state_change_callback(this.get_state());
}
}
private execute_instruction(inst: TempInstrState): boolean {
const instr_param_count = InstParamCount.get(inst.instr);
const current_pram_count = inst.params.length;
if (instr_param_count !== current_pram_count) {
throw new Error(
`Tried executing instruction #${inst.instr} without proper parameters. (has ${current_pram_count}, needs ${instr_param_count})`
);
}
switch (inst.instr) {
case Instr.Print: {
const [register_no] = inst.params;
const value = this.registers[register_no];
console.log(value);
break;
}
case Instr.Goto: {
const [parameter] = inst.params;
console.log(`Goto ${parameter}`);
this.program_counter = parameter;
return false;
}
case Instr.GotoIfLowBit: {
const [mem_address, register_no] = inst.params;
if (this.registers[register_no] % 2 === 1) {
this.program_counter = mem_address;
return false;
}
return true;
}
case Instr.AssignRegister: {
const [register_no, new_value] = inst.params;
if (register_no >= this.registers.length) {
throw new Error(`Got register number ${register_no} in assign register`);
}
console.log(`Set register ${register_no} to ${new_value}`);
this.registers[register_no] = new_value;
break;
}
case Instr.LoadToRegister: {
const [register_no, mem_address] = inst.params;
this.registers[register_no] = this.memory[this.registers[mem_address]];
break;
}
case Instr.WriteToMem: {
const [register_no, mem_address] = inst.params;
this.memory[mem_address] = this.memory[this.registers[mem_address]];
break;
}
case Instr.Add: {
const [register_1, register_2] = inst.params;
this.registers[register_1] += this.registers[register_2];
break;
}
case Instr.And: {
const [register_no_1, register_no_2] = inst.params;
this.registers[register_no_1] &= this.registers[register_no_2];
break;
}
case Instr.Or: {
const [register_no_1, register_no_2] = inst.params;
this.registers[register_no_1] |= this.registers[register_no_2];
break;
}
case Instr.Not: {
const [register_no_1] = inst.params;
this.registers[register_no_1] = ~this.registers[register_no_1];
break;
}
case Instr.LeftBitShift: {
const [register_no_1, register_no_2] = inst.params;
this.registers[register_no_1] <<= this.registers[register_no_2];
break;
}
case Instr.RightBitShift: {
const [register_no_1, register_no_2] = inst.params;
this.registers[register_no_1] >>= this.registers[register_no_2];
break;
}
case Instr.Equals: {
const [register_out, register_no_1, register_no_2] = inst.params;
if (this.registers[register_no_1] === this.registers[register_no_2]) {
this.registers[register_out] = 0x01;
} else {
this.registers[register_out] = 0x00;
}
break;
}
case Instr.LessThan: {
const [register_out, register_no_1, register_no_2] = inst.params;
if (this.registers[register_no_1] < this.registers[register_no_2]) {
this.registers[register_out] = 0x01;
}
break;
}
case Instr.GreaterThan: {
const [register_out, register_no_1, register_no_2] = inst.params;
if (this.registers[register_no_1] > this.registers[register_no_2]) {
this.registers[register_out] = 0x01;
}
break;
}
case Instr.PrintASCII: {
const [register_num] = inst.params;
const ASCIIbyte = this.registers[register_num];
const char = String.fromCharCode(ASCIIbyte);
console.log(char);
$("printout").textContent += char;
break;
}
default:
break;
}
return true;
}
load_program(program: Array<u8>): void {
const max_loop = Math.min(this.memory.length, program.length);
for (let i = 0; i < max_loop; i++) {
this.memory[i] = program[i];
}
this.program_counter = 0;
this.state_change_callback(this.get_state());
}
private step_forward(): void {
this.program_counter = (this.program_counter + 1) % 256;
this.state_change_callback(this.get_state());
}
get_state(): ComputerState {
return {
memory: this.memory,
program_counter: this.program_counter,
registers: this.registers,
current_instruction: this.current_instr,
};
}
static parse_instruction(byte: u8): null | Instr {
if (byte === 0x00) return Instr.NoOp;
if (byte === 0x10) return Instr.Goto;
if (byte === 0x11) return Instr.GotoIfLowBit;
if (byte === 0x20) return Instr.LoadToRegister;
if (byte === 0x21) return Instr.WriteToMem;
if (byte === 0x2f) return Instr.AssignRegister;
if (byte === 0x28) return Instr.CopyRegReg;
if (byte === 0x40) return Instr.Add;
if (byte === 0x48) return Instr.And;
if (byte === 0x49) return Instr.Or;
if (byte === 0x4a) return Instr.Not;
if (byte === 0x4b) return Instr.LeftBitShift;
if (byte === 0x4c) return Instr.RightBitShift;
if (byte === 0x50) return Instr.Equals;
if (byte === 0x51) return Instr.LessThan;
if (byte === 0x51) return Instr.GreaterThan;
if (byte === 0xff) return Instr.Print;
if (byte === 0xfe) return Instr.PrintASCII;
return null;
}
}

12
src/etc.ts Normal file
View file

@ -0,0 +1,12 @@
// The u8 type represents an unsigned 8bit integer: byte. It does not add any safety, other than as a hint to the programmer.
export type u8 = number;
export const $ = (s: string): HTMLElement => document.getElementById(s) as HTMLElement;
export function el(type: string, id?: string): HTMLElement {
const div = document.createElement(type);
if (id === undefined) {
return div;
}
div.id = id;
return div;
}

52
src/index.ts Normal file
View file

@ -0,0 +1,52 @@
import { Computer } from "./computer";
import { $ } from "./etc";
import { UI } from "./ui";
function main(): void {
// const program = [0x2f, 0x01, 0x01, 0x40, 0x00, 0x01, 0x21, 0x00, 0x02, 0x10, 0x00];
const program = [0x2f, 0x00, 0x49, 0xfe, 0x00, 0x10, 0x03];
const container = document.getElementById("container");
if (container === null) {
throw new Error("no");
}
const ui = new UI(container);
const computer = new Computer(ui.stateUpdateEvent.bind(ui));
computer.load_program(program);
ui.set_step_func(computer.cycle.bind(computer));
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(<any>window).comp = computer;
// eslint-disable-next-line prefer-arrow-callback
$("binary_upload").addEventListener("change", function (e) {
if (e.target === null) {
return;
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any, prefer-destructuring
const file: File = (<any>e.target).files[0];
const reader = new FileReader();
console.log(file);
reader.addEventListener("load", (e) => {
if (e.target !== null) {
const data = e.target.result;
if (data instanceof ArrayBuffer) {
const view = new Uint8Array(data);
const array = [...view];
ui.stop_auto();
computer.load_program(array);
} else {
console.log("not array");
}
}
});
reader.readAsArrayBuffer(file);
});
}
document.addEventListener("DOMContentLoaded", () => {
main();
});

151
src/ui.ts Normal file
View file

@ -0,0 +1,151 @@
import { ComputerState } from "./computer";
import { $, el } from "./etc";
export class UI {
container: HTMLElement;
program_memory: HTMLElement;
program_memory_cells: Array<HTMLElement>;
registers: HTMLElement;
register_cells: Array<HTMLElement>;
step_func: null | (() => void);
auto_running: boolean;
constructor(parent: HTMLElement) {
this.container = parent;
const program_mem = el("div", "program_memory");
this.program_memory_cells = [];
for (let i = 0; i < 256; i++) {
const mem_cell = el("div", `p_${i}`);
mem_cell.textContent = "0x00";
program_mem.appendChild(mem_cell);
this.program_memory_cells.push(mem_cell);
}
this.program_memory_cells[0].classList.add("div", "program_counter");
this.program_memory = program_mem;
this.register_cells = [];
const registers = el("div", "registers");
for (let i = 0; i < 8; i++) {
const reg_cell = el("div", `R_${i}`);
reg_cell.textContent = "00";
reg_cell.setAttribute("contenteditable", "true");
reg_cell.setAttribute("spellcheck", "false");
registers.appendChild(reg_cell);
this.register_cells.push(reg_cell);
}
// eslint-disable-next-line prefer-arrow-callback
registers.addEventListener("input", function (e) {
const allowed_chars = "0123456789ABCDEFG";
const r = e.target as HTMLElement;
let data = (r.textContent as string).toUpperCase();
for (let i = 0; i < data.length; i++) {
if (!allowed_chars.includes(data[i])) {
data = "00";
break;
}
}
if (data.length > 2) {
// data = r.textContent?.substring(0, 2) ?? "00";
}
e.preventDefault();
return false;
// r.textContent = ;
});
registers.addEventListener("keydown", (e) => {
if (e.key === "Enter") {
e.preventDefault();
(e.target as HTMLElement).blur();
}
});
registers.addEventListener("blur", (e) => {
const allowed_chars = "0123456789ABCDEFG";
const r = e.target as HTMLElement;
const data = (r.textContent as string).toUpperCase();
});
this.registers = registers;
this.container.append(registers, program_mem);
this.step_func = null;
this.auto_running = false;
const pp_button = $("pause_play_button");
if (pp_button === null) {
throw new Error("Cant find pause_play button");
}
pp_button.addEventListener("click", () => {
if (this.auto_running) {
this.stop_auto();
pp_button.textContent = "Starp";
} else {
this.start_auto();
pp_button.textContent = "Storp";
}
});
document.getElementById("step_button")?.addEventListener("click", () => {
if (this.auto_running) {
this.stop_auto();
}
if (this.step_func === null) {
return;
}
this.step_func();
});
}
start_auto(speed: number = 0): void {
if (this.step_func === null) {
return;
}
if (this.auto_running) {
return;
}
this.auto_running = true;
const loop = (): void => {
if (this.step_func === null) {
this.auto_running = false;
return;
}
if (this.auto_running === false) {
return;
}
this.step_func();
setTimeout(loop, speed);
};
loop();
}
stop_auto(): void {
this.auto_running = false;
}
set_step_func(f: () => void): void {
this.step_func = f;
}
stateUpdateEvent(state: ComputerState): void {
for (let i = 0; i < 256; i++) {
const current = this.program_memory_cells[i];
current.className = "";
current.textContent = state.memory[i].toString(16).toUpperCase().padStart(2, "0");
}
this.program_memory_cells[state.program_counter].classList.add("program_counter");
const current_instr = state.current_instruction;
if (current_instr !== null) {
this.program_memory_cells[current_instr.pos].classList.add("current_instruction");
for (let i = 0; i < current_instr.params_found; i++) {
const offset = i + 1 + current_instr.pos;
this.program_memory_cells[offset].classList.add("instruction_argument");
}
}
for (let i = 0; i < state.registers.length; i++) {
const new_text = state.registers[i].toString(16).toUpperCase().padStart(2, "0");
const old = this.register_cells[i].textContent;
if (new_text !== old) {
this.register_cells[i].textContent = new_text;
}
}
}
}

61
style.css Normal file
View file

@ -0,0 +1,61 @@
#program_memory {
display: grid;
grid-template-columns: 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr;
grid-gap: 5px;
padding: 10px;
}
#program_memory div {
aspect-ratio: 1;
text-align: center;
margin: auto;
}
body {
color: gray;
background-color: black;
font-size: 2.5em;
font-family: monospace;
}
#main {
display: flex;
flex-direction: row;
}
#printout {
border: 4px dashed yellow;
width: 1000px;
padding: 10px;
margin-left: 20px;
word-wrap: break-word;
}
#program_memory div.program_counter {
outline: 3px solid orange;
}
#program_memory {
border: 5px solid yellow;
}
#program_memory div.instruction_argument {
outline: 3px dashed purple;
}
#program_memory div.current_instruction {
outline: 3px dashed greenyellow;
}
#registers {
border: 5px solid yellow;
max-width: fit-content;
display: flex;
column-gap: 5px;
padding: 10px;
}
#container {
font-family: monospace;
max-width: min-content;
max-height: min-content;
}

21
tsconfig.json Executable file
View file

@ -0,0 +1,21 @@
{
"compilerOptions": {
"lib": [
"esnext",
"dom",
"DOM.Iterable"
],
"isolatedModules": true,
"target": "ES6",
"module": "ES6",
"rootDir": "src/",
"sourceMap": true,
"strict": true
},
"include": [
"src"
],
"exclude": [
"node_modules"
]
}

35
webpack.config.js Executable file
View file

@ -0,0 +1,35 @@
const path = require("path");
const config = {
entry: "./src/index.ts",
module: {
rules: [
{
test: /\.ts$/,
use: "ts-loader",
exclude: /node_modules/,
},
{
test: /\.html/,
type: "asset/resource"
}
],
},
resolve: {
extensions: [".ts"],
},
output: {
filename: "main.js",
path: path.resolve(__dirname, "dist"),
},
};
module.exports = (env, argv) => {
if (argv.mode === "development") {
config.devtool = "source-map";
}
if (argv.mode === "production") {
}
return config;
};