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/dist/watr.js +134 -99
- package/dist/watr.min.js +6 -6
- package/package.json +1 -1
- package/src/compile.js +9 -2
- package/src/const.js +2 -2
- package/src/encode.js +33 -12
- package/src/optimize.js +119 -85
- package/types/src/const.d.ts +2 -0
- package/types/src/encode.d.ts.map +1 -1
- package/types/src/optimize.d.ts +13 -0
- package/types/src/optimize.d.ts.map +1 -1
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
|
-
|
|
132
|
-
|
|
133
|
-
|
|
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
|
-
|
|
140
|
-
|
|
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
|
-
|
|
156
|
-
|
|
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
|
-
|
|
184
|
-
|
|
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:
|
|
20
|
-
inline:
|
|
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:
|
|
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:
|
|
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
|
-
|
|
66
|
-
//
|
|
67
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
1125
|
+
if (inlinable.size === 0) return ast
|
|
1106
1126
|
|
|
1107
|
-
walkPost(
|
|
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
|
|
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(
|
|
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(
|
|
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
|
|
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(
|
|
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
|
|
1341
|
+
if (constGlobals.size === 0) return ast
|
|
1323
1342
|
|
|
1324
1343
|
// Substitute global.get with const
|
|
1325
|
-
return walkPost(
|
|
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(
|
|
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
|
-
|
|
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
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
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
|
|
1663
|
+
if (redirects.size === 0) return ast
|
|
1649
1664
|
|
|
1650
1665
|
// Rewrite all references: calls, ref.func, elem segments, call_indirect type
|
|
1651
|
-
walkPost(
|
|
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
|
|
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
|
|
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
|
|
1721
|
+
if (redirects.size === 0) return ast
|
|
1708
1722
|
|
|
1709
1723
|
// Remove duplicate type nodes
|
|
1710
|
-
for (let i =
|
|
1711
|
-
const node =
|
|
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))
|
|
1728
|
+
if (name && redirects.has(name)) ast.splice(i, 1)
|
|
1715
1729
|
}
|
|
1716
1730
|
}
|
|
1717
1731
|
|
|
1718
|
-
walkPost(
|
|
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
|
|
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
|
|
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 <
|
|
1921
|
-
const node =
|
|
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
|
-
|
|
1963
|
+
ast = ast.filter((_, i) => !toRemove.has(i))
|
|
1951
1964
|
}
|
|
1952
1965
|
|
|
1953
|
-
return
|
|
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
|
|
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
|
|
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(
|
|
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
|
|
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
|
-
|
|
2086
|
+
const strictGuard = opts === true // default: zero tolerance for bloat
|
|
2076
2087
|
opts = normalize(opts)
|
|
2077
2088
|
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
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
|
-
|
|
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 }
|
package/types/src/const.d.ts
CHANGED
|
@@ -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,
|
|
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"}
|