jz 0.5.0 → 0.6.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 +288 -314
- package/bench/README.md +319 -0
- package/bench/bench.svg +112 -0
- package/cli.js +32 -24
- package/index.js +177 -55
- package/interop.js +88 -159
- 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 +179 -0
- package/module/array.js +322 -153
- package/module/collection.js +603 -145
- package/module/console.js +55 -43
- package/module/core.js +281 -142
- 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 +461 -185
- package/module/number.js +306 -60
- package/module/object.js +448 -184
- package/module/regex.js +255 -25
- package/module/schema.js +24 -6
- package/module/simd.js +85 -0
- package/module/string.js +591 -220
- package/module/symbol.js +1 -1
- package/module/timer.js +9 -14
- package/module/typedarray.js +45 -48
- package/package.json +41 -12
- package/src/abi/index.js +39 -23
- package/src/abi/string.js +38 -41
- package/src/ast.js +460 -0
- package/src/autoload.js +26 -24
- package/src/bridge.js +111 -0
- package/src/compile/analyze-scans.js +661 -0
- package/src/compile/analyze.js +1565 -0
- package/src/compile/emit-assign.js +408 -0
- package/src/compile/emit.js +3201 -0
- package/src/compile/flow-types.js +103 -0
- package/src/{compile.js → compile/index.js} +497 -125
- package/src/{infer.js → compile/infer.js} +27 -98
- package/src/{narrow.js → compile/narrow.js} +302 -96
- package/src/compile/plan/advise.js +316 -0
- package/src/compile/plan/common.js +150 -0
- package/src/compile/plan/index.js +118 -0
- package/src/compile/plan/inline.js +679 -0
- package/src/compile/plan/literals.js +984 -0
- package/src/compile/plan/loops.js +472 -0
- package/src/compile/plan/scope.js +573 -0
- package/src/compile/program-facts.js +404 -0
- package/src/ctx.js +176 -58
- package/src/ir.js +540 -171
- package/src/kind-traits.js +105 -0
- package/src/kind.js +462 -0
- package/src/op-policy.js +57 -0
- package/src/{optimize.js → optimize/index.js} +1106 -446
- package/src/optimize/vectorize.js +1874 -0
- package/src/param-reps.js +65 -0
- package/src/parse.js +44 -0
- package/src/{prepare.js → prepare/index.js} +600 -205
- package/src/reps.js +115 -0
- package/src/resolve.js +12 -3
- package/src/static.js +199 -0
- package/src/type.js +647 -0
- package/src/{assemble.js → wat/assemble.js} +86 -48
- package/src/wat/optimize.js +3760 -0
- package/transform.js +21 -0
- package/wasi.js +47 -5
- package/src/analyze.js +0 -3818
- package/src/emit.js +0 -2997
- package/src/jzify.js +0 -1553
- package/src/plan.js +0 -2132
- package/src/vectorize.js +0 -1088
- /package/src/{codegen.js → wat/codegen.js} +0 -0
|
@@ -0,0 +1,472 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Row-length pad-loop unrolling — recognizes the `pad-the-shorter-rows` shape
|
|
3
|
+
* over a nested int-row table and emits the equivalent straight-line padding:
|
|
4
|
+
*
|
|
5
|
+
* const table = [[1,2,3], [4,5], [6]]
|
|
6
|
+
* for (let i = row.length; i < N; i++) row[i] = 0
|
|
7
|
+
*
|
|
8
|
+
* becomes the per-row unrolled assignments (no loop body, no comparisons), as
|
|
9
|
+
* long as the row's length is statically known (one of the entries in `table`)
|
|
10
|
+
* and the pad bound `N` is also static. The transform binds a `rowlen` lookup
|
|
11
|
+
* up front so reads of `table[i].length` collapse to scalar `rowlen[i]`.
|
|
12
|
+
*
|
|
13
|
+
* Two passes, each idempotent:
|
|
14
|
+
* - `bindNestedRowLengths` introduces / threads the `rowlen` binding
|
|
15
|
+
* - `unrollRowLenPadLoops` unrolls each qualifying pad loop in place
|
|
16
|
+
*
|
|
17
|
+
* @module compile/plan/loops
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { ctx } from '../../ctx.js'
|
|
21
|
+
import { stmtList, T, some, isReassigned, hasControlTransfer } from '../../ast.js'
|
|
22
|
+
import { constIntExpr } from '../../static.js'
|
|
23
|
+
import { containsDeclOf, cloneWithSubst, isUnitIncrement } from '../../type.js'
|
|
24
|
+
import { includeModule } from '../../autoload.js'
|
|
25
|
+
import { clonePlain, forLoopBodyIndex, withForLoopBody, collectBindings, nodeSize, optimizing } from './common.js'
|
|
26
|
+
|
|
27
|
+
// Nested int row literal: `[[1,2],[3]]` → { flat, starts, lens }.
|
|
28
|
+
const parseNestedIntRowLit = (expr) => {
|
|
29
|
+
if (!Array.isArray(expr) || expr[0] !== '[') return null
|
|
30
|
+
let rows = expr.slice(1)
|
|
31
|
+
if (rows.length === 1 && Array.isArray(rows[0]) && rows[0][0] === ',') rows = rows[0].slice(1)
|
|
32
|
+
if (!rows.length) return null
|
|
33
|
+
const flat = [], starts = [], lens = []
|
|
34
|
+
for (const row of rows) {
|
|
35
|
+
if (!Array.isArray(row) || row[0] !== '[') return null
|
|
36
|
+
starts.push(flat.length)
|
|
37
|
+
let elems = row.slice(1)
|
|
38
|
+
if (elems.length === 1 && Array.isArray(elems[0]) && elems[0][0] === ',') elems = elems[0].slice(1)
|
|
39
|
+
let rowLen = 0
|
|
40
|
+
for (const el of elems) {
|
|
41
|
+
const v = constIntExpr(el)
|
|
42
|
+
if (v == null) return null
|
|
43
|
+
flat.push(v)
|
|
44
|
+
rowLen++
|
|
45
|
+
}
|
|
46
|
+
lens.push(rowLen)
|
|
47
|
+
}
|
|
48
|
+
return { flat, starts, lens }
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const intRowLitIR = nums => ['[', ...nums.map(n => [null, n])]
|
|
52
|
+
|
|
53
|
+
const wrapStmtList = (stmts, orig) => {
|
|
54
|
+
if (Array.isArray(orig) && orig[0] === ';') return [';', ...stmts]
|
|
55
|
+
if (Array.isArray(orig) && orig[0] === '{}') return ['{}', [';', ...stmts]]
|
|
56
|
+
return [';', ...stmts]
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// `row[ci].length` on nested static int-row tables → `rowlen[ci]`.
|
|
60
|
+
// General: `const rows = [[…],…]; const row = rows[idx]; … row.length`.
|
|
61
|
+
export const bindNestedRowLengths = () => {
|
|
62
|
+
let changed = false
|
|
63
|
+
for (const func of ctx.func.list) {
|
|
64
|
+
if (!func.body || func.raw) continue
|
|
65
|
+
const r = bindNestedRowLengthsInBody(func.body)
|
|
66
|
+
if (r.changed) { func.body = r.node; changed = true }
|
|
67
|
+
}
|
|
68
|
+
return changed
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const bindNestedRowLengthsInBody = (body) => {
|
|
72
|
+
if (Array.isArray(body) && body[0] === '=>') {
|
|
73
|
+
const inner = bindNestedRowLengthsInBody(body[2])
|
|
74
|
+
if (!inner.changed) return { node: body, changed: false }
|
|
75
|
+
return { node: [body[0], body[1], inner.node], changed: true }
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const direct = bindNestedRowLengthsSeq(body)
|
|
79
|
+
if (direct.changed) return direct
|
|
80
|
+
|
|
81
|
+
const stmts = stmtList(body)
|
|
82
|
+
if (!stmts?.length) return { node: body, changed: false }
|
|
83
|
+
|
|
84
|
+
let changed = false
|
|
85
|
+
const out = []
|
|
86
|
+
for (const stmt of stmts) {
|
|
87
|
+
if (Array.isArray(stmt) && stmt[0] === 'while') {
|
|
88
|
+
const r = bindNestedRowLengthsInBody(stmt[2])
|
|
89
|
+
if (r.changed) { changed = true; out.push(['while', stmt[1], r.node]); continue }
|
|
90
|
+
}
|
|
91
|
+
if (Array.isArray(stmt) && stmt[0] === 'for') {
|
|
92
|
+
const idx = forLoopBodyIndex(stmt)
|
|
93
|
+
const r = bindNestedRowLengthsInBody(stmt[idx])
|
|
94
|
+
if (r.changed) { changed = true; out.push(withForLoopBody(stmt, r.node)); continue }
|
|
95
|
+
}
|
|
96
|
+
if (Array.isArray(stmt) && stmt[0] === 'if') {
|
|
97
|
+
const thenR = bindNestedRowLengthsInBody(stmt[2])
|
|
98
|
+
const elseR = stmt.length > 3 ? bindNestedRowLengthsInBody(stmt[3]) : null
|
|
99
|
+
if (thenR.changed || elseR?.changed) {
|
|
100
|
+
changed = true
|
|
101
|
+
out.push(stmt.length > 3 ? ['if', stmt[1], thenR.node, elseR.node] : ['if', stmt[1], thenR.node])
|
|
102
|
+
continue
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
out.push(stmt)
|
|
106
|
+
}
|
|
107
|
+
return changed ? { node: wrapStmtList(out, body), changed: true } : { node: body, changed: false }
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const bindNestedRowLengthsSeq = (body) => {
|
|
111
|
+
const stmts = stmtList(body)
|
|
112
|
+
if (!stmts) return { node: body, changed: false }
|
|
113
|
+
|
|
114
|
+
const progRows = new Map()
|
|
115
|
+
const rowAliases = new Map()
|
|
116
|
+
|
|
117
|
+
for (const stmt of stmts) {
|
|
118
|
+
if (!Array.isArray(stmt) || (stmt[0] !== 'let' && stmt[0] !== 'const') || stmt.length !== 2) continue
|
|
119
|
+
const decl = stmt[1]
|
|
120
|
+
if (!Array.isArray(decl) || decl[0] !== '=' || typeof decl[1] !== 'string') continue
|
|
121
|
+
const parsed = parseNestedIntRowLit(decl[2])
|
|
122
|
+
if (parsed) progRows.set(decl[1], parsed)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
for (const stmt of stmts) {
|
|
126
|
+
if (!Array.isArray(stmt) || (stmt[0] !== 'let' && stmt[0] !== 'const') || stmt.length !== 2) continue
|
|
127
|
+
const decl = stmt[1]
|
|
128
|
+
if (!Array.isArray(decl) || decl[0] !== '=' || typeof decl[1] !== 'string') continue
|
|
129
|
+
const rhs = decl[2]
|
|
130
|
+
if (!Array.isArray(rhs) || rhs[0] !== '[]' || typeof rhs[1] !== 'string') continue
|
|
131
|
+
const rows = progRows.get(rhs[1])
|
|
132
|
+
if (!rows) continue
|
|
133
|
+
rowAliases.set(decl[1], { prog: rhs[1], rowExpr: rhs[2], lens: rows.lens })
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (!rowAliases.size) return { node: body, changed: false }
|
|
137
|
+
|
|
138
|
+
const needsLen = (node) => {
|
|
139
|
+
if (!Array.isArray(node)) return false
|
|
140
|
+
const op = node[0]
|
|
141
|
+
if (op === '.' && typeof node[1] === 'string' && rowAliases.has(node[1]) && node[2] === 'length') return true
|
|
142
|
+
if (op === '[]' && typeof node[1] === 'string' && rowAliases.has(node[1])) {
|
|
143
|
+
const idx = node[2]
|
|
144
|
+
if (Array.isArray(idx) && idx[0] === '%' && Array.isArray(idx[2])
|
|
145
|
+
&& idx[2][0] === '.' && idx[2][1] === node[1] && idx[2][2] === 'length') return true
|
|
146
|
+
}
|
|
147
|
+
for (let i = 1; i < node.length; i++) if (needsLen(node[i])) return true
|
|
148
|
+
return false
|
|
149
|
+
}
|
|
150
|
+
if (!stmts.some(s => needsLen(s))) return { node: body, changed: false }
|
|
151
|
+
|
|
152
|
+
const rowIndexExpr = (rowExpr, progName) =>
|
|
153
|
+
(Array.isArray(rowExpr) && rowExpr[0] === '[]' && rowExpr[1] === progName)
|
|
154
|
+
? clonePlain(rowExpr[2]) : clonePlain(rowExpr)
|
|
155
|
+
|
|
156
|
+
const lensSyms = new Map()
|
|
157
|
+
for (const alias of rowAliases.values()) {
|
|
158
|
+
if (lensSyms.has(alias.prog)) continue
|
|
159
|
+
const id = ctx.func.uniq++
|
|
160
|
+
lensSyms.set(alias.prog, { name: `${T}rowlen${id}`, lens: alias.lens })
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const rewrite = (node) => {
|
|
164
|
+
if (!Array.isArray(node)) return node
|
|
165
|
+
const op = node[0]
|
|
166
|
+
if (op === '=>') {
|
|
167
|
+
const inner = rewrite(node[2])
|
|
168
|
+
return inner === node[2] ? node : [node[0], node[1], inner]
|
|
169
|
+
}
|
|
170
|
+
if (op === '.' && typeof node[1] === 'string' && rowAliases.has(node[1]) && node[2] === 'length') {
|
|
171
|
+
const { rowExpr, prog } = rowAliases.get(node[1])
|
|
172
|
+
return ['[]', lensSyms.get(prog).name, rowIndexExpr(rowExpr, prog)]
|
|
173
|
+
}
|
|
174
|
+
if (op === '[]' && typeof node[1] === 'string' && rowAliases.has(node[1])) {
|
|
175
|
+
const alias = rowAliases.get(node[1])
|
|
176
|
+
const idx = node[2]
|
|
177
|
+
if (Array.isArray(idx) && idx[0] === '%' && Array.isArray(idx[2])
|
|
178
|
+
&& idx[2][0] === '.' && idx[2][1] === node[1] && idx[2][2] === 'length') {
|
|
179
|
+
const tab = lensSyms.get(alias.prog)
|
|
180
|
+
return ['%', rewrite(idx[1]), ['[]', tab.name, rowIndexExpr(alias.rowExpr, alias.prog)]]
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
return node.map((part, i) => i === 0 ? part : rewrite(part))
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const prologue = [...lensSyms.values()].map(tab =>
|
|
187
|
+
['const', ['=', tab.name, intRowLitIR(tab.lens)]])
|
|
188
|
+
|
|
189
|
+
const out = [...prologue]
|
|
190
|
+
for (const stmt of stmts) out.push(rewrite(stmt))
|
|
191
|
+
return { node: wrapStmtList(out, body), changed: true }
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// `for (i < rowlen[ci])` inner loops — full unroll when lens are uniform;
|
|
195
|
+
// min-length loop + one guarded tail when they vary (mixed static row lengths).
|
|
196
|
+
const MAX_ROWLEN_PAD_UNROLL = 8
|
|
197
|
+
|
|
198
|
+
const parseFlatIntRowLit = (expr) => {
|
|
199
|
+
if (!Array.isArray(expr) || expr[0] !== '[') return null
|
|
200
|
+
let elems = expr.slice(1)
|
|
201
|
+
if (elems.length === 1 && Array.isArray(elems[0]) && elems[0][0] === ',') elems = elems[0].slice(1)
|
|
202
|
+
const lens = []
|
|
203
|
+
for (const el of elems) {
|
|
204
|
+
const v = constIntExpr(el)
|
|
205
|
+
if (v == null || v < 0) return null
|
|
206
|
+
lens.push(v)
|
|
207
|
+
}
|
|
208
|
+
return lens.length ? lens : null
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const collectRowLenTables = (stmts) => {
|
|
212
|
+
const tables = new Map()
|
|
213
|
+
for (const stmt of stmts) {
|
|
214
|
+
if (!Array.isArray(stmt) || (stmt[0] !== 'let' && stmt[0] !== 'const') || stmt.length !== 2) continue
|
|
215
|
+
const decl = stmt[1]
|
|
216
|
+
if (!Array.isArray(decl) || decl[0] !== '=' || typeof decl[1] !== 'string') continue
|
|
217
|
+
const lens = parseFlatIntRowLit(decl[2])
|
|
218
|
+
if (lens) tables.set(decl[1], { lens, min: Math.min(...lens), max: Math.max(...lens) })
|
|
219
|
+
}
|
|
220
|
+
return tables
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const parseRowLenBound = (expr) => {
|
|
224
|
+
if (Array.isArray(expr) && expr[0] === '[]' && typeof expr[1] === 'string') {
|
|
225
|
+
return { rowlen: expr[1], ci: expr[2] }
|
|
226
|
+
}
|
|
227
|
+
if (typeof expr === 'string') return { lenVar: expr }
|
|
228
|
+
return null
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const rowLenBoundFromHoist = (init, lenVar) => {
|
|
232
|
+
const scan = (node) => {
|
|
233
|
+
if (!Array.isArray(node)) return null
|
|
234
|
+
if (node[0] === 'let' && node.length === 2) {
|
|
235
|
+
const decl = node[1]
|
|
236
|
+
if (Array.isArray(decl) && decl[0] === '=' && decl[1] === lenVar) {
|
|
237
|
+
const rhs = decl[2]
|
|
238
|
+
if (Array.isArray(rhs) && (rhs[0] === '|' || rhs[0] === '>>>' || rhs[0] === '&') && rhs.length === 3) {
|
|
239
|
+
const inner = parseRowLenBound(rhs[1])
|
|
240
|
+
if (inner?.rowlen) return inner
|
|
241
|
+
}
|
|
242
|
+
return parseRowLenBound(rhs)
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
if (node[0] === ';') {
|
|
246
|
+
for (let i = 1; i < node.length; i++) {
|
|
247
|
+
const r = scan(node[i])
|
|
248
|
+
if (r) return r
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
return null
|
|
252
|
+
}
|
|
253
|
+
return scan(init)
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const parseForIncStep = (step, idx) => {
|
|
257
|
+
if (Array.isArray(step) && step[0] === '++' && step[1] === idx) return true
|
|
258
|
+
// `i++` in a for-head step preps to ['-', ['++', i], 1].
|
|
259
|
+
return Array.isArray(step) && step[0] === '-' && Array.isArray(step[1])
|
|
260
|
+
&& step[1][0] === '++' && step[1][1] === idx && constIntExpr(step[2]) === 1
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const parseRowLenForTrip = (init, cond, step) => {
|
|
264
|
+
let idx = null
|
|
265
|
+
if (Array.isArray(init) && init[0] === 'let' && init.length === 2) {
|
|
266
|
+
const decl = init[1]
|
|
267
|
+
if (Array.isArray(decl) && decl[0] === '=' && typeof decl[1] === 'string' && constIntExpr(decl[2]) === 0)
|
|
268
|
+
idx = decl[1]
|
|
269
|
+
} else if (Array.isArray(init) && init[0] === ';') {
|
|
270
|
+
for (let i = 1; i < init.length; i++) {
|
|
271
|
+
const s = init[i]
|
|
272
|
+
if (Array.isArray(s) && s[0] === 'let' && s.length === 2) {
|
|
273
|
+
const decl = s[1]
|
|
274
|
+
if (Array.isArray(decl) && decl[0] === '=' && typeof decl[1] === 'string' && constIntExpr(decl[2]) === 0) {
|
|
275
|
+
idx = decl[1]
|
|
276
|
+
break
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
if (!idx) return null
|
|
282
|
+
if (!Array.isArray(cond) || cond[0] !== '<' || cond[1] !== idx) return null
|
|
283
|
+
if (!parseForIncStep(step, idx)) return null
|
|
284
|
+
|
|
285
|
+
let bound = parseRowLenBound(cond[2])
|
|
286
|
+
if (bound?.lenVar) {
|
|
287
|
+
const resolved = rowLenBoundFromHoist(init, bound.lenVar)
|
|
288
|
+
if (!resolved) return null
|
|
289
|
+
bound = resolved
|
|
290
|
+
}
|
|
291
|
+
if (!bound?.rowlen) return null
|
|
292
|
+
return { idx, rowlen: bound.rowlen, ci: bound.ci }
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
const tryUnrollRowLenFor = (forNode, tables) => {
|
|
296
|
+
if (!Array.isArray(forNode) || forNode[0] !== 'for' || forNode.length !== 5) return null
|
|
297
|
+
const trip = parseRowLenForTrip(forNode[1], forNode[2], forNode[3])
|
|
298
|
+
if (!trip) return null
|
|
299
|
+
// `ci` must be a variable (outer loop index) — a constant index like `a[1]`
|
|
300
|
+
// means the bound is always the same value, so per-row unrolling doesn't apply.
|
|
301
|
+
if (typeof trip.ci !== 'string') return null
|
|
302
|
+
const tab = tables.get(trip.rowlen)
|
|
303
|
+
if (!tab || tab.max > MAX_ROWLEN_PAD_UNROLL || tab.max < 2) return null
|
|
304
|
+
const body = forNode[4]
|
|
305
|
+
const step = forNode[3]
|
|
306
|
+
if (hasControlTransfer(body) || containsDeclOf(body, trip.idx) || isReassigned(body, trip.idx)) return null
|
|
307
|
+
|
|
308
|
+
const bound = ['[]', trip.rowlen, clonePlain(trip.ci)]
|
|
309
|
+
const idxInit = ['let', ['=', trip.idx, [null, 0]]]
|
|
310
|
+
const out = []
|
|
311
|
+
|
|
312
|
+
if (tab.min === tab.max) {
|
|
313
|
+
for (let k = 0; k < tab.max; k++) {
|
|
314
|
+
out.push(cloneWithSubst(body, new Map([[trip.idx, [null, k]]]), new Map()))
|
|
315
|
+
}
|
|
316
|
+
} else {
|
|
317
|
+
// Variable row lengths (e.g. waltz 5/4/4/5): a short loop for the common
|
|
318
|
+
// prefix (min iterations), then one guarded tail per possible extra row
|
|
319
|
+
// length (min..max-1). The guards are monotonic in `k` — `bound` is loop-
|
|
320
|
+
// invariant across them — so they run exactly `bound` iterations in total
|
|
321
|
+
// for any `min <= bound <= max` (not just `max === min + 1`).
|
|
322
|
+
out.push(['for', idxInit, ['<', trip.idx, [null, tab.min]], step, body])
|
|
323
|
+
for (let k = tab.min; k < tab.max; k++) {
|
|
324
|
+
const tail = cloneWithSubst(body, new Map([[trip.idx, [null, k]]]), new Map())
|
|
325
|
+
out.push(['if', ['<', [null, k], clonePlain(bound)], tail])
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
return out.length === 1 ? out[0] : [';', ...out]
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
const unrollRowLenPadLoopsSeq = (body, outerTables = null) => {
|
|
332
|
+
const stmts = stmtList(body)
|
|
333
|
+
if (!stmts) return { node: body, changed: false }
|
|
334
|
+
|
|
335
|
+
const tables = new Map(outerTables)
|
|
336
|
+
for (const [name, tab] of collectRowLenTables(stmts)) tables.set(name, tab)
|
|
337
|
+
|
|
338
|
+
let changed = false
|
|
339
|
+
const out = []
|
|
340
|
+
for (const stmt of stmts) {
|
|
341
|
+
if (Array.isArray(stmt) && stmt[0] === 'for' && stmt.length === 5) {
|
|
342
|
+
const unrolled = tryUnrollRowLenFor(stmt, tables)
|
|
343
|
+
if (unrolled) { changed = true; out.push(unrolled); continue }
|
|
344
|
+
}
|
|
345
|
+
if (Array.isArray(stmt) && stmt[0] === 'while') {
|
|
346
|
+
const r = unrollRowLenPadLoopsInBody(stmt[2], tables)
|
|
347
|
+
if (r.changed) { changed = true; out.push(['while', stmt[1], r.node]); continue }
|
|
348
|
+
}
|
|
349
|
+
if (Array.isArray(stmt) && stmt[0] === 'for') {
|
|
350
|
+
const idx = forLoopBodyIndex(stmt)
|
|
351
|
+
if (idx != null) {
|
|
352
|
+
const r = unrollRowLenPadLoopsInBody(stmt[idx], tables)
|
|
353
|
+
if (r.changed) { changed = true; out.push(withForLoopBody(stmt, r.node)); continue }
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
if (Array.isArray(stmt) && stmt[0] === 'if') {
|
|
357
|
+
const thenR = unrollRowLenPadLoopsInBody(stmt[2], tables)
|
|
358
|
+
const elseR = stmt.length > 3 ? unrollRowLenPadLoopsInBody(stmt[3], tables) : null
|
|
359
|
+
if (thenR.changed || elseR?.changed) {
|
|
360
|
+
changed = true
|
|
361
|
+
out.push(stmt.length > 3 ? ['if', stmt[1], thenR.node, elseR.node] : ['if', stmt[1], thenR.node])
|
|
362
|
+
continue
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
out.push(stmt)
|
|
366
|
+
}
|
|
367
|
+
return changed ? { node: wrapStmtList(out, body), changed: true } : { node: body, changed: false }
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
const unrollRowLenPadLoopsInBody = (body, outerTables = null) => {
|
|
371
|
+
if (Array.isArray(body) && body[0] === '=>') {
|
|
372
|
+
const inner = unrollRowLenPadLoopsInBody(body[2], outerTables)
|
|
373
|
+
if (!inner.changed) return { node: body, changed: false }
|
|
374
|
+
return { node: [body[0], body[1], inner.node], changed: true }
|
|
375
|
+
}
|
|
376
|
+
return unrollRowLenPadLoopsSeq(body, outerTables)
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
export const unrollRowLenPadLoops = () => {
|
|
380
|
+
let changed = false
|
|
381
|
+
for (const func of ctx.func.list) {
|
|
382
|
+
if (!func.body || func.raw) continue
|
|
383
|
+
const r = unrollRowLenPadLoopsInBody(func.body)
|
|
384
|
+
if (r.changed) { func.body = r.node; changed = true }
|
|
385
|
+
}
|
|
386
|
+
return changed
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// === Char-scan range splitting ===
|
|
390
|
+
//
|
|
391
|
+
// `for (let i = 0; i < N; i++) { … s.charCodeAt(i) … }` with an arbitrary
|
|
392
|
+
// bound N pays the OOB arm on EVERY char: charCodeAt's `i ≥ s.length` case
|
|
393
|
+
// yields NaN, which forces the char local to f64 and every classifier compare
|
|
394
|
+
// to f64 ops (the tokenizer/watr substrate cost). Split the iteration space:
|
|
395
|
+
//
|
|
396
|
+
// for (let __ccm = Math.min(N, s.length), i = 0; i < __ccm; i++) BODY
|
|
397
|
+
// for (let i2 = Math.ceil(__ccm); i2 < N; i2++) BODY′
|
|
398
|
+
//
|
|
399
|
+
// The main loop's bound proof (scanBoundedLoops, via the min-aware lengthRecv)
|
|
400
|
+
// makes charCodeAt emit the raw i32 fast path — the char carrier narrows to
|
|
401
|
+
// i32 and the classifier chain follows. The tail keeps the original NaN
|
|
402
|
+
// semantics and only runs past the string's end (cold). `Math.ceil(__ccm)` is
|
|
403
|
+
// exactly the main loop's exit value (unit increments from 0): integral min →
|
|
404
|
+
// min itself, fractional → first integer above, NaN → NaN (zero-trip tail,
|
|
405
|
+
// matching the original's zero trips). BODY′ renames the body's decls and the
|
|
406
|
+
// induction var so the main loop's narrowed i32 locals aren't widened by the
|
|
407
|
+
// tail's NaN arm (locals are per-name function-wide).
|
|
408
|
+
//
|
|
409
|
+
// Gates: unit-increment counter from literal 0, name bound, no break/label
|
|
410
|
+
// (a main-loop break must not fall into the tail), no closures (cloning would
|
|
411
|
+
// duplicate them), receiver/bound/counter unwritten in body, body ≤ 400 nodes
|
|
412
|
+
// (the clone doubles it — `splitCharScan: false` under optimize:'size').
|
|
413
|
+
const hasOp = (node, ops) => some(node, n => ops.has(n[0]))
|
|
414
|
+
const SPLIT_BLOCKERS = new Set(['break', 'label', '=>'])
|
|
415
|
+
|
|
416
|
+
const trySplitFor = (node, parent, idx) => {
|
|
417
|
+
const [, init, cond, step, body] = node
|
|
418
|
+
if (!Array.isArray(cond) || cond[0] !== '<' || typeof cond[1] !== 'string' || typeof cond[2] !== 'string') return false
|
|
419
|
+
const i = cond[1], B = cond[2]
|
|
420
|
+
if (B === i) return false
|
|
421
|
+
// init: single decl `let i = 0`
|
|
422
|
+
if (!Array.isArray(init) || (init[0] !== 'let' && init[0] !== 'const') || init.length !== 2) return false
|
|
423
|
+
const iDecl = init[1]
|
|
424
|
+
if (!Array.isArray(iDecl) || iDecl[0] !== '=' || iDecl[1] !== i || constIntExpr(iDecl[2]) !== 0) return false
|
|
425
|
+
if (!isUnitIncrement(step, i)) return false
|
|
426
|
+
if (hasOp(body, SPLIT_BLOCKERS)) return false
|
|
427
|
+
if (nodeSize(body) > 250) return false
|
|
428
|
+
if (isReassigned(body, i) || isReassigned(body, B) || containsDeclOf(body, i)) return false
|
|
429
|
+
// at least one `recv.charCodeAt(i)` with a stable name receiver
|
|
430
|
+
let recv = null
|
|
431
|
+
some(body, n => {
|
|
432
|
+
if (n[0] === '()' && Array.isArray(n[1]) && n[1][0] === '.' && n[1][2] === 'charCodeAt'
|
|
433
|
+
&& typeof n[1][1] === 'string' && n[2] === i) { recv = n[1][1]; return true }
|
|
434
|
+
return false
|
|
435
|
+
})
|
|
436
|
+
if (!recv || recv === i || recv === B || isReassigned(body, recv)) return false
|
|
437
|
+
|
|
438
|
+
// tail clone: fresh induction var + fresh names for every body decl
|
|
439
|
+
const declNames = new Set()
|
|
440
|
+
collectBindings(body, declNames)
|
|
441
|
+
const rename = new Map()
|
|
442
|
+
for (const d of declNames) rename.set(d, `${T}rs${ctx.func.uniq++}_${d}`)
|
|
443
|
+
const i2 = `${T}rsi${ctx.func.uniq++}`
|
|
444
|
+
const subst = new Map([[i, i2]])
|
|
445
|
+
const tailBody = cloneWithSubst(body, subst, rename)
|
|
446
|
+
const tailStep = cloneWithSubst(step, subst, new Map())
|
|
447
|
+
|
|
448
|
+
includeModule('math') // plan-time injection of math.min/math.ceil — prepare's auto-import has already run
|
|
449
|
+
const M = `${T}ccm${ctx.func.uniq++}`
|
|
450
|
+
node[1] = ['let', ['=', M, ['()', 'math.min', [',', B, ['.', recv, 'length']]]], iDecl]
|
|
451
|
+
node[2] = ['<', i, M]
|
|
452
|
+
const tailFor = ['for', ['let', ['=', i2, ['()', 'math.ceil', M]]], ['<', i2, B], tailStep, tailBody]
|
|
453
|
+
parent[idx] = [';', node, tailFor]
|
|
454
|
+
return true
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
export const splitCharScanLoops = () => {
|
|
458
|
+
if (!optimizing() || ctx.transform.optimize?.splitCharScan === false) return false
|
|
459
|
+
let changed = false
|
|
460
|
+
const visit = (node, parent, idx) => {
|
|
461
|
+
if (!Array.isArray(node) || node[0] === '=>') return
|
|
462
|
+
if (node[0] === 'for' && node.length === 5 && parent && trySplitFor(node, parent, idx)) { changed = true; return }
|
|
463
|
+
for (let k = 1; k < node.length; k++) visit(node[k], node, k)
|
|
464
|
+
}
|
|
465
|
+
for (const func of ctx.func.list) {
|
|
466
|
+
if (func.raw || !func.body) continue
|
|
467
|
+
visit(func.body, null, -1)
|
|
468
|
+
// body root itself can't be a bare `for` without a parent slot — wrap-walk
|
|
469
|
+
// handles every nested position; a top-level-for body is `{}`-wrapped.
|
|
470
|
+
}
|
|
471
|
+
return changed
|
|
472
|
+
}
|