Compare commits

..

1 Commits

Author SHA1 Message Date
90bf0691a3 Update README 2026-03-18 14:49:30 +00:00
9 changed files with 72 additions and 187 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 non-text file is the favicon :) the only binary 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's a [Guile](https://www.gnu.org/software/guile/) script in the There is simple [Guile](https://www.gnu.org/software/guile/) script
repo you can use for this: you can use for this:
``` ```
guile server.scm guile server.scm
@@ -27,17 +27,15 @@ 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 Since everything is bootstrapped on the client, you only need an HTTP
HTTP server will do as long as it sets the appropriate response server, so if you don't have Guile on your system you can bring your
headers for `SharedMemoryBuffer` use: own. The only requirement is that it sets the cross-origin isolation
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`
So, if you don't have Guile on your system you can use something else You should **definitely not** use `server.scm` to serve the
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.

29
asm.js
View File

@@ -187,9 +187,6 @@ const opcodes = {
"i32.shl": 0x74, "i32.shl": 0x74,
"i32.shr_s": 0x75, "i32.shr_s": 0x75,
"i32.shr_u": 0x76, "i32.shr_u": 0x76,
"i64.or": 0x84,
"i64.shl": 0x86,
"i64.extend_i32_u": 0xad,
// Threads instructions // Threads instructions
"memory.atomic.notify": [ 0xfe, 0x00 ], "memory.atomic.notify": [ 0xfe, 0x00 ],
@@ -197,7 +194,6 @@ const opcodes = {
"i32.atomic.load": [ 0xfe, 0x10 ], "i32.atomic.load": [ 0xfe, 0x10 ],
"i32.atomic.load8_u": [ 0xfe, 0x12 ], "i32.atomic.load8_u": [ 0xfe, 0x12 ],
"i32.atomic.store": [ 0xfe, 0x17 ], "i32.atomic.store": [ 0xfe, 0x17 ],
"i64.atomic.store": [ 0xfe, 0x18 ],
"i32.atomic.store8": [ 0xfe, 0x19 ], "i32.atomic.store8": [ 0xfe, 0x19 ],
}; };
@@ -892,7 +888,6 @@ const Section = Object.freeze({
const Kind = Object.freeze({ const Kind = Object.freeze({
FUNC: 0x00, FUNC: 0x00,
MEM: 0x02, MEM: 0x02,
GLOBAL: 0x03,
}); });
export class Assembler { export class Assembler {
@@ -947,28 +942,8 @@ export class Assembler {
} }
action_export(action) { action_export(action) {
const func_index = Object.keys(this.funcs).indexOf(action.name); const index = Object.keys(this.funcs).indexOf(action.name);
if (func_index != -1) { this.exports[action.name] = { kind: Kind.FUNC, index };
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) { action_func(action) {

15
boot.js
View File

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

9
driver.js Normal file
View File

@@ -0,0 +1,9 @@
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,28 +51,13 @@ 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.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 "); this.print("Assembling kernel ");
const dots = setInterval(() => this.print("."), DOT_INTERVAL_MS); const dots = setInterval(() => this.print("."), DOT_INTERVAL_MS);
this.worker = new Worker('boot.js', { type: 'module' }); this.worker = new Worker('boot.js', { type: 'module' });
this.worker.postMessage({ type: "load", mem: this.mem }); this.worker.postMessage(this.mem);
this.worker.onmessage = (e) => { this.worker.onmessage = (e) => {
clearInterval(dots); clearInterval(dots);
this.print(" done\n"); this.print(" done\n");
this.worker.postMessage({ type: "boot" });
this.prof.postMessage({ type: "start", mem: this.mem });
}; };
fetch('prelude.f') fetch('prelude.f')
@@ -99,7 +84,6 @@ 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) {
this.prof.postMessage({ type: "stop" });
this.input_enable = true; this.input_enable = true;
this.flush_output(); this.flush_output();
document.getElementById('cursor').classList.add('blinking'); document.getElementById('cursor').classList.add('blinking');

View File

@@ -250,7 +250,7 @@ CHAR . EMIT
0 CONSTANT VERSION-MAJOR 0 CONSTANT VERSION-MAJOR
2 CONSTANT VERSION-MINOR 2 CONSTANT VERSION-MINOR
1 CONSTANT VERSION-PATCH 0 CONSTANT VERSION-PATCH
: PRINT-VERSION : PRINT-VERSION
CHAR v EMIT VERSION-MAJOR . 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

@@ -9,9 +9,6 @@
.def RXHEAD 48h .def RXHEAD 48h
.def RXTAIL 4Ch .def RXTAIL 4Ch
;; Mirror of registers for profiler to sample
.def PROF_DATA 58h
.def DICT_START 0200h .def DICT_START 0200h
.def RSP_INIT 10000h .def RSP_INIT 10000h
@@ -2076,13 +2073,6 @@ KERNEL_DEFS_END:
.func trampoline .func trampoline
loop iter loop iter
global.get fn call_indirect codeword codewords 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 global.get run br_if iter
end end

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