@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.
- package/README.md +25 -24
- package/package.json +1 -1
- package/src/aggregate.ts +149 -0
- package/src/backend/c.ts +2 -0
- package/src/backend/cfamily.ts +197 -61
- package/src/backend/cpp.ts +3 -1
- package/src/backend/lines.ts +10 -0
- package/src/backend/pascal.ts +47 -16
- package/src/cdecl/parse.ts +21 -0
- package/src/contracts.ts +246 -36
- package/src/declare.ts +69 -20
- package/src/frontend/arm-body.ts +155 -0
- package/src/frontend/asmdata.ts +9 -2
- package/src/frontend/call-plan.ts +1117 -61
- package/src/frontend/callee-body.ts +370 -0
- package/src/frontend/dead-reads.ts +628 -0
- package/src/frontend/device-pins.ts +33 -4
- package/src/frontend/emit.ts +4 -3
- package/src/frontend/fpu.ts +224 -17
- package/src/frontend/frame-objects.ts +1689 -206
- package/src/frontend/frontend.ts +84 -3
- package/src/frontend/high-half.ts +53 -4
- package/src/frontend/jump-table.ts +545 -0
- package/src/frontend/local-object.ts +158 -32
- package/src/frontend/mips.ts +2296 -217
- package/src/frontend/own-struct-return.ts +500 -0
- package/src/frontend/ppc.ts +2748 -245
- package/src/frontend/register-struct-call.ts +399 -0
- package/src/frontend/reloc-symbol.ts +22 -7
- package/src/frontend/ssa.ts +1721 -43
- package/src/frontend/stackargs.ts +199 -72
- package/src/frontend/thumb-entry.ts +340 -0
- package/src/frontend/thumb.ts +2400 -325
- package/src/ir/alias.ts +65 -2
- package/src/ir/bits.ts +1699 -4
- package/src/ir/core.ts +254 -9
- package/src/ir/discipline.ts +86 -21
- package/src/ir/entry.ts +157 -0
- package/src/{structure → ir}/loops.ts +6 -2
- package/src/ir/opcodes.ts +137 -9
- package/src/ir/parse.ts +1 -0
- package/src/ir/simplify.ts +61 -10
- package/src/ir/types.ts +86 -2
- package/src/ir/verify.ts +96 -11
- package/src/l3/argbase.ts +2 -0
- package/src/l3/ast.ts +582 -18
- package/src/l3/basecse.ts +68 -15
- package/src/l3/caseendgoto.ts +223 -0
- package/src/l3/coalesce.ts +42 -14
- package/src/l3/constinit.ts +410 -0
- package/src/l3/dce.ts +53 -9
- package/src/l3/initfirst.ts +42 -11
- package/src/l3/intcell.ts +2 -1
- package/src/l3/narrowdecl.ts +77 -36
- package/src/l3/pollguard.ts +8 -2
- package/src/l3/ptrcell.ts +33 -34
- package/src/l3/ptrfield.ts +123 -8
- package/src/l3/regspell.ts +11 -4
- package/src/l3/reindex.ts +295 -99
- package/src/l3/scopebase.ts +2 -0
- package/src/l3/storage.ts +98 -1
- package/src/l3/symbol-refs.ts +84 -6
- package/src/l3/tailmerge.ts +6 -2
- package/src/l3/typing.ts +108 -15
- package/src/l3/unreduce.ts +6 -5
- package/src/l3/volatileptr.ts +17 -54
- package/src/mangle.ts +30 -0
- package/src/pattern/engine.ts +361 -73
- package/src/pipeline.ts +1039 -38
- package/src/proto-context.ts +1039 -45
- package/src/proto.ts +555 -40
- package/src/raise/aggmembers.ts +290 -0
- package/src/raise/arrays.ts +5 -1
- package/src/raise/const.ts +25 -0
- package/src/raise/declared-volatile.ts +207 -31
- package/src/raise/divpow2.ts +192 -0
- package/src/raise/floathelpers.ts +254 -33
- package/src/raise/gvn.ts +3 -3
- package/src/raise/latch.ts +162 -6
- package/src/raise/memberarrays.ts +10 -4
- package/src/raise/offsetnames.ts +422 -36
- package/src/raise/pairparams.ts +33 -0
- package/src/raise/paramwidth.ts +196 -6
- package/src/raise/pre-recovery.ts +107 -21
- package/src/raise/pre-structure.ts +73 -0
- package/src/raise/recover.ts +12 -5
- package/src/raise/reroll.ts +674 -0
- package/src/raise/shortcircuit.ts +217 -34
- package/src/raise/softdiv.ts +1 -1
- package/src/raise/storesign.ts +153 -0
- package/src/raise/struct-arrays.ts +30 -11
- package/src/raise/structs.ts +396 -33
- package/src/raise/truncload.ts +4 -3
- package/src/raise/unpre.ts +594 -0
- package/src/raise/unthread.ts +400 -0
- package/src/rank-declare.ts +118 -12
- package/src/rank-variations.ts +186 -35
- package/src/rank.ts +536 -68
- package/src/runtime-helpers.ts +140 -22
- package/src/structure/analysis.ts +217 -8
- package/src/structure/arith-ops.ts +23 -0
- package/src/structure/cross-jumped-tail.ts +216 -0
- package/src/structure/globalaccess.ts +157 -10
- package/src/structure/hazards.ts +16 -64
- package/src/structure/local-statics.ts +145 -9
- package/src/structure/namecoalesce.ts +1 -1
- package/src/structure/pointer-spelling.ts +1106 -0
- package/src/structure/redundant-test.ts +1 -1
- package/src/structure/structure.ts +6192 -1213
- package/src/structure/switch-recover.ts +583 -120
- package/src/structure/unfolded-branch-constant.ts +90 -0
- package/src/symbols.ts +58 -15
- package/src/target.ts +1028 -54
- package/src/trace.ts +49 -17
- package/src/variation-definitions.ts +333 -20
- package/src/variation-gates.ts +4 -0
- 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
|
|
30
|
-
*
|
|
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
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
|
|
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
|
|
87
|
-
*
|
|
88
|
-
* `
|
|
89
|
-
*
|
|
90
|
-
*
|
|
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
|
-
*
|
|
93
|
-
*
|
|
94
|
-
*
|
|
95
|
-
function
|
|
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
|
-
|
|
101
|
-
|
|
127
|
+
if (from === undefined) {
|
|
128
|
+
return Infinity;
|
|
129
|
+
}
|
|
102
130
|
let bound = Infinity;
|
|
103
|
-
for (
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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
|
|
168
|
-
// (`
|
|
169
|
-
//
|
|
170
|
-
//
|
|
171
|
-
//
|
|
172
|
-
//
|
|
173
|
-
//
|
|
174
|
-
//
|
|
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; `
|
|
187
|
-
// word
|
|
188
|
-
//
|
|
189
|
-
//
|
|
190
|
-
//
|
|
191
|
-
// A spill with no
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
459
|
-
|
|
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
|