1452 lines
42 KiB
JavaScript
1452 lines
42 KiB
JavaScript
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;
|
|
this.cursor = 0;
|
|
}
|
|
|
|
find(pred) {
|
|
for (let i = this.cursor; i < this.buffer.length; ++i) {
|
|
if (pred(this.buffer[i]))
|
|
return i;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
skip() {
|
|
const idx = this.find((cp) => !this.skips.has(cp));
|
|
this.cursor = idx == null ? this.buffer.length : idx;
|
|
}
|
|
|
|
next_string() {
|
|
const idx = this.find((cp) => cp == this.string_quote);
|
|
if (idx == null) {
|
|
this.string = true;
|
|
} else {
|
|
const string = this.buffer.slice(this.cursor, idx).join("");
|
|
this.cursor = idx + 1;
|
|
this.string = false;
|
|
return { string: string };
|
|
}
|
|
}
|
|
|
|
next() {
|
|
if (this.string)
|
|
return this.next_string();
|
|
|
|
this.skip();
|
|
if (this.buffer[this.cursor] == LINE_END) {
|
|
++this.cursor;
|
|
return LINE_END;
|
|
}
|
|
|
|
if (this.buffer[this.cursor] == this.string_quote) {
|
|
++this.cursor;
|
|
return this.next_string();
|
|
}
|
|
|
|
const idx = this.find((cp) => this.delims.has(cp));
|
|
if (idx != null) {
|
|
const token = this.buffer.slice(this.cursor, idx).join("");
|
|
this.cursor = 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;
|
|
yield token;
|
|
} 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,
|
|
ZERO: 36,
|
|
});
|
|
|
|
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,
|
|
".zero": State.ZERO,
|
|
};
|
|
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),
|
|
[State.ZERO]: (token) => this.token_zero(token),
|
|
};
|
|
|
|
this.results = [];
|
|
this.params = {};
|
|
this.locals = {};
|
|
}
|
|
|
|
integer(token) {
|
|
let base, regex;
|
|
switch (token.slice(-1)) {
|
|
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;
|
|
}
|
|
return regex.test(token) ? parseInt(token, base) : null;
|
|
}
|
|
|
|
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)
|
|
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;
|
|
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;
|
|
}
|
|
|
|
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,
|
|
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 = {};
|
|
this.unresolved = [];
|
|
}
|
|
|
|
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.lookup_def(action.at.addr_symbol);
|
|
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) {
|
|
this.unresolved.push({
|
|
type: "data",
|
|
size: action.size,
|
|
symbol: action.symbol,
|
|
target: data,
|
|
offset: data.length,
|
|
});
|
|
value = 0;
|
|
}
|
|
}
|
|
bytes = this.le(value, action.size);
|
|
}
|
|
data.push(...bytes);
|
|
this.pos.addr += action.size;
|
|
}
|
|
|
|
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"],
|
|
...this.uleb128(data.length),
|
|
...data,
|
|
]
|
|
});
|
|
return [ contents.length, contents ].flat(Infinity);
|
|
}
|
|
|
|
wasm() {
|
|
this.resolve_refs();
|
|
this.resolve_func_types();
|
|
|
|
const template = [
|
|
[ Section.TYPE, () => this.wasm_section_type() ],
|
|
[ Section.IMPORT, () => this.wasm_section_import() ],
|
|
[ Section.FUNC, () => this.wasm_section_func() ],
|
|
[ Section.TABLE, () => this.wasm_section_table() ],
|
|
[ Section.MEM, () => this.wasm_section_mem() ],
|
|
[ Section.GLOBAL, () => this.wasm_section_global() ],
|
|
[ Section.EXPORT, () => this.wasm_section_export() ],
|
|
[ Section.ELEM, () => this.wasm_section_elem() ],
|
|
[ Section.CODE, () => this.wasm_section_code() ],
|
|
[ Section.DATA, () => this.wasm_section_data() ],
|
|
];
|
|
const sections = template.map(([ code, generator ]) => {
|
|
const body = generator();
|
|
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;
|
|
}, []);
|
|
}
|
|
|
|
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}`);
|
|
}
|
|
}
|
|
}
|