watr 3.2.0 → 3.3.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.2.0",
3
+ "version": "3.3.0",
4
4
  "description": "Ligth & fast WAT compiler",
5
5
  "main": "watr.js",
6
6
  "exports": {
package/readme.md CHANGED
@@ -77,25 +77,22 @@ print(src, {
77
77
  ## Status
78
78
 
79
79
  * [x] core
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)
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), [sign extension](https://github.com/WebAssembly/sign-extension-ops), [nontrapping float to int](https://github.com/WebAssembly/nontrapping-float-to-int-conversions)
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/main/proposals/multi-memory/Overview.md)
82
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)
83
83
  * [x] [tail_call](https://github.com/WebAssembly/tail-call)
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)
86
- * [ ] [exceptions](https://github.com/WebAssembly/exception-handling)
87
- * [ ] [memory64](https://github.com/WebAssembly/memory64)
88
- * [ ] [annotations](https://github.com/WebAssembly/annotations), [code_metadata](https://github.com/WebAssembly/tool-conventions/blob/main/CodeMetadata.md)
89
- * [ ] [js strings](https://github.com/WebAssembly/js-string-builtins/blob/main/proposals/js-string-builtins/Overview.md)
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), [gc](https://github.com/WebAssembly/gc)
85
+ * [x] [annotations](https://github.com/WebAssembly/annotations), [code_metadata](https://github.com/WebAssembly/tool-conventions/blob/main/CodeMetadata.md)
86
+ * [ ] [exceptions](https://github.com/WebAssembly/exception-handling), [memory64](https://github.com/WebAssembly/memory64), [js strings](https://github.com/WebAssembly/js-string-builtins/blob/main/proposals/js-string-builtins/Overview.md), wide arithmetic, threads, custom page size, wasm 3
90
87
 
91
88
  ## Alternatives
92
89
 
93
90
    | Size (gzipped) | Performance
94
91
  ---|---|---
95
- watr | 6.2 kb | 11.6 op/s
96
- [spec/wast.js](https://github.com/WebAssembly/spec/tree/main/interpreter#javascript-library) | 216 kb | 7.1 op/s
97
- [wabt](https://github.com/WebAssembly/wabt) | 282 kb | 2.3 op/s
98
- [wat-compiler](https://github.com/stagas/wat-compiler) | 7.7 kb | 1.34 op/s
92
+ watr | 7.5 kb | 6.0 op/s
93
+ [spec/wast.js](https://github.com/WebAssembly/spec/tree/main/interpreter#javascript-library) | 216 kb | 2.2 op/s
94
+ [wabt](https://github.com/WebAssembly/wabt) | 282 kb | 1.2 op/s
95
+ [wat-compiler](https://github.com/stagas/wat-compiler) | 7.7 kb | 0.7 op/s
99
96
 
100
97
  <!--
101
98
  ## Projects using watr
package/src/compile.js CHANGED
@@ -1,12 +1,14 @@
1
1
  import * as encode from './encode.js'
2
2
  import { uleb, i32, i64 } from './encode.js'
3
- import { SECTION, TYPE, KIND, INSTR, HEAPTYPE, DEFTYPE, RECTYPE, REFTYPE } from './const.js'
3
+ import { SECTION, TYPE, KIND, INSTR, HEAPTYPE, DEFTYPE, RECTYPE, REFTYPE, ESCAPE } from './const.js'
4
4
  import parse from './parse.js'
5
- import { clone, err } from './util.js'
5
+ import { clone, err, str } from './util.js'
6
6
 
7
7
  // build instructions index
8
8
  INSTR.forEach((op, i) => INSTR[op] = i >= 0x133 ? [0xfd, i - 0x133] : i >= 0x11b ? [0xfc, i - 0x11b] : i >= 0xfb ? [0xfb, i - 0xfb] : [i]);
9
9
 
10
+ // recursively strip all annotation nodes from AST, except @custom and @metadata.code.*
11
+ const unannot = (node) => Array.isArray(node) ? (node[0]?.[0] === '@' && node[0] !== '@custom' && !node[0]?.startsWith?.('@metadata.code.') ? null : node.map(unannot).filter(n => n != null)) : node
10
12
 
11
13
  /**
12
14
  * Converts a WebAssembly Text Format (WAT) tree to a WebAssembly binary format (WASM).
@@ -20,6 +22,9 @@ export default function watr(nodes) {
20
22
  if (typeof nodes === 'string') nodes = parse(nodes);
21
23
  else nodes = clone(nodes)
22
24
 
25
+ // strip annotations (text-format only), except @custom which becomes binary custom sections
26
+ nodes = unannot(nodes) || []
27
+
23
28
  // module abbr https://webassembly.github.io/spec/core/text/modules.html#id10
24
29
  if (nodes[0] === 'module') nodes.shift(), nodes[0]?.[0] === '$' && nodes.shift()
25
30
  // single node, not module
@@ -28,115 +33,109 @@ export default function watr(nodes) {
28
33
  // binary abbr "\00" "\0x61" ...
29
34
  if (nodes[0] === 'binary') {
30
35
  nodes.shift()
31
- return Uint8Array.from(str(nodes.map(i => i.slice(1, -1)).join('')))
36
+ return Uint8Array.from(str(...nodes))
32
37
  }
33
38
  // quote "a" "b"
34
39
  else if (nodes[0] === 'quote') {
35
40
  nodes.shift()
36
- return watr(nodes.map(i => i.slice(1, -1)).join(''))
41
+ return watr(nodes.map(s => s.slice(1, -1)).join(''))
37
42
  }
38
43
 
39
44
  // scopes are aliased by key as well, eg. section.func.$name = section[SECTION.func] = idx
40
45
  const ctx = []
41
46
  for (let kind in SECTION) (ctx[SECTION[kind]] = ctx[kind] = []).name = kind
42
- ctx._ = {} // implicit types
43
-
44
- let subc // current subtype count
45
-
46
- // prepare/normalize nodes
47
- while (nodes.length) {
48
- let [kind, ...node] = nodes.shift()
49
- let imported // if node needs to be imported
50
- let rec // number of subtypes under rec type
51
47
 
48
+ // initialize types
49
+ nodes.filter(([kind, ...node]) => {
52
50
  // (rec (type $a (sub final? $sup* (func ...))...) (type $b ...)) -> save subtypes
53
51
  if (kind === 'rec') {
54
52
  // node contains a list of subtypes, (type ...) or (type (sub final? ...))
55
53
  // convert rec type into regular type (first subtype) with stashed subtypes length
56
54
  // 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
- }
60
-
61
- // import abbr
62
- // (import m n (table|memory|global|func id? type)) -> (table|memory|global|func id? (import m n) type)
63
- else if (kind === 'import') [kind, ...node] = (imported = node).pop()
64
-
65
- // index, alias
66
- let items = ctx[kind];
67
- let name = alias(node, items)
68
-
69
- // export abbr
70
- // (table|memory|global|func id? (export n)* ...) -> (table|memory|global|func id ...) (export n (table|memory|global|func id))
71
- while (node[0]?.[0] === 'export') ctx.export.push([node.shift()[1], [kind, items.length]])
72
-
73
- // for import nodes - redirect output to import
74
- if (node[0]?.[0] === 'import') [, ...imported] = node.shift()
75
-
76
- // table abbr
77
- if (kind === 'table') {
78
- // (table id? reftype (elem ...{n})) -> (table id? n n reftype) (elem (table id) (i32.const 0) reftype ...)
79
- if (node[1]?.[0] === 'elem') {
80
- let [reftype, [, ...els]] = node
81
- node = [els.length, els.length, reftype]
82
- ctx.elem.push([['table', name || items.length], ['i32.const', '0'], reftype, ...els])
55
+ for (let i = 0; i < node.length; i++) {
56
+ let [, ...subnode] = node[i]
57
+ alias(subnode, ctx.type);
58
+ (subnode = typedef(subnode, ctx)).push(i ? true : [ctx.type.length, node.length])
59
+ ctx.type.push(subnode)
83
60
  }
84
61
  }
85
-
86
- // data abbr
87
- // (memory id? (data str)) -> (memory id? n n) (data (memory id) (i32.const 0) str)
88
- else if (kind === 'memory' && node[0]?.[0] === 'data') {
89
- let [, ...data] = node.shift(), m = '' + Math.ceil(data.map(s => s.slice(1, -1)).join('').length / 65536) // FIXME: figure out actual data size
90
- ctx.data.push([['memory', items.length], ['i32.const', 0], ...data])
91
- node = [m, m]
92
- }
93
-
94
- // keep start name
95
- else if (kind === 'start') name && node.push(name)
96
-
97
- // normalize type definition to (func|array|struct dfn) form
98
62
  // (type (func param* result*))
99
63
  // (type (array (mut i8)))
100
64
  // (type (struct (field a)*)
101
65
  // (type (sub final? $nm* (struct|array|func ...)))
102
66
  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
67
+ alias(node, ctx.type);
68
+ ctx.type.push(typedef(node, ctx));
69
+ }
70
+ // (@custom "name" placement? data)
71
+ else if (kind === '@custom') {
72
+ ctx.custom.push(node) // node is just the arguments, not including @custom
73
+ }
74
+ // other sections may have id
75
+ else if (kind === 'start' || kind === 'export') ctx[kind].push(node)
76
+
77
+ else return true
78
+ })
79
+
80
+ // prepare/normalize nodes
81
+ .forEach(([kind, ...node]) => {
82
+ let imported // if node needs to be imported
83
+
84
+ // import abbr
85
+ // (import m n (table|memory|global|func id? type)) -> (table|memory|global|func id? (import m n) type)
86
+ if (kind === 'import') [kind, ...node] = (imported = node).pop()
87
+
88
+ // index, alias
89
+ let items = ctx[kind];
90
+ let name = alias(node, items);
91
+
92
+ // export abbr
93
+ // (table|memory|global|func id? (export n)* ...) -> (table|memory|global|func id ...) (export n (table|memory|global|func id))
94
+ while (node[0]?.[0] === 'export') ctx.export.push([node.shift()[1], [kind, items?.length]])
95
+
96
+ // for import nodes - redirect output to import
97
+ if (node[0]?.[0] === 'import') [, ...imported] = node.shift()
98
+
99
+ // table abbr
100
+ if (kind === 'table') {
101
+ // (table id? reftype (elem ...{n})) -> (table id? n n reftype) (elem (table id) (i32.const 0) reftype ...)
102
+ if (node[1]?.[0] === 'elem') {
103
+ let [reftype, [, ...els]] = node
104
+ node = [els.length, els.length, reftype]
105
+ ctx.elem.push([['table', name || items.length], ['i32.const', '0'], reftype, ...els])
106
+ }
109
107
  }
110
108
 
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()
109
+ // data abbr
110
+ // (memory id? (data str)) -> (memory id? n n) (data (memory id) (i32.const 0) str)
111
+ else if (kind === 'memory' && node[0]?.[0] === 'data') {
112
+ let [, ...data] = node.shift(), m = '' + Math.ceil(data.map(s => s.slice(1, -1)).join('').length / 65536) // FIXME: figure out actual data size
113
+ ctx.data.push([['memory', items.length], ['i32.const', 0], ...data])
114
+ node = [m, m]
115
+ }
115
116
 
116
- node = [ckind, dfn, subkind, supertypes, rec ? [ctx.type.length, rec] : issub]
117
- }
117
+ // dupe to code section, save implicit type
118
+ else if (kind === 'func') {
119
+ let [idx, param, result] = typeuse(node, ctx);
120
+ idx ??= regtype(param, result, ctx)
118
121
 
119
- // dupe to code section, save implicit type
120
- else if (kind === 'func') {
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
125
- node.unshift(['type', idx])
126
- }
127
-
128
- // import writes to import section amd adds placeholder for (kind) section
129
- if (imported) ctx.import.push([...imported, [kind, ...node]]), node = null
122
+ // we save idx because type can be defined after
123
+ !imported && ctx.code.push([[idx, param, result], ...plain(node, ctx)]) // pass param since they may have names
124
+ node.unshift(['type', idx])
125
+ }
130
126
 
131
- items.push(node)
132
- }
127
+ // tag has a type similar to func
128
+ else if (kind === 'tag') {
129
+ let [idx, param] = typeuse(node, ctx);
130
+ idx ??= regtype(param, [], ctx)
131
+ node.unshift(['type', idx])
132
+ }
133
133
 
134
- // add implicit types - main types receive aliases, implicit types are added if no explicit types exist
135
- for (let n in ctx._) ctx.type[n] ??= (ctx.type.push(['func', ctx._[n]]) - 1)
134
+ // import writes to import section amd adds placeholder for (kind) section
135
+ if (imported) ctx.import.push([...imported, [kind, ...node]]), node = null
136
136
 
137
- // patch datacount if data === 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
137
+ items?.push(node)
138
+ })
140
139
 
141
140
  // convert nodes to bytes
142
141
  const bin = (kind, count = true) => {
@@ -145,11 +144,14 @@ export default function watr(nodes) {
145
144
  .map(item => build[kind](item, ctx))
146
145
  .filter(Boolean) // filter out unrenderable things (subtype or data.length)
147
146
 
147
+ // Custom sections - each is output as separate section with own header
148
+ if (kind === SECTION.custom) return items.flatMap(content => [kind, ...vec(content)])
149
+
148
150
  return !items.length ? [] : [kind, ...vec(count ? vec(items) : items)]
149
151
  }
150
152
 
151
153
  // build final binary
152
- return Uint8Array.from([
154
+ const out = [
153
155
  0x00, 0x61, 0x73, 0x6d, // magic
154
156
  0x01, 0x00, 0x00, 0x00, // version
155
157
  ...bin(SECTION.custom),
@@ -158,31 +160,153 @@ export default function watr(nodes) {
158
160
  ...bin(SECTION.func),
159
161
  ...bin(SECTION.table),
160
162
  ...bin(SECTION.memory),
163
+ ...bin(SECTION.tag),
161
164
  ...bin(SECTION.global),
162
165
  ...bin(SECTION.export),
163
166
  ...bin(SECTION.start, false),
164
167
  ...bin(SECTION.elem),
165
168
  ...bin(SECTION.datacount, false),
166
- ...bin(SECTION.code),
167
- ...bin(SECTION.data)
168
- ])
169
+ ]
170
+
171
+ // Build code section first (populates ctx.meta)
172
+ const codeSection = bin(SECTION.code)
173
+
174
+ // Build code metadata custom sections: metadata.code.<type>
175
+ for (const type in ctx.meta) {
176
+ const name = vec(str(`"metadata.code.${type}"`))
177
+ const content = vec(ctx.meta[type].map(([funcIdx, instances]) =>
178
+ [...uleb(funcIdx), ...vec(instances.map(([pos, data]) => [...uleb(pos), ...vec(str(data))]))]
179
+ ))
180
+ out.push(0, ...vec([...name, ...content]))
181
+ }
182
+
183
+ out.push(...codeSection, ...bin(SECTION.data))
184
+
185
+ return Uint8Array.from(out)
169
186
  }
170
187
 
171
188
  // consume name eg. $t ...
172
189
  const alias = (node, list) => {
173
190
  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
191
+ if (name && list) name in list ? err(`Duplicate ${list.name} ${name}`) : list[name] = list.length; // save alias
175
192
  return name
176
193
  }
177
194
 
195
+ // (type $id? (func param* result*))
196
+ // (type $id? (array (mut i8)))
197
+ // (type $id? (struct (field a)*)
198
+ // (type $id? (sub final? $nm* (struct|array|func ...)))
199
+ const typedef = ([dfn], ctx) => {
200
+ let subkind = 'subfinal', supertypes = [], compkind
201
+ if (dfn[0] === 'sub') {
202
+ subkind = dfn.shift(), dfn[0] === 'final' && (subkind += dfn.shift())
203
+ dfn = (supertypes = dfn).pop() // last item is definition
204
+ }
205
+
206
+ [compkind, ...dfn] = dfn // composite type kind
207
+
208
+ if (compkind === 'func') dfn = paramres(dfn), ctx.type['$' + dfn.join('>')] ??= ctx.type.length
209
+ else if (compkind === 'struct') dfn = fieldseq(dfn, 'field', true)
210
+ else if (compkind === 'array') [dfn] = dfn
211
+
212
+ return [compkind, dfn, subkind, supertypes]
213
+ }
214
+
215
+ // register (implicit) type
216
+ const regtype = (param, result, ctx, idx = '$' + param + '>' + result) => (
217
+ (ctx.type[idx] ??= ctx.type.push(['func', [param, result]]) - 1),
218
+ idx
219
+ )
220
+
221
+ // consume typeuse nodes, return type index/params, or null idx if no type
222
+ // https://webassembly.github.io/spec/core/text/modules.html#type-uses
223
+ const typeuse = (nodes, ctx, names) => {
224
+ let idx, param, result
225
+
226
+ // explicit type (type 0|$name)
227
+ if (nodes[0]?.[0] === 'type') {
228
+ [, idx] = nodes.shift();
229
+ [param, result] = paramres(nodes, names);
230
+
231
+ const [, srcParamRes] = ctx.type[id(idx, ctx.type)] ?? err(`Unknown type ${idx}`)
232
+
233
+ // check type consistency (excludes forward refs)
234
+ if ((param.length || result.length) && srcParamRes.join('>') !== param + '>' + result) err(`Type ${idx} mismatch`)
235
+
236
+ return [idx, ...srcParamRes]
237
+ }
238
+
239
+ // implicit type (param i32 i32)(result i32)
240
+ return [idx, ...paramres(nodes, names)]
241
+ }
242
+
243
+ // consume (param t+)* (result t+)* sequence
244
+ const paramres = (nodes, names = true) => {
245
+ // let param = [], result = []
246
+
247
+ // collect param (param i32 i64) (param $x? i32)
248
+ let param = fieldseq(nodes, 'param', names)
249
+
250
+ // collect result eg. (result f64 f32)(result i32)
251
+ let result = fieldseq(nodes, 'result')
252
+
253
+ if (nodes[0]?.[0] === 'param') err(`Unexpected param`)
254
+
255
+ return [param, result]
256
+ }
257
+
258
+ // collect sequence of field, eg. (param a) (param b c), (field a) (field b c) or (result a b) (result c)
259
+ // optionally allow or not names
260
+ const fieldseq = (nodes, field, names = false) => {
261
+ let seq = []
262
+ // collect field eg. (field f64 f32)(field i32)
263
+ while (nodes[0]?.[0] === field) {
264
+ let [, ...args] = nodes.shift()
265
+ let name = args[0]?.[0] === '$' && args.shift()
266
+ // expose name refs, if allowed
267
+ if (name) {
268
+ if (names) name in seq ? err(`Duplicate ${field} ${name}`) : seq[name] = seq.length
269
+ else err(`Unexpected ${field} name ${name}`)
270
+ }
271
+ seq.push(...args)
272
+ }
273
+ return seq
274
+ }
275
+
276
+ // consume blocktype - makes sure either type or single result is returned
277
+ const blocktype = (nodes, ctx) => {
278
+ let [idx, param, result] = typeuse(nodes, ctx, 0)
279
+
280
+ // get type - can be either idx or valtype (numtype | reftype)
281
+ if (!param.length && !result.length) return
282
+
283
+ // (result i32) - doesn't require registering type
284
+ if (!param.length && result.length === 1) return ['result', ...result]
285
+
286
+ // register implicit type
287
+ idx ??= regtype(param, result, ctx)
288
+
289
+ return ['type', idx]
290
+ }
291
+
178
292
  // abbr blocks, loops, ifs; collect implicit types via typeuses; resolve optional immediates
179
293
  // https://webassembly.github.io/spec/core/text/instructions.html#folded-instructions
180
294
  const plain = (nodes, ctx) => {
181
295
  let out = [], stack = [], label
296
+ // helper: check if node is immediate (not array operand)
297
+ const isImm = n => typeof n === 'string' || typeof n === 'number'
182
298
 
183
299
  while (nodes.length) {
184
300
  let node = nodes.shift()
185
301
 
302
+ // code metadata annotations - pass through as marker with metadata type and data
303
+ // (@metadata.code.<type> data:str)
304
+ if (Array.isArray(node) && node[0]?.startsWith?.('@metadata.code.')) {
305
+ let type = node[0].slice(15) // remove '@metadata.code.' prefix
306
+ out.push(['@metadata', type, node[1]])
307
+ continue
308
+ }
309
+
186
310
  // lookup is slower than sequence of known ifs
187
311
  if (typeof node === 'string') {
188
312
  out.push(node)
@@ -198,7 +322,7 @@ const plain = (nodes, ctx) => {
198
322
  // else $label
199
323
  // end $label - make sure it matches block label
200
324
  else if (node === 'else' || node === 'end') {
201
- if (nodes[0]?.[0] === '$') (node === 'end' ? stack.pop() : label) !== (label = nodes.shift()) && err(`Mismatched label ${label}`)
325
+ if (nodes[0]?.[0] === '$') (node === 'end' ? stack.pop() : label) !== (label = nodes.shift()) && err(`Mismatched ${node} label ${label}`)
202
326
  }
203
327
 
204
328
  // select (result i32 i32 i32)?
@@ -211,12 +335,24 @@ const plain = (nodes, ctx) => {
211
335
  else if (node.endsWith('call_indirect')) {
212
336
  let tableidx = nodes[0]?.[0] === '$' || !isNaN(nodes[0]) ? nodes.shift() : 0
213
337
  let [idx, param, result] = typeuse(nodes, ctx, 0)
214
- out.push(tableidx, ['type', idx ?? (ctx._[idx = '$' + param + '>' + result] = [param, result], idx)])
338
+ out.push(tableidx, ['type', idx ?? regtype(param, result, ctx)])
215
339
  }
216
340
 
217
341
  // mark datacount section as required
218
342
  else if (node === 'memory.init' || node === 'data.drop' || node === 'array.new_data' || node === 'array.init_data') {
219
343
  ctx.datacount[0] = true
344
+ // memory.init memidx? dataidx
345
+ if (node === 'memory.init') out.push(isImm(nodes[1]) ? nodes.shift() : 0, isImm(nodes[0]) ? nodes.shift() : 0)
346
+ }
347
+
348
+ // memory.* memidx? - multi-memory proposal
349
+ else if (node === 'memory.size' || node === 'memory.grow' || node === 'memory.fill') {
350
+ out.push(isImm(nodes[0]) ? nodes.shift() : 0)
351
+ }
352
+
353
+ // memory.copy dstmem? srcmem?
354
+ else if (node === 'memory.copy') {
355
+ out.push(isImm(nodes[0]) ? nodes.shift() : 0, isImm(nodes[0]) ? nodes.shift() : 0)
220
356
  }
221
357
 
222
358
  // table.init tableidx? elemidx -> table.init tableidx elemidx
@@ -239,6 +375,9 @@ const plain = (nodes, ctx) => {
239
375
 
240
376
  // (if ...) -> if ... end
241
377
  else if (node[0] === 'if') {
378
+ // Pop pending metadata (branch_hint) if present
379
+ let meta = out[out.length - 1]?.[0] === '@metadata' && out.pop()
380
+
242
381
  let then = [], els = [], immed = [node.shift()]
243
382
  // (if label? blocktype? cond*? (then instr*) (else instr*)?) -> cond*? if label? blocktype? instr* else instr*? end
244
383
  // https://webassembly.github.io/spec/core/text/instructions.html#control-instructions
@@ -258,7 +397,8 @@ const plain = (nodes, ctx) => {
258
397
 
259
398
  if (typeof node[0] === 'string') err('Unfolded condition')
260
399
 
261
- out.push(...plain(node, ctx), ...immed, ...then, ...els, 'end')
400
+ // conditions, metadata (if any), if, then, else, end
401
+ out.push(...plain(node, ctx), ...(meta ? [meta] : []), ...immed, ...then, ...els, 'end')
262
402
  }
263
403
  else out.push(plain(node, ctx))
264
404
  }
@@ -267,83 +407,23 @@ const plain = (nodes, ctx) => {
267
407
  return out
268
408
  }
269
409
 
270
- // consume typeuse nodes, return type index/params, or null idx if no type
271
- // https://webassembly.github.io/spec/core/text/modules.html#type-uses
272
- const typeuse = (nodes, ctx, names) => {
273
- let idx, param, result
274
-
275
- // explicit type (type 0|$name)
276
- if (nodes[0]?.[0] === 'type') {
277
- [, idx] = nodes.shift();
278
- [param, result] = paramres(nodes, names);
279
-
280
- // check type consistency (excludes forward refs)
281
- if ((param.length || result.length) && idx in ctx.type)
282
- if (ctx.type[id(idx, ctx.type)][1].join('>') !== param + '>' + result) err(`Type ${idx} mismatch`)
283
-
284
- return [idx]
285
- }
286
-
287
- // implicit type (param i32 i32)(result i32)
288
- [param, result] = paramres(nodes, names)
289
-
290
- return [, param, result]
291
- }
292
-
293
- // consume (param t+)* (result t+)* sequence
294
- const paramres = (nodes, names = true) => {
295
- // let param = [], result = []
296
-
297
- // collect param (param i32 i64) (param $x? i32)
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
410
 
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) {
314
- let [, ...args] = nodes.shift()
315
- let name = args[0]?.[0] === '$' && args.shift()
316
- // expose name refs, if allowed
317
- if (name) {
318
- if (names) name in seq ? err(`Duplicate ${field} ${name}`) : seq[name] = seq.length
319
- else err(`Unexpected ${field} name ${name}`)
411
+ // build section binary [by section codes] (non consuming)
412
+ const build = [
413
+ // (@custom "name" placement? data)
414
+ // placement is optional: (before|after section) or (before first)|(after last)
415
+ // For now we ignore placement and just output the custom section
416
+ ([name, ...rest], ctx) => {
417
+ // Check if second arg is placement directive
418
+ let data = rest
419
+ if (rest[0]?.[0] === 'before' || rest[0]?.[0] === 'after') {
420
+ // Skip placement for now - would need more complex section ordering
421
+ data = rest.slice(1)
320
422
  }
321
- seq.push(...args)
322
- }
323
- return seq
324
- }
325
-
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)
329
-
330
- // direct idx (no params/result needed)
331
- if (idx != null) return ['type', idx]
332
423
 
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]
342
- }
343
-
344
-
345
- // build section binary [by section codes] (non consuming)
346
- const build = [,
424
+ // Custom section format: name (vec string) + raw content bytes
425
+ return [...vec(str(name)), ...str(...data)]
426
+ },
347
427
  // type kinds
348
428
  // (func params result)
349
429
  // (array i8)
@@ -354,7 +434,6 @@ const build = [,
354
434
  let details
355
435
  // (rec (sub ...)*)
356
436
  if (rec) {
357
- // FIXME: rec of one type
358
437
  kind = 'rec'
359
438
  let [from, length] = rec, subtypes = Array.from({ length }, (_, i) => build[SECTION.type](ctx.type[from + i].slice(0, 4), ctx))
360
439
  details = vec(subtypes)
@@ -378,7 +457,7 @@ const build = [,
378
457
  return [DEFTYPE[kind], ...details]
379
458
  },
380
459
 
381
- // (import "math" "add" (func|table|global|memory typedef?))
460
+ // (import "math" "add" (func|table|global|memory dfn?))
382
461
  ([mod, field, [kind, ...dfn]], ctx) => {
383
462
  let details
384
463
 
@@ -387,6 +466,10 @@ const build = [,
387
466
  let [[, typeidx]] = dfn
388
467
  details = uleb(id(typeidx, ctx.type))
389
468
  }
469
+ else if (kind === 'tag') {
470
+ let [[, typeidx]] = dfn
471
+ details = [0x00, ...uleb(id(typeidx, ctx.type))]
472
+ }
390
473
  else if (kind === 'memory') {
391
474
  details = limits(dfn)
392
475
  }
@@ -398,7 +481,7 @@ const build = [,
398
481
  }
399
482
  else err(`Unknown kind ${kind}`)
400
483
 
401
- return ([...vec(str(mod.slice(1, -1))), ...vec(str(field.slice(1, -1))), KIND[kind], ...details])
484
+ return ([...vec(str(mod)), ...vec(str(field)), KIND[kind], ...details])
402
485
  },
403
486
 
404
487
  // (func $name? ...params result ...body)
@@ -417,7 +500,7 @@ const build = [,
417
500
  ([t, init], ctx) => [...fieldtype(t, ctx), ...expr(init, ctx)],
418
501
 
419
502
  // (export "name" (func|table|mem $name|idx))
420
- ([nm, [kind, l]], ctx) => ([...vec(str(nm.slice(1, -1))), KIND[kind], ...uleb(id(l, ctx[kind]))]),
503
+ ([nm, [kind, l]], ctx) => ([...vec(str(nm)), KIND[kind], ...uleb(id(l, ctx[kind]))]),
421
504
 
422
505
  // (start $main)
423
506
  ([l], ctx) => uleb(id(l, ctx.func)),
@@ -518,6 +601,10 @@ const build = [,
518
601
  ctx.local.name = 'local'
519
602
  ctx.block.name = 'block'
520
603
 
604
+ // Track current code index for code metadata
605
+ if (ctx._codeIdx === undefined) ctx._codeIdx = 0
606
+ let codeIdx = ctx._codeIdx++
607
+
521
608
  // collect locals
522
609
  while (body[0]?.[0] === 'local') {
523
610
  let [, ...types] = body.shift()
@@ -529,10 +616,21 @@ const build = [,
529
616
  ctx.local.push(...types)
530
617
  }
531
618
 
619
+ ctx._meta = null
532
620
  const bytes = []
533
621
  while (body.length) bytes.push(...instr(body, ctx))
534
622
  bytes.push(0x0b)
535
623
 
624
+ // Extract metadata placeholders (arrays), group by type
625
+ const metaByType = {}, cleanBytes = []
626
+ for (const b of bytes)
627
+ if (Array.isArray(b)) for (const [type, data] of b) (metaByType[type] ??= []).push([cleanBytes.length, data])
628
+ else cleanBytes.push(b)
629
+
630
+ // Store metadata for this function, grouped by type
631
+ const funcIdx = ctx.import.filter(imp => imp[2][0] === 'func').length + codeIdx
632
+ for (const type in metaByType) ((ctx.meta ??= {})[type] ??= []).push([funcIdx, metaByType[type]])
633
+
536
634
  // squash locals into (n:u32 t:valtype)*, n is number and t is type
537
635
  // we skip locals provided by params
538
636
  let loctypes = ctx.local.slice(param.length).reduce((a, type) => (type == a[a.length - 1]?.[1] ? a[a.length - 1][0]++ : a.push([1, type]), a), [])
@@ -541,7 +639,7 @@ const build = [,
541
639
  ctx.local = ctx.block = null
542
640
 
543
641
  // https://webassembly.github.io/spec/core/binary/modules.html#code-section
544
- return vec([...vec(loctypes.map(([n, t]) => [...uleb(n), ...reftype(t, ctx)])), ...bytes])
642
+ return vec([...vec(loctypes.map(([n, t]) => [...uleb(n), ...reftype(t, ctx)])), ...cleanBytes])
545
643
  },
546
644
 
547
645
  // (data (i32.const 0) "\aa" "\bb"?)
@@ -557,10 +655,10 @@ const build = [,
557
655
  }
558
656
 
559
657
  // (offset (i32.const 0)) or (i32.const 0)
560
- if (typeof inits[0] !== 'string') {
658
+ if (typeof inits[0] !== 'string' && inits[0]) {
561
659
  offset = inits.shift()
562
- if (offset[0] === 'offset') [, offset] = offset
563
- offset ?? err('Bad offset', offset)
660
+ if (offset?.[0] === 'offset') [, offset] = offset
661
+ else offset ?? err('Bad offset', offset)
564
662
  }
565
663
 
566
664
  return ([
@@ -572,7 +670,7 @@ const build = [,
572
670
  // passive: 1
573
671
  [1]
574
672
  ),
575
- ...vec(str(inits.map(i => i.slice(1, -1)).join('')))
673
+ ...vec(str(...inits))
576
674
  ])
577
675
  },
578
676
 
@@ -580,6 +678,9 @@ const build = [,
580
678
  (nodes, ctx) => uleb(ctx.data.length)
581
679
  ]
582
680
 
681
+ // (tag $id? (param i32)*) - tags for exception handling
682
+ build[SECTION.tag] = ([[, typeidx]], ctx) => [0x00, ...uleb(id(typeidx, ctx.type))]
683
+
583
684
  // build reftype, either direct absheaptype or wrapped heaptype https://webassembly.github.io/gc/core/binary/types.html#reference-types
584
685
  const reftype = (t, ctx) => (
585
686
  t[0] === 'ref' ?
@@ -594,17 +695,26 @@ const reftype = (t, ctx) => (
594
695
  const fieldtype = (t, ctx, mut = t[0] === 'mut' ? 1 : 0) => [...reftype(mut ? t[1] : t, ctx), mut];
595
696
 
596
697
 
597
-
598
698
  // consume one instruction from nodes sequence
599
699
  const instr = (nodes, ctx) => {
600
700
  if (!nodes?.length) return []
601
701
 
602
702
  let out = [], op = nodes.shift(), immed, code
703
+ const isImm = n => typeof n === 'string' || typeof n === 'number'
704
+
705
+ // Handle code metadata marker - store for next instruction
706
+ // ['@metadata', type, data]
707
+ if (op?.[0] === '@metadata') {
708
+ ;(ctx._meta ??= []).push(op.slice(1))
709
+ return nodes.length ? instr(nodes, ctx) : []
710
+ }
603
711
 
604
712
  // consume group
605
713
  if (Array.isArray(op)) {
606
714
  immed = instr(op, ctx)
607
715
  while (op.length) out.push(...instr(op, ctx))
716
+ // Insert metadata placeholder before instruction
717
+ if (ctx._meta) out.push(ctx._meta), ctx._meta = null
608
718
  out.push(...immed)
609
719
  return out
610
720
  }
@@ -627,7 +737,9 @@ const instr = (nodes, ctx) => {
627
737
  // array.new_fixed $t n
628
738
  else if (code === 8) immed.push(...uleb(nodes.shift()))
629
739
  // array.new_data|init_data $t $d
630
- else if (code === 9 || code === 18) immed.push(...uleb(id(nodes.shift(), ctx.data)))
740
+ else if (code === 9 || code === 18) {
741
+ immed.push(...uleb(id(isImm(nodes[0]) ? nodes.shift() : 0, ctx.data)))
742
+ }
631
743
  // array.new_elem|init_elem $t $e
632
744
  else if (code === 10 || code === 19) immed.push(...uleb(id(nodes.shift(), ctx.elem)))
633
745
  // array.copy $t $t
@@ -635,20 +747,18 @@ const instr = (nodes, ctx) => {
635
747
  }
636
748
  // ref.test|cast (ref null? $t|heaptype)
637
749
  else if (code >= 20 && code <= 23) {
638
- // FIXME: normalizer is supposed to resolve this
639
750
  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
751
+ if (ht[0] !== REFTYPE.ref) immed.push(code = immed.pop() + 1) // ref.test|cast (ref null $t) is next op
641
752
  if (ht.length > 1) ht.shift() // pop ref
642
753
  immed.push(...ht)
643
754
  }
644
755
  // br_on_cast[_fail] $l? (ref null? ht1) (ref null? ht2)
645
- // FIXME: normalizer should resolve anyref|etc to (ref null any|etc)
646
756
  else if (code === 24 || code === 25) {
647
757
  let i = blockid(nodes.shift(), ctx.block),
648
758
  ht1 = reftype(nodes.shift(), ctx),
649
759
  ht2 = reftype(nodes.shift(), ctx),
650
760
  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
761
+ immed.push(castflags, ...uleb(i), ht1.pop(), ht2.pop()) // we take only abstype or
652
762
  }
653
763
  }
654
764
 
@@ -658,14 +768,23 @@ const instr = (nodes, ctx) => {
658
768
  else if (code == 0xfc) {
659
769
  [, code] = immed
660
770
 
661
- // memory.init idx, data.drop idx,
662
- if (code === 0x08 || code === 0x09) {
771
+ // memory.init memidx dataidx (binary: dataidx memidx)
772
+ if (code === 0x08) {
773
+ let m = isImm(nodes[0]) ? nodes.shift() : 0, d = isImm(nodes[0]) ? nodes.shift() : 0
774
+ immed.push(...uleb(id(d, ctx.data)), ...uleb(id(m, ctx.memory)))
775
+ }
776
+ // data.drop idx
777
+ else if (code === 0x09) {
663
778
  immed.push(...uleb(id(nodes.shift(), ctx.data)))
664
779
  }
665
-
666
- // memory placeholders
667
- if (code == 0x08 || code == 0x0b) immed.push(0)
668
- else if (code === 0x0a) immed.push(0, 0)
780
+ // memory.copy dstmem srcmem
781
+ else if (code === 0x0a) {
782
+ immed.push(...uleb(id(isImm(nodes[0]) ? nodes.shift() : 0, ctx.memory)), ...uleb(id(isImm(nodes[0]) ? nodes.shift() : 0, ctx.memory)))
783
+ }
784
+ // memory.fill memidx
785
+ else if (code === 0x0b) {
786
+ immed.push(...uleb(id(isImm(nodes[0]) ? nodes.shift() : 0, ctx.memory)))
787
+ }
669
788
 
670
789
  // elem.drop elemidx
671
790
  if (code === 0x0d) {
@@ -744,7 +863,6 @@ const instr = (nodes, ctx) => {
744
863
  ctx.block.push(code)
745
864
 
746
865
  // (block $x) (loop $y) - save label pointer
747
- // FIXME: do in normalizer
748
866
  if (nodes[0]?.[0] === '$') ctx.block[nodes.shift()] = ctx.block.length
749
867
 
750
868
  let t = nodes.shift();
@@ -754,13 +872,8 @@ const instr = (nodes, ctx) => {
754
872
  // (result i32) - doesn't require registering type
755
873
  // FIXME: Make sure it is signed positive integer (leb, not uleb) https://webassembly.github.io/gc/core/binary/instructions.html#control-instructions
756
874
  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
- }
875
+ // (type idx)
876
+ else immed.push(...uleb(id(t[1], ctx.type)))
764
877
  }
765
878
  // else
766
879
  else if (code === 5) { }
@@ -851,10 +964,10 @@ const instr = (nodes, ctx) => {
851
964
  immed.push(...encode[op.split('.')[0]](nodes.shift()))
852
965
  }
853
966
 
854
- // memory.grow|size $idx - mandatory 0x00
967
+ // memory.grow|size memidx
855
968
  // https://webassembly.github.io/spec/core/binary/instructions.html#memory-instructions
856
969
  else if (code == 0x3f || code == 0x40) {
857
- immed.push(0)
970
+ immed.push(...uleb(id(isImm(nodes[0]) ? nodes.shift() : 0, ctx.memory)))
858
971
  }
859
972
 
860
973
  // table.get|set $id
@@ -862,6 +975,9 @@ const instr = (nodes, ctx) => {
862
975
  immed.push(...uleb(id(nodes.shift(), ctx.table)))
863
976
  }
864
977
 
978
+ // Insert metadata placeholder before instruction in flat form
979
+ if (ctx._meta) out.push(ctx._meta), ctx._meta = null
980
+
865
981
  out.push(...immed)
866
982
 
867
983
  return out
@@ -883,10 +999,12 @@ const blockid = (nm, block, i) => (
883
999
  // consume align/offset params
884
1000
  const memarg = (args) => {
885
1001
  let align, offset, k, v
886
- while (args[0]?.includes('=')) [k, v] = args.shift().split('='), k === 'offset' ? offset = +v : k === 'align' ? align = +v : err(`Unknown param ${k}=${v}`)
887
-
888
- if (offset < 0 || offset > 0xffffffff) err(`Bad offset ${offset}`)
889
- if (align <= 0 || align > 0xffffffff) err(`Bad align ${align}`)
1002
+ while (args[0]?.includes('=')) {
1003
+ [k, v] = args.shift().split('='), v = v.replaceAll('_', '')
1004
+ k === 'offset' ? offset = +v : k === 'align' ? align = +v : err(`Unknown param ${k}=${v}`)
1005
+ }
1006
+ if ((offset < 0 || offset > 0xffffffff)) err(`Bad offset ${offset}`)
1007
+ if ((align <= 0 || align > 0xffffffff)) err(`Bad align ${align}`)
890
1008
  if (align) ((align = Math.log2(align)) % 1) && err(`Bad align ${align}`)
891
1009
  return [align, offset]
892
1010
  }
@@ -915,20 +1033,5 @@ const limits = (node) => (
915
1033
  // we put extra condition for index ints for tests complacency
916
1034
  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
1035
 
918
-
919
- // escape codes
920
- const escape = { n: 10, r: 13, t: 9, v: 1, '"': 34, "'": 39, '\\': 92 }
921
-
922
- // build string binary
923
- const str = str => {
924
- let res = [], i = 0, c, BSLASH = 92
925
- // https://webassembly.github.io/spec/core/text/values.html#strings
926
- for (; i < str.length;) {
927
- c = str.charCodeAt(i++)
928
- res.push(c === BSLASH ? escape[str[i++]] || parseInt(str.slice(i - 1, ++i), 16) : c)
929
- }
930
- return res
931
- }
932
-
933
1036
  // serialize binary array
934
1037
  const vec = a => [...uleb(a.length), ...a.flat()]
package/src/const.js CHANGED
@@ -49,17 +49,19 @@ export const INSTR = [
49
49
  // relaxed SIMD instructions
50
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'
51
51
  ],
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 },
52
+ SECTION = { custom: 0, type: 1, import: 2, func: 3, table: 4, memory: 5, global: 6, tag: 13, export: 7, start: 8, elem: 9, datacount: 12, code: 10, data: 11 },
53
53
  RECTYPE = { sub: 0x50, subfinal: 0x4F, rec: 0x4E },
54
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 },
55
+ HEAPTYPE = { nofunc: 0x73, noextern: 0x72, noexn: 0x74, none: 0x71, func: 0x70, extern: 0x6F, exn: 0x75, any: 0x6E, eq: 0x6D, i31: 0x6C, struct: 0x6B, array: 0x6A },
56
56
  REFTYPE = {
57
57
  // absheaptype abbrs
58
58
  nullfuncref: HEAPTYPE.nofunc,
59
59
  nullexternref: HEAPTYPE.noextern,
60
+ nullexnref: HEAPTYPE.noexn,
60
61
  nullref: HEAPTYPE.none,
61
62
  funcref: HEAPTYPE.func,
62
63
  externref: HEAPTYPE.extern,
64
+ exnref: HEAPTYPE.exn,
63
65
  anyref: HEAPTYPE.any,
64
66
  eqref: HEAPTYPE.eq,
65
67
  i31ref: HEAPTYPE.i31,
@@ -70,4 +72,6 @@ export const INSTR = [
70
72
  ref: 0x64 /* -0x1c */, refnull: 0x63 /* -0x1d */
71
73
  },
72
74
  TYPE = { i8: 0x78, i16: 0x77, i32: 0x7f, i64: 0x7e, f32: 0x7d, f64: 0x7c, void: 0x40, v128: 0x7B, ...HEAPTYPE, ...REFTYPE },
73
- KIND = { func: 0, table: 1, memory: 2, global: 3 }
75
+ KIND = { func: 0, table: 1, memory: 2, global: 3, tag: 4 },
76
+ // WAT escape codes: https://webassembly.github.io/spec/core/text/values.html#strings
77
+ ESCAPE = { n: 10, r: 13, t: 9, v: 11, '"': 34, "'": 39, '\\': 92 }
package/src/parse.js CHANGED
@@ -1,5 +1,8 @@
1
+ import { unescape } from "./util.js"
2
+
1
3
  const OPAREN = 40, CPAREN = 41, OBRACK = 91, CBRACK = 93, SPACE = 32, DQUOTE = 34, PERIOD = 46,
2
- _0 = 48, _9 = 57, SEMIC = 59, NEWLINE = 32, PLUS = 43, MINUS = 45, COLON = 58, BSLASH = 39
4
+ _0 = 48, _9 = 57, SEMIC = 59, NEWLINE = 32, PLUS = 43, MINUS = 45, COLON = 58, BACKSLASH = 92, AT = 64
5
+
3
6
 
4
7
  /**
5
8
  * Parses a wasm text string and constructs a nested array structure (AST).
@@ -17,22 +20,23 @@ export default (str, o={ comments: false }) => {
17
20
  )
18
21
 
19
22
  const parseLevel = () => {
20
- for (let c, root, q; i < str.length;) {
23
+ for (let c, root, q, id; i < str.length;) {
21
24
 
22
25
  c = str.charCodeAt(i)
23
26
  if (q) {
24
27
  buf += str[i++]
25
- if (str[i-1] === '\\') buf += str[i++]
26
- else if (c === DQUOTE) commit(), q = 0
28
+ if (c === BACKSLASH) buf += str[i++]
29
+ else if (c === DQUOTE) id && (buf = '$' + unescape(buf)), commit(), q = id = 0
27
30
  }
28
31
  else if (c === DQUOTE) {
29
- commit(), q = c, buf += str[i++]
32
+ q = c, id = buf == '$', !id && commit(), buf = '"', i++
30
33
  }
31
34
  else if (c === OPAREN) {
32
35
  if (str.charCodeAt(i + 1) === SEMIC) comment = str.slice(i, i = str.indexOf(';)', i) + 2), o.comments && level.push(comment) // (; ... ;)
36
+ else if (str.charCodeAt(i + 1) === AT) commit(), i += 2, buf = '@', (root = level).push(level = []), parseLevel(), level = root // (@annotid ...)
33
37
  else commit(), i++, (root = level).push(level = []), parseLevel(), level = root
34
38
  }
35
- else if (c === SEMIC) comment = str.slice(i, i = str.indexOf('\n', i) + 1 || str.length), o.comments && level.push(comment) // ; ...
39
+ else if (c === SEMIC && str.charCodeAt(i + 1) === SEMIC) comment = str.slice(i, i = str.indexOf('\n', i) + 1 || str.length), o.comments && level.push(comment) // ;; ...
36
40
  else if (c <= SPACE) commit(), i++
37
41
  else if (c === CPAREN) return commit(), i++
38
42
  else buf += str[i++]
package/src/util.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { ESCAPE } from './const.js'
1
2
 
2
3
  export const err = text => { throw Error(text) }
3
4
 
@@ -6,3 +7,48 @@ export const clone = items => items.map(item => Array.isArray(item) ? clone(item
6
7
  export const sepRE = /^_|_$|[^\da-f]_|_[^\da-f]/i
7
8
 
8
9
  export const intRE = /^[+-]?(?:0x[\da-f]+|\d+)$/i
10
+
11
+ // build string binary - convert WAT string to byte array
12
+ const enc = new TextEncoder()
13
+ export const str = (...parts) => {
14
+ let s = parts.map(s => s[0] === '"' ? s.slice(1, -1) : s).join(''), res = []
15
+
16
+ for (let i = 0; i < s.length; i++) {
17
+ let c = s.charCodeAt(i)
18
+ if (c === 92) { // backslash
19
+ let n = s[i + 1]
20
+ // \u{...} unicode - decode and UTF-8 encode
21
+ if (n === 'u' && s[i + 2] === '{') {
22
+ let hex = s.slice(i + 3, i = s.indexOf('}', i + 3))
23
+ res.push(...enc.encode(String.fromCodePoint(parseInt(hex, 16))))
24
+ // i now points to '}', loop i++ will move past it
25
+ }
26
+ // Named escape
27
+ else if (ESCAPE[n]) {
28
+ res.push(ESCAPE[n])
29
+ i++ // skip the named char, loop i++ will move past backslash
30
+ }
31
+ // \xx hex byte (raw byte, not UTF-8 decoded)
32
+ else {
33
+ res.push(parseInt(s.slice(i + 1, i + 3), 16))
34
+ i += 2 // skip two hex digits, loop i++ will complete the skip
35
+ }
36
+ }
37
+ // Multi-byte char - UTF-8 encode
38
+ else if (c > 255) {
39
+ res.push(...enc.encode(s[i]))
40
+ }
41
+ // Raw byte
42
+ else res.push(c)
43
+ }
44
+ return res
45
+ }
46
+
47
+ /**
48
+ * Unescapes a WAT string literal by parsing escapes to bytes, then UTF-8 decoding.
49
+ * Reuses str() for escape parsing to eliminate duplication.
50
+ *
51
+ * @param {string} s - String with quotes and escapes, e.g. '"hello\\nworld"'
52
+ * @returns {string} Unescaped string without quotes, e.g. 'hello\nworld'
53
+ */
54
+ export const unescape = s => new TextDecoder().decode(new Uint8Array(str(s)))