Compare commits

..

1 Commits

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

View File

@@ -1,24 +1,31 @@
# Wipforth # Wipforth
Wipforth is a Forth implementation that runs in the WebAssembly Wipforth is a simple Forth implementation that runs in the WebAssembly
virtual machine. The system is bootstrapped from source on page load: virtual machine. It does I/O via memory-mapped peripherals, which are
the only non-text file is the favicon :) emulated in JavaScript.
I/O is done via memory-mapped peripherals, which are emulated in - For the Forth kernel, see [wipforth.wat](./wipforth.wat)
JavaScript. - For the JavaScript emulator, see [emu.js](./emu.js)
- For the Forth prelude, which is loaded at start-up, see
- For the Forth kernel, see [wipforth.ws](./wipforth.ws) [prelude.f](./prelude.f)
- 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
There's a [Guile](https://www.gnu.org/software/guile/) script in the You'll need:
repo you can use for this:
- [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 guile server.scm
@@ -27,20 +34,14 @@ 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.
However, since everything is bootstrapped on the client, basically any **NOTE**: The server is very simple and just serves the files with the
HTTP server will do as long as it sets the appropriate response cross-origin isolation headers required for `SharedMemoryBuffer` use.
headers for `SharedArrayBuffer` use: You could use any HTTP server that sets these headers.
- `Cross-Origin-Opener-Policy: same-origin` You should **definitely not** use the development server to serve the
- `Cross-Origin-Embedder-Policy: require-corp` application on the open internet; I just hacked it together for
testing on localhost during development and it's probably hilariously
So, if you don't have Guile on your system you can use something else insecure.
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
@@ -61,34 +62,36 @@ 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 It will print a JUnit XML report to standard out, you can pretty-print
pretty-print it with `xmllint` if you have it installed: it with:
``` ```
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
| Name | Address | Size / B | Access | | Name | Address | Size / B | Access |
|--------|---------|----------|--------------| |--------|---------|----------|--------------|
| TXBUF | 000h | 32 | write | | TXBUF | 00h | 32 | write |
| RXBUF | 080h | 32 | read | | RXBUF | 20h | 32 | read |
| TXHEAD | 100h | 4 | atomic read | | TXHEAD | 40h | 4 | atomic read |
| TXTAIL | 104h | 4 | atomic write | | TXTAIL | 44h | 4 | atomic write |
| RXHEAD | 108h | 4 | atomic write | | RXHEAD | 48h | 4 | atomic write |
| RXTAIL | 10Ch | 4 | atomic read | | RXTAIL | 4Ch | 4 | atomic read |
For both sending (`TX`) and receiving (`RX`), there are three For both sending (`TX`) and receiving (`RX`), there are three
registers: `xBUF`, `xHEAD` and `xTAIL`: registers: `xBUF`, `xHEAD` and `xTAIL`:
- `xBUF` registers are 128-byte FIFO ring buffers used for data - `xBUF` registers are 32-byte FIFO ring buffers used for data
- The `xHEAD` and `xTAIL` registers specify the start and end of data - The `xHEAD` and `xTAIL` registers specify the start and end of data
in the ring buffer, `xHEAD` being the offset of the first byte of in the ring buffer, `xHEAD` being the offset of the first byte of
data, and `xTAIL` being the offset of the first byte *after* the data, and `xTAIL` being the offset of the first byte *after* the data.
data.
In order to be distinguishable from the empty state, the ring buffers In order to be distinguishable from the empty state, the ring buffers
must never be completely full -- there must always be *at least one* must never be completely full -- there must always be *at least one*
@@ -98,10 +101,7 @@ unoccupied byte between the tail and the head.
| Name | Address | Size / B | Access | | Name | Address | Size / B | Access |
|----------|---------|----------|--------------| |----------|---------|----------|--------------|
| SYSREADY | 110h | 4 | atomic write | | SYSREADY | 50h | 1 | atomic write |
| SYSINTER | 114h | 4 | atomic read |
The `SYSREADY` register is used to indicate when the system has booted The `SYSREADY` register is used to indicate when the system has booted
up and is ready for user input. `SYSINTER` is set (and notified on) up and is ready for user input.
once the emulator has enabled user input and the system is
interactive.

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

View File

@@ -1,8 +1,6 @@
asm.js
boot.js boot.js
emu.js emu.js
favicon.png
index.html index.html
prelude.f prelude.f
styles.css styles.css
wipforth.ws wipforth.wasm

70
emu.js
View File

@@ -1,17 +1,16 @@
const TXBUF = 0x000; const TXBUF = 0x00;
const RXBUF = 0x080; const RXBUF = 0x20;
const TXHEAD = 0x100; const TXHEAD = 0x40;
const TXTAIL = 0x104; const TXTAIL = 0x44;
const RXHEAD = 0x108; const RXHEAD = 0x48;
const RXTAIL = 0x10c; const RXTAIL = 0x4c;
const SYSREADY = 0x50;
const SYSREADY = 0x110; const TXBUF_SIZE = 32;
const SYSINTER = 0x114; const RXBUF_SIZE = 32;
const PERIPHS_SIZE = 81;
const PERIPHS_SIZE = 0x200; const POLL_INTERVAL_MS = 20;
const POLL_INTERVAL_MS = 5;
const DOT_INTERVAL_MS = 25;
const COLS = 80; const COLS = 80;
const TAB_WIDTH = 8; const TAB_WIDTH = 8;
@@ -51,19 +50,8 @@ 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.forth = new Worker('boot.js', { type: 'module' }); this.worker = new Worker('boot.js');
this.worker.postMessage(this.mem);
this.print("Assembling kernel ");
this.dots = setInterval(() => this.print("."), DOT_INTERVAL_MS);
this.forth.postMessage({ type: "load", mem: this.mem });
this.forth.onmessage = (e) => {
clearInterval(this.dots);
this.print(" done\n");
this.print("Loading prelude ");
this.forth.postMessage({ type: "boot" });
this.dots = setInterval(() => this.print("."), DOT_INTERVAL_MS);
};
fetch('prelude.f') fetch('prelude.f')
.then(res => res.text()) .then(res => res.text())
@@ -89,21 +77,15 @@ class Emulator {
if (!this.input_enable) { if (!this.input_enable) {
const sysready = Atomics.load(this.mem_u8, SYSREADY); const sysready = Atomics.load(this.mem_u8, SYSREADY);
if (sysready != 0) { if (sysready != 0) {
clearInterval(this.dots);
this.print(" done\n");
Atomics.store(this.mem_u8, SYSINTER, 1);
Atomics.notify(this.mem_i32, SYSINTER / 4);
this.input_enable = true; this.input_enable = true;
this.blink = true;
this.flush_output(); this.flush_output();
document.getElementById('cursor').classList.add('blinking');
} }
} }
} }
fifo_next(idx) { fifo_next(idx) {
return (idx + 1) & 0x7f; return (idx + 1) & 0x1f;
} }
handle_tx_data(head, tail) { handle_tx_data(head, tail) {
@@ -191,14 +173,12 @@ class Emulator {
} }
e.preventDefault(); e.preventDefault();
this.blink = false;
this.flush_output(); this.flush_output();
document.getElementById('cursor').classList.remove('blinking'); document.getElementById('cursor').classList.remove('blinking');
if (this.idle_timer) if (this.idle_timer)
clearTimeout(this.idle_timer); clearTimeout(this.idle_timer);
this.idle_timer = setTimeout(() => { this.idle_timer = setTimeout(() => {
this.blink = true;
document.getElementById('cursor').classList.add('blinking'); document.getElementById('cursor').classList.add('blinking');
}, CURSOR_IDLE_TIME_MS); }, CURSOR_IDLE_TIME_MS);
} }
@@ -285,12 +265,10 @@ class Emulator {
return row.map((c, x) => { return row.map((c, x) => {
const ec = this.html_escape(c); const ec = this.html_escape(c);
if (this.input_enable if (this.input_enable
&& x == this.cursor.x && y == this.cursor.y) { && x == this.cursor.x && y == this.cursor.y)
const cl = this.blink ? 'class="blinking"' : ''; return '<span id="cursor">' + ec + '</span>';
return `<span id="cursor" ${cl}>` + ec + '</span>'; else
} else {
return ec; return ec;
}
}).join('').trimEnd(); }).join('').trimEnd();
}).join('\n'); }).join('\n');
this.output.innerHTML = html; this.output.innerHTML = html;
@@ -347,16 +325,6 @@ class Emulator {
} }
window.addEventListener('DOMContentLoaded', () => { window.addEventListener('DOMContentLoaded', () => {
const output = document.getElementById('output'); document.getElementById('output').innerText = '';
output.innerText = '';
if (!self.crossOriginIsolated) {
output.innerText = "Yeah so there's this thing where Chromium ends "
+ "up ignoring COOP/COEP\nheaders after a hard reload "
+ "sometimes, and I haven't been able to\nfigure out how to "
+ "work around it yet. If you just wait a little while\nand "
+ "then reload normally then hopefully it should work haha";
} else {
window.emu = new Emulator(); window.emu = new Emulator();
}
}); });

View File

@@ -7,7 +7,8 @@
</head> </head>
<body> <body>
<script type="text/javascript" src="emu.js"></script> <script type="text/javascript" src="emu.js"></script>
<div id="output"><noscript>I'm afraid you need javascript enabled for this to work :( <div id="output">I'm afraid you need javascript enabled for this to work :(
cringe, I know... but I promise there are no frameworks&#8482;</noscript></div></body> cringe, I know... but I promise there are no frameworks&#8482;</div>
</body>
</html> </html>

View File

@@ -1,5 +1,12 @@
76 EMIT 111 EMIT 97 EMIT 100 EMIT 105 EMIT 110 EMIT 103 EMIT 32 EMIT
112 EMIT 114 EMIT 101 EMIT 108 EMIT 117 EMIT 100 EMIT 101 EMIT 32 EMIT
: \ KEY 10 = 0BRANCH [ -20 , ] ; IMMEDIATE \ Now we have line comments :) : \ KEY 10 = 0BRANCH [ -20 , ] ; IMMEDIATE \ Now we have line comments :)
\ We'll periodically sprinkle these in so that it's clear to the user
\ that things are happening.
46 EMIT
\ Conditionals \ Conditionals
: IF : IF
@@ -20,6 +27,8 @@
SWAP ! SWAP !
; IMMEDIATE ; IMMEDIATE
46 EMIT
\ Loops \ Loops
: BEGIN HERE @ ; IMMEDIATE : BEGIN HERE @ ; IMMEDIATE
@@ -34,6 +43,8 @@
HERE @ - , HERE @ - ,
; IMMEDIATE ; IMMEDIATE
46 EMIT
\ Recursive calls \ Recursive calls
: RECURSE LATEST @ >CFA , ; IMMEDIATE : RECURSE LATEST @ >CFA , ; IMMEDIATE
@@ -50,6 +61,8 @@
( ( Take that, C ) ) ( ( Take that, C ) )
46 EMIT
\ Printing utilities \ Printing utilities
: CR 10 EMIT ; : CR 10 EMIT ;
@@ -67,6 +80,8 @@
+ EMIT + EMIT
; ;
CHAR . EMIT
: . : .
\ Handle negatives \ Handle negatives
DUP 0< IF CHAR - EMIT NEGATE THEN DUP 0< IF CHAR - EMIT NEGATE THEN
@@ -95,6 +110,8 @@
2DROP 2DROP
; ;
CHAR . EMIT
: TYPE ( addr len -- ) : TYPE ( addr len -- )
BEGIN BEGIN
DUP 0= IF 2DROP EXIT THEN DUP 0= IF 2DROP EXIT THEN
@@ -103,6 +120,8 @@
AGAIN AGAIN
; ;
CHAR . EMIT
: C, HERE @ C! 1 HERE +! ; : C, HERE @ C! 1 HERE +! ;
: ." : ."
@@ -132,6 +151,8 @@
THEN THEN
; IMMEDIATE ; IMMEDIATE
CHAR . EMIT
\ Misc utilities \ Misc utilities
: NIP SWAP DROP ; : NIP SWAP DROP ;
@@ -150,6 +171,8 @@
: [COMPILE] ' , ; IMMEDIATE : [COMPILE] ' , ; IMMEDIATE
CHAR . EMIT
\ Constants, variables and values \ Constants, variables and values
: CONSTANT : CONSTANT
@@ -181,21 +204,24 @@
THEN THEN
; IMMEDIATE ; IMMEDIATE
CHAR . EMIT
\ Peripheral register addresses \ Peripheral register addresses
HEX HEX
000 CONSTANT TXBUF 00 CONSTANT TXBUF
080 CONSTANT RXBUF 20 CONSTANT RXBUF
100 CONSTANT TXHEAD 40 CONSTANT TXHEAD
104 CONSTANT TXTAIL 44 CONSTANT TXTAIL
108 CONSTANT RXHEAD 48 CONSTANT RXHEAD
10C CONSTANT RXTAIL 4C CONSTANT RXTAIL
110 CONSTANT SYSREADY 50 CONSTANT SYSREADY
114 CONSTANT SYSINTER
DECIMAL DECIMAL
46 EMIT
\ A better word-not-found handler \ A better word-not-found handler
: ANY-RX? RXHEAD AC@ RXTAIL AC@ <> ; : ANY-RX? RXHEAD AC@ RXTAIL AC@ <> ;
@@ -218,11 +244,13 @@ DECIMAL
' WNF-HANDLER TO WNFHOOK ' WNF-HANDLER TO WNFHOOK
CHAR . EMIT
\ Version number \ Version number
0 CONSTANT VERSION-MAJOR 0 CONSTANT VERSION-MAJOR
2 CONSTANT VERSION-MINOR 1 CONSTANT VERSION-MINOR
2 CONSTANT VERSION-PATCH 0 CONSTANT VERSION-PATCH
: PRINT-VERSION : PRINT-VERSION
CHAR v EMIT VERSION-MAJOR . CHAR v EMIT VERSION-MAJOR .
@@ -230,6 +258,8 @@ DECIMAL
CHAR . EMIT VERSION-PATCH . CHAR . EMIT VERSION-PATCH .
; ;
CHAR . EMIT
\ Welcome banner \ Welcome banner
: BANNER : BANNER
@@ -239,7 +269,7 @@ DECIMAL
." |__,__/_/ .__(_)___/_//_/" CR ." |__,__/_/ .__(_)___/_//_/" CR
." /_/ " CR ." /_/ " CR
CR CR
." Welcome to Wipforth " PRINT-VERSION ." !" CR ." Wipforth " PRINT-VERSION CR
." Copyright (c) Camden Dixie O'Brien" CR ." Copyright (c) Camden Dixie O'Brien" CR
CR CR
." Wipforth is freely available to use, modify and distribute for personal use" CR ." Wipforth is freely available to use, modify and distribute for personal use" CR
@@ -247,8 +277,8 @@ DECIMAL
CR CR
; ;
\ Set SYSREADY high and wait until interactive ." done" CR
1 SYSREADY AC!
SYSINTER WAIT DROP
BANNER BANNER
\ Set SYSREADY high to enable user input
1 SYSREADY AC!

View File

@@ -16,8 +16,9 @@
'(("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))
("ws" . (text/plain)) ("wat" . (text/plain))
("png" . (image/png)))) ("png" . (image/png))))
(define (mime-type path) (define (mime-type path)

View File

@@ -19,19 +19,13 @@
(define client (client-setup)) (define client (client-setup))
(navigate client "http://localhost:8080") (navigate client "http://localhost:8080")
(sleep 1) (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))
(line (second (lines display)))) (first-line (first (lines display))))
(assert (string-match "Loading prelude \\.+ done" line) (assert (string-match "Loading prelude \\.+ done" first-line)
(format #f "Prelude load line: ~s" line)))) (format #f "Prelude load line: ~s" first-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")

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