From 554d91864044b9123d3b6295134772a69b6a274c Mon Sep 17 00:00:00 2001 From: Camden Dixie O'Brien Date: Mon, 9 Mar 2026 19:52:07 +0000 Subject: [PATCH 01/72] Create initial scaffolding for JS WASM assembler --- README.md | 2 +- asm.js | 258 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 259 insertions(+), 1 deletion(-) create mode 100644 asm.js diff --git a/README.md b/README.md index 7b79c65..6cfa3fc 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ emulated in JavaScript. You'll need: -- [WABT](https://github.com/WebAssembly/wabt) +- [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) diff --git a/asm.js b/asm.js new file mode 100644 index 0000000..3f71387 --- /dev/null +++ b/asm.js @@ -0,0 +1,258 @@ +const HEADER = [ + 0x00, 0x61, 0x73, 0x6d, + 0x01, 0x00, 0x00, 0x00 +]; + +const LINE_END = "\n" + +class Tokenizer { + constructor() { + this.delims = new Set([" ", "\r", "\n", "\t"]); + this.skips = new Set([" ", "\r", "\t"]); + this.comment_start = ";" + this.buffer = []; + this.comment = false; + } + + skip() { + const idx = this.buffer.findIndex((cp) => !this.skips.has(cp)); + this.buffer = idx == -1 ? [] : this.buffer.slice(idx); + } + + next() { + this.skip(); + if (this.buffer[0] == LINE_END) + return this.buffer.shift(); + + const idx = this.buffer.findIndex((cp) => this.delims.has(cp)); + if (idx != -1) { + const token = this.buffer.slice(0, idx).join(""); + this.buffer = this.buffer.slice(idx); + return token; + } + } + + *handle(src) { + this.buffer.push(...src); + let token; + while (token = this.next()) { + if (token == this.comment_start) + this.comment = true; + else if (this.comment && token == LINE_END) + this.comment = false; + else if (!this.comment) + yield token; + } + } +} + +const State = Object.freeze({ + TOP: 0, + EXPORT: 1, + FUNC: 2, + RESULT: 3, +}); + +const Action = Object.freeze({ + APPEND: 0, + EXPORT: 1, + FUNC: 2, + RESULT: 3, +}); + +const types = { + "func": 0x60, + "i32": 0x7f, +}; + +const opcodes = { + "end": 0x0b, + "i32.const": 0x41, + "i32.mul": 0x6c, +}; + +class Parser { + constructor() { + this.tokens = []; + this.tokenizer = new Tokenizer(); + this.state = State.TOP; + this.directives = { + ".export": State.EXPORT, + ".func": State.FUNC, + ".result": State.RESULT, + }; + this.handlers = { + [State.TOP]: (token) => this.token_top(token), + [State.EXPORT]: (token) => this.token_export(token), + [State.FUNC]: (token) => this.token_func(token), + [State.RESULT]: (token) => this.token_result(token), + }; + + this.results = []; + } + + translate_code(token) { + return opcodes[token] ?? parseInt(token); + } + + translate_type(token) { + return types[token]; + } + + token_top(token) { + if (token == LINE_END) + return; + let state; + if (state = this.directives[token]) { + this.state = state; + return; + } + const code = this.translate_code(token); + return { type: Action.APPEND, code }; + } + + token_export(token) { + this.state = State.TOP; + return { type: Action.EXPORT, name: token }; + } + + token_func(token) { + this.state = State.TOP; + return { type: Action.FUNC, name: token }; + } + + token_result(token) { + if (token == LINE_END) { + const action = { type: Action.RESULT, results: this.results }; + this.state = State.TOP; + this.results = []; + return action; + } else { + this.results.push(this.translate_type(token)); + } + } + + *handle(src) { + let action; + for (const token of this.tokenizer.handle(src)) { + if (action = this.handlers[this.state](token)) + yield action; + } + } +} + +const Section = Object.freeze({ + TYPE: 0x01, + FUNC: 0x03, + EXPORT: 0x07, + CODE: 0x0a +}); + +export class Assembler { + constructor() { + this.encoder = new TextEncoder("utf-8"); + this.decoder = new TextDecoder("utf-8"); + this.parser = new Parser(); + this.handlers = { + [Action.APPEND]: (action) => this.action_append(action), + [Action.EXPORT]: (action) => this.action_export(action), + [Action.FUNC]: (action) => this.action_func(action), + [Action.RESULT]: (action) => this.action_result(action), + }; + + this.exports = []; + this.funcs = {} + } + + action_append(action) { + this.funcs[this.current_func].body.push(action.code); + } + + action_export(action) { + const index = Object.keys(this.funcs).indexOf(action.name); + this.exports[action.name] = { kind: 0x00, index }; + } + + action_func(action) { + this.funcs[action.name] = { + params: {}, + results: [], + locals: {}, + body: [], + } + this.current_func = action.name; + } + + action_result(action) { + this.funcs[this.current_func].results.push(...action.results); + } + + push(chunk) { + const text = this.decoder.decode(chunk, { stream: true }); + for (const action of this.parser.handle(text)) + this.handlers[action.type](action); + } + + wasm_section_type() { + const funcs = Object.values(this.funcs); + 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); + } + + wasm_section_func() { + const func_count = Object.entries(this.funcs).length; + return [ func_count, ...Array(func_count).keys() ]; + } + + wasm_section_export() { + const exports = Object.entries(this.exports); + const contents = exports.map(([ name, { kind, index }]) => { + const name_utf8 = this.encoder.encode(name); + return [ + name_utf8.length, + ...name_utf8, + kind, + index, + ]; + }); + return [ exports.length ].concat(...contents); + } + + wasm_section_code() { + const funcs = Object.values(this.funcs); + const contents = funcs.map(({ body, locals }) => { + const local_count = Object.entries(locals).length; + return [ + body.length + 2, + local_count, + ...body, + opcodes["end"] + ] + }); + return [ contents.length ].concat(...contents); + } + + wasm() { + const template = [ + [ Section.TYPE, () => this.wasm_section_type() ], + [ Section.FUNC, () => this.wasm_section_func() ], + [ Section.EXPORT, () => this.wasm_section_export() ], + [ Section.CODE, () => this.wasm_section_code() ], + ]; + const sections = template.map(([ code, generator ]) => { + const body = generator(); + return [ code, body.length, ...body ]; + }); + + return new Uint8Array(HEADER.concat(...sections)); + } +} -- 2.34.1 From 6a4877d52cc3941d2658e4e23c2f8757203122ff Mon Sep 17 00:00:00 2001 From: Camden Dixie O'Brien Date: Mon, 9 Mar 2026 23:46:55 +0000 Subject: [PATCH 02/72] Implement .param directive --- asm.js | 54 ++++++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 46 insertions(+), 8 deletions(-) diff --git a/asm.js b/asm.js index 3f71387..c687de7 100644 --- a/asm.js +++ b/asm.js @@ -47,10 +47,12 @@ class Tokenizer { } const State = Object.freeze({ - TOP: 0, - EXPORT: 1, - FUNC: 2, - RESULT: 3, + TOP: 0, + EXPORT: 1, + FUNC: 2, + RESULT: 3, + PARAM_NAME: 4, + PARAM_TYPE: 5, }); const Action = Object.freeze({ @@ -58,6 +60,7 @@ const Action = Object.freeze({ EXPORT: 1, FUNC: 2, RESULT: 3, + PARAM: 4, }); const types = { @@ -67,6 +70,7 @@ const types = { const opcodes = { "end": 0x0b, + "local.get": 0x20, "i32.const": 0x41, "i32.mul": 0x6c, }; @@ -80,15 +84,19 @@ class Parser { ".export": State.EXPORT, ".func": State.FUNC, ".result": State.RESULT, + ".param": State.PARAM_NAME, }; this.handlers = { - [State.TOP]: (token) => this.token_top(token), - [State.EXPORT]: (token) => this.token_export(token), - [State.FUNC]: (token) => this.token_func(token), - [State.RESULT]: (token) => this.token_result(token), + [State.TOP]: (token) => this.token_top(token), + [State.EXPORT]: (token) => this.token_export(token), + [State.FUNC]: (token) => this.token_func(token), + [State.RESULT]: (token) => this.token_result(token), + [State.PARAM_NAME]: (token) => this.token_param_name(token), + [State.PARAM_TYPE]: (token) => this.token_param_type(token), }; this.results = []; + this.params = {}; } translate_code(token) { @@ -132,6 +140,31 @@ class Parser { } } + token_param_name(token) { + if (token == LINE_END) { + const action = { type: Action.PARAM, params: this.params }; + this.state = State.TOP; + this.params = {}; + return action; + } else { + this.current_param = token; + this.state = State.PARAM_TYPE; + } + } + + token_param_type(token) { + if (token == LINE_END) { + console.error( + "ERROR: Unexpected newline in .params: expected type"); + this.state = State.TOP; + this.params = {}; + } else { + this.params[this.current_param] = types[token]; + this.current_param = undefined; + this.state = State.PARAM_NAME; + } + } + *handle(src) { let action; for (const token of this.tokenizer.handle(src)) { @@ -158,6 +191,7 @@ export class Assembler { [Action.EXPORT]: (action) => this.action_export(action), [Action.FUNC]: (action) => this.action_func(action), [Action.RESULT]: (action) => this.action_result(action), + [Action.PARAM]: (action) => this.action_param(action), }; this.exports = []; @@ -187,6 +221,10 @@ export class Assembler { this.funcs[this.current_func].results.push(...action.results); } + action_param(action) { + Object.assign(this.funcs[this.current_func].params, action.params); + } + push(chunk) { const text = this.decoder.decode(chunk, { stream: true }); for (const action of this.parser.handle(text)) -- 2.34.1 From 75600d0568d39eb0dd8f61621cead8dbf2ef09f5 Mon Sep 17 00:00:00 2001 From: Camden Dixie O'Brien Date: Tue, 10 Mar 2026 00:11:47 +0000 Subject: [PATCH 03/72] Add symbol resolution (params only) --- asm.js | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/asm.js b/asm.js index c687de7..87ce8fa 100644 --- a/asm.js +++ b/asm.js @@ -61,6 +61,7 @@ const Action = Object.freeze({ FUNC: 2, RESULT: 3, PARAM: 4, + SYMBOL: 5, }); const types = { @@ -99,8 +100,13 @@ class Parser { this.params = {}; } + integer(token) { + const x = parseInt(token); + return Number.isNaN(x) ? null : x; + } + translate_code(token) { - return opcodes[token] ?? parseInt(token); + return opcodes[token] ?? this.integer(token); } translate_type(token) { @@ -116,7 +122,10 @@ class Parser { return; } const code = this.translate_code(token); - return { type: Action.APPEND, code }; + if (code) + return { type: Action.APPEND, code }; + else + return { type: Action.SYMBOL, symbol: token }; } token_export(token) { @@ -155,7 +164,7 @@ class Parser { token_param_type(token) { if (token == LINE_END) { console.error( - "ERROR: Unexpected newline in .params: expected type"); + "ERROR: Unexpected newline in .param: expected type"); this.state = State.TOP; this.params = {}; } else { @@ -192,6 +201,7 @@ export class Assembler { [Action.FUNC]: (action) => this.action_func(action), [Action.RESULT]: (action) => this.action_result(action), [Action.PARAM]: (action) => this.action_param(action), + [Action.SYMBOL]: (action) => this.action_symbol(action), }; this.exports = []; @@ -225,6 +235,12 @@ export class Assembler { Object.assign(this.funcs[this.current_func].params, action.params); } + action_symbol(action) { + const func = this.funcs[this.current_func]; + const index = Object.keys(func.params).indexOf(action.symbol); + func.body.push(index); + } + push(chunk) { const text = this.decoder.decode(chunk, { stream: true }); for (const action of this.parser.handle(text)) -- 2.34.1 From 510a74aa04be15577436f8482d9dd82b266e38b7 Mon Sep 17 00:00:00 2001 From: Camden Dixie O'Brien Date: Tue, 10 Mar 2026 00:14:24 +0000 Subject: [PATCH 04/72] Add base suffix for integers --- asm.js | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/asm.js b/asm.js index 87ce8fa..6a11618 100644 --- a/asm.js +++ b/asm.js @@ -101,7 +101,14 @@ class Parser { } integer(token) { - const x = parseInt(token); + let base; + switch (token.slice(-1)) { + case "b": base = 2; break; + case "o": base = 8; break; + case "h": base = 16; break; + default: base = 10; break; + } + const x = parseInt(token, base); return Number.isNaN(x) ? null : x; } -- 2.34.1 From 77f6d57e1b183586fafec4010d2f39c9e734ca88 Mon Sep 17 00:00:00 2001 From: Camden Dixie O'Brien Date: Tue, 10 Mar 2026 00:37:05 +0000 Subject: [PATCH 05/72] Add support for locals --- asm.js | 60 +++++++++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 57 insertions(+), 3 deletions(-) diff --git a/asm.js b/asm.js index 6a11618..11dbc8f 100644 --- a/asm.js +++ b/asm.js @@ -53,6 +53,8 @@ const State = Object.freeze({ RESULT: 3, PARAM_NAME: 4, PARAM_TYPE: 5, + LOCAL_NAME: 6, + LOCAL_TYPE: 7, }); const Action = Object.freeze({ @@ -62,6 +64,7 @@ const Action = Object.freeze({ RESULT: 3, PARAM: 4, SYMBOL: 5, + LOCAL: 6, }); const types = { @@ -72,6 +75,8 @@ const types = { const opcodes = { "end": 0x0b, "local.get": 0x20, + "local.set": 0x21, + "local.tee": 0x22, "i32.const": 0x41, "i32.mul": 0x6c, }; @@ -86,6 +91,7 @@ class Parser { ".func": State.FUNC, ".result": State.RESULT, ".param": State.PARAM_NAME, + ".local": State.LOCAL_NAME, }; this.handlers = { [State.TOP]: (token) => this.token_top(token), @@ -94,10 +100,13 @@ class Parser { [State.RESULT]: (token) => this.token_result(token), [State.PARAM_NAME]: (token) => this.token_param_name(token), [State.PARAM_TYPE]: (token) => this.token_param_type(token), + [State.LOCAL_NAME]: (token) => this.token_local_name(token), + [State.LOCAL_TYPE]: (token) => this.token_local_type(token), }; this.results = []; this.params = {}; + this.locals = {}; } integer(token) { @@ -181,6 +190,31 @@ class Parser { } } + token_local_name(token) { + if (token == LINE_END) { + const action = { type: Action.LOCAL, locals: this.locals }; + this.state = State.TOP; + this.locals = {}; + return action; + } else { + this.current_local = token; + this.state = State.LOCAL_TYPE; + } + } + + token_local_type(token) { + if (token == LINE_END) { + console.error( + "ERROR: Unexpected newline in .local: expected type"); + this.state = State.TOP; + this.locals = {}; + } else { + this.locals[this.current_local] = types[token]; + this.current_local = undefined; + this.state = State.LOCAL_NAME; + } + } + *handle(src) { let action; for (const token of this.tokenizer.handle(src)) { @@ -209,6 +243,7 @@ export class Assembler { [Action.RESULT]: (action) => this.action_result(action), [Action.PARAM]: (action) => this.action_param(action), [Action.SYMBOL]: (action) => this.action_symbol(action), + [Action.LOCAL]: (action) => this.action_local(action), }; this.exports = []; @@ -242,9 +277,14 @@ export class Assembler { Object.assign(this.funcs[this.current_func].params, action.params); } + action_local(action) { + Object.assign(this.funcs[this.current_func].locals, action.locals); + } + action_symbol(action) { const func = this.funcs[this.current_func]; - const index = Object.keys(func.params).indexOf(action.symbol); + const index = this.lookup_param(func, action.symbol) + ?? this.lookup_local(func, action.symbol); func.body.push(index); } @@ -254,6 +294,17 @@ export class Assembler { this.handlers[action.type](action); } + lookup_param(func, symbol) { + const index = Object.keys(func.params).indexOf(symbol); + return index == -1 ? null : index; + } + + lookup_local(func, symbol) { + const param_count = Object.entries(func.params).length; + const index = param_count + Object.keys(func.locals).indexOf(symbol); + return index == -1 ? null : index; + } + wasm_section_type() { const funcs = Object.values(this.funcs); const contents = funcs.map(({ params, results }) => { @@ -291,10 +342,13 @@ export class Assembler { wasm_section_code() { const funcs = Object.values(this.funcs); const contents = funcs.map(({ body, locals }) => { - const local_count = Object.entries(locals).length; + const local_types = Object.values(locals); + const local_count = local_types.length; return [ - body.length + 2, + body.length + local_count + 3, local_count, + local_count, + ...local_types, ...body, opcodes["end"] ] -- 2.34.1 From 5a3084dd16325cbfe9ca6794391e9f7846b1fb46 Mon Sep 17 00:00:00 2001 From: Camden Dixie O'Brien Date: Tue, 10 Mar 2026 15:33:41 +0000 Subject: [PATCH 06/72] Implement .mem directive --- asm.js | 121 +++++++++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 110 insertions(+), 11 deletions(-) diff --git a/asm.js b/asm.js index 11dbc8f..9cdb05e 100644 --- a/asm.js +++ b/asm.js @@ -47,14 +47,18 @@ class Tokenizer { } const State = Object.freeze({ - TOP: 0, - EXPORT: 1, - FUNC: 2, - RESULT: 3, - PARAM_NAME: 4, - PARAM_TYPE: 5, - LOCAL_NAME: 6, - LOCAL_TYPE: 7, + TOP: 0, + EXPORT: 1, + FUNC: 2, + RESULT: 3, + PARAM_NAME: 4, + PARAM_TYPE: 5, + LOCAL_NAME: 6, + LOCAL_TYPE: 7, + MEM_NAME: 8, + MEM_INITIAL: 9, + MEM_MAX: 10, + MEM_FLAGS: 11, }); const Action = Object.freeze({ @@ -65,6 +69,7 @@ const Action = Object.freeze({ PARAM: 4, SYMBOL: 5, LOCAL: 6, + MEM: 7, }); const types = { @@ -81,6 +86,12 @@ const opcodes = { "i32.mul": 0x6c, }; +const mem_flags = { + "max": 1, + "shared": 2, + "64": 4, +}; + class Parser { constructor() { this.tokens = []; @@ -92,6 +103,7 @@ class Parser { ".result": State.RESULT, ".param": State.PARAM_NAME, ".local": State.LOCAL_NAME, + ".mem": State.MEM_NAME, }; this.handlers = { [State.TOP]: (token) => this.token_top(token), @@ -102,6 +114,10 @@ class Parser { [State.PARAM_TYPE]: (token) => this.token_param_type(token), [State.LOCAL_NAME]: (token) => this.token_local_name(token), [State.LOCAL_TYPE]: (token) => this.token_local_type(token), + [State.MEM_NAME]: (token) => this.token_mem_name(token), + [State.MEM_INIT]: (token) => this.token_mem_init(token), + [State.MEM_MAX]: (token) => this.token_mem_max(token), + [State.MEM_FLAGS]: (token) => this.token_mem_flags(token), }; this.results = []; @@ -215,6 +231,67 @@ class Parser { } } + token_mem_name(token) { + if (token == LINE_END) { + console.error( + "ERROR: Unexpected newline in .mem: expected name"); + this.state = State.TOP; + } else { + this.mem = { flags: 0 }; + this.mem_name = token; + this.state = State.MEM_INIT; + } + } + + token_mem_init(token) { + if (token == LINE_END) { + console.error( + "ERROR: Unexpected newline in .mem: expected initial size"); + this.mem = undefined; + this.mem_name = undefined; + this.state = State.TOP; + } else { + this.mem.init = this.integer(token) ?? console.error( + `ERROR: Invalid initial size {token} in .mem`); + this.state = State.MEM_MAX; + } + } + + token_mem_max(token) { + if (token == LINE_END) { + return this.mem_action(); + } else { + this.mem.max = this.integer(token) ?? console.error( + `ERROR: Invalid maximum size {token} in .mem`); + this.mem.flags |= mem_flags.max; + this.state = State.MEM_FLAGS; + } + } + + token_mem_flags(token) { + if (token == LINE_END) { + return this.mem_action(); + } else { + for (const flag of token.split(",")) { + this.mem.flags |= mem_flags[flag] ?? console.error( + `ERROR: Invalid flag {flag} in .mem`); + } + this.state = State.TOP; + return this.mem_action(); + } + } + + mem_action() { + const action = { + type: Action.MEM, + mem: { [this.mem_name]: { ...this.mem } } + }; + this.mem = undefined; + this.mem_name = undefined; + this.state = State.TOP; + return action; + } + *handle(src) { let action; for (const token of this.tokenizer.handle(src)) { @@ -227,8 +304,9 @@ class Parser { const Section = Object.freeze({ TYPE: 0x01, FUNC: 0x03, + MEM: 0x05, EXPORT: 0x07, - CODE: 0x0a + CODE: 0x0a, }); export class Assembler { @@ -244,10 +322,12 @@ export class Assembler { [Action.PARAM]: (action) => this.action_param(action), [Action.SYMBOL]: (action) => this.action_symbol(action), [Action.LOCAL]: (action) => this.action_local(action), + [Action.MEM]: (action) => this.action_mem(action), }; this.exports = []; - this.funcs = {} + this.funcs = {}; + this.mems = {}; } action_append(action) { @@ -288,6 +368,10 @@ export class Assembler { func.body.push(index); } + action_mem(action) { + Object.assign(this.mems, action.mem); + } + push(chunk) { const text = this.decoder.decode(chunk, { stream: true }); for (const action of this.parser.handle(text)) @@ -325,6 +409,20 @@ export class Assembler { return [ func_count, ...Array(func_count).keys() ]; } + wasm_section_mem() { + const mems = Object.values(this.mems); + if (mems.length == 0) + return null; + + const contents = mems.map(({ init, max, flags }) => { + if (flags & mem_flags.max) + return [ flags, init, max ]; + else + return [ flags, init ]; + }); + return [ mems.length ].concat(...contents); + } + wasm_section_export() { const exports = Object.entries(this.exports); const contents = exports.map(([ name, { kind, index }]) => { @@ -360,12 +458,13 @@ export class Assembler { const template = [ [ Section.TYPE, () => this.wasm_section_type() ], [ Section.FUNC, () => this.wasm_section_func() ], + [ Section.MEM, () => this.wasm_section_mem() ], [ Section.EXPORT, () => this.wasm_section_export() ], [ Section.CODE, () => this.wasm_section_code() ], ]; const sections = template.map(([ code, generator ]) => { const body = generator(); - return [ code, body.length, ...body ]; + return body == null ? [] : [ code, body.length, ...body ]; }); return new Uint8Array(HEADER.concat(...sections)); -- 2.34.1 From 672a453f6cfe71891290496d6a7fcbe84b43e04b Mon Sep 17 00:00:00 2001 From: Camden Dixie O'Brien Date: Tue, 10 Mar 2026 17:53:58 +0000 Subject: [PATCH 07/72] Add string support to tokenizer --- asm.js | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/asm.js b/asm.js index 9cdb05e..a6683fd 100644 --- a/asm.js +++ b/asm.js @@ -9,9 +9,11 @@ class Tokenizer { constructor() { this.delims = new Set([" ", "\r", "\n", "\t"]); this.skips = new Set([" ", "\r", "\t"]); - this.comment_start = ";" + this.comment_start = ";"; + this.string_quote = '"'; this.buffer = []; this.comment = false; + this.string = false; } skip() { @@ -19,11 +21,31 @@ class Tokenizer { this.buffer = idx == -1 ? [] : this.buffer.slice(idx); } + next_string() { + const idx = this.buffer.findIndex((cp) => cp == this.string_quote); + if (idx == -1) { + this.string = true; + } else { + const string = this.buffer.slice(0, idx).join(""); + this.buffer = this.buffer.slice(idx + 1); + this.string = false; + return { string: string }; + } + } + next() { + if (this.string) + return this.next_string(); + this.skip(); if (this.buffer[0] == LINE_END) return this.buffer.shift(); + if (this.buffer[0] == this.string_quote) { + this.buffer.shift(); + return this.next_string(); + } + const idx = this.buffer.findIndex((cp) => this.delims.has(cp)); if (idx != -1) { const token = this.buffer.slice(0, idx).join(""); -- 2.34.1 From 1c4b9f850a23b48d4a467da96877077849fdf1a9 Mon Sep 17 00:00:00 2001 From: Camden Dixie O'Brien Date: Tue, 10 Mar 2026 17:54:27 +0000 Subject: [PATCH 08/72] Add support for imports (memory only) --- asm.js | 165 +++++++++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 132 insertions(+), 33 deletions(-) diff --git a/asm.js b/asm.js index a6683fd..e6b2ba3 100644 --- a/asm.js +++ b/asm.js @@ -69,18 +69,21 @@ class Tokenizer { } const State = Object.freeze({ - TOP: 0, - EXPORT: 1, - FUNC: 2, - RESULT: 3, - PARAM_NAME: 4, - PARAM_TYPE: 5, - LOCAL_NAME: 6, - LOCAL_TYPE: 7, - MEM_NAME: 8, - MEM_INITIAL: 9, - MEM_MAX: 10, - MEM_FLAGS: 11, + TOP: 0, + EXPORT: 1, + FUNC: 2, + RESULT: 3, + PARAM_NAME: 4, + PARAM_TYPE: 5, + LOCAL_NAME: 6, + LOCAL_TYPE: 7, + MEM_NAME: 8, + MEM_INITIAL: 9, + MEM_MAX: 10, + MEM_FLAGS: 11, + IMPORT_NAME: 12, + IMPORT_MOD: 13, + IMPORT_FIELD: 14 }); const Action = Object.freeze({ @@ -92,6 +95,7 @@ const Action = Object.freeze({ SYMBOL: 5, LOCAL: 6, MEM: 7, + IMPORT: 8, }); const types = { @@ -126,20 +130,24 @@ class Parser { ".param": State.PARAM_NAME, ".local": State.LOCAL_NAME, ".mem": State.MEM_NAME, + ".import": State.IMPORT_NAME, }; this.handlers = { - [State.TOP]: (token) => this.token_top(token), - [State.EXPORT]: (token) => this.token_export(token), - [State.FUNC]: (token) => this.token_func(token), - [State.RESULT]: (token) => this.token_result(token), - [State.PARAM_NAME]: (token) => this.token_param_name(token), - [State.PARAM_TYPE]: (token) => this.token_param_type(token), - [State.LOCAL_NAME]: (token) => this.token_local_name(token), - [State.LOCAL_TYPE]: (token) => this.token_local_type(token), - [State.MEM_NAME]: (token) => this.token_mem_name(token), - [State.MEM_INIT]: (token) => this.token_mem_init(token), - [State.MEM_MAX]: (token) => this.token_mem_max(token), - [State.MEM_FLAGS]: (token) => this.token_mem_flags(token), + [State.TOP]: (token) => this.token_top(token), + [State.EXPORT]: (token) => this.token_export(token), + [State.FUNC]: (token) => this.token_func(token), + [State.RESULT]: (token) => this.token_result(token), + [State.PARAM_NAME]: (token) => this.token_param_name(token), + [State.PARAM_TYPE]: (token) => this.token_param_type(token), + [State.LOCAL_NAME]: (token) => this.token_local_name(token), + [State.LOCAL_TYPE]: (token) => this.token_local_type(token), + [State.MEM_NAME]: (token) => this.token_mem_name(token), + [State.MEM_INIT]: (token) => this.token_mem_init(token), + [State.MEM_MAX]: (token) => this.token_mem_max(token), + [State.MEM_FLAGS]: (token) => this.token_mem_flags(token), + [State.IMPORT_NAME]: (token) => this.token_import_name(token), + [State.IMPORT_MOD]: (token) => this.token_import_mod(token), + [State.IMPORT_FIELD]: (token) => this.token_import_field(token), }; this.results = []; @@ -303,6 +311,55 @@ class Parser { } } + token_import_name(token) { + if (token == LINE_END) { + console.error( + "ERROR: Unexpected end of line in .import: expected name"); + this.state = State.TOP; + } else { + this.import = { name: token }; + this.state = State.IMPORT_MOD; + } + } + + token_import_mod(token) { + if (token == LINE_END) { + console.error( + "ERROR: Unexpected end of line in .import: expected name"); + this.import = undefined; + this.state = State.TOP; + } else if (token.string == undefined) { + console.error( + `ERROR: Unexpected token {token} in .import: expected` + + " module string"); + this.import = undefined; + this.state = State.TOP; + } else { + this.import.mod = token.string; + this.state = State.IMPORT_FIELD; + } + } + + token_import_field(token) { + if (token == LINE_END) { + console.error( + "ERROR: Unexpected end of line in .import: expected name"); + this.import = undefined; + this.state = State.TOP; + } else if (token.string == undefined) { + console.error( + "ERROR: Unexpected token in .import: expected field string"); + this.import = undefined; + this.state = State.TOP; + } else { + this.import.field = token.string; + const action = { type: Action.IMPORT, import: this.import }; + this.import = undefined; + this.state = State.TOP; + return action; + } + } + mem_action() { const action = { type: Action.MEM, @@ -325,12 +382,18 @@ class Parser { const Section = Object.freeze({ TYPE: 0x01, + IMPORT: 0x02, FUNC: 0x03, MEM: 0x05, EXPORT: 0x07, CODE: 0x0a, }); +const Kind = Object.freeze({ + FUNC: 0x00, + MEM: 0x02, +}); + export class Assembler { constructor() { this.encoder = new TextEncoder("utf-8"); @@ -345,11 +408,13 @@ export class Assembler { [Action.SYMBOL]: (action) => this.action_symbol(action), [Action.LOCAL]: (action) => this.action_local(action), [Action.MEM]: (action) => this.action_mem(action), + [Action.IMPORT]: (action) => this.action_import(action), }; this.exports = []; this.funcs = {}; this.mems = {}; + this.imports = []; } action_append(action) { @@ -358,7 +423,7 @@ export class Assembler { action_export(action) { const index = Object.keys(this.funcs).indexOf(action.name); - this.exports[action.name] = { kind: 0x00, index }; + this.exports[action.name] = { kind: Kind.FUNC, index }; } action_func(action) { @@ -394,6 +459,19 @@ export class Assembler { Object.assign(this.mems, action.mem); } + action_import(action) { + const mem = this.mems[action.import.name]; + mem.imported = true; + this.imports.push({ + mod: action.import.mod, + field: action.import.field, + kind: Kind.MEM, + flags: mem.flags, + init: mem.init, + max: mem.max, + }) + } + push(chunk) { const text = this.decoder.decode(chunk, { stream: true }); for (const action of this.parser.handle(text)) @@ -426,22 +504,35 @@ export class Assembler { return [ contents.length ].concat(...contents); } + wasm_section_import() { + if (this.imports.length == 0) + return null; + const contents = this.imports.map((imp) => { + const mod_utf8 = this.encoder.encode(imp.mod); + const field_utf8 = this.encoder.encode(imp.field); + return [ + mod_utf8.length, + ...mod_utf8, + field_utf8.length, + ...field_utf8, + imp.kind, + ...this.mem_wasm(imp), + ]; + }); + return [ this.imports.length ].concat(...contents); + } + wasm_section_func() { const func_count = Object.entries(this.funcs).length; return [ func_count, ...Array(func_count).keys() ]; } wasm_section_mem() { - const mems = Object.values(this.mems); + const mems = Object.values(this.mems).filter( + ({imported}) => !imported); if (mems.length == 0) return null; - - const contents = mems.map(({ init, max, flags }) => { - if (flags & mem_flags.max) - return [ flags, init, max ]; - else - return [ flags, init ]; - }); + const contents = mems.map((mem) => this.mem_wasm(mem)); return [ mems.length ].concat(...contents); } @@ -479,6 +570,7 @@ export class Assembler { wasm() { const template = [ [ Section.TYPE, () => this.wasm_section_type() ], + [ Section.IMPORT, () => this.wasm_section_import() ], [ Section.FUNC, () => this.wasm_section_func() ], [ Section.MEM, () => this.wasm_section_mem() ], [ Section.EXPORT, () => this.wasm_section_export() ], @@ -491,4 +583,11 @@ export class Assembler { return new Uint8Array(HEADER.concat(...sections)); } + + mem_wasm({ flags, init, max }) { + if (flags & mem_flags.max) + return [ flags, init, max ]; + else + return [ flags, init ]; + } } -- 2.34.1 From 118e6af896d404f4a165f9bcbd697304fc6650fe Mon Sep 17 00:00:00 2001 From: Camden Dixie O'Brien Date: Tue, 10 Mar 2026 19:56:13 +0000 Subject: [PATCH 09/72] Add support for globals (mutable only) I'm planning on adding symbolic constants to the assembler, so I won't really have much use for immutable globals. --- asm.js | 135 +++++++++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 118 insertions(+), 17 deletions(-) diff --git a/asm.js b/asm.js index e6b2ba3..9882baa 100644 --- a/asm.js +++ b/asm.js @@ -83,7 +83,10 @@ const State = Object.freeze({ MEM_FLAGS: 11, IMPORT_NAME: 12, IMPORT_MOD: 13, - IMPORT_FIELD: 14 + IMPORT_FIELD: 14, + GLOBAL_NAME: 15, + GLOBAL_TYPE: 16, + GLOBAL_INIT: 17, }); const Action = Object.freeze({ @@ -96,6 +99,7 @@ const Action = Object.freeze({ LOCAL: 6, MEM: 7, IMPORT: 8, + GLOBAL: 9, }); const types = { @@ -104,12 +108,14 @@ const types = { }; const opcodes = { - "end": 0x0b, - "local.get": 0x20, - "local.set": 0x21, - "local.tee": 0x22, - "i32.const": 0x41, - "i32.mul": 0x6c, + "end": 0x0b, + "local.get": 0x20, + "local.set": 0x21, + "local.tee": 0x22, + "global.get": 0x23, + "global.set": 0x24, + "i32.const": 0x41, + "i32.mul": 0x6c, }; const mem_flags = { @@ -118,6 +124,10 @@ const mem_flags = { "64": 4, }; +const const_opcodes = { + [types["i32"]]: opcodes["i32.const"], +}; + class Parser { constructor() { this.tokens = []; @@ -131,6 +141,7 @@ class Parser { ".local": State.LOCAL_NAME, ".mem": State.MEM_NAME, ".import": State.IMPORT_NAME, + ".global": State.GLOBAL_NAME, }; this.handlers = { [State.TOP]: (token) => this.token_top(token), @@ -148,7 +159,10 @@ class Parser { [State.IMPORT_NAME]: (token) => this.token_import_name(token), [State.IMPORT_MOD]: (token) => this.token_import_mod(token), [State.IMPORT_FIELD]: (token) => this.token_import_field(token), - }; + [State.GLOBAL_NAME]: (token) => this.token_global_name(token), + [State.GLOBAL_TYPE]: (token) => this.token_global_type(token), + [State.GLOBAL_INIT]: (token) => this.token_global_init(token), + }; this.results = []; this.params = {}; @@ -360,6 +374,57 @@ class Parser { } } + token_global_name(token) { + if (token == LINE_END) { + console.error( + "ERROR: Unexpected end of line in .global: expected name"); + this.state = State.TOP; + } else { + this.global = {}; + this.global_name = token; + this.state = State.GLOBAL_TYPE; + } + } + + token_global_type(token) { + if (token == LINE_END) { + console.error( + "ERROR: Unexpected newline in .global: expected type"); + this.global = undefined; + this.global_name = undefined; + this.state = State.TOP; + } else { + this.global.type = types[token] ?? console.error( + `ERROR: Unexpected token {token} in .global: expected type`); + this.state = State.GLOBAL_INIT; + } + } + + 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; + } 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; + } + } + mem_action() { const action = { type: Action.MEM, @@ -385,6 +450,7 @@ const Section = Object.freeze({ IMPORT: 0x02, FUNC: 0x03, MEM: 0x05, + GLOBAL: 0x06, EXPORT: 0x07, CODE: 0x0a, }); @@ -409,12 +475,14 @@ export class Assembler { [Action.LOCAL]: (action) => this.action_local(action), [Action.MEM]: (action) => this.action_mem(action), [Action.IMPORT]: (action) => this.action_import(action), + [Action.GLOBAL]: (action) => this.action_global(action), }; this.exports = []; this.funcs = {}; this.mems = {}; this.imports = []; + this.globals = {}; } action_append(action) { @@ -451,7 +519,12 @@ export class Assembler { action_symbol(action) { const func = this.funcs[this.current_func]; const index = this.lookup_param(func, action.symbol) - ?? this.lookup_local(func, action.symbol); + ?? this.lookup_local(func, action.symbol) + ?? this.lookup_global(action.symbol); + if (index == null) { + console.error(`ERROR: Unable to resolve symbol {action.symbol}`); + index = 0; + } func.body.push(index); } @@ -472,6 +545,10 @@ export class Assembler { }) } + action_global(action) { + Object.assign(this.globals, action.global); + } + push(chunk) { const text = this.decoder.decode(chunk, { stream: true }); for (const action of this.parser.handle(text)) @@ -489,6 +566,11 @@ export class Assembler { return index == -1 ? null : index; } + lookup_global(symbol) { + const index = Object.keys(this.globals).indexOf(symbol); + return index == -1 ? null : index; + } + wasm_section_type() { const funcs = Object.values(this.funcs); const contents = funcs.map(({ params, results }) => { @@ -536,6 +618,15 @@ export class Assembler { return [ mems.length ].concat(...contents); } + wasm_section_global() { + const globals = Object.values(this.globals); + if (globals.length == 0) + return null; + const contents = globals.map( + ({ type, init }) => [ type, 1, ...init ]); + return [ globals.length ].concat(...contents); + } + wasm_section_export() { const exports = Object.entries(this.exports); const contents = exports.map(([ name, { kind, index }]) => { @@ -555,14 +646,23 @@ export class Assembler { const contents = funcs.map(({ body, locals }) => { const local_types = Object.values(locals); const local_count = local_types.length; - return [ - body.length + local_count + 3, - local_count, - local_count, - ...local_types, - ...body, - opcodes["end"] - ] + if (local_count == 0) { + return [ + body.length + 2, + 0, + ...body, + opcodes["end"] + ] + } else { + return [ + body.length + local_count + 3, + local_count, + local_count, + ...local_types, + ...body, + opcodes["end"] + ]; + } }); return [ contents.length ].concat(...contents); } @@ -573,6 +673,7 @@ export class Assembler { [ Section.IMPORT, () => this.wasm_section_import() ], [ Section.FUNC, () => this.wasm_section_func() ], [ Section.MEM, () => this.wasm_section_mem() ], + [ Section.GLOBAL, () => this.wasm_section_global() ], [ Section.EXPORT, () => this.wasm_section_export() ], [ Section.CODE, () => this.wasm_section_code() ], ]; -- 2.34.1 From 5369a0969efa20173c604471a822849230204cd9 Mon Sep 17 00:00:00 2001 From: Camden Dixie O'Brien Date: Tue, 10 Mar 2026 21:25:37 +0000 Subject: [PATCH 10/72] Restructure copy implementation to avoid type-indexed block --- wipforth.wat | 25 +++++++++++++++---------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/wipforth.wat b/wipforth.wat index ede59f4..66cf67e 100644 --- a/wipforth.wat +++ b/wipforth.wat @@ -542,20 +542,25 @@ call $next) (func $copy (local $src i32) (local $dst i32) (local $n i32) - call $pop local.tee $dst - call $pop local.tee $src - call $pop local.tee $n + call $pop local.set $dst + call $pop local.set $src + call $pop local.set $n - block $done (param i32 i32 i32) - loop $loop (param i32 i32 i32) (result i32 i32 i32) + block $done + loop $loop + local.get $n i32.eqz br_if $done + + local.get $dst + local.get $src i32.load8_u i32.store8 - local.get $dst i32.const 1 i32.add local.tee $dst - local.get $src i32.const 1 i32.add local.tee $src - local.get $n i32.const 1 i32.sub local.tee $n - br $loop + + local.get $dst i32.const 1 i32.add local.set $dst + local.get $src i32.const 1 i32.add local.set $src + local.get $n i32.const 1 i32.sub local.set $n + + br $loop end - drop drop drop end call $next) -- 2.34.1 From 6db71ee38248e28a22444b107e2de31f1d526729 Mon Sep 17 00:00:00 2001 From: Camden Dixie O'Brien Date: Sat, 14 Mar 2026 11:47:01 +0000 Subject: [PATCH 11/72] Add .at directive --- asm.js | 41 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 40 insertions(+), 1 deletion(-) diff --git a/asm.js b/asm.js index 9882baa..8fad25c 100644 --- a/asm.js +++ b/asm.js @@ -87,6 +87,8 @@ const State = Object.freeze({ GLOBAL_NAME: 15, GLOBAL_TYPE: 16, GLOBAL_INIT: 17, + AT_MEM: 18, + AT_ADDR: 19, }); const Action = Object.freeze({ @@ -100,6 +102,7 @@ const Action = Object.freeze({ MEM: 7, IMPORT: 8, GLOBAL: 9, + AT: 10, }); const types = { @@ -142,6 +145,7 @@ class Parser { ".mem": State.MEM_NAME, ".import": State.IMPORT_NAME, ".global": State.GLOBAL_NAME, + ".at": State.AT_MEM, }; this.handlers = { [State.TOP]: (token) => this.token_top(token), @@ -162,7 +166,9 @@ class Parser { [State.GLOBAL_NAME]: (token) => this.token_global_name(token), [State.GLOBAL_TYPE]: (token) => this.token_global_type(token), [State.GLOBAL_INIT]: (token) => this.token_global_init(token), - }; + [State.AT_MEM]: (token) => this.token_at_mem(token), + [State.AT_ADDR]: (token) => this.token_at_addr(token), + }; this.results = []; this.params = {}; @@ -425,6 +431,27 @@ class Parser { } } + token_at_mem(token) { + this.at = { mem: token }; + this.state = State.AT_ADDR; + } + + 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; + const action = { type: Action.AT, at: this.at }; + this.at = undefined; + this.state = State.TOP; + return action; + } + mem_action() { const action = { type: Action.MEM, @@ -476,6 +503,7 @@ export class Assembler { [Action.MEM]: (action) => this.action_mem(action), [Action.IMPORT]: (action) => this.action_import(action), [Action.GLOBAL]: (action) => this.action_global(action), + [Action.AT]: (action) => this.action_at(action), }; this.exports = []; @@ -483,6 +511,7 @@ export class Assembler { this.mems = {}; this.imports = []; this.globals = {}; + this.pos = { mem: 0, addr: 0 }; } action_append(action) { @@ -549,6 +578,16 @@ export class Assembler { Object.assign(this.globals, action.global); } + action_at(action) { + const mem = Object.keys(this.mems).indexOf(action.at.mem); + if (mem == -1) { + console.error(`ERROR: No memory named {action.at.mem}`); + return; + } + this.pos.mem = mem; + this.pos.addr = action.at.addr; + } + push(chunk) { const text = this.decoder.decode(chunk, { stream: true }); for (const action of this.parser.handle(text)) -- 2.34.1 From 092d870a9c73fce000dcd1941266228c20395007 Mon Sep 17 00:00:00 2001 From: Camden Dixie O'Brien Date: Sat, 14 Mar 2026 12:49:46 +0000 Subject: [PATCH 12/72] Implement .byte directive --- asm.js | 49 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/asm.js b/asm.js index 8fad25c..242f9fd 100644 --- a/asm.js +++ b/asm.js @@ -89,6 +89,7 @@ const State = Object.freeze({ GLOBAL_INIT: 17, AT_MEM: 18, AT_ADDR: 19, + BYTE: 20, }); const Action = Object.freeze({ @@ -103,6 +104,7 @@ const Action = Object.freeze({ IMPORT: 8, GLOBAL: 9, AT: 10, + DATA: 11, }); const types = { @@ -146,6 +148,7 @@ class Parser { ".import": State.IMPORT_NAME, ".global": State.GLOBAL_NAME, ".at": State.AT_MEM, + ".byte": State.BYTE, }; this.handlers = { [State.TOP]: (token) => this.token_top(token), @@ -168,6 +171,7 @@ class Parser { [State.GLOBAL_INIT]: (token) => this.token_global_init(token), [State.AT_MEM]: (token) => this.token_at_mem(token), [State.AT_ADDR]: (token) => this.token_at_addr(token), + [State.BYTE]: (token) => this.token_byte(token), }; this.results = []; @@ -452,6 +456,25 @@ class Parser { return action; } + token_byte(token) { + if (token == LINE_END) { + this.state = State.TOP; + return; + } + const action = { type: Action.DATA, size: 1 }; + const value = this.integer(token); + if (value == null) { + console.error( + `ERROR: Unexpected token ${token}, expected value`); + return; + } else { + if (value > 0xff) + console.error(`WARNING: Value ${token} is truncated`); + action.value = [ value & 0xff ]; + } + return action; + } + mem_action() { const action = { type: Action.MEM, @@ -480,6 +503,7 @@ const Section = Object.freeze({ GLOBAL: 0x06, EXPORT: 0x07, CODE: 0x0a, + DATA: 0x0b, }); const Kind = Object.freeze({ @@ -504,6 +528,7 @@ export class Assembler { [Action.IMPORT]: (action) => this.action_import(action), [Action.GLOBAL]: (action) => this.action_global(action), [Action.AT]: (action) => this.action_at(action), + [Action.DATA]: (action) => this.action_data(action), }; this.exports = []; @@ -512,6 +537,7 @@ export class Assembler { this.imports = []; this.globals = {}; this.pos = { mem: 0, addr: 0 }; + this.data = []; } action_append(action) { @@ -586,6 +612,13 @@ export class Assembler { } this.pos.mem = mem; this.pos.addr = action.at.addr; + this.data.push({ loc: { ...this.pos }, data: [] }) + } + + action_data(action) { + const data = this.data.at(-1).data; + data.push(...action.value); + this.pos.addr += action.size; } push(chunk) { @@ -706,6 +739,21 @@ export class Assembler { return [ contents.length ].concat(...contents); } + wasm_section_data() { + if (this.data.length == 0) return null; + const contents = this.data.map(({ loc, data }) => { + return [ + ...(loc.mem == 0 ? [ 0 ] : [ 2, loc.mem ]), + opcodes["i32.const"], + loc.addr, + opcodes["end"], + data.length, + ...data, + ] + }); + return [ contents.length ].concat(...contents); + } + wasm() { const template = [ [ Section.TYPE, () => this.wasm_section_type() ], @@ -715,6 +763,7 @@ export class Assembler { [ Section.GLOBAL, () => this.wasm_section_global() ], [ Section.EXPORT, () => this.wasm_section_export() ], [ Section.CODE, () => this.wasm_section_code() ], + [ Section.DATA, () => this.wasm_section_data() ], ]; const sections = template.map(([ code, generator ]) => { const body = generator(); -- 2.34.1 From 94cee7d2584608f9797269ad02ad0566d886da75 Mon Sep 17 00:00:00 2001 From: Camden Dixie O'Brien Date: Sat, 14 Mar 2026 13:01:46 +0000 Subject: [PATCH 13/72] Fix string interpolation in error messages --- asm.js | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/asm.js b/asm.js index 242f9fd..c9d42eb 100644 --- a/asm.js +++ b/asm.js @@ -306,7 +306,7 @@ class Parser { this.state = State.TOP; } else { this.mem.init = this.integer(token) ?? console.error( - `ERROR: Invalid initial size {token} in .mem`); + `ERROR: Invalid initial size ${token} in .mem`); this.state = State.MEM_MAX; } } @@ -316,7 +316,7 @@ class Parser { return this.mem_action(); } else { this.mem.max = this.integer(token) ?? console.error( - `ERROR: Invalid maximum size {token} in .mem`); + `ERROR: Invalid maximum size ${token} in .mem`); this.mem.flags |= mem_flags.max; this.state = State.MEM_FLAGS; } @@ -328,7 +328,7 @@ class Parser { } else { for (const flag of token.split(",")) { this.mem.flags |= mem_flags[flag] ?? console.error( - `ERROR: Invalid flag {flag} in .mem`); + `ERROR: Invalid flag ${flag} in .mem`); } this.state = State.TOP; return this.mem_action(); @@ -354,7 +354,7 @@ class Parser { this.state = State.TOP; } else if (token.string == undefined) { console.error( - `ERROR: Unexpected token {token} in .import: expected` + `ERROR: Unexpected token ${token} in .import: expected` + " module string"); this.import = undefined; this.state = State.TOP; @@ -405,7 +405,8 @@ class Parser { this.state = State.TOP; } else { this.global.type = types[token] ?? console.error( - `ERROR: Unexpected token {token} in .global: expected type`); + `ERROR: Unexpected token ${token} in .global: ` + + "expected type"); this.state = State.GLOBAL_INIT; } } @@ -420,7 +421,7 @@ class Parser { this.state = State.TOP; } else { const value = this.integer(token) ?? console.error( - `ERROR: Unexpected token {token} in .global: expected` + `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"] ]; @@ -444,7 +445,8 @@ class Parser { const value = this.integer(token); if (value == null) { console.error( - `ERROR: Unexpected token {token} in .mem: expected address`); + `ERROR: Unexpected token ${token} in .mem: ` + + "expected address"); this.at = undefined; return; } -- 2.34.1 From cfa4fa7d4fd4bac82817dcafc7c19a470b6b1e98 Mon Sep 17 00:00:00 2001 From: Camden Dixie O'Brien Date: Sat, 14 Mar 2026 13:03:17 +0000 Subject: [PATCH 14/72] Add .word directive --- asm.js | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/asm.js b/asm.js index c9d42eb..e79d62a 100644 --- a/asm.js +++ b/asm.js @@ -90,6 +90,7 @@ const State = Object.freeze({ AT_MEM: 18, AT_ADDR: 19, BYTE: 20, + WORD: 21, }); const Action = Object.freeze({ @@ -149,6 +150,7 @@ class Parser { ".global": State.GLOBAL_NAME, ".at": State.AT_MEM, ".byte": State.BYTE, + ".word": State.WORD, }; this.handlers = { [State.TOP]: (token) => this.token_top(token), @@ -172,6 +174,7 @@ class Parser { [State.AT_MEM]: (token) => this.token_at_mem(token), [State.AT_ADDR]: (token) => this.token_at_addr(token), [State.BYTE]: (token) => this.token_byte(token), + [State.WORD]: (token) => this.token_word(token), }; this.results = []; @@ -477,6 +480,25 @@ class Parser { return action; } + token_word(token) { + if (token == LINE_END) { + this.state = State.TOP; + return; + } + const action = { type: Action.DATA, size: 2 }; + const value = this.integer(token); + if (value == null) { + console.error( + `ERROR: Unexpected token ${token}, expected value`); + return; + } else { + if (value > 0xffff) + console.error(`WARNING: Value ${token} is truncated`); + action.value = [ value & 0xff, (value >> 8) & 0xff ]; + } + return action; + } + mem_action() { const action = { type: Action.MEM, -- 2.34.1 From 93f3dd1f416884f307e02efa529dcd650d54e0ff Mon Sep 17 00:00:00 2001 From: Camden Dixie O'Brien Date: Sat, 14 Mar 2026 13:10:48 +0000 Subject: [PATCH 15/72] Implement .utf directive --- asm.js | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/asm.js b/asm.js index e79d62a..068e8eb 100644 --- a/asm.js +++ b/asm.js @@ -91,6 +91,7 @@ const State = Object.freeze({ AT_ADDR: 19, BYTE: 20, WORD: 21, + UTF8: 22, }); const Action = Object.freeze({ @@ -135,7 +136,8 @@ const const_opcodes = { }; class Parser { - constructor() { + constructor(encoder) { + this.encoder = encoder; this.tokens = []; this.tokenizer = new Tokenizer(); this.state = State.TOP; @@ -151,6 +153,7 @@ class Parser { ".at": State.AT_MEM, ".byte": State.BYTE, ".word": State.WORD, + ".utf8": State.UTF8, }; this.handlers = { [State.TOP]: (token) => this.token_top(token), @@ -175,6 +178,7 @@ class Parser { [State.AT_ADDR]: (token) => this.token_at_addr(token), [State.BYTE]: (token) => this.token_byte(token), [State.WORD]: (token) => this.token_word(token), + [State.UTF8]: (token) => this.token_utf8(token), }; this.results = []; @@ -499,6 +503,20 @@ class Parser { return action; } + token_utf8(token) { + if (token == LINE_END) { + this.state = State.TOP; + return; + } else if (token.string == undefined) { + console.error( + `ERROR: unexpected token ${token}, expected string`); + return; + } + const value = this.encoder.encode(token.string); + const action = { type: Action.DATA, size: value.length, value }; + return action; + } + mem_action() { const action = { type: Action.MEM, @@ -539,7 +557,7 @@ export class Assembler { constructor() { this.encoder = new TextEncoder("utf-8"); this.decoder = new TextDecoder("utf-8"); - this.parser = new Parser(); + this.parser = new Parser(this.encoder); this.handlers = { [Action.APPEND]: (action) => this.action_append(action), [Action.EXPORT]: (action) => this.action_export(action), -- 2.34.1 From 2c3e5f46da0b2d9df0adf05912b2776c09fe190e Mon Sep 17 00:00:00 2001 From: Camden Dixie O'Brien Date: Sat, 14 Mar 2026 13:19:59 +0000 Subject: [PATCH 16/72] Implement .align directive --- asm.js | 33 ++++++++++++++++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/asm.js b/asm.js index 068e8eb..47ac5d1 100644 --- a/asm.js +++ b/asm.js @@ -92,6 +92,7 @@ const State = Object.freeze({ BYTE: 20, WORD: 21, UTF8: 22, + ALIGN: 23, }); const Action = Object.freeze({ @@ -107,6 +108,7 @@ const Action = Object.freeze({ GLOBAL: 9, AT: 10, DATA: 11, + ALIGN: 12, }); const types = { @@ -154,6 +156,7 @@ class Parser { ".byte": State.BYTE, ".word": State.WORD, ".utf8": State.UTF8, + ".align": State.ALIGN, }; this.handlers = { [State.TOP]: (token) => this.token_top(token), @@ -179,6 +182,7 @@ class Parser { [State.BYTE]: (token) => this.token_byte(token), [State.WORD]: (token) => this.token_word(token), [State.UTF8]: (token) => this.token_utf8(token), + [State.ALIGN]: (token) => this.token_align(token), }; this.results = []; @@ -509,7 +513,7 @@ class Parser { return; } else if (token.string == undefined) { console.error( - `ERROR: unexpected token ${token}, expected string`); + `ERROR: Unexpected token ${token}, expected string`); return; } const value = this.encoder.encode(token.string); @@ -517,6 +521,23 @@ class Parser { return action; } + token_align(token) { + const action = { type: Action.ALIGN }; + if (token == LINE_END) { + action.alignment = 4; + } else { + action.alignment = this.integer(token); + if (action.alignment == null) { + console.error( + `ERROR: Unexpected token ${token}, expected alignment`); + this.state = State.TOP; + return action; + } + } + this.state = State.TOP + return action; + } + mem_action() { const action = { type: Action.MEM, @@ -571,6 +592,7 @@ export class Assembler { [Action.GLOBAL]: (action) => this.action_global(action), [Action.AT]: (action) => this.action_at(action), [Action.DATA]: (action) => this.action_data(action), + [Action.ALIGN]: (action) => this.action_align(action), }; this.exports = []; @@ -663,6 +685,15 @@ export class Assembler { this.pos.addr += action.size; } + action_align(action) { + const alignment = action.alignment; + const data = this.data.at(-1).data; + while (this.pos.addr % alignment != 0) { + data.push(0); + ++this.pos.addr; + } + } + push(chunk) { const text = this.decoder.decode(chunk, { stream: true }); for (const action of this.parser.handle(text)) -- 2.34.1 From 2972030d0ae834b1706e9eba16ebceacb0f45019 Mon Sep 17 00:00:00 2001 From: Camden Dixie O'Brien Date: Sat, 14 Mar 2026 13:48:07 +0000 Subject: [PATCH 17/72] Add .def support --- asm.js | 46 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/asm.js b/asm.js index 47ac5d1..5a013d0 100644 --- a/asm.js +++ b/asm.js @@ -93,6 +93,8 @@ const State = Object.freeze({ WORD: 21, UTF8: 22, ALIGN: 23, + DEF_NAME: 24, + DEF_VALUE: 25, }); const Action = Object.freeze({ @@ -109,6 +111,7 @@ const Action = Object.freeze({ AT: 10, DATA: 11, ALIGN: 12, + DEF: 13, }); const types = { @@ -157,6 +160,7 @@ class Parser { ".word": State.WORD, ".utf8": State.UTF8, ".align": State.ALIGN, + ".def": State.DEF_NAME, }; this.handlers = { [State.TOP]: (token) => this.token_top(token), @@ -183,6 +187,8 @@ class Parser { [State.WORD]: (token) => this.token_word(token), [State.UTF8]: (token) => this.token_utf8(token), [State.ALIGN]: (token) => this.token_align(token), + [State.DEF_NAME]: (token) => this.token_def_name(token), + [State.DEF_VALUE]: (token) => this.token_def_value(token), }; this.results = []; @@ -538,6 +544,40 @@ class Parser { return action; } + token_def_name(token) { + if (token == LINE_END) { + console.error("ERROR: Unexpected end of line, expected name"); + this.state = State.TOP; + return; + } + this.def_name = token; + this.state = State.DEF_VALUE; + } + + token_def_value(token) { + if (token == LINE_END) { + console.error("ERROR: Unexpected end of line, expected value"); + this.def_name = undefined; + this.state = State.TOP; + return; + } + const value = this.integer(token); + if (value == null) { + console.error( + `ERROR: Unexpected token ${token}, expected value`); + this.def_name = undefined; + this.state = State.TOP; + return; + } + const action = { + type: Action.DEF, + def: { name: this.def_name, value }, + }; + this.def_name = undefined; + this.state = State.TOP; + return action; + } + mem_action() { const action = { type: Action.MEM, @@ -593,6 +633,7 @@ export class Assembler { [Action.AT]: (action) => this.action_at(action), [Action.DATA]: (action) => this.action_data(action), [Action.ALIGN]: (action) => this.action_align(action), + [Action.DEF]: (action) => this.action_def(action), }; this.exports = []; @@ -602,6 +643,7 @@ export class Assembler { this.globals = {}; this.pos = { mem: 0, addr: 0 }; this.data = []; + this.defs = {}; } action_append(action) { @@ -694,6 +736,10 @@ export class Assembler { } } + action_def(action) { + this.defs[action.def.name] = action.def.value; + } + push(chunk) { const text = this.decoder.decode(chunk, { stream: true }); for (const action of this.parser.handle(text)) -- 2.34.1 From e2429b2b03e2649b9e796467eb1c164a33def2fe Mon Sep 17 00:00:00 2001 From: Camden Dixie O'Brien Date: Sat, 14 Mar 2026 13:51:04 +0000 Subject: [PATCH 18/72] Enable using defs in .byte and .word directives --- asm.js | 28 +++++++++++++++++++++------- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/asm.js b/asm.js index 5a013d0..4b89ace 100644 --- a/asm.js +++ b/asm.js @@ -483,9 +483,7 @@ class Parser { const action = { type: Action.DATA, size: 1 }; const value = this.integer(token); if (value == null) { - console.error( - `ERROR: Unexpected token ${token}, expected value`); - return; + action.symbol = token; } else { if (value > 0xff) console.error(`WARNING: Value ${token} is truncated`); @@ -502,9 +500,7 @@ class Parser { const action = { type: Action.DATA, size: 2 }; const value = this.integer(token); if (value == null) { - console.error( - `ERROR: Unexpected token ${token}, expected value`); - return; + action.symbol = token; } else { if (value > 0xffff) console.error(`WARNING: Value ${token} is truncated`); @@ -723,7 +719,10 @@ export class Assembler { action_data(action) { const data = this.data.at(-1).data; - data.push(...action.value); + const value = action.value != null + ? action.value + : this.le_bytes(this.lookup_def(action.symbol), action.size); + data.push(...value); this.pos.addr += action.size; } @@ -762,6 +761,21 @@ export class Assembler { return index == -1 ? null : index; } + lookup_def(symbol) { + return this.defs[symbol]; + } + + le_bytes(value, count) { + let bytes = [] + while (value != 0 && bytes.length < count) { + bytes.push(value & 0xff); + value >>= 8; + } + while (bytes.length < count) + bytes.push(0); + return bytes; + } + wasm_section_type() { const funcs = Object.values(this.funcs); const contents = funcs.map(({ params, results }) => { -- 2.34.1 From 33f5a4be06683c927573a3428c63b6ce2b07c7d2 Mon Sep 17 00:00:00 2001 From: Camden Dixie O'Brien Date: Sat, 14 Mar 2026 13:59:29 +0000 Subject: [PATCH 19/72] Fix bug in local lookup --- asm.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/asm.js b/asm.js index 4b89ace..cb1de69 100644 --- a/asm.js +++ b/asm.js @@ -752,8 +752,8 @@ export class Assembler { lookup_local(func, symbol) { const param_count = Object.entries(func.params).length; - const index = param_count + Object.keys(func.locals).indexOf(symbol); - return index == -1 ? null : index; + const index = Object.keys(func.locals).indexOf(symbol); + return index == -1 ? null : param_count + index; } lookup_global(symbol) { -- 2.34.1 From d4718f1106b5d82ddd70733c1ba7d440177c1f39 Mon Sep 17 00:00:00 2001 From: Camden Dixie O'Brien Date: Sat, 14 Mar 2026 13:52:10 +0000 Subject: [PATCH 20/72] Allow using defs in code --- asm.js | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/asm.js b/asm.js index cb1de69..6d69eb2 100644 --- a/asm.js +++ b/asm.js @@ -675,14 +675,15 @@ export class Assembler { action_symbol(action) { const func = this.funcs[this.current_func]; - const index = this.lookup_param(func, action.symbol) + const value = this.lookup_param(func, action.symbol) ?? this.lookup_local(func, action.symbol) - ?? this.lookup_global(action.symbol); - if (index == null) { + ?? this.lookup_global(action.symbol) + ?? this.lookup_def(action.symbol); + if (value == null) { console.error(`ERROR: Unable to resolve symbol {action.symbol}`); - index = 0; + value = 0; } - func.body.push(index); + func.body.push(value); } action_mem(action) { -- 2.34.1 From 902404cb10d6a56bc6836ed10a6ab0d21781f09b Mon Sep 17 00:00:00 2001 From: Camden Dixie O'Brien Date: Sat, 14 Mar 2026 14:00:39 +0000 Subject: [PATCH 21/72] Fix string interpolation in error messages --- asm.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/asm.js b/asm.js index 6d69eb2..0240a37 100644 --- a/asm.js +++ b/asm.js @@ -680,7 +680,8 @@ export class Assembler { ?? this.lookup_global(action.symbol) ?? this.lookup_def(action.symbol); if (value == null) { - console.error(`ERROR: Unable to resolve symbol {action.symbol}`); + console.error( + `ERROR: Unable to resolve symbol ${action.symbol}`); value = 0; } func.body.push(value); @@ -710,7 +711,7 @@ export class Assembler { action_at(action) { const mem = Object.keys(this.mems).indexOf(action.at.mem); if (mem == -1) { - console.error(`ERROR: No memory named {action.at.mem}`); + console.error(`ERROR: No memory named ${action.at.mem}`); return; } this.pos.mem = mem; -- 2.34.1 From cc51b2d7beb917b2419ef312b61709fe6ee77bf8 Mon Sep 17 00:00:00 2001 From: Camden Dixie O'Brien Date: Sat, 14 Mar 2026 14:50:40 +0000 Subject: [PATCH 22/72] Fix data word size --- asm.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/asm.js b/asm.js index 0240a37..d04a423 100644 --- a/asm.js +++ b/asm.js @@ -497,7 +497,7 @@ class Parser { this.state = State.TOP; return; } - const action = { type: Action.DATA, size: 2 }; + const action = { type: Action.DATA, size: 4 }; const value = this.integer(token); if (value == null) { action.symbol = token; -- 2.34.1 From 22dc1fc0ca0bb67808b7dfc442dbf57843c49c80 Mon Sep 17 00:00:00 2001 From: Camden Dixie O'Brien Date: Sat, 14 Mar 2026 14:52:44 +0000 Subject: [PATCH 23/72] Add support for labels --- asm.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/asm.js b/asm.js index d04a423..b52aede 100644 --- a/asm.js +++ b/asm.js @@ -112,6 +112,7 @@ const Action = Object.freeze({ DATA: 11, ALIGN: 12, DEF: 13, + LABEL: 14, }); const types = { @@ -224,6 +225,8 @@ class Parser { this.state = state; return; } + if (token.endsWith(":")) + return { type: Action.LABEL, name: token.slice(0, -1) }; const code = this.translate_code(token); if (code) return { type: Action.APPEND, code }; @@ -630,6 +633,7 @@ export class Assembler { [Action.DATA]: (action) => this.action_data(action), [Action.ALIGN]: (action) => this.action_align(action), [Action.DEF]: (action) => this.action_def(action), + [Action.LABEL]: (action) => this.action_label(action), }; this.exports = []; @@ -741,6 +745,10 @@ export class Assembler { this.defs[action.def.name] = action.def.value; } + action_label(action) { + this.defs[action.name] = this.pos.addr; + } + push(chunk) { const text = this.decoder.decode(chunk, { stream: true }); for (const action of this.parser.handle(text)) -- 2.34.1 From 9fb3910a1615ee1434f15649687a4f0bcb3cbe8c Mon Sep 17 00:00:00 2001 From: Camden Dixie O'Brien Date: Sat, 14 Mar 2026 15:04:13 +0000 Subject: [PATCH 24/72] Allow defs to reference other defs --- asm.js | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/asm.js b/asm.js index b52aede..a21bb52 100644 --- a/asm.js +++ b/asm.js @@ -560,18 +560,15 @@ class Parser { this.state = State.TOP; return; } - const value = this.integer(token); - if (value == null) { - console.error( - `ERROR: Unexpected token ${token}, expected value`); - this.def_name = undefined; - this.state = State.TOP; - return; - } const action = { type: Action.DEF, - def: { name: this.def_name, value }, + def: { name: this.def_name }, }; + const value = this.integer(token); + if (value != null) + action.def.value = value; + else + action.def.symbol = token; this.def_name = undefined; this.state = State.TOP; return action; @@ -742,7 +739,9 @@ export class Assembler { } action_def(action) { - this.defs[action.def.name] = action.def.value; + const value = action.def.value + ?? this.lookup_def(action.def.symbol); + this.defs[action.def.name] = value; } action_label(action) { -- 2.34.1 From 4f878fdbab4a00a9510e2506489ef2b5dde2cd23 Mon Sep 17 00:00:00 2001 From: Camden Dixie O'Brien Date: Sat, 14 Mar 2026 18:26:12 +0000 Subject: [PATCH 25/72] Add suport for block/loop/if/else --- asm.js | 130 +++++++++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 118 insertions(+), 12 deletions(-) diff --git a/asm.js b/asm.js index a21bb52..39dc49d 100644 --- a/asm.js +++ b/asm.js @@ -78,7 +78,7 @@ const State = Object.freeze({ LOCAL_NAME: 6, LOCAL_TYPE: 7, MEM_NAME: 8, - MEM_INITIAL: 9, + MEM_INIT: 9, MEM_MAX: 10, MEM_FLAGS: 11, IMPORT_NAME: 12, @@ -95,6 +95,8 @@ const State = Object.freeze({ ALIGN: 23, DEF_NAME: 24, DEF_VALUE: 25, + BLOCK_NAME: 26, + BLOCK_TYPE: 27, }); const Action = Object.freeze({ @@ -113,21 +115,32 @@ const Action = Object.freeze({ ALIGN: 12, DEF: 13, LABEL: 14, + ENTER: 16, + EXIT: 17, + ELSE: 18, }); const types = { + "void": 0x40, "func": 0x60, "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, }; @@ -163,6 +176,7 @@ class Parser { ".align": State.ALIGN, ".def": State.DEF_NAME, }; + this.blocks = new Set(["block", "loop", "if"]); this.handlers = { [State.TOP]: (token) => this.token_top(token), [State.EXPORT]: (token) => this.token_export(token), @@ -190,6 +204,8 @@ class Parser { [State.ALIGN]: (token) => this.token_align(token), [State.DEF_NAME]: (token) => this.token_def_name(token), [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), }; this.results = []; @@ -220,18 +236,34 @@ class Parser { token_top(token) { if (token == LINE_END) return; - let state; - if (state = this.directives[token]) { + const state = this.directives[token]; + if (state) { this.state = state; return; } + if (token.endsWith(":")) return { type: Action.LABEL, name: token.slice(0, -1) }; - const code = this.translate_code(token); - if (code) - return { type: Action.APPEND, code }; - else - return { type: Action.SYMBOL, symbol: token }; + + const opcode = opcodes[token]; + + if (this.blocks.has(token)) { + this.state = State.BLOCK_NAME; + this.block = { code: opcode }; + return; + } + if (token == "else") + return { type: Action.ELSE } + if (token == "end") + return { type: Action.EXIT }; + + if (opcode) + return { type: Action.APPEND, opcode }; + const literal = this.integer(token); + if (literal) + return { type: Action.APPEND, literal }; + + return { type: Action.SYMBOL, symbol: token }; } token_export(token) { @@ -574,6 +606,36 @@ class Parser { return action; } + token_block_name(token) { + if (token == LINE_END) { + this.block.type = types["void"]; + const action = { type: Action.ENTER, block: this.block }; + this.state = State.TOP; + this.block = undefined; + return action; + } else { + this.block.label = token; + this.state = State.BLOCK_TYPE; + return; + } + } + + token_block_type(token) { + if (token == LINE_END) { + this.block.type = types["void"]; + } else { + this.block.type = types[token]; + if (this.block.type == undefined) { + console.error( + `ERROR: Unexpected token ${token}, expected type`); + } + } + const action = { type: Action.ENTER, block: this.block }; + this.state = State.TOP; + this.block = undefined; + return action; + } + mem_action() { const action = { type: Action.MEM, @@ -588,8 +650,9 @@ class Parser { *handle(src) { let action; for (const token of this.tokenizer.handle(src)) { - if (action = this.handlers[this.state](token)) + if (action = this.handlers[this.state](token)) { yield action; + } } } } @@ -631,6 +694,9 @@ export class Assembler { [Action.ALIGN]: (action) => this.action_align(action), [Action.DEF]: (action) => this.action_def(action), [Action.LABEL]: (action) => this.action_label(action), + [Action.ENTER]: (action) => this.action_enter(action), + [Action.EXIT]: (action) => this.action_exit(action), + [Action.ELSE]: (action) => this.action_else(action), }; this.exports = []; @@ -641,10 +707,14 @@ export class Assembler { this.pos = { mem: 0, addr: 0 }; this.data = []; this.defs = {}; + this.blocks = []; } action_append(action) { - this.funcs[this.current_func].body.push(action.code); + const code = action.opcode != undefined + ? [ action.opcode ] + : this.leb128(action.literal); + this.funcs[this.current_func].body.push(...code); } action_export(action) { @@ -676,14 +746,14 @@ export class Assembler { action_symbol(action) { const func = this.funcs[this.current_func]; - const value = this.lookup_param(func, action.symbol) + const value = this.lookup_block(action.symbol) + ?? this.lookup_param(func, action.symbol) ?? this.lookup_local(func, action.symbol) ?? this.lookup_global(action.symbol) ?? this.lookup_def(action.symbol); if (value == null) { console.error( `ERROR: Unable to resolve symbol ${action.symbol}`); - value = 0; } func.body.push(value); } @@ -748,12 +818,34 @@ export class Assembler { this.defs[action.name] = this.pos.addr; } + action_enter(action) { + this.funcs[this.current_func].body.push(action.block.code); + this.funcs[this.current_func].body.push(action.block.type); + this.blocks.push(action.block.label); + } + + action_exit(action) { + this.funcs[this.current_func].body.push(opcodes.end); + this.blocks.pop(); + } + + action_else(action) { + this.blocks.pop(); + this.funcs[this.current_func].body.push(opcodes.else); + this.blocks.push(undefined); + } + push(chunk) { const text = this.decoder.decode(chunk, { stream: true }); for (const action of this.parser.handle(text)) this.handlers[action.type](action); } + lookup_block(symbol) { + const index = this.blocks.toReversed().indexOf(symbol); + return index == -1 ? null : index; + } + lookup_param(func, symbol) { const index = Object.keys(func.params).indexOf(symbol); return index == -1 ? null : index; @@ -921,4 +1013,18 @@ export class Assembler { else 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); + } + } } -- 2.34.1 From 714973f0525edeb0c63d97b88101ac95cc746bb7 Mon Sep 17 00:00:00 2001 From: Camden Dixie O'Brien Date: Sat, 14 Mar 2026 18:29:55 +0000 Subject: [PATCH 26/72] LEB128-encode values from defs --- asm.js | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/asm.js b/asm.js index 39dc49d..cfb0912 100644 --- a/asm.js +++ b/asm.js @@ -750,12 +750,17 @@ export class Assembler { ?? this.lookup_param(func, action.symbol) ?? this.lookup_local(func, action.symbol) ?? this.lookup_global(action.symbol) - ?? this.lookup_def(action.symbol); if (value == null) { - console.error( - `ERROR: Unable to resolve symbol ${action.symbol}`); + const def_value = this.lookup_def(action.symbol); + if (def_value == null) { + console.error( + `ERROR: Unable to resolve symbol ${action.symbol}`); + return; + } + func.body.push(...this.leb128(def_value)); + } else { + func.body.push(value); } - func.body.push(value); } action_mem(action) { -- 2.34.1 From f4433ce3a3448024897126fbed32272d26845154 Mon Sep 17 00:00:00 2001 From: Camden Dixie O'Brien Date: Sat, 14 Mar 2026 18:35:33 +0000 Subject: [PATCH 27/72] LEB128-encode addresses in data section --- asm.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/asm.js b/asm.js index cfb0912..a59e16c 100644 --- a/asm.js +++ b/asm.js @@ -984,7 +984,7 @@ export class Assembler { return [ ...(loc.mem == 0 ? [ 0 ] : [ 2, loc.mem ]), opcodes["i32.const"], - loc.addr, + ...this.leb128(loc.addr), opcodes["end"], data.length, ...data, -- 2.34.1 From 347dd8f5344b860fb883fb0fc38607a44ccadb61 Mon Sep 17 00:00:00 2001 From: Camden Dixie O'Brien Date: Sat, 14 Mar 2026 18:36:21 +0000 Subject: [PATCH 28/72] Make all sections optional --- asm.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/asm.js b/asm.js index a59e16c..103f493 100644 --- a/asm.js +++ b/asm.js @@ -884,6 +884,7 @@ export class Assembler { 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 [ @@ -917,6 +918,7 @@ 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() ]; } @@ -940,6 +942,7 @@ export class Assembler { wasm_section_export() { const exports = Object.entries(this.exports); + if (exports.length == 0) return null; const contents = exports.map(([ name, { kind, index }]) => { const name_utf8 = this.encoder.encode(name); return [ @@ -954,6 +957,7 @@ export class Assembler { wasm_section_code() { const funcs = Object.values(this.funcs); + if (funcs.length == 0) return null; const contents = funcs.map(({ body, locals }) => { const local_types = Object.values(locals); const local_count = local_types.length; -- 2.34.1 From 1105daaad0d20271bd34233ba6768a3f78c285d3 Mon Sep 17 00:00:00 2001 From: Camden Dixie O'Brien Date: Sat, 14 Mar 2026 18:48:11 +0000 Subject: [PATCH 29/72] Add support for extended opcodes --- asm.js | 42 ++++++++++++++++++++++-------------------- 1 file changed, 22 insertions(+), 20 deletions(-) diff --git a/asm.js b/asm.js index 103f493..5099352 100644 --- a/asm.js +++ b/asm.js @@ -712,7 +712,7 @@ export class Assembler { 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); } @@ -799,7 +799,7 @@ export class Assembler { const data = this.data.at(-1).data; const value = action.value != null ? action.value - : this.le_bytes(this.lookup_def(action.symbol), action.size); + : this.le(this.lookup_def(action.symbol), action.size); data.push(...value); this.pos.addr += action.size; } @@ -871,17 +871,33 @@ export class Assembler { return this.defs[symbol]; } - le_bytes(value, count) { + le(value, count) { let bytes = [] - while (value != 0 && bytes.length < count) { + while (value != 0) { bytes.push(value & 0xff); value >>= 8; } - while (bytes.length < count) - bytes.push(0); + if (count != undefined) { + while (bytes.length < count) + bytes.push(0); + } return bytes; } + 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); + } + } + wasm_section_type() { const funcs = Object.values(this.funcs); if (funcs.length == 0) return null; @@ -1022,18 +1038,4 @@ export class Assembler { else 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); - } - } } -- 2.34.1 From 580d5d2a4a16952ec1f08ce4646902bf9de22945 Mon Sep 17 00:00:00 2001 From: Camden Dixie O'Brien Date: Sat, 14 Mar 2026 19:10:03 +0000 Subject: [PATCH 30/72] Implement function type de-duplication --- asm.js | 50 ++++++++++++++++++++++++++++++++++---------------- 1 file changed, 34 insertions(+), 16 deletions(-) diff --git a/asm.js b/asm.js index 5099352..d10eedb 100644 --- a/asm.js +++ b/asm.js @@ -708,6 +708,7 @@ export class Assembler { this.data = []; this.defs = {}; this.blocks = []; + this.types = []; } action_append(action) { @@ -899,19 +900,8 @@ export class Assembler { } 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() { @@ -933,9 +923,10 @@ 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_mem() { @@ -1014,6 +1005,8 @@ export class Assembler { } wasm() { + this.resolve_func_types(); + const template = [ [ Section.TYPE, () => this.wasm_section_type() ], [ Section.IMPORT, () => this.wasm_section_import() ], @@ -1038,4 +1031,29 @@ export class Assembler { else return [ flags, init ]; } + + func_type({ params, results }) { + const param_types = Object.values(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)); + } } -- 2.34.1 From 671e7f60d2cb2e12cacf26ce97b79df93511addc Mon Sep 17 00:00:00 2001 From: Camden Dixie O'Brien Date: Sat, 14 Mar 2026 19:17:28 +0000 Subject: [PATCH 31/72] Add a bunch of opcodes --- asm.js | 55 ++++++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 40 insertions(+), 15 deletions(-) diff --git a/asm.js b/asm.js index d10eedb..3668b19 100644 --- a/asm.js +++ b/asm.js @@ -127,21 +127,46 @@ const types = { }; 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, + "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.and": 0x71, + "i32.or": 0x72, + "i32.xor": 0x73, + "i32.shl": 0x74, + "i32.shr_s": 0x75, + "i32.shr_u": 0x76, }; const mem_flags = { -- 2.34.1 From a3cfd405a9f1517fa471372cc81f1d30b1928675 Mon Sep 17 00:00:00 2001 From: Camden Dixie O'Brien Date: Sat, 14 Mar 2026 19:23:52 +0000 Subject: [PATCH 32/72] Add some threads opcodes --- asm.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/asm.js b/asm.js index 3668b19..2c277fb 100644 --- a/asm.js +++ b/asm.js @@ -167,6 +167,14 @@ const opcodes = { "i32.shl": 0x74, "i32.shr_s": 0x75, "i32.shr_u": 0x76, + + // Threads instructions + "memory.atomic.notify": [ 0xfe, 0x00 ], + "memory.atomic.wait32": [ 0xfe, 0x01 ], + "memory.atomic.load": [ 0xfe, 0x10 ], + "memory.atomic.load8_u": [ 0xfe, 0x12 ], + "memory.atomic.store": [ 0xfe, 0x17 ], + "memory.atomic.store8": [ 0xfe, 0x19 ], }; const mem_flags = { -- 2.34.1 From d35b13fed0ee8ea7da74217409ae1a0e8c6d211a Mon Sep 17 00:00:00 2001 From: Camden Dixie O'Brien Date: Sun, 15 Mar 2026 11:05:37 +0000 Subject: [PATCH 33/72] Add .type directive --- asm.js | 89 +++++++++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 85 insertions(+), 4 deletions(-) diff --git a/asm.js b/asm.js index 2c277fb..9297868 100644 --- a/asm.js +++ b/asm.js @@ -97,6 +97,9 @@ const State = Object.freeze({ DEF_VALUE: 25, BLOCK_NAME: 26, BLOCK_TYPE: 27, + TYPE_NAME: 28, + TYPE_PARAM: 29, + TYPE_RESULT: 30, }); const Action = Object.freeze({ @@ -118,12 +121,14 @@ const Action = Object.freeze({ ENTER: 16, EXIT: 17, ELSE: 18, + TYPE: 19, }); const types = { - "void": 0x40, - "func": 0x60, - "i32": 0x7f, + "void": 0x40, + "func": 0x60, + "funcref": 0x70, + "i32": 0x7f, }; const opcodes = { @@ -208,6 +213,7 @@ class Parser { ".utf8": State.UTF8, ".align": State.ALIGN, ".def": State.DEF_NAME, + ".type": State.TYPE_NAME, }; this.blocks = new Set(["block", "loop", "if"]); this.handlers = { @@ -239,6 +245,9 @@ 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), }; this.results = []; @@ -669,6 +678,68 @@ 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); + } + mem_action() { const action = { type: Action.MEM, @@ -730,6 +801,7 @@ 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), }; this.exports = []; @@ -742,6 +814,7 @@ export class Assembler { this.defs = {}; this.blocks = []; this.types = []; + this.type_bindings = {}; } action_append(action) { @@ -874,6 +947,12 @@ 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; + } + push(chunk) { const text = this.decoder.decode(chunk, { stream: true }); for (const action of this.parser.handle(text)) @@ -1066,7 +1145,9 @@ export class Assembler { } func_type({ params, results }) { - const param_types = Object.values(params); + const param_types = params.length == undefined + ? Object.values(params) + : params; return [ types["func"], param_types.length, -- 2.34.1 From 46a571be93eba5689afd19ea473d24ac82e3af65 Mon Sep 17 00:00:00 2001 From: Camden Dixie O'Brien Date: Sun, 15 Mar 2026 11:05:56 +0000 Subject: [PATCH 34/72] Add error message for unhandled states and actions --- asm.js | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/asm.js b/asm.js index 9297868..5036c66 100644 --- a/asm.js +++ b/asm.js @@ -754,9 +754,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; } } } @@ -955,8 +961,13 @@ export class Assembler { 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) { -- 2.34.1 From 1452ffe6155f47347d4cb8661002ddc8513455df Mon Sep 17 00:00:00 2001 From: Camden Dixie O'Brien Date: Sun, 15 Mar 2026 12:14:35 +0000 Subject: [PATCH 35/72] Implement .table and .elem --- asm.js | 145 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 144 insertions(+), 1 deletion(-) diff --git a/asm.js b/asm.js index 5036c66..5f4a556 100644 --- a/asm.js +++ b/asm.js @@ -100,6 +100,10 @@ const State = Object.freeze({ TYPE_NAME: 28, TYPE_PARAM: 29, TYPE_RESULT: 30, + TABLE_NAME: 31, + TABLE_SIZE: 32, + ELEM_TABLE: 33, + ELEM_ELEM: 34, }); const Action = Object.freeze({ @@ -122,6 +126,8 @@ const Action = Object.freeze({ EXIT: 17, ELSE: 18, TYPE: 19, + TABLE: 20, + ELEM: 21, }); const types = { @@ -214,6 +220,8 @@ class Parser { ".align": State.ALIGN, ".def": State.DEF_NAME, ".type": State.TYPE_NAME, + ".table": State.TABLE_NAME, + ".elem": State.ELEM_TABLE, }; this.blocks = new Set(["block", "loop", "if"]); this.handlers = { @@ -248,6 +256,10 @@ class Parser { [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), }; this.results = []; @@ -740,6 +752,73 @@ class Parser { 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; + const action = { type: Action.ELEM, elem: this.elem }; + this.elem = undefined + this.state = State.TOP; + return action; + } + mem_action() { const action = { type: Action.MEM, @@ -771,9 +850,11 @@ const Section = Object.freeze({ TYPE: 0x01, IMPORT: 0x02, FUNC: 0x03, + TABLE: 0x04, MEM: 0x05, GLOBAL: 0x06, EXPORT: 0x07, + ELEM: 0x09, CODE: 0x0a, DATA: 0x0b, }); @@ -808,6 +889,8 @@ export class Assembler { [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 = []; @@ -821,6 +904,7 @@ export class Assembler { this.blocks = []; this.types = []; this.type_bindings = {}; + this.tables = {}; } action_append(action) { @@ -959,6 +1043,23 @@ export class Assembler { 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 index = Object.keys(this.funcs).indexOf(action.elem.elem); + if (index == -1) { + console.error(`ERROR: ${action.elem.elem}: no such function`); + return; + } + table.elems.push(index); + } + push(chunk) { const text = this.decoder.decode(chunk, { stream: true }); for (const action of this.parser.handle(text)) { @@ -1009,7 +1110,6 @@ export class Assembler { } leb128(x) { - const orig = x; const bytes = []; while (true) { const b = x & 0x7f; @@ -1022,6 +1122,18 @@ export class Assembler { } } + uleb128(x) { + const bytes = []; + do { + const b = x & 0x7f; + x >>= 7; + if (x != 0) + b |= 0x80; + bytes.push(b); + } while (x != 0); + return bytes; + } + wasm_section_type() { if (this.types.length == 0) return null; return [ this.types.length ].concat(...this.types); @@ -1052,6 +1164,17 @@ export class Assembler { 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() { const mems = Object.values(this.mems).filter( ({imported}) => !imported); @@ -1085,6 +1208,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; @@ -1134,9 +1275,11 @@ export class Assembler { [ 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() ], ]; -- 2.34.1 From 2155d177313da2ba46ec6a54dec132555aa2d3a3 Mon Sep 17 00:00:00 2001 From: Camden Dixie O'Brien Date: Sun, 15 Mar 2026 12:26:45 +0000 Subject: [PATCH 36/72] Implement type, table and func symbol resolution --- asm.js | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/asm.js b/asm.js index 5f4a556..705f04a 100644 --- a/asm.js +++ b/asm.js @@ -947,6 +947,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) { @@ -1092,6 +1095,20 @@ 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]; } -- 2.34.1 From 0dd2a925d8c4dcd0996e5183ac2ae0492ae8bc9d Mon Sep 17 00:00:00 2001 From: Camden Dixie O'Brien Date: Sun, 15 Mar 2026 12:34:41 +0000 Subject: [PATCH 37/72] Allow table elems to be labelled --- asm.js | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/asm.js b/asm.js index 705f04a..97f62b7 100644 --- a/asm.js +++ b/asm.js @@ -104,6 +104,7 @@ const State = Object.freeze({ TABLE_SIZE: 32, ELEM_TABLE: 33, ELEM_ELEM: 34, + ELEM_LABEL: 35, }); const Action = Object.freeze({ @@ -260,6 +261,7 @@ class Parser { [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), }; this.results = []; @@ -813,6 +815,12 @@ class Parser { } 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; @@ -1055,12 +1063,14 @@ export class Assembler { action_elem(action) { const table = this.tables[action.elem.table]; - const index = Object.keys(this.funcs).indexOf(action.elem.elem); - if (index == -1) { + const fn = Object.keys(this.funcs).indexOf(action.elem.elem); + if (fn == -1) { console.error(`ERROR: ${action.elem.elem}: no such function`); return; } - table.elems.push(index); + const index = table.elems.push(fn) - 1; + if (action.elem.label) + this.defs[action.elem.label] = index; } push(chunk) { -- 2.34.1 From 3ebb74c73cd6466f15494c9a5ed785644e30ea4c Mon Sep 17 00:00:00 2001 From: Camden Dixie O'Brien Date: Sun, 15 Mar 2026 13:40:43 +0000 Subject: [PATCH 38/72] Check for null explicitly in token_top() --- asm.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/asm.js b/asm.js index 97f62b7..3a2c7e4 100644 --- a/asm.js +++ b/asm.js @@ -316,7 +316,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 }; -- 2.34.1 From 7099ca34a3950ad25be830546458b1b0bdd45ab7 Mon Sep 17 00:00:00 2001 From: Camden Dixie O'Brien Date: Sun, 15 Mar 2026 13:41:05 +0000 Subject: [PATCH 39/72] Fix .word size --- asm.js | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/asm.js b/asm.js index 3a2c7e4..4128148 100644 --- a/asm.js +++ b/asm.js @@ -593,9 +593,14 @@ 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 & 0xff, + (value >> 8) & 0xff, + (value >> 16) & 0xff, + (value >> 24) & 0xff, + ]; } return action; } -- 2.34.1 From 7135eeba74f724083fb2ef0909dd5282cb4095cf Mon Sep 17 00:00:00 2001 From: Camden Dixie O'Brien Date: Sun, 15 Mar 2026 13:41:39 +0000 Subject: [PATCH 40/72] Restructure uleb128 --- asm.js | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/asm.js b/asm.js index 4128148..7db3575 100644 --- a/asm.js +++ b/asm.js @@ -1156,13 +1156,15 @@ export class Assembler { uleb128(x) { const bytes = []; - do { + while (true) { const b = x & 0x7f; x >>= 7; - if (x != 0) - b |= 0x80; - bytes.push(b); - } while (x != 0); + if (x == 0) { + bytes.push(b); + return bytes; + } + bytes.push(b | 0x80); + } return bytes; } -- 2.34.1 From 72c5f643122cec628c55897ec6cdb0c38241c3cd Mon Sep 17 00:00:00 2001 From: Camden Dixie O'Brien Date: Sun, 15 Mar 2026 13:42:02 +0000 Subject: [PATCH 41/72] Handle global init value encoding in Assembler --- asm.js | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/asm.js b/asm.js index 7db3575..9fce033 100644 --- a/asm.js +++ b/asm.js @@ -531,8 +531,7 @@ class Parser { 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"] ]; + this.global.init = value; const action = { type: Action.GLOBAL, global: { [this.global_name]: this.global } @@ -1222,8 +1221,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); } -- 2.34.1 From acf5b6e284a848e0e7a7b283bc5b57a08afb3b72 Mon Sep 17 00:00:00 2001 From: Camden Dixie O'Brien Date: Sun, 15 Mar 2026 13:42:56 +0000 Subject: [PATCH 42/72] Handle failed def lookup in action_data() --- asm.js | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/asm.js b/asm.js index 9fce033..38fe606 100644 --- a/asm.js +++ b/asm.js @@ -1009,9 +1009,16 @@ export class Assembler { action_data(action) { const data = this.data.at(-1).data; - const value = action.value != null - ? action.value - : this.le(this.lookup_def(action.symbol), action.size); + let value = action.value; + if (value == undefined) { + const raw = this.lookup_def(action.symbol); + if (raw == undefined) { + console.error( + `ERROR: Unable to resolve symbol ${action.symbol}`); + return; + } + value = this.le(raw, action.size); + } data.push(...value); this.pos.addr += action.size; } -- 2.34.1 From e9beacba3a60e297f7e045fea87f4d3fb10d7b65 Mon Sep 17 00:00:00 2001 From: Camden Dixie O'Brien Date: Sun, 15 Mar 2026 13:43:42 +0000 Subject: [PATCH 43/72] De-duplicate consecutive locals of same type in wasm_section_code() --- asm.js | 37 ++++++++++++++++++++++--------------- 1 file changed, 22 insertions(+), 15 deletions(-) diff --git a/asm.js b/asm.js index 38fe606..3cbe273 100644 --- a/asm.js +++ b/asm.js @@ -1278,24 +1278,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() { @@ -1369,4 +1365,15 @@ export class Assembler { 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; + }, []); + } } -- 2.34.1 From 9b4ff3e8f617d4b84f9bad20d3b27448f04f0d7e Mon Sep 17 00:00:00 2001 From: Camden Dixie O'Brien Date: Sun, 15 Mar 2026 13:44:21 +0000 Subject: [PATCH 44/72] Use array flattening instead of spread operator in a few places --- asm.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/asm.js b/asm.js index 3cbe273..bb03c0c 100644 --- a/asm.js +++ b/asm.js @@ -1306,7 +1306,7 @@ export class Assembler { ...data, ] }); - return [ contents.length ].concat(...contents); + return [ contents.length, contents ].flat(Infinity); } wasm() { @@ -1326,10 +1326,10 @@ export class Assembler { ]; const sections = template.map(([ code, generator ]) => { const body = generator(); - return body == null ? [] : [ code, body.length, ...body ]; + return body == null ? [] : [ code, body.length, body ]; }); - return new Uint8Array(HEADER.concat(...sections)); + return new Uint8Array([ HEADER, sections ].flat(Infinity)); } mem_wasm({ flags, init, max }) { -- 2.34.1 From 0056610238ab34979ade122cb6deaea209d856ad Mon Sep 17 00:00:00 2001 From: Camden Dixie O'Brien Date: Sun, 15 Mar 2026 13:44:45 +0000 Subject: [PATCH 45/72] Add missing semicolon --- asm.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/asm.js b/asm.js index bb03c0c..1f85c5f 100644 --- a/asm.js +++ b/asm.js @@ -961,7 +961,7 @@ export class Assembler { ?? this.lookup_global(action.symbol) ?? this.lookup_table(action.symbol) ?? this.lookup_type(action.symbol) - ?? this.lookup_func(action.symbol) + ?? this.lookup_func(action.symbol); if (value == null) { const def_value = this.lookup_def(action.symbol); if (def_value == null) { -- 2.34.1 From c93e9009daf7f047876c6a33923395751025a824 Mon Sep 17 00:00:00 2001 From: Camden Dixie O'Brien Date: Sun, 15 Mar 2026 13:45:03 +0000 Subject: [PATCH 46/72] LEB128-encode index in action_symbol --- asm.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/asm.js b/asm.js index 1f85c5f..4417384 100644 --- a/asm.js +++ b/asm.js @@ -971,7 +971,7 @@ export class Assembler { } func.body.push(...this.leb128(def_value)); } else { - func.body.push(value); + func.body.push(...this.leb128(value)); } } -- 2.34.1 From d4c837216a5ff2d207fa116181d17d929a738fdd Mon Sep 17 00:00:00 2001 From: Camden Dixie O'Brien Date: Sun, 15 Mar 2026 13:58:33 +0000 Subject: [PATCH 47/72] Add f32 type --- asm.js | 1 + 1 file changed, 1 insertion(+) diff --git a/asm.js b/asm.js index 4417384..031ec44 100644 --- a/asm.js +++ b/asm.js @@ -135,6 +135,7 @@ const types = { "void": 0x40, "func": 0x60, "funcref": 0x70, + "f32": 0x7d, "i32": 0x7f, }; -- 2.34.1 From 401e8e1fad254bc9a95bdf59d62c3728de4ba13b Mon Sep 17 00:00:00 2001 From: Camden Dixie O'Brien Date: Sun, 15 Mar 2026 14:07:26 +0000 Subject: [PATCH 48/72] Use unsigned right shift in Assembler.le() --- asm.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/asm.js b/asm.js index 031ec44..6c5d0f8 100644 --- a/asm.js +++ b/asm.js @@ -1136,10 +1136,10 @@ export class Assembler { } le(value, count) { - let bytes = [] + const bytes = [] while (value != 0) { bytes.push(value & 0xff); - value >>= 8; + value >>>= 8; } if (count != undefined) { while (bytes.length < count) -- 2.34.1 From b85a4e8bc9b8de47ffb18398f2bc64172a2aaf99 Mon Sep 17 00:00:00 2001 From: Camden Dixie O'Brien Date: Sun, 15 Mar 2026 14:07:40 +0000 Subject: [PATCH 49/72] Encode data values in assembler, not parser --- asm.js | 44 ++++++++++++++++++++++++-------------------- 1 file changed, 24 insertions(+), 20 deletions(-) diff --git a/asm.js b/asm.js index 6c5d0f8..0a902a2 100644 --- a/asm.js +++ b/asm.js @@ -578,7 +578,7 @@ class Parser { } else { if (value > 0xff) console.error(`WARNING: Value ${token} is truncated`); - action.value = [ value & 0xff ]; + action.value = value; } return action; } @@ -595,12 +595,7 @@ class Parser { } else { if (value > 0xffffffff) console.error(`WARNING: Value ${token} is truncated`); - action.value = [ - value & 0xff, - (value >> 8) & 0xff, - (value >> 16) & 0xff, - (value >> 24) & 0xff, - ]; + action.value = value; } return action; } @@ -614,8 +609,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; } @@ -1010,17 +1005,26 @@ export class Assembler { action_data(action) { const data = this.data.at(-1).data; - let value = action.value; - if (value == undefined) { - const raw = this.lookup_def(action.symbol); - if (raw == undefined) { - console.error( - `ERROR: Unable to resolve symbol ${action.symbol}`); - return; - } - value = this.le(raw, 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) { + console.error( + `ERROR: Unable to resolve symbol ${action.symbol}`); + return; + } + } + bytes = this.le(value, action.size); + } + data.push(...bytes); this.pos.addr += action.size; } -- 2.34.1 From 5e39024f6dd87839e6fd7f06880143c2b338d884 Mon Sep 17 00:00:00 2001 From: Camden Dixie O'Brien Date: Sun, 15 Mar 2026 14:15:40 +0000 Subject: [PATCH 50/72] Use unsigned shift in uleb128() --- asm.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/asm.js b/asm.js index 0a902a2..c60e27f 100644 --- a/asm.js +++ b/asm.js @@ -1169,7 +1169,7 @@ export class Assembler { const bytes = []; while (true) { const b = x & 0x7f; - x >>= 7; + x >>>= 7; if (x == 0) { bytes.push(b); return bytes; -- 2.34.1 From 8d4c53ca92dc4b90571569f709d1c771d206acd8 Mon Sep 17 00:00:00 2001 From: Camden Dixie O'Brien Date: Sun, 15 Mar 2026 14:26:22 +0000 Subject: [PATCH 51/72] Allow implicit zero-init for globals --- asm.js | 25 ++++++++++--------------- 1 file changed, 10 insertions(+), 15 deletions(-) diff --git a/asm.js b/asm.js index c60e27f..f8377a5 100644 --- a/asm.js +++ b/asm.js @@ -522,26 +522,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"); 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; - } + } + 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) { -- 2.34.1 From 3a103c46d1c647ede0f8c9b0d8a4b41428f25b57 Mon Sep 17 00:00:00 2001 From: Camden Dixie O'Brien Date: Sun, 15 Mar 2026 17:32:14 +0000 Subject: [PATCH 52/72] Don't require ; to have space after in comments --- asm.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/asm.js b/asm.js index f8377a5..6154f00 100644 --- a/asm.js +++ b/asm.js @@ -58,7 +58,8 @@ 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) this.comment = false; -- 2.34.1 From 6784cd02b4ff4329d07ef949f7d1174181cef056 Mon Sep 17 00:00:00 2001 From: Camden Dixie O'Brien Date: Sun, 15 Mar 2026 20:03:47 +0000 Subject: [PATCH 53/72] Encode section lengths with unsigned LEB128 --- asm.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/asm.js b/asm.js index 6154f00..2266591 100644 --- a/asm.js +++ b/asm.js @@ -1327,7 +1327,9 @@ export class Assembler { ]; const sections = template.map(([ code, generator ]) => { const body = generator(); - return body == null ? [] : [ code, body.length, body ]; + if (body == null) + return []; + return [ code, this.uleb128(body.length), body ]; }); return new Uint8Array([ HEADER, sections ].flat(Infinity)); -- 2.34.1 From 74a8f21379a34fe6ff34abb6f67b391fc587324a Mon Sep 17 00:00:00 2001 From: Camden Dixie O'Brien Date: Sun, 15 Mar 2026 20:04:06 +0000 Subject: [PATCH 54/72] Encode indices as unsigned LEB128 instead of signed --- asm.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/asm.js b/asm.js index 2266591..5041d3f 100644 --- a/asm.js +++ b/asm.js @@ -963,7 +963,7 @@ export class Assembler { } func.body.push(...this.leb128(def_value)); } else { - func.body.push(...this.leb128(value)); + func.body.push(...this.uleb128(value)); } } -- 2.34.1 From 1318c3cc4ec4464e039f429c39032e5af57f1b83 Mon Sep 17 00:00:00 2001 From: Camden Dixie O'Brien Date: Sun, 15 Mar 2026 20:04:27 +0000 Subject: [PATCH 55/72] Add i64.const, i32.div_s and i32.rem_s opcodes --- asm.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/asm.js b/asm.js index 5041d3f..4748b02 100644 --- a/asm.js +++ b/asm.js @@ -161,6 +161,7 @@ const opcodes = { "i32.store": 0x36, "i32.store8": 0x3a, "i32.const": 0x41, + "i64.const": 0x42, "i32.eqz": 0x45, "i32.eq": 0x46, "i32.ne": 0x47, @@ -175,6 +176,8 @@ const opcodes = { "i32.add": 0x6a, "i32.sub": 0x6b, "i32.mul": 0x6c, + "i32.div_s": 0x6d, + "i32.rem_s": 0x6f, "i32.and": 0x71, "i32.or": 0x72, "i32.xor": 0x73, -- 2.34.1 From c21b3c79c71dc7eb48ab4abc80ba5ef268e21060 Mon Sep 17 00:00:00 2001 From: Camden Dixie O'Brien Date: Sun, 15 Mar 2026 20:04:49 +0000 Subject: [PATCH 56/72] Fix names of atomic load and store opcodes --- asm.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/asm.js b/asm.js index 4748b02..80b032a 100644 --- a/asm.js +++ b/asm.js @@ -186,12 +186,12 @@ const opcodes = { "i32.shr_u": 0x76, // Threads instructions - "memory.atomic.notify": [ 0xfe, 0x00 ], - "memory.atomic.wait32": [ 0xfe, 0x01 ], - "memory.atomic.load": [ 0xfe, 0x10 ], - "memory.atomic.load8_u": [ 0xfe, 0x12 ], - "memory.atomic.store": [ 0xfe, 0x17 ], - "memory.atomic.store8": [ 0xfe, 0x19 ], + "memory.atomic.notify": [ 0xfe, 0x00 ], + "memory.atomic.wait32": [ 0xfe, 0x01 ], + "i32.atomic.load": [ 0xfe, 0x10 ], + "i32.atomic.load8_u": [ 0xfe, 0x12 ], + "i32.atomic.store": [ 0xfe, 0x17 ], + "i32.atomic.store8": [ 0xfe, 0x19 ], }; const mem_flags = { -- 2.34.1 From 02ee4c3c8843f1c3fcf3f9bd50908146c07a884b Mon Sep 17 00:00:00 2001 From: Camden Dixie O'Brien Date: Sun, 15 Mar 2026 21:27:48 +0000 Subject: [PATCH 57/72] Support symbols in .at address field --- asm.js | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/asm.js b/asm.js index 80b032a..9699496 100644 --- a/asm.js +++ b/asm.js @@ -550,15 +550,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; @@ -998,7 +993,8 @@ export class Assembler { return; } this.pos.mem = mem; - this.pos.addr = action.at.addr; + this.pos.addr = action.at.addr + ?? this.lookup_def(action.at.addr_symbol); this.data.push({ loc: { ...this.pos }, data: [] }) } -- 2.34.1 From e7affbf8b7e963de8114e4d25aab7ac2a16a1bf8 Mon Sep 17 00:00:00 2001 From: Camden Dixie O'Brien Date: Sun, 15 Mar 2026 21:28:25 +0000 Subject: [PATCH 58/72] Add .zero directive --- asm.js | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/asm.js b/asm.js index 9699496..e727a6d 100644 --- a/asm.js +++ b/asm.js @@ -106,6 +106,7 @@ const State = Object.freeze({ ELEM_TABLE: 33, ELEM_ELEM: 34, ELEM_LABEL: 35, + ZERO: 36, }); const Action = Object.freeze({ @@ -228,6 +229,7 @@ class Parser { ".type": State.TYPE_NAME, ".table": State.TABLE_NAME, ".elem": State.ELEM_TABLE, + ".zero": State.ZERO, }; this.blocks = new Set(["block", "loop", "if"]); this.handlers = { @@ -267,6 +269,7 @@ class Parser { [State.ELEM_TABLE]: (token) => this.token_elem_table(token), [State.ELEM_ELEM]: (token) => this.token_elem_elem(token), [State.ELEM_LABEL]: (token) => this.token_elem_label(token), + [State.ZERO]: (token) => this.token_zero(token), }; this.results = []; @@ -821,6 +824,26 @@ class Parser { return action; } + token_zero(token) { + if (token == LINE_END) { + console.error( + "ERROR: Unexpected newline in .zero, expected count") + this.state = State.TOP; + return; + } + + const count = this.integer(token); + if (count == null) { + console.error( + `ERROR: Unexpected token ${token} in .zero, expected count`); + this.state = State.TOP; + return; + } + + this.state = State.TOP; + return { type: Action.DATA, size: count, value: 0 } + } + mem_action() { const action = { type: Action.MEM, -- 2.34.1 From 7828b0f112b9af0a7d6f8b434b1e465c5ad05a88 Mon Sep 17 00:00:00 2001 From: Camden Dixie O'Brien Date: Sun, 15 Mar 2026 21:39:14 +0000 Subject: [PATCH 59/72] Yield newline token at end of comment --- asm.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/asm.js b/asm.js index e727a6d..55165e2 100644 --- a/asm.js +++ b/asm.js @@ -59,12 +59,14 @@ class Tokenizer { let token; while (token = this.next()) { if (token.string == undefined - && token.startsWith(this.comment_start)) + && token.startsWith(this.comment_start)) { this.comment = true; - else if (this.comment && token == LINE_END) + } else if (this.comment && token == LINE_END) { this.comment = false; - else if (!this.comment) + yield token; + } else if (!this.comment) { yield token; + } } } } -- 2.34.1 From 6c643f840223e197e34755d7fe206df0c6728ecc Mon Sep 17 00:00:00 2001 From: Camden Dixie O'Brien Date: Wed, 18 Mar 2026 10:29:46 +0000 Subject: [PATCH 60/72] Don't silently ignore trailing characters in numbers --- asm.js | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/asm.js b/asm.js index 55165e2..b58bca9 100644 --- a/asm.js +++ b/asm.js @@ -280,15 +280,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) { -- 2.34.1 From 37d56988ef7404b96975f1fea80bc8fd05bc5cc0 Mon Sep 17 00:00:00 2001 From: Camden Dixie O'Brien Date: Wed, 18 Mar 2026 10:30:26 +0000 Subject: [PATCH 61/72] Make a couple of tweaks to the kernel in preparation for porting --- wipforth.wat | 27 ++++++++++++++------------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/wipforth.wat b/wipforth.wat index 66cf67e..5e8d7ce 100644 --- a/wipforth.wat +++ b/wipforth.wat @@ -635,10 +635,12 @@ (func $key (local $head i32) global.get $RXHEAD i32.atomic.load8_u - local.tee $head + local.set $head ;; Wait for RXBUF to be non-empty - loop $wait (param i32) + loop $wait + local.get $head + global.get $RXTAIL i32.atomic.load8_u i32.eq @@ -648,7 +650,6 @@ i64.const -1 memory.atomic.wait32 - local.get $head br $wait end end @@ -1641,10 +1642,10 @@ ;; ;; \ Handle number ;; DROP \ Discard nil entry - ;; DUP NUMBER? 0BRANCH [44] \ Convert to number + ;; DUP NUMBER? 0BRANCH [48] \ Convert to number ;; SWAP DROP \ Discard word length - ;; STATE @ 0BRANCH [16] \ Check state - ;; LIT-CFA , , \ If compiling, append LIT and the value + ;; STATE @ 0BRANCH [20] \ Check state + ;; LIT LIT , , \ If compiling, append LIT and the value ;; EXIT ;; ;; \ Word was not found @@ -1682,14 +1683,15 @@ "\08\02\00\00" ;; DUP "\5c\09\00\00" ;; NUMBER? "\b4\04\00\00" ;; 0BRANCH - "\2c\00\00\00" ;; 44 + "\30\00\00\00" ;; 48 "\28\02\00\00" ;; SWAP "\18\02\00\00" ;; DROP "\5c\05\00\00" ;; STATE "\cc\03\00\00" ;; @ "\b4\04\00\00" ;; 0BRANCH - "\10\00\00\00" ;; 16 - "\00\05\00\00" ;; LIT-CFA + "\14\00\00\00" ;; 20 + "\18\04\00\00" ;; LIT + "\18\04\00\00" ;; LIT "\5c\0a\00\00" ;; , "\5c\0a\00\00" ;; , "\0c\04\00\00" ;; EXIT @@ -1697,11 +1699,10 @@ "\bc\0b\00\00" ;; EXECUTE "\0c\04\00\00") ;; EXIT - ;; => 0xb8 bytes + ;; => 0xbc bytes - ;; The previous version of INTERPRET was 0xc4 bytes, so we have 12 - ;; unused bytes here. Could fit an extra codeword definition in - ;; here, if the name is less than 4 bytes long. + ;; The previous version of INTERPRET was 0xc4 bytes, so we have 8 + ;; unused bytes here lol ;; : QUIT R0 RSP! INTERPRET BRANCH [-8] ; -- 2.34.1 From 896a1ca5634a2b7f6f87334e9e5b4c878927778f Mon Sep 17 00:00:00 2001 From: Camden Dixie O'Brien Date: Wed, 18 Mar 2026 10:32:49 +0000 Subject: [PATCH 62/72] Implement (limited) forward reference handling --- asm.js | 43 +++++++++++++++++++++++++++++++++++++++---- 1 file changed, 39 insertions(+), 4 deletions(-) diff --git a/asm.js b/asm.js index b58bca9..604b750 100644 --- a/asm.js +++ b/asm.js @@ -931,6 +931,7 @@ export class Assembler { this.types = []; this.type_bindings = {}; this.tables = {}; + this.unresolved = []; } action_append(action) { @@ -1036,9 +1037,14 @@ export class Assembler { } value = this.lookup_def(action.symbol); if (value == undefined) { - console.error( - `ERROR: Unable to resolve symbol ${action.symbol}`); - return; + this.unresolved.push({ + type: "data", + size: action.size, + symbol: action.symbol, + target: data, + offset: data.length, + }); + value = 0; } } bytes = this.le(value, action.size); @@ -1326,7 +1332,7 @@ export class Assembler { opcodes["i32.const"], ...this.leb128(loc.addr), opcodes["end"], - data.length, + ...this.uleb128(data.length), ...data, ] }); @@ -1334,6 +1340,7 @@ export class Assembler { } wasm() { + this.resolve_refs(); this.resolve_func_types(); const template = [ @@ -1402,4 +1409,32 @@ export class Assembler { return acc; }, []); } + + resolve_refs() { + const failed = []; + for (const ref of this.unresolved) { + if (ref.type != "data") { + console.error( + `ERROR: Unsupported ref type ${ref.type} for ` + + `symbol ${ref.symbol}` + ); + failed.push(ref.symbol); + continue; + } + + const value = this.defs[ref.symbol]; + if (value == undefined) { + failed.push(ref.symbol); + continue; + } + + const bytes = this.le(value, ref.size); + ref.target.splice(ref.offset, ref.size, ...bytes); + } + + if (failed.length != 0) { + const failed_str = failed.join(" "); + console.error(`ERROR: Unable to resolve refs: ${failed_str}`); + } + } } -- 2.34.1 From 5dc0a7a60107b06323311224e2e03b9fb702ed4f Mon Sep 17 00:00:00 2001 From: Camden Dixie O'Brien Date: Wed, 18 Mar 2026 10:35:14 +0000 Subject: [PATCH 63/72] Add temporary driver script --- driver.js | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 driver.js diff --git a/driver.js b/driver.js new file mode 100644 index 0000000..6b53cc5 --- /dev/null +++ b/driver.js @@ -0,0 +1,9 @@ +import { Assembler } from "./asm.js"; +import { writeAll } from "jsr:@std/io/write-all"; + +const asm = new Assembler(); +for await (const chunk of Deno.stdin.readable) { + asm.push(chunk); +} +const wasm = asm.wasm(); +await writeAll(Deno.stdout, wasm); -- 2.34.1 From 6ee4adfea5cb44c9a6f3e7f9bd75814ced10265b Mon Sep 17 00:00:00 2001 From: Camden Dixie O'Brien Date: Wed, 18 Mar 2026 10:36:12 +0000 Subject: [PATCH 64/72] Translate kernel to Wasmasm --- wipforth.wat | 2024 ------------------------------------------------ wipforth.ws | 2088 ++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 2088 insertions(+), 2024 deletions(-) delete mode 100644 wipforth.wat create mode 100644 wipforth.ws diff --git a/wipforth.wat b/wipforth.wat deleted file mode 100644 index 5e8d7ce..0000000 --- a/wipforth.wat +++ /dev/null @@ -1,2024 +0,0 @@ -;; Don't panic! WAT's just like assembly, really -- if you're missing -;; macros, labels and jumps. - -(module - (import "emu" "mem" (memory 1 1 shared)) - - ;; Peripheral registers - (global $TXBUF i32 (i32.const 0x0000)) - (global $RXBUF i32 (i32.const 0x0020)) - (global $TXHEAD i32 (i32.const 0x0040)) - (global $TXTAIL i32 (i32.const 0x0044)) - (global $RXHEAD i32 (i32.const 0x0048)) - (global $RXTAIL i32 (i32.const 0x004c)) - - ;; Forth registers - (global $rsp (mut i32) (i32.const 0)) - (global $sp (mut i32) (i32.const 0)) - (global $ip (mut i32) (i32.const 0)) - (global $cfa (mut i32) (i32.const 0)) - (global $fn (mut i32) (i32.const 0)) - - ;; Trampoline control flag - (global $run (mut i32) (i32.const 0)) - - ;; Some little helper functions - - (func $push (param $x i32) - global.get $sp - i32.const 4 - i32.sub - global.set $sp - - global.get $sp - local.get $x - i32.store) - - (func $pop (result i32) - global.get $sp - i32.load - - global.get $sp - i32.const 4 - i32.add - global.set $sp) - - (func $pushrsp (param $x i32) - global.get $rsp - i32.const 4 - i32.sub - global.set $rsp - - global.get $rsp - local.get $x - i32.store) - - (func $poprsp (result i32) - global.get $rsp - i32.load - - global.get $rsp - i32.const 4 - i32.add - global.set $rsp) - - ;; The rather bizzare nature of WebAssembly means that we can't - ;; actually jump to code, which makes this NEXT implemenation - ;; somewhat strange. Instead of doing the jump here, we store the - ;; function index (seemingly the closest you can get to a code - ;; address in WebAssembly) in the $fn global and return. It's then - ;; down to the $trampoline loop to actually run the codeword. - - (func $next - global.get $ip - global.get $ip - - i32.const 4 - i32.add - global.set $ip - - i32.load - global.set $cfa - global.get $cfa - i32.load - global.set $fn) - - ;; Our special inner interpreters <3 - - (func $docol - global.get $ip - call $pushrsp - - global.get $cfa - i32.const 4 - i32.add - global.set $ip - - call $next) - - (func $doval - global.get $cfa - i32.const 4 - i32.add - i32.load - call $push - call $next) - - (func $dovar - global.get $cfa - i32.const 4 - i32.add - call $push - call $next) - - ;; Codewords! - ;; - ;; Most of these are implemented here (as opposed to in the forth - ;; itself) for performance rather than necessity. - - ;; Stack manipulation - - (func $dup - global.get $sp - i32.load - call $push - call $next) - - (func $drop - global.get $sp - i32.const 4 - i32.add - global.set $sp - call $next) - - (func $swap - global.get $sp - i32.const 4 - i32.add - global.get $sp - i32.load - - global.get $sp - global.get $sp - i32.const 4 - i32.add - i32.load - - i32.store - i32.store - - call $next) - - (func $rot - global.get $sp - i32.const 4 - i32.add - global.get $sp - i32.load - - global.get $sp - i32.const 8 - i32.add - global.get $sp - i32.const 4 - i32.add - i32.load - - global.get $sp - global.get $sp - i32.const 8 - i32.add - i32.load - - i32.store - i32.store - i32.store - - call $next) - - (func $nrot - global.get $sp - i32.const 8 - i32.add - global.get $sp - i32.load - - global.get $sp - global.get $sp - i32.const 4 - i32.add - i32.load - - global.get $sp - i32.const 4 - i32.add - global.get $sp - i32.const 8 - i32.add - i32.load - - i32.store - i32.store - i32.store - - call $next) - - (func $over - global.get $sp - i32.const 4 - i32.add - i32.load - call $push - call $next) - - (func $twodup - global.get $sp i32.load - global.get $sp i32.const 4 i32.add i32.load - call $push - call $push - call $next) - - (func $twodrop - global.get $sp - i32.const 8 - i32.add - global.set $sp - call $next) - - (func $twoswap - global.get $sp - i32.const 8 - i32.add - global.get $sp - i32.load - - global.get $sp - i32.const 12 - i32.add - global.get $sp - i32.const 4 - i32.add - i32.load - - global.get $sp - global.get $sp - i32.const 8 - i32.add - i32.load - - global.get $sp - i32.const 4 - i32.add - global.get $sp - i32.const 12 - i32.add - i32.load - - i32.store - i32.store - i32.store - i32.store - - call $next) - - ;; Arithmetic and logic - - (func $inc - global.get $sp - global.get $sp - i32.load - i32.const 1 - i32.add - i32.store - call $next) - - (func $dec - global.get $sp - global.get $sp - i32.load - i32.const 1 - i32.sub - i32.store - call $next) - - (func $inc4 - global.get $sp - global.get $sp - i32.load - i32.const 4 - i32.add - i32.store - call $next) - - (func $dec4 - global.get $sp - global.get $sp - i32.load - i32.const 4 - i32.sub - i32.store - call $next) - - (func $add - call $pop - call $pop - i32.add - call $push - call $next) - - (func $sub (local $tmp i32) - call $pop - local.set $tmp - call $pop - local.get $tmp - i32.sub - call $push - call $next) - - (func $mul - call $pop - call $pop - i32.mul - call $push - call $next) - - (func $div (local $tmp i32) - call $pop - local.set $tmp - call $pop - local.get $tmp - i32.div_s - call $push - call $next) - - (func $mod (local $tmp i32) - call $pop - local.set $tmp - call $pop - local.get $tmp - i32.rem_s - call $push - call $next) - - (func $eq - call $pop - call $pop - i32.eq - i32.const 0 i32.sub - call $push - call $next) - - (func $neq - call $pop - call $pop - i32.ne - i32.const 0 i32.sub - call $push - call $next) - - (func $lt (local $tmp i32) - call $pop - local.set $tmp - call $pop - local.get $tmp - i32.lt_s - i32.const 0 i32.sub - call $push - call $next) - - (func $gt (local $tmp i32) - call $pop - local.set $tmp - call $pop - local.get $tmp - i32.gt_s - i32.const 0 i32.sub - call $push - call $next) - - (func $lte (local $tmp i32) - call $pop - local.set $tmp - call $pop - local.get $tmp - i32.le_s - i32.const 0 i32.sub - call $push - call $next) - - (func $gte (local $tmp i32) - call $pop - local.set $tmp - call $pop - local.get $tmp - i32.ge_s - i32.const 0 i32.sub - call $push - call $next) - - (func $zeq - call $pop - i32.eqz - i32.const 0 i32.sub - call $push - call $next) - - (func $zneq - call $pop - i32.const 0 - i32.ne - i32.const 0 i32.sub - call $push - call $next) - - (func $zlt - call $pop - i32.const 0 - i32.lt_s - i32.const 0 i32.sub - call $push - call $next) - - (func $zgt - call $pop - i32.const 0 - i32.gt_s - i32.const 0 i32.sub - call $push - call $next) - - (func $zlte - call $pop - i32.const 0 - i32.le_s - i32.const 0 i32.sub - call $push - call $next) - - (func $zgte - call $pop - i32.const 0 - i32.ge_s - i32.const 0 i32.sub - call $push - call $next) - - (func $and - call $pop - call $pop - i32.and - call $push - call $next) - - (func $or - call $pop - call $pop - i32.or - call $push - call $next) - - (func $xor - call $pop - call $pop - i32.xor - call $push - call $next) - - (func $invert - call $pop - i32.const 0xffffffff - i32.xor - call $push - call $next) - - ;; Memory - - (func $store - call $pop - call $pop - i32.store - call $next) - - (func $fetch - call $pop - i32.load - call $push - call $next) - - (func $addstore (local $tmp i32) - call $pop - local.tee $tmp - local.get $tmp - i32.load - call $pop - i32.add - i32.store - call $next) - - (func $substore (local $tmp i32) - call $pop - local.tee $tmp - local.get $tmp - i32.load - call $pop - i32.sub - i32.store - call $next) - - (func $storebyte - call $pop - call $pop - i32.store8 - call $next) - - (func $fetchbyte - call $pop - i32.load8_u - call $push - call $next) - - (func $atomic-store - call $pop - call $pop - i32.atomic.store - call $next) - - (func $atomic-fetch - call $pop - i32.atomic.load - call $push - call $next) - - (func $atomic-storebyte - call $pop - call $pop - i32.atomic.store8 - call $next) - - (func $atomic-fetchbyte - call $pop - i32.atomic.load8_u - call $push - call $next) - - (func $copy (local $src i32) (local $dst i32) (local $n i32) - call $pop local.set $dst - call $pop local.set $src - call $pop local.set $n - - block $done - loop $loop - local.get $n - i32.eqz br_if $done - - local.get $dst - local.get $src - i32.load8_u i32.store8 - - local.get $dst i32.const 1 i32.add local.set $dst - local.get $src i32.const 1 i32.add local.set $src - local.get $n i32.const 1 i32.sub local.set $n - - br $loop - end - end - - call $next) - - ;; Core utility words - - (func $exit - call $poprsp - global.set $ip - call $next) - - (func $lit - global.get $ip - i32.load - call $push - global.get $ip - i32.const 4 - i32.add - global.set $ip - call $next) - - (func $execute - call $pop - global.set $cfa - global.get $cfa - i32.load - global.set $fn) - - (func $halt i32.const 0 global.set $run) - - ;; Return and parameter stack primitives - - (func $tor - call $pop - call $pushrsp - call $next) - - (func $fromr - call $poprsp - call $push - call $next) - - (func $rspfetch - global.get $rsp - call $push - call $next) - - (func $rspstore - call $pop - global.set $rsp - call $next) - - (func $rdrop - global.get $rsp - i32.const 4 - i32.add - global.set $rsp - call $next) - - (func $spfetch - global.get $sp - call $push - call $next) - - (func $spstore - call $pop - global.set $sp - call $next) - - ;; Serial I/O - - (func $key (local $head i32) - global.get $RXHEAD - i32.atomic.load8_u - local.set $head - - ;; Wait for RXBUF to be non-empty - loop $wait - local.get $head - - global.get $RXTAIL - i32.atomic.load8_u - i32.eq - if - global.get $RXTAIL - local.get $head - i64.const -1 - memory.atomic.wait32 - - br $wait - end - end - - ;; Read byte at head position - global.get $RXBUF - local.get $head - i32.add - i32.load8_u - call $push - - ;; Advance RXHEAD - global.get $RXHEAD - local.get $head - i32.const 1 - i32.add - i32.const 0x1f - i32.and - i32.atomic.store8 - - call $next) - - (func $emit (local $tail i32) (local $next i32) - ;; Wait for TXBUF to be non-full - loop $wait - global.get $TXTAIL - i32.atomic.load8_u - local.tee $tail - i32.const 1 - i32.add - i32.const 0x1f - i32.and - local.tee $next - global.get $TXHEAD - i32.atomic.load8_u - i32.eq - br_if $wait - end - - ;; Write byte at tail position - global.get $TXBUF - local.get $tail - i32.add - call $pop - i32.store8 - - ;; Advance TXTAIL - global.get $TXTAIL - local.get $next - i32.atomic.store8 - - call $next) - - ;; Branching - - (func $branch - global.get $ip - i32.load - global.get $ip - i32.add - global.set $ip - call $next) - - (func $zbranch - call $pop - if (result i32) - i32.const 4 - else - global.get $ip - i32.load - end - global.get $ip - i32.add - global.set $ip - call $next) - - ;; The codewords function table must contain every codeword we want - ;; to be able to run, as it's indices into this table that can be ran - ;; with call_indirect in the $trampoline loop. Fantastically, there - ;; doesn't seem to be a way to sensibly give symbolic names to the - ;; indices the functions are inserted at so, instead of having all - ;; the entries here, most of them are defined in-line with the - ;; dictionary entries so that the definition of the index isn't too - ;; far from its use. - ;; - ;; The exceptions are the special inner interpreters, DOCOL, DOVAL - ;; and DOVAR, which I've put here in indices 0, 1 and 2 for - ;; convenience. - - (type $codeword (func)) - (table $codewords 100 funcref) - - (elem (i32.const 0x00) $docol) - (elem (i32.const 0x01) $doval) - (elem (i32.const 0x02) $dovar) - - ;; Dictionary time D: - ;; - ;; I <3 writing dictionary entries by hand! - - (elem (i32.const 0x03) $dup) - (data (i32.const 0x0200) - "\00\00\00\00" - "\03DUP" - "\03\00\00\00") - - (elem (i32.const 0x04) $drop) - (data (i32.const 0x020c) - "\00\02\00\00" - "\04DROP\00\00\00" - "\04\00\00\00") - - (elem (i32.const 0x05) $swap) - (data (i32.const 0x021c) - "\0c\02\00\00" - "\04SWAP\00\00\00" - "\05\00\00\00") - - (elem (i32.const 0x06) $rot) - (data (i32.const 0x022c) - "\1c\02\00\00" - "\03ROT" - "\06\00\00\00") - - (elem (i32.const 0x07) $nrot) - (data (i32.const 0x0238) - "\2c\02\00\00" - "\04-ROT\00\00\00" - "\07\00\00\00") - - (elem (i32.const 0x08) $over) - (data (i32.const 0x0248) - "\38\02\00\00" - "\04OVER\00\00\00" - "\08\00\00\00") - - (elem (i32.const 0x09) $twodup) - (data (i32.const 0x0258) - "\48\02\00\00" - "\042DUP\00\00\00" - "\09\00\00\00") - - (elem (i32.const 0x0a) $twodrop) - (data (i32.const 0x0268) - "\58\02\00\00" - "\052DROP\00\00" - "\0a\00\00\00") - - (elem (i32.const 0x0b) $twoswap) - (data (i32.const 0x0278) - "\68\02\00\00" - "\052SWAP\00\00" - "\0b\00\00\00") - - (elem (i32.const 0x0c) $inc) - (data (i32.const 0x0288) - "\78\02\00\00" - "\021+\00" - "\0c\00\00\00") - - (elem (i32.const 0x0d) $dec) - (data (i32.const 0x0294) - "\88\02\00\00" - "\021-\00" - "\0d\00\00\00") - - (elem (i32.const 0x0e) $inc4) - (data (i32.const 0x02a0) - "\94\02\00\00" - "\024+\00" - "\0e\00\00\00") - - (elem (i32.const 0x0f) $dec4) - (data (i32.const 0x02ac) - "\a0\02\00\00" - "\024-\00" - "\0f\00\00\00") - - (elem (i32.const 0x10) $add) - (data (i32.const 0x02b8) - "\ac\02\00\00" - "\01+\00\00" - "\10\00\00\00") - - (elem (i32.const 0x11) $sub) - (data (i32.const 0x02c4) - "\b8\02\00\00" - "\01-\00\00" - "\11\00\00\00") - - (elem (i32.const 0x12) $mul) - (data (i32.const 0x02d0) - "\c4\02\00\00" - "\01*\00\00" - "\12\00\00\00") - - (elem (i32.const 0x13) $div) - (data (i32.const 0x02dc) - "\d0\02\00\00" - "\01/\00\00" - "\13\00\00\00") - - (elem (i32.const 0x14) $mod) - (data (i32.const 0x02e8) - "\dc\02\00\00" - "\03MOD" - "\14\00\00\00") - - (elem (i32.const 0x15) $eq) - (data (i32.const 0x02f4) - "\e8\02\00\00" - "\01=\00\00" - "\15\00\00\00") - - (elem (i32.const 0x16) $neq) - (data (i32.const 0x0300) - "\f4\02\00\00" - "\02<>\00" - "\16\00\00\00") - - (elem (i32.const 0x17) $lt) - (data (i32.const 0x030c) - "\00\03\00\00" - "\01<\00\00" - "\17\00\00\00") - - (elem (i32.const 0x18) $gt) - (data (i32.const 0x0318) - "\0c\03\00\00" - "\01>\00\00" - "\18\00\00\00") - - (elem (i32.const 0x19) $lte) - (data (i32.const 0x0324) - "\18\03\00\00" - "\02<=\00" - "\19\00\00\00") - - (elem (i32.const 0x1a) $gte) - (data (i32.const 0x0330) - "\24\03\00\00" - "\02>=\00" - "\1a\00\00\00") - - (elem (i32.const 0x1b) $zeq) - (data (i32.const 0x033c) - "\30\03\00\00" - "\020=\00" - "\1b\00\00\00") - - (elem (i32.const 0x1c) $zneq) - (data (i32.const 0x0348) - "\3c\03\00\00" - "\030<>" - "\1c\00\00\00") - - (elem (i32.const 0x1d) $zlt) - (data (i32.const 0x0354) - "\48\03\00\00" - "\020<\00" - "\1d\00\00\00") - - (elem (i32.const 0x1e) $zgt) - (data (i32.const 0x0360) - "\54\03\00\00" - "\020>\00" - "\1e\00\00\00") - - (elem (i32.const 0x1f) $zlte) - (data (i32.const 0x036c) - "\60\03\00\00" - "\030<=" - "\1f\00\00\00") - - (elem (i32.const 0x20) $zgte) - (data (i32.const 0x0378) - "\6c\03\00\00" - "\030>=" - "\20\00\00\00") - - (elem (i32.const 0x21) $and) - (data (i32.const 0x0384) - "\78\03\00\00" - "\03AND" - "\21\00\00\00") - - (elem (i32.const 0x22) $or) - (data (i32.const 0x0390) - "\84\03\00\00" - "\02OR\00" - "\22\00\00\00") - - (elem (i32.const 0x23) $xor) - (data (i32.const 0x039c) - "\90\03\00\00" - "\03XOR" - "\23\00\00\00") - - (elem (i32.const 0x24) $invert) - (data (i32.const 0x03a8) - "\9c\03\00\00" - "\06INVERT\00" - "\24\00\00\00") - - (elem (i32.const 0x25) $store) - (data (i32.const 0x03b8) - "\a8\03\00\00" - "\01!\00\00" - "\25\00\00\00") - - (elem (i32.const 0x26) $fetch) - (data (i32.const 0x03c4) - "\b8\03\00\00" - "\01@\00\00" - "\26\00\00\00") - - (elem (i32.const 0x27) $addstore) - (data (i32.const 0x03d0) - "\c4\03\00\00" - "\02+!\00" - "\27\00\00\00") - - (elem (i32.const 0x28) $substore) - (data (i32.const 0x03dc) - "\d0\03\00\00" - "\02-!\00" - "\28\00\00\00") - - (elem (i32.const 0x29) $storebyte) - (data (i32.const 0x03e8) - "\dc\03\00\00" - "\02C!\00" - "\29\00\00\00") - - (elem (i32.const 0x2a) $fetchbyte) - (data (i32.const 0x03f4) - "\e8\03\00\00" - "\02C@\00" - "\2a\00\00\00") - - (elem (i32.const 0x2b) $exit) - (data (i32.const 0x0400) - "\f4\03\00\00" - "\04EXIT\00\00\00" - "\2b\00\00\00") - - (elem (i32.const 0x2c) $lit) - (data (i32.const 0x0410) - "\00\04\00\00" - "\03LIT" - "\2c\00\00\00") - - (elem (i32.const 0x2d) $tor) - (data (i32.const 0x041c) - "\10\04\00\00" - "\02>R\00" - "\2d\00\00\00") - - (elem (i32.const 0x2e) $fromr) - (data (i32.const 0x0428) - "\1c\04\00\00" - "\02R>\00" - "\2e\00\00\00") - - (elem (i32.const 0x2f) $rspfetch) - (data (i32.const 0x434) - "\28\04\00\00" - "\04RSP@\00\00\00" - "\2f\00\00\00") - - (elem (i32.const 0x30) $rspstore) - (data (i32.const 0x0444) - "\34\04\00\00" - "\04RSP!\00\00\00" - "\30\00\00\00") - - (elem (i32.const 0x31) $rdrop) - (data (i32.const 0x0454) - "\44\04\00\00" - "\05RDROP\00\00" - "\31\00\00\00") - - (elem (i32.const 0x32) $spfetch) - (data (i32.const 0x0464) - "\54\04\00\00" - "\03SP@" - "\32\00\00\00") - - (elem (i32.const 0x33) $spstore) - (data (i32.const 0x0470) - "\64\04\00\00" - "\03SP!" - "\33\00\00\00") - - (elem (i32.const 0x34) $key) - (data (i32.const 0x047c) - "\70\04\00\00" - "\03KEY" - "\34\00\00\00") - - (elem (i32.const 0x35) $emit) - (data (i32.const 0x0488) - "\7c\04\00\00" - "\04EMIT\00\00\00" - "\35\00\00\00") - - (elem (i32.const 0x36) $branch) - (data (i32.const 0x0498) - "\88\04\00\00" - "\06BRANCH\00" - "\36\00\00\00") - - (elem (i32.const 0x37) $zbranch) - (data (i32.const 0x04a8) - "\98\04\00\00" - "\070BRANCH" - "\37\00\00\00") - - ;; Built-in values and variables - - (data (i32.const 0x04b8) - "\a8\04\00\00" - "\05DOCOL\00\00" - "\01\00\00\00" - "\00\00\00\00") - - (data (i32.const 0x04cc) - "\b8\04\00\00" - "\05DOVAL\00\00" - "\01\00\00\00" - "\01\00\00\00") - - (data (i32.const 0x04e0) - "\cc\04\00\00" - "\05DOVAR\00\00" - "\01\00\00\00" - "\02\00\00\00") - - (data (i32.const 0x04f4) - "\e0\04\00\00" - "\07LIT-CFA" - "\01\00\00\00" - "\18\04\00\00") - - (data (i32.const 0x0508) - "\f4\04\00\00" - "\02R0\00" - "\01\00\00\00" - "\00\00\01\00") - - (data (i32.const 0x0518) - "\08\05\00\00" - "\02S0\00" - "\01\00\00\00" - "\00\f0\00\00") - - (data (i32.const 0x0528) - "\18\05\00\00" - "\04TRUE\00\00\00" - "\01\00\00\00" - "\ff\ff\ff\ff") - - (data (i32.const 0x053c) - "\28\05\00\00" - "\05FALSE\00\00" - "\01\00\00\00" - "\00\00\00\00") - - (data (i32.const 0x0550) - "\3c\05\00\00" - "\05STATE\00\00" - "\02\00\00\00" - "\00\00\00\00") - - (data (i32.const 0x0564) - "\50\05\00\00" - "\04BASE\00\00\00" - "\02\00\00\00" - "\0a\00\00\00") - - (data (i32.const 0x0578) - "\64\05\00\00" - "\04HERE\00\00\00" - "\02\00\00\00" - "\60\0e\00\00") - - (data (i32.const 0x058c) - "\78\05\00\00" - "\06LATEST\00" - "\02\00\00\00" - "\30\0e\00\00") - - (data (i32.const 0x05a0) - "\8c\05\00\00" - "\07WORDBUF" - "\02\00\00\00") - - ;; Skip 0x20 bytes for the word buffer - - (data (i32.const 0x05d0) - "\a0\05\00\00" - "\07WNFHOOK" - "\01\00\00\00" - "\f0\0d\00\00") - - ;; And now, it's time for some hand-compiled colon words. It - ;; probably would have made more sense to write most of these - ;; directly in WAT, but I think this is way cooler. - - ;; : TUCK DUP -ROT ; - - (data (i32.const 0x05e4) - "\d0\05\00\00" - "\04TUCK\00\00\00" - "\00\00\00\00" - "\08\02\00\00" ;; DUP - "\44\02\00\00" ;; -ROT - "\0c\04\00\00") ;; EXIT - - ;; => 0x1c bytes - - ;; : NSPACE ( byte -- bool ) - ;; DUP 9 <> SWAP \ Compare to horizontal tab - ;; DUP 10 <> SWAP \ Compare to line-feed - ;; DUP 13 <> SWAP \ Compare to carriage return - ;; 32 <> \ Compare to space - ;; AND AND AND \ And all results together - ;; ; - - (data (i32.const 0x0600) - "\e4\05\00\00" - "\06NSPACE\00" - "\00\00\00\00" - "\08\02\00\00" ;; DUP - "\18\04\00\00" ;; LIT - "\09\00\00\00" ;; 9 - "\08\03\00\00" ;; <> - "\28\02\00\00" ;; SWAP - "\08\02\00\00" ;; DUP - "\18\04\00\00" ;; LIT - "\0a\00\00\00" ;; 10 - "\08\03\00\00" ;; <> - "\28\02\00\00" ;; SWAP - "\08\02\00\00" ;; DUP - "\18\04\00\00" ;; LIT - "\0d\00\00\00" ;; 13 - "\08\03\00\00" ;; <> - "\28\02\00\00" ;; SWAP - "\18\04\00\00" ;; LIT - "\20\00\00\00" ;; 32 - "\08\03\00\00" ;; <> - "\8c\03\00\00" ;; AND - "\8c\03\00\00" ;; AND - "\8c\03\00\00" ;; AND - "\0c\04\00\00") ;; EXIT - - ;; => 0x68 bytes - - ;; : WORD ( -- len ) - ;; 0 \ Initial length - ;; - ;; KEY \ Get byte from input - ;; TUCK NSPACE 0BRANCH [60] \ Check if whitespace - ;; TUCK WORDBUF + C! \ Append byte to WORDBUF - ;; 1+ \ Increment length - ;; DUP 32 >= 0BRANCH [8] EXIT \ Exit if at max length - ;; BRANCH [-72] \ Loop back to KEY - ;; - ;; \ Byte is whitespace - ;; SWAP DROP - ;; DUP 0<> 0BRANCH [-96] \ Loop back to KEY if zero length - ;; ; - - (data (i32.const 0x0668) - "\00\06\00\00" - "\04WORD\00\00\00" - "\00\00\00\00" - "\18\04\00\00" ;; LIT - "\00\00\00\00" ;; 0 - "\84\04\00\00" ;; KEY - "\f0\05\00\00" ;; TUCK - "\0c\06\00\00" ;; NSPACE - "\b4\04\00\00" ;; 0BRANCH - "\3c\00\00\00" ;; 60 - "\f0\05\00\00" ;; TUCK - "\ac\05\00\00" ;; WORDBUF - "\c0\02\00\00" ;; + - "\f0\03\00\00" ;; C! - "\90\02\00\00" ;; 1+ - "\08\02\00\00" ;; DUP - "\18\04\00\00" ;; LIT - "\20\00\00\00" ;; 32 - "\38\03\00\00" ;; >= - "\b4\04\00\00" ;; 0BRANCH - "\08\00\00\00" ;; 8 - "\0c\04\00\00" ;; EXIT - "\a4\04\00\00" ;; BRANCH - "\b8\ff\ff\ff" ;; -72 - "\28\02\00\00" ;; SWAP - "\18\02\00\00" ;; DROP - "\08\02\00\00" ;; DUP - "\50\03\00\00" ;; 0<> - "\b4\04\00\00" ;; 0BRANCH - "\a0\ff\ff\ff" ;; -96 - "\0c\04\00\00") ;; EXIT - - ;; => 0x80 bytes - - ;; : STRING= ( len str1 str2 -- bool ) - ;; 2DUP C@ SWAP C@ \ Load a byte from each address - ;; = 0BRANCH [48] \ Check for byte mismatch - ;; 1+ -ROT 1+ -ROT 1- \ Increment addresses, decrement length - ;; DUP 0BRANCH [32] \ Check for zero remaining bytes - ;; -ROT BRANCH [-68] \ Loop - ;; - ;; 2DROP DROP FALSE EXIT \ Strings not equal - ;; 2DROP DROP TRUE \ Strings equal - ;; ; - - (data (i32.const 0x06e8) - "\68\06\00\00" - "\07STRING=" - "\00\00\00\00" - "\64\02\00\00" ;; 2DUP - "\fc\03\00\00" ;; C@ - "\28\02\00\00" ;; SWAP - "\fc\03\00\00" ;; C@ - "\fc\02\00\00" ;; = - "\b4\04\00\00" ;; 0BRANCH - "\30\00\00\00" ;; 48 - "\90\02\00\00" ;; 1+ - "\44\02\00\00" ;; -ROT - "\90\02\00\00" ;; 1+ - "\44\02\00\00" ;; -ROT - "\9c\02\00\00" ;; 1- - "\08\02\00\00" ;; DUP - "\b4\04\00\00" ;; 0BRANCH - "\20\00\00\00" ;; 32 - "\44\02\00\00" ;; -ROT - "\a4\04\00\00" ;; BRANCH - "\bc\ff\ff\ff" ;; -68 - "\74\02\00\00" ;; 2DROP - "\18\02\00\00" ;; DROP - "\48\05\00\00" ;; FALSE - "\0c\04\00\00" ;; EXIT - "\74\02\00\00" ;; 2DROP - "\18\02\00\00" ;; DROP - "\34\05\00\00" ;; TRUE - "\0c\04\00\00") ;; EXIT - - ;; => 0x78 bytes - - ;; : NAME-LEN 4+ C@ 31 AND ; - - (data (i32.const 0x0760) - "\e8\06\00\00" - "\08NAME-LEN\00\00\00" - "\00\00\00\00" - "\a8\02\00\00" ;; 4+ - "\fc\03\00\00" ;; C@ - "\18\04\00\00" ;; LIT - "\1f\00\00\00" ;; 31 - "\8c\03\00\00" ;; AND - "\0c\04\00\00") ;; EXIT - - ;; => 0x2c bytes - - ;; : FIND ( len -- entry ) - ;; LATEST @ \ Initial entry - ;; - ;; TUCK FIND-NAME-LEN \ Get name length - ;; OVER = 0BRANCH [52] \ Check for length mismatch - ;; OVER 5 + \ Get name address - ;; OVER SWAP WORDBUF STRING= \ Check if name matches - ;; 0BRANCH [12] - ;; DROP EXIT - ;; - ;; \ Name doesn't match - ;; OVER @ 0<> 0BRANCH [20] \ Check for nil link - ;; SWAP @ BRANCH [-104] \ Follow link and loop - ;; 2DROP FALSE \ Return false - ;; ; - - (data (i32.const 0x078c) - "\60\07\00\00" - "\04FIND\00\00\00" - "\00\00\00\00" - "\98\05\00\00" ;; LATEST - "\cc\03\00\00" ;; @ - "\f0\05\00\00" ;; TUCK - "\44\0e\00\00" ;; FIND-NAME-LEN - "\54\02\00\00" ;; OVER - "\fc\02\00\00" ;; = - "\b4\04\00\00" ;; 0BRANCH - "\34\00\00\00" ;; 52 - "\54\02\00\00" ;; OVER - "\18\04\00\00" ;; LIT - "\05\00\00\00" ;; 5 - "\c0\02\00\00" ;; + - "\54\02\00\00" ;; OVER - "\28\02\00\00" ;; SWAP - "\ac\05\00\00" ;; WORDBUF - "\f4\06\00\00" ;; STRING= - "\b4\04\00\00" ;; 0BRANCH - "\0c\00\00\00" ;; 12 - "\18\02\00\00" ;; DROP - "\0c\04\00\00" ;; EXIT - "\54\02\00\00" ;; OVER - "\cc\03\00\00" ;; @ - "\50\03\00\00" ;; 0<> - "\b4\04\00\00" ;; 0BRANCH - "\14\00\00\00" ;; 20 - "\28\02\00\00" ;; SWAP - "\cc\03\00\00" ;; @ - "\a4\04\00\00" ;; BRANCH - "\98\ff\ff\ff" ;; -104 - "\74\02\00\00" ;; 2DROP - "\48\05\00\00" ;; FALSE - "\0c\04\00\00") ;; EXIT - - ;; => 0x90 bytes - - ;; : >CFA ( entry -- cfa ) - ;; DUP NAME-LEN \ Get name length - ;; 5 + + \ Increment address to end of name - ;; DUP 3 AND 0BRANCH [16] \ Check if aligned - ;; 1+ BRANCH [-32] \ Increment address, loop to alignment check - ;; ; - - (data (i32.const 0x081c) - "\8c\07\00\00" - "\04>CFA\00\00\00" - "\00\00\00\00" - "\08\02\00\00" ;; DUP - "\70\07\00\00" ;; NAME-LEN - "\18\04\00\00" ;; LIT - "\05\00\00\00" ;; 5 - "\c0\02\00\00" ;; + - "\c0\02\00\00" ;; + - "\08\02\00\00" ;; DUP - "\18\04\00\00" ;; LIT - "\03\00\00\00" ;; 3 - "\8c\03\00\00" ;; AND - "\b4\04\00\00" ;; 0BRANCH - "\10\00\00\00" ;; 16 - "\90\02\00\00" ;; 1+ - "\a4\04\00\00" ;; BRANCH - "\e0\ff\ff\ff" ;; -32 - "\0c\04\00\00") ;; EXIT - - ;; => 0x50 bytes - - ;; : DIGIT ( byte -- value ) - ;; DUP DUP 48 >= SWAP 57 <= AND \ Test if 0-9 - ;; 0BRANCH [24] \ Jump to A-Z test if not - ;; 48 - \ Get digit value - ;; BRANCH [64] \ Go to range check - ;; - ;; DUP DUP 65 >= SWAP 90 <= AND \ Test if A-Z - ;; 0BRANCH [60] \ Jump to invalid digit if not - ;; 55 - \ Get digit value - ;; - ;; DUP DUP 0>= SWAP BASE @ < AND \ Test if 0 <= value < BASE - ;; 0BRANCH [8] \ Jump to invalid digit if not - ;; EXIT - ;; - ;; DROP TRUE \ Return -1 for an invalid digit - ;; ; - - (data (i32.const 0x086c) - "\1c\08\00\00" - "\05DIGIT\00\00" - "\00\00\00\00" - "\08\02\00\00" ;; DUP - "\08\02\00\00" ;; DUP - "\18\04\00\00" ;; LIT - "\30\00\00\00" ;; 48 - "\38\03\00\00" ;; >= - "\28\02\00\00" ;; SWAP - "\18\04\00\00" ;; LIT - "\39\00\00\00" ;; 57 - "\2c\03\00\00" ;; <= - "\8c\03\00\00" ;; AND - "\b4\04\00\00" ;; 0BRANCH - "\18\00\00\00" ;; 24 - "\18\04\00\00" ;; LIT - "\30\00\00\00" ;; 48 - "\cc\02\00\00" ;; - - "\a4\04\00\00" ;; BRANCH - "\40\00\00\00" ;; 64 - "\08\02\00\00" ;; DUP - "\08\02\00\00" ;; DUP - "\18\04\00\00" ;; LIT - "\41\00\00\00" ;; 65 - "\38\03\00\00" ;; >= - "\28\02\00\00" ;; SWAP - "\18\04\00\00" ;; LIT - "\5a\00\00\00" ;; 90 - "\2c\03\00\00" ;; <= - "\8c\03\00\00" ;; AND - "\b4\04\00\00" ;; 0BRANCH - "\3c\00\00\00" ;; 60 - "\18\04\00\00" ;; LIT - "\37\00\00\00" ;; 55 - "\cc\02\00\00" ;; - - "\08\02\00\00" ;; DUP - "\08\02\00\00" ;; DUP - "\80\03\00\00" ;; 0>= - "\28\02\00\00" ;; SWAP - "\70\05\00\00" ;; BASE - "\cc\03\00\00" ;; @ - "\14\03\00\00" ;; < - "\8c\03\00\00" ;; AND - "\b4\04\00\00" ;; 0BRANCH - "\08\00\00\00" ;; 8 - "\0c\04\00\00" ;; EXIT - "\18\02\00\00" ;; DROP - "\34\05\00\00" ;; TRUE - "\0c\04\00\00") ;; EXIT - - ;; => 0xc8 bytes - - ;; : NEGATE INVERT 1+ ; - - (data (i32.const 0x0934) - "\6c\08\00\00" - "\06NEGATE\00" - "\00\00\00\00" - "\b4\03\00\00" ;; INVERT - "\90\02\00\00" ;; 1+ - "\0c\04\00\00") ;; EXIT - - ;; => 0x1c bytes - - ;; : NUMBER? ( len -- value TRUE | FALSE ) - ;; WORDBUF \ Initial address - ;; - ;; DUP C@ 45 = 0BRANCH [60] \ Check for minus sign - ;; 1+ \ Increment address - ;; SWAP 1- \ Decrement length - ;; DUP 0BRANCH [24] \ Check if any characters remain - ;; TRUE -ROT \ Set negate flag true - ;; SWAP BRANCH [24] \ Jump to setting initial value - ;; - ;; \ No characters after minus, exit with failure - ;; 2DROP FALSE EXIT - ;; - ;; \ No leading minus, set negate flag false - ;; FALSE -ROT - ;; - ;; \ Set initial value - ;; 0 - ;; - ;; \ Main loop - ;; OVER C@ DIGIT \ Get byte's digit value - ;; DUP 0>= 0BRANCH [68] \ Check if digit valid - ;; - ;; SWAP BASE @ * + \ Incorporate digit - ;; SWAP 1+ \ Increment address - ;; ROT 1- \ Decrement length - ;; DUP 0BRANCH [40] \ Go to end if length zero - ;; -ROT SWAP BRANCH [-88] \ Loop if characters remain - ;; - ;; \ Invalid digit - ;; 2DROP 2DROP DROP FALSE EXIT \ Exit with failure - ;; - ;; \ Reached end of word - ;; 2DROP \ Discard pointer and length - ;; SWAP 0BRANCH [8] NEGATE \ Negate if -ve flag set - ;; TRUE \ Exit with success - ;; ; - - (data (i32.const 0x0950) - "\34\09\00\00" - "\07NUMBER?" - "\00\00\00\00" - "\ac\05\00\00" ;; WORDBUF - "\08\02\00\00" ;; DUP - "\fc\03\00\00" ;; C@ - "\18\04\00\00" ;; LIT - "\2d\00\00\00" ;; 45 - "\fc\02\00\00" ;; = - "\b4\04\00\00" ;; 0BRANCH - "\3c\00\00\00" ;; 60 - "\90\02\00\00" ;; 1+ - "\28\02\00\00" ;; SWAP - "\9c\02\00\00" ;; 1- - "\08\02\00\00" ;; DUP - "\b4\04\00\00" ;; 0BRANCH - "\18\00\00\00" ;; 24 - "\34\05\00\00" ;; TRUE - "\44\02\00\00" ;; -ROT - "\28\02\00\00" ;; SWAP - "\a4\04\00\00" ;; BRANCH - "\18\00\00\00" ;; 24 - "\74\02\00\00" ;; 2DROP - "\48\05\00\00" ;; FALSE - "\0c\04\00\00" ;; EXIT - "\48\05\00\00" ;; FALSE - "\44\02\00\00" ;; -ROT - "\18\04\00\00" ;; LIT - "\00\00\00\00" ;; 0 - "\54\02\00\00" ;; OVER - "\fc\03\00\00" ;; C@ - "\78\08\00\00" ;; DIGIT - "\08\02\00\00" ;; DUP - "\80\03\00\00" ;; 0>= - "\b4\04\00\00" ;; 0BRANCH - "\44\00\00\00" ;; 68 - "\28\02\00\00" ;; SWAP - "\70\05\00\00" ;; BASE - "\cc\03\00\00" ;; @ - "\d8\02\00\00" ;; * - "\c0\02\00\00" ;; + - "\28\02\00\00" ;; SWAP - "\90\02\00\00" ;; 1+ - "\34\02\00\00" ;; ROT - "\9c\02\00\00" ;; 1- - "\08\02\00\00" ;; DUP - "\b4\04\00\00" ;; 0BRANCH - "\28\00\00\00" ;; 40 - "\44\02\00\00" ;; -ROT - "\28\02\00\00" ;; SWAP - "\a4\04\00\00" ;; BRANCH - "\a8\ff\ff\ff" ;; -88 - "\74\02\00\00" ;; 2DROP - "\74\02\00\00" ;; 2DROP - "\18\02\00\00" ;; DROP - "\48\05\00\00" ;; FALSE - "\0c\04\00\00" ;; EXIT - "\74\02\00\00" ;; 2DROP - "\28\02\00\00" ;; SWAP - "\b4\04\00\00" ;; 0BRANCH - "\08\00\00\00" ;; 8 - "\40\09\00\00" ;; NEGATE - "\34\05\00\00" ;; TRUE - "\0c\04\00\00") ;; EXIT - - ;; => 0x104 bytes - - ;; : , HERE @ ! 4 HERE +! ; - - (data (i32.const 0x0a54) - "\50\09\00\00" - "\01,\00\00" - "\00\00\00\00" - "\84\05\00\00" ;; HERE - "\cc\03\00\00" ;; @ - "\c0\03\00\00" ;; ! - "\18\04\00\00" ;; LIT - "\04\00\00\00" ;; 4 - "\84\05\00\00" ;; HERE - "\d8\03\00\00" ;; +! - "\0c\04\00\00") ;; EXIT - - ;; => 0x2c bytes - - ;; : IMMEDIATE? 4+ @ 128 AND 0BRANCH [12] TRUE EXIT FALSE ; - - (data (i32.const 0x0a80) - "\54\0a\00\00" - "\0aIMMEDIATE?\00" - "\00\00\00\00" - "\a8\02\00\00" ;; 4+ - "\cc\03\00\00" ;; @ - "\18\04\00\00" ;; LIT - "\80\00\00\00" ;; 128 - "\8c\03\00\00" ;; AND - "\b4\04\00\00" ;; 0BRANCH - "\0c\00\00\00" ;; 12 - "\34\05\00\00" ;; TRUE - "\0c\04\00\00" ;; EXIT - "\48\05\00\00" ;; FALSE - "\0c\04\00\00") ;; EXIT - - ;; => 0x40 bytes - - ;; : INTERPRET - ;; WORD \ Read a word - ;; DUP FIND \ Look it up - ;; DUP 0BRANCH [72] \ Branch to number handling if not found - ;; SWAP DROP \ Discard word length - ;; - ;; \ Handle word - ;; DUP IMMEDIATE? \ Check if the word is immediate - ;; INVERT 0BRANCH [32] \ Jump straight to executing if so - ;; STATE @ 0BRANCH [16] \ Check state - ;; >CFA , EXIT \ Compile word - ;; >CFA EXECUTE EXIT \ Execute word - ;; - ;; \ Handle number - ;; DROP \ Discard nil entry - ;; DUP NUMBER? 0BRANCH [48] \ Convert to number - ;; SWAP DROP \ Discard word length - ;; STATE @ 0BRANCH [20] \ Check state - ;; LIT LIT , , \ If compiling, append LIT and the value - ;; EXIT - ;; - ;; \ Word was not found - ;; WNFHOOK EXECUTE - ;; ; - - (data (i32.const 0x0ac0) - "\80\0a\00\00" - "\09INTERPRET\00\00" - "\00\00\00\00" - "\74\06\00\00" ;; WORD - "\08\02\00\00" ;; DUP - "\98\07\00\00" ;; FIND - "\08\02\00\00" ;; DUP - "\b4\04\00\00" ;; 0BRANCH - "\48\00\00\00" ;; 72 - "\28\02\00\00" ;; SWAP - "\18\02\00\00" ;; DROP - "\08\02\00\00" ;; DUP - "\90\0a\00\00" ;; IMMEDIATE? - "\b4\03\00\00" ;; INVERT - "\b4\04\00\00" ;; 0BRANCH - "\20\00\00\00" ;; 32 - "\5c\05\00\00" ;; STATE - "\cc\03\00\00" ;; @ - "\b4\04\00\00" ;; 0BRANCH - "\10\00\00\00" ;; 16 - "\28\08\00\00" ;; >CFA - "\5c\0a\00\00" ;; , - "\0c\04\00\00" ;; EXIT - "\28\08\00\00" ;; >CFA - "\bc\0b\00\00" ;; EXECUTE - "\0c\04\00\00" ;; EXIT - "\18\02\00\00" ;; DROP - "\08\02\00\00" ;; DUP - "\5c\09\00\00" ;; NUMBER? - "\b4\04\00\00" ;; 0BRANCH - "\30\00\00\00" ;; 48 - "\28\02\00\00" ;; SWAP - "\18\02\00\00" ;; DROP - "\5c\05\00\00" ;; STATE - "\cc\03\00\00" ;; @ - "\b4\04\00\00" ;; 0BRANCH - "\14\00\00\00" ;; 20 - "\18\04\00\00" ;; LIT - "\18\04\00\00" ;; LIT - "\5c\0a\00\00" ;; , - "\5c\0a\00\00" ;; , - "\0c\04\00\00" ;; EXIT - "\dc\05\00\00" ;; WNFHOOK - "\bc\0b\00\00" ;; EXECUTE - "\0c\04\00\00") ;; EXIT - - ;; => 0xbc bytes - - ;; The previous version of INTERPRET was 0xc4 bytes, so we have 8 - ;; unused bytes here lol - - ;; : QUIT R0 RSP! INTERPRET BRANCH [-8] ; - - (data (i32.const 0x0b84) - "\c0\0a\00\00" - "\04QUIT\00\00\00" - "\00\00\00\00" - "\10\05\00\00" ;; R0 - "\50\04\00\00" ;; RSP! - "\d0\0a\00\00" ;; INTERPRET - "\a4\04\00\00" ;; BRANCH - "\f8\ff\ff\ff" ;; -8 - "\0c\04\00\00") ;; EXIT - - ;; => 0x28 bytes - - ;; Cold start - - (data (i32.const 0x0bac) "\90\0b\00\00") ;; QUIT - - ;; Bro thought she didn't need EXECUTE - - (elem (i32.const 0x38) $execute) - (data (i32.const 0x0bb0) - "\84\0b\00\00" - "\07EXECUTE" - "\38\00\00\00") - - ;; Some extra codewords I forgot to define earlier - - (elem (i32.const 0x39) $atomic-store) - (data (i32.const 0x0bc0) - "\b0\0b\00\00" - "\02A!\00" - "\39\00\00\00") - - (elem (i32.const 0x3a) $atomic-fetch) - (data (i32.const 0x0bcc) - "\c0\0b\00\00" - "\02A@\00" - "\3a\00\00\00") - - (elem (i32.const 0x3b) $atomic-storebyte) - (data (i32.const 0x0bd8) - "\c0\0b\00\00" - "\03AC!" - "\3b\00\00\00") - - (elem (i32.const 0x3c) $atomic-fetchbyte) - (data (i32.const 0x0be4) - "\d8\0b\00\00" - "\03AC@" - "\3c\00\00\00") - - (elem (i32.const 0x3d) $copy) - (data (i32.const 0x0bf0) - "\e4\0b\00\00" - "\04COPY\00\00\00" - "\3d\00\00\00") - - (elem (i32.const 0x3e) $halt) - (data (i32.const 0x0c00) - "\f0\0b\00\00" - "\04HALT\00\00\00" - "\3e\00\00\00") - - ;; And now to make myself obsolete as the compiler (finally) - - ;; : [ 0 STATE ! ; IMMEDIATE - - (data (i32.const 0x0c10) - "\00\0c\00\00" - "\81[\00\00" - "\00\00\00\00" - "\18\04\00\00" ;; LIT - "\00\00\00\00" ;; 0 - "\5c\05\00\00" ;; STATE - "\c0\03\00\00" ;; ! - "\0c\04\00\00") ;; EXIT - - ;; => 0x20 bytes - - ;; : ] 1 STATE ! ; - - (data (i32.const 0x0c30) - "\10\0c\00\00" - "\01]\00\00" - "\00\00\00\00" - "\18\04\00\00" ;; LIT - "\01\00\00\00" ;; 1 - "\5c\05\00\00" ;; STATE - "\c0\03\00\00" ;; ! - "\0c\04\00\00") ;; EXIT - - ;; => 0x20 bytes - - ;; : ALIGN - ;; DUP 3 AND 0BRANCH [32] - ;; 0 OVER C! 1+ BRANCH [-48] - ;; ; - - (data (i32.const 0x0c50) - "\30\0c\00\00" - "\05ALIGN\00\00" - "\00\00\00\00" - "\08\02\00\00" ;; DUP - "\18\04\00\00" ;; LIT - "\03\00\00\00" ;; 3 - "\8c\03\00\00" ;; AND - "\b4\04\00\00" ;; 0BRANCH - "\20\00\00\00" ;; 32 - "\18\04\00\00" ;; LIT - "\00\00\00\00" ;; 0 - "\54\02\00\00" ;; OVER - "\f0\03\00\00" ;; C! - "\90\02\00\00" ;; 1+ - "\a4\04\00\00" ;; BRANCH - "\d0\ff\ff\ff" ;; -48 - "\0c\04\00\00") ;; EXIT - - ;; => 0x48 bytes - - ;; : HIDDEN 4+ DUP C@ 32 XOR SWAP C! ; - - (data (i32.const 0x0c98) - "\50\0c\00\00" - "\06HIDDEN\00" - "\00\00\00\00" - "\a8\02\00\00" ;; 4+ - "\08\02\00\00" ;; DUP - "\fc\03\00\00" ;; C@ - "\18\04\00\00" ;; LIT - "\20\00\00\00" ;; 32 - "\a4\03\00\00" ;; XOR - "\28\02\00\00" ;; SWAP - "\f0\03\00\00" ;; C! - "\0c\04\00\00") ;; EXIT - - ;; => 0x34 bytes - - ;; : IMMEDIATE - ;; LATEST @ - ;; 4+ DUP C@ 128 XOR SWAP C! - ;; ; IMMEDIATE - - (data (i32.const 0x0ccc) - "\98\0c\00\00" - "\89IMMEDIATE\00\00" - "\00\00\00\00" - "\98\05\00\00" ;; LATEST - "\cc\03\00\00" ;; @ - "\a8\02\00\00" ;; 4+ - "\08\02\00\00" ;; DUP - "\fc\03\00\00" ;; C@ - "\18\04\00\00" ;; LIT - "\80\00\00\00" ;; 128 - "\a4\03\00\00" ;; XOR - "\28\02\00\00" ;; SWAP - "\f0\03\00\00" ;; C! - "\0c\04\00\00") ;; EXIT - - ;; => 0x40 bytes - - ;; : CREATE - ;; HERE @ \ Get initial address - ;; LATEST @ OVER ! 4+ \ Write link - ;; 2DUP C! 1+ \ Write length - ;; 2DUP WORDBUF SWAP COPY + \ Write name - ;; ALIGN \ Pad to alignment - ;; - ;; HERE @ LATEST ! \ Update LATEST - ;; HERE ! \ Update HERE - ;; ; - - (data (i32.const 0x0d0c) - "\cc\0c\00\00" - "\06CREATE\00" - "\00\00\00\00" - "\84\05\00\00" ;; HERE - "\cc\03\00\00" ;; @ - "\98\05\00\00" ;; LATEST - "\cc\03\00\00" ;; @ - "\54\02\00\00" ;; OVER - "\c0\03\00\00" ;; ! - "\a8\02\00\00" ;; 4+ - "\64\02\00\00" ;; 2DUP - "\f0\03\00\00" ;; C! - "\90\02\00\00" ;; 1+ - "\64\02\00\00" ;; 2DUP - "\ac\05\00\00" ;; WORDBUF - "\28\02\00\00" ;; SWAP - "\fc\0b\00\00" ;; COPY - "\c0\02\00\00" ;; + - "\5c\0c\00\00" ;; ALIGN - "\84\05\00\00" ;; HERE - "\cc\03\00\00" ;; @ - "\98\05\00\00" ;; LATEST - "\c0\03\00\00" ;; ! - "\84\05\00\00" ;; HERE - "\c0\03\00\00" ;; ! - "\0c\04\00\00") ;; EXIT - - ;; => 0x6c bytes - - ;; : : - ;; WORD - ;; CREATE - ;; DOCOL , - ;; LATEST @ HIDDEN - ;; ] - ;; ; - - (data (i32.const 0x0d78) - "\0c\0d\00\00" - "\01:\00\00" - "\00\00\00\00" - "\74\06\00\00" ;; WORD - "\18\0d\00\00" ;; CREATE - "\c4\04\00\00" ;; DOCOL - "\5c\0a\00\00" ;; , - "\98\05\00\00" ;; LATEST - "\cc\03\00\00" ;; @ - "\a4\0c\00\00" ;; HIDDEN - "\38\0c\00\00" ;; ] - "\0c\04\00\00") ;; EXIT - - ;; => 0x30 bytes - - ;; : ; - ;; LIT EXIT , - ;; LATEST @ HIDDEN - ;; [ - ;; ; IMMEDIATE - - (data (i32.const 0x0db4) - "\78\0d\00\00" - "\81;\00\00" - "\00\00\00\00" - "\18\04\00\00" ;; LIT - "\0c\04\00\00" ;; EXIT - "\5c\0a\00\00" ;; , - "\98\05\00\00" ;; LATEST - "\cc\03\00\00" ;; @ - "\a4\0c\00\00" ;; HIDDEN - "\18\0c\00\00" ;; [ - "\0c\04\00\00") ;; EXIT - - ;; => 0x2c bytes - - ;; : WNF-HANDLER - ;; DROP \ Discard word length - ;; 87 EMIT 78 EMIT 70 EMIT 10 EMIT \ Print WNF - ;; QUIT \ Reset return stack - ;; ; - - (data (i32.const 0x0de0) - "\b4\0d\00\00" - "\0bWNF-HANDLER" - "\00\00\00\00" - "\18\02\00\00" ;; DROP - "\18\04\00\00" ;; LIT - "\57\00\00\00" ;; 87 - "\94\04\00\00" ;; EMIT - "\18\04\00\00" ;; LIT - "\4e\00\00\00" ;; 78 - "\94\04\00\00" ;; EMIT - "\18\04\00\00" ;; LIT - "\46\00\00\00" ;; 70 - "\94\04\00\00" ;; EMIT - "\18\04\00\00" ;; LIT - "\0a\00\00\00" ;; 10 - "\94\04\00\00" ;; EMIT - "\90\0b\00\00" ;; QUIT - "\0c\04\00\00") ;; EXIT - - ;; => 0x50 bytes - - ;; : FIND-NAME-LEN 4+ C@ 63 AND ; - - ;; The 0x3f mask we use here includes the hidden flag, so that FIND - ;; never matches a hidden entry as its length will appear to be - ;; greater than the maximum length returned by WORD. - - (data (i32.const 0x0e30) - "\e0\0d\00\00" - "\0dFIND-NAME-LEN\00\00" - "\00\00\00\00" - "\a8\02\00\00" ;; 4+ - "\fc\03\00\00" ;; C@ - "\18\04\00\00" ;; LIT - "\3f\00\00\00" ;; 63 - "\8c\03\00\00" ;; AND - "\0c\04\00\00") ;; EXIT - - ;; => 0x30 bytes - - ;; The trampoline is a workaround for WebAssembly's lack of indirect - ;; jumps and code addresses. Instead of jumping into the next - ;; codeword, NEXT sets the $fn global to the function index of the - ;; next codeword and returns to the trampoline. - ;; - ;; This way, we can simulate jumping to the code within WebAssembly's - ;; restrictions without the WebAssembly call stack growing unbounded - ;; as would happen if we used call_indirect inside NEXT. - - (func $trampoline - loop $loop - global.get $fn call_indirect (type $codeword) - global.get $run br_if $loop - end) - - (func (export "reset") - i32.const 0x10000 global.set $rsp ;; Set the return stack pointer - i32.const 0xf000 global.set $sp ;; Set the stack pointer - i32.const 0x0bac global.set $ip ;; Set the IP to the cold start - call $next - - i32.const 1 global.set $run - call $trampoline)) diff --git a/wipforth.ws b/wipforth.ws new file mode 100644 index 0000000..2e84e8b --- /dev/null +++ b/wipforth.ws @@ -0,0 +1,2088 @@ +.mem main 1 1 shared +.import main "emu" "mem" + +;; Peripheral registers +.def TXBUF 00h +.def RXBUF 20h +.def TXHEAD 40h +.def TXTAIL 44h +.def RXHEAD 48h +.def RXTAIL 4Ch + +.def DICT_START 0200h + +.def RSP_INIT 10000h +.def SP_INIT F000h + +;; Forth registers +.global rsp i32 +.global sp i32 +.global ip i32 +.global cfa i32 +.global fn i32 + +;; Trampoline control flag +.global run i32 0 + +;; Some little helper functions + +.func push +.param x i32 + global.get sp + i32.const 4 + i32.sub + global.set sp + + global.get sp + local.get x + i32.store 2 0 + +.func pop +.result i32 + global.get sp + i32.load 2 0 + + global.get sp + i32.const 4 + i32.add + global.set sp + +.func pushrsp +.param x i32 + global.get rsp + i32.const 4 + i32.sub + global.set rsp + + global.get rsp + local.get x + i32.store 2 0 + +.func poprsp +.result i32 + global.get rsp + i32.load 2 0 + + global.get rsp + i32.const 4 + i32.add + global.set rsp + +;; The rather bizzare nature of WebAssembly means that we can't +;; actually jump to code, which makes this NEXT implemenation somewhat +;; strange. Instead of doing the jump here, we store the function +;; index (seemingly the closest you can get to a code address in +;; WebAssembly) in the fn global and return. It's then down to the +;; trampoline loop to actually run the codeword. + +.func next + global.get ip + global.get ip + + i32.const 4 + i32.add + global.set ip + + i32.load 2 0 + global.set cfa + global.get cfa + i32.load 2 0 + global.set fn + +;; The codewords function table must contain every codeword we want to +;; be able to run, as it's indices into this table that can be ran +;; with call_indirect in the $trampoline loop. + +.type codeword result +.table codewords 100 + +;; Our special inner interpreters <3 + +.func docol +.elem codewords docol DOCOL_CODEWORD + global.get ip + call pushrsp + + global.get cfa + i32.const 4 + i32.add + global.set ip + + call next + +.func doval +.elem codewords doval DOVAL_CODEWORD + global.get cfa + i32.const 4 + i32.add + i32.load 2 0 + call push + call next + +.func dovar +.elem codewords dovar DOVAR_CODEWORD + global.get cfa + i32.const 4 + i32.add + call push + call next + +;; Codewords! +;; +;; Most of these are implemented here (as opposed to in the forth +;; itself) for performance rather than necessity. + +;; Stack manipulation + +.func dup +.elem codewords dup DUP_CODEWORD + global.get sp + i32.load 2 0 + call push + call next + +.func drop +.elem codewords drop DROP_CODEWORD + global.get sp + i32.const 4 + i32.add + global.set sp + call next + +.func swap +.elem codewords swap SWAP_CODEWORD + global.get sp + i32.const 4 + i32.add + global.get sp + i32.load 2 0 + + global.get sp + global.get sp + i32.const 4 + i32.add + i32.load 2 0 + + i32.store 2 0 + i32.store 2 0 + + call next + +.func rot +.elem codewords rot ROT_CODEWORD + global.get sp + i32.const 4 + i32.add + global.get sp + i32.load 2 0 + + global.get sp + i32.const 8 + i32.add + global.get sp + i32.const 4 + i32.add + i32.load 2 0 + + global.get sp + global.get sp + i32.const 8 + i32.add + i32.load 2 0 + + i32.store 2 0 + i32.store 2 0 + i32.store 2 0 + + call next + +.func nrot +.elem codewords nrot NROT_CODEWORD + global.get sp + i32.const 8 + i32.add + global.get sp + i32.load 2 0 + + global.get sp + global.get sp + i32.const 4 + i32.add + i32.load 2 0 + + global.get sp + i32.const 4 + i32.add + global.get sp + i32.const 8 + i32.add + i32.load 2 0 + + i32.store 2 0 + i32.store 2 0 + i32.store 2 0 + + call next + +.func over +.elem codewords over OVER_CODEWORD + global.get sp + i32.const 4 + i32.add + i32.load 2 0 + call push + call next + +.func twodup +.elem codewords twodup TWODUP_CODEWORD + global.get sp i32.load 2 0 + global.get sp i32.const 4 i32.add i32.load 2 0 + call push + call push + call next + +.func twodrop +.elem codewords twodrop TWODROP_CODEWORD + global.get sp + i32.const 8 + i32.add + global.set sp + call next + +.func twoswap +.elem codewords twoswap TWOSWAP_CODEWORD + global.get sp + i32.const 8 + i32.add + global.get sp + i32.load 2 0 + + global.get sp + i32.const 12 + i32.add + global.get sp + i32.const 4 + i32.add + i32.load 2 0 + + global.get sp + global.get sp + i32.const 8 + i32.add + i32.load 2 0 + + global.get sp + i32.const 4 + i32.add + global.get sp + i32.const 12 + i32.add + i32.load 2 0 + + i32.store 2 0 + i32.store 2 0 + i32.store 2 0 + i32.store 2 0 + + call next + +;; Arithmetic and logic + +.func inc +.elem codewords inc INC_CODEWORD + global.get sp + global.get sp + i32.load 2 0 + i32.const 1 + i32.add + i32.store 2 0 + call next + +.func dec +.elem codewords dec DEC_CODEWORD + global.get sp + global.get sp + i32.load 2 0 + i32.const 1 + i32.sub + i32.store 2 0 + call next + +.func inc4 +.elem codewords inc4 INC4_CODEWORD + global.get sp + global.get sp + i32.load 2 0 + i32.const 4 + i32.add + i32.store 2 0 + call next + +.func dec4 +.elem codewords dec4 DEC4_CODEWORD + global.get sp + global.get sp + i32.load 2 0 + i32.const 4 + i32.sub + i32.store 2 0 + call next + +.func add +.elem codewords add ADD_CODEWORD + call pop + call pop + i32.add + call push + call next + +.func sub +.elem codewords sub SUB_CODEWORD +.local tmp i32 + call pop + local.set tmp + call pop + local.get tmp + i32.sub + call push + call next + +.func mul +.elem codewords mul MUL_CODEWORD + call pop + call pop + i32.mul + call push + call next + +.func div +.elem codewords div DIV_CODEWORD +.local tmp i32 + call pop + local.set tmp + call pop + local.get tmp + i32.div_s + call push + call next + +.func mod +.elem codewords mod MOD_CODEWORD +.local tmp i32 + call pop + local.set tmp + call pop + local.get tmp + i32.rem_s + call push + call next + +.func eq +.elem codewords eq EQ_CODEWORD + call pop + call pop + i32.eq + i32.const 0 i32.sub + call push + call next + +.func neq +.elem codewords neq NEQ_CODEWORD + call pop + call pop + i32.ne + i32.const 0 i32.sub + call push + call next + +.func lt +.elem codewords lt LT_CODEWORD +.local tmp i32 + call pop + local.set tmp + call pop + local.get tmp + i32.lt_s + i32.const 0 i32.sub + call push + call next + +.func gt +.elem codewords gt GT_CODEWORD +.local tmp i32 + call pop + local.set tmp + call pop + local.get tmp + i32.gt_s + i32.const 0 i32.sub + call push + call next + +.func lte +.elem codewords lte LTE_CODEWORD +.local tmp i32 + call pop + local.set tmp + call pop + local.get tmp + i32.le_s + i32.const 0 i32.sub + call push + call next + +.func gte +.elem codewords gte GTE_CODEWORD +.local tmp i32 + call pop + local.set tmp + call pop + local.get tmp + i32.ge_s + i32.const 0 i32.sub + call push + call next + +.func zeq +.elem codewords zeq ZEQ_CODEWORD + call pop + i32.eqz + i32.const 0 i32.sub + call push + call next + +.func zneq +.elem codewords zneq ZNEQ_CODEWORD + call pop + i32.const 0 + i32.ne + i32.const 0 i32.sub + call push + call next + +.func zlt +.elem codewords zlt ZLT_CODEWORD + call pop + i32.const 0 + i32.lt_s + i32.const 0 i32.sub + call push + call next + +.func zgt +.elem codewords zgt ZGT_CODEWORD + call pop + i32.const 0 + i32.gt_s + i32.const 0 i32.sub + call push + call next + +.func zlte +.elem codewords zlte ZLTE_CODEWORD + call pop + i32.const 0 + i32.le_s + i32.const 0 i32.sub + call push + call next + +.func zgte +.elem codewords zgte ZGTE_CODEWORD + call pop + i32.const 0 + i32.ge_s + i32.const 0 i32.sub + call push + call next + +.func and +.elem codewords and AND_CODEWORD + call pop + call pop + i32.and + call push + call next + +.func or +.elem codewords or OR_CODEWORD + call pop + call pop + i32.or + call push + call next + +.func xor +.elem codewords xor XOR_CODEWORD + call pop + call pop + i32.xor + call push + call next + +.func invert +.elem codewords invert INVERT_CODEWORD + call pop + i32.const FFFFFFFFh + i32.xor + call push + call next + +;; Memory + +.func store +.elem codewords store STORE_CODEWORD + call pop + call pop + i32.store 2 0 + call next + +.func fetch +.elem codewords fetch FETCH_CODEWORD + call pop + i32.load 2 0 + call push + call next + +.func addstore +.elem codewords addstore ADDSTORE_CODEWORD +.local tmp i32 + call pop + local.tee tmp + local.get tmp + i32.load 2 0 + call pop + i32.add + i32.store 2 0 + call next + +.func substore +.elem codewords substore SUBSTORE_CODEWORD +.local tmp i32 + call pop + local.tee tmp + local.get tmp + i32.load 2 0 + call pop + i32.sub + i32.store 2 0 + call next + +.func storebyte +.elem codewords storebyte STOREBYTE_CODEWORD + call pop + call pop + i32.store8 0 0 + call next + +.func fetchbyte +.elem codewords fetchbyte FETCHBYTE_CODEWORD + call pop + i32.load8_u 0 0 + call push + call next + +.func atomic-store +.elem codewords atomic-store ATOMIC_STORE_CODEWORD + call pop + call pop + i32.atomic.store 2 0 + call next + +.func atomic-fetch +.elem codewords atomic-fetch ATOMIC_FETCH_CODEWORD + call pop + i32.atomic.load 2 0 + call push + call next + +.func atomic-storebyte +.elem codewords atomic-storebyte ATOMIC_STOREBYTE_CODEWORD + call pop + call pop + i32.atomic.store8 0 0 + call next + +.func atomic-fetchbyte +.elem codewords atomic-fetchbyte ATOMIC_FETCHBYTE_CODEWORD + call pop + i32.atomic.load8_u 0 0 + call push + call next + +.func copy +.elem codewords copy COPY_CODEWORD +.local src i32 dst i32 n i32 + call pop local.set dst + call pop local.set src + call pop local.set n + block done + loop iter + local.get n + i32.eqz br_if done + + local.get dst + local.get src + i32.load8_u 0 0 + i32.store8 0 0 + + local.get dst i32.const 1 i32.add local.set dst + local.get src i32.const 1 i32.add local.set src + local.get n i32.const 1 i32.sub local.set n + + br iter + end + end + call next + +;; Core utility words + +.func exit +.elem codewords exit EXIT_CODEWORD + call poprsp + global.set ip + call next + +.func lit +.elem codewords lit LIT_CODEWORD + global.get ip + i32.load 2 0 + call push + global.get ip + i32.const 4 + i32.add + global.set ip + call next + +.func execute +.elem codewords execute EXECUTE_CODEWORD + call pop + global.set cfa + global.get cfa + i32.load 2 0 + global.set fn + +.func halt +.elem codewords halt HALT_CODEWORD + i32.const 0 + global.set run + +;; Return and parameter stack primitives + +.func tor +.elem codewords tor TOR_CODEWORD + call pop + call pushrsp + call next + +.func fromr +.elem codewords fromr FROMR_CODEWORD + call poprsp + call push + call next + +.func rspfetch +.elem codewords rspfetch RSPFETCH_CODEWORD + + global.get rsp + call push + call next + +.func rspstore +.elem codewords rspstore RSPSTORE_CODEWORD + call pop + global.set rsp + call next + +.func rdrop +.elem codewords rdrop RDROP_CODEWORD + global.get rsp + i32.const 4 + i32.add + global.set rsp + call next + +.func spfetch +.elem codewords spfetch SPFETCH_CODEWORD + global.get sp + call push + call next + +.func spstore +.elem codewords spstore SPSTORE_CODEWORD + call pop + global.set sp + call next + +;; Serial I/O + +.func key +.elem codewords key KEY_CODEWORD +.local head i32 + i32.const RXHEAD + i32.atomic.load8_u 0 0 + local.set head + + ;; Wait for RXBUF to be non-empty + loop wait + local.get head + + i32.const RXTAIL + i32.atomic.load8_u 0 0 + i32.eq + if + i32.const RXTAIL + local.get head + i64.const -1 + memory.atomic.wait32 2 0 + + br wait + end + end + + ;; Read byte at head position + i32.const RXBUF + local.get head + i32.add + i32.load8_u 0 0 + call push + + ;; Advance RXHEAD + i32.const RXHEAD + local.get head + i32.const 1 + i32.add + i32.const 1Fh + i32.and + i32.atomic.store8 0 0 + + call next + +.func emit +.elem codewords emit EMIT_CODEWORD +.local tail i32 n i32 + ;; Wait for TXBUF to be non-full + loop wait + i32.const TXTAIL + i32.atomic.load8_u 0 0 + local.tee tail + i32.const 1 + i32.add + i32.const 1Fh + i32.and + local.tee n + i32.const TXHEAD + i32.atomic.load8_u 0 0 + i32.eq + br_if wait + end + + ;; Write byte at tail position + i32.const TXBUF + local.get tail + i32.add + call pop + i32.store8 0 0 + + ;; Advance TXTAIL + i32.const TXTAIL + local.get n + i32.atomic.store8 0 0 + + call next + +;; Branching + +.func branch +.elem codewords branch BRANCH_CODEWORD + global.get ip + i32.load 2 0 + global.get ip + i32.add + global.set ip + call next + +.func zbranch +.elem codewords zbranch ZBRANCH_CODEWORD + call pop + if _ i32 + i32.const 4 + else + global.get ip + i32.load 2 0 + end + global.get ip + i32.add + global.set ip + call next + +;; Dictionary time :D + +.at main DICT_START +.def PREV 0 + +_DUP: + .word PREV + .byte 3 + .utf8 "DUP" + .align +DUP: + .word DUP_CODEWORD +.def PREV _DUP + +_DROP: + .word PREV + .byte 4 + .utf8 "DROP" + .align +DROP: + .word DROP_CODEWORD +.def PREV _DROP + +_SWAP: + .word PREV + .byte 4 + .utf8 "SWAP" + .align +SWAP: + .word SWAP_CODEWORD +.def PREV _SWAP + +_ROT: + .word PREV + .byte 3 + .utf8 "ROT" + .align +ROT: + .word ROT_CODEWORD +.def PREV _ROT + +_NROT: + .word PREV + .byte 4 + .utf8 "-ROT" + .align +NROT: + .word NROT_CODEWORD +.def PREV _NROT + +_OVER: + .word PREV + .byte 4 + .utf8 "OVER" + .align +OVER: + .word OVER_CODEWORD +.def PREV _OVER + +_TWODUP: + .word PREV + .byte 4 + .utf8 "2DUP" + .align +TWODUP: + .word TWODUP_CODEWORD +.def PREV _TWODUP + +_TWODROP: + .word PREV + .byte 5 + .utf8 "2DROP" + .align +TWODROP: + .word TWODROP_CODEWORD +.def PREV _TWODROP + +_TWOSWAP: + .word PREV + .byte 5 + .utf8 "2SWAP" + .align +TWOSWAP: + .word TWOSWAP_CODEWORD +.def PREV _TWOSWAP + +_INC: + .word PREV + .byte 2 + .utf8 "1+" + .align +INC: + .word INC_CODEWORD +.def PREV _INC + +_DEC: + .word PREV + .byte 2 + .utf8 "1-" + .align +DEC: + .word DEC_CODEWORD +.def PREV _DEC + +_INC4: + .word PREV + .byte 2 + .utf8 "4+" + .align +INC4: + .word INC4_CODEWORD +.def PREV _INC4 + +_DEC4: + .word PREV + .byte 2 + .utf8 "4-" + .align +DEC4: + .word DEC4_CODEWORD +.def PREV _DEC4 + +_ADD: + .word PREV + .byte 1 + .utf8 "+" + .align +ADD: + .word ADD_CODEWORD +.def PREV _ADD + +_SUB: + .word PREV + .byte 1 + .utf8 "-" + .align +SUB: + .word SUB_CODEWORD +.def PREV _SUB + +_MUL: + .word PREV + .byte 1 + .utf8 "*" + .align +MUL: + .word MUL_CODEWORD +.def PREV _MUL + +_DIV: + .word PREV + .byte 1 + .utf8 "/" + .align +DIV: + .word DIV_CODEWORD +.def PREV _DIV + +_MOD: + .word PREV + .byte 3 + .utf8 "MOD" + .align +MOD: + .word MOD_CODEWORD +.def PREV _MOD + +_EQ: + .word PREV + .byte 1 + .utf8 "=" + .align +EQ: + .word EQ_CODEWORD +.def PREV _EQ + +_NEQ: + .word PREV + .byte 2 + .utf8 "<>" + .align +NEQ: + .word NEQ_CODEWORD +.def PREV _NEQ + +_LT: + .word PREV + .byte 1 + .utf8 "<" + .align +LT: + .word LT_CODEWORD +.def PREV _LT + +_GT: + .word PREV + .byte 1 + .utf8 ">" + .align +GT: + .word GT_CODEWORD +.def PREV _GT + +_LTE: + .word PREV + .byte 2 + .utf8 "<=" + .align +LTE: + .word LTE_CODEWORD +.def PREV _LTE + +_GTE: + .word PREV + .byte 2 + .utf8 ">=" + .align +GTE: + .word GTE_CODEWORD +.def PREV _GTE + +_ZEQ: + .word PREV + .byte 2 + .utf8 "0=" + .align +ZEQ: + .word ZEQ_CODEWORD +.def PREV _ZEQ + +_ZNEQ: + .word PREV + .byte 3 + .utf8 "0<>" + .align +ZNEQ: + .word ZNEQ_CODEWORD +.def PREV _ZNEQ + +_ZLT: + .word PREV + .byte 2 + .utf8 "0<" + .align +ZLT: + .word ZLT_CODEWORD +.def PREV _ZLT + +_ZGT: + .word PREV + .byte 2 + .utf8 "0>" + .align +ZGT: + .word ZGT_CODEWORD +.def PREV _ZGT + +_ZLTE: + .word PREV + .byte 3 + .utf8 "0<=" + .align +ZLTE: + .word ZLTE_CODEWORD +.def PREV _ZLTE + +_ZGTE: + .word PREV + .byte 3 + .utf8 "0>=" + .align +ZGTE: + .word ZGTE_CODEWORD +.def PREV _ZGTE + +_AND: + .word PREV + .byte 3 + .utf8 "AND" + .align +AND: + .word AND_CODEWORD +.def PREV _AND + +_OR: + .word PREV + .byte 2 + .utf8 "OR" + .align +OR: + .word OR_CODEWORD +.def PREV _OR + +_XOR: + .word PREV + .byte 3 + .utf8 "XOR" + .align +XOR: + .word XOR_CODEWORD +.def PREV _XOR + +_INVERT: + .word PREV + .byte 6 + .utf8 "INVERT" + .align +INVERT: + .word INVERT_CODEWORD +.def PREV _INVERT + +_STORE: + .word PREV + .byte 1 + .utf8 "!" + .align +STORE: + .word STORE_CODEWORD +.def PREV _STORE + +_FETCH: + .word PREV + .byte 1 + .utf8 "@" + .align +FETCH: + .word FETCH_CODEWORD +.def PREV _FETCH + +_ADDSTORE: + .word PREV + .byte 2 + .utf8 "+!" + .align +ADDSTORE: + .word ADDSTORE_CODEWORD +.def PREV _ADDSTORE + +_SUBSTORE: + .word PREV + .byte 2 + .utf8 "-!" + .align +SUBSTORE: + .word SUBSTORE_CODEWORD +.def PREV _SUBSTORE + +_STOREBYTE: + .word PREV + .byte 2 + .utf8 "C!" + .align +STOREBYTE: + .word STOREBYTE_CODEWORD +.def PREV _STOREBYTE + +_FETCHBYTE: + .word PREV + .byte 2 + .utf8 "C@" + .align +FETCHBYTE: + .word FETCHBYTE_CODEWORD +.def PREV _FETCHBYTE + +_ATOMIC_STORE: + .word PREV + .byte 2 + .utf8 "A!" + .align +ATOMIC_STORE: + .word ATOMIC_STORE_CODEWORD +.def PREV _ATOMIC_STORE + +_ATOMIC_FETCH: + .word PREV + .byte 2 + .utf8 "A@" + .align +ATOMIC_FETCH: + .word ATOMIC_FETCH_CODEWORD +.def PREV _ATOMIC_FETCH + +_ATOMIC_STOREBYTE: + .word PREV + .byte 3 + .utf8 "AC!" + .align +ATOMIC_STOREBYTE: + .word ATOMIC_STOREBYTE_CODEWORD +.def PREV _ATOMIC_STOREBYTE + +_ATOMIC_FETCHBYTE: + .word PREV + .byte 3 + .utf8 "AC@" + .align +ATOMIC_FETCHBYTE: + .word ATOMIC_FETCHBYTE_CODEWORD +.def PREV _ATOMIC_FETCHBYTE + +_COPY: + .word PREV + .byte 4 + .utf8 "COPY" + .align +COPY: + .word COPY_CODEWORD +.def PREV _COPY + +_EXIT: + .word PREV + .byte 4 + .utf8 "EXIT" + .align +EXIT: + .word EXIT_CODEWORD +.def PREV _EXIT + +_LIT: + .word PREV + .byte 3 + .utf8 "LIT" + .align +LIT: + .word LIT_CODEWORD +.def PREV _LIT + +_EXECUTE: + .word PREV + .byte 7 + .utf8 "EXECUTE" + .align +EXECUTE: + .word EXECUTE_CODEWORD +.def PREV _EXECUTE + +_HALT: + .word PREV + .byte 4 + .utf8 "HALT" + .align +HALT: + .word HALT_CODEWORD +.def PREV _HALT + +_TOR: + .word PREV + .byte 2 + .utf8 ">R" + .align +TOR: + .word TOR_CODEWORD +.def PREV _TOR + +_FROMR: + .word PREV + .byte 2 + .utf8 "R>" + .align +FROMR: + .word FROMR_CODEWORD +.def PREV _FROMR + +_RSPFETCH: + .word PREV + .byte 4 + .utf8 "RSP@" + .align +RSPFETCH: + .word RSPFETCH_CODEWORD +.def PREV _RSPFETCH + +_RSPSTORE: + .word PREV + .byte 4 + .utf8 "RSP!" + .align +RSPSTORE: + .word RSPSTORE_CODEWORD +.def PREV _RSPSTORE + +_RDROP: + .word PREV + .byte 5 + .utf8 "RDROP" + .align +RDROP: + .word RDROP_CODEWORD +.def PREV _RDROP + +_SPFETCH: + .word PREV + .byte 3 + .utf8 "SP@" + .align +SPFETCH: + .word SPFETCH_CODEWORD +.def PREV _SPFETCH + +_SPSTORE: + .word PREV + .byte 3 + .utf8 "SP!" + .align +SPSTORE: + .word SPSTORE_CODEWORD +.def PREV _SPSTORE + +_KEY: + .word PREV + .byte 3 + .utf8 "KEY" + .align +KEY: + .word KEY_CODEWORD +.def PREV _KEY + +_EMIT: + .word PREV + .byte 4 + .utf8 "EMIT" + .align +EMIT: + .word EMIT_CODEWORD +.def PREV _EMIT + +_BRANCH: + .word PREV + .byte 6 + .utf8 "BRANCH" + .align +BRANCH: + .word BRANCH_CODEWORD +.def PREV _BRANCH + +_ZBRANCH: + .word PREV + .byte 7 + .utf8 "0BRANCH" + .align +ZBRANCH: + .word ZBRANCH_CODEWORD +.def PREV _ZBRANCH + +;; Built-in values and variables + +_DOCOL: + .word PREV + .byte 5 + .utf8 "DOCOL" + .align +DOCOL: + .word DOVAL_CODEWORD + .word DOCOL_CODEWORD +.def PREV _DOCOL + +_DOVAL: + .word PREV + .byte 5 + .utf8 "DOVAL" + .align +DOVAL: + .word DOVAL_CODEWORD + .word DOVAL_CODEWORD +.def PREV _DOVAL + +_DOVAR: + .word PREV + .byte 5 + .utf8 "DOVAR" + .align +DOVAR: + .word DOVAL_CODEWORD + .word DOVAR_CODEWORD +.def PREV _DOVAR + +_R0: + .word PREV + .byte 2 + .utf8 "R0" + .align +R0: + .word DOVAL_CODEWORD + .word RSP_INIT +.def PREV _R0 + +_S0: + .word PREV + .byte 2 + .utf8 "S0" + .align +S0: + .word DOVAL_CODEWORD + .word SP_INIT +.def PREV _S0 + +_FALSE: + .word PREV + .byte 5 + .utf8 "FALSE" + .align +FALSE: + .word DOVAL_CODEWORD + .word 0 +.def PREV _FALSE + +_TRUE: + .word PREV + .byte 4 + .utf8 "TRUE" + .align +TRUE: + .word DOVAL_CODEWORD + .word FFFFFFFFh +.def PREV _TRUE + +_STATE: + .word PREV + .byte 5 + .utf8 "STATE" + .align +STATE: + .word DOVAR_CODEWORD + .word 0 +.def PREV _STATE + +_BASE: + .word PREV + .byte 4 + .utf8 "BASE" + .align +BASE: + .word DOVAR_CODEWORD + .word 10 +.def PREV _BASE + +_HERE: + .word PREV + .byte 4 + .utf8 "HERE" + .align +HERE: + .word DOVAR_CODEWORD + .word KERNEL_DEFS_END +.def PREV _HERE + +_LATEST: + .word PREV + .byte 6 + .utf8 "LATEST" + .align +LATEST: + .word DOVAR_CODEWORD + .word KERNEL_DEFS_LAST +.def PREV _LATEST + +_WORDBUF: + .word PREV + .byte 7 + .utf8 "WORDBUF" + .align +WORDBUF: + .word DOVAR_CODEWORD + .zero 20h +.def PREV _WORDBUF + +_WNFHOOK: + .word PREV + .byte 7 + .utf8 "WNFHOOK" + .align +WNFHOOK: + .word DOVAL_CODEWORD + .word WNF_HANDLER +.def PREV _WNFHOOK + +;; And now, it's time for some colon words! + +;; : TUCK DUP -ROT ; + +_TUCK: + .word PREV + .byte 4 + .utf8 "TUCK" + .align +TUCK: + .word DOCOL_CODEWORD + .word DUP NROT + .word EXIT +.def PREV _TUCK + +;; : NSPACE ( byte -- bool ) +;; DUP 9 <> SWAP \ Compare to horizontal tab +;; DUP 10 <> SWAP \ Compare to line-feed +;; DUP 13 <> SWAP \ Compare to carriage return +;; 32 <> \ Compare to space +;; AND AND AND \ And all results together +;; ; + +_NSPACE: + .word PREV + .byte 6 + .utf8 "NSPACE" + .align +NSPACE: + .word DOCOL_CODEWORD + .word DUP LIT 9 NEQ SWAP + .word DUP LIT 10 NEQ SWAP + .word DUP LIT 13 NEQ SWAP + .word LIT 32 NEQ + .word AND AND AND + .word EXIT +.def PREV _NSPACE + +;; : WORD ( -- len ) +;; 0 \ Initial length +;; +;; KEY \ Get byte from input +;; TUCK NSPACE 0BRANCH [60] \ Check if whitespace +;; TUCK WORDBUF + C! \ Append byte to WORDBUF +;; 1+ \ Increment length +;; DUP 32 >= 0BRANCH [8] EXIT \ Exit if at max length +;; BRANCH [-72] \ Loop back to KEY +;; +;; \ Byte is whitespace +;; SWAP DROP +;; DUP 0<> 0BRANCH [-96] \ Loop back to KEY if zero length +;; ; + +_WORD: + .word PREV + .byte 4 + .utf8 "WORD" + .align +WORD: + .word DOCOL_CODEWORD + .word LIT 0 + .word KEY + .word TUCK NSPACE ZBRANCH 60 + .word TUCK WORDBUF ADD STOREBYTE + .word INC + .word DUP LIT 32 GTE ZBRANCH 8 EXIT + .word BRANCH -72 + .word SWAP DROP + .word DUP ZNEQ ZBRANCH -96 + .word EXIT +.def PREV _WORD + +;; : STRING= ( len str1 str2 -- bool ) +;; 2DUP C@ SWAP C@ \ Load a byte from each address +;; = 0BRANCH [48] \ Check for byte mismatch +;; 1+ -ROT 1+ -ROT 1- \ Increment addresses, decrement length +;; DUP 0BRANCH [32] \ Check for zero remaining bytes +;; -ROT BRANCH [-68] \ Loop +;; +;; 2DROP DROP FALSE EXIT \ Strings not equal +;; 2DROP DROP TRUE \ Strings equal +;; ; + +_STRINGEQ: + .word PREV + .byte 7 + .utf8 "STRING=" + .align +STRINGEQ: + .word DOCOL_CODEWORD + .word TWODUP FETCHBYTE SWAP FETCHBYTE + .word EQ ZBRANCH 48 + .word INC NROT INC NROT DEC + .word DUP ZBRANCH 32 + .word NROT BRANCH -68 + .word TWODROP DROP FALSE EXIT + .word TWODROP DROP TRUE + .word EXIT +.def PREV _STRINGEQ + +;; : FIND-NAME-LEN 4+ C@ 63 AND ; + +;; The 3Fh mask we use here includes the hidden flag, so that FIND +;; never matches a hidden entry as its length will appear to be +;; greater than the maximum length returned by WORD. + +_FIND_NAME_LEN: + .word PREV + .byte 13 + .utf8 "FIND-NAME-LEN" + .align +FIND_NAME_LEN: + .word DOCOL_CODEWORD + .word INC4 FETCHBYTE LIT 3Fh AND + .word EXIT +.def PREV _FIND_NAME_LEN + +;; : FIND ( len -- entry ) +;; LATEST @ \ Initial entry +;; +;; TUCK FIND-NAME-LEN \ Get name length +;; OVER = 0BRANCH [52] \ Check for length mismatch +;; OVER 5 + \ Get name address +;; OVER SWAP WORDBUF STRING= \ Check if name matches +;; 0BRANCH [12] +;; DROP EXIT +;; +;; \ Name doesn't match +;; OVER @ 0<> 0BRANCH [20] \ Check for nil link +;; SWAP @ BRANCH [-104] \ Follow link and loop +;; 2DROP FALSE \ Return false +;; ; + +_FIND: + .word PREV + .byte 4 + .utf8 "FIND" + .align +FIND: + .word DOCOL_CODEWORD + .word LATEST FETCH + .word TUCK FIND_NAME_LEN + .word OVER EQ ZBRANCH 52 + .word OVER LIT 5 ADD + .word OVER SWAP WORDBUF STRINGEQ + .word ZBRANCH 12 + .word DROP EXIT + .word OVER FETCH ZNEQ ZBRANCH 20 + .word SWAP FETCH BRANCH -104 + .word TWODROP FALSE + .word EXIT +.def PREV _FIND + +;; : NAME-LEN 4+ C@ 31 AND ; + +_NAME_LEN: + .word PREV + .byte 8 + .utf8 "NAME-LEN" + .align +NAME_LEN: + .word DOCOL_CODEWORD + .word INC4 FETCHBYTE LIT 31 AND + .word EXIT +.def PREV _NAME_LEN + +;; : >CFA ( entry -- cfa ) +;; DUP NAME-LEN \ Get name length +;; 5 + + \ Increment address to end of name +;; DUP 3 AND 0BRANCH [16] \ Check if aligned +;; 1+ BRANCH [-32] \ Increment address, loop to align check +;; ; + +_TOCFA: + .word PREV + .byte 4 + .utf8 ">CFA" + .align +TOCFA: + .word DOCOL_CODEWORD + .word DUP NAME_LEN + .word LIT 5 ADD ADD + .word DUP LIT 3 AND ZBRANCH 16 + .word INC BRANCH -32 + .word EXIT +.def PREV _TOCFA + +;; : DIGIT ( byte -- value ) +;; DUP DUP 48 >= SWAP 57 <= AND \ Test if 0-9 +;; 0BRANCH [24] \ Jump to A-Z test if not +;; 48 - \ Get digit value +;; BRANCH [64] \ Go to range check +;; +;; DUP DUP 65 >= SWAP 90 <= AND \ Test if A-Z +;; 0BRANCH [60] \ Jump to invalid digit if not +;; 55 - \ Get digit value +;; +;; DUP DUP 0>= SWAP BASE @ < AND \ Test if 0 <= value < BASE +;; 0BRANCH [8] \ Jump to invalid digit if not +;; EXIT +;; +;; DROP TRUE \ Return -1 for an invalid digit +;; ; + +_DIGIT: + .word PREV + .byte 5 + .utf8 "DIGIT" + .align +DIGIT: + .word DOCOL_CODEWORD + .word DUP DUP LIT 48 GTE SWAP LIT 57 LTE AND + .word ZBRANCH 24 + .word LIT 48 SUB + .word BRANCH 64 + .word DUP DUP LIT 65 GTE SWAP LIT 90 LTE AND + .word ZBRANCH 60 + .word LIT 55 SUB + .word DUP DUP ZGTE SWAP BASE FETCH LT AND + .word ZBRANCH 8 + .word EXIT + .word DROP TRUE + .word EXIT +.def PREV _DIGIT + +;; : NEGATE INVERT 1+ ; + +_NEGATE: + .word PREV + .byte 6 + .utf8 "NEGATE" + .align +NEGATE: + .word DOCOL_CODEWORD + .word INVERT INC + .word EXIT +.def PREV _NEGATE + +;; : NUMBER? ( len -- value TRUE | FALSE ) +;; WORDBUF \ Initial address +;; +;; DUP C@ 45 = 0BRANCH [60] \ Check for minus sign +;; 1+ \ Increment address +;; SWAP 1- \ Decrement length +;; DUP 0BRANCH [24] \ Check if any characters remain +;; TRUE -ROT \ Set negate flag true +;; SWAP BRANCH [24] \ Jump to setting initial value +;; +;; \ No characters after minus, exit with failure +;; 2DROP FALSE EXIT +;; +;; \ No leading minus, set negate flag false +;; FALSE -ROT +;; +;; \ Set initial value +;; 0 +;; +;; \ Main loop +;; OVER C@ DIGIT \ Get byte's digit value +;; DUP 0>= 0BRANCH [68] \ Check if digit valid +;; +;; SWAP BASE @ * + \ Incorporate digit +;; SWAP 1+ \ Increment address +;; ROT 1- \ Decrement length +;; DUP 0BRANCH [40] \ Go to end if length zero +;; -ROT SWAP BRANCH [-88] \ Loop if characters remain +;; +;; \ Invalid digit +;; 2DROP 2DROP DROP FALSE EXIT \ Exit with failure +;; +;; \ Reached end of word +;; 2DROP \ Discard pointer and length +;; SWAP 0BRANCH [8] NEGATE \ Negate if -ve flag set +;; TRUE \ Exit with success +;; ; + +_NUMBER: + .word PREV + .byte 7 + .utf8 "NUMBER?" + .align +NUMBER: + .word DOCOL_CODEWORD + .word WORDBUF + .word DUP FETCHBYTE LIT 45 EQ ZBRANCH 60 + .word INC + .word SWAP DEC + .word DUP ZBRANCH 24 + .word TRUE NROT + .word SWAP BRANCH 24 + .word TWODROP FALSE EXIT + .word FALSE NROT + .word LIT 0 + .word OVER FETCHBYTE DIGIT + .word DUP ZGTE ZBRANCH 68 + .word SWAP BASE FETCH MUL ADD + .word SWAP INC + .word ROT DEC + .word DUP ZBRANCH 40 + .word NROT SWAP BRANCH -88 + .word TWODROP TWODROP DROP FALSE EXIT + .word TWODROP + .word SWAP ZBRANCH 8 NEGATE + .word TRUE + .word EXIT +.def PREV _NUMBER + +;; : , HERE @ ! 4 HERE +! ; + +_COMMA: + .word PREV + .byte 1 + .utf8 "," + .align +COMMA: + .word DOCOL_CODEWORD + .word HERE FETCH STORE LIT 4 HERE ADDSTORE + .word EXIT +.def PREV _COMMA + +;; : IMMEDIATE? 4+ @ 128 AND 0BRANCH [12] TRUE EXIT FALSE ; + +_IS_IMMEDIATE: + .word PREV + .byte 10 + .utf8 "IMMEDIATE?" + .align +IS_IMMEDIATE: + .word DOCOL_CODEWORD + .word INC4 FETCH LIT 128 AND ZBRANCH 12 TRUE EXIT FALSE + .word EXIT +.def PREV _IS_IMMEDIATE + +;; : INTERPRET +;; WORD \ Read a word +;; DUP FIND \ Look it up +;; DUP 0BRANCH [72] \ Branch to number handling if not found +;; SWAP DROP \ Discard word length +;; +;; \ Handle word +;; DUP IMMEDIATE? \ Check if the word is immediate +;; INVERT 0BRANCH [32] \ Jump straight to executing if so +;; STATE @ 0BRANCH [16] \ Check state +;; >CFA , EXIT \ Compile word +;; >CFA EXECUTE EXIT \ Execute word +;; +;; \ Handle number +;; DROP \ Discard nil entry +;; DUP NUMBER? 0BRANCH [48] \ Convert to number +;; SWAP DROP \ Discard word length +;; STATE @ 0BRANCH [20] \ Check state +;; LIT LIT , , \ If compiling, append LIT and the value +;; EXIT +;; +;; \ Word was not found +;; WNFHOOK EXECUTE +;; ; + +_INTERPRET: + .word PREV + .byte 9 + .utf8 "INTERPRET" + .align +INTERPRET: + .word DOCOL_CODEWORD + .word WORD + .word DUP FIND + .word DUP ZBRANCH 72 + .word SWAP DROP + .word DUP IS_IMMEDIATE + .word INVERT ZBRANCH 32 + .word STATE FETCH ZBRANCH 16 + .word TOCFA COMMA EXIT + .word TOCFA EXECUTE EXIT + .word DROP + .word DUP NUMBER ZBRANCH 48 + .word SWAP DROP + .word STATE FETCH ZBRANCH 20 + .word LIT LIT COMMA COMMA + .word EXIT + .word WNFHOOK EXECUTE + .word EXIT +.def PREV _INTERPRET + +;; : QUIT R0 RSP! INTERPRET BRANCH [-8] ; + +_QUIT: + .word PREV + .byte 4 + .utf8 "QUIT" + .align +QUIT: + .word DOCOL_CODEWORD + .word R0 RSPSTORE + .word INTERPRET BRANCH -8 + .word EXIT +.def PREV _QUIT + +;; And now to make myself obsolete as the compiler (finally) + +;; : [ 0 STATE ! ; IMMEDIATE + +_LBRAC: + .word PREV + .byte 81h + .utf8 "[" + .align +LBRAC: + .word DOCOL_CODEWORD + .word LIT 0 STATE STORE + .word EXIT +.def PREV _LBRAC + +;; : ] 1 STATE ! ; + +_RBRAC: + .word PREV + .byte 1 + .utf8 "]" + .align +RBRAC: + .word DOCOL_CODEWORD + .word LIT 1 STATE STORE + .word EXIT +.def PREV _RBRAC + +;; : ALIGN +;; DUP 3 AND 0BRANCH [32] +;; 0 OVER C! 1+ BRANCH [-48] +;; ; + +_ALIGN: + .word PREV + .byte 5 + .utf8 "ALIGN" + .align +ALIGN: + .word DOCOL_CODEWORD + .word DUP LIT 3 AND ZBRANCH 32 + .word LIT 0 OVER STOREBYTE INC BRANCH -48 + .word EXIT +.def PREV _ALIGN + +;; : HIDDEN 4+ DUP C@ 32 XOR SWAP C! ; + +_HIDDEN: + .word PREV + .byte 6 + .utf8 "HIDDEN" + .align +HIDDEN: + .word DOCOL_CODEWORD + .word INC4 DUP FETCHBYTE LIT 32 XOR SWAP STOREBYTE + .word EXIT +.def PREV _HIDDEN + +;; : IMMEDIATE +;; LATEST @ +;; 4+ DUP C@ 128 XOR SWAP C! +;; ; IMMEDIATE + +_IMMEDIATE: + .word PREV + .byte 89h + .utf8 "IMMEDIATE" + .align +IMMEDIATE: + .word DOCOL_CODEWORD + .word LATEST FETCH + .word INC4 DUP FETCHBYTE LIT 128 XOR SWAP STOREBYTE + .word EXIT +.def PREV _IMMEDIATE + +;; : CREATE +;; HERE @ \ Get initial address +;; LATEST @ OVER ! 4+ \ Write link +;; 2DUP C! 1+ \ Write length +;; 2DUP WORDBUF SWAP COPY + \ Write name +;; ALIGN \ Pad to alignment +;; +;; HERE @ LATEST ! \ Update LATEST +;; HERE ! \ Update HERE +;; ; + +_CREATE: + .word PREV + .byte 6 + .utf8 "CREATE" + .align +CREATE: + .word DOCOL_CODEWORD + .word HERE FETCH + .word LATEST FETCH OVER STORE INC4 + .word TWODUP STOREBYTE INC + .word TWODUP WORDBUF SWAP COPY ADD + .word ALIGN + .word HERE FETCH LATEST STORE + .word HERE STORE + .word EXIT +.def PREV _CREATE + +;; : : +;; WORD +;; CREATE +;; DOCOL , +;; LATEST @ HIDDEN +;; ] +;; ; + +_COLON: + .word PREV + .byte 1 + .utf8 ":" + .align +COLON: + .word DOCOL_CODEWORD + .word WORD + .word CREATE + .word DOCOL COMMA + .word LATEST FETCH HIDDEN + .word RBRAC + .word EXIT +.def PREV _COLON + +;; : ; +;; LIT EXIT , +;; LATEST @ HIDDEN +;; [ +;; ; IMMEDIATE + +_SEMICOLON: + .word PREV + .byte 81h + .utf8 ";" + .align +SEMICOLON: + .word DOCOL_CODEWORD + .word LIT EXIT COMMA + .word LATEST FETCH HIDDEN + .word LBRAC + .word EXIT +.def PREV _SEMICOLON + +;; : WNF-HANDLER +;; DROP \ Discard word length +;; 87 EMIT 78 EMIT 70 EMIT 10 EMIT \ Print WNF +;; QUIT \ Reset return stack +;; ; + +_WNF_HANDLER: + .word PREV + .byte 11 + .utf8 "WNF-HANDLER" + .align +WNF_HANDLER: + .word DOCOL_CODEWORD + .word DROP + .word LIT 87 EMIT LIT 78 EMIT LIT 70 EMIT LIT 10 EMIT + .word QUIT + .word EXIT +.def PREV _WNF_HANDLER + +COLD_START: + .word QUIT + +.def KERNEL_DEFS_LAST PREV +KERNEL_DEFS_END: + +;; The trampoline is a workaround for WebAssembly's lack of indirect +;; jumps and code addresses. Instead of jumping into the next +;; codeword, NEXT sets the fn global to the function index of the next +;; codeword and returns to the trampoline. +;; +;; This way, we can simulate jumping to the code within WebAssembly's +;; restrictions without the WebAssembly call stack growing unbounded +;; as would happen if we used call_indirect inside NEXT. + +.func trampoline + loop iter + global.get fn call_indirect codeword codewords + global.get run br_if iter + end + +.func reset + i32.const RSP_INIT global.set rsp ;; Set the return stack pointer + i32.const SP_INIT global.set sp ;; Set the stack pointer + i32.const COLD_START global.set ip ;; Set the IP to the cold start + call next + + i32.const 1 global.set run + call trampoline + +.export reset -- 2.34.1 From c91f46be887d1adb41b5c78058e88e4e1ec8dfc6 Mon Sep 17 00:00:00 2001 From: Camden Dixie O'Brien Date: Wed, 18 Mar 2026 14:23:37 +0000 Subject: [PATCH 65/72] Assemble kernel on client --- boot.js | 16 ++++++++++++++-- emu.js | 9 ++++++++- 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/boot.js b/boot.js index 263b8f9..6bca6ae 100644 --- a/boot.js +++ b/boot.js @@ -1,7 +1,19 @@ +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) + const wasm = await assemble; + const mod = await WebAssembly.instantiate(wasm, exports); + await self.postMessage('booting'); mod.instance.exports.reset(); console.log('System halt'); }; diff --git a/emu.js b/emu.js index 03e6477..337b2a6 100644 --- a/emu.js +++ b/emu.js @@ -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,14 @@ class Emulator { document.addEventListener('keydown', (e) => this.handle_keydown(e)); window.addEventListener('resize', () => this.handle_resize()); - this.worker = new Worker('boot.js'); + this.print("Assembling kernel "); + const dots = setInterval(() => this.print("."), DOT_INTERVAL_MS); + this.worker = new Worker('boot.js', { type: 'module' }); this.worker.postMessage(this.mem); + this.worker.onmessage = (e) => { + clearInterval(dots); + this.print(" done\n"); + }; fetch('prelude.f') .then(res => res.text()) -- 2.34.1 From f77adffbefc63dff9a0db663c7687e3b9a59ff53 Mon Sep 17 00:00:00 2001 From: Camden Dixie O'Brien Date: Wed, 18 Mar 2026 14:24:05 +0000 Subject: [PATCH 66/72] Update MIME types in server.scm --- server.scm | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/server.scm b/server.scm index f2a3db4..361d32a 100644 --- a/server.scm +++ b/server.scm @@ -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) -- 2.34.1 From eaa3242cc08075c4b23962c95bf11f469d51fbe9 Mon Sep 17 00:00:00 2001 From: Camden Dixie O'Brien Date: Wed, 18 Mar 2026 14:24:25 +0000 Subject: [PATCH 67/72] Update e2e tests --- tests.scm | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/tests.scm b/tests.scm index 064e048..3f4c9e1 100644 --- a/tests.scm +++ b/tests.scm @@ -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") -- 2.34.1 From 6e8439eeaf4ee14854cfc3500fdacc01afa4effe Mon Sep 17 00:00:00 2001 From: Camden Dixie O'Brien Date: Wed, 18 Mar 2026 14:24:41 +0000 Subject: [PATCH 68/72] Bump version number --- prelude.f | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/prelude.f b/prelude.f index 7a711e3..b8191b9 100644 --- a/prelude.f +++ b/prelude.f @@ -249,7 +249,7 @@ CHAR . EMIT \ Version number 0 CONSTANT VERSION-MAJOR -1 CONSTANT VERSION-MINOR +2 CONSTANT VERSION-MINOR 0 CONSTANT VERSION-PATCH : PRINT-VERSION -- 2.34.1 From 0a52388030805970e4f820be39658a62694b6a2d Mon Sep 17 00:00:00 2001 From: Camden Dixie O'Brien Date: Wed, 18 Mar 2026 14:25:11 +0000 Subject: [PATCH 69/72] Update deploy manifest --- deploy-manifest.conf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deploy-manifest.conf b/deploy-manifest.conf index 188566b..3511156 100644 --- a/deploy-manifest.conf +++ b/deploy-manifest.conf @@ -3,4 +3,4 @@ emu.js index.html prelude.f styles.css -wipforth.wasm +wipforth.ws -- 2.34.1 From 19ef69958dffb0540d581fc83ad324398dc6eab7 Mon Sep 17 00:00:00 2001 From: Camden Dixie O'Brien Date: Wed, 18 Mar 2026 14:36:47 +0000 Subject: [PATCH 70/72] Update README --- README.md | 60 ++++++++++++++++++++++++++----------------------------- 1 file changed, 28 insertions(+), 32 deletions(-) diff --git a/README.md b/README.md index 6cfa3fc..a13cb54 100644 --- a/README.md +++ b/README.md @@ -1,31 +1,24 @@ # Wipforth -Wipforth is a simple Forth implementation that runs in the WebAssembly -virtual machine. It does I/O via memory-mapped peripherals, which are -emulated in JavaScript. +Wipforth is a Forth implementation that runs in the WebAssembly +virtual machine. The system is bootstrapped from source on page load: +the only non-text file is the favicon :) -- For the Forth kernel, see [wipforth.wat](./wipforth.wat) -- For the JavaScript emulator, see [emu.js](./emu.js) -- For the Forth prelude, which is loaded at start-up, see - [prelude.f](./prelude.f) +I/O is done via memory-mapped peripherals, which are emulated in +JavaScript. + +- For the Forth kernel, see [wipforth.ws](./wipforth.ws) +- For the emulator, see [emu.js](./emu.js) +- For the assembler, see [asm.js](./asm.js) +- For the prelude (Forth code loaded right after the kernel boots), + see [prelude.f](./prelude.f) - For a description of the peripherals, see the [Peripherals](#peripherals) section below. ## Building and Running Locally -You'll need: - -- [WABT](https://github.com/WebAssembly/wabt) (not for long mwahaha) -- [Guile](https://www.gnu.org/software/guile/) (or bring your own HTTP - server -- see note below) - -To run, first compile the WebAssembly module: - -``` -wat2wasm --enable-threads wipforth.wat -``` - -Then run the development server: +There's a [Guile](https://www.gnu.org/software/guile/) script in the +repo you can use for this: ``` guile server.scm @@ -34,14 +27,20 @@ guile server.scm You should then be able to open 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 -- 2.34.1 From 4000522b3a659c48f583f97787418a141ba41628 Mon Sep 17 00:00:00 2001 From: Camden Dixie O'Brien Date: Wed, 18 Mar 2026 15:08:41 +0000 Subject: [PATCH 71/72] Remove obsolete assembly driver script --- driver.js | 9 --------- 1 file changed, 9 deletions(-) delete mode 100644 driver.js diff --git a/driver.js b/driver.js deleted file mode 100644 index 6b53cc5..0000000 --- a/driver.js +++ /dev/null @@ -1,9 +0,0 @@ -import { Assembler } from "./asm.js"; -import { writeAll } from "jsr:@std/io/write-all"; - -const asm = new Assembler(); -for await (const chunk of Deno.stdin.readable) { - asm.push(chunk); -} -const wasm = asm.wasm(); -await writeAll(Deno.stdout, wasm); -- 2.34.1 From 67fc1d8d7b9e13f4a9bbb0a89fcbbebd9255dc12 Mon Sep 17 00:00:00 2001 From: Camden Dixie O'Brien Date: Wed, 18 Mar 2026 15:17:08 +0000 Subject: [PATCH 72/72] Remove race condition between assemble and prelude load prints --- boot.js | 19 +++++++++++++------ emu.js | 3 ++- prelude.f | 2 +- 3 files changed, 16 insertions(+), 8 deletions(-) diff --git a/boot.js b/boot.js index 6bca6ae..96bc257 100644 --- a/boot.js +++ b/boot.js @@ -10,10 +10,17 @@ const assemble = (async () => { })(); self.onmessage = async (e) => { - const exports = { emu: { mem: e.data } }; - const wasm = await assemble; - const mod = await WebAssembly.instantiate(wasm, exports); - await self.postMessage('booting'); - 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; + } }; diff --git a/emu.js b/emu.js index 337b2a6..ad4488a 100644 --- a/emu.js +++ b/emu.js @@ -54,10 +54,11 @@ class Emulator { this.print("Assembling kernel "); const dots = setInterval(() => this.print("."), DOT_INTERVAL_MS); this.worker = new Worker('boot.js', { type: 'module' }); - this.worker.postMessage(this.mem); + this.worker.postMessage({ type: "load", mem: this.mem }); this.worker.onmessage = (e) => { clearInterval(dots); this.print(" done\n"); + this.worker.postMessage({ type: "boot" }); }; fetch('prelude.f') diff --git a/prelude.f b/prelude.f index b8191b9..379b181 100644 --- a/prelude.f +++ b/prelude.f @@ -250,7 +250,7 @@ CHAR . EMIT 0 CONSTANT VERSION-MAJOR 2 CONSTANT VERSION-MINOR -0 CONSTANT VERSION-PATCH +1 CONSTANT VERSION-PATCH : PRINT-VERSION CHAR v EMIT VERSION-MAJOR . -- 2.34.1