Compare commits

...

50 Commits

Author SHA1 Message Date
956d42d008 Create assembler driver script for recording word addresses 2026-03-20 12:40:09 +00:00
87d8345017 Run profiler while prelude is loading 2026-03-19 22:03:46 +00:00
d39fe580fc Implement sampling profiler 2026-03-19 22:03:36 +00:00
ba8c99a123 Mirror IP and RSP regs in memory
This will allow the profiler to sample the current return stack and
instruction pointer.  It can't just use the ip and rsp globals
directly, as I had initially planned, since these cannot be sent
in between threads.
2026-03-19 22:01:51 +00:00
cc8ae742f0 Add more opcodes to assembler 2026-03-19 22:01:33 +00:00
812443d6ee Support exports of globals in assembler 2026-03-19 19:53:50 +00:00
67fc1d8d7b Remove race condition between assemble and prelude load prints 2026-03-18 15:17:08 +00:00
4000522b3a Remove obsolete assembly driver script 2026-03-18 15:08:57 +00:00
19ef69958d Update README 2026-03-18 15:04:32 +00:00
0a52388030 Update deploy manifest 2026-03-18 14:25:11 +00:00
6e8439eeaf Bump version number 2026-03-18 14:24:41 +00:00
eaa3242cc0 Update e2e tests 2026-03-18 14:24:25 +00:00
f77adffbef Update MIME types in server.scm 2026-03-18 14:24:05 +00:00
c91f46be88 Assemble kernel on client 2026-03-18 14:23:37 +00:00
6ee4adfea5 Translate kernel to Wasmasm 2026-03-18 10:41:09 +00:00
5dc0a7a601 Add temporary driver script 2026-03-18 10:35:14 +00:00
896a1ca563 Implement (limited) forward reference handling 2026-03-18 10:32:49 +00:00
37d56988ef Make a couple of tweaks to the kernel in preparation for porting 2026-03-18 10:30:26 +00:00
6c643f8402 Don't silently ignore trailing characters in numbers 2026-03-18 10:29:46 +00:00
7828b0f112 Yield newline token at end of comment 2026-03-15 21:39:14 +00:00
e7affbf8b7 Add .zero directive 2026-03-15 21:28:25 +00:00
02ee4c3c88 Support symbols in .at address field 2026-03-15 21:27:48 +00:00
c21b3c79c7 Fix names of atomic load and store opcodes 2026-03-15 20:04:49 +00:00
1318c3cc4e Add i64.const, i32.div_s and i32.rem_s opcodes 2026-03-15 20:04:27 +00:00
74a8f21379 Encode indices as unsigned LEB128 instead of signed 2026-03-15 20:04:06 +00:00
6784cd02b4 Encode section lengths with unsigned LEB128 2026-03-15 20:03:47 +00:00
3a103c46d1 Don't require ; to have space after in comments 2026-03-15 17:32:14 +00:00
8d4c53ca92 Allow implicit zero-init for globals 2026-03-15 14:26:22 +00:00
5e39024f6d Use unsigned shift in uleb128() 2026-03-15 14:15:40 +00:00
b85a4e8bc9 Encode data values in assembler, not parser 2026-03-15 14:15:34 +00:00
401e8e1fad Use unsigned right shift in Assembler.le() 2026-03-15 14:07:26 +00:00
d4c837216a Add f32 type 2026-03-15 13:58:33 +00:00
c93e9009da LEB128-encode index in action_symbol 2026-03-15 13:58:25 +00:00
0056610238 Add missing semicolon 2026-03-15 13:58:24 +00:00
9b4ff3e8f6 Use array flattening instead of spread operator in a few places 2026-03-15 13:58:23 +00:00
e9beacba3a De-duplicate consecutive locals of same type in wasm_section_code() 2026-03-15 13:58:23 +00:00
acf5b6e284 Handle failed def lookup in action_data() 2026-03-15 13:58:22 +00:00
72c5f64312 Handle global init value encoding in Assembler 2026-03-15 13:58:20 +00:00
7135eeba74 Restructure uleb128 2026-03-15 13:41:39 +00:00
7099ca34a3 Fix .word size 2026-03-15 13:41:05 +00:00
3ebb74c73c Check for null explicitly in token_top() 2026-03-15 13:40:43 +00:00
0dd2a925d8 Allow table elems to be labelled 2026-03-15 12:34:41 +00:00
2155d17731 Implement type, table and func symbol resolution 2026-03-15 12:28:29 +00:00
1452ffe615 Implement .table and .elem 2026-03-15 12:14:35 +00:00
46a571be93 Add error message for unhandled states and actions 2026-03-15 11:09:16 +00:00
d35b13fed0 Add .type directive 2026-03-15 11:05:37 +00:00
a3cfd405a9 Add some threads opcodes 2026-03-14 19:30:44 +00:00
671e7f60d2 Add a bunch of opcodes 2026-03-14 19:30:43 +00:00
580d5d2a4a Implement function type de-duplication 2026-03-14 19:23:02 +00:00
1105daaad0 Add support for extended opcodes 2026-03-14 19:23:00 +00:00
12 changed files with 2808 additions and 2200 deletions

View File

@@ -1,31 +1,24 @@
# Wipforth
Wipforth is a simple Forth implementation that runs in the WebAssembly
virtual machine. It does I/O via memory-mapped peripherals, which are
emulated in JavaScript.
Wipforth is a Forth implementation that runs in the WebAssembly
virtual machine. The system is bootstrapped from source on page load:
the only non-text file is the favicon :)
- For the Forth kernel, see [wipforth.wat](./wipforth.wat)
- For the JavaScript emulator, see [emu.js](./emu.js)
- For the Forth prelude, which is loaded at start-up, see
[prelude.f](./prelude.f)
I/O is done via memory-mapped peripherals, which are emulated in
JavaScript.
- For the Forth kernel, see [wipforth.ws](./wipforth.ws)
- For the emulator, see [emu.js](./emu.js)
- For the assembler, see [asm.js](./asm.js)
- For the prelude (Forth code loaded right after the kernel boots),
see [prelude.f](./prelude.f)
- For a description of the peripherals, see the
[Peripherals](#peripherals) section below.
## Building and Running Locally
You'll need:
- [WABT](https://github.com/WebAssembly/wabt) (not for long mwahaha)
- [Guile](https://www.gnu.org/software/guile/) (or bring your own HTTP
server -- see note below)
To run, first compile the WebAssembly module:
```
wat2wasm --enable-threads wipforth.wat
```
Then run the development server:
There's a [Guile](https://www.gnu.org/software/guile/) script in the
repo you can use for this:
```
guile server.scm
@@ -34,14 +27,20 @@ guile server.scm
You should then be able to open <http://localhost:8080> in a browser
and use the system from there.
**NOTE**: The server is very simple and just serves the files with the
cross-origin isolation headers required for `SharedMemoryBuffer` use.
You could use any HTTP server that sets these headers.
However, since everything is bootstrapped on the client, basically any
HTTP server will do as long as it sets the appropriate response
headers for `SharedMemoryBuffer` use:
You should **definitely not** use the development server to serve the
application on the open internet; I just hacked it together for
testing on localhost during development and it's probably hilariously
insecure.
- `Cross-Origin-Opener-Policy: same-origin`
- `Cross-Origin-Embedder-Policy: require-corp`
So, if you don't have Guile on your system you can use something else
like Python's `http.server`.
**NOTE**: You should **definitely not** use `server.scm` to serve the
application on the open internet or anything like that; I just hacked
it together for testing on localhost during development and it's
probably hilariously insecure.
## End-to-End Tests
@@ -62,16 +61,13 @@ Given that's all sorted, you should be able to run:
guile tests.scm
```
It will print a JUnit XML report to standard out, you can pretty-print
it with:
It will print a JUnit XML report to standard out. You can
pretty-print it with `xmllint` if you have it installed:
```
guile tests.scm | xmllint --format -
```
Though, of course, this will require that you have `xmllint` on your
system.
## Peripherals
### Terminal

686
asm.js
View File

@@ -58,12 +58,15 @@ class Tokenizer {
this.buffer.push(...src);
let token;
while (token = this.next()) {
if (token == this.comment_start)
if (token.string == undefined
&& token.startsWith(this.comment_start)) {
this.comment = true;
else if (this.comment && token == LINE_END)
} else if (this.comment && token == LINE_END) {
this.comment = false;
else if (!this.comment)
yield token;
} else if (!this.comment) {
yield token;
}
}
}
}
@@ -97,6 +100,15 @@ const State = Object.freeze({
DEF_VALUE: 25,
BLOCK_NAME: 26,
BLOCK_TYPE: 27,
TYPE_NAME: 28,
TYPE_PARAM: 29,
TYPE_RESULT: 30,
TABLE_NAME: 31,
TABLE_SIZE: 32,
ELEM_TABLE: 33,
ELEM_ELEM: 34,
ELEM_LABEL: 35,
ZERO: 36,
});
const Action = Object.freeze({
@@ -118,30 +130,75 @@ const Action = Object.freeze({
ENTER: 16,
EXIT: 17,
ELSE: 18,
TYPE: 19,
TABLE: 20,
ELEM: 21,
});
const types = {
"void": 0x40,
"func": 0x60,
"i32": 0x7f,
"void": 0x40,
"func": 0x60,
"funcref": 0x70,
"f32": 0x7d,
"i32": 0x7f,
};
const opcodes = {
"block": 0x02,
"loop": 0x03,
"if": 0x04,
"else": 0x05,
"end": 0x0b,
"br": 0x0c,
"br_if": 0x0d,
"local.get": 0x20,
"local.set": 0x21,
"local.tee": 0x22,
"global.get": 0x23,
"global.set": 0x24,
"i32.const": 0x41,
"i32.gt_u": 0x4b,
"i32.mul": 0x6c,
"block": 0x02,
"loop": 0x03,
"if": 0x04,
"else": 0x05,
"end": 0x0b,
"br": 0x0c,
"br_if": 0x0d,
"call": 0x10,
"call_indirect": 0x11,
"drop": 0x0a,
"local.get": 0x20,
"local.set": 0x21,
"local.tee": 0x22,
"global.get": 0x23,
"global.set": 0x24,
"i32.load": 0x28,
"i32.load8_u": 0x2d,
"i32.store": 0x36,
"i32.store8": 0x3a,
"i32.const": 0x41,
"i64.const": 0x42,
"i32.eqz": 0x45,
"i32.eq": 0x46,
"i32.ne": 0x47,
"i32.lt_s": 0x48,
"i32.lt_u": 0x49,
"i32.gt_s": 0x4a,
"i32.gt_u": 0x4b,
"i32.le_s": 0x4c,
"i32.le_u": 0x4d,
"i32.ge_s": 0x4e,
"i32.ge_u": 0x4f,
"i32.add": 0x6a,
"i32.sub": 0x6b,
"i32.mul": 0x6c,
"i32.div_s": 0x6d,
"i32.rem_s": 0x6f,
"i32.and": 0x71,
"i32.or": 0x72,
"i32.xor": 0x73,
"i32.shl": 0x74,
"i32.shr_s": 0x75,
"i32.shr_u": 0x76,
"i64.or": 0x84,
"i64.shl": 0x86,
"i64.extend_i32_u": 0xad,
// Threads instructions
"memory.atomic.notify": [ 0xfe, 0x00 ],
"memory.atomic.wait32": [ 0xfe, 0x01 ],
"i32.atomic.load": [ 0xfe, 0x10 ],
"i32.atomic.load8_u": [ 0xfe, 0x12 ],
"i32.atomic.store": [ 0xfe, 0x17 ],
"i64.atomic.store": [ 0xfe, 0x18 ],
"i32.atomic.store8": [ 0xfe, 0x19 ],
};
const mem_flags = {
@@ -175,6 +232,10 @@ class Parser {
".utf8": State.UTF8,
".align": State.ALIGN,
".def": State.DEF_NAME,
".type": State.TYPE_NAME,
".table": State.TABLE_NAME,
".elem": State.ELEM_TABLE,
".zero": State.ZERO,
};
this.blocks = new Set(["block", "loop", "if"]);
this.handlers = {
@@ -206,6 +267,15 @@ class Parser {
[State.DEF_VALUE]: (token) => this.token_def_value(token),
[State.BLOCK_NAME]: (token) => this.token_block_name(token),
[State.BLOCK_TYPE]: (token) => this.token_block_type(token),
[State.TYPE_NAME]: (token) => this.token_type_name(token),
[State.TYPE_PARAM]: (token) => this.token_type_param(token),
[State.TYPE_RESULT]: (token) => this.token_type_result(token),
[State.TABLE_NAME]: (token) => this.token_table_name(token),
[State.TABLE_SIZE]: (token) => this.token_table_size(token),
[State.ELEM_TABLE]: (token) => this.token_elem_table(token),
[State.ELEM_ELEM]: (token) => this.token_elem_elem(token),
[State.ELEM_LABEL]: (token) => this.token_elem_label(token),
[State.ZERO]: (token) => this.token_zero(token),
};
this.results = [];
@@ -214,15 +284,14 @@ class Parser {
}
integer(token) {
let base;
let base, regex;
switch (token.slice(-1)) {
case "b": base = 2; break;
case "o": base = 8; break;
case "h": base = 16; break;
default: base = 10; break;
case "b": base = 2; regex = /^-?[01]+b$/; break;
case "o": base = 8; regex = /^-?[0-7]+o$/; break;
case "h": base = 16; regex = /^-?[0-9A-F]+h$/; break;
default: base = 10; regex = /^-?[0-9]+d?$/; break;
}
const x = parseInt(token, base);
return Number.isNaN(x) ? null : x;
return regex.test(token) ? parseInt(token, base) : null;
}
translate_code(token) {
@@ -260,7 +329,7 @@ class Parser {
if (opcode)
return { type: Action.APPEND, opcode };
const literal = this.integer(token);
if (literal)
if (literal != null)
return { type: Action.APPEND, literal };
return { type: Action.SYMBOL, symbol: token };
@@ -465,27 +534,21 @@ class Parser {
token_global_init(token) {
if (token == LINE_END) {
console.error(
"ERROR: Unexpected newline in .global: expected"
+ " initial value");
this.global = undefined;
this.global_name = undefined;
this.state = State.TOP;
this.global.init = 0;
} else {
const value = this.integer(token) ?? console.error(
`ERROR: Unexpected token ${token} in .global: expected`
+ " initial value");
const const_opcode = const_opcodes[this.global.type];
this.global.init = [ const_opcode, value, opcodes["end"] ];
const action = {
type: Action.GLOBAL,
global: { [this.global_name]: this.global }
};
this.global = undefined;
this.global_name = undefined;
this.state = State.TOP;
return action;
}
this.global.init = value;
}
const action = {
type: Action.GLOBAL,
global: { [this.global_name]: this.global }
};
this.global = undefined;
this.global_name = undefined;
this.state = State.TOP;
return action;
}
token_at_mem(token) {
@@ -495,15 +558,10 @@ class Parser {
token_at_addr(token) {
const value = this.integer(token);
if (value == null) {
console.error(
`ERROR: Unexpected token ${token} in .mem: `
+ "expected address");
this.at = undefined;
return;
}
this.at.addr = value;
if (value != null)
this.at.addr = value;
else
this.at.addr_symbol = token;
const action = { type: Action.AT, at: this.at };
this.at = undefined;
this.state = State.TOP;
@@ -522,7 +580,7 @@ class Parser {
} else {
if (value > 0xff)
console.error(`WARNING: Value ${token} is truncated`);
action.value = [ value & 0xff ];
action.value = value;
}
return action;
}
@@ -537,9 +595,9 @@ class Parser {
if (value == null) {
action.symbol = token;
} else {
if (value > 0xffff)
if (value > 0xffffffff)
console.error(`WARNING: Value ${token} is truncated`);
action.value = [ value & 0xff, (value >> 8) & 0xff ];
action.value = value;
}
return action;
}
@@ -553,8 +611,8 @@ class Parser {
`ERROR: Unexpected token ${token}, expected string`);
return;
}
const value = this.encoder.encode(token.string);
const action = { type: Action.DATA, size: value.length, value };
const bytes = this.encoder.encode(token.string);
const action = { type: Action.DATA, size: bytes.length, bytes };
return action;
}
@@ -636,6 +694,161 @@ class Parser {
return action;
}
token_type_name(token) {
if (token == LINE_END) {
console.error(
"ERROR: Unexpected end of line in .type, expected name");
this.state = State.TOP;
return;
}
this.type = { name: token, params: [] };
this.state = State.TYPE_PARAM;
}
token_type_param(token) {
if (token == LINE_END) {
console.error(
"ERROR: Unexpected end of line in .type, expected "
+ "parameter type");
this.type = undefined;
this.state = State.TOP;
return;
}
if (token == "result") {
this.type.results = [];
this.state = State.TYPE_RESULT;
return;
}
const type = types[token];
if (type == undefined) {
console.error(
`ERROR: Unexpected token ${token} in .type, expected `
+ "parameter type");
this.type = undefined;
this.state = State.TOP;
return;
}
this.type.params.push(type);
}
token_type_result(token) {
if (token == LINE_END) {
const action = { type: Action.TYPE, the_type: this.type };
this.type = undefined;
this.state = State.TOP;
return action;
}
const type = types[token];
if (type == undefined) {
console.error(
`ERROR: Unexpected token ${token} in .type, expected `
+ "result type");
this.type = undefined;
this.state = State.TOP;
return;
}
this.type.results.push(type);
}
token_table_name(token) {
if (token == LINE_END) {
console.error(
"ERROR: Unexpected end of line in .table, expected name");
this.state = State.TOP;
return;
}
this.table = { name: token };
this.state = State.TABLE_SIZE;
}
token_table_size(token) {
if (token == LINE_END) {
console.error(
"ERROR: Unexpected end of line in .table, expected size");
this.table = undefined;
this.state = State.TOP;
return;
}
const size = this.integer(token);
if (size == null) {
console.error(
`ERROR: Unexpected token ${token} in .table, expected size`);
this.table = undefined;
this.state = State.TOP;
return;
}
this.table.size = size;
const action = { type: Action.TABLE, table: this.table };
this.table = undefined;
this.state = State.TOP;
return action;
}
token_elem_table(token) {
if (token == LINE_END) {
console.error(
"ERROR: Unexpected end of line in .elem, expected "
+ "table name");
this.state = State.TOP;
return;
}
this.elem = { table: token };
this.state = State.ELEM_ELEM;
}
token_elem_elem(token) {
if (token == LINE_END) {
console.error(
"ERROR: Unexpected end of line in .elem, expected "
+ "element name");
this.state = State.TOP;
this.elem = undefined;
return;
}
this.elem.elem = token;
this.state = State.ELEM_LABEL;
}
token_elem_label(token) {
if (token != LINE_END)
this.elem.label = token;
const action = { type: Action.ELEM, elem: this.elem };
this.elem = undefined
this.state = State.TOP;
return action;
}
token_zero(token) {
if (token == LINE_END) {
console.error(
"ERROR: Unexpected newline in .zero, expected count")
this.state = State.TOP;
return;
}
const count = this.integer(token);
if (count == null) {
console.error(
`ERROR: Unexpected token ${token} in .zero, expected count`);
this.state = State.TOP;
return;
}
this.state = State.TOP;
return { type: Action.DATA, size: count, value: 0 }
}
mem_action() {
const action = {
type: Action.MEM,
@@ -650,9 +863,15 @@ class Parser {
*handle(src) {
let action;
for (const token of this.tokenizer.handle(src)) {
if (action = this.handlers[this.state](token)) {
yield action;
const handler = this.handlers[this.state];
if (handler == undefined) {
console.error(`ERROR: Unhandled state ${this.state}`);
this.state = State.TOP;
continue;
}
if (action = handler(token))
yield action;
}
}
}
@@ -661,16 +880,19 @@ const Section = Object.freeze({
TYPE: 0x01,
IMPORT: 0x02,
FUNC: 0x03,
TABLE: 0x04,
MEM: 0x05,
GLOBAL: 0x06,
EXPORT: 0x07,
ELEM: 0x09,
CODE: 0x0a,
DATA: 0x0b,
});
const Kind = Object.freeze({
FUNC: 0x00,
MEM: 0x02,
FUNC: 0x00,
MEM: 0x02,
GLOBAL: 0x03,
});
export class Assembler {
@@ -697,6 +919,9 @@ export class Assembler {
[Action.ENTER]: (action) => this.action_enter(action),
[Action.EXIT]: (action) => this.action_exit(action),
[Action.ELSE]: (action) => this.action_else(action),
[Action.TYPE]: (action) => this.action_type(action),
[Action.TABLE]: (action) => this.action_table(action),
[Action.ELEM]: (action) => this.action_elem(action),
};
this.exports = [];
@@ -708,18 +933,42 @@ export class Assembler {
this.data = [];
this.defs = {};
this.blocks = [];
this.types = [];
this.type_bindings = {};
this.tables = {};
this.unresolved = [];
}
action_append(action) {
const code = action.opcode != undefined
? [ action.opcode ]
? [ action.opcode ].flat()
: this.leb128(action.literal);
this.funcs[this.current_func].body.push(...code);
}
action_export(action) {
const index = Object.keys(this.funcs).indexOf(action.name);
this.exports[action.name] = { kind: Kind.FUNC, index };
const func_index = Object.keys(this.funcs).indexOf(action.name);
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) {
@@ -750,6 +999,9 @@ export class Assembler {
?? this.lookup_param(func, action.symbol)
?? this.lookup_local(func, action.symbol)
?? this.lookup_global(action.symbol)
?? this.lookup_table(action.symbol)
?? this.lookup_type(action.symbol)
?? this.lookup_func(action.symbol);
if (value == null) {
const def_value = this.lookup_def(action.symbol);
if (def_value == null) {
@@ -759,7 +1011,7 @@ export class Assembler {
}
func.body.push(...this.leb128(def_value));
} else {
func.body.push(value);
func.body.push(...this.uleb128(value));
}
}
@@ -791,16 +1043,38 @@ export class Assembler {
return;
}
this.pos.mem = mem;
this.pos.addr = action.at.addr;
this.pos.addr = action.at.addr
?? this.lookup_def(action.at.addr_symbol);
this.data.push({ loc: { ...this.pos }, data: [] })
}
action_data(action) {
const data = this.data.at(-1).data;
const value = action.value != null
? action.value
: this.le_bytes(this.lookup_def(action.symbol), action.size);
data.push(...value);
let bytes;
if (action.bytes != undefined) {
bytes = action.bytes;
} else {
let value = action.value;
if (value == undefined) {
if (action.symbol == undefined) {
console.error("ERROR: Invalid data action", action);
return;
}
value = this.lookup_def(action.symbol);
if (value == undefined) {
this.unresolved.push({
type: "data",
size: action.size,
symbol: action.symbol,
target: data,
offset: data.length,
});
value = 0;
}
}
bytes = this.le(value, action.size);
}
data.push(...bytes);
this.pos.addr += action.size;
}
@@ -840,10 +1114,40 @@ export class Assembler {
this.blocks.push(undefined);
}
action_type(action) {
const type = this.func_type(action.the_type);
const index = this.ensure_type(type);
this.type_bindings[action.the_type.name] = index;
}
action_table(action) {
this.tables[action.table.name] = {
size: action.table.size,
elems: [],
};
}
action_elem(action) {
const table = this.tables[action.elem.table];
const fn = Object.keys(this.funcs).indexOf(action.elem.elem);
if (fn == -1) {
console.error(`ERROR: ${action.elem.elem}: no such function`);
return;
}
const index = table.elems.push(fn) - 1;
if (action.elem.label)
this.defs[action.elem.label] = index;
}
push(chunk) {
const text = this.decoder.decode(chunk, { stream: true });
for (const action of this.parser.handle(text))
this.handlers[action.type](action);
for (const action of this.parser.handle(text)) {
const handler = this.handlers[action.type];
if (handler == undefined)
console.error("ERROR: Unhandled action", action);
else
handler(action);
}
}
lookup_block(symbol) {
@@ -867,35 +1171,67 @@ export class Assembler {
return index == -1 ? null : index;
}
lookup_table(symbol) {
const index = Object.keys(this.tables).indexOf(symbol);
return index == -1 ? null : index;
}
lookup_type(symbol) {
return this.type_bindings[symbol];
}
lookup_func(symbol) {
const index = Object.keys(this.funcs).indexOf(symbol);
return index == -1 ? null : index;
}
lookup_def(symbol) {
return this.defs[symbol];
}
le_bytes(value, count) {
let bytes = []
while (value != 0 && bytes.length < count) {
le(value, count) {
const bytes = []
while (value != 0) {
bytes.push(value & 0xff);
value >>= 8;
value >>>= 8;
}
if (count != undefined) {
while (bytes.length < count)
bytes.push(0);
}
return bytes;
}
leb128(x) {
const bytes = [];
while (true) {
const b = x & 0x7f;
x >>= 7;
if (x == 0 && (b & 0x40) == 0 || x == -1 && (b & 0x40) != 0) {
bytes.push(b);
return bytes;
}
bytes.push(b | 0x80);
}
}
uleb128(x) {
const bytes = [];
while (true) {
const b = x & 0x7f;
x >>>= 7;
if (x == 0) {
bytes.push(b);
return bytes;
}
bytes.push(b | 0x80);
}
while (bytes.length < count)
bytes.push(0);
return bytes;
}
wasm_section_type() {
const funcs = Object.values(this.funcs);
if (funcs.length == 0) return null;
const contents = funcs.map(({ params, results }) => {
const param_types = Object.values(params);
return [
types["func"],
param_types.length,
...param_types,
results.length,
...results,
];
});
return [ contents.length ].concat(...contents);
if (this.types.length == 0) return null;
return [ this.types.length ].concat(...this.types);
}
wasm_section_import() {
@@ -917,9 +1253,21 @@ export class Assembler {
}
wasm_section_func() {
const func_count = Object.entries(this.funcs).length;
if (func_count == 0) return null;
return [ func_count, ...Array(func_count).keys() ];
const types = Object.values(this.funcs).map(({type}) => type);
const count = types.length;
if (count == 0) return null;
return [ count, ...types ];
}
wasm_section_table() {
const sizes = Object.values(this.tables).map(({size}) => size);
if (sizes.length == 0) return null;
const contents = sizes.map((size) => [
types.funcref,
0x00,
this.uleb128(size),
]);
return [ sizes.length, contents ].flat(Infinity)
}
wasm_section_mem() {
@@ -935,8 +1283,13 @@ export class Assembler {
const globals = Object.values(this.globals);
if (globals.length == 0)
return null;
const contents = globals.map(
({ type, init }) => [ type, 1, ...init ]);
const contents = globals.map(({ type, init }) => [
type,
1,
const_opcodes[type],
...this.leb128(init),
opcodes["end"],
]);
return [ globals.length ].concat(...contents);
}
@@ -955,6 +1308,24 @@ export class Assembler {
return [ exports.length ].concat(...contents);
}
wasm_section_elem() {
const table_elems = Object.values(this.tables).map(
({elems}) => elems);
const count = table_elems.flat().length;
if (count == 0) return null;
const contents = table_elems.flatMap((elems, table) =>
elems.flatMap((fn, index) => [
table == 0 ? 0 : [ 2, table ],
opcodes["i32.const"],
index,
opcodes["end"],
1,
fn,
])
);
return [ count, contents ].flat();
}
wasm_section_code() {
const funcs = Object.values(this.funcs);
if (funcs.length == 0) return null;
@@ -962,24 +1333,20 @@ export class Assembler {
const local_types = Object.values(locals);
const local_count = local_types.length;
if (local_count == 0) {
return [
body.length + 2,
0,
...body,
opcodes["end"]
]
const full_body = [ 0, body, opcodes.end ].flat()
return [ full_body.length, full_body ].flat();
} else {
return [
body.length + local_count + 3,
local_count,
local_count,
...local_types,
...body,
opcodes["end"]
];
const groups = this.group(local_types);
const full_body = [
groups.length,
...groups.flat(),
body,
opcodes.end,
].flat();
return [ full_body.length, full_body ].flat();
}
});
return [ contents.length ].concat(...contents);
return [ contents.length, contents ].flat(Infinity);
}
wasm_section_data() {
@@ -990,30 +1357,37 @@ export class Assembler {
opcodes["i32.const"],
...this.leb128(loc.addr),
opcodes["end"],
data.length,
...this.uleb128(data.length),
...data,
]
});
return [ contents.length ].concat(...contents);
return [ contents.length, contents ].flat(Infinity);
}
wasm() {
this.resolve_refs();
this.resolve_func_types();
const template = [
[ Section.TYPE, () => this.wasm_section_type() ],
[ Section.IMPORT, () => this.wasm_section_import() ],
[ Section.FUNC, () => this.wasm_section_func() ],
[ Section.TABLE, () => this.wasm_section_table() ],
[ Section.MEM, () => this.wasm_section_mem() ],
[ Section.GLOBAL, () => this.wasm_section_global() ],
[ Section.EXPORT, () => this.wasm_section_export() ],
[ Section.ELEM, () => this.wasm_section_elem() ],
[ Section.CODE, () => this.wasm_section_code() ],
[ Section.DATA, () => this.wasm_section_data() ],
];
const sections = template.map(([ code, generator ]) => {
const body = generator();
return body == null ? [] : [ code, body.length, ...body ];
if (body == null)
return [];
return [ code, this.uleb128(body.length), body ];
});
return new Uint8Array(HEADER.concat(...sections));
return new Uint8Array([ HEADER, sections ].flat(Infinity));
}
mem_wasm({ flags, init, max }) {
@@ -1023,17 +1397,69 @@ export class Assembler {
return [ flags, init ];
}
leb128(x) {
const orig = x;
const bytes = [];
while (true) {
const b = x & 0x7f;
x >>= 7;
if (x == 0 && (b & 0x40) == 0 || x == -1 && (b & 0x40) != 0) {
bytes.push(b);
return bytes;
}
bytes.push(b | 0x80);
}
func_type({ params, results }) {
const param_types = params.length == undefined
? Object.values(params)
: params;
return [
types["func"],
param_types.length,
...param_types,
results.length,
...results,
];
}
array_eq(a, b) {
return a.length == b.length && a.every((x, i) => x == b[i]);
}
ensure_type(type) {
const index = this.types.findIndex((t) => this.array_eq(type, t));
return index != -1 ? index : this.types.push(type) - 1;
}
resolve_func_types() {
for (const func of Object.values(this.funcs))
func.type = this.ensure_type(this.func_type(func));
}
group(array) {
return array.reduce((acc, val) => {
const last = acc.at(-1);
if (last != undefined && last[1] == val)
++last[0]
else
acc.push([1, val]);
return acc;
}, []);
}
resolve_refs() {
const failed = [];
for (const ref of this.unresolved) {
if (ref.type != "data") {
console.error(
`ERROR: Unsupported ref type ${ref.type} for `
+ `symbol ${ref.symbol}`
);
failed.push(ref.symbol);
continue;
}
const value = this.defs[ref.symbol];
if (value == undefined) {
failed.push(ref.symbol);
continue;
}
const bytes = this.le(value, ref.size);
ref.target.splice(ref.offset, ref.size, ...bytes);
}
if (failed.length != 0) {
const failed_str = failed.join(" ");
console.error(`ERROR: Unable to resolve refs: ${failed_str}`);
}
}
}

29
boot.js
View File

@@ -1,7 +1,26 @@
import { Assembler } from './asm.js';
const assemble = (async () => {
const asm = new Assembler();
const resp = await fetch('wipforth.ws');
for await (const chunk of resp.body) {
asm.push(chunk);
}
return asm.wasm();
})();
self.onmessage = async (e) => {
const exports = { emu: { mem: e.data } };
const mod = await WebAssembly.instantiateStreaming(
fetch('wipforth.wasm'), exports)
mod.instance.exports.reset();
console.log('System halt');
switch (e.data.type) {
case "load":
const exports = { emu: { mem: e.data.mem } };
const wasm = await assemble;
self.mod = await WebAssembly.instantiate(wasm, exports);
await self.postMessage('ready');
break;
case "boot":
self.mod.instance.exports.reset();
console.log('System halt');
break;
}
};

View File

@@ -3,4 +3,4 @@ emu.js
index.html
prelude.f
styles.css
wipforth.wasm
wipforth.ws

27
emu.js
View File

@@ -11,6 +11,7 @@ const RXBUF_SIZE = 32;
const PERIPHS_SIZE = 81;
const POLL_INTERVAL_MS = 20;
const DOT_INTERVAL_MS = 120;
const COLS = 80;
const TAB_WIDTH = 8;
@@ -50,8 +51,29 @@ class Emulator {
document.addEventListener('keydown', (e) => this.handle_keydown(e));
window.addEventListener('resize', () => this.handle_resize());
this.worker = new Worker('boot.js');
this.worker.postMessage(this.mem);
this.prof = new Worker("prof.js");
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')
.then(res => res.text())
@@ -77,6 +99,7 @@ class Emulator {
if (!this.input_enable) {
const sysready = Atomics.load(this.mem_u8, SYSREADY);
if (sysready != 0) {
this.prof.postMessage({ type: "stop" });
this.input_enable = true;
this.flush_output();
document.getElementById('cursor').classList.add('blinking');

View File

@@ -249,8 +249,8 @@ CHAR . EMIT
\ Version number
0 CONSTANT VERSION-MAJOR
1 CONSTANT VERSION-MINOR
0 CONSTANT VERSION-PATCH
2 CONSTANT VERSION-MINOR
1 CONSTANT VERSION-PATCH
: PRINT-VERSION
CHAR v EMIT VERSION-MAJOR .

48
prof.js Normal file
View 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));
}
};

View File

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

View File

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

File diff suppressed because it is too large Load Diff

2098
wipforth.ws Normal file

File diff suppressed because it is too large Load Diff

16
words.js Normal file
View 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));