Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
d3d0801236
|
@@ -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 `SharedMemoryBuffer` 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,13 +62,16 @@ 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
|
||||||
|
|||||||
@@ -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;
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,4 +3,4 @@ emu.js
|
|||||||
index.html
|
index.html
|
||||||
prelude.f
|
prelude.f
|
||||||
styles.css
|
styles.css
|
||||||
wipforth.ws
|
wipforth.wasm
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ 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;
|
||||||
@@ -51,29 +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.prof = new Worker("prof.js");
|
this.worker = new Worker('boot.js');
|
||||||
this.prof.onmessage = (e) => {
|
this.worker.postMessage(this.mem);
|
||||||
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({ 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')
|
fetch('prelude.f')
|
||||||
.then(res => res.text())
|
.then(res => res.text())
|
||||||
@@ -99,7 +77,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');
|
||||||
|
|||||||
@@ -249,8 +249,8 @@ CHAR . EMIT
|
|||||||
\ Version number
|
\ Version number
|
||||||
|
|
||||||
0 CONSTANT VERSION-MAJOR
|
0 CONSTANT VERSION-MAJOR
|
||||||
2 CONSTANT VERSION-MINOR
|
1 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 .
|
||||||
|
|||||||
@@ -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));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
+2
-1
@@ -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)
|
||||||
|
|||||||
@@ -21,17 +21,11 @@
|
|||||||
(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))
|
||||||
(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
File diff suppressed because it is too large
Load Diff
-2098
File diff suppressed because it is too large
Load Diff
@@ -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));
|
|
||||||
Reference in New Issue
Block a user