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.string_quote = '"'; this.buffer = []; this.comment = false; this.string = false; } skip() { const idx = this.buffer.findIndex((cp) => !this.skips.has(cp)); 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(""); this.buffer = this.buffer.slice(idx); return token; } } *handle(src) { this.buffer.push(...src); let token; while (token = this.next()) { if (token.string == undefined && token.startsWith(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, PARAM_NAME: 4, PARAM_TYPE: 5, LOCAL_NAME: 6, LOCAL_TYPE: 7, MEM_NAME: 8, MEM_INIT: 9, MEM_MAX: 10, MEM_FLAGS: 11, IMPORT_NAME: 12, IMPORT_MOD: 13, IMPORT_FIELD: 14, GLOBAL_NAME: 15, GLOBAL_TYPE: 16, GLOBAL_INIT: 17, AT_MEM: 18, AT_ADDR: 19, BYTE: 20, WORD: 21, UTF8: 22, ALIGN: 23, DEF_NAME: 24, DEF_VALUE: 25, BLOCK_NAME: 26, BLOCK_TYPE: 27, TYPE_NAME: 28, TYPE_PARAM: 29, TYPE_RESULT: 30, TABLE_NAME: 31, TABLE_SIZE: 32, ELEM_TABLE: 33, ELEM_ELEM: 34, ELEM_LABEL: 35, }); const Action = Object.freeze({ APPEND: 0, EXPORT: 1, FUNC: 2, RESULT: 3, PARAM: 4, SYMBOL: 5, LOCAL: 6, MEM: 7, IMPORT: 8, GLOBAL: 9, AT: 10, DATA: 11, ALIGN: 12, DEF: 13, LABEL: 14, ENTER: 16, EXIT: 17, ELSE: 18, TYPE: 19, TABLE: 20, ELEM: 21, }); const types = { "void": 0x40, "func": 0x60, "funcref": 0x70, "f32": 0x7d, "i32": 0x7f, }; const opcodes = { "block": 0x02, "loop": 0x03, "if": 0x04, "else": 0x05, "end": 0x0b, "br": 0x0c, "br_if": 0x0d, "call": 0x10, "call_indirect": 0x11, "drop": 0x0a, "local.get": 0x20, "local.set": 0x21, "local.tee": 0x22, "global.get": 0x23, "global.set": 0x24, "i32.load": 0x28, "i32.load8_u": 0x2d, "i32.store": 0x36, "i32.store8": 0x3a, "i32.const": 0x41, "i64.const": 0x42, "i32.eqz": 0x45, "i32.eq": 0x46, "i32.ne": 0x47, "i32.lt_s": 0x48, "i32.lt_u": 0x49, "i32.gt_s": 0x4a, "i32.gt_u": 0x4b, "i32.le_s": 0x4c, "i32.le_u": 0x4d, "i32.ge_s": 0x4e, "i32.ge_u": 0x4f, "i32.add": 0x6a, "i32.sub": 0x6b, "i32.mul": 0x6c, "i32.div_s": 0x6d, "i32.rem_s": 0x6f, "i32.and": 0x71, "i32.or": 0x72, "i32.xor": 0x73, "i32.shl": 0x74, "i32.shr_s": 0x75, "i32.shr_u": 0x76, // Threads instructions "memory.atomic.notify": [ 0xfe, 0x00 ], "memory.atomic.wait32": [ 0xfe, 0x01 ], "i32.atomic.load": [ 0xfe, 0x10 ], "i32.atomic.load8_u": [ 0xfe, 0x12 ], "i32.atomic.store": [ 0xfe, 0x17 ], "i32.atomic.store8": [ 0xfe, 0x19 ], }; const mem_flags = { "max": 1, "shared": 2, "64": 4, }; const const_opcodes = { [types["i32"]]: opcodes["i32.const"], }; class Parser { constructor(encoder) { this.encoder = encoder; this.tokens = []; this.tokenizer = new Tokenizer(); this.state = State.TOP; this.directives = { ".export": State.EXPORT, ".func": State.FUNC, ".result": State.RESULT, ".param": State.PARAM_NAME, ".local": State.LOCAL_NAME, ".mem": State.MEM_NAME, ".import": State.IMPORT_NAME, ".global": State.GLOBAL_NAME, ".at": State.AT_MEM, ".byte": State.BYTE, ".word": State.WORD, ".utf8": State.UTF8, ".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 = { [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), [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), [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), [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), [State.TYPE_NAME]: (token) => this.token_type_name(token), [State.TYPE_PARAM]: (token) => this.token_type_param(token), [State.TYPE_RESULT]: (token) => this.token_type_result(token), [State.TABLE_NAME]: (token) => this.token_table_name(token), [State.TABLE_SIZE]: (token) => this.token_table_size(token), [State.ELEM_TABLE]: (token) => this.token_elem_table(token), [State.ELEM_ELEM]: (token) => this.token_elem_elem(token), [State.ELEM_LABEL]: (token) => this.token_elem_label(token), }; this.results = []; this.params = {}; this.locals = {}; } integer(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; } translate_code(token) { return opcodes[token] ?? this.integer(token); } translate_type(token) { return types[token]; } token_top(token) { if (token == LINE_END) return; const state = this.directives[token]; if (state) { this.state = state; return; } if (token.endsWith(":")) return { type: Action.LABEL, name: token.slice(0, -1) }; 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 != null) return { type: Action.APPEND, literal }; return { type: Action.SYMBOL, symbol: token }; } 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)); } } 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 .param: 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; } } 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; } } 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(); } } 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; } } 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) { 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; } 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; } 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) { action.symbol = token; } else { if (value > 0xff) console.error(`WARNING: Value ${token} is truncated`); action.value = value; } return action; } token_word(token) { if (token == LINE_END) { this.state = State.TOP; return; } const action = { type: Action.DATA, size: 4 }; const value = this.integer(token); if (value == null) { action.symbol = token; } else { if (value > 0xffffffff) console.error(`WARNING: Value ${token} is truncated`); action.value = value; } 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 bytes = this.encoder.encode(token.string); const action = { type: Action.DATA, size: bytes.length, bytes }; 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; } 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 action = { type: Action.DEF, 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; } 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; } token_type_name(token) { if (token == LINE_END) { console.error( "ERROR: Unexpected end of line in .type, expected name"); this.state = State.TOP; return; } this.type = { name: token, params: [] }; this.state = State.TYPE_PARAM; } token_type_param(token) { if (token == LINE_END) { console.error( "ERROR: Unexpected end of line in .type, expected " + "parameter type"); this.type = undefined; this.state = State.TOP; return; } if (token == "result") { this.type.results = []; this.state = State.TYPE_RESULT; return; } const type = types[token]; if (type == undefined) { console.error( `ERROR: Unexpected token ${token} in .type, expected ` + "parameter type"); this.type = undefined; this.state = State.TOP; return; } this.type.params.push(type); } token_type_result(token) { if (token == LINE_END) { const action = { type: Action.TYPE, the_type: this.type }; this.type = undefined; this.state = State.TOP; return action; } const type = types[token]; if (type == undefined) { console.error( `ERROR: Unexpected token ${token} in .type, expected ` + "result type"); this.type = undefined; this.state = State.TOP; return; } this.type.results.push(type); } token_table_name(token) { if (token == LINE_END) { console.error( "ERROR: Unexpected end of line in .table, expected name"); this.state = State.TOP; return; } this.table = { name: token }; this.state = State.TABLE_SIZE; } token_table_size(token) { if (token == LINE_END) { console.error( "ERROR: Unexpected end of line in .table, expected size"); this.table = undefined; this.state = State.TOP; return; } const size = this.integer(token); if (size == null) { console.error( `ERROR: Unexpected token ${token} in .table, expected size`); this.table = undefined; this.state = State.TOP; return; } this.table.size = size; const action = { type: Action.TABLE, table: this.table }; this.table = undefined; this.state = State.TOP; return action; } token_elem_table(token) { if (token == LINE_END) { console.error( "ERROR: Unexpected end of line in .elem, expected " + "table name"); this.state = State.TOP; return; } this.elem = { table: token }; this.state = State.ELEM_ELEM; } token_elem_elem(token) { if (token == LINE_END) { console.error( "ERROR: Unexpected end of line in .elem, expected " + "element name"); this.state = State.TOP; this.elem = undefined; return; } this.elem.elem = token; this.state = State.ELEM_LABEL; } token_elem_label(token) { if (token != LINE_END) this.elem.label = token; const action = { type: Action.ELEM, elem: this.elem }; this.elem = undefined this.state = State.TOP; return action; } 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)) { 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; } } } const Section = Object.freeze({ TYPE: 0x01, IMPORT: 0x02, FUNC: 0x03, TABLE: 0x04, MEM: 0x05, GLOBAL: 0x06, EXPORT: 0x07, ELEM: 0x09, CODE: 0x0a, DATA: 0x0b, }); const Kind = Object.freeze({ FUNC: 0x00, MEM: 0x02, }); export class Assembler { constructor() { this.encoder = new TextEncoder("utf-8"); this.decoder = new TextDecoder("utf-8"); this.parser = new Parser(this.encoder); 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), [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), [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), [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), [Action.TYPE]: (action) => this.action_type(action), [Action.TABLE]: (action) => this.action_table(action), [Action.ELEM]: (action) => this.action_elem(action), }; this.exports = []; this.funcs = {}; this.mems = {}; this.imports = []; this.globals = {}; this.pos = { mem: 0, addr: 0 }; this.data = []; this.defs = {}; this.blocks = []; this.types = []; this.type_bindings = {}; this.tables = {}; } action_append(action) { const code = action.opcode != undefined ? [ action.opcode ].flat() : this.leb128(action.literal); this.funcs[this.current_func].body.push(...code); } action_export(action) { const index = Object.keys(this.funcs).indexOf(action.name); this.exports[action.name] = { kind: Kind.FUNC, index }; } 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); } action_param(action) { 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 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_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) { console.error( `ERROR: Unable to resolve symbol ${action.symbol}`); return; } func.body.push(...this.leb128(def_value)); } else { func.body.push(...this.uleb128(value)); } } action_mem(action) { 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, }) } action_global(action) { 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; this.data.push({ loc: { ...this.pos }, data: [] }) } action_data(action) { const data = this.data.at(-1).data; 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; } 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; } } action_def(action) { const value = action.def.value ?? this.lookup_def(action.def.symbol); this.defs[action.def.name] = value; } action_label(action) { 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); } action_type(action) { const type = this.func_type(action.the_type); const index = this.ensure_type(type); this.type_bindings[action.the_type.name] = index; } action_table(action) { this.tables[action.table.name] = { size: action.table.size, elems: [], }; } action_elem(action) { const table = this.tables[action.elem.table]; const fn = Object.keys(this.funcs).indexOf(action.elem.elem); if (fn == -1) { console.error(`ERROR: ${action.elem.elem}: no such function`); return; } const index = table.elems.push(fn) - 1; if (action.elem.label) this.defs[action.elem.label] = index; } push(chunk) { const text = this.decoder.decode(chunk, { stream: true }); for (const action of this.parser.handle(text)) { const handler = this.handlers[action.type]; if (handler == undefined) console.error("ERROR: Unhandled action", action); else handler(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; } lookup_local(func, symbol) { const param_count = Object.entries(func.params).length; const index = Object.keys(func.locals).indexOf(symbol); return index == -1 ? null : param_count + index; } lookup_global(symbol) { const index = Object.keys(this.globals).indexOf(symbol); return index == -1 ? null : index; } lookup_table(symbol) { const index = Object.keys(this.tables).indexOf(symbol); return index == -1 ? null : index; } lookup_type(symbol) { return this.type_bindings[symbol]; } lookup_func(symbol) { const index = Object.keys(this.funcs).indexOf(symbol); return index == -1 ? null : index; } lookup_def(symbol) { return this.defs[symbol]; } le(value, count) { const bytes = [] while (value != 0) { bytes.push(value & 0xff); value >>>= 8; } if (count != undefined) { while (bytes.length < count) bytes.push(0); } return bytes; } leb128(x) { const bytes = []; while (true) { const b = x & 0x7f; x >>= 7; if (x == 0 && (b & 0x40) == 0 || x == -1 && (b & 0x40) != 0) { bytes.push(b); return bytes; } bytes.push(b | 0x80); } } uleb128(x) { const bytes = []; while (true) { const b = x & 0x7f; x >>>= 7; if (x == 0) { bytes.push(b); return bytes; } bytes.push(b | 0x80); } return bytes; } wasm_section_type() { if (this.types.length == 0) return null; return [ this.types.length ].concat(...this.types); } 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 types = Object.values(this.funcs).map(({type}) => type); const count = types.length; if (count == 0) return null; return [ count, ...types ]; } wasm_section_table() { const sizes = Object.values(this.tables).map(({size}) => size); if (sizes.length == 0) return null; const contents = sizes.map((size) => [ types.funcref, 0x00, this.uleb128(size), ]); return [ sizes.length, contents ].flat(Infinity) } wasm_section_mem() { const mems = Object.values(this.mems).filter( ({imported}) => !imported); if (mems.length == 0) return null; const contents = mems.map((mem) => this.mem_wasm(mem)); 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, const_opcodes[type], ...this.leb128(init), opcodes["end"], ]); return [ globals.length ].concat(...contents); } 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 [ name_utf8.length, ...name_utf8, kind, index, ]; }); 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; const contents = funcs.map(({ body, locals }) => { const local_types = Object.values(locals); const local_count = local_types.length; if (local_count == 0) { const full_body = [ 0, body, opcodes.end ].flat() return [ full_body.length, full_body ].flat(); } else { 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, contents ].flat(Infinity); } 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"], ...this.leb128(loc.addr), opcodes["end"], data.length, ...data, ] }); return [ contents.length, contents ].flat(Infinity); } wasm() { this.resolve_func_types(); const template = [ [ Section.TYPE, () => this.wasm_section_type() ], [ Section.IMPORT, () => this.wasm_section_import() ], [ Section.FUNC, () => this.wasm_section_func() ], [ Section.TABLE, () => this.wasm_section_table() ], [ Section.MEM, () => this.wasm_section_mem() ], [ Section.GLOBAL, () => this.wasm_section_global() ], [ Section.EXPORT, () => this.wasm_section_export() ], [ Section.ELEM, () => this.wasm_section_elem() ], [ Section.CODE, () => this.wasm_section_code() ], [ Section.DATA, () => this.wasm_section_data() ], ]; const sections = template.map(([ code, generator ]) => { const body = generator(); if (body == null) return []; return [ code, this.uleb128(body.length), body ]; }); return new Uint8Array([ HEADER, sections ].flat(Infinity)); } mem_wasm({ flags, init, max }) { if (flags & mem_flags.max) return [ flags, init, max ]; else return [ flags, init ]; } func_type({ params, results }) { const param_types = params.length == undefined ? Object.values(params) : params; return [ types["func"], param_types.length, ...param_types, results.length, ...results, ]; } array_eq(a, b) { return a.length == b.length && a.every((x, i) => x == b[i]); } ensure_type(type) { const index = this.types.findIndex((t) => this.array_eq(type, t)); return index != -1 ? index : this.types.push(type) - 1; } resolve_func_types() { for (const func of Object.values(this.funcs)) func.type = this.ensure_type(this.func_type(func)); } group(array) { return array.reduce((acc, val) => { const last = acc.at(-1); if (last != undefined && last[1] == val) ++last[0] else acc.push([1, val]); return acc; }, []); } }