Compare commits

..

16 Commits

Author SHA1 Message Date
aa9c18346d Add message for when chromium hard reload bug breaks things 2026-03-21 23:16:37 +00:00
a2f06c977e Wrap no JS message in noscript tag 2026-03-21 23:15:57 +00:00
58e2cf3e1f Fix SharedArrayBuffer name in README 2026-03-20 13:36:54 +00:00
efdae93d90 Add favicon to deploy manifest 2026-03-20 13:35:59 +00:00
804626ddad Add asm.js to deploy manifest 2026-03-20 13:15:49 +00:00
cbe5733fcd Bump version patch number 2026-03-20 13:04:36 +00:00
7961c68639 Reduce wait in e2e tests 2026-03-20 13:04:28 +00:00
d202157a58 Reduce dot interval to 25 ms 2026-03-20 13:04:17 +00:00
97fc43bf93 Add "Welcome to" to welcome message 2026-03-20 13:04:10 +00:00
2c13ad4e1f Handle prelude loading message in JS rather than Forth 2026-03-20 13:03:57 +00:00
36429bf8bc Rework tokenizer to avoid unecessary allocations 2026-03-20 13:02:40 +00:00
c20e7e181b Reduce poll interval to 5 ms 2026-03-20 13:02:29 +00:00
fe5c55cabf Increase TX and RX buffer size to 128 bytes 2026-03-20 13:02:07 +00:00
67fc1d8d7b Remove race condition between assemble and prelude load prints 2026-03-18 15:17:08 +00:00
4000522b3a Remove obsolete assembly driver script 2026-03-18 15:08:57 +00:00
19ef69958d Update README 2026-03-18 15:04:32 +00:00
10 changed files with 156 additions and 127 deletions

View File

@@ -2,7 +2,7 @@
Wipforth is a Forth implementation that runs in the WebAssembly Wipforth is a Forth implementation that runs in the WebAssembly
virtual machine. The system is bootstrapped from source on page load: virtual machine. The system is bootstrapped from source on page load:
the only binary file is the favicon :) the only non-text file is the favicon :)
I/O is done via memory-mapped peripherals, which are emulated in I/O is done via memory-mapped peripherals, which are emulated in
JavaScript. JavaScript.
@@ -17,8 +17,8 @@ JavaScript.
## Building and Running Locally ## Building and Running Locally
There is simple [Guile](https://www.gnu.org/software/guile/) script There's a [Guile](https://www.gnu.org/software/guile/) script in the
you can use for this: repo you can use for this:
``` ```
guile server.scm guile server.scm
@@ -27,15 +27,17 @@ 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.
Since everything is bootstrapped on the client, you only need an HTTP However, since everything is bootstrapped on the client, basically any
server, so if you don't have Guile on your system you can bring your HTTP server will do as long as it sets the appropriate response
own. The only requirement is that it sets the cross-origin isolation headers for `SharedArrayBuffer` use:
headers required for `SharedMemoryBuffer` use:
- `Cross-Origin-Opener-Policy: same-origin` - `Cross-Origin-Opener-Policy: same-origin`
- `Cross-Origin-Embedder-Policy: require-corp` - `Cross-Origin-Embedder-Policy: require-corp`
You should **definitely not** use `server.scm` to serve the 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 application on the open internet or anything like that; I just hacked
it together for testing on localhost during development and it's it together for testing on localhost during development and it's
probably hilariously insecure. probably hilariously insecure.
@@ -72,20 +74,21 @@ guile tests.scm | xmllint --format -
| Name | Address | Size / B | Access | | Name | Address | Size / B | Access |
|--------|---------|----------|--------------| |--------|---------|----------|--------------|
| TXBUF | 00h | 32 | write | | TXBUF | 000h | 32 | write |
| RXBUF | 20h | 32 | read | | RXBUF | 080h | 32 | read |
| TXHEAD | 40h | 4 | atomic read | | TXHEAD | 100h | 4 | atomic read |
| TXTAIL | 44h | 4 | atomic write | | TXTAIL | 104h | 4 | atomic write |
| RXHEAD | 48h | 4 | atomic write | | RXHEAD | 108h | 4 | atomic write |
| RXTAIL | 4Ch | 4 | atomic read | | RXTAIL | 10Ch | 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 32-byte FIFO ring buffers used for data - `xBUF` registers are 128-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. data, and `xTAIL` being the offset of the first byte *after* the
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*
@@ -95,7 +98,10 @@ unoccupied byte between the tail and the head.
| Name | Address | Size / B | Access | | Name | Address | Size / B | Access |
|----------|---------|----------|--------------| |----------|---------|----------|--------------|
| SYSREADY | 50h | 1 | atomic write | | SYSREADY | 110h | 4 | 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. up and is ready for user input. `SYSINTER` is set (and notified on)
once the emulator has enabled user input and the system is
interactive.

39
asm.js
View File

@@ -14,20 +14,29 @@ class Tokenizer {
this.buffer = []; this.buffer = [];
this.comment = false; this.comment = false;
this.string = false; this.string = false;
this.cursor = 0;
} }
find(pred) {
for (let i = this.cursor; i < this.buffer.length; ++i) {
if (pred(this.buffer[i]))
return i;
}
return null;
}
skip() { skip() {
const idx = this.buffer.findIndex((cp) => !this.skips.has(cp)); const idx = this.find((cp) => !this.skips.has(cp));
this.buffer = idx == -1 ? [] : this.buffer.slice(idx); this.cursor = idx == null ? this.buffer.length : idx;
} }
next_string() { next_string() {
const idx = this.buffer.findIndex((cp) => cp == this.string_quote); const idx = this.find((cp) => cp == this.string_quote);
if (idx == -1) { if (idx == null) {
this.string = true; this.string = true;
} else { } else {
const string = this.buffer.slice(0, idx).join(""); const string = this.buffer.slice(this.cursor, idx).join("");
this.buffer = this.buffer.slice(idx + 1); this.cursor = idx + 1;
this.string = false; this.string = false;
return { string: string }; return { string: string };
} }
@@ -38,18 +47,20 @@ class Tokenizer {
return this.next_string(); return this.next_string();
this.skip(); this.skip();
if (this.buffer[0] == LINE_END) if (this.buffer[this.cursor] == LINE_END) {
return this.buffer.shift(); ++this.cursor;
return LINE_END;
}
if (this.buffer[0] == this.string_quote) { if (this.buffer[this.cursor] == this.string_quote) {
this.buffer.shift(); ++this.cursor;
return this.next_string(); return this.next_string();
} }
const idx = this.buffer.findIndex((cp) => this.delims.has(cp)); const idx = this.find((cp) => this.delims.has(cp));
if (idx != -1) { if (idx != null) {
const token = this.buffer.slice(0, idx).join(""); const token = this.buffer.slice(this.cursor, idx).join("");
this.buffer = this.buffer.slice(idx); this.cursor = idx;
return token; return token;
} }
} }

19
boot.js
View File

@@ -10,10 +10,17 @@ const assemble = (async () => {
})(); })();
self.onmessage = async (e) => { self.onmessage = async (e) => {
const exports = { emu: { mem: e.data } }; switch (e.data.type) {
const wasm = await assemble; case "load":
const mod = await WebAssembly.instantiate(wasm, exports); const exports = { emu: { mem: e.data.mem } };
await self.postMessage('booting'); const wasm = await assemble;
mod.instance.exports.reset(); self.mod = await WebAssembly.instantiate(wasm, exports);
console.log('System halt'); await self.postMessage('ready');
break;
case "boot":
self.mod.instance.exports.reset();
console.log('System halt');
break;
}
}; };

View File

@@ -1,5 +1,7 @@
asm.js
boot.js boot.js
emu.js emu.js
favicon.png
index.html index.html
prelude.f prelude.f
styles.css styles.css

View File

@@ -1,9 +0,0 @@
import { Assembler } from "./asm.js";
import { writeAll } from "jsr:@std/io/write-all";
const asm = new Assembler();
for await (const chunk of Deno.stdin.readable) {
asm.push(chunk);
}
const wasm = asm.wasm();
await writeAll(Deno.stdout, wasm);

73
emu.js
View File

@@ -1,17 +1,17 @@
const TXBUF = 0x00; const TXBUF = 0x000;
const RXBUF = 0x20; const RXBUF = 0x080;
const TXHEAD = 0x40; const TXHEAD = 0x100;
const TXTAIL = 0x44; const TXTAIL = 0x104;
const RXHEAD = 0x48; const RXHEAD = 0x108;
const RXTAIL = 0x4c; const RXTAIL = 0x10c;
const SYSREADY = 0x50;
const TXBUF_SIZE = 32; const SYSREADY = 0x110;
const RXBUF_SIZE = 32; const SYSINTER = 0x114;
const PERIPHS_SIZE = 81;
const POLL_INTERVAL_MS = 20; const PERIPHS_SIZE = 0x200;
const DOT_INTERVAL_MS = 120;
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,13 +51,18 @@ 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.print("Assembling kernel "); this.print("Assembling kernel ");
const dots = setInterval(() => this.print("."), DOT_INTERVAL_MS); this.dots = setInterval(() => this.print("."), DOT_INTERVAL_MS);
this.worker = new Worker('boot.js', { type: 'module' }); this.forth.postMessage({ type: "load", mem: this.mem });
this.worker.postMessage(this.mem); this.forth.onmessage = (e) => {
this.worker.onmessage = (e) => { clearInterval(this.dots);
clearInterval(dots);
this.print(" done\n"); 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')
@@ -84,15 +89,21 @@ 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) & 0x1f; return (idx + 1) & 0x7f;
} }
handle_tx_data(head, tail) { handle_tx_data(head, tail) {
@@ -180,12 +191,14 @@ 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);
} }
@@ -272,10 +285,12 @@ 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) {
return '<span id="cursor">' + ec + '</span>'; const cl = this.blink ? 'class="blinking"' : '';
else return `<span id="cursor" ${cl}>` + ec + '</span>';
} else {
return ec; return ec;
}
}).join('').trimEnd(); }).join('').trimEnd();
}).join('\n'); }).join('\n');
this.output.innerHTML = html; this.output.innerHTML = html;
@@ -332,6 +347,16 @@ class Emulator {
} }
window.addEventListener('DOMContentLoaded', () => { window.addEventListener('DOMContentLoaded', () => {
document.getElementById('output').innerText = ''; const output = document.getElementById('output');
window.emu = new Emulator(); 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();
}
}); });

View File

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

View File

@@ -1,12 +1,5 @@
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
@@ -27,8 +20,6 @@
SWAP ! SWAP !
; IMMEDIATE ; IMMEDIATE
46 EMIT
\ Loops \ Loops
: BEGIN HERE @ ; IMMEDIATE : BEGIN HERE @ ; IMMEDIATE
@@ -43,8 +34,6 @@
HERE @ - , HERE @ - ,
; IMMEDIATE ; IMMEDIATE
46 EMIT
\ Recursive calls \ Recursive calls
: RECURSE LATEST @ >CFA , ; IMMEDIATE : RECURSE LATEST @ >CFA , ; IMMEDIATE
@@ -61,8 +50,6 @@
( ( Take that, C ) ) ( ( Take that, C ) )
46 EMIT
\ Printing utilities \ Printing utilities
: CR 10 EMIT ; : CR 10 EMIT ;
@@ -80,8 +67,6 @@
+ EMIT + EMIT
; ;
CHAR . EMIT
: . : .
\ Handle negatives \ Handle negatives
DUP 0< IF CHAR - EMIT NEGATE THEN DUP 0< IF CHAR - EMIT NEGATE THEN
@@ -110,8 +95,6 @@ CHAR . EMIT
2DROP 2DROP
; ;
CHAR . EMIT
: TYPE ( addr len -- ) : TYPE ( addr len -- )
BEGIN BEGIN
DUP 0= IF 2DROP EXIT THEN DUP 0= IF 2DROP EXIT THEN
@@ -120,8 +103,6 @@ CHAR . EMIT
AGAIN AGAIN
; ;
CHAR . EMIT
: C, HERE @ C! 1 HERE +! ; : C, HERE @ C! 1 HERE +! ;
: ." : ."
@@ -151,8 +132,6 @@ CHAR . EMIT
THEN THEN
; IMMEDIATE ; IMMEDIATE
CHAR . EMIT
\ Misc utilities \ Misc utilities
: NIP SWAP DROP ; : NIP SWAP DROP ;
@@ -171,8 +150,6 @@ CHAR . EMIT
: [COMPILE] ' , ; IMMEDIATE : [COMPILE] ' , ; IMMEDIATE
CHAR . EMIT
\ Constants, variables and values \ Constants, variables and values
: CONSTANT : CONSTANT
@@ -204,24 +181,21 @@ CHAR . EMIT
THEN THEN
; IMMEDIATE ; IMMEDIATE
CHAR . EMIT
\ Peripheral register addresses \ Peripheral register addresses
HEX HEX
00 CONSTANT TXBUF 000 CONSTANT TXBUF
20 CONSTANT RXBUF 080 CONSTANT RXBUF
40 CONSTANT TXHEAD 100 CONSTANT TXHEAD
44 CONSTANT TXTAIL 104 CONSTANT TXTAIL
48 CONSTANT RXHEAD 108 CONSTANT RXHEAD
4C CONSTANT RXTAIL 10C CONSTANT RXTAIL
50 CONSTANT SYSREADY 110 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@ <> ;
@@ -244,13 +218,11 @@ 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 2 CONSTANT VERSION-MINOR
0 CONSTANT VERSION-PATCH 2 CONSTANT VERSION-PATCH
: PRINT-VERSION : PRINT-VERSION
CHAR v EMIT VERSION-MAJOR . CHAR v EMIT VERSION-MAJOR .
@@ -258,8 +230,6 @@ CHAR . EMIT
CHAR . EMIT VERSION-PATCH . CHAR . EMIT VERSION-PATCH .
; ;
CHAR . EMIT
\ Welcome banner \ Welcome banner
: BANNER : BANNER
@@ -269,7 +239,7 @@ CHAR . EMIT
." |__,__/_/ .__(_)___/_//_/" CR ." |__,__/_/ .__(_)___/_//_/" CR
." /_/ " CR ." /_/ " CR
CR CR
." Wipforth " PRINT-VERSION CR ." Welcome to 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
@@ -277,8 +247,8 @@ CHAR . EMIT
CR CR
; ;
." done" CR \ Set SYSREADY high and wait until interactive
BANNER
\ Set SYSREADY high to enable user input
1 SYSREADY AC! 1 SYSREADY AC!
SYSINTER WAIT DROP
BANNER

View File

@@ -19,7 +19,7 @@
(define client (client-setup)) (define client (client-setup))
(navigate client "http://localhost:8080") (navigate client "http://localhost:8080")
(sleep 5) (sleep 1)
(define-test kernel-assembles-successfully (define-test kernel-assembles-successfully
(let* ((display (get-display client)) (let* ((display (get-display client))

View File

@@ -2,12 +2,12 @@
.import main "emu" "mem" .import main "emu" "mem"
;; Peripheral registers ;; Peripheral registers
.def TXBUF 00h .def TXBUF 000h
.def RXBUF 20h .def RXBUF 080h
.def TXHEAD 40h .def TXHEAD 100h
.def TXTAIL 44h .def TXTAIL 104h
.def RXHEAD 48h .def RXHEAD 108h
.def RXTAIL 4Ch .def RXTAIL 10Ch
.def DICT_START 0200h .def DICT_START 0200h
@@ -635,6 +635,15 @@
end end
call next call next
.func wait
.elem codewords wait WAIT_CODEWORD
call pop
i32.const 0
i64.const -1
memory.atomic.wait32 2 0
call push
call next
;; Core utility words ;; Core utility words
.func exit .func exit
@@ -752,7 +761,7 @@
local.get head local.get head
i32.const 1 i32.const 1
i32.add i32.add
i32.const 1Fh i32.const 7Fh
i32.and i32.and
i32.atomic.store8 0 0 i32.atomic.store8 0 0
@@ -768,7 +777,7 @@
local.tee tail local.tee tail
i32.const 1 i32.const 1
i32.add i32.add
i32.const 1Fh i32.const 7Fh
i32.and i32.and
local.tee n local.tee n
i32.const TXHEAD i32.const TXHEAD
@@ -1226,6 +1235,15 @@ COPY:
.word COPY_CODEWORD .word COPY_CODEWORD
.def PREV _COPY .def PREV _COPY
_WAIT:
.word PREV
.byte 4
.utf8 "WAIT"
.align
WAIT:
.word WAIT_CODEWORD
.def PREV _WAIT
_EXIT: _EXIT:
.word PREV .word PREV
.byte 4 .byte 4