watr 5.0.0 → 5.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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, INSTR, DEFTYPE } from './const.js'
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
  /**
@@ -17,8 +17,16 @@ import { err, unescape, str } from './util.js'
17
17
  // load past parse — strip them. Predicate stays separate from `cleanup` so
18
18
  // neither has to invent a "drop me" sentinel and risk colliding with a
19
19
  // legitimate `null`/`undefined` immediate in the AST.
20
+ // A block-comment token is `(;…` (parse.js: `buf = '(' + ';'`) — checking n[1]===';'
21
+ // ALONE (without also requiring n[0]==='(') also matches an ordinary quoted STRING
22
+ // whose CONTENT happens to start with ';' (a string token always starts with n[0]==='"',
23
+ // so n[1] is its first content byte). A data-segment string is exactly this shape once
24
+ // packData splits a segment at a zero run and the surviving byte run starts with ';'
25
+ // (e.g. a WAT-text stdlib template's own comment, interned as static data) — cleanup()
26
+ // then silently drops that segment's whole content as if it were a comment. Mirrors the
27
+ // already-correct guard in optimize.js's own comment-strip (`c[0]==='(' && c[1]===';'`).
20
28
  const isDroppable = (n) =>
21
- (typeof n === 'string' && (n[0] === ';' || n[1] === ';')) ||
29
+ (typeof n === 'string' && (n[0] === ';' || (n[0] === '(' && n[1] === ';'))) ||
22
30
  (Array.isArray(n) && n[0]?.[0] === '@' && n[0] !== '@custom' && !n[0]?.startsWith?.('@metadata.code.'))
23
31
 
24
32
  const cleanup = (node, result) => {
@@ -42,9 +50,9 @@ const cleanup = (node, result) => {
42
50
  // reason to exist (the optimizer's size-revert guard can't afford to build bytes it discards).
43
51
  function assemble(nodes, sizeOnly) {
44
52
  // normalize to (module ...) form
45
- if (typeof nodes === 'string') err.src = nodes, nodes = parse(nodes) || []
46
- else err.src = '' // clear source if AST passed directly
47
- err.loc = 0
53
+ if (typeof nodes === 'string') setErrSrc(nodes), nodes = parse(nodes) || []
54
+ else setErrSrc('') // clear source if AST passed directly
55
+ setErrLoc(0)
48
56
 
49
57
  nodes = isDroppable(nodes) ? [] : (cleanup(nodes) ?? [])
50
58
 
@@ -84,7 +92,7 @@ function assemble(nodes, sizeOnly) {
84
92
  nodes.slice(idx).filter((n) => {
85
93
  if (!Array.isArray(n)) {
86
94
  // find token as standalone word (not substring of another token)
87
- let pos = err.loc, src = err.src, c
95
+ let pos = getErrLoc(), src = getErrSrc(), c
88
96
  while ((pos = src.indexOf(n, pos)) >= 0) {
89
97
  c = src.charCodeAt(pos - 1)
90
98
  // check not preceded by word char or $
@@ -94,11 +102,11 @@ function assemble(nodes, sizeOnly) {
94
102
  if (c > 47 && c < 58 || c > 64 && c < 91 || c > 96 && c < 123 || c === 95) { pos++; continue }
95
103
  break
96
104
  }
97
- if (pos >= 0) err.loc = pos
105
+ if (pos >= 0) setErrLoc(pos)
98
106
  err(`Unexpected token ${n}`)
99
107
  }
100
108
  let [kind, ...node] = n
101
- err.loc = n.loc // track position for errors
109
+ setErrLoc(n.loc) // track position for errors
102
110
  // (@custom "name" placement? data) - custom section support
103
111
  if (kind === '@custom') {
104
112
  ctx.custom.push(node)
@@ -141,7 +149,7 @@ function assemble(nodes, sizeOnly) {
141
149
  // prepare/normalize nodes
142
150
  .forEach((n) => {
143
151
  let [kind, ...node] = n
144
- err.loc = n.loc // track position for errors
152
+ setErrLoc(n.loc) // track position for errors
145
153
  let imported // if node needs to be imported
146
154
 
147
155
  // import abbr
@@ -257,7 +265,15 @@ function assemble(nodes, sizeOnly) {
257
265
  // inline bin(code) so the per-function item bytes survive — with ctx.codeSizePrefix
258
266
  // they let us lift code-metadata positions to absolute binary offsets below
259
267
  const codeItems = ctx.code.filter(Boolean).map(item => build[SECTION.code](item, ctx)).filter(Boolean)
260
- const codeSection = codeItems.length ? [SECTION.code, ...vec(vec(codeItems))] : []
268
+ // fused vec(vec(items)): count + items appended once, then the section frame —
269
+ // the generic path copies the multi-MB stream twice more
270
+ let codeSection = []
271
+ if (codeItems.length) {
272
+ const inner = uleb(codeItems.length)
273
+ for (const it of codeItems) for (let i = 0; i < it.length; i++) inner.push(it[i])
274
+ codeSection = [SECTION.code, ...uleb(inner.length)]
275
+ for (let i = 0; i < inner.length; i++) codeSection.push(inner[i])
276
+ }
261
277
  const metaSection = binMeta()
262
278
  const dataSection = bin(SECTION.data)
263
279
  const stringsSection = ctx.strings.length ? [SECTION.strings, ...vec([0x00, ...vec(ctx.strings.map(s => vec(s)))])] : []
@@ -282,12 +298,14 @@ function assemble(nodes, sizeOnly) {
282
298
  dataSection
283
299
  ]
284
300
 
285
- // build final binary
286
- const wasm = Uint8Array.from([
287
- 0x00, 0x61, 0x73, 0x6d, // magic
288
- 0x01, 0x00, 0x00, 0x00, // version
289
- ...sections.flat()
290
- ])
301
+ // build final binary — sections are flat byte arrays; copy them into place
302
+ // instead of flattening a multi-MB nested array through Uint8Array.from
303
+ let total = 8
304
+ for (const sec of sections) total += sec.length
305
+ const wasm = new Uint8Array(total)
306
+ wasm.set([0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00])
307
+ let off = 8
308
+ for (const sec of sections) { if (sec.length) { wasm.set(sec, off); off += sec.length } }
291
309
 
292
310
  // Lift any (@metadata.code.*) annotations to absolute binary offsets and hang
293
311
  // them off the result, so a source-map emitter can correlate wasm back to
@@ -360,42 +378,65 @@ const isMemParam = n => n?.[0] === 'a' || n?.[0] === 'o'
360
378
  * @param {Object} ctx - Compilation context with type info
361
379
  * @returns {Array} Flattened instruction sequence
362
380
  */
363
- function normalize(nodes, ctx) {
364
- const out = []
365
- nodes = [...nodes]
381
+ // bare-token classification — one lookup replaces a six-way string-scan chain
382
+ // (built lazily over OPCODE's key set; unknown tokens fall through untouched)
383
+ let NCLS = null
384
+ const nclsOf = (op) => {
385
+ if (!NCLS) {
386
+ NCLS = Object.create(null)
387
+ for (const k in OPCODE) {
388
+ if (k === 'block' || k === 'if' || k === 'loop') NCLS[k] = 1
389
+ else if (k === 'else' || k === 'end') NCLS[k] = 2
390
+ else if (k === 'select') NCLS[k] = 3
391
+ else if (k.endsWith('call_indirect')) NCLS[k] = 4
392
+ else if (k === 'table.init') NCLS[k] = 5
393
+ else if (k === 'table.copy' || k === 'memory.copy') NCLS[k] = 6
394
+ else if (k.startsWith('table.')) NCLS[k] = 7
395
+ else if (k === 'memory.init') NCLS[k] = 8
396
+ else if (k === 'data.drop' || k === 'array.new_data' || k === 'array.init_data') NCLS[k] = 9
397
+ else if (k.startsWith('memory.') || k.endsWith('load') || k.endsWith('store')) NCLS[k] = 10
398
+ }
399
+ }
400
+ return NCLS[op]
401
+ }
402
+
403
+ function normalize(nodes, ctx, out = [], owned = false) {
404
+ if (!owned) nodes = [...nodes]
366
405
  while (nodes.length) {
367
406
  let node = nodes.shift()
368
407
  if (typeof node === 'string') {
369
408
  out.push(node)
370
- if (node === 'block' || node === 'if' || node === 'loop') {
409
+ const cls = nclsOf(node)
410
+ if (cls === undefined) continue
411
+ if (cls === 1) {
371
412
  if (isId(nodes[0])) out.push(nodes.shift())
372
413
  out.push(blocktype(nodes, ctx))
373
414
  }
374
- else if (node === 'else' || node === 'end') {
415
+ else if (cls === 2) {
375
416
  if (isId(nodes[0])) nodes.shift()
376
417
  }
377
- else if (node === 'select') out.push(paramres(nodes)[1])
378
- else if (node.endsWith('call_indirect')) {
418
+ else if (cls === 3) out.push(paramres(nodes)[1])
419
+ else if (cls === 4) {
379
420
  let tableidx = isIdx(nodes[0]) ? nodes.shift() : 0, [idx, param, result] = typeuse(nodes, ctx)
380
421
  out.push(tableidx, ['type', idx ?? regtype(param, result, ctx)])
381
422
  }
382
- else if (node === 'table.init') out.push(isIdx(nodes[1]) ? nodes.shift() : 0, nodes.shift())
383
- else if (node === 'table.copy' || node === 'memory.copy') out.push(isIdx(nodes[0]) ? nodes.shift() : 0, isIdx(nodes[0]) ? nodes.shift() : 0)
384
- else if (node.startsWith('table.')) out.push(isIdx(nodes[0]) ? nodes.shift() : 0)
385
- else if (node === 'memory.init') {
423
+ else if (cls === 5) out.push(isIdx(nodes[1]) ? nodes.shift() : 0, nodes.shift())
424
+ else if (cls === 6) out.push(isIdx(nodes[0]) ? nodes.shift() : 0, isIdx(nodes[0]) ? nodes.shift() : 0)
425
+ else if (cls === 7) out.push(isIdx(nodes[0]) ? nodes.shift() : 0)
426
+ else if (cls === 8) {
386
427
  out.push(...(isIdx(nodes[1]) ? [nodes.shift(), nodes.shift()].reverse() : [nodes.shift(), 0]))
387
428
  ctx.datacount && (ctx.datacount[0] = true)
388
429
  }
389
- else if (node === 'data.drop' || node === 'array.new_data' || node === 'array.init_data') {
430
+ else if (cls === 9) {
390
431
  node === 'data.drop' && out.push(nodes.shift())
391
432
  ctx.datacount && (ctx.datacount[0] = true)
392
433
  }
393
434
  // memory.* instructions and load/store with optional memory index
394
- else if ((node.startsWith('memory.') || node.endsWith('load') || node.endsWith('store')) && isIdx(nodes[0])) out.push(nodes.shift())
435
+ else if (isIdx(nodes[0])) out.push(nodes.shift())
395
436
  }
396
437
  else if (Array.isArray(node)) {
397
438
  let op = node[0]
398
- node.loc != null && (err.loc = node.loc) // track position for errors
439
+ node.loc != null && setErrLoc(node.loc) // track position for errors
399
440
 
400
441
  // code metadata annotations - pass through as marker with metadata type and data
401
442
  // (@metadata.code.<type> data:str)
@@ -405,23 +446,29 @@ function normalize(nodes, ctx) {
405
446
  continue
406
447
  }
407
448
 
408
- // Check if node is a valid instruction (string with opcode in INSTR)
409
- if (typeof op !== 'string' || !Array.isArray(INSTR[op])) { out.push(node); continue }
449
+ // Check if node is a valid instruction (string with a known opcode)
450
+ if (typeof op !== 'string' || typeof OPCODE[op] !== 'number') { out.push(node); continue }
410
451
  const parts = node.slice(1)
411
452
  if (op === 'block' || op === 'loop') {
412
453
  out.push(op)
413
454
  if (isId(parts[0])) out.push(parts.shift())
414
- out.push(blocktype(parts, ctx), ...normalize(parts, ctx), 'end')
455
+ out.push(blocktype(parts, ctx))
456
+ normalize(parts, ctx, out, true)
457
+ out.push('end')
415
458
  }
416
459
  else if (op === 'if') {
460
+ // then/else normalize BEFORE the condition but EMIT after it — the temp
461
+ // arrays preserve the original type-registration order exactly
417
462
  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)
463
+ if (parts.at(-1)?.[0] === 'else') els = normalize(parts.pop().slice(1), ctx, [], true)
464
+ if (parts.at(-1)?.[0] === 'then') then = normalize(parts.pop().slice(1), ctx, [], true)
420
465
  let immed = [op]
421
466
  if (isId(parts[0])) immed.push(parts.shift())
422
467
  immed.push(blocktype(parts, ctx))
423
- out.push(...normalize(parts, ctx), ...immed, ...then)
424
- els.length && out.push('else', ...els)
468
+ normalize(parts, ctx, out, true)
469
+ for (let i = 0; i < immed.length; i++) out.push(immed[i])
470
+ for (let i = 0; i < then.length; i++) out.push(then[i])
471
+ if (els.length) { out.push('else'); for (let i = 0; i < els.length; i++) out.push(els[i]) }
425
472
  out.push('end')
426
473
  }
427
474
  else if (op === 'try_table') {
@@ -432,20 +479,23 @@ function normalize(nodes, ctx) {
432
479
  while (parts[0]?.[0] === 'catch' || parts[0]?.[0] === 'catch_ref' || parts[0]?.[0] === 'catch_all' || parts[0]?.[0] === 'catch_all_ref') {
433
480
  out.push(parts.shift())
434
481
  }
435
- out.push(...normalize(parts, ctx), 'end')
482
+ normalize(parts, ctx, out, true)
483
+ out.push('end')
436
484
  }
437
485
  else if (op === 'ref.test' || op === 'ref.cast') {
438
486
  const type = parts[0]
439
487
  const isNullable = !Array.isArray(type) || type[1] === 'null' || type[0] !== 'ref'
440
488
  if (isNullable) op += '_null'
441
- out.push(...normalize(parts.slice(1), ctx), op, type)
489
+ normalize(parts.slice(1), ctx, out, true)
490
+ out.push(op, type)
442
491
  nodes.unshift(...out.splice(out.length - 2))
443
492
  }
444
493
  else {
445
494
  const imm = []
446
495
  // Collect immediate operands (non-arrays or special forms like type/param/result/ref)
447
496
  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
- out.push(...normalize(parts, ctx), op, ...imm)
497
+ normalize(parts, ctx, out, true)
498
+ out.push(op, ...imm)
449
499
  nodes.unshift(...out.splice(out.length - 1 - imm.length))
450
500
  }
451
501
  } else out.push(node)
@@ -819,10 +869,10 @@ const build = [
819
869
  ctx.local = ctx.block = ctx.meta = null
820
870
 
821
871
  // https://webassembly.github.io/spec/core/binary/modules.html#code-section
822
- const item = vec([...locals, ...bytes])
823
- // size-prefix length (item minus its body) lets the now function-body-relative
824
- // metadata positions be lifted to absolute binary offsets
825
- ;(ctx.codeSizePrefix ??= [])[codeIdx] = item.length - locals.length - bytes.length
872
+ const item = uleb(locals.length + bytes.length)
873
+ ;(ctx.codeSizePrefix ??= [])[codeIdx] = item.length // = vec prefix width
874
+ for (let i = 0; i < locals.length; i++) item.push(locals[i])
875
+ for (let i = 0; i < bytes.length; i++) item.push(bytes[i])
826
876
  return item
827
877
  },
828
878
 
@@ -894,15 +944,19 @@ const fieldtype = (t, ctx, mut = t[0] === 'mut' ? 1 : 0) => [...reftype(mut ? t[
894
944
 
895
945
 
896
946
 
897
- // Pre-defined instruction handlers
898
- const IMM = {
899
- null: () => [],
947
+ // Immediate encoders, keyed by immediate type (IMM in const.js maps op name → immediate type)
948
+ // leb into `out` (returning undefined — the write-mode sentinel) or a fresh array
949
+ const wleb = (v, out) => { if (out) { uleb(v, out); return } return uleb(v) }
950
+
951
+ const HANDLER = {
900
952
  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) => {
953
+ block: (n, c, op, out) => {
902
954
  c.block.push(1)
903
955
  isId(n[0]) && (c.block[n.shift()] = c.block.length)
904
956
  let t = n.shift()
905
- return !t ? [TYPE.void] : t[0] === 'result' ? reftype(t[1], c) : uleb(id(t[1], c.type))
957
+ const b = !t ? [TYPE.void] : t[0] === 'result' ? reftype(t[1], c) : uleb(id(t[1], c.type))
958
+ if (out) { for (let i = 0; i < b.length; i++) out.push(b[i]); return }
959
+ return b
906
960
  },
907
961
  try_table: (n, c) => {
908
962
  isId(n[0]) && (c.block[n.shift()] = c.block.length + 1)
@@ -921,7 +975,11 @@ const IMM = {
921
975
  return [...result, ...uleb(count), ...catches]
922
976
  },
923
977
  end: (_n, c) => (c.block.pop(), []),
924
- call_indirect: (n, c) => { let t = n.shift(), [, idx] = n.shift(); return [...uleb(id(idx, c.type)), ...uleb(id(t, c.table))] },
978
+ call_indirect: (n, c, op, out) => {
979
+ let t = n.shift(), [, idx] = n.shift()
980
+ if (out) { uleb(id(idx, c.type), out); uleb(id(t, c.table), out); return }
981
+ return [...uleb(id(idx, c.type)), ...uleb(id(t, c.table))]
982
+ },
925
983
  br_table: (n, c) => {
926
984
  let labels = [], count = 0
927
985
  while (n[0] && (!isNaN(n[0]) || isId(n[0]))) (labels.push(...uleb(blockid(n.shift(), c.block))), count++)
@@ -929,8 +987,8 @@ const IMM = {
929
987
  },
930
988
  select: (n, c) => { let r = n.shift() || []; return r.length ? vec(r.map(t => reftype(t, c))) : [] },
931
989
  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) => uleb(id(isIdx(n[0]) ? n.shift() : 0, c.memory)),
990
+ memarg: (n, c, op, out) => memargEnc(n, op, isIdx(n[0]) && !isMemParam(n[0]) ? id(n.shift(), c.memory) : 0, out),
991
+ opt_memory: (n, c, op, out) => wleb(id(isIdx(n[0]) ? n.shift() : 0, c.memory), out),
934
992
  reftype: (n, c) => { let ht = reftype(n.shift(), c); return ht.length > 1 ? ht.slice(1) : ht },
935
993
  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
994
  v128const: (n) => {
@@ -955,28 +1013,28 @@ const IMM = {
955
1013
  const memIdx = isId(n[0]) || (isIdx(n[0]) && (isMemParam(n[1]) || isIdx(n[1]))) ? id(n.shift(), c.memory) : 0
956
1014
  return [...memargEnc(n, op, memIdx), ...uleb(parseUint(n.shift()))]
957
1015
  },
958
- '*': (n) => uleb(n.shift()),
959
-
960
- // *idx types
961
- labelidx: (n, c) => uleb(blockid(n.shift(), c.block)),
962
- laneidx: (n) => [parseUint(n.shift(), 0xff)],
963
- funcidx: (n, c) => uleb(id(n.shift(), c.func)),
964
- typeidx: (n, c) => uleb(id(n.shift(), c.type)),
965
- tableidx: (n, c) => uleb(id(n.shift(), c.table)),
966
- memoryidx: (n, c) => uleb(id(n.shift(), c.memory)),
967
- globalidx: (n, c) => uleb(id(n.shift(), c.global)),
968
- localidx: (n, c) => uleb(id(n.shift(), c.local)),
969
- dataidx: (n, c) => uleb(id(n.shift(), c.data)),
970
- elemidx: (n, c) => uleb(id(n.shift(), c.elem)),
971
- tagidx: (n, c) => uleb(id(n.shift(), c.tag)),
972
- 'memoryidx?': (n, c) => uleb(id(isIdx(n[0]) ? n.shift() : 0, c.memory)),
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 uleb(idx) },
1016
+ // *idx types — write-mode (out present) pushes in place and returns undefined;
1017
+ // return-mode hands back a fresh array. The sentinel is EXPLICIT: a boxed-
1018
+ // pointer identity compare (`returned !== out`) misfires under the jz kernel.
1019
+ labelidx: (n, c, op, out) => wleb(blockid(n.shift(), c.block), out),
1020
+ laneidx: (n, c, op, out) => { const v = parseUint(n.shift(), 0xff); if (out) { out.push(v); return } return [v] },
1021
+ funcidx: (n, c, op, out) => wleb(id(n.shift(), c.func), out),
1022
+ typeidx: (n, c, op, out) => wleb(id(n.shift(), c.type), out),
1023
+ tableidx: (n, c, op, out) => wleb(id(n.shift(), c.table), out),
1024
+ memoryidx: (n, c, op, out) => wleb(id(n.shift(), c.memory), out),
1025
+ globalidx: (n, c, op, out) => wleb(id(n.shift(), c.global), out),
1026
+ localidx: (n, c, op, out) => wleb(id(n.shift(), c.local), out),
1027
+ dataidx: (n, c, op, out) => wleb(id(n.shift(), c.data), out),
1028
+ elemidx: (n, c, op, out) => wleb(id(n.shift(), c.elem), out),
1029
+ tagidx: (n, c, op, out) => wleb(id(n.shift(), c.tag), out),
1030
+ 'memoryidx?': (n, c, op, out) => wleb(id(isIdx(n[0]) ? n.shift() : 0, c.memory), out),
1031
+ 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
1032
 
975
1033
  // 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()),
1034
+ i32: (n, c, op, out) => { if (out) { encode.i32(n.shift(), out); return } return encode.i32(n.shift()) },
1035
+ i64: (n, c, op, out) => { if (out) { encode.i64(n.shift(), out); return } return encode.i64(n.shift()) },
1036
+ f32: (n, c, op, out) => encode.f32(n.shift(), out),
1037
+ f64: (n, c, op, out) => encode.f64(n.shift(), out),
980
1038
  v128: (n) => encode.v128(n.shift()),
981
1039
 
982
1040
  // Combinations
@@ -1028,21 +1086,9 @@ const IMM = {
1028
1086
  }
1029
1087
  };
1030
1088
 
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
1089
 
1045
- // instruction encoder
1090
+ // instruction encoder — bytes land DIRECTLY in the output stream (write-mode
1091
+ // handlers push immediates themselves; cold handlers still return small arrays)
1046
1092
  const instr = (nodes, ctx) => {
1047
1093
  let out = [], meta = []
1048
1094
 
@@ -1058,22 +1104,12 @@ const instr = (nodes, ctx) => {
1058
1104
 
1059
1105
  // Array = unknown instruction passed through from normalize
1060
1106
  if (Array.isArray(op)) {
1061
- op.loc != null && (err.loc = op.loc)
1107
+ op.loc != null && setErrLoc(op.loc)
1062
1108
  err(`Unknown instruction ${op[0]}`)
1063
1109
  }
1064
1110
 
1065
- let [...bytes] = INSTR[op] || err(`Unknown instruction ${op}`)
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
- }
1111
+ const code = OPCODE[op]
1112
+ if (typeof code !== 'number') err(`Unknown instruction ${op}`)
1077
1113
 
1078
1114
  // Attach any pending annotations to this instruction's byte position, then
1079
1115
  // clear — a (@metadata.code.*) annotation applies to the next instruction only
@@ -1082,14 +1118,95 @@ const instr = (nodes, ctx) => {
1082
1118
  meta = []
1083
1119
  }
1084
1120
 
1085
- out.push(...bytes)
1121
+ const at = out.length
1122
+ // unpack: top byte is the page prefix (0xfb-0xfe), low bits the uleb-encoded subopcode
1123
+ if (code > 0xffff) { out.push(code >>> 16); uleb(code & 0xffff, out) }
1124
+ else out.push(code)
1125
+
1126
+ // immediate encoding
1127
+ const imm = IMM[op]
1128
+ if (imm) {
1129
+ // select: becomes typed select (opcode+1) if next node is an array with result types
1130
+ if (op === 'select' && nodes[0]?.length) out[at]++
1131
+ // ref.type|cast: opcode+1 if type is nullable: (ref null $t) or (funcref, anyref, etc.)
1132
+ else if (imm === 'reftype' && !op.endsWith('_null') && (nodes[0][1] === 'null' || nodes[0][0] !== 'ref')) {
1133
+ out[out.length - 1]++
1134
+ }
1135
+ const b = HANDLER[imm](nodes, ctx, op, out)
1136
+ if (b) for (let i = 0; i < b.length; i++) out.push(b[i])
1137
+ }
1086
1138
  }
1087
1139
 
1088
1140
  return out.push(0x0b), out
1089
1141
  }
1090
1142
 
1091
- // LEB128 byte-width of an unsigned integer (exact, reuses uleb so it can never drift).
1092
- const ulebSize = (n) => uleb(n).length
1143
+ // LEB128 byte-width of a non-negative number (7 bits per byte). Non-number
1144
+ // input (BigInt, numeric string) falls back to the real encoder's length.
1145
+ const ulebSize = (n) => {
1146
+ if (typeof n !== 'number' || n < 0) return uleb(n).length
1147
+ let k = 1; n >>>= 7
1148
+ while (n) k++, n >>>= 7
1149
+ return k
1150
+ }
1151
+
1152
+ // Signed-LEB byte-width of an i32 immediate (mirrors encode.i32's loop).
1153
+ const slebSize32 = (n) => {
1154
+ if (typeof n === 'string') n = i32.parse(n)
1155
+ let k = 1
1156
+ while (true) {
1157
+ const byte = n & 0x7F
1158
+ n >>= 7
1159
+ if ((n === 0 && (byte & 0x40) === 0) || (n === -1 && (byte & 0x40) !== 0)) return k
1160
+ k++
1161
+ }
1162
+ }
1163
+
1164
+ // Width of a memarg (align + optional memidx + offset) without building it.
1165
+ const memargSize = (nodes, op, memIdx = 0) => {
1166
+ const [a, o] = memarg(nodes), alignVal = (a ?? align(op)) | (memIdx && 0x40)
1167
+ return memIdx ? ulebSize(alignVal) + ulebSize(memIdx) + ulebSize(o ?? 0) : ulebSize(alignVal) + ulebSize(o ?? 0)
1168
+ }
1169
+
1170
+ // Width-only twins of the HOT immediate handlers: identical token consumption
1171
+ // and ctx side effects (block stack, string interning), integer widths instead
1172
+ // of materialized byte arrays — immediate allocation dominates instrSize on
1173
+ // multi-MB modules. Cold immtypes fall back to the real handler's .length, so
1174
+ // the tables cannot drift; equality with compile().length is invariant-tested.
1175
+ const SIZE_HANDLER = {}
1176
+ for (const k in HANDLER) SIZE_HANDLER[k] = (n, c, op) => HANDLER[k](n, c, op).length
1177
+ Object.assign(SIZE_HANDLER, {
1178
+ i32: (n) => slebSize32(n.shift()),
1179
+ f32: (n) => (n.shift(), 4),
1180
+ f64: (n) => (n.shift(), 8),
1181
+ localidx: (n, c) => ulebSize(id(n.shift(), c.local)),
1182
+ funcidx: (n, c) => ulebSize(id(n.shift(), c.func)),
1183
+ typeidx: (n, c) => ulebSize(id(n.shift(), c.type)),
1184
+ tableidx: (n, c) => ulebSize(id(n.shift(), c.table)),
1185
+ memoryidx: (n, c) => ulebSize(id(n.shift(), c.memory)),
1186
+ globalidx: (n, c) => ulebSize(id(n.shift(), c.global)),
1187
+ dataidx: (n, c) => ulebSize(id(n.shift(), c.data)),
1188
+ elemidx: (n, c) => ulebSize(id(n.shift(), c.elem)),
1189
+ tagidx: (n, c) => ulebSize(id(n.shift(), c.tag)),
1190
+ labelidx: (n, c) => ulebSize(blockid(n.shift(), c.block)),
1191
+ laneidx: (n) => (parseUint(n.shift(), 0xff), 1),
1192
+ memarg: (n, c, op) => memargSize(n, op, isIdx(n[0]) && !isMemParam(n[0]) ? id(n.shift(), c.memory) : 0),
1193
+ opt_memory: (n, c) => ulebSize(id(isIdx(n[0]) ? n.shift() : 0, c.memory)),
1194
+ 'memoryidx?': (n, c) => ulebSize(id(isIdx(n[0]) ? n.shift() : 0, c.memory)),
1195
+ call_indirect: (n, c) => { const t = n.shift(), [, ti] = n.shift(); return ulebSize(id(ti, c.type)) + ulebSize(id(t, c.table)) },
1196
+ block: (n, c) => {
1197
+ c.block.push(1)
1198
+ isId(n[0]) && (c.block[n.shift()] = c.block.length)
1199
+ const t = n.shift()
1200
+ return !t ? 1 : t[0] === 'result' ? reftype(t[1], c).length : ulebSize(id(t[1], c.type))
1201
+ },
1202
+ end: (_n, c) => (c.block.pop(), 0),
1203
+ stringidx: (n, c) => {
1204
+ const str = n.shift(), key = str.valueOf()
1205
+ let idx = c.strings.findIndex(x => x.valueOf() === key)
1206
+ if (idx < 0) idx = c.strings.push(str) - 1
1207
+ return ulebSize(idx)
1208
+ },
1209
+ })
1093
1210
 
1094
1211
  // Size-only twin of instr(): the byte LENGTH of the encoded instruction stream,
1095
1212
  // WITHOUT building the (multi-MB) byte array. Reuses every HANDLER for exact
@@ -1103,10 +1220,11 @@ const instrSize = (nodes, ctx) => {
1103
1220
  while (nodes?.length) {
1104
1221
  let op = nodes.shift()
1105
1222
  if (op?.[0] === '@metadata') { meta.push(op.slice(1)); continue }
1106
- if (Array.isArray(op)) { op.loc != null && (err.loc = op.loc); err(`Unknown instruction ${op[0]}`) }
1107
- const opc = INSTR[op] || err(`Unknown instruction ${op}`)
1108
- let n = opc.length
1109
- if (HANDLER[op]) n += HANDLER[op](nodes, ctx, op).length
1223
+ if (Array.isArray(op)) { op.loc != null && setErrLoc(op.loc); err(`Unknown instruction ${op[0]}`) }
1224
+ const code = OPCODE[op]
1225
+ if (typeof code !== 'number') err(`Unknown instruction ${op}`)
1226
+ let n = code > 0xffff ? 1 + ulebSize(code & 0xffff) : 1
1227
+ if (IMM[op]) n += SIZE_HANDLER[IMM[op]](nodes, ctx, op)
1110
1228
  if (meta.length) { for (const [type, data] of meta) ((ctx.meta[type] ??= []).push([size, data])); meta = [] }
1111
1229
  size += n
1112
1230
  }
@@ -1145,7 +1263,9 @@ const codeItemSize = (body, ctx) => {
1145
1263
  const expr = (node, ctx) => instr(normalize([node], ctx), ctx)
1146
1264
 
1147
1265
  // deref id node to numeric idx
1148
- const id = (nm, list, n) => (n = isId(nm) ? list[nm] : +nm, n in list ? n : err(`Unknown ${list.name} ${nm}`))
1266
+ // dense index spaces: a resolved name or an in-range number needs no second
1267
+ // dictionary hit — `n in list` only arbitrates the miss path
1268
+ 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
1269
 
1150
1270
  // block id - same as id but for block
1151
1271
  // index indicates how many block items to pop
@@ -1167,8 +1287,14 @@ const memarg = (args) => {
1167
1287
 
1168
1288
  // Encode memarg (align + offset) with default values based on instruction
1169
1289
  // If memIdx is non-zero, set bit 6 in alignment flags and insert memIdx after align
1170
- const memargEnc = (nodes, op, memIdx = 0) => {
1290
+ const memargEnc = (nodes, op, memIdx = 0, out) => {
1171
1291
  const [a, o] = memarg(nodes), alignVal = (a ?? align(op)) | (memIdx && 0x40)
1292
+ if (out) {
1293
+ uleb(alignVal, out)
1294
+ if (memIdx) uleb(memIdx, out)
1295
+ uleb(o ?? 0, out)
1296
+ return
1297
+ }
1172
1298
  return memIdx ? [...uleb(alignVal), ...uleb(memIdx), ...uleb(o ?? 0)] : [...uleb(alignVal), ...uleb(o ?? 0)]
1173
1299
  }
1174
1300
 
@@ -1194,7 +1320,7 @@ const align = (op) => {
1194
1320
  }
1195
1321
 
1196
1322
  // Convert WAT numeric data (i8/i16/i32/i64/f32/f64 lists) to bytes (Phase 2: WAT numeric values)
1197
- const numdata = (item) => {
1323
+ export const numdata = (item) => {
1198
1324
  if (!Array.isArray(item)) return null
1199
1325
  const [t, ...vs] = item
1200
1326
  if (t !== 'i8' && t !== 'i16' && t !== 'i32' && t !== 'i64' && t !== 'f32' && t !== 'f64') return null