watr 4.5.1 → 4.5.3

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/const.js CHANGED
@@ -59,7 +59,7 @@ export const INSTR = [
59
59
  'struct.new typeidx', 'struct.new_default typeidx', 'struct.get typeidx_field', 'struct.get_s typeidx_field', 'struct.get_u typeidx_field', 'struct.set typeidx_field',
60
60
  'array.new typeidx', 'array.new_default typeidx', 'array.new_fixed typeidx_multi', 'array.new_data typeidx_dataidx', 'array.new_elem typeidx_elemidx',
61
61
  'array.get typeidx', 'array.get_s typeidx', 'array.get_u typeidx', 'array.set typeidx', 'array.len', 'array.fill typeidx', 'array.copy typeidx_typeidx',
62
- 'array.init_data typeidx_dataidx', 'array.init_elem typeidx_elemidx', 'ref.test reftype', '', 'ref.cast reftype', '', 'br_on_cast reftype2', 'br_on_cast_fail reftype2',
62
+ 'array.init_data typeidx_dataidx', 'array.init_elem typeidx_elemidx', 'ref.test reftype', 'ref.test_null reftype', 'ref.cast reftype', 'ref.cast_null reftype', 'br_on_cast reftype2', 'br_on_cast_fail reftype2',
63
63
  'any.convert_extern', 'extern.convert_any', 'ref.i31', 'i31.get_s', 'i31.get_u',
64
64
  // custom descriptors (Phase 3): 0xFB 0x20-0x26
65
65
  , 'struct.new_desc typeidx', 'struct.new_default_desc typeidx', 'ref.get_desc typeidx', 'ref.cast_desc_eq reftype', , 'br_on_cast_desc_eq reftype2', 'br_on_cast_desc_eq_fail reftype2',
@@ -182,7 +182,7 @@ export const TYPE = {
182
182
  // Value types
183
183
  i8: 0x78, i16: 0x77, i32: 0x7f, i64: 0x7e, f32: 0x7d, f64: 0x7c, void: 0x40, v128: 0x7B,
184
184
  // Heap types
185
- exn: 0x69, noexn: 0x74, nofunc: 0x73, noextern: 0x72, none: 0x71, func: 0x70, extern: 0x6F, any: 0x6E, eq: 0x6D, i31: 0x6C, struct: 0x6B, array: 0x6A,
185
+ exn: 0x69, noexn: 0x74, nofunc: 0x73, noextern: 0x72, none: 0x71, func: 0x70, extern: 0x6F, any: 0x6E, eq: 0x6D, i31: 0x6C, struct: 0x6B, array: 0x6A, data: 0x6B,
186
186
  cont: 0x68, nocont: 0x75, // stack switching (Phase 3)
187
187
  string: 0x67, stringview_wtf8: 0x66, stringview_wtf16: 0x60, stringview_iter: 0x61, // stringref
188
188
  // Reference type abbreviations (absheaptype abbrs)
package/src/encode.js CHANGED
@@ -128,16 +128,30 @@ const _u8 = new Uint8Array(_buf), _i32 = new Int32Array(_buf), _f32 = new Float3
128
128
 
129
129
  i64.parse = n => {
130
130
  n = cleanInt(n)
131
- n = n[0] === '-' ? -BigInt(n.slice(1)) : BigInt(n) // can be -0x123
132
- if (n < -0x8000000000000000n || n > 0xffffffffffffffffn) err(`i64 constant out of range`)
133
- _i64[0] = n
131
+ const neg = n[0] === '-'
132
+ const body = neg ? n.slice(1) : n
133
+ // Range check on the literal string before BigInt conversion (lexicographic compare on clean digits).
134
+ let max
135
+ if (body[0] === '0' && (body[1] === 'x' || body[1] === 'X')) {
136
+ const hex = body.slice(2).replace(/^0+/, '') || '0'
137
+ max = neg ? '8000000000000000' : 'ffffffffffffffff'
138
+ if (hex.length > 16 || (hex.length === 16 && hex.toLowerCase() > max)) err(`i64 constant out of range`)
139
+ } else {
140
+ const dec = body.replace(/^0+/, '') || '0'
141
+ max = neg ? '9223372036854775808' : '18446744073709551615'
142
+ if (dec.length > max.length || (dec.length === max.length && dec > max)) err(`i64 constant out of range`)
143
+ }
144
+ let bi = BigInt(body)
145
+ if (neg) bi = 0n - bi
146
+ _i64[0] = bi
134
147
  return _i64[0]
135
148
  }
136
149
 
137
- const F32_SIGN = 0x80000000, F32_NAN = 0x7f800000
150
+ const F32_SIGN = 0x80000000, F32_NAN = 0x7f800000, F32_QUIET = 0x400000
138
151
  export function f32(input, value, idx) {
139
- if (typeof input === 'string' && ~(idx = input.indexOf('nan:'))) {
140
- value = i32.parse(input.slice(idx + 4))
152
+ // Plain `nan` / `-nan` (with optional `:0xPAYLOAD`) set the bit pattern explicitly.
153
+ if (typeof input === 'string' && (idx = input.indexOf('nan')) >= 0) {
154
+ value = input[idx + 3] === ':' ? i32.parse(input.slice(idx + 4)) : F32_QUIET
141
155
  value |= F32_NAN
142
156
  if (input[0] === '-') value |= F32_SIGN
143
157
  _i32[0] = value
@@ -150,10 +164,11 @@ export function f32(input, value, idx) {
150
164
  return [_u8[0], _u8[1], _u8[2], _u8[3]]
151
165
  }
152
166
 
153
- const F64_SIGN = 0x8000000000000000n, F64_NAN = 0x7ff0000000000000n
167
+ const F64_SIGN = 0x8000000000000000n, F64_NAN = 0x7ff0000000000000n, F64_QUIET = 0x8000000000000n
154
168
  export function f64(input, value, idx) {
155
- if (typeof input === 'string' && ~(idx = input.indexOf('nan:'))) {
156
- value = i64.parse(input.slice(idx + 4))
169
+ // Plain `nan` / `-nan` (with optional `:0xPAYLOAD`) set the bit pattern explicitly.
170
+ if (typeof input === 'string' && (idx = input.indexOf('nan')) >= 0) {
171
+ value = input[idx + 3] === ':' ? i64.parse(input.slice(idx + 4)) : F64_QUIET
157
172
  value |= F64_NAN
158
173
  if (input[0] === '-') value |= F64_SIGN
159
174
  _i64[0] = value
@@ -179,9 +194,15 @@ f64.parse = (input, max=Number.MAX_VALUE) => {
179
194
  let [int, fract=''] = sig.split('.'); // integer and fractional parts
180
195
  let flen = fract.length ?? 0;
181
196
 
182
- // Parse integer part
183
- let intVal = parseInt(int); // 0x is included in int
184
- isNaN(intVal) && err()
197
+ // Parse integer part — accumulate from least-significant digit to preserve precision.
198
+ // parseInt loses low bits for values > 2^53 because left-to-right
199
+ // accumulation rounds at each step; right-to-left keeps intermediates
200
+ // small so the final large+small addition rounds correctly.
201
+ let intVal = 0;
202
+ for (let i = int.length - 1; i >= 2; i--) {
203
+ let digit = parseInt(int[i], 16);
204
+ intVal += digit * (16 ** (int.length - 1 - i));
205
+ }
185
206
 
186
207
  // 0x10a.fbc = 0x10afbc * 16⁻³ = 266.9833984375
187
208
  // Parse fractional part: fract / 16^flen
package/src/optimize.js CHANGED
@@ -6,8 +6,10 @@
6
6
  */
7
7
 
8
8
  import parse from './parse.js'
9
+ import compile from './compile.js'
9
10
 
10
- /** Optimizations that can be applied */
11
+ /** Optimizations that can be applied.
12
+ * Passes defaulting to false can bloat output or are expensive — opt-in only. */
11
13
  const OPTS = {
12
14
  treeshake: true, // remove unused funcs/globals/types/tables
13
15
  fold: true, // constant folding
@@ -16,8 +18,8 @@ const OPTS = {
16
18
  identity: true, // remove identity ops (x + 0 → x)
17
19
  strength: true, // strength reduction (x * 2 → x << 1)
18
20
  branch: true, // simplify constant branches
19
- propagate: true, // constant propagation through locals
20
- inline: true, // inline tiny functions
21
+ propagate: false, // constant propagation can duplicate expressions
22
+ inline: false, // inline tiny functions — can duplicate bodies
21
23
  vacuum: true, // remove nops, drop-of-pure, empty branches
22
24
  peephole: true, // x-x→0, x&0→0, etc.
23
25
  globals: true, // propagate immutable global constants
@@ -25,11 +27,9 @@ const OPTS = {
25
27
  unbranch: true, // remove redundant br at end of own block
26
28
  stripmut: true, // strip mut from never-written globals
27
29
  brif: true, // if-then-br → br_if
28
- foldarms: true, // merge identical trailing if arms
29
- // minify: true, // NOTE: disabled — renaming $ids has no binary-size effect
30
- // without a names section, and risks local-name collisions.
30
+ foldarms: false, // merge identical trailing if arms — can add block wrapper
31
31
  dedupe: true, // eliminate duplicate functions
32
- reorder: true, // put hot functions first for smaller LEBs
32
+ reorder: false, // put hot functions first no AST reduction
33
33
  dedupTypes: true, // merge identical type definitions
34
34
  packData: true, // trim trailing zeros, merge adjacent data segments
35
35
  minifyImports: false, // shorten import names — enable only when you control the host
@@ -38,6 +38,27 @@ const OPTS = {
38
38
  /** All optimization names */
39
39
  const ALL = Object.keys(OPTS)
40
40
 
41
+ /**
42
+ * Recursively count AST nodes — fast size heuristic without compiling.
43
+ * @param {any} node
44
+ * @returns {number}
45
+ */
46
+ const count = (node) => {
47
+ if (!Array.isArray(node)) return 1
48
+ let n = 1
49
+ for (let i = 0; i < node.length; i++) n += count(node[i])
50
+ return n
51
+ }
52
+
53
+ /**
54
+ * Compile AST and measure binary size in bytes.
55
+ * @param {Array} ast
56
+ * @returns {number}
57
+ */
58
+ const binarySize = (ast) => {
59
+ try { return compile(ast).length } catch { return Infinity }
60
+ }
61
+
41
62
  /**
42
63
  * Fast structural equality of two AST nodes.
43
64
  * Stops at first difference. Handles BigInt without stringification.
@@ -62,12 +83,9 @@ const normalize = (opts) => {
62
83
  if (opts === false) return {}
63
84
  if (typeof opts === 'string') {
64
85
  const set = new Set(opts.split(/\s+/).filter(Boolean))
65
- // Special case: a single explicit pass name (e.g. 'fold') enables only that pass,
66
- // rather than treating it as a sparse map where everything else is disabled.
67
- if (set.size === 1 && ALL.includes([...set][0])) {
68
- return Object.fromEntries(ALL.map(f => [f, set.has(f)]))
69
- }
70
- return Object.fromEntries(ALL.map(f => [f, set.has(f) || set.has('all')]))
86
+ if (set.has('all')) return Object.fromEntries(ALL.map(f => [f, true]))
87
+ // Explicit pass names enable ONLY those passes (not the full default set).
88
+ return Object.fromEntries(ALL.map(f => [f, set.has(f)]))
71
89
  }
72
90
  return { ...OPTS, ...opts }
73
91
  }
@@ -440,7 +458,7 @@ const makeConst = (type, value) => {
440
458
  * @returns {Array}
441
459
  */
442
460
  const fold = (ast) => {
443
- return walkPost(clone(ast), (node) => {
461
+ return walkPost(ast, (node) => {
444
462
  if (!Array.isArray(node)) return
445
463
  const entry = FOLDABLE[node[0]]
446
464
  if (!entry) return
@@ -526,7 +544,7 @@ const IDENTITIES = {
526
544
  * @returns {Array}
527
545
  */
528
546
  const identity = (ast) => {
529
- return walkPost(clone(ast), (node) => {
547
+ return walkPost(ast, (node) => {
530
548
  if (!Array.isArray(node) || node.length !== 3) return
531
549
  const fn = IDENTITIES[node[0]]
532
550
  if (!fn) return
@@ -544,7 +562,7 @@ const identity = (ast) => {
544
562
  * @returns {Array}
545
563
  */
546
564
  const strength = (ast) => {
547
- return walkPost(clone(ast), (node) => {
565
+ return walkPost(ast, (node) => {
548
566
  if (!Array.isArray(node) || node.length !== 3) return
549
567
  const [op, a, b] = node
550
568
 
@@ -614,7 +632,7 @@ const strength = (ast) => {
614
632
  * @returns {Array}
615
633
  */
616
634
  const branch = (ast) => {
617
- return walkPost(clone(ast), (node) => {
635
+ return walkPost(ast, (node) => {
618
636
  if (!Array.isArray(node)) return
619
637
  const op = node[0]
620
638
 
@@ -665,10 +683,8 @@ const TERMINATORS = new Set(['unreachable', 'return', 'br', 'br_table'])
665
683
  * @returns {Array}
666
684
  */
667
685
  const deadcode = (ast) => {
668
- const result = clone(ast)
669
-
670
686
  // Process each function body
671
- walk(result, (node) => {
687
+ walk(ast, (node) => {
672
688
  if (!Array.isArray(node)) return
673
689
  const kind = node[0]
674
690
 
@@ -686,7 +702,7 @@ const deadcode = (ast) => {
686
702
  }
687
703
  })
688
704
 
689
- return result
705
+ return ast
690
706
  }
691
707
 
692
708
  /**
@@ -741,9 +757,7 @@ const eliminateDeadInBlock = (block) => {
741
757
  * @returns {Array}
742
758
  */
743
759
  const localReuse = (ast) => {
744
- const result = clone(ast)
745
-
746
- walk(result, (node) => {
760
+ walk(ast, (node) => {
747
761
  if (!Array.isArray(node) || node[0] !== 'func') return
748
762
 
749
763
  // Collect local declarations and their types
@@ -792,7 +806,7 @@ const localReuse = (ast) => {
792
806
  }
793
807
  })
794
808
 
795
- return result
809
+ return ast
796
810
  }
797
811
 
798
812
  // ==================== PROPAGATION & LOCAL ELIMINATION ====================
@@ -870,8 +884,9 @@ const forwardPropagate = (funcNode, params, useCounts) => {
870
884
 
871
885
  if (op === 'param' || op === 'result' || op === 'local' || op === 'type' || op === 'export') continue
872
886
 
873
- // Track local.set values
874
- if (op === 'local.set' && instr.length === 3 && typeof instr[1] === 'string') {
887
+ // Track local.set / local.tee values (tee writes too — its result also leaves
888
+ // the value on the stack but the local is updated identically to set).
889
+ if ((op === 'local.set' || op === 'local.tee') && instr.length === 3 && typeof instr[1] === 'string') {
875
890
  substGets(instr[2], known) // substitute known values in RHS
876
891
  const uses = getUseCount(instr[1])
877
892
  known.set(instr[1], {
@@ -902,6 +917,14 @@ const forwardPropagate = (funcNode, params, useCounts) => {
902
917
  const prev = clone(instr)
903
918
  substGets(instr, known)
904
919
  if (!equal(prev, instr)) changed = true
920
+ // Invalidate tracking for any names written by a nested set/tee — those
921
+ // writes happened mid-expression and the substGets above used the
922
+ // pre-write tracked value (correct), but later reads must see the new
923
+ // (untracked) value, not the stale constant.
924
+ walk(instr, n => {
925
+ if (Array.isArray(n) && (n[0] === 'local.set' || n[0] === 'local.tee') && typeof n[1] === 'string')
926
+ known.delete(n[1])
927
+ })
905
928
  }
906
929
  }
907
930
 
@@ -1002,9 +1025,7 @@ const eliminateDeadStores = (funcNode, params, useCounts) => {
1002
1025
  * Multi-pass with batch counting for convergence.
1003
1026
  */
1004
1027
  const propagate = (ast) => {
1005
- const result = clone(ast)
1006
-
1007
- walk(result, (funcNode) => {
1028
+ walk(ast, (funcNode) => {
1008
1029
  if (!Array.isArray(funcNode) || funcNode[0] !== 'func') return
1009
1030
 
1010
1031
  const params = new Set()
@@ -1024,7 +1045,7 @@ const propagate = (ast) => {
1024
1045
  }
1025
1046
  })
1026
1047
 
1027
- return result
1048
+ return ast
1028
1049
  }
1029
1050
 
1030
1051
  // ==================== FUNCTION INLINING ====================
@@ -1036,12 +1057,11 @@ const propagate = (ast) => {
1036
1057
  */
1037
1058
  const inline = (ast) => {
1038
1059
  if (!Array.isArray(ast) || ast[0] !== 'module') return ast
1039
- const result = clone(ast)
1040
1060
 
1041
1061
  // Collect inlinable functions
1042
1062
  const inlinable = new Map() // $name → { body, params }
1043
1063
 
1044
- for (const node of result.slice(1)) {
1064
+ for (const node of ast.slice(1)) {
1045
1065
  if (!Array.isArray(node) || node[0] !== 'func') continue
1046
1066
 
1047
1067
  const name = typeof node[1] === 'string' && node[1][0] === '$' ? node[1] : null
@@ -1102,9 +1122,9 @@ const inline = (ast) => {
1102
1122
  }
1103
1123
 
1104
1124
  // Replace calls with inlined body
1105
- if (inlinable.size === 0) return result
1125
+ if (inlinable.size === 0) return ast
1106
1126
 
1107
- walkPost(result, (node) => {
1127
+ walkPost(ast, (node) => {
1108
1128
  if (!Array.isArray(node) || node[0] !== 'call') return
1109
1129
  const fname = node[1]
1110
1130
  if (!inlinable.has(fname)) return
@@ -1131,7 +1151,7 @@ const inline = (ast) => {
1131
1151
  return substituted
1132
1152
  })
1133
1153
 
1134
- return result
1154
+ return ast
1135
1155
  }
1136
1156
 
1137
1157
  // ==================== VACUUM ====================
@@ -1143,7 +1163,7 @@ const inline = (ast) => {
1143
1163
  * @returns {Array}
1144
1164
  */
1145
1165
  const vacuum = (ast) => {
1146
- return walkPost(clone(ast), (node) => {
1166
+ return walkPost(ast, (node) => {
1147
1167
  if (!Array.isArray(node)) return
1148
1168
  const op = node[0]
1149
1169
 
@@ -1269,7 +1289,7 @@ const PEEPHOLE = {
1269
1289
  * @returns {Array}
1270
1290
  */
1271
1291
  const peephole = (ast) => {
1272
- return walkPost(clone(ast), (node) => {
1292
+ return walkPost(ast, (node) => {
1273
1293
  if (!Array.isArray(node) || node.length !== 3) return
1274
1294
  const fn = PEEPHOLE[node[0]]
1275
1295
  if (!fn) return
@@ -1287,13 +1307,12 @@ const peephole = (ast) => {
1287
1307
  */
1288
1308
  const globals = (ast) => {
1289
1309
  if (!Array.isArray(ast) || ast[0] !== 'module') return ast
1290
- const result = clone(ast)
1291
1310
 
1292
1311
  // Find immutable globals with const init
1293
1312
  const constGlobals = new Map() // name → const node
1294
1313
  const mutableGlobals = new Set()
1295
1314
 
1296
- for (const node of result.slice(1)) {
1315
+ for (const node of ast.slice(1)) {
1297
1316
  if (!Array.isArray(node) || node[0] !== 'global') continue
1298
1317
  const name = typeof node[1] === 'string' && node[1][0] === '$' ? node[1] : null
1299
1318
  if (!name) continue
@@ -1311,7 +1330,7 @@ const globals = (ast) => {
1311
1330
  }
1312
1331
 
1313
1332
  // Also mark any global that is ever written as mutable
1314
- walk(result, (n) => {
1333
+ walk(ast, (n) => {
1315
1334
  if (!Array.isArray(n) || n[0] !== 'global.set') return
1316
1335
  const ref = n[1]
1317
1336
  if (typeof ref === 'string' && ref[0] === '$') mutableGlobals.add(ref)
@@ -1319,10 +1338,10 @@ const globals = (ast) => {
1319
1338
 
1320
1339
  // Remove mutable ones from propagation set
1321
1340
  for (const name of mutableGlobals) constGlobals.delete(name)
1322
- if (constGlobals.size === 0) return result
1341
+ if (constGlobals.size === 0) return ast
1323
1342
 
1324
1343
  // Substitute global.get with const
1325
- return walkPost(result, (node) => {
1344
+ return walkPost(ast, (node) => {
1326
1345
  if (!Array.isArray(node) || node[0] !== 'global.get' || node.length !== 2) return
1327
1346
  const ref = node[1]
1328
1347
  if (constGlobals.has(ref)) return clone(constGlobals.get(ref))
@@ -1333,7 +1352,7 @@ const globals = (ast) => {
1333
1352
 
1334
1353
  /** Match (type.load/store (i32.add ptr (type.const N))) and fold offset */
1335
1354
  const offset = (ast) => {
1336
- return walkPost(clone(ast), (node) => {
1355
+ return walkPost(ast, (node) => {
1337
1356
  if (!Array.isArray(node)) return
1338
1357
  const op = node[0]
1339
1358
  if (typeof op !== 'string' || (!op.endsWith('load') && !op.endsWith('store'))) return
@@ -1406,9 +1425,7 @@ const offset = (ast) => {
1406
1425
  * @returns {Array}
1407
1426
  */
1408
1427
  const unbranch = (ast) => {
1409
- const result = clone(ast)
1410
-
1411
- walk(result, (node) => {
1428
+ walk(ast, (node) => {
1412
1429
  if (!Array.isArray(node)) return
1413
1430
  const op = node[0]
1414
1431
  // Loops: `br $loop_label` jumps BACK to loop top (continue), not out.
@@ -1445,7 +1462,7 @@ const unbranch = (ast) => {
1445
1462
  }
1446
1463
  })
1447
1464
 
1448
- return result
1465
+ return ast
1449
1466
  }
1450
1467
 
1451
1468
  // ==================== STRIP MUT FROM GLOBALS ====================
@@ -1458,14 +1475,13 @@ const unbranch = (ast) => {
1458
1475
  */
1459
1476
  const stripmut = (ast) => {
1460
1477
  if (!Array.isArray(ast) || ast[0] !== 'module') return ast
1461
- const result = clone(ast)
1462
1478
 
1463
1479
  const written = new Set()
1464
- walk(result, (n) => {
1480
+ walk(ast, (n) => {
1465
1481
  if (Array.isArray(n) && n[0] === 'global.set' && typeof n[1] === 'string') written.add(n[1])
1466
1482
  })
1467
1483
 
1468
- return walkPost(result, (node) => {
1484
+ return walkPost(ast, (node) => {
1469
1485
  if (!Array.isArray(node) || node[0] !== 'global') return
1470
1486
  const name = typeof node[1] === 'string' && node[1][0] === '$' ? node[1] : null
1471
1487
  if (!name || written.has(name)) return
@@ -1490,7 +1506,7 @@ const stripmut = (ast) => {
1490
1506
  * @returns {Array}
1491
1507
  */
1492
1508
  const brif = (ast) => {
1493
- return walkPost(clone(ast), (node) => {
1509
+ return walkPost(ast, (node) => {
1494
1510
  if (!Array.isArray(node) || node[0] !== 'if') return
1495
1511
  const { cond, thenBranch, elseBranch } = parseIf(node)
1496
1512
  const thenEmpty = !thenBranch || thenBranch.length <= 1
@@ -1519,7 +1535,7 @@ const brif = (ast) => {
1519
1535
  * @returns {Array}
1520
1536
  */
1521
1537
  const foldarms = (ast) => {
1522
- return walkPost(clone(ast), (node) => {
1538
+ return walkPost(ast, (node) => {
1523
1539
  if (!Array.isArray(node) || node[0] !== 'if') return
1524
1540
  const { thenBranch, elseBranch } = parseIf(node)
1525
1541
  if (!thenBranch || !elseBranch) return
@@ -1611,13 +1627,12 @@ const hashFunc = (node, localNames) => {
1611
1627
  */
1612
1628
  const dedupe = (ast) => {
1613
1629
  if (!Array.isArray(ast) || ast[0] !== 'module') return ast
1614
- const result = clone(ast)
1615
1630
 
1616
1631
  // Hash function bodies (normalize local/param names to avoid false negatives)
1617
1632
  const signatures = new Map() // hash → canonical $name
1618
1633
  const redirects = new Map() // duplicate $name → canonical $name
1619
1634
 
1620
- for (const node of result.slice(1)) {
1635
+ for (const node of ast.slice(1)) {
1621
1636
  if (!Array.isArray(node) || node[0] !== 'func') continue
1622
1637
  const name = typeof node[1] === 'string' && node[1][0] === '$' ? node[1] : null
1623
1638
  if (!name) continue
@@ -1645,10 +1660,10 @@ const dedupe = (ast) => {
1645
1660
  }
1646
1661
  }
1647
1662
 
1648
- if (redirects.size === 0) return result
1663
+ if (redirects.size === 0) return ast
1649
1664
 
1650
1665
  // Rewrite all references: calls, ref.func, elem segments, call_indirect type
1651
- walkPost(result, (node) => {
1666
+ walkPost(ast, (node) => {
1652
1667
  if (!Array.isArray(node)) return
1653
1668
  const op = node[0]
1654
1669
  if ((op === 'call' || op === 'return_call') && redirects.has(node[1])) {
@@ -1671,7 +1686,7 @@ const dedupe = (ast) => {
1671
1686
  }
1672
1687
  })
1673
1688
 
1674
- return result
1689
+ return ast
1675
1690
  }
1676
1691
 
1677
1692
  // ==================== TYPE DEDUPLICATION ====================
@@ -1684,12 +1699,11 @@ const dedupe = (ast) => {
1684
1699
  */
1685
1700
  const dedupTypes = (ast) => {
1686
1701
  if (!Array.isArray(ast) || ast[0] !== 'module') return ast
1687
- const result = clone(ast)
1688
1702
 
1689
1703
  const signatures = new Map() // hash → canonical $name
1690
1704
  const redirects = new Map() // duplicate $name → canonical $name
1691
1705
 
1692
- for (const node of result.slice(1)) {
1706
+ for (const node of ast.slice(1)) {
1693
1707
  if (!Array.isArray(node) || node[0] !== 'type') continue
1694
1708
  const name = typeof node[1] === 'string' && node[1][0] === '$' ? node[1] : null
1695
1709
  if (!name) continue
@@ -1704,18 +1718,18 @@ const dedupTypes = (ast) => {
1704
1718
  }
1705
1719
  }
1706
1720
 
1707
- if (redirects.size === 0) return result
1721
+ if (redirects.size === 0) return ast
1708
1722
 
1709
1723
  // Remove duplicate type nodes
1710
- for (let i = result.length - 1; i >= 0; i--) {
1711
- const node = result[i]
1724
+ for (let i = ast.length - 1; i >= 0; i--) {
1725
+ const node = ast[i]
1712
1726
  if (Array.isArray(node) && node[0] === 'type') {
1713
1727
  const name = typeof node[1] === 'string' && node[1][0] === '$' ? node[1] : null
1714
- if (name && redirects.has(name)) result.splice(i, 1)
1728
+ if (name && redirects.has(name)) ast.splice(i, 1)
1715
1729
  }
1716
1730
  }
1717
1731
 
1718
- walkPost(result, (node) => {
1732
+ walkPost(ast, (node) => {
1719
1733
  if (!Array.isArray(node)) return
1720
1734
  const op = node[0]
1721
1735
 
@@ -1755,7 +1769,7 @@ const dedupTypes = (ast) => {
1755
1769
  }
1756
1770
  })
1757
1771
 
1758
- return result
1772
+ return ast
1759
1773
  }
1760
1774
 
1761
1775
  // ==================== DATA SEGMENT PACKING ====================
@@ -1895,10 +1909,9 @@ const mergeDataSegments = (a, b) => {
1895
1909
  */
1896
1910
  const packData = (ast) => {
1897
1911
  if (!Array.isArray(ast) || ast[0] !== 'module') return ast
1898
- let result = clone(ast)
1899
1912
 
1900
1913
  // Trim trailing zeros
1901
- for (const node of result) {
1914
+ for (const node of ast) {
1902
1915
  if (!Array.isArray(node) || node[0] !== 'data') continue
1903
1916
  let contentStart = 1
1904
1917
  if (typeof node[1] === 'string' && node[1][0] === '$') contentStart = 2
@@ -1917,8 +1930,8 @@ const packData = (ast) => {
1917
1930
 
1918
1931
  // Merge adjacent active segments with same memory and consecutive offsets
1919
1932
  const dataNodes = []
1920
- for (let i = 0; i < result.length; i++) {
1921
- const node = result[i]
1933
+ for (let i = 0; i < ast.length; i++) {
1934
+ const node = ast[i]
1922
1935
  if (Array.isArray(node) && node[0] === 'data') {
1923
1936
  const info = getDataOffset(node)
1924
1937
  if (info) {
@@ -1947,10 +1960,10 @@ const packData = (ast) => {
1947
1960
  }
1948
1961
 
1949
1962
  if (toRemove.size > 0) {
1950
- result = result.filter((_, i) => !toRemove.has(i))
1963
+ ast = ast.filter((_, i) => !toRemove.has(i))
1951
1964
  }
1952
1965
 
1953
- return result
1966
+ return ast
1954
1967
  }
1955
1968
 
1956
1969
  // ==================== IMPORT FIELD MINIFICATION ====================
@@ -1980,11 +1993,10 @@ const makeShortener = () => {
1980
1993
  */
1981
1994
  const minifyImports = (ast) => {
1982
1995
  if (!Array.isArray(ast) || ast[0] !== 'module') return ast
1983
- const result = clone(ast)
1984
1996
  const shortMod = makeShortener()
1985
1997
  const shortField = makeShortener()
1986
1998
 
1987
- for (const node of result) {
1999
+ for (const node of ast) {
1988
2000
  if (!Array.isArray(node) || node[0] !== 'import') continue
1989
2001
  if (typeof node[1] === 'string' && node[1][0] === '"') {
1990
2002
  node[1] = '"' + shortMod(node[1].slice(1, -1)) + '"'
@@ -1994,7 +2006,7 @@ const minifyImports = (ast) => {
1994
2006
  }
1995
2007
  }
1996
2008
 
1997
- return result
2009
+ return ast
1998
2010
  }
1999
2011
 
2000
2012
  // ==================== REORDER FUNCTIONS ====================
@@ -2033,10 +2045,9 @@ const reorder = (ast) => {
2033
2045
  // Sorting changes the function index space. Skip if any reference is numeric,
2034
2046
  // since we'd silently retarget unnamed callers/start/elem entries.
2035
2047
  if (!reorderSafe(ast)) return ast
2036
- const result = clone(ast)
2037
2048
 
2038
2049
  const callCounts = new Map()
2039
- walk(result, (n) => {
2050
+ walk(ast, (n) => {
2040
2051
  if (!Array.isArray(n)) return
2041
2052
  if (n[0] === 'call' || n[0] === 'return_call') {
2042
2053
  callCounts.set(n[1], (callCounts.get(n[1]) || 0) + 1)
@@ -2045,7 +2056,7 @@ const reorder = (ast) => {
2045
2056
 
2046
2057
  // Imports must precede defined funcs (compile.js assigns indices in AST order).
2047
2058
  const imports = [], funcs = [], others = []
2048
- for (const node of result.slice(1)) {
2059
+ for (const node of ast.slice(1)) {
2049
2060
  if (!Array.isArray(node)) { others.push(node); continue }
2050
2061
  if (node[0] === 'import') imports.push(node)
2051
2062
  else if (node[0] === 'func') funcs.push(node)
@@ -2072,14 +2083,18 @@ const reorder = (ast) => {
2072
2083
  */
2073
2084
  export default function optimize(ast, opts = true) {
2074
2085
  if (typeof ast === 'string') ast = parse(ast)
2075
- ast = clone(ast)
2086
+ const strictGuard = opts === true // default: zero tolerance for bloat
2076
2087
  opts = normalize(opts)
2077
2088
 
2078
- // Each pass clones its input before mutating, so the original `before`
2079
- // reference stays untouched and can be used for the convergence check
2080
- // without an extra deep clone.
2081
- for (let round = 0; round < 6; round++) {
2082
- const before = ast
2089
+ const log = opts.log ? (msg, delta) => opts.log(msg, delta) : () => {}
2090
+ const verbose = opts.verbose || opts.log
2091
+
2092
+ ast = clone(ast)
2093
+ let beforeRound = null
2094
+
2095
+ for (let round = 0; round < 3; round++) {
2096
+ beforeRound = clone(ast)
2097
+ const sizeBefore = count(ast)
2083
2098
 
2084
2099
  if (opts.stripmut) ast = stripmut(ast)
2085
2100
  if (opts.globals) ast = globals(ast)
@@ -2103,10 +2118,29 @@ export default function optimize(ast, opts = true) {
2103
2118
  if (opts.reorder) ast = reorder(ast)
2104
2119
  if (opts.treeshake) ast = treeshake(ast)
2105
2120
  if (opts.minifyImports) ast = minifyImports(ast)
2106
- if (equal(before, ast)) break
2121
+
2122
+ const sizeAfter = count(ast)
2123
+ const delta = sizeAfter - sizeBefore
2124
+
2125
+ if (verbose || delta !== 0) {
2126
+ log(` round ${round + 1}: ${delta > 0 ? '+' : ''}${delta} nodes`, delta)
2127
+ }
2128
+
2129
+ // Size guard: default optimize must never inflate. Explicit passes
2130
+ // get leniency (+5 nodes) so inline/propagate/foldarms can chain.
2131
+ const tolerance = strictGuard ? 0 : 5
2132
+ if (delta > tolerance) {
2133
+ if (verbose) log(` ⚠ round ${round + 1} inflated by ${delta}, reverting`, delta)
2134
+ ast = beforeRound
2135
+ break
2136
+ }
2137
+
2138
+ if (equal(beforeRound, ast)) break
2107
2139
  }
2108
2140
 
2109
2141
  return ast
2110
2142
  }
2111
2143
 
2144
+ /** Count AST nodes (fast size heuristic). */
2145
+ export { count as size, count, binarySize }
2112
2146
  export { optimize, treeshake, fold, deadcode, localReuse, identity, strength, branch, propagate, inline, normalize, OPTS, vacuum, peephole, globals, offset, unbranch, stripmut, brif, foldarms, dedupe, reorder, dedupTypes, packData, minifyImports }
@@ -41,6 +41,8 @@ export namespace TYPE {
41
41
  export let i31: number;
42
42
  export let struct: number;
43
43
  export let array: number;
44
+ let data_1: number;
45
+ export { data_1 as data };
44
46
  export let cont: number;
45
47
  export let nocont: number;
46
48
  export let string: number;
@@ -1 +1 @@
1
- {"version":3,"file":"encode.d.ts","sourceRoot":"","sources":["../../src/encode.js"],"names":[],"mappings":"AA6CA;;;;;;GAMG;AACH,6BAHW,MAAM,GACJ,MAAM,EAAE,CAapB;AAED;;;;;;GAMG;AACH,uBAJW,MAAM,GAAC,MAAM,WACb,MAAM,EAAE,GACN,MAAM,EAAE,CAepB;;IAQD,4BAIC;;AAED;;;;;;GAMG;AACH,uBAJW,MAAM,GAAC,MAAM,WACb,MAAM,EAAE,GACN,MAAM,EAAE,CAoBpB;;IAID,4BAMC;;AAGD,gEAaC;;IA0DD,mCAA6D;;AAvD7D,gEAaC;;IAED,iDAsCC;;AA/LM,wBAJI,MAAM,GAAC,MAAM,GAAC,MAAM,GAAC,IAAI,WACzB,MAAM,EAAE,GACN,MAAM,EAAE,CA8BpB;AAsBD;;;;;;GAMG;AACH,sBAJW,MAAM,GAAC,MAAM,WACb,MAAM,EAAE,GACN,MAAM,EAAE,CAepB;;AApBD;;;;;;GAMG;AACH,uBAJW,MAAM,GAAC,MAAM,WACb,MAAM,EAAE,GACN,MAAM,EAAE,CAepB;;AA6HM,wCAKN"}
1
+ {"version":3,"file":"encode.d.ts","sourceRoot":"","sources":["../../src/encode.js"],"names":[],"mappings":"AA6CA;;;;;;GAMG;AACH,6BAHW,MAAM,GACJ,MAAM,EAAE,CAapB;AAED;;;;;;GAMG;AACH,uBAJW,MAAM,GAAC,MAAM,WACb,MAAM,EAAE,GACN,MAAM,EAAE,CAepB;;IAQD,4BAIC;;AAED;;;;;;GAMG;AACH,uBAJW,MAAM,GAAC,MAAM,WACb,MAAM,EAAE,GACN,MAAM,EAAE,CAoBpB;;IAID,4BAmBC;;AAGD,gEAcC;;IAiED,mCAA6D;;AA9D7D,gEAcC;;IAED,iDA4CC;;AApNM,wBAJI,MAAM,GAAC,MAAM,GAAC,MAAM,GAAC,IAAI,WACzB,MAAM,EAAE,GACN,MAAM,EAAE,CA8BpB;AAsBD;;;;;;GAMG;AACH,sBAJW,MAAM,GAAC,MAAM,WACb,MAAM,EAAE,GACN,MAAM,EAAE,CAepB;;AApBD;;;;;;GAMG;AACH,uBAJW,MAAM,GAAC,MAAM,WACb,MAAM,EAAE,GACN,MAAM,EAAE,CAepB;;AAkJM,wCAKN"}