@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
@@ -15,7 +15,7 @@
15
15
  * that matter — a sub-word or register-offset access is not a slot, and one anywhere turns the
16
16
  * whole word-slot model off at the same gate this analysis's `blocker` feeds, so no access that
17
17
  * could alias a licensed word is ever silently missing from the events. */
18
- export type StackArgsEvent<C> = StackArgsSlot | StackArgsCall<C>;
18
+ export type StackArgsEvent<C> = StackArgsSlot | StackArgsCall<C> | StackArgsEscape;
19
19
 
20
20
  /** A whole-word access to a reserved frame slot at `off`, which must lie inside `[0, localArea)`. */
21
21
  export interface StackArgsSlot {
@@ -23,11 +23,25 @@ export interface StackArgsSlot {
23
23
  readonly off: number;
24
24
  }
25
25
 
26
+ /** A frame address `sp + off` LEAVING this function — in an argument register at a call, or stored
27
+ * to memory a device or another function reads. Its receiver may read the frame from `off` up, so
28
+ * a word there that this function stored and never reloads has been read, by the receiver.
29
+ *
30
+ * Emitted only under `compilerBehaviors.escapedFrameAddressNamesALocal`: that premise is what
31
+ * makes the words from `off` up a local's rather than an argument area's. It ends only what is
32
+ * pending WHERE it fires, never a word staged after it, and the field says why that is the safe
33
+ * reading of a whole-function layout fact (a block copy's base escapes too). */
34
+ export interface StackArgsEscape {
35
+ readonly kind: 'escape';
36
+ readonly off: number;
37
+ }
38
+
26
39
  /** A call, and what its callee's DECLARATION says it takes on the stack. `declared` is the block
27
40
  * `[0, 4*(n - argRegs))` as an ascending offset list, or null when nothing declares this call —
28
41
  * an indirect call, a callee with no prototype, or one whose arity fits in registers. The code
29
- * alone cannot say where a null declaration's block ENDS, so such a call is never handed a stack
30
- * word; it lifts only when `survivorBound` 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. */
31
45
  export interface StackArgsCall<C> {
32
46
  readonly kind: 'call';
33
47
  /** The frontend's own handle for this call — the key of `OutgoingArgs.blocks`. */
@@ -38,6 +52,11 @@ export interface StackArgsCall<C> {
38
52
  /** The parameters the declaration lists, for the refusal messages — fewer than its words when a
39
53
  * 64-bit parameter takes two or a hidden struct-return pointer takes one. Absent, one per word. */
40
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;
41
60
  }
42
61
 
43
62
  /** One basic block's events. Where control goes afterwards is read out of `preds`, so a block is
@@ -60,10 +79,12 @@ export interface StackArgsInput<C> {
60
79
  readonly argRegs: number;
61
80
  /** The frontend's verdict that the whole frame is one addressable object handed to a callee. */
62
81
  readonly capturedWholeFrame: boolean;
63
- /** The compiler keeps ONE outgoing area at the bottom of the frame, sized for its widest call and
64
- * below every local, and never reads a word of it back after a call. Only then does a word
65
- * loaded after a call prove itself a local — see `survivorBound`. */
66
- readonly localsAboveOutgoingArea: boolean;
82
+ /** The lowest frame word this function loads where its compiler loads no word of the outgoing
83
+ * area, before a call or after (`target.ts` `outgoingAreaNeverLoadedFrom`, the strong form of
84
+ * `localsAboveOutgoingArea`, 0 on agbcc): `loadedWordBound` over these blocks, which the frontend
85
+ * computes once and hands the frame-object audit too. Every word at or above it is a local's.
86
+ * `Infinity` where the compiler claims no such offset. */
87
+ readonly loadedWordBound: number;
67
88
  }
68
89
 
69
90
  /** What this function's calls do with the BOTTOM of its frame — the outgoing stack-argument area
@@ -82,45 +103,35 @@ export interface OutgoingArgs<C> {
82
103
  }
83
104
 
84
105
  const isCallEvent = <C>(ev: StackArgsEvent<C>): ev is StackArgsCall<C> => ev.kind === 'call';
106
+ const isSlotEvent = <C>(ev: StackArgsEvent<C>): ev is StackArgsSlot => ev.kind === 'store' || ev.kind === 'load';
85
107
 
86
- /** THE LOWEST FRAME WORD THAT SURVIVES A CALL, or `Infinity` when none does. A word LOADED on some
87
- * path out of a live call before anything re-stores it held a value across that call. Under
88
- * `localsAboveOutgoingArea` the caller never reads its outgoing area back after a call, so that
89
- * word is a local; and since the area is one region at the frame bottom that every call shares,
90
- * every word at or above the bound is proven not to be an argument.
108
+ /** THE LOWEST FRAME WORD THIS FUNCTION LOADS AT OR ABOVE `from`, or `Infinity` when it loads none
109
+ * or nothing states `from`. The Thumb frontend calls it once per lift and hands the number to both
110
+ * readers: `analyzeOutgoingArgs` and the frame-object audit (`FrameObjectAudit.loadedWords`).
111
+ * Under `outgoingAreaNeverLoadedFrom` the caller never reads a word of its outgoing area from that
112
+ * offset up — not after a call, where the callee may have assigned to it, and not before one
113
+ * either: the area holds only what the caller staged for a callee, every argument that contains a
114
+ * call is computed before any word is staged, and nothing is saved there around a nested call
115
+ * (agbcc-restage.s pins all three). So a word it loads is a local; and since the area is one
116
+ * region at the frame bottom that every call shares, every word at or above the bound is proven
117
+ * not to be an argument.
91
118
  *
92
- * A backward liveness over the live blocks: a load makes its offset live, a store kills it, and a
93
- * call is transparent — a word read after two calls survived both. Dead blocks contribute nothing:
94
- * a load that cannot run proves nothing, and a call that cannot run is not a call it survived. */
95
- function survivorBound<C>(
119
+ * Only a load in a live block counts: a load that cannot run proves nothing. An ESCAPE does not
120
+ * count: its receiver MAY read the words, and a bound is a proof, which a read that may not happen
121
+ * cannot supply. */
122
+ export function loadedWordBound<C>(
96
123
  blocks: readonly StackArgsBlock<C>[],
97
- succs: readonly (readonly number[])[],
98
124
  live: ReadonlySet<number>,
125
+ from: number | undefined,
99
126
  ): number {
100
- const liveIn = blocks.map(() => new Set<number>());
101
- const liveOut = (b: number): Set<number> => new Set(succs[b].flatMap((s) => [...liveIn[s]]));
127
+ if (from === undefined) {
128
+ return Infinity;
129
+ }
102
130
  let bound = Infinity;
103
- for (let changed = true; changed;) {
104
- changed = false;
105
- for (let b = blocks.length - 1; b >= 0; b--) {
106
- if (!live.has(b)) {
107
- continue;
108
- }
109
- const cur = liveOut(b);
110
- for (const ev of [...blocks[b].events].reverse()) {
111
- if (isCallEvent(ev)) {
112
- for (const o of cur) {
113
- bound = Math.min(bound, o);
114
- }
115
- } else if (ev.kind === 'load') {
116
- cur.add(ev.off);
117
- } else {
118
- cur.delete(ev.off);
119
- }
120
- }
121
- if ([...cur].some((o) => !liveIn[b].has(o))) {
122
- liveIn[b] = cur;
123
- changed = true;
131
+ for (const b of live) {
132
+ for (const ev of blocks[b].events) {
133
+ if (ev.kind === 'load' && ev.off >= from) {
134
+ bound = Math.min(bound, ev.off);
124
135
  }
125
136
  }
126
137
  }
@@ -164,14 +175,14 @@ function survivorBound<C>(
164
175
  // and (b) below, kept for every call no declaration covers).
165
176
  //
166
177
  // A THIRD WITNESS SAYS WHERE THE AREA ENDS, and it only ever clears words. Under a compiler that
167
- // keeps one outgoing area below every local and never reads it back after a call
168
- // (`localsAboveOutgoingArea`), a word LOADED after a call before any re-store is a local, and so
169
- // is every word above it (`survivorBound`). Every refusal that reads the code — NOTHING EXTRA,
170
- // (a) and (b) — then weighs only the words beneath the bound, so an undeclared call lifts once no
171
- // word there could open an argument block, and takes no stack word. Hand-written asm that reads
172
- // its own outgoing argument back after a call defeats it, and lifts with that argument dropped — a
173
- // producer assumption of the same kind as CONTIGUITY below, which is why the premise is declared
174
- // per compiler (`target.ts` `localsAboveOutgoingArea`) and never inferred.
178
+ // keeps one outgoing area below every local and never loads a word of it
179
+ // (`outgoingAreaNeverLoadedFrom`), a word this function LOADS is a local, and so is every word above it
180
+ // (`loadedWordBound`). Every refusal that reads the code — NOTHING EXTRA, (a) and (b) — then
181
+ // weighs only the words beneath the bound, so an undeclared call lifts once no word there could
182
+ // open an argument block, and takes no stack word. Hand-written asm that reads its own outgoing
183
+ // argument defeats it, and lifts with that argument dropped — a producer assumption of the same
184
+ // kind as CONTIGUITY below, which is why the premise is declared per compiler (`target.ts`
185
+ // `outgoingAreaNeverLoadedFrom`) and never inferred.
175
186
  //
176
187
  // THE TWO SIDES ARE CHECKED AGAINST DIFFERENT SETS, and the asymmetry is the point.
177
188
  // * NOTHING EXTRA is checked against the MAY set (stored and unreloaded on SOME path): the
@@ -183,12 +194,12 @@ function survivorBound<C>(
183
194
  // must never render as a plausible value.
184
195
  //
185
196
  // A SPILL LIVE ACROSS A LICENSED CALL sits in the may set and is not in the declared block — agbcc's
186
- // commonest frame with an outgoing area. The declaration cannot excuse it; `survivorBound` can: a
187
- // word loaded after a call before any re-store is a local, so NOTHING EXTRA counts only the pending
188
- // words beneath the bound. A declared block that reaches the bound contradicts it, and needs no
189
- // check of its own: the block is contiguous words from zero, so it holds the bound's word, which
190
- // this function loads — "this function also LOADS it" below refuses it if nothing earlier does.
191
- // A spill with no survivor at or beneath it still refuses, and its decline names a STORE ("[sp,#k]
197
+ // commonest frame with an outgoing area. The declaration cannot excuse it; `loadedWordBound` can: a
198
+ // word this function loads is a local, so NOTHING EXTRA counts only the pending words beneath the
199
+ // bound. A declared block that reaches the bound contradicts it, and needs no check of its own:
200
+ // the block is contiguous words from zero, so it holds the bound's word, which this function loads
201
+ // — "this function also LOADS it" below refuses it if nothing earlier does.
202
+ // A spill with no load at or beneath it still refuses, and its decline names a STORE ("[sp,#k]
192
203
  // also reaches the call unread") rather than the capability, so a gap histogram groups it under
193
204
  // that message.
194
205
  // The must set is an intersection over predecessors, which is exactly what a TAIL-MERGED call
@@ -204,8 +215,33 @@ function survivorBound<C>(
204
215
  //
205
216
  // A LICENSED CALL CONSUMES ITS BLOCK, which is what lets a function make several calls: the
206
217
  // callee reads those words, so they stop being pending after it, exactly as a reload would end
207
- // them. The deletion is driven by the DECLARATION alone, never by the licence, so the fixpoint
208
- // 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
+
209
245
  export function analyzeOutgoingArgs<C>({
210
246
  blocks: asmBlocks,
211
247
  preds,
@@ -213,9 +249,29 @@ export function analyzeOutgoingArgs<C>({
213
249
  localArea,
214
250
  argRegs,
215
251
  capturedWholeFrame,
216
- localsAboveOutgoingArea,
252
+ loadedWordBound: bound,
217
253
  }: StackArgsInput<C>): OutgoingArgs<C> {
218
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
+ };
219
275
  // EVERY block, not the entry-reachable ones: a call in dead code stages nothing, so the
220
276
  // dataflow below never finds its block and it refuses — which is the verdict it had before
221
277
  // consumption existed. An unreachable `bl` is not evidence about the frame either way, and
@@ -230,6 +286,7 @@ export function analyzeOutgoingArgs<C>({
230
286
  return { blocker: null, blocks: new Map(), area: 0 };
231
287
  }
232
288
  const say = (offs: readonly number[]) => offs.map((o) => `[sp,#${o}]`).join(', ');
289
+ const sayAll = (offs: readonly number[]) => (offs.length === 0 ? 'none' : say(offs));
233
290
  const declaredAs = (ev: StackArgsCall<C>, offs: readonly number[]) => {
234
291
  const words = argRegs + offs.length;
235
292
  return ev.params === undefined || ev.params === words
@@ -242,6 +299,9 @@ export function analyzeOutgoingArgs<C>({
242
299
  // that computes it owns which escapes qualify. A declared fifth argument says [sp,#0] is a
243
300
  // DIFFERENT callee's argument slot. Two contradictory claims about the same word, and nothing
244
301
  // here can decide which to believe, so the honest answer is the decline it has always been.
302
+ // This is the flow-INSENSITIVE reading of an escape, sound only at one word; the `escape` event
303
+ // is the per-program-point reading of the same escapes at any frame size, and its contradiction
304
+ // with a declared block is the "ESCAPED ADDRESS AT OR BELOW A LICENSED WORD" refusal below.
245
305
  if (capturedWholeFrame) {
246
306
  for (const ev of calls) {
247
307
  const offs = ev.declared;
@@ -272,7 +332,6 @@ export function analyzeOutgoingArgs<C>({
272
332
  // code-reading refusals below — the licence's NOTHING EXTRA, (a) and (b) — each hunt for a word
273
333
  // that could be one, so the words they weigh are cut to those beneath the bound once, where
274
334
  // they are collected (`mayAt`, `storedAnywhere`), and none of them repeats the test.
275
- const bound = localsAboveOutgoingArea ? survivorBound(asmBlocks, succs, live) : Infinity;
276
335
  const belowBound = (s: Iterable<number>): Set<number> => new Set([...s].filter((o) => o < bound));
277
336
 
278
337
  const everySlot: number[] = [];
@@ -324,7 +383,7 @@ export function analyzeOutgoingArgs<C>({
324
383
  mayAt.set(ev, belowBound(may));
325
384
  mustAt.set(ev, new Set(must));
326
385
  storedAt.set(ev, new Set(stored));
327
- for (const o of ev.declared ?? []) {
386
+ for (const o of ev.declared ?? bodyBlock(ev)) {
328
387
  may.delete(o);
329
388
  must.delete(o);
330
389
  }
@@ -332,9 +391,18 @@ export function analyzeOutgoingArgs<C>({
332
391
  may.add(ev.off);
333
392
  must.add(ev.off);
334
393
  stored.add(ev.off);
335
- } else {
394
+ } else if (ev.kind === 'load') {
336
395
  may.delete(ev.off);
337
396
  must.delete(ev.off);
397
+ } else {
398
+ // the receiver of the escaped address reads what it was handed, so those words stop
399
+ // being pending exactly as a reload would end them
400
+ for (const o of everySlot) {
401
+ if (o >= ev.off) {
402
+ may.delete(o);
403
+ must.delete(o);
404
+ }
405
+ }
338
406
  }
339
407
  }
340
408
  const grow = (out: Array<Set<number>>, cur: Set<number>): void => {
@@ -376,6 +444,13 @@ export function analyzeOutgoingArgs<C>({
376
444
  for (const ev of calls) {
377
445
  const offs = ev.declared;
378
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
+ }
379
454
  continue;
380
455
  }
381
456
  const may = mayAt.get(ev) ?? new Set<number>();
@@ -400,6 +475,24 @@ export function analyzeOutgoingArgs<C>({
400
475
  licensed.add(o);
401
476
  }
402
477
  }
478
+ // An ESCAPED ADDRESS AT OR BELOW A LICENSED WORD — the one-word captured frame's contradiction,
479
+ // at any frame size. The escape names the words from its offset up as an object's, a licensed
480
+ // block names one of them as a callee's argument, and nothing here can decide which to believe.
481
+ // Under agbcc the two never meet (an address-taken local sits ABOVE the outgoing area, and at
482
+ // [sp,#0] only when no call takes a stack word), so a function that has both is not agbcc's
483
+ // layout and declines.
484
+ const escapes = [...live].flatMap((b) => asmBlocks[b].events.flatMap((ev) => (ev.kind === 'escape' ? [ev.off] : [])));
485
+ const lowestEscape = Math.min(Infinity, ...escapes);
486
+ for (const ev of calls) {
487
+ const offs = blocks.get(ev.call) ?? [];
488
+ const clash = offs.find((o) => o >= lowestEscape);
489
+ if (clash !== undefined) {
490
+ return refuse(
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 — ` +
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`,
493
+ );
494
+ }
495
+ }
403
496
  // The two whole-function facts the next two refusals read: every offset live code stores, and
404
497
  // every offset live code loads back. A reload in dead code is not evidence that anything reads
405
498
  // the slot back, so it does not count.
@@ -407,11 +500,26 @@ export function analyzeOutgoingArgs<C>({
407
500
  const stores = new Set<number>();
408
501
  for (const b of live) {
409
502
  for (const ev of asmBlocks[b].events) {
410
- if (ev.kind !== 'call') {
503
+ if (isSlotEvent(ev)) {
411
504
  (ev.kind === 'store' ? stores : reloaded).add(ev.off);
412
505
  }
413
506
  }
414
507
  }
508
+ // …and a word an escaped address hands over is read by its receiver, which is a reload for (a):
509
+ // its never being read back HERE is explained by who reads it THERE.
510
+ //
511
+ // WHOLE-FUNCTION, unlike the pending sets above: a store after the last escape, or on a path
512
+ // that never escapes, is excused here too. That is sound because (a) is not what clears a word
513
+ // for a call. A word that is some call's argument reaches that call pending, and the licence's
514
+ // equality (a declared callee) or (b) (an undeclared one) refuses it there, per program point:
515
+ // `qh5`'s fifth `h5` word, staged after its memcpy base escapes, is refused by (b). What this
516
+ // widening lets through is a store that reaches no call pending with its lower slots supplied,
517
+ // which by CONTIGUITY (above) no call takes as an argument.
518
+ for (const o of everySlot) {
519
+ if (o >= lowestEscape) {
520
+ reloaded.add(o);
521
+ }
522
+ }
415
523
  const storedAnywhere = belowBound(stores);
416
524
  // A LICENSED WORD THIS FUNCTION ALSO LOADS. The area belongs to the CALLEE — which may assign
417
525
  // to a stack parameter — so after the `bl` the word holds whatever the callee left, and an
@@ -432,6 +540,33 @@ export function analyzeOutgoingArgs<C>({
432
540
  );
433
541
  }
434
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
+ }
435
570
  // (a) — a store never reloaded ANYWHERE, with its lower slots supplied, is an argument's
436
571
  // signature: an outgoing argument is read by the CALLEE, never by the caller. Its real theorem
437
572
  // is the layout one (the area sits at the BOTTOM of localArea, disjoint from the locals, so no
@@ -455,17 +590,9 @@ export function analyzeOutgoingArgs<C>({
455
590
  // histogram keys on, and scanning a Set in insertion order named whichever offset the code stored
456
591
  // FIRST instead (pokeemerald's `PickLotteryCornerTicket` stores [sp,#4] before [sp,#0]).
457
592
  for (const ev of calls) {
458
- if (ev.declared !== null) {
459
- continue;
460
- }
461
- const may = mayAt.get(ev) ?? new Set<number>();
462
- const stored = storedAt.get(ev) ?? new Set<number>();
463
- for (const k of asc(may)) {
464
- if (prefixStored(k, stored)) {
465
- return refuse(
466
- `the store to [sp,#${k}] reaches \`bl ${ev.callee}\` unread with its lower slots supplied — it may be that call's outgoing stack argument`,
467
- );
468
- }
593
+ const refusal = pendingAtUndeclared(ev);
594
+ if (refusal !== null) {
595
+ return refuse(refusal);
469
596
  }
470
597
  }
471
598
  // NOTHING LEFT OVER. The exclusion above is per OFFSET, so it would also excuse a store to a