watr 4.7.2 → 5.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/watr.js +876 -4780
- package/dist/watr.min.js +5 -6
- package/dist/watr.wasm +0 -0
- package/package.json +13 -3
- package/readme.md +28 -10
- package/src/compile.js +227 -109
- package/src/const.js +125 -131
- package/src/encode.js +28 -3
- package/src/optimize.js +2909 -421
- package/src/template.js +23 -5
- package/src/util.js +33 -9
- package/types/src/compile.d.ts +1 -0
- package/types/src/compile.d.ts.map +1 -1
- package/types/src/const.d.ts +2 -1
- package/types/src/const.d.ts.map +1 -1
- package/types/src/encode.d.ts +5 -26
- package/types/src/encode.d.ts.map +1 -1
- package/types/src/optimize.d.ts +41 -33
- package/types/src/optimize.d.ts.map +1 -1
- package/types/src/template.d.ts.map +1 -1
- package/types/src/util.d.ts +5 -10
- package/types/src/util.d.ts.map +1 -1
- package/types/watr.d.ts +4 -5
- package/types/watr.d.ts.map +1 -1
- package/watr.js +10 -7
package/readme.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
_Light & fast WAT compiler_
|
|
5
5
|
|
|
6
6
|
* [feature](https://webassembly.org/features/?categories=browsers%2Cstandalones%2Ctools) & [spec](https://webassembly.github.io/spec/core/text/index.html)-complete, zero deps
|
|
7
|
-
* [compile](./docs.md#compilesource
|
|
7
|
+
* [compile](./docs.md#compilesource) · [polyfill](./docs.md#polyfillast-options) · [optimize](./docs.md#optimizeast-options) · [parse](./docs.md#parsesource-options) · [prettify](./docs.md#printtree-options) · [minify](./docs.md#printtree-options)
|
|
8
8
|
* instant wasm, JS interop, CLI, clear errors
|
|
9
9
|
|
|
10
10
|
**[docs](./docs.md)** **·** **[demo](https://dy.github.io/watr/play/)**
|
|
@@ -19,13 +19,16 @@ _Light & fast WAT compiler_
|
|
|
19
19
|
import watr, { compile, parse, print } from 'watr'
|
|
20
20
|
|
|
21
21
|
// compile to binary
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
optimize: true // fold constants, treeshake, eliminate dead code ...
|
|
25
|
-
})
|
|
22
|
+
const src = '(func (export "f") (result f64) (f64.const 1))'
|
|
23
|
+
const binary = compile(src)
|
|
26
24
|
const module = new WebAssembly.Module(binary)
|
|
27
25
|
const { f } = new WebAssembly.Instance(module).exports
|
|
28
26
|
|
|
27
|
+
// optional, heavy transforms ship as separate entries — compose them
|
|
28
|
+
import optimize from 'watr/optimize' // fold constants, treeshake, eliminate dead code …
|
|
29
|
+
import polyfill from 'watr/polyfill' // newer features → MVP
|
|
30
|
+
compile(optimize(polyfill(src)))
|
|
31
|
+
|
|
29
32
|
// parse
|
|
30
33
|
parse('(i32.const 42)') // ['i32.const', 42]
|
|
31
34
|
|
|
@@ -56,11 +59,26 @@ npx watr input.wat --polyfill # newer features → MVP
|
|
|
56
59
|
|
|
57
60
|
## Metrics
|
|
58
61
|
|
|
59
|
-
* **watr** — ~
|
|
60
|
-
* [spec/wast.js](https://github.com/WebAssembly/spec) — 216 KB,
|
|
61
|
-
* [wabt](https://github.com/WebAssembly/wabt) — 282 KB,
|
|
62
|
-
* [binaryen](https://github.com/WebAssembly/binaryen) — 1,100 KB,
|
|
63
|
-
* [wat-compiler](https://github.com/stagas/wat-compiler) — ~152 KB (+ wabt dep),
|
|
62
|
+
* **watr** — ~44 KB minified (~14 KB gzipped), 3,123 op/s
|
|
63
|
+
* [spec/wast.js](https://github.com/WebAssembly/spec) — 216 KB, 1,509 op/s
|
|
64
|
+
* [wabt](https://github.com/WebAssembly/wabt) — 282 KB, 859 op/s
|
|
65
|
+
* [binaryen](https://github.com/WebAssembly/binaryen) — 1,100 KB, 473 op/s
|
|
66
|
+
* [wat-compiler](https://github.com/stagas/wat-compiler) — ~152 KB (+ wabt dep), 355 op/s
|
|
67
|
+
|
|
68
|
+
### Optimizer vs binaryen (wasm-opt 128)
|
|
69
|
+
|
|
70
|
+
Measured on [test/example](./test/example) (21 modules, `optimize(src)` defaults vs `wasm-opt -all`):
|
|
71
|
+
|
|
72
|
+
| | size (total) | time (batch) | footprint |
|
|
73
|
+
|---|---|---|---|
|
|
74
|
+
| **watr/optimize** | **19,734 B** | **~110 ms** in-process | 149 KB min (46 KB gz) |
|
|
75
|
+
| `wasm-opt -Oz` | 19,852 B | 990 ms CLI | ~1.1 MB js / native binary |
|
|
76
|
+
| `wasm-opt -O3` | 22,302 B | — | — |
|
|
77
|
+
|
|
78
|
+
Smaller than `-O3` on every module and than `-Oz` in aggregate (19,734 vs 19,852 B); ties or beats `-Oz` on 18 of 20 files.
|
|
79
|
+
On a 5.5 MB module watr's output is 1.7% SMALLER than `-Oz` (261,345 vs 265,977 B — cross-function outlining);
|
|
80
|
+
binaryen's native core is faster there (0.44 s vs ~2.6 s optimize + 0.19 s compile) — watr's edge is output size, batch/in-process use
|
|
81
|
+
with no process spawn, and a ~25× smaller footprint.
|
|
64
82
|
|
|
65
83
|
|
|
66
84
|
## Used by
|
package/src/compile.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import * as encode from './encode.js'
|
|
2
2
|
import { uleb, i32, i64 } from './encode.js'
|
|
3
|
-
import { SECTION, TYPE, KIND,
|
|
3
|
+
import { SECTION, TYPE, KIND, OPCODE, IMM, DEFTYPE } from './const.js'
|
|
4
4
|
import parse from './parse.js'
|
|
5
|
-
import { err, unescape, str } from './util.js'
|
|
5
|
+
import { err, unescape, str, setErrLoc, setErrSrc, getErrLoc, getErrSrc } from './util.js'
|
|
6
6
|
|
|
7
7
|
|
|
8
8
|
/**
|
|
@@ -42,9 +42,9 @@ const cleanup = (node, result) => {
|
|
|
42
42
|
// reason to exist (the optimizer's size-revert guard can't afford to build bytes it discards).
|
|
43
43
|
function assemble(nodes, sizeOnly) {
|
|
44
44
|
// normalize to (module ...) form
|
|
45
|
-
if (typeof nodes === 'string')
|
|
46
|
-
else
|
|
47
|
-
|
|
45
|
+
if (typeof nodes === 'string') setErrSrc(nodes), nodes = parse(nodes) || []
|
|
46
|
+
else setErrSrc('') // clear source if AST passed directly
|
|
47
|
+
setErrLoc(0)
|
|
48
48
|
|
|
49
49
|
nodes = isDroppable(nodes) ? [] : (cleanup(nodes) ?? [])
|
|
50
50
|
|
|
@@ -84,7 +84,7 @@ function assemble(nodes, sizeOnly) {
|
|
|
84
84
|
nodes.slice(idx).filter((n) => {
|
|
85
85
|
if (!Array.isArray(n)) {
|
|
86
86
|
// find token as standalone word (not substring of another token)
|
|
87
|
-
let pos =
|
|
87
|
+
let pos = getErrLoc(), src = getErrSrc(), c
|
|
88
88
|
while ((pos = src.indexOf(n, pos)) >= 0) {
|
|
89
89
|
c = src.charCodeAt(pos - 1)
|
|
90
90
|
// check not preceded by word char or $
|
|
@@ -94,11 +94,11 @@ function assemble(nodes, sizeOnly) {
|
|
|
94
94
|
if (c > 47 && c < 58 || c > 64 && c < 91 || c > 96 && c < 123 || c === 95) { pos++; continue }
|
|
95
95
|
break
|
|
96
96
|
}
|
|
97
|
-
if (pos >= 0)
|
|
97
|
+
if (pos >= 0) setErrLoc(pos)
|
|
98
98
|
err(`Unexpected token ${n}`)
|
|
99
99
|
}
|
|
100
100
|
let [kind, ...node] = n
|
|
101
|
-
|
|
101
|
+
setErrLoc(n.loc) // track position for errors
|
|
102
102
|
// (@custom "name" placement? data) - custom section support
|
|
103
103
|
if (kind === '@custom') {
|
|
104
104
|
ctx.custom.push(node)
|
|
@@ -141,7 +141,7 @@ function assemble(nodes, sizeOnly) {
|
|
|
141
141
|
// prepare/normalize nodes
|
|
142
142
|
.forEach((n) => {
|
|
143
143
|
let [kind, ...node] = n
|
|
144
|
-
|
|
144
|
+
setErrLoc(n.loc) // track position for errors
|
|
145
145
|
let imported // if node needs to be imported
|
|
146
146
|
|
|
147
147
|
// import abbr
|
|
@@ -257,7 +257,15 @@ function assemble(nodes, sizeOnly) {
|
|
|
257
257
|
// inline bin(code) so the per-function item bytes survive — with ctx.codeSizePrefix
|
|
258
258
|
// they let us lift code-metadata positions to absolute binary offsets below
|
|
259
259
|
const codeItems = ctx.code.filter(Boolean).map(item => build[SECTION.code](item, ctx)).filter(Boolean)
|
|
260
|
-
|
|
260
|
+
// fused vec(vec(items)): count + items appended once, then the section frame —
|
|
261
|
+
// the generic path copies the multi-MB stream twice more
|
|
262
|
+
let codeSection = []
|
|
263
|
+
if (codeItems.length) {
|
|
264
|
+
const inner = uleb(codeItems.length)
|
|
265
|
+
for (const it of codeItems) for (let i = 0; i < it.length; i++) inner.push(it[i])
|
|
266
|
+
codeSection = [SECTION.code, ...uleb(inner.length)]
|
|
267
|
+
for (let i = 0; i < inner.length; i++) codeSection.push(inner[i])
|
|
268
|
+
}
|
|
261
269
|
const metaSection = binMeta()
|
|
262
270
|
const dataSection = bin(SECTION.data)
|
|
263
271
|
const stringsSection = ctx.strings.length ? [SECTION.strings, ...vec([0x00, ...vec(ctx.strings.map(s => vec(s)))])] : []
|
|
@@ -282,12 +290,14 @@ function assemble(nodes, sizeOnly) {
|
|
|
282
290
|
dataSection
|
|
283
291
|
]
|
|
284
292
|
|
|
285
|
-
// build final binary
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
])
|
|
293
|
+
// build final binary — sections are flat byte arrays; copy them into place
|
|
294
|
+
// instead of flattening a multi-MB nested array through Uint8Array.from
|
|
295
|
+
let total = 8
|
|
296
|
+
for (const sec of sections) total += sec.length
|
|
297
|
+
const wasm = new Uint8Array(total)
|
|
298
|
+
wasm.set([0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00])
|
|
299
|
+
let off = 8
|
|
300
|
+
for (const sec of sections) { if (sec.length) { wasm.set(sec, off); off += sec.length } }
|
|
291
301
|
|
|
292
302
|
// Lift any (@metadata.code.*) annotations to absolute binary offsets and hang
|
|
293
303
|
// them off the result, so a source-map emitter can correlate wasm back to
|
|
@@ -360,42 +370,65 @@ const isMemParam = n => n?.[0] === 'a' || n?.[0] === 'o'
|
|
|
360
370
|
* @param {Object} ctx - Compilation context with type info
|
|
361
371
|
* @returns {Array} Flattened instruction sequence
|
|
362
372
|
*/
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
373
|
+
// bare-token classification — one lookup replaces a six-way string-scan chain
|
|
374
|
+
// (built lazily over OPCODE's key set; unknown tokens fall through untouched)
|
|
375
|
+
let NCLS = null
|
|
376
|
+
const nclsOf = (op) => {
|
|
377
|
+
if (!NCLS) {
|
|
378
|
+
NCLS = Object.create(null)
|
|
379
|
+
for (const k in OPCODE) {
|
|
380
|
+
if (k === 'block' || k === 'if' || k === 'loop') NCLS[k] = 1
|
|
381
|
+
else if (k === 'else' || k === 'end') NCLS[k] = 2
|
|
382
|
+
else if (k === 'select') NCLS[k] = 3
|
|
383
|
+
else if (k.endsWith('call_indirect')) NCLS[k] = 4
|
|
384
|
+
else if (k === 'table.init') NCLS[k] = 5
|
|
385
|
+
else if (k === 'table.copy' || k === 'memory.copy') NCLS[k] = 6
|
|
386
|
+
else if (k.startsWith('table.')) NCLS[k] = 7
|
|
387
|
+
else if (k === 'memory.init') NCLS[k] = 8
|
|
388
|
+
else if (k === 'data.drop' || k === 'array.new_data' || k === 'array.init_data') NCLS[k] = 9
|
|
389
|
+
else if (k.startsWith('memory.') || k.endsWith('load') || k.endsWith('store')) NCLS[k] = 10
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
return NCLS[op]
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
function normalize(nodes, ctx, out = [], owned = false) {
|
|
396
|
+
if (!owned) nodes = [...nodes]
|
|
366
397
|
while (nodes.length) {
|
|
367
398
|
let node = nodes.shift()
|
|
368
399
|
if (typeof node === 'string') {
|
|
369
400
|
out.push(node)
|
|
370
|
-
|
|
401
|
+
const cls = nclsOf(node)
|
|
402
|
+
if (cls === undefined) continue
|
|
403
|
+
if (cls === 1) {
|
|
371
404
|
if (isId(nodes[0])) out.push(nodes.shift())
|
|
372
405
|
out.push(blocktype(nodes, ctx))
|
|
373
406
|
}
|
|
374
|
-
else if (
|
|
407
|
+
else if (cls === 2) {
|
|
375
408
|
if (isId(nodes[0])) nodes.shift()
|
|
376
409
|
}
|
|
377
|
-
else if (
|
|
378
|
-
else if (
|
|
410
|
+
else if (cls === 3) out.push(paramres(nodes)[1])
|
|
411
|
+
else if (cls === 4) {
|
|
379
412
|
let tableidx = isIdx(nodes[0]) ? nodes.shift() : 0, [idx, param, result] = typeuse(nodes, ctx)
|
|
380
413
|
out.push(tableidx, ['type', idx ?? regtype(param, result, ctx)])
|
|
381
414
|
}
|
|
382
|
-
else if (
|
|
383
|
-
else if (
|
|
384
|
-
else if (
|
|
385
|
-
else if (
|
|
415
|
+
else if (cls === 5) out.push(isIdx(nodes[1]) ? nodes.shift() : 0, nodes.shift())
|
|
416
|
+
else if (cls === 6) out.push(isIdx(nodes[0]) ? nodes.shift() : 0, isIdx(nodes[0]) ? nodes.shift() : 0)
|
|
417
|
+
else if (cls === 7) out.push(isIdx(nodes[0]) ? nodes.shift() : 0)
|
|
418
|
+
else if (cls === 8) {
|
|
386
419
|
out.push(...(isIdx(nodes[1]) ? [nodes.shift(), nodes.shift()].reverse() : [nodes.shift(), 0]))
|
|
387
420
|
ctx.datacount && (ctx.datacount[0] = true)
|
|
388
421
|
}
|
|
389
|
-
else if (
|
|
422
|
+
else if (cls === 9) {
|
|
390
423
|
node === 'data.drop' && out.push(nodes.shift())
|
|
391
424
|
ctx.datacount && (ctx.datacount[0] = true)
|
|
392
425
|
}
|
|
393
426
|
// memory.* instructions and load/store with optional memory index
|
|
394
|
-
else if (
|
|
427
|
+
else if (isIdx(nodes[0])) out.push(nodes.shift())
|
|
395
428
|
}
|
|
396
429
|
else if (Array.isArray(node)) {
|
|
397
430
|
let op = node[0]
|
|
398
|
-
node.loc != null && (
|
|
431
|
+
node.loc != null && setErrLoc(node.loc) // track position for errors
|
|
399
432
|
|
|
400
433
|
// code metadata annotations - pass through as marker with metadata type and data
|
|
401
434
|
// (@metadata.code.<type> data:str)
|
|
@@ -405,23 +438,29 @@ function normalize(nodes, ctx) {
|
|
|
405
438
|
continue
|
|
406
439
|
}
|
|
407
440
|
|
|
408
|
-
// Check if node is a valid instruction (string with
|
|
409
|
-
if (typeof op !== 'string' ||
|
|
441
|
+
// Check if node is a valid instruction (string with a known opcode)
|
|
442
|
+
if (typeof op !== 'string' || typeof OPCODE[op] !== 'number') { out.push(node); continue }
|
|
410
443
|
const parts = node.slice(1)
|
|
411
444
|
if (op === 'block' || op === 'loop') {
|
|
412
445
|
out.push(op)
|
|
413
446
|
if (isId(parts[0])) out.push(parts.shift())
|
|
414
|
-
out.push(blocktype(parts, ctx)
|
|
447
|
+
out.push(blocktype(parts, ctx))
|
|
448
|
+
normalize(parts, ctx, out, true)
|
|
449
|
+
out.push('end')
|
|
415
450
|
}
|
|
416
451
|
else if (op === 'if') {
|
|
452
|
+
// then/else normalize BEFORE the condition but EMIT after it — the temp
|
|
453
|
+
// arrays preserve the original type-registration order exactly
|
|
417
454
|
let then = [], els = []
|
|
418
|
-
if (parts.at(-1)?.[0] === 'else') els = normalize(parts.pop().slice(1), ctx)
|
|
419
|
-
if (parts.at(-1)?.[0] === 'then') then = normalize(parts.pop().slice(1), ctx)
|
|
455
|
+
if (parts.at(-1)?.[0] === 'else') els = normalize(parts.pop().slice(1), ctx, [], true)
|
|
456
|
+
if (parts.at(-1)?.[0] === 'then') then = normalize(parts.pop().slice(1), ctx, [], true)
|
|
420
457
|
let immed = [op]
|
|
421
458
|
if (isId(parts[0])) immed.push(parts.shift())
|
|
422
459
|
immed.push(blocktype(parts, ctx))
|
|
423
|
-
|
|
424
|
-
|
|
460
|
+
normalize(parts, ctx, out, true)
|
|
461
|
+
for (let i = 0; i < immed.length; i++) out.push(immed[i])
|
|
462
|
+
for (let i = 0; i < then.length; i++) out.push(then[i])
|
|
463
|
+
if (els.length) { out.push('else'); for (let i = 0; i < els.length; i++) out.push(els[i]) }
|
|
425
464
|
out.push('end')
|
|
426
465
|
}
|
|
427
466
|
else if (op === 'try_table') {
|
|
@@ -432,20 +471,23 @@ function normalize(nodes, ctx) {
|
|
|
432
471
|
while (parts[0]?.[0] === 'catch' || parts[0]?.[0] === 'catch_ref' || parts[0]?.[0] === 'catch_all' || parts[0]?.[0] === 'catch_all_ref') {
|
|
433
472
|
out.push(parts.shift())
|
|
434
473
|
}
|
|
435
|
-
|
|
474
|
+
normalize(parts, ctx, out, true)
|
|
475
|
+
out.push('end')
|
|
436
476
|
}
|
|
437
477
|
else if (op === 'ref.test' || op === 'ref.cast') {
|
|
438
478
|
const type = parts[0]
|
|
439
479
|
const isNullable = !Array.isArray(type) || type[1] === 'null' || type[0] !== 'ref'
|
|
440
480
|
if (isNullable) op += '_null'
|
|
441
|
-
|
|
481
|
+
normalize(parts.slice(1), ctx, out, true)
|
|
482
|
+
out.push(op, type)
|
|
442
483
|
nodes.unshift(...out.splice(out.length - 2))
|
|
443
484
|
}
|
|
444
485
|
else {
|
|
445
486
|
const imm = []
|
|
446
487
|
// Collect immediate operands (non-arrays or special forms like type/param/result/ref)
|
|
447
488
|
while (parts.length && (!Array.isArray(parts[0]) || parts[0].valueOf !== Array.prototype.valueOf || 'type,param,result,ref,exact,on'.includes(parts[0][0]))) imm.push(parts.shift())
|
|
448
|
-
|
|
489
|
+
normalize(parts, ctx, out, true)
|
|
490
|
+
out.push(op, ...imm)
|
|
449
491
|
nodes.unshift(...out.splice(out.length - 1 - imm.length))
|
|
450
492
|
}
|
|
451
493
|
} else out.push(node)
|
|
@@ -819,10 +861,10 @@ const build = [
|
|
|
819
861
|
ctx.local = ctx.block = ctx.meta = null
|
|
820
862
|
|
|
821
863
|
// https://webassembly.github.io/spec/core/binary/modules.html#code-section
|
|
822
|
-
const item =
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
864
|
+
const item = uleb(locals.length + bytes.length)
|
|
865
|
+
;(ctx.codeSizePrefix ??= [])[codeIdx] = item.length // = vec prefix width
|
|
866
|
+
for (let i = 0; i < locals.length; i++) item.push(locals[i])
|
|
867
|
+
for (let i = 0; i < bytes.length; i++) item.push(bytes[i])
|
|
826
868
|
return item
|
|
827
869
|
},
|
|
828
870
|
|
|
@@ -894,15 +936,19 @@ const fieldtype = (t, ctx, mut = t[0] === 'mut' ? 1 : 0) => [...reftype(mut ? t[
|
|
|
894
936
|
|
|
895
937
|
|
|
896
938
|
|
|
897
|
-
//
|
|
898
|
-
|
|
899
|
-
|
|
939
|
+
// Immediate encoders, keyed by immediate type (IMM in const.js maps op name → immediate type)
|
|
940
|
+
// leb into `out` (returning undefined — the write-mode sentinel) or a fresh array
|
|
941
|
+
const wleb = (v, out) => { if (out) { uleb(v, out); return } return uleb(v) }
|
|
942
|
+
|
|
943
|
+
const HANDLER = {
|
|
900
944
|
reversed: (n, c) => { let t = n.shift(), e = n.shift(); return [...uleb(id(e, c.elem)), ...uleb(id(t, c.table))] },
|
|
901
|
-
block: (n, c) => {
|
|
945
|
+
block: (n, c, op, out) => {
|
|
902
946
|
c.block.push(1)
|
|
903
947
|
isId(n[0]) && (c.block[n.shift()] = c.block.length)
|
|
904
948
|
let t = n.shift()
|
|
905
|
-
|
|
949
|
+
const b = !t ? [TYPE.void] : t[0] === 'result' ? reftype(t[1], c) : uleb(id(t[1], c.type))
|
|
950
|
+
if (out) { for (let i = 0; i < b.length; i++) out.push(b[i]); return }
|
|
951
|
+
return b
|
|
906
952
|
},
|
|
907
953
|
try_table: (n, c) => {
|
|
908
954
|
isId(n[0]) && (c.block[n.shift()] = c.block.length + 1)
|
|
@@ -921,7 +967,11 @@ const IMM = {
|
|
|
921
967
|
return [...result, ...uleb(count), ...catches]
|
|
922
968
|
},
|
|
923
969
|
end: (_n, c) => (c.block.pop(), []),
|
|
924
|
-
call_indirect: (n, c
|
|
970
|
+
call_indirect: (n, c, op, out) => {
|
|
971
|
+
let t = n.shift(), [, idx] = n.shift()
|
|
972
|
+
if (out) { uleb(id(idx, c.type), out); uleb(id(t, c.table), out); return }
|
|
973
|
+
return [...uleb(id(idx, c.type)), ...uleb(id(t, c.table))]
|
|
974
|
+
},
|
|
925
975
|
br_table: (n, c) => {
|
|
926
976
|
let labels = [], count = 0
|
|
927
977
|
while (n[0] && (!isNaN(n[0]) || isId(n[0]))) (labels.push(...uleb(blockid(n.shift(), c.block))), count++)
|
|
@@ -929,8 +979,8 @@ const IMM = {
|
|
|
929
979
|
},
|
|
930
980
|
select: (n, c) => { let r = n.shift() || []; return r.length ? vec(r.map(t => reftype(t, c))) : [] },
|
|
931
981
|
ref_null: (n, c) => { let t = n.shift(); return Array.isArray(t) && t[0] === 'exact' ? [0x62, ...uleb(id(t[1], c.type))] : TYPE[t] ? [TYPE[t]] : uleb(id(t, c.type)) },
|
|
932
|
-
memarg: (n, c, op) => memargEnc(n, op, isIdx(n[0]) && !isMemParam(n[0]) ? id(n.shift(), c.memory) : 0),
|
|
933
|
-
opt_memory: (n, c) =>
|
|
982
|
+
memarg: (n, c, op, out) => memargEnc(n, op, isIdx(n[0]) && !isMemParam(n[0]) ? id(n.shift(), c.memory) : 0, out),
|
|
983
|
+
opt_memory: (n, c, op, out) => wleb(id(isIdx(n[0]) ? n.shift() : 0, c.memory), out),
|
|
934
984
|
reftype: (n, c) => { let ht = reftype(n.shift(), c); return ht.length > 1 ? ht.slice(1) : ht },
|
|
935
985
|
reftype2: (n, c) => { let b = blockid(n.shift(), c.block), h1 = reftype(n.shift(), c), h2 = reftype(n.shift(), c), ht = h => h.length > 1 ? h.slice(1) : h; return [((h2[0] !== TYPE.ref) << 1) | (h1[0] !== TYPE.ref), ...uleb(b), ...ht(h1), ...ht(h2)] },
|
|
936
986
|
v128const: (n) => {
|
|
@@ -955,28 +1005,28 @@ const IMM = {
|
|
|
955
1005
|
const memIdx = isId(n[0]) || (isIdx(n[0]) && (isMemParam(n[1]) || isIdx(n[1]))) ? id(n.shift(), c.memory) : 0
|
|
956
1006
|
return [...memargEnc(n, op, memIdx), ...uleb(parseUint(n.shift()))]
|
|
957
1007
|
},
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
//
|
|
961
|
-
labelidx: (n, c) =>
|
|
962
|
-
laneidx: (n) =>
|
|
963
|
-
funcidx: (n, c) =>
|
|
964
|
-
typeidx: (n, c) =>
|
|
965
|
-
tableidx: (n, c) =>
|
|
966
|
-
memoryidx: (n, c) =>
|
|
967
|
-
globalidx: (n, c) =>
|
|
968
|
-
localidx: (n, c) =>
|
|
969
|
-
dataidx: (n, c) =>
|
|
970
|
-
elemidx: (n, c) =>
|
|
971
|
-
tagidx: (n, c) =>
|
|
972
|
-
'memoryidx?': (n, c) =>
|
|
973
|
-
stringidx: (n, c) => { let s = n.shift(), key = s.valueOf(), idx = c.strings.findIndex(x => x.valueOf() === key); if (idx < 0) idx = c.strings.push(s) - 1; return
|
|
1008
|
+
// *idx types — write-mode (out present) pushes in place and returns undefined;
|
|
1009
|
+
// return-mode hands back a fresh array. The sentinel is EXPLICIT: a boxed-
|
|
1010
|
+
// pointer identity compare (`returned !== out`) misfires under the jz kernel.
|
|
1011
|
+
labelidx: (n, c, op, out) => wleb(blockid(n.shift(), c.block), out),
|
|
1012
|
+
laneidx: (n, c, op, out) => { const v = parseUint(n.shift(), 0xff); if (out) { out.push(v); return } return [v] },
|
|
1013
|
+
funcidx: (n, c, op, out) => wleb(id(n.shift(), c.func), out),
|
|
1014
|
+
typeidx: (n, c, op, out) => wleb(id(n.shift(), c.type), out),
|
|
1015
|
+
tableidx: (n, c, op, out) => wleb(id(n.shift(), c.table), out),
|
|
1016
|
+
memoryidx: (n, c, op, out) => wleb(id(n.shift(), c.memory), out),
|
|
1017
|
+
globalidx: (n, c, op, out) => wleb(id(n.shift(), c.global), out),
|
|
1018
|
+
localidx: (n, c, op, out) => wleb(id(n.shift(), c.local), out),
|
|
1019
|
+
dataidx: (n, c, op, out) => wleb(id(n.shift(), c.data), out),
|
|
1020
|
+
elemidx: (n, c, op, out) => wleb(id(n.shift(), c.elem), out),
|
|
1021
|
+
tagidx: (n, c, op, out) => wleb(id(n.shift(), c.tag), out),
|
|
1022
|
+
'memoryidx?': (n, c, op, out) => wleb(id(isIdx(n[0]) ? n.shift() : 0, c.memory), out),
|
|
1023
|
+
stringidx: (n, c, op, out) => { let s = n.shift(), key = s.valueOf(), idx = c.strings.findIndex(x => x.valueOf() === key); if (idx < 0) idx = c.strings.push(s) - 1; return wleb(idx, out) },
|
|
974
1024
|
|
|
975
1025
|
// Value type
|
|
976
|
-
i32: (n) => encode.i32(n.shift()),
|
|
977
|
-
i64: (n) => encode.i64(n.shift()),
|
|
978
|
-
f32: (n) => encode.f32(n.shift()),
|
|
979
|
-
f64: (n) => encode.f64(n.shift()),
|
|
1026
|
+
i32: (n, c, op, out) => { if (out) { encode.i32(n.shift(), out); return } return encode.i32(n.shift()) },
|
|
1027
|
+
i64: (n, c, op, out) => { if (out) { encode.i64(n.shift(), out); return } return encode.i64(n.shift()) },
|
|
1028
|
+
f32: (n, c, op, out) => encode.f32(n.shift(), out),
|
|
1029
|
+
f64: (n, c, op, out) => encode.f64(n.shift(), out),
|
|
980
1030
|
v128: (n) => encode.v128(n.shift()),
|
|
981
1031
|
|
|
982
1032
|
// Combinations
|
|
@@ -1028,21 +1078,9 @@ const IMM = {
|
|
|
1028
1078
|
}
|
|
1029
1079
|
};
|
|
1030
1080
|
|
|
1031
|
-
// per-op imm handlers
|
|
1032
|
-
const HANDLER = {};
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
// Populate INSTR and IMM
|
|
1036
|
-
(function populate(items, pre) {
|
|
1037
|
-
for (let op = 0, item, nm, imm; op < items.length; op++) if (item = items[op]) {
|
|
1038
|
-
// Nested array (0xfb, 0xfc, 0xfd opcodes)
|
|
1039
|
-
if (Array.isArray(item)) populate(item, op)
|
|
1040
|
-
else [nm, imm] = item.split(' '), INSTR[nm] = pre ? [pre, ...uleb(op)] : [op], imm && (HANDLER[nm] = IMM[imm])
|
|
1041
|
-
}
|
|
1042
|
-
})(INSTR);
|
|
1043
|
-
|
|
1044
1081
|
|
|
1045
|
-
// instruction encoder
|
|
1082
|
+
// instruction encoder — bytes land DIRECTLY in the output stream (write-mode
|
|
1083
|
+
// handlers push immediates themselves; cold handlers still return small arrays)
|
|
1046
1084
|
const instr = (nodes, ctx) => {
|
|
1047
1085
|
let out = [], meta = []
|
|
1048
1086
|
|
|
@@ -1058,22 +1096,12 @@ const instr = (nodes, ctx) => {
|
|
|
1058
1096
|
|
|
1059
1097
|
// Array = unknown instruction passed through from normalize
|
|
1060
1098
|
if (Array.isArray(op)) {
|
|
1061
|
-
op.loc != null && (
|
|
1099
|
+
op.loc != null && setErrLoc(op.loc)
|
|
1062
1100
|
err(`Unknown instruction ${op[0]}`)
|
|
1063
1101
|
}
|
|
1064
1102
|
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
// special op handlers
|
|
1068
|
-
if (HANDLER[op]) {
|
|
1069
|
-
// select: becomes typed select (opcode+1) if next node is an array with result types
|
|
1070
|
-
if (op === 'select' && nodes[0]?.length) bytes[0]++
|
|
1071
|
-
// ref.type|cast: opcode+1 if type is nullable: (ref null $t) or (funcref, anyref, etc.)
|
|
1072
|
-
else if (HANDLER[op] === IMM.reftype && !op.endsWith('_null') && (nodes[0][1] === 'null' || nodes[0][0] !== 'ref')) {
|
|
1073
|
-
bytes[bytes.length - 1]++
|
|
1074
|
-
}
|
|
1075
|
-
bytes.push(...HANDLER[op](nodes, ctx, op))
|
|
1076
|
-
}
|
|
1103
|
+
const code = OPCODE[op]
|
|
1104
|
+
if (typeof code !== 'number') err(`Unknown instruction ${op}`)
|
|
1077
1105
|
|
|
1078
1106
|
// Attach any pending annotations to this instruction's byte position, then
|
|
1079
1107
|
// clear — a (@metadata.code.*) annotation applies to the next instruction only
|
|
@@ -1082,14 +1110,95 @@ const instr = (nodes, ctx) => {
|
|
|
1082
1110
|
meta = []
|
|
1083
1111
|
}
|
|
1084
1112
|
|
|
1085
|
-
out.
|
|
1113
|
+
const at = out.length
|
|
1114
|
+
// unpack: top byte is the page prefix (0xfb-0xfe), low bits the uleb-encoded subopcode
|
|
1115
|
+
if (code > 0xffff) { out.push(code >>> 16); uleb(code & 0xffff, out) }
|
|
1116
|
+
else out.push(code)
|
|
1117
|
+
|
|
1118
|
+
// immediate encoding
|
|
1119
|
+
const imm = IMM[op]
|
|
1120
|
+
if (imm) {
|
|
1121
|
+
// select: becomes typed select (opcode+1) if next node is an array with result types
|
|
1122
|
+
if (op === 'select' && nodes[0]?.length) out[at]++
|
|
1123
|
+
// ref.type|cast: opcode+1 if type is nullable: (ref null $t) or (funcref, anyref, etc.)
|
|
1124
|
+
else if (imm === 'reftype' && !op.endsWith('_null') && (nodes[0][1] === 'null' || nodes[0][0] !== 'ref')) {
|
|
1125
|
+
out[out.length - 1]++
|
|
1126
|
+
}
|
|
1127
|
+
const b = HANDLER[imm](nodes, ctx, op, out)
|
|
1128
|
+
if (b) for (let i = 0; i < b.length; i++) out.push(b[i])
|
|
1129
|
+
}
|
|
1086
1130
|
}
|
|
1087
1131
|
|
|
1088
1132
|
return out.push(0x0b), out
|
|
1089
1133
|
}
|
|
1090
1134
|
|
|
1091
|
-
// LEB128 byte-width of
|
|
1092
|
-
|
|
1135
|
+
// LEB128 byte-width of a non-negative number (7 bits per byte). Non-number
|
|
1136
|
+
// input (BigInt, numeric string) falls back to the real encoder's length.
|
|
1137
|
+
const ulebSize = (n) => {
|
|
1138
|
+
if (typeof n !== 'number' || n < 0) return uleb(n).length
|
|
1139
|
+
let k = 1; n >>>= 7
|
|
1140
|
+
while (n) k++, n >>>= 7
|
|
1141
|
+
return k
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
// Signed-LEB byte-width of an i32 immediate (mirrors encode.i32's loop).
|
|
1145
|
+
const slebSize32 = (n) => {
|
|
1146
|
+
if (typeof n === 'string') n = i32.parse(n)
|
|
1147
|
+
let k = 1
|
|
1148
|
+
while (true) {
|
|
1149
|
+
const byte = n & 0x7F
|
|
1150
|
+
n >>= 7
|
|
1151
|
+
if ((n === 0 && (byte & 0x40) === 0) || (n === -1 && (byte & 0x40) !== 0)) return k
|
|
1152
|
+
k++
|
|
1153
|
+
}
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1156
|
+
// Width of a memarg (align + optional memidx + offset) without building it.
|
|
1157
|
+
const memargSize = (nodes, op, memIdx = 0) => {
|
|
1158
|
+
const [a, o] = memarg(nodes), alignVal = (a ?? align(op)) | (memIdx && 0x40)
|
|
1159
|
+
return memIdx ? ulebSize(alignVal) + ulebSize(memIdx) + ulebSize(o ?? 0) : ulebSize(alignVal) + ulebSize(o ?? 0)
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
// Width-only twins of the HOT immediate handlers: identical token consumption
|
|
1163
|
+
// and ctx side effects (block stack, string interning), integer widths instead
|
|
1164
|
+
// of materialized byte arrays — immediate allocation dominates instrSize on
|
|
1165
|
+
// multi-MB modules. Cold immtypes fall back to the real handler's .length, so
|
|
1166
|
+
// the tables cannot drift; equality with compile().length is invariant-tested.
|
|
1167
|
+
const SIZE_HANDLER = {}
|
|
1168
|
+
for (const k in HANDLER) SIZE_HANDLER[k] = (n, c, op) => HANDLER[k](n, c, op).length
|
|
1169
|
+
Object.assign(SIZE_HANDLER, {
|
|
1170
|
+
i32: (n) => slebSize32(n.shift()),
|
|
1171
|
+
f32: (n) => (n.shift(), 4),
|
|
1172
|
+
f64: (n) => (n.shift(), 8),
|
|
1173
|
+
localidx: (n, c) => ulebSize(id(n.shift(), c.local)),
|
|
1174
|
+
funcidx: (n, c) => ulebSize(id(n.shift(), c.func)),
|
|
1175
|
+
typeidx: (n, c) => ulebSize(id(n.shift(), c.type)),
|
|
1176
|
+
tableidx: (n, c) => ulebSize(id(n.shift(), c.table)),
|
|
1177
|
+
memoryidx: (n, c) => ulebSize(id(n.shift(), c.memory)),
|
|
1178
|
+
globalidx: (n, c) => ulebSize(id(n.shift(), c.global)),
|
|
1179
|
+
dataidx: (n, c) => ulebSize(id(n.shift(), c.data)),
|
|
1180
|
+
elemidx: (n, c) => ulebSize(id(n.shift(), c.elem)),
|
|
1181
|
+
tagidx: (n, c) => ulebSize(id(n.shift(), c.tag)),
|
|
1182
|
+
labelidx: (n, c) => ulebSize(blockid(n.shift(), c.block)),
|
|
1183
|
+
laneidx: (n) => (parseUint(n.shift(), 0xff), 1),
|
|
1184
|
+
memarg: (n, c, op) => memargSize(n, op, isIdx(n[0]) && !isMemParam(n[0]) ? id(n.shift(), c.memory) : 0),
|
|
1185
|
+
opt_memory: (n, c) => ulebSize(id(isIdx(n[0]) ? n.shift() : 0, c.memory)),
|
|
1186
|
+
'memoryidx?': (n, c) => ulebSize(id(isIdx(n[0]) ? n.shift() : 0, c.memory)),
|
|
1187
|
+
call_indirect: (n, c) => { const t = n.shift(), [, ti] = n.shift(); return ulebSize(id(ti, c.type)) + ulebSize(id(t, c.table)) },
|
|
1188
|
+
block: (n, c) => {
|
|
1189
|
+
c.block.push(1)
|
|
1190
|
+
isId(n[0]) && (c.block[n.shift()] = c.block.length)
|
|
1191
|
+
const t = n.shift()
|
|
1192
|
+
return !t ? 1 : t[0] === 'result' ? reftype(t[1], c).length : ulebSize(id(t[1], c.type))
|
|
1193
|
+
},
|
|
1194
|
+
end: (_n, c) => (c.block.pop(), 0),
|
|
1195
|
+
stringidx: (n, c) => {
|
|
1196
|
+
const str = n.shift(), key = str.valueOf()
|
|
1197
|
+
let idx = c.strings.findIndex(x => x.valueOf() === key)
|
|
1198
|
+
if (idx < 0) idx = c.strings.push(str) - 1
|
|
1199
|
+
return ulebSize(idx)
|
|
1200
|
+
},
|
|
1201
|
+
})
|
|
1093
1202
|
|
|
1094
1203
|
// Size-only twin of instr(): the byte LENGTH of the encoded instruction stream,
|
|
1095
1204
|
// WITHOUT building the (multi-MB) byte array. Reuses every HANDLER for exact
|
|
@@ -1103,10 +1212,11 @@ const instrSize = (nodes, ctx) => {
|
|
|
1103
1212
|
while (nodes?.length) {
|
|
1104
1213
|
let op = nodes.shift()
|
|
1105
1214
|
if (op?.[0] === '@metadata') { meta.push(op.slice(1)); continue }
|
|
1106
|
-
if (Array.isArray(op)) { op.loc != null && (
|
|
1107
|
-
const
|
|
1108
|
-
|
|
1109
|
-
|
|
1215
|
+
if (Array.isArray(op)) { op.loc != null && setErrLoc(op.loc); err(`Unknown instruction ${op[0]}`) }
|
|
1216
|
+
const code = OPCODE[op]
|
|
1217
|
+
if (typeof code !== 'number') err(`Unknown instruction ${op}`)
|
|
1218
|
+
let n = code > 0xffff ? 1 + ulebSize(code & 0xffff) : 1
|
|
1219
|
+
if (IMM[op]) n += SIZE_HANDLER[IMM[op]](nodes, ctx, op)
|
|
1110
1220
|
if (meta.length) { for (const [type, data] of meta) ((ctx.meta[type] ??= []).push([size, data])); meta = [] }
|
|
1111
1221
|
size += n
|
|
1112
1222
|
}
|
|
@@ -1145,7 +1255,9 @@ const codeItemSize = (body, ctx) => {
|
|
|
1145
1255
|
const expr = (node, ctx) => instr(normalize([node], ctx), ctx)
|
|
1146
1256
|
|
|
1147
1257
|
// deref id node to numeric idx
|
|
1148
|
-
|
|
1258
|
+
// dense index spaces: a resolved name or an in-range number needs no second
|
|
1259
|
+
// dictionary hit — `n in list` only arbitrates the miss path
|
|
1260
|
+
const id = (nm, list, n) => (n = isId(nm) ? list[nm] : +nm, n >= 0 && n < list.length ? n : n in list ? n : err(`Unknown ${list.name} ${nm}`))
|
|
1149
1261
|
|
|
1150
1262
|
// block id - same as id but for block
|
|
1151
1263
|
// index indicates how many block items to pop
|
|
@@ -1167,8 +1279,14 @@ const memarg = (args) => {
|
|
|
1167
1279
|
|
|
1168
1280
|
// Encode memarg (align + offset) with default values based on instruction
|
|
1169
1281
|
// If memIdx is non-zero, set bit 6 in alignment flags and insert memIdx after align
|
|
1170
|
-
const memargEnc = (nodes, op, memIdx = 0) => {
|
|
1282
|
+
const memargEnc = (nodes, op, memIdx = 0, out) => {
|
|
1171
1283
|
const [a, o] = memarg(nodes), alignVal = (a ?? align(op)) | (memIdx && 0x40)
|
|
1284
|
+
if (out) {
|
|
1285
|
+
uleb(alignVal, out)
|
|
1286
|
+
if (memIdx) uleb(memIdx, out)
|
|
1287
|
+
uleb(o ?? 0, out)
|
|
1288
|
+
return
|
|
1289
|
+
}
|
|
1172
1290
|
return memIdx ? [...uleb(alignVal), ...uleb(memIdx), ...uleb(o ?? 0)] : [...uleb(alignVal), ...uleb(o ?? 0)]
|
|
1173
1291
|
}
|
|
1174
1292
|
|
|
@@ -1194,7 +1312,7 @@ const align = (op) => {
|
|
|
1194
1312
|
}
|
|
1195
1313
|
|
|
1196
1314
|
// Convert WAT numeric data (i8/i16/i32/i64/f32/f64 lists) to bytes (Phase 2: WAT numeric values)
|
|
1197
|
-
const numdata = (item) => {
|
|
1315
|
+
export const numdata = (item) => {
|
|
1198
1316
|
if (!Array.isArray(item)) return null
|
|
1199
1317
|
const [t, ...vs] = item
|
|
1200
1318
|
if (t !== 'i8' && t !== 'i16' && t !== 'i32' && t !== 'i64' && t !== 'f32' && t !== 'f64') return null
|