watr 2.4.1 → 3.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/compile.js CHANGED
@@ -1,494 +1,778 @@
1
1
  import * as encode from './encode.js'
2
2
  import { uleb } from './encode.js'
3
- import { OP, SECTION, ALIGN, TYPE, KIND } from './const.js'
3
+ import { SECTION, TYPE, KIND, INSTR, HEAPTYPE, REFTYPE } from './const.js'
4
4
  import parse from './parse.js'
5
- import { err } from './util.js'
6
5
 
6
+ // build instructions index
7
+ INSTR.forEach((op, i) => INSTR[op] = i >= 0x11a ? [0xfd, i - 0x11a] : i >= 0xfc ? [0xfc, i - 0xfc] : [i]);
7
8
 
9
+ // console.log(INSTR)
8
10
  /**
9
- * Converts a WebAssembly Text Format (WAT) tree to a WebAssembly binary format (Wasm).
11
+ * Converts a WebAssembly Text Format (WAT) tree to a WebAssembly binary format (WASM).
10
12
  *
11
- * @param {string|Array} nodes - The WAT tree or string to be compiled to Wasm binary.
12
- * @returns {Uint8Array} The compiled Wasm binary data.
13
+ * @param {string|Array} nodes - The WAT tree or string to be compiled to WASM binary.
14
+ * @param {Object} opt - opt.fullSize for fixed-width uleb encoding
15
+ * @returns {Uint8Array} The compiled WASM binary data.
13
16
  */
14
- export default (nodes) => {
17
+ export default function watr(nodes) {
18
+ // normalize to (module ...) form
15
19
  if (typeof nodes === 'string') nodes = parse(nodes);
20
+ else nodes = clone(nodes)
16
21
 
17
- // IR. Alias is stored directly to section array by key, eg. section.func.$name = idx
18
- let sections = {
19
- type: [], import: [], func: [], table: [], memory: [], global: [], export: [], start: [], elem: [], code: [], data: []
20
- }, binary = [
21
- 0x00, 0x61, 0x73, 0x6d, // magic
22
- 0x01, 0x00, 0x00, 0x00, // version
23
- ]
24
-
25
- // 1. transform tree
26
- // (func) → [(func)]
27
- if (typeof nodes[0] === 'string' && nodes[0] !== 'module') nodes = [nodes]
28
-
29
- // (global $a (import "a" "b") (mut i32)) → (import "a" "b" (global $a (mut i32)))
30
- // (memory (import "a" "b") min max shared)(import "a" "b" (memory min max shared))
31
- nodes = nodes.map(node => {
32
- if (node[2]?.[0] === 'import') {
33
- let [kind, name, imp, ...args] = node
34
- return [...imp, [kind, name, ...args]]
22
+ // module abbr https://webassembly.github.io/spec/core/text/modules.html#id10
23
+ if (nodes[0] === 'module') nodes.shift(), nodes[0]?.[0] === '$' && nodes.shift()
24
+ // single node, not module
25
+ else if (typeof nodes[0] === 'string') nodes = [nodes]
26
+
27
+ // binary abbr "\00" "\0x61" ...
28
+ if (nodes[0] === 'binary') {
29
+ nodes.shift()
30
+ return Uint8Array.from(str(nodes.map(i => i.slice(1, -1)).join('')))
31
+ }
32
+ // quote "a" "b"
33
+ else if (nodes[0] === 'quote') {
34
+ nodes.shift()
35
+ return watr(nodes.map(i => i.slice(1, -1)).join(''))
36
+ }
37
+
38
+ // Scopes are stored directly on section array by key, eg. section.func.$name = idx
39
+ const sections = []
40
+ for (let kind in SECTION) (sections[SECTION[kind]] = sections[kind] = []).name = kind
41
+ sections._ = {} // implicit types
42
+
43
+ for (let [kind, ...node] of nodes) {
44
+ let imported // if node needs to be imported
45
+
46
+ // import abbr
47
+ // (import m n (table|memory|global|func id? type)) -> (table|memory|global|func id? (import m n) type)
48
+ if (kind === 'import') [kind, ...node] = (imported = node).pop()
49
+
50
+ // index, alias
51
+ let name = node[0]?.[0] === '$' && node.shift(), idx = sections[kind].length;
52
+ if (name) name in sections[kind] ? err(`Duplicate ${kind} ${name}`) : sections[kind][name] = idx; // save alias
53
+
54
+ // export abbr
55
+ // (table|memory|global|func id? (export n)* ...) -> (table|memory|global|func id ...) (export n (table|memory|global|func id))
56
+ while (node[0]?.[0] === 'export') sections.export.push([node.shift()[1], [kind, idx]])
57
+
58
+ // for import nodes - redirect output to import
59
+ if (node[0]?.[0] === 'import') [, ...imported] = node.shift()
60
+
61
+ // table abbr
62
+ if (kind === 'table') {
63
+ // (table id? reftype (elem ...{n})) -> (table id? n n reftype) (elem (table id) (i32.const 0) reftype ...)
64
+ if (node[1]?.[0] === 'elem') {
65
+ let [reftype, [, ...els]] = node
66
+ node = [els.length, els.length, reftype]
67
+ sections.elem.push([['table', name || sections.table.length], ['i32.const', '0'], reftype, ...els])
68
+ }
35
69
  }
36
- else if (node[1]?.[0] === 'import') {
37
- let [kind, imp, ...args] = node
38
- return [...imp, [kind, ...args]]
70
+
71
+ // data abbr
72
+ // (memory id? (data str)) -> (memory id? n n) (data (memory id) (i32.const 0) str)
73
+ else if (kind === 'memory' && node[0]?.[0] === 'data') {
74
+ let [, ...data] = node.shift(), m = '' + Math.ceil(data.map(s => s.slice(1, -1)).join('').length / 65536) // FIXME: figure out actual data size
75
+ sections.data.push([['memory', idx], ['i32.const', 0], ...data])
76
+ node = [m, m]
39
77
  }
40
- return node
41
- })
42
78
 
43
- // 2. build IR. import must be initialized first, global before func, elem after func
44
- let order = ['type', 'import', 'table', 'memory', 'global', 'func', 'export', 'start', 'elem', 'data'], postcall = []
79
+ // keep start name
80
+ else if (kind === 'start') name && node.push(name);
81
+
82
+ // [func, [param, result]] -> [param, result], alias
83
+ else if (kind === 'type') node[0].shift(), node = paramres(node[0]), sections.type['$' + node.join('>')] ??= idx
45
84
 
46
- for (let name of order) {
47
- let remaining = []
48
- for (let node of nodes) {
49
- node[0] === name ? postcall.push(build[name](node, sections)) : remaining.push(node)
85
+ // dupe to code section, save implicit type
86
+ else if (kind === 'func') {
87
+ let [idx, param, result] = typeuse(node, sections);
88
+ idx ?? (sections._[idx = '$' + param + '>' + result] = [param, result]);
89
+ !imported && nodes.push(['code', [idx, param, result], ...plain(node, sections)]) // pass param since they may have names
90
+ node.unshift(['type', idx])
50
91
  }
51
- nodes = remaining
92
+
93
+ // import writes to import section amd adds placeholder for (kind) section
94
+ if (imported) sections.import.push([...imported, [kind, ...node]]), node = null
95
+
96
+ sections[kind].push(node)
52
97
  }
53
98
 
54
- // code must be compiled after all definitions
55
- for (let cb of postcall) cb?.()
56
-
57
- // 3. build binary
58
- for (let name in sections) {
59
- let items = sections[name]
60
- if (items.importc) items = items.slice(items.importc) // discard imported functions/globals
61
- if (!items.length) continue
62
- let sectionCode = SECTION[name], bytes = []
63
- if (sectionCode !== 8) bytes.push(items.length) // skip start section count
64
- for (let item of items) bytes.push(...item)
65
- binary.push(sectionCode, ...uleb(bytes.length), ...bytes)
99
+ // add implicit types - main types receive aliases, implicit types are added if no explicit types exist
100
+ for (let n in sections._) sections.type[n] ??= sections.type.push(sections._[n]) - 1
101
+
102
+ // patch datacount if data === 0
103
+ if (!sections.data.length) sections.datacount.length = 0
104
+
105
+ // convert nodes to bytes
106
+ const bin = (kind, count = true) => {
107
+ let items = sections[kind].filter(Boolean).map(item => build[kind](item, sections))
108
+ return !items.length ? [] : [kind, ...vec(count ? vec(items) : items)]
66
109
  }
67
110
 
68
- return new Uint8Array(binary)
111
+ // build final binary
112
+ return Uint8Array.from([
113
+ 0x00, 0x61, 0x73, 0x6d, // magic
114
+ 0x01, 0x00, 0x00, 0x00, // version
115
+ ...bin(SECTION.custom),
116
+ ...bin(SECTION.type),
117
+ ...bin(SECTION.import),
118
+ ...bin(SECTION.func),
119
+ ...bin(SECTION.table),
120
+ ...bin(SECTION.memory),
121
+ ...bin(SECTION.global),
122
+ ...bin(SECTION.export),
123
+ ...bin(SECTION.start, false),
124
+ ...bin(SECTION.elem),
125
+ ...bin(SECTION.datacount, false),
126
+ ...bin(SECTION.code),
127
+ ...bin(SECTION.data)
128
+ ])
69
129
  }
70
130
 
71
- const build = {
72
- // (type $name? (func (param $x i32) (param i64 i32) (result i32 i64)))
73
- // signature part is identical to function
74
- // FIXME: handle non-function types
75
- type([, typeName, [kind, ...sig]], ctx) {
76
- if (kind !== 'func') err(`Unknown type kind '${kind}'`)
77
- const [idx] = consumeType(sig, ctx)
78
- if (typeName) ctx.type[typeName] = idx
79
- },
131
+ // abbr blocks, loops, ifs; collect implicit types via typeuses; resolve optional immediates
132
+ // https://webassembly.github.io/spec/core/text/instructions.html#folded-instructions
133
+ const plain = (nodes, ctx) => {
134
+ let out = []
80
135
 
81
- // (func $name? ...params result ...body)
82
- func([, ...body], ctx) {
83
- let locals = [], // list of local variables
84
- blocks = [] // control instructions / blocks stack
136
+ while (nodes.length) {
137
+ let node = nodes.shift()
85
138
 
86
- // fn name
87
- if (body[0]?.[0] === '$') ctx.func[body.shift()] = ctx.func.length
139
+ if (typeof node === 'string') {
140
+ out.push(node)
88
141
 
89
- // export binding
90
- if (body[0]?.[0] === 'export') build.export([...body.shift(), ['func', ctx.func.length]], ctx)
142
+ if (abbr[node]) out.push(...abbr[node](nodes, ctx))
143
+ }
91
144
 
92
- // register/consume type info
93
- let [typeIdx, params, result] = consumeType(body, ctx)
145
+ // FIXME: try to move this to abbr
146
+ else {
147
+ node = plain(node, ctx)
94
148
 
95
- // register new function
96
- ctx.func.push([typeIdx])
149
+ // (block ...) -> block ... end
150
+ if (node[0] === 'block' || node[0] === 'loop') {
151
+ out.push(...node, 'end')
152
+ }
97
153
 
98
- // collect locals
99
- while (body[0]?.[0] === 'local') {
100
- let [, ...types] = body.shift(), name
101
- if (types[0][0] === '$')
102
- params[name = types.shift()] ? err('Ambiguous name ' + name) :
103
- locals[name] = params.length + locals.length
104
- locals.push(...types.map(t => TYPE[t]))
105
- }
154
+ // (if ...) -> if ... end
155
+ else if (node[0] === 'if') {
156
+ let thenelse = [], blocktype = [node.shift()]
157
+ // (if label? blocktype? cond*? (then instr*) (else instr*)?) -> cond*? if label? blocktype? instr* else instr*? end
158
+ // https://webassembly.github.io/spec/core/text/instructions.html#control-instructions
159
+ if (node[node.length - 1]?.[0] === 'else') thenelse.unshift(...node.pop())
160
+ if (node[node.length - 1]?.[0] === 'then') thenelse.unshift(...node.pop())
106
161
 
107
- // squash local types
108
- let locTypes = locals.reduce((a, type) => (type == a[a.length - 1] ? a[a.length - 2]++ : a.push(1, type), a), [])
162
+ // label?
163
+ if (node[0]?.[0] === '$') blocktype.push(node.shift())
109
164
 
110
- // convert sequence of instructions from input nodes to out bytes
111
- const consume = (nodes, out = []) => {
112
- if (!nodes?.length) return out
165
+ // blocktype? - (param) are removed already via plain
166
+ if (node[0]?.[0] === 'type' || node[0]?.[0] === 'result') blocktype.push(node.shift());
113
167
 
114
- let op = nodes.shift(), opCode, args = nodes, immed, id, group
168
+ // ignore empty else
169
+ // https://webassembly.github.io/spec/core/text/instructions.html#abbreviations
170
+ if (thenelse[thenelse.length - 1] === 'else') thenelse.pop()
115
171
 
116
- // groups are flattened, eg. (cmd z w) -> z w cmd
117
- if (group = Array.isArray(op)) {
118
- args = [...op] // op is immutable
119
- opCode = OP.indexOf(op = args.shift())
120
- }
121
- else opCode = OP.indexOf(op)
122
-
123
- // NOTE: numeric comparison is faster than generic hash lookup
124
-
125
- // v128s: (v128.load x) etc
126
- // https://github.com/WebAssembly/simd/blob/master/proposals/simd/BinarySIMD.md
127
- if (opCode >= 268) {
128
- opCode -= 268
129
- immed = [0xfd, ...uleb(opCode)]
130
- // (v128.load)
131
- if (opCode <= 0x0b) {
132
- const o = consumeParams(args)
133
- immed.push(Math.log2(o.align ?? ALIGN[op]), ...uleb(o.offset ?? 0))
134
- }
135
- // (v128.load_lane offset? align? idx)
136
- else if (opCode >= 0x54 && opCode <= 0x5d) {
137
- const o = consumeParams(args)
138
- immed.push(Math.log2(o.align ?? ALIGN[op]), ...uleb(o.offset ?? 0))
139
- // (v128.load_lane_zero)
140
- if (opCode <= 0x5b) immed.push(...uleb(args.shift()))
141
- }
142
- // (i8x16.shuffle 0 1 ... 15 a b)
143
- else if (opCode === 0x0d) {
144
- // i8, i16, i32 - bypass the encoding
145
- for (let i = 0; i < 16; i++) immed.push(encode.i32.parse(args.shift()))
146
- }
147
- // (v128.const i32x4)
148
- else if (opCode === 0x0c) {
149
- args.unshift(op)
150
- immed = consumeConst(args, ctx)
151
- }
152
- // (i8x16.extract_lane_s 0 ...)
153
- else if (opCode >= 0x15 && opCode <= 0x22) {
154
- immed.push(...uleb(args.shift()))
155
- }
156
- opCode = null // ignore opcode
172
+ out.push(...node, ...blocktype, ...thenelse, 'end')
157
173
  }
174
+ else out.push(node)
175
+ }
176
+ }
158
177
 
159
- // bulk memory: (memory.init) (memory.copy) etc
160
- // https://github.com/WebAssembly/bulk-memory-operations/blob/master/proposals/bulk-memory-operations/Overview.md#instruction-encoding
161
- else if (opCode >= 252) {
162
- immed = [0xfc, ...uleb(opCode -= 252)]
163
- // memory.init idx, memory.drop idx, table.init idx, table.drop idx
164
- if (!(opCode & 0b10)) immed.push(...uleb(args.shift()))
165
- else immed.push(0)
166
- // even opCodes (memory.init, memory.copy, table.init, table.copy) have 2nd predefined immediate
167
- if (!(opCode & 0b1)) immed.push(0)
168
- opCode = null // ignore opcode
169
- }
178
+ return out
179
+ }
170
180
 
171
- // binary/unary (i32.add a b) - no immed
172
- else if (opCode >= 0x45) { }
181
+ const abbr = {
182
+ // block typeuse?
183
+ block: (nodes, ctx) => {
184
+ let out = []
173
185
 
174
- // (i32.store align=n offset=m at value) etc
175
- else if (opCode >= 40 && opCode <= 62) {
176
- // FIXME: figure out point in Math.log2 aligns
177
- let o = consumeParams(args)
178
- immed = [Math.log2(o.align ?? ALIGN[op]), ...uleb(o.offset ?? 0)]
179
- }
186
+ // (loop $l?)
187
+ if (nodes[0]?.[0] === '$') out.push(nodes.shift())
180
188
 
181
- // (i32.const 123), (f32.const 123.45) etc
182
- else if (opCode >= 0x41 && opCode <= 0x44) {
183
- immed = encode[op.split('.')[0]](args.shift())
184
- }
189
+ let [idx, param, result] = typeuse(nodes, ctx, 0)
185
190
 
186
- // (local.get $id), (local.tee $id x)
187
- else if (opCode >= 32 && opCode <= 34) {
188
- immed = uleb(args[0]?.[0] === '$' ? params[id = args.shift()] || locals[id] : args.shift())
189
- }
191
+ // direct idx (no params/result needed)
192
+ if (idx != null) out.push(['type', idx])
193
+ // get type - can be either idx or valtype (numtype | reftype)
194
+ else if (!param.length && !result.length);
195
+ // (result i32) - doesn't require registering type
196
+ else if (!param.length && result.length === 1) out.push(['result', ...result])
197
+ // (param i32 i32)? (result i32 i32) - implicit type
198
+ else ctx._[idx = '$' + param + '>' + result] = [param, result], out.push(['type', idx])
190
199
 
191
- // (global.get id), (global.set id)
192
- else if (opCode == 0x23 || opCode == 36) {
193
- immed = uleb(args[0]?.[0] === '$' ? ctx.global[args.shift()] : args.shift())
194
- }
200
+ return out
201
+ },
202
+ loop: (nodes, ctx) => abbr.block(nodes, ctx),
203
+ if: (nodes, ctx) => abbr.block(nodes, ctx),
204
+
205
+ // select (result i32 i32 i32)?
206
+ select: (nodes, ctx) => [paramres(nodes, 0)[1]],
207
+
208
+ // call_indirect $table? $typeidx
209
+ // return_call_indirect $table? $typeidx
210
+ call_indirect: (nodes, ctx) => {
211
+ let tableidx = nodes[0]?.[0] === '$' || !isNaN(nodes[0]) ? nodes.shift() : 0
212
+ let [idx, param, result] = typeuse(nodes, ctx, 0)
213
+ return [tableidx, ['type', idx ?? (ctx._[idx = '$' + param + '>' + result] = [param, result], idx)]]
214
+ },
215
+ return_call_indirect: (nodes, ctx) => abbr.call_indirect(nodes, ctx),
216
+
217
+ // else $label
218
+ else: (nodes, ctx) => (nodes[0]?.[0] === '$' && nodes.shift(), []),
219
+ // end $label
220
+ end: (nodes, ctx) => (nodes[0]?.[0] === '$' && nodes.shift(), []),
221
+
222
+ // mark datacount section as required
223
+ 'memory.init': (nodes, ctx) => (ctx.datacount[0] = true, []),
224
+ 'data.drop': (nodes, ctx) => (ctx.datacount[0] = true, []),
225
+
226
+ // table.init tableidx? elemidx -> table.init tableidx elemidx
227
+ 'table.init': (nodes, ctx) => [(nodes[1][0] === '$' || !isNaN(nodes[1])) ? nodes.shift() : 0, nodes.shift()],
228
+
229
+ // table.* tableidx?
230
+ 'table.get': (nodes, ctx) => [nodes[0]?.[0] === '$' || !isNaN(nodes[0]) ? nodes.shift() : 0],
231
+ 'table.set': (nodes, ctx) => abbr['table.get'](nodes, ctx),
232
+ 'table.fill': (nodes, ctx) => abbr['table.get'](nodes, ctx),
233
+ 'table.size': (nodes, ctx) => abbr['table.get'](nodes, ctx),
234
+ 'table.grow': (nodes, ctx) => abbr['table.get'](nodes, ctx),
235
+ // table.copy tableidx? tableidx?
236
+ 'table.copy': (nodes, ctx) => [...abbr['table.get'](nodes, ctx), nodes[0][0] === '$' || !isNaN(nodes[0]) ? nodes.shift() : 0],
237
+ }
195
238
 
196
- // (call id ...nodes)
197
- else if (opCode == 16) {
198
- let fnName = args.shift()
199
- immed = uleb(id = fnName[0] === '$' ? ctx.func[fnName] ?? err('Unknown function `' + fnName + '`') : fnName);
200
- // FIXME: how to get signature of imported function
201
- }
239
+ // consume typeuse nodes, return type index/params, or null idx if no type
240
+ // https://webassembly.github.io/spec/core/text/modules.html#type-uses
241
+ const typeuse = (nodes, ctx, names) => {
242
+ let idx, param, result
202
243
 
203
- // (call_indirect (type $typeName) (idx) ...nodes)
204
- else if (opCode == 17) {
205
- let typeId = args.shift()[1];
206
- typeId = typeId[0] === '$' ? ctx.type[typeId] : typeId
207
- immed = uleb(typeId), immed.push(0) // extra immediate indicates table idx (reserved)
208
- }
244
+ // explicit type (type 0|$name)
245
+ if (nodes[0]?.[0] === 'type') {
246
+ [, idx] = nodes.shift();
247
+ [param, result] = paramres(nodes, names);
209
248
 
210
- // FIXME multiple memory (memory.grow $idx?)
211
- else if (opCode == 63 || opCode == 64) {
212
- immed = [0]
213
- }
249
+ // check type consistency (excludes forward refs)
250
+ if ((param.length || result.length) && idx in ctx.type)
251
+ if (ctx.type[id(idx, ctx.type)].join('>') !== param + '>' + result) err(`Type ${idx} mismatch`)
214
252
 
215
- // (block ...), (loop ...), (if ...)
216
- else if (opCode === 2 || opCode === 3 || opCode === 4) {
217
- blocks.push(opCode)
253
+ return [idx]
254
+ }
218
255
 
219
- // (block $x) (loop $y)
220
- if (opCode < 4 && args[0]?.[0] === '$') (blocks[args.shift()] = blocks.length)
256
+ // implicit type (param i32 i32)(result i32)
257
+ [param, result] = paramres(nodes, names)
221
258
 
222
- // get type
223
- // (result i32) - doesn't require registering type
224
- if (args[0]?.[0] === 'result' && args[0].length < 3) {
225
- let [, type] = args.shift()
226
- immed = [TYPE[type]]
227
- }
228
- // (result i32 i32)
229
- else if (args[0]?.[0] === 'result' || args[0]?.[0] === 'param') {
230
- let [typeId] = consumeType(args, ctx)
231
- immed = [typeId]
232
- }
233
- else {
234
- immed = [TYPE.void]
235
- }
259
+ return [, param, result]
260
+ }
236
261
 
237
- if (group) {
238
- // (block xxx) -> block xxx end
239
- nodes.unshift('end')
240
-
241
- if (opCode < 4) while (args.length) nodes.unshift(args.pop())
242
-
243
- // (if cond a) -> cond if a end
244
- else if (args.length < 3) nodes.unshift(args.pop())
245
- // (if cond (then a) (else b)) -> `cond if a else b end`
246
- else {
247
- nodes.unshift(args.pop())
248
- // (if cond a b) -> (if cond a else b)
249
- if (nodes[0][0] !== 'else') nodes.unshift('else')
250
- // (if a b (else)) -> (if a b)
251
- else if (nodes[0].length < 2) nodes.shift()
252
- nodes.unshift(args.pop())
253
- }
254
- }
255
- }
262
+ // consume (param t+)* (result t+)* sequence
263
+ const paramres = (nodes, names = true) => {
264
+ let param = [], result = []
256
265
 
257
- // (else)
258
- else if (opCode === 5) {
259
- // (else xxx) -> else xxx
260
- if (group) while (args.length) nodes.unshift(args.pop())
261
- }
262
- // (then)
263
- else if (opCode === 6) {
264
- opCode = null // ignore opcode
265
- }
266
+ // collect param (param i32 i64) (param $x? i32)
267
+ while (nodes[0]?.[0] === 'param') {
268
+ let [, ...args] = nodes.shift()
269
+ args = args.map(t => t[0] === 'ref' && t[2] ? (HEAPTYPE[t[2]] ? (t[2]+t[0]) : t) : t); // deabbr
270
+ let name = args[0]?.[0] === '$' && args.shift()
271
+ // expose name refs, if allowed
272
+ if (name) names ? param[name] = param.length : err(`Unexpected param name ${name}`)
273
+ param.push(...args)
274
+ }
266
275
 
267
- // (end)
268
- else if (opCode == 0x0b) blocks.pop()
276
+ // collect result eg. (result f64 f32)(result i32)
277
+ while (nodes[0]?.[0] === 'result') {
278
+ let [, ...args] = nodes.shift()
279
+ args = args.map(t => t[0] === 'ref' && t[2] ? (HEAPTYPE[t[2]] ? (t[2]+t[0]) : t) : t); // deabbr
280
+ result.push(...args)
281
+ }
269
282
 
270
- // (br $label result?)
271
- // (br_if $label cond result?)
272
- else if (opCode == 0x0c || opCode == 0x0d) {
273
- // br index indicates how many block items to pop
274
- immed = uleb(args[0]?.[0] === '$' ? blocks.length - blocks[args.shift()] : args.shift())
275
- }
283
+ if (nodes[0]?.[0] === 'param') err(`Unexpected param`)
276
284
 
277
- // (br_table 1 2 3 4 0 selector result?)
278
- else if (opCode == 0x0e) {
279
- immed = []
280
- while (!Array.isArray(args[0])) id = args.shift(), immed.push(...uleb(id[0][0] === '$' ? blocks.length - blocks[id] : id))
281
- immed.unshift(...uleb(immed.length - 1))
282
- }
283
- else if (opCode < 0) err(`Unknown instruction \`${op}\``)
285
+ return [param, result]
286
+ }
284
287
 
285
- // if group (cmd im1 im2 arg1 arg2) - insert any remaining args first: arg1 arg2
286
- // because inline case has them in stack already
287
- if (group) {
288
- while (args.length) consume(args, out)
289
- }
288
+ // build section binary [by section codes] (non consuming)
289
+ const build = [,
290
+ // (type $id? (func params result))
291
+ ([param, result], ctx) => ([0x60, ...vec(param.map(t => type(t, ctx))), ...vec(result.map(t => type(t, ctx)))]),
290
292
 
291
- if (opCode) out.push(opCode)
292
- if (immed) out.push(...immed)
293
- }
293
+ // (import "math" "add" (func|table|global|memory typedef?))
294
+ ([mod, field, [kind, ...dfn]], ctx) => {
295
+ let details
294
296
 
295
- // evaluates after all definitions (need globals, elements, data etc.)
296
- // FIXME: get rid of this postcall
297
- return () => {
298
- const bytes = []
299
- while (body.length) consume(body, bytes)
300
- ctx.code.push([...uleb(bytes.length + 2 + locTypes.length), ...uleb(locTypes.length >> 1), ...locTypes, ...bytes, 0x0b])
297
+ if (kind[0] === 'f') {
298
+ // we track imported funcs in func section to share namespace, and skip them on final build
299
+ let [[, typeidx]] = dfn
300
+ details = uleb(id(typeidx, ctx.type))
301
+ }
302
+ else if (kind[0] === 'm') {
303
+ details = limits(dfn)
304
+ }
305
+ else if (kind[0] === 'g') {
306
+ let [t] = dfn, mut = t[0] === 'mut' ? 1 : 0
307
+ details = [...type(mut ? t[1] : t, ctx), mut]
308
+ }
309
+ else if (kind[0] === 't') {
310
+ details = [...type(dfn.pop(), ctx), ...limits(dfn)]
301
311
  }
302
- },
303
312
 
304
- // (memory min max shared)
305
- // (memory $name min max shared)
306
- // (memory (export "mem") 5)
307
- memory([, ...parts], ctx) {
308
- if (parts[0][0] === '$') ctx.memory[parts.shift()] = ctx.memory.length
309
- if (parts[0][0] === 'export') build.export([...parts.shift(), ['memory', ctx.memory.length]], ctx)
310
- ctx.memory.push(range(parts))
313
+ return ([...vec(str(mod.slice(1, -1))), ...vec(str(field.slice(1, -1))), KIND[kind], ...details])
311
314
  },
312
315
 
313
- // (global i32 (i32.const 42))
314
- // (global $id i32 (i32.const 42))
315
- // (global $id (mut i32) (i32.const 42))
316
- global([, ...args], ctx) {
317
- let name = args[0][0] === '$' && args.shift()
318
- if (name) ctx.global[name] = ctx.global.length
319
- let [type, [...init]] = args, mut = type[0] === 'mut' ? 1 : 0
320
- ctx.global.push([TYPE[mut ? type[1] : type], mut, ...consumeConst(init, ctx), 0x0b])
321
- },
316
+ // (func $name? ...params result ...body)
317
+ ([[, typeidx]], ctx) => (uleb(id(typeidx, ctx.type))),
322
318
 
323
- // (table 1 2? funcref)
324
- // (table $name 1 2? funcref)
325
- table([, ...args], ctx) {
326
- let name = args[0][0] === '$' && args.shift()
327
- if (name) ctx.table[name] = ctx.table.length
328
- let lims = range(args)
329
- ctx.table.push([TYPE[args.pop()], ...lims])
319
+ // (table 1 2 funcref)
320
+ (node, ctx) => {
321
+ let lims = limits(node), t = type(node.shift(), ctx), [init] = node
322
+ return init ? [0x40, 0x00, ...t, ...lims, ...expr(init, ctx)] : [...t, ...lims]
330
323
  },
331
324
 
332
- // (elem (i32.const 0) $f1 $f2), (elem (global.get 0) $f1 $f2)
333
- elem([, [...offset], ...elems], ctx) {
334
- const tableIdx = 0 // FIXME: table index can be defined
335
- ctx.elem.push([tableIdx, ...consumeConst(offset, ctx), 0x0b, ...uleb(elems.length), ...elems.flatMap(el => uleb(el[0] === '$' ? ctx.func[el] : el))])
336
- },
325
+ // (memory id? export* min max shared)
326
+ (node, ctx) => limits(node),
337
327
 
338
- // (export "name" (kind $name|idx))
339
- export([, name, [kind, idx]], ctx) {
340
- if (idx[0] === '$') idx = ctx[kind][idx]
341
- ctx.export.push([...str(name), KIND[kind], ...uleb(idx)])
328
+ // (global $id? (mut i32) (i32.const 42))
329
+ (node, ctx) => {
330
+ let [t] = node, mut = t[0] === 'mut' ? 1 : 0
331
+
332
+ let [, init] = node
333
+ return ([...type(mut ? t[1] : t, ctx), mut, ...expr(init, ctx)])
342
334
  },
343
335
 
344
- // (import "math" "add" (func $add (param i32 i32 externref) (result i32)))
345
- // (import "js" "mem" (memory 1))
346
- // (import "js" "mem" (memory $name 1))
347
- // (import "js" "v" (global $name (mut f64)))
348
- import([, mod, field, ref], ctx) {
349
- let details, [kind, ...parts] = ref,
350
- name = parts[0]?.[0] === '$' && parts.shift();
336
+ // (export "name" (func|table|mem $name|idx))
337
+ ([nm, [kind, l]], ctx) => ([...vec(str(nm.slice(1, -1))), KIND[kind], ...uleb(id(l, ctx[kind]))]),
351
338
 
352
- if (kind === 'func') {
353
- // we track imported funcs in func section to share namespace, and skip them on final build
354
- if (name) ctx.func[name] = ctx.func.length
355
- let [typeIdx] = consumeType(parts, ctx)
356
- ctx.func.push(details = uleb(typeIdx))
357
- ctx.func.importc = (ctx.func.importc || 0) + 1
339
+ // (start $main)
340
+ ([l], ctx) => uleb(id(l, ctx.func)),
341
+
342
+ // (elem elem*) - passive
343
+ // (elem declare elem*) - declarative
344
+ // (elem (table idx)? (offset expr)|(expr) elem*) - active
345
+ // elems := funcref|externref (item expr)|expr (item expr)|expr
346
+ // idxs := func? $id0 $id1
347
+ // ref: https://webassembly.github.io/spec/core/binary/modules.html#element-section
348
+ (parts, ctx) => {
349
+ let tabidx, offset, mode = 0b000, reftype
350
+
351
+ // declare?
352
+ if (parts[0] === 'declare') parts.shift(), mode |= 0b010
353
+
354
+ // table?
355
+ if (parts[0][0] === 'table') {
356
+ [, tabidx] = parts.shift()
357
+ tabidx = id(tabidx, ctx.table)
358
+ // ignore table=0
359
+ if (tabidx) mode |= 0b010
358
360
  }
359
- else if (kind === 'memory') {
360
- if (name) ctx.memory[name] = ctx.memory.length
361
- details = range(parts)
361
+
362
+ // (offset expr)|expr
363
+ if (parts[0]?.[0] === 'offset' || (Array.isArray(parts[0]) && parts[0][0] !== 'item' && !parts[0][0].startsWith('ref'))) {
364
+ offset = parts.shift()
365
+ if (offset[0] === 'offset') [, offset] = offset
362
366
  }
363
- else if (kind === 'global') {
364
- // imported globals share namespace with internal globals - we skip them in final build
365
- if (name) ctx.global[name] = ctx.global.length
366
- let [type] = parts, mut = type[0] === 'mut' ? 1 : 0
367
- details = [TYPE[mut ? type[1] : type], mut]
368
- ctx.global.push(details)
369
- ctx.global.importc = (ctx.global.importc || 0) + 1
367
+ else mode |= 0b001 // passive
368
+
369
+ // func ... === funcref ..., https://webassembly.github.io/function-references/core/text/modules.html#id7
370
+ if (HEAPTYPE[parts[0]]) parts[0] += 'ref'
371
+ // reftype: funcref|externref|(ref ...)
372
+ if (parts[0] === 'funcref' || parts[0] === 'externref' || parts[0]?.[0] === 'ref') reftype = parts.shift()
373
+
374
+ // legacy abbr if func is skipped
375
+ if (!reftype) !tabidx ? reftype = 'funcref' : err(`Undefined reftype`)
376
+
377
+ // externref makes explicit table index
378
+ if (reftype === 'externref' || reftype[0] === 'ref') offset ||= ['i32.const', 0], mode = 0b110
379
+ // reset to simplest mode if no actual elements
380
+ else if (!parts.length) mode &= 0b011
381
+
382
+ // simplify els sequence
383
+ parts = parts.map(el => {
384
+ if (el[0] === 'item') [, ...el] = el
385
+ if (el[0] === 'ref.func') [, el] = el
386
+ // (ref.null func) and other expressions turn expr init mode
387
+ if (typeof el !== 'string') mode |= 0b100
388
+ return el
389
+ })
390
+
391
+ return ([
392
+ mode,
393
+ ...(
394
+ // 0b000 e:expr y*:vec(funcidx) | type=funcref, init ((ref.func y)end)*, active (table=0,offset=e)
395
+ mode === 0b000 ? expr(offset, ctx) :
396
+ // 0b001 et:elkind y*:vec(funcidx) | type=0x00, init ((ref.func y)end)*, passive
397
+ mode === 0b001 ? [0x00] :
398
+ // 0b010 x:tabidx e:expr et:elkind y*:vec(funcidx) | type=0x00, init ((ref.func y)end)*, active (table=x,offset=e)
399
+ mode === 0b010 ? [...uleb(tabidx || 0), ...expr(offset, ctx), 0x00] :
400
+ // 0b011 et:elkind y*:vec(funcidx) | type=0x00, init ((ref.func y)end)*, passive declare
401
+ mode === 0b011 ? [0x00] :
402
+ // 0b100 e:expr el*:vec(expr) | type=funcref, init el*, active (table=0, offset=e)
403
+ mode === 0b100 ? expr(offset, ctx) :
404
+ // 0b101 et:reftype el*:vec(expr) | type=et, init el*, passive
405
+ mode === 0b101 ? type(reftype, ctx) :
406
+ // 0b110 x:tabidx e:expr et:reftype el*:vec(expr) | type=et, init el*, active (table=x, offset=e)
407
+ mode === 0b110 ? [...uleb(tabidx || 0), ...expr(offset, ctx), ...type(reftype, ctx)] :
408
+ // 0b111 et:reftype el*:vec(expr) | type=et, init el*, passive declare
409
+ type(reftype, ctx)
410
+ ),
411
+ ...vec(
412
+ parts.map(mode & 0b100 ?
413
+ // ((ref.func y)end)*
414
+ el => expr(typeof el === 'string' ? ['ref.func', el] : el, ctx) :
415
+ // el*
416
+ el => uleb(id(el, ctx.func))
417
+ )
418
+ )
419
+ ])
420
+ },
421
+
422
+ // (code)
423
+ (body, ctx) => {
424
+ let [typeidx, param] = body.shift()
425
+ if (!param) [param] = ctx.type[id(typeidx, ctx.type)]
426
+
427
+ // provide param/local in ctx
428
+ ctx.local = Object.create(param) // list of local variables - some of them are params
429
+ ctx.block = [] // control instructions / blocks stack
430
+
431
+ // display names for error messages
432
+ ctx.local.name = 'local'
433
+ ctx.block.name = 'block'
434
+
435
+ // collect locals
436
+ while (body[0]?.[0] === 'local') {
437
+ let [, ...types] = body.shift()
438
+ if (types[0]?.[0] === '$') ctx.local[types.shift()] = ctx.local.length
439
+ ctx.local.push(...types)
370
440
  }
371
- else throw Error('Unimplemented ' + kind)
372
441
 
373
- ctx.import.push([...str(mod), ...str(field), KIND[kind], ...details])
442
+ const bytes = []
443
+ while (body.length) bytes.push(...instr(body, ctx))
444
+ bytes.push(0x0b)
445
+
446
+ // squash locals into (n:u32 t:valtype)*, n is number and t is type
447
+ // we skip locals provided by params
448
+ 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), [])
449
+
450
+ // cleanup tmp state
451
+ ctx.local = ctx.block = null
452
+
453
+ // https://webassembly.github.io/spec/core/binary/modules.html#code-section
454
+ return vec([...vec(loctypes.map(([n, t]) => [...uleb(n), ...type(t, ctx)])), ...bytes])
374
455
  },
375
456
 
376
457
  // (data (i32.const 0) "\aa" "\bb"?)
377
- // (data (offset (i32.const 0)) (memory ref) "\aa" "\bb"?)
458
+ // (data (memory ref) (offset (i32.const 0)) "\aa" "\bb"?)
378
459
  // (data (global.get $x) "\aa" "\bb"?)
379
- data([, ...inits], ctx) {
380
- let offset, mem
460
+ (inits, ctx) => {
461
+ let offset, memidx = 0
462
+
463
+ // (memory ref)?
464
+ if (inits[0]?.[0] === 'memory') {
465
+ [, memidx] = inits.shift()
466
+ memidx = id(memidx, ctx.memory)
467
+ }
381
468
 
382
- if (inits[0]?.[0] === 'offset') [, offset] = inits.shift()
383
- if (inits[0]?.[0] === 'memory') [, mem] = inits.shift()
384
- if (inits[0]?.[0] === 'offset') [, offset] = inits.shift()
385
- if (!offset && !mem) offset = inits.shift()
386
- if (!offset) offset = ['i32.const', 0]
469
+ // (offset (i32.const 0)) or (i32.const 0)
470
+ if (typeof inits[0] !== 'string') {
471
+ offset = inits.shift()
472
+ if (offset[0] === 'offset') [, offset] = offset
473
+ offset ?? err('Bad offset', offset)
474
+ }
387
475
 
388
- ctx.data.push([0, ...consumeConst([...offset], ctx), 0x0b, ...str(inits.map(i => i[0] === '"' ? i.slice(1, -1) : i).join(''))])
476
+ return ([
477
+ ...(
478
+ // active: 2, x=memidx, e=expr
479
+ memidx ? [2, ...uleb(memidx), ...expr(offset, ctx)] :
480
+ // active: 0, e=expr
481
+ offset ? [0, ...expr(offset, ctx)] :
482
+ // passive: 1
483
+ [1]
484
+ ),
485
+ ...vec(str(inits.map(i => i.slice(1, -1)).join('')))
486
+ ])
389
487
  },
390
488
 
391
- // (start $main)
392
- start([, name], ctx) {
393
- if (!ctx.start.length) ctx.start.push([name[0] === '$' ? ctx.func[name] : name])
489
+ // datacount
490
+ (nodes, ctx) => uleb(ctx.data.length)
491
+ ]
492
+
493
+ // insert type, either direct or ref type
494
+ const type = (t, ctx) =>
495
+ t[0] === 'ref' ? ([t[1] == 'null' ? TYPE.refnull : TYPE.ref, ...uleb(TYPE[t[t.length - 1]] || id(t[t.length - 1], ctx.type))])
496
+ : [TYPE[t] ?? err(`Unknown type ${t}`)]
497
+
498
+ // consume one instruction from nodes sequence
499
+ const instr = (nodes, ctx) => {
500
+ if (!nodes?.length) return []
501
+
502
+ let out = [], op = nodes.shift(), immed, code
503
+
504
+ // consume group
505
+ if (Array.isArray(op)) {
506
+ immed = instr(op, ctx)
507
+ while (op.length) out.push(...instr(op, ctx))
508
+ out.push(...immed)
509
+ return out
394
510
  }
395
- }
396
511
 
397
- // instantiation time const initializer
398
- const consumeConst = (node, ctx) => {
399
- let op = node.shift(), [type, cmd] = op.split('.')
512
+ [...immed] = isNaN(op[0]) && INSTR[op] || err(`Unknown instruction ${op}`)
513
+ code = immed[0]
514
+
515
+ // v128s: (v128.load x) etc
516
+ // https://github.com/WebAssembly/simd/blob/master/proposals/simd/BinarySIMD.md
517
+ if (code === 0xfd) {
518
+ [, code] = immed
519
+ immed = [0xfd, ...uleb(code)]
520
+ // (v128.load)
521
+ if (code <= 0x0b) {
522
+ const [a, o] = memarg(nodes)
523
+ immed.push(...uleb((a ?? align(op))), ...uleb(o ?? 0))
524
+ }
525
+ // (v128.load_lane offset? align? idx)
526
+ else if (code >= 0x54 && code <= 0x5d) {
527
+ const [a, o] = memarg(nodes)
528
+ immed.push(...uleb((a ?? align(op))), ...uleb(o ?? 0))
529
+ // (v128.load_lane_zero)
530
+ if (code <= 0x5b) immed.push(...uleb(nodes.shift()))
531
+ }
532
+ // (i8x16.shuffle 0 1 ... 15 a b)
533
+ else if (code === 0x0d) {
534
+ // i8, i16, i32 - bypass the encoding
535
+ for (let i = 0; i < 16; i++) immed.push(encode.i32.parse(nodes.shift()))
536
+ }
537
+ // (v128.const i32x4 1 2 3 4)
538
+ else if (code === 0x0c) {
539
+ let [t, n] = nodes.shift().split('x'), stride = t.slice(1) >>> 3 // i16 -> 2, f32 -> 4
540
+ n = +n
541
+ // i8, i16, i32 - bypass the encoding
542
+ if (t[0] === 'i') {
543
+ let arr = n === 16 ? new Uint8Array(16) : n === 8 ? new Uint16Array(8) : n === 4 ? new Uint32Array(4) : new BigInt64Array(2)
544
+ for (let i = 0; i < n; i++) {
545
+ arr[i] = encode[t].parse(nodes.shift())
546
+ }
547
+ immed.push(...(new Uint8Array(arr.buffer)))
548
+ }
549
+ // f32, f64 - encode
550
+ else {
551
+ let arr = new Uint8Array(16)
552
+ for (let i = 0; i < n; i++) arr.set(encode[t](nodes.shift()), i * stride)
553
+ immed.push(...arr)
554
+ }
555
+ }
556
+ // (i8x16.extract_lane_s 0 ...)
557
+ else if (code >= 0x15 && code <= 0x22) {
558
+ immed.push(...uleb(nodes.shift()))
559
+ }
560
+ }
400
561
 
401
- // (global.get idx)
402
- if (type === 'global') return [0x23, ...uleb(node[0][0] === '$' ? ctx.global[node[0]] : node[0])]
562
+ // bulk memory: (memory.init) (memory.copy) (data.drop) (memory.fill)
563
+ // table ops: (table.init|copy|grow|size|fill) (elem.drop)
564
+ // https://github.com/WebAssembly/bulk-memory-operations/blob/master/proposals/bulk-memory-operations/Overview.md#instruction-encoding
565
+ else if (code == 0xfc) {
566
+ [, code] = immed
403
567
 
404
- // (v128.const i32x4 1 2 3 4)
405
- if (type === 'v128') return [0xfd, 0x0c, ...v128(node)]
568
+ // memory.init idx, data.drop idx,
569
+ if (code === 0x08 || code === 0x09) {
570
+ immed.push(...uleb(id(nodes.shift(), ctx.data)))
571
+ }
406
572
 
407
- // (i32.const 1)
408
- if (cmd === 'const') return [0x41 + ['i32', 'i64', 'f32', 'f64'].indexOf(type), ...encode[type](node[0])]
573
+ // memory placeholders
574
+ if (code == 0x08 || code == 0x0b) immed.push(0)
575
+ else if (code === 0x0a) immed.push(0, 0)
409
576
 
410
- // (i32.add a b), (i32.mult a b) etc
411
- return [
412
- ...consumeConst(node.shift(), ctx),
413
- ...consumeConst(node.shift(), ctx),
414
- OP.indexOf(op)
415
- ]
416
- }
577
+ // elem.drop elemidx
578
+ if (code === 0x0d) {
579
+ immed.push(...uleb(id(nodes.shift(), ctx.elem)))
580
+ }
581
+ // table.init tableidx elemidx -> 0xfc 0x0c elemidx tableidx
582
+ else if (code === 0x0c) {
583
+ immed.push(...uleb(id(nodes[1], ctx.elem)), ...uleb(id(nodes.shift(), ctx.table)))
584
+ nodes.shift()
585
+ }
586
+ // table.* tableidx?
587
+ // abbrs https://webassembly.github.io/spec/core/text/instructions.html#id1
588
+ else if (code >= 0x0c && code < 0x13) {
589
+ immed.push(...uleb(id(nodes.shift(), ctx.table)))
590
+ // table.copy tableidx? tableidx?
591
+ if (code === 0x0e) immed.push(...uleb(id(nodes.shift(), ctx.table)))
592
+ }
593
+ }
594
+
595
+ // control block abbrs
596
+ // block ..., loop ..., if ...
597
+ else if (code === 2 || code === 3 || code === 4) {
598
+ ctx.block.push(code)
599
+
600
+ // (block $x) (loop $y)
601
+ if (nodes[0]?.[0] === '$') ctx.block[nodes.shift()] = ctx.block.length
602
+
603
+ let typeidx = nodes[0]?.[0] === 'type' && id(nodes.shift()[1], ctx.type)
604
+
605
+ let [param, result] = typeidx !== false ? ctx.type[typeidx] : nodes[0]?.[0] === 'result' ? [, [nodes.shift()[1]]] : []
606
+
607
+ // void
608
+ if (!param?.length && !result?.length) immed.push(TYPE.void)
609
+ // (result i32) - doesn't require registering type
610
+ else if (!param?.length && result.length === 1) immed.push(...type(result[0], ctx))
611
+ // (type idx)
612
+ else immed.push(...uleb(typeidx))
613
+ }
614
+ // else
615
+ else if (code === 5) { }
616
+ // then
617
+ else if (code === 6) immed = [] // ignore
618
+
619
+ // local.get $id, local.tee $id x
620
+ else if (code == 0x20 || code == 0x21 || code == 0x22) {
621
+ immed.push(...uleb(id(nodes.shift(), ctx.local)))
622
+ }
417
623
 
418
- // (v128.const i32x4 1 2 3 4)
419
- const v128 = (args) => {
420
- let [t, n] = args.shift().split('x'),
421
- stride = t.slice(1) >>> 3 // i16 -> 2, f32 -> 4
624
+ // global.get $id, global.set $id
625
+ else if (code == 0x23 || code == 0x24) {
626
+ immed.push(...uleb(id(nodes.shift(), ctx.global)))
627
+ }
628
+
629
+ // call $func ...nodes
630
+ // return_call $func
631
+ else if (code == 0x10 || code == 0x12) {
632
+ immed.push(...uleb(id(nodes.shift(), ctx.func)))
633
+ }
634
+
635
+ // call_indirect $table (type $typeName) ...nodes
636
+ // return_call_indirect $table (type $typeName) ... nodes
637
+ else if (code == 0x11 || code == 0x13) {
638
+ immed.push(
639
+ ...uleb(id(nodes[1][1], ctx.type)),
640
+ ...uleb(id(nodes.shift(), ctx.table))
641
+ ), nodes.shift()
642
+ }
422
643
 
423
- n = +n
644
+ // call_ref $type
645
+ // return_call_ref $type
646
+ else if (code == 0x14 || code == 0x15) {
647
+ immed.push(...uleb(id(nodes.shift(), ctx.type)))
648
+ }
649
+
650
+ // end
651
+ else if (code == 0x0b) ctx.block.pop()
652
+
653
+ // br $label result?
654
+ // br_if $label cond result?
655
+ // br_on_null $l, br_on_non_null $l
656
+ else if (code == 0x0c || code == 0x0d || code == 0xd5 || code == 0xd6) {
657
+ // br index indicates how many block items to pop
658
+ let l = nodes.shift(), i = l?.[0] === '$' ? ctx.block.length - ctx.block[l] : +l
659
+ i <= ctx.block.length || err(`Bad label ${l}`)
660
+ immed.push(...uleb(i))
661
+ }
424
662
 
425
- // i8, i16, i32 - bypass the encoding
426
- if (t[0] === 'i') {
427
- let arr = n === 16 ? new Uint8Array(16) : n === 8 ? new Uint16Array(8) : n === 4 ? new Uint32Array(4) : new BigInt64Array(2)
428
- for (let i = 0; i < n; i++) {
429
- arr[i] = encode[t].parse(args.shift())
663
+ // br_table 1 2 3 4 0 selector result?
664
+ else if (code == 0x0e) {
665
+ let args = []
666
+ while (nodes[0] && (!isNaN(nodes[0]) || nodes[0][0] === '$')) {
667
+ let l = nodes.shift(), i = l[0][0] === '$' ? ctx.block.length - ctx.block[l] : +l
668
+ i <= ctx.block.length || err(`Bad label ${l}`)
669
+ args.push(...uleb(i))
430
670
  }
431
- return new Uint8Array(arr.buffer)
671
+ args.unshift(...uleb(args.length - 1))
672
+ immed.push(...args)
673
+ }
674
+
675
+ // select (result t+)
676
+ else if (code == 0x1b) {
677
+ let result = nodes.shift()
678
+ // 0x1b -> 0x1c
679
+ if (result.length) immed.push(immed.pop() + 1, ...vec(result.map(t => type(t, ctx))))
432
680
  }
433
681
 
434
- // f32, f64 - encode
435
- let arr = new Uint8Array(16)
436
- for (let i = 0; i < n; i++) {
437
- arr.set(encode[t](args.shift()), i * stride)
682
+ // ref.func $id
683
+ else if (code == 0xd2) {
684
+ immed.push(...uleb(id(nodes.shift(), ctx.func)))
438
685
  }
439
686
 
440
- return arr
687
+ // ref.null func
688
+ else if (code == 0xd0) {
689
+ let t = nodes.shift()
690
+ immed.push(HEAPTYPE[t] || id(t, ctx.type)) // func->funcref, extern->externref
691
+ }
692
+
693
+ // binary/unary (i32.add a b) - no immed
694
+ else if (code >= 0x45) { }
695
+
696
+ // i32.store align=n offset=m
697
+ else if (code >= 0x28 && code <= 0x3e) {
698
+ let [a, o] = memarg(nodes)
699
+ immed.push(...uleb((a ?? align(op))), ...uleb(o ?? 0))
700
+ }
701
+
702
+ // i32.const 123, f32.const 123.45
703
+ else if (code >= 0x41 && code <= 0x44) {
704
+ immed.push(...encode[op.split('.')[0]](nodes.shift()))
705
+ }
706
+
707
+ // memory.grow|size $idx - mandatory 0x00
708
+ // https://webassembly.github.io/spec/core/binary/instructions.html#memory-instructions
709
+ else if (code == 0x3f || code == 0x40) {
710
+ immed.push(0)
711
+ }
712
+
713
+ // table.get $id
714
+ else if (code == 0x25 || code == 0x26) {
715
+ immed.push(...uleb(id(nodes.shift(), ctx.table)))
716
+ }
717
+
718
+ out.push(...immed)
719
+
720
+ return out
721
+ }
722
+
723
+ // instantiation time value initializer (consuming) - we redirect to instr
724
+ const expr = (node, ctx) => [...instr([node], ctx), 0x0b]
725
+
726
+ // consume align/offset params
727
+ const memarg = (args) => {
728
+ let align, offset, k, v
729
+ while (args[0]?.includes('=')) [k, v] = args.shift().split('='), k === 'offset' ? offset = +v : k === 'align' ? align = +v : err(`Unknown param ${k}=${v}`)
730
+
731
+ if (offset < 0 || offset > 0xffffffff) err(`Bad offset ${offset}`)
732
+ if (align <= 0 || align > 0xffffffff) err(`Bad align ${align}`)
733
+ if (align) ((align = Math.log2(align)) % 1) && err(`Bad align ${align}`)
734
+ return [align, offset]
735
+ }
736
+
737
+ // deref id node to idx
738
+ const id = (n, list) => (n = n[0] === '$' ? list[n] : !isNaN(n) && +n, n in list ? n : err(`Unknown ${list.name} ${n}`))
739
+
740
+ // ref:
741
+ // const ALIGN = {
742
+ // 'i32.load': 4, 'i64.load': 8, 'f32.load': 4, 'f64.load': 8,
743
+ // 'i32.load8_s': 1, 'i32.load8_u': 1, 'i32.load16_s': 2, 'i32.load16_u': 2,
744
+ // '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,
745
+ // 'i64.store': 8, 'f32.store': 4, 'f64.store': 8, 'i32.store8': 1, 'i32.store16': 2, 'i64.store8': 1, 'i64.store16': 2, 'i64.store32': 4,
746
+ // '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,
747
+ // '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
748
+ // }
749
+ const align = (op) => {
750
+ let [group, opname] = op.split('.'); // v128.load8x8_u -> group = v128, opname = load8x8_u
751
+ let [lsize] = (opname[0] === 'l' ? opname.slice(4) : opname.slice(5)).split('_') // load8x8_u -> lsize = 8x8
752
+ let [size, x] = lsize ? lsize.split('x') : [group.slice(1)] // 8x8 -> size = 8
753
+ return Math.log2(x ? 8 : +size / 8)
441
754
  }
442
755
 
756
+ // build limits sequence (consuming)
757
+ const limits = (node) => isNaN(parseInt(node[1])) ? [0, ...uleb(node.shift())] : [node[2] === 'shared' ? 3 : 1, ...uleb(node.shift()), ...uleb(node.shift())]
758
+
443
759
  // escape codes
444
- const escape = { n: 10, r: 13, t: 9, v: 1, '\\': 92 }
760
+ const escape = { n: 10, r: 13, t: 9, v: 1, '"': 34, "'": 39, '\\': 92 }
445
761
 
446
762
  // build string binary
447
763
  const str = str => {
448
- str = str[0] === '"' ? str.slice(1, -1) : str
449
764
  let res = [], i = 0, c, BSLASH = 92
450
- // spec https://webassembly.github.io/spec/core/text/values.html#strings
765
+ // https://webassembly.github.io/spec/core/text/values.html#strings
451
766
  for (; i < str.length;) {
452
767
  c = str.charCodeAt(i++)
453
768
  res.push(c === BSLASH ? escape[str[i++]] || parseInt(str.slice(i - 1, ++i), 16) : c)
454
769
  }
455
-
456
- res.unshift(...uleb(res.length))
457
770
  return res
458
771
  }
459
772
 
460
- // build range/limits sequence (non-consuming)
461
- const range = ([min, max, shared]) => isNaN(parseInt(max)) ? [0, ...uleb(min)] : [shared === 'shared' ? 3 : 1, ...uleb(min), ...uleb(max)]
773
+ // serialize binary array
774
+ const vec = a => [...uleb(a.length), ...a.flat()]
462
775
 
463
- // get type info from (params) (result) nodes sequence (consumes nodes)
464
- // returns registered (reused) type idx, params bytes, result bytes
465
- // eg. (type $return_i32 (func (result i32)))
466
- const consumeType = (nodes, ctx) => {
467
- let params = [], result = [], idx, bytes
776
+ const err = text => { throw Error(text) }
468
777
 
469
- // collect params
470
- while (nodes[0]?.[0] === 'param') {
471
- let [, ...types] = nodes.shift()
472
- if (types[0]?.[0] === '$') params[types.shift()] = params.length
473
- params.push(...types.map(t => TYPE[t]))
474
- }
475
-
476
- // collect result type
477
- if (nodes[0]?.[0] === 'result') result = nodes.shift().slice(1).map(t => TYPE[t])
478
-
479
- // reuse existing type or register new one
480
- bytes = [TYPE.func, ...uleb(params.length), ...params, ...uleb(result.length), ...result]
481
- idx = ctx.type.findIndex((t) => t.every((byte, i) => byte === bytes[i]))
482
-
483
- // register new type, if not found
484
- if (idx < 0) idx = ctx.type.push(bytes) - 1
485
-
486
- return [idx, params, result]
487
- }
488
-
489
- // consume align/offset/etc params
490
- const consumeParams = (args) => {
491
- let params = {}, param
492
- while (args[0]?.includes('=')) param = args.shift().split('='), params[param[0]] = Number(param[1])
493
- return params
494
- }
778
+ const clone = items => items.map(item => Array.isArray(item) ? clone(item) : item)