jz 0.7.0 → 0.8.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 +37 -33
- package/bench/README.md +176 -73
- package/bench/bench.svg +58 -71
- package/cli.js +12 -5
- package/dist/interop.js +1 -1
- package/dist/jz.js +1156 -984
- package/index.js +40 -9
- package/interop.js +193 -138
- package/layout.js +29 -18
- package/module/array.js +29 -73
- package/module/collection.js +83 -25
- package/module/console.js +1 -1
- package/module/core.js +161 -15
- package/module/json.js +3 -3
- package/module/math.js +26 -3
- package/module/number.js +247 -13
- package/module/object.js +11 -5
- package/module/regex.js +8 -7
- package/module/string.js +162 -156
- package/module/typedarray.js +169 -105
- package/package.json +7 -3
- package/src/abi/string.js +29 -29
- package/src/ast.js +19 -2
- package/src/compile/analyze.js +64 -2
- package/src/compile/cse-load.js +200 -0
- package/src/compile/emit-assign.js +73 -14
- package/src/compile/emit.js +253 -23
- package/src/compile/index.js +198 -61
- package/src/compile/loop-divmod.js +12 -58
- package/src/compile/loop-model.js +91 -0
- package/src/compile/loop-square.js +102 -0
- package/src/compile/narrow.js +169 -32
- package/src/compile/peel-stencil.js +18 -64
- package/src/compile/plan/common.js +29 -0
- package/src/compile/plan/index.js +4 -1
- package/src/compile/plan/inline.js +176 -21
- package/src/compile/plan/literals.js +93 -19
- package/src/ctx.js +51 -12
- package/src/helper-counters.js +137 -0
- package/src/ir.js +102 -13
- package/src/kind-traits.js +7 -3
- package/src/kind.js +14 -1
- package/src/op-policy.js +5 -2
- package/src/ops.js +119 -0
- package/src/optimize/index.js +960 -136
- package/src/optimize/recurse.js +182 -0
- package/src/optimize/vectorize.js +1292 -144
- package/src/prepare/index.js +11 -7
- package/src/reps.js +4 -1
- package/src/type.js +53 -45
- package/src/wat/assemble.js +92 -9
- package/src/widen.js +21 -0
- package/src/wat/optimize.js +0 -3938
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
// Bounded-square narrowing: carry `i*i` as i32 inside a loop guarded by `i*i < CONST`.
|
|
2
|
+
//
|
|
3
|
+
// A PRODUCT `i*i` is f64 in unhinted JS — the integer-overflow contract: a product can
|
|
4
|
+
// exceed 2³¹, where JS keeps a Number, so jz can't blindly use i32.mul (it would wrap).
|
|
5
|
+
// In a Sieve-of-Eratosthenes `for(i=2; i*i<LIMIT; i++) for(j=i*i; j<LIMIT; j+=i) …`, that
|
|
6
|
+
// makes the outer bound, the inner counter `j`'s init, and the whole index chain f64 —
|
|
7
|
+
// each typed-array access then pays an f64→i32 convert.
|
|
8
|
+
//
|
|
9
|
+
// But when the loop GUARD is `i*i < CONST` with CONST a compile-time constant ≤ 2³⁰, the
|
|
10
|
+
// counter is i ≤ ⌈√CONST⌉ ≤ 2¹⁵ throughout the body (the loop is still running, and i is
|
|
11
|
+
// only incremented by +1), so EVERY `i*i` there is < 2³⁰ < 2³¹ and `Math.imul(i,i) == i*i`
|
|
12
|
+
// exactly. The exit overshoot (the first i with i*i ≥ CONST, evaluated in the condition)
|
|
13
|
+
// is ≤ CONST + 2√CONST+1 < 2³¹ for CONST ≤ 2³⁰, so the condition narrows soundly too. The
|
|
14
|
+
// hard cap is 2³⁰ — well under the 2³¹ overflow point, with margin for the overshoot.
|
|
15
|
+
//
|
|
16
|
+
// So we rewrite those `i*i` to `Math.imul(i,i)` (jz's i32 multiply): semantically identical
|
|
17
|
+
// in the proven range, and it lets the EXISTING i32 machinery carry the index chain as i32
|
|
18
|
+
// — the inner `j` (init now i32, step `j+i` i32, bound `j<CONST` i32) cascades on its own.
|
|
19
|
+
//
|
|
20
|
+
// Sound iff: the loop condition is `(i*i) </≤ CONST` (CONST ≤ 2³⁰), and the IV `i` is
|
|
21
|
+
// incremented by +1 and NOT otherwise mutated (so within a body iteration i ∈ {entry, +1},
|
|
22
|
+
// both squares < 2³¹). Mirrors strength-reduce-divmod's structure (post-prepare `while`).
|
|
23
|
+
|
|
24
|
+
import { findMutations } from './analyze-scans.js'
|
|
25
|
+
import { includeMods } from '../autoload.js'
|
|
26
|
+
import { ctx } from '../ctx.js'
|
|
27
|
+
import { litVal, unitIncVar, normalizeLoop, closureMutatedVars, rewriteBlocks } from './loop-model.js'
|
|
28
|
+
|
|
29
|
+
const SQUARE_BOUND_MAX = 2 ** 30
|
|
30
|
+
// The constant numeric value of a bound: a literal, OR a module const folded to an int
|
|
31
|
+
// (`const LIMIT = 1<<20` → ctx.scope.constInts.get('LIMIT') = 1048576 — the bench form).
|
|
32
|
+
const boundVal = (n) => {
|
|
33
|
+
const lit = litVal(n)
|
|
34
|
+
if (lit != null) return lit
|
|
35
|
+
if (typeof n === 'string') { const v = ctx.scope.constInts?.get(n); return typeof v === 'number' ? v : null }
|
|
36
|
+
return null
|
|
37
|
+
}
|
|
38
|
+
// `i * i` — the IV squared.
|
|
39
|
+
const isSquare = (n, iv) => Array.isArray(n) && n[0] === '*' && n[1] === iv && n[2] === iv && typeof iv === 'string'
|
|
40
|
+
// Math.imul(i, i) in CANONICAL post-prepare form: prepare resolves `Math.imul` → the string
|
|
41
|
+
// ref `'math.imul'`, so the call is `['()', 'math.imul', [',', i, i]]`. math.imul emits a
|
|
42
|
+
// primitive `i32.mul` (no stdlib helper / module include needed).
|
|
43
|
+
const imulOf = (iv) => ['()', 'math.imul', [',', iv, iv]]
|
|
44
|
+
|
|
45
|
+
// The IV of a `(i*i) </≤ CONST` (or mirrored `CONST >/≥ (i*i)`) guard, CONST ≤ 2³⁰, else null.
|
|
46
|
+
function boundedSquareIV(cond) {
|
|
47
|
+
if (!Array.isArray(cond)) return null
|
|
48
|
+
let prod, bound
|
|
49
|
+
if (cond[0] === '<' || cond[0] === '<=') { prod = cond[1]; bound = cond[2] }
|
|
50
|
+
else if (cond[0] === '>' || cond[0] === '>=') { prod = cond[2]; bound = cond[1] }
|
|
51
|
+
else return null
|
|
52
|
+
if (!isSquare(prod, prod && prod[1])) return null
|
|
53
|
+
const b = boundVal(bound)
|
|
54
|
+
if (b == null || b < 0 || b > SQUARE_BOUND_MAX) return null
|
|
55
|
+
return prod[1]
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Narrow a `for`/`while` whose guard is `(i*i) </≤ CONST` (CONST ≤ 2³⁰) and whose IV `i` is
|
|
59
|
+
// incremented by +1 and not otherwise mutated — then within any body iteration i ∈ {entry,
|
|
60
|
+
// entry+1}, entry ≤ ⌈√CONST⌉ ≤ 2¹⁵, so every `i*i` (and the exit overshoot) is < 2³¹ and
|
|
61
|
+
// Math.imul(i,i) == i*i. Rewrites those products; the dependent counter chain cascades to i32.
|
|
62
|
+
// `cm` is the function's closure-mutated-vars set (an IV in it has an unprovable entry value).
|
|
63
|
+
function tryNarrow(stmt, cm) {
|
|
64
|
+
const L = normalizeLoop(stmt)
|
|
65
|
+
if (!L) return null
|
|
66
|
+
const { kind, cond, step, body } = L, isFor = kind === 'for'
|
|
67
|
+
|
|
68
|
+
const iv = boundedSquareIV(cond)
|
|
69
|
+
if (!iv) return null
|
|
70
|
+
if (cm.has(iv)) return null // mutable via a closure call — entry value unprovable
|
|
71
|
+
|
|
72
|
+
if (isFor) {
|
|
73
|
+
// The `for` update is the IV's sole, +1 mutation; the body must not reassign i.
|
|
74
|
+
if (unitIncVar(step) !== iv) return null
|
|
75
|
+
const ivMut = new Set(); findMutations(body, new Set([iv]), ivMut)
|
|
76
|
+
if (ivMut.has(iv)) return null
|
|
77
|
+
} else {
|
|
78
|
+
// `while`: the increment lives in the body (exactly one +1; nothing else mutates i).
|
|
79
|
+
if (!Array.isArray(body) || body[0] !== ';') return null
|
|
80
|
+
let ivIdx = -1
|
|
81
|
+
for (let k = 1; k < body.length; k++) if (unitIncVar(body[k]) === iv) { if (ivIdx >= 0) return null; ivIdx = k }
|
|
82
|
+
if (ivIdx < 0) return null
|
|
83
|
+
const ivMut = new Set()
|
|
84
|
+
findMutations([';', ...body.slice(1).filter((_, k) => k !== ivIdx - 1)], new Set([iv]), ivMut)
|
|
85
|
+
if (ivMut.has(iv)) return null
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// We're injecting `math.imul` after prepare's auto-import step, so ensure the math module
|
|
89
|
+
// (which registers the `math.imul` → i32.mul primitive emitter) is included.
|
|
90
|
+
includeMods('math')
|
|
91
|
+
// Rewrite every `i*i` in the condition + body to Math.imul(i,i) (NOT init/update — they're
|
|
92
|
+
// `i=2` / `i++`). The inner counter whose init this feeds cascades to i32 on its own.
|
|
93
|
+
const rw = (n) => !Array.isArray(n) ? n : isSquare(n, iv) ? imulOf(iv) : n.map(rw)
|
|
94
|
+
return [isFor
|
|
95
|
+
? ['for', L.init, rw(cond), step, rw(body)]
|
|
96
|
+
: ['while', rw(cond), rw(body)]]
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function narrowBoundedSquare(body) {
|
|
100
|
+
const cm = closureMutatedVars(body)
|
|
101
|
+
return rewriteBlocks(body, stmt => tryNarrow(stmt, cm))
|
|
102
|
+
}
|
package/src/compile/narrow.js
CHANGED
|
@@ -28,6 +28,10 @@ import {
|
|
|
28
28
|
} from './infer.js'
|
|
29
29
|
|
|
30
30
|
const PTR_ABI_KINDS = new Set([VAL.OBJECT, VAL.SET, VAL.MAP, VAL.BUFFER])
|
|
31
|
+
// Integer-preserving ops: an expr over integers stays integer (ToInt32-consistent) through these.
|
|
32
|
+
// Excludes /, %, ** (fractional). Used to recognize a recursive arg whose i32-ness follows from
|
|
33
|
+
// its inputs' i32-ness (`f(n - 1)`), so it carries no independent type evidence.
|
|
34
|
+
const RECUR_INT_OPS = new Set(['+', '-', '*', 'u-', 'u+', '&', '|', '^', '<<', '>>', '>>>', '~'])
|
|
31
35
|
|
|
32
36
|
|
|
33
37
|
function filterLiveCallSites(callSites, valueUsed) {
|
|
@@ -165,9 +169,10 @@ function refreshCallerValTypes(callerCtx) {
|
|
|
165
169
|
// it is the same single typed-array ctor (scope.js invalidates on any conflict),
|
|
166
170
|
// so it can't denote a different kind at the call site.
|
|
167
171
|
function callerTypedElemsFor(func, globalTE) {
|
|
168
|
-
const
|
|
172
|
+
const facts = analyzeBody(func.body)
|
|
173
|
+
const local = facts.typedElems
|
|
169
174
|
if (!globalTE.size) return local
|
|
170
|
-
const shadowed = new Set(
|
|
175
|
+
const shadowed = new Set(facts.locals.keys())
|
|
171
176
|
for (const p of func.sig?.params || []) shadowed.add(p.name)
|
|
172
177
|
const merged = new Map()
|
|
173
178
|
for (const [k, v] of globalTE) if (!shadowed.has(k)) merged.set(k, v)
|
|
@@ -223,6 +228,7 @@ function enrichCallerValTypesFromPointerParams(callerCtx) {
|
|
|
223
228
|
}
|
|
224
229
|
|
|
225
230
|
function refreshCallerLocals(callerCtx) {
|
|
231
|
+
const prevTE = ctx.types.typedElem
|
|
226
232
|
for (const func of ctx.func.list) {
|
|
227
233
|
if (!func.body || func.raw) continue
|
|
228
234
|
// Seed pointer-narrowed params' val-kind so analyzeBody recognises e.g.
|
|
@@ -232,13 +238,24 @@ function refreshCallerLocals(callerCtx) {
|
|
|
232
238
|
// (heapsort→siftDown's `end`). analyzeFuncForEmit re-seeds + re-invalidates at
|
|
233
239
|
// emit time, so this transient localReps doesn't leak past narrowing.
|
|
234
240
|
ctx.func.localReps = new Map()
|
|
235
|
-
|
|
241
|
+
// Seed the typedElem overlay with this func's TYPED-pointer params (element ctor from
|
|
242
|
+
// ptrAux), exactly as analyzeFuncForEmit does at emit time. Without it, a local bound to
|
|
243
|
+
// an integer typed-array PARAM element — `aa = perm[perm[X]+Y]` (noise), perm an Int32
|
|
244
|
+
// pointer param — types f64 here, so a callee fed it (`grad(aa,…)`, used only as `aa&3`)
|
|
245
|
+
// never narrows its param to i32. Mirrors emit so narrow-time callerLocals agree with it.
|
|
246
|
+
const te = ctx.scope.globalTypedElem ? new Map(ctx.scope.globalTypedElem) : new Map()
|
|
247
|
+
for (const p of func.sig.params) {
|
|
248
|
+
if (p.ptrKind != null) ctx.func.localReps.set(p.name, { val: p.ptrKind })
|
|
249
|
+
if (p.ptrKind === VAL.TYPED && p.ptrAux != null) { const c = ctorFromElemAux(p.ptrAux); if (c != null) te.set(p.name, c) }
|
|
250
|
+
}
|
|
251
|
+
ctx.types.typedElem = te
|
|
236
252
|
invalidateLocalsCache(func.body)
|
|
237
253
|
const fresh = analyzeBody(func.body).locals
|
|
238
254
|
for (const p of func.sig.params) if (!fresh.has(p.name)) fresh.set(p.name, p.type)
|
|
239
255
|
callerCtx.get(func).callerLocals = fresh
|
|
240
256
|
}
|
|
241
257
|
ctx.func.localReps = null
|
|
258
|
+
ctx.types.typedElem = prevTE
|
|
242
259
|
}
|
|
243
260
|
|
|
244
261
|
function resetParamWasmFacts(paramReps) {
|
|
@@ -280,6 +297,35 @@ function narrowI32Results(funcs) {
|
|
|
280
297
|
e[0] === '>>>' ||
|
|
281
298
|
(e[0] === '()' && typeof e[1] === 'string' && ctx.func.map?.get(e[1])?.sig?.unsignedResult === true)
|
|
282
299
|
)
|
|
300
|
+
const callsSelf = (n, name) => Array.isArray(n) && ((n[0] === '()' && n[1] === name) || n.some(c => callsSelf(c, name)))
|
|
301
|
+
// Classify a func's return tails as all-v128 / all-i32 (+ sign) under the CURRENT sig.results.
|
|
302
|
+
const evalTails = (func, body, exprs) => {
|
|
303
|
+
const savedCurrent = ctx.func.current
|
|
304
|
+
ctx.func.current = func.sig
|
|
305
|
+
const locals = isBlockBody(body) ? analyzeBody(body).locals : new Map()
|
|
306
|
+
for (const p of func.sig.params) if (!locals.has(p.name)) locals.set(p.name, p.type)
|
|
307
|
+
// Seed the typedElem overlay with this func's TYPED-pointer params so a return tail
|
|
308
|
+
// reading a typed-array element — `return vals[h]`, vals an Int32Array param (dict's
|
|
309
|
+
// `lookup`) — types as i32, not NaN-boxed f64. Without it the call site keeps the full
|
|
310
|
+
// __typed_idx/ToNumber unbox dispatch (491520× per dict kernel run). Mirrors
|
|
311
|
+
// refreshCallerLocals + analyzeFuncForEmit. Only meaningful once Phase G has tagged params
|
|
312
|
+
// ptrKind=TYPED (the I2 re-run below); harmless before (no typed params → overlay untouched).
|
|
313
|
+
const savedTE = ctx.types.typedElem
|
|
314
|
+
let te = null
|
|
315
|
+
for (const p of func.sig.params) {
|
|
316
|
+
if (p.ptrKind === VAL.TYPED && p.ptrAux != null) {
|
|
317
|
+
const c = ctorFromElemAux(p.ptrAux)
|
|
318
|
+
if (c != null) { if (!te) te = savedTE ? new Map(savedTE) : new Map(); te.set(p.name, c) }
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
if (te) ctx.types.typedElem = te
|
|
322
|
+
const allV128 = exprs.every(e => exprType(e, locals) === 'v128')
|
|
323
|
+
const allI32 = !allV128 && exprs.every(e => exprType(e, locals) === 'i32')
|
|
324
|
+
if (te) ctx.types.typedElem = savedTE
|
|
325
|
+
const r = { allV128, allI32, anyUnsigned: exprs.some(isUnsignedTail), allUnsigned: exprs.every(isUnsignedTail) }
|
|
326
|
+
ctx.func.current = savedCurrent
|
|
327
|
+
return r
|
|
328
|
+
}
|
|
283
329
|
let changed = true
|
|
284
330
|
while (changed) {
|
|
285
331
|
changed = false
|
|
@@ -289,22 +335,40 @@ function narrowI32Results(funcs) {
|
|
|
289
335
|
if (isBlockBody(body) && hasBareReturn(body)) continue
|
|
290
336
|
const exprs = returnExprs(body)
|
|
291
337
|
if (!exprs.length) continue
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
338
|
+
let r = evalTails(func, body, exprs)
|
|
339
|
+
// Recursive result cycle: a self-call in a return tail — or feeding a returned local
|
|
340
|
+
// (nqueens' `cnt = cnt + solve(…); return cnt`) — reads solve's own not-yet-narrowed
|
|
341
|
+
// f64 result, so `cnt` widens to f64 and the i32 narrowing never fires. Break the cycle
|
|
342
|
+
// optimistically: tentatively assume the i32 result, re-analyze, and keep it ONLY if every
|
|
343
|
+
// tail is then i32 (else revert). Sound — committed only when self-consistent.
|
|
344
|
+
if (!r.allI32 && !r.allV128 && callsSelf(body, func.name)) {
|
|
345
|
+
const saved = func.sig.results
|
|
346
|
+
func.sig.results = ['i32']
|
|
347
|
+
invalidateLocalsCache(body)
|
|
348
|
+
const opt = evalTails(func, body, exprs)
|
|
349
|
+
if (opt.allI32 && (!opt.anyUnsigned || opt.allUnsigned)) {
|
|
350
|
+
if (opt.allUnsigned) func.sig.unsignedResult = true
|
|
351
|
+
changed = true
|
|
352
|
+
continue
|
|
353
|
+
}
|
|
354
|
+
func.sig.results = saved
|
|
355
|
+
invalidateLocalsCache(body)
|
|
356
|
+
r = evalTails(func, body, exprs)
|
|
357
|
+
}
|
|
301
358
|
// SIMD: every tail returns a lane vector → v128 result.
|
|
302
|
-
if (allV128) {
|
|
359
|
+
if (r.allV128) {
|
|
303
360
|
func.sig.results = ['v128']
|
|
304
361
|
changed = true
|
|
305
|
-
} else if (allI32 && (!anyUnsigned || allUnsigned)) { // sign-consistent i32 tails
|
|
362
|
+
} else if (r.allI32 && (!r.anyUnsigned || r.allUnsigned)) { // sign-consistent i32 tails
|
|
306
363
|
func.sig.results = ['i32']
|
|
307
|
-
if (allUnsigned) func.sig.unsignedResult = true
|
|
364
|
+
if (r.allUnsigned) func.sig.unsignedResult = true
|
|
365
|
+
// A committed i32 result is a genuine NUMBER, so stamp valResult for the call-site
|
|
366
|
+
// VAL dispatch — E2 (narrowValResults) ran ABOVE the param lattice and so couldn't
|
|
367
|
+
// type a `return typedArrayParam[idx]` tail (hashjoin's `probe` → `vals[h]`), leaving
|
|
368
|
+
// valResult unset → the hot `sum + probe()` stayed the polymorphic string-or-number
|
|
369
|
+
// `+`. Only-if-unset: an UNBOXED-pointer i32 result already carries its ARRAY/OBJECT/
|
|
370
|
+
// TYPED valResult (the unboxing ABI needs it), so this never overwrites a pointer kind.
|
|
371
|
+
if (func.valResult == null) func.valResult = VAL.NUMBER
|
|
308
372
|
changed = true
|
|
309
373
|
}
|
|
310
374
|
}
|
|
@@ -672,10 +736,20 @@ export default function narrowSignatures(programFacts, ast) {
|
|
|
672
736
|
const state = siteState(callSites[s])
|
|
673
737
|
if (!state) continue
|
|
674
738
|
const { func, argList } = state
|
|
739
|
+
const recursive = state.callee === state.callerFunc?.name
|
|
675
740
|
for (let k = 0; k < func.sig.params.length; k++) {
|
|
676
741
|
const r = ensureParamRep(paramReps, state.callee, k)
|
|
677
742
|
if (k >= argList.length) { for (const rule of rules) rule.missing(r, k, state); continue }
|
|
678
|
-
|
|
743
|
+
const arg = argList[k]
|
|
744
|
+
// Recursive identity arg — `f(…, p, …)` calling itself with its own param p threaded
|
|
745
|
+
// through at the same position — is a fixpoint identity: it carries whatever type p
|
|
746
|
+
// settles to, so it constrains nothing. Skip it, else exprType(p) reads p's not-yet-
|
|
747
|
+
// narrowed f64 and the meet poisons the type the non-recursive call sites would prove
|
|
748
|
+
// (nqueens' `solve(all, …)` — `all` stuck f64 while cols/d1/d2, passed as i32 bitwise
|
|
749
|
+
// exprs, narrowed fine).
|
|
750
|
+
const pname = func.sig.params[k].name
|
|
751
|
+
if (recursive && (arg === pname || (Array.isArray(arg) && arg[0] === 'local.get' && arg[1] === pname))) continue
|
|
752
|
+
for (const rule of rules) rule.apply(r, arg, k, state)
|
|
679
753
|
}
|
|
680
754
|
}
|
|
681
755
|
}
|
|
@@ -743,6 +817,46 @@ export default function narrowSignatures(programFacts, ast) {
|
|
|
743
817
|
mergeParamFact(r, field, v)
|
|
744
818
|
},
|
|
745
819
|
})
|
|
820
|
+
// WASM type of a call arg. exprType resolves most shapes, but an INTEGER typed-array
|
|
821
|
+
// element read `intArr[idx]` (and arithmetic over it, `intArr[idx]+1`) types f64 here:
|
|
822
|
+
// exprType's `[]` rule reads the typedElem OVERLAY, which doesn't see a typedCtor-narrowed
|
|
823
|
+
// PARAM array at fixpoint time — yet the element is a 32-bit machine integer. Install the
|
|
824
|
+
// caller's resolved param-typedCtors (+ module globals) as that overlay for the duration
|
|
825
|
+
// of the type query, so a param fed only such integer elements (dict's key `k` ← src[i],
|
|
826
|
+
// threaded through Math.imul / === keys[h] / keys[h]=k) narrows to i32 instead of paying
|
|
827
|
+
// convert + f64-compare + trunc round-trips through its probe loop.
|
|
828
|
+
// A value built ONLY from the callee's own params + already-i32 locals + integer constants via
|
|
829
|
+
// integer-preserving ops. Its i32-ness follows from its inputs' — for a recursive self-call it
|
|
830
|
+
// carries no INDEPENDENT evidence about whether the params are i32. Used for the optimism below.
|
|
831
|
+
const isRecurIntExpr = (n, pnames, callerLocals) => {
|
|
832
|
+
if (typeof n === 'string') return pnames.has(n) || callerLocals?.get?.(n) === 'i32'
|
|
833
|
+
if (typeof n === 'number') return Number.isInteger(n)
|
|
834
|
+
if (!Array.isArray(n)) return false
|
|
835
|
+
if (n[0] == null) return typeof n[1] === 'number' && Number.isInteger(n[1]) // boxed int literal
|
|
836
|
+
if (n[0] === 'local.get') return pnames.has(n[1]) || callerLocals?.get?.(n[1]) === 'i32'
|
|
837
|
+
if (RECUR_INT_OPS.has(n[0])) return n.slice(1).every(c => isRecurIntExpr(c, pnames, callerLocals))
|
|
838
|
+
return false
|
|
839
|
+
}
|
|
840
|
+
const argWasmType = (arg, state) => {
|
|
841
|
+
// Recursive self-call: an arg built only from the callee's own params + already-i32 locals +
|
|
842
|
+
// int constants (`f(n - 1)`, `f(n - 1 - i)`) is i32 IFF those params are i32 — a fixpoint
|
|
843
|
+
// identity carrying no INDEPENDENT type evidence. Optimistically type it i32 so the NON-
|
|
844
|
+
// recursive call sites decide: all i32 ⇒ the param narrows; any f64 ⇒ the meet still poisons
|
|
845
|
+
// it. Lets a plain decreasing recursion narrow with no `|0` source crutch. (The bare-identity
|
|
846
|
+
// arg `f(n)` is already skipped wholesale in runCallsiteLattice.)
|
|
847
|
+
if (state.callee === state.callerFunc?.name &&
|
|
848
|
+
isRecurIntExpr(arg, new Set(state.func.sig.params.map(p => p.name)), state.callerLocals)) return 'i32'
|
|
849
|
+
if (!state._teOverlay) {
|
|
850
|
+
const m = new Map(ctx.scope.globalTypedElem || [])
|
|
851
|
+
const pf = state.callerParamFacts('typedCtor')
|
|
852
|
+
if (pf) for (const [name, ctor] of pf) if (ctor != null) m.set(name, ctor)
|
|
853
|
+
state._teOverlay = m
|
|
854
|
+
}
|
|
855
|
+
const prev = ctx.func.localTypedElemsOverlay
|
|
856
|
+
ctx.func.localTypedElemsOverlay = state._teOverlay
|
|
857
|
+
try { return exprType(arg, state.callerLocals) }
|
|
858
|
+
finally { ctx.func.localTypedElemsOverlay = prev }
|
|
859
|
+
}
|
|
746
860
|
const runFixpoint = () => runCallsiteLattice([
|
|
747
861
|
// val runs SOFT (monotone): a TYPED param's val only becomes inferable after the
|
|
748
862
|
// typedCtor fixpoint + pointer-ABI enrichment, so an early hard merge would
|
|
@@ -754,7 +868,7 @@ export default function narrowSignatures(programFacts, ast) {
|
|
|
754
868
|
missing: poison('wasm'),
|
|
755
869
|
apply(r, arg, _k, state) {
|
|
756
870
|
if (r.wasm === null) return
|
|
757
|
-
const wt =
|
|
871
|
+
const wt = argWasmType(arg, state)
|
|
758
872
|
if (r.wasm === undefined) r.wasm = wt
|
|
759
873
|
else if (r.wasm !== wt) r.wasm = null
|
|
760
874
|
},
|
|
@@ -919,6 +1033,16 @@ export default function narrowSignatures(programFacts, ast) {
|
|
|
919
1033
|
// (callback bench: mix is FNV — params and result all i32-shaped, but inferred
|
|
920
1034
|
// only after E phase narrowed mix's result).
|
|
921
1035
|
phase.refreshLocals()
|
|
1036
|
+
// I2: Re-narrow i32 RESULTS now that Phase G (applyTypedPointerParamAbi) has tagged
|
|
1037
|
+
// typed-array params ptrKind=TYPED. Phase E ran before G, so a function returning a
|
|
1038
|
+
// typed-array element — dict's `lookup = (keys, vals, k) => { … return vals[h] }` with
|
|
1039
|
+
// vals an Int32Array param — had its return tail type as NaN-boxed f64 (vals not yet a
|
|
1040
|
+
// typed pointer), leaving sig.results f64 and the call site running the full
|
|
1041
|
+
// __typed_idx/ToNumber unbox on every probe step (491520× per dict kernel run). Now that
|
|
1042
|
+
// evalTails seeds the typed-param overlay and params carry ptrAux, the fixpoint catches
|
|
1043
|
+
// `vals[h]` as i32, narrows the result, and the dispatch vanishes; the runFixpoint below
|
|
1044
|
+
// then propagates the i32 result into `let v = lookup(...)` at the call sites.
|
|
1045
|
+
narrowI32Results(funcsWithNarrowableResult)
|
|
922
1046
|
// Reset wasm field unconditionally — first pass populated it from stale callerLocals
|
|
923
1047
|
// (where `let h = mix(...)` widened h to f64 because mix's result wasn't narrowed
|
|
924
1048
|
// yet). clearStickyNull only resets null; here we need to reset f64-observed too
|
|
@@ -1334,25 +1458,37 @@ export function specializeBimorphicTyped(programFacts) {
|
|
|
1334
1458
|
|
|
1335
1459
|
// Build one clone per distinct combo.
|
|
1336
1460
|
const cloneByKey = new Map()
|
|
1337
|
-
for (const [
|
|
1338
|
-
|
|
1461
|
+
for (const [dkey, cmb] of distinct) {
|
|
1462
|
+
// NB: this loop variable must NOT reuse the name `combo` (declared twice above, at the
|
|
1463
|
+
// site loop and the distinct-building loop). The self-host miscompiles a for-of loop
|
|
1464
|
+
// variable whose name collides with an earlier block-scoped declaration — it aliases the
|
|
1465
|
+
// prior binding instead of rebinding per iteration, so `combo` would stay stuck at the
|
|
1466
|
+
// last site's ctor and every clone would get the same (wrong) element type → silent
|
|
1467
|
+
// garbage. A unique name gets a clean per-iteration binding.
|
|
1468
|
+
const suffix = cmb.map(c => c.replace(/^new\./, '').replace(/\./g, '_')).join('$')
|
|
1339
1469
|
let cloneName = `${func.name}$${suffix}`
|
|
1340
1470
|
let n = 0
|
|
1341
1471
|
while (ctx.func.names.has(cloneName)) cloneName = `${func.name}$${suffix}$${++n}`
|
|
1342
1472
|
|
|
1473
|
+
// Build cloneSig with clean, fully-formed object literals — never by spreading a
|
|
1474
|
+
// live object and then overriding/extending its keys. A MULTI-prop spread of a
|
|
1475
|
+
// member-access source (`{ ...func.sig, params, results }`) takes the static
|
|
1476
|
+
// allKnown OBJECT-merge path, which trusts func.sig's COMPILE-TIME schema; sig
|
|
1477
|
+
// objects are polymorphic (some carry result/ptrKind/unsignedResult), so that
|
|
1478
|
+
// schema can be a subset of the runtime shape and the slot-copy then faults a
|
|
1479
|
+
// later `sig.params` read out of bounds in the self-host. (The single-unknown
|
|
1480
|
+
// `{ ...x }` clone is fixed at the root — __obj_clone — but the allKnown merge
|
|
1481
|
+
// path is a separate hazard.) Constructing each param with its pointer ABI baked
|
|
1482
|
+
// in sidesteps it; output is unchanged on the host.
|
|
1343
1483
|
const cloneSig = {
|
|
1344
|
-
|
|
1345
|
-
|
|
1484
|
+
params: func.sig.params.map((p, idx) => {
|
|
1485
|
+
const bi = bimorphic.indexOf(idx)
|
|
1486
|
+
return bi < 0
|
|
1487
|
+
? { ...p }
|
|
1488
|
+
: { name: p.name, type: 'i32', ptrKind: VAL.TYPED, ptrAux: typedElemAux(cmb[bi]) }
|
|
1489
|
+
}),
|
|
1346
1490
|
results: [...func.sig.results],
|
|
1347
1491
|
}
|
|
1348
|
-
for (let i = 0; i < bimorphic.length; i++) {
|
|
1349
|
-
const k = bimorphic[i]
|
|
1350
|
-
const aux = typedElemAux(combo[i])
|
|
1351
|
-
const p = cloneSig.params[k]
|
|
1352
|
-
p.type = 'i32'
|
|
1353
|
-
p.ptrKind = VAL.TYPED
|
|
1354
|
-
p.ptrAux = aux
|
|
1355
|
-
}
|
|
1356
1492
|
const clone = { ...func, name: cloneName, sig: cloneSig }
|
|
1357
1493
|
ctx.func.list.push(clone)
|
|
1358
1494
|
ctx.func.map.set(cloneName, clone)
|
|
@@ -1360,19 +1496,20 @@ export function specializeBimorphicTyped(programFacts) {
|
|
|
1360
1496
|
|
|
1361
1497
|
// Mirror per-param reps under the clone's name with mono ctors at bimorphic
|
|
1362
1498
|
// positions. emitFunc's preseed reads typedCtor → seeds typedElem map →
|
|
1363
|
-
// `arr[i]` lowers to direct typed load.
|
|
1499
|
+
// `arr[i]` lowers to direct typed load. Each `{ ...r }` is a true clone, so
|
|
1500
|
+
// pinning typedCtor on it leaves the source rep untouched (__obj_clone).
|
|
1364
1501
|
const cloneReps = new Map()
|
|
1365
1502
|
for (const [k, r] of reps) cloneReps.set(k, { ...r })
|
|
1366
1503
|
for (let i = 0; i < bimorphic.length; i++) {
|
|
1367
1504
|
const k = bimorphic[i]
|
|
1368
1505
|
const r = cloneReps.get(k) || {}
|
|
1369
|
-
r.typedCtor =
|
|
1506
|
+
r.typedCtor = cmb[i]
|
|
1370
1507
|
r.val = VAL.TYPED
|
|
1371
1508
|
cloneReps.set(k, r)
|
|
1372
1509
|
}
|
|
1373
1510
|
paramReps.set(cloneName, cloneReps)
|
|
1374
1511
|
|
|
1375
|
-
cloneByKey.set(
|
|
1512
|
+
cloneByKey.set(dkey, clone)
|
|
1376
1513
|
}
|
|
1377
1514
|
|
|
1378
1515
|
// Rewrite each site's call AST to point at the matching clone.
|
|
@@ -20,8 +20,9 @@
|
|
|
20
20
|
// literal tests use `== null`; created literals are bare numbers.
|
|
21
21
|
|
|
22
22
|
import { findMutations } from './analyze-scans.js'
|
|
23
|
+
import { ASSIGN_OPS } from '../ast.js'
|
|
24
|
+
import { litN, unitIncVar, normalizeLoop, closureMutatedVars, rewriteBlocks, freshLoopId } from './loop-model.js'
|
|
23
25
|
|
|
24
|
-
const litN = (n, k) => Array.isArray(n) && n.length === 2 && n[0] == null && n[1] === k
|
|
25
26
|
const isVar = (n) => typeof n === 'string'
|
|
26
27
|
|
|
27
28
|
// `k = -r`: prepared as ['u-', r] (unary minus) or ['-', 0, r].
|
|
@@ -31,11 +32,10 @@ const negOf = (n) => Array.isArray(n) && n[0] === 'u-' ? n[1]
|
|
|
31
32
|
// Every write to `iv` in `node` is a strictly-positive step (++iv / iv+=1 / iv=iv+1),
|
|
32
33
|
// so iv advances monotonically — required so the three split loops partition [0,bound)
|
|
33
34
|
// and the clamp-free interior never re-runs at an edge index. Any other write → false.
|
|
34
|
-
const ASSIGN = new Set(['=', '+=', '-=', '*=', '/=', '%=', '&=', '|=', '^=', '<<=', '>>=', '>>>=', '**=', '&&=', '||=', '??='])
|
|
35
35
|
function ivMonotonic(node, iv) {
|
|
36
36
|
if (!Array.isArray(node)) return true
|
|
37
37
|
if ((node[0] === '++' || node[0] === '--') && node[1] === iv) return node[0] === '++'
|
|
38
|
-
if (
|
|
38
|
+
if (ASSIGN_OPS.has(node[0]) && node[1] === iv) {
|
|
39
39
|
if (node[0] === '+=' && litN(node[2], 1)) return true
|
|
40
40
|
if (node[0] === '=' && Array.isArray(node[2]) && node[2][0] === '+'
|
|
41
41
|
&& ((node[2][1] === iv && litN(node[2][2], 1)) || (node[2][2] === iv && litN(node[2][1], 1)))) return true
|
|
@@ -44,22 +44,6 @@ function ivMonotonic(node, iv) {
|
|
|
44
44
|
return node.every(c => ivMonotonic(c, iv))
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
-
// Vars assigned inside any closure in the function — a call in the loop can mutate
|
|
48
|
-
// them even though findMutations (direct writes only) misses it. iv/bound/r in this
|
|
49
|
-
// set must bail (same class as the loop-SR closure-mutation guard).
|
|
50
|
-
const collectAssigns = (n, out) => {
|
|
51
|
-
if (!Array.isArray(n)) return
|
|
52
|
-
if (typeof n[1] === 'string' && (ASSIGN.has(n[0]) || n[0] === '++' || n[0] === '--')) out.add(n[1])
|
|
53
|
-
n.forEach(c => collectAssigns(c, out))
|
|
54
|
-
}
|
|
55
|
-
const closureMutated = (n, out) => {
|
|
56
|
-
if (!Array.isArray(n)) return out
|
|
57
|
-
if (n[0] === '=>') collectAssigns(n, out)
|
|
58
|
-
n.forEach(c => closureMutated(c, out))
|
|
59
|
-
return out
|
|
60
|
-
}
|
|
61
|
-
let _cm = new Set()
|
|
62
|
-
|
|
63
47
|
// Find, anywhere in `node`, a clamp `if (ci < 0) ci = 0; else if (ci >= B) ci = B-1`
|
|
64
48
|
// over a var `ci` and bound var `B`. Returns { ci, bound } or null (first match).
|
|
65
49
|
function findClamp(node) {
|
|
@@ -121,7 +105,7 @@ function countWrites(node, v) {
|
|
|
121
105
|
const visit = (x) => {
|
|
122
106
|
if (!Array.isArray(x)) return
|
|
123
107
|
if ((x[0] === '++' || x[0] === '--') && x[1] === v) n++
|
|
124
|
-
else if (
|
|
108
|
+
else if (ASSIGN_OPS.has(x[0]) && x[1] === v) n++
|
|
125
109
|
x.forEach(visit)
|
|
126
110
|
}
|
|
127
111
|
visit(node)
|
|
@@ -151,7 +135,7 @@ function ivWrittenInNestedLoop(body, iv) {
|
|
|
151
135
|
let found = false
|
|
152
136
|
const visit = (n, inLoop) => {
|
|
153
137
|
if (!Array.isArray(n)) return
|
|
154
|
-
if (inLoop && (((n[0] === '++' || n[0] === '--') && n[1] === iv) || (
|
|
138
|
+
if (inLoop && (((n[0] === '++' || n[0] === '--') && n[1] === iv) || (ASSIGN_OPS.has(n[0]) && n[1] === iv))) found = true
|
|
155
139
|
const deeper = inLoop || n[0] === 'while' || n[0] === 'for'
|
|
156
140
|
n.forEach(c => visit(c, deeper))
|
|
157
141
|
}
|
|
@@ -159,41 +143,24 @@ function ivWrittenInNestedLoop(body, iv) {
|
|
|
159
143
|
return found
|
|
160
144
|
}
|
|
161
145
|
|
|
162
|
-
let _uniq = 0
|
|
163
|
-
|
|
164
146
|
// Drop the clamp `if` node from a (cloned) body, leaving the bare `ci = iv + k`.
|
|
165
147
|
const dropClamp = (node, clampNode) =>
|
|
166
148
|
!Array.isArray(node) ? node
|
|
167
149
|
: node === clampNode ? ['{}', [';']] // empty block (the if is a statement)
|
|
168
150
|
: node.map(c => dropClamp(c, clampNode))
|
|
169
151
|
|
|
170
|
-
|
|
171
|
-
const stepIsPosInc = (s, iv) => {
|
|
172
|
-
if (!Array.isArray(s)) return false
|
|
173
|
-
let inc = s
|
|
174
|
-
if (s[0] === '-' && litN(s[2], 1) && Array.isArray(s[1]) && s[1][0] === '++') inc = s[1]
|
|
175
|
-
if (inc[0] === '++' && inc[1] === iv) return true
|
|
176
|
-
if (s[0] === '+=' && s[1] === iv && litN(s[2], 1)) return true
|
|
177
|
-
return s[0] === '=' && s[1] === iv && Array.isArray(s[2]) && s[2][0] === '+'
|
|
178
|
-
&& ((s[2][1] === iv && litN(s[2][2], 1)) || (s[2][2] === iv && litN(s[2][1], 1)))
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
function tryPeel(stmt) {
|
|
182
|
-
if (!Array.isArray(stmt)) return null
|
|
152
|
+
function tryPeel(stmt, cm) {
|
|
183
153
|
// Normalize while / for into (iv, bound, body, init, step). For a `while`, the
|
|
184
154
|
// increment is inside the body; for a `for` it is the separate step clause, which
|
|
185
155
|
// we re-append to each split loop's body (converting the for into init + whiles).
|
|
186
|
-
|
|
187
|
-
if (
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
iv = cond[1]; bound = cond[2]
|
|
195
|
-
if (!stepIsPosInc(step, iv)) return null
|
|
196
|
-
} else return null
|
|
156
|
+
const L = normalizeLoop(stmt)
|
|
157
|
+
if (!L) return null
|
|
158
|
+
let { init, cond, step, body } = L
|
|
159
|
+
if (!Array.isArray(cond) || cond[0] !== '<' || !isVar(cond[1])) return null
|
|
160
|
+
const iv = cond[1], bound = cond[2]
|
|
161
|
+
// The `for` step must be the IV's strictly-positive +1 increment; a `while` increments in
|
|
162
|
+
// its body (validated below by ivMonotonic / the exactly-one-+1 checks).
|
|
163
|
+
if (L.kind === 'for' && unitIncVar(step) !== iv) return null
|
|
197
164
|
if (!isVar(bound) || !Array.isArray(body)) return null
|
|
198
165
|
// A loop whose body is a single statement (e.g. an outer row loop wrapping one
|
|
199
166
|
// inner loop — the vertical blur pass) isn't a `;` sequence; normalize it.
|
|
@@ -227,9 +194,9 @@ function tryPeel(stmt) {
|
|
|
227
194
|
if (!ivMonotonic(body, iv)) return null
|
|
228
195
|
const mut = new Set(); findMutations(body, new Set([bound, r]), mut)
|
|
229
196
|
if (mut.has(bound) || mut.has(r)) return null
|
|
230
|
-
if (
|
|
197
|
+
if (cm.has(iv) || cm.has(bound) || cm.has(r)) return null // closure-mutable → unsafe
|
|
231
198
|
|
|
232
|
-
const id =
|
|
199
|
+
const id = freshLoopId()
|
|
233
200
|
const xs = `__pks${id}`, xe = `__pke${id}`
|
|
234
201
|
// xs = r < bound ? r : bound ; xe = (bound - r) > xs ? bound - r : xs
|
|
235
202
|
const seed = ['let',
|
|
@@ -242,20 +209,7 @@ function tryPeel(stmt) {
|
|
|
242
209
|
return init ? [init, seed, ...loops] : [seed, ...loops]
|
|
243
210
|
}
|
|
244
211
|
|
|
245
|
-
function walk(node) {
|
|
246
|
-
if (!Array.isArray(node)) return node
|
|
247
|
-
const n = node.map(walk)
|
|
248
|
-
if (n[0] !== ';') return n
|
|
249
|
-
const out = [';']
|
|
250
|
-
for (let k = 1; k < n.length; k++) {
|
|
251
|
-
const r = tryPeel(n[k])
|
|
252
|
-
if (r) out.push(...r)
|
|
253
|
-
else out.push(n[k])
|
|
254
|
-
}
|
|
255
|
-
return out
|
|
256
|
-
}
|
|
257
|
-
|
|
258
212
|
export function peelClampedStencil(body) {
|
|
259
|
-
|
|
260
|
-
return
|
|
213
|
+
const cm = closureMutatedVars(body)
|
|
214
|
+
return rewriteBlocks(body, stmt => tryPeel(stmt, cm))
|
|
261
215
|
}
|
|
@@ -129,6 +129,35 @@ export const fixedScalarTypedArray = (expr) => {
|
|
|
129
129
|
? { len, coerce: SCALAR_TYPED_COERCE[ctor] } : null
|
|
130
130
|
}
|
|
131
131
|
|
|
132
|
+
/** Does `expr` allocate a FRESH typed-array buffer of static numeric length —
|
|
133
|
+
* `new Float64Array(N)` with N a compile-time integer? Excludes view ctors
|
|
134
|
+
* (`new Float64Array(someBuffer)` — the arg is a buffer ref, not an int), so the
|
|
135
|
+
* result provably aliases no other buffer. Used by param-distinctness. */
|
|
136
|
+
export const isFreshTypedArrayAlloc = (expr) => {
|
|
137
|
+
if (typedElemCtor(expr) == null) return false
|
|
138
|
+
const args = callArgs(expr)
|
|
139
|
+
return !!args && args.length === 1 && constIntExpr(args[0]) != null
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** Local names const-bound to a fresh typed-array allocation (each a unique buffer, distinct from
|
|
143
|
+
* every other such binding and from any pre-existing value). `const` only — an immutable binding
|
|
144
|
+
* always holds that fresh buffer. Element writes (`a[i]=…`) don't reassign `a`, so they're fine. */
|
|
145
|
+
export const freshTypedArrayLocals = (body) => {
|
|
146
|
+
const fresh = new Set()
|
|
147
|
+
const walk = node => {
|
|
148
|
+
if (!Array.isArray(node) || node[0] === '=>') return
|
|
149
|
+
if (node[0] === 'const') {
|
|
150
|
+
for (let i = 1; i < node.length; i++) {
|
|
151
|
+
const d = node[i]
|
|
152
|
+
if (Array.isArray(d) && d[0] === '=' && typeof d[1] === 'string' && isFreshTypedArrayAlloc(d[2])) fresh.add(d[1])
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
for (let i = 1; i < node.length; i++) walk(node[i])
|
|
156
|
+
}
|
|
157
|
+
walk(body)
|
|
158
|
+
return fresh
|
|
159
|
+
}
|
|
160
|
+
|
|
132
161
|
/** Map of name → {len, coerce} for every fixed-size typed-array binding declared
|
|
133
162
|
* via `let`/`const` directly in `body` (no descent into nested arrows). */
|
|
134
163
|
export const fixedTypedArraysInBody = (body) => {
|
|
@@ -43,7 +43,7 @@ import { inlineHotInternalCalls, inlineLocalLambdas, specializeFixedRestCalls }
|
|
|
43
43
|
import { bindNestedRowLengths, unrollRowLenPadLoops, splitCharScanLoops } from './loops.js'
|
|
44
44
|
import {
|
|
45
45
|
scalarizeFunctionTypedArrays, scalarizeFunctionArrayLiterals,
|
|
46
|
-
promoteIntArrayLiterals, scalarizeFunctionObjectLiterals,
|
|
46
|
+
promoteIntArrayLiterals, scalarizeFunctionObjectLiterals, analyzeParamDistinctness,
|
|
47
47
|
} from './literals.js'
|
|
48
48
|
|
|
49
49
|
export default function plan(ast, profiler) {
|
|
@@ -113,6 +113,9 @@ export default function plan(ast, profiler) {
|
|
|
113
113
|
}
|
|
114
114
|
|
|
115
115
|
t('narrowSignatures', () => narrowSignatures(programFacts, ast))
|
|
116
|
+
// After narrowSignatures (params now carry ptrKind): mark typed-array params that every call
|
|
117
|
+
// site passes a distinct fresh buffer for → enables alias-aware LICM in the optimizer.
|
|
118
|
+
if (optimizing()) t('analyzeParamDistinctness', () => analyzeParamDistinctness(programFacts))
|
|
116
119
|
t('specializeBimorphicTyped', () => specializeBimorphicTyped(programFacts))
|
|
117
120
|
t('refineDynKeys', () => refineDynKeys(programFacts))
|
|
118
121
|
strictBoundaryTypeCheck(programFacts)
|