@asmlift/core 0.8.1 → 0.9.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.
- package/README.md +34 -30
- package/package.json +1 -1
- package/src/aggregate.ts +248 -0
- package/src/backend/c.ts +5 -2
- package/src/backend/cfamily.ts +236 -37
- package/src/backend/cpp.ts +114 -9
- package/src/backend/pascal.ts +39 -0
- package/src/cdecl/constant.ts +59 -0
- package/src/cdecl/lex.ts +209 -0
- package/src/cdecl/parse.ts +975 -0
- package/src/cdecl/spell.ts +69 -0
- package/src/codegen-flags.ts +38 -6
- package/src/compiler-diagnostics.ts +194 -0
- package/src/contracts.ts +615 -79
- package/src/declare.ts +75 -5
- package/src/frontend/asmdata.ts +19 -5
- package/src/frontend/call-plan.ts +686 -0
- package/src/frontend/device-pins.ts +277 -0
- package/src/frontend/errors.ts +5 -0
- package/src/frontend/flags-edge.ts +101 -0
- package/src/frontend/fpu.ts +167 -0
- package/src/frontend/frame-objects.ts +2131 -0
- package/src/frontend/high-half.ts +2 -1
- package/src/frontend/local-object.ts +490 -0
- package/src/frontend/mips.ts +363 -40
- package/src/frontend/opaque.ts +97 -18
- package/src/frontend/ppc.ts +520 -129
- package/src/frontend/reloc-symbol.ts +94 -13
- package/src/frontend/splat.ts +108 -5
- package/src/frontend/ssa.ts +669 -76
- package/src/frontend/stackargs.ts +120 -39
- package/src/frontend/thumb.ts +2764 -2018
- package/src/ir/alias.ts +29 -0
- package/src/ir/core.ts +101 -3
- package/src/ir/discipline.ts +248 -0
- package/src/ir/float-bits.ts +47 -0
- package/src/ir/opcodes.ts +84 -112
- package/src/ir/parse.ts +1 -0
- package/src/ir/types.ts +118 -2
- package/src/ir/verify.ts +150 -3
- package/src/l3/address.ts +5 -0
- package/src/l3/argcopy.ts +38 -6
- package/src/l3/ast.ts +186 -14
- package/src/l3/coalesce.ts +21 -13
- package/src/l3/dce.ts +32 -10
- package/src/l3/gates.ts +9 -3
- package/src/l3/hoist.ts +8 -7
- package/src/l3/initfirst.ts +27 -9
- package/src/l3/intcell.ts +97 -0
- package/src/l3/mentions.ts +10 -4
- package/src/l3/narrowdecl.ts +218 -0
- package/src/l3/nearbase.ts +10 -5
- package/src/l3/parkfirst.ts +8 -5
- package/src/l3/pollguard.ts +3 -2
- package/src/l3/ptrcell.ts +357 -0
- package/src/l3/regspell.ts +17 -3
- package/src/l3/reindex.ts +10 -7
- package/src/l3/symbol-refs.ts +104 -23
- package/src/l3/tailmerge.ts +6 -19
- package/src/l3/typing.ts +137 -25
- package/src/l3/unreduce.ts +4 -4
- package/src/l3/volstore.ts +96 -7
- package/src/mangle.ts +11 -1
- package/src/pattern/engine.ts +19 -16
- package/src/pipeline.ts +57 -12
- package/src/proto-context.ts +788 -0
- package/src/proto.ts +689 -50
- package/src/raise/arrays.ts +4 -3
- package/src/raise/declared-volatile.ts +131 -0
- package/src/raise/divpow2.ts +2 -2
- package/src/raise/errors.ts +17 -0
- package/src/raise/floathelpers.ts +243 -0
- package/src/raise/globalshape.ts +11 -5
- package/src/raise/gvn.ts +26 -5
- package/src/raise/narrow.ts +29 -2
- package/src/raise/narrowlocal.ts +11 -6
- package/src/raise/offsetnames.ts +510 -0
- package/src/raise/pairparams.ts +81 -0
- package/src/raise/paramwidth.ts +7 -0
- package/src/raise/pre-recovery.ts +78 -4
- package/src/raise/recover.ts +14 -10
- package/src/raise/retsink.ts +1 -1
- package/src/raise/shortcircuit.ts +41 -19
- package/src/raise/softdiv.ts +21 -23
- package/src/raise/struct-arrays.ts +5 -4
- package/src/raise/structs.ts +311 -98
- package/src/raise/truncload.ts +366 -0
- package/src/raise/widehelpers.ts +106 -0
- package/src/rank-declare.ts +30 -5
- package/src/rank-variations.ts +36 -8
- package/src/rank.ts +267 -79
- package/src/runtime-helpers.ts +287 -0
- package/src/stillborn.ts +289 -0
- package/src/structure/analysis.ts +1285 -448
- package/src/structure/hazards.ts +633 -58
- package/src/structure/local-statics.ts +381 -0
- package/src/structure/namecoalesce.ts +1 -1
- package/src/structure/redundant-test.ts +99 -0
- package/src/structure/structure.ts +1772 -395
- package/src/structure/switch-recover.ts +159 -78
- package/src/symbols.ts +38 -3
- package/src/target.ts +662 -59
- package/src/trace.ts +44 -24
- package/src/variation-definitions.ts +119 -1
- package/src/variation-tokens.ts +20 -6
|
@@ -25,8 +25,9 @@ export interface StackArgsSlot {
|
|
|
25
25
|
|
|
26
26
|
/** A call, and what its callee's DECLARATION says it takes on the stack. `declared` is the block
|
|
27
27
|
* `[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.
|
|
29
|
-
*
|
|
28
|
+
* 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. */
|
|
30
31
|
export interface StackArgsCall<C> {
|
|
31
32
|
readonly kind: 'call';
|
|
32
33
|
/** The frontend's own handle for this call — the key of `OutgoingArgs.blocks`. */
|
|
@@ -34,6 +35,9 @@ export interface StackArgsCall<C> {
|
|
|
34
35
|
/** The callee's name, for the refusal messages. `?` where the frontend has none. */
|
|
35
36
|
readonly callee: string;
|
|
36
37
|
readonly declared: readonly number[] | null;
|
|
38
|
+
/** The parameters the declaration lists, for the refusal messages — fewer than its words when a
|
|
39
|
+
* 64-bit parameter takes two or a hidden struct-return pointer takes one. Absent, one per word. */
|
|
40
|
+
readonly params?: number;
|
|
37
41
|
}
|
|
38
42
|
|
|
39
43
|
/** One basic block's events. Where control goes afterwards is read out of `preds`, so a block is
|
|
@@ -56,6 +60,10 @@ export interface StackArgsInput<C> {
|
|
|
56
60
|
readonly argRegs: number;
|
|
57
61
|
/** The frontend's verdict that the whole frame is one addressable object handed to a callee. */
|
|
58
62
|
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;
|
|
59
67
|
}
|
|
60
68
|
|
|
61
69
|
/** What this function's calls do with the BOTTOM of its frame — the outgoing stack-argument area
|
|
@@ -75,6 +83,50 @@ export interface OutgoingArgs<C> {
|
|
|
75
83
|
|
|
76
84
|
const isCallEvent = <C>(ev: StackArgsEvent<C>): ev is StackArgsCall<C> => ev.kind === 'call';
|
|
77
85
|
|
|
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.
|
|
91
|
+
*
|
|
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>(
|
|
96
|
+
blocks: readonly StackArgsBlock<C>[],
|
|
97
|
+
succs: readonly (readonly number[])[],
|
|
98
|
+
live: ReadonlySet<number>,
|
|
99
|
+
): number {
|
|
100
|
+
const liveIn = blocks.map(() => new Set<number>());
|
|
101
|
+
const liveOut = (b: number): Set<number> => new Set(succs[b].flatMap((s) => [...liveIn[s]]));
|
|
102
|
+
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;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return bound;
|
|
128
|
+
}
|
|
129
|
+
|
|
78
130
|
// THE OUTGOING STACK-ARGUMENT AREA, AND WHO MAY CONSUME IT.
|
|
79
131
|
//
|
|
80
132
|
// agbcc's ACCUMULATE_OUTGOING_ARGS reserves the BOTTOM of the frame for arguments 5+ of the calls
|
|
@@ -86,7 +138,7 @@ const isCallEvent = <C>(ev: StackArgsEvent<C>): ev is StackArgsCall<C> => ev.kin
|
|
|
86
138
|
// CreateEntity_Platform_0_0 (platform.c:734) forwards SIX arguments and came out as
|
|
87
139
|
// `CreateEntity_Platform(0, 0, a0, (u16)a1)`.
|
|
88
140
|
//
|
|
89
|
-
// TWO INDEPENDENT WITNESSES MUST AGREE
|
|
141
|
+
// TWO INDEPENDENT WITNESSES MUST AGREE for a call to CONSUME words — the licence:
|
|
90
142
|
// * the DECLARATION says how many words a call takes. AAPCS lays arguments 5..n at [sp,#0]
|
|
91
143
|
// upward, one word each, so the block is `[0, 4*(n - |argRegs|))`, contiguous from zero.
|
|
92
144
|
// * the CODE says which words are staged for it — the offsets stored and not yet reloaded when
|
|
@@ -100,15 +152,27 @@ const isCallEvent = <C>(ev: StackArgsEvent<C>): ev is StackArgsCall<C> => ev.kin
|
|
|
100
152
|
// * a variadic callee's list is a prefix — `sprintf` truthfully declares two and is handed six,
|
|
101
153
|
// * a large struct return adds a hidden pointer argument that appears in no parameter list.
|
|
102
154
|
//
|
|
103
|
-
//
|
|
104
|
-
//
|
|
105
|
-
//
|
|
106
|
-
//
|
|
107
|
-
//
|
|
155
|
+
// A `SymbolSignature` records none of those, and neither does a COUNT. A typed `FnProto` list
|
|
156
|
+
// records only a `long long`, which the Thumb frontend refuses past the argument registers; a
|
|
157
|
+
// `double` or a by-value struct has no width there, so the list states no layout at all. An
|
|
158
|
+
// ARITY-ONLY acceptance therefore has all three holes: supplying a TRUE fact
|
|
159
|
+
// (`{ sprintf: { params: 2 } }`) turns a correct decline into `return sprintf(a0, a1)` with both
|
|
160
|
+
// stack arguments deleted. Under the rule here the four words `sprintf` is really handed are four
|
|
161
|
+
// 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
162
|
// 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
|
|
163
|
+
// so is a dead local, which is why reading the code alone never licenses a word (conditions (a)
|
|
110
164
|
// and (b) below, kept for every call no declaration covers).
|
|
111
165
|
//
|
|
166
|
+
// 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.
|
|
175
|
+
//
|
|
112
176
|
// THE TWO SIDES ARE CHECKED AGAINST DIFFERENT SETS, and the asymmetry is the point.
|
|
113
177
|
// * NOTHING EXTRA is checked against the MAY set (stored and unreloaded on SOME path): the
|
|
114
178
|
// weakest thing that could still be a word this call takes must be inside the block.
|
|
@@ -118,14 +182,15 @@ const isCallEvent = <C>(ev: StackArgsEvent<C>): ev is StackArgsCall<C> => ev.kin
|
|
|
118
182
|
// 0x0 in block")`; here it is a decline, and for the same reason — it is a GAP, and a gap
|
|
119
183
|
// must never render as a plausible value.
|
|
120
184
|
//
|
|
121
|
-
//
|
|
122
|
-
//
|
|
123
|
-
//
|
|
124
|
-
//
|
|
125
|
-
//
|
|
126
|
-
//
|
|
127
|
-
//
|
|
128
|
-
//
|
|
185
|
+
// 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]
|
|
192
|
+
// also reaches the call unread") rather than the capability, so a gap histogram groups it under
|
|
193
|
+
// that message.
|
|
129
194
|
// The must set is an intersection over predecessors, which is exactly what a TAIL-MERGED call
|
|
130
195
|
// site needs: agbcc does tail-merge (`Task_BonusFlower_Spawn`, sa3 bonus_game_enemies, stores
|
|
131
196
|
// argument 5 in both predecessors with the `bl` in the join), and a one-armed store — the same
|
|
@@ -148,6 +213,7 @@ export function analyzeOutgoingArgs<C>({
|
|
|
148
213
|
localArea,
|
|
149
214
|
argRegs,
|
|
150
215
|
capturedWholeFrame,
|
|
216
|
+
localsAboveOutgoingArea,
|
|
151
217
|
}: StackArgsInput<C>): OutgoingArgs<C> {
|
|
152
218
|
const refuse = (blocker: string): OutgoingArgs<C> => ({ blocker, blocks: new Map(), area: 0 });
|
|
153
219
|
// EVERY block, not the entry-reachable ones: a call in dead code stages nothing, so the
|
|
@@ -164,19 +230,25 @@ export function analyzeOutgoingArgs<C>({
|
|
|
164
230
|
return { blocker: null, blocks: new Map(), area: 0 };
|
|
165
231
|
}
|
|
166
232
|
const say = (offs: readonly number[]) => offs.map((o) => `[sp,#${o}]`).join(', ');
|
|
167
|
-
const
|
|
233
|
+
const declaredAs = (ev: StackArgsCall<C>, offs: readonly number[]) => {
|
|
234
|
+
const words = argRegs + offs.length;
|
|
235
|
+
return ev.params === undefined || ev.params === words
|
|
236
|
+
? `${words} arguments`
|
|
237
|
+
: `${ev.params} arguments in ${words} argument words`;
|
|
238
|
+
};
|
|
168
239
|
|
|
169
|
-
// THE ONE-WORD CAPTURED FRAME. `capturedWholeFrame` says the whole frame is an
|
|
170
|
-
//
|
|
171
|
-
//
|
|
172
|
-
//
|
|
240
|
+
// THE ONE-WORD CAPTURED FRAME. `capturedWholeFrame` says the whole frame is an object whose
|
|
241
|
+
// address has ESCAPED this function — handed to a callee, or published to memory; the frontend
|
|
242
|
+
// that computes it owns which escapes qualify. A declared fifth argument says [sp,#0] is a
|
|
243
|
+
// DIFFERENT callee's argument slot. Two contradictory claims about the same word, and nothing
|
|
244
|
+
// here can decide which to believe, so the honest answer is the decline it has always been.
|
|
173
245
|
if (capturedWholeFrame) {
|
|
174
246
|
for (const ev of calls) {
|
|
175
247
|
const offs = ev.declared;
|
|
176
248
|
if (offs !== null) {
|
|
177
249
|
return refuse(
|
|
178
|
-
`callee \`${ev.callee}\` is declared with ${
|
|
179
|
-
'but this one-word frame is an object whose address
|
|
250
|
+
`callee \`${ev.callee}\` is declared with ${declaredAs(ev, offs)}, so [sp,#0] is its outgoing stack argument — ` +
|
|
251
|
+
'but this one-word frame is an object whose address escapes the function, and the two name the same word',
|
|
180
252
|
);
|
|
181
253
|
}
|
|
182
254
|
}
|
|
@@ -188,6 +260,21 @@ export function analyzeOutgoingArgs<C>({
|
|
|
188
260
|
return { blocker: null, blocks: new Map(), area: 0 };
|
|
189
261
|
}
|
|
190
262
|
|
|
263
|
+
const succs = asmBlocks.map((): number[] => []);
|
|
264
|
+
for (const b of live) {
|
|
265
|
+
for (const q of preds[b]) {
|
|
266
|
+
if (live.has(q)) {
|
|
267
|
+
succs[q].push(b);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
// Every word at or above this is a proven local, so it is no call's argument. The three
|
|
272
|
+
// code-reading refusals below — the licence's NOTHING EXTRA, (a) and (b) — each hunt for a word
|
|
273
|
+
// that could be one, so the words they weigh are cut to those beneath the bound once, where
|
|
274
|
+
// they are collected (`mayAt`, `storedAnywhere`), and none of them repeats the test.
|
|
275
|
+
const bound = localsAboveOutgoingArea ? survivorBound(asmBlocks, succs, live) : Infinity;
|
|
276
|
+
const belowBound = (s: Iterable<number>): Set<number> => new Set([...s].filter((o) => o < bound));
|
|
277
|
+
|
|
191
278
|
const everySlot: number[] = [];
|
|
192
279
|
for (let o = 0; o + 4 <= localArea; o += 4) {
|
|
193
280
|
everySlot.push(o);
|
|
@@ -234,7 +321,7 @@ export function analyzeOutgoingArgs<C>({
|
|
|
234
321
|
);
|
|
235
322
|
for (const ev of asmBlocks[b].events) {
|
|
236
323
|
if (isCallEvent(ev)) {
|
|
237
|
-
mayAt.set(ev,
|
|
324
|
+
mayAt.set(ev, belowBound(may));
|
|
238
325
|
mustAt.set(ev, new Set(must));
|
|
239
326
|
storedAt.set(ev, new Set(stored));
|
|
240
327
|
for (const o of ev.declared ?? []) {
|
|
@@ -297,7 +384,7 @@ export function analyzeOutgoingArgs<C>({
|
|
|
297
384
|
const extra = asc(may).filter((o) => !offs.includes(o));
|
|
298
385
|
if (missing.length > 0 || extra.length > 0) {
|
|
299
386
|
return refuse(
|
|
300
|
-
`callee \`${ev.callee}\` is declared with ${
|
|
387
|
+
`callee \`${ev.callee}\` is declared with ${declaredAs(ev, offs)}, so its outgoing stack-argument block is ${say(offs)} — but ` +
|
|
301
388
|
(missing.length > 0
|
|
302
389
|
? `${say(missing)} is not stored on every path to the call`
|
|
303
390
|
: `${say(extra)} also reaches the call unread, so the declaration does not account for every word staged here`),
|
|
@@ -317,14 +404,15 @@ export function analyzeOutgoingArgs<C>({
|
|
|
317
404
|
// every offset live code loads back. A reload in dead code is not evidence that anything reads
|
|
318
405
|
// the slot back, so it does not count.
|
|
319
406
|
const reloaded = new Set<number>();
|
|
320
|
-
const
|
|
407
|
+
const stores = new Set<number>();
|
|
321
408
|
for (const b of live) {
|
|
322
409
|
for (const ev of asmBlocks[b].events) {
|
|
323
410
|
if (ev.kind !== 'call') {
|
|
324
|
-
(ev.kind === 'store' ?
|
|
411
|
+
(ev.kind === 'store' ? stores : reloaded).add(ev.off);
|
|
325
412
|
}
|
|
326
413
|
}
|
|
327
414
|
}
|
|
415
|
+
const storedAnywhere = belowBound(stores);
|
|
328
416
|
// A LICENSED WORD THIS FUNCTION ALSO LOADS. The area belongs to the CALLEE — which may assign
|
|
329
417
|
// to a stack parameter — so after the `bl` the word holds whatever the callee left, and an
|
|
330
418
|
// `ldr` off that offset reads a GAP. The dataflow above cannot catch it: the call consumes the
|
|
@@ -347,7 +435,8 @@ export function analyzeOutgoingArgs<C>({
|
|
|
347
435
|
// (a) — a store never reloaded ANYWHERE, with its lower slots supplied, is an argument's
|
|
348
436
|
// signature: an outgoing argument is read by the CALLEE, never by the caller. Its real theorem
|
|
349
437
|
// 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
|
|
438
|
+
// local load can land on an argument offset), which is why it is a whole-function question, and
|
|
439
|
+
// why a word at or above `bound` never reaches it: the bound proves that theorem for it.
|
|
351
440
|
// A LICENSED offset is excluded: its never being reloaded is explained by the call that takes it.
|
|
352
441
|
for (const off of asc(storedAnywhere)) {
|
|
353
442
|
if (!licensed.has(off) && !reloaded.has(off) && prefixStored(off, storedAnywhere)) {
|
|
@@ -384,7 +473,7 @@ export function analyzeOutgoingArgs<C>({
|
|
|
384
473
|
// where the function ENDS. That is not an argument and not a local anyone reloads, so nothing
|
|
385
474
|
// here can say what it is: decline rather than let it drop as a dead def.
|
|
386
475
|
//
|
|
387
|
-
// "WHERE THE FUNCTION ENDS" IS A LIVE BLOCK WITH NO LIVE SUCCESSOR, read off `
|
|
476
|
+
// "WHERE THE FUNCTION ENDS" IS A LIVE BLOCK WITH NO LIVE SUCCESSOR, read off `succs`, not a
|
|
388
477
|
// terminator the caller classified. Under Thumb the two coincide — a computed PC write has no
|
|
389
478
|
// static successor and the frontend throws on one long before here — but asking the CFG costs
|
|
390
479
|
// nothing and removes a fact the caller could get wrong.
|
|
@@ -396,16 +485,8 @@ export function analyzeOutgoingArgs<C>({
|
|
|
396
485
|
// declines at L2: "unrecovered back-edge into block #1 (loop-recovery declined this shape)". So
|
|
397
486
|
// the loud answer is preserved by a DIFFERENT family's refusal, not by this one. Closing it needs
|
|
398
487
|
// 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
488
|
for (let b = 0; b < asmBlocks.length; b++) {
|
|
408
|
-
if (!live.has(b) ||
|
|
489
|
+
if (!live.has(b) || succs[b].length > 0) {
|
|
409
490
|
continue;
|
|
410
491
|
}
|
|
411
492
|
for (const off of asc(mayOut[b])) {
|