Working demo

This commit is contained in:
Neale Pickett 2022-05-27 13:39:12 -06:00
commit 3cf5ac90e9
7 changed files with 503 additions and 0 deletions

21
README.md Normal file
View File

@ -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.

88
binutils.mjs Normal file
View File

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

1
httpd.conf Normal file
View File

@ -0,0 +1 @@
.mjs:text/javascript

81
index.html Normal file
View File

@ -0,0 +1,81 @@
<!DOCTYPE html>
<html>
<head>
<title>Triscit</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@0.9.4/css/bulma.min.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@mdi/font@6.5.95/css/materialdesignicons.min.css">
<script src="triscit.mjs" type="module"></script>
<link rel="stylesheet" href="triscit.css">
</head>
<body>
<section class="section">
<div class="columns is-multiline">
<div class="column">
<div class="box">
<div class="block is-flex is-justify-content-space-between is-align-items-end">
<div class="block">
<span class="tag is-info">PC=<b data-value="pc"></b></span>
<span class="tag" data-flag="negative"><i class="mdi mdi-not-equal-variant"></i></span>
<span class="tag" data-flag="halt"><i class="mdi mdi-stop"></i></span>
<span class="tag" data-flag="fire"><i class="mdi mdi-fire"></i></span>
</div>
<div class="controls">
<button class="button is-primary" data-control="reset"><i class="mdi mdi-undo"></i></button>
<button class="button is-primary" data-control="back"><i class="mdi mdi-skip-previous"></i></button>
<button class="button is-primary" data-control="step"><i class="mdi mdi-skip-next"></i></button>
</div>
</div>
<table class="table is-striped is-hoverable" id="instructions"></table>
</div>
</div>
<div class="column">
<div class="box">
<div class="field">
<label class="label">Program</label>
<textarea class="textarea" data-control="program">
05 0d
61 62 63 64 65 66 67 68 69 6a 00
02 02
01 02
06
</textarea>
</div>
<div class="field">
<label class="label">Input</label>
<input class="input" data-control="input" placeholder="\x00 style escapes accepted" value="hello">
</div>
<div class="field">
<label class="label">Output</label>
<input class="input" data-value="output" readonly>
</div>
</div>
</div>
<div class="column">
<div class="box">
<h2 class="title">Instructions</h2>
<table class="table is-striped is-hoverable" id="instructions-help"></table>
</div>
</div>
</div>
</section>
<template id="instruction">
<tr>
<th class="addr has-text-right"></th>
<td class="name"></td>
<td class="args"></td>
<td class="hex"></td>
</tr>
</template>
<template id="instruction-help">
<tr>
<th class="num has-text-right"></th>
<td class="name"></td>
<td class="description"></td>
</tr>
</template>
</body>
</html>

3
run.sh Executable file
View File

@ -0,0 +1,3 @@
#! /bin/sh
busybox httpd -fv -p 8081 -c httpd.conf

16
triscit.css Normal file
View File

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

293
triscit.mjs Normal file
View File

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