Assemble kernel on the client #1

Merged
cdo merged 72 commits from client-side-assembler into main 2026-03-18 15:21:33 +00:00
10 changed files with 3603 additions and 2065 deletions

View File

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

1440
asm.js Normal file

File diff suppressed because it is too large Load Diff

29
boot.js
View File

@@ -1,7 +1,26 @@
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) => { self.onmessage = async (e) => {
const exports = { emu: { mem: e.data } }; switch (e.data.type) {
const mod = await WebAssembly.instantiateStreaming( case "load":
fetch('wipforth.wasm'), exports) const exports = { emu: { mem: e.data.mem } };
mod.instance.exports.reset(); const wasm = await assemble;
console.log('System halt'); self.mod = await WebAssembly.instantiate(wasm, exports);
await self.postMessage('ready');
break;
case "boot":
self.mod.instance.exports.reset();
console.log('System halt');
break;
}
}; };

View File

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

12
emu.js
View File

@@ -11,6 +11,7 @@ const RXBUF_SIZE = 32;
const PERIPHS_SIZE = 81; const PERIPHS_SIZE = 81;
const POLL_INTERVAL_MS = 20; const POLL_INTERVAL_MS = 20;
const DOT_INTERVAL_MS = 120;
const COLS = 80; const COLS = 80;
const TAB_WIDTH = 8; const TAB_WIDTH = 8;
@@ -50,8 +51,15 @@ class Emulator {
document.addEventListener('keydown', (e) => this.handle_keydown(e)); document.addEventListener('keydown', (e) => this.handle_keydown(e));
window.addEventListener('resize', () => this.handle_resize()); window.addEventListener('resize', () => this.handle_resize());
this.worker = new Worker('boot.js'); this.print("Assembling kernel ");
this.worker.postMessage(this.mem); 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" });
};
fetch('prelude.f') fetch('prelude.f')
.then(res => res.text()) .then(res => res.text())

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

2088
wipforth.ws Normal file

File diff suppressed because it is too large Load Diff