Compare commits

...

33 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
0a52388030 Update deploy manifest 2026-03-18 14:25:11 +00:00
6e8439eeaf Bump version number 2026-03-18 14:24:41 +00:00
eaa3242cc0 Update e2e tests 2026-03-18 14:24:25 +00:00
f77adffbef Update MIME types in server.scm 2026-03-18 14:24:05 +00:00
c91f46be88 Assemble kernel on client 2026-03-18 14:23:37 +00:00
6ee4adfea5 Translate kernel to Wasmasm 2026-03-18 10:41:09 +00:00
5dc0a7a601 Add temporary driver script 2026-03-18 10:35:14 +00:00
896a1ca563 Implement (limited) forward reference handling 2026-03-18 10:32:49 +00:00
37d56988ef Make a couple of tweaks to the kernel in preparation for porting 2026-03-18 10:30:26 +00:00
6c643f8402 Don't silently ignore trailing characters in numbers 2026-03-18 10:29:46 +00:00
7828b0f112 Yield newline token at end of comment 2026-03-15 21:39:14 +00:00
e7affbf8b7 Add .zero directive 2026-03-15 21:28:25 +00:00
02ee4c3c88 Support symbols in .at address field 2026-03-15 21:27:48 +00:00
c21b3c79c7 Fix names of atomic load and store opcodes 2026-03-15 20:04:49 +00:00
1318c3cc4e Add i64.const, i32.div_s and i32.rem_s opcodes 2026-03-15 20:04:27 +00:00
74a8f21379 Encode indices as unsigned LEB128 instead of signed 2026-03-15 20:04:06 +00:00
6784cd02b4 Encode section lengths with unsigned LEB128 2026-03-15 20:03:47 +00:00
11 changed files with 2372 additions and 2191 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 `SharedArrayBuffer` 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,36 +61,34 @@ 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
| Name | Address | Size / B | Access |
|--------|---------|----------|--------------|
| TXBUF | 00h | 32 | write |
| RXBUF | 20h | 32 | read |
| TXHEAD | 40h | 4 | atomic read |
| TXTAIL | 44h | 4 | atomic write |
| RXHEAD | 48h | 4 | atomic write |
| RXTAIL | 4Ch | 4 | atomic read |
| TXBUF | 000h | 32 | write |
| RXBUF | 080h | 32 | read |
| TXHEAD | 100h | 4 | atomic read |
| TXTAIL | 104h | 4 | atomic write |
| RXHEAD | 108h | 4 | atomic write |
| RXTAIL | 10Ch | 4 | atomic read |
For both sending (`TX`) and receiving (`RX`), there are three
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
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
must never be completely full -- there must always be *at least one*
@@ -101,7 +98,10 @@ unoccupied byte between the tail and the head.
| 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
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.

163
asm.js
View File

@@ -14,20 +14,29 @@ class Tokenizer {
this.buffer = [];
this.comment = 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() {
const idx = this.buffer.findIndex((cp) => !this.skips.has(cp));
this.buffer = idx == -1 ? [] : this.buffer.slice(idx);
const idx = this.find((cp) => !this.skips.has(cp));
this.cursor = idx == null ? this.buffer.length : idx;
}
next_string() {
const idx = this.buffer.findIndex((cp) => cp == this.string_quote);
if (idx == -1) {
const idx = this.find((cp) => cp == this.string_quote);
if (idx == null) {
this.string = true;
} else {
const string = this.buffer.slice(0, idx).join("");
this.buffer = this.buffer.slice(idx + 1);
const string = this.buffer.slice(this.cursor, idx).join("");
this.cursor = idx + 1;
this.string = false;
return { string: string };
}
@@ -38,18 +47,20 @@ class Tokenizer {
return this.next_string();
this.skip();
if (this.buffer[0] == LINE_END)
return this.buffer.shift();
if (this.buffer[this.cursor] == LINE_END) {
++this.cursor;
return LINE_END;
}
if (this.buffer[0] == this.string_quote) {
this.buffer.shift();
if (this.buffer[this.cursor] == this.string_quote) {
++this.cursor;
return this.next_string();
}
const idx = this.buffer.findIndex((cp) => this.delims.has(cp));
if (idx != -1) {
const token = this.buffer.slice(0, idx).join("");
this.buffer = this.buffer.slice(idx);
const idx = this.find((cp) => this.delims.has(cp));
if (idx != null) {
const token = this.buffer.slice(this.cursor, idx).join("");
this.cursor = idx;
return token;
}
}
@@ -59,12 +70,14 @@ class Tokenizer {
let token;
while (token = this.next()) {
if (token.string == undefined
&& token.startsWith(this.comment_start))
&& token.startsWith(this.comment_start)) {
this.comment = true;
else if (this.comment && token == LINE_END)
} else if (this.comment && token == LINE_END) {
this.comment = false;
else if (!this.comment)
yield token;
} else if (!this.comment) {
yield token;
}
}
}
}
@@ -106,6 +119,7 @@ const State = Object.freeze({
ELEM_TABLE: 33,
ELEM_ELEM: 34,
ELEM_LABEL: 35,
ZERO: 36,
});
const Action = Object.freeze({
@@ -161,6 +175,7 @@ const opcodes = {
"i32.store": 0x36,
"i32.store8": 0x3a,
"i32.const": 0x41,
"i64.const": 0x42,
"i32.eqz": 0x45,
"i32.eq": 0x46,
"i32.ne": 0x47,
@@ -175,6 +190,8 @@ const opcodes = {
"i32.add": 0x6a,
"i32.sub": 0x6b,
"i32.mul": 0x6c,
"i32.div_s": 0x6d,
"i32.rem_s": 0x6f,
"i32.and": 0x71,
"i32.or": 0x72,
"i32.xor": 0x73,
@@ -183,12 +200,12 @@ const opcodes = {
"i32.shr_u": 0x76,
// Threads instructions
"memory.atomic.notify": [ 0xfe, 0x00 ],
"memory.atomic.wait32": [ 0xfe, 0x01 ],
"memory.atomic.load": [ 0xfe, 0x10 ],
"memory.atomic.load8_u": [ 0xfe, 0x12 ],
"memory.atomic.store": [ 0xfe, 0x17 ],
"memory.atomic.store8": [ 0xfe, 0x19 ],
"memory.atomic.notify": [ 0xfe, 0x00 ],
"memory.atomic.wait32": [ 0xfe, 0x01 ],
"i32.atomic.load": [ 0xfe, 0x10 ],
"i32.atomic.load8_u": [ 0xfe, 0x12 ],
"i32.atomic.store": [ 0xfe, 0x17 ],
"i32.atomic.store8": [ 0xfe, 0x19 ],
};
const mem_flags = {
@@ -225,6 +242,7 @@ class Parser {
".type": State.TYPE_NAME,
".table": State.TABLE_NAME,
".elem": State.ELEM_TABLE,
".zero": State.ZERO,
};
this.blocks = new Set(["block", "loop", "if"]);
this.handlers = {
@@ -264,6 +282,7 @@ class Parser {
[State.ELEM_TABLE]: (token) => this.token_elem_table(token),
[State.ELEM_ELEM]: (token) => this.token_elem_elem(token),
[State.ELEM_LABEL]: (token) => this.token_elem_label(token),
[State.ZERO]: (token) => this.token_zero(token),
};
this.results = [];
@@ -272,15 +291,14 @@ class Parser {
}
integer(token) {
let base;
let base, regex;
switch (token.slice(-1)) {
case "b": base = 2; break;
case "o": base = 8; break;
case "h": base = 16; break;
default: base = 10; break;
case "b": base = 2; regex = /^-?[01]+b$/; break;
case "o": base = 8; regex = /^-?[0-7]+o$/; break;
case "h": base = 16; regex = /^-?[0-9A-F]+h$/; break;
default: base = 10; regex = /^-?[0-9]+d?$/; break;
}
const x = parseInt(token, base);
return Number.isNaN(x) ? null : x;
return regex.test(token) ? parseInt(token, base) : null;
}
translate_code(token) {
@@ -547,15 +565,10 @@ class Parser {
token_at_addr(token) {
const value = this.integer(token);
if (value == null) {
console.error(
`ERROR: Unexpected token ${token} in .mem: `
+ "expected address");
this.at = undefined;
return;
}
this.at.addr = value;
if (value != null)
this.at.addr = value;
else
this.at.addr_symbol = token;
const action = { type: Action.AT, at: this.at };
this.at = undefined;
this.state = State.TOP;
@@ -823,6 +836,26 @@ class Parser {
return action;
}
token_zero(token) {
if (token == LINE_END) {
console.error(
"ERROR: Unexpected newline in .zero, expected count")
this.state = State.TOP;
return;
}
const count = this.integer(token);
if (count == null) {
console.error(
`ERROR: Unexpected token ${token} in .zero, expected count`);
this.state = State.TOP;
return;
}
this.state = State.TOP;
return { type: Action.DATA, size: count, value: 0 }
}
mem_action() {
const action = {
type: Action.MEM,
@@ -909,6 +942,7 @@ export class Assembler {
this.types = [];
this.type_bindings = {};
this.tables = {};
this.unresolved = [];
}
action_append(action) {
@@ -963,7 +997,7 @@ export class Assembler {
}
func.body.push(...this.leb128(def_value));
} else {
func.body.push(...this.leb128(value));
func.body.push(...this.uleb128(value));
}
}
@@ -995,7 +1029,8 @@ export class Assembler {
return;
}
this.pos.mem = mem;
this.pos.addr = action.at.addr;
this.pos.addr = action.at.addr
?? this.lookup_def(action.at.addr_symbol);
this.data.push({ loc: { ...this.pos }, data: [] })
}
@@ -1013,9 +1048,14 @@ export class Assembler {
}
value = this.lookup_def(action.symbol);
if (value == undefined) {
console.error(
`ERROR: Unable to resolve symbol ${action.symbol}`);
return;
this.unresolved.push({
type: "data",
size: action.size,
symbol: action.symbol,
target: data,
offset: data.length,
});
value = 0;
}
}
bytes = this.le(value, action.size);
@@ -1303,7 +1343,7 @@ export class Assembler {
opcodes["i32.const"],
...this.leb128(loc.addr),
opcodes["end"],
data.length,
...this.uleb128(data.length),
...data,
]
});
@@ -1311,6 +1351,7 @@ export class Assembler {
}
wasm() {
this.resolve_refs();
this.resolve_func_types();
const template = [
@@ -1327,7 +1368,9 @@ export class Assembler {
];
const sections = template.map(([ code, generator ]) => {
const body = generator();
return body == null ? [] : [ code, body.length, body ];
if (body == null)
return [];
return [ code, this.uleb128(body.length), body ];
});
return new Uint8Array([ HEADER, sections ].flat(Infinity));
@@ -1377,4 +1420,32 @@ export class Assembler {
return acc;
}, []);
}
resolve_refs() {
const failed = [];
for (const ref of this.unresolved) {
if (ref.type != "data") {
console.error(
`ERROR: Unsupported ref type ${ref.type} for `
+ `symbol ${ref.symbol}`
);
failed.push(ref.symbol);
continue;
}
const value = this.defs[ref.symbol];
if (value == undefined) {
failed.push(ref.symbol);
continue;
}
const bytes = this.le(value, ref.size);
ref.target.splice(ref.offset, ref.size, ...bytes);
}
if (failed.length != 0) {
const failed_str = failed.join(" ");
console.error(`ERROR: Unable to resolve refs: ${failed_str}`);
}
}
}

29
boot.js
View File

@@ -1,7 +1,26 @@
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) => {
const exports = { emu: { mem: e.data } };
const mod = await WebAssembly.instantiateStreaming(
fetch('wipforth.wasm'), exports)
mod.instance.exports.reset();
console.log('System halt');
switch (e.data.type) {
case "load":
const exports = { emu: { mem: e.data.mem } };
const wasm = await assemble;
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,6 +1,8 @@
asm.js
boot.js
emu.js
favicon.png
index.html
prelude.f
styles.css
wipforth.wasm
wipforth.ws

72
emu.js
View File

@@ -1,16 +1,17 @@
const TXBUF = 0x00;
const RXBUF = 0x20;
const TXHEAD = 0x40;
const TXTAIL = 0x44;
const RXHEAD = 0x48;
const RXTAIL = 0x4c;
const SYSREADY = 0x50;
const TXBUF = 0x000;
const RXBUF = 0x080;
const TXHEAD = 0x100;
const TXTAIL = 0x104;
const RXHEAD = 0x108;
const RXTAIL = 0x10c;
const TXBUF_SIZE = 32;
const RXBUF_SIZE = 32;
const PERIPHS_SIZE = 81;
const SYSREADY = 0x110;
const SYSINTER = 0x114;
const POLL_INTERVAL_MS = 20;
const PERIPHS_SIZE = 0x200;
const POLL_INTERVAL_MS = 5;
const DOT_INTERVAL_MS = 25;
const COLS = 80;
const TAB_WIDTH = 8;
@@ -50,8 +51,19 @@ class Emulator {
document.addEventListener('keydown', (e) => this.handle_keydown(e));
window.addEventListener('resize', () => this.handle_resize());
this.worker = new Worker('boot.js');
this.worker.postMessage(this.mem);
this.forth = new Worker('boot.js', { type: 'module' });
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')
.then(res => res.text())
@@ -77,15 +89,21 @@ class Emulator {
if (!this.input_enable) {
const sysready = Atomics.load(this.mem_u8, SYSREADY);
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.blink = true;
this.flush_output();
document.getElementById('cursor').classList.add('blinking');
}
}
}
fifo_next(idx) {
return (idx + 1) & 0x1f;
return (idx + 1) & 0x7f;
}
handle_tx_data(head, tail) {
@@ -173,12 +191,14 @@ class Emulator {
}
e.preventDefault();
this.blink = false;
this.flush_output();
document.getElementById('cursor').classList.remove('blinking');
if (this.idle_timer)
clearTimeout(this.idle_timer);
this.idle_timer = setTimeout(() => {
this.blink = true;
document.getElementById('cursor').classList.add('blinking');
}, CURSOR_IDLE_TIME_MS);
}
@@ -265,10 +285,12 @@ class Emulator {
return row.map((c, x) => {
const ec = this.html_escape(c);
if (this.input_enable
&& x == this.cursor.x && y == this.cursor.y)
return '<span id="cursor">' + ec + '</span>';
else
&& x == this.cursor.x && y == this.cursor.y) {
const cl = this.blink ? 'class="blinking"' : '';
return `<span id="cursor" ${cl}>` + ec + '</span>';
} else {
return ec;
}
}).join('').trimEnd();
}).join('\n');
this.output.innerHTML = html;
@@ -325,6 +347,16 @@ class Emulator {
}
window.addEventListener('DOMContentLoaded', () => {
document.getElementById('output').innerText = '';
window.emu = new Emulator();
const output = document.getElementById('output');
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>
<body>
<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>
</body>
cringe, I know... but I promise there are no frameworks&#8482;</noscript></div></body>
</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 :)
\ We'll periodically sprinkle these in so that it's clear to the user
\ that things are happening.
46 EMIT
\ Conditionals
: IF
@@ -27,8 +20,6 @@
SWAP !
; IMMEDIATE
46 EMIT
\ Loops
: BEGIN HERE @ ; IMMEDIATE
@@ -43,8 +34,6 @@
HERE @ - ,
; IMMEDIATE
46 EMIT
\ Recursive calls
: RECURSE LATEST @ >CFA , ; IMMEDIATE
@@ -61,8 +50,6 @@
( ( Take that, C ) )
46 EMIT
\ Printing utilities
: CR 10 EMIT ;
@@ -80,8 +67,6 @@
+ EMIT
;
CHAR . EMIT
: .
\ Handle negatives
DUP 0< IF CHAR - EMIT NEGATE THEN
@@ -110,8 +95,6 @@ CHAR . EMIT
2DROP
;
CHAR . EMIT
: TYPE ( addr len -- )
BEGIN
DUP 0= IF 2DROP EXIT THEN
@@ -120,8 +103,6 @@ CHAR . EMIT
AGAIN
;
CHAR . EMIT
: C, HERE @ C! 1 HERE +! ;
: ."
@@ -151,8 +132,6 @@ CHAR . EMIT
THEN
; IMMEDIATE
CHAR . EMIT
\ Misc utilities
: NIP SWAP DROP ;
@@ -171,8 +150,6 @@ CHAR . EMIT
: [COMPILE] ' , ; IMMEDIATE
CHAR . EMIT
\ Constants, variables and values
: CONSTANT
@@ -204,24 +181,21 @@ CHAR . EMIT
THEN
; IMMEDIATE
CHAR . EMIT
\ Peripheral register addresses
HEX
00 CONSTANT TXBUF
20 CONSTANT RXBUF
40 CONSTANT TXHEAD
44 CONSTANT TXTAIL
48 CONSTANT RXHEAD
4C CONSTANT RXTAIL
50 CONSTANT SYSREADY
000 CONSTANT TXBUF
080 CONSTANT RXBUF
100 CONSTANT TXHEAD
104 CONSTANT TXTAIL
108 CONSTANT RXHEAD
10C CONSTANT RXTAIL
110 CONSTANT SYSREADY
114 CONSTANT SYSINTER
DECIMAL
46 EMIT
\ A better word-not-found handler
: ANY-RX? RXHEAD AC@ RXTAIL AC@ <> ;
@@ -244,13 +218,11 @@ DECIMAL
' WNF-HANDLER TO WNFHOOK
CHAR . EMIT
\ Version number
0 CONSTANT VERSION-MAJOR
1 CONSTANT VERSION-MINOR
0 CONSTANT VERSION-PATCH
2 CONSTANT VERSION-MINOR
2 CONSTANT VERSION-PATCH
: PRINT-VERSION
CHAR v EMIT VERSION-MAJOR .
@@ -258,8 +230,6 @@ CHAR . EMIT
CHAR . EMIT VERSION-PATCH .
;
CHAR . EMIT
\ Welcome banner
: BANNER
@@ -269,7 +239,7 @@ CHAR . EMIT
." |__,__/_/ .__(_)___/_//_/" CR
." /_/ " CR
CR
." Wipforth " PRINT-VERSION CR
." Welcome to Wipforth " PRINT-VERSION ." !" CR
." Copyright (c) Camden Dixie O'Brien" CR
CR
." Wipforth is freely available to use, modify and distribute for personal use" CR
@@ -277,8 +247,8 @@ CHAR . EMIT
CR
;
." done" CR
BANNER
\ Set SYSREADY high to enable user input
\ Set SYSREADY high and wait until interactive
1 SYSREADY AC!
SYSINTER WAIT DROP
BANNER

View File

@@ -16,9 +16,8 @@
'(("html" . (text/html))
("css" . (text/css))
("js" . (application/javascript))
("wasm" . (application/wasm))
("f" . (text/plain))
("wat" . (text/plain))
("ws" . (text/plain))
("png" . (image/png))))
(define (mime-type path)

View File

@@ -19,13 +19,19 @@
(define client (client-setup))
(navigate client "http://localhost:8080")
(sleep 5)
(sleep 1)
(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
(let* ((display (get-display client))
(first-line (first (lines display))))
(assert (string-match "Loading prelude \\.+ done" first-line)
(format #f "Prelude load line: ~s" first-line))))
(line (second (lines display))))
(assert (string-match "Loading prelude \\.+ done" line)
(format #f "Prelude load line: ~s" line))))
(define-test six-seven-times-dot-cr-yields-42
(input-line client "6 7 * . CR")

File diff suppressed because it is too large Load Diff

2106
wipforth.ws Normal file

File diff suppressed because it is too large Load Diff