@asmlift/core 0.8.1 → 0.10.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 +36 -31
- package/package.json +1 -1
- package/src/aggregate.ts +397 -0
- package/src/backend/c.ts +6 -2
- package/src/backend/cfamily.ts +370 -56
- package/src/backend/cpp.ts +116 -10
- package/src/backend/pascal.ts +69 -11
- 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 +777 -84
- package/src/declare.ts +132 -13
- package/src/frontend/asmdata.ts +26 -6
- package/src/frontend/call-plan.ts +1590 -0
- package/src/frontend/dead-reads.ts +628 -0
- package/src/frontend/device-pins.ts +296 -0
- package/src/frontend/emit.ts +4 -3
- package/src/frontend/errors.ts +5 -0
- package/src/frontend/flags-edge.ts +101 -0
- package/src/frontend/fpu.ts +374 -0
- package/src/frontend/frame-objects.ts +3171 -0
- package/src/frontend/frontend.ts +64 -3
- package/src/frontend/high-half.ts +55 -5
- package/src/frontend/jump-table.ts +530 -0
- package/src/frontend/local-object.ts +616 -0
- package/src/frontend/mips.ts +2392 -208
- package/src/frontend/opaque.ts +97 -18
- package/src/frontend/own-struct-return.ts +500 -0
- package/src/frontend/ppc.ts +3137 -267
- package/src/frontend/register-struct-call.ts +484 -0
- package/src/frontend/reloc-symbol.ts +109 -13
- package/src/frontend/splat.ts +108 -5
- package/src/frontend/ssa.ts +2098 -89
- package/src/frontend/stackargs.ts +172 -42
- package/src/frontend/thumb.ts +4563 -2091
- package/src/ir/alias.ts +71 -0
- package/src/ir/bits.ts +681 -1
- package/src/ir/core.ts +199 -6
- package/src/ir/discipline.ts +313 -0
- package/src/ir/entry.ts +159 -0
- package/src/ir/float-bits.ts +47 -0
- package/src/{structure → ir}/loops.ts +6 -2
- package/src/ir/opcodes.ts +193 -121
- package/src/ir/parse.ts +1 -0
- package/src/ir/types.ts +202 -2
- package/src/ir/verify.ts +219 -3
- package/src/l3/address.ts +5 -0
- package/src/l3/argcopy.ts +38 -6
- package/src/l3/ast.ts +467 -23
- package/src/l3/basecse.ts +33 -5
- package/src/l3/coalesce.ts +21 -13
- package/src/l3/constinit.ts +382 -0
- package/src/l3/dce.ts +34 -12
- package/src/l3/gates.ts +9 -3
- package/src/l3/hoist.ts +8 -7
- package/src/l3/initfirst.ts +68 -19
- package/src/l3/intcell.ts +98 -0
- package/src/l3/mentions.ts +10 -4
- package/src/l3/narrowdecl.ts +259 -0
- package/src/l3/nearbase.ts +10 -5
- package/src/l3/parkfirst.ts +8 -5
- package/src/l3/pollguard.ts +11 -4
- package/src/l3/ptrcell.ts +354 -0
- package/src/l3/ptrfield.ts +123 -8
- package/src/l3/regspell.ts +26 -7
- package/src/l3/reindex.ts +293 -103
- package/src/l3/storage.ts +98 -1
- package/src/l3/symbol-refs.ts +182 -23
- package/src/l3/tailmerge.ts +9 -20
- package/src/l3/typing.ts +237 -32
- package/src/l3/unreduce.ts +10 -9
- package/src/l3/volatileptr.ts +17 -54
- package/src/l3/volstore.ts +96 -7
- package/src/mangle.ts +41 -1
- package/src/pattern/engine.ts +373 -82
- package/src/pipeline.ts +589 -27
- package/src/proto-context.ts +1648 -0
- package/src/proto.ts +1203 -49
- package/src/raise/aggmembers.ts +290 -0
- package/src/raise/arrays.ts +8 -3
- package/src/raise/declared-volatile.ts +307 -0
- package/src/raise/divpow2.ts +2 -2
- package/src/raise/errors.ts +17 -0
- package/src/raise/floathelpers.ts +464 -0
- package/src/raise/globalshape.ts +11 -5
- package/src/raise/gvn.ts +29 -8
- package/src/raise/latch.ts +162 -6
- package/src/raise/memberarrays.ts +10 -4
- package/src/raise/narrow.ts +29 -2
- package/src/raise/narrowlocal.ts +11 -6
- package/src/raise/offsetnames.ts +494 -0
- package/src/raise/pairparams.ts +114 -0
- package/src/raise/paramwidth.ts +203 -6
- package/src/raise/pre-recovery.ts +174 -17
- package/src/raise/pre-structure.ts +54 -0
- package/src/raise/recover.ts +26 -15
- package/src/raise/reroll.ts +694 -0
- package/src/raise/retsink.ts +1 -1
- package/src/raise/shortcircuit.ts +66 -30
- package/src/raise/softdiv.ts +21 -23
- package/src/raise/storesign.ts +153 -0
- package/src/raise/struct-arrays.ts +34 -14
- package/src/raise/structs.ts +674 -98
- package/src/raise/truncload.ts +367 -0
- package/src/raise/unthread.ts +400 -0
- package/src/raise/widehelpers.ts +106 -0
- package/src/rank-declare.ts +143 -12
- package/src/rank-variations.ts +117 -31
- package/src/rank.ts +563 -122
- package/src/runtime-helpers.ts +405 -0
- package/src/stillborn.ts +289 -0
- package/src/structure/analysis.ts +1449 -449
- package/src/structure/arith-ops.ts +23 -0
- package/src/structure/globalaccess.ts +111 -9
- package/src/structure/hazards.ts +641 -114
- package/src/structure/local-statics.ts +517 -0
- package/src/structure/namecoalesce.ts +2 -2
- package/src/structure/pointer-spelling.ts +1106 -0
- package/src/structure/redundant-test.ts +99 -0
- package/src/structure/structure.ts +3478 -975
- package/src/structure/switch-recover.ts +403 -178
- package/src/symbols.ts +44 -3
- package/src/target.ts +1387 -71
- package/src/trace.ts +66 -28
- package/src/variation-definitions.ts +243 -13
- package/src/variation-gates.ts +2 -0
- package/src/variation-tokens.ts +25 -7
package/src/proto.ts
CHANGED
|
@@ -1,15 +1,22 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { parseTypeName } from './cdecl/parse';
|
|
2
|
+
import type { IrType } from './ir/types';
|
|
3
|
+
import { demangle, implicitArgumentWords, spellType } from './mangle';
|
|
4
|
+
import type { SymbolInfo, SymbolMap, SymbolTypeFacts } from './symbols';
|
|
5
|
+
import type { TargetDescription } from './target';
|
|
2
6
|
|
|
3
7
|
// asmlift — function prototypes: the single carrier for the caller-supplied facts a
|
|
4
8
|
// matching-decomp project reads from its headers (arg counts, parameter widths, void-ness). One
|
|
5
9
|
// `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
|
|
10
|
+
// point of use — a callee's `params` gives the registers its call occupies, a function's own entry gives its
|
|
7
11
|
// `returnsVoid` and the widths raise/paramwidth.ts checks against. It also keeps the frontend seam
|
|
8
12
|
// honest: a frontend receives prototypes, not a grab-bag of ISA-specific options.
|
|
9
13
|
|
|
10
|
-
/** One declared parameter, as its C type text (`"u8"`, `"s32"`, `"void *"`, `"int"`).
|
|
11
|
-
*
|
|
12
|
-
* (`declaredWidth`)
|
|
14
|
+
/** One declared parameter, as its C type text (`"u8"`, `"s32"`, `"void *"`, `"int"`). The lift reads
|
|
15
|
+
* ONE fact off it and derives everything else from that fact: the WIDTH it spells
|
|
16
|
+
* (`declaredWidth`). raise/paramwidth.ts checks its inference against that width, and
|
|
17
|
+
* `declaredCallArgs` sums the list's widths into the argument registers the call occupies. On a
|
|
18
|
+
* function compiled as C++ the printer reads the SPELLING too: a call argument is cast to it where
|
|
19
|
+
* C++ converts nothing implicitly (backend/cfamily.ts `argConversion`).
|
|
13
20
|
*
|
|
14
21
|
* A DECLARED WIDTH ONLY VETOES, never pins. Where the asm carries a prologue extension the
|
|
15
22
|
* declaration contradicts, the declaration wins — it is a fact from the project's headers, where
|
|
@@ -22,11 +29,46 @@ export type ParamType = string;
|
|
|
22
29
|
/** What the headers know about one function. All fields optional: a partial table (only
|
|
23
30
|
* callee arities, or only the current function's void-ness) is the common case. */
|
|
24
31
|
export interface FnProto {
|
|
25
|
-
/** declared parameters — either
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
32
|
+
/** declared parameters — either the typed parameter list a header extraction produces
|
|
33
|
+
* (`["u8", "s32"]`), which is a list of C PARAMETERS, or a bare COUNT, which is the user's word
|
|
34
|
+
* for how many argument REGISTERS the call occupies — read so at a call. On the function's OWN
|
|
35
|
+
* entry a count bounds no argument register (`ownArgumentWords` abstains): a header's count may
|
|
36
|
+
* be of C parameters, and one of those may span any number of words. The two are the
|
|
37
|
+
* same number only while every parameter fits in a register, and `declaredCallArgs` is what
|
|
38
|
+
* converts the first into the second. Omit — or spell a parameter that conversion cannot size — to let the frontend
|
|
39
|
+
* fall back to its contiguous-arg-register heuristic. */
|
|
29
40
|
params?: number | ParamType[];
|
|
41
|
+
/** The parameters, by index into a typed `params`, whose spelling `declaredWidth` cannot size and
|
|
42
|
+
* which still travel in one register: an enum, or an array or function type a typedef name
|
|
43
|
+
* stands for, which the parameter adjusts to a pointer. A context states it, reading the types
|
|
44
|
+
* behind the names (`prototypesFromContext`). Every reader sizes such a parameter at one word
|
|
45
|
+
* (`paramClassOf`): the call lowering's widths (`declaredCallArgs`) and a frontend's frame
|
|
46
|
+
* partition alike. */
|
|
47
|
+
wordParams?: readonly number[];
|
|
48
|
+
/** The parameters, by index into a typed `params`, that are a struct, union or class passed BY
|
|
49
|
+
* VALUE: mwcc passes the address of a copy it builds in the caller's own frame, which no source
|
|
50
|
+
* declares (`frontend/ppc.ts` `measurePpcFrame`). A context states it, reading the tag behind the
|
|
51
|
+
* name (`prototypesFromContext`). A spelling neither this nor `wordParams` lists, and that
|
|
52
|
+
* nothing else sizes, is UNSIZED — a name nothing here resolved, not a struct by inference — and
|
|
53
|
+
* every reader abstains on it. */
|
|
54
|
+
byValueParams?: readonly number[];
|
|
55
|
+
/** The members of each struct or union a typed `params` list passes BY VALUE, keyed by the
|
|
56
|
+
* parameter's index: what sizes it, and so how many argument words it takes on a target that
|
|
57
|
+
* passes one in words (`compilerBehaviors.byValueAggregateArgWords`, read by `aggregate.ts`
|
|
58
|
+
* `byValueCallArgs`). A context states it from the struct's definition, as it states
|
|
59
|
+
* `returnLayout`. A by-value parameter with no layout here is unsized, and a call to it refuses
|
|
60
|
+
* (`aggregate.ts` `byValueRefusal`) rather than take the argument-register guess. */
|
|
61
|
+
byValueLayouts?: Readonly<Record<number, AggregateLayout>>;
|
|
62
|
+
/** The FIXED parameters of a VARIADIC declaration, spelled as a typed `params` list spells them.
|
|
63
|
+
* Such a declaration states no `params`, because its arity is the call's and a list would read
|
|
64
|
+
* as the whole of it; `wordParams` and `byValueParams` index into this list instead. A context
|
|
65
|
+
* states it (`prototypesFromContext`). Read for two things, and never as a layout of its own.
|
|
66
|
+
* Where each fixed parameter is one word, they are the call's first argument words, and only
|
|
67
|
+
* what the `...` takes is guessed (frontend/call-plan.ts `variadicFloor`). Where one is not, the
|
|
68
|
+
* call refuses (`variadicRefusal`): the arity guess reads argument words one by one, so a fixed
|
|
69
|
+
* parameter that is not one word is split or dropped: `void takeV(struct W, ...)` would lift
|
|
70
|
+
* `takeV(*(s32 *)&gW, 5)`, which compiles against the header. */
|
|
71
|
+
variadicParams?: ParamType[];
|
|
30
72
|
/** The declared return type is `void`. Read for the function under decompilation, where a
|
|
31
73
|
* trailing `bx lr` leaves a meaningless return register that must not surface as a `return`
|
|
32
74
|
* value — and, since the out-parameter path landed, for a CALLEE, where it is the only thing
|
|
@@ -45,29 +87,624 @@ export interface FnProto {
|
|
|
45
87
|
* Both readers see one field through two trust levels: caller-supplied on `--proto`, and
|
|
46
88
|
* machine-derived from DWARF through `prototypesFromSymbols`. Neither is distinguished here. */
|
|
47
89
|
returnsVoid?: boolean;
|
|
90
|
+
/** The declared return type, as its C type text — the same vocabulary `params` is written in and
|
|
91
|
+
* read through the same {@link declaredWidth}, because a return and a parameter spell a width
|
|
92
|
+
* the same way and two spellings of one vocabulary is where they start to disagree.
|
|
93
|
+
*
|
|
94
|
+
* WHAT IT BUYS THAT `returnsVoid` CANNOT: how many registers the callee hands BACK. A value
|
|
95
|
+
* wider than a register comes home in a register PAIR, so after a `bl` the second register holds
|
|
96
|
+
* the returned high half — a legitimate definition — where for every other callee it holds the
|
|
97
|
+
* callee's leftovers and reading it is the wrong value `frontend/ssa.ts` refuses. Those two are
|
|
98
|
+
* the same instructions, so nothing in the assembly tells them apart and the caller must be
|
|
99
|
+
* TOLD, exactly as `returnsVoid` must be.
|
|
100
|
+
*
|
|
101
|
+
* A SPELLING THIS CANNOT SIZE — OR CANNOT PRINT — IS SILENCE, never a word. {@link spellableProto}
|
|
102
|
+
* is the one gate and {@link declaredReturnWidth} is defined on top of it, so a `returns` whose
|
|
103
|
+
* width the frontend acts on is always one the candidate's own translation unit can declare;
|
|
104
|
+
* anything else lifts the call the way an undeclared callee is lifted. Under-declaring stays the
|
|
105
|
+
* safe direction here too — a return wrongly declared 64-bit names a register the callee really
|
|
106
|
+
* did destroy, which is the defect the refusal exists for.
|
|
107
|
+
*
|
|
108
|
+
* `"void"` HERE IS `returnsVoid`, not a width. The two keys are one fact and
|
|
109
|
+
* {@link declaresVoidReturn} reads both, so either spelling suppresses the phantom return value
|
|
110
|
+
* on the function's own entry and rules out a hidden struct-return pointer at a callee.
|
|
111
|
+
* `validatePrototypes` refuses a table that states them differently, in either direction.
|
|
112
|
+
*
|
|
113
|
+
* A HEADER STATES IT AND NOTHING DERIVES IT. `prototypesFromSymbols` does not fill this in:
|
|
114
|
+
* `typeSpelling` sizes 1, 2 and 4 bytes, so a DWARF-derived entry could only ever spell a return
|
|
115
|
+
* that already fits a register, where the field changes nothing. */
|
|
116
|
+
returns?: ParamType;
|
|
117
|
+
/** The declared return is a struct or union handed back BY VALUE. A caller that calls such a
|
|
118
|
+
* function may be handing it a hidden pointer to the storage the return lands in, in argument 0
|
|
119
|
+
* (agbcc thumb.h:672 STRUCT_VALUE_REGNUM 0), with every declared argument one register up
|
|
120
|
+
* (thumb.h:644-645) — so the declared `params` do not say which registers the call reads, and a
|
|
121
|
+
* lift that reads them from r0 names the hidden pointer as the first argument. A `returns` that
|
|
122
|
+
* spells `struct Tag` or `union Tag` states the same fact ({@link declaresAggregateReturn});
|
|
123
|
+
* this key also says it of a typedef name, which spells no keyword, and carries the members
|
|
124
|
+
* whose layout decides whether the target returns it through memory at all (`aggregate.ts`). */
|
|
125
|
+
returnLayout?: AggregateLayout;
|
|
126
|
+
/** ON THE FUNCTION'S OWN ENTRY, `true` says the entry is its declaration context's header entry
|
|
127
|
+
* (proto-context.ts `ownContextEntry`) and not the caller's word: it bounds the argument
|
|
128
|
+
* registers SOFTLY (pipeline.ts `liftWithinContextBound`), and a struct it returns through
|
|
129
|
+
* memory is lifted as that struct where the lift proves the hidden pointer is only its storage
|
|
130
|
+
* (pipeline.ts `returnOwnStruct`), and kept as the pointer function with a note where it does
|
|
131
|
+
* not; it is read as nothing else — not as a scalar return, not by any reader of a stated own
|
|
132
|
+
* entry. Each entry path takes it out of the table before anything reads it (proto-context.ts `splitOwnContext`). It is how a table carries the
|
|
133
|
+
* entry a `--context` lift was handed: a benchmark row publishes it so its `--proto` replay
|
|
134
|
+
* lifts as the harness did. On a callee's entry it is ignored. */
|
|
135
|
+
fromContext?: boolean;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** A struct or union a declaration spells: what it is, and its members as the header lists them.
|
|
139
|
+
* No offset or size is stored, because both are the TARGET's — agbcc rounds every struct and
|
|
140
|
+
* union to a word (thumb.h:367 STRUCTURE_SIZE_BOUNDARY 32) where mwcc does not — and are computed
|
|
141
|
+
* where a target is in hand (`aggregate.ts`). */
|
|
142
|
+
export interface AggregateLayout {
|
|
143
|
+
kind: 'struct' | 'union';
|
|
144
|
+
/** every member in declaration order; absent when one of them could not be read, which leaves
|
|
145
|
+
* the aggregate known to exist and nothing known about its shape */
|
|
146
|
+
members?: AggregateMember[];
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** One member of an {@link AggregateLayout}. */
|
|
150
|
+
export interface AggregateMember {
|
|
151
|
+
/** empty only for an unnamed bitfield, which pads */
|
|
152
|
+
name: string;
|
|
153
|
+
/** a scalar or pointer type as C spells it (`u32`, `float`, `u8 *`), an enum as `enum` and its
|
|
154
|
+
* name, or a nested struct or union */
|
|
155
|
+
type: ParamType | AggregateLayout;
|
|
156
|
+
/** an array member's extents, outermost first */
|
|
157
|
+
dims?: number[];
|
|
158
|
+
/** a bitfield's width in bits */
|
|
159
|
+
bits?: number;
|
|
48
160
|
}
|
|
49
161
|
|
|
50
162
|
/** symbol → prototype. The function under decompilation and its callees share one table. */
|
|
51
163
|
export type Prototypes = Record<string, FnProto>;
|
|
52
164
|
|
|
53
|
-
/**
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
|
|
165
|
+
/** How many argument WORDS a list of parameter WIDTHS occupies: a parameter wider than a machine
|
|
166
|
+
* word travels in a pair of words, every other one in a single word, and where the target aligns
|
|
167
|
+
* an 8-byte argument (`TargetDescription.evenWordAlignedArgs`) a pair that would start at an odd
|
|
168
|
+
* word skips one.
|
|
169
|
+
*
|
|
170
|
+
* The rule lives here rather than beside either caller because it has two, and they are the two
|
|
171
|
+
* vocabularies a declaration is written in. `runtime-helpers.ts` states a compiler's own helper
|
|
172
|
+
* signatures as widths (`__ashrdi3` is `[64, 32]` — two C parameters, three registers), and
|
|
173
|
+
* `declaredCallArgs` below reads the same widths off a project's C types. One ABI fact, one
|
|
174
|
+
* copy of it; a second copy is a rule that can disagree with itself.
|
|
175
|
+
*
|
|
176
|
+
* agbcc DOES NOT ALIGN, a measured fact rather than an omission: its `thumb.h` computes an
|
|
177
|
+
* argument's register from a plain byte counter with no rounding, so `void f(s32, long long)`
|
|
178
|
+
* passes the pair in r1:r2 where AAPCS would pad to r2:r3. O32 does, for a scalar and a struct
|
|
179
|
+
* alike, and that is the one fact both read here: `aggregate.ts` `byValueCallArgs` places a struct
|
|
180
|
+
* by this count, and the lowering that builds an o32 pair, which `frontend/mips.ts` does not yet
|
|
181
|
+
* (it refuses a declared 64-bit parameter, `CallLowering.pairs`), counts its words the same way. */
|
|
182
|
+
export function wordsOf(params: readonly number[], target: Pick<TargetDescription, 'evenWordAlignedArgs'>): number {
|
|
183
|
+
return params.reduce((n, w) => (w > 32 ? n + (target.evenWordAlignedArgs === true ? n % 2 : 0) + 2 : n + 1), 0);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/** Whether a proto states a parameter list at all — readably or not. The tier question, asked
|
|
187
|
+
* before the layout question: a project that declares a callee has re-declared it, and falling
|
|
188
|
+
* through to a compiler's runtime table or to the C standard's signatures behind a declaration
|
|
189
|
+
* nobody could SIZE would answer with a different function's shape. */
|
|
190
|
+
export function declaresParams(p: FnProto | undefined): boolean {
|
|
191
|
+
return typeof p?.params === 'number' || Array.isArray(p?.params);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/** The declaration a call to `callee` is read at — THE TIER ORDER, written once: the project's own
|
|
195
|
+
* entry where it states a parameter list (`declaresParams`), then the compiler's runtime table
|
|
196
|
+
* (`helpers`: `runtime-helpers.ts` `helperPrototypes` of the target), then the signatures the C
|
|
197
|
+
* standard fixes. `frontend/call-plan.ts` lowers every call at it, and the CLI asks it which
|
|
198
|
+
* callees a run guessed, so a callee the lowering reads a signature for is never reported as
|
|
199
|
+
* guessed, and one it guesses for always is. `Object.hasOwn` on all three tables: a callee named
|
|
200
|
+
* `toString` would otherwise read a `Function` off `Object.prototype` as its entry. */
|
|
201
|
+
export function calleeDeclaration(callee: string, prototypes: Prototypes, helpers: Prototypes): FnProto | undefined {
|
|
202
|
+
const known = (t: Prototypes) => (Object.hasOwn(t, callee) ? t[callee] : undefined);
|
|
203
|
+
const own = known(prototypes);
|
|
204
|
+
return declaresParams(own) ? own : (known(helpers) ?? known(STANDARD_SIGNATURES));
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/** What a declaration says about the argument registers a call occupies — the width each declared
|
|
208
|
+
* parameter travels in, in argument order — or `undefined` when it says nothing this can act on.
|
|
209
|
+
*
|
|
210
|
+
* THREE WAYS TO SAY NOTHING, AND THEY ARE ONE ANSWER. `params` omitted; `params` malformed (a
|
|
211
|
+
* bare `"u8"` string, the shape the untyped CLI `--proto` JSON admits); and a list holding a
|
|
212
|
+
* spelling neither `declaredWidth` reads nor the declaration sizes — a project typedef
|
|
213
|
+
* (`bool8`, `Direction`, `TaskFunc`) that `wordParams` does not list, a by-value struct, a
|
|
214
|
+
* floating type (a by-value struct is the exception below). The first two are not a reading of the
|
|
215
|
+
* list at all. The third is a list read to the end that cannot be CONVERTED: a parameter of
|
|
216
|
+
* unknown width occupies one argument register or two, and the choice moves every later argument's
|
|
217
|
+
* home. Where the declaration does settle it
|
|
218
|
+
* — a typedef'd enum, an array or a function type the context resolved to one word, which
|
|
219
|
+
* `wordParams` lists (`paramClassOf`) — the parameter is 32 bits and the list converts.
|
|
220
|
+
*
|
|
221
|
+
* SO IT ABSTAINS, AND ABSTAINING IS NEITHER A REFUSAL NOR A GUESS. The caller falls back to its
|
|
222
|
+
* own contiguous-arg-register scan — exactly what it does for a callee the project never declared
|
|
223
|
+
* — and that scan names itself a guess: `SsaBuilder.readGuessedArg` ASKS whether a register was
|
|
224
|
+
* set up, and `finish()` retracts the ones an intervening call destroyed. Refusing instead would
|
|
225
|
+
* be a function that lifts when told nothing and declines when told more. The list's LENGTH is
|
|
226
|
+
* still read: every parameter is at least one word, so a guess that counts fewer registers has
|
|
227
|
+
* dropped one, and that refuses (frontend/call-plan.ts `refuseShortOfDeclared`). Returning
|
|
228
|
+
* one-register-each instead would be a layout nothing could know to be right, and the frontend
|
|
229
|
+
* would then ASSERT those registers rather than ask for them.
|
|
230
|
+
*
|
|
231
|
+
* A PARAMETER THE DECLARATION PASSES AS A STRUCT OR UNION BY VALUE IS THE EXCEPTION, because it is
|
|
232
|
+
* known not to be one register: the caller asks `aggregate.ts` `byValueCallArgs` to place it, and
|
|
233
|
+
* where no rule does (`byValueRefusal`) the call refuses (frontend/call-plan.ts
|
|
234
|
+
* `refuseUnplacedByValue`). That is a function that declines when told more, on purpose: a guess
|
|
235
|
+
* over a struct drops it or reads its words as separate arguments, and both compile.
|
|
236
|
+
*
|
|
237
|
+
* THE MACHINE IS NOT A WITNESS FOR A WIDTH NOBODY STATED, which is the reading this replaces. The
|
|
238
|
+
* contiguous scan is wrong in both directions and neither is recoverable here. It OVER-counts:
|
|
239
|
+
* it is read before `frontend/ssa.ts` `narrowToSetupArgs` trims the registers a call sat between,
|
|
240
|
+
* so `bl __muldi3` leaves r2 and r3 counted as arguments of the NEXT call. It UNDER-counts: an
|
|
241
|
+
* incoming argument register this function passes straight through has no definition here at all,
|
|
242
|
+
* so it counts zero — measured on agbcc, `void g(MyU64); void f(MyU64 x){ g(x|1); }` leaves the
|
|
243
|
+
* high half in r1 untouched and the scan answers 1 for a call the ABI hands a pair. An equality
|
|
244
|
+
* between that scan and the one-register-each reading is therefore as likely to be a miscounted
|
|
245
|
+
* pair as a confirmation of anything, and a witness that cannot fail is not a witness.
|
|
246
|
+
*
|
|
247
|
+
* The COUNT form is the user's word for argument REGISTERS and says so at its declaration, so it
|
|
248
|
+
* expands to that many words and can never abstain. It is the way past a header this cannot size.
|
|
249
|
+
*
|
|
250
|
+
* A `double` IS A TARGET FACT. Where it crosses a call in general argument words
|
|
251
|
+
* (`TargetDescription.doubleArgWords`) it is 64 bits of words and its index is in `doubles`, so a
|
|
252
|
+
* reader can tell it from a `long long`, whose bits are the same words in another order; on a
|
|
253
|
+
* target that states no such words the list abstains. A `float` states no layout on either: it
|
|
254
|
+
* is one word on a soft-float target, but that word is a float only to a callee whose declaration
|
|
255
|
+
* the candidate prints, and `spellableProto` prints no `float` — an undeclared callee is handed a
|
|
256
|
+
* `float` promoted to a `double`, which is two words. A known spelling with no layout is not the
|
|
257
|
+
* unsized abstention, so on a target with no FPU a call to such a declaration refuses rather than
|
|
258
|
+
* guess (frontend/call-plan.ts `refuseUnplacedFloat`). */
|
|
259
|
+
export function declaredCallArgs(
|
|
260
|
+
p: FnProto | undefined,
|
|
261
|
+
target: Pick<TargetDescription, 'doubleArgWords'>,
|
|
262
|
+
): { widths: readonly number[]; doubles: ReadonlySet<number> } | undefined {
|
|
58
263
|
if (typeof p?.params === 'number') {
|
|
59
|
-
return p.params;
|
|
264
|
+
return { widths: Array.from({ length: p.params }, () => 32), doubles: new Set() };
|
|
60
265
|
}
|
|
61
|
-
if (Array.isArray(p?.params)) {
|
|
62
|
-
return
|
|
266
|
+
if (!Array.isArray(p?.params)) {
|
|
267
|
+
return undefined;
|
|
268
|
+
}
|
|
269
|
+
const widths: number[] = [];
|
|
270
|
+
const doubles = new Set<number>();
|
|
271
|
+
for (const [i, t] of p.params.entries()) {
|
|
272
|
+
const w = declaredWidth(t) ?? (paramClassOf(p, p.params, i) === 'word' ? 32 : undefined);
|
|
273
|
+
if (w !== undefined) {
|
|
274
|
+
widths.push(w);
|
|
275
|
+
} else if (target.doubleArgWords !== undefined && isDoubleSpelling(t)) {
|
|
276
|
+
doubles.add(i);
|
|
277
|
+
widths.push(64);
|
|
278
|
+
} else {
|
|
279
|
+
return undefined;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
return { widths, doubles };
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/** WHAT ONE DECLARED PARAMETER IS, as far as where a caller places it goes — the one reading every
|
|
286
|
+
* reader of a declaration takes, so the call lowering (`declaredCallArgs`) and a frontend's frame
|
|
287
|
+
* partition (`frontend/call-plan.ts` `declaredParams`) cannot give one declaration two answers: a
|
|
288
|
+
* value of one word (`declaredWidth` sizes it at 32 bits or less, or `wordParams` lists it), one
|
|
289
|
+
* wider than a word, a floating type (`isFloatingSpelling`), a struct or union passed by value
|
|
290
|
+
* (`byValueParams` lists it, or the spelling says so with its keyword: `struct B`), or UNSIZED — a
|
|
291
|
+
* spelling nothing here resolved, which states none of those and is not read as any of them. A
|
|
292
|
+
* count states words. */
|
|
293
|
+
export type ParamClass = 'word' | 'wide' | 'float' | 'by-value' | 'unsized';
|
|
294
|
+
|
|
295
|
+
/** A struct, union or class named by its keyword and tag, with no declarator: passed by value. */
|
|
296
|
+
const BY_VALUE_SPELLING = /^\s*(?:struct|union|class)\s+\w+\s*$/;
|
|
297
|
+
|
|
298
|
+
/** The class of parameter `k` of the typed list `params` that declaration `p` states (`ParamClass`). */
|
|
299
|
+
export function paramClassOf(p: FnProto, params: readonly ParamType[], k: number): ParamClass {
|
|
300
|
+
const spelling = params[k];
|
|
301
|
+
const w = declaredWidth(spelling);
|
|
302
|
+
return w !== undefined
|
|
303
|
+
? w > 32
|
|
304
|
+
? 'wide'
|
|
305
|
+
: 'word'
|
|
306
|
+
: isFloatingSpelling(spelling)
|
|
307
|
+
? 'float'
|
|
308
|
+
: p.wordParams?.includes(k)
|
|
309
|
+
? 'word'
|
|
310
|
+
: p.byValueParams?.includes(k) || BY_VALUE_SPELLING.test(spelling.replace(/\b(?:const|volatile)\b/g, ' '))
|
|
311
|
+
? 'by-value'
|
|
312
|
+
: 'unsized';
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/** Why a call to the VARIADIC declaration `p` cannot be placed by the arity guess: its first fixed
|
|
316
|
+
* parameter (`FnProto.variadicParams`, an index into it) that is not one argument word, and the
|
|
317
|
+
* reason. Undefined where `p` is no variadic declaration, or where every fixed parameter is a word
|
|
318
|
+
* or UNSIZED — a spelling nothing here resolved, which may be one word, and on which every reader
|
|
319
|
+
* abstains (`declaredCallArgs`) rather than refuse. */
|
|
320
|
+
export function variadicRefusal(p: FnProto | undefined): { refused: string; param: number } | undefined {
|
|
321
|
+
const fixed = p?.variadicParams;
|
|
322
|
+
if (fixed === undefined || p?.params !== undefined) {
|
|
323
|
+
return undefined;
|
|
324
|
+
}
|
|
325
|
+
const reasons: Partial<Record<ParamClass, string>> = {
|
|
326
|
+
'by-value': 'reads a struct or union passed by value as that many separate words',
|
|
327
|
+
wide: 'reads a value wider than a word as two separate arguments',
|
|
328
|
+
float: 'reads a floating-point value as integer words, or misses it in the FPU registers',
|
|
329
|
+
};
|
|
330
|
+
for (const k of fixed.keys()) {
|
|
331
|
+
const why = reasons[paramClassOf(p!, fixed, k)];
|
|
332
|
+
if (why !== undefined) {
|
|
333
|
+
return {
|
|
334
|
+
refused: `its declaration is variadic: the call's arguments are placed by the arity guess, which ${why}`,
|
|
335
|
+
param: k,
|
|
336
|
+
};
|
|
337
|
+
}
|
|
63
338
|
}
|
|
64
|
-
// Omitted OR malformed (e.g. a bare `"u8"` string reaching the untyped CLI `--proto` JSON):
|
|
65
|
-
// fall back to the frontend's arg-register heuristic rather than misread a string's `.length`.
|
|
66
339
|
return undefined;
|
|
67
340
|
}
|
|
68
341
|
|
|
69
|
-
/**
|
|
70
|
-
*
|
|
342
|
+
/** HOW MANY GENERAL ARGUMENT WORDS THE FUNCTION UNDER DECOMPILATION'S OWN DECLARATION FILLS — an
|
|
343
|
+
* UPPER BOUND, never an exact layout — or `undefined` where the declaration bounds nothing.
|
|
344
|
+
*
|
|
345
|
+
* Its one reader is the live-in model (`frontend/ssa.ts` `registerLiveIns`): an argument register
|
|
346
|
+
* at or past this many words is one no caller fills, so a read of it before any write is an
|
|
347
|
+
* uninitialised local the compiler homed there, never a parameter. KMC and gcc 2.7.2 home a local
|
|
348
|
+
* in `a2`/`a3` past a two-parameter declaration, agbcc in `r3` and mwcc 2.3.3 in `r5`..`r10`, and
|
|
349
|
+
* unbounded, each such read is a made-up parameter (`uninitfirst(a0, *a1, a2, a3)`).
|
|
350
|
+
*
|
|
351
|
+
* AN UPPER BOUND, because the direction of every error decides what it costs. A bound too HIGH
|
|
352
|
+
* leaves a register a parameter, as no bound at all does. A bound
|
|
353
|
+
* too LOW turns a real argument into `uninit_<reg>`, which is wrong C. So:
|
|
354
|
+
* - the declared words are laid out by {@link wordsOf}, the call's own rule: a floating parameter
|
|
355
|
+
* takes no general word where floats count on their own (`fpu.slots: 'separate'`, the PowerPC
|
|
356
|
+
* EABI), and everywhere else a floating parameter takes two, as a `long long` does (O32's
|
|
357
|
+
* leading floats still take their integer slots, and agbcc passes both in general words).
|
|
358
|
+
* A `float` takes two as well: a list cannot say whether the definition is prototyped, and an
|
|
359
|
+
* old-style one (`int k(a, b) float a; int b;`) is handed `a` promoted to `double`, so `b`
|
|
360
|
+
* sits in r2 / a2. Two words are an upper bound for a prototyped `float` too.
|
|
361
|
+
* - UP TO TWO HIDDEN WORDS ARE ADDED to a typed list where they may come first, because no
|
|
362
|
+
* parameter list states them and each pushes every declared parameter one register up. A return the
|
|
363
|
+
* declaration does not show is a scalar or `void` may be a struct handed back through a
|
|
364
|
+
* pointer in argument 0, and a symbol that may name a C++ member function is handed `this`
|
|
365
|
+
* there (`mangle.ts` `implicitArgumentWords`). Both are "may": a static member or a struct
|
|
366
|
+
* returned in registers leaves one register a parameter.
|
|
367
|
+
*
|
|
368
|
+
* THE PARAMETERS ARE THE STATED ONES, or where the declaration states none, the ones a mangled
|
|
369
|
+
* symbol records (`demangle`: `uninitfirst__FiPi` is `(int, int *)`), which is the compiler's own
|
|
370
|
+
* copy of the declaration. A BARE COUNT BOUNDS NOTHING: it may count argument registers
|
|
371
|
+
* (`FnProto.params`) or C parameters, any of which may be a pair or a struct passed by value of
|
|
372
|
+
* any size. IT ABSTAINS where neither states a list, or on one it cannot lay out: an
|
|
373
|
+
* unsized spelling (which may be two words), a struct passed by value (sized by a layout this
|
|
374
|
+
* does not read), a `long double`; and on a constructor or destructor, which
|
|
375
|
+
* may be handed a flag beside `this`. A variadic declaration states no `params` and so
|
|
376
|
+
* abstains: its arity is each caller's.
|
|
377
|
+
*
|
|
378
|
+
* `registerReturn` says the declared struct return is one the target hands back in registers, so
|
|
379
|
+
* no hidden pointer comes ahead of the parameters: the target's rule, which this module cannot
|
|
380
|
+
* read, so every reader asks through `aggregate.ts` `ownArgumentWordsOn`, which reads it as the call
|
|
381
|
+
* lowering does. */
|
|
382
|
+
export function ownArgumentWords(
|
|
383
|
+
name: string,
|
|
384
|
+
p: FnProto | undefined,
|
|
385
|
+
target: Pick<TargetDescription, 'fpu' | 'evenWordAlignedArgs'>,
|
|
386
|
+
registerReturn = false,
|
|
387
|
+
): number | undefined {
|
|
388
|
+
const own = ownArguments(name, p, target, registerReturn);
|
|
389
|
+
if (own === undefined || own.hidden === undefined) {
|
|
390
|
+
return undefined;
|
|
391
|
+
}
|
|
392
|
+
// The hidden words are laid out WITH the declared ones, not added after them: they come first,
|
|
393
|
+
// so they move where the even-word rule puts a 64-bit parameter. O32's `struct P f(double x)`
|
|
394
|
+
// passes the struct pointer in a0, skips a1 and passes `x` in a2:a3 — 4 words, where `1 +
|
|
395
|
+
// wordsOf([64])` says 3 and turns a3 into `uninit_a3`. The call side counts the same way
|
|
396
|
+
// (`frontend/call-plan.ts`, `[32, ...params]`).
|
|
397
|
+
return wordsOf([...Array.from({ length: own.hidden }, () => 32), ...own.widths], target);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/** WHETHER THE OWN DECLARATION SAYS THE CALLER FILLS ARGUMENT 0, asked of a function that RETURNS A
|
|
401
|
+
* WORD — so no hidden struct pointer is in question: `true` where a declared parameter word or
|
|
402
|
+
* `this` comes first, `false` where it states no argument word at all (`g__Fv`, a free
|
|
403
|
+
* function of none), and `undefined` where it does not say (no declaration, one {@link
|
|
404
|
+
* ownArgumentWords} cannot lay out, or no parameters on a symbol that only MAY be a method, which a
|
|
405
|
+
* static member is not). The PowerPC frontend asks it before a bare return hands back r3's entry
|
|
406
|
+
* value (`frontend/ppc.ts` `r3IsAnArgument`), and it reads the declaration the bound reads, so the
|
|
407
|
+
* two cannot give one declaration two answers. */
|
|
408
|
+
export function ownFirstArgumentPassed(
|
|
409
|
+
name: string,
|
|
410
|
+
p: FnProto | undefined,
|
|
411
|
+
target: Pick<TargetDescription, 'fpu' | 'evenWordAlignedArgs'>,
|
|
412
|
+
): boolean | undefined {
|
|
413
|
+
const own = ownArguments(name, p, target);
|
|
414
|
+
if (own === undefined) {
|
|
415
|
+
return undefined;
|
|
416
|
+
}
|
|
417
|
+
if (own.widths.length > 0 || own.passesThis === true) {
|
|
418
|
+
return true;
|
|
419
|
+
}
|
|
420
|
+
return own.passesThis === false ? false : undefined;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
/** The own declaration as both readers above read it: the declared parameter words' widths, the
|
|
424
|
+
* hidden words that may come ahead of them (`undefined` where they cannot be bounded: a
|
|
425
|
+
* constructor's or destructor's flag), and whether `this` is surely passed (`true` for a
|
|
426
|
+
* constructor or destructor, `false` for a free function, `undefined` for a symbol that only may
|
|
427
|
+
* be a method).
|
|
428
|
+
*
|
|
429
|
+
* A bare count is no declaration this can lay out, so it reads as none. */
|
|
430
|
+
function ownArguments(
|
|
431
|
+
name: string,
|
|
432
|
+
p: FnProto | undefined,
|
|
433
|
+
target: Pick<TargetDescription, 'fpu' | 'evenWordAlignedArgs'>,
|
|
434
|
+
registerReturn = false,
|
|
435
|
+
): { widths: number[]; hidden: number | undefined; passesThis: boolean | undefined } | undefined {
|
|
436
|
+
// A COUNT BOUNDS NOTHING: it may mean argument registers or C parameters, and a C parameter
|
|
437
|
+
// may be a pair or a struct passed by value of any size (`f(struct S12 s)` {params: 1} fills 3
|
|
438
|
+
// words), so no layout of it is an upper bound.
|
|
439
|
+
if (typeof p?.params === 'number') {
|
|
440
|
+
return undefined;
|
|
441
|
+
}
|
|
442
|
+
const implicit = implicitArgumentWords(name);
|
|
443
|
+
const widths = ownParamWidths(name, p ?? undefined, target);
|
|
444
|
+
if (widths === undefined) {
|
|
445
|
+
return undefined;
|
|
446
|
+
}
|
|
447
|
+
return {
|
|
448
|
+
widths,
|
|
449
|
+
hidden: implicit === undefined ? undefined : (registerReturn || showsScalarReturn(p) ? 0 : 1) + implicit,
|
|
450
|
+
passesThis: implicit === undefined ? true : implicit === 0 ? false : undefined,
|
|
451
|
+
};
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
/** Whether a declaration shows its return is a scalar or `void`, so no struct pointer is handed
|
|
455
|
+
* in ahead of its parameters: a `void`, or a stated return that is no aggregate and whose width
|
|
456
|
+
* or floating type this can read. A project typedef (`Pt`) shows nothing. */
|
|
457
|
+
function showsScalarReturn(p: FnProto | undefined): boolean {
|
|
458
|
+
return (
|
|
459
|
+
declaresVoidReturn(p) ||
|
|
460
|
+
(p?.returns !== undefined &&
|
|
461
|
+
!declaresAggregateReturn(p) &&
|
|
462
|
+
(declaredWidth(p.returns) !== undefined || isFloatingSpelling(p.returns)))
|
|
463
|
+
);
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
/** The parameter list the own declaration states, or where it states none the one a mangled symbol
|
|
467
|
+
* records (`demangle`), as {@link ownArgumentWords} reads it. */
|
|
468
|
+
function ownParamList(name: string, p: FnProto | undefined): (FnProto & { params: ParamType[] }) | undefined {
|
|
469
|
+
const mangled = Array.isArray(p?.params) ? null : demangle(name);
|
|
470
|
+
const decl: FnProto | undefined = Array.isArray(p?.params)
|
|
471
|
+
? p
|
|
472
|
+
: mangled === null
|
|
473
|
+
? undefined
|
|
474
|
+
: { params: mangled.params.map(spellType) };
|
|
475
|
+
return Array.isArray(decl?.params) ? (decl as FnProto & { params: ParamType[] }) : undefined;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
/** HOW MANY OF THE FIRST ARGUMENT REGISTERS SURELY HOLD ONE OF THE FUNCTION'S OWN PARAMETERS, each a
|
|
479
|
+
* single integer-class word — a LOWER bound, where {@link ownArgumentWords} is an upper one, and 0
|
|
480
|
+
* where the declaration says nothing surely.
|
|
481
|
+
*
|
|
482
|
+
* Its reader is the guessed-arity gap (`frontend/call-plan.ts` `decideGap`), which may read an
|
|
483
|
+
* argument register left at its incoming value as an argument passed on only where that register
|
|
484
|
+
* surely holds a parameter. So every "may" counts against it: a hidden word that may come first (a
|
|
485
|
+
* return the declaration does not show is a scalar, a symbol that may be a method) makes it 0,
|
|
486
|
+
* and the count stops at the first parameter that is not one integer word — a floating one may
|
|
487
|
+
* travel in the FPU's registers and leave its general words holding nothing (O32's leading
|
|
488
|
+
* float), a wide or unsized one takes words this does not place.
|
|
489
|
+
*
|
|
490
|
+
* {@link ownLeadingWordRegisters} asks the other question the same run of parameters answers. */
|
|
491
|
+
export function ownLeadingWordParams(
|
|
492
|
+
name: string,
|
|
493
|
+
p: FnProto | undefined,
|
|
494
|
+
target: Pick<TargetDescription, 'fpu' | 'evenWordAlignedArgs'>,
|
|
495
|
+
registerReturn = false,
|
|
496
|
+
): number {
|
|
497
|
+
const decl = ownParamList(name, p);
|
|
498
|
+
const own = ownArguments(name, p, target, registerReturn);
|
|
499
|
+
return decl === undefined || own === undefined || own.hidden !== 0 ? 0 : leadingWordRun(decl);
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
/** HOW MANY OF THE FIRST ARGUMENT REGISTERS SURELY HOLD ONE INTEGER WORD, WHOEVER'S, by the own
|
|
503
|
+
* declaration (or the parameter list a mangled symbol records), and 0 where it states no list. A
|
|
504
|
+
* hidden word (a struct return's pointer, `this`, a constructor's flag) is a word too, and only
|
|
505
|
+
* moves the parameters to later registers, so each of the first that-many registers holds a word
|
|
506
|
+
* whichever way it falls, and a later parameter this cannot place does not move the ones before
|
|
507
|
+
* it. Its reader is the Thumb frame's by-value home test (`frontend/thumb.ts` `argumentHomes`): a
|
|
508
|
+
* register that surely holds a word is no by-value struct's home. */
|
|
509
|
+
export function ownLeadingWordRegisters(name: string, p: FnProto | undefined): number {
|
|
510
|
+
const decl = ownParamList(name, p);
|
|
511
|
+
return decl === undefined ? 0 : leadingWordRun(decl);
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
/** The run of leading parameters that are each one integer-class word: it stops at the first
|
|
515
|
+
* floating one or the first {@link paramClassOf} does not call `'word'`. */
|
|
516
|
+
function leadingWordRun(decl: FnProto & { params: ParamType[] }): number {
|
|
517
|
+
let n = 0;
|
|
518
|
+
while (
|
|
519
|
+
n < decl.params.length &&
|
|
520
|
+
!isFloatingSpelling(decl.params[n]) &&
|
|
521
|
+
paramClassOf(decl, decl.params, n) === 'word'
|
|
522
|
+
) {
|
|
523
|
+
n++;
|
|
524
|
+
}
|
|
525
|
+
return n;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
/** Whether the own declaration (or the parameter list a mangled symbol records) states a floating
|
|
529
|
+
* parameter, or `undefined` where it states no list. Its reader is the guessed-arity gap
|
|
530
|
+
* (`frontend/call-plan.ts` `decidedCallRefusal`): a float may travel in an FPU register that no
|
|
531
|
+
* guess counts. */
|
|
532
|
+
export function ownDeclaresFloatingParam(name: string, p: FnProto | undefined): boolean | undefined {
|
|
533
|
+
return ownParamList(name, p)?.params.some((t) => isFloatingSpelling(t));
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
/** The widths `ownArgumentWords` lays out: one per declared parameter that takes general words. */
|
|
537
|
+
function ownParamWidths(
|
|
538
|
+
name: string,
|
|
539
|
+
p: FnProto | undefined,
|
|
540
|
+
target: Pick<TargetDescription, 'fpu'>,
|
|
541
|
+
): number[] | undefined {
|
|
542
|
+
const decl = ownParamList(name, p);
|
|
543
|
+
if (decl === undefined) {
|
|
544
|
+
return undefined;
|
|
545
|
+
}
|
|
546
|
+
const params = decl.params;
|
|
547
|
+
const widths: number[] = [];
|
|
548
|
+
for (const k of params.keys()) {
|
|
549
|
+
const cls = paramClassOf(decl, params, k);
|
|
550
|
+
if (cls === 'word' || cls === 'wide') {
|
|
551
|
+
widths.push(cls === 'word' ? 32 : (declaredWidth(params[k]) ?? 64));
|
|
552
|
+
} else if (cls === 'float') {
|
|
553
|
+
const bare = params[k]
|
|
554
|
+
.replace(/\b(?:const|volatile)\b/g, ' ')
|
|
555
|
+
.trim()
|
|
556
|
+
.replace(/\s+/g, ' ');
|
|
557
|
+
const spelled = FLOAT_TYPEDEFS.get(bare) ?? bare;
|
|
558
|
+
if (spelled === 'long double') {
|
|
559
|
+
return undefined;
|
|
560
|
+
}
|
|
561
|
+
// a `float` may be an old-style definition's, promoted to `double`
|
|
562
|
+
if (target.fpu?.slots !== 'separate') {
|
|
563
|
+
widths.push(64);
|
|
564
|
+
}
|
|
565
|
+
} else {
|
|
566
|
+
return undefined;
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
return widths;
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
/** Whether a declared type is `double`, qualifiers aside. */
|
|
573
|
+
const isDoubleSpelling = (t: ParamType): boolean => t.replace(/\b(?:const|volatile)\b/g, ' ').trim() === 'double';
|
|
574
|
+
|
|
575
|
+
/** The floating type a declared type is, qualifiers aside — a C keyword, or a typedef of one every
|
|
576
|
+
* project declares (`FLOAT_TYPEDEFS`) — or undefined for any other type. */
|
|
577
|
+
export const floatingSpelling = (t: ParamType): 'float' | 'double' | 'long double' | undefined => {
|
|
578
|
+
const bare = t
|
|
579
|
+
.replace(/\b(?:const|volatile)\b/g, ' ')
|
|
580
|
+
.trim()
|
|
581
|
+
.replace(/\s+/g, ' ');
|
|
582
|
+
const spelled = FLOAT_TYPEDEFS.get(bare) ?? bare;
|
|
583
|
+
return spelled === 'float' || spelled === 'double' || spelled === 'long double' ? spelled : undefined;
|
|
584
|
+
};
|
|
585
|
+
|
|
586
|
+
/** Whether a declared type is a floating type (`floatingSpelling`). */
|
|
587
|
+
export const isFloatingSpelling = (t: ParamType): boolean => floatingSpelling(t) !== undefined;
|
|
588
|
+
|
|
589
|
+
/** A signature the C standard fixes is a COMPLETE one, which an `FnProto` is not: a project
|
|
590
|
+
* prototype is a lower bound assembled from whatever a header extraction could read, and omits
|
|
591
|
+
* what it could not. The standard omits nothing, so the RETURN is spelled here and is required —
|
|
592
|
+
* it is the fact `returnsWithoutHiddenPointer` needs and the fact no header ever supplied. */
|
|
593
|
+
interface StandardSignature extends FnProto {
|
|
594
|
+
/** the return type, as its C spelling (`"void *"`, `"u32"`, `"void"`) */
|
|
595
|
+
returns: ParamType;
|
|
596
|
+
/** for a BLOCK COPY, the operand each role is: it reads `[0, n)` through `src`, writes `[0, n)`
|
|
597
|
+
* through `dst`, and keeps neither address (`blockCopyRoles`) */
|
|
598
|
+
copies?: BlockCopyRoles;
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
/** The operands of a standard block copy, by role (`StandardSignature.copies`). */
|
|
602
|
+
export interface BlockCopyRoles {
|
|
603
|
+
readonly dst: number;
|
|
604
|
+
readonly src: number;
|
|
605
|
+
readonly n: number;
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
/** Signatures FIXED BY THE C STANDARD, so they are not a project fact and need no header to be
|
|
609
|
+
* known. Consumed by a frontend's arity lookup behind both the caller-supplied prototype and the
|
|
610
|
+
* compiler's own runtime helpers (raise/softdiv.ts) — a project that declares one of these wins,
|
|
611
|
+
* because a decomp may legitimately be building against its own re-declaration.
|
|
612
|
+
*
|
|
613
|
+
* WHAT AN ENTRY BUYS, which is not the same as what it changes: the arity a call recovers is
|
|
614
|
+
* usually the same number the arg-register heuristic already guessed, so an entry moves no code.
|
|
615
|
+
* What it moves is what is KNOWN — a guess cannot witness anything, and an entry can. The
|
|
616
|
+
* frame-object audit reads the `returns` of one to rule out a hidden struct-return pointer
|
|
617
|
+
* (`aggregate.ts` `returnsWithoutHiddenPointer`, asked by frontend/frame-objects.ts).
|
|
618
|
+
*
|
|
619
|
+
* THE LIST IS SHORT ON PURPOSE. `memcpy` is here because a corpus row exercises it and its price
|
|
620
|
+
* was measured. `memset`, `strcpy` and the rest of the standard library are equally fixed by the
|
|
621
|
+
* standard and equally addable, and they are absent because nothing measures them — a table
|
|
622
|
+
* grown by appetite would be a table nobody priced. */
|
|
623
|
+
export const STANDARD_SIGNATURES: Record<string, StandardSignature> = {
|
|
624
|
+
memcpy: { params: ['void *', 'const void *', 'u32'], returns: 'void *', copies: { dst: 0, src: 1, n: 2 } },
|
|
625
|
+
};
|
|
626
|
+
|
|
627
|
+
/** The roles of `callee`'s operands where the C standard fixes it as a BLOCK COPY, or `undefined`.
|
|
628
|
+
* ONE TABLE for every reader: the frame audit bounds the bytes a copy with a literal count reaches
|
|
629
|
+
* (frontend/frame-objects.ts `boundedCopy`), and the constant-initializer fold reads the copy an
|
|
630
|
+
* aggregate initializer compiles to (l3/constinit.ts). `memmove` is equally fixed, and absent for
|
|
631
|
+
* the reason the list above is short. */
|
|
632
|
+
export function blockCopyRoles(callee: unknown): BlockCopyRoles | undefined {
|
|
633
|
+
return typeof callee === 'string' && Object.hasOwn(STANDARD_SIGNATURES, callee)
|
|
634
|
+
? STANDARD_SIGNATURES[callee].copies
|
|
635
|
+
: undefined;
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
/** Whether a declaration says the function returns NO VALUE — the one question `returnsVoid` was
|
|
639
|
+
* added for, asked through both keys that can answer it.
|
|
640
|
+
*
|
|
641
|
+
* THE VALIDATOR'S MESSAGE CALLS THE TWO KEYS "one fact spelled two ways", and this is what makes
|
|
642
|
+
* that true rather than a description of an intention. Before it, `returns: "void"` validated,
|
|
643
|
+
* read as interchangeable, and did nothing at all on the function under decompilation: a void
|
|
644
|
+
* function whose asm leaves garbage in r0 got `return <garbage>;` — a candidate that compiles and
|
|
645
|
+
* scores, which is the exact defect `returnsVoid` exists to prevent. Every reader of the fact
|
|
646
|
+
* calls this, so a key that validates is a key that acts.
|
|
647
|
+
*
|
|
648
|
+
* ONE DIRECTION ONLY, and that asymmetry is the field's: `returnsVoid: false` states nothing that
|
|
649
|
+
* any reader acts on (silence and "not void" take the same path), so this answers the positive
|
|
650
|
+
* claim and leaves the rest to the machine. */
|
|
651
|
+
export function declaresVoidReturn(p: FnProto | undefined): boolean {
|
|
652
|
+
return p?.returnsVoid === true || p?.returns?.trim() === 'void';
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
/** The bit width of the scalar the function UNDER DECOMPILATION declares it returns, or `undefined`
|
|
656
|
+
* where its declaration states none this can read: no `returns`, a struct or union by value, a
|
|
657
|
+
* floating type or a project typedef (`declaredWidth`'s "no opinion"). A pointer answers 32,
|
|
658
|
+
* `long long` 64.
|
|
659
|
+
*
|
|
660
|
+
* Not {@link declaredReturnWidth}, which answers for a CALLEE and only where the candidate can
|
|
661
|
+
* print its declaration: the lifted function's own signature is the one the candidate defines,
|
|
662
|
+
* so whether its parameters are spellable says nothing about what it returns. */
|
|
663
|
+
export function ownReturnWidth(p: FnProto | undefined): number | undefined {
|
|
664
|
+
return p?.returns === undefined || declaresAggregateReturn(p) ? undefined : declaredWidth(p.returns);
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
/** Whether a declaration says nothing any reader acts on about the return — no spelling, no
|
|
668
|
+
* layout, and no `returnsVoid: true`. `returnsVoid: false` is silence (`declaresVoidReturn`), and
|
|
669
|
+
* it is how a manifest spells a callee it knows only the parameters of. Such an entry can take a
|
|
670
|
+
* struct return another source states without contradicting itself. */
|
|
671
|
+
export function statesNoReturn(p: FnProto): boolean {
|
|
672
|
+
return p.returns === undefined && p.returnsVoid !== true && p.returnLayout === undefined;
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
/** Whether a declaration says the function returns a struct or union by value — through either
|
|
676
|
+
* key that can say it (`FnProto.returnLayout`). Read through `?.` for the reason
|
|
677
|
+
* `returnsWithoutHiddenPointer` gives: a `null` entry out of parsed JSON reaches every reader. */
|
|
678
|
+
export function declaresAggregateReturn(p: FnProto | undefined): boolean {
|
|
679
|
+
// the whole spelling, qualifiers aside — `struct S *` is a pointer, returned in a register
|
|
680
|
+
const bare = (p?.returns ?? '')
|
|
681
|
+
.replace(/\b(?:const|volatile)\b/g, ' ')
|
|
682
|
+
.trim()
|
|
683
|
+
.replace(/\s+/g, ' ');
|
|
684
|
+
return p?.returnLayout !== undefined || /^(?:struct|union) [A-Za-z_]\w*$/.test(bare);
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
/** Bit width per C89 base type on every target asmlift lifts (all ILP32 with a 64-bit `long long`).
|
|
688
|
+
* `long` is 32 here and would not be on an LP64 host, so it is a target fact rather than a
|
|
689
|
+
* language one.
|
|
690
|
+
*
|
|
691
|
+
* THE FLOATING TYPES ARE ABSENT, and that is a measured ABI fact rather than a hole in the table.
|
|
692
|
+
* How many argument registers a `float` or a `double` occupies is a property of the TARGET and
|
|
693
|
+
* not of the spelling: `target.ts` declares `hwFloat` on three of its four descriptions, and on a
|
|
694
|
+
* PowerPC EABI with an FPU a floating argument travels in f1..f8 and occupies NO general argument
|
|
695
|
+
* register at all — compiled with the benchmark's own mwcc, `void g(int, double); void fa(int a,
|
|
696
|
+
* double b){ g(a,b); }` sets up r3 and nothing else. The one consumer that lays out argument
|
|
697
|
+
* registers would read a width here as "one register" or "a pair", and on those targets both are
|
|
698
|
+
* wrong — so `declaredCallArgs` sizes a `double` from the target, and only where it states one;
|
|
699
|
+
* `long double` is absent for the neighbouring reason, 8 bytes on these ABIs and 10 or 16
|
|
700
|
+
* on others with nothing having measured which one a target's compiler means. A spelling this
|
|
701
|
+
* table does not hold costs a caller nothing it was owed: `declaredCallArgs` abstains and the
|
|
702
|
+
* machine's own guess stands, which is what the declaration replaced.
|
|
703
|
+
*
|
|
704
|
+
* THE FIXED-WIDTH NAMES ARE FIXED BY THE STANDARD, not by a project, which is the same reason
|
|
705
|
+
* `STANDARD_SIGNATURES` exists: `int32_t` is 32 bits wherever it compiles at all, and reading it
|
|
706
|
+
* as "a project typedef" put `size_t` and `int64_t` — the spellings a header extraction produces
|
|
707
|
+
* most — into the population that cannot be sized. */
|
|
71
708
|
const BASE_WIDTHS: ReadonlyMap<string, number> = new Map([
|
|
72
709
|
['char', 8],
|
|
73
710
|
['short', 16],
|
|
@@ -75,23 +712,46 @@ const BASE_WIDTHS: ReadonlyMap<string, number> = new Map([
|
|
|
75
712
|
['int', 32],
|
|
76
713
|
['long', 32],
|
|
77
714
|
['long int', 32],
|
|
715
|
+
['long long', 64],
|
|
716
|
+
['long long int', 64],
|
|
717
|
+
['int8_t', 8],
|
|
718
|
+
['uint8_t', 8],
|
|
719
|
+
['int16_t', 16],
|
|
720
|
+
['uint16_t', 16],
|
|
721
|
+
['int32_t', 32],
|
|
722
|
+
['uint32_t', 32],
|
|
723
|
+
['int64_t', 64],
|
|
724
|
+
['uint64_t', 64],
|
|
725
|
+
['size_t', 32],
|
|
726
|
+
['ssize_t', 32],
|
|
727
|
+
['ptrdiff_t', 32],
|
|
728
|
+
['intptr_t', 32],
|
|
729
|
+
['uintptr_t', 32],
|
|
78
730
|
]);
|
|
79
731
|
|
|
80
732
|
/** 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.
|
|
733
|
+
* read — a project typedef, a by-value struct, a floating type. UNDEFINED IS "NO OPINION", never
|
|
734
|
+
* "wide": a consumer treats a width it can read as authority and a width it cannot as absence, so
|
|
735
|
+
* an unrecognized spelling leaves the asm's own inference standing. Its two readers differ in how
|
|
736
|
+
* far the absence spreads, and that is a property of what they are asking rather than of this
|
|
737
|
+
* answer: raise/paramwidth.ts abstains for that one parameter, and `declaredCallArgs` abstains
|
|
738
|
+
* for the whole list, because one unknown width moves every later argument's home.
|
|
739
|
+
*
|
|
740
|
+
* A pointer is register-wide whatever it points at (`isAddress`).
|
|
84
741
|
*
|
|
85
|
-
* A
|
|
742
|
+
* A WIDTH WIDER THAN A REGISTER IS A READABLE ANSWER, not an absence. `long long` and `s64`/`u64`
|
|
743
|
+
* answer 64, and that is a different fact from silence even though both refuse a narrowing: an
|
|
744
|
+
* unreadable spelling MIGHT be wide, where a `long long` IS — which is what a reader asking how
|
|
745
|
+
* many argument registers a parameter occupies has to be able to tell apart. */
|
|
86
746
|
export function declaredWidth(t: ParamType): number | undefined {
|
|
87
747
|
const s = t
|
|
88
748
|
.replace(/\b(?:const|volatile)\b/g, ' ')
|
|
89
749
|
.trim()
|
|
90
750
|
.replace(/\s+/g, ' ');
|
|
91
|
-
if (s
|
|
751
|
+
if (isAddress(s)) {
|
|
92
752
|
return 32;
|
|
93
753
|
}
|
|
94
|
-
const own = /^([su])(8|16|32)$/.exec(s);
|
|
754
|
+
const own = /^([su])(8|16|32|64)$/.exec(s);
|
|
95
755
|
if (own) {
|
|
96
756
|
return Number(own[2]);
|
|
97
757
|
}
|
|
@@ -103,12 +763,253 @@ export function declaredWidth(t: ParamType): number | undefined {
|
|
|
103
763
|
return BASE_WIDTHS.get(base === '' && s !== '' ? 'int' : base);
|
|
104
764
|
}
|
|
105
765
|
|
|
766
|
+
/** Whether one declared parameter type spells a signed integer, or `undefined` where it does not say:
|
|
767
|
+
* a project typedef, a pointer, a plain `char` (whose signedness is the compiler's). */
|
|
768
|
+
export function declaredSigned(t: ParamType): boolean | undefined {
|
|
769
|
+
const s = t
|
|
770
|
+
.replace(/\b(?:const|volatile)\b/g, ' ')
|
|
771
|
+
.trim()
|
|
772
|
+
.replace(/\s+/g, ' ');
|
|
773
|
+
if (isAddress(s)) {
|
|
774
|
+
return undefined;
|
|
775
|
+
}
|
|
776
|
+
const own = /^([su])(8|16|32|64)$/.exec(s);
|
|
777
|
+
if (own) {
|
|
778
|
+
return own[1] === 's';
|
|
779
|
+
}
|
|
780
|
+
if (/\bunsigned\b/.test(s) || /^uint\d+_t$/.test(s)) {
|
|
781
|
+
return false;
|
|
782
|
+
}
|
|
783
|
+
return /\bsigned\b/.test(s) || /^int\d+_t$/.test(s) || /^(?:short|int|long)\b/.test(s) ? true : undefined;
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
/** Whether the type name `s` is a pointer by its abstract declarator, the derivation nearest the
|
|
787
|
+
* absent name: `u8 *`, `void (*)(s32)`, `f32 (*)[3]`, `void (**)(void)`. A C++ pointer to a data
|
|
788
|
+
* member is one too, and a pointer to a member function is not: compiled with mwcc GC/1.2.5n,
|
|
789
|
+
* GC/1.3.2 and GC/2.6, `int T::*` is 4 bytes passed in one register, where `void (T::*)(int)` is
|
|
790
|
+
* 12, passed as the address of a copy the way a struct of that size is. A spelling with neither a
|
|
791
|
+
* `(` nor a `::` is a pointer exactly when it ends in its `*`. */
|
|
792
|
+
function isAddress(s: string): boolean {
|
|
793
|
+
if (!s.includes('(') && !s.includes('::')) {
|
|
794
|
+
return s.endsWith('*');
|
|
795
|
+
}
|
|
796
|
+
const [first, next] = parseTypeName(s)?.declarator.derivations ?? [];
|
|
797
|
+
return first?.kind === 'pointer' && !(first.member !== undefined && next?.kind === 'function');
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
/** The scalar vocabulary every candidate's prelude declares: the typedef NAME, and the C89 text it
|
|
801
|
+
* stands for. `s64`/`u64` are spelt `long long` because every compiler this repo targets is a C89
|
|
802
|
+
* one with the GNU/CW extension, which is what the projects themselves use.
|
|
803
|
+
*
|
|
804
|
+
* THE DATA LIVES HERE AND `target.ts` PRINTS IT (`C_TYPEDEFS`), rather than the other way round,
|
|
805
|
+
* because this file sits BELOW that one — `target.ts` imports `runtime-helpers.ts` which imports
|
|
806
|
+
* this — and the two readers need the same answer. A COPY is what the import direction made
|
|
807
|
+
* tempting and it is a defect either way round: a name listed here and not declared there prints
|
|
808
|
+
* a type no translation unit defines, and a typedef declared there and not listed here is a
|
|
809
|
+
* spelling asmlift refuses to print for no reason. One of those two directions is a compile
|
|
810
|
+
* error in every candidate of the row, with nothing naming the cause, and a test over two lists
|
|
811
|
+
* can only ever cover the direction someone thought of — measured: deleting `s64` from the old
|
|
812
|
+
* `C_TYPEDEFS` left `proto.test.ts` green while `spellableType('s64')` still answered true. */
|
|
813
|
+
export const PRELUDE_TYPEDEFS: ReadonlyMap<string, string> = new Map([
|
|
814
|
+
['u8', 'unsigned char'],
|
|
815
|
+
['u16', 'unsigned short'],
|
|
816
|
+
['u32', 'unsigned int'],
|
|
817
|
+
['s8', 'signed char'],
|
|
818
|
+
['s16', 'short'],
|
|
819
|
+
['s32', 'int'],
|
|
820
|
+
['s64', 'long long'],
|
|
821
|
+
['u64', 'unsigned long long'],
|
|
822
|
+
]);
|
|
823
|
+
|
|
824
|
+
/** The floating typedefs a decomp project declares (`f32`, `f64`), and the C keyword each stands for.
|
|
825
|
+
* A candidate's prelude does not declare them: a candidate spells a float with the keyword
|
|
826
|
+
* (backend/cfamily.ts), so a project context that typedefs them cannot collide with it. They are
|
|
827
|
+
* read to tell that a declared spelling is floating. */
|
|
828
|
+
const FLOAT_TYPEDEFS: ReadonlyMap<string, string> = new Map([
|
|
829
|
+
['f32', 'float'],
|
|
830
|
+
['f64', 'double'],
|
|
831
|
+
]);
|
|
832
|
+
|
|
833
|
+
/** The C89 integer bases, which need no declaration anywhere. `signed`/`unsigned` is stripped
|
|
834
|
+
* before the lookup, exactly as {@link declaredWidth} strips it. */
|
|
835
|
+
const C89_INTEGER_BASES: ReadonlySet<string> = new Set([
|
|
836
|
+
'char',
|
|
837
|
+
'short',
|
|
838
|
+
'short int',
|
|
839
|
+
'int',
|
|
840
|
+
'long',
|
|
841
|
+
'long int',
|
|
842
|
+
'long long',
|
|
843
|
+
'long long int',
|
|
844
|
+
]);
|
|
845
|
+
|
|
846
|
+
/** Whether asmlift may PRINT this type text into a candidate's translation unit.
|
|
847
|
+
*
|
|
848
|
+
* SIZING AND SPELLING ARE DIFFERENT QUESTIONS AND THE SPELLABLE SET IS THE SMALLER ONE, which is
|
|
849
|
+
* the whole reason this exists beside {@link declaredWidth}. `int32_t` and `size_t` have widths the
|
|
850
|
+
* C standard fixes, so they size — and a candidate includes no header, so printing either is a
|
|
851
|
+
* parse error. `Fixed64` sizes to nothing and prints to nothing. Measured with the project agbcc:
|
|
852
|
+
* `Fixed64 DoThing(void);` and `int (*)(void) DoThing(void);` both exit 1 on a syntax error, and
|
|
853
|
+
* a candidate whose TU does not compile loses every variation with nothing naming the cause.
|
|
854
|
+
*
|
|
855
|
+
* SO THE VOCABULARY IS CLOSED AND SMALL: `void`, the C89 integer bases with any signedness, the
|
|
856
|
+
* prelude's own typedefs, and a pointer to any of those. Everything else is silence, and silence
|
|
857
|
+
* here means the callee is left undeclared exactly as it was before a prototype could declare one.
|
|
858
|
+
*
|
|
859
|
+
* A POINTER'S POINTEE STILL HAS TO BE SPELLABLE, and that is the one place this is deliberately
|
|
860
|
+
* STRICTER than the compiler: `struct Sprite * DoThing(struct Sprite *);` does compile (the tag is
|
|
861
|
+
* declared by the prototype itself), while `Fixed64 *` does not, and both size to 32 through the
|
|
862
|
+
* `*` rule — so sizing cannot tell them apart. One unspellable pointee loses every candidate for
|
|
863
|
+
* the row, so the pointer form asks the same question of what it points at and a tagged pointer
|
|
864
|
+
* goes undeclared rather than half-checked. A round that wants struct pointers declared owns
|
|
865
|
+
* proving the tag is safe to mint; `void *` is the spelling that always works. */
|
|
866
|
+
export function spellableType(t: ParamType): boolean {
|
|
867
|
+
const s = t
|
|
868
|
+
.replace(/\b(?:const|volatile)\b/g, ' ')
|
|
869
|
+
.trim()
|
|
870
|
+
.replace(/\s+/g, ' ');
|
|
871
|
+
const bare = s.replace(/[\s*]*\*$/, '').trim();
|
|
872
|
+
if (bare === '') {
|
|
873
|
+
return false;
|
|
874
|
+
}
|
|
875
|
+
if (bare === 'void' || PRELUDE_TYPEDEFS.has(bare)) {
|
|
876
|
+
return true;
|
|
877
|
+
}
|
|
878
|
+
const base = bare
|
|
879
|
+
.replace(/\b(?:signed|unsigned)\b/g, ' ')
|
|
880
|
+
.trim()
|
|
881
|
+
.replace(/\s+/g, ' ');
|
|
882
|
+
// `unsigned` alone is `unsigned int`. `unsigned Fixed64` is not a type, so a signedness keyword
|
|
883
|
+
// only ever qualifies a C89 base — a typedef that survived the set test above is rejected here.
|
|
884
|
+
return C89_INTEGER_BASES.has(base === '' && base !== bare ? 'int' : base);
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
/** The C prototype an `FnProto` states, or `undefined` where it does not state one this can print.
|
|
888
|
+
*
|
|
889
|
+
* ONE ADMISSION RULE FOR BOTH READERS OF `returns`, and that is what this function is for. The
|
|
890
|
+
* frontend reads the return WIDTH off a declaration to decide whether a callee hands back a
|
|
891
|
+
* register pair; `declare.ts` prints the declaration into the candidate's own translation unit.
|
|
892
|
+
* Those two have to admit the same table or the pipeline lifts a pair and then compiles it against
|
|
893
|
+
* an implicitly-`int` callee — measured with the project agbcc, `bl llsrc ; add r0, r1, #0` with
|
|
894
|
+
* the declaration and `bl llsrc ; mov r1,#0x20 ; asr r0,r0,r1` without it. A different program,
|
|
895
|
+
* and the row simply nonmatches with nothing saying why. So {@link declaredReturnWidth} is defined
|
|
896
|
+
* on top of this rather than beside it.
|
|
897
|
+
*
|
|
898
|
+
* COMPLETE MEANS BOTH HALVES, IN TYPES THIS CAN SPELL. A `returns` is required — a proto without
|
|
899
|
+
* one states no return type. A parameter LIST is required and every entry must be spellable; the
|
|
900
|
+
* zero-argument COUNT form is admitted too, because `params: 0` and `params: []` are the same
|
|
901
|
+
* `(void)`, but a count above zero names no C type and cannot be printed. None of those is a
|
|
902
|
+
* defect in the proto — all are the common shape — and the answer for them is what it has always
|
|
903
|
+
* been, which is to leave the callee undeclared.
|
|
904
|
+
*
|
|
905
|
+
* WHAT THIS PRINTS, THE FRONTEND MUST ALSO BE ABLE TO SIZE, and the two exclusions below are what
|
|
906
|
+
* make that true rather than nearly true. Both were reachable and both killed the whole row:
|
|
907
|
+
*
|
|
908
|
+
* `void` AS A PARAMETER. `spellableType` admits it for the RETURN, and `(void)` is C's
|
|
909
|
+
* parameterless list — but `declaredWidth('void')` is no width, so `declaredCallArgs` abstains
|
|
910
|
+
* and the call is emitted at the arg-register GUESS while the declaration beside it says the
|
|
911
|
+
* callee takes nothing. Measured with the project agbcc: `long long DoThing(void);` over
|
|
912
|
+
* `DoThing(&DoThing)` is "too many arguments to function `DoThing'", exit 1. The empty list is
|
|
913
|
+
* how a parameterless callee is stated and it already prints `(void)`.
|
|
914
|
+
*
|
|
915
|
+
* `void` AS THE RETURN, which is `returnsVoid` under the other spelling ({@link
|
|
916
|
+
* declaresVoidReturn} is where the two are one fact). The Thumb and PowerPC lowerings read the
|
|
917
|
+
* return register whatever the callee is (`CallLowering.voidReturn`), so `void DoThing(void);`
|
|
918
|
+
* printed beside a candidate that USES the result is "void value not ignored as it ought to be",
|
|
919
|
+
* exit 1 on the same compiler. It is also what the paragraph below already decided: `returnsVoid` is not a
|
|
920
|
+
* source for the printed prototype, and one fact cannot be barred under one spelling and
|
|
921
|
+
* admitted under the other. So what this prints is always a VALUE-returning prototype, which is
|
|
922
|
+
* what makes {@link declaredReturnWidth} total on its output.
|
|
923
|
+
*
|
|
924
|
+
* `returnsVoid` IS NOT A SECOND SOURCE FOR THE RETURN, and that is a measured decision rather
|
|
925
|
+
* than an oversight. `grep -rh '"returnsVoid": true' apps/benchmark/dataset/real/*.json | wc -l`
|
|
926
|
+
* prints 175, across all 8 vendored manifests. It is documented UNCHECKED data whose wrong value
|
|
927
|
+
* already turns a loud decline into a compiling wrong program, and reading it here would put that
|
|
928
|
+
* field into 175 real candidates' own translation units at a price nothing has measured — a
|
|
929
|
+
* different change, with a bench behind it, from the one this makes. `returns` states a return
|
|
930
|
+
* TYPE, nothing carries it yet, and a project that wants the prototype emitted spells it. */
|
|
931
|
+
export function spellableProto(
|
|
932
|
+
p: FnProto | undefined,
|
|
933
|
+
target: Pick<TargetDescription, 'doubleArgWords'>,
|
|
934
|
+
returned?: IrType,
|
|
935
|
+
byValue?: ReadonlySet<number>,
|
|
936
|
+
): { readonly params: readonly ParamType[]; readonly returns: ParamType } | undefined {
|
|
937
|
+
// A struct returned through memory prints as the headers spell the struct the lift typed its call
|
|
938
|
+
// as (`returned`), which the declarations block defines beside this prototype (declare.ts). With
|
|
939
|
+
// no such call it is a return this cannot print.
|
|
940
|
+
// A callee passed a struct by value prints whenever its declaration is complete, `returnsVoid`
|
|
941
|
+
// standing for `void`: the struct is defined only beside it, and the local the lift passes is of
|
|
942
|
+
// that type, so without it the candidate cannot declare its own local.
|
|
943
|
+
const passesStruct = byValue !== undefined && byValue.size > 0;
|
|
944
|
+
const returns =
|
|
945
|
+
returned?.kind === 'struct'
|
|
946
|
+
? (returned.declared ?? `struct ${returned.name}`)
|
|
947
|
+
: (p?.returns ?? (passesStruct && p?.returnsVoid === true ? 'void' : undefined));
|
|
948
|
+
if (
|
|
949
|
+
p === undefined ||
|
|
950
|
+
returns === undefined ||
|
|
951
|
+
(returned === undefined &&
|
|
952
|
+
!(passesStruct && returns.trim() === 'void') &&
|
|
953
|
+
(!spellableType(returns) || declaredWidth(returns) === undefined))
|
|
954
|
+
) {
|
|
955
|
+
return undefined;
|
|
956
|
+
}
|
|
957
|
+
if (p.params === 0) {
|
|
958
|
+
return { params: [], returns };
|
|
959
|
+
}
|
|
960
|
+
// A parameter prints where the frontend lays it out (`declaredCallArgs`): a type this spells, or a
|
|
961
|
+
// `double` the target gives general words, which as a C keyword needs no typedef — or a struct
|
|
962
|
+
// the lift passed by value (`byValue`, by parameter index), which the declarations block defines
|
|
963
|
+
// beside this prototype from its `byValueLayouts`, as it defines a returned one.
|
|
964
|
+
if (Array.isArray(p.params) && byValue !== undefined && passesStruct) {
|
|
965
|
+
const params = p.params;
|
|
966
|
+
const rest = params.map((t, i) => (byValue.has(i) ? 'u32' : t));
|
|
967
|
+
const laid = declaredCallArgs({ ...p, params: rest }, target);
|
|
968
|
+
return laid !== undefined &&
|
|
969
|
+
[...byValue].every((i) => p.byValueLayouts !== undefined && Object.hasOwn(p.byValueLayouts, i)) &&
|
|
970
|
+
rest.every((t, i) => laid.doubles.has(i) || spellableType(t))
|
|
971
|
+
? { params, returns }
|
|
972
|
+
: undefined;
|
|
973
|
+
}
|
|
974
|
+
const laid = declaredCallArgs(p, target);
|
|
975
|
+
return Array.isArray(p.params) &&
|
|
976
|
+
laid !== undefined &&
|
|
977
|
+
p.params.every((t, i) => laid.doubles.has(i) || spellableType(t))
|
|
978
|
+
? { params: p.params, returns }
|
|
979
|
+
: undefined;
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
/** The bit width a declaration states its callee RETURNS, or `undefined` when it states nothing a
|
|
983
|
+
* reader can act on — which is exactly a proto {@link spellableProto} cannot print.
|
|
984
|
+
*
|
|
985
|
+
* IT IS GATED ON BEING SPELLABLE, not merely on being sizable, and that gate is the soundness of
|
|
986
|
+
* the whole reading: a width read here makes the frontend name a second register, and that is only
|
|
987
|
+
* the right lift if the candidate's own translation unit declares the callee the same way. The two
|
|
988
|
+
* questions are asked through one function so they cannot answer differently — and the answer is
|
|
989
|
+
* the RESOLVED table (`prototypesFromSymbols`), which is where a proto the symbol map contradicts
|
|
990
|
+
* has already lost its `returns`, so the frontend and the printer see one decision.
|
|
991
|
+
*
|
|
992
|
+
* `returnsVoid` is not consulted and must not be: a void return is not a width of zero, it is the
|
|
993
|
+
* absence of a returned value, and the one consumer here asks how many registers come back with a
|
|
994
|
+
* value in them. `validatePrototypes` is what keeps the two from contradicting each other.
|
|
995
|
+
*
|
|
996
|
+
* A DESIGNATED SAFE READER, the way `declaredCallArgs` is one: a frontend indexes `prototypes` by a
|
|
997
|
+
* callee's name, and a callee named `toString` reads a `Function` off `Object.prototype` — which
|
|
998
|
+
* has no `returns`, so it answers here what an undeclared callee answers. */
|
|
999
|
+
export function declaredReturnWidth(
|
|
1000
|
+
p: FnProto | undefined,
|
|
1001
|
+
target: Pick<TargetDescription, 'doubleArgWords'>,
|
|
1002
|
+
): number | undefined {
|
|
1003
|
+
const spelled = spellableProto(p, target);
|
|
1004
|
+
return spelled === undefined ? undefined : declaredWidth(spelled.returns);
|
|
1005
|
+
}
|
|
1006
|
+
|
|
106
1007
|
/** Problems with a HAND-WRITTEN prototype table — empty when it is well formed.
|
|
107
1008
|
*
|
|
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. */
|
|
1009
|
+
* `declaredCallArgs` above falls back to the arg-register heuristic on a `params` it cannot
|
|
1010
|
+
* read, which is right when `params` is omitted and silent when it is mistyped: `params: "2"` then
|
|
1011
|
+
* decompiles at a guessed arity, and a misspelled `returnsVoid` does nothing at all. Neither is
|
|
1012
|
+
* visible in the output, so a table that came from outside is checked before it reaches either. */
|
|
112
1013
|
export function validatePrototypes(value: unknown): string[] {
|
|
113
1014
|
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
|
|
114
1015
|
return ['must be an object mapping a symbol name to its prototype'];
|
|
@@ -120,30 +1021,228 @@ export function validatePrototypes(value: unknown): string[] {
|
|
|
120
1021
|
continue;
|
|
121
1022
|
}
|
|
122
1023
|
for (const key of Object.keys(proto)) {
|
|
123
|
-
if (
|
|
124
|
-
|
|
1024
|
+
if (
|
|
1025
|
+
key !== 'params' &&
|
|
1026
|
+
key !== 'returnsVoid' &&
|
|
1027
|
+
key !== 'returns' &&
|
|
1028
|
+
key !== 'returnLayout' &&
|
|
1029
|
+
key !== 'wordParams' &&
|
|
1030
|
+
key !== 'byValueParams' &&
|
|
1031
|
+
key !== 'byValueLayouts' &&
|
|
1032
|
+
key !== 'variadicParams' &&
|
|
1033
|
+
key !== 'fromContext'
|
|
1034
|
+
) {
|
|
1035
|
+
problems.push(
|
|
1036
|
+
`${sym}: unknown key "${key}" (expected "params", "returnsVoid", "returns", "returnLayout", "wordParams", ` +
|
|
1037
|
+
'"byValueParams", "byValueLayouts", "variadicParams" or "fromContext")',
|
|
1038
|
+
);
|
|
125
1039
|
}
|
|
126
1040
|
}
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
1041
|
+
if ('fromContext' in proto && typeof (proto as { fromContext?: unknown }).fromContext !== 'boolean') {
|
|
1042
|
+
problems.push(`${sym}: "fromContext" must be a boolean`);
|
|
1043
|
+
}
|
|
1044
|
+
const { params, returnsVoid, returns, returnLayout, wordParams, byValueParams, byValueLayouts, variadicParams } =
|
|
1045
|
+
proto as {
|
|
1046
|
+
params?: unknown;
|
|
1047
|
+
returnsVoid?: unknown;
|
|
1048
|
+
returns?: unknown;
|
|
1049
|
+
returnLayout?: unknown;
|
|
1050
|
+
wordParams?: unknown;
|
|
1051
|
+
byValueParams?: unknown;
|
|
1052
|
+
byValueLayouts?: unknown;
|
|
1053
|
+
variadicParams?: unknown;
|
|
1054
|
+
};
|
|
1055
|
+
// a variadic declaration's fixed parameters, which it states beside no typed list
|
|
1056
|
+
if (
|
|
1057
|
+
variadicParams !== undefined &&
|
|
1058
|
+
!(Array.isArray(variadicParams) && variadicParams.every((t) => typeof t === 'string') && params === undefined)
|
|
1059
|
+
) {
|
|
1060
|
+
problems.push(`${sym}: "variadicParams" must be a list of parameter types, beside no "params"`);
|
|
1061
|
+
}
|
|
1062
|
+
const listed = Array.isArray(params) ? params : Array.isArray(variadicParams) ? variadicParams : undefined;
|
|
1063
|
+
for (const [key, list] of [
|
|
1064
|
+
['wordParams', wordParams],
|
|
1065
|
+
['byValueParams', byValueParams],
|
|
1066
|
+
] as const) {
|
|
1067
|
+
if (
|
|
1068
|
+
list !== undefined &&
|
|
1069
|
+
!(
|
|
1070
|
+
listed !== undefined &&
|
|
1071
|
+
Array.isArray(list) &&
|
|
1072
|
+
list.every((k) => Number.isInteger(k) && k >= 0 && k < listed.length)
|
|
1073
|
+
)
|
|
1074
|
+
) {
|
|
1075
|
+
problems.push(`${sym}: "${key}" must list indices into a typed "params" or "variadicParams" list`);
|
|
133
1076
|
}
|
|
134
1077
|
}
|
|
1078
|
+
// Two answers to one question — whether the parameter travels in a register or as a copy.
|
|
1079
|
+
if (
|
|
1080
|
+
Array.isArray(wordParams) &&
|
|
1081
|
+
Array.isArray(byValueParams) &&
|
|
1082
|
+
wordParams.some((k) => byValueParams.includes(k))
|
|
1083
|
+
) {
|
|
1084
|
+
problems.push(`${sym}: "wordParams" and "byValueParams" both list one parameter`);
|
|
1085
|
+
}
|
|
1086
|
+
// A layout keyed by a parameter the list does not pass by value would size a word as a struct.
|
|
1087
|
+
if (byValueLayouts !== undefined) {
|
|
1088
|
+
if (typeof byValueLayouts !== 'object' || byValueLayouts === null || Array.isArray(byValueLayouts)) {
|
|
1089
|
+
problems.push(`${sym}: "byValueLayouts" must map a parameter index to its layout`);
|
|
1090
|
+
} else {
|
|
1091
|
+
for (const [key, layout] of Object.entries(byValueLayouts)) {
|
|
1092
|
+
const k = Number(key);
|
|
1093
|
+
if (
|
|
1094
|
+
!Array.isArray(params) ||
|
|
1095
|
+
!params.every((t) => typeof t === 'string') ||
|
|
1096
|
+
!/^\d+$/.test(key) ||
|
|
1097
|
+
k >= params.length ||
|
|
1098
|
+
paramClassOf(
|
|
1099
|
+
{
|
|
1100
|
+
wordParams: Array.isArray(wordParams) ? wordParams : [],
|
|
1101
|
+
byValueParams: Array.isArray(byValueParams) ? byValueParams : [],
|
|
1102
|
+
},
|
|
1103
|
+
params,
|
|
1104
|
+
k,
|
|
1105
|
+
) !== 'by-value'
|
|
1106
|
+
) {
|
|
1107
|
+
problems.push(`${sym}: "byValueLayouts" key "${key}" is not a parameter the typed "params" pass by value`);
|
|
1108
|
+
continue;
|
|
1109
|
+
}
|
|
1110
|
+
problems.push(...layoutProblems(`${sym}: "byValueLayouts" ${key}`, layout));
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
1113
|
+
}
|
|
1114
|
+
if (returnLayout !== undefined) {
|
|
1115
|
+
problems.push(...layoutProblems(`${sym}: "returnLayout"`, returnLayout));
|
|
1116
|
+
// A layout beside a return that says `void` or spells a scalar is two answers to one question
|
|
1117
|
+
// — whether the call hands the callee a hidden pointer — and neither can be picked.
|
|
1118
|
+
if (
|
|
1119
|
+
returnsVoid === true ||
|
|
1120
|
+
(typeof returns === 'string' && (returns.trim() === 'void' || declaredWidth(returns) !== undefined))
|
|
1121
|
+
) {
|
|
1122
|
+
problems.push(
|
|
1123
|
+
`${sym}: "returnLayout" says the return is a struct or union, and the return is also declared scalar or void`,
|
|
1124
|
+
);
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
1127
|
+
const countOk = typeof params === 'number' && Number.isInteger(params) && params >= 0;
|
|
1128
|
+
const listOk = Array.isArray(params) && params.every((t) => typeof t === 'string');
|
|
1129
|
+
if (params !== undefined && !countOk && !listOk) {
|
|
1130
|
+
problems.push(`${sym}: "params" must be a non-negative integer or a list of type strings`);
|
|
1131
|
+
}
|
|
135
1132
|
if (returnsVoid !== undefined && typeof returnsVoid !== 'boolean') {
|
|
136
1133
|
problems.push(`${sym}: "returnsVoid" must be a boolean`);
|
|
137
1134
|
}
|
|
1135
|
+
if (returns !== undefined && typeof returns !== 'string') {
|
|
1136
|
+
problems.push(`${sym}: "returns" must be a type string, e.g. "long long"`);
|
|
1137
|
+
}
|
|
1138
|
+
// The two return keys are one fact spelled two ways (`declaresVoidReturn` is where both are
|
|
1139
|
+
// read), and a table that says both DIFFERENTLY is a table whose author meant one of them.
|
|
1140
|
+
// Neither reading is safe to pick: honouring `returnsVoid` would silently drop a pair the other
|
|
1141
|
+
// key says comes back, and honouring `returns` would license an out-parameter frame the `void`
|
|
1142
|
+
// was there to rule out. BOTH directions are a contradiction, because both are now read — an
|
|
1143
|
+
// explicit `returnsVoid: false` beside `returns: "void"` is the same disagreement written the
|
|
1144
|
+
// other way round, and checking one direction while claiming to check the fact is a completeness
|
|
1145
|
+
// the code would not have.
|
|
1146
|
+
if (
|
|
1147
|
+
typeof returnsVoid === 'boolean' &&
|
|
1148
|
+
typeof returns === 'string' &&
|
|
1149
|
+
returnsVoid !== (returns.trim() === 'void')
|
|
1150
|
+
) {
|
|
1151
|
+
problems.push(`${sym}: "returnsVoid" is ${returnsVoid} but "returns" says "${returns}"`);
|
|
1152
|
+
}
|
|
1153
|
+
// A `returns` WIDER THAN A REGISTER IS THE ONE THAT NEEDS THE WHOLE PROTOTYPE, and a table
|
|
1154
|
+
// that states one the rest of the entry cannot carry is a table whose author will see nothing
|
|
1155
|
+
// happen. A register-width return is not checked here because it is not inert — it rules out a
|
|
1156
|
+
// hidden struct-return pointer (`returnsWithoutHiddenPointer`) whatever else the entry says —
|
|
1157
|
+
// but the register PAIR is read only where the callee's own declaration can be printed into
|
|
1158
|
+
// the candidate, so a wide `returns` behind an unprintable `params`, or spelled in a type no
|
|
1159
|
+
// candidate's prelude declares, buys exactly nothing and says so nowhere. Naming the gate is
|
|
1160
|
+
// the point: the refusal the user then meets is `frontend/ssa.ts` reporting a destroyed
|
|
1161
|
+
// register, which is true about the bytes and silent about their prototype.
|
|
1162
|
+
const shaped: FnProto | undefined =
|
|
1163
|
+
typeof returns === 'string' && (params === undefined || countOk || listOk)
|
|
1164
|
+
? { ...(params === undefined ? {} : { params: params as number | ParamType[] }), returns }
|
|
1165
|
+
: undefined;
|
|
1166
|
+
// A table is checked for no target, so a `double` parameter counts as one no target lays out.
|
|
1167
|
+
if (
|
|
1168
|
+
shaped !== undefined &&
|
|
1169
|
+
(declaredWidth(shaped.returns!) ?? 0) > 32 &&
|
|
1170
|
+
spellableProto(shaped, {}) === undefined
|
|
1171
|
+
) {
|
|
1172
|
+
problems.push(
|
|
1173
|
+
`${sym}: "returns": ${JSON.stringify(shaped.returns)} is wider than a register, and the pair it ` +
|
|
1174
|
+
'comes home in is read only where the whole prototype can be printed into the candidate — so ' +
|
|
1175
|
+
'"params" must be a list of spellable C types (or 0), and the return must be spelled ' +
|
|
1176
|
+
`"long long" or ${[...PRELUDE_TYPEDEFS.keys()]
|
|
1177
|
+
.filter((n) => (declaredWidth(n) ?? 0) > 32)
|
|
1178
|
+
.map((n) => JSON.stringify(n))
|
|
1179
|
+
.join('/')}`,
|
|
1180
|
+
);
|
|
1181
|
+
}
|
|
138
1182
|
}
|
|
139
1183
|
return problems;
|
|
140
1184
|
}
|
|
141
1185
|
|
|
1186
|
+
/** Problems with one hand-written {@link AggregateLayout}, each prefixed with `where`. */
|
|
1187
|
+
function layoutProblems(where: string, value: unknown): string[] {
|
|
1188
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
|
|
1189
|
+
return [`${where} must be an object, e.g. {"kind": "struct"}`];
|
|
1190
|
+
}
|
|
1191
|
+
const problems: string[] = [];
|
|
1192
|
+
for (const key of Object.keys(value)) {
|
|
1193
|
+
if (key !== 'kind' && key !== 'members') {
|
|
1194
|
+
problems.push(`${where}: unknown key "${key}" (expected "kind" or "members")`);
|
|
1195
|
+
}
|
|
1196
|
+
}
|
|
1197
|
+
const { kind, members } = value as { kind?: unknown; members?: unknown };
|
|
1198
|
+
if (kind !== 'struct' && kind !== 'union') {
|
|
1199
|
+
problems.push(`${where}: "kind" must be "struct" or "union"`);
|
|
1200
|
+
}
|
|
1201
|
+
if (members === undefined) {
|
|
1202
|
+
return problems;
|
|
1203
|
+
}
|
|
1204
|
+
if (!Array.isArray(members)) {
|
|
1205
|
+
return [...problems, `${where}: "members" must be a list`];
|
|
1206
|
+
}
|
|
1207
|
+
members.forEach((m: unknown, i) => {
|
|
1208
|
+
const at = `${where}: member ${i + 1}`;
|
|
1209
|
+
if (typeof m !== 'object' || m === null || Array.isArray(m)) {
|
|
1210
|
+
problems.push(`${at} must be an object, e.g. {"name": "w", "type": "u32", "dims": [16]}`);
|
|
1211
|
+
return;
|
|
1212
|
+
}
|
|
1213
|
+
for (const key of Object.keys(m)) {
|
|
1214
|
+
if (key !== 'name' && key !== 'type' && key !== 'dims' && key !== 'bits') {
|
|
1215
|
+
problems.push(`${at}: unknown key "${key}" (expected "name", "type", "dims" or "bits")`);
|
|
1216
|
+
}
|
|
1217
|
+
}
|
|
1218
|
+
const { name, type, dims, bits } = m as { name?: unknown; type?: unknown; dims?: unknown; bits?: unknown };
|
|
1219
|
+
const positive = (n: unknown): boolean => typeof n === 'number' && Number.isInteger(n) && n > 0;
|
|
1220
|
+
if (typeof name !== 'string' || !(/^[A-Za-z_]\w*$/.test(name) || (name === '' && bits !== undefined))) {
|
|
1221
|
+
problems.push(`${at}: "name" must be a C identifier (empty only for an unnamed bitfield)`);
|
|
1222
|
+
}
|
|
1223
|
+
if (typeof type !== 'string') {
|
|
1224
|
+
problems.push(...layoutProblems(`${at}: "type"`, type));
|
|
1225
|
+
}
|
|
1226
|
+
if (dims !== undefined && !(Array.isArray(dims) && dims.length > 0 && dims.every(positive))) {
|
|
1227
|
+
problems.push(`${at}: "dims" must be a list of positive integers`);
|
|
1228
|
+
}
|
|
1229
|
+
if (bits !== undefined && !(typeof bits === 'number' && Number.isInteger(bits) && bits >= 0 && bits <= 64)) {
|
|
1230
|
+
problems.push(`${at}: "bits" must be an integer from 0 to 64`);
|
|
1231
|
+
}
|
|
1232
|
+
if (bits !== undefined && dims !== undefined) {
|
|
1233
|
+
problems.push(`${at}: a bitfield has no extents`);
|
|
1234
|
+
}
|
|
1235
|
+
});
|
|
1236
|
+
return problems;
|
|
1237
|
+
}
|
|
1238
|
+
|
|
142
1239
|
/** The C type spelling for one declared parameter/return, or null when the facts do not
|
|
143
1240
|
* determine one. A pointer is `void *` — address-identical to any object pointer, and asmlift
|
|
144
1241
|
* makes every stride explicit — so nothing is guessed about what it points at. A richer spelling
|
|
145
|
-
* would
|
|
146
|
-
* are read for the
|
|
1242
|
+
* would move no byte: `declaredWidth` answers 32 for every `*`, and a CALLEE's parameter types
|
|
1243
|
+
* are read for the register widths they sum to (test/param-pointee-variation.test.ts). The one
|
|
1244
|
+
* reader of a pointee's spelling is a C++ call argument's cast, which a header's declaration
|
|
1245
|
+
* supplies (proto-context.ts, whose sized entries win over this one). */
|
|
147
1246
|
function typeSpelling(t: SymbolTypeFacts): ParamType | null {
|
|
148
1247
|
if (t.pointer) {
|
|
149
1248
|
return 'void *';
|
|
@@ -159,17 +1258,61 @@ function typeSpelling(t: SymbolTypeFacts): ParamType | null {
|
|
|
159
1258
|
return null;
|
|
160
1259
|
}
|
|
161
1260
|
|
|
1261
|
+
/** The prototype a code symbol's DWARF signature states, or `undefined` when it states none this
|
|
1262
|
+
* can spell — every parameter must spell faithfully (see `prototypesFromSymbols`).
|
|
1263
|
+
*
|
|
1264
|
+
* A STRUCT OR UNION RETURN IS KEPT WHEN THE PARAMETERS ARE NOT, because it is what says argument 0
|
|
1265
|
+
* may be a hidden pointer, and a call read without it names that pointer as the first argument.
|
|
1266
|
+
* DWARF sizes the return and states no kind: a base type carries a sign and a pointer says so, so
|
|
1267
|
+
* a signless non-pointer return is an enum, a struct or a union — and one wider than a word is not
|
|
1268
|
+
* an enum on any target here. The layout it states has no members, which is how a call to it
|
|
1269
|
+
* declines. KNOWN GAP: a signless return of a word or less is an enum or a small aggregate, and
|
|
1270
|
+
* nothing here tells them apart, so it states nothing — agbcc returns `struct { u16 a, b; }` through
|
|
1271
|
+
* memory, and a call storing one straight into a global lifts with the pointer as argument 0. */
|
|
1272
|
+
export function symbolPrototype(info: SymbolInfo): FnProto | undefined {
|
|
1273
|
+
if (info.kind !== 'code' || !info.signature) {
|
|
1274
|
+
return undefined;
|
|
1275
|
+
}
|
|
1276
|
+
const r = info.signature.returns;
|
|
1277
|
+
const aggregate = r !== null && r.signed === null && r.pointer !== true && r.size !== null && r.size > 4;
|
|
1278
|
+
const layout = aggregate ? { returnLayout: { kind: 'struct' as const } } : {};
|
|
1279
|
+
const params = info.signature.params.map(typeSpelling);
|
|
1280
|
+
if (params.some((p) => p === null)) {
|
|
1281
|
+
return aggregate ? layout : undefined;
|
|
1282
|
+
}
|
|
1283
|
+
return { params: params as ParamType[], ...(r === null ? { returnsVoid: true } : {}), ...layout };
|
|
1284
|
+
}
|
|
1285
|
+
|
|
162
1286
|
/**
|
|
163
1287
|
* Prototypes the project's own DWARF states, merged UNDER the caller's.
|
|
164
1288
|
*
|
|
165
1289
|
* A caller-supplied proto always wins: it comes from the user's headers or the benchmark
|
|
166
1290
|
* manifest, and it is the thing a real user actually has for the function they are decompiling.
|
|
1291
|
+
* The one fact it takes from the map is a struct return, and only when it says nothing of its own
|
|
1292
|
+
* return (`statesNoReturn`).
|
|
167
1293
|
* The map fills the rest — in practice the CALLEES, since a function still written in assembly
|
|
168
1294
|
* has no signature in its project's ELF (see SymbolSignature).
|
|
169
1295
|
*
|
|
170
1296
|
* Every parameter must spell faithfully or the whole entry is dropped: a partly-typed list would
|
|
171
1297
|
* be read for its LENGTH and give the right arity with the wrong widths, which is worse than the
|
|
172
1298
|
* arg-register heuristic it would replace.
|
|
1299
|
+
*
|
|
1300
|
+
* AND IT IS WHERE THE TWO SOURCES ARE RECONCILED, not merely merged, because this is the one
|
|
1301
|
+
* lookup every reader of the table goes through: `pipeline.ts` and `rank.ts` each call this once
|
|
1302
|
+
* and hand the RESULT to the frontend and to the declaration collector alike. A guard put at one
|
|
1303
|
+
* of those readers instead leaves the other acting on a fact it has withheld — measured, with the
|
|
1304
|
+
* map calling `llsrc` a shaped data object and the proto calling it a `long long` function, the
|
|
1305
|
+
* declaration was withheld and the register pair was lifted anyway, so the candidate compiled
|
|
1306
|
+
* against an implicitly-`int` callee: `bl llsrc ; mov r1,#0x20 ; asr r0,r0,r1` where the target
|
|
1307
|
+
* has `bl llsrc ; add r0,r1,#0`. Adding map knowledge made the tool strictly worse.
|
|
1308
|
+
*
|
|
1309
|
+
* A DISAGREEMENT COSTS THE `returns` AND NOTHING ELSE. The map stating a data SHAPE for a name
|
|
1310
|
+
* contradicts a return type — `long long g(void);` is not a declaration of a byte the project
|
|
1311
|
+
* calls `u8` — and that is the fact both readers need to lose together. The rest of the entry is
|
|
1312
|
+
* untouched: `params` says how many argument registers the call occupies, which nothing about the
|
|
1313
|
+
* name being data makes wrong, and dropping it would change the arity of calls nothing has
|
|
1314
|
+
* measured. A `kind: 'data'` entry with no shape is not the disagreement (see l3/symbol-refs.ts:
|
|
1315
|
+
* that is what a map-less lift mints for every name the IR mentions).
|
|
173
1316
|
*/
|
|
174
1317
|
export function prototypesFromSymbols(symbols: SymbolMap | undefined, base: Prototypes = {}): Prototypes {
|
|
175
1318
|
if (!symbols) {
|
|
@@ -178,17 +1321,28 @@ export function prototypesFromSymbols(symbols: SymbolMap | undefined, base: Prot
|
|
|
178
1321
|
const out: Prototypes = { ...base };
|
|
179
1322
|
for (const infos of symbols.values()) {
|
|
180
1323
|
for (const info of infos) {
|
|
181
|
-
if (info.kind !== 'code'
|
|
182
|
-
|
|
1324
|
+
if (info.kind !== 'code' && (info.shape !== undefined || info.declared === true)) {
|
|
1325
|
+
const stated = Object.hasOwn(out, info.name) ? out[info.name] : undefined;
|
|
1326
|
+
if (stated?.returns !== undefined) {
|
|
1327
|
+
const { returns: _dropped, ...rest } = stated;
|
|
1328
|
+
out[info.name] = rest;
|
|
1329
|
+
}
|
|
183
1330
|
}
|
|
184
|
-
const
|
|
185
|
-
|
|
1331
|
+
const signed = symbolPrototype(info);
|
|
1332
|
+
const held = out[info.name];
|
|
1333
|
+
if (held !== undefined) {
|
|
1334
|
+
// AN ENTRY THAT SAYS NOTHING OF THE RETURN STILL TAKES THE MAP'S STRUCT RETURN, which is
|
|
1335
|
+
// what says argument 0 may be a hidden pointer; without it that pointer is read as the
|
|
1336
|
+
// first declared argument. A header whose return spelling this could not read (`Blob64T`,
|
|
1337
|
+
// defined behind an `#include`) is such an entry too.
|
|
1338
|
+
if (signed?.returnLayout !== undefined && statesNoReturn(held)) {
|
|
1339
|
+
out[info.name] = { ...held, returnLayout: signed.returnLayout };
|
|
1340
|
+
}
|
|
186
1341
|
continue;
|
|
187
1342
|
}
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
};
|
|
1343
|
+
if (signed !== undefined) {
|
|
1344
|
+
out[info.name] = signed;
|
|
1345
|
+
}
|
|
192
1346
|
}
|
|
193
1347
|
}
|
|
194
1348
|
return out;
|