Compare commits

..

1 Commits

Author SHA1 Message Date
d3d0801236 Create initial scaffolding for JS WASM assembler 2026-03-09 20:08:29 +00:00
12 changed files with 2118 additions and 3512 deletions

View File

@@ -1,24 +1,31 @@
# Wipforth
Wipforth is a Forth implementation that runs in the WebAssembly
virtual machine. The system is bootstrapped from source on page load:
the only non-text file is the favicon :)
Wipforth is a simple Forth implementation that runs in the WebAssembly
virtual machine. It does I/O via memory-mapped peripherals, which are
emulated in JavaScript.
I/O is done via memory-mapped peripherals, which are emulated in
JavaScript.
- For the Forth kernel, see [wipforth.ws](./wipforth.ws)
- For the emulator, see [emu.js](./emu.js)
- For the assembler, see [asm.js](./asm.js)
- For the prelude (Forth code loaded right after the kernel boots),
see [prelude.f](./prelude.f)
- For the Forth kernel, see [wipforth.wat](./wipforth.wat)
- For the JavaScript emulator, see [emu.js](./emu.js)
- For the Forth prelude, which is loaded at start-up, see
[prelude.f](./prelude.f)
- For a description of the peripherals, see the
[Peripherals](#peripherals) section below.
## Building and Running Locally
There's a [Guile](https://www.gnu.org/software/guile/) script in the
repo you can use for this:
You'll need:
- [WABT](https://github.com/WebAssembly/wabt) (not for long mwahaha)
- [Guile](https://www.gnu.org/software/guile/) (or bring your own HTTP
server -- see note below)
To run, first compile the WebAssembly module:
```
wat2wasm --enable-threads wipforth.wat
```
Then run the development server:
```
guile server.scm
@@ -27,20 +34,14 @@ guile server.scm
You should then be able to open <http://localhost:8080> in a browser
and use the system from there.
However, since everything is bootstrapped on the client, basically any
HTTP server will do as long as it sets the appropriate response
headers for `SharedMemoryBuffer` use:
**NOTE**: The server is very simple and just serves the files with the
cross-origin isolation headers required for `SharedMemoryBuffer` use.
You could use any HTTP server that sets these headers.
- `Cross-Origin-Opener-Policy: same-origin`
- `Cross-Origin-Embedder-Policy: require-corp`
So, if you don't have Guile on your system you can use something else
like Python's `http.server`.
**NOTE**: You should **definitely not** use `server.scm` to serve the
application on the open internet or anything like that; I just hacked
it together for testing on localhost during development and it's
probably hilariously insecure.
You should **definitely not** use the development server to serve the
application on the open internet; I just hacked it together for
testing on localhost during development and it's probably hilariously
insecure.
## End-to-End Tests
@@ -61,13 +62,16 @@ Given that's all sorted, you should be able to run:
guile tests.scm
```
It will print a JUnit XML report to standard out. You can
pretty-print it with `xmllint` if you have it installed:
It will print a JUnit XML report to standard out, you can pretty-print
it with:
```
guile tests.scm | xmllint --format -
```
Though, of course, this will require that you have `xmllint` on your
system.
## Peripherals
### Terminal

1287
asm.js

File diff suppressed because it is too large Load Diff

27
boot.js
View File

@@ -1,26 +1,7 @@
import { Assembler } from './asm.js';
const assemble = (async () => {
const asm = new Assembler();
const resp = await fetch('wipforth.ws');
for await (const chunk of resp.body) {
asm.push(chunk);
}
return asm.wasm();
})();
self.onmessage = async (e) => {
switch (e.data.type) {
case "load":
const exports = { emu: { mem: e.data.mem } };
const wasm = await assemble;
self.mod = await WebAssembly.instantiate(wasm, exports);
await self.postMessage('ready');
break;
case "boot":
self.mod.instance.exports.reset();
const exports = { emu: { mem: e.data } };
const mod = await WebAssembly.instantiateStreaming(
fetch('wipforth.wasm'), exports)
mod.instance.exports.reset();
console.log('System halt');
break;
}
};

View File

@@ -3,4 +3,4 @@ emu.js
index.html
prelude.f
styles.css
wipforth.ws
wipforth.wasm

27
emu.js
View File

@@ -11,7 +11,6 @@ const RXBUF_SIZE = 32;
const PERIPHS_SIZE = 81;
const POLL_INTERVAL_MS = 20;
const DOT_INTERVAL_MS = 120;
const COLS = 80;
const TAB_WIDTH = 8;
@@ -51,29 +50,8 @@ class Emulator {
document.addEventListener('keydown', (e) => this.handle_keydown(e));
window.addEventListener('resize', () => this.handle_resize());
this.prof = new Worker("prof.js");
this.prof.onmessage = (e) => {
const blob = new Blob(
[JSON.stringify(e.data)],
{ type: "application/json" });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = "wipforth-profile.json";
a.click();
URL.revokeObjectURL(url);
};
this.print("Assembling kernel ");
const dots = setInterval(() => this.print("."), DOT_INTERVAL_MS);
this.worker = new Worker('boot.js', { type: 'module' });
this.worker.postMessage({ type: "load", mem: this.mem });
this.worker.onmessage = (e) => {
clearInterval(dots);
this.print(" done\n");
this.worker.postMessage({ type: "boot" });
this.prof.postMessage({ type: "start", mem: this.mem });
};
this.worker = new Worker('boot.js');
this.worker.postMessage(this.mem);
fetch('prelude.f')
.then(res => res.text())
@@ -99,7 +77,6 @@ class Emulator {
if (!this.input_enable) {
const sysready = Atomics.load(this.mem_u8, SYSREADY);
if (sysready != 0) {
this.prof.postMessage({ type: "stop" });
this.input_enable = true;
this.flush_output();
document.getElementById('cursor').classList.add('blinking');

View File

@@ -249,8 +249,8 @@ CHAR . EMIT
\ Version number
0 CONSTANT VERSION-MAJOR
2 CONSTANT VERSION-MINOR
1 CONSTANT VERSION-PATCH
1 CONSTANT VERSION-MINOR
0 CONSTANT VERSION-PATCH
: PRINT-VERSION
CHAR v EMIT VERSION-MAJOR .

48
prof.js
View File

@@ -1,48 +0,0 @@
const INTERVAL_MS = 1;
const RS_TOP_ADDR = 0x10000;
const PROF_DATA_ADDR = 0x58;
const PROF_DATA_IDX = PROF_DATA_ADDR / 8;
let mem_8;
let mem_64;
let sampler;
const samples = [];
function sample() {
const data = Atomics.load(mem_64, PROF_DATA_IDX);
const ip = Number(data & 0xffffffffn);
const rsp = Number(data >> 32n);
samples.push({ ip, rs_bytes: mem_8.slice(rsp, RS_TOP_ADDR) });
}
function i32(bytes) {
return bytes[0]
| (bytes[1] << 8)
| (bytes[2] << 16)
| (bytes[3] << 24);
}
function postproc({ ip, rs_bytes }) {
const rs = [];
for (let i = 0; i < rs_bytes.length; i += 4)
rs.push(i32(rs_bytes.slice(i, i + 4)));
rs.reverse();
return { ip, rs };
}
self.onmessage = (e) => {
switch (e.data.type) {
case "start":
console.log("Starting profiler");
mem_8 = new Uint8Array(e.data.mem.buffer);
mem_64 = new BigUint64Array(e.data.mem.buffer);
ip = e.data.ip;
rsp = e.data.rsp;
sampler = setInterval(sample, INTERVAL_MS);
break;
case "stop":
clearInterval(sample);
console.log("Stopped profiler");
self.postMessage(samples.map(postproc));
}
};

View File

@@ -16,8 +16,9 @@
'(("html" . (text/html))
("css" . (text/css))
("js" . (application/javascript))
("wasm" . (application/wasm))
("f" . (text/plain))
("ws" . (text/plain))
("wat" . (text/plain))
("png" . (image/png))))
(define (mime-type path)

View File

@@ -21,17 +21,11 @@
(navigate client "http://localhost:8080")
(sleep 5)
(define-test kernel-assembles-successfully
(let* ((display (get-display client))
(line (first (lines display))))
(assert (string-match "Assembling kernel \\.+ done" line)
(format #f "Kernel assemble line: ~s" line))))
(define-test prelude-loads-successfully
(let* ((display (get-display client))
(line (second (lines display))))
(assert (string-match "Loading prelude \\.+ done" line)
(format #f "Prelude load line: ~s" line))))
(first-line (first (lines display))))
(assert (string-match "Loading prelude \\.+ done" first-line)
(format #f "Prelude load line: ~s" first-line))))
(define-test six-seven-times-dot-cr-yields-42
(input-line client "6 7 * . CR")

2018
wipforth.wat Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,16 +0,0 @@
import { Assembler } from "./asm.js";
const asm = new Assembler();
for await (const chunk of Deno.stdin.readable) {
asm.push(chunk);
}
asm.wasm();
const defs = Object.entries(asm.defs);
while (defs[0][0] != '_DUP')
defs.shift();
while (defs.at(-1)[0] != 'WNF_HANDLER')
defs.pop();
const words = Object.fromEntries(defs.filter(([k,v]) => !k.startsWith("_")));
console.log(JSON.stringify(words));