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
package/src/compile/infer.js
CHANGED
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
import { ctx } from '../ctx.js'
|
|
47
47
|
import { collectParamNames, ASSIGN_OPS } from '../ast.js'
|
|
48
48
|
import { analyzeValTypes, analyzeIntCertain } from './analyze.js'
|
|
49
|
-
import { staticObjectProps } from '../static.js'
|
|
49
|
+
import { staticObjectProps, staticArrayElems } from '../static.js'
|
|
50
50
|
import { typedElemCtor } from '../type.js'
|
|
51
51
|
import { ctorFromElemAux } from '../../layout.js'
|
|
52
52
|
import { shapeOfObjectLiteralAst, valTypeOf } from '../kind.js'
|
|
@@ -341,6 +341,41 @@ export function recordGlobalRep(name, expr) {
|
|
|
341
341
|
}
|
|
342
342
|
const ctor = typedElemCtor(expr)
|
|
343
343
|
if (ctor) (ctx.scope.globalTypedElem ||= new Map()).set(name, ctor)
|
|
344
|
+
// Module-level const array literal with a uniform element val-type (e.g. a numeric
|
|
345
|
+
// table `const FREQS = [261.63, …]`): record it so `FREQS[i]` reads in any using
|
|
346
|
+
// function are typed (NUMBER) rather than untyped. Without this an untyped element
|
|
347
|
+
// read makes `s += FREQS[i]` take the polymorphic +/ToString path — dragging the
|
|
348
|
+
// entire string runtime (~5 kB) into a kernel that uses no strings. A function-local
|
|
349
|
+
// array gets this from analyzeValTypes; a module-level one is invisible to the using
|
|
350
|
+
// function's body walk, so capture it here. Soundness for a later `FREQS[i]=…` is the
|
|
351
|
+
// read-site dynWriteVars guard in valTypeOf (kind.js) — this is just the literal fact.
|
|
352
|
+
if (vt === VAL.ARRAY) {
|
|
353
|
+
const elems = staticArrayElems(expr)
|
|
354
|
+
if (elems && elems.length && elems.every(e => e != null)) {
|
|
355
|
+
let common = valTypeOf(elems[0])
|
|
356
|
+
for (let k = 1; k < elems.length && common != null; k++)
|
|
357
|
+
if (valTypeOf(elems[k]) !== common) common = null
|
|
358
|
+
if (common != null) updateGlobalRep(name, { arrayElemValType: common })
|
|
359
|
+
// Array-of-arrays numeric table (`const C = [[0,4,7], …]`): also record the
|
|
360
|
+
// nested element kind so `C[i][j]` (and `ch = C[i]; ch[j]`) reads stay typed —
|
|
361
|
+
// the same string-runtime drop as the flat case, one level down. Single-level
|
|
362
|
+
// (mirrors analyzeValTypes' local arrElemElemValTypes); deeper nesting falls back.
|
|
363
|
+
if (common === VAL.ARRAY) {
|
|
364
|
+
let nested = null, seen = false, ok = true
|
|
365
|
+
for (const el of elems) {
|
|
366
|
+
const inner = staticArrayElems(el)
|
|
367
|
+
if (!inner || !inner.length || !inner.every(e => e != null)) { ok = false; break }
|
|
368
|
+
for (const ie of inner) {
|
|
369
|
+
const ivt = valTypeOf(ie)
|
|
370
|
+
if (!seen) { nested = ivt; seen = true }
|
|
371
|
+
else if (ivt !== nested) { ok = false; break }
|
|
372
|
+
}
|
|
373
|
+
if (!ok) break
|
|
374
|
+
}
|
|
375
|
+
if (ok && nested != null) updateGlobalRep(name, { arrayElemElemValType: nested })
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
}
|
|
344
379
|
// Static-shape capture for module-level object literals — lets `{ ...G.path }`
|
|
345
380
|
// resolve its source schema by walking the global rep's shape tree at the
|
|
346
381
|
// spread site (see shape walk in analyze.js / resolveSchema in object.js).
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
// Loop induction strength-reduction for `i % w` and `(i / w) | 0`.
|
|
2
|
+
//
|
|
3
|
+
// JS `%` and `/` are float ops; `i % w` with a RUNTIME divisor lowers to f64
|
|
4
|
+
// (i % 0 is NaN, so it can't be soundly typed i32 — see type.js exprType `%`).
|
|
5
|
+
// That makes the column `x = i % w` an f64 local, which cascades: every dependent
|
|
6
|
+
// neighbour index becomes f64 and each typed-array access pays an f64→i32 convert.
|
|
7
|
+
// Measured ~5× slower than V8, which speculates the divisor non-zero and uses i32.
|
|
8
|
+
//
|
|
9
|
+
// This pass replaces the per-iteration division with incremental i32 counters in a
|
|
10
|
+
// unit-stride loop: the column `cx` increments by 1 and wraps to 0 at `w`, bumping
|
|
11
|
+
// the row `cy`. No division survives in the body, so the whole index chain stays
|
|
12
|
+
// i32. Fully sound — counters are pure increment (the divisor-zero question never
|
|
13
|
+
// arises), and if `w == 0` the loop's `i < w*h` guard never admits an iteration, so
|
|
14
|
+
// the one-time seed `(i%w)|0` (NaN→0) is unused.
|
|
15
|
+
//
|
|
16
|
+
// Recognized (post-prepare AST): a `while` whose body increments one IV `i` by +1
|
|
17
|
+
// exactly once, reads `['%', i, w]` and/or `['|', ['/', i, w], LIT0]` with `w`
|
|
18
|
+
// loop-invariant, and has no `continue` / closure-capture of i,w (which could
|
|
19
|
+
// desync the counters). Number literals are sparse-array holes `[, v]` (n[0] is the
|
|
20
|
+
// hole = undefined), so literal tests use `== null`; created literals are bare numbers.
|
|
21
|
+
|
|
22
|
+
import { findMutations } from './analyze-scans.js'
|
|
23
|
+
import { litN, unitIncVar, normalizeLoop, closureMutatedVars, rewriteBlocks, freshLoopId } from './loop-model.js'
|
|
24
|
+
|
|
25
|
+
const isMod = (n, i, w) => Array.isArray(n) && n[0] === '%' && n[1] === i && n[2] === w
|
|
26
|
+
const isFloorDiv = (n, i, w) =>
|
|
27
|
+
Array.isArray(n) && n[0] === '|' && litN(n[2], 0) &&
|
|
28
|
+
Array.isArray(n[1]) && n[1][0] === '/' && n[1][1] === i && n[1][2] === w
|
|
29
|
+
|
|
30
|
+
const usesPattern = (n, i, w, pred) => Array.isArray(n) && (pred(n, i, w) || n.some(c => usesPattern(c, i, w, pred)))
|
|
31
|
+
const replace = (n, i, w, cx, cy) =>
|
|
32
|
+
!Array.isArray(n) ? n : isMod(n, i, w) ? cx : isFloorDiv(n, i, w) ? cy : n.map(c => replace(c, i, w, cx, cy))
|
|
33
|
+
// a `continue` that targets THIS loop (not one nested inside) — would skip the increment
|
|
34
|
+
const hasOuterContinue = (n) => Array.isArray(n) &&
|
|
35
|
+
(n[0] === 'continue' || (n[0] !== 'while' && n[0] !== 'for' && n[0] !== 'do' && n[0] !== '=>' && n.some(hasOuterContinue)))
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
// Try to strength-reduce one `while` statement. Returns [seed, loop] or null. `cm` is
|
|
39
|
+
// the function's closure-mutated-vars set (an IV/divisor in it is unsafe).
|
|
40
|
+
function tryReduce(stmt, cm) {
|
|
41
|
+
const L = normalizeLoop(stmt)
|
|
42
|
+
if (!L || L.kind !== 'while') return null
|
|
43
|
+
const cond = L.cond, lbody = L.body
|
|
44
|
+
if (!Array.isArray(lbody) || lbody[0] !== ';') return null
|
|
45
|
+
|
|
46
|
+
// exactly one IV incremented by +1
|
|
47
|
+
let iv = null, ivIdx = -1
|
|
48
|
+
for (let k = 1; k < lbody.length; k++) {
|
|
49
|
+
const v = unitIncVar(lbody[k])
|
|
50
|
+
if (v) { if (iv) return null; iv = v; ivIdx = k }
|
|
51
|
+
}
|
|
52
|
+
if (!iv) return null
|
|
53
|
+
|
|
54
|
+
// a single invariant divisor `w` (`i % w` / `(i/w)|0` all share it)
|
|
55
|
+
let w = null
|
|
56
|
+
const findW = (n) => {
|
|
57
|
+
if (!Array.isArray(n)) return
|
|
58
|
+
const d = n[0] === '%' && n[1] === iv ? n[2]
|
|
59
|
+
: (n[0] === '|' && litN(n[2], 0) && Array.isArray(n[1]) && n[1][0] === '/' && n[1][1] === iv) ? n[1][2] : null
|
|
60
|
+
if (typeof d === 'string') { if (w == null) w = d; else if (w !== d) w = false }
|
|
61
|
+
n.forEach(findW)
|
|
62
|
+
}
|
|
63
|
+
findW(lbody)
|
|
64
|
+
if (!w || w === iv) return null
|
|
65
|
+
|
|
66
|
+
const usesMod = usesPattern(lbody, iv, w, isMod)
|
|
67
|
+
const usesDiv = usesPattern(lbody, iv, w, isFloorDiv)
|
|
68
|
+
if (!usesMod && !usesDiv) return null
|
|
69
|
+
|
|
70
|
+
// soundness: w invariant, iv written ONLY by the increment, no continue/closure capture
|
|
71
|
+
const wMut = new Set(); findMutations(lbody, new Set([w]), wMut)
|
|
72
|
+
if (wMut.has(w)) return null
|
|
73
|
+
const ivMut = new Set()
|
|
74
|
+
findMutations([';', ...lbody.slice(1).filter((_, k) => k !== ivIdx - 1)], new Set([iv]), ivMut)
|
|
75
|
+
if (ivMut.has(iv)) return null
|
|
76
|
+
if (hasOuterContinue(lbody)) return null
|
|
77
|
+
if (cm.has(iv) || cm.has(w)) return null // IV/divisor mutable via a closure call
|
|
78
|
+
|
|
79
|
+
const id = freshLoopId()
|
|
80
|
+
const cx = `__lsrx${id}`, cy = `__lsry${id}`
|
|
81
|
+
// seed (inside the w>0 branch): cx = (i%w)|0, cy = (i/w)|0 — one-time, i32 via |0
|
|
82
|
+
const seedDecls = [['=', cx, ['|', ['%', iv, w], 0]]]
|
|
83
|
+
if (usesDiv) seedDecls.push(['=', cy, ['|', ['/', iv, w], 0]])
|
|
84
|
+
const seed = ['let', ...seedDecls]
|
|
85
|
+
|
|
86
|
+
// transformed body: replace patterns, inject `cx++; if(cx>=w){cx=0; cy++}` after the increment
|
|
87
|
+
const newBody = [';']
|
|
88
|
+
for (let k = 1; k < lbody.length; k++) {
|
|
89
|
+
newBody.push(replace(lbody[k], iv, w, cx, cy))
|
|
90
|
+
if (k === ivIdx) {
|
|
91
|
+
newBody.push(['=', cx, ['+', cx, 1]])
|
|
92
|
+
if (usesDiv) newBody.push(['=', cy, ['+', cy, ['?:', ['>=', cx, w], 1, 0]]])
|
|
93
|
+
newBody.push(['=', cx, ['?:', ['>=', cx, w], 0, cx]])
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
const fast = ['while', replace(cond, iv, w, cx, cy), newBody]
|
|
97
|
+
// The counters track i%w only when w>0 AND i>=0: w<=0 gives NaN / a negative-divisor
|
|
98
|
+
// modulo, and i<0 makes i%w negative (JS modulo takes the dividend's sign) — neither
|
|
99
|
+
// follows the 0..w-1 +1-wrap the counters model. A one-time `w>0 && i>=0` guard (i is
|
|
100
|
+
// the IV's entry value; it only increments, so it stays ≥0) keeps the fast i32 path
|
|
101
|
+
// for the universal positive-dimension, forward-counting case and falls back to the
|
|
102
|
+
// unmodified loop otherwise — sound for any w, any start.
|
|
103
|
+
return [['if', ['&&', ['>', w, 0], ['>=', iv, 0]], ['{}', [';', seed, fast]], stmt]]
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function strengthReduceLoopDivMod(body) {
|
|
107
|
+
const cm = closureMutatedVars(body)
|
|
108
|
+
return rewriteBlocks(body, stmt => tryReduce(stmt, cm))
|
|
109
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
// Shared AST-level loop primitives for the per-function loop transforms
|
|
2
|
+
// (loop-divmod, loop-square, peel-stencil). Operates on the post-prepare AST.
|
|
3
|
+
//
|
|
4
|
+
// Each of those passes recognizes one narrow loop idiom and rewrites it, but they had
|
|
5
|
+
// re-derived the same building blocks verbatim: the `[, v]` number-literal-hole
|
|
6
|
+
// recognizer, the "+1 induction variable" matcher, the closure-mutated-variable
|
|
7
|
+
// analysis (a var assigned inside any `=>` may be mutated by a call in the loop, so it
|
|
8
|
+
// is unsafe as an IV/bound/divisor), the while/for structural normalizer, and the
|
|
9
|
+
// post-order block walk that applies a per-statement rewrite. One home for all of them
|
|
10
|
+
// — adding the next loop transform is then one recognizer over these, not a fourth copy.
|
|
11
|
+
|
|
12
|
+
import { ASSIGN_OPS } from '../ast.js'
|
|
13
|
+
import { ctx } from '../ctx.js'
|
|
14
|
+
|
|
15
|
+
// Fresh id for a loop transform's generated locals (`__lsrx<id>`, `__pks<id>`, …). Backed by
|
|
16
|
+
// a per-compile counter (reset in ctx.reset), NOT a module-global: a module-`let` counter
|
|
17
|
+
// grows unbounded across a long-lived host and makes compile(P) non-deterministic — its output
|
|
18
|
+
// names depend on how many programs were compiled before it. Distinct prefixes keep the shared
|
|
19
|
+
// id space collision-free across transforms.
|
|
20
|
+
export const freshLoopId = () => ctx.transform.loopXformId++
|
|
21
|
+
|
|
22
|
+
// Post-prepare number literals are sparse-array holes `[<hole>, v]` (length 2, the op
|
|
23
|
+
// slot `n[0]` is the elided hole == null). `litVal` returns the numeric value or null;
|
|
24
|
+
// `litN(n, k)` tests for the exact literal `k`.
|
|
25
|
+
export const litVal = (n) => Array.isArray(n) && n.length === 2 && n[0] == null && typeof n[1] === 'number' ? n[1] : null
|
|
26
|
+
export const litN = (n, k) => Array.isArray(n) && n.length === 2 && n[0] == null && n[1] === k
|
|
27
|
+
|
|
28
|
+
// The induction variable a statement increments by exactly +1, else null. Covers
|
|
29
|
+
// `i++` (post-inc desugars to `(++i) - 1`), `++i`, `i += 1`, `i = i + 1` / `i = 1 + i`.
|
|
30
|
+
export function unitIncVar(stmt) {
|
|
31
|
+
if (!Array.isArray(stmt)) return null
|
|
32
|
+
let inc = stmt
|
|
33
|
+
if (stmt[0] === '-' && litN(stmt[2], 1) && Array.isArray(stmt[1]) && stmt[1][0] === '++') inc = stmt[1]
|
|
34
|
+
if (inc[0] === '++' && typeof inc[1] === 'string') return inc[1]
|
|
35
|
+
if (stmt[0] === '+=' && typeof stmt[1] === 'string' && litN(stmt[2], 1)) return stmt[1]
|
|
36
|
+
if (stmt[0] === '=' && typeof stmt[1] === 'string' && Array.isArray(stmt[2]) && stmt[2][0] === '+') {
|
|
37
|
+
const [, a, b] = stmt[2]
|
|
38
|
+
if (a === stmt[1] && litN(b, 1)) return stmt[1]
|
|
39
|
+
if (b === stmt[1] && litN(a, 1)) return stmt[1]
|
|
40
|
+
}
|
|
41
|
+
return null
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Normalize a `while` / flat `for` (`['for', init, cond, update, body]`) into a common
|
|
45
|
+
// shape, else null. `init`/`step` are null for a while; a `for` with fewer than the 5
|
|
46
|
+
// flat slots (no body) is not a loop here.
|
|
47
|
+
export function normalizeLoop(stmt) {
|
|
48
|
+
if (!Array.isArray(stmt)) return null
|
|
49
|
+
if (stmt[0] === 'while') return { kind: 'while', init: null, cond: stmt[1], step: null, body: stmt[2] }
|
|
50
|
+
if (stmt[0] === 'for' && stmt.length >= 5) return { kind: 'for', init: stmt[1], cond: stmt[2], step: stmt[3], body: stmt[4] }
|
|
51
|
+
return null
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Variables assigned anywhere inside a closure (`=>`) in `body`. A call in the loop can
|
|
55
|
+
// mutate such a var even though a direct-write scan (findMutations) misses it (the
|
|
56
|
+
// closure may be defined outside the loop), so an IV / bound / divisor in this set is
|
|
57
|
+
// unsafe to strength-reduce. (ASSIGN_OPS plus the `++`/`--` updates.)
|
|
58
|
+
export function closureMutatedVars(body) {
|
|
59
|
+
const out = new Set()
|
|
60
|
+
const collect = (n) => {
|
|
61
|
+
if (!Array.isArray(n)) return
|
|
62
|
+
if (typeof n[1] === 'string' && (ASSIGN_OPS.has(n[0]) || n[0] === '++' || n[0] === '--')) out.add(n[1])
|
|
63
|
+
n.forEach(collect)
|
|
64
|
+
}
|
|
65
|
+
const walk = (n) => {
|
|
66
|
+
if (!Array.isArray(n)) return
|
|
67
|
+
if (n[0] === '=>') collect(n)
|
|
68
|
+
n.forEach(walk)
|
|
69
|
+
}
|
|
70
|
+
walk(body)
|
|
71
|
+
return out
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Post-order block rewriter: walk `body`; for every `;`-sequence, apply `tryStmt` to
|
|
75
|
+
// each statement. A truthy result is an ARRAY of replacement statements spliced in
|
|
76
|
+
// place; a falsy result keeps the statement unchanged. Children are rewritten first, so
|
|
77
|
+
// a nested loop is transformed before the block that encloses it.
|
|
78
|
+
export function rewriteBlocks(body, tryStmt) {
|
|
79
|
+
const walk = (node) => {
|
|
80
|
+
if (!Array.isArray(node)) return node
|
|
81
|
+
const n = node.map(walk)
|
|
82
|
+
if (n[0] !== ';') return n
|
|
83
|
+
const out = [';']
|
|
84
|
+
for (let k = 1; k < n.length; k++) {
|
|
85
|
+
const r = tryStmt(n[k])
|
|
86
|
+
if (r) out.push(...r); else out.push(n[k])
|
|
87
|
+
}
|
|
88
|
+
return out
|
|
89
|
+
}
|
|
90
|
+
return walk(body)
|
|
91
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
// Bounded-square narrowing: carry `i*i` as i32 inside a loop guarded by `i*i < CONST`.
|
|
2
|
+
//
|
|
3
|
+
// A PRODUCT `i*i` is f64 in unhinted JS — the integer-overflow contract: a product can
|
|
4
|
+
// exceed 2³¹, where JS keeps a Number, so jz can't blindly use i32.mul (it would wrap).
|
|
5
|
+
// In a Sieve-of-Eratosthenes `for(i=2; i*i<LIMIT; i++) for(j=i*i; j<LIMIT; j+=i) …`, that
|
|
6
|
+
// makes the outer bound, the inner counter `j`'s init, and the whole index chain f64 —
|
|
7
|
+
// each typed-array access then pays an f64→i32 convert.
|
|
8
|
+
//
|
|
9
|
+
// But when the loop GUARD is `i*i < CONST` with CONST a compile-time constant ≤ 2³⁰, the
|
|
10
|
+
// counter is i ≤ ⌈√CONST⌉ ≤ 2¹⁵ throughout the body (the loop is still running, and i is
|
|
11
|
+
// only incremented by +1), so EVERY `i*i` there is < 2³⁰ < 2³¹ and `Math.imul(i,i) == i*i`
|
|
12
|
+
// exactly. The exit overshoot (the first i with i*i ≥ CONST, evaluated in the condition)
|
|
13
|
+
// is ≤ CONST + 2√CONST+1 < 2³¹ for CONST ≤ 2³⁰, so the condition narrows soundly too. The
|
|
14
|
+
// hard cap is 2³⁰ — well under the 2³¹ overflow point, with margin for the overshoot.
|
|
15
|
+
//
|
|
16
|
+
// So we rewrite those `i*i` to `Math.imul(i,i)` (jz's i32 multiply): semantically identical
|
|
17
|
+
// in the proven range, and it lets the EXISTING i32 machinery carry the index chain as i32
|
|
18
|
+
// — the inner `j` (init now i32, step `j+i` i32, bound `j<CONST` i32) cascades on its own.
|
|
19
|
+
//
|
|
20
|
+
// Sound iff: the loop condition is `(i*i) </≤ CONST` (CONST ≤ 2³⁰), and the IV `i` is
|
|
21
|
+
// incremented by +1 and NOT otherwise mutated (so within a body iteration i ∈ {entry, +1},
|
|
22
|
+
// both squares < 2³¹). Mirrors strength-reduce-divmod's structure (post-prepare `while`).
|
|
23
|
+
|
|
24
|
+
import { findMutations } from './analyze-scans.js'
|
|
25
|
+
import { includeMods } from '../autoload.js'
|
|
26
|
+
import { ctx } from '../ctx.js'
|
|
27
|
+
import { litVal, unitIncVar, normalizeLoop, closureMutatedVars, rewriteBlocks } from './loop-model.js'
|
|
28
|
+
|
|
29
|
+
const SQUARE_BOUND_MAX = 2 ** 30
|
|
30
|
+
// The constant numeric value of a bound: a literal, OR a module const folded to an int
|
|
31
|
+
// (`const LIMIT = 1<<20` → ctx.scope.constInts.get('LIMIT') = 1048576 — the bench form).
|
|
32
|
+
const boundVal = (n) => {
|
|
33
|
+
const lit = litVal(n)
|
|
34
|
+
if (lit != null) return lit
|
|
35
|
+
if (typeof n === 'string') { const v = ctx.scope.constInts?.get(n); return typeof v === 'number' ? v : null }
|
|
36
|
+
return null
|
|
37
|
+
}
|
|
38
|
+
// `i * i` — the IV squared.
|
|
39
|
+
const isSquare = (n, iv) => Array.isArray(n) && n[0] === '*' && n[1] === iv && n[2] === iv && typeof iv === 'string'
|
|
40
|
+
// Math.imul(i, i) in CANONICAL post-prepare form: prepare resolves `Math.imul` → the string
|
|
41
|
+
// ref `'math.imul'`, so the call is `['()', 'math.imul', [',', i, i]]`. math.imul emits a
|
|
42
|
+
// primitive `i32.mul` (no stdlib helper / module include needed).
|
|
43
|
+
const imulOf = (iv) => ['()', 'math.imul', [',', iv, iv]]
|
|
44
|
+
|
|
45
|
+
// The IV of a `(i*i) </≤ CONST` (or mirrored `CONST >/≥ (i*i)`) guard, CONST ≤ 2³⁰, else null.
|
|
46
|
+
function boundedSquareIV(cond) {
|
|
47
|
+
if (!Array.isArray(cond)) return null
|
|
48
|
+
let prod, bound
|
|
49
|
+
if (cond[0] === '<' || cond[0] === '<=') { prod = cond[1]; bound = cond[2] }
|
|
50
|
+
else if (cond[0] === '>' || cond[0] === '>=') { prod = cond[2]; bound = cond[1] }
|
|
51
|
+
else return null
|
|
52
|
+
if (!isSquare(prod, prod && prod[1])) return null
|
|
53
|
+
const b = boundVal(bound)
|
|
54
|
+
if (b == null || b < 0 || b > SQUARE_BOUND_MAX) return null
|
|
55
|
+
return prod[1]
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Narrow a `for`/`while` whose guard is `(i*i) </≤ CONST` (CONST ≤ 2³⁰) and whose IV `i` is
|
|
59
|
+
// incremented by +1 and not otherwise mutated — then within any body iteration i ∈ {entry,
|
|
60
|
+
// entry+1}, entry ≤ ⌈√CONST⌉ ≤ 2¹⁵, so every `i*i` (and the exit overshoot) is < 2³¹ and
|
|
61
|
+
// Math.imul(i,i) == i*i. Rewrites those products; the dependent counter chain cascades to i32.
|
|
62
|
+
// `cm` is the function's closure-mutated-vars set (an IV in it has an unprovable entry value).
|
|
63
|
+
function tryNarrow(stmt, cm) {
|
|
64
|
+
const L = normalizeLoop(stmt)
|
|
65
|
+
if (!L) return null
|
|
66
|
+
const { kind, cond, step, body } = L, isFor = kind === 'for'
|
|
67
|
+
|
|
68
|
+
const iv = boundedSquareIV(cond)
|
|
69
|
+
if (!iv) return null
|
|
70
|
+
if (cm.has(iv)) return null // mutable via a closure call — entry value unprovable
|
|
71
|
+
|
|
72
|
+
if (isFor) {
|
|
73
|
+
// The `for` update is the IV's sole, +1 mutation; the body must not reassign i.
|
|
74
|
+
if (unitIncVar(step) !== iv) return null
|
|
75
|
+
const ivMut = new Set(); findMutations(body, new Set([iv]), ivMut)
|
|
76
|
+
if (ivMut.has(iv)) return null
|
|
77
|
+
} else {
|
|
78
|
+
// `while`: the increment lives in the body (exactly one +1; nothing else mutates i).
|
|
79
|
+
if (!Array.isArray(body) || body[0] !== ';') return null
|
|
80
|
+
let ivIdx = -1
|
|
81
|
+
for (let k = 1; k < body.length; k++) if (unitIncVar(body[k]) === iv) { if (ivIdx >= 0) return null; ivIdx = k }
|
|
82
|
+
if (ivIdx < 0) return null
|
|
83
|
+
const ivMut = new Set()
|
|
84
|
+
findMutations([';', ...body.slice(1).filter((_, k) => k !== ivIdx - 1)], new Set([iv]), ivMut)
|
|
85
|
+
if (ivMut.has(iv)) return null
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// We're injecting `math.imul` after prepare's auto-import step, so ensure the math module
|
|
89
|
+
// (which registers the `math.imul` → i32.mul primitive emitter) is included.
|
|
90
|
+
includeMods('math')
|
|
91
|
+
// Rewrite every `i*i` in the condition + body to Math.imul(i,i) (NOT init/update — they're
|
|
92
|
+
// `i=2` / `i++`). The inner counter whose init this feeds cascades to i32 on its own.
|
|
93
|
+
const rw = (n) => !Array.isArray(n) ? n : isSquare(n, iv) ? imulOf(iv) : n.map(rw)
|
|
94
|
+
return [isFor
|
|
95
|
+
? ['for', L.init, rw(cond), step, rw(body)]
|
|
96
|
+
: ['while', rw(cond), rw(body)]]
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function narrowBoundedSquare(body) {
|
|
100
|
+
const cm = closureMutatedVars(body)
|
|
101
|
+
return rewriteBlocks(body, stmt => tryNarrow(stmt, cm))
|
|
102
|
+
}
|