jz 0.5.1 → 0.7.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 (86) hide show
  1. package/README.md +315 -318
  2. package/bench/README.md +369 -0
  3. package/bench/bench.svg +102 -0
  4. package/cli.js +104 -30
  5. package/dist/interop.js +1 -0
  6. package/dist/jz.js +6024 -0
  7. package/index.d.ts +126 -0
  8. package/index.js +362 -84
  9. package/interop.js +159 -186
  10. package/jz.svg +5 -0
  11. package/jzify/arguments.js +97 -0
  12. package/jzify/bundler.js +382 -0
  13. package/jzify/classes.js +328 -0
  14. package/jzify/hoist-vars.js +177 -0
  15. package/jzify/index.js +51 -0
  16. package/jzify/names.js +37 -0
  17. package/jzify/switch.js +106 -0
  18. package/jzify/transform.js +349 -0
  19. package/layout.js +184 -0
  20. package/module/array.js +377 -176
  21. package/module/collection.js +639 -145
  22. package/module/console.js +55 -43
  23. package/module/core.js +264 -153
  24. package/module/date.js +15 -3
  25. package/module/function.js +73 -6
  26. package/module/index.js +2 -1
  27. package/module/json.js +226 -61
  28. package/module/math.js +551 -187
  29. package/module/number.js +327 -60
  30. package/module/object.js +474 -184
  31. package/module/regex.js +255 -25
  32. package/module/schema.js +24 -6
  33. package/module/simd.js +117 -0
  34. package/module/string.js +621 -226
  35. package/module/symbol.js +1 -1
  36. package/module/timer.js +9 -14
  37. package/module/typedarray.js +459 -73
  38. package/package.json +58 -14
  39. package/src/abi/index.js +39 -23
  40. package/src/abi/string.js +38 -41
  41. package/src/ast.js +460 -0
  42. package/src/autoload.js +29 -24
  43. package/src/bridge.js +111 -0
  44. package/src/compile/analyze-scans.js +824 -0
  45. package/src/compile/analyze.js +1600 -0
  46. package/src/compile/emit-assign.js +411 -0
  47. package/src/compile/emit.js +3512 -0
  48. package/src/compile/flow-types.js +103 -0
  49. package/src/{compile.js → compile/index.js} +778 -128
  50. package/src/{infer.js → compile/infer.js} +62 -98
  51. package/src/compile/loop-divmod.js +155 -0
  52. package/src/{narrow.js → compile/narrow.js} +409 -106
  53. package/src/compile/peel-stencil.js +261 -0
  54. package/src/compile/plan/advise.js +370 -0
  55. package/src/compile/plan/common.js +150 -0
  56. package/src/compile/plan/index.js +122 -0
  57. package/src/compile/plan/inline.js +682 -0
  58. package/src/compile/plan/literals.js +1199 -0
  59. package/src/compile/plan/loops.js +472 -0
  60. package/src/compile/plan/scope.js +649 -0
  61. package/src/compile/program-facts.js +423 -0
  62. package/src/ctx.js +220 -64
  63. package/src/ir.js +589 -172
  64. package/src/kind-traits.js +132 -0
  65. package/src/kind.js +524 -0
  66. package/src/op-policy.js +57 -0
  67. package/src/{optimize.js → optimize/index.js} +1432 -473
  68. package/src/optimize/vectorize.js +4660 -0
  69. package/src/param-reps.js +65 -0
  70. package/src/parse.js +44 -0
  71. package/src/{prepare.js → prepare/index.js} +649 -205
  72. package/src/prepare/lift-iife.js +149 -0
  73. package/src/reps.js +116 -0
  74. package/src/resolve.js +12 -3
  75. package/src/static.js +208 -0
  76. package/src/type.js +651 -0
  77. package/src/{assemble.js → wat/assemble.js} +275 -55
  78. package/src/wat/optimize.js +3938 -0
  79. package/transform.js +21 -0
  80. package/wasi.js +47 -5
  81. package/src/analyze.js +0 -3818
  82. package/src/emit.js +0 -3040
  83. package/src/jzify.js +0 -1580
  84. package/src/plan.js +0 -2132
  85. package/src/vectorize.js +0 -1088
  86. /package/src/{codegen.js → wat/codegen.js} +0 -0
@@ -0,0 +1,261 @@
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
+
24
+ const litN = (n, k) => Array.isArray(n) && n.length === 2 && n[0] == null && n[1] === k
25
+ const isVar = (n) => typeof n === 'string'
26
+
27
+ // `k = -r`: prepared as ['u-', r] (unary minus) or ['-', 0, r].
28
+ const negOf = (n) => Array.isArray(n) && n[0] === 'u-' ? n[1]
29
+ : (Array.isArray(n) && n[0] === '-' && litN(n[1], 0) ? n[2] : null)
30
+
31
+ // Every write to `iv` in `node` is a strictly-positive step (++iv / iv+=1 / iv=iv+1),
32
+ // so iv advances monotonically — required so the three split loops partition [0,bound)
33
+ // and the clamp-free interior never re-runs at an edge index. Any other write → false.
34
+ const ASSIGN = new Set(['=', '+=', '-=', '*=', '/=', '%=', '&=', '|=', '^=', '<<=', '>>=', '>>>=', '**=', '&&=', '||=', '??='])
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.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
+ // 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
+ // Find, anywhere in `node`, a clamp `if (ci < 0) ci = 0; else if (ci >= B) ci = B-1`
64
+ // over a var `ci` and bound var `B`. Returns { ci, bound } or null (first match).
65
+ function findClamp(node) {
66
+ if (!Array.isArray(node)) return null
67
+ if (node[0] === 'if') {
68
+ const [, cond, then, els] = node
69
+ // outer: if (ci < 0) ci = 0; else <inner>
70
+ if (Array.isArray(cond) && cond[0] === '<' && isVar(cond[1]) && litN(cond[2], 0)
71
+ && Array.isArray(then) && then[0] === '=' && then[1] === cond[1] && litN(then[2], 0)
72
+ && Array.isArray(els) && els[0] === 'if') {
73
+ const ci = cond[1], [, c2, t2] = els
74
+ // inner: if (ci >= B) ci = B-1
75
+ if (Array.isArray(c2) && c2[0] === '>=' && c2[1] === ci && isVar(c2[2])
76
+ && Array.isArray(t2) && t2[0] === '=' && t2[1] === ci
77
+ && Array.isArray(t2[2]) && t2[2][0] === '-' && t2[2][1] === c2[2] && litN(t2[2][2], 1))
78
+ return { ci, bound: c2[2], node }
79
+ }
80
+ }
81
+ for (const c of node) { const r = findClamp(c); if (r) return r }
82
+ return null
83
+ }
84
+
85
+ // `ci = iv + k` (or `k + iv`) assignment/decl: returns { iv, tap } given the clamp var.
86
+ function clampSource(node, ci) {
87
+ let found = null
88
+ const visit = (n) => {
89
+ if (found || !Array.isArray(n)) return
90
+ if (n[0] === '=' && n[1] === ci && Array.isArray(n[2]) && n[2][0] === '+') {
91
+ const [, a, b] = n[2]
92
+ if (isVar(a) && isVar(b)) found = { a, b }
93
+ }
94
+ n.forEach(visit)
95
+ }
96
+ visit(node)
97
+ return found
98
+ }
99
+
100
+ // The tap IV must range EXACTLY [-r, r]: a loop `while (t <= r)` AND `t` seeded to
101
+ // `-r` (same `r`). Returns the radius var `r` only when both match; null otherwise.
102
+ // Asymmetric / wider ranges would make xs=r, xe=bound-r unsound, so they bail.
103
+ function tapRadius(loopBody, tap) {
104
+ let boundR = null, initR = null
105
+ const visit = (n) => {
106
+ if (!Array.isArray(n)) return
107
+ // tap loop bound `k <= r`: while (cond at [1]) or for (cond at [2]).
108
+ const cond = n[0] === 'while' ? n[1] : n[0] === 'for' ? n[2] : null
109
+ if (Array.isArray(cond) && cond[0] === '<=' && cond[1] === tap && isVar(cond[2])) boundR = cond[2]
110
+ // tap init `k = -r` (a bare assignment, or inside the for's `let` init clause).
111
+ if (n[0] === '=' && n[1] === tap) { const neg = negOf(n[2]); if (isVar(neg)) initR = neg }
112
+ n.forEach(visit)
113
+ }
114
+ visit(loopBody)
115
+ return boundR != null && boundR === initR ? boundR : null
116
+ }
117
+
118
+ // Count every write (=, compound-assign, ++/--) to variable `v` in `node`.
119
+ function countWrites(node, v) {
120
+ let n = 0
121
+ const visit = (x) => {
122
+ if (!Array.isArray(x)) return
123
+ if ((x[0] === '++' || x[0] === '--') && x[1] === v) n++
124
+ else if (ASSIGN.has(x[0]) && x[1] === v) n++
125
+ x.forEach(visit)
126
+ }
127
+ visit(node)
128
+ return n
129
+ }
130
+
131
+ // Count tap-shaped seeds (`tap = -r`) and bounds (`tap <= r`) in `body`. The peel is
132
+ // sound only for a SINGLE tap loop; two loops sharing the tap var make tapRadius pick
133
+ // the wrong (last-seen) radius, so xs=r/xe=bound-r no longer match the clamped loop.
134
+ function tapStructures(body, tap) {
135
+ let seeds = 0, bounds = 0
136
+ const visit = (n) => {
137
+ if (!Array.isArray(n)) return
138
+ const cond = n[0] === 'while' ? n[1] : n[0] === 'for' ? n[2] : null
139
+ if (Array.isArray(cond) && cond[0] === '<=' && cond[1] === tap && isVar(cond[2])) bounds++
140
+ if (n[0] === '=' && n[1] === tap && isVar(negOf(n[2]))) seeds++
141
+ n.forEach(visit)
142
+ }
143
+ visit(body)
144
+ return { seeds, bounds }
145
+ }
146
+
147
+ // True if `iv` is written anywhere INSIDE a nested loop within `body`. Then iv is not
148
+ // constant across the tap accumulation (e.g. an `iv++` living inside the tap loop, so
149
+ // the real outer step is 2r+1), and the per-outer-iteration peel is unsound.
150
+ function ivWrittenInNestedLoop(body, iv) {
151
+ let found = false
152
+ const visit = (n, inLoop) => {
153
+ if (!Array.isArray(n)) return
154
+ if (inLoop && (((n[0] === '++' || n[0] === '--') && n[1] === iv) || (ASSIGN.has(n[0]) && n[1] === iv))) found = true
155
+ const deeper = inLoop || n[0] === 'while' || n[0] === 'for'
156
+ n.forEach(c => visit(c, deeper))
157
+ }
158
+ visit(body, false)
159
+ return found
160
+ }
161
+
162
+ let _uniq = 0
163
+
164
+ // Drop the clamp `if` node from a (cloned) body, leaving the bare `ci = iv + k`.
165
+ const dropClamp = (node, clampNode) =>
166
+ !Array.isArray(node) ? node
167
+ : node === clampNode ? ['{}', [';']] // empty block (the if is a statement)
168
+ : node.map(c => dropClamp(c, clampNode))
169
+
170
+ // A strictly-positive +1 step on `iv` (the for-loop increment): i++, ++i, i+=1, i=i+1.
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
183
+ // Normalize while / for into (iv, bound, body, init, step). For a `while`, the
184
+ // increment is inside the body; for a `for` it is the separate step clause, which
185
+ // we re-append to each split loop's body (converting the for into init + whiles).
186
+ let iv, bound, body, init = null, step = null
187
+ if (stmt[0] === 'while') {
188
+ const cond = stmt[1]
189
+ if (!Array.isArray(cond) || cond[0] !== '<' || !isVar(cond[1])) return null
190
+ iv = cond[1]; bound = cond[2]; body = stmt[2]
191
+ } else if (stmt[0] === 'for') {
192
+ init = stmt[1]; const cond = stmt[2]; step = stmt[3]; body = stmt[4]
193
+ if (!Array.isArray(cond) || cond[0] !== '<' || !isVar(cond[1])) return null
194
+ iv = cond[1]; bound = cond[2]
195
+ if (!stepIsPosInc(step, iv)) return null
196
+ } else return null
197
+ if (!isVar(bound) || !Array.isArray(body)) return null
198
+ // A loop whose body is a single statement (e.g. an outer row loop wrapping one
199
+ // inner loop — the vertical blur pass) isn't a `;` sequence; normalize it.
200
+ if (body[0] !== ';') body = [';', body]
201
+
202
+ const clamp = findClamp(body)
203
+ if (!clamp || clamp.bound !== bound) return null // clamp bound must be this loop's bound
204
+ const src = clampSource(body, clamp.ci)
205
+ if (!src) return null
206
+ // ci = a + b: one operand is the loop IV, the other the tap IV
207
+ const tap = src.a === iv ? src.b : src.b === iv ? src.a : null
208
+ if (!tap) return null
209
+ const r = tapRadius(body, tap)
210
+ if (r == null) return null
211
+
212
+ // The clamp var must be written EXACTLY three times: its `ci = iv+k` source and the
213
+ // two clamp branches (`ci=0`, `ci=bound-1`). Any extra write — `ci = ci-1`, `ci = -ci`
214
+ // between the source and the clamp — changes what value the clamp actually guards, so
215
+ // dropping the clamp in the interior (which assumes the guarded value is iv+k) is
216
+ // unsound. Three is the exact count for a well-formed clamp; more ⇒ a mutation.
217
+ if (countWrites(body, clamp.ci) !== 3) return null
218
+ // Exactly one tap loop (one seed, one bound): two loops sharing the tap var would let
219
+ // tapRadius pick the wrong radius.
220
+ const ts = tapStructures(body, tap)
221
+ if (ts.seeds !== 1 || ts.bounds !== 1) return null
222
+ // iv must be constant across the tap accumulation — not bumped inside the tap loop.
223
+ if (ivWrittenInNestedLoop(body, iv)) return null
224
+
225
+ // Soundness: iv advances monotonically (else the interior re-runs at an edge index),
226
+ // and bound/r are loop-invariant (else the once-computed xs/xe go stale mid-loop).
227
+ if (!ivMonotonic(body, iv)) return null
228
+ const mut = new Set(); findMutations(body, new Set([bound, r]), mut)
229
+ if (mut.has(bound) || mut.has(r)) return null
230
+ if (_cm.has(iv) || _cm.has(bound) || _cm.has(r)) return null // closure-mutable → unsafe
231
+
232
+ const id = _uniq++
233
+ const xs = `__pks${id}`, xe = `__pke${id}`
234
+ // xs = r < bound ? r : bound ; xe = (bound - r) > xs ? bound - r : xs
235
+ const seed = ['let',
236
+ ['=', xs, ['?:', ['<', r, bound], r, bound]],
237
+ ['=', xe, ['?:', ['>', ['-', bound, r], xs], ['-', bound, r], xs]]]
238
+ const interiorBody = dropClamp(body, clamp.node)
239
+ // for: append the step to each split loop's body; while: body already increments.
240
+ const mk = (B, bod) => ['while', ['<', iv, B], step ? [';', ...bod.slice(1), step] : bod]
241
+ const loops = [mk(xs, body), mk(xe, interiorBody), mk(bound, body)]
242
+ return init ? [init, seed, ...loops] : [seed, ...loops]
243
+ }
244
+
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
+ export function peelClampedStencil(body) {
259
+ _cm = closureMutated(body, new Set())
260
+ return walk(body)
261
+ }
@@ -0,0 +1,370 @@
1
+ import { ctx, warn, err } from '../../ctx.js'
2
+ import { refsName, REFS_IN_EXPR } from '../../ast.js'
3
+ import { intLiteralValue } from '../../static.js'
4
+ import { VAL } from '../../reps.js'
5
+ import { adviseJsstringCarrier } from '../narrow.js'
6
+
7
+ /** Compile-time advisories — heap growth, Map iteration order, SIMD hints. */
8
+ const HEAP_LOOP_OPS = new Set(['for', 'for-in', 'for-of', 'while', 'do', 'do-while'])
9
+ const HEAP_VALS = new Set([
10
+ VAL.ARRAY, VAL.STRING, VAL.OBJECT, VAL.HASH, VAL.SET, VAL.MAP,
11
+ VAL.CLOSURE, VAL.TYPED, VAL.REGEX, VAL.BUFFER,
12
+ ])
13
+
14
+ function returnsHeap(func) {
15
+ if (func.sig.ptrKind != null) return true
16
+ return func.valResult != null && HEAP_VALS.has(func.valResult)
17
+ }
18
+
19
+ function isHeapAlloc(node) {
20
+ if (!Array.isArray(node)) return false
21
+ const op = node[0]
22
+ if (op === '{}') {
23
+ // `['{}', [';', …]]` is a block body, not an object literal.
24
+ if (node.length === 2 && Array.isArray(node[1]) && node[1][0] === ';') return false
25
+ return node.length > 1
26
+ }
27
+ if (op === '[]') return node.length === 2
28
+ if (op === '()' && Array.isArray(node[1]) && node[1][0] === '.') {
29
+ const method = node[1][2]
30
+ if (method === 'push' || method === 'concat') return true
31
+ }
32
+ return false
33
+ }
34
+
35
+ function containsHeapAlloc(node) {
36
+ if (!Array.isArray(node)) return false
37
+ if (isHeapAlloc(node)) return true
38
+ for (let i = 1; i < node.length; i++)
39
+ if (containsHeapAlloc(node[i])) return true
40
+ return false
41
+ }
42
+
43
+ function heapLoopBody(node) {
44
+ if (!Array.isArray(node) || !HEAP_LOOP_OPS.has(node[0])) return null
45
+ return node[node.length - 1]
46
+ }
47
+
48
+ function heapLoopAllocSites(body) {
49
+ const sites = []
50
+ const walk = (node) => {
51
+ if (!Array.isArray(node)) return
52
+ if (HEAP_LOOP_OPS.has(node[0])) {
53
+ const lb = heapLoopBody(node)
54
+ if (lb && containsHeapAlloc(lb))
55
+ sites.push({ loc: node.loc ?? lb.loc })
56
+ }
57
+ for (let i = 1; i < node.length; i++) walk(node[i])
58
+ }
59
+ walk(body)
60
+ return sites
61
+ }
62
+
63
+ function bodyHeapAllocates(body) {
64
+ return body != null && containsHeapAlloc(body)
65
+ }
66
+
67
+ /** Mirrors `applyArenaRewind` eligibility in src/assemble.js (AST-level). */
68
+ function isArenaRewindable(func) {
69
+ if (func.raw) return false
70
+ if (func.sig.params.length !== 0) return false
71
+ if (func.sig.results.length !== 1) return false
72
+ if (func.sig.ptrKind != null) return false
73
+ if (returnsHeap(func)) return false
74
+ if (func.sig.results[0] === 'f64' && func.valResult !== VAL.NUMBER && func.valResult != null)
75
+ return false
76
+ if (func.sig.results[0] !== 'f64' && func.sig.results[0] !== 'i32') return false
77
+ return bodyHeapAllocates(func.body)
78
+ }
79
+
80
+ function exportedFuncNames() {
81
+ const names = new Set()
82
+ for (const [key, val] of Object.entries(ctx.func.exports)) {
83
+ const name = val === true ? key : (typeof val === 'string' ? val : null)
84
+ if (name) names.add(name)
85
+ }
86
+ return names
87
+ }
88
+
89
+ /** Bump-allocator growth advisories — no-op without an `opts.warnings` sink. */
90
+ function adviseHeapGrowth() {
91
+ if (!ctx.warnings) return
92
+ if (ctx.transform.alloc === false) return
93
+
94
+ const exported = exportedFuncNames()
95
+
96
+ for (const func of ctx.func.list) {
97
+ if (func.raw || !func.body) continue
98
+
99
+ const fn = func.name
100
+ const isExport = exported.has(fn)
101
+
102
+ if (isExport && returnsHeap(func)) {
103
+ warn('heap-return',
104
+ `export '${fn}' returns a heap value — repeated calls grow linear memory; call memory.reset() between batches from the host`,
105
+ { fn }, func.body.loc)
106
+ continue
107
+ }
108
+
109
+ const loopSites = heapLoopAllocSites(func.body)
110
+ for (const site of loopSites) {
111
+ warn('heap-loop',
112
+ `${isExport ? `export '${fn}'` : `'${fn}'`} allocates heap values inside a loop — peak memory grows with trip count; call memory.reset() between batches from the host`,
113
+ { fn }, site.loc)
114
+ }
115
+
116
+ if (isExport && !returnsHeap(func) && bodyHeapAllocates(func.body)
117
+ && !isArenaRewindable(func) && loopSites.length === 0) {
118
+ const code = func.sig.params.length > 0 ? 'arena-rewind-skipped' : 'heap-per-call'
119
+ const detail = func.sig.params.length > 0
120
+ ? `export '${fn}' allocates heap values but cannot rewind per call (parameters or returned pointers prevent arena rewind)`
121
+ : `export '${fn}' allocates heap values — jz does not reclaim between calls`
122
+ warn(code,
123
+ `${detail}; call memory.reset() between batches from the host`,
124
+ { fn }, func.body.loc)
125
+ }
126
+ }
127
+ }
128
+
129
+ const SET_MAP_ITER_OPS = new Set(['for-in', 'for-of'])
130
+ const SET_MAP_METHODS = new Set(['keys', 'values', 'entries', 'forEach'])
131
+ const SET_MAP_SLOT_ORDER = 'uses slot order, not insertion order — results may differ from JavaScript'
132
+
133
+ function newSetMapKind(node) {
134
+ if (!Array.isArray(node)) return null
135
+ if (node[0] === 'new') {
136
+ const ctor = node[1]
137
+ const name = typeof ctor === 'string' ? ctor
138
+ : Array.isArray(ctor) && ctor[0] === '()' && typeof ctor[1] === 'string' ? ctor[1]
139
+ : null
140
+ if (name === 'Set') return 'set'
141
+ if (name === 'Map') return 'map'
142
+ }
143
+ if (node[0] === '()' && typeof node[1] === 'string') {
144
+ if (node[1] === 'new.Set') return 'set'
145
+ if (node[1] === 'new.Map') return 'map'
146
+ }
147
+ return null
148
+ }
149
+
150
+ function collectSetMapBindings(body) {
151
+ const bindings = new Map()
152
+ const walk = (node) => {
153
+ if (!Array.isArray(node)) return
154
+ const op = node[0]
155
+ if (op === 'let' || op === 'const') {
156
+ for (let i = 1; i < node.length; i++) {
157
+ const d = node[i]
158
+ if (!Array.isArray(d) || d[0] !== '=' || typeof d[1] !== 'string') continue
159
+ const kind = newSetMapKind(d[2])
160
+ if (kind) bindings.set(d[1], kind)
161
+ }
162
+ }
163
+ for (let i = 1; i < node.length; i++) walk(node[i])
164
+ }
165
+ walk(body)
166
+ return bindings
167
+ }
168
+
169
+ function exprSetMapKind(expr, bindings) {
170
+ const direct = newSetMapKind(expr)
171
+ if (direct) return direct
172
+ return typeof expr === 'string' ? bindings.get(expr) || null : null
173
+ }
174
+
175
+ function isJsonStringifyCall(node) {
176
+ if (!Array.isArray(node) || node[0] !== '()') return false
177
+ const callee = node[1]
178
+ if (callee === 'JSON.stringify') return true
179
+ return Array.isArray(callee) && callee[0] === '.' && callee[1] === 'JSON' && callee[2] === 'stringify'
180
+ }
181
+
182
+ function adviseSetMapIterationOrder() {
183
+ if (!ctx.warnings) return
184
+
185
+ for (const func of ctx.func.list) {
186
+ if (func.raw || !func.body) continue
187
+ const fn = func.name
188
+ const bindings = collectSetMapBindings(func.body)
189
+
190
+ const warnOrder = (msg, loc) => warn('set-map-order', msg, { fn }, loc)
191
+ const label = (kind) => kind === 'set' ? 'Set' : 'Map'
192
+
193
+ const walk = (node) => {
194
+ if (!Array.isArray(node)) return
195
+ const op = node[0]
196
+
197
+ if (SET_MAP_ITER_OPS.has(op)) {
198
+ const kind = exprSetMapKind(node[2], bindings)
199
+ if (kind) warnOrder(`${label(kind)} iteration ${SET_MAP_SLOT_ORDER}`, node.loc ?? node[2]?.loc)
200
+ }
201
+
202
+ if (op === '()' && Array.isArray(node[1]) && node[1][0] === '.') {
203
+ const [, recv, method] = node[1]
204
+ const kind = SET_MAP_METHODS.has(method) ? exprSetMapKind(recv, bindings) : null
205
+ if (kind) warnOrder(`${label(kind)}.${method}() ${SET_MAP_SLOT_ORDER}`, node.loc ?? recv?.loc)
206
+ }
207
+
208
+ if (isJsonStringifyCall(node)) {
209
+ const kind = exprSetMapKind(node[2], bindings)
210
+ if (kind) warnOrder(`JSON.stringify on a ${kind} serializes entries in slot order, not insertion order — output may differ from JavaScript`, node.loc)
211
+ }
212
+
213
+ if (op === '...') {
214
+ const kind = exprSetMapKind(node[1], bindings)
215
+ if (kind) warnOrder(`spread over a ${kind} follows slot order, not insertion order — element order may differ from JavaScript`, node.loc)
216
+ }
217
+
218
+ for (let i = 1; i < node.length; i++) walk(node[i])
219
+ }
220
+ walk(func.body)
221
+ }
222
+ }
223
+
224
+ function forInductionVar(step) {
225
+ if (!Array.isArray(step)) return null
226
+ if (step[0] === '++' || step[0] === '--') return typeof step[1] === 'string' ? step[1] : null
227
+ if (step[0] === '-' && Array.isArray(step[1]) && step[1][0] === '++')
228
+ return typeof step[1][1] === 'string' ? step[1][1] : null
229
+ if ((step[0] === '+=' || step[0] === '-=') && step[2]?.[0] == null && step[2]?.[1] === 1)
230
+ return typeof step[1] === 'string' ? step[1] : null
231
+ return null
232
+ }
233
+
234
+ function indexStrideOnVar(indexExpr, iv) {
235
+ if (!Array.isArray(indexExpr)) return 1
236
+ const op = indexExpr[0]
237
+ if (indexExpr === iv) return 1
238
+ if (op === '[]' && indexExpr[2] === iv) return 1
239
+ if (op === '*' && ((indexExpr[1] === iv && intLiteralValue(indexExpr[2]) > 1)
240
+ || (indexExpr[2] === iv && intLiteralValue(indexExpr[1]) > 1)))
241
+ return intLiteralValue(indexExpr[1] === iv ? indexExpr[2] : indexExpr[1])
242
+ if (op === '+') {
243
+ for (let i = 1; i < indexExpr.length; i++) {
244
+ const s = indexStrideOnVar(indexExpr[i], iv)
245
+ if (s > 1) return s
246
+ }
247
+ }
248
+ return 1
249
+ }
250
+
251
+ const SIMD_REDUCE_OPS = new Set(['+=', '|=', '&=', '^=', '-=', '*=', '/=', '%='])
252
+
253
+ function simdLoopIssues(body, iv) {
254
+ let indexed = false, carried = false, maxStride = 1
255
+ const walk = (node) => {
256
+ if (!Array.isArray(node)) return
257
+ const op = node[0]
258
+ if (op === '=>') return
259
+ if (op === '[]' && node.length === 3) {
260
+ const idx = node[2]
261
+ if (idx === iv || (Array.isArray(idx) && refsName(idx, iv, REFS_IN_EXPR))) indexed = true
262
+ const stride = indexStrideOnVar(idx, iv)
263
+ if (stride > maxStride) maxStride = stride
264
+ }
265
+ if (SIMD_REDUCE_OPS.has(op) && typeof node[1] === 'string' && node[1] !== iv) carried = true
266
+ if (op === '=' && typeof node[1] === 'string' && node[1] !== iv) {
267
+ const rhs = node[2]
268
+ if (rhs === node[1] || (Array.isArray(rhs) && refsName(rhs, node[1], REFS_IN_EXPR))) carried = true
269
+ }
270
+ for (let i = 1; i < node.length; i++) walk(node[i])
271
+ }
272
+ walk(body)
273
+ return { indexed, carried, maxStride }
274
+ }
275
+
276
+ function adviseSimdLoops() {
277
+ if (!ctx.warnings) return
278
+ if (ctx.transform.optimize?.vectorizeLaneLocal === false) return
279
+
280
+ for (const func of ctx.func.list) {
281
+ if (func.raw || !func.body) continue
282
+ const fn = func.name
283
+
284
+ const walk = (node) => {
285
+ if (!Array.isArray(node)) return
286
+ if (node[0] === 'for' && node.length >= 5) {
287
+ const [, , , step, body] = node
288
+ const iv = forInductionVar(step)
289
+ if (iv) {
290
+ const { indexed, carried, maxStride } = simdLoopIssues(body, iv)
291
+ if (indexed && carried) {
292
+ warn('simd-loop-carried',
293
+ `'${fn}' loop carries a scalar updated each iteration — SIMD vectorization skipped; split the reduction or use a separate accumulator`,
294
+ { fn }, node.loc)
295
+ }
296
+ if (indexed && maxStride > 1) {
297
+ warn('simd-aos-stride',
298
+ `'${fn}' indexed access stride ${maxStride} on loop counter — split into one typed array per field for SIMD (array-of-structures blocks vectorization)`,
299
+ { fn }, node.loc)
300
+ }
301
+ }
302
+ }
303
+ for (let i = 1; i < node.length; i++) walk(node[i])
304
+ }
305
+ walk(func.body)
306
+ }
307
+ }
308
+
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
+
363
+ export function adviseProgram(programFacts) {
364
+ adviseHeapGrowth()
365
+ adviseSetMapIterationOrder()
366
+ if (programFacts) adviseJsstringCarrier(programFacts.paramReps, programFacts.valueUsed)
367
+ adviseSimdLoops()
368
+ adviseGenericDispatch()
369
+ }
370
+