@asmlift/core 0.8.1 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +34 -30
- package/package.json +1 -1
- package/src/aggregate.ts +248 -0
- package/src/backend/c.ts +5 -2
- package/src/backend/cfamily.ts +236 -37
- package/src/backend/cpp.ts +114 -9
- package/src/backend/pascal.ts +39 -0
- package/src/cdecl/constant.ts +59 -0
- package/src/cdecl/lex.ts +209 -0
- package/src/cdecl/parse.ts +975 -0
- package/src/cdecl/spell.ts +69 -0
- package/src/codegen-flags.ts +38 -6
- package/src/compiler-diagnostics.ts +194 -0
- package/src/contracts.ts +615 -79
- package/src/declare.ts +75 -5
- package/src/frontend/asmdata.ts +19 -5
- package/src/frontend/call-plan.ts +686 -0
- package/src/frontend/device-pins.ts +277 -0
- package/src/frontend/errors.ts +5 -0
- package/src/frontend/flags-edge.ts +101 -0
- package/src/frontend/fpu.ts +167 -0
- package/src/frontend/frame-objects.ts +2131 -0
- package/src/frontend/high-half.ts +2 -1
- package/src/frontend/local-object.ts +490 -0
- package/src/frontend/mips.ts +363 -40
- package/src/frontend/opaque.ts +97 -18
- package/src/frontend/ppc.ts +520 -129
- package/src/frontend/reloc-symbol.ts +94 -13
- package/src/frontend/splat.ts +108 -5
- package/src/frontend/ssa.ts +669 -76
- package/src/frontend/stackargs.ts +120 -39
- package/src/frontend/thumb.ts +2764 -2018
- package/src/ir/alias.ts +29 -0
- package/src/ir/core.ts +101 -3
- package/src/ir/discipline.ts +248 -0
- package/src/ir/float-bits.ts +47 -0
- package/src/ir/opcodes.ts +84 -112
- package/src/ir/parse.ts +1 -0
- package/src/ir/types.ts +118 -2
- package/src/ir/verify.ts +150 -3
- package/src/l3/address.ts +5 -0
- package/src/l3/argcopy.ts +38 -6
- package/src/l3/ast.ts +186 -14
- package/src/l3/coalesce.ts +21 -13
- package/src/l3/dce.ts +32 -10
- package/src/l3/gates.ts +9 -3
- package/src/l3/hoist.ts +8 -7
- package/src/l3/initfirst.ts +27 -9
- package/src/l3/intcell.ts +97 -0
- package/src/l3/mentions.ts +10 -4
- package/src/l3/narrowdecl.ts +218 -0
- package/src/l3/nearbase.ts +10 -5
- package/src/l3/parkfirst.ts +8 -5
- package/src/l3/pollguard.ts +3 -2
- package/src/l3/ptrcell.ts +357 -0
- package/src/l3/regspell.ts +17 -3
- package/src/l3/reindex.ts +10 -7
- package/src/l3/symbol-refs.ts +104 -23
- package/src/l3/tailmerge.ts +6 -19
- package/src/l3/typing.ts +137 -25
- package/src/l3/unreduce.ts +4 -4
- package/src/l3/volstore.ts +96 -7
- package/src/mangle.ts +11 -1
- package/src/pattern/engine.ts +19 -16
- package/src/pipeline.ts +57 -12
- package/src/proto-context.ts +788 -0
- package/src/proto.ts +689 -50
- package/src/raise/arrays.ts +4 -3
- package/src/raise/declared-volatile.ts +131 -0
- package/src/raise/divpow2.ts +2 -2
- package/src/raise/errors.ts +17 -0
- package/src/raise/floathelpers.ts +243 -0
- package/src/raise/globalshape.ts +11 -5
- package/src/raise/gvn.ts +26 -5
- package/src/raise/narrow.ts +29 -2
- package/src/raise/narrowlocal.ts +11 -6
- package/src/raise/offsetnames.ts +510 -0
- package/src/raise/pairparams.ts +81 -0
- package/src/raise/paramwidth.ts +7 -0
- package/src/raise/pre-recovery.ts +78 -4
- package/src/raise/recover.ts +14 -10
- package/src/raise/retsink.ts +1 -1
- package/src/raise/shortcircuit.ts +41 -19
- package/src/raise/softdiv.ts +21 -23
- package/src/raise/struct-arrays.ts +5 -4
- package/src/raise/structs.ts +311 -98
- package/src/raise/truncload.ts +366 -0
- package/src/raise/widehelpers.ts +106 -0
- package/src/rank-declare.ts +30 -5
- package/src/rank-variations.ts +36 -8
- package/src/rank.ts +267 -79
- package/src/runtime-helpers.ts +287 -0
- package/src/stillborn.ts +289 -0
- package/src/structure/analysis.ts +1285 -448
- package/src/structure/hazards.ts +633 -58
- package/src/structure/local-statics.ts +381 -0
- package/src/structure/namecoalesce.ts +1 -1
- package/src/structure/redundant-test.ts +99 -0
- package/src/structure/structure.ts +1772 -395
- package/src/structure/switch-recover.ts +159 -78
- package/src/symbols.ts +38 -3
- package/src/target.ts +662 -59
- package/src/trace.ts +44 -24
- package/src/variation-definitions.ts +119 -1
- package/src/variation-tokens.ts +20 -6
package/src/proto.ts
CHANGED
|
@@ -1,15 +1,21 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { parseTypeName } from './cdecl/parse';
|
|
2
|
+
import type { IrType } from './ir/types';
|
|
3
|
+
import type { SymbolInfo, SymbolMap, SymbolTypeFacts } from './symbols';
|
|
4
|
+
import type { TargetDescription } from './target';
|
|
2
5
|
|
|
3
6
|
// asmlift — function prototypes: the single carrier for the caller-supplied facts a
|
|
4
7
|
// matching-decomp project reads from its headers (arg counts, parameter widths, void-ness). One
|
|
5
8
|
// `Prototypes` map, keyed by symbol, is threaded through every entry point and resolved at the
|
|
6
|
-
// point of use — a callee's `params` gives its call
|
|
9
|
+
// point of use — a callee's `params` gives the registers its call occupies, a function's own entry gives its
|
|
7
10
|
// `returnsVoid` and the widths raise/paramwidth.ts checks against. It also keeps the frontend seam
|
|
8
11
|
// honest: a frontend receives prototypes, not a grab-bag of ISA-specific options.
|
|
9
12
|
|
|
10
|
-
/** One declared parameter, as its C type text (`"u8"`, `"s32"`, `"void *"`, `"int"`).
|
|
11
|
-
*
|
|
12
|
-
* (`declaredWidth`)
|
|
13
|
+
/** One declared parameter, as its C type text (`"u8"`, `"s32"`, `"void *"`, `"int"`). The lift reads
|
|
14
|
+
* ONE fact off it and derives everything else from that fact: the WIDTH it spells
|
|
15
|
+
* (`declaredWidth`). raise/paramwidth.ts checks its inference against that width, and
|
|
16
|
+
* `declaredCallArgs` sums the list's widths into the argument registers the call occupies. On a
|
|
17
|
+
* function compiled as C++ the printer reads the SPELLING too: a call argument is cast to it where
|
|
18
|
+
* C++ converts nothing implicitly (backend/cfamily.ts `argConversion`).
|
|
13
19
|
*
|
|
14
20
|
* A DECLARED WIDTH ONLY VETOES, never pins. Where the asm carries a prologue extension the
|
|
15
21
|
* declaration contradicts, the declaration wins — it is a fact from the project's headers, where
|
|
@@ -22,10 +28,12 @@ export type ParamType = string;
|
|
|
22
28
|
/** What the headers know about one function. All fields optional: a partial table (only
|
|
23
29
|
* callee arities, or only the current function's void-ness) is the common case. */
|
|
24
30
|
export interface FnProto {
|
|
25
|
-
/** declared parameters — either
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
31
|
+
/** declared parameters — either the typed parameter list a header extraction produces
|
|
32
|
+
* (`["u8", "s32"]`), which is a list of C PARAMETERS, or a bare COUNT, which is the user's word
|
|
33
|
+
* for how many argument REGISTERS the call occupies. The two are the same number only while
|
|
34
|
+
* every parameter fits in a register, and `declaredCallArgs` is what converts the first into
|
|
35
|
+
* the second. Omit — or spell a parameter that conversion cannot size — to let the frontend
|
|
36
|
+
* fall back to its contiguous-arg-register heuristic. */
|
|
29
37
|
params?: number | ParamType[];
|
|
30
38
|
/** The declared return type is `void`. Read for the function under decompilation, where a
|
|
31
39
|
* trailing `bx lr` leaves a meaningless return register that must not surface as a `return`
|
|
@@ -45,29 +53,265 @@ export interface FnProto {
|
|
|
45
53
|
* Both readers see one field through two trust levels: caller-supplied on `--proto`, and
|
|
46
54
|
* machine-derived from DWARF through `prototypesFromSymbols`. Neither is distinguished here. */
|
|
47
55
|
returnsVoid?: boolean;
|
|
56
|
+
/** The declared return type, as its C type text — the same vocabulary `params` is written in and
|
|
57
|
+
* read through the same {@link declaredWidth}, because a return and a parameter spell a width
|
|
58
|
+
* the same way and two spellings of one vocabulary is where they start to disagree.
|
|
59
|
+
*
|
|
60
|
+
* WHAT IT BUYS THAT `returnsVoid` CANNOT: how many registers the callee hands BACK. A value
|
|
61
|
+
* wider than a register comes home in a register PAIR, so after a `bl` the second register holds
|
|
62
|
+
* the returned high half — a legitimate definition — where for every other callee it holds the
|
|
63
|
+
* callee's leftovers and reading it is the wrong value `frontend/ssa.ts` refuses. Those two are
|
|
64
|
+
* the same instructions, so nothing in the assembly tells them apart and the caller must be
|
|
65
|
+
* TOLD, exactly as `returnsVoid` must be.
|
|
66
|
+
*
|
|
67
|
+
* A SPELLING THIS CANNOT SIZE — OR CANNOT PRINT — IS SILENCE, never a word. {@link spellableProto}
|
|
68
|
+
* is the one gate and {@link declaredReturnWidth} is defined on top of it, so a `returns` whose
|
|
69
|
+
* width the frontend acts on is always one the candidate's own translation unit can declare;
|
|
70
|
+
* anything else lifts the call the way an undeclared callee is lifted. Under-declaring stays the
|
|
71
|
+
* safe direction here too — a return wrongly declared 64-bit names a register the callee really
|
|
72
|
+
* did destroy, which is the defect the refusal exists for.
|
|
73
|
+
*
|
|
74
|
+
* `"void"` HERE IS `returnsVoid`, not a width. The two keys are one fact and
|
|
75
|
+
* {@link declaresVoidReturn} reads both, so either spelling suppresses the phantom return value
|
|
76
|
+
* on the function's own entry and rules out a hidden struct-return pointer at a callee.
|
|
77
|
+
* `validatePrototypes` refuses a table that states them differently, in either direction.
|
|
78
|
+
*
|
|
79
|
+
* A HEADER STATES IT AND NOTHING DERIVES IT. `prototypesFromSymbols` does not fill this in:
|
|
80
|
+
* `typeSpelling` sizes 1, 2 and 4 bytes, so a DWARF-derived entry could only ever spell a return
|
|
81
|
+
* that already fits a register, where the field changes nothing. */
|
|
82
|
+
returns?: ParamType;
|
|
83
|
+
/** The declared return is a struct or union handed back BY VALUE. A caller that calls such a
|
|
84
|
+
* function may be handing it a hidden pointer to the storage the return lands in, in argument 0
|
|
85
|
+
* (agbcc thumb.h:672 STRUCT_VALUE_REGNUM 0), with every declared argument one register up
|
|
86
|
+
* (thumb.h:644-645) — so the declared `params` do not say which registers the call reads, and a
|
|
87
|
+
* lift that reads them from r0 names the hidden pointer as the first argument. A `returns` that
|
|
88
|
+
* spells `struct Tag` or `union Tag` states the same fact ({@link declaresAggregateReturn});
|
|
89
|
+
* this key also says it of a typedef name, which spells no keyword, and carries the members
|
|
90
|
+
* whose layout decides whether the target returns it through memory at all (`aggregate.ts`). */
|
|
91
|
+
returnLayout?: AggregateLayout;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** A struct or union a declaration spells: what it is, and its members as the header lists them.
|
|
95
|
+
* No offset or size is stored, because both are the TARGET's — agbcc rounds every struct and
|
|
96
|
+
* union to a word (thumb.h:367 STRUCTURE_SIZE_BOUNDARY 32) where mwcc does not — and are computed
|
|
97
|
+
* where a target is in hand (`aggregate.ts`). */
|
|
98
|
+
export interface AggregateLayout {
|
|
99
|
+
kind: 'struct' | 'union';
|
|
100
|
+
/** every member in declaration order; absent when one of them could not be read, which leaves
|
|
101
|
+
* the aggregate known to exist and nothing known about its shape */
|
|
102
|
+
members?: AggregateMember[];
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** One member of an {@link AggregateLayout}. */
|
|
106
|
+
export interface AggregateMember {
|
|
107
|
+
/** empty only for an unnamed bitfield, which pads */
|
|
108
|
+
name: string;
|
|
109
|
+
/** a scalar or pointer type as C spells it (`u32`, `float`, `u8 *`), an enum as `enum` and its
|
|
110
|
+
* name, or a nested struct or union */
|
|
111
|
+
type: ParamType | AggregateLayout;
|
|
112
|
+
/** an array member's extents, outermost first */
|
|
113
|
+
dims?: number[];
|
|
114
|
+
/** a bitfield's width in bits */
|
|
115
|
+
bits?: number;
|
|
48
116
|
}
|
|
49
117
|
|
|
50
118
|
/** symbol → prototype. The function under decompilation and its callees share one table. */
|
|
51
119
|
export type Prototypes = Record<string, FnProto>;
|
|
52
120
|
|
|
53
|
-
/**
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
|
|
121
|
+
/** How many argument REGISTERS a list of parameter WIDTHS occupies: a parameter wider than a
|
|
122
|
+
* machine word travels in a register pair, every other one in a single register.
|
|
123
|
+
*
|
|
124
|
+
* The rule lives here rather than beside either caller because it has two, and they are the two
|
|
125
|
+
* vocabularies a declaration is written in. `runtime-helpers.ts` states a compiler's own helper
|
|
126
|
+
* signatures as widths (`__ashrdi3` is `[64, 32]` — two C parameters, three registers), and
|
|
127
|
+
* `declaredCallArgs` below reads the same widths off a project's C types. One ABI fact, one
|
|
128
|
+
* copy of it; a second copy is a rule that can disagree with itself.
|
|
129
|
+
*
|
|
130
|
+
* NO EVEN-REGISTER ALIGNMENT, and that is a measured agbcc fact rather than an omission: its
|
|
131
|
+
* `thumb.h` computes an argument's register from a plain byte counter with no rounding, so
|
|
132
|
+
* `void f(s32, long long)` passes the pair in r1:r2 where AAPCS would pad to r2:r3.
|
|
133
|
+
*
|
|
134
|
+
* AN ABI THAT DOES ALIGN HAS NO READER HERE YET, which is why the rule is flat rather than a
|
|
135
|
+
* knob on `TargetDescription` beside `argRegs` and `stagesOutgoingArgsInFrame`. MIPS o32 aligns a
|
|
136
|
+
* 64-bit argument to an even register pair and is the target that will want one, and
|
|
137
|
+
* `frontend/mips.ts` refuses a declared 64-bit parameter (`CallLowering.pairs`), so every list it
|
|
138
|
+
* counts is one word per parameter. The lowering that builds an o32 pair owes the rule a home. */
|
|
139
|
+
export function wordsOf(params: readonly number[]): number {
|
|
140
|
+
return params.reduce((n, w) => n + (w > 32 ? 2 : 1), 0);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** Whether a proto states a parameter list at all — readably or not. The tier question, asked
|
|
144
|
+
* before the layout question: a project that declares a callee has re-declared it, and falling
|
|
145
|
+
* through to a compiler's runtime table or to the C standard's signatures behind a declaration
|
|
146
|
+
* nobody could SIZE would answer with a different function's shape. */
|
|
147
|
+
export function declaresParams(p: FnProto | undefined): boolean {
|
|
148
|
+
return typeof p?.params === 'number' || Array.isArray(p?.params);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** What a declaration says about the argument registers a call occupies — the width each declared
|
|
152
|
+
* parameter travels in, in argument order — or `undefined` when it says nothing this can act on.
|
|
153
|
+
*
|
|
154
|
+
* THREE WAYS TO SAY NOTHING, AND THEY ARE ONE ANSWER. `params` omitted; `params` malformed (a
|
|
155
|
+
* bare `"u8"` string, the shape the untyped CLI `--proto` JSON admits); and a list holding a
|
|
156
|
+
* spelling `declaredWidth` cannot read — a project typedef (`bool8`, `Direction`, `TaskFunc`), a
|
|
157
|
+
* by-value struct, a floating type. The first two are not a reading of the list at all. The third
|
|
158
|
+
* is a list read to the end that cannot be CONVERTED: a parameter of unknown width occupies one
|
|
159
|
+
* argument register or two, the choice moves every later argument's home, and nothing a
|
|
160
|
+
* declaration holds settles it.
|
|
161
|
+
*
|
|
162
|
+
* SO IT ABSTAINS, AND ABSTAINING IS NEITHER A REFUSAL NOR A GUESS. The caller falls back to its
|
|
163
|
+
* own contiguous-arg-register scan — exactly what it does for a callee the project never declared
|
|
164
|
+
* — and that scan names itself a guess: `SsaBuilder.readGuessedArg` ASKS whether a register was
|
|
165
|
+
* set up, and `finish()` retracts the ones an intervening call destroyed. Refusing instead would
|
|
166
|
+
* be a function that lifts when told nothing and declines when told more. Returning
|
|
167
|
+
* one-register-each instead would be a layout nothing could know to be right, and the frontend
|
|
168
|
+
* would then ASSERT those registers rather than ask for them.
|
|
169
|
+
*
|
|
170
|
+
* THE MACHINE IS NOT A WITNESS FOR A WIDTH NOBODY STATED, which is the reading this replaces. The
|
|
171
|
+
* contiguous scan is wrong in both directions and neither is recoverable here. It OVER-counts:
|
|
172
|
+
* it is read before `frontend/ssa.ts` `narrowToSetupArgs` trims the registers a call sat between,
|
|
173
|
+
* so `bl __muldi3` leaves r2 and r3 counted as arguments of the NEXT call. It UNDER-counts: an
|
|
174
|
+
* incoming argument register this function passes straight through has no definition here at all,
|
|
175
|
+
* so it counts zero — measured on agbcc, `void g(MyU64); void f(MyU64 x){ g(x|1); }` leaves the
|
|
176
|
+
* high half in r1 untouched and the scan answers 1 for a call the ABI hands a pair. An equality
|
|
177
|
+
* between that scan and the one-register-each reading is therefore as likely to be a miscounted
|
|
178
|
+
* pair as a confirmation of anything, and a witness that cannot fail is not a witness.
|
|
179
|
+
*
|
|
180
|
+
* The COUNT form is the user's word for argument REGISTERS and says so at its declaration, so it
|
|
181
|
+
* expands to that many words and can never abstain. It is the way past a header this cannot size.
|
|
182
|
+
*
|
|
183
|
+
* A `double` IS A TARGET FACT. Where it crosses a call in general argument words
|
|
184
|
+
* (`TargetDescription.doubleArgWords`) it is 64 bits of words and its index is in `doubles`, so a
|
|
185
|
+
* reader can tell it from a `long long`, whose bits are the same words in another order; on a
|
|
186
|
+
* target that states no such words the list abstains. A `float` states no layout on either: it
|
|
187
|
+
* is one word on a soft-float target, but that word is a float only to a callee whose declaration
|
|
188
|
+
* the candidate prints, and `spellableProto` prints no `float` — an undeclared callee is handed a
|
|
189
|
+
* `float` promoted to a `double`, which is two words. */
|
|
190
|
+
export function declaredCallArgs(
|
|
191
|
+
p: FnProto | undefined,
|
|
192
|
+
target: Pick<TargetDescription, 'doubleArgWords'>,
|
|
193
|
+
): { widths: readonly number[]; doubles: ReadonlySet<number> } | undefined {
|
|
58
194
|
if (typeof p?.params === 'number') {
|
|
59
|
-
return p.params;
|
|
195
|
+
return { widths: Array.from({ length: p.params }, () => 32), doubles: new Set() };
|
|
196
|
+
}
|
|
197
|
+
if (!Array.isArray(p?.params)) {
|
|
198
|
+
return undefined;
|
|
60
199
|
}
|
|
61
|
-
|
|
62
|
-
|
|
200
|
+
const widths: number[] = [];
|
|
201
|
+
const doubles = new Set<number>();
|
|
202
|
+
for (const [i, t] of p.params.entries()) {
|
|
203
|
+
const w = declaredWidth(t);
|
|
204
|
+
if (w !== undefined) {
|
|
205
|
+
widths.push(w);
|
|
206
|
+
} else if (target.doubleArgWords !== undefined && isDoubleSpelling(t)) {
|
|
207
|
+
doubles.add(i);
|
|
208
|
+
widths.push(64);
|
|
209
|
+
} else {
|
|
210
|
+
return undefined;
|
|
211
|
+
}
|
|
63
212
|
}
|
|
64
|
-
|
|
65
|
-
// fall back to the frontend's arg-register heuristic rather than misread a string's `.length`.
|
|
66
|
-
return undefined;
|
|
213
|
+
return { widths, doubles };
|
|
67
214
|
}
|
|
68
215
|
|
|
69
|
-
/**
|
|
70
|
-
|
|
216
|
+
/** Whether a declared type is `double`, qualifiers aside. */
|
|
217
|
+
const isDoubleSpelling = (t: ParamType): boolean => t.replace(/\b(?:const|volatile)\b/g, ' ').trim() === 'double';
|
|
218
|
+
|
|
219
|
+
/** Whether a declared type is a floating type, qualifiers aside: a C keyword, or a typedef of one
|
|
220
|
+
* every project declares (`FLOAT_TYPEDEFS`). */
|
|
221
|
+
export const isFloatingSpelling = (t: ParamType): boolean => {
|
|
222
|
+
const bare = t
|
|
223
|
+
.replace(/\b(?:const|volatile)\b/g, ' ')
|
|
224
|
+
.trim()
|
|
225
|
+
.replace(/\s+/g, ' ');
|
|
226
|
+
return /^(?:float|double|long double)$/.test(FLOAT_TYPEDEFS.get(bare) ?? bare);
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
/** A signature the C standard fixes is a COMPLETE one, which an `FnProto` is not: a project
|
|
230
|
+
* prototype is a lower bound assembled from whatever a header extraction could read, and omits
|
|
231
|
+
* what it could not. The standard omits nothing, so the RETURN is spelled here and is required —
|
|
232
|
+
* it is the fact `returnsWithoutHiddenPointer` needs and the fact no header ever supplied. */
|
|
233
|
+
interface StandardSignature extends FnProto {
|
|
234
|
+
/** the return type, as its C spelling (`"void *"`, `"u32"`, `"void"`) */
|
|
235
|
+
returns: ParamType;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/** Signatures FIXED BY THE C STANDARD, so they are not a project fact and need no header to be
|
|
239
|
+
* known. Consumed by a frontend's arity lookup behind both the caller-supplied prototype and the
|
|
240
|
+
* compiler's own runtime helpers (raise/softdiv.ts) — a project that declares one of these wins,
|
|
241
|
+
* because a decomp may legitimately be building against its own re-declaration.
|
|
242
|
+
*
|
|
243
|
+
* WHAT AN ENTRY BUYS, which is not the same as what it changes: the arity a call recovers is
|
|
244
|
+
* usually the same number the arg-register heuristic already guessed, so an entry moves no code.
|
|
245
|
+
* What it moves is what is KNOWN — a guess cannot witness anything, and an entry can. The
|
|
246
|
+
* frame-object audit reads the `returns` of one to rule out a hidden struct-return pointer
|
|
247
|
+
* (`aggregate.ts` `returnsWithoutHiddenPointer`, asked by frontend/frame-objects.ts).
|
|
248
|
+
*
|
|
249
|
+
* THE LIST IS SHORT ON PURPOSE. `memcpy` is here because a corpus row exercises it and its price
|
|
250
|
+
* was measured. `memset`, `strcpy` and the rest of the standard library are equally fixed by the
|
|
251
|
+
* standard and equally addable, and they are absent because nothing measures them — a table
|
|
252
|
+
* grown by appetite would be a table nobody priced. */
|
|
253
|
+
export const STANDARD_SIGNATURES: Record<string, StandardSignature> = {
|
|
254
|
+
memcpy: { params: ['void *', 'const void *', 'u32'], returns: 'void *' },
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
/** Whether a declaration says the function returns NO VALUE — the one question `returnsVoid` was
|
|
258
|
+
* added for, asked through both keys that can answer it.
|
|
259
|
+
*
|
|
260
|
+
* THE VALIDATOR'S MESSAGE CALLS THE TWO KEYS "one fact spelled two ways", and this is what makes
|
|
261
|
+
* that true rather than a description of an intention. Before it, `returns: "void"` validated,
|
|
262
|
+
* read as interchangeable, and did nothing at all on the function under decompilation: a void
|
|
263
|
+
* function whose asm leaves garbage in r0 got `return <garbage>;` — a candidate that compiles and
|
|
264
|
+
* scores, which is the exact defect `returnsVoid` exists to prevent. Every reader of the fact
|
|
265
|
+
* calls this, so a key that validates is a key that acts.
|
|
266
|
+
*
|
|
267
|
+
* ONE DIRECTION ONLY, and that asymmetry is the field's: `returnsVoid: false` states nothing that
|
|
268
|
+
* any reader acts on (silence and "not void" take the same path), so this answers the positive
|
|
269
|
+
* claim and leaves the rest to the machine. */
|
|
270
|
+
export function declaresVoidReturn(p: FnProto | undefined): boolean {
|
|
271
|
+
return p?.returnsVoid === true || p?.returns?.trim() === 'void';
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/** Whether a declaration says nothing any reader acts on about the return — no spelling, no
|
|
275
|
+
* layout, and no `returnsVoid: true`. `returnsVoid: false` is silence (`declaresVoidReturn`), and
|
|
276
|
+
* it is how a manifest spells a callee it knows only the parameters of. Such an entry can take a
|
|
277
|
+
* struct return another source states without contradicting itself. */
|
|
278
|
+
export function statesNoReturn(p: FnProto): boolean {
|
|
279
|
+
return p.returns === undefined && p.returnsVoid !== true && p.returnLayout === undefined;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/** Whether a declaration says the function returns a struct or union by value — through either
|
|
283
|
+
* key that can say it (`FnProto.returnLayout`). Read through `?.` for the reason
|
|
284
|
+
* `returnsWithoutHiddenPointer` gives: a `null` entry out of parsed JSON reaches every reader. */
|
|
285
|
+
export function declaresAggregateReturn(p: FnProto | undefined): boolean {
|
|
286
|
+
// the whole spelling, qualifiers aside — `struct S *` is a pointer, returned in a register
|
|
287
|
+
const bare = (p?.returns ?? '')
|
|
288
|
+
.replace(/\b(?:const|volatile)\b/g, ' ')
|
|
289
|
+
.trim()
|
|
290
|
+
.replace(/\s+/g, ' ');
|
|
291
|
+
return p?.returnLayout !== undefined || /^(?:struct|union) [A-Za-z_]\w*$/.test(bare);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/** Bit width per C89 base type on every target asmlift lifts (all ILP32 with a 64-bit `long long`).
|
|
295
|
+
* `long` is 32 here and would not be on an LP64 host, so it is a target fact rather than a
|
|
296
|
+
* language one.
|
|
297
|
+
*
|
|
298
|
+
* THE FLOATING TYPES ARE ABSENT, and that is a measured ABI fact rather than a hole in the table.
|
|
299
|
+
* How many argument registers a `float` or a `double` occupies is a property of the TARGET and
|
|
300
|
+
* not of the spelling: `target.ts` declares `hwFloat` on three of its four descriptions, and on a
|
|
301
|
+
* PowerPC EABI with an FPU a floating argument travels in f1..f8 and occupies NO general argument
|
|
302
|
+
* register at all — compiled with the benchmark's own mwcc, `void g(int, double); void fa(int a,
|
|
303
|
+
* double b){ g(a,b); }` sets up r3 and nothing else. The one consumer that lays out argument
|
|
304
|
+
* registers would read a width here as "one register" or "a pair", and on those targets both are
|
|
305
|
+
* wrong — so `declaredCallArgs` sizes a `double` from the target, and only where it states one;
|
|
306
|
+
* `long double` is absent for the neighbouring reason, 8 bytes on these ABIs and 10 or 16
|
|
307
|
+
* on others with nothing having measured which one a target's compiler means. A spelling this
|
|
308
|
+
* table does not hold costs a caller nothing it was owed: `declaredCallArgs` abstains and the
|
|
309
|
+
* machine's own guess stands, which is what the declaration replaced.
|
|
310
|
+
*
|
|
311
|
+
* THE FIXED-WIDTH NAMES ARE FIXED BY THE STANDARD, not by a project, which is the same reason
|
|
312
|
+
* `STANDARD_SIGNATURES` exists: `int32_t` is 32 bits wherever it compiles at all, and reading it
|
|
313
|
+
* as "a project typedef" put `size_t` and `int64_t` — the spellings a header extraction produces
|
|
314
|
+
* most — into the population that cannot be sized. */
|
|
71
315
|
const BASE_WIDTHS: ReadonlyMap<string, number> = new Map([
|
|
72
316
|
['char', 8],
|
|
73
317
|
['short', 16],
|
|
@@ -75,23 +319,46 @@ const BASE_WIDTHS: ReadonlyMap<string, number> = new Map([
|
|
|
75
319
|
['int', 32],
|
|
76
320
|
['long', 32],
|
|
77
321
|
['long int', 32],
|
|
322
|
+
['long long', 64],
|
|
323
|
+
['long long int', 64],
|
|
324
|
+
['int8_t', 8],
|
|
325
|
+
['uint8_t', 8],
|
|
326
|
+
['int16_t', 16],
|
|
327
|
+
['uint16_t', 16],
|
|
328
|
+
['int32_t', 32],
|
|
329
|
+
['uint32_t', 32],
|
|
330
|
+
['int64_t', 64],
|
|
331
|
+
['uint64_t', 64],
|
|
332
|
+
['size_t', 32],
|
|
333
|
+
['ssize_t', 32],
|
|
334
|
+
['ptrdiff_t', 32],
|
|
335
|
+
['intptr_t', 32],
|
|
336
|
+
['uintptr_t', 32],
|
|
78
337
|
]);
|
|
79
338
|
|
|
80
339
|
/** The bit width one declared parameter type spells, or `undefined` for a spelling this does not
|
|
81
|
-
* read — a project typedef, a struct, a
|
|
82
|
-
* consumer treats a width it can read as authority and a width it cannot as absence, so
|
|
83
|
-
* unrecognized spelling leaves the asm's own inference standing.
|
|
340
|
+
* read — a project typedef, a by-value struct, a floating type. UNDEFINED IS "NO OPINION", never
|
|
341
|
+
* "wide": a consumer treats a width it can read as authority and a width it cannot as absence, so
|
|
342
|
+
* an unrecognized spelling leaves the asm's own inference standing. Its two readers differ in how
|
|
343
|
+
* far the absence spreads, and that is a property of what they are asking rather than of this
|
|
344
|
+
* answer: raise/paramwidth.ts abstains for that one parameter, and `declaredCallArgs` abstains
|
|
345
|
+
* for the whole list, because one unknown width moves every later argument's home.
|
|
84
346
|
*
|
|
85
|
-
* A pointer is register-wide whatever it points at
|
|
347
|
+
* A pointer is register-wide whatever it points at (`isAddress`).
|
|
348
|
+
*
|
|
349
|
+
* A WIDTH WIDER THAN A REGISTER IS A READABLE ANSWER, not an absence. `long long` and `s64`/`u64`
|
|
350
|
+
* answer 64, and that is a different fact from silence even though both refuse a narrowing: an
|
|
351
|
+
* unreadable spelling MIGHT be wide, where a `long long` IS — which is what a reader asking how
|
|
352
|
+
* many argument registers a parameter occupies has to be able to tell apart. */
|
|
86
353
|
export function declaredWidth(t: ParamType): number | undefined {
|
|
87
354
|
const s = t
|
|
88
355
|
.replace(/\b(?:const|volatile)\b/g, ' ')
|
|
89
356
|
.trim()
|
|
90
357
|
.replace(/\s+/g, ' ');
|
|
91
|
-
if (s
|
|
358
|
+
if (isAddress(s)) {
|
|
92
359
|
return 32;
|
|
93
360
|
}
|
|
94
|
-
const own = /^([su])(8|16|32)$/.exec(s);
|
|
361
|
+
const own = /^([su])(8|16|32|64)$/.exec(s);
|
|
95
362
|
if (own) {
|
|
96
363
|
return Number(own[2]);
|
|
97
364
|
}
|
|
@@ -103,12 +370,211 @@ export function declaredWidth(t: ParamType): number | undefined {
|
|
|
103
370
|
return BASE_WIDTHS.get(base === '' && s !== '' ? 'int' : base);
|
|
104
371
|
}
|
|
105
372
|
|
|
373
|
+
/** Whether the type name `s` is a pointer by its abstract declarator, the derivation nearest the
|
|
374
|
+
* absent name: `u8 *`, `void (*)(s32)`, `f32 (*)[3]`, `void (**)(void)`. A C++ pointer to a data
|
|
375
|
+
* member is one too, and a pointer to a member function is not: compiled with mwcc GC/1.2.5n,
|
|
376
|
+
* GC/1.3.2 and GC/2.6, `int T::*` is 4 bytes passed in one register, where `void (T::*)(int)` is
|
|
377
|
+
* 12, passed as the address of a copy the way a struct of that size is. A spelling with neither a
|
|
378
|
+
* `(` nor a `::` is a pointer exactly when it ends in its `*`. */
|
|
379
|
+
function isAddress(s: string): boolean {
|
|
380
|
+
if (!s.includes('(') && !s.includes('::')) {
|
|
381
|
+
return s.endsWith('*');
|
|
382
|
+
}
|
|
383
|
+
const [first, next] = parseTypeName(s)?.declarator.derivations ?? [];
|
|
384
|
+
return first?.kind === 'pointer' && !(first.member !== undefined && next?.kind === 'function');
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/** The scalar vocabulary every candidate's prelude declares: the typedef NAME, and the C89 text it
|
|
388
|
+
* stands for. `s64`/`u64` are spelt `long long` because every compiler this repo targets is a C89
|
|
389
|
+
* one with the GNU/CW extension, which is what the projects themselves use.
|
|
390
|
+
*
|
|
391
|
+
* THE DATA LIVES HERE AND `target.ts` PRINTS IT (`C_TYPEDEFS`), rather than the other way round,
|
|
392
|
+
* because this file sits BELOW that one — `target.ts` imports `runtime-helpers.ts` which imports
|
|
393
|
+
* this — and the two readers need the same answer. A COPY is what the import direction made
|
|
394
|
+
* tempting and it is a defect either way round: a name listed here and not declared there prints
|
|
395
|
+
* a type no translation unit defines, and a typedef declared there and not listed here is a
|
|
396
|
+
* spelling asmlift refuses to print for no reason. One of those two directions is a compile
|
|
397
|
+
* error in every candidate of the row, with nothing naming the cause, and a test over two lists
|
|
398
|
+
* can only ever cover the direction someone thought of — measured: deleting `s64` from the old
|
|
399
|
+
* `C_TYPEDEFS` left `proto.test.ts` green while `spellableType('s64')` still answered true. */
|
|
400
|
+
export const PRELUDE_TYPEDEFS: ReadonlyMap<string, string> = new Map([
|
|
401
|
+
['u8', 'unsigned char'],
|
|
402
|
+
['u16', 'unsigned short'],
|
|
403
|
+
['u32', 'unsigned int'],
|
|
404
|
+
['s8', 'signed char'],
|
|
405
|
+
['s16', 'short'],
|
|
406
|
+
['s32', 'int'],
|
|
407
|
+
['s64', 'long long'],
|
|
408
|
+
['u64', 'unsigned long long'],
|
|
409
|
+
]);
|
|
410
|
+
|
|
411
|
+
/** The floating typedefs a decomp project declares (`f32`, `f64`), and the C keyword each stands for.
|
|
412
|
+
* A candidate's prelude does not declare them: a candidate spells a float with the keyword
|
|
413
|
+
* (backend/cfamily.ts), so a project context that typedefs them cannot collide with it. They are
|
|
414
|
+
* read to tell that a declared spelling is floating. */
|
|
415
|
+
const FLOAT_TYPEDEFS: ReadonlyMap<string, string> = new Map([
|
|
416
|
+
['f32', 'float'],
|
|
417
|
+
['f64', 'double'],
|
|
418
|
+
]);
|
|
419
|
+
|
|
420
|
+
/** The C89 integer bases, which need no declaration anywhere. `signed`/`unsigned` is stripped
|
|
421
|
+
* before the lookup, exactly as {@link declaredWidth} strips it. */
|
|
422
|
+
const C89_INTEGER_BASES: ReadonlySet<string> = new Set([
|
|
423
|
+
'char',
|
|
424
|
+
'short',
|
|
425
|
+
'short int',
|
|
426
|
+
'int',
|
|
427
|
+
'long',
|
|
428
|
+
'long int',
|
|
429
|
+
'long long',
|
|
430
|
+
'long long int',
|
|
431
|
+
]);
|
|
432
|
+
|
|
433
|
+
/** Whether asmlift may PRINT this type text into a candidate's translation unit.
|
|
434
|
+
*
|
|
435
|
+
* SIZING AND SPELLING ARE DIFFERENT QUESTIONS AND THE SPELLABLE SET IS THE SMALLER ONE, which is
|
|
436
|
+
* the whole reason this exists beside {@link declaredWidth}. `int32_t` and `size_t` have widths the
|
|
437
|
+
* C standard fixes, so they size — and a candidate includes no header, so printing either is a
|
|
438
|
+
* parse error. `Fixed64` sizes to nothing and prints to nothing. Measured with the project agbcc:
|
|
439
|
+
* `Fixed64 DoThing(void);` and `int (*)(void) DoThing(void);` both exit 1 on a syntax error, and
|
|
440
|
+
* a candidate whose TU does not compile loses every variation with nothing naming the cause.
|
|
441
|
+
*
|
|
442
|
+
* SO THE VOCABULARY IS CLOSED AND SMALL: `void`, the C89 integer bases with any signedness, the
|
|
443
|
+
* prelude's own typedefs, and a pointer to any of those. Everything else is silence, and silence
|
|
444
|
+
* here means the callee is left undeclared exactly as it was before a prototype could declare one.
|
|
445
|
+
*
|
|
446
|
+
* A POINTER'S POINTEE STILL HAS TO BE SPELLABLE, and that is the one place this is deliberately
|
|
447
|
+
* STRICTER than the compiler: `struct Sprite * DoThing(struct Sprite *);` does compile (the tag is
|
|
448
|
+
* declared by the prototype itself), while `Fixed64 *` does not, and both size to 32 through the
|
|
449
|
+
* `*` rule — so sizing cannot tell them apart. One unspellable pointee loses every candidate for
|
|
450
|
+
* the row, so the pointer form asks the same question of what it points at and a tagged pointer
|
|
451
|
+
* goes undeclared rather than half-checked. A round that wants struct pointers declared owns
|
|
452
|
+
* proving the tag is safe to mint; `void *` is the spelling that always works. */
|
|
453
|
+
export function spellableType(t: ParamType): boolean {
|
|
454
|
+
const s = t
|
|
455
|
+
.replace(/\b(?:const|volatile)\b/g, ' ')
|
|
456
|
+
.trim()
|
|
457
|
+
.replace(/\s+/g, ' ');
|
|
458
|
+
const bare = s.replace(/[\s*]*\*$/, '').trim();
|
|
459
|
+
if (bare === '') {
|
|
460
|
+
return false;
|
|
461
|
+
}
|
|
462
|
+
if (bare === 'void' || PRELUDE_TYPEDEFS.has(bare)) {
|
|
463
|
+
return true;
|
|
464
|
+
}
|
|
465
|
+
const base = bare
|
|
466
|
+
.replace(/\b(?:signed|unsigned)\b/g, ' ')
|
|
467
|
+
.trim()
|
|
468
|
+
.replace(/\s+/g, ' ');
|
|
469
|
+
// `unsigned` alone is `unsigned int`. `unsigned Fixed64` is not a type, so a signedness keyword
|
|
470
|
+
// only ever qualifies a C89 base — a typedef that survived the set test above is rejected here.
|
|
471
|
+
return C89_INTEGER_BASES.has(base === '' && base !== bare ? 'int' : base);
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
/** The C prototype an `FnProto` states, or `undefined` where it does not state one this can print.
|
|
475
|
+
*
|
|
476
|
+
* ONE ADMISSION RULE FOR BOTH READERS OF `returns`, and that is what this function is for. The
|
|
477
|
+
* frontend reads the return WIDTH off a declaration to decide whether a callee hands back a
|
|
478
|
+
* register pair; `declare.ts` prints the declaration into the candidate's own translation unit.
|
|
479
|
+
* Those two have to admit the same table or the pipeline lifts a pair and then compiles it against
|
|
480
|
+
* an implicitly-`int` callee — measured with the project agbcc, `bl llsrc ; add r0, r1, #0` with
|
|
481
|
+
* the declaration and `bl llsrc ; mov r1,#0x20 ; asr r0,r0,r1` without it. A different program,
|
|
482
|
+
* and the row simply nonmatches with nothing saying why. So {@link declaredReturnWidth} is defined
|
|
483
|
+
* on top of this rather than beside it.
|
|
484
|
+
*
|
|
485
|
+
* COMPLETE MEANS BOTH HALVES, IN TYPES THIS CAN SPELL. A `returns` is required — a proto without
|
|
486
|
+
* one states no return type. A parameter LIST is required and every entry must be spellable; the
|
|
487
|
+
* zero-argument COUNT form is admitted too, because `params: 0` and `params: []` are the same
|
|
488
|
+
* `(void)`, but a count above zero names no C type and cannot be printed. None of those is a
|
|
489
|
+
* defect in the proto — all are the common shape — and the answer for them is what it has always
|
|
490
|
+
* been, which is to leave the callee undeclared.
|
|
491
|
+
*
|
|
492
|
+
* WHAT THIS PRINTS, THE FRONTEND MUST ALSO BE ABLE TO SIZE, and the two exclusions below are what
|
|
493
|
+
* make that true rather than nearly true. Both were reachable and both killed the whole row:
|
|
494
|
+
*
|
|
495
|
+
* `void` AS A PARAMETER. `spellableType` admits it for the RETURN, and `(void)` is C's
|
|
496
|
+
* parameterless list — but `declaredWidth('void')` is no width, so `declaredCallArgs` abstains
|
|
497
|
+
* and the call is emitted at the arg-register GUESS while the declaration beside it says the
|
|
498
|
+
* callee takes nothing. Measured with the project agbcc: `long long DoThing(void);` over
|
|
499
|
+
* `DoThing(&DoThing)` is "too many arguments to function `DoThing'", exit 1. The empty list is
|
|
500
|
+
* how a parameterless callee is stated and it already prints `(void)`.
|
|
501
|
+
*
|
|
502
|
+
* `void` AS THE RETURN, which is `returnsVoid` under the other spelling ({@link
|
|
503
|
+
* declaresVoidReturn} is where the two are one fact). The Thumb and PowerPC lowerings read the
|
|
504
|
+
* return register whatever the callee is (`CallLowering.voidReturn`), so `void DoThing(void);`
|
|
505
|
+
* printed beside a candidate that USES the result is "void value not ignored as it ought to be",
|
|
506
|
+
* exit 1 on the same compiler. It is also what the paragraph below already decided: `returnsVoid` is not a
|
|
507
|
+
* source for the printed prototype, and one fact cannot be barred under one spelling and
|
|
508
|
+
* admitted under the other. So what this prints is always a VALUE-returning prototype, which is
|
|
509
|
+
* what makes {@link declaredReturnWidth} total on its output.
|
|
510
|
+
*
|
|
511
|
+
* `returnsVoid` IS NOT A SECOND SOURCE FOR THE RETURN, and that is a measured decision rather
|
|
512
|
+
* than an oversight. `grep -rh '"returnsVoid": true' apps/benchmark/dataset/real/*.json | wc -l`
|
|
513
|
+
* prints 175, across all 8 vendored manifests. It is documented UNCHECKED data whose wrong value
|
|
514
|
+
* already turns a loud decline into a compiling wrong program, and reading it here would put that
|
|
515
|
+
* field into 175 real candidates' own translation units at a price nothing has measured — a
|
|
516
|
+
* different change, with a bench behind it, from the one this makes. `returns` states a return
|
|
517
|
+
* TYPE, nothing carries it yet, and a project that wants the prototype emitted spells it. */
|
|
518
|
+
export function spellableProto(
|
|
519
|
+
p: FnProto | undefined,
|
|
520
|
+
target: Pick<TargetDescription, 'doubleArgWords'>,
|
|
521
|
+
returned?: IrType,
|
|
522
|
+
): { readonly params: readonly ParamType[]; readonly returns: ParamType } | undefined {
|
|
523
|
+
// A struct returned through memory prints as the headers spell the struct the lift typed its call
|
|
524
|
+
// as (`returned`), which the declarations block defines beside this prototype (declare.ts). With
|
|
525
|
+
// no such call it is a return this cannot print.
|
|
526
|
+
const returns = returned?.kind === 'struct' ? (returned.declared ?? `struct ${returned.name}`) : p?.returns;
|
|
527
|
+
if (
|
|
528
|
+
p === undefined ||
|
|
529
|
+
returns === undefined ||
|
|
530
|
+
(returned === undefined && (!spellableType(returns) || declaredWidth(returns) === undefined))
|
|
531
|
+
) {
|
|
532
|
+
return undefined;
|
|
533
|
+
}
|
|
534
|
+
if (p.params === 0) {
|
|
535
|
+
return { params: [], returns };
|
|
536
|
+
}
|
|
537
|
+
// A parameter prints where the frontend lays it out (`declaredCallArgs`): a type this spells, or a
|
|
538
|
+
// `double` the target gives general words, which as a C keyword needs no typedef.
|
|
539
|
+
const laid = declaredCallArgs(p, target);
|
|
540
|
+
return Array.isArray(p.params) &&
|
|
541
|
+
laid !== undefined &&
|
|
542
|
+
p.params.every((t, i) => laid.doubles.has(i) || spellableType(t))
|
|
543
|
+
? { params: p.params, returns }
|
|
544
|
+
: undefined;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
/** The bit width a declaration states its callee RETURNS, or `undefined` when it states nothing a
|
|
548
|
+
* reader can act on — which is exactly a proto {@link spellableProto} cannot print.
|
|
549
|
+
*
|
|
550
|
+
* IT IS GATED ON BEING SPELLABLE, not merely on being sizable, and that gate is the soundness of
|
|
551
|
+
* the whole reading: a width read here makes the frontend name a second register, and that is only
|
|
552
|
+
* the right lift if the candidate's own translation unit declares the callee the same way. The two
|
|
553
|
+
* questions are asked through one function so they cannot answer differently — and the answer is
|
|
554
|
+
* the RESOLVED table (`prototypesFromSymbols`), which is where a proto the symbol map contradicts
|
|
555
|
+
* has already lost its `returns`, so the frontend and the printer see one decision.
|
|
556
|
+
*
|
|
557
|
+
* `returnsVoid` is not consulted and must not be: a void return is not a width of zero, it is the
|
|
558
|
+
* absence of a returned value, and the one consumer here asks how many registers come back with a
|
|
559
|
+
* value in them. `validatePrototypes` is what keeps the two from contradicting each other.
|
|
560
|
+
*
|
|
561
|
+
* A DESIGNATED SAFE READER, the way `declaredCallArgs` is one: a frontend indexes `prototypes` by a
|
|
562
|
+
* callee's name, and a callee named `toString` reads a `Function` off `Object.prototype` — which
|
|
563
|
+
* has no `returns`, so it answers here what an undeclared callee answers. */
|
|
564
|
+
export function declaredReturnWidth(
|
|
565
|
+
p: FnProto | undefined,
|
|
566
|
+
target: Pick<TargetDescription, 'doubleArgWords'>,
|
|
567
|
+
): number | undefined {
|
|
568
|
+
const spelled = spellableProto(p, target);
|
|
569
|
+
return spelled === undefined ? undefined : declaredWidth(spelled.returns);
|
|
570
|
+
}
|
|
571
|
+
|
|
106
572
|
/** Problems with a HAND-WRITTEN prototype table — empty when it is well formed.
|
|
107
573
|
*
|
|
108
|
-
* `
|
|
109
|
-
* is right when `params` is omitted and silent when it is mistyped: `params: "2"` then
|
|
110
|
-
* at a guessed arity, and a misspelled `returnsVoid` does nothing at all. Neither is
|
|
111
|
-
* the output, so a table that came from outside is checked before it reaches either. */
|
|
574
|
+
* `declaredCallArgs` above falls back to the arg-register heuristic on a `params` it cannot
|
|
575
|
+
* read, which is right when `params` is omitted and silent when it is mistyped: `params: "2"` then
|
|
576
|
+
* decompiles at a guessed arity, and a misspelled `returnsVoid` does nothing at all. Neither is
|
|
577
|
+
* visible in the output, so a table that came from outside is checked before it reaches either. */
|
|
112
578
|
export function validatePrototypes(value: unknown): string[] {
|
|
113
579
|
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
|
|
114
580
|
return ['must be an object mapping a symbol name to its prototype'];
|
|
@@ -120,30 +586,148 @@ export function validatePrototypes(value: unknown): string[] {
|
|
|
120
586
|
continue;
|
|
121
587
|
}
|
|
122
588
|
for (const key of Object.keys(proto)) {
|
|
123
|
-
if (key !== 'params' && key !== 'returnsVoid') {
|
|
124
|
-
problems.push(`${sym}: unknown key "${key}" (expected "params" or "
|
|
589
|
+
if (key !== 'params' && key !== 'returnsVoid' && key !== 'returns' && key !== 'returnLayout') {
|
|
590
|
+
problems.push(`${sym}: unknown key "${key}" (expected "params", "returnsVoid", "returns" or "returnLayout")`);
|
|
125
591
|
}
|
|
126
592
|
}
|
|
127
|
-
const { params, returnsVoid } = proto as {
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
593
|
+
const { params, returnsVoid, returns, returnLayout } = proto as {
|
|
594
|
+
params?: unknown;
|
|
595
|
+
returnsVoid?: unknown;
|
|
596
|
+
returns?: unknown;
|
|
597
|
+
returnLayout?: unknown;
|
|
598
|
+
};
|
|
599
|
+
if (returnLayout !== undefined) {
|
|
600
|
+
problems.push(...layoutProblems(`${sym}: "returnLayout"`, returnLayout));
|
|
601
|
+
// A layout beside a return that says `void` or spells a scalar is two answers to one question
|
|
602
|
+
// — whether the call hands the callee a hidden pointer — and neither can be picked.
|
|
603
|
+
if (
|
|
604
|
+
returnsVoid === true ||
|
|
605
|
+
(typeof returns === 'string' && (returns.trim() === 'void' || declaredWidth(returns) !== undefined))
|
|
606
|
+
) {
|
|
607
|
+
problems.push(
|
|
608
|
+
`${sym}: "returnLayout" says the return is a struct or union, and the return is also declared scalar or void`,
|
|
609
|
+
);
|
|
133
610
|
}
|
|
134
611
|
}
|
|
612
|
+
const countOk = typeof params === 'number' && Number.isInteger(params) && params >= 0;
|
|
613
|
+
const listOk = Array.isArray(params) && params.every((t) => typeof t === 'string');
|
|
614
|
+
if (params !== undefined && !countOk && !listOk) {
|
|
615
|
+
problems.push(`${sym}: "params" must be a non-negative integer or a list of type strings`);
|
|
616
|
+
}
|
|
135
617
|
if (returnsVoid !== undefined && typeof returnsVoid !== 'boolean') {
|
|
136
618
|
problems.push(`${sym}: "returnsVoid" must be a boolean`);
|
|
137
619
|
}
|
|
620
|
+
if (returns !== undefined && typeof returns !== 'string') {
|
|
621
|
+
problems.push(`${sym}: "returns" must be a type string, e.g. "long long"`);
|
|
622
|
+
}
|
|
623
|
+
// The two return keys are one fact spelled two ways (`declaresVoidReturn` is where both are
|
|
624
|
+
// read), and a table that says both DIFFERENTLY is a table whose author meant one of them.
|
|
625
|
+
// Neither reading is safe to pick: honouring `returnsVoid` would silently drop a pair the other
|
|
626
|
+
// key says comes back, and honouring `returns` would license an out-parameter frame the `void`
|
|
627
|
+
// was there to rule out. BOTH directions are a contradiction, because both are now read — an
|
|
628
|
+
// explicit `returnsVoid: false` beside `returns: "void"` is the same disagreement written the
|
|
629
|
+
// other way round, and checking one direction while claiming to check the fact is a completeness
|
|
630
|
+
// the code would not have.
|
|
631
|
+
if (
|
|
632
|
+
typeof returnsVoid === 'boolean' &&
|
|
633
|
+
typeof returns === 'string' &&
|
|
634
|
+
returnsVoid !== (returns.trim() === 'void')
|
|
635
|
+
) {
|
|
636
|
+
problems.push(`${sym}: "returnsVoid" is ${returnsVoid} but "returns" says "${returns}"`);
|
|
637
|
+
}
|
|
638
|
+
// A `returns` WIDER THAN A REGISTER IS THE ONE THAT NEEDS THE WHOLE PROTOTYPE, and a table
|
|
639
|
+
// that states one the rest of the entry cannot carry is a table whose author will see nothing
|
|
640
|
+
// happen. A register-width return is not checked here because it is not inert — it rules out a
|
|
641
|
+
// hidden struct-return pointer (`returnsWithoutHiddenPointer`) whatever else the entry says —
|
|
642
|
+
// but the register PAIR is read only where the callee's own declaration can be printed into
|
|
643
|
+
// the candidate, so a wide `returns` behind an unprintable `params`, or spelled in a type no
|
|
644
|
+
// candidate's prelude declares, buys exactly nothing and says so nowhere. Naming the gate is
|
|
645
|
+
// the point: the refusal the user then meets is `frontend/ssa.ts` reporting a destroyed
|
|
646
|
+
// register, which is true about the bytes and silent about their prototype.
|
|
647
|
+
const shaped: FnProto | undefined =
|
|
648
|
+
typeof returns === 'string' && (params === undefined || countOk || listOk)
|
|
649
|
+
? { ...(params === undefined ? {} : { params: params as number | ParamType[] }), returns }
|
|
650
|
+
: undefined;
|
|
651
|
+
// A table is checked for no target, so a `double` parameter counts as one no target lays out.
|
|
652
|
+
if (
|
|
653
|
+
shaped !== undefined &&
|
|
654
|
+
(declaredWidth(shaped.returns!) ?? 0) > 32 &&
|
|
655
|
+
spellableProto(shaped, {}) === undefined
|
|
656
|
+
) {
|
|
657
|
+
problems.push(
|
|
658
|
+
`${sym}: "returns": ${JSON.stringify(shaped.returns)} is wider than a register, and the pair it ` +
|
|
659
|
+
'comes home in is read only where the whole prototype can be printed into the candidate — so ' +
|
|
660
|
+
'"params" must be a list of spellable C types (or 0), and the return must be spelled ' +
|
|
661
|
+
`"long long" or ${[...PRELUDE_TYPEDEFS.keys()]
|
|
662
|
+
.filter((n) => (declaredWidth(n) ?? 0) > 32)
|
|
663
|
+
.map((n) => JSON.stringify(n))
|
|
664
|
+
.join('/')}`,
|
|
665
|
+
);
|
|
666
|
+
}
|
|
138
667
|
}
|
|
139
668
|
return problems;
|
|
140
669
|
}
|
|
141
670
|
|
|
671
|
+
/** Problems with one hand-written {@link AggregateLayout}, each prefixed with `where`. */
|
|
672
|
+
function layoutProblems(where: string, value: unknown): string[] {
|
|
673
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
|
|
674
|
+
return [`${where} must be an object, e.g. {"kind": "struct"}`];
|
|
675
|
+
}
|
|
676
|
+
const problems: string[] = [];
|
|
677
|
+
for (const key of Object.keys(value)) {
|
|
678
|
+
if (key !== 'kind' && key !== 'members') {
|
|
679
|
+
problems.push(`${where}: unknown key "${key}" (expected "kind" or "members")`);
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
const { kind, members } = value as { kind?: unknown; members?: unknown };
|
|
683
|
+
if (kind !== 'struct' && kind !== 'union') {
|
|
684
|
+
problems.push(`${where}: "kind" must be "struct" or "union"`);
|
|
685
|
+
}
|
|
686
|
+
if (members === undefined) {
|
|
687
|
+
return problems;
|
|
688
|
+
}
|
|
689
|
+
if (!Array.isArray(members)) {
|
|
690
|
+
return [...problems, `${where}: "members" must be a list`];
|
|
691
|
+
}
|
|
692
|
+
members.forEach((m: unknown, i) => {
|
|
693
|
+
const at = `${where}: member ${i + 1}`;
|
|
694
|
+
if (typeof m !== 'object' || m === null || Array.isArray(m)) {
|
|
695
|
+
problems.push(`${at} must be an object, e.g. {"name": "w", "type": "u32", "dims": [16]}`);
|
|
696
|
+
return;
|
|
697
|
+
}
|
|
698
|
+
for (const key of Object.keys(m)) {
|
|
699
|
+
if (key !== 'name' && key !== 'type' && key !== 'dims' && key !== 'bits') {
|
|
700
|
+
problems.push(`${at}: unknown key "${key}" (expected "name", "type", "dims" or "bits")`);
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
const { name, type, dims, bits } = m as { name?: unknown; type?: unknown; dims?: unknown; bits?: unknown };
|
|
704
|
+
const positive = (n: unknown): boolean => typeof n === 'number' && Number.isInteger(n) && n > 0;
|
|
705
|
+
if (typeof name !== 'string' || !(/^[A-Za-z_]\w*$/.test(name) || (name === '' && bits !== undefined))) {
|
|
706
|
+
problems.push(`${at}: "name" must be a C identifier (empty only for an unnamed bitfield)`);
|
|
707
|
+
}
|
|
708
|
+
if (typeof type !== 'string') {
|
|
709
|
+
problems.push(...layoutProblems(`${at}: "type"`, type));
|
|
710
|
+
}
|
|
711
|
+
if (dims !== undefined && !(Array.isArray(dims) && dims.length > 0 && dims.every(positive))) {
|
|
712
|
+
problems.push(`${at}: "dims" must be a list of positive integers`);
|
|
713
|
+
}
|
|
714
|
+
if (bits !== undefined && !(typeof bits === 'number' && Number.isInteger(bits) && bits >= 0 && bits <= 64)) {
|
|
715
|
+
problems.push(`${at}: "bits" must be an integer from 0 to 64`);
|
|
716
|
+
}
|
|
717
|
+
if (bits !== undefined && dims !== undefined) {
|
|
718
|
+
problems.push(`${at}: a bitfield has no extents`);
|
|
719
|
+
}
|
|
720
|
+
});
|
|
721
|
+
return problems;
|
|
722
|
+
}
|
|
723
|
+
|
|
142
724
|
/** The C type spelling for one declared parameter/return, or null when the facts do not
|
|
143
725
|
* determine one. A pointer is `void *` — address-identical to any object pointer, and asmlift
|
|
144
726
|
* makes every stride explicit — so nothing is guessed about what it points at. A richer spelling
|
|
145
|
-
* would
|
|
146
|
-
* are read for the
|
|
727
|
+
* would move no byte: `declaredWidth` answers 32 for every `*`, and a CALLEE's parameter types
|
|
728
|
+
* are read for the register widths they sum to (test/param-pointee-variation.test.ts). The one
|
|
729
|
+
* reader of a pointee's spelling is a C++ call argument's cast, which a header's declaration
|
|
730
|
+
* supplies (proto-context.ts, whose sized entries win over this one). */
|
|
147
731
|
function typeSpelling(t: SymbolTypeFacts): ParamType | null {
|
|
148
732
|
if (t.pointer) {
|
|
149
733
|
return 'void *';
|
|
@@ -159,17 +743,61 @@ function typeSpelling(t: SymbolTypeFacts): ParamType | null {
|
|
|
159
743
|
return null;
|
|
160
744
|
}
|
|
161
745
|
|
|
746
|
+
/** The prototype a code symbol's DWARF signature states, or `undefined` when it states none this
|
|
747
|
+
* can spell — every parameter must spell faithfully (see `prototypesFromSymbols`).
|
|
748
|
+
*
|
|
749
|
+
* A STRUCT OR UNION RETURN IS KEPT WHEN THE PARAMETERS ARE NOT, because it is what says argument 0
|
|
750
|
+
* may be a hidden pointer, and a call read without it names that pointer as the first argument.
|
|
751
|
+
* DWARF sizes the return and states no kind: a base type carries a sign and a pointer says so, so
|
|
752
|
+
* a signless non-pointer return is an enum, a struct or a union — and one wider than a word is not
|
|
753
|
+
* an enum on any target here. The layout it states has no members, which is how a call to it
|
|
754
|
+
* declines. KNOWN GAP: a signless return of a word or less is an enum or a small aggregate, and
|
|
755
|
+
* nothing here tells them apart, so it states nothing — agbcc returns `struct { u16 a, b; }` through
|
|
756
|
+
* memory, and a call storing one straight into a global lifts with the pointer as argument 0. */
|
|
757
|
+
export function symbolPrototype(info: SymbolInfo): FnProto | undefined {
|
|
758
|
+
if (info.kind !== 'code' || !info.signature) {
|
|
759
|
+
return undefined;
|
|
760
|
+
}
|
|
761
|
+
const r = info.signature.returns;
|
|
762
|
+
const aggregate = r !== null && r.signed === null && r.pointer !== true && r.size !== null && r.size > 4;
|
|
763
|
+
const layout = aggregate ? { returnLayout: { kind: 'struct' as const } } : {};
|
|
764
|
+
const params = info.signature.params.map(typeSpelling);
|
|
765
|
+
if (params.some((p) => p === null)) {
|
|
766
|
+
return aggregate ? layout : undefined;
|
|
767
|
+
}
|
|
768
|
+
return { params: params as ParamType[], ...(r === null ? { returnsVoid: true } : {}), ...layout };
|
|
769
|
+
}
|
|
770
|
+
|
|
162
771
|
/**
|
|
163
772
|
* Prototypes the project's own DWARF states, merged UNDER the caller's.
|
|
164
773
|
*
|
|
165
774
|
* A caller-supplied proto always wins: it comes from the user's headers or the benchmark
|
|
166
775
|
* manifest, and it is the thing a real user actually has for the function they are decompiling.
|
|
776
|
+
* The one fact it takes from the map is a struct return, and only when it says nothing of its own
|
|
777
|
+
* return (`statesNoReturn`).
|
|
167
778
|
* The map fills the rest — in practice the CALLEES, since a function still written in assembly
|
|
168
779
|
* has no signature in its project's ELF (see SymbolSignature).
|
|
169
780
|
*
|
|
170
781
|
* Every parameter must spell faithfully or the whole entry is dropped: a partly-typed list would
|
|
171
782
|
* be read for its LENGTH and give the right arity with the wrong widths, which is worse than the
|
|
172
783
|
* arg-register heuristic it would replace.
|
|
784
|
+
*
|
|
785
|
+
* AND IT IS WHERE THE TWO SOURCES ARE RECONCILED, not merely merged, because this is the one
|
|
786
|
+
* lookup every reader of the table goes through: `pipeline.ts` and `rank.ts` each call this once
|
|
787
|
+
* and hand the RESULT to the frontend and to the declaration collector alike. A guard put at one
|
|
788
|
+
* of those readers instead leaves the other acting on a fact it has withheld — measured, with the
|
|
789
|
+
* map calling `llsrc` a shaped data object and the proto calling it a `long long` function, the
|
|
790
|
+
* declaration was withheld and the register pair was lifted anyway, so the candidate compiled
|
|
791
|
+
* against an implicitly-`int` callee: `bl llsrc ; mov r1,#0x20 ; asr r0,r0,r1` where the target
|
|
792
|
+
* has `bl llsrc ; add r0,r1,#0`. Adding map knowledge made the tool strictly worse.
|
|
793
|
+
*
|
|
794
|
+
* A DISAGREEMENT COSTS THE `returns` AND NOTHING ELSE. The map stating a data SHAPE for a name
|
|
795
|
+
* contradicts a return type — `long long g(void);` is not a declaration of a byte the project
|
|
796
|
+
* calls `u8` — and that is the fact both readers need to lose together. The rest of the entry is
|
|
797
|
+
* untouched: `params` says how many argument registers the call occupies, which nothing about the
|
|
798
|
+
* name being data makes wrong, and dropping it would change the arity of calls nothing has
|
|
799
|
+
* measured. A `kind: 'data'` entry with no shape is not the disagreement (see l3/symbol-refs.ts:
|
|
800
|
+
* that is what a map-less lift mints for every name the IR mentions).
|
|
173
801
|
*/
|
|
174
802
|
export function prototypesFromSymbols(symbols: SymbolMap | undefined, base: Prototypes = {}): Prototypes {
|
|
175
803
|
if (!symbols) {
|
|
@@ -178,17 +806,28 @@ export function prototypesFromSymbols(symbols: SymbolMap | undefined, base: Prot
|
|
|
178
806
|
const out: Prototypes = { ...base };
|
|
179
807
|
for (const infos of symbols.values()) {
|
|
180
808
|
for (const info of infos) {
|
|
181
|
-
if (info.kind !== 'code'
|
|
182
|
-
|
|
809
|
+
if (info.kind !== 'code' && (info.shape !== undefined || info.declared === true)) {
|
|
810
|
+
const stated = Object.hasOwn(out, info.name) ? out[info.name] : undefined;
|
|
811
|
+
if (stated?.returns !== undefined) {
|
|
812
|
+
const { returns: _dropped, ...rest } = stated;
|
|
813
|
+
out[info.name] = rest;
|
|
814
|
+
}
|
|
183
815
|
}
|
|
184
|
-
const
|
|
185
|
-
|
|
816
|
+
const signed = symbolPrototype(info);
|
|
817
|
+
const held = out[info.name];
|
|
818
|
+
if (held !== undefined) {
|
|
819
|
+
// AN ENTRY THAT SAYS NOTHING OF THE RETURN STILL TAKES THE MAP'S STRUCT RETURN, which is
|
|
820
|
+
// what says argument 0 may be a hidden pointer; without it that pointer is read as the
|
|
821
|
+
// first declared argument. A header whose return spelling this could not read (`Blob64T`,
|
|
822
|
+
// defined behind an `#include`) is such an entry too.
|
|
823
|
+
if (signed?.returnLayout !== undefined && statesNoReturn(held)) {
|
|
824
|
+
out[info.name] = { ...held, returnLayout: signed.returnLayout };
|
|
825
|
+
}
|
|
186
826
|
continue;
|
|
187
827
|
}
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
};
|
|
828
|
+
if (signed !== undefined) {
|
|
829
|
+
out[info.name] = signed;
|
|
830
|
+
}
|
|
192
831
|
}
|
|
193
832
|
}
|
|
194
833
|
return out;
|