Compare commits
57 Commits
347dd8f534
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
aa9c18346d
|
|||
|
a2f06c977e
|
|||
|
58e2cf3e1f
|
|||
|
efdae93d90
|
|||
|
804626ddad
|
|||
|
cbe5733fcd
|
|||
|
7961c68639
|
|||
|
d202157a58
|
|||
|
97fc43bf93
|
|||
|
2c13ad4e1f
|
|||
|
36429bf8bc
|
|||
|
c20e7e181b
|
|||
|
fe5c55cabf
|
|||
|
67fc1d8d7b
|
|||
|
4000522b3a
|
|||
|
19ef69958d
|
|||
|
0a52388030
|
|||
|
6e8439eeaf
|
|||
|
eaa3242cc0
|
|||
|
f77adffbef
|
|||
|
c91f46be88
|
|||
|
6ee4adfea5
|
|||
|
5dc0a7a601
|
|||
|
896a1ca563
|
|||
|
37d56988ef
|
|||
|
6c643f8402
|
|||
|
7828b0f112
|
|||
|
e7affbf8b7
|
|||
|
02ee4c3c88
|
|||
|
c21b3c79c7
|
|||
|
1318c3cc4e
|
|||
|
74a8f21379
|
|||
|
6784cd02b4
|
|||
|
3a103c46d1
|
|||
|
8d4c53ca92
|
|||
|
5e39024f6d
|
|||
|
b85a4e8bc9
|
|||
|
401e8e1fad
|
|||
|
d4c837216a
|
|||
|
c93e9009da
|
|||
|
0056610238
|
|||
|
9b4ff3e8f6
|
|||
|
e9beacba3a
|
|||
|
acf5b6e284
|
|||
|
72c5f64312
|
|||
|
7135eeba74
|
|||
|
7099ca34a3
|
|||
|
3ebb74c73c
|
|||
|
0dd2a925d8
|
|||
|
2155d17731
|
|||
|
1452ffe615
|
|||
|
46a571be93
|
|||
|
d35b13fed0
|
|||
|
a3cfd405a9
|
|||
|
671e7f60d2
|
|||
|
580d5d2a4a
|
|||
|
1105daaad0
|
84
README.md
84
README.md
@@ -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.
|
||||
|
||||
692
asm.js
692
asm.js
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -58,12 +69,15 @@ class Tokenizer {
|
||||
this.buffer.push(...src);
|
||||
let token;
|
||||
while (token = this.next()) {
|
||||
if (token == this.comment_start)
|
||||
if (token.string == undefined
|
||||
&& 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -97,6 +111,15 @@ const State = Object.freeze({
|
||||
DEF_VALUE: 25,
|
||||
BLOCK_NAME: 26,
|
||||
BLOCK_TYPE: 27,
|
||||
TYPE_NAME: 28,
|
||||
TYPE_PARAM: 29,
|
||||
TYPE_RESULT: 30,
|
||||
TABLE_NAME: 31,
|
||||
TABLE_SIZE: 32,
|
||||
ELEM_TABLE: 33,
|
||||
ELEM_ELEM: 34,
|
||||
ELEM_LABEL: 35,
|
||||
ZERO: 36,
|
||||
});
|
||||
|
||||
const Action = Object.freeze({
|
||||
@@ -118,30 +141,71 @@ const Action = Object.freeze({
|
||||
ENTER: 16,
|
||||
EXIT: 17,
|
||||
ELSE: 18,
|
||||
TYPE: 19,
|
||||
TABLE: 20,
|
||||
ELEM: 21,
|
||||
});
|
||||
|
||||
const types = {
|
||||
"void": 0x40,
|
||||
"func": 0x60,
|
||||
"i32": 0x7f,
|
||||
"void": 0x40,
|
||||
"func": 0x60,
|
||||
"funcref": 0x70,
|
||||
"f32": 0x7d,
|
||||
"i32": 0x7f,
|
||||
};
|
||||
|
||||
const opcodes = {
|
||||
"block": 0x02,
|
||||
"loop": 0x03,
|
||||
"if": 0x04,
|
||||
"else": 0x05,
|
||||
"end": 0x0b,
|
||||
"br": 0x0c,
|
||||
"br_if": 0x0d,
|
||||
"local.get": 0x20,
|
||||
"local.set": 0x21,
|
||||
"local.tee": 0x22,
|
||||
"global.get": 0x23,
|
||||
"global.set": 0x24,
|
||||
"i32.const": 0x41,
|
||||
"i32.gt_u": 0x4b,
|
||||
"i32.mul": 0x6c,
|
||||
"block": 0x02,
|
||||
"loop": 0x03,
|
||||
"if": 0x04,
|
||||
"else": 0x05,
|
||||
"end": 0x0b,
|
||||
"br": 0x0c,
|
||||
"br_if": 0x0d,
|
||||
"call": 0x10,
|
||||
"call_indirect": 0x11,
|
||||
"drop": 0x0a,
|
||||
"local.get": 0x20,
|
||||
"local.set": 0x21,
|
||||
"local.tee": 0x22,
|
||||
"global.get": 0x23,
|
||||
"global.set": 0x24,
|
||||
"i32.load": 0x28,
|
||||
"i32.load8_u": 0x2d,
|
||||
"i32.store": 0x36,
|
||||
"i32.store8": 0x3a,
|
||||
"i32.const": 0x41,
|
||||
"i64.const": 0x42,
|
||||
"i32.eqz": 0x45,
|
||||
"i32.eq": 0x46,
|
||||
"i32.ne": 0x47,
|
||||
"i32.lt_s": 0x48,
|
||||
"i32.lt_u": 0x49,
|
||||
"i32.gt_s": 0x4a,
|
||||
"i32.gt_u": 0x4b,
|
||||
"i32.le_s": 0x4c,
|
||||
"i32.le_u": 0x4d,
|
||||
"i32.ge_s": 0x4e,
|
||||
"i32.ge_u": 0x4f,
|
||||
"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,
|
||||
"i32.shl": 0x74,
|
||||
"i32.shr_s": 0x75,
|
||||
"i32.shr_u": 0x76,
|
||||
|
||||
// Threads instructions
|
||||
"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 = {
|
||||
@@ -175,6 +239,10 @@ class Parser {
|
||||
".utf8": State.UTF8,
|
||||
".align": State.ALIGN,
|
||||
".def": State.DEF_NAME,
|
||||
".type": State.TYPE_NAME,
|
||||
".table": State.TABLE_NAME,
|
||||
".elem": State.ELEM_TABLE,
|
||||
".zero": State.ZERO,
|
||||
};
|
||||
this.blocks = new Set(["block", "loop", "if"]);
|
||||
this.handlers = {
|
||||
@@ -206,6 +274,15 @@ class Parser {
|
||||
[State.DEF_VALUE]: (token) => this.token_def_value(token),
|
||||
[State.BLOCK_NAME]: (token) => this.token_block_name(token),
|
||||
[State.BLOCK_TYPE]: (token) => this.token_block_type(token),
|
||||
[State.TYPE_NAME]: (token) => this.token_type_name(token),
|
||||
[State.TYPE_PARAM]: (token) => this.token_type_param(token),
|
||||
[State.TYPE_RESULT]: (token) => this.token_type_result(token),
|
||||
[State.TABLE_NAME]: (token) => this.token_table_name(token),
|
||||
[State.TABLE_SIZE]: (token) => this.token_table_size(token),
|
||||
[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 = [];
|
||||
@@ -214,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) {
|
||||
@@ -260,7 +336,7 @@ class Parser {
|
||||
if (opcode)
|
||||
return { type: Action.APPEND, opcode };
|
||||
const literal = this.integer(token);
|
||||
if (literal)
|
||||
if (literal != null)
|
||||
return { type: Action.APPEND, literal };
|
||||
|
||||
return { type: Action.SYMBOL, symbol: token };
|
||||
@@ -465,27 +541,21 @@ class Parser {
|
||||
|
||||
token_global_init(token) {
|
||||
if (token == LINE_END) {
|
||||
console.error(
|
||||
"ERROR: Unexpected newline in .global: expected"
|
||||
+ " initial value");
|
||||
this.global = undefined;
|
||||
this.global_name = undefined;
|
||||
this.state = State.TOP;
|
||||
this.global.init = 0;
|
||||
} else {
|
||||
const value = this.integer(token) ?? console.error(
|
||||
`ERROR: Unexpected token ${token} in .global: expected`
|
||||
+ " initial value");
|
||||
const const_opcode = const_opcodes[this.global.type];
|
||||
this.global.init = [ const_opcode, value, opcodes["end"] ];
|
||||
const action = {
|
||||
type: Action.GLOBAL,
|
||||
global: { [this.global_name]: this.global }
|
||||
};
|
||||
this.global = undefined;
|
||||
this.global_name = undefined;
|
||||
this.state = State.TOP;
|
||||
return action;
|
||||
}
|
||||
this.global.init = value;
|
||||
}
|
||||
const action = {
|
||||
type: Action.GLOBAL,
|
||||
global: { [this.global_name]: this.global }
|
||||
};
|
||||
this.global = undefined;
|
||||
this.global_name = undefined;
|
||||
this.state = State.TOP;
|
||||
return action;
|
||||
}
|
||||
|
||||
token_at_mem(token) {
|
||||
@@ -495,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;
|
||||
@@ -522,7 +587,7 @@ class Parser {
|
||||
} else {
|
||||
if (value > 0xff)
|
||||
console.error(`WARNING: Value ${token} is truncated`);
|
||||
action.value = [ value & 0xff ];
|
||||
action.value = value;
|
||||
}
|
||||
return action;
|
||||
}
|
||||
@@ -537,9 +602,9 @@ class Parser {
|
||||
if (value == null) {
|
||||
action.symbol = token;
|
||||
} else {
|
||||
if (value > 0xffff)
|
||||
if (value > 0xffffffff)
|
||||
console.error(`WARNING: Value ${token} is truncated`);
|
||||
action.value = [ value & 0xff, (value >> 8) & 0xff ];
|
||||
action.value = value;
|
||||
}
|
||||
return action;
|
||||
}
|
||||
@@ -553,8 +618,8 @@ class Parser {
|
||||
`ERROR: Unexpected token ${token}, expected string`);
|
||||
return;
|
||||
}
|
||||
const value = this.encoder.encode(token.string);
|
||||
const action = { type: Action.DATA, size: value.length, value };
|
||||
const bytes = this.encoder.encode(token.string);
|
||||
const action = { type: Action.DATA, size: bytes.length, bytes };
|
||||
return action;
|
||||
}
|
||||
|
||||
@@ -636,6 +701,161 @@ class Parser {
|
||||
return action;
|
||||
}
|
||||
|
||||
token_type_name(token) {
|
||||
if (token == LINE_END) {
|
||||
console.error(
|
||||
"ERROR: Unexpected end of line in .type, expected name");
|
||||
this.state = State.TOP;
|
||||
return;
|
||||
}
|
||||
|
||||
this.type = { name: token, params: [] };
|
||||
this.state = State.TYPE_PARAM;
|
||||
}
|
||||
|
||||
token_type_param(token) {
|
||||
if (token == LINE_END) {
|
||||
console.error(
|
||||
"ERROR: Unexpected end of line in .type, expected "
|
||||
+ "parameter type");
|
||||
this.type = undefined;
|
||||
this.state = State.TOP;
|
||||
return;
|
||||
}
|
||||
|
||||
if (token == "result") {
|
||||
this.type.results = [];
|
||||
this.state = State.TYPE_RESULT;
|
||||
return;
|
||||
}
|
||||
|
||||
const type = types[token];
|
||||
if (type == undefined) {
|
||||
console.error(
|
||||
`ERROR: Unexpected token ${token} in .type, expected `
|
||||
+ "parameter type");
|
||||
this.type = undefined;
|
||||
this.state = State.TOP;
|
||||
return;
|
||||
}
|
||||
|
||||
this.type.params.push(type);
|
||||
}
|
||||
|
||||
token_type_result(token) {
|
||||
if (token == LINE_END) {
|
||||
const action = { type: Action.TYPE, the_type: this.type };
|
||||
this.type = undefined;
|
||||
this.state = State.TOP;
|
||||
return action;
|
||||
}
|
||||
|
||||
const type = types[token];
|
||||
if (type == undefined) {
|
||||
console.error(
|
||||
`ERROR: Unexpected token ${token} in .type, expected `
|
||||
+ "result type");
|
||||
this.type = undefined;
|
||||
this.state = State.TOP;
|
||||
return;
|
||||
}
|
||||
|
||||
this.type.results.push(type);
|
||||
}
|
||||
|
||||
token_table_name(token) {
|
||||
if (token == LINE_END) {
|
||||
console.error(
|
||||
"ERROR: Unexpected end of line in .table, expected name");
|
||||
this.state = State.TOP;
|
||||
return;
|
||||
}
|
||||
|
||||
this.table = { name: token };
|
||||
this.state = State.TABLE_SIZE;
|
||||
}
|
||||
|
||||
token_table_size(token) {
|
||||
if (token == LINE_END) {
|
||||
console.error(
|
||||
"ERROR: Unexpected end of line in .table, expected size");
|
||||
this.table = undefined;
|
||||
this.state = State.TOP;
|
||||
return;
|
||||
}
|
||||
|
||||
const size = this.integer(token);
|
||||
if (size == null) {
|
||||
console.error(
|
||||
`ERROR: Unexpected token ${token} in .table, expected size`);
|
||||
this.table = undefined;
|
||||
this.state = State.TOP;
|
||||
return;
|
||||
}
|
||||
|
||||
this.table.size = size;
|
||||
const action = { type: Action.TABLE, table: this.table };
|
||||
this.table = undefined;
|
||||
this.state = State.TOP;
|
||||
return action;
|
||||
}
|
||||
|
||||
token_elem_table(token) {
|
||||
if (token == LINE_END) {
|
||||
console.error(
|
||||
"ERROR: Unexpected end of line in .elem, expected "
|
||||
+ "table name");
|
||||
this.state = State.TOP;
|
||||
return;
|
||||
}
|
||||
|
||||
this.elem = { table: token };
|
||||
this.state = State.ELEM_ELEM;
|
||||
}
|
||||
|
||||
token_elem_elem(token) {
|
||||
if (token == LINE_END) {
|
||||
console.error(
|
||||
"ERROR: Unexpected end of line in .elem, expected "
|
||||
+ "element name");
|
||||
this.state = State.TOP;
|
||||
this.elem = undefined;
|
||||
return;
|
||||
}
|
||||
|
||||
this.elem.elem = token;
|
||||
this.state = State.ELEM_LABEL;
|
||||
}
|
||||
|
||||
token_elem_label(token) {
|
||||
if (token != LINE_END)
|
||||
this.elem.label = token;
|
||||
const action = { type: Action.ELEM, elem: this.elem };
|
||||
this.elem = undefined
|
||||
this.state = State.TOP;
|
||||
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,
|
||||
@@ -650,9 +870,15 @@ class Parser {
|
||||
*handle(src) {
|
||||
let action;
|
||||
for (const token of this.tokenizer.handle(src)) {
|
||||
if (action = this.handlers[this.state](token)) {
|
||||
yield action;
|
||||
const handler = this.handlers[this.state];
|
||||
if (handler == undefined) {
|
||||
console.error(`ERROR: Unhandled state ${this.state}`);
|
||||
this.state = State.TOP;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (action = handler(token))
|
||||
yield action;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -661,9 +887,11 @@ const Section = Object.freeze({
|
||||
TYPE: 0x01,
|
||||
IMPORT: 0x02,
|
||||
FUNC: 0x03,
|
||||
TABLE: 0x04,
|
||||
MEM: 0x05,
|
||||
GLOBAL: 0x06,
|
||||
EXPORT: 0x07,
|
||||
ELEM: 0x09,
|
||||
CODE: 0x0a,
|
||||
DATA: 0x0b,
|
||||
});
|
||||
@@ -697,6 +925,9 @@ export class Assembler {
|
||||
[Action.ENTER]: (action) => this.action_enter(action),
|
||||
[Action.EXIT]: (action) => this.action_exit(action),
|
||||
[Action.ELSE]: (action) => this.action_else(action),
|
||||
[Action.TYPE]: (action) => this.action_type(action),
|
||||
[Action.TABLE]: (action) => this.action_table(action),
|
||||
[Action.ELEM]: (action) => this.action_elem(action),
|
||||
};
|
||||
|
||||
this.exports = [];
|
||||
@@ -708,11 +939,15 @@ export class Assembler {
|
||||
this.data = [];
|
||||
this.defs = {};
|
||||
this.blocks = [];
|
||||
this.types = [];
|
||||
this.type_bindings = {};
|
||||
this.tables = {};
|
||||
this.unresolved = [];
|
||||
}
|
||||
|
||||
action_append(action) {
|
||||
const code = action.opcode != undefined
|
||||
? [ action.opcode ]
|
||||
? [ action.opcode ].flat()
|
||||
: this.leb128(action.literal);
|
||||
this.funcs[this.current_func].body.push(...code);
|
||||
}
|
||||
@@ -750,6 +985,9 @@ export class Assembler {
|
||||
?? this.lookup_param(func, action.symbol)
|
||||
?? this.lookup_local(func, action.symbol)
|
||||
?? this.lookup_global(action.symbol)
|
||||
?? this.lookup_table(action.symbol)
|
||||
?? this.lookup_type(action.symbol)
|
||||
?? this.lookup_func(action.symbol);
|
||||
if (value == null) {
|
||||
const def_value = this.lookup_def(action.symbol);
|
||||
if (def_value == null) {
|
||||
@@ -759,7 +997,7 @@ export class Assembler {
|
||||
}
|
||||
func.body.push(...this.leb128(def_value));
|
||||
} else {
|
||||
func.body.push(value);
|
||||
func.body.push(...this.uleb128(value));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -791,16 +1029,38 @@ 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: [] })
|
||||
}
|
||||
|
||||
action_data(action) {
|
||||
const data = this.data.at(-1).data;
|
||||
const value = action.value != null
|
||||
? action.value
|
||||
: this.le_bytes(this.lookup_def(action.symbol), action.size);
|
||||
data.push(...value);
|
||||
let bytes;
|
||||
if (action.bytes != undefined) {
|
||||
bytes = action.bytes;
|
||||
} else {
|
||||
let value = action.value;
|
||||
if (value == undefined) {
|
||||
if (action.symbol == undefined) {
|
||||
console.error("ERROR: Invalid data action", action);
|
||||
return;
|
||||
}
|
||||
value = this.lookup_def(action.symbol);
|
||||
if (value == undefined) {
|
||||
this.unresolved.push({
|
||||
type: "data",
|
||||
size: action.size,
|
||||
symbol: action.symbol,
|
||||
target: data,
|
||||
offset: data.length,
|
||||
});
|
||||
value = 0;
|
||||
}
|
||||
}
|
||||
bytes = this.le(value, action.size);
|
||||
}
|
||||
data.push(...bytes);
|
||||
this.pos.addr += action.size;
|
||||
}
|
||||
|
||||
@@ -840,10 +1100,40 @@ export class Assembler {
|
||||
this.blocks.push(undefined);
|
||||
}
|
||||
|
||||
action_type(action) {
|
||||
const type = this.func_type(action.the_type);
|
||||
const index = this.ensure_type(type);
|
||||
this.type_bindings[action.the_type.name] = index;
|
||||
}
|
||||
|
||||
action_table(action) {
|
||||
this.tables[action.table.name] = {
|
||||
size: action.table.size,
|
||||
elems: [],
|
||||
};
|
||||
}
|
||||
|
||||
action_elem(action) {
|
||||
const table = this.tables[action.elem.table];
|
||||
const fn = Object.keys(this.funcs).indexOf(action.elem.elem);
|
||||
if (fn == -1) {
|
||||
console.error(`ERROR: ${action.elem.elem}: no such function`);
|
||||
return;
|
||||
}
|
||||
const index = table.elems.push(fn) - 1;
|
||||
if (action.elem.label)
|
||||
this.defs[action.elem.label] = index;
|
||||
}
|
||||
|
||||
push(chunk) {
|
||||
const text = this.decoder.decode(chunk, { stream: true });
|
||||
for (const action of this.parser.handle(text))
|
||||
this.handlers[action.type](action);
|
||||
for (const action of this.parser.handle(text)) {
|
||||
const handler = this.handlers[action.type];
|
||||
if (handler == undefined)
|
||||
console.error("ERROR: Unhandled action", action);
|
||||
else
|
||||
handler(action);
|
||||
}
|
||||
}
|
||||
|
||||
lookup_block(symbol) {
|
||||
@@ -867,35 +1157,67 @@ export class Assembler {
|
||||
return index == -1 ? null : index;
|
||||
}
|
||||
|
||||
lookup_table(symbol) {
|
||||
const index = Object.keys(this.tables).indexOf(symbol);
|
||||
return index == -1 ? null : index;
|
||||
}
|
||||
|
||||
lookup_type(symbol) {
|
||||
return this.type_bindings[symbol];
|
||||
}
|
||||
|
||||
lookup_func(symbol) {
|
||||
const index = Object.keys(this.funcs).indexOf(symbol);
|
||||
return index == -1 ? null : index;
|
||||
}
|
||||
|
||||
lookup_def(symbol) {
|
||||
return this.defs[symbol];
|
||||
}
|
||||
|
||||
le_bytes(value, count) {
|
||||
let bytes = []
|
||||
while (value != 0 && bytes.length < count) {
|
||||
le(value, count) {
|
||||
const bytes = []
|
||||
while (value != 0) {
|
||||
bytes.push(value & 0xff);
|
||||
value >>= 8;
|
||||
value >>>= 8;
|
||||
}
|
||||
if (count != undefined) {
|
||||
while (bytes.length < count)
|
||||
bytes.push(0);
|
||||
}
|
||||
return bytes;
|
||||
}
|
||||
|
||||
leb128(x) {
|
||||
const bytes = [];
|
||||
while (true) {
|
||||
const b = x & 0x7f;
|
||||
x >>= 7;
|
||||
if (x == 0 && (b & 0x40) == 0 || x == -1 && (b & 0x40) != 0) {
|
||||
bytes.push(b);
|
||||
return bytes;
|
||||
}
|
||||
bytes.push(b | 0x80);
|
||||
}
|
||||
}
|
||||
|
||||
uleb128(x) {
|
||||
const bytes = [];
|
||||
while (true) {
|
||||
const b = x & 0x7f;
|
||||
x >>>= 7;
|
||||
if (x == 0) {
|
||||
bytes.push(b);
|
||||
return bytes;
|
||||
}
|
||||
bytes.push(b | 0x80);
|
||||
}
|
||||
while (bytes.length < count)
|
||||
bytes.push(0);
|
||||
return bytes;
|
||||
}
|
||||
|
||||
wasm_section_type() {
|
||||
const funcs = Object.values(this.funcs);
|
||||
if (funcs.length == 0) return null;
|
||||
const contents = funcs.map(({ params, results }) => {
|
||||
const param_types = Object.values(params);
|
||||
return [
|
||||
types["func"],
|
||||
param_types.length,
|
||||
...param_types,
|
||||
results.length,
|
||||
...results,
|
||||
];
|
||||
});
|
||||
return [ contents.length ].concat(...contents);
|
||||
if (this.types.length == 0) return null;
|
||||
return [ this.types.length ].concat(...this.types);
|
||||
}
|
||||
|
||||
wasm_section_import() {
|
||||
@@ -917,9 +1239,21 @@ export class Assembler {
|
||||
}
|
||||
|
||||
wasm_section_func() {
|
||||
const func_count = Object.entries(this.funcs).length;
|
||||
if (func_count == 0) return null;
|
||||
return [ func_count, ...Array(func_count).keys() ];
|
||||
const types = Object.values(this.funcs).map(({type}) => type);
|
||||
const count = types.length;
|
||||
if (count == 0) return null;
|
||||
return [ count, ...types ];
|
||||
}
|
||||
|
||||
wasm_section_table() {
|
||||
const sizes = Object.values(this.tables).map(({size}) => size);
|
||||
if (sizes.length == 0) return null;
|
||||
const contents = sizes.map((size) => [
|
||||
types.funcref,
|
||||
0x00,
|
||||
this.uleb128(size),
|
||||
]);
|
||||
return [ sizes.length, contents ].flat(Infinity)
|
||||
}
|
||||
|
||||
wasm_section_mem() {
|
||||
@@ -935,8 +1269,13 @@ export class Assembler {
|
||||
const globals = Object.values(this.globals);
|
||||
if (globals.length == 0)
|
||||
return null;
|
||||
const contents = globals.map(
|
||||
({ type, init }) => [ type, 1, ...init ]);
|
||||
const contents = globals.map(({ type, init }) => [
|
||||
type,
|
||||
1,
|
||||
const_opcodes[type],
|
||||
...this.leb128(init),
|
||||
opcodes["end"],
|
||||
]);
|
||||
return [ globals.length ].concat(...contents);
|
||||
}
|
||||
|
||||
@@ -955,6 +1294,24 @@ export class Assembler {
|
||||
return [ exports.length ].concat(...contents);
|
||||
}
|
||||
|
||||
wasm_section_elem() {
|
||||
const table_elems = Object.values(this.tables).map(
|
||||
({elems}) => elems);
|
||||
const count = table_elems.flat().length;
|
||||
if (count == 0) return null;
|
||||
const contents = table_elems.flatMap((elems, table) =>
|
||||
elems.flatMap((fn, index) => [
|
||||
table == 0 ? 0 : [ 2, table ],
|
||||
opcodes["i32.const"],
|
||||
index,
|
||||
opcodes["end"],
|
||||
1,
|
||||
fn,
|
||||
])
|
||||
);
|
||||
return [ count, contents ].flat();
|
||||
}
|
||||
|
||||
wasm_section_code() {
|
||||
const funcs = Object.values(this.funcs);
|
||||
if (funcs.length == 0) return null;
|
||||
@@ -962,24 +1319,20 @@ export class Assembler {
|
||||
const local_types = Object.values(locals);
|
||||
const local_count = local_types.length;
|
||||
if (local_count == 0) {
|
||||
return [
|
||||
body.length + 2,
|
||||
0,
|
||||
...body,
|
||||
opcodes["end"]
|
||||
]
|
||||
const full_body = [ 0, body, opcodes.end ].flat()
|
||||
return [ full_body.length, full_body ].flat();
|
||||
} else {
|
||||
return [
|
||||
body.length + local_count + 3,
|
||||
local_count,
|
||||
local_count,
|
||||
...local_types,
|
||||
...body,
|
||||
opcodes["end"]
|
||||
];
|
||||
const groups = this.group(local_types);
|
||||
const full_body = [
|
||||
groups.length,
|
||||
...groups.flat(),
|
||||
body,
|
||||
opcodes.end,
|
||||
].flat();
|
||||
return [ full_body.length, full_body ].flat();
|
||||
}
|
||||
});
|
||||
return [ contents.length ].concat(...contents);
|
||||
return [ contents.length, contents ].flat(Infinity);
|
||||
}
|
||||
|
||||
wasm_section_data() {
|
||||
@@ -990,30 +1343,37 @@ export class Assembler {
|
||||
opcodes["i32.const"],
|
||||
...this.leb128(loc.addr),
|
||||
opcodes["end"],
|
||||
data.length,
|
||||
...this.uleb128(data.length),
|
||||
...data,
|
||||
]
|
||||
});
|
||||
return [ contents.length ].concat(...contents);
|
||||
return [ contents.length, contents ].flat(Infinity);
|
||||
}
|
||||
|
||||
wasm() {
|
||||
this.resolve_refs();
|
||||
this.resolve_func_types();
|
||||
|
||||
const template = [
|
||||
[ Section.TYPE, () => this.wasm_section_type() ],
|
||||
[ Section.IMPORT, () => this.wasm_section_import() ],
|
||||
[ Section.FUNC, () => this.wasm_section_func() ],
|
||||
[ Section.TABLE, () => this.wasm_section_table() ],
|
||||
[ Section.MEM, () => this.wasm_section_mem() ],
|
||||
[ Section.GLOBAL, () => this.wasm_section_global() ],
|
||||
[ Section.EXPORT, () => this.wasm_section_export() ],
|
||||
[ Section.ELEM, () => this.wasm_section_elem() ],
|
||||
[ Section.CODE, () => this.wasm_section_code() ],
|
||||
[ Section.DATA, () => this.wasm_section_data() ],
|
||||
];
|
||||
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.concat(...sections));
|
||||
return new Uint8Array([ HEADER, sections ].flat(Infinity));
|
||||
}
|
||||
|
||||
mem_wasm({ flags, init, max }) {
|
||||
@@ -1023,17 +1383,69 @@ export class Assembler {
|
||||
return [ flags, init ];
|
||||
}
|
||||
|
||||
leb128(x) {
|
||||
const orig = x;
|
||||
const bytes = [];
|
||||
while (true) {
|
||||
const b = x & 0x7f;
|
||||
x >>= 7;
|
||||
if (x == 0 && (b & 0x40) == 0 || x == -1 && (b & 0x40) != 0) {
|
||||
bytes.push(b);
|
||||
return bytes;
|
||||
}
|
||||
bytes.push(b | 0x80);
|
||||
}
|
||||
func_type({ params, results }) {
|
||||
const param_types = params.length == undefined
|
||||
? Object.values(params)
|
||||
: params;
|
||||
return [
|
||||
types["func"],
|
||||
param_types.length,
|
||||
...param_types,
|
||||
results.length,
|
||||
...results,
|
||||
];
|
||||
}
|
||||
|
||||
array_eq(a, b) {
|
||||
return a.length == b.length && a.every((x, i) => x == b[i]);
|
||||
}
|
||||
|
||||
ensure_type(type) {
|
||||
const index = this.types.findIndex((t) => this.array_eq(type, t));
|
||||
return index != -1 ? index : this.types.push(type) - 1;
|
||||
}
|
||||
|
||||
resolve_func_types() {
|
||||
for (const func of Object.values(this.funcs))
|
||||
func.type = this.ensure_type(this.func_type(func));
|
||||
}
|
||||
|
||||
group(array) {
|
||||
return array.reduce((acc, val) => {
|
||||
const last = acc.at(-1);
|
||||
if (last != undefined && last[1] == val)
|
||||
++last[0]
|
||||
else
|
||||
acc.push([1, val]);
|
||||
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
29
boot.js
@@ -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;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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
72
emu.js
@@ -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();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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™</div>
|
||||
</body>
|
||||
cringe, I know... but I promise there are no frameworks™</noscript></div></body>
|
||||
</html>
|
||||
|
||||
60
prelude.f
60
prelude.f
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
14
tests.scm
14
tests.scm
@@ -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")
|
||||
|
||||
2023
wipforth.wat
2023
wipforth.wat
File diff suppressed because it is too large
Load Diff
2106
wipforth.ws
Normal file
2106
wipforth.ws
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user