@asmlift/core 0.4.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 (87) hide show
  1. package/README.md +22 -16
  2. package/package.json +1 -1
  3. package/src/backend/c.ts +1 -0
  4. package/src/backend/cfamily.ts +238 -164
  5. package/src/backend/cpp.ts +1 -0
  6. package/src/backend/pascal.ts +26 -12
  7. package/src/contracts.ts +341 -22
  8. package/src/declare.ts +41 -4
  9. package/src/frontend/mips.ts +24 -6
  10. package/src/frontend/opaque.ts +31 -18
  11. package/src/frontend/ppc.ts +54 -7
  12. package/src/frontend/ssa.ts +632 -13
  13. package/src/frontend/thumb.ts +2786 -286
  14. package/src/ir/alias.ts +129 -0
  15. package/src/ir/bits.ts +75 -0
  16. package/src/ir/core.ts +337 -2
  17. package/src/ir/opcodes.ts +156 -27
  18. package/src/ir/parse.ts +19 -2
  19. package/src/ir/print.ts +27 -2
  20. package/src/ir/simplify.ts +190 -3
  21. package/src/ir/struct-names.ts +42 -0
  22. package/src/ir/verify.ts +43 -49
  23. package/src/l3/address.ts +62 -0
  24. package/src/l3/argbase.ts +8 -2
  25. package/src/l3/ast.ts +464 -49
  26. package/src/l3/basecse.ts +709 -88
  27. package/src/l3/coalesce.ts +521 -66
  28. package/src/l3/dce.ts +54 -19
  29. package/src/l3/gates.ts +88 -0
  30. package/src/l3/hoist.ts +293 -14
  31. package/src/l3/homesplit.ts +285 -0
  32. package/src/l3/initfirst.ts +301 -0
  33. package/src/l3/inlinebase.ts +193 -0
  34. package/src/l3/mentions.ts +113 -0
  35. package/src/l3/mulfirst.ts +42 -0
  36. package/src/l3/nearbase.ts +152 -0
  37. package/src/l3/offmember.ts +371 -0
  38. package/src/l3/parkfirst.ts +96 -0
  39. package/src/l3/pollguard.ts +154 -0
  40. package/src/l3/ptrfield.ts +227 -0
  41. package/src/l3/regspell.ts +110 -85
  42. package/src/l3/reindex.ts +715 -78
  43. package/src/l3/scopebase.ts +649 -219
  44. package/src/l3/sinkinit.ts +40 -0
  45. package/src/l3/slotorder.ts +123 -0
  46. package/src/l3/storage.ts +48 -0
  47. package/src/l3/symbol-refs.ts +41 -8
  48. package/src/l3/tailmerge.ts +23 -4
  49. package/src/l3/typing.ts +198 -9
  50. package/src/l3/unmerge.ts +263 -0
  51. package/src/l3/unreduce.ts +971 -0
  52. package/src/l3/volatileptr.ts +207 -0
  53. package/src/l3/volatileval.ts +130 -0
  54. package/src/l3/volstore.ts +229 -0
  55. package/src/l3/zerosub.ts +62 -0
  56. package/src/pattern/engine.ts +236 -13
  57. package/src/pipeline.ts +206 -49
  58. package/src/proto.ts +112 -14
  59. package/src/raise/arrays.ts +6 -1
  60. package/src/raise/divpow2.ts +4 -3
  61. package/src/raise/globalshape.ts +1038 -0
  62. package/src/raise/gvn.ts +44 -19
  63. package/src/raise/latch.ts +126 -0
  64. package/src/raise/memberarrays.ts +594 -0
  65. package/src/raise/narrow.ts +124 -0
  66. package/src/raise/narrowlocal.ts +556 -0
  67. package/src/raise/paramwidth.ts +179 -0
  68. package/src/raise/pre-recovery.ts +101 -16
  69. package/src/raise/recover.ts +56 -23
  70. package/src/raise/retsink.ts +215 -14
  71. package/src/raise/shortcircuit.ts +477 -79
  72. package/src/raise/struct-arrays.ts +21 -3
  73. package/src/raise/structs.ts +61 -3
  74. package/src/rank-axes.ts +630 -0
  75. package/src/rank-declare.ts +256 -0
  76. package/src/rank.ts +1726 -251
  77. package/src/structure/analysis.ts +1516 -220
  78. package/src/structure/bitfields.ts +332 -0
  79. package/src/structure/globalaccess.ts +274 -0
  80. package/src/structure/hazards.ts +411 -20
  81. package/src/structure/loops.ts +2 -49
  82. package/src/structure/namecoalesce.ts +435 -0
  83. package/src/structure/structure.ts +2850 -533
  84. package/src/structure/switch-recover.ts +688 -147
  85. package/src/symbols.ts +62 -1
  86. package/src/target.ts +367 -24
  87. package/src/trace.ts +111 -32
package/src/l3/reindex.ts CHANGED
@@ -20,27 +20,103 @@
20
20
  // for k 0), the bound → `i <op> N`, the step → `i = i + 1`. Everything else declines — the
21
21
  // function keeps only its walk spelling, and no candidate is emitted.
22
22
  //
23
- // MEASURED GAP (2026-07-17, benchmark survey): this v1 template fires on ZERO current nonmatch
24
- // rows — the real agbcc shape for `for(i=0;i<n;i++) a[i]` is a GUARDED COUNTDOWN do-while with
25
- // TWIN induction vars (`if (0 >= n) {...} else { p = a; k = n; do { ...*p...; p += 1; k -= 1 }
26
- // while (k != 0) }`, e.g. synthetic:countpos at diff 5). Re-deriving the counted `for` from that
27
- // form needs guard-branch merging + induction-variable unification the v2 recognizer, a
28
- // candidate for the capability-ROI queue. The MECHANISM (candidates + boundary contracts +
29
- // differ referee) is what this module establishes; the recognizer set grows against measured
30
- // shapes.
23
+ // v2 SCOPE the GUARDED COUNTDOWN, the shape agbcc actually emits for `for(i=0;i<n;i++) a[i]`:
24
+ //
25
+ // if (0 >= n) { C } else { C ⊎ { p = B; k = n }; do { BODY; p += 1; k -= 1 } while (k != 0) }
26
+ //
27
+ // re-spells as `C; p = B; for (i = 0; i < n; i++) BODY[p[c] → p[i + c]]`. The `for` tests at top,
28
+ // so the guard disappears (it IS the for-condition at entry), and the skip arm C disappears with
29
+ // it (its statements are the loop-preceding subset of the else arm, now unconditionally hoisted).
30
+ // The walk pointers KEEP their init and lose their step — gcc folds a loop-invariant pointer
31
+ // local into addressing, so `p = B; … p[i]` and `B[i]` compile identically, and keeping the
32
+ // local is what lets the `/volatile` lever qualify a numeric B. Several walk pointers share the
33
+ // one counter (`dotprod`'s a/b pair). Its OWN rules, on top of the shared table below —
34
+ // • the guard tests THE SAME var the counter is initialised from, against 0, in the sense that
35
+ // skips the loop; the do-while exit is exactly `k != 0`;
36
+ // • the skip arm's statements equal, in order, the else arm's loop-preceding statements minus
37
+ // the induction inits — anything left over means the arms are not the same computation;
38
+ // • n is never assigned in the function (a moving bound has no single trip count).
39
+ // Everything else declines — the function keeps its countdown spelling, no candidate emitted.
40
+ //
41
+ // THE SHARED ADMISSION TABLE (`COUNTDOWN_GATES`) — the rules v2 and v4 both answer, as data, so
42
+ // "is this rule load-bearing?" is an ablation a test runs rather than an argument a reader
43
+ // audits. Each entry's `why` is its label; the argument is here:
44
+ // • the counter is INTEGER-typed. A pointer's `k - 1` strides its element size, so the trip
45
+ // count the rewrite computes would be a different loop's;
46
+ // • the counter is a DECLARED local or param. The rewrite deletes its init and its decrement,
47
+ // licensed by the four-roles rule proving nothing in THIS function reads it afterwards — not
48
+ // a property a global has, whose final value every other caller, ISR and translation unit
49
+ // observes. structure.ts spells a store to a bare scalar global as an `assign` like any
50
+ // other, so a global counter arrives here looking exactly like a local one;
51
+ // • neither the counter nor any walk pointer is declared `volatile`. Deleting the init, the
52
+ // decrement or a `p += 1` deletes accesses another agent is expected to see — a DMA-published
53
+ // frame slot (structure.ts stamps those) is the live shape;
54
+ // • the counter appears in EXACTLY its four roles (init write, decrement write + read, exit
55
+ // read), counted over the whole function; any other use — a leftover's read, the body's, a
56
+ // nested loop's, or an ESCAPED ADDRESS (`&k`, which the mention count sees) — would survive
57
+ // the deletion;
58
+ // • no walk pointer is stepped TWICE in one body — two `p += 1` stride 2, which `p[i]` does
59
+ // not. (That the tail holds a decrement and at least one step is shape recognition, not an
60
+ // admission rule: without them there is no countdown here to refuse.);
61
+ // • each walk pointer has an init ahead of the loop whose base the rewrite can leave standing:
62
+ // a var, or a rematerializable address (see `rematerializableAddress`);
63
+ // • every p is mentioned only inside the loop shape, and never by a leftover statement — a
64
+ // leftover outlives the deleted step, and its skip-arm twin reads a pointer that path never
65
+ // initialised;
66
+ // • every deref of a walk pointer reads its own element size (a `*(u8 *)p` over an `s32 *`
67
+ // walk strides 4 as a walk but 1 re-indexed — the v1 stride rule, same reason);
68
+ // • BODY has no `break`/`continue` targeting this loop — the original steps sat in the body's
69
+ // tail, which a `continue` skips, but a `for`'s inc runs: the two shapes genuinely differ.
70
+ // Past the table, the counter's DECLARATION is retired with its writes: nothing mentions it any
71
+ // more, and a declared-but-untouched local reads as the deliberate `uninit` slot it is not.
72
+ //
73
+ // v3 SCOPE — the up-counting BYTE walk with an EXPRESSION base (`p = (u8 *)(EXPR)` ahead of a
74
+ // counter-carried do-while): full rules at `tryExprWalk` below.
75
+ //
76
+ // v4 SCOPE — the UNGUARDED countdown, the shape agbcc emits for `for(i=0;i<C;i++) a[i]` with a
77
+ // LITERAL bound. A constant trip count needs no zero-trip guard, and the exit test agbcc writes
78
+ // for it is `>= 0` rather than v2's `!= 0`:
79
+ //
80
+ // p = B; k = C; do { BODY; p += 1; k -= 1 } while (k >= 0)
81
+ //
82
+ // re-spells as `p = B; for (i = 0; i < C + 1; i++) BODY[p[c] → p[i + c]]` — k counts C…0, so the
83
+ // body runs C + 1 times. Everything downstream of the counter's init is v2's (respellCountdown);
84
+ // what this shape adds is its own two rules —
85
+ // • the counter is declared SIGNED. `k >= 0` never fails for an unsigned k, so an unsigned
86
+ // declaration describes a loop that does not terminate, and `C + 1` would be a fiction;
87
+ // • C is a constant in [0, S32_MAX). Below zero `k = -1` runs the body once rather than
88
+ // `C + 1 = 0` times; at the top C + 1 wraps negative and the `for` runs zero times.
89
+ // The induction inits are read out of the statements PRECEDING the do-while in its own list
90
+ // rather than out of a guard arm, and the counter's init is the only one the rewrite deletes.
31
91
  import { IrType, T } from '../ir/types';
32
- import { Expr, SFn, Stmt, mapExprChildren, stmtExprs } from './ast';
92
+ import {
93
+ Expr,
94
+ SFn,
95
+ Stmt,
96
+ mapExprChildren,
97
+ mapStmtExprs,
98
+ rematerializableAddress,
99
+ stmtChildren,
100
+ stmtExprs,
101
+ } from './ast';
102
+ import { type Gate, firstRejection } from './gates';
103
+ import { takenNames } from './hoist';
104
+ import { nameStorage } from './storage';
105
+ import { type VarTypes, declaredTypes } from './typing';
33
106
 
34
107
  interface WalkLoop {
35
108
  p: string; // the pointer induction var
36
109
  base: string; // the var `p` was initialised from
37
110
  }
38
111
 
39
- /** Total mentions of `name` across a statement list (reads, writes, everywhere). */
112
+ /** Total mentions of `name` across a statement list (reads, writes, everywhere). `addr` counts:
113
+ * `&x` is how a frame local's address renders (structure.ts laddrName), and an escaped address
114
+ * is a read of the object by whatever holds it — the accounting these recognizers rest on is
115
+ * "nothing else in the function touches this name", which a var-only count cannot say. */
40
116
  function countMentions(stmts: Stmt[], name: string): number {
41
117
  let n = 0;
42
118
  const inExpr = (e: Expr): void => {
43
- if (e.k === 'var' && e.name === name) {
119
+ if ((e.k === 'var' || e.k === 'addr') && e.name === name) {
44
120
  n++;
45
121
  }
46
122
  mapExprChildren(e, (c) => {
@@ -53,17 +129,7 @@ function countMentions(stmts: Stmt[], name: string): number {
53
129
  n++;
54
130
  }
55
131
  stmtExprs(s).forEach(inExpr);
56
- const kids: Stmt[] =
57
- s.k === 'if'
58
- ? [...s.then, ...s.else]
59
- : s.k === 'while' || s.k === 'dowhile'
60
- ? s.body
61
- : s.k === 'for'
62
- ? [s.init, s.inc, ...s.body]
63
- : s.k === 'switch'
64
- ? [...s.cases.flatMap((c) => c.body), ...(s.default ?? [])]
65
- : [];
66
- kids.forEach(inStmt);
132
+ stmtChildren(s).forEach(inStmt);
67
133
  };
68
134
  stmts.forEach(inStmt);
69
135
  return n;
@@ -96,25 +162,15 @@ function derefWidths(stmts: Stmt[], p: string): number[] {
96
162
  };
97
163
  const inStmt = (s: Stmt): void => {
98
164
  stmtExprs(s).forEach(inExpr);
99
- const kids: Stmt[] =
100
- s.k === 'if'
101
- ? [...s.then, ...s.else]
102
- : s.k === 'while' || s.k === 'dowhile'
103
- ? s.body
104
- : s.k === 'for'
105
- ? [s.init, s.inc, ...s.body]
106
- : s.k === 'switch'
107
- ? [...s.cases.flatMap((c) => c.body), ...(s.default ?? [])]
108
- : [];
109
- kids.forEach(inStmt);
165
+ stmtChildren(s).forEach(inStmt);
110
166
  };
111
167
  stmts.forEach(inStmt);
112
168
  return out;
113
169
  }
114
170
 
115
- /** Does `e` mention var `name` anywhere? */
171
+ /** Does `e` mention `name` anywhere — as a value or as an escaped address (see countMentions)? */
116
172
  function mentionsVar(e: Expr, name: string): boolean {
117
- if (e.k === 'var') {
173
+ if (e.k === 'var' || e.k === 'addr') {
118
174
  return e.name === name;
119
175
  }
120
176
  let found = false;
@@ -125,26 +181,151 @@ function mentionsVar(e: Expr, name: string): boolean {
125
181
  return found;
126
182
  }
127
183
 
184
+ /** Is `name` the target of any assignment, at any depth? */
185
+ function stmtAssigns(s: Stmt, name: string): boolean {
186
+ if (s.k === 'assign' && s.name === name) {
187
+ return true;
188
+ }
189
+ return stmtChildren(s).some((x) => stmtAssigns(x, name));
190
+ }
191
+
192
+ /** One countdown loop as the shared admission rules read it: every fact collected before any rule
193
+ * runs. What is NOT here is the shape recognition — a body tail holding a decrement and at least
194
+ * one pointer step — because without it there is no countdown to admit or refuse. */
195
+ export interface CountdownCtx {
196
+ /** the counter's declaration, as the tree spells it */
197
+ kIsPointer: boolean;
198
+ kIsDeclared: boolean;
199
+ kIsVolatile: boolean;
200
+ /** a walk pointer stepped twice in one body */
201
+ dupStep: boolean;
202
+ /** walk pointers declared `volatile` */
203
+ volatileWalks: readonly string[];
204
+ /** the body left after the induction tail carries a `break`/`continue` for this loop */
205
+ coreHasExit: boolean;
206
+ /** walk pointers with no init ahead of the loop, or one whose base the rewrite cannot keep */
207
+ badBases: readonly string[];
208
+ /** total mentions of the counter across the WHOLE function */
209
+ kMentions: number;
210
+ /** walk pointers mentioned outside the loop shape */
211
+ unconfined: readonly string[];
212
+ /** walk pointers whose derefs disagree with their own element size */
213
+ badStrides: readonly string[];
214
+ /** statements ahead of the loop that survive it and still mention a walk pointer */
215
+ leakyLeftovers: number;
216
+ }
217
+
218
+ /** The admission rules v2 and v4 share, as data. The argument for each lives in the file header;
219
+ * `why` is the label. */
220
+ export const COUNTDOWN_GATES: readonly Gate<CountdownCtx>[] = [
221
+ {
222
+ id: 'pointer-counter',
223
+ why: 'C pointer arithmetic strides the pointee, so `k - 1` on a pointer counts elements, not iterations',
224
+ sound: true,
225
+ guardedBy: 'reindex.test.ts: a pointer-typed counter declines',
226
+ rejects: (c) => c.kIsPointer,
227
+ },
228
+ {
229
+ id: 'global-counter',
230
+ why: 'a global counter’s final value is observable to every other caller, ISR and translation unit',
231
+ sound: true,
232
+ guardedBy: 'reindex.test.ts: the counter may not be a GLOBAL',
233
+ rejects: (c) => !c.kIsDeclared,
234
+ },
235
+ {
236
+ id: 'volatile-counter',
237
+ why: 'the rewrite deletes the counter’s init and decrement, which a volatile object’s reader still expects',
238
+ sound: true,
239
+ guardedBy: 'reindex.test.ts: a volatile counter declines',
240
+ rejects: (c) => c.kIsVolatile,
241
+ },
242
+ {
243
+ id: 'volatile-walk',
244
+ why: 're-basing the derefs off a plain local turns N accesses to volatile data into N plain ones',
245
+ sound: true,
246
+ guardedBy: 'reindex.test.ts: a volatile walk pointer declines',
247
+ rejects: (c) => c.volatileWalks.length > 0,
248
+ },
249
+ {
250
+ id: 'two-steps-one-pointer',
251
+ why: 'a pointer stepped twice per iteration strides 2, which `p[i]` does not',
252
+ sound: true,
253
+ guardedBy: 'reindex.test.ts: a pointer stepped twice in one body declines',
254
+ rejects: (c) => c.dupStep,
255
+ },
256
+ {
257
+ id: 'body-exit',
258
+ why: 'the steps sat in the body tail, which a `continue` skips and a `for`’s inc does not',
259
+ sound: true,
260
+ guardedBy: 'reindex.test.ts: a `break` in the body declines',
261
+ rejects: (c) => c.coreHasExit,
262
+ },
263
+ {
264
+ id: 'walk-base',
265
+ why: 'the kept init must be a value the rewrite can leave standing — a var, or a rematerializable address',
266
+ sound: true,
267
+ guardedBy: 'reindex.test.ts: a walk pointer with no init ahead of the loop declines',
268
+ rejects: (c) => c.badBases.length > 0,
269
+ },
270
+ {
271
+ id: 'counter-roles',
272
+ why: 'exactly four mentions — init write, decrement write and read, exit read — are what the rewrite deletes',
273
+ sound: true,
274
+ guardedBy: 'reindex.test.ts: a fifth mention of the counter declines',
275
+ rejects: (c) => c.kMentions !== 4,
276
+ },
277
+ {
278
+ id: 'walk-confined',
279
+ why: 'a mention outside the loop shape reads a pointer whose step the rewrite deleted',
280
+ sound: true,
281
+ guardedBy: 'reindex.test.ts: a walk pointer read after the loop declines',
282
+ rejects: (c) => c.unconfined.length > 0,
283
+ },
284
+ {
285
+ id: 'walk-stride',
286
+ why: 'a deref reading a width other than the pointee’s makes walk and index forms read different addresses',
287
+ sound: true,
288
+ guardedBy: 'reindex.test.ts: a byte deref of a word walk declines',
289
+ rejects: (c) => c.badStrides.length > 0,
290
+ },
291
+ {
292
+ id: 'leftover-walk',
293
+ why: 'a leftover outlives the deleted step, and its skip-arm twin reads a pointer that path never set',
294
+ sound: true,
295
+ guardedBy: 'reindex.test.ts: a leftover mentioning a walk pointer declines',
296
+ rejects: (c) => c.leakyLeftovers > 0,
297
+ },
298
+ ];
299
+
300
+ /** A `break`/`continue` that would target the ENCLOSING loop: found at if/switch depth, but not
301
+ * inside a nested loop (whose own exits target itself). */
302
+ function hasLoopExit(stmts: Stmt[]): boolean {
303
+ return stmts.some((s) => {
304
+ switch (s.k) {
305
+ case 'break':
306
+ case 'continue':
307
+ return true;
308
+ case 'if':
309
+ return hasLoopExit(s.then) || hasLoopExit(s.else);
310
+ case 'switch':
311
+ return s.cases.some((c) => hasLoopExit(c.body)) || hasLoopExit(s.default ?? []);
312
+ default:
313
+ return false;
314
+ }
315
+ });
316
+ }
317
+
128
318
  function stmtMentions(s: Stmt, name: string): boolean {
129
319
  if (s.k === 'assign' && s.name === name) {
130
320
  return true;
131
321
  }
132
- const kids: Stmt[] =
133
- s.k === 'if'
134
- ? [...s.then, ...s.else]
135
- : s.k === 'while' || s.k === 'dowhile'
136
- ? s.body
137
- : s.k === 'for'
138
- ? [s.init, s.inc, ...s.body]
139
- : s.k === 'switch'
140
- ? [...s.cases.flatMap((c) => c.body), ...(s.default ?? [])]
141
- : [];
322
+ const kids = stmtChildren(s);
142
323
  return stmtExprs(s).some((e) => mentionsVar(e, name)) || kids.some((k) => stmtMentions(k, name));
143
324
  }
144
325
 
145
- /** `assign(p, p + 1)` on a pointer-typed `p`? */
146
- function isUnitStep(s: Stmt, ptrVars: Map<string, IrType>): string | null {
147
- if (s.k !== 'assign' || !ptrVars.has(s.name)) {
326
+ /** `x = x + 1` on ANY variable the assigned name, or null. */
327
+ function isUnitIncrement(s: Stmt): string | null {
328
+ if (s.k !== 'assign') {
148
329
  return null;
149
330
  }
150
331
  const v = s.value;
@@ -153,6 +334,20 @@ function isUnitStep(s: Stmt, ptrVars: Map<string, IrType>): string | null {
153
334
  return ok ? s.name : null;
154
335
  }
155
336
 
337
+ /** `assign(p, p + 1)` on a pointer-typed `p` — the walk's step; the assigned name, or null. */
338
+ function isUnitStep(s: Stmt, ptrVars: Map<string, IrType>): string | null {
339
+ const n = isUnitIncrement(s);
340
+ return n !== null && ptrVars.has(n) ? n : null;
341
+ }
342
+
343
+ /** ADVERSARIALLY LEARNED (reproduced as an uninitialized-read escape): the walk pointer must be
344
+ * mentioned NOWHERE in the function outside its init and its loop — counted GLOBALLY, because a
345
+ * suffix-only scan missed reads after an ENCLOSING construct, leaving the deleted init's var
346
+ * read uninitialized. One spelling for every recognizer. */
347
+ function confinedToWalk(fnBody: Stmt[], name: string, mentionsOutsideLoop: number, loop: Stmt): boolean {
348
+ return countMentions(fnBody, name) === mentionsOutsideLoop + countMentions([loop], name);
349
+ }
350
+
156
351
  /** Rewrite every deref of `p` into an indexed access off `base`, and every OTHER mention of `p`
157
352
  * fails the walk (returns null): `p[k]` → `base[i + k]` (`base[i]` for k 0). */
158
353
  function reindexExpr(e: Expr, walk: WalkLoop, iv: string): Expr | null {
@@ -171,9 +366,14 @@ function reindexExpr(e: Expr, walk: WalkLoop, iv: string): Expr | null {
171
366
  e.idx.k === 'const' && e.idx.value === 0
172
367
  ? { k: 'var', name: iv }
173
368
  : { k: 'bin', op: '+', l: { k: 'var', name: iv }, r: e.idx };
174
- // NOTE: this rebuilds the node from parts, so any field not named here is DROPPED. `lead` is
175
- // declined above (the deref side); it cannot arrive on the base side either, since `walk.base`
176
- // is a local pointer and structuring only ever puts `lead` on an array GLOBAL's own name.
369
+ // NOTE: this rebuilds the node from parts, so any field not named here is DROPPED and the
370
+ // `index` node has exactly three optional ones (ast.ts): `lead`, `operandOff`, `baseOrdered`.
371
+ // `lead` is declined above (the deref side); it cannot arrive on the base side either, since
372
+ // `walk.base` is a local pointer and structuring only ever puts `lead` on an array GLOBAL's
373
+ // own name. Dropping the other two is right rather than merely harmless: `baseOrdered` is
374
+ // stamped per SYMBOL on an order-licensed GLOBAL, which a local var base never is, and
375
+ // `operandOff`'s readers (l3/basecse.ts, l3/offmember.ts) ask about a CONSTANT subscript,
376
+ // which the rewritten `i`/`i + k` index never is.
177
377
  return { k: 'index', base: { k: 'var', name: walk.base }, idx, width: e.width, signed: e.signed };
178
378
  }
179
379
  let failed = false;
@@ -296,11 +496,181 @@ function reindexStmt(s: Stmt, walk: WalkLoop, iv: string): Stmt | null {
296
496
  }
297
497
  }
298
498
 
499
+ // ── v3: the up-counting BYTE walk with an EXPRESSION base ────────────────────────────────────
500
+ //
501
+ // p = (u8 *)(EXPR); do { … *p … ; p = p + 1; i = i + 1; } while (i < n);
502
+ //
503
+ // The loop already carries its own integer counter, so the walk pointer is pure strength
504
+ // reduction: deleting its init and step and spelling the deref as BASE[i + REST] — the sole
505
+ // bare-var addend of EXPR as the index base, the counter FIRST in the index sum — is the
506
+ // indexed source the compiler reduces back. v3 SCOPE (decline over approximate):
507
+ // • `p` is a `u8 *` (byte walk: the index and the byte arithmetic agree by construction —
508
+ // a wider element would need the REST addends rescaled from bytes to elements);
509
+ // • the init `p = EXPR` immediately precedes the `dowhile`; EXPR (casts stripped) flattens
510
+ // over `+` into exactly ONE bare-var addend (the base — itself not a non-byte pointer, or
511
+ // `base[i]` would stride its element) plus pure invariant addends (var/const/mul/shl trees
512
+ // whose vars the body never assigns);
513
+ // • the body's statements include exactly one `p = p + 1` and one `i = i + 1` with integer
514
+ // `i` read by the do-while condition; `p` is mentioned nowhere else in the function beyond
515
+ // its init, its step, and derefs AT OFFSET 0 inside this body, and not by the condition.
516
+ function tryExprWalk(
517
+ prev2: Stmt | undefined,
518
+ prev: Stmt | undefined,
519
+ dw: Stmt & { k: 'dowhile' },
520
+ ptrVars: Map<string, IrType>,
521
+ fnBody: Stmt[],
522
+ declTypes: VarTypes,
523
+ volatileLocals: ReadonlySet<string>,
524
+ ): Stmt | null {
525
+ if (prev?.k !== 'assign' || !ptrVars.has(prev.name) || volatileLocals.has(prev.name)) {
526
+ return null;
527
+ }
528
+ const p = prev.name;
529
+ const pT = ptrVars.get(p)!;
530
+ if (pT.kind !== 'ptr' || pT.to.kind !== 'int' || pT.to.width !== 8) {
531
+ return null;
532
+ }
533
+ if (!derefWidths([dw], p).every((w) => w === 1)) {
534
+ return null;
535
+ }
536
+ // EXPR decomposition
537
+ let expr: Expr = prev.value;
538
+ while (expr.k === 'cast') {
539
+ expr = expr.e;
540
+ }
541
+ const addends: Expr[] = [];
542
+ const flatten = (e: Expr): void => {
543
+ if (e.k === 'bin' && e.op === '+') {
544
+ flatten(e.l);
545
+ flatten(e.r);
546
+ } else {
547
+ addends.push(e);
548
+ }
549
+ };
550
+ flatten(expr);
551
+ const bare = addends.filter((a): a is Extract<Expr, { k: 'var' }> => a.k === 'var');
552
+ const rest = addends.filter((a) => a.k !== 'var');
553
+ if (bare.length !== 1) {
554
+ return null;
555
+ }
556
+ const base = bare[0] as Extract<Expr, { k: 'var' }>;
557
+ // declTypes carries params, locals AND globals: a base declared nowhere has an unknowable C
558
+ // stride (its project declaration decides), and a wider pointer strides its element under [i]
559
+ // — both decline.
560
+ const baseT = declTypes(base.name);
561
+ if (baseT === undefined) {
562
+ return null;
563
+ }
564
+ if (baseT.kind === 'ptr' && !(baseT.to.kind === 'int' && baseT.to.width === 8)) {
565
+ return null;
566
+ }
567
+ const pure = (e: Expr): boolean =>
568
+ e.k === 'var' || e.k === 'const'
569
+ ? true
570
+ : e.k === 'bin' && (e.op === '*' || e.op === '+' || e.op === '<<')
571
+ ? pure(e.l) && pure(e.r)
572
+ : e.k === 'cast'
573
+ ? pure(e.e)
574
+ : false;
575
+ const assignedIn = (stmts: Stmt[], name: string): boolean => stmts.some((st) => stmtAssigns(st, name));
576
+ const restVars = new Set<string>();
577
+ const collectVars = (e: Expr): void => {
578
+ if (e.k === 'var') {
579
+ restVars.add(e.name);
580
+ }
581
+ mapExprChildren(e, (c) => {
582
+ collectVars(c);
583
+ return c;
584
+ });
585
+ };
586
+ rest.forEach(collectVars);
587
+ restVars.add(base.name);
588
+ if (!rest.every(pure) || [...restVars].some((n) => assignedIn(dw.body, n))) {
589
+ return null;
590
+ }
591
+ // the steps and the counter
592
+ const isIncOf = (st: Stmt, name?: string): string | null => {
593
+ const n = isUnitIncrement(st);
594
+ return n !== null && (name === undefined || n === name) ? n : null;
595
+ };
596
+ // BOTH steps as the body's contiguous TAIL (either order): every deref then runs with `i`
597
+ // completed steps equal to `p`'s, which is what `BASE[i + REST]` states. A step anywhere
598
+ // earlier (the `*++p` walk, a deref straddling the step) reads a different address.
599
+ const tail = dw.body.slice(-2);
600
+ const tailIncs = tail.map((st) => isIncOf(st)).filter((n): n is string => n !== null);
601
+ if (tailIncs.length !== 2 || !tailIncs.includes(p)) {
602
+ return null;
603
+ }
604
+ const iv = tailIncs.find((n) => n !== p);
605
+ if (iv === undefined) {
606
+ return null;
607
+ }
608
+ const pStepIdx = dw.body.length - 2 + tail.findIndex((st) => isIncOf(st, p) !== null);
609
+ const ivT = declTypes(iv);
610
+ if (ivT !== undefined && ivT.kind !== 'int') {
611
+ return null;
612
+ }
613
+ if (!mentionsVar(dw.cond, iv) || mentionsVar(dw.cond, p)) {
614
+ return null;
615
+ }
616
+ // The counter STARTS AT ZERO (the adjacent `i = 0` init — `BASE[i + REST]` counts completed
617
+ // steps from the walk's own start) and its ONLY write anywhere in the body is the tail step.
618
+ if (prev2?.k !== 'assign' || prev2.name !== iv || prev2.value.k !== 'const' || prev2.value.value !== 0) {
619
+ return null;
620
+ }
621
+ if (dw.body.slice(0, -2).some((st) => stmtAssigns(st, iv)) || tail.filter((st) => isIncOf(st, iv)).length !== 1) {
622
+ return null;
623
+ }
624
+ if (volatileLocals.has(iv) || [...restVars].some((n) => volatileLocals.has(n))) {
625
+ return null;
626
+ }
627
+ // rewrite the derefs, then verify nothing of `p` survives
628
+ const idxOf = (): Expr => rest.reduce<Expr>((acc, r) => ({ k: 'bin', op: '+', l: acc, r }), { k: 'var', name: iv });
629
+ let respelled = true;
630
+ const rewriteExpr = (e: Expr): Expr => {
631
+ const m = mapExprChildren(e, rewriteExpr);
632
+ if (m.k === 'index' && m.base.k === 'var' && m.base.name === p) {
633
+ if (m.idx.k !== 'const' || m.idx.value !== 0) {
634
+ respelled = false;
635
+ return m;
636
+ }
637
+ return { ...m, base: { k: 'var', name: base.name }, idx: idxOf() };
638
+ }
639
+ return m;
640
+ };
641
+ const body = dw.body.filter((_, k) => k !== pStepIdx).map((st) => mapStmtExprs(st, rewriteExpr));
642
+ const dw2: Stmt = { ...dw, body };
643
+ if (!respelled || !confinedToWalk(fnBody, p, countMentions([prev], p), dw)) {
644
+ return null;
645
+ }
646
+ if (countMentions([dw2], p) !== 0) {
647
+ return null;
648
+ }
649
+ return dw2;
650
+ }
651
+
299
652
  /** Try the walk→index re-spelling on one function. Returns the transformed COPY when at least
300
653
  * one loop re-spelled, or null (no candidate) when nothing fired — callers emit the extra
301
- * candidate only on non-null. Pure: never mutates the input SFn. */
302
- export function reindexWalks(sfn: SFn): SFn | null {
654
+ * candidate only on non-null. Pure: never mutates the input SFn. `keptWalks` collects the names
655
+ * of the pointers each fired loop kept as its base — v1 the walk's base (a param lands in the
656
+ * set too, inertly: the volatile lever marks only declared locals), v2 the walk pointers
657
+ * themselves — the locals the /indexed/volatile product (rank.ts) narrows the volatile lever
658
+ * to. A v3 loop contributes nothing: it DELETES its pointer, and its base is qualified through
659
+ * the /livebase pairings instead. `gates` is the shared countdown admission table — a parameter
660
+ * so a test can ablate one entry and re-run the real pass. */
661
+ export function reindexWalks(
662
+ sfn: SFn,
663
+ keptWalks?: Set<string>,
664
+ gates: readonly Gate<CountdownCtx>[] = COUNTDOWN_GATES,
665
+ ): SFn | null {
303
666
  const ptrVars = new Map<string, IrType>();
667
+ const declTypes = declaredTypes(sfn);
668
+ // BOTH volatility facts (ast.ts SFn.locals): the object-volatile counter, and the pointer whose
669
+ // POINTEE is volatile — which is the one the `/volatile` lever mints, and the one a walk carries.
670
+ const volatileLocals = new Set(
671
+ sfn.locals.filter((l) => l.volatile === true || l.pointeeVolatile === true).map((l) => l.name),
672
+ );
673
+ const storage = nameStorage(sfn);
304
674
  for (const v of [...sfn.params, ...sfn.locals]) {
305
675
  if (v.type.kind === 'ptr') {
306
676
  ptrVars.set(v.name, v.type);
@@ -311,7 +681,7 @@ export function reindexWalks(sfn: SFn): SFn | null {
311
681
  }
312
682
 
313
683
  let fired = 0;
314
- let ivCount = 0;
684
+ const taken = takenNames(sfn);
315
685
  const locals = [...sfn.locals];
316
686
 
317
687
  // SOUNDNESS GATE (adversarially learned; every rule REPRODUCED as a wrong-bytes or crash
@@ -320,14 +690,11 @@ export function reindexWalks(sfn: SFn): SFn | null {
320
690
  // • base and p must be pointers of the SAME element size, and every deref of p must read
321
691
  // exactly that size — otherwise the walk (strides p's pointee) and the indexed form
322
692
  // (strides base's) read different addresses;
323
- // • p must not be mentioned ANYWHERE in the function outside the init + the loop — the
324
- // suffix-only check missed reads after an ENCLOSING construct, leaving the deleted init's
325
- // var read uninitialized. Counted globally: total mentions == init + loop mentions.
326
- const soundWalk = (walk: WalkLoop, initMentions: number, loop: Stmt): boolean =>
693
+ // • confinedToWalk the global mention accounting, shared with v3.
694
+ const soundWalk = (walk: WalkLoop, mentionsOutsideLoop: number, loop: Stmt): boolean =>
327
695
  walk.p !== walk.base &&
328
- strideAgrees(ptrVars.get(walk.p), ptrVars.get(walk.base) ?? paramType(walk.base), derefWidths([loop], walk.p)) &&
329
- countMentions(sfn.body, walk.p) === initMentions + countMentions([loop], walk.p);
330
- const paramType = (n: string): IrType | undefined => sfn.params.find((x) => x.name === n)?.type;
696
+ strideAgrees(ptrVars.get(walk.p), ptrVars.get(walk.base), derefWidths([loop], walk.p)) &&
697
+ confinedToWalk(sfn.body, walk.p, mentionsOutsideLoop, loop);
331
698
 
332
699
  // walk a statement LIST so the `while` shape can see its preceding init statement
333
700
  const walkList = (stmts: Stmt[]): Stmt[] => {
@@ -343,15 +710,15 @@ export function reindexWalks(sfn: SFn): SFn | null {
343
710
  init.k === 'assign' &&
344
711
  init.name === p &&
345
712
  init.value.k === 'var' &&
346
- // init contributes 2 mentions (the write to p and the inc's read... the for init is
347
- // part of the loop stmt itself, so count the whole `for` node) — see soundWalk
713
+ // a `for`'s init lives inside the loop node, so confinedToWalk already counts it
348
714
  soundWalk({ p, base: init.value.name }, 0, s)
349
715
  ) {
350
716
  const walk: WalkLoop = { p, base: init.value.name };
351
- const iv = freshIv();
717
+ const iv = nextIv();
352
718
  const cond = reindexCond(s.cond, walk, iv);
353
719
  const body = cond ? reindexStmts(s.body, walk, iv) : null;
354
720
  if (cond && body) {
721
+ declareIv(iv);
355
722
  out.push({
356
723
  k: 'for',
357
724
  init: { k: 'assign', name: iv, value: { k: 'const', value: 0 } },
@@ -364,9 +731,9 @@ export function reindexWalks(sfn: SFn): SFn | null {
364
731
  body,
365
732
  });
366
733
  fired++;
734
+ keptWalks?.add(walk.base);
367
735
  continue;
368
736
  }
369
- retireIv();
370
737
  }
371
738
  }
372
739
  // `p = base; while (p < base + n) { …; p = p + 1; }`
@@ -382,10 +749,11 @@ export function reindexWalks(sfn: SFn): SFn | null {
382
749
  soundWalk({ p, base: prev.value.name }, 1, s)
383
750
  ) {
384
751
  const walk: WalkLoop = { p, base: prev.value.name };
385
- const iv = freshIv();
752
+ const iv = nextIv();
386
753
  const cond = reindexCond(s.cond, walk, iv);
387
754
  const body = cond ? reindexStmts(s.body.slice(0, -1), walk, iv) : null;
388
755
  if (cond && body) {
756
+ declareIv(iv);
389
757
  out[out.length - 1] = { k: 'assign', name: iv, value: { k: 'const', value: 0 } };
390
758
  out.push({
391
759
  k: 'while',
@@ -400,9 +768,40 @@ export function reindexWalks(sfn: SFn): SFn | null {
400
768
  ],
401
769
  });
402
770
  fired++;
771
+ keptWalks?.add(walk.base);
403
772
  continue;
404
773
  }
405
- retireIv();
774
+ }
775
+ }
776
+ // v3 — the up-counting byte walk with an expression base: `p = (u8 *)(EXPR)` immediately
777
+ // ahead of a counter-carried do-while (see tryExprWalk)
778
+ if (s.k === 'dowhile') {
779
+ const prev = out[out.length - 1];
780
+ const prev2 = out[out.length - 2];
781
+ const dw2 = tryExprWalk(prev2, prev, s, ptrVars, sfn.body, declTypes, volatileLocals);
782
+ if (dw2) {
783
+ out.pop(); // the walk init is subsumed by the indexed spelling
784
+ out.push(recurse(dw2));
785
+ fired++;
786
+ continue;
787
+ }
788
+ // v4 — the unguarded constant-trip countdown (see the header)
789
+ const cd = respellConstCountdown(out, s);
790
+ if (cd) {
791
+ out.length = 0;
792
+ out.push(...cd.pre, cd.counted);
793
+ fired++;
794
+ continue;
795
+ }
796
+ }
797
+ // the guarded countdown (v2 — see the header): an `if` whose one arm is skip-copies C and
798
+ // whose other arm is C ⊎ inductions followed by the do-while
799
+ if (s.k === 'if') {
800
+ const r = respellGuardedCountdown(s);
801
+ if (r) {
802
+ out.push(...r);
803
+ fired++;
804
+ continue;
406
805
  }
407
806
  }
408
807
  // recurse into compound statements without re-spelling them
@@ -431,19 +830,257 @@ export function reindexWalks(sfn: SFn): SFn | null {
431
830
  }
432
831
  };
433
832
 
434
- function freshIv(): string {
435
- // collide-checked: pipeline naming is a*/v*/t*, but future naming (DWARF) may import
436
- // real source names never conflate with an existing i<N>.
437
- let name = `i${ivCount++}`;
438
- while (sfn.params.some((x) => x.name === name) || locals.some((x) => x.name === name)) {
439
- name = `i${ivCount++}`;
833
+ /** The countdown machinery the guarded (v2) and the unguarded constant-trip (v4) shapes share:
834
+ * everything downstream of "which statement inits the counter, and what is the trip count".
835
+ * Recognizes the shape, collects what `gates` reads, and refuses on the first entry that
836
+ * rejects. `confineTo` names the statements every mention of the counter and of each walk
837
+ * pointer must live in, given the induction inits this found; `accept` is the caller's own
838
+ * admission. Returns the inits it consumed, `pre` minus them, and the counted `for` — or null
839
+ * (decline). */
840
+ function respellCountdown(
841
+ loop: Stmt & { k: 'dowhile' },
842
+ pre: Stmt[],
843
+ k: string,
844
+ kInit: Stmt & { k: 'assign' },
845
+ trip: Expr,
846
+ confineTo: (induction: Stmt[]) => Stmt[],
847
+ accept?: (leftovers: Stmt[]) => boolean,
848
+ ): { inductionInits: Set<Stmt>; leftovers: Stmt[]; counted: Stmt & { k: 'for' } } | null {
849
+ // SHAPE RECOGNITION — the body's contiguous tail is `k -= 1` and one `p += 1` per walk
850
+ // pointer. Not an admission rule: without a decrement and a step there is no countdown here
851
+ // at all, and the facts the rules read are derived from what this scan found.
852
+ const isDec = (x: Stmt): boolean =>
853
+ x.k === 'assign' &&
854
+ x.name === k &&
855
+ x.value.k === 'bin' &&
856
+ x.value.op === '-' &&
857
+ x.value.l.k === 'var' &&
858
+ x.value.l.name === k &&
859
+ x.value.r.k === 'const' &&
860
+ x.value.r.value === 1;
861
+ const walks: string[] = [];
862
+ let cut = loop.body.length;
863
+ let sawDec = false;
864
+ let dupStep = false;
865
+ for (let i = loop.body.length - 1; i >= 0; i--) {
866
+ const x = loop.body[i];
867
+ const p = isUnitStep(x, ptrVars);
868
+ if (p !== null) {
869
+ dupStep ||= walks.includes(p);
870
+ walks.push(p);
871
+ cut = i;
872
+ continue;
873
+ }
874
+ if (!sawDec && isDec(x)) {
875
+ sawDec = true;
876
+ cut = i;
877
+ continue;
878
+ }
879
+ break;
440
880
  }
441
- locals.push({ name, type: T.s(32) });
442
- return name;
881
+ if (!sawDec || walks.length === 0) {
882
+ return null;
883
+ }
884
+ const bodyCore = loop.body.slice(0, cut);
885
+ const inits = new Map<string, Stmt & { k: 'assign' }>();
886
+ const badBases: string[] = [];
887
+ for (const p of walks) {
888
+ const init = pre.find((x): x is Stmt & { k: 'assign' } => x.k === 'assign' && x.name === p);
889
+ const bOk =
890
+ init &&
891
+ (init.value.k === 'var' ? init.value.name !== p && init.value.name !== k : rematerializableAddress(init.value));
892
+ if (bOk) {
893
+ inits.set(p, init);
894
+ } else {
895
+ badBases.push(p);
896
+ }
897
+ }
898
+ const shape = confineTo([kInit, ...inits.values(), loop]);
899
+ const inductionInits = new Set<Stmt>([kInit, ...inits.values()]);
900
+ const leftovers = pre.filter((x) => !inductionInits.has(x));
901
+ const elemSize = (t: IrType | undefined): number =>
902
+ t?.kind === 'ptr' ? (t.to.kind === 'int' ? t.to.width / 8 : t.to.kind === 'ptr' ? 4 : 0) : 0;
903
+ const ctx: CountdownCtx = {
904
+ kIsPointer: ptrVars.has(k),
905
+ kIsDeclared: storage.get(k) === 'local' || storage.get(k) === 'param',
906
+ kIsVolatile: volatileLocals.has(k),
907
+ dupStep,
908
+ volatileWalks: walks.filter((p) => volatileLocals.has(p)),
909
+ coreHasExit: hasLoopExit(bodyCore),
910
+ badBases,
911
+ kMentions: countMentions(sfn.body, k),
912
+ unconfined: walks.filter((p) => countMentions(sfn.body, p) !== countMentions(shape, p)),
913
+ badStrides: walks.filter((p) => {
914
+ const es = elemSize(ptrVars.get(p));
915
+ return es === 0 || !derefWidths([loop], p).every((w) => w === es);
916
+ }),
917
+ leakyLeftovers: leftovers.filter((x) => walks.some((w) => stmtMentions(x, w))).length,
918
+ };
919
+ if (firstRejection(gates, ctx) !== null) {
920
+ return null;
921
+ }
922
+ // The caller's own admission. It runs before anything is minted, and so does every rule
923
+ // above: the rewrite below is the first statement in this function that mutates `locals` or
924
+ // `keptWalks`, so a decline has nothing to undo.
925
+ if (accept && !accept(leftovers)) {
926
+ return null;
927
+ }
928
+ // rewrite: each walk pointer is its OWN base (kept local, step dropped)
929
+ const iv = nextIv();
930
+ let body: Stmt[] | null = bodyCore;
931
+ for (const p of walks) {
932
+ body = body === null ? null : reindexStmts(body, { p, base: p }, iv);
933
+ }
934
+ if (body === null) {
935
+ return null;
936
+ }
937
+ declareIv(iv);
938
+ for (const p of walks) {
939
+ keptWalks?.add(p);
940
+ }
941
+ // Nothing mentions the counter past this point: its init, its decrement and the exit test are
942
+ // all gone, and the four-roles rule says those were every mention it had. Its declaration goes
943
+ // with them — a local declared, never written and never read reads as the deliberately
944
+ // uninitialized slot `uninit` exists to mark. A PARAM keeps its declaration, having one for a
945
+ // reason the body does not decide.
946
+ const kDecl = locals.findIndex((l) => l.name === k);
947
+ if (kDecl >= 0) {
948
+ locals.splice(kDecl, 1);
949
+ }
950
+ return {
951
+ inductionInits,
952
+ leftovers,
953
+ counted: {
954
+ k: 'for',
955
+ init: { k: 'assign', name: iv, value: { k: 'const', value: 0 } },
956
+ cond: { k: 'bin', op: '<', l: { k: 'var', name: iv }, r: trip },
957
+ inc: {
958
+ k: 'assign',
959
+ name: iv,
960
+ value: { k: 'bin', op: '+', l: { k: 'var', name: iv }, r: { k: 'const', value: 1 } },
961
+ },
962
+ body,
963
+ },
964
+ };
965
+ }
966
+
967
+ /** v2: `s` re-spelled as [C…, walk inits…, for], or null (decline). */
968
+ function respellGuardedCountdown(s: Stmt & { k: 'if' }): Stmt[] | null {
969
+ // which arm holds the loop, and does the guard skip it in the right sense?
970
+ const armWith = (arm: Stmt[]): { pre: Stmt[]; loop: Stmt & { k: 'dowhile' } } | null => {
971
+ const last = arm[arm.length - 1];
972
+ return last?.k === 'dowhile' ? { pre: arm.slice(0, -1), loop: last } : null;
973
+ };
974
+ const inElse = armWith(s.else);
975
+ const inThen = armWith(s.then);
976
+ const c = s.cond;
977
+ const guardSkips = (n: string, loopInElse: boolean): boolean => {
978
+ if (c.k !== 'bin') {
979
+ return false;
980
+ }
981
+ const zl = c.l.k === 'const' && c.l.value === 0 && c.r.k === 'var' && c.r.name === n;
982
+ const zr = c.r.k === 'const' && c.r.value === 0 && c.l.k === 'var' && c.l.name === n;
983
+ // loop in else ⇒ cond true skips: 0 >= n / n <= 0. Loop in then ⇒ cond true enters: 0 < n / n > 0.
984
+ return loopInElse ? (zl && c.op === '>=') || (zr && c.op === '<=') : (zl && c.op === '<') || (zr && c.op === '>');
985
+ };
986
+ const pick = inElse ?? inThen;
987
+ const skipArm = inElse ? s.then : s.else;
988
+ if (!pick || (inElse && inThen)) {
989
+ return null;
990
+ }
991
+ const { pre, loop } = pick;
992
+ // the do-while exit: exactly `k != 0`
993
+ const lc = loop.cond;
994
+ if (lc.k !== 'bin' || lc.op !== '!=' || lc.l.k !== 'var' || lc.r.k !== 'const' || lc.r.value !== 0) {
995
+ return null;
996
+ }
997
+ const k = lc.l.name;
998
+ // the counter's init `k = N`, N a var the guard tests
999
+ const kInit = pre.find((x): x is Stmt & { k: 'assign' } => x.k === 'assign' && x.name === k);
1000
+ if (!kInit || kInit.value.k !== 'var') {
1001
+ return null;
1002
+ }
1003
+ const n = kInit.value.name;
1004
+ if (n === k || !guardSkips(n, pick === inElse)) {
1005
+ return null;
1006
+ }
1007
+ if (sfn.body.some((x) => stmtAssigns(x, n))) {
1008
+ return null; // a moving bound
1009
+ }
1010
+ // the skip arm must be, in order, exactly `pre` minus the induction inits
1011
+ const skipArmMatches = (leftovers: Stmt[]): boolean =>
1012
+ skipArm.length === leftovers.length &&
1013
+ skipArm.every((x, i) => JSON.stringify(x) === JSON.stringify(leftovers[i]));
1014
+ const r = respellCountdown(loop, pre, k, kInit, { k: 'var', name: n }, () => [s], skipArmMatches);
1015
+ if (!r) {
1016
+ return null;
1017
+ }
1018
+ return [...pre.filter((x) => x !== kInit), r.counted];
1019
+ }
1020
+
1021
+ /** v4: the UNGUARDED constant-trip countdown (see the header). `out` holds the statements
1022
+ * already emitted before `dw`, the induction inits among them. Returns the list replacing
1023
+ * `out` (itself minus the counter's init) and the counted `for`, or null (decline). */
1024
+ function respellConstCountdown(out: Stmt[], dw: Stmt & { k: 'dowhile' }): { pre: Stmt[]; counted: Stmt } | null {
1025
+ // the do-while exit: exactly `k >= 0` / `0 <= k`
1026
+ const lc = dw.cond;
1027
+ if (lc.k !== 'bin') {
1028
+ return null;
1029
+ }
1030
+ const kv =
1031
+ lc.op === '>=' && lc.l.k === 'var' && lc.r.k === 'const' && lc.r.value === 0
1032
+ ? lc.l.name
1033
+ : lc.op === '<=' && lc.r.k === 'var' && lc.l.k === 'const' && lc.l.value === 0
1034
+ ? lc.r.name
1035
+ : null;
1036
+ if (kv === null) {
1037
+ return null;
1038
+ }
1039
+ // `k >= 0` terminates only for a SIGNED counter — an unsigned one never goes below zero, and
1040
+ // the compiler that emitted this test proved it signed (`bge`). A declaration asmlift settled
1041
+ // on unsigned describes a different loop, so the trip count `C + 1` would be a fiction.
1042
+ const kT = declTypes(kv);
1043
+ if (kT?.kind !== 'int' || !kT.signed) {
1044
+ return null;
1045
+ }
1046
+ // the counter's init `k = C`, C a NON-NEGATIVE constant: the loop runs while k counts C…0, so
1047
+ // the trip count is C + 1. That is a count only while BOTH ends are representable in the
1048
+ // counter's OWN type — below zero `k = -1` runs the body once rather than `C + 1 = 0` times,
1049
+ // and above its maximum `k = 200` in an `s8` is -56, which fails `k >= 0` at the first test
1050
+ // and runs the body once where `i < 201` runs it 201 times.
1051
+ const kMax = 2 ** (kT.width - 1) - 1;
1052
+ const kInit = out.find((x): x is Stmt & { k: 'assign' } => x.k === 'assign' && x.name === kv);
1053
+ if (!kInit || kInit.value.k !== 'const' || kInit.value.value < 0 || kInit.value.value >= kMax) {
1054
+ return null;
1055
+ }
1056
+ const r = respellCountdown(dw, out, kv, kInit, { k: 'const', value: kInit.value.value + 1 }, (i) => i);
1057
+ if (!r) {
1058
+ return null;
1059
+ }
1060
+ return { pre: out.filter((x) => x !== kInit), counted: r.counted };
1061
+ }
1062
+
1063
+ /** The next free induction name: the lowest `i<N>` nothing in `sfn` already claims and no
1064
+ * earlier mint has COMMITTED. Pipeline naming is `a`/`v`/`t` prefixed, but a global is
1065
+ * referenced by bare name and future naming (DWARF) may import real source ones, so the
1066
+ * collision set is `takenNames`' — declarations, mentions, call targets and assignment
1067
+ * targets alike — not the declaration lists.
1068
+ *
1069
+ * PURE: it reads that snapshot and the locals `declareIv` committed, so a recognizer that
1070
+ * mints and then declines leaves the next mint returning the same name. Callers hold at most
1071
+ * one uncommitted name at a time — every mint site rewrites and commits before the walk
1072
+ * recursion can reach another loop. */
1073
+ function nextIv(): string {
1074
+ let n = 0;
1075
+ while (taken.has(`i${n}`) || locals.some((x) => x.name === `i${n}`)) {
1076
+ n++;
1077
+ }
1078
+ return `i${n}`;
443
1079
  }
444
- function retireIv(): void {
445
- locals.pop();
446
- ivCount--;
1080
+ /** Commit a minted name: the ONE place the induction local is declared, called only once the
1081
+ * rewrite has succeeded. */
1082
+ function declareIv(name: string): void {
1083
+ locals.push({ name, type: T.s(32) });
447
1084
  }
448
1085
 
449
1086
  const body = walkList(sfn.body);