@asmlift/core 0.10.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 (68) hide show
  1. package/package.json +1 -1
  2. package/src/backend/c.ts +1 -0
  3. package/src/backend/cfamily.ts +33 -12
  4. package/src/backend/cpp.ts +1 -0
  5. package/src/backend/lines.ts +10 -0
  6. package/src/backend/pascal.ts +17 -5
  7. package/src/cdecl/parse.ts +21 -0
  8. package/src/contracts.ts +73 -20
  9. package/src/frontend/arm-body.ts +155 -0
  10. package/src/frontend/asmdata.ts +2 -1
  11. package/src/frontend/call-plan.ts +197 -45
  12. package/src/frontend/callee-body.ts +370 -0
  13. package/src/frontend/device-pins.ts +18 -8
  14. package/src/frontend/frame-objects.ts +504 -61
  15. package/src/frontend/frontend.ts +20 -0
  16. package/src/frontend/jump-table.ts +35 -20
  17. package/src/frontend/mips.ts +226 -8
  18. package/src/frontend/ppc.ts +26 -2
  19. package/src/frontend/register-struct-call.ts +1 -86
  20. package/src/frontend/ssa.ts +263 -1
  21. package/src/frontend/stackargs.ts +95 -17
  22. package/src/frontend/thumb-entry.ts +340 -0
  23. package/src/frontend/thumb.ts +410 -61
  24. package/src/ir/alias.ts +29 -8
  25. package/src/ir/bits.ts +1081 -66
  26. package/src/ir/core.ts +154 -4
  27. package/src/ir/entry.ts +3 -5
  28. package/src/ir/opcodes.ts +28 -0
  29. package/src/ir/parse.ts +1 -0
  30. package/src/ir/simplify.ts +61 -10
  31. package/src/ir/verify.ts +16 -0
  32. package/src/l3/argbase.ts +2 -0
  33. package/src/l3/ast.ts +294 -2
  34. package/src/l3/basecse.ts +35 -10
  35. package/src/l3/caseendgoto.ts +223 -0
  36. package/src/l3/coalesce.ts +42 -14
  37. package/src/l3/constinit.ts +34 -6
  38. package/src/l3/dce.ts +52 -8
  39. package/src/l3/ptrcell.ts +2 -0
  40. package/src/l3/regspell.ts +2 -0
  41. package/src/l3/reindex.ts +13 -4
  42. package/src/l3/scopebase.ts +2 -0
  43. package/src/l3/tailmerge.ts +2 -0
  44. package/src/pipeline.ts +507 -23
  45. package/src/proto-context.ts +135 -1
  46. package/src/raise/const.ts +25 -0
  47. package/src/raise/declared-volatile.ts +1 -1
  48. package/src/raise/divpow2.ts +192 -0
  49. package/src/raise/offsetnames.ts +412 -10
  50. package/src/raise/pre-recovery.ts +8 -5
  51. package/src/raise/pre-structure.ts +24 -5
  52. package/src/raise/reroll.ts +4 -24
  53. package/src/raise/shortcircuit.ts +192 -23
  54. package/src/raise/unpre.ts +594 -0
  55. package/src/rank-variations.ts +97 -4
  56. package/src/rank.ts +258 -43
  57. package/src/structure/analysis.ts +56 -10
  58. package/src/structure/cross-jumped-tail.ts +216 -0
  59. package/src/structure/globalaccess.ts +46 -1
  60. package/src/structure/structure.ts +4221 -368
  61. package/src/structure/switch-recover.ts +345 -26
  62. package/src/structure/unfolded-branch-constant.ts +90 -0
  63. package/src/symbols.ts +52 -15
  64. package/src/target.ts +269 -8
  65. package/src/trace.ts +28 -14
  66. package/src/variation-definitions.ts +207 -6
  67. package/src/variation-gates.ts +2 -0
  68. package/src/variation-tokens.ts +63 -2
@@ -25,6 +25,7 @@
25
25
  // drops the rest, a mask. A function that reads past them — stores the whole word, compares it,
26
26
  // returns it where its own declaration says it returns a value — declines; one whose return
27
27
  // nothing declares returns no value instead, where that guessed return is the only such reader.
28
+ import { demandedBits } from '../ir/bits';
28
29
  import {
29
30
  type Block,
30
31
  CALL_RETURN_OBJECT,
@@ -396,89 +397,3 @@ function foldFrameHome(fn: Fn, { block, local, size, word, loaded }: Image): voi
396
397
  }
397
398
 
398
399
  const FULL = 0xffffffff;
399
-
400
- /** THE BITS OF `v` ANY READER NEEDS, as a mask: a store of W bytes needs the low 8W bits of the
401
- * value it stores, a constant mask and the bitwise ops pass their readers' bits through, a shift
402
- * by a constant moves them, an add, a subtract or a multiply needs every bit up to its readers'
403
- * highest, since a carry only travels up, and a join needs what its parameter's readers need.
404
- * Every other reader — a compare, a call, an address — needs all 32, and so does a join that
405
- * reaches itself again. A `ret` needs none where the function's own declaration returns nothing. */
406
- function demandedBits(fn: Fn, root: Value, voidOwn: boolean): number {
407
- const defs = defOpMap(fn);
408
- const memo = new Map<Value, number>();
409
- const constOf = (x: Value): number | undefined => {
410
- const d = defs.get(x);
411
- return d?.opcode === 'const' && typeof d.attrs.value === 'number' ? d.attrs.value >>> 0 : undefined;
412
- };
413
- const upTo = (d: number): number => (d === 0 ? 0 : d >= 0x80000000 ? FULL : (2 ** (32 - Math.clz32(d)) - 1) >>> 0);
414
- const of = (v: Value): number => {
415
- const known = memo.get(v);
416
- if (known !== undefined) {
417
- return known;
418
- }
419
- memo.set(v, FULL); // a reader reached again through a join needs everything
420
- let need = 0;
421
- for (const b of fn.blocks) {
422
- for (const op of b.ops) {
423
- for (const e of op.successors) {
424
- e.args.forEach((a, i) => {
425
- if (a === v) {
426
- need |= of(e.block.params[i]);
427
- }
428
- });
429
- }
430
- if (!op.operands.includes(v)) {
431
- continue;
432
- }
433
- const out = () => (op.results.length === 1 ? of(op.results[0]) : FULL);
434
- const imm = typeof op.attrs.imm === 'number' && op.operands.length === 1 ? op.attrs.imm : undefined;
435
- const other = op.operands.length === 2 ? op.operands[op.operands[0] === v ? 1 : 0] : undefined;
436
- switch (op.opcode) {
437
- case 'store':
438
- need |=
439
- op.operands[0] === v || typeof op.attrs.width !== 'number' || op.attrs.width >= 4
440
- ? FULL
441
- : (1 << (8 * op.attrs.width)) - 1;
442
- break;
443
- case 'ret':
444
- need |= voidOwn ? 0 : FULL;
445
- break;
446
- case 'and': {
447
- const c = other === undefined || other === v ? undefined : constOf(other);
448
- need |= c === undefined ? out() : out() & c;
449
- break;
450
- }
451
- case 'or':
452
- case 'xor':
453
- case 'not':
454
- need |= other === v ? FULL : out();
455
- break;
456
- case 'add':
457
- case 'sub':
458
- case 'mul':
459
- case 'neg':
460
- need |= other === v ? FULL : upTo(out());
461
- break;
462
- case 'shl':
463
- need |= imm === undefined ? FULL : out() >>> imm;
464
- break;
465
- case 'shr_u':
466
- need |= imm === undefined ? FULL : (out() << imm) >>> 0;
467
- break;
468
- case 'shr_s': {
469
- const d = out();
470
- need |=
471
- imm === undefined ? FULL : ((d << imm) | (imm > 0 && d >>> (32 - imm) !== 0 ? 0x80000000 : 0)) >>> 0;
472
- break;
473
- }
474
- default:
475
- need |= FULL;
476
- }
477
- need >>>= 0;
478
- }
479
- }
480
- memo.set(v, need);
481
- return need;
482
- };
483
- return of(root);
484
- }
@@ -16,6 +16,7 @@
16
16
  // `irBlocks`, args left empty — phi wiring appends them), then call `markFilled(b)`. When all
17
17
  // blocks are filled, call `finish()` to remove trivial phis.
18
18
  import {
19
+ type AsmLayout,
19
20
  Block,
20
21
  Fn,
21
22
  Op,
@@ -65,6 +66,11 @@ export interface SsaBuilder {
65
66
  *
66
67
  * One of the three call verbs `CallDeclarations.lower` owns ({@link noteCall}). */
67
68
  readGuessedArg(reg: string, b: number): Value;
69
+ /** Record that the call being lowered in block `b` reads `v` from argument register `reg` it
70
+ * guessed it takes, for {@link readAfterEveryReachingWrite} alone: the call is an instruction
71
+ * reading what the register's last write left there, as a sibling path's call may be, while
72
+ * {@link readGuessedArg} makes the read carry no claim for `finish` to judge. */
73
+ noteGuessedArgRead(reg: string, b: number, v: Value): void;
68
74
  /** The value `reg` holds at the END of block `b` once `b` is filled, as the edges out of it carry
69
75
  * it; `null` before. A lookup, not a read: it records no read for `finish` to judge. */
70
76
  readAtEnd(reg: string, b: number): Value | null;
@@ -125,6 +131,46 @@ export interface SsaBuilder {
125
131
  /** Whether block `b` WROTE `reg` since its last call so far (since its start if it made none).
126
132
  * Only a write counts: a read leaves a definition behind but is not one (`writeVar`). */
127
133
  writtenSinceCall(reg: string, b: number): boolean;
134
+ /** How many ops block `b` held when it last wrote `reg` so far, or `undefined` where `b` has not
135
+ * written it: every op at or past that index came after the write. A copy (`add r1, r5, #0`) names
136
+ * no new value, so this is how a question about one WRITE of a register is asked, where its value
137
+ * was read before the write too (frontend/call-plan.ts `leftover`). */
138
+ lastWriteOp(reg: string, b: number): number | undefined;
139
+ /** Whether an instruction READ `reg` after EACH write of it that reaches block `b` and whose value
140
+ * `passes`, on ANY path out of that write but the ones into `b`, the first read on that path being
141
+ * one whose value `accept` takes. A write no read follows on any path was
142
+ * made for `b`'s use, so one is enough to answer no. A call destroying `reg` is no write of this
143
+ * function's, and neither is the entry's value where a path from the entry writes neither, so they
144
+ * do not answer no; the entry's value read after (as `accept` takes it), or read by a lowered op
145
+ * at all (as `unlowered` takes it: no write placed that read), counts as a write read. Where a
146
+ * call destroyed `reg` on EVERY path in, it holds what a callee left there and the answer is yes.
147
+ * The question is about the write, not its value: a copy (`add r4, r0, #0`) reads `r0` and lowers
148
+ * to no op, so no op-level question sees that read, and it counts here; a copy made in an earlier
149
+ * block names the value it copies, read before that write too, and is no read of it. Any path out
150
+ * of the write, because a MIPS compiler hoists one call's argument into the delay slot of the
151
+ * branch that picks it (`bnez v0, L; move a0, zero`), so the write reaches the other call unread
152
+ * on the path between. `b`'s own reads are not asked: what a read there that lowers to no op says
153
+ * differs by compiler (frontend/call-plan.ts `leftover`). Only the frontend's own reads count
154
+ * ({@link readVar}), and a guessed call's argument read the planner records for it
155
+ * ({@link noteGuessedArgRead}: a sibling path's call reads the write); a lookup this construction
156
+ * makes, or the planner's own probe ({@link readGuessedArg} alone), is none. A block not
157
+ * filled yet is passed through looking for the write, as {@link holdsValue} does; a path out of
158
+ * a write that reaches one has reads nobody has seen yet, and the write's value answers for it
159
+ * as `unlowered` takes it (IDO reloads `lw a0, 24(sp)` in a branch's delay slot for the arm laid
160
+ * out after the call). A write of a value `answersIfRead` takes (an earlier call's result, copied
161
+ * or reloaded) answers yes where it was read after, and is otherwise none: such a value is the
162
+ * callee's, saved across a call as often as handed to one. `undefined` where no write of this
163
+ * function's is left to judge and no entry value read: the question has no answer here. */
164
+ readAfterEveryReachingWrite(
165
+ reg: string,
166
+ b: number,
167
+ ask: {
168
+ passes: (v: Value) => boolean;
169
+ accept: (v: Value) => boolean;
170
+ unlowered: (v: Value) => boolean;
171
+ answersIfRead: (v: Value) => boolean;
172
+ },
173
+ ): boolean | undefined;
128
174
  /** Whether EVERY path into this point of block `b` wrote `reg` (or had a call destroy it) after
129
175
  * leaving the last block `stop` names: {@link writtenSinceCall} asked of `b` so far and, where `b`
130
176
  * did not write it, of each predecessor in turn. A path reaching a `stop` block, the entry, or a
@@ -610,6 +656,57 @@ export function clobberedByCall(target: {
610
656
  return target.callerSaved.filter((r) => r !== target.returnReg);
611
657
  }
612
658
 
659
+ /** One asm block as {@link recordAsmLayout} reads it: its instructions without its control transfer,
660
+ * and whether that transfer is an unconditional jump to a label. `undefined` for a block the
661
+ * frontend made up, which has no place in the asm. An instruction's `reloc` is the relocation that
662
+ * fills it, where the operands do not name the symbol (disasm.ts `DisasmInstr.reloc`). */
663
+ export interface AsmBlockText {
664
+ readonly body: readonly {
665
+ readonly mnemonic: string;
666
+ readonly ops: readonly string[];
667
+ readonly reloc?: { readonly sym: string; readonly addend: number };
668
+ }[];
669
+ readonly endsInJump: boolean;
670
+ /** which instructions of `body` make a call, which only the frontend's ISA knows */
671
+ readonly isCall: (ins: AsmBlockText['body'][number]) => boolean;
672
+ }
673
+
674
+ /** One instruction of {@link AsmBlockText} as the layout record spells it: `mnemonic op,op`, then the
675
+ * symbol its relocation fills in, if any. In a relocatable object's disassembly the operands of a
676
+ * global's load or store, or of a call, read the same whichever symbol it is: gcc 2.7.2's `lui
677
+ * at,0x0` / `sw a2,0(at)` for `gT = c` and for `gW = c`, and `jal 0 <f>` for every callee. Spelled
678
+ * without the symbol, two arms that store to different globals would read as identical bodies. */
679
+ function layoutText(ins: AsmBlockText['body'][number]): string {
680
+ const text = ins.ops.length ? `${ins.mnemonic} ${ins.ops.join(',')}` : ins.mnemonic;
681
+ if (ins.reloc === undefined) {
682
+ return text;
683
+ }
684
+ const addend = ins.reloc.addend === 0 ? '' : `+0x${ins.reloc.addend.toString(16)}`;
685
+ return `${text} <${ins.reloc.sym}${addend}>`;
686
+ }
687
+
688
+ /** The layout record (ir/core.ts `AsmLayout`): asm block `i` is `irBlocks[i]`, which is how every
689
+ * frontend's builder numbers them, and its index is its position among the blocks that have a
690
+ * place in the asm. */
691
+ export function recordAsmLayout(
692
+ irBlocks: readonly Block[],
693
+ asmBlocks: readonly (AsmBlockText | undefined)[],
694
+ ): AsmLayout {
695
+ const layout: AsmLayout = new Map();
696
+ let index = 0;
697
+ asmBlocks.forEach((ab, i) => {
698
+ if (ab !== undefined) {
699
+ layout.set(irBlocks[i], {
700
+ index: index++,
701
+ body: ab.body.map(layoutText),
702
+ calls: ab.body.flatMap((ins, at) => (ab.isCall(ins) ? [at] : [])),
703
+ endsInJump: ab.endsInJump,
704
+ });
705
+ }
706
+ });
707
+ return layout;
708
+ }
709
+
613
710
  export function makeSsaBuilder(
614
711
  name: string,
615
712
  blockCount: number,
@@ -670,6 +767,7 @@ export function makeSsaBuilder(
670
767
  slotHomes: undefined,
671
768
  paramEvidence: undefined,
672
769
  localObjects: undefined,
770
+ asmLayout: undefined,
673
771
  };
674
772
 
675
773
  const defs: Array<Map<string, Value>> = irBlocks.map(() => new Map());
@@ -696,6 +794,16 @@ export function makeSsaBuilder(
696
794
 
697
795
  // `preds` lists an entry per CFG EDGE; these are the distinct predecessor BLOCKS.
698
796
  const distinctPreds = (b: number): number[] => [...new Set(preds[b])];
797
+ // the distinct successor blocks of each, built on first ask (`readAfterEveryReachingWrite`)
798
+ let successorsMemo: number[][] | undefined;
799
+ const successors = (): number[][] => {
800
+ if (successorsMemo === undefined) {
801
+ const succ: number[][] = irBlocks.map(() => []);
802
+ irBlocks.forEach((_, s) => distinctPreds(s).forEach((p) => succ[p].push(s)));
803
+ successorsMemo = succ;
804
+ }
805
+ return successorsMemo;
806
+ };
699
807
 
700
808
  // CALLER-SAVED CLOBBER, for guessed call arities (see trimClobberedCallArgs). Tracked HERE
701
809
  // because every register write in every frontend already goes through `writeVar`: a frontend
@@ -744,6 +852,13 @@ export function makeSsaBuilder(
744
852
  // every ISA with no frontend code. Per block, because the consumer asks a per-EDGE question.
745
853
  const writeCount: number[] = irBlocks.map(() => 0);
746
854
  const lastWriteAt: Array<Map<string, number>> = irBlocks.map(() => new Map());
855
+ // the ops block `b` held at each register's last write in it (`lastWriteOp`)
856
+ const lastWriteOp: Array<Map<string, number>> = irBlocks.map(() => new Map());
857
+ // the value an instruction last read of each register block `b` read since its last write or the
858
+ // last call destroying it there (since the block's start where neither), and the value it first
859
+ // read of each before writing it or a call destroying it: `readAfterEveryReachingWrite`
860
+ const readSinceWrite: Array<Map<string, Value>> = irBlocks.map(() => new Map());
861
+ const readBeforeWrite: Array<Map<string, Value>> = irBlocks.map(() => new Map());
747
862
  const writeOrder: WriteOrder = { lastWrite: new Map(), writes: new Map() };
748
863
  fn.writeOrder = writeOrder;
749
864
 
@@ -935,6 +1050,8 @@ export function makeSsaBuilder(
935
1050
  decidedLocal[b].add(reg);
936
1051
  defs[b].set(reg, v);
937
1052
  lastWriteAt[b].set(reg, writeCount[b]++);
1053
+ lastWriteOp[b].set(reg, irBlocks[b].ops.length);
1054
+ readSinceWrite[b].delete(reg);
938
1055
  };
939
1056
  const readVar = (reg: string, b: number): Value => {
940
1057
  if (globalRegs.has(reg)) {
@@ -948,8 +1065,16 @@ export function makeSsaBuilder(
948
1065
  }
949
1066
  const v = readAny(reg, b);
950
1067
  noteStaleCandidate(reg, b, v);
1068
+ noteRead(reg, b, v);
951
1069
  return v;
952
1070
  };
1071
+ // an instruction's read, for `readAfterEveryReachingWrite`
1072
+ const noteRead = (reg: string, b: number, v: Value): void => {
1073
+ readSinceWrite[b].set(reg, v);
1074
+ if (!lastWriteOp[b].has(reg) && !clobberedLocal[b].has(reg) && !readBeforeWrite[b].has(reg)) {
1075
+ readBeforeWrite[b].set(reg, v);
1076
+ }
1077
+ };
953
1078
  /** The lookup, with no claim attached. The FRONTEND's read is what asserts "this register holds
954
1079
  * this value here"; the lookups this construction makes on its own — a single predecessor's
955
1080
  * definition, a phi's operand at each in-edge — are how that one read is answered, not further
@@ -1644,6 +1769,7 @@ export function makeSsaBuilder(
1644
1769
  irBlocks,
1645
1770
  readVar,
1646
1771
  readGuessedArg: readAny, // see the interface: the trim is this read's answer, not a refusal
1772
+ noteGuessedArgRead: noteRead,
1647
1773
  readAtEnd: (reg: string, b: number) => (filled[b] ? readAny(reg, b) : null),
1648
1774
  writeVar,
1649
1775
  frameWrites,
@@ -1655,6 +1781,79 @@ export function makeSsaBuilder(
1655
1781
  entryValue,
1656
1782
  holdsEntryValue,
1657
1783
  writtenSinceCall: (reg: string, b: number) => writtenSinceCall[b].has(reg),
1784
+ lastWriteOp: (reg: string, b: number) => lastWriteOp[b].get(reg),
1785
+ readAfterEveryReachingWrite: (reg, b, { passes, accept, unlowered, answersIfRead }) => {
1786
+ const writes = (at: number) => lastWriteOp[at].has(reg) || clobberedLocal[at].has(reg);
1787
+ // the blocks whose last write of `reg` reaches `b`, and whether a path from the entry wrote none.
1788
+ // `b` is judged up to here: met again round a loop, its end is no write that reaches here yet
1789
+ const writers: number[] = [];
1790
+ let fromEntry = false;
1791
+ const back = new Set<number>([b]);
1792
+ const walkBack = (at: number): void => {
1793
+ for (const p of distinctPreds(at)) {
1794
+ if (back.has(p)) {
1795
+ continue;
1796
+ }
1797
+ back.add(p);
1798
+ if (filled[p] && writes(p)) {
1799
+ writers.push(p);
1800
+ } else if (preds[p].length === 0) {
1801
+ fromEntry = true;
1802
+ } else {
1803
+ walkBack(p);
1804
+ }
1805
+ }
1806
+ };
1807
+ walkBack(b);
1808
+ const succs = (at: number): number[] => successors()[at].filter((s) => s !== b);
1809
+ // forward from a write of `written` (none for the entry's): the first read on each path, until
1810
+ // another write ends it
1811
+ const ahead = (from: number, seen: Set<number>, written: Value | undefined): boolean =>
1812
+ succs(from).some((s) => {
1813
+ if (seen.has(s)) {
1814
+ return false;
1815
+ }
1816
+ seen.add(s);
1817
+ if (!filled[s]) {
1818
+ return written !== undefined && unlowered(written);
1819
+ }
1820
+ const read = readBeforeWrite[s].get(reg);
1821
+ if (read !== undefined) {
1822
+ return accept(read);
1823
+ }
1824
+ return !writes(s) && ahead(s, seen, written);
1825
+ });
1826
+ const readAfter = (w: number): boolean => {
1827
+ const read = readSinceWrite[w].get(reg);
1828
+ return read !== undefined ? accept(read) : ahead(w, new Set(), defs[w].get(reg));
1829
+ };
1830
+ // a call's destroying it is no write of this function's: it neither counts nor answers; and a
1831
+ // write of a value `answersIfRead` takes answers only where it was read after
1832
+ const own = writers.filter((w) => {
1833
+ const v = defs[w].get(reg)!;
1834
+ return !clobberedLocal[w].has(reg) && passes(v) && (!answersIfRead(v) || readAfter(w));
1835
+ });
1836
+ // nor does the entry's value unread: nothing wrote it for the call. Read, it answers by its
1837
+ // value too (`unlowered`): no write of this function's made it, so no path out of one places
1838
+ // the read, and a path from the entry into a block not lowered yet holds reads nobody has seen
1839
+ const param = irBlocks[0].params.find((v) => paramReg.get(v) === reg);
1840
+ const entryRead =
1841
+ fromEntry &&
1842
+ ((param !== undefined && unlowered(param)) ||
1843
+ (readBeforeWrite[0].has(reg) ? accept(readBeforeWrite[0].get(reg)!) : ahead(0, new Set(), undefined)));
1844
+ // where a call destroyed it on every path in, it holds what a callee left there and nothing
1845
+ // this function named: no argument, whatever an earlier lookup's phi of the bytes before
1846
+ // those calls says (counted, sbk func_800B6E5C_A6D0C's a1 and a2 would be minted as entry
1847
+ // parameters the trim cuts from the call but leaves in the signature)
1848
+ if (!fromEntry && writers.length > 0 && writers.every((w) => clobberedLocal[w].has(reg))) {
1849
+ return true;
1850
+ }
1851
+ // no write of this function's left to judge, and no entry value read: nothing to answer
1852
+ if (own.length === 0 && !entryRead) {
1853
+ return undefined;
1854
+ }
1855
+ return own.every(readAfter);
1856
+ },
1658
1857
  writtenOnEveryPath: (reg, b, stop, visit) => {
1659
1858
  // `b` is judged up to here on the first visit; met again round a loop, it is the end of `b`,
1660
1859
  // which a back edge brings, and a cycle adds no path the first visit did not judge
@@ -1702,6 +1901,7 @@ export function makeSsaBuilder(
1702
1901
  for (const r of clobbers) {
1703
1902
  clobberedLocal[b].add(r);
1704
1903
  decidedLocal[b].add(r);
1904
+ readSinceWrite[b].delete(r);
1705
1905
  }
1706
1906
  imageLocal[b] = new Set();
1707
1907
  },
@@ -2108,6 +2308,27 @@ export function fallbackArgc(
2108
2308
  * ahead of the idiom and `bl h; mov r0,#1; bl g` in a case lifts `g(1)` — the gap rule's
2109
2309
  * `setUp` walks through calls and would count r3's copy, which `h` destroyed. */
2110
2310
  parked?: (reg: string) => boolean;
2311
+ /** how many argument words the callee's own body reads at most (frontend/callee-body.ts),
2312
+ * asked only where it can answer something. It is an upper bound on what the callee READS, and
2313
+ * a source call passes more than that whenever a parameter goes unused or a K&R call hands
2314
+ * extra arguments — `setx(a + 1, 4)` to a body reading only its first, `rd1(a, b, 7)` to one
2315
+ * reading r0 — so it never caps what the caller set up for the call: such a register stays
2316
+ * counted, and a value above a gap still decides the gap below it (`decide`). It answers only
2317
+ * for registers at or past it, whose being arguments changes nothing the callee computes:
2318
+ * - one that is a `leftover` stops the count: a write something else read since, which the
2319
+ * call found there (`gH = 5; gA = rnd()` leaves the store's 5 in r0, which the guess took as
2320
+ * `rnd(5)`). A copy made for the call is none, whatever read its value before
2321
+ * (`g4(a, b, c, d); un(a, b, c, d)` copies a..d back from their homes for `un`);
2322
+ * - a gap starting there, under a register that is a leftover or that no proof makes an
2323
+ * argument, stops the count at the gap rather than refuse.
2324
+ * A gap BELOW the bound refuses as ever, whatever lies above it: the callee may read it, and it
2325
+ * holds this function's own incoming value or nothing. `gp = 3; gq = rd1(a)` leaves the store's 3
2326
+ * in r1 above r0's a, and stopping there would drop the `a` rd1 reads. */
2327
+ bound?: () => number | undefined;
2328
+ /** whether what reaches argument register `reg` was written for something other than the call,
2329
+ * which read it since, and left there (frontend/call-plan.ts `leftover`). Asked only at or past
2330
+ * `bound`. */
2331
+ leftover?: (reg: string) => boolean;
2111
2332
  },
2112
2333
  ): number {
2113
2334
  const accept = opts.accept ?? (() => true);
@@ -2121,9 +2342,16 @@ export function fallbackArgc(
2121
2342
  const top = parkedAt < 0 ? argRegs.length : parkedAt;
2122
2343
  // the run of argument registers the caller set up from `from`, each past a declaration's
2123
2344
  // stated words asked whether it still holds the incoming value
2345
+ // whether argument register `k` lies at or past what the callee's own body reads
2346
+ const unread = (k: number): boolean => {
2347
+ const b = opts.bound?.();
2348
+ return b !== undefined && k >= b;
2349
+ };
2350
+ // past the bound, a leftover is no argument (`bound`): leftover first, the cheaper question
2351
+ const notPassed = (k: number): boolean => opts.leftover?.(argRegs[k]) === true && unread(k);
2124
2352
  const run = (from: number): number => {
2125
2353
  let n = from;
2126
- while (n < top && setUp(n)) {
2354
+ while (n < top && setUp(n) && !notPassed(n)) {
2127
2355
  // PAST A DECLARATION'S STATED WORDS, A REGISTER STILL HOLDING ITS INCOMING VALUE REFUSES. A
2128
2356
  // read of it (a home store, a compare, a store to a global) leaves the entry parameter behind
2129
2357
  // as a definition, so it reads as set up, and it is exactly as undecidable as the gap below: the
@@ -2151,7 +2379,13 @@ export function fallbackArgc(
2151
2379
  }
2152
2380
  return n;
2153
2381
  };
2382
+ // where the run stopped at a register set up but no argument (`bound`), every one above is past
2383
+ // the bound too, and the count ends there
2384
+ const stoppedPast = (n: number): boolean => n < top && setUp(n);
2154
2385
  let n = run(floor);
2386
+ if (stoppedPast(n)) {
2387
+ return n;
2388
+ }
2155
2389
  // A GAP BELOW A REGISTER THAT HOLDS A VALUE refuses, unless the frontend decides it: then the run
2156
2390
  // goes on through the gap and the register above it, and the next gap is asked the same.
2157
2391
  //
@@ -2176,15 +2410,30 @@ export function fallbackArgc(
2176
2410
  `value and ${argRegs[gapAt]} holds none — an argument register left at its incoming value and one ` +
2177
2411
  `the call does not pass look the same here, so the argument count is not decidable`,
2178
2412
  );
2413
+ // the gap and the register above it are past what the callee reads: no argument it can tell
2414
+ // from a missing one, so the count stops below the gap as at a gapless call
2415
+ if (opts.leftover?.(argRegs[k]) === true && unread(gapAt)) {
2416
+ return gapAt;
2417
+ }
2179
2418
  if (!opts.decide?.(argRegs.slice(n, k), argRegs[k], refuse)) {
2419
+ if (unread(gapAt)) {
2420
+ return gapAt;
2421
+ }
2180
2422
  refuse();
2181
2423
  }
2182
2424
  n = run(k);
2425
+ const stopped = stoppedPast(n);
2183
2426
  for (let above = k + 1; above < n; above++) {
2184
2427
  if (!opts.decide?.([], argRegs[above], refuse)) {
2428
+ if (unread(above)) {
2429
+ return above;
2430
+ }
2185
2431
  refuse();
2186
2432
  }
2187
2433
  }
2434
+ if (stopped) {
2435
+ return n;
2436
+ }
2188
2437
  k = n;
2189
2438
  }
2190
2439
  }
@@ -2909,8 +3158,21 @@ export function abiSortEntryParams(
2909
3158
  assertTrueEntry(entryHasPreds);
2910
3159
  const rank = (v: Value): number => slots.slotOf(paramReg.get(v) ?? '') ?? NOT_AN_ARGUMENT;
2911
3160
  entry.params.sort((x, y) => rank(x) - rank(y));
3161
+ for (const v of entry.params) {
3162
+ const r = rank(v);
3163
+ if (r !== NOT_AN_ARGUMENT) {
3164
+ argRanks.set(v, r);
3165
+ }
3166
+ }
2912
3167
  }
2913
3168
 
3169
+ /** The ABI argument rank each entry parameter arrived in, as {@link abiSortEntryParams} ranked it. */
3170
+ const argRanks = new WeakMap<Value, number>();
3171
+
3172
+ /** The argument rank (registers counted, then incoming stack words) entry parameter `v` arrived in,
3173
+ * or undefined for a live-in that is no argument slot, or a value no entry sort ranked. */
3174
+ export const entryArgRank = (v: Value): number | undefined => argRanks.get(v);
3175
+
2914
3176
  function assertTrueEntry(entryHasPreds: boolean): void {
2915
3177
  if (entryHasPreds) {
2916
3178
  throw new Error('internal: the entry-parameter rule needs an entry block with no predecessors');
@@ -39,8 +39,9 @@ export interface StackArgsEscape {
39
39
  /** A call, and what its callee's DECLARATION says it takes on the stack. `declared` is the block
40
40
  * `[0, 4*(n - argRegs))` as an ascending offset list, or null when nothing declares this call —
41
41
  * an indirect call, a callee with no prototype, or one whose arity fits in registers. The code
42
- * alone cannot say where a null declaration's block ENDS, so such a call is never handed a stack
43
- * word; it lifts only when `loadedWordBound` clears every word that could open one. */
42
+ * alone cannot say where a null declaration's block ENDS, so such a call is handed a stack word
43
+ * only where its callee's body says where it starts (`bodyReads`); otherwise it lifts only when
44
+ * `loadedWordBound` clears every word that could open one. */
44
45
  export interface StackArgsCall<C> {
45
46
  readonly kind: 'call';
46
47
  /** The frontend's own handle for this call — the key of `OutgoingArgs.blocks`. */
@@ -51,6 +52,11 @@ export interface StackArgsCall<C> {
51
52
  /** The parameters the declaration lists, for the refusal messages — fewer than its words when a
52
53
  * 64-bit parameter takes two or a hidden struct-return pointer takes one. Absent, one per word. */
53
54
  readonly params?: number;
55
+ /** Where nothing declares the call: one past the highest word of its incoming stack area the
56
+ * callee's own body is proven to load (frontend/callee-body.ts `CalleeStackWords`), a LOWER bound
57
+ * on its stack words — asked, since it may lift the callee, only where [sp,#0] is stored and never
58
+ * loaded (`analyzeOutgoingArgs`). Absent, or answering 0 or nothing, the body proves nothing. */
59
+ readonly bodyReads?: () => number | undefined;
54
60
  }
55
61
 
56
62
  /** One basic block's events. Where control goes afterwards is read out of `preds`, so a block is
@@ -209,8 +215,33 @@ export function loadedWordBound<C>(
209
215
  //
210
216
  // A LICENSED CALL CONSUMES ITS BLOCK, which is what lets a function make several calls: the
211
217
  // callee reads those words, so they stop being pending after it, exactly as a reload would end
212
- // them. The deletion is driven by the DECLARATION alone, never by the licence, so the fixpoint
213
- // cannot depend on its own outcome.
218
+ // them. The deletion is driven by the DECLARATION (or the body's reads, below) alone, never by the
219
+ // licence, so the fixpoint cannot depend on its own outcome.
220
+ //
221
+ // THE CALLEE'S BODY STANDS IN FOR A MISSING DECLARATION, on one side only. A compiled callee loads
222
+ // incoming stack word k only as its argument |argRegs|+k, so a body proven to load word n-1
223
+ // (`bodyReads` n) takes at least n stack words — a LOWER bound, as a declaration is, and never an
224
+ // upper one: a parameter it does not load, or one it loads past the straight-line entry it was
225
+ // scanned over, is a word it takes all the same. The CODE closes the block from above: the call is
226
+ // licensed only where what reaches it staged, on every path (must) and on some path (may, beneath
227
+ // the loaded-word bound), is exactly `[0, 4n)` — by CONTIGUITY a conforming caller stores every
228
+ // argument word of a call, so a word past the body's reads that it does not stage is no argument.
229
+ // Anything else is the call it was before the witness existed: unlicensed, and refused by (a) or
230
+ // (b) wherever they refused it — so the witness only ever turns a decline into a lift, and only at
231
+ // a call [sp,#0] reached staged, which (b) refused.
232
+ /** THE BODY'S LICENCE, the one rule both ISAs that stage stack arguments decide by (this file's
233
+ * dataflow for Thumb, frontend/mips.ts `lowerJal` for O32 over its SSA reaching definitions): a
234
+ * call nothing declares, whose callee's body loads `n` incoming stack words, takes exactly those
235
+ * `n` where the words staged for it are exactly `[0, 4n)` from the start of the outgoing area —
236
+ * `may` the words that reach it pending on SOME path (beneath the loaded-word bound), and `must`
237
+ * whether a word reaches it pending on EVERY path. Pending means stored and not consumed since: a
238
+ * load, or an earlier call whose block held the word, ends it. Offsets are relative to the area's
239
+ * start (0 on agbcc, 16 on O32). */
240
+ export function bodyBlockAgrees(n: number, may: Iterable<number>, must: (off: number) => boolean): boolean {
241
+ const staged = [...may].sort((x, y) => x - y);
242
+ return n > 0 && staged.length === n && staged.every((o, i) => o === 4 * i && must(o));
243
+ }
244
+
214
245
  export function analyzeOutgoingArgs<C>({
215
246
  blocks: asmBlocks,
216
247
  preds,
@@ -221,6 +252,26 @@ export function analyzeOutgoingArgs<C>({
221
252
  loadedWordBound: bound,
222
253
  }: StackArgsInput<C>): OutgoingArgs<C> {
223
254
  const refuse = (blocker: string): OutgoingArgs<C> => ({ blocker, blocks: new Map(), area: 0 });
255
+ // the block the callee's body proves it reads into, for an undeclared call (`bodyReads`), asked
256
+ // only where [sp,#0] is stored and never loaded: every block a licence names starts there, and
257
+ // a load of it puts the loaded-word bound at 0, so no word reaches any call beneath it. A fact of
258
+ // the code, not of the dataflow, so the fixpoint below never decides which calls ask.
259
+ const events = asmBlocks.flatMap((ab) => ab.events);
260
+ const asksBodies =
261
+ events.some((e) => e.kind === 'store' && e.off === 0) && !events.some((e) => e.kind === 'load' && e.off === 0);
262
+ const bodyWords = new Map<StackArgsCall<C>, number[]>();
263
+ const bodyBlock = (ev: StackArgsCall<C>): number[] => {
264
+ if (ev.declared !== null || ev.bodyReads === undefined || !asksBodies) {
265
+ return [];
266
+ }
267
+ if (!bodyWords.has(ev)) {
268
+ bodyWords.set(
269
+ ev,
270
+ Array.from({ length: ev.bodyReads() ?? 0 }, (_, i) => 4 * i),
271
+ );
272
+ }
273
+ return bodyWords.get(ev)!;
274
+ };
224
275
  // EVERY block, not the entry-reachable ones: a call in dead code stages nothing, so the
225
276
  // dataflow below never finds its block and it refuses — which is the verdict it had before
226
277
  // consumption existed. An unreachable `bl` is not evidence about the frame either way, and
@@ -235,6 +286,7 @@ export function analyzeOutgoingArgs<C>({
235
286
  return { blocker: null, blocks: new Map(), area: 0 };
236
287
  }
237
288
  const say = (offs: readonly number[]) => offs.map((o) => `[sp,#${o}]`).join(', ');
289
+ const sayAll = (offs: readonly number[]) => (offs.length === 0 ? 'none' : say(offs));
238
290
  const declaredAs = (ev: StackArgsCall<C>, offs: readonly number[]) => {
239
291
  const words = argRegs + offs.length;
240
292
  return ev.params === undefined || ev.params === words
@@ -331,7 +383,7 @@ export function analyzeOutgoingArgs<C>({
331
383
  mayAt.set(ev, belowBound(may));
332
384
  mustAt.set(ev, new Set(must));
333
385
  storedAt.set(ev, new Set(stored));
334
- for (const o of ev.declared ?? []) {
386
+ for (const o of ev.declared ?? bodyBlock(ev)) {
335
387
  may.delete(o);
336
388
  must.delete(o);
337
389
  }
@@ -392,6 +444,13 @@ export function analyzeOutgoingArgs<C>({
392
444
  for (const ev of calls) {
393
445
  const offs = ev.declared;
394
446
  if (offs === null) {
447
+ // the body's witness: licensed on exact agreement, else left to (a) and (b) as before
448
+ const body = bodyBlock(ev);
449
+ const must = mustAt.get(ev) ?? new Set<number>();
450
+ if (bodyBlockAgrees(body.length, mayAt.get(ev) ?? [], (o) => must.has(o))) {
451
+ blocks.set(ev.call, body);
452
+ area = Math.max(area, 4 * body.length);
453
+ }
395
454
  continue;
396
455
  }
397
456
  const may = mayAt.get(ev) ?? new Set<number>();
@@ -429,7 +488,7 @@ export function analyzeOutgoingArgs<C>({
429
488
  const clash = offs.find((o) => o >= lowestEscape);
430
489
  if (clash !== undefined) {
431
490
  return refuse(
432
- `callee \`${ev.callee}\` is declared with ${declaredAs(ev, offs)}, so [sp,#${clash}] is its outgoing stack argument — ` +
491
+ `callee \`${ev.callee}\` ${ev.declared === null ? `loads ${offs.length} incoming stack words in its body` : `is declared with ${declaredAs(ev, offs)}`}, so [sp,#${clash}] is its outgoing stack argument — ` +
433
492
  `but the frame address [sp,#${lowestEscape}] escapes this function, so the words from there up are an object's, and the two name the same word`,
434
493
  );
435
494
  }
@@ -481,6 +540,33 @@ export function analyzeOutgoingArgs<C>({
481
540
  );
482
541
  }
483
542
  }
543
+ // (b)'s question, asked of one call (see (b) below): the refusal, or null where nothing staged
544
+ // there could open an argument block. A call the body's witness was asked of and did not license
545
+ // is asked it BEFORE (a), so its decline names both witnesses; (a) would refuse it anyway, or (b)
546
+ // would, so only the message moves.
547
+ const pendingAtUndeclared = (ev: StackArgsCall<C>): string | null => {
548
+ if (ev.declared !== null || blocks.has(ev.call)) {
549
+ return null;
550
+ }
551
+ const may = mayAt.get(ev) ?? new Set<number>();
552
+ const stored = storedAt.get(ev) ?? new Set<number>();
553
+ const body = bodyBlock(ev);
554
+ const why =
555
+ body.length === 0
556
+ ? ''
557
+ : ` (its body loads ${body.length} incoming stack word${body.length === 1 ? '' : 's'}, ${say(body)}, ` +
558
+ `but the words staged for it are ${sayAll(asc(may))}, on every path ${sayAll(asc(mustAt.get(ev) ?? []))})`;
559
+ const k = asc(may).find((o) => prefixStored(o, stored));
560
+ return k === undefined
561
+ ? null
562
+ : `the store to [sp,#${k}] reaches \`bl ${ev.callee}\` unread with its lower slots supplied — it may be that call's outgoing stack argument${why}`;
563
+ };
564
+ for (const ev of calls) {
565
+ const refusal = bodyBlock(ev).length > 0 ? pendingAtUndeclared(ev) : null;
566
+ if (refusal !== null) {
567
+ return refuse(refusal);
568
+ }
569
+ }
484
570
  // (a) — a store never reloaded ANYWHERE, with its lower slots supplied, is an argument's
485
571
  // signature: an outgoing argument is read by the CALLEE, never by the caller. Its real theorem
486
572
  // is the layout one (the area sits at the BOTTOM of localArea, disjoint from the locals, so no
@@ -504,17 +590,9 @@ export function analyzeOutgoingArgs<C>({
504
590
  // histogram keys on, and scanning a Set in insertion order named whichever offset the code stored
505
591
  // FIRST instead (pokeemerald's `PickLotteryCornerTicket` stores [sp,#4] before [sp,#0]).
506
592
  for (const ev of calls) {
507
- if (ev.declared !== null) {
508
- continue;
509
- }
510
- const may = mayAt.get(ev) ?? new Set<number>();
511
- const stored = storedAt.get(ev) ?? new Set<number>();
512
- for (const k of asc(may)) {
513
- if (prefixStored(k, stored)) {
514
- return refuse(
515
- `the store to [sp,#${k}] reaches \`bl ${ev.callee}\` unread with its lower slots supplied — it may be that call's outgoing stack argument`,
516
- );
517
- }
593
+ const refusal = pendingAtUndeclared(ev);
594
+ if (refusal !== null) {
595
+ return refuse(refusal);
518
596
  }
519
597
  }
520
598
  // NOTHING LEFT OVER. The exclusion above is per OFFSET, so it would also excuse a store to a