diff --git a/README.md b/README.md
index a5789dc..9cc3b73 100644
--- a/README.md
+++ b/README.md
@@ -282,6 +282,15 @@ Reference implementations
program file name is optional and used for preloading MU8 program files into
VM's memory. The Busybox AWK implementation is compact but quite slow, so use
it as a last resort solution when no other programming environment is available.
+* [Web (HTML5/JS) implementation](https://mu808.luxferre.top) aka mu8web:
+ supports the entire mu808 spec except the I/O port 2 and offers a way to
+ load program files in the same MU8 format via a convenient UI.
+ Uses a convoluted flow to achieve true synchronous input.
+ Since there's no exiting to the OS environment, the -5 instruction number
+ resets the VM instead (by zeroing out both memory areas and the counter).
+ Entry conventions within UI are pretty much the same as for 808UL. Tested
+ on various devices with physical keypads and touch screens. The source code
+ can be found in the [mu8web](mu8web/) directory of this repo.
### Assembler reference implementations
@@ -293,6 +302,29 @@ Reference implementations
These lists are going to be expanded as soon as new reference implementations
appear.
+### Web reference implementation keyboard controls
+
+The virtual keyboard (unavailable on the smallest screens) is duplicated with
+the following keys at all times in the mu808 Web reference implementation:
+
+* `0`-`9`: decimal digits input;
+* `*` (star), comma, dot: decimal point input;
+* Space, `+`, `/`, down arrow, Call key (if present): whitespace input;
+* Backspace, Delete, left arrow, `-`: minus or backspace input (as the minus sign
+ can only be entered at the beginning of the instruction or data input line);
+* `#` (pound, hash sign), Enter: newline input;
+* `l`, `L`, up arrow, Left Soft key (if present): open a program file loading dialogue.
+
+### 12-key (DTMF) input method proposal
+
+For hobbyist projects and other environments where all input is limited to 12
+keys of the standard phone keypad, the following convention is recommended:
+
+* the `*` (star) key is inputting a minus if this is the first position in
+ the entry, otherwise it's inputting a decimal point;
+* the `#` (pound) key actually confirms entering a single number field, and
+ all instructions are required to contain exactly five numbers.
+
FAQ
---
diff --git a/mu8web/icon.png b/mu8web/icon.png
new file mode 100644
index 0000000..7371471
Binary files /dev/null and b/mu8web/icon.png differ
diff --git a/mu8web/index.html b/mu8web/index.html
new file mode 100644
index 0000000..46c42d7
--- /dev/null
+++ b/mu8web/index.html
@@ -0,0 +1,52 @@
+
+
+
+
+
+ mu808 VM (Web version)
+
+
+
+
+
+
mu808
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/mu8web/mu808-core.js b/mu8web/mu808-core.js
new file mode 100644
index 0000000..80f36e9
--- /dev/null
+++ b/mu8web/mu808-core.js
@@ -0,0 +1,183 @@
+/*
+ * mu808: an ultralight numeric-only VM
+ * This is the core JS file for the reference implementation
+ * of mu808 VM for the Web and other ES5 engines
+ * See the README.md file in the repo root for all documentation
+ * Created by Luxferre in 2025, released into public domain
+ */
+
+/* Due to the modular nature, this file only defines the mu808 VM kernel
+ * which accepts all I/O routines as the parameters for its construction */
+function createmu808(lineOut, charOut, lineIn) {
+ var MEMLIMIT = 16384,
+ PMEM = new Uint16Array(MEMLIMIT << 2), /* program memory */
+ DMEM = new Float64Array(MEMLIMIT), /* data memory */
+ runlimit = MEMLIMIT,
+ traceflag = false,
+ runcount = 0
+
+ /* memory range input function */
+ /* accepts the continuation callback as the last parameter */
+ function dataRangeInput(start, end, contfunc) {
+ (function inp(loc) {
+ lineIn(function(v) {
+ DMEM[loc] = parseFloat(v) || 0.0
+ if(loc < end) inp(loc+1)
+ else {
+ if(contfunc) contfunc()
+ }
+ })
+ })(start)
+ }
+
+ /* port output function */
+ function portout(port, data) {
+ if(port == 0) lineOut(data + "\n")
+ else if(port == 1) charOut(data & 255)
+ }
+
+ /* instruction line execution function (the main 808UL logic defined here) */
+ /* accepts the continuation callback as the last parameter */
+ function ilexec(lno, cmd, x, y, z, contfunc) {
+ var halt = 0, data_override = 0, bcheck, i, v1, v2, v3
+ while(halt == 0) {
+ v1 = v2 = v3 = data_override = DMEM[0] = 0 /* force the value at 0 to be always 0 */
+ if(lno == 0) halt = 1 /* immediate instruction */
+ if(runlimit > 0 && runcount > runlimit) {
+ halt = 1
+ if(traceflag) lineOut("Runlimit hit, halting...\n")
+ } else runcount++
+ if(traceflag)
+ lineOut("PC: " + lno + " INSTR: " + [cmd, x, y, z].join(" ") + "\n")
+ bcheck = (x < MEMLIMIT) && (y < MEMLIMIT) && (z < MEMLIMIT)
+ if(bcheck) { /* boundary checks passed */
+ v1 = DMEM[x]; v2 = DMEM[y]; v3 = DMEM[z] /* prefetch the memory values */
+ if(cmd == 1) { /* JMP */
+ if((v2 == 0 && x == 0) || (v2 > 0 && x == 1) || (v2 < 0 && x == 2)
+ || (v2 >= 0 && x == 3) || (v2 <= 0 && x == 4) || (v2 != 0 && x == 5)
+ || x == 6) { halt = 0; lno = z - 1 }
+ else if((v2 == 0 && x == 7) || (v2 > 0 && x == 8) || (v2 < 0 && x == 9)
+ || (v2 >= 0 && x == 10) || (v2 <= 0 && x == 11) || (v2 != 0 && x == 12)
+ || x == 13) { halt = 0; lno = (v3|0) - 1 }
+ } else if(cmd == 2) data_override = 1 /* IAT */
+ else if(cmd == 3) for(i=x;i<=y;i++) portout(z, DMEM[i]) /* OUT */
+ else if(cmd == 4) { /* INP */
+ dataRangeInput(x, y, function() {
+ /* continuation function duplicating what's after the switch */
+ /* except the data override branch */
+ lno++ /* increment the program counter */
+ if(lno >= MEMLIMIT || lno < 1 || (runlimit > 0 && runcount > runlimit)) {
+ if(traceflag) lineOut("Memory limit or runlimit hit, halting...\n")
+ halt = 1
+ } else { /* fetch the next instruction and execute it */
+ cmd = PMEM[lno << 2]
+ x = PMEM[(lno << 2) + 1]
+ y = PMEM[(lno << 2) + 2]
+ z = PMEM[(lno << 2) + 3]
+ ilexec(lno, cmd, x, y, z, contfunc)
+ }
+ })
+ return
+ } else if(cmd == 5) DMEM[z] = (x % 10000) + (y % 10000) / 10000.0 /* SET */
+ else if(cmd == 6) { /* CPY */
+ if(x == 0) DMEM[z] = y
+ else if(x == 1) DMEM[z] = v2
+ else if(v2 < MEMLIMIT && v3 < MEMLIMIT) DMEM[0|v3] = DMEM[0|v2]
+ } else if(cmd == 7) DMEM[z] = v1 + v2 * v3 /* FMA */
+ else if(cmd == 8) DMEM[z] = v1 - v2 /* SUB */
+ else if(cmd == 9) DMEM[z] = (v2 == 0) ? 0 : (v1 / v2) /* DIV */
+ else if(cmd == 10) DMEM[z] = (v2 == 0) ? (0|v1) : ((0|v1) % (0|v2)) /* MDF */
+ else if(cmd == 11) DMEM[z] = Math.abs(v2) /* ABS */
+ else if(cmd == 12) DMEM[z] = Math.sqrt(Math.abs(v2)) /* SQR */
+ else if(cmd == 13) { /* NEL */
+ if(x == 0) DMEM[z] = Math.exp(v2)
+ else DMEM[z] = (v2 == 0) ? 0 : Math.log(Math.abs(v2))
+ } else if(cmd == 14) { /* TRI */
+ if(x == 0) DMEM[z] = Math.sin(v2)
+ else if(x == 1) DMEM[z] = Math.cos(v2)
+ else DMEM[z] = Math.atan(v2)
+ } else if(cmd == 15) { /* RND */
+ v1 = 0|v1
+ DMEM[z] = v1 + (0|(Math.random() * ((0|v2) - v1 + 1)))
+ }
+ } /* command switch ends here */
+ lno++ /* increment the program counter */
+ if(lno >= MEMLIMIT || lno < 1 || (runlimit > 0 && runcount > runlimit)) {
+ if(traceflag) lineOut("Memory limit or runlimit hit, halting...\n")
+ halt = 1
+ } else { /* fetch the next instruction */
+ if(data_override && bcheck) {
+ cmd = PMEM[lno << 2]
+ x = (0|v1) % MEMLIMIT
+ y = (0|v2) % MEMLIMIT
+ z = (0|v3) % MEMLIMIT
+ } else {
+ cmd = PMEM[lno << 2]
+ x = PMEM[(lno << 2) + 1]
+ y = PMEM[(lno << 2) + 2]
+ z = PMEM[(lno << 2) + 3]
+ }
+ }
+ }
+ if(contfunc) contfunc()
+ }
+
+ /* instruction line entry function (interactive mode) */
+ /* accepts the continuation callback as the last parameter */
+ function ilenter(lno, cmd, x, y, z, contfunc) {
+ if(lno > 0) { /* record the instruction in memory */
+ PMEM[lno << 2] = cmd
+ PMEM[(lno << 2) + 1] = x
+ PMEM[(lno << 2) + 2] = y
+ PMEM[(lno << 2) + 3] = z
+ if(contfunc) contfunc()
+ } else if(lno == 0) { /* immediate execution */
+ runcount = 0
+ ilexec(lno, cmd, x, y, z, contfunc)
+ } else if(lno == -1) { /* display a range of instructions from cmd to x */
+ if(cmd < MEMLIMIT && x < MEMLIMIT) for(y=cmd;y<=x;y++) {
+ z = y << 2
+ lineOut("@" + y + ":\t" + [PMEM[z], PMEM[z+1], PMEM[z+2], PMEM[z+3]].join(' ') + '\n')
+ }
+ if(contfunc) contfunc()
+ } else if(lno == -2) { /* clear a range of data or instructions from x to y */
+ if(cmd < MEMLIMIT && x < MEMLIMIT) for(z=x;z<=y;z++) {
+ if(cmd == 0) PMEM[z*4] = PMEM[z*4+1] = PMEM[z*4+2] = PMEM[z*4+3] = 0
+ else DMEM[z] = 0.0
+ }
+ if(contfunc) contfunc()
+ } else if(lno == -3) { /* tracing on/off */
+ if(cmd == 0) {traceflag = 0; lineOut("Tracing off\n")}
+ else {traceflag = 1; lineOut("Tracing on\n")}
+ if(contfunc) contfunc()
+ } else if(lno == -4) { /* set the runlimit to the value of cmd */
+ runlimit = cmd
+ lineOut("Runlimit set to " + cmd + "\n")
+ if(contfunc) contfunc()
+ } else if(lno == -5) { /* can't exit to the environment, reset instead */
+ lineOut("Resetting the machine...\n")
+ for(y=0;y 0) {
+ var fobj = e.target.files[0], rdr = new FileReader()
+ rdr.onload = function() {
+ var progsrc = rdr.result
+ if(progsrc.length > 0) {
+ var instrs = progsrc.split(/\s+/), i, l = instrs.length,
+ instr, lno, cmd, x, y, z, icount = 0
+ for(i=0;i ")
+ }
+ }
+ if(fobj) rdr.readAsText(fobj)
+ }
+ }
+
+ /* main input routine:
+ * digits 0 to 9, 10 is space,
+ * 11 is minus/backspace, 12 is enter, 13 is decimal point,
+ * 14 is program load action */
+ function doInput(number) {
+ if(number >= 0 && number < 10) /* normal digit */
+ inputBuffer += ("" + number)
+ else if(number == 10) /* space */
+ inputBuffer += " "
+ else if(number == 11) { /* backspace or minus */
+ if(inputBuffer.length == 0)
+ inputBuffer += "-"
+ else
+ inputBuffer = inputBuffer.slice(0, inputBuffer.length - 1)
+ } else if(number == 12) { /* enter */
+ screenBuffer += inputBuffer + "\n"
+ inputRes = inputBuffer
+ inputBuffer = ""
+ } else if(number == 13) /* decimal point */
+ inputBuffer += "."
+ else if(number == 14) floader.click() /* trigger program loader */
+ renderScreen()
+ }
+
+ function setupInput() {
+ /* setup physical keys */
+ w.addEventListener("keydown", function(e) {
+ switch(e.key) {
+ case "0":
+ case "1":
+ case "2":
+ case "3":
+ case "4":
+ case "5":
+ case "6":
+ case "7":
+ case "8":
+ case "9":
+ e.preventDefault()
+ doInput(parseInt(e.key))
+ break
+ case "+":
+ case "/":
+ case " ":
+ case "Call":
+ case "ArrowDown":
+ e.preventDefault()
+ doInput(10)
+ break
+ case "Backspace":
+ case "Back":
+ case "ArrowLeft":
+ case "Delete":
+ case "-":
+ e.preventDefault()
+ e.stopPropagation()
+ doInput(11)
+ break
+ case "#":
+ case "Enter":
+ e.preventDefault()
+ doInput(12)
+ break
+ case "*":
+ case ",":
+ case ".":
+ e.preventDefault()
+ doInput(13)
+ break
+ case "l":
+ case "L":
+ case "ArrowUp":
+ case "SoftLeft":
+ e.preventDefault()
+ doInput(14)
+ break
+ }
+ }, false)
+ /* setup virtual keys */
+ w.addEventListener("click", function(e) {
+ switch(e.target.id) {
+ case "b0":
+ case "b1":
+ case "b2":
+ case "b3":
+ case "b4":
+ case "b5":
+ case "b6":
+ case "b7":
+ case "b8":
+ case "b9":
+ e.preventDefault()
+ doInput(parseInt(e.target.id.slice(1)))
+ break
+ case "bspc":
+ e.preventDefault()
+ doInput(10)
+ break
+ case "bbksp":
+ e.preventDefault()
+ doInput(11)
+ break
+ case "benter":
+ e.preventDefault()
+ doInput(12)
+ break
+ case "bpoint":
+ e.preventDefault()
+ doInput(13)
+ break
+ case "bprogload":
+ e.preventDefault()
+ doInput(14)
+ break
+ }
+ }, false)
+ }
+
+ function lineOut(s) { /* line output routine */
+ screenBuffer += s
+ renderScreen()
+ }
+
+ function charOut(c) { /* character output routine */
+ screenBuffer += String.fromCharCode(+c)
+ renderScreen()
+ }
+
+ function lineIn(cb) {
+ inputRes = ""
+ var inv = setInterval(function resWait() {
+ if(inputRes != "") {
+ clearInterval(inv)
+ cb(inputRes)
+ inputRes = ""
+ }
+ }, 100)
+ }
+
+ function main() { /* UI entry point */
+ screenDom = d.getElementById("screenfield")
+ floader = d.getElementById("programloadfield")
+ floader.addEventListener("change", loadProgFileHandler, false)
+ setupInput()
+ prompt = "> "
+ vm = createmu808(lineOut, charOut, lineIn)
+ lineOut("mu808 v1 by Luxferre\nPROGMEM: 16383 steps\nDATAMEM: 16384 floats\n")
+ ;(function repl() {
+ lineOut(prompt)
+ lineIn(function(entryString) {
+ var eparts = entryString.split(/[^\-0-9]+/).slice(0, 5),
+ lno = parseInt(eparts[0] || 0),
+ cmd = parseInt(eparts[1] || 0),
+ x = parseInt(eparts[2] || 0),
+ y = parseInt(eparts[3] || 0),
+ z = parseInt(eparts[4] || 0)
+ vm.ilenter(lno, cmd, x, y, z, repl)
+ })
+ })()
+ }
+
+ w.addEventListener("DOMContentLoaded", main, false)
+})(window, document)
diff --git a/mu8web/style.css b/mu8web/style.css
new file mode 100644
index 0000000..f79d2a8
--- /dev/null
+++ b/mu8web/style.css
@@ -0,0 +1,83 @@
+* {box-sizing: border-box; margin: 0; padding: 0}
+html, body {
+ width: 100%;
+ height: 100%;
+ font-family: "Noto Sans", Roboto, "Droid Sans", sans-serif;
+ font-size: 12pt;
+ text-align: center;
+ background: #111;
+ color: #eee;
+}
+
+#workspace {
+ padding: 1rem 0.6rem;
+}
+
+#screenfield {
+ display: inline-block;
+ min-width: 240px;
+ height: 50vh;
+ width: 100%;
+ max-width: 800px;
+ font-family: "Fira Code", "Droid Sans Mono", monospace;
+ font-size: inherit;
+ border: 1px solid #ccc;
+ border-radius: 2px;
+ overflow-y: scroll;
+ text-align: left;
+ text-wrap: auto;
+ white-space: pre-wrap;
+}
+
+#keypad {
+ display: inline-block;
+ width: 50vw;
+ min-width: 206px;
+ max-width: 400px;
+ height: 20vh;
+ min-height: 180px;
+}
+
+@media only screen and (max-width: 480px) {
+ #keypad, #keypad * {
+ display:none!important;
+ }
+#workspace {
+ padding: 0.5rem 0!important;
+ }
+}
+
+#keypad div {
+ padding: 1px 0;
+ height: 25%;
+}
+
+#keypad button {
+ background: inherit;
+ color: inherit;
+ font-family: inherit;
+ font-size: inherit;
+ border: 1px solid #ddd;
+ border-radius: 3px;
+ width: calc(25% - 4px);
+ height: calc(100% - 4px);
+ cursor: pointer;
+}
+
+#keypad button:hover {
+ color: cyan;
+ border-color: cyan;
+}
+
+#keypad #bspc {
+ width: calc(50% - 4px);
+}
+
+header {padding: 0.6rem 0}
+footer {padding-top: 0.6rem}
+footer div {padding: 0.6em 0}
+
+#programloadfield {display:none}
+
+a,a:active,a:visited {color: inherit}
+a:hover {color:cyan}