Event based UI. Abstract instructions. Colored mem
This commit is contained in:
parent
51751c568b
commit
ea7ea322f7
|
@ -41,7 +41,6 @@
|
|||
"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",
|
||||
|
@ -54,7 +53,6 @@
|
|||
"no-param-reassign":"warn",
|
||||
"prefer-arrow-callback":"warn",
|
||||
"no-array-constructor": "warn",
|
||||
"object-shorthand": "warn",
|
||||
"no-empty": "off",
|
||||
"no-self-compare": "warn",
|
||||
"eqeqeq": "warn",
|
||||
|
|
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -1,3 +1,3 @@
|
|||
node_modules
|
||||
out/
|
||||
dist/
|
||||
makefile
|
|
@ -4,6 +4,6 @@
|
|||
"semi": true,
|
||||
"endOfLine": "lf",
|
||||
"singleQuote": false,
|
||||
"printWidth": 100
|
||||
"printWidth": 120
|
||||
|
||||
}
|
2
ISA.txt
2
ISA.txt
|
@ -9,6 +9,8 @@ INSTRUCTIONS
|
|||
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 ---
|
||||
0x30: increment register - 1 Parameter - Increments register (P1) by 1.
|
||||
0x31: decrement register - 1 Parameter - Decrements register (P1) by 1.
|
||||
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 ---
|
||||
|
|
11
TODO
11
TODO
|
@ -1,5 +1,6 @@
|
|||
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
|
||||
Add screen (VRAM?)
|
||||
Live memory and register editing (Probably should pause autostep when it reaches the cell you're modifying)
|
||||
Draw lines between registers and memory when used
|
||||
Explain mode to explain what instructions are doing
|
||||
Window showing ISA
|
||||
Save binary button
|
11
index.html
11
index.html
|
@ -2,7 +2,7 @@
|
|||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<script src="./dist/main.js"></script>
|
||||
<script src="main.js"></script>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Document</title>
|
||||
<link rel="stylesheet" href="style.css" />
|
||||
|
@ -26,13 +26,8 @@
|
|||
<button type="button" id="pause_play_button">Start</button>
|
||||
<button type="button" id="step_button">Step</button>
|
||||
<label for="binary_upload" class="button">Load Binary</label>
|
||||
<input
|
||||
id="binary_upload"
|
||||
name="binary_upload"
|
||||
id="binary_upload"
|
||||
style="visibility: hidden"
|
||||
type="file"
|
||||
/>
|
||||
<input id="binary_upload" name="binary_upload" id="binary_upload" style="visibility: hidden" type="file" />
|
||||
</div>
|
||||
<pre id="ISA"></pre>
|
||||
</body>
|
||||
</html>
|
||||
|
|
303
src/computer.ts
303
src/computer.ts
|
@ -1,58 +1,12 @@
|
|||
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,
|
||||
HaltCatchFire,
|
||||
}
|
||||
|
||||
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);
|
||||
InstParamCount.set(Instr.HaltCatchFire, 0);
|
||||
import { CpuEvent, MemoryCellType } from "./events";
|
||||
import { u8 } from "./etc";
|
||||
import { EventHandler } from "./eventHandler";
|
||||
import { ConstParam, Instruction, ISA, MemorParam, RegisParam } from "./instructionSet";
|
||||
|
||||
export type TempInstrState = {
|
||||
pos: u8;
|
||||
params_found: number;
|
||||
instr: Instr;
|
||||
instr: Instruction;
|
||||
params: Uint8Array;
|
||||
};
|
||||
export type ComputerState = {
|
||||
|
@ -67,235 +21,132 @@ export class Computer {
|
|||
private program_counter: u8;
|
||||
private registers: Uint8Array;
|
||||
private current_instr: TempInstrState | null;
|
||||
events: EventHandler<CpuEvent>;
|
||||
|
||||
private state_change_callback: (c: ComputerState) => void;
|
||||
constructor(callback: (c: ComputerState) => void) {
|
||||
constructor() {
|
||||
// 256 bytes for both program and general purpose memory.
|
||||
this.memory = new Uint8Array(256);
|
||||
// 8 registers
|
||||
this.registers = new Uint8Array(8);
|
||||
this.program_counter = 0;
|
||||
this.current_instr = null;
|
||||
this.events = new EventHandler<CpuEvent>();
|
||||
|
||||
this.state_change_callback = callback;
|
||||
// Add events
|
||||
for (const [, e_type] of Object.entries(CpuEvent)) {
|
||||
this.events.register_event(e_type as CpuEvent);
|
||||
}
|
||||
this.events.seal();
|
||||
}
|
||||
|
||||
cycle(): void {
|
||||
const current_byte = this.memory[this.program_counter];
|
||||
|
||||
if (this.current_instr === null) {
|
||||
const parsed_instruction = Computer.parse_instruction(current_byte);
|
||||
const parsed_instruction = ISA.getInstruction(current_byte);
|
||||
if (parsed_instruction === null) {
|
||||
// console.log("invalid instruction");
|
||||
this.events.dispatch(CpuEvent.MemoryByteParsed, {
|
||||
type: MemoryCellType.InvalidInstruction,
|
||||
pos: this.program_counter,
|
||||
});
|
||||
console.log(`Invalid instruction: ${current_byte.toString(16).toUpperCase().padStart(2, "0")}`);
|
||||
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),
|
||||
params: new Uint8Array(parsed_instruction.params.length),
|
||||
};
|
||||
this.events.dispatch(CpuEvent.MemoryByteParsed, {
|
||||
type: MemoryCellType.Instruction,
|
||||
pos: this.program_counter,
|
||||
instr: parsed_instruction,
|
||||
});
|
||||
}
|
||||
|
||||
if (this.current_instr.pos === this.program_counter) {
|
||||
if (this.current_instr.pos === this.program_counter && this.current_instr.params.length > 0) {
|
||||
this.step_forward();
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.current_instr.params.length !== this.current_instr.params_found) {
|
||||
let a;
|
||||
const b = this.current_instr.instr.params[this.current_instr.params_found];
|
||||
if (b instanceof ConstParam) {
|
||||
a = MemoryCellType.Constant;
|
||||
} else if (b instanceof RegisParam) {
|
||||
a = MemoryCellType.Register;
|
||||
} else if (b instanceof MemorParam) {
|
||||
a = MemoryCellType.Memory;
|
||||
}
|
||||
this.events.dispatch(CpuEvent.MemoryByteParsed, { type: a, pos: this.program_counter, param: b });
|
||||
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 execution_post_action_state = {
|
||||
should_step: true,
|
||||
noStep: function (): void {
|
||||
this.should_step = false;
|
||||
},
|
||||
event: this.events.dispatch.bind(this.events),
|
||||
};
|
||||
this.current_instr.instr.execute(this, this.current_instr.params, execution_post_action_state);
|
||||
|
||||
const should_step = this.execute_instruction(this.current_instr);
|
||||
this.current_instr = null;
|
||||
|
||||
if (should_step) {
|
||||
if (execution_post_action_state.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})`
|
||||
);
|
||||
getMemory(address: u8): u8 {
|
||||
return this.memory[address];
|
||||
}
|
||||
setMemory(address: u8, value: u8): void {
|
||||
this.events.dispatch(CpuEvent.MemoryChanged, { address, value });
|
||||
this.memory[address] = value;
|
||||
}
|
||||
|
||||
switch (inst.instr) {
|
||||
case Instr.Goto: {
|
||||
const [parameter] = inst.params;
|
||||
// console.log(`Goto ${parameter}`);
|
||||
this.program_counter = parameter;
|
||||
return false;
|
||||
getRegister(register_no: u8): u8 {
|
||||
return this.registers[register_no];
|
||||
}
|
||||
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.Print: {
|
||||
const [register_no] = inst.params;
|
||||
const value = this.registers[register_no];
|
||||
$("printout").textContent += this.registers[register_no].toString(10);
|
||||
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;
|
||||
}
|
||||
case Instr.HaltCatchFire: {
|
||||
throw new Error("FIRE FIRE FIRE FIRE");
|
||||
}
|
||||
case Instr.CopyRegReg: {
|
||||
const [register_no_to, register_no_from] = inst.params;
|
||||
this.registers[register_no_to] = this.registers[register_no_from];
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return true;
|
||||
setRegister(register_no: u8, value: u8): void {
|
||||
this.events.dispatch(CpuEvent.RegisterChanged, { register_no, value });
|
||||
this.registers[register_no] = value;
|
||||
}
|
||||
|
||||
load_program(program: Array<u8>): void {
|
||||
getProgramCounter(): u8 {
|
||||
return this.program_counter;
|
||||
}
|
||||
setProgramCounter(new_value: u8): void {
|
||||
this.events.dispatch(CpuEvent.ProgramCounterChanged, { counter: new_value });
|
||||
this.program_counter = new_value;
|
||||
}
|
||||
|
||||
reset(): void {
|
||||
this.memory = new Uint8Array(256);
|
||||
this.registers = new Uint8Array(8);
|
||||
this.current_instr = null;
|
||||
this.program_counter = 0;
|
||||
this.events.dispatch(CpuEvent.Reset, null);
|
||||
}
|
||||
|
||||
load_memory(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.events.dispatch(CpuEvent.MemoryChanged, { address: i, value: 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;
|
||||
if (byte === 0x66) return Instr.HaltCatchFire;
|
||||
return null;
|
||||
this.events.dispatch(CpuEvent.ProgramCounterChanged, { counter: this.program_counter });
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
// 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;
|
||||
|
||||
// Jquery lite
|
||||
export const $ = (s: string): HTMLElement => document.getElementById(s) as HTMLElement;
|
||||
|
||||
export function el(type: string, id?: string): HTMLElement {
|
||||
const element = document.createElement(type);
|
||||
if (id === undefined) {
|
||||
|
|
49
src/eventHandler.ts
Normal file
49
src/eventHandler.ts
Normal file
|
@ -0,0 +1,49 @@
|
|||
export class Event<T> {
|
||||
identifier: T;
|
||||
callbacks: Array<(event_data: unknown) => void>;
|
||||
constructor(identifier: T) {
|
||||
this.identifier = identifier;
|
||||
this.callbacks = [];
|
||||
}
|
||||
}
|
||||
|
||||
export class EventHandler<T> {
|
||||
events: Array<Event<T>>;
|
||||
private sealed: boolean;
|
||||
constructor() {
|
||||
this.sealed = false;
|
||||
this.events = [];
|
||||
}
|
||||
|
||||
seal(): void {
|
||||
if (this.sealed) {
|
||||
throw new Error("Already Sealed");
|
||||
}
|
||||
this.sealed = true;
|
||||
}
|
||||
|
||||
register_event(identifier: T): void {
|
||||
if (this.sealed) {
|
||||
throw new Error("Can't add event to sealed event handler");
|
||||
}
|
||||
const event = new Event<T>(identifier);
|
||||
this.events.push(event);
|
||||
}
|
||||
dispatch(identifier: T, event_data: unknown): void {
|
||||
const event = this.events.find((e) => e.identifier === identifier);
|
||||
if (event === undefined) {
|
||||
throw new Error("Event not found");
|
||||
}
|
||||
for (const callback of event.callbacks) {
|
||||
callback(event_data);
|
||||
}
|
||||
}
|
||||
add_listener(identifier: T, callback: (event_data: unknown) => void): void {
|
||||
if (!this.sealed) throw new Error("Event handler must be sealed before adding listener");
|
||||
const event = this.events.find((e) => e.identifier === identifier);
|
||||
if (event === undefined) {
|
||||
throw new Error("No event found given identifier");
|
||||
}
|
||||
event.callbacks.push(callback);
|
||||
}
|
||||
}
|
16
src/events.ts
Normal file
16
src/events.ts
Normal file
|
@ -0,0 +1,16 @@
|
|||
export enum CpuEvent {
|
||||
MemoryChanged,
|
||||
RegisterChanged,
|
||||
MemoryByteParsed,
|
||||
ProgramCounterChanged,
|
||||
Print,
|
||||
Reset,
|
||||
}
|
||||
|
||||
export enum MemoryCellType {
|
||||
Instruction,
|
||||
InvalidInstruction,
|
||||
Register,
|
||||
Memory,
|
||||
Constant,
|
||||
}
|
19
src/index.ts
19
src/index.ts
|
@ -1,25 +1,27 @@
|
|||
import { Computer } from "./computer";
|
||||
import { $ } from "./etc";
|
||||
import { ISA } from "./instructionSet";
|
||||
import { generate_isa } from "./isaGenerator";
|
||||
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 program = [0x2f, 0x00, 0x41, 0xfe, 0x00, 0x30, 0x00, 0x10, 0x03];
|
||||
|
||||
const container = document.getElementById("container");
|
||||
const container = $("container");
|
||||
if (container === null) {
|
||||
throw new Error("no");
|
||||
}
|
||||
const computer = new Computer();
|
||||
|
||||
const ui = new UI(container);
|
||||
|
||||
const computer = new Computer(ui.state_update_event.bind(ui));
|
||||
|
||||
computer.load_program(program);
|
||||
const ui = new UI(container, computer.events);
|
||||
computer.load_memory(program);
|
||||
ui.set_step_func(computer.cycle.bind(computer));
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(<any>window).comp = computer;
|
||||
|
||||
$("ISA").textContent = generate_isa(ISA);
|
||||
|
||||
// eslint-disable-next-line prefer-arrow-callback
|
||||
$("binary_upload").addEventListener("change", function (e) {
|
||||
if (e.target === null) {
|
||||
|
@ -37,7 +39,8 @@ function main(): void {
|
|||
const view = new Uint8Array(data);
|
||||
const array = [...view];
|
||||
ui.stop_auto();
|
||||
computer.load_program(array);
|
||||
computer.reset();
|
||||
computer.load_memory(array);
|
||||
} else {
|
||||
console.log("not array");
|
||||
}
|
||||
|
|
173
src/instructionSet.ts
Normal file
173
src/instructionSet.ts
Normal file
|
@ -0,0 +1,173 @@
|
|||
import { CpuEvent } from "./events";
|
||||
import { u8 } from "./etc";
|
||||
|
||||
class ParameterType {
|
||||
readonly description: string;
|
||||
constructor(description: string) {
|
||||
this.description = description;
|
||||
}
|
||||
}
|
||||
|
||||
export class ConstParam extends ParameterType {}
|
||||
export class RegisParam extends ParameterType {}
|
||||
export class MemorParam extends ParameterType {}
|
||||
|
||||
interface GenericComputer {
|
||||
getMemory: (address: u8) => u8;
|
||||
setMemory: (address: u8, value: u8) => void;
|
||||
setProgramCounter: (address: u8) => void;
|
||||
getProgramCounter: () => u8;
|
||||
getRegister: (number: u8) => u8;
|
||||
setRegister: (number: u8, value: u8) => void;
|
||||
}
|
||||
|
||||
interface AfterExecutionComputerAction {
|
||||
// Does not step forward the program counter
|
||||
noStep: () => void;
|
||||
event: (e: CpuEvent, data: unknown) => void;
|
||||
}
|
||||
|
||||
export interface Instruction {
|
||||
readonly name: string;
|
||||
readonly desc: string;
|
||||
readonly params: Array<ParameterType>;
|
||||
execute: (computer_reference: GenericComputer, parameters: Uint8Array, a: AfterExecutionComputerAction) => void;
|
||||
}
|
||||
|
||||
export class InstructionSet {
|
||||
instructions: Map<u8, Instruction>;
|
||||
|
||||
constructor() {
|
||||
this.instructions = new Map();
|
||||
}
|
||||
|
||||
insertInstruction(hexCode: u8, instruction: Instruction): void {
|
||||
if (this.instructions.has(hexCode)) {
|
||||
throw new Error(`Instruction "${hexCode.toString(16)}" already exists`);
|
||||
}
|
||||
this.instructions.set(hexCode, instruction);
|
||||
}
|
||||
|
||||
getInstruction(hexCode: u8): Instruction | null {
|
||||
return this.instructions.get(hexCode) ?? null;
|
||||
}
|
||||
}
|
||||
|
||||
export const ISA = new InstructionSet();
|
||||
|
||||
ISA.insertInstruction(0x00, {
|
||||
name: "NoOp",
|
||||
desc: "No operation; do nothing",
|
||||
params: [],
|
||||
execute: () => {},
|
||||
});
|
||||
|
||||
ISA.insertInstruction(0x10, {
|
||||
name: "Goto",
|
||||
desc: "Moves the CPU instruction counter to the value in (P1)",
|
||||
params: [new ConstParam("new instruction counter location")],
|
||||
execute: (c, p, a) => {
|
||||
const new_address = p[0];
|
||||
c.setProgramCounter(new_address);
|
||||
a.noStep();
|
||||
},
|
||||
});
|
||||
|
||||
ISA.insertInstruction(0x20, {
|
||||
name: "LoadToRegister",
|
||||
desc: "Sets the byte in register (P1) to be the contents of memory cell (P2)",
|
||||
params: [new RegisParam(""), new MemorParam("")],
|
||||
execute(c, p) {
|
||||
const [register_no, mem_address] = p;
|
||||
c.setRegister(register_no, c.getMemory(mem_address));
|
||||
},
|
||||
});
|
||||
|
||||
ISA.insertInstruction(0x21, {
|
||||
name: "SaveToMemory",
|
||||
desc: "Writes the byte in register (P1) to the processing memory location (P2)",
|
||||
params: [new RegisParam(""), new MemorParam("")],
|
||||
execute(c, p) {
|
||||
const [register_no, mem_address] = p;
|
||||
c.setMemory(mem_address, c.getRegister(register_no));
|
||||
},
|
||||
});
|
||||
|
||||
ISA.insertInstruction(0x2f, {
|
||||
name: "AssignRegister",
|
||||
desc: "Assigns constant value (P2) to register (P1)",
|
||||
params: [new RegisParam(""), new ConstParam("")],
|
||||
execute(c, p) {
|
||||
const [register_no, value] = p;
|
||||
c.setRegister(register_no, value);
|
||||
},
|
||||
});
|
||||
|
||||
ISA.insertInstruction(0x11, {
|
||||
name: "GotoIfLowBit",
|
||||
desc: "Moves the CPU instruction counter to the value in (P1) if the value in register (P2) has the lowest bit true",
|
||||
params: [new ConstParam("new instruction counter location"), new RegisParam("Register to check")],
|
||||
execute(c, p, a) {
|
||||
const [new_address, check_register_no] = p;
|
||||
if (c.getRegister(check_register_no) % 2 === 1) {
|
||||
c.setProgramCounter(new_address);
|
||||
a.noStep();
|
||||
}
|
||||
},
|
||||
});
|
||||
ISA.insertInstruction(0x30, {
|
||||
name: "Increment",
|
||||
desc: "Increments the value within register (P1) by 1",
|
||||
params: [new RegisParam("register to be incremented")],
|
||||
execute(c, p) {
|
||||
const register_no = p[0];
|
||||
const current_value = c.getRegister(register_no);
|
||||
c.setRegister(register_no, current_value + 1);
|
||||
},
|
||||
});
|
||||
|
||||
ISA.insertInstruction(0x31, {
|
||||
name: "Decrement",
|
||||
desc: "Decrements the value within register (P1) by 1",
|
||||
params: [new RegisParam("register to be decremented")],
|
||||
execute(c, p) {
|
||||
const register_no = p[0];
|
||||
const current_value = c.getRegister(register_no);
|
||||
c.setRegister(register_no, current_value - 1);
|
||||
},
|
||||
});
|
||||
|
||||
ISA.insertInstruction(0x40, {
|
||||
name: "Add",
|
||||
desc: "Adds the contents of (P1) and (P2) and stores result to register (P1). (Overflow will be taken mod 256)",
|
||||
params: [new RegisParam(""), new RegisParam("")],
|
||||
execute(c, p) {
|
||||
const [register_no_1, register_no_2] = p;
|
||||
const new_value = (c.getRegister(register_no_1) + c.getRegister(register_no_2)) % 256;
|
||||
c.setRegister(register_no_1, new_value);
|
||||
},
|
||||
});
|
||||
|
||||
ISA.insertInstruction(0x50, {
|
||||
name: "Equals",
|
||||
desc: "If byte in register (P1) equals byte in register (P2), set byte in register (P3) to 0x01",
|
||||
params: [new RegisParam(""), new RegisParam(""), new RegisParam("")],
|
||||
execute(c, p) {
|
||||
const [register_no_1, register_no_2, register_no_3] = p;
|
||||
const truth = c.getRegister(register_no_1) === c.getRegister(register_no_2) ? 0x01 : 0x00;
|
||||
c.setRegister(register_no_3, truth);
|
||||
},
|
||||
});
|
||||
|
||||
ISA.insertInstruction(0xfe, {
|
||||
name: "PrintASCII",
|
||||
desc: "Prints the ASCII byte in register (P1) to console",
|
||||
params: [new RegisParam("")],
|
||||
execute(c, p, a) {
|
||||
const register_no = p[0];
|
||||
const asciiByte = c.getRegister(register_no);
|
||||
|
||||
const char = String.fromCharCode(asciiByte);
|
||||
a.event(CpuEvent.Print, { data: char });
|
||||
},
|
||||
});
|
23
src/isaGenerator.ts
Normal file
23
src/isaGenerator.ts
Normal file
|
@ -0,0 +1,23 @@
|
|||
import { u8 } from "./etc";
|
||||
import { Instruction, InstructionSet } from "./instructionSet";
|
||||
|
||||
export function generate_isa(iset: InstructionSet): string {
|
||||
const instructions: Array<[u8, Instruction]> = [];
|
||||
for (const kv of iset.instructions.entries()) {
|
||||
instructions.push(kv);
|
||||
}
|
||||
let output_string = "INSTRUCTIONS\n";
|
||||
let max_instr_name_len = 0;
|
||||
for (const instruction of instructions) {
|
||||
const short_description = instruction[1].name;
|
||||
max_instr_name_len = Math.max(max_instr_name_len, short_description.length);
|
||||
}
|
||||
for (const instruction of instructions) {
|
||||
const hex_code = instruction[0].toString(16).toUpperCase().padStart(2, "0");
|
||||
const short_description = instruction[1].name.padEnd(max_instr_name_len, " ");
|
||||
const parameter_count = instruction[1].params.length;
|
||||
const description = instruction[1].desc;
|
||||
output_string += `0x${hex_code}: ${short_description} - ${parameter_count} Parameter - ${description}\n`;
|
||||
}
|
||||
return output_string;
|
||||
}
|
114
src/ui.ts
114
src/ui.ts
|
@ -1,5 +1,7 @@
|
|||
import { ComputerState } from "./computer";
|
||||
import { $, el } from "./etc";
|
||||
import { CpuEvent, MemoryCellType } from "./events";
|
||||
import { $, el, u8 } from "./etc";
|
||||
import { EventHandler } from "./eventHandler";
|
||||
|
||||
export class UI {
|
||||
container: HTMLElement;
|
||||
|
@ -8,15 +10,18 @@ export class UI {
|
|||
registers: HTMLElement;
|
||||
register_cells: Array<HTMLElement>;
|
||||
step_func: null | (() => void);
|
||||
auto_running: boolean;
|
||||
constructor(parent: HTMLElement) {
|
||||
this.container = parent;
|
||||
|
||||
program_counter: u8;
|
||||
|
||||
auto_running: boolean;
|
||||
constructor(parent: HTMLElement, cpu_events: EventHandler<CpuEvent>) {
|
||||
this.container = parent;
|
||||
this.program_counter = 0;
|
||||
const program_mem = $("memory");
|
||||
this.program_memory_cells = [];
|
||||
for (let i = 0; i < 256; i++) {
|
||||
const mem_cell = el("div", `p_${i}`);
|
||||
mem_cell.textContent = "0x00";
|
||||
mem_cell.textContent = "00";
|
||||
program_mem.appendChild(mem_cell);
|
||||
this.program_memory_cells.push(mem_cell);
|
||||
}
|
||||
|
@ -28,43 +33,12 @@ export class UI {
|
|||
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;
|
||||
// }
|
||||
// }
|
||||
|
||||
// e.preventDefault();
|
||||
// return false;
|
||||
// });
|
||||
|
||||
// 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");
|
||||
|
@ -89,6 +63,60 @@ export class UI {
|
|||
}
|
||||
this.step_func();
|
||||
});
|
||||
|
||||
cpu_events.add_listener(CpuEvent.MemoryChanged, (e) => {
|
||||
const { address, value } = e as { address: u8; value: u8 };
|
||||
this.program_memory_cells[address].textContent = value.toString(16).toUpperCase().padStart(2, "0");
|
||||
});
|
||||
cpu_events.add_listener(CpuEvent.RegisterChanged, (e) => {
|
||||
const { register_no, value } = e as { register_no: u8; value: u8 };
|
||||
this.register_cells[register_no].textContent = value.toString(16).toUpperCase().padStart(2, "0");
|
||||
});
|
||||
cpu_events.add_listener(CpuEvent.ProgramCounterChanged, (e) => {
|
||||
const { counter } = e as { counter: u8 };
|
||||
this.program_memory_cells[this.program_counter].classList.remove("program_counter");
|
||||
this.program_memory_cells[counter].classList.add("program_counter");
|
||||
this.program_counter = counter;
|
||||
});
|
||||
cpu_events.add_listener(CpuEvent.Print, (e) => {
|
||||
const { data } = e as { data: u8 };
|
||||
const printout = $("printout");
|
||||
if (printout === null) {
|
||||
throw new Error("Couldn't get printout");
|
||||
}
|
||||
printout.textContent = (printout.textContent ?? "") + data;
|
||||
});
|
||||
cpu_events.add_listener(CpuEvent.Reset, () => {
|
||||
this.stop_auto();
|
||||
this.program_memory_cells.forEach((c) => {
|
||||
c.className = "";
|
||||
c.textContent = "00";
|
||||
});
|
||||
this.register_cells.forEach((r) => {
|
||||
r.textContent = "00";
|
||||
});
|
||||
this.program_counter = 0;
|
||||
});
|
||||
|
||||
const map: Map<MemoryCellType, string> = new Map();
|
||||
|
||||
map.set(MemoryCellType.Constant, "constant");
|
||||
map.set(MemoryCellType.Instruction, "instruction");
|
||||
map.set(MemoryCellType.InvalidInstruction, "invalid_instruction");
|
||||
map.set(MemoryCellType.Memory, "memory");
|
||||
map.set(MemoryCellType.Register, "register");
|
||||
cpu_events.add_listener(CpuEvent.MemoryByteParsed, (e) => {
|
||||
const { type, pos } = e as { type: MemoryCellType; pos: u8 };
|
||||
const css_class = map.get(type);
|
||||
if (css_class === undefined) {
|
||||
throw new Error("Something went wrong");
|
||||
}
|
||||
for (const other_class of map.values()) {
|
||||
if (other_class === css_class) continue;
|
||||
this.program_memory_cells[pos].classList.remove(other_class);
|
||||
}
|
||||
this.program_memory_cells[pos].classList.add(css_class);
|
||||
});
|
||||
}
|
||||
|
||||
start_auto(speed: number = 200): void {
|
||||
|
@ -109,6 +137,7 @@ export class UI {
|
|||
}
|
||||
this.step_func();
|
||||
setTimeout(loop, speed);
|
||||
// requestAnimationFrame(loop);
|
||||
};
|
||||
loop();
|
||||
}
|
||||
|
@ -122,12 +151,6 @@ export class UI {
|
|||
}
|
||||
|
||||
state_update_event(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");
|
||||
|
@ -136,12 +159,5 @@ export class UI {
|
|||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
27
style.css
27
style.css
|
@ -8,7 +8,27 @@
|
|||
grid-gap: 5px;
|
||||
padding: 10px;
|
||||
border: 5px solid yellow;
|
||||
color: lightgray;
|
||||
/* color: lightgray; */
|
||||
}
|
||||
|
||||
pre {
|
||||
font-size: 0.5em;
|
||||
}
|
||||
|
||||
#memory .instruction {
|
||||
color: greenyellow;
|
||||
}
|
||||
#memory .constant {
|
||||
color: purple;
|
||||
}
|
||||
#memory .register {
|
||||
color: orange;
|
||||
}
|
||||
#memory .memory {
|
||||
color: pink;
|
||||
}
|
||||
#memory .invalid_instruction {
|
||||
color: maroon;
|
||||
}
|
||||
|
||||
#memory div {
|
||||
|
@ -50,9 +70,10 @@ body {
|
|||
}
|
||||
|
||||
#title {
|
||||
writing-mode: sideways-lr;
|
||||
writing-mode: vertical-lr;
|
||||
text-align: left;
|
||||
user-select: none;
|
||||
text-orientation: mixed;
|
||||
transform: scale(-1, -1);
|
||||
}
|
||||
|
||||
#printout {
|
||||
|
|
Loading…
Reference in a new issue