jz 0.6.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.
Files changed (63) hide show
  1. package/README.md +99 -72
  2. package/bench/README.md +253 -100
  3. package/bench/bench.svg +58 -81
  4. package/cli.js +85 -12
  5. package/dist/interop.js +1 -0
  6. package/dist/jz.js +6196 -0
  7. package/index.d.ts +126 -0
  8. package/index.js +222 -35
  9. package/interop.js +240 -141
  10. package/layout.js +34 -18
  11. package/module/array.js +99 -111
  12. package/module/collection.js +124 -30
  13. package/module/console.js +1 -1
  14. package/module/core.js +163 -19
  15. package/module/json.js +3 -3
  16. package/module/math.js +162 -3
  17. package/module/number.js +268 -13
  18. package/module/object.js +37 -5
  19. package/module/regex.js +8 -7
  20. package/module/simd.js +37 -5
  21. package/module/string.js +203 -168
  22. package/module/typedarray.js +565 -112
  23. package/package.json +26 -7
  24. package/src/abi/string.js +29 -29
  25. package/src/ast.js +19 -2
  26. package/src/autoload.js +3 -0
  27. package/src/compile/analyze-scans.js +174 -11
  28. package/src/compile/analyze.js +101 -4
  29. package/src/compile/cse-load.js +200 -0
  30. package/src/compile/emit-assign.js +82 -20
  31. package/src/compile/emit.js +592 -51
  32. package/src/compile/index.js +504 -89
  33. package/src/compile/infer.js +36 -1
  34. package/src/compile/loop-divmod.js +109 -0
  35. package/src/compile/loop-model.js +91 -0
  36. package/src/compile/loop-square.js +102 -0
  37. package/src/compile/narrow.js +275 -41
  38. package/src/compile/peel-stencil.js +215 -0
  39. package/src/compile/plan/advise.js +55 -1
  40. package/src/compile/plan/common.js +29 -0
  41. package/src/compile/plan/index.js +8 -1
  42. package/src/compile/plan/inline.js +180 -22
  43. package/src/compile/plan/literals.js +313 -24
  44. package/src/compile/plan/scope.js +115 -39
  45. package/src/compile/program-facts.js +21 -2
  46. package/src/ctx.js +96 -19
  47. package/src/helper-counters.js +137 -0
  48. package/src/ir.js +157 -20
  49. package/src/kind-traits.js +34 -3
  50. package/src/kind.js +79 -4
  51. package/src/op-policy.js +5 -2
  52. package/src/ops.js +119 -0
  53. package/src/optimize/index.js +1274 -151
  54. package/src/optimize/recurse.js +182 -0
  55. package/src/optimize/vectorize.js +4187 -253
  56. package/src/prepare/index.js +75 -14
  57. package/src/prepare/lift-iife.js +149 -0
  58. package/src/reps.js +6 -2
  59. package/src/static.js +9 -0
  60. package/src/type.js +63 -51
  61. package/src/wat/assemble.js +286 -21
  62. package/src/widen.js +21 -0
  63. package/src/wat/optimize.js +0 -3760
@@ -0,0 +1,215 @@
1
+ // Edge-clamp peeling for box-filter / stencil loops.
2
+ //
3
+ // A stencil loop reads `arr[clamp(iv + k, 0, BOUND-1)]` for a window of taps k ∈
4
+ // [-r, r]. The clamp guards the array edges, but for the interior iv ∈ [r, BOUND-r)
5
+ // every `iv + k` is already in range, so the clamp is a proven no-op there. Per-tap
6
+ // the branch is cheap-but-not-free, and it blocks the marching-pointer / SIMD lift
7
+ // of the inner accumulation. Measured ~18% of the box-blur pass.
8
+ //
9
+ // Split the loop over `iv` (whose bound is the clamp's BOUND) into three runs —
10
+ // left edge [0, xs), clamp-free interior [xs, xe), right edge [xe, BOUND) — where
11
+ // xs = min(r, BOUND), xe = max(xs, BOUND - r). The interior copy has the clamp `if`
12
+ // dropped (the bare `iv + k` index remains). Bit-exact: for iv ∈ [r, BOUND-r),
13
+ // iv+k ∈ [iv-r, iv+r] ⊆ [0, BOUND-1].
14
+ //
15
+ // Recognized (post-prepare AST): a `while (iv < BOUND)` loop whose body contains a
16
+ // clamp `ci = iv + k; if (ci < 0) ci = 0; else if (ci >= BOUND) ci = BOUND-1` whose
17
+ // BOUND is the SAME var as the loop bound and whose `k` is a tap-loop IV ranging
18
+ // [-r, r] (`k = -r … k <= r`). Both hblur (peel the x-loop) and vblur (peel the
19
+ // y-loop) match. Number literals are sparse-array holes (`n[0]` is undefined), so
20
+ // literal tests use `== null`; created literals are bare numbers.
21
+
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'
25
+
26
+ const isVar = (n) => typeof n === 'string'
27
+
28
+ // `k = -r`: prepared as ['u-', r] (unary minus) or ['-', 0, r].
29
+ const negOf = (n) => Array.isArray(n) && n[0] === 'u-' ? n[1]
30
+ : (Array.isArray(n) && n[0] === '-' && litN(n[1], 0) ? n[2] : null)
31
+
32
+ // Every write to `iv` in `node` is a strictly-positive step (++iv / iv+=1 / iv=iv+1),
33
+ // so iv advances monotonically — required so the three split loops partition [0,bound)
34
+ // and the clamp-free interior never re-runs at an edge index. Any other write → false.
35
+ function ivMonotonic(node, iv) {
36
+ if (!Array.isArray(node)) return true
37
+ if ((node[0] === '++' || node[0] === '--') && node[1] === iv) return node[0] === '++'
38
+ if (ASSIGN_OPS.has(node[0]) && node[1] === iv) {
39
+ if (node[0] === '+=' && litN(node[2], 1)) return true
40
+ if (node[0] === '=' && Array.isArray(node[2]) && node[2][0] === '+'
41
+ && ((node[2][1] === iv && litN(node[2][2], 1)) || (node[2][2] === iv && litN(node[2][1], 1)))) return true
42
+ return false // any other assignment to iv (=, -=, *=, …) breaks monotonicity
43
+ }
44
+ return node.every(c => ivMonotonic(c, iv))
45
+ }
46
+
47
+ // Find, anywhere in `node`, a clamp `if (ci < 0) ci = 0; else if (ci >= B) ci = B-1`
48
+ // over a var `ci` and bound var `B`. Returns { ci, bound } or null (first match).
49
+ function findClamp(node) {
50
+ if (!Array.isArray(node)) return null
51
+ if (node[0] === 'if') {
52
+ const [, cond, then, els] = node
53
+ // outer: if (ci < 0) ci = 0; else <inner>
54
+ if (Array.isArray(cond) && cond[0] === '<' && isVar(cond[1]) && litN(cond[2], 0)
55
+ && Array.isArray(then) && then[0] === '=' && then[1] === cond[1] && litN(then[2], 0)
56
+ && Array.isArray(els) && els[0] === 'if') {
57
+ const ci = cond[1], [, c2, t2] = els
58
+ // inner: if (ci >= B) ci = B-1
59
+ if (Array.isArray(c2) && c2[0] === '>=' && c2[1] === ci && isVar(c2[2])
60
+ && Array.isArray(t2) && t2[0] === '=' && t2[1] === ci
61
+ && Array.isArray(t2[2]) && t2[2][0] === '-' && t2[2][1] === c2[2] && litN(t2[2][2], 1))
62
+ return { ci, bound: c2[2], node }
63
+ }
64
+ }
65
+ for (const c of node) { const r = findClamp(c); if (r) return r }
66
+ return null
67
+ }
68
+
69
+ // `ci = iv + k` (or `k + iv`) assignment/decl: returns { iv, tap } given the clamp var.
70
+ function clampSource(node, ci) {
71
+ let found = null
72
+ const visit = (n) => {
73
+ if (found || !Array.isArray(n)) return
74
+ if (n[0] === '=' && n[1] === ci && Array.isArray(n[2]) && n[2][0] === '+') {
75
+ const [, a, b] = n[2]
76
+ if (isVar(a) && isVar(b)) found = { a, b }
77
+ }
78
+ n.forEach(visit)
79
+ }
80
+ visit(node)
81
+ return found
82
+ }
83
+
84
+ // The tap IV must range EXACTLY [-r, r]: a loop `while (t <= r)` AND `t` seeded to
85
+ // `-r` (same `r`). Returns the radius var `r` only when both match; null otherwise.
86
+ // Asymmetric / wider ranges would make xs=r, xe=bound-r unsound, so they bail.
87
+ function tapRadius(loopBody, tap) {
88
+ let boundR = null, initR = null
89
+ const visit = (n) => {
90
+ if (!Array.isArray(n)) return
91
+ // tap loop bound `k <= r`: while (cond at [1]) or for (cond at [2]).
92
+ const cond = n[0] === 'while' ? n[1] : n[0] === 'for' ? n[2] : null
93
+ if (Array.isArray(cond) && cond[0] === '<=' && cond[1] === tap && isVar(cond[2])) boundR = cond[2]
94
+ // tap init `k = -r` (a bare assignment, or inside the for's `let` init clause).
95
+ if (n[0] === '=' && n[1] === tap) { const neg = negOf(n[2]); if (isVar(neg)) initR = neg }
96
+ n.forEach(visit)
97
+ }
98
+ visit(loopBody)
99
+ return boundR != null && boundR === initR ? boundR : null
100
+ }
101
+
102
+ // Count every write (=, compound-assign, ++/--) to variable `v` in `node`.
103
+ function countWrites(node, v) {
104
+ let n = 0
105
+ const visit = (x) => {
106
+ if (!Array.isArray(x)) return
107
+ if ((x[0] === '++' || x[0] === '--') && x[1] === v) n++
108
+ else if (ASSIGN_OPS.has(x[0]) && x[1] === v) n++
109
+ x.forEach(visit)
110
+ }
111
+ visit(node)
112
+ return n
113
+ }
114
+
115
+ // Count tap-shaped seeds (`tap = -r`) and bounds (`tap <= r`) in `body`. The peel is
116
+ // sound only for a SINGLE tap loop; two loops sharing the tap var make tapRadius pick
117
+ // the wrong (last-seen) radius, so xs=r/xe=bound-r no longer match the clamped loop.
118
+ function tapStructures(body, tap) {
119
+ let seeds = 0, bounds = 0
120
+ const visit = (n) => {
121
+ if (!Array.isArray(n)) return
122
+ const cond = n[0] === 'while' ? n[1] : n[0] === 'for' ? n[2] : null
123
+ if (Array.isArray(cond) && cond[0] === '<=' && cond[1] === tap && isVar(cond[2])) bounds++
124
+ if (n[0] === '=' && n[1] === tap && isVar(negOf(n[2]))) seeds++
125
+ n.forEach(visit)
126
+ }
127
+ visit(body)
128
+ return { seeds, bounds }
129
+ }
130
+
131
+ // True if `iv` is written anywhere INSIDE a nested loop within `body`. Then iv is not
132
+ // constant across the tap accumulation (e.g. an `iv++` living inside the tap loop, so
133
+ // the real outer step is 2r+1), and the per-outer-iteration peel is unsound.
134
+ function ivWrittenInNestedLoop(body, iv) {
135
+ let found = false
136
+ const visit = (n, inLoop) => {
137
+ if (!Array.isArray(n)) return
138
+ if (inLoop && (((n[0] === '++' || n[0] === '--') && n[1] === iv) || (ASSIGN_OPS.has(n[0]) && n[1] === iv))) found = true
139
+ const deeper = inLoop || n[0] === 'while' || n[0] === 'for'
140
+ n.forEach(c => visit(c, deeper))
141
+ }
142
+ visit(body, false)
143
+ return found
144
+ }
145
+
146
+ // Drop the clamp `if` node from a (cloned) body, leaving the bare `ci = iv + k`.
147
+ const dropClamp = (node, clampNode) =>
148
+ !Array.isArray(node) ? node
149
+ : node === clampNode ? ['{}', [';']] // empty block (the if is a statement)
150
+ : node.map(c => dropClamp(c, clampNode))
151
+
152
+ function tryPeel(stmt, cm) {
153
+ // Normalize while / for into (iv, bound, body, init, step). For a `while`, the
154
+ // increment is inside the body; for a `for` it is the separate step clause, which
155
+ // we re-append to each split loop's body (converting the for into init + whiles).
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
164
+ if (!isVar(bound) || !Array.isArray(body)) return null
165
+ // A loop whose body is a single statement (e.g. an outer row loop wrapping one
166
+ // inner loop — the vertical blur pass) isn't a `;` sequence; normalize it.
167
+ if (body[0] !== ';') body = [';', body]
168
+
169
+ const clamp = findClamp(body)
170
+ if (!clamp || clamp.bound !== bound) return null // clamp bound must be this loop's bound
171
+ const src = clampSource(body, clamp.ci)
172
+ if (!src) return null
173
+ // ci = a + b: one operand is the loop IV, the other the tap IV
174
+ const tap = src.a === iv ? src.b : src.b === iv ? src.a : null
175
+ if (!tap) return null
176
+ const r = tapRadius(body, tap)
177
+ if (r == null) return null
178
+
179
+ // The clamp var must be written EXACTLY three times: its `ci = iv+k` source and the
180
+ // two clamp branches (`ci=0`, `ci=bound-1`). Any extra write — `ci = ci-1`, `ci = -ci`
181
+ // between the source and the clamp — changes what value the clamp actually guards, so
182
+ // dropping the clamp in the interior (which assumes the guarded value is iv+k) is
183
+ // unsound. Three is the exact count for a well-formed clamp; more ⇒ a mutation.
184
+ if (countWrites(body, clamp.ci) !== 3) return null
185
+ // Exactly one tap loop (one seed, one bound): two loops sharing the tap var would let
186
+ // tapRadius pick the wrong radius.
187
+ const ts = tapStructures(body, tap)
188
+ if (ts.seeds !== 1 || ts.bounds !== 1) return null
189
+ // iv must be constant across the tap accumulation — not bumped inside the tap loop.
190
+ if (ivWrittenInNestedLoop(body, iv)) return null
191
+
192
+ // Soundness: iv advances monotonically (else the interior re-runs at an edge index),
193
+ // and bound/r are loop-invariant (else the once-computed xs/xe go stale mid-loop).
194
+ if (!ivMonotonic(body, iv)) return null
195
+ const mut = new Set(); findMutations(body, new Set([bound, r]), mut)
196
+ if (mut.has(bound) || mut.has(r)) return null
197
+ if (cm.has(iv) || cm.has(bound) || cm.has(r)) return null // closure-mutable → unsafe
198
+
199
+ const id = freshLoopId()
200
+ const xs = `__pks${id}`, xe = `__pke${id}`
201
+ // xs = r < bound ? r : bound ; xe = (bound - r) > xs ? bound - r : xs
202
+ const seed = ['let',
203
+ ['=', xs, ['?:', ['<', r, bound], r, bound]],
204
+ ['=', xe, ['?:', ['>', ['-', bound, r], xs], ['-', bound, r], xs]]]
205
+ const interiorBody = dropClamp(body, clamp.node)
206
+ // for: append the step to each split loop's body; while: body already increments.
207
+ const mk = (B, bod) => ['while', ['<', iv, B], step ? [';', ...bod.slice(1), step] : bod]
208
+ const loops = [mk(xs, body), mk(xe, interiorBody), mk(bound, body)]
209
+ return init ? [init, seed, ...loops] : [seed, ...loops]
210
+ }
211
+
212
+ export function peelClampedStencil(body) {
213
+ const cm = closureMutatedVars(body)
214
+ return rewriteBlocks(body, stmt => tryPeel(stmt, cm))
215
+ }
@@ -1,4 +1,4 @@
1
- import { ctx, warn } from '../../ctx.js'
1
+ import { ctx, warn, err } from '../../ctx.js'
2
2
  import { refsName, REFS_IN_EXPR } from '../../ast.js'
3
3
  import { intLiteralValue } from '../../static.js'
4
4
  import { VAL } from '../../reps.js'
@@ -307,10 +307,64 @@ function adviseSimdLoops() {
307
307
  }
308
308
 
309
309
  /** Compile-time advisories at end of plan — extensible home for soft warnings. */
310
+ // Generic-dispatch deopt advisory. A module global indexed inside a loop whose type
311
+ // never resolved to a container (its VAL stays null — "any") lowers EVERY `g[i]` to
312
+ // the runtime tag-dispatch path (__typed_idx / string fork): ~13× slower than a proven
313
+ // typed load, and almost always a MISSED type rather than intentional polymorphism — a
314
+ // loop-hot indexed container has one kind in practice (jz's value model already handles
315
+ // genuinely-polymorphic parser data efficiently; that's a different regime). Like TS
316
+ // flagging `any`, surface the bailout so it's fixed at the source — a `new T()`-typed
317
+ // global, or an `instanceof`/`+` guard — instead of silently paying the cliff. Scoped to
318
+ // module globals: their type is final here (params/locals resolve only at emit). Strict
319
+ // mode, which already rejects dynamic features, escalates this to a hard error.
320
+ function adviseGenericDispatch() {
321
+ if (!ctx.warnings && !ctx.transform.strict) return
322
+ const globals = ctx.scope.userGlobals
323
+ if (!globals?.size) return
324
+ const isGeneric = (name) => globals.has(name) && !ctx.scope.globalValTypes?.get(name)
325
+ // A global narrowed by `g instanceof Ctor` / `typeof g` in this function reads
326
+ // through the refined fast path — the user already applied the recommended fix,
327
+ // so flagging it would be noise. (Refinements are emit-time; this AST probe is
328
+ // the sound, conservative suppressor — a guard anywhere in the fn silences it.)
329
+ const guarded = (body) => {
330
+ const set = new Set()
331
+ const scan = (n) => {
332
+ if (!Array.isArray(n)) return
333
+ // Raw forms (strict mode skips jzify, so `instanceof`/`typeof` survive)…
334
+ if ((n[0] === 'instanceof' || n[0] === 'typeof') && typeof n[1] === 'string') set.add(n[1])
335
+ // …and the lowered predicate jzify emits — `g instanceof Float64Array` becomes
336
+ // `__is_typed(g)`, `typeof g === 'string'` becomes `__is_str(g)`, etc.
337
+ else if (n[0] === '()' && typeof n[1] === 'string' && n[1].startsWith('__is') && typeof n[2] === 'string') set.add(n[2])
338
+ for (let i = 1; i < n.length; i++) scan(n[i])
339
+ }
340
+ scan(body)
341
+ return set
342
+ }
343
+ for (const func of ctx.func.list) {
344
+ if (func.raw || !func.body) continue
345
+ const fn = func.name
346
+ const narrowed = guarded(func.body)
347
+ const walk = (node, inLoop) => {
348
+ if (!Array.isArray(node)) return
349
+ const op = node[0]
350
+ if (op === '[]' && inLoop && typeof node[1] === 'string' && isGeneric(node[1]) && !narrowed.has(node[1])) {
351
+ const g = node[1]
352
+ const msg = `'${g}' is indexed (\`${g}[…]\`) in a loop but its type never resolved — every access falls back to runtime dynamic dispatch (~10× slower than a typed load). Give it one provable kind: assign \`${g} = new Float64Array(…)\`, or narrow with \`instanceof\`/\`+\`.`
353
+ if (ctx.transform.strict) err(`strict mode: ${msg} Pass { strict: false } to allow dynamic dispatch.`)
354
+ warn('deopt-generic', msg, { fn }, node.loc)
355
+ }
356
+ const nowLoop = inLoop || HEAP_LOOP_OPS.has(op)
357
+ for (let i = 1; i < node.length; i++) walk(node[i], nowLoop)
358
+ }
359
+ walk(func.body, false)
360
+ }
361
+ }
362
+
310
363
  export function adviseProgram(programFacts) {
311
364
  adviseHeapGrowth()
312
365
  adviseSetMapIterationOrder()
313
366
  if (programFacts) adviseJsstringCarrier(programFacts.paramReps, programFacts.valueUsed)
314
367
  adviseSimdLoops()
368
+ adviseGenericDispatch()
315
369
  }
316
370
 
@@ -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) => {
@@ -29,6 +29,7 @@ import { collectProgramFacts, refreshProgramFacts } from '../program-facts.js'
29
29
  import narrowSignatures, {
30
30
  specializeBimorphicTyped, refineDynKeys,
31
31
  applyJsstringBoundaryCarrierStandalone, narrowBoolResults,
32
+ strictBoundaryTypeCheck,
32
33
  } from '../narrow.js'
33
34
 
34
35
  import { optimizing } from './common.js'
@@ -42,7 +43,7 @@ import { inlineHotInternalCalls, inlineLocalLambdas, specializeFixedRestCalls }
42
43
  import { bindNestedRowLengths, unrollRowLenPadLoops, splitCharScanLoops } from './loops.js'
43
44
  import {
44
45
  scalarizeFunctionTypedArrays, scalarizeFunctionArrayLiterals,
45
- promoteIntArrayLiterals, scalarizeFunctionObjectLiterals,
46
+ promoteIntArrayLiterals, scalarizeFunctionObjectLiterals, analyzeParamDistinctness,
46
47
  } from './literals.js'
47
48
 
48
49
  export default function plan(ast, profiler) {
@@ -94,6 +95,7 @@ export default function plan(ast, profiler) {
94
95
  sweep('scalarizeTypedArrays', () => scalarizeFunctionTypedArrays(programFacts))
95
96
  }
96
97
  ctx.types.dynKeyVars = programFacts.dynVars
98
+ ctx.types.dynWriteVars = programFacts.dynWriteVars
97
99
  ctx.types.anyDynKey = programFacts.anyDyn
98
100
 
99
101
  t('materializeAutoBoxSchemas', () => materializeAutoBoxSchemas(programFacts))
@@ -105,13 +107,18 @@ export default function plan(ast, profiler) {
105
107
  // fact, so `export let f = (a) => a > 2` boxes its boundary atom.
106
108
  applyJsstringBoundaryCarrierStandalone(programFacts)
107
109
  narrowBoolResults()
110
+ strictBoundaryTypeCheck(programFacts)
108
111
  adviseProgram(programFacts)
109
112
  return programFacts
110
113
  }
111
114
 
112
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))
113
119
  t('specializeBimorphicTyped', () => specializeBimorphicTyped(programFacts))
114
120
  t('refineDynKeys', () => refineDynKeys(programFacts))
121
+ strictBoundaryTypeCheck(programFacts)
115
122
 
116
123
  adviseProgram(programFacts)
117
124
  return programFacts