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
|
60
README.md
60
README.md
@@ -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
|
||||||
|
|||||||
633
asm.js
633
asm.js
@@ -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;
|
||||||
|
} else if (!this.comment) {
|
||||||
yield token;
|
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,63 +130,75 @@ 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,
|
||||||
"i32": 0x7f,
|
"funcref": 0x70,
|
||||||
|
"f32": 0x7d,
|
||||||
|
"i32": 0x7f,
|
||||||
};
|
};
|
||||||
|
|
||||||
const opcodes = {
|
const opcodes = {
|
||||||
"block": 0x02,
|
"block": 0x02,
|
||||||
"loop": 0x03,
|
"loop": 0x03,
|
||||||
"if": 0x04,
|
"if": 0x04,
|
||||||
"else": 0x05,
|
"else": 0x05,
|
||||||
"end": 0x0b,
|
"end": 0x0b,
|
||||||
"br": 0x0c,
|
"br": 0x0c,
|
||||||
"br_if": 0x0d,
|
"br_if": 0x0d,
|
||||||
"call": 0x10,
|
"call": 0x10,
|
||||||
"call_indirect": 0x11,
|
"call_indirect": 0x11,
|
||||||
"drop": 0x0a,
|
"drop": 0x0a,
|
||||||
"local.get": 0x20,
|
"local.get": 0x20,
|
||||||
"local.set": 0x21,
|
"local.set": 0x21,
|
||||||
"local.tee": 0x22,
|
"local.tee": 0x22,
|
||||||
"global.get": 0x23,
|
"global.get": 0x23,
|
||||||
"global.set": 0x24,
|
"global.set": 0x24,
|
||||||
"i32.load": 0x28,
|
"i32.load": 0x28,
|
||||||
"i32.load8_u": 0x2d,
|
"i32.load8_u": 0x2d,
|
||||||
"i32.store": 0x36,
|
"i32.store": 0x36,
|
||||||
"i32.store8": 0x3a,
|
"i32.store8": 0x3a,
|
||||||
"i32.const": 0x41,
|
"i32.const": 0x41,
|
||||||
"i32.eqz": 0x45,
|
"i64.const": 0x42,
|
||||||
"i32.eq": 0x46,
|
"i32.eqz": 0x45,
|
||||||
"i32.ne": 0x47,
|
"i32.eq": 0x46,
|
||||||
"i32.lt_s": 0x48,
|
"i32.ne": 0x47,
|
||||||
"i32.lt_u": 0x49,
|
"i32.lt_s": 0x48,
|
||||||
"i32.gt_s": 0x4a,
|
"i32.lt_u": 0x49,
|
||||||
"i32.gt_u": 0x4b,
|
"i32.gt_s": 0x4a,
|
||||||
"i32.le_s": 0x4c,
|
"i32.gt_u": 0x4b,
|
||||||
"i32.le_u": 0x4d,
|
"i32.le_s": 0x4c,
|
||||||
"i32.ge_s": 0x4e,
|
"i32.le_u": 0x4d,
|
||||||
"i32.ge_u": 0x4f,
|
"i32.ge_s": 0x4e,
|
||||||
"i32.add": 0x6a,
|
"i32.ge_u": 0x4f,
|
||||||
"i32.sub": 0x6b,
|
"i32.add": 0x6a,
|
||||||
"i32.mul": 0x6c,
|
"i32.sub": 0x6b,
|
||||||
"i32.and": 0x71,
|
"i32.mul": 0x6c,
|
||||||
"i32.or": 0x72,
|
"i32.div_s": 0x6d,
|
||||||
"i32.xor": 0x73,
|
"i32.rem_s": 0x6f,
|
||||||
"i32.shl": 0x74,
|
"i32.and": 0x71,
|
||||||
"i32.shr_s": 0x75,
|
"i32.or": 0x72,
|
||||||
"i32.shr_u": 0x76,
|
"i32.xor": 0x73,
|
||||||
|
"i32.shl": 0x74,
|
||||||
|
"i32.shr_s": 0x75,
|
||||||
|
"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,27 +534,21 @@ 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 }
|
||||||
};
|
};
|
||||||
this.global = undefined;
|
this.global = undefined;
|
||||||
this.global_name = undefined;
|
this.global_name = undefined;
|
||||||
this.state = State.TOP;
|
this.state = State.TOP;
|
||||||
return action;
|
return action;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
token_at_mem(token) {
|
token_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(
|
this.at.addr = value;
|
||||||
`ERROR: Unexpected token ${token} in .mem: `
|
else
|
||||||
+ "expected address");
|
this.at.addr_symbol = token;
|
||||||
this.at = undefined;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.at.addr = value;
|
|
||||||
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,16 +880,19 @@ 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,
|
||||||
});
|
});
|
||||||
|
|
||||||
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}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
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) => {
|
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;
|
||||||
console.log('System halt');
|
self.mod = await WebAssembly.instantiate(wasm, exports);
|
||||||
|
await self.postMessage('ready');
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "boot":
|
||||||
|
self.mod.instance.exports.reset();
|
||||||
|
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
|
||||||
|
|||||||
27
emu.js
27
emu.js
@@ -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 .
|
||||||
|
|||||||
48
prof.js
Normal file
48
prof.js
Normal file
@@ -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));
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -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)
|
||||||
|
|||||||
12
tests.scm
12
tests.scm
@@ -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
wipforth.wat
2023
wipforth.wat
File diff suppressed because it is too large
Load Diff
2098
wipforth.ws
Normal file
2098
wipforth.ws
Normal file
File diff suppressed because it is too large
Load Diff
16
words.js
Normal file
16
words.js
Normal file
@@ -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