jz 0.8.0 → 0.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bench/bench.svg +14 -14
- package/dist/jz.js +897 -804
- package/module/array.js +20 -0
- package/module/math.js +148 -121
- package/module/string.js +133 -15
- package/package.json +2 -2
- package/src/abi/string.js +11 -6
- package/src/compile/emit.js +77 -17
- package/src/compile/index.js +6 -0
- package/src/compile/infer.js +8 -1
- package/src/compile/loop-recurrence.js +167 -0
- package/src/compile/narrow.js +12 -3
- package/src/optimize/index.js +166 -1
- package/src/optimize/vectorize.js +10 -0
- package/src/prepare/index.js +18 -5
package/src/optimize/index.js
CHANGED
|
@@ -97,6 +97,14 @@ export const PASS_NAMES = [
|
|
|
97
97
|
'hoistInvariantLoop', // unified LICM (subsumes the former ToInt32/PtrOffsetLoop/CellLoads hoists)
|
|
98
98
|
'narrowLoopBound', // f64 loop bound → hoisted i32 (unblocks the lane-vectorizer)
|
|
99
99
|
'splitCharScan', // charCodeAt scan loops: split at min(N, s.length) → i32 char carrier (plan-level)
|
|
100
|
+
// Pre-analyze loop-shape transforms — applied in compile/index.js (NOT this pass pipeline), but
|
|
101
|
+
// gated by these flags. Listing them here is load-bearing: ALL_OFF sets them false so level 0/1
|
|
102
|
+
// (the self-host fast path) actually skip them, instead of `undefined !== false` running them
|
|
103
|
+
// unconditionally at every level. ALL_ON keeps them on at level 2+ where they belong.
|
|
104
|
+
'loopIVDivMod', // strength-reduce per-iter `i%w` / `(i/w)|0` → incremental i32 counters
|
|
105
|
+
'loopSquare', // bounded `i*i < CONST` → Math.imul (i32 product chain)
|
|
106
|
+
'unrollRecurrence', // unit-stride DP/scan: scalar-replace arr[j-1]/arr[j] recurrence + ×2 unroll
|
|
107
|
+
'clampPeel', // edge-clamp stencil: split into clamp-free interior (vectorizes) + edges
|
|
100
108
|
'hoistGlobalPtrOffset', // stable typed GLOBALS: __ptr_offset resolve → once per function (post-watr, module-level)
|
|
101
109
|
'fusedRewrite', // peephole + ptr-helper inline + memarg fold
|
|
102
110
|
'hoistAddrBase',
|
|
@@ -106,6 +114,7 @@ export const PASS_NAMES = [
|
|
|
106
114
|
'unswitchTypedParamLoop', // Float64Array param loop-unswitch → base-hoisted f64.load/store fast path (vectorizes)
|
|
107
115
|
'dropDeadZeroInit',
|
|
108
116
|
'deadStoreElim',
|
|
117
|
+
'propagateSingleUse', // forward-substitute single-def/single-use pure temps (watr's "propagate")
|
|
109
118
|
'promoteGlobals', // read-only global.get → local for multi-read globals
|
|
110
119
|
'sortLocalsByUse',
|
|
111
120
|
'specializeMkptr',
|
|
@@ -157,10 +166,14 @@ const LEVEL_PRESETS = Object.freeze({
|
|
|
157
166
|
...ALL_ON,
|
|
158
167
|
smallConstForUnroll: false, nestedSmallConstForUnroll: false, vectorizeLaneLocal: false, splitCharScan: false,
|
|
159
168
|
recursionUnroll: false, // body tripling is a size regression — speed-only
|
|
169
|
+
unrollRecurrence: false, // ×2 body duplication is a size regression — speed-only
|
|
170
|
+
clampPeel: false, // edge-clamp peel triples a stencil loop (clamp-free interior + 2 edges) to vectorize — speed-only
|
|
160
171
|
|
|
161
172
|
boolConvertToSelect: false, // adds a const + op per site — speed-only latency trade
|
|
162
173
|
devirtIndirect: false, // guards + duplicated args grow bytes — speed-only trade
|
|
163
174
|
internStrings: false, // the intern index costs ~16 B per eligible literal — speed-only trade
|
|
175
|
+
promoteGlobals: false, // snapshots a multi-read global into an entry local — pure speed (V8 can't CSE a mutable global); the snapshot is dead size weight at -Os
|
|
176
|
+
hoistInvariantLoop: false,// LICM: hoists loop-invariants to entry temps — a speed/latency trade whose entry cost outweighs the per-iter saving in bytes
|
|
164
177
|
scalarTypedLoopUnroll: 4, scalarTypedNestedUnroll: 8, scalarTypedArrayLen: 8,
|
|
165
178
|
}),
|
|
166
179
|
// 'speed' === level 3: full watr (inlining on) + L3 cap/hash tuning, pool off.
|
|
@@ -589,6 +602,25 @@ const NON_MUTATING_CALLS = new Set(['$__is_str_key', '$__str_concat', '$__to_num
|
|
|
589
602
|
// `for(j) { ... grid[i][j] ... }` inner loop (the jagged-array deopt).
|
|
590
603
|
const READONLY_MEM_CALLS = new Set(['$__typed_idx', '$__str_idx'])
|
|
591
604
|
|
|
605
|
+
// PURE FUNCTION calls — result is a function of the ARGUMENTS alone, with no
|
|
606
|
+
// dependence on mutable state: math reads no memory; the string search/compare
|
|
607
|
+
// helpers read only their operands' bytes, and jz strings are IMMUTABLE, so their
|
|
608
|
+
// content can't change under the loop's stores. So on loop-invariant args the
|
|
609
|
+
// RESULT is loop-invariant regardless of anything else the loop does (unlike a
|
|
610
|
+
// READONLY_MEM_CALLS element read, which an aliasing store could invalidate — no
|
|
611
|
+
// !hasDirectStore guard is needed here). Speculative pre-header execution can't
|
|
612
|
+
// trap: math returns NaN/∞ rather than trapping, and a value reaching .indexOf/===
|
|
613
|
+
// is a valid string (in-bounds reads). This is the LICM V8's wasm tier won't do —
|
|
614
|
+
// it treats every call as opaque and recomputes the search/transcendental each
|
|
615
|
+
// iteration. (Math.random is INLINED — it mutates a global PRNG seed, never a
|
|
616
|
+
// `$math.` call — but exclude it by name defensively; $__str_eq_cold is the cold
|
|
617
|
+
// half of __str_eq, equally pure.) byteLen/length stay OUT: $__length is polymorphic
|
|
618
|
+
// over MUTABLE arrays (push changes it), so it isn't arg-pure.
|
|
619
|
+
const PURE_CALL_I32 = new Set(['$__str_indexof', '$__str_lastindexof', '$__str_eq', '$__str_eq_cold', '$__is_str_key'])
|
|
620
|
+
const isPureFnCall = (callee) =>
|
|
621
|
+
typeof callee === 'string' &&
|
|
622
|
+
((callee.startsWith('$math.') && !callee.startsWith('$math.random')) || PURE_CALL_I32.has(callee))
|
|
623
|
+
|
|
592
624
|
export function hoistInvariantPtrOffset(fn) {
|
|
593
625
|
if (!Array.isArray(fn) || fn[0] !== 'func') return
|
|
594
626
|
const bodyStart = findBodyStart(fn)
|
|
@@ -796,7 +828,7 @@ function loopInvariance(loopNode, { distinctParams, baseParamOf }) {
|
|
|
796
828
|
if (op.startsWith('v128.') || /^[if]\d+x\d+\./.test(op)) hasV128 = true
|
|
797
829
|
if (op === 'local.set' || op === 'local.tee') { if (typeof node[1] === 'string') locals.add(node[1]); for (let i = 2; i < node.length; i++) scan(node[i]); return }
|
|
798
830
|
if (op === 'global.set') { if (typeof node[1] === 'string') globals.add(node[1]); for (let i = 2; i < node.length; i++) scan(node[i]); return }
|
|
799
|
-
if (op === 'call') { hasAnyCall = true; if (!SAFE_OFFSET_CALLS.has(node[1]) && !READONLY_MEM_CALLS.has(node[1]) && !NON_MUTATING_CALLS.has(node[1])) hasUnsafeCall = true; for (let i = 2; i < node.length; i++) scan(node[i]); return }
|
|
831
|
+
if (op === 'call') { hasAnyCall = true; if (!SAFE_OFFSET_CALLS.has(node[1]) && !READONLY_MEM_CALLS.has(node[1]) && !NON_MUTATING_CALLS.has(node[1]) && !isPureFnCall(node[1])) hasUnsafeCall = true; for (let i = 2; i < node.length; i++) scan(node[i]); return }
|
|
800
832
|
if (op === 'call_ref' || op === 'call_indirect') { hasAnyCall = hasUnsafeCall = true; for (let i = 1; i < node.length; i++) scan(node[i]); return }
|
|
801
833
|
if ((op === 'f64.store' || op === 'i32.store') && node.length >= 3) {
|
|
802
834
|
hasDirectStore = true
|
|
@@ -843,6 +875,13 @@ function loopInvariance(loopNode, { distinctParams, baseParamOf }) {
|
|
|
843
875
|
return false
|
|
844
876
|
}
|
|
845
877
|
if (op === 'call') {
|
|
878
|
+
// Pure-function call: invariant iff its ARGS are. No effect-summary barrier —
|
|
879
|
+
// its result depends on nothing the loop can mutate (math: no memory; string
|
|
880
|
+
// search/compare: immutable operands), so neither a store nor another call
|
|
881
|
+
// can invalidate it. This hoists the loop-invariant transcendental / substr
|
|
882
|
+
// search V8's wasm tier recomputes every iteration.
|
|
883
|
+
if (isPureFnCall(node[1]))
|
|
884
|
+
return node.slice(2).every(c => pureGiven(c, bound))
|
|
846
885
|
if (SAFE_OFFSET_CALLS.has(node[1]))
|
|
847
886
|
return !hasUnsafeCall && node.slice(2).every(c => pureGiven(c, bound))
|
|
848
887
|
// Read-only heap reads: additionally require no direct store (alias-safe).
|
|
@@ -1088,6 +1127,8 @@ export function hoistInvariantLoop(fn) {
|
|
|
1088
1127
|
// SAFE_OFFSET_CALLS all return i32; READONLY_MEM_CALLS return f64 (NaN-boxed element)
|
|
1089
1128
|
if (SAFE_OFFSET_CALLS.has(node[1])) return 'i32'
|
|
1090
1129
|
if (READONLY_MEM_CALLS.has(node[1])) return 'f64'
|
|
1130
|
+
if (PURE_CALL_I32.has(node[1])) return 'i32' // string search/compare → i32
|
|
1131
|
+
if (typeof node[1] === 'string' && node[1].startsWith('$math.')) return 'f64' // transcendentals → f64
|
|
1091
1132
|
return null
|
|
1092
1133
|
}
|
|
1093
1134
|
if (op === 'local.get' || op === 'local.tee') return localTypes.get(node[1]) ?? null
|
|
@@ -2127,6 +2168,121 @@ export function deadStoreElim(fn) {
|
|
|
2127
2168
|
}
|
|
2128
2169
|
}
|
|
2129
2170
|
|
|
2171
|
+
/**
|
|
2172
|
+
* Forward-substitute a single-def / single-use local into its sole use, eliminating the local,
|
|
2173
|
+
* its `local.set` and its `local.get`. This is watr's "propagate": jz emits short-lived address/
|
|
2174
|
+
* index temps (`set $t (i32.add …); … (load $t) …`) that the WAT optimizer folds away — the
|
|
2175
|
+
* dominant slice of the watr-optimizer-OFF size gap (a matmul carries ~14 such temps, ≈ the whole
|
|
2176
|
+
* delta). Closing it lets jz lean on its own optimizer instead of watr's.
|
|
2177
|
+
*
|
|
2178
|
+
* SOUND only when moving the def's RHS `E` to the use can't change order or value:
|
|
2179
|
+
* - `E` is PURE — reads only locals (no load/store/call/global/memory): its value is a function
|
|
2180
|
+
* of its read-locals alone, and it has no side effects to reorder past intervening statements.
|
|
2181
|
+
* - the use is at the SAME loop nesting — never under a deeper `loop` than the def (which would
|
|
2182
|
+
* re-evaluate `E` per iteration and could read a clobbered input).
|
|
2183
|
+
* - no read-local of `E` is written between the def and the use (incl. the use statement itself).
|
|
2184
|
+
* Anything it can't prove safe is left untouched.
|
|
2185
|
+
*/
|
|
2186
|
+
export function propagateSingleUse(fn) {
|
|
2187
|
+
if (!Array.isArray(fn) || fn[0] !== 'func') return
|
|
2188
|
+
const bodyStart = findBodyStart(fn)
|
|
2189
|
+
if (bodyStart < 0) return
|
|
2190
|
+
|
|
2191
|
+
// Leave a vectorized function alone: it carries v128 lane sequences that are already register-tight,
|
|
2192
|
+
// and forward-substituting into them only adds pressure (mirrors hoistInvariantLoop's hasV128 gate).
|
|
2193
|
+
let hasV128 = false
|
|
2194
|
+
const scanV = (n) => { if (hasV128 || !Array.isArray(n)) return; const op = n[0]; if (typeof op === 'string' && (op.startsWith('v128.') || /^[if]\d+x\d+\./.test(op))) { hasV128 = true; return } for (let i = 1; i < n.length; i++) scanV(n[i]) }
|
|
2195
|
+
for (let i = bodyStart; i < fn.length && !hasV128; i++) scanV(fn[i])
|
|
2196
|
+
if (hasV128) return
|
|
2197
|
+
|
|
2198
|
+
// def/use tally over the whole body. A `local.tee` is both a read and a write — exclude any
|
|
2199
|
+
// tee'd local from candidacy rather than reason about it.
|
|
2200
|
+
const setN = new Map(), getN = new Map(), teed = new Set()
|
|
2201
|
+
const tally = (n) => {
|
|
2202
|
+
if (!Array.isArray(n)) return
|
|
2203
|
+
const op = n[0]
|
|
2204
|
+
if (op === 'local.set' && typeof n[1] === 'string') setN.set(n[1], (setN.get(n[1]) || 0) + 1)
|
|
2205
|
+
else if (op === 'local.tee' && typeof n[1] === 'string') teed.add(n[1])
|
|
2206
|
+
else if (op === 'local.get' && typeof n[1] === 'string') getN.set(n[1], (getN.get(n[1]) || 0) + 1)
|
|
2207
|
+
for (let i = 1; i < n.length; i++) tally(n[i])
|
|
2208
|
+
}
|
|
2209
|
+
for (let i = bodyStart; i < fn.length; i++) tally(fn[i])
|
|
2210
|
+
|
|
2211
|
+
const cand = new Set()
|
|
2212
|
+
for (const [name, c] of setN) if (c === 1 && getN.get(name) === 1 && !teed.has(name)) cand.add(name)
|
|
2213
|
+
if (!cand.size) return
|
|
2214
|
+
|
|
2215
|
+
const movablePure = (n) => {
|
|
2216
|
+
if (!Array.isArray(n)) return true
|
|
2217
|
+
const op = n[0]
|
|
2218
|
+
if (op === 'local.get') return true
|
|
2219
|
+
if (op === 'local.set' || op === 'local.tee' || op === 'call' || op === 'call_indirect' || op === 'call_ref'
|
|
2220
|
+
|| op === 'global.get' || op === 'global.set' || op === 'memory.size' || op === 'memory.grow'
|
|
2221
|
+
|| op === 'memory.copy' || op === 'memory.fill') return false
|
|
2222
|
+
if (typeof op === 'string' && (op.includes('.load') || op.includes('.store') || op.includes('.atomic'))) return false
|
|
2223
|
+
for (let i = 1; i < n.length; i++) if (!movablePure(n[i])) return false
|
|
2224
|
+
return true
|
|
2225
|
+
}
|
|
2226
|
+
const readsOf = (n, out) => { if (!Array.isArray(n)) return; if (n[0] === 'local.get' && typeof n[1] === 'string') out.add(n[1]); for (let i = 1; i < n.length; i++) readsOf(n[i], out) }
|
|
2227
|
+
const writesAny = (n, R) => { if (!Array.isArray(n)) return false; if ((n[0] === 'local.set' || n[0] === 'local.tee') && R.has(n[1])) return true; for (let i = 1; i < n.length; i++) if (writesAny(n[i], R)) return true; return false }
|
|
2228
|
+
// Locate the (local.get $t) within `root`'s subtree (not root itself); flag if it sits under a
|
|
2229
|
+
// `loop` relative to root (→ would re-evaluate the moved RHS each iteration).
|
|
2230
|
+
const locateUse = (root, t) => {
|
|
2231
|
+
let found = null
|
|
2232
|
+
const rec = (node, underLoop) => {
|
|
2233
|
+
if (found || !Array.isArray(node)) return
|
|
2234
|
+
for (let i = 1; i < node.length; i++) {
|
|
2235
|
+
if (found) return
|
|
2236
|
+
const c = node[i]
|
|
2237
|
+
if (Array.isArray(c) && c[0] === 'local.get' && c[1] === t) { found = { parent: node, idx: i, underLoop }; return }
|
|
2238
|
+
rec(c, underLoop || node[0] === 'loop')
|
|
2239
|
+
}
|
|
2240
|
+
}
|
|
2241
|
+
rec(root, false)
|
|
2242
|
+
return found
|
|
2243
|
+
}
|
|
2244
|
+
|
|
2245
|
+
const removed = new Set()
|
|
2246
|
+
const optimizeList = (list, start) => {
|
|
2247
|
+
for (let i = start; i < list.length; i++) {
|
|
2248
|
+
const s = list[i]
|
|
2249
|
+
if (!Array.isArray(s)) continue
|
|
2250
|
+
// s.length === 3: an explicit-RHS `(local.set $t E)`. A bare `(local.set $t)` (length 2)
|
|
2251
|
+
// binds a value already on the stack — e.g. the try_table catch payload — and has no RHS to
|
|
2252
|
+
// move; treating its undefined RHS as movable would substitute `undefined` into the use.
|
|
2253
|
+
if (s[0] === 'local.set' && s.length === 3 && typeof s[1] === 'string' && cand.has(s[1]) && movablePure(s[2])) {
|
|
2254
|
+
const t = s[1], E = s[2], R = new Set(); readsOf(E, R)
|
|
2255
|
+
for (let j = i + 1; j < list.length; j++) {
|
|
2256
|
+
const sj = list[j]
|
|
2257
|
+
const u = Array.isArray(sj) ? locateUse(sj, t) : null
|
|
2258
|
+
if (u) { // found the sole use's statement
|
|
2259
|
+
if (!u.underLoop && !writesAny(sj, R)) { // same nesting + read-locals intact
|
|
2260
|
+
u.parent[u.idx] = E
|
|
2261
|
+
list.splice(i, 1)
|
|
2262
|
+
cand.delete(t); removed.add(t)
|
|
2263
|
+
i-- // re-process from the freed slot (forward chains)
|
|
2264
|
+
}
|
|
2265
|
+
break // use located — stop scanning this candidate
|
|
2266
|
+
}
|
|
2267
|
+
if (writesAny(sj, R)) break // a read-local clobbered before the use → can't move
|
|
2268
|
+
}
|
|
2269
|
+
if (removed.has(s[1])) continue
|
|
2270
|
+
}
|
|
2271
|
+
// recurse into nested statement lists
|
|
2272
|
+
if (s[0] === 'block' || s[0] === 'loop') {
|
|
2273
|
+
let k = 1; while (k < s.length && Array.isArray(s[k]) && s[k][0] === 'result') k++
|
|
2274
|
+
optimizeList(s, k)
|
|
2275
|
+
} else if (s[0] === 'if') {
|
|
2276
|
+
for (let k = 1; k < s.length; k++) { const c = s[k]; if (Array.isArray(c) && (c[0] === 'then' || c[0] === 'else')) optimizeList(c, 1) }
|
|
2277
|
+
}
|
|
2278
|
+
}
|
|
2279
|
+
}
|
|
2280
|
+
optimizeList(fn, bodyStart)
|
|
2281
|
+
|
|
2282
|
+
// drop the now-orphaned decls (deferred so the body walk above sees stable indices)
|
|
2283
|
+
if (removed.size) for (let i = fn.length - 1; i >= 2; i--) { const c = fn[i]; if (Array.isArray(c) && c[0] === 'local' && removed.has(c[1])) fn.splice(i, 1) }
|
|
2284
|
+
}
|
|
2285
|
+
|
|
2130
2286
|
/**
|
|
2131
2287
|
* Module-wide scan for "volatile" globals — those mutated (`global.set`) in any
|
|
2132
2288
|
* function other than `$__start`. Globals written only in `$__start` are
|
|
@@ -3216,6 +3372,7 @@ export function optimizeFunc(fn, cfg, globalTypes, volatileGlobals, phase = 'pre
|
|
|
3216
3372
|
cfg.csePureExpr === false &&
|
|
3217
3373
|
cfg.dropDeadZeroInit === false &&
|
|
3218
3374
|
cfg.deadStoreElim === false &&
|
|
3375
|
+
cfg.propagateSingleUse === false &&
|
|
3219
3376
|
cfg.promoteGlobals === false &&
|
|
3220
3377
|
cfg.sortLocalsByUse === false &&
|
|
3221
3378
|
cfg.vectorizeLaneLocal === false) return
|
|
@@ -3277,6 +3434,14 @@ export function optimizeFunc(fn, cfg, globalTypes, volatileGlobals, phase = 'pre
|
|
|
3277
3434
|
// iteration, the hot-loop waste audit-fixpoint.mjs flagged on dot/sum.
|
|
3278
3435
|
if (runVectorizer) foldV128Memargs(fn)
|
|
3279
3436
|
}
|
|
3437
|
+
// Forward-substitute single-use temps — AFTER the vectorizer, never before: it pattern-matches a
|
|
3438
|
+
// STRAIGHT-LINE `s += a[i]*2`, and folding an address/index temp out scrambles it (the typed-array
|
|
3439
|
+
// loop fell from a SIMD body to a scalar unroll, +231 B). For watr:false the whole pipeline is the
|
|
3440
|
+
// 'pre' phase (no 'post' re-run), so vectorize already ran above; for full watr the vectorizer is
|
|
3441
|
+
// deferred to 'post', so skip 'pre' here to stay after it. (propagateSingleUse itself skips any
|
|
3442
|
+
// function the vectorizer already lifted to v128.)
|
|
3443
|
+
const fullWatr_psu = !!(cfg && (cfg.watr === true || typeof cfg.watr === 'object'))
|
|
3444
|
+
if ((phase === 'post' || !fullWatr_psu) && (!cfg || cfg.propagateSingleUse !== false)) propagateSingleUse(fn)
|
|
3280
3445
|
// SSA-split loop-private unrolled scratch (post-vectorize: vectorized loops now carry
|
|
3281
3446
|
// v128 and are skipped) so the LICM below hoists the per-iteration invariants the
|
|
3282
3447
|
// unroller's name-merging hid — rust/LLVM's free-after-unroll register hoist (closes
|
|
@@ -4061,6 +4061,16 @@ function tryDivergentEscapeVectorize(blockNode, fnLocals, freshIdRef) {
|
|
|
4061
4061
|
const kTop = kindOf(keepTopC)
|
|
4062
4062
|
const limitKeep = keepTopC // initialised here; may be reassigned below for single-break
|
|
4063
4063
|
let kMid = null, boundExpr, boundI32, itLeft
|
|
4064
|
+
// Compound-top guard `while (A && B)`: classify the SECOND keep too. Left unclassified (kMid=null),
|
|
4065
|
+
// the downstream lift/keepMask treats keepMidC as a per-lane f64 escape — right for the Julia order
|
|
4066
|
+
// (limit, escape) but it then lifts the i32 LIMIT of the mandelbrot order (escape, `it<MAX`) into an
|
|
4067
|
+
// f64 lane and crashes. Setting kMid routes the limit through the scalar guard instead; for the
|
|
4068
|
+
// Julia order kMid='escape' is identical to the old null (both lift). Bail if neither keep is an
|
|
4069
|
+
// escape — a two-limit guard has no per-lane divergence for this vectorizer to exploit.
|
|
4070
|
+
if (compoundTop) {
|
|
4071
|
+
kMid = kindOf(keepMidC)
|
|
4072
|
+
if (kTop === 'limit' && kMid === 'limit') return null
|
|
4073
|
+
}
|
|
4064
4074
|
if (midBreaks.length === 1) {
|
|
4065
4075
|
kMid = kindOf(keepMidC)
|
|
4066
4076
|
if (kTop === kMid) return null
|
package/src/prepare/index.js
CHANGED
|
@@ -196,6 +196,19 @@ const stripBoolNot = c => {
|
|
|
196
196
|
while (Array.isArray(c) && c[0] === '!' && Array.isArray(c[1]) && c[1][0] === '!') c = c[1][1]
|
|
197
197
|
return c
|
|
198
198
|
}
|
|
199
|
+
// In a statement (value-discarded) position, postfix `x++`/`x--` is lowered to `(++x) − 1` /
|
|
200
|
+
// `(--x) + 1` to recover the old value — but nobody reads it, so drop the ∓1 and keep the bare
|
|
201
|
+
// increment. (`obj.p++` lowers via `obj.p = obj.p + 1`, also wrapped.) Cleaner AST for the loop/
|
|
202
|
+
// recurrence passes; codegen already discarded the ∓1, so this is purely canonicalization.
|
|
203
|
+
const isOne = n => Array.isArray(n) && n[0] == null && n[1] === 1
|
|
204
|
+
const dropDeadPostfix = s => {
|
|
205
|
+
if (Array.isArray(s) && s.length === 3 && isOne(s[2]) && Array.isArray(s[1])) {
|
|
206
|
+
const inner = s[1][0]
|
|
207
|
+
if ((s[0] === '-' && (inner === '++' || inner === '=')) ||
|
|
208
|
+
(s[0] === '+' && (inner === '--' || inner === '='))) return s[1]
|
|
209
|
+
}
|
|
210
|
+
return s
|
|
211
|
+
}
|
|
199
212
|
const stringValue = node => Array.isArray(node) && node[0] == null && typeof node[1] === 'string' ? node[1] : null
|
|
200
213
|
const MUTATING_ARRAY_METHODS = new Set(['copyWithin', 'fill', 'pop', 'push', 'reverse', 'shift', 'sort', 'splice', 'unshift'])
|
|
201
214
|
|
|
@@ -1636,20 +1649,20 @@ const handlers = {
|
|
|
1636
1649
|
'!=='(a, b) { return prepStrictEq('!==', a, b) },
|
|
1637
1650
|
|
|
1638
1651
|
// Statements
|
|
1639
|
-
';': (...stmts) => [';', ...stmts.map(prep).filter(x => x != null)],
|
|
1652
|
+
';': (...stmts) => [';', ...stmts.map(prep).filter(x => x != null).map(dropDeadPostfix)],
|
|
1640
1653
|
'let': (...inits) => prepDecl('let', ...inits),
|
|
1641
1654
|
'const': (...inits) => prepDecl('const', ...inits),
|
|
1642
1655
|
|
|
1643
1656
|
// Block-scoped control flow: push scope for bodies so inner let/const shadows correctly
|
|
1644
1657
|
'if': (cond, then, els) => {
|
|
1645
1658
|
const c = prep(stripBoolNot(cond))
|
|
1646
|
-
pushScope(); const t = prep(then); popScope()
|
|
1647
|
-
if (els != null) { pushScope(); const e = prep(els); popScope(); return ['if', c, t, e] }
|
|
1659
|
+
pushScope(); const t = dropDeadPostfix(prep(then)); popScope()
|
|
1660
|
+
if (els != null) { pushScope(); const e = dropDeadPostfix(prep(els)); popScope(); return ['if', c, t, e] }
|
|
1648
1661
|
return ['if', c, t]
|
|
1649
1662
|
},
|
|
1650
1663
|
'while': (cond, body) => {
|
|
1651
1664
|
const c = prep(stripBoolNot(cond))
|
|
1652
|
-
pushScope(); const b = prep(body); popScope()
|
|
1665
|
+
pushScope(); const b = dropDeadPostfix(prep(body)); popScope()
|
|
1653
1666
|
return ['while', c, b]
|
|
1654
1667
|
},
|
|
1655
1668
|
// do { body } while (cond) → flag-guarded while: `flag=true; while (flag||cond) { flag=false; body }`.
|
|
@@ -2127,7 +2140,7 @@ const handlers = {
|
|
|
2127
2140
|
}
|
|
2128
2141
|
}
|
|
2129
2142
|
}
|
|
2130
|
-
r = ['for', init ? prep(init) : null, cond ? prep(cond) : null, step ? prep(step) : null, prep(body)]
|
|
2143
|
+
r = ['for', init ? prep(init) : null, cond ? prep(cond) : null, step ? dropDeadPostfix(prep(step)) : null, dropDeadPostfix(prep(body))]
|
|
2131
2144
|
} else if (Array.isArray(head) && head[0] === 'of') {
|
|
2132
2145
|
// for (let x of arr) → hoist arr (if non-trivial) and arr.length once, iterate by index.
|
|
2133
2146
|
// Divergence from JS: mutating arr during iteration won't extend/shorten the loop.
|