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.
Files changed (80) hide show
  1. package/README.md +288 -314
  2. package/bench/README.md +319 -0
  3. package/bench/bench.svg +112 -0
  4. package/cli.js +32 -24
  5. package/index.js +177 -55
  6. package/interop.js +88 -159
  7. package/jz.svg +5 -0
  8. package/jzify/arguments.js +97 -0
  9. package/jzify/bundler.js +382 -0
  10. package/jzify/classes.js +328 -0
  11. package/jzify/hoist-vars.js +177 -0
  12. package/jzify/index.js +51 -0
  13. package/jzify/names.js +37 -0
  14. package/jzify/switch.js +106 -0
  15. package/jzify/transform.js +349 -0
  16. package/layout.js +179 -0
  17. package/module/array.js +322 -153
  18. package/module/collection.js +603 -145
  19. package/module/console.js +55 -43
  20. package/module/core.js +281 -142
  21. package/module/date.js +15 -3
  22. package/module/function.js +73 -6
  23. package/module/index.js +2 -1
  24. package/module/json.js +226 -61
  25. package/module/math.js +461 -185
  26. package/module/number.js +306 -60
  27. package/module/object.js +448 -184
  28. package/module/regex.js +255 -25
  29. package/module/schema.js +24 -6
  30. package/module/simd.js +85 -0
  31. package/module/string.js +591 -220
  32. package/module/symbol.js +1 -1
  33. package/module/timer.js +9 -14
  34. package/module/typedarray.js +45 -48
  35. package/package.json +41 -12
  36. package/src/abi/index.js +39 -23
  37. package/src/abi/string.js +38 -41
  38. package/src/ast.js +460 -0
  39. package/src/autoload.js +26 -24
  40. package/src/bridge.js +111 -0
  41. package/src/compile/analyze-scans.js +661 -0
  42. package/src/compile/analyze.js +1565 -0
  43. package/src/compile/emit-assign.js +408 -0
  44. package/src/compile/emit.js +3201 -0
  45. package/src/compile/flow-types.js +103 -0
  46. package/src/{compile.js → compile/index.js} +497 -125
  47. package/src/{infer.js → compile/infer.js} +27 -98
  48. package/src/{narrow.js → compile/narrow.js} +302 -96
  49. package/src/compile/plan/advise.js +316 -0
  50. package/src/compile/plan/common.js +150 -0
  51. package/src/compile/plan/index.js +118 -0
  52. package/src/compile/plan/inline.js +679 -0
  53. package/src/compile/plan/literals.js +984 -0
  54. package/src/compile/plan/loops.js +472 -0
  55. package/src/compile/plan/scope.js +573 -0
  56. package/src/compile/program-facts.js +404 -0
  57. package/src/ctx.js +176 -58
  58. package/src/ir.js +540 -171
  59. package/src/kind-traits.js +105 -0
  60. package/src/kind.js +462 -0
  61. package/src/op-policy.js +57 -0
  62. package/src/{optimize.js → optimize/index.js} +1106 -446
  63. package/src/optimize/vectorize.js +1874 -0
  64. package/src/param-reps.js +65 -0
  65. package/src/parse.js +44 -0
  66. package/src/{prepare.js → prepare/index.js} +600 -205
  67. package/src/reps.js +115 -0
  68. package/src/resolve.js +12 -3
  69. package/src/static.js +199 -0
  70. package/src/type.js +647 -0
  71. package/src/{assemble.js → wat/assemble.js} +86 -48
  72. package/src/wat/optimize.js +3760 -0
  73. package/transform.js +21 -0
  74. package/wasi.js +47 -5
  75. package/src/analyze.js +0 -3818
  76. package/src/emit.js +0 -2997
  77. package/src/jzify.js +0 -1553
  78. package/src/plan.js +0 -2132
  79. package/src/vectorize.js +0 -1088
  80. /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
+ }