Compare commits
46 Commits
a3cfd405a9
...
profiler
| Author | SHA1 | Date | |
|---|---|---|---|
|
956d42d008
|
|||
|
87d8345017
|
|||
|
d39fe580fc
|
|||
|
ba8c99a123
|
|||
|
cc8ae742f0
|
|||
|
812443d6ee
|
|||
|
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
|
@@ -1,31 +1,24 @@
|
|||||||
# Wipforth
|
# Wipforth
|
||||||
|
|
||||||
Wipforth is a simple Forth implementation that runs in the WebAssembly
|
Wipforth is a Forth implementation that runs in the WebAssembly
|
||||||
virtual machine. It does I/O via memory-mapped peripherals, which are
|
virtual machine. The system is bootstrapped from source on page load:
|
||||||
emulated in JavaScript.
|
the only non-text file is the favicon :)
|
||||||
|
|
||||||
- For the Forth kernel, see [wipforth.wat](./wipforth.wat)
|
I/O is done via memory-mapped peripherals, which are emulated in
|
||||||
- For the JavaScript emulator, see [emu.js](./emu.js)
|
JavaScript.
|
||||||
- For the Forth prelude, which is loaded at start-up, see
|
|
||||||
[prelude.f](./prelude.f)
|
- 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
|
- For a description of the peripherals, see the
|
||||||
[Peripherals](#peripherals) section below.
|
[Peripherals](#peripherals) section below.
|
||||||
|
|
||||||
## Building and Running Locally
|
## Building and Running Locally
|
||||||
|
|
||||||
You'll need:
|
There's a [Guile](https://www.gnu.org/software/guile/) script in the
|
||||||
|
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
|
||||||
@@ -34,14 +27,20 @@ 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.
|
||||||
|
|
||||||
**NOTE**: The server is very simple and just serves the files with the
|
However, since everything is bootstrapped on the client, basically any
|
||||||
cross-origin isolation headers required for `SharedMemoryBuffer` use.
|
HTTP server will do as long as it sets the appropriate response
|
||||||
You could use any HTTP server that sets these headers.
|
headers for `SharedMemoryBuffer` use:
|
||||||
|
|
||||||
You should **definitely not** use the development server to serve the
|
- `Cross-Origin-Opener-Policy: same-origin`
|
||||||
application on the open internet; I just hacked it together for
|
- `Cross-Origin-Embedder-Policy: require-corp`
|
||||||
testing on localhost during development and it's probably hilariously
|
|
||||||
insecure.
|
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
|
## End-to-End Tests
|
||||||
|
|
||||||
@@ -62,16 +61,13 @@ 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 pretty-print
|
It will print a JUnit XML report to standard out. You can
|
||||||
it with:
|
pretty-print it with `xmllint` if you have it installed:
|
||||||
|
|
||||||
```
|
```
|
||||||
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
|
||||||
|
|||||||
@@ -58,12 +58,15 @@ class Tokenizer {
|
|||||||
this.buffer.push(...src);
|
this.buffer.push(...src);
|
||||||
let token;
|
let token;
|
||||||
while (token = this.next()) {
|
while (token = this.next()) {
|
||||||
if (token == this.comment_start)
|
if (token.string == undefined
|
||||||
|
&& token.startsWith(this.comment_start)) {
|
||||||
this.comment = true;
|
this.comment = true;
|
||||||
else if (this.comment && token == LINE_END)
|
} else if (this.comment && token == LINE_END) {
|
||||||
this.comment = false;
|
this.comment = false;
|
||||||
else if (!this.comment)
|
|
||||||
yield token;
|
yield token;
|
||||||
|
} else if (!this.comment) {
|
||||||
|
yield token;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -97,6 +100,15 @@ const State = Object.freeze({
|
|||||||
DEF_VALUE: 25,
|
DEF_VALUE: 25,
|
||||||
BLOCK_NAME: 26,
|
BLOCK_NAME: 26,
|
||||||
BLOCK_TYPE: 27,
|
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({
|
const Action = Object.freeze({
|
||||||
@@ -118,11 +130,16 @@ const Action = Object.freeze({
|
|||||||
ENTER: 16,
|
ENTER: 16,
|
||||||
EXIT: 17,
|
EXIT: 17,
|
||||||
ELSE: 18,
|
ELSE: 18,
|
||||||
|
TYPE: 19,
|
||||||
|
TABLE: 20,
|
||||||
|
ELEM: 21,
|
||||||
});
|
});
|
||||||
|
|
||||||
const types = {
|
const types = {
|
||||||
"void": 0x40,
|
"void": 0x40,
|
||||||
"func": 0x60,
|
"func": 0x60,
|
||||||
|
"funcref": 0x70,
|
||||||
|
"f32": 0x7d,
|
||||||
"i32": 0x7f,
|
"i32": 0x7f,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -147,6 +164,7 @@ const opcodes = {
|
|||||||
"i32.store": 0x36,
|
"i32.store": 0x36,
|
||||||
"i32.store8": 0x3a,
|
"i32.store8": 0x3a,
|
||||||
"i32.const": 0x41,
|
"i32.const": 0x41,
|
||||||
|
"i64.const": 0x42,
|
||||||
"i32.eqz": 0x45,
|
"i32.eqz": 0x45,
|
||||||
"i32.eq": 0x46,
|
"i32.eq": 0x46,
|
||||||
"i32.ne": 0x47,
|
"i32.ne": 0x47,
|
||||||
@@ -161,20 +179,26 @@ const opcodes = {
|
|||||||
"i32.add": 0x6a,
|
"i32.add": 0x6a,
|
||||||
"i32.sub": 0x6b,
|
"i32.sub": 0x6b,
|
||||||
"i32.mul": 0x6c,
|
"i32.mul": 0x6c,
|
||||||
|
"i32.div_s": 0x6d,
|
||||||
|
"i32.rem_s": 0x6f,
|
||||||
"i32.and": 0x71,
|
"i32.and": 0x71,
|
||||||
"i32.or": 0x72,
|
"i32.or": 0x72,
|
||||||
"i32.xor": 0x73,
|
"i32.xor": 0x73,
|
||||||
"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 ],
|
||||||
"memory.atomic.wait32": [ 0xfe, 0x01 ],
|
"memory.atomic.wait32": [ 0xfe, 0x01 ],
|
||||||
"memory.atomic.load": [ 0xfe, 0x10 ],
|
"i32.atomic.load": [ 0xfe, 0x10 ],
|
||||||
"memory.atomic.load8_u": [ 0xfe, 0x12 ],
|
"i32.atomic.load8_u": [ 0xfe, 0x12 ],
|
||||||
"memory.atomic.store": [ 0xfe, 0x17 ],
|
"i32.atomic.store": [ 0xfe, 0x17 ],
|
||||||
"memory.atomic.store8": [ 0xfe, 0x19 ],
|
"i64.atomic.store": [ 0xfe, 0x18 ],
|
||||||
|
"i32.atomic.store8": [ 0xfe, 0x19 ],
|
||||||
};
|
};
|
||||||
|
|
||||||
const mem_flags = {
|
const mem_flags = {
|
||||||
@@ -208,6 +232,10 @@ class Parser {
|
|||||||
".utf8": State.UTF8,
|
".utf8": State.UTF8,
|
||||||
".align": State.ALIGN,
|
".align": State.ALIGN,
|
||||||
".def": State.DEF_NAME,
|
".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.blocks = new Set(["block", "loop", "if"]);
|
||||||
this.handlers = {
|
this.handlers = {
|
||||||
@@ -239,6 +267,15 @@ class Parser {
|
|||||||
[State.DEF_VALUE]: (token) => this.token_def_value(token),
|
[State.DEF_VALUE]: (token) => this.token_def_value(token),
|
||||||
[State.BLOCK_NAME]: (token) => this.token_block_name(token),
|
[State.BLOCK_NAME]: (token) => this.token_block_name(token),
|
||||||
[State.BLOCK_TYPE]: (token) => this.token_block_type(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 = [];
|
this.results = [];
|
||||||
@@ -247,15 +284,14 @@ class Parser {
|
|||||||
}
|
}
|
||||||
|
|
||||||
integer(token) {
|
integer(token) {
|
||||||
let base;
|
let base, regex;
|
||||||
switch (token.slice(-1)) {
|
switch (token.slice(-1)) {
|
||||||
case "b": base = 2; break;
|
case "b": base = 2; regex = /^-?[01]+b$/; break;
|
||||||
case "o": base = 8; break;
|
case "o": base = 8; regex = /^-?[0-7]+o$/; break;
|
||||||
case "h": base = 16; break;
|
case "h": base = 16; regex = /^-?[0-9A-F]+h$/; break;
|
||||||
default: base = 10; break;
|
default: base = 10; regex = /^-?[0-9]+d?$/; break;
|
||||||
}
|
}
|
||||||
const x = parseInt(token, base);
|
return regex.test(token) ? parseInt(token, base) : null;
|
||||||
return Number.isNaN(x) ? null : x;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
translate_code(token) {
|
translate_code(token) {
|
||||||
@@ -293,7 +329,7 @@ class Parser {
|
|||||||
if (opcode)
|
if (opcode)
|
||||||
return { type: Action.APPEND, opcode };
|
return { type: Action.APPEND, opcode };
|
||||||
const literal = this.integer(token);
|
const literal = this.integer(token);
|
||||||
if (literal)
|
if (literal != null)
|
||||||
return { type: Action.APPEND, literal };
|
return { type: Action.APPEND, literal };
|
||||||
|
|
||||||
return { type: Action.SYMBOL, symbol: token };
|
return { type: Action.SYMBOL, symbol: token };
|
||||||
@@ -498,18 +534,13 @@ class Parser {
|
|||||||
|
|
||||||
token_global_init(token) {
|
token_global_init(token) {
|
||||||
if (token == LINE_END) {
|
if (token == LINE_END) {
|
||||||
console.error(
|
this.global.init = 0;
|
||||||
"ERROR: Unexpected newline in .global: expected"
|
|
||||||
+ " initial value");
|
|
||||||
this.global = undefined;
|
|
||||||
this.global_name = undefined;
|
|
||||||
this.state = State.TOP;
|
|
||||||
} else {
|
} else {
|
||||||
const value = this.integer(token) ?? console.error(
|
const value = this.integer(token) ?? console.error(
|
||||||
`ERROR: Unexpected token ${token} in .global: expected`
|
`ERROR: Unexpected token ${token} in .global: expected`
|
||||||
+ " initial value");
|
+ " initial value");
|
||||||
const const_opcode = const_opcodes[this.global.type];
|
this.global.init = value;
|
||||||
this.global.init = [ const_opcode, value, opcodes["end"] ];
|
}
|
||||||
const action = {
|
const action = {
|
||||||
type: Action.GLOBAL,
|
type: Action.GLOBAL,
|
||||||
global: { [this.global_name]: this.global }
|
global: { [this.global_name]: this.global }
|
||||||
@@ -519,7 +550,6 @@ class Parser {
|
|||||||
this.state = State.TOP;
|
this.state = State.TOP;
|
||||||
return action;
|
return action;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
token_at_mem(token) {
|
token_at_mem(token) {
|
||||||
this.at = { mem: token };
|
this.at = { mem: token };
|
||||||
@@ -528,15 +558,10 @@ class Parser {
|
|||||||
|
|
||||||
token_at_addr(token) {
|
token_at_addr(token) {
|
||||||
const value = this.integer(token);
|
const value = this.integer(token);
|
||||||
if (value == null) {
|
if (value != null)
|
||||||
console.error(
|
|
||||||
`ERROR: Unexpected token ${token} in .mem: `
|
|
||||||
+ "expected address");
|
|
||||||
this.at = undefined;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.at.addr = value;
|
this.at.addr = value;
|
||||||
|
else
|
||||||
|
this.at.addr_symbol = token;
|
||||||
const action = { type: Action.AT, at: this.at };
|
const action = { type: Action.AT, at: this.at };
|
||||||
this.at = undefined;
|
this.at = undefined;
|
||||||
this.state = State.TOP;
|
this.state = State.TOP;
|
||||||
@@ -555,7 +580,7 @@ class Parser {
|
|||||||
} else {
|
} else {
|
||||||
if (value > 0xff)
|
if (value > 0xff)
|
||||||
console.error(`WARNING: Value ${token} is truncated`);
|
console.error(`WARNING: Value ${token} is truncated`);
|
||||||
action.value = [ value & 0xff ];
|
action.value = value;
|
||||||
}
|
}
|
||||||
return action;
|
return action;
|
||||||
}
|
}
|
||||||
@@ -570,9 +595,9 @@ class Parser {
|
|||||||
if (value == null) {
|
if (value == null) {
|
||||||
action.symbol = token;
|
action.symbol = token;
|
||||||
} else {
|
} else {
|
||||||
if (value > 0xffff)
|
if (value > 0xffffffff)
|
||||||
console.error(`WARNING: Value ${token} is truncated`);
|
console.error(`WARNING: Value ${token} is truncated`);
|
||||||
action.value = [ value & 0xff, (value >> 8) & 0xff ];
|
action.value = value;
|
||||||
}
|
}
|
||||||
return action;
|
return action;
|
||||||
}
|
}
|
||||||
@@ -586,8 +611,8 @@ class Parser {
|
|||||||
`ERROR: Unexpected token ${token}, expected string`);
|
`ERROR: Unexpected token ${token}, expected string`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const value = this.encoder.encode(token.string);
|
const bytes = this.encoder.encode(token.string);
|
||||||
const action = { type: Action.DATA, size: value.length, value };
|
const action = { type: Action.DATA, size: bytes.length, bytes };
|
||||||
return action;
|
return action;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -669,6 +694,161 @@ class Parser {
|
|||||||
return action;
|
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() {
|
mem_action() {
|
||||||
const action = {
|
const action = {
|
||||||
type: Action.MEM,
|
type: Action.MEM,
|
||||||
@@ -683,9 +863,15 @@ class Parser {
|
|||||||
*handle(src) {
|
*handle(src) {
|
||||||
let action;
|
let action;
|
||||||
for (const token of this.tokenizer.handle(src)) {
|
for (const token of this.tokenizer.handle(src)) {
|
||||||
if (action = this.handlers[this.state](token)) {
|
const handler = this.handlers[this.state];
|
||||||
yield action;
|
if (handler == undefined) {
|
||||||
|
console.error(`ERROR: Unhandled state ${this.state}`);
|
||||||
|
this.state = State.TOP;
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (action = handler(token))
|
||||||
|
yield action;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -694,9 +880,11 @@ const Section = Object.freeze({
|
|||||||
TYPE: 0x01,
|
TYPE: 0x01,
|
||||||
IMPORT: 0x02,
|
IMPORT: 0x02,
|
||||||
FUNC: 0x03,
|
FUNC: 0x03,
|
||||||
|
TABLE: 0x04,
|
||||||
MEM: 0x05,
|
MEM: 0x05,
|
||||||
GLOBAL: 0x06,
|
GLOBAL: 0x06,
|
||||||
EXPORT: 0x07,
|
EXPORT: 0x07,
|
||||||
|
ELEM: 0x09,
|
||||||
CODE: 0x0a,
|
CODE: 0x0a,
|
||||||
DATA: 0x0b,
|
DATA: 0x0b,
|
||||||
});
|
});
|
||||||
@@ -704,6 +892,7 @@ 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 {
|
||||||
@@ -730,6 +919,9 @@ export class Assembler {
|
|||||||
[Action.ENTER]: (action) => this.action_enter(action),
|
[Action.ENTER]: (action) => this.action_enter(action),
|
||||||
[Action.EXIT]: (action) => this.action_exit(action),
|
[Action.EXIT]: (action) => this.action_exit(action),
|
||||||
[Action.ELSE]: (action) => this.action_else(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 = [];
|
this.exports = [];
|
||||||
@@ -742,6 +934,9 @@ export class Assembler {
|
|||||||
this.defs = {};
|
this.defs = {};
|
||||||
this.blocks = [];
|
this.blocks = [];
|
||||||
this.types = [];
|
this.types = [];
|
||||||
|
this.type_bindings = {};
|
||||||
|
this.tables = {};
|
||||||
|
this.unresolved = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
action_append(action) {
|
action_append(action) {
|
||||||
@@ -752,8 +947,28 @@ export class Assembler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
action_export(action) {
|
action_export(action) {
|
||||||
const index = Object.keys(this.funcs).indexOf(action.name);
|
const func_index = Object.keys(this.funcs).indexOf(action.name);
|
||||||
this.exports[action.name] = { kind: Kind.FUNC, index };
|
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) {
|
action_func(action) {
|
||||||
@@ -784,6 +999,9 @@ export class Assembler {
|
|||||||
?? this.lookup_param(func, action.symbol)
|
?? this.lookup_param(func, action.symbol)
|
||||||
?? this.lookup_local(func, action.symbol)
|
?? this.lookup_local(func, action.symbol)
|
||||||
?? this.lookup_global(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) {
|
if (value == null) {
|
||||||
const def_value = this.lookup_def(action.symbol);
|
const def_value = this.lookup_def(action.symbol);
|
||||||
if (def_value == null) {
|
if (def_value == null) {
|
||||||
@@ -793,7 +1011,7 @@ export class Assembler {
|
|||||||
}
|
}
|
||||||
func.body.push(...this.leb128(def_value));
|
func.body.push(...this.leb128(def_value));
|
||||||
} else {
|
} else {
|
||||||
func.body.push(value);
|
func.body.push(...this.uleb128(value));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -825,16 +1043,38 @@ export class Assembler {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.pos.mem = mem;
|
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: [] })
|
this.data.push({ loc: { ...this.pos }, data: [] })
|
||||||
}
|
}
|
||||||
|
|
||||||
action_data(action) {
|
action_data(action) {
|
||||||
const data = this.data.at(-1).data;
|
const data = this.data.at(-1).data;
|
||||||
const value = action.value != null
|
let bytes;
|
||||||
? action.value
|
if (action.bytes != undefined) {
|
||||||
: this.le(this.lookup_def(action.symbol), action.size);
|
bytes = action.bytes;
|
||||||
data.push(...value);
|
} 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;
|
this.pos.addr += action.size;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -874,10 +1114,40 @@ export class Assembler {
|
|||||||
this.blocks.push(undefined);
|
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) {
|
push(chunk) {
|
||||||
const text = this.decoder.decode(chunk, { stream: true });
|
const text = this.decoder.decode(chunk, { stream: true });
|
||||||
for (const action of this.parser.handle(text))
|
for (const action of this.parser.handle(text)) {
|
||||||
this.handlers[action.type](action);
|
const handler = this.handlers[action.type];
|
||||||
|
if (handler == undefined)
|
||||||
|
console.error("ERROR: Unhandled action", action);
|
||||||
|
else
|
||||||
|
handler(action);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
lookup_block(symbol) {
|
lookup_block(symbol) {
|
||||||
@@ -901,15 +1171,29 @@ export class Assembler {
|
|||||||
return index == -1 ? null : index;
|
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) {
|
lookup_def(symbol) {
|
||||||
return this.defs[symbol];
|
return this.defs[symbol];
|
||||||
}
|
}
|
||||||
|
|
||||||
le(value, count) {
|
le(value, count) {
|
||||||
let bytes = []
|
const bytes = []
|
||||||
while (value != 0) {
|
while (value != 0) {
|
||||||
bytes.push(value & 0xff);
|
bytes.push(value & 0xff);
|
||||||
value >>= 8;
|
value >>>= 8;
|
||||||
}
|
}
|
||||||
if (count != undefined) {
|
if (count != undefined) {
|
||||||
while (bytes.length < count)
|
while (bytes.length < count)
|
||||||
@@ -919,7 +1203,6 @@ export class Assembler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
leb128(x) {
|
leb128(x) {
|
||||||
const orig = x;
|
|
||||||
const bytes = [];
|
const bytes = [];
|
||||||
while (true) {
|
while (true) {
|
||||||
const b = x & 0x7f;
|
const b = x & 0x7f;
|
||||||
@@ -932,6 +1215,20 @@ export class Assembler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
uleb128(x) {
|
||||||
|
const bytes = [];
|
||||||
|
while (true) {
|
||||||
|
const b = x & 0x7f;
|
||||||
|
x >>>= 7;
|
||||||
|
if (x == 0) {
|
||||||
|
bytes.push(b);
|
||||||
|
return bytes;
|
||||||
|
}
|
||||||
|
bytes.push(b | 0x80);
|
||||||
|
}
|
||||||
|
return bytes;
|
||||||
|
}
|
||||||
|
|
||||||
wasm_section_type() {
|
wasm_section_type() {
|
||||||
if (this.types.length == 0) return null;
|
if (this.types.length == 0) return null;
|
||||||
return [ this.types.length ].concat(...this.types);
|
return [ this.types.length ].concat(...this.types);
|
||||||
@@ -962,6 +1259,17 @@ export class Assembler {
|
|||||||
return [ count, ...types ];
|
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() {
|
wasm_section_mem() {
|
||||||
const mems = Object.values(this.mems).filter(
|
const mems = Object.values(this.mems).filter(
|
||||||
({imported}) => !imported);
|
({imported}) => !imported);
|
||||||
@@ -975,8 +1283,13 @@ export class Assembler {
|
|||||||
const globals = Object.values(this.globals);
|
const globals = Object.values(this.globals);
|
||||||
if (globals.length == 0)
|
if (globals.length == 0)
|
||||||
return null;
|
return null;
|
||||||
const contents = globals.map(
|
const contents = globals.map(({ type, init }) => [
|
||||||
({ type, init }) => [ type, 1, ...init ]);
|
type,
|
||||||
|
1,
|
||||||
|
const_opcodes[type],
|
||||||
|
...this.leb128(init),
|
||||||
|
opcodes["end"],
|
||||||
|
]);
|
||||||
return [ globals.length ].concat(...contents);
|
return [ globals.length ].concat(...contents);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -995,6 +1308,24 @@ export class Assembler {
|
|||||||
return [ exports.length ].concat(...contents);
|
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() {
|
wasm_section_code() {
|
||||||
const funcs = Object.values(this.funcs);
|
const funcs = Object.values(this.funcs);
|
||||||
if (funcs.length == 0) return null;
|
if (funcs.length == 0) return null;
|
||||||
@@ -1002,24 +1333,20 @@ export class Assembler {
|
|||||||
const local_types = Object.values(locals);
|
const local_types = Object.values(locals);
|
||||||
const local_count = local_types.length;
|
const local_count = local_types.length;
|
||||||
if (local_count == 0) {
|
if (local_count == 0) {
|
||||||
return [
|
const full_body = [ 0, body, opcodes.end ].flat()
|
||||||
body.length + 2,
|
return [ full_body.length, full_body ].flat();
|
||||||
0,
|
|
||||||
...body,
|
|
||||||
opcodes["end"]
|
|
||||||
]
|
|
||||||
} else {
|
} else {
|
||||||
return [
|
const groups = this.group(local_types);
|
||||||
body.length + local_count + 3,
|
const full_body = [
|
||||||
local_count,
|
groups.length,
|
||||||
local_count,
|
...groups.flat(),
|
||||||
...local_types,
|
body,
|
||||||
...body,
|
opcodes.end,
|
||||||
opcodes["end"]
|
].flat();
|
||||||
];
|
return [ full_body.length, full_body ].flat();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return [ contents.length ].concat(...contents);
|
return [ contents.length, contents ].flat(Infinity);
|
||||||
}
|
}
|
||||||
|
|
||||||
wasm_section_data() {
|
wasm_section_data() {
|
||||||
@@ -1030,32 +1357,37 @@ export class Assembler {
|
|||||||
opcodes["i32.const"],
|
opcodes["i32.const"],
|
||||||
...this.leb128(loc.addr),
|
...this.leb128(loc.addr),
|
||||||
opcodes["end"],
|
opcodes["end"],
|
||||||
data.length,
|
...this.uleb128(data.length),
|
||||||
...data,
|
...data,
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
return [ contents.length ].concat(...contents);
|
return [ contents.length, contents ].flat(Infinity);
|
||||||
}
|
}
|
||||||
|
|
||||||
wasm() {
|
wasm() {
|
||||||
|
this.resolve_refs();
|
||||||
this.resolve_func_types();
|
this.resolve_func_types();
|
||||||
|
|
||||||
const template = [
|
const template = [
|
||||||
[ Section.TYPE, () => this.wasm_section_type() ],
|
[ Section.TYPE, () => this.wasm_section_type() ],
|
||||||
[ Section.IMPORT, () => this.wasm_section_import() ],
|
[ Section.IMPORT, () => this.wasm_section_import() ],
|
||||||
[ Section.FUNC, () => this.wasm_section_func() ],
|
[ Section.FUNC, () => this.wasm_section_func() ],
|
||||||
|
[ Section.TABLE, () => this.wasm_section_table() ],
|
||||||
[ Section.MEM, () => this.wasm_section_mem() ],
|
[ Section.MEM, () => this.wasm_section_mem() ],
|
||||||
[ Section.GLOBAL, () => this.wasm_section_global() ],
|
[ Section.GLOBAL, () => this.wasm_section_global() ],
|
||||||
[ Section.EXPORT, () => this.wasm_section_export() ],
|
[ Section.EXPORT, () => this.wasm_section_export() ],
|
||||||
|
[ Section.ELEM, () => this.wasm_section_elem() ],
|
||||||
[ Section.CODE, () => this.wasm_section_code() ],
|
[ Section.CODE, () => this.wasm_section_code() ],
|
||||||
[ Section.DATA, () => this.wasm_section_data() ],
|
[ Section.DATA, () => this.wasm_section_data() ],
|
||||||
];
|
];
|
||||||
const sections = template.map(([ code, generator ]) => {
|
const sections = template.map(([ code, generator ]) => {
|
||||||
const body = 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 }) {
|
mem_wasm({ flags, init, max }) {
|
||||||
@@ -1066,7 +1398,9 @@ export class Assembler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func_type({ params, results }) {
|
func_type({ params, results }) {
|
||||||
const param_types = Object.values(params);
|
const param_types = params.length == undefined
|
||||||
|
? Object.values(params)
|
||||||
|
: params;
|
||||||
return [
|
return [
|
||||||
types["func"],
|
types["func"],
|
||||||
param_types.length,
|
param_types.length,
|
||||||
@@ -1089,4 +1423,43 @@ export class Assembler {
|
|||||||
for (const func of Object.values(this.funcs))
|
for (const func of Object.values(this.funcs))
|
||||||
func.type = this.ensure_type(this.func_type(func));
|
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}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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) => {
|
self.onmessage = async (e) => {
|
||||||
const exports = { emu: { mem: e.data } };
|
switch (e.data.type) {
|
||||||
const mod = await WebAssembly.instantiateStreaming(
|
case "load":
|
||||||
fetch('wipforth.wasm'), exports)
|
const exports = { emu: { mem: e.data.mem } };
|
||||||
mod.instance.exports.reset();
|
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');
|
console.log('System halt');
|
||||||
|
break;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,4 +3,4 @@ emu.js
|
|||||||
index.html
|
index.html
|
||||||
prelude.f
|
prelude.f
|
||||||
styles.css
|
styles.css
|
||||||
wipforth.wasm
|
wipforth.ws
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ 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;
|
||||||
@@ -50,8 +51,29 @@ 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.worker = new Worker('boot.js');
|
this.prof = new Worker("prof.js");
|
||||||
this.worker.postMessage(this.mem);
|
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({ 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())
|
||||||
@@ -77,6 +99,7 @@ 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
|
||||||
1 CONSTANT VERSION-MINOR
|
2 CONSTANT VERSION-MINOR
|
||||||
0 CONSTANT VERSION-PATCH
|
1 CONSTANT VERSION-PATCH
|
||||||
|
|
||||||
: PRINT-VERSION
|
: PRINT-VERSION
|
||||||
CHAR v EMIT VERSION-MAJOR .
|
CHAR v EMIT VERSION-MAJOR .
|
||||||
|
|||||||
@@ -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));
|
||||||
|
}
|
||||||
|
};
|
||||||
+1
-2
@@ -16,9 +16,8 @@
|
|||||||
'(("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))
|
||||||
("wat" . (text/plain))
|
("ws" . (text/plain))
|
||||||
("png" . (image/png))))
|
("png" . (image/png))))
|
||||||
|
|
||||||
(define (mime-type path)
|
(define (mime-type path)
|
||||||
|
|||||||
@@ -21,11 +21,17 @@
|
|||||||
(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))
|
||||||
(first-line (first (lines display))))
|
(line (second (lines display))))
|
||||||
(assert (string-match "Loading prelude \\.+ done" first-line)
|
(assert (string-match "Loading prelude \\.+ done" line)
|
||||||
(format #f "Prelude load line: ~s" first-line))))
|
(format #f "Prelude load line: ~s" 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")
|
||||||
|
|||||||
-2023
File diff suppressed because it is too large
Load Diff
+2098
File diff suppressed because it is too large
Load Diff
@@ -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));
|
||||||
Reference in New Issue
Block a user