@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
|
@@ -0,0 +1,370 @@
|
|
|
1
|
+
// asmlift — what a callee whose body is in the same input, or handed in from outside it, can read
|
|
2
|
+
// of its arguments.
|
|
3
|
+
//
|
|
4
|
+
// A callee nobody declared is called at a guessed arity (frontend/call-plan.ts, `fallbackArgc`),
|
|
5
|
+
// and on most ABIs `g()` and `g(x)` compile to the same caller bytes whenever x is already in the
|
|
6
|
+
// first argument register — so the caller cannot decide it. Where the callee's own body is in the
|
|
7
|
+
// input, the body can: an argument it never reads is no argument it can tell from a missing one.
|
|
8
|
+
// That answers only a question about a register at or past what the body reads (frontend/ssa.ts
|
|
9
|
+
// `fallbackArgc`'s `bound`, and the dispatch pass-through gate in frontend/call-plan.ts). It never
|
|
10
|
+
// caps what the caller passes: a source call hands an unused parameter, or a K&R call extra
|
|
11
|
+
// arguments, all the same, and a register below the bound may be read.
|
|
12
|
+
//
|
|
13
|
+
// THE BOUND IS A LIFT OF THE CALLEE, NOT ITS LIFTED SIGNATURE. A callee's own lifted parameters
|
|
14
|
+
// rest on the same guess one level down: a forwarding thunk (`push {lr}; bl g; pop {r1}; bx r1`)
|
|
15
|
+
// lifts as `thk(void)` while g is undeclared, though it hands g every argument register it got. So
|
|
16
|
+
// every call inside the callee whose arity nothing lays out is taken at every argument register
|
|
17
|
+
// that may still hold the callee's own incoming value there (`LiftOptions.entryReadCalls`), and
|
|
18
|
+
// only then are the callee's entry parameters counted: an argument register or incoming stack word
|
|
19
|
+
// the body reads becomes one, and the frontend mints every argument slot below the highest one read
|
|
20
|
+
// (frontend/ssa.ts `mintArgSlotHoles`). That count is an UPPER bound on the argument words the
|
|
21
|
+
// callee reads. A register an earlier call destroyed on every path is not taken: it holds none of
|
|
22
|
+
// the callee's arguments, and a read of it would only make the lift refuse (`push {lr}; mov r0-r3,
|
|
23
|
+
// #0; bl g; bl h` would lose its bound so, though it reads none).
|
|
24
|
+
//
|
|
25
|
+
// A RETURN READS THE RETURN REGISTER. Where the target returns in its first argument register (ARM
|
|
26
|
+
// r0, PowerPC r3), a return that no path wrote that register for hands back the caller's first
|
|
27
|
+
// argument: `void *id(void *p) { return p; }` is a lone `blr` and `bx lr`, which no instruction reads,
|
|
28
|
+
// and its lift returns nothing and counts no parameter. So a return the lift left bare, on a path
|
|
29
|
+
// from the entry that makes no call (a call destroys the register), counts the first argument word
|
|
30
|
+
// as read, unless the callee's entry declares a void return. KNOWN GAP: a return in a register PAIR
|
|
31
|
+
// (r0:r1, r3:r4) that nothing declares is counted for its first register only.
|
|
32
|
+
//
|
|
33
|
+
// It answers nothing where:
|
|
34
|
+
// - the callee is the function being lifted (its own declaration is a different question);
|
|
35
|
+
// - the text the body is read from (the input, or a body handed in) defines no label of that
|
|
36
|
+
// name: a `name:` line, an objdump `<name>:` header or a `glabel name` (the Thumb frontend lifts
|
|
37
|
+
// a text with no such label whole, as one function, so a lift alone does not say the callee is
|
|
38
|
+
// here);
|
|
39
|
+
// - that text marks the callee `.weak`, which another definition may replace at link time;
|
|
40
|
+
// - the callee does not lift, inside or outside the input (a lift that fails proves nothing);
|
|
41
|
+
// - the callee's lift holds an `opaque` op: an instruction no frontend models is decoded only for
|
|
42
|
+
// which registers it names, and which of them it reads is a guess (PowerPC `mtmsr r3` and MIPS
|
|
43
|
+
// `mtc0 a0, c0_sr` each read the argument the decode takes for written), so a lift holding one
|
|
44
|
+
// proves no more than one that fails;
|
|
45
|
+
// - the callee makes a call through a register, whose arity nothing here can force;
|
|
46
|
+
// - the callee's entry declares a struct or union return: one handed back through memory takes a
|
|
47
|
+
// hidden pointer the lift reads as no entry parameter, so its count is one argument word short
|
|
48
|
+
// (`struct Big mk(x)` reads r0 and r1 and lifts one parameter);
|
|
49
|
+
// - the callee takes a floating-point parameter: the count is of PARAMETERS, and the callers ask
|
|
50
|
+
// it as an argument-register INDEX. One per word holds only where every parameter is a word in
|
|
51
|
+
// its own register: o32 passes a leading double in $f12, which shadows a0 and a1, so
|
|
52
|
+
// `int hdx(double d, int x)` lifts two parameters and reads x from a2, index 2.
|
|
53
|
+
//
|
|
54
|
+
// A BODY OUTSIDE THE INPUT (`CalleeBodies`) answers only where the input defines no label of the
|
|
55
|
+
// callee's name, and is read under every rule above: a listing in the target frontend's own input
|
|
56
|
+
// format is lifted exactly as the same text placed in the input would be, with no side table of
|
|
57
|
+
// the input's (its relocations and jump tables are keyed by the input object's own sections). The
|
|
58
|
+
// driver that hands it in answers for what the input cannot say of it: that the definition is the
|
|
59
|
+
// one the link keeps (cli elf-bodies.ts reads the binding off the linked ELF's symbol table). An ARM
|
|
60
|
+
// body behind a Thumb veneer is not lifted at all: a straight-line scan of its words reads which
|
|
61
|
+
// argument registers it reads before writing them, and answers nothing for any other shape
|
|
62
|
+
// (frontend/arm-body.ts), and only on a target whose argument registers are the ones that scan
|
|
63
|
+
// counts (`scansArgumentsOf`, read off the ABI's `argRegs`).
|
|
64
|
+
// A disassembly of a linked ELF carries no relocations, and the MIPS and PowerPC frontends name a
|
|
65
|
+
// callee only by its relocation, so a listing body comes with the call relocations the driver
|
|
66
|
+
// names off the image's own symbol table (`CalleeBody.relocs`, or relocation lines in the text where
|
|
67
|
+
// the frontend reads them there): a call it cannot name stays unnamed, and the body does not lift.
|
|
68
|
+
// KNOWN GAP: an objdump listing (MIPS, PowerPC) shows no symbol binding, so a weak definition there
|
|
69
|
+
// is read as the one the link keeps. A weak copy of one inline definition is that definition; a weak
|
|
70
|
+
// default some other object overrides is not, and no row here has one.
|
|
71
|
+
import { type Block, type Fn, successorsOf } from '../ir/core';
|
|
72
|
+
import {
|
|
73
|
+
type FnProto,
|
|
74
|
+
type Prototypes,
|
|
75
|
+
calleeDeclaration,
|
|
76
|
+
declaredCallArgs,
|
|
77
|
+
declaresAggregateReturn,
|
|
78
|
+
declaresParams,
|
|
79
|
+
declaresVoidReturn,
|
|
80
|
+
} from '../proto';
|
|
81
|
+
import { helperPrototypes } from '../runtime-helpers';
|
|
82
|
+
import type { TargetDescription } from '../target';
|
|
83
|
+
import { armArgumentWords, scansArgumentsOf } from './arm-body';
|
|
84
|
+
import type { Reloc } from './asmdata';
|
|
85
|
+
import { entryArgRank } from './ssa';
|
|
86
|
+
import { thumbEntryWordsFromHalfwords } from './thumb-entry';
|
|
87
|
+
|
|
88
|
+
/** A callee's body that the input does not hold, handed in by a driver that found it elsewhere
|
|
89
|
+
* (the linked ELF): `listing` is text in the target frontend's own input format holding the
|
|
90
|
+
* callee's label, with `relocs` the relocations an object of that text would carry in its side
|
|
91
|
+
* table (`AsmData.relocs`), for a frontend that reads them there and not in the text (MIPS);
|
|
92
|
+
* `arm` is the instruction words, from its entry, of the ARM body a Thumb caller enters through an
|
|
93
|
+
* interworking veneer, which no frontend here lifts and frontend/arm-body.ts scans instead;
|
|
94
|
+
* `thumb` is the first halfwords of a Thumb body, which bound nothing of its argument registers
|
|
95
|
+
* and only say which incoming stack words its entry loads (frontend/thumb-entry.ts). */
|
|
96
|
+
export type CalleeBody =
|
|
97
|
+
| { kind: 'listing'; text: string; relocs?: readonly Reloc[] }
|
|
98
|
+
| { kind: 'arm'; words: readonly number[] }
|
|
99
|
+
| { kind: 'thumb'; halfwords: readonly number[] };
|
|
100
|
+
|
|
101
|
+
/** The bodies a driver found outside the input, keyed by callee (`DecompileOptions.calleeBodies`). */
|
|
102
|
+
export type CalleeBodies = Readonly<Record<string, CalleeBody>>;
|
|
103
|
+
|
|
104
|
+
/** One function of some text lifted under a table, with no bound of its own (`calleeBodyBounds`):
|
|
105
|
+
* the input, or a body handed in, with the relocations it carries (undefined for the input). */
|
|
106
|
+
export type CalleeLift = (
|
|
107
|
+
callee: string,
|
|
108
|
+
table: Prototypes,
|
|
109
|
+
entryReadCalls: ReadonlySet<string>,
|
|
110
|
+
text: string,
|
|
111
|
+
relocs?: readonly Reloc[],
|
|
112
|
+
) => Fn;
|
|
113
|
+
|
|
114
|
+
/** The argument words callee `callee`'s own body can read at most, or `undefined` where its body
|
|
115
|
+
* proves nothing (frontend/frontend.ts `LiftOptions.calleeBodyBound`). */
|
|
116
|
+
export type CalleeBodyBound = (callee: string) => number | undefined;
|
|
117
|
+
|
|
118
|
+
/** Whether a call to a callee with entry `entry` asks its body at all, where nothing lays the call
|
|
119
|
+
* out: only where the entry states no parameter list. A list nothing can size, or a variadic one,
|
|
120
|
+
* still states parameters the call must be handed. */
|
|
121
|
+
export function asksCalleeBody(entry: FnProto | undefined): boolean {
|
|
122
|
+
return !declaresParams(entry) && entry?.variadicParams === undefined;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** What each callee's body says, read as a call to it first asks and kept for the rest: the
|
|
126
|
+
* register bound, and the incoming stack words it loads (`CalleeStackWords` below), off ONE lift.
|
|
127
|
+
* `lift` lifts one function of a text (the input, or a body in `external`) under a table, with no
|
|
128
|
+
* bound of its own: one level of bodies is read, never the callee's callees'. `entry` is the
|
|
129
|
+
* target frontend's entry scan (`Frontend.entryStackWords`), asked only of a body that does not
|
|
130
|
+
* lift. */
|
|
131
|
+
export function calleeBodyReads(
|
|
132
|
+
self: string,
|
|
133
|
+
asm: string,
|
|
134
|
+
target: TargetDescription,
|
|
135
|
+
prototypes: Prototypes,
|
|
136
|
+
lift: CalleeLift,
|
|
137
|
+
external?: CalleeBodies,
|
|
138
|
+
entry?: EntryStackWords,
|
|
139
|
+
): { bound: CalleeBodyBound; stackWords: CalleeStackWords } {
|
|
140
|
+
const memo = new Map<string, BodyReads>();
|
|
141
|
+
const read = (callee: string): BodyReads => {
|
|
142
|
+
if (!memo.has(callee)) {
|
|
143
|
+
memo.set(callee, bodyReads(self, callee, asm, target, prototypes, lift, external, entry));
|
|
144
|
+
}
|
|
145
|
+
return memo.get(callee)!;
|
|
146
|
+
};
|
|
147
|
+
return { bound: (callee) => read(callee).bound, stackWords: (callee) => read(callee).stack };
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** {@link calleeBodyReads}'s register bound alone. */
|
|
151
|
+
export function calleeBodyBounds(
|
|
152
|
+
self: string,
|
|
153
|
+
asm: string,
|
|
154
|
+
target: TargetDescription,
|
|
155
|
+
prototypes: Prototypes,
|
|
156
|
+
lift: CalleeLift,
|
|
157
|
+
external?: CalleeBodies,
|
|
158
|
+
): CalleeBodyBound {
|
|
159
|
+
return calleeBodyReads(self, asm, target, prototypes, lift, external).bound;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** Whether `text` defines a label named `callee`: a `name:` line, an objdump `<name>:` header or a
|
|
163
|
+
* `glabel name`. */
|
|
164
|
+
export function definesLabel(text: string, callee: string): boolean {
|
|
165
|
+
const name = escaped(callee);
|
|
166
|
+
return new RegExp(`^(?:\\s*${name}\\s*:|[0-9a-fA-F]+ <${name}>:|\\s*glabel\\s+${name}\\s*$)`, 'm').test(text);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** Every name a `.weak` directive marks, in each spelling GNU as reads: one name or a comma-separated
|
|
170
|
+
* list (`.weak other, cb`), with a comment after it (`@` on ARM, `#` on MIPS and PowerPC). */
|
|
171
|
+
function weakNames(asm: string): Set<string> {
|
|
172
|
+
const out = new Set<string>();
|
|
173
|
+
for (const m of asm.matchAll(/^\s*\.weak\s+([^\n@#;]*)/gm)) {
|
|
174
|
+
m[1]
|
|
175
|
+
.split(',')
|
|
176
|
+
.map((n) => n.trim())
|
|
177
|
+
.filter((n) => n !== '')
|
|
178
|
+
.forEach((n) => out.add(n));
|
|
179
|
+
}
|
|
180
|
+
return out;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const escaped = (s: string): string => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
184
|
+
|
|
185
|
+
/** What a callee's entry says of its return, and nothing of its parameters or of its own context
|
|
186
|
+
* bound: those are what is being measured. */
|
|
187
|
+
const returnOnly = (p: FnProto | undefined): FnProto => {
|
|
188
|
+
const out: FnProto = {};
|
|
189
|
+
if (p?.returnsVoid !== undefined) {
|
|
190
|
+
out.returnsVoid = p.returnsVoid;
|
|
191
|
+
}
|
|
192
|
+
if (p?.returns !== undefined) {
|
|
193
|
+
out.returns = p.returns;
|
|
194
|
+
}
|
|
195
|
+
if (p?.returnLayout !== undefined) {
|
|
196
|
+
out.returnLayout = p.returnLayout;
|
|
197
|
+
}
|
|
198
|
+
return out;
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
/** What one callee's body answers: `bound`, the argument words it reads at most, and `stack`, the
|
|
202
|
+
* incoming stack words it loads (`CalleeStackWords`); each undefined where the body proves none. */
|
|
203
|
+
interface BodyReads {
|
|
204
|
+
readonly bound?: number;
|
|
205
|
+
readonly stack?: number;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function bodyReads(
|
|
209
|
+
self: string,
|
|
210
|
+
callee: string,
|
|
211
|
+
asm: string,
|
|
212
|
+
target: TargetDescription,
|
|
213
|
+
prototypes: Prototypes,
|
|
214
|
+
lift: CalleeLift,
|
|
215
|
+
external: CalleeBodies | undefined,
|
|
216
|
+
entry: EntryStackWords | undefined,
|
|
217
|
+
): BodyReads {
|
|
218
|
+
if (callee === self || declaresAggregateReturn(Object.hasOwn(prototypes, callee) ? prototypes[callee] : undefined)) {
|
|
219
|
+
return {};
|
|
220
|
+
}
|
|
221
|
+
// a body that lifts answers both; one that does not answers its entry's loads, and no bound
|
|
222
|
+
const lifted = (text: string, relocs?: readonly Reloc[]): BodyReads => {
|
|
223
|
+
const got = liftedReads(callee, text, target, prototypes, lift, relocs);
|
|
224
|
+
// a scan of the entry's stack loads sees no register read, so where the ABI can skip an
|
|
225
|
+
// argument register (`evenWordAlignedArgs`) it cannot say the registers are all arguments
|
|
226
|
+
return got ?? { stack: target.evenWordAlignedArgs === true ? undefined : entry?.(callee, text) };
|
|
227
|
+
};
|
|
228
|
+
if (definesLabel(asm, callee)) {
|
|
229
|
+
return weakNames(asm).has(callee) ? {} : lifted(asm);
|
|
230
|
+
}
|
|
231
|
+
const body = external !== undefined && Object.hasOwn(external, callee) ? external[callee] : undefined;
|
|
232
|
+
if (body?.kind === 'thumb') {
|
|
233
|
+
return { stack: scansArgumentsOf(target) ? thumbEntryWordsFromHalfwords(body.halfwords) : undefined };
|
|
234
|
+
}
|
|
235
|
+
if (body?.kind === 'arm') {
|
|
236
|
+
return scansArgumentsOf(target)
|
|
237
|
+
? {
|
|
238
|
+
bound: armArgumentWords(
|
|
239
|
+
body.words,
|
|
240
|
+
!declaresVoidReturn(Object.hasOwn(prototypes, callee) ? prototypes[callee] : undefined),
|
|
241
|
+
),
|
|
242
|
+
}
|
|
243
|
+
: {};
|
|
244
|
+
}
|
|
245
|
+
if (body === undefined || !definesLabel(body.text, callee) || weakNames(body.text).has(callee)) {
|
|
246
|
+
return {};
|
|
247
|
+
}
|
|
248
|
+
return lifted(body.text, body.relocs);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/** The entry parameters a lift of `callee` out of `text` counts, once every call in it nothing lays
|
|
252
|
+
* out is taken at every argument register, and one past the highest incoming stack word among them
|
|
253
|
+
* (`entryArgRank`): an argument slot past the registers is minted only below a stack word the body
|
|
254
|
+
* loads, so that count is a word it reads. Undefined where no lift proves it. */
|
|
255
|
+
function liftedReads(
|
|
256
|
+
callee: string,
|
|
257
|
+
text: string,
|
|
258
|
+
target: TargetDescription,
|
|
259
|
+
prototypes: Prototypes,
|
|
260
|
+
lift: CalleeLift,
|
|
261
|
+
relocs?: readonly Reloc[],
|
|
262
|
+
): BodyReads | undefined {
|
|
263
|
+
const helpers = helperPrototypes(target);
|
|
264
|
+
const table: Prototypes = { ...prototypes, [callee]: returnOnly(prototypes[callee]) };
|
|
265
|
+
const forced = new Set<string>();
|
|
266
|
+
// Each round forces the calls the last lift guessed and lifts again; a round that forces nothing
|
|
267
|
+
// new is the answer. The forced set only grows, so this ends.
|
|
268
|
+
for (;;) {
|
|
269
|
+
let fn: Fn;
|
|
270
|
+
try {
|
|
271
|
+
fn = lift(callee, table, forced, text, relocs);
|
|
272
|
+
} catch {
|
|
273
|
+
return undefined;
|
|
274
|
+
}
|
|
275
|
+
const guessed = new Set<string>();
|
|
276
|
+
for (const b of fn.blocks) {
|
|
277
|
+
for (const op of b.ops) {
|
|
278
|
+
if (op.opcode === 'opaque') {
|
|
279
|
+
return undefined;
|
|
280
|
+
}
|
|
281
|
+
if (op.opcode !== 'call') {
|
|
282
|
+
continue;
|
|
283
|
+
}
|
|
284
|
+
const at = op.attrs.target;
|
|
285
|
+
if (typeof at !== 'string') {
|
|
286
|
+
return undefined;
|
|
287
|
+
}
|
|
288
|
+
if (!forced.has(at) && declaredCallArgs(calleeDeclaration(at, table, helpers), target) === undefined) {
|
|
289
|
+
guessed.add(at);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
if (guessed.size === 0) {
|
|
294
|
+
const { params } = fn.blocks[0];
|
|
295
|
+
if (params.some((p) => p.type.kind === 'float')) {
|
|
296
|
+
return undefined;
|
|
297
|
+
}
|
|
298
|
+
const passesFirst =
|
|
299
|
+
target.returnReg === target.argRegs[0] && !declaresVoidReturn(table[callee]) && returnsUntouched(fn);
|
|
300
|
+
const top = Math.max(0, ...params.map((p) => (entryArgRank(p) ?? -1) + 1));
|
|
301
|
+
const stack = Math.max(0, top - target.argRegs.length);
|
|
302
|
+
return {
|
|
303
|
+
bound: Math.max(params.length, passesFirst ? 1 : 0),
|
|
304
|
+
stack:
|
|
305
|
+
stack > 0 && target.evenWordAlignedArgs === true && !readsEveryArgReg(fn, target, forced) ? undefined : stack,
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
// an entry whose parameter list nothing can lay out (an unsizable or variadic one) is dropped
|
|
309
|
+
// too, so the call is taken at the incoming values rather than at what the guess counted
|
|
310
|
+
for (const g of guessed) {
|
|
311
|
+
forced.add(g);
|
|
312
|
+
table[g] = returnOnly(Object.hasOwn(table, g) ? table[g] : undefined);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/** Whether the lifted body READS every argument register: each is an entry parameter some op or
|
|
318
|
+
* edge uses. Below a stack word the body loads, every register slot is minted a parameter
|
|
319
|
+
* (frontend/ssa.ts `mintArgSlotHoles`) whether or not it is read, which counts the slots right
|
|
320
|
+
* only on an ABI that never skips one. Where an 8-aligned argument starts at an even word
|
|
321
|
+
* (`evenWordAlignedArgs`: O32 `int f(int, int, int, long long)` leaves a3 empty and passes the
|
|
322
|
+
* `long long` at 16(sp)), a minted register may be that skipped word, which no caller writes;
|
|
323
|
+
* taken as an argument it hands the callee whatever the caller left there, and gives a caller
|
|
324
|
+
* that never wrote it a parameter it does not have. So there the stack words count only where
|
|
325
|
+
* every register below them is one the body reads. A call in `forced` (taken at every argument
|
|
326
|
+
* register its incoming value may still sit in, for the register bound) is no read: it takes the
|
|
327
|
+
* skipped register too. */
|
|
328
|
+
function readsEveryArgReg(fn: Fn, target: TargetDescription, forced: ReadonlySet<string>): boolean {
|
|
329
|
+
const used = new Set<unknown>();
|
|
330
|
+
for (const b of fn.blocks) {
|
|
331
|
+
for (const op of b.ops) {
|
|
332
|
+
if (op.opcode === 'call' && typeof op.attrs.target === 'string' && forced.has(op.attrs.target)) {
|
|
333
|
+
continue;
|
|
334
|
+
}
|
|
335
|
+
op.operands.forEach((v) => used.add(v));
|
|
336
|
+
op.successors.forEach((e) => e.args.forEach((v) => used.add(v)));
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
const read = new Set(fn.blocks[0].params.filter((p) => used.has(p)).map((p) => entryArgRank(p)));
|
|
340
|
+
return target.argRegs.every((_, k) => read.has(k));
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/** Whether some return of `fn` hands back nothing the lift names (a bare `ret`) on a path from the
|
|
344
|
+
* entry that makes no call: there the return register still holds what the caller left in it. */
|
|
345
|
+
function returnsUntouched(fn: Fn): boolean {
|
|
346
|
+
const callFree = new Set<Block>();
|
|
347
|
+
const work = fn.blocks.length === 0 ? [] : [fn.blocks[0]];
|
|
348
|
+
while (work.length > 0) {
|
|
349
|
+
const b = work.pop()!;
|
|
350
|
+
if (callFree.has(b) || b.ops.some((o) => o.opcode === 'call')) {
|
|
351
|
+
continue;
|
|
352
|
+
}
|
|
353
|
+
callFree.add(b);
|
|
354
|
+
work.push(...successorsOf(b));
|
|
355
|
+
}
|
|
356
|
+
return [...callFree].some((b) => b.ops.some((o) => o.opcode === 'ret' && o.operands.length === 0));
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/** WHAT A CALLEE'S BODY SAYS OF ITS STACK ARGUMENTS: one past the highest incoming stack word it is
|
|
360
|
+
* proven to load, a LOWER bound on the words a call to it takes (frontend/stackargs.ts, "THE
|
|
361
|
+
* CALLEE'S BODY"), or `undefined` where it proves none. Read under the rules of the register bound
|
|
362
|
+
* and answering nothing where they do. Where the body lifts it is the lift's count (the whole body,
|
|
363
|
+
* every path); where it does not, the frontend's scan of its straight-line entry
|
|
364
|
+
* (`Frontend.entryStackWords`, or a Thumb body's halfwords: frontend/thumb-entry.ts), which sees
|
|
365
|
+
* only the words loaded before the first branch, and so says less. */
|
|
366
|
+
export type CalleeStackWords = (callee: string) => number | undefined;
|
|
367
|
+
|
|
368
|
+
/** Reads the incoming stack words the straight-line entry of `callee` in a text of the frontend's
|
|
369
|
+
* input format loads (`Frontend.entryStackWords`). */
|
|
370
|
+
export type EntryStackWords = (callee: string, text: string) => number | undefined;
|
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
* (target.ts `readCouldBeVolatile`): on a compiler that sign-extends a qualified narrow read in a
|
|
55
55
|
* register, a sign-extending load is a plain read in the source, and qualified it recompiles to
|
|
56
56
|
* another instruction sequence. */
|
|
57
|
-
import { type RangeCtx, type StridedRange,
|
|
57
|
+
import { type RangeCtx, type StridedRange, edgeFacts, stridedRange } from '../ir/bits';
|
|
58
58
|
import type { Block, Op, Value } from '../ir/core';
|
|
59
59
|
import type { SymbolMap } from '../symbols';
|
|
60
60
|
import { type TargetDescription, readCouldBeVolatile } from '../target';
|
|
@@ -66,9 +66,14 @@ export interface LiteralAddresses {
|
|
|
66
66
|
constOf(v: Value): number | undefined;
|
|
67
67
|
/** the literal address `v` denotes, or undefined when this cannot say */
|
|
68
68
|
literalAddrOf(v: Value): number | undefined;
|
|
69
|
-
/** every
|
|
70
|
-
* bound it
|
|
71
|
-
|
|
69
|
+
/** every value the word `v` may hold, as a strided unsigned interval, or undefined when this
|
|
70
|
+
* cannot bound it: not only the addresses an address may denote — a runtime index into a frame
|
|
71
|
+
* object is bounded here too (frame-objects.ts `indexedAccess`); asked `at` the block that reads
|
|
72
|
+
* `v`, also under what the branches above that block prove there. In the `narrowest` window,
|
|
73
|
+
* for an index added to a base: the signed integers those words are where that interval is
|
|
74
|
+
* narrower (`lo` then below 0: IDO folds `+ 32` into the frame base, and its `i % 32` index is
|
|
75
|
+
* [-32, 31] from there, where the words are every word) */
|
|
76
|
+
addressRangeOf(v: Value, at?: Block, window?: 'word' | 'narrowest'): StridedRange | undefined;
|
|
72
77
|
}
|
|
73
78
|
|
|
74
79
|
/** `literalAddrOf`: `const` is the bare pool word, `gaddr` is the same word after the symbol map
|
|
@@ -133,12 +138,17 @@ export function literalAddresses(irBlocks: readonly Block[], symbols: SymbolMap
|
|
|
133
138
|
}
|
|
134
139
|
return undefined;
|
|
135
140
|
};
|
|
136
|
-
// `addressRangeOf`: where `literalAddrOf` names one address, this names every
|
|
137
|
-
//
|
|
141
|
+
// `addressRangeOf`: where `literalAddrOf` names one address, this names every value a word may
|
|
142
|
+
// hold (ir/bits.ts `stridedRange`), an address or not, a placed name being the one leaf only this
|
|
143
|
+
// module reads.
|
|
144
|
+
// A block param is read through what each in-edge's branch proves: a loop counter's latch test
|
|
145
|
+
// bounds the counter its back edge passes; a value asked at a block, through what holds there.
|
|
146
|
+
const edges = edgeFacts(irBlocks);
|
|
138
147
|
const rangeCtx: RangeCtx = {
|
|
139
148
|
defs: defOf,
|
|
140
149
|
constOf,
|
|
141
|
-
incoming:
|
|
150
|
+
incoming: edges.incoming,
|
|
151
|
+
holdsAt: edges.holdsAt,
|
|
142
152
|
entry: new Set(irBlocks[0]?.params ?? []),
|
|
143
153
|
pointOf: (v) => (defOf.get(v)?.opcode === 'gaddr' ? literalAddrOf(v) : undefined),
|
|
144
154
|
};
|
|
@@ -146,7 +156,7 @@ export function literalAddresses(irBlocks: readonly Block[], symbols: SymbolMap
|
|
|
146
156
|
defOf,
|
|
147
157
|
constOf,
|
|
148
158
|
literalAddrOf: (v) => literalAddrOf(v),
|
|
149
|
-
addressRangeOf: (v) => stridedRange(rangeCtx, v),
|
|
159
|
+
addressRangeOf: (v, at, window) => stridedRange(rangeCtx, v, at, window),
|
|
150
160
|
};
|
|
151
161
|
}
|
|
152
162
|
|