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] 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)); + } +}