watr 5.0.0 → 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/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
  /**
@@ -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') err.src = nodes, nodes = parse(nodes) || []
46
- else err.src = '' // clear source if AST passed directly
47
- err.loc = 0
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 = err.loc, src = err.src, c
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) err.loc = pos
97
+ if (pos >= 0) setErrLoc(pos)
98
98
  err(`Unexpected token ${n}`)
99
99
  }
100
100
  let [kind, ...node] = n
101
- err.loc = n.loc // track position for errors
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
- err.loc = n.loc // track position for errors
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
- const codeSection = codeItems.length ? [SECTION.code, ...vec(vec(codeItems))] : []
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
- const wasm = Uint8Array.from([
287
- 0x00, 0x61, 0x73, 0x6d, // magic
288
- 0x01, 0x00, 0x00, 0x00, // version
289
- ...sections.flat()
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
- function normalize(nodes, ctx) {
364
- const out = []
365
- nodes = [...nodes]
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
- if (node === 'block' || node === 'if' || node === 'loop') {
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 (node === 'else' || node === 'end') {
407
+ else if (cls === 2) {
375
408
  if (isId(nodes[0])) nodes.shift()
376
409
  }
377
- else if (node === 'select') out.push(paramres(nodes)[1])
378
- else if (node.endsWith('call_indirect')) {
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 (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') {
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 (node === 'data.drop' || node === 'array.new_data' || node === 'array.init_data') {
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 ((node.startsWith('memory.') || node.endsWith('load') || node.endsWith('store')) && isIdx(nodes[0])) out.push(nodes.shift())
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 && (err.loc = node.loc) // track position for errors
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 opcode in INSTR)
409
- if (typeof op !== 'string' || !Array.isArray(INSTR[op])) { out.push(node); continue }
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), ...normalize(parts, ctx), 'end')
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
- out.push(...normalize(parts, ctx), ...immed, ...then)
424
- els.length && out.push('else', ...els)
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
- out.push(...normalize(parts, ctx), 'end')
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
- out.push(...normalize(parts.slice(1), ctx), op, type)
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
- out.push(...normalize(parts, ctx), op, ...imm)
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 = 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
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
- // Pre-defined instruction handlers
898
- const IMM = {
899
- null: () => [],
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
- return !t ? [TYPE.void] : t[0] === 'result' ? reftype(t[1], c) : uleb(id(t[1], c.type))
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) => { let t = n.shift(), [, idx] = n.shift(); return [...uleb(id(idx, c.type)), ...uleb(id(t, c.table))] },
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) => uleb(id(isIdx(n[0]) ? n.shift() : 0, c.memory)),
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
- '*': (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) },
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 && (err.loc = op.loc)
1099
+ op.loc != null && setErrLoc(op.loc)
1062
1100
  err(`Unknown instruction ${op[0]}`)
1063
1101
  }
1064
1102
 
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
- }
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.push(...bytes)
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 an unsigned integer (exact, reuses uleb so it can never drift).
1092
- const ulebSize = (n) => uleb(n).length
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 && (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
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
- const id = (nm, list, n) => (n = isId(nm) ? list[nm] : +nm, n in list ? n : err(`Unknown ${list.name} ${nm}`))
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