commit 3cf5ac90e96f348ebb2c160036e68dbbdc537d61 Author: Neale Pickett Date: Fri May 27 13:39:12 2022 -0600 Working demo diff --git a/README.md b/README.md new file mode 100644 index 0000000..fa125f0 --- /dev/null +++ b/README.md @@ -0,0 +1,21 @@ +Triscit +====== + +This is a tiny RISC instruction set designed for teaching. + + +Instructions +======== + +| Number | Name | Arguments | Description | +| --- | --- | --- | --- | +| 00 | PRNT | x | Print string at x | +| 01 | READ | x | Read input, store in x | +| 02 | COMP | x y | Compare string x to y | +| 03 | JNEQ | x | If not equal, set PC to x | +| 04 | JUMP | x | Set PC to x | +| 05 | HALT | | Terminate program | +| 06 | HACF | | Burn up computer (never use this!) | +| 07 | NOOP | | Do absolutely nothing | + +This assumes the user comes with a notion of what it means to read and print. \ No newline at end of file diff --git a/binutils.mjs b/binutils.mjs new file mode 100644 index 0000000..e4dd990 --- /dev/null +++ b/binutils.mjs @@ -0,0 +1,88 @@ +const glyphs = [ + "\\x00", "\\x01", "\\x02", "\\x03", "\\x04", "\\x05", "\\x06", "\\x07", + "\\x08", "\\x09", "\\x0A", "\\x0B", "\\x0C", "\\x0D", "\\x0E", "\\x0F", + "⏵", "⏴", "↕", "‼", "¶", "§", "‽", "↨", "↑", "↓", "→", "←", "∟", "↔", "⏶", "⏷", + " ", "!", "\"", "#", "$", "%", "&", "'", "(", ")", "*", "+", ",", "-", ".", "/", + "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", ":", ";", "<", "=", ">", "?", + "@", "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", + "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z", "[", "\\", "]", "^", "_", + "`", "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", + "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z", "{", "|", "}", "~", "⌂", + + "Ç", "ü", "é", "â", "ä", "à", "å", "ç", "ê", "ë", "è", "ï", "î", "ì", "Ä", "Å", + "É", "æ", "Æ", "ô", "ö", "ò", "û", "ù", "ÿ", "Ö", "Ü", "¢", "£", "¥", "₧", "ƒ", + "á", "í", "ó", "ú", "ñ", "Ñ", "ª", "º", "¿", "⌐", "¬", "½", "¼", "¡", "«", "»", + "░", "▒", "▓", "│", "┤", "╡", "╢", "╖", "╕", "╣", "║", "╗", "╝", "╜", "╛", "┐", + "└", "┴", "┬", "├", "─", "┼", "╞", "╟", "╚", "╔", "╩", "╦", "╠", "═", "╬", "╧", + "╨", "╤", "╥", "╙", "╘", "╒", "╓", "╫", "╪", "┘", "┌", "█", "▄", "▌", "▐", "▀", + "α", "ß", "Γ", "π", "Σ", "σ", "µ", "τ", "Φ", "Θ", "Ω", "δ", "∞", "φ", "ε", "∩", + "≡", "±", "≥", "≤", "⌠", "⌡", "÷", "≈", "°", "∞", "⊻", "√", "ⁿ", "²", "■", "¤", +] + +/** + * Encode a buffer using fluffy glyphs + * + * @param {Uint8Array} buf Buffer to encode + * @returns {String} Glyph-encoded string + */ +function Stringify(buf) { + let ret = [] + for (let i of buf) { + ret.push(glyphs[i]) + } + return ret.join("") +} + + +/** + * Hex encode a buffer + * + * @param {Uint8Array} buf Buffer to encode + * @returns {String} Hexlified version + */ +function Hexlify(buf) { + let ret = [] + for (let b of buf) { + let hex = "00" + b.toString(16) + ret.push(hex.slice(-2)) + } + return ret.join(" ") +} + +/** + * Hex decode a string + * + * @param {String} str String to decode + * @returns {Uint8Array} Decoded array + */ +function Unhexlify(str) { + let a = str.match(/[0-9a-fA-F]{2}/g).map(v => parseInt(v, 16)) + return new Uint8Array(a) +} + +/** + * Read a NULL-Terminated string from a buffer. + * + * @param {Uint8Arrary} buf Buffer to read + * @param {Boolean} includeNull True to include the trailing NUL + * @returns {Uint8Array} String + */ +function CString(buf, includeNull=false) { + let end = buf.indexOf(0) + if (end == -1) { + return buf + } + return buf.slice(0, end + (includeNull?1:0)) +} + +/** + * Unescapes a string with things like "\x02" in it. + * + * @param {String} str String to unescape + * @returns {String} Unescaped string + */ +function Unescape(str) { + return str.replace(/\\x([0-9]+)/, (_, p1) => String.fromCharCode(p1)) +} + +export {Stringify, Hexlify, Unhexlify, CString, Unescape} diff --git a/httpd.conf b/httpd.conf new file mode 100644 index 0000000..b2cfc55 --- /dev/null +++ b/httpd.conf @@ -0,0 +1 @@ +.mjs:text/javascript diff --git a/index.html b/index.html new file mode 100644 index 0000000..f96ca3a --- /dev/null +++ b/index.html @@ -0,0 +1,81 @@ + + + + Triscit + + + + + + + +
+
+
+
+
+
+ PC= + + + +
+
+ + + +
+
+
+
+
+ +
+
+
+ + +
+
+ + +
+
+ + +
+
+
+ +
+
+

Instructions

+
+
+
+
+
+ + + + + \ No newline at end of file diff --git a/run.sh b/run.sh new file mode 100755 index 0000000..b85b1cb --- /dev/null +++ b/run.sh @@ -0,0 +1,3 @@ +#! /bin/sh + +busybox httpd -fv -p 8081 -c httpd.conf diff --git a/triscit.css b/triscit.css new file mode 100644 index 0000000..b3b48c8 --- /dev/null +++ b/triscit.css @@ -0,0 +1,16 @@ +#instructions th.addr { + text-align: right; + color: brown; +} + +td.name { + font-weight: bold; +} + +td.args { + color: green; +} + +td.hex { + color: #777; +} diff --git a/triscit.mjs b/triscit.mjs new file mode 100644 index 0000000..58a3e7e --- /dev/null +++ b/triscit.mjs @@ -0,0 +1,293 @@ +import * as Binutils from "./binutils.mjs" + +const FLAG_NEGATIVE = 1 << 0 +const FLAG_HALT = 1 << 1 +const FLAG_ABLAZE = 1 << 2 + +class Instruction { + constructor(name, args, description) { + this.Name = name + this.Args = args + this.Description = description + } +} + +const Instructions = [ + new Instruction("NOOP", [], "Do nothing"), + new Instruction("PRNT", ["x"], "Print string at x"), + new Instruction("READ", ["x"], "Read input, store in x"), + new Instruction("CMPS", ["x", "y"], "Compare string x to string y"), + new Instruction("JNEQ", ["x"], "If not equal, set PC to x"), + new Instruction("JUMP", ["x"], "Set PC to x"), + new Instruction("HALT", [], "Terminate program"), + new Instruction("HACF", [], "Halt And Catch Fire (never use this!)"), +] + +function disassemble(program) { + let b = program[0] + let inst = Instructions[b] + if (inst) { + let nargs = inst.Args.length + let args = [] + for (let i = 0; i < nargs; i++) { + args.push(program[i+1]) + } + + if (1+nargs <= program.length) { + // Only return an opcode if there was enough data for its arguments as well + return {length: 1+nargs, name: inst.Name, args: args} + } + } + + let data = Binutils.CString(program, true) + let s = Binutils.Stringify(data) + return {length: data.length, name: "DATA", args: [s]} +} + +class CPU { + /** + * @param {Uint8Array} program Program to load + * @param {string} input input string + * @param {Number} pc initial program counter + * @param {Number} flags initial flags + */ + constructor(program, input, output="", pc=0, flags=0) { + this.Program = program + this.Input = input + this.Output = output + this.PC = pc + this.Flags = flags + } + + Clone() { + return new CPU(this.Program, this.Input, this.Output, this.PC, this.Flags) + } + + DisassembleProgram() { + let ret = [] + let prog1 = this.Program.slice(0, this.PC) + let addr = 0 + for (let prog of [prog1, this.Program]) { + while (addr < prog.length) { + let subprog = prog.slice(addr) + let inst = disassemble(subprog) + ret.push({ + addr, + buf: this.Program.slice(addr, addr+inst.length), + name: inst.name, + args: inst.args, + }) + addr += inst.length + } + } + return ret + } + + Step() { + let prog = this.Program.slice(this.PC) + let inst = disassemble(prog) + this.Flags = 0 + switch (inst.name) { + case "JUMP": + this.PC = inst.args[0] + return + case "JNEQ": + if (this.Flags && FLAG_NEGATIVE) { + this.PC = inst.args[0] + return + } + break + case "READ": { + let addr = inst.args[0] + let b = new TextEncoder().encode(this.Input + "\0") + let newproglen = Math.max(this.Program.length, addr+b.length) + let newprog = new Uint8Array(newproglen) + + for (let i = 0; i < addr; i++) { + newprog[i] = this.Program[i] + } + for (let i = 0; i < b.length; i++) { + newprog[addr+i] = b[i] + } + for (let i = addr+b.length; i < this.Program.length; i++) { + newprog[i] = this.Program[i] + } + this.Program = newprog + break + } + case "PRNT": { + let addr = inst.args[0] + let prog = this.Program.slice(addr) + this.Output = Binutils.CString(prog) + console.log(addr, this.Output) + break + } + case "CMPS": { + let a = Binutils.CString(this.Program.slice(inst.args[0])) + let b = Binutils.CString(this.Program.slice(inst.args[1])) + if (a == b) { + this.Flags = FLAG_NEGATIVE + } + } + case "HALT": + // Don't modify PC + this.Flags = FLAG_HALT + return + case "HACF": + this.Flags = FLAG_HALT | FLAG_ABLAZE + return + case "DATA": + // Keep stepping through data one byte at a time trying to execute something + inst.length = 1 + break + } + this.PC += inst.length + } +} + +class Triscit { + constructor(program=[]) { + this.program = program + this.Init() + this.Reset() + } + + Init() { + for (let e of document.querySelectorAll("[data-control]")) { + switch (e.dataset.control) { + case "step": + e.addEventListener("click", () => this.Step()) + break + case "back": + e.addEventListener("click", () => this.Unstep()) + break + case "reset": + e.addEventListener("click", () => this.Reset()) + break + case "input": + e.addEventListener("input", () => this.SetInput()) + break + case "program": + e.addEventListener("input", () => this.SetProgram()) + } + } + + let ih = document.querySelector("#instructions-help") + for (let i = 0; i < Instructions.length; i++) { + let inst = Instructions[i] + let doc = document.querySelector("template#instruction-help").content.cloneNode(true) + let tr = doc.firstElementChild + tr.querySelector(".num").textContent = i + tr.querySelector(".name").textContent = inst.Name + tr.querySelector(".description").textContent = inst.Description + ih.appendChild(doc) + } + + this.SetInput() + this.SetProgram() + } + + Reset() { + this.cpu = new CPU(this.program, this.input) + this.stack = [] + this.Refresh() + } + + Refresh() { + let inste = document.querySelector("#instructions") + while (inste.firstChild) inste.firstChild.remove() + for (let i of this.cpu.DisassembleProgram()) { + let doc = document.querySelector("template#instruction").content.cloneNode(true) + let tr = doc.firstElementChild + tr.querySelector(".addr").textContent = i.addr + tr.querySelector(".name").textContent = i.name + tr.querySelector(".hex").textContent = Binutils.Hexlify(i.buf) + + let args = tr.querySelector(".args") + for (let a of i.args) { + args.textContent += `${a} ` + } + + if (i.addr == this.cpu.PC) { + tr.classList.add("is-selected") + } + inste.appendChild(tr) + } + + for (let e of document.querySelectorAll("[data-flag]")) { + let mask = 0 + let className = "is-info" + switch (e.dataset.flag) { + case "negative": + mask = FLAG_NEGATIVE + break + case "halt": + mask = FLAG_HALT + break + case "fire": + mask = FLAG_ABLAZE + className = "is-danger" + break + } + if (this.cpu.Flags & mask) { + e.classList.add(className) + } else { + e.classList.remove(className) + } + } + + for (let e of document.querySelectorAll("[data-value]")) { + switch (e.dataset.value) { + case "pc": + e.textContent = this.cpu.PC + break + case "output": + console.log(this.cpu.Output) + e.value = Binutils.Stringify(this.cpu.Output) + break + } + } + } + + Step() { + if (this.cpu.Flags & FLAG_HALT) { + return + } + this.stack.push(this.cpu) + this.cpu = this.cpu.Clone() + this.cpu.Step() + this.Refresh() + } + + Unstep() { + if (this.stack.length == 0) { + return + } + this.cpu = this.stack.pop() + this.Refresh() + } + + SetProgram() { + let e =document.querySelector('[data-control="program"]') + this.program = Binutils.Unhexlify(e.value || "") + this.Reset() + } + + SetInput() { + let e = document.querySelector('[data-control="input"]') + let v = e.value || "" + this.input = Binutils.Unescape(v) + this.Reset() + } +} + +function init() { + let app = new Triscit() + window.app = app +} + +if (document.readyState === "loading") { + document.addEventListener("DOMContentLoaded", init) +} else { + init() +}