watr 3.1.0 → 3.2.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "watr",
3
- "version": "3.1.0",
3
+ "version": "3.2.0",
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,25 +36,39 @@ 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
+ ctx._ = {} // implicit types
43
+
44
+ let subc // current subtype count
42
45
 
43
- for (let [kind, ...node] of nodes) {
46
+ // prepare/normalize nodes
47
+ while (nodes.length) {
48
+ let [kind, ...node] = nodes.shift()
44
49
  let imported // if node needs to be imported
50
+ let rec // number of subtypes under rec type
51
+
52
+ // (rec (type $a (sub final? $sup* (func ...))...) (type $b ...)) -> save subtypes
53
+ if (kind === 'rec') {
54
+ // node contains a list of subtypes, (type ...) or (type (sub final? ...))
55
+ // convert rec type into regular type (first subtype) with stashed subtypes length
56
+ // add rest of subtypes as regular type nodes with subtype flag
57
+ if (node.length > 1) rec = subc = node.length, nodes.unshift(...node), node = nodes.shift(), kind = node.shift()
58
+ else kind = (node = node[0]).shift()
59
+ }
45
60
 
46
61
  // import abbr
47
62
  // (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()
63
+ else if (kind === 'import') [kind, ...node] = (imported = node).pop()
49
64
 
50
65
  // 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
66
+ let items = ctx[kind];
67
+ let name = alias(node, items)
53
68
 
54
69
  // export abbr
55
70
  // (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]])
71
+ while (node[0]?.[0] === 'export') ctx.export.push([node.shift()[1], [kind, items.length]])
57
72
 
58
73
  // for import nodes - redirect output to import
59
74
  if (node[0]?.[0] === 'import') [, ...imported] = node.shift()
@@ -64,7 +79,7 @@ export default function watr(nodes) {
64
79
  if (node[1]?.[0] === 'elem') {
65
80
  let [reftype, [, ...els]] = node
66
81
  node = [els.length, els.length, reftype]
67
- sections.elem.push([['table', name || sections.table.length], ['i32.const', '0'], reftype, ...els])
82
+ ctx.elem.push([['table', name || items.length], ['i32.const', '0'], reftype, ...els])
68
83
  }
69
84
  }
70
85
 
@@ -72,39 +87,64 @@ export default function watr(nodes) {
72
87
  // (memory id? (data str)) -> (memory id? n n) (data (memory id) (i32.const 0) str)
73
88
  else if (kind === 'memory' && node[0]?.[0] === 'data') {
74
89
  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])
90
+ ctx.data.push([['memory', items.length], ['i32.const', 0], ...data])
76
91
  node = [m, m]
77
92
  }
78
93
 
79
94
  // keep start name
80
- else if (kind === 'start') name && node.push(name);
95
+ else if (kind === 'start') name && node.push(name)
96
+
97
+ // normalize type definition to (func|array|struct dfn) form
98
+ // (type (func param* result*))
99
+ // (type (array (mut i8)))
100
+ // (type (struct (field a)*)
101
+ // (type (sub final? $nm* (struct|array|func ...)))
102
+ else if (kind === 'type') {
103
+ let [dfn] = node
104
+ let issub = subc-- > 0
105
+ let subkind = issub && 'subfinal', supertypes = []
106
+ if (dfn[0] === 'sub') {
107
+ subkind = dfn.shift(), dfn[0] === 'final' && (subkind += dfn.shift())
108
+ dfn = (supertypes = dfn).pop() // last item is definition
109
+ }
81
110
 
82
- // [func, [param, result]] -> [param, result], alias
83
- else if (kind === 'type') node[0].shift(), node = paramres(node[0]), sections.type['$' + node.join('>')] ??= idx
111
+ let ckind = dfn.shift() // composite type kind
112
+ if (ckind === 'func') dfn = paramres(dfn), ctx.type['$' + dfn.join('>')] ??= ctx.type.length
113
+ else if (ckind === 'struct') dfn = fieldseq(dfn, 'field', true)
114
+ else if (ckind === 'array') dfn = dfn.shift()
115
+
116
+ node = [ckind, dfn, subkind, supertypes, rec ? [ctx.type.length, rec] : issub]
117
+ }
84
118
 
85
119
  // dupe to code section, save implicit type
86
120
  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
121
+ let [idx, param, result] = typeuse(node, ctx);
122
+ idx ?? (ctx._[idx = '$' + param + '>' + result] = [param, result]);
123
+ // we save idx because type can be defined after
124
+ !imported && nodes.push(['code', [idx, param, result], ...plain(node, ctx)]) // pass param since they may have names
90
125
  node.unshift(['type', idx])
91
126
  }
92
127
 
93
128
  // import writes to import section amd adds placeholder for (kind) section
94
- if (imported) sections.import.push([...imported, [kind, ...node]]), node = null
129
+ if (imported) ctx.import.push([...imported, [kind, ...node]]), node = null
95
130
 
96
- sections[kind].push(node)
131
+ items.push(node)
97
132
  }
98
133
 
99
134
  // 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
135
+ for (let n in ctx._) ctx.type[n] ??= (ctx.type.push(['func', ctx._[n]]) - 1)
101
136
 
102
137
  // patch datacount if data === 0
103
- if (!sections.data.length) sections.datacount.length = 0
138
+ // FIXME: let's try to return empty in datacount builder, since we filter after builder as well
139
+ // if (!ctx.data.length) ctx.datacount.length = 0
104
140
 
105
141
  // convert nodes to bytes
106
142
  const bin = (kind, count = true) => {
107
- let items = sections[kind].filter(Boolean).map(item => build[kind](item, sections))
143
+ const items = ctx[kind]
144
+ .filter(Boolean) // filter out (type, imported) placeholders
145
+ .map(item => build[kind](item, ctx))
146
+ .filter(Boolean) // filter out unrenderable things (subtype or data.length)
147
+
108
148
  return !items.length ? [] : [kind, ...vec(count ? vec(items) : items)]
109
149
  }
110
150
 
@@ -128,114 +168,105 @@ export default function watr(nodes) {
128
168
  ])
129
169
  }
130
170
 
171
+ // consume name eg. $t ...
172
+ const alias = (node, list) => {
173
+ let name = (node[0]?.[0] === '$' || node[0]?.[0] == null) && node.shift();
174
+ if (name) name in list ? err(`Duplicate ${list.name} ${name}`) : list[name] = list.length; // save alias
175
+ return name
176
+ }
177
+
131
178
  // abbr blocks, loops, ifs; collect implicit types via typeuses; resolve optional immediates
132
179
  // https://webassembly.github.io/spec/core/text/instructions.html#folded-instructions
133
180
  const plain = (nodes, ctx) => {
134
- let out = []
181
+ let out = [], stack = [], label
135
182
 
136
183
  while (nodes.length) {
137
184
  let node = nodes.shift()
138
185
 
186
+ // lookup is slower than sequence of known ifs
139
187
  if (typeof node === 'string') {
140
188
  out.push(node)
141
189
 
142
- if (abbr[node]) out.push(...abbr[node](nodes, ctx))
190
+ // block typeuse?
191
+ if (node === 'block' || node === 'if' || node === 'loop') {
192
+ // (loop $l?)
193
+ if (nodes[0]?.[0] === '$') label = nodes.shift(), out.push(label), stack.push(label)
194
+
195
+ out.push(blocktype(nodes, ctx))
196
+ }
197
+
198
+ // else $label
199
+ // end $label - make sure it matches block label
200
+ else if (node === 'else' || node === 'end') {
201
+ if (nodes[0]?.[0] === '$') (node === 'end' ? stack.pop() : label) !== (label = nodes.shift()) && err(`Mismatched label ${label}`)
202
+ }
203
+
204
+ // select (result i32 i32 i32)?
205
+ else if (node === 'select') {
206
+ out.push(paramres(nodes, 0)[1])
207
+ }
208
+
209
+ // call_indirect $table? $typeidx
210
+ // return_call_indirect $table? $typeidx
211
+ else if (node.endsWith('call_indirect')) {
212
+ let tableidx = nodes[0]?.[0] === '$' || !isNaN(nodes[0]) ? nodes.shift() : 0
213
+ let [idx, param, result] = typeuse(nodes, ctx, 0)
214
+ out.push(tableidx, ['type', idx ?? (ctx._[idx = '$' + param + '>' + result] = [param, result], idx)])
215
+ }
216
+
217
+ // mark datacount section as required
218
+ else if (node === 'memory.init' || node === 'data.drop' || node === 'array.new_data' || node === 'array.init_data') {
219
+ ctx.datacount[0] = true
220
+ }
221
+
222
+ // table.init tableidx? elemidx -> table.init tableidx elemidx
223
+ else if (node === 'table.init') out.push((nodes[1][0] === '$' || !isNaN(nodes[1])) ? nodes.shift() : 0, nodes.shift())
224
+
225
+ // table.* tableidx?
226
+ else if (node.startsWith('table.')) {
227
+ out.push(nodes[0]?.[0] === '$' || !isNaN(nodes[0]) ? nodes.shift() : 0)
228
+
229
+ // table.copy tableidx? tableidx?
230
+ if (node === 'table.copy') out.push(nodes[0][0] === '$' || !isNaN(nodes[0]) ? nodes.shift() : 0)
231
+ }
143
232
  }
144
233
 
145
- // FIXME: try to move this to abbr
146
234
  else {
147
- node = plain(node, ctx)
148
-
149
235
  // (block ...) -> block ... end
150
236
  if (node[0] === 'block' || node[0] === 'loop') {
151
- out.push(...node, 'end')
237
+ out.push(...plain(node, ctx), 'end')
152
238
  }
153
239
 
154
240
  // (if ...) -> if ... end
155
241
  else if (node[0] === 'if') {
156
- let thenelse = [], blocktype = [node.shift()]
242
+ let then = [], els = [], immed = [node.shift()]
157
243
  // (if label? blocktype? cond*? (then instr*) (else instr*)?) -> cond*? if label? blocktype? instr* else instr*? end
158
244
  // 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())
245
+ if (node[node.length - 1]?.[0] === 'else') {
246
+ els = plain(node.pop(), ctx)
247
+ // ignore empty else
248
+ // https://webassembly.github.io/spec/core/text/instructions.html#abbreviations
249
+ if (els.length === 1) els.length = 0
250
+ }
251
+ if (node[node.length - 1]?.[0] === 'then') then = plain(node.pop(), ctx)
161
252
 
162
253
  // label?
163
- if (node[0]?.[0] === '$') blocktype.push(node.shift())
254
+ if (node[0]?.[0] === '$') immed.push(node.shift())
164
255
 
165
- // blocktype? - (param) are removed already via plain
166
- if (node[0]?.[0] === 'type' || node[0]?.[0] === 'result') blocktype.push(node.shift());
256
+ // blocktype?
257
+ immed.push(blocktype(node, ctx))
167
258
 
168
- // ignore empty else
169
- // https://webassembly.github.io/spec/core/text/instructions.html#abbreviations
170
- if (thenelse[thenelse.length - 1] === 'else') thenelse.pop()
259
+ if (typeof node[0] === 'string') err('Unfolded condition')
171
260
 
172
- out.push(...node, ...blocktype, ...thenelse, 'end')
261
+ out.push(...plain(node, ctx), ...immed, ...then, ...els, 'end')
173
262
  }
174
- else out.push(node)
263
+ else out.push(plain(node, ctx))
175
264
  }
176
265
  }
177
266
 
178
267
  return out
179
268
  }
180
269
 
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())
188
-
189
- let [idx, param, result] = typeuse(nodes, ctx, 0)
190
-
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])
199
-
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
- }
238
-
239
270
  // consume typeuse nodes, return type index/params, or null idx if no type
240
271
  // https://webassembly.github.io/spec/core/text/modules.html#type-uses
241
272
  const typeuse = (nodes, ctx, names) => {
@@ -248,7 +279,7 @@ const typeuse = (nodes, ctx, names) => {
248
279
 
249
280
  // check type consistency (excludes forward refs)
250
281
  if ((param.length || result.length) && idx in ctx.type)
251
- if (ctx.type[id(idx, ctx.type)].join('>') !== param + '>' + result) err(`Type ${idx} mismatch`)
282
+ if (ctx.type[id(idx, ctx.type)][1].join('>') !== param + '>' + result) err(`Type ${idx} mismatch`)
252
283
 
253
284
  return [idx]
254
285
  }
@@ -261,54 +292,111 @@ const typeuse = (nodes, ctx, names) => {
261
292
 
262
293
  // consume (param t+)* (result t+)* sequence
263
294
  const paramres = (nodes, names = true) => {
264
- let param = [], result = []
295
+ // let param = [], result = []
265
296
 
266
297
  // collect param (param i32 i64) (param $x? i32)
267
- while (nodes[0]?.[0] === 'param') {
298
+ let param = fieldseq(nodes, 'param', names)
299
+
300
+ // collect result eg. (result f64 f32)(result i32)
301
+ let result = fieldseq(nodes, 'result')
302
+
303
+ if (nodes[0]?.[0] === 'param') err(`Unexpected param`)
304
+
305
+ return [param, result]
306
+ }
307
+
308
+ // collect sequence of field, eg. (param a) (param b c), (field a) (field b c) or (result a b) (result c)
309
+ // optionally allow or not names
310
+ const fieldseq = (nodes, field, names = false) => {
311
+ let seq = []
312
+ // collect field eg. (field f64 f32)(field i32)
313
+ while (nodes[0]?.[0] === field) {
268
314
  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
315
  let name = args[0]?.[0] === '$' && args.shift()
271
316
  // expose name refs, if allowed
272
- if (name) names ? param[name] = param.length : err(`Unexpected param name ${name}`)
273
- param.push(...args)
317
+ if (name) {
318
+ if (names) name in seq ? err(`Duplicate ${field} ${name}`) : seq[name] = seq.length
319
+ else err(`Unexpected ${field} name ${name}`)
320
+ }
321
+ seq.push(...args)
274
322
  }
323
+ return seq
324
+ }
275
325
 
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
- }
326
+ // consume blocktype - makes sure either type or single result is returned
327
+ const blocktype = (nodes, ctx) => {
328
+ let [idx, param, result] = typeuse(nodes, ctx, 0)
282
329
 
283
- if (nodes[0]?.[0] === 'param') err(`Unexpected param`)
330
+ // direct idx (no params/result needed)
331
+ if (idx != null) return ['type', idx]
284
332
 
285
- return [param, result]
333
+ // get type - can be either idx or valtype (numtype | reftype)
334
+ if (!param.length && !result.length) return
335
+
336
+ // (result i32) - doesn't require registering type
337
+ if (!param.length && result.length === 1) return ['result', ...result]
338
+
339
+ // (param i32 i32)? (result i32 i32) - implicit type
340
+ ctx._[idx = '$' + param + '>' + result] = [param, result]
341
+ return ['type', idx]
286
342
  }
287
343
 
344
+
288
345
  // build section binary [by section codes] (non consuming)
289
346
  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)))]),
347
+ // type kinds
348
+ // (func params result)
349
+ // (array i8)
350
+ // (struct ...fields)
351
+ ([kind, fields, subkind, supertypes, rec], ctx) => {
352
+ if (rec === true) return // ignore rec subtypes cept for 1st one
353
+
354
+ let details
355
+ // (rec (sub ...)*)
356
+ if (rec) {
357
+ // FIXME: rec of one type
358
+ kind = 'rec'
359
+ let [from, length] = rec, subtypes = Array.from({ length }, (_, i) => build[SECTION.type](ctx.type[from + i].slice(0, 4), ctx))
360
+ details = vec(subtypes)
361
+ }
362
+ // (sub final? sups* (type...))
363
+ else if (subkind === 'sub' || supertypes?.length) {
364
+ details = [...vec(supertypes.map(n => id(n, ctx.type))), ...build[SECTION.type]([kind, fields], ctx)]
365
+ kind = subkind
366
+ }
367
+
368
+ else if (kind === 'func') {
369
+ details = [...vec(fields[0].map(t => reftype(t, ctx))), ...vec(fields[1].map(t => reftype(t, ctx)))]
370
+ }
371
+ else if (kind === 'array') {
372
+ details = fieldtype(fields, ctx)
373
+ }
374
+ else if (kind === 'struct') {
375
+ details = vec(fields.map(t => fieldtype(t, ctx)))
376
+ }
377
+
378
+ return [DEFTYPE[kind], ...details]
379
+ },
292
380
 
293
381
  // (import "math" "add" (func|table|global|memory typedef?))
294
382
  ([mod, field, [kind, ...dfn]], ctx) => {
295
383
  let details
296
384
 
297
- if (kind[0] === 'f') {
385
+ if (kind === 'func') {
298
386
  // we track imported funcs in func section to share namespace, and skip them on final build
299
387
  let [[, typeidx]] = dfn
300
388
  details = uleb(id(typeidx, ctx.type))
301
389
  }
302
- else if (kind[0] === 'm') {
390
+ else if (kind === 'memory') {
303
391
  details = limits(dfn)
304
392
  }
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]
393
+ else if (kind === 'global') {
394
+ details = fieldtype(dfn[0], ctx)
308
395
  }
309
- else if (kind[0] === 't') {
310
- details = [...type(dfn.pop(), ctx), ...limits(dfn)]
396
+ else if (kind === 'table') {
397
+ details = [...reftype(dfn.pop(), ctx), ...limits(dfn)]
311
398
  }
399
+ else err(`Unknown kind ${kind}`)
312
400
 
313
401
  return ([...vec(str(mod.slice(1, -1))), ...vec(str(field.slice(1, -1))), KIND[kind], ...details])
314
402
  },
@@ -318,20 +406,15 @@ const build = [,
318
406
 
319
407
  // (table 1 2 funcref)
320
408
  (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]
409
+ let lims = limits(node), t = reftype(node.shift(), ctx), [init] = node
410
+ return init ? [0x40, 0x00, ...t, ...lims, ...expr(init, ctx)] : [...t, ...lims]
323
411
  },
324
412
 
325
413
  // (memory id? export* min max shared)
326
414
  (node, ctx) => limits(node),
327
415
 
328
416
  // (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
- },
417
+ ([t, init], ctx) => [...fieldtype(t, ctx), ...expr(init, ctx)],
335
418
 
336
419
  // (export "name" (func|table|mem $name|idx))
337
420
  ([nm, [kind, l]], ctx) => ([...vec(str(nm.slice(1, -1))), KIND[kind], ...uleb(id(l, ctx[kind]))]),
@@ -342,74 +425,77 @@ const build = [,
342
425
  // (elem elem*) - passive
343
426
  // (elem declare elem*) - declarative
344
427
  // (elem (table idx)? (offset expr)|(expr) elem*) - active
345
- // elems := funcref|externref (item expr)|expr (item expr)|expr
346
- // idxs := func? $id0 $id1
347
428
  // ref: https://webassembly.github.io/spec/core/binary/modules.html#element-section
348
429
  (parts, ctx) => {
349
- let tabidx, offset, mode = 0b000, reftype
430
+ let passive = 0, declare = 0, elexpr = 0, nofunc = 0, tabidx, offset, rt
350
431
 
351
432
  // declare?
352
- if (parts[0] === 'declare') parts.shift(), mode |= 0b010
433
+ if (parts[0] === 'declare') parts.shift(), declare = 1
353
434
 
354
435
  // table?
355
436
  if (parts[0][0] === 'table') {
356
437
  [, tabidx] = parts.shift()
357
438
  tabidx = id(tabidx, ctx.table)
358
- // ignore table=0
359
- if (tabidx) mode |= 0b010
360
439
  }
361
440
 
362
441
  // (offset expr)|expr
363
442
  if (parts[0]?.[0] === 'offset' || (Array.isArray(parts[0]) && parts[0][0] !== 'item' && !parts[0][0].startsWith('ref'))) {
364
443
  offset = parts.shift()
365
444
  if (offset[0] === 'offset') [, offset] = offset
445
+ offset = expr(offset, ctx)
366
446
  }
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()
447
+ // no offset = passive
448
+ else if (!declare) passive = 1
373
449
 
374
- // legacy abbr if func is skipped
375
- if (!reftype) !tabidx ? reftype = 'funcref' : err(`Undefined reftype`)
450
+ // funcref|externref|(ref ...)
451
+ if (REFTYPE[parts[0]] || parts[0]?.[0] === 'ref') rt = reftype(parts.shift(), ctx)
452
+ // func ... abbr https://webassembly.github.io/function-references/core/text/modules.html#id7
453
+ else if (parts[0] === 'func') rt = [HEAPTYPE[parts.shift()]]
454
+ // or anything else
455
+ else rt = [HEAPTYPE.func]
376
456
 
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
457
+ // deabbr els sequence, detect expr usage
383
458
  parts = parts.map(el => {
384
459
  if (el[0] === 'item') [, ...el] = el
385
460
  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
461
+ // (ref.null func) and other expressions turn expr els mode
462
+ if (typeof el !== 'string') elexpr = 1
388
463
  return el
389
464
  })
390
465
 
466
+ // reftype other than (ref null? func) forces table index via nofunc flag
467
+ // also it forces elexpr
468
+ if (rt[0] !== REFTYPE.funcref) nofunc = 1, elexpr = 1
469
+
470
+ // mode:
471
+ // bit 0 indicates a passive or declarative segment
472
+ // bit 1 indicates the presence of an explicit table index for an active segment
473
+ // and otherwise distinguishes passive from declarative segments
474
+ // bit 2 indicates the use of element type and element expressions instead of elemkind=0x00 and element indices.
475
+ let mode = (elexpr << 2) | ((passive || declare ? declare : (!!tabidx || nofunc)) << 1) | (passive || declare);
476
+
391
477
  return ([
392
478
  mode,
393
479
  ...(
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) :
480
+ // 0b000 e:expr y*:vec(funcidx) | type=(ref func), init ((ref.func y)end)*, active (table=0,offset=e)
481
+ mode === 0b000 ? offset :
396
482
  // 0b001 et:elkind y*:vec(funcidx) | type=0x00, init ((ref.func y)end)*, passive
397
483
  mode === 0b001 ? [0x00] :
398
484
  // 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] :
485
+ mode === 0b010 ? [...uleb(tabidx || 0), ...offset, 0x00] :
400
486
  // 0b011 et:elkind y*:vec(funcidx) | type=0x00, init ((ref.func y)end)*, passive declare
401
487
  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) :
488
+ // 0b100 e:expr el*:vec(expr) | type=(ref null func), init el*, active (table=0, offset=e)
489
+ mode === 0b100 ? offset :
404
490
  // 0b101 et:reftype el*:vec(expr) | type=et, init el*, passive
405
- mode === 0b101 ? type(reftype, ctx) :
491
+ mode === 0b101 ? rt :
406
492
  // 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)] :
493
+ mode === 0b110 ? [...uleb(tabidx || 0), ...offset, ...rt] :
408
494
  // 0b111 et:reftype el*:vec(expr) | type=et, init el*, passive declare
409
- type(reftype, ctx)
495
+ rt
410
496
  ),
411
497
  ...vec(
412
- parts.map(mode & 0b100 ?
498
+ parts.map(elexpr ?
413
499
  // ((ref.func y)end)*
414
500
  el => expr(typeof el === 'string' ? ['ref.func', el] : el, ctx) :
415
501
  // el*
@@ -422,7 +508,7 @@ const build = [,
422
508
  // (code)
423
509
  (body, ctx) => {
424
510
  let [typeidx, param] = body.shift()
425
- if (!param) [param] = ctx.type[id(typeidx, ctx.type)]
511
+ if (!param) [, [param]] = ctx.type[id(typeidx, ctx.type)]
426
512
 
427
513
  // provide param/local in ctx
428
514
  ctx.local = Object.create(param) // list of local variables - some of them are params
@@ -435,7 +521,11 @@ const build = [,
435
521
  // collect locals
436
522
  while (body[0]?.[0] === 'local') {
437
523
  let [, ...types] = body.shift()
438
- if (types[0]?.[0] === '$') ctx.local[types.shift()] = ctx.local.length
524
+ if (types[0]?.[0] === '$') {
525
+ let name = types.shift()
526
+ if (name in ctx.local) err(`Duplicate local ${name}`)
527
+ else ctx.local[name] = ctx.local.length
528
+ }
439
529
  ctx.local.push(...types)
440
530
  }
441
531
 
@@ -451,7 +541,7 @@ const build = [,
451
541
  ctx.local = ctx.block = null
452
542
 
453
543
  // 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])
544
+ return vec([...vec(loctypes.map(([n, t]) => [...uleb(n), ...reftype(t, ctx)])), ...bytes])
455
545
  },
456
546
 
457
547
  // (data (i32.const 0) "\aa" "\bb"?)
@@ -490,10 +580,20 @@ const build = [,
490
580
  (nodes, ctx) => uleb(ctx.data.length)
491
581
  ]
492
582
 
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}`)]
583
+ // build reftype, either direct absheaptype or wrapped heaptype https://webassembly.github.io/gc/core/binary/types.html#reference-types
584
+ const reftype = (t, ctx) => (
585
+ t[0] === 'ref' ?
586
+ t[1] == 'null' ?
587
+ HEAPTYPE[t[2]] ? [HEAPTYPE[t[2]]] : [REFTYPE.refnull, ...uleb(id(t[t.length - 1], ctx.type))] :
588
+ [TYPE.ref, ...uleb(HEAPTYPE[t[t.length - 1]] || id(t[t.length - 1], ctx.type))] :
589
+ // abbrs
590
+ [TYPE[t] ?? err(`Unknown type ${t}`)]
591
+ );
592
+
593
+ // build type with mutable flag (mut t) or t
594
+ const fieldtype = (t, ctx, mut = t[0] === 'mut' ? 1 : 0) => [...reftype(mut ? t[1] : t, ctx), mut];
595
+
596
+
497
597
 
498
598
  // consume one instruction from nodes sequence
499
599
  const instr = (nodes, ctx) => {
@@ -512,50 +612,43 @@ const instr = (nodes, ctx) => {
512
612
  [...immed] = isNaN(op[0]) && INSTR[op] || err(`Unknown instruction ${op}`)
513
613
  code = immed[0]
514
614
 
515
- // v128s: (v128.load x) etc
516
- // https://github.com/WebAssembly/simd/blob/master/proposals/simd/BinarySIMD.md
517
- if (code === 0xfd) {
615
+ // gc-related
616
+ // https://webassembly.github.io/gc/core/binary/instructions.html#reference-instructions
617
+ if (code === 0x0fb) {
518
618
  [, 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))
619
+
620
+ // struct.new $t ... array.set $t
621
+ if ((code >= 0 && code <= 14) || (code >= 16 && code <= 19)) {
622
+ let tidx = id(nodes.shift(), ctx.type)
623
+ immed.push(...uleb(tidx))
624
+
625
+ // struct.get|set* $t $f - read field by index from struct definition (ctx.type[structidx][dfnidx])
626
+ if (code >= 2 && code <= 5) immed.push(...uleb(id(nodes.shift(), ctx.type[tidx][1])))
627
+ // array.new_fixed $t n
628
+ else if (code === 8) immed.push(...uleb(nodes.shift()))
629
+ // array.new_data|init_data $t $d
630
+ else if (code === 9 || code === 18) immed.push(...uleb(id(nodes.shift(), ctx.data)))
631
+ // array.new_elem|init_elem $t $e
632
+ else if (code === 10 || code === 19) immed.push(...uleb(id(nodes.shift(), ctx.elem)))
633
+ // array.copy $t $t
634
+ else if (code === 17) immed.push(...uleb(id(nodes.shift(), ctx.type)))
524
635
  }
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()))
636
+ // ref.test|cast (ref null? $t|heaptype)
637
+ else if (code >= 20 && code <= 23) {
638
+ // FIXME: normalizer is supposed to resolve this
639
+ let ht = reftype(nodes.shift(), ctx)
640
+ if (ht[0] !== REFTYPE.ref) immed.push(code = immed.pop()+1) // ref.test|cast (ref null $t) is next op
641
+ if (ht.length > 1) ht.shift() // pop ref
642
+ immed.push(...ht)
531
643
  }
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()))
644
+ // br_on_cast[_fail] $l? (ref null? ht1) (ref null? ht2)
645
+ // FIXME: normalizer should resolve anyref|etc to (ref null any|etc)
646
+ else if (code === 24 || code === 25) {
647
+ let i = blockid(nodes.shift(), ctx.block),
648
+ ht1 = reftype(nodes.shift(), ctx),
649
+ ht2 = reftype(nodes.shift(), ctx),
650
+ castflags = ((ht2[0] !== REFTYPE.ref) << 1) | (ht1[0] !== REFTYPE.ref)
651
+ immed.push(castflags, ...uleb(i), ht1.pop(), ht2.pop()) // we take only abstype or
559
652
  }
560
653
  }
561
654
 
@@ -592,24 +685,82 @@ const instr = (nodes, ctx) => {
592
685
  }
593
686
  }
594
687
 
688
+ // v128s: (v128.load x) etc
689
+ // https://github.com/WebAssembly/simd/blob/master/proposals/simd/BinarySIMD.md
690
+ else if (code === 0xfd) {
691
+ [, code] = immed
692
+ immed = [0xfd, ...uleb(code)]
693
+ // (v128.load offset? align?)
694
+ if (code <= 0x0b) {
695
+ const [a, o] = memarg(nodes)
696
+ immed.push(...uleb((a ?? align(op))), ...uleb(o ?? 0))
697
+ }
698
+ // (v128.load_lane offset? align? idx)
699
+ else if (code >= 0x54 && code <= 0x5d) {
700
+ const [a, o] = memarg(nodes)
701
+ immed.push(...uleb((a ?? align(op))), ...uleb(o ?? 0))
702
+ // (v128.load_lane_zero)
703
+ if (code <= 0x5b) immed.push(...uleb(nodes.shift()))
704
+ }
705
+ // (i8x16.shuffle 0 1 ... 15 a b)
706
+ else if (code === 0x0d) {
707
+ // i8, i16, i32 - bypass the encoding
708
+ for (let i = 0; i < 16; i++) immed.push(parseUint(nodes.shift(), 32))
709
+ }
710
+ // (v128.const i32x4 1 2 3 4)
711
+ else if (code === 0x0c) {
712
+ let [t, n] = nodes.shift().split('x'),
713
+ bits = +t.slice(1),
714
+ stride = bits >>> 3 // i16 -> 2, f32 -> 4
715
+ n = +n
716
+ // i8, i16, i32 - bypass the encoding
717
+ if (t[0] === 'i') {
718
+ let arr = n === 16 ? new Uint8Array(16) : n === 8 ? new Uint16Array(8) : n === 4 ? new Uint32Array(4) : new BigUint64Array(2)
719
+ for (let i = 0; i < n; i++) {
720
+ let s = nodes.shift(), v = encode[t].parse(s)
721
+ arr[i] = v
722
+ }
723
+ immed.push(...(new Uint8Array(arr.buffer)))
724
+ }
725
+ // f32, f64 - encode
726
+ else {
727
+ let arr = new Uint8Array(16)
728
+ for (let i = 0; i < n; i++) {
729
+ let s = nodes.shift(), v = encode[t](s)
730
+ arr.set(v, i * stride)
731
+ }
732
+ immed.push(...arr)
733
+ }
734
+ }
735
+ // (i8x16.extract_lane_s 0 ...)
736
+ else if (code >= 0x15 && code <= 0x22) {
737
+ immed.push(...uleb(parseUint(nodes.shift())))
738
+ }
739
+ }
740
+
595
741
  // control block abbrs
596
742
  // block ..., loop ..., if ...
597
743
  else if (code === 2 || code === 3 || code === 4) {
598
744
  ctx.block.push(code)
599
745
 
600
- // (block $x) (loop $y)
746
+ // (block $x) (loop $y) - save label pointer
747
+ // FIXME: do in normalizer
601
748
  if (nodes[0]?.[0] === '$') ctx.block[nodes.shift()] = ctx.block.length
602
749
 
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]]] : []
750
+ let t = nodes.shift();
606
751
 
607
752
  // void
608
- if (!param?.length && !result?.length) immed.push(TYPE.void)
753
+ if (!t) immed.push(TYPE.void)
609
754
  // (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))
755
+ // FIXME: Make sure it is signed positive integer (leb, not uleb) https://webassembly.github.io/gc/core/binary/instructions.html#control-instructions
756
+ else if (t[0] === 'result') immed.push(...reftype(t[1], ctx))
757
+ else {
758
+ let typeidx = id(t[1], ctx.type), [param, result] = ctx.type[typeidx][1]
759
+ // (type $idx (func (result i32)))
760
+ if (!param?.length && result.length === 1) immed.push(...reftype(result[0], ctx))
761
+ // (type idx)
762
+ else immed.push(...uleb(typeidx))
763
+ }
613
764
  }
614
765
  // else
615
766
  else if (code === 5) { }
@@ -638,7 +789,8 @@ const instr = (nodes, ctx) => {
638
789
  immed.push(
639
790
  ...uleb(id(nodes[1][1], ctx.type)),
640
791
  ...uleb(id(nodes.shift(), ctx.table))
641
- ), nodes.shift()
792
+ )
793
+ nodes.shift()
642
794
  }
643
795
 
644
796
  // call_ref $type
@@ -654,19 +806,14 @@ const instr = (nodes, ctx) => {
654
806
  // br_if $label cond result?
655
807
  // br_on_null $l, br_on_non_null $l
656
808
  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))
809
+ immed.push(...uleb(blockid(nodes.shift(), ctx.block)))
661
810
  }
662
811
 
663
812
  // br_table 1 2 3 4 0 selector result?
664
813
  else if (code == 0x0e) {
665
814
  let args = []
666
815
  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))
816
+ args.push(...uleb(blockid(nodes.shift(), ctx.block)))
670
817
  }
671
818
  args.unshift(...uleb(args.length - 1))
672
819
  immed.push(...args)
@@ -676,7 +823,7 @@ const instr = (nodes, ctx) => {
676
823
  else if (code == 0x1b) {
677
824
  let result = nodes.shift()
678
825
  // 0x1b -> 0x1c
679
- if (result.length) immed.push(immed.pop() + 1, ...vec(result.map(t => type(t, ctx))))
826
+ if (result.length) immed.push(immed.pop() + 1, ...vec(result.map(t => reftype(t, ctx))))
680
827
  }
681
828
 
682
829
  // ref.func $id
@@ -687,7 +834,7 @@ const instr = (nodes, ctx) => {
687
834
  // ref.null func
688
835
  else if (code == 0xd0) {
689
836
  let t = nodes.shift()
690
- immed.push(HEAPTYPE[t] || id(t, ctx.type)) // func->funcref, extern->externref
837
+ immed.push(...(HEAPTYPE[t] ? [HEAPTYPE[t]] : uleb(id(t, ctx.type)))) // func->funcref, extern->externref
691
838
  }
692
839
 
693
840
  // binary/unary (i32.add a b) - no immed
@@ -710,7 +857,7 @@ const instr = (nodes, ctx) => {
710
857
  immed.push(0)
711
858
  }
712
859
 
713
- // table.get $id
860
+ // table.get|set $id
714
861
  else if (code == 0x25 || code == 0x26) {
715
862
  immed.push(...uleb(id(nodes.shift(), ctx.table)))
716
863
  }
@@ -723,6 +870,16 @@ const instr = (nodes, ctx) => {
723
870
  // instantiation time value initializer (consuming) - we redirect to instr
724
871
  const expr = (node, ctx) => [...instr([node], ctx), 0x0b]
725
872
 
873
+ // deref id node to numeric idx
874
+ const id = (nm, list, n) => (n = nm[0] === '$' ? list[nm] : +nm, n in list ? n : err(`Unknown ${list.name} ${nm}`))
875
+
876
+ // block id - same as id but for block
877
+ // index indicates how many block items to pop
878
+ const blockid = (nm, block, i) => (
879
+ i = nm?.[0] === '$' ? block.length - block[nm] : +nm,
880
+ isNaN(i) || i > block.length ? err(`Bad label ${nm}`) : i
881
+ )
882
+
726
883
  // consume align/offset params
727
884
  const memarg = (args) => {
728
885
  let align, offset, k, v
@@ -734,10 +891,6 @@ const memarg = (args) => {
734
891
  return [align, offset]
735
892
  }
736
893
 
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
894
  // const ALIGN = {
742
895
  // 'i32.load': 4, 'i64.load': 8, 'f32.load': 4, 'f64.load': 8,
743
896
  // 'i32.load8_s': 1, 'i32.load8_u': 1, 'i32.load16_s': 2, 'i32.load16_u': 2,
@@ -754,7 +907,14 @@ const align = (op) => {
754
907
  }
755
908
 
756
909
  // 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())]
910
+ const limits = (node) => (
911
+ isNaN(parseInt(node[1])) ? [0, ...uleb(parseUint(node.shift()))] : [node[2] === 'shared' ? 3 : 1, ...uleb(parseUint(node.shift())), ...uleb(parseUint(node.shift()))]
912
+ )
913
+
914
+ // check if node is valid int in a range
915
+ // we put extra condition for index ints for tests complacency
916
+ 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
917
+
758
918
 
759
919
  // escape codes
760
920
  const escape = { n: 10, r: 13, t: 9, v: 1, '"': 34, "'": 39, '\\': 92 }
@@ -772,7 +932,3 @@ const str = str => {
772
932
 
773
933
  // serialize binary array
774
934
  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