watr
Version:
Ligth & fast WAT compiler
932 lines (781 loc) • 33.6 kB
JavaScript
import * as encode from './encode.js'
import { uleb, i32, i64 } from './encode.js'
import { SECTION, TYPE, KIND, INSTR, HEAPTYPE, DEFTYPE, RECTYPE, REFTYPE } from './const.js'
import parse from './parse.js'
import { clone, err } from './util.js'
// build instructions index
INSTR.forEach((op, i) => INSTR[op] = i >= 0x133 ? [0xfd, i - 0x133] : i >= 0x11b ? [0xfc, i - 0x11b] : i >= 0xfb ? [0xfb, i - 0xfb] : [i]);
/**
* Converts a WebAssembly Text Format (WAT) tree to a WebAssembly binary format (WASM).
*
* @param {string|Array} nodes - The WAT tree or string to be compiled to WASM binary.
* @param {Object} opt - opt.fullSize for fixed-width uleb encoding
* @returns {Uint8Array} The compiled WASM binary data.
*/
export default function watr(nodes) {
// normalize to (module ...) form
if (typeof nodes === 'string') nodes = parse(nodes);
else nodes = clone(nodes)
// module abbr https://webassembly.github.io/spec/core/text/modules.html#id10
if (nodes[0] === 'module') nodes.shift(), nodes[0]?.[0] === '$' && nodes.shift()
// single node, not module
else if (typeof nodes[0] === 'string') nodes = [nodes]
// binary abbr "\00" "\0x61" ...
if (nodes[0] === 'binary') {
nodes.shift()
return Uint8Array.from(str(nodes.map(i => i.slice(1, -1)).join('')))
}
// quote "a" "b"
else if (nodes[0] === 'quote') {
nodes.shift()
return watr(nodes.map(i => i.slice(1, -1)).join(''))
}
// scopes are aliased by key as well, eg. section.func.$name = section[SECTION.func] = idx
const ctx = []
for (let kind in SECTION) (ctx[SECTION[kind]] = ctx[kind] = []).name = kind
// initialize types
nodes.filter(([kind, ...node]) => {
// (rec (type $a (sub final? $sup* (func ...))...) (type $b ...)) -> save subtypes
if (kind === 'rec') {
// node contains a list of subtypes, (type ...) or (type (sub final? ...))
// convert rec type into regular type (first subtype) with stashed subtypes length
// add rest of subtypes as regular type nodes with subtype flag
for (let i = 0; i < node.length; i++) {
let [,...subnode] = node[i]
alias(subnode, ctx.type);
(subnode = typedef(subnode, ctx)).push(i ? true : [ctx.type.length, node.length])
ctx.type.push(subnode)
}
}
// (type (func param* result*))
// (type (array (mut i8)))
// (type (struct (field a)*)
// (type (sub final? $nm* (struct|array|func ...)))
else if (kind === 'type') {
alias(node, ctx.type);
ctx.type.push(typedef(node, ctx));
}
// other sections may have id
else if (kind === 'start' || kind === 'export') ctx[kind].push(node)
else return true
})
// prepare/normalize nodes
.forEach(([kind, ...node]) => {
let imported // if node needs to be imported
// import abbr
// (import m n (table|memory|global|func id? type)) -> (table|memory|global|func id? (import m n) type)
if (kind === 'import') [kind, ...node] = (imported = node).pop()
// index, alias
let items = ctx[kind];
let name = alias(node, items);
// export abbr
// (table|memory|global|func id? (export n)* ...) -> (table|memory|global|func id ...) (export n (table|memory|global|func id))
while (node[0]?.[0] === 'export') ctx.export.push([node.shift()[1], [kind, items.length]])
// for import nodes - redirect output to import
if (node[0]?.[0] === 'import') [, ...imported] = node.shift()
// table abbr
if (kind === 'table') {
// (table id? reftype (elem ...{n})) -> (table id? n n reftype) (elem (table id) (i32.const 0) reftype ...)
if (node[1]?.[0] === 'elem') {
let [reftype, [, ...els]] = node
node = [els.length, els.length, reftype]
ctx.elem.push([['table', name || items.length], ['i32.const', '0'], reftype, ...els])
}
}
// data abbr
// (memory id? (data str)) -> (memory id? n n) (data (memory id) (i32.const 0) str)
else if (kind === 'memory' && node[0]?.[0] === 'data') {
let [, ...data] = node.shift(), m = '' + Math.ceil(data.map(s => s.slice(1, -1)).join('').length / 65536) // FIXME: figure out actual data size
ctx.data.push([['memory', items.length], ['i32.const', 0], ...data])
node = [m, m]
}
// dupe to code section, save implicit type
else if (kind === 'func') {
let [idx, param, result] = typeuse(node, ctx);
idx ??= regtype(param, result, ctx)
// we save idx because type can be defined after
!imported && ctx.code.push([[idx, param, result], ...plain(node, ctx)]) // pass param since they may have names
node.unshift(['type', idx])
}
// import writes to import section amd adds placeholder for (kind) section
if (imported) ctx.import.push([...imported, [kind, ...node]]), node = null
items.push(node)
})
// convert nodes to bytes
const bin = (kind, count = true) => {
const items = ctx[kind]
.filter(Boolean) // filter out (type, imported) placeholders
.map(item => build[kind](item, ctx))
.filter(Boolean) // filter out unrenderable things (subtype or data.length)
return !items.length ? [] : [kind, ...vec(count ? vec(items) : items)]
}
// build final binary
return Uint8Array.from([
0x00, 0x61, 0x73, 0x6d, // magic
0x01, 0x00, 0x00, 0x00, // version
...bin(SECTION.custom),
...bin(SECTION.type),
...bin(SECTION.import),
...bin(SECTION.func),
...bin(SECTION.table),
...bin(SECTION.memory),
...bin(SECTION.global),
...bin(SECTION.export),
...bin(SECTION.start, false),
...bin(SECTION.elem),
...bin(SECTION.datacount, false),
...bin(SECTION.code),
...bin(SECTION.data)
])
}
// consume name eg. $t ...
const alias = (node, list) => {
let name = (node[0]?.[0] === '$' || node[0]?.[0] == null) && node.shift();
if (name) name in list ? err(`Duplicate ${list.name} ${name}`) : list[name] = list.length; // save alias
return name
}
// (type $id? (func param* result*))
// (type $id? (array (mut i8)))
// (type $id? (struct (field a)*)
// (type $id? (sub final? $nm* (struct|array|func ...)))
const typedef = ([dfn], ctx) => {
let subkind = 'subfinal', supertypes = [], compkind
if (dfn[0] === 'sub') {
subkind = dfn.shift(), dfn[0] === 'final' && (subkind += dfn.shift())
dfn = (supertypes = dfn).pop() // last item is definition
}
[compkind, ...dfn] = dfn // composite type kind
if (compkind === 'func') dfn = paramres(dfn), ctx.type['$' + dfn.join('>')] ??= ctx.type.length
else if (compkind === 'struct') dfn = fieldseq(dfn, 'field', true)
else if (compkind === 'array') [dfn] = dfn
return [compkind, dfn, subkind, supertypes]
}
// register (implicit) type
const regtype = (param, result, ctx, idx='$' + param + '>' + result) => (
(ctx.type[idx] ??= ctx.type.push(['func', [param, result]]) - 1),
idx
)
// consume typeuse nodes, return type index/params, or null idx if no type
// https://webassembly.github.io/spec/core/text/modules.html#type-uses
const typeuse = (nodes, ctx, names) => {
let idx, param, result
// explicit type (type 0|$name)
if (nodes[0]?.[0] === 'type') {
[, idx] = nodes.shift();
[param, result] = paramres(nodes, names);
const [,srcParamRes] = ctx.type[id(idx, ctx.type)] ?? err(`Unknown type ${idx}`)
// check type consistency (excludes forward refs)
if ((param.length || result.length) && srcParamRes.join('>') !== param + '>' + result) err(`Type ${idx} mismatch`)
return [idx, ...srcParamRes]
}
// implicit type (param i32 i32)(result i32)
return [idx, ...paramres(nodes, names)]
}
// consume (param t+)* (result t+)* sequence
const paramres = (nodes, names = true) => {
// let param = [], result = []
// collect param (param i32 i64) (param $x? i32)
let param = fieldseq(nodes, 'param', names)
// collect result eg. (result f64 f32)(result i32)
let result = fieldseq(nodes, 'result')
if (nodes[0]?.[0] === 'param') err(`Unexpected param`)
return [param, result]
}
// collect sequence of field, eg. (param a) (param b c), (field a) (field b c) or (result a b) (result c)
// optionally allow or not names
const fieldseq = (nodes, field, names = false) => {
let seq = []
// collect field eg. (field f64 f32)(field i32)
while (nodes[0]?.[0] === field) {
let [, ...args] = nodes.shift()
let name = args[0]?.[0] === '$' && args.shift()
// expose name refs, if allowed
if (name) {
if (names) name in seq ? err(`Duplicate ${field} ${name}`) : seq[name] = seq.length
else err(`Unexpected ${field} name ${name}`)
}
seq.push(...args)
}
return seq
}
// consume blocktype - makes sure either type or single result is returned
const blocktype = (nodes, ctx) => {
let [idx, param, result] = typeuse(nodes, ctx, 0)
// get type - can be either idx or valtype (numtype | reftype)
if (!param.length && !result.length) return
// (result i32) - doesn't require registering type
if (!param.length && result.length === 1) return ['result', ...result]
// register implicit type
idx ??= regtype(param, result, ctx)
return ['type', idx]
}
// abbr blocks, loops, ifs; collect implicit types via typeuses; resolve optional immediates
// https://webassembly.github.io/spec/core/text/instructions.html#folded-instructions
const plain = (nodes, ctx) => {
let out = [], stack = [], label
while (nodes.length) {
let node = nodes.shift()
// lookup is slower than sequence of known ifs
if (typeof node === 'string') {
out.push(node)
// block typeuse?
if (node === 'block' || node === 'if' || node === 'loop') {
// (loop $l?)
if (nodes[0]?.[0] === '$') label = nodes.shift(), out.push(label), stack.push(label)
out.push(blocktype(nodes, ctx))
}
// else $label
// end $label - make sure it matches block label
else if (node === 'else' || node === 'end') {
if (nodes[0]?.[0] === '$') (node === 'end' ? stack.pop() : label) !== (label = nodes.shift()) && err(`Mismatched label ${label}`)
}
// select (result i32 i32 i32)?
else if (node === 'select') {
out.push(paramres(nodes, 0)[1])
}
// call_indirect $table? $typeidx
// return_call_indirect $table? $typeidx
else if (node.endsWith('call_indirect')) {
let tableidx = nodes[0]?.[0] === '$' || !isNaN(nodes[0]) ? nodes.shift() : 0
let [idx, param, result] = typeuse(nodes, ctx, 0)
out.push(tableidx, ['type', idx ?? regtype(param, result, ctx)])
}
// mark datacount section as required
else if (node === 'memory.init' || node === 'data.drop' || node === 'array.new_data' || node === 'array.init_data') {
ctx.datacount[0] = true
}
// table.init tableidx? elemidx -> table.init tableidx elemidx
else if (node === 'table.init') out.push((nodes[1][0] === '$' || !isNaN(nodes[1])) ? nodes.shift() : 0, nodes.shift())
// table.* tableidx?
else if (node.startsWith('table.')) {
out.push(nodes[0]?.[0] === '$' || !isNaN(nodes[0]) ? nodes.shift() : 0)
// table.copy tableidx? tableidx?
if (node === 'table.copy') out.push(nodes[0][0] === '$' || !isNaN(nodes[0]) ? nodes.shift() : 0)
}
}
else {
// (block ...) -> block ... end
if (node[0] === 'block' || node[0] === 'loop') {
out.push(...plain(node, ctx), 'end')
}
// (if ...) -> if ... end
else if (node[0] === 'if') {
let then = [], els = [], immed = [node.shift()]
// (if label? blocktype? cond*? (then instr*) (else instr*)?) -> cond*? if label? blocktype? instr* else instr*? end
// https://webassembly.github.io/spec/core/text/instructions.html#control-instructions
if (node[node.length - 1]?.[0] === 'else') {
els = plain(node.pop(), ctx)
// ignore empty else
// https://webassembly.github.io/spec/core/text/instructions.html#abbreviations
if (els.length === 1) els.length = 0
}
if (node[node.length - 1]?.[0] === 'then') then = plain(node.pop(), ctx)
// label?
if (node[0]?.[0] === '$') immed.push(node.shift())
// blocktype?
immed.push(blocktype(node, ctx))
if (typeof node[0] === 'string') err('Unfolded condition')
out.push(...plain(node, ctx), ...immed, ...then, ...els, 'end')
}
else out.push(plain(node, ctx))
}
}
return out
}
// build section binary [by section codes] (non consuming)
const build = [,
// type kinds
// (func params result)
// (array i8)
// (struct ...fields)
([kind, fields, subkind, supertypes, rec], ctx) => {
if (rec === true) return // ignore rec subtypes cept for 1st one
let details
// (rec (sub ...)*)
if (rec) {
kind = 'rec'
let [from, length] = rec, subtypes = Array.from({ length }, (_, i) => build[SECTION.type](ctx.type[from + i].slice(0, 4), ctx))
details = vec(subtypes)
}
// (sub final? sups* (type...))
else if (subkind === 'sub' || supertypes?.length) {
details = [...vec(supertypes.map(n => id(n, ctx.type))), ...build[SECTION.type]([kind, fields], ctx)]
kind = subkind
}
else if (kind === 'func') {
details = [...vec(fields[0].map(t => reftype(t, ctx))), ...vec(fields[1].map(t => reftype(t, ctx)))]
}
else if (kind === 'array') {
details = fieldtype(fields, ctx)
}
else if (kind === 'struct') {
details = vec(fields.map(t => fieldtype(t, ctx)))
}
return [DEFTYPE[kind], ...details]
},
// (import "math" "add" (func|table|global|memory dfn?))
([mod, field, [kind, ...dfn]], ctx) => {
let details
if (kind === 'func') {
// we track imported funcs in func section to share namespace, and skip them on final build
let [[, typeidx]] = dfn
details = uleb(id(typeidx, ctx.type))
}
else if (kind === 'memory') {
details = limits(dfn)
}
else if (kind === 'global') {
details = fieldtype(dfn[0], ctx)
}
else if (kind === 'table') {
details = [...reftype(dfn.pop(), ctx), ...limits(dfn)]
}
else err(`Unknown kind ${kind}`)
return ([...vec(str(mod.slice(1, -1))), ...vec(str(field.slice(1, -1))), KIND[kind], ...details])
},
// (func $name? ...params result ...body)
([[, typeidx]], ctx) => (uleb(id(typeidx, ctx.type))),
// (table 1 2 funcref)
(node, ctx) => {
let lims = limits(node), t = reftype(node.shift(), ctx), [init] = node
return init ? [0x40, 0x00, ...t, ...lims, ...expr(init, ctx)] : [...t, ...lims]
},
// (memory id? export* min max shared)
(node, ctx) => limits(node),
// (global $id? (mut i32) (i32.const 42))
([t, init], ctx) => [...fieldtype(t, ctx), ...expr(init, ctx)],
// (export "name" (func|table|mem $name|idx))
([nm, [kind, l]], ctx) => ([...vec(str(nm.slice(1, -1))), KIND[kind], ...uleb(id(l, ctx[kind]))]),
// (start $main)
([l], ctx) => uleb(id(l, ctx.func)),
// (elem elem*) - passive
// (elem declare elem*) - declarative
// (elem (table idx)? (offset expr)|(expr) elem*) - active
// ref: https://webassembly.github.io/spec/core/binary/modules.html#element-section
(parts, ctx) => {
let passive = 0, declare = 0, elexpr = 0, nofunc = 0, tabidx, offset, rt
// declare?
if (parts[0] === 'declare') parts.shift(), declare = 1
// table?
if (parts[0][0] === 'table') {
[, tabidx] = parts.shift()
tabidx = id(tabidx, ctx.table)
}
// (offset expr)|expr
if (parts[0]?.[0] === 'offset' || (Array.isArray(parts[0]) && parts[0][0] !== 'item' && !parts[0][0].startsWith('ref'))) {
offset = parts.shift()
if (offset[0] === 'offset') [, offset] = offset
offset = expr(offset, ctx)
}
// no offset = passive
else if (!declare) passive = 1
// funcref|externref|(ref ...)
if (REFTYPE[parts[0]] || parts[0]?.[0] === 'ref') rt = reftype(parts.shift(), ctx)
// func ... abbr https://webassembly.github.io/function-references/core/text/modules.html#id7
else if (parts[0] === 'func') rt = [HEAPTYPE[parts.shift()]]
// or anything else
else rt = [HEAPTYPE.func]
// deabbr els sequence, detect expr usage
parts = parts.map(el => {
if (el[0] === 'item') [, ...el] = el
if (el[0] === 'ref.func') [, el] = el
// (ref.null func) and other expressions turn expr els mode
if (typeof el !== 'string') elexpr = 1
return el
})
// reftype other than (ref null? func) forces table index via nofunc flag
// also it forces elexpr
if (rt[0] !== REFTYPE.funcref) nofunc = 1, elexpr = 1
// mode:
// bit 0 indicates a passive or declarative segment
// bit 1 indicates the presence of an explicit table index for an active segment
// and otherwise distinguishes passive from declarative segments
// bit 2 indicates the use of element type and element expressions instead of elemkind=0x00 and element indices.
let mode = (elexpr << 2) | ((passive || declare ? declare : (!!tabidx || nofunc)) << 1) | (passive || declare);
return ([
mode,
...(
// 0b000 e:expr y*:vec(funcidx) | type=(ref func), init ((ref.func y)end)*, active (table=0,offset=e)
mode === 0b000 ? offset :
// 0b001 et:elkind y*:vec(funcidx) | type=0x00, init ((ref.func y)end)*, passive
mode === 0b001 ? [0x00] :
// 0b010 x:tabidx e:expr et:elkind y*:vec(funcidx) | type=0x00, init ((ref.func y)end)*, active (table=x,offset=e)
mode === 0b010 ? [...uleb(tabidx || 0), ...offset, 0x00] :
// 0b011 et:elkind y*:vec(funcidx) | type=0x00, init ((ref.func y)end)*, passive declare
mode === 0b011 ? [0x00] :
// 0b100 e:expr el*:vec(expr) | type=(ref null func), init el*, active (table=0, offset=e)
mode === 0b100 ? offset :
// 0b101 et:reftype el*:vec(expr) | type=et, init el*, passive
mode === 0b101 ? rt :
// 0b110 x:tabidx e:expr et:reftype el*:vec(expr) | type=et, init el*, active (table=x, offset=e)
mode === 0b110 ? [...uleb(tabidx || 0), ...offset, ...rt] :
// 0b111 et:reftype el*:vec(expr) | type=et, init el*, passive declare
rt
),
...vec(
parts.map(elexpr ?
// ((ref.func y)end)*
el => expr(typeof el === 'string' ? ['ref.func', el] : el, ctx) :
// el*
el => uleb(id(el, ctx.func))
)
)
])
},
// (code)
(body, ctx) => {
let [typeidx, param] = body.shift()
if (!param) [, [param]] = ctx.type[id(typeidx, ctx.type)]
// provide param/local in ctx
ctx.local = Object.create(param) // list of local variables - some of them are params
ctx.block = [] // control instructions / blocks stack
// display names for error messages
ctx.local.name = 'local'
ctx.block.name = 'block'
// collect locals
while (body[0]?.[0] === 'local') {
let [, ...types] = body.shift()
if (types[0]?.[0] === '$') {
let name = types.shift()
if (name in ctx.local) err(`Duplicate local ${name}`)
else ctx.local[name] = ctx.local.length
}
ctx.local.push(...types)
}
const bytes = []
while (body.length) bytes.push(...instr(body, ctx))
bytes.push(0x0b)
// squash locals into (n:u32 t:valtype)*, n is number and t is type
// we skip locals provided by params
let loctypes = ctx.local.slice(param.length).reduce((a, type) => (type == a[a.length - 1]?.[1] ? a[a.length - 1][0]++ : a.push([1, type]), a), [])
// cleanup tmp state
ctx.local = ctx.block = null
// https://webassembly.github.io/spec/core/binary/modules.html#code-section
return vec([...vec(loctypes.map(([n, t]) => [...uleb(n), ...reftype(t, ctx)])), ...bytes])
},
// (data (i32.const 0) "\aa" "\bb"?)
// (data (memory ref) (offset (i32.const 0)) "\aa" "\bb"?)
// (data (global.get $x) "\aa" "\bb"?)
(inits, ctx) => {
let offset, memidx = 0
// (memory ref)?
if (inits[0]?.[0] === 'memory') {
[, memidx] = inits.shift()
memidx = id(memidx, ctx.memory)
}
// (offset (i32.const 0)) or (i32.const 0)
if (typeof inits[0] !== 'string') {
offset = inits.shift()
if (offset[0] === 'offset') [, offset] = offset
offset ?? err('Bad offset', offset)
}
return ([
...(
// active: 2, x=memidx, e=expr
memidx ? [2, ...uleb(memidx), ...expr(offset, ctx)] :
// active: 0, e=expr
offset ? [0, ...expr(offset, ctx)] :
// passive: 1
[1]
),
...vec(str(inits.map(i => i.slice(1, -1)).join('')))
])
},
// datacount
(nodes, ctx) => uleb(ctx.data.length)
]
// build reftype, either direct absheaptype or wrapped heaptype https://webassembly.github.io/gc/core/binary/types.html#reference-types
const reftype = (t, ctx) => (
t[0] === 'ref' ?
t[1] == 'null' ?
HEAPTYPE[t[2]] ? [HEAPTYPE[t[2]]] : [REFTYPE.refnull, ...uleb(id(t[t.length - 1], ctx.type))] :
[TYPE.ref, ...uleb(HEAPTYPE[t[t.length - 1]] || id(t[t.length - 1], ctx.type))] :
// abbrs
[TYPE[t] ?? err(`Unknown type ${t}`)]
);
// build type with mutable flag (mut t) or t
const fieldtype = (t, ctx, mut = t[0] === 'mut' ? 1 : 0) => [...reftype(mut ? t[1] : t, ctx), mut];
// consume one instruction from nodes sequence
const instr = (nodes, ctx) => {
if (!nodes?.length) return []
let out = [], op = nodes.shift(), immed, code
// consume group
if (Array.isArray(op)) {
immed = instr(op, ctx)
while (op.length) out.push(...instr(op, ctx))
out.push(...immed)
return out
}
[...immed] = isNaN(op[0]) && INSTR[op] || err(`Unknown instruction ${op}`)
code = immed[0]
// gc-related
// https://webassembly.github.io/gc/core/binary/instructions.html#reference-instructions
if (code === 0x0fb) {
[, code] = immed
// struct.new $t ... array.set $t
if ((code >= 0 && code <= 14) || (code >= 16 && code <= 19)) {
let tidx = id(nodes.shift(), ctx.type)
immed.push(...uleb(tidx))
// struct.get|set* $t $f - read field by index from struct definition (ctx.type[structidx][dfnidx])
if (code >= 2 && code <= 5) immed.push(...uleb(id(nodes.shift(), ctx.type[tidx][1])))
// array.new_fixed $t n
else if (code === 8) immed.push(...uleb(nodes.shift()))
// array.new_data|init_data $t $d
else if (code === 9 || code === 18) immed.push(...uleb(id(nodes.shift(), ctx.data)))
// array.new_elem|init_elem $t $e
else if (code === 10 || code === 19) immed.push(...uleb(id(nodes.shift(), ctx.elem)))
// array.copy $t $t
else if (code === 17) immed.push(...uleb(id(nodes.shift(), ctx.type)))
}
// ref.test|cast (ref null? $t|heaptype)
else if (code >= 20 && code <= 23) {
let ht = reftype(nodes.shift(), ctx)
if (ht[0] !== REFTYPE.ref) immed.push(code = immed.pop()+1) // ref.test|cast (ref null $t) is next op
if (ht.length > 1) ht.shift() // pop ref
immed.push(...ht)
}
// br_on_cast[_fail] $l? (ref null? ht1) (ref null? ht2)
else if (code === 24 || code === 25) {
let i = blockid(nodes.shift(), ctx.block),
ht1 = reftype(nodes.shift(), ctx),
ht2 = reftype(nodes.shift(), ctx),
castflags = ((ht2[0] !== REFTYPE.ref) << 1) | (ht1[0] !== REFTYPE.ref)
immed.push(castflags, ...uleb(i), ht1.pop(), ht2.pop()) // we take only abstype or
}
}
// bulk memory: (memory.init) (memory.copy) (data.drop) (memory.fill)
// table ops: (table.init|copy|grow|size|fill) (elem.drop)
// https://github.com/WebAssembly/bulk-memory-operations/blob/master/proposals/bulk-memory-operations/Overview.md#instruction-encoding
else if (code == 0xfc) {
[, code] = immed
// memory.init idx, data.drop idx,
if (code === 0x08 || code === 0x09) {
immed.push(...uleb(id(nodes.shift(), ctx.data)))
}
// memory placeholders
if (code == 0x08 || code == 0x0b) immed.push(0)
else if (code === 0x0a) immed.push(0, 0)
// elem.drop elemidx
if (code === 0x0d) {
immed.push(...uleb(id(nodes.shift(), ctx.elem)))
}
// table.init tableidx elemidx -> 0xfc 0x0c elemidx tableidx
else if (code === 0x0c) {
immed.push(...uleb(id(nodes[1], ctx.elem)), ...uleb(id(nodes.shift(), ctx.table)))
nodes.shift()
}
// table.* tableidx?
// abbrs https://webassembly.github.io/spec/core/text/instructions.html#id1
else if (code >= 0x0c && code < 0x13) {
immed.push(...uleb(id(nodes.shift(), ctx.table)))
// table.copy tableidx? tableidx?
if (code === 0x0e) immed.push(...uleb(id(nodes.shift(), ctx.table)))
}
}
// v128s: (v128.load x) etc
// https://github.com/WebAssembly/simd/blob/master/proposals/simd/BinarySIMD.md
else if (code === 0xfd) {
[, code] = immed
immed = [0xfd, ...uleb(code)]
// (v128.load offset? align?)
if (code <= 0x0b) {
const [a, o] = memarg(nodes)
immed.push(...uleb((a ?? align(op))), ...uleb(o ?? 0))
}
// (v128.load_lane offset? align? idx)
else if (code >= 0x54 && code <= 0x5d) {
const [a, o] = memarg(nodes)
immed.push(...uleb((a ?? align(op))), ...uleb(o ?? 0))
// (v128.load_lane_zero)
if (code <= 0x5b) immed.push(...uleb(nodes.shift()))
}
// (i8x16.shuffle 0 1 ... 15 a b)
else if (code === 0x0d) {
// i8, i16, i32 - bypass the encoding
for (let i = 0; i < 16; i++) immed.push(parseUint(nodes.shift(), 32))
}
// (v128.const i32x4 1 2 3 4)
else if (code === 0x0c) {
let [t, n] = nodes.shift().split('x'),
bits = +t.slice(1),
stride = bits >>> 3 // i16 -> 2, f32 -> 4
n = +n
// i8, i16, i32 - bypass the encoding
if (t[0] === 'i') {
let arr = n === 16 ? new Uint8Array(16) : n === 8 ? new Uint16Array(8) : n === 4 ? new Uint32Array(4) : new BigUint64Array(2)
for (let i = 0; i < n; i++) {
let s = nodes.shift(), v = encode[t].parse(s)
arr[i] = v
}
immed.push(...(new Uint8Array(arr.buffer)))
}
// f32, f64 - encode
else {
let arr = new Uint8Array(16)
for (let i = 0; i < n; i++) {
let s = nodes.shift(), v = encode[t](s)
arr.set(v, i * stride)
}
immed.push(...arr)
}
}
// (i8x16.extract_lane_s 0 ...)
else if (code >= 0x15 && code <= 0x22) {
immed.push(...uleb(parseUint(nodes.shift())))
}
}
// control block abbrs
// block ..., loop ..., if ...
else if (code === 2 || code === 3 || code === 4) {
ctx.block.push(code)
// (block $x) (loop $y) - save label pointer
if (nodes[0]?.[0] === '$') ctx.block[nodes.shift()] = ctx.block.length
let t = nodes.shift();
// void
if (!t) immed.push(TYPE.void)
// (result i32) - doesn't require registering type
// FIXME: Make sure it is signed positive integer (leb, not uleb) https://webassembly.github.io/gc/core/binary/instructions.html#control-instructions
else if (t[0] === 'result') immed.push(...reftype(t[1], ctx))
// (type idx)
else immed.push(...uleb(id(t[1], ctx.type)))
}
// else
else if (code === 5) { }
// then
else if (code === 6) immed = [] // ignore
// local.get $id, local.tee $id x
else if (code == 0x20 || code == 0x21 || code == 0x22) {
immed.push(...uleb(id(nodes.shift(), ctx.local)))
}
// global.get $id, global.set $id
else if (code == 0x23 || code == 0x24) {
immed.push(...uleb(id(nodes.shift(), ctx.global)))
}
// call $func ...nodes
// return_call $func
else if (code == 0x10 || code == 0x12) {
immed.push(...uleb(id(nodes.shift(), ctx.func)))
}
// call_indirect $table (type $typeName) ...nodes
// return_call_indirect $table (type $typeName) ... nodes
else if (code == 0x11 || code == 0x13) {
immed.push(
...uleb(id(nodes[1][1], ctx.type)),
...uleb(id(nodes.shift(), ctx.table))
)
nodes.shift()
}
// call_ref $type
// return_call_ref $type
else if (code == 0x14 || code == 0x15) {
immed.push(...uleb(id(nodes.shift(), ctx.type)))
}
// end
else if (code == 0x0b) ctx.block.pop()
// br $label result?
// br_if $label cond result?
// br_on_null $l, br_on_non_null $l
else if (code == 0x0c || code == 0x0d || code == 0xd5 || code == 0xd6) {
immed.push(...uleb(blockid(nodes.shift(), ctx.block)))
}
// br_table 1 2 3 4 0 selector result?
else if (code == 0x0e) {
let args = []
while (nodes[0] && (!isNaN(nodes[0]) || nodes[0][0] === '$')) {
args.push(...uleb(blockid(nodes.shift(), ctx.block)))
}
args.unshift(...uleb(args.length - 1))
immed.push(...args)
}
// select (result t+)
else if (code == 0x1b) {
let result = nodes.shift()
// 0x1b -> 0x1c
if (result.length) immed.push(immed.pop() + 1, ...vec(result.map(t => reftype(t, ctx))))
}
// ref.func $id
else if (code == 0xd2) {
immed.push(...uleb(id(nodes.shift(), ctx.func)))
}
// ref.null func
else if (code == 0xd0) {
let t = nodes.shift()
immed.push(...(HEAPTYPE[t] ? [HEAPTYPE[t]] : uleb(id(t, ctx.type)))) // func->funcref, extern->externref
}
// binary/unary (i32.add a b) - no immed
else if (code >= 0x45) { }
// i32.store align=n offset=m
else if (code >= 0x28 && code <= 0x3e) {
let [a, o] = memarg(nodes)
immed.push(...uleb((a ?? align(op))), ...uleb(o ?? 0))
}
// i32.const 123, f32.const 123.45
else if (code >= 0x41 && code <= 0x44) {
immed.push(...encode[op.split('.')[0]](nodes.shift()))
}
// memory.grow|size $idx - mandatory 0x00
// https://webassembly.github.io/spec/core/binary/instructions.html#memory-instructions
else if (code == 0x3f || code == 0x40) {
immed.push(0)
}
// table.get|set $id
else if (code == 0x25 || code == 0x26) {
immed.push(...uleb(id(nodes.shift(), ctx.table)))
}
out.push(...immed)
return out
}
// instantiation time value initializer (consuming) - we redirect to instr
const expr = (node, ctx) => [...instr([node], ctx), 0x0b]
// deref id node to numeric idx
const id = (nm, list, n) => (n = nm[0] === '$' ? list[nm] : +nm, n in list ? n : err(`Unknown ${list.name} ${nm}`))
// block id - same as id but for block
// index indicates how many block items to pop
const blockid = (nm, block, i) => (
i = nm?.[0] === '$' ? block.length - block[nm] : +nm,
isNaN(i) || i > block.length ? err(`Bad label ${nm}`) : i
)
// consume align/offset params
const memarg = (args) => {
let align, offset, k, v
while (args[0]?.includes('=')) [k, v] = args.shift().split('='), k === 'offset' ? offset = +v : k === 'align' ? align = +v : err(`Unknown param ${k}=${v}`)
if (offset < 0 || offset > 0xffffffff) err(`Bad offset ${offset}`)
if (align <= 0 || align > 0xffffffff) err(`Bad align ${align}`)
if (align) ((align = Math.log2(align)) % 1) && err(`Bad align ${align}`)
return [align, offset]
}
// const ALIGN = {
// 'i32.load': 4, 'i64.load': 8, 'f32.load': 4, 'f64.load': 8,
// 'i32.load8_s': 1, 'i32.load8_u': 1, 'i32.load16_s': 2, 'i32.load16_u': 2,
// 'i64.load8_s': 1, 'i64.load8_u': 1, 'i64.load16_s': 2, 'i64.load16_u': 2, 'i64.load32_s': 4, 'i64.load32_u': 4, 'i32.store': 4,
// 'i64.store': 8, 'f32.store': 4, 'f64.store': 8, 'i32.store8': 1, 'i32.store16': 2, 'i64.store8': 1, 'i64.store16': 2, 'i64.store32': 4,
// 'v128.load': 16, 'v128.load8x8_s': 8, 'v128.load8x8_u': 8, 'v128.load16x4_s': 8, 'v128.load16x4_u': 8, 'v128.load32x2_s': 8, 'v128.load32x2_u': 8, 'v128.load8_splat': 1, 'v128.load16_splat': 2, 'v128.load32_splat': 4, 'v128.load64_splat': 8, 'v128.store': 16,
// 'v128.load': 16, 'v128.load8_lane': 1, 'v128.load16_lane': 2, 'v128.load32_lane': 4, 'v128.load64_lane': 8, 'v128.store8_lane': 1, 'v128.store16_lane': 2, 'v128.store32_lane': 4, 'v128.store64_lane': 8, 'v128.load32_zero': 4, 'v128.load64_zero': 8
// }
const align = (op) => {
let [group, opname] = op.split('.'); // v128.load8x8_u -> group = v128, opname = load8x8_u
let [lsize] = (opname[0] === 'l' ? opname.slice(4) : opname.slice(5)).split('_') // load8x8_u -> lsize = 8x8
let [size, x] = lsize ? lsize.split('x') : [group.slice(1)] // 8x8 -> size = 8
return Math.log2(x ? 8 : +size / 8)
}
// build limits sequence (consuming)
const limits = (node) => (
isNaN(parseInt(node[1])) ? [0, ...uleb(parseUint(node.shift()))] : [node[2] === 'shared' ? 3 : 1, ...uleb(parseUint(node.shift())), ...uleb(parseUint(node.shift()))]
)
// check if node is valid int in a range
// we put extra condition for index ints for tests complacency
const parseUint = (v, max = 0xFFFFFFFF) => (typeof v === 'string' && v[0] !== '+' ? (typeof max === 'bigint' ? i64 : i32).parse(v) : typeof v === 'number' ? v : err(`Bad int ${v}`)) > max ? err(`Value out of range ${v}`) : v
// escape codes
const escape = { n: 10, r: 13, t: 9, v: 1, '"': 34, "'": 39, '\\': 92 }
// build string binary
const str = str => {
let res = [], i = 0, c, BSLASH = 92
// https://webassembly.github.io/spec/core/text/values.html#strings
for (; i < str.length;) {
c = str.charCodeAt(i++)
res.push(c === BSLASH ? escape[str[i++]] || parseInt(str.slice(i - 1, ++i), 16) : c)
}
return res
}
// serialize binary array
const vec = a => [...uleb(a.length), ...a.flat()]