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.
- package/README.md +315 -318
- package/bench/README.md +369 -0
- package/bench/bench.svg +102 -0
- package/cli.js +104 -30
- package/dist/interop.js +1 -0
- package/dist/jz.js +6024 -0
- package/index.d.ts +126 -0
- package/index.js +362 -84
- package/interop.js +159 -186
- package/jz.svg +5 -0
- package/jzify/arguments.js +97 -0
- package/jzify/bundler.js +382 -0
- package/jzify/classes.js +328 -0
- package/jzify/hoist-vars.js +177 -0
- package/jzify/index.js +51 -0
- package/jzify/names.js +37 -0
- package/jzify/switch.js +106 -0
- package/jzify/transform.js +349 -0
- package/layout.js +184 -0
- package/module/array.js +377 -176
- package/module/collection.js +639 -145
- package/module/console.js +55 -43
- package/module/core.js +264 -153
- package/module/date.js +15 -3
- package/module/function.js +73 -6
- package/module/index.js +2 -1
- package/module/json.js +226 -61
- package/module/math.js +551 -187
- package/module/number.js +327 -60
- package/module/object.js +474 -184
- package/module/regex.js +255 -25
- package/module/schema.js +24 -6
- package/module/simd.js +117 -0
- package/module/string.js +621 -226
- package/module/symbol.js +1 -1
- package/module/timer.js +9 -14
- package/module/typedarray.js +459 -73
- package/package.json +58 -14
- package/src/abi/index.js +39 -23
- package/src/abi/string.js +38 -41
- package/src/ast.js +460 -0
- package/src/autoload.js +29 -24
- package/src/bridge.js +111 -0
- package/src/compile/analyze-scans.js +824 -0
- package/src/compile/analyze.js +1600 -0
- package/src/compile/emit-assign.js +411 -0
- package/src/compile/emit.js +3512 -0
- package/src/compile/flow-types.js +103 -0
- package/src/{compile.js → compile/index.js} +778 -128
- package/src/{infer.js → compile/infer.js} +62 -98
- package/src/compile/loop-divmod.js +155 -0
- package/src/{narrow.js → compile/narrow.js} +409 -106
- package/src/compile/peel-stencil.js +261 -0
- package/src/compile/plan/advise.js +370 -0
- package/src/compile/plan/common.js +150 -0
- package/src/compile/plan/index.js +122 -0
- package/src/compile/plan/inline.js +682 -0
- package/src/compile/plan/literals.js +1199 -0
- package/src/compile/plan/loops.js +472 -0
- package/src/compile/plan/scope.js +649 -0
- package/src/compile/program-facts.js +423 -0
- package/src/ctx.js +220 -64
- package/src/ir.js +589 -172
- package/src/kind-traits.js +132 -0
- package/src/kind.js +524 -0
- package/src/op-policy.js +57 -0
- package/src/{optimize.js → optimize/index.js} +1432 -473
- package/src/optimize/vectorize.js +4660 -0
- package/src/param-reps.js +65 -0
- package/src/parse.js +44 -0
- package/src/{prepare.js → prepare/index.js} +649 -205
- package/src/prepare/lift-iife.js +149 -0
- package/src/reps.js +116 -0
- package/src/resolve.js +12 -3
- package/src/static.js +208 -0
- package/src/type.js +651 -0
- package/src/{assemble.js → wat/assemble.js} +275 -55
- package/src/wat/optimize.js +3938 -0
- package/transform.js +21 -0
- package/wasi.js +47 -5
- package/src/analyze.js +0 -3818
- package/src/emit.js +0 -3040
- package/src/jzify.js +0 -1580
- package/src/plan.js +0 -2132
- package/src/vectorize.js +0 -1088
- /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
|
+
|