jz 0.3.0 → 0.4.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/README.md +24 -14
- package/cli.js +37 -8
- package/index.js +32 -15
- package/module/array.js +16 -7
- package/module/collection.js +28 -13
- package/module/core.js +16 -1
- package/module/json.js +8 -8
- package/module/math.js +21 -4
- package/module/object.js +104 -9
- package/module/string.js +15 -0
- package/package.json +7 -4
- package/src/analyze.js +2 -2
- package/src/assemble.js +31 -8
- package/src/codegen.js +177 -0
- package/src/compile.js +21 -12
- package/src/emit.js +6 -1
- package/src/fuse.js +159 -0
- package/src/ir.js +11 -4
- package/src/jzify.js +165 -178
- package/src/narrow.js +36 -9
- package/src/optimize.js +182 -22
- package/src/plan.js +10 -1
- package/src/prepare.js +2 -149
package/src/narrow.js
CHANGED
|
@@ -192,11 +192,20 @@ function enrichCallerValTypesFromPointerParams(callerCtx) {
|
|
|
192
192
|
function refreshCallerLocals(callerCtx) {
|
|
193
193
|
for (const func of ctx.func.list) {
|
|
194
194
|
if (!func.body || func.raw) continue
|
|
195
|
+
// Seed pointer-narrowed params' val-kind so analyzeBody recognises e.g.
|
|
196
|
+
// `n = arr.length` (arr a TYPED/BUFFER pointer param) as an i32 local — without
|
|
197
|
+
// this, post-G `refreshCallerLocals` still walks bodies with arr untyped, the
|
|
198
|
+
// length stays f64, and any callee taking that length never gets an i32 param
|
|
199
|
+
// (heapsort→siftDown's `end`). analyzeFuncForEmit re-seeds + re-invalidates at
|
|
200
|
+
// emit time, so this transient repByLocal doesn't leak past narrowing.
|
|
201
|
+
ctx.func.repByLocal = new Map()
|
|
202
|
+
for (const p of func.sig.params) if (p.ptrKind != null) ctx.func.repByLocal.set(p.name, { val: p.ptrKind })
|
|
195
203
|
invalidateLocalsCache(func.body)
|
|
196
204
|
const fresh = analyzeLocals(func.body)
|
|
197
205
|
for (const p of func.sig.params) if (!fresh.has(p.name)) fresh.set(p.name, p.type)
|
|
198
206
|
callerCtx.get(func).callerLocals = fresh
|
|
199
207
|
}
|
|
208
|
+
ctx.func.repByLocal = null
|
|
200
209
|
}
|
|
201
210
|
|
|
202
211
|
function resetParamWasmFacts(paramReps) {
|
|
@@ -361,10 +370,24 @@ export default function narrowSignatures(programFacts, ast) {
|
|
|
361
370
|
},
|
|
362
371
|
},
|
|
363
372
|
])
|
|
373
|
+
// Transitive ctor/schema propagation down call chains. A naive single-pass
|
|
374
|
+
// mergeRule poisons a callee's param on the *first* sweep if the caller's own
|
|
375
|
+
// param (the very thing that supplies the ctor) hasn't been typed yet — and the
|
|
376
|
+
// poison is sticky, so later sweeps can't recover. Two-pass was the old patch;
|
|
377
|
+
// it still loses any chain deeper than `main→f→g→h` (e.g. heapsort's siftDown).
|
|
378
|
+
// Fix: iterate a *soft* merge — propagate known ctors, treat "can't tell yet"
|
|
379
|
+
// as skip (no poison) — to a fixpoint, then one *hard* validating sweep that
|
|
380
|
+
// poisons params whose call sites still can't be proven (genuinely-untyped args).
|
|
364
381
|
const runArrElemFixpoint = (field, inferFn, elemsCtxMap) => {
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
)])
|
|
382
|
+
const infer = (arg, _k, state) => inferFn(arg, elemsCtxMap.get(state.callerFunc), state.callerParamFacts(field))
|
|
383
|
+
let changed
|
|
384
|
+
const bump = (r, v) => { if (v == null || r[field] === null) return; const b = r[field]; mergeParamFact(r, field, v); if (r[field] !== b) changed = true }
|
|
385
|
+
const soft = {
|
|
386
|
+
missing(r, k, state) { const def = defaultArg(state, k); if (def != null) bump(r, infer(def, k, state)) },
|
|
387
|
+
apply(r, arg, k, state) { bump(r, infer(arg, k, state)) },
|
|
388
|
+
}
|
|
389
|
+
do { changed = false; runCallsiteLattice([soft]) } while (changed)
|
|
390
|
+
runCallsiteLattice([mergeRule(field, infer)])
|
|
368
391
|
}
|
|
369
392
|
const runArrFixpoint = () => runArrElemFixpoint('arrayElemSchema', inferArgArrElemSchema, phase.callerElems('arrElemSchemas'))
|
|
370
393
|
const runArrValTypeFixpoint = () => runArrElemFixpoint('arrayElemValType', inferArgArrElemValType, phase.callerElems('arrElemValTypes'))
|
|
@@ -423,18 +446,22 @@ export default function narrowSignatures(programFacts, ast) {
|
|
|
423
446
|
if (isBlockBody(body) && hasBareReturn(body)) continue
|
|
424
447
|
const exprs = returnExprs(body)
|
|
425
448
|
if (!exprs.length) continue
|
|
426
|
-
//
|
|
427
|
-
//
|
|
428
|
-
//
|
|
429
|
-
//
|
|
430
|
-
|
|
449
|
+
// Narrow result to i32 when every return-tail types as i32 (incl. bitwise ops, `|0`,
|
|
450
|
+
// and `>>>`). When ANY tail is `>>>` (unsigned uint32 idiom) we still narrow, but tag
|
|
451
|
+
// `sig.unsignedResult` so the call-site rebox uses `f64.convert_i32_u` — preserving
|
|
452
|
+
// the [0, 2^32) range that `(x >>> 0)` carries through hash/CRC finalizers.
|
|
453
|
+
const anyUnsigned = exprs.some(e => Array.isArray(e) && e[0] === '>>>')
|
|
431
454
|
const savedCurrent = ctx.func.current
|
|
432
455
|
ctx.func.current = func.sig
|
|
433
456
|
const locals = isBlockBody(body) ? analyzeLocals(body) : new Map()
|
|
434
457
|
for (const p of func.sig.params) if (!locals.has(p.name)) locals.set(p.name, p.type)
|
|
435
458
|
const allI32 = exprs.every(e => exprTypeWithCalls(e, locals) === 'i32')
|
|
436
459
|
ctx.func.current = savedCurrent
|
|
437
|
-
if (allI32) {
|
|
460
|
+
if (allI32) {
|
|
461
|
+
func.sig.results = ['i32']
|
|
462
|
+
if (anyUnsigned) func.sig.unsignedResult = true
|
|
463
|
+
changed = true
|
|
464
|
+
}
|
|
438
465
|
}
|
|
439
466
|
}
|
|
440
467
|
|
package/src/optimize.js
CHANGED
|
@@ -45,17 +45,19 @@ const UNDEF_BITS = '0x' + (LAYOUT.NAN_PREFIX_BITS | (2n << BigInt(LAYOUT.AUX_SHI
|
|
|
45
45
|
* 0 — nothing. Fastest compile, largest output. Useful for live coding.
|
|
46
46
|
* 1 — encoding-compactness only (treeshake + sortLocalsByUse + fusedRewrite-inline).
|
|
47
47
|
* Cheap, no IR rewrites that perturb V8's tier-up shape.
|
|
48
|
-
* 2 — default.
|
|
49
|
-
*
|
|
50
|
-
*
|
|
48
|
+
* 2 — default. All stable jz passes + watr in 'light' mode (everything except
|
|
49
|
+
* `inline` / `inlineOnce`). 'light' delivers most of the size win
|
|
50
|
+
* (treeshake / dedupe / dedupTypes / coalesce / propagate / packData / fold /
|
|
51
|
+
* peephole / vacuum / mergeBlocks / brif / loopify / …) at essentially zero
|
|
52
|
+
* net compile cost — the smaller wasm makes watrCompile downstream faster.
|
|
53
|
+
* 3 — level 2 + full watr (adds inlining) + aggressive experimental tunings.
|
|
51
54
|
*
|
|
52
55
|
* String aliases (the size↔speed tradeoff lives entirely in the unroll/scalar
|
|
53
|
-
* knobs
|
|
54
|
-
* 'size' —
|
|
55
|
-
*
|
|
56
|
+
* knobs; watr is on for all three):
|
|
57
|
+
* 'size' — loop/const unroll + lane vectorization off, tight scalar-replacement
|
|
58
|
+
* caps. Smallest wasm.
|
|
56
59
|
* 'balanced' — the default (= level 2).
|
|
57
|
-
* 'speed' —
|
|
58
|
-
* minus the heavy third-party watr pass).
|
|
60
|
+
* 'speed' — full nested unroll + lane vectorization (= level 3).
|
|
59
61
|
*/
|
|
60
62
|
export const PASS_NAMES = [
|
|
61
63
|
'watr', // third-party WAT-level CSE/DCE/inlining (heaviest)
|
|
@@ -67,6 +69,7 @@ export const PASS_NAMES = [
|
|
|
67
69
|
'hoistInvariantCellLoads',
|
|
68
70
|
'cseScalarLoad',
|
|
69
71
|
'csePureExpr',
|
|
72
|
+
'dropDeadZeroInit',
|
|
70
73
|
'deadStoreElim',
|
|
71
74
|
'promoteGlobals', // read-only global.get → local for multi-read globals
|
|
72
75
|
'sortLocalsByUse',
|
|
@@ -87,16 +90,30 @@ const ALL_OFF = Object.freeze(Object.fromEntries(PASS_NAMES.map(n => [n, false])
|
|
|
87
90
|
const LEVEL_PRESETS = Object.freeze({
|
|
88
91
|
0: ALL_OFF,
|
|
89
92
|
1: Object.freeze({ ...ALL_OFF, treeshake: true, sortLocalsByUse: true, fusedRewrite: true }),
|
|
90
|
-
2:
|
|
91
|
-
|
|
92
|
-
//
|
|
93
|
-
|
|
93
|
+
// Default (level 2 / 'balanced'): every stable pass + watr in 'light' mode.
|
|
94
|
+
// 'light' = all watr passes except inlining (`inline` / `inlineOnce`). Inlining is
|
|
95
|
+
// skipped at L2 because it breaks regex-split semantics (watr 4.6.4) and reshapes
|
|
96
|
+
// codegen tests that assert on pre-inline function structure. The remaining passes
|
|
97
|
+
// (treeshake/dedupe/dedupTypes/coalesce/propagate/packData/fold/peephole/...) still
|
|
98
|
+
// deliver most of watr's size win at essentially zero compile cost.
|
|
99
|
+
2: Object.freeze({ ...ALL_ON, watr: 'light', nestedSmallConstForUnroll: 'auto' }),
|
|
100
|
+
// L3/'speed' trades a bit of heap headroom for fewer __arr_grow / __hash growth
|
|
101
|
+
// cycles. arrayMinCap=16 means `[]` and `new Array()` skip the first two doublings
|
|
102
|
+
// (0→2→4→8→16); hashSmallInitCap=8 keeps per-object __dyn_props at the same load
|
|
103
|
+
// factor as the global __hash_new on first set, avoiding the 2→4→8 grow chain.
|
|
104
|
+
// Net cost: ~128 B per empty array, ~144 B per per-object hash. Net win on the
|
|
105
|
+
// watr.compile profile: __arr_grow ~6.7% → ~3%, and lower __ihash_get_local
|
|
106
|
+
// probe depth from a denser-load global hash.
|
|
107
|
+
3: Object.freeze({ ...ALL_ON, arrayMinCap: 16, hashSmallInitCap: 8 }),
|
|
108
|
+
// 'balanced' = level 2; 'size' tightens scalar/unroll caps; 'speed' = level 3.
|
|
109
|
+
balanced: Object.freeze({ ...ALL_ON, watr: 'light', nestedSmallConstForUnroll: 'auto' }),
|
|
94
110
|
size: Object.freeze({
|
|
95
|
-
...ALL_ON,
|
|
111
|
+
...ALL_ON,
|
|
96
112
|
smallConstForUnroll: false, nestedSmallConstForUnroll: false, vectorizeLaneLocal: false,
|
|
97
113
|
scalarTypedLoopUnroll: 4, scalarTypedNestedUnroll: 8, scalarTypedArrayLen: 8,
|
|
98
114
|
}),
|
|
99
|
-
speed:
|
|
115
|
+
// 'speed' === level 3: full watr (inlining on) + L3 cap/hash tuning.
|
|
116
|
+
speed: Object.freeze({ ...ALL_ON, arrayMinCap: 16, hashSmallInitCap: 8 }),
|
|
100
117
|
})
|
|
101
118
|
|
|
102
119
|
/**
|
|
@@ -118,7 +135,16 @@ export function resolveOptimize(opt) {
|
|
|
118
135
|
const baseLevel = typeof opt.level === 'number' || typeof opt.level === 'string' ? opt.level : 2
|
|
119
136
|
const base = LEVEL_PRESETS[baseLevel] || ALL_ON
|
|
120
137
|
const out = { ...base }
|
|
121
|
-
for (const n of PASS_NAMES)
|
|
138
|
+
for (const n of PASS_NAMES) {
|
|
139
|
+
if (!(n in opt)) continue
|
|
140
|
+
const v = opt[n]
|
|
141
|
+
// Preserve sentinel values that downstream resolution depends on:
|
|
142
|
+
// nestedSmallConstForUnroll: 'auto' (heuristic at emit time)
|
|
143
|
+
// watr: 'light' (curated subset — see index.js watrOpts)
|
|
144
|
+
if (n === 'nestedSmallConstForUnroll' && v === 'auto') out[n] = 'auto'
|
|
145
|
+
else if (n === 'watr' && v === 'light') out[n] = 'light'
|
|
146
|
+
else out[n] = !!v
|
|
147
|
+
}
|
|
122
148
|
// Preserve non-pass tuning keys (e.g. plan.js thresholds)
|
|
123
149
|
for (const k of Object.keys(opt)) if (!PASS_NAMES.includes(k)) out[k] = opt[k]
|
|
124
150
|
return out
|
|
@@ -1148,6 +1174,72 @@ export function csePureExpr(fn) {
|
|
|
1148
1174
|
if (newLocals.length) fn.splice(bodyStart, 0, ...newLocals)
|
|
1149
1175
|
}
|
|
1150
1176
|
|
|
1177
|
+
/**
|
|
1178
|
+
* Drop redundant zero-initialisation of fresh function-scope locals.
|
|
1179
|
+
*
|
|
1180
|
+
* WASM zero-initialises every local on entry (0 / 0.0 / null). jz lowers source
|
|
1181
|
+
* `let x = 0` to `(local $x …)` + `(local.set $x (<zero const>))` at the top of
|
|
1182
|
+
* the function body — the explicit set is a no-op when nothing has touched `$x`
|
|
1183
|
+
* yet. `wasm-opt -Oz` elides these; do the same so jz's own output is minimal.
|
|
1184
|
+
*
|
|
1185
|
+
* Only removes a `(local.set $L (i32|i64|f64|f32.const 0))` when:
|
|
1186
|
+
* - `$L` is a non-param local (a param's "default" is the incoming arg, not 0),
|
|
1187
|
+
* - it is a *top-level* body statement (never descend into block/loop/if — a
|
|
1188
|
+
* nested zero-set inside a loop genuinely re-initialises across iterations),
|
|
1189
|
+
* - `$L` has not been referenced by any earlier top-level statement (so the
|
|
1190
|
+
* local still holds its entry-time zero at this point),
|
|
1191
|
+
* - `$L` is read (`local.get`) somewhere in the function (otherwise leave the
|
|
1192
|
+
* store for deadStoreElim and avoid orphaning the `(local $L …)` decl),
|
|
1193
|
+
* - the constant is +0 / +0.0 (a `-0.0` f64 set is *not* redundant — locals
|
|
1194
|
+
* default to +0.0, which differs in bits from -0.0).
|
|
1195
|
+
*/
|
|
1196
|
+
export function dropDeadZeroInit(fn) {
|
|
1197
|
+
if (!Array.isArray(fn) || fn[0] !== 'func') return
|
|
1198
|
+
const bodyStart = findBodyStart(fn)
|
|
1199
|
+
if (bodyStart < 0) return
|
|
1200
|
+
|
|
1201
|
+
const seen = new Set() // params + locals referenced by an earlier stmt
|
|
1202
|
+
const reads = new Set() // locals read by `local.get` anywhere
|
|
1203
|
+
for (const c of fn) if (Array.isArray(c) && c[0] === 'param' && typeof c[1] === 'string') seen.add(c[1])
|
|
1204
|
+
|
|
1205
|
+
const collectGets = (node) => {
|
|
1206
|
+
if (!Array.isArray(node)) return
|
|
1207
|
+
if (node[0] === 'local.get' && typeof node[1] === 'string') reads.add(node[1])
|
|
1208
|
+
for (let i = 1; i < node.length; i++) collectGets(node[i])
|
|
1209
|
+
}
|
|
1210
|
+
for (let i = bodyStart; i < fn.length; i++) collectGets(fn[i])
|
|
1211
|
+
|
|
1212
|
+
const collectRefs = (node) => {
|
|
1213
|
+
if (!Array.isArray(node)) return
|
|
1214
|
+
const op = node[0]
|
|
1215
|
+
if ((op === 'local.get' || op === 'local.set' || op === 'local.tee') && typeof node[1] === 'string') seen.add(node[1])
|
|
1216
|
+
for (let i = 1; i < node.length; i++) collectRefs(node[i])
|
|
1217
|
+
}
|
|
1218
|
+
const isPlusZeroConst = (e) => {
|
|
1219
|
+
if (!Array.isArray(e) || e.length !== 2) return false
|
|
1220
|
+
if (e[0] !== 'i32.const' && e[0] !== 'i64.const' && e[0] !== 'f64.const' && e[0] !== 'f32.const') return false
|
|
1221
|
+
const v = e[1]
|
|
1222
|
+
if (typeof v === 'bigint') return v === 0n
|
|
1223
|
+
if (typeof v === 'number') return v === 0 && !Object.is(v, -0)
|
|
1224
|
+
if (typeof v === 'string') { const t = v.trim(); return t === '0' || t === '0.0' || t === '+0' || t === '+0.0' }
|
|
1225
|
+
return false
|
|
1226
|
+
}
|
|
1227
|
+
|
|
1228
|
+
const drop = []
|
|
1229
|
+
for (let i = bodyStart; i < fn.length; i++) {
|
|
1230
|
+
const node = fn[i]
|
|
1231
|
+
if (!Array.isArray(node)) continue
|
|
1232
|
+
if (node[0] === 'local.set' && node.length === 3 && typeof node[1] === 'string' &&
|
|
1233
|
+
!seen.has(node[1]) && reads.has(node[1]) && isPlusZeroConst(node[2])) {
|
|
1234
|
+
drop.push(i)
|
|
1235
|
+
seen.add(node[1])
|
|
1236
|
+
continue
|
|
1237
|
+
}
|
|
1238
|
+
collectRefs(node)
|
|
1239
|
+
}
|
|
1240
|
+
for (let i = drop.length - 1; i >= 0; i--) fn.splice(drop[i], 1)
|
|
1241
|
+
}
|
|
1242
|
+
|
|
1151
1243
|
/**
|
|
1152
1244
|
* Dead-store elimination: remove `local.set` / `local.tee` and `drop` of pure
|
|
1153
1245
|
* expressions whose values are never consumed.
|
|
@@ -1206,7 +1298,14 @@ export function deadStoreElim(fn) {
|
|
|
1206
1298
|
// Local write tracking
|
|
1207
1299
|
if ((op === 'local.set' || op === 'local.tee') && typeof node[1] === 'string') {
|
|
1208
1300
|
const prev = lastWrite.get(node[1])
|
|
1209
|
-
if (prev)
|
|
1301
|
+
if (prev) {
|
|
1302
|
+
// The store-to-local is dead, but a `local.set` is only *removable*
|
|
1303
|
+
// if its RHS is pure — `local.set $x (call f …)` where `f` mutates
|
|
1304
|
+
// memory must still run. (A `local.tee` is always safe: removal demotes
|
|
1305
|
+
// it to its value expression, so any side effects there are preserved.)
|
|
1306
|
+
const pn = prev.parent[prev.idx]
|
|
1307
|
+
if (pn[0] === 'local.tee' || isPure(pn[2])) dead.push(prev)
|
|
1308
|
+
}
|
|
1210
1309
|
lastWrite.set(node[1], { parent: items, idx: i })
|
|
1211
1310
|
}
|
|
1212
1311
|
|
|
@@ -1472,7 +1571,8 @@ export function specializeMkptr(funcs, addFunc, parseWat) {
|
|
|
1472
1571
|
// Any target not listed here is left untouched. Order matters only for readability.
|
|
1473
1572
|
const SPECS = {
|
|
1474
1573
|
'$__mkptr': { params: ['i32', 'i32', 'i32'], result: 'f64', inline: true },
|
|
1475
|
-
'$__alloc_hdr':
|
|
1574
|
+
'$__alloc_hdr': { params: ['i32', 'i32'], result: 'i32' },
|
|
1575
|
+
'$__alloc_hdr_n': { params: ['i32', 'i32', 'i32'], result: 'i32' },
|
|
1476
1576
|
'$__typed_idx': { params: ['i64', 'i32'], result: 'f64' },
|
|
1477
1577
|
'$__str_idx': { params: ['i64', 'i32'], result: 'f64' },
|
|
1478
1578
|
}
|
|
@@ -1782,6 +1882,7 @@ export function optimizeFunc(fn, cfg, globalTypes) {
|
|
|
1782
1882
|
cfg.hoistInvariantCellLoads === false &&
|
|
1783
1883
|
cfg.cseScalarLoad === false &&
|
|
1784
1884
|
cfg.csePureExpr === false &&
|
|
1885
|
+
cfg.dropDeadZeroInit === false &&
|
|
1785
1886
|
cfg.deadStoreElim === false &&
|
|
1786
1887
|
cfg.promoteGlobals === false &&
|
|
1787
1888
|
cfg.sortLocalsByUse === false &&
|
|
@@ -1795,12 +1896,17 @@ export function optimizeFunc(fn, cfg, globalTypes) {
|
|
|
1795
1896
|
if (!cfg || cfg.hoistInvariantCellLoads !== false) hoistInvariantCellLoads(fn)
|
|
1796
1897
|
if (!cfg || cfg.cseScalarLoad !== false) cseScalarLoad(fn)
|
|
1797
1898
|
if (!cfg || cfg.csePureExpr !== false) csePureExpr(fn)
|
|
1899
|
+
if (!cfg || cfg.dropDeadZeroInit !== false) dropDeadZeroInit(fn)
|
|
1798
1900
|
if (!cfg || cfg.deadStoreElim !== false) deadStoreElim(fn)
|
|
1799
1901
|
if (!cfg || cfg.promoteGlobals !== false) promoteGlobals(fn, globalTypes)
|
|
1800
|
-
// Vectorizer runs
|
|
1801
|
-
//
|
|
1902
|
+
// Vectorizer runs PRE-watr unless full watr is enabled (`watr: true`). For full watr,
|
|
1903
|
+
// defer to post — full passes (notably `inlineOnce` + the post-inline `propagate`
|
|
1904
|
+
// sweep) reshape the IR so much that pre-watr SIMD patterns get scrambled. Light
|
|
1905
|
+
// watr (or no watr) leaves the lane locals intact for vectorize to pattern-match,
|
|
1906
|
+
// and lets a non-trivial chunk of SIMD survive the propagate+fold pipeline.
|
|
1802
1907
|
if (cfg && cfg.vectorizeLaneLocal === true) {
|
|
1803
|
-
const
|
|
1908
|
+
const fullWatr = cfg.watr === true
|
|
1909
|
+
const runVectorizer = (fullWatr && cfg.__phase === 'post') || (!fullWatr && cfg.__phase !== 'post')
|
|
1804
1910
|
if (runVectorizer) vectorizeLaneLocal(fn)
|
|
1805
1911
|
}
|
|
1806
1912
|
if (!cfg || cfg.sortLocalsByUse !== false) sortLocalsByUse(fn, cfg && cfg.fusedRewrite !== false ? counts : null)
|
|
@@ -1925,6 +2031,30 @@ function walkRewrite(node, doInline, counts) {
|
|
|
1925
2031
|
for (let i = 1; i < inner.length; i++) out.push(inner[i])
|
|
1926
2032
|
return out
|
|
1927
2033
|
}
|
|
2034
|
+
// (i32.wrap_i64 (i64.reinterpret_f64 (call $__mkptr* … offset))) → offset.
|
|
2035
|
+
// A NaN-boxed pointer keeps type/aux in the high bits and the i32 offset in
|
|
2036
|
+
// the low 32, so the round-trip through f64 is pure overhead whenever the
|
|
2037
|
+
// consumer only wants the offset (typical when the pointer feeds an unboxed
|
|
2038
|
+
// i32 local). Covers the generic 3-arg `$__mkptr` and the specialized
|
|
2039
|
+
// single-arg `$__mkptr_T_A_d` trampolines alike — offset is the last arg.
|
|
2040
|
+
const isMkptr = n => Array.isArray(n) && n[0] === 'call' && typeof n[1] === 'string'
|
|
2041
|
+
&& (n[1] === '$__mkptr' || n[1].startsWith('$__mkptr_'))
|
|
2042
|
+
if (isMkptr(inner)) return inner[inner.length - 1]
|
|
2043
|
+
// …and reach through a `(block (result f64) …stmts (call $__mkptr …))` —
|
|
2044
|
+
// `new TypedArray(n)` lowers to exactly this shape — by retyping the block
|
|
2045
|
+
// to i32 and dropping the box on its tail.
|
|
2046
|
+
if (Array.isArray(inner) && inner[0] === 'block' && isMkptr(inner[inner.length - 1])) {
|
|
2047
|
+
let ri = -1
|
|
2048
|
+
for (let i = 1; i <= 2 && i < inner.length; i++)
|
|
2049
|
+
if (Array.isArray(inner[i]) && inner[i][0] === 'result') { ri = i; break }
|
|
2050
|
+
if (ri >= 0 && inner[ri][1] === 'f64') {
|
|
2051
|
+
const tail = inner[inner.length - 1]
|
|
2052
|
+
const nb = inner.slice()
|
|
2053
|
+
nb[ri] = ['result', 'i32']
|
|
2054
|
+
nb[nb.length - 1] = tail[tail.length - 1]
|
|
2055
|
+
return nb
|
|
2056
|
+
}
|
|
2057
|
+
}
|
|
1928
2058
|
}
|
|
1929
2059
|
if (Array.isArray(a) && a[0] === 'i64.or' && a.length === 3) {
|
|
1930
2060
|
const l = a[1], r = a[2]
|
|
@@ -2081,6 +2211,36 @@ export function treeshake(funcSections, allModuleNodes, opts) {
|
|
|
2081
2211
|
}
|
|
2082
2212
|
}
|
|
2083
2213
|
}
|
|
2214
|
+
|
|
2215
|
+
// Dead-global elimination: after dead funcs are gone, drop `(global $g …)` decls
|
|
2216
|
+
// that nothing references (a `global.get`/`global.set` in a remaining func, a kept
|
|
2217
|
+
// global's init expr, a data/elem offset, or an `(export … (global $g))`). Imported
|
|
2218
|
+
// globals live in `allModuleNodes`, not in `opts.globals`, so they're never touched.
|
|
2219
|
+
// Fixpoint: a kept global's init may reference another global.
|
|
2220
|
+
const globals = removeDead && opts && Array.isArray(opts.globals) ? opts.globals : null
|
|
2221
|
+
if (globals) {
|
|
2222
|
+
const collectGlobalRefs = (node, refd) => {
|
|
2223
|
+
if (!Array.isArray(node)) return
|
|
2224
|
+
if ((node[0] === 'global.get' || node[0] === 'global.set') && typeof node[1] === 'string') refd.add(node[1])
|
|
2225
|
+
else if (node[0] === 'export' && Array.isArray(node[2]) && node[2][0] === 'global' && typeof node[2][1] === 'string') refd.add(node[2][1])
|
|
2226
|
+
for (const c of node) collectGlobalRefs(c, refd)
|
|
2227
|
+
}
|
|
2228
|
+
let changed = true
|
|
2229
|
+
while (changed) {
|
|
2230
|
+
changed = false
|
|
2231
|
+
const refd = new Set()
|
|
2232
|
+
for (const { arr } of funcSections) for (const n of arr) collectGlobalRefs(n, refd)
|
|
2233
|
+
for (const n of allModuleNodes) collectGlobalRefs(n, refd)
|
|
2234
|
+
for (const g of globals) collectGlobalRefs(g, refd)
|
|
2235
|
+
for (let i = globals.length - 1; i >= 0; i--) {
|
|
2236
|
+
const g = globals[i]
|
|
2237
|
+
if (Array.isArray(g) && g[0] === 'global' && typeof g[1] === 'string' && !refd.has(g[1])) {
|
|
2238
|
+
globals.splice(i, 1); changed = true
|
|
2239
|
+
}
|
|
2240
|
+
}
|
|
2241
|
+
}
|
|
2242
|
+
}
|
|
2243
|
+
|
|
2084
2244
|
return { removed, callCount }
|
|
2085
2245
|
}
|
|
2086
2246
|
|
|
@@ -2146,7 +2306,7 @@ export function sortLocalsByUse(fn, precomputedCounts) {
|
|
|
2146
2306
|
*/
|
|
2147
2307
|
export function arenaRewindModule(fns) {
|
|
2148
2308
|
const BUILTIN_SAFE = new Set([
|
|
2149
|
-
'$__alloc', '$__alloc_hdr', '$__mkptr',
|
|
2309
|
+
'$__alloc', '$__alloc_hdr', '$__alloc_hdr_n', '$__mkptr',
|
|
2150
2310
|
'$__ptr_offset', '$__ptr_type', '$__ptr_aux',
|
|
2151
2311
|
'$__len', '$__cap', '$__typed_shift', '$__typed_data',
|
|
2152
2312
|
])
|
|
@@ -2179,7 +2339,7 @@ export function arenaRewindModule(fns) {
|
|
|
2179
2339
|
else if (op === 'call_ref') hasCallRef = true
|
|
2180
2340
|
else if (op === 'call') {
|
|
2181
2341
|
const callee = node[1]
|
|
2182
|
-
if (callee === '$__alloc' || callee === '$__alloc_hdr') hasAlloc = true
|
|
2342
|
+
if (callee === '$__alloc' || callee === '$__alloc_hdr' || callee === '$__alloc_hdr_n') hasAlloc = true
|
|
2183
2343
|
if (typeof callee === 'string' && !BUILTIN_SAFE.has(callee)) calls.add(callee)
|
|
2184
2344
|
}
|
|
2185
2345
|
for (let i = 1; i < node.length; i++) scan(node[i])
|
package/src/plan.js
CHANGED
|
@@ -935,7 +935,12 @@ const inlineInStmt = (stmt, candidates) => {
|
|
|
935
935
|
}
|
|
936
936
|
if (op === '{}') {
|
|
937
937
|
const r = inlineInStmt(stmt[1], candidates)
|
|
938
|
-
|
|
938
|
+
if (!r.changed) return { node: stmt, changed: false }
|
|
939
|
+
// If the child was itself a candidate call (or a let/assign-of-call), it
|
|
940
|
+
// already returned a `['{}', [';', ...prefix]]` shape. Re-wrapping here
|
|
941
|
+
// would yield `['{}', ['{}', …]]`, which codegen rejects ("Unknown op: {}").
|
|
942
|
+
if (Array.isArray(r.node) && r.node[0] === '{}') return { node: r.node, changed: true }
|
|
943
|
+
return { node: ['{}', r.node], changed: true }
|
|
939
944
|
}
|
|
940
945
|
if (op === 'for') {
|
|
941
946
|
const r = inlineInStmt(stmt[4], candidates)
|
|
@@ -1433,6 +1438,10 @@ export default function plan(ast) {
|
|
|
1433
1438
|
unboxConstTypedGlobals()
|
|
1434
1439
|
|
|
1435
1440
|
let programFacts = collectProgramFacts(ast)
|
|
1441
|
+
// The call-inlining family (`inlineHotInternalCalls` self-gates on `sourceInline`)
|
|
1442
|
+
// is a pure speed optimization — the un-inlined calls emit correctly. Scalar
|
|
1443
|
+
// replacement (`scalarize*`) is *not* gated on `sourceInline`: callers turn it on
|
|
1444
|
+
// independently via `optimize: { sourceInline: false }` to test heap elision alone.
|
|
1436
1445
|
if (inlineHotInternalCalls(programFacts, ast)) programFacts = collectProgramFacts(ast)
|
|
1437
1446
|
if (inlineLocalLambdas()) programFacts = collectProgramFacts(ast)
|
|
1438
1447
|
if (specializeFixedRestCalls(programFacts)) programFacts = collectProgramFacts(ast)
|
package/src/prepare.js
CHANGED
|
@@ -26,6 +26,7 @@ import { parse } from 'subscript/feature/jessie'
|
|
|
26
26
|
import { ctx, err, derive } from './ctx.js'
|
|
27
27
|
import { T, STMT_OPS, VAL, valTypeOf, typedElemCtor, extractParams, collectParamNames, classifyParam, observeNodeFacts, staticPropertyKey } from './analyze.js'
|
|
28
28
|
import { isFuncRef } from './ir.js'
|
|
29
|
+
import { fuseSparseMapReads } from './fuse.js'
|
|
29
30
|
import {
|
|
30
31
|
CTORS, TIMER_NAMES,
|
|
31
32
|
hasModule, includeModule,
|
|
@@ -184,7 +185,7 @@ export default function prepare(node) {
|
|
|
184
185
|
funcLocalNames = [new Set()]
|
|
185
186
|
includeModule('core')
|
|
186
187
|
normalizeIdents(node)
|
|
187
|
-
fuseSparseMapReads(node)
|
|
188
|
+
fuseSparseMapReads(node) // AST-level fusion; needs pre-resolution shape — see src/fuse.js
|
|
188
189
|
const ast = prep(node)
|
|
189
190
|
// Top-level functions referenced as first-class values (e.g. `let o = { fn: g }`,
|
|
190
191
|
// `arr.push(g)`, `return g`) need trampoline emission, which depends on the fn
|
|
@@ -340,154 +341,6 @@ const cloneNode = (node) => {
|
|
|
340
341
|
return copy
|
|
341
342
|
}
|
|
342
343
|
|
|
343
|
-
/** Sparse-read .map fusion: rewrite `const b = a.map(arrow); for(...; j<b.length; ...) USE(b[j])`
|
|
344
|
-
* into a fused for-loop that inlines `arrow(a[j])` at the read site, eliminating the materialized
|
|
345
|
-
* intermediate array. Pre-prep AST mutation; only fires on shapes where every use of `b` is a
|
|
346
|
-
* numeric `b[idx]` read or a `b.length` read, the arrow is pure with a single named param, and
|
|
347
|
-
* `b` is not referenced after the consumer for-loop. Preserves observable behavior because the
|
|
348
|
-
* arrow's pure-expression body has no order-dependent effects. */
|
|
349
|
-
function fuseSparseMapReads(root) {
|
|
350
|
-
walkSparse(root)
|
|
351
|
-
}
|
|
352
|
-
function walkSparse(node) {
|
|
353
|
-
if (!Array.isArray(node)) return
|
|
354
|
-
for (let i = 1; i < node.length; i++) walkSparse(node[i])
|
|
355
|
-
if (node[0] === ';') tryFuseInBlock(node)
|
|
356
|
-
}
|
|
357
|
-
function tryFuseInBlock(seq) {
|
|
358
|
-
for (let i = 1; i < seq.length - 1; i++) {
|
|
359
|
-
const fused = tryFusePair(seq[i], seq[i + 1], seq, i)
|
|
360
|
-
if (fused) {
|
|
361
|
-
seq.splice(i, 2, ...fused)
|
|
362
|
-
i-- // re-examine same position (chained fusions)
|
|
363
|
-
}
|
|
364
|
-
}
|
|
365
|
-
}
|
|
366
|
-
function tryFusePair(decl, forNode, seq, declIdx) {
|
|
367
|
-
if (!Array.isArray(decl) || (decl[0] !== 'const' && decl[0] !== 'let')) return null
|
|
368
|
-
if (decl.length !== 2) return null // single binding only
|
|
369
|
-
const bind = decl[1]
|
|
370
|
-
if (!Array.isArray(bind) || bind[0] !== '=' || typeof bind[1] !== 'string') return null
|
|
371
|
-
const NAME = bind[1], rhs = bind[2]
|
|
372
|
-
if (!Array.isArray(rhs) || rhs[0] !== '()') return null
|
|
373
|
-
const callee = rhs[1]
|
|
374
|
-
if (!Array.isArray(callee) || callee[0] !== '.' || callee[2] !== 'map') return null
|
|
375
|
-
const RECV = callee[1]
|
|
376
|
-
if (typeof RECV !== 'string' || RECV === NAME) return null
|
|
377
|
-
const arrow = rhs[2]
|
|
378
|
-
if (!Array.isArray(arrow) || arrow[0] !== '=>') return null
|
|
379
|
-
// Single-name param only: `x => …` or `(x) => …`
|
|
380
|
-
const ap = arrow[1]
|
|
381
|
-
const PARAM = typeof ap === 'string' ? ap :
|
|
382
|
-
(Array.isArray(ap) && ap[0] === '()' && typeof ap[1] === 'string' ? ap[1] : null)
|
|
383
|
-
if (!PARAM || PARAM === NAME || PARAM === RECV) return null
|
|
384
|
-
// Body: single-expression arrow only (block bodies skipped — could extend later).
|
|
385
|
-
const aBody = arrow[2]
|
|
386
|
-
if (Array.isArray(aBody) && aBody[0] === '{}') return null
|
|
387
|
-
if (!isPureSparseArrowBody(aBody, PARAM)) return null
|
|
388
|
-
// For-loop: ['for', [';', initStmt, cond, inc], body]
|
|
389
|
-
if (!Array.isArray(forNode) || forNode[0] !== 'for' || forNode.length !== 3) return null
|
|
390
|
-
const head = forNode[1]
|
|
391
|
-
if (!Array.isArray(head) || head[0] !== ';' || head.length !== 4) return null
|
|
392
|
-
const cond = head[2], forBody = forNode[2]
|
|
393
|
-
// Verify `NAME` is used only as `NAME[idx]` or `NAME.length` inside cond+forBody.
|
|
394
|
-
if (!hasOnlySparseUses(cond, NAME)) return null
|
|
395
|
-
if (!hasOnlySparseUses(forBody, NAME)) return null
|
|
396
|
-
if (!hasAnyIndexedRead(forBody, NAME) && !hasAnyIndexedRead(cond, NAME)) return null
|
|
397
|
-
// `NAME` must not be read after the for-loop in the same block.
|
|
398
|
-
for (let k = declIdx + 2; k < seq.length; k++) {
|
|
399
|
-
if (refsName(seq[k], NAME)) return null
|
|
400
|
-
}
|
|
401
|
-
// RECV must not be reassigned inside the for-loop (would invalidate substitution).
|
|
402
|
-
if (assignsName(forNode, RECV) || assignsName(forNode, NAME)) return null
|
|
403
|
-
// PARAM must not collide with any binding inside forBody (otherwise substitution shadows wrongly).
|
|
404
|
-
if (bindsName(forNode, PARAM)) return null
|
|
405
|
-
// Apply substitution: NAME.length → RECV.length; NAME[idx] → arrowBody[PARAM ← RECV[idx]].
|
|
406
|
-
const newCond = substSparse(cond, NAME, RECV, PARAM, aBody)
|
|
407
|
-
const newBody = substSparse(forBody, NAME, RECV, PARAM, aBody)
|
|
408
|
-
const newHead = [';', head[1], newCond, head[3]]
|
|
409
|
-
return [['for', newHead, newBody]]
|
|
410
|
-
}
|
|
411
|
-
function isPureSparseArrowBody(n, PARAM) {
|
|
412
|
-
if (typeof n === 'string') return true
|
|
413
|
-
if (!Array.isArray(n)) return true
|
|
414
|
-
const op = n[0]
|
|
415
|
-
// Calls / new / assignments / increments are unsafe for repeated-substitution semantics.
|
|
416
|
-
if (op === '()' || op === '?.()' || op === 'new' || op === '++' || op === '--') return false
|
|
417
|
-
if (op === '=>') return false // nested closure is opaque
|
|
418
|
-
if (typeof op === 'string' && op !== '=>' && op !== '===' && op !== '!==' && op !== '==' && op !== '!=' && op !== '<=' && op !== '>=' && op.endsWith('=') && op !== '=') return false
|
|
419
|
-
if (op === '=') return false
|
|
420
|
-
for (let i = 1; i < n.length; i++) if (!isPureSparseArrowBody(n[i], PARAM)) return false
|
|
421
|
-
return true
|
|
422
|
-
}
|
|
423
|
-
function hasOnlySparseUses(n, NAME) {
|
|
424
|
-
if (typeof n === 'string') return n !== NAME
|
|
425
|
-
if (!Array.isArray(n)) return true
|
|
426
|
-
const op = n[0]
|
|
427
|
-
if (op === '[]' && n.length === 3 && n[1] === NAME) return hasOnlySparseUses(n[2], NAME) // NAME[idx] — idx must not reference NAME
|
|
428
|
-
if (op === '.' && n[1] === NAME) {
|
|
429
|
-
if (n[2] === 'length') return true
|
|
430
|
-
return false // any other property access on NAME is opaque
|
|
431
|
-
}
|
|
432
|
-
for (let i = 1; i < n.length; i++) if (!hasOnlySparseUses(n[i], NAME)) return false
|
|
433
|
-
return true
|
|
434
|
-
}
|
|
435
|
-
function hasAnyIndexedRead(n, NAME) {
|
|
436
|
-
if (!Array.isArray(n)) return false
|
|
437
|
-
if (n[0] === '[]' && n.length === 3 && n[1] === NAME) return true
|
|
438
|
-
for (let i = 1; i < n.length; i++) if (hasAnyIndexedRead(n[i], NAME)) return true
|
|
439
|
-
return false
|
|
440
|
-
}
|
|
441
|
-
function refsName(n, NAME) {
|
|
442
|
-
if (typeof n === 'string') return n === NAME
|
|
443
|
-
if (!Array.isArray(n)) return false
|
|
444
|
-
for (let i = 1; i < n.length; i++) if (refsName(n[i], NAME)) return true
|
|
445
|
-
return false
|
|
446
|
-
}
|
|
447
|
-
function assignsName(n, NAME) {
|
|
448
|
-
if (!Array.isArray(n)) return false
|
|
449
|
-
const op = n[0]
|
|
450
|
-
if ((op === '=' || op === '++' || op === '--' ||
|
|
451
|
-
(typeof op === 'string' && op.endsWith('=') && op !== '==' && op !== '===' && op !== '!=' && op !== '!==' && op !== '<=' && op !== '>='))
|
|
452
|
-
&& n[1] === NAME) return true
|
|
453
|
-
for (let i = 1; i < n.length; i++) if (assignsName(n[i], NAME)) return true
|
|
454
|
-
return false
|
|
455
|
-
}
|
|
456
|
-
function bindsName(n, NAME) {
|
|
457
|
-
if (!Array.isArray(n)) return false
|
|
458
|
-
const op = n[0]
|
|
459
|
-
if ((op === 'let' || op === 'const')) {
|
|
460
|
-
for (let i = 1; i < n.length; i++) {
|
|
461
|
-
const bind = n[i]
|
|
462
|
-
if (Array.isArray(bind) && bind[0] === '=' && bind[1] === NAME) return true
|
|
463
|
-
}
|
|
464
|
-
}
|
|
465
|
-
if (op === '=>') {
|
|
466
|
-
const p = n[1]
|
|
467
|
-
if (p === NAME) return true
|
|
468
|
-
if (Array.isArray(p)) {
|
|
469
|
-
if (p[0] === '()' && p[1] === NAME) return true
|
|
470
|
-
// skip deeper destructuring forms — conservative
|
|
471
|
-
}
|
|
472
|
-
}
|
|
473
|
-
for (let i = 1; i < n.length; i++) if (bindsName(n[i], NAME)) return true
|
|
474
|
-
return false
|
|
475
|
-
}
|
|
476
|
-
function substSparse(n, NAME, RECV, PARAM, arrowBody) {
|
|
477
|
-
if (typeof n !== 'object' || n === null || !Array.isArray(n)) return n
|
|
478
|
-
if (n[0] === '.' && n[1] === NAME && n[2] === 'length') return ['.', RECV, 'length']
|
|
479
|
-
if (n[0] === '[]' && n.length === 3 && n[1] === NAME) {
|
|
480
|
-
const idx = substSparse(n[2], NAME, RECV, PARAM, arrowBody)
|
|
481
|
-
return cloneAndBind(arrowBody, PARAM, ['[]', RECV, idx])
|
|
482
|
-
}
|
|
483
|
-
return n.map((c, i) => i === 0 ? c : substSparse(c, NAME, RECV, PARAM, arrowBody))
|
|
484
|
-
}
|
|
485
|
-
function cloneAndBind(node, PARAM, replacement) {
|
|
486
|
-
if (node === PARAM) return replacement
|
|
487
|
-
if (!Array.isArray(node)) return node
|
|
488
|
-
return node.map((c, i) => i === 0 ? c : cloneAndBind(c, PARAM, replacement))
|
|
489
|
-
}
|
|
490
|
-
|
|
491
344
|
function prep(node) {
|
|
492
345
|
if (Array.isArray(node)) includeForOp(node[0])
|
|
493
346
|
if (Array.isArray(node) && node.loc != null) ctx.error.loc = node.loc
|