@asmlift/core 0.10.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/package.json +1 -1
- package/src/backend/c.ts +1 -0
- package/src/backend/cfamily.ts +33 -12
- package/src/backend/cpp.ts +1 -0
- package/src/backend/lines.ts +10 -0
- package/src/backend/pascal.ts +17 -5
- package/src/cdecl/parse.ts +21 -0
- package/src/contracts.ts +73 -20
- package/src/frontend/arm-body.ts +155 -0
- package/src/frontend/asmdata.ts +2 -1
- package/src/frontend/call-plan.ts +197 -45
- package/src/frontend/callee-body.ts +370 -0
- package/src/frontend/device-pins.ts +18 -8
- package/src/frontend/frame-objects.ts +504 -61
- package/src/frontend/frontend.ts +20 -0
- package/src/frontend/jump-table.ts +35 -20
- package/src/frontend/mips.ts +226 -8
- package/src/frontend/ppc.ts +26 -2
- package/src/frontend/register-struct-call.ts +1 -86
- package/src/frontend/ssa.ts +263 -1
- package/src/frontend/stackargs.ts +95 -17
- package/src/frontend/thumb-entry.ts +340 -0
- package/src/frontend/thumb.ts +410 -61
- package/src/ir/alias.ts +29 -8
- package/src/ir/bits.ts +1081 -66
- package/src/ir/core.ts +154 -4
- package/src/ir/entry.ts +3 -5
- package/src/ir/opcodes.ts +28 -0
- package/src/ir/parse.ts +1 -0
- package/src/ir/simplify.ts +61 -10
- package/src/ir/verify.ts +16 -0
- package/src/l3/argbase.ts +2 -0
- package/src/l3/ast.ts +294 -2
- package/src/l3/basecse.ts +35 -10
- package/src/l3/caseendgoto.ts +223 -0
- package/src/l3/coalesce.ts +42 -14
- package/src/l3/constinit.ts +34 -6
- package/src/l3/dce.ts +52 -8
- package/src/l3/ptrcell.ts +2 -0
- package/src/l3/regspell.ts +2 -0
- package/src/l3/reindex.ts +13 -4
- package/src/l3/scopebase.ts +2 -0
- package/src/l3/tailmerge.ts +2 -0
- package/src/pipeline.ts +507 -23
- package/src/proto-context.ts +135 -1
- package/src/raise/const.ts +25 -0
- package/src/raise/declared-volatile.ts +1 -1
- package/src/raise/divpow2.ts +192 -0
- package/src/raise/offsetnames.ts +412 -10
- package/src/raise/pre-recovery.ts +8 -5
- package/src/raise/pre-structure.ts +24 -5
- package/src/raise/reroll.ts +4 -24
- package/src/raise/shortcircuit.ts +192 -23
- package/src/raise/unpre.ts +594 -0
- package/src/rank-variations.ts +97 -4
- package/src/rank.ts +258 -43
- package/src/structure/analysis.ts +56 -10
- package/src/structure/cross-jumped-tail.ts +216 -0
- package/src/structure/globalaccess.ts +46 -1
- package/src/structure/structure.ts +4221 -368
- package/src/structure/switch-recover.ts +345 -26
- package/src/structure/unfolded-branch-constant.ts +90 -0
- package/src/symbols.ts +52 -15
- package/src/target.ts +269 -8
- package/src/trace.ts +28 -14
- package/src/variation-definitions.ts +207 -6
- package/src/variation-gates.ts +2 -0
- package/src/variation-tokens.ts +63 -2
package/src/frontend/thumb.ts
CHANGED
|
@@ -22,12 +22,20 @@ import { ownArgumentWordsOn } from '../aggregate';
|
|
|
22
22
|
import { Block, Fn, Successor, Value, mergeClasses, mkOp, mkValue } from '../ir/core';
|
|
23
23
|
import type { Opcode } from '../ir/opcodes';
|
|
24
24
|
import { T } from '../ir/types';
|
|
25
|
-
import {
|
|
25
|
+
import {
|
|
26
|
+
type FnProto,
|
|
27
|
+
type Prototypes,
|
|
28
|
+
declaresVoidReturn,
|
|
29
|
+
ownLeadingWordRegisters,
|
|
30
|
+
ownReturnWidth,
|
|
31
|
+
wordsOf,
|
|
32
|
+
} from '../proto';
|
|
26
33
|
import type { ContextGlobals } from '../proto-context';
|
|
27
34
|
import { type SymbolMap, lookupInterior, lookupSymbol } from '../symbols';
|
|
28
35
|
import { ARM_REGISTER_ALIASES, type TargetDescription } from '../target';
|
|
29
36
|
import type { AsmData } from './asmdata';
|
|
30
37
|
import { type CallDeclarations, type CallLowering, callDeclarations, foldByValueArgs } from './call-plan';
|
|
38
|
+
import { type CalleeStackWords, asksCalleeBody } from './callee-body';
|
|
31
39
|
import { pinDeadReads, unreadPairHalf } from './dead-reads';
|
|
32
40
|
import { pinDeviceAccesses } from './device-pins';
|
|
33
41
|
import { pushSwitchBr } from './emit';
|
|
@@ -70,6 +78,7 @@ import {
|
|
|
70
78
|
assertScratchRegsPartitioned,
|
|
71
79
|
makeSsaBuilder,
|
|
72
80
|
mintArgSlotHoles,
|
|
81
|
+
recordAsmLayout,
|
|
73
82
|
registerArgSlots,
|
|
74
83
|
registerLiveIns,
|
|
75
84
|
stackArgKey,
|
|
@@ -84,6 +93,7 @@ import {
|
|
|
84
93
|
analyzeOutgoingArgs,
|
|
85
94
|
loadedWordBound,
|
|
86
95
|
} from './stackargs';
|
|
96
|
+
import { thumbEntryWordsFromText } from './thumb-entry';
|
|
87
97
|
|
|
88
98
|
interface Instr {
|
|
89
99
|
/** the CANONICAL spelling — legacy names are normalised (see LEGACY_MNEMONICS) so that every
|
|
@@ -1357,9 +1367,17 @@ function decode(
|
|
|
1357
1367
|
if (funcLabels.length > 0) {
|
|
1358
1368
|
const fi = funcLabels.indexOf(name);
|
|
1359
1369
|
if (fi !== -1) {
|
|
1360
|
-
|
|
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;
|
|
1361
1379
|
const starts = funcLabels
|
|
1362
|
-
.map((l) =>
|
|
1380
|
+
.map((l) => firstAt.get(l) ?? -1)
|
|
1363
1381
|
.filter((s) => s > sliceStart)
|
|
1364
1382
|
.sort((a, b) => a - b);
|
|
1365
1383
|
boundaries = [...starts, allFlat.length];
|
|
@@ -2524,10 +2542,10 @@ function recoverJumpTable(
|
|
|
2524
2542
|
// - a store, a call, a transfer, a flag READ (`adc`/`sbc`), or any other mnemonic;
|
|
2525
2543
|
// - a write to the loaded target (it is what `mov pc` jumps through), or to sp/pc/lr. A write to
|
|
2526
2544
|
// the scrutinee is admitted: the switch does not read it after the extras, and the case blocks
|
|
2527
|
-
// read what the extras left there on the hardware too. lr is refused
|
|
2528
|
-
// `bx lr`
|
|
2529
|
-
//
|
|
2530
|
-
//
|
|
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;
|
|
2531
2549
|
// - a read of a register the idiom wrote (index, table pointer, target) before an extra rewrote it:
|
|
2532
2550
|
// the idiom is not lifted, so such a read would see the value from before the dispatch.
|
|
2533
2551
|
// The reason is returned so the decline can name it (`readThumbText`'s indirect-jump refusal).
|
|
@@ -2580,8 +2598,8 @@ export function lift(
|
|
|
2580
2598
|
context?: ContextGlobals,
|
|
2581
2599
|
options: LiftOptions = {},
|
|
2582
2600
|
): Fn {
|
|
2583
|
-
return liftWithRelift(name, (oneObject) =>
|
|
2584
|
-
liftOnce(name, asm, target, prototypes, asmData, symbols, context, oneObject, options),
|
|
2601
|
+
return liftWithRelift(name, (oneObject, homesReleased) =>
|
|
2602
|
+
liftOnce(name, asm, target, prototypes, asmData, symbols, context, oneObject, homesReleased, options),
|
|
2585
2603
|
);
|
|
2586
2604
|
}
|
|
2587
2605
|
|
|
@@ -2603,9 +2621,19 @@ function liftOnce(
|
|
|
2603
2621
|
symbols: SymbolMap | undefined,
|
|
2604
2622
|
context: ContextGlobals | undefined,
|
|
2605
2623
|
oneObject: FrameRange | undefined,
|
|
2606
|
-
|
|
2624
|
+
homesReleased: boolean,
|
|
2625
|
+
options: Pick<
|
|
2626
|
+
LiftOptions,
|
|
2627
|
+
| 'contextOwnWords'
|
|
2628
|
+
| 'contextOwnProto'
|
|
2629
|
+
| 'ownMemoryReturn'
|
|
2630
|
+
| 'calleeBodyBound'
|
|
2631
|
+
| 'calleeStackWords'
|
|
2632
|
+
| 'stackSized'
|
|
2633
|
+
| 'entryReadCalls'
|
|
2634
|
+
>,
|
|
2607
2635
|
): Fn | FrameObjectRelift {
|
|
2608
|
-
const fill = thumbFillOf(name, asm, target, prototypes, symbols, oneObject, options);
|
|
2636
|
+
const fill = thumbFillOf(name, asm, target, prototypes, symbols, oneObject, options, homesReleased);
|
|
2609
2637
|
const { cfg, frame, ssa, pairs, statics } = fill;
|
|
2610
2638
|
const { fn, irBlocks, paramReg } = ssa;
|
|
2611
2639
|
|
|
@@ -2624,6 +2652,7 @@ function liftOnce(
|
|
|
2624
2652
|
throw new FrontendUnsupportedError(m);
|
|
2625
2653
|
},
|
|
2626
2654
|
fill.calls,
|
|
2655
|
+
declaresVoidReturn(prototypes[name]),
|
|
2627
2656
|
);
|
|
2628
2657
|
// After the fill, so a block's own refusal (the frame's, the operands') keeps its place in the
|
|
2629
2658
|
// refusal order: the proof reads only the text and the CFG, and changes nothing.
|
|
@@ -2649,6 +2678,20 @@ function liftOnce(
|
|
|
2649
2678
|
const argSlots = stackArgSlots(registerArgSlots(target.argRegs), target.argRegs);
|
|
2650
2679
|
mintArgSlotHoles(ssa, cfg.preds[0].length > 0, argSlots);
|
|
2651
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
|
+
}
|
|
2652
2695
|
|
|
2653
2696
|
// A struct passed by value becomes the one object its words hold (call-plan.ts `foldByValueArgs`)
|
|
2654
2697
|
// before the audit judges the frame objects, that one among them.
|
|
@@ -2671,6 +2714,7 @@ function liftOnce(
|
|
|
2671
2714
|
symbols,
|
|
2672
2715
|
target,
|
|
2673
2716
|
oneObject,
|
|
2717
|
+
releasableHomes: frame.releasableHomes,
|
|
2674
2718
|
});
|
|
2675
2719
|
if (verdict !== undefined && 'oneObject' in verdict) {
|
|
2676
2720
|
return verdict;
|
|
@@ -2803,16 +2847,23 @@ interface LrState {
|
|
|
2803
2847
|
* far-jump relay's lowering needs and does not itself prove (`lowerFarJumpRelays`): with every
|
|
2804
2848
|
* return reading the CALLER's lr, the lr a relay writes cannot be what a return jumps to.
|
|
2805
2849
|
*
|
|
2806
|
-
*
|
|
2807
|
-
*
|
|
2808
|
-
*
|
|
2809
|
-
*
|
|
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.
|
|
2810
2863
|
*
|
|
2811
2864
|
* Blocks no path from the entry reaches are not asked: they never run. */
|
|
2812
2865
|
function refuseReturnsNotThroughCallersLr(name: string, cfg: ThumbCfg): void {
|
|
2813
|
-
const { asmBlocks,
|
|
2814
|
-
const succs: number[][] = asmBlocks.map(() => []);
|
|
2815
|
-
preds.forEach((ps, b) => ps.forEach((p) => succs[p].push(b)));
|
|
2866
|
+
const { asmBlocks, succs, entryReachable } = cfg;
|
|
2816
2867
|
const LR = 'r14';
|
|
2817
2868
|
const join = (a: LrState, b: LrState): LrState => {
|
|
2818
2869
|
const depth = a.depth !== null && a.depth === b.depth ? a.depth : null;
|
|
@@ -2863,10 +2914,9 @@ function refuseReturnsNotThroughCallersLr(name: string, cfg: ThumbCfg): void {
|
|
|
2863
2914
|
}
|
|
2864
2915
|
}
|
|
2865
2916
|
} else if (xfer === 'return') {
|
|
2866
|
-
// `bx rN
|
|
2867
|
-
// returns by name (see the doc comment).
|
|
2917
|
+
// `bx rN`, `bx lr` and `mov pc, lr` alike: the register must still hold the caller's lr.
|
|
2868
2918
|
const via = canonReg(mn === 'bx' ? (ins.ops[0] ?? '') : (ins.ops[1] ?? ''));
|
|
2869
|
-
if (via
|
|
2919
|
+
if (via === null || !regs.has(via)) {
|
|
2870
2920
|
refuse(ins);
|
|
2871
2921
|
}
|
|
2872
2922
|
} else if (mn === 'push') {
|
|
@@ -2887,7 +2937,12 @@ function refuseReturnsNotThroughCallersLr(name: string, cfg: ThumbCfg): void {
|
|
|
2887
2937
|
} else if (!isSpReg(ins.ops[0])) {
|
|
2888
2938
|
const writes = regWritesOf(ins);
|
|
2889
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);
|
|
2890
2942
|
regs.clear();
|
|
2943
|
+
if (kept) {
|
|
2944
|
+
regs.add(LR);
|
|
2945
|
+
}
|
|
2891
2946
|
} else {
|
|
2892
2947
|
const src = /^movs?$/.test(mn) ? canonReg(ins.ops[1] ?? '') : null;
|
|
2893
2948
|
const copies = src !== null && regs.has(src);
|
|
@@ -3196,6 +3251,8 @@ interface ThumbCfg {
|
|
|
3196
3251
|
readonly labelIndex: ReadonlyMap<string, number>;
|
|
3197
3252
|
/** Each block's predecessors, one entry per edge. */
|
|
3198
3253
|
readonly preds: number[][];
|
|
3254
|
+
/** Each block's successors, `preds` turned around. */
|
|
3255
|
+
readonly succs: readonly (readonly number[])[];
|
|
3199
3256
|
readonly entryReachable: ReadonlySet<number>;
|
|
3200
3257
|
}
|
|
3201
3258
|
|
|
@@ -3238,7 +3295,9 @@ function thumbCfg(name: string, blocks: readonly AsmBlock[], tables: ReadonlyMap
|
|
|
3238
3295
|
}
|
|
3239
3296
|
return live;
|
|
3240
3297
|
})();
|
|
3241
|
-
|
|
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 };
|
|
3242
3301
|
}
|
|
3243
3302
|
|
|
3244
3303
|
// --- CFG (successors per block) as label lists; fallthrough + branch targets (via classifyXfer) ---
|
|
@@ -3332,6 +3391,9 @@ interface ThumbFrame {
|
|
|
3332
3391
|
* (`narrowSlotReloads`), and the copies those reloads consume, which name no object */
|
|
3333
3392
|
readonly narrowReloadOf: ReadonlyMap<Instr, number>;
|
|
3334
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>;
|
|
3335
3397
|
}
|
|
3336
3398
|
|
|
3337
3399
|
/** The frame offset of each word of a declared call's outgoing block, or null where it stages none.
|
|
@@ -3377,18 +3439,31 @@ function calleeRegister(ins: Instr, target: TargetDescription): string | null |
|
|
|
3377
3439
|
function measureThumbFrame({
|
|
3378
3440
|
name,
|
|
3379
3441
|
target,
|
|
3380
|
-
cfg: { asmBlocks, preds, entryReachable },
|
|
3442
|
+
cfg: { asmBlocks, preds, succs, entryReachable },
|
|
3381
3443
|
calls,
|
|
3382
3444
|
oneObject,
|
|
3445
|
+
homesReleased = false,
|
|
3383
3446
|
own,
|
|
3447
|
+
dropsVoidReturn,
|
|
3448
|
+
stackWordsOf,
|
|
3384
3449
|
}: {
|
|
3385
3450
|
name: string;
|
|
3386
3451
|
target: TargetDescription;
|
|
3387
3452
|
cfg: ThumbCfg;
|
|
3388
3453
|
calls: CallDeclarations;
|
|
3389
3454
|
oneObject: FrameRange | undefined;
|
|
3455
|
+
/** whether a first audit released the argument homes to the slot model (`releasableHomes`) */
|
|
3456
|
+
homesReleased?: boolean;
|
|
3390
3457
|
/** the function's own declaration (`ownDeclarationOf`), read only where it `proves` */
|
|
3391
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;
|
|
3392
3467
|
}): ThumbFrame {
|
|
3393
3468
|
// THE FRAME BASE PASSED TO A CALLEE. What this computes is exactly what its name says and nothing
|
|
3394
3469
|
// more: somewhere in entry-reachable code a bare `mov rD, sp` copies the frame base into a
|
|
@@ -3469,7 +3544,7 @@ function measureThumbFrame({
|
|
|
3469
3544
|
// wider frame the publish escape admits exactly the shape the callee escape's table warns
|
|
3470
3545
|
// about; at one word it cannot occur. A wider frame's publish is read by the outgoing-argument
|
|
3471
3546
|
// `escape` event instead (below), per program point, which is the reading that survives a block
|
|
3472
|
-
// copy: see "ONE DECISION,
|
|
3547
|
+
// copy: see "ONE DECISION, FOUR LICENCES" above `capturedObjectIsTheWholeFrame`.
|
|
3473
3548
|
//
|
|
3474
3549
|
// THE WALK IS `heldFrameWalk`, the same one the callee escape is spelled with, because the
|
|
3475
3550
|
// question is the same one and only the consuming event differs. Here that event is a WORD store
|
|
@@ -3606,7 +3681,7 @@ function measureThumbFrame({
|
|
|
3606
3681
|
// `localArea >= 4` (measured). Nothing about the frame size moves it either way — which is what
|
|
3607
3682
|
// makes it the wrong function to reason about a wider licence from.
|
|
3608
3683
|
//
|
|
3609
|
-
// ONE DECISION,
|
|
3684
|
+
// ONE DECISION, FOUR LICENCES. This gate and the outgoing-argument `escape` event (fed below,
|
|
3610
3685
|
// under `compilerBehaviors.escapedFrameAddressNamesALocal`) read the same escapes
|
|
3611
3686
|
// (`frameAddressReceivers`) and differ only in flow-sensitivity. This gate is flow-INSENSITIVE —
|
|
3612
3687
|
// one escape anywhere and no frame word is any call's argument — which is sound only where no
|
|
@@ -3614,6 +3689,17 @@ function measureThumbFrame({
|
|
|
3614
3689
|
// leaves, at any frame size; that field says why that survives a block copy. So what this gate
|
|
3615
3690
|
// adds is a store that reaches an undeclared call BEFORE the address leaves (the CONTROL of "a
|
|
3616
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.
|
|
3617
3703
|
const capturedObjectIsTheWholeFrame =
|
|
3618
3704
|
target.compilerBehaviors.oneWordFrameIsTheCapturedObject === true &&
|
|
3619
3705
|
(frameBasePassedToCallee || frameBasePublishedToMemory) &&
|
|
@@ -3661,22 +3747,35 @@ function measureThumbFrame({
|
|
|
3661
3747
|
// offsets either: a store there reaching a call unread is the object being filled for the
|
|
3662
3748
|
// callee that is handed it.
|
|
3663
3749
|
//
|
|
3664
|
-
// THE FRAME BASE, ONLY
|
|
3665
|
-
//
|
|
3666
|
-
// outgoing area, and [sp,#0] is then a call's fifth argument word (the
|
|
3667
|
-
// `capturedObjectIsTheWholeFrame`, which keeps its own licence for a
|
|
3668
|
-
//
|
|
3669
|
-
//
|
|
3670
|
-
//
|
|
3671
|
-
//
|
|
3672
|
-
//
|
|
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);
|
|
3673
3770
|
const noCallTakesAStackWord = [...entryReachable].every((b) =>
|
|
3674
3771
|
asmBlocks[b].instrs.every((ins) => {
|
|
3675
3772
|
if (ins.mnemonic !== 'bl' && ins.mnemonic !== 'blx') {
|
|
3676
3773
|
return true;
|
|
3677
3774
|
}
|
|
3678
3775
|
const declared = calleeRegister(ins, target) === null ? calls.declaredCall(ins.ops[0] ?? '?') : null;
|
|
3679
|
-
|
|
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;
|
|
3680
3779
|
}),
|
|
3681
3780
|
);
|
|
3682
3781
|
|
|
@@ -3738,12 +3837,67 @@ function measureThumbFrame({
|
|
|
3738
3837
|
// `ownLeadingWordRegisters`): a `u32 k` spilled on entry and read back as `(u8)k` is no struct's
|
|
3739
3838
|
// home, and lifts as the slot narrowed. A copy kept here is no capture, so its offset leaves
|
|
3740
3839
|
// `constantCaptureOffsets` and the slot's own `str`/`ldr` stay slot accesses; one dropped stays a
|
|
3741
|
-
// capture.
|
|
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
|
+
);
|
|
3742
3889
|
const narrowCandidates = narrowSlotReloads(
|
|
3743
3890
|
asmBlocks,
|
|
3744
3891
|
entryReachable,
|
|
3745
3892
|
localArea,
|
|
3746
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
|
+
},
|
|
3747
3901
|
);
|
|
3748
3902
|
const candidateCopies = new Set([...narrowCandidates.values()].map((n) => n.copy));
|
|
3749
3903
|
const otherCaptureOffsets = new Set(
|
|
@@ -3783,15 +3937,21 @@ function measureThumbFrame({
|
|
|
3783
3937
|
}
|
|
3784
3938
|
}
|
|
3785
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>();
|
|
3786
3945
|
for (const [load, { slot }] of narrowCandidates) {
|
|
3787
|
-
if (
|
|
3788
|
-
|
|
3789
|
-
|
|
3790
|
-
|
|
3791
|
-
|
|
3792
|
-
|
|
3793
|
-
|
|
3794
|
-
|
|
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
|
+
}
|
|
3795
3955
|
}
|
|
3796
3956
|
}
|
|
3797
3957
|
const narrowReloadCopies: ReadonlySet<Instr> = new Set(
|
|
@@ -3801,7 +3961,13 @@ function measureThumbFrame({
|
|
|
3801
3961
|
[...captureOffsetOf]
|
|
3802
3962
|
.filter(([ins]) => !narrowReloadCopies.has(ins))
|
|
3803
3963
|
.map(([, o]) => o)
|
|
3804
|
-
.filter(
|
|
3964
|
+
.filter(
|
|
3965
|
+
(o) =>
|
|
3966
|
+
o > 0 ||
|
|
3967
|
+
(o === 0 &&
|
|
3968
|
+
((noCallTakesAStackWord && frameBaseEscapes) ||
|
|
3969
|
+
(frameBaseIsReturnStorage && target.compilerBehaviors.escapedFrameAddressNamesALocal === true))),
|
|
3970
|
+
),
|
|
3805
3971
|
);
|
|
3806
3972
|
|
|
3807
3973
|
// THE OUTGOING STACK-ARGUMENT AREA. `frontend/stackargs.ts` holds the licence and every refusal;
|
|
@@ -3825,14 +3991,18 @@ function measureThumbFrame({
|
|
|
3825
3991
|
const off = escapedOffsetOf.get(ins);
|
|
3826
3992
|
return escapesHere && off !== undefined ? [{ kind: 'escape', off }] : [];
|
|
3827
3993
|
};
|
|
3994
|
+
const stagesOutgoing = target.compilerBehaviors.stagesOutgoingArgsInFrame === true;
|
|
3828
3995
|
const eventBlocks = asmBlocks.map((ab) => ({
|
|
3829
3996
|
events: ab.instrs.flatMap((ins): StackArgsEvent<Instr>[] => {
|
|
3830
3997
|
const escaped = escapeEvent(ins);
|
|
3831
3998
|
if (escaped.length > 0 && ins.mnemonic === 'str') {
|
|
3832
3999
|
return escaped;
|
|
3833
4000
|
}
|
|
3834
|
-
// …and a narrow reload of a word slot is that slot's reload, as its word reload would be
|
|
3835
|
-
|
|
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;
|
|
3836
4006
|
if (off !== null && !constantCaptureOffsets.has(off) && !inByValueBlock(off, 4)) {
|
|
3837
4007
|
return [{ kind: /^str/.test(ins.mnemonic) ? 'store' : 'load', off }];
|
|
3838
4008
|
}
|
|
@@ -3847,6 +4017,10 @@ function measureThumbFrame({
|
|
|
3847
4017
|
callee,
|
|
3848
4018
|
declared: declared === null ? null : outgoingBlock(declared.widths, target),
|
|
3849
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
|
+
: {}),
|
|
3850
4024
|
},
|
|
3851
4025
|
];
|
|
3852
4026
|
}
|
|
@@ -4021,6 +4195,7 @@ function measureThumbFrame({
|
|
|
4021
4195
|
isOwnFrameWordSlot,
|
|
4022
4196
|
narrowReloadOf,
|
|
4023
4197
|
narrowReloadCopies,
|
|
4198
|
+
releasableHomes,
|
|
4024
4199
|
};
|
|
4025
4200
|
}
|
|
4026
4201
|
|
|
@@ -4138,10 +4313,11 @@ function frameAddressDefined(ins: Instr, held: ReadonlyMap<string, number>): [st
|
|
|
4138
4313
|
//
|
|
4139
4314
|
// What this finds is a shape and not yet a licence; the copy is a capture everywhere it fails:
|
|
4140
4315
|
// * the copy is `mov rD, sp` or `add rD, sp, #c`, entry-reachable, and in ONE block it is read
|
|
4141
|
-
// by exactly one instruction, a zero-extending `ldrb`/`ldrh` at an immediate offset from rD
|
|
4142
|
-
//
|
|
4143
|
-
//
|
|
4144
|
-
//
|
|
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.
|
|
4145
4321
|
// * the load's bytes are the low-order part of a whole aligned word of the reserved local area.
|
|
4146
4322
|
// A load at any other byte of the word is not that word's value narrowed, whatever it reads.
|
|
4147
4323
|
// * `ldrsb`/`ldrsh` exist only register-offset in Thumb-1, and agbcc's signed use reloads a word
|
|
@@ -4157,6 +4333,7 @@ function narrowSlotReloads(
|
|
|
4157
4333
|
entryReachable: ReadonlySet<number>,
|
|
4158
4334
|
localArea: number,
|
|
4159
4335
|
littleEndian: boolean,
|
|
4336
|
+
diesAfter: (b: number, from: number, r: string) => boolean,
|
|
4160
4337
|
): Map<Instr, { copy: Instr; slot: number }> {
|
|
4161
4338
|
const found = new Map<Instr, { copy: Instr; slot: number }>();
|
|
4162
4339
|
for (const b of entryReachable) {
|
|
@@ -4172,7 +4349,8 @@ function narrowSlotReloads(
|
|
|
4172
4349
|
return;
|
|
4173
4350
|
}
|
|
4174
4351
|
const rD = reg(d);
|
|
4175
|
-
for (
|
|
4352
|
+
for (let li = at + 1; li < instrs.length; li++) {
|
|
4353
|
+
const ins = instrs[li];
|
|
4176
4354
|
if (ins.mnemonic === 'bl' || ins.mnemonic === 'blx' || classifyXfer(ins) !== null || modifiesSp(ins)) {
|
|
4177
4355
|
return;
|
|
4178
4356
|
}
|
|
@@ -4181,7 +4359,10 @@ function narrowSlotReloads(
|
|
|
4181
4359
|
}
|
|
4182
4360
|
const width = ins.mnemonic === 'ldrb' ? 1 : ins.mnemonic === 'ldrh' ? 2 : 0;
|
|
4183
4361
|
const addr = ins.ops[1] === undefined ? null : parseAddr(ins.ops[1]);
|
|
4184
|
-
if (width === 0 ||
|
|
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)) {
|
|
4185
4366
|
return;
|
|
4186
4367
|
}
|
|
4187
4368
|
const slot = from + addr.off - (littleEndian ? 0 : 4 - width);
|
|
@@ -4638,7 +4819,14 @@ const ALL_REGS = ['r0', 'r1', 'r2', 'r3', 'r4', 'r5', 'r6', 'r7', 'r8', 'r9', 'r
|
|
|
4638
4819
|
function bodySavedRegs(
|
|
4639
4820
|
blocks: readonly AsmBlock[],
|
|
4640
4821
|
preds: readonly (readonly number[])[],
|
|
4641
|
-
|
|
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
|
+
} {
|
|
4642
4830
|
// What each instruction saves, restores and otherwise names; the staging `mov`s first.
|
|
4643
4831
|
const savesHi = new Map<Instr, string>(); // `mov rLow, rHi` ⇒ rHi, where rLow is pushed unchanged
|
|
4644
4832
|
const restoresHi = new Set<Instr>(); // `mov rHi, rLow` where rLow was popped unchanged
|
|
@@ -4711,6 +4899,8 @@ function bodySavedRegs(
|
|
|
4711
4899
|
i === 0 ? { cur: new Set(), open: new Set(), depth: 0, stack: [], clobbered: new Map(), dirty: new Set() } : null,
|
|
4712
4900
|
);
|
|
4713
4901
|
const leaked = new Map<string, Instr>();
|
|
4902
|
+
const copied = new Map<string, Instr>();
|
|
4903
|
+
const returned = new Map<string, Instr>();
|
|
4714
4904
|
const word = (r: string) => canonReg(r) ?? r;
|
|
4715
4905
|
const walk = (b: number, record: boolean): State => {
|
|
4716
4906
|
const cur = new Set(stateIn[b]!.cur);
|
|
@@ -4721,7 +4911,7 @@ function bodySavedRegs(
|
|
|
4721
4911
|
let stack: (string | null)[] | null = stateIn[b]!.stack === null ? null : [...stateIn[b]!.stack!];
|
|
4722
4912
|
const holds = new Map<string, string>(); // low register ← the high one a save's staging `mov` copied
|
|
4723
4913
|
const loaded = new Map<string, string | null>(); // a staging pop's low register ← whose word it loaded
|
|
4724
|
-
for (const x of blocks[b].instrs) {
|
|
4914
|
+
for (const [i, x] of blocks[b].instrs.entries()) {
|
|
4725
4915
|
const before = frame.depthNow();
|
|
4726
4916
|
frame.step(x);
|
|
4727
4917
|
const after = frame.depthNow();
|
|
@@ -4815,6 +5005,13 @@ function bodySavedRegs(
|
|
|
4815
5005
|
cur.delete(r);
|
|
4816
5006
|
if (/^r([4-9]|1[01])$/.test(word(r))) {
|
|
4817
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
|
+
}
|
|
4818
5015
|
}
|
|
4819
5016
|
});
|
|
4820
5017
|
// Only a return the frame is balanced at: one with the `push` still on the stack is an
|
|
@@ -4871,9 +5068,79 @@ function bodySavedRegs(
|
|
|
4871
5068
|
saved: new Set([...savedSomewhere].filter((r) => !mis(r))),
|
|
4872
5069
|
misSaved: new Set([...savedSomewhere, ...restoredSomewhere].filter(mis)),
|
|
4873
5070
|
leaked,
|
|
5071
|
+
copied,
|
|
5072
|
+
returned,
|
|
4874
5073
|
};
|
|
4875
5074
|
}
|
|
4876
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;
|
|
5142
|
+
}
|
|
5143
|
+
|
|
4877
5144
|
/** How the fill reads and writes an operand: the guards that keep sp, pc and a data label from
|
|
4878
5145
|
* being read or written as a register, and the function-scope statics a pool word names. */
|
|
4879
5146
|
interface ThumbOperands {
|
|
@@ -5361,6 +5628,13 @@ interface ThumbFill {
|
|
|
5361
5628
|
/** Each callee-saved register a `pop` loads another word into and that reaches a balanced return
|
|
5362
5629
|
* so (`bodySavedRegs`'s `leaked`), with that pop: refused once the return proof has run. */
|
|
5363
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>;
|
|
5364
5638
|
}
|
|
5365
5639
|
|
|
5366
5640
|
/** EVERY STAGE BEFORE THE FILL, in their refusal order: the text, the CFG, the target's scratch
|
|
@@ -5378,7 +5652,21 @@ function thumbFillOf(
|
|
|
5378
5652
|
contextOwnWords,
|
|
5379
5653
|
contextOwnProto,
|
|
5380
5654
|
ownMemoryReturn,
|
|
5381
|
-
|
|
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,
|
|
5382
5670
|
): ThumbFill {
|
|
5383
5671
|
const text = readThumbText(name, asm, target.compilerBehaviors.farJumpRelay?.comment);
|
|
5384
5672
|
const cfg = thumbCfg(name, text.blocks, text.tables);
|
|
@@ -5392,6 +5680,9 @@ function thumbFillOf(
|
|
|
5392
5680
|
throw new FrontendUnsupportedError(message);
|
|
5393
5681
|
},
|
|
5394
5682
|
() => ownDeclarationOf(name, prototypes, contextOwnProto),
|
|
5683
|
+
calleeBodyBound,
|
|
5684
|
+
entryReadCalls,
|
|
5685
|
+
stackSized,
|
|
5395
5686
|
);
|
|
5396
5687
|
const frame = measureThumbFrame({
|
|
5397
5688
|
name,
|
|
@@ -5399,7 +5690,13 @@ function thumbFillOf(
|
|
|
5399
5690
|
cfg,
|
|
5400
5691
|
calls,
|
|
5401
5692
|
oneObject,
|
|
5693
|
+
homesReleased,
|
|
5402
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,
|
|
5403
5700
|
});
|
|
5404
5701
|
|
|
5405
5702
|
// --- ISA-neutral SSA construction (shared Braun builder) ---
|
|
@@ -5416,7 +5713,7 @@ function thumbFillOf(
|
|
|
5416
5713
|
// a two-parameter one.
|
|
5417
5714
|
// A prologue save that some path uses past its restore, or carries to a return unrestored, is
|
|
5418
5715
|
// `misSaved` and no save: the prologue's reading of it sees only the `push`.
|
|
5419
|
-
const body = bodySavedRegs(cfg.asmBlocks, cfg.preds);
|
|
5716
|
+
const body = bodySavedRegs(cfg.asmBlocks, cfg.preds, cfg.succs);
|
|
5420
5717
|
const liveIns = registerLiveIns(
|
|
5421
5718
|
target,
|
|
5422
5719
|
new Set([...frame.savedRegs, ...body.saved].filter((r) => !body.misSaved.has(r))),
|
|
@@ -5431,6 +5728,21 @@ function thumbFillOf(
|
|
|
5431
5728
|
undefined,
|
|
5432
5729
|
liveIns.globalRegs,
|
|
5433
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
|
+
);
|
|
5434
5746
|
const statics = makeLocalStatics();
|
|
5435
5747
|
const operands = thumbOperands({
|
|
5436
5748
|
name,
|
|
@@ -5471,6 +5783,8 @@ function thumbFillOf(
|
|
|
5471
5783
|
staleIdiomValues: new Map(),
|
|
5472
5784
|
caseEntries: elidedCaseEntries(cfg, text.tables),
|
|
5473
5785
|
poppedForeign: body.leaked,
|
|
5786
|
+
poppedCopies: body.copied,
|
|
5787
|
+
poppedReturns: body.returned,
|
|
5474
5788
|
};
|
|
5475
5789
|
}
|
|
5476
5790
|
|
|
@@ -5999,6 +6313,7 @@ function lowerMov(fill: ThumbFill, cur: BlockCursor, ins: Instr): void {
|
|
|
5999
6313
|
if (frame.slotsOk && frame.localArea > 0) {
|
|
6000
6314
|
// a copy only a narrow slot reload reads names no object; the reload reads the slot
|
|
6001
6315
|
if (frame.narrowReloadCopies.has(ins)) {
|
|
6316
|
+
writeNarrowCopyMarker(fill, cur, reg(a));
|
|
6002
6317
|
return;
|
|
6003
6318
|
}
|
|
6004
6319
|
const res = mkValue(T.unk(32));
|
|
@@ -6012,6 +6327,18 @@ function lowerMov(fill: ThumbFill, cur: BlockCursor, ins: Instr): void {
|
|
|
6012
6327
|
writeData(reg(a), bi, v);
|
|
6013
6328
|
}
|
|
6014
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
|
+
|
|
6015
6342
|
/** `add`: a frame adjustment, a 64-bit pair with the `adc` after it, a frame capture,
|
|
6016
6343
|
* the copy idiom, or an add. */
|
|
6017
6344
|
function lowerAdd(fill: ThumbFill, cur: BlockCursor, ins: Instr, ii: number): void {
|
|
@@ -6036,7 +6363,8 @@ function lowerAdd(fill: ThumbFill, cur: BlockCursor, ins: Instr, ii: number): vo
|
|
|
6036
6363
|
if (isSpReg(b ?? '') && !isSpReg(a ?? '') && c !== undefined && IMM_LITERAL.test(c)) {
|
|
6037
6364
|
if (frame.slotsOk && frame.localArea > 0) {
|
|
6038
6365
|
if (frame.narrowReloadCopies.has(ins)) {
|
|
6039
|
-
|
|
6366
|
+
writeNarrowCopyMarker(fill, cur, reg(a)); // see lowerMov
|
|
6367
|
+
return;
|
|
6040
6368
|
}
|
|
6041
6369
|
const res = mkValue(T.unk(32));
|
|
6042
6370
|
irb.ops.push(mkOp('laddr', { results: [res], attrs: { off: imm(c) } }));
|
|
@@ -6287,7 +6615,16 @@ function lowerLoad(fill: ThumbFill, cur: BlockCursor, ins: Instr): void {
|
|
|
6287
6615
|
irb.ops.push(
|
|
6288
6616
|
mkOp('gaddr', {
|
|
6289
6617
|
results: [res],
|
|
6290
|
-
|
|
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
|
+
},
|
|
6291
6628
|
}),
|
|
6292
6629
|
);
|
|
6293
6630
|
writeData(reg(a), bi, res);
|
|
@@ -6304,7 +6641,12 @@ function lowerLoad(fill: ThumbFill, cur: BlockCursor, ins: Instr): void {
|
|
|
6304
6641
|
const g = mkValue(T.unk(32));
|
|
6305
6642
|
const k = mkValue(T.unk(32));
|
|
6306
6643
|
const res = mkValue(T.unk(32));
|
|
6307
|
-
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
|
+
);
|
|
6308
6650
|
irb.ops.push(mkOp('const', { results: [k], attrs: { value: interior.offset } }));
|
|
6309
6651
|
irb.ops.push(mkOp('add', { operands: [g, k], results: [res] }));
|
|
6310
6652
|
writeData(reg(a), bi, res);
|
|
@@ -6541,6 +6883,8 @@ function lowerCall(fill: ThumbFill, cur: BlockCursor, ins: Instr): void {
|
|
|
6541
6883
|
usedSlotOffsets.add(off);
|
|
6542
6884
|
return ssa.readVar(stackSlotKey(off), bi);
|
|
6543
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,
|
|
6544
6888
|
// A FRAME ADDRESS AN UNDECLARED ARITY LEAVES OUT IS AN ARGUMENT ALL THE SAME. The frame walks
|
|
6545
6889
|
// read the call as handed every argument register it holds (`frameAddressReceivers`), and the
|
|
6546
6890
|
// lift reads the object as written by that receiver (`isFrameObjectAccess`, with no reaching-
|
|
@@ -6735,7 +7079,12 @@ function fallLabel(asmBlocks: readonly AsmBlock[], name: string, bi: number): st
|
|
|
6735
7079
|
}
|
|
6736
7080
|
|
|
6737
7081
|
/** The ARMv4T / Thumb (agbcc) frontend, registered for the `armv4t` target. */
|
|
6738
|
-
export const thumbFrontend: Frontend = {
|
|
7082
|
+
export const thumbFrontend: Frontend = {
|
|
7083
|
+
id: 'thumb',
|
|
7084
|
+
inputFormat: 'gnu-as',
|
|
7085
|
+
lift,
|
|
7086
|
+
entryStackWords: thumbEntryWordsFromText,
|
|
7087
|
+
};
|
|
6739
7088
|
|
|
6740
7089
|
/** Internal surface for this module's own tests, and for nothing else. `@asmlift/core` exports
|
|
6741
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
|