@asmlift/core 0.8.1 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (130) hide show
  1. package/README.md +36 -31
  2. package/package.json +1 -1
  3. package/src/aggregate.ts +397 -0
  4. package/src/backend/c.ts +6 -2
  5. package/src/backend/cfamily.ts +370 -56
  6. package/src/backend/cpp.ts +116 -10
  7. package/src/backend/pascal.ts +69 -11
  8. package/src/cdecl/constant.ts +59 -0
  9. package/src/cdecl/lex.ts +209 -0
  10. package/src/cdecl/parse.ts +975 -0
  11. package/src/cdecl/spell.ts +69 -0
  12. package/src/codegen-flags.ts +38 -6
  13. package/src/compiler-diagnostics.ts +194 -0
  14. package/src/contracts.ts +777 -84
  15. package/src/declare.ts +132 -13
  16. package/src/frontend/asmdata.ts +26 -6
  17. package/src/frontend/call-plan.ts +1590 -0
  18. package/src/frontend/dead-reads.ts +628 -0
  19. package/src/frontend/device-pins.ts +296 -0
  20. package/src/frontend/emit.ts +4 -3
  21. package/src/frontend/errors.ts +5 -0
  22. package/src/frontend/flags-edge.ts +101 -0
  23. package/src/frontend/fpu.ts +374 -0
  24. package/src/frontend/frame-objects.ts +3171 -0
  25. package/src/frontend/frontend.ts +64 -3
  26. package/src/frontend/high-half.ts +55 -5
  27. package/src/frontend/jump-table.ts +530 -0
  28. package/src/frontend/local-object.ts +616 -0
  29. package/src/frontend/mips.ts +2392 -208
  30. package/src/frontend/opaque.ts +97 -18
  31. package/src/frontend/own-struct-return.ts +500 -0
  32. package/src/frontend/ppc.ts +3137 -267
  33. package/src/frontend/register-struct-call.ts +484 -0
  34. package/src/frontend/reloc-symbol.ts +109 -13
  35. package/src/frontend/splat.ts +108 -5
  36. package/src/frontend/ssa.ts +2098 -89
  37. package/src/frontend/stackargs.ts +172 -42
  38. package/src/frontend/thumb.ts +4563 -2091
  39. package/src/ir/alias.ts +71 -0
  40. package/src/ir/bits.ts +681 -1
  41. package/src/ir/core.ts +199 -6
  42. package/src/ir/discipline.ts +313 -0
  43. package/src/ir/entry.ts +159 -0
  44. package/src/ir/float-bits.ts +47 -0
  45. package/src/{structure → ir}/loops.ts +6 -2
  46. package/src/ir/opcodes.ts +193 -121
  47. package/src/ir/parse.ts +1 -0
  48. package/src/ir/types.ts +202 -2
  49. package/src/ir/verify.ts +219 -3
  50. package/src/l3/address.ts +5 -0
  51. package/src/l3/argcopy.ts +38 -6
  52. package/src/l3/ast.ts +467 -23
  53. package/src/l3/basecse.ts +33 -5
  54. package/src/l3/coalesce.ts +21 -13
  55. package/src/l3/constinit.ts +382 -0
  56. package/src/l3/dce.ts +34 -12
  57. package/src/l3/gates.ts +9 -3
  58. package/src/l3/hoist.ts +8 -7
  59. package/src/l3/initfirst.ts +68 -19
  60. package/src/l3/intcell.ts +98 -0
  61. package/src/l3/mentions.ts +10 -4
  62. package/src/l3/narrowdecl.ts +259 -0
  63. package/src/l3/nearbase.ts +10 -5
  64. package/src/l3/parkfirst.ts +8 -5
  65. package/src/l3/pollguard.ts +11 -4
  66. package/src/l3/ptrcell.ts +354 -0
  67. package/src/l3/ptrfield.ts +123 -8
  68. package/src/l3/regspell.ts +26 -7
  69. package/src/l3/reindex.ts +293 -103
  70. package/src/l3/storage.ts +98 -1
  71. package/src/l3/symbol-refs.ts +182 -23
  72. package/src/l3/tailmerge.ts +9 -20
  73. package/src/l3/typing.ts +237 -32
  74. package/src/l3/unreduce.ts +10 -9
  75. package/src/l3/volatileptr.ts +17 -54
  76. package/src/l3/volstore.ts +96 -7
  77. package/src/mangle.ts +41 -1
  78. package/src/pattern/engine.ts +373 -82
  79. package/src/pipeline.ts +589 -27
  80. package/src/proto-context.ts +1648 -0
  81. package/src/proto.ts +1203 -49
  82. package/src/raise/aggmembers.ts +290 -0
  83. package/src/raise/arrays.ts +8 -3
  84. package/src/raise/declared-volatile.ts +307 -0
  85. package/src/raise/divpow2.ts +2 -2
  86. package/src/raise/errors.ts +17 -0
  87. package/src/raise/floathelpers.ts +464 -0
  88. package/src/raise/globalshape.ts +11 -5
  89. package/src/raise/gvn.ts +29 -8
  90. package/src/raise/latch.ts +162 -6
  91. package/src/raise/memberarrays.ts +10 -4
  92. package/src/raise/narrow.ts +29 -2
  93. package/src/raise/narrowlocal.ts +11 -6
  94. package/src/raise/offsetnames.ts +494 -0
  95. package/src/raise/pairparams.ts +114 -0
  96. package/src/raise/paramwidth.ts +203 -6
  97. package/src/raise/pre-recovery.ts +174 -17
  98. package/src/raise/pre-structure.ts +54 -0
  99. package/src/raise/recover.ts +26 -15
  100. package/src/raise/reroll.ts +694 -0
  101. package/src/raise/retsink.ts +1 -1
  102. package/src/raise/shortcircuit.ts +66 -30
  103. package/src/raise/softdiv.ts +21 -23
  104. package/src/raise/storesign.ts +153 -0
  105. package/src/raise/struct-arrays.ts +34 -14
  106. package/src/raise/structs.ts +674 -98
  107. package/src/raise/truncload.ts +367 -0
  108. package/src/raise/unthread.ts +400 -0
  109. package/src/raise/widehelpers.ts +106 -0
  110. package/src/rank-declare.ts +143 -12
  111. package/src/rank-variations.ts +117 -31
  112. package/src/rank.ts +563 -122
  113. package/src/runtime-helpers.ts +405 -0
  114. package/src/stillborn.ts +289 -0
  115. package/src/structure/analysis.ts +1449 -449
  116. package/src/structure/arith-ops.ts +23 -0
  117. package/src/structure/globalaccess.ts +111 -9
  118. package/src/structure/hazards.ts +641 -114
  119. package/src/structure/local-statics.ts +517 -0
  120. package/src/structure/namecoalesce.ts +2 -2
  121. package/src/structure/pointer-spelling.ts +1106 -0
  122. package/src/structure/redundant-test.ts +99 -0
  123. package/src/structure/structure.ts +3478 -975
  124. package/src/structure/switch-recover.ts +403 -178
  125. package/src/symbols.ts +44 -3
  126. package/src/target.ts +1387 -71
  127. package/src/trace.ts +66 -28
  128. package/src/variation-definitions.ts +243 -13
  129. package/src/variation-gates.ts +2 -0
  130. package/src/variation-tokens.ts +25 -7
@@ -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,10 +23,24 @@ 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
- * an indirect call, a callee with no prototype, or one whose arity fits in registers. A null
29
- * declaration can only ever lead to a refusal: the code alone cannot say where a block ENDS. */
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. */
30
44
  export interface StackArgsCall<C> {
31
45
  readonly kind: 'call';
32
46
  /** The frontend's own handle for this call — the key of `OutgoingArgs.blocks`. */
@@ -34,6 +48,9 @@ export interface StackArgsCall<C> {
34
48
  /** The callee's name, for the refusal messages. `?` where the frontend has none. */
35
49
  readonly callee: string;
36
50
  readonly declared: readonly number[] | null;
51
+ /** The parameters the declaration lists, for the refusal messages — fewer than its words when a
52
+ * 64-bit parameter takes two or a hidden struct-return pointer takes one. Absent, one per word. */
53
+ readonly params?: number;
37
54
  }
38
55
 
39
56
  /** One basic block's events. Where control goes afterwards is read out of `preds`, so a block is
@@ -56,6 +73,12 @@ export interface StackArgsInput<C> {
56
73
  readonly argRegs: number;
57
74
  /** The frontend's verdict that the whole frame is one addressable object handed to a callee. */
58
75
  readonly capturedWholeFrame: boolean;
76
+ /** The lowest frame word this function loads where its compiler loads no word of the outgoing
77
+ * area, before a call or after (`target.ts` `outgoingAreaNeverLoadedFrom`, the strong form of
78
+ * `localsAboveOutgoingArea`, 0 on agbcc): `loadedWordBound` over these blocks, which the frontend
79
+ * computes once and hands the frame-object audit too. Every word at or above it is a local's.
80
+ * `Infinity` where the compiler claims no such offset. */
81
+ readonly loadedWordBound: number;
59
82
  }
60
83
 
61
84
  /** What this function's calls do with the BOTTOM of its frame — the outgoing stack-argument area
@@ -74,6 +97,40 @@ export interface OutgoingArgs<C> {
74
97
  }
75
98
 
76
99
  const isCallEvent = <C>(ev: StackArgsEvent<C>): ev is StackArgsCall<C> => ev.kind === 'call';
100
+ const isSlotEvent = <C>(ev: StackArgsEvent<C>): ev is StackArgsSlot => ev.kind === 'store' || ev.kind === 'load';
101
+
102
+ /** THE LOWEST FRAME WORD THIS FUNCTION LOADS AT OR ABOVE `from`, or `Infinity` when it loads none
103
+ * or nothing states `from`. The Thumb frontend calls it once per lift and hands the number to both
104
+ * readers: `analyzeOutgoingArgs` and the frame-object audit (`FrameObjectAudit.loadedWords`).
105
+ * Under `outgoingAreaNeverLoadedFrom` the caller never reads a word of its outgoing area from that
106
+ * offset up — not after a call, where the callee may have assigned to it, and not before one
107
+ * either: the area holds only what the caller staged for a callee, every argument that contains a
108
+ * call is computed before any word is staged, and nothing is saved there around a nested call
109
+ * (agbcc-restage.s pins all three). So a word it loads is a local; and since the area is one
110
+ * region at the frame bottom that every call shares, every word at or above the bound is proven
111
+ * not to be an argument.
112
+ *
113
+ * Only a load in a live block counts: a load that cannot run proves nothing. An ESCAPE does not
114
+ * count: its receiver MAY read the words, and a bound is a proof, which a read that may not happen
115
+ * cannot supply. */
116
+ export function loadedWordBound<C>(
117
+ blocks: readonly StackArgsBlock<C>[],
118
+ live: ReadonlySet<number>,
119
+ from: number | undefined,
120
+ ): number {
121
+ if (from === undefined) {
122
+ return Infinity;
123
+ }
124
+ let bound = Infinity;
125
+ for (const b of live) {
126
+ for (const ev of blocks[b].events) {
127
+ if (ev.kind === 'load' && ev.off >= from) {
128
+ bound = Math.min(bound, ev.off);
129
+ }
130
+ }
131
+ }
132
+ return bound;
133
+ }
77
134
 
78
135
  // THE OUTGOING STACK-ARGUMENT AREA, AND WHO MAY CONSUME IT.
79
136
  //
@@ -86,7 +143,7 @@ const isCallEvent = <C>(ev: StackArgsEvent<C>): ev is StackArgsCall<C> => ev.kin
86
143
  // CreateEntity_Platform_0_0 (platform.c:734) forwards SIX arguments and came out as
87
144
  // `CreateEntity_Platform(0, 0, a0, (u16)a1)`.
88
145
  //
89
- // TWO INDEPENDENT WITNESSES MUST AGREE, and that agreement is the whole licence:
146
+ // TWO INDEPENDENT WITNESSES MUST AGREE for a call to CONSUME words — the licence:
90
147
  // * the DECLARATION says how many words a call takes. AAPCS lays arguments 5..n at [sp,#0]
91
148
  // upward, one word each, so the block is `[0, 4*(n - |argRegs|))`, contiguous from zero.
92
149
  // * the CODE says which words are staged for it — the offsets stored and not yet reloaded when
@@ -100,15 +157,27 @@ const isCallEvent = <C>(ev: StackArgsEvent<C>): ev is StackArgsCall<C> => ev.kin
100
157
  // * a variadic callee's list is a prefix — `sprintf` truthfully declares two and is handed six,
101
158
  // * a large struct return adds a hidden pointer argument that appears in no parameter list.
102
159
  //
103
- // None of those is recorded by `FnProto` or `SymbolSignature`, so an ARITY-ONLY acceptance had
104
- // all three holes: supplying a TRUE fact (`{ sprintf: { params: 2 } }`) turned a correct decline
105
- // into `return sprintf(a0, a1)` with both stack arguments deleted. Under the rule here the four
106
- // words `sprintf` is really handed are four offsets reaching the call that its declaration does
107
- // not account for, the witnesses disagree, and the answer is the decline again. And the CODE
160
+ // A `SymbolSignature` records none of those, and neither does a COUNT. A typed `FnProto` list
161
+ // records only a `long long`, which the Thumb frontend refuses past the argument registers; a
162
+ // `double` or a by-value struct has no width there, so the list states no layout at all. An
163
+ // ARITY-ONLY acceptance therefore has all three holes: supplying a TRUE fact
164
+ // (`{ sprintf: { params: 2 } }`) turns a correct decline into `return sprintf(a0, a1)` with both
165
+ // stack arguments deleted. Under the rule here the four words `sprintf` is really handed are four
166
+ // offsets reaching the call that its declaration does not account for, the witnesses disagree, and the answer is the decline again. And the CODE
108
167
  // alone cannot say where a block ENDS — a store never reloaded is an argument's signature, but
109
- // so is a dead local, which is why reading the code alone could only ever refuse (conditions (a)
168
+ // so is a dead local, which is why reading the code alone never licenses a word (conditions (a)
110
169
  // and (b) below, kept for every call no declaration covers).
111
170
  //
171
+ // A THIRD WITNESS SAYS WHERE THE AREA ENDS, and it only ever clears words. Under a compiler that
172
+ // keeps one outgoing area below every local and never loads a word of it
173
+ // (`outgoingAreaNeverLoadedFrom`), a word this function LOADS is a local, and so is every word above it
174
+ // (`loadedWordBound`). Every refusal that reads the code — NOTHING EXTRA, (a) and (b) — then
175
+ // weighs only the words beneath the bound, so an undeclared call lifts once no word there could
176
+ // open an argument block, and takes no stack word. Hand-written asm that reads its own outgoing
177
+ // argument defeats it, and lifts with that argument dropped — a producer assumption of the same
178
+ // kind as CONTIGUITY below, which is why the premise is declared per compiler (`target.ts`
179
+ // `outgoingAreaNeverLoadedFrom`) and never inferred.
180
+ //
112
181
  // THE TWO SIDES ARE CHECKED AGAINST DIFFERENT SETS, and the asymmetry is the point.
113
182
  // * NOTHING EXTRA is checked against the MAY set (stored and unreloaded on SOME path): the
114
183
  // weakest thing that could still be a word this call takes must be inside the block.
@@ -118,14 +187,15 @@ const isCallEvent = <C>(ev: StackArgsEvent<C>): ev is StackArgsCall<C> => ev.kin
118
187
  // 0x0 in block")`; here it is a decline, and for the same reason — it is a GAP, and a gap
119
188
  // must never render as a plausible value.
120
189
  //
121
- // WHAT "NOTHING EXTRA" COSTS, because the reach is narrower than the disappearance of the old
122
- // decline suggests. A genuine SPILL that is live across a licensed call sits in the may set and is
123
- // not in the declared block, so the call refuses — and that is agbcc's commonest frame with an
124
- // outgoing area. Tolerating it means arguing that a pending word which is RELOADED later is a
125
- // local rather than argument n+1, which needs a gate and a row that gate protects; none exists.
126
- // The cost is in attribution, not correctness: the decline such a function gets names a STORE
127
- // ("[sp,#k] also reaches the call unread") rather than the capability, so a gap histogram groups
128
- // this class under that message and not under anything about stack arguments.
190
+ // A SPILL LIVE ACROSS A LICENSED CALL sits in the may set and is not in the declared block — agbcc's
191
+ // commonest frame with an outgoing area. The declaration cannot excuse it; `loadedWordBound` can: a
192
+ // word this function loads is a local, so NOTHING EXTRA counts only the pending words beneath the
193
+ // bound. A declared block that reaches the bound contradicts it, and needs no check of its own:
194
+ // the block is contiguous words from zero, so it holds the bound's word, which this function loads
195
+ // — "this function also LOADS it" below refuses it if nothing earlier does.
196
+ // A spill with no load at or beneath it still refuses, and its decline names a STORE ("[sp,#k]
197
+ // also reaches the call unread") rather than the capability, so a gap histogram groups it under
198
+ // that message.
129
199
  // The must set is an intersection over predecessors, which is exactly what a TAIL-MERGED call
130
200
  // site needs: agbcc does tail-merge (`Task_BonusFlower_Spawn`, sa3 bonus_game_enemies, stores
131
201
  // argument 5 in both predecessors with the `bl` in the join), and a one-armed store — the same
@@ -148,6 +218,7 @@ export function analyzeOutgoingArgs<C>({
148
218
  localArea,
149
219
  argRegs,
150
220
  capturedWholeFrame,
221
+ loadedWordBound: bound,
151
222
  }: StackArgsInput<C>): OutgoingArgs<C> {
152
223
  const refuse = (blocker: string): OutgoingArgs<C> => ({ blocker, blocks: new Map(), area: 0 });
153
224
  // EVERY block, not the entry-reachable ones: a call in dead code stages nothing, so the
@@ -164,19 +235,28 @@ export function analyzeOutgoingArgs<C>({
164
235
  return { blocker: null, blocks: new Map(), area: 0 };
165
236
  }
166
237
  const say = (offs: readonly number[]) => offs.map((o) => `[sp,#${o}]`).join(', ');
167
- const arityOf = (offs: readonly number[]) => argRegs + offs.length;
238
+ const declaredAs = (ev: StackArgsCall<C>, offs: readonly number[]) => {
239
+ const words = argRegs + offs.length;
240
+ return ev.params === undefined || ev.params === words
241
+ ? `${words} arguments`
242
+ : `${ev.params} arguments in ${words} argument words`;
243
+ };
168
244
 
169
- // THE ONE-WORD CAPTURED FRAME. `capturedWholeFrame` says the whole frame is an
170
- // object whose address a callee holds; a declared fifth argument says [sp,#0] is a DIFFERENT
171
- // callee's argument slot. Two contradictory claims about the same word, and nothing here can
172
- // decide which to believe, so the honest answer is the decline it has always been.
245
+ // THE ONE-WORD CAPTURED FRAME. `capturedWholeFrame` says the whole frame is an object whose
246
+ // address has ESCAPED this function — handed to a callee, or published to memory; the frontend
247
+ // that computes it owns which escapes qualify. A declared fifth argument says [sp,#0] is a
248
+ // DIFFERENT callee's argument slot. Two contradictory claims about the same word, and nothing
249
+ // here can decide which to believe, so the honest answer is the decline it has always been.
250
+ // This is the flow-INSENSITIVE reading of an escape, sound only at one word; the `escape` event
251
+ // is the per-program-point reading of the same escapes at any frame size, and its contradiction
252
+ // with a declared block is the "ESCAPED ADDRESS AT OR BELOW A LICENSED WORD" refusal below.
173
253
  if (capturedWholeFrame) {
174
254
  for (const ev of calls) {
175
255
  const offs = ev.declared;
176
256
  if (offs !== null) {
177
257
  return refuse(
178
- `callee \`${ev.callee}\` is declared with ${arityOf(offs)} arguments, so [sp,#0] is its outgoing stack argument — ` +
179
- 'but this one-word frame is an object whose address is passed to a callee, and the two name the same word',
258
+ `callee \`${ev.callee}\` is declared with ${declaredAs(ev, offs)}, so [sp,#0] is its outgoing stack argument — ` +
259
+ 'but this one-word frame is an object whose address escapes the function, and the two name the same word',
180
260
  );
181
261
  }
182
262
  }
@@ -188,6 +268,20 @@ export function analyzeOutgoingArgs<C>({
188
268
  return { blocker: null, blocks: new Map(), area: 0 };
189
269
  }
190
270
 
271
+ const succs = asmBlocks.map((): number[] => []);
272
+ for (const b of live) {
273
+ for (const q of preds[b]) {
274
+ if (live.has(q)) {
275
+ succs[q].push(b);
276
+ }
277
+ }
278
+ }
279
+ // Every word at or above this is a proven local, so it is no call's argument. The three
280
+ // code-reading refusals below — the licence's NOTHING EXTRA, (a) and (b) — each hunt for a word
281
+ // that could be one, so the words they weigh are cut to those beneath the bound once, where
282
+ // they are collected (`mayAt`, `storedAnywhere`), and none of them repeats the test.
283
+ const belowBound = (s: Iterable<number>): Set<number> => new Set([...s].filter((o) => o < bound));
284
+
191
285
  const everySlot: number[] = [];
192
286
  for (let o = 0; o + 4 <= localArea; o += 4) {
193
287
  everySlot.push(o);
@@ -234,7 +328,7 @@ export function analyzeOutgoingArgs<C>({
234
328
  );
235
329
  for (const ev of asmBlocks[b].events) {
236
330
  if (isCallEvent(ev)) {
237
- mayAt.set(ev, new Set(may));
331
+ mayAt.set(ev, belowBound(may));
238
332
  mustAt.set(ev, new Set(must));
239
333
  storedAt.set(ev, new Set(stored));
240
334
  for (const o of ev.declared ?? []) {
@@ -245,9 +339,18 @@ export function analyzeOutgoingArgs<C>({
245
339
  may.add(ev.off);
246
340
  must.add(ev.off);
247
341
  stored.add(ev.off);
248
- } else {
342
+ } else if (ev.kind === 'load') {
249
343
  may.delete(ev.off);
250
344
  must.delete(ev.off);
345
+ } else {
346
+ // the receiver of the escaped address reads what it was handed, so those words stop
347
+ // being pending exactly as a reload would end them
348
+ for (const o of everySlot) {
349
+ if (o >= ev.off) {
350
+ may.delete(o);
351
+ must.delete(o);
352
+ }
353
+ }
251
354
  }
252
355
  }
253
356
  const grow = (out: Array<Set<number>>, cur: Set<number>): void => {
@@ -297,7 +400,7 @@ export function analyzeOutgoingArgs<C>({
297
400
  const extra = asc(may).filter((o) => !offs.includes(o));
298
401
  if (missing.length > 0 || extra.length > 0) {
299
402
  return refuse(
300
- `callee \`${ev.callee}\` is declared with ${arityOf(offs)} arguments, so its outgoing stack-argument block is ${say(offs)} — but ` +
403
+ `callee \`${ev.callee}\` is declared with ${declaredAs(ev, offs)}, so its outgoing stack-argument block is ${say(offs)} — but ` +
301
404
  (missing.length > 0
302
405
  ? `${say(missing)} is not stored on every path to the call`
303
406
  : `${say(extra)} also reaches the call unread, so the declaration does not account for every word staged here`),
@@ -313,18 +416,52 @@ export function analyzeOutgoingArgs<C>({
313
416
  licensed.add(o);
314
417
  }
315
418
  }
419
+ // An ESCAPED ADDRESS AT OR BELOW A LICENSED WORD — the one-word captured frame's contradiction,
420
+ // at any frame size. The escape names the words from its offset up as an object's, a licensed
421
+ // block names one of them as a callee's argument, and nothing here can decide which to believe.
422
+ // Under agbcc the two never meet (an address-taken local sits ABOVE the outgoing area, and at
423
+ // [sp,#0] only when no call takes a stack word), so a function that has both is not agbcc's
424
+ // layout and declines.
425
+ const escapes = [...live].flatMap((b) => asmBlocks[b].events.flatMap((ev) => (ev.kind === 'escape' ? [ev.off] : [])));
426
+ const lowestEscape = Math.min(Infinity, ...escapes);
427
+ for (const ev of calls) {
428
+ const offs = blocks.get(ev.call) ?? [];
429
+ const clash = offs.find((o) => o >= lowestEscape);
430
+ if (clash !== undefined) {
431
+ return refuse(
432
+ `callee \`${ev.callee}\` is declared with ${declaredAs(ev, offs)}, so [sp,#${clash}] is its outgoing stack argument — ` +
433
+ `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
+ );
435
+ }
436
+ }
316
437
  // The two whole-function facts the next two refusals read: every offset live code stores, and
317
438
  // every offset live code loads back. A reload in dead code is not evidence that anything reads
318
439
  // the slot back, so it does not count.
319
440
  const reloaded = new Set<number>();
320
- const storedAnywhere = new Set<number>();
441
+ const stores = new Set<number>();
321
442
  for (const b of live) {
322
443
  for (const ev of asmBlocks[b].events) {
323
- if (ev.kind !== 'call') {
324
- (ev.kind === 'store' ? storedAnywhere : reloaded).add(ev.off);
444
+ if (isSlotEvent(ev)) {
445
+ (ev.kind === 'store' ? stores : reloaded).add(ev.off);
325
446
  }
326
447
  }
327
448
  }
449
+ // …and a word an escaped address hands over is read by its receiver, which is a reload for (a):
450
+ // its never being read back HERE is explained by who reads it THERE.
451
+ //
452
+ // WHOLE-FUNCTION, unlike the pending sets above: a store after the last escape, or on a path
453
+ // that never escapes, is excused here too. That is sound because (a) is not what clears a word
454
+ // for a call. A word that is some call's argument reaches that call pending, and the licence's
455
+ // equality (a declared callee) or (b) (an undeclared one) refuses it there, per program point:
456
+ // `qh5`'s fifth `h5` word, staged after its memcpy base escapes, is refused by (b). What this
457
+ // widening lets through is a store that reaches no call pending with its lower slots supplied,
458
+ // which by CONTIGUITY (above) no call takes as an argument.
459
+ for (const o of everySlot) {
460
+ if (o >= lowestEscape) {
461
+ reloaded.add(o);
462
+ }
463
+ }
464
+ const storedAnywhere = belowBound(stores);
328
465
  // A LICENSED WORD THIS FUNCTION ALSO LOADS. The area belongs to the CALLEE — which may assign
329
466
  // to a stack parameter — so after the `bl` the word holds whatever the callee left, and an
330
467
  // `ldr` off that offset reads a GAP. The dataflow above cannot catch it: the call consumes the
@@ -347,7 +484,8 @@ export function analyzeOutgoingArgs<C>({
347
484
  // (a) — a store never reloaded ANYWHERE, with its lower slots supplied, is an argument's
348
485
  // signature: an outgoing argument is read by the CALLEE, never by the caller. Its real theorem
349
486
  // is the layout one (the area sits at the BOTTOM of localArea, disjoint from the locals, so no
350
- // local load can land on an argument offset), which is why it is a whole-function question.
487
+ // local load can land on an argument offset), which is why it is a whole-function question, and
488
+ // why a word at or above `bound` never reaches it: the bound proves that theorem for it.
351
489
  // A LICENSED offset is excluded: its never being reloaded is explained by the call that takes it.
352
490
  for (const off of asc(storedAnywhere)) {
353
491
  if (!licensed.has(off) && !reloaded.has(off) && prefixStored(off, storedAnywhere)) {
@@ -384,7 +522,7 @@ export function analyzeOutgoingArgs<C>({
384
522
  // where the function ENDS. That is not an argument and not a local anyone reloads, so nothing
385
523
  // here can say what it is: decline rather than let it drop as a dead def.
386
524
  //
387
- // "WHERE THE FUNCTION ENDS" IS A LIVE BLOCK WITH NO LIVE SUCCESSOR, read off `preds`, not a
525
+ // "WHERE THE FUNCTION ENDS" IS A LIVE BLOCK WITH NO LIVE SUCCESSOR, read off `succs`, not a
388
526
  // terminator the caller classified. Under Thumb the two coincide — a computed PC write has no
389
527
  // static successor and the frontend throws on one long before here — but asking the CFG costs
390
528
  // nothing and removes a fact the caller could get wrong.
@@ -396,16 +534,8 @@ export function analyzeOutgoingArgs<C>({
396
534
  // declines at L2: "unrecovered back-edge into block #1 (loop-recovery declined this shape)". So
397
535
  // the loud answer is preserved by a DIFFERENT family's refusal, not by this one. Closing it needs
398
536
  // a backward "can this word still be consumed?" pass, which no row in the corpus asks for.
399
- const hasLiveSucc = asmBlocks.map(() => false);
400
- for (let b = 0; b < asmBlocks.length; b++) {
401
- if (live.has(b)) {
402
- for (const q of preds[b]) {
403
- hasLiveSucc[q] = true;
404
- }
405
- }
406
- }
407
537
  for (let b = 0; b < asmBlocks.length; b++) {
408
- if (!live.has(b) || hasLiveSucc[b]) {
538
+ if (!live.has(b) || succs[b].length > 0) {
409
539
  continue;
410
540
  }
411
541
  for (const off of asc(mayOut[b])) {