@asmlift/core 0.8.1 → 0.10.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 (130) hide show
  1. package/README.md +36 -31
  2. package/package.json +1 -1
  3. package/src/aggregate.ts +397 -0
  4. package/src/backend/c.ts +6 -2
  5. package/src/backend/cfamily.ts +370 -56
  6. package/src/backend/cpp.ts +116 -10
  7. package/src/backend/pascal.ts +69 -11
  8. package/src/cdecl/constant.ts +59 -0
  9. package/src/cdecl/lex.ts +209 -0
  10. package/src/cdecl/parse.ts +975 -0
  11. package/src/cdecl/spell.ts +69 -0
  12. package/src/codegen-flags.ts +38 -6
  13. package/src/compiler-diagnostics.ts +194 -0
  14. package/src/contracts.ts +777 -84
  15. package/src/declare.ts +132 -13
  16. package/src/frontend/asmdata.ts +26 -6
  17. package/src/frontend/call-plan.ts +1590 -0
  18. package/src/frontend/dead-reads.ts +628 -0
  19. package/src/frontend/device-pins.ts +296 -0
  20. package/src/frontend/emit.ts +4 -3
  21. package/src/frontend/errors.ts +5 -0
  22. package/src/frontend/flags-edge.ts +101 -0
  23. package/src/frontend/fpu.ts +374 -0
  24. package/src/frontend/frame-objects.ts +3171 -0
  25. package/src/frontend/frontend.ts +64 -3
  26. package/src/frontend/high-half.ts +55 -5
  27. package/src/frontend/jump-table.ts +530 -0
  28. package/src/frontend/local-object.ts +616 -0
  29. package/src/frontend/mips.ts +2392 -208
  30. package/src/frontend/opaque.ts +97 -18
  31. package/src/frontend/own-struct-return.ts +500 -0
  32. package/src/frontend/ppc.ts +3137 -267
  33. package/src/frontend/register-struct-call.ts +484 -0
  34. package/src/frontend/reloc-symbol.ts +109 -13
  35. package/src/frontend/splat.ts +108 -5
  36. package/src/frontend/ssa.ts +2098 -89
  37. package/src/frontend/stackargs.ts +172 -42
  38. package/src/frontend/thumb.ts +4563 -2091
  39. package/src/ir/alias.ts +71 -0
  40. package/src/ir/bits.ts +681 -1
  41. package/src/ir/core.ts +199 -6
  42. package/src/ir/discipline.ts +313 -0
  43. package/src/ir/entry.ts +159 -0
  44. package/src/ir/float-bits.ts +47 -0
  45. package/src/{structure → ir}/loops.ts +6 -2
  46. package/src/ir/opcodes.ts +193 -121
  47. package/src/ir/parse.ts +1 -0
  48. package/src/ir/types.ts +202 -2
  49. package/src/ir/verify.ts +219 -3
  50. package/src/l3/address.ts +5 -0
  51. package/src/l3/argcopy.ts +38 -6
  52. package/src/l3/ast.ts +467 -23
  53. package/src/l3/basecse.ts +33 -5
  54. package/src/l3/coalesce.ts +21 -13
  55. package/src/l3/constinit.ts +382 -0
  56. package/src/l3/dce.ts +34 -12
  57. package/src/l3/gates.ts +9 -3
  58. package/src/l3/hoist.ts +8 -7
  59. package/src/l3/initfirst.ts +68 -19
  60. package/src/l3/intcell.ts +98 -0
  61. package/src/l3/mentions.ts +10 -4
  62. package/src/l3/narrowdecl.ts +259 -0
  63. package/src/l3/nearbase.ts +10 -5
  64. package/src/l3/parkfirst.ts +8 -5
  65. package/src/l3/pollguard.ts +11 -4
  66. package/src/l3/ptrcell.ts +354 -0
  67. package/src/l3/ptrfield.ts +123 -8
  68. package/src/l3/regspell.ts +26 -7
  69. package/src/l3/reindex.ts +293 -103
  70. package/src/l3/storage.ts +98 -1
  71. package/src/l3/symbol-refs.ts +182 -23
  72. package/src/l3/tailmerge.ts +9 -20
  73. package/src/l3/typing.ts +237 -32
  74. package/src/l3/unreduce.ts +10 -9
  75. package/src/l3/volatileptr.ts +17 -54
  76. package/src/l3/volstore.ts +96 -7
  77. package/src/mangle.ts +41 -1
  78. package/src/pattern/engine.ts +373 -82
  79. package/src/pipeline.ts +589 -27
  80. package/src/proto-context.ts +1648 -0
  81. package/src/proto.ts +1203 -49
  82. package/src/raise/aggmembers.ts +290 -0
  83. package/src/raise/arrays.ts +8 -3
  84. package/src/raise/declared-volatile.ts +307 -0
  85. package/src/raise/divpow2.ts +2 -2
  86. package/src/raise/errors.ts +17 -0
  87. package/src/raise/floathelpers.ts +464 -0
  88. package/src/raise/globalshape.ts +11 -5
  89. package/src/raise/gvn.ts +29 -8
  90. package/src/raise/latch.ts +162 -6
  91. package/src/raise/memberarrays.ts +10 -4
  92. package/src/raise/narrow.ts +29 -2
  93. package/src/raise/narrowlocal.ts +11 -6
  94. package/src/raise/offsetnames.ts +494 -0
  95. package/src/raise/pairparams.ts +114 -0
  96. package/src/raise/paramwidth.ts +203 -6
  97. package/src/raise/pre-recovery.ts +174 -17
  98. package/src/raise/pre-structure.ts +54 -0
  99. package/src/raise/recover.ts +26 -15
  100. package/src/raise/reroll.ts +694 -0
  101. package/src/raise/retsink.ts +1 -1
  102. package/src/raise/shortcircuit.ts +66 -30
  103. package/src/raise/softdiv.ts +21 -23
  104. package/src/raise/storesign.ts +153 -0
  105. package/src/raise/struct-arrays.ts +34 -14
  106. package/src/raise/structs.ts +674 -98
  107. package/src/raise/truncload.ts +367 -0
  108. package/src/raise/unthread.ts +400 -0
  109. package/src/raise/widehelpers.ts +106 -0
  110. package/src/rank-declare.ts +143 -12
  111. package/src/rank-variations.ts +117 -31
  112. package/src/rank.ts +563 -122
  113. package/src/runtime-helpers.ts +405 -0
  114. package/src/stillborn.ts +289 -0
  115. package/src/structure/analysis.ts +1449 -449
  116. package/src/structure/arith-ops.ts +23 -0
  117. package/src/structure/globalaccess.ts +111 -9
  118. package/src/structure/hazards.ts +641 -114
  119. package/src/structure/local-statics.ts +517 -0
  120. package/src/structure/namecoalesce.ts +2 -2
  121. package/src/structure/pointer-spelling.ts +1106 -0
  122. package/src/structure/redundant-test.ts +99 -0
  123. package/src/structure/structure.ts +3478 -975
  124. package/src/structure/switch-recover.ts +403 -178
  125. package/src/symbols.ts +44 -3
  126. package/src/target.ts +1387 -71
  127. package/src/trace.ts +66 -28
  128. package/src/variation-definitions.ts +243 -13
  129. package/src/variation-gates.ts +2 -0
  130. package/src/variation-tokens.ts +25 -7
@@ -40,6 +40,7 @@ function isPure(e: Expr): boolean {
40
40
  switch (e.k) {
41
41
  case 'var':
42
42
  case 'const':
43
+ case 'fconst':
43
44
  return true;
44
45
  case 'un':
45
46
  case 'cast':
@@ -56,6 +57,9 @@ function renameVar(e: Expr, from: string, to: string): Expr {
56
57
  if (e.k === 'var') {
57
58
  return e.name === from ? { k: 'var', name: to } : e;
58
59
  }
60
+ if (e.k === 'postincr') {
61
+ return e.name === from ? { ...e, name: to } : e;
62
+ }
59
63
  return mapExprChildren(e, (c) => renameVar(c, from, to));
60
64
  }
61
65
 
@@ -185,16 +189,21 @@ function condOtherMentions(cond: Expr, E: Expr, v: string): boolean {
185
189
  };
186
190
  return other ? mentions(other) : false;
187
191
  }
188
- /** cond compares E against a pure operand → same comparison reading the named var. */
192
+ /** cond compares E against a pure operand → same comparison reading the named var.
193
+ * Both rebuilds below SPREAD the compare, so its `unsignedCmp` evidence (l3/ast.ts) rides along:
194
+ * the copy is declared at E's type, which keeps the compare's C meaning but not its asm meaning,
195
+ * and rank.ts's judgment of the finished tree (typing.ts `signedOverUnsignedCompare`) is what
196
+ * says whether `w0 >= a1` still compares the way the machine did. The negation keeps it too: the
197
+ * complement of an unsigned ordering is unsigned. */
189
198
  function rewriteCond(cond: Expr, E: Expr, name: string): Expr | null {
190
199
  if (cond.k !== 'bin' || !(cond.op in FLIP)) {
191
200
  return null;
192
201
  }
193
202
  if (exprEq(cond.l, E) && isPure(cond.r)) {
194
- return { k: 'bin', op: cond.op, l: { k: 'var', name }, r: cond.r };
203
+ return { ...cond, l: { k: 'var', name } };
195
204
  }
196
205
  if (exprEq(cond.r, E) && isPure(cond.l)) {
197
- return { k: 'bin', op: cond.op, l: cond.l, r: { k: 'var', name } };
206
+ return { ...cond, r: { k: 'var', name } };
198
207
  }
199
208
  return null;
200
209
  }
@@ -203,7 +212,7 @@ function flipCmp(cond: Expr): Expr | null {
203
212
  return null;
204
213
  }
205
214
  const op = FLIP[cond.op];
206
- return op ? { k: 'bin', op: op as Extract<Expr, { k: 'bin' }>['op'], l: cond.l, r: cond.r } : null;
215
+ return op ? { ...cond, op: op as Extract<Expr, { k: 'bin' }>['op'] } : null;
207
216
  }
208
217
  /** in `upd`, the occurrence of subtree E replaced by var `name` (E was just assigned to it). */
209
218
  function renameSubexpr(e: Expr, E: Expr, name: string): Expr {
@@ -251,12 +260,22 @@ export function registerishSpellings(sfn: SFn): RegcopySpelling[] {
251
260
  // • E and the cond pure; the cond's non-E operand must NOT mention v (a clamp's
252
261
  // `if (a < v)` would compare against the POST-assignment v — reproduced);
253
262
  // • the copy w carries E's RENDERED type, not v's declared one (retyping a u32
254
- // comparison signed flipped its sense — reproduced).
255
- if (!isPure(E) || !isPure(cond) || condOtherMentions(cond, E, v)) {
263
+ // comparison signed flipped its sense — reproduced), PROMOTED: the update assigns
264
+ // back into w, and a narrow w would truncate what the update computed;
265
+ // • v not narrow, because then v's declaration IS the update's truncation, which a
266
+ // word-wide w would drop.
267
+ const vType = typeOf(v);
268
+ if (
269
+ !isPure(E) ||
270
+ !isPure(cond) ||
271
+ condOtherMentions(cond, E, v) ||
272
+ (vType.kind === 'int' && vType.width < 32)
273
+ ) {
256
274
  out.push(s);
257
275
  continue;
258
276
  }
259
- const wType = exprCType(E, declaredTypes({ ...sfn, locals })) ?? typeOf(v);
277
+ const rendered = exprCType(E, declaredTypes({ ...sfn, locals })) ?? vType;
278
+ const wType = rendered.kind === 'int' && rendered.width < 32 ? T.s(32) : rendered;
260
279
  const w = freshVar(wType);
261
280
  const condOnW = rewriteCond(cond, E, w);
262
281
  if (condOnW) {
package/src/l3/reindex.ts CHANGED
@@ -70,7 +70,7 @@
70
70
  // Past the table, the counter's DECLARATION is retired with its writes: nothing mentions it any
71
71
  // more, and a declared-but-untouched local reads as the deliberate `uninit` slot it is not.
72
72
  //
73
- // v3 SCOPE — the up-counting BYTE walk with an EXPRESSION base (`p = (u8 *)(EXPR)` ahead of a
73
+ // v3 SCOPE — the up-counting walk with an EXPRESSION base (`p = (T *)(EXPR)` ahead of a
74
74
  // counter-carried do-while): full rules at `tryExprWalk` below.
75
75
  //
76
76
  // v4 SCOPE — the UNGUARDED countdown, the shape agbcc emits for `for(i=0;i<C;i++) a[i]` with a
@@ -95,9 +95,11 @@ import {
95
95
  Stmt,
96
96
  mapExprChildren,
97
97
  mapStmtExprs,
98
+ mentionedName,
98
99
  rematerializableAddress,
99
100
  stmtChildren,
100
101
  stmtExprs,
102
+ walkExprs,
101
103
  } from './ast';
102
104
  import { type Gate, firstRejection } from './gates';
103
105
  import { takenNames } from './hoist';
@@ -109,14 +111,15 @@ interface WalkLoop {
109
111
  base: string; // the var `p` was initialised from
110
112
  }
111
113
 
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. */
114
+ /** Total mentions of `name` across a statement list (reads, writes, everywhere) — `mentionedName`'s
115
+ * whole vocabulary, because the accounting these recognizers rest on is "nothing else in the
116
+ * function touches this name", which a var-only count cannot say. `&x` is how a frame local's
117
+ * address renders (structure.ts laddrName), and an escaped address is a read of the object by
118
+ * whatever holds it. */
116
119
  function countMentions(stmts: Stmt[], name: string): number {
117
120
  let n = 0;
118
121
  const inExpr = (e: Expr): void => {
119
- if ((e.k === 'var' || e.k === 'addr') && e.name === name) {
122
+ if (mentionedName(e) === name) {
120
123
  n++;
121
124
  }
122
125
  mapExprChildren(e, (c) => {
@@ -170,8 +173,9 @@ function derefWidths(stmts: Stmt[], p: string): number[] {
170
173
 
171
174
  /** Does `e` mention `name` anywhere — as a value or as an escaped address (see countMentions)? */
172
175
  function mentionsVar(e: Expr, name: string): boolean {
173
- if (e.k === 'var' || e.k === 'addr') {
174
- return e.name === name;
176
+ const own = mentionedName(e);
177
+ if (own !== undefined) {
178
+ return own === name;
175
179
  }
176
180
  let found = false;
177
181
  mapExprChildren(e, (c) => {
@@ -298,11 +302,13 @@ export const COUNTDOWN_GATES: readonly Gate<CountdownCtx>[] = [
298
302
  ];
299
303
 
300
304
  /** 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). */
305
+ * inside a nested loop (whose own exits target itself). A switch's own `break` (`ofSwitch`)
306
+ * leaves only its switch, and the loop's tail still runs after it, so it is not one. */
302
307
  function hasLoopExit(stmts: Stmt[]): boolean {
303
308
  return stmts.some((s) => {
304
309
  switch (s.k) {
305
310
  case 'break':
311
+ return s.ofSwitch !== true;
306
312
  case 'continue':
307
313
  return true;
308
314
  case 'if':
@@ -421,6 +427,11 @@ function reindexCond(cond: Expr, walk: WalkLoop, iv: string): Expr | null {
421
427
  return null;
422
428
  }
423
429
  const i: Expr = { k: 'var', name: iv };
430
+ // A fresh node, so the compare's `unsignedCmp` evidence (l3/ast.ts) is dropped, and that is
431
+ // PROVED rather than lost: `p < base + n` compared addresses, which C compares unsigned, and the
432
+ // walk makes `p == base + i` (scaled), so `i < n` compares the same values. Kept, the evidence
433
+ // would have rank.ts withhold the index spelling over an `s32` counter that the pointer
434
+ // spelling, judged on the same rule, never owed.
424
435
  return swap ? { k: 'bin', op: cond.op, l: n, r: i } : { k: 'bin', op: cond.op, l: i, r: n };
425
436
  }
426
437
 
@@ -501,46 +512,81 @@ function reindexStmt(s: Stmt, walk: WalkLoop, iv: string): Stmt | null {
501
512
  }
502
513
  }
503
514
 
504
- // ── v3: the up-counting BYTE walk with an EXPRESSION base ────────────────────────────────────
515
+ /** Whether any statement here, at any depth, writes memory (`store`). */
516
+ function stmtsStore(stmts: Stmt[]): boolean {
517
+ return stmts.some((s) => s.k === 'store' || stmtsStore(stmtChildren(s)));
518
+ }
519
+
520
+ /** `e + c`, spelled `e - |c|` for a negative c and `e` alone for 0. */
521
+ function plusConst(e: Expr, c: number): Expr {
522
+ return c === 0 ? e : { k: 'bin', op: c < 0 ? '-' : '+', l: e, r: { k: 'const', value: Math.abs(c) } };
523
+ }
524
+
525
+ // ── v3: the up-counting walk with an EXPRESSION base ─────────────────────────────────────────
505
526
  //
506
- // p = (u8 *)(EXPR); do { … *p … ; p = p + 1; i = i + 1; } while (i < n);
527
+ // i = S; p = (T *)(EXPR); do { … *p … ; p = p + 1; i = i + 1; } while (i < n);
507
528
  //
508
529
  // 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,
530
+ // reduction: deleting its init and step and spelling the deref as BASE[i - S + REST], REST in
531
+ // elements — the sole base addend of EXPR as the index base, the counter FIRST in the index sum —
532
+ // is the indexed source the compiler reduces back. One counter may carry SEVERAL walk pointers
533
+ // (KMC reduces `for (i = 1; …) { gH[i] = 7; gW[i] = 8; }` to one pointer per array): each keeps
534
+ // its own base and unit, and each re-spells on its own merits — one out of scope keeps walking,
535
+ // whatever the order of the steps. v3 SCOPE (decline over approximate):
536
+ // • each `p` is a `T *` over an integer T of W bytes, and every deref of `p` reads W. EXPR's sum
537
+ // counts in its BASE's unit. Over a W-element pointer base that is already elements, and REST
538
+ // stays verbatim, symbolic addends included. Over a byte base (a non-pointer var, a byte
539
+ // pointer, `(u32)&g`) a wider walk needs a constant total W divides (it is rescaled to
540
+ // elements), and a symbolic addend, which would need dividing, stays in the BYTE sum instead:
541
+ // the index base is `(T *)(BASE + SYM)`, the spelling `p = b + k; … p[i]` reduces back to;
542
+ // • the inits `i = S` and `p_k = EXPR_k` are the run of assignments immediately ahead of the
543
+ // `dowhile` (any order); EXPR (its pointer and 32-bit integer casts stripped; a narrowing cast
544
+ // declines) flattens over `+` into exactly ONE base addend — a bare var declared a non-pointer,
545
+ // a byte pointer or a pointer to W-byte integers (any other pointer strides a unit no element
546
+ // index names), or an object's address, `(u32)&g` beside other addends or `&g` alone — plus
547
+ // pure invariant addends (var/const/mul/shl trees whose vars the body never assigns, and,
548
+ // beside a call or a store in the loop, only params and locals whose address never escapes);
549
+ // • the body's contiguous TAIL is exactly one `p_k = p_k + 1` per walk pointer and one
550
+ // `i = i + 1` (any order), with `i` read by the do-while condition; each `p_k` is mentioned
551
+ // nowhere else in the function beyond its init, its step, and derefs AT OFFSET 0 inside this
552
+ // body, and not by the condition;
553
+ // • S is a constant and `i` a declared integer: any S at 32 bits, only S = 0 when narrower and
554
+ // unsigned, never when narrower and signed. The index is `i - S + REST` in elements, which is
555
+ // plain `i` for the init KMC folds from `for (i = S; …) base[i]`: `p = (u32)&g + S*W` over an
556
+ // object, `p = a0 + S` over a W-element pointer.
557
+
558
+ /** One v3 walk pointer's re-spelling: its deref `*p` becomes `base[i - S + idx… + elems]`. */
559
+ interface ExprWalkSpelling {
560
+ readonly p: string;
561
+ readonly base: Expr;
562
+ readonly idx: readonly Expr[];
563
+ readonly elems: number;
564
+ }
565
+
566
+ /** The per-pointer half of v3: `init` (`p = EXPR`) decomposed into the index base and REST in
567
+ * elements, or null when `p`'s walk is out of scope. The loop-wide half is `tryExprWalkRun`. */
568
+ function exprWalkSpelling(
569
+ init: Stmt & { k: 'assign' },
524
570
  dw: Stmt & { k: 'dowhile' },
525
571
  ptrVars: Map<string, IrType>,
526
- fnBody: Stmt[],
527
572
  declTypes: VarTypes,
528
573
  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) {
574
+ privateNames: ReadonlySet<string>,
575
+ ): ExprWalkSpelling | null {
576
+ const p = init.name;
577
+ const pT = ptrVars.get(p);
578
+ if (pT?.kind !== 'ptr' || pT.to.kind !== 'int' || pT.to.width % 8 !== 0 || volatileLocals.has(p)) {
536
579
  return null;
537
580
  }
538
- if (!derefWidths([dw], p).every((w) => w === 1)) {
581
+ const W = pT.to.width / 8;
582
+ if (!derefWidths([dw], p).every((w) => w === W)) {
539
583
  return null;
540
584
  }
541
- // EXPR decomposition
542
- let expr: Expr = prev.value;
543
- while (expr.k === 'cast') {
585
+ // EXPR decomposition. Only casts that keep all 32 bits are stripped — a pointer cast, or an
586
+ // integer cast at 32 bits. A narrowing cast over EXPR (`(u16 *)(s16)((u32)&g + 2)`) truncates the
587
+ // address, which no index off the untruncated base reproduces, so it stays and the walk declines.
588
+ let expr: Expr = init.value;
589
+ while (expr.k === 'cast' && (expr.to.kind === 'ptr' || (expr.to.kind === 'int' && expr.to.width === 32))) {
544
590
  expr = expr.e;
545
591
  }
546
592
  const addends: Expr[] = [];
@@ -553,21 +599,40 @@ function tryExprWalk(
553
599
  }
554
600
  };
555
601
  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) {
602
+ // The base candidates: a bare var, or an object's address. Beside other addends the address
603
+ // must sit under a 32-bit integer cast — the cast is what makes EXPR's `+` byte arithmetic (a
604
+ // bare `&g + 2` strides g's own size). ALONE it needs no cast: there is no `+` to stride, and
605
+ // the zero-offset init structure prints is `p = (T *)&g`, whose casts the strip above removed.
606
+ const addrOf = (a: Expr): Extract<Expr, { k: 'addr' }> | null =>
607
+ a.k === 'addr' && addends.length === 1
608
+ ? a
609
+ : a.k === 'cast' && a.to.kind === 'int' && a.to.width === 32 && a.e.k === 'addr'
610
+ ? a.e
611
+ : null;
612
+ const bases = addends.filter((a) => a.k === 'var' || addrOf(a) !== null);
613
+ if (bases.length !== 1) {
567
614
  return null;
568
615
  }
569
- if (baseT.kind === 'ptr' && !(baseT.to.kind === 'int' && baseT.to.width === 8)) {
570
- return null;
616
+ const rest = addends.filter((a) => a !== bases[0]);
617
+ const base: Extract<Expr, { k: 'var' | 'addr' }> = bases[0].k === 'var' ? bases[0] : addrOf(bases[0])!;
618
+ // The UNIT EXPR's `+` counts in is the base's own stride (declTypes carries params, locals AND
619
+ // globals). KMC folds `for (i = S; …) a0[i]` over a `u16 *a0` to `p = a0 + S`, element
620
+ // arithmetic. A var declared nowhere has an unknowable stride (its project declaration decides).
621
+ // An address needs no declaration: `&g` is g's address whatever g's type, and the printer always
622
+ // casts it to the access width (cfamily.ts legalizedIndexBase: the type walk answers nothing for
623
+ // an `addr`, so even a g declared with W-byte elements prints the cast).
624
+ let unit = 1;
625
+ if (base.k === 'var') {
626
+ const baseT = declTypes(base.name);
627
+ if (baseT === undefined) {
628
+ return null;
629
+ }
630
+ if (baseT.kind === 'ptr') {
631
+ if (baseT.to.kind !== 'int' || (baseT.to.width !== 8 && baseT.to.width !== W * 8)) {
632
+ return null;
633
+ }
634
+ unit = baseT.to.width / 8;
635
+ }
571
636
  }
572
637
  const pure = (e: Expr): boolean =>
573
638
  e.k === 'var' || e.k === 'const'
@@ -577,7 +642,6 @@ function tryExprWalk(
577
642
  : e.k === 'cast'
578
643
  ? pure(e.e)
579
644
  : false;
580
- const assignedIn = (stmts: Stmt[], name: string): boolean => stmts.some((st) => stmtAssigns(st, name));
581
645
  const restVars = new Set<string>();
582
646
  const collectVars = (e: Expr): void => {
583
647
  if (e.k === 'var') {
@@ -589,69 +653,192 @@ function tryExprWalk(
589
653
  });
590
654
  };
591
655
  rest.forEach(collectVars);
592
- restVars.add(base.name);
593
- if (!rest.every(pure) || [...restVars].some((n) => assignedIn(dw.body, n))) {
656
+ if (base.k === 'var') {
657
+ restVars.add(base.name);
658
+ }
659
+ // INVARIANT across the loop, because the indexed spelling re-reads every name on every pass
660
+ // where the walk read it once, at the init: no `assign` in the body writes it, and — unless it
661
+ // is a param or a local whose address is never taken — no call and no memory store in the loop
662
+ // can either. A global (`p = (u16 *)(b + gK * 2)`, `g()` in the body) or an escaped local is
663
+ // written by a callee or through an aliasing pointer with no `assign` in sight.
664
+ // (a `postincr` is an assign in expression clothing; a `marker` is an instruction nobody modelled)
665
+ const loopExprs = [...walkExprs([dw])];
666
+ const loopWritesMemory = loopExprs.some((e) => e.k === 'call' || e.k === 'marker') || stmtsStore(dw.body);
667
+ if (
668
+ !rest.every(pure) ||
669
+ [...restVars].some(
670
+ (n) =>
671
+ volatileLocals.has(n) ||
672
+ dw.body.some((st) => stmtAssigns(st, n)) ||
673
+ loopExprs.some((e) => e.k === 'postincr' && e.name === n) ||
674
+ (loopWritesMemory && !privateNames.has(n)),
675
+ )
676
+ ) {
594
677
  return null;
595
678
  }
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)) {
679
+ // REST in ELEMENTS. Counted in the walk's own unit (a byte walk, or a W-element pointer base)
680
+ // it is already elements: the symbolic addends stay verbatim and the constants fold into one.
681
+ // Counted in bytes under a wider walk, the constants must total a multiple of W (rescaled to
682
+ // elements), and the symbolic addends would need dividing — so they stay in the BYTE sum, which
683
+ // becomes the index base: `(T *)(a0 + (a1 << 1))`, every non-constant addend in EXPR's order,
684
+ // the address keeping its `(u32)` (the printer casts the sum to the access width).
685
+ const consts = rest.filter((a): a is Extract<Expr, { k: 'const' }> => a.k === 'const');
686
+ const symbolic = rest.filter((a) => a.k !== 'const');
687
+ const restUnits = consts.reduce((acc, c) => acc + c.value, 0) | 0; // the 32-bit sum, signed
688
+ if (unit === W) {
689
+ return { p, base: { ...base }, idx: symbolic, elems: restUnits };
690
+ }
691
+ if (restUnits % W !== 0) {
607
692
  return null;
608
693
  }
609
- const iv = tailIncs.find((n) => n !== p);
610
- if (iv === undefined) {
694
+ const byteSum = addends
695
+ .filter((a) => a.k !== 'const')
696
+ .reduce<Expr | null>((acc, a) => (acc === null ? a : { k: 'bin', op: '+', l: acc, r: a }), null)!;
697
+ return { p, base: symbolic.length === 0 ? { ...base } : byteSum, idx: [], elems: restUnits / W };
698
+ }
699
+
700
+ /** v3 on one do-while: `out` is the statement list already walked ahead of it. Returns the
701
+ * re-spelled loop and the statements that replace `out`'s trailing init run (the counter's init
702
+ * kept, the walk pointers' deleted), or null. Tries the longest init run first. */
703
+ function tryExprWalk(
704
+ out: readonly Stmt[],
705
+ dw: Stmt & { k: 'dowhile' },
706
+ ptrVars: Map<string, IrType>,
707
+ fnBody: Stmt[],
708
+ declTypes: VarTypes,
709
+ volatileLocals: ReadonlySet<string>,
710
+ privateNames: ReadonlySet<string>,
711
+ ): { loop: Stmt; run: number; kept: Stmt[] } | null {
712
+ let nTail = 0;
713
+ while (nTail < dw.body.length && isUnitIncrement(dw.body[dw.body.length - 1 - nTail]) !== null) {
714
+ nTail++;
715
+ }
716
+ let nInit = 0;
717
+ while (nInit < out.length && out[out.length - 1 - nInit].k === 'assign') {
718
+ nInit++;
719
+ }
720
+ for (let n = Math.min(nTail, nInit); n >= 2; n--) {
721
+ const r = tryExprWalkRun(
722
+ out.slice(-n) as (Stmt & { k: 'assign' })[],
723
+ n,
724
+ dw,
725
+ ptrVars,
726
+ fnBody,
727
+ declTypes,
728
+ volatileLocals,
729
+ privateNames,
730
+ );
731
+ if (r) {
732
+ return r;
733
+ }
734
+ }
735
+ return null;
736
+ }
737
+
738
+ /** v3 with the last `n` statements of the body as the step tail and `inits` (the last `n` ahead
739
+ * of the loop) as their inits: one counter, n - 1 walk pointers. */
740
+ function tryExprWalkRun(
741
+ inits: (Stmt & { k: 'assign' })[],
742
+ n: number,
743
+ dw: Stmt & { k: 'dowhile' },
744
+ ptrVars: Map<string, IrType>,
745
+ fnBody: Stmt[],
746
+ declTypes: VarTypes,
747
+ volatileLocals: ReadonlySet<string>,
748
+ privateNames: ReadonlySet<string>,
749
+ ): { loop: Stmt; run: number; kept: Stmt[] } | null {
750
+ // BOTH kinds of step as the body's contiguous TAIL (any order): every deref then runs with `i`
751
+ // completed steps equal to each `p`'s, which is what `BASE[i + REST]` states. A step anywhere
752
+ // earlier (the `*++p` walk, a deref straddling the step) reads a different address. The tail
753
+ // and the init run name the same variables, each once.
754
+ const tail = dw.body.slice(-n);
755
+ const tailNames = tail.map((st) => isUnitIncrement(st)!);
756
+ const initNames = inits.map((st) => st.name);
757
+ if (
758
+ new Set(tailNames).size !== n ||
759
+ new Set(initNames).size !== n ||
760
+ !initNames.every((x) => tailNames.includes(x))
761
+ ) {
611
762
  return null;
612
763
  }
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') {
764
+ const counters = inits.filter((st) => !ptrVars.has(st.name));
765
+ if (counters.length !== 1) {
616
766
  return null;
617
767
  }
618
- if (!mentionsVar(dw.cond, iv) || mentionsVar(dw.cond, p)) {
768
+ const ivInit = counters[0];
769
+ const iv = ivInit.name;
770
+ if (!mentionsVar(dw.cond, iv)) {
619
771
  return null;
620
772
  }
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) {
773
+ // The counter starts at a CONSTANT S and its ONLY write anywhere in the body is the tail step:
774
+ // k completed steps put each `p` at EXPR + k*W and `i` at S + k, so the deref is
775
+ // BASE[i - S + REST]. That holds while `i` takes the values S + k, which a DECLARED integer
776
+ // counter does: at 32 bits always (it wraps with the address), and narrower only from S = 0 and
777
+ // unsigned, where it names elements 0 … 2^w - 1 before it wraps. A signed narrow counter wraps
778
+ // at 2^(w-1) (an s8 goes 127 -> -128 and names an element 256 behind the walk), and from a
779
+ // nonzero S `i - S` wraps sooner still; an undeclared one has no width to argue from.
780
+ if (ivInit.value.k !== 'const') {
624
781
  return null;
625
782
  }
626
- if (dw.body.slice(0, -2).some((st) => stmtAssigns(st, iv)) || tail.filter((st) => isIncOf(st, iv)).length !== 1) {
783
+ const start = ivInit.value.value | 0;
784
+ const ivT = declTypes(iv);
785
+ if (ivT?.kind !== 'int' || !(ivT.width === 32 || (start === 0 && !ivT.signed)) || volatileLocals.has(iv)) {
627
786
  return null;
628
787
  }
629
- if (volatileLocals.has(iv) || [...restVars].some((n) => volatileLocals.has(n))) {
788
+ if (dw.body.slice(0, -n).some((st) => stmtAssigns(st, iv))) {
630
789
  return null;
631
790
  }
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() };
791
+ // Each walk pointer re-spells on its OWN merits: one out of scope (its init, an off-0 deref, a
792
+ // mention outside the walk, a mention in the condition) keeps walking, init and step in place,
793
+ // beside the ones that re-spell — each `p` is its own address, so deleting one walk changes no
794
+ // other. Which pointers re-spell is therefore a property of each, not of the tail's step order.
795
+ // Dropping one can only remove rewrites, so the loop below shrinks the set to a fixpoint.
796
+ const spellings = new Map<string, ExprWalkSpelling>();
797
+ for (const st of inits) {
798
+ if (st === ivInit || mentionsVar(dw.cond, st.name)) {
799
+ continue;
800
+ }
801
+ const s = exprWalkSpelling(st, dw, ptrVars, declTypes, volatileLocals, privateNames);
802
+ if (s !== null) {
803
+ spellings.set(s.p, s);
643
804
  }
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
805
  }
651
- if (countMentions([dw2], p) !== 0) {
652
- return null;
806
+ const idxOf = (s: ExprWalkSpelling): Expr =>
807
+ plusConst(
808
+ s.idx.reduce<Expr>((acc, r) => ({ k: 'bin', op: '+', l: acc, r }), { k: 'var', name: iv }),
809
+ (s.elems - start) | 0,
810
+ );
811
+ while (spellings.size > 0) {
812
+ // rewrite the derefs, then verify nothing of any re-spelled `p` survives
813
+ const offsetDerefs = new Set<string>();
814
+ const rewriteExpr = (e: Expr): Expr => {
815
+ const m = mapExprChildren(e, rewriteExpr);
816
+ const s = m.k === 'index' && m.base.k === 'var' ? spellings.get(m.base.name) : undefined;
817
+ if (m.k === 'index' && s !== undefined) {
818
+ if (m.idx.k !== 'const' || m.idx.value !== 0) {
819
+ offsetDerefs.add(s.p);
820
+ return m;
821
+ }
822
+ return { ...m, base: { ...s.base }, idx: idxOf(s) };
823
+ }
824
+ return m;
825
+ };
826
+ const steps = new Set(tail.filter((st) => spellings.has(isUnitIncrement(st)!)));
827
+ const body = dw.body.filter((st) => !steps.has(st)).map((st) => mapStmtExprs(st, rewriteExpr));
828
+ const dw2: Stmt = { ...dw, body };
829
+ const failed = inits.filter(
830
+ (st) =>
831
+ spellings.has(st.name) &&
832
+ (offsetDerefs.has(st.name) ||
833
+ !confinedToWalk(fnBody, st.name, countMentions([st], st.name), dw) ||
834
+ countMentions([dw2], st.name) !== 0),
835
+ );
836
+ if (failed.length === 0) {
837
+ return { loop: dw2, run: n, kept: inits.filter((st) => !spellings.has(st.name)) };
838
+ }
839
+ failed.forEach((st) => spellings.delete(st.name));
653
840
  }
654
- return dw2;
841
+ return null;
655
842
  }
656
843
 
657
844
  /** Try the walk→index re-spelling on one function. Returns the transformed COPY when at least
@@ -676,6 +863,10 @@ export function reindexWalks(
676
863
  sfn.locals.filter((l) => l.volatile === true || l.pointeeVolatile === true).map((l) => l.name),
677
864
  );
678
865
  const storage = nameStorage(sfn);
866
+ // v3's invariant-addend test: the names no callee and no aliasing store can write — a param or a
867
+ // local whose address the function never takes
868
+ const addressTaken = new Set([...walkExprs(sfn.body)].flatMap((e) => (e.k === 'addr' ? [e.name] : [])));
869
+ const privateNames = new Set([...storage].filter(([n, c]) => c !== 'global' && !addressTaken.has(n)).map(([n]) => n));
679
870
  for (const v of [...sfn.params, ...sfn.locals]) {
680
871
  if (v.type.kind === 'ptr') {
681
872
  ptrVars.set(v.name, v.type);
@@ -778,15 +969,14 @@ export function reindexWalks(
778
969
  }
779
970
  }
780
971
  }
781
- // v3 — the up-counting byte walk with an expression base: `p = (u8 *)(EXPR)` immediately
972
+ // v3 — the up-counting walk with an expression base: `p = (T *)(EXPR)` immediately
782
973
  // ahead of a counter-carried do-while (see tryExprWalk)
783
974
  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));
975
+ const v3 = tryExprWalk(out, s, ptrVars, sfn.body, declTypes, volatileLocals, privateNames);
976
+ if (v3) {
977
+ // the walk inits are subsumed by the indexed spelling; the counter's init stays
978
+ out.splice(out.length - v3.run, v3.run, ...v3.kept);
979
+ out.push(recurse(v3.loop));
790
980
  fired++;
791
981
  continue;
792
982
  }