@asmlift/core 0.9.0 → 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 (101) 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 +1 -0
  5. package/src/backend/cfamily.ts +165 -50
  6. package/src/backend/cpp.ts +2 -1
  7. package/src/backend/pascal.ts +31 -12
  8. package/src/contracts.ts +173 -16
  9. package/src/declare.ts +69 -20
  10. package/src/frontend/asmdata.ts +7 -1
  11. package/src/frontend/call-plan.ts +964 -60
  12. package/src/frontend/dead-reads.ts +628 -0
  13. package/src/frontend/device-pins.ts +23 -4
  14. package/src/frontend/emit.ts +4 -3
  15. package/src/frontend/fpu.ts +224 -17
  16. package/src/frontend/frame-objects.ts +1232 -192
  17. package/src/frontend/frontend.ts +64 -3
  18. package/src/frontend/high-half.ts +53 -4
  19. package/src/frontend/jump-table.ts +530 -0
  20. package/src/frontend/local-object.ts +158 -32
  21. package/src/frontend/mips.ts +2076 -215
  22. package/src/frontend/own-struct-return.ts +500 -0
  23. package/src/frontend/ppc.ts +2711 -232
  24. package/src/frontend/register-struct-call.ts +484 -0
  25. package/src/frontend/reloc-symbol.ts +22 -7
  26. package/src/frontend/ssa.ts +1460 -44
  27. package/src/frontend/stackargs.ts +106 -57
  28. package/src/frontend/thumb.ts +2044 -318
  29. package/src/ir/alias.ts +42 -0
  30. package/src/ir/bits.ts +681 -1
  31. package/src/ir/core.ts +100 -5
  32. package/src/ir/discipline.ts +86 -21
  33. package/src/ir/entry.ts +159 -0
  34. package/src/{structure → ir}/loops.ts +6 -2
  35. package/src/ir/opcodes.ts +109 -9
  36. package/src/ir/types.ts +86 -2
  37. package/src/ir/verify.ts +80 -11
  38. package/src/l3/ast.ts +290 -18
  39. package/src/l3/basecse.ts +33 -5
  40. package/src/l3/constinit.ts +382 -0
  41. package/src/l3/dce.ts +2 -2
  42. package/src/l3/initfirst.ts +42 -11
  43. package/src/l3/intcell.ts +2 -1
  44. package/src/l3/narrowdecl.ts +77 -36
  45. package/src/l3/pollguard.ts +8 -2
  46. package/src/l3/ptrcell.ts +31 -34
  47. package/src/l3/ptrfield.ts +123 -8
  48. package/src/l3/regspell.ts +9 -4
  49. package/src/l3/reindex.ts +283 -96
  50. package/src/l3/storage.ts +98 -1
  51. package/src/l3/symbol-refs.ts +84 -6
  52. package/src/l3/tailmerge.ts +4 -2
  53. package/src/l3/typing.ts +108 -15
  54. package/src/l3/unreduce.ts +6 -5
  55. package/src/l3/volatileptr.ts +17 -54
  56. package/src/mangle.ts +30 -0
  57. package/src/pattern/engine.ts +361 -73
  58. package/src/pipeline.ts +541 -24
  59. package/src/proto-context.ts +905 -45
  60. package/src/proto.ts +555 -40
  61. package/src/raise/aggmembers.ts +290 -0
  62. package/src/raise/arrays.ts +5 -1
  63. package/src/raise/declared-volatile.ts +207 -31
  64. package/src/raise/floathelpers.ts +254 -33
  65. package/src/raise/gvn.ts +3 -3
  66. package/src/raise/latch.ts +162 -6
  67. package/src/raise/memberarrays.ts +10 -4
  68. package/src/raise/offsetnames.ts +10 -26
  69. package/src/raise/pairparams.ts +33 -0
  70. package/src/raise/paramwidth.ts +196 -6
  71. package/src/raise/pre-recovery.ts +102 -19
  72. package/src/raise/pre-structure.ts +54 -0
  73. package/src/raise/recover.ts +12 -5
  74. package/src/raise/reroll.ts +694 -0
  75. package/src/raise/shortcircuit.ts +26 -12
  76. package/src/raise/softdiv.ts +1 -1
  77. package/src/raise/storesign.ts +153 -0
  78. package/src/raise/struct-arrays.ts +30 -11
  79. package/src/raise/structs.ts +396 -33
  80. package/src/raise/truncload.ts +4 -3
  81. package/src/raise/unthread.ts +400 -0
  82. package/src/rank-declare.ts +118 -12
  83. package/src/rank-variations.ts +89 -31
  84. package/src/rank.ts +307 -54
  85. package/src/runtime-helpers.ts +140 -22
  86. package/src/structure/analysis.ts +165 -2
  87. package/src/structure/arith-ops.ts +23 -0
  88. package/src/structure/globalaccess.ts +111 -9
  89. package/src/structure/hazards.ts +16 -64
  90. package/src/structure/local-statics.ts +145 -9
  91. package/src/structure/namecoalesce.ts +1 -1
  92. package/src/structure/pointer-spelling.ts +1106 -0
  93. package/src/structure/redundant-test.ts +1 -1
  94. package/src/structure/structure.ts +2180 -1054
  95. package/src/structure/switch-recover.ts +244 -100
  96. package/src/symbols.ts +6 -0
  97. package/src/target.ts +765 -52
  98. package/src/trace.ts +26 -8
  99. package/src/variation-definitions.ts +126 -14
  100. package/src/variation-gates.ts +2 -0
  101. package/src/variation-tokens.ts +5 -1
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
@@ -99,6 +99,7 @@ import {
99
99
  rematerializableAddress,
100
100
  stmtChildren,
101
101
  stmtExprs,
102
+ walkExprs,
102
103
  } from './ast';
103
104
  import { type Gate, firstRejection } from './gates';
104
105
  import { takenNames } from './hoist';
@@ -301,11 +302,13 @@ export const COUNTDOWN_GATES: readonly Gate<CountdownCtx>[] = [
301
302
  ];
302
303
 
303
304
  /** 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). */
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. */
305
307
  function hasLoopExit(stmts: Stmt[]): boolean {
306
308
  return stmts.some((s) => {
307
309
  switch (s.k) {
308
310
  case 'break':
311
+ return s.ofSwitch !== true;
309
312
  case 'continue':
310
313
  return true;
311
314
  case 'if':
@@ -424,6 +427,11 @@ function reindexCond(cond: Expr, walk: WalkLoop, iv: string): Expr | null {
424
427
  return null;
425
428
  }
426
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.
427
435
  return swap ? { k: 'bin', op: cond.op, l: n, r: i } : { k: 'bin', op: cond.op, l: i, r: n };
428
436
  }
429
437
 
@@ -504,46 +512,81 @@ function reindexStmt(s: Stmt, walk: WalkLoop, iv: string): Stmt | null {
504
512
  }
505
513
  }
506
514
 
507
- // ── 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 ─────────────────────────────────────────
508
526
  //
509
- // 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);
510
528
  //
511
529
  // 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,
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' },
527
570
  dw: Stmt & { k: 'dowhile' },
528
571
  ptrVars: Map<string, IrType>,
529
- fnBody: Stmt[],
530
572
  declTypes: VarTypes,
531
573
  volatileLocals: ReadonlySet<string>,
532
- ): Stmt | null {
533
- if (prev?.k !== 'assign' || !ptrVars.has(prev.name) || volatileLocals.has(prev.name)) {
534
- return null;
535
- }
536
- const p = prev.name;
537
- const pT = ptrVars.get(p)!;
538
- 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)) {
539
579
  return null;
540
580
  }
541
- if (!derefWidths([dw], p).every((w) => w === 1)) {
581
+ const W = pT.to.width / 8;
582
+ if (!derefWidths([dw], p).every((w) => w === W)) {
542
583
  return null;
543
584
  }
544
- // EXPR decomposition
545
- let expr: Expr = prev.value;
546
- 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))) {
547
590
  expr = expr.e;
548
591
  }
549
592
  const addends: Expr[] = [];
@@ -556,21 +599,40 @@ function tryExprWalk(
556
599
  }
557
600
  };
558
601
  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) {
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) {
570
614
  return null;
571
615
  }
572
- if (baseT.kind === 'ptr' && !(baseT.to.kind === 'int' && baseT.to.width === 8)) {
573
- 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
+ }
574
636
  }
575
637
  const pure = (e: Expr): boolean =>
576
638
  e.k === 'var' || e.k === 'const'
@@ -580,7 +642,6 @@ function tryExprWalk(
580
642
  : e.k === 'cast'
581
643
  ? pure(e.e)
582
644
  : false;
583
- const assignedIn = (stmts: Stmt[], name: string): boolean => stmts.some((st) => stmtAssigns(st, name));
584
645
  const restVars = new Set<string>();
585
646
  const collectVars = (e: Expr): void => {
586
647
  if (e.k === 'var') {
@@ -592,69 +653,192 @@ function tryExprWalk(
592
653
  });
593
654
  };
594
655
  rest.forEach(collectVars);
595
- restVars.add(base.name);
596
- 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
+ ) {
597
677
  return null;
598
678
  }
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)) {
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) {
610
692
  return null;
611
693
  }
612
- const iv = tailIncs.find((n) => n !== p);
613
- 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
+ ) {
614
762
  return null;
615
763
  }
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') {
764
+ const counters = inits.filter((st) => !ptrVars.has(st.name));
765
+ if (counters.length !== 1) {
619
766
  return null;
620
767
  }
621
- 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)) {
622
771
  return null;
623
772
  }
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) {
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') {
627
781
  return null;
628
782
  }
629
- 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)) {
630
786
  return null;
631
787
  }
632
- if (volatileLocals.has(iv) || [...restVars].some((n) => volatileLocals.has(n))) {
788
+ if (dw.body.slice(0, -n).some((st) => stmtAssigns(st, iv))) {
633
789
  return null;
634
790
  }
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() };
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);
646
804
  }
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
805
  }
654
- if (countMentions([dw2], p) !== 0) {
655
- 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));
656
840
  }
657
- return dw2;
841
+ return null;
658
842
  }
659
843
 
660
844
  /** Try the walk→index re-spelling on one function. Returns the transformed COPY when at least
@@ -679,6 +863,10 @@ export function reindexWalks(
679
863
  sfn.locals.filter((l) => l.volatile === true || l.pointeeVolatile === true).map((l) => l.name),
680
864
  );
681
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));
682
870
  for (const v of [...sfn.params, ...sfn.locals]) {
683
871
  if (v.type.kind === 'ptr') {
684
872
  ptrVars.set(v.name, v.type);
@@ -781,15 +969,14 @@ export function reindexWalks(
781
969
  }
782
970
  }
783
971
  }
784
- // 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
785
973
  // ahead of a counter-carried do-while (see tryExprWalk)
786
974
  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));
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));
793
980
  fired++;
794
981
  continue;
795
982
  }
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
+ }