Web implementation appears

This commit is contained in:
Luxferre
2025-04-23 18:57:33 +03:00
parent db8422a1fd
commit 72bea2b2e1
6 changed files with 553 additions and 0 deletions
+32
View File
@@ -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
---
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 8.0 KiB

+52
View File
@@ -0,0 +1,52 @@
<!DOCTYPE html>
<html>
<head>
<meta charset=utf-8>
<meta name=viewport content="width=device-width">
<title>mu808 VM (Web version)</title>
<link rel=icon href=icon.png />
<link rel=stylesheet href=style.css />
</head>
<body>
<header>
<h1>mu808</h1>
</header>
<main>
<hr>
<section id=workspace>
<pre id=screenfield></pre>
<input type=file id=programloadfield />
</section>
<section id=keypad>
<div>
<button type=button id=b1>1</button>
<button type=button id=b2>2</button>
<button type=button id=b3>3</button>
<button type=button id=bprogload>LOAD</button>
</div>
<div>
<button type=button id=b4>4</button>
<button type=button id=b5>5</button>
<button type=button id=b6>6</button>
<button type=button id=bbksp>(-)/&larr;</button>
</div>
<div>
<button type=button id=b7>7</button>
<button type=button id=b8>8</button>
<button type=button id=b9>9</button>
<button type=button id=benter>ENT</button>
</div>
<div>
<button type=button id=bpoint>.</button>
<button type=button id=b0>0</button>
<button type=button id=bspc>SPACE</button>
</div>
</section>
</main>
<footer>
<hr><div><a href="https://codeberg.org/luxferre/mu808">Created</a> by <a href="https://luxferre.top">Luxferre</a> in 2025</div>
</footer>
<script src=mu808-core.js></script>
<script src=mu808-ui.js></script>
</body>
</html>
+183
View File
@@ -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<MEMLIMIT;y++) {
DMEM[y] = 0.0
PMEM[y << 2] = 0
PMEM[(y << 2) + 1] = 0
PMEM[(y << 2) + 2] = 0
PMEM[(y << 2) + 3] = 0
}
runcount = 0
runlimit = MEMLIMIT
traceflag = false
lineOut("Machine reset complete\n")
if(contfunc) contfunc()
}
}
/* now, export the ilenter function as the outside API, as well as debug methods */
return {
ilenter: ilenter,
savemem: function() {return {"PMEM":PMEM,"DMEM":DMEM}},
loadmem: function(memobj) {
PMEM.set(memobj.PMEM)
DMEM.set(memobj.DMEM)
}
}
}
+203
View File
@@ -0,0 +1,203 @@
;(function(w, d) {
var screenBuffer = "", inputBuffer = "", screenDom, inputRes = "",
floader, vm
/* screen rendering routine */
function renderScreen() {
screenDom.textContent = screenBuffer + inputBuffer + "\u258f"
screenDom.scrollTop = screenDom.scrollHeight
}
/* external program loading routine */
function loadProgFileHandler(e) {
if(e.target.files.length > 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<l;i++) {
instr = parseInt(instrs[i] || 0)
if(icount == 0) lno = instr
else if(icount == 1) cmd = instr
else if(icount == 2) x = instr
else if(icount == 3) y = instr
else if(icount == 4) z = instr
icount++
if(icount == 5) {
vm.ilenter(lno, cmd, x, y, z)
icount = 0
}
}
lineOut("\nProgram " + fobj.name + " loaded successfully\n> ")
}
}
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)
+83
View File
@@ -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}