@asmlift/core 0.9.0 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (117) hide show
  1. package/README.md +25 -24
  2. package/package.json +1 -1
  3. package/src/aggregate.ts +149 -0
  4. package/src/backend/c.ts +2 -0
  5. package/src/backend/cfamily.ts +197 -61
  6. package/src/backend/cpp.ts +3 -1
  7. package/src/backend/lines.ts +10 -0
  8. package/src/backend/pascal.ts +47 -16
  9. package/src/cdecl/parse.ts +21 -0
  10. package/src/contracts.ts +246 -36
  11. package/src/declare.ts +69 -20
  12. package/src/frontend/arm-body.ts +155 -0
  13. package/src/frontend/asmdata.ts +9 -2
  14. package/src/frontend/call-plan.ts +1117 -61
  15. package/src/frontend/callee-body.ts +370 -0
  16. package/src/frontend/dead-reads.ts +628 -0
  17. package/src/frontend/device-pins.ts +33 -4
  18. package/src/frontend/emit.ts +4 -3
  19. package/src/frontend/fpu.ts +224 -17
  20. package/src/frontend/frame-objects.ts +1689 -206
  21. package/src/frontend/frontend.ts +84 -3
  22. package/src/frontend/high-half.ts +53 -4
  23. package/src/frontend/jump-table.ts +545 -0
  24. package/src/frontend/local-object.ts +158 -32
  25. package/src/frontend/mips.ts +2296 -217
  26. package/src/frontend/own-struct-return.ts +500 -0
  27. package/src/frontend/ppc.ts +2748 -245
  28. package/src/frontend/register-struct-call.ts +399 -0
  29. package/src/frontend/reloc-symbol.ts +22 -7
  30. package/src/frontend/ssa.ts +1721 -43
  31. package/src/frontend/stackargs.ts +199 -72
  32. package/src/frontend/thumb-entry.ts +340 -0
  33. package/src/frontend/thumb.ts +2400 -325
  34. package/src/ir/alias.ts +65 -2
  35. package/src/ir/bits.ts +1699 -4
  36. package/src/ir/core.ts +254 -9
  37. package/src/ir/discipline.ts +86 -21
  38. package/src/ir/entry.ts +157 -0
  39. package/src/{structure → ir}/loops.ts +6 -2
  40. package/src/ir/opcodes.ts +137 -9
  41. package/src/ir/parse.ts +1 -0
  42. package/src/ir/simplify.ts +61 -10
  43. package/src/ir/types.ts +86 -2
  44. package/src/ir/verify.ts +96 -11
  45. package/src/l3/argbase.ts +2 -0
  46. package/src/l3/ast.ts +582 -18
  47. package/src/l3/basecse.ts +68 -15
  48. package/src/l3/caseendgoto.ts +223 -0
  49. package/src/l3/coalesce.ts +42 -14
  50. package/src/l3/constinit.ts +410 -0
  51. package/src/l3/dce.ts +53 -9
  52. package/src/l3/initfirst.ts +42 -11
  53. package/src/l3/intcell.ts +2 -1
  54. package/src/l3/narrowdecl.ts +77 -36
  55. package/src/l3/pollguard.ts +8 -2
  56. package/src/l3/ptrcell.ts +33 -34
  57. package/src/l3/ptrfield.ts +123 -8
  58. package/src/l3/regspell.ts +11 -4
  59. package/src/l3/reindex.ts +295 -99
  60. package/src/l3/scopebase.ts +2 -0
  61. package/src/l3/storage.ts +98 -1
  62. package/src/l3/symbol-refs.ts +84 -6
  63. package/src/l3/tailmerge.ts +6 -2
  64. package/src/l3/typing.ts +108 -15
  65. package/src/l3/unreduce.ts +6 -5
  66. package/src/l3/volatileptr.ts +17 -54
  67. package/src/mangle.ts +30 -0
  68. package/src/pattern/engine.ts +361 -73
  69. package/src/pipeline.ts +1039 -38
  70. package/src/proto-context.ts +1039 -45
  71. package/src/proto.ts +555 -40
  72. package/src/raise/aggmembers.ts +290 -0
  73. package/src/raise/arrays.ts +5 -1
  74. package/src/raise/const.ts +25 -0
  75. package/src/raise/declared-volatile.ts +207 -31
  76. package/src/raise/divpow2.ts +192 -0
  77. package/src/raise/floathelpers.ts +254 -33
  78. package/src/raise/gvn.ts +3 -3
  79. package/src/raise/latch.ts +162 -6
  80. package/src/raise/memberarrays.ts +10 -4
  81. package/src/raise/offsetnames.ts +422 -36
  82. package/src/raise/pairparams.ts +33 -0
  83. package/src/raise/paramwidth.ts +196 -6
  84. package/src/raise/pre-recovery.ts +107 -21
  85. package/src/raise/pre-structure.ts +73 -0
  86. package/src/raise/recover.ts +12 -5
  87. package/src/raise/reroll.ts +674 -0
  88. package/src/raise/shortcircuit.ts +217 -34
  89. package/src/raise/softdiv.ts +1 -1
  90. package/src/raise/storesign.ts +153 -0
  91. package/src/raise/struct-arrays.ts +30 -11
  92. package/src/raise/structs.ts +396 -33
  93. package/src/raise/truncload.ts +4 -3
  94. package/src/raise/unpre.ts +594 -0
  95. package/src/raise/unthread.ts +400 -0
  96. package/src/rank-declare.ts +118 -12
  97. package/src/rank-variations.ts +186 -35
  98. package/src/rank.ts +536 -68
  99. package/src/runtime-helpers.ts +140 -22
  100. package/src/structure/analysis.ts +217 -8
  101. package/src/structure/arith-ops.ts +23 -0
  102. package/src/structure/cross-jumped-tail.ts +216 -0
  103. package/src/structure/globalaccess.ts +157 -10
  104. package/src/structure/hazards.ts +16 -64
  105. package/src/structure/local-statics.ts +145 -9
  106. package/src/structure/namecoalesce.ts +1 -1
  107. package/src/structure/pointer-spelling.ts +1106 -0
  108. package/src/structure/redundant-test.ts +1 -1
  109. package/src/structure/structure.ts +6192 -1213
  110. package/src/structure/switch-recover.ts +583 -120
  111. package/src/structure/unfolded-branch-constant.ts +90 -0
  112. package/src/symbols.ts +58 -15
  113. package/src/target.ts +1028 -54
  114. package/src/trace.ts +49 -17
  115. package/src/variation-definitions.ts +333 -20
  116. package/src/variation-gates.ts +4 -0
  117. package/src/variation-tokens.ts +68 -3
@@ -7,7 +7,6 @@ import {
7
7
  type DeclaredType,
8
8
  type Derivation,
9
9
  type Language,
10
- type Parameter,
11
10
  type ParsedContext,
12
11
  type Qualifier,
13
12
  type Range,
@@ -17,17 +16,21 @@ import {
17
16
  parseDeclarations,
18
17
  } from './cdecl/parse';
19
18
  import { asParameter, spellType } from './cdecl/spell';
19
+ import { demangledName, implicitArgumentWords } from './mangle';
20
20
  import type { AggregateLayout, AggregateMember, FnProto, ParamType, Prototypes } from './proto';
21
21
  import {
22
22
  declaredCallArgs,
23
+ declaredSigned,
23
24
  declaredWidth,
24
25
  declaresAggregateReturn,
26
+ declaresVoidReturn,
25
27
  isFloatingSpelling,
28
+ paramClassOf,
26
29
  statesNoReturn,
27
30
  symbolPrototype,
28
31
  validatePrototypes,
29
32
  } from './proto';
30
- import type { SymbolMap } from './symbols';
33
+ import type { SymbolMap, SymbolTypeFacts } from './symbols';
31
34
  import type { TargetDescription } from './target';
32
35
 
33
36
  // asmlift — callee prototypes read out of a DECLARATION CONTEXT: the preprocessed headers a
@@ -68,6 +71,512 @@ export function prototypesFromContext(src: string, language: Language): Prototyp
68
71
  return functionPrototypes(ctx, table, layoutReader(ctx, table));
69
72
  }
70
73
 
74
+ /** What a preprocessed declaration context states about the globals it declares, beyond their
75
+ * prototypes. The sets travel together: the aggregate spelling `*(T *)&g` is a cast, which drops
76
+ * the declaration's qualifiers, so a reader handed the aggregates without the volatile objects
77
+ * would print a plain read of a volatile one. */
78
+ export interface ContextGlobals {
79
+ /** The objects declared as a struct, a union or an array. Such an object has no value a bare name
80
+ * reads: `v = gInfo;` over `extern struct Info gInfo;` is a compile error, and over
81
+ * `extern u8 gBuf[];` it reads the address. A project whose symbol map does not describe a
82
+ * global (one its linker script places has no DWARF) states it here, so the structurer reads
83
+ * this set where it would read the map's `shape`. A type this cannot resolve — an unexpanded
84
+ * macro — is left out, which keeps the usage-inferred spelling. */
85
+ aggregates: ReadonlySet<string>;
86
+ /** The objects any byte of which is volatile (`hasVolatileStorage`): the object, an array's
87
+ * element, a typedef in its chain, or a member of a struct or union in it, nested or anonymous.
88
+ * A body this cannot read counts as volatile. The set is not per byte: a read of the plain member
89
+ * of `struct { vu16 a; u16 b; }` is placed too, which costs at most a read the compiler kept that
90
+ * the source did not ask it to, never one it may drop. raise/declared-volatile.ts stamps every
91
+ * access to such an object the map does not lay out, and the structurer then puts the qualifier
92
+ * on the cast (`*(volatile u16 *)&gV`). Scalars are in it too: the header already qualifies
93
+ * `gReg` where it is spelled bare, and the stamp keeps the raising passes from merging two of its
94
+ * reads. */
95
+ volatiles: ReadonlySet<string>;
96
+ /** The objects declared as a pointer whose pointee has a volatile byte, read as `volatiles` reads
97
+ * an object: `extern volatile u16 *gP;`, `extern volatile struct V *gQ;`. The pointer itself is
98
+ * plain, so a read of `gP` is not placed; an access through the value it holds is, and the
99
+ * structurer puts the qualifier on the cast it is spelled through (`*(volatile u16 *)gP`).
100
+ * Without it, `while (*gP == 0);` lifts as a plain read the compiler hoists out of the loop. */
101
+ pointeeVolatiles: ReadonlySet<string>;
102
+ /** Every object the context declares, volatile or not; a function is no object. Read beside
103
+ * `volatiles`: an object here and not there is declared with no volatile byte, which is what the
104
+ * dead-read pin's `declared-plain` gate reads (frontend/dead-reads.ts) — the source spelled a
105
+ * read of it plain, so a read the target keeps that nothing uses is not the source's `volatile`. */
106
+ declared: ReadonlySet<string>;
107
+ /** The type each object of `declared` that is no array, pointer or function is declared as, as
108
+ * written (`Q_t`, `struct Q`). An object passed bare as a struct by value must be declared as
109
+ * that struct (frontend/call-plan.ts `foldByValueArgs`): `gQs` over `extern struct Q gQs[4];` or
110
+ * `gO2` over `extern struct O2 gO2;` names the right address and is a type error as an argument
111
+ * of `struct Q`. Compared by the struct it names (`structNamed`, which follows the typedefs),
112
+ * not by spelling. */
113
+ objectTypes: ReadonlyMap<string, string>;
114
+ /** The struct or union each typedef name stands for, as far as its chain goes with no
115
+ * derivation: by its tag (`struct Q`), or, for an untagged body, by the typedef name that binds
116
+ * it. Read through `structNamed`, which is how two spellings of one struct are told to be one:
117
+ * `typedef struct Q {…} Q;` spells the struct `Q`, `struct Q` and every other alias of it. */
118
+ structTypes: ReadonlyMap<string, string>;
119
+ /** The storage each object of `declared` that is no aggregate and no function is declared with,
120
+ * its typedefs resolved (`declaredWidth`, `declaredSigned`): `extern s32 gN;` is a four-byte
121
+ * signed integer, `extern u8 *gP;` a four-byte one whose sign nothing reads, `extern float gF;`
122
+ * a four-byte float. A bare name reads and writes exactly this, so the structurer spells a
123
+ * global bare only where its one access agrees (structure.ts, the declaration-shape override): a
124
+ * byte read of `gN` is `*(u8 *)&gN`, never `gN`, and so is a halfword read of `gF`, which
125
+ * reinterprets the bytes where the bare name would convert them. A type this cannot read — a
126
+ * typedef carrying an attribute, an unexpanded macro, a name it never saw — is `unread`, which
127
+ * agrees with no access: the cast states the access whatever the declaration is. A pointer
128
+ * behind an attributed typedef is still read as one (`writtenDerivation`). A C++ reference is
129
+ * a `reference`, whose cell no spelling names, so the structurer declines a function that
130
+ * touches it. */
131
+ storage: ReadonlyMap<string, DeclaredStorage>;
132
+ }
133
+
134
+ /** What one declared object that is no aggregate occupies ({@link ContextGlobals.storage}). */
135
+ export type DeclaredStorage =
136
+ /** an integer or a pointer of `bytes` bytes; `signed` says whether a read sign-extends it, `char`
137
+ * for a plain `char`, whose sign is the compiler's (`compilerBehaviors.plainCharSigned`), and
138
+ * undefined where nothing reads it (a pointer). A declaration with no type specifier at all
139
+ * (`extern gN;`, C89's implicit `int`) is an `int`. */
140
+ | { kind: 'integer'; bytes: number; signed: boolean | 'char' | undefined }
141
+ /** an enum, whose size is the compiler's (`compilerBehaviors.enumBytes`), unless `unsized`: one
142
+ * `packed` or holding a value an int cannot (`TypeTable.unsizedEnums`) is no size this knows */
143
+ | { kind: 'enum'; unsized: boolean }
144
+ /** a `float` (4 bytes) or a `double` (8) */
145
+ | { kind: 'float'; bytes: number }
146
+ /** a C++ reference (`extern int &gRef;`): the symbol is a cell holding the referent's address,
147
+ * which no C++ spelling names — `gRef` and `&gRef` both name the referent */
148
+ | { kind: 'reference' }
149
+ /** a type this cannot read */
150
+ | { kind: 'unread' };
151
+
152
+ /** The struct or union `spelling` names, qualifiers aside, by the identity `structTypes` keys it
153
+ * under: `struct Tag` as written, or what a typedef name stands for. Undefined for any other
154
+ * spelling, and for a name the context does not resolve to one — which then names no struct this
155
+ * can compare, and the spellings are compared as written. */
156
+ export function structNamed(
157
+ spelling: string,
158
+ context: Pick<ContextGlobals, 'structTypes'> | undefined,
159
+ ): string | undefined {
160
+ const s = spelling
161
+ .replace(/\b(?:const|volatile)\b/g, ' ')
162
+ .trim()
163
+ .replace(/\s+/g, ' ');
164
+ return /^(?:struct|union) [A-Za-z_$][\w$]*$/.test(s) ? s : context?.structTypes.get(s);
165
+ }
166
+
167
+ /** The globals a preprocessed declaration context declares, read for {@link ContextGlobals}. */
168
+ export function globalsFromContext(src: string, language: Language): ContextGlobals {
169
+ const ctx = parseDeclarations(src, language);
170
+ const table = typeTable(ctx);
171
+ const scope: VolatileScope = { table, typedefs: typedefTypes(ctx), bodies: tagBodies(ctx), ctx, read: new Map() };
172
+ const aggregates = new Set<string>();
173
+ const volatiles = new Set<string>();
174
+ const pointeeVolatiles = new Set<string>();
175
+ const declared = new Set<string>();
176
+ const objectTypes = new Map<string, string>();
177
+ const storage = new Map<string, DeclaredStorage>();
178
+ for (const d of ctx.declarations) {
179
+ if (d.specifiers.typedef || d.body !== undefined) {
180
+ continue;
181
+ }
182
+ const implicit = language === 'c' ? implicitIntObject(d, table) : undefined;
183
+ if (implicit !== undefined) {
184
+ declared.add(implicit);
185
+ objectTypes.set(implicit, 'int');
186
+ storage.set(implicit, { kind: 'integer', bytes: 4, signed: true });
187
+ if (d.specifiers.qualifiers.includes('volatile')) {
188
+ volatiles.add(implicit);
189
+ }
190
+ }
191
+ for (const x of d.declarators) {
192
+ if (x.name === undefined || !WORD.test(x.name)) {
193
+ continue;
194
+ }
195
+ const t = declaredType(d.specifiers, x.derivations);
196
+ if (t.derivations[0]?.kind !== 'function') {
197
+ declared.add(x.name);
198
+ }
199
+ if (t.derivations.length === 0) {
200
+ objectTypes.set(x.name, t.spelling);
201
+ }
202
+ if (isAggregate(t, table, scope.typedefs)) {
203
+ aggregates.add(x.name);
204
+ } else if (t.derivations[0]?.kind !== 'function') {
205
+ storage.set(x.name, declaredStorage(t, table, scope.typedefs, ctx.tokens));
206
+ }
207
+ if (hasVolatileStorage(t, scope, 0)) {
208
+ volatiles.add(x.name);
209
+ }
210
+ // derivations run outermost first: the object is a pointer, and what it points at is the rest
211
+ if (t.derivations[0]?.kind === 'pointer') {
212
+ const pointee = { ...t, derivations: t.derivations.slice(1) };
213
+ if (hasVolatileStorage(pointee, scope, 0)) {
214
+ pointeeVolatiles.add(x.name);
215
+ }
216
+ }
217
+ }
218
+ }
219
+ const structTypes = new Map<string, string>();
220
+ for (const name of scope.typedefs.keys()) {
221
+ const named = structOfTypedef(name, scope.typedefs);
222
+ if (named !== undefined) {
223
+ structTypes.set(name, named);
224
+ }
225
+ }
226
+ return { aggregates, volatiles, pointeeVolatiles, declared, objectTypes, structTypes, storage };
227
+ }
228
+
229
+ /** The object a C declaration with a storage class and no type specifier declares, C89's implicit
230
+ * `int`: `extern gN;` reads as the type name `gN` with no declarator, since the parser cannot tell
231
+ * a name from an unknown type with nothing after it. Only a lone word that names no type the
232
+ * context declares and no base type is one. */
233
+ function implicitIntObject(d: Declaration, table: TypeTable): string | undefined {
234
+ const { storage, type, unknownWords } = d.specifiers;
235
+ if (d.declarators.length > 0 || unknownWords.length > 0 || type.kind !== 'words' || type.words.length !== 1) {
236
+ return undefined;
237
+ }
238
+ const [w] = type.words;
239
+ const named =
240
+ !WORD.test(w) ||
241
+ table.typedefs.has(w) ||
242
+ table.enums.has(w) ||
243
+ table.named.has(w) ||
244
+ declaredWidth(w) !== undefined ||
245
+ ['void', 'float', 'double', '_Bool'].includes(w);
246
+ return !named && (storage.includes('extern') || storage.includes('static')) ? w : undefined;
247
+ }
248
+
249
+ /** The storage an object of type `t`, no aggregate and no function, is declared with
250
+ * ({@link DeclaredStorage}). `written` is every typedef as written, an attributed one included
251
+ * (`typedefTypes`), which `writtenDerivation` reads past. */
252
+ function declaredStorage(
253
+ t: DeclaredType,
254
+ table: TypeTable,
255
+ written: ReadonlyMap<string, DeclaredType>,
256
+ tokens: Tokens,
257
+ ): DeclaredStorage {
258
+ const [first] = t.derivations;
259
+ if (first?.kind === 'reference') {
260
+ return { kind: 'reference' };
261
+ }
262
+ if (first !== undefined && !isPointer(first)) {
263
+ return { kind: 'unread' };
264
+ }
265
+ const r = resolve(t, table.typedefs, tokens);
266
+ // the derivation the type reaches, through a typedef the table does not read too
267
+ const behind = r.derivations[0] ?? writtenDerivation(r, written);
268
+ if (behind?.kind === 'reference') {
269
+ return { kind: 'reference' };
270
+ }
271
+ const spelled = spellType(r, tokens);
272
+ const bare = spelled
273
+ .replace(/\b(?:const|volatile)\b/g, ' ')
274
+ .trim()
275
+ .replace(/\s+/g, ' ');
276
+ const bits = declaredWidth(bare === '' ? 'int' : spelled);
277
+ if (bits !== undefined) {
278
+ const signed = bare === 'char' ? 'char' : declaredSigned(bare === '' ? 'int' : spelled);
279
+ return { kind: 'integer', bytes: bits / 8, signed };
280
+ }
281
+ if (r.derivations.length > 0) {
282
+ return { kind: 'unread' };
283
+ }
284
+ if (bare === 'float' || bare === 'double') {
285
+ return { kind: 'float', bytes: bare === 'float' ? 4 : 8 };
286
+ }
287
+ if (table.enums.has(bare) || (r.type.kind === 'tag' && r.type.keyword === 'enum')) {
288
+ return { kind: 'enum', unsized: table.unsizedEnums.has(bare) };
289
+ }
290
+ // A pointer behind a typedef that carries an attribute (`typedef u8 *P8
291
+ // __attribute__((aligned(4)));`), which the table leaves unread because the attribute may retype
292
+ // it (`realigns`). No attribute changes a pointer's width on these 32-bit targets — `aligned` and
293
+ // `__declspec(align)` move its alignment alone, and `mode` takes no pointer mode but the 4-byte
294
+ // one — so it is a 4-byte pointer, read and written bare as a word. Read `unread`, that word is
295
+ // cast to an integer, which mwcc refuses wherever it flows into a pointer (`gQ = *(s32 *)&gP;`).
296
+ return behind !== undefined && isPointer(behind)
297
+ ? { kind: 'integer', bytes: 4, signed: undefined }
298
+ : { kind: 'unread' };
299
+ }
300
+
301
+ /** The first derivation a type with none of its own reaches through the typedefs as written
302
+ * (`typedefTypes`), an attributed one and a reference the type table does not read included:
303
+ * `P8` over `typedef u8 *P8 __attribute__((aligned(4)));` is a pointer, `IR` over
304
+ * `typedef int &IR;` a reference. Undefined where the chain ends at a base type or a name it
305
+ * never saw. */
306
+ function writtenDerivation(t: DeclaredType, written: ReadonlyMap<string, DeclaredType>): Derivation | undefined {
307
+ let cur = t;
308
+ for (let hops = 0; hops < 16 && cur.derivations.length === 0; hops++) {
309
+ const name = nameOf(cur);
310
+ const next = name === undefined ? undefined : written.get(name);
311
+ if (next === undefined || nameOf(next) === name) {
312
+ return undefined;
313
+ }
314
+ cur = next;
315
+ }
316
+ return cur.derivations[0];
317
+ }
318
+
319
+ /** The struct or union the typedef `name` stands for (`ContextGlobals.structTypes`): its tag where
320
+ * the chain ends at one, else the last name in the chain, which binds the untagged body. */
321
+ function structOfTypedef(name: string, typedefs: ReadonlyMap<string, DeclaredType>): string | undefined {
322
+ let binder = name;
323
+ let at = typedefs.get(name);
324
+ for (let hops = 0; hops < 16 && at !== undefined && at.derivations.length === 0; hops++) {
325
+ const t = at.type;
326
+ if (t.kind === 'tag') {
327
+ return t.keyword === 'struct' || t.keyword === 'union'
328
+ ? t.tag === undefined
329
+ ? binder
330
+ : `${t.keyword} ${t.tag}`
331
+ : undefined;
332
+ }
333
+ if (t.words.length !== 1 || at.unknownWords.length > 0) {
334
+ return undefined;
335
+ }
336
+ binder = t.words[0];
337
+ at = typedefs.get(binder);
338
+ }
339
+ return undefined;
340
+ }
341
+
342
+ /** Every struct, union and class body the context defines, by `keyword Tag` (and, in C++, by the bare
343
+ * tag, as C++ spells one), attributed or not, at the top level and nested in another body. */
344
+ function tagBodies(ctx: ParsedContext): Map<string, Range> {
345
+ const out = new Map<string, Range>();
346
+ const visit = (ds: readonly Declaration[], depth: number): void => {
347
+ for (const d of ds) {
348
+ const t = d.specifiers.type;
349
+ if (t.kind !== 'tag' || t.keyword === 'enum' || t.body === undefined) {
350
+ continue;
351
+ }
352
+ if (t.tag !== undefined && WORD.test(t.tag)) {
353
+ out.set(`${t.keyword} ${t.tag}`, t.body);
354
+ if (ctx.language === 'c++') {
355
+ out.set(t.tag, t.body);
356
+ }
357
+ }
358
+ const members = depth < 8 ? memberDeclarations(ctx, t.body) : undefined;
359
+ if (members !== undefined) {
360
+ visit(members, depth + 1);
361
+ }
362
+ }
363
+ };
364
+ visit(ctx.declarations, 0);
365
+ return out;
366
+ }
367
+
368
+ /** Every typedef name the context declares, and the type it stands for as written: with the body a
369
+ * typedef defines (`typedef struct { vu8 a; } S;`), its qualifiers (`typedef volatile struct {…} S;`)
370
+ * and an array's bounds (`typedef vu8 Buf[4];`), none of which the type table keeps — it binds a
371
+ * body's name to the name itself, and it reads no typedef of an array. */
372
+ function typedefTypes(ctx: ParsedContext): Map<string, DeclaredType> {
373
+ const out = new Map<string, DeclaredType>();
374
+ for (const d of ctx.declarations) {
375
+ if (!d.specifiers.typedef) {
376
+ continue;
377
+ }
378
+ for (const x of d.declarators) {
379
+ const t = declaredType(d.specifiers, x.derivations);
380
+ // `typedef S S;` restates a name and keeps the type it already stands for
381
+ const restates = t.type.kind === 'words' && t.type.words.length === 1 && t.type.words[0] === x.name;
382
+ if (x.name !== undefined && WORD.test(x.name) && !(restates && t.derivations.length === 0)) {
383
+ out.set(x.name, t);
384
+ }
385
+ }
386
+ }
387
+ return out;
388
+ }
389
+
390
+ /** What `hasVolatileStorage` reads a type through, and what it has already read: each typedef name
391
+ * and each body by its first token, so a type every member reaches through the same typedefs
392
+ * (`mActor_name_t`, then `u16`) is walked once, and a long chain of them costs no nesting depth. */
393
+ interface VolatileScope {
394
+ table: TypeTable;
395
+ typedefs: ReadonlyMap<string, DeclaredType>;
396
+ bodies: ReadonlyMap<string, Range>;
397
+ ctx: ParsedContext;
398
+ read: Map<string, boolean>;
399
+ }
400
+
401
+ /** Whether any byte of an object of type `t` is volatile: the type or an array's element is
402
+ * qualified, through the typedef names it is built on, or a struct or union in it has such a member.
403
+ * A pointer ends the walk at its own qualifiers, since what it points at is no byte of the object.
404
+ * A body that cannot be read, or bodies nested past the bound, is read as volatile: a qualifier
405
+ * placed where none was declared only keeps a read the compiler may have dropped. A tag with no
406
+ * body here has no member to qualify.
407
+ * A name met again while it is being read is a cycle, read as volatile (`once`). */
408
+ function hasVolatileStorage(t: DeclaredType, scope: VolatileScope, depth: number): boolean {
409
+ if (depth > 32) {
410
+ return true;
411
+ }
412
+ for (const x of t.derivations) {
413
+ if (x.kind === 'pointer') {
414
+ return x.qualifiers.includes('volatile');
415
+ }
416
+ if (x.kind !== 'array') {
417
+ return false;
418
+ }
419
+ }
420
+ // GNU's alternate spellings, which the declaration parser keeps as words it does not know, or as
421
+ // the type's own name when a typedef name follows (`__volatile__ u8 a;`)
422
+ const words = t.type.kind === 'words' ? [...t.type.words, ...t.unknownWords] : t.unknownWords;
423
+ if (t.qualifiers.includes('volatile') || words.some((w) => w === '__volatile' || w === '__volatile__')) {
424
+ return true;
425
+ }
426
+ let body: Range | undefined;
427
+ if (t.type.kind === 'tag') {
428
+ if (t.type.keyword === 'enum') {
429
+ return false;
430
+ }
431
+ body = t.type.body ?? (t.type.tag !== undefined ? scope.bodies.get(`${t.type.keyword} ${t.type.tag}`) : undefined);
432
+ } else if (t.type.words.length === 1) {
433
+ const name = t.type.words[0];
434
+ const next = scope.typedefs.get(name);
435
+ if (next !== undefined) {
436
+ const resolves = next.type.kind === 'words' && next.type.words.length === 1 && next.type.words[0] === name;
437
+ return !resolves && once(scope, `typedef ${name}`, () => hasVolatileStorage(next, scope, depth));
438
+ }
439
+ body = scope.table.named.get(name)?.body ?? scope.bodies.get(name);
440
+ }
441
+ if (body === undefined) {
442
+ return false;
443
+ }
444
+ const members = memberDeclarations(scope.ctx, body);
445
+ const bases = baseClause(scope.ctx, body);
446
+ if (members === undefined || bases === undefined) {
447
+ return true;
448
+ }
449
+ // a C++ member typedef names a type for the members after it, so they are read through it, in a
450
+ // scope of their own. That scope also reads a body defined elsewhere through it, which can only
451
+ // leave more out.
452
+ const local = typedefTypes({ ...scope.ctx, declarations: members });
453
+ const inner: VolatileScope =
454
+ local.size === 0 ? scope : { ...scope, typedefs: new Map([...scope.typedefs, ...local]), read: new Map() };
455
+ return once(
456
+ scope,
457
+ `body ${body.from}`,
458
+ () =>
459
+ // a C++ base's members are bytes of the object too, and a base with no body here (one a
460
+ // namespace defines, which this does not read) is read as volatile
461
+ bases.some((b) => !definesBody(b, inner) || hasVolatileStorage(b, inner, depth + 1)) ||
462
+ members.some((m) =>
463
+ // a C++ member typedef or static member is no byte of the object
464
+ m.specifiers.typedef || m.specifiers.storage.includes('static')
465
+ ? false
466
+ : m.declarators.length === 0
467
+ ? hasVolatileStorage(declaredType(m.specifiers, []), inner, depth + 1)
468
+ : m.declarators.some((x) =>
469
+ hasVolatileStorage(declaredType(m.specifiers, x.derivations), inner, depth + 1),
470
+ ),
471
+ ),
472
+ );
473
+ }
474
+
475
+ /** The C++ base classes ahead of `body`, each as the type its name spells, or undefined when a
476
+ * clause holds anything else (a qualified name, a template argument list, `decltype`), which the
477
+ * caller reads as volatile. Read backwards from the `{` to the clause's `:`; reaching the
478
+ * tag keyword first means there is no clause. */
479
+ function baseClause(ctx: ParsedContext, body: Range): DeclaredType[] | undefined {
480
+ if (ctx.language !== 'c++') {
481
+ return [];
482
+ }
483
+ const t = ctx.tokens;
484
+ let k = body.from - 2;
485
+ for (; k >= 0 && !t.is(k, ':'); k--) {
486
+ if (TAG_KEYWORDS.has(t.text(k))) {
487
+ return [];
488
+ }
489
+ if (t.kind(k) !== 'identifier' && !t.is(k, '::') && !t.is(k, ',')) {
490
+ // an attribute, a template argument list: a `:` between it and the tag keyword is a clause
491
+ // this does not read, and none means there is no clause
492
+ for (let j = k; j >= 0 && !t.is(j, ';') && !t.is(j, '}') && !t.is(j, '{') && !TAG_KEYWORDS.has(t.text(j)); j--) {
493
+ if (t.is(j, ':')) {
494
+ return undefined;
495
+ }
496
+ }
497
+ return [];
498
+ }
499
+ }
500
+ if (k < 0) {
501
+ return [];
502
+ }
503
+ const out: DeclaredType[] = [];
504
+ let last: string | undefined;
505
+ for (let j = k + 1; j < body.from; j++) {
506
+ if (t.is(j, ',') || j === body.from - 1) {
507
+ if (last === undefined) {
508
+ return undefined;
509
+ }
510
+ out.push({
511
+ qualifiers: [],
512
+ type: { kind: 'words', words: [last] },
513
+ unknownWords: [],
514
+ spelling: last,
515
+ derivations: [],
516
+ });
517
+ last = undefined;
518
+ } else if (t.is(j, '::')) {
519
+ return undefined;
520
+ } else if (t.kind(j) === 'identifier' && !BASE_WORDS.has(t.text(j))) {
521
+ last = t.text(j);
522
+ }
523
+ }
524
+ return out;
525
+ }
526
+
527
+ /** Whether the base `b` names a typedef or a body this context defines. */
528
+ function definesBody(b: DeclaredType, scope: VolatileScope): boolean {
529
+ const name = b.spelling;
530
+ return scope.typedefs.has(name) || scope.table.named.get(name)?.body !== undefined || scope.bodies.has(name);
531
+ }
532
+
533
+ const TAG_KEYWORDS: ReadonlySet<string> = new Set(['struct', 'class', 'union', 'enum']);
534
+ const BASE_WORDS: ReadonlySet<string> = new Set(['public', 'protected', 'private', 'virtual']);
535
+
536
+ /** `read()`, remembered under `key`. A key met again while it is being read is a cycle no compiler
537
+ * lays out (`typedef T U; typedef U T;`, which C11 and C++ accept), read as volatile, for the same
538
+ * reason as an unreadable body. */
539
+ function once(scope: VolatileScope, key: string, read: () => boolean): boolean {
540
+ const known = scope.read.get(key);
541
+ if (known !== undefined) {
542
+ return known;
543
+ }
544
+ scope.read.set(key, true);
545
+ const v = read();
546
+ scope.read.set(key, v);
547
+ return v;
548
+ }
549
+
550
+ /** Whether `t` is a struct, union, class or array type, through the typedef names it is built on.
551
+ * A typedef the type table does not read — one of an array, `typedef u16 Arr4[4];` — is followed
552
+ * as written (`typedefTypes`), or `extern Arr4 gTB;` reads as a scalar whose offset-0 read is
553
+ * spelled `gTB`, the array's address. */
554
+ function isAggregate(t: DeclaredType, table: TypeTable, written: ReadonlyMap<string, DeclaredType>): boolean {
555
+ let cur = t;
556
+ for (let hops = 0; hops < 16; hops++) {
557
+ const [first] = cur.derivations;
558
+ if (first !== undefined) {
559
+ return first.kind === 'array';
560
+ }
561
+ if (cur.type.kind === 'tag') {
562
+ return cur.type.keyword !== 'enum';
563
+ }
564
+ const name = nameOf(cur);
565
+ if (name === undefined) {
566
+ return false;
567
+ }
568
+ if (table.named.has(name)) {
569
+ return true;
570
+ }
571
+ const next = table.typedefs.get(name) ?? written.get(name);
572
+ if (next === undefined || nameOf(next) === name) {
573
+ return false;
574
+ }
575
+ cur = next;
576
+ }
577
+ return false;
578
+ }
579
+
71
580
  const WORD = /^[A-Za-z_]\w*$/;
72
581
 
73
582
  /** `mode(…)` in an attribute, in either spelling: it retypes what it is written on */
@@ -165,6 +674,14 @@ function readTag(
165
674
  if ((attributed || wide) && name !== undefined) {
166
675
  table.unsizedEnums.add(`enum ${name}`);
167
676
  }
677
+ // In C++ an enum's tag names its type with no keyword, as a typedef bound to its body does in C
678
+ // (`readTypedefs`), so the bare `Fmt` of `enum Fmt { … }` is that enum, sized as it is.
679
+ if (ctx.language === 'c++' && name !== undefined) {
680
+ table.enums.add(name);
681
+ if (attributed || wide) {
682
+ table.unsizedEnums.add(name);
683
+ }
684
+ }
168
685
  return attributed || wide;
169
686
  }
170
687
 
@@ -580,17 +1097,82 @@ function memberShape(
580
1097
  return { name: d.name, pointer: stars.length > 0, ...(dims.length > 0 ? { dims } : {}), returns: [] };
581
1098
  }
582
1099
 
1100
+ /** A declaration's reading: the entry as written, the `key` two declarations are compared by, and
1101
+ * the `lists` of every declaration it stands for, which say whether a declaration that states no
1102
+ * parameters can be one with a declaration that does (`composable`). */
1103
+ type Read = { proto: FnProto; key: FnProto; lists: readonly ParameterList[] };
1104
+
1105
+ /** What a declaration's parentheses say of its parameters, as `composable` needs it: `()` in C,
1106
+ * which states none; a list this could not read, with how many FIXED parameters its top-level commas
1107
+ * separate and whether it ends in `...`; or a list it read, with whether every parameter keeps its
1108
+ * type under the default argument promotions (C99 6.5.2.2p6) — undefined where one may not. */
1109
+ type ParameterList =
1110
+ | { kind: 'unstated' }
1111
+ | { kind: 'unread'; fixed: number; variadic: boolean }
1112
+ | { kind: 'read'; fixed: number; variadic: boolean; promotes: boolean };
1113
+
583
1114
  /** Every function the declarations name by a plain identifier, keyed by it. A declaration that
584
1115
  * spells no type is a constructor's, a destructor's or a conversion operator's; one that defines a
585
1116
  * body, or spells a `class`, declares no C function. */
586
1117
  function functionPrototypes(ctx: ParsedContext, table: TypeTable, layoutOf: LayoutOf): Prototypes {
587
- const found = new Map<string, FnProto | null>();
1118
+ const found = new Map<string, Read | null>();
1119
+ const written = typedefTypes(ctx);
588
1120
  // a function one of whose declarations retypes a parameter with `mode`, which gives it the type
589
1121
  // the mode names in place of the one spelled (c-common.c:563): compiled, `int x
590
1122
  // __attribute__((mode(DI)))` takes a register pair. Its parameters are not read, in any
591
1123
  // declaration of it; its return is.
592
1124
  const unreadParams = new Set<string>();
593
- const returnOnly = ({ params: _unread, ...rest }: FnProto): FnProto => rest;
1125
+ // Two declarations of one function name one type, and the entry is that type's: the same reading
1126
+ // twice, or — in C — a declaration whose parameters are not read (`()`, which states none, or a
1127
+ // list this cannot read, `const char *restrict fmt`) beside one whose are, where the return reads
1128
+ // alike and the two are COMPATIBLE (`composable`). The C compiler takes the composite of the two
1129
+ // (C99 6.2.7p3), which is the one that states the parameters, so the entry is that one: dropping
1130
+ // the name would drop the refusals its parameters carry with it, and send the call to the arity
1131
+ // guess. Anything else is a conflict (C++ overloads, or a header this misreads), and the name is
1132
+ // dropped.
1133
+ //
1134
+ // A `()` is read with a list only where it PRECEDES it: one that follows a list is a conflict,
1135
+ // whatever the types (below). And `()` beside a list that is not compatible with it is no
1136
+ // composite: agbcc, gcc2.7.2kmc and
1137
+ // gcc2.7.2 compile `void f(char); void f();` without a word, the later `()` wins, and a call is
1138
+ // compiled UNPROTOTYPED — any arity, no conversion. Read as `f(char)`, `f(1, 2)` would lift as
1139
+ // `f(1)`, and `f(5)` under `f(short, char)` as `f(5, a1)`.
1140
+ const unstated = (p: FnProto) => p.params === undefined && p.variadicParams === undefined;
1141
+ const merged = (kept: Read, a: Read, b: Read): Read => ({ ...kept, lists: [...a.lists, ...b.lists] });
1142
+ const composite = (a: Read, b: Read): Read | null => {
1143
+ if (b.lists.some((l) => l.kind === 'unstated') && a.lists.some((l) => l.kind === 'read')) {
1144
+ // a `()` AFTER a parameter list: mwcc (2.4.2, 2.4.7, 2.3.3) lets it replace the prototype
1145
+ // outright, even where every parameter is a word — `void f(int); void f();` compiles `f(1, 2)`
1146
+ // as two arguments, `f(1.5)` in f1, and `f(1)` under `void f(double)` in r3 — so the list is
1147
+ // no fact about the call. Read as the list, `f(1, 2)` lifts as `f(1)`. gcc and IDO reject what
1148
+ // that call would need, so nothing is lost on them by a conflict.
1149
+ return null;
1150
+ }
1151
+ if (same(a.key, b.key)) {
1152
+ return merged(b, a, b);
1153
+ }
1154
+ // …and where the list follows a `()` that itself followed a list this could not read, the `()`
1155
+ // still replaced it: the two keep their return (`same`, above), and no later list joins them
1156
+ const all = [...a.lists, ...b.lists];
1157
+ if (
1158
+ ctx.language !== 'c' ||
1159
+ all.some((l, i) => l.kind === 'unstated' && all.slice(0, i).some((m) => m.kind !== 'unstated'))
1160
+ ) {
1161
+ return null;
1162
+ }
1163
+ if (unstated(a.key) && same(a.key, returnOnly(b.key)) && composable(a.lists, b.lists)) {
1164
+ return merged(b, a, b);
1165
+ }
1166
+ return unstated(b.key) && same(b.key, returnOnly(a.key)) && composable(b.lists, a.lists) ? merged(a, a, b) : null;
1167
+ };
1168
+ const returnOnly = ({
1169
+ params: _unread,
1170
+ wordParams: _words,
1171
+ byValueParams: _copies,
1172
+ byValueLayouts: _layouts,
1173
+ variadicParams: _fixed,
1174
+ ...rest
1175
+ }: FnProto): FnProto => rest;
594
1176
  for (const d of ctx.declarations) {
595
1177
  const s = d.specifiers;
596
1178
  if (
@@ -614,15 +1196,40 @@ function functionPrototypes(ctx: ParsedContext, table: TypeTable, layoutOf: Layo
614
1196
  if (retyped) {
615
1197
  unreadParams.add(x.name);
616
1198
  }
617
- const read = readSignature(declaredType(s, returns), fn, ctx, table.typedefs, (t) => layoutOf(t, 0));
618
- const proto = unreadParams.has(x.name) ? returnOnly(read) : read;
1199
+ const reading = (qualifiers: 'kept' | 'dropped') =>
1200
+ readSignature(
1201
+ declaredType(s, returns),
1202
+ fn,
1203
+ ctx,
1204
+ table.typedefs,
1205
+ (t) => layoutOf(t, 0),
1206
+ (t) => passingOf(t, table, written),
1207
+ qualifiers,
1208
+ );
1209
+ const read = reading('kept');
1210
+ // what the declaration says of the function's TYPE, which is what two declarations of it are
1211
+ // compared by: a parameter's top-level qualifiers are not part of it (C99 6.7.5.3p15), so
1212
+ // `void takeW(const struct W w);` and `void takeW(struct W w);` declare one function. Read
1213
+ // as a conflict, they would drop the name, and with it every refusal its entry carries
1214
+ // (`byValueRefusal`, `variadicRefusal`): the call would take the arity guess they exist to
1215
+ // refuse. The entry kept is the later declaration's, spelled as written.
1216
+ const typed = reading('dropped');
1217
+ const [proto, key] = unreadParams.has(x.name) ? [returnOnly(read), returnOnly(typed)] : [read, typed];
1218
+ const lists = [parameterList(fn, ctx, table.typedefs, (t) => passingOf(t, table, written))];
619
1219
  const prior = found.get(x.name);
620
- const had = prior && unreadParams.has(x.name) ? returnOnly(prior) : prior;
621
- found.set(x.name, had === undefined || same(had, proto) ? proto : null);
1220
+ const had =
1221
+ prior && unreadParams.has(x.name)
1222
+ ? { ...prior, proto: returnOnly(prior.proto), key: returnOnly(prior.key) }
1223
+ : prior;
1224
+ found.set(
1225
+ x.name,
1226
+ had === undefined ? { proto, key, lists } : had === null ? null : composite(had, { proto, key, lists }),
1227
+ );
622
1228
  }
623
1229
  }
624
1230
  const out: Prototypes = {};
625
- for (const [name, p] of found) {
1231
+ for (const [name, read] of found) {
1232
+ const p = read?.proto ?? null;
626
1233
  const valid = p === null ? undefined : admissible(name, p);
627
1234
  if (valid !== undefined) {
628
1235
  out[name] = valid;
@@ -631,6 +1238,101 @@ function functionPrototypes(ctx: ParsedContext, table: TypeTable, layoutOf: Layo
631
1238
  return out;
632
1239
  }
633
1240
 
1241
+ /** Whether declarations that state no parameters (`unstated`, each `()` or a list this could not
1242
+ * read) are compatible with ones that state them (`stated`), as two declarations of one C function
1243
+ * must be for their composite to be the stated one (C99 6.7.5.3p15). `()` is compatible with a
1244
+ * list that is not variadic and whose every parameter keeps its type under the default argument
1245
+ * promotions: not `char`, `short`, `float`, `_Bool`, nor a typedef or an enum that may be one — an
1246
+ * enum's type is the compiler's to pick, and it may be narrow (`-fshort-enums`, mwcc's `-enum
1247
+ * min`). A name the context never resolves is read by its spelling as `declaredWidth` reads it
1248
+ * (`s32` is a word, `u8` a byte) and is otherwise taken to promote. An unread list is compatible with a list of as many fixed parameters, variadic alike. */
1249
+ function composable(unstated: readonly ParameterList[], stated: readonly ParameterList[]): boolean {
1250
+ const lists = stated.filter((l) => l.kind === 'read');
1251
+ return (
1252
+ lists.length > 0 &&
1253
+ unstated.every((u) =>
1254
+ lists.every((l) =>
1255
+ u.kind === 'unstated'
1256
+ ? !l.variadic && !l.promotes
1257
+ : u.kind === 'unread' && u.fixed === l.fixed && u.variadic === l.variadic,
1258
+ ),
1259
+ )
1260
+ );
1261
+ }
1262
+
1263
+ /** Whether `fn`'s parentheses are C's empty list: `(void)`, or one parameter whose type is `void`
1264
+ * through a typedef (`typedef void V; int k(V);`, C99 6.7.5.3p10). Read as a list of one
1265
+ * parameter spelled `void`, the second would have the call pass a register nothing set up for it
1266
+ * (`k()` as `k(a0)`). */
1267
+ function isVoidList(
1268
+ fn: Extract<Derivation, { kind: 'function' }>,
1269
+ tokens: Tokens,
1270
+ typedefs: ReadonlyMap<string, DeclaredType>,
1271
+ ): boolean {
1272
+ if (fn.list.to === fn.list.from + 1 && tokens.is(fn.list.from, 'void')) {
1273
+ return true;
1274
+ }
1275
+ const only = fn.params?.length === 1 && !fn.variadic ? fn.params[0] : undefined;
1276
+ return (
1277
+ only !== undefined &&
1278
+ spellType(resolve(declaredType(only.specifiers, only.declarator.derivations), typedefs, tokens), tokens) === 'void'
1279
+ );
1280
+ }
1281
+
1282
+ /** What `fn`'s parentheses say of its parameters (`ParameterList`). */
1283
+ function parameterList(
1284
+ fn: Extract<Derivation, { kind: 'function' }>,
1285
+ { tokens, language }: ParsedContext,
1286
+ typedefs: ReadonlyMap<string, DeclaredType>,
1287
+ passing: (t: DeclaredType) => Passing,
1288
+ ): ParameterList {
1289
+ const { params, list } = fn;
1290
+ if (params === undefined) {
1291
+ // the top-level commas, and a trailing `...`
1292
+ let items = list.to > list.from ? 1 : 0;
1293
+ let variadic = false;
1294
+ for (let i = list.from; i < list.to; i++) {
1295
+ const close = tokens.match(i);
1296
+ if (close > i) {
1297
+ i = close;
1298
+ } else if (tokens.is(i, ',')) {
1299
+ items++;
1300
+ } else if (tokens.is(i, '...')) {
1301
+ variadic = true;
1302
+ }
1303
+ }
1304
+ return { kind: 'unread', fixed: variadic ? items - 1 : items, variadic };
1305
+ }
1306
+ if (params.length === 0 && !fn.variadic && language === 'c') {
1307
+ return { kind: 'unstated' };
1308
+ }
1309
+ if (isVoidList(fn, tokens, typedefs)) {
1310
+ return { kind: 'read', fixed: 0, variadic: false, promotes: false };
1311
+ }
1312
+ const promotes = params.some((p) => {
1313
+ const declared = asParameter(declaredType(p.specifiers, p.declarator.derivations));
1314
+ if (passing(declared) === 'by-value') {
1315
+ // a struct, union or class, which no promotion converts
1316
+ return false;
1317
+ }
1318
+ const t = resolve(declared, typedefs, tokens);
1319
+ if (t.derivations.length > 0) {
1320
+ // a pointer, or an array or a function the parameter adjusts to one
1321
+ return false;
1322
+ }
1323
+ if (t.type.kind === 'tag') {
1324
+ return t.type.keyword === 'enum';
1325
+ }
1326
+ const spelled = spellType(withoutTopQualifiers(t), tokens);
1327
+ if (spelled === 'double' || spelled === 'long double') {
1328
+ return false;
1329
+ }
1330
+ const width = declaredWidth(spelled);
1331
+ return width === undefined || width < 32;
1332
+ });
1333
+ return { kind: 'read', fixed: params.length, variadic: fn.variadic, promotes };
1334
+ }
1335
+
634
1336
  /** The entry as a table `validatePrototypes` accepts, the check every `--proto` table passes, so a
635
1337
  * context's prototypes can travel as one: a `returns` it refuses is dropped — a return wider than a
636
1338
  * register with a parameter list that cannot be printed — and the entry with it if that is not
@@ -649,6 +1351,8 @@ function readSignature(
649
1351
  { tokens, language }: ParsedContext,
650
1352
  typedefs: ReadonlyMap<string, DeclaredType>,
651
1353
  layoutOf: (t: DeclaredType) => AggregateLayout | undefined,
1354
+ passing: (t: DeclaredType) => Passing,
1355
+ qualifiers: 'kept' | 'dropped' = 'kept',
652
1356
  ): FnProto {
653
1357
  const proto: FnProto = {};
654
1358
  const resolved = resolve(ret, typedefs, tokens);
@@ -689,34 +1393,146 @@ function readSignature(
689
1393
  }
690
1394
  return proto;
691
1395
  }
692
- if (fn.list.to === fn.list.from + 1 && tokens.is(fn.list.from, 'void')) {
1396
+ if (isVoidList(fn, tokens, typedefs)) {
693
1397
  proto.params = [];
694
1398
  return proto;
695
1399
  }
1400
+ // Each parameter's type, its name taken off: an array or a function nearest the name is a
1401
+ // pointer, and the typedef names it is built on resolve as a return's do. A spelling that sizes to
1402
+ // nothing and is no floating type is read through those names for what it positively is: a type
1403
+ // that still travels in one register — an enum, or an array or function type behind a typedef
1404
+ // name, which the parameter adjusts to a pointer (`GXTexFmt`, `Mtx`, `__va_list`) — listed in
1405
+ // `FnProto.wordParams`; or a struct, union or class passed BY VALUE, which mwcc passes as the
1406
+ // address of a copy in the caller's frame — listed in `FnProto.byValueParams`. A name the context
1407
+ // never resolves is in neither, and every reader leaves it unsized.
1408
+ //
1409
+ // A VARIADIC declaration's are read alike and kept as its fixed parameters (`variadicParams`): its
1410
+ // arity is the call's, so they are no `params`, and no layout is read for a struct among them,
1411
+ // whose call refuses (`variadicRefusal`).
1412
+ const types = params.map((p) => asParameter(declaredType(p.specifiers, p.declarator.derivations)));
1413
+ const spelled = types.map((t) => {
1414
+ const r = resolve(t, typedefs, tokens);
1415
+ return spellType(qualifiers === 'kept' ? r : withoutTopQualifiers(r), tokens);
1416
+ });
696
1417
  if (fn.variadic) {
697
- return proto;
1418
+ proto.variadicParams = spelled;
1419
+ } else {
1420
+ proto.params = spelled;
1421
+ }
1422
+ const unsized = (k: number) => declaredWidth(spelled[k]) === undefined && !isFloatingSpelling(spelled[k]);
1423
+ const listed = (as: Passing) => types.flatMap((t, k) => (unsized(k) && passing(t) === as ? [k] : []));
1424
+ const words = listed('word');
1425
+ const copies = listed('by-value');
1426
+ if (words.length > 0) {
1427
+ proto.wordParams = words;
1428
+ }
1429
+ if (copies.length > 0) {
1430
+ proto.byValueParams = copies;
1431
+ }
1432
+ if (copies.length > 0 && !fn.variadic) {
1433
+ // …and the members of each one the context defines, which size it (`FnProto.byValueLayouts`)
1434
+ const layouts = Object.fromEntries(
1435
+ copies.flatMap((k) => {
1436
+ const l = layoutOf(resolve(types[k], typedefs, tokens));
1437
+ return l === undefined ? [] : [[k, l] as const];
1438
+ }),
1439
+ );
1440
+ if (Object.keys(layouts).length > 0) {
1441
+ proto.byValueLayouts = layouts;
1442
+ }
698
1443
  }
699
- proto.params = params.map((p) => parameterType(p, tokens, typedefs));
700
1444
  return proto;
701
1445
  }
702
1446
 
703
- /** A parameter's type, its name taken off: an array or a function nearest the name is a pointer, and
704
- * the typedef names it is built on resolve as a return's do. */
705
- function parameterType(p: Parameter, tokens: Tokens, typedefs: ReadonlyMap<string, DeclaredType>): ParamType {
706
- const t = asParameter(declaredType(p.specifiers, p.declarator.derivations));
707
- return spellType(resolve(t, typedefs, tokens), tokens);
1447
+ /** `t` less its top-level qualifiers: the derivation nearest the name's (`char *const`), or the
1448
+ * base's where there is none (`const struct W`). A pointer's target keeps its own (`const char *`). */
1449
+ function withoutTopQualifiers(t: DeclaredType): DeclaredType {
1450
+ const [first, ...rest] = t.derivations;
1451
+ if (first === undefined) {
1452
+ return { ...t, qualifiers: [] };
1453
+ }
1454
+ return first.kind === 'pointer' ? { ...t, derivations: [{ ...first, qualifiers: [] }, ...rest] } : t;
1455
+ }
1456
+
1457
+ /** How a parameter of type `t` is passed, read through the typedef names it is built on: in ONE
1458
+ * WORD — an enum the target sizes; a pointer; an array or a function, which a parameter adjusts to
1459
+ * a pointer — or BY VALUE, a struct, union or class (a tag, or a name bound to a body). Undefined
1460
+ * where nothing here says: a name this never saw, or an enum the target does not size
1461
+ * (`unsizedEnums`: `packed`, or a value an int cannot hold), which may take two words. */
1462
+ type Passing = 'word' | 'by-value' | undefined;
1463
+
1464
+ function passingOf(t: DeclaredType, table: TypeTable, written: ReadonlyMap<string, DeclaredType>): Passing {
1465
+ let cur = t;
1466
+ for (let hops = 0; hops < 16; hops++) {
1467
+ const [first] = cur.derivations;
1468
+ if (first !== undefined) {
1469
+ return first.kind === 'array' || first.kind === 'function' || first.kind === 'reference' || isPointer(first)
1470
+ ? 'word'
1471
+ : undefined;
1472
+ }
1473
+ if (cur.type.kind === 'tag') {
1474
+ const { keyword, tag } = cur.type;
1475
+ if (keyword !== 'enum') {
1476
+ return 'by-value';
1477
+ }
1478
+ return tag !== undefined && table.unsizedEnums.has(`enum ${tag}`) ? undefined : 'word';
1479
+ }
1480
+ const name = nameOf(cur);
1481
+ if (name === undefined) {
1482
+ return undefined;
1483
+ }
1484
+ if (table.named.has(name)) {
1485
+ return 'by-value';
1486
+ }
1487
+ if (table.enums.has(name)) {
1488
+ return table.unsizedEnums.has(name) ? undefined : 'word';
1489
+ }
1490
+ const next = table.typedefs.get(name) ?? written.get(name);
1491
+ if (next === undefined || nameOf(next) === name) {
1492
+ return undefined;
1493
+ }
1494
+ cur = next;
1495
+ }
1496
+ return undefined;
708
1497
  }
709
1498
 
710
1499
  /** The prototypes a lift of `own` reads when a context is in hand: `stated` — a caller's own
711
1500
  * prototypes, which win per name — over the context's declarations less those the symbol map
712
- * states better (`contextPrototypesUnder`). `own`'s declaration is left out, as the map's is
713
- * (`asIfUndecompiled`): a header's signature for the function being decompiled is that kind of
714
- * fact, and only what the caller states about it is kept.
1501
+ * states better (`contextPrototypesUnder`). `own`'s declaration is left out of the table, as the
1502
+ * map's is (`asIfUndecompiled`): a header's signature for the function being decompiled was written
1503
+ * with no compiler checking it against a definition while the function was still `INCLUDE_ASM`, so
1504
+ * it is not stated, and only what the caller states about it bounds the lift HARD.
1505
+ *
1506
+ * IT BOUNDS THE ARGUMENT REGISTERS SOFTLY INSTEAD ({@link ownContextEntry}, pipeline.ts
1507
+ * `liftWithinContextBound`). A hard bound puts the risk on the very functions a user decompiles:
1508
+ * an entry too short turns a real argument into `uninit_<reg>` — wrong C, where every other error
1509
+ * of the bound only keeps a parameter — and sbk2 has 16 `void f(void);` task callbacks whose
1510
+ * bodies read a0. The soft bound drops only the registers past the entry that the finished lift
1511
+ * never reads; one it reads (or one the machine homes to a slot, as IDO -g3 does) stays a
1512
+ * parameter, with an `[own-arity]` note naming the declaration and the register. (On kleod, whose
1513
+ * every header its compiler checked, the entry stated as a hard bound takes the lifts with more
1514
+ * parameters than their header from 52 of 377 to 1 of 373.) Only a typed list the bound can lay
1515
+ * out is handed over; every other shape abstains. A struct the entry returns through memory is
1516
+ * read on the same soft footing (pipeline.ts `returnOwnStruct`): lifted as that struct where the
1517
+ * machine proves the hidden pointer is only its storage, and kept as the pointer function with an
1518
+ * `[own-return]` note where it does not.
1519
+ *
1520
+ * The benchmark hands each row the same entry (`withOwnContextMark`, apps/benchmark
1521
+ * context-proto.ts `rowPrototypes`) where its manifest `proto` states none for the row's own
1522
+ * function, as m2c's vendored context already carries it; not every real row restates its own
1523
+ * header entry as `proto`. A user who trusts the entry outright restates it with `--proto`, which
1524
+ * bounds hard.
715
1525
  *
716
1526
  * A stated entry that says nothing of the return (`statesNoReturn`, which counts `returnsVoid:
717
- * false` as nothing) keeps a struct return the context states: it is what says argument 0 may be
718
- * a hidden pointer, and an entry stating only the arity would otherwise hand that pointer to the
719
- * call as its first argument. */
1527
+ * false` as nothing) keeps the context's return by `contextReturnUnder`'s rule: a struct return
1528
+ * always — it is what says argument 0 may be a hidden pointer, and an entry stating only the arity
1529
+ * would otherwise hand that pointer to the call as its first argument; a register return only
1530
+ * where the stated `params` number as many as the header's parameters. A list is parameters; a
1531
+ * COUNT is argument registers (`FnProto.params`), and it is compared with the header's parameter
1532
+ * count all the same, so under a 64-bit parameter a correct register count keeps no return. That
1533
+ * costs a lift, never a wrong one: a return kept is the header's own, and one dropped declines as
1534
+ * an undeclared callee does. `{ returnsVoid: false }` alone states no count, so it keeps no
1535
+ * register return. */
720
1536
  export function withContextPrototypes(
721
1537
  stated: Prototypes | undefined,
722
1538
  context: Prototypes,
@@ -729,21 +1545,131 @@ export function withContextPrototypes(
729
1545
  for (const [name, p] of Object.entries(stated ?? {})) {
730
1546
  const heard = Object.hasOwn(callees, name) ? callees[name] : undefined;
731
1547
  if (p && statesNoReturn(p)) {
732
- out[name] = { ...p, ...aggregateReturnOf(heard) };
1548
+ // `heard` is the entry as the map left it: its return, where it has one, is either the whole
1549
+ // context entry's or one `contextPrototypesUnder` already checked against the map
1550
+ out[name] = { ...p, ...contextReturnUnder(heard ?? undefined, p.params, undefined) };
733
1551
  }
734
1552
  }
735
1553
  return out;
736
1554
  }
737
1555
 
738
- /** The keys that state `p`'s struct or union return, or none where it states no such return. */
739
- function aggregateReturnOf(p: FnProto | undefined): Pick<FnProto, 'returns' | 'returnLayout'> {
740
- if (p === undefined || !declaresAggregateReturn(p)) {
741
- return {};
1556
+ /** THE CONTEXT'S OWN ENTRY FOR `own`, where it is a TYPED LIST a lift may bound its argument
1557
+ * registers by — the entry `withContextPrototypes` leaves out — and `undefined` everywhere else.
1558
+ *
1559
+ * It has two readers, and both are SOFT. The context bound (pipeline.ts `liftWithinContextBound`):
1560
+ * a register past it that the lift goes on to read stays a parameter, so a header entry too short
1561
+ * costs a diagnostic, never `uninit_<reg>`. And the entry's struct return (pipeline.ts
1562
+ * `ownStructReturnThroughMemory`), lifted as that struct only where the machine proves the hidden
1563
+ * pointer is its storage, and kept as the pointer function with an `[own-return]` note where it
1564
+ * does not. An entry whose every parameter the bound can lay out is handed over, and so is one
1565
+ * that returns a struct or union by value whatever its parameters say, since what the return reads
1566
+ * is the return alone: a hidden pointer comes first in every parameter list, `...` or a struct
1567
+ * passed by value included (kleod's `struct Unk_0800BEF0_2 sub_0800BEF0(struct Unk_0800BEF0)`).
1568
+ * The bound reads such an entry through `ownArgumentWordsOn`, which lays out none of those lists
1569
+ * and so bounds nothing. Where the entry returns no struct, every other shape ABSTAINS:
1570
+ * - no entry, or one the context dropped (two declarations that conflict: C++ overloads);
1571
+ * - a variadic declaration (`variadicParams`, no `params`): its arity is each caller's;
1572
+ * - `()` (no `params` at all): it states none, and a K&R definition may take any;
1573
+ * - a struct passed by value, or a spelling nothing here sizes: either may fill any number of
1574
+ * words;
1575
+ * - a symbol that names a C++ member function or a mangled free function, whose declaration the
1576
+ * context does not key by the symbol; a context naming one the same is not its declaration, so
1577
+ * this abstains on it whatever it returns.
1578
+ * A bare count is no context entry (`prototypesFromContext` reads lists), and abstains as well. */
1579
+ export function ownContextEntry(context: Prototypes, own: string): FnProto | undefined {
1580
+ const p = Object.hasOwn(context, own) ? context[own] : undefined;
1581
+ if (p === undefined || p === null || typeof p.params === 'number') {
1582
+ return undefined;
742
1583
  }
743
- return {
744
- ...(p.returns !== undefined ? { returns: p.returns } : {}),
745
- ...(p.returnLayout !== undefined ? { returnLayout: p.returnLayout } : {}),
746
- };
1584
+ if (demangledName(own) !== null || implicitArgumentWords(own) !== 0) {
1585
+ return undefined;
1586
+ }
1587
+ if (declaresAggregateReturn(p)) {
1588
+ return p;
1589
+ }
1590
+ if (!Array.isArray(p.params) || p.variadicParams !== undefined) {
1591
+ return undefined;
1592
+ }
1593
+ const params = p.params;
1594
+ const laidOut = params.every((_, k) => {
1595
+ const cls = paramClassOf(p, params, k);
1596
+ return cls === 'word' || cls === 'wide' || cls === 'float';
1597
+ });
1598
+ return laidOut ? p : undefined;
1599
+ }
1600
+
1601
+ /** A TABLE'S OWN CONTEXT ENTRY, TAKEN OUT OF IT: where `prototypes`' entry for `own` carries the
1602
+ * mark (`FnProto.fromContext`), the table without it, and the entry as the soft bound reads it
1603
+ * ({@link ownContextEntry}, which abstains on any shape it cannot lay out); otherwise the table as
1604
+ * it is. `given` — an entry the caller hands over beside the table — wins over a marked one.
1605
+ *
1606
+ * Every entry path asks this before any reader looks at the table, so a marked entry is never
1607
+ * read as a stated one: a stated entry bounds hard, and a context entry a body reads past would
1608
+ * turn a real argument into `uninit_<reg>`. */
1609
+ export function splitOwnContext(
1610
+ prototypes: Prototypes | undefined,
1611
+ own: string,
1612
+ given?: FnProto,
1613
+ ): { prototypes: Prototypes | undefined; ownContextProto: FnProto | undefined } {
1614
+ const p = prototypes !== undefined && Object.hasOwn(prototypes, own) ? prototypes[own] : undefined;
1615
+ if (p?.fromContext !== true) {
1616
+ return { prototypes, ownContextProto: given };
1617
+ }
1618
+ const { [own]: _marked, ...rest } = prototypes!;
1619
+ const { fromContext: _mark, ...entry } = p;
1620
+ return { prototypes: rest, ownContextProto: given ?? ownContextEntry({ [own]: entry }, own) };
1621
+ }
1622
+
1623
+ /** `prototypes` with the context's own entry for `own` carried in it under the mark
1624
+ * ({@link splitOwnContext} reads it back), where the context has one {@link ownContextEntry}
1625
+ * hands over and the table states none: how a benchmark row publishes the entry its lift read, so
1626
+ * that its `--proto` replay lifts as it did. */
1627
+ export function withOwnContextMark(
1628
+ prototypes: Prototypes | undefined,
1629
+ own: string,
1630
+ context: Prototypes,
1631
+ ): Prototypes | undefined {
1632
+ if (prototypes !== undefined && Object.hasOwn(prototypes, own)) {
1633
+ return prototypes;
1634
+ }
1635
+ const entry = ownContextEntry(context, own);
1636
+ return entry === undefined ? prototypes : { ...prototypes, [own]: { ...entry, fromContext: true } };
1637
+ }
1638
+
1639
+ /** The caller's stated entry for `own`, with the struct or union its stated `returns` names laid out
1640
+ * by the context — `struct S`, or a typedef of one, `T8` over `typedef struct {u32 a, b;} T8;` —
1641
+ * read as the context reads a declaration's return (`readSignature`, through a declaration of that
1642
+ * return the context is handed for the purpose). A `--proto` table spells a type and lays nothing
1643
+ * out, and `withContextPrototypes` leaves the context's own entry for `own` out, so without this
1644
+ * the lifted function's own struct return is a spelling: `struct S1` sized by nothing, where the
1645
+ * rule that decides whether it comes back through a hidden pointer reads a size
1646
+ * (`returnsInMemory`), and `T8`, which reads as no aggregate at all and lifts as the pointer
1647
+ * function its hidden pointer makes it (pipeline.ts `ownStructReturnThroughMemory`).
1648
+ * A return already laid out, a scalar, a pointer or a floating type is left as stated, and so is
1649
+ * a name the context does not lay out. */
1650
+ export function withOwnReturnLayout(
1651
+ stated: Prototypes | undefined,
1652
+ own: string,
1653
+ src: string,
1654
+ language: Language,
1655
+ ): Prototypes | undefined {
1656
+ const p = stated !== undefined && Object.hasOwn(stated, own) ? stated[own] : undefined;
1657
+ const r = p?.returns?.trim();
1658
+ if (
1659
+ p === undefined ||
1660
+ p === null ||
1661
+ r === undefined ||
1662
+ p.returnLayout !== undefined ||
1663
+ r === 'void' ||
1664
+ r.includes('*') ||
1665
+ declaredWidth(r) !== undefined ||
1666
+ isFloatingSpelling(r)
1667
+ ) {
1668
+ return stated;
1669
+ }
1670
+ const probe = '__asmlift_own_return';
1671
+ const layout = prototypesFromContext(`${src}\n${r} ${probe}(void);\n`, language)[probe]?.returnLayout;
1672
+ return layout === undefined ? stated : { ...stated, [own]: { ...p, returnLayout: layout } };
747
1673
  }
748
1674
 
749
1675
  const same = (a: FnProto | null, b: FnProto): boolean => a !== null && JSON.stringify(a) === JSON.stringify(b);
@@ -752,9 +1678,28 @@ const same = (a: FnProto | null, b: FnProto): boolean => a !== null && JSON.stri
752
1678
  * lets any entry it is handed shadow the map's signature for that name, so what reaches it decides
753
1679
  * which source wins, per name: a context entry that sizes every parameter wins — it is the
754
1680
  * declaration the candidate is compiled against, and its spellings are the ones a C++ call's casts
755
- * need; an entry that cannot size one yields to a map signature that can be spelled, which sizes by
756
- * byte count what a declaration names (a by-value struct of a register's width); and where the map
757
- * spells nothing either, the context entry stays. */
1681
+ * need; it is taken whole, its return included, and the map is not consulted for it. An entry that
1682
+ * cannot size one yields to a map signature that can be spelled, which sizes by byte count what a
1683
+ * declaration names (a by-value struct of a register's width); and where the map spells nothing
1684
+ * either, the context entry stays.
1685
+ *
1686
+ * AN ENTRY THAT YIELDS ITS PARAMETERS KEEPS THE RETURN IT STATES where `contextReturnUnder` says
1687
+ * it may, because the map's signature carries no `returns`. DWARF does say that a pointer, or a
1688
+ * signed or unsigned base type, comes back in a register; `symbolPrototype` chooses not to spell it
1689
+ * (`FnProto.returns`: a header states it and nothing derives it), because a spelled return also
1690
+ * types the call's result and prints in the candidate's declarations, and the map's `void *` is not
1691
+ * the header's `u8 *` — C++ does not convert it implicitly. So a map-only lift (no context) still
1692
+ * declines this shape, and a context entry dropped whole leaves every non-void call to that name
1693
+ * undeclared — and a frame address handed to an undeclared callee may be its hidden struct-return
1694
+ * pointer (`returnsWithoutHiddenPointer`). pokeemerald's `u8 *ConvertIntToDecimalStringN(u8 *,
1695
+ * s32, enum StringConvertMode, u8)` is the case: its enum parameter has no width its SPELLING
1696
+ * states, so the entry yields its parameters and keeps its `u8 *`.
1697
+ *
1698
+ * THE QUESTION HERE IS ASKED OF THE SPELLINGS, the context's own reading of the names behind them
1699
+ * (`wordParams`, `byValueParams`) set aside: which source states the declaration better is not
1700
+ * which one a lowering can place. The map sizes an enum or an array typedef by its byte count and
1701
+ * spells it, which the context's reading of the name only classifies; so an entry that sizes such
1702
+ * a parameter through that reading still yields to a map that spells it. Whichever entry wins is the one every reader of it takes. */
758
1703
  function contextPrototypesUnder(
759
1704
  context: Prototypes,
760
1705
  symbols: SymbolMap | undefined,
@@ -763,26 +1708,75 @@ function contextPrototypesUnder(
763
1708
  if (symbols === undefined) {
764
1709
  return context;
765
1710
  }
766
- const mapped = new Map<string, FnProto>();
1711
+ const mapped = new Map<string, { signed: FnProto; returned: SymbolTypeFacts | null }>();
767
1712
  for (const infos of symbols.values()) {
768
1713
  for (const info of infos) {
769
1714
  const signed = symbolPrototype(info);
770
- if (signed?.params !== undefined && !mapped.has(info.name)) {
771
- mapped.set(info.name, signed);
1715
+ if (signed?.params !== undefined && info.signature !== undefined && !mapped.has(info.name)) {
1716
+ mapped.set(info.name, { signed, returned: info.signature.returns });
772
1717
  }
773
1718
  }
774
1719
  }
775
- // An entry that yields keeps a struct return it states, over the map's parameters: DWARF names no
776
- // struct, so the map states one only by its size (`symbolPrototype`), and never its members.
777
1720
  return Object.fromEntries(
778
1721
  Object.entries(context).flatMap(([name, p]): [string, FnProto][] => {
779
- const signed = mapped.get(name);
780
- if (declaredCallArgs(p, target) !== undefined || signed === undefined) {
1722
+ const map = mapped.get(name);
1723
+ if (declaredCallArgs({ params: p.params }, target) !== undefined || map === undefined) {
781
1724
  return [[name, p]];
782
1725
  }
783
- return declaresAggregateReturn(p) && signed.returnsVoid !== true
784
- ? [[name, { ...signed, ...aggregateReturnOf(p) }]]
785
- : [];
1726
+ const kept = contextReturnUnder(p, map.signed.params, map.returned);
1727
+ return kept === undefined ? [] : [[name, { ...map.signed, ...kept }]];
786
1728
  }),
787
1729
  );
788
1730
  }
1731
+
1732
+ /** The return a context entry `p` keeps under parameters another source states (`under`: the
1733
+ * map's signature, or a stated entry that says nothing of the return) — or `undefined` when it
1734
+ * keeps none. `returned` is the map's DWARF return where the map's signature is the other source,
1735
+ * `null` for a map that says `void`, and `undefined` where the map is not consulted.
1736
+ *
1737
+ * A `void` return is not kept: whether the callee returns is the map's to state.
1738
+ *
1739
+ * A STRUCT RETURN is kept as stated: DWARF names no struct, so the map states one only by its size
1740
+ * (`symbolPrototype`), and never its members; and keeping it is the safe direction, since it only
1741
+ * says argument 0 may be a hidden pointer.
1742
+ *
1743
+ * A REGISTER RETURN says argument 0 is NOT one, so it is the direction that lifts, and it is kept
1744
+ * only where it is the same fact as the parameters it sits under:
1745
+ * - the context states every parameter, and as many as `under` does — a count `under` is read as
1746
+ * a number of parameters here, though it states registers (`withContextPrototypes`). A variadic
1747
+ * declaration, and a K&R one, state none (`readSignature`), and the map's DWARF parameters
1748
+ * leave the `...` out — so `int pr(char *, const char *, ...)` under a two-parameter map
1749
+ * signature would lower `pr(buf, s, a, b)` as `pr(buf, s)`. Dropped whole, the call declines;
1750
+ * - the return has a width (`declaredWidth`). `float` and `double` have none here, cannot say
1751
+ * anything about a hidden pointer the map's word does not, and on a target with an FPU make
1752
+ * call-plan refuse every call to the name (`$f0`, `f1`), whose result is used or not;
1753
+ * - the map, where it is consulted, does not contradict it: a return the map calls `void`, or a
1754
+ * width its byte size disagrees with (`int` declared, `u8` defined). This is a rule of the merged entry only — its parameters are the map's and its
1755
+ * return the header's, and the two are one fact a table must not hold half of. A context entry
1756
+ * taken whole (it sizes every parameter) never meets the map, and keeps its return unchecked.
1757
+ * Each refusal leaves the call undeclared (`FnProto.returns`). */
1758
+ function contextReturnUnder(
1759
+ p: FnProto | undefined,
1760
+ under: FnProto['params'],
1761
+ returned: SymbolTypeFacts | null | undefined,
1762
+ ): Pick<FnProto, 'returns' | 'returnLayout'> | undefined {
1763
+ if (p === undefined || returned === null || statesNoReturn(p) || declaresVoidReturn(p)) {
1764
+ return undefined;
1765
+ }
1766
+ const kept = {
1767
+ ...(p.returns !== undefined ? { returns: p.returns } : {}),
1768
+ ...(p.returnLayout !== undefined ? { returnLayout: p.returnLayout } : {}),
1769
+ };
1770
+ if (declaresAggregateReturn(p)) {
1771
+ return kept;
1772
+ }
1773
+ const arity = typeof under === 'number' ? under : under?.length;
1774
+ if (!Array.isArray(p.params) || p.params.length !== arity) {
1775
+ return undefined;
1776
+ }
1777
+ const width = p.returns === undefined ? undefined : declaredWidth(p.returns);
1778
+ if (width === undefined) {
1779
+ return undefined;
1780
+ }
1781
+ return returned !== undefined && returned.size !== null && returned.size * 8 !== width ? undefined : kept;
1782
+ }