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.
- package/README.md +99 -72
- package/bench/README.md +253 -100
- package/bench/bench.svg +58 -81
- package/cli.js +85 -12
- package/dist/interop.js +1 -0
- package/dist/jz.js +6196 -0
- package/index.d.ts +126 -0
- package/index.js +222 -35
- package/interop.js +240 -141
- package/layout.js +34 -18
- package/module/array.js +99 -111
- package/module/collection.js +124 -30
- package/module/console.js +1 -1
- package/module/core.js +163 -19
- package/module/json.js +3 -3
- package/module/math.js +162 -3
- package/module/number.js +268 -13
- package/module/object.js +37 -5
- package/module/regex.js +8 -7
- package/module/simd.js +37 -5
- package/module/string.js +203 -168
- package/module/typedarray.js +565 -112
- package/package.json +26 -7
- package/src/abi/string.js +29 -29
- package/src/ast.js +19 -2
- package/src/autoload.js +3 -0
- package/src/compile/analyze-scans.js +174 -11
- package/src/compile/analyze.js +101 -4
- package/src/compile/cse-load.js +200 -0
- package/src/compile/emit-assign.js +82 -20
- package/src/compile/emit.js +592 -51
- package/src/compile/index.js +504 -89
- package/src/compile/infer.js +36 -1
- package/src/compile/loop-divmod.js +109 -0
- package/src/compile/loop-model.js +91 -0
- package/src/compile/loop-square.js +102 -0
- package/src/compile/narrow.js +275 -41
- package/src/compile/peel-stencil.js +215 -0
- package/src/compile/plan/advise.js +55 -1
- package/src/compile/plan/common.js +29 -0
- package/src/compile/plan/index.js +8 -1
- package/src/compile/plan/inline.js +180 -22
- package/src/compile/plan/literals.js +313 -24
- package/src/compile/plan/scope.js +115 -39
- package/src/compile/program-facts.js +21 -2
- package/src/ctx.js +96 -19
- package/src/helper-counters.js +137 -0
- package/src/ir.js +157 -20
- package/src/kind-traits.js +34 -3
- package/src/kind.js +79 -4
- package/src/op-policy.js +5 -2
- package/src/ops.js +119 -0
- package/src/optimize/index.js +1274 -151
- package/src/optimize/recurse.js +182 -0
- package/src/optimize/vectorize.js +4187 -253
- package/src/prepare/index.js +75 -14
- package/src/prepare/lift-iife.js +149 -0
- package/src/reps.js +6 -2
- package/src/static.js +9 -0
- package/src/type.js +63 -51
- package/src/wat/assemble.js +286 -21
- package/src/widen.js +21 -0
- 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
|