watr 3.1.0 → 3.2.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "watr",
3
- "version": "3.1.0",
3
+ "version": "3.2.1",
4
4
  "description": "Ligth & fast WAT compiler",
5
5
  "main": "watr.js",
6
6
  "exports": {
@@ -42,9 +42,6 @@
42
42
  },
43
43
  "homepage": "https://github.com/dy/watr#readme",
44
44
  "devDependencies": {
45
- "tst": "^7.3.0",
46
- "wabt": "^1.0.28",
47
- "wassemble": "^0.0.2",
48
- "wat-compiler": "^1.0.0"
45
+ "tst": "^8.0.2"
49
46
  }
50
47
  }
package/readme.md CHANGED
@@ -1,9 +1,11 @@
1
- # watr [![test](https://github.com/audio-lab/watr/actions/workflows/test.js.yml/badge.svg)](https://github.com/audio-lab/watr/actions/workflows/test.js.yml) [![npm bundle size](https://img.shields.io/bundlephobia/minzip/watr/latest?color=brightgreen&label=gzip)](https://bundlephobia.com/package/watr) [![npm](https://img.shields.io/npm/v/watr?color=red)](https://npmjs.org/watr)
1
+ # watr [![test](https://github.com/audio-lab/watr/actions/workflows/test.js.yml/badge.svg)](https://github.com/audio-lab/watr/actions/workflows/test.js.yml) [![npm bundle size](https://img.shields.io/bundlephobia/minzip/watr/latest?color=brightgreen&label=gzip)](https://bundlephobia.com/package/watr) [![npm](https://img.shields.io/npm/v/watr?color=white)](https://npmjs.org/watr)
2
2
 
3
- Light & fast WASM compiler. An alternative to [wabt/wat2wasm](https://github.com/AssemblyScript/wabt.js) or [spec/wast]().<br/>
4
- Useful for high-level languages or dynamic (in-browser) compilation.<br>
3
+ Light & fast WAT compiler.<br/>
4
+ Useful for high-level languages or dynamic (in-browser) compilation.<br/>
5
5
  Supports full [spec text syntax](https://webassembly.github.io/spec/core/text/index.html) and [official testsuite](https://github.com/WebAssembly/testsuite).
6
6
 
7
+ **[REPL](https://dy.github.io/watr/docs/repl)**
8
+
7
9
  ## Usage
8
10
 
9
11
  ### Compile
@@ -55,18 +57,19 @@ print(src, {
55
57
  indent: ' ',
56
58
  newline: '\n',
57
59
  })
58
- // (func (export "double")
59
- // (param f64) (result f64)
60
- // (f64.mul
61
- // (local.get 0)
62
- // (f64.const 2)))
60
+ // (func
61
+ // (export "double")
62
+ // (param f64)
63
+ // (result f64)
64
+ // (f64.mul (local.get 0) (f64.const 2))
65
+ // )
63
66
 
64
67
  // minify
65
68
  print(src, {
66
69
  indent: false,
67
70
  newline: false
68
71
  })
69
- // (func (export "double")(param f64)(result f64)(f64.mul (local.get 0)(f64.const 2)))
72
+ // (func(export "double")(param f64)(result f64)(f64.mul(local.get 0)(f64.const 2)))
70
73
  ```
71
74
 
72
75
  <!-- See [REPL](https://audio-lab.github.io/watr/repl.html).-->
@@ -74,24 +77,15 @@ print(src, {
74
77
  ## Status
75
78
 
76
79
  * [x] core
77
- * [x] [mutable globals](https://github.com/WebAssembly/mutable-global)
78
- * [x] [extended const](https://github.com/WebAssembly/extended-const/blob/main/proposals/extended-const/Overview.md)
79
- * [x] [nontrapping float to int](https://github.com/WebAssembly/nontrapping-float-to-int-conversions)
80
- * [x] [sign extension](https://github.com/WebAssembly/sign-extension-ops)
81
- * [x] [multi-value](https://github.com/WebAssembly/spec/blob/master/proposals/multi-value/Overview.md)
82
- * [x] [bulk memory ops](https://github.com/WebAssembly/bulk-memory-operations/blob/master/proposals/bulk-memory-operations/Overview.md)
83
- * [x] [multiple memories](https://github.com/WebAssembly/multi-memory/blob/master/proposals/multi-memory/Overview.md)
84
- * [x] [simd](https://github.com/WebAssembly/simd/blob/master/proposals/simd/SIMD.md)
85
- * [x] [relaxed simd](https://github.com/WebAssembly/relaxed-simd)
86
- * [x] [fixed-width simd](https://github.com/WebAssembly/simd/blob/master/proposals/simd/SIMD.md)
80
+ * [x] [mutable globals](https://github.com/WebAssembly/mutable-global), [extended const](https://github.com/WebAssembly/extended-const/blob/main/proposals/extended-const/Overview.md), [nontrapping float to int](https://github.com/WebAssembly/nontrapping-float-to-int-conversions), [sign extension](https://github.com/WebAssembly/sign-extension-ops)
81
+ * [x] [multi-value](https://github.com/WebAssembly/spec/blob/master/proposals/multi-value/Overview.md), [bulk memory ops](https://github.com/WebAssembly/bulk-memory-operations/blob/master/proposals/bulk-memory-operations/Overview.md), [multiple memories](https://github.com/WebAssembly/multi-memory/blob/master/proposals/multi-memory/Overview.md)
82
+ * [x] [simd](https://github.com/WebAssembly/simd/blob/master/proposals/simd/SIMD.md), [relaxed simd](https://github.com/WebAssembly/relaxed-simd), [fixed-width simd](https://github.com/WebAssembly/simd/blob/master/proposals/simd/SIMD.md)
87
83
  * [x] [tail_call](https://github.com/WebAssembly/tail-call)
88
- * [x] [ref types](https://github.com/WebAssembly/reference-types/blob/master/proposals/reference-types/Overview.md)
89
- * [x] [func refs](https://github.com/WebAssembly/function-references/blob/main/proposals/function-references/Overview.md)
90
- * [ ] [gc](https://github.com/WebAssembly/gc)
84
+ * [x] [ref types](https://github.com/WebAssembly/reference-types/blob/master/proposals/reference-types/Overview.md), [func refs](https://github.com/WebAssembly/function-references/blob/main/proposals/function-references/Overview.md)
85
+ * [x] [gc](https://github.com/WebAssembly/gc)
91
86
  * [ ] [exceptions](https://github.com/WebAssembly/exception-handling)
92
87
  * [ ] [memory64](https://github.com/WebAssembly/memory64)
93
- * [ ] [annotations](https://github.com/WebAssembly/annotations)
94
- * [ ] [code_metadata](https://github.com/WebAssembly/tool-conventions/blob/main/CodeMetadata.md)
88
+ * [ ] [annotations](https://github.com/WebAssembly/annotations), [code_metadata](https://github.com/WebAssembly/tool-conventions/blob/main/CodeMetadata.md)
95
89
  * [ ] [js strings](https://github.com/WebAssembly/js-string-builtins/blob/main/proposals/js-string-builtins/Overview.md)
96
90
 
97
91
  ## Alternatives
package/src/compile.js CHANGED
@@ -1,12 +1,13 @@
1
1
  import * as encode from './encode.js'
2
- import { uleb } from './encode.js'
3
- import { SECTION, TYPE, KIND, INSTR, HEAPTYPE, REFTYPE } from './const.js'
2
+ import { uleb, i32, i64 } from './encode.js'
3
+ import { SECTION, TYPE, KIND, INSTR, HEAPTYPE, DEFTYPE, RECTYPE, REFTYPE } from './const.js'
4
4
  import parse from './parse.js'
5
+ import { clone, err } from './util.js'
5
6
 
6
7
  // build instructions index
7
- INSTR.forEach((op, i) => INSTR[op] = i >= 0x11a ? [0xfd, i - 0x11a] : i >= 0xfc ? [0xfc, i - 0xfc] : [i]);
8
+ INSTR.forEach((op, i) => INSTR[op] = i >= 0x133 ? [0xfd, i - 0x133] : i >= 0x11b ? [0xfc, i - 0x11b] : i >= 0xfb ? [0xfb, i - 0xfb] : [i]);
9
+
8
10
 
9
- // console.log(INSTR)
10
11
  /**
11
12
  * Converts a WebAssembly Text Format (WAT) tree to a WebAssembly binary format (WASM).
12
13
  *
@@ -35,12 +36,40 @@ export default function watr(nodes) {
35
36
  return watr(nodes.map(i => i.slice(1, -1)).join(''))
36
37
  }
37
38
 
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
39
+ // scopes are aliased by key as well, eg. section.func.$name = section[SECTION.func] = idx
40
+ const ctx = []
41
+ for (let kind in SECTION) (ctx[SECTION[kind]] = ctx[kind] = []).name = kind
42
+
43
+ // initialize types
44
+ nodes.filter(([kind, ...node]) => {
45
+ // (rec (type $a (sub final? $sup* (func ...))...) (type $b ...)) -> save subtypes
46
+ if (kind === 'rec') {
47
+ // node contains a list of subtypes, (type ...) or (type (sub final? ...))
48
+ // convert rec type into regular type (first subtype) with stashed subtypes length
49
+ // add rest of subtypes as regular type nodes with subtype flag
50
+ for (let i = 0; i < node.length; i++) {
51
+ let [,...subnode] = node[i]
52
+ alias(subnode, ctx.type);
53
+ (subnode = typedef(subnode, ctx)).push(i ? true : [ctx.type.length, node.length])
54
+ ctx.type.push(subnode)
55
+ }
56
+ }
57
+ // (type (func param* result*))
58
+ // (type (array (mut i8)))
59
+ // (type (struct (field a)*)
60
+ // (type (sub final? $nm* (struct|array|func ...)))
61
+ else if (kind === 'type') {
62
+ alias(node, ctx.type);
63
+ ctx.type.push(typedef(node, ctx));
64
+ }
65
+ // other sections may have id
66
+ else if (kind === 'start' || kind === 'export') ctx[kind].push(node)
67
+
68
+ else return true
69
+ })
42
70
 
43
- for (let [kind, ...node] of nodes) {
71
+ // prepare/normalize nodes
72
+ .forEach(([kind, ...node]) => {
44
73
  let imported // if node needs to be imported
45
74
 
46
75
  // import abbr
@@ -48,12 +77,12 @@ export default function watr(nodes) {
48
77
  if (kind === 'import') [kind, ...node] = (imported = node).pop()
49
78
 
50
79
  // 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
80
+ let items = ctx[kind];
81
+ let name = alias(node, items);
53
82
 
54
83
  // export abbr
55
84
  // (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]])
85
+ while (node[0]?.[0] === 'export') ctx.export.push([node.shift()[1], [kind, items.length]])
57
86
 
58
87
  // for import nodes - redirect output to import
59
88
  if (node[0]?.[0] === 'import') [, ...imported] = node.shift()
@@ -64,7 +93,7 @@ export default function watr(nodes) {
64
93
  if (node[1]?.[0] === 'elem') {
65
94
  let [reftype, [, ...els]] = node
66
95
  node = [els.length, els.length, reftype]
67
- sections.elem.push([['table', name || sections.table.length], ['i32.const', '0'], reftype, ...els])
96
+ ctx.elem.push([['table', name || items.length], ['i32.const', '0'], reftype, ...els])
68
97
  }
69
98
  }
70
99
 
@@ -72,39 +101,33 @@ export default function watr(nodes) {
72
101
  // (memory id? (data str)) -> (memory id? n n) (data (memory id) (i32.const 0) str)
73
102
  else if (kind === 'memory' && node[0]?.[0] === 'data') {
74
103
  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])
104
+ ctx.data.push([['memory', items.length], ['i32.const', 0], ...data])
76
105
  node = [m, m]
77
106
  }
78
107
 
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
84
-
85
108
  // dupe to code section, save implicit type
86
109
  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
110
+ let [idx, param, result] = typeuse(node, ctx);
111
+ idx ??= regtype(param, result, ctx)
112
+
113
+ // we save idx because type can be defined after
114
+ !imported && ctx.code.push([[idx, param, result], ...plain(node, ctx)]) // pass param since they may have names
90
115
  node.unshift(['type', idx])
91
116
  }
92
117
 
93
118
  // import writes to import section amd adds placeholder for (kind) section
94
- if (imported) sections.import.push([...imported, [kind, ...node]]), node = null
119
+ if (imported) ctx.import.push([...imported, [kind, ...node]]), node = null
95
120
 
96
- sections[kind].push(node)
97
- }
98
-
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
121
+ items.push(node)
122
+ })
104
123
 
105
124
  // convert nodes to bytes
106
125
  const bin = (kind, count = true) => {
107
- let items = sections[kind].filter(Boolean).map(item => build[kind](item, sections))
126
+ const items = ctx[kind]
127
+ .filter(Boolean) // filter out (type, imported) placeholders
128
+ .map(item => build[kind](item, ctx))
129
+ .filter(Boolean) // filter out unrenderable things (subtype or data.length)
130
+
108
131
  return !items.length ? [] : [kind, ...vec(count ? vec(items) : items)]
109
132
  }
110
133
 
@@ -128,114 +151,39 @@ export default function watr(nodes) {
128
151
  ])
129
152
  }
130
153
 
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 = []
135
-
136
- while (nodes.length) {
137
- let node = nodes.shift()
138
-
139
- if (typeof node === 'string') {
140
- out.push(node)
141
-
142
- if (abbr[node]) out.push(...abbr[node](nodes, ctx))
143
- }
144
-
145
- // FIXME: try to move this to abbr
146
- else {
147
- node = plain(node, ctx)
148
-
149
- // (block ...) -> block ... end
150
- if (node[0] === 'block' || node[0] === 'loop') {
151
- out.push(...node, 'end')
152
- }
153
-
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())
161
-
162
- // label?
163
- if (node[0]?.[0] === '$') blocktype.push(node.shift())
164
-
165
- // blocktype? - (param) are removed already via plain
166
- if (node[0]?.[0] === 'type' || node[0]?.[0] === 'result') blocktype.push(node.shift());
167
-
168
- // ignore empty else
169
- // https://webassembly.github.io/spec/core/text/instructions.html#abbreviations
170
- if (thenelse[thenelse.length - 1] === 'else') thenelse.pop()
171
-
172
- out.push(...node, ...blocktype, ...thenelse, 'end')
173
- }
174
- else out.push(node)
175
- }
176
- }
177
-
178
- return out
154
+ // consume name eg. $t ...
155
+ const alias = (node, list) => {
156
+ let name = (node[0]?.[0] === '$' || node[0]?.[0] == null) && node.shift();
157
+ if (name) name in list ? err(`Duplicate ${list.name} ${name}`) : list[name] = list.length; // save alias
158
+ return name
179
159
  }
180
160
 
181
- const abbr = {
182
- // block typeuse?
183
- block: (nodes, ctx) => {
184
- let out = []
185
-
186
- // (loop $l?)
187
- if (nodes[0]?.[0] === '$') out.push(nodes.shift())
161
+ // (type $id? (func param* result*))
162
+ // (type $id? (array (mut i8)))
163
+ // (type $id? (struct (field a)*)
164
+ // (type $id? (sub final? $nm* (struct|array|func ...)))
165
+ const typedef = ([dfn], ctx) => {
166
+ let subkind = 'subfinal', supertypes = [], compkind
167
+ if (dfn[0] === 'sub') {
168
+ subkind = dfn.shift(), dfn[0] === 'final' && (subkind += dfn.shift())
169
+ dfn = (supertypes = dfn).pop() // last item is definition
170
+ }
188
171
 
189
- let [idx, param, result] = typeuse(nodes, ctx, 0)
172
+ [compkind, ...dfn] = dfn // composite type kind
190
173
 
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])
174
+ if (compkind === 'func') dfn = paramres(dfn), ctx.type['$' + dfn.join('>')] ??= ctx.type.length
175
+ else if (compkind === 'struct') dfn = fieldseq(dfn, 'field', true)
176
+ else if (compkind === 'array') [dfn] = dfn
199
177
 
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],
178
+ return [compkind, dfn, subkind, supertypes]
237
179
  }
238
180
 
181
+ // register (implicit) type
182
+ const regtype = (param, result, ctx, idx='$' + param + '>' + result) => (
183
+ (ctx.type[idx] ??= ctx.type.push(['func', [param, result]]) - 1),
184
+ idx
185
+ )
186
+
239
187
  // consume typeuse nodes, return type index/params, or null idx if no type
240
188
  // https://webassembly.github.io/spec/core/text/modules.html#type-uses
241
189
  const typeuse = (nodes, ctx, names) => {
@@ -246,69 +194,214 @@ const typeuse = (nodes, ctx, names) => {
246
194
  [, idx] = nodes.shift();
247
195
  [param, result] = paramres(nodes, names);
248
196
 
197
+ const [,srcParamRes] = ctx.type[id(idx, ctx.type)] ?? err(`Unknown type ${idx}`)
198
+
249
199
  // 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`)
200
+ if ((param.length || result.length) && srcParamRes.join('>') !== param + '>' + result) err(`Type ${idx} mismatch`)
252
201
 
253
- return [idx]
202
+ return [idx, ...srcParamRes]
254
203
  }
255
204
 
256
205
  // implicit type (param i32 i32)(result i32)
257
- [param, result] = paramres(nodes, names)
258
-
259
- return [, param, result]
206
+ return [idx, ...paramres(nodes, names)]
260
207
  }
261
208
 
262
209
  // consume (param t+)* (result t+)* sequence
263
210
  const paramres = (nodes, names = true) => {
264
- let param = [], result = []
211
+ // let param = [], result = []
265
212
 
266
213
  // collect param (param i32 i64) (param $x? i32)
267
- while (nodes[0]?.[0] === 'param') {
214
+ let param = fieldseq(nodes, 'param', names)
215
+
216
+ // collect result eg. (result f64 f32)(result i32)
217
+ let result = fieldseq(nodes, 'result')
218
+
219
+ if (nodes[0]?.[0] === 'param') err(`Unexpected param`)
220
+
221
+ return [param, result]
222
+ }
223
+
224
+ // collect sequence of field, eg. (param a) (param b c), (field a) (field b c) or (result a b) (result c)
225
+ // optionally allow or not names
226
+ const fieldseq = (nodes, field, names = false) => {
227
+ let seq = []
228
+ // collect field eg. (field f64 f32)(field i32)
229
+ while (nodes[0]?.[0] === field) {
268
230
  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
231
  let name = args[0]?.[0] === '$' && args.shift()
271
232
  // expose name refs, if allowed
272
- if (name) names ? param[name] = param.length : err(`Unexpected param name ${name}`)
273
- param.push(...args)
233
+ if (name) {
234
+ if (names) name in seq ? err(`Duplicate ${field} ${name}`) : seq[name] = seq.length
235
+ else err(`Unexpected ${field} name ${name}`)
236
+ }
237
+ seq.push(...args)
274
238
  }
239
+ return seq
240
+ }
275
241
 
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
- }
242
+ // consume blocktype - makes sure either type or single result is returned
243
+ const blocktype = (nodes, ctx) => {
244
+ let [idx, param, result] = typeuse(nodes, ctx, 0)
282
245
 
283
- if (nodes[0]?.[0] === 'param') err(`Unexpected param`)
246
+ // get type - can be either idx or valtype (numtype | reftype)
247
+ if (!param.length && !result.length) return
284
248
 
285
- return [param, result]
249
+ // (result i32) - doesn't require registering type
250
+ if (!param.length && result.length === 1) return ['result', ...result]
251
+
252
+ // register implicit type
253
+ idx ??= regtype(param, result, ctx)
254
+
255
+ return ['type', idx]
286
256
  }
287
257
 
258
+ // abbr blocks, loops, ifs; collect implicit types via typeuses; resolve optional immediates
259
+ // https://webassembly.github.io/spec/core/text/instructions.html#folded-instructions
260
+ const plain = (nodes, ctx) => {
261
+ let out = [], stack = [], label
262
+
263
+ while (nodes.length) {
264
+ let node = nodes.shift()
265
+
266
+ // lookup is slower than sequence of known ifs
267
+ if (typeof node === 'string') {
268
+ out.push(node)
269
+
270
+ // block typeuse?
271
+ if (node === 'block' || node === 'if' || node === 'loop') {
272
+ // (loop $l?)
273
+ if (nodes[0]?.[0] === '$') label = nodes.shift(), out.push(label), stack.push(label)
274
+
275
+ out.push(blocktype(nodes, ctx))
276
+ }
277
+
278
+ // else $label
279
+ // end $label - make sure it matches block label
280
+ else if (node === 'else' || node === 'end') {
281
+ if (nodes[0]?.[0] === '$') (node === 'end' ? stack.pop() : label) !== (label = nodes.shift()) && err(`Mismatched label ${label}`)
282
+ }
283
+
284
+ // select (result i32 i32 i32)?
285
+ else if (node === 'select') {
286
+ out.push(paramres(nodes, 0)[1])
287
+ }
288
+
289
+ // call_indirect $table? $typeidx
290
+ // return_call_indirect $table? $typeidx
291
+ else if (node.endsWith('call_indirect')) {
292
+ let tableidx = nodes[0]?.[0] === '$' || !isNaN(nodes[0]) ? nodes.shift() : 0
293
+ let [idx, param, result] = typeuse(nodes, ctx, 0)
294
+ out.push(tableidx, ['type', idx ?? regtype(param, result, ctx)])
295
+ }
296
+
297
+ // mark datacount section as required
298
+ else if (node === 'memory.init' || node === 'data.drop' || node === 'array.new_data' || node === 'array.init_data') {
299
+ ctx.datacount[0] = true
300
+ }
301
+
302
+ // table.init tableidx? elemidx -> table.init tableidx elemidx
303
+ else if (node === 'table.init') out.push((nodes[1][0] === '$' || !isNaN(nodes[1])) ? nodes.shift() : 0, nodes.shift())
304
+
305
+ // table.* tableidx?
306
+ else if (node.startsWith('table.')) {
307
+ out.push(nodes[0]?.[0] === '$' || !isNaN(nodes[0]) ? nodes.shift() : 0)
308
+
309
+ // table.copy tableidx? tableidx?
310
+ if (node === 'table.copy') out.push(nodes[0][0] === '$' || !isNaN(nodes[0]) ? nodes.shift() : 0)
311
+ }
312
+ }
313
+
314
+ else {
315
+ // (block ...) -> block ... end
316
+ if (node[0] === 'block' || node[0] === 'loop') {
317
+ out.push(...plain(node, ctx), 'end')
318
+ }
319
+
320
+ // (if ...) -> if ... end
321
+ else if (node[0] === 'if') {
322
+ let then = [], els = [], immed = [node.shift()]
323
+ // (if label? blocktype? cond*? (then instr*) (else instr*)?) -> cond*? if label? blocktype? instr* else instr*? end
324
+ // https://webassembly.github.io/spec/core/text/instructions.html#control-instructions
325
+ if (node[node.length - 1]?.[0] === 'else') {
326
+ els = plain(node.pop(), ctx)
327
+ // ignore empty else
328
+ // https://webassembly.github.io/spec/core/text/instructions.html#abbreviations
329
+ if (els.length === 1) els.length = 0
330
+ }
331
+ if (node[node.length - 1]?.[0] === 'then') then = plain(node.pop(), ctx)
332
+
333
+ // label?
334
+ if (node[0]?.[0] === '$') immed.push(node.shift())
335
+
336
+ // blocktype?
337
+ immed.push(blocktype(node, ctx))
338
+
339
+ if (typeof node[0] === 'string') err('Unfolded condition')
340
+
341
+ out.push(...plain(node, ctx), ...immed, ...then, ...els, 'end')
342
+ }
343
+ else out.push(plain(node, ctx))
344
+ }
345
+ }
346
+
347
+ return out
348
+ }
349
+
350
+
288
351
  // build section binary [by section codes] (non consuming)
289
352
  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)))]),
353
+ // type kinds
354
+ // (func params result)
355
+ // (array i8)
356
+ // (struct ...fields)
357
+ ([kind, fields, subkind, supertypes, rec], ctx) => {
358
+ if (rec === true) return // ignore rec subtypes cept for 1st one
359
+
360
+ let details
361
+ // (rec (sub ...)*)
362
+ if (rec) {
363
+ kind = 'rec'
364
+ let [from, length] = rec, subtypes = Array.from({ length }, (_, i) => build[SECTION.type](ctx.type[from + i].slice(0, 4), ctx))
365
+ details = vec(subtypes)
366
+ }
367
+ // (sub final? sups* (type...))
368
+ else if (subkind === 'sub' || supertypes?.length) {
369
+ details = [...vec(supertypes.map(n => id(n, ctx.type))), ...build[SECTION.type]([kind, fields], ctx)]
370
+ kind = subkind
371
+ }
372
+
373
+ else if (kind === 'func') {
374
+ details = [...vec(fields[0].map(t => reftype(t, ctx))), ...vec(fields[1].map(t => reftype(t, ctx)))]
375
+ }
376
+ else if (kind === 'array') {
377
+ details = fieldtype(fields, ctx)
378
+ }
379
+ else if (kind === 'struct') {
380
+ details = vec(fields.map(t => fieldtype(t, ctx)))
381
+ }
382
+
383
+ return [DEFTYPE[kind], ...details]
384
+ },
292
385
 
293
- // (import "math" "add" (func|table|global|memory typedef?))
386
+ // (import "math" "add" (func|table|global|memory dfn?))
294
387
  ([mod, field, [kind, ...dfn]], ctx) => {
295
388
  let details
296
389
 
297
- if (kind[0] === 'f') {
390
+ if (kind === 'func') {
298
391
  // we track imported funcs in func section to share namespace, and skip them on final build
299
392
  let [[, typeidx]] = dfn
300
393
  details = uleb(id(typeidx, ctx.type))
301
394
  }
302
- else if (kind[0] === 'm') {
395
+ else if (kind === 'memory') {
303
396
  details = limits(dfn)
304
397
  }
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]
398
+ else if (kind === 'global') {
399
+ details = fieldtype(dfn[0], ctx)
308
400
  }
309
- else if (kind[0] === 't') {
310
- details = [...type(dfn.pop(), ctx), ...limits(dfn)]
401
+ else if (kind === 'table') {
402
+ details = [...reftype(dfn.pop(), ctx), ...limits(dfn)]
311
403
  }
404
+ else err(`Unknown kind ${kind}`)
312
405
 
313
406
  return ([...vec(str(mod.slice(1, -1))), ...vec(str(field.slice(1, -1))), KIND[kind], ...details])
314
407
  },
@@ -318,20 +411,15 @@ const build = [,
318
411
 
319
412
  // (table 1 2 funcref)
320
413
  (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]
414
+ let lims = limits(node), t = reftype(node.shift(), ctx), [init] = node
415
+ return init ? [0x40, 0x00, ...t, ...lims, ...expr(init, ctx)] : [...t, ...lims]
323
416
  },
324
417
 
325
418
  // (memory id? export* min max shared)
326
419
  (node, ctx) => limits(node),
327
420
 
328
421
  // (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)])
334
- },
422
+ ([t, init], ctx) => [...fieldtype(t, ctx), ...expr(init, ctx)],
335
423
 
336
424
  // (export "name" (func|table|mem $name|idx))
337
425
  ([nm, [kind, l]], ctx) => ([...vec(str(nm.slice(1, -1))), KIND[kind], ...uleb(id(l, ctx[kind]))]),
@@ -342,74 +430,77 @@ const build = [,
342
430
  // (elem elem*) - passive
343
431
  // (elem declare elem*) - declarative
344
432
  // (elem (table idx)? (offset expr)|(expr) elem*) - active
345
- // elems := funcref|externref (item expr)|expr (item expr)|expr
346
- // idxs := func? $id0 $id1
347
433
  // ref: https://webassembly.github.io/spec/core/binary/modules.html#element-section
348
434
  (parts, ctx) => {
349
- let tabidx, offset, mode = 0b000, reftype
435
+ let passive = 0, declare = 0, elexpr = 0, nofunc = 0, tabidx, offset, rt
350
436
 
351
437
  // declare?
352
- if (parts[0] === 'declare') parts.shift(), mode |= 0b010
438
+ if (parts[0] === 'declare') parts.shift(), declare = 1
353
439
 
354
440
  // table?
355
441
  if (parts[0][0] === 'table') {
356
442
  [, tabidx] = parts.shift()
357
443
  tabidx = id(tabidx, ctx.table)
358
- // ignore table=0
359
- if (tabidx) mode |= 0b010
360
444
  }
361
445
 
362
446
  // (offset expr)|expr
363
447
  if (parts[0]?.[0] === 'offset' || (Array.isArray(parts[0]) && parts[0][0] !== 'item' && !parts[0][0].startsWith('ref'))) {
364
448
  offset = parts.shift()
365
449
  if (offset[0] === 'offset') [, offset] = offset
450
+ offset = expr(offset, ctx)
366
451
  }
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()
452
+ // no offset = passive
453
+ else if (!declare) passive = 1
373
454
 
374
- // legacy abbr if func is skipped
375
- if (!reftype) !tabidx ? reftype = 'funcref' : err(`Undefined reftype`)
455
+ // funcref|externref|(ref ...)
456
+ if (REFTYPE[parts[0]] || parts[0]?.[0] === 'ref') rt = reftype(parts.shift(), ctx)
457
+ // func ... abbr https://webassembly.github.io/function-references/core/text/modules.html#id7
458
+ else if (parts[0] === 'func') rt = [HEAPTYPE[parts.shift()]]
459
+ // or anything else
460
+ else rt = [HEAPTYPE.func]
376
461
 
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
462
+ // deabbr els sequence, detect expr usage
383
463
  parts = parts.map(el => {
384
464
  if (el[0] === 'item') [, ...el] = el
385
465
  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
466
+ // (ref.null func) and other expressions turn expr els mode
467
+ if (typeof el !== 'string') elexpr = 1
388
468
  return el
389
469
  })
390
470
 
471
+ // reftype other than (ref null? func) forces table index via nofunc flag
472
+ // also it forces elexpr
473
+ if (rt[0] !== REFTYPE.funcref) nofunc = 1, elexpr = 1
474
+
475
+ // mode:
476
+ // bit 0 indicates a passive or declarative segment
477
+ // bit 1 indicates the presence of an explicit table index for an active segment
478
+ // and otherwise distinguishes passive from declarative segments
479
+ // bit 2 indicates the use of element type and element expressions instead of elemkind=0x00 and element indices.
480
+ let mode = (elexpr << 2) | ((passive || declare ? declare : (!!tabidx || nofunc)) << 1) | (passive || declare);
481
+
391
482
  return ([
392
483
  mode,
393
484
  ...(
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) :
485
+ // 0b000 e:expr y*:vec(funcidx) | type=(ref func), init ((ref.func y)end)*, active (table=0,offset=e)
486
+ mode === 0b000 ? offset :
396
487
  // 0b001 et:elkind y*:vec(funcidx) | type=0x00, init ((ref.func y)end)*, passive
397
488
  mode === 0b001 ? [0x00] :
398
489
  // 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] :
490
+ mode === 0b010 ? [...uleb(tabidx || 0), ...offset, 0x00] :
400
491
  // 0b011 et:elkind y*:vec(funcidx) | type=0x00, init ((ref.func y)end)*, passive declare
401
492
  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) :
493
+ // 0b100 e:expr el*:vec(expr) | type=(ref null func), init el*, active (table=0, offset=e)
494
+ mode === 0b100 ? offset :
404
495
  // 0b101 et:reftype el*:vec(expr) | type=et, init el*, passive
405
- mode === 0b101 ? type(reftype, ctx) :
496
+ mode === 0b101 ? rt :
406
497
  // 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)] :
498
+ mode === 0b110 ? [...uleb(tabidx || 0), ...offset, ...rt] :
408
499
  // 0b111 et:reftype el*:vec(expr) | type=et, init el*, passive declare
409
- type(reftype, ctx)
500
+ rt
410
501
  ),
411
502
  ...vec(
412
- parts.map(mode & 0b100 ?
503
+ parts.map(elexpr ?
413
504
  // ((ref.func y)end)*
414
505
  el => expr(typeof el === 'string' ? ['ref.func', el] : el, ctx) :
415
506
  // el*
@@ -422,7 +513,7 @@ const build = [,
422
513
  // (code)
423
514
  (body, ctx) => {
424
515
  let [typeidx, param] = body.shift()
425
- if (!param) [param] = ctx.type[id(typeidx, ctx.type)]
516
+ if (!param) [, [param]] = ctx.type[id(typeidx, ctx.type)]
426
517
 
427
518
  // provide param/local in ctx
428
519
  ctx.local = Object.create(param) // list of local variables - some of them are params
@@ -435,7 +526,11 @@ const build = [,
435
526
  // collect locals
436
527
  while (body[0]?.[0] === 'local') {
437
528
  let [, ...types] = body.shift()
438
- if (types[0]?.[0] === '$') ctx.local[types.shift()] = ctx.local.length
529
+ if (types[0]?.[0] === '$') {
530
+ let name = types.shift()
531
+ if (name in ctx.local) err(`Duplicate local ${name}`)
532
+ else ctx.local[name] = ctx.local.length
533
+ }
439
534
  ctx.local.push(...types)
440
535
  }
441
536
 
@@ -451,7 +546,7 @@ const build = [,
451
546
  ctx.local = ctx.block = null
452
547
 
453
548
  // 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])
549
+ return vec([...vec(loctypes.map(([n, t]) => [...uleb(n), ...reftype(t, ctx)])), ...bytes])
455
550
  },
456
551
 
457
552
  // (data (i32.const 0) "\aa" "\bb"?)
@@ -490,10 +585,20 @@ const build = [,
490
585
  (nodes, ctx) => uleb(ctx.data.length)
491
586
  ]
492
587
 
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}`)]
588
+ // build reftype, either direct absheaptype or wrapped heaptype https://webassembly.github.io/gc/core/binary/types.html#reference-types
589
+ const reftype = (t, ctx) => (
590
+ t[0] === 'ref' ?
591
+ t[1] == 'null' ?
592
+ HEAPTYPE[t[2]] ? [HEAPTYPE[t[2]]] : [REFTYPE.refnull, ...uleb(id(t[t.length - 1], ctx.type))] :
593
+ [TYPE.ref, ...uleb(HEAPTYPE[t[t.length - 1]] || id(t[t.length - 1], ctx.type))] :
594
+ // abbrs
595
+ [TYPE[t] ?? err(`Unknown type ${t}`)]
596
+ );
597
+
598
+ // build type with mutable flag (mut t) or t
599
+ const fieldtype = (t, ctx, mut = t[0] === 'mut' ? 1 : 0) => [...reftype(mut ? t[1] : t, ctx), mut];
600
+
601
+
497
602
 
498
603
  // consume one instruction from nodes sequence
499
604
  const instr = (nodes, ctx) => {
@@ -512,50 +617,41 @@ const instr = (nodes, ctx) => {
512
617
  [...immed] = isNaN(op[0]) && INSTR[op] || err(`Unknown instruction ${op}`)
513
618
  code = immed[0]
514
619
 
515
- // v128s: (v128.load x) etc
516
- // https://github.com/WebAssembly/simd/blob/master/proposals/simd/BinarySIMD.md
517
- if (code === 0xfd) {
620
+ // gc-related
621
+ // https://webassembly.github.io/gc/core/binary/instructions.html#reference-instructions
622
+ if (code === 0x0fb) {
518
623
  [, 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()))
624
+
625
+ // struct.new $t ... array.set $t
626
+ if ((code >= 0 && code <= 14) || (code >= 16 && code <= 19)) {
627
+ let tidx = id(nodes.shift(), ctx.type)
628
+ immed.push(...uleb(tidx))
629
+
630
+ // struct.get|set* $t $f - read field by index from struct definition (ctx.type[structidx][dfnidx])
631
+ if (code >= 2 && code <= 5) immed.push(...uleb(id(nodes.shift(), ctx.type[tidx][1])))
632
+ // array.new_fixed $t n
633
+ else if (code === 8) immed.push(...uleb(nodes.shift()))
634
+ // array.new_data|init_data $t $d
635
+ else if (code === 9 || code === 18) immed.push(...uleb(id(nodes.shift(), ctx.data)))
636
+ // array.new_elem|init_elem $t $e
637
+ else if (code === 10 || code === 19) immed.push(...uleb(id(nodes.shift(), ctx.elem)))
638
+ // array.copy $t $t
639
+ else if (code === 17) immed.push(...uleb(id(nodes.shift(), ctx.type)))
536
640
  }
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
- }
641
+ // ref.test|cast (ref null? $t|heaptype)
642
+ else if (code >= 20 && code <= 23) {
643
+ let ht = reftype(nodes.shift(), ctx)
644
+ if (ht[0] !== REFTYPE.ref) immed.push(code = immed.pop()+1) // ref.test|cast (ref null $t) is next op
645
+ if (ht.length > 1) ht.shift() // pop ref
646
+ immed.push(...ht)
555
647
  }
556
- // (i8x16.extract_lane_s 0 ...)
557
- else if (code >= 0x15 && code <= 0x22) {
558
- immed.push(...uleb(nodes.shift()))
648
+ // br_on_cast[_fail] $l? (ref null? ht1) (ref null? ht2)
649
+ else if (code === 24 || code === 25) {
650
+ let i = blockid(nodes.shift(), ctx.block),
651
+ ht1 = reftype(nodes.shift(), ctx),
652
+ ht2 = reftype(nodes.shift(), ctx),
653
+ castflags = ((ht2[0] !== REFTYPE.ref) << 1) | (ht1[0] !== REFTYPE.ref)
654
+ immed.push(castflags, ...uleb(i), ht1.pop(), ht2.pop()) // we take only abstype or
559
655
  }
560
656
  }
561
657
 
@@ -592,24 +688,76 @@ const instr = (nodes, ctx) => {
592
688
  }
593
689
  }
594
690
 
691
+ // v128s: (v128.load x) etc
692
+ // https://github.com/WebAssembly/simd/blob/master/proposals/simd/BinarySIMD.md
693
+ else if (code === 0xfd) {
694
+ [, code] = immed
695
+ immed = [0xfd, ...uleb(code)]
696
+ // (v128.load offset? align?)
697
+ if (code <= 0x0b) {
698
+ const [a, o] = memarg(nodes)
699
+ immed.push(...uleb((a ?? align(op))), ...uleb(o ?? 0))
700
+ }
701
+ // (v128.load_lane offset? align? idx)
702
+ else if (code >= 0x54 && code <= 0x5d) {
703
+ const [a, o] = memarg(nodes)
704
+ immed.push(...uleb((a ?? align(op))), ...uleb(o ?? 0))
705
+ // (v128.load_lane_zero)
706
+ if (code <= 0x5b) immed.push(...uleb(nodes.shift()))
707
+ }
708
+ // (i8x16.shuffle 0 1 ... 15 a b)
709
+ else if (code === 0x0d) {
710
+ // i8, i16, i32 - bypass the encoding
711
+ for (let i = 0; i < 16; i++) immed.push(parseUint(nodes.shift(), 32))
712
+ }
713
+ // (v128.const i32x4 1 2 3 4)
714
+ else if (code === 0x0c) {
715
+ let [t, n] = nodes.shift().split('x'),
716
+ bits = +t.slice(1),
717
+ stride = bits >>> 3 // i16 -> 2, f32 -> 4
718
+ n = +n
719
+ // i8, i16, i32 - bypass the encoding
720
+ if (t[0] === 'i') {
721
+ let arr = n === 16 ? new Uint8Array(16) : n === 8 ? new Uint16Array(8) : n === 4 ? new Uint32Array(4) : new BigUint64Array(2)
722
+ for (let i = 0; i < n; i++) {
723
+ let s = nodes.shift(), v = encode[t].parse(s)
724
+ arr[i] = v
725
+ }
726
+ immed.push(...(new Uint8Array(arr.buffer)))
727
+ }
728
+ // f32, f64 - encode
729
+ else {
730
+ let arr = new Uint8Array(16)
731
+ for (let i = 0; i < n; i++) {
732
+ let s = nodes.shift(), v = encode[t](s)
733
+ arr.set(v, i * stride)
734
+ }
735
+ immed.push(...arr)
736
+ }
737
+ }
738
+ // (i8x16.extract_lane_s 0 ...)
739
+ else if (code >= 0x15 && code <= 0x22) {
740
+ immed.push(...uleb(parseUint(nodes.shift())))
741
+ }
742
+ }
743
+
595
744
  // control block abbrs
596
745
  // block ..., loop ..., if ...
597
746
  else if (code === 2 || code === 3 || code === 4) {
598
747
  ctx.block.push(code)
599
748
 
600
- // (block $x) (loop $y)
749
+ // (block $x) (loop $y) - save label pointer
601
750
  if (nodes[0]?.[0] === '$') ctx.block[nodes.shift()] = ctx.block.length
602
751
 
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]]] : []
752
+ let t = nodes.shift();
606
753
 
607
754
  // void
608
- if (!param?.length && !result?.length) immed.push(TYPE.void)
755
+ if (!t) immed.push(TYPE.void)
609
756
  // (result i32) - doesn't require registering type
610
- else if (!param?.length && result.length === 1) immed.push(...type(result[0], ctx))
757
+ // FIXME: Make sure it is signed positive integer (leb, not uleb) https://webassembly.github.io/gc/core/binary/instructions.html#control-instructions
758
+ else if (t[0] === 'result') immed.push(...reftype(t[1], ctx))
611
759
  // (type idx)
612
- else immed.push(...uleb(typeidx))
760
+ else immed.push(...uleb(id(t[1], ctx.type)))
613
761
  }
614
762
  // else
615
763
  else if (code === 5) { }
@@ -638,7 +786,8 @@ const instr = (nodes, ctx) => {
638
786
  immed.push(
639
787
  ...uleb(id(nodes[1][1], ctx.type)),
640
788
  ...uleb(id(nodes.shift(), ctx.table))
641
- ), nodes.shift()
789
+ )
790
+ nodes.shift()
642
791
  }
643
792
 
644
793
  // call_ref $type
@@ -654,19 +803,14 @@ const instr = (nodes, ctx) => {
654
803
  // br_if $label cond result?
655
804
  // br_on_null $l, br_on_non_null $l
656
805
  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))
806
+ immed.push(...uleb(blockid(nodes.shift(), ctx.block)))
661
807
  }
662
808
 
663
809
  // br_table 1 2 3 4 0 selector result?
664
810
  else if (code == 0x0e) {
665
811
  let args = []
666
812
  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))
813
+ args.push(...uleb(blockid(nodes.shift(), ctx.block)))
670
814
  }
671
815
  args.unshift(...uleb(args.length - 1))
672
816
  immed.push(...args)
@@ -676,7 +820,7 @@ const instr = (nodes, ctx) => {
676
820
  else if (code == 0x1b) {
677
821
  let result = nodes.shift()
678
822
  // 0x1b -> 0x1c
679
- if (result.length) immed.push(immed.pop() + 1, ...vec(result.map(t => type(t, ctx))))
823
+ if (result.length) immed.push(immed.pop() + 1, ...vec(result.map(t => reftype(t, ctx))))
680
824
  }
681
825
 
682
826
  // ref.func $id
@@ -687,7 +831,7 @@ const instr = (nodes, ctx) => {
687
831
  // ref.null func
688
832
  else if (code == 0xd0) {
689
833
  let t = nodes.shift()
690
- immed.push(HEAPTYPE[t] || id(t, ctx.type)) // func->funcref, extern->externref
834
+ immed.push(...(HEAPTYPE[t] ? [HEAPTYPE[t]] : uleb(id(t, ctx.type)))) // func->funcref, extern->externref
691
835
  }
692
836
 
693
837
  // binary/unary (i32.add a b) - no immed
@@ -710,7 +854,7 @@ const instr = (nodes, ctx) => {
710
854
  immed.push(0)
711
855
  }
712
856
 
713
- // table.get $id
857
+ // table.get|set $id
714
858
  else if (code == 0x25 || code == 0x26) {
715
859
  immed.push(...uleb(id(nodes.shift(), ctx.table)))
716
860
  }
@@ -723,6 +867,16 @@ const instr = (nodes, ctx) => {
723
867
  // instantiation time value initializer (consuming) - we redirect to instr
724
868
  const expr = (node, ctx) => [...instr([node], ctx), 0x0b]
725
869
 
870
+ // deref id node to numeric idx
871
+ const id = (nm, list, n) => (n = nm[0] === '$' ? list[nm] : +nm, n in list ? n : err(`Unknown ${list.name} ${nm}`))
872
+
873
+ // block id - same as id but for block
874
+ // index indicates how many block items to pop
875
+ const blockid = (nm, block, i) => (
876
+ i = nm?.[0] === '$' ? block.length - block[nm] : +nm,
877
+ isNaN(i) || i > block.length ? err(`Bad label ${nm}`) : i
878
+ )
879
+
726
880
  // consume align/offset params
727
881
  const memarg = (args) => {
728
882
  let align, offset, k, v
@@ -734,10 +888,6 @@ const memarg = (args) => {
734
888
  return [align, offset]
735
889
  }
736
890
 
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
891
  // const ALIGN = {
742
892
  // 'i32.load': 4, 'i64.load': 8, 'f32.load': 4, 'f64.load': 8,
743
893
  // 'i32.load8_s': 1, 'i32.load8_u': 1, 'i32.load16_s': 2, 'i32.load16_u': 2,
@@ -754,7 +904,14 @@ const align = (op) => {
754
904
  }
755
905
 
756
906
  // 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())]
907
+ const limits = (node) => (
908
+ isNaN(parseInt(node[1])) ? [0, ...uleb(parseUint(node.shift()))] : [node[2] === 'shared' ? 3 : 1, ...uleb(parseUint(node.shift())), ...uleb(parseUint(node.shift()))]
909
+ )
910
+
911
+ // check if node is valid int in a range
912
+ // we put extra condition for index ints for tests complacency
913
+ 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
914
+
758
915
 
759
916
  // escape codes
760
917
  const escape = { n: 10, r: 13, t: 9, v: 1, '"': 34, "'": 39, '\\': 92 }
@@ -772,7 +929,3 @@ const str = str => {
772
929
 
773
930
  // serialize binary array
774
931
  const vec = a => [...uleb(a.length), ...a.flat()]
775
-
776
- const err = text => { throw Error(text) }
777
-
778
- const clone = items => items.map(item => Array.isArray(item) ? clone(item) : item)
package/src/const.js CHANGED
@@ -1,8 +1,8 @@
1
1
  // https://webassembly.github.io/spec/core/appendix/index-instructions.html
2
2
  export const INSTR = [
3
3
  'unreachable', 'nop', 'block', 'loop', 'if', 'else', 'then', , , , ,
4
- 'end', 'br', 'br_if', 'br_table', 'return', 'call', 'call_indirect', 'return_call', 'return_call_indirect','call_ref' ,'return_call_ref' , , , , ,
5
- 'drop', 'select', 'select2', , , ,
4
+ 'end', 'br', 'br_if', 'br_table', 'return', 'call', 'call_indirect', 'return_call', 'return_call_indirect', 'call_ref', 'return_call_ref', , , , ,
5
+ 'drop', 'select', '', , , ,
6
6
  'local.get', 'local.set', 'local.tee', 'global.get', 'global.set', 'table.get', 'table.set', ,
7
7
  'i32.load', 'i64.load', 'f32.load', 'f64.load',
8
8
  'i32.load8_s', 'i32.load8_u', 'i32.load16_s', 'i32.load16_u',
@@ -26,14 +26,17 @@ export const INSTR = [
26
26
  'f64.convert_i32_s', 'f64.convert_i32_u', 'f64.convert_i64_s', 'f64.convert_i64_u', 'f64.promote_f32',
27
27
  'i32.reinterpret_f32', 'i64.reinterpret_f64', 'f32.reinterpret_i32', 'f64.reinterpret_i64',
28
28
  'i32.extend8_s', 'i32.extend16_s', 'i64.extend8_s', 'i64.extend16_s', 'i64.extend32_s', , , , , , , , , , , ,
29
- 'ref.null', 'ref.is_null', 'ref.func', ,'ref.as_non_null' ,'br_on_null' ,'br_on_non_null' , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , ,
29
+ 'ref.null', 'ref.is_null', 'ref.func', 'ref.eq', 'ref.as_non_null', 'br_on_null', 'br_on_non_null', , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , ,
30
30
 
31
- // 0xFC 0xNN (0xfc shift)
31
+ // 0xFB 0xNN (0xFB shift)
32
+ 'struct.new', 'struct.new_default', 'struct.get', 'struct.get_s', 'struct.get_u', 'struct.set', 'array.new', 'array.new_default', 'array.new_fixed', 'array.new_data', 'array.new_elem', 'array.get', 'array.get_s', 'array.get_u', 'array.set', 'array.len', 'array.fill', 'array.copy', 'array.init_data', 'array.init_elem', 'ref.test', '', 'ref.cast', '', 'br_on_cast', 'br_on_cast_fail', 'any.convert_extern', 'extern.convert_any', 'ref.i31', 'i31.get_s', 'i31.get_u', ,
33
+
34
+ // 0xFC 0xNN (0x11b shift)
32
35
  'i32.trunc_sat_f32_s', 'i32.trunc_sat_f32_u', 'i32.trunc_sat_f64_s', 'i32.trunc_sat_f64_u', 'i64.trunc_sat_f32_s', 'i64.trunc_sat_f32_u', 'i64.trunc_sat_f64_s', 'i64.trunc_sat_f64_u',
33
36
  'memory.init', 'data.drop', 'memory.copy', 'memory.fill', 'table.init', 'elem.drop', 'table.copy', 'table.grow', 'table.size', 'table.fill', ,
34
- 'i64.add128', 'i64.sub128', 'i64.mul_wide_s', 'i64.mul_wide_u', , , , , , , ,
37
+ 'i64.add128', 'i64.sub128', 'i64.mul_wide_s', 'i64.mul_wide_u', ,
35
38
 
36
- // 0xFD 0xNN (0x10f shift)
39
+ // 0xFD 0xNN (0x133 shift)
37
40
  'v128.load', 'v128.load8x8_s', 'v128.load8x8_u', 'v128.load16x4_s', 'v128.load16x4_u', 'v128.load32x2_s', 'v128.load32x2_u', 'v128.load8_splat', 'v128.load16_splat', 'v128.load32_splat', 'v128.load64_splat', 'v128.store', 'v128.const', 'i8x16.shuffle',
38
41
  'i8x16.swizzle', 'i8x16.splat', 'i16x8.splat', 'i32x4.splat', 'i64x2.splat', 'f32x4.splat', 'f64x2.splat',
39
42
  'i8x16.extract_lane_s', 'i8x16.extract_lane_u', 'i8x16.replace_lane', 'i16x8.extract_lane_s', 'i16x8.extract_lane_u', 'i16x8.replace_lane', 'i32x4.extract_lane', 'i32x4.replace_lane', 'i64x2.extract_lane', 'i64x2.replace_lane', 'f32x4.extract_lane', 'f32x4.replace_lane', 'f64x2.extract_lane', 'f64x2.replace_lane',
@@ -47,7 +50,24 @@ export const INSTR = [
47
50
  'i8x16.relaxed_swizzle', 'i32x4.relaxed_trunc_f32x4_s', 'i32x4.relaxed_trunc_f32x4_u', 'i32x4.relaxed_trunc_f64x2_s_zero', 'i32x4.relaxed_trunc_f64x2_u_zero', 'f32x4.relaxed_madd', 'f32x4.relaxed_nmadd', 'f64x2.relaxed_madd', 'f64x2.relaxed_nmadd', 'i8x16.relaxed_laneselect', 'i16x8.relaxed_laneselect', 'i32x4.relaxed_laneselect', 'i64x2.relaxed_laneselect', 'f32x4.relaxed_min', 'f32x4.relaxed_max', 'f64x2.relaxed_min', 'f64x2.relaxed_max', 'i16x8.relaxed_q15mulr_s', 'i16x8.relaxed_dot_i8x16_i7x16_s', 'i32x4.relaxed_dot_i8x16_i7x16_add_s'
48
51
  ],
49
52
  SECTION = { custom: 0, type: 1, import: 2, func: 3, table: 4, memory: 5, global: 6, export: 7, start: 8, elem: 9, datacount: 12, code: 10, data: 11 },
50
- HEAPTYPE = {func: 0x70, extern: 0x6F},
51
- REFTYPE = {funcref: 0x70 /* -0x10 */, externref: 0x6F /* -0x11 */, ref: 0x64 /* -0x1c */, refnull: 0x63 /* -0x1d */},
52
- TYPE = { i32: 0x7f, i64: 0x7e, f32: 0x7d, f64: 0x7c, void: 0x40, v128: 0x7B, ...HEAPTYPE, ...REFTYPE },
53
+ RECTYPE = { sub: 0x50, subfinal: 0x4F, rec: 0x4E },
54
+ DEFTYPE = { func: 0x60, struct: 0x5F, array: 0x5E, ...RECTYPE },
55
+ HEAPTYPE = { nofunc: 0x73, noextern: 0x72, none: 0x71, func: 0x70, extern: 0x6F, any: 0x6E, eq: 0x6D, i31: 0x6C, struct: 0x6B, array: 0x6A },
56
+ REFTYPE = {
57
+ // absheaptype abbrs
58
+ nullfuncref: HEAPTYPE.nofunc,
59
+ nullexternref: HEAPTYPE.noextern,
60
+ nullref: HEAPTYPE.none,
61
+ funcref: HEAPTYPE.func,
62
+ externref: HEAPTYPE.extern,
63
+ anyref: HEAPTYPE.any,
64
+ eqref: HEAPTYPE.eq,
65
+ i31ref: HEAPTYPE.i31,
66
+ structref: HEAPTYPE.struct,
67
+ arrayref: HEAPTYPE.array,
68
+
69
+ // ref, refnull
70
+ ref: 0x64 /* -0x1c */, refnull: 0x63 /* -0x1d */
71
+ },
72
+ TYPE = { i8: 0x78, i16: 0x77, i32: 0x7f, i64: 0x7e, f32: 0x7d, f64: 0x7c, void: 0x40, v128: 0x7B, ...HEAPTYPE, ...REFTYPE },
53
73
  KIND = { func: 0, table: 1, memory: 2, global: 3 }
package/src/encode.js CHANGED
@@ -1,3 +1,5 @@
1
+ import { err, intRE, sepRE } from './util.js'
2
+
1
3
  // encoding ref: https://github.com/j-s-n/WebBS/blob/master/compiler/byteCode.js
2
4
 
3
5
  // uleb
@@ -48,11 +50,13 @@ export function i32(n, buffer = []) {
48
50
  return buffer
49
51
  }
50
52
 
53
+ // for tests complacency we check format
54
+ const cleanInt = (v) => (!sepRE.test(v) && intRE.test(v=v.replaceAll('_',''))) ? v : err(`Bad int ${v}`)
51
55
 
52
56
  // alias
53
57
  export const i8 = i32, i16 = i32
54
58
 
55
- i32.parse = n => parseInt(n.replaceAll('_', ''))
59
+ i32.parse = n => parseInt(cleanInt(n))
56
60
 
57
61
  // bigleb
58
62
  export function i64(n, buffer = []) {
@@ -70,10 +74,10 @@ export function i64(n, buffer = []) {
70
74
  return buffer
71
75
  }
72
76
  i64.parse = n => {
73
- n = n.replaceAll('_', '')
74
- n = n[0] === '-' ? -BigInt(n.slice(1)) : BigInt(n)
77
+ n = cleanInt(n)
78
+ n = n[0] === '-' ? -BigInt(n.slice(1)) : BigInt(n) // can be -0x123
75
79
  byteView.setBigInt64(0, n)
76
- return n = byteView.getBigInt64(0)
80
+ return byteView.getBigInt64(0)
77
81
  }
78
82
 
79
83
  const byteView = new DataView(new Float64Array(1).buffer)
package/src/print.js CHANGED
@@ -1,69 +1,57 @@
1
1
  import parse from './parse.js';
2
2
 
3
- let indent = '', newline = '\n'
4
3
 
5
4
  /**
6
- * Formats a tree or a WAT string.
5
+ * Formats a tree or a WAT (WebAssembly Text) string into a readable format.
7
6
  *
8
- * @param {string | Array} tree - The code to print. If a string is provided, it will be parsed first.
9
- * @param {Object} [options] - Printing options.
10
- * @param {string} [options.indent=' '] - The string used for one level of indentation.
11
- * @param {string} [options.newline='\n'] - The string used for line breaks.
7
+ * @param {string | Array} tree - The code to print. If a string is provided, it will be parsed into a tree structure first.
8
+ * @param {Object} [options={}] - Optional settings for printing.
9
+ * @param {string} [options.indent=' '] - The string used for one level of indentation. Defaults to two spaces.
10
+ * @param {string} [options.newline='\n'] - The string used for line breaks. Defaults to a newline character.
12
11
  * @returns {string} The formatted WAT string.
13
12
  */
14
13
  export default function print(tree, options = {}) {
15
14
  if (typeof tree === 'string') tree = parse(tree);
16
15
 
17
- ({ indent=' ', newline='\n' } = options);
18
- indent ||= '', newline ||= '';
16
+ let { indent=' ', newline='\n' } = options;
17
+ indent ||= '', newline ||= ''; // false -> str
19
18
 
20
19
  return typeof tree[0] === 'string' ? printNode(tree) : tree.map(node => printNode(node)).join(newline)
21
- }
22
20
 
23
- const INLINE = [
24
- 'param',
25
- 'local',
26
- 'drop',
27
- 'f32.const',
28
- 'f64.const',
29
- 'i32.const',
30
- 'i64.const',
31
- 'local.get',
32
- 'global.get',
33
- 'memory.size',
34
- 'result',
35
- 'export',
36
- 'unreachable',
37
- 'nop'
38
- ]
21
+ function printNode(node, level = 0) {
22
+ if (!Array.isArray(node)) return node
39
23
 
40
- function printNode(node, level = 0) {
41
- if (!Array.isArray(node)) return node + ''
24
+ let content = node[0]
42
25
 
43
- let content = node[0]
26
+ // flat node (no deep subnodes), eg. (i32.const 1), (module (export "") 1)
27
+ let flat = !!newline && node.length < 4
28
+ let curIndent = indent.repeat(level + 1)
44
29
 
45
- for (let i = 1; i < node.length; i++) {
46
- // new node doesn't need space separator, eg. [x,[y]] -> `x(y)`
47
- if (Array.isArray(node[i])) {
48
- // inline nodes like (param x)(param y)
49
- // (func (export "xxx")..., but not (func (export "a")(param "b")...
30
+ for (let i = 1; i < node.length; i++) {
31
+ // (<keyword> ...)
32
+ if (Array.isArray(node[i])) {
33
+ // check if it's still flat
34
+ if (flat) flat = node[i].every(subnode => !Array.isArray(subnode))
50
35
 
51
- if (
52
- INLINE.includes(node[i][0]) &&
53
- (!Array.isArray(node[i - 1]) || INLINE.includes(node[i - 1][0]))
54
- ) {
55
- if (!Array.isArray(node[i - 1])) content += ` `
56
- } else {
57
- content += newline
58
- if (node[i]) content += indent.repeat(level + 1)
36
+ // new line
37
+ content += newline + curIndent + printNode(node[i], level + 1)
38
+ }
39
+ // data chunks "\00..."
40
+ else if (node[0] === 'data') {
41
+ flat = false;
42
+ if (newline || content[content.length-1] !== ')') content += newline || ' '
43
+ content += curIndent + node[i]
44
+ }
45
+ // inline nodes
46
+ else {
47
+ if (newline || content[content.length-1] !== ')') content += ' '
48
+ content += node[i]
59
49
  }
60
-
61
- content += printNode(node[i], level + 1)
62
- }
63
- else {
64
- content += ` `
65
- content += node[i]
66
50
  }
51
+
52
+ // shrink unnecessary spaces
53
+ if (flat) return `(${content.replaceAll(newline + curIndent + '(', ' (')})`
54
+
55
+ return `(${content + newline + indent.repeat(level)})`
67
56
  }
68
- return `(${content})`
69
57
  }
package/src/util.js ADDED
@@ -0,0 +1,8 @@
1
+
2
+ export const err = text => { throw Error(text) }
3
+
4
+ export const clone = items => items.map(item => Array.isArray(item) ? clone(item) : item)
5
+
6
+ export const sepRE = /^_|_$|[^\da-f]_|_[^\da-f]/i
7
+
8
+ export const intRE = /^[+-]?(?:0x[\da-f]+|\d+)$/i