@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
|
@@ -0,0 +1,2131 @@
|
|
|
1
|
+
/** THE FRAME-OBJECT AUDIT, over finished IR and the frame partition a frontend measured.
|
|
2
|
+
*
|
|
3
|
+
* NOTHING HERE DECODES. A frontend lowers each address it can name as a frame offset to an
|
|
4
|
+
* `laddr` (Thumb: `mov rD, sp`, `add rD, sp, #k`, a capture moved by a constant) and hands over
|
|
5
|
+
* what only it can measure — the frame partition, as the same two ranges its `LiveInModel`
|
|
6
|
+
* carries, the offsets its slot model keyed, the one licence it grants on the text
|
|
7
|
+
* (`capturedObjectIsTheWholeFrame`), and the captures it moved. The rules that decide whether
|
|
8
|
+
* those `laddr`s are objects the emitted C can declare are asked of the IR here, the same for
|
|
9
|
+
* every ISA — the level-tower split for this frame model: the frontend supplies the PARTITION,
|
|
10
|
+
* the shared pass applies the rule. Messages name the frame base `sp`.
|
|
11
|
+
*
|
|
12
|
+
* Thumb is the only caller today; the worked examples below are agbcc's, because agbcc is the
|
|
13
|
+
* compiler every rule was measured against. */
|
|
14
|
+
import { type Block, type Op, type Value, mkOp, mkValue } from '../ir/core';
|
|
15
|
+
import { type IrType, T, typeEquals, typeToString } from '../ir/types';
|
|
16
|
+
import type { Gate } from '../l3/gates';
|
|
17
|
+
import type { SymbolMap } from '../symbols';
|
|
18
|
+
import { type TargetDescription, blockTransferRead, sourceControlRead, sourceReach } from '../target';
|
|
19
|
+
import type { CallDeclarations } from './call-plan';
|
|
20
|
+
import { type DevicePins, type LiteralAddresses, literalAddresses } from './device-pins';
|
|
21
|
+
import { FrontendUnsupportedError } from './errors';
|
|
22
|
+
import { type LiveInModel, slotKeyOffset } from './ssa';
|
|
23
|
+
|
|
24
|
+
export interface FrameObjectAudit {
|
|
25
|
+
name: string;
|
|
26
|
+
irBlocks: Block[];
|
|
27
|
+
/** the frame this function owns (`LiveInModel.ownedLocals`): every local lies inside it, and
|
|
28
|
+
* every word of it is an object, a slot, or storage nothing here describes */
|
|
29
|
+
ownedLocals: NonNullable<LiveInModel['ownedLocals']>;
|
|
30
|
+
/** where its locals are declared (`LiveInModel.declaredLocals`): `ownedLocals` less the outgoing
|
|
31
|
+
* stack-argument block staged at its bottom, which is where an untyped object claims to start */
|
|
32
|
+
declaredLocals: NonNullable<LiveInModel['declaredLocals']>;
|
|
33
|
+
usedSlotOffsets: ReadonlySet<number>;
|
|
34
|
+
capturedObjectIsTheWholeFrame: boolean;
|
|
35
|
+
/** every capture the add arm re-minted at a constant offset from it */
|
|
36
|
+
movedCaptures: ReadonlySet<Value>;
|
|
37
|
+
/** whether a callee's declaration rules out a struct returned through a hidden pointer at
|
|
38
|
+
* argument 0 (aggregate.ts `returnsWithoutHiddenPointer`), asked of the frontend's call
|
|
39
|
+
* declarations */
|
|
40
|
+
returnsWithoutHiddenPointer: CallDeclarations['returnsWithoutHiddenPointer'];
|
|
41
|
+
symbols: SymbolMap | undefined;
|
|
42
|
+
target: TargetDescription;
|
|
43
|
+
/** the retraction rules an escape is judged by; `FRAME_ESCAPE_GATES` when absent, and a
|
|
44
|
+
* census or an ablation hands in its own (`FRAME_OBJECT_AUDIT`) */
|
|
45
|
+
gates?: readonly Gate<FrameEscape>[];
|
|
46
|
+
/** the bytes a first audit asked to have kept as ONE object in memory (`FrameObjectRelift`):
|
|
47
|
+
* the frontend routes every access inside them through an `laddr`, and the audit declares
|
|
48
|
+
* them one object rather than judging each offset on its own */
|
|
49
|
+
oneObject?: FrameRange;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface FrameRange {
|
|
53
|
+
readonly from: number;
|
|
54
|
+
readonly to: number;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** What an audit answers instead of a refusal when a device may read the frame, or a callee write
|
|
58
|
+
* it, without bound: lift again with `oneObject` set to these bytes. */
|
|
59
|
+
export interface FrameObjectRelift {
|
|
60
|
+
readonly oneObject: FrameRange;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** What an audit answers: lift again (`FrameObjectRelift`), the device pin the accepted function
|
|
64
|
+
* owes (`DevicePins`, applied by `pinDeviceAccesses`), or nothing, when no frame address was
|
|
65
|
+
* taken. */
|
|
66
|
+
export type FrameObjectVerdict = FrameObjectRelift | DevicePins | undefined;
|
|
67
|
+
|
|
68
|
+
/** One escaped object, as the rules an escape retracts read it: what it may reach, `[lo, hi)`
|
|
69
|
+
* from its own offset, whether it may write, and the first other object, keyed slot and
|
|
70
|
+
* unaccounted frame word inside that reach — so a refusal names the fact its gate tested. */
|
|
71
|
+
export interface FrameEscape {
|
|
72
|
+
readonly off: number;
|
|
73
|
+
readonly lo: number;
|
|
74
|
+
readonly hi: number;
|
|
75
|
+
readonly writes: boolean;
|
|
76
|
+
/** how the address left, as the refusal words it */
|
|
77
|
+
readonly how: string;
|
|
78
|
+
readonly objectReached: number | undefined;
|
|
79
|
+
readonly slotReached: { readonly slot: number; readonly above: boolean } | undefined;
|
|
80
|
+
/** the lowest frame slot inside that reach the function holds an `undef` of */
|
|
81
|
+
readonly undefReached: number | undefined;
|
|
82
|
+
/** the lowest owned word inside that reach that no object and no slot accounts for */
|
|
83
|
+
readonly unaccountedWord: number | undefined;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// WHAT AN ESCAPE COSTS. The audit bounds what WE access through an object, never what a callee
|
|
87
|
+
// does with the address it was handed — and a callee may write any offset from it. So an escape
|
|
88
|
+
// retracts four claims, each over the bytes it may reach (`readWindow`): the whole frame for a
|
|
89
|
+
// writer nothing bounds. They are
|
|
90
|
+
// a table (`l3/gates.ts`), taken as `FrameObjectAudit.gates` so a census or an ablation can reach
|
|
91
|
+
// them; the rest of the audit — the split, the use classification, the premise re-check, the
|
|
92
|
+
// shape and overlap checks — decides what the objects ARE, in the stages `auditFrameObjects` runs.
|
|
93
|
+
export const FRAME_ESCAPE_GATES: readonly Gate<FrameEscape>[] = [
|
|
94
|
+
// The first is that the other ADDRESS-TAKEN objects are private, and it keys on the REACH, not
|
|
95
|
+
// on `mayWrite`. Its argument is about LAYOUT, and layout is symmetric: two objects are two
|
|
96
|
+
// separate C locals with no guaranteed adjacency, so a device that READS past the one it was
|
|
97
|
+
// given is as wrong as a callee that writes past it. `DmaCopy` with a count of two halfwords
|
|
98
|
+
// off `&sp0` transfers `[sp,#2]` too, and the emitted source transfers whatever the recompiler
|
|
99
|
+
// put after `sp0`, and the second object's own store is whatever the recompiler made of it.
|
|
100
|
+
// Marking both volatile would not repair that: the locals are still placed independently. A
|
|
101
|
+
// read the control word bounds to the object's own bytes reaches no neighbour, whatever its
|
|
102
|
+
// placement: two fixed-source DMA fills in one function, each off its own `volatile` local.
|
|
103
|
+
//
|
|
104
|
+
// It counts `laddr` objects. A neighbour SPILLED to an SSA slot is the slot rule's below.
|
|
105
|
+
{
|
|
106
|
+
id: 'reaches-another-object',
|
|
107
|
+
why: 'two locals have no guaranteed adjacency, so a reach past one lands on whatever the recompile put there',
|
|
108
|
+
sound: true,
|
|
109
|
+
guardedBy: 'thumb-frontend.test.ts: an incrementing fill reads the object above its own',
|
|
110
|
+
rejects: (e) => e.objectReached !== undefined,
|
|
111
|
+
},
|
|
112
|
+
// The second is `undef`, which rests on this function's own stores being the ONLY writer of
|
|
113
|
+
// its frame. A wider real object (`struct P p; g(&p);` where only `p.x` is read here) has its
|
|
114
|
+
// later words written by `g` and read back at a slot no store of ours reaches — declaring
|
|
115
|
+
// those uninitialised spells the callee's value as garbage. The extents here are inferred
|
|
116
|
+
// from OUR accesses, which is the number that is too small in this shape.
|
|
117
|
+
//
|
|
118
|
+
// On an escape and not on "a laddr exists": an address dereferenced only in-function cannot
|
|
119
|
+
// be written by anyone else, and the audit's overlap checks cover its aliasing.
|
|
120
|
+
//
|
|
121
|
+
// FRAME undefs only. A register-keyed one says a local lives in a register the ABI does not
|
|
122
|
+
// pass arguments in, and no address reaches a register — the escape this retraction is about
|
|
123
|
+
// cannot touch it, and counting it would refuse the whole function for an unrelated escape.
|
|
124
|
+
//
|
|
125
|
+
// …and only the frame slots inside the writer's reach. A callee handed only its own return
|
|
126
|
+
// storage writes the struct it returns and nothing else, so a slot outside that struct has no
|
|
127
|
+
// writer but this function: `if (a) x = gF(h); r = mk8(b); g4(i, j, x, a);` spills `x` beside
|
|
128
|
+
// `r`'s temp, unstored on one path, and that slot is still uninitialised there.
|
|
129
|
+
{
|
|
130
|
+
id: 'writer-over-undef',
|
|
131
|
+
why: 'an unstored frame slot is uninitialised only while this function is its sole writer',
|
|
132
|
+
sound: true,
|
|
133
|
+
guardedBy: 'thumb-frontend.test.ts: an ESCAPED frame address retracts the undef argument',
|
|
134
|
+
rejects: (e) => e.writes && e.undefReached !== undefined,
|
|
135
|
+
},
|
|
136
|
+
// …and the SLOT MODEL is the third claim an escape retracts — the undef rule's argument
|
|
137
|
+
// taken one step further. The audit's extents are inferred from OUR accesses, so an object
|
|
138
|
+
// wider in the SOURCE than the bytes this function touches has its later words written by
|
|
139
|
+
// the callee — and any of those modelled as an SSA slot is a value the slot model forwards
|
|
140
|
+
// ACROSS the call that overwrote it.
|
|
141
|
+
//
|
|
142
|
+
// The object has to be reached ONLY through the captured pointer (an `[sp,#0]` access of its own collides with the slot model and
|
|
143
|
+
// declines at the overlap check), which is what four corpus functions do:
|
|
144
|
+
//
|
|
145
|
+
// mov r2, sp / str r0, [r2] @ the object, written through the captured address
|
|
146
|
+
// str r1, [sp, #0x4] @ a word the slot model keys
|
|
147
|
+
// mov r0, r2 / bl g @ the base escapes; `g` may write [sp,#4]
|
|
148
|
+
// ldr r0, [sp, #0x4] @ …and the machine RELOADS it after the call
|
|
149
|
+
//
|
|
150
|
+
// and lifted, the reload is the value from BEFORE the call, the callee's write dropped, no
|
|
151
|
+
// diagnostic. Exactly the silent-wrong-answer trade the sp guards exist to prevent, so it refuses.
|
|
152
|
+
//
|
|
153
|
+
// WHAT IT COSTS: it refuses every word slot the escape may reach, which is blunter than the
|
|
154
|
+
// hazard it names. Narrowing it needs the object's real extent, and this model does not carry
|
|
155
|
+
// one: `extent` is a single width from a single access. The asm sometimes cannot
|
|
156
|
+
// supply it either — the compiled twin at `capturedObjectIsTheWholeFrame` is exactly this
|
|
157
|
+
// rule's shape, a slot THIS FUNCTION stores and reloads, undecidable between a spill and a
|
|
158
|
+
// member.
|
|
159
|
+
//
|
|
160
|
+
// A callee is not the only writer. `struct M { u8 b; u8 pad[3]; s32 t; }; gp = &m; g2();
|
|
161
|
+
// use2(m.t);` PUBLISHES the base to an ordinary global and the machine reloads [sp,#4] after
|
|
162
|
+
// `bl g2` — `g2` writes through `gp`, which points here: the same hazard as the call shape, one
|
|
163
|
+
// escape over.
|
|
164
|
+
//
|
|
165
|
+
// …and a writer is not the only hazard, so the rule keys on `escaped`, not `mayWrite`. A
|
|
166
|
+
// device that only READS through the address reads the slot's bytes from memory, where the
|
|
167
|
+
// slot model never put them: `struct P { u32 a, b; } s; s.a = x; s.b = y; REG_DMA3SAD =
|
|
168
|
+
// (u32)&s;` above an outgoing block is `str r4, [sp, #0x4] / str r5, [sp, #0x8] / add r1, sp,
|
|
169
|
+
// #0x4 / str r1, [DMA3SAD]`, where the slot model would keep `s.b` in a register and drop its
|
|
170
|
+
// store as dead while the DMA reads that word. The asm cannot tell that second word from a
|
|
171
|
+
// neighbour spilled beside the object, so what bounds the read is the transfer's own control
|
|
172
|
+
// word, `readWindow`.
|
|
173
|
+
//
|
|
174
|
+
// …and BELOW the object as well as above it. A C object extends upward from its base, but a
|
|
175
|
+
// captured address need not BE a base: `add r0, sp, #0x4` is `&buf[1]` as readily as `&b`, and
|
|
176
|
+
// `buf[0]` at [sp,#0] is then read or written through `p[-1]`. Only down to the outgoing
|
|
177
|
+
// block, whose words are the callee's arguments and never part of a local.
|
|
178
|
+
{
|
|
179
|
+
id: 'reaches-a-slot',
|
|
180
|
+
why: 'the slot model keeps a slot in a register, so a write or read through the frame misses it',
|
|
181
|
+
sound: true,
|
|
182
|
+
guardedBy: 'thumb-frontend.test.ts: above: a word a device may read past the object',
|
|
183
|
+
rejects: (e) => e.slotReached !== undefined,
|
|
184
|
+
},
|
|
185
|
+
// …and the FOURTH claim an escape retracts is the object's TOP, which the three rules above
|
|
186
|
+
// leave to whatever this function happened to touch. `extent` is one width from one access,
|
|
187
|
+
// so an object wider in the SOURCE than those bytes is declared too small — and a callee
|
|
188
|
+
// holding its address writes frame bytes the emitted C never allocated. Compiled:
|
|
189
|
+
//
|
|
190
|
+
// u8 buf[12]; buf[0] = x; garr(buf); use2(buf[0]);
|
|
191
|
+
// → add sp,sp,#-0xc / mov r1,sp / strb r0,[r1] / mov r0,sp / bl garr
|
|
192
|
+
//
|
|
193
|
+
// lifted as `u8 sp0; garr(&sp0); use2(sp0)` — a 12-byte object declared one byte, in a frame
|
|
194
|
+
// the recompile makes 4 bytes wide, with `garr` writing the other 8 into the caller's. The
|
|
195
|
+
// three rules above all pass it: one object, no `undef` op, no slot above it.
|
|
196
|
+
//
|
|
197
|
+
// What licenses an answer is the frame being ACCOUNTED FOR, word by word. Every word of the
|
|
198
|
+
// reserved local area the escape reaches has to be an object this audit modelled or a slot the
|
|
199
|
+
// slot model keys; a word that is neither is storage nothing here describes, so the emitted C
|
|
200
|
+
// reserves less than the machine did and the writer reaches past what it allocated. A writer
|
|
201
|
+
// that may write anywhere through the address reaches the whole local area, below the object as
|
|
202
|
+
// well as above it; a callee handed only its own return storage writes the struct it returns
|
|
203
|
+
// and nothing else (`readWindow`), so a word outside that struct is one it never reaches. Word
|
|
204
|
+
// granularity, not byte — the stack is word-aligned, so a halfword object owns its word and the
|
|
205
|
+
// padding beside it is not a second local.
|
|
206
|
+
//
|
|
207
|
+
// …and on a READER as far as it reads, since a read past the object copies frame bytes the
|
|
208
|
+
// source reserved and the recompile does not. `u16 buf[8]; buf[0] = x; CpuSet(buf, gDst, 8);`
|
|
209
|
+
// reads the sixteen bytes its control word names, and a DMA copy incrementing from `buf` reads
|
|
210
|
+
// every word above it; lifted as `u16 sp0`, the recompile's frame is four bytes wide and the
|
|
211
|
+
// transfer copies the saved `lr` and the caller's frame out with it. A read unbounded both ways,
|
|
212
|
+
// and a callee's write, meet this rule only beside a writer the one-object answer cannot hold
|
|
213
|
+
// (`writerNotKept`): otherwise the audit keeps the local area as one object instead.
|
|
214
|
+
//
|
|
215
|
+
// WHAT IT LEAVES, since this is the extent question the gate comment above is about: an escape
|
|
216
|
+
// is accepted only where the modelled objects and the keyed slots tile the reserved area it
|
|
217
|
+
// reaches — a word above the object is a slot (refused above), a second object (refused above),
|
|
218
|
+
// or unaccounted (refused here). That is not a wider extent model; it is the same one-scalar
|
|
219
|
+
// `extent`, made to say when it does not fit. An object of two words cannot be built here at
|
|
220
|
+
// all — the second access that would reach it is a `[+4]` the `scalar()` guard refuses — so no
|
|
221
|
+
// widening of the frame licence admits a shape this rule would then have to judge.
|
|
222
|
+
//
|
|
223
|
+
// AND IT IS THE SCALAR ARM THIS BOUNDS. An UNTYPED object is the whole DECLARED area by
|
|
224
|
+
// construction — `notTheWholeArea` accepts nothing else — and every word below the declared
|
|
225
|
+
// area is the licensed outgoing block, whose argument words the slot model keys. The object and
|
|
226
|
+
// those slots together account for every word this walk asks about, so no input makes the rule
|
|
227
|
+
// fire on that path; `accountedWords` needs its slot term for that as much as its object term.
|
|
228
|
+
// What bounds THAT path is `notTheWholeArea`'s own live clauses — a second object, an object
|
|
229
|
+
// that does not start at the bottom of the declared area, an address that reaches memory rather
|
|
230
|
+
// than a callee, and the callee's declared return — and `overSlot`, which refuses a slot keyed
|
|
231
|
+
// inside the declared area.
|
|
232
|
+
{
|
|
233
|
+
id: 'reaches-an-unaccounted-word',
|
|
234
|
+
why: 'an escape reaching a frame word no declaration covers reaches past what the recompile allocates',
|
|
235
|
+
sound: true,
|
|
236
|
+
guardedBy: [
|
|
237
|
+
'thumb-frontend.test.ts: an array whose top nothing bounds declines rather than shrinking the frame',
|
|
238
|
+
'thumb-frontend.test.ts: a copy that reads past its object into a word nothing accounts for declines',
|
|
239
|
+
],
|
|
240
|
+
rejects: (e) => e.unaccountedWord !== undefined,
|
|
241
|
+
},
|
|
242
|
+
];
|
|
243
|
+
|
|
244
|
+
/** FRAME-OBJECT AUDIT. Every `laddr` the frontend emitted is only a CLAIM that the address it
|
|
245
|
+
* names is used as "the address of one local object"; this proves it, over the finished function,
|
|
246
|
+
* the same boundary-total style as the slot-escape assert in finish(). The address may flow
|
|
247
|
+
* anywhere as a VALUE — into an MMIO register (the DMA-fill idiom), a call, a phi — and it is
|
|
248
|
+
* judged in one of two models, chosen before any shape is:
|
|
249
|
+
* - PER OBJECT, the default: every MEMORY access through it must be at offset 0, with one agreed
|
|
250
|
+
* width and one agreed extension, or a byte read or written through a runtime index into
|
|
251
|
+
* storage nothing else types, and its bytes must belong to nothing else in the frame;
|
|
252
|
+
* - ONE OBJECT, where a device reads or a callee writes without bound and every writer is one
|
|
253
|
+
* that answer can hold (`writerNotKept`; `oneObject`, which this answers and the frontend
|
|
254
|
+
* lifts again with): the local area is one `u8` array in memory, every fixed-offset access in
|
|
255
|
+
* it is a member at its own offset, a runtime index may reach a byte of it, and two widths at
|
|
256
|
+
* one byte refuse.
|
|
257
|
+
* Any use the audit cannot vouch for declines the whole function loudly. Nothing here guesses: a
|
|
258
|
+
* scalar's declared type is exactly the access type the machine used, and an object NO access
|
|
259
|
+
* reaches is sized by the frame reservation and left untyped.
|
|
260
|
+
*
|
|
261
|
+
* Takes its inputs explicitly rather than closing over `lift`. Every one of them is READ, none is
|
|
262
|
+
* reassigned, and the only mutations are to the ops reachable through `irBlocks`: an `add` of a
|
|
263
|
+
* capture and a constant is re-minted as the `laddr` it names, a moved-from capture nothing reads
|
|
264
|
+
* is dropped, a capture addressed through at fixed offsets is split into the objects its accesses
|
|
265
|
+
* name, and each surviving `laddr` is stamped with its width, signedness, count and `volatile`.
|
|
266
|
+
* One object mutates more: every access through a member is re-based onto one minted `laddr`,
|
|
267
|
+
* and the member `laddr`s are deleted or re-minted as that one moved by a `const`. An accepted
|
|
268
|
+
* function is answered with the device pin it owes, under the model it was accepted in
|
|
269
|
+
* (frontend/device-pins.ts).
|
|
270
|
+
*
|
|
271
|
+
* STAGED, and the order the stages run in is the refusal order: the first stage to refuse names
|
|
272
|
+
* the decline. Three things cross stages besides their returned records: `irBlocks`, rewritten by
|
|
273
|
+
* the fold, the split, the one-object answer and the stamp; `foldedHere`, which the split adds to
|
|
274
|
+
* and the slot-overlap refusal reads; and `ModelChoice.refused`, which the model choice and the
|
|
275
|
+
* shape stage set and the relift test reads. */
|
|
276
|
+
export function auditFrameObjects({
|
|
277
|
+
name,
|
|
278
|
+
irBlocks,
|
|
279
|
+
ownedLocals: owned,
|
|
280
|
+
declaredLocals: declared,
|
|
281
|
+
usedSlotOffsets,
|
|
282
|
+
capturedObjectIsTheWholeFrame,
|
|
283
|
+
movedCaptures,
|
|
284
|
+
returnsWithoutHiddenPointer,
|
|
285
|
+
symbols,
|
|
286
|
+
target,
|
|
287
|
+
gates = FRAME_ESCAPE_GATES,
|
|
288
|
+
oneObject,
|
|
289
|
+
}: FrameObjectAudit): FrameObjectVerdict {
|
|
290
|
+
const { laddrs: captures, foldedHere } = foldMovedCaptures(irBlocks, movedCaptures);
|
|
291
|
+
// …and the audit runs for a licensed acceptance with no object at all, so the premise re-check
|
|
292
|
+
// is total rather than resting on "the capture always survives into the IR".
|
|
293
|
+
if (captures.length === 0 && !capturedObjectIsTheWholeFrame) {
|
|
294
|
+
return undefined;
|
|
295
|
+
}
|
|
296
|
+
const fail = frameRefusal(name);
|
|
297
|
+
const facts = frameAddressFacts(irBlocks, symbols, target);
|
|
298
|
+
const { laddrs, splitRefusal } = splitAddressingCopies(irBlocks, captures, foldedHere);
|
|
299
|
+
const objects = frameObjects(laddrs, owned, fail);
|
|
300
|
+
const flow = addressFlow(irBlocks, objects, facts, fail);
|
|
301
|
+
const uses = classifyFrameUses({ irBlocks, objects, flow, facts, splitRefusal, oneObject, target, fail });
|
|
302
|
+
const windows = escapeWindows(irBlocks, uses, flow, facts, target);
|
|
303
|
+
const model = chooseFrameModel(oneObject, uses, windows, returnsWithoutHiddenPointer, fail);
|
|
304
|
+
if (capturedObjectIsTheWholeFrame) {
|
|
305
|
+
recheckWholeFramePremise({ uses, returnsWithoutHiddenPointer, fail });
|
|
306
|
+
}
|
|
307
|
+
// The device pin's trigger: the `readOnlyAddressSinks` registers a frame address was stored to.
|
|
308
|
+
const sinks = [...new Set([...uses.sourceStores.values()].flat().map((s) => s.sink))];
|
|
309
|
+
if (oneObject !== undefined) {
|
|
310
|
+
keepAsOneObject({ oneObject, irBlocks, objects, uses, usedSlotOffsets, returnsWithoutHiddenPointer, fail });
|
|
311
|
+
return { policy: 'one-object', sinks };
|
|
312
|
+
}
|
|
313
|
+
const shapes = objectShapes({
|
|
314
|
+
objects,
|
|
315
|
+
uses,
|
|
316
|
+
owned,
|
|
317
|
+
declared,
|
|
318
|
+
usedSlotOffsets,
|
|
319
|
+
returnsWithoutHiddenPointer,
|
|
320
|
+
model,
|
|
321
|
+
fail,
|
|
322
|
+
});
|
|
323
|
+
const escapes = frameEscapes({ irBlocks, uses, windows, shapes, owned, declared, usedSlotOffsets });
|
|
324
|
+
// THE ONE-OBJECT ANSWER, where it is on offer (one escape reaches without bound, and every
|
|
325
|
+
// writer is one it can hold) and the per-object model does not describe the frame: it refused a
|
|
326
|
+
// shape, an object sits over a slot, or the unbounded reach meets another object, a slot or a
|
|
327
|
+
// word nothing accounts for. Then what the device may read or the callee may write is kept
|
|
328
|
+
// rather than refused: lift again with the local area as one object in memory
|
|
329
|
+
// (`keepAsOneObject`). Below the local area are the outgoing arguments and above it the saved
|
|
330
|
+
// registers, neither of them an object. Where the per-object model does describe the frame —
|
|
331
|
+
// one object and nothing else in reach — it stands, and the object keeps its own type.
|
|
332
|
+
const answered = (e: FrameEscape): boolean =>
|
|
333
|
+
e.lo === -Infinity &&
|
|
334
|
+
e.hi === Infinity &&
|
|
335
|
+
(e.objectReached !== undefined || e.slotReached !== undefined || e.unaccountedWord !== undefined);
|
|
336
|
+
if (model.onOffer && (model.refused || shapes.overSlot.length > 0 || escapes.some(answered))) {
|
|
337
|
+
return { oneObject: { from: declared.from, to: declared.to } };
|
|
338
|
+
}
|
|
339
|
+
const failWithheld: Refuse = (why) => fail(model.withheld(why));
|
|
340
|
+
for (const [off, width] of shapes.overSlot) {
|
|
341
|
+
failIfSlotKeysIt(off, width, { usedSlotOffsets, objects, foldedHere, fail: failWithheld });
|
|
342
|
+
}
|
|
343
|
+
// RULE-MAJOR, not escape-major: every escape is asked a rule before any is asked the next, so
|
|
344
|
+
// the refusal a function reports does not turn on the order its escapes were found in.
|
|
345
|
+
for (const gate of gates) {
|
|
346
|
+
const hit = escapes.find((e) => gate.rejects(e));
|
|
347
|
+
if (hit !== undefined) {
|
|
348
|
+
(answered(hit) ? failWithheld : fail)(escapeRefusal(gate.id, hit));
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
stampObjects(objects, shapes, uses);
|
|
352
|
+
return { policy: 'per-object', sinks };
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/** Refuses the whole function, naming the address-taken local as what it cannot lift. */
|
|
356
|
+
type Refuse = (why: string) => never;
|
|
357
|
+
|
|
358
|
+
function frameRefusal(name: string): Refuse {
|
|
359
|
+
return (why) => {
|
|
360
|
+
throw new FrontendUnsupportedError(`cannot lift '${name}': address-taken stack local — ${why}`);
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/** A CAPTURE MOVED BY A CONSTANT IS THE CAPTURE AT THE SUM, and here the constant is exact: an
|
|
365
|
+
* `add` of a `laddr` and a `const` — a register the lift could not see through, `mov r0, sp /
|
|
366
|
+
* movs r2, #0 / ldrsh r1, [r0, r2]`, or a move the pre-lift walk does not follow — is re-minted
|
|
367
|
+
* as the `laddr` it names. The walk still decides which `[sp,#k]` words are routed to an object
|
|
368
|
+
* rather than keyed as slots, so a fold it did not make is remembered (`foldedHere`): an object it
|
|
369
|
+
* lands on a keyed slot is refused as the move, the capability that is missing
|
|
370
|
+
* (`failIfSlotKeysIt`).
|
|
371
|
+
*
|
|
372
|
+
* Returns every `laddr` that survives, in block order. */
|
|
373
|
+
function foldMovedCaptures(
|
|
374
|
+
irBlocks: Block[],
|
|
375
|
+
movedCaptures: ReadonlySet<Value>,
|
|
376
|
+
): { laddrs: Op[]; foldedHere: Set<Op> } {
|
|
377
|
+
const moved = new Set<Value>(movedCaptures);
|
|
378
|
+
const foldedHere = new Set<Op>();
|
|
379
|
+
const constOf = new Map<Value, number>();
|
|
380
|
+
const laddrOf = new Map<Value, Op>();
|
|
381
|
+
for (const blk of irBlocks) {
|
|
382
|
+
for (const op of blk.ops) {
|
|
383
|
+
if (op.opcode === 'const') {
|
|
384
|
+
constOf.set(op.results[0], op.attrs.value as number);
|
|
385
|
+
} else if (op.opcode === 'laddr') {
|
|
386
|
+
laddrOf.set(op.results[0], op);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
for (let changed = true; changed;) {
|
|
391
|
+
changed = false;
|
|
392
|
+
for (const blk of irBlocks) {
|
|
393
|
+
blk.ops = blk.ops.map((op) => {
|
|
394
|
+
const [x, y] = op.operands;
|
|
395
|
+
const base = laddrOf.get(x) ?? laddrOf.get(y);
|
|
396
|
+
const by = constOf.get(laddrOf.has(x) ? y : x);
|
|
397
|
+
if (op.opcode !== 'add' || op.operands.length !== 2 || base === undefined || by === undefined) {
|
|
398
|
+
return op;
|
|
399
|
+
}
|
|
400
|
+
const off = ((base.attrs.off as number) + by) | 0;
|
|
401
|
+
const object = mkOp('laddr', { results: op.results, attrs: { off } });
|
|
402
|
+
laddrOf.set(op.results[0], object);
|
|
403
|
+
moved.add(base.results[0]);
|
|
404
|
+
foldedHere.add(object);
|
|
405
|
+
changed = true;
|
|
406
|
+
return object;
|
|
407
|
+
});
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
// A capture MOVED by a constant and nothing else reads names no object — the moved one does —
|
|
411
|
+
// so it is dropped rather than judged as an object with no use, and so is a fold nothing reads:
|
|
412
|
+
// the write-back of `stmia r1!, {r2, r3, r4}` through a capture is `add r1, r1, #12`, a pointer
|
|
413
|
+
// to the end of what was copied that the machine never uses. Only those: an unused `mov rD, sp`
|
|
414
|
+
// of the machine's own is still a capture, and is judged.
|
|
415
|
+
const read = new Set<Value>();
|
|
416
|
+
for (const blk of irBlocks) {
|
|
417
|
+
for (const op of blk.ops) {
|
|
418
|
+
op.operands.forEach((v) => read.add(v));
|
|
419
|
+
(op.successors ?? []).forEach((s) => s.args.forEach((v) => read.add(v)));
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
const laddrs: Op[] = [];
|
|
423
|
+
for (const blk of irBlocks) {
|
|
424
|
+
blk.ops = blk.ops.filter(
|
|
425
|
+
(op) => op.opcode !== 'laddr' || read.has(op.results[0]) || !(moved.has(op.results[0]) || foldedHere.has(op)),
|
|
426
|
+
);
|
|
427
|
+
for (const op of blk.ops) {
|
|
428
|
+
if (op.opcode === 'laddr') {
|
|
429
|
+
laddrs.push(op);
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
return { laddrs, foldedHere };
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/** What the audit reads a value as: its defining op, its constant, the literal address it denotes,
|
|
437
|
+
* and the read-only device registers a store hands it to. Read off the IR as the fold left it, so
|
|
438
|
+
* `defOf` names no `laddr` the split mints after. */
|
|
439
|
+
interface FrameAddressFacts extends LiteralAddresses {
|
|
440
|
+
/** the registers this store hands the WHOLE address to, when each is one a device only reads
|
|
441
|
+
* through, else none */
|
|
442
|
+
readsThrough(op: Op): readonly number[];
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
function frameAddressFacts(
|
|
446
|
+
irBlocks: readonly Block[],
|
|
447
|
+
symbols: SymbolMap | undefined,
|
|
448
|
+
target: TargetDescription,
|
|
449
|
+
): FrameAddressFacts {
|
|
450
|
+
const readOnlySinks = new Set(target.capabilities.readOnlyAddressSinks ?? []);
|
|
451
|
+
const addresses = literalAddresses(irBlocks, symbols);
|
|
452
|
+
const { defOf, literalAddrOf } = addresses;
|
|
453
|
+
// The spacing of the read-only registers: a runtime index that is a multiple of it, from one
|
|
454
|
+
// of them, names one of them and never a register between — the channel stride.
|
|
455
|
+
const sinkSpacing = [...readOnlySinks].reduce((g, s, _, all) => gcd(g, s - all[0]), 0);
|
|
456
|
+
// Word stores only: a `strh` to a source register hands over half an address, so the device's
|
|
457
|
+
// source is not this object. A literal base names one register; a literal plus a runtime index —
|
|
458
|
+
// `(vu32 *)(0x40000B0 + ch * 12)`, a channel chosen at run time — names every read-only register
|
|
459
|
+
// its stride reaches, and only when that stride is a multiple of their spacing, so no index lands
|
|
460
|
+
// between them. A base this cannot resolve — computed, register-offset, merged by a phi — is the
|
|
461
|
+
// conservative answer.
|
|
462
|
+
const readsThrough = (op: Op): readonly number[] => {
|
|
463
|
+
if (readOnlySinks.size === 0 || (op.attrs.width as number) !== 4) {
|
|
464
|
+
return [];
|
|
465
|
+
}
|
|
466
|
+
const off = op.attrs.off as number;
|
|
467
|
+
const base = literalAddrOf(op.operands[0]);
|
|
468
|
+
if (base !== undefined) {
|
|
469
|
+
return readOnlySinks.has(base + off) ? [base + off] : [];
|
|
470
|
+
}
|
|
471
|
+
const d = defOf.get(op.operands[0]);
|
|
472
|
+
if (d?.opcode !== 'add' || d.operands.length !== 2 || sinkSpacing === 0) {
|
|
473
|
+
return [];
|
|
474
|
+
}
|
|
475
|
+
for (const [lit, index] of [d.operands, [...d.operands].reverse()]) {
|
|
476
|
+
const from = literalAddrOf(lit);
|
|
477
|
+
const stride = from === undefined ? 0 : factorOf(addresses, index);
|
|
478
|
+
if (from !== undefined && readOnlySinks.has(from + off) && stride > 0 && stride % sinkSpacing === 0) {
|
|
479
|
+
return [...readOnlySinks].filter((s) => (s - (from + off)) % stride === 0);
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
return [];
|
|
483
|
+
};
|
|
484
|
+
return { ...addresses, readsThrough };
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
// `v` as a sum of runtime values times constants, plus a constant (the `1` key): agbcc's `ch *
|
|
488
|
+
// 12` is `((ch << 1) + ch) << 2`, which is 12 times `ch` only once the two `ch` terms are added.
|
|
489
|
+
// Undefined past what this can read.
|
|
490
|
+
function linearOf(facts: LiteralAddresses, v: Value, depth = 0): Map<Value | 1, number> | undefined {
|
|
491
|
+
const c = facts.constOf(v);
|
|
492
|
+
if (c !== undefined) {
|
|
493
|
+
return new Map([[1, c]]);
|
|
494
|
+
}
|
|
495
|
+
const d = facts.defOf.get(v);
|
|
496
|
+
if (d === undefined || depth > 8) {
|
|
497
|
+
return new Map([[v, 1]]);
|
|
498
|
+
}
|
|
499
|
+
const scaled = (x: Value, by: number) => {
|
|
500
|
+
const l = linearOf(facts, x, depth + 1);
|
|
501
|
+
return l && new Map([...l].map(([t, k]) => [t, k * by] as const));
|
|
502
|
+
};
|
|
503
|
+
const k = d.operands.length === 1 ? (d.attrs.imm as number | undefined) : facts.constOf(d.operands[1]);
|
|
504
|
+
if (d.opcode === 'shl' && k !== undefined && k >= 0 && k < 31) {
|
|
505
|
+
return scaled(d.operands[0], 2 ** k);
|
|
506
|
+
}
|
|
507
|
+
if (d.opcode === 'mul' && d.operands.length === 2 && k !== undefined) {
|
|
508
|
+
return scaled(d.operands[0], k);
|
|
509
|
+
}
|
|
510
|
+
if ((d.opcode === 'add' || d.opcode === 'sub') && d.operands.length === 2) {
|
|
511
|
+
const x = linearOf(facts, d.operands[0], depth + 1);
|
|
512
|
+
const y = scaled(d.operands[1], d.opcode === 'add' ? 1 : -1);
|
|
513
|
+
if (x === undefined || y === undefined) {
|
|
514
|
+
return undefined;
|
|
515
|
+
}
|
|
516
|
+
y.forEach((n, t) => x.set(t, (x.get(t) ?? 0) + n));
|
|
517
|
+
return x;
|
|
518
|
+
}
|
|
519
|
+
return new Map([[v, 1]]);
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
// A number every value of `v` is a multiple of, or 0 when this cannot name one.
|
|
523
|
+
function factorOf(facts: LiteralAddresses, v: Value): number {
|
|
524
|
+
const l = linearOf(facts, v);
|
|
525
|
+
return l === undefined ? 0 : [...l.values()].reduce(gcd, 0);
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
/** A FRAME BASE ADDRESSED THROUGH IS NOT A CAPTURE. Thumb-1 gives `ldr`/`str` an `[sp,#imm]`
|
|
529
|
+
* encoding and gives the sub-word forms none, so a byte or halfword spill can only be spelled
|
|
530
|
+
* by copying sp into a register and addressing through the copy:
|
|
531
|
+
*
|
|
532
|
+
* mov r2, sp
|
|
533
|
+
* strh r3, [r2, #0x30]
|
|
534
|
+
*
|
|
535
|
+
* That is an ADDRESSING MODE. The copy never becomes a value, and the access is the
|
|
536
|
+
* `[sp,#0x30]` the instruction set cannot spell — so what the machine named is one object at
|
|
537
|
+
* frame offset 48, not a `[+48]` reach through the frame base.
|
|
538
|
+
*
|
|
539
|
+
* A captured address whose every use is a fixed-offset sub-word ACCESS is that shape, and
|
|
540
|
+
* each of its accesses names its own object: re-root them onto a `laddr` at their own offset,
|
|
541
|
+
* read at 0, and the rest of this audit judges the objects. A capture with ANY other use is a
|
|
542
|
+
* real capture and keeps the frame base.
|
|
543
|
+
*
|
|
544
|
+
* What makes that judgement total is that the walk below enumerates every ROLE a value can
|
|
545
|
+
* appear in — every operand of every op, and every edge argument — instead of asking what an
|
|
546
|
+
* instruction looks like. One instruction can hold two roles: `str rD, [rD, #k]` stores the
|
|
547
|
+
* frame address through itself, a base use AND an escape, and the escape is what stops the
|
|
548
|
+
* split.
|
|
549
|
+
*
|
|
550
|
+
* Returns the `laddr`s left to judge, and why a capture was NOT split, when the reason is one no
|
|
551
|
+
* later message carries — the `slotsOffReason` idiom: a refusal reported as the wrong capability
|
|
552
|
+
* sends the improvement loop to build the wrong thing. Per CAPTURE and not per offset: every
|
|
553
|
+
* direct `[sp,#k]` word access is a capture of its own at k, refused here for its width, and a
|
|
554
|
+
* different capture at k whose access is refused for another reason must not be handed that one. */
|
|
555
|
+
function splitAddressingCopies(
|
|
556
|
+
irBlocks: Block[],
|
|
557
|
+
laddrs: Op[],
|
|
558
|
+
foldedHere: Set<Op>,
|
|
559
|
+
): { laddrs: Op[]; splitRefusal: Map<Value, string> } {
|
|
560
|
+
const splitRefusal = new Map<Value, string>();
|
|
561
|
+
const uses = new Map<Value, { op: Op; idx: number; blk: Block }[]>();
|
|
562
|
+
const record = (v: Value, op: Op, idx: number, blk: Block) =>
|
|
563
|
+
(uses.get(v) ?? uses.set(v, []).get(v)!).push({ op, idx, blk });
|
|
564
|
+
for (const blk of irBlocks) {
|
|
565
|
+
for (const op of blk.ops) {
|
|
566
|
+
op.operands.forEach((v, idx) => record(v, op, idx, blk));
|
|
567
|
+
// An EDGE ARGUMENT is a use role too, and never an access: a capture that reaches a
|
|
568
|
+
// block parameter is live past this block, so the taint closure (`addressFlow`) is what
|
|
569
|
+
// judges it. Recorded at index -1 so it can never be counted as an access — a split there
|
|
570
|
+
// would delete a capture the successor argument still names.
|
|
571
|
+
for (const succ of op.successors ?? []) {
|
|
572
|
+
for (const a of succ.args) {
|
|
573
|
+
record(a, op, -1, blk);
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
const minted: Op[] = [];
|
|
579
|
+
const consumed = new Set<Op>();
|
|
580
|
+
for (const capture of laddrs) {
|
|
581
|
+
const at = uses.get(capture.results[0]) ?? [];
|
|
582
|
+
const accesses = at.filter((u) => (u.op.opcode === 'load' || u.op.opcode === 'store') && u.idx === 0);
|
|
583
|
+
// SUB-WORD ONLY, because that is the whole of what the encoding gap forces: `ldr`/`str` DO
|
|
584
|
+
// have an `[sp,#imm]` form, so a WORD access through a copy is some other shape and must
|
|
585
|
+
// not be read as this one. It is also what keeps the outgoing-argument area safe — that
|
|
586
|
+
// guard reads `[sp,#k]` accesses (spMemAccess), which an access through a copy is not, and
|
|
587
|
+
// agbcc stages arguments 5+ there with `str`.
|
|
588
|
+
const subWord = accesses.every((u) => (u.op.attrs.width as number) < 4);
|
|
589
|
+
// A use that is not an access leaves the capture naming the frame base, and the judgement
|
|
590
|
+
// after reports that use itself — an escape, a phi, arithmetic — so it needs no reason
|
|
591
|
+
// here. The WIDTH does: nothing downstream mentions it, so a refused word access would be
|
|
592
|
+
// reported as "a store at [+4]" and the histogram would be asked for the wrong capability.
|
|
593
|
+
if (at.length === 0 || accesses.length !== at.length) {
|
|
594
|
+
continue;
|
|
595
|
+
}
|
|
596
|
+
if (!subWord) {
|
|
597
|
+
splitRefusal.set(capture.results[0], 'a WORD access through the copy, and `ldr`/`str` have an `[sp,#imm]` form');
|
|
598
|
+
continue;
|
|
599
|
+
}
|
|
600
|
+
// Nothing to split when the capture already names ONE object: every access at offset 0 is
|
|
601
|
+
// the frame base itself, which is what the DMA-fill idiom captures.
|
|
602
|
+
if (accesses.every((u) => u.op.attrs.off === 0)) {
|
|
603
|
+
continue;
|
|
604
|
+
}
|
|
605
|
+
for (const u of accesses) {
|
|
606
|
+
const res = mkValue(T.unk(32));
|
|
607
|
+
const at = (capture.attrs.off as number) + (u.op.attrs.off as number);
|
|
608
|
+
const object = mkOp('laddr', { results: [res], attrs: { off: at } });
|
|
609
|
+
u.blk.ops.splice(u.blk.ops.indexOf(u.op), 0, object);
|
|
610
|
+
minted.push(object);
|
|
611
|
+
if (foldedHere.has(capture)) {
|
|
612
|
+
foldedHere.add(object);
|
|
613
|
+
}
|
|
614
|
+
// The ADDRESS operand only — the stored value (operand 1) is passed through
|
|
615
|
+
// untouched, so no slot home moves (ir/core.ts `SlotHomes`). These accesses go through
|
|
616
|
+
// a COPY of `sp` rather than the `[sp,#k]` keys the stamp reads, so none of them
|
|
617
|
+
// carried one to begin with.
|
|
618
|
+
u.op.operands = [res, ...u.op.operands.slice(1)];
|
|
619
|
+
u.op.attrs = { ...u.op.attrs, off: 0 };
|
|
620
|
+
}
|
|
621
|
+
consumed.add(capture);
|
|
622
|
+
}
|
|
623
|
+
if (consumed.size === 0) {
|
|
624
|
+
return { laddrs, splitRefusal };
|
|
625
|
+
}
|
|
626
|
+
for (const blk of irBlocks) {
|
|
627
|
+
blk.ops = blk.ops.filter((op) => !consumed.has(op));
|
|
628
|
+
}
|
|
629
|
+
return { laddrs: [...laddrs.filter((op) => !consumed.has(op)), ...minted], splitRefusal };
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
/** ONE OBJECT PER FRAME OFFSET. Two `laddr` at the same offset name the same storage; two at
|
|
633
|
+
* different offsets are different objects, so width, signedness, escape and the overlap
|
|
634
|
+
* window are decided per offset — one width shared by every capture in the function would
|
|
635
|
+
* declare a halfword spill and a word spill as one object. */
|
|
636
|
+
function frameObjects(laddrs: readonly Op[], owned: FrameRange, fail: Refuse): Map<number, Op[]> {
|
|
637
|
+
const objects = new Map<number, Op[]>();
|
|
638
|
+
for (const op of laddrs) {
|
|
639
|
+
const off = op.attrs.off as number;
|
|
640
|
+
(objects.get(off) ?? objects.set(off, []).get(off)!).push(op);
|
|
641
|
+
}
|
|
642
|
+
// A LOCAL STARTS INSIDE THE RESERVED AREA. Above it are the registers the prologue saved and
|
|
643
|
+
// then the caller's outgoing block, so an address there is an incoming stack argument's or a
|
|
644
|
+
// saved register's — `g(&e)` for a fifth parameter `e` is `add r0, sp, #0x8` over a one-word
|
|
645
|
+
// local area — and no other rule of the audit is about that gap. The TOP itself is also C's
|
|
646
|
+
// one-past-the-end pointer, `g(buf, buf + 8)` over an 8-byte array, and the refusal names both
|
|
647
|
+
// readings.
|
|
648
|
+
for (const off of objects.keys()) {
|
|
649
|
+
if (off === owned.to) {
|
|
650
|
+
fail(
|
|
651
|
+
`the captured address at [sp,#${off}) is the top of the reserved local area of ${owned.to - owned.from} bytes — ` +
|
|
652
|
+
'one past the end of a local array, or an incoming stack argument, neither of which is modelled',
|
|
653
|
+
);
|
|
654
|
+
}
|
|
655
|
+
if (off > owned.to) {
|
|
656
|
+
fail(
|
|
657
|
+
`the captured address at [sp,#${off}) is above the reserved local area of ${owned.to - owned.from} bytes — ` +
|
|
658
|
+
'an incoming stack argument or a saved register, whose address is not modelled',
|
|
659
|
+
);
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
return objects;
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
/** Where each frame address may flow: `taint`, the object a value may hold the address of on SOME
|
|
666
|
+
* path, and `frameOnEveryPath`, the values that hold a frame address on EVERY path. */
|
|
667
|
+
interface AddressFlow {
|
|
668
|
+
readonly taint: ReadonlyMap<Value, number>;
|
|
669
|
+
readonly frameOnEveryPath: ReadonlySet<Value>;
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
function addressFlow(
|
|
673
|
+
irBlocks: readonly Block[],
|
|
674
|
+
objects: ReadonlyMap<number, readonly Op[]>,
|
|
675
|
+
facts: LiteralAddresses,
|
|
676
|
+
fail: Refuse,
|
|
677
|
+
): AddressFlow {
|
|
678
|
+
const { defOf, constOf } = facts;
|
|
679
|
+
// Taint maps a value to the OBJECT whose address it may hold, closed over phis: a tainted
|
|
680
|
+
// edge arg taints the receiving block param. A phi that merges two objects has no single
|
|
681
|
+
// answer, and picking one would put an access on the wrong storage: `get(c ? &a : &b)` is
|
|
682
|
+
// `add r0, sp, #0x4 / beq / add r0, sp, #0x8 / bl get`, and refuses here.
|
|
683
|
+
const taint = new Map<Value, number>();
|
|
684
|
+
for (const [off, ops] of objects) {
|
|
685
|
+
for (const op of ops) {
|
|
686
|
+
taint.set(op.results[0], off);
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
for (let changed = true; changed;) {
|
|
690
|
+
changed = false;
|
|
691
|
+
for (const blk of irBlocks) {
|
|
692
|
+
for (const op of blk.ops) {
|
|
693
|
+
for (const s of op.successors ?? []) {
|
|
694
|
+
s.args.forEach((arg, i) => {
|
|
695
|
+
const from = taint.get(arg);
|
|
696
|
+
const param = s.block.params[i];
|
|
697
|
+
if (from === undefined || param === undefined) {
|
|
698
|
+
return;
|
|
699
|
+
}
|
|
700
|
+
const had = taint.get(param);
|
|
701
|
+
if (had === from) {
|
|
702
|
+
return;
|
|
703
|
+
}
|
|
704
|
+
if (had !== undefined) {
|
|
705
|
+
fail(`a phi merges the frame objects at [sp,#${had}] and [sp,#${from}] — one value, two objects`);
|
|
706
|
+
}
|
|
707
|
+
taint.set(param, from);
|
|
708
|
+
changed = true;
|
|
709
|
+
});
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
// …and the values that hold a frame address on EVERY path, where `taint` answers "on some
|
|
715
|
+
// path": a `laddr`, a phi whose every incoming value is one, or one of those moved by a
|
|
716
|
+
// constant. `p = cnt ? cnt : &o` is tainted and not in here: it may be `cnt`, which may be
|
|
717
|
+
// anything, and reading it as this frame is how a store through it goes unseen.
|
|
718
|
+
const frameOnEveryPath = new Set<Value>(taint.keys());
|
|
719
|
+
const incoming = new Map<Value, Value[]>();
|
|
720
|
+
for (const blk of irBlocks) {
|
|
721
|
+
for (const op of blk.ops) {
|
|
722
|
+
if (op.opcode === 'add' && op.operands.length === 2 && op.operands.some((v) => constOf(v) !== undefined)) {
|
|
723
|
+
frameOnEveryPath.add(op.results[0]);
|
|
724
|
+
}
|
|
725
|
+
for (const s of op.successors ?? []) {
|
|
726
|
+
s.args.forEach((arg, i) => {
|
|
727
|
+
const param = s.block.params[i];
|
|
728
|
+
if (param !== undefined) {
|
|
729
|
+
(incoming.get(param) ?? incoming.set(param, []).get(param)!).push(arg);
|
|
730
|
+
}
|
|
731
|
+
});
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
const objectValues = new Set([...objects.values()].flat().map((op) => op.results[0]));
|
|
736
|
+
const holdsFrame = (v: Value): boolean => {
|
|
737
|
+
const d = defOf.get(v);
|
|
738
|
+
if (objectValues.has(v)) {
|
|
739
|
+
return true;
|
|
740
|
+
}
|
|
741
|
+
if (d?.opcode === 'add') {
|
|
742
|
+
return d.operands.some((x) => constOf(x) === undefined && frameOnEveryPath.has(x));
|
|
743
|
+
}
|
|
744
|
+
const ins = incoming.get(v);
|
|
745
|
+
return d === undefined && ins !== undefined && ins.every((a) => frameOnEveryPath.has(a));
|
|
746
|
+
};
|
|
747
|
+
for (let changed = true; changed;) {
|
|
748
|
+
changed = false;
|
|
749
|
+
for (const v of frameOnEveryPath) {
|
|
750
|
+
if (!holdsFrame(v)) {
|
|
751
|
+
frameOnEveryPath.delete(v);
|
|
752
|
+
changed = true;
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
return { taint, frameOnEveryPath };
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
/** One fixed-offset access at an object's own address. */
|
|
760
|
+
interface FrameAccess {
|
|
761
|
+
readonly width: number;
|
|
762
|
+
readonly signed: boolean;
|
|
763
|
+
readonly isLoad: boolean;
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
/** A load or store judged an access to the object at `off`, and the address value it reaches the
|
|
767
|
+
* object through: a capture, or a phi that carries one — for a runtime index, the indexed one. */
|
|
768
|
+
interface FrameAccessOp {
|
|
769
|
+
readonly off: number;
|
|
770
|
+
readonly through: Value;
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
/** Every use of every frame object, by offset — what each later stage reads. Each field is
|
|
774
|
+
* declared, with the question it answers, where `classifyFrameUses` builds it. */
|
|
775
|
+
interface FrameUses {
|
|
776
|
+
readonly accesses: ReadonlyMap<number, readonly FrameAccess[]>;
|
|
777
|
+
readonly accessOps: ReadonlyMap<Op, FrameAccessOp>;
|
|
778
|
+
readonly escaped: ReadonlySet<number>;
|
|
779
|
+
readonly sourceStores: ReadonlyMap<number, readonly { readonly op: Op; readonly sink: number }[]>;
|
|
780
|
+
readonly calleeReads: ReadonlyMap<number, { readonly lo: number; readonly hi: number }>;
|
|
781
|
+
readonly filledFrom: ReadonlySet<number>;
|
|
782
|
+
readonly passedToCallee: ReadonlySet<number>;
|
|
783
|
+
readonly passedToWriter: ReadonlySet<number>;
|
|
784
|
+
readonly published: ReadonlySet<number>;
|
|
785
|
+
readonly publishedOutward: ReadonlySet<number>;
|
|
786
|
+
readonly publishedToWriter: ReadonlySet<number>;
|
|
787
|
+
readonly arg0Callees: ReadonlyMap<number, ReadonlySet<string | null>>;
|
|
788
|
+
readonly returnTemps: ReadonlyMap<number, readonly Op[]>;
|
|
789
|
+
readonly useCount: ReadonlyMap<number, number>;
|
|
790
|
+
readonly indexed: ReadonlyMap<number, readonly { readonly width: number; readonly signed: boolean }[]>;
|
|
791
|
+
readonly members: readonly { readonly at: number; readonly width: number }[];
|
|
792
|
+
readonly memberRefusal: string | undefined;
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
/** Did the address at `off` reach something that may write the frame back: a callee that is not a
|
|
796
|
+
* bounded block transfer reading through it, or memory a writer may hold. */
|
|
797
|
+
function mayWrite(uses: FrameUses, off: number): boolean {
|
|
798
|
+
return uses.passedToWriter.has(off) || uses.publishedToWriter.has(off);
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
/** Judges every use of a tainted value, against the object it names. */
|
|
802
|
+
function classifyFrameUses({
|
|
803
|
+
irBlocks,
|
|
804
|
+
objects,
|
|
805
|
+
flow: { taint, frameOnEveryPath },
|
|
806
|
+
facts,
|
|
807
|
+
splitRefusal,
|
|
808
|
+
oneObject,
|
|
809
|
+
target,
|
|
810
|
+
fail,
|
|
811
|
+
}: {
|
|
812
|
+
irBlocks: readonly Block[];
|
|
813
|
+
objects: ReadonlyMap<number, readonly Op[]>;
|
|
814
|
+
flow: AddressFlow;
|
|
815
|
+
facts: FrameAddressFacts;
|
|
816
|
+
splitRefusal: ReadonlyMap<Value, string>;
|
|
817
|
+
oneObject: FrameRange | undefined;
|
|
818
|
+
target: TargetDescription;
|
|
819
|
+
fail: Refuse;
|
|
820
|
+
}): FrameUses {
|
|
821
|
+
const { defOf } = facts;
|
|
822
|
+
const accesses = new Map<number, FrameAccess[]>();
|
|
823
|
+
// …and every load and store judged an access, by the object it reaches: through a capture, a
|
|
824
|
+
// phi that carries one, or a runtime index into one. A later stage that rewrites or qualifies
|
|
825
|
+
// "the accesses to the object" reads this list rather than recognizing them again, since a
|
|
826
|
+
// narrower recognizer misses exactly the phi-carried ones `taint` admits.
|
|
827
|
+
const accessOps = new Map<Op, FrameAccessOp>();
|
|
828
|
+
// TWO QUESTIONS, not one. `escaped` asks whether the address LEFT the function, in the order the
|
|
829
|
+
// escapes were found — the order windows and refusals are read in. Whether it reached something
|
|
830
|
+
// that could write the frame BACK — what every "a callee may write any frame offset" refusal
|
|
831
|
+
// rests on — is `mayWrite`, read off the splits below. A store into a device's SOURCE register
|
|
832
|
+
// answers yes to the first and no to the second: the hardware reads the object, and the
|
|
833
|
+
// DMA-fill idiom this capability was built for (`vu16 tmp; DmaSet(n, &tmp, …)`) is exactly
|
|
834
|
+
// that shape.
|
|
835
|
+
const escaped = new Set<number>();
|
|
836
|
+
// …and for the others, the stores that handed the address to a source register, which is
|
|
837
|
+
// where `readWindow` reads how far the device reads
|
|
838
|
+
const sourceStores = new Map<number, { op: Op; sink: number }[]>();
|
|
839
|
+
// …and a CALL that only reads is the same answer: the address handed to a block transfer
|
|
840
|
+
// (`blockTransferCalls`) as its source, with a literal control word, is read `[lo, hi)` from
|
|
841
|
+
// the object and written by nobody. `CPU_FILL`'s `vu32 tmp = v; CpuSet(&tmp, dest, …)` is the
|
|
842
|
+
// shape. A control word this cannot read, or any other argument position, is a callee that
|
|
843
|
+
// may write.
|
|
844
|
+
const calleeReads = new Map<number, { lo: number; hi: number }>();
|
|
845
|
+
// …and of those, the objects a transfer FILLS from — a fixed source, the `tmp` of every fill
|
|
846
|
+
// macro — which the `volatile` stamp keys on beside `published`
|
|
847
|
+
const filledFrom = new Set<number>();
|
|
848
|
+
// …and the two escapes SPLIT, because each decides something the other does not.
|
|
849
|
+
// `passedToCallee` is the address handed to a callee as an argument — the one escape whose
|
|
850
|
+
// writer this frontend can name, which is what the struct-return premise re-check rests on, and
|
|
851
|
+
// what tells a refusal message which escape it is talking about. `published` is the address
|
|
852
|
+
// WRITTEN TO MEMORY, how the DMA idiom hands the object to hardware, and what `volatile` at the
|
|
853
|
+
// stamp keys on. Reading either off `escaped` gets the other one wrong.
|
|
854
|
+
const passedToCallee = new Set<number>();
|
|
855
|
+
// …and of those, the callees that may WRITE through it: every one but a bounded block transfer
|
|
856
|
+
// reading its source (`calleeReads`), which names no writer
|
|
857
|
+
const passedToWriter = new Set<number>();
|
|
858
|
+
const published = new Set<number>();
|
|
859
|
+
// …and `published` SPLITS AGAIN, because it answers two questions of different strengths and
|
|
860
|
+
// the weaker one may not be read as the stronger. "Did the address reach memory at all" is
|
|
861
|
+
// what `volatile` keys on: a halfword of it written anywhere is still a write this function
|
|
862
|
+
// does not own, and the qualifier has to survive it. "Did the address reach memory OUTSIDE
|
|
863
|
+
// this frame, whole" is what the licence is re-proven against (`recheckWholeFramePremise`), and
|
|
864
|
+
// it is strictly narrower — a store back into the object's own bytes publishes the address to
|
|
865
|
+
// nobody, and half an address is not the address. The pre-lift scan that grants the licence
|
|
866
|
+
// (`frameBasePublishedToMemory`) admits exactly the narrow one, so the re-proof must too: an
|
|
867
|
+
// audit that re-proves a WEAKER premise than the licence it audits is not a containment, it
|
|
868
|
+
// is a second, wider door into the same acceptance.
|
|
869
|
+
const publishedOutward = new Set<number>();
|
|
870
|
+
// …and the third question `published` answers, of what HOLDS the address it wrote: anything but
|
|
871
|
+
// a device's read-only source register (`facts.readsThrough`) may write through it — an
|
|
872
|
+
// interrupt reading a global, a device writing its destination — at any point in this function,
|
|
873
|
+
// so a re-read of the object is a read the recompile must make too.
|
|
874
|
+
const publishedToWriter = new Set<number>();
|
|
875
|
+
// …and WHICH CALLEE took it at ARGUMENT 0, because that is the one position a hidden
|
|
876
|
+
// struct-return pointer can occupy and that callee's declared RETURN TYPE is the one fact that
|
|
877
|
+
// tells an out-parameter from one. A `call`'s operand index IS the argument index here (the
|
|
878
|
+
// `bl` arm reads r0..r<argc-1> in order), so an address that appears in no entry of this map
|
|
879
|
+
// was handed over at r1 or above every time — an argument the source wrote. `null` is the
|
|
880
|
+
// narrowing of an unstamped `target` attr — the `bl`/`blx` lowering always stamps one — and
|
|
881
|
+
// reads as a callee nothing can be declared about, so it refuses.
|
|
882
|
+
const arg0Callees = new Map<number, Set<string | null>>();
|
|
883
|
+
// …except where the call's own declaration says argument 0 IS the hidden pointer (the frontend
|
|
884
|
+
// stamps `sret` on a call to a callee declared to return a struct through memory): that object
|
|
885
|
+
// is the callee's return storage, a local of the declared type. `useCount` counts every use of
|
|
886
|
+
// each object, so a return temp this function does anything else with is told apart.
|
|
887
|
+
const returnTemps = new Map<number, Op[]>();
|
|
888
|
+
const useCount = new Map<number, number>();
|
|
889
|
+
// The accesses through a runtime-indexed address, per object — kept apart from `accesses`,
|
|
890
|
+
// which types the object at its own offset: `a[i]` says what one ELEMENT is, not what sits at
|
|
891
|
+
// `a`.
|
|
892
|
+
const indexed = new Map<number, { width: number; signed: boolean }[]>();
|
|
893
|
+
for (const off of objects.keys()) {
|
|
894
|
+
accesses.set(off, []);
|
|
895
|
+
}
|
|
896
|
+
// …and with `oneObject`, every fixed-offset access as the frame bytes it touches, since then an
|
|
897
|
+
// access at [+k] through a capture is a member of the one object rather than a second object
|
|
898
|
+
const members: { at: number; width: number }[] = [];
|
|
899
|
+
// …and without it, the first such access, which the per-object model refuses once the model is
|
|
900
|
+
// chosen (`chooseFrameModel`)
|
|
901
|
+
let memberRefusal: string | undefined;
|
|
902
|
+
const scalar = (kind: string, op: Op, off: number, v: Value): void => {
|
|
903
|
+
if (oneObject !== undefined) {
|
|
904
|
+
members.push({ at: off + (op.attrs.off as number), width: op.attrs.width as number });
|
|
905
|
+
return;
|
|
906
|
+
}
|
|
907
|
+
if ((op.attrs.off as number) !== 0) {
|
|
908
|
+
memberRefusal ??=
|
|
909
|
+
`a ${kind} at [+${op.attrs.off}] through the captured address — ` +
|
|
910
|
+
(splitRefusal.get(v) ?? 'only a scalar at the captured address is modelled');
|
|
911
|
+
}
|
|
912
|
+
};
|
|
913
|
+
for (const blk of irBlocks) {
|
|
914
|
+
for (const op of blk.ops) {
|
|
915
|
+
for (const [idx, v] of op.operands.entries()) {
|
|
916
|
+
const off = taint.get(v);
|
|
917
|
+
if (off === undefined) {
|
|
918
|
+
continue;
|
|
919
|
+
}
|
|
920
|
+
useCount.set(off, (useCount.get(off) ?? 0) + 1);
|
|
921
|
+
if (op.opcode === 'load' && idx === 0) {
|
|
922
|
+
scalar('load', op, off, v);
|
|
923
|
+
accessOps.set(op, { off, through: v });
|
|
924
|
+
accesses.get(off)!.push({
|
|
925
|
+
width: op.attrs.width as number,
|
|
926
|
+
signed: (op.attrs.signed as boolean) ?? false,
|
|
927
|
+
isLoad: true,
|
|
928
|
+
});
|
|
929
|
+
continue;
|
|
930
|
+
}
|
|
931
|
+
if (op.opcode === 'store' && idx === 0) {
|
|
932
|
+
scalar('store', op, off, v);
|
|
933
|
+
accessOps.set(op, { off, through: v });
|
|
934
|
+
accesses.get(off)!.push({ width: op.attrs.width as number, signed: false, isLoad: false });
|
|
935
|
+
continue;
|
|
936
|
+
}
|
|
937
|
+
if ((op.opcode === 'store' && idx === 1) || op.opcode === 'call') {
|
|
938
|
+
escaped.add(off); // the address ESCAPES as a value — the point of the capability
|
|
939
|
+
const handedTo = op.opcode === 'store' ? facts.readsThrough(op) : [];
|
|
940
|
+
const read = op.opcode === 'call' ? transferRead(op, idx, off, target, facts) : undefined;
|
|
941
|
+
if (read !== undefined) {
|
|
942
|
+
const had = calleeReads.get(off) ?? read;
|
|
943
|
+
calleeReads.set(off, { lo: Math.min(had.lo, read.lo), hi: Math.max(had.hi, read.hi) });
|
|
944
|
+
if (read.fill) {
|
|
945
|
+
filledFrom.add(off);
|
|
946
|
+
}
|
|
947
|
+
} else if (handedTo.length === 0) {
|
|
948
|
+
if (op.opcode === 'store') {
|
|
949
|
+
publishedToWriter.add(off);
|
|
950
|
+
}
|
|
951
|
+
} else {
|
|
952
|
+
const stores = sourceStores.get(off) ?? sourceStores.set(off, []).get(off)!;
|
|
953
|
+
handedTo.forEach((sink) => stores.push({ op, sink }));
|
|
954
|
+
}
|
|
955
|
+
if (op.opcode === 'call') {
|
|
956
|
+
passedToCallee.add(off);
|
|
957
|
+
if (read === undefined) {
|
|
958
|
+
passedToWriter.add(off);
|
|
959
|
+
}
|
|
960
|
+
if (idx === 0 && op.attrs.sret === true) {
|
|
961
|
+
(returnTemps.get(off) ?? returnTemps.set(off, []).get(off)!).push(op);
|
|
962
|
+
} else if (idx === 0) {
|
|
963
|
+
const t = op.attrs.target;
|
|
964
|
+
const cs = arg0Callees.get(off) ?? new Set<string | null>();
|
|
965
|
+
cs.add(typeof t === 'string' ? t : null);
|
|
966
|
+
arg0Callees.set(off, cs);
|
|
967
|
+
}
|
|
968
|
+
} else {
|
|
969
|
+
published.add(off); // written to memory — the DMA idiom's `*dmaReg = &tmp`
|
|
970
|
+
// The licence's two conditions, asked of the IR: the WHOLE address (a word), and a
|
|
971
|
+
// destination that is not this frame. `taint` answers the second exactly — it holds
|
|
972
|
+
// every value that may carry one of this function's frame addresses, which is both
|
|
973
|
+
// spellings the pre-lift scan excludes by name (`str rS, [sp]` and a store through
|
|
974
|
+
// another held capture, since at a one-word frame [sp,#0] IS the object).
|
|
975
|
+
if ((op.attrs.width as number) === 4 && taint.get(op.operands[0]) === undefined) {
|
|
976
|
+
publishedOutward.add(off);
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
continue;
|
|
980
|
+
}
|
|
981
|
+
// A RUNTIME INDEX into the object: `mov r1, sp / add r0, r1, r4 / ldrb r0, [r0]` is
|
|
982
|
+
// agbcc's `a[i]` on a `u8 a[n]` local. The sum names no fixed offset, so it is not a
|
|
983
|
+
// capture of its own; what it may do is judged here, whole, and the object it indexes is
|
|
984
|
+
// judged with the rest (`objectShapes`).
|
|
985
|
+
const other = op.opcode === 'add' && op.operands.length === 2 ? op.operands[1 - idx] : undefined;
|
|
986
|
+
if (other !== undefined && taint.get(other) === undefined && defOf.get(other)?.opcode !== 'const') {
|
|
987
|
+
indexedAccess(irBlocks, { indexed, accessOps }, { off, through: v }, op.results[0], fail);
|
|
988
|
+
continue;
|
|
989
|
+
}
|
|
990
|
+
// A CONSTANT move left after the fold (`foldMovedCaptures`) moves a capture a phi carried.
|
|
991
|
+
// When the phi also carries a pointer from outside the frame — `if (!info) info = &local;
|
|
992
|
+
// info->x` — the missing capability is that merge, not a walk through the frame.
|
|
993
|
+
if (other !== undefined && defOf.get(other)?.opcode === 'const' && !frameOnEveryPath.has(v)) {
|
|
994
|
+
fail(
|
|
995
|
+
`the captured address at [sp,#${off}) reaches a phi that merges it with a pointer from outside the ` +
|
|
996
|
+
'frame, and is then moved by a constant — a field of what may or may not be a local is not modelled',
|
|
997
|
+
);
|
|
998
|
+
}
|
|
999
|
+
// …and when every value the phi carries is this frame's, it is a pointer stepped through
|
|
1000
|
+
// the frame, which names a different object on each trip.
|
|
1001
|
+
if (other !== undefined && defOf.get(other)?.opcode === 'const') {
|
|
1002
|
+
fail(
|
|
1003
|
+
`the captured address at [sp,#${off}) reaches a phi and is then moved by a constant — ` +
|
|
1004
|
+
'a pointer stepped through the frame names no one object',
|
|
1005
|
+
);
|
|
1006
|
+
}
|
|
1007
|
+
fail(`the captured address flows into \`${op.opcode}\` — not an access, an escape, or a phi`);
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
return {
|
|
1012
|
+
accesses,
|
|
1013
|
+
accessOps,
|
|
1014
|
+
escaped,
|
|
1015
|
+
sourceStores,
|
|
1016
|
+
calleeReads,
|
|
1017
|
+
filledFrom,
|
|
1018
|
+
passedToCallee,
|
|
1019
|
+
passedToWriter,
|
|
1020
|
+
published,
|
|
1021
|
+
publishedOutward,
|
|
1022
|
+
publishedToWriter,
|
|
1023
|
+
arg0Callees,
|
|
1024
|
+
returnTemps,
|
|
1025
|
+
useCount,
|
|
1026
|
+
indexed,
|
|
1027
|
+
members,
|
|
1028
|
+
memberRefusal,
|
|
1029
|
+
};
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
// The bytes a block transfer that took the address at `idx` reads, when `idx` is its source and
|
|
1033
|
+
// its control word is a literal.
|
|
1034
|
+
function transferRead(
|
|
1035
|
+
op: Op,
|
|
1036
|
+
idx: number,
|
|
1037
|
+
off: number,
|
|
1038
|
+
target: TargetDescription,
|
|
1039
|
+
facts: LiteralAddresses,
|
|
1040
|
+
): { lo: number; hi: number; fill: boolean } | undefined {
|
|
1041
|
+
const callee = op.attrs.target;
|
|
1042
|
+
const calls = target.capabilities.blockTransferCalls;
|
|
1043
|
+
const call = typeof callee === 'string' && calls && Object.hasOwn(calls, callee) ? calls[callee] : undefined;
|
|
1044
|
+
const control = call === undefined || idx !== call.source ? undefined : op.operands[call.control];
|
|
1045
|
+
const word = control === undefined ? undefined : facts.constOf(control);
|
|
1046
|
+
if (call === undefined || word === undefined) {
|
|
1047
|
+
return undefined;
|
|
1048
|
+
}
|
|
1049
|
+
const read = blockTransferRead(call, word);
|
|
1050
|
+
return { ...sourceReach(read, off), fill: read.walk === 'fixed' };
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
// Records every access through `sum`, a runtime index into the object at `off`.
|
|
1054
|
+
function indexedAccess(
|
|
1055
|
+
irBlocks: readonly Block[],
|
|
1056
|
+
{
|
|
1057
|
+
indexed,
|
|
1058
|
+
accessOps,
|
|
1059
|
+
}: { indexed: Map<number, { width: number; signed: boolean }[]>; accessOps: Map<Op, FrameAccessOp> },
|
|
1060
|
+
object: FrameAccessOp,
|
|
1061
|
+
sum: Value,
|
|
1062
|
+
fail: Refuse,
|
|
1063
|
+
): void {
|
|
1064
|
+
const { off } = object;
|
|
1065
|
+
const got = indexed.get(off) ?? indexed.set(off, []).get(off)!;
|
|
1066
|
+
for (const blk of irBlocks) {
|
|
1067
|
+
for (const op of blk.ops) {
|
|
1068
|
+
const roles = [
|
|
1069
|
+
...op.operands.flatMap((v, i) => (v === sum ? [i] : [])),
|
|
1070
|
+
...(op.successors ?? []).flatMap((s) => (s.args.includes(sum) ? [-1] : [])),
|
|
1071
|
+
];
|
|
1072
|
+
for (const i of roles) {
|
|
1073
|
+
if ((op.opcode !== 'load' && op.opcode !== 'store') || i !== 0 || (op.attrs.off as number) !== 0) {
|
|
1074
|
+
fail(
|
|
1075
|
+
`a runtime index into the object at [sp,#${off}) flows into \`${i === -1 ? 'a phi' : op.opcode}\` — ` +
|
|
1076
|
+
'only a load or store at the indexed address is modelled',
|
|
1077
|
+
);
|
|
1078
|
+
}
|
|
1079
|
+
got.push({ width: op.attrs.width as number, signed: op.opcode === 'load' && op.attrs.signed === true });
|
|
1080
|
+
accessOps.set(op, object);
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
/** The bytes an escape may reach, `[lo, hi)` from the object's own offset, and how it reads them
|
|
1087
|
+
* as a refusal words it. */
|
|
1088
|
+
interface ReadWindow {
|
|
1089
|
+
readonly lo: number;
|
|
1090
|
+
readonly hi: number;
|
|
1091
|
+
readonly why: string;
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
/** Each op's block and index, over the IR as the classification judged it. */
|
|
1095
|
+
function opPositions(irBlocks: readonly Block[]): Map<Op, { blk: Block; i: number }> {
|
|
1096
|
+
const at = new Map<Op, { blk: Block; i: number }>();
|
|
1097
|
+
for (const blk of irBlocks) {
|
|
1098
|
+
blk.ops.forEach((op, i) => at.set(op, { blk, i }));
|
|
1099
|
+
}
|
|
1100
|
+
return at;
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
/** The read window of every escaped object, in the order the classification found the escapes. */
|
|
1104
|
+
function escapeWindows(
|
|
1105
|
+
irBlocks: readonly Block[],
|
|
1106
|
+
uses: FrameUses,
|
|
1107
|
+
flow: AddressFlow,
|
|
1108
|
+
facts: FrameAddressFacts,
|
|
1109
|
+
target: TargetDescription,
|
|
1110
|
+
): Map<number, ReadWindow> {
|
|
1111
|
+
const at = opPositions(irBlocks);
|
|
1112
|
+
return new Map([...uses.escaped].map((off) => [off, readWindow(off, { at, uses, flow, facts, target })] as const));
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1115
|
+
const unbounded = (why: string): ReadWindow => ({ lo: -Infinity, hi: Infinity, why });
|
|
1116
|
+
|
|
1117
|
+
/** THE BYTES AN ESCAPE MAY REACH, as `[lo, hi)` relative to the object's own offset. Anything
|
|
1118
|
+
* that may write, and anything this cannot bound, reaches the whole frame — save a callee
|
|
1119
|
+
* handed only its own return storage, which writes the struct it returns (`returnTemps`). A
|
|
1120
|
+
* device that only reads is bounded by its channel's control halfword (`readSourceControl`),
|
|
1121
|
+
* per TRANSFER: the device reads the object on every arm of the channel from the store that
|
|
1122
|
+
* handed it the address until a later word store to the same source register replaces it, so
|
|
1123
|
+
* the control stores that bound it are the ones reachable in between. Each has to be a literal
|
|
1124
|
+
* the target decodes. A store through a pointer this cannot resolve — one that is this frame's
|
|
1125
|
+
* on only some paths included (`frameOnEveryPath`) — may BE the control halfword, so it leaves
|
|
1126
|
+
* the read unbounded — as does a transfer never armed here at all. An unbounded device read
|
|
1127
|
+
* says which of those it met (`why`): each is a different capability to build. A block transfer
|
|
1128
|
+
* that took the address as its source adds the bytes its control word reads (`calleeReads`).
|
|
1129
|
+
*
|
|
1130
|
+
* A NAMED SYMBOL PLUS A CONSTANT IS RESOLVED WHERE THE SYMBOL MAP PLACES THE NAME, and then
|
|
1131
|
+
* exactly, by the overlap test below. agbcc's alias model never lets a C identifier plus a
|
|
1132
|
+
* constant meet a literal address (alias.c:812-819, 1064-1068), but nothing in the asm says a
|
|
1133
|
+
* name is a C identifier: a disassembly spells an I/O register the way it spells a global
|
|
1134
|
+
* (`.word REG_VCOUNT`). So a name the map does not place is an unresolved pointer here.
|
|
1135
|
+
*
|
|
1136
|
+
* THE PREMISE, stated once and not checkable from one function: a callee or an interrupt
|
|
1137
|
+
* handler arms only a transfer it set up itself, source register first. So a call on the path
|
|
1138
|
+
* does not unbound the read, and neither does an interrupt at any instruction: whatever re-arms
|
|
1139
|
+
* the channel with this frame's address still in the source register is this function's own
|
|
1140
|
+
* store, and the walk sees every one of those. */
|
|
1141
|
+
function readWindow(
|
|
1142
|
+
off: number,
|
|
1143
|
+
{
|
|
1144
|
+
at,
|
|
1145
|
+
uses,
|
|
1146
|
+
flow,
|
|
1147
|
+
facts,
|
|
1148
|
+
target,
|
|
1149
|
+
}: {
|
|
1150
|
+
at: ReadonlyMap<Op, { blk: Block; i: number }>;
|
|
1151
|
+
uses: FrameUses;
|
|
1152
|
+
flow: AddressFlow;
|
|
1153
|
+
facts: FrameAddressFacts;
|
|
1154
|
+
target: TargetDescription;
|
|
1155
|
+
},
|
|
1156
|
+
): ReadWindow {
|
|
1157
|
+
// a callee handed only its own return storage writes the struct it returns and nothing else
|
|
1158
|
+
const temps = uses.returnTemps.get(off);
|
|
1159
|
+
if (temps !== undefined && uses.useCount.get(off) === temps.length) {
|
|
1160
|
+
return { lo: 0, hi: returnedSize(temps[0]), why: 'that returns its struct into it' };
|
|
1161
|
+
}
|
|
1162
|
+
const control = target.capabilities.readSourceControl;
|
|
1163
|
+
const stores = uses.sourceStores.get(off);
|
|
1164
|
+
const called = uses.calleeReads.get(off);
|
|
1165
|
+
if (!mayWrite(uses, off) && stores === undefined && called !== undefined) {
|
|
1166
|
+
return { lo: called.lo, hi: called.hi, why: 'that reads through it' };
|
|
1167
|
+
}
|
|
1168
|
+
if (mayWrite(uses, off) || stores === undefined || control === undefined) {
|
|
1169
|
+
return unbounded('that reads through it');
|
|
1170
|
+
}
|
|
1171
|
+
const little = target.capabilities.endianness === 'little';
|
|
1172
|
+
const halves: number[] = [];
|
|
1173
|
+
for (const { op: handed, sink } of stores) {
|
|
1174
|
+
const cnt = sink + control.offset;
|
|
1175
|
+
let armed = false;
|
|
1176
|
+
const entered = new Set<Block>();
|
|
1177
|
+
const work: [Block, number][] = [[at.get(handed)!.blk, at.get(handed)!.i + 1]];
|
|
1178
|
+
walk: while (work.length > 0) {
|
|
1179
|
+
const [blk, from] = work.pop()!;
|
|
1180
|
+
for (let i = from; i < blk.ops.length; i++) {
|
|
1181
|
+
const op = blk.ops[i];
|
|
1182
|
+
if (op.opcode !== 'store') {
|
|
1183
|
+
continue;
|
|
1184
|
+
}
|
|
1185
|
+
const base = facts.literalAddrOf(op.operands[0]);
|
|
1186
|
+
if (base === undefined) {
|
|
1187
|
+
if (!flow.frameOnEveryPath.has(op.operands[0])) {
|
|
1188
|
+
return unbounded('a later store through an unresolved pointer may re-arm');
|
|
1189
|
+
}
|
|
1190
|
+
continue;
|
|
1191
|
+
}
|
|
1192
|
+
const a = base + (op.attrs.off as number);
|
|
1193
|
+
const w = op.attrs.width as number;
|
|
1194
|
+
if (a === sink && w === 4) {
|
|
1195
|
+
continue walk;
|
|
1196
|
+
}
|
|
1197
|
+
if (a + w <= cnt || a >= cnt + 2) {
|
|
1198
|
+
continue;
|
|
1199
|
+
}
|
|
1200
|
+
const v = facts.defOf.get(op.operands[1]);
|
|
1201
|
+
if (v?.opcode !== 'const' || a > cnt || a + w < cnt + 2) {
|
|
1202
|
+
return unbounded('whose control word is not a literal');
|
|
1203
|
+
}
|
|
1204
|
+
const shift = 8 * (little ? cnt - a : a + w - cnt - 2);
|
|
1205
|
+
halves.push(((v.attrs.value as number) >>> shift) & 0xffff);
|
|
1206
|
+
armed = true;
|
|
1207
|
+
}
|
|
1208
|
+
for (const s of blk.ops[blk.ops.length - 1]?.successors ?? []) {
|
|
1209
|
+
if (!entered.has(s.block)) {
|
|
1210
|
+
entered.add(s.block);
|
|
1211
|
+
work.push([s.block, 0]);
|
|
1212
|
+
}
|
|
1213
|
+
}
|
|
1214
|
+
}
|
|
1215
|
+
if (!armed) {
|
|
1216
|
+
return unbounded('this function never arms');
|
|
1217
|
+
}
|
|
1218
|
+
}
|
|
1219
|
+
let { lo, hi } = called ?? { lo: 0, hi: 0 };
|
|
1220
|
+
for (const h of halves) {
|
|
1221
|
+
const read = sourceControlRead(control, h);
|
|
1222
|
+
if (read === null) {
|
|
1223
|
+
return unbounded('whose control word bounds nothing');
|
|
1224
|
+
}
|
|
1225
|
+
const reach = sourceReach(read, off);
|
|
1226
|
+
lo = Math.min(lo, reach.lo);
|
|
1227
|
+
hi = Math.max(hi, reach.hi);
|
|
1228
|
+
}
|
|
1229
|
+
return { lo, hi, why: 'that reads through it' };
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1232
|
+
/** Which model the audit judges the frame in. `refused` is the one mutable fact the audit
|
|
1233
|
+
* carries across stages: set when a shape the per-object model refuses is met while the
|
|
1234
|
+
* one-object answer is on offer, and read by the relift test. */
|
|
1235
|
+
interface ModelChoice {
|
|
1236
|
+
readonly onOffer: boolean;
|
|
1237
|
+
refused: boolean;
|
|
1238
|
+
/** refuses `why` where it stands, or, with the one-object answer on offer, asks for it instead */
|
|
1239
|
+
shapeRefused(why: string): void;
|
|
1240
|
+
/** `why`, naming the writer that withheld the one-object answer where one did — for a refusal
|
|
1241
|
+
* that answer would otherwise have taken */
|
|
1242
|
+
withheld(why: string): string;
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1245
|
+
/** Why a writer of the frame keeps the one-object answer from holding it, or null. The one object
|
|
1246
|
+
* is every byte of the declared area, so what a holder writes through any address inside it is
|
|
1247
|
+
* bytes one declaration owns, at offsets one array makes adjacent — below them is the outgoing
|
|
1248
|
+
* block and above them the saved registers, neither a local. That is the whole-area argument the
|
|
1249
|
+
* untyped object is sized by, and it holds the same escapes here (`wholeAreaEscapeRefusal`). Not
|
|
1250
|
+
* kept besides is a callee's RETURN STORAGE: the struct it returns through a hidden pointer is its
|
|
1251
|
+
* declared type, not bytes of an array. */
|
|
1252
|
+
function writerNotKept(uses: FrameUses, returnsWithoutHiddenPointer: (callee: string) => boolean): string | null {
|
|
1253
|
+
for (const off of [...uses.escaped].filter((o) => mayWrite(uses, o))) {
|
|
1254
|
+
const at = `the captured address at [sp,#${off})`;
|
|
1255
|
+
const temp = uses.returnTemps.get(off)?.[0];
|
|
1256
|
+
if (temp !== undefined) {
|
|
1257
|
+
return `${at} is where \`${temp.attrs.target}\` returns its struct`;
|
|
1258
|
+
}
|
|
1259
|
+
const why = wholeAreaEscapeRefusal(off, uses, returnsWithoutHiddenPointer);
|
|
1260
|
+
if (why !== null) {
|
|
1261
|
+
return `${at}: ${why}`;
|
|
1262
|
+
}
|
|
1263
|
+
}
|
|
1264
|
+
return null;
|
|
1265
|
+
}
|
|
1266
|
+
|
|
1267
|
+
/** THE MODEL IS CHOSEN BEFORE ANY SHAPE IS JUDGED. Where one escape reaches without bound and the
|
|
1268
|
+
* one-object answer can hold every writer (`writerNotKept`), that answer is on offer — two
|
|
1269
|
+
* out-parameters handed to one callee as readily as a device read — and the shapes the
|
|
1270
|
+
* per-object model refuses are ones it can hold: a member at [+k] through a capture, two widths
|
|
1271
|
+
* or two signednesses at one address, a runtime index, overlapping objects, a second object
|
|
1272
|
+
* beside an untyped one, an object over a slot. So there each of those refusals asks for that
|
|
1273
|
+
* answer instead of declining (`shapeRefused`), and the second audit judges the bytes as one
|
|
1274
|
+
* object by its own rules — two types at one byte still refuse there. Where the answer is not on offer, each refuses where it stands. */
|
|
1275
|
+
function chooseFrameModel(
|
|
1276
|
+
oneObject: FrameRange | undefined,
|
|
1277
|
+
uses: FrameUses,
|
|
1278
|
+
windows: ReadonlyMap<number, ReadWindow>,
|
|
1279
|
+
returnsWithoutHiddenPointer: (callee: string) => boolean,
|
|
1280
|
+
fail: Refuse,
|
|
1281
|
+
): ModelChoice {
|
|
1282
|
+
const unbounded =
|
|
1283
|
+
oneObject === undefined && [...windows.values()].some((w) => w.lo === -Infinity && w.hi === Infinity);
|
|
1284
|
+
const writer = unbounded ? writerNotKept(uses, returnsWithoutHiddenPointer) : null;
|
|
1285
|
+
const onOffer = unbounded && writer === null;
|
|
1286
|
+
const model: ModelChoice = {
|
|
1287
|
+
onOffer,
|
|
1288
|
+
refused: false,
|
|
1289
|
+
shapeRefused: (why) => {
|
|
1290
|
+
if (!onOffer) {
|
|
1291
|
+
fail(model.withheld(why));
|
|
1292
|
+
}
|
|
1293
|
+
model.refused = true;
|
|
1294
|
+
},
|
|
1295
|
+
// A refusal the one-object answer would have taken names the writer that withheld it: the
|
|
1296
|
+
// shape alone is one the answer holds, so the shape alone does not say why this declined.
|
|
1297
|
+
withheld: (why) => (writer === null ? why : `${why} — and the one object cannot hold every writer: ${writer}`),
|
|
1298
|
+
};
|
|
1299
|
+
if (uses.memberRefusal !== undefined) {
|
|
1300
|
+
model.shapeRefused(uses.memberRefusal);
|
|
1301
|
+
}
|
|
1302
|
+
return model;
|
|
1303
|
+
}
|
|
1304
|
+
|
|
1305
|
+
/** THE ACCEPTANCE'S PREMISE, RE-ASKED OF THE IR. `capturedObjectIsTheWholeFrame` is a reading
|
|
1306
|
+
* of the TEXT and it is the one thing in this file that switches a refusal OFF, so the two
|
|
1307
|
+
* facts it claims are re-proven here, where they are exact, rather than left to the
|
|
1308
|
+
* approximation that licensed them. Neither is a second opinion on the same evidence: the
|
|
1309
|
+
* scan asks what a REGISTER holds at a `bl`; this asks what the finished function does with
|
|
1310
|
+
* the OBJECT.
|
|
1311
|
+
*
|
|
1312
|
+
* THE ADDRESS ESCAPED. The whole licence is "something outside this function is holding the
|
|
1313
|
+
* address of this frame", and the pre-lift scan can say that of a register whose value never
|
|
1314
|
+
* reaches a call operand or a store — a declared arity trims it away, or the register is dead
|
|
1315
|
+
* by the time the call is built. When it does, [sp,#0] has been re-modelled as an addressable
|
|
1316
|
+
* object on no evidence at all, and the outgoing argument that really lived there is gone
|
|
1317
|
+
* from the call. EITHER escape re-proves it, because either one licensed it: a call taking
|
|
1318
|
+
* the address, or a store publishing it outward. They are asked as one question because they
|
|
1319
|
+
* license one thing.
|
|
1320
|
+
*
|
|
1321
|
+
* AND EACH ARM IS AT MOST AS WIDE AS THE LICENCE IT RE-PROVES, which is what makes this a
|
|
1322
|
+
* containment rather than a second door. The callee arm is narrower for free — a `call`
|
|
1323
|
+
* operand is the address reaching a callee, which is what the scan approximated. The publish
|
|
1324
|
+
* arm is `publishedOutward` and not `published` for the same reason spelled out there: read
|
|
1325
|
+
* off the wider set, this re-proof accepts a halfword store and a store back into the
|
|
1326
|
+
* object's own bytes, both of which the licence refuses by name — so a frame the pre-lift
|
|
1327
|
+
* scan would never have licensed passes the check that exists to re-prove the licence.
|
|
1328
|
+
*
|
|
1329
|
+
* THE CONTAINMENT IS STRICT AND THE SLACK IS ON THIS SIDE. `taint` is whole-function where the
|
|
1330
|
+
* pre-lift walk is block-local, so a base captured in an earlier block is refused HERE and
|
|
1331
|
+
* accepted THERE. That is the only direction the two can differ in without a wrong answer
|
|
1332
|
+
* reaching a caller, and it is structural rather than lucky: this set is built from a superset
|
|
1333
|
+
* of the facts the walk has. `packages/core/test/thumb-frontend.test.ts` carries the input.
|
|
1334
|
+
*
|
|
1335
|
+
* NOT A STRUCT-RETURN TEMP, which `hiddenReturnPointerStands` asks. */
|
|
1336
|
+
function recheckWholeFramePremise({
|
|
1337
|
+
uses,
|
|
1338
|
+
returnsWithoutHiddenPointer,
|
|
1339
|
+
fail,
|
|
1340
|
+
}: {
|
|
1341
|
+
uses: FrameUses;
|
|
1342
|
+
returnsWithoutHiddenPointer: (callee: string) => boolean;
|
|
1343
|
+
fail: Refuse;
|
|
1344
|
+
}): void {
|
|
1345
|
+
if (!uses.passedToCallee.has(0) && !uses.publishedOutward.has(0)) {
|
|
1346
|
+
fail(
|
|
1347
|
+
'the one-word-frame proof licensed this lift on the frame base escaping, and in the ' +
|
|
1348
|
+
'lifted function no call takes it and no word store publishes it outside this frame — ' +
|
|
1349
|
+
'so nothing rules out an outgoing stack argument at [sp,#0]',
|
|
1350
|
+
);
|
|
1351
|
+
}
|
|
1352
|
+
const writtenHere = uses.accesses.get(0)?.some((a) => !a.isLoad) === true;
|
|
1353
|
+
const whyItStands = writtenHere ? null : hiddenReturnPointerStands(0, uses.arg0Callees, returnsWithoutHiddenPointer);
|
|
1354
|
+
if (whyItStands !== null) {
|
|
1355
|
+
fail(`the one-word frame is never written here, and ${whyItStands}`);
|
|
1356
|
+
}
|
|
1357
|
+
}
|
|
1358
|
+
|
|
1359
|
+
/** NOT A STRUCT-RETURN TEMP. A one-word frame rules out agbcc's block-copy bases (each needs
|
|
1360
|
+
* two words) but NOT the hidden return pointer of a <=4-byte non-integer-like struct, which is
|
|
1361
|
+
* exactly one word: `struct S4 { char a,b,c,d; }; struct S4 s = mk(x);` compiles to `add
|
|
1362
|
+
* sp,#-4 / mov r0,sp / bl mk / ldr r0,[sp]`, instruction for instruction an out-parameter
|
|
1363
|
+
* call. Left alone that lifted as `mk(&sp0, a0)` — a call the real prototype rejects.
|
|
1364
|
+
*
|
|
1365
|
+
* TWO facts rule it out and either will do, because a return temp is storage the CALLEE owns
|
|
1366
|
+
* outright: it is written only by the callee, and the callee RETURNS the struct THROUGH IT.
|
|
1367
|
+
* So a store of our own says the object is one this function fills; and a callee whose RETURN
|
|
1368
|
+
* is known to need no hidden pointer says the same by the ABI — a function that returns
|
|
1369
|
+
* nothing, or returns in a register, has no such pointer to be given, whatever sits in r0.
|
|
1370
|
+
*
|
|
1371
|
+
* AND THE QUESTION IS PER-CALL, which is what bounds how far it has to be asked: the pointer
|
|
1372
|
+
* is argument 0, always (compiled — `struct S4 mk3(int,int,int)` puts sp in r0 and shifts all
|
|
1373
|
+
* three real arguments up), so an address NO call takes at argument 0 cannot be one, whatever
|
|
1374
|
+
* else the function does with it. That is the whole of this function and it is why the
|
|
1375
|
+
* block-copy idiom `memcpy(dst, buf, sizeof buf)` — buffer at argument 1 — needs no declaration
|
|
1376
|
+
* at all. Per-call and not per-object: an address handed over at argument 1 somewhere leaves the
|
|
1377
|
+
* call that takes it at argument 0 exactly as ambiguous as before, so position acquits a call
|
|
1378
|
+
* rather than an object.
|
|
1379
|
+
*
|
|
1380
|
+
* THE THIRD IS A DECLARATION, NOT AN INFERENCE, and so is its converse: a callee declared to
|
|
1381
|
+
* return a struct through memory, whose call the frontend stamps `sret`, makes the object that
|
|
1382
|
+
* call's return storage (`returnTemps`). Those two are the only answers here read off something
|
|
1383
|
+
* other than the instruction stream. The frontend's call declarations answer the third
|
|
1384
|
+
* (`FrameObjectAudit.returnsWithoutHiddenPointer`, which aggregate.ts's
|
|
1385
|
+
* `returnsWithoutHiddenPointer` decides), from the project's own `returnsVoid`, from a struct its
|
|
1386
|
+
* declaration returns that the target hands back in a register, or from the `returns` of a
|
|
1387
|
+
* signature the C standard fixes — the same table whose `params` decide a call's arity. It is
|
|
1388
|
+
* asked of EVERY callee that took the address at argument 0, because the object gets one
|
|
1389
|
+
* decision: one callee about whose return nothing is known leaves the ambiguity standing and the
|
|
1390
|
+
* refusal fires.
|
|
1391
|
+
*
|
|
1392
|
+
* AN ARITY CANNOT ANSWER IT, which is worth saying because the count is right there and looks
|
|
1393
|
+
* like evidence: a hidden pointer does set one argument register more than the callee
|
|
1394
|
+
* declares, but the register count is what the machine WROTE, and a register already holding
|
|
1395
|
+
* this function's own incoming parameter is written by nobody. Compiled: `void f(const void
|
|
1396
|
+
* *a, const void *b){ struct Blob64 s = makeblob(b); }` emits `add sp,#-0x40 / mov r0,sp /
|
|
1397
|
+
* bl makeblob` — one written register against one declared parameter — so counting registers
|
|
1398
|
+
* reads a real struct return as an out-parameter and declares the callee's own storage as a
|
|
1399
|
+
* local. The question is about the RETURN and only a statement about the return decides it.
|
|
1400
|
+
*
|
|
1401
|
+
* WHAT IT COSTS WHEN THE DECLARATION IS WRONG, measured rather than compared. On the `sret`
|
|
1402
|
+
* shape above, with `mk` (which really returns `struct S4`) declared `params: 1,
|
|
1403
|
+
* returnsVoid: true`, the lift succeeds and emits `s32 sret(s32 a0) { s32 sp0; mk(&sp0);
|
|
1404
|
+
* return (u8)sp0; }` — a compiling, plausible, WRONG program with the real argument dropped,
|
|
1405
|
+
* where a loud decline stood. Not a smaller cost than a wrong ARITY, either: the same entry
|
|
1406
|
+
* supplies both facts, so a wrong `returnsVoid` drops the argument too, and the frame re-model
|
|
1407
|
+
* is the silent half. The trade is accepted because there IS no other discriminator: compiled
|
|
1408
|
+
* through
|
|
1409
|
+
* the benchmark's own agbcc command, the hidden struct return and the out-parameter emit the
|
|
1410
|
+
* same instructions in the same order, the slot is read back at a scalar width in both, and
|
|
1411
|
+
* in both the value read back is what the function returns — so an asm-side corroboration
|
|
1412
|
+
* would be a rule with no discriminating input. The mitigation is that under-declaring is the
|
|
1413
|
+
* safe direction (a callee whose return nothing describes still declines) and that `FnProto`
|
|
1414
|
+
* says so at the field.
|
|
1415
|
+
*
|
|
1416
|
+
* The residual cost is stated rather than hidden: an OUTPUT-only parameter taken at argument
|
|
1417
|
+
* 0 of a callee the project has NOT declared is still byte-for-byte a struct return, and
|
|
1418
|
+
* still declines with it.
|
|
1419
|
+
*
|
|
1420
|
+
* ONE SOURCE FOR THE DECISION AND ITS REASON, because every arm of this audit asks it and a
|
|
1421
|
+
* predicate beside a message is two things that can disagree. Returns why the pointer is not
|
|
1422
|
+
* ruled out — the caller frames it for its own arm — or null. */
|
|
1423
|
+
function hiddenReturnPointerStands(
|
|
1424
|
+
off: number,
|
|
1425
|
+
arg0Callees: FrameUses['arg0Callees'],
|
|
1426
|
+
returnsWithoutHiddenPointer: (callee: string) => boolean,
|
|
1427
|
+
): string | null {
|
|
1428
|
+
const cs = arg0Callees.get(off);
|
|
1429
|
+
if (cs === undefined || cs.size === 0) {
|
|
1430
|
+
return null;
|
|
1431
|
+
}
|
|
1432
|
+
const unknown = [...cs].filter((c) => c === null || !returnsWithoutHiddenPointer(c));
|
|
1433
|
+
if (unknown.length === 0) {
|
|
1434
|
+
return null;
|
|
1435
|
+
}
|
|
1436
|
+
return (
|
|
1437
|
+
`\`${unknown.map((c) => c ?? '?').join('`, `')}\` takes it at argument 0 and nothing says ` +
|
|
1438
|
+
'what that callee returns — a struct returned through a hidden pointer is handed this same frame'
|
|
1439
|
+
);
|
|
1440
|
+
}
|
|
1441
|
+
|
|
1442
|
+
// TWO MODELS FOR ONE BYTE is a silent disagreement: the slot model keeps an SSA slot in a
|
|
1443
|
+
// register, so a store through an object over the same bytes would never be seen there.
|
|
1444
|
+
function overlaps(a: number, aw: number, b: number, bw: number): boolean {
|
|
1445
|
+
return a < b + bw && b < a + aw;
|
|
1446
|
+
}
|
|
1447
|
+
|
|
1448
|
+
function failIfSlotKeysIt(
|
|
1449
|
+
off: number,
|
|
1450
|
+
width: number,
|
|
1451
|
+
{
|
|
1452
|
+
usedSlotOffsets,
|
|
1453
|
+
objects,
|
|
1454
|
+
foldedHere,
|
|
1455
|
+
fail,
|
|
1456
|
+
}: {
|
|
1457
|
+
usedSlotOffsets: ReadonlySet<number>;
|
|
1458
|
+
objects: ReadonlyMap<number, readonly Op[]>;
|
|
1459
|
+
foldedHere: ReadonlySet<Op>;
|
|
1460
|
+
fail: Refuse;
|
|
1461
|
+
},
|
|
1462
|
+
): void {
|
|
1463
|
+
for (const slot of usedSlotOffsets) {
|
|
1464
|
+
if (overlaps(off, width, slot, 4)) {
|
|
1465
|
+
fail(
|
|
1466
|
+
objects.get(off)?.some((op) => foldedHere.has(op))
|
|
1467
|
+
? `the capture moved by a constant to [sp,#${off}) is a move the pre-lift walk does not follow, ` +
|
|
1468
|
+
`so the slot model keys [sp,#${slot}] too — one byte, two models`
|
|
1469
|
+
: `the object at [sp,#${off}) overlaps the SSA slot at [sp,#${slot}] — one byte, two models`,
|
|
1470
|
+
);
|
|
1471
|
+
}
|
|
1472
|
+
}
|
|
1473
|
+
}
|
|
1474
|
+
|
|
1475
|
+
/** ONE OBJECT IN MEMORY, the answer a device read or a callee write nothing bounds is given
|
|
1476
|
+
* instead of a refusal (`oneObject`, requested by the relift test where the escapes are judged).
|
|
1477
|
+
* Every byte of `[from, to)` is declared one `u8` array whose address the device or the callee
|
|
1478
|
+
* holds (`writerNotKept` says which writers it can hold), and every access inside it is a
|
|
1479
|
+
* cast-spelled access to that array — so each store the machine made there is a store the
|
|
1480
|
+
* recompile makes too. That is what agbcc does for an object whose address escaped: it keeps
|
|
1481
|
+
* every store to it, in order (flow.c deletes a memory store only when an identical later
|
|
1482
|
+
* store in the same block overwrites it). And it is right whichever the source had there: a
|
|
1483
|
+
* member of the escaped object, which the device may read, or a spill, which no one reads —
|
|
1484
|
+
* the asm spells both as a store and a reload, and keeping a spill in memory changes no value. */
|
|
1485
|
+
function keepAsOneObject({
|
|
1486
|
+
oneObject: { from, to },
|
|
1487
|
+
irBlocks,
|
|
1488
|
+
objects,
|
|
1489
|
+
uses,
|
|
1490
|
+
usedSlotOffsets,
|
|
1491
|
+
returnsWithoutHiddenPointer,
|
|
1492
|
+
fail,
|
|
1493
|
+
}: {
|
|
1494
|
+
oneObject: FrameRange;
|
|
1495
|
+
irBlocks: Block[];
|
|
1496
|
+
objects: ReadonlyMap<number, readonly Op[]>;
|
|
1497
|
+
uses: FrameUses;
|
|
1498
|
+
usedSlotOffsets: ReadonlySet<number>;
|
|
1499
|
+
returnsWithoutHiddenPointer: (callee: string) => boolean;
|
|
1500
|
+
fail: Refuse;
|
|
1501
|
+
}): void {
|
|
1502
|
+
const kept = `the bytes [sp,#${from}) to [sp,#${to}) kept as one object`;
|
|
1503
|
+
for (const off of objects.keys()) {
|
|
1504
|
+
if (off < from || off >= to) {
|
|
1505
|
+
fail(`the object at [sp,#${off}) lies outside ${kept}`);
|
|
1506
|
+
}
|
|
1507
|
+
}
|
|
1508
|
+
for (const slot of usedSlotOffsets) {
|
|
1509
|
+
if (overlaps(slot, 4, from, to - from)) {
|
|
1510
|
+
fail(`the SSA slot at [sp,#${slot}] lies inside ${kept} — one byte, two models`);
|
|
1511
|
+
}
|
|
1512
|
+
}
|
|
1513
|
+
const writer = writerNotKept(uses, returnsWithoutHiddenPointer);
|
|
1514
|
+
if (writer !== null) {
|
|
1515
|
+
fail(`${kept} cannot hold every writer — ${writer}`);
|
|
1516
|
+
}
|
|
1517
|
+
// A runtime index names no byte, so no access type can be checked against the others at the
|
|
1518
|
+
// bytes it reaches — except a byte access, which needs none: character types alias every type.
|
|
1519
|
+
if ([...uses.indexed.values()].some((xs) => xs.some((x) => x.width !== 1))) {
|
|
1520
|
+
fail(
|
|
1521
|
+
`a runtime index into ${kept} accesses more than a byte, and names no byte whose type it can be checked against`,
|
|
1522
|
+
);
|
|
1523
|
+
}
|
|
1524
|
+
// ONE TYPE PER BYTE. agbcc at -O2 turns on type-based alias analysis (toplev.c:3616), and
|
|
1525
|
+
// compiled, a `u32` read through a cast is served the earlier `u32` store straight past a
|
|
1526
|
+
// `u16` store to the same bytes — a stale value. So a byte two accesses of different widths
|
|
1527
|
+
// reach cannot be spelled through casts. A byte access is exempt, since character types
|
|
1528
|
+
// alias everything, and so is a signedness difference, since the signed and unsigned types
|
|
1529
|
+
// of one width share an alias set (c-common.c:1962-1974). The first audit asks for this
|
|
1530
|
+
// answer rather than refusing two widths — an object read at both, or a `strh` member through
|
|
1531
|
+
// the captured address over a word the slot model had — so this is where they refuse.
|
|
1532
|
+
const widthAt = new Map<number, number>();
|
|
1533
|
+
for (const m of uses.members) {
|
|
1534
|
+
if (m.at < from || m.at + m.width > to) {
|
|
1535
|
+
fail(`an access of ${m.width} bytes at [sp,#${m.at}) reaches past ${kept}`);
|
|
1536
|
+
}
|
|
1537
|
+
if (m.width === 1) {
|
|
1538
|
+
continue;
|
|
1539
|
+
}
|
|
1540
|
+
for (let b = m.at; b < m.at + m.width; b++) {
|
|
1541
|
+
const had = widthAt.get(b);
|
|
1542
|
+
if (had !== undefined && had !== m.width) {
|
|
1543
|
+
fail(
|
|
1544
|
+
`the byte at [sp,#${b}] of ${kept} is accessed ${had} and ${m.width} bytes wide, and agbcc at -O2 ` +
|
|
1545
|
+
'lets a read of one type pass a store of the other (strict aliasing) — a cast spelling would read stale',
|
|
1546
|
+
);
|
|
1547
|
+
}
|
|
1548
|
+
widthAt.set(b, m.width);
|
|
1549
|
+
}
|
|
1550
|
+
}
|
|
1551
|
+
// Rewritten onto one `laddr` at `from`: an access through a member at `k` becomes an access
|
|
1552
|
+
// at `k - from` off the object, and a member address used any other way becomes the object's
|
|
1553
|
+
// address moved by that constant.
|
|
1554
|
+
//
|
|
1555
|
+
// `volatile` goes on each ACCESS where the address was stored somewhere a writer may hold it
|
|
1556
|
+
// (`publishedToWriter`): unqualified, a spin on a byte a holder of the address sets compiles to
|
|
1557
|
+
// a loop that never reads it again. Never on the array: every member is spelled through a cast,
|
|
1558
|
+
// whose pointee drops the array's qualifier, so it would reach no access and only make agbcc
|
|
1559
|
+
// warn `discards qualifiers` wherever the bare array is named. A reader alone — a DMA source, a
|
|
1560
|
+
// transfer's fill source (`filledFrom`) — changes no byte a re-read returns. KNOWN GAP: a
|
|
1561
|
+
// callee that keeps the address it was handed, for an interrupt to write later, is a holder
|
|
1562
|
+
// nothing here sees, so a spin on such a byte compiles unqualified; the per-object stamp has the
|
|
1563
|
+
// same gap (`stampObjects`: not on an ordinary `&local` argument).
|
|
1564
|
+
const reread = uses.publishedToWriter.size > 0;
|
|
1565
|
+
const object = mkOp('laddr', {
|
|
1566
|
+
results: [mkValue(T.unk(32))],
|
|
1567
|
+
attrs: { off: from, width: 1, signed: false, count: to - from },
|
|
1568
|
+
});
|
|
1569
|
+
const base = object.results[0];
|
|
1570
|
+
const memberAt = new Map<Value, number>();
|
|
1571
|
+
for (const op of [...objects.values()].flat()) {
|
|
1572
|
+
memberAt.set(op.results[0], op.attrs.off as number);
|
|
1573
|
+
}
|
|
1574
|
+
// The qualifier goes on every access the classification judged one (`accessOps`) — through a
|
|
1575
|
+
// member, a runtime index into one, or a phi that carries one (`p = c ? buf : gOther; while
|
|
1576
|
+
// (*p == 0);`). Only an access through a member is rebased: a phi's incoming member is
|
|
1577
|
+
// rewritten below, where it is an edge argument.
|
|
1578
|
+
const stillUsed = new Set<Value>();
|
|
1579
|
+
for (const blk of irBlocks) {
|
|
1580
|
+
for (const op of blk.ops) {
|
|
1581
|
+
const at = memberAt.get(op.operands[0]);
|
|
1582
|
+
const access = op.opcode === 'load' || op.opcode === 'store';
|
|
1583
|
+
if (reread && uses.accessOps.has(op)) {
|
|
1584
|
+
op.attrs = { ...op.attrs, volatile: true };
|
|
1585
|
+
}
|
|
1586
|
+
if (access && at !== undefined) {
|
|
1587
|
+
op.operands = [base, ...op.operands.slice(1)];
|
|
1588
|
+
op.attrs = { ...op.attrs, off: (op.attrs.off as number) + at - from };
|
|
1589
|
+
}
|
|
1590
|
+
op.operands.forEach((v) => memberAt.has(v) && stillUsed.add(v));
|
|
1591
|
+
(op.successors ?? []).forEach((sx) => sx.args.forEach((v) => memberAt.has(v) && stillUsed.add(v)));
|
|
1592
|
+
}
|
|
1593
|
+
}
|
|
1594
|
+
const atBase = (v: Value): Value => (memberAt.get(v) === from ? base : v);
|
|
1595
|
+
for (const blk of irBlocks) {
|
|
1596
|
+
blk.ops = blk.ops.flatMap((op) => {
|
|
1597
|
+
const at = op.opcode === 'laddr' ? memberAt.get(op.results[0]) : undefined;
|
|
1598
|
+
if (at === undefined) {
|
|
1599
|
+
op.operands = op.operands.map(atBase);
|
|
1600
|
+
(op.successors ?? []).forEach((sx) => (sx.args = sx.args.map(atBase)));
|
|
1601
|
+
return [op];
|
|
1602
|
+
}
|
|
1603
|
+
if (!stillUsed.has(op.results[0]) || at === from) {
|
|
1604
|
+
return [];
|
|
1605
|
+
}
|
|
1606
|
+
const by = mkValue(T.unk(32));
|
|
1607
|
+
return [
|
|
1608
|
+
mkOp('const', { results: [by], attrs: { value: at - from } }),
|
|
1609
|
+
mkOp('add', { operands: [base, by], results: op.results }),
|
|
1610
|
+
];
|
|
1611
|
+
});
|
|
1612
|
+
}
|
|
1613
|
+
irBlocks[0].ops.unshift(object);
|
|
1614
|
+
}
|
|
1615
|
+
|
|
1616
|
+
/** One object's shape: `count` elements of `width` bytes. */
|
|
1617
|
+
interface ObjectShape {
|
|
1618
|
+
readonly width: number;
|
|
1619
|
+
readonly count: number;
|
|
1620
|
+
}
|
|
1621
|
+
|
|
1622
|
+
/** The per-object model's reading of every object. */
|
|
1623
|
+
interface ObjectShapes {
|
|
1624
|
+
readonly extent: ReadonlyMap<number, ObjectShape>;
|
|
1625
|
+
/** the declared struct each return temp is, by offset */
|
|
1626
|
+
readonly aggregateAt: ReadonlyMap<number, IrType>;
|
|
1627
|
+
/** each object over a slot the slot model keys, as `[off, width]`, refused last (`failIfSlotKeysIt`) */
|
|
1628
|
+
readonly overSlot: readonly (readonly [number, number])[];
|
|
1629
|
+
}
|
|
1630
|
+
|
|
1631
|
+
const span = (o: ObjectShape) => o.width * o.count;
|
|
1632
|
+
|
|
1633
|
+
function slotKeys(usedSlotOffsets: ReadonlySet<number>, off: number, width: number): boolean {
|
|
1634
|
+
return [...usedSlotOffsets].some((slot) => overlaps(off, width, slot, 4));
|
|
1635
|
+
}
|
|
1636
|
+
|
|
1637
|
+
/** The SHAPE of each object — `count` elements of `width` bytes, spanning `width * count` —
|
|
1638
|
+
* and then that its bytes belong to nothing else.
|
|
1639
|
+
*
|
|
1640
|
+
* AN OBJECT OVER A SLOT IS REFUSED LAST, after the one-object answer is asked: that answer
|
|
1641
|
+
* routes every word of the local area through `laddr`, so no slot is left for the object to
|
|
1642
|
+
* overlap, and refusing first would decline exactly the frames it exists for — an object at
|
|
1643
|
+
* [sp,#0] whose first member is a word stored `str rN, [sp]`. The other per-object refusals here
|
|
1644
|
+
* go through `shapeRefused`, for the same reason. */
|
|
1645
|
+
function objectShapes({
|
|
1646
|
+
objects,
|
|
1647
|
+
uses,
|
|
1648
|
+
owned,
|
|
1649
|
+
declared,
|
|
1650
|
+
usedSlotOffsets,
|
|
1651
|
+
returnsWithoutHiddenPointer,
|
|
1652
|
+
model,
|
|
1653
|
+
fail,
|
|
1654
|
+
}: {
|
|
1655
|
+
objects: ReadonlyMap<number, readonly Op[]>;
|
|
1656
|
+
uses: FrameUses;
|
|
1657
|
+
owned: FrameRange;
|
|
1658
|
+
declared: FrameRange;
|
|
1659
|
+
usedSlotOffsets: ReadonlySet<number>;
|
|
1660
|
+
returnsWithoutHiddenPointer: (callee: string) => boolean;
|
|
1661
|
+
model: ModelChoice;
|
|
1662
|
+
fail: Refuse;
|
|
1663
|
+
}): ObjectShapes {
|
|
1664
|
+
const extent = new Map<number, ObjectShape>();
|
|
1665
|
+
const aggregateAt = new Map<number, IrType>();
|
|
1666
|
+
const overSlot: [number, number][] = [];
|
|
1667
|
+
for (const [off, acc] of uses.accesses) {
|
|
1668
|
+
const byIndex = uses.indexed.get(off) ?? [];
|
|
1669
|
+
const temps = uses.returnTemps.get(off);
|
|
1670
|
+
if (temps !== undefined) {
|
|
1671
|
+
// The callee writes the whole struct and this function names it once, as the call's
|
|
1672
|
+
// destination. A read of a member, or any other use of the address, is not modelled —
|
|
1673
|
+
// and most such reads never arrive here: the Thumb slot model refuses a word of the temp
|
|
1674
|
+
// read at a constant offset first ("stack pointer used as data"), a copy of the whole
|
|
1675
|
+
// struct into a global (`gS8 = mk8(x)`, which agbcc stages through the temp) included. So
|
|
1676
|
+
// modelling member reads starts with the slot model handing this audit a read return temp.
|
|
1677
|
+
const callee = temps[0].attrs.target as string;
|
|
1678
|
+
const type = temps[0].results[0].type;
|
|
1679
|
+
const spelling = type.kind === 'struct' ? (type.declared ?? `struct ${type.name}`) : typeToString(type);
|
|
1680
|
+
if (temps.some((t) => !typeEquals(t.results[0].type, type))) {
|
|
1681
|
+
fail(`the object at [sp,#${off}) is the struct-return storage of calls declared to return different types`);
|
|
1682
|
+
}
|
|
1683
|
+
if (acc.length > 0 || byIndex.length > 0) {
|
|
1684
|
+
fail(
|
|
1685
|
+
`the object at [sp,#${off}) is where \`${callee}\` returns ${spelling}, and this function also reads or ` +
|
|
1686
|
+
'writes it — a member of a returned struct is not modelled',
|
|
1687
|
+
);
|
|
1688
|
+
}
|
|
1689
|
+
if (uses.useCount.get(off) !== temps.length) {
|
|
1690
|
+
fail(
|
|
1691
|
+
`the object at [sp,#${off}) is where \`${callee}\` returns ${spelling}, and its address is also used ` +
|
|
1692
|
+
'another way — only the call it is returned by is modelled',
|
|
1693
|
+
);
|
|
1694
|
+
}
|
|
1695
|
+
extent.set(off, { width: 1, count: returnedSize(temps[0]) });
|
|
1696
|
+
aggregateAt.set(off, type);
|
|
1697
|
+
continue;
|
|
1698
|
+
}
|
|
1699
|
+
if (acc.length === 0) {
|
|
1700
|
+
// An object with no access of its own has no declared type and no extent, and the two
|
|
1701
|
+
// ways it gets there are two different gaps. Its bytes may already be keyed by the slot
|
|
1702
|
+
// model, which one byte is enough to decide; otherwise nothing in-function pins it at
|
|
1703
|
+
// all, and a guessed declaration is the plausible-but-wrong class.
|
|
1704
|
+
if (slotKeys(usedSlotOffsets, off, 1)) {
|
|
1705
|
+
overSlot.push([off, 1]);
|
|
1706
|
+
continue;
|
|
1707
|
+
}
|
|
1708
|
+
const why = notTheWholeArea(off, byIndex.length > 0, {
|
|
1709
|
+
objects,
|
|
1710
|
+
uses,
|
|
1711
|
+
declared,
|
|
1712
|
+
returnsWithoutHiddenPointer,
|
|
1713
|
+
});
|
|
1714
|
+
if (why !== null) {
|
|
1715
|
+
model.shapeRefused(
|
|
1716
|
+
(byIndex.length > 0
|
|
1717
|
+
? 'the captured address is addressed only through a runtime index'
|
|
1718
|
+
: 'the captured address is never dereferenced in this function') +
|
|
1719
|
+
`, so nothing pins the local object type — and ${why}`,
|
|
1720
|
+
);
|
|
1721
|
+
// …asked for the one object instead, which sizes no object of its own
|
|
1722
|
+
continue;
|
|
1723
|
+
}
|
|
1724
|
+
// An indexed access reads ONE ELEMENT of the storage declared below, so it must be one:
|
|
1725
|
+
// an unsigned byte. A wider element is a different array over the same bytes, and a
|
|
1726
|
+
// sign-extending read is not what `u8` spells.
|
|
1727
|
+
for (const a of byIndex) {
|
|
1728
|
+
if (a.width !== 1) {
|
|
1729
|
+
model.shapeRefused(
|
|
1730
|
+
`a runtime index into the object at [sp,#${off}) accesses ${a.width} bytes, and the storage ` +
|
|
1731
|
+
'nothing else types is declared as bytes — only a byte element is modelled',
|
|
1732
|
+
);
|
|
1733
|
+
}
|
|
1734
|
+
if (a.signed) {
|
|
1735
|
+
model.shapeRefused(
|
|
1736
|
+
`a runtime index into the object at [sp,#${off}) sign-extends, and the storage is declared unsigned`,
|
|
1737
|
+
);
|
|
1738
|
+
}
|
|
1739
|
+
}
|
|
1740
|
+
// STORAGE, NOT A TYPE. The reservation says how many bytes the frame holds and the
|
|
1741
|
+
// conjuncts above say they are all this object's, so the declaration commits to an EXTENT
|
|
1742
|
+
// and to nothing else: `u8 name[n]` over the declared range, unsigned bytes because no
|
|
1743
|
+
// access named an element type and inventing one is the guess this refuses everywhere else.
|
|
1744
|
+
//
|
|
1745
|
+
// THE EXTENT IS A ROUNDED ONE, stated because it is not a defect. agbcc reserves the
|
|
1746
|
+
// local area in whole words, so a source object of 13, 14, 15 or 16 bytes all reserve
|
|
1747
|
+
// sixteen — compiled and diffed at the row's own flags, `u8 x[0xD]` and `u8 x[0x10]`
|
|
1748
|
+
// reach the same object where `u8 x[0xC]` and `u8 x[0x11]` do not. What is declared is
|
|
1749
|
+
// the RESERVATION, which is the thing the asm carries; every source extent inside one
|
|
1750
|
+
// word of it emits the same object, so no member of that class is more right than this.
|
|
1751
|
+
//
|
|
1752
|
+
// NO COMPILER TERM, unlike `capturedObjectIsTheWholeFrame`, whose one-word reading is a
|
|
1753
|
+
// fact about what agbcc puts in a four-byte frame. This argument needs only that the
|
|
1754
|
+
// reservation ROUNDS UP to some granularity, which is a property of every stack ABI, and
|
|
1755
|
+
// it declares the reservation rather than a guess inside it — so a coarser rounding makes
|
|
1756
|
+
// the declared extent coarser too, never wrong about the bytes the machine reserved.
|
|
1757
|
+
extent.set(off, { width: 1, count: declared.to - declared.from });
|
|
1758
|
+
continue;
|
|
1759
|
+
}
|
|
1760
|
+
if (byIndex.length > 0) {
|
|
1761
|
+
model.shapeRefused(
|
|
1762
|
+
`a runtime index into the object at [sp,#${off}), which an access of its own types as one ` +
|
|
1763
|
+
'scalar — only the untyped storage of the whole declared area is indexed',
|
|
1764
|
+
);
|
|
1765
|
+
}
|
|
1766
|
+
const widths = new Set(acc.map((a) => a.width));
|
|
1767
|
+
if (widths.size > 1) {
|
|
1768
|
+
model.shapeRefused(`the accesses through the captured address disagree on width (${[...widths].join(' vs ')})`);
|
|
1769
|
+
}
|
|
1770
|
+
// …and on SIGNEDNESS, over the loads, for the same reason: one declared type extends one
|
|
1771
|
+
// way, so an object read by both `ldrsb` and `ldrb` has no faithful declaration —
|
|
1772
|
+
// `sp4 - sp4` would fold to 0 where the machine computes sext(b) - zext(b). Loads only: a
|
|
1773
|
+
// store extends nothing, and `strb` beside `ldrsb` is not a disagreement.
|
|
1774
|
+
const signs = new Set(acc.filter((a) => a.isLoad).map((a) => a.signed));
|
|
1775
|
+
if (signs.size > 1) {
|
|
1776
|
+
model.shapeRefused(
|
|
1777
|
+
'the loads through the captured address disagree on signedness — one declared type extends one way',
|
|
1778
|
+
);
|
|
1779
|
+
}
|
|
1780
|
+
// …and a scalar this function only READS is the callee's to fill, which is what a struct
|
|
1781
|
+
// return's hidden temp is: agbcc spells `s = mk(x)` above an outgoing block as `add r0, sp,
|
|
1782
|
+
// #0x4 / bl mk / ldr r0, [sp, #0x4]`, instruction for instruction an out-parameter call. The
|
|
1783
|
+
// question the whole-frame and untyped arms ask, asked of every object.
|
|
1784
|
+
const whyItStands = acc.some((a) => !a.isLoad)
|
|
1785
|
+
? null
|
|
1786
|
+
: hiddenReturnPointerStands(off, uses.arg0Callees, returnsWithoutHiddenPointer);
|
|
1787
|
+
if (whyItStands !== null) {
|
|
1788
|
+
fail(`the object at [sp,#${off}) is never written here, and ${whyItStands}`);
|
|
1789
|
+
}
|
|
1790
|
+
extent.set(off, { width: acc[0].width, count: 1 });
|
|
1791
|
+
}
|
|
1792
|
+
// Each object must own its bytes outright: inside the reserved local area, clear of every SSA
|
|
1793
|
+
// slot (`overSlot`, refused last), and clear of every other object.
|
|
1794
|
+
const objs = [...extent].sort((x, y) => x[0] - y[0]);
|
|
1795
|
+
for (const [off, obj] of objs) {
|
|
1796
|
+
if (off < owned.from || off + span(obj) > owned.to) {
|
|
1797
|
+
fail(`the object at [sp,#${off}) of width ${span(obj)} lies outside the reserved local area`);
|
|
1798
|
+
}
|
|
1799
|
+
if (slotKeys(usedSlotOffsets, off, span(obj))) {
|
|
1800
|
+
overSlot.push([off, span(obj)]);
|
|
1801
|
+
}
|
|
1802
|
+
}
|
|
1803
|
+
for (let i = 1; i < objs.length; i++) {
|
|
1804
|
+
const [off, obj] = objs[i];
|
|
1805
|
+
const [prev, prevObj] = objs[i - 1];
|
|
1806
|
+
if (overlaps(prev, span(prevObj), off, span(obj))) {
|
|
1807
|
+
model.shapeRefused(`the objects at [sp,#${prev}) and [sp,#${off}) overlap — one byte, two models`);
|
|
1808
|
+
}
|
|
1809
|
+
}
|
|
1810
|
+
return { extent, aggregateAt, overSlot };
|
|
1811
|
+
}
|
|
1812
|
+
|
|
1813
|
+
/** THE FRAME RESERVATION IS AN EXTENT, when the declared local area is provably one object's alone.
|
|
1814
|
+
* `add sp, sp, #-0x10` reserves sixteen bytes; if exactly one address-taken object sits at the
|
|
1815
|
+
* bottom of the DECLARED part of them and the slot model keys none of that part (`overSlot`, which
|
|
1816
|
+
* `objectShapes` asks of every extent), then there is nothing else those bytes can be — a
|
|
1817
|
+
* compiler does not reserve frame for nothing. That is the frame-accounting equation the escape
|
|
1818
|
+
* rules (`FRAME_ESCAPE_GATES`) already solve word by word, asked in the other direction: they
|
|
1819
|
+
* check that the objects and slots TILE the reserved area, this reads the declared range off as
|
|
1820
|
+
* the one object's size.
|
|
1821
|
+
*
|
|
1822
|
+
* THE DECLARED RANGE, NOT THE OWNED ONE. Below `declared.from` lies the outgoing stack-argument
|
|
1823
|
+
* block `analyzeOutgoingArgs` licensed — arguments 5+ of this function's calls, which C stages
|
|
1824
|
+
* and never declares — so those bytes are not this object's, and the slots keyed there are those
|
|
1825
|
+
* arguments. The escape gates read the frame the same way: `reaches-a-slot` looks no lower than
|
|
1826
|
+
* `declared.from`, and `reaches-an-unaccounted-word` counts the argument words as accounted.
|
|
1827
|
+
* agbcc lays its locals out like this (ACCUMULATE_OUTGOING_ARGS 1, STARTING_FRAME_OFFSET 0,
|
|
1828
|
+
* thumb.h): `struct S t; f(&t, b, c, d, e, g, h, i)` with an 8-byte `S` reserves 0x18, stages
|
|
1829
|
+
* the four argument words at [sp,#0..#0xc] and puts `t` at [sp,#0x10]. The block is LICENSED by
|
|
1830
|
+
* construction: one not stored on every path is `analyzeOutgoingArgs`'s own blocker, which turns
|
|
1831
|
+
* the slot model off, and every `laddr` mint is behind `slotsOk`.
|
|
1832
|
+
*
|
|
1833
|
+
* Returns why it does not apply, or null. Every clause refuses in its own words: the reason an
|
|
1834
|
+
* acceptance did not fire is as much an attribution as the reason a lift declined, and one
|
|
1835
|
+
* sentence covering all of them is how several gaps come to look like one.
|
|
1836
|
+
*
|
|
1837
|
+
* FOUR CLAUSES BOUND THIS PATH — a second object, an object that does not start at the bottom of
|
|
1838
|
+
* the declared area, an address that reaches memory rather than a callee, and the callee's
|
|
1839
|
+
* declared return — and each has a test that fails without it. A fifth return, the precautionary
|
|
1840
|
+
* one marked where it sits, has none.
|
|
1841
|
+
*
|
|
1842
|
+
* The last two are about an ESCAPE (`wholeAreaEscapeRefusal`), and they are asked only of an
|
|
1843
|
+
* object that escapes or that nothing in this function addresses. An object this function indexes
|
|
1844
|
+
* and never lets go of is written and read by its own indexed accesses alone, so the reservation
|
|
1845
|
+
* is all there is to size it by: `u8 a[8]; for (j = 0; j < 8; j++) a[j] = tbl[j]; return a[i];`. */
|
|
1846
|
+
function notTheWholeArea(
|
|
1847
|
+
off: number,
|
|
1848
|
+
indexedHere: boolean,
|
|
1849
|
+
{
|
|
1850
|
+
objects,
|
|
1851
|
+
uses,
|
|
1852
|
+
declared,
|
|
1853
|
+
returnsWithoutHiddenPointer,
|
|
1854
|
+
}: {
|
|
1855
|
+
objects: ReadonlyMap<number, readonly Op[]>;
|
|
1856
|
+
uses: FrameUses;
|
|
1857
|
+
declared: FrameRange;
|
|
1858
|
+
returnsWithoutHiddenPointer: (callee: string) => boolean;
|
|
1859
|
+
},
|
|
1860
|
+
): string | null {
|
|
1861
|
+
if (objects.size !== 1) {
|
|
1862
|
+
return 'another address-taken object shares the frame, so the reservation is not this one alone';
|
|
1863
|
+
}
|
|
1864
|
+
if (off !== declared.from || declared.to <= declared.from) {
|
|
1865
|
+
return 'the object does not start at the bottom of the declared area, so something below it is unaccounted for';
|
|
1866
|
+
}
|
|
1867
|
+
// PRECAUTIONARY for an object nothing indexes: an address that neither accesses nor escapes
|
|
1868
|
+
// already declines where the audit classifies its uses ("flows into `ret`"), so it never arrives
|
|
1869
|
+
// here unescaped. Kept because storage declared with no writer is a SILENT wrong answer.
|
|
1870
|
+
if (!uses.escaped.has(off)) {
|
|
1871
|
+
return indexedHere
|
|
1872
|
+
? null
|
|
1873
|
+
: 'the address never leaves this function, so there is no writer of the storage to size it for';
|
|
1874
|
+
}
|
|
1875
|
+
return wholeAreaEscapeRefusal(off, uses, returnsWithoutHiddenPointer);
|
|
1876
|
+
}
|
|
1877
|
+
|
|
1878
|
+
/** Why the escape of the address at `off` is not one the WHOLE-AREA argument holds, or null. That
|
|
1879
|
+
* argument — the object is every byte of the declared area, so whatever a holder of the address
|
|
1880
|
+
* does through it lands on bytes one declaration owns — is made twice: for one untyped object
|
|
1881
|
+
* (`notTheWholeArea`) and for the one object several captures are members of (`writerNotKept`).
|
|
1882
|
+
* It needs a callee to have been handed the address as an argument, the one holder this frontend
|
|
1883
|
+
* can name; an address that only reached memory has none. A store of the address beside that
|
|
1884
|
+
* call is held with it, whether to an ordinary global or into a device's source register — and
|
|
1885
|
+
* where the store hands the address to something that may write through it (`publishedToWriter`),
|
|
1886
|
+
* only when that callee may write through it too: a bounded block transfer reading its source
|
|
1887
|
+
* names no writer.
|
|
1888
|
+
*
|
|
1889
|
+
* The frame's SIZE changes nothing about whose storage it is, so the hidden-pointer question is
|
|
1890
|
+
* the one the one-word arm asks, asked here of the same callees. */
|
|
1891
|
+
function wholeAreaEscapeRefusal(
|
|
1892
|
+
off: number,
|
|
1893
|
+
uses: FrameUses,
|
|
1894
|
+
returnsWithoutHiddenPointer: (callee: string) => boolean,
|
|
1895
|
+
): string | null {
|
|
1896
|
+
if (!uses.passedToCallee.has(off)) {
|
|
1897
|
+
return 'the address is published rather than passed as an argument, and nothing declares what reads it';
|
|
1898
|
+
}
|
|
1899
|
+
if (uses.publishedToWriter.has(off) && !uses.passedToWriter.has(off)) {
|
|
1900
|
+
return (
|
|
1901
|
+
'the address is published, and the only callee handed it is a block transfer that reads through it, ' +
|
|
1902
|
+
'so nothing declares what reads it'
|
|
1903
|
+
);
|
|
1904
|
+
}
|
|
1905
|
+
return hiddenReturnPointerStands(off, uses.arg0Callees, returnsWithoutHiddenPointer);
|
|
1906
|
+
}
|
|
1907
|
+
|
|
1908
|
+
/** Every escaped object as the rules an escape retracts read it (`FRAME_ESCAPE_GATES`), computed
|
|
1909
|
+
* ONCE, with whether the escape may write and how it left, so a new bound — a callee's declared
|
|
1910
|
+
* extent — has one place to go. */
|
|
1911
|
+
function frameEscapes({
|
|
1912
|
+
irBlocks,
|
|
1913
|
+
uses,
|
|
1914
|
+
windows,
|
|
1915
|
+
shapes: { extent },
|
|
1916
|
+
owned,
|
|
1917
|
+
declared,
|
|
1918
|
+
usedSlotOffsets,
|
|
1919
|
+
}: {
|
|
1920
|
+
irBlocks: readonly Block[];
|
|
1921
|
+
uses: FrameUses;
|
|
1922
|
+
windows: ReadonlyMap<number, ReadWindow>;
|
|
1923
|
+
shapes: ObjectShapes;
|
|
1924
|
+
owned: FrameRange;
|
|
1925
|
+
declared: FrameRange;
|
|
1926
|
+
usedSlotOffsets: ReadonlySet<number>;
|
|
1927
|
+
}): FrameEscape[] {
|
|
1928
|
+
const undefSlots = irBlocks
|
|
1929
|
+
.flatMap((blk) => blk.ops.filter((op) => op.opcode === 'undef'))
|
|
1930
|
+
.map((op) => slotKeyOffset(op.attrs.key as string))
|
|
1931
|
+
.filter((slot) => slot !== null)
|
|
1932
|
+
.sort((x, y) => x - y);
|
|
1933
|
+
const accountedWords = new Set<number>();
|
|
1934
|
+
for (const [off, obj] of extent) {
|
|
1935
|
+
for (let w = off - (off % 4); w < off + span(obj); w += 4) {
|
|
1936
|
+
accountedWords.add(w);
|
|
1937
|
+
}
|
|
1938
|
+
}
|
|
1939
|
+
for (const slot of usedSlotOffsets) {
|
|
1940
|
+
accountedWords.add(slot - (slot % 4));
|
|
1941
|
+
}
|
|
1942
|
+
const unaccountedIn = (lo: number, hi: number): number | undefined => {
|
|
1943
|
+
for (let w = owned.from; w < owned.to; w += 4) {
|
|
1944
|
+
if (!accountedWords.has(w) && w < hi && w + 4 > lo) {
|
|
1945
|
+
return w;
|
|
1946
|
+
}
|
|
1947
|
+
}
|
|
1948
|
+
return undefined;
|
|
1949
|
+
};
|
|
1950
|
+
const escapes: FrameEscape[] = [];
|
|
1951
|
+
for (const off of uses.escaped) {
|
|
1952
|
+
const { lo, hi, why } = windows.get(off)!;
|
|
1953
|
+
const writes = mayWrite(uses, off);
|
|
1954
|
+
const objectReached = [...extent].find(([o, obj]) => o !== off && o < off + hi && off + lo < o + span(obj));
|
|
1955
|
+
let slotReached: FrameEscape['slotReached'];
|
|
1956
|
+
for (const slot of usedSlotOffsets) {
|
|
1957
|
+
if (slot > off && slot < off + hi) {
|
|
1958
|
+
slotReached = { slot, above: true };
|
|
1959
|
+
break;
|
|
1960
|
+
}
|
|
1961
|
+
if (slot < off && slot >= declared.from && slot + 4 > off + lo) {
|
|
1962
|
+
slotReached = { slot, above: false };
|
|
1963
|
+
break;
|
|
1964
|
+
}
|
|
1965
|
+
}
|
|
1966
|
+
escapes.push({
|
|
1967
|
+
off,
|
|
1968
|
+
lo,
|
|
1969
|
+
hi,
|
|
1970
|
+
writes,
|
|
1971
|
+
how: uses.passedToCallee.has(off)
|
|
1972
|
+
? 'is passed to a callee'
|
|
1973
|
+
: writes
|
|
1974
|
+
? 'is stored to memory'
|
|
1975
|
+
: `is handed to a device ${why}`,
|
|
1976
|
+
objectReached: objectReached?.[0],
|
|
1977
|
+
slotReached,
|
|
1978
|
+
undefReached: undefSlots.find((slot) => slot < off + hi && slot + 4 > off + lo),
|
|
1979
|
+
unaccountedWord: unaccountedIn(off + lo, off + hi),
|
|
1980
|
+
});
|
|
1981
|
+
}
|
|
1982
|
+
return escapes;
|
|
1983
|
+
}
|
|
1984
|
+
|
|
1985
|
+
function escapeRefusal(id: string, e: FrameEscape): string {
|
|
1986
|
+
const at = `the captured address at [sp,#${e.off}) ${e.how}`;
|
|
1987
|
+
switch (id) {
|
|
1988
|
+
case 'reaches-another-object':
|
|
1989
|
+
return e.lo === -Infinity && e.hi === Infinity
|
|
1990
|
+
? 'the captured address escapes, so something outside this function reaches the whole frame — including another object'
|
|
1991
|
+
: `${at}, which may read the object at [sp,#${e.objectReached})`;
|
|
1992
|
+
case 'writer-over-undef':
|
|
1993
|
+
return e.lo === -Infinity && e.hi === Infinity
|
|
1994
|
+
? 'the captured address escapes, so a callee may write any frame offset and an unstored slot is not provably uninitialised'
|
|
1995
|
+
: `${at}, which may write the slot at [sp,#${e.undefReached}] — an unstored slot it reaches is not provably uninitialised`;
|
|
1996
|
+
case 'reaches-a-slot':
|
|
1997
|
+
return !e.slotReached!.above
|
|
1998
|
+
? `${at}, and it may point INTO an object that starts lower — the slot at [sp,#${e.slotReached!.slot}] below it is kept in a register, not the frame`
|
|
1999
|
+
: e.writes
|
|
2000
|
+
? `${at}, which may write the slot at [sp,#${e.slotReached!.slot}] — this function's own store there would be forwarded past the write`
|
|
2001
|
+
: `${at}, which may read the slot at [sp,#${e.slotReached!.slot}] — this function's own store there is kept in a register, not the frame`;
|
|
2002
|
+
default:
|
|
2003
|
+
return (
|
|
2004
|
+
`the word at [sp,#${e.unaccountedWord}] is neither an object this lift models nor a slot it keys, ` +
|
|
2005
|
+
`and the captured address reaches something that may ${e.writes ? 'write' : 'read'} it — nothing accounts for the ` +
|
|
2006
|
+
"rest of the frame, so nothing bounds the captured object's extent"
|
|
2007
|
+
);
|
|
2008
|
+
}
|
|
2009
|
+
}
|
|
2010
|
+
|
|
2011
|
+
/** Proven. Stamp the MACHINE FACTS the audit established — width and signedness are what the
|
|
2012
|
+
* accesses used, so the declaration downstream is a fact, not a guess. The C-level NAME is
|
|
2013
|
+
* deliberately NOT chosen here: identifiers live in the structurer's namespace (params,
|
|
2014
|
+
* locals, globals, the symbol map), which the frontend cannot see — a frontend-chosen `sp0`
|
|
2015
|
+
* silently shadowed a project global of the same name.
|
|
2016
|
+
* `volatile` iff the address is PUBLISHED — written to memory, rather than handed to a
|
|
2017
|
+
* callee. That is the DMA idiom this rule was written for and it IS the source's own
|
|
2018
|
+
* spelling there: klonoa's `DMA_FILL` writes `vu##bit tmp` outright, sa3's does under
|
|
2019
|
+
* `PLATFORM_GBA`, pokeemerald's inside `DMA_FILL_UNCHECKED`, and the address goes to a device
|
|
2020
|
+
* register through a store. Reproducing that source means reproducing the qualifier.
|
|
2021
|
+
*
|
|
2022
|
+
* …and iff it is the FIXED source of a block transfer (`filledFrom`), the same idiom through a
|
|
2023
|
+
* BIOS call: every fill macro writes `vu##bit tmp = value; CpuSet((void *)&tmp, …)` (sa3's
|
|
2024
|
+
* cpuset_macros.h, pokeemerald's macro.h). Compiled, the plain spelling is not the same
|
|
2025
|
+
* program: a `u16` parameter stored to a plain `u16 tmp` loses its `lsl`/`lsr` pair, and the
|
|
2026
|
+
* pool loads around the call reorder.
|
|
2027
|
+
*
|
|
2028
|
+
* NOT on an ordinary `&local` ARGUMENT, where no source in the corpus writes one and the
|
|
2029
|
+
* qualifier is not free. `void f(u32 i){ s32 w; w = gEnts[i].h; use(&w); four(w,w,w,w); }`
|
|
2030
|
+
* compiles to one `ldr` reloaded into four registers by copies; the structurer emits one C
|
|
2031
|
+
* read per USE rather than per machine load, so `volatile` forbids the CSE and makes it four
|
|
2032
|
+
* `ldr`s — a byte-exact candidate turned into a four-instruction nonmatch (compiled, agbcc
|
|
2033
|
+
* 2.9-arm-000512, `-O2 -mthumb-interwork -Wimplicit -fhex-asm -fprologue-bugfix`). It is free
|
|
2034
|
+
* only where the object is read at most once, which is all the rows that first shipped it
|
|
2035
|
+
* did. agbcc also warns `discards qualifiers` at every such call.
|
|
2036
|
+
*
|
|
2037
|
+
* NOT because gcc would otherwise delete the store. That claim was here for several releases
|
|
2038
|
+
* and does not reproduce: taking `&tmp` makes the local addressable, so gcc-2.9 keeps the
|
|
2039
|
+
* store with or without the qualifier, measured on store-then-escape, publish-then-fill, and
|
|
2040
|
+
* a loop that stores and escapes each iteration. What the qualifier does change is register
|
|
2041
|
+
* ALLOCATION — the same function compiled `vu16` and `u16` is 98 instructions either way and
|
|
2042
|
+
* differs in three register assignments — which is why it still has to be right. asmlift's
|
|
2043
|
+
* OWN dead-store pass keys on address-taken, not on the qualifier (l3/dce.ts), so dropping the
|
|
2044
|
+
* qualifier here cannot cost a store.
|
|
2045
|
+
*
|
|
2046
|
+
* An object whose address never leaves the function needs no volatile and must not pay it.
|
|
2047
|
+
*
|
|
2048
|
+
* AN UNTYPED OBJECT REACHES THIS RULE TOO, where the address is both published and handed to
|
|
2049
|
+
* a callee whose return is declared. It cannot pay the price above — that price is a read the
|
|
2050
|
+
* compiler may no longer fold, and an object with no access in this function has none —
|
|
2051
|
+
* compiled at the corpus's flags, the qualified and plain spellings are byte-identical and
|
|
2052
|
+
* differ in two `discards qualifiers` warnings. So the rule is the same rule, and the reason
|
|
2053
|
+
* it is free here is not the reason it is free on a scalar read once. */
|
|
2054
|
+
function stampObjects(
|
|
2055
|
+
objects: ReadonlyMap<number, readonly Op[]>,
|
|
2056
|
+
{ extent, aggregateAt }: ObjectShapes,
|
|
2057
|
+
uses: FrameUses,
|
|
2058
|
+
): void {
|
|
2059
|
+
for (const [off, ops] of objects) {
|
|
2060
|
+
const { width, count } = extent.get(off)!;
|
|
2061
|
+
const signed = uses.accesses.get(off)!.some((a) => a.signed);
|
|
2062
|
+
const aggregate = aggregateAt.get(off);
|
|
2063
|
+
for (const op of ops) {
|
|
2064
|
+
const qualified = uses.published.has(off) || uses.filledFrom.has(off);
|
|
2065
|
+
op.attrs = {
|
|
2066
|
+
...op.attrs,
|
|
2067
|
+
width,
|
|
2068
|
+
signed,
|
|
2069
|
+
count,
|
|
2070
|
+
...(qualified ? { volatile: true } : {}),
|
|
2071
|
+
...(aggregate !== undefined ? { aggregate: true } : {}),
|
|
2072
|
+
};
|
|
2073
|
+
// a return temp's address points at the struct the call returns
|
|
2074
|
+
if (aggregate !== undefined) {
|
|
2075
|
+
op.results[0].type = T.ptr(aggregate);
|
|
2076
|
+
}
|
|
2077
|
+
}
|
|
2078
|
+
}
|
|
2079
|
+
// The declaration carries the qualifier to an access spelled through the object's own name, and
|
|
2080
|
+
// not to one through a phi that carries its address: that phi is a pointer local typed from the
|
|
2081
|
+
// access, so `p = c ? &b : gOther; while (*p == 0);` would compile to a loop that never reads
|
|
2082
|
+
// `b` again. Such an access carries the qualifier itself.
|
|
2083
|
+
for (const [op, { off, through }] of uses.accessOps) {
|
|
2084
|
+
const own = objects.get(off)!.some((o) => o.results[0] === through);
|
|
2085
|
+
if (!own && (uses.published.has(off) || uses.filledFrom.has(off))) {
|
|
2086
|
+
op.attrs = { ...op.attrs, volatile: true };
|
|
2087
|
+
}
|
|
2088
|
+
}
|
|
2089
|
+
}
|
|
2090
|
+
|
|
2091
|
+
function gcd(a: number, b: number): number {
|
|
2092
|
+
return b === 0 ? Math.abs(a) : gcd(b, a % b);
|
|
2093
|
+
}
|
|
2094
|
+
|
|
2095
|
+
/** The size of the struct a call stamped `sret` returns through its argument 0 (frontend/thumb.ts
|
|
2096
|
+
* types the call's result as that struct). */
|
|
2097
|
+
function returnedSize(call: Op): number {
|
|
2098
|
+
const t = call.results[0].type;
|
|
2099
|
+
if (t.kind !== 'struct' || t.size === undefined) {
|
|
2100
|
+
throw new Error(`a call stamped sret returns ${typeToString(t)}, not a laid-out struct`);
|
|
2101
|
+
}
|
|
2102
|
+
return t.size;
|
|
2103
|
+
}
|
|
2104
|
+
|
|
2105
|
+
/** THE CALLER-SIDE SEAM. A frontend calls the audit THROUGH this record rather than through the
|
|
2106
|
+
* binding above, so a process outside core can put a wrapped `gates` table in front of a real
|
|
2107
|
+
* lift (`pnpm bench gates --pass frame-objects`): a module-namespace binding is read-only and
|
|
2108
|
+
* cannot be swapped (`apps/benchmark/src/run/gate-census.ts`, WHAT PUTS A PASS IN THE REGISTRY). */
|
|
2109
|
+
export const FRAME_OBJECT_AUDIT: { run: (audit: FrameObjectAudit) => FrameObjectVerdict } = {
|
|
2110
|
+
run: (audit) => auditFrameObjects(audit),
|
|
2111
|
+
};
|
|
2112
|
+
|
|
2113
|
+
/** Internal surface for this module's own tests, and for nothing else. `@asmlift/core` exports
|
|
2114
|
+
* every source path under `./*`, so a plain `export` here would put the audit's stages into the
|
|
2115
|
+
* package's public API, where no consumer should be able to depend on them. The name says what it
|
|
2116
|
+
* is; nothing in `src` reads it. */
|
|
2117
|
+
export const __testing = {
|
|
2118
|
+
frameRefusal,
|
|
2119
|
+
foldMovedCaptures,
|
|
2120
|
+
frameAddressFacts,
|
|
2121
|
+
splitAddressingCopies,
|
|
2122
|
+
frameObjects,
|
|
2123
|
+
addressFlow,
|
|
2124
|
+
classifyFrameUses,
|
|
2125
|
+
opPositions,
|
|
2126
|
+
escapeWindows,
|
|
2127
|
+
readWindow,
|
|
2128
|
+
chooseFrameModel,
|
|
2129
|
+
objectShapes,
|
|
2130
|
+
frameEscapes,
|
|
2131
|
+
};
|