@asmlift/core 0.8.1 → 0.9.0

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