watr 3.2.1 → 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.1",
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,12 +33,12 @@ 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
@@ -48,7 +53,7 @@ export default function watr(nodes) {
48
53
  // convert rec type into regular type (first subtype) with stashed subtypes length
49
54
  // add rest of subtypes as regular type nodes with subtype flag
50
55
  for (let i = 0; i < node.length; i++) {
51
- let [,...subnode] = node[i]
56
+ let [, ...subnode] = node[i]
52
57
  alias(subnode, ctx.type);
53
58
  (subnode = typedef(subnode, ctx)).push(i ? true : [ctx.type.length, node.length])
54
59
  ctx.type.push(subnode)
@@ -62,64 +67,75 @@ export default function watr(nodes) {
62
67
  alias(node, ctx.type);
63
68
  ctx.type.push(typedef(node, ctx));
64
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
+ }
65
74
  // other sections may have id
66
75
  else if (kind === 'start' || kind === 'export') ctx[kind].push(node)
67
76
 
68
77
  else return true
69
78
  })
70
79
 
71
- // prepare/normalize nodes
72
- .forEach(([kind, ...node]) => {
73
- let imported // if node needs to be imported
80
+ // prepare/normalize nodes
81
+ .forEach(([kind, ...node]) => {
82
+ let imported // if node needs to be imported
74
83
 
75
- // import abbr
76
- // (import m n (table|memory|global|func id? type)) -> (table|memory|global|func id? (import m n) type)
77
- if (kind === 'import') [kind, ...node] = (imported = node).pop()
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()
78
87
 
79
- // index, alias
80
- let items = ctx[kind];
81
- let name = alias(node, items);
88
+ // index, alias
89
+ let items = ctx[kind];
90
+ let name = alias(node, items);
82
91
 
83
- // export abbr
84
- // (table|memory|global|func id? (export n)* ...) -> (table|memory|global|func id ...) (export n (table|memory|global|func id))
85
- while (node[0]?.[0] === 'export') ctx.export.push([node.shift()[1], [kind, items.length]])
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]])
86
95
 
87
- // for import nodes - redirect output to import
88
- if (node[0]?.[0] === 'import') [, ...imported] = node.shift()
96
+ // for import nodes - redirect output to import
97
+ if (node[0]?.[0] === 'import') [, ...imported] = node.shift()
89
98
 
90
- // table abbr
91
- if (kind === 'table') {
92
- // (table id? reftype (elem ...{n})) -> (table id? n n reftype) (elem (table id) (i32.const 0) reftype ...)
93
- if (node[1]?.[0] === 'elem') {
94
- let [reftype, [, ...els]] = node
95
- node = [els.length, els.length, reftype]
96
- ctx.elem.push([['table', name || items.length], ['i32.const', '0'], reftype, ...els])
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
+ }
97
107
  }
98
- }
99
108
 
100
- // data abbr
101
- // (memory id? (data str)) -> (memory id? n n) (data (memory id) (i32.const 0) str)
102
- else if (kind === 'memory' && node[0]?.[0] === 'data') {
103
- let [, ...data] = node.shift(), m = '' + Math.ceil(data.map(s => s.slice(1, -1)).join('').length / 65536) // FIXME: figure out actual data size
104
- ctx.data.push([['memory', items.length], ['i32.const', 0], ...data])
105
- node = [m, m]
106
- }
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
+ }
107
116
 
108
- // dupe to code section, save implicit type
109
- else if (kind === 'func') {
110
- let [idx, param, result] = typeuse(node, ctx);
111
- idx ??= regtype(param, result, ctx)
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)
112
121
 
113
- // we save idx because type can be defined after
114
- !imported && ctx.code.push([[idx, param, result], ...plain(node, ctx)]) // pass param since they may have names
115
- node.unshift(['type', idx])
116
- }
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
+ }
126
+
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
+ }
117
133
 
118
- // import writes to import section amd adds placeholder for (kind) section
119
- if (imported) ctx.import.push([...imported, [kind, ...node]]), node = null
134
+ // import writes to import section amd adds placeholder for (kind) section
135
+ if (imported) ctx.import.push([...imported, [kind, ...node]]), node = null
120
136
 
121
- items.push(node)
122
- })
137
+ items?.push(node)
138
+ })
123
139
 
124
140
  // convert nodes to bytes
125
141
  const bin = (kind, count = true) => {
@@ -128,11 +144,14 @@ export default function watr(nodes) {
128
144
  .map(item => build[kind](item, ctx))
129
145
  .filter(Boolean) // filter out unrenderable things (subtype or data.length)
130
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
+
131
150
  return !items.length ? [] : [kind, ...vec(count ? vec(items) : items)]
132
151
  }
133
152
 
134
153
  // build final binary
135
- return Uint8Array.from([
154
+ const out = [
136
155
  0x00, 0x61, 0x73, 0x6d, // magic
137
156
  0x01, 0x00, 0x00, 0x00, // version
138
157
  ...bin(SECTION.custom),
@@ -141,20 +160,35 @@ export default function watr(nodes) {
141
160
  ...bin(SECTION.func),
142
161
  ...bin(SECTION.table),
143
162
  ...bin(SECTION.memory),
163
+ ...bin(SECTION.tag),
144
164
  ...bin(SECTION.global),
145
165
  ...bin(SECTION.export),
146
166
  ...bin(SECTION.start, false),
147
167
  ...bin(SECTION.elem),
148
168
  ...bin(SECTION.datacount, false),
149
- ...bin(SECTION.code),
150
- ...bin(SECTION.data)
151
- ])
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)
152
186
  }
153
187
 
154
188
  // consume name eg. $t ...
155
189
  const alias = (node, list) => {
156
190
  let name = (node[0]?.[0] === '$' || node[0]?.[0] == null) && node.shift();
157
- if (name) name in list ? err(`Duplicate ${list.name} ${name}`) : list[name] = list.length; // save alias
191
+ if (name && list) name in list ? err(`Duplicate ${list.name} ${name}`) : list[name] = list.length; // save alias
158
192
  return name
159
193
  }
160
194
 
@@ -179,7 +213,7 @@ const typedef = ([dfn], ctx) => {
179
213
  }
180
214
 
181
215
  // register (implicit) type
182
- const regtype = (param, result, ctx, idx='$' + param + '>' + result) => (
216
+ const regtype = (param, result, ctx, idx = '$' + param + '>' + result) => (
183
217
  (ctx.type[idx] ??= ctx.type.push(['func', [param, result]]) - 1),
184
218
  idx
185
219
  )
@@ -194,7 +228,7 @@ const typeuse = (nodes, ctx, names) => {
194
228
  [, idx] = nodes.shift();
195
229
  [param, result] = paramres(nodes, names);
196
230
 
197
- const [,srcParamRes] = ctx.type[id(idx, ctx.type)] ?? err(`Unknown type ${idx}`)
231
+ const [, srcParamRes] = ctx.type[id(idx, ctx.type)] ?? err(`Unknown type ${idx}`)
198
232
 
199
233
  // check type consistency (excludes forward refs)
200
234
  if ((param.length || result.length) && srcParamRes.join('>') !== param + '>' + result) err(`Type ${idx} mismatch`)
@@ -259,10 +293,20 @@ const blocktype = (nodes, ctx) => {
259
293
  // https://webassembly.github.io/spec/core/text/instructions.html#folded-instructions
260
294
  const plain = (nodes, ctx) => {
261
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'
262
298
 
263
299
  while (nodes.length) {
264
300
  let node = nodes.shift()
265
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
+
266
310
  // lookup is slower than sequence of known ifs
267
311
  if (typeof node === 'string') {
268
312
  out.push(node)
@@ -278,7 +322,7 @@ const plain = (nodes, ctx) => {
278
322
  // else $label
279
323
  // end $label - make sure it matches block label
280
324
  else if (node === 'else' || node === 'end') {
281
- 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}`)
282
326
  }
283
327
 
284
328
  // select (result i32 i32 i32)?
@@ -297,6 +341,18 @@ const plain = (nodes, ctx) => {
297
341
  // mark datacount section as required
298
342
  else if (node === 'memory.init' || node === 'data.drop' || node === 'array.new_data' || node === 'array.init_data') {
299
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)
300
356
  }
301
357
 
302
358
  // table.init tableidx? elemidx -> table.init tableidx elemidx
@@ -319,6 +375,9 @@ const plain = (nodes, ctx) => {
319
375
 
320
376
  // (if ...) -> if ... end
321
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
+
322
381
  let then = [], els = [], immed = [node.shift()]
323
382
  // (if label? blocktype? cond*? (then instr*) (else instr*)?) -> cond*? if label? blocktype? instr* else instr*? end
324
383
  // https://webassembly.github.io/spec/core/text/instructions.html#control-instructions
@@ -338,7 +397,8 @@ const plain = (nodes, ctx) => {
338
397
 
339
398
  if (typeof node[0] === 'string') err('Unfolded condition')
340
399
 
341
- 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')
342
402
  }
343
403
  else out.push(plain(node, ctx))
344
404
  }
@@ -349,7 +409,21 @@ const plain = (nodes, ctx) => {
349
409
 
350
410
 
351
411
  // build section binary [by section codes] (non consuming)
352
- const build = [,
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)
422
+ }
423
+
424
+ // Custom section format: name (vec string) + raw content bytes
425
+ return [...vec(str(name)), ...str(...data)]
426
+ },
353
427
  // type kinds
354
428
  // (func params result)
355
429
  // (array i8)
@@ -392,6 +466,10 @@ const build = [,
392
466
  let [[, typeidx]] = dfn
393
467
  details = uleb(id(typeidx, ctx.type))
394
468
  }
469
+ else if (kind === 'tag') {
470
+ let [[, typeidx]] = dfn
471
+ details = [0x00, ...uleb(id(typeidx, ctx.type))]
472
+ }
395
473
  else if (kind === 'memory') {
396
474
  details = limits(dfn)
397
475
  }
@@ -403,7 +481,7 @@ const build = [,
403
481
  }
404
482
  else err(`Unknown kind ${kind}`)
405
483
 
406
- 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])
407
485
  },
408
486
 
409
487
  // (func $name? ...params result ...body)
@@ -422,7 +500,7 @@ const build = [,
422
500
  ([t, init], ctx) => [...fieldtype(t, ctx), ...expr(init, ctx)],
423
501
 
424
502
  // (export "name" (func|table|mem $name|idx))
425
- ([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]))]),
426
504
 
427
505
  // (start $main)
428
506
  ([l], ctx) => uleb(id(l, ctx.func)),
@@ -523,6 +601,10 @@ const build = [,
523
601
  ctx.local.name = 'local'
524
602
  ctx.block.name = 'block'
525
603
 
604
+ // Track current code index for code metadata
605
+ if (ctx._codeIdx === undefined) ctx._codeIdx = 0
606
+ let codeIdx = ctx._codeIdx++
607
+
526
608
  // collect locals
527
609
  while (body[0]?.[0] === 'local') {
528
610
  let [, ...types] = body.shift()
@@ -534,10 +616,21 @@ const build = [,
534
616
  ctx.local.push(...types)
535
617
  }
536
618
 
619
+ ctx._meta = null
537
620
  const bytes = []
538
621
  while (body.length) bytes.push(...instr(body, ctx))
539
622
  bytes.push(0x0b)
540
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
+
541
634
  // squash locals into (n:u32 t:valtype)*, n is number and t is type
542
635
  // we skip locals provided by params
543
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), [])
@@ -546,7 +639,7 @@ const build = [,
546
639
  ctx.local = ctx.block = null
547
640
 
548
641
  // https://webassembly.github.io/spec/core/binary/modules.html#code-section
549
- 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])
550
643
  },
551
644
 
552
645
  // (data (i32.const 0) "\aa" "\bb"?)
@@ -562,10 +655,10 @@ const build = [,
562
655
  }
563
656
 
564
657
  // (offset (i32.const 0)) or (i32.const 0)
565
- if (typeof inits[0] !== 'string') {
658
+ if (typeof inits[0] !== 'string' && inits[0]) {
566
659
  offset = inits.shift()
567
- if (offset[0] === 'offset') [, offset] = offset
568
- offset ?? err('Bad offset', offset)
660
+ if (offset?.[0] === 'offset') [, offset] = offset
661
+ else offset ?? err('Bad offset', offset)
569
662
  }
570
663
 
571
664
  return ([
@@ -577,7 +670,7 @@ const build = [,
577
670
  // passive: 1
578
671
  [1]
579
672
  ),
580
- ...vec(str(inits.map(i => i.slice(1, -1)).join('')))
673
+ ...vec(str(...inits))
581
674
  ])
582
675
  },
583
676
 
@@ -585,6 +678,9 @@ const build = [,
585
678
  (nodes, ctx) => uleb(ctx.data.length)
586
679
  ]
587
680
 
681
+ // (tag $id? (param i32)*) - tags for exception handling
682
+ build[SECTION.tag] = ([[, typeidx]], ctx) => [0x00, ...uleb(id(typeidx, ctx.type))]
683
+
588
684
  // build reftype, either direct absheaptype or wrapped heaptype https://webassembly.github.io/gc/core/binary/types.html#reference-types
589
685
  const reftype = (t, ctx) => (
590
686
  t[0] === 'ref' ?
@@ -599,17 +695,26 @@ const reftype = (t, ctx) => (
599
695
  const fieldtype = (t, ctx, mut = t[0] === 'mut' ? 1 : 0) => [...reftype(mut ? t[1] : t, ctx), mut];
600
696
 
601
697
 
602
-
603
698
  // consume one instruction from nodes sequence
604
699
  const instr = (nodes, ctx) => {
605
700
  if (!nodes?.length) return []
606
701
 
607
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
+ }
608
711
 
609
712
  // consume group
610
713
  if (Array.isArray(op)) {
611
714
  immed = instr(op, ctx)
612
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
613
718
  out.push(...immed)
614
719
  return out
615
720
  }
@@ -632,7 +737,9 @@ const instr = (nodes, ctx) => {
632
737
  // array.new_fixed $t n
633
738
  else if (code === 8) immed.push(...uleb(nodes.shift()))
634
739
  // array.new_data|init_data $t $d
635
- 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
+ }
636
743
  // array.new_elem|init_elem $t $e
637
744
  else if (code === 10 || code === 19) immed.push(...uleb(id(nodes.shift(), ctx.elem)))
638
745
  // array.copy $t $t
@@ -641,7 +748,7 @@ const instr = (nodes, ctx) => {
641
748
  // ref.test|cast (ref null? $t|heaptype)
642
749
  else if (code >= 20 && code <= 23) {
643
750
  let ht = reftype(nodes.shift(), ctx)
644
- if (ht[0] !== REFTYPE.ref) immed.push(code = immed.pop()+1) // ref.test|cast (ref null $t) is next op
751
+ if (ht[0] !== REFTYPE.ref) immed.push(code = immed.pop() + 1) // ref.test|cast (ref null $t) is next op
645
752
  if (ht.length > 1) ht.shift() // pop ref
646
753
  immed.push(...ht)
647
754
  }
@@ -651,7 +758,7 @@ const instr = (nodes, ctx) => {
651
758
  ht1 = reftype(nodes.shift(), ctx),
652
759
  ht2 = reftype(nodes.shift(), ctx),
653
760
  castflags = ((ht2[0] !== REFTYPE.ref) << 1) | (ht1[0] !== REFTYPE.ref)
654
- immed.push(castflags, ...uleb(i), ht1.pop(), ht2.pop()) // we take only abstype or
761
+ immed.push(castflags, ...uleb(i), ht1.pop(), ht2.pop()) // we take only abstype or
655
762
  }
656
763
  }
657
764
 
@@ -661,14 +768,23 @@ const instr = (nodes, ctx) => {
661
768
  else if (code == 0xfc) {
662
769
  [, code] = immed
663
770
 
664
- // memory.init idx, data.drop idx,
665
- 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) {
666
778
  immed.push(...uleb(id(nodes.shift(), ctx.data)))
667
779
  }
668
-
669
- // memory placeholders
670
- if (code == 0x08 || code == 0x0b) immed.push(0)
671
- 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
+ }
672
788
 
673
789
  // elem.drop elemidx
674
790
  if (code === 0x0d) {
@@ -848,10 +964,10 @@ const instr = (nodes, ctx) => {
848
964
  immed.push(...encode[op.split('.')[0]](nodes.shift()))
849
965
  }
850
966
 
851
- // memory.grow|size $idx - mandatory 0x00
967
+ // memory.grow|size memidx
852
968
  // https://webassembly.github.io/spec/core/binary/instructions.html#memory-instructions
853
969
  else if (code == 0x3f || code == 0x40) {
854
- immed.push(0)
970
+ immed.push(...uleb(id(isImm(nodes[0]) ? nodes.shift() : 0, ctx.memory)))
855
971
  }
856
972
 
857
973
  // table.get|set $id
@@ -859,6 +975,9 @@ const instr = (nodes, ctx) => {
859
975
  immed.push(...uleb(id(nodes.shift(), ctx.table)))
860
976
  }
861
977
 
978
+ // Insert metadata placeholder before instruction in flat form
979
+ if (ctx._meta) out.push(ctx._meta), ctx._meta = null
980
+
862
981
  out.push(...immed)
863
982
 
864
983
  return out
@@ -880,10 +999,12 @@ const blockid = (nm, block, i) => (
880
999
  // consume align/offset params
881
1000
  const memarg = (args) => {
882
1001
  let align, offset, k, v
883
- while (args[0]?.includes('=')) [k, v] = args.shift().split('='), k === 'offset' ? offset = +v : k === 'align' ? align = +v : err(`Unknown param ${k}=${v}`)
884
-
885
- if (offset < 0 || offset > 0xffffffff) err(`Bad offset ${offset}`)
886
- 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}`)
887
1008
  if (align) ((align = Math.log2(align)) % 1) && err(`Bad align ${align}`)
888
1009
  return [align, offset]
889
1010
  }
@@ -912,20 +1033,5 @@ const limits = (node) => (
912
1033
  // we put extra condition for index ints for tests complacency
913
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
914
1035
 
915
-
916
- // escape codes
917
- const escape = { n: 10, r: 13, t: 9, v: 1, '"': 34, "'": 39, '\\': 92 }
918
-
919
- // build string binary
920
- const str = str => {
921
- let res = [], i = 0, c, BSLASH = 92
922
- // https://webassembly.github.io/spec/core/text/values.html#strings
923
- for (; i < str.length;) {
924
- c = str.charCodeAt(i++)
925
- res.push(c === BSLASH ? escape[str[i++]] || parseInt(str.slice(i - 1, ++i), 16) : c)
926
- }
927
- return res
928
- }
929
-
930
1036
  // serialize binary array
931
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)))