@asmlift/core 0.5.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (94) 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 +270 -171
  5. package/src/backend/cpp.ts +1 -0
  6. package/src/backend/pascal.ts +26 -12
  7. package/src/contracts.ts +243 -39
  8. package/src/declare.ts +41 -4
  9. package/src/frontend/mips.ts +11 -0
  10. package/src/frontend/ppc.ts +43 -7
  11. package/src/frontend/ssa.ts +404 -29
  12. package/src/frontend/thumb.ts +2176 -686
  13. package/src/ir/alias.ts +78 -0
  14. package/src/ir/bits.ts +75 -0
  15. package/src/ir/core.ts +345 -2
  16. package/src/ir/opcodes.ts +176 -21
  17. package/src/ir/parse.ts +19 -2
  18. package/src/ir/print.ts +27 -2
  19. package/src/ir/simplify.ts +190 -3
  20. package/src/ir/struct-names.ts +42 -0
  21. package/src/ir/verify.ts +43 -49
  22. package/src/l3/address.ts +62 -0
  23. package/src/l3/advance.ts +373 -0
  24. package/src/l3/argbase.ts +6 -5
  25. package/src/l3/ast.ts +510 -59
  26. package/src/l3/basecse.ts +686 -78
  27. package/src/l3/coalesce.ts +432 -46
  28. package/src/l3/dce.ts +31 -9
  29. package/src/l3/gates.ts +96 -1
  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 +176 -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 +114 -89
  42. package/src/l3/reindex.ts +722 -80
  43. package/src/l3/scopebase.ts +649 -220
  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 +16 -1
  49. package/src/l3/typing.ts +198 -9
  50. package/src/l3/unmerge.ts +687 -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 +239 -16
  57. package/src/pipeline.ts +173 -60
  58. package/src/proto.ts +112 -14
  59. package/src/raise/arrays.ts +6 -1
  60. package/src/raise/const.ts +203 -3
  61. package/src/raise/divpow2.ts +4 -4
  62. package/src/raise/extscale.ts +342 -0
  63. package/src/raise/globalshape.ts +1058 -0
  64. package/src/raise/gvn.ts +33 -18
  65. package/src/raise/latch.ts +126 -0
  66. package/src/raise/magicdiv.ts +2 -2
  67. package/src/raise/memberarrays.ts +594 -0
  68. package/src/raise/narrow.ts +124 -0
  69. package/src/raise/narrowlocal.ts +572 -0
  70. package/src/raise/paramwidth.ts +201 -0
  71. package/src/raise/pre-recovery.ts +169 -21
  72. package/src/raise/recover.ts +56 -23
  73. package/src/raise/retsink.ts +585 -19
  74. package/src/raise/shortcircuit.ts +1050 -89
  75. package/src/raise/struct-arrays.ts +19 -2
  76. package/src/raise/structs.ts +34 -4
  77. package/src/raise/tailsink.ts +126 -0
  78. package/src/rank-declare.ts +256 -0
  79. package/src/rank-variations.ts +760 -0
  80. package/src/rank.ts +2122 -326
  81. package/src/structure/analysis.ts +1398 -150
  82. package/src/structure/bitfields.ts +432 -0
  83. package/src/structure/globalaccess.ts +300 -0
  84. package/src/structure/hazards.ts +411 -20
  85. package/src/structure/loops.ts +2 -49
  86. package/src/structure/namecoalesce.ts +454 -0
  87. package/src/structure/structure.ts +3979 -612
  88. package/src/structure/switch-recover.ts +710 -145
  89. package/src/symbols.ts +188 -6
  90. package/src/target.ts +495 -32
  91. package/src/trace.ts +112 -33
  92. package/src/variation-definitions.ts +1540 -0
  93. package/src/variation-gates.ts +89 -0
  94. package/src/variation-tokens.ts +355 -0
package/src/l3/reindex.ts CHANGED
@@ -1,11 +1,11 @@
1
- // asmlift L3 — the walk→index RE-SPELLING, a differ-ranked representation lever.
1
+ // asmlift L3 — the walk→index RESPELL VARIATION, differ-ranked.
2
2
  //
3
3
  // A compiler strength-reduces a source-level `arr[i]` loop into a pointer WALK (`*p; p += 1`),
4
4
  // so asmlift's faithful lift of the machine form emits the walk — but recompiling the walk
5
5
  // rarely reproduces the bytes the INDEXED source produced (different induction variable,
6
6
  // different regalloc). Which representation the source used is genuinely ambiguous from asm —
7
7
  // exactly the class of ambiguity asmlift resolves by CANDIDATES, not guesses (rank.ts: "types
8
- // are differ-ranked levers"). This module produces the indexed re-spelling of a structured
8
+ // are differ-ranked variations"). This module produces the indexed re-spelling of a structured
9
9
  // function; enumerateCandidates emits BOTH and the objdiff score referees.
10
10
  //
11
11
  // v1 SCOPE (decline over approximate): a loop is re-spelled only when ALL hold —
@@ -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` variation 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, interrupt handler 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 at the end of the body, which a `continue` skips and a `for`’s increment 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 variable, or an address that can be loaded again',
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 statement left outside the deleted step still reads the walk pointer, which a path that skipped the step 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,19 @@ 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 five optional ones (ast.ts): `lead`, `baseElem`, `operandOff`,
371
+ // `baseOrdered`, `baseAdvanced`. `lead` is declined above (the deref side); it cannot arrive on
372
+ // the base side either, since `walk.base` is a local pointer and structuring only ever puts
373
+ // `lead` on an array GLOBAL's own name. Dropping the other four is right rather than merely
374
+ // harmless: `baseElem` states the element type of a base the C type walk cannot type, and the
375
+ // rebuilt base is `{k:'var', name: walk.base}` — a LOCAL pointer, whose declared type the walk
376
+ // reads straight out of the print env, so a statement about the base it replaced would describe
377
+ // a different base and the walk answers without one; `baseOrdered` is stamped per SYMBOL on an
378
+ // order-licensed GLOBAL, which a local var base never is; `operandOff`'s readers
379
+ // (l3/basecse.ts, l3/offmember.ts) ask about a CONSTANT subscript, which the rewritten
380
+ // `i`/`i + k` index never is; and `baseAdvanced` describes an address the machine reached from
381
+ // a CONSTANT one, which a walk pointer's is not.
177
382
  return { k: 'index', base: { k: 'var', name: walk.base }, idx, width: e.width, signed: e.signed };
178
383
  }
179
384
  let failed = false;
@@ -296,11 +501,181 @@ function reindexStmt(s: Stmt, walk: WalkLoop, iv: string): Stmt | null {
296
501
  }
297
502
  }
298
503
 
504
+ // ── v3: the up-counting BYTE walk with an EXPRESSION base ────────────────────────────────────
505
+ //
506
+ // p = (u8 *)(EXPR); do { … *p … ; p = p + 1; i = i + 1; } while (i < n);
507
+ //
508
+ // The loop already carries its own integer counter, so the walk pointer is pure strength
509
+ // reduction: deleting its init and step and spelling the deref as BASE[i + REST] — the sole
510
+ // bare-var addend of EXPR as the index base, the counter FIRST in the index sum — is the
511
+ // indexed source the compiler reduces back. v3 SCOPE (decline over approximate):
512
+ // • `p` is a `u8 *` (byte walk: the index and the byte arithmetic agree by construction —
513
+ // a wider element would need the REST addends rescaled from bytes to elements);
514
+ // • the init `p = EXPR` immediately precedes the `dowhile`; EXPR (casts stripped) flattens
515
+ // over `+` into exactly ONE bare-var addend (the base — itself not a non-byte pointer, or
516
+ // `base[i]` would stride its element) plus pure invariant addends (var/const/mul/shl trees
517
+ // whose vars the body never assigns);
518
+ // • the body's statements include exactly one `p = p + 1` and one `i = i + 1` with integer
519
+ // `i` read by the do-while condition; `p` is mentioned nowhere else in the function beyond
520
+ // its init, its step, and derefs AT OFFSET 0 inside this body, and not by the condition.
521
+ function tryExprWalk(
522
+ prev2: Stmt | undefined,
523
+ prev: Stmt | undefined,
524
+ dw: Stmt & { k: 'dowhile' },
525
+ ptrVars: Map<string, IrType>,
526
+ fnBody: Stmt[],
527
+ declTypes: VarTypes,
528
+ volatileLocals: ReadonlySet<string>,
529
+ ): Stmt | null {
530
+ if (prev?.k !== 'assign' || !ptrVars.has(prev.name) || volatileLocals.has(prev.name)) {
531
+ return null;
532
+ }
533
+ const p = prev.name;
534
+ const pT = ptrVars.get(p)!;
535
+ if (pT.kind !== 'ptr' || pT.to.kind !== 'int' || pT.to.width !== 8) {
536
+ return null;
537
+ }
538
+ if (!derefWidths([dw], p).every((w) => w === 1)) {
539
+ return null;
540
+ }
541
+ // EXPR decomposition
542
+ let expr: Expr = prev.value;
543
+ while (expr.k === 'cast') {
544
+ expr = expr.e;
545
+ }
546
+ const addends: Expr[] = [];
547
+ const flatten = (e: Expr): void => {
548
+ if (e.k === 'bin' && e.op === '+') {
549
+ flatten(e.l);
550
+ flatten(e.r);
551
+ } else {
552
+ addends.push(e);
553
+ }
554
+ };
555
+ flatten(expr);
556
+ const bare = addends.filter((a): a is Extract<Expr, { k: 'var' }> => a.k === 'var');
557
+ const rest = addends.filter((a) => a.k !== 'var');
558
+ if (bare.length !== 1) {
559
+ return null;
560
+ }
561
+ const base = bare[0] as Extract<Expr, { k: 'var' }>;
562
+ // declTypes carries params, locals AND globals: a base declared nowhere has an unknowable C
563
+ // stride (its project declaration decides), and a wider pointer strides its element under [i]
564
+ // — both decline.
565
+ const baseT = declTypes(base.name);
566
+ if (baseT === undefined) {
567
+ return null;
568
+ }
569
+ if (baseT.kind === 'ptr' && !(baseT.to.kind === 'int' && baseT.to.width === 8)) {
570
+ return null;
571
+ }
572
+ const pure = (e: Expr): boolean =>
573
+ e.k === 'var' || e.k === 'const'
574
+ ? true
575
+ : e.k === 'bin' && (e.op === '*' || e.op === '+' || e.op === '<<')
576
+ ? pure(e.l) && pure(e.r)
577
+ : e.k === 'cast'
578
+ ? pure(e.e)
579
+ : false;
580
+ const assignedIn = (stmts: Stmt[], name: string): boolean => stmts.some((st) => stmtAssigns(st, name));
581
+ const restVars = new Set<string>();
582
+ const collectVars = (e: Expr): void => {
583
+ if (e.k === 'var') {
584
+ restVars.add(e.name);
585
+ }
586
+ mapExprChildren(e, (c) => {
587
+ collectVars(c);
588
+ return c;
589
+ });
590
+ };
591
+ rest.forEach(collectVars);
592
+ restVars.add(base.name);
593
+ if (!rest.every(pure) || [...restVars].some((n) => assignedIn(dw.body, n))) {
594
+ return null;
595
+ }
596
+ // the steps and the counter
597
+ const isIncOf = (st: Stmt, name?: string): string | null => {
598
+ const n = isUnitIncrement(st);
599
+ return n !== null && (name === undefined || n === name) ? n : null;
600
+ };
601
+ // BOTH steps as the body's contiguous TAIL (either order): every deref then runs with `i`
602
+ // completed steps equal to `p`'s, which is what `BASE[i + REST]` states. A step anywhere
603
+ // earlier (the `*++p` walk, a deref straddling the step) reads a different address.
604
+ const tail = dw.body.slice(-2);
605
+ const tailIncs = tail.map((st) => isIncOf(st)).filter((n): n is string => n !== null);
606
+ if (tailIncs.length !== 2 || !tailIncs.includes(p)) {
607
+ return null;
608
+ }
609
+ const iv = tailIncs.find((n) => n !== p);
610
+ if (iv === undefined) {
611
+ return null;
612
+ }
613
+ const pStepIdx = dw.body.length - 2 + tail.findIndex((st) => isIncOf(st, p) !== null);
614
+ const ivT = declTypes(iv);
615
+ if (ivT !== undefined && ivT.kind !== 'int') {
616
+ return null;
617
+ }
618
+ if (!mentionsVar(dw.cond, iv) || mentionsVar(dw.cond, p)) {
619
+ return null;
620
+ }
621
+ // The counter STARTS AT ZERO (the adjacent `i = 0` init — `BASE[i + REST]` counts completed
622
+ // steps from the walk's own start) and its ONLY write anywhere in the body is the tail step.
623
+ if (prev2?.k !== 'assign' || prev2.name !== iv || prev2.value.k !== 'const' || prev2.value.value !== 0) {
624
+ return null;
625
+ }
626
+ if (dw.body.slice(0, -2).some((st) => stmtAssigns(st, iv)) || tail.filter((st) => isIncOf(st, iv)).length !== 1) {
627
+ return null;
628
+ }
629
+ if (volatileLocals.has(iv) || [...restVars].some((n) => volatileLocals.has(n))) {
630
+ return null;
631
+ }
632
+ // rewrite the derefs, then verify nothing of `p` survives
633
+ const idxOf = (): Expr => rest.reduce<Expr>((acc, r) => ({ k: 'bin', op: '+', l: acc, r }), { k: 'var', name: iv });
634
+ let respelled = true;
635
+ const rewriteExpr = (e: Expr): Expr => {
636
+ const m = mapExprChildren(e, rewriteExpr);
637
+ if (m.k === 'index' && m.base.k === 'var' && m.base.name === p) {
638
+ if (m.idx.k !== 'const' || m.idx.value !== 0) {
639
+ respelled = false;
640
+ return m;
641
+ }
642
+ return { ...m, base: { k: 'var', name: base.name }, idx: idxOf() };
643
+ }
644
+ return m;
645
+ };
646
+ const body = dw.body.filter((_, k) => k !== pStepIdx).map((st) => mapStmtExprs(st, rewriteExpr));
647
+ const dw2: Stmt = { ...dw, body };
648
+ if (!respelled || !confinedToWalk(fnBody, p, countMentions([prev], p), dw)) {
649
+ return null;
650
+ }
651
+ if (countMentions([dw2], p) !== 0) {
652
+ return null;
653
+ }
654
+ return dw2;
655
+ }
656
+
299
657
  /** Try the walk→index re-spelling on one function. Returns the transformed COPY when at least
300
658
  * 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 {
659
+ * candidate only on non-null. Pure: never mutates the input SFn. `keptWalks` collects the names
660
+ * of the pointers each fired loop kept as its base — v1 the walk's base (a param lands in the
661
+ * set too, inertly: the volatile variation marks only declared locals), v2 the walk pointers
662
+ * themselves — the locals the /indexed/volatile composition (rank.ts) narrows the volatile variation
663
+ * to. A v3 loop contributes nothing: it DELETES its pointer, and its base is qualified through
664
+ * the /livebase pairings instead. `gates` is the shared countdown admission table — a parameter
665
+ * so a test can ablate one entry and re-run the real pass. */
666
+ export function reindexWalks(
667
+ sfn: SFn,
668
+ keptWalks?: Set<string>,
669
+ gates: readonly Gate<CountdownCtx>[] = COUNTDOWN_GATES,
670
+ ): SFn | null {
303
671
  const ptrVars = new Map<string, IrType>();
672
+ const declTypes = declaredTypes(sfn);
673
+ // BOTH volatility facts (ast.ts SFn.locals): the object-volatile counter, and the pointer whose
674
+ // POINTEE is volatile — which is the one the `/volatile` variation mints, and the one a walk carries.
675
+ const volatileLocals = new Set(
676
+ sfn.locals.filter((l) => l.volatile === true || l.pointeeVolatile === true).map((l) => l.name),
677
+ );
678
+ const storage = nameStorage(sfn);
304
679
  for (const v of [...sfn.params, ...sfn.locals]) {
305
680
  if (v.type.kind === 'ptr') {
306
681
  ptrVars.set(v.name, v.type);
@@ -311,7 +686,7 @@ export function reindexWalks(sfn: SFn): SFn | null {
311
686
  }
312
687
 
313
688
  let fired = 0;
314
- let ivCount = 0;
689
+ const taken = takenNames(sfn);
315
690
  const locals = [...sfn.locals];
316
691
 
317
692
  // SOUNDNESS GATE (adversarially learned; every rule REPRODUCED as a wrong-bytes or crash
@@ -320,14 +695,11 @@ export function reindexWalks(sfn: SFn): SFn | null {
320
695
  // • base and p must be pointers of the SAME element size, and every deref of p must read
321
696
  // exactly that size — otherwise the walk (strides p's pointee) and the indexed form
322
697
  // (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 =>
698
+ // • confinedToWalk the global mention accounting, shared with v3.
699
+ const soundWalk = (walk: WalkLoop, mentionsOutsideLoop: number, loop: Stmt): boolean =>
327
700
  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;
701
+ strideAgrees(ptrVars.get(walk.p), ptrVars.get(walk.base), derefWidths([loop], walk.p)) &&
702
+ confinedToWalk(sfn.body, walk.p, mentionsOutsideLoop, loop);
331
703
 
332
704
  // walk a statement LIST so the `while` shape can see its preceding init statement
333
705
  const walkList = (stmts: Stmt[]): Stmt[] => {
@@ -343,15 +715,15 @@ export function reindexWalks(sfn: SFn): SFn | null {
343
715
  init.k === 'assign' &&
344
716
  init.name === p &&
345
717
  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
718
+ // a `for`'s init lives inside the loop node, so confinedToWalk already counts it
348
719
  soundWalk({ p, base: init.value.name }, 0, s)
349
720
  ) {
350
721
  const walk: WalkLoop = { p, base: init.value.name };
351
- const iv = freshIv();
722
+ const iv = nextIv();
352
723
  const cond = reindexCond(s.cond, walk, iv);
353
724
  const body = cond ? reindexStmts(s.body, walk, iv) : null;
354
725
  if (cond && body) {
726
+ declareIv(iv);
355
727
  out.push({
356
728
  k: 'for',
357
729
  init: { k: 'assign', name: iv, value: { k: 'const', value: 0 } },
@@ -364,9 +736,9 @@ export function reindexWalks(sfn: SFn): SFn | null {
364
736
  body,
365
737
  });
366
738
  fired++;
739
+ keptWalks?.add(walk.base);
367
740
  continue;
368
741
  }
369
- retireIv();
370
742
  }
371
743
  }
372
744
  // `p = base; while (p < base + n) { …; p = p + 1; }`
@@ -382,10 +754,11 @@ export function reindexWalks(sfn: SFn): SFn | null {
382
754
  soundWalk({ p, base: prev.value.name }, 1, s)
383
755
  ) {
384
756
  const walk: WalkLoop = { p, base: prev.value.name };
385
- const iv = freshIv();
757
+ const iv = nextIv();
386
758
  const cond = reindexCond(s.cond, walk, iv);
387
759
  const body = cond ? reindexStmts(s.body.slice(0, -1), walk, iv) : null;
388
760
  if (cond && body) {
761
+ declareIv(iv);
389
762
  out[out.length - 1] = { k: 'assign', name: iv, value: { k: 'const', value: 0 } };
390
763
  out.push({
391
764
  k: 'while',
@@ -400,9 +773,40 @@ export function reindexWalks(sfn: SFn): SFn | null {
400
773
  ],
401
774
  });
402
775
  fired++;
776
+ keptWalks?.add(walk.base);
403
777
  continue;
404
778
  }
405
- retireIv();
779
+ }
780
+ }
781
+ // v3 — the up-counting byte walk with an expression base: `p = (u8 *)(EXPR)` immediately
782
+ // ahead of a counter-carried do-while (see tryExprWalk)
783
+ if (s.k === 'dowhile') {
784
+ const prev = out[out.length - 1];
785
+ const prev2 = out[out.length - 2];
786
+ const dw2 = tryExprWalk(prev2, prev, s, ptrVars, sfn.body, declTypes, volatileLocals);
787
+ if (dw2) {
788
+ out.pop(); // the walk init is subsumed by the indexed spelling
789
+ out.push(recurse(dw2));
790
+ fired++;
791
+ continue;
792
+ }
793
+ // v4 — the unguarded constant-trip countdown (see the header)
794
+ const cd = respellConstCountdown(out, s);
795
+ if (cd) {
796
+ out.length = 0;
797
+ out.push(...cd.pre, cd.counted);
798
+ fired++;
799
+ continue;
800
+ }
801
+ }
802
+ // the guarded countdown (v2 — see the header): an `if` whose one arm is skip-copies C and
803
+ // whose other arm is C ⊎ inductions followed by the do-while
804
+ if (s.k === 'if') {
805
+ const r = respellGuardedCountdown(s);
806
+ if (r) {
807
+ out.push(...r);
808
+ fired++;
809
+ continue;
406
810
  }
407
811
  }
408
812
  // recurse into compound statements without re-spelling them
@@ -431,19 +835,257 @@ export function reindexWalks(sfn: SFn): SFn | null {
431
835
  }
432
836
  };
433
837
 
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++}`;
838
+ /** The countdown machinery the guarded (v2) and the unguarded constant-trip (v4) shapes share:
839
+ * everything downstream of "which statement inits the counter, and what is the trip count".
840
+ * Recognizes the shape, collects what `gates` reads, and refuses on the first entry that
841
+ * rejects. `confineTo` names the statements every mention of the counter and of each walk
842
+ * pointer must live in, given the induction inits this found; `accept` is the caller's own
843
+ * admission. Returns the inits it consumed, `pre` minus them, and the counted `for` — or null
844
+ * (decline). */
845
+ function respellCountdown(
846
+ loop: Stmt & { k: 'dowhile' },
847
+ pre: Stmt[],
848
+ k: string,
849
+ kInit: Stmt & { k: 'assign' },
850
+ trip: Expr,
851
+ confineTo: (induction: Stmt[]) => Stmt[],
852
+ accept?: (leftovers: Stmt[]) => boolean,
853
+ ): { inductionInits: Set<Stmt>; leftovers: Stmt[]; counted: Stmt & { k: 'for' } } | null {
854
+ // SHAPE RECOGNITION — the body's contiguous tail is `k -= 1` and one `p += 1` per walk
855
+ // pointer. Not an admission rule: without a decrement and a step there is no countdown here
856
+ // at all, and the facts the rules read are derived from what this scan found.
857
+ const isDec = (x: Stmt): boolean =>
858
+ x.k === 'assign' &&
859
+ x.name === k &&
860
+ x.value.k === 'bin' &&
861
+ x.value.op === '-' &&
862
+ x.value.l.k === 'var' &&
863
+ x.value.l.name === k &&
864
+ x.value.r.k === 'const' &&
865
+ x.value.r.value === 1;
866
+ const walks: string[] = [];
867
+ let cut = loop.body.length;
868
+ let sawDec = false;
869
+ let dupStep = false;
870
+ for (let i = loop.body.length - 1; i >= 0; i--) {
871
+ const x = loop.body[i];
872
+ const p = isUnitStep(x, ptrVars);
873
+ if (p !== null) {
874
+ dupStep ||= walks.includes(p);
875
+ walks.push(p);
876
+ cut = i;
877
+ continue;
878
+ }
879
+ if (!sawDec && isDec(x)) {
880
+ sawDec = true;
881
+ cut = i;
882
+ continue;
883
+ }
884
+ break;
440
885
  }
441
- locals.push({ name, type: T.s(32) });
442
- return name;
886
+ if (!sawDec || walks.length === 0) {
887
+ return null;
888
+ }
889
+ const bodyCore = loop.body.slice(0, cut);
890
+ const inits = new Map<string, Stmt & { k: 'assign' }>();
891
+ const badBases: string[] = [];
892
+ for (const p of walks) {
893
+ const init = pre.find((x): x is Stmt & { k: 'assign' } => x.k === 'assign' && x.name === p);
894
+ const bOk =
895
+ init &&
896
+ (init.value.k === 'var' ? init.value.name !== p && init.value.name !== k : rematerializableAddress(init.value));
897
+ if (bOk) {
898
+ inits.set(p, init);
899
+ } else {
900
+ badBases.push(p);
901
+ }
902
+ }
903
+ const shape = confineTo([kInit, ...inits.values(), loop]);
904
+ const inductionInits = new Set<Stmt>([kInit, ...inits.values()]);
905
+ const leftovers = pre.filter((x) => !inductionInits.has(x));
906
+ const elemSize = (t: IrType | undefined): number =>
907
+ t?.kind === 'ptr' ? (t.to.kind === 'int' ? t.to.width / 8 : t.to.kind === 'ptr' ? 4 : 0) : 0;
908
+ const ctx: CountdownCtx = {
909
+ kIsPointer: ptrVars.has(k),
910
+ kIsDeclared: storage.get(k) === 'local' || storage.get(k) === 'param',
911
+ kIsVolatile: volatileLocals.has(k),
912
+ dupStep,
913
+ volatileWalks: walks.filter((p) => volatileLocals.has(p)),
914
+ coreHasExit: hasLoopExit(bodyCore),
915
+ badBases,
916
+ kMentions: countMentions(sfn.body, k),
917
+ unconfined: walks.filter((p) => countMentions(sfn.body, p) !== countMentions(shape, p)),
918
+ badStrides: walks.filter((p) => {
919
+ const es = elemSize(ptrVars.get(p));
920
+ return es === 0 || !derefWidths([loop], p).every((w) => w === es);
921
+ }),
922
+ leakyLeftovers: leftovers.filter((x) => walks.some((w) => stmtMentions(x, w))).length,
923
+ };
924
+ if (firstRejection(gates, ctx) !== null) {
925
+ return null;
926
+ }
927
+ // The caller's own admission. It runs before anything is minted, and so does every rule
928
+ // above: the rewrite below is the first statement in this function that mutates `locals` or
929
+ // `keptWalks`, so a decline has nothing to undo.
930
+ if (accept && !accept(leftovers)) {
931
+ return null;
932
+ }
933
+ // rewrite: each walk pointer is its OWN base (kept local, step dropped)
934
+ const iv = nextIv();
935
+ let body: Stmt[] | null = bodyCore;
936
+ for (const p of walks) {
937
+ body = body === null ? null : reindexStmts(body, { p, base: p }, iv);
938
+ }
939
+ if (body === null) {
940
+ return null;
941
+ }
942
+ declareIv(iv);
943
+ for (const p of walks) {
944
+ keptWalks?.add(p);
945
+ }
946
+ // Nothing mentions the counter past this point: its init, its decrement and the exit test are
947
+ // all gone, and the four-roles rule says those were every mention it had. Its declaration goes
948
+ // with them — a local declared, never written and never read reads as the deliberately
949
+ // uninitialized slot `uninit` exists to mark. A PARAM keeps its declaration, having one for a
950
+ // reason the body does not decide.
951
+ const kDecl = locals.findIndex((l) => l.name === k);
952
+ if (kDecl >= 0) {
953
+ locals.splice(kDecl, 1);
954
+ }
955
+ return {
956
+ inductionInits,
957
+ leftovers,
958
+ counted: {
959
+ k: 'for',
960
+ init: { k: 'assign', name: iv, value: { k: 'const', value: 0 } },
961
+ cond: { k: 'bin', op: '<', l: { k: 'var', name: iv }, r: trip },
962
+ inc: {
963
+ k: 'assign',
964
+ name: iv,
965
+ value: { k: 'bin', op: '+', l: { k: 'var', name: iv }, r: { k: 'const', value: 1 } },
966
+ },
967
+ body,
968
+ },
969
+ };
970
+ }
971
+
972
+ /** v2: `s` re-spelled as [C…, walk inits…, for], or null (decline). */
973
+ function respellGuardedCountdown(s: Stmt & { k: 'if' }): Stmt[] | null {
974
+ // which arm holds the loop, and does the guard skip it in the right sense?
975
+ const armWith = (arm: Stmt[]): { pre: Stmt[]; loop: Stmt & { k: 'dowhile' } } | null => {
976
+ const last = arm[arm.length - 1];
977
+ return last?.k === 'dowhile' ? { pre: arm.slice(0, -1), loop: last } : null;
978
+ };
979
+ const inElse = armWith(s.else);
980
+ const inThen = armWith(s.then);
981
+ const c = s.cond;
982
+ const guardSkips = (n: string, loopInElse: boolean): boolean => {
983
+ if (c.k !== 'bin') {
984
+ return false;
985
+ }
986
+ const zl = c.l.k === 'const' && c.l.value === 0 && c.r.k === 'var' && c.r.name === n;
987
+ const zr = c.r.k === 'const' && c.r.value === 0 && c.l.k === 'var' && c.l.name === n;
988
+ // loop in else ⇒ cond true skips: 0 >= n / n <= 0. Loop in then ⇒ cond true enters: 0 < n / n > 0.
989
+ return loopInElse ? (zl && c.op === '>=') || (zr && c.op === '<=') : (zl && c.op === '<') || (zr && c.op === '>');
990
+ };
991
+ const pick = inElse ?? inThen;
992
+ const skipArm = inElse ? s.then : s.else;
993
+ if (!pick || (inElse && inThen)) {
994
+ return null;
995
+ }
996
+ const { pre, loop } = pick;
997
+ // the do-while exit: exactly `k != 0`
998
+ const lc = loop.cond;
999
+ if (lc.k !== 'bin' || lc.op !== '!=' || lc.l.k !== 'var' || lc.r.k !== 'const' || lc.r.value !== 0) {
1000
+ return null;
1001
+ }
1002
+ const k = lc.l.name;
1003
+ // the counter's init `k = N`, N a var the guard tests
1004
+ const kInit = pre.find((x): x is Stmt & { k: 'assign' } => x.k === 'assign' && x.name === k);
1005
+ if (!kInit || kInit.value.k !== 'var') {
1006
+ return null;
1007
+ }
1008
+ const n = kInit.value.name;
1009
+ if (n === k || !guardSkips(n, pick === inElse)) {
1010
+ return null;
1011
+ }
1012
+ if (sfn.body.some((x) => stmtAssigns(x, n))) {
1013
+ return null; // a moving bound
1014
+ }
1015
+ // the skip arm must be, in order, exactly `pre` minus the induction inits
1016
+ const skipArmMatches = (leftovers: Stmt[]): boolean =>
1017
+ skipArm.length === leftovers.length &&
1018
+ skipArm.every((x, i) => JSON.stringify(x) === JSON.stringify(leftovers[i]));
1019
+ const r = respellCountdown(loop, pre, k, kInit, { k: 'var', name: n }, () => [s], skipArmMatches);
1020
+ if (!r) {
1021
+ return null;
1022
+ }
1023
+ return [...pre.filter((x) => x !== kInit), r.counted];
1024
+ }
1025
+
1026
+ /** v4: the UNGUARDED constant-trip countdown (see the header). `out` holds the statements
1027
+ * already emitted before `dw`, the induction inits among them. Returns the list replacing
1028
+ * `out` (itself minus the counter's init) and the counted `for`, or null (decline). */
1029
+ function respellConstCountdown(out: Stmt[], dw: Stmt & { k: 'dowhile' }): { pre: Stmt[]; counted: Stmt } | null {
1030
+ // the do-while exit: exactly `k >= 0` / `0 <= k`
1031
+ const lc = dw.cond;
1032
+ if (lc.k !== 'bin') {
1033
+ return null;
1034
+ }
1035
+ const kv =
1036
+ lc.op === '>=' && lc.l.k === 'var' && lc.r.k === 'const' && lc.r.value === 0
1037
+ ? lc.l.name
1038
+ : lc.op === '<=' && lc.r.k === 'var' && lc.l.k === 'const' && lc.l.value === 0
1039
+ ? lc.r.name
1040
+ : null;
1041
+ if (kv === null) {
1042
+ return null;
1043
+ }
1044
+ // `k >= 0` terminates only for a SIGNED counter — an unsigned one never goes below zero, and
1045
+ // the compiler that emitted this test proved it signed (`bge`). A declaration asmlift settled
1046
+ // on unsigned describes a different loop, so the trip count `C + 1` would be a fiction.
1047
+ const kT = declTypes(kv);
1048
+ if (kT?.kind !== 'int' || !kT.signed) {
1049
+ return null;
1050
+ }
1051
+ // the counter's init `k = C`, C a NON-NEGATIVE constant: the loop runs while k counts C…0, so
1052
+ // the trip count is C + 1. That is a count only while BOTH ends are representable in the
1053
+ // counter's OWN type — below zero `k = -1` runs the body once rather than `C + 1 = 0` times,
1054
+ // and above its maximum `k = 200` in an `s8` is -56, which fails `k >= 0` at the first test
1055
+ // and runs the body once where `i < 201` runs it 201 times.
1056
+ const kMax = 2 ** (kT.width - 1) - 1;
1057
+ const kInit = out.find((x): x is Stmt & { k: 'assign' } => x.k === 'assign' && x.name === kv);
1058
+ if (!kInit || kInit.value.k !== 'const' || kInit.value.value < 0 || kInit.value.value >= kMax) {
1059
+ return null;
1060
+ }
1061
+ const r = respellCountdown(dw, out, kv, kInit, { k: 'const', value: kInit.value.value + 1 }, (i) => i);
1062
+ if (!r) {
1063
+ return null;
1064
+ }
1065
+ return { pre: out.filter((x) => x !== kInit), counted: r.counted };
1066
+ }
1067
+
1068
+ /** The next free induction name: the lowest `i<N>` nothing in `sfn` already claims and no
1069
+ * earlier mint has COMMITTED. Pipeline naming is `a`/`v`/`t` prefixed, but a global is
1070
+ * referenced by bare name and future naming (DWARF) may import real source ones, so the
1071
+ * collision set is `takenNames`' — declarations, mentions, call targets and assignment
1072
+ * targets alike — not the declaration lists.
1073
+ *
1074
+ * PURE: it reads that snapshot and the locals `declareIv` committed, so a recognizer that
1075
+ * mints and then declines leaves the next mint returning the same name. Callers hold at most
1076
+ * one uncommitted name at a time — every mint site rewrites and commits before the walk
1077
+ * recursion can reach another loop. */
1078
+ function nextIv(): string {
1079
+ let n = 0;
1080
+ while (taken.has(`i${n}`) || locals.some((x) => x.name === `i${n}`)) {
1081
+ n++;
1082
+ }
1083
+ return `i${n}`;
443
1084
  }
444
- function retireIv(): void {
445
- locals.pop();
446
- ivCount--;
1085
+ /** Commit a minted name: the ONE place the induction local is declared, called only once the
1086
+ * rewrite has succeeded. */
1087
+ function declareIv(name: string): void {
1088
+ locals.push({ name, type: T.s(32) });
447
1089
  }
448
1090
 
449
1091
  const body = walkList(sfn.body);