Compare commits

...

9 Commits

Author SHA1 Message Date
956d42d008 Create assembler driver script for recording word addresses 2026-03-20 12:40:09 +00:00
87d8345017 Run profiler while prelude is loading 2026-03-19 22:03:46 +00:00
d39fe580fc Implement sampling profiler 2026-03-19 22:03:36 +00:00
ba8c99a123 Mirror IP and RSP regs in memory
This will allow the profiler to sample the current return stack and
instruction pointer.  It can't just use the ip and rsp globals
directly, as I had initially planned, since these cannot be sent
in between threads.
2026-03-19 22:01:51 +00:00
cc8ae742f0 Add more opcodes to assembler 2026-03-19 22:01:33 +00:00
812443d6ee Support exports of globals in assembler 2026-03-19 19:53:50 +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
9 changed files with 205 additions and 96 deletions

View File

@@ -1,31 +1,24 @@
# Wipforth
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.
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 :)
- 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)
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 a description of the peripherals, see the
[Peripherals](#peripherals) section below.
## Building and Running Locally
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:
There's a [Guile](https://www.gnu.org/software/guile/) script in the
repo you can use for this:
```
guile server.scm
@@ -34,14 +27,20 @@ guile server.scm
You should then be able to open <http://localhost:8080> in a browser
and use the system from there.
**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.
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:
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.
- `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.
## End-to-End Tests
@@ -62,16 +61,13 @@ 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:
It will print a JUnit XML report to standard out. You can
pretty-print it with `xmllint` if you have it installed:
```
guile tests.scm | xmllint --format -
```
Though, of course, this will require that you have `xmllint` on your
system.
## Peripherals
### Terminal

29
asm.js
View File

@@ -187,6 +187,9 @@ const opcodes = {
"i32.shl": 0x74,
"i32.shr_s": 0x75,
"i32.shr_u": 0x76,
"i64.or": 0x84,
"i64.shl": 0x86,
"i64.extend_i32_u": 0xad,
// Threads instructions
"memory.atomic.notify": [ 0xfe, 0x00 ],
@@ -194,6 +197,7 @@ const opcodes = {
"i32.atomic.load": [ 0xfe, 0x10 ],
"i32.atomic.load8_u": [ 0xfe, 0x12 ],
"i32.atomic.store": [ 0xfe, 0x17 ],
"i64.atomic.store": [ 0xfe, 0x18 ],
"i32.atomic.store8": [ 0xfe, 0x19 ],
};
@@ -888,6 +892,7 @@ const Section = Object.freeze({
const Kind = Object.freeze({
FUNC: 0x00,
MEM: 0x02,
GLOBAL: 0x03,
});
export class Assembler {
@@ -942,8 +947,28 @@ export class Assembler {
}
action_export(action) {
const index = Object.keys(this.funcs).indexOf(action.name);
this.exports[action.name] = { kind: Kind.FUNC, index };
const func_index = Object.keys(this.funcs).indexOf(action.name);
if (func_index != -1) {
this.exports[action.name] = {
kind: Kind.FUNC,
index: func_index,
};
return;
}
const global_index = Object.keys(this.globals).indexOf(action.name);
if (global_index != -1) {
this.exports[action.name] = {
kind: Kind.GLOBAL,
index: global_index,
};
return;
}
console.error(
`ERROR: Unable to resolve export ${action.name} `
+ "(only functions and globals currently supported)"
);
}
action_func(action) {

15
boot.js
View File

@@ -10,10 +10,17 @@ const assemble = (async () => {
})();
self.onmessage = async (e) => {
const exports = { emu: { mem: e.data } };
switch (e.data.type) {
case "load":
const exports = { emu: { mem: e.data.mem } };
const wasm = await assemble;
const mod = await WebAssembly.instantiate(wasm, exports);
await self.postMessage('booting');
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');
break;
}
};

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);

18
emu.js
View File

@@ -51,13 +51,28 @@ 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(this.mem);
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 });
};
fetch('prelude.f')
@@ -84,6 +99,7 @@ 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

@@ -250,7 +250,7 @@ CHAR . EMIT
0 CONSTANT VERSION-MAJOR
2 CONSTANT VERSION-MINOR
0 CONSTANT VERSION-PATCH
1 CONSTANT VERSION-PATCH
: PRINT-VERSION
CHAR v EMIT VERSION-MAJOR .

48
prof.js Normal file
View File

@@ -0,0 +1,48 @@
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

@@ -9,6 +9,9 @@
.def RXHEAD 48h
.def RXTAIL 4Ch
;; Mirror of registers for profiler to sample
.def PROF_DATA 58h
.def DICT_START 0200h
.def RSP_INIT 10000h
@@ -2073,6 +2076,13 @@ KERNEL_DEFS_END:
.func trampoline
loop iter
global.get fn call_indirect codeword codewords
i32.const PROF_DATA
global.get ip i64.extend_i32_u
global.get rsp i64.extend_i32_u
i64.const 32 i64.shl i64.or
i64.atomic.store 3 0
global.get run br_if iter
end

16
words.js Normal file
View File

@@ -0,0 +1,16 @@
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));