@asmlift/core 0.9.0 → 0.11.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 (117) hide show
  1. package/README.md +25 -24
  2. package/package.json +1 -1
  3. package/src/aggregate.ts +149 -0
  4. package/src/backend/c.ts +2 -0
  5. package/src/backend/cfamily.ts +197 -61
  6. package/src/backend/cpp.ts +3 -1
  7. package/src/backend/lines.ts +10 -0
  8. package/src/backend/pascal.ts +47 -16
  9. package/src/cdecl/parse.ts +21 -0
  10. package/src/contracts.ts +246 -36
  11. package/src/declare.ts +69 -20
  12. package/src/frontend/arm-body.ts +155 -0
  13. package/src/frontend/asmdata.ts +9 -2
  14. package/src/frontend/call-plan.ts +1117 -61
  15. package/src/frontend/callee-body.ts +370 -0
  16. package/src/frontend/dead-reads.ts +628 -0
  17. package/src/frontend/device-pins.ts +33 -4
  18. package/src/frontend/emit.ts +4 -3
  19. package/src/frontend/fpu.ts +224 -17
  20. package/src/frontend/frame-objects.ts +1689 -206
  21. package/src/frontend/frontend.ts +84 -3
  22. package/src/frontend/high-half.ts +53 -4
  23. package/src/frontend/jump-table.ts +545 -0
  24. package/src/frontend/local-object.ts +158 -32
  25. package/src/frontend/mips.ts +2296 -217
  26. package/src/frontend/own-struct-return.ts +500 -0
  27. package/src/frontend/ppc.ts +2748 -245
  28. package/src/frontend/register-struct-call.ts +399 -0
  29. package/src/frontend/reloc-symbol.ts +22 -7
  30. package/src/frontend/ssa.ts +1721 -43
  31. package/src/frontend/stackargs.ts +199 -72
  32. package/src/frontend/thumb-entry.ts +340 -0
  33. package/src/frontend/thumb.ts +2400 -325
  34. package/src/ir/alias.ts +65 -2
  35. package/src/ir/bits.ts +1699 -4
  36. package/src/ir/core.ts +254 -9
  37. package/src/ir/discipline.ts +86 -21
  38. package/src/ir/entry.ts +157 -0
  39. package/src/{structure → ir}/loops.ts +6 -2
  40. package/src/ir/opcodes.ts +137 -9
  41. package/src/ir/parse.ts +1 -0
  42. package/src/ir/simplify.ts +61 -10
  43. package/src/ir/types.ts +86 -2
  44. package/src/ir/verify.ts +96 -11
  45. package/src/l3/argbase.ts +2 -0
  46. package/src/l3/ast.ts +582 -18
  47. package/src/l3/basecse.ts +68 -15
  48. package/src/l3/caseendgoto.ts +223 -0
  49. package/src/l3/coalesce.ts +42 -14
  50. package/src/l3/constinit.ts +410 -0
  51. package/src/l3/dce.ts +53 -9
  52. package/src/l3/initfirst.ts +42 -11
  53. package/src/l3/intcell.ts +2 -1
  54. package/src/l3/narrowdecl.ts +77 -36
  55. package/src/l3/pollguard.ts +8 -2
  56. package/src/l3/ptrcell.ts +33 -34
  57. package/src/l3/ptrfield.ts +123 -8
  58. package/src/l3/regspell.ts +11 -4
  59. package/src/l3/reindex.ts +295 -99
  60. package/src/l3/scopebase.ts +2 -0
  61. package/src/l3/storage.ts +98 -1
  62. package/src/l3/symbol-refs.ts +84 -6
  63. package/src/l3/tailmerge.ts +6 -2
  64. package/src/l3/typing.ts +108 -15
  65. package/src/l3/unreduce.ts +6 -5
  66. package/src/l3/volatileptr.ts +17 -54
  67. package/src/mangle.ts +30 -0
  68. package/src/pattern/engine.ts +361 -73
  69. package/src/pipeline.ts +1039 -38
  70. package/src/proto-context.ts +1039 -45
  71. package/src/proto.ts +555 -40
  72. package/src/raise/aggmembers.ts +290 -0
  73. package/src/raise/arrays.ts +5 -1
  74. package/src/raise/const.ts +25 -0
  75. package/src/raise/declared-volatile.ts +207 -31
  76. package/src/raise/divpow2.ts +192 -0
  77. package/src/raise/floathelpers.ts +254 -33
  78. package/src/raise/gvn.ts +3 -3
  79. package/src/raise/latch.ts +162 -6
  80. package/src/raise/memberarrays.ts +10 -4
  81. package/src/raise/offsetnames.ts +422 -36
  82. package/src/raise/pairparams.ts +33 -0
  83. package/src/raise/paramwidth.ts +196 -6
  84. package/src/raise/pre-recovery.ts +107 -21
  85. package/src/raise/pre-structure.ts +73 -0
  86. package/src/raise/recover.ts +12 -5
  87. package/src/raise/reroll.ts +674 -0
  88. package/src/raise/shortcircuit.ts +217 -34
  89. package/src/raise/softdiv.ts +1 -1
  90. package/src/raise/storesign.ts +153 -0
  91. package/src/raise/struct-arrays.ts +30 -11
  92. package/src/raise/structs.ts +396 -33
  93. package/src/raise/truncload.ts +4 -3
  94. package/src/raise/unpre.ts +594 -0
  95. package/src/raise/unthread.ts +400 -0
  96. package/src/rank-declare.ts +118 -12
  97. package/src/rank-variations.ts +186 -35
  98. package/src/rank.ts +536 -68
  99. package/src/runtime-helpers.ts +140 -22
  100. package/src/structure/analysis.ts +217 -8
  101. package/src/structure/arith-ops.ts +23 -0
  102. package/src/structure/cross-jumped-tail.ts +216 -0
  103. package/src/structure/globalaccess.ts +157 -10
  104. package/src/structure/hazards.ts +16 -64
  105. package/src/structure/local-statics.ts +145 -9
  106. package/src/structure/namecoalesce.ts +1 -1
  107. package/src/structure/pointer-spelling.ts +1106 -0
  108. package/src/structure/redundant-test.ts +1 -1
  109. package/src/structure/structure.ts +6192 -1213
  110. package/src/structure/switch-recover.ts +583 -120
  111. package/src/structure/unfolded-branch-constant.ts +90 -0
  112. package/src/symbols.ts +58 -15
  113. package/src/target.ts +1028 -54
  114. package/src/trace.ts +49 -17
  115. package/src/variation-definitions.ts +333 -20
  116. package/src/variation-gates.ts +4 -0
  117. package/src/variation-tokens.ts +68 -3
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
@@ -93,12 +93,15 @@ import {
93
93
  Expr,
94
94
  SFn,
95
95
  Stmt,
96
+ SwitchCase,
97
+ caseEndGotoLeaves,
96
98
  mapExprChildren,
97
99
  mapStmtExprs,
98
100
  mentionedName,
99
101
  rematerializableAddress,
100
102
  stmtChildren,
101
103
  stmtExprs,
104
+ walkExprs,
102
105
  } from './ast';
103
106
  import { type Gate, firstRejection } from './gates';
104
107
  import { takenNames } from './hoist';
@@ -301,17 +304,24 @@ export const COUNTDOWN_GATES: readonly Gate<CountdownCtx>[] = [
301
304
  ];
302
305
 
303
306
  /** A `break`/`continue` that would target the ENCLOSING loop: found at if/switch depth, but not
304
- * inside a nested loop (whose own exits target itself). */
307
+ * inside a nested loop (whose own exits target itself). A switch's own `break` (`ofSwitch`)
308
+ * leaves only its switch, and the loop's tail still runs after it, so it is not one. A case-end
309
+ * `goto` whose label lies outside `stmts` leaves every loop it sits in, at any depth. */
305
310
  function hasLoopExit(stmts: Stmt[]): boolean {
311
+ return caseEndGotoLeaves(stmts) || hasShallowLoopExit(stmts);
312
+ }
313
+
314
+ function hasShallowLoopExit(stmts: Stmt[]): boolean {
306
315
  return stmts.some((s) => {
307
316
  switch (s.k) {
308
317
  case 'break':
318
+ return s.ofSwitch !== true;
309
319
  case 'continue':
310
320
  return true;
311
321
  case 'if':
312
- return hasLoopExit(s.then) || hasLoopExit(s.else);
322
+ return hasShallowLoopExit(s.then) || hasShallowLoopExit(s.else);
313
323
  case 'switch':
314
- return s.cases.some((c) => hasLoopExit(c.body)) || hasLoopExit(s.default ?? []);
324
+ return s.cases.some((c) => hasShallowLoopExit(c.body)) || hasShallowLoopExit(s.default ?? []);
315
325
  default:
316
326
  return false;
317
327
  }
@@ -424,6 +434,11 @@ function reindexCond(cond: Expr, walk: WalkLoop, iv: string): Expr | null {
424
434
  return null;
425
435
  }
426
436
  const i: Expr = { k: 'var', name: iv };
437
+ // A fresh node, so the compare's `unsignedCmp` evidence (l3/ast.ts) is dropped, and that is
438
+ // PROVED rather than lost: `p < base + n` compared addresses, which C compares unsigned, and the
439
+ // walk makes `p == base + i` (scaled), so `i < n` compares the same values. Kept, the evidence
440
+ // would have rank.ts withhold the index spelling over an `s32` counter that the pointer
441
+ // spelling, judged on the same rule, never owed.
427
442
  return swap ? { k: 'bin', op: cond.op, l: n, r: i } : { k: 'bin', op: cond.op, l: i, r: n };
428
443
  }
429
444
 
@@ -494,56 +509,93 @@ function reindexStmt(s: Stmt, walk: WalkLoop, iv: string): Stmt | null {
494
509
  return {
495
510
  ...s,
496
511
  scrutinee,
497
- cases: cases as { values: number[]; body: Stmt[]; fallsThrough: boolean }[],
512
+ cases: cases as SwitchCase[],
498
513
  default: dflt ?? undefined,
499
514
  };
500
515
  }
501
516
  case 'break':
502
517
  case 'continue':
518
+ case 'goto':
519
+ case 'caseEndGoto':
503
520
  return s;
504
521
  }
505
522
  }
506
523
 
507
- // ── v3: the up-counting BYTE walk with an EXPRESSION base ────────────────────────────────────
524
+ /** Whether any statement here, at any depth, writes memory (`store`). */
525
+ function stmtsStore(stmts: Stmt[]): boolean {
526
+ return stmts.some((s) => s.k === 'store' || stmtsStore(stmtChildren(s)));
527
+ }
528
+
529
+ /** `e + c`, spelled `e - |c|` for a negative c and `e` alone for 0. */
530
+ function plusConst(e: Expr, c: number): Expr {
531
+ return c === 0 ? e : { k: 'bin', op: c < 0 ? '-' : '+', l: e, r: { k: 'const', value: Math.abs(c) } };
532
+ }
533
+
534
+ // ── v3: the up-counting walk with an EXPRESSION base ─────────────────────────────────────────
508
535
  //
509
- // p = (u8 *)(EXPR); do { … *p … ; p = p + 1; i = i + 1; } while (i < n);
536
+ // i = S; p = (T *)(EXPR); do { … *p … ; p = p + 1; i = i + 1; } while (i < n);
510
537
  //
511
538
  // The loop already carries its own integer counter, so the walk pointer is pure strength
512
- // reduction: deleting its init and step and spelling the deref as BASE[i + REST] — the sole
513
- // bare-var addend of EXPR as the index base, the counter FIRST in the index sum — is the
514
- // indexed source the compiler reduces back. v3 SCOPE (decline over approximate):
515
- // • `p` is a `u8 *` (byte walk: the index and the byte arithmetic agree by construction —
516
- // a wider element would need the REST addends rescaled from bytes to elements);
517
- // • the init `p = EXPR` immediately precedes the `dowhile`; EXPR (casts stripped) flattens
518
- // over `+` into exactly ONE bare-var addend (the base — itself not a non-byte pointer, or
519
- // `base[i]` would stride its element) plus pure invariant addends (var/const/mul/shl trees
520
- // whose vars the body never assigns);
521
- // • the body's statements include exactly one `p = p + 1` and one `i = i + 1` with integer
522
- // `i` read by the do-while condition; `p` is mentioned nowhere else in the function beyond
523
- // its init, its step, and derefs AT OFFSET 0 inside this body, and not by the condition.
524
- function tryExprWalk(
525
- prev2: Stmt | undefined,
526
- prev: Stmt | undefined,
539
+ // reduction: deleting its init and step and spelling the deref as BASE[i - S + REST], REST in
540
+ // elements — the sole base addend of EXPR as the index base, the counter FIRST in the index sum —
541
+ // is the indexed source the compiler reduces back. One counter may carry SEVERAL walk pointers
542
+ // (KMC reduces `for (i = 1; …) { gH[i] = 7; gW[i] = 8; }` to one pointer per array): each keeps
543
+ // its own base and unit, and each re-spells on its own merits — one out of scope keeps walking,
544
+ // whatever the order of the steps. v3 SCOPE (decline over approximate):
545
+ // • each `p` is a `T *` over an integer T of W bytes, and every deref of `p` reads W. EXPR's sum
546
+ // counts in its BASE's unit. Over a W-element pointer base that is already elements, and REST
547
+ // stays verbatim, symbolic addends included. Over a byte base (a non-pointer var, a byte
548
+ // pointer, `(u32)&g`) a wider walk needs a constant total W divides (it is rescaled to
549
+ // elements), and a symbolic addend, which would need dividing, stays in the BYTE sum instead:
550
+ // the index base is `(T *)(BASE + SYM)`, the spelling `p = b + k; … p[i]` reduces back to;
551
+ // • the inits `i = S` and `p_k = EXPR_k` are the run of assignments immediately ahead of the
552
+ // `dowhile` (any order); EXPR (its pointer and 32-bit integer casts stripped; a narrowing cast
553
+ // declines) flattens over `+` into exactly ONE base addend — a bare var declared a non-pointer,
554
+ // a byte pointer or a pointer to W-byte integers (any other pointer strides a unit no element
555
+ // index names), or an object's address, `(u32)&g` beside other addends or `&g` alone — plus
556
+ // pure invariant addends (var/const/mul/shl trees whose vars the body never assigns, and,
557
+ // beside a call or a store in the loop, only params and locals whose address never escapes);
558
+ // • the body's contiguous TAIL is exactly one `p_k = p_k + 1` per walk pointer and one
559
+ // `i = i + 1` (any order), with `i` read by the do-while condition; each `p_k` is mentioned
560
+ // nowhere else in the function beyond its init, its step, and derefs AT OFFSET 0 inside this
561
+ // body, and not by the condition;
562
+ // • S is a constant and `i` a declared integer: any S at 32 bits, only S = 0 when narrower and
563
+ // unsigned, never when narrower and signed. The index is `i - S + REST` in elements, which is
564
+ // plain `i` for the init KMC folds from `for (i = S; …) base[i]`: `p = (u32)&g + S*W` over an
565
+ // object, `p = a0 + S` over a W-element pointer.
566
+
567
+ /** One v3 walk pointer's re-spelling: its deref `*p` becomes `base[i - S + idx… + elems]`. */
568
+ interface ExprWalkSpelling {
569
+ readonly p: string;
570
+ readonly base: Expr;
571
+ readonly idx: readonly Expr[];
572
+ readonly elems: number;
573
+ }
574
+
575
+ /** The per-pointer half of v3: `init` (`p = EXPR`) decomposed into the index base and REST in
576
+ * elements, or null when `p`'s walk is out of scope. The loop-wide half is `tryExprWalkRun`. */
577
+ function exprWalkSpelling(
578
+ init: Stmt & { k: 'assign' },
527
579
  dw: Stmt & { k: 'dowhile' },
528
580
  ptrVars: Map<string, IrType>,
529
- fnBody: Stmt[],
530
581
  declTypes: VarTypes,
531
582
  volatileLocals: ReadonlySet<string>,
532
- ): Stmt | null {
533
- if (prev?.k !== 'assign' || !ptrVars.has(prev.name) || volatileLocals.has(prev.name)) {
583
+ privateNames: ReadonlySet<string>,
584
+ ): ExprWalkSpelling | null {
585
+ const p = init.name;
586
+ const pT = ptrVars.get(p);
587
+ if (pT?.kind !== 'ptr' || pT.to.kind !== 'int' || pT.to.width % 8 !== 0 || volatileLocals.has(p)) {
534
588
  return null;
535
589
  }
536
- const p = prev.name;
537
- const pT = ptrVars.get(p)!;
538
- if (pT.kind !== 'ptr' || pT.to.kind !== 'int' || pT.to.width !== 8) {
590
+ const W = pT.to.width / 8;
591
+ if (!derefWidths([dw], p).every((w) => w === W)) {
539
592
  return null;
540
593
  }
541
- if (!derefWidths([dw], p).every((w) => w === 1)) {
542
- return null;
543
- }
544
- // EXPR decomposition
545
- let expr: Expr = prev.value;
546
- while (expr.k === 'cast') {
594
+ // EXPR decomposition. Only casts that keep all 32 bits are stripped — a pointer cast, or an
595
+ // integer cast at 32 bits. A narrowing cast over EXPR (`(u16 *)(s16)((u32)&g + 2)`) truncates the
596
+ // address, which no index off the untruncated base reproduces, so it stays and the walk declines.
597
+ let expr: Expr = init.value;
598
+ while (expr.k === 'cast' && (expr.to.kind === 'ptr' || (expr.to.kind === 'int' && expr.to.width === 32))) {
547
599
  expr = expr.e;
548
600
  }
549
601
  const addends: Expr[] = [];
@@ -556,21 +608,40 @@ function tryExprWalk(
556
608
  }
557
609
  };
558
610
  flatten(expr);
559
- const bare = addends.filter((a): a is Extract<Expr, { k: 'var' }> => a.k === 'var');
560
- const rest = addends.filter((a) => a.k !== 'var');
561
- if (bare.length !== 1) {
562
- return null;
563
- }
564
- const base = bare[0] as Extract<Expr, { k: 'var' }>;
565
- // declTypes carries params, locals AND globals: a base declared nowhere has an unknowable C
566
- // stride (its project declaration decides), and a wider pointer strides its element under [i]
567
- // — both decline.
568
- const baseT = declTypes(base.name);
569
- if (baseT === undefined) {
611
+ // The base candidates: a bare var, or an object's address. Beside other addends the address
612
+ // must sit under a 32-bit integer cast — the cast is what makes EXPR's `+` byte arithmetic (a
613
+ // bare `&g + 2` strides g's own size). ALONE it needs no cast: there is no `+` to stride, and
614
+ // the zero-offset init structure prints is `p = (T *)&g`, whose casts the strip above removed.
615
+ const addrOf = (a: Expr): Extract<Expr, { k: 'addr' }> | null =>
616
+ a.k === 'addr' && addends.length === 1
617
+ ? a
618
+ : a.k === 'cast' && a.to.kind === 'int' && a.to.width === 32 && a.e.k === 'addr'
619
+ ? a.e
620
+ : null;
621
+ const bases = addends.filter((a) => a.k === 'var' || addrOf(a) !== null);
622
+ if (bases.length !== 1) {
570
623
  return null;
571
624
  }
572
- if (baseT.kind === 'ptr' && !(baseT.to.kind === 'int' && baseT.to.width === 8)) {
573
- return null;
625
+ const rest = addends.filter((a) => a !== bases[0]);
626
+ const base: Extract<Expr, { k: 'var' | 'addr' }> = bases[0].k === 'var' ? bases[0] : addrOf(bases[0])!;
627
+ // The UNIT EXPR's `+` counts in is the base's own stride (declTypes carries params, locals AND
628
+ // globals). KMC folds `for (i = S; …) a0[i]` over a `u16 *a0` to `p = a0 + S`, element
629
+ // arithmetic. A var declared nowhere has an unknowable stride (its project declaration decides).
630
+ // An address needs no declaration: `&g` is g's address whatever g's type, and the printer always
631
+ // casts it to the access width (cfamily.ts legalizedIndexBase: the type walk answers nothing for
632
+ // an `addr`, so even a g declared with W-byte elements prints the cast).
633
+ let unit = 1;
634
+ if (base.k === 'var') {
635
+ const baseT = declTypes(base.name);
636
+ if (baseT === undefined) {
637
+ return null;
638
+ }
639
+ if (baseT.kind === 'ptr') {
640
+ if (baseT.to.kind !== 'int' || (baseT.to.width !== 8 && baseT.to.width !== W * 8)) {
641
+ return null;
642
+ }
643
+ unit = baseT.to.width / 8;
644
+ }
574
645
  }
575
646
  const pure = (e: Expr): boolean =>
576
647
  e.k === 'var' || e.k === 'const'
@@ -580,7 +651,6 @@ function tryExprWalk(
580
651
  : e.k === 'cast'
581
652
  ? pure(e.e)
582
653
  : false;
583
- const assignedIn = (stmts: Stmt[], name: string): boolean => stmts.some((st) => stmtAssigns(st, name));
584
654
  const restVars = new Set<string>();
585
655
  const collectVars = (e: Expr): void => {
586
656
  if (e.k === 'var') {
@@ -592,69 +662,192 @@ function tryExprWalk(
592
662
  });
593
663
  };
594
664
  rest.forEach(collectVars);
595
- restVars.add(base.name);
596
- if (!rest.every(pure) || [...restVars].some((n) => assignedIn(dw.body, n))) {
665
+ if (base.k === 'var') {
666
+ restVars.add(base.name);
667
+ }
668
+ // INVARIANT across the loop, because the indexed spelling re-reads every name on every pass
669
+ // where the walk read it once, at the init: no `assign` in the body writes it, and — unless it
670
+ // is a param or a local whose address is never taken — no call and no memory store in the loop
671
+ // can either. A global (`p = (u16 *)(b + gK * 2)`, `g()` in the body) or an escaped local is
672
+ // written by a callee or through an aliasing pointer with no `assign` in sight.
673
+ // (a `postincr` is an assign in expression clothing; a `marker` is an instruction nobody modelled)
674
+ const loopExprs = [...walkExprs([dw])];
675
+ const loopWritesMemory = loopExprs.some((e) => e.k === 'call' || e.k === 'marker') || stmtsStore(dw.body);
676
+ if (
677
+ !rest.every(pure) ||
678
+ [...restVars].some(
679
+ (n) =>
680
+ volatileLocals.has(n) ||
681
+ dw.body.some((st) => stmtAssigns(st, n)) ||
682
+ loopExprs.some((e) => e.k === 'postincr' && e.name === n) ||
683
+ (loopWritesMemory && !privateNames.has(n)),
684
+ )
685
+ ) {
597
686
  return null;
598
687
  }
599
- // the steps and the counter
600
- const isIncOf = (st: Stmt, name?: string): string | null => {
601
- const n = isUnitIncrement(st);
602
- return n !== null && (name === undefined || n === name) ? n : null;
603
- };
604
- // BOTH steps as the body's contiguous TAIL (either order): every deref then runs with `i`
605
- // completed steps equal to `p`'s, which is what `BASE[i + REST]` states. A step anywhere
606
- // earlier (the `*++p` walk, a deref straddling the step) reads a different address.
607
- const tail = dw.body.slice(-2);
608
- const tailIncs = tail.map((st) => isIncOf(st)).filter((n): n is string => n !== null);
609
- if (tailIncs.length !== 2 || !tailIncs.includes(p)) {
688
+ // REST in ELEMENTS. Counted in the walk's own unit (a byte walk, or a W-element pointer base)
689
+ // it is already elements: the symbolic addends stay verbatim and the constants fold into one.
690
+ // Counted in bytes under a wider walk, the constants must total a multiple of W (rescaled to
691
+ // elements), and the symbolic addends would need dividing — so they stay in the BYTE sum, which
692
+ // becomes the index base: `(T *)(a0 + (a1 << 1))`, every non-constant addend in EXPR's order,
693
+ // the address keeping its `(u32)` (the printer casts the sum to the access width).
694
+ const consts = rest.filter((a): a is Extract<Expr, { k: 'const' }> => a.k === 'const');
695
+ const symbolic = rest.filter((a) => a.k !== 'const');
696
+ const restUnits = consts.reduce((acc, c) => acc + c.value, 0) | 0; // the 32-bit sum, signed
697
+ if (unit === W) {
698
+ return { p, base: { ...base }, idx: symbolic, elems: restUnits };
699
+ }
700
+ if (restUnits % W !== 0) {
610
701
  return null;
611
702
  }
612
- const iv = tailIncs.find((n) => n !== p);
613
- if (iv === undefined) {
703
+ const byteSum = addends
704
+ .filter((a) => a.k !== 'const')
705
+ .reduce<Expr | null>((acc, a) => (acc === null ? a : { k: 'bin', op: '+', l: acc, r: a }), null)!;
706
+ return { p, base: symbolic.length === 0 ? { ...base } : byteSum, idx: [], elems: restUnits / W };
707
+ }
708
+
709
+ /** v3 on one do-while: `out` is the statement list already walked ahead of it. Returns the
710
+ * re-spelled loop and the statements that replace `out`'s trailing init run (the counter's init
711
+ * kept, the walk pointers' deleted), or null. Tries the longest init run first. */
712
+ function tryExprWalk(
713
+ out: readonly Stmt[],
714
+ dw: Stmt & { k: 'dowhile' },
715
+ ptrVars: Map<string, IrType>,
716
+ fnBody: Stmt[],
717
+ declTypes: VarTypes,
718
+ volatileLocals: ReadonlySet<string>,
719
+ privateNames: ReadonlySet<string>,
720
+ ): { loop: Stmt; run: number; kept: Stmt[] } | null {
721
+ let nTail = 0;
722
+ while (nTail < dw.body.length && isUnitIncrement(dw.body[dw.body.length - 1 - nTail]) !== null) {
723
+ nTail++;
724
+ }
725
+ let nInit = 0;
726
+ while (nInit < out.length && out[out.length - 1 - nInit].k === 'assign') {
727
+ nInit++;
728
+ }
729
+ for (let n = Math.min(nTail, nInit); n >= 2; n--) {
730
+ const r = tryExprWalkRun(
731
+ out.slice(-n) as (Stmt & { k: 'assign' })[],
732
+ n,
733
+ dw,
734
+ ptrVars,
735
+ fnBody,
736
+ declTypes,
737
+ volatileLocals,
738
+ privateNames,
739
+ );
740
+ if (r) {
741
+ return r;
742
+ }
743
+ }
744
+ return null;
745
+ }
746
+
747
+ /** v3 with the last `n` statements of the body as the step tail and `inits` (the last `n` ahead
748
+ * of the loop) as their inits: one counter, n - 1 walk pointers. */
749
+ function tryExprWalkRun(
750
+ inits: (Stmt & { k: 'assign' })[],
751
+ n: number,
752
+ dw: Stmt & { k: 'dowhile' },
753
+ ptrVars: Map<string, IrType>,
754
+ fnBody: Stmt[],
755
+ declTypes: VarTypes,
756
+ volatileLocals: ReadonlySet<string>,
757
+ privateNames: ReadonlySet<string>,
758
+ ): { loop: Stmt; run: number; kept: Stmt[] } | null {
759
+ // BOTH kinds of step as the body's contiguous TAIL (any order): every deref then runs with `i`
760
+ // completed steps equal to each `p`'s, which is what `BASE[i + REST]` states. A step anywhere
761
+ // earlier (the `*++p` walk, a deref straddling the step) reads a different address. The tail
762
+ // and the init run name the same variables, each once.
763
+ const tail = dw.body.slice(-n);
764
+ const tailNames = tail.map((st) => isUnitIncrement(st)!);
765
+ const initNames = inits.map((st) => st.name);
766
+ if (
767
+ new Set(tailNames).size !== n ||
768
+ new Set(initNames).size !== n ||
769
+ !initNames.every((x) => tailNames.includes(x))
770
+ ) {
614
771
  return null;
615
772
  }
616
- const pStepIdx = dw.body.length - 2 + tail.findIndex((st) => isIncOf(st, p) !== null);
617
- const ivT = declTypes(iv);
618
- if (ivT !== undefined && ivT.kind !== 'int') {
773
+ const counters = inits.filter((st) => !ptrVars.has(st.name));
774
+ if (counters.length !== 1) {
619
775
  return null;
620
776
  }
621
- if (!mentionsVar(dw.cond, iv) || mentionsVar(dw.cond, p)) {
777
+ const ivInit = counters[0];
778
+ const iv = ivInit.name;
779
+ if (!mentionsVar(dw.cond, iv)) {
622
780
  return null;
623
781
  }
624
- // The counter STARTS AT ZERO (the adjacent `i = 0` init — `BASE[i + REST]` counts completed
625
- // steps from the walk's own start) and its ONLY write anywhere in the body is the tail step.
626
- if (prev2?.k !== 'assign' || prev2.name !== iv || prev2.value.k !== 'const' || prev2.value.value !== 0) {
782
+ // The counter starts at a CONSTANT S and its ONLY write anywhere in the body is the tail step:
783
+ // k completed steps put each `p` at EXPR + k*W and `i` at S + k, so the deref is
784
+ // BASE[i - S + REST]. That holds while `i` takes the values S + k, which a DECLARED integer
785
+ // counter does: at 32 bits always (it wraps with the address), and narrower only from S = 0 and
786
+ // unsigned, where it names elements 0 … 2^w - 1 before it wraps. A signed narrow counter wraps
787
+ // at 2^(w-1) (an s8 goes 127 -> -128 and names an element 256 behind the walk), and from a
788
+ // nonzero S `i - S` wraps sooner still; an undeclared one has no width to argue from.
789
+ if (ivInit.value.k !== 'const') {
627
790
  return null;
628
791
  }
629
- if (dw.body.slice(0, -2).some((st) => stmtAssigns(st, iv)) || tail.filter((st) => isIncOf(st, iv)).length !== 1) {
792
+ const start = ivInit.value.value | 0;
793
+ const ivT = declTypes(iv);
794
+ if (ivT?.kind !== 'int' || !(ivT.width === 32 || (start === 0 && !ivT.signed)) || volatileLocals.has(iv)) {
630
795
  return null;
631
796
  }
632
- if (volatileLocals.has(iv) || [...restVars].some((n) => volatileLocals.has(n))) {
797
+ if (dw.body.slice(0, -n).some((st) => stmtAssigns(st, iv))) {
633
798
  return null;
634
799
  }
635
- // rewrite the derefs, then verify nothing of `p` survives
636
- const idxOf = (): Expr => rest.reduce<Expr>((acc, r) => ({ k: 'bin', op: '+', l: acc, r }), { k: 'var', name: iv });
637
- let respelled = true;
638
- const rewriteExpr = (e: Expr): Expr => {
639
- const m = mapExprChildren(e, rewriteExpr);
640
- if (m.k === 'index' && m.base.k === 'var' && m.base.name === p) {
641
- if (m.idx.k !== 'const' || m.idx.value !== 0) {
642
- respelled = false;
643
- return m;
644
- }
645
- return { ...m, base: { k: 'var', name: base.name }, idx: idxOf() };
800
+ // Each walk pointer re-spells on its OWN merits: one out of scope (its init, an off-0 deref, a
801
+ // mention outside the walk, a mention in the condition) keeps walking, init and step in place,
802
+ // beside the ones that re-spell — each `p` is its own address, so deleting one walk changes no
803
+ // other. Which pointers re-spell is therefore a property of each, not of the tail's step order.
804
+ // Dropping one can only remove rewrites, so the loop below shrinks the set to a fixpoint.
805
+ const spellings = new Map<string, ExprWalkSpelling>();
806
+ for (const st of inits) {
807
+ if (st === ivInit || mentionsVar(dw.cond, st.name)) {
808
+ continue;
809
+ }
810
+ const s = exprWalkSpelling(st, dw, ptrVars, declTypes, volatileLocals, privateNames);
811
+ if (s !== null) {
812
+ spellings.set(s.p, s);
646
813
  }
647
- return m;
648
- };
649
- const body = dw.body.filter((_, k) => k !== pStepIdx).map((st) => mapStmtExprs(st, rewriteExpr));
650
- const dw2: Stmt = { ...dw, body };
651
- if (!respelled || !confinedToWalk(fnBody, p, countMentions([prev], p), dw)) {
652
- return null;
653
814
  }
654
- if (countMentions([dw2], p) !== 0) {
655
- return null;
815
+ const idxOf = (s: ExprWalkSpelling): Expr =>
816
+ plusConst(
817
+ s.idx.reduce<Expr>((acc, r) => ({ k: 'bin', op: '+', l: acc, r }), { k: 'var', name: iv }),
818
+ (s.elems - start) | 0,
819
+ );
820
+ while (spellings.size > 0) {
821
+ // rewrite the derefs, then verify nothing of any re-spelled `p` survives
822
+ const offsetDerefs = new Set<string>();
823
+ const rewriteExpr = (e: Expr): Expr => {
824
+ const m = mapExprChildren(e, rewriteExpr);
825
+ const s = m.k === 'index' && m.base.k === 'var' ? spellings.get(m.base.name) : undefined;
826
+ if (m.k === 'index' && s !== undefined) {
827
+ if (m.idx.k !== 'const' || m.idx.value !== 0) {
828
+ offsetDerefs.add(s.p);
829
+ return m;
830
+ }
831
+ return { ...m, base: { ...s.base }, idx: idxOf(s) };
832
+ }
833
+ return m;
834
+ };
835
+ const steps = new Set(tail.filter((st) => spellings.has(isUnitIncrement(st)!)));
836
+ const body = dw.body.filter((st) => !steps.has(st)).map((st) => mapStmtExprs(st, rewriteExpr));
837
+ const dw2: Stmt = { ...dw, body };
838
+ const failed = inits.filter(
839
+ (st) =>
840
+ spellings.has(st.name) &&
841
+ (offsetDerefs.has(st.name) ||
842
+ !confinedToWalk(fnBody, st.name, countMentions([st], st.name), dw) ||
843
+ countMentions([dw2], st.name) !== 0),
844
+ );
845
+ if (failed.length === 0) {
846
+ return { loop: dw2, run: n, kept: inits.filter((st) => !spellings.has(st.name)) };
847
+ }
848
+ failed.forEach((st) => spellings.delete(st.name));
656
849
  }
657
- return dw2;
850
+ return null;
658
851
  }
659
852
 
660
853
  /** Try the walk→index re-spelling on one function. Returns the transformed COPY when at least
@@ -679,6 +872,10 @@ export function reindexWalks(
679
872
  sfn.locals.filter((l) => l.volatile === true || l.pointeeVolatile === true).map((l) => l.name),
680
873
  );
681
874
  const storage = nameStorage(sfn);
875
+ // v3's invariant-addend test: the names no callee and no aliasing store can write — a param or a
876
+ // local whose address the function never takes
877
+ const addressTaken = new Set([...walkExprs(sfn.body)].flatMap((e) => (e.k === 'addr' ? [e.name] : [])));
878
+ const privateNames = new Set([...storage].filter(([n, c]) => c !== 'global' && !addressTaken.has(n)).map(([n]) => n));
682
879
  for (const v of [...sfn.params, ...sfn.locals]) {
683
880
  if (v.type.kind === 'ptr') {
684
881
  ptrVars.set(v.name, v.type);
@@ -781,15 +978,14 @@ export function reindexWalks(
781
978
  }
782
979
  }
783
980
  }
784
- // v3 — the up-counting byte walk with an expression base: `p = (u8 *)(EXPR)` immediately
981
+ // v3 — the up-counting walk with an expression base: `p = (T *)(EXPR)` immediately
785
982
  // ahead of a counter-carried do-while (see tryExprWalk)
786
983
  if (s.k === 'dowhile') {
787
- const prev = out[out.length - 1];
788
- const prev2 = out[out.length - 2];
789
- const dw2 = tryExprWalk(prev2, prev, s, ptrVars, sfn.body, declTypes, volatileLocals);
790
- if (dw2) {
791
- out.pop(); // the walk init is subsumed by the indexed spelling
792
- out.push(recurse(dw2));
984
+ const v3 = tryExprWalk(out, s, ptrVars, sfn.body, declTypes, volatileLocals, privateNames);
985
+ if (v3) {
986
+ // the walk inits are subsumed by the indexed spelling; the counter's init stays
987
+ out.splice(out.length - v3.run, v3.run, ...v3.kept);
988
+ out.push(recurse(v3.loop));
793
989
  fired++;
794
990
  continue;
795
991
  }
@@ -855,6 +855,8 @@ export function applyScopedBasePlan(sfn: SFn, { entries: plan, repoint, compound
855
855
  };
856
856
  case 'break':
857
857
  case 'continue':
858
+ case 'goto':
859
+ case 'caseEndGoto':
858
860
  return s;
859
861
  }
860
862
  };
package/src/l3/storage.ts CHANGED
@@ -17,7 +17,10 @@
17
17
  // silently a different object: `addressableGlobals`;
18
18
  // • it asks which of the three classes a bare `assign` name denotes at all: `nameStorage`, the
19
19
  // primitive the other two are the shadow-sensitive projections over.
20
- import type { SFn } from './ast';
20
+ // One question about LOCAL storage sits beside them, because it is asked from the same lists:
21
+ // which expressions are a local object's ADDRESS (`localAddresses`), the frame bytes a respelled
22
+ // tree must keep reading as integers (contracts.ts `assertLocalStorageStaysInteger`).
23
+ import { type Expr, type SFn, type Stmt, assignsIn, dotBase, stmtChildren, taintOverAssigns, walkExprs } from './ast';
21
24
 
22
25
  export type Storage = 'local' | 'param' | 'global';
23
26
 
@@ -46,3 +49,97 @@ export function declaredGlobals(sfn: SFn): Set<string> {
46
49
  export function addressableGlobals(sfn: SFn): Set<string> {
47
50
  return new Set([...nameStorage(sfn)].filter(([, s]) => s === 'global').map(([n]) => n));
48
51
  }
52
+
53
+ /** The ADDRESSES of a tree's local objects — the frame bytes, which nothing but the function's own
54
+ * accesses and the callees it hands an address to can reach.
55
+ *
56
+ * `isAddress(e)`: `e` evaluates to an address inside a local object — a local array (`sp0`,
57
+ * which decays), a local's address (`&v`), a local ever assigned one (`p0 = (s32 *)sp0;`, a
58
+ * flow-insensitive taint), under a cast or moved by `+`/`-`. PRECISE in the direction that
59
+ * matters to its readers: a value READ OUT of the bytes (`((s32 *)sp0)[i]`, a pointer the frame
60
+ * holds) is not an address of them, so an access through it is not an access to the frame.
61
+ * Known gap, on the side of answering no: an array MEMBER spelled as a `field` (`s.arr`) is an
62
+ * address C decays, and this does not follow it.
63
+ *
64
+ * `accessAddress(e)`: the operand an access node reads its address from — an `index`'s base, an
65
+ * arrow `field`'s base, a dot `field`'s array element's base, or a struct VALUE's own address
66
+ * (`s.f` reads at `&s`). A param counts as a local throughout: its home is the frame once its
67
+ * address is taken, and a param assigned an address holds it as a local would. */
68
+ export function localAddresses(sfn: SFn): {
69
+ isAddress: (e: Expr) => boolean;
70
+ accessAddress: (e: Extract<Expr, { k: 'index' } | { k: 'field' }>) => Expr;
71
+ } {
72
+ const storage = nameStorage(sfn);
73
+ // a param is a local variable of the callee: its home is the frame once its address is taken
74
+ const isLocal = (n: string): boolean => storage.get(n) === 'local' || storage.get(n) === 'param';
75
+ const addressIn = (e: Expr, tainted: ReadonlySet<string>): boolean => {
76
+ switch (e.k) {
77
+ case 'var':
78
+ case 'postincr':
79
+ return tainted.has(e.name);
80
+ case 'addr':
81
+ return isLocal(e.name);
82
+ case 'cast':
83
+ return addressIn(e.e, tainted);
84
+ case 'bin':
85
+ return (
86
+ (e.op === '+' && (addressIn(e.l, tainted) || addressIn(e.r, tainted))) ||
87
+ (e.op === '-' && addressIn(e.l, tainted))
88
+ );
89
+ default:
90
+ return false;
91
+ }
92
+ };
93
+ const tainted = taintOverAssigns(
94
+ assignsIn(sfn.body),
95
+ sfn.locals.filter((l) => l.type.kind === 'array').map((l) => l.name),
96
+ (a, t) => isLocal(a.name) && addressIn(a.value, t),
97
+ );
98
+ return {
99
+ isAddress: (e) => addressIn(e, tainted),
100
+ accessAddress: (e) =>
101
+ e.k === 'index'
102
+ ? e.base
103
+ : (dotBase(e)?.base ?? (e.dot === true && e.base.k === 'var' ? { k: 'addr', name: e.base.name } : e.base)),
104
+ };
105
+ }
106
+
107
+ /** Whether a local object's address LEAVES the function — passed to a call, stored to memory,
108
+ * assigned to a non-local, or returned — AND the function itself writes local storage. Both
109
+ * halves are what let a pointer the function obtains LATER (a call's result, a load) name bytes
110
+ * the function also stores to: a callee that keeps the address can hand it back.
111
+ *
112
+ * OVER-WIDE ON PURPOSE, and what it costs is measured: a store to a DEVICE register counts as an
113
+ * escape, since the tree cannot tell a DMA source register (`REG_DMA3SAD = sp0;`, the GBA's fill
114
+ * idiom) from memory a later load reads back. On kleod:PauseMenuScreenHandler that refuses every
115
+ * `/ptr-field` candidate, none of which was its winner. */
116
+ export function localStorageEscapesWritten(sfn: SFn): boolean {
117
+ const { isAddress, accessAddress } = localAddresses(sfn);
118
+ const storage = nameStorage(sfn);
119
+ const held = (n: string): boolean => storage.get(n) === 'local' || storage.get(n) === 'param';
120
+ let escapes = false;
121
+ let written = false;
122
+ const addressTaken = new Set<string>();
123
+ for (const e of walkExprs(sfn.body)) {
124
+ if (e.k === 'call' && e.args.some(isAddress)) {
125
+ escapes = true;
126
+ }
127
+ if (e.k === 'addr' && held(e.name)) {
128
+ addressTaken.add(e.name);
129
+ }
130
+ }
131
+ const visit = (s: Stmt): void => {
132
+ if (s.k === 'store') {
133
+ escapes ||= isAddress(s.value);
134
+ written ||= (s.lval.k === 'index' || s.lval.k === 'field') && isAddress(accessAddress(s.lval));
135
+ } else if (s.k === 'assign') {
136
+ escapes ||= !held(s.name) && isAddress(s.value);
137
+ written ||= addressTaken.has(s.name);
138
+ } else if (s.k === 'return' && s.value !== undefined) {
139
+ escapes ||= isAddress(s.value);
140
+ }
141
+ stmtChildren(s).forEach(visit);
142
+ };
143
+ sfn.body.forEach(visit);
144
+ return escapes && written;
145
+ }