@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/frontend.ts
CHANGED
|
@@ -8,6 +8,7 @@ import type { ContextGlobals } from '../proto-context';
|
|
|
8
8
|
import type { SymbolMap } from '../symbols';
|
|
9
9
|
import type { TargetDescription } from '../target';
|
|
10
10
|
import type { AsmData } from './asmdata';
|
|
11
|
+
import type { CalleeBodyBound, CalleeStackWords, EntryStackWords } from './callee-body';
|
|
11
12
|
import type { AsmTextFormat } from './format';
|
|
12
13
|
|
|
13
14
|
export interface Frontend {
|
|
@@ -34,6 +35,10 @@ export interface Frontend {
|
|
|
34
35
|
context?: ContextGlobals,
|
|
35
36
|
options?: LiftOptions,
|
|
36
37
|
): Fn;
|
|
38
|
+
/** how many incoming stack words the straight-line entry of a callee, in a text of this
|
|
39
|
+
* frontend's input format, loads (frontend/callee-body.ts `CalleeStackWords`), asked of a callee
|
|
40
|
+
* whose body does not lift; absent where this frontend reads no entry */
|
|
41
|
+
entryStackWords?: EntryStackWords;
|
|
37
42
|
}
|
|
38
43
|
|
|
39
44
|
/** THE FUNCTION'S OWN DECLARATION, as a call lowering reads it (frontend/call-plan.ts
|
|
@@ -90,4 +95,19 @@ export interface LiftOptions {
|
|
|
90
95
|
/** Called when the lift met such a slot, whichever way it answered: what rank.ts asks before
|
|
91
96
|
* enumerating `/kept-slot` at all. */
|
|
92
97
|
onTailSlot?: () => void;
|
|
98
|
+
/** The argument words a callee's own body in the same input can read at most
|
|
99
|
+
* (frontend/callee-body.ts), asked of a call whose arity no declaration states. pipeline.ts
|
|
100
|
+
* `liftStamped` supplies it on every lift; absent, nothing is bounded. */
|
|
101
|
+
calleeBodyBound?: CalleeBodyBound;
|
|
102
|
+
/** The incoming stack words a callee's own body, in the input or handed in, is proven to load
|
|
103
|
+
* (frontend/callee-body.ts `CalleeStackWords`), asked of a call no declaration lays out that
|
|
104
|
+
* stages words in the outgoing area. pipeline.ts `liftStamped` supplies it on every lift. */
|
|
105
|
+
calleeStackWords?: CalleeStackWords;
|
|
106
|
+
/** Told of each call nothing declares that the lift laid out at every argument register and the
|
|
107
|
+
* stack words its callee's body loads (`calleeStackWords`), with that word count. */
|
|
108
|
+
stackSized?: (callee: string, words: number) => void;
|
|
109
|
+
/** The callees whose calls are taken at every argument register that may still hold this
|
|
110
|
+
* function's own incoming value at the call, where no declaration lays the call out: how
|
|
111
|
+
* frontend/callee-body.ts lifts a callee's body to bound what it reads. Absent, none is. */
|
|
112
|
+
entryReadCalls?: ReadonlySet<string>;
|
|
93
113
|
}
|
|
@@ -215,11 +215,12 @@ export interface ElidedDispatch {
|
|
|
215
215
|
|
|
216
216
|
/** A MARKER ({@link staleMarker}): what the idiom left in `reg` that no C expression spells, and
|
|
217
217
|
* whether the dispatch that left it was kept (its block lifted, holding the code the compiler
|
|
218
|
-
* placed in it) or elided
|
|
218
|
+
* placed in it) or elided — or, `narrow`, the copy of sp a Thumb narrow slot reload read through,
|
|
219
|
+
* which is not lifted either. A read of one declines ({@link refuseStaleIdiomReads}). */
|
|
219
220
|
export interface StaleMarker {
|
|
220
221
|
kind: 'marker';
|
|
221
222
|
reg: string;
|
|
222
|
-
form: 'kept' | 'elided';
|
|
223
|
+
form: 'kept' | 'elided' | 'narrow';
|
|
223
224
|
/** Whether the marker stands for the register on EVERY path that brings it ({@link parkedIn}): a
|
|
224
225
|
* kept dispatch's, and an EXACT target's. A MIXED target's marker also replaced what the other
|
|
225
226
|
* paths into it brought, so it is read like any value, and its read declines. */
|
|
@@ -388,7 +389,10 @@ export const scaledIndexIn =
|
|
|
388
389
|
* address, none of which a C expression spells. Run on the filled SSA, before `finish` collapses a
|
|
389
390
|
* join of an undefined value into its other argument: a marker some operand demands, directly or
|
|
390
391
|
* through the joins that carry it, declines. A `ret` operand demands nothing under a declared
|
|
391
|
-
* `void` return, which drops it (pipeline.ts `dropVoidReturnOperands`). A
|
|
392
|
+
* `void` return, which drops it (pipeline.ts `dropVoidReturnOperands`). A narrow reload's marker
|
|
393
|
+
* (form `narrow`) is held to the drop itself, `dropsVoidReturn`: pipeline.ts drops the operand only
|
|
394
|
+
* for a STATED void entry, and under the context's own `void` alone the lifted `ret` still reads
|
|
395
|
+
* the register, so the marker would print as `return uninit_r0`. A MIXED target's scaled
|
|
392
396
|
* index is settled first ({@link unsettledScaledIndices}): one that does not stand is a marker,
|
|
393
397
|
* read or not. `fail` is the frontend's own refusal. Last, `calls` refuses a guessed arity's
|
|
394
398
|
* trailing pass-through where the function lifts only through this modelling: a parked ceiling that
|
|
@@ -402,6 +406,7 @@ export function refuseStaleIdiomReads(
|
|
|
402
406
|
own: FnProto | undefined,
|
|
403
407
|
fail: FrontendRefusal,
|
|
404
408
|
calls: Pick<CallDeclarations, 'refuseDispatchUnlockedPassThrough'>,
|
|
409
|
+
dropsVoidReturn: boolean = declaresVoidReturn(own),
|
|
405
410
|
): void {
|
|
406
411
|
if (stale.size === 0) {
|
|
407
412
|
return;
|
|
@@ -418,32 +423,42 @@ export function refuseStaleIdiomReads(
|
|
|
418
423
|
}
|
|
419
424
|
}
|
|
420
425
|
const unsettled = unsettledScaledIndices(irBlocks, stale, incoming, ssa);
|
|
421
|
-
|
|
422
|
-
const
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
426
|
+
// what the ops demand, a void return's operand aside; then what that operand alone demands
|
|
427
|
+
const demandedBy = (roots: (op: Op) => boolean): Set<Value> => {
|
|
428
|
+
const demanded = new Set<Value>();
|
|
429
|
+
const work: Value[] = [];
|
|
430
|
+
const demand = (v: Value) => {
|
|
431
|
+
if (!demanded.has(v)) {
|
|
432
|
+
demanded.add(v);
|
|
433
|
+
work.push(v);
|
|
434
|
+
}
|
|
435
|
+
};
|
|
436
|
+
irBlocks.forEach((blk) => blk.ops.filter(roots).forEach((op) => op.operands.forEach(demand)));
|
|
437
|
+
while (work.length > 0) {
|
|
438
|
+
(incoming.get(work.pop()!) ?? []).forEach(demand);
|
|
427
439
|
}
|
|
440
|
+
return demanded;
|
|
428
441
|
};
|
|
429
442
|
const voidReturn = declaresVoidReturn(own);
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
}
|
|
436
|
-
}
|
|
437
|
-
while (work.length > 0) {
|
|
438
|
-
(incoming.get(work.pop()!) ?? []).forEach(demand);
|
|
439
|
-
}
|
|
443
|
+
const demanded = demandedBy((op) => !(op.opcode === 'ret' && voidReturn));
|
|
444
|
+
const keptRet =
|
|
445
|
+
voidReturn && !dropsVoidReturn && [...stale.values()].some((s) => s.kind === 'marker' && s.form === 'narrow')
|
|
446
|
+
? demandedBy((op) => op.opcode === 'ret')
|
|
447
|
+
: new Set<Value>();
|
|
440
448
|
const read = [...stale].flatMap(([v, s]) =>
|
|
441
|
-
demanded.has(v)
|
|
449
|
+
(demanded.has(v) || (s.kind === 'marker' && s.form === 'narrow' && keptRet.has(v))) &&
|
|
450
|
+
(s.kind === 'marker' || unsettled.has(v))
|
|
442
451
|
? [s.kind === 'marker' ? s : { reg: s.reg, form: 'elided' as const }]
|
|
443
452
|
: [],
|
|
444
453
|
)[0];
|
|
445
454
|
if (read !== undefined) {
|
|
446
455
|
const { reg, form } = read;
|
|
456
|
+
if (form === 'narrow') {
|
|
457
|
+
fail(
|
|
458
|
+
`cannot lift '${name}': address-taken stack local — ${reg} is read past the narrow slot reload that read ` +
|
|
459
|
+
'through it, and the lift does not model the copy of sp it holds there',
|
|
460
|
+
);
|
|
461
|
+
}
|
|
447
462
|
const why =
|
|
448
463
|
"which the unlifted dispatch idiom wrote — the lift would read the value from before the dispatch, not the table's " +
|
|
449
464
|
'index, address or target';
|
package/src/frontend/mips.ts
CHANGED
|
@@ -28,8 +28,9 @@ import { type Prototypes, declaresVoidReturn, ownReturnWidth, wordsOf } from '..
|
|
|
28
28
|
import type { ContextGlobals } from '../proto-context';
|
|
29
29
|
import type { SymbolMap } from '../symbols';
|
|
30
30
|
import { O32_REGISTER_ALIASES, type TargetDescription } from '../target';
|
|
31
|
-
import { type AsmData, readJumpTable, textRelocAt } from './asmdata';
|
|
31
|
+
import { type AsmData, readJumpTable, readU32, textRelocAt } from './asmdata';
|
|
32
32
|
import { callDeclarations, foldByValueArgs } from './call-plan';
|
|
33
|
+
import { asksCalleeBody } from './callee-body';
|
|
33
34
|
import { pinDeadReads } from './dead-reads';
|
|
34
35
|
import { pinDeviceAccesses } from './device-pins';
|
|
35
36
|
import {
|
|
@@ -61,6 +62,7 @@ import {
|
|
|
61
62
|
type FrameObjectRelift,
|
|
62
63
|
type FrameRange,
|
|
63
64
|
areaAboveOutgoing,
|
|
65
|
+
keptInRelift,
|
|
64
66
|
landsOnArea,
|
|
65
67
|
liftWithRelift,
|
|
66
68
|
nameMergedSretObjects,
|
|
@@ -94,7 +96,14 @@ import {
|
|
|
94
96
|
stackArgSlots,
|
|
95
97
|
stackSlotKey,
|
|
96
98
|
} from './ssa';
|
|
97
|
-
import {
|
|
99
|
+
import {
|
|
100
|
+
makeSsaBuilder,
|
|
101
|
+
recordAsmLayout,
|
|
102
|
+
refuseSavedRegisterLeaks,
|
|
103
|
+
registerLiveIns,
|
|
104
|
+
unifyRegisterSpellings,
|
|
105
|
+
} from './ssa';
|
|
106
|
+
import { bodyBlockAgrees } from './stackargs';
|
|
98
107
|
|
|
99
108
|
type Instr = DisasmInstr;
|
|
100
109
|
|
|
@@ -313,8 +322,31 @@ function attachMipsRelocs(name: string, instrs: Instr[], ad: AsmData, freed: Rea
|
|
|
313
322
|
`0x${ins.addr.toString(16)}): '${ins.reloc.type} ${ins.reloc.sym}' and '${r.type} ${r.sym}'`,
|
|
314
323
|
);
|
|
315
324
|
}
|
|
316
|
-
ins.reloc = {
|
|
325
|
+
ins.reloc = {
|
|
326
|
+
type: r.type,
|
|
327
|
+
sym: ins.mnemonic === 'jal' ? (textCallee(r.sym, r.offset, ad) ?? r.sym) : r.sym,
|
|
328
|
+
addend: 0,
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/** THE FUNCTION A `jal` RELOCATED AGAINST `.text` ITSELF ENTERS, by name. gcc 2.7.2 and KMC relocate a
|
|
334
|
+
* call to a function of the same object against the section, not the function's symbol (`jal 0
|
|
335
|
+
* <rnd>` over `R_MIPS_26 .text`), so the record names no callee. MIPS objects are REL: the jump's
|
|
336
|
+
* 26-bit field holds the addend, the target's offset in the section shifted right by two. The
|
|
337
|
+
* symbol table names the function at that offset. Undefined, and the call keeps its section
|
|
338
|
+
* callee, which refuses: where `.text`'s bytes or the word are not in the dump, or where no one
|
|
339
|
+
* name, or more than one, starts there. Only a `jal`'s: gcc 2.7.2 and KMC relocate a `j` against
|
|
340
|
+
* `.text` too, and a leaf loop whose head is the entry jumps to its own function's first word, so
|
|
341
|
+
* naming that `j` would read the loop's back edge as a tail call. */
|
|
342
|
+
function textCallee(sym: string, offset: number, ad: AsmData): string | undefined {
|
|
343
|
+
const text = ad.sections.get('.text');
|
|
344
|
+
if (sym !== '.text' || text === undefined || offset + 4 > text.length) {
|
|
345
|
+
return undefined;
|
|
317
346
|
}
|
|
347
|
+
const at = (readU32(text, offset, ad.bigEndian) & 0x03ffffff) * 4;
|
|
348
|
+
const names = [...ad.symbols].filter(([n, s]) => s.section === '.text' && s.value === at && /^[A-Za-z_]\w*$/.test(n));
|
|
349
|
+
return names.length === 1 ? names[0][0] : undefined;
|
|
318
350
|
}
|
|
319
351
|
|
|
320
352
|
interface MipsBlock {
|
|
@@ -1628,6 +1660,16 @@ function liftOnce(
|
|
|
1628
1660
|
liveIns.globalRegs,
|
|
1629
1661
|
);
|
|
1630
1662
|
const { irBlocks, readVar, writeVar, paramReg } = ssa;
|
|
1663
|
+
// Where the compiler put each block (ir/core.ts `AsmLayout`). A delay slot runs in its branch's
|
|
1664
|
+
// block, so it is part of the body; a branch-likely's slot is a block of its own already.
|
|
1665
|
+
ssa.fn.asmLayout = recordAsmLayout(
|
|
1666
|
+
irBlocks,
|
|
1667
|
+
blocks.map((b) => ({
|
|
1668
|
+
body: b.delay ? [...b.body, b.delay] : b.body,
|
|
1669
|
+
endsInJump: b.branch !== null && isUncond(b.branch),
|
|
1670
|
+
isCall: (ins) => isCallInstr(ins as Instr),
|
|
1671
|
+
})),
|
|
1672
|
+
);
|
|
1631
1673
|
const RET = target.returnReg;
|
|
1632
1674
|
// Whether this function's own declaration says its return fits one word (`v1MayHoldLowWord`):
|
|
1633
1675
|
// void, or a scalar or pointer no wider than 32 bits. An aggregate says nothing here. A declared
|
|
@@ -1717,6 +1759,9 @@ function liftOnce(
|
|
|
1717
1759
|
{ pairs: false, memoryReturn: true, stackArgs: true, voidReturn: true, argRegisterBoundsArity: false },
|
|
1718
1760
|
fail,
|
|
1719
1761
|
() => ownDeclarationOf(name, prototypes, options.contextOwnProto),
|
|
1762
|
+
options.calleeBodyBound,
|
|
1763
|
+
options.entryReadCalls,
|
|
1764
|
+
options.stackSized,
|
|
1720
1765
|
);
|
|
1721
1766
|
// The first instruction naming the FPU's file, a data register or the control register. A call
|
|
1722
1767
|
// in a function that has one refuses: which floating-point registers a callee reads, returns in
|
|
@@ -1767,6 +1812,63 @@ function liftOnce(
|
|
|
1767
1812
|
// epilogue restore in any block.
|
|
1768
1813
|
const saveSlots = new Map<number, string>();
|
|
1769
1814
|
const valueSlots = new Set<number>();
|
|
1815
|
+
// WHETHER EACH OUTGOING-AREA WORD IS PENDING, per block, as lowered so far: a store to it
|
|
1816
|
+
// (`emitStore`) makes it pending, and a call that took it as a stack argument (`lowerJal`'s
|
|
1817
|
+
// `stackWord`) ends it, so it is pending for no later call until it is stored again. Kept per
|
|
1818
|
+
// STORE, not per value: a value one call took and that is stored again for a later call, at the
|
|
1819
|
+
// same word or another, is staged for that call all the same, and keyed by the value it would
|
|
1820
|
+
// hide a word the call is handed (frontend/stackargs.ts ends its pending words per offset too).
|
|
1821
|
+
const outgoingWordState: Map<number, 'stored' | 'taken'>[] = blocks.map(() => new Map());
|
|
1822
|
+
// Whether the state of word `off` some path brings into this point of block `b` is `want`: the
|
|
1823
|
+
// walk `ssa.hasReachingDef` makes, over this state, so a block not filled yet is passed through.
|
|
1824
|
+
// `here` is `b`'s state at this point (its state so far, by default); met again round a loop, `b`
|
|
1825
|
+
// is its state at its end, which a back edge brings.
|
|
1826
|
+
const outgoingWordReaches = (
|
|
1827
|
+
off: number,
|
|
1828
|
+
b: number,
|
|
1829
|
+
want: 'stored' | 'taken',
|
|
1830
|
+
here: ReadonlyMap<number, 'stored' | 'taken'> = outgoingWordState[b],
|
|
1831
|
+
): boolean => {
|
|
1832
|
+
const seen = new Set<number>();
|
|
1833
|
+
const walk = (at: number, own: 'stored' | 'taken' | undefined): boolean => {
|
|
1834
|
+
if (own !== undefined) {
|
|
1835
|
+
return own === want;
|
|
1836
|
+
}
|
|
1837
|
+
if (seen.has(at)) {
|
|
1838
|
+
return false;
|
|
1839
|
+
}
|
|
1840
|
+
seen.add(at);
|
|
1841
|
+
return preds[at].some((p) => walk(p, outgoingWordState[p].get(off)));
|
|
1842
|
+
};
|
|
1843
|
+
return walk(b, here.get(off));
|
|
1844
|
+
};
|
|
1845
|
+
// EACH CALL ITS CALLEE'S BODY LICENSED, judged again once every block is filled: while a call is
|
|
1846
|
+
// lowered, a block not filled yet — a loop's latch, or the call's own block met round a back
|
|
1847
|
+
// edge — holds no state, so a word the call itself takes is not seen reaching its loop head and a
|
|
1848
|
+
// word staged once before the loop passes for every iteration's. Judged again on the finished
|
|
1849
|
+
// state (frontend/stackargs.ts reaches the same answer by its fixpoint), a word of the block some
|
|
1850
|
+
// path brings in taken, or a word past it some path brings in stored, refuses the lift.
|
|
1851
|
+
const bodyLicences = new Map<
|
|
1852
|
+
Instr,
|
|
1853
|
+
{ bi: number; here: ReadonlyMap<number, 'stored' | 'taken'>; n: number; callee: string }
|
|
1854
|
+
>();
|
|
1855
|
+
// EACH CALL A DECLARATION SIZED, AND THE STACK WORDS IT TOOK, judged again the same way: its
|
|
1856
|
+
// arity is the declaration's, so a word past its block says nothing, but a word it takes that
|
|
1857
|
+
// some path brings in already taken by an earlier call — licensed or declared — is a word the
|
|
1858
|
+
// earlier callee owned and may have written, not the value stored there (frontend/stackargs.ts
|
|
1859
|
+
// ends a pending word at the call that takes it for either kind of call).
|
|
1860
|
+
const declaredTakes = new Map<
|
|
1861
|
+
Instr,
|
|
1862
|
+
{ bi: number; here: ReadonlyMap<number, 'stored' | 'taken'>; offs: number[]; callee: string }
|
|
1863
|
+
>();
|
|
1864
|
+
const refuseTakenWord = (callee: string, addr: number, off: number): never => {
|
|
1865
|
+
throw new FrontendUnsupportedError(
|
|
1866
|
+
`cannot lift '${name}': outgoing stack arguments not modelled — the call to '${callee}' at ` +
|
|
1867
|
+
`0x${addr.toString(16)} takes the word at ${off}(sp) as a stack argument, but it reaches the call on ` +
|
|
1868
|
+
'some path already taken by an earlier call and not stored again, so what the earlier callee left there ' +
|
|
1869
|
+
'is not known',
|
|
1870
|
+
);
|
|
1871
|
+
};
|
|
1770
1872
|
const restores = new Map<Value, { reg: string; addr: number }>();
|
|
1771
1873
|
// Each `lw` that restores a save, and the register it restores: a save holds only while every path
|
|
1772
1874
|
// restores it after its last write (`refuseSavedRegisterLeaks`).
|
|
@@ -1863,7 +1965,12 @@ function liftOnce(
|
|
|
1863
1965
|
`local area, sp+${frame.area.from} up to sp+${frame.area.to} — part memory, part slot`,
|
|
1864
1966
|
);
|
|
1865
1967
|
}
|
|
1866
|
-
|
|
1968
|
+
// …and in a relift, only the bytes the first audit asked to keep as one object are memory: a
|
|
1969
|
+
// word of the area outside them is one it handed back to the slot model (`keptInRelift`)
|
|
1970
|
+
return (
|
|
1971
|
+
oneObject === undefined ||
|
|
1972
|
+
keptInRelift(oneObject, off, width, () => `'${ins.mnemonic}' at 0x${ins.addr.toString(16)}`, name)
|
|
1973
|
+
);
|
|
1867
1974
|
};
|
|
1868
1975
|
|
|
1869
1976
|
// SOUNDNESS GUARD. The word stack-slot model (emitLoad/emitStore) is safe ONLY when every
|
|
@@ -1905,11 +2012,21 @@ function liftOnce(
|
|
|
1905
2012
|
// must run at the frame's own depth, save the incoming stack words that are keyed `@sarg<k>`.
|
|
1906
2013
|
// The caller's home `[frameSize, frameSize + 16)` is in no range, so a def-less read there still
|
|
1907
2014
|
// refuses. Sub-word frame traffic turns the slot model off (`spSlotSafe`) and claims nothing.
|
|
2015
|
+
//
|
|
2016
|
+
// A RELIFT takes a frame address by construction, and claims the local area alone: there the
|
|
2017
|
+
// bytes kept as one object are memory and key no slot, and every word outside them is one the
|
|
2018
|
+
// first audit handed back (`reliftRange`) only where no escape is unbounded, no copy reaches it
|
|
2019
|
+
// and no index runs, so nothing but its own direct accesses reaches it. kmc homes `u32 r; if (i)
|
|
2020
|
+
// r = gA[9]; … if (i) gA[10] = r;` in such a word beside two copied arrays, and reads it on the
|
|
2021
|
+
// path that never stored it.
|
|
1908
2022
|
let ownedMemo: { range: { from: number; to: number } | undefined } | undefined;
|
|
1909
2023
|
const ownedFrame = (): { from: number; to: number } | undefined =>
|
|
1910
2024
|
(ownedMemo ??= { range: measureOwnedFrame() }).range;
|
|
1911
2025
|
const measureOwnedFrame = (): { from: number; to: number } | undefined => {
|
|
1912
|
-
if (frameSize === undefined || depthAt === null || !spSlotSafe
|
|
2026
|
+
if (frameSize === undefined || depthAt === null || !spSlotSafe) {
|
|
2027
|
+
return undefined;
|
|
2028
|
+
}
|
|
2029
|
+
if (oneObject === undefined ? code.some(takesFrameAddress) : frame === null) {
|
|
1913
2030
|
return undefined;
|
|
1914
2031
|
}
|
|
1915
2032
|
for (const ins of code) {
|
|
@@ -1935,7 +2052,9 @@ function liftOnce(
|
|
|
1935
2052
|
}
|
|
1936
2053
|
throw e;
|
|
1937
2054
|
}
|
|
1938
|
-
|
|
2055
|
+
const from = Math.max(outgoing.widest, oneObject === undefined || frame === null ? 0 : frame.area.from);
|
|
2056
|
+
const to = oneObject === undefined || frame === null ? frameSize : Math.min(frameSize, frame.area.to);
|
|
2057
|
+
return from < to ? { from, to } : undefined;
|
|
1939
2058
|
};
|
|
1940
2059
|
ownedFrameOf = ownedFrame;
|
|
1941
2060
|
|
|
@@ -1950,6 +2069,8 @@ function liftOnce(
|
|
|
1950
2069
|
// The frame offset each `laddr` this lift emitted names, and the deferred `sp + index` sums
|
|
1951
2070
|
// (`emitLaddr`, `materialize`) — function-scoped, since a register holding one crosses blocks.
|
|
1952
2071
|
const laddrAt = new Map<Value, number>();
|
|
2072
|
+
// …and of those, the ones a direct `off(sp)` access in the local area was lowered to, not a capture
|
|
2073
|
+
const directAccesses = new Set<Value>();
|
|
1953
2074
|
const frameSums = new Map<Value, FrameSum>();
|
|
1954
2075
|
// Every capture this lift moved into another (`addiu rD,rCapture,k` re-minted as the `laddr` of
|
|
1955
2076
|
// the sum, or `addu` deferring one into a `FrameSum`), handed to the audit as Thumb hands its
|
|
@@ -2714,7 +2835,9 @@ function liftOnce(
|
|
|
2714
2835
|
`cannot lift '${name}': address-taken stack local — '${ins.mnemonic}' at 0x${ins.addr.toString(16)} saves the caller's ${stored} inside the local area, a frame this does not describe`,
|
|
2715
2836
|
);
|
|
2716
2837
|
}
|
|
2717
|
-
|
|
2838
|
+
const at = emitLaddr(off);
|
|
2839
|
+
directAccesses.add(at);
|
|
2840
|
+
return at;
|
|
2718
2841
|
}
|
|
2719
2842
|
const raw = rawFrame(base);
|
|
2720
2843
|
const sum = raw === undefined ? undefined : frameSums.get(raw);
|
|
@@ -2845,7 +2968,11 @@ function liftOnce(
|
|
|
2845
2968
|
refuseMixedSlot(ins, mem, off, 'value');
|
|
2846
2969
|
valueSlots.add(off);
|
|
2847
2970
|
// an assignment to an incoming stack parameter is that parameter's SSA variable
|
|
2848
|
-
|
|
2971
|
+
const incoming = incomingStackKey(ins, off);
|
|
2972
|
+
writeVar(incoming ?? stackSlot(off), bi, read(srcReg));
|
|
2973
|
+
if (incoming === undefined) {
|
|
2974
|
+
outgoingWordState[bi].set(off, 'stored');
|
|
2975
|
+
}
|
|
2849
2976
|
return;
|
|
2850
2977
|
}
|
|
2851
2978
|
ops.push(mkOp('store', { operands: [read(base), read(srcReg)], attrs: { off, width } }));
|
|
@@ -2857,6 +2984,8 @@ function liftOnce(
|
|
|
2857
2984
|
// argument k ≥ 4 lives, and the guess that cannot see past a3.
|
|
2858
2985
|
const lowerJal = (ins: Instr) => {
|
|
2859
2986
|
const at = `0x${ins.addr.toString(16)}`;
|
|
2987
|
+
// this block's outgoing words as the call is reached, before it takes any
|
|
2988
|
+
const wordsHere = new Map(outgoingWordState[bi]);
|
|
2860
2989
|
const callee = jalCallee(ins, splat);
|
|
2861
2990
|
if (callee?.startsWith('.')) {
|
|
2862
2991
|
throw new FrontendUnsupportedError(
|
|
@@ -2883,12 +3012,64 @@ function liftOnce(
|
|
|
2883
3012
|
'where its outgoing stack arguments end is not known',
|
|
2884
3013
|
);
|
|
2885
3014
|
}
|
|
3015
|
+
// A CALL NOTHING DECLARES TAKES THE WORDS ITS CALLEE'S BODY LOADS, where exactly those reach
|
|
3016
|
+
// it staged (frontend/stackargs.ts, "THE CALLEE'S BODY": the same rule, `bodyBlockAgrees`,
|
|
3017
|
+
// asked here of SSA's reaching definitions, which O32 has when its calls are lowered):
|
|
3018
|
+
// the body loads incoming word n-1, so the call takes at least n stack words, and a word past
|
|
3019
|
+
// them that nothing staged is no argument. Licensed only where the words `[16, 16+4n)` are
|
|
3020
|
+
// written on every path to the call beneath the loaded-word bound and inside the frame, and
|
|
3021
|
+
// no other word of the area there reaches it; asked only once a word does. KNOWN GAP: a
|
|
3022
|
+
// frame with a local area (`frame`) is not licensed, since that area is partitioned off the
|
|
3023
|
+
// DECLARED widest call before any call is lowered (`stackWordsOf` above), so such a call keeps
|
|
3024
|
+
// the refusals below.
|
|
3025
|
+
// A word an earlier call took as its stack argument is pending for no later one until it is
|
|
3026
|
+
// stored again (`outgoingWordState`), as frontend/stackargs.ts's dataflow ends it at the call
|
|
3027
|
+
// whose block held it. A block not filled yet holds no state, so each licence is judged again
|
|
3028
|
+
// once every block is (`bodyLicences`).
|
|
3029
|
+
const licensedStackWords = (): number | undefined => {
|
|
3030
|
+
if (frame !== null || !asksCalleeBody(Object.hasOwn(prototypes, callee) ? prototypes[callee] : undefined)) {
|
|
3031
|
+
return undefined;
|
|
3032
|
+
}
|
|
3033
|
+
const reaching = [...valueSlots].filter(
|
|
3034
|
+
(off) =>
|
|
3035
|
+
off >= STACK_ARGS_AT &&
|
|
3036
|
+
off < frameSize &&
|
|
3037
|
+
off < loadedWordBound &&
|
|
3038
|
+
ssa.hasReachingDef(stackSlot(off), bi) &&
|
|
3039
|
+
outgoingWordReaches(off, bi, 'stored', wordsHere),
|
|
3040
|
+
);
|
|
3041
|
+
if (reaching.length === 0) {
|
|
3042
|
+
return undefined;
|
|
3043
|
+
}
|
|
3044
|
+
// O32 can skip an argument register for an 8-aligned argument, so the plan's count — every
|
|
3045
|
+
// register, then the block — holds only where each register is one this call sets up: a
|
|
3046
|
+
// register still at its entry value on some path may be the skipped word, and taken as an
|
|
3047
|
+
// argument it is a parameter this function does not have. (The callee's half of the same
|
|
3048
|
+
// proof is frontend/callee-body.ts `readsEveryArgReg`.)
|
|
3049
|
+
if (ARG_REGS.some((r) => !ssa.writtenOnEveryPath(r, bi, () => false))) {
|
|
3050
|
+
return undefined;
|
|
3051
|
+
}
|
|
3052
|
+
const n = options.calleeStackWords?.(callee) ?? 0;
|
|
3053
|
+
return bodyBlockAgrees(
|
|
3054
|
+
n,
|
|
3055
|
+
reaching.map((off) => off - STACK_ARGS_AT),
|
|
3056
|
+
(o) =>
|
|
3057
|
+
ssa.writtenOnEveryPath(stackSlot(STACK_ARGS_AT + o), bi, () => false) &&
|
|
3058
|
+
!outgoingWordReaches(STACK_ARGS_AT + o, bi, 'taken', wordsHere),
|
|
3059
|
+
)
|
|
3060
|
+
? (bodyLicences.set(ins, { bi, here: wordsHere, n, callee }), n)
|
|
3061
|
+
: undefined;
|
|
3062
|
+
};
|
|
3063
|
+
const took: number[] = [];
|
|
2886
3064
|
calls.lower({
|
|
2887
3065
|
callee,
|
|
2888
3066
|
ssa,
|
|
2889
3067
|
bi,
|
|
2890
3068
|
read,
|
|
2891
3069
|
write,
|
|
3070
|
+
get stackWords() {
|
|
3071
|
+
return licensedStackWords();
|
|
3072
|
+
},
|
|
2892
3073
|
// ARGUMENT WORD k ≥ 4 travels in `16+4(k-4)(sp)`, inside the one frame this function pushed
|
|
2893
3074
|
stackWord: (k) => {
|
|
2894
3075
|
const off = STACK_ARGS_AT + 4 * (k - ARG_REGS.length);
|
|
@@ -2904,6 +3085,13 @@ function liftOnce(
|
|
|
2904
3085
|
`${off}(sp), and no value stored there reaches the call`,
|
|
2905
3086
|
);
|
|
2906
3087
|
}
|
|
3088
|
+
// a word an earlier call took is the earlier callee's, not the value stored there, on the
|
|
3089
|
+
// blocks filled so far; the rest are judged once every block is (`declaredTakes`)
|
|
3090
|
+
if (outgoingWordReaches(off, bi, 'taken', wordsHere)) {
|
|
3091
|
+
refuseTakenWord(callee, ins.addr, off);
|
|
3092
|
+
}
|
|
3093
|
+
took.push(off);
|
|
3094
|
+
outgoingWordState[bi].set(off, 'taken');
|
|
2907
3095
|
return readVar(stackSlot(off), bi);
|
|
2908
3096
|
},
|
|
2909
3097
|
// a pending `%hi` half and a table base are no argument
|
|
@@ -2970,6 +3158,9 @@ function liftOnce(
|
|
|
2970
3158
|
},
|
|
2971
3159
|
},
|
|
2972
3160
|
});
|
|
3161
|
+
if (took.length > 0 && !bodyLicences.has(ins)) {
|
|
3162
|
+
declaredTakes.set(ins, { bi, here: wordsHere, offs: took, callee });
|
|
3163
|
+
}
|
|
2973
3164
|
// hi and lo are the callee's to overwrite
|
|
2974
3165
|
divState = null;
|
|
2975
3166
|
mulState = null;
|
|
@@ -3126,6 +3317,32 @@ function liftOnce(
|
|
|
3126
3317
|
fillBlock(b, bi);
|
|
3127
3318
|
ssa.markFilled(bi);
|
|
3128
3319
|
});
|
|
3320
|
+
for (const [ins, { bi, here, n, callee }] of bodyLicences) {
|
|
3321
|
+
for (const off of valueSlots) {
|
|
3322
|
+
if (off < STACK_ARGS_AT || off >= (frameSize ?? 0) || off >= loadedWordBound) {
|
|
3323
|
+
continue;
|
|
3324
|
+
}
|
|
3325
|
+
const taken = off < STACK_ARGS_AT + 4 * n;
|
|
3326
|
+
if (outgoingWordReaches(off, bi, taken ? 'taken' : 'stored', here)) {
|
|
3327
|
+
throw new FrontendUnsupportedError(
|
|
3328
|
+
`cannot lift '${name}': outgoing stack arguments not modelled — the call to '${callee}' at ` +
|
|
3329
|
+
`0x${ins.addr.toString(16)} takes the ${n} stack ` +
|
|
3330
|
+
`word(s) its callee's body loads, but ${
|
|
3331
|
+
taken
|
|
3332
|
+
? `the word at ${off}(sp) reaches it on some path already taken by a call and not stored again`
|
|
3333
|
+
: `the word at ${off}(sp), past them, reaches it stored on some path`
|
|
3334
|
+
} — the block staged for it is not the same on every path, so its arguments are not known`,
|
|
3335
|
+
);
|
|
3336
|
+
}
|
|
3337
|
+
}
|
|
3338
|
+
}
|
|
3339
|
+
for (const [ins, { bi, here, offs, callee }] of declaredTakes) {
|
|
3340
|
+
for (const off of offs) {
|
|
3341
|
+
if (outgoingWordReaches(off, bi, 'taken', here)) {
|
|
3342
|
+
refuseTakenWord(callee, ins.addr, off);
|
|
3343
|
+
}
|
|
3344
|
+
}
|
|
3345
|
+
}
|
|
3129
3346
|
refuseSavedRegisterLeaks(
|
|
3130
3347
|
name,
|
|
3131
3348
|
blocks.map((b) => ({
|
|
@@ -3197,6 +3414,7 @@ function liftOnce(
|
|
|
3197
3414
|
loadedWords: loadedFrom === undefined ? undefined : { from: loadedFrom, bound: loadedWordBound },
|
|
3198
3415
|
capturedObjectIsTheWholeFrame: false,
|
|
3199
3416
|
movedCaptures,
|
|
3417
|
+
directAccesses,
|
|
3200
3418
|
returnsWithoutHiddenPointer: calls.returnsWithoutHiddenPointer,
|
|
3201
3419
|
symbols,
|
|
3202
3420
|
target,
|
package/src/frontend/ppc.ts
CHANGED
|
@@ -70,6 +70,7 @@ import type { SymbolMap } from '../symbols';
|
|
|
70
70
|
import type { TargetDescription } from '../target';
|
|
71
71
|
import { type AsmData, readJumpTable } from './asmdata';
|
|
72
72
|
import { type CallDeclarations, type ParamClass, callDeclarations } from './call-plan';
|
|
73
|
+
import type { CalleeBodyBound } from './callee-body';
|
|
73
74
|
import {
|
|
74
75
|
type Declarations,
|
|
75
76
|
computedOnlyToReturn,
|
|
@@ -1335,6 +1336,8 @@ function liftPrelude(
|
|
|
1335
1336
|
prototypes: Prototypes,
|
|
1336
1337
|
jts: Map<number, PpcJT>,
|
|
1337
1338
|
ownDeclaration?: () => OwnDeclaration | undefined,
|
|
1339
|
+
calleeBodyBound?: CalleeBodyBound,
|
|
1340
|
+
entryReadCalls?: ReadonlySet<string>,
|
|
1338
1341
|
) {
|
|
1339
1342
|
const recoveredBctr = new Set([...jts.values()].map((j) => j.bctrAddr));
|
|
1340
1343
|
// TRUSTWORTHINESS: fail loud on an unmodelled control transfer rather than dropping it (which
|
|
@@ -1383,7 +1386,16 @@ function liftPrelude(
|
|
|
1383
1386
|
}
|
|
1384
1387
|
const r1At = r1Displacements(blocks, succIdx);
|
|
1385
1388
|
const saves = prologueSavedRegs(entryBody, target, r1At, blocks);
|
|
1386
|
-
const calls = callDeclarations(
|
|
1389
|
+
const calls = callDeclarations(
|
|
1390
|
+
name,
|
|
1391
|
+
target,
|
|
1392
|
+
prototypes,
|
|
1393
|
+
PPC_CALL_LOWERING,
|
|
1394
|
+
ppcRefusal,
|
|
1395
|
+
ownDeclaration,
|
|
1396
|
+
calleeBodyBound,
|
|
1397
|
+
entryReadCalls,
|
|
1398
|
+
);
|
|
1387
1399
|
// THE LOCAL AREA, where a function takes a frame address, reaches its frame below a word, or
|
|
1388
1400
|
// keeps a dead local there (`measurePpcFrame`, `compilerBehaviors.linkageLocalArea`): every r1
|
|
1389
1401
|
// access inside it, any width, is MEMORY through an `laddr` the shared frame-object audit then
|
|
@@ -1435,7 +1447,12 @@ function liftOnce(
|
|
|
1435
1447
|
asmData: AsmData | undefined,
|
|
1436
1448
|
declarations: Declarations,
|
|
1437
1449
|
oneObject: FrameRange | undefined,
|
|
1438
|
-
{
|
|
1450
|
+
{
|
|
1451
|
+
contextOwnWords,
|
|
1452
|
+
contextOwnProto,
|
|
1453
|
+
calleeBodyBound,
|
|
1454
|
+
entryReadCalls,
|
|
1455
|
+
}: Pick<LiftOptions, 'contextOwnWords' | 'contextOwnProto' | 'calleeBodyBound' | 'entryReadCalls'>,
|
|
1439
1456
|
): Fn | FrameObjectRelift {
|
|
1440
1457
|
assertInputFormat('ppc', 'objdump', asm);
|
|
1441
1458
|
const instrs = parseDisasm(sliceSymbol(asm, name)); // ONE function only — an absent symbol declines loud
|
|
@@ -1516,6 +1533,8 @@ function liftOnce(
|
|
|
1516
1533
|
prototypes,
|
|
1517
1534
|
jts,
|
|
1518
1535
|
() => (!ownDeclared ? undefined : ownDeclarationOf(name, prototypes, contextOwnProto)),
|
|
1536
|
+
calleeBodyBound,
|
|
1537
|
+
entryReadCalls,
|
|
1519
1538
|
);
|
|
1520
1539
|
const lrUnproven = linkRegisterProof(blocks, succIdx, r1At, target);
|
|
1521
1540
|
|
|
@@ -2770,6 +2789,11 @@ function liftOnce(
|
|
|
2770
2789
|
// of the parameter area (mwcc 2.3.3 builds `g(1, 2, 3, 4, 5, 6, 7, x)` with `long long x`
|
|
2771
2790
|
// that way). A single word there cannot be an argument after seven: the eighth word would
|
|
2772
2791
|
// have gone in r10.
|
|
2792
|
+
//
|
|
2793
|
+
// KNOWN GAP: the callee's body is not asked how many stack words it loads, as the
|
|
2794
|
+
// Thumb and O32 frontends ask it (frontend/callee-body.ts `CalleeStackWords`): this
|
|
2795
|
+
// frontend passes no stack word to any call, and `measurePpcFrame` refuses even a
|
|
2796
|
+
// declared one that takes some, so a body would have nothing to license here.
|
|
2773
2797
|
refuse: (argc) => {
|
|
2774
2798
|
if (argc < ARG_REGS.length - 1) {
|
|
2775
2799
|
return;
|