@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.
- package/README.md +36 -31
- package/package.json +1 -1
- package/src/aggregate.ts +397 -0
- package/src/backend/c.ts +6 -2
- package/src/backend/cfamily.ts +370 -56
- package/src/backend/cpp.ts +116 -10
- package/src/backend/pascal.ts +69 -11
- 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 +777 -84
- package/src/declare.ts +132 -13
- package/src/frontend/asmdata.ts +26 -6
- package/src/frontend/call-plan.ts +1590 -0
- package/src/frontend/dead-reads.ts +628 -0
- package/src/frontend/device-pins.ts +296 -0
- package/src/frontend/emit.ts +4 -3
- package/src/frontend/errors.ts +5 -0
- package/src/frontend/flags-edge.ts +101 -0
- package/src/frontend/fpu.ts +374 -0
- package/src/frontend/frame-objects.ts +3171 -0
- package/src/frontend/frontend.ts +64 -3
- package/src/frontend/high-half.ts +55 -5
- package/src/frontend/jump-table.ts +530 -0
- package/src/frontend/local-object.ts +616 -0
- package/src/frontend/mips.ts +2392 -208
- package/src/frontend/opaque.ts +97 -18
- package/src/frontend/own-struct-return.ts +500 -0
- package/src/frontend/ppc.ts +3137 -267
- package/src/frontend/register-struct-call.ts +484 -0
- package/src/frontend/reloc-symbol.ts +109 -13
- package/src/frontend/splat.ts +108 -5
- package/src/frontend/ssa.ts +2098 -89
- package/src/frontend/stackargs.ts +172 -42
- package/src/frontend/thumb.ts +4563 -2091
- package/src/ir/alias.ts +71 -0
- package/src/ir/bits.ts +681 -1
- package/src/ir/core.ts +199 -6
- package/src/ir/discipline.ts +313 -0
- package/src/ir/entry.ts +159 -0
- package/src/ir/float-bits.ts +47 -0
- package/src/{structure → ir}/loops.ts +6 -2
- package/src/ir/opcodes.ts +193 -121
- package/src/ir/parse.ts +1 -0
- package/src/ir/types.ts +202 -2
- package/src/ir/verify.ts +219 -3
- package/src/l3/address.ts +5 -0
- package/src/l3/argcopy.ts +38 -6
- package/src/l3/ast.ts +467 -23
- package/src/l3/basecse.ts +33 -5
- package/src/l3/coalesce.ts +21 -13
- package/src/l3/constinit.ts +382 -0
- package/src/l3/dce.ts +34 -12
- package/src/l3/gates.ts +9 -3
- package/src/l3/hoist.ts +8 -7
- package/src/l3/initfirst.ts +68 -19
- package/src/l3/intcell.ts +98 -0
- package/src/l3/mentions.ts +10 -4
- package/src/l3/narrowdecl.ts +259 -0
- package/src/l3/nearbase.ts +10 -5
- package/src/l3/parkfirst.ts +8 -5
- package/src/l3/pollguard.ts +11 -4
- package/src/l3/ptrcell.ts +354 -0
- package/src/l3/ptrfield.ts +123 -8
- package/src/l3/regspell.ts +26 -7
- package/src/l3/reindex.ts +293 -103
- package/src/l3/storage.ts +98 -1
- package/src/l3/symbol-refs.ts +182 -23
- package/src/l3/tailmerge.ts +9 -20
- package/src/l3/typing.ts +237 -32
- package/src/l3/unreduce.ts +10 -9
- package/src/l3/volatileptr.ts +17 -54
- package/src/l3/volstore.ts +96 -7
- package/src/mangle.ts +41 -1
- package/src/pattern/engine.ts +373 -82
- package/src/pipeline.ts +589 -27
- package/src/proto-context.ts +1648 -0
- package/src/proto.ts +1203 -49
- package/src/raise/aggmembers.ts +290 -0
- package/src/raise/arrays.ts +8 -3
- package/src/raise/declared-volatile.ts +307 -0
- package/src/raise/divpow2.ts +2 -2
- package/src/raise/errors.ts +17 -0
- package/src/raise/floathelpers.ts +464 -0
- package/src/raise/globalshape.ts +11 -5
- package/src/raise/gvn.ts +29 -8
- package/src/raise/latch.ts +162 -6
- package/src/raise/memberarrays.ts +10 -4
- package/src/raise/narrow.ts +29 -2
- package/src/raise/narrowlocal.ts +11 -6
- package/src/raise/offsetnames.ts +494 -0
- package/src/raise/pairparams.ts +114 -0
- package/src/raise/paramwidth.ts +203 -6
- package/src/raise/pre-recovery.ts +174 -17
- package/src/raise/pre-structure.ts +54 -0
- package/src/raise/recover.ts +26 -15
- package/src/raise/reroll.ts +694 -0
- package/src/raise/retsink.ts +1 -1
- package/src/raise/shortcircuit.ts +66 -30
- package/src/raise/softdiv.ts +21 -23
- package/src/raise/storesign.ts +153 -0
- package/src/raise/struct-arrays.ts +34 -14
- package/src/raise/structs.ts +674 -98
- package/src/raise/truncload.ts +367 -0
- package/src/raise/unthread.ts +400 -0
- package/src/raise/widehelpers.ts +106 -0
- package/src/rank-declare.ts +143 -12
- package/src/rank-variations.ts +117 -31
- package/src/rank.ts +563 -122
- package/src/runtime-helpers.ts +405 -0
- package/src/stillborn.ts +289 -0
- package/src/structure/analysis.ts +1449 -449
- package/src/structure/arith-ops.ts +23 -0
- package/src/structure/globalaccess.ts +111 -9
- package/src/structure/hazards.ts +641 -114
- package/src/structure/local-statics.ts +517 -0
- package/src/structure/namecoalesce.ts +2 -2
- package/src/structure/pointer-spelling.ts +1106 -0
- package/src/structure/redundant-test.ts +99 -0
- package/src/structure/structure.ts +3478 -975
- package/src/structure/switch-recover.ts +403 -178
- package/src/symbols.ts +44 -3
- package/src/target.ts +1387 -71
- package/src/trace.ts +66 -28
- package/src/variation-definitions.ts +243 -13
- package/src/variation-gates.ts +2 -0
- 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.
|
|
29
|
-
*
|
|
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
|
|
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
|
-
//
|
|
104
|
-
//
|
|
105
|
-
//
|
|
106
|
-
//
|
|
107
|
-
//
|
|
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
|
|
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
|
-
//
|
|
122
|
-
//
|
|
123
|
-
//
|
|
124
|
-
//
|
|
125
|
-
//
|
|
126
|
-
//
|
|
127
|
-
//
|
|
128
|
-
//
|
|
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
|
|
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
|
-
//
|
|
171
|
-
//
|
|
172
|
-
//
|
|
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 ${
|
|
179
|
-
'but this one-word frame is an object whose address
|
|
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,
|
|
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 ${
|
|
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
|
|
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
|
|
324
|
-
(ev.kind === 'store' ?
|
|
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 `
|
|
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) ||
|
|
538
|
+
if (!live.has(b) || succs[b].length > 0) {
|
|
409
539
|
continue;
|
|
410
540
|
}
|
|
411
541
|
for (const off of asc(mayOut[b])) {
|