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
|
@@ -43,12 +43,15 @@
|
|
|
43
43
|
* @module src/infer
|
|
44
44
|
*/
|
|
45
45
|
|
|
46
|
-
import { ctx } from '
|
|
47
|
-
import {
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
} from '
|
|
46
|
+
import { ctx } from '../ctx.js'
|
|
47
|
+
import { collectParamNames, ASSIGN_OPS } from '../ast.js'
|
|
48
|
+
import { analyzeValTypes, analyzeIntCertain } from './analyze.js'
|
|
49
|
+
import { staticObjectProps, staticArrayElems } from '../static.js'
|
|
50
|
+
import { typedElemCtor } from '../type.js'
|
|
51
|
+
import { ctorFromElemAux } from '../../layout.js'
|
|
52
|
+
import { shapeOfObjectLiteralAst, valTypeOf } from '../kind.js'
|
|
53
|
+
import { includeForStringValue } from '../autoload.js'
|
|
54
|
+
import { VAL, updateRep, updateGlobalRep } from '../reps.js'
|
|
52
55
|
|
|
53
56
|
// === typeof predicate helper ==============================================
|
|
54
57
|
//
|
|
@@ -63,7 +66,7 @@ import {
|
|
|
63
66
|
/** Match a `typeof name <op> lit` predicate. Returns `{ name, code, eq }` —
|
|
64
67
|
* `name` is the typeof's operand binding, `code` is either the raw type
|
|
65
68
|
* string ('string'|'number'|'function'|…) or the prepare-normalized typeof
|
|
66
|
-
* code (
|
|
69
|
+
* code (TYPEOF in ast.js), and `eq` is true
|
|
67
70
|
* for `==`/`===` (false for `!=`/`!==`). Returns null when the node isn't a
|
|
68
71
|
* typeof predicate. */
|
|
69
72
|
export function typeofPredicate(node) {
|
|
@@ -80,91 +83,10 @@ export function typeofPredicate(node) {
|
|
|
80
83
|
return { name: typeofSide[1], code, eq: op === '==' || op === '===' }
|
|
81
84
|
}
|
|
82
85
|
|
|
83
|
-
// === paramReps lattice
|
|
86
|
+
// === paramReps lattice =====================================================
|
|
84
87
|
//
|
|
85
|
-
//
|
|
86
|
-
//
|
|
87
|
-
// raw observations; these primitives apply them with sticky-null poison
|
|
88
|
-
// semantics: undefined → observe (set); equal → stay; disagreement → null
|
|
89
|
-
// (sticky). null is "no consensus" — readers treat it as missing.
|
|
90
|
-
//
|
|
91
|
-
// Lifecycle phases (chronological — readers should know which fields are
|
|
92
|
-
// valid at which point):
|
|
93
|
-
//
|
|
94
|
-
// 1. prepare ─ no paramReps yet. AST walk collects callSites with raw
|
|
95
|
-
// arg lists; programFacts.paramReps is allocated empty.
|
|
96
|
-
//
|
|
97
|
-
// 2. collectProgramFacts ─ unchanged paramReps; aggregates module-global
|
|
98
|
-
// callSites, valueUsed, hasSchemaLiterals into
|
|
99
|
-
// programFacts. paramReps still empty here.
|
|
100
|
-
//
|
|
101
|
-
// 3. narrowSignatures phase D (call-site lattice) ─
|
|
102
|
-
// runCallsiteLattice merges raw call-site facts via
|
|
103
|
-
// mergeParamFact for fields: val, schemaId, intConst,
|
|
104
|
-
// arrayElemSchema, arrayElemValType, typedCtor, wasm.
|
|
105
|
-
// After D, these may be undefined/null/value.
|
|
106
|
-
// Sticky-null reachable on any field whose call sites
|
|
107
|
-
// disagreed.
|
|
108
|
-
//
|
|
109
|
-
// 4. validateIntConstParams ─ clears intConst on any param whose body
|
|
110
|
-
// contains a write to it (intConst's contract is "no
|
|
111
|
-
// writes after the call").
|
|
112
|
-
//
|
|
113
|
-
// 5. phase E / E2 / E3 (param ABI narrowing) ─
|
|
114
|
-
// applyI32ParamSpecialization sets sig.params[k].type
|
|
115
|
-
// to 'i32' based on rep.wasm consensus. Then
|
|
116
|
-
// applyPointerParamAbi sets rep.ptrKind on
|
|
117
|
-
// consistently-OBJECT/ARRAY/etc params; this prepares
|
|
118
|
-
// the i32-offset ABI for unboxed pointer params.
|
|
119
|
-
//
|
|
120
|
-
// 6. phase F (signature fixpoint) ─ clearStickyNull on val + schemaId,
|
|
121
|
-
// then re-runs D until stable. New evidence from
|
|
122
|
-
// newly-narrowed return types (valResult) can unstick.
|
|
123
|
-
//
|
|
124
|
-
// 7. phase G (narrowReturnArrayElems) ─ propagates Array<T> element
|
|
125
|
-
// facts back through return paths into caller param
|
|
126
|
-
// reps. After G, arrayElemSchema/arrayElemValType
|
|
127
|
-
// reflect the transitive closure.
|
|
128
|
-
//
|
|
129
|
-
// 8. phase H (applyTypedPointerParamAbi) ─ sets ptrKind=TYPED + ptrAux
|
|
130
|
-
// (elem code) for params whose val converged to TYPED.
|
|
131
|
-
// Depends on F having seeded val first.
|
|
132
|
-
//
|
|
133
|
-
// 9. phase I (resetParamWasmFacts + final i32 spec) ─ last WASM-level
|
|
134
|
-
// pass. After H/I, sig.params[k].type and rep.ptrKind
|
|
135
|
-
// are frozen for emit.
|
|
136
|
-
//
|
|
137
|
-
// 10. per-function compile (emit start) ─ each func reads its
|
|
138
|
-
// paramReps[name][k] and folds the consensus facts
|
|
139
|
-
// into ctx.func.localReps via updateRep. Locals and
|
|
140
|
-
// params then share one ValueRep store for the
|
|
141
|
-
// remainder of emit.
|
|
142
|
-
|
|
143
|
-
/** Per-call-site fact merge into a param's ValueRep field. */
|
|
144
|
-
export const mergeParamFact = (rep, key, observed) => {
|
|
145
|
-
if (rep[key] === null) return // sticky poison
|
|
146
|
-
if (observed == null) { rep[key] = null; return } // unknown → poison
|
|
147
|
-
if (rep[key] === undefined) rep[key] = observed
|
|
148
|
-
else if (rep[key] !== observed) rep[key] = null
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
/** Get-or-create per-param rep at (funcName, paramIdx) on a paramReps map. */
|
|
152
|
-
export const ensureParamRep = (paramReps, funcName, k) => {
|
|
153
|
-
let m = paramReps.get(funcName)
|
|
154
|
-
if (!m) { m = new Map(); paramReps.set(funcName, m) }
|
|
155
|
-
let r = m.get(k)
|
|
156
|
-
if (!r) { r = {}; m.set(k, r) }
|
|
157
|
-
return r
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
/** Reset sticky-null on a single field across all params program-wide.
|
|
161
|
-
* Used between fixpoint phases when newly-narrowed facts unblock previously-
|
|
162
|
-
* poisoned observations (e.g. valResult set after first pass). */
|
|
163
|
-
export const clearStickyNull = (paramReps, key) => {
|
|
164
|
-
for (const m of paramReps.values()) for (const r of m.values()) {
|
|
165
|
-
if (r[key] === null) r[key] = undefined
|
|
166
|
-
}
|
|
167
|
-
}
|
|
88
|
+
// Primitives live in src/param-reps.js (cycle-free leaf). Lifecycle phases
|
|
89
|
+
// below document when each field is valid during narrowSignatures.
|
|
168
90
|
|
|
169
91
|
// === Source registry =======================================================
|
|
170
92
|
|
|
@@ -212,7 +134,7 @@ const STRING_ONLY_METHODS = new Set([
|
|
|
212
134
|
'charCodeAt', 'charAt', 'codePointAt', 'startsWith', 'endsWith',
|
|
213
135
|
'toUpperCase', 'toLowerCase', 'toLocaleLowerCase', 'normalize', 'localeCompare',
|
|
214
136
|
'padStart', 'padEnd', 'repeat', 'trimStart', 'trimEnd', 'trim',
|
|
215
|
-
'matchAll', 'match', 'replace', 'replaceAll', 'split',
|
|
137
|
+
'matchAll', 'match', 'replace', 'replaceAll', 'split', 'lastIndexOf',
|
|
216
138
|
])
|
|
217
139
|
const ARRAY_ONLY_POISON = new Set([
|
|
218
140
|
'push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'fill', 'reverse',
|
|
@@ -312,9 +234,6 @@ const isLengthAccess = (n) =>
|
|
|
312
234
|
const isIndexAccess = (n) =>
|
|
313
235
|
Array.isArray(n) && n[0] === '[]' && typeof n[1] === 'string'
|
|
314
236
|
|
|
315
|
-
const isAssignOp = (op) =>
|
|
316
|
-
typeof op === 'string' && (op === '=' || (op.length > 1 && op.endsWith('=') && op !== '==' && op !== '===' && op !== '!=' && op !== '!==' && op !== '<=' && op !== '>='))
|
|
317
|
-
|
|
318
237
|
const isStringLiteralRhs = (rhs) =>
|
|
319
238
|
Array.isArray(rhs) && (rhs[0] === 'str' || (rhs[0] == null && typeof rhs[1] === 'string'))
|
|
320
239
|
|
|
@@ -354,11 +273,11 @@ const notStringEvidence = (body, names) => {
|
|
|
354
273
|
markStringy(node[1])
|
|
355
274
|
}
|
|
356
275
|
// Index write: `xs[i] = v` or compound `xs[i] op= v`.
|
|
357
|
-
if (
|
|
276
|
+
if (ASSIGN_OPS.has(op) && isIndexAccess(node[1]) && scope.has(node[1][1]) && !stringy.has(node[1][1])) {
|
|
358
277
|
writes.add(node[1][1])
|
|
359
278
|
}
|
|
360
279
|
// Length mutation: `xs.length = n`, `xs.length += k`, `xs.length++`.
|
|
361
|
-
if (
|
|
280
|
+
if (ASSIGN_OPS.has(op) && isLengthAccess(node[1]) && scope.has(node[1][1]) && !stringy.has(node[1][1])) {
|
|
362
281
|
writes.add(node[1][1])
|
|
363
282
|
}
|
|
364
283
|
if ((op === '++' || op === '--') && isLengthAccess(node[1]) && scope.has(node[1][1]) && !stringy.has(node[1][1])) {
|
|
@@ -422,11 +341,56 @@ export function recordGlobalRep(name, expr) {
|
|
|
422
341
|
}
|
|
423
342
|
const ctor = typedElemCtor(expr)
|
|
424
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
|
+
}
|
|
425
379
|
// Static-shape capture for module-level object literals — lets `{ ...G.path }`
|
|
426
380
|
// resolve its source schema by walking the global rep's shape tree at the
|
|
427
381
|
// spread site (see shape walk in analyze.js / resolveSchema in object.js).
|
|
428
382
|
const sh = shapeOfObjectLiteralAst(expr)
|
|
429
|
-
if (sh)
|
|
383
|
+
if (sh) {
|
|
384
|
+
updateGlobalRep(name, { jsonShape: sh })
|
|
385
|
+
// A module-global object is read via __dyn_get (its props live behind a
|
|
386
|
+
// runtime key, not a function-local schema slot), which emits a `['str', key]`
|
|
387
|
+
// node at compile time. Ensure the string module's `str` emitter is bound now
|
|
388
|
+
// — a string-literal-free program (`let g = {l:{a:1}}; export let f = () => g.l`)
|
|
389
|
+
// would otherwise abort late with "Unknown op: str". Scoped to globals that
|
|
390
|
+
// actually capture an object shape, so string-free local-object programs keep
|
|
391
|
+
// their minimal bundle (and golden-size pins).
|
|
392
|
+
includeForStringValue()
|
|
393
|
+
}
|
|
430
394
|
}
|
|
431
395
|
|
|
432
396
|
// === Call-site argument inference =========================================
|
|
@@ -0,0 +1,155 @@
|
|
|
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
|
+
|
|
24
|
+
const litN = (n, k) => Array.isArray(n) && n.length === 2 && n[0] == null && n[1] === k
|
|
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
|
+
// IV a statement increments by exactly +1, or null. Covers `i++` (post-inc desugars
|
|
31
|
+
// to `(++i) - 1`), `++i`, `i += 1`, `i = i + 1`.
|
|
32
|
+
function incVarOf(stmt) {
|
|
33
|
+
if (!Array.isArray(stmt)) return null
|
|
34
|
+
let inc = stmt
|
|
35
|
+
if (stmt[0] === '-' && litN(stmt[2], 1) && Array.isArray(stmt[1]) && stmt[1][0] === '++') inc = stmt[1]
|
|
36
|
+
if (inc[0] === '++' && typeof inc[1] === 'string') return inc[1]
|
|
37
|
+
if (stmt[0] === '+=' && typeof stmt[1] === 'string' && litN(stmt[2], 1)) return stmt[1]
|
|
38
|
+
if (stmt[0] === '=' && typeof stmt[1] === 'string' && Array.isArray(stmt[2]) && stmt[2][0] === '+') {
|
|
39
|
+
const [, a, b] = stmt[2]
|
|
40
|
+
if (a === stmt[1] && litN(b, 1)) return stmt[1]
|
|
41
|
+
if (b === stmt[1] && litN(a, 1)) return stmt[1]
|
|
42
|
+
}
|
|
43
|
+
return null
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const usesPattern = (n, i, w, pred) => Array.isArray(n) && (pred(n, i, w) || n.some(c => usesPattern(c, i, w, pred)))
|
|
47
|
+
const replace = (n, i, w, cx, cy) =>
|
|
48
|
+
!Array.isArray(n) ? n : isMod(n, i, w) ? cx : isFloorDiv(n, i, w) ? cy : n.map(c => replace(c, i, w, cx, cy))
|
|
49
|
+
// a `continue` that targets THIS loop (not one nested inside) — would skip the increment
|
|
50
|
+
const hasOuterContinue = (n) => Array.isArray(n) &&
|
|
51
|
+
(n[0] === 'continue' || (n[0] !== 'while' && n[0] !== 'for' && n[0] !== 'do' && n[0] !== '=>' && n.some(hasOuterContinue)))
|
|
52
|
+
// Vars assigned anywhere inside a closure in the function. Such a var can be
|
|
53
|
+
// mutated by a call in the loop body (the closure may be defined outside the loop,
|
|
54
|
+
// so a body-local findMutations misses it), so it is not safe as the IV or divisor.
|
|
55
|
+
const ASSIGN_OPS = new Set(['=', '+=', '-=', '*=', '/=', '%=', '**=', '&=', '|=', '^=', '<<=', '>>=', '>>>=', '&&=', '||=', '??='])
|
|
56
|
+
const collectAssigns = (n, out) => {
|
|
57
|
+
if (!Array.isArray(n)) return
|
|
58
|
+
if (typeof n[1] === 'string' && (ASSIGN_OPS.has(n[0]) || n[0] === '++' || n[0] === '--')) out.add(n[1])
|
|
59
|
+
n.forEach(c => collectAssigns(c, out))
|
|
60
|
+
}
|
|
61
|
+
const closureMutated = (n, out) => {
|
|
62
|
+
if (!Array.isArray(n)) return out
|
|
63
|
+
if (n[0] === '=>') collectAssigns(n, out)
|
|
64
|
+
n.forEach(c => closureMutated(c, out))
|
|
65
|
+
return out
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
let _uniq = 0
|
|
69
|
+
let _cm = new Set() // closure-mutated vars for the function currently being transformed
|
|
70
|
+
|
|
71
|
+
// Try to strength-reduce one `while` statement. Returns [seed, loop] or null.
|
|
72
|
+
function tryReduce(stmt) {
|
|
73
|
+
if (!Array.isArray(stmt) || stmt[0] !== 'while') return null
|
|
74
|
+
const cond = stmt[1], lbody = stmt[2]
|
|
75
|
+
if (!Array.isArray(lbody) || lbody[0] !== ';') return null
|
|
76
|
+
|
|
77
|
+
// exactly one IV incremented by +1
|
|
78
|
+
let iv = null, ivIdx = -1
|
|
79
|
+
for (let k = 1; k < lbody.length; k++) {
|
|
80
|
+
const v = incVarOf(lbody[k])
|
|
81
|
+
if (v) { if (iv) return null; iv = v; ivIdx = k }
|
|
82
|
+
}
|
|
83
|
+
if (!iv) return null
|
|
84
|
+
|
|
85
|
+
// a single invariant divisor `w` (`i % w` / `(i/w)|0` all share it)
|
|
86
|
+
let w = null
|
|
87
|
+
const findW = (n) => {
|
|
88
|
+
if (!Array.isArray(n)) return
|
|
89
|
+
const d = n[0] === '%' && n[1] === iv ? n[2]
|
|
90
|
+
: (n[0] === '|' && litN(n[2], 0) && Array.isArray(n[1]) && n[1][0] === '/' && n[1][1] === iv) ? n[1][2] : null
|
|
91
|
+
if (typeof d === 'string') { if (w == null) w = d; else if (w !== d) w = false }
|
|
92
|
+
n.forEach(findW)
|
|
93
|
+
}
|
|
94
|
+
findW(lbody)
|
|
95
|
+
if (!w || w === iv) return null
|
|
96
|
+
|
|
97
|
+
const usesMod = usesPattern(lbody, iv, w, isMod)
|
|
98
|
+
const usesDiv = usesPattern(lbody, iv, w, isFloorDiv)
|
|
99
|
+
if (!usesMod && !usesDiv) return null
|
|
100
|
+
|
|
101
|
+
// soundness: w invariant, iv written ONLY by the increment, no continue/closure capture
|
|
102
|
+
const wMut = new Set(); findMutations(lbody, new Set([w]), wMut)
|
|
103
|
+
if (wMut.has(w)) return null
|
|
104
|
+
const ivMut = new Set()
|
|
105
|
+
findMutations([';', ...lbody.slice(1).filter((_, k) => k !== ivIdx - 1)], new Set([iv]), ivMut)
|
|
106
|
+
if (ivMut.has(iv)) return null
|
|
107
|
+
if (hasOuterContinue(lbody)) return null
|
|
108
|
+
if (_cm.has(iv) || _cm.has(w)) return null // IV/divisor mutable via a closure call
|
|
109
|
+
|
|
110
|
+
const id = _uniq++
|
|
111
|
+
const cx = `__lsrx${id}`, cy = `__lsry${id}`
|
|
112
|
+
// seed (inside the w>0 branch): cx = (i%w)|0, cy = (i/w)|0 — one-time, i32 via |0
|
|
113
|
+
const seedDecls = [['=', cx, ['|', ['%', iv, w], 0]]]
|
|
114
|
+
if (usesDiv) seedDecls.push(['=', cy, ['|', ['/', iv, w], 0]])
|
|
115
|
+
const seed = ['let', ...seedDecls]
|
|
116
|
+
|
|
117
|
+
// transformed body: replace patterns, inject `cx++; if(cx>=w){cx=0; cy++}` after the increment
|
|
118
|
+
const newBody = [';']
|
|
119
|
+
for (let k = 1; k < lbody.length; k++) {
|
|
120
|
+
newBody.push(replace(lbody[k], iv, w, cx, cy))
|
|
121
|
+
if (k === ivIdx) {
|
|
122
|
+
newBody.push(['=', cx, ['+', cx, 1]])
|
|
123
|
+
if (usesDiv) newBody.push(['=', cy, ['+', cy, ['?:', ['>=', cx, w], 1, 0]]])
|
|
124
|
+
newBody.push(['=', cx, ['?:', ['>=', cx, w], 0, cx]])
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
const fast = ['while', replace(cond, iv, w, cx, cy), newBody]
|
|
128
|
+
// The counters track i%w only when w>0 AND i>=0: w<=0 gives NaN / a negative-divisor
|
|
129
|
+
// modulo, and i<0 makes i%w negative (JS modulo takes the dividend's sign) — neither
|
|
130
|
+
// follows the 0..w-1 +1-wrap the counters model. A one-time `w>0 && i>=0` guard (i is
|
|
131
|
+
// the IV's entry value; it only increments, so it stays ≥0) keeps the fast i32 path
|
|
132
|
+
// for the universal positive-dimension, forward-counting case and falls back to the
|
|
133
|
+
// unmodified loop otherwise — sound for any w, any start.
|
|
134
|
+
return [['if', ['&&', ['>', w, 0], ['>=', iv, 0]], ['{}', [';', seed, fast]], stmt]]
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Walk the body; transform `while` loops inside every block (post-order so a nested
|
|
138
|
+
// loop is reduced before its enclosing one is examined).
|
|
139
|
+
function walk(node) {
|
|
140
|
+
if (!Array.isArray(node)) return node
|
|
141
|
+
const n = node.map(walk)
|
|
142
|
+
if (n[0] !== ';') return n
|
|
143
|
+
const out = [';']
|
|
144
|
+
for (let k = 1; k < n.length; k++) {
|
|
145
|
+
const r = tryReduce(n[k])
|
|
146
|
+
if (r) out.push(...r)
|
|
147
|
+
else out.push(n[k])
|
|
148
|
+
}
|
|
149
|
+
return out
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export function strengthReduceLoopDivMod(body) {
|
|
153
|
+
_cm = closureMutated(body, new Set())
|
|
154
|
+
return walk(body)
|
|
155
|
+
}
|