@asmlift/core 0.9.0 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +25 -24
- package/package.json +1 -1
- package/src/aggregate.ts +149 -0
- package/src/backend/c.ts +2 -0
- package/src/backend/cfamily.ts +197 -61
- package/src/backend/cpp.ts +3 -1
- package/src/backend/lines.ts +10 -0
- package/src/backend/pascal.ts +47 -16
- package/src/cdecl/parse.ts +21 -0
- package/src/contracts.ts +246 -36
- package/src/declare.ts +69 -20
- package/src/frontend/arm-body.ts +155 -0
- package/src/frontend/asmdata.ts +9 -2
- package/src/frontend/call-plan.ts +1117 -61
- package/src/frontend/callee-body.ts +370 -0
- package/src/frontend/dead-reads.ts +628 -0
- package/src/frontend/device-pins.ts +33 -4
- package/src/frontend/emit.ts +4 -3
- package/src/frontend/fpu.ts +224 -17
- package/src/frontend/frame-objects.ts +1689 -206
- package/src/frontend/frontend.ts +84 -3
- package/src/frontend/high-half.ts +53 -4
- package/src/frontend/jump-table.ts +545 -0
- package/src/frontend/local-object.ts +158 -32
- package/src/frontend/mips.ts +2296 -217
- package/src/frontend/own-struct-return.ts +500 -0
- package/src/frontend/ppc.ts +2748 -245
- package/src/frontend/register-struct-call.ts +399 -0
- package/src/frontend/reloc-symbol.ts +22 -7
- package/src/frontend/ssa.ts +1721 -43
- package/src/frontend/stackargs.ts +199 -72
- package/src/frontend/thumb-entry.ts +340 -0
- package/src/frontend/thumb.ts +2400 -325
- package/src/ir/alias.ts +65 -2
- package/src/ir/bits.ts +1699 -4
- package/src/ir/core.ts +254 -9
- package/src/ir/discipline.ts +86 -21
- package/src/ir/entry.ts +157 -0
- package/src/{structure → ir}/loops.ts +6 -2
- package/src/ir/opcodes.ts +137 -9
- package/src/ir/parse.ts +1 -0
- package/src/ir/simplify.ts +61 -10
- package/src/ir/types.ts +86 -2
- package/src/ir/verify.ts +96 -11
- package/src/l3/argbase.ts +2 -0
- package/src/l3/ast.ts +582 -18
- package/src/l3/basecse.ts +68 -15
- package/src/l3/caseendgoto.ts +223 -0
- package/src/l3/coalesce.ts +42 -14
- package/src/l3/constinit.ts +410 -0
- package/src/l3/dce.ts +53 -9
- package/src/l3/initfirst.ts +42 -11
- package/src/l3/intcell.ts +2 -1
- package/src/l3/narrowdecl.ts +77 -36
- package/src/l3/pollguard.ts +8 -2
- package/src/l3/ptrcell.ts +33 -34
- package/src/l3/ptrfield.ts +123 -8
- package/src/l3/regspell.ts +11 -4
- package/src/l3/reindex.ts +295 -99
- package/src/l3/scopebase.ts +2 -0
- package/src/l3/storage.ts +98 -1
- package/src/l3/symbol-refs.ts +84 -6
- package/src/l3/tailmerge.ts +6 -2
- package/src/l3/typing.ts +108 -15
- package/src/l3/unreduce.ts +6 -5
- package/src/l3/volatileptr.ts +17 -54
- package/src/mangle.ts +30 -0
- package/src/pattern/engine.ts +361 -73
- package/src/pipeline.ts +1039 -38
- package/src/proto-context.ts +1039 -45
- package/src/proto.ts +555 -40
- package/src/raise/aggmembers.ts +290 -0
- package/src/raise/arrays.ts +5 -1
- package/src/raise/const.ts +25 -0
- package/src/raise/declared-volatile.ts +207 -31
- package/src/raise/divpow2.ts +192 -0
- package/src/raise/floathelpers.ts +254 -33
- package/src/raise/gvn.ts +3 -3
- package/src/raise/latch.ts +162 -6
- package/src/raise/memberarrays.ts +10 -4
- package/src/raise/offsetnames.ts +422 -36
- package/src/raise/pairparams.ts +33 -0
- package/src/raise/paramwidth.ts +196 -6
- package/src/raise/pre-recovery.ts +107 -21
- package/src/raise/pre-structure.ts +73 -0
- package/src/raise/recover.ts +12 -5
- package/src/raise/reroll.ts +674 -0
- package/src/raise/shortcircuit.ts +217 -34
- package/src/raise/softdiv.ts +1 -1
- package/src/raise/storesign.ts +153 -0
- package/src/raise/struct-arrays.ts +30 -11
- package/src/raise/structs.ts +396 -33
- package/src/raise/truncload.ts +4 -3
- package/src/raise/unpre.ts +594 -0
- package/src/raise/unthread.ts +400 -0
- package/src/rank-declare.ts +118 -12
- package/src/rank-variations.ts +186 -35
- package/src/rank.ts +536 -68
- package/src/runtime-helpers.ts +140 -22
- package/src/structure/analysis.ts +217 -8
- package/src/structure/arith-ops.ts +23 -0
- package/src/structure/cross-jumped-tail.ts +216 -0
- package/src/structure/globalaccess.ts +157 -10
- package/src/structure/hazards.ts +16 -64
- package/src/structure/local-statics.ts +145 -9
- package/src/structure/namecoalesce.ts +1 -1
- package/src/structure/pointer-spelling.ts +1106 -0
- package/src/structure/redundant-test.ts +1 -1
- package/src/structure/structure.ts +6192 -1213
- package/src/structure/switch-recover.ts +583 -120
- package/src/structure/unfolded-branch-constant.ts +90 -0
- package/src/symbols.ts +58 -15
- package/src/target.ts +1028 -54
- package/src/trace.ts +49 -17
- package/src/variation-definitions.ts +333 -20
- package/src/variation-gates.ts +4 -0
- package/src/variation-tokens.ts +68 -3
package/src/frontend/thumb.ts
CHANGED
|
@@ -18,33 +18,82 @@
|
|
|
18
18
|
// incoming stack argument at `[sp, #N]` locatable at all. Because agbcc may
|
|
19
19
|
// copy a callee-saved argument (e.g. into r4) before touching r0, entry parameters are
|
|
20
20
|
// ordered by ABI register (r0, r1, …), not by the order they were first read.
|
|
21
|
+
import { ownArgumentWordsOn } from '../aggregate';
|
|
21
22
|
import { Block, Fn, Successor, Value, mergeClasses, mkOp, mkValue } from '../ir/core';
|
|
22
23
|
import type { Opcode } from '../ir/opcodes';
|
|
23
|
-
import { T
|
|
24
|
-
import {
|
|
24
|
+
import { T } from '../ir/types';
|
|
25
|
+
import {
|
|
26
|
+
type FnProto,
|
|
27
|
+
type Prototypes,
|
|
28
|
+
declaresVoidReturn,
|
|
29
|
+
ownLeadingWordRegisters,
|
|
30
|
+
ownReturnWidth,
|
|
31
|
+
wordsOf,
|
|
32
|
+
} from '../proto';
|
|
33
|
+
import type { ContextGlobals } from '../proto-context';
|
|
25
34
|
import { type SymbolMap, lookupInterior, lookupSymbol } from '../symbols';
|
|
26
|
-
import type
|
|
35
|
+
import { ARM_REGISTER_ALIASES, type TargetDescription } from '../target';
|
|
27
36
|
import type { AsmData } from './asmdata';
|
|
28
|
-
import { type CallDeclarations, type CallLowering, callDeclarations } from './call-plan';
|
|
37
|
+
import { type CallDeclarations, type CallLowering, callDeclarations, foldByValueArgs } from './call-plan';
|
|
38
|
+
import { type CalleeStackWords, asksCalleeBody } from './callee-body';
|
|
39
|
+
import { pinDeadReads, unreadPairHalf } from './dead-reads';
|
|
29
40
|
import { pinDeviceAccesses } from './device-pins';
|
|
30
41
|
import { pushSwitchBr } from './emit';
|
|
31
42
|
import { FrontendUnsupportedError } from './errors';
|
|
32
43
|
import { inheritFlags } from './flags-edge';
|
|
33
44
|
import { assertInputFormat } from './format';
|
|
34
|
-
import {
|
|
35
|
-
|
|
36
|
-
|
|
45
|
+
import {
|
|
46
|
+
FRAME_OBJECT_AUDIT,
|
|
47
|
+
type FrameObjectAudit,
|
|
48
|
+
type FrameObjectRelift,
|
|
49
|
+
type FrameRange,
|
|
50
|
+
liftWithRelift,
|
|
51
|
+
nameMergedSretObjects,
|
|
52
|
+
refuseSretOutsideFrame,
|
|
53
|
+
} from './frame-objects';
|
|
54
|
+
import { type Frontend, type LiftOptions, type OwnDeclaration, ownDeclarationOf } from './frontend';
|
|
55
|
+
import {
|
|
56
|
+
type CaseEntry,
|
|
57
|
+
type IdiomLeft,
|
|
58
|
+
type StaleIdiomRead,
|
|
59
|
+
caseEntries,
|
|
60
|
+
enterCaseTarget,
|
|
61
|
+
idiomLeaves,
|
|
62
|
+
parkedIn,
|
|
63
|
+
refuseStaleIdiomReads,
|
|
64
|
+
staleMarker,
|
|
65
|
+
} from './jump-table';
|
|
66
|
+
import {
|
|
67
|
+
type LocalStatics,
|
|
68
|
+
gasPoolReferrers,
|
|
69
|
+
makeLocalStatics,
|
|
70
|
+
readGasAnonymousObject,
|
|
71
|
+
readGasLocalObject,
|
|
72
|
+
} from './local-object';
|
|
37
73
|
import { opaqueDest } from './opaque';
|
|
38
74
|
import { classifyRelocSymbol, unspellableReason } from './reloc-symbol';
|
|
39
75
|
import {
|
|
40
|
-
type ArgSlots,
|
|
41
76
|
type SsaBuilder,
|
|
42
77
|
abiSortEntryParams,
|
|
78
|
+
assertScratchRegsPartitioned,
|
|
43
79
|
makeSsaBuilder,
|
|
44
80
|
mintArgSlotHoles,
|
|
81
|
+
recordAsmLayout,
|
|
82
|
+
registerArgSlots,
|
|
83
|
+
registerLiveIns,
|
|
84
|
+
stackArgKey,
|
|
85
|
+
stackArgSlots,
|
|
45
86
|
stackSlotKey,
|
|
87
|
+
unifyRegisterSpellings,
|
|
46
88
|
} from './ssa';
|
|
47
|
-
import {
|
|
89
|
+
import {
|
|
90
|
+
type OutgoingArgs,
|
|
91
|
+
type StackArgsBlock,
|
|
92
|
+
type StackArgsEvent,
|
|
93
|
+
analyzeOutgoingArgs,
|
|
94
|
+
loadedWordBound,
|
|
95
|
+
} from './stackargs';
|
|
96
|
+
import { thumbEntryWordsFromText } from './thumb-entry';
|
|
48
97
|
|
|
49
98
|
interface Instr {
|
|
50
99
|
/** the CANONICAL spelling — legacy names are normalised (see LEGACY_MNEMONICS) so that every
|
|
@@ -55,7 +104,16 @@ interface Instr {
|
|
|
55
104
|
* Messages must use this: a decline naming `ldrsh` for a file containing `ldsh` sends the
|
|
56
105
|
* reader looking for an instruction that is not there. */
|
|
57
106
|
asWritten?: string;
|
|
107
|
+
/** set only on a `bl <label>` whose line carried the target's far-jump mark — agbcc's
|
|
108
|
+
* `@far jump` / `@ far jump`, the relay it spells a branch past Thumb's ±2 KB reach with (see
|
|
109
|
+
* `compilerBehaviors.farJumpRelay`); never on a target that declares no mark. The
|
|
110
|
+
* comment is the ONLY text that separates it from a call: everything else on the line is
|
|
111
|
+
* dropped at tokenizing, and a call thunk is the same `bl` to a label of this function. */
|
|
112
|
+
farJump?: true;
|
|
58
113
|
}
|
|
114
|
+
/** The label a block the .s does not label is given — one no .s spells (`thumbCallLocator`). */
|
|
115
|
+
const ANON_BLOCK_PREFIX = '.L_anon';
|
|
116
|
+
|
|
59
117
|
interface AsmBlock {
|
|
60
118
|
label: string;
|
|
61
119
|
instrs: Instr[];
|
|
@@ -224,15 +282,21 @@ const controlContinuesPast = (ins: Instr) => {
|
|
|
224
282
|
* `mov pc, rN`.) */
|
|
225
283
|
function classifyXfer(ins: Instr): XferKind | null {
|
|
226
284
|
const mn = ins.mnemonic;
|
|
227
|
-
|
|
285
|
+
// agbcc's far-jump relay is a branch wearing the call mnemonic. Classified here so the layout
|
|
286
|
+
// questions asked BEFORE it is lowered to `b` (`readThumbText`: `lowerFarJumpRelays`) — whether
|
|
287
|
+
// a fill after it is sealed, whether the block before a pool falls into data — read it as the
|
|
288
|
+
// transfer it is; it keeps its 4 bytes there because the mnemonic is still `bl`.
|
|
289
|
+
if (mn === 'b' || (mn === 'bl' && ins.farJump)) {
|
|
228
290
|
return 'uncond';
|
|
229
291
|
}
|
|
230
292
|
if (COND_OPCODE[mn]) {
|
|
231
293
|
return 'cond';
|
|
232
294
|
}
|
|
233
|
-
// `bx rN`: agbcc's return is `bx lr` or `pop {rN}; bx rN` (rN holds the restored LR) — a return.
|
|
234
|
-
//
|
|
235
|
-
//
|
|
295
|
+
// `bx rN`: agbcc's return is `bx lr` or `pop {…, rN}; bx rN` (rN holds the restored LR) — a return.
|
|
296
|
+
// This sees one instruction, so it cannot tell what rN holds; once the CFG exists, every return
|
|
297
|
+
// of the selected function is asked whether it reads the caller's lr from the frame word the
|
|
298
|
+
// prologue saved it in (`refuseReturnsNotThroughCallersLr`), and any other declines as the
|
|
299
|
+
// indirect jump it is. (agbcc emits jump tables via `mov pc`, NOT `bx`.)
|
|
236
300
|
if (mn === 'bx') {
|
|
237
301
|
return 'return';
|
|
238
302
|
}
|
|
@@ -332,8 +396,9 @@ const REG_NUM: Record<string, number> = Object.assign(Object.create(null), { sp:
|
|
|
332
396
|
|
|
333
397
|
// Registers Thumb-1's `push` cannot name, in every spelling this ISA's asm uses for them. Saving
|
|
334
398
|
// one takes agbcc's `mov rLow, rHi; push {rLow}`, which is why the prologue's save set has to read
|
|
335
|
-
// the `mov` as well as the list (see `savedRegs`). Spellings, not numbers:
|
|
336
|
-
//
|
|
399
|
+
// the `mov` as well as the list (see `savedRegs`). Spellings, not numbers: a register keeps the
|
|
400
|
+
// name it was written with, and only a function that writes one two ways is rewritten to one of them
|
|
401
|
+
// (`unifyRegisterSpellings` in `readThumbText`), so `sl` and `r10` are never both keys of one lift.
|
|
337
402
|
const HIGH_REGS: ReadonlySet<string> = new Set(['r8', 'r9', 'r10', 'r11', 'r12', 'sb', 'sl', 'fp', 'ip']);
|
|
338
403
|
|
|
339
404
|
/** The operands of a `cmp` whose flags are still live — the whole of the condition state this
|
|
@@ -352,16 +417,25 @@ const HIGH_REGS: ReadonlySet<string> = new Set(['r8', 'r9', 'r10', 'r11', 'r12',
|
|
|
352
417
|
* the edges of a conditional branch (mwcc's switch dispatch reads one `cmpwi` from a `beq`
|
|
353
418
|
* and then from the `bge` on its fall-through), and Thumb, with no row inhabiting that shape,
|
|
354
419
|
* does not. */
|
|
355
|
-
type PendingCmp = {
|
|
420
|
+
type PendingCmp = {
|
|
421
|
+
lhs: Value;
|
|
422
|
+
rhs: Value;
|
|
423
|
+
/** The flags came from `cmn lhs, rhs`, which stands for `cmp lhs, -rhs`. Nothing is minted for
|
|
424
|
+
* the negation until a branch reads it: the tag travels with the compare, across an edge too
|
|
425
|
+
* (`inheritFlags` carries the record whole), and the terminator mints the `neg` in the READING
|
|
426
|
+
* block with the attrs that branch needs, or refuses. See `CMN_EXACT`. */
|
|
427
|
+
cmn?: true;
|
|
428
|
+
};
|
|
356
429
|
|
|
357
430
|
// Thumb-1 data-processing mnemonics that write the condition flags when their destination is a LOW
|
|
358
431
|
// register — which is all of them on this ISA, `s`-suffix or not (the assembler picks the encoding).
|
|
359
432
|
// Used to invalidate a pending compare: see the decode loop. Loads, stores and push/pop leave the
|
|
360
|
-
// flags alone. Three
|
|
433
|
+
// flags alone. Three kinds of writer that DO write them are absent, each for its own reason, and lumping
|
|
361
434
|
// them under one was how `cmn`/`tst` came to be described as instructions that set no flags:
|
|
362
|
-
// * `cmp`
|
|
363
|
-
//
|
|
364
|
-
//
|
|
435
|
+
// * `cmp` and `cmn` seed the pending compare instead of invalidating it — they are the modelled
|
|
436
|
+
// ones (`cmn` as `cmp rA, -rB`, `lowerCmn`);
|
|
437
|
+
// * `tst` writes the flags and defines no register, so the membership test below (which reads a
|
|
438
|
+
// destination) cannot judge it — `FLAG_ONLY` does;
|
|
365
439
|
// * a CALL is not an instruction's flag write at all: `bl` writes none itself, but the function
|
|
366
440
|
// it enters retires compares of its own, and AAPCS lets it. Invalidated separately.
|
|
367
441
|
const FLAG_SETTING = new Set([
|
|
@@ -401,20 +475,45 @@ const FLAG_SETTING = new Set([
|
|
|
401
475
|
'sbcs',
|
|
402
476
|
]);
|
|
403
477
|
|
|
404
|
-
// The flag
|
|
405
|
-
//
|
|
406
|
-
//
|
|
478
|
+
// The flag writer with no destination to read. `tst` computes a value it throws away and keeps
|
|
479
|
+
// only the flags, so `FLAG_SETTING`'s destination test cannot see it — yet it takes the flags from
|
|
480
|
+
// a pending compare exactly as `sub` does, and the branch that follows tests ITS.
|
|
407
481
|
//
|
|
408
|
-
// Unconditional, unlike `FLAG_SETTING`:
|
|
409
|
-
// non-flag-setting encoding, and Thumb-1 encodes
|
|
482
|
+
// Unconditional, unlike `FLAG_SETTING`: it has no destination whose width could pick a
|
|
483
|
+
// non-flag-setting encoding, and Thumb-1 encodes it on low registers only.
|
|
410
484
|
//
|
|
411
|
-
//
|
|
485
|
+
// It still decodes to an `opaque` (the default arm), so a function containing one declines either
|
|
412
486
|
// way and this changes no verdict — only which decline, and the terminator's is the true one.
|
|
413
487
|
// Without it `cmp r0,r1 / tst r2,r3 / bge` FOLDED the stale compare into the condition and was
|
|
414
488
|
// loud solely because the opaque it minted is unresolvable: the right answer resting on an
|
|
415
|
-
// accident of an unrelated model.
|
|
416
|
-
// `
|
|
417
|
-
|
|
489
|
+
// accident of an unrelated model. Modelling `tst` is a capability with no inhabitant: agbcc's
|
|
490
|
+
// `tstsi` prints `cmp %0, #0` (gcc/thumb.md:818-821), so every `tst` in the corpus is hand-written.
|
|
491
|
+
//
|
|
492
|
+
// `cmn` is not here: it is a compare (the decode loop's `cmn` arm). agbcc's `cmnsi`
|
|
493
|
+
// (thumb.md:823-827) is `(compare rA (neg rB))`, which combine makes of `a == -b` and `a <= -b`.
|
|
494
|
+
const FLAG_ONLY = new Set(['tst']);
|
|
495
|
+
|
|
496
|
+
// The branches that read a `cmn`'s flags as the compare it stands for, and why the others cannot.
|
|
497
|
+
// `cmn rA, rB` sets NZCV from rA + rB, and `cmp rA, -rB` from rA - (-rB):
|
|
498
|
+
// * N and Z come from the same 32-bit result, so `beq`/`bne` read the WRAPPED a + b == 0, which
|
|
499
|
+
// the plain wrapping negation spells: exactly for an unsigned or narrower `b`, and for an `s32`
|
|
500
|
+
// one everywhere but b == INT_MIN, where C's `-b` overflows — the spelling every wrapping `neg`
|
|
501
|
+
// in this frontend already has;
|
|
502
|
+
// * V differs only at rB == INT_MIN, where cmn reports the true sign of rA + 2^31 and the
|
|
503
|
+
// compare's `-rB` wraps back to INT_MIN. The lift spells the negation SIGNED (`neg_s`), so its
|
|
504
|
+
// C differs from the bytes at that one input only, where its `-b` is undefined. agbcc folds the
|
|
505
|
+
// defined `a > (int)(0u - b)` into `cmn; bgt` too, and miscompiles it there, so the claim is
|
|
506
|
+
// narrower than "exact for every source": a C spelling that recompiles to the same bytes AND
|
|
507
|
+
// diverges from them only where it is itself undefined. When rB always holds INT_MIN that one
|
|
508
|
+
// input is every input, and the structurer refuses (structure.ts, `neg_s`);
|
|
509
|
+
// * C is the carry out of rA + rB, which is NOT rA >=u -rB: at rB == 0 cmn's carry is clear and
|
|
510
|
+
// the compare's is set. agbcc folds unsigned `a < -b` into `cmn; bcs` regardless — it
|
|
511
|
+
// miscompiles at b == 0 — and the C that is exact there (`a + b >= a`) compiles to
|
|
512
|
+
// `add; cmp; bcc` instead. Unsigned C defines every input, so no spelling both matches the
|
|
513
|
+
// bytes and leaves the divergence undefined, and the unsigned branches refuse. The sentence
|
|
514
|
+
// stays literal at its throw in `lowerTerminator`: the decline taxonomy (`cmn-carry`) is
|
|
515
|
+
// checked against throw texts as written.
|
|
516
|
+
const CMN_EXACT = new Set(['beq', 'bne', 'blt', 'ble', 'bgt', 'bge']);
|
|
418
517
|
const regNum = (r: string) => (r[0] === 'r' ? Number(r.slice(1)) : REG_NUM[r]);
|
|
419
518
|
function expandRegList(tokens: string[]): string[] {
|
|
420
519
|
const out: string[] = [];
|
|
@@ -629,25 +728,15 @@ const isFrameAdjust = (
|
|
|
629
728
|
(base === undefined || isSpReg(base)) &&
|
|
630
729
|
(off?.startsWith('#') ?? false);
|
|
631
730
|
|
|
632
|
-
// A virtual register key per incoming stack argument. Reading it goes through the ordinary Braun
|
|
633
|
-
// live-in path (frontend/ssa.ts), which turns a read with no reaching def into a function
|
|
634
|
-
// parameter — so this needs NO new representation, opcode or pass. The `@` cannot appear in a
|
|
635
|
-
// real register token, so the key cannot collide with one.
|
|
636
|
-
const stackArgKey = (index: number) => `@sarg${index}`;
|
|
637
|
-
// Defined beside its mint site on purpose: the format string and its parser drifted 650 lines
|
|
638
|
-
// apart in the first version, with the convention explained only at one end.
|
|
639
|
-
const stackArgIndex = (key: string): number | null => {
|
|
640
|
-
const m = /^@sarg(\d+)$/.exec(key);
|
|
641
|
-
return m ? +m[1] : null;
|
|
642
|
-
};
|
|
643
|
-
|
|
644
731
|
// INCOMING STACK ARGUMENTS (AAPCS). Args 1-4 arrive in r0-r3; args 5+ are pushed by the CALLER,
|
|
645
732
|
// so the callee reads them at `[sp, #N]` where N is at or above its own frame. Those are
|
|
646
733
|
// PARAMETERS, not locals — declining them as "sp used as data" refuses a calling convention.
|
|
647
734
|
//
|
|
648
|
-
// The frame depth is tracked by a linear walk of
|
|
649
|
-
//
|
|
650
|
-
//
|
|
735
|
+
// The frame depth is tracked by a linear walk of ONE BLOCK, from a depth that is exact where the
|
|
736
|
+
// block starts: 0 at the entry block (when nothing branches back to it), and for every other block
|
|
737
|
+
// the depth the entry block leaves, but only where the function is proven never to move sp outside
|
|
738
|
+
// its prologue and its returning tails (`frameDepthBlocker`). Within a straight-line block the depth
|
|
739
|
+
// at each instruction is then exact. Every case that would need more declines.
|
|
651
740
|
//
|
|
652
741
|
// `push {a,b,c}` deepens by 4 per register; `sub sp, #N` and `add sp, sp, #-N` deepen by N.
|
|
653
742
|
//
|
|
@@ -660,12 +749,12 @@ const stackArgIndex = (key: string): number | null => {
|
|
|
660
749
|
// preserve is written next to the state it is about.
|
|
661
750
|
//
|
|
662
751
|
// `deps` carries the only two things about the FUNCTION this needs — the ABI's argument-register
|
|
663
|
-
// count, and
|
|
664
|
-
// instructions it is stepped over.
|
|
665
|
-
const makeFrameWalk = (deps: { argRegs: readonly string[];
|
|
752
|
+
// count, and the depth this block starts at (null where none is proven) — so the walk itself reads
|
|
753
|
+
// nothing but the instructions it is stepped over.
|
|
754
|
+
const makeFrameWalk = (deps: { argRegs: readonly string[]; start: number | null }) => {
|
|
666
755
|
// Bytes the frame has grown since function entry, along this block's linear order only.
|
|
667
|
-
let depth = 0;
|
|
668
|
-
let depthKnown =
|
|
756
|
+
let depth = deps.start ?? 0;
|
|
757
|
+
let depthKnown = deps.start !== null;
|
|
669
758
|
|
|
670
759
|
// Returns null whenever the depth cannot be computed exactly — including for any write to sp
|
|
671
760
|
// this does not model. The capability rests on the depth being EXACT, so an approximation is
|
|
@@ -730,6 +819,11 @@ const makeFrameWalk = (deps: { argRegs: readonly string[]; entryHasPreds: boolea
|
|
|
730
819
|
}
|
|
731
820
|
},
|
|
732
821
|
|
|
822
|
+
/** The depth the instructions stepped so far leave, or null where it is not exact. */
|
|
823
|
+
depthNow(): number | null {
|
|
824
|
+
return depthKnown ? depth : null;
|
|
825
|
+
},
|
|
826
|
+
|
|
733
827
|
// Is `[sp, #off]` an incoming stack argument, and which one? `null` means NO — and every null is
|
|
734
828
|
// a DECLINE, because the caller falls through to readData's sp guard.
|
|
735
829
|
//
|
|
@@ -753,19 +847,16 @@ const makeFrameWalk = (deps: { argRegs: readonly string[]; entryHasPreds: boolea
|
|
|
753
847
|
// reads. `step` enforces it, and never un-poisons: once sp has been above the line, a push during
|
|
754
848
|
// the excursion may have written the later slots too. This is a PREMISE, not a detail — leaving
|
|
755
849
|
// it unstated is what let the first version hand back a callee-saved register as an argument.
|
|
756
|
-
argIndex(addr: { base: string; off: number; regOff?: string }, width: number
|
|
850
|
+
argIndex(addr: { base: string; off: number; regOff?: string }, width: number): number | null {
|
|
757
851
|
const { base, off, regOff } = addr;
|
|
758
852
|
if (!isSpReg(base) || regOff !== undefined) {
|
|
759
853
|
return null; // not sp, or `[sp, rX]` — not a fixed argument slot
|
|
760
854
|
}
|
|
761
|
-
if (bi !== 0 || deps.entryHasPreds) {
|
|
762
|
-
return null; // depth is exact only along the ENTRY block's linear order, and only when its
|
|
763
|
-
// params are parameters rather than phis
|
|
764
|
-
}
|
|
765
855
|
if (!depthKnown || depth <= 0) {
|
|
766
|
-
return null; // an unmeasurable frame, or none established: a
|
|
767
|
-
// prologue was sliced off looks identical to a frameless
|
|
768
|
-
// locals — minting one would be the silent-wrong trade
|
|
856
|
+
return null; // no exact start (`start`), an unmeasurable frame, or none established: a
|
|
857
|
+
// headerless FRAGMENT whose prologue was sliced off looks identical to a frameless
|
|
858
|
+
// function, and there the slots are locals — minting one would be the silent-wrong trade
|
|
859
|
+
// this frontend refuses
|
|
769
860
|
}
|
|
770
861
|
if (width !== 4 || off < depth || (off - depth) % 4 !== 0) {
|
|
771
862
|
return null; // the argument area is word-granular; BELOW the top is a local, which is the
|
|
@@ -966,11 +1057,14 @@ const sayFact = (f: LayoutFact): string => {
|
|
|
966
1057
|
function decode(
|
|
967
1058
|
name: string,
|
|
968
1059
|
asm: string,
|
|
1060
|
+
farJumpMark?: RegExp,
|
|
969
1061
|
): {
|
|
970
1062
|
blocks: AsmBlock[];
|
|
971
1063
|
dataWords: Map<string, string[]>;
|
|
972
1064
|
nonWordData: Map<string, string>;
|
|
973
1065
|
funcLabels: Set<string>;
|
|
1066
|
+
/** Each label that shares its instruction with a later one, to the label whose block was kept. */
|
|
1067
|
+
aliasOf: ReadonlyMap<string, string>;
|
|
974
1068
|
} {
|
|
975
1069
|
// Flatten to (label | instr | data) items, then split into blocks at labels / after branches.
|
|
976
1070
|
// `.word LABEL` directives are captured into dataWords keyed by the most recent label (the
|
|
@@ -1022,10 +1116,19 @@ function decode(
|
|
|
1022
1116
|
// `.Letext0`) that no code line names. A label starts a block, and a block split at a
|
|
1023
1117
|
// lexical-scope marker restructures the loop around it: kept, it lifts the pair's `gcd` loop as
|
|
1024
1118
|
// an `if` around a `do` instead of a `while`.
|
|
1119
|
+
//
|
|
1120
|
+
// Every `@` comment is dropped here save ONE fact read from it: that a `bl` line carried the
|
|
1121
|
+
// target's far-jump mark (`farJumpMark`, from `compilerBehaviors.farJumpRelay`). Kept as a set of
|
|
1122
|
+
// line positions, not as text, so nothing downstream can read any other comment.
|
|
1025
1123
|
const codeLines: string[] = [];
|
|
1124
|
+
const farJumpLines = new Set<number>();
|
|
1026
1125
|
for (const rawLine of withoutDebugSections(asm).split('\n')) {
|
|
1027
|
-
const
|
|
1126
|
+
const at = rawLine.indexOf('@');
|
|
1127
|
+
const line = (at === -1 ? rawLine : rawLine.slice(0, at)).trim();
|
|
1028
1128
|
if (line) {
|
|
1129
|
+
if (at !== -1 && farJumpMark?.test(rawLine.slice(at))) {
|
|
1130
|
+
farJumpLines.add(codeLines.length);
|
|
1131
|
+
}
|
|
1029
1132
|
codeLines.push(line);
|
|
1030
1133
|
}
|
|
1031
1134
|
}
|
|
@@ -1037,7 +1140,8 @@ function decode(
|
|
|
1037
1140
|
}
|
|
1038
1141
|
}
|
|
1039
1142
|
const isDebugMarker = (label: string) => /^\.L(?:FB|FE|M|BB|BE|etext)\d+$/.test(label) && !namedByCode.has(label);
|
|
1040
|
-
for (
|
|
1143
|
+
for (const [lineNo, line] of codeLines.entries()) {
|
|
1144
|
+
let rest = line;
|
|
1041
1145
|
// A label may share the line with what follows it (pret pools: `_08x: .4byte 0x…`) — peel it.
|
|
1042
1146
|
const lm = rest.match(/^([A-Za-z_.$][\w.$]*):\s*(.*)$/);
|
|
1043
1147
|
if (lm && isDebugMarker(lm[1]) && lm[2] === '') {
|
|
@@ -1234,6 +1338,7 @@ function decode(
|
|
|
1234
1338
|
mnemonic: canon,
|
|
1235
1339
|
ops,
|
|
1236
1340
|
...(canon === m[1] ? {} : { asWritten: m[1] }),
|
|
1341
|
+
...(canon === 'bl' && ops.length === 1 && farJumpLines.has(lineNo) ? { farJump: true as const } : {}),
|
|
1237
1342
|
},
|
|
1238
1343
|
});
|
|
1239
1344
|
}
|
|
@@ -1262,9 +1367,17 @@ function decode(
|
|
|
1262
1367
|
if (funcLabels.length > 0) {
|
|
1263
1368
|
const fi = funcLabels.indexOf(name);
|
|
1264
1369
|
if (fi !== -1) {
|
|
1265
|
-
|
|
1370
|
+
// each label's first item, read in one pass: a search per function label costs labels × lines
|
|
1371
|
+
// on every lift, and a callee body bound lifts the whole unit once per callee it asks
|
|
1372
|
+
const firstAt = new Map<string, number>();
|
|
1373
|
+
allFlat.forEach((f, k) => {
|
|
1374
|
+
if (f.label !== undefined && !firstAt.has(f.label)) {
|
|
1375
|
+
firstAt.set(f.label, k);
|
|
1376
|
+
}
|
|
1377
|
+
});
|
|
1378
|
+
sliceStart = firstAt.get(name) ?? -1;
|
|
1266
1379
|
const starts = funcLabels
|
|
1267
|
-
.map((l) =>
|
|
1380
|
+
.map((l) => firstAt.get(l) ?? -1)
|
|
1268
1381
|
.filter((s) => s > sliceStart)
|
|
1269
1382
|
.sort((a, b) => a - b);
|
|
1270
1383
|
boundaries = [...starts, allFlat.length];
|
|
@@ -1738,7 +1851,7 @@ function decode(
|
|
|
1738
1851
|
continue;
|
|
1739
1852
|
}
|
|
1740
1853
|
if (!cur) {
|
|
1741
|
-
cur = { label: first ? name :
|
|
1854
|
+
cur = { label: first ? name : `${ANON_BLOCK_PREFIX}${anon++}`, instrs: [] };
|
|
1742
1855
|
blocks.push(cur);
|
|
1743
1856
|
first = false;
|
|
1744
1857
|
}
|
|
@@ -1787,7 +1900,9 @@ function decode(
|
|
|
1787
1900
|
for (const b of aliasOf.size ? blocks : []) {
|
|
1788
1901
|
for (const ins of b.instrs) {
|
|
1789
1902
|
const k = ins.ops.length - 1;
|
|
1790
|
-
|
|
1903
|
+
// A marked relay is a branch here too (classifyXfer), though still spelt `bl` until
|
|
1904
|
+
// `lowerFarJumpRelays`: a relay to a dropped alias must name the label it stands for.
|
|
1905
|
+
if ((ins.mnemonic === 'b' || COND_OPCODE[ins.mnemonic] || ins.farJump) && k >= 0) {
|
|
1791
1906
|
ins.ops[k] = aliasOf.get(ins.ops[k]) ?? ins.ops[k];
|
|
1792
1907
|
}
|
|
1793
1908
|
}
|
|
@@ -1870,7 +1985,7 @@ function decode(
|
|
|
1870
1985
|
boundaryIdx++;
|
|
1871
1986
|
continue;
|
|
1872
1987
|
}
|
|
1873
|
-
return { blocks: live, dataWords, nonWordData, funcLabels: new Set(funcLabels) };
|
|
1988
|
+
return { blocks: live, dataWords, nonWordData, funcLabels: new Set(funcLabels), aliasOf };
|
|
1874
1989
|
}
|
|
1875
1990
|
}
|
|
1876
1991
|
|
|
@@ -1994,6 +2109,7 @@ type PoolRef =
|
|
|
1994
2109
|
| { kind: 'const'; value: number }
|
|
1995
2110
|
| { kind: 'gaddr'; sym: string; addend: number }
|
|
1996
2111
|
| { kind: 'local-static'; symbol: string; addend: number }
|
|
2112
|
+
| { kind: 'anonymous'; label: string }
|
|
1997
2113
|
| { kind: 'unmodelled'; why: string };
|
|
1998
2114
|
|
|
1999
2115
|
/** Classify a word-load operand `LABEL[+N]` against the captured literal pools. Returns null when
|
|
@@ -2126,6 +2242,13 @@ function poolRef(
|
|
|
2126
2242
|
why: `pool word '${w}' has a leading-zero magnitude, which is octal to the assembler`,
|
|
2127
2243
|
};
|
|
2128
2244
|
}
|
|
2245
|
+
// A bare `.L` label in a pool word is the address of something the compiler placed and the
|
|
2246
|
+
// source never named (reloc-symbol.ts `anon-pool`): agbcc's `.LC0` is the constant image a local
|
|
2247
|
+
// aggregate's initializer is copied from. The caller reads it as an anonymous object, or refuses
|
|
2248
|
+
// naming why it is not one.
|
|
2249
|
+
if (classifyRelocSymbol(w) === 'anon-pool') {
|
|
2250
|
+
return { kind: 'anonymous', label: w };
|
|
2251
|
+
}
|
|
2129
2252
|
return { kind: 'unmodelled', why: `pool word '${w}' is not a symbol, symbol±offset, or number` };
|
|
2130
2253
|
}
|
|
2131
2254
|
|
|
@@ -2181,7 +2304,8 @@ function poolNamesASymbol(
|
|
|
2181
2304
|
// and its unique bounds predecessor `bounds` ending in `cmp rX,#(N-1); bhi DEF`, verify the exact
|
|
2182
2305
|
// idiom and read the inline table — else return null (→ the indirect-jump loud-fail fires). The
|
|
2183
2306
|
// recovered switch REPLACES both blocks: `bounds` emits a `switch_br` (scrutinee rX; successors =
|
|
2184
|
-
// case blocks + DEF)
|
|
2307
|
+
// case blocks + DEF) — unless the dispatch carries code of its own (`extras`), which keeps it as
|
|
2308
|
+
// the block that emits the switch, with the bounds test left as an ordinary branch.
|
|
2185
2309
|
//
|
|
2186
2310
|
// bounds: cmp rX,#(N-1); bhi DEF disp: lsl rY,rX,#2 ; ldr rP,=PTR ; add rA,rY,rP
|
|
2187
2311
|
// ; ldr rV,[rA] ; mov pc,rV
|
|
@@ -2193,6 +2317,13 @@ interface JumpTable {
|
|
|
2193
2317
|
scrutReg: string;
|
|
2194
2318
|
caseLabels: string[];
|
|
2195
2319
|
defaultLabel: string;
|
|
2320
|
+
/** What runs between the table load and the `mov pc` ({@link dispatchExtraRefusal}). */
|
|
2321
|
+
extras: Instr[];
|
|
2322
|
+
/** What the dispatch idiom leaves in each register it writes (index, table pointer, target;
|
|
2323
|
+
* jump-table.ts `idiomLeaves`). The `add` rewrites the index the `lsl` scaled, so every one is a
|
|
2324
|
+
* marker here: an elided dispatch writes them at the entry of each case target (`caseEntries`), a
|
|
2325
|
+
* kept one at its own block's end, save the ones its extras rewrote. */
|
|
2326
|
+
leaves: ReadonlyMap<string, IdiomLeft>;
|
|
2196
2327
|
}
|
|
2197
2328
|
|
|
2198
2329
|
// Accept a data-processing mnemonic in either the pre-UAL (`lsl`, `add`) or UAL (`lsls`, `adds`)
|
|
@@ -2235,9 +2366,10 @@ interface JumpTable {
|
|
|
2235
2366
|
// Inside the dispatch block the distinction is additionally UNOBSERVABLE: the block computes
|
|
2236
2367
|
// `table_base + index*4`, loads the target and writes `pc`. Nothing between the `lsl` and the
|
|
2237
2368
|
// `mov pc` reads NZCV, no path leaves the block by falling through, and on a successful recovery the
|
|
2238
|
-
//
|
|
2239
|
-
// in `bounds`, whose `cmp`/`bhi`/`bls`
|
|
2240
|
-
// since `lsl rD, rS, #imm` is
|
|
2369
|
+
// idiom is never lifted (the block is elided, or keeps only the code `dispatchExtraRefusal` admits)
|
|
2370
|
+
// — the bounds test that does feed a conditional branch lives in `bounds`, whose `cmp`/`bhi`/`bls`
|
|
2371
|
+
// this function matches by exact name. Doubly moot in fact, since `lsl rD, rS, #imm` is
|
|
2372
|
+
// low-register-only, so the add here is always the low-register form.
|
|
2241
2373
|
const isDataOp = (mn: string, base: 'lsl' | 'add'): boolean => mn === base || mn === `${base}s`;
|
|
2242
2374
|
|
|
2243
2375
|
function recoverJumpTable(
|
|
@@ -2246,6 +2378,8 @@ function recoverJumpTable(
|
|
|
2246
2378
|
dataWords: Map<string, string[]>,
|
|
2247
2379
|
nonWordData: Map<string, string>,
|
|
2248
2380
|
blockLabels: Set<string>,
|
|
2381
|
+
aliasOf: ReadonlyMap<string, string>,
|
|
2382
|
+
refused: (why: string) => void,
|
|
2249
2383
|
longDefault?: string,
|
|
2250
2384
|
): JumpTable | null {
|
|
2251
2385
|
// bounds: last two instrs are `cmp rX,#M` then the out-of-range guard, in one of two spellings.
|
|
@@ -2290,12 +2424,15 @@ function recoverJumpTable(
|
|
|
2290
2424
|
return null; // a bound that admits no case at all is not a dispatch — fail closed
|
|
2291
2425
|
}
|
|
2292
2426
|
|
|
2293
|
-
// disp:
|
|
2427
|
+
// disp: the 5-op idiom, threading a single index register from `lsl rY,rX,#2`, with any
|
|
2428
|
+
// instructions between the table load and the `mov pc` (`extras`, judged below).
|
|
2294
2429
|
const d = disp.instrs;
|
|
2295
|
-
if (d.length
|
|
2430
|
+
if (d.length < 5) {
|
|
2296
2431
|
return null;
|
|
2297
2432
|
}
|
|
2298
|
-
const [lsl, ldrP, add, ldrV
|
|
2433
|
+
const [lsl, ldrP, add, ldrV] = d;
|
|
2434
|
+
const movpc = d[d.length - 1];
|
|
2435
|
+
const extras = d.slice(4, -1);
|
|
2299
2436
|
if (!isDataOp(lsl.mnemonic, 'lsl') || lsl.ops[1] !== scrutReg || !immEq(lsl.ops[2], 2)) {
|
|
2300
2437
|
return null;
|
|
2301
2438
|
}
|
|
@@ -2344,12 +2481,13 @@ function recoverJumpTable(
|
|
|
2344
2481
|
if (movpc.mnemonic !== 'mov' || movpc.ops[0] !== 'pc') {
|
|
2345
2482
|
return null;
|
|
2346
2483
|
}
|
|
2484
|
+
const extraRefusal = dispatchExtraRefusal(extras, ldrV.ops[0], [idxReg, ptrReg, ldrV.ops[0]]);
|
|
2485
|
+
if (extraRefusal !== null) {
|
|
2486
|
+
refused(extraRefusal);
|
|
2487
|
+
return null;
|
|
2488
|
+
}
|
|
2347
2489
|
|
|
2348
2490
|
// Read the table: the ldr loads a POINTER word (PTR: .word TABLE); the table is TABLE: .word C0…
|
|
2349
|
-
// Note the case labels are matched against `blockLabels` as WRITTEN: the adjacent-label aliasing in
|
|
2350
|
-
// `decode` rewrites branch operands, not `.word` entries, so a table naming an aliased label would
|
|
2351
|
-
// decline here rather than dispatch anywhere. Loud, and no corpus instance — left as a known edge
|
|
2352
|
-
// rather than fixed speculatively.
|
|
2353
2491
|
//
|
|
2354
2492
|
// The pointer word is addressed the same way every other pool load in this frontend is —
|
|
2355
2493
|
// `LABEL[+N]`, selecting word N/4 — because a literal pool is a POOL: agbcc packs the dispatch
|
|
@@ -2373,16 +2511,71 @@ function recoverJumpTable(
|
|
|
2373
2511
|
if (ptrOff % 4 !== 0 || ptrOff / 4 >= ptrWords.length) {
|
|
2374
2512
|
return null; // misaligned or past the end of the pool — not a word this pool holds
|
|
2375
2513
|
}
|
|
2376
|
-
const
|
|
2377
|
-
if (!
|
|
2514
|
+
const written = recordedWords(ptrWords[ptrOff / 4].trim(), dataWords, nonWordData);
|
|
2515
|
+
if (!written || written.length !== n) {
|
|
2378
2516
|
return null;
|
|
2379
2517
|
} // table length must equal the bound
|
|
2518
|
+
// An entry naming a label that shares its instruction with a later one dispatches to that one, as
|
|
2519
|
+
// a branch to it does (`decode`'s `aliasOf`): jump.c cross-jumps a case arm identical to the
|
|
2520
|
+
// default's onto the default's first instruction and leaves the case's label standing on it. Only
|
|
2521
|
+
// the TABLE's entries are read through it — a pool word naming a code label elsewhere is an
|
|
2522
|
+
// address constant, not a transfer — and `aliasOf` already holds no data label on either side, so
|
|
2523
|
+
// an entry naming a label that fronts data, or one left trailing, still declines below.
|
|
2524
|
+
const caseLabels = written.map((l) => aliasOf.get(l) ?? l);
|
|
2380
2525
|
// Every case target and the default must resolve to a real decoded block; a label that is an
|
|
2381
2526
|
// expression (`.L4+4`) or points outside the function would otherwise crash later — decline cleanly.
|
|
2382
2527
|
if (!blockLabels.has(defaultLabel) || caseLabels.some((l) => !blockLabels.has(l))) {
|
|
2383
2528
|
return null;
|
|
2384
2529
|
}
|
|
2385
|
-
|
|
2530
|
+
const leaves = idiomLeaves([idxReg, ptrReg, ldrV.ops[0]].map((reg) => ({ reg, scaled: false })));
|
|
2531
|
+
return { scrutReg, caseLabels, defaultLabel, extras, leaves };
|
|
2532
|
+
}
|
|
2533
|
+
|
|
2534
|
+
// The instructions an agbcc dispatch may carry between the table load and the `mov pc`. gcse's
|
|
2535
|
+
// PRE (`insert_insn_end_bb`, gcse.c:4231-4236) places an expression a later block needs "just before
|
|
2536
|
+
// the tablejump" — thumb.md's `tablejump` is the `mov pc` alone, so after the load. They run on every
|
|
2537
|
+
// dispatch and on nothing else, so they stay in a block of their own between the bounds test and the
|
|
2538
|
+
// switch (`readThumbText`), which switches on the scrutinee as the dispatch's first instruction read
|
|
2539
|
+
// it (`lowerTerminator`). PRE inserts `reg = expr` and nothing more, so the instructions accepted are
|
|
2540
|
+
// the ones `mayWriteReg` knows write their first operand — the data-processing ops and the single
|
|
2541
|
+
// loads — less the two that read the carry; anything else refuses:
|
|
2542
|
+
// - a store, a call, a transfer, a flag READ (`adc`/`sbc`), or any other mnemonic;
|
|
2543
|
+
// - a write to the loaded target (it is what `mov pc` jumps through), or to sp/pc/lr. A write to
|
|
2544
|
+
// the scrutinee is admitted: the switch does not read it after the extras, and the case blocks
|
|
2545
|
+
// read what the extras left there on the hardware too. lr is refused as the register a case's
|
|
2546
|
+
// `bx lr` returns through: a write to it here makes that `bx lr` a jump, which only the return
|
|
2547
|
+
// proof (`refuseReturnsNotThroughCallersLr`) would otherwise have to catch. PRE never inserts
|
|
2548
|
+
// an lr write, so the refusal costs no compiled dispatch;
|
|
2549
|
+
// - a read of a register the idiom wrote (index, table pointer, target) before an extra rewrote it:
|
|
2550
|
+
// the idiom is not lifted, so such a read would see the value from before the dispatch.
|
|
2551
|
+
// The reason is returned so the decline can name it (`readThumbText`'s indirect-jump refusal).
|
|
2552
|
+
const READS_CARRY = /^(?:adc|sbc)s?$/;
|
|
2553
|
+
/** Besides a load (`SINGLE_LOAD`), the forms that write their destination without reading it. */
|
|
2554
|
+
const DEST_ONLY = /^(?:mov|mvn|neg)s?$/;
|
|
2555
|
+
function dispatchExtraRefusal(
|
|
2556
|
+
extras: readonly Instr[],
|
|
2557
|
+
targetReg: string,
|
|
2558
|
+
idiomRegs: readonly string[],
|
|
2559
|
+
): string | null {
|
|
2560
|
+
const stale = new Set(idiomRegs);
|
|
2561
|
+
for (const ins of extras) {
|
|
2562
|
+
const mn = ins.mnemonic;
|
|
2563
|
+
const say = `'${mn} ${ins.ops.join(', ')}'`;
|
|
2564
|
+
if (!(SINGLE_LOAD.test(mn) || WRITES_OPERAND_0.test(mn)) || READS_CARRY.test(mn) || ins.ops.length < 2) {
|
|
2565
|
+
return `${say} is not a register computation or a load`;
|
|
2566
|
+
}
|
|
2567
|
+
const dest = ins.ops[0];
|
|
2568
|
+
if ([targetReg, 'sp', 'r13', 'pc', 'r15', 'lr', 'r14'].includes(dest)) {
|
|
2569
|
+
return `${say} writes the register the dispatch needs, '${dest}'`;
|
|
2570
|
+
}
|
|
2571
|
+
const reads = ins.ops.length > 2 || SINGLE_LOAD.test(mn) || DEST_ONLY.test(mn) ? ins.ops.slice(1) : ins.ops;
|
|
2572
|
+
const read = [...stale].find((r) => mentionsReg({ ops: reads }, r));
|
|
2573
|
+
if (read !== undefined) {
|
|
2574
|
+
return `${say} reads '${read}', which the unlifted dispatch idiom wrote`;
|
|
2575
|
+
}
|
|
2576
|
+
stale.delete(dest);
|
|
2577
|
+
}
|
|
2578
|
+
return null;
|
|
2386
2579
|
}
|
|
2387
2580
|
|
|
2388
2581
|
/** Lift decoded asm → an L1 Fn with block-argument SSA. `prototypes` supplies each callee's
|
|
@@ -2402,16 +2595,12 @@ export function lift(
|
|
|
2402
2595
|
prototypes: Prototypes = {},
|
|
2403
2596
|
asmData?: AsmData,
|
|
2404
2597
|
symbols?: SymbolMap,
|
|
2598
|
+
context?: ContextGlobals,
|
|
2599
|
+
options: LiftOptions = {},
|
|
2405
2600
|
): Fn {
|
|
2406
|
-
|
|
2407
|
-
|
|
2408
|
-
|
|
2409
|
-
}
|
|
2410
|
-
const again = liftOnce(name, asm, target, prototypes, asmData, symbols, first.oneObject);
|
|
2411
|
-
if ('oneObject' in again) {
|
|
2412
|
-
throw new Error(`internal: the frame-object audit of '${name}' asked for a second relift`);
|
|
2413
|
-
}
|
|
2414
|
-
return again;
|
|
2601
|
+
return liftWithRelift(name, (oneObject, homesReleased) =>
|
|
2602
|
+
liftOnce(name, asm, target, prototypes, asmData, symbols, context, oneObject, homesReleased, options),
|
|
2603
|
+
);
|
|
2415
2604
|
}
|
|
2416
2605
|
|
|
2417
2606
|
/** ONE LIFT, AS STAGES RUN IN THEIR REFUSAL ORDER: every stage before the fill (`thumbFillOf`: the
|
|
@@ -2430,9 +2619,21 @@ function liftOnce(
|
|
|
2430
2619
|
prototypes: Prototypes,
|
|
2431
2620
|
_asmData: AsmData | undefined,
|
|
2432
2621
|
symbols: SymbolMap | undefined,
|
|
2622
|
+
context: ContextGlobals | undefined,
|
|
2433
2623
|
oneObject: FrameRange | undefined,
|
|
2624
|
+
homesReleased: boolean,
|
|
2625
|
+
options: Pick<
|
|
2626
|
+
LiftOptions,
|
|
2627
|
+
| 'contextOwnWords'
|
|
2628
|
+
| 'contextOwnProto'
|
|
2629
|
+
| 'ownMemoryReturn'
|
|
2630
|
+
| 'calleeBodyBound'
|
|
2631
|
+
| 'calleeStackWords'
|
|
2632
|
+
| 'stackSized'
|
|
2633
|
+
| 'entryReadCalls'
|
|
2634
|
+
>,
|
|
2434
2635
|
): Fn | FrameObjectRelift {
|
|
2435
|
-
const fill = thumbFillOf(name, asm, target, prototypes, symbols, oneObject);
|
|
2636
|
+
const fill = thumbFillOf(name, asm, target, prototypes, symbols, oneObject, options, homesReleased);
|
|
2436
2637
|
const { cfg, frame, ssa, pairs, statics } = fill;
|
|
2437
2638
|
const { fn, irBlocks, paramReg } = ssa;
|
|
2438
2639
|
|
|
@@ -2441,25 +2642,61 @@ function liftOnce(
|
|
|
2441
2642
|
fillThumbBlock(fill, ab, bi);
|
|
2442
2643
|
ssa.markFilled(bi);
|
|
2443
2644
|
});
|
|
2645
|
+
refuseStaleIdiomReads(
|
|
2646
|
+
name,
|
|
2647
|
+
irBlocks,
|
|
2648
|
+
fill.staleIdiomValues,
|
|
2649
|
+
ssa,
|
|
2650
|
+
prototypes[name] ?? options.contextOwnProto?.proto,
|
|
2651
|
+
(m) => {
|
|
2652
|
+
throw new FrontendUnsupportedError(m);
|
|
2653
|
+
},
|
|
2654
|
+
fill.calls,
|
|
2655
|
+
declaresVoidReturn(prototypes[name]),
|
|
2656
|
+
);
|
|
2657
|
+
// After the fill, so a block's own refusal (the frame's, the operands') keeps its place in the
|
|
2658
|
+
// refusal order: the proof reads only the text and the CFG, and changes nothing.
|
|
2659
|
+
refuseReturnsNotThroughCallersLr(name, cfg);
|
|
2660
|
+
// The fill reads a `pop` as transparent, so a callee-saved register one fills with another
|
|
2661
|
+
// register's word is a write no op shows: refused here, after the return proof, which judges the
|
|
2662
|
+
// same words for lr and has its own answer for a pop that takes lr's.
|
|
2663
|
+
const [foreign] = fill.poppedForeign;
|
|
2664
|
+
if (foreign !== undefined) {
|
|
2665
|
+
const [reg, at] = foreign;
|
|
2666
|
+
throw new FrontendUnsupportedError(
|
|
2667
|
+
`cannot lift '${name}': '${at.mnemonic} ${at.ops.join(', ')}' loads a word that is not ${reg}'s own save ` +
|
|
2668
|
+
`into callee-saved ${reg}, and ${reg} reaches a return so, unrestored — the caller sees the write, which ` +
|
|
2669
|
+
'no C statement spells',
|
|
2670
|
+
);
|
|
2671
|
+
}
|
|
2444
2672
|
|
|
2445
2673
|
pairs.refuseWordReturns();
|
|
2446
2674
|
// An argument register by its place in `argRegs`, an incoming stack word by its index after them.
|
|
2447
2675
|
// One word per argument, which is what this frontend assumes everywhere: an 8-byte argument,
|
|
2448
2676
|
// which AAPCS may align into r1 or straddle across r3 and the stack, would break the index-slot
|
|
2449
2677
|
// correspondence, and recovering one is its own capability.
|
|
2450
|
-
const
|
|
2451
|
-
const i = target.argRegs.indexOf(key);
|
|
2452
|
-
return i >= 0 ? i : stackArgIndex(key);
|
|
2453
|
-
};
|
|
2454
|
-
const argSlots: ArgSlots = {
|
|
2455
|
-
slotOf,
|
|
2456
|
-
holes: (readKeys) =>
|
|
2457
|
-
Array.from({ length: Math.max(0, ...readKeys.map((k) => slotOf(k) ?? -1)) }, (_, k) =>
|
|
2458
|
-
k < target.argRegs.length ? target.argRegs[k] : stackArgKey(k),
|
|
2459
|
-
),
|
|
2460
|
-
};
|
|
2678
|
+
const argSlots = stackArgSlots(registerArgSlots(target.argRegs), target.argRegs);
|
|
2461
2679
|
mintArgSlotHoles(ssa, cfg.preds[0].length > 0, argSlots);
|
|
2462
2680
|
ssa.finish();
|
|
2681
|
+
// A scratch register a `pop` fills with another word is a copy the fill never makes: `push {r1};
|
|
2682
|
+
// pop {r0}; bx lr` returns a1, and read by name it would lift `return a0`. Asked after the SSA's own
|
|
2683
|
+
// refusals, which name a register misused across its save before the copy that misuse makes. A
|
|
2684
|
+
// return hands r0 back unless the function's own declaration says it returns nothing.
|
|
2685
|
+
const declaredVoid = (prototypes[name] ?? options.contextOwnProto?.proto)?.returnsVoid === true;
|
|
2686
|
+
const [copy] = [...fill.poppedCopies, ...(declaredVoid ? [] : fill.poppedReturns)];
|
|
2687
|
+
if (copy !== undefined) {
|
|
2688
|
+
const [reg, at] = copy;
|
|
2689
|
+
throw new FrontendUnsupportedError(
|
|
2690
|
+
`cannot lift '${name}': '${at.mnemonic} ${at.ops.join(', ')}' loads a word that is not ${reg}'s own ` +
|
|
2691
|
+
`into ${reg}, which is then used as a value — a pop is read as restoring each register it names, ` +
|
|
2692
|
+
'so the copy it makes is not modelled',
|
|
2693
|
+
);
|
|
2694
|
+
}
|
|
2695
|
+
|
|
2696
|
+
// A struct passed by value becomes the one object its words hold (call-plan.ts `foldByValueArgs`)
|
|
2697
|
+
// before the audit judges the frame objects, that one among them.
|
|
2698
|
+
foldByValueArgs(name, irBlocks, fill.calls, { symbols, context });
|
|
2699
|
+
nameMergedSretObjects(irBlocks);
|
|
2463
2700
|
|
|
2464
2701
|
// Prove every `laddr` this function emitted really does name storage of this function's own, at
|
|
2465
2702
|
// a shape the machine states, or decline (auditFrameObjects); an accepted function then gets the
|
|
@@ -2469,12 +2706,15 @@ function liftOnce(
|
|
|
2469
2706
|
irBlocks,
|
|
2470
2707
|
...frame.partition,
|
|
2471
2708
|
usedSlotOffsets: fill.frameUses.usedSlotOffsets,
|
|
2709
|
+
loadedWords: frame.loadedWords,
|
|
2472
2710
|
capturedObjectIsTheWholeFrame: frame.capturedObjectIsTheWholeFrame,
|
|
2711
|
+
escapesReliedOn: frame.escapesReliedOn,
|
|
2473
2712
|
movedCaptures: fill.frameUses.movedCaptures,
|
|
2474
2713
|
returnsWithoutHiddenPointer: fill.calls.returnsWithoutHiddenPointer,
|
|
2475
2714
|
symbols,
|
|
2476
2715
|
target,
|
|
2477
2716
|
oneObject,
|
|
2717
|
+
releasableHomes: frame.releasableHomes,
|
|
2478
2718
|
});
|
|
2479
2719
|
if (verdict !== undefined && 'oneObject' in verdict) {
|
|
2480
2720
|
return verdict;
|
|
@@ -2482,21 +2722,31 @@ function liftOnce(
|
|
|
2482
2722
|
if (verdict !== undefined) {
|
|
2483
2723
|
pinDeviceAccesses(irBlocks, verdict, target, symbols);
|
|
2484
2724
|
}
|
|
2485
|
-
refuseSretOutsideFrame(name, irBlocks);
|
|
2725
|
+
refuseSretOutsideFrame(name, irBlocks, target);
|
|
2726
|
+
pinDeadReads(irBlocks, target, {
|
|
2727
|
+
name,
|
|
2728
|
+
own: prototypes[name],
|
|
2729
|
+
atWordReturn: pairs.unreadHigh,
|
|
2730
|
+
atUndeclaredCall: fill.calls.heldAtUndeclaredCall,
|
|
2731
|
+
compared: fill.flags.compared,
|
|
2732
|
+
spilled: ssa.frameWrites,
|
|
2733
|
+
declarations: { symbols, context },
|
|
2734
|
+
});
|
|
2486
2735
|
|
|
2487
2736
|
abiSortEntryParams(irBlocks[0], cfg.preds[0].length > 0, paramReg, argSlots);
|
|
2488
2737
|
fn.localObjects = statics.finish();
|
|
2489
2738
|
return fn;
|
|
2490
2739
|
}
|
|
2491
2740
|
|
|
2492
|
-
/** THE TEXT A LIFT READS: the decoded blocks with every recovered jump table's dispatch elided
|
|
2493
|
-
*
|
|
2494
|
-
* time this exists. */
|
|
2741
|
+
/** THE TEXT A LIFT READS: the decoded blocks with every recovered jump table's dispatch elided
|
|
2742
|
+
* (or, where it carries code of its own, cut down to that code), and the data its pool loads
|
|
2743
|
+
* read. Every control transfer the CFG cannot model is refused by the time this exists. */
|
|
2495
2744
|
interface ThumbText {
|
|
2496
2745
|
/** The blocks the CFG is built from — read by `thumbCfg` alone; every later stage indexes
|
|
2497
2746
|
* `ThumbCfg.asmBlocks`. */
|
|
2498
2747
|
readonly blocks: readonly AsmBlock[];
|
|
2499
|
-
/** A recovered jump table, by its bounds block
|
|
2748
|
+
/** A recovered jump table, by the block that emits its switch: the bounds block, or a kept
|
|
2749
|
+
* dispatch block. */
|
|
2500
2750
|
readonly tables: ReadonlyMap<AsmBlock, JumpTable>;
|
|
2501
2751
|
readonly dataWords: ReadonlyMap<string, string[]>;
|
|
2502
2752
|
readonly nonWordData: ReadonlyMap<string, string>;
|
|
@@ -2505,14 +2755,319 @@ interface ThumbText {
|
|
|
2505
2755
|
readonly poolNamesSymbols: boolean;
|
|
2506
2756
|
}
|
|
2507
2757
|
|
|
2508
|
-
|
|
2758
|
+
// One spelling per register for the return proof below, which tracks registers by identity:
|
|
2759
|
+
// `r14` and `lr` are one register, and so are `ip` and `r12`.
|
|
2760
|
+
const REG_ALIAS: Readonly<Record<string, string>> = {
|
|
2761
|
+
sp: 'r13',
|
|
2762
|
+
lr: 'r14',
|
|
2763
|
+
pc: 'r15',
|
|
2764
|
+
sb: 'r9',
|
|
2765
|
+
sl: 'r10',
|
|
2766
|
+
fp: 'r11',
|
|
2767
|
+
ip: 'r12',
|
|
2768
|
+
};
|
|
2769
|
+
const canonReg = (r: string): string | null => {
|
|
2770
|
+
const t = r.replace(/!$/, '');
|
|
2771
|
+
return /^r(\d|1[0-5])$/.test(t) ? t : (REG_ALIAS[t] ?? null);
|
|
2772
|
+
};
|
|
2773
|
+
const regIndex = (r: string): number => Number(r.slice(1));
|
|
2774
|
+
/** A push/pop/ldm list in the order the machine moves it, canonical names (`canonReg`): by register
|
|
2775
|
+
* number, lowest address first. Null where a name is not a register. */
|
|
2776
|
+
const byRegNumber = (list: readonly string[]): string[] | null => {
|
|
2777
|
+
const regs = list.map(canonReg);
|
|
2778
|
+
return regs.some((r) => r === null) ? null : (regs as string[]).sort((x, y) => regIndex(x) - regIndex(y));
|
|
2779
|
+
};
|
|
2780
|
+
// Registers `ins` may write, besides sp and besides a `pop`'s list (the proof reads both itself).
|
|
2781
|
+
// OVER-approximate: naming a register here that the instruction does not write costs a decline,
|
|
2782
|
+
// and missing one costs a wrong return. 'all' for a call, and for anything this cannot read.
|
|
2783
|
+
const NO_REG_WRITE: ReadonlySet<string> = new Set([
|
|
2784
|
+
'str',
|
|
2785
|
+
'strb',
|
|
2786
|
+
'strh',
|
|
2787
|
+
'push',
|
|
2788
|
+
'cmp',
|
|
2789
|
+
'cmn',
|
|
2790
|
+
'tst',
|
|
2791
|
+
'nop',
|
|
2792
|
+
'b',
|
|
2793
|
+
'bx',
|
|
2794
|
+
]);
|
|
2795
|
+
function regWritesOf(ins: Instr): readonly string[] | 'all' {
|
|
2796
|
+
const mn = ins.mnemonic;
|
|
2797
|
+
if (NO_REG_WRITE.has(mn) || COND_OPCODE[mn]) {
|
|
2798
|
+
return [];
|
|
2799
|
+
}
|
|
2800
|
+
if (mn === 'bl' || mn === 'blx' || mn === 'swi') {
|
|
2801
|
+
return 'all';
|
|
2802
|
+
}
|
|
2803
|
+
if (/^(ldm|stm)/.test(mn)) {
|
|
2804
|
+
const base = canonReg(ins.ops[0] ?? '');
|
|
2805
|
+
const list = /^ldm/.test(mn) ? regListOf(ins.ops.slice(1)) : [];
|
|
2806
|
+
if (base === null || list === null) {
|
|
2807
|
+
return 'all';
|
|
2808
|
+
}
|
|
2809
|
+
const listed = list.map(canonReg);
|
|
2810
|
+
return listed.some((r) => r === null) ? 'all' : [base, ...(listed as string[])];
|
|
2811
|
+
}
|
|
2812
|
+
const dest = canonReg(ins.ops[0] ?? '');
|
|
2813
|
+
return dest === null ? 'all' : [dest];
|
|
2814
|
+
}
|
|
2815
|
+
|
|
2816
|
+
/** What the return proof knows at one point of the function: the frame depth `makeFrameWalk`
|
|
2817
|
+
* measures, which word of the frame holds the caller's lr (`slot`, as the depth at which it sits:
|
|
2818
|
+
* the word at entry sp − slot), and which registers hold it. */
|
|
2819
|
+
interface LrState {
|
|
2820
|
+
readonly depth: number | null;
|
|
2821
|
+
readonly slot: number | null;
|
|
2822
|
+
readonly regs: ReadonlySet<string>;
|
|
2823
|
+
}
|
|
2824
|
+
|
|
2825
|
+
/** EVERY RETURN OF THIS FUNCTION RETURNS THROUGH THE CALLER'S lr, or the lift declines.
|
|
2826
|
+
*
|
|
2827
|
+
* `classifyXfer` reads `bx rN`, `pop {…, pc}` and `mov pc, lr` as returns from the instruction
|
|
2828
|
+
* alone, and each is one only when the word it transfers to is the lr this function was called
|
|
2829
|
+
* with: in lr itself, in the frame word the prologue's `push` saved it to, or in a register a
|
|
2830
|
+
* `pop` loaded from that word. Which word a `pop` reads is a question about the FRAME, not about
|
|
2831
|
+
* register names — a `pop` fills its list in register-number order from the lowest address up —
|
|
2832
|
+
* so this walks the frame (`makeFrameWalk`) over the CFG and tracks where the caller's lr is:
|
|
2833
|
+
* • a `push` that saves a register holding it records the word it went to;
|
|
2834
|
+
* • a `pop` whose k-th register (by number) reads that word puts it in that register, and every
|
|
2835
|
+
* other register it fills loses it;
|
|
2836
|
+
* • the word stops holding it when sp rises past it (a `pop` or `add sp` releases it), a
|
|
2837
|
+
* `str` through sp overwrites it, or the depth stops being one number;
|
|
2838
|
+
* • a register stops holding it when anything writes the register (a call writes them all);
|
|
2839
|
+
* a `mov` copies it.
|
|
2840
|
+
* Paths meet by agreement: a depth or a word the predecessors disagree on is unknown, and a
|
|
2841
|
+
* register holds lr only if it does on every path in.
|
|
2842
|
+
*
|
|
2843
|
+
* So `push {r4, lr}; pop {r0}; bx r0` (the saved r4), `push {lr}; sub sp, #8; pop {r1}; bx r1` (a
|
|
2844
|
+
* local), `pop {r4, r1}` after `push {r4, lr}` (r1 is filled first, from r4's word) and
|
|
2845
|
+
* `mov r0, #7; bx r3` all decline, where agbcc's `pop {r4}; pop {r0}; bx r0` and its variadic
|
|
2846
|
+
* `push {r0-r3}; push {lr}; … pop {r3}; add sp, sp, #0x10; bx r3` are returns. It is also what the
|
|
2847
|
+
* far-jump relay's lowering needs and does not itself prove (`lowerFarJumpRelays`): with every
|
|
2848
|
+
* return reading the CALLER's lr, the lr a relay writes cannot be what a return jumps to.
|
|
2849
|
+
*
|
|
2850
|
+
* `bx lr` and `mov pc, lr` are asked the same question: lr itself must still hold the caller's lr,
|
|
2851
|
+
* so an instruction that writes lr by name before one (`mov lr, r4`, `pop {lr}`, `ldr lr, …`)
|
|
2852
|
+
* declines. Read by the register's name instead, `push {r4, lr}; ldr r4, =g; mov lr, r4; pop {r4};
|
|
2853
|
+
* bx lr` — a tail jump to g with every argument register live — would lift as a return, and a
|
|
2854
|
+
* caller whose call to it no declaration lays out would take that lift's one parameter as the
|
|
2855
|
+
* callee's whole arity (frontend/callee-body.ts). A compiler returns through the word it saved lr in, so no
|
|
2856
|
+
* compiled function reaches the refusal.
|
|
2857
|
+
*
|
|
2858
|
+
* NOT ASKED: a CALL's write of lr. `bl g; bx lr` returns to the instruction after the call on the
|
|
2859
|
+
* hardware, and lifts as `g(); return;`. No compiler emits it (one saves lr before it calls), and
|
|
2860
|
+
* many call tests' fixtures are written so, so a call leaves lr held. The far-jump relay leans on
|
|
2861
|
+
* neither: its lowering declines any function in which an instruction other than the entry `push`
|
|
2862
|
+
* names lr.
|
|
2863
|
+
*
|
|
2864
|
+
* Blocks no path from the entry reaches are not asked: they never run. */
|
|
2865
|
+
function refuseReturnsNotThroughCallersLr(name: string, cfg: ThumbCfg): void {
|
|
2866
|
+
const { asmBlocks, succs, entryReachable } = cfg;
|
|
2867
|
+
const LR = 'r14';
|
|
2868
|
+
const join = (a: LrState, b: LrState): LrState => {
|
|
2869
|
+
const depth = a.depth !== null && a.depth === b.depth ? a.depth : null;
|
|
2870
|
+
return {
|
|
2871
|
+
depth,
|
|
2872
|
+
slot: depth !== null && a.slot !== null && a.slot === b.slot ? a.slot : null,
|
|
2873
|
+
regs: new Set([...a.regs].filter((r) => b.regs.has(r))),
|
|
2874
|
+
};
|
|
2875
|
+
};
|
|
2876
|
+
const same = (a: LrState, b: LrState) =>
|
|
2877
|
+
a.depth === b.depth && a.slot === b.slot && a.regs.size === b.regs.size && [...a.regs].every((r) => b.regs.has(r));
|
|
2878
|
+
const refuse = (ins: Instr): never => {
|
|
2879
|
+
throw new FrontendUnsupportedError(
|
|
2880
|
+
`cannot lift '${name}': indirect/computed jump '${ins.mnemonic} ${ins.ops.join(', ')}' is not a return ` +
|
|
2881
|
+
`through the caller's lr — on some path what it jumps to is not lr, nor the frame word the prologue ` +
|
|
2882
|
+
`saved lr in — jump tables / computed gotos / register tail calls not supported`,
|
|
2883
|
+
);
|
|
2884
|
+
};
|
|
2885
|
+
|
|
2886
|
+
// Run one block from its in-state, asking each return as it is reached; the out-state.
|
|
2887
|
+
const run = (b: number, inState: LrState): LrState => {
|
|
2888
|
+
const walk = makeFrameWalk({ argRegs: [], start: inState.depth });
|
|
2889
|
+
let slot = inState.slot;
|
|
2890
|
+
const regs = new Set(inState.regs);
|
|
2891
|
+
for (const ins of asmBlocks[b].instrs) {
|
|
2892
|
+
const mn = ins.mnemonic;
|
|
2893
|
+
const depth = walk.depthNow();
|
|
2894
|
+
const xfer = classifyXfer(ins);
|
|
2895
|
+
const isPop =
|
|
2896
|
+
mn === 'pop' || ((mn === 'ldmia' || mn === 'ldmfd') && isSpReg((ins.ops[0] ?? '').replace(/!$/, '')));
|
|
2897
|
+
if (isPop) {
|
|
2898
|
+
const list = byRegNumber(regListOf(mn === 'pop' ? ins.ops : ins.ops.slice(1)) ?? []);
|
|
2899
|
+
if (list === null || list.length === 0) {
|
|
2900
|
+
if (xfer === 'return') {
|
|
2901
|
+
refuse(ins);
|
|
2902
|
+
}
|
|
2903
|
+
regs.clear();
|
|
2904
|
+
} else {
|
|
2905
|
+
list.forEach((r, k) => {
|
|
2906
|
+
if (depth !== null && slot !== null && depth - 4 * k === slot) {
|
|
2907
|
+
regs.add(r);
|
|
2908
|
+
} else {
|
|
2909
|
+
regs.delete(r);
|
|
2910
|
+
}
|
|
2911
|
+
});
|
|
2912
|
+
if (xfer === 'return' && !regs.has('r15')) {
|
|
2913
|
+
refuse(ins); // `pop {…, pc}` that does not load pc from the saved lr's word
|
|
2914
|
+
}
|
|
2915
|
+
}
|
|
2916
|
+
} else if (xfer === 'return') {
|
|
2917
|
+
// `bx rN`, `bx lr` and `mov pc, lr` alike: the register must still hold the caller's lr.
|
|
2918
|
+
const via = canonReg(mn === 'bx' ? (ins.ops[0] ?? '') : (ins.ops[1] ?? ''));
|
|
2919
|
+
if (via === null || !regs.has(via)) {
|
|
2920
|
+
refuse(ins);
|
|
2921
|
+
}
|
|
2922
|
+
} else if (mn === 'push') {
|
|
2923
|
+
const list = byRegNumber(regListOf(ins.ops) ?? []);
|
|
2924
|
+
const k = list?.findIndex((r) => regs.has(r)) ?? -1;
|
|
2925
|
+
if (list !== null && k >= 0 && depth !== null) {
|
|
2926
|
+
slot = depth + 4 * list.length - 4 * k;
|
|
2927
|
+
}
|
|
2928
|
+
} else if (/^str[bh]?$/.test(mn)) {
|
|
2929
|
+
const { base, off, regOff } = parseAddr(ins.ops[1] ?? '');
|
|
2930
|
+
if (isSpReg(base) && slot !== null) {
|
|
2931
|
+
const width = mn === 'strb' ? 1 : mn === 'strh' ? 2 : 4;
|
|
2932
|
+
const at = depth === null || regOff !== undefined ? null : depth - off; // the word's depth
|
|
2933
|
+
if (at === null || (at - width < slot && at > slot - 4)) {
|
|
2934
|
+
slot = null;
|
|
2935
|
+
}
|
|
2936
|
+
}
|
|
2937
|
+
} else if (!isSpReg(ins.ops[0])) {
|
|
2938
|
+
const writes = regWritesOf(ins);
|
|
2939
|
+
if (writes === 'all') {
|
|
2940
|
+
// a call's own write of lr is not asked (see the doc comment)
|
|
2941
|
+
const kept = /^(bl|blx|swi)$/.test(mn) && regs.has(LR);
|
|
2942
|
+
regs.clear();
|
|
2943
|
+
if (kept) {
|
|
2944
|
+
regs.add(LR);
|
|
2945
|
+
}
|
|
2946
|
+
} else {
|
|
2947
|
+
const src = /^movs?$/.test(mn) ? canonReg(ins.ops[1] ?? '') : null;
|
|
2948
|
+
const copies = src !== null && regs.has(src);
|
|
2949
|
+
writes.forEach((r) => regs.delete(r));
|
|
2950
|
+
if (copies) {
|
|
2951
|
+
regs.add(writes[0]);
|
|
2952
|
+
}
|
|
2953
|
+
}
|
|
2954
|
+
}
|
|
2955
|
+
walk.step(ins);
|
|
2956
|
+
const after = walk.depthNow();
|
|
2957
|
+
if (slot !== null && (after === null || slot > after)) {
|
|
2958
|
+
slot = null; // released, or the frame is no longer one number
|
|
2959
|
+
}
|
|
2960
|
+
}
|
|
2961
|
+
return { depth: walk.depthNow(), slot, regs };
|
|
2962
|
+
};
|
|
2963
|
+
|
|
2964
|
+
const inStates = new Map<number, LrState>([[0, { depth: 0, slot: null, regs: new Set([LR]) }]]);
|
|
2965
|
+
const work = [0];
|
|
2966
|
+
while (work.length > 0) {
|
|
2967
|
+
const b = work.pop()!;
|
|
2968
|
+
const out = run(b, inStates.get(b)!);
|
|
2969
|
+
for (const s of succs[b]) {
|
|
2970
|
+
if (!entryReachable.has(s)) {
|
|
2971
|
+
continue;
|
|
2972
|
+
}
|
|
2973
|
+
const prev = inStates.get(s);
|
|
2974
|
+
const next = prev === undefined ? out : join(prev, out);
|
|
2975
|
+
if (prev === undefined || !same(prev, next)) {
|
|
2976
|
+
inStates.set(s, next);
|
|
2977
|
+
work.push(s);
|
|
2978
|
+
}
|
|
2979
|
+
}
|
|
2980
|
+
}
|
|
2981
|
+
}
|
|
2982
|
+
|
|
2983
|
+
/** Lower every far-jump relay the target marks (`bl .Lx @far jump`, agbcc's; see
|
|
2984
|
+
* `compilerBehaviors.farJumpRelay`) in the selected function to the `b .Lx` it stands for, in
|
|
2985
|
+
* place, or decline. Only a target that declares the mark gets here with one: `decode` sets
|
|
2986
|
+
* `farJump` from that declaration alone.
|
|
2987
|
+
*
|
|
2988
|
+
* A relay and a call differ in one effect: `bl` writes lr, `b` does not. So the lowering is sound
|
|
2989
|
+
* exactly when nothing can observe the lr the relay wrote. What the mark MEANS is the compiler's
|
|
2990
|
+
* fact, declared with it. That nothing observes lr is checked here on the function's own text,
|
|
2991
|
+
* not assumed from agbcc's habit (thumb.c `far_jump_used_p` makes it push lr in any function with
|
|
2992
|
+
* a far jump, which is why every relay in practice passes):
|
|
2993
|
+
* • the entry block saves lr with a `push`, and no branch of this function names the entry
|
|
2994
|
+
* label, so that push runs once, before any relay, and saves the CALLER's lr. A recovered
|
|
2995
|
+
* jump table is the other way back to the entry, and `readThumbText` asks that once the
|
|
2996
|
+
* tables exist (`relayed`). A `.word` naming the entry is not one: `dataWords` is the whole
|
|
2997
|
+
* file's, and a function stored as a callback in another function's pool is a pointer;
|
|
2998
|
+
* • no other instruction of the function names lr at all — no `bx lr`, no `mov pc, lr`, no
|
|
2999
|
+
* `mov rX, lr`, no second `push`. So the lr the relay writes never reaches a register, and no
|
|
3000
|
+
* code can return to it: `b` dropping that write is unobservable.
|
|
3001
|
+
* That every RETURN jumps through the caller's lr — not through a word the relay's lr could have
|
|
3002
|
+
* reached, nor through a saved callee register — is the other half of the argument, and it is
|
|
3003
|
+
* asked of every Thumb function once the CFG exists (`refuseReturnsNotThroughCallersLr`), where
|
|
3004
|
+
* the frame walk says which word each `pop` reads.
|
|
3005
|
+
* The mark itself is the only thing that makes a `bl` a relay: a call thunk (`bl call_r3`, in a
|
|
3006
|
+
* function that pushes lr too) has the same shape and no mark, and declines below.
|
|
3007
|
+
*
|
|
3008
|
+
* The relay must also target a code label of THIS function that is not a declared function start:
|
|
3009
|
+
* agbcc only relays within a function, and a marked `bl` anywhere else is not its relay. */
|
|
3010
|
+
function lowerFarJumpRelays(name: string, blocks: readonly AsmBlock[], funcLabels: ReadonlySet<string>): Instr | null {
|
|
3011
|
+
const instrs = blocks.flatMap((b) => b.instrs);
|
|
3012
|
+
const relays = instrs.filter((ins) => ins.farJump);
|
|
3013
|
+
if (relays.length === 0) {
|
|
3014
|
+
return null;
|
|
3015
|
+
}
|
|
3016
|
+
const blockLabels = new Set(blocks.map((b) => b.label));
|
|
3017
|
+
// The compiler is the target's declaration, not a name this function knows: "a far-jump relay".
|
|
3018
|
+
const say = (ins: Instr) => `'${ins.asWritten ?? ins.mnemonic} ${ins.ops.join(', ')}' (a far-jump relay)`;
|
|
3019
|
+
const refuse = (ins: Instr, why: string) => {
|
|
3020
|
+
throw new FrontendUnsupportedError(
|
|
3021
|
+
`cannot lift '${name}': ${say(ins)} ${why} — lowering it to a branch would drop the lr write a \`bl\` makes`,
|
|
3022
|
+
);
|
|
3023
|
+
};
|
|
3024
|
+
for (const relay of relays) {
|
|
3025
|
+
const target = relay.ops[0];
|
|
3026
|
+
if (!blockLabels.has(target) || funcLabels.has(target)) {
|
|
3027
|
+
throw new FrontendUnsupportedError(
|
|
3028
|
+
`cannot lift '${name}': ${say(relay)} does not target a code label of this function`,
|
|
3029
|
+
);
|
|
3030
|
+
}
|
|
3031
|
+
}
|
|
3032
|
+
const namesLr = (ins: Instr) => mentionsReg(ins, 'lr') || mentionsReg(ins, 'r14');
|
|
3033
|
+
const entry = blocks[0];
|
|
3034
|
+
const save = entry?.instrs.find(namesLr);
|
|
3035
|
+
if (!save || save.mnemonic !== 'push' || !(regListOf(save.ops) ?? []).some((r) => r === 'lr' || r === 'r14')) {
|
|
3036
|
+
refuse(relays[0], 'is in a function whose entry block does not save lr with a `push`');
|
|
3037
|
+
}
|
|
3038
|
+
if (instrs.some((ins) => classifyXfer(ins) !== null && ins.ops.includes(entry.label))) {
|
|
3039
|
+
refuse(relays[0], `is in a function that branches back to its entry '${entry.label}', past the lr save`);
|
|
3040
|
+
}
|
|
3041
|
+
const reader = instrs.find((ins) => ins !== save && namesLr(ins));
|
|
3042
|
+
if (reader) {
|
|
3043
|
+
refuse(relays[0], `is in a function that names lr after saving it ('${reader.mnemonic} ${reader.ops.join(', ')}')`);
|
|
3044
|
+
}
|
|
3045
|
+
for (const relay of relays) {
|
|
3046
|
+
relay.mnemonic = 'b';
|
|
3047
|
+
relay.asWritten = 'bl';
|
|
3048
|
+
}
|
|
3049
|
+
return relays[0];
|
|
3050
|
+
}
|
|
3051
|
+
|
|
3052
|
+
/** `farJumpMark` is the target's `compilerBehaviors.farJumpRelay` comment; absent, no `bl` is a
|
|
3053
|
+
* relay, and one to a label of this function declines below. */
|
|
3054
|
+
function readThumbText(name: string, asm: string, farJumpMark?: RegExp): ThumbText {
|
|
2509
3055
|
assertInputFormat('thumb', 'gnu-as', asm);
|
|
2510
|
-
const { blocks: rawBlocks, dataWords, nonWordData, funcLabels } = decode(name, asm);
|
|
3056
|
+
const { blocks: rawBlocks, dataWords, nonWordData, funcLabels, aliasOf } = decode(name, asm, farJumpMark);
|
|
3057
|
+
// Everything below keys a register by its spelling, so a function that writes one two ways
|
|
3058
|
+
// (`sl` and `r10`) is read in one of them.
|
|
3059
|
+
unifyRegisterSpellings(
|
|
3060
|
+
rawBlocks.flatMap((b) => b.instrs),
|
|
3061
|
+
ARM_REGISTER_ALIASES,
|
|
3062
|
+
);
|
|
3063
|
+
// Before anything below reads a `b`: the jump-table recovery counts branch references, and a
|
|
3064
|
+
// relay is one.
|
|
3065
|
+
const relayed = lowerFarJumpRelays(name, rawBlocks, funcLabels);
|
|
2511
3066
|
|
|
2512
3067
|
// Regime B: recover agbcc jump tables. A dispatch block (`mov pc, rN`) plus its bounds
|
|
2513
3068
|
// predecessor (`cmp; bhi DEF`) collapse into a `switch_br` emitted from the BOUNDS block; the
|
|
2514
|
-
// dispatch block is ELIDED from the CFG. A `mov pc` that
|
|
2515
|
-
// to the loud-fail below.
|
|
3069
|
+
// dispatch block is ELIDED from the CFG — or kept, when it carries code (below). A `mov pc` that
|
|
3070
|
+
// is NOT a recognised table falls through to the loud-fail below.
|
|
2516
3071
|
const blockLabels = new Set(rawBlocks.map((b) => b.label));
|
|
2517
3072
|
const poolNamesSymbols = poolNamesASymbol(dataWords, nonWordData, blockLabels);
|
|
2518
3073
|
// Any label referenced as a branch target (so we can tell if an elided dispatch block has a SECOND
|
|
@@ -2528,8 +3083,25 @@ function readThumbText(name: string, asm: string): ThumbText {
|
|
|
2528
3083
|
}
|
|
2529
3084
|
}
|
|
2530
3085
|
}
|
|
2531
|
-
const tables = new Map<AsmBlock, JumpTable>(); //
|
|
2532
|
-
const elided = new Set<AsmBlock>(); // dispatch
|
|
3086
|
+
const tables = new Map<AsmBlock, JumpTable>(); // the block emitting the switch → recovered table
|
|
3087
|
+
const elided = new Set<AsmBlock>(); // dispatch blocks removed from the CFG
|
|
3088
|
+
const relays = new Set<AsmBlock>(); // long-jump `b DEF` blocks removed with an elided dispatch
|
|
3089
|
+
// A dispatch carrying code of its own (`JumpTable.extras`) is not elided: it stays as the block the
|
|
3090
|
+
// bounds test leads to, holding only that code, and the switch is emitted from it. The bounds test
|
|
3091
|
+
// and the long-jump `b DEF` keep their ordinary edges, so the code runs on the dispatched path and
|
|
3092
|
+
// on no other.
|
|
3093
|
+
const kept = new Map<AsmBlock, AsmBlock>();
|
|
3094
|
+
const keepDispatch = (d: AsmBlock, jt: JumpTable): boolean => {
|
|
3095
|
+
if (jt.extras.length === 0) {
|
|
3096
|
+
return false;
|
|
3097
|
+
}
|
|
3098
|
+
const own = { ...d, instrs: jt.extras };
|
|
3099
|
+
kept.set(d, own);
|
|
3100
|
+
tables.set(own, jt);
|
|
3101
|
+
return true;
|
|
3102
|
+
};
|
|
3103
|
+
// Why a dispatch's table was recognised and then refused, for the indirect-jump decline below.
|
|
3104
|
+
const tableRefusals = new Map<AsmBlock, string>();
|
|
2533
3105
|
rawBlocks.forEach((d, i) => {
|
|
2534
3106
|
const last = d.instrs[d.instrs.length - 1];
|
|
2535
3107
|
if (!last || last.mnemonic !== 'mov' || last.ops[0] !== 'pc' || last.ops[1] === 'lr') {
|
|
@@ -2540,7 +3112,12 @@ function readThumbText(name: string, asm: string): ThumbText {
|
|
|
2540
3112
|
// Direct form: the dispatch is reached ONLY by falling through from its bounds predecessor. A
|
|
2541
3113
|
// `b disp` from anywhere else would leave a dangling edge after elision, so decline (→ loud-fail).
|
|
2542
3114
|
if (prev && refs === 0) {
|
|
2543
|
-
const jt = recoverJumpTable(prev, d, dataWords, nonWordData, blockLabels)
|
|
3115
|
+
const jt = recoverJumpTable(prev, d, dataWords, nonWordData, blockLabels, aliasOf, (why) =>
|
|
3116
|
+
tableRefusals.set(d, why),
|
|
3117
|
+
);
|
|
3118
|
+
if (jt && keepDispatch(d, jt)) {
|
|
3119
|
+
return;
|
|
3120
|
+
}
|
|
2544
3121
|
if (jt) {
|
|
2545
3122
|
tables.set(prev, jt);
|
|
2546
3123
|
elided.add(d);
|
|
@@ -2548,29 +3125,71 @@ function readThumbText(name: string, asm: string): ThumbText {
|
|
|
2548
3125
|
}
|
|
2549
3126
|
}
|
|
2550
3127
|
// Long-jump form: `bounds` (cmp; bls DISP), then a lone `b DEF` block, then the dispatch. The
|
|
2551
|
-
// dispatch is entered by exactly that one `bls` and nothing else
|
|
2552
|
-
//
|
|
2553
|
-
// elided WITH it
|
|
2554
|
-
// and wiring a phi through it fabricates an
|
|
3128
|
+
// dispatch is entered by exactly that one `bls` and nothing else. When the dispatch is elided,
|
|
3129
|
+
// the `b DEF` block — named by no branch, so unreachable once the bounds block dispatches — is
|
|
3130
|
+
// elided WITH it unless a table entry names it. Leaving an unreachable one would make it a
|
|
3131
|
+
// parameterless predecessor of the default block, and wiring a phi through it fabricates an
|
|
3132
|
+
// entry parameter (the phantom-param miscompile). A kept dispatch keeps it as the bounds test's
|
|
3133
|
+
// fall-through.
|
|
2555
3134
|
const boundsB = rawBlocks[i - 2];
|
|
2556
3135
|
const prevNamed = prev ? (branchRefs.get(prev.label) ?? 0) > 0 : false;
|
|
2557
3136
|
if (refs === 1 && prev && boundsB && !prevNamed && prev.instrs.length === 1 && prev.instrs[0].mnemonic === 'b') {
|
|
2558
|
-
const jt = recoverJumpTable(
|
|
2559
|
-
|
|
3137
|
+
const jt = recoverJumpTable(
|
|
3138
|
+
boundsB,
|
|
3139
|
+
d,
|
|
3140
|
+
dataWords,
|
|
3141
|
+
nonWordData,
|
|
3142
|
+
blockLabels,
|
|
3143
|
+
aliasOf,
|
|
3144
|
+
(why) => tableRefusals.set(d, why),
|
|
3145
|
+
prev.instrs[0].ops[0],
|
|
3146
|
+
);
|
|
3147
|
+
if (jt && !keepDispatch(d, jt)) {
|
|
2560
3148
|
tables.set(boundsB, jt);
|
|
2561
3149
|
elided.add(d);
|
|
2562
|
-
|
|
3150
|
+
relays.add(prev);
|
|
2563
3151
|
}
|
|
2564
3152
|
}
|
|
2565
3153
|
});
|
|
2566
|
-
|
|
3154
|
+
// A table entry naming a relay is an edge to the default through it, so that relay stays: it is
|
|
3155
|
+
// reachable, and the kept form's relay is the same block.
|
|
3156
|
+
const named = new Set([...tables.values()].flatMap((t) => [...t.caseLabels, t.defaultLabel]));
|
|
3157
|
+
const asmBlocks = rawBlocks
|
|
3158
|
+
.filter((b) => !elided.has(b) && !(relays.has(b) && !named.has(b.label)))
|
|
3159
|
+
.map((b) => kept.get(b) ?? b);
|
|
3160
|
+
// The branch counts above vouch that a recovered dispatch is entered only from its bounds test, but
|
|
3161
|
+
// a TABLE entry is an edge too, and no table existed while they were counted — the rule MIPS and
|
|
3162
|
+
// PowerPC check on their built CFG (jump-table.ts `refuseReachableDispatch`). An entry naming a
|
|
3163
|
+
// dispatch block jumps through the code address the idiom loaded; a kept dispatch would instead
|
|
3164
|
+
// switch again on the unscaled scrutinee, and an elided one names no block at all. Both decline.
|
|
3165
|
+
const dispatchLabels = new Set([...elided, ...kept.keys()].map((b) => b.label));
|
|
3166
|
+
for (const [at, t] of tables) {
|
|
3167
|
+
const entered = [...t.caseLabels, t.defaultLabel].find((l) => dispatchLabels.has(l));
|
|
3168
|
+
if (entered !== undefined) {
|
|
3169
|
+
const movpc = rawBlocks.find((b) => b.label === entered)!.instrs.at(-1)!;
|
|
3170
|
+
throw new FrontendUnsupportedError(
|
|
3171
|
+
`cannot lift '${name}': the recovered jump table's '${movpc.mnemonic} ${movpc.ops.join(', ')}' at '${entered}' ` +
|
|
3172
|
+
`is still reachable — a case of the table dispatched at '${at.label}' enters its dispatch block past the bounds check`,
|
|
3173
|
+
);
|
|
3174
|
+
}
|
|
3175
|
+
}
|
|
3176
|
+
// The far-jump lowering's last question, askable only now: a recovered table dispatching to the
|
|
3177
|
+
// entry would re-run the lr save after a relay clobbered lr (see `lowerFarJumpRelays`).
|
|
3178
|
+
const entryLabel = rawBlocks[0]?.label;
|
|
3179
|
+
if (relayed && [...tables.values()].some((t) => [...t.caseLabels, t.defaultLabel].includes(entryLabel))) {
|
|
3180
|
+
throw new FrontendUnsupportedError(
|
|
3181
|
+
`cannot lift '${name}': '${relayed.asWritten} ${relayed.ops.join(', ')}' (a far-jump relay) is in a function ` +
|
|
3182
|
+
`whose jump table dispatches back to its entry '${entryLabel}', past the lr save — lowering it to a branch ` +
|
|
3183
|
+
`would drop the lr write a \`bl\` makes`,
|
|
3184
|
+
);
|
|
3185
|
+
}
|
|
2567
3186
|
|
|
2568
3187
|
// TRUSTWORTHINESS: loud-fail on a control transfer this frontend cannot model, rather than
|
|
2569
3188
|
// silently dropping it. A computed/loaded PC write (`mov pc, rN`, `ldr pc, …`, `add/sub pc`,
|
|
2570
3189
|
// `ldmia rN!,{…,pc}` with rN≠sp) is a jump table / computed goto / register tail call — decode
|
|
2571
3190
|
// ends the block at it, but it has no static successor, so it must be a catchable "out of scope"
|
|
2572
3191
|
// signal, not a vanished branch. Mirrors MIPS `jr`/PPC `bctr`. (A RECOGNISED jump table's
|
|
2573
|
-
// dispatch block is already elided above, so it is not scanned here.)
|
|
3192
|
+
// dispatch block is already elided or cut to its extras above, so it is not scanned here.)
|
|
2574
3193
|
//
|
|
2575
3194
|
// The same rule for a `bl` whose target is inside this function's own text. agbcc relays a
|
|
2576
3195
|
// conditional branch past Thumb's ±256-byte reach through an unconditional one, and past THAT
|
|
@@ -2579,13 +3198,14 @@ function readThumbText(name: string, asm: string): ThumbText {
|
|
|
2579
3198
|
// lift emits a call to a block label (`.L3(a0, a1, a2)`): not a branch that vanished but a
|
|
2580
3199
|
// transfer turned into something that RETURNS, and syntactically not C.
|
|
2581
3200
|
//
|
|
2582
|
-
//
|
|
3201
|
+
// A relay agbcc MARKED (`@far jump`) never reaches here: `lowerFarJumpRelays` has already lowered
|
|
3202
|
+
// it to `b`, behind a proof that lr is dead. What is left is a `bl` to a label of this function
|
|
3203
|
+
// with no mark, and the shape does not separate a relay whose comment a splitter dropped from a
|
|
2583
3204
|
// locally-defined call thunk — `call_r3: bx r3`, ARMv4T's stand-in for the `blx rN` it has no
|
|
2584
3205
|
// encoding for, which pokeemerald's m4a_1.s calls four times from inside MPlayMain's own slice.
|
|
2585
3206
|
// Both are a `bl` to a bare label the slice defines, both sit under a conditional branch that
|
|
2586
|
-
// skips them,
|
|
2587
|
-
//
|
|
2588
|
-
// proof that `lr` is dead there, since `bl` overwrites it and a branch must not.
|
|
3207
|
+
// skips them, both leave `lr` clobbered, and the thunk's function pushes lr too (`push {r0, lr}`),
|
|
3208
|
+
// so no liveness, layout or prologue fact tells them apart. Only the mark does.
|
|
2589
3209
|
//
|
|
2590
3210
|
// DECLARED function starts are excluded: a `.thumb_func`/`thumb_func_start` label is a function
|
|
2591
3211
|
// by declaration, so `bl` to one is a call however the slice came to contain it — the entry
|
|
@@ -2602,9 +3222,12 @@ function readThumbText(name: string, asm: string): ThumbText {
|
|
|
2602
3222
|
);
|
|
2603
3223
|
}
|
|
2604
3224
|
if (classifyXfer(ins) === 'indirect') {
|
|
3225
|
+
const why = tableRefusals.get(ab);
|
|
3226
|
+
const detail =
|
|
3227
|
+
why === undefined ? '' : ` — this one is a jump table whose dispatch carries code it refuses: ${why}`;
|
|
2605
3228
|
throw new FrontendUnsupportedError(
|
|
2606
3229
|
`cannot lift '${name}': indirect/computed jump '${ins.mnemonic} ${ins.ops.join(', ')}' ` +
|
|
2607
|
-
`— jump tables / computed gotos / register tail calls not supported`,
|
|
3230
|
+
`— jump tables / computed gotos / register tail calls not supported${detail}`,
|
|
2608
3231
|
);
|
|
2609
3232
|
}
|
|
2610
3233
|
const callee = (ins.mnemonic === 'bl' || ins.mnemonic === 'blx') && ins.ops.length === 1 ? ins.ops[0] : undefined;
|
|
@@ -2628,6 +3251,8 @@ interface ThumbCfg {
|
|
|
2628
3251
|
readonly labelIndex: ReadonlyMap<string, number>;
|
|
2629
3252
|
/** Each block's predecessors, one entry per edge. */
|
|
2630
3253
|
readonly preds: number[][];
|
|
3254
|
+
/** Each block's successors, `preds` turned around. */
|
|
3255
|
+
readonly succs: readonly (readonly number[])[];
|
|
2631
3256
|
readonly entryReachable: ReadonlySet<number>;
|
|
2632
3257
|
}
|
|
2633
3258
|
|
|
@@ -2670,7 +3295,9 @@ function thumbCfg(name: string, blocks: readonly AsmBlock[], tables: ReadonlyMap
|
|
|
2670
3295
|
}
|
|
2671
3296
|
return live;
|
|
2672
3297
|
})();
|
|
2673
|
-
|
|
3298
|
+
const succs: number[][] = asmBlocks.map(() => []);
|
|
3299
|
+
preds.forEach((ps, at) => ps.forEach((p) => succs[p].push(at)));
|
|
3300
|
+
return { asmBlocks, labelIndex, preds, succs, entryReachable };
|
|
2674
3301
|
}
|
|
2675
3302
|
|
|
2676
3303
|
// --- CFG (successors per block) as label lists; fallthrough + branch targets (via classifyXfer) ---
|
|
@@ -2682,8 +3309,9 @@ function buildCfg(
|
|
|
2682
3309
|
const labelIndex = new Map<string, number>();
|
|
2683
3310
|
blocks.forEach((b, i) => labelIndex.set(b.label, i));
|
|
2684
3311
|
const succLabels: string[][] = blocks.map((b, i) => {
|
|
2685
|
-
//
|
|
2686
|
-
//
|
|
3312
|
+
// The block emitting a recovered table's switch dispatches to its case blocks + default. For a
|
|
3313
|
+
// BOUNDS block (the dispatch elided) that replaces its `bhi`/fall-through successors entirely; a
|
|
3314
|
+
// kept dispatch block had no static successor of its own.
|
|
2687
3315
|
const jt = tables.get(b);
|
|
2688
3316
|
if (jt) {
|
|
2689
3317
|
return [...jt.caseLabels, jt.defaultLabel];
|
|
@@ -2719,19 +3347,6 @@ function buildCfg(
|
|
|
2719
3347
|
return { labelIndex, preds };
|
|
2720
3348
|
}
|
|
2721
3349
|
|
|
2722
|
-
// A `scratchRegs` entry outside `nonArgRegs` is inert, and inert is how a partition rots: the
|
|
2723
|
-
// list would go on reading as if it exempted something. Refused as a target bug, like the
|
|
2724
|
-
// argRegs/uninitRegs contradiction, rather than declined as a property of the input.
|
|
2725
|
-
function assertScratchRegsPartitioned(target: TargetDescription): ReadonlySet<string> {
|
|
2726
|
-
const scratchRegs = new Set(target.scratchRegs ?? []);
|
|
2727
|
-
for (const r of scratchRegs) {
|
|
2728
|
-
if (!(target.nonArgRegs ?? []).includes(r)) {
|
|
2729
|
-
throw new Error(`target '${target.id}': scratch register ${r} is not among the non-argument registers`);
|
|
2730
|
-
}
|
|
2731
|
-
}
|
|
2732
|
-
return scratchRegs;
|
|
2733
|
-
}
|
|
2734
|
-
|
|
2735
3350
|
/** A `bl` passes a pair in two argument words, a struct-return pointer in r0, and the words past r3
|
|
2736
3351
|
* in the outgoing block `analyzeOutgoingArgs` licensed; a call through rN passes r0..r(N-1). */
|
|
2737
3352
|
const THUMB_CALL_LOWERING: CallLowering = {
|
|
@@ -2746,16 +3361,39 @@ const THUMB_CALL_LOWERING: CallLowering = {
|
|
|
2746
3361
|
interface ThumbFrame {
|
|
2747
3362
|
readonly localArea: number;
|
|
2748
3363
|
readonly savedRegs: ReadonlySet<string>;
|
|
3364
|
+
/** the prologue's `mov rLow, rHi` that only stage a high register for its `push` (see
|
|
3365
|
+
* `prologueSavedRegs`); the fill does not lift them */
|
|
3366
|
+
readonly saveCarriers: ReadonlySet<Instr>;
|
|
2749
3367
|
readonly capturedObjectIsTheWholeFrame: boolean;
|
|
3368
|
+
/** the offsets of the escaped frame addresses the outgoing-argument verdict leans on, which the
|
|
3369
|
+
* audit re-proves of the lifted IR; empty where the verdict leans on none */
|
|
3370
|
+
readonly escapesReliedOn: ReadonlySet<number>;
|
|
3371
|
+
/** the calls that hand out an escape the verdict leans on; empty where it leans on none */
|
|
3372
|
+
readonly callsReliedOn: ReadonlySet<Instr>;
|
|
2750
3373
|
/** the frame offset each instruction leaves a register holding, where `heldFrameWalk` follows it */
|
|
2751
3374
|
readonly captureOffsetOf: ReadonlyMap<Instr, number>;
|
|
3375
|
+
/** at each call, the frame addresses its argument registers may and must hold (`frameAddressesAtCalls`) */
|
|
3376
|
+
readonly frameAddressesAtCall: ReadonlyMap<Instr, FrameAddressesAtCall>;
|
|
2752
3377
|
readonly outgoingArgs: OutgoingArgs<Instr>;
|
|
3378
|
+
/** the loaded-word bound both `outgoingArgs` and the frame-object audit take
|
|
3379
|
+
* (`FrameObjectAudit.loadedWords`); absent where the target states no start offset */
|
|
3380
|
+
readonly loadedWords: FrameObjectAudit['loadedWords'];
|
|
2753
3381
|
/** why the word-slot model is off, or null when it is on (`slotModelBlocker`) */
|
|
2754
3382
|
readonly slotsOffReason: string | null;
|
|
2755
3383
|
readonly slotsOk: boolean;
|
|
3384
|
+
/** the frame depth at every instruction outside the entry block, or null where it is not proven
|
|
3385
|
+
* one number (`frameDepthBlocker`) */
|
|
3386
|
+
readonly settledDepth: number | null;
|
|
2756
3387
|
readonly partition: Readonly<Pick<FrameObjectAudit, 'ownedLocals' | 'declaredLocals'>>;
|
|
2757
3388
|
isFrameObjectAccess(base: string, off: number, regOff: string | undefined, width: number): boolean;
|
|
2758
3389
|
isOwnFrameWordSlot(base: string, off: number, regOff: string | undefined, width: number): boolean;
|
|
3390
|
+
/** the word slot each narrow reload through a copy of sp reads the low-order part of
|
|
3391
|
+
* (`narrowSlotReloads`), and the copies those reloads consume, which name no object */
|
|
3392
|
+
readonly narrowReloadOf: ReadonlyMap<Instr, number>;
|
|
3393
|
+
readonly narrowReloadCopies: ReadonlySet<Instr>;
|
|
3394
|
+
/** the argument homes whose narrow reloads were kept as captures for the one-object answer alone
|
|
3395
|
+
* (`argumentHomes`), which the audit may ask to release (`FrameObjectAudit.releasableHomes`) */
|
|
3396
|
+
readonly releasableHomes: ReadonlySet<number>;
|
|
2759
3397
|
}
|
|
2760
3398
|
|
|
2761
3399
|
/** The frame offset of each word of a declared call's outgoing block, or null where it stages none.
|
|
@@ -2775,7 +3413,7 @@ interface ThumbFrame {
|
|
|
2775
3413
|
*
|
|
2776
3414
|
* Null for an arity that fits in registers, where there IS no outgoing block. */
|
|
2777
3415
|
function outgoingBlock(widths: readonly number[], target: TargetDescription): number[] | null {
|
|
2778
|
-
const words = wordsOf(widths) - target.argRegs.length;
|
|
3416
|
+
const words = wordsOf(widths, target) - target.argRegs.length;
|
|
2779
3417
|
return words > 0 && target.compilerBehaviors.stagesOutgoingArgsInFrame === true
|
|
2780
3418
|
? Array.from({ length: words }, (_, i) => 4 * i)
|
|
2781
3419
|
: null;
|
|
@@ -2799,15 +3437,33 @@ function calleeRegister(ins: Instr, target: TargetDescription): string | null |
|
|
|
2799
3437
|
}
|
|
2800
3438
|
|
|
2801
3439
|
function measureThumbFrame({
|
|
3440
|
+
name,
|
|
2802
3441
|
target,
|
|
2803
|
-
cfg: { asmBlocks, preds, entryReachable },
|
|
3442
|
+
cfg: { asmBlocks, preds, succs, entryReachable },
|
|
2804
3443
|
calls,
|
|
2805
3444
|
oneObject,
|
|
3445
|
+
homesReleased = false,
|
|
3446
|
+
own,
|
|
3447
|
+
dropsVoidReturn,
|
|
3448
|
+
stackWordsOf,
|
|
2806
3449
|
}: {
|
|
3450
|
+
name: string;
|
|
2807
3451
|
target: TargetDescription;
|
|
2808
3452
|
cfg: ThumbCfg;
|
|
2809
3453
|
calls: CallDeclarations;
|
|
2810
3454
|
oneObject: FrameRange | undefined;
|
|
3455
|
+
/** whether a first audit released the argument homes to the slot model (`releasableHomes`) */
|
|
3456
|
+
homesReleased?: boolean;
|
|
3457
|
+
/** the function's own declaration (`ownDeclarationOf`), read only where it `proves` */
|
|
3458
|
+
own: OwnDeclaration | undefined;
|
|
3459
|
+
/** whether the lift's `ret` reads nothing: a STATED void entry (`prototypes[name]`), the one
|
|
3460
|
+
* pipeline.ts `raiseRecovered` drops ret operands for. The context's own entry, proving or not,
|
|
3461
|
+
* leaves the operand in place. */
|
|
3462
|
+
dropsVoidReturn: boolean;
|
|
3463
|
+
/** the incoming stack words a callee nothing declares loads, off its own body
|
|
3464
|
+
* (`LiftOptions.calleeStackWords`): the outgoing-argument analysis's `bodyReads`, which asks it
|
|
3465
|
+
* only where the code stages a block */
|
|
3466
|
+
stackWordsOf?: CalleeStackWords;
|
|
2811
3467
|
}): ThumbFrame {
|
|
2812
3468
|
// THE FRAME BASE PASSED TO A CALLEE. What this computes is exactly what its name says and nothing
|
|
2813
3469
|
// more: somewhere in entry-reachable code a bare `mov rD, sp` copies the frame base into a
|
|
@@ -2817,15 +3473,19 @@ function measureThumbFrame({
|
|
|
2817
3473
|
// feeds (`capturedObjectIsTheWholeFrame`, below `localArea`), not here.
|
|
2818
3474
|
//
|
|
2819
3475
|
// ARGUMENT registers only: the frame base merely live across a call is not evidence that it was
|
|
2820
|
-
// passed to one, and for `blx rN` the TARGET register is not an argument either.
|
|
3476
|
+
// passed to one, and for `blx rN` the TARGET register is not an argument either. Which registers
|
|
3477
|
+
// a call reads, and which stores send a word outside the frame, are ONE rule for every escape
|
|
3478
|
+
// this function asks about — `frameAddressReceivers` — so the three walks that ask differ only
|
|
3479
|
+
// in the kill rule, and `heldFrameWalk` says why that one differs.
|
|
3480
|
+
const mayHoldFrameAddress = registersThatMayHoldAFrameAddress(asmBlocks, entryReachable);
|
|
3481
|
+
const handedOutRegs = (ins: Instr): readonly string[] =>
|
|
3482
|
+
frameAddressReceivers(ins, target, calls, mayHoldFrameAddress);
|
|
2821
3483
|
const frameBasePassedToCallee = heldFrameWalk(asmBlocks, entryReachable, mentionsReg, (ins, held) => {
|
|
2822
3484
|
if (ins.mnemonic !== 'bl' && ins.mnemonic !== 'blx') {
|
|
2823
3485
|
return false;
|
|
2824
3486
|
}
|
|
2825
|
-
|
|
2826
|
-
|
|
2827
|
-
const targetReg = calleeRegister(ins, target);
|
|
2828
|
-
return [...held].some(([r, off]) => off === 0 && target.argRegs.includes(r) && r !== targetReg);
|
|
3487
|
+
const handed = handedOutRegs(ins);
|
|
3488
|
+
return [...held].some(([r, off]) => off === 0 && handed.includes(r));
|
|
2829
3489
|
});
|
|
2830
3490
|
|
|
2831
3491
|
// THE FRAME BASE PUBLISHED TO MEMORY — the other way an agbcc frame address escapes, and the
|
|
@@ -2882,7 +3542,9 @@ function measureThumbFrame({
|
|
|
2882
3542
|
// (u32)&b;` compiles to `add sp,#-0x44 / mov r0,sp / mov r2,#0x44 / bl memcpy / ldr r0,.L9+4 /
|
|
2883
3543
|
// mov r1,sp / str r1,[r0]` — a block-copy base that IS published to memory (measured). At a
|
|
2884
3544
|
// wider frame the publish escape admits exactly the shape the callee escape's table warns
|
|
2885
|
-
// about; at one word it cannot occur.
|
|
3545
|
+
// about; at one word it cannot occur. A wider frame's publish is read by the outgoing-argument
|
|
3546
|
+
// `escape` event instead (below), per program point, which is the reading that survives a block
|
|
3547
|
+
// copy: see "ONE DECISION, FOUR LICENCES" above `capturedObjectIsTheWholeFrame`.
|
|
2886
3548
|
//
|
|
2887
3549
|
// THE WALK IS `heldFrameWalk`, the same one the callee escape is spelled with, because the
|
|
2888
3550
|
// question is the same one and only the consuming event differs. Here that event is a WORD store
|
|
@@ -2898,21 +3560,21 @@ function measureThumbFrame({
|
|
|
2898
3560
|
// refusal it produces stops being the two-models-for-one-byte one and becomes the audit's. A
|
|
2899
3561
|
// clause without a row is decoration until an input says otherwise.
|
|
2900
3562
|
//
|
|
2901
|
-
// THE AUDIT'S RE-PROOF IS NOT THIS PREDICATE RE-SPELLED,
|
|
2902
|
-
//
|
|
2903
|
-
//
|
|
2904
|
-
//
|
|
2905
|
-
//
|
|
2906
|
-
|
|
2907
|
-
|
|
2908
|
-
|
|
2909
|
-
|
|
2910
|
-
|
|
2911
|
-
|
|
2912
|
-
|
|
3563
|
+
// THE AUDIT'S RE-PROOF IS NOT THIS PREDICATE RE-SPELLED, but its BASE clause is the same
|
|
3564
|
+
// whole-function fact on both sides. The stored VALUE must be held here (block-local); the BASE
|
|
3565
|
+
// must be no register that holds a frame address ANYWHERE in the function
|
|
3566
|
+
// (`registersThatMayHoldAFrameAddress`), the audit's whole-function `taint` read off the
|
|
3567
|
+
// instructions. So a base captured in an earlier block is refused by the licence itself, before
|
|
3568
|
+
// the audit sees it. Pinned as a row.
|
|
3569
|
+
const frameBasePublishedToMemory = heldFrameWalk(
|
|
3570
|
+
asmBlocks,
|
|
3571
|
+
entryReachable,
|
|
3572
|
+
mentionsReg,
|
|
3573
|
+
(ins, held) => ins.mnemonic === 'str' && handedOutRegs(ins).some((r) => held.get(r) === 0),
|
|
3574
|
+
);
|
|
2913
3575
|
|
|
2914
3576
|
const localArea = reservedLocalArea(asmBlocks[0]);
|
|
2915
|
-
const savedRegs = prologueSavedRegs(asmBlocks[0]);
|
|
3577
|
+
const { saved: savedRegs, carriers: saveCarriers } = prologueSavedRegs(asmBlocks[0]);
|
|
2916
3578
|
|
|
2917
3579
|
// …AND THE FRAME IS THAT OBJECT. The two escape scans above are facts about a REGISTER — that a
|
|
2918
3580
|
// bare `mov rD, sp` is still held when a callee is entered, or when it is stored to memory.
|
|
@@ -2950,7 +3612,8 @@ function measureThumbFrame({
|
|
|
2950
3612
|
// block-copy base — each needs two frame words before agbcc names its base with a register at
|
|
2951
3613
|
// all — and the two-word struct returns with them. Any larger frame has bytes this model does not
|
|
2952
3614
|
// describe, and they are either the rest of a wider object the callee writes or another call's
|
|
2953
|
-
// argument slots
|
|
3615
|
+
// argument slots. Neither is provable from the frame alone, so THIS licence stays at one word,
|
|
3616
|
+
// and a larger frame is left to the `escape` event, which does not have to decide it.
|
|
2954
3617
|
//
|
|
2955
3618
|
// What the frame size does NOT exclude is the one-word struct return in the table above, which is
|
|
2956
3619
|
// instruction-for-instruction an out-parameter call. That one is settled after the lift, by the
|
|
@@ -2962,8 +3625,8 @@ function measureThumbFrame({
|
|
|
2962
3625
|
// handed to a callee arrives there through a frame this conjunct never looks at, and what bounds
|
|
2963
3626
|
// its extent is the audit's frame-accounting rule rather than anything here.
|
|
2964
3627
|
//
|
|
2965
|
-
// WHY
|
|
2966
|
-
// each compiled with agbcc 2.9-arm-000512, `-O2 -mthumb-interwork -Wimplicit -fhex-asm
|
|
3628
|
+
// WHY THIS FLOW-INSENSITIVE LICENCE IS NOT WIDENED, since a wider frame is the obvious next
|
|
3629
|
+
// change (the `escape` event reads a wider frame instead). Three shapes, each compiled with agbcc 2.9-arm-000512, `-O2 -mthumb-interwork -Wimplicit -fhex-asm
|
|
2967
3630
|
// -fprologue-bugfix`, and only the first is about extent at all:
|
|
2968
3631
|
//
|
|
2969
3632
|
// UNDECIDABLE — a slot THIS FUNCTION stores and reloads. Two sources that disagree about who
|
|
@@ -3017,6 +3680,26 @@ function measureThumbFrame({
|
|
|
3017
3680
|
// modelled as `volatile u16 sp0`, and its lift is byte-identical with this conjunct widened to
|
|
3018
3681
|
// `localArea >= 4` (measured). Nothing about the frame size moves it either way — which is what
|
|
3019
3682
|
// makes it the wrong function to reason about a wider licence from.
|
|
3683
|
+
//
|
|
3684
|
+
// ONE DECISION, FOUR LICENCES. This gate and the outgoing-argument `escape` event (fed below,
|
|
3685
|
+
// under `compilerBehaviors.escapedFrameAddressNamesALocal`) read the same escapes
|
|
3686
|
+
// (`frameAddressReceivers`) and differ only in flow-sensitivity. This gate is flow-INSENSITIVE —
|
|
3687
|
+
// one escape anywhere and no frame word is any call's argument — which is sound only where no
|
|
3688
|
+
// block-copy base fits, a one-word frame. The event ends only the stores pending where the address
|
|
3689
|
+
// leaves, at any frame size; that field says why that survives a block copy. So what this gate
|
|
3690
|
+
// adds is a store that reaches an undeclared call BEFORE the address leaves (the CONTROL of "a
|
|
3691
|
+
// capture in an unreachable block proves nothing"), and every "not widened" above is about it.
|
|
3692
|
+
//
|
|
3693
|
+
// The THIRD is the [sp,#0] struct-return capture (`frameBaseIsReturnStorage`, below): flow-
|
|
3694
|
+
// insensitive at any frame size, but only for the hidden pointer of a callee declared to return a
|
|
3695
|
+
// struct through memory, which a block copy's base never is (why: the field's comment). It
|
|
3696
|
+
// matters only where some call may take a stack word: the hidden pointer is r0, which
|
|
3697
|
+
// `frameAddressReceivers` hands over, so such a base always escapes and the fourth licence
|
|
3698
|
+
// admits it everywhere else.
|
|
3699
|
+
//
|
|
3700
|
+
// The FOURTH is the [sp,#0] capture where no call takes a stack word (`noCallTakesAStackWord &&
|
|
3701
|
+
// frameBaseEscapes`, below): flow-insensitive at any frame size too, and sound for a block copy's
|
|
3702
|
+
// base as well, because with no outgoing area no frame word is any call's argument at all.
|
|
3020
3703
|
const capturedObjectIsTheWholeFrame =
|
|
3021
3704
|
target.compilerBehaviors.oneWordFrameIsTheCapturedObject === true &&
|
|
3022
3705
|
(frameBasePassedToCallee || frameBasePublishedToMemory) &&
|
|
@@ -3028,20 +3711,264 @@ function measureThumbFrame({
|
|
|
3028
3711
|
// What the walk cannot follow (a move in another block than the capture) the lift does not
|
|
3029
3712
|
// fold either; the audit folds it from the IR, and refuses it by that name where the object
|
|
3030
3713
|
// lands on a word this left keyed as a slot.
|
|
3714
|
+
//
|
|
3715
|
+
// …AND THE LOWEST FRAME ADDRESS EACH INSTRUCTION HANDS OUT, off the same walk: one held in an
|
|
3716
|
+
// argument register the callee's declaration reads at a call (every one, where nothing declares
|
|
3717
|
+
// it), or stored as a word to memory outside the frame — the two escapes above, at any offset.
|
|
3718
|
+
// This walk keeps a capture addressed through (`mov r0, sp / strb r3, [r0, #8] / bl use`, sa3's
|
|
3719
|
+
// `sub_8060940`), because the question here is what the receiver CAN read, and the outgoing-
|
|
3720
|
+
// argument analysis only lets it end a word's pending store (`escapeEvent`, below). Its receivers
|
|
3721
|
+
// are `handedOutRegs`, the rule the two licences above read too.
|
|
3031
3722
|
const captureOffsetOf = new Map<Instr, number>();
|
|
3723
|
+
const escapedOffsetOf = new Map<Instr, number>();
|
|
3724
|
+
let frameBaseIsReturnStorage = false;
|
|
3032
3725
|
heldFrameWalk(asmBlocks, entryReachable, mayWriteReg, (ins, held) => {
|
|
3726
|
+
if (
|
|
3727
|
+
ins.mnemonic === 'bl' &&
|
|
3728
|
+
held.get(target.argRegs[0]) === 0 &&
|
|
3729
|
+
calleeRegister(ins, target) === null &&
|
|
3730
|
+
typeof calls.declaredCall(ins.ops[0] ?? '?')?.returned === 'object'
|
|
3731
|
+
) {
|
|
3732
|
+
frameBaseIsReturnStorage = true;
|
|
3733
|
+
}
|
|
3734
|
+
const handed = handedOutRegs(ins);
|
|
3735
|
+
const offs = [...held].filter(([r, off]) => handed.includes(r) && off >= 0).map(([, off]) => off);
|
|
3736
|
+
if (offs.length > 0) {
|
|
3737
|
+
escapedOffsetOf.set(ins, Math.min(...offs));
|
|
3738
|
+
}
|
|
3033
3739
|
const defined = frameAddressDefined(ins, held);
|
|
3034
|
-
if (defined !== null) {
|
|
3740
|
+
if (defined !== null && Number.isFinite(defined[1])) {
|
|
3035
3741
|
captureOffsetOf.set(ins, defined[1]);
|
|
3036
3742
|
}
|
|
3037
3743
|
return false;
|
|
3038
3744
|
});
|
|
3039
|
-
// …and the offsets themselves
|
|
3040
|
-
// `capturedObjectIsTheWholeFrame`. A word whose address is taken is not an outgoing argument —
|
|
3745
|
+
// …and the offsets themselves. A word whose address is taken is not an outgoing argument —
|
|
3041
3746
|
// C gives an argument no address — so the outgoing-argument analysis below does not see these
|
|
3042
3747
|
// offsets either: a store there reaching a call unread is the object being filled for the
|
|
3043
3748
|
// callee that is handed it.
|
|
3044
|
-
|
|
3749
|
+
//
|
|
3750
|
+
// THE FRAME BASE, ONLY WHERE ITS ADDRESS ESCAPES AND NO CALL TAKES A STACK WORD, OR AS A
|
|
3751
|
+
// DECLARED CALL'S RETURN STORAGE. A bare `mov rD, sp` is also the base of a by-value argument's
|
|
3752
|
+
// block copy into the outgoing area, and [sp,#0] is then a call's fifth argument word (the
|
|
3753
|
+
// producer table above `capturedObjectIsTheWholeFrame`, which keeps its own licence for a
|
|
3754
|
+
// one-word frame). Where every call's declaration places all of its arguments in registers — no
|
|
3755
|
+
// call at all included — there is no outgoing area on any compiler, so a frame base handed out
|
|
3756
|
+
// (`escapedOffsetOf`: to a call, or stored as a word to memory) is a local's address whoever
|
|
3757
|
+
// receives it: a struct-return temp (`mov r0, sp / bl mk`), or a DMA fill's source published to
|
|
3758
|
+
// the device's registers (`dmafill_wh`). A direct `str rX, [sp]` is then that local's store, as
|
|
3759
|
+
// one at [sp,#4] beside `add rD, sp, #4` is. A frame base only ADDRESSED THROUGH (`mov r3, sp /
|
|
3760
|
+
// strh r1, [r3, #4]`, Thumb's sole spelling of a sub-word frame access) hands nothing out and
|
|
3761
|
+
// leaves [sp,#0] a slot. A call nothing declares, a call through a register, or a declaration
|
|
3762
|
+
// nothing sizes may take a stack word, and keeps offset 0 out — unless the base is a
|
|
3763
|
+
// struct-return callee's hidden pointer and `compilerBehaviors.escapedFrameAddressNamesALocal`
|
|
3764
|
+
// holds (its second reader).
|
|
3765
|
+
//
|
|
3766
|
+
// THUMB ONLY: the MIPS and PPC frontends (`inLocalArea`) route every local-area access through
|
|
3767
|
+
// memory once any frame address is taken, so a direct store at the frame base is already the
|
|
3768
|
+
// object's there.
|
|
3769
|
+
const frameBaseEscapes = [...escapedOffsetOf.values()].includes(0);
|
|
3770
|
+
const noCallTakesAStackWord = [...entryReachable].every((b) =>
|
|
3771
|
+
asmBlocks[b].instrs.every((ins) => {
|
|
3772
|
+
if (ins.mnemonic !== 'bl' && ins.mnemonic !== 'blx') {
|
|
3773
|
+
return true;
|
|
3774
|
+
}
|
|
3775
|
+
const declared = calleeRegister(ins, target) === null ? calls.declaredCall(ins.ops[0] ?? '?') : null;
|
|
3776
|
+
// the declaration alone, not `outgoingBlock`: a call whose arguments run past the registers
|
|
3777
|
+
// takes a stack word whether or not the target claims agbcc's staging premise
|
|
3778
|
+
return declared !== null && wordsOf(declared.widths, target) <= target.argRegs.length;
|
|
3779
|
+
}),
|
|
3780
|
+
);
|
|
3781
|
+
|
|
3782
|
+
// A STRUCT PASSED BY VALUE, LOADED OFF THE FRAME A WORD AT A TIME. agbcc builds the argument in
|
|
3783
|
+
// the frame — through a copy of sp, Thumb having no sp-relative halfword or byte store — and
|
|
3784
|
+
// then loads its words into the argument registers the declaration places it in (`struct A
|
|
3785
|
+
// {u16 a,b,c,d; s8 e,f;} a; a.a = x; … take2(a);` is `mov r2, sp / strh r0, [r2] / … / ldr r0,
|
|
3786
|
+
// [sp] / ldr r1, [sp, #0x4] / ldr r2, [sp, #0x8] / bl take2`). Those words are the object's, not
|
|
3787
|
+
// slots: each `[sp,#k]` inside such a block is routed through an `laddr` (`isFrameObjectAccess`),
|
|
3788
|
+
// and the loads are folded into the object passed whole or refused (call-plan.ts
|
|
3789
|
+
// `foldByValueArgs`). A block is read only where every one of its registers is
|
|
3790
|
+
// last written, in the call's own block, by a word load at the next word of one frame range.
|
|
3791
|
+
const byValueBlocks: FrameRange[] = [];
|
|
3792
|
+
for (const b of entryReachable) {
|
|
3793
|
+
const instrs = asmBlocks[b].instrs;
|
|
3794
|
+
instrs.forEach((call, at) => {
|
|
3795
|
+
if (call.mnemonic !== 'bl' || calleeRegister(call, target) !== null) {
|
|
3796
|
+
return;
|
|
3797
|
+
}
|
|
3798
|
+
const declared = calls.declaredCall(call.ops[0] ?? '?');
|
|
3799
|
+
for (const arg of declared?.byValue ?? []) {
|
|
3800
|
+
// `word` indexes the call's widths, where a 64-bit argument is one entry of two registers
|
|
3801
|
+
const reg = wordsOf(declared!.widths.slice(0, arg.word), target);
|
|
3802
|
+
const offs = target.argRegs.slice(reg, reg + arg.words).map((r) => {
|
|
3803
|
+
const def = instrs
|
|
3804
|
+
.slice(0, at)
|
|
3805
|
+
.reverse()
|
|
3806
|
+
.find((ins) => mayWriteReg(ins, r));
|
|
3807
|
+
const acc = def !== undefined && def.mnemonic === 'ldr' && def.ops[0] === r ? spMemAccess(def) : null;
|
|
3808
|
+
return acc !== null && !acc.regOff && acc.width === 4 ? acc.off : null;
|
|
3809
|
+
});
|
|
3810
|
+
if (offs.every((o, i) => o !== null && o === offs[0]! + 4 * i)) {
|
|
3811
|
+
byValueBlocks.push({ from: offs[0]!, to: offs[0]! + 4 * arg.words });
|
|
3812
|
+
}
|
|
3813
|
+
}
|
|
3814
|
+
});
|
|
3815
|
+
}
|
|
3816
|
+
const inByValueBlock = (off: number, width: number): boolean =>
|
|
3817
|
+
byValueBlocks.some((r) => off >= r.from && off + width <= r.to);
|
|
3818
|
+
|
|
3819
|
+
// THE NARROW RELOADS OF A WORD SLOT (`narrowSlotReloads`), kept only where the word is a slot
|
|
3820
|
+
// and no object's: a word `str` to [sp,#slot] in live code writes it — without one the bytes are
|
|
3821
|
+
// a sub-word local's (`mov r3, sp / strh r2, [r3, #4]`), which the object model owns — no other
|
|
3822
|
+
// copy of sp names the slot's offset, and neither a by-value block nor the one object covers it.
|
|
3823
|
+
// The one-word frame object (`capturedObjectIsTheWholeFrame`) needs no test of its own: its
|
|
3824
|
+
// escape is a copy of sp at offset 0 handed out, which no candidate's copy can be (that one dies
|
|
3825
|
+
// in its load), so it is another capture at the slot's offset.
|
|
3826
|
+
//
|
|
3827
|
+
// …AND THE WORD IS NOT AN INCOMING ARGUMENT'S HOME. A by-value struct parameter is the other
|
|
3828
|
+
// producer of these instructions: agbcc stores its argument registers word by word on entry and
|
|
3829
|
+
// reads each member at its width through a copy of sp (`struct P { u16 a, b, c, d; s8 e; }; int
|
|
3830
|
+
// b2(struct P p){ return p.a + p.c - p.e; }` is `str r0, [sp] / … / mov r0, sp / ldrh r0, [r0]`),
|
|
3831
|
+
// and there the frame is the parameter, one object whose union recompiles byte-exact
|
|
3832
|
+
// (cli/test/matching/frame-union.test.ts) where the words narrowed recompile with no frame at
|
|
3833
|
+
// all. A word the entry block stores straight from an argument register it has not written is
|
|
3834
|
+
// that home, and its reloads keep the object model. A spilled promoted local is stored from the
|
|
3835
|
+
// value it was computed as. That test reads the asm alone, and the function's own declaration
|
|
3836
|
+
// overrules it where it proves the register holds one integer word (`proto.ts`
|
|
3837
|
+
// `ownLeadingWordRegisters`): a `u32 k` spilled on entry and read back as `(u8)k` is no struct's
|
|
3838
|
+
// home, and lifts as the slot narrowed. A copy kept here is no capture, so its offset leaves
|
|
3839
|
+
// `constantCaptureOffsets` and the slot's own `str`/`ldr` stay slot accesses; one dropped stays a
|
|
3840
|
+
// capture. A home is held for the one-object answer alone, so the audit may hand it back: where
|
|
3841
|
+
// that answer is withheld (a struct-return temp escapes beside the home) and the audit declines
|
|
3842
|
+
// with the home held, the second lift reads it as any other slot (`releasableHomes`,
|
|
3843
|
+
// `homesReleased`). The hold decides how the LIFT reads a home's reloads, not whether the word
|
|
3844
|
+
// is reloaded: the stack-argument analysis takes a held home's narrow reload as that word's
|
|
3845
|
+
// reload (`capturedReloadOf`) in every lift, as it takes a narrow reload inside the bytes a
|
|
3846
|
+
// first audit asked to keep as one object, which that relift keeps as a capture too. Read as no
|
|
3847
|
+
// reload, a home at the frame's bottom would decline as a possible outgoing argument, and agbcc
|
|
3848
|
+
// compiles that shape: reload spills a DImode struct's registers above the locals, but
|
|
3849
|
+
// `assign_parms` stores a BLKmode one (over 8 bytes) at the frame's bottom, below any temp
|
|
3850
|
+
// (`str r0, [sp] / … / add r0, sp, #0xc / bl mk0 / … / mov r0, sp / ldrb r0, [r0]`). Below an
|
|
3851
|
+
// escaping temp the first lift would decline before any audit could release the home; with a
|
|
3852
|
+
// call and no temp (`g(); return p.m0 + p.m8;`) the one-object relift would.
|
|
3853
|
+
//
|
|
3854
|
+
// A COPY THAT OUTLIVES ITS LOAD. Where reload hands the load another register (`add r3, sp, #0x28
|
|
3855
|
+
// / ldrb r1, [r3]`, the left operand of a `?:` join whose hard register is r1), rD still holds the
|
|
3856
|
+
// address after it. The copy is not lifted, so rD must be DEAD there: no path from the load reads
|
|
3857
|
+
// it before writing it (`readsBeforeWrite`), a return that may hand it back counting as a read.
|
|
3858
|
+
// The lift holds to that proof: the dropped copy leaves a marker in rD (`writeNarrowCopyMarker`),
|
|
3859
|
+
// and a read of it declines. A CALL READS WHAT IT IS HANDED, the one receiver rule
|
|
3860
|
+
// (`frameAddressReceivers`: a declared callee's argument words, every argument register where
|
|
3861
|
+
// nothing declares it), plus the register it calls through (`calleeRegister`). Any other register
|
|
3862
|
+
// it clobbers, r0 among them, is dead past it, and a callee-saved one still holds the copy. Read
|
|
3863
|
+
// as `readsBeforeWrite` reads a call for the save analysis (every register but r12), kleod
|
|
3864
|
+
// sub_08016EEC keeps two copies live, in r3 and in r6.
|
|
3865
|
+
//
|
|
3866
|
+
// A RETURN HANDS BACK WHAT THE OWN DECLARATION RETURNS. Where a STATED entry declares a void
|
|
3867
|
+
// return, a return reads neither r0 nor r1 (pipeline.ts `dropVoidReturnOperands`, the exemption
|
|
3868
|
+
// `refuseStaleIdiomReads` makes for the marker), and where the own declaration proves a return no
|
|
3869
|
+
// wider than a word, it reads r0 alone: `add r1, sp, #4 / ldrb r0, [r1] / … / pop {r0} / bx r0` in
|
|
3870
|
+
// a `void f(u32)` leaves no read of r1. Undeclared, a return may hand back the pair r0:r1. The void
|
|
3871
|
+
// reading of r0 is the stated entry's alone because the drop is: under the context's `void f(u32)`
|
|
3872
|
+
// the lifted `ret` still reads r0, and an r0 copy dropped there would print as `return uninit_r0`.
|
|
3873
|
+
// The reading of r1 needs no such care, under a void or a one-word declaration from either source,
|
|
3874
|
+
// as the lifted `ret` reads r1 only through a pair the asm builds (`wideReturn`), where the marker
|
|
3875
|
+
// declines.
|
|
3876
|
+
const narrowCallReads = (call: Instr): readonly string[] => {
|
|
3877
|
+
const through = calleeRegister(call, target);
|
|
3878
|
+
return typeof through === 'string' ? [...handedOutRegs(call), through] : handedOutRegs(call);
|
|
3879
|
+
};
|
|
3880
|
+
// `callClobbers` leaves out r0, which the lift writes with what the call returns
|
|
3881
|
+
const narrowCallClobbers = [...calls.callClobbers, target.returnReg];
|
|
3882
|
+
const ownProven = own?.proves === true ? own.proto : undefined;
|
|
3883
|
+
const returnMayHand = (r: string): boolean =>
|
|
3884
|
+
!dropsVoidReturn &&
|
|
3885
|
+
!(
|
|
3886
|
+
r !== target.returnReg &&
|
|
3887
|
+
(declaresVoidReturn(ownProven) || (ownReturnWidth(ownProven) ?? Number.POSITIVE_INFINITY) <= 32)
|
|
3888
|
+
);
|
|
3889
|
+
const narrowCandidates = narrowSlotReloads(
|
|
3890
|
+
asmBlocks,
|
|
3891
|
+
entryReachable,
|
|
3892
|
+
localArea,
|
|
3893
|
+
target.capabilities.endianness === 'little',
|
|
3894
|
+
(b, from, r) => {
|
|
3895
|
+
const use = readsBeforeWrite(asmBlocks, succs, b, from, r, {
|
|
3896
|
+
reads: narrowCallReads,
|
|
3897
|
+
clobbers: narrowCallClobbers,
|
|
3898
|
+
});
|
|
3899
|
+
return use === false || (use === 'returned' && !returnMayHand(r));
|
|
3900
|
+
},
|
|
3901
|
+
);
|
|
3902
|
+
const candidateCopies = new Set([...narrowCandidates.values()].map((n) => n.copy));
|
|
3903
|
+
const otherCaptureOffsets = new Set(
|
|
3904
|
+
[...captureOffsetOf].filter(([ins]) => !candidateCopies.has(ins)).map(([, off]) => off),
|
|
3905
|
+
);
|
|
3906
|
+
const wordStoredSlots = new Set(
|
|
3907
|
+
[...entryReachable].flatMap((b) =>
|
|
3908
|
+
asmBlocks[b].instrs.flatMap((ins) => {
|
|
3909
|
+
const a = spMemAccess(ins);
|
|
3910
|
+
return ins.mnemonic === 'str' && a !== null && !a.regOff ? [a.off] : [];
|
|
3911
|
+
}),
|
|
3912
|
+
),
|
|
3913
|
+
);
|
|
3914
|
+
const argumentHomes = new Set<number>();
|
|
3915
|
+
{
|
|
3916
|
+
const proving = own?.proves === true ? own.proto : undefined;
|
|
3917
|
+
const wordArgRegs = new Set(target.argRegs.slice(0, ownLeadingWordRegisters(name, proving)));
|
|
3918
|
+
const written = new Set<string>();
|
|
3919
|
+
for (const ins of asmBlocks[0].instrs) {
|
|
3920
|
+
const a = spMemAccess(ins);
|
|
3921
|
+
const src = reg(ins.ops[0] ?? '');
|
|
3922
|
+
if (
|
|
3923
|
+
ins.mnemonic === 'str' &&
|
|
3924
|
+
a !== null &&
|
|
3925
|
+
!a.regOff &&
|
|
3926
|
+
target.argRegs.includes(src) &&
|
|
3927
|
+
!wordArgRegs.has(src) &&
|
|
3928
|
+
!written.has(src)
|
|
3929
|
+
) {
|
|
3930
|
+
argumentHomes.add(a.off);
|
|
3931
|
+
}
|
|
3932
|
+
for (const r of target.argRegs) {
|
|
3933
|
+
if (ins.mnemonic === 'bl' || ins.mnemonic === 'blx' || mayWriteReg(ins, r)) {
|
|
3934
|
+
written.add(r);
|
|
3935
|
+
}
|
|
3936
|
+
}
|
|
3937
|
+
}
|
|
3938
|
+
}
|
|
3939
|
+
const narrowReloadOf = new Map<Instr, number>();
|
|
3940
|
+
const releasableHomes = new Set<number>();
|
|
3941
|
+
// the narrow reloads the lift keeps as captures (a held home's, and every one inside the bytes
|
|
3942
|
+
// a first audit asked to keep as one object), which are still reloads of that word for the
|
|
3943
|
+
// outgoing-argument analysis below
|
|
3944
|
+
const capturedReloadOf = new Map<Instr, number>();
|
|
3945
|
+
for (const [load, { slot }] of narrowCandidates) {
|
|
3946
|
+
if (oneObject !== undefined && slot < oneObject.to && slot + 4 > oneObject.from) {
|
|
3947
|
+
capturedReloadOf.set(load, slot);
|
|
3948
|
+
} else if (wordStoredSlots.has(slot) && !otherCaptureOffsets.has(slot) && !inByValueBlock(slot, 4)) {
|
|
3949
|
+
if (argumentHomes.has(slot) && !homesReleased) {
|
|
3950
|
+
releasableHomes.add(slot);
|
|
3951
|
+
capturedReloadOf.set(load, slot);
|
|
3952
|
+
} else {
|
|
3953
|
+
narrowReloadOf.set(load, slot);
|
|
3954
|
+
}
|
|
3955
|
+
}
|
|
3956
|
+
}
|
|
3957
|
+
const narrowReloadCopies: ReadonlySet<Instr> = new Set(
|
|
3958
|
+
[...narrowCandidates].filter(([load]) => narrowReloadOf.has(load)).map(([, n]) => n.copy),
|
|
3959
|
+
);
|
|
3960
|
+
const constantCaptureOffsets: ReadonlySet<number> = new Set(
|
|
3961
|
+
[...captureOffsetOf]
|
|
3962
|
+
.filter(([ins]) => !narrowReloadCopies.has(ins))
|
|
3963
|
+
.map(([, o]) => o)
|
|
3964
|
+
.filter(
|
|
3965
|
+
(o) =>
|
|
3966
|
+
o > 0 ||
|
|
3967
|
+
(o === 0 &&
|
|
3968
|
+
((noCallTakesAStackWord && frameBaseEscapes) ||
|
|
3969
|
+
(frameBaseIsReturnStorage && target.compilerBehaviors.escapedFrameAddressNamesALocal === true))),
|
|
3970
|
+
),
|
|
3971
|
+
);
|
|
3045
3972
|
|
|
3046
3973
|
// THE OUTGOING STACK-ARGUMENT AREA. `frontend/stackargs.ts` holds the licence and every refusal;
|
|
3047
3974
|
// what belongs HERE is the decoding it deliberately does not do — which accesses are whole frame
|
|
@@ -3055,40 +3982,110 @@ function measureThumbFrame({
|
|
|
3055
3982
|
const a = spMemAccess(ins);
|
|
3056
3983
|
return a && !a.regOff && a.width === 4 && a.off % 4 === 0 && a.off >= 0 && a.off + 4 <= localArea ? a.off : null;
|
|
3057
3984
|
};
|
|
3058
|
-
|
|
3059
|
-
|
|
3060
|
-
|
|
3061
|
-
|
|
3062
|
-
|
|
3063
|
-
|
|
3064
|
-
|
|
3065
|
-
|
|
3066
|
-
|
|
3067
|
-
|
|
3068
|
-
|
|
3069
|
-
|
|
3070
|
-
|
|
3071
|
-
|
|
3072
|
-
|
|
3073
|
-
|
|
3074
|
-
|
|
3075
|
-
|
|
3076
|
-
|
|
3077
|
-
|
|
3078
|
-
|
|
3079
|
-
|
|
3080
|
-
|
|
3081
|
-
|
|
3082
|
-
|
|
3083
|
-
|
|
3084
|
-
|
|
3085
|
-
|
|
3086
|
-
|
|
3087
|
-
|
|
3985
|
+
//
|
|
3986
|
+
// AN ESCAPED FRAME ADDRESS IS READ BY ITS RECEIVER, and the event says so just before the call or
|
|
3987
|
+
// the store that hands it over — but only where the target declares the layout premise that
|
|
3988
|
+
// makes the words from that address up a local's (`escapedFrameAddressNamesALocal`).
|
|
3989
|
+
const escapesHere = target.compilerBehaviors.escapedFrameAddressNamesALocal === true;
|
|
3990
|
+
const escapeEvent = (ins: Instr): StackArgsEvent<Instr>[] => {
|
|
3991
|
+
const off = escapedOffsetOf.get(ins);
|
|
3992
|
+
return escapesHere && off !== undefined ? [{ kind: 'escape', off }] : [];
|
|
3993
|
+
};
|
|
3994
|
+
const stagesOutgoing = target.compilerBehaviors.stagesOutgoingArgsInFrame === true;
|
|
3995
|
+
const eventBlocks = asmBlocks.map((ab) => ({
|
|
3996
|
+
events: ab.instrs.flatMap((ins): StackArgsEvent<Instr>[] => {
|
|
3997
|
+
const escaped = escapeEvent(ins);
|
|
3998
|
+
if (escaped.length > 0 && ins.mnemonic === 'str') {
|
|
3999
|
+
return escaped;
|
|
4000
|
+
}
|
|
4001
|
+
// …and a narrow reload of a word slot is that slot's reload, as its word reload would be,
|
|
4002
|
+
// one the lift keeps as a capture included (a held argument home's, or one inside the
|
|
4003
|
+
// one-object bytes): that decides how the lift reads the load, not whether the word is
|
|
4004
|
+
// loaded, and a word this function loads is no outgoing argument
|
|
4005
|
+
const off = slotAcc(ins) ?? narrowReloadOf.get(ins) ?? capturedReloadOf.get(ins) ?? null;
|
|
4006
|
+
if (off !== null && !constantCaptureOffsets.has(off) && !inByValueBlock(off, 4)) {
|
|
4007
|
+
return [{ kind: /^str/.test(ins.mnemonic) ? 'store' : 'load', off }];
|
|
4008
|
+
}
|
|
4009
|
+
if (ins.mnemonic === 'bl' || ins.mnemonic === 'blx') {
|
|
4010
|
+
const callee = ins.ops[0] ?? '?';
|
|
4011
|
+
const declared = calleeRegister(ins, target) === null ? calls.declaredCall(callee) : null;
|
|
4012
|
+
return [
|
|
4013
|
+
...escaped,
|
|
4014
|
+
{
|
|
4015
|
+
kind: 'call',
|
|
4016
|
+
call: ins,
|
|
4017
|
+
callee,
|
|
4018
|
+
declared: declared === null ? null : outgoingBlock(declared.widths, target),
|
|
4019
|
+
params: declared?.params,
|
|
4020
|
+
// where nothing declares a named call, what its callee's body loads of the stack words
|
|
4021
|
+
...(declared === null && calleeRegister(ins, target) === null && stagesOutgoing
|
|
4022
|
+
? { bodyReads: () => stackWordsOf?.(callee) }
|
|
4023
|
+
: {}),
|
|
4024
|
+
},
|
|
4025
|
+
];
|
|
4026
|
+
}
|
|
4027
|
+
return [];
|
|
4028
|
+
}),
|
|
4029
|
+
}));
|
|
4030
|
+
// THE LOADED-WORD BOUND, read once (`outgoingAreaNeverLoadedFrom`, 0 on agbcc): the outgoing
|
|
4031
|
+
// argument analysis and the frame-object audit both take this number.
|
|
4032
|
+
const loadedFrom = target.compilerBehaviors.outgoingAreaNeverLoadedFrom;
|
|
4033
|
+
const loadedBound = loadedWordBound(eventBlocks, entryReachable, loadedFrom);
|
|
4034
|
+
const outgoingArgsOver = (blocks: readonly StackArgsBlock<Instr>[]): OutgoingArgs<Instr> =>
|
|
4035
|
+
analyzeOutgoingArgs<Instr>({
|
|
4036
|
+
blocks,
|
|
4037
|
+
preds,
|
|
4038
|
+
live: entryReachable,
|
|
4039
|
+
localArea,
|
|
4040
|
+
argRegs: target.argRegs.length,
|
|
4041
|
+
capturedWholeFrame: capturedObjectIsTheWholeFrame,
|
|
4042
|
+
loadedWordBound: loadedBound,
|
|
4043
|
+
});
|
|
4044
|
+
const outgoingArgs = outgoingArgsOver(eventBlocks);
|
|
4045
|
+
// THE ESCAPES THE VERDICT LEANS ON, re-asked of the lifted IR (`auditFrameObjects`'
|
|
4046
|
+
// `escapesReliedOn`). The `escape` event is read off what a REGISTER holds at a `bl` or a `str`,
|
|
4047
|
+
// and it switches refusals OFF; the lift then decides what the call is really handed, and an
|
|
4048
|
+
// arity that shrinks after the event was read (a guess, then the trim) can leave the address out
|
|
4049
|
+
// of the call while its stores stay excused. The one-word licence has this re-check
|
|
4050
|
+
// (`recheckWholeFramePremise`); this is the same one at any frame size. Only where the analysis
|
|
4051
|
+
// would refuse WITHOUT the escapes, so an escape that excused nothing asks nothing of the IR.
|
|
4052
|
+
const escapeOffsets = new Set(
|
|
4053
|
+
[...entryReachable].flatMap((b) => eventBlocks[b].events.flatMap((ev) => (ev.kind === 'escape' ? [ev.off] : []))),
|
|
4054
|
+
);
|
|
4055
|
+
const escapesReliedOn: ReadonlySet<number> =
|
|
4056
|
+
outgoingArgs.blocker === null &&
|
|
4057
|
+
escapeOffsets.size > 0 &&
|
|
4058
|
+
outgoingArgsOver(eventBlocks.map((b) => ({ events: b.events.filter((ev) => ev.kind !== 'escape') }))).blocker !==
|
|
4059
|
+
null
|
|
4060
|
+
? escapeOffsets
|
|
4061
|
+
: new Set();
|
|
4062
|
+
const callsReliedOn: ReadonlySet<Instr> = new Set(
|
|
4063
|
+
escapesReliedOn.size === 0
|
|
4064
|
+
? []
|
|
4065
|
+
: [...escapedOffsetOf.keys()].filter((ins) => ins.mnemonic === 'bl' || ins.mnemonic === 'blx'),
|
|
4066
|
+
);
|
|
3088
4067
|
|
|
3089
4068
|
const slotsOffReason = slotModelBlocker(asmBlocks, localArea, outgoingArgs);
|
|
3090
4069
|
const slotsOk = slotsOffReason === null;
|
|
3091
4070
|
|
|
4071
|
+
// THE DEPTH EVERY OTHER BLOCK RUNS AT: what the entry block leaves, where `frameDepthBlocker`
|
|
4072
|
+
// proves no block but a returning one moves sp again. Not where the entry is itself a loop
|
|
4073
|
+
// target (its prologue would run twice) or returns (no block runs after it).
|
|
4074
|
+
const entry = asmBlocks[0];
|
|
4075
|
+
const entryWalk = makeFrameWalk({ argRegs: target.argRegs, start: 0 });
|
|
4076
|
+
for (const ins of entry.instrs) {
|
|
4077
|
+
if (!classifyXfer(ins)) {
|
|
4078
|
+
entryWalk.step(ins);
|
|
4079
|
+
}
|
|
4080
|
+
}
|
|
4081
|
+
const entryEnd = entry.instrs[entry.instrs.length - 1];
|
|
4082
|
+
const settledDepth =
|
|
4083
|
+
frameDepthBlocker(asmBlocks) === null &&
|
|
4084
|
+
preds[0].length === 0 &&
|
|
4085
|
+
!(entryEnd !== undefined && classifyXfer(entryEnd) === 'return')
|
|
4086
|
+
? entryWalk.depthNow()
|
|
4087
|
+
: null;
|
|
4088
|
+
|
|
3092
4089
|
// …AND THE ONE OFFSET THAT MUST NOT BE A SLOT. When `capturedObjectIsTheWholeFrame` holds, the
|
|
3093
4090
|
// frame is one word and a callee is being handed its address, so an `[sp,#0]` access is an access
|
|
3094
4091
|
// 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
|
|
@@ -3121,6 +4118,7 @@ function measureThumbFrame({
|
|
|
3121
4118
|
width === 4 &&
|
|
3122
4119
|
((capturedObjectIsTheWholeFrame && off === 0) ||
|
|
3123
4120
|
(constantCaptureOffsets.has(off) && off >= outgoingArgs.area && off + 4 <= localArea) ||
|
|
4121
|
+
(inByValueBlock(off, 4) && off >= outgoingArgs.area && off + 4 <= localArea) ||
|
|
3124
4122
|
(oneObject !== undefined && off >= oneObject.from && off + 4 <= oneObject.to));
|
|
3125
4123
|
|
|
3126
4124
|
// LOCAL STACK SLOTS. A spill or a local that never has its address taken is transparent to
|
|
@@ -3181,14 +4179,23 @@ function measureThumbFrame({
|
|
|
3181
4179
|
return {
|
|
3182
4180
|
localArea,
|
|
3183
4181
|
savedRegs,
|
|
4182
|
+
saveCarriers,
|
|
3184
4183
|
capturedObjectIsTheWholeFrame,
|
|
4184
|
+
escapesReliedOn,
|
|
4185
|
+
callsReliedOn,
|
|
3185
4186
|
captureOffsetOf,
|
|
4187
|
+
frameAddressesAtCall: frameAddressesAtCalls(asmBlocks, preds, entryReachable, calls.callClobbers),
|
|
3186
4188
|
outgoingArgs,
|
|
4189
|
+
loadedWords: loadedFrom === undefined ? undefined : { from: loadedFrom, bound: loadedBound },
|
|
3187
4190
|
slotsOffReason,
|
|
3188
4191
|
slotsOk,
|
|
4192
|
+
settledDepth,
|
|
3189
4193
|
partition,
|
|
3190
4194
|
isFrameObjectAccess,
|
|
3191
4195
|
isOwnFrameWordSlot,
|
|
4196
|
+
narrowReloadOf,
|
|
4197
|
+
narrowReloadCopies,
|
|
4198
|
+
releasableHomes,
|
|
3192
4199
|
};
|
|
3193
4200
|
}
|
|
3194
4201
|
|
|
@@ -3216,6 +4223,13 @@ function measureThumbFrame({
|
|
|
3216
4223
|
// question, and there the capture has to reach the consumer UNTOUCHED (`mentionsReg`): a copy
|
|
3217
4224
|
// addressed through and still live at a call is an addressing copy, not an argument —
|
|
3218
4225
|
// `EReader_Reset` is `mov r1, sp / strh r0, [r1] / … / bl` into callees that take nothing.
|
|
4226
|
+
// WHAT THE RECEIVER CAN READ is the third question, the outgoing-argument analysis's `escape`
|
|
4227
|
+
// event, and it reads the first rule: sa3's `sub_8060940` fills its object through the very copy it
|
|
4228
|
+
// passes (`mov r0, sp / strb r3, [r0, #8] / bl use`). What keeps `EReader_Reset` out there is the
|
|
4229
|
+
// receiver rule both share (`frameAddressReceivers`): a callee declared to take nothing is handed
|
|
4230
|
+
// no register. And an undeclared callee that took a stack word needs all four argument registers
|
|
4231
|
+
// written for it first, which the write rule sees, so a capture still held in one at the `bl` is
|
|
4232
|
+
// passed to it.
|
|
3219
4233
|
//
|
|
3220
4234
|
// A `bl` CLEARS EVERY HELD REGISTER, and for the callee escape that is the ABI — the argument
|
|
3221
4235
|
// registers are the only ones it tests and the callee clobbers them. The publish escape can hold
|
|
@@ -3256,6 +4270,13 @@ function heldFrameWalk(
|
|
|
3256
4270
|
// null. The shapes that carry one: the capture itself (`mov rD, sp`, `add rD, sp, #k`), a copy
|
|
3257
4271
|
// of a held register (`mov rD, rS`, and agbcc's `add rD, rS, #0`), and a held register moved by
|
|
3258
4272
|
// a constant (`add rD, rS, #c`, `add rD, #c`). Everything else only kills.
|
|
4273
|
+
//
|
|
4274
|
+
// …AND A FRAME ADDRESS MOVED BY A REGISTER, at an offset nothing here knows (NaN): sp or a held
|
|
4275
|
+
// register plus a runtime index, `add r2, r2, sp` — agbcc's `&buf[i]` — or `mov r1, sp / add r0,
|
|
4276
|
+
// r1, r4`. It names no one word, so no walk that asks for an offset can credit it (NaN equals
|
|
4277
|
+
// none, and an escape at NaN is no escape); what it is carried for is the "may hold a frame
|
|
4278
|
+
// address" questions: without it an undeclared call's guessed arity drops `&buf[i]` with no
|
|
4279
|
+
// refusal (`g3(a, b, &buf[i])` as `g3()`).
|
|
3259
4280
|
function frameAddressDefined(ins: Instr, held: ReadonlyMap<string, number>): [string, number] | null {
|
|
3260
4281
|
const [d, s1, s2] = ins.ops;
|
|
3261
4282
|
if (d === undefined || isSpReg(d)) {
|
|
@@ -3269,15 +4290,241 @@ function frameAddressDefined(ins: Instr, held: ReadonlyMap<string, number>): [st
|
|
|
3269
4290
|
}
|
|
3270
4291
|
if (/^adds?$/.test(ins.mnemonic)) {
|
|
3271
4292
|
const [src, by] = s2 === undefined ? [d, s1] : [s1, s2];
|
|
3272
|
-
if (src === undefined || by === undefined
|
|
4293
|
+
if (src === undefined || by === undefined) {
|
|
3273
4294
|
return null;
|
|
3274
4295
|
}
|
|
3275
|
-
const
|
|
4296
|
+
const at = (r: string): number | undefined => (isSpReg(r) ? 0 : held.get(reg(r)));
|
|
4297
|
+
if (!IMM_LITERAL.test(by)) {
|
|
4298
|
+
return at(src) !== undefined || at(by) !== undefined ? [reg(d), NaN] : null;
|
|
4299
|
+
}
|
|
4300
|
+
const from = at(src);
|
|
3276
4301
|
return from === undefined ? null : [reg(d), from + imm(by)];
|
|
3277
4302
|
}
|
|
3278
4303
|
return null;
|
|
3279
4304
|
}
|
|
3280
4305
|
|
|
4306
|
+
// A NARROW RELOAD OF A WORD SLOT, read through a copy of sp. agbcc promotes a `u8`/`u16` local to
|
|
4307
|
+
// a word (thumb.h PROMOTE_MODE), spills it with `str`, and where a use is narrow it reloads only
|
|
4308
|
+
// the narrow part. Thumb has no sp-relative `ldrb`/`ldrh`, so that reload goes through a copy of
|
|
4309
|
+
// sp made for it alone: `add r3, sp, #0x28 / ldrb r3, [r3]` (a byte slot past the `ldrb` offset
|
|
4310
|
+
// range), `mov r2, sp / ldrh r2, [r2, #0x4]`. The narrow bytes are the slot's LOW-ORDER part —
|
|
4311
|
+
// on a little-endian target its own address, on a big-endian one its last `width` bytes — so the
|
|
4312
|
+
// load reads the stored word's value narrowed, `zext(sp@<slot>)`, and the copy names no object.
|
|
4313
|
+
//
|
|
4314
|
+
// What this finds is a shape and not yet a licence; the copy is a capture everywhere it fails:
|
|
4315
|
+
// * the copy is `mov rD, sp` or `add rD, sp, #c`, entry-reachable, and in ONE block it is read
|
|
4316
|
+
// by exactly one instruction, a zero-extending `ldrb`/`ldrh` at an immediate offset from rD,
|
|
4317
|
+
// after which the address is dead: the load writes rD itself, or it writes another register
|
|
4318
|
+
// and the caller's `diesAfter` proves no path from it reads rD before writing it. Anything else
|
|
4319
|
+
// that mentions rD before the load (a second load, a store through it, an escape), a call, a
|
|
4320
|
+
// control transfer or a write of sp ends the search, and the copy stays a capture.
|
|
4321
|
+
// * the load's bytes are the low-order part of a whole aligned word of the reserved local area.
|
|
4322
|
+
// A load at any other byte of the word is not that word's value narrowed, whatever it reads.
|
|
4323
|
+
// * `ldrsb`/`ldrsh` exist only register-offset in Thumb-1, and agbcc's signed use reloads a word
|
|
4324
|
+
// and shifts (measured), so no signed shape is taken.
|
|
4325
|
+
// What the caller then adds, because it needs the frame model: a word `str` to the slot in live
|
|
4326
|
+
// code, the slot named by no other capture, no argument's home, outside a by-value block and
|
|
4327
|
+
// outside the one object (`measureThumbFrame`), and at the lowering a slot the word reload would
|
|
4328
|
+
// read (`lowerLoad` `readsOwnSlot`). A narrow STORE into
|
|
4329
|
+
// the slot through any other address is the frame-object audit's: it sees that slot used and the
|
|
4330
|
+
// object overlapping it, the "one byte, two models" refusal.
|
|
4331
|
+
function narrowSlotReloads(
|
|
4332
|
+
asmBlocks: readonly AsmBlock[],
|
|
4333
|
+
entryReachable: ReadonlySet<number>,
|
|
4334
|
+
localArea: number,
|
|
4335
|
+
littleEndian: boolean,
|
|
4336
|
+
diesAfter: (b: number, from: number, r: string) => boolean,
|
|
4337
|
+
): Map<Instr, { copy: Instr; slot: number }> {
|
|
4338
|
+
const found = new Map<Instr, { copy: Instr; slot: number }>();
|
|
4339
|
+
for (const b of entryReachable) {
|
|
4340
|
+
const instrs = asmBlocks[b].instrs;
|
|
4341
|
+
instrs.forEach((copy, at) => {
|
|
4342
|
+
const [d, s1, s2] = copy.ops;
|
|
4343
|
+
const from = capturesSp(copy)
|
|
4344
|
+
? 0
|
|
4345
|
+
: /^adds?$/.test(copy.mnemonic) && isSpReg(s1) && s2 !== undefined && IMM_LITERAL.test(s2)
|
|
4346
|
+
? imm(s2)
|
|
4347
|
+
: null;
|
|
4348
|
+
if (from === null || d === undefined || isSpReg(d)) {
|
|
4349
|
+
return;
|
|
4350
|
+
}
|
|
4351
|
+
const rD = reg(d);
|
|
4352
|
+
for (let li = at + 1; li < instrs.length; li++) {
|
|
4353
|
+
const ins = instrs[li];
|
|
4354
|
+
if (ins.mnemonic === 'bl' || ins.mnemonic === 'blx' || classifyXfer(ins) !== null || modifiesSp(ins)) {
|
|
4355
|
+
return;
|
|
4356
|
+
}
|
|
4357
|
+
if (!mentionsReg(ins, rD)) {
|
|
4358
|
+
continue;
|
|
4359
|
+
}
|
|
4360
|
+
const width = ins.mnemonic === 'ldrb' ? 1 : ins.mnemonic === 'ldrh' ? 2 : 0;
|
|
4361
|
+
const addr = ins.ops[1] === undefined ? null : parseAddr(ins.ops[1]);
|
|
4362
|
+
if (width === 0 || addr === null || reg(addr.base) !== rD || addr.regOff) {
|
|
4363
|
+
return;
|
|
4364
|
+
}
|
|
4365
|
+
if (reg(ins.ops[0] ?? '') !== rD && !diesAfter(b, li + 1, rD)) {
|
|
4366
|
+
return;
|
|
4367
|
+
}
|
|
4368
|
+
const slot = from + addr.off - (littleEndian ? 0 : 4 - width);
|
|
4369
|
+
if (slot % 4 === 0 && slot >= 0 && slot + 4 <= localArea) {
|
|
4370
|
+
found.set(ins, { copy, slot });
|
|
4371
|
+
}
|
|
4372
|
+
return;
|
|
4373
|
+
}
|
|
4374
|
+
});
|
|
4375
|
+
}
|
|
4376
|
+
return found;
|
|
4377
|
+
}
|
|
4378
|
+
|
|
4379
|
+
// Every register that holds a frame address at some instruction of live code — the shapes
|
|
4380
|
+
// `frameAddressDefined` carries, closed over every block at once rather than along one. So it is
|
|
4381
|
+
// a SUPERSET of every block's `held`, and what it EXCLUDES is the only thing it is used for: a
|
|
4382
|
+
// store through one of these is not a store outside the frame. A frame address reloaded from
|
|
4383
|
+
// memory is not followed; the store through it then counts as leaving the frame, which is the
|
|
4384
|
+
// imprecision `frameAddressReceivers` states.
|
|
4385
|
+
function registersThatMayHoldAFrameAddress(
|
|
4386
|
+
asmBlocks: readonly AsmBlock[],
|
|
4387
|
+
entryReachable: ReadonlySet<number>,
|
|
4388
|
+
): ReadonlySet<string> {
|
|
4389
|
+
const held = new Map<string, number>();
|
|
4390
|
+
for (let grew = true; grew;) {
|
|
4391
|
+
grew = false;
|
|
4392
|
+
for (const b of entryReachable) {
|
|
4393
|
+
for (const ins of asmBlocks[b].instrs) {
|
|
4394
|
+
// CONSTANT OFFSETS ONLY. Closed over every block at once, an index sum (NaN) would carry
|
|
4395
|
+
// the frame base through every `add rD, rS, rT` in the function — measured: klonoa's
|
|
4396
|
+
// `DmaControllerInit` then held its DMA register's base, and its publish stopped counting
|
|
4397
|
+
// as one. A store through `&buf[i]` stays the counted-as-leaving imprecision stated above.
|
|
4398
|
+
const defined = frameAddressDefined(ins, held);
|
|
4399
|
+
if (defined !== null && Number.isFinite(defined[1]) && !held.has(defined[0])) {
|
|
4400
|
+
held.set(defined[0], 0); // the offset is not the question here, only the register
|
|
4401
|
+
grew = true;
|
|
4402
|
+
}
|
|
4403
|
+
}
|
|
4404
|
+
}
|
|
4405
|
+
}
|
|
4406
|
+
return new Set(held.keys());
|
|
4407
|
+
}
|
|
4408
|
+
|
|
4409
|
+
// THE FRAME ADDRESSES ARGUMENT REGISTERS HOLD AT EACH CALL, two ways. The shapes carried are
|
|
4410
|
+
// `frameAddressDefined`'s, and a write of anything else ends one, as in `heldFrameWalk`.
|
|
4411
|
+
// - `may`: on SOME path from the entry, ACROSS blocks, joined by union — the question is a
|
|
4412
|
+
// refusal's, so a capture made in one block and handed over in the next (`mov r5, sp` / … /
|
|
4413
|
+
// `.L3: add r3, r5, #0` / `bl`) must count, and a call ends only what it clobbers, because a
|
|
4414
|
+
// capture in r4-r7 outlives it. Two paths that disagree on the offset leave it unknown (NaN).
|
|
4415
|
+
// The entry block starts empty, which keeps an incoming argument register from counting:
|
|
4416
|
+
// nothing arrives in it but the caller's value.
|
|
4417
|
+
// - `must`: defined in the call's own block, which every path to the call runs.
|
|
4418
|
+
interface FrameAddressesAtCall {
|
|
4419
|
+
readonly may: ReadonlyMap<string, number>;
|
|
4420
|
+
readonly must: ReadonlyMap<string, number>;
|
|
4421
|
+
}
|
|
4422
|
+
function frameAddressesAtCalls(
|
|
4423
|
+
asmBlocks: readonly AsmBlock[],
|
|
4424
|
+
preds: readonly (readonly number[])[],
|
|
4425
|
+
entryReachable: ReadonlySet<number>,
|
|
4426
|
+
clobbers: readonly string[],
|
|
4427
|
+
): ReadonlyMap<Instr, FrameAddressesAtCall> {
|
|
4428
|
+
const atCall = new Map<Instr, FrameAddressesAtCall>();
|
|
4429
|
+
const out = asmBlocks.map(() => new Map<string, number>());
|
|
4430
|
+
const step = (ins: Instr, held: Map<string, number>): void => {
|
|
4431
|
+
if (ins.mnemonic === 'bl' || ins.mnemonic === 'blx') {
|
|
4432
|
+
clobbers.forEach((r) => held.delete(r));
|
|
4433
|
+
return;
|
|
4434
|
+
}
|
|
4435
|
+
const carried = frameAddressDefined(ins, held);
|
|
4436
|
+
for (const r of [...held.keys()]) {
|
|
4437
|
+
if (mayWriteReg(ins, r)) {
|
|
4438
|
+
held.delete(r);
|
|
4439
|
+
}
|
|
4440
|
+
}
|
|
4441
|
+
if (carried !== null) {
|
|
4442
|
+
held.set(carried[0], carried[1]);
|
|
4443
|
+
}
|
|
4444
|
+
};
|
|
4445
|
+
const flow = (b: number, record: boolean): Map<string, number> => {
|
|
4446
|
+
const may = new Map<string, number>();
|
|
4447
|
+
for (const p of preds[b] ?? []) {
|
|
4448
|
+
for (const [r, off] of out[p]) {
|
|
4449
|
+
may.set(r, !may.has(r) || may.get(r) === off ? off : NaN);
|
|
4450
|
+
}
|
|
4451
|
+
}
|
|
4452
|
+
const must = new Map<string, number>();
|
|
4453
|
+
for (const ins of asmBlocks[b].instrs) {
|
|
4454
|
+
if (record && (ins.mnemonic === 'bl' || ins.mnemonic === 'blx')) {
|
|
4455
|
+
atCall.set(ins, { may: new Map(may), must: new Map(must) });
|
|
4456
|
+
}
|
|
4457
|
+
step(ins, may);
|
|
4458
|
+
step(ins, must);
|
|
4459
|
+
}
|
|
4460
|
+
return may;
|
|
4461
|
+
};
|
|
4462
|
+
const same = (a: ReadonlyMap<string, number>, b: ReadonlyMap<string, number>) =>
|
|
4463
|
+
a.size === b.size && [...a].every(([r, off]) => b.has(r) && Object.is(b.get(r), off));
|
|
4464
|
+
for (let changed = true; changed;) {
|
|
4465
|
+
changed = false;
|
|
4466
|
+
for (const b of entryReachable) {
|
|
4467
|
+
const next = flow(b, false);
|
|
4468
|
+
if (!same(next, out[b])) {
|
|
4469
|
+
out[b] = next;
|
|
4470
|
+
changed = true;
|
|
4471
|
+
}
|
|
4472
|
+
}
|
|
4473
|
+
}
|
|
4474
|
+
for (const b of entryReachable) {
|
|
4475
|
+
flow(b, true);
|
|
4476
|
+
}
|
|
4477
|
+
return atCall;
|
|
4478
|
+
}
|
|
4479
|
+
|
|
4480
|
+
// THE REGISTERS AN INSTRUCTION HANDS TO CODE OUTSIDE THIS FUNCTION — the one definition of a frame
|
|
4481
|
+
// address escaping, which every escape walk in `measureThumbFrame` reads, so which registers a
|
|
4482
|
+
// callee reads and which stores leave the frame are decided once.
|
|
4483
|
+
//
|
|
4484
|
+
// A CALL hands over the argument registers its callee's DECLARATION reads — one per 32-bit word,
|
|
4485
|
+
// so `g(void)` is handed nothing — and every argument register where nothing declares it. A call
|
|
4486
|
+
// through a register (`blx r3`, a `_call_via_r3` thunk) branches THROUGH that register; it does not
|
|
4487
|
+
// pass it. "Every register" is wider than what the lift passes an undeclared callee — a guessed
|
|
4488
|
+
// count, or the bound of a call through an argument register — and the two agree on a frame address
|
|
4489
|
+
// only because the lowering refuses to leave one out (`lowerCall`'s `leftOut`, over
|
|
4490
|
+
// `frameAddressesAtCalls`, which holds at each call a superset of what the walks here hold) — of
|
|
4491
|
+
// the registers its guess leaves out, of those the trim cuts afterwards (`GuessedCallSite.
|
|
4492
|
+
// trimmed`), and of those the call's own block wrote that the ranked `/setup-args` reading would
|
|
4493
|
+
// cut, which is withheld for that site instead (it runs in the ranker, after the audit below). Where
|
|
4494
|
+
// the outgoing-argument verdict leans on an escape, the audit re-proves it of the lifted IR as well
|
|
4495
|
+
// (`escapesReliedOn`), which catches a shrink inside the lift that bypassed the question, and
|
|
4496
|
+
// `/setup-args` is not offered at a call that hands out such an escape (`lowerCall`'s
|
|
4497
|
+
// `withholdsSetupArgs`), since this definition read every argument register it holds as handed
|
|
4498
|
+
// over; at the function's other calls the reading asks `leftOut` of every register it cuts
|
|
4499
|
+
// (`asksEveryCut`), an earlier block's frame address included. Elsewhere that a value an EARLIER
|
|
4500
|
+
// block left is dead is the whole guess of that reading, and agbcc does leave a dead `mov r3, sp`
|
|
4501
|
+
// base in a call's argument registers (sa3 sub_805DADC). Those cuts are asked of the lifted IR instead: the reading is withheld where the cut leaves the object
|
|
4502
|
+
// it addresses with no reader (`frontend/ssa.ts` cutsUnreadFrameObjects).
|
|
4503
|
+
//
|
|
4504
|
+
// A WORD `str` hands its source register to whoever reads the memory it writes, unless the base is
|
|
4505
|
+
// sp or any register that may hold a frame address (`registersThatMayHoldAFrameAddress`, whole-
|
|
4506
|
+
// function): then the address is written INTO this frame, and nothing outside has been handed it.
|
|
4507
|
+
// A frame address the base reached through memory is the one this misses, and it counts as
|
|
4508
|
+
// leaving. A `strh`/`strb` hands over part of an address, which no reader can follow.
|
|
4509
|
+
function frameAddressReceivers(
|
|
4510
|
+
ins: Instr,
|
|
4511
|
+
target: TargetDescription,
|
|
4512
|
+
calls: CallDeclarations,
|
|
4513
|
+
mayHoldFrameAddress: ReadonlySet<string>,
|
|
4514
|
+
): readonly string[] {
|
|
4515
|
+
if (ins.mnemonic === 'bl' || ins.mnemonic === 'blx') {
|
|
4516
|
+
const targetReg = calleeRegister(ins, target);
|
|
4517
|
+
const declared = targetReg === null ? calls.declaredCall(ins.ops[0] ?? '?') : null;
|
|
4518
|
+
const words = declared?.widths.reduce((n, w) => n + Math.ceil(w / 32), 0) ?? target.argRegs.length;
|
|
4519
|
+
return target.argRegs.slice(0, words).filter((r) => r !== targetReg);
|
|
4520
|
+
}
|
|
4521
|
+
if (ins.mnemonic !== 'str') {
|
|
4522
|
+
return [];
|
|
4523
|
+
}
|
|
4524
|
+
const { base } = parseAddr(ins.ops[1] ?? '');
|
|
4525
|
+
return isSpReg(base) || mayHoldFrameAddress.has(reg(base)) ? [] : [reg(ins.ops[0] ?? '')];
|
|
4526
|
+
}
|
|
4527
|
+
|
|
3281
4528
|
// Is the word-slot model safe for THIS function? Every disqualifier below leaves every `[sp,#k]`
|
|
3282
4529
|
// access on the old path, which declines — so the answer to "not sure" is the loud one.
|
|
3283
4530
|
// Returns null when the word-slot model is SAFE for this function, else the reason it is off —
|
|
@@ -3309,13 +4556,54 @@ function slotModelBlocker(
|
|
|
3309
4556
|
// frameless function taking sp's address has nothing to model and refuses in those arms.
|
|
3310
4557
|
}
|
|
3311
4558
|
}
|
|
3312
|
-
|
|
3313
|
-
|
|
3314
|
-
|
|
3315
|
-
|
|
3316
|
-
//
|
|
3317
|
-
//
|
|
3318
|
-
//
|
|
4559
|
+
const depthBlocker = frameDepthBlocker(asmBlocks);
|
|
4560
|
+
if (depthBlocker !== null) {
|
|
4561
|
+
return depthBlocker;
|
|
4562
|
+
}
|
|
4563
|
+
// OUTGOING ARGUMENTS — one analysis, and its refusals are this guard's. `analyzeOutgoingArgs`
|
|
4564
|
+
// decides per call whether the words staged at the bottom of the frame are a callee's
|
|
4565
|
+
// arguments; everything it cannot license refuses here, so a `[sp,#k]` access in such a
|
|
4566
|
+
// function still declines loud.
|
|
4567
|
+
if (outgoing.blocker !== null) {
|
|
4568
|
+
return outgoing.blocker;
|
|
4569
|
+
}
|
|
4570
|
+
// A `pop`/`ldm` off sp READS frame memory, and push/pop are transparent to dataflow, so a pop
|
|
4571
|
+
// taken while the local area is still reserved reads a slot this model has retargeted into SSA
|
|
4572
|
+
// — the load simply disagrees with the store. A real epilogue releases the locals first
|
|
4573
|
+
// (`add sp,#N; pop {…}`), which is what this requires; `pop {r1}` mid-frame does not.
|
|
4574
|
+
for (const ab of asmBlocks) {
|
|
4575
|
+
let released = 0;
|
|
4576
|
+
for (const ins of ab.instrs) {
|
|
4577
|
+
if ((ins.mnemonic === 'pop' || ins.mnemonic === 'ldmia') && released < localArea) {
|
|
4578
|
+
const base = ins.mnemonic === 'pop' ? 'sp' : (ins.ops[0] ?? '').replace(/!$/, '');
|
|
4579
|
+
if (isSpReg(base)) {
|
|
4580
|
+
return 'a pop reads the frame while the local area is still reserved';
|
|
4581
|
+
}
|
|
4582
|
+
}
|
|
4583
|
+
if ((ins.mnemonic === 'add' || ins.mnemonic === 'sub') && isSpReg(ins.ops[0])) {
|
|
4584
|
+
const d = spAdjust(ins);
|
|
4585
|
+
if (d !== null && d > 0) {
|
|
4586
|
+
released += d;
|
|
4587
|
+
}
|
|
4588
|
+
}
|
|
4589
|
+
}
|
|
4590
|
+
}
|
|
4591
|
+
return null;
|
|
4592
|
+
}
|
|
4593
|
+
|
|
4594
|
+
// Is sp ONE VALUE at every frame access of this function, the one its prologue leaves? Null when it
|
|
4595
|
+
// is, else why not. The slot model needs it because a slot's key IS the raw offset, and an incoming
|
|
4596
|
+
// stack argument read outside the entry block needs it because its index is the offset less the
|
|
4597
|
+
// depth (`makeFrameWalk`).
|
|
4598
|
+
//
|
|
4599
|
+
// Two shapes are legitimate and everything else refuses: a PROLOGUE (sp moves before the block
|
|
4600
|
+
// touches the frame) and an EPILOGUE (sp moves after it has finished touching it, and the block
|
|
4601
|
+
// returns, so nothing downstream can key a slot against the changed sp). A modification BETWEEN two
|
|
4602
|
+
// accesses moves the frame under a slot already keyed, and only the entry block may deepen —
|
|
4603
|
+
// elsewhere a pre-access modification would put the block at a different depth from the one the
|
|
4604
|
+
// prologue established. So every block but the entry starts at the depth the entry leaves, and
|
|
4605
|
+
// only a block that returns moves sp.
|
|
4606
|
+
function frameDepthBlocker(asmBlocks: readonly AsmBlock[]): string | null {
|
|
3319
4607
|
for (let bi = 0; bi < asmBlocks.length; bi++) {
|
|
3320
4608
|
const ins = asmBlocks[bi].instrs;
|
|
3321
4609
|
const mems = ins.map((x, i) => (touchesFrame(x) ? i : -1)).filter((i) => i >= 0);
|
|
@@ -3346,34 +4634,6 @@ function slotModelBlocker(
|
|
|
3346
4634
|
}
|
|
3347
4635
|
}
|
|
3348
4636
|
}
|
|
3349
|
-
// OUTGOING ARGUMENTS — one analysis, and its refusals are this guard's. `analyzeOutgoingArgs`
|
|
3350
|
-
// decides per call whether the words staged at the bottom of the frame are a callee's
|
|
3351
|
-
// arguments; everything it cannot license refuses here, so a `[sp,#k]` access in such a
|
|
3352
|
-
// function still declines loud.
|
|
3353
|
-
if (outgoing.blocker !== null) {
|
|
3354
|
-
return outgoing.blocker;
|
|
3355
|
-
}
|
|
3356
|
-
// A `pop`/`ldm` off sp READS frame memory, and push/pop are transparent to dataflow, so a pop
|
|
3357
|
-
// taken while the local area is still reserved reads a slot this model has retargeted into SSA
|
|
3358
|
-
// — the load simply disagrees with the store. A real epilogue releases the locals first
|
|
3359
|
-
// (`add sp,#N; pop {…}`), which is what this requires; `pop {r1}` mid-frame does not.
|
|
3360
|
-
for (const ab of asmBlocks) {
|
|
3361
|
-
let released = 0;
|
|
3362
|
-
for (const ins of ab.instrs) {
|
|
3363
|
-
if ((ins.mnemonic === 'pop' || ins.mnemonic === 'ldmia') && released < localArea) {
|
|
3364
|
-
const base = ins.mnemonic === 'pop' ? 'sp' : (ins.ops[0] ?? '').replace(/!$/, '');
|
|
3365
|
-
if (isSpReg(base)) {
|
|
3366
|
-
return 'a pop reads the frame while the local area is still reserved';
|
|
3367
|
-
}
|
|
3368
|
-
}
|
|
3369
|
-
if ((ins.mnemonic === 'add' || ins.mnemonic === 'sub') && isSpReg(ins.ops[0])) {
|
|
3370
|
-
const d = spAdjust(ins);
|
|
3371
|
-
if (d !== null && d > 0) {
|
|
3372
|
-
released += d;
|
|
3373
|
-
}
|
|
3374
|
-
}
|
|
3375
|
-
}
|
|
3376
|
-
}
|
|
3377
4637
|
return null;
|
|
3378
4638
|
}
|
|
3379
4639
|
|
|
@@ -3437,14 +4697,18 @@ function reservedLocalArea(entry: AsmBlock): number {
|
|
|
3437
4697
|
return Math.max(0, reserved);
|
|
3438
4698
|
}
|
|
3439
4699
|
|
|
4700
|
+
/** the low registers agbcc's callers expect back intact (r0-r3 are argument and scratch) */
|
|
4701
|
+
const CALLEE_SAVED_LOW_REGS: ReadonlySet<string> = new Set(['r4', 'r5', 'r6', 'r7']);
|
|
4702
|
+
|
|
3440
4703
|
// WHAT THE PROLOGUE SAVED — the second half of the register partition (frontend/ssa.ts,
|
|
3441
4704
|
// LiveInModel.uninitRegs), in operand spellings. "The ABI does not pass arguments here" is a fact
|
|
3442
4705
|
// about the CALLER and cannot on its own make a def-less read an uninitialised local; what does is
|
|
3443
4706
|
// that the compiler homed a local in the register, which it may only do after saving it. Asm that
|
|
3444
4707
|
// saves nothing follows no such convention, and some of it really is handed live values there:
|
|
3445
4708
|
// the MP2K engine's hand-written `ChnVolSetAsm`, vendored in klonoa, sa3 and pokeemerald alike,
|
|
3446
|
-
//
|
|
3447
|
-
//
|
|
4709
|
+
// reads r4/r5 with no prologue at all — classified by the ABI alone it stored through reads of
|
|
4710
|
+
// registers nothing ever wrote. Unsaved, they are global register variables
|
|
4711
|
+
// (`LiveInModel.globalRegs`), which is what agbcc compiles `register u8 *p asm("r4")` to.
|
|
3448
4712
|
//
|
|
3449
4713
|
// PER REGISTER, because saving r5 says nothing about r4 — a mid-function fragment reached by
|
|
3450
4714
|
// agbcc's `bl`-as-a-long-branch saves what it uses and is handed the rest.
|
|
@@ -3453,9 +4717,30 @@ function reservedLocalArea(entry: AsmBlock): number {
|
|
|
3453
4717
|
// the set. `HIGH_REGS` have no `push` encoding, so agbcc saves them as `mov rLow, rHi; push
|
|
3454
4718
|
// {rLow}` and the source of such a `mov` joins the set when its low register is pushed — every
|
|
3455
4719
|
// high-register inhabitant in the corpus goes through that idiom.
|
|
3456
|
-
|
|
4720
|
+
//
|
|
4721
|
+
// THAT `mov` IS THE SAVE'S, NOT THE FUNCTION'S, and is returned as `carriers` for the fill to
|
|
4722
|
+
// skip. Lifted, it leaves the low register holding the high one's entry value, so where a local
|
|
4723
|
+
// homed in each is left uninitialised on one path (`synthetic:dma_fill_uninit`: a switch with no
|
|
4724
|
+
// default, ten locals spread over r5-r7 and r8-sl) the two read as one undefined value, and
|
|
4725
|
+
// frontend/ssa.ts `markCopiedUndefs` spells that relation as an assignment between two locals the
|
|
4726
|
+
// source never related. What the machine relates there is the CALLER's register to its own save
|
|
4727
|
+
// copy; no statement of this function computed it. Skipped, the low register's def-less read is
|
|
4728
|
+
// its own undefined value, which the push has saved (so it is in `saved`). The blind side, which
|
|
4729
|
+
// the relation this drops is: a body that reads the low register before writing it and compares
|
|
4730
|
+
// it with the high one sees two unrelated values where the machine holds one — agbcc only does
|
|
4731
|
+
// this for two distinct uninitialised locals, which no statement of the source relates.
|
|
4732
|
+
//
|
|
4733
|
+
// ONLY A CALLEE-SAVED LOW REGISTER THAT AN EARLIER `push` ALREADY SAVED IS A CARRIER. That is the
|
|
4734
|
+
// shape agbcc emits (`push {r4-r7, lr}; mov r7, sl; mov r6, r8; push {r6, r7}`): the low register's
|
|
4735
|
+
// own entry value is safe in the first save, so the `mov` destroys nothing the function could
|
|
4736
|
+
// read. Hand-written asm may stage through anything — `mov r3, r8; push {r3}; add r0, r0, r3`
|
|
4737
|
+
// reads r3 as the high register's value, and skipping that `mov` there would turn r3 into the
|
|
4738
|
+
// caller's fourth argument. Such a `mov` is lifted as the function's own; the high register still
|
|
4739
|
+
// joins `saved`, as it always has.
|
|
4740
|
+
function prologueSavedRegs(entry: AsmBlock): { saved: ReadonlySet<string>; carriers: ReadonlySet<Instr> } {
|
|
3457
4741
|
const saved = new Set<string>();
|
|
3458
|
-
const
|
|
4742
|
+
const carriers = new Set<Instr>();
|
|
4743
|
+
const carries = new Map<string, { mov: Instr; carrier: boolean }>(); // low register ← the `mov` that moved a high one into it
|
|
3459
4744
|
for (const x of entry.instrs) {
|
|
3460
4745
|
if (x.mnemonic === 'push') {
|
|
3461
4746
|
const list = regListOf(x.ops);
|
|
@@ -3464,24 +4749,403 @@ function prologueSavedRegs(entry: AsmBlock): ReadonlySet<string> {
|
|
|
3464
4749
|
}
|
|
3465
4750
|
for (const r of list) {
|
|
3466
4751
|
saved.add(r);
|
|
3467
|
-
const
|
|
3468
|
-
if (
|
|
3469
|
-
saved.add(
|
|
4752
|
+
const staged = carries.get(r);
|
|
4753
|
+
if (staged !== undefined) {
|
|
4754
|
+
saved.add(staged.mov.ops[1]);
|
|
4755
|
+
if (staged.carrier) {
|
|
4756
|
+
carriers.add(staged.mov);
|
|
4757
|
+
}
|
|
4758
|
+
carries.delete(r);
|
|
3470
4759
|
}
|
|
3471
4760
|
}
|
|
3472
4761
|
} else if (/^movs?$/.test(x.mnemonic) && HIGH_REGS.has(x.ops[1] ?? '') && !HIGH_REGS.has(x.ops[0] ?? '')) {
|
|
3473
|
-
carries.set(x.ops[0], x.ops[
|
|
4762
|
+
carries.set(x.ops[0], { mov: x, carrier: CALLEE_SAVED_LOW_REGS.has(x.ops[0]) && saved.has(x.ops[0]) });
|
|
3474
4763
|
} else if (spAdjust(x) === null) {
|
|
3475
4764
|
break;
|
|
3476
4765
|
}
|
|
3477
4766
|
}
|
|
3478
|
-
return saved;
|
|
4767
|
+
return { saved, carriers };
|
|
4768
|
+
}
|
|
4769
|
+
|
|
4770
|
+
/** WHAT A `push` SAVES AROUND EVERY USE, ON EVERY PATH — the prologue's (`prologueSavedRegs`) and
|
|
4771
|
+
* any other. A register the function names nowhere before a `push` of it, nor after the `pop`
|
|
4772
|
+
* that restores it, along any path from the entry, and that every path restores before it
|
|
4773
|
+
* returns, is saved as surely as one the prologue saves (`saved`): its writes are undone and its
|
|
4774
|
+
* entry value is the caller's, so it is a local's home like the prologue's
|
|
4775
|
+
* (`LiveInModel.uninitRegs`). Hand-written asm saves late: `cmp r0, #0; beq .L1; push {r4}; …;
|
|
4776
|
+
* pop {r4}; .L1: bx lr` uses r4 only between its save and its restore. A `push` is nothing but a
|
|
4777
|
+
* save and a `pop` nothing but a restore, which is what lets this read them anywhere; MIPS's `sw`
|
|
4778
|
+
* and PPC's `stw` also store a value (a global register variable passed on the stack is
|
|
4779
|
+
* `sw s0,16(sp)`), so those frontends read saves only in the entry block.
|
|
4780
|
+
*
|
|
4781
|
+
* A high register is saved through the `mov rLow, rHi` that stages it, and that `mov` is the
|
|
4782
|
+
* save's, provided `rLow` reaches the `push` unchanged — PROLOGUESAVEDREGS' RULE, here past the
|
|
4783
|
+
* leading run too, so the two readings of one idiom agree: `mov r0, r8; add r0, #1; push {r0}`
|
|
4784
|
+
* saves no r8. It is restored by `pop {rLow}; mov rHi, rLow` likewise, and that `pop` restores no
|
|
4785
|
+
* `rLow`: agbcc's `pop {r3, r4}; mov r8, r3; mov sl, r4; pop {r4-r7}` hands r4 sl's word first.
|
|
4786
|
+
*
|
|
4787
|
+
* THE RESTORE ENDS THE SAVE. Past it the register holds the caller's value again, so a read there
|
|
4788
|
+
* is the caller's value and a write there is one the caller sees, neither of them a local's —
|
|
4789
|
+
* and the fill keeps the `pop` transparent, so a read would see the function's last write
|
|
4790
|
+
* (`push {r4}; mov r4, #5; pop {r4}; mov r0, r4` would lift `return 5;`). For the same reason a save
|
|
4791
|
+
* that some path carries unrestored to a return the frame is balanced at — a `push` whose word is
|
|
4792
|
+
* popped into another register or dropped with `add sp` — saved nothing on that path. A return
|
|
4793
|
+
* with the word still on the stack is an input whose epilogue was left out, and is not asked: the
|
|
4794
|
+
* blind side is hand-written asm that returns with its frame unbalanced, which no caller survives.
|
|
4795
|
+
*
|
|
4796
|
+
* A RESTORE IS THE REGISTER'S OWN WORD, not its name. The walk keeps the stack word by word —
|
|
4797
|
+
* whose entry value each pushed word holds — and a `pop` (or a staging pop's `mov rHi, rLow`)
|
|
4798
|
+
* restores a register only when the word it loads is that register's: `push {r4, r5}; pop {r5};
|
|
4799
|
+
* pop {r4}` names both and swaps the caller's r4 and r5, and agbcc's staging with its two `mov`s
|
|
4800
|
+
* crossed swaps r8 and r9. A push labels a word with a register only while that register still
|
|
4801
|
+
* holds its entry value (`dirty`): pushed again after a write, a foreign pop or a save dropped
|
|
4802
|
+
* with `add sp`, it pushes a value of the function's, and the pop of that word restores nothing.
|
|
4803
|
+
* Where paths as deep disagree about a word, the word is no register's. Such a pop is a write the
|
|
4804
|
+
* caller sees that the fill, reading the `pop` as transparent, never shows, so a callee-saved
|
|
4805
|
+
* register one reaches a balanced return with is `leaked`, and refused outright (after the
|
|
4806
|
+
* return proof, `poppedForeign`). The blind side: where the stack is not known — paths that
|
|
4807
|
+
* disagree about its depth, a move of sp the frame walk cannot follow, a store through a
|
|
4808
|
+
* register-indexed sp address — a restore is read by name, and a store through a copy of sp
|
|
4809
|
+
* into a pushed word is not seen. `refuseReturnsNotThroughCallersLr`
|
|
4810
|
+
* asks the same word question of lr alone, on its own walk.
|
|
4811
|
+
*
|
|
4812
|
+
* `misSaved` is every register a save or a restore names that some path uses outside a save, or
|
|
4813
|
+
* returns with still saved: a `pop {r4}` with no `push` before it (`mov r0, r4; pop {r4}; bx lr`),
|
|
4814
|
+
* a `push` past a read, a use past the restore, a staged `push {r3}` of r8 popped into r1. Its
|
|
4815
|
+
* writes are not all the caller's and its entry value is no global's, so `registerLiveIns`
|
|
4816
|
+
* refuses it rather than spell it either way. "Uses" is any naming in an operand, read or write. */
|
|
4817
|
+
const CALL_CLOBBERS = ['r0', 'r1', 'r2', 'r3', 'r12', 'r14'];
|
|
4818
|
+
const ALL_REGS = ['r0', 'r1', 'r2', 'r3', 'r4', 'r5', 'r6', 'r7', 'r8', 'r9', 'r10', 'r11', 'r12', 'r14'];
|
|
4819
|
+
function bodySavedRegs(
|
|
4820
|
+
blocks: readonly AsmBlock[],
|
|
4821
|
+
preds: readonly (readonly number[])[],
|
|
4822
|
+
succs: readonly (readonly number[])[],
|
|
4823
|
+
): {
|
|
4824
|
+
saved: ReadonlySet<string>;
|
|
4825
|
+
misSaved: ReadonlySet<string>;
|
|
4826
|
+
leaked: ReadonlyMap<string, Instr>;
|
|
4827
|
+
copied: ReadonlyMap<string, Instr>;
|
|
4828
|
+
returned: ReadonlyMap<string, Instr>;
|
|
4829
|
+
} {
|
|
4830
|
+
// What each instruction saves, restores and otherwise names; the staging `mov`s first.
|
|
4831
|
+
const savesHi = new Map<Instr, string>(); // `mov rLow, rHi` ⇒ rHi, where rLow is pushed unchanged
|
|
4832
|
+
const restoresHi = new Set<Instr>(); // `mov rHi, rLow` where rLow was popped unchanged
|
|
4833
|
+
const stagingPops = new Map<Instr, Set<string>>(); // a `pop`'s rLow that a `mov rHi, rLow` restores through
|
|
4834
|
+
for (const b of blocks) {
|
|
4835
|
+
const staged = new Map<string, Instr>(); // low register ← the `mov` that put a high one there
|
|
4836
|
+
const popped = new Map<string, Instr>(); // low register ← the `pop` that filled it
|
|
4837
|
+
for (const x of b.instrs) {
|
|
4838
|
+
const list = x.mnemonic === 'push' || x.mnemonic === 'pop' ? (regListOf(x.ops) ?? []) : [];
|
|
4839
|
+
if (x.mnemonic === 'push') {
|
|
4840
|
+
list.forEach((r) => staged.has(r) && savesHi.set(staged.get(r)!, staged.get(r)!.ops[1]));
|
|
4841
|
+
}
|
|
4842
|
+
const isMov = /^movs?$/.test(x.mnemonic);
|
|
4843
|
+
const from = isMov && HIGH_REGS.has(x.ops[0] ?? '') ? popped.get(x.ops[1] ?? '') : undefined;
|
|
4844
|
+
if (from !== undefined) {
|
|
4845
|
+
restoresHi.add(x);
|
|
4846
|
+
stagingPops.set(from, (stagingPops.get(from) ?? new Set()).add(x.ops[1]));
|
|
4847
|
+
}
|
|
4848
|
+
const writes = x.mnemonic === 'push' ? [] : x.mnemonic === 'pop' ? list : regWritesOf(x);
|
|
4849
|
+
if (writes === 'all') {
|
|
4850
|
+
staged.clear();
|
|
4851
|
+
popped.clear();
|
|
4852
|
+
} else {
|
|
4853
|
+
for (const w of writes) {
|
|
4854
|
+
staged.delete(w);
|
|
4855
|
+
popped.delete(w);
|
|
4856
|
+
}
|
|
4857
|
+
}
|
|
4858
|
+
if (x.mnemonic === 'pop') {
|
|
4859
|
+
list.forEach((r) => popped.set(r, x));
|
|
4860
|
+
} else if (isMov && HIGH_REGS.has(x.ops[1] ?? '') && !HIGH_REGS.has(x.ops[0] ?? '')) {
|
|
4861
|
+
staged.set(x.ops[0], x);
|
|
4862
|
+
}
|
|
4863
|
+
}
|
|
4864
|
+
}
|
|
4865
|
+
const namedBy = (x: Instr): string[] => [
|
|
4866
|
+
...(x.ops.join(',').match(/(?<![\w.#])(r1[0-5]|r\d|sb|sl|fp|ip)(?![\w])/g) ?? []),
|
|
4867
|
+
...(/^(ldm|stm)/.test(x.mnemonic) ? (regListOf(x.ops.slice(1)) ?? []) : []),
|
|
4868
|
+
];
|
|
4869
|
+
const savedSomewhere = new Set<string>();
|
|
4870
|
+
const restoredSomewhere = new Set<string>();
|
|
4871
|
+
const usedUnsaved = new Set<string>();
|
|
4872
|
+
const unrestored = new Set<string>(); // still saved at a balanced return on some path
|
|
4873
|
+
// Forward dataflow over two sets: the registers saved on EVERY path into each block (`cur`, a
|
|
4874
|
+
// must-set; null: not reached yet) and on SOME path (`open`, a may-set) — a use needs the first,
|
|
4875
|
+
// a return must find the second empty — and the frame depth (`makeFrameWalk`; null where paths
|
|
4876
|
+
// disagree or it is not exact).
|
|
4877
|
+
//
|
|
4878
|
+
// And the STACK, word by word (`stack`, top last; null where paths disagree about its depth or a
|
|
4879
|
+
// move of sp is not exact; a word null where they disagree about it): whose entry value each pushed word holds, so that a restore is a restore only when the
|
|
4880
|
+
// word its `pop` loads is that register's own. `pop {r5}; pop {r4}` after `push {r4, r5}` NAMES
|
|
4881
|
+
// both registers and restores neither — it swaps the caller's r4 and r5.
|
|
4882
|
+
type Stack = readonly (string | null)[];
|
|
4883
|
+
// `clobbered` (a may-set) is each callee-saved register a pop has loaded another word into since
|
|
4884
|
+
// its last restore, with that pop: the fill reads a `pop` as transparent, so no write of the body
|
|
4885
|
+
// shows it, and one that reaches a balanced return is refused here (`leaked`).
|
|
4886
|
+
type State = {
|
|
4887
|
+
cur: Set<string>;
|
|
4888
|
+
open: Set<string>;
|
|
4889
|
+
depth: number | null;
|
|
4890
|
+
stack: Stack | null;
|
|
4891
|
+
clobbered: Map<string, Instr>;
|
|
4892
|
+
dirty: Set<string>;
|
|
4893
|
+
};
|
|
4894
|
+
// `dirty` (a may-set) is each register that may no longer hold its entry value: written, or loaded
|
|
4895
|
+
// by a pop from a word not its own, since the entry or its last restore. A push labels a word with
|
|
4896
|
+
// a register only while it is not: `push {r4}; mov r4, r0; add sp, #4; push {r4}; pop {r4}` pushes
|
|
4897
|
+
// a0 the second time, and that pop is no restore — however its push named the register.
|
|
4898
|
+
const stateIn: (State | null)[] = blocks.map((_, i) =>
|
|
4899
|
+
i === 0 ? { cur: new Set(), open: new Set(), depth: 0, stack: [], clobbered: new Map(), dirty: new Set() } : null,
|
|
4900
|
+
);
|
|
4901
|
+
const leaked = new Map<string, Instr>();
|
|
4902
|
+
const copied = new Map<string, Instr>();
|
|
4903
|
+
const returned = new Map<string, Instr>();
|
|
4904
|
+
const word = (r: string) => canonReg(r) ?? r;
|
|
4905
|
+
const walk = (b: number, record: boolean): State => {
|
|
4906
|
+
const cur = new Set(stateIn[b]!.cur);
|
|
4907
|
+
const open = new Set(stateIn[b]!.open);
|
|
4908
|
+
const clobbered = new Map(stateIn[b]!.clobbered);
|
|
4909
|
+
const dirty = new Set(stateIn[b]!.dirty);
|
|
4910
|
+
const frame = makeFrameWalk({ argRegs: [], start: stateIn[b]!.depth });
|
|
4911
|
+
let stack: (string | null)[] | null = stateIn[b]!.stack === null ? null : [...stateIn[b]!.stack!];
|
|
4912
|
+
const holds = new Map<string, string>(); // low register ← the high one a save's staging `mov` copied
|
|
4913
|
+
const loaded = new Map<string, string | null>(); // a staging pop's low register ← whose word it loaded
|
|
4914
|
+
for (const [i, x] of blocks[b].instrs.entries()) {
|
|
4915
|
+
const before = frame.depthNow();
|
|
4916
|
+
frame.step(x);
|
|
4917
|
+
const after = frame.depthNow();
|
|
4918
|
+
const saves = x.mnemonic === 'push' ? (regListOf(x.ops) ?? []) : savesHi.has(x) ? [savesHi.get(x)!] : [];
|
|
4919
|
+
const named =
|
|
4920
|
+
x.mnemonic === 'pop'
|
|
4921
|
+
? (regListOf(x.ops) ?? []).filter((r) => !stagingPops.get(x)?.has(r))
|
|
4922
|
+
: restoresHi.has(x)
|
|
4923
|
+
? [x.ops[0]]
|
|
4924
|
+
: [];
|
|
4925
|
+
// Which of them the word they load really restores: all, by name, where the stack is not known.
|
|
4926
|
+
let own: ((r: string) => boolean) | null = null;
|
|
4927
|
+
const list = x.mnemonic === 'push' || x.mnemonic === 'pop' ? byRegNumber(regListOf(x.ops) ?? []) : null;
|
|
4928
|
+
if (x.mnemonic === 'push') {
|
|
4929
|
+
// a push stores its highest-numbered register deepest
|
|
4930
|
+
// a word is a register's own only while that register holds its entry value (`dirty`)
|
|
4931
|
+
const label = (r: string) => (dirty.has(holds.get(r) ?? r) ? null : (holds.get(r) ?? r));
|
|
4932
|
+
stack = stack === null || list === null ? null : [...stack, ...list.map(label).reverse()];
|
|
4933
|
+
} else if (x.mnemonic === 'pop') {
|
|
4934
|
+
if (stack === null || list === null || stack.length < list.length) {
|
|
4935
|
+
stack = null;
|
|
4936
|
+
} else {
|
|
4937
|
+
const popped = new Map(list.map((r) => [r, stack!.pop()!] as const));
|
|
4938
|
+
own = (r) => popped.get(word(r)) === word(r);
|
|
4939
|
+
stagingPops.get(x)?.forEach((r) => loaded.set(word(r), popped.get(word(r)) ?? null));
|
|
4940
|
+
}
|
|
4941
|
+
} else if (restoresHi.has(x)) {
|
|
4942
|
+
const from = loaded.get(word(x.ops[1]));
|
|
4943
|
+
own = stack === null || from === undefined ? null : (r) => from === word(r);
|
|
4944
|
+
} else if (stack !== null && /^str[bh]?$/.test(x.mnemonic) && isSpReg(parseAddr(x.ops[1] ?? '').base)) {
|
|
4945
|
+
// a store into a pushed word replaces what it holds
|
|
4946
|
+
const { off, regOff } = parseAddr(x.ops[1] ?? '');
|
|
4947
|
+
if (regOff !== undefined) {
|
|
4948
|
+
stack = null;
|
|
4949
|
+
} else if (Math.floor(off / 4) < stack.length) {
|
|
4950
|
+
stack[stack.length - 1 - Math.floor(off / 4)] = null;
|
|
4951
|
+
}
|
|
4952
|
+
} else if (stack !== null && before !== after) {
|
|
4953
|
+
// any other move of sp (`sub sp, #8`, `add sp, #4`) pushes or drops whole anonymous words
|
|
4954
|
+
const words = before === null || after === null || (after - before) % 4 !== 0 ? null : (after - before) / 4;
|
|
4955
|
+
stack =
|
|
4956
|
+
words === null || stack.length + words < 0
|
|
4957
|
+
? null
|
|
4958
|
+
: words > 0
|
|
4959
|
+
? [...stack, ...Array<null>(words).fill(null)]
|
|
4960
|
+
: stack.slice(0, stack.length + words);
|
|
4961
|
+
}
|
|
4962
|
+
const writes = x.mnemonic === 'push' ? [] : x.mnemonic === 'pop' ? (regListOf(x.ops) ?? []) : regWritesOf(x);
|
|
4963
|
+
if (writes === 'all') {
|
|
4964
|
+
holds.clear();
|
|
4965
|
+
// a call leaves the registers the ABI has it preserve; anything else unread may write any
|
|
4966
|
+
(/^(bl|blx|swi)$/.test(x.mnemonic) ? CALL_CLOBBERS : ALL_REGS).forEach((r) => dirty.add(r));
|
|
4967
|
+
} else {
|
|
4968
|
+
writes.forEach((r) => {
|
|
4969
|
+
holds.delete(word(r));
|
|
4970
|
+
dirty.add(word(r));
|
|
4971
|
+
});
|
|
4972
|
+
}
|
|
4973
|
+
if (savesHi.has(x)) {
|
|
4974
|
+
holds.set(word(x.ops[0]), word(savesHi.get(x)!));
|
|
4975
|
+
}
|
|
4976
|
+
const restores = own === null ? named : named.filter(own);
|
|
4977
|
+
const uses =
|
|
4978
|
+
x.mnemonic === 'push'
|
|
4979
|
+
? []
|
|
4980
|
+
: savesHi.has(x)
|
|
4981
|
+
? [x.ops[0]]
|
|
4982
|
+
: [...named, ...namedBy(x), ...(x.mnemonic === 'pop' ? (regListOf(x.ops) ?? []) : [])];
|
|
4983
|
+
if (record) {
|
|
4984
|
+
saves.forEach((r) => savedSomewhere.add(r));
|
|
4985
|
+
named.forEach((r) => restoredSomewhere.add(r));
|
|
4986
|
+
uses.filter((r) => !cur.has(r)).forEach((r) => usedUnsaved.add(r));
|
|
4987
|
+
}
|
|
4988
|
+
saves
|
|
4989
|
+
.filter((r) => !uses.includes(r))
|
|
4990
|
+
.forEach((r) => {
|
|
4991
|
+
cur.add(r);
|
|
4992
|
+
open.add(r);
|
|
4993
|
+
});
|
|
4994
|
+
restores.forEach((r) => {
|
|
4995
|
+
cur.delete(r);
|
|
4996
|
+
open.delete(r);
|
|
4997
|
+
clobbered.delete(word(r));
|
|
4998
|
+
dirty.delete(word(r));
|
|
4999
|
+
});
|
|
5000
|
+
// A pop that loads another register's word writes this one and restores nothing: it is no
|
|
5001
|
+
// longer saved, and its own save, still open, is unrestored at a balanced return.
|
|
5002
|
+
named
|
|
5003
|
+
.filter((r) => !restores.includes(r))
|
|
5004
|
+
.forEach((r) => {
|
|
5005
|
+
cur.delete(r);
|
|
5006
|
+
if (/^r([4-9]|1[01])$/.test(word(r))) {
|
|
5007
|
+
clobbered.set(word(r), x);
|
|
5008
|
+
} else if (record && own !== null && /^r([0-3]|12)$/.test(word(r))) {
|
|
5009
|
+
// a scratch register: the fill never makes the copy, so a later use of it is refused
|
|
5010
|
+
const use = readsBeforeWrite(blocks, succs, b, i + 1, word(r));
|
|
5011
|
+
const into = use === 'read' ? copied : use === 'returned' ? returned : undefined;
|
|
5012
|
+
if (into !== undefined && !into.has(word(r))) {
|
|
5013
|
+
into.set(word(r), x);
|
|
5014
|
+
}
|
|
5015
|
+
}
|
|
5016
|
+
});
|
|
5017
|
+
// Only a return the frame is balanced at: one with the `push` still on the stack is an
|
|
5018
|
+
// input whose epilogue was left out (a fragment, a fixture), which the prologue's reading
|
|
5019
|
+
// has always taken as restored.
|
|
5020
|
+
if (record && classifyXfer(x) === 'return' && frame.depthNow() === 0) {
|
|
5021
|
+
open.forEach((r) => unrestored.add(r));
|
|
5022
|
+
clobbered.forEach((at, r) => leaked.has(r) || leaked.set(r, at));
|
|
5023
|
+
}
|
|
5024
|
+
}
|
|
5025
|
+
return { cur, open, depth: frame.depthNow(), stack, clobbered, dirty };
|
|
5026
|
+
};
|
|
5027
|
+
const sameStack = (x: Stack | null, y: Stack | null) =>
|
|
5028
|
+
x === null || y === null ? x === y : x.length === y.length && x.every((w, i) => w === y[i]);
|
|
5029
|
+
// Paths whose stacks are as deep keep each word they agree on, and the rest is no register's: a
|
|
5030
|
+
// pop of it restores nothing. Paths that disagree about the depth leave the stack unknown.
|
|
5031
|
+
const joinStacks = (xs: readonly (Stack | null)[]): Stack | null =>
|
|
5032
|
+
xs.some((x) => x === null || x.length !== xs[0]!.length)
|
|
5033
|
+
? null
|
|
5034
|
+
: xs[0]!.map((w, i) => (xs.every((x) => x![i] === w) ? w : null));
|
|
5035
|
+
const sameSet = (x: ReadonlySet<string>, y: ReadonlySet<string>) =>
|
|
5036
|
+
x.size === y.size && [...x].every((r) => y.has(r));
|
|
5037
|
+
for (let changed = true; changed;) {
|
|
5038
|
+
changed = false;
|
|
5039
|
+
for (let b = 1; b < blocks.length; b++) {
|
|
5040
|
+
const ins = preds[b].filter((p) => stateIn[p] !== null).map((p) => walk(p, false));
|
|
5041
|
+
if (ins.length === 0) {
|
|
5042
|
+
continue;
|
|
5043
|
+
}
|
|
5044
|
+
const cur = new Set([...ins[0].cur].filter((r) => ins.every((s) => s.cur.has(r))));
|
|
5045
|
+
const open = new Set(ins.flatMap((s) => [...s.open]));
|
|
5046
|
+
const depth = ins.every((s) => s.depth === ins[0].depth) ? ins[0].depth : null;
|
|
5047
|
+
const stack = joinStacks(ins.map((s) => s.stack));
|
|
5048
|
+
const clobbered = new Map(ins.flatMap((s) => [...s.clobbered]));
|
|
5049
|
+
const dirty = new Set(ins.flatMap((s) => [...s.dirty]));
|
|
5050
|
+
const was = stateIn[b];
|
|
5051
|
+
if (
|
|
5052
|
+
was === null ||
|
|
5053
|
+
was.cur.size !== cur.size ||
|
|
5054
|
+
was.open.size !== open.size ||
|
|
5055
|
+
was.depth !== depth ||
|
|
5056
|
+
!sameStack(was.stack, stack) ||
|
|
5057
|
+
was.clobbered.size !== clobbered.size ||
|
|
5058
|
+
!sameSet(was.dirty, dirty)
|
|
5059
|
+
) {
|
|
5060
|
+
stateIn[b] = { cur, open, depth, stack, clobbered, dirty };
|
|
5061
|
+
changed = true;
|
|
5062
|
+
}
|
|
5063
|
+
}
|
|
5064
|
+
}
|
|
5065
|
+
blocks.forEach((_, b) => stateIn[b] !== null && walk(b, true));
|
|
5066
|
+
const mis = (r: string) => usedUnsaved.has(r) || unrestored.has(r);
|
|
5067
|
+
return {
|
|
5068
|
+
saved: new Set([...savedSomewhere].filter((r) => !mis(r))),
|
|
5069
|
+
misSaved: new Set([...savedSomewhere, ...restoredSomewhere].filter(mis)),
|
|
5070
|
+
leaked,
|
|
5071
|
+
copied,
|
|
5072
|
+
returned,
|
|
5073
|
+
};
|
|
5074
|
+
}
|
|
5075
|
+
|
|
5076
|
+
/** Whether some path from instruction `from` of block `b` uses what scratch register `reg` holds
|
|
5077
|
+
* there as a VALUE before writing it: 'read' for an operand that reads it (a `push` of it among
|
|
5078
|
+
* them) or a call (which may be handed r0-r3), else 'returned' where a return may hand it back (r0
|
|
5079
|
+
* or r1), which is a use only in a function not declared to return nothing. A `bx` through it is
|
|
5080
|
+
* the return idiom `pop {r1}; bx r1` (agbcc's `pop {r3}; add sp, #0x10; bx r3` after a variadic
|
|
5081
|
+
* save), which uses the word as where to go; a write that reads no `reg` ends the path. Any
|
|
5082
|
+
* spelling of the register counts (`ip` is r12).
|
|
5083
|
+
*
|
|
5084
|
+
* `calls`, where a caller passes it, reads a `bl`/`blx` by what it is handed instead: a call that
|
|
5085
|
+
* names `reg` or whose `reads` include it reads it, one that clobbers it (`clobbers`) ends the
|
|
5086
|
+
* path, and past one that does neither the callee preserved it and the walk goes on. A `swi` is
|
|
5087
|
+
* read as without it. */
|
|
5088
|
+
function readsBeforeWrite(
|
|
5089
|
+
blocks: readonly AsmBlock[],
|
|
5090
|
+
succs: readonly (readonly number[])[],
|
|
5091
|
+
b: number,
|
|
5092
|
+
from: number,
|
|
5093
|
+
reg: string,
|
|
5094
|
+
calls?: { readonly reads: (call: Instr) => readonly string[]; readonly clobbers: readonly string[] },
|
|
5095
|
+
): 'read' | 'returned' | false {
|
|
5096
|
+
const names = (x: Instr): boolean =>
|
|
5097
|
+
expandRegList(
|
|
5098
|
+
x.ops
|
|
5099
|
+
.join(' ')
|
|
5100
|
+
.toLowerCase()
|
|
5101
|
+
.replace(/[[\]{}!#]/g, ' ')
|
|
5102
|
+
.split(/[,\s]+/)
|
|
5103
|
+
.filter(Boolean),
|
|
5104
|
+
).some((t) => canonReg(t) === reg);
|
|
5105
|
+
const seen = new Set<number>();
|
|
5106
|
+
let returned = false;
|
|
5107
|
+
// true where this path reads it; a path that returns it notes so and reads nothing
|
|
5108
|
+
const path = (at: number, start: number): boolean => {
|
|
5109
|
+
for (const x of blocks[at].instrs.slice(start)) {
|
|
5110
|
+
const mn = x.mnemonic;
|
|
5111
|
+
if (classifyXfer(x) === 'return') {
|
|
5112
|
+
returned ||= !(mn === 'bx' && canonReg(x.ops[0] ?? '') === reg) && (reg === 'r0' || reg === 'r1');
|
|
5113
|
+
return false;
|
|
5114
|
+
}
|
|
5115
|
+
if (calls !== undefined && (mn === 'bl' || mn === 'blx')) {
|
|
5116
|
+
if (names(x) || calls.reads(x).some((r) => canonReg(r) === reg)) {
|
|
5117
|
+
return true;
|
|
5118
|
+
}
|
|
5119
|
+
if (calls.clobbers.some((r) => canonReg(r) === reg)) {
|
|
5120
|
+
return false;
|
|
5121
|
+
}
|
|
5122
|
+
continue;
|
|
5123
|
+
}
|
|
5124
|
+
if (/^(bl|blx|swi)$/.test(mn)) {
|
|
5125
|
+
return reg !== 'r12';
|
|
5126
|
+
}
|
|
5127
|
+
if (names(x)) {
|
|
5128
|
+
const dest = canonReg(x.ops[0] ?? '');
|
|
5129
|
+
// Thumb's three-operand forms (`add rD, rN, #i`, `add rD, sp, #k`, `lsl rD, rM, #s`, `sub rD,
|
|
5130
|
+
// rN, rM`) read only their sources; the two-operand ones (`add rD, #i`, `mul rD, rM`) read rD.
|
|
5131
|
+
const writesOnly =
|
|
5132
|
+
mn === 'pop' ||
|
|
5133
|
+
((DEST_ONLY.test(mn) || SINGLE_LOAD.test(mn) || (WRITES_OPERAND_0.test(mn) && x.ops.length === 3)) &&
|
|
5134
|
+
dest === reg &&
|
|
5135
|
+
!names({ ...x, ops: x.ops.slice(1) }));
|
|
5136
|
+
return !writesOnly;
|
|
5137
|
+
}
|
|
5138
|
+
}
|
|
5139
|
+
return succs[at].some((s) => !seen.has(s) && (seen.add(s), path(s, 0)));
|
|
5140
|
+
};
|
|
5141
|
+
return path(b, from) ? 'read' : returned ? 'returned' : false;
|
|
3479
5142
|
}
|
|
3480
5143
|
|
|
3481
5144
|
/** How the fill reads and writes an operand: the guards that keep sp, pc and a data label from
|
|
3482
5145
|
* being read or written as a register, and the function-scope statics a pool word names. */
|
|
3483
5146
|
interface ThumbOperands {
|
|
3484
5147
|
localStatic(symbol: string): string;
|
|
5148
|
+
anonymousObject(label: string): string;
|
|
3485
5149
|
constVal(n: number, b: number): Value;
|
|
3486
5150
|
spAsDataError(): FrontendUnsupportedError;
|
|
3487
5151
|
readData(r: string, b: number): Value;
|
|
@@ -3523,6 +5187,22 @@ function thumbOperands({
|
|
|
3523
5187
|
return statics.define(read);
|
|
3524
5188
|
};
|
|
3525
5189
|
|
|
5190
|
+
/** The compiler's anonymous constant a pool word names (agbcc's `.LC0`), read out of this same
|
|
5191
|
+
* asm and recorded beside the statics — or a decline naming why it is not one. Unlike a static,
|
|
5192
|
+
* it need not be this function's alone: agbcc places one image for every identical constant in
|
|
5193
|
+
* a unit (varasm.c `output_constant_def` hashes them), and each function that copies it spells
|
|
5194
|
+
* its own initializer, which the recompiled unit shares again. */
|
|
5195
|
+
const anonymousObject = (label: string): string => {
|
|
5196
|
+
const read = readGasAnonymousObject(asm, label);
|
|
5197
|
+
if ('refused' in read) {
|
|
5198
|
+
throw new FrontendUnsupportedError(
|
|
5199
|
+
`cannot lift '${name}': literal-pool load of a pool word that names the compiler's anonymous data ` +
|
|
5200
|
+
`('${label}') ${read.refused}`,
|
|
5201
|
+
);
|
|
5202
|
+
}
|
|
5203
|
+
return statics.define(read);
|
|
5204
|
+
};
|
|
5205
|
+
|
|
3526
5206
|
const constVal = (n: number, b: number): Value => {
|
|
3527
5207
|
const v = mkValue(T.unk(32));
|
|
3528
5208
|
irBlocks[b].ops.push(mkOp('const', { results: [v], attrs: { value: n } }));
|
|
@@ -3632,7 +5312,7 @@ function thumbOperands({
|
|
|
3632
5312
|
}
|
|
3633
5313
|
writeVar(r, b, v);
|
|
3634
5314
|
};
|
|
3635
|
-
return { localStatic, constVal, spAsDataError, readData, writeData };
|
|
5315
|
+
return { localStatic, anonymousObject, constVal, spAsDataError, readData, writeData };
|
|
3636
5316
|
}
|
|
3637
5317
|
|
|
3638
5318
|
/** One half of a 64-bit value this lift projected into a register. */
|
|
@@ -3647,21 +5327,30 @@ interface PairHalf {
|
|
|
3647
5327
|
interface ThumbPairs {
|
|
3648
5328
|
readonly halfOf: ReadonlyMap<Value, PairHalf>;
|
|
3649
5329
|
readonly wordReturns: { bi: number; lo: Value; widerEpilogue: boolean }[];
|
|
5330
|
+
/** what r1 may hold at each word return, where no declaration pins the width (dead-reads.ts
|
|
5331
|
+
* `unreadPairHalf`) */
|
|
5332
|
+
readonly unreadHigh: Set<Value>;
|
|
3650
5333
|
projectHalf(irb: Block, whole: Value, half: 'lo' | 'hi', at: Instr): Value;
|
|
3651
5334
|
fuseHalves(irb: Block, lo: Value, hi: Value): Value;
|
|
3652
5335
|
wideReturn(lo: Value, bi: number): Value | null;
|
|
5336
|
+
/** record a return of one word in block `bi`, for `refuseWordReturns` and the dead-read pin */
|
|
5337
|
+
noteWordReturn(bi: number, lo: Value, widerEpilogue: boolean): void;
|
|
3653
5338
|
refuseWordReturns(): void;
|
|
3654
5339
|
}
|
|
3655
5340
|
|
|
3656
5341
|
function thumbPairs({
|
|
3657
5342
|
name,
|
|
3658
5343
|
target,
|
|
5344
|
+
own,
|
|
5345
|
+
ownMemoryReturn,
|
|
3659
5346
|
ssa,
|
|
3660
5347
|
asmBlocks,
|
|
3661
5348
|
callClobbers,
|
|
3662
5349
|
}: {
|
|
3663
5350
|
name: string;
|
|
3664
5351
|
target: TargetDescription;
|
|
5352
|
+
own: FnProto | undefined;
|
|
5353
|
+
ownMemoryReturn: LiftOptions['ownMemoryReturn'];
|
|
3665
5354
|
ssa: SsaBuilder;
|
|
3666
5355
|
asmBlocks: readonly AsmBlock[];
|
|
3667
5356
|
callClobbers: readonly string[];
|
|
@@ -3725,6 +5414,7 @@ function thumbPairs({
|
|
|
3725
5414
|
// epilogue says the return type is 5 to 8 bytes — judged by `refuseWordReturns` once every block
|
|
3726
5415
|
// is filled.
|
|
3727
5416
|
const wordReturns: ThumbPairs['wordReturns'] = [];
|
|
5417
|
+
const unreadHigh = new Set<Value>();
|
|
3728
5418
|
/** THE WORD RETURNS THAT WOULD DROP A HIGH HALF, refused. Run after every block is filled and
|
|
3729
5419
|
* before `ssa.finish()`: a join's phi operands are wired by then, and not yet folded away.
|
|
3730
5420
|
*
|
|
@@ -3741,7 +5431,10 @@ function thumbPairs({
|
|
|
3741
5431
|
* eightByteReturnScratch`), over r0:r1 that hold no pair this lift built — a high word computed
|
|
3742
5432
|
* from shifts, a `double`, or an 8-byte aggregate returned through memory, which agbcc sizes
|
|
3743
5433
|
* the same way. A word return drops r1 in the first two, and the third is a false decline the
|
|
3744
|
-
* epilogue cannot tell apart from them
|
|
5434
|
+
* epilogue cannot tell apart from them — save where the function's own declaration says it
|
|
5435
|
+
* returns a struct of 5 to 8 bytes through memory (`ownMemoryReturn`): the refusal is then
|
|
5436
|
+
* deferred to the pipeline, which throws it unless the lift proves r0 is only that struct's
|
|
5437
|
+
* storage (frontend/own-struct-return.ts), and that proof is what tells the third case apart. */
|
|
3745
5438
|
const refuseWordReturns = () => {
|
|
3746
5439
|
const classes = halfOf.size ? mergeClasses({ blocks: ssa.irBlocks }) : new Map<Value, readonly Value[]>();
|
|
3747
5440
|
const carries = (v: Value, half: 'lo' | 'hi') => (classes.get(v) ?? [v]).some((m) => halfOf.get(m)?.half === half);
|
|
@@ -3758,14 +5451,31 @@ function thumbPairs({
|
|
|
3758
5451
|
);
|
|
3759
5452
|
}
|
|
3760
5453
|
if (widerEpilogue) {
|
|
3761
|
-
|
|
3762
|
-
|
|
3763
|
-
|
|
3764
|
-
|
|
5454
|
+
const label = asmBlocks[bi].label;
|
|
5455
|
+
// `unproved`: why the declared struct, which would explain the epilogue, is not shown to be it
|
|
5456
|
+
const refuse = (unproved?: string): never => {
|
|
5457
|
+
throw new FrontendUnsupportedError(
|
|
5458
|
+
`cannot lift '${name}': the epilogue of '${label}' says the return type is 5 to 8 bytes ` +
|
|
5459
|
+
'(a 64-bit integer, a double, or an 8-byte aggregate), and no 64-bit pair this lift built is in r0:r1' +
|
|
5460
|
+
(unproved === undefined
|
|
5461
|
+
? ''
|
|
5462
|
+
: `; the ${ownMemoryReturn!.bytes}-byte struct its own declaration returns through r0 would explain it, ` +
|
|
5463
|
+
`but the lift cannot show r0 is only that struct's storage: ${unproved}`),
|
|
5464
|
+
);
|
|
5465
|
+
};
|
|
5466
|
+
if (ownMemoryReturn !== undefined && ownMemoryReturn.bytes > 4 && ownMemoryReturn.bytes <= 8) {
|
|
5467
|
+
ownMemoryReturn.defer(refuse);
|
|
5468
|
+
continue;
|
|
5469
|
+
}
|
|
5470
|
+
refuse();
|
|
3765
5471
|
}
|
|
3766
5472
|
}
|
|
3767
5473
|
};
|
|
3768
|
-
|
|
5474
|
+
const noteWordReturn = (bi: number, lo: Value, widerEpilogue: boolean): void => {
|
|
5475
|
+
wordReturns.push({ bi, lo, widerEpilogue });
|
|
5476
|
+
unreadPairHalf(ssa, own, target.argRegs[1], bi, unreadHigh);
|
|
5477
|
+
};
|
|
5478
|
+
return { halfOf, wordReturns, unreadHigh, noteWordReturn, projectHalf, fuseHalves, wideReturn, refuseWordReturns };
|
|
3769
5479
|
}
|
|
3770
5480
|
|
|
3771
5481
|
/** Whether, from instruction `from` of block `bi` to its end, nothing puts into the pair's high
|
|
@@ -3840,6 +5550,10 @@ function highRegisterHeld(ab: AsmBlock, from: number, hi: string, callClobbers:
|
|
|
3840
5550
|
interface ThumbFlagCarry {
|
|
3841
5551
|
readonly exitCmp: Map<number, PendingCmp | string>;
|
|
3842
5552
|
inheritedCmp(bi: number): PendingCmp | string;
|
|
5553
|
+
/** every value a `cmp` compared, whether or not a branch reads the flags it set: a compare
|
|
5554
|
+
* nothing reads leaves no op, so this is the only record that the value was consumed (the
|
|
5555
|
+
* dead-read pin's `compared`, frontend/dead-reads.ts) */
|
|
5556
|
+
readonly compared: Set<Value>;
|
|
3843
5557
|
}
|
|
3844
5558
|
|
|
3845
5559
|
function thumbFlagCarry({ asmBlocks, preds }: ThumbCfg, tables: ReadonlyMap<AsmBlock, JumpTable>): ThumbFlagCarry {
|
|
@@ -3875,7 +5589,7 @@ function thumbFlagCarry({ asmBlocks, preds }: ThumbCfg, tables: ReadonlyMap<AsmB
|
|
|
3875
5589
|
return plast !== undefined && classifyXfer(plast) === 'cond';
|
|
3876
5590
|
},
|
|
3877
5591
|
});
|
|
3878
|
-
return { exitCmp, inheritedCmp };
|
|
5592
|
+
return { exitCmp, inheritedCmp, compared: new Set<Value>() };
|
|
3879
5593
|
}
|
|
3880
5594
|
|
|
3881
5595
|
/** The two frame facts the fill accumulates and the frame-object audit reads: every offset the
|
|
@@ -3885,6 +5599,9 @@ function thumbFlagCarry({ asmBlocks, preds }: ThumbCfg, tables: ReadonlyMap<AsmB
|
|
|
3885
5599
|
interface ThumbFrameUses {
|
|
3886
5600
|
readonly usedSlotOffsets: Set<number>;
|
|
3887
5601
|
readonly movedCaptures: Set<Value>;
|
|
5602
|
+
/** the blocks other than the entry that read an incoming stack argument
|
|
5603
|
+
* (`ThumbFrame.settledDepth`), which `lowerCall`'s `withholdsSetupArgs` reads */
|
|
5604
|
+
readonly offEntryArgReads: Set<number>;
|
|
3888
5605
|
}
|
|
3889
5606
|
|
|
3890
5607
|
/** WHAT THE FILL OF A BLOCK READS: the records of the stages before it, the SSA builder it writes
|
|
@@ -3904,6 +5621,20 @@ interface ThumbFill {
|
|
|
3904
5621
|
/** the function-scope statics the operands define, finished into the lifted function's objects */
|
|
3905
5622
|
readonly statics: LocalStatics;
|
|
3906
5623
|
readonly frameUses: ThumbFrameUses;
|
|
5624
|
+
/** What an elided or kept dispatch left in its idiom's registers, by the value standing for each. */
|
|
5625
|
+
readonly staleIdiomValues: Map<Value, StaleIdiomRead>;
|
|
5626
|
+
/** What an elided dispatch left at each case target's entry, by block index. */
|
|
5627
|
+
readonly caseEntries: ReadonlyMap<number, CaseEntry>;
|
|
5628
|
+
/** Each callee-saved register a `pop` loads another word into and that reaches a balanced return
|
|
5629
|
+
* so (`bodySavedRegs`'s `leaked`), with that pop: refused once the return proof has run. */
|
|
5630
|
+
readonly poppedForeign: ReadonlyMap<string, Instr>;
|
|
5631
|
+
/** Each caller-saved register a `pop` loads another word into and some path then reads as a value
|
|
5632
|
+
* before writing it (`bodySavedRegs`'s `copied`, `readsBeforeWrite`), with that pop: refused once
|
|
5633
|
+
* the return proof has run. */
|
|
5634
|
+
readonly poppedCopies: ReadonlyMap<string, Instr>;
|
|
5635
|
+
/** …and each one a return may hand back so (`bodySavedRegs`'s `returned`): refused unless the
|
|
5636
|
+
* function's own declaration returns nothing. */
|
|
5637
|
+
readonly poppedReturns: ReadonlyMap<string, Instr>;
|
|
3907
5638
|
}
|
|
3908
5639
|
|
|
3909
5640
|
/** EVERY STAGE BEFORE THE FILL, in their refusal order: the text, the CFG, the target's scratch
|
|
@@ -3917,33 +5648,101 @@ function thumbFillOf(
|
|
|
3917
5648
|
prototypes: Prototypes,
|
|
3918
5649
|
symbols: SymbolMap | undefined,
|
|
3919
5650
|
oneObject: FrameRange | undefined,
|
|
5651
|
+
{
|
|
5652
|
+
contextOwnWords,
|
|
5653
|
+
contextOwnProto,
|
|
5654
|
+
ownMemoryReturn,
|
|
5655
|
+
calleeBodyBound,
|
|
5656
|
+
calleeStackWords,
|
|
5657
|
+
stackSized,
|
|
5658
|
+
entryReadCalls,
|
|
5659
|
+
}: Pick<
|
|
5660
|
+
LiftOptions,
|
|
5661
|
+
| 'contextOwnWords'
|
|
5662
|
+
| 'contextOwnProto'
|
|
5663
|
+
| 'ownMemoryReturn'
|
|
5664
|
+
| 'calleeBodyBound'
|
|
5665
|
+
| 'calleeStackWords'
|
|
5666
|
+
| 'stackSized'
|
|
5667
|
+
| 'entryReadCalls'
|
|
5668
|
+
> = {},
|
|
5669
|
+
homesReleased = false,
|
|
3920
5670
|
): ThumbFill {
|
|
3921
|
-
const text = readThumbText(name, asm);
|
|
5671
|
+
const text = readThumbText(name, asm, target.compilerBehaviors.farJumpRelay?.comment);
|
|
3922
5672
|
const cfg = thumbCfg(name, text.blocks, text.tables);
|
|
3923
|
-
|
|
3924
|
-
const calls = callDeclarations(
|
|
3925
|
-
|
|
5673
|
+
assertScratchRegsPartitioned(target); // first, for the refusal order; `registerLiveIns` asks again
|
|
5674
|
+
const calls = callDeclarations(
|
|
5675
|
+
name,
|
|
5676
|
+
target,
|
|
5677
|
+
prototypes,
|
|
5678
|
+
THUMB_CALL_LOWERING,
|
|
5679
|
+
(message) => {
|
|
5680
|
+
throw new FrontendUnsupportedError(message);
|
|
5681
|
+
},
|
|
5682
|
+
() => ownDeclarationOf(name, prototypes, contextOwnProto),
|
|
5683
|
+
calleeBodyBound,
|
|
5684
|
+
entryReadCalls,
|
|
5685
|
+
stackSized,
|
|
5686
|
+
);
|
|
5687
|
+
const frame = measureThumbFrame({
|
|
5688
|
+
name,
|
|
5689
|
+
target,
|
|
5690
|
+
cfg,
|
|
5691
|
+
calls,
|
|
5692
|
+
oneObject,
|
|
5693
|
+
homesReleased,
|
|
5694
|
+
own: ownDeclarationOf(name, prototypes, contextOwnProto),
|
|
5695
|
+
dropsVoidReturn: declaresVoidReturn(prototypes[name]),
|
|
5696
|
+
stackWordsOf: (callee) =>
|
|
5697
|
+
asksCalleeBody(Object.hasOwn(prototypes, callee) ? prototypes[callee] : undefined)
|
|
5698
|
+
? calleeStackWords?.(callee)
|
|
5699
|
+
: undefined,
|
|
3926
5700
|
});
|
|
3927
|
-
const frame = measureThumbFrame({ target, cfg, calls, oneObject });
|
|
3928
5701
|
|
|
3929
5702
|
// --- ISA-neutral SSA construction (shared Braun builder) ---
|
|
3930
5703
|
// THE LIVE-IN MODEL is the frame's partition (`ThumbFrame.partition`) and a register half.
|
|
3931
5704
|
// The register half needs both of its facts, and they come from different places. The target says
|
|
3932
5705
|
// which registers no caller can hand a value over in; `savedRegs` says which ones THIS function
|
|
3933
|
-
// saved, and so could have homed a local in
|
|
3934
|
-
//
|
|
3935
|
-
//
|
|
3936
|
-
// ABI requires preserving: `target.scratchRegs` need none, so demanding one
|
|
3937
|
-
// local the compiler was entitled to put in place with no prologue at all.
|
|
3938
|
-
|
|
3939
|
-
|
|
3940
|
-
|
|
3941
|
-
|
|
3942
|
-
|
|
3943
|
-
|
|
3944
|
-
|
|
3945
|
-
|
|
3946
|
-
|
|
5706
|
+
// saved, and so could have homed a local in — in its prologue, or by a `push` ahead of every use
|
|
5707
|
+
// (`bodySavedRegs`). A register in only the first is one the function shares with every other: a
|
|
5708
|
+
// global register variable, or a refusal where the target cannot spell one. The save is asked only
|
|
5709
|
+
// of the registers the ABI requires preserving: `target.scratchRegs` need none, so demanding one
|
|
5710
|
+
// there would refuse a local the compiler was entitled to put in place with no prologue at all.
|
|
5711
|
+
// `registerLiveIns` (frontend/ssa.ts) composes it, as it does for PPC. The function's own
|
|
5712
|
+
// declaration bounds the argument registers (`ownArgumentWordsOn`): agbcc homes a local in r3 past
|
|
5713
|
+
// a two-parameter one.
|
|
5714
|
+
// A prologue save that some path uses past its restore, or carries to a return unrestored, is
|
|
5715
|
+
// `misSaved` and no save: the prologue's reading of it sees only the `push`.
|
|
5716
|
+
const body = bodySavedRegs(cfg.asmBlocks, cfg.preds, cfg.succs);
|
|
5717
|
+
const liveIns = registerLiveIns(
|
|
5718
|
+
target,
|
|
5719
|
+
new Set([...frame.savedRegs, ...body.saved].filter((r) => !body.misSaved.has(r))),
|
|
5720
|
+
contextOwnWords ?? ownArgumentWordsOn(name, prototypes[name], target),
|
|
5721
|
+
body.misSaved,
|
|
5722
|
+
);
|
|
5723
|
+
const ssa = makeSsaBuilder(
|
|
5724
|
+
name,
|
|
5725
|
+
cfg.asmBlocks.length,
|
|
5726
|
+
cfg.preds,
|
|
5727
|
+
() => ({ ...frame.partition, ...liveIns, saveSite: '`push` ahead of every use' }),
|
|
5728
|
+
undefined,
|
|
5729
|
+
liveIns.globalRegs,
|
|
5730
|
+
);
|
|
5731
|
+
// Where agbcc put each block (ir/core.ts `AsmLayout`), read off the text the CFG was cut from:
|
|
5732
|
+
// the transfer `classifyXfer` names is the block's branch, and everything before it its body.
|
|
5733
|
+
ssa.fn.asmLayout = recordAsmLayout(
|
|
5734
|
+
ssa.irBlocks,
|
|
5735
|
+
cfg.asmBlocks.map((ab) => {
|
|
5736
|
+
const xfer = ab.instrs.length ? classifyXfer(ab.instrs[ab.instrs.length - 1]) : null;
|
|
5737
|
+
const branch = xfer === 'uncond' || xfer === 'cond';
|
|
5738
|
+
return {
|
|
5739
|
+
body: branch ? ab.instrs.slice(0, -1) : ab.instrs,
|
|
5740
|
+
endsInJump: xfer === 'uncond',
|
|
5741
|
+
// a far-jump relay wears `bl` and is a branch (`classifyXfer`)
|
|
5742
|
+
isCall: (ins) => (ins.mnemonic === 'bl' && !(ins as Instr).farJump) || ins.mnemonic === 'blx',
|
|
5743
|
+
};
|
|
5744
|
+
}),
|
|
5745
|
+
);
|
|
3947
5746
|
const statics = makeLocalStatics();
|
|
3948
5747
|
const operands = thumbOperands({
|
|
3949
5748
|
name,
|
|
@@ -3953,7 +5752,15 @@ function thumbFillOf(
|
|
|
3953
5752
|
statics,
|
|
3954
5753
|
slotsOffReason: frame.slotsOffReason,
|
|
3955
5754
|
});
|
|
3956
|
-
const pairs = thumbPairs({
|
|
5755
|
+
const pairs = thumbPairs({
|
|
5756
|
+
name,
|
|
5757
|
+
target,
|
|
5758
|
+
own: prototypes[name],
|
|
5759
|
+
ownMemoryReturn,
|
|
5760
|
+
ssa,
|
|
5761
|
+
asmBlocks: cfg.asmBlocks,
|
|
5762
|
+
callClobbers: calls.callClobbers,
|
|
5763
|
+
});
|
|
3957
5764
|
const flags = thumbFlagCarry(cfg, text.tables);
|
|
3958
5765
|
return {
|
|
3959
5766
|
name,
|
|
@@ -3968,10 +5775,38 @@ function thumbFillOf(
|
|
|
3968
5775
|
pairs,
|
|
3969
5776
|
flags,
|
|
3970
5777
|
statics,
|
|
3971
|
-
frameUses: {
|
|
5778
|
+
frameUses: {
|
|
5779
|
+
usedSlotOffsets: new Set(),
|
|
5780
|
+
movedCaptures: new Set(),
|
|
5781
|
+
offEntryArgReads: new Set(),
|
|
5782
|
+
},
|
|
5783
|
+
staleIdiomValues: new Map(),
|
|
5784
|
+
caseEntries: elidedCaseEntries(cfg, text.tables),
|
|
5785
|
+
poppedForeign: body.leaked,
|
|
5786
|
+
poppedCopies: body.copied,
|
|
5787
|
+
poppedReturns: body.returned,
|
|
3972
5788
|
};
|
|
3973
5789
|
}
|
|
3974
5790
|
|
|
5791
|
+
/** Each case target of an ELIDED dispatch, with what the idiom left at its entry (jump-table.ts
|
|
5792
|
+
* `caseEntries`). A kept dispatch's block is lifted and leaves its markers at its own end
|
|
5793
|
+
* (`lowerTerminator`), where the default path never goes. */
|
|
5794
|
+
function elidedCaseEntries(cfg: ThumbCfg, tables: ReadonlyMap<AsmBlock, JumpTable>): Map<number, CaseEntry> {
|
|
5795
|
+
const at = (label: string) => cfg.labelIndex.get(label)!;
|
|
5796
|
+
return caseEntries(
|
|
5797
|
+
[...tables]
|
|
5798
|
+
.filter(([, jt]) => jt.extras.length === 0)
|
|
5799
|
+
.map(([ab, jt]) => ({
|
|
5800
|
+
at: at(ab.label),
|
|
5801
|
+
scrutReg: jt.scrutReg,
|
|
5802
|
+
cases: jt.caseLabels.map(at),
|
|
5803
|
+
defaultBlock: at(jt.defaultLabel),
|
|
5804
|
+
leaves: jt.leaves,
|
|
5805
|
+
})),
|
|
5806
|
+
cfg.preds,
|
|
5807
|
+
);
|
|
5808
|
+
}
|
|
5809
|
+
|
|
3975
5810
|
type FrameWalk = ReturnType<typeof makeFrameWalk>;
|
|
3976
5811
|
|
|
3977
5812
|
/** ONE BLOCK BEING FILLED, and the three things that change as its instructions are read: the
|
|
@@ -3985,10 +5820,28 @@ interface BlockCursor {
|
|
|
3985
5820
|
pendingCmp: PendingCmp | null;
|
|
3986
5821
|
noCmpWhy: string;
|
|
3987
5822
|
consumed: Instr | null;
|
|
5823
|
+
/** A kept jump-table dispatch's scrutinee, read at the block's entry (`lowerTerminator`). */
|
|
5824
|
+
readonly dispatchScrutinee: Value | null;
|
|
3988
5825
|
}
|
|
3989
5826
|
|
|
3990
|
-
function openBlockCursor(
|
|
5827
|
+
function openBlockCursor(fill: ThumbFill, ab: AsmBlock, bi: number): BlockCursor {
|
|
5828
|
+
const { target, text, cfg, ssa, flags, frame } = fill;
|
|
3991
5829
|
const irb = ssa.irBlocks[bi];
|
|
5830
|
+
// A case target of an elided dispatch is entered with what the idiom left in its registers.
|
|
5831
|
+
const entry = fill.caseEntries.get(bi);
|
|
5832
|
+
if (entry !== undefined) {
|
|
5833
|
+
enterCaseTarget(
|
|
5834
|
+
entry,
|
|
5835
|
+
irb.ops,
|
|
5836
|
+
{
|
|
5837
|
+
key: reg,
|
|
5838
|
+
scrutinee: (r) => ssa.readVar(reg(r), bi),
|
|
5839
|
+
dispatchScrutinee: (r) => ssa.readAtEnd(reg(r), entry.at),
|
|
5840
|
+
write: (r, v) => ssa.writeVar(reg(r), bi, v),
|
|
5841
|
+
},
|
|
5842
|
+
fill.staleIdiomValues,
|
|
5843
|
+
);
|
|
5844
|
+
}
|
|
3992
5845
|
// Seeded at the block's FIRST instruction, not at its terminator, so an inherited compare is
|
|
3993
5846
|
// judged by the in-block clear (`retireFlags`) exactly as one this block made itself would be: a
|
|
3994
5847
|
// block that inherits flags and then writes its own must lose them.
|
|
@@ -3998,13 +5851,15 @@ function openBlockCursor({ target, cfg, ssa, flags }: ThumbFill, ab: AsmBlock, b
|
|
|
3998
5851
|
// that made a compare and then overwrote it is a different gap from one that never had a
|
|
3999
5852
|
// compare at all, and a single message for both makes two gaps read as one.
|
|
4000
5853
|
const noCmpWhy = typeof inherited === 'string' ? inherited : '';
|
|
4001
|
-
// Tracks the frame through this block's linear instruction order
|
|
4002
|
-
// block
|
|
4003
|
-
//
|
|
4004
|
-
//
|
|
4005
|
-
|
|
4006
|
-
const walk = makeFrameWalk({ argRegs: target.argRegs,
|
|
4007
|
-
|
|
5854
|
+
// Tracks the frame through this block's linear instruction order, from the depth the block starts
|
|
5855
|
+
// at: 0 for an entry block nothing branches back to (its params are parameters, not phis), and
|
|
5856
|
+
// the depth the entry leaves for every other block where the frame is proven settled
|
|
5857
|
+
// (`ThumbFrame.settledDepth`). Elsewhere a `[sp,#N]` access declines.
|
|
5858
|
+
const start = bi === 0 ? (cfg.preds[0].length > 0 ? null : 0) : frame.settledDepth;
|
|
5859
|
+
const walk = makeFrameWalk({ argRegs: target.argRegs, start });
|
|
5860
|
+
const jt = text.tables.get(ab);
|
|
5861
|
+
const dispatchScrutinee = jt && jt.extras.length > 0 ? ssa.readVar(reg(jt.scrutReg), bi) : null;
|
|
5862
|
+
return { ab, bi, irb, walk, pendingCmp, noCmpWhy, consumed: null, dispatchScrutinee };
|
|
4008
5863
|
}
|
|
4009
5864
|
|
|
4010
5865
|
/** Fill block `bi`: lower each instruction in order, record what the block leaves in the flags,
|
|
@@ -4031,6 +5886,9 @@ function fillThumbBlock(fill: ThumbFill, ab: AsmBlock, bi: number): void {
|
|
|
4031
5886
|
switch (ins.mnemonic) {
|
|
4032
5887
|
case 'mov':
|
|
4033
5888
|
case 'movs': {
|
|
5889
|
+
if (fill.frame.saveCarriers.has(ins)) {
|
|
5890
|
+
break; // the save's staging copy, not the function's (`prologueSavedRegs`)
|
|
5891
|
+
}
|
|
4034
5892
|
lowerMov(fill, cur, ins);
|
|
4035
5893
|
break;
|
|
4036
5894
|
}
|
|
@@ -4166,6 +6024,11 @@ function fillThumbBlock(fill: ThumbFill, ab: AsmBlock, bi: number): void {
|
|
|
4166
6024
|
}
|
|
4167
6025
|
const rhs = b.startsWith('#') ? constVal(imm(b), bi) : readData(reg(b), bi);
|
|
4168
6026
|
cur.pendingCmp = { lhs: readData(reg(a), bi), rhs };
|
|
6027
|
+
fill.flags.compared.add(cur.pendingCmp.lhs).add(rhs);
|
|
6028
|
+
break;
|
|
6029
|
+
}
|
|
6030
|
+
case 'cmn': {
|
|
6031
|
+
lowerCmn(fill, cur, ins);
|
|
4169
6032
|
break;
|
|
4170
6033
|
}
|
|
4171
6034
|
case 'ldr':
|
|
@@ -4209,6 +6072,25 @@ function fillThumbBlock(fill: ThumbFill, ab: AsmBlock, bi: number): void {
|
|
|
4209
6072
|
lowerTerminator(fill, cur);
|
|
4210
6073
|
}
|
|
4211
6074
|
|
|
6075
|
+
/** `cmn rA, rB` as the compare it stands for, `cmp rA, -rB`: it records both registers' values and
|
|
6076
|
+
* the terminator mints the negation once it knows which branch reads it (`CMN_EXACT`). Thumb-1
|
|
6077
|
+
* encodes `cmn` on low registers only; hand-written text with any other operand takes the flags
|
|
6078
|
+
* with nothing modelled, exactly as `tst` does. */
|
|
6079
|
+
function lowerCmn(fill: ThumbFill, cur: BlockCursor, ins: Instr): void {
|
|
6080
|
+
const { readData } = fill.operands;
|
|
6081
|
+
const [a, b] = ins.ops;
|
|
6082
|
+
if (a === undefined || b === undefined || !/^r[0-7]$/.test(reg(a)) || !/^r[0-7]$/.test(reg(b))) {
|
|
6083
|
+
cur.noCmpWhy = `the flags it tests were written by 'cmn' in '${cur.ab.label}', and only a register 'cmn' is modelled`;
|
|
6084
|
+
cur.pendingCmp = null;
|
|
6085
|
+
emitOpaqueDest(fill, cur, ins);
|
|
6086
|
+
return;
|
|
6087
|
+
}
|
|
6088
|
+
const lhs = readData(reg(a), cur.bi);
|
|
6089
|
+
const rhs = readData(reg(b), cur.bi);
|
|
6090
|
+
cur.pendingCmp = { lhs, rhs, cmn: true };
|
|
6091
|
+
fill.flags.compared.add(lhs).add(rhs);
|
|
6092
|
+
}
|
|
6093
|
+
|
|
4212
6094
|
/** What an instruction does to the flags the block holds: a writer retires the compare, and
|
|
4213
6095
|
* `noCmpWhy` names it. */
|
|
4214
6096
|
function retireFlags(cur: BlockCursor, ins: Instr): void {
|
|
@@ -4238,7 +6120,8 @@ function retireFlags(cur: BlockCursor, ins: Instr): void {
|
|
|
4238
6120
|
// The inhabitant the guard itself exists for is hand-written asm in the playground, where
|
|
4239
6121
|
// there is no oracle to catch a lie.
|
|
4240
6122
|
//
|
|
4241
|
-
// `tst
|
|
6123
|
+
// `tst` takes them with no destination to read at all (`FLAG_ONLY`, above). `cmn` is a compare
|
|
6124
|
+
// and seeds its own (`lowerCmn`), as `cmp` does.
|
|
4242
6125
|
//
|
|
4243
6126
|
// A CALL takes them too, and this is the one flag writer that is not an instruction: `bl`
|
|
4244
6127
|
// writes no flags, but the function it enters retires compares of its own, and the ABI lets
|
|
@@ -4428,6 +6311,11 @@ function lowerMov(fill: ThumbFill, cur: BlockCursor, ins: Instr): void {
|
|
|
4428
6311
|
// a reserved local area for the object to live in.
|
|
4429
6312
|
if (!b?.startsWith('#') && isSpReg(b ?? '') && !isSpReg(a ?? '')) {
|
|
4430
6313
|
if (frame.slotsOk && frame.localArea > 0) {
|
|
6314
|
+
// a copy only a narrow slot reload reads names no object; the reload reads the slot
|
|
6315
|
+
if (frame.narrowReloadCopies.has(ins)) {
|
|
6316
|
+
writeNarrowCopyMarker(fill, cur, reg(a));
|
|
6317
|
+
return;
|
|
6318
|
+
}
|
|
4431
6319
|
const res = mkValue(T.unk(32));
|
|
4432
6320
|
irb.ops.push(mkOp('laddr', { results: [res], attrs: { off: 0 } }));
|
|
4433
6321
|
writeData(reg(a), bi, res);
|
|
@@ -4439,6 +6327,18 @@ function lowerMov(fill: ThumbFill, cur: BlockCursor, ins: Instr): void {
|
|
|
4439
6327
|
writeData(reg(a), bi, v);
|
|
4440
6328
|
}
|
|
4441
6329
|
|
|
6330
|
+
/** What a narrow reload's unlifted copy of sp leaves in `r`: an address no C spells.
|
|
6331
|
+
* `measureThumbFrame` accepts the copy only where `r` is dead past its load, and the marker holds
|
|
6332
|
+
* the lift to that: a read of it declines (`refuseStaleIdiomReads`) rather than reading what `r`
|
|
6333
|
+
* held before the copy. */
|
|
6334
|
+
function writeNarrowCopyMarker(fill: ThumbFill, cur: BlockCursor, r: string): void {
|
|
6335
|
+
fill.operands.writeData(
|
|
6336
|
+
r,
|
|
6337
|
+
cur.bi,
|
|
6338
|
+
staleMarker(cur.irb.ops, r, { reg: r, form: 'narrow', parks: false }, fill.staleIdiomValues),
|
|
6339
|
+
);
|
|
6340
|
+
}
|
|
6341
|
+
|
|
4442
6342
|
/** `add`: a frame adjustment, a 64-bit pair with the `adc` after it, a frame capture,
|
|
4443
6343
|
* the copy idiom, or an add. */
|
|
4444
6344
|
function lowerAdd(fill: ThumbFill, cur: BlockCursor, ins: Instr, ii: number): void {
|
|
@@ -4462,6 +6362,10 @@ function lowerAdd(fill: ThumbFill, cur: BlockCursor, ins: Instr, ii: number): vo
|
|
|
4462
6362
|
// audit; only the offset differs.
|
|
4463
6363
|
if (isSpReg(b ?? '') && !isSpReg(a ?? '') && c !== undefined && IMM_LITERAL.test(c)) {
|
|
4464
6364
|
if (frame.slotsOk && frame.localArea > 0) {
|
|
6365
|
+
if (frame.narrowReloadCopies.has(ins)) {
|
|
6366
|
+
writeNarrowCopyMarker(fill, cur, reg(a)); // see lowerMov
|
|
6367
|
+
return;
|
|
6368
|
+
}
|
|
4465
6369
|
const res = mkValue(T.unk(32));
|
|
4466
6370
|
irb.ops.push(mkOp('laddr', { results: [res], attrs: { off: imm(c) } }));
|
|
4467
6371
|
writeData(reg(a), bi, res);
|
|
@@ -4656,6 +6560,9 @@ function lowerBlockTransfer(fill: ThumbFill, cur: BlockCursor, ins: Instr): void
|
|
|
4656
6560
|
const adv = mkValue(T.unk(32));
|
|
4657
6561
|
irb.ops.push(mkOp('add', { operands: [base0, constVal(4 * list.length, bi)], results: [adv] }));
|
|
4658
6562
|
writeData(baseReg, bi, adv);
|
|
6563
|
+
// agbcc's block copy leaves this dead in an argument register before a call (`ssa.ts`
|
|
6564
|
+
// `noteIncidentalWrite`), so it proves no argument there.
|
|
6565
|
+
fill.ssa.noteIncidentalWrite(adv);
|
|
4659
6566
|
}
|
|
4660
6567
|
}
|
|
4661
6568
|
|
|
@@ -4665,7 +6572,7 @@ function lowerLoad(fill: ThumbFill, cur: BlockCursor, ins: Instr): void {
|
|
|
4665
6572
|
const { name, symbols, text, frame, ssa } = fill;
|
|
4666
6573
|
const { readVar } = ssa;
|
|
4667
6574
|
const { usedSlotOffsets } = fill.frameUses;
|
|
4668
|
-
const { localStatic, readData, writeData } = fill.operands;
|
|
6575
|
+
const { localStatic, anonymousObject, readData, writeData } = fill.operands;
|
|
4669
6576
|
const { bi, irb, walk } = cur;
|
|
4670
6577
|
const [a, b] = ins.ops;
|
|
4671
6578
|
// A word load whose operand NAMES a literal pool is a pool reference, not a memory base:
|
|
@@ -4677,7 +6584,11 @@ function lowerLoad(fill: ThumbFill, cur: BlockCursor, ins: Instr): void {
|
|
|
4677
6584
|
// A function-scope static is the address of an object this function DEFINES: the
|
|
4678
6585
|
// `gaddr` below under its linker name, with the definition recorded beside the Fn.
|
|
4679
6586
|
const pr: PoolRef | null =
|
|
4680
|
-
ref?.kind === 'local-static'
|
|
6587
|
+
ref?.kind === 'local-static'
|
|
6588
|
+
? { kind: 'gaddr', sym: localStatic(ref.symbol), addend: ref.addend }
|
|
6589
|
+
: ref?.kind === 'anonymous'
|
|
6590
|
+
? { kind: 'gaddr', sym: anonymousObject(ref.label), addend: 0 }
|
|
6591
|
+
: ref;
|
|
4681
6592
|
if (pr?.kind === 'const') {
|
|
4682
6593
|
// Numeric-pool PROMOTION (symbols.ts): a pool-loaded word whose value the
|
|
4683
6594
|
// project's symbol map knows becomes the NAMED global's address — the same
|
|
@@ -4704,7 +6615,16 @@ function lowerLoad(fill: ThumbFill, cur: BlockCursor, ins: Instr): void {
|
|
|
4704
6615
|
irb.ops.push(
|
|
4705
6616
|
mkOp('gaddr', {
|
|
4706
6617
|
results: [res],
|
|
4707
|
-
|
|
6618
|
+
// `fromMap`: the NAME is asmlift's reading of a numeric word, not the asm's own
|
|
6619
|
+
// spelling (raise/offsetnames.ts `cellSpelledTwice` asks which), and `mapAddr` the
|
|
6620
|
+
// word itself — the one address of a name the map places at several that this
|
|
6621
|
+
// spelling denotes. A macro expands to the numeric literal the asm holds, so it is
|
|
6622
|
+
// not marked.
|
|
6623
|
+
attrs: {
|
|
6624
|
+
sym: si.name,
|
|
6625
|
+
...(si.kind === 'code' ? { code: true } : {}),
|
|
6626
|
+
...(si.macroBody === undefined ? { fromMap: true, mapAddr: pr.value >>> 0 } : {}),
|
|
6627
|
+
},
|
|
4708
6628
|
}),
|
|
4709
6629
|
);
|
|
4710
6630
|
writeData(reg(a), bi, res);
|
|
@@ -4721,7 +6641,12 @@ function lowerLoad(fill: ThumbFill, cur: BlockCursor, ins: Instr): void {
|
|
|
4721
6641
|
const g = mkValue(T.unk(32));
|
|
4722
6642
|
const k = mkValue(T.unk(32));
|
|
4723
6643
|
const res = mkValue(T.unk(32));
|
|
4724
|
-
irb.ops.push(
|
|
6644
|
+
irb.ops.push(
|
|
6645
|
+
mkOp('gaddr', {
|
|
6646
|
+
results: [g],
|
|
6647
|
+
attrs: { sym: interior.info.name, fromMap: true, mapAddr: (pr.value - interior.offset) >>> 0 },
|
|
6648
|
+
}),
|
|
6649
|
+
);
|
|
4725
6650
|
irb.ops.push(mkOp('const', { results: [k], attrs: { value: interior.offset } }));
|
|
4726
6651
|
irb.ops.push(mkOp('add', { operands: [g, k], results: [res] }));
|
|
4727
6652
|
writeData(reg(a), bi, res);
|
|
@@ -4769,12 +6694,58 @@ function lowerLoad(fill: ThumbFill, cur: BlockCursor, ins: Instr): void {
|
|
|
4769
6694
|
// `argIndex` can make, and the soundness proof that each one is needed for, lives on the
|
|
4770
6695
|
// predicate itself — restating them here is how the two copies drifted.
|
|
4771
6696
|
{
|
|
4772
|
-
const index = walk.argIndex({ base, off, regOff }, width
|
|
6697
|
+
const index = walk.argIndex({ base, off, regOff }, width);
|
|
4773
6698
|
if (index !== null) {
|
|
6699
|
+
if (bi !== 0) {
|
|
6700
|
+
fill.frameUses.offEntryArgReads.add(bi);
|
|
6701
|
+
}
|
|
4774
6702
|
writeData(reg(a), bi, readVar(stackArgKey(index), bi));
|
|
4775
6703
|
return;
|
|
4776
6704
|
}
|
|
4777
6705
|
}
|
|
6706
|
+
// WHICH RELOADS OF THIS FUNCTION'S OWN FRAME ARE SLOT READS — the dual of the spill in the str
|
|
6707
|
+
// arm, and the whole soundness of it. The narrow reload next and the word reload below both ask
|
|
6708
|
+
// it.
|
|
6709
|
+
//
|
|
6710
|
+
// One a store reaches on some path reads that store, and a path that brings none reads an
|
|
6711
|
+
// uninitialised local: the frame is `ownedLocals`, so `readVar` answers that path with `undef`,
|
|
6712
|
+
// never with a parameter (frontend/ssa.ts `LiveInModel`).
|
|
6713
|
+
//
|
|
6714
|
+
// A slot NO store reaches, on any path, is the same local read before any store — `if (k > 5)
|
|
6715
|
+
// use(v0); … v0 = p[0];`, or a local the source never initialises. Only two kinds of callee
|
|
6716
|
+
// could have written it instead. One that assigned the word as its own stack parameter ran
|
|
6717
|
+
// before the load, so the load is after a call, which the after-call layout claim
|
|
6718
|
+
// (`localsAboveOutgoingArea`) says no word of the outgoing area is; with no call reaching the
|
|
6719
|
+
// load there is no such callee. Without the claim it may be one, and falls through to the
|
|
6720
|
+
// decline. The stronger `outgoingAreaNeverLoadedFrom` adds nothing here: a load BEFORE a call reads
|
|
6721
|
+
// a staged word, and a staged word has a reaching store. The other is a callee holding an
|
|
6722
|
+
// address of the frame, which the frame-object audit refuses: an `undef` slot an escaped writer
|
|
6723
|
+
// reaches (`writer-over-undef`), or one a captured object overlaps.
|
|
6724
|
+
const readsOwnSlot = (at: string, o: number, ro: string | undefined, w: number): boolean =>
|
|
6725
|
+
frame.isOwnFrameWordSlot(at, o, ro, w) &&
|
|
6726
|
+
(ssa.hasReachingDef(stackSlotKey(o), bi) || fill.target.compilerBehaviors.localsAboveOutgoingArea === true);
|
|
6727
|
+
// A NARROW RELOAD OF A WORD SLOT through a copy of sp nothing else reads (`narrowSlotReloads`):
|
|
6728
|
+
// the low-order part of the word the slot holds, so the slot's value narrowed — taken where the
|
|
6729
|
+
// word reload of the slot would be, and the slot is no object's. Its copy was not lifted, so a
|
|
6730
|
+
// reload this cannot take refuses here rather than reading through a register never set.
|
|
6731
|
+
{
|
|
6732
|
+
const slot = frame.narrowReloadOf.get(ins);
|
|
6733
|
+
if (slot !== undefined) {
|
|
6734
|
+
if (!readsOwnSlot('sp', slot, undefined, 4) || frame.isFrameObjectAccess('sp', slot, undefined, 4)) {
|
|
6735
|
+
throw new FrontendUnsupportedError(
|
|
6736
|
+
`cannot lift '${name}': narrow reload of a stack local through a copy of sp — ` +
|
|
6737
|
+
`'${ins.mnemonic} ${ins.ops.join(', ')}' reads the word slot [sp,#${slot}], which the word-slot model does not key here`,
|
|
6738
|
+
);
|
|
6739
|
+
}
|
|
6740
|
+
usedSlotOffsets.add(slot);
|
|
6741
|
+
const res = mkValue(T.unk(32));
|
|
6742
|
+
irb.ops.push(
|
|
6743
|
+
mkOp('zext', { operands: [readVar(stackSlotKey(slot), bi)], results: [res], attrs: { width: 8 * width } }),
|
|
6744
|
+
);
|
|
6745
|
+
writeData(reg(a), bi, res);
|
|
6746
|
+
return;
|
|
6747
|
+
}
|
|
6748
|
+
}
|
|
4778
6749
|
// An address-taken object comes FIRST: it is memory, not a slot, so it is
|
|
4779
6750
|
// read with a real `load` through its `laddr` (see isFrameObjectAccess). No
|
|
4780
6751
|
// reaching-def test — the callee holding the address is a writer this function cannot
|
|
@@ -4787,16 +6758,8 @@ function lowerLoad(fill: ThumbFill, cur: BlockCursor, ins: Instr): void {
|
|
|
4787
6758
|
writeData(reg(a), bi, res);
|
|
4788
6759
|
return;
|
|
4789
6760
|
}
|
|
4790
|
-
// A word reload from this function's own frame
|
|
4791
|
-
|
|
4792
|
-
// The reaching-def test is the whole soundness of it, and it mirrors the MIPS guard
|
|
4793
|
-
// exactly: a slot that was never STORED holds nothing this function put there, so
|
|
4794
|
-
// `readVar` would mint a phantom entry parameter for it and hand back a value the machine
|
|
4795
|
-
// never had. Above the frame that reading is right and is the incoming-argument path
|
|
4796
|
-
// above; INSIDE the frame it is an uninitialised local (or one whose address escaped
|
|
4797
|
-
// through a path the model missed), and the honest answer is the decline this falls
|
|
4798
|
-
// through to.
|
|
4799
|
-
if (frame.isOwnFrameWordSlot(base, off, regOff, width) && ssa.hasReachingDef(stackSlotKey(off), bi)) {
|
|
6761
|
+
// A word reload from this function's own frame (`readsOwnSlot`, above).
|
|
6762
|
+
if (readsOwnSlot(base, off, regOff, width)) {
|
|
4800
6763
|
usedSlotOffsets.add(off);
|
|
4801
6764
|
writeData(reg(a), bi, readVar(stackSlotKey(off), bi));
|
|
4802
6765
|
return;
|
|
@@ -4853,6 +6816,28 @@ function lowerStore(fill: ThumbFill, cur: BlockCursor, ins: Instr): void {
|
|
|
4853
6816
|
irb.ops.push(mkOp('store', { operands: [storeBase, readData(reg(a), bi)], attrs: { off, width } }));
|
|
4854
6817
|
}
|
|
4855
6818
|
|
|
6819
|
+
/** WHERE A READER FINDS A CALL IN THE .s, for a refusal that names it: a Thumb `Instr` carries no
|
|
6820
|
+
* address. A block the .s labels (the function's own entry, a `.L3`) names it by that label and
|
|
6821
|
+
* the call; a block the splitter opened after a branch has only a label it made up (`.L_anon0`),
|
|
6822
|
+
* which appears nowhere in the .s, so the call is named by its ordinal among the calls to the same
|
|
6823
|
+
* callee in the function, in the order the .s lists them. */
|
|
6824
|
+
function thumbCallLocator(blocks: readonly AsmBlock[], bi: number, ins: Instr): string {
|
|
6825
|
+
const call = `'${ins.mnemonic} ${ins.ops[0]}'`;
|
|
6826
|
+
const label = blocks[bi].label;
|
|
6827
|
+
if (!label.startsWith(ANON_BLOCK_PREFIX)) {
|
|
6828
|
+
return `${call} in block '${label}'`;
|
|
6829
|
+
}
|
|
6830
|
+
const same = blocks.flatMap((b) => b.instrs).filter((i) => i.mnemonic === ins.mnemonic && i.ops[0] === ins.ops[0]);
|
|
6831
|
+
const k = same.indexOf(ins);
|
|
6832
|
+
if (k < 0 || same.length === 1) {
|
|
6833
|
+
return `${call} in '${blocks[0].label}'`;
|
|
6834
|
+
}
|
|
6835
|
+
return `${call} in '${blocks[0].label}' (the ${ordinal(k + 1)} of its ${same.length} calls there)`;
|
|
6836
|
+
}
|
|
6837
|
+
|
|
6838
|
+
const ordinal = (n: number): string =>
|
|
6839
|
+
`${n}${n % 100 >= 11 && n % 100 <= 13 ? 'th' : (['th', 'st', 'nd', 'rd'][n % 10] ?? 'th')}`;
|
|
6840
|
+
|
|
4856
6841
|
/** `bl` / `blx`: the call its callee's declaration plans, lowered by `CallDeclarations.lower`. */
|
|
4857
6842
|
function lowerCall(fill: ThumbFill, cur: BlockCursor, ins: Instr): void {
|
|
4858
6843
|
const { name, target, text, calls, frame, ssa, pairs } = fill;
|
|
@@ -4882,6 +6867,8 @@ function lowerCall(fill: ThumbFill, cur: BlockCursor, ins: Instr): void {
|
|
|
4882
6867
|
callee: through === null ? callee : { address: readData(through, bi), reg: through },
|
|
4883
6868
|
ssa,
|
|
4884
6869
|
bi,
|
|
6870
|
+
// what an elided or kept dispatch left in an argument register is no argument
|
|
6871
|
+
parked: parkedIn(fill.staleIdiomValues, reg),
|
|
4885
6872
|
read: (r) => ssa.readVar(r, bi),
|
|
4886
6873
|
write: (r, v) => writeData(r, bi, v),
|
|
4887
6874
|
// ARGUMENT WORD `k` past the registers is a word of this frame's outgoing area, at [sp,#0]
|
|
@@ -4896,6 +6883,74 @@ function lowerCall(fill: ThumbFill, cur: BlockCursor, ins: Instr): void {
|
|
|
4896
6883
|
usedSlotOffsets.add(off);
|
|
4897
6884
|
return ssa.readVar(stackSlotKey(off), bi);
|
|
4898
6885
|
},
|
|
6886
|
+
// …and that block is the arity of a call nothing declares, where its callee's body licensed it
|
|
6887
|
+
stackWords: frame.slotsOk ? frame.outgoingArgs.blocks.get(ins)?.length : undefined,
|
|
6888
|
+
// A FRAME ADDRESS AN UNDECLARED ARITY LEAVES OUT IS AN ARGUMENT ALL THE SAME. The frame walks
|
|
6889
|
+
// read the call as handed every argument register it holds (`frameAddressReceivers`), and the
|
|
6890
|
+
// lift reads the object as written by that receiver (`isFrameObjectAccess`, with no reaching-
|
|
6891
|
+
// definition test). A count that leaves out a register holding one lifts the call without the
|
|
6892
|
+
// address, and the object's reads come back as a local nothing wrote (`func(p0, p1, p2, &d[1])`
|
|
6893
|
+
// as `func()`, then `*data = sp1`). A GUESSED count leaves one out only past a register it did
|
|
6894
|
+
// not set, which is a gap the guess refuses first (`guess` below); what reaches here is a call
|
|
6895
|
+
// through r1, which stops at the bound r1 sets and leaves r2 out (`CallDeclarations.lower`'s
|
|
6896
|
+
// KNOWN GAP), and the `/setup-args` cut (`asksEveryCut`). Which count is right the register
|
|
6897
|
+
// file cannot say, so this refuses.
|
|
6898
|
+
//
|
|
6899
|
+
// …UNLESS A REGISTER IT DOES PASS HOLDS THE SAME ADDRESS, in the call's own block: the callee is
|
|
6900
|
+
// handed the object either way, and agbcc leaves a capture it addressed through in r3 beside
|
|
6901
|
+
// the copy it passes in r0 (`mov r3, sp / str r0, [r3] / mov r0, sp / bl g`).
|
|
6902
|
+
leftOut: (regs, passed) => {
|
|
6903
|
+
const at = frame.frameAddressesAtCall.get(ins);
|
|
6904
|
+
// …and an address at an offset nothing knows (NaN, `&buf[i]`) is the same as no other one
|
|
6905
|
+
const handed = new Set(
|
|
6906
|
+
passed.flatMap((p) => (at?.must.has(p) && Number.isFinite(at.must.get(p)) ? [at.must.get(p)] : [])),
|
|
6907
|
+
);
|
|
6908
|
+
const r = regs.find((r) => at?.may.has(r) && !handed.has(at.may.get(r)!));
|
|
6909
|
+
if (r !== undefined) {
|
|
6910
|
+
throw new FrontendUnsupportedError(
|
|
6911
|
+
`cannot lift '${name}': the call '${ins.mnemonic} ${callee}' has no prototype, and the arity lifted ` +
|
|
6912
|
+
`without one leaves out ${r}, which may hold a frame address — so whether the callee is handed the ` +
|
|
6913
|
+
`object is not decidable`,
|
|
6914
|
+
);
|
|
6915
|
+
}
|
|
6916
|
+
},
|
|
6917
|
+
// NO `/setup-args` READING at a call whose escape the outgoing-argument verdict leaned on (an
|
|
6918
|
+
// escaped `sp+k` excuses every frame word from k up as the callee's to read). That verdict read
|
|
6919
|
+
// every argument register the call holds as handed over (`frameAddressReceivers`), so a reading
|
|
6920
|
+
// that cuts one contradicts it, whatever the register holds: `&t` an earlier block formed, a
|
|
6921
|
+
// second `mov r3, sp` the `leftOut` exception excuses beside a passed `sp`, or the `mov r2, #0`
|
|
6922
|
+
// agbcc reuses from the frame's initialisation (`g(a, s, 0, 0)` would narrow to `g(a, s)`).
|
|
6923
|
+
//
|
|
6924
|
+
// …NOR AT ANY CALL OF A FUNCTION WHOSE BLOCK OTHER THAN THE ENTRY READS AN INCOMING STACK
|
|
6925
|
+
// ARGUMENT. That is a conservative guard, not a property of the cut: the licence that lifts such a
|
|
6926
|
+
// read (`ThumbFrame.settledDepth`) reads no argument register. It keeps the guess off the
|
|
6927
|
+
// functions only that licence lifts (`if (a) g(n, w, e)` with `&w` left in r1 by the entry would
|
|
6928
|
+
// narrow to `g(n)`). The same leftover still narrows where agbcc schedules the stack argument's
|
|
6929
|
+
// load into the entry block instead (`w[1] = e; if (a) g(n, w)` as `g(n)`, which assembles to the
|
|
6930
|
+
// same bytes), as it does with four parameters.
|
|
6931
|
+
withholdsSetupArgs: () => frame.callsReliedOn.has(ins) || fill.frameUses.offEntryArgReads.size > 0,
|
|
6932
|
+
// A GAP IN THE ARGUMENT REGISTERS REFUSES (`fallbackArgc`) UNLESS IT IS DECIDED: `logv(s, 1)`
|
|
6933
|
+
// sets r1 alone and leaves `s` in r0, so a count that stops at r0 would lift `logv()`. The gap is
|
|
6934
|
+
// decided where r1 surely is an argument (frontend/call-plan.ts `decideGap`: a value nothing but
|
|
6935
|
+
// the call reads and no memory read produced). agbcc fills argument words in order, so `s` is
|
|
6936
|
+
// then passed on and the call lifts `logv(a0, 1)`. A run of incoming arguments
|
|
6937
|
+
// passed on with no register set up past it (`g2(a, b)`) leaves no gap, is the same machine code
|
|
6938
|
+
// as `g2()`, and is lifted as the shorter call. The instruction carries no address, so the
|
|
6939
|
+
// refusal names the call by where the .s puts it (`thumbCallLocator`).
|
|
6940
|
+
//
|
|
6941
|
+
// No `refuse`: a guess that fills r0..r3 asks nothing of the outgoing area here. A word agbcc
|
|
6942
|
+
// stages at [sp, #k] for the call is an argument only where `analyzeOutgoingArgs` licensed the
|
|
6943
|
+
// block, and the frame model refuses any other store there. agbcc's `g5(1, 2, 3, 4, a)` to an
|
|
6944
|
+
// undeclared `g5` declines as "stack pointer used as data" before any call is lowered.
|
|
6945
|
+
guess: { at: thumbCallLocator(fill.cfg.asmBlocks, bi, ins) },
|
|
6946
|
+
// …AND AT EVERY OTHER CALL OF A FUNCTION WHOSE VERDICT LEANED ON AN ESCAPE, the reading asks
|
|
6947
|
+
// `leftOut` of every register it cuts, not only of those this block wrote. The verdict read every
|
|
6948
|
+
// argument register as possibly handed over, so a frame address an EARLIER block left there is
|
|
6949
|
+
// not the dead base the reading guesses at: `g(b, s)` escapes, then `&t` formed before a branch
|
|
6950
|
+
// reaches `if (a) h(b, a, t); else *q = t[0];` in r2, and the cut `h(b, a)` would drop it while
|
|
6951
|
+
// the else arm still reads `t`. The question refuses only frame addresses, so a call that cuts
|
|
6952
|
+
// none (a sibling `f2()`) still narrows.
|
|
6953
|
+
asksEveryCut: () => frame.escapesReliedOn.size > 0,
|
|
4899
6954
|
pairs: {
|
|
4900
6955
|
fuse: (lo, hi) => pairs.fuseHalves(irb, lo, hi),
|
|
4901
6956
|
project: (whole, half) => pairs.projectHalf(irb, whole, half, ins),
|
|
@@ -4915,9 +6970,27 @@ function lowerTerminator(fill: ThumbFill, cur: BlockCursor): void {
|
|
|
4915
6970
|
const succ = (label: string): Successor => ({ block: irBlocks[cfg.labelIndex.get(label)!], args: [] });
|
|
4916
6971
|
const jt = text.tables.get(ab);
|
|
4917
6972
|
if (jt) {
|
|
4918
|
-
// Regime B:
|
|
4919
|
-
//
|
|
4920
|
-
|
|
6973
|
+
// Regime B: a `switch_br` over the scrutinee — N case blocks (values 0..N-1, dense) followed by
|
|
6974
|
+
// the default block (last successor). From the bounds block, the `cmp`/`bhi` are subsumed. A
|
|
6975
|
+
// kept dispatch (`JumpTable.extras`) switches on the scrutinee its block was entered with: the
|
|
6976
|
+
// idiom read it first, and the extras after it may overwrite the register. Its bounds test stays
|
|
6977
|
+
// a branch and already sent every value past N-1 to the default, so its switch is `exhaustive`.
|
|
6978
|
+
const scrut = cur.dispatchScrutinee ?? readVar(reg(jt.scrutReg), bi);
|
|
6979
|
+
// What the unlifted idiom left in the registers no extra rewrote is no value the lift models: on
|
|
6980
|
+
// the hardware the case blocks see the scaled index, the table's address and the case's own code
|
|
6981
|
+
// address. Each is a marker here, and a read of one declines (`refuseStaleIdiomReads`). The
|
|
6982
|
+
// elided form cannot put them here, since its bounds block's default path never ran the idiom: it
|
|
6983
|
+
// writes them at each case target's entry instead (`openBlockCursor`).
|
|
6984
|
+
if (cur.dispatchScrutinee !== null) {
|
|
6985
|
+
for (const r of [...jt.leaves.keys()].filter((r) => !jt.extras.some((ins) => ins.ops[0] === r))) {
|
|
6986
|
+
ssa.writeVar(
|
|
6987
|
+
reg(r),
|
|
6988
|
+
bi,
|
|
6989
|
+
staleMarker(irb.ops, reg(r), { reg: r, form: 'kept', parks: true }, fill.staleIdiomValues),
|
|
6990
|
+
);
|
|
6991
|
+
}
|
|
6992
|
+
}
|
|
6993
|
+
pushSwitchBr(irb.ops, scrut, [...jt.caseLabels.map(succ), succ(jt.defaultLabel)], cur.dispatchScrutinee !== null);
|
|
4921
6994
|
} else if (!last) {
|
|
4922
6995
|
// an EMPTY block is only ever the synthetic entry preheader (decoded blocks are non-empty):
|
|
4923
6996
|
// fall through to the real entry, whose loop-header phis take their entry operand from here.
|
|
@@ -4951,7 +7024,7 @@ function lowerTerminator(fill: ThumbFill, cur: BlockCursor): void {
|
|
|
4951
7024
|
last.ops[0] === scratch &&
|
|
4952
7025
|
prev?.mnemonic === 'pop' &&
|
|
4953
7026
|
regListOf(prev.ops)?.join() === scratch;
|
|
4954
|
-
pairs.
|
|
7027
|
+
pairs.noteWordReturn(bi, lo, widerEpilogue);
|
|
4955
7028
|
}
|
|
4956
7029
|
irb.ops.push(mkOp('ret', { operands: [whole ?? lo] }));
|
|
4957
7030
|
}
|
|
@@ -4965,10 +7038,25 @@ function lowerTerminator(fill: ThumbFill, cur: BlockCursor): void {
|
|
|
4965
7038
|
`cannot lift '${name}': conditional branch '${last.mnemonic}' has no reaching compare: ${cur.noCmpWhy}`,
|
|
4966
7039
|
);
|
|
4967
7040
|
}
|
|
7041
|
+
const { lhs, cmn } = cur.pendingCmp;
|
|
7042
|
+
let { rhs } = cur.pendingCmp;
|
|
7043
|
+
if (cmn) {
|
|
7044
|
+
if (!CMN_EXACT.has(last.mnemonic)) {
|
|
7045
|
+
throw new FrontendUnsupportedError(
|
|
7046
|
+
`cannot lift '${name}': conditional branch '${last.mnemonic}' tests the carry of a 'cmn', and the carry ` +
|
|
7047
|
+
'out of a + b is not a >=u -b (they differ at b == 0)',
|
|
7048
|
+
);
|
|
7049
|
+
}
|
|
7050
|
+
// The signed branches read the TRUE sign of a + b, so the negation they compare against is
|
|
7051
|
+
// the mathematical one (`neg_s`, ir/opcodes.ts). `beq`/`bne` read the wrapped sum, which the
|
|
7052
|
+
// plain wrapping `neg` spells (see `CMN_EXACT` for the one `s32` input it leaves undefined).
|
|
7053
|
+
const negated = mkValue(T.unk(32));
|
|
7054
|
+
const signed = last.mnemonic !== 'beq' && last.mnemonic !== 'bne';
|
|
7055
|
+
irb.ops.push(mkOp(signed ? 'neg_s' : 'neg', { operands: [rhs], results: [negated] }));
|
|
7056
|
+
rhs = negated;
|
|
7057
|
+
}
|
|
4968
7058
|
const cond = mkValue(T.unk(32));
|
|
4969
|
-
irb.ops.push(
|
|
4970
|
-
mkOp(COND_OPCODE[last.mnemonic], { operands: [cur.pendingCmp.lhs, cur.pendingCmp.rhs], results: [cond] }),
|
|
4971
|
-
);
|
|
7059
|
+
irb.ops.push(mkOp(COND_OPCODE[last.mnemonic], { operands: [lhs, rhs], results: [cond] }));
|
|
4972
7060
|
irb.ops.push(
|
|
4973
7061
|
mkOp('cond_br', { operands: [cond], successors: [succ(last.ops[0]), succ(fallLabel(cfg.asmBlocks, name, bi))] }),
|
|
4974
7062
|
);
|
|
@@ -4990,33 +7078,20 @@ function fallLabel(asmBlocks: readonly AsmBlock[], name: string, bi: number): st
|
|
|
4990
7078
|
return nb.label;
|
|
4991
7079
|
}
|
|
4992
7080
|
|
|
4993
|
-
// A struct returned through memory is spelled `local = f(..)`, which hands the callee that local's
|
|
4994
|
-
// address: the destination must be a frame object the audit accepted. A global or a pointer handed
|
|
4995
|
-
// over directly is another spelling (`gDst = f(..)` needs the global declared as the struct), which
|
|
4996
|
-
// nothing here states.
|
|
4997
|
-
function refuseSretOutsideFrame(name: string, irBlocks: readonly Block[]): void {
|
|
4998
|
-
const laddrs = new Set(
|
|
4999
|
-
irBlocks.flatMap((b) => b.ops.filter((op) => op.opcode === 'laddr').map((op) => op.results[0])),
|
|
5000
|
-
);
|
|
5001
|
-
for (const op of irBlocks.flatMap((b) => b.ops)) {
|
|
5002
|
-
if (op.opcode === 'call' && op.attrs.sret === true && !laddrs.has(op.operands[0])) {
|
|
5003
|
-
throw new FrontendUnsupportedError(
|
|
5004
|
-
`cannot lift '${name}': \`${op.attrs.target as string}\` returns struct ${typeToString(op.results[0].type)} through the pointer in r0, ` +
|
|
5005
|
-
'and that pointer is not the address of a local of this frame — a struct returned into a global or ' +
|
|
5006
|
-
'through a pointer is not modelled',
|
|
5007
|
-
);
|
|
5008
|
-
}
|
|
5009
|
-
}
|
|
5010
|
-
}
|
|
5011
|
-
|
|
5012
7081
|
/** The ARMv4T / Thumb (agbcc) frontend, registered for the `armv4t` target. */
|
|
5013
|
-
export const thumbFrontend: Frontend = {
|
|
7082
|
+
export const thumbFrontend: Frontend = {
|
|
7083
|
+
id: 'thumb',
|
|
7084
|
+
inputFormat: 'gnu-as',
|
|
7085
|
+
lift,
|
|
7086
|
+
entryStackWords: thumbEntryWordsFromText,
|
|
7087
|
+
};
|
|
5014
7088
|
|
|
5015
7089
|
/** Internal surface for this module's own tests, and for nothing else. `@asmlift/core` exports
|
|
5016
7090
|
* every source path under `./*`, so a plain `export` here would put the pad table, the pad predicate, the witness serialisation and the lift's stages into the
|
|
5017
7091
|
* package's public API, where no consumer should be able to depend on them. The name says what it
|
|
5018
7092
|
* is; nothing in `src` reads it. */
|
|
5019
7093
|
export const __testing = {
|
|
7094
|
+
decode,
|
|
5020
7095
|
PAD_ENCODINGS,
|
|
5021
7096
|
isPadInstr,
|
|
5022
7097
|
factKey,
|