@asmlift/core 0.5.0 → 0.6.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 +22 -16
- package/package.json +1 -1
- package/src/backend/c.ts +1 -0
- package/src/backend/cfamily.ts +238 -167
- package/src/backend/cpp.ts +1 -0
- package/src/backend/pascal.ts +26 -12
- package/src/contracts.ts +194 -39
- package/src/declare.ts +41 -4
- package/src/frontend/mips.ts +11 -0
- package/src/frontend/ppc.ts +43 -7
- package/src/frontend/ssa.ts +404 -29
- package/src/frontend/thumb.ts +2176 -686
- package/src/ir/alias.ts +54 -0
- package/src/ir/bits.ts +75 -0
- package/src/ir/core.ts +337 -2
- package/src/ir/opcodes.ts +140 -21
- package/src/ir/parse.ts +19 -2
- package/src/ir/print.ts +27 -2
- package/src/ir/simplify.ts +190 -3
- package/src/ir/struct-names.ts +42 -0
- package/src/ir/verify.ts +43 -49
- package/src/l3/address.ts +62 -0
- package/src/l3/argbase.ts +2 -1
- package/src/l3/ast.ts +464 -57
- package/src/l3/basecse.ts +664 -76
- package/src/l3/coalesce.ts +429 -43
- package/src/l3/dce.ts +31 -9
- package/src/l3/gates.ts +21 -0
- package/src/l3/hoist.ts +293 -14
- package/src/l3/homesplit.ts +285 -0
- package/src/l3/initfirst.ts +301 -0
- package/src/l3/inlinebase.ts +193 -0
- package/src/l3/mentions.ts +113 -0
- package/src/l3/mulfirst.ts +42 -0
- package/src/l3/nearbase.ts +152 -0
- package/src/l3/offmember.ts +371 -0
- package/src/l3/parkfirst.ts +96 -0
- package/src/l3/pollguard.ts +154 -0
- package/src/l3/ptrfield.ts +227 -0
- package/src/l3/regspell.ts +110 -85
- package/src/l3/reindex.ts +715 -78
- package/src/l3/scopebase.ts +644 -218
- package/src/l3/sinkinit.ts +40 -0
- package/src/l3/slotorder.ts +123 -0
- package/src/l3/storage.ts +48 -0
- package/src/l3/symbol-refs.ts +41 -8
- package/src/l3/tailmerge.ts +15 -0
- package/src/l3/typing.ts +198 -9
- package/src/l3/unmerge.ts +263 -0
- package/src/l3/unreduce.ts +971 -0
- package/src/l3/volatileptr.ts +207 -0
- package/src/l3/volatileval.ts +130 -0
- package/src/l3/volstore.ts +229 -0
- package/src/l3/zerosub.ts +62 -0
- package/src/pattern/engine.ts +236 -13
- package/src/pipeline.ts +157 -56
- package/src/proto.ts +112 -14
- package/src/raise/arrays.ts +6 -1
- package/src/raise/divpow2.ts +2 -2
- package/src/raise/globalshape.ts +1038 -0
- package/src/raise/gvn.ts +33 -18
- package/src/raise/latch.ts +126 -0
- package/src/raise/memberarrays.ts +594 -0
- package/src/raise/narrow.ts +124 -0
- package/src/raise/narrowlocal.ts +556 -0
- package/src/raise/paramwidth.ts +179 -0
- package/src/raise/pre-recovery.ts +97 -14
- package/src/raise/recover.ts +56 -23
- package/src/raise/retsink.ts +210 -10
- package/src/raise/shortcircuit.ts +474 -74
- package/src/raise/struct-arrays.ts +19 -2
- package/src/raise/structs.ts +33 -3
- package/src/rank-axes.ts +630 -0
- package/src/rank-declare.ts +256 -0
- package/src/rank.ts +1723 -272
- package/src/structure/analysis.ts +1392 -141
- package/src/structure/bitfields.ts +332 -0
- package/src/structure/globalaccess.ts +274 -0
- package/src/structure/hazards.ts +411 -20
- package/src/structure/loops.ts +2 -49
- package/src/structure/namecoalesce.ts +435 -0
- package/src/structure/structure.ts +2678 -526
- package/src/structure/switch-recover.ts +616 -144
- package/src/symbols.ts +62 -1
- package/src/target.ts +367 -24
- package/src/trace.ts +111 -32
package/src/frontend/thumb.ts
CHANGED
|
@@ -8,13 +8,17 @@
|
|
|
8
8
|
// filled. Trivial phis (one real operand) are removed afterwards so a loop-invariant
|
|
9
9
|
// register does not leak a spurious block parameter.
|
|
10
10
|
//
|
|
11
|
-
// Callee-saved stack frames: `push`/`pop` (and the `pop {rN}; bx rN` return idiom)
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
11
|
+
// Callee-saved stack frames: `push`/`pop` (and the `pop {rN}; bx rN` return idiom) emit nothing —
|
|
12
|
+
// the pushed registers are restored to the same values, so they fall through the decode/fill
|
|
13
|
+
// switch. They are not IGNORED, though, and two readers outside that switch depend on them.
|
|
14
|
+
// `savedRegs` reads the prologue's save set explicitly and hands it to `LiveInModel.uninitRegs`,
|
|
15
|
+
// which partitions a def-less read of a callee-saved register into an uninitialised LOCAL rather
|
|
16
|
+
// than a parameter — asm that saves nothing follows no such convention and keeps its live-in. And
|
|
17
|
+
// `makeFrameWalk` counts the pushed bytes (4 per register) as frame depth, which is what makes an
|
|
18
|
+
// incoming stack argument at `[sp, #N]` locatable at all. Because agbcc may
|
|
15
19
|
// copy a callee-saved argument (e.g. into r4) before touching r0, entry parameters are
|
|
16
20
|
// ordered by ABI register (r0, r1, …), not by the order they were first read.
|
|
17
|
-
import { Fn, Op, Successor, Value, mkOp, mkValue } from '../ir/core';
|
|
21
|
+
import { Block, Fn, Op, Successor, Value, mkOp, mkValue } from '../ir/core';
|
|
18
22
|
import type { Opcode } from '../ir/opcodes';
|
|
19
23
|
import { T } from '../ir/types';
|
|
20
24
|
import { type Prototypes, protoArity } from '../proto';
|
|
@@ -27,7 +31,7 @@ import { FrontendUnsupportedError } from './errors';
|
|
|
27
31
|
import { assertInputFormat } from './format';
|
|
28
32
|
import type { Frontend } from './frontend';
|
|
29
33
|
import { opaqueDest } from './opaque';
|
|
30
|
-
import { abiSortEntryParams, fallbackArgc, makeSsaBuilder, stackSlotKey } from './ssa';
|
|
34
|
+
import { abiSortEntryParams, fallbackArgc, makeSsaBuilder, slotKeyOffset, stackSlotKey } from './ssa';
|
|
31
35
|
|
|
32
36
|
interface Instr {
|
|
33
37
|
/** the CANONICAL spelling — legacy names are normalised (see LEGACY_MNEMONICS) so that every
|
|
@@ -125,15 +129,74 @@ const COND_OPCODE: Record<string, Opcode> = {
|
|
|
125
129
|
bhs: 'icmp_uge',
|
|
126
130
|
};
|
|
127
131
|
|
|
128
|
-
//
|
|
129
|
-
//
|
|
130
|
-
//
|
|
131
|
-
//
|
|
132
|
-
//
|
|
133
|
-
//
|
|
134
|
-
//
|
|
135
|
-
//
|
|
132
|
+
// The halfword encodings that ARE alignment fill, and the instruction each one is. 0x0000 is
|
|
133
|
+
// `lsls r0, r0, #0`; 0x46C0 is the ARM7TDMI Thumb NOP — what `nop` assembles to, and what objdump
|
|
134
|
+
// prints as `nop @ (mov r8, r8)`. It is decoded as `nop`, NOT as `mov r8, r8`: the two spell the
|
|
135
|
+
// same encoding, but `mov r8, r8` is modelled downstream as a READ of a callee-saved register,
|
|
136
|
+
// which invents a parameter — the same two bytes would then carry a signature the object does not
|
|
137
|
+
// have.
|
|
138
|
+
//
|
|
139
|
+
// ONE table, because this knowledge is spelled twice: here as encodings, and in `isPadInstr` below
|
|
140
|
+
// as instruction shapes. A third encoding added to one and forgotten in the other would produce a
|
|
141
|
+
// pad nothing ever prunes, silently and with no test failing — so a test walks this table through
|
|
142
|
+
// that predicate.
|
|
143
|
+
const PAD_ENCODINGS: { hw: number; mnemonic: string; ops: string[] }[] = [
|
|
144
|
+
{ hw: 0x0000, mnemonic: 'lsls', ops: ['r0', 'r0', '#0x00'] },
|
|
145
|
+
{ hw: 0x46c0, mnemonic: 'nop', ops: [] },
|
|
146
|
+
];
|
|
147
|
+
|
|
148
|
+
/** Is this instruction alignment fill, however a splitter spelled it? A block made ONLY of these
|
|
149
|
+
* is pool/section padding when unreachable — pruned in `decode`. A REACHABLE pad block is a real
|
|
150
|
+
* (degenerate) instruction and is kept. `lsl`/`lsls r0, r0, #0` is how 0x0000 is written; 0x46C0
|
|
151
|
+
* is `nop`, and the `mov r8, r8` spelling of it is normalised to `nop` at parse — so there is no
|
|
152
|
+
* r8 clause here, and this predicate judges exactly the shapes PAD_ENCODINGS can produce. */
|
|
153
|
+
const isPadInstr = (i: Instr) =>
|
|
154
|
+
i.mnemonic === 'nop' ||
|
|
155
|
+
((i.mnemonic === 'lsl' || i.mnemonic === 'lsls') &&
|
|
156
|
+
i.ops[0] === 'r0' &&
|
|
157
|
+
i.ops[1] === 'r0' &&
|
|
158
|
+
/^#0x?0*$/.test(i.ops[2] ?? ''));
|
|
159
|
+
|
|
160
|
+
// A `.2byte`/`.hword`/`.short` operand this pass may read as an ENCODING: a complete 16-bit hex
|
|
161
|
+
// literal and nothing else. `parseInt` stops at the first character it cannot use, so `0x0000+2`
|
|
162
|
+
// parsed as 0x0000 and `0xD001+2` as 0xD001 — an expression, a relocation or a symbol would have
|
|
163
|
+
// been rewritten into the instruction its PREFIX encodes, which is not the instruction the
|
|
164
|
+
// assembler emits. Anything else returns null and falls through to the loud refusal.
|
|
165
|
+
function halfwordLiteral(text: string): number | null {
|
|
166
|
+
return /^0[xX][0-9a-fA-F]{1,4}$/.test(text) ? parseInt(text, 16) : null;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// A splitter that writes the pad as data (`.2byte 0x0000`) and one that writes it as an
|
|
170
|
+
// instruction are describing the SAME two bytes, so turning the halfword into the instruction it
|
|
171
|
+
// encodes is a decode, not a fabrication — the same move the raw-BRANCH decoder below already
|
|
172
|
+
// makes for `.2byte 0xD10E`. Everything that decides what padding MEANS (isPadInstr, the
|
|
173
|
+
// reachability prune) then sees one representation instead of three spellings.
|
|
174
|
+
// Returns a FRESH Instr per call: later passes rewrite operands in place.
|
|
175
|
+
function padHalfword(v: number): Instr | null {
|
|
176
|
+
const e = PAD_ENCODINGS.find((p) => p.hw === v);
|
|
177
|
+
return e ? { mnemonic: e.mnemonic, ops: [...e.ops] } : null;
|
|
178
|
+
}
|
|
179
|
+
|
|
136
180
|
type XferKind = 'return' | 'uncond' | 'cond' | 'indirect';
|
|
181
|
+
|
|
182
|
+
/** Does straight-line control continue past this instruction into the bytes that follow it? The ONE
|
|
183
|
+
* definition of that question, so no caller can spell its own answer and drift from this one.
|
|
184
|
+
* Only an open instruction or a conditional branch continues. A `return`, an `uncond` branch and
|
|
185
|
+
* an `indirect` (computed/loaded PC write) all seal what follows: for `indirect` that is a
|
|
186
|
+
* DELIBERATE conservatism, not an oversight — this frontend does not model where a computed jump
|
|
187
|
+
* goes, so it cannot claim the next bytes are entered, and it declines on such a transfer
|
|
188
|
+
* elsewhere rather than guessing here. */
|
|
189
|
+
const controlContinuesPast = (ins: Instr) => {
|
|
190
|
+
const k = classifyXfer(ins);
|
|
191
|
+
return k === null || k === 'cond';
|
|
192
|
+
};
|
|
193
|
+
/** Classify a block-terminating control transfer, or `null` for a non-transfer instruction (the
|
|
194
|
+
* block falls through). The SINGLE source of truth for "what ends a Thumb block and how", so a
|
|
195
|
+
* transfer form cannot be modelled in one place and missed in another. A return via a restored
|
|
196
|
+
* link register is distinguished from a COMPUTED/loaded PC write (jump table / computed goto /
|
|
197
|
+
* register tail call), which this frontend does not model and must LOUD-FAIL rather than silently
|
|
198
|
+
* drop — mirroring the MIPS `jr` and PPC `bctr` guards. (agbcc dispatches a dense switch via
|
|
199
|
+
* `mov pc, rN`.) */
|
|
137
200
|
function classifyXfer(ins: Instr): XferKind | null {
|
|
138
201
|
const mn = ins.mnemonic;
|
|
139
202
|
if (mn === 'b') {
|
|
@@ -150,9 +213,16 @@ function classifyXfer(ins: Instr): XferKind | null {
|
|
|
150
213
|
}
|
|
151
214
|
// A write to PC is a control transfer. `mov pc, lr` restores the link register → return; any other
|
|
152
215
|
// computed/loaded PC write (`mov pc, rN` rN≠lr, `ldr pc, …`, `add/sub pc, …`) is an indirect jump.
|
|
216
|
+
//
|
|
217
|
+
// BY REGISTER, NOT BY ALIAS. `pc` and `r15` are one register, and `readData` already says so; here
|
|
218
|
+
// the alias decides CONTROL FLOW, so missing it does not degrade — the write is not a transfer at
|
|
219
|
+
// all, the terminator never forms, and execution runs on into the next block. `mov r15, lr` mid
|
|
220
|
+
// function deleted the early return outright AND minted a phantom parameter for the `lr` read that
|
|
221
|
+
// was left behind, with no diagnostic. `lr`/`r14` is the same question one operand over, and it is
|
|
222
|
+
// the safe half: missing it reads as an indirect jump, which declines loud.
|
|
153
223
|
const dest = ins.ops[0]?.replace(/[[\]]/g, '');
|
|
154
|
-
if (dest === 'pc') {
|
|
155
|
-
if ((mn === 'mov' || mn === 'movs') && ins.ops[1] === 'lr') {
|
|
224
|
+
if (dest === 'pc' || dest === 'r15') {
|
|
225
|
+
if ((mn === 'mov' || mn === 'movs') && (ins.ops[1] === 'lr' || ins.ops[1] === 'r14')) {
|
|
156
226
|
return 'return';
|
|
157
227
|
}
|
|
158
228
|
return 'indirect';
|
|
@@ -179,7 +249,49 @@ function classifyXfer(ins: Instr): XferKind | null {
|
|
|
179
249
|
return null;
|
|
180
250
|
}
|
|
181
251
|
|
|
182
|
-
|
|
252
|
+
// A raw immediate's value. Case-insensitive on the radix because gas accepts `0X` and `parseInt`
|
|
253
|
+
// with the wrong one reads `#0X1` as 0 — see `immEq` below for why this stays loose about binary,
|
|
254
|
+
// octal and expressions, and which callers therefore have to refuse rather than ask this.
|
|
255
|
+
const imm = (s: string) => parseInt(s.replace(/^#/, ''), /0[xX]/.test(s) ? 16 : 10);
|
|
256
|
+
|
|
257
|
+
// AN IMMEDIATE IS A NUMBER, NOT A SPELLING, and which spelling appears is the producer's choice
|
|
258
|
+
// rather than the machine's. Counting the gated shapes (`add`/`rsb rD, rS, #0`) over the vendored
|
|
259
|
+
// checkouts, every producer writes `#0` — sa3's split 11158, sa3's build 9837, klonoa's build 1072,
|
|
260
|
+
// and `#0x0` not once between them — EXCEPT klonoa's own disassembly, which writes `#0x0` 3533
|
|
261
|
+
// times against 36. So an idiom keyed on the token is off for one whole project in silence, which
|
|
262
|
+
// is what this predicate exists to stop. `#2`, `#0x2` and `#0x02` are likewise one shift, and the
|
|
263
|
+
// jump-table shift recogniser used to compare the operand text and so rejected the third.
|
|
264
|
+
//
|
|
265
|
+
// The operand must be a plain integer LITERAL, and that shape check is the whole point of this
|
|
266
|
+
// helper rather than an incidental guard. `imm()` is `parseInt`, which stops at the first character
|
|
267
|
+
// it cannot consume — `#0b1` and `#0.5` read as 0, `#010` as 10 where gas means 8 — and it reads
|
|
268
|
+
// `#2*2` as 2, which is not malformed but an expression gas
|
|
269
|
+
// accepts and assembles to `lsls r0, r1, #4`. Matching it as a shift by two would recover a switch
|
|
270
|
+
// whose stride is wrong by a factor of four: the emitted C is entirely ordinary and dispatches to
|
|
271
|
+
// the wrong BLOCK, with no marker. `#2+1`, `#2-1` and `#2<<1` are the same trap. An adversarial
|
|
272
|
+
// probe found this after the first cut of this helper shipped with exactly that hole, and the test
|
|
273
|
+
// that claimed to pin the property sampled only `#3`/`#0x3`/`#0x1`/`r2` and so passed anyway.
|
|
274
|
+
//
|
|
275
|
+
// Anything that is not a bare decimal or hex literal therefore fails this test — which is a
|
|
276
|
+
// DECLINE only where the caller declines, and the three callers differ. The jump-table recogniser
|
|
277
|
+
// returns null; the `add` and `rsb` copy/negate arms fall through to their own lowering, and for
|
|
278
|
+
// `add`/`sub` that lowering is `constVal(imm(…))`, which is loose. So a non-match is a refusal at
|
|
279
|
+
// two of the three sites and a possibly-wrong constant at the third; `#0b1` renders `+ 0` there.
|
|
280
|
+
// The hole is `imm`'s rather than this predicate's, and a characterization test pins it.
|
|
281
|
+
//
|
|
282
|
+
// The refused class that DOES occur is the signed literal: 722 negative immediates across the
|
|
283
|
+
// corpus (`#-0x4` ×287, `#-0x004`, …), all of them inert here because neither 0 nor 2 is negative —
|
|
284
|
+
// but `imm` reads them correctly and this does not, so the two disagree on a populated class and a
|
|
285
|
+
// future `want` has to know that.
|
|
286
|
+
//
|
|
287
|
+
// One divergence is knowingly left in, and it is inert at both values this is used with.
|
|
288
|
+
// A leading zero means octal to gas and decimal to `Number`, so `#010` is 8 there and 10 here —
|
|
289
|
+
// but for `want === 2` both readings fail and the dispatch declines either way, and for
|
|
290
|
+
// `want === 0` every octal spelling of zero (`#0`, `#00`, `#000`) is zero under both. A `want`
|
|
291
|
+
// other than those two has to revisit this, because for e.g. `want === 8` the readings disagree.
|
|
292
|
+
const IMM_LITERAL = /^#\s*(?:0[xX][0-9a-fA-F]+|[0-9]+)$/;
|
|
293
|
+
const immEq = (op: string | undefined, want: number): boolean =>
|
|
294
|
+
op !== undefined && IMM_LITERAL.test(op) && Number(op.slice(1).trim()) === want;
|
|
183
295
|
|
|
184
296
|
// Expand fused register-range tokens (`r4-r7` → r4,r5,r6,r7) in a register list. Ranges are
|
|
185
297
|
// numeric-endpoint only (`rN-rM`); a range whose endpoint is an ALIAS (`r4-pc`/`-lr`/`-sp`) is
|
|
@@ -192,6 +304,12 @@ const imm = (s: string) => parseInt(s.replace(/^#/, ''), s.includes('0x') ? 16 :
|
|
|
192
304
|
// depth. Unreachable from real assembly; the guarantee should not depend on that.
|
|
193
305
|
const REG_NUM: Record<string, number> = Object.assign(Object.create(null), { sp: 13, lr: 14, pc: 15 });
|
|
194
306
|
|
|
307
|
+
// Registers Thumb-1's `push` cannot name, in every spelling this ISA's asm uses for them. Saving
|
|
308
|
+
// one takes agbcc's `mov rLow, rHi; push {rLow}`, which is why the prologue's save set has to read
|
|
309
|
+
// the `mov` as well as the list (see `savedRegs`). Spellings, not numbers: nothing here normalises
|
|
310
|
+
// a register name, so `sl` and `r10` are separate keys everywhere the frontend uses one.
|
|
311
|
+
const HIGH_REGS: ReadonlySet<string> = new Set(['r8', 'r9', 'r10', 'r11', 'r12', 'sb', 'sl', 'fp', 'ip']);
|
|
312
|
+
|
|
195
313
|
// Thumb-1 data-processing mnemonics that write the condition flags when their destination is a LOW
|
|
196
314
|
// register — which is all of them on this ISA, `s`-suffix or not (the assembler picks the encoding).
|
|
197
315
|
// Used to invalidate a pending compare: see the decode loop. `cmp`/`cmn`/`tst` are absent on purpose
|
|
@@ -261,19 +379,28 @@ function expandRegList(tokens: string[]): string[] {
|
|
|
261
379
|
|
|
262
380
|
// Expand a register list and vouch that every entry is a DEFINITE register, or return null.
|
|
263
381
|
//
|
|
264
|
-
//
|
|
265
|
-
// frame walk
|
|
266
|
-
//
|
|
267
|
-
//
|
|
268
|
-
//
|
|
269
|
-
// fabricated from a token that names no register, which is the phantom this frontend's guards
|
|
270
|
-
// exist to prevent.
|
|
382
|
+
// ONE validation for every consumer, because the hand-rolled versions had drifted to unequal
|
|
383
|
+
// strength. The frame walk required every token to be a real register; the ldm/stm arm only
|
|
384
|
+
// rejected a leftover `-`, so `ldmia r1!, {foo}` lifted and emitted
|
|
385
|
+
// `s32 f(s32 a0, s32 a1) { return a0; }` — a parameter fabricated from a token that names no
|
|
386
|
+
// register, which is the phantom this frontend's guards exist to prevent.
|
|
271
387
|
//
|
|
272
388
|
// An unexpandable range leaves its raw `-` token (see expandRegList) and fails here; so does an
|
|
273
389
|
// unknown alias, which a `Number.isNaN(regNum(t))` test would MISS, since regNum returns undefined
|
|
274
390
|
// for one and `Number.isNaN(undefined)` is false. An empty list is a malformed list, not an empty
|
|
275
|
-
// transfer.
|
|
276
|
-
//
|
|
391
|
+
// transfer.
|
|
392
|
+
//
|
|
393
|
+
// LOWERCASE-ONLY IS A HOLE, NOT A FREEBIE. GNU as accepts `PUSH {R4, LR}`, and the three consumers
|
|
394
|
+
// of this predicate do not agree
|
|
395
|
+
// about it. The ldm/stm arm degrades to a loud opaque and the frame walk poisons its depth, both of
|
|
396
|
+
// which decline; `savedRegs` does neither. It `break`s out of the prologue scan on an unreadable
|
|
397
|
+
// list, which yields a SMALLER save set rather than no answer, so the def-less `r4` it should have
|
|
398
|
+
// partitioned into an uninitialised local becomes a parameter instead. Measured 2026-09-06 on
|
|
399
|
+
// `push {r4,lr}; add r0,r4,#0; pop {r4}; bx lr`: lowercase gives `s32 f(void)` with `uninit_r4`,
|
|
400
|
+
// while both `push {R4, LR}` and `PUSH {r4, lr}` give `s32 f(s32 a0) { return a0; }`. Nothing in
|
|
401
|
+
// the corpus reaches it — 0 uppercase register tokens across the reference asm of the benchmark's
|
|
402
|
+
// agbcc rows — but folding case belongs in `expandRegList`, where it also changes classifyXfer's
|
|
403
|
+
// `popsPc`, i.e. block splitting. That is a measured change, not a free one.
|
|
277
404
|
function definiteRegList(tokens: string[]): string[] | null {
|
|
278
405
|
const list = expandRegList(tokens);
|
|
279
406
|
if (list.length === 0) {
|
|
@@ -282,6 +409,24 @@ function definiteRegList(tokens: string[]): string[] | null {
|
|
|
282
409
|
return list.every((s) => /^r\d+$/.test(s) || s in REG_NUM) ? list : null;
|
|
283
410
|
}
|
|
284
411
|
|
|
412
|
+
/** The braced register list of a `push`/`pop`/`ldmia`/`stmia`, as definite registers or null.
|
|
413
|
+
*
|
|
414
|
+
* `splitOperands` has already torn `{r4, r5, lr}` at its commas, so the list arrives as several
|
|
415
|
+
* operand tokens with the braces on the outer two; re-joining and re-splitting is what puts it
|
|
416
|
+
* back together. Three consumers ask this — the frame walk's `delta`, `savedRegs`, and the ldm/stm
|
|
417
|
+
* arm (which slices its base register off first) — and they get ONE tokenizer, because the
|
|
418
|
+
* tokenizing is where the drift that {@link definiteRegList} documents started. */
|
|
419
|
+
function regListOf(ops: string[]): string[] | null {
|
|
420
|
+
return definiteRegList(
|
|
421
|
+
ops
|
|
422
|
+
.join(',')
|
|
423
|
+
.replace(/[{}]/g, '')
|
|
424
|
+
.split(',')
|
|
425
|
+
.map((r) => r.trim())
|
|
426
|
+
.filter(Boolean),
|
|
427
|
+
);
|
|
428
|
+
}
|
|
429
|
+
|
|
285
430
|
// Split an operand list on commas that are NOT inside brackets, so a memory operand like
|
|
286
431
|
// `[r0, #0x8]` (base + offset) stays a single token instead of being torn at its comma.
|
|
287
432
|
function splitOperands(s: string): string[] {
|
|
@@ -307,15 +452,25 @@ function splitOperands(s: string): string[] {
|
|
|
307
452
|
return out;
|
|
308
453
|
}
|
|
309
454
|
|
|
310
|
-
// Parse a Thumb memory addressing operand `[base]` or `[base,
|
|
311
|
-
// constant byte offset
|
|
312
|
-
//
|
|
455
|
+
// Parse a Thumb memory addressing operand `[base]`, `[base, #off]` or `[base, rX]` into base
|
|
456
|
+
// register + constant byte offset + optional register index.
|
|
457
|
+
//
|
|
458
|
+
// KNOWN HOLE: only the first TWO tokens are read. A third — the scale in `[rB, rX, lsl #2]`, or a
|
|
459
|
+
// second index register — is dropped with no diagnostic, and the load arm then lowers the result as
|
|
460
|
+
// a plain `rB + rX`. Measured 2026-09-06: `ldr r0, [r0, r1, lsl #2]` lifts to
|
|
461
|
+
// `*(s32 *)(a0 + a1)`, silently wrong by a factor of four. ARMv4T Thumb has no scaled-index load
|
|
462
|
+
// encoding, so nothing an agbcc-shaped input contains reaches it — but this frontend parses TEXT,
|
|
463
|
+
// and the honest fix is a decline here rather than a comment. Adding one is a behaviour change and
|
|
464
|
+
// has to be measured.
|
|
313
465
|
function parseAddr(operand: string): { base: string; off: number; regOff?: string } {
|
|
314
466
|
const inner = operand.replace(/[[\]]/g, '').trim();
|
|
315
467
|
const parts = inner.split(',').map((s) => s.trim());
|
|
316
468
|
const base = parts[0];
|
|
317
|
-
// `[rB, rX]` — REGISTER-offset addressing
|
|
318
|
-
//
|
|
469
|
+
// `[rB, rX]` — REGISTER-offset addressing, surfaced rather than swallowed, for two different
|
|
470
|
+
// callers: the sp paths (argIndex, the slot arms, isFrameObjectAccess) all refuse a register
|
|
471
|
+
// index outright, and the ordinary load/store path lowers it as an explicit `rB + rX` add before
|
|
472
|
+
// a load at offset 0. Reading it as a bare `[rB]` drops the index, and `ldrsh` exists ONLY in
|
|
473
|
+
// this form in Thumb-1, so every `ldrsh` goes through this branch.
|
|
319
474
|
if (parts[1] !== undefined && !parts[1].startsWith('#')) {
|
|
320
475
|
return { base, off: 0, regOff: parts[1] };
|
|
321
476
|
}
|
|
@@ -323,6 +478,227 @@ function parseAddr(operand: string): { base: string; off: number; regOff?: strin
|
|
|
323
478
|
return { base, off };
|
|
324
479
|
}
|
|
325
480
|
|
|
481
|
+
// ── FRAME AND REGISTER PREDICATES ────────────────────────────────────────────────────────────
|
|
482
|
+
// Capture-free helpers: pure functions of an instruction, an operand token or an explicit
|
|
483
|
+
// dependency bag. Nothing here reads lift state, which is why they sit at module level — the size
|
|
484
|
+
// of `lift` should reflect what it actually decides.
|
|
485
|
+
|
|
486
|
+
const reg = (s: string) => s.replace(/[[\]]/g, '');
|
|
487
|
+
|
|
488
|
+
// THE one test for "is this token the stack pointer". Case-insensitive because GNU as accepts
|
|
489
|
+
// uppercase register names, and a case-sensitive test here is a silent-wrong-answer hole rather
|
|
490
|
+
// than a cosmetic one: `add r0, SP, #4` is `&local`, and missing it fabricates a phantom
|
|
491
|
+
// parameter and emits confident arithmetic on it.
|
|
492
|
+
const isSpReg = (s: string | undefined): boolean => {
|
|
493
|
+
const r = reg(s ?? '').toLowerCase();
|
|
494
|
+
return r === 'sp' || r === 'r13';
|
|
495
|
+
};
|
|
496
|
+
|
|
497
|
+
const isFrameAdjust = (
|
|
498
|
+
mnemonic: string,
|
|
499
|
+
dest: string | undefined,
|
|
500
|
+
base: string | undefined,
|
|
501
|
+
off: string | undefined,
|
|
502
|
+
): boolean =>
|
|
503
|
+
(mnemonic === 'add' || mnemonic === 'sub') &&
|
|
504
|
+
isSpReg(dest) &&
|
|
505
|
+
(base === undefined || isSpReg(base)) &&
|
|
506
|
+
(off?.startsWith('#') ?? false);
|
|
507
|
+
|
|
508
|
+
// A virtual register key per incoming stack argument. Reading it goes through the ordinary Braun
|
|
509
|
+
// live-in path (frontend/ssa.ts), which turns a read with no reaching def into a function
|
|
510
|
+
// parameter — so this needs NO new representation, opcode or pass. The `@` cannot appear in a
|
|
511
|
+
// real register token, so the key cannot collide with one.
|
|
512
|
+
const stackArgKey = (index: number) => `@sarg${index}`;
|
|
513
|
+
// Defined beside its mint site on purpose: the format string and its parser drifted 650 lines
|
|
514
|
+
// apart in the first version, with the convention explained only at one end.
|
|
515
|
+
const stackArgIndex = (key: string): number | null => {
|
|
516
|
+
const m = /^@sarg(\d+)$/.exec(key);
|
|
517
|
+
return m ? +m[1] : null;
|
|
518
|
+
};
|
|
519
|
+
|
|
520
|
+
// INCOMING STACK ARGUMENTS (AAPCS). Args 1-4 arrive in r0-r3; args 5+ are pushed by the CALLER,
|
|
521
|
+
// so the callee reads them at `[sp, #N]` where N is at or above its own frame. Those are
|
|
522
|
+
// PARAMETERS, not locals — declining them as "sp used as data" refuses a calling convention.
|
|
523
|
+
//
|
|
524
|
+
// The frame depth is tracked by a linear walk of the ENTRY BLOCK only, which is
|
|
525
|
+
// why the gate is what it is: within one straight-line block the depth at each instruction is
|
|
526
|
+
// exact and needs no CFG reasoning. Every case that would need more declines.
|
|
527
|
+
//
|
|
528
|
+
// `push {a,b,c}` deepens by 4 per register; `sub sp, #N` and `add sp, sp, #-N` deepen by N.
|
|
529
|
+
//
|
|
530
|
+
// The walk, its two invariants and the predicate that reads them are ONE object on purpose. They
|
|
531
|
+
// were three loose pieces — a delta function, a pair of `let`s updated in the instruction loop,
|
|
532
|
+
// and a seven-parameter predicate taking the pair by value — and both bugs the second review pass
|
|
533
|
+
// found lived in the seams: an invariant the predicate's proof needed but only the loop could
|
|
534
|
+
// enforce, and a `0` returned for an unrecognised shape that only a guard in a third place made
|
|
535
|
+
// safe. Anything that changes how sp moves now has one place to change, and the proof it has to
|
|
536
|
+
// preserve is written next to the state it is about.
|
|
537
|
+
//
|
|
538
|
+
// `deps` carries the only two things about the FUNCTION this needs — the ABI's argument-register
|
|
539
|
+
// count, and whether the entry block has predecessors — so the walk itself reads nothing but the
|
|
540
|
+
// instructions it is stepped over.
|
|
541
|
+
const makeFrameWalk = (deps: { argRegs: readonly string[]; entryHasPreds: boolean }) => {
|
|
542
|
+
// Bytes the frame has grown since function entry, along this block's linear order only.
|
|
543
|
+
let depth = 0;
|
|
544
|
+
let depthKnown = true;
|
|
545
|
+
|
|
546
|
+
// Returns null whenever the depth cannot be computed exactly — including for any write to sp
|
|
547
|
+
// this does not model. The capability rests on the depth being EXACT, so an approximation is
|
|
548
|
+
// never acceptable: understate the frame and a local sits above the computed top and gets
|
|
549
|
+
// minted as a parameter reading uninitialised stack. A null poisons the depth for the rest of
|
|
550
|
+
// the block, which disables argument recovery and leaves every `[sp,#N]` to decline as before.
|
|
551
|
+
// Nothing here may return a NUMBER for a shape it merely failed to recognise.
|
|
552
|
+
const delta = (ins: { mnemonic: string; ops: string[] }): number | null => {
|
|
553
|
+
const m = ins.mnemonic;
|
|
554
|
+
if (m === 'push' || m === 'pop') {
|
|
555
|
+
// expandRegList, NOT a comma count: `push {r4-r7, lr}` is FIVE registers, and counting it as
|
|
556
|
+
// two makes the frame 12 bytes too shallow — which turns a genuine LOCAL into a fabricated
|
|
557
|
+
// parameter reading uninitialised stack. Caught by a probe, not by the corpus: agbcc emits no
|
|
558
|
+
// range pushes at all — re-counted 2026-09-06 over the 269 of the benchmark's 404 agbcc rows
|
|
559
|
+
// whose reference asm the local bench cache holds, where NO `push`/`pop` list carries a range
|
|
560
|
+
// — but GNU as accepts them and the disassembly path can produce them. (The population is the
|
|
561
|
+
// rows, not the cache's file count: a cache also holds entries for rows that no longer exist,
|
|
562
|
+
// so two checkouts of this commit disagree about how many files there are and agree about
|
|
563
|
+
// this.)
|
|
564
|
+
// Counting comma tokens instead would undercount `{r4-lr}` as two registers, and an
|
|
565
|
+
// unexpandable range must poison the depth rather than be guessed at — definiteRegList owns
|
|
566
|
+
// both rules, and owns them for the ldm/stm arm too.
|
|
567
|
+
const list = regListOf(ins.ops);
|
|
568
|
+
if (list === null) {
|
|
569
|
+
return null;
|
|
570
|
+
}
|
|
571
|
+
return (m === 'push' ? 1 : -1) * 4 * list.length;
|
|
572
|
+
}
|
|
573
|
+
if ((m === 'add' || m === 'sub') && isSpReg(ins.ops[0])) {
|
|
574
|
+
const off = ins.ops[2] ?? ins.ops[1];
|
|
575
|
+
if (off?.startsWith('#')) {
|
|
576
|
+
const v = imm(off);
|
|
577
|
+
return m === 'sub' ? v : -v;
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
// Any OTHER write to sp poisons the depth. This used to fall through to 0 — "no change" — for
|
|
581
|
+
// shapes it does not model (`add sp, r4`, `mov sp, rN`, `add sp, r0, #4`), which was safe only
|
|
582
|
+
// because writeData declines every one of them elsewhere. That is the same
|
|
583
|
+
// enumeration-of-arms mistake writeData itself exists to end, exported one function away: a
|
|
584
|
+
// number meaning "no change" is the wrong answer to "I do not understand this". Now the walk is
|
|
585
|
+
// self-sufficient — unknown ⇒ depth poisoned ⇒ decline — and writeData's refusal is an
|
|
586
|
+
// independent second guarantee instead of a load-bearing one.
|
|
587
|
+
if (isSpReg(ins.ops[0])) {
|
|
588
|
+
return null;
|
|
589
|
+
}
|
|
590
|
+
return 0;
|
|
591
|
+
};
|
|
592
|
+
|
|
593
|
+
return {
|
|
594
|
+
// Advance the walk over one instruction. Call for EVERY instruction, in order.
|
|
595
|
+
step(ins: { mnemonic: string; ops: string[] }): void {
|
|
596
|
+
const d = delta(ins);
|
|
597
|
+
if (d === null) {
|
|
598
|
+
depthKnown = false;
|
|
599
|
+
} else {
|
|
600
|
+
depth += d;
|
|
601
|
+
}
|
|
602
|
+
// sp ABOVE the incoming sp poisons the walk for the rest of the block, permanently — the
|
|
603
|
+
// premise argIndex's proof rests on. See the proof there for what it costs to omit.
|
|
604
|
+
if (depth < 0) {
|
|
605
|
+
depthKnown = false;
|
|
606
|
+
}
|
|
607
|
+
},
|
|
608
|
+
|
|
609
|
+
// Is `[sp, #off]` an incoming stack argument, and which one? `null` means NO — and every null is
|
|
610
|
+
// a DECLINE, because the caller falls through to readData's sp guard.
|
|
611
|
+
//
|
|
612
|
+
// Why a slot at or above the frame top cannot have been written by this function, which is the
|
|
613
|
+
// whole soundness argument: every sp-relative STORE declines (readData, via the str arm), and a
|
|
614
|
+
// `push` only ever writes strictly BELOW the current top. An argument slot is at or above the
|
|
615
|
+
// incoming sp, so no instruction here can have defined it — the value can only be the caller's.
|
|
616
|
+
//
|
|
617
|
+
// That second step needs sp to have stayed at or below where it came in — `depth >= 0` at every
|
|
618
|
+
// point of the walk — or a `push` reaches back up over the argument area and the conclusion is
|
|
619
|
+
// false:
|
|
620
|
+
//
|
|
621
|
+
// add sp, sp, #8 ; sp = S+8
|
|
622
|
+
// push {r4, r5, r6} ; sp = S-4, and this WROTE r5 to S+0
|
|
623
|
+
// ldr r0, [sp, #4] ; = S+0 — r5's slot, not the caller's argument
|
|
624
|
+
//
|
|
625
|
+
// The depth is back to a plausible +4 by the load, so nothing downstream can tell: it emitted
|
|
626
|
+
// `s32 f(s32 a0, …, s32 a4) { return a4; }`, the function's own incoming r5 handed back as
|
|
627
|
+
// argument 5. `pop {r4}; push {r4,r5}` gets there without an `add sp` at all, and a sliced
|
|
628
|
+
// fragment whose prologue was cut off is exactly this shape — which is the corpus this frontend
|
|
629
|
+
// reads. `step` enforces it, and never un-poisons: once sp has been above the line, a push during
|
|
630
|
+
// the excursion may have written the later slots too. This is a PREMISE, not a detail — leaving
|
|
631
|
+
// it unstated is what let the first version hand back a callee-saved register as an argument.
|
|
632
|
+
argIndex(addr: { base: string; off: number; regOff?: string }, width: number, bi: number): number | null {
|
|
633
|
+
const { base, off, regOff } = addr;
|
|
634
|
+
if (!isSpReg(base) || regOff !== undefined) {
|
|
635
|
+
return null; // not sp, or `[sp, rX]` — not a fixed argument slot
|
|
636
|
+
}
|
|
637
|
+
if (bi !== 0 || deps.entryHasPreds) {
|
|
638
|
+
return null; // depth is exact only along the ENTRY block's linear order, and only when its
|
|
639
|
+
// params are parameters rather than phis
|
|
640
|
+
}
|
|
641
|
+
if (!depthKnown || depth <= 0) {
|
|
642
|
+
return null; // an unmeasurable frame, or none established: a headerless FRAGMENT whose
|
|
643
|
+
// prologue was sliced off looks identical to a frameless function, and there the slots are
|
|
644
|
+
// locals — minting one would be the silent-wrong trade this frontend refuses
|
|
645
|
+
}
|
|
646
|
+
if (width !== 4 || off < depth || (off - depth) % 4 !== 0) {
|
|
647
|
+
return null; // the argument area is word-granular; BELOW the top is a local, which is the
|
|
648
|
+
// separate slot-promotion capability
|
|
649
|
+
}
|
|
650
|
+
const index = deps.argRegs.length + (off - depth) / 4;
|
|
651
|
+
return index < MAX_RECOVERED_ARITY ? index : null; // a wild offset must not mint a
|
|
652
|
+
// 400-parameter signature
|
|
653
|
+
},
|
|
654
|
+
};
|
|
655
|
+
};
|
|
656
|
+
|
|
657
|
+
// Deliberately OVER-inclusive: a `cmp sp, rN` only reads sp but counts here too. Every false
|
|
658
|
+
// positive costs a decline, every false negative costs a wrong slot — so it errs loudly.
|
|
659
|
+
const modifiesSp = (ins: Instr): boolean =>
|
|
660
|
+
ins.mnemonic === 'push' || ins.mnemonic === 'pop' || isSpReg((ins.ops[0] ?? '').replace(/!$/, ''));
|
|
661
|
+
// A `mov rD, sp` CAPTURES the frame address; the captured value means "the frame base" only if
|
|
662
|
+
// sp still holds that base wherever the value is used — so a capture participates in the
|
|
663
|
+
// constancy proof exactly like a literal [sp,#k] access, and ends the prologue for localArea.
|
|
664
|
+
const capturesSp = (ins: Instr): boolean =>
|
|
665
|
+
/^movs?$/.test(ins.mnemonic) && !isSpReg(ins.ops[0] ?? '') && isSpReg(ins.ops[1] ?? '');
|
|
666
|
+
const touchesFrame = (ins: Instr): boolean => spMemAccess(ins) !== null || capturesSp(ins);
|
|
667
|
+
const spMemAccess = (ins: Instr): { off: number; width: number; regOff: boolean } | null => {
|
|
668
|
+
if (!/^(ldr|ldrb|ldrh|ldrsb|ldrsh|str|strb|strh)$/.test(ins.mnemonic)) {
|
|
669
|
+
return null;
|
|
670
|
+
}
|
|
671
|
+
const mem = ins.ops[1];
|
|
672
|
+
if (mem === undefined) {
|
|
673
|
+
return null;
|
|
674
|
+
}
|
|
675
|
+
const { base, off, regOff } = parseAddr(mem);
|
|
676
|
+
return isSpReg(base)
|
|
677
|
+
? { off, width: /b$/.test(ins.mnemonic) ? 1 : /h$/.test(ins.mnemonic) ? 2 : 4, regOff: regOff !== undefined }
|
|
678
|
+
: null;
|
|
679
|
+
};
|
|
680
|
+
|
|
681
|
+
// How much sp moves for `add/sub sp, #imm`, positive = sp RISES (frame shrinks). It models THAT
|
|
682
|
+
// SHAPE AND NOTHING ELSE — a `push`, a register-sized adjust, a `mov sp, rN` all answer null,
|
|
683
|
+
// which each of its three readers takes as "this is not a reservation or a release" and handles
|
|
684
|
+
// for itself: `localArea` sums the prologue's reservations and poisons to 0 on an sp write it
|
|
685
|
+
// cannot read, `savedRegs` ends the prologue run at the first instruction that is neither a save
|
|
686
|
+
// nor an adjust, and the pop/release scan uses it to recognise the epilogue's release. The
|
|
687
|
+
// authoritative depth arithmetic is `makeFrameWalk`'s `delta`, which counts pushes too.
|
|
688
|
+
const spAdjust = (ins: Instr): number | null => {
|
|
689
|
+
if (!/^(add|sub)$/.test(ins.mnemonic) || !isSpReg(ins.ops[0])) {
|
|
690
|
+
return null;
|
|
691
|
+
}
|
|
692
|
+
const o = ins.ops[2] ?? ins.ops[1];
|
|
693
|
+
if (o === undefined || !o.startsWith('#')) {
|
|
694
|
+
return null;
|
|
695
|
+
}
|
|
696
|
+
const v = imm(o);
|
|
697
|
+
return ins.mnemonic === 'sub' ? -v : v;
|
|
698
|
+
};
|
|
699
|
+
|
|
700
|
+
const isThumbReg = (s: string | undefined): s is string => /^r\d+$/.test(s ?? '');
|
|
701
|
+
|
|
326
702
|
/** Parse one function's GNU-as text into labelled basic blocks + the CFG, plus the inline `.word`
|
|
327
703
|
* data tables (label → the list of label operands under it) — the jump-table target arrays agbcc
|
|
328
704
|
* emits in `.text` (Regime B). Non-`.word` directives are skipped, EXCEPT sub-word data
|
|
@@ -339,9 +715,69 @@ interface FlatItem {
|
|
|
339
715
|
instr?: Instr;
|
|
340
716
|
/** a data directive's payload, kept in-stream so byte layout is computable */
|
|
341
717
|
data?: { halfwords: boolean; values: string[]; inCode: boolean };
|
|
718
|
+
/** an alignment directive whose fill byte count depends on the function's base alignment —
|
|
719
|
+
* resolved into pad instructions by the layout pass, which is the first place that knows it.
|
|
720
|
+
* `fill` is the halfword encoding the assembler repeats: 0x0000 for an explicit `, 0`, 0x46C0
|
|
721
|
+
* (the Thumb nop) for no fill operand. Both are PAD_ENCODINGS entries. */
|
|
722
|
+
align?: { pow: number; fill: number };
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
/** One hypothesis about a function's base alignment, laid out: the item stream with every
|
|
726
|
+
* alignment directive replaced by the pad instructions its fill bytes ARE, each item's byte
|
|
727
|
+
* offset, where each item came from in the unexpanded stream (-1 = an inserted fill), and the
|
|
728
|
+
* fill sites themselves. Two of these — base 0 and base 2 — are built for every slice. */
|
|
729
|
+
interface Candidate {
|
|
730
|
+
base: number;
|
|
731
|
+
items: FlatItem[];
|
|
732
|
+
offs: number[];
|
|
733
|
+
from: number[];
|
|
734
|
+
fills: { site: number; start: number; bytes: number }[];
|
|
342
735
|
}
|
|
343
736
|
|
|
344
|
-
|
|
737
|
+
/** One fact a candidate layout decided, as DATA rather than as a sentence. Two candidates are
|
|
738
|
+
* compared by these, so the comparison cannot depend on how a message happens to be worded — and
|
|
739
|
+
* a fact that must distinguish two layouts cannot lose the thing that distinguishes them. The
|
|
740
|
+
* branch target is an ITEM identity, never a byte offset: one layout's offsets mean nothing in
|
|
741
|
+
* the other. A target inside alignment fill carries WHICH fill and how far in, because two
|
|
742
|
+
* layouts that send the same branch into different fill sites disagree. */
|
|
743
|
+
type LayoutFact =
|
|
744
|
+
| { kind: 'branch'; at: number; to: { item: number } | { fill: number; off: number } | { off: number } }
|
|
745
|
+
| { kind: 'load'; at: number; value: string }
|
|
746
|
+
| { kind: 'fill'; site: number; bytes: number };
|
|
747
|
+
|
|
748
|
+
/** A candidate that survived resolution: the rewritten stream, the literal pools it discovered,
|
|
749
|
+
* and the WITNESS — every fact this layout decided, so two candidates can be COMPARED rather
|
|
750
|
+
* than merely counted. */
|
|
751
|
+
interface Resolved {
|
|
752
|
+
base: number;
|
|
753
|
+
items: FlatItem[];
|
|
754
|
+
pools: Map<string, string[]>;
|
|
755
|
+
witness: LayoutFact[];
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
/** The canonical serialisation a witness is compared BY, and the prose it is reported AS. Keeping
|
|
759
|
+
* them apart is the point: rewording `sayFact` changes a message, never a decision. */
|
|
760
|
+
const factKey = (f: LayoutFact) => JSON.stringify(f);
|
|
761
|
+
const sayFact = (f: LayoutFact): string => {
|
|
762
|
+
if (f.kind === 'load') {
|
|
763
|
+
return `pc-relative load at item ${f.at} → ${f.value}`;
|
|
764
|
+
}
|
|
765
|
+
if (f.kind === 'fill') {
|
|
766
|
+
return `alignment fill at item ${f.site} is ${f.bytes} bytes`;
|
|
767
|
+
}
|
|
768
|
+
const to =
|
|
769
|
+
'item' in f.to
|
|
770
|
+
? `item ${f.to.item}`
|
|
771
|
+
: 'fill' in f.to
|
|
772
|
+
? `alignment fill at item ${f.to.fill} (+${f.to.off} bytes)`
|
|
773
|
+
: `byte offset 0x${f.to.off.toString(16)}, which is no item`;
|
|
774
|
+
return `raw branch at item ${f.at} → ${to}`;
|
|
775
|
+
};
|
|
776
|
+
|
|
777
|
+
function decode(
|
|
778
|
+
name: string,
|
|
779
|
+
asm: string,
|
|
780
|
+
): { blocks: AsmBlock[]; dataWords: Map<string, string[]>; funcLabels: Set<string> } {
|
|
345
781
|
// Flatten to (label | instr | data) items, then split into blocks at labels / after branches.
|
|
346
782
|
// `.word LABEL` directives are captured into dataWords keyed by the most recent label (the
|
|
347
783
|
// jump table); ALL word/halfword data also stays in-stream as items, so the raw-halfword and
|
|
@@ -351,10 +787,22 @@ function decode(name: string, asm: string): { blocks: AsmBlock[]; dataWords: Map
|
|
|
351
787
|
const funcLabels: string[] = []; // labels marked as function starts (.thumb_func / pret macros)
|
|
352
788
|
const armLabels = new Set<string>(); // function starts declared ARM-mode (arm_func_start)
|
|
353
789
|
const subwordData = new Map<string, string>(); // label → sub-word data directive under it
|
|
354
|
-
// Directives
|
|
790
|
+
// Directives this pass cannot read completely, recorded by allFlat POSITION so the layout check
|
|
355
791
|
// is scoped to the SELECTED function's slice — a `.align` between two functions must not
|
|
356
|
-
// poison a sibling that needs byte-accurate layout.
|
|
357
|
-
|
|
792
|
+
// poison a sibling that needs byte-accurate layout. That scoping is what the `>= sliceStart`
|
|
793
|
+
// bound on the code-hazard scan below enforces; reaching one item lower admitted exactly the
|
|
794
|
+
// sibling this sentence forbids.
|
|
795
|
+
//
|
|
796
|
+
// `code` separates the two reasons an unreadable directive matters. An alignment directive emits its
|
|
797
|
+
// fill INTO THE INSTRUCTION STREAM, so when anything can execute those bytes the function
|
|
798
|
+
// cannot be lifted at all, whatever else the slice needs. A labelled data table's bytes are
|
|
799
|
+
// data: they can only shift the offsets of what follows, which matters only when something in
|
|
800
|
+
// the slice actually needs byte-accurate offsets.
|
|
801
|
+
//
|
|
802
|
+
// `why` says which question the directive left unanswered, so the refusal can state the fact it
|
|
803
|
+
// actually tested: 'size' (how many fill bytes) or 'fill' (which bytes they are). A data
|
|
804
|
+
// directive leaves only the first open.
|
|
805
|
+
const hazards: { at: number; what: string; code: boolean; why?: 'size' | 'fill' }[] = [];
|
|
358
806
|
let dataLabel: string | null = null;
|
|
359
807
|
let pendingFn = false;
|
|
360
808
|
let pendingArm = false;
|
|
@@ -397,15 +845,33 @@ function decode(name: string, asm: string): { blocks: AsmBlock[]; dataWords: Map
|
|
|
397
845
|
}
|
|
398
846
|
const hw = rest.match(/^\.(2byte|hword|short)\s+(.+)$/);
|
|
399
847
|
if (hw) {
|
|
848
|
+
const values = hw[2].split(',').map((w) => w.trim());
|
|
400
849
|
// In the instruction stream these are raw undecoded instructions (luvdis emits branches
|
|
401
850
|
// this way) — kept as items and DECODED (or declined) below. Under a label: a sub-word
|
|
402
851
|
// data table — declines below iff the selected function references it.
|
|
403
|
-
if (dataLabel
|
|
852
|
+
if (dataLabel === null) {
|
|
853
|
+
// …EXCEPT the halfwords that ENCODE alignment fill, decoded here to the instruction they
|
|
854
|
+
// are (see padHalfword), so the pad enters the stream as an `instr` item — the same item
|
|
855
|
+
// the `lsls r0, r0, #0` spelling of the same two bytes produces, and the same 2 bytes to
|
|
856
|
+
// the layout walk. That hands padding-versus-real-instruction to the ONE place that
|
|
857
|
+
// decides it: isPadInstr plus the unreachable-block prune below. Decoding it HERE rather
|
|
858
|
+
// than in the raw-branch pass matters: a function whose only in-code data was pad then
|
|
859
|
+
// needs no byte-accurate layout at all, exactly as it needs none under the instruction
|
|
860
|
+
// spelling.
|
|
861
|
+
const pads = values.map((v) => {
|
|
862
|
+
const hw = halfwordLiteral(v);
|
|
863
|
+
return hw === null ? null : padHalfword(hw);
|
|
864
|
+
});
|
|
865
|
+
if (pads.every((p) => p !== null)) {
|
|
866
|
+
for (const p of pads) {
|
|
867
|
+
flat.push({ instr: p! });
|
|
868
|
+
}
|
|
869
|
+
continue;
|
|
870
|
+
}
|
|
871
|
+
} else {
|
|
404
872
|
subwordData.set(dataLabel, hw[1]);
|
|
405
873
|
}
|
|
406
|
-
flat.push({
|
|
407
|
-
data: { halfwords: true, values: hw[2].split(',').map((w) => w.trim()), inCode: dataLabel === null },
|
|
408
|
-
});
|
|
874
|
+
flat.push({ data: { halfwords: true, values, inCode: dataLabel === null } });
|
|
409
875
|
continue;
|
|
410
876
|
}
|
|
411
877
|
const raw = rest.match(
|
|
@@ -419,11 +885,77 @@ function decode(name: string, asm: string): { blocks: AsmBlock[]; dataWords: Map
|
|
|
419
885
|
);
|
|
420
886
|
}
|
|
421
887
|
subwordData.set(dataLabel, raw[1]);
|
|
422
|
-
hazards.push({ at: flat.length - 1, what: `.${raw[1]}
|
|
888
|
+
hazards.push({ at: flat.length - 1, what: `.${raw[1]}`, code: false }); // size unknown / non-word
|
|
423
889
|
continue;
|
|
424
890
|
}
|
|
425
|
-
|
|
426
|
-
|
|
891
|
+
// `.align N[, fill]` emits (-address) mod 2^N bytes of `fill` — the pad, spelled as the
|
|
892
|
+
// directive that produces it. Kept as an item so the layout walk can size it (below); its
|
|
893
|
+
// byte count is not knowable here, which is why it was a hazard before.
|
|
894
|
+
//
|
|
895
|
+
// It asks TWO questions, and fusing them is what made the old refusal describe neither the
|
|
896
|
+
// code nor the assembler ("makes item sizes unknowable" for a form whose size is plain
|
|
897
|
+
// arithmetic). HOW MANY bytes is `-(base + at) mod 2^N` and does not depend on the fill at
|
|
898
|
+
// all; WHICH bytes those are is the fill operand. Only the second can make a sizable
|
|
899
|
+
// alignment unliftable, and only when something can execute the bytes.
|
|
900
|
+
//
|
|
901
|
+
// size unknown — a max-skip third operand (padding suppressed past a threshold this
|
|
902
|
+
// frontend does not model), a non-numeric N, or N above the 4-byte
|
|
903
|
+
// boundary the base is recovered to. This bound is load-bearing rather
|
|
904
|
+
// than cautious: the layout pass recovers the function's base address only
|
|
905
|
+
// MOD 4 (from 4-aligned pool words), so sizing `.align 3, 0` would mean
|
|
906
|
+
// picking one of four base residues the input says nothing about.
|
|
907
|
+
// fill unknown — an explicit fill this pass cannot map to a pad encoding (`.align 2, 0xFF`
|
|
908
|
+
// emits a real, undefined instruction, not padding).
|
|
909
|
+
//
|
|
910
|
+
// The two fills it CAN map are ground truth from `arm-none-eabi-as`, not inference: an
|
|
911
|
+
// explicit `, 0` gives 0x0000 (`lsls r0, r0, #0`), and NO fill operand in a Thumb code
|
|
912
|
+
// section gives 0x46C0 — `movs r0, #1` then `.align 2` assembles to `0120 c046`, the nop
|
|
913
|
+
// that is already PAD_ENCODINGS[1]. (gas pads to the next even offset with 0x00 first, but
|
|
914
|
+
// base ∈ {0, 2} and every item size is even, so the fill is always whole halfwords.) Both
|
|
915
|
+
// are pad encodings, so the fill enters the stream as the same `Instr` item the other two
|
|
916
|
+
// spellings produce and `isPadInstr` + the reachability prune decide it the same way.
|
|
917
|
+
//
|
|
918
|
+
// `.p2align` is `.align` with the power-of-two reading made explicit and `.balign` is the
|
|
919
|
+
// same directive counting BYTES; GNU as emits identical fill for all three. They are read
|
|
920
|
+
// here for the same reason the pad halfword is decoded above: a directive this pass does not
|
|
921
|
+
// recognise contributes ZERO bytes to the layout while emitting real ones, which silently
|
|
922
|
+
// retargets every branch and pool load after it.
|
|
923
|
+
//
|
|
924
|
+
// Which is why this matches the FAMILY, not three literal spellings. gas directives are
|
|
925
|
+
// case-insensitive (`.ALIGN 2, 0` assembles identically — measured), and `.balignw` /
|
|
926
|
+
// `.balignl` / `.p2alignw` / `.p2alignl` fill with a repeated halfword or word PATTERN
|
|
927
|
+
// rather than a byte. A three-word lowercase whitelist skipped all of those as "other
|
|
928
|
+
// directives", giving them ZERO bytes in the layout while gas emitted real ones: measured,
|
|
929
|
+
// `.balignw 4, 0x0000` between a raw branch and its target retargeted the branch two bytes
|
|
930
|
+
// early and lifted the wrong C. A pattern fill stays a loud refusal — the byte COUNT is
|
|
931
|
+
// computable, but the bytes are a pattern this pass does not decode into pad encodings.
|
|
932
|
+
const ad = rest.match(/^\.(align|balign|p2align)([wl]?)\b\s*(.*)$/i);
|
|
933
|
+
if (ad) {
|
|
934
|
+
const kind = ad[1].toLowerCase();
|
|
935
|
+
const patternFill = ad[2] !== ''; // the `w`/`l` variants
|
|
936
|
+
const am = ad[3].trim().match(/^(\d{1,3})(?:\s*,\s*([^,]*?))?\s*$/); // N [, fill]; max-skip → no match
|
|
937
|
+
const n = am ? Number(am[1]) : NaN;
|
|
938
|
+
const pow = kind === 'balign' ? Math.log2(n) : n;
|
|
939
|
+
// No fill operand → the assembler's own code-section fill; `, 0` → zeros. Anything else
|
|
940
|
+
// (including an empty `.align 2,`) is a fill this pass does not model.
|
|
941
|
+
const fillText = am?.[2]?.trim();
|
|
942
|
+
const fill = fillText === undefined ? 0x46c0 : /^0[xX]?0*$/.test(fillText) ? 0x0000 : null;
|
|
943
|
+
const what = `.${ad[1]}${ad[2]}`; // as written, so the refusal names the reader's own line
|
|
944
|
+
if (!(Number.isInteger(pow) && pow <= 2)) {
|
|
945
|
+
hazards.push({ at: flat.length - 1, what, code: true, why: 'size' });
|
|
946
|
+
} else if (patternFill || fill === null) {
|
|
947
|
+
hazards.push({ at: flat.length - 1, what, code: true, why: 'fill' });
|
|
948
|
+
} else {
|
|
949
|
+
flat.push({ align: { pow, fill } });
|
|
950
|
+
}
|
|
951
|
+
continue;
|
|
952
|
+
}
|
|
953
|
+
// Belt and braces for the next spelling nobody has thought of: an unrecognised directive
|
|
954
|
+
// whose NAME says alignment is loud rather than silently zero-sized. Skipping one is the
|
|
955
|
+
// failure above, and it leaves no marker behind.
|
|
956
|
+
const alignish = rest.match(/^\.([\w.$]*align[\w.$]*)\b/i);
|
|
957
|
+
if (alignish) {
|
|
958
|
+
hazards.push({ at: flat.length - 1, what: `.${alignish[1]}`, code: true, why: 'size' });
|
|
427
959
|
}
|
|
428
960
|
continue; // other directives skipped
|
|
429
961
|
}
|
|
@@ -443,10 +975,22 @@ function decode(name: string, asm: string): { blocks: AsmBlock[]; dataWords: Map
|
|
|
443
975
|
}
|
|
444
976
|
dataLabel = null; // a real instruction ends a data run
|
|
445
977
|
const canon = canonicalMnemonic(m[1]);
|
|
978
|
+
const ops = m[2] ? splitOperands(m[2]) : [];
|
|
979
|
+
// `mov r8, r8` is the OTHER way to write 0x46C0 — objdump prints those two bytes as
|
|
980
|
+
// `nop @ (mov r8, r8)`, and a splitter emits either. It is normalised here, at the same seam
|
|
981
|
+
// the `.2byte 0x46C0` decode uses (padHalfword), because further down it would be modelled as
|
|
982
|
+
// a live read of a callee-saved register and mint a parameter the object does not have: the
|
|
983
|
+
// three spellings of one encoding gave two different signatures. `isPadInstr` only governs the
|
|
984
|
+
// prune of UNREACHABLE all-pad blocks, so it could not see the divergence — a REACHABLE
|
|
985
|
+
// `mov r8, r8` is kept, and was kept as a register read.
|
|
986
|
+
if ((canon === 'mov' || canon === 'movs') && ops.length === 2 && ops[0] === 'r8' && ops[1] === 'r8') {
|
|
987
|
+
flat.push({ instr: { ...padHalfword(0x46c0)!, asWritten: m[1] } });
|
|
988
|
+
continue;
|
|
989
|
+
}
|
|
446
990
|
flat.push({
|
|
447
991
|
instr: {
|
|
448
992
|
mnemonic: canon,
|
|
449
|
-
ops
|
|
993
|
+
ops,
|
|
450
994
|
...(canon === m[1] ? {} : { asWritten: m[1] }),
|
|
451
995
|
},
|
|
452
996
|
});
|
|
@@ -541,161 +1085,414 @@ function decode(name: string, asm: string): { blocks: AsmBlock[]; dataWords: Map
|
|
|
541
1085
|
// frontend already models; anything the decoder cannot prove declines loud.
|
|
542
1086
|
const isPcRelLdr = (ins?: Instr) =>
|
|
543
1087
|
ins?.mnemonic === 'ldr' && /^\[pc,\s*#(0x[0-9a-fA-F]+|\d+)\]$/.test(ins.ops[1] ?? '');
|
|
544
|
-
|
|
1088
|
+
// An alignment directive's fill bytes are INSTRUCTIONS in the code stream, not decoration: on
|
|
1089
|
+
// ARM7TDMI `.align 2, 0` emits the halfword 0x0000, which IS `lsls r0, r0, #0` and sets the
|
|
1090
|
+
// flags. They are padding only when nothing can execute them. When something CAN, their count
|
|
1091
|
+
// is part of the answer and the slice needs the byte-accurate layout below, exactly as a raw
|
|
1092
|
+
// branch halfword does — so this predicate, not the accident of whether some other item needed
|
|
1093
|
+
// offsets, is what decides. Scanning back from the align: an open or conditional instruction
|
|
1094
|
+
// means control falls into the fill, a label something in the slice NAMES means a branch can
|
|
1095
|
+
// land on it, and nothing at all means the fill sits at the function's entry. Only an
|
|
1096
|
+
// unconditional transfer seals it off, and a label nothing names does not re-open it — agbcc
|
|
1097
|
+
// writes a dead `.L10:` in front of every literal-pool alignment and never branches there, so
|
|
1098
|
+
// reading every label as a way in sends every agbcc function through the base solver for a
|
|
1099
|
+
// fill no instruction can execute (measured when the clause landed: 75 of the then-253 real-tier
|
|
1100
|
+
// reference functions lost; the real tier holds 252 rows today, and the 75 has not been re-run).
|
|
1101
|
+
//
|
|
1102
|
+
// `named` is every operand token, which OVERSHOOTS: `ldr r0, _pool` names `_pool` without
|
|
1103
|
+
// control ever going there. The bound that matters is the one the block builder already draws
|
|
1104
|
+
// — a label heading DATA is not a code entry, so naming it is a load, not a way in. Without
|
|
1105
|
+
// that clause an align between two words of a labelled literal pool read as executable fill
|
|
1106
|
+
// and declined the function, over bytes in the data stream that nothing can execute.
|
|
1107
|
+
const named = new Set<string>(); // labels an operand or a data word names
|
|
1108
|
+
const labelShape = /^([A-Za-z_.$][\w.$]*)/;
|
|
1109
|
+
for (const f of flat) {
|
|
1110
|
+
for (const tok of [...(f.instr?.ops ?? []), ...(f.data?.values ?? [])]) {
|
|
1111
|
+
const m = tok.match(labelShape);
|
|
1112
|
+
if (m) {
|
|
1113
|
+
named.add(m[1]);
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
}
|
|
1117
|
+
const headsData = (l: string) => dataWords.has(l) || subwordData.has(l);
|
|
1118
|
+
// A LINEAR, PRE-LAYOUT APPROXIMATION of the CFG walk forty lines below — not a second opinion
|
|
1119
|
+
// about it. Ordering forces the approximation: the real walk needs blocks, blocks need the
|
|
1120
|
+
// item stream, and the item stream needs the fill sizes this predicate is being asked about.
|
|
1121
|
+
// So it answers "can control reach these bytes" by scanning backwards over `flat` instead of
|
|
1122
|
+
// forwards over the CFG, which makes it CONSERVATIVE in one direction on purpose: it looks at
|
|
1123
|
+
// the nearest preceding instruction and does not ask whether THAT instruction is itself
|
|
1124
|
+
// reachable, so a pad in front of an unsizable `.align` reads as "control continues". Loud
|
|
1125
|
+
// where the walk would be quiet, never the reverse. `controlContinuesPast` is shared with the
|
|
1126
|
+
// block-level scan so the two cannot drift on what a transfer kind means.
|
|
1127
|
+
const fillIsReachable = (upto: number) => {
|
|
1128
|
+
for (let j = upto - 1; j >= 0; j--) {
|
|
1129
|
+
const g = flat[j];
|
|
1130
|
+
if (g.label !== undefined) {
|
|
1131
|
+
if (named.has(g.label) && !headsData(g.label)) {
|
|
1132
|
+
return true;
|
|
1133
|
+
}
|
|
1134
|
+
continue; // a label nothing names — or one naming data — cannot bring control here
|
|
1135
|
+
}
|
|
1136
|
+
if (g.instr) {
|
|
1137
|
+
return controlContinuesPast(g.instr);
|
|
1138
|
+
}
|
|
1139
|
+
// data: not executed — keep looking for the instruction whose flow reaches this point
|
|
1140
|
+
}
|
|
1141
|
+
return true;
|
|
1142
|
+
};
|
|
1143
|
+
const sealedFill = flat.map((f, i) => f.align !== undefined && !fillIsReachable(i));
|
|
1144
|
+
// An alignment directive this pass could not READ — its byte count unknown (a max-skip limit,
|
|
1145
|
+
// an alignment wider than the base is known to) or its fill bytes unknown (a fill that is not
|
|
1146
|
+
// a pad encoding) — records a hazard instead of an item,
|
|
1147
|
+
// and its fill bytes are still in the instruction stream. If anything can execute them the
|
|
1148
|
+
// function cannot be lifted, whether or not the slice needs byte offsets for another reason;
|
|
1149
|
+
// reading the size question as a LAYOUT question only is what let a `.2byte 0x0000` pad, once
|
|
1150
|
+
// decoded at parse time, carry a `.align 2` past this check into a silent lift.
|
|
1151
|
+
//
|
|
1152
|
+
// The hazard is recorded at the item BEFORE the directive (`flat.length - 1` at push time), so
|
|
1153
|
+
// its fill occupies slice position `at - sliceStart + 1`. The lower bound is `sliceStart`, not
|
|
1154
|
+
// `sliceStart - 1`: `allFlat[sliceStart]` is the function's OWN label, so an align inside the
|
|
1155
|
+
// slice always records `at >= sliceStart` — and `at === sliceStart - 1` can only mean the
|
|
1156
|
+
// directive sits between the previous function's last item and this function's entry label,
|
|
1157
|
+
// i.e. its fill is emitted at addresses BELOW this function and belongs to the sibling.
|
|
1158
|
+
// Admitting it declined every function that merely FOLLOWED an unsizable alignment, which is
|
|
1159
|
+
// the poisoning the allFlat-position bookkeeping exists to prevent (see `hazards` above).
|
|
1160
|
+
const liveHazard = hazards.find(
|
|
1161
|
+
(h) => h.code && h.at >= sliceStart && h.at < boundaries[boundaryIdx] && fillIsReachable(h.at - sliceStart + 1),
|
|
1162
|
+
);
|
|
1163
|
+
if (liveHazard) {
|
|
1164
|
+
throw new FrontendUnsupportedError(
|
|
1165
|
+
`cannot lift '${name}': '${liveHazard.what}' emits fill into the code stream and ` +
|
|
1166
|
+
(liveHazard.why === 'fill'
|
|
1167
|
+
? `its fill bytes are not a pad encoding this frontend models — they are real instructions`
|
|
1168
|
+
: `makes item sizes unknowable`),
|
|
1169
|
+
);
|
|
1170
|
+
}
|
|
1171
|
+
const needsLayout = flat.some(
|
|
1172
|
+
(f, i) => (f.data?.inCode ?? false) || isPcRelLdr(f.instr) || (f.align !== undefined && !sealedFill[i]),
|
|
1173
|
+
);
|
|
1174
|
+
// NO BENCHMARK REACH BELOW THIS LINE, and it is worth knowing which side of it you are on.
|
|
1175
|
+
// Everything ABOVE — the pad decoding, `fillIsReachable`, `sealedFill`, the live-hazard refusal
|
|
1176
|
+
// — runs on every agbcc function and is bench-load-bearing. The layout solver below runs on
|
|
1177
|
+
// none of them: instrumented and re-measured 2026-09-06 over the 269 of the benchmark's 404
|
|
1178
|
+
// agbcc rows whose reference asm the local bench cache holds, this branch was entered 0 times.
|
|
1179
|
+
// agbcc emits no `[pc, #N]` load and no sub-word data in `.text`, and every `.align` it writes
|
|
1180
|
+
// sits behind a dead `.L` label that the rule above seals. So a clean `bench diff` says NOTHING about any code inside this branch —
|
|
1181
|
+
// `thumb-pad-directives.test.ts` is its only gate, and a change here has to be argued there.
|
|
545
1182
|
if (needsLayout) {
|
|
546
|
-
// Only a hazard WITHIN this function's slice makes its layout unknowable.
|
|
1183
|
+
// Only a hazard WITHIN this function's slice makes its layout unknowable. A fill this pass
|
|
1184
|
+
// cannot model keeps the directive out of the item stream entirely, so it contributes no
|
|
1185
|
+
// size here even though its count is arithmetic — a different fact from an unknown count,
|
|
1186
|
+
// and the message says which one it is.
|
|
547
1187
|
const sliceHazard = hazards.find((h) => h.at >= sliceStart && h.at < boundaries[boundaryIdx]);
|
|
548
1188
|
if (sliceHazard) {
|
|
549
1189
|
throw new FrontendUnsupportedError(
|
|
550
|
-
`cannot lift '${name}': raw-encoded input needs byte-accurate layout, but '${sliceHazard.what}'
|
|
1190
|
+
`cannot lift '${name}': raw-encoded input needs byte-accurate layout, but '${sliceHazard.what}' ` +
|
|
1191
|
+
(sliceHazard.why === 'fill'
|
|
1192
|
+
? `fills with bytes this frontend does not model, so it contributes no size to the layout`
|
|
1193
|
+
: `makes item sizes unknowable`),
|
|
551
1194
|
);
|
|
552
1195
|
}
|
|
553
|
-
//
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
1196
|
+
// ── the function's base alignment is SOLVED, not derived ─────────────────────────────────
|
|
1197
|
+
// `.align N, 0` emits (-(base + off)) mod 2^N bytes, where `base` is the function's own
|
|
1198
|
+
// address mod 4 — and `base` was until now recovered only AFTER this walk, from literal-pool
|
|
1199
|
+
// positions that the align itself moves. That cycle is why `.align` had to be a hazard.
|
|
1200
|
+
//
|
|
1201
|
+
// Break it by treating `base` as the unknown it is. A Thumb function starts on a 2-byte
|
|
1202
|
+
// boundary, so `base` is 0 or 2; lay the slice out BOTH ways and let the structural
|
|
1203
|
+
// invariants this frontend already enforces on a single layout eliminate the wrong one — a
|
|
1204
|
+
// pool word off a 4-byte boundary, a raw branch landing between instructions, a pc-relative
|
|
1205
|
+
// load landing outside every pool. With nothing to size, the two layouts are identical and
|
|
1206
|
+
// the first invariant alone picks the same base the old derivation did.
|
|
1207
|
+
//
|
|
1208
|
+
// What the input genuinely fails to determine STILL declines: if both bases survive every
|
|
1209
|
+
// invariant and disagree, there is no honest answer to give.
|
|
1210
|
+
const itemSize = (f: FlatItem) =>
|
|
1211
|
+
f.instr ? (f.instr.mnemonic === 'bl' ? 4 : 2) : f.data ? f.data.values.length * (f.data.halfwords ? 2 : 4) : 0;
|
|
1212
|
+
// Replace each `.align pow, 0` with the pad instructions its zero-fill bytes ARE — the same
|
|
1213
|
+
// item the other two spellings of a pad produce. The fill is always a whole number of
|
|
1214
|
+
// halfwords and cannot be otherwise: `base` is 0 or 2, every item size is even (2, 4, or a
|
|
1215
|
+
// whole number of 2/4-byte values), and `pow` is at most 2 — so `-(base + at) & 3` is even.
|
|
1216
|
+
// It was an assertion here; nothing could fail it, and being raised OUTSIDE the candidate
|
|
1217
|
+
// try/catch below it would have bypassed the base machinery it appeared to belong to.
|
|
1218
|
+
const expandAligns = (base: number): Candidate => {
|
|
1219
|
+
const items: FlatItem[] = [];
|
|
1220
|
+
const offs: number[] = [];
|
|
1221
|
+
const from: number[] = []; // items[k] came from flat[from[k]]; -1 marks an inserted fill
|
|
1222
|
+
const fills: { site: number; start: number; bytes: number }[] = [];
|
|
1223
|
+
let at = 0;
|
|
1224
|
+
flat.forEach((f, i) => {
|
|
1225
|
+
if (f.align) {
|
|
1226
|
+
const fill = -(base + at) & ((1 << f.align.pow) - 1);
|
|
1227
|
+
fills.push({ site: i, start: at, bytes: fill });
|
|
1228
|
+
for (let k = 0; k < fill; k += 2) {
|
|
1229
|
+
offs.push(at + k);
|
|
1230
|
+
from.push(-1);
|
|
1231
|
+
items.push({ instr: padHalfword(f.align.fill)! });
|
|
1232
|
+
}
|
|
1233
|
+
at += fill;
|
|
1234
|
+
return;
|
|
1235
|
+
}
|
|
1236
|
+
offs.push(at);
|
|
1237
|
+
from.push(i);
|
|
1238
|
+
items.push({ ...f }); // cloned: pass 2 replaces `instr` on the chosen candidate's items
|
|
1239
|
+
at += itemSize(f);
|
|
1240
|
+
});
|
|
1241
|
+
return { base, items, offs, from, fills };
|
|
1242
|
+
};
|
|
1243
|
+
// The one invariant that costs nothing to check: every literal-pool word is 4-aligned in the
|
|
1244
|
+
// ROM. This IS the old `basePar` derivation, read as a filter instead of an assignment.
|
|
1245
|
+
const laid = [0, 2].map(expandAligns);
|
|
1246
|
+
const viable = laid.filter(({ base, items, offs }) =>
|
|
1247
|
+
items.every((f, i) => !f.data || f.data.halfwords || (base + offs[i]) % 4 === 0),
|
|
1248
|
+
);
|
|
1249
|
+
if (viable.length === 0) {
|
|
1250
|
+
// With an align in the slice this is reachable with ONE pool word: the align's own size
|
|
1251
|
+
// moves it, and neither base lands it on a 4-byte boundary. Nothing is inconsistent with
|
|
1252
|
+
// anything, so the pre-existing multi-pool message would send a reader hunting for a
|
|
1253
|
+
// second pool that does not exist.
|
|
1254
|
+
throw new FrontendUnsupportedError(
|
|
1255
|
+
flat.some((f) => f.align)
|
|
1256
|
+
? `cannot lift '${name}': no base alignment (0 or 2 mod 4) puts every literal pool word on a ` +
|
|
1257
|
+
`4-byte boundary once alignment fill is accounted for`
|
|
1258
|
+
: `cannot lift '${name}': literal pools at inconsistent alignments — cannot determine the function's base alignment`,
|
|
1259
|
+
);
|
|
577
1260
|
}
|
|
578
|
-
//
|
|
579
|
-
//
|
|
580
|
-
const
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
1261
|
+
// Two viable bases that lay the slice out identically are one candidate: the base is then
|
|
1262
|
+
// unobservable except through a pc-relative load, which declines for want of a pool below.
|
|
1263
|
+
const candidates = viable.filter((c, i) => viable.findIndex((o) => o.offs.join() === c.offs.join()) === i);
|
|
1264
|
+
|
|
1265
|
+
// Everything from here down is the resolution of ONE candidate layout: byte offsets, raw
|
|
1266
|
+
// branch decode, pc-relative pool loads. It reads the candidate and returns the rewritten
|
|
1267
|
+
// stream; it must not touch anything shared, or a rejected candidate would leave its marks.
|
|
1268
|
+
//
|
|
1269
|
+
// It also returns a WITNESS: every fact about the input this layout decided — which pool
|
|
1270
|
+
// word each pc-relative load reads, which item each raw branch targets, and how many fill
|
|
1271
|
+
// bytes each align emits WHEN those bytes are observable. Two candidate bases are compared
|
|
1272
|
+
// by their witnesses, so "they disagree" is something the code establishes rather than
|
|
1273
|
+
// asserts. A sealed fill (nothing can execute it) that no branch targets is left out: those
|
|
1274
|
+
// pad instructions form an all-pad block the unreachable-pad prune below deletes, so their
|
|
1275
|
+
// count cannot reach the answer.
|
|
1276
|
+
const resolveAt = (
|
|
1277
|
+
{ base, items, offs: itemOff, from, fills }: Candidate,
|
|
1278
|
+
basePar: number | undefined,
|
|
1279
|
+
): Resolved => {
|
|
1280
|
+
const pools = new Map<string, string[]>();
|
|
1281
|
+
const observed: LayoutFact[] = [];
|
|
1282
|
+
const fillEntered = new Set<number>(); // fill sites a raw branch lands inside
|
|
1283
|
+
const labelOff = new Map<string, number>();
|
|
1284
|
+
const codeStart = new Set<number>(); // offsets that begin an instruction or carry a label
|
|
1285
|
+
items.forEach((f, i) => {
|
|
1286
|
+
if (f.label && !labelOff.has(f.label)) {
|
|
1287
|
+
labelOff.set(f.label, itemOff[i]);
|
|
1288
|
+
codeStart.add(itemOff[i]);
|
|
1289
|
+
}
|
|
1290
|
+
if (f.instr) {
|
|
1291
|
+
codeStart.add(itemOff[i]);
|
|
1292
|
+
}
|
|
1293
|
+
});
|
|
1294
|
+
const labelAt = new Map<number, string>();
|
|
1295
|
+
for (const [lab, lo] of labelOff) {
|
|
1296
|
+
if (!labelAt.has(lo)) {
|
|
1297
|
+
labelAt.set(lo, lab);
|
|
586
1298
|
}
|
|
587
|
-
const d = (v & 0xff) - (v & 0x80 ? 0x100 : 0);
|
|
588
|
-
return { mnemonic: mn, target: at + 4 + d * 2 };
|
|
589
|
-
}
|
|
590
|
-
if (v >= 0xe000 && v <= 0xe7ff) {
|
|
591
|
-
const d = (v & 0x7ff) - (v & 0x400 ? 0x800 : 0);
|
|
592
|
-
return { mnemonic: 'b', target: at + 4 + d * 2 };
|
|
593
1299
|
}
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
}
|
|
606
|
-
f.data.values.forEach((raw, k) => {
|
|
607
|
-
const at = itemOff[i] + k * 2;
|
|
608
|
-
const v = parseInt(raw, 16);
|
|
609
|
-
const br = Number.isFinite(v) ? decodeHalfword(v, at) : null;
|
|
610
|
-
if (!br) {
|
|
611
|
-
throw new FrontendUnsupportedError(
|
|
612
|
-
`cannot lift '${name}': raw halfword '${raw}' in the code stream is not a decodable branch — ` +
|
|
613
|
-
`skipping it would silently delete its effect`,
|
|
614
|
-
);
|
|
1300
|
+
// Thumb-1 branch encodings this frontend models (cond codes 4–7 = mi/pl/vs/vc have no
|
|
1301
|
+
// lifted comparison semantics here; 14 is undefined, 15 is swi — all decline).
|
|
1302
|
+
const COND_MN = ['beq', 'bne', 'bcs', 'bcc', '', '', '', '', 'bhi', 'bls', 'bge', 'blt', 'bgt', 'ble'];
|
|
1303
|
+
const decodeHalfword = (v: number, at: number): { mnemonic: string; target: number } | null => {
|
|
1304
|
+
if (v >= 0xd000 && v <= 0xddff) {
|
|
1305
|
+
const mn = COND_MN[(v >> 8) & 0xf];
|
|
1306
|
+
if (!mn) {
|
|
1307
|
+
return null;
|
|
1308
|
+
}
|
|
1309
|
+
const d = (v & 0xff) - (v & 0x80 ? 0x100 : 0);
|
|
1310
|
+
return { mnemonic: mn, target: at + 4 + d * 2 };
|
|
615
1311
|
}
|
|
616
|
-
if (
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
);
|
|
1312
|
+
if (v >= 0xe000 && v <= 0xe7ff) {
|
|
1313
|
+
const d = (v & 0x7ff) - (v & 0x400 ? 0x800 : 0);
|
|
1314
|
+
return { mnemonic: 'b', target: at + 4 + d * 2 };
|
|
620
1315
|
}
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
1316
|
+
return null;
|
|
1317
|
+
};
|
|
1318
|
+
// Pass 1: decode every in-code halfword; collect synthesized labels for branch targets.
|
|
1319
|
+
const synthLabels = new Map<number, string>(); // target offset → label to ensure there
|
|
1320
|
+
const decoded = new Map<number, Instr>(); // item index → replacement branch instr
|
|
1321
|
+
items.forEach((f, i) => {
|
|
1322
|
+
if (!f.data?.inCode) {
|
|
1323
|
+
return;
|
|
1324
|
+
}
|
|
1325
|
+
if (!f.data.halfwords) {
|
|
1326
|
+
return; // unlabelled word pool — layout bytes only (reached via [pc, #off] below)
|
|
625
1327
|
}
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
1328
|
+
f.data.values.forEach((raw, k) => {
|
|
1329
|
+
const at = itemOff[i] + k * 2;
|
|
1330
|
+
const v = halfwordLiteral(raw);
|
|
1331
|
+
const br = v === null ? null : decodeHalfword(v, at);
|
|
1332
|
+
if (!br) {
|
|
1333
|
+
throw new FrontendUnsupportedError(
|
|
1334
|
+
`cannot lift '${name}': raw halfword '${raw}' in the code stream is not a decodable branch — ` +
|
|
1335
|
+
`skipping it would silently delete its effect`,
|
|
1336
|
+
);
|
|
1337
|
+
}
|
|
1338
|
+
if (!codeStart.has(br.target)) {
|
|
1339
|
+
throw new FrontendUnsupportedError(
|
|
1340
|
+
`cannot lift '${name}': raw branch '${raw}' targets byte offset 0x${br.target.toString(16)}, which is not an instruction boundary`,
|
|
1341
|
+
);
|
|
1342
|
+
}
|
|
1343
|
+
if (f.data!.values.length > 1) {
|
|
1344
|
+
throw new FrontendUnsupportedError(
|
|
1345
|
+
`cannot lift '${name}': multi-value raw halfword directive mixing branches is not supported`,
|
|
1346
|
+
);
|
|
1347
|
+
}
|
|
1348
|
+
const lab = labelAt.get(br.target) ?? synthLabels.get(br.target) ?? `.Lraw_${br.target.toString(16)}`;
|
|
1349
|
+
synthLabels.set(br.target, lab);
|
|
1350
|
+
decoded.set(i, { mnemonic: br.mnemonic, ops: [lab] });
|
|
1351
|
+
const inFill = fills.find((fl) => br.target >= fl.start && br.target < fl.start + fl.bytes);
|
|
1352
|
+
if (inFill) {
|
|
1353
|
+
fillEntered.add(inFill.site);
|
|
1354
|
+
}
|
|
1355
|
+
// The branch is witnessed by WHICH item it lands on, not by the byte offset: the
|
|
1356
|
+
// offsets of one layout mean nothing in the other. A target inside fill names the fill
|
|
1357
|
+
// SITE and the distance into it — rendering it as the bare words `alignment fill` made
|
|
1358
|
+
// two layouts that send the same branch into DIFFERENT fill sites compare equal, which
|
|
1359
|
+
// is precisely the disagreement this witness exists to establish.
|
|
1360
|
+
const ti = items.findIndex((_g, j) => itemOff[j] === br.target);
|
|
1361
|
+
observed.push({
|
|
1362
|
+
kind: 'branch',
|
|
1363
|
+
at: from[i],
|
|
1364
|
+
to:
|
|
1365
|
+
ti >= 0 && from[ti] >= 0
|
|
1366
|
+
? { item: from[ti] }
|
|
1367
|
+
: inFill
|
|
1368
|
+
? { fill: inFill.site, off: br.target - inFill.start }
|
|
1369
|
+
: { off: br.target },
|
|
1370
|
+
});
|
|
1371
|
+
});
|
|
629
1372
|
});
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
const p = (4 - (itemOff[j] % 4)) % 4;
|
|
1373
|
+
// Pass 2: pc-relative literal loads → rewrite to a synthesized pool label so the existing
|
|
1374
|
+
// `poolRef` machinery applies to it like any other pool operand. `(pc & ~3) + off` depends on the
|
|
1375
|
+
// function's absolute alignment (mod 4) — `basePar`, the candidate base this layout was
|
|
1376
|
+
// built at, which survived the pool-alignment filter above (the luvdis `@ address`
|
|
1377
|
+
// comments are not trusted). It is `undefined` when the slice holds no pool word at all.
|
|
1378
|
+
items.forEach((f, i) => {
|
|
1379
|
+
if (!isPcRelLdr(f.instr)) {
|
|
1380
|
+
return;
|
|
1381
|
+
}
|
|
640
1382
|
if (basePar === undefined) {
|
|
641
|
-
basePar = p;
|
|
642
|
-
} else if (basePar !== p) {
|
|
643
1383
|
throw new FrontendUnsupportedError(
|
|
644
|
-
`cannot lift '${name}': literal
|
|
1384
|
+
`cannot lift '${name}': pc-relative literal load with no literal pool in the function to resolve into`,
|
|
645
1385
|
);
|
|
646
1386
|
}
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
if (!isPcRelLdr(f.instr)) {
|
|
651
|
-
return;
|
|
652
|
-
}
|
|
653
|
-
if (basePar === undefined) {
|
|
654
|
-
throw new FrontendUnsupportedError(
|
|
655
|
-
`cannot lift '${name}': pc-relative literal load with no literal pool in the function to resolve into`,
|
|
1387
|
+
const imm = parseInt(
|
|
1388
|
+
f.instr!.ops[1].match(/#(0x[0-9a-fA-F]+|\d+)/)![1],
|
|
1389
|
+
f.instr!.ops[1].includes('0x') ? 16 : 10,
|
|
656
1390
|
);
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
1391
|
+
const wordOff = ((basePar + itemOff[i] + 4) & ~3) - basePar + imm;
|
|
1392
|
+
// locate the word: a 4-byte data item covering [wordOff, wordOff+4)
|
|
1393
|
+
let value: string | undefined;
|
|
1394
|
+
items.forEach((g, j) => {
|
|
1395
|
+
if (!g.data || g.data.halfwords) {
|
|
1396
|
+
return;
|
|
1397
|
+
}
|
|
1398
|
+
const rel = wordOff - itemOff[j];
|
|
1399
|
+
if (rel >= 0 && rel < g.data.values.length * 4 && rel % 4 === 0) {
|
|
1400
|
+
value = g.data.values[rel / 4];
|
|
1401
|
+
}
|
|
1402
|
+
});
|
|
1403
|
+
if (value === undefined) {
|
|
1404
|
+
throw new FrontendUnsupportedError(
|
|
1405
|
+
`cannot lift '${name}': pc-relative load at offset 0x${itemOff[i].toString(16)} resolves to byte offset ` +
|
|
1406
|
+
`0x${wordOff.toString(16)}, which is not a word in a literal pool`,
|
|
1407
|
+
);
|
|
668
1408
|
}
|
|
669
|
-
const
|
|
670
|
-
|
|
671
|
-
|
|
1409
|
+
const poolLab = `.Lpcpool_${wordOff.toString(16)}`;
|
|
1410
|
+
pools.set(poolLab, [value]);
|
|
1411
|
+
f.instr = { mnemonic: 'ldr', ops: [f.instr!.ops[0], poolLab] };
|
|
1412
|
+
observed.push({ kind: 'load', at: from[i], value });
|
|
1413
|
+
});
|
|
1414
|
+
// Pass 3: rebuild the stream — insert synthesized target labels, replace decoded halfwords.
|
|
1415
|
+
const next: FlatItem[] = [];
|
|
1416
|
+
items.forEach((f, i) => {
|
|
1417
|
+
const lab = synthLabels.get(itemOff[i]);
|
|
1418
|
+
if (lab && f.label !== lab && !labelAt.has(itemOff[i])) {
|
|
1419
|
+
next.push({ label: lab });
|
|
1420
|
+
}
|
|
1421
|
+
const br = decoded.get(i);
|
|
1422
|
+
if (br) {
|
|
1423
|
+
next.push({ instr: br });
|
|
1424
|
+
} else {
|
|
1425
|
+
next.push(f);
|
|
672
1426
|
}
|
|
673
1427
|
});
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
);
|
|
1428
|
+
for (const fl of fills) {
|
|
1429
|
+
if (fl.bytes > 0 && (!sealedFill[fl.site] || fillEntered.has(fl.site))) {
|
|
1430
|
+
observed.push({ kind: 'fill', site: fl.site, bytes: fl.bytes });
|
|
1431
|
+
}
|
|
679
1432
|
}
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
const
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
1433
|
+
return { base, items: next, pools, witness: observed.sort((a, b) => (factKey(a) < factKey(b) ? -1 : 1)) };
|
|
1434
|
+
};
|
|
1435
|
+
|
|
1436
|
+
const resolved: Resolved[] = [];
|
|
1437
|
+
const refusals: Error[] = [];
|
|
1438
|
+
for (const c of candidates) {
|
|
1439
|
+
// A slice with no pool word leaves the base unobservable, exactly as before: `basePar`
|
|
1440
|
+
// stays undefined and a pc-relative load declines for want of a pool to resolve into.
|
|
1441
|
+
const hasPool = c.items.some((f) => f.data && !f.data.halfwords);
|
|
1442
|
+
try {
|
|
1443
|
+
resolved.push(resolveAt(c, hasPool ? c.base : undefined));
|
|
1444
|
+
} catch (e) {
|
|
1445
|
+
// ONLY a refusal is a vote against this base. Swallowing every exception would let a
|
|
1446
|
+
// programming error inside resolveAt elect the other base, and the lift would then rest
|
|
1447
|
+
// on a hypothesis chosen because the code threw.
|
|
1448
|
+
if (!(e instanceof FrontendUnsupportedError)) {
|
|
1449
|
+
throw e;
|
|
1450
|
+
}
|
|
1451
|
+
refusals.push(e);
|
|
696
1452
|
}
|
|
697
|
-
}
|
|
698
|
-
|
|
1453
|
+
}
|
|
1454
|
+
if (resolved.length === 0) {
|
|
1455
|
+
// With one candidate the message is the single hypothesis's own, byte for byte as before.
|
|
1456
|
+
// With two, reporting only candidate 0's message states one hypothesis as fact — including
|
|
1457
|
+
// byte offsets computed in a layout that was never established, which `onGap: 'annotate'`
|
|
1458
|
+
// writes into the emitted artifact.
|
|
1459
|
+
if (candidates.length === 1) {
|
|
1460
|
+
throw refusals[0];
|
|
1461
|
+
}
|
|
1462
|
+
const why = refusals.map(
|
|
1463
|
+
(e, i) => `base ${candidates[i].base}: ${e.message.replace(/^cannot lift '[^']*': /, '')}`,
|
|
1464
|
+
);
|
|
1465
|
+
throw new FrontendUnsupportedError(`cannot lift '${name}': neither base alignment fits — ${why.join('; ')}`);
|
|
1466
|
+
}
|
|
1467
|
+
// More than one base can survive and still decide EVERY question the same way — an align
|
|
1468
|
+
// whose fill is unreachable padding moves the two layouts apart without moving the answer,
|
|
1469
|
+
// which is the ordinary shape, not a corner. Only a difference in what the layouts decided
|
|
1470
|
+
// is a refusal, and the message quotes the difference it found.
|
|
1471
|
+
const seal = (r: Resolved) => r.witness.map(factKey).join('\n');
|
|
1472
|
+
const other = resolved.find((r) => seal(r) !== seal(resolved[0]));
|
|
1473
|
+
if (other) {
|
|
1474
|
+
const only = (a: Resolved, b: Resolved) => {
|
|
1475
|
+
const bk = new Set(b.witness.map(factKey));
|
|
1476
|
+
return a.witness.filter((w) => !bk.has(factKey(w)));
|
|
1477
|
+
};
|
|
1478
|
+
const say = (w: LayoutFact[]) => (w.length > 0 ? w.map(sayFact).join('; ') : 'nothing');
|
|
1479
|
+
throw new FrontendUnsupportedError(
|
|
1480
|
+
`cannot lift '${name}': alignment padding depends on the function's base alignment, which this ` +
|
|
1481
|
+
`input does not determine — base ${resolved[0].base} and base ${other.base} both decode ` +
|
|
1482
|
+
`consistently but disagree: base ${resolved[0].base} has ${say(only(resolved[0], other))}, ` +
|
|
1483
|
+
`base ${other.base} has ${say(only(other, resolved[0]))}`,
|
|
1484
|
+
);
|
|
1485
|
+
}
|
|
1486
|
+
for (const [lab, words] of resolved[0].pools) {
|
|
1487
|
+
dataWords.set(lab, words);
|
|
1488
|
+
}
|
|
1489
|
+
flat = resolved[0].items;
|
|
1490
|
+
} else {
|
|
1491
|
+
// Every align in this slice is sealed off by an unconditional transfer (see `sealedFill`),
|
|
1492
|
+
// so its fill is pool padding no instruction can execute — the same verdict the prune below
|
|
1493
|
+
// reaches for a pad block it finds unreachable, arrived at before the layout is needed. Its
|
|
1494
|
+
// bytes cannot move any answer, so the item is dropped and no base is solved.
|
|
1495
|
+
flat = flat.filter((f) => !f.align);
|
|
699
1496
|
}
|
|
700
1497
|
|
|
701
1498
|
const blocks: AsmBlock[] = [];
|
|
@@ -723,11 +1520,8 @@ function decode(name: string, asm: string): { blocks: AsmBlock[]; dataWords: Map
|
|
|
723
1520
|
prev = blocks[j];
|
|
724
1521
|
}
|
|
725
1522
|
}
|
|
726
|
-
if (prev) {
|
|
727
|
-
|
|
728
|
-
if (k === null || k === 'cond') {
|
|
729
|
-
fallsIntoData.add(prev.label);
|
|
730
|
-
}
|
|
1523
|
+
if (prev && controlContinuesPast(prev.instrs[prev.instrs.length - 1])) {
|
|
1524
|
+
fallsIntoData.add(prev.label);
|
|
731
1525
|
}
|
|
732
1526
|
cur = null;
|
|
733
1527
|
continue;
|
|
@@ -788,17 +1582,21 @@ function decode(name: string, asm: string): { blocks: AsmBlock[]; dataWords: Map
|
|
|
788
1582
|
}
|
|
789
1583
|
}
|
|
790
1584
|
let live = blocks.filter((b) => b.instrs.length > 0);
|
|
791
|
-
// Alignment
|
|
792
|
-
//
|
|
793
|
-
//
|
|
794
|
-
//
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
1585
|
+
// Alignment fill a splitter emits around returns and literal pools (isPadInstr, above, is the
|
|
1586
|
+
// single definition of which instructions those are). This is the ONE place padding-versus-real
|
|
1587
|
+
// -instruction is decided, for all three ways the same two bytes get spelled: as the
|
|
1588
|
+
// instruction itself (including the `mov r8, r8` spelling of the nop, normalised at parse), as
|
|
1589
|
+
// `.2byte 0x0000` (decoded at parse — padHalfword), or as an alignment directive (expanded
|
|
1590
|
+
// into these very instructions by the layout pass above whenever anything can execute the
|
|
1591
|
+
// fill; when nothing can, that pass reached the SAME verdict — padding — and dropped the
|
|
1592
|
+
// item). None of them arrives here as its own kind of thing, so none can be judged by a
|
|
1593
|
+
// different rule.
|
|
1594
|
+
//
|
|
1595
|
+
// That claim is about the PRUNE, and the prune only ever sees UNREACHABLE all-pad blocks: a
|
|
1596
|
+
// reachable pad is kept and modelled as the instruction it is. So a spelling that reaches this
|
|
1597
|
+
// point unnormalised is not caught here — `mov r8, r8` was kept as a live read of a
|
|
1598
|
+
// callee-saved register and minted a parameter, invisible to this predicate. Normalisation
|
|
1599
|
+
// belongs at parse, and that is where it now is.
|
|
802
1600
|
const padBlocks = new Set(live.filter((b) => b.instrs.every(isPadInstr)).map((b) => b.label));
|
|
803
1601
|
if (fallsIntoData.size > 0 || padBlocks.size > 0) {
|
|
804
1602
|
// Targeted reachability: a block that falls into data is either luvdis's unreachable
|
|
@@ -819,7 +1617,7 @@ function decode(name: string, asm: string): { blocks: AsmBlock[]; dataWords: Map
|
|
|
819
1617
|
if (kind === 'cond' || kind === 'uncond') {
|
|
820
1618
|
targets.push(last.ops[0]);
|
|
821
1619
|
}
|
|
822
|
-
if (
|
|
1620
|
+
if (last && controlContinuesPast(last)) {
|
|
823
1621
|
const fall = live[i + 1]?.label;
|
|
824
1622
|
if (fall !== undefined && !fallsIntoData.has(b.label)) {
|
|
825
1623
|
targets.push(fall);
|
|
@@ -836,7 +1634,7 @@ function decode(name: string, asm: string): { blocks: AsmBlock[]; dataWords: Map
|
|
|
836
1634
|
for (const b of live) {
|
|
837
1635
|
if (fallsIntoData.has(b.label) && reach.has(idx.get(b.label)!)) {
|
|
838
1636
|
const last = b.instrs[b.instrs.length - 1];
|
|
839
|
-
if (!last ||
|
|
1637
|
+
if (!last || controlContinuesPast(last)) {
|
|
840
1638
|
throw new FrontendUnsupportedError(
|
|
841
1639
|
`cannot lift '${name}': reachable code in block '${b.label}' falls through into data bytes`,
|
|
842
1640
|
);
|
|
@@ -861,16 +1659,10 @@ function decode(name: string, asm: string): { blocks: AsmBlock[]; dataWords: Map
|
|
|
861
1659
|
boundaryIdx++;
|
|
862
1660
|
continue;
|
|
863
1661
|
}
|
|
864
|
-
return { blocks: live, dataWords };
|
|
1662
|
+
return { blocks: live, dataWords, funcLabels: new Set(funcLabels) };
|
|
865
1663
|
}
|
|
866
1664
|
}
|
|
867
1665
|
|
|
868
|
-
// Resolve an agbcc/Thumb literal-pool reference (`ldr rD, .Lpool` / `.Lpool+byteOff`) to the NUMERIC
|
|
869
|
-
// 32-bit word it loads — the `ldr rD, =const` idiom. Returns null when the operand is NOT a numeric
|
|
870
|
-
// pool constant: a register/`[base]` memory operand, an unknown label, a misaligned offset, or a
|
|
871
|
-
// word that is a SYMBOL (an address / jump-table pointer — left for recoverJumpTable or the normal
|
|
872
|
-
// load path). The byte offset selects the word (index = off/4). This keeps a real literal constant
|
|
873
|
-
// (`.word 0x8408`) from being lifted as a phantom pointer parameter and dereferenced (`*a2`).
|
|
874
1666
|
// The label-operand shape shared by BOTH pool paths: agbcc `.Lpool`, pret `_08012358`, with an
|
|
875
1667
|
// optional `+N` byte offset. Kept in one place so the const and symbol resolvers cannot drift
|
|
876
1668
|
// (they did — the drift fabricated phantom pointer params on symbol-pool loads).
|
|
@@ -885,8 +1677,12 @@ type PoolRef =
|
|
|
885
1677
|
* the operand does NOT name a pool (a real register/memory base → the normal load path). When it
|
|
886
1678
|
* DOES name a pool the outcome is const | gaddr | unmodelled — NEVER a fall-through to the load
|
|
887
1679
|
* path, which would materialise the pool label as a phantom pointer parameter (a silent
|
|
888
|
-
* miscompile). `unmodelled`
|
|
889
|
-
*
|
|
1680
|
+
* miscompile). `unmodelled` is the caller's cue to decline loud, and it has exactly three
|
|
1681
|
+
* inhabitants, all of them below: an offset that does not select a whole word of the pool
|
|
1682
|
+
* (misaligned, or past its end); a word that matched the numeric shape but does not parse to a
|
|
1683
|
+
* finite value; and a word that is neither a number nor `symbol±offset` — a `.L` code label is
|
|
1684
|
+
* here, since the symbol pattern admits no leading dot. A `sym+N` word is NOT unmodelled: it is
|
|
1685
|
+
* the gaddr-plus-addend path and lifts cleanly. */
|
|
890
1686
|
function poolRef(operand: string, dataWords: Map<string, string[]>): PoolRef | null {
|
|
891
1687
|
const m = operand.match(POOL_LABEL);
|
|
892
1688
|
if (!m) {
|
|
@@ -1019,30 +1815,6 @@ interface JumpTable {
|
|
|
1019
1815
|
// since `lsl rD, rS, #imm` is low-register-only, so the add here is always the low-register form.
|
|
1020
1816
|
const isDataOp = (mn: string, base: 'lsl' | 'add'): boolean => mn === base || mn === `${base}s`;
|
|
1021
1817
|
|
|
1022
|
-
// A shift amount is a NUMBER, not a spelling. `#2`, `#0x2` and `#0x02` are the same shift; the
|
|
1023
|
-
// recogniser used to compare the operand text and so rejected the third.
|
|
1024
|
-
//
|
|
1025
|
-
// The operand must be a plain integer LITERAL, and that shape check is the whole point of this
|
|
1026
|
-
// helper rather than an incidental guard. `imm()` is `parseInt`, which stops at the first character
|
|
1027
|
-
// it cannot consume, so it reads `#2*2` as 2 — and `#2*2` is not malformed, it is an expression gas
|
|
1028
|
-
// accepts and assembles to `lsls r0, r1, #4`. Matching it as a shift by two would recover a switch
|
|
1029
|
-
// whose stride is wrong by a factor of four: the emitted C is entirely ordinary and dispatches to
|
|
1030
|
-
// the wrong BLOCK, with no marker. `#2+1`, `#2-1` and `#2<<1` are the same trap. An adversarial
|
|
1031
|
-
// probe found this after the first cut of this helper shipped with exactly that hole, and the test
|
|
1032
|
-
// that claimed to pin the property sampled only `#3`/`#0x3`/`#0x1`/`r2` and so passed anyway.
|
|
1033
|
-
//
|
|
1034
|
-
// Anything that is not a bare decimal or hex literal therefore DECLINES, which is the safe
|
|
1035
|
-
// direction: a real dispatch spells its shift as a literal. Known false declines, all loud and none
|
|
1036
|
-
// observed in any corpus: a binary literal (`#0b10`) and a signed one (`#+2`).
|
|
1037
|
-
//
|
|
1038
|
-
// One divergence is knowingly left in, and it is inert AT THE ONLY VALUE THIS IS USED WITH.
|
|
1039
|
-
// A leading zero means octal to gas and decimal to `Number`, so `#010` is 8 there and 10 here —
|
|
1040
|
-
// but both readings are compared against 2, both fail, and the dispatch declines either way; `#02`
|
|
1041
|
-
// is 2 under both. **If this helper is ever reused for a `want` other than 2, that has to be
|
|
1042
|
-
// revisited**, because for e.g. `want === 8` the two readings disagree about `#010`.
|
|
1043
|
-
const IMM_LITERAL = /^#\s*(?:0[xX][0-9a-fA-F]+|[0-9]+)$/;
|
|
1044
|
-
const immEq = (op: string | undefined, want: number): boolean =>
|
|
1045
|
-
op !== undefined && IMM_LITERAL.test(op) && Number(op.slice(1).trim()) === want;
|
|
1046
1818
|
function recoverJumpTable(
|
|
1047
1819
|
bounds: AsmBlock,
|
|
1048
1820
|
disp: AsmBlock,
|
|
@@ -1055,11 +1827,13 @@ function recoverJumpTable(
|
|
|
1055
1827
|
// direct cmp rX,#M ; bhi DEF → fall through to the dispatch
|
|
1056
1828
|
// long jump cmp rX,#M ; bls DISP ; b DEF → branch TO the dispatch, long-branch the default
|
|
1057
1829
|
//
|
|
1058
|
-
// The second is what agbcc emits whenever the default is out of a conditional branch's reach
|
|
1830
|
+
// The second is what agbcc emits whenever the default is out of a conditional branch's reach:
|
|
1059
1831
|
// Thumb-1 `B<cond>` carries a signed 8-bit HALFWORD offset, so ±256 BYTES, about 128
|
|
1060
|
-
// instructions
|
|
1061
|
-
//
|
|
1062
|
-
//
|
|
1832
|
+
// instructions. Both forms occur and both must be read, and no census says which DOMINATES:
|
|
1833
|
+
// re-counted 2026-09-06 over the 269 of the benchmark's 404 agbcc rows whose reference asm the
|
|
1834
|
+
// local bench cache holds, six carry a table and all six take the DIRECT form. That is a proper
|
|
1835
|
+
// subset of the corpus, so a claim about which form dominates needs a full run to make. `longDefault` is the target of the trailing `b`, read by the caller from the
|
|
1836
|
+
// block after `bounds`.
|
|
1063
1837
|
const bi = bounds.instrs;
|
|
1064
1838
|
const guard = bi[bi.length - 1],
|
|
1065
1839
|
cmp = bi[bi.length - 2];
|
|
@@ -1145,38 +1919,671 @@ function recoverJumpTable(
|
|
|
1145
1919
|
return null;
|
|
1146
1920
|
}
|
|
1147
1921
|
|
|
1148
|
-
// Read the table: the ldr loads a POINTER word (PTR: .word TABLE); the table is TABLE: .word C0…
|
|
1149
|
-
// Note the case labels are matched against `blockLabels` as WRITTEN: the adjacent-label aliasing in
|
|
1150
|
-
// `decode` rewrites branch operands, not `.word` entries, so a table naming an aliased label would
|
|
1151
|
-
// decline here rather than dispatch anywhere. Loud, and no corpus instance — left as a known edge
|
|
1152
|
-
// rather than fixed speculatively.
|
|
1153
|
-
//
|
|
1154
|
-
// The pointer word is addressed the same way every other pool load in this frontend is —
|
|
1155
|
-
// `LABEL[+N]`, selecting word N/4 — because a literal pool is a POOL: agbcc packs the dispatch
|
|
1156
|
-
// pointer in beside whatever else the function needed, and which slot it lands in is an artifact
|
|
1157
|
-
// of emission order. Reading only a bare label whose pool held exactly ONE word declined six real
|
|
1158
|
-
// benchmark functions whose table pointer merely sat later in the pool. Same fix m2c made in
|
|
1159
|
-
// `a7c5c2d`, and the same shared POOL_LABEL grammar the const/gaddr resolvers use, so the three
|
|
1160
|
-
// cannot disagree about what `.L21+0x4` addresses.
|
|
1161
|
-
const pm = ptrLabel.match(POOL_LABEL);
|
|
1162
|
-
const ptrWords = pm ? dataWords.get(pm[1]) : undefined;
|
|
1163
|
-
if (!pm || !ptrWords) {
|
|
1164
|
-
return null;
|
|
1165
|
-
}
|
|
1166
|
-
const ptrOff = pm[2] ? Number(pm[2]) : 0;
|
|
1167
|
-
if (ptrOff % 4 !== 0 || ptrOff / 4 >= ptrWords.length) {
|
|
1168
|
-
return null; // misaligned or past the end of the pool — not a word this pool holds
|
|
1169
|
-
}
|
|
1170
|
-
const caseLabels = dataWords.get(ptrWords[ptrOff / 4].trim());
|
|
1171
|
-
if (!caseLabels || caseLabels.length !== n) {
|
|
1172
|
-
return null;
|
|
1173
|
-
} // table length must equal the bound
|
|
1174
|
-
// Every case target and the default must resolve to a real decoded block; a label that is an
|
|
1175
|
-
// expression (`.L4+4`) or points outside the function would otherwise crash later — decline cleanly.
|
|
1176
|
-
if (!blockLabels.has(defaultLabel) || caseLabels.some((l) => !blockLabels.has(l))) {
|
|
1177
|
-
return null;
|
|
1922
|
+
// Read the table: the ldr loads a POINTER word (PTR: .word TABLE); the table is TABLE: .word C0…
|
|
1923
|
+
// Note the case labels are matched against `blockLabels` as WRITTEN: the adjacent-label aliasing in
|
|
1924
|
+
// `decode` rewrites branch operands, not `.word` entries, so a table naming an aliased label would
|
|
1925
|
+
// decline here rather than dispatch anywhere. Loud, and no corpus instance — left as a known edge
|
|
1926
|
+
// rather than fixed speculatively.
|
|
1927
|
+
//
|
|
1928
|
+
// The pointer word is addressed the same way every other pool load in this frontend is —
|
|
1929
|
+
// `LABEL[+N]`, selecting word N/4 — because a literal pool is a POOL: agbcc packs the dispatch
|
|
1930
|
+
// pointer in beside whatever else the function needed, and which slot it lands in is an artifact
|
|
1931
|
+
// of emission order. Reading only a bare label whose pool held exactly ONE word declined six real
|
|
1932
|
+
// benchmark functions whose table pointer merely sat later in the pool. Same fix m2c made in
|
|
1933
|
+
// `a7c5c2d`, and the same shared POOL_LABEL grammar the const/gaddr resolvers use, so the three
|
|
1934
|
+
// cannot disagree about what `.L21+0x4` addresses.
|
|
1935
|
+
const pm = ptrLabel.match(POOL_LABEL);
|
|
1936
|
+
const ptrWords = pm ? dataWords.get(pm[1]) : undefined;
|
|
1937
|
+
if (!pm || !ptrWords) {
|
|
1938
|
+
return null;
|
|
1939
|
+
}
|
|
1940
|
+
const ptrOff = pm[2] ? Number(pm[2]) : 0;
|
|
1941
|
+
if (ptrOff % 4 !== 0 || ptrOff / 4 >= ptrWords.length) {
|
|
1942
|
+
return null; // misaligned or past the end of the pool — not a word this pool holds
|
|
1943
|
+
}
|
|
1944
|
+
const caseLabels = dataWords.get(ptrWords[ptrOff / 4].trim());
|
|
1945
|
+
if (!caseLabels || caseLabels.length !== n) {
|
|
1946
|
+
return null;
|
|
1947
|
+
} // table length must equal the bound
|
|
1948
|
+
// Every case target and the default must resolve to a real decoded block; a label that is an
|
|
1949
|
+
// expression (`.L4+4`) or points outside the function would otherwise crash later — decline cleanly.
|
|
1950
|
+
if (!blockLabels.has(defaultLabel) || caseLabels.some((l) => !blockLabels.has(l))) {
|
|
1951
|
+
return null;
|
|
1952
|
+
}
|
|
1953
|
+
return { scrutReg, caseLabels, defaultLabel };
|
|
1954
|
+
}
|
|
1955
|
+
|
|
1956
|
+
interface FrameObjectAudit {
|
|
1957
|
+
name: string;
|
|
1958
|
+
irBlocks: Block[];
|
|
1959
|
+
localArea: number;
|
|
1960
|
+
usedSlotOffsets: ReadonlySet<number>;
|
|
1961
|
+
capturedObjectIsTheWholeFrame: boolean;
|
|
1962
|
+
prototypes: Prototypes;
|
|
1963
|
+
symbols: SymbolMap | undefined;
|
|
1964
|
+
target: TargetDescription;
|
|
1965
|
+
}
|
|
1966
|
+
|
|
1967
|
+
/** FRAME-OBJECT AUDIT. Every `laddr` the frontend emitted is only a CLAIM that the address it
|
|
1968
|
+
* names is used as "the address of one scalar local"; this proves it, over the finished function,
|
|
1969
|
+
* the same boundary-total style as the slot-escape assert in finish(). The address may flow
|
|
1970
|
+
* anywhere as a VALUE — into an MMIO register (the DMA-fill idiom), a call, a phi — but every
|
|
1971
|
+
* MEMORY access through it must be at offset 0, with one agreed width and one agreed extension,
|
|
1972
|
+
* its bytes must belong to nothing else in the frame, and any use the audit cannot vouch for declines the whole function
|
|
1973
|
+
* loudly. Nothing here guesses: the object's declared type is exactly the access type the machine
|
|
1974
|
+
* used.
|
|
1975
|
+
*
|
|
1976
|
+
* Takes its inputs explicitly rather than closing over `lift`. All eight are READ, none is
|
|
1977
|
+
* reassigned, and the only mutation is to the ops reachable through `irBlocks` — the widths,
|
|
1978
|
+
* signedness and `volatile` this stamps onto each surviving `laddr`. */
|
|
1979
|
+
function auditFrameObjects({
|
|
1980
|
+
name,
|
|
1981
|
+
irBlocks,
|
|
1982
|
+
localArea,
|
|
1983
|
+
usedSlotOffsets,
|
|
1984
|
+
capturedObjectIsTheWholeFrame,
|
|
1985
|
+
prototypes,
|
|
1986
|
+
symbols,
|
|
1987
|
+
target,
|
|
1988
|
+
}: FrameObjectAudit): void {
|
|
1989
|
+
let laddrs: Op[] = [];
|
|
1990
|
+
for (const blk of irBlocks) {
|
|
1991
|
+
for (const op of blk.ops) {
|
|
1992
|
+
if (op.opcode === 'laddr') {
|
|
1993
|
+
laddrs.push(op);
|
|
1994
|
+
}
|
|
1995
|
+
}
|
|
1996
|
+
}
|
|
1997
|
+
// …and it runs for a licensed acceptance with no object at all, so the premise re-check below
|
|
1998
|
+
// is total rather than resting on "the capture always survives into the IR".
|
|
1999
|
+
if (laddrs.length > 0 || capturedObjectIsTheWholeFrame) {
|
|
2000
|
+
const readOnlySinks = new Set(target.capabilities.readOnlyAddressSinks ?? []);
|
|
2001
|
+
const defOf = new Map<Value, Op>();
|
|
2002
|
+
for (const blk of irBlocks) {
|
|
2003
|
+
for (const op of blk.ops) {
|
|
2004
|
+
for (const res of op.results) {
|
|
2005
|
+
defOf.set(res, op);
|
|
2006
|
+
}
|
|
2007
|
+
}
|
|
2008
|
+
}
|
|
2009
|
+
// A NAME IS NOT AN ADDRESS. The same symbol name can sit at two addresses — a symbol map is
|
|
2010
|
+
// free to carry one — and a `gaddr`'s `sym` can also come straight from the assembly text
|
|
2011
|
+
// (`.word REG_DMA3SAD`), where nothing looked it up at all. So names resolve to an address
|
|
2012
|
+
// here or they resolve to nothing: a name at more than one address vouches for neither.
|
|
2013
|
+
const addrOfName = new Map<string, number | null>();
|
|
2014
|
+
for (const [addr, infos] of symbols ?? []) {
|
|
2015
|
+
for (const si of infos) {
|
|
2016
|
+
addrOfName.set(si.name, addrOfName.has(si.name) ? null : addr);
|
|
2017
|
+
}
|
|
2018
|
+
}
|
|
2019
|
+
// The literal address a value denotes, or undefined when this cannot say. `const` is the
|
|
2020
|
+
// bare pool word, `gaddr` is the same word after the symbol map named it, and `add` is the
|
|
2021
|
+
// base+displacement form an interior attribution produces — three spellings of one address,
|
|
2022
|
+
// which is the point: the answer must not turn on which one the assembly happened to use.
|
|
2023
|
+
const literalAddrOf = (v: Value, depth = 0): number | undefined => {
|
|
2024
|
+
const d = defOf.get(v);
|
|
2025
|
+
if (d === undefined || depth > 2) {
|
|
2026
|
+
return undefined;
|
|
2027
|
+
}
|
|
2028
|
+
if (d.opcode === 'const') {
|
|
2029
|
+
return d.attrs.value as number;
|
|
2030
|
+
}
|
|
2031
|
+
if (d.opcode === 'gaddr') {
|
|
2032
|
+
return addrOfName.get(d.attrs.sym as string) ?? undefined;
|
|
2033
|
+
}
|
|
2034
|
+
if (d.opcode === 'add' && d.operands.length === 2) {
|
|
2035
|
+
const base = literalAddrOf(d.operands[0], depth + 1);
|
|
2036
|
+
const disp = defOf.get(d.operands[1]);
|
|
2037
|
+
if (base !== undefined && disp?.opcode === 'const') {
|
|
2038
|
+
return base + (disp.attrs.value as number);
|
|
2039
|
+
}
|
|
2040
|
+
}
|
|
2041
|
+
return undefined;
|
|
2042
|
+
};
|
|
2043
|
+
// Does this store hand the WHOLE address to something that only reads through it? Word stores
|
|
2044
|
+
// only: a `strh` to a source register hands over half an address, so the device's source is
|
|
2045
|
+
// not this object. A base this cannot resolve — computed, register-offset, merged by a phi —
|
|
2046
|
+
// is the conservative answer.
|
|
2047
|
+
const readsThrough = (op: Op): boolean => {
|
|
2048
|
+
if (readOnlySinks.size === 0 || (op.attrs.width as number) !== 4) {
|
|
2049
|
+
return false;
|
|
2050
|
+
}
|
|
2051
|
+
const base = literalAddrOf(op.operands[0]);
|
|
2052
|
+
return base !== undefined && readOnlySinks.has(base + (op.attrs.off as number));
|
|
2053
|
+
};
|
|
2054
|
+
const fail = (why: string): never => {
|
|
2055
|
+
throw new FrontendUnsupportedError(`cannot lift '${name}': address-taken stack local — ${why}`);
|
|
2056
|
+
};
|
|
2057
|
+
// A FRAME BASE ADDRESSED THROUGH IS NOT A CAPTURE. Thumb-1 gives `ldr`/`str` an `[sp,#imm]`
|
|
2058
|
+
// encoding and gives the sub-word forms none, so a byte or halfword spill can only be spelled
|
|
2059
|
+
// by copying sp into a register and addressing through the copy:
|
|
2060
|
+
//
|
|
2061
|
+
// mov r2, sp
|
|
2062
|
+
// strh r3, [r2, #0x30]
|
|
2063
|
+
//
|
|
2064
|
+
// That is an ADDRESSING MODE. The copy never becomes a value, and the access is the
|
|
2065
|
+
// `[sp,#0x30]` the instruction set cannot spell — so what the machine named is one object at
|
|
2066
|
+
// frame offset 48, not a `[+48]` reach through the frame base.
|
|
2067
|
+
//
|
|
2068
|
+
// A captured address whose every use is a fixed-offset sub-word ACCESS is that shape, and
|
|
2069
|
+
// each of its accesses names its own object: re-root them onto a `laddr` at their own offset,
|
|
2070
|
+
// read at 0, and the rest of this audit judges the objects. A capture with ANY other use is a
|
|
2071
|
+
// real capture and keeps the frame base.
|
|
2072
|
+
//
|
|
2073
|
+
// What makes that judgement total is that the walk below enumerates every ROLE a value can
|
|
2074
|
+
// appear in — every operand of every op, and every edge argument — instead of asking what an
|
|
2075
|
+
// instruction looks like. One instruction can hold two roles: `str rD, [rD, #k]` stores the
|
|
2076
|
+
// frame address through itself, a base use AND an escape, and the escape is what stops the
|
|
2077
|
+
// split.
|
|
2078
|
+
// Why a capture was NOT split, when the reason is one no later message carries — the
|
|
2079
|
+
// `slotsOffReason` idiom: a refusal reported as the wrong capability sends the improvement
|
|
2080
|
+
// loop to build the wrong thing.
|
|
2081
|
+
let splitRefusal: string | null = null;
|
|
2082
|
+
{
|
|
2083
|
+
const uses = new Map<Value, { op: Op; idx: number; blk: Block }[]>();
|
|
2084
|
+
const record = (v: Value, op: Op, idx: number, blk: Block) =>
|
|
2085
|
+
(uses.get(v) ?? uses.set(v, []).get(v)!).push({ op, idx, blk });
|
|
2086
|
+
for (const blk of irBlocks) {
|
|
2087
|
+
for (const op of blk.ops) {
|
|
2088
|
+
op.operands.forEach((v, idx) => record(v, op, idx, blk));
|
|
2089
|
+
// An EDGE ARGUMENT is a use role too, and never an access: a capture that reaches a
|
|
2090
|
+
// block parameter is live past this block, so the taint closure below is what judges
|
|
2091
|
+
// it. Recorded at index -1 so it can never be counted as an access — a split there
|
|
2092
|
+
// would delete a capture the successor argument still names.
|
|
2093
|
+
for (const succ of op.successors ?? []) {
|
|
2094
|
+
for (const a of succ.args) {
|
|
2095
|
+
record(a, op, -1, blk);
|
|
2096
|
+
}
|
|
2097
|
+
}
|
|
2098
|
+
}
|
|
2099
|
+
}
|
|
2100
|
+
const minted: Op[] = [];
|
|
2101
|
+
const consumed = new Set<Op>();
|
|
2102
|
+
for (const capture of laddrs) {
|
|
2103
|
+
const at = uses.get(capture.results[0]) ?? [];
|
|
2104
|
+
const accesses = at.filter((u) => (u.op.opcode === 'load' || u.op.opcode === 'store') && u.idx === 0);
|
|
2105
|
+
// SUB-WORD ONLY, because that is the whole of what the encoding gap forces: `ldr`/`str` DO
|
|
2106
|
+
// have an `[sp,#imm]` form, so a WORD access through a copy is some other shape and must
|
|
2107
|
+
// not be read as this one. It is also what keeps the outgoing-argument area safe — that
|
|
2108
|
+
// guard reads `[sp,#k]` accesses (spMemAccess), which an access through a copy is not, and
|
|
2109
|
+
// agbcc stages arguments 5+ there with `str`.
|
|
2110
|
+
const subWord = accesses.every((u) => (u.op.attrs.width as number) < 4);
|
|
2111
|
+
// A use that is not an access leaves the capture naming the frame base, and the judgement
|
|
2112
|
+
// below reports that use itself — an escape, a phi, arithmetic — so it needs no reason
|
|
2113
|
+
// here. The WIDTH does: nothing downstream mentions it, so a refused word access would be
|
|
2114
|
+
// reported as "a store at [+4]" and the histogram would be asked for the wrong capability.
|
|
2115
|
+
if (at.length === 0 || accesses.length !== at.length) {
|
|
2116
|
+
continue;
|
|
2117
|
+
}
|
|
2118
|
+
if (!subWord) {
|
|
2119
|
+
splitRefusal ??= 'a WORD access through the copy, and `ldr`/`str` have an `[sp,#imm]` form';
|
|
2120
|
+
continue;
|
|
2121
|
+
}
|
|
2122
|
+
// Nothing to split when the capture already names ONE object: every access at offset 0 is
|
|
2123
|
+
// the frame base itself, which is what the DMA-fill idiom captures.
|
|
2124
|
+
if (accesses.every((u) => u.op.attrs.off === 0)) {
|
|
2125
|
+
continue;
|
|
2126
|
+
}
|
|
2127
|
+
for (const u of accesses) {
|
|
2128
|
+
const res = mkValue(T.unk(32));
|
|
2129
|
+
const object = mkOp('laddr', { results: [res], attrs: { off: u.op.attrs.off as number } });
|
|
2130
|
+
u.blk.ops.splice(u.blk.ops.indexOf(u.op), 0, object);
|
|
2131
|
+
minted.push(object);
|
|
2132
|
+
// The ADDRESS operand only — the stored value (operand 1) is passed through
|
|
2133
|
+
// untouched, so no slot home moves (ir/core.ts `SlotHomes`). These accesses go through
|
|
2134
|
+
// a COPY of `sp` rather than the `[sp,#k]` keys the stamp reads, so none of them
|
|
2135
|
+
// carried one to begin with.
|
|
2136
|
+
u.op.operands = [res, ...u.op.operands.slice(1)];
|
|
2137
|
+
u.op.attrs = { ...u.op.attrs, off: 0 };
|
|
2138
|
+
}
|
|
2139
|
+
consumed.add(capture);
|
|
2140
|
+
}
|
|
2141
|
+
if (consumed.size > 0) {
|
|
2142
|
+
for (const blk of irBlocks) {
|
|
2143
|
+
blk.ops = blk.ops.filter((op) => !consumed.has(op));
|
|
2144
|
+
}
|
|
2145
|
+
laddrs = [...laddrs.filter((op) => !consumed.has(op)), ...minted];
|
|
2146
|
+
}
|
|
2147
|
+
}
|
|
2148
|
+
// ONE OBJECT PER FRAME OFFSET. Two `laddr` at the same offset name the same storage; two at
|
|
2149
|
+
// different offsets are different objects, so width, signedness, escape and the overlap
|
|
2150
|
+
// window are decided per offset — one width shared by every capture in the function would
|
|
2151
|
+
// declare a halfword spill and a word spill as one object.
|
|
2152
|
+
const objects = new Map<number, Op[]>();
|
|
2153
|
+
for (const op of laddrs) {
|
|
2154
|
+
const off = op.attrs.off as number;
|
|
2155
|
+
(objects.get(off) ?? objects.set(off, []).get(off)!).push(op);
|
|
2156
|
+
}
|
|
2157
|
+
// Taint maps a value to the OBJECT whose address it may hold, closed over phis: a tainted
|
|
2158
|
+
// edge arg taints the receiving block param. A phi that merges two objects has no single
|
|
2159
|
+
// answer, and picking one would put an access on the wrong storage. Nothing builds one today,
|
|
2160
|
+
// and the reason is worth knowing before changing the split: an object at a nonzero offset
|
|
2161
|
+
// exists only where the split ran, the split refuses any capture with an edge-argument use,
|
|
2162
|
+
// and every frame-base object is the same object.
|
|
2163
|
+
const taint = new Map<Value, number>();
|
|
2164
|
+
for (const [off, ops] of objects) {
|
|
2165
|
+
for (const op of ops) {
|
|
2166
|
+
taint.set(op.results[0], off);
|
|
2167
|
+
}
|
|
2168
|
+
}
|
|
2169
|
+
for (let changed = true; changed;) {
|
|
2170
|
+
changed = false;
|
|
2171
|
+
for (const blk of irBlocks) {
|
|
2172
|
+
for (const op of blk.ops) {
|
|
2173
|
+
for (const s of op.successors ?? []) {
|
|
2174
|
+
s.args.forEach((arg, i) => {
|
|
2175
|
+
const from = taint.get(arg);
|
|
2176
|
+
const param = s.block.params[i];
|
|
2177
|
+
if (from === undefined || param === undefined) {
|
|
2178
|
+
return;
|
|
2179
|
+
}
|
|
2180
|
+
const had = taint.get(param);
|
|
2181
|
+
if (had === from) {
|
|
2182
|
+
return;
|
|
2183
|
+
}
|
|
2184
|
+
if (had !== undefined) {
|
|
2185
|
+
fail(`a phi merges the frame objects at [sp,#${had}] and [sp,#${from}] — one value, two objects`);
|
|
2186
|
+
}
|
|
2187
|
+
taint.set(param, from);
|
|
2188
|
+
changed = true;
|
|
2189
|
+
});
|
|
2190
|
+
}
|
|
2191
|
+
}
|
|
2192
|
+
}
|
|
2193
|
+
}
|
|
2194
|
+
// Judge every use of a tainted value, against the object it names.
|
|
2195
|
+
const accesses = new Map<number, { width: number; signed: boolean; isLoad: boolean }[]>();
|
|
2196
|
+
const escaped = new Set<number>();
|
|
2197
|
+
// TWO QUESTIONS, not one. `escaped` asks whether the address LEFT the function, which is what
|
|
2198
|
+
// decides `volatile`. `mayWrite` asks whether it reached something that could write the frame
|
|
2199
|
+
// BACK, which is what every "a callee may write any frame offset" refusal below rests on. A
|
|
2200
|
+
// store into a device's SOURCE register answers yes to the first and no to the second: the
|
|
2201
|
+
// hardware reads the object, and the DMA-fill idiom this capability was built for
|
|
2202
|
+
// (`vu16 tmp; DmaSet(n, &tmp, …)`) is exactly that shape.
|
|
2203
|
+
const mayWrite = new Set<number>();
|
|
2204
|
+
// …and the two escapes SPLIT, because each decides something the other does not.
|
|
2205
|
+
// `passedToCallee` is the address handed to a callee as an argument — the one escape whose
|
|
2206
|
+
// writer this frontend can name, which is what the struct-return premise re-check below rests
|
|
2207
|
+
// on, and what tells a refusal message which escape it is talking about. `published` is the
|
|
2208
|
+
// address WRITTEN TO MEMORY, how the DMA idiom hands the object to hardware, and what
|
|
2209
|
+
// `volatile` at the stamp keys on. Reading either off `escaped` gets the other one wrong.
|
|
2210
|
+
const passedToCallee = new Set<number>();
|
|
2211
|
+
const published = new Set<number>();
|
|
2212
|
+
// …and WHICH ARGUMENT it was passed as, because argument 0 is the one position a hidden
|
|
2213
|
+
// struct-return pointer can occupy. A `call`'s operand index IS the argument index here (the
|
|
2214
|
+
// `bl` arm reads r0..r<argc-1> in order), so an address handed over at r1 or above is an
|
|
2215
|
+
// argument the source wrote.
|
|
2216
|
+
const passedAboveArg0 = new Set<number>();
|
|
2217
|
+
// …and WHICH CALLEE took it at argument 0, because that callee's declared RETURN TYPE is the
|
|
2218
|
+
// one fact that tells an out-parameter from a hidden struct return. `null` is the narrowing of
|
|
2219
|
+
// an unstamped `target` attr — the `bl`/`blx` lowering always stamps one — and reads as a
|
|
2220
|
+
// callee nothing can be declared about, so it refuses.
|
|
2221
|
+
const arg0Callees = new Map<number, Set<string | null>>();
|
|
2222
|
+
for (const off of objects.keys()) {
|
|
2223
|
+
accesses.set(off, []);
|
|
2224
|
+
}
|
|
2225
|
+
for (const blk of irBlocks) {
|
|
2226
|
+
for (const op of blk.ops) {
|
|
2227
|
+
op.operands.forEach((v, idx) => {
|
|
2228
|
+
const off = taint.get(v);
|
|
2229
|
+
if (off === undefined) {
|
|
2230
|
+
return;
|
|
2231
|
+
}
|
|
2232
|
+
const scalar = (kind: string) => {
|
|
2233
|
+
if ((op.attrs.off as number) !== 0) {
|
|
2234
|
+
fail(
|
|
2235
|
+
`a ${kind} at [+${op.attrs.off}] through the captured address — ` +
|
|
2236
|
+
(splitRefusal ?? 'only a scalar at the captured address is modelled'),
|
|
2237
|
+
);
|
|
2238
|
+
}
|
|
2239
|
+
};
|
|
2240
|
+
if (op.opcode === 'load' && idx === 0) {
|
|
2241
|
+
scalar('load');
|
|
2242
|
+
accesses.get(off)!.push({
|
|
2243
|
+
width: op.attrs.width as number,
|
|
2244
|
+
signed: (op.attrs.signed as boolean) ?? false,
|
|
2245
|
+
isLoad: true,
|
|
2246
|
+
});
|
|
2247
|
+
return;
|
|
2248
|
+
}
|
|
2249
|
+
if (op.opcode === 'store' && idx === 0) {
|
|
2250
|
+
scalar('store');
|
|
2251
|
+
accesses.get(off)!.push({ width: op.attrs.width as number, signed: false, isLoad: false });
|
|
2252
|
+
return;
|
|
2253
|
+
}
|
|
2254
|
+
if ((op.opcode === 'store' && idx === 1) || op.opcode === 'call') {
|
|
2255
|
+
escaped.add(off); // the address ESCAPES as a value — the point of the capability
|
|
2256
|
+
if (!(op.opcode === 'store' && readsThrough(op))) {
|
|
2257
|
+
mayWrite.add(off);
|
|
2258
|
+
}
|
|
2259
|
+
if (op.opcode === 'call') {
|
|
2260
|
+
passedToCallee.add(off);
|
|
2261
|
+
if (idx > 0) {
|
|
2262
|
+
passedAboveArg0.add(off);
|
|
2263
|
+
} else {
|
|
2264
|
+
const t = op.attrs.target;
|
|
2265
|
+
const cs = arg0Callees.get(off) ?? new Set<string | null>();
|
|
2266
|
+
cs.add(typeof t === 'string' ? t : null);
|
|
2267
|
+
arg0Callees.set(off, cs);
|
|
2268
|
+
}
|
|
2269
|
+
} else {
|
|
2270
|
+
published.add(off); // written to memory — the DMA idiom's `*dmaReg = &tmp`
|
|
2271
|
+
}
|
|
2272
|
+
return;
|
|
2273
|
+
}
|
|
2274
|
+
fail(`the captured address flows into \`${op.opcode}\` — not an access, an escape, or a phi`);
|
|
2275
|
+
});
|
|
2276
|
+
}
|
|
2277
|
+
}
|
|
2278
|
+
|
|
2279
|
+
// THE ACCEPTANCE'S PREMISE, RE-ASKED OF THE IR. `capturedObjectIsTheWholeFrame` is a reading
|
|
2280
|
+
// of the TEXT and it is the one thing in this file that switches a refusal OFF, so the two
|
|
2281
|
+
// facts it claims are re-proven here, where they are exact, rather than left to the
|
|
2282
|
+
// approximation that licensed them. Neither is a second opinion on the same evidence: the
|
|
2283
|
+
// scan asks what a REGISTER holds at a `bl`; this asks what the finished function does with
|
|
2284
|
+
// the OBJECT.
|
|
2285
|
+
//
|
|
2286
|
+
// PASSED TO A CALLEE. The whole licence is "a callee is holding the address of this frame",
|
|
2287
|
+
// and the pre-lift scan can say that of a register whose value never reaches a call operand —
|
|
2288
|
+
// a declared arity trims it away, or the register is dead by the time the call is built. When
|
|
2289
|
+
// it does, [sp,#0] has been re-modelled as an addressable object on no evidence at all, and
|
|
2290
|
+
// the outgoing argument that really lived there is gone from the call.
|
|
2291
|
+
//
|
|
2292
|
+
// NOT A STRUCT-RETURN TEMP. A one-word frame rules out agbcc's block-copy bases (each needs
|
|
2293
|
+
// two words) but NOT the hidden return pointer of a <=4-byte non-integer-like struct, which is
|
|
2294
|
+
// exactly one word: `struct S4 { char a,b,c,d; }; struct S4 s = mk(x);` compiles to `add
|
|
2295
|
+
// sp,#-4 / mov r0,sp / bl mk / ldr r0,[sp]`, instruction for instruction an out-parameter
|
|
2296
|
+
// call. Left alone that lifted as `mk(&sp0, a0)` — a call the real prototype rejects.
|
|
2297
|
+
//
|
|
2298
|
+
// THREE facts rule it out and any one will do, because a return temp is storage the CALLEE
|
|
2299
|
+
// owns outright: it is written only by the callee, its pointer is argument 0, always
|
|
2300
|
+
// (compiled — `struct S4 mk3(int,int,int)` puts sp in r0 and shifts all three real arguments
|
|
2301
|
+
// up), and the callee RETURNS the struct. So a store of our own says the object is one this
|
|
2302
|
+
// function fills; an address handed over at r1 or above says the same by position; and a
|
|
2303
|
+
// callee the project declares `void` says it by the ABI — a function that returns nothing has
|
|
2304
|
+
// no hidden return pointer to be given, whatever sits in r0.
|
|
2305
|
+
//
|
|
2306
|
+
// THE THIRD IS A DECLARATION, NOT AN INFERENCE, and it is the ONLY refusal this frontend
|
|
2307
|
+
// switches off on something other than the instruction stream. `FnProto.returnsVoid` is the
|
|
2308
|
+
// project's own header fact, arriving through the same table whose `params` this file already
|
|
2309
|
+
// trusts to decide a call's arity. It is asked of EVERY callee that took the address at
|
|
2310
|
+
// argument 0, because the object gets one decision: one callee undeclared or declared
|
|
2311
|
+
// non-void leaves the ambiguity standing and the refusal fires.
|
|
2312
|
+
//
|
|
2313
|
+
// WHAT IT COSTS WHEN THE DECLARATION IS WRONG, measured rather than compared. On the `sret`
|
|
2314
|
+
// shape above, with `mk` (which really returns `struct S4`) declared `params: 1,
|
|
2315
|
+
// returnsVoid: true`, the lift succeeds and emits `s32 sret(s32 a0) { s32 sp0; mk(&sp0);
|
|
2316
|
+
// return (u8)sp0; }` — a compiling, plausible, WRONG program with the real argument dropped,
|
|
2317
|
+
// where a loud decline stood. Not a smaller cost than a wrong ARITY, either: the same entry
|
|
2318
|
+
// supplies both facts, so a wrong `returnsVoid` drops the argument too, and the frame re-model
|
|
2319
|
+
// is the silent half. The trade is accepted because there IS no other discriminator: compiled
|
|
2320
|
+
// through
|
|
2321
|
+
// the benchmark's own agbcc command, the hidden struct return and the out-parameter emit the
|
|
2322
|
+
// same instructions in the same order, the slot is read back at a scalar width in both, and
|
|
2323
|
+
// in both the value read back is what the function returns — so an asm-side corroboration
|
|
2324
|
+
// would be a rule with no discriminating input. The mitigation is that under-declaring is the
|
|
2325
|
+
// safe direction (an undeclared or non-void callee still declines) and that `FnProto` says
|
|
2326
|
+
// so at the field.
|
|
2327
|
+
//
|
|
2328
|
+
// The residual cost is stated rather than hidden: an OUTPUT-only parameter taken at argument
|
|
2329
|
+
// 0 of a callee the project has NOT declared is still byte-for-byte a struct return, and
|
|
2330
|
+
// still declines with it.
|
|
2331
|
+
const arg0AllDeclaredVoid = (off: number): boolean => {
|
|
2332
|
+
const cs = arg0Callees.get(off);
|
|
2333
|
+
return cs !== undefined && cs.size > 0 && [...cs].every((c) => c !== null && prototypes[c]?.returnsVoid === true);
|
|
2334
|
+
};
|
|
2335
|
+
if (capturedObjectIsTheWholeFrame) {
|
|
2336
|
+
if (!passedToCallee.has(0)) {
|
|
2337
|
+
fail(
|
|
2338
|
+
'the one-word-frame proof licensed this lift on the frame base reaching a callee, and ' +
|
|
2339
|
+
'no call in the lifted function takes it — so nothing rules out an outgoing stack argument at [sp,#0]',
|
|
2340
|
+
);
|
|
2341
|
+
}
|
|
2342
|
+
if (!accesses.get(0)?.some((a) => !a.isLoad) && !passedAboveArg0.has(0) && !arg0AllDeclaredVoid(0)) {
|
|
2343
|
+
fail(
|
|
2344
|
+
'the one-word frame is handed to a callee as argument 0 and never written here, which ' +
|
|
2345
|
+
'is how a hidden struct-return pointer looks — and the callee is not declared `void`, so ' +
|
|
2346
|
+
'nothing says it does not own the storage',
|
|
2347
|
+
);
|
|
2348
|
+
}
|
|
2349
|
+
}
|
|
2350
|
+
|
|
2351
|
+
// The declared type of each object, and then that its bytes belong to nothing else.
|
|
2352
|
+
const extent = new Map<number, number>();
|
|
2353
|
+
for (const [off, acc] of accesses) {
|
|
2354
|
+
if (acc.length === 0) {
|
|
2355
|
+
// nothing in-function pins the object's type, and a guessed declaration is the
|
|
2356
|
+
// plausible-but-wrong class — decline until an inhabitant needs this
|
|
2357
|
+
fail('the captured address is never dereferenced in this function, so nothing pins the local object type');
|
|
2358
|
+
}
|
|
2359
|
+
const widths = new Set(acc.map((a) => a.width));
|
|
2360
|
+
if (widths.size > 1) {
|
|
2361
|
+
fail(`the accesses through the captured address disagree on width (${[...widths].join(' vs ')})`);
|
|
2362
|
+
}
|
|
2363
|
+
// …and on SIGNEDNESS, over the loads, for the same reason: one declared type extends one
|
|
2364
|
+
// way, so an object read by both `ldrsb` and `ldrb` has no faithful declaration —
|
|
2365
|
+
// `sp4 - sp4` would fold to 0 where the machine computes sext(b) - zext(b). Loads only: a
|
|
2366
|
+
// store extends nothing, and `strb` beside `ldrsb` is not a disagreement.
|
|
2367
|
+
const signs = new Set(acc.filter((a) => a.isLoad).map((a) => a.signed));
|
|
2368
|
+
if (signs.size > 1) {
|
|
2369
|
+
fail('the loads through the captured address disagree on signedness — one declared type extends one way');
|
|
2370
|
+
}
|
|
2371
|
+
extent.set(off, acc[0].width);
|
|
2372
|
+
}
|
|
2373
|
+
// TWO MODELS FOR ONE BYTE is a silent disagreement, so each object must own its bytes
|
|
2374
|
+
// outright: inside the reserved local area, clear of every SSA slot (which the slot model
|
|
2375
|
+
// keeps in registers, so a store through the object would not be seen there), and clear of
|
|
2376
|
+
// every other object.
|
|
2377
|
+
const overlaps = (a: number, aw: number, b: number, bw: number) => a < b + bw && b < a + aw;
|
|
2378
|
+
const objs = [...extent].sort((x, y) => x[0] - y[0]);
|
|
2379
|
+
for (const [off, width] of objs) {
|
|
2380
|
+
if (off < 0 || off + width > localArea) {
|
|
2381
|
+
fail(`the object at [sp,#${off}) of width ${width} lies outside the reserved local area`);
|
|
2382
|
+
}
|
|
2383
|
+
for (const slot of usedSlotOffsets) {
|
|
2384
|
+
if (overlaps(off, width, slot, 4)) {
|
|
2385
|
+
fail(`the object at [sp,#${off}) overlaps the SSA slot at [sp,#${slot}] — one byte, two models`);
|
|
2386
|
+
}
|
|
2387
|
+
}
|
|
2388
|
+
}
|
|
2389
|
+
for (let i = 1; i < objs.length; i++) {
|
|
2390
|
+
const [off, width] = objs[i];
|
|
2391
|
+
const [prev, prevWidth] = objs[i - 1];
|
|
2392
|
+
if (overlaps(prev, prevWidth, off, width)) {
|
|
2393
|
+
fail(`the objects at [sp,#${prev}) and [sp,#${off}) overlap — one byte, two models`);
|
|
2394
|
+
}
|
|
2395
|
+
}
|
|
2396
|
+
// WHAT AN ESCAPE COSTS. The audit bounds what WE access through an object, never what a callee
|
|
2397
|
+
// does with the address it was handed — and a callee may write any offset from it. So an
|
|
2398
|
+
// escape retracts two claims, both of them function-wide because one address reaches the
|
|
2399
|
+
// whole frame.
|
|
2400
|
+
//
|
|
2401
|
+
// The first is that the other ADDRESS-TAKEN objects are private, and it keys on ANY escape —
|
|
2402
|
+
// this is the rule `mayWrite` does NOT narrow. Its argument is about LAYOUT, and layout is
|
|
2403
|
+
// symmetric: two objects are two separate C locals with no guaranteed adjacency, so a device
|
|
2404
|
+
// that READS past the one it was given is as wrong as a callee that writes past it. `DmaCopy`
|
|
2405
|
+
// with a count of two halfwords off `&sp0` transfers `[sp,#2]` too, and the emitted source
|
|
2406
|
+
// transfers whatever the recompiler put after `sp0`, and the second object's own store is
|
|
2407
|
+
// whatever the recompiler made of it. Marking both volatile would not repair that: the locals
|
|
2408
|
+
// are still placed independently.
|
|
2409
|
+
//
|
|
2410
|
+
// ACCEPTED RESIDUE, so the rule is not read as wider than it is: it counts `laddr` objects,
|
|
2411
|
+
// so a neighbour that is merely SPILLED to an SSA slot is over-read just the same and nothing
|
|
2412
|
+
// refuses, and the audit never reads the transfer's control word, so an incrementing source
|
|
2413
|
+
// is vouched for exactly as a fixed one is. Both are reads, so the `undef` argument holds
|
|
2414
|
+
// either way, and both predate this rule.
|
|
2415
|
+
if (escaped.size > 0 && objects.size > 1) {
|
|
2416
|
+
fail(
|
|
2417
|
+
'the captured address escapes, so something outside this function reaches the whole ' +
|
|
2418
|
+
'frame — including another object',
|
|
2419
|
+
);
|
|
2420
|
+
}
|
|
2421
|
+
// The second is `undef`, which rests on this function's own stores being the ONLY writer of
|
|
2422
|
+
// its frame. A wider real object (`struct P p; g(&p);` where only `p.x` is read here) has its
|
|
2423
|
+
// later words written by `g` and read back at a slot no store of ours reaches — declaring
|
|
2424
|
+
// those uninitialised spells the callee's value as garbage. The extents here are inferred
|
|
2425
|
+
// from OUR accesses, which is the number that is too small in this shape.
|
|
2426
|
+
//
|
|
2427
|
+
// On an escape and not on "a laddr exists": an address dereferenced only in-function cannot
|
|
2428
|
+
// be written by anyone else, and the overlap checks above cover its aliasing.
|
|
2429
|
+
//
|
|
2430
|
+
// FRAME undefs only. A register-keyed one says a local lives in a register the ABI does not
|
|
2431
|
+
// pass arguments in, and no address reaches a register — the escape this retraction is about
|
|
2432
|
+
// cannot touch it, and counting it would refuse the whole function for an unrelated escape.
|
|
2433
|
+
const undefSlots = irBlocks.some((blk) =>
|
|
2434
|
+
blk.ops.some((op) => op.opcode === 'undef' && slotKeyOffset(op.attrs.key as string) !== null),
|
|
2435
|
+
);
|
|
2436
|
+
if (mayWrite.size > 0 && undefSlots) {
|
|
2437
|
+
fail(
|
|
2438
|
+
'the captured address escapes, so a callee may write any frame offset and an unstored slot is not provably uninitialised',
|
|
2439
|
+
);
|
|
2440
|
+
}
|
|
2441
|
+
|
|
2442
|
+
// …and the SLOT MODEL is the third claim an escape retracts — the undef rule's argument
|
|
2443
|
+
// taken one step further. The extents above are inferred from OUR accesses, so an object
|
|
2444
|
+
// wider in the SOURCE than the bytes this function touches has its later words written by
|
|
2445
|
+
// the callee — and any of those modelled as an SSA slot is a value the slot model forwards
|
|
2446
|
+
// ACROSS the call that overwrote it.
|
|
2447
|
+
//
|
|
2448
|
+
// Not a hypothetical, and not new with the outgoing-argument gate above either: this shape
|
|
2449
|
+
// reached the old capture path and lifted wrongly. The object has to be reached ONLY through
|
|
2450
|
+
// the captured pointer (an `[sp,#0]` access of its own collides with the slot model and
|
|
2451
|
+
// declines at the overlap check), which is what four corpus functions do:
|
|
2452
|
+
//
|
|
2453
|
+
// mov r2, sp / str r0, [r2] @ the object, written through the captured address
|
|
2454
|
+
// str r1, [sp, #0x4] @ a word the slot model keys
|
|
2455
|
+
// mov r0, r2 / bl g @ the base escapes; `g` may write [sp,#4]
|
|
2456
|
+
// ldr r0, [sp, #0x4] @ …and the machine RELOADS it after the call
|
|
2457
|
+
//
|
|
2458
|
+
// and the lift emitted `use2(a1)` — the reload replaced by the value from BEFORE the call,
|
|
2459
|
+
// the callee's write dropped, no diagnostic. Exactly the silent-wrong-answer trade the sp
|
|
2460
|
+
// guards exist to prevent, so it refuses.
|
|
2461
|
+
//
|
|
2462
|
+
// WHAT IT COSTS, stated because the benchmark cannot see it: it refuses every word slot above
|
|
2463
|
+
// a `mayWrite` object, which is blunter than the hazard it names — four corpus functions
|
|
2464
|
+
// decline on it (sa3 `sub_809C274`, `UpdateAnimations`, `sub_801C4A0`, `sub_8062CFC`), none
|
|
2465
|
+
// of them a benchmark row. Narrowing it needs the object's real extent, and this model does
|
|
2466
|
+
// not carry one: `extent` is a single width from a single access. The asm sometimes cannot
|
|
2467
|
+
// supply it either — the compiled twin at `capturedObjectIsTheWholeFrame` is exactly this
|
|
2468
|
+
// rule's shape, a slot THIS FUNCTION stores and reloads, undecidable between a spill and a
|
|
2469
|
+
// member.
|
|
2470
|
+
//
|
|
2471
|
+
// ABOVE the object only: a C object extends upward from its base, so a slot BELOW it cannot
|
|
2472
|
+
// be part of it, and the overlap checks above already own the bytes it does cover.
|
|
2473
|
+
//
|
|
2474
|
+
// `mayWrite`, the same predicate the undef rule takes, because the two rules rest on one
|
|
2475
|
+
// argument and a callee is not the only writer. `struct M { u8 b; u8 pad[3]; s32 t; };
|
|
2476
|
+
// gp = &m; g2(); use2(m.t);` PUBLISHES the base to an ordinary global and the machine reloads
|
|
2477
|
+
// [sp,#4] after `bl g2` — `g2` writes through `gp`, which points here. Keyed on
|
|
2478
|
+
// `passedToCallee` that lifted as `use2(v0)`, the reload replaced by the value from before
|
|
2479
|
+
// the call, no diagnostic: the same silent wrong answer as the call shape, one escape over.
|
|
2480
|
+
//
|
|
2481
|
+
// Not `escaped`, which is the strictly wider set and the one that costs: the DMA-fill idiom
|
|
2482
|
+
// publishes to a device SOURCE register, which reads the object and never writes it, and
|
|
2483
|
+
// `readsThrough` is exactly the exemption that keeps `mayWrite` off those rows. What stays
|
|
2484
|
+
// residue is a base stored through a pointer this cannot resolve: unresolvable is the
|
|
2485
|
+
// conservative answer there, so such a store IS in `mayWrite` and such a frame declines.
|
|
2486
|
+
for (const off of mayWrite) {
|
|
2487
|
+
for (const slot of usedSlotOffsets) {
|
|
2488
|
+
if (slot > off) {
|
|
2489
|
+
const how = passedToCallee.has(off) ? 'is passed to a callee' : 'is stored to memory';
|
|
2490
|
+
fail(
|
|
2491
|
+
`the captured address at [sp,#${off}) ${how}, which may write the ` +
|
|
2492
|
+
`slot at [sp,#${slot}] — this function's own store there would be forwarded past the write`,
|
|
2493
|
+
);
|
|
2494
|
+
}
|
|
2495
|
+
}
|
|
2496
|
+
}
|
|
2497
|
+
// …and the FOURTH claim an escape retracts is the object's TOP, which the three rules above
|
|
2498
|
+
// leave to whatever this function happened to touch. `extent` is one width from one access,
|
|
2499
|
+
// so an object wider in the SOURCE than those bytes is declared too small — and a callee
|
|
2500
|
+
// holding its address writes frame bytes the emitted C never allocated. Compiled:
|
|
2501
|
+
//
|
|
2502
|
+
// u8 buf[12]; buf[0] = x; garr(buf); use2(buf[0]);
|
|
2503
|
+
// → add sp,sp,#-0xc / mov r1,sp / strb r0,[r1] / mov r0,sp / bl garr
|
|
2504
|
+
//
|
|
2505
|
+
// lifted as `u8 sp0; garr(&sp0); use2(sp0)` — a 12-byte object declared one byte, in a frame
|
|
2506
|
+
// the recompile makes 4 bytes wide, with `garr` writing the other 8 into the caller's. The
|
|
2507
|
+
// three rules above all pass it: one object, no `undef` op, no slot above it.
|
|
2508
|
+
//
|
|
2509
|
+
// What licenses an answer is the frame being ACCOUNTED FOR, word by word. Every word of the
|
|
2510
|
+
// reserved local area has to be an object this audit modelled or a slot the slot model keys;
|
|
2511
|
+
// a word that is neither is storage nothing here describes, so the emitted C reserves less
|
|
2512
|
+
// than the machine did and the writer reaches past what it allocated. Whole local area and
|
|
2513
|
+
// not only the words above the object: a word BELOW cannot be part of the object, but it is
|
|
2514
|
+
// still frame the declaration has to account for. Word granularity, not byte — the stack is
|
|
2515
|
+
// word-aligned, so a halfword object owns its word and the padding beside it is not a second
|
|
2516
|
+
// local.
|
|
2517
|
+
//
|
|
2518
|
+
// `mayWrite`, the predicate the two rules above take, and for the same reason: a device
|
|
2519
|
+
// SOURCE register reads through the address and cannot write the frame back.
|
|
2520
|
+
//
|
|
2521
|
+
// WHAT IT LEAVES, since this is the extent question the gate comment above is about: a
|
|
2522
|
+
// `mayWrite` escape is accepted only where the modelled objects and the keyed slots tile the
|
|
2523
|
+
// reserved area between them — a word above the object is a slot (refused above), a second
|
|
2524
|
+
// object (refused above), or unaccounted (refused here). That is not a wider extent model; it
|
|
2525
|
+
// is the same one-scalar `extent`, made to say when it does not fit. An object of two words
|
|
2526
|
+
// cannot be built here at all — the second access that would reach it is a `[+4]` the
|
|
2527
|
+
// `scalar()` guard refuses — so no widening of the frame licence admits a shape this rule
|
|
2528
|
+
// would then have to judge.
|
|
2529
|
+
if (mayWrite.size > 0) {
|
|
2530
|
+
const accountedWords = new Set<number>();
|
|
2531
|
+
for (const [off, width] of extent) {
|
|
2532
|
+
for (let w = off - (off % 4); w < off + width; w += 4) {
|
|
2533
|
+
accountedWords.add(w);
|
|
2534
|
+
}
|
|
2535
|
+
}
|
|
2536
|
+
for (const slot of usedSlotOffsets) {
|
|
2537
|
+
accountedWords.add(slot - (slot % 4));
|
|
2538
|
+
}
|
|
2539
|
+
for (let w = 0; w < localArea; w += 4) {
|
|
2540
|
+
if (!accountedWords.has(w)) {
|
|
2541
|
+
fail(
|
|
2542
|
+
`the word at [sp,#${w}] is neither an object this lift models nor a slot it keys, ` +
|
|
2543
|
+
`and the captured address reaches something that may write it — nothing accounts for the ` +
|
|
2544
|
+
`rest of the frame, so nothing bounds the captured object's extent`,
|
|
2545
|
+
);
|
|
2546
|
+
}
|
|
2547
|
+
}
|
|
2548
|
+
}
|
|
2549
|
+
// Proven. Stamp the MACHINE FACTS the audit established — width and signedness are what the
|
|
2550
|
+
// accesses used, so the declaration downstream is a fact, not a guess. The C-level NAME is
|
|
2551
|
+
// deliberately NOT chosen here: identifiers live in the structurer's namespace (params,
|
|
2552
|
+
// locals, globals, the symbol map), which the frontend cannot see — a frontend-chosen `sp0`
|
|
2553
|
+
// silently shadowed a project global of the same name.
|
|
2554
|
+
// `volatile` iff the address is PUBLISHED — written to memory, rather than handed to a
|
|
2555
|
+
// callee. That is the DMA idiom this rule was written for and it IS the source's own
|
|
2556
|
+
// spelling there: klonoa's `DMA_FILL` writes `vu##bit tmp` outright, sa3's does under
|
|
2557
|
+
// `PLATFORM_GBA`, pokeemerald's inside `DMA_FILL_UNCHECKED`, and the address goes to a device
|
|
2558
|
+
// register through a store. Reproducing that source means reproducing the qualifier.
|
|
2559
|
+
//
|
|
2560
|
+
// NOT on an ordinary `&local` ARGUMENT, where no source in the corpus writes one and the
|
|
2561
|
+
// qualifier is not free. `void f(u32 i){ s32 w; w = gEnts[i].h; use(&w); four(w,w,w,w); }`
|
|
2562
|
+
// compiles to one `ldr` reloaded into four registers by copies; the structurer emits one C
|
|
2563
|
+
// read per USE rather than per machine load, so `volatile` forbids the CSE and makes it four
|
|
2564
|
+
// `ldr`s — a byte-exact candidate turned into a four-instruction nonmatch (compiled, agbcc
|
|
2565
|
+
// 2.9-arm-000512, `-O2 -mthumb-interwork -Wimplicit -fhex-asm -fprologue-bugfix`). It is free
|
|
2566
|
+
// only where the object is read at most once, which is all the rows that first shipped it
|
|
2567
|
+
// did. agbcc also warns `discards qualifiers` at every such call.
|
|
2568
|
+
//
|
|
2569
|
+
// NOT because gcc would otherwise delete the store. That claim was here for several releases
|
|
2570
|
+
// and does not reproduce: taking `&tmp` makes the local addressable, so gcc-2.9 keeps the
|
|
2571
|
+
// store with or without the qualifier, measured on store-then-escape, publish-then-fill, and
|
|
2572
|
+
// a loop that stores and escapes each iteration. What the qualifier does change is register
|
|
2573
|
+
// ALLOCATION — the same function compiled `vu16` and `u16` is 98 instructions either way and
|
|
2574
|
+
// differs in three register assignments — which is why it still has to be right. asmlift's
|
|
2575
|
+
// OWN dead-store pass used to key on it; it keys on address-taken now (l3/dce.ts), so
|
|
2576
|
+
// dropping the qualifier here cannot cost a store.
|
|
2577
|
+
//
|
|
2578
|
+
// An object whose address never leaves the function needs no volatile and must not pay it.
|
|
2579
|
+
for (const [off, ops] of objects) {
|
|
2580
|
+
const width = extent.get(off)!;
|
|
2581
|
+
const signed = accesses.get(off)!.some((a) => a.signed);
|
|
2582
|
+
for (const op of ops) {
|
|
2583
|
+
op.attrs = { ...op.attrs, width, signed, ...(published.has(off) ? { volatile: true } : {}) };
|
|
2584
|
+
}
|
|
2585
|
+
}
|
|
1178
2586
|
}
|
|
1179
|
-
return { scrutReg, caseLabels, defaultLabel };
|
|
1180
2587
|
}
|
|
1181
2588
|
|
|
1182
2589
|
/** Lift decoded asm → an L1 Fn with block-argument SSA. `prototypes` supplies each callee's
|
|
@@ -1191,7 +2598,7 @@ export function lift(
|
|
|
1191
2598
|
symbols?: SymbolMap,
|
|
1192
2599
|
): Fn {
|
|
1193
2600
|
assertInputFormat('thumb', 'gnu-as', asm);
|
|
1194
|
-
const { blocks: rawBlocks, dataWords } = decode(name, asm);
|
|
2601
|
+
const { blocks: rawBlocks, dataWords, funcLabels } = decode(name, asm);
|
|
1195
2602
|
|
|
1196
2603
|
// Regime B: recover agbcc jump tables. A dispatch block (`mov pc, rN`) plus its bounds
|
|
1197
2604
|
// predecessor (`cmp; bhi DEF`) collapse into a `switch_br` emitted from the BOUNDS block; the
|
|
@@ -1257,6 +2664,25 @@ export function lift(
|
|
|
1257
2664
|
// ends the block at it, but it has no static successor, so it must be a catchable "out of scope"
|
|
1258
2665
|
// signal, not a vanished branch. Mirrors MIPS `jr`/PPC `bctr`. (A RECOGNISED jump table's
|
|
1259
2666
|
// dispatch block is already elided above, so it is not scanned here.)
|
|
2667
|
+
//
|
|
2668
|
+
// The same rule for a `bl` whose target is inside this function's own text. agbcc relays a
|
|
2669
|
+
// conditional branch past Thumb's ±256-byte reach through an unconditional one, and past THAT
|
|
2670
|
+
// branch's own ±2 KB reach the relay becomes `bl .Lfar @far jump` — an intra-function long
|
|
2671
|
+
// branch wearing the call mnemonic. The decode switch reads it as a call, so without this the
|
|
2672
|
+
// lift emits a call to a block label (`.L3(a0, a1, a2)`): not a branch that vanished but a
|
|
2673
|
+
// transfer turned into something that RETURNS, and syntactically not C.
|
|
2674
|
+
//
|
|
2675
|
+
// The refusal is wider than that one shape because the asm does not separate it from a
|
|
2676
|
+
// locally-defined call thunk — `call_r3: bx r3`, ARMv4T's stand-in for the `blx rN` it has no
|
|
2677
|
+
// encoding for, which pokeemerald's m4a_1.s calls four times from inside MPlayMain's own slice.
|
|
2678
|
+
// Both are a `bl` to a bare label the slice defines, both sit under a conditional branch that
|
|
2679
|
+
// skips them, and both leave `lr` clobbered, so no liveness or layout fact tells them apart.
|
|
2680
|
+
// Lifting the relay as a `br` is the other half of the gap and needs the same distinction plus a
|
|
2681
|
+
// proof that `lr` is dead there, since `bl` overwrites it and a branch must not.
|
|
2682
|
+
//
|
|
2683
|
+
// DECLARED function starts are excluded: a `.thumb_func`/`thumb_func_start` label is a function
|
|
2684
|
+
// by declaration, so `bl` to one is a call however the slice came to contain it — the entry
|
|
2685
|
+
// itself (recursion), or a sibling a shared-tail slice extends through.
|
|
1260
2686
|
for (const ab of asmBlocks) {
|
|
1261
2687
|
for (const ins of ab.instrs) {
|
|
1262
2688
|
if (classifyXfer(ins) === 'indirect') {
|
|
@@ -1265,6 +2691,14 @@ export function lift(
|
|
|
1265
2691
|
`— jump tables / computed gotos / register tail calls not supported`,
|
|
1266
2692
|
);
|
|
1267
2693
|
}
|
|
2694
|
+
const callee = (ins.mnemonic === 'bl' || ins.mnemonic === 'blx') && ins.ops.length === 1 ? ins.ops[0] : undefined;
|
|
2695
|
+
if (callee !== undefined && !funcLabels.has(callee) && blockLabels.has(callee)) {
|
|
2696
|
+
throw new FrontendUnsupportedError(
|
|
2697
|
+
`cannot lift '${name}': '${ins.mnemonic} ${callee}' targets a label inside this function ` +
|
|
2698
|
+
`and not a declared function — an intra-function long branch and a locally-defined call ` +
|
|
2699
|
+
`thunk are the same shape here, so this declines rather than guessing which`,
|
|
2700
|
+
);
|
|
2701
|
+
}
|
|
1268
2702
|
}
|
|
1269
2703
|
}
|
|
1270
2704
|
|
|
@@ -1328,8 +2762,69 @@ export function lift(
|
|
|
1328
2762
|
({ labelIndex, preds } = buildCfg(asmBlocks));
|
|
1329
2763
|
}
|
|
1330
2764
|
|
|
2765
|
+
// ENTRY-REACHABLE BLOCKS. Dead code is not evidence about anything: a reload in a block that
|
|
2766
|
+
// never executes is not a read of the slot, and an instruction there is not a fact about the
|
|
2767
|
+
// frame this function actually builds. Two analyses below rest on that, so it is computed once
|
|
2768
|
+
// rather than once each — the second one was written without it and a single unreachable
|
|
2769
|
+
// `mov r0, sp; bl use` appended to a five-argument forwarder was enough to turn a loud decline
|
|
2770
|
+
// into a call with every argument dropped.
|
|
2771
|
+
const entryReachable = ((): Set<number> => {
|
|
2772
|
+
const live = new Set<number>([0]);
|
|
2773
|
+
for (let changed = true; changed;) {
|
|
2774
|
+
changed = false;
|
|
2775
|
+
for (let b = 0; b < asmBlocks.length; b++) {
|
|
2776
|
+
if (!live.has(b) && preds[b].some((q) => live.has(q))) {
|
|
2777
|
+
live.add(b);
|
|
2778
|
+
changed = true;
|
|
2779
|
+
}
|
|
2780
|
+
}
|
|
2781
|
+
}
|
|
2782
|
+
return live;
|
|
2783
|
+
})();
|
|
2784
|
+
|
|
2785
|
+
// A `scratchRegs` entry outside `nonArgRegs` is inert, and inert is how a partition rots: the
|
|
2786
|
+
// list would go on reading as if it exempted something. Refused as a target bug, like the
|
|
2787
|
+
// argRegs/uninitRegs contradiction, rather than declined as a property of the input.
|
|
2788
|
+
const scratchRegs: ReadonlySet<string> = new Set(target.scratchRegs ?? []);
|
|
2789
|
+
for (const r of scratchRegs) {
|
|
2790
|
+
if (!(target.nonArgRegs ?? []).includes(r)) {
|
|
2791
|
+
throw new Error(`target '${target.id}': scratch register ${r} is not among the non-argument registers`);
|
|
2792
|
+
}
|
|
2793
|
+
}
|
|
2794
|
+
|
|
1331
2795
|
// --- ISA-neutral SSA construction (shared Braun builder) ---
|
|
1332
|
-
|
|
2796
|
+
// THE LIVE-IN PARTITION (frontend/ssa.ts, LiveInModel). `[0, localArea)` is the whole of what this
|
|
2797
|
+
// function owns: an incoming stack argument is keyed `@sarg<k>` rather than `sp@<off>` precisely
|
|
2798
|
+
// because it sits at or above this frame, so `callerParams` is empty. `localArea` is 0 whenever
|
|
2799
|
+
// the prologue walk cannot measure the frame, and the empty range then refuses every slot —
|
|
2800
|
+
// `slotOff` applies the same bound when minting keys, so this is the independent check.
|
|
2801
|
+
//
|
|
2802
|
+
// The register half needs both of its facts, and they come from different places. The target says
|
|
2803
|
+
// which registers no caller can hand a value over in; `savedRegs` says which ones THIS function
|
|
2804
|
+
// saved, and so could have homed a local in. A register in only the first is one the ABI does not
|
|
2805
|
+
// describe — hand-written asm with a private convention, or a mid-function fragment — and it keeps
|
|
2806
|
+
// the treatment a target claiming no partition gets. The save is asked only of the registers the
|
|
2807
|
+
// ABI requires preserving: `target.scratchRegs` need none, so demanding one there would refuse a
|
|
2808
|
+
// local the compiler was entitled to put in place with no prologue at all.
|
|
2809
|
+
const ssa = makeSsaBuilder(name, asmBlocks.length, preds, () => ({
|
|
2810
|
+
ownedLocals: { from: 0, to: localArea },
|
|
2811
|
+
// THE SAME RANGE, AND NOT THE SAME CLAIM. `ownedLocals` answers "is a def-less read here an
|
|
2812
|
+
// uninitialised local?"; `declaredLocals` answers "is a spill here a DECLARATION RANK?", which
|
|
2813
|
+
// `ir/core.ts` `SlotHomes` and `l3/slotorder.ts` consume. Under agbcc's
|
|
2814
|
+
// ACCUMULATE_OUTGOING_ARGS the outgoing stack-argument area sits at the BOTTOM of `localArea`
|
|
2815
|
+
// (see the decline below), so this range would be WRONG for the second question on any
|
|
2816
|
+
// function that has one — and it is written as the same range only because `prefixStored`
|
|
2817
|
+
// declines every such function before it gets here. Lifting that decline obliges narrowing
|
|
2818
|
+
// THIS range above the argument block; the two fields exist apart so that obligation is
|
|
2819
|
+
// visible where it is created rather than inferred from a guard in another module.
|
|
2820
|
+
declaredLocals: { from: 0, to: localArea },
|
|
2821
|
+
...(target.nonArgRegs
|
|
2822
|
+
? {
|
|
2823
|
+
uninitRegs: target.nonArgRegs.filter((r) => scratchRegs.has(r) || savedRegs.has(r)),
|
|
2824
|
+
argRegs: target.argRegs,
|
|
2825
|
+
}
|
|
2826
|
+
: {}),
|
|
2827
|
+
}));
|
|
1333
2828
|
const { fn, irBlocks, readVar, writeVar, paramReg } = ssa;
|
|
1334
2829
|
|
|
1335
2830
|
const constVal = (n: number, b: number): Value => {
|
|
@@ -1337,16 +2832,6 @@ export function lift(
|
|
|
1337
2832
|
irBlocks[b].ops.push(mkOp('const', { results: [v], attrs: { value: n } }));
|
|
1338
2833
|
return v;
|
|
1339
2834
|
};
|
|
1340
|
-
const reg = (s: string) => s.replace(/[[\]]/g, '');
|
|
1341
|
-
|
|
1342
|
-
// THE one test for "is this token the stack pointer". Case-insensitive because GNU as accepts
|
|
1343
|
-
// uppercase register names, and a case-sensitive test here is a silent-wrong-answer hole rather
|
|
1344
|
-
// than a cosmetic one: `add r0, SP, #4` is `&local`, and missing it fabricates a phantom
|
|
1345
|
-
// parameter and emits confident arithmetic on it.
|
|
1346
|
-
const isSpReg = (s: string | undefined): boolean => {
|
|
1347
|
-
const r = reg(s ?? '').toLowerCase();
|
|
1348
|
-
return r === 'sp' || r === 'r13';
|
|
1349
|
-
};
|
|
1350
2835
|
|
|
1351
2836
|
// Writing sp is transparent frame bookkeeping ONLY in the one shape that cannot change anything
|
|
1352
2837
|
// observable: `sp = sp ± immediate`. Two producers feed this frontend and each emits exactly ONE
|
|
@@ -1357,8 +2842,7 @@ export function lift(
|
|
|
1357
2842
|
// disassembly (klonoa asm/ · sa3 asm/) 203 · 1250 0
|
|
1358
2843
|
//
|
|
1359
2844
|
// So this is not "one tool with two spellings" — it is the compiler's convention against the
|
|
1360
|
-
// disassembler's, and asmlift reads both kinds of file.
|
|
1361
|
-
// claimed agbcc emitted both; it does not, and the counts above are the check that settles it.)
|
|
2845
|
+
// disassembler's, and asmlift reads both kinds of file.
|
|
1362
2846
|
//
|
|
1363
2847
|
// Everything else that writes sp is a
|
|
1364
2848
|
// frame change this frontend cannot model: a register-sized adjustment (`add sp, r4`, agbcc's
|
|
@@ -1382,17 +2866,6 @@ export function lift(
|
|
|
1382
2866
|
(slotsOffReason ?? 'not a modelled slot (address-taken local / frame arithmetic / above the local area)'),
|
|
1383
2867
|
);
|
|
1384
2868
|
|
|
1385
|
-
const isFrameAdjust = (
|
|
1386
|
-
mnemonic: string,
|
|
1387
|
-
dest: string | undefined,
|
|
1388
|
-
base: string | undefined,
|
|
1389
|
-
off: string | undefined,
|
|
1390
|
-
): boolean =>
|
|
1391
|
-
(mnemonic === 'add' || mnemonic === 'sub') &&
|
|
1392
|
-
isSpReg(dest) &&
|
|
1393
|
-
(base === undefined || isSpReg(base)) &&
|
|
1394
|
-
(off?.startsWith('#') ?? false);
|
|
1395
|
-
|
|
1396
2869
|
// Reading sp as a DATA operand means an address-taken local (`add rD, sp, #N` = `&local`),
|
|
1397
2870
|
// an sp-relative spill slot (`ldr/str …, [sp, #N]`), or frame-pointer arithmetic — none
|
|
1398
2871
|
// modellable without a stack abstraction. Without this guard Braun SSA would materialize sp as a
|
|
@@ -1400,9 +2873,10 @@ export function lift(
|
|
|
1400
2873
|
// (`isStackPtr`) and PPC (`r1`).
|
|
1401
2874
|
//
|
|
1402
2875
|
// sp is never WRITTEN either — but by `writeData` declining, NOT because sp-dest ops are inert.
|
|
1403
|
-
//
|
|
1404
|
-
//
|
|
1405
|
-
// whitelisted in the add/sub arms.
|
|
2876
|
+
// Five decode arms (`lsl`/`neg`/`mvn` with an sp destination, `ldr sp`, `ldmia` with sp in the
|
|
2877
|
+
// list) reach a write with no arm-local sp check of their own, so the guard has to sit on the
|
|
2878
|
+
// write itself. The single transparent shape is `sp = sp ± imm`, whitelisted in the add/sub arms.
|
|
2879
|
+
// Read and write are guarded symmetrically.
|
|
1406
2880
|
const readData = (r: string, b: number): Value => {
|
|
1407
2881
|
if (isSpReg(r)) {
|
|
1408
2882
|
throw spAsDataError();
|
|
@@ -1422,154 +2896,6 @@ export function lift(
|
|
|
1422
2896
|
return readVar(r, b);
|
|
1423
2897
|
};
|
|
1424
2898
|
|
|
1425
|
-
// A virtual register key per incoming stack argument. Reading it goes through the ordinary Braun
|
|
1426
|
-
// live-in path (frontend/ssa.ts), which turns a read with no reaching def into a function
|
|
1427
|
-
// parameter — so this needs NO new representation, opcode or pass. The `@` cannot appear in a
|
|
1428
|
-
// real register token, so the key cannot collide with one.
|
|
1429
|
-
const stackArgKey = (index: number) => `@sarg${index}`;
|
|
1430
|
-
// Defined beside its mint site on purpose: the format string and its parser drifted 650 lines
|
|
1431
|
-
// apart in the first version, with the convention explained only at one end.
|
|
1432
|
-
const stackArgIndex = (key: string): number | null => {
|
|
1433
|
-
const m = /^@sarg(\d+)$/.exec(key);
|
|
1434
|
-
return m ? +m[1] : null;
|
|
1435
|
-
};
|
|
1436
|
-
|
|
1437
|
-
// INCOMING STACK ARGUMENTS (AAPCS). Args 1-4 arrive in r0-r3; args 5+ are pushed by the CALLER,
|
|
1438
|
-
// so the callee reads them at `[sp, #N]` where N is at or above its own frame. Those are
|
|
1439
|
-
// PARAMETERS, not locals — declining them as "sp used as data" refuses a calling convention.
|
|
1440
|
-
//
|
|
1441
|
-
// The frame depth is tracked by a linear walk of the ENTRY BLOCK only, which is
|
|
1442
|
-
// why the gate is what it is: within one straight-line block the depth at each instruction is
|
|
1443
|
-
// exact and needs no CFG reasoning. Every case that would need more declines.
|
|
1444
|
-
//
|
|
1445
|
-
// `push {a,b,c}` deepens by 4 per register; `sub sp, #N` and `add sp, sp, #-N` deepen by N.
|
|
1446
|
-
//
|
|
1447
|
-
// The walk, its two invariants and the predicate that reads them are ONE object on purpose. They
|
|
1448
|
-
// were three loose pieces — a delta function, a pair of `let`s updated in the instruction loop,
|
|
1449
|
-
// and a seven-parameter predicate taking the pair by value — and both bugs the second review pass
|
|
1450
|
-
// found lived in the seams: an invariant the predicate's proof needed but only the loop could
|
|
1451
|
-
// enforce, and a `0` returned for an unrecognised shape that only a guard in a third place made
|
|
1452
|
-
// safe. Anything that changes how sp moves now has one place to change, and the proof it has to
|
|
1453
|
-
// preserve is written next to the state it is about.
|
|
1454
|
-
const makeFrameWalk = () => {
|
|
1455
|
-
// Bytes the frame has grown since function entry, along this block's linear order only.
|
|
1456
|
-
let depth = 0;
|
|
1457
|
-
let depthKnown = true;
|
|
1458
|
-
|
|
1459
|
-
// Returns null whenever the depth cannot be computed exactly — including for any write to sp
|
|
1460
|
-
// this does not model. The capability rests on the depth being EXACT, so an approximation is
|
|
1461
|
-
// never acceptable: understate the frame and a local sits above the computed top and gets
|
|
1462
|
-
// minted as a parameter reading uninitialised stack. A null poisons the depth for the rest of
|
|
1463
|
-
// the block, which disables argument recovery and leaves every `[sp,#N]` to decline as before.
|
|
1464
|
-
// Nothing here may return a NUMBER for a shape it merely failed to recognise.
|
|
1465
|
-
const delta = (ins: { mnemonic: string; ops: string[] }): number | null => {
|
|
1466
|
-
const m = ins.mnemonic;
|
|
1467
|
-
if (m === 'push' || m === 'pop') {
|
|
1468
|
-
// expandRegList, NOT a comma count: `push {r4-r7, lr}` is FIVE registers, and counting it as
|
|
1469
|
-
// two makes the frame 12 bytes too shallow — which turns a genuine LOCAL into a fabricated
|
|
1470
|
-
// parameter reading uninitialised stack. Caught by a probe, not by the corpus: agbcc emits no
|
|
1471
|
-
// range pushes and 0 of the 743 benchmark rows contain one, but GNU as accepts them and the
|
|
1472
|
-
// disassembly path can produce them.
|
|
1473
|
-
// Counting comma tokens instead would undercount `{r4-lr}` as two registers, and an
|
|
1474
|
-
// unexpandable range must poison the depth rather than be guessed at — definiteRegList owns
|
|
1475
|
-
// both rules, and owns them for the ldm/stm arm too.
|
|
1476
|
-
const list = definiteRegList(
|
|
1477
|
-
ins.ops
|
|
1478
|
-
.join(',')
|
|
1479
|
-
.replace(/[{}]/g, '')
|
|
1480
|
-
.split(',')
|
|
1481
|
-
.map((r) => r.trim())
|
|
1482
|
-
.filter(Boolean),
|
|
1483
|
-
);
|
|
1484
|
-
if (list === null) {
|
|
1485
|
-
return null;
|
|
1486
|
-
}
|
|
1487
|
-
return (m === 'push' ? 1 : -1) * 4 * list.length;
|
|
1488
|
-
}
|
|
1489
|
-
if ((m === 'add' || m === 'sub') && isSpReg(ins.ops[0])) {
|
|
1490
|
-
const off = ins.ops[2] ?? ins.ops[1];
|
|
1491
|
-
if (off?.startsWith('#')) {
|
|
1492
|
-
const v = imm(off);
|
|
1493
|
-
return m === 'sub' ? v : -v;
|
|
1494
|
-
}
|
|
1495
|
-
}
|
|
1496
|
-
// Any OTHER write to sp poisons the depth. This used to fall through to 0 — "no change" — for
|
|
1497
|
-
// shapes it does not model (`add sp, r4`, `mov sp, rN`, `add sp, r0, #4`), which was safe only
|
|
1498
|
-
// because writeData declines every one of them elsewhere. That is the same
|
|
1499
|
-
// enumeration-of-arms mistake writeData itself exists to end, exported one function away: a
|
|
1500
|
-
// number meaning "no change" is the wrong answer to "I do not understand this". Now the walk is
|
|
1501
|
-
// self-sufficient — unknown ⇒ depth poisoned ⇒ decline — and writeData's refusal is an
|
|
1502
|
-
// independent second guarantee instead of a load-bearing one.
|
|
1503
|
-
if (isSpReg(ins.ops[0])) {
|
|
1504
|
-
return null;
|
|
1505
|
-
}
|
|
1506
|
-
return 0;
|
|
1507
|
-
};
|
|
1508
|
-
|
|
1509
|
-
return {
|
|
1510
|
-
// Advance the walk over one instruction. Call for EVERY instruction, in order.
|
|
1511
|
-
step(ins: { mnemonic: string; ops: string[] }): void {
|
|
1512
|
-
const d = delta(ins);
|
|
1513
|
-
if (d === null) {
|
|
1514
|
-
depthKnown = false;
|
|
1515
|
-
} else {
|
|
1516
|
-
depth += d;
|
|
1517
|
-
}
|
|
1518
|
-
// sp ABOVE the incoming sp poisons the walk for the rest of the block, permanently — the
|
|
1519
|
-
// premise argIndex's proof rests on. See the proof there for what it costs to omit.
|
|
1520
|
-
if (depth < 0) {
|
|
1521
|
-
depthKnown = false;
|
|
1522
|
-
}
|
|
1523
|
-
},
|
|
1524
|
-
|
|
1525
|
-
// Is `[sp, #off]` an incoming stack argument, and which one? `null` means NO — and every null is
|
|
1526
|
-
// a DECLINE, because the caller falls through to readData's sp guard.
|
|
1527
|
-
//
|
|
1528
|
-
// Why a slot at or above the frame top cannot have been written by this function, which is the
|
|
1529
|
-
// whole soundness argument: every sp-relative STORE declines (readData, via the str arm), and a
|
|
1530
|
-
// `push` only ever writes strictly BELOW the current top. An argument slot is at or above the
|
|
1531
|
-
// incoming sp, so no instruction here can have defined it — the value can only be the caller's.
|
|
1532
|
-
//
|
|
1533
|
-
// That second step needs sp to have stayed at or below where it came in — `depth >= 0` at every
|
|
1534
|
-
// point of the walk — or a `push` reaches back up over the argument area and the conclusion is
|
|
1535
|
-
// false:
|
|
1536
|
-
//
|
|
1537
|
-
// add sp, sp, #8 ; sp = S+8
|
|
1538
|
-
// push {r4, r5, r6} ; sp = S-4, and this WROTE r5 to S+0
|
|
1539
|
-
// ldr r0, [sp, #4] ; = S+0 — r5's slot, not the caller's argument
|
|
1540
|
-
//
|
|
1541
|
-
// The depth is back to a plausible +4 by the load, so nothing downstream can tell: it emitted
|
|
1542
|
-
// `s32 f(s32 a0, …, s32 a4) { return a4; }`, the function's own incoming r5 handed back as
|
|
1543
|
-
// argument 5. `pop {r4}; push {r4,r5}` gets there without an `add sp` at all, and a sliced
|
|
1544
|
-
// fragment whose prologue was cut off is exactly this shape — which is the corpus this frontend
|
|
1545
|
-
// reads. `step` enforces it, and never un-poisons: once sp has been above the line, a push during
|
|
1546
|
-
// the excursion may have written the later slots too. This is a PREMISE, not a detail — leaving
|
|
1547
|
-
// it unstated is what let the first version hand back a callee-saved register as an argument.
|
|
1548
|
-
argIndex(addr: { base: string; off: number; regOff?: string }, width: number, bi: number): number | null {
|
|
1549
|
-
const { base, off, regOff } = addr;
|
|
1550
|
-
if (!isSpReg(base) || regOff !== undefined) {
|
|
1551
|
-
return null; // not sp, or `[sp, rX]` — not a fixed argument slot
|
|
1552
|
-
}
|
|
1553
|
-
if (bi !== 0 || preds[0].length > 0) {
|
|
1554
|
-
return null; // depth is exact only along the ENTRY block's linear order, and only when its
|
|
1555
|
-
// params are parameters rather than phis
|
|
1556
|
-
}
|
|
1557
|
-
if (!depthKnown || depth <= 0) {
|
|
1558
|
-
return null; // an unmeasurable frame, or none established: a headerless FRAGMENT whose
|
|
1559
|
-
// prologue was sliced off looks identical to a frameless function, and there the slots are
|
|
1560
|
-
// locals — minting one would be the silent-wrong trade this frontend refuses
|
|
1561
|
-
}
|
|
1562
|
-
if (width !== 4 || off < depth || (off - depth) % 4 !== 0) {
|
|
1563
|
-
return null; // the argument area is word-granular; BELOW the top is a local, which is the
|
|
1564
|
-
// separate slot-promotion capability
|
|
1565
|
-
}
|
|
1566
|
-
const index = target.argRegs.length + (off - depth) / 4;
|
|
1567
|
-
return index < MAX_RECOVERED_ARITY ? index : null; // a wild offset must not mint a
|
|
1568
|
-
// 400-parameter signature
|
|
1569
|
-
},
|
|
1570
|
-
};
|
|
1571
|
-
};
|
|
1572
|
-
|
|
1573
2899
|
// LOCAL STACK SLOTS. A spill or a local that never has its address taken is transparent to
|
|
1574
2900
|
// dataflow: `str rX,[sp,#k]` … `ldr rY,[sp,#k]` moves a value, it does not touch memory anyone
|
|
1575
2901
|
// else can see. Modelling it as an SSA variable keyed by the offset (the same `sp@<off>` spelling
|
|
@@ -1580,29 +2906,76 @@ export function lift(
|
|
|
1580
2906
|
// the same value at every access, and MIPS gets that for free (IDO establishes sp with one
|
|
1581
2907
|
// `addiu` and never moves it). Thumb's `push` moves sp, so constancy has to be PROVEN here.
|
|
1582
2908
|
const slotKey = stackSlotKey; // shared spelling: frontend/ssa.ts
|
|
1583
|
-
|
|
1584
|
-
//
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
//
|
|
1588
|
-
//
|
|
1589
|
-
//
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
2909
|
+
|
|
2910
|
+
// THE FRAME BASE PASSED TO A CALLEE. What this computes is exactly what its name says and nothing
|
|
2911
|
+
// more: somewhere in entry-reachable code a bare `mov rD, sp` copies the frame base into a
|
|
2912
|
+
// register that is still an ARGUMENT register at a `bl`. It is a fact about a REGISTER, not about
|
|
2913
|
+
// the frame's layout — an earlier cut of this treated it as proof that "[sp,#0] is an addressable
|
|
2914
|
+
// local", and agbcc falsified that outright. The layout question is settled by the gate this
|
|
2915
|
+
// feeds (`capturedObjectIsTheWholeFrame`, below `localArea`), not here.
|
|
2916
|
+
//
|
|
2917
|
+
// ENTRY-REACHABLE, BLOCK-LOCAL AND KILL-ON-MENTION, because this feeds an ACCEPTANCE and so may
|
|
2918
|
+
// never over-approximate. Unreachable blocks are skipped for the same reason (a)'s reload scan
|
|
2919
|
+
// skips them — an instruction that never executes is not a fact about the frame, and one appended
|
|
2920
|
+
// `mov r0, sp; bl use` after the return was enough to license a whole function. A block is
|
|
2921
|
+
// straight-line, so a capture that is still held when the `bl` is decoded is held on every
|
|
2922
|
+
// execution that reaches it; and a register is dropped the moment ANY other instruction so much
|
|
2923
|
+
// as MENTIONS it, since a write cannot happen without the token appearing. That over-kills (a
|
|
2924
|
+
// `cmp` on the register between the capture and the call ends it) and over-killing only costs a
|
|
2925
|
+
// decline. ARGUMENT registers only: the frame base merely live across a call is not evidence that
|
|
2926
|
+
// it was passed to one, and for `blx rN` the TARGET register is not an argument either.
|
|
2927
|
+
const frameBasePassedToCallee = ((): boolean => {
|
|
2928
|
+
for (const b of entryReachable) {
|
|
2929
|
+
const ab = asmBlocks[b];
|
|
2930
|
+
const held = new Set<string>();
|
|
2931
|
+
for (const ins of ab.instrs) {
|
|
2932
|
+
if (ins.mnemonic === 'bl' || ins.mnemonic === 'blx') {
|
|
2933
|
+
// `blx rN` names its TARGET in the operand slot, so exclude it: `mov r3, sp; blx r3`
|
|
2934
|
+
// branches THROUGH the frame base, it does not pass it.
|
|
2935
|
+
const targetReg = ins.mnemonic === 'blx' ? reg(ins.ops[0] ?? '') : null;
|
|
2936
|
+
if ([...held].some((r) => target.argRegs.includes(r) && r !== targetReg)) {
|
|
2937
|
+
return true;
|
|
2938
|
+
}
|
|
2939
|
+
held.clear(); // the callee clobbers the argument registers
|
|
2940
|
+
continue;
|
|
2941
|
+
}
|
|
2942
|
+
// The two shapes that can carry the base forward: the capture itself, and a bare register
|
|
2943
|
+
// copy of a value already held. Everything else only kills.
|
|
2944
|
+
const carried = capturesSp(ins)
|
|
2945
|
+
? reg(ins.ops[0] ?? '')
|
|
2946
|
+
: /^movs?$/.test(ins.mnemonic) && held.has(reg(ins.ops[1] ?? ''))
|
|
2947
|
+
? reg(ins.ops[0] ?? '')
|
|
2948
|
+
: null;
|
|
2949
|
+
// The OPERAND TOKENS, not the mnemonic: `asWritten` carries only the normalised mnemonic,
|
|
2950
|
+
// so the operands are the only place a written register can appear — and they are
|
|
2951
|
+
// RANGE-EXPANDED first, because a range spells none of the registers it writes. `pop
|
|
2952
|
+
// {r0-r3}` writes r2 with the string `r2` nowhere in the instruction, so the bare `\bR\b`
|
|
2953
|
+
// test let a dead capture survive the `pop` that destroyed it, the acceptance fired on a
|
|
2954
|
+
// frame that really did stage an outgoing argument, and the lift dropped all five of that
|
|
2955
|
+
// call's arguments. Two spellings of one instruction must not give two verdicts, and CASE
|
|
2956
|
+
// is a third spelling of the same one — `expandRegList` is lowercase-only, so `{R0-R3}`
|
|
2957
|
+
// leaked through the expansion exactly as the range leaked through the regex.
|
|
2958
|
+
const mentions = expandRegList(
|
|
2959
|
+
ins.ops
|
|
2960
|
+
.join(' ')
|
|
2961
|
+
.toLowerCase()
|
|
2962
|
+
.replace(/[[\]{}!#]/g, ' ')
|
|
2963
|
+
.split(/[,\s]+/)
|
|
2964
|
+
.filter(Boolean),
|
|
2965
|
+
);
|
|
2966
|
+
for (const r of [...held]) {
|
|
2967
|
+
if (mentions.includes(r) || ins.ops.some((o) => new RegExp(`\\b${r}\\b`, 'i').test(o))) {
|
|
2968
|
+
held.delete(r);
|
|
2969
|
+
}
|
|
2970
|
+
}
|
|
2971
|
+
if (carried !== null) {
|
|
2972
|
+
held.add(carried);
|
|
2973
|
+
}
|
|
2974
|
+
}
|
|
1600
2975
|
}
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
: null;
|
|
1605
|
-
};
|
|
2976
|
+
return false;
|
|
2977
|
+
})();
|
|
2978
|
+
|
|
1606
2979
|
// Is the word-slot model safe for THIS function? Every disqualifier below leaves every `[sp,#k]`
|
|
1607
2980
|
// access on the old path, which declines — so the answer to "not sure" is the loud one.
|
|
1608
2981
|
// Returns null when the word-slot model is SAFE for this function, else the reason it is off —
|
|
@@ -1699,7 +3072,9 @@ export function lift(
|
|
|
1699
3072
|
//
|
|
1700
3073
|
// Measured, this costs nothing it was buying: forcing the old acceptance path off changed 0
|
|
1701
3074
|
// lift/decline verdicts across 2686 corpus functions (sa3's vendored map carries no signatures
|
|
1702
|
-
// at all), so the path that carried those holes was never load-bearing.
|
|
3075
|
+
// at all), so the path that carried those holes was never load-bearing. That sweep was run over
|
|
3076
|
+
// the sa3/klonoa CHECKOUTS, which this repo does not vendor — it is not the benchmark's 404
|
|
3077
|
+
// agbcc rows, and it has not been re-run since it was taken.
|
|
1703
3078
|
for (const ab of asmBlocks) {
|
|
1704
3079
|
for (const ins of ab.instrs) {
|
|
1705
3080
|
if (ins.mnemonic !== 'bl' && ins.mnemonic !== 'blx') {
|
|
@@ -1725,6 +3100,18 @@ export function lift(
|
|
|
1725
3100
|
// tail-merged call site breaks, and agbcc DOES tail-merge: `Task_BonusFlower_Spawn` (sa3
|
|
1726
3101
|
// bonus_game_enemies) stores argument 5 in both predecessors with the `bl` in the join.
|
|
1727
3102
|
//
|
|
3103
|
+
// A SECOND THING NOW DEPENDS ON THIS DECLINE, and it is not in this file. `SlotHomes`
|
|
3104
|
+
// (ir/core.ts) reads a `[sp,#k]` spill as a DECLARATION RANK and `l3/slotorder.ts` orders the
|
|
3105
|
+
// declaration list by it. The partition that admits an offset is `LiveInModel.declaredLocals`,
|
|
3106
|
+
// which this frontend sets to `[0, localArea)` — a range that CONTAINS the outgoing area. The
|
|
3107
|
+
// only thing keeping an argument slot from becoming a declaration rank is that this decline
|
|
3108
|
+
// removes such functions from the population first. So lifting or narrowing this guard is not
|
|
3109
|
+
// a local change: it must come with a narrowed `declaredLocals`, or the ordering starts
|
|
3110
|
+
// ranking argument positions with no diagnostic. The class is populated — of 2,001 lifted real
|
|
3111
|
+
// agbcc functions, 12 carry an L1 slot home AND call, and 11 of those home offset 0. "Real
|
|
3112
|
+
// agbcc functions" there means a CHECKOUT sweep this repo does not vendor, not benchmark rows
|
|
3113
|
+
// (the benchmark has 126 real agbcc rows in total), and it has not been re-run since.
|
|
3114
|
+
//
|
|
1728
3115
|
// (b) is a forward may-analysis over the CFG, and it has been wrong twice in the other
|
|
1729
3116
|
// direction. Scanning per block let a LABEL decide accept versus refuse; scanning the flat
|
|
1730
3117
|
// listing let BLOCK ORDER decide, because a load in one arm of a branch cleared a store that
|
|
@@ -1734,28 +3121,37 @@ export function lift(
|
|
|
1734
3121
|
// Only for a function that CALLS. With no call there is no outgoing area to mistake a local
|
|
1735
3122
|
// for, and a never-reloaded store there is an ordinary dead local — which PR #30 modelled and
|
|
1736
3123
|
// which must keep working.
|
|
1737
|
-
|
|
3124
|
+
//
|
|
3125
|
+
// …and not when the WHOLE FRAME is an object whose address a callee holds
|
|
3126
|
+
// (`capturedObjectIsTheWholeFrame` — a one-word frame, passed by a bare `mov rD, sp`). Both
|
|
3127
|
+
// conditions hunt for an argument block; every argument block starts at [sp,#0] (that is what
|
|
3128
|
+
// `prefixStored` encodes); and a one-word frame that is entirely an addressable local has no
|
|
3129
|
+
// room for one. So there is no argument block to find and both conditions can only fire as
|
|
3130
|
+
// FALSE ALARMS — which is what they did: the three address-taken rows in the synthetic tier
|
|
3131
|
+
// declined at (a) on a store that is never reloaded for the ordinary reason, that the CALLEE
|
|
3132
|
+
// reads it through the pointer.
|
|
3133
|
+
//
|
|
3134
|
+
// This is the only acceptance in this function, so keep straight what licenses it. NOT arity: a
|
|
3135
|
+
// callee declared with four arguments proves nothing (see above), and the layout gate is
|
|
3136
|
+
// deliberately independent of the declarations. The arity refusal still runs FIRST and still
|
|
3137
|
+
// wins — in a one-word frame a declared fifth argument and an addressable local at offset 0 are
|
|
3138
|
+
// contradictory claims about the same word, so the honest answer there is the decline, not a
|
|
3139
|
+
// guess about which one to believe.
|
|
3140
|
+
if (
|
|
3141
|
+
!capturedObjectIsTheWholeFrame &&
|
|
3142
|
+
asmBlocks.some((ab) => ab.instrs.some((i) => i.mnemonic === 'bl' || i.mnemonic === 'blx'))
|
|
3143
|
+
) {
|
|
1738
3144
|
const slotAcc = (ins: Instr) => {
|
|
1739
3145
|
const a = spMemAccess(ins);
|
|
1740
|
-
return a && !a.regOff && a.width === 4 && a.off % 4 === 0 && a.off >= 0 && a.off
|
|
3146
|
+
return a && !a.regOff && a.width === 4 && a.off % 4 === 0 && a.off >= 0 && a.off + 4 <= localArea
|
|
3147
|
+
? a.off
|
|
3148
|
+
: null;
|
|
1741
3149
|
};
|
|
1742
3150
|
const isStore = (ins: Instr) => /^str/.test(ins.mnemonic);
|
|
1743
3151
|
// Entry-REACHABLE blocks only: a reload in dead code is not evidence that live code reads the
|
|
1744
3152
|
// slot back, and counting it lets an argument store satisfy (a) on the strength of an
|
|
1745
3153
|
// instruction that never executes.
|
|
1746
|
-
const live =
|
|
1747
|
-
for (let changed = true; changed;) {
|
|
1748
|
-
changed = false;
|
|
1749
|
-
for (let b = 0; b < asmBlocks.length; b++) {
|
|
1750
|
-
if (live.has(b)) {
|
|
1751
|
-
continue;
|
|
1752
|
-
}
|
|
1753
|
-
if (preds[b].some((q) => live.has(q))) {
|
|
1754
|
-
live.add(b);
|
|
1755
|
-
changed = true;
|
|
1756
|
-
}
|
|
1757
|
-
}
|
|
1758
|
-
}
|
|
3154
|
+
const live = entryReachable;
|
|
1759
3155
|
const reloaded = new Set<number>();
|
|
1760
3156
|
for (const b of live) {
|
|
1761
3157
|
for (const ins of asmBlocks[b].instrs) {
|
|
@@ -1877,30 +3273,20 @@ export function lift(
|
|
|
1877
3273
|
}
|
|
1878
3274
|
return null;
|
|
1879
3275
|
};
|
|
1880
|
-
//
|
|
1881
|
-
//
|
|
1882
|
-
//
|
|
1883
|
-
//
|
|
1884
|
-
//
|
|
1885
|
-
//
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
if (o === undefined || !o.startsWith('#')) {
|
|
1892
|
-
return null;
|
|
1893
|
-
}
|
|
1894
|
-
const v = imm(o);
|
|
1895
|
-
return ins.mnemonic === 'sub' ? -v : v;
|
|
1896
|
-
};
|
|
1897
|
-
// The EXPLICITLY reserved local area — the prologue's `add sp,sp,#-N`. A slot must live strictly
|
|
1898
|
-
// inside this, not merely inside the whole frame: the rest of the frame is the callee-saved block
|
|
1899
|
-
// the entry `push` wrote, which belongs to the epilogue's `pop`, so a `str` there is retargeted
|
|
1900
|
-
// away from the memory the pop will read.
|
|
3276
|
+
// THE EXPLICITLY RESERVED LOCAL AREA — the prologue's `add sp,sp,#-N`, and the frame the body
|
|
3277
|
+
// sees. "Prologue" is everything before the entry block first touches the frame (or the whole
|
|
3278
|
+
// block if it never does). A frame this cannot measure yields 0, which disables every slot
|
|
3279
|
+
// (`off + 4 <= localArea` is then false).
|
|
3280
|
+
//
|
|
3281
|
+
// A slot must live strictly inside this, not merely inside the whole frame: the rest of the frame
|
|
3282
|
+
// is the callee-saved block the entry `push` wrote, which belongs to the epilogue's `pop`, so a
|
|
3283
|
+
// `str` there is retargeted away from the memory the pop will read. Which is also why this is NOT
|
|
3284
|
+
// the walk `argIndex` uses — `makeFrameWalk`'s `delta` counts a `push` at 4 bytes per register and
|
|
3285
|
+
// this skips it, because the callee-saved block sits ABOVE the local area. The cost of that
|
|
3286
|
+
// difference is the push/pop arm below.
|
|
1901
3287
|
const localArea = ((): number => {
|
|
1902
3288
|
// The PROLOGUE only — everything before the entry block first touches the frame. Summing the
|
|
1903
|
-
// whole block instead let a store made BEFORE the reservation fall inside
|
|
3289
|
+
// whole block instead let a store made BEFORE the reservation fall inside the window, so
|
|
1904
3290
|
// `str r0,[sp]; add sp,sp,#-4; …` claimed a write to the CALLER's frame as a private local and
|
|
1905
3291
|
// deleted it.
|
|
1906
3292
|
//
|
|
@@ -1911,35 +3297,230 @@ export function lift(
|
|
|
1911
3297
|
// store and rendered a computed `bx` as an ordinary return, and it fooled the pop gate too
|
|
1912
3298
|
// (`released` is compared against this number). Not corpus-reachable — 0 of 2805 Thumb
|
|
1913
3299
|
// functions adjust sp upward before their first frame access — which is exactly why only a
|
|
1914
|
-
// probe finds it.
|
|
3300
|
+
// probe finds it. That 2805 is a CHECKOUT sweep this repo does not vendor, not a benchmark
|
|
3301
|
+
// count, and it has not been re-run since.
|
|
1915
3302
|
//
|
|
1916
3303
|
// An sp write this cannot read poisons the whole thing to 0, disabling every slot, rather than
|
|
1917
|
-
// being skipped as if it were no movement: the same rule
|
|
3304
|
+
// being skipped as if it were no movement: the same rule `makeFrameWalk`'s `delta` follows.
|
|
1918
3305
|
const ins = asmBlocks[0].instrs;
|
|
1919
3306
|
const firstMem = ins.findIndex((x) => touchesFrame(x));
|
|
1920
3307
|
let reserved = 0;
|
|
3308
|
+
let reservedYet = false;
|
|
1921
3309
|
for (const x of ins.slice(0, firstMem === -1 ? ins.length : firstMem)) {
|
|
1922
3310
|
const d = spAdjust(x);
|
|
1923
3311
|
if (d !== null) {
|
|
1924
3312
|
reserved -= d; // d > 0 = sp rises = the frame shrinks
|
|
1925
|
-
|
|
3313
|
+
reservedYet ||= reserved > 0;
|
|
3314
|
+
} else if (x.mnemonic === 'push' || x.mnemonic === 'pop') {
|
|
3315
|
+
// BEFORE any reservation this is the callee-saved block, measured from below — the
|
|
3316
|
+
// ordinary agbcc prologue, and why push/pop are skipped at all. AFTER one it slides sp
|
|
3317
|
+
// under the window just measured, so `[0, localArea)` stops naming the reserved area and
|
|
3318
|
+
// starts naming the pushed words. Those words ARE written, by an instruction that is
|
|
3319
|
+
// dataflow-transparent, so nothing downstream can tell:
|
|
3320
|
+
// `push {r4,lr}; add sp,#-8; push {r0}; … ldr r1,[sp]` reads back the pushed `r0` on every
|
|
3321
|
+
// path and the def-less shape renders it as an uninitialised local — 0 from the machine,
|
|
3322
|
+
// garbage from the C. Poisoned to 0 rather than corrected by subtracting the push bytes,
|
|
3323
|
+
// the same rule the sp-write arm follows. Not corpus-reachable — agbcc pushes before it
|
|
3324
|
+
// reserves in all 2343 Thumb functions of the same un-vendored CHECKOUT sweep as the 2805
|
|
3325
|
+
// above, not re-run since — which is why only a probe finds it.
|
|
3326
|
+
if (reservedYet) {
|
|
3327
|
+
return 0;
|
|
3328
|
+
}
|
|
3329
|
+
} else if (isSpReg((x.ops[0] ?? '').replace(/!$/, ''))) {
|
|
1926
3330
|
return 0; // an sp write of a shape this does not model
|
|
1927
3331
|
}
|
|
1928
3332
|
}
|
|
1929
3333
|
return Math.max(0, reserved);
|
|
1930
3334
|
})();
|
|
1931
3335
|
|
|
3336
|
+
// WHAT THE PROLOGUE SAVED — the second half of the register partition (frontend/ssa.ts,
|
|
3337
|
+
// LiveInModel.uninitRegs), in operand spellings. "The ABI does not pass arguments here" is a fact
|
|
3338
|
+
// about the CALLER and cannot on its own make a def-less read an uninitialised local; what does is
|
|
3339
|
+
// that the compiler homed a local in the register, which it may only do after saving it. Asm that
|
|
3340
|
+
// saves nothing follows no such convention, and some of it really is handed live values there:
|
|
3341
|
+
// the MP2K engine's hand-written `ChnVolSetAsm`, vendored in klonoa, sa3 and pokeemerald alike,
|
|
3342
|
+
// takes two pointers in r4/r5 and has no prologue at all — classified by the ABI alone it lost its
|
|
3343
|
+
// signature and stored through reads of registers nothing ever wrote.
|
|
3344
|
+
//
|
|
3345
|
+
// PER REGISTER, because saving r5 says nothing about r4 — a mid-function fragment reached by
|
|
3346
|
+
// agbcc's `bl`-as-a-long-branch saves what it uses and is handed the rest.
|
|
3347
|
+
//
|
|
3348
|
+
// The prologue is the LEADING run of saves and reservations, so a `push` in the body cannot join
|
|
3349
|
+
// the set. `HIGH_REGS` have no `push` encoding, so agbcc saves them as `mov rLow, rHi; push
|
|
3350
|
+
// {rLow}` and the source of such a `mov` joins the set when its low register is pushed — every
|
|
3351
|
+
// high-register inhabitant in the corpus goes through that idiom.
|
|
3352
|
+
const savedRegs = ((): ReadonlySet<string> => {
|
|
3353
|
+
const saved = new Set<string>();
|
|
3354
|
+
const carries = new Map<string, string>(); // low register ← the high one moved into it
|
|
3355
|
+
for (const x of asmBlocks[0].instrs) {
|
|
3356
|
+
if (x.mnemonic === 'push') {
|
|
3357
|
+
const list = regListOf(x.ops);
|
|
3358
|
+
if (list === null) {
|
|
3359
|
+
break; // a list this cannot read is a save set this cannot vouch for
|
|
3360
|
+
}
|
|
3361
|
+
for (const r of list) {
|
|
3362
|
+
saved.add(r);
|
|
3363
|
+
const hi = carries.get(r);
|
|
3364
|
+
if (hi !== undefined) {
|
|
3365
|
+
saved.add(hi);
|
|
3366
|
+
}
|
|
3367
|
+
}
|
|
3368
|
+
} else if (/^movs?$/.test(x.mnemonic) && HIGH_REGS.has(x.ops[1] ?? '') && !HIGH_REGS.has(x.ops[0] ?? '')) {
|
|
3369
|
+
carries.set(x.ops[0], x.ops[1]);
|
|
3370
|
+
} else if (spAdjust(x) === null) {
|
|
3371
|
+
break;
|
|
3372
|
+
}
|
|
3373
|
+
}
|
|
3374
|
+
return saved;
|
|
3375
|
+
})();
|
|
3376
|
+
|
|
3377
|
+
// …AND THE FRAME IS THAT OBJECT. `frameBasePassedToCallee` is a fact about a register; this is
|
|
3378
|
+
// the layout fact, and it is the one that licenses turning the outgoing-argument refusals off.
|
|
3379
|
+
//
|
|
3380
|
+
// The reason it is needed at all: a bare `mov rD, sp` names frame offset 0, but agbcc emits one
|
|
3381
|
+
// for TWO different things, and only one of them is an addressable local. The other is a
|
|
3382
|
+
// BLOCK-COPY BASE — the destination of a by-value struct argument (which IS the outgoing argument
|
|
3383
|
+
// area) and the hidden return pointer of a struct-returning call. Both compiled, agbcc
|
|
3384
|
+
// 2.9-arm-000512, `-O2 -mthumb-interwork -Wimplicit -fhex-asm -fprologue-bugfix`:
|
|
3385
|
+
//
|
|
3386
|
+
// by-value struct argument, `void f(struct S *p){ take(*p); }`, by stack words beyond r0-r3:
|
|
3387
|
+
// 1 word → `ldr r0,[r3,#0x10]; str r0,[sp]` — no capture at all, frame 4
|
|
3388
|
+
// 2 words → `mov r1, sp; ldmia r0!,{…}; stmia r1!,{…}` — frame 8
|
|
3389
|
+
// 13+ → `mov r0, sp; mov r2,#0x44; bl memcpy` — frame 0x44 and up
|
|
3390
|
+
// struct return, `void f(int x){ struct R r = mk(x); use2(r.a[0]); }`:
|
|
3391
|
+
// 1 word, INTEGER-LIKE (`{int a;}`, `{int a[1];}`, `{float f;}`) → r0, no frame temp
|
|
3392
|
+
// 1 word, otherwise (`{char a,b,c,d;}`, `{short a,b;}`) → `mov r0, sp; bl mk` — frame 4
|
|
3393
|
+
// 2 words → `mov r0, sp; bl mk` — frame 8
|
|
3394
|
+
//
|
|
3395
|
+
// Left unguarded that cost an argument: `void g3(struct Huge *p, int x){ takesH(*p);
|
|
3396
|
+
// five(1,2,3,4,x); }` puts `str r5,[sp]` — argument 5 of `five` — in a frame whose offset 0 the
|
|
3397
|
+
// memcpy also names, and the lift emitted `five(1, 2, 3, 4)` with the fifth argument written into
|
|
3398
|
+
// a fabricated 4-byte local, in a frame declared 4 bytes where the machine reserves 0x90. Order
|
|
3399
|
+
// is not the discriminator either: with the five-argument call FIRST the same thing happens.
|
|
3400
|
+
//
|
|
3401
|
+
// So the gate is the MODEL'S OWN EXTENT. This frontend models the captured object as the single
|
|
3402
|
+
// word at [sp,#0] (`isFrameObjectAccess` below), and a one-word model is a description of the
|
|
3403
|
+
// frame only when the frame IS that word. `localArea === 4` says exactly that. It excludes every
|
|
3404
|
+
// block-copy base — each needs two frame words before agbcc names its base with a register at
|
|
3405
|
+
// all — and the two-word struct returns with them. Any larger frame has bytes this model does not
|
|
3406
|
+
// describe, and they are either the rest of a wider object the callee writes or another call's
|
|
3407
|
+
// argument slots; neither is provable here, so the answer stays the decline.
|
|
3408
|
+
//
|
|
3409
|
+
// What the frame size does NOT exclude is the one-word struct return in the table above, which is
|
|
3410
|
+
// instruction-for-instruction an out-parameter call. That one is settled after the lift, by the
|
|
3411
|
+
// premise re-check in the frame-object audit — which is also where the licence's other half is
|
|
3412
|
+
// re-proven.
|
|
3413
|
+
//
|
|
3414
|
+
// WHAT THIS GATE IS NOT. It switches the outgoing-argument refusals off, and nothing else. The
|
|
3415
|
+
// object model runs on ANY frame — the `laddrs` path in the audit below — so a `u8 buf[12]`
|
|
3416
|
+
// handed to a callee arrives there through a frame this conjunct never looks at, and what bounds
|
|
3417
|
+
// its extent is the audit's frame-accounting rule rather than anything here.
|
|
3418
|
+
//
|
|
3419
|
+
// WHY IT IS NOT WIDENED anyway, since a wider frame is the obvious next lever. Three shapes,
|
|
3420
|
+
// each compiled with agbcc 2.9-arm-000512, `-O2 -mthumb-interwork -Wimplicit -fhex-asm
|
|
3421
|
+
// -fprologue-bugfix`, and only the first is about extent at all:
|
|
3422
|
+
//
|
|
3423
|
+
// UNDECIDABLE — a slot THIS FUNCTION stores and reloads. Two sources that disagree about who
|
|
3424
|
+
// owns [sp,#4] compile to one instruction stream, byte for byte, with eight values live across
|
|
3425
|
+
// the calls so one of them spills:
|
|
3426
|
+
//
|
|
3427
|
+
// s32 loc; s32 t0..t7; loc = x; t0 = h(0); … g(&loc); k(loc + t0 + …);
|
|
3428
|
+
// struct P { s32 a, b; } p; s32 t1..t7; p.a = x; p.b = h(0); … g(&p); k(p.a + p.b + …);
|
|
3429
|
+
//
|
|
3430
|
+
// The first says a callee may not touch [sp,#4]; the second says it may, and the reload after the
|
|
3431
|
+
// call must read what it wrote. Nothing distinguishes them, so a licence over THAT slot would be
|
|
3432
|
+
// a guess — which is why the slot rule in the audit refuses the shape rather than deciding it.
|
|
3433
|
+
//
|
|
3434
|
+
// PINNED, and refused by the MODEL — sub-word members. Thumb has no sp-relative `strb`, so a
|
|
3435
|
+
// byte or halfword member is reached through a copy of sp, and the access at +4 witnesses that
|
|
3436
|
+
// the object reaches past its first word: `struct Q { u8 a; u8 pad[3]; u8 b; }` filled and then
|
|
3437
|
+
// handed over compiles to `mov r1, sp / strb r0, [r1] / strb r0, [r1, #0x4] / mov r0, sp / bl g`.
|
|
3438
|
+
// The escape is a use that is not an access, so the capture cannot be split per offset, and the
|
|
3439
|
+
// audit judges the [+4] access against the one object it does model — "only a scalar at the
|
|
3440
|
+
// captured address is modelled", and the SAME message when this conjunct is widened to
|
|
3441
|
+
// `localArea >= 4` (measured).
|
|
3442
|
+
//
|
|
3443
|
+
// PINNED, and ambiguous in its ROLE — a frame-covering block copy. `struct Big { s32 a[17]; };
|
|
3444
|
+
// b = gK; g(&b);` compiles to `add sp,#-0x44 / mov r0,sp / mov r2,#0x44 / bl memcpy`: the copy
|
|
3445
|
+
// bounds the object from below and the reservation from above, so the extent is exact. What is
|
|
3446
|
+
// NOT pinned is what the object IS — the producer table above records those same instructions
|
|
3447
|
+
// for a by-value struct ARGUMENT block and for a struct-return temp. Widening this conjunct
|
|
3448
|
+
// moves it exactly there: it then declines at "which is how a hidden struct-return pointer
|
|
3449
|
+
// looks" (measured), never at anything about extent.
|
|
3450
|
+
//
|
|
3451
|
+
// So a wider frame licence admits nothing this model can describe. `extent` is one scalar width
|
|
3452
|
+
// from one access, and the second access that would build a wider object is a `[+k]` the audit
|
|
3453
|
+
// refuses first: widened, the sub-word shape declines on the very same message and the
|
|
3454
|
+
// block-copy one moves onto the struct-return refusal (both measured).
|
|
3455
|
+
//
|
|
3456
|
+
// RESIDUE: the producer table is agbcc's, so hand-written asm that reserves one word, stages it
|
|
3457
|
+
// as a call's fifth argument and ALSO puts sp in an argument register defeats this — the same
|
|
3458
|
+
// producer assumption the contiguity filter below makes. The producer is named in the gate
|
|
3459
|
+
// rather than left to the prose: `armv4t` has one compiler entry today, and a second one free to
|
|
3460
|
+
// overlay a dead one-word local with a one-word outgoing area would inherit an acceptance whose
|
|
3461
|
+
// only evidence is an agbcc compile table.
|
|
3462
|
+
//
|
|
3463
|
+
// WHICH CONJUNCT REFUSES WHAT, for a reader arriving with a wide frame in hand. klonoa's
|
|
3464
|
+
// `LoadBGTilemapData` (a checkout function, not a benchmark row) reserves 0x3C and fails the
|
|
3465
|
+
// OTHER conjunct: its `mov r5, sp` is the DMA-fill PUBLISH (`strh r7, [r5]` / `mov r0, sp` /
|
|
3466
|
+
// `str r0, [r2]`, r2 = 0x040000D4), not a base live in an argument register at a `bl`.
|
|
3467
|
+
// Instrumented, it arrives with localArea=60 and frameBasePassedToCallee=false, lifts today with
|
|
3468
|
+
// the object modelled as `volatile u16 sp0`, and its lift is byte-identical with this conjunct
|
|
3469
|
+
// widened to `localArea >= 4` (measured). No answer to the frame size moves it.
|
|
3470
|
+
const capturedObjectIsTheWholeFrame = target.compiler === 'agbcc' && frameBasePassedToCallee && localArea === 4;
|
|
3471
|
+
|
|
1932
3472
|
const slotsOffReason = slotModelBlocker();
|
|
1933
3473
|
const slotsOk = slotsOffReason === null;
|
|
1934
3474
|
// Every offset the body actually keys as an SSA slot — the frame-object audit checks the
|
|
1935
3475
|
// address-taken object cannot overlap one (two models for one byte is a silent disagreement).
|
|
1936
3476
|
const usedSlotOffsets = new Set<number>();
|
|
1937
3477
|
|
|
1938
|
-
//
|
|
1939
|
-
//
|
|
1940
|
-
//
|
|
1941
|
-
//
|
|
1942
|
-
//
|
|
3478
|
+
// …AND THE ONE OFFSET THAT MUST NOT BE A SLOT. When `capturedObjectIsTheWholeFrame` holds, the
|
|
3479
|
+
// frame is one word and a callee is being handed its address, so an `[sp,#0]` access is an access
|
|
3480
|
+
// to THAT OBJECT — provisionally, since the audit is what proves the object is a local at all. Keying it as an SSA slot instead moves the value into a
|
|
3481
|
+
// register and deletes the store from memory — and the callee reading it through the pointer is
|
|
3482
|
+
// invisible to every check the slot model makes, so the deletion would be silent.
|
|
3483
|
+
//
|
|
3484
|
+
// The frame-object audit does catch the collision today ("one byte, two models"), as a DECLINE.
|
|
3485
|
+
// Routing the access through an `laddr` here is what turns that decline into the lift, and it
|
|
3486
|
+
// hands the audit the same object it would have judged anyway — offset, width and escape all
|
|
3487
|
+
// come from the machine.
|
|
3488
|
+
//
|
|
3489
|
+
// Word accesses only, and only while the slot model is on: a sub-word or register-offset
|
|
3490
|
+
// `[sp,#k]` anywhere turns the whole model off (slotModelBlocker), and the `mov rD, sp` arm then
|
|
3491
|
+
// declines the capture rather than reaching this.
|
|
3492
|
+
const isFrameObjectAccess = (base: string, off: number, regOff: string | undefined, width: number): boolean =>
|
|
3493
|
+
slotsOk && capturedObjectIsTheWholeFrame && isSpReg(base) && regOff === undefined && off === 0 && width === 4;
|
|
3494
|
+
|
|
3495
|
+
// A WHOLE WORD OF THIS FUNCTION'S OWN RESERVED LOCAL AREA — the shape the ldr and str arms model
|
|
3496
|
+
// as an SSA slot (`sp@<off>`) instead of memory. The two arms spelled these seven terms out
|
|
3497
|
+
// twice — the ldr arm with the reaching-def test as an eighth — which is one edit away from
|
|
3498
|
+
// disagreeing about which bytes are private.
|
|
3499
|
+
//
|
|
3500
|
+
// Each term is a refusal:
|
|
3501
|
+
// * `slotsOk` — the frame must be proven private and immovable (slotModelBlocker)
|
|
3502
|
+
// * sp base, no register index — `[sp, rX]` names no fixed slot
|
|
3503
|
+
// * word width, word-aligned, non-negative — the model keys whole aligned words and nothing else
|
|
3504
|
+
// * `off + 4 <= localArea` — strictly inside the EXPLICITLY reserved area. Not merely inside the
|
|
3505
|
+
// frame: above it sits the callee-saved block the epilogue pops, and above THAT the incoming
|
|
3506
|
+
// argument area, so a slot straying up there would either fight the `pop` or claim the
|
|
3507
|
+
// caller's word.
|
|
3508
|
+
//
|
|
3509
|
+
// What is NOT here, deliberately: the load side's reaching-def test. That one is impure (it asks
|
|
3510
|
+
// the SSA builder) and one-sided — a store needs no reaching def — so it stays at the ldr arm.
|
|
3511
|
+
const isOwnFrameWordSlot = (base: string, off: number, regOff: string | undefined, width: number): boolean =>
|
|
3512
|
+
slotsOk &&
|
|
3513
|
+
isSpReg(base) &&
|
|
3514
|
+
regOff === undefined &&
|
|
3515
|
+
width === 4 &&
|
|
3516
|
+
off % 4 === 0 &&
|
|
3517
|
+
off >= 0 &&
|
|
3518
|
+
off + 4 <= localArea;
|
|
3519
|
+
|
|
3520
|
+
// The WRITE dual of readData, and it guards the WRITE rather than the arms that perform one,
|
|
3521
|
+
// because an enumeration of arms can only cover the arms someone thought of. Checking sp in the
|
|
3522
|
+
// mov/add/sub arms alone leaves `lsl sp, r4, #2`, `neg sp, r4`, `mvn sp, r4`, `ldr sp, [r0,#4]`
|
|
3523
|
+
// and `ldmia r0!, {sp}` lifting, each dropping the sp write silently. Guarding the write ITSELF
|
|
1943
3524
|
// cannot be incomplete.
|
|
1944
3525
|
//
|
|
1945
3526
|
// sp is writable in exactly one shape — the frame adjust the add/sub arms `break` on before
|
|
@@ -1963,15 +3544,17 @@ export function lift(
|
|
|
1963
3544
|
const irb = irBlocks[bi];
|
|
1964
3545
|
let pendingCmp: { lhs: Value; rhs: Value } | null = null;
|
|
1965
3546
|
// Tracks the frame through this block's linear instruction order. Meaningful for the entry
|
|
1966
|
-
// block; elsewhere a `[sp,#N]` access declines.
|
|
1967
|
-
|
|
3547
|
+
// block; elsewhere a `[sp,#N]` access declines. Both dependencies are read HERE rather than
|
|
3548
|
+
// closed over: `preds` is final long before the first `fillBlock` runs, so the boolean is the
|
|
3549
|
+
// same for every block, and passing it in makes that a property of this line instead of a
|
|
3550
|
+
// property of wherever the walk happens to be declared.
|
|
3551
|
+
const frame = makeFrameWalk({ argRegs: target.argRegs, entryHasPreds: preds[0].length > 0 });
|
|
1968
3552
|
|
|
1969
3553
|
// TRUSTWORTHINESS GUARD (mirrors the MIPS/PPC frontends): an unmodelled instruction must not
|
|
1970
3554
|
// silently drop its destination register — emit an honest `opaque`, which fails LOUD at
|
|
1971
3555
|
// assertResolved whether or not anything reads that register (see frontend/opaque.ts). Push/pop and sp
|
|
1972
3556
|
// adjustments have no low-register data destination, so they fall through harmlessly;
|
|
1973
3557
|
// terminators are handled in the terminator section below.
|
|
1974
|
-
const isThumbReg = (s: string | undefined): s is string => /^r\d+$/.test(s ?? '');
|
|
1975
3558
|
const emitOpaqueDest = (ins: { mnemonic: string; ops: string[]; asWritten?: string }) => {
|
|
1976
3559
|
// storeClass: unmodelled Thumb stores are str*/stm* — `stmia rN!, {…}`'s dest token `r0!`
|
|
1977
3560
|
// fails isReg, so without this it would be skipped as "no reg dest", silently deleting the
|
|
@@ -2007,13 +3590,14 @@ export function lift(
|
|
|
2007
3590
|
// destination here is by construction NOT that shape (`add sp, r4`: a register-sized frame
|
|
2008
3591
|
// adjustment, how agbcc spells a frame too large for the 7-bit immediate).
|
|
2009
3592
|
//
|
|
2010
|
-
//
|
|
2011
|
-
//
|
|
2012
|
-
//
|
|
2013
|
-
//
|
|
3593
|
+
// This IS the site that declines `add sp, r4` today — traced 2026-09-06, the throw comes from
|
|
3594
|
+
// here, not from `writeData` and not from an arm. The add/sub arms let only the whitelisted
|
|
3595
|
+
// `sp = sp ± imm` break out, so everything else with an sp destination and no third operand
|
|
3596
|
+
// arrives here.
|
|
2014
3597
|
//
|
|
2015
|
-
//
|
|
2016
|
-
//
|
|
3598
|
+
// Honesty about what that is worth on real input: the 4 `add sp, rN` sites in the sa3
|
|
3599
|
+
// checkout all sit in functions that ALSO do `mov rN, sp` 70+ times, so they declined before
|
|
3600
|
+
// this guard existed and decline after it. No wrong C was ever emitted by this shape.
|
|
2017
3601
|
if (isSpReg(dReg)) {
|
|
2018
3602
|
throw spAsDataError();
|
|
2019
3603
|
}
|
|
@@ -2086,11 +3670,19 @@ export function lift(
|
|
|
2086
3670
|
break;
|
|
2087
3671
|
}
|
|
2088
3672
|
// `add rD, rS, #0` is agbcc's low-register copy idiom (Thumb `mov rD, rS` between
|
|
2089
|
-
// low regs isn't always available). Model it as a pure copy —
|
|
2090
|
-
// an `x + 0` add.
|
|
2091
|
-
//
|
|
2092
|
-
//
|
|
2093
|
-
|
|
3673
|
+
// low regs isn't always available). Model it as a pure copy — the SAME SSA VALUE — not
|
|
3674
|
+
// an `x + 0` add. Value identity is what it buys: the pattern engine matches on it
|
|
3675
|
+
// (`{same:'X'}`), and the structurer's pre-update loop test compares a back-edge argument
|
|
3676
|
+
// against an exit argument by identity, so an `x + 0` between them reads as a different
|
|
3677
|
+
// value and declines a loop that is perfectly ordinary.
|
|
3678
|
+
//
|
|
3679
|
+
// NOT call-argument liveness, which this comment claimed for several releases: both arms
|
|
3680
|
+
// end in `writeData(reg(a), …)`, and the arity machinery (`fallbackArgc`,
|
|
3681
|
+
// `trimClobberedCallArgs`) is keyed on the register, never on the value — measured, zero
|
|
3682
|
+
// arity changes across 3337 corpus functions even with this idiom ablated entirely. That
|
|
3683
|
+
// 3337 is a CHECKOUT sweep this repo does not vendor, not a benchmark count, and it has
|
|
3684
|
+
// not been re-run since.
|
|
3685
|
+
if (immEq(c, 0)) {
|
|
2094
3686
|
writeData(reg(a), bi, readData(reg(b), bi));
|
|
2095
3687
|
break;
|
|
2096
3688
|
}
|
|
@@ -2163,7 +3755,7 @@ export function lift(
|
|
|
2163
3755
|
// Reverse subtract. `rsb rD, rS, #0` is the negate idiom (0 - rS) → -x. Any other form
|
|
2164
3756
|
// (`rsb rD, rS, #N`, N≠0 — not a Thumb-1 encoding, but be safe) is NOT modelled: degrade
|
|
2165
3757
|
// to a loud `opaque` rather than silently leaving rD unwritten (a silent miscompile).
|
|
2166
|
-
if (c
|
|
3758
|
+
if (immEq(c, 0)) {
|
|
2167
3759
|
const res = mkValue(T.unk(32));
|
|
2168
3760
|
irb.ops.push(mkOp('neg', { operands: [readData(reg(b), bi)], results: [res] }));
|
|
2169
3761
|
writeData(reg(a), bi, res);
|
|
@@ -2242,15 +3834,7 @@ export function lift(
|
|
|
2242
3834
|
// e.g. `r4-lr`), a token naming no register, an empty list — leaves the transfer set
|
|
2243
3835
|
// ambiguous, so degrade to the loud opaque rather than guess. Checking only for the
|
|
2244
3836
|
// leftover `-` let `{foo}` through and fabricated a parameter out of it.
|
|
2245
|
-
const list =
|
|
2246
|
-
ins.ops
|
|
2247
|
-
.slice(1)
|
|
2248
|
-
.join(',')
|
|
2249
|
-
.replace(/[{}]/g, '')
|
|
2250
|
-
.split(',')
|
|
2251
|
-
.map((r) => r.trim())
|
|
2252
|
-
.filter(Boolean),
|
|
2253
|
-
);
|
|
3837
|
+
const list = regListOf(ins.ops.slice(1));
|
|
2254
3838
|
if (list === null) {
|
|
2255
3839
|
emitOpaqueDest(ins);
|
|
2256
3840
|
break;
|
|
@@ -2288,7 +3872,15 @@ export function lift(
|
|
|
2288
3872
|
if (ins.mnemonic === 'ldmia') {
|
|
2289
3873
|
const res = mkValue(T.unk(32));
|
|
2290
3874
|
irb.ops.push(
|
|
2291
|
-
|
|
3875
|
+
// `listOrder: true` — this load's stream position is the LIST position, not the
|
|
3876
|
+
// order the source evaluated it (structure.ts's def-order re-spelling must not
|
|
3877
|
+
// trust it; the aload rebuilds in raise/arrays.ts and raise/struct-arrays.ts
|
|
3878
|
+
// must carry it forward)
|
|
3879
|
+
mkOp('load', {
|
|
3880
|
+
operands: [base0],
|
|
3881
|
+
results: [res],
|
|
3882
|
+
attrs: { off: 4 * i, signed: true, width: 4, listOrder: true },
|
|
3883
|
+
}),
|
|
2292
3884
|
);
|
|
2293
3885
|
writeData(reg(r), bi, res);
|
|
2294
3886
|
} else {
|
|
@@ -2451,15 +4043,9 @@ export function lift(
|
|
|
2451
4043
|
// the same address arithmetic the encoding performs. (parseAddr used to silently
|
|
2452
4044
|
// read `[rB]`, dropping the index — a silent miscompile; ldrsh exists ONLY in this
|
|
2453
4045
|
// form in Thumb-1, so every ldrsh went through here.)
|
|
2454
|
-
// An incoming stack argument, read before its base becomes an sp decline. Every
|
|
2455
|
-
//
|
|
2456
|
-
//
|
|
2457
|
-
// • entry has no preds — otherwise its params are phis, not parameters
|
|
2458
|
-
// • no register offset — `[sp, rX]` is not a fixed argument slot
|
|
2459
|
-
// • word width, word-aligned — the argument area is word-granular
|
|
2460
|
-
// • off >= the frame depth — BELOW the frame top is a local/spill: still declines,
|
|
2461
|
-
// that is the separate slot-promotion capability
|
|
2462
|
-
// • a sane arity bound — a wild offset must not mint a 400-parameter signature
|
|
4046
|
+
// An incoming stack argument, read before its base becomes an sp decline. Every refusal
|
|
4047
|
+
// `argIndex` can make, and the soundness proof that each one is needed for, lives on the
|
|
4048
|
+
// predicate itself — restating them here is how the two copies drifted.
|
|
2463
4049
|
{
|
|
2464
4050
|
const index = frame.argIndex({ base, off, regOff }, width, bi);
|
|
2465
4051
|
if (index !== null) {
|
|
@@ -2489,6 +4075,18 @@ export function lift(
|
|
|
2489
4075
|
break;
|
|
2490
4076
|
}
|
|
2491
4077
|
}
|
|
4078
|
+
// The address-taken object at offset 0 comes FIRST: it is memory, not a slot, so it is
|
|
4079
|
+
// read with a real `load` through its `laddr` (see isFrameObjectAccess). No
|
|
4080
|
+
// reaching-def test — the callee holding the address is a writer this function cannot
|
|
4081
|
+
// see, so "never stored here" is not "holds nothing".
|
|
4082
|
+
if (isFrameObjectAccess(base, off, regOff, width)) {
|
|
4083
|
+
const addr = mkValue(T.unk(32));
|
|
4084
|
+
irb.ops.push(mkOp('laddr', { results: [addr], attrs: { off: 0 } }));
|
|
4085
|
+
const res = mkValue(T.unk(32));
|
|
4086
|
+
irb.ops.push(mkOp('load', { operands: [addr], results: [res], attrs: { off: 0, width, signed } }));
|
|
4087
|
+
writeData(reg(a), bi, res);
|
|
4088
|
+
break;
|
|
4089
|
+
}
|
|
2492
4090
|
// A word reload from this function's own frame — the dual of the spill in the str arm.
|
|
2493
4091
|
//
|
|
2494
4092
|
// The reaching-def test is the whole soundness of it, and it mirrors the MIPS guard
|
|
@@ -2498,16 +4096,7 @@ export function lift(
|
|
|
2498
4096
|
// above; INSIDE the frame it is an uninitialised local (or one whose address escaped
|
|
2499
4097
|
// through a path the model missed), and the honest answer is the decline this falls
|
|
2500
4098
|
// through to.
|
|
2501
|
-
if (
|
|
2502
|
-
slotsOk &&
|
|
2503
|
-
isSpReg(base) &&
|
|
2504
|
-
regOff === undefined &&
|
|
2505
|
-
width === 4 &&
|
|
2506
|
-
off % 4 === 0 &&
|
|
2507
|
-
off >= 0 &&
|
|
2508
|
-
off < localArea &&
|
|
2509
|
-
ssa.hasReachingDef(slotKey(off), bi)
|
|
2510
|
-
) {
|
|
4099
|
+
if (isOwnFrameWordSlot(base, off, regOff, width) && ssa.hasReachingDef(slotKey(off), bi)) {
|
|
2511
4100
|
usedSlotOffsets.add(off);
|
|
2512
4101
|
writeData(reg(a), bi, readVar(slotKey(off), bi));
|
|
2513
4102
|
break;
|
|
@@ -2534,22 +4123,17 @@ export function lift(
|
|
|
2534
4123
|
const width = /b/.test(ins.mnemonic) ? 1 : /h/.test(ins.mnemonic) ? 2 : 4;
|
|
2535
4124
|
const { base, off, regOff } = parseAddr(b);
|
|
2536
4125
|
// A word spill into this function's own frame: record the slot's value in SSA rather than
|
|
2537
|
-
// emitting a store through sp
|
|
2538
|
-
//
|
|
2539
|
-
//
|
|
2540
|
-
//
|
|
2541
|
-
|
|
2542
|
-
|
|
2543
|
-
|
|
2544
|
-
|
|
2545
|
-
|
|
2546
|
-
|
|
2547
|
-
|
|
2548
|
-
width === 4 &&
|
|
2549
|
-
off % 4 === 0 &&
|
|
2550
|
-
off >= 0 &&
|
|
2551
|
-
off < localArea
|
|
2552
|
-
) {
|
|
4126
|
+
// emitting a store through sp (which bytes qualify: see isOwnFrameWordSlot). A spill that
|
|
4127
|
+
// is never reloaded becomes a dead def and drops.
|
|
4128
|
+
// …unless offset 0 is the address-taken object (see isFrameObjectAccess), where the
|
|
4129
|
+
// store is a real write to memory that the callee holding the address reads back.
|
|
4130
|
+
if (isFrameObjectAccess(base, off, regOff, width)) {
|
|
4131
|
+
const addr = mkValue(T.unk(32));
|
|
4132
|
+
irb.ops.push(mkOp('laddr', { results: [addr], attrs: { off: 0 } }));
|
|
4133
|
+
irb.ops.push(mkOp('store', { operands: [addr, readData(reg(a), bi)], attrs: { off: 0, width } }));
|
|
4134
|
+
break;
|
|
4135
|
+
}
|
|
4136
|
+
if (isOwnFrameWordSlot(base, off, regOff, width)) {
|
|
2553
4137
|
usedSlotOffsets.add(off);
|
|
2554
4138
|
writeVar(slotKey(off), bi, readData(reg(a), bi));
|
|
2555
4139
|
break;
|
|
@@ -2585,10 +4169,10 @@ export function lift(
|
|
|
2585
4169
|
// known whether every path to here passes through another call, which would have clobbered
|
|
2586
4170
|
// the argument registers this guess just read.
|
|
2587
4171
|
if (declared === undefined) {
|
|
2588
|
-
ssa.recordGuessedCall(callOp, bi, target
|
|
4172
|
+
ssa.recordGuessedCall(callOp, bi, target);
|
|
2589
4173
|
}
|
|
2590
|
-
|
|
2591
|
-
|
|
4174
|
+
writeData('r0', bi, res); // the callee defines r0 …
|
|
4175
|
+
ssa.noteCall(bi); // … and the clobber is recorded after it, so that def is the CALLEE's
|
|
2592
4176
|
break;
|
|
2593
4177
|
}
|
|
2594
4178
|
default:
|
|
@@ -2665,121 +4249,18 @@ export function lift(
|
|
|
2665
4249
|
|
|
2666
4250
|
ssa.finish();
|
|
2667
4251
|
|
|
2668
|
-
//
|
|
2669
|
-
//
|
|
2670
|
-
|
|
2671
|
-
|
|
2672
|
-
|
|
2673
|
-
|
|
2674
|
-
|
|
2675
|
-
|
|
2676
|
-
|
|
2677
|
-
|
|
2678
|
-
|
|
2679
|
-
|
|
2680
|
-
laddrs.push(op);
|
|
2681
|
-
}
|
|
2682
|
-
}
|
|
2683
|
-
}
|
|
2684
|
-
if (laddrs.length > 0) {
|
|
2685
|
-
const fail = (why: string): never => {
|
|
2686
|
-
throw new FrontendUnsupportedError(`cannot lift '${name}': address-taken stack local — ${why}`);
|
|
2687
|
-
};
|
|
2688
|
-
// Taint = values that may hold the object's address, closed over phis: a tainted edge arg
|
|
2689
|
-
// taints the receiving block param. (A phi mixing the address with a non-address would taint
|
|
2690
|
-
// the param and then fail the use judgement below — mixing is not vouched for.)
|
|
2691
|
-
for (const op of laddrs) {
|
|
2692
|
-
if ((op.attrs.off as number) !== 0) {
|
|
2693
|
-
// the audit's offset arithmetic, overlap window and naming all assume the frame base;
|
|
2694
|
-
// if a computed capture ever mints off!==0, this line is what keeps it loud
|
|
2695
|
-
fail(`a capture at frame offset ${op.attrs.off} — only the frame base is modelled`);
|
|
2696
|
-
}
|
|
2697
|
-
}
|
|
2698
|
-
const taint = new Set<Value>(laddrs.flatMap((o) => o.results));
|
|
2699
|
-
for (let changed = true; changed;) {
|
|
2700
|
-
changed = false;
|
|
2701
|
-
for (const blk of irBlocks) {
|
|
2702
|
-
for (const op of blk.ops) {
|
|
2703
|
-
for (const s of op.successors ?? []) {
|
|
2704
|
-
s.args.forEach((arg, i) => {
|
|
2705
|
-
const param = s.block.params[i];
|
|
2706
|
-
if (taint.has(arg) && param !== undefined && !taint.has(param)) {
|
|
2707
|
-
taint.add(param);
|
|
2708
|
-
changed = true;
|
|
2709
|
-
}
|
|
2710
|
-
});
|
|
2711
|
-
}
|
|
2712
|
-
}
|
|
2713
|
-
}
|
|
2714
|
-
}
|
|
2715
|
-
// Judge every use of a tainted value.
|
|
2716
|
-
const accesses: { width: number; signed: boolean }[] = [];
|
|
2717
|
-
let escapes = false;
|
|
2718
|
-
for (const blk of irBlocks) {
|
|
2719
|
-
for (const op of blk.ops) {
|
|
2720
|
-
op.operands.forEach((v, idx) => {
|
|
2721
|
-
if (!taint.has(v)) {
|
|
2722
|
-
return;
|
|
2723
|
-
}
|
|
2724
|
-
if (op.opcode === 'load' && idx === 0) {
|
|
2725
|
-
if ((op.attrs.off as number) !== 0) {
|
|
2726
|
-
fail(
|
|
2727
|
-
`a load at [+${op.attrs.off}] through the captured address — only the scalar at offset 0 is modelled`,
|
|
2728
|
-
);
|
|
2729
|
-
}
|
|
2730
|
-
accesses.push({ width: op.attrs.width as number, signed: (op.attrs.signed as boolean) ?? false });
|
|
2731
|
-
return;
|
|
2732
|
-
}
|
|
2733
|
-
if (op.opcode === 'store' && idx === 0) {
|
|
2734
|
-
if ((op.attrs.off as number) !== 0) {
|
|
2735
|
-
fail(
|
|
2736
|
-
`a store at [+${op.attrs.off}] through the captured address — only the scalar at offset 0 is modelled`,
|
|
2737
|
-
);
|
|
2738
|
-
}
|
|
2739
|
-
accesses.push({ width: op.attrs.width as number, signed: false });
|
|
2740
|
-
return;
|
|
2741
|
-
}
|
|
2742
|
-
if ((op.opcode === 'store' && idx === 1) || op.opcode === 'call') {
|
|
2743
|
-
escapes = true; // the address ESCAPES as a value — the point of the capability
|
|
2744
|
-
return;
|
|
2745
|
-
}
|
|
2746
|
-
fail(`the captured address flows into \`${op.opcode}\` — not an access, an escape, or a phi`);
|
|
2747
|
-
});
|
|
2748
|
-
}
|
|
2749
|
-
}
|
|
2750
|
-
if (accesses.length === 0) {
|
|
2751
|
-
// nothing in-function pins the object's type, and a guessed declaration is the
|
|
2752
|
-
// plausible-but-wrong class — decline until an inhabitant needs this
|
|
2753
|
-
fail('the captured address is never dereferenced in this function, so nothing pins the local object type');
|
|
2754
|
-
}
|
|
2755
|
-
const widths = new Set(accesses.map((a) => a.width));
|
|
2756
|
-
if (widths.size > 1) {
|
|
2757
|
-
fail(`the accesses through the captured address disagree on width (${[...widths].join(' vs ')})`);
|
|
2758
|
-
}
|
|
2759
|
-
const width = accesses[0].width;
|
|
2760
|
-
const signed = accesses.some((a) => a.signed);
|
|
2761
|
-
if (width > localArea) {
|
|
2762
|
-
fail('the object extends past the reserved local area');
|
|
2763
|
-
}
|
|
2764
|
-
for (const off of usedSlotOffsets) {
|
|
2765
|
-
if (off < width) {
|
|
2766
|
-
fail(`the object at [0,${width}) overlaps the SSA slot at [sp,#${off}] — one byte, two models`);
|
|
2767
|
-
}
|
|
2768
|
-
}
|
|
2769
|
-
// Proven. Stamp the MACHINE FACTS the audit established — width and signedness are what the
|
|
2770
|
-
// accesses used, so the declaration downstream is a fact, not a guess. The C-level NAME is
|
|
2771
|
-
// deliberately NOT chosen here: identifiers live in the structurer's namespace (params,
|
|
2772
|
-
// locals, globals, the symbol map), which the frontend cannot see — a frontend-chosen `sp0`
|
|
2773
|
-
// silently shadowed a project global of the same name.
|
|
2774
|
-
// `volatile` iff the address escapes: gcc-2.9 DELETES a store to a non-volatile local nothing
|
|
2775
|
-
// in-function reads — measured, the recompiled loop loaded the value and never stored it —
|
|
2776
|
-
// and the reference idiom's own spelling is `vu16 tmp` for exactly that reason. An object
|
|
2777
|
-
// whose address never leaves the function needs no volatile and must not pay its codegen.
|
|
2778
|
-
for (const op of laddrs) {
|
|
2779
|
-
op.attrs = { ...op.attrs, width, signed, ...(escapes ? { volatile: true } : {}) };
|
|
2780
|
-
}
|
|
2781
|
-
}
|
|
2782
|
-
}
|
|
4252
|
+
// Prove every `laddr` this function emitted really does name one scalar local, or decline
|
|
4253
|
+
// (auditFrameObjects).
|
|
4254
|
+
auditFrameObjects({
|
|
4255
|
+
name,
|
|
4256
|
+
irBlocks,
|
|
4257
|
+
localArea,
|
|
4258
|
+
usedSlotOffsets,
|
|
4259
|
+
capturedObjectIsTheWholeFrame,
|
|
4260
|
+
prototypes,
|
|
4261
|
+
symbols,
|
|
4262
|
+
target,
|
|
4263
|
+
});
|
|
2783
4264
|
|
|
2784
4265
|
// Order the entry block's parameters by ABI register (r0, r1, r2, …) so downstream
|
|
2785
4266
|
// naming (`a0`, `a1`, …) matches the calling convention, not the read order. Safe only
|
|
@@ -2797,9 +4278,11 @@ export function lift(
|
|
|
2797
4278
|
// the `r8` live-in and `@sarg8` tied at 8, the sort is stable, the prologue reads r8 first — so
|
|
2798
4279
|
// ABI argument 8 was emitted as `a9` and every parameter after it was off by one.
|
|
2799
4280
|
//
|
|
2800
|
-
//
|
|
2801
|
-
//
|
|
2802
|
-
//
|
|
4281
|
+
// The register partition (LiveInModel.uninitRegs) takes most of r4-sl before they reach here — one
|
|
4282
|
+
// the ABI does not pass arguments in and this function saved is an uninitialised local, not a
|
|
4283
|
+
// parameter. What still ranks 99 is `lr`/`pc`, which the partition does not list, and an r4-sl the
|
|
4284
|
+
// prologue did not save. Not an argument either way, and the honest place for one is after
|
|
4285
|
+
// everything the convention actually describes.
|
|
2803
4286
|
abiSortEntryParams(entry, preds[0].length > 0, (v) => {
|
|
2804
4287
|
const key = paramReg.get(v) ?? '';
|
|
2805
4288
|
// an incoming STACK argument ranks by its ABI index, after every register argument
|
|
@@ -2815,3 +4298,10 @@ export function lift(
|
|
|
2815
4298
|
|
|
2816
4299
|
/** The ARMv4T / Thumb (agbcc) frontend, registered for the `armv4t` target. */
|
|
2817
4300
|
export const thumbFrontend: Frontend = { id: 'thumb', inputFormat: 'gnu-as', lift };
|
|
4301
|
+
|
|
4302
|
+
/** Internal surface for this module's own tests, and for nothing else. `@asmlift/core` exports
|
|
4303
|
+
* every source path under `./*`, so a plain `export` here would put the pad table, the pad
|
|
4304
|
+
* predicate and the witness serialisation into the package's public API — three things no
|
|
4305
|
+
* consumer should be able to depend on, exported only so a cross-check test could reach them.
|
|
4306
|
+
* The name says what it is; nothing in `src` reads it. */
|
|
4307
|
+
export const __testing = { PAD_ENCODINGS, isPadInstr, factKey, sayFact };
|