jz 0.8.0 → 0.9.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 +56 -9
- package/bench/README.md +121 -50
- package/bench/bench.svg +27 -27
- package/cli.js +3 -1
- package/dist/interop.js +1 -1
- package/dist/jz.js +7541 -6178
- package/index.js +165 -74
- package/interop.js +189 -17
- package/jzify/arguments.js +32 -1
- package/jzify/async.js +409 -0
- package/jzify/classes.js +136 -18
- package/jzify/generators.js +639 -0
- package/jzify/hoist-vars.js +73 -1
- package/jzify/index.js +123 -4
- package/jzify/names.js +1 -0
- package/jzify/transform.js +239 -1
- package/layout.js +48 -3
- package/module/array.js +318 -43
- package/module/atomics.js +144 -0
- package/module/collection.js +1429 -155
- package/module/core.js +431 -40
- package/module/date.js +142 -120
- package/module/fs.js +144 -0
- package/module/function.js +6 -3
- package/module/index.js +4 -1
- package/module/json.js +270 -49
- package/module/math.js +1032 -145
- package/module/number.js +532 -163
- package/module/object.js +353 -95
- package/module/regex.js +157 -7
- package/module/schema.js +100 -2
- package/module/string.js +428 -93
- package/module/typedarray.js +264 -72
- package/module/web.js +36 -0
- package/package.json +5 -5
- package/src/abi/string.js +82 -11
- package/src/ast.js +11 -5
- package/src/autoload.js +28 -1
- package/src/bridge.js +7 -2
- package/src/compile/analyze-scans.js +22 -7
- package/src/compile/analyze.js +136 -30
- package/src/compile/dyn-closure-tables.js +277 -0
- package/src/compile/emit-assign.js +281 -15
- package/src/compile/emit.js +1055 -70
- package/src/compile/index.js +277 -29
- package/src/compile/infer.js +42 -4
- package/src/compile/inplace-store.js +329 -0
- package/src/compile/loop-recurrence.js +167 -0
- package/src/compile/narrow.js +555 -18
- package/src/compile/peel-stencil.js +5 -0
- package/src/compile/plan/index.js +45 -3
- package/src/compile/plan/inline.js +8 -1
- package/src/compile/plan/literals.js +39 -0
- package/src/compile/plan/scope.js +430 -23
- package/src/compile/program-facts.js +732 -38
- package/src/ctx.js +64 -9
- package/src/helper-counters.js +7 -1
- package/src/ir.js +113 -5
- package/src/kind-traits.js +66 -3
- package/src/kind.js +84 -12
- package/src/op-policy.js +7 -4
- package/src/optimize/index.js +1179 -704
- package/src/optimize/recurse.js +2 -2
- package/src/optimize/vectorize.js +982 -67
- package/src/prepare/index.js +809 -67
- package/src/prepare/math-kernel.js +331 -0
- package/src/prepare/pre-eval.js +714 -0
- package/src/snapshot.js +194 -0
- package/src/type.js +1170 -56
- package/src/wat/assemble.js +403 -65
- package/src/wat/codegen.js +74 -14
- package/transform.js +113 -4
- package/wasi.js +3 -0
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* In-place replace-store eligibility sweep — the immutable-update idiom
|
|
3
|
+
* (`arr[i] = { x, y, … }` in a loop) allocates a fresh object per element per
|
|
4
|
+
* step. When no alias can observe the difference, overwriting the OLD
|
|
5
|
+
* element's payload slots is bit-identical and allocation-free.
|
|
6
|
+
*
|
|
7
|
+
* Identity change is the only observable: after `arr[i] = fresh`, reading
|
|
8
|
+
* `arr[i].f` yields the new value either way; only a SAVED alias to the old
|
|
9
|
+
* element — field-read after the store, identity-compared, or stored
|
|
10
|
+
* elsewhere — distinguishes fresh-object from in-place. So a store site is
|
|
11
|
+
* eligible iff the whole program provably creates no such alias for that
|
|
12
|
+
* schema:
|
|
13
|
+
*
|
|
14
|
+
* 1. Schema-S values may appear in a VALUE position (call arg, return,
|
|
15
|
+
* container/slot store, comparison, bare use) ONLY as fresh `{}` literals
|
|
16
|
+
* — an OBJECT-typed NAME in value position aliases its object → poison S
|
|
17
|
+
* (unknown sid → poison all). `.field` receivers are safe (atomic read).
|
|
18
|
+
* 2. Element reads of maybe-object arrays may only be (a) an immediate
|
|
19
|
+
* `.field` receiver, or (b) the whole init RHS of a single-decl binding
|
|
20
|
+
* (a tracked ALIAS). Anything else → poison the array's elem sid.
|
|
21
|
+
* 3. `for…of` over a maybe-object array binds untracked element aliases →
|
|
22
|
+
* poison its elem sid.
|
|
23
|
+
* 4. A candidate store `arr[i] = {lit}` (elem sid of arr == sid of lit, all
|
|
24
|
+
* lit slot values NUMBER — no ephemeral pointers stored into a possibly
|
|
25
|
+
* durable receiver) is valid iff its sid survives 1-3, every tracked
|
|
26
|
+
* alias of that sid lives in the SAME function, same statement block,
|
|
27
|
+
* declared before the store, and never mentioned in a later statement of
|
|
28
|
+
* that block (block-scoped per iteration, so next-iteration uses re-read).
|
|
29
|
+
* Mentions inside the store statement itself are fine — the emit spills
|
|
30
|
+
* the literal's values before overwriting.
|
|
31
|
+
*
|
|
32
|
+
* Result: `ctx.schema.inplaceStores = WeakSet<literalNode>` — consulted by
|
|
33
|
+
* emitElementAssign, which emits the guarded in-place fast path (old box has
|
|
34
|
+
* OBJECT tag + sid S → slot stores; anything else → the generic fresh-alloc
|
|
35
|
+
* path, so runtime aliens stay bit-exact).
|
|
36
|
+
*/
|
|
37
|
+
import { ctx } from '../ctx.js'
|
|
38
|
+
import { analyzeBody } from './analyze.js'
|
|
39
|
+
import { staticObjectProps } from '../static.js'
|
|
40
|
+
import { VAL } from '../reps.js'
|
|
41
|
+
|
|
42
|
+
/** Canonical content key for a store site — the ','-wrapper around literal
|
|
43
|
+
* props is normalized away between plan and emit, so flatten before
|
|
44
|
+
* serializing. Shared by the sweep and emitElementAssign. */
|
|
45
|
+
export const inplaceKey = (arrName, lit) => {
|
|
46
|
+
const props = lit.slice(1)
|
|
47
|
+
const flat = props.length === 1 && Array.isArray(props[0]) && props[0][0] === ',' ? props[0].slice(1) : props
|
|
48
|
+
return `${arrName}|${JSON.stringify(flat)}`
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// env-gated debug — dist/jz.js runs in browsers where `process` doesn't exist
|
|
52
|
+
const DBG = typeof process !== 'undefined' && process.env.JZ_DBG_INPLACE
|
|
53
|
+
|
|
54
|
+
const CONTAINER_METHODS = new Set(['push', 'unshift', 'splice', 'fill', 'set', 'add'])
|
|
55
|
+
|
|
56
|
+
export function scanInplaceStores(programFacts) {
|
|
57
|
+
const poisoned = new Set() // sids with a possible surviving alias
|
|
58
|
+
let poisonAll = false
|
|
59
|
+
const aliases = [] // { fn, sid, name, block, declIdx }
|
|
60
|
+
const candidates = [] // { fn, sid, lit, block, stmtIdx, arrName }
|
|
61
|
+
|
|
62
|
+
const paramReps = programFacts.paramReps
|
|
63
|
+
|
|
64
|
+
// Element facts for params narrow.js leaves untracked (exported functions):
|
|
65
|
+
// meet over INTERNAL call sites. Zero internal call sites → only the host
|
|
66
|
+
// can call, and host-marshaled containers are fresh deep copies (interop
|
|
67
|
+
// wrapVal) that cannot alias any compile-internal object → non-aliasing.
|
|
68
|
+
// Any unprovable internal arg → maybe-object with unknown sid.
|
|
69
|
+
const _csCache = new Map()
|
|
70
|
+
const callSiteElemInfo = (fnName, k) => {
|
|
71
|
+
const key = fnName + ' ' + k
|
|
72
|
+
if (_csCache.has(key)) return _csCache.get(key)
|
|
73
|
+
let mayBeObject = false, sid = null, seen = 0
|
|
74
|
+
for (const cs of programFacts.callSites || []) {
|
|
75
|
+
if (cs.callee !== fnName) continue
|
|
76
|
+
seen++
|
|
77
|
+
const arg = cs.argList?.[k]
|
|
78
|
+
const cf = cs.callerFunc
|
|
79
|
+
const cfacts = cf?.body && !cf.raw ? analyzeBody(cf.body) : null
|
|
80
|
+
let proven = false
|
|
81
|
+
if (typeof arg === 'string' && cfacts) {
|
|
82
|
+
if (cfacts.typedElems?.has(arg) || cfacts.locals?.get(arg) === 'typed') proven = true
|
|
83
|
+
else {
|
|
84
|
+
const ev = cfacts.arrElemValTypes?.get(arg)
|
|
85
|
+
if (ev != null && ev !== VAL.OBJECT) proven = true
|
|
86
|
+
const s = cfacts.arrElemSchemas?.get(arg)
|
|
87
|
+
if (s != null) { mayBeObject = true; sid = sid == null || sid === s ? s : -1; proven = true }
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
if (!proven) { mayBeObject = true; sid = -1 }
|
|
91
|
+
}
|
|
92
|
+
const r = { sid: sid === -1 ? null : sid, mayBeObject: seen === 0 ? false : mayBeObject }
|
|
93
|
+
_csCache.set(key, r)
|
|
94
|
+
return r
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
for (const fn of ctx.func.list) {
|
|
98
|
+
if (!fn.body || fn.raw) continue
|
|
99
|
+
const facts = analyzeBody(fn.body)
|
|
100
|
+
const reps = paramReps?.get(fn.name)
|
|
101
|
+
const paramIdx = new Map(fn.sig.params.map((p, k) => [p.name, k]))
|
|
102
|
+
|
|
103
|
+
// Element info for an array-valued name: elem sid (if known) and whether
|
|
104
|
+
// elements could be schema objects at all (typed/NUMBER/STRING elems can't).
|
|
105
|
+
const elemInfo = (name) => {
|
|
106
|
+
if (typeof name !== 'string') return { sid: null, mayBeObject: true }
|
|
107
|
+
if (facts.typedElems?.has(name) || facts.locals?.get(name) === 'typed') return { sid: null, mayBeObject: false }
|
|
108
|
+
let sid = facts.arrElemSchemas?.get(name)
|
|
109
|
+
let ev = facts.arrElemValTypes?.get(name)
|
|
110
|
+
const k = paramIdx.get(name)
|
|
111
|
+
if (k != null) {
|
|
112
|
+
const r = reps?.get(k)
|
|
113
|
+
if (r) {
|
|
114
|
+
if (r.typedElem != null || r.val === VAL.TYPED) return { sid: null, mayBeObject: false }
|
|
115
|
+
if (sid == null) sid = r.arrayElemSchema
|
|
116
|
+
if (ev == null) ev = r.arrayElemValType
|
|
117
|
+
}
|
|
118
|
+
if (sid == null && (ev == null || ev === VAL.OBJECT)) {
|
|
119
|
+
// untracked param (exported fn) — derive from internal call sites
|
|
120
|
+
const cs = callSiteElemInfo(fn.name, k)
|
|
121
|
+
if (!cs.mayBeObject) return { sid: null, mayBeObject: false }
|
|
122
|
+
if (sid == null) sid = cs.sid
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
if (sid == null && ev != null && ev !== VAL.OBJECT) return { sid: null, mayBeObject: false }
|
|
126
|
+
return { sid: sid ?? null, mayBeObject: true }
|
|
127
|
+
}
|
|
128
|
+
const poisonElem = (name) => {
|
|
129
|
+
const { sid, mayBeObject } = elemInfo(name)
|
|
130
|
+
if (!mayBeObject) return
|
|
131
|
+
if (sid != null) poisoned.add(sid)
|
|
132
|
+
else { poisonAll = true; if (DBG) console.error('[inplace-poisonAll] elem read of', name, 'in', fn.name) }
|
|
133
|
+
}
|
|
134
|
+
// A NAME in value position: aliases its object if OBJECT-typed. An S value
|
|
135
|
+
// can only reach an unknown-typed name through a path poisoned elsewhere,
|
|
136
|
+
// so unknown types are ignored (conservatism lives at the leak sites).
|
|
137
|
+
const poisonName = (name) => {
|
|
138
|
+
const vt = facts.valTypes?.get(name) ?? (paramIdx.has(name) ? reps?.get(paramIdx.get(name))?.val : null)
|
|
139
|
+
if (vt !== VAL.OBJECT) return
|
|
140
|
+
const sid = ctx.schema.vars?.get(name) ?? (paramIdx.has(name) ? reps?.get(paramIdx.get(name))?.schemaId : null)
|
|
141
|
+
if (sid != null) poisoned.add(sid)
|
|
142
|
+
else { poisonAll = true; if (DBG) console.error('[inplace-poisonAll] OBJECT name', name, 'in', fn.name) }
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const litSid = (lit) => {
|
|
146
|
+
const parsed = staticObjectProps(lit.slice(1))
|
|
147
|
+
return parsed ? ctx.schema.register(parsed.names) : null
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// value-position walk; `stmts`/`stmtIdx` track the innermost statement
|
|
151
|
+
// block so alias decls and stores get comparable positions
|
|
152
|
+
const walkVal = (n, stmts, stmtIdx) => {
|
|
153
|
+
if (!Array.isArray(n)) { if (typeof n === 'string') poisonName(n); return }
|
|
154
|
+
const op = n[0]
|
|
155
|
+
if (typeof op !== 'string') return
|
|
156
|
+
if (op === 'str') return
|
|
157
|
+
if (op === '{}') { for (let i = 1; i < n.length; i++) walkSlotValues(n[i], stmts, stmtIdx); return }
|
|
158
|
+
if (op === '.' || op === '?.') {
|
|
159
|
+
// receiver is a safe atomic read — an element-read receiver is fine,
|
|
160
|
+
// a NAME receiver doesn't alias
|
|
161
|
+
const r = n[1]
|
|
162
|
+
if (Array.isArray(r) && r[0] === '[]') { if (typeof r[1] === 'string') { /* arr[i].f — safe */ } else walkVal(r[1], stmts, stmtIdx); if (r[2] != null) walkVal(r[2], stmts, stmtIdx) }
|
|
163
|
+
else if (Array.isArray(r)) walkVal(r, stmts, stmtIdx)
|
|
164
|
+
return
|
|
165
|
+
}
|
|
166
|
+
if (op === '[]' && n.length === 3) {
|
|
167
|
+
// element read in value position — a leak unless provably non-object
|
|
168
|
+
if (typeof n[1] === 'string') poisonElem(n[1])
|
|
169
|
+
else walkVal(n[1], stmts, stmtIdx)
|
|
170
|
+
if (n[2] != null) walkVal(n[2], stmts, stmtIdx)
|
|
171
|
+
return
|
|
172
|
+
}
|
|
173
|
+
for (let i = 1; i < n.length; i++) walkVal(n[i], stmts, stmtIdx)
|
|
174
|
+
}
|
|
175
|
+
// `{}` literal slot values: `[':' key value]` pairs (or `,`-joined) —
|
|
176
|
+
// nested fresh literals are fine, everything else is a value position
|
|
177
|
+
const walkSlotValues = (n, stmts, stmtIdx) => {
|
|
178
|
+
if (!Array.isArray(n)) { if (typeof n === 'string') poisonName(n); return }
|
|
179
|
+
if (n[0] === ',') { for (let i = 1; i < n.length; i++) walkSlotValues(n[i], stmts, stmtIdx); return }
|
|
180
|
+
if (n[0] === ':') { walkVal(n[2], stmts, stmtIdx); return }
|
|
181
|
+
walkVal(n, stmts, stmtIdx)
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const walkStmt = (n, stmts, stmtIdx) => {
|
|
185
|
+
if (!Array.isArray(n)) { if (typeof n === 'string') poisonName(n); return }
|
|
186
|
+
const op = n[0]
|
|
187
|
+
// statement-position '{}' is a BLOCK (post-prepare bodies are
|
|
188
|
+
// ['{}', [';', ...stmts]]), not an object literal
|
|
189
|
+
if (op === ';' || op === '{' || op === '{}') {
|
|
190
|
+
const list = n.slice(1)
|
|
191
|
+
for (let i = 0; i < list.length; i++) walkStmt(list[i], list, i)
|
|
192
|
+
return
|
|
193
|
+
}
|
|
194
|
+
if (op === 'let' || op === 'const') {
|
|
195
|
+
for (let i = 1; i < n.length; i++) {
|
|
196
|
+
const d = n[i]
|
|
197
|
+
if (!Array.isArray(d) || d[0] !== '=') { walkVal(d, stmts, stmtIdx); continue }
|
|
198
|
+
const [, lhs, rhs] = d
|
|
199
|
+
if (typeof lhs === 'string' && Array.isArray(rhs) && rhs[0] === '[]' && rhs.length === 3 && typeof rhs[1] === 'string') {
|
|
200
|
+
// tracked alias: whole-RHS element read into a single binding
|
|
201
|
+
const { sid, mayBeObject } = elemInfo(rhs[1])
|
|
202
|
+
if (mayBeObject) {
|
|
203
|
+
if (sid != null) aliases.push({ fn, sid, name: lhs, block: stmts, declIdx: stmtIdx, arr: rhs[1], idx: typeof rhs[2] === 'string' ? rhs[2] : null })
|
|
204
|
+
else { poisonAll = true; if (DBG) console.error('[inplace-poisonAll] alias unknown-sid', lhs, 'of', rhs[1], 'in', fn.name) }
|
|
205
|
+
}
|
|
206
|
+
if (rhs[2] != null) walkVal(rhs[2], stmts, stmtIdx)
|
|
207
|
+
continue
|
|
208
|
+
}
|
|
209
|
+
walkVal(rhs, stmts, stmtIdx)
|
|
210
|
+
}
|
|
211
|
+
return
|
|
212
|
+
}
|
|
213
|
+
if (op === '=' && Array.isArray(n[1]) && n[1][0] === '[]' && n[1].length === 3 && typeof n[1][1] === 'string') {
|
|
214
|
+
const [, lhs, rhs] = n
|
|
215
|
+
if (lhs[2] != null) walkVal(lhs[2], stmts, stmtIdx)
|
|
216
|
+
if (Array.isArray(rhs) && rhs[0] === '{}') {
|
|
217
|
+
const { sid, mayBeObject } = elemInfo(lhs[1])
|
|
218
|
+
const s = mayBeObject ? litSid(rhs) : null
|
|
219
|
+
if (s != null && s === sid) candidates.push({ fn, sid: s, lit: rhs, block: stmts, stmtIdx, arrName: lhs[1], idxName: typeof lhs[2] === 'string' ? lhs[2] : null })
|
|
220
|
+
// literal is a fresh value — walk only its slot values
|
|
221
|
+
for (let i = 1; i < rhs.length; i++) walkSlotValues(rhs[i], stmts, stmtIdx)
|
|
222
|
+
} else walkVal(rhs, stmts, stmtIdx)
|
|
223
|
+
return
|
|
224
|
+
}
|
|
225
|
+
if (op === 'for-of') {
|
|
226
|
+
// `for (const p of arr)` binds untracked element aliases
|
|
227
|
+
const src = n[2]
|
|
228
|
+
if (typeof src === 'string') poisonElem(src)
|
|
229
|
+
else walkVal(src, stmts, stmtIdx)
|
|
230
|
+
for (let i = 3; i < n.length; i++) walkStmt(n[i], stmts, stmtIdx)
|
|
231
|
+
return
|
|
232
|
+
}
|
|
233
|
+
if (op === '=>') {
|
|
234
|
+
// closure body: value-walk everything (captured aliases poison via names)
|
|
235
|
+
for (let i = 1; i < n.length; i++) walkVal(n[i], stmts, stmtIdx)
|
|
236
|
+
return
|
|
237
|
+
}
|
|
238
|
+
if (op === 'for' || op === 'while' || op === 'do' || op === 'if' || op === 'for-in') {
|
|
239
|
+
// for is flat post-prepare: ['for', init, cond, step, body]
|
|
240
|
+
for (let i = 1; i < n.length; i++) walkStmt(n[i], stmts, stmtIdx)
|
|
241
|
+
return
|
|
242
|
+
}
|
|
243
|
+
// container-method calls: literal args are fresh single-home values
|
|
244
|
+
if (op === '()' && Array.isArray(n[1]) && (n[1][0] === '.') && CONTAINER_METHODS.has(n[1][2])) {
|
|
245
|
+
if (typeof n[1][1] !== 'string') walkVal(n[1][1], stmts, stmtIdx)
|
|
246
|
+
const argNode = n[2]
|
|
247
|
+
const args = argNode == null ? [] : (Array.isArray(argNode) && argNode[0] === ',') ? argNode.slice(1) : [argNode]
|
|
248
|
+
for (const a of args) {
|
|
249
|
+
if (Array.isArray(a) && a[0] === '{}') { for (let i = 1; i < a.length; i++) walkSlotValues(a[i], stmts, stmtIdx) }
|
|
250
|
+
else walkVal(a, stmts, stmtIdx)
|
|
251
|
+
}
|
|
252
|
+
return
|
|
253
|
+
}
|
|
254
|
+
walkVal(n, stmts, stmtIdx)
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
walkStmt(fn.body, null, 0)
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const containsName = (n, name) => {
|
|
261
|
+
if (n === name) return true
|
|
262
|
+
if (!Array.isArray(n)) return false
|
|
263
|
+
for (let i = (n[0] === 'str' ? n.length : 1); i < n.length; i++) if (containsName(n[i], name)) return true
|
|
264
|
+
return false
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Content-keyed result: per-function body transforms and emit-time inlining
|
|
268
|
+
// between plan and emit rebuild the tree (and splice bodies across function
|
|
269
|
+
// frames), so neither node identity nor the enclosing function name survives
|
|
270
|
+
// to emitElementAssign. A `receiver|literal` content key is sound only if
|
|
271
|
+
// EVERY same-content candidate program-wide validates — group and meet.
|
|
272
|
+
// (An inliner that RENAMES the receiver makes the key miss → no transform —
|
|
273
|
+
// safe in the conservative direction.)
|
|
274
|
+
// Target-binding reuse: when THE tracked alias of a store site reads the
|
|
275
|
+
// SAME array at the SAME plain-name index, and every statement between its
|
|
276
|
+
// decl and the store is a pure local decl (no calls, no writes to the array
|
|
277
|
+
// or the index — nothing that could rebind arr[i]), the emit can overwrite
|
|
278
|
+
// through the alias binding directly instead of re-reading the element
|
|
279
|
+
// (`__arr_idx` + forwarding + bounds, per iteration). Field writes to the
|
|
280
|
+
// alias itself between are fine — the store overwrites every slot anyway.
|
|
281
|
+
const impureBetween = (n, idxName, arrName) => {
|
|
282
|
+
if (!Array.isArray(n)) return false
|
|
283
|
+
const op = n[0]
|
|
284
|
+
if (op === '()' || op === 'new' || op === 'await' || op === 'yield') return true
|
|
285
|
+
if (op === '=' || op === '++' || op === '--' || (typeof op === 'string' && op.length > 1 && op.endsWith('=') && !op.endsWith('==') && op !== '>=' && op !== '<=')) {
|
|
286
|
+
const t = n[1]
|
|
287
|
+
if (t === idxName || t === arrName) return true
|
|
288
|
+
if (Array.isArray(t) && t[0] === '[]' && t[1] === arrName) return true
|
|
289
|
+
}
|
|
290
|
+
for (let i = op === 'str' ? n.length : 1; i < n.length; i++) if (impureBetween(n[i], idxName, arrName)) return true
|
|
291
|
+
return false
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
const verdict = new Map() // key → false | { alias, idx } | { alias: null }
|
|
295
|
+
for (const c of candidates) {
|
|
296
|
+
const key = inplaceKey(c.arrName, c.lit)
|
|
297
|
+
let ok = !poisonAll && !poisoned.has(c.sid)
|
|
298
|
+
for (const a of aliases) {
|
|
299
|
+
if (!ok) break
|
|
300
|
+
if (a.sid !== c.sid) continue
|
|
301
|
+
// an alias in another function (or another block) could live across the
|
|
302
|
+
// call/iteration into this store — reject
|
|
303
|
+
if (a.fn !== c.fn || a.block !== c.block || a.declIdx >= c.stmtIdx) { ok = false; break }
|
|
304
|
+
// any mention after the store in the same block observes the overwrite
|
|
305
|
+
for (let i = c.stmtIdx + 1; i < c.block.length && ok; i++)
|
|
306
|
+
if (containsName(c.block[i], a.name)) ok = false
|
|
307
|
+
}
|
|
308
|
+
let reuse = null
|
|
309
|
+
if (ok && c.idxName) for (const a of aliases) {
|
|
310
|
+
if (a.fn !== c.fn || a.block !== c.block || a.sid !== c.sid) continue
|
|
311
|
+
if (a.declIdx >= c.stmtIdx || a.arr !== c.arrName || a.idx !== c.idxName) continue
|
|
312
|
+
let pure = true
|
|
313
|
+
for (let i = a.declIdx + 1; i < c.stmtIdx && pure; i++)
|
|
314
|
+
pure = !impureBetween(c.block[i], c.idxName, c.arrName)
|
|
315
|
+
if (pure) { reuse = { alias: a.name, idx: a.idx }; break }
|
|
316
|
+
}
|
|
317
|
+
// meet across same-content sites: validity ANDs; reuse must agree exactly
|
|
318
|
+
const prev = verdict.get(key)
|
|
319
|
+
if (!ok || prev === false) verdict.set(key, false)
|
|
320
|
+
else if (prev === undefined) verdict.set(key, reuse ?? { alias: null })
|
|
321
|
+
else if (prev.alias !== (reuse?.alias ?? null) || (prev.alias && prev.idx !== reuse.idx)) verdict.set(key, { alias: null })
|
|
322
|
+
if (ok && DBG) console.error('[inplace-ok]', c.fn.name, c.arrName, 'sid', c.sid, reuse ? 'reuse ' + reuse.alias : '')
|
|
323
|
+
}
|
|
324
|
+
const out = new Map()
|
|
325
|
+
for (const [key, v] of verdict) if (v) out.set(key, v)
|
|
326
|
+
ctx.schema.inplaceStores = out
|
|
327
|
+
if (DBG) console.error('[inplace]', 'candidates:', candidates.length, 'aliases:', aliases.length, 'poisoned:', [...poisoned], 'poisonAll:', poisonAll, 'eligible:', candidates.filter(c => out.has(c.lit)).length)
|
|
328
|
+
return out
|
|
329
|
+
}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
// Partial unroll (×2) + scalar replacement of a unit-stride ARRAY RECURRENCE.
|
|
2
|
+
//
|
|
3
|
+
// A DP/scan loop that reads `arr[j-1]` and writes `arr[j]` (unit stride) carries the
|
|
4
|
+
// just-written value to the next iteration THROUGH MEMORY: it stores `arr[j]`, then the next
|
|
5
|
+
// iteration loads `arr[j-1]` — the very cell it just wrote. V8/TurboFan forwards that store→load
|
|
6
|
+
// and unrolls the loop internally; Cranelift/wasmtime and the baseline tiers do neither, so the
|
|
7
|
+
// loop pays a store→load round trip plus full per-iteration overhead on every cell. clang/gcc
|
|
8
|
+
// fix exactly this with this transform — measured 2.15× on wasmtime for the Levenshtein DP
|
|
9
|
+
// (V8-neutral, bit-exact).
|
|
10
|
+
//
|
|
11
|
+
// Recognized (post-prepare AST): a unit-stride `for (let j = LO; j </<= HI; j++)` whose body, for
|
|
12
|
+
// ONE array `arr`, has a single store `arr[j] = <var>` and ≥1 read `arr[j-1]`, accesses `arr` at
|
|
13
|
+
// no other index, never aliases `arr` elsewhere, and contains no call / nested loop / break /
|
|
14
|
+
// continue / return / closure. The `arr[j-1]` read becomes a scalar `left` seeded from `arr[LO-1]`
|
|
15
|
+
// and refreshed after each store; the body is then unrolled ×2 (with a 1-cell tail) so the carry
|
|
16
|
+
// between the paired cells lives in a register and the loop overhead is halved. A `LO <= HI` guard
|
|
17
|
+
// keeps the seed load in step with the original (which reads `arr[LO-1]` only when it iterates),
|
|
18
|
+
// and falls back to the untouched loop on the empty range — sound for any trip count.
|
|
19
|
+
|
|
20
|
+
import { findMutations } from './analyze-scans.js'
|
|
21
|
+
import { litVal, litN, unitIncVar, normalizeLoop, freshLoopId } from './loop-model.js'
|
|
22
|
+
import { rewriteBlocks, closureMutatedVars } from './loop-model.js'
|
|
23
|
+
|
|
24
|
+
const isArr = (n) => Array.isArray(n) // wrap (not alias): the self-host kernel rejects a builtin used as a first-class value
|
|
25
|
+
const clone = (n) => isArr(n) ? n.map(clone) : n
|
|
26
|
+
const isIvMinus1 = (n, iv) => isArr(n) && n[0] === '-' && n[1] === iv && litN(n[2], 1) // (iv - 1)
|
|
27
|
+
|
|
28
|
+
// Ops whose presence makes duplicating the body in place unsound (control that escapes the cell,
|
|
29
|
+
// or a call that could alias/mutate `arr` or reorder side effects).
|
|
30
|
+
const REJECT = new Set(['for', 'while', 'do', 'for-in', 'for-of', 'break', 'continue', 'return',
|
|
31
|
+
'throw', 'switch', 'try', 'catch', 'finally', '=>', 'label'])
|
|
32
|
+
const hasUnsafe = (n) => {
|
|
33
|
+
if (!isArr(n)) return false
|
|
34
|
+
if (REJECT.has(n[0])) return true
|
|
35
|
+
if (n[0] === '()' && typeof n[1] === 'string') return true // function call `f(args)`
|
|
36
|
+
return n.some(hasUnsafe)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Substitute every value-reference of `iv` with (iv + 1); leave the op slot and property keys.
|
|
40
|
+
const subPlus1 = (n, iv) => {
|
|
41
|
+
if (n === iv) return ['+', iv, 1]
|
|
42
|
+
if (!isArr(n)) return n
|
|
43
|
+
if (n[0] === '.' && n.length === 3) return ['.', subPlus1(n[1], iv), n[2]]
|
|
44
|
+
return [n[0], ...n.slice(1).map(c => subPlus1(c, iv))]
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Rename every let/const-DECLARED var in `stmts` with a suffix, throughout — so the 2nd unrolled
|
|
48
|
+
// cell's locals don't collide with the 1st. Loop-carried outer vars (assigned, not declared here)
|
|
49
|
+
// are untouched, so the recurrence still threads through them.
|
|
50
|
+
function renameDecls(stmts, suf) {
|
|
51
|
+
const declared = new Set()
|
|
52
|
+
const collect = (n) => {
|
|
53
|
+
if (!isArr(n)) return
|
|
54
|
+
if (n[0] === 'let' || n[0] === 'const')
|
|
55
|
+
for (let k = 1; k < n.length; k++) if (isArr(n[k]) && n[k][0] === '=' && typeof n[k][1] === 'string') declared.add(n[k][1])
|
|
56
|
+
n.forEach(collect)
|
|
57
|
+
}
|
|
58
|
+
stmts.forEach(collect)
|
|
59
|
+
if (!declared.size) return stmts
|
|
60
|
+
const ren = (n) => {
|
|
61
|
+
if (typeof n === 'string') return declared.has(n) ? n + suf : n
|
|
62
|
+
if (!isArr(n)) return n
|
|
63
|
+
if (n[0] === '.' && n.length === 3) return ['.', ren(n[1]), n[2]]
|
|
64
|
+
return [n[0], ...n.slice(1).map(ren)]
|
|
65
|
+
}
|
|
66
|
+
return stmts.map(ren)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Replace `arr[iv-1]` reads with `left`; keep the store; emit `left = storeVal` after each store.
|
|
70
|
+
function scalarReplace(stmts, arr, iv, left, storeVal) {
|
|
71
|
+
const repl = (n) => {
|
|
72
|
+
if (!isArr(n)) return n
|
|
73
|
+
if (n[0] === '[]' && n[1] === arr && isIvMinus1(n[2], iv)) return left
|
|
74
|
+
if (n[0] === '.' && n.length === 3) return ['.', repl(n[1]), n[2]]
|
|
75
|
+
return [n[0], ...n.slice(1).map(repl)]
|
|
76
|
+
}
|
|
77
|
+
const out = []
|
|
78
|
+
for (const s of stmts) {
|
|
79
|
+
out.push(repl(s))
|
|
80
|
+
if (isArr(s) && s[0] === '=' && isArr(s[1]) && s[1][0] === '[]' && s[1][1] === arr && s[1][2] === iv)
|
|
81
|
+
out.push(['=', left, storeVal])
|
|
82
|
+
}
|
|
83
|
+
return out
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function tryUnroll(stmt, cm) {
|
|
87
|
+
const L = normalizeLoop(stmt)
|
|
88
|
+
if (!L || L.kind !== 'for') return null
|
|
89
|
+
const body = L.body
|
|
90
|
+
if (!isArr(body) || body[0] !== ';') return null
|
|
91
|
+
const iv = unitIncVar(L.step)
|
|
92
|
+
if (!iv) return null
|
|
93
|
+
|
|
94
|
+
// init `let iv = LO`, LO a literal ≥ 1 (so arr[LO-1] is a valid in-bounds index)
|
|
95
|
+
if (!(isArr(L.init) && L.init[0] === 'let' && isArr(L.init[1]) && L.init[1][0] === '=' && L.init[1][1] === iv)) return null
|
|
96
|
+
const LO = L.init[1][2], loVal = litVal(LO)
|
|
97
|
+
if (loVal == null || loVal < 1) return null
|
|
98
|
+
|
|
99
|
+
// cond `iv <= HI` / `iv < HI`, HI loop-invariant
|
|
100
|
+
if (!(isArr(L.cond) && (L.cond[0] === '<=' || L.cond[0] === '<') && L.cond[1] === iv)) return null
|
|
101
|
+
const cmpOp = L.cond[0], HI = L.cond[2]
|
|
102
|
+
if (!(typeof HI === 'string' || litVal(HI) != null)) return null
|
|
103
|
+
|
|
104
|
+
if (hasUnsafe(body)) return null
|
|
105
|
+
const stmts = body.slice(1)
|
|
106
|
+
|
|
107
|
+
// exactly one store `arr[iv] = <var>` — the recurrence array + carried value
|
|
108
|
+
let arr = null, storeVal = null, nStore = 0
|
|
109
|
+
for (const s of stmts)
|
|
110
|
+
if (isArr(s) && s[0] === '=' && isArr(s[1]) && s[1][0] === '[]' && s[1][2] === iv) {
|
|
111
|
+
if (typeof s[2] !== 'string') return null
|
|
112
|
+
arr = s[1][1]; storeVal = s[2]; nStore++
|
|
113
|
+
}
|
|
114
|
+
if (nStore !== 1 || typeof arr !== 'string') return null
|
|
115
|
+
if (storeVal === iv || storeVal === arr) return null
|
|
116
|
+
|
|
117
|
+
// every `arr[...]` is `arr[iv]` or `arr[iv-1]`, the only write is the store, ≥1 recurrence read,
|
|
118
|
+
// and `arr` never appears bare (passed/aliased)
|
|
119
|
+
let hasRec = false, bad = false
|
|
120
|
+
const scan = (n) => {
|
|
121
|
+
if (!isArr(n)) return
|
|
122
|
+
if (n[0] === '[]' && n[1] === arr) {
|
|
123
|
+
if (n[2] === iv) {} else if (isIvMinus1(n[2], iv)) hasRec = true; else bad = true
|
|
124
|
+
}
|
|
125
|
+
if (n[0] === '=' && isArr(n[1]) && n[1][0] === '[]' && n[1][1] === arr && n[1][2] !== iv) bad = true
|
|
126
|
+
if (!(n[0] === '[]' || n[0] === '.')) for (let k = 1; k < n.length; k++) if (n[k] === arr) bad = true
|
|
127
|
+
n.forEach(scan)
|
|
128
|
+
}
|
|
129
|
+
scan(body)
|
|
130
|
+
if (bad || !hasRec) return null
|
|
131
|
+
|
|
132
|
+
// The carry `left = storeVal` is emitted right after the store, so a recurrence read AFTER the
|
|
133
|
+
// store would see this cell's value, not arr[iv-1]. Require every arr[iv-1] read to precede it.
|
|
134
|
+
const storeIdx = stmts.findIndex(s => isArr(s) && s[0] === '=' && isArr(s[1]) && s[1][0] === '[]' && s[1][1] === arr && s[1][2] === iv)
|
|
135
|
+
const readsRec = (n) => isArr(n) && ((n[0] === '[]' && n[1] === arr && isIvMinus1(n[2], iv)) || n.some(readsRec))
|
|
136
|
+
for (let k = storeIdx + 1; k < stmts.length; k++) if (readsRec(stmts[k])) return null
|
|
137
|
+
|
|
138
|
+
// iv assigned only by the step; iv/arr/HI loop-invariant (not mutated, incl. via a closure call)
|
|
139
|
+
const ivMut = new Set(); findMutations(body, new Set([iv]), ivMut)
|
|
140
|
+
if (ivMut.has(iv)) return null
|
|
141
|
+
if (cm.has(iv) || cm.has(arr)) return null
|
|
142
|
+
if (typeof HI === 'string') { const hiMut = new Set(); findMutations(body, new Set([HI]), hiMut); if (hiMut.has(HI) || cm.has(HI)) return null }
|
|
143
|
+
|
|
144
|
+
// --- transform ---
|
|
145
|
+
const id = freshLoopId()
|
|
146
|
+
const left = `__rec${id}`
|
|
147
|
+
const bodyS = scalarReplace(stmts, arr, iv, left, storeVal)
|
|
148
|
+
const cellJ = () => bodyS.map(clone)
|
|
149
|
+
const cellJ1 = renameDecls(bodyS.map(s => subPlus1(clone(s), iv)), `$r${id}`)
|
|
150
|
+
|
|
151
|
+
const seed = ['let', ['=', left, ['[]', arr, loVal - 1]]] // left = arr[LO-1]
|
|
152
|
+
const letIv = ['let', ['=', iv, clone(LO)]] // let iv = LO
|
|
153
|
+
const twoFit = cmpOp === '<=' ? ['<', iv, clone(HI)] : ['<', iv, ['-', clone(HI), 1]]
|
|
154
|
+
const main = ['while', twoFit,
|
|
155
|
+
[';', ['{}', [';', ...cellJ()]], ['{}', [';', ...cellJ1]], ['=', iv, ['+', iv, 2]]]]
|
|
156
|
+
const tail = ['if', [cmpOp, iv, clone(HI)],
|
|
157
|
+
['{}', [';', ...cellJ(), ['=', iv, ['+', iv, 1]]]]]
|
|
158
|
+
const block = ['{}', [';', letIv, seed, main, tail]]
|
|
159
|
+
// Run the unrolled form only on a non-empty range (so the seed's arr[LO-1] load matches the
|
|
160
|
+
// original, which reads it only when it iterates); otherwise the untouched loop.
|
|
161
|
+
return [['if', [cmpOp, clone(LO), clone(HI)], block, stmt]]
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export function unrollRecurrence(body) {
|
|
165
|
+
const cm = closureMutatedVars(body)
|
|
166
|
+
return rewriteBlocks(body, stmt => tryUnroll(stmt, cm))
|
|
167
|
+
}
|