@asmlift/core 0.9.0 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (101) 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 +1 -0
  5. package/src/backend/cfamily.ts +165 -50
  6. package/src/backend/cpp.ts +2 -1
  7. package/src/backend/pascal.ts +31 -12
  8. package/src/contracts.ts +173 -16
  9. package/src/declare.ts +69 -20
  10. package/src/frontend/asmdata.ts +7 -1
  11. package/src/frontend/call-plan.ts +964 -60
  12. package/src/frontend/dead-reads.ts +628 -0
  13. package/src/frontend/device-pins.ts +23 -4
  14. package/src/frontend/emit.ts +4 -3
  15. package/src/frontend/fpu.ts +224 -17
  16. package/src/frontend/frame-objects.ts +1232 -192
  17. package/src/frontend/frontend.ts +64 -3
  18. package/src/frontend/high-half.ts +53 -4
  19. package/src/frontend/jump-table.ts +530 -0
  20. package/src/frontend/local-object.ts +158 -32
  21. package/src/frontend/mips.ts +2076 -215
  22. package/src/frontend/own-struct-return.ts +500 -0
  23. package/src/frontend/ppc.ts +2711 -232
  24. package/src/frontend/register-struct-call.ts +484 -0
  25. package/src/frontend/reloc-symbol.ts +22 -7
  26. package/src/frontend/ssa.ts +1460 -44
  27. package/src/frontend/stackargs.ts +106 -57
  28. package/src/frontend/thumb.ts +2044 -318
  29. package/src/ir/alias.ts +42 -0
  30. package/src/ir/bits.ts +681 -1
  31. package/src/ir/core.ts +100 -5
  32. package/src/ir/discipline.ts +86 -21
  33. package/src/ir/entry.ts +159 -0
  34. package/src/{structure → ir}/loops.ts +6 -2
  35. package/src/ir/opcodes.ts +109 -9
  36. package/src/ir/types.ts +86 -2
  37. package/src/ir/verify.ts +80 -11
  38. package/src/l3/ast.ts +290 -18
  39. package/src/l3/basecse.ts +33 -5
  40. package/src/l3/constinit.ts +382 -0
  41. package/src/l3/dce.ts +2 -2
  42. package/src/l3/initfirst.ts +42 -11
  43. package/src/l3/intcell.ts +2 -1
  44. package/src/l3/narrowdecl.ts +77 -36
  45. package/src/l3/pollguard.ts +8 -2
  46. package/src/l3/ptrcell.ts +31 -34
  47. package/src/l3/ptrfield.ts +123 -8
  48. package/src/l3/regspell.ts +9 -4
  49. package/src/l3/reindex.ts +283 -96
  50. package/src/l3/storage.ts +98 -1
  51. package/src/l3/symbol-refs.ts +84 -6
  52. package/src/l3/tailmerge.ts +4 -2
  53. package/src/l3/typing.ts +108 -15
  54. package/src/l3/unreduce.ts +6 -5
  55. package/src/l3/volatileptr.ts +17 -54
  56. package/src/mangle.ts +30 -0
  57. package/src/pattern/engine.ts +361 -73
  58. package/src/pipeline.ts +541 -24
  59. package/src/proto-context.ts +905 -45
  60. package/src/proto.ts +555 -40
  61. package/src/raise/aggmembers.ts +290 -0
  62. package/src/raise/arrays.ts +5 -1
  63. package/src/raise/declared-volatile.ts +207 -31
  64. package/src/raise/floathelpers.ts +254 -33
  65. package/src/raise/gvn.ts +3 -3
  66. package/src/raise/latch.ts +162 -6
  67. package/src/raise/memberarrays.ts +10 -4
  68. package/src/raise/offsetnames.ts +10 -26
  69. package/src/raise/pairparams.ts +33 -0
  70. package/src/raise/paramwidth.ts +196 -6
  71. package/src/raise/pre-recovery.ts +102 -19
  72. package/src/raise/pre-structure.ts +54 -0
  73. package/src/raise/recover.ts +12 -5
  74. package/src/raise/reroll.ts +694 -0
  75. package/src/raise/shortcircuit.ts +26 -12
  76. package/src/raise/softdiv.ts +1 -1
  77. package/src/raise/storesign.ts +153 -0
  78. package/src/raise/struct-arrays.ts +30 -11
  79. package/src/raise/structs.ts +396 -33
  80. package/src/raise/truncload.ts +4 -3
  81. package/src/raise/unthread.ts +400 -0
  82. package/src/rank-declare.ts +118 -12
  83. package/src/rank-variations.ts +89 -31
  84. package/src/rank.ts +307 -54
  85. package/src/runtime-helpers.ts +140 -22
  86. package/src/structure/analysis.ts +165 -2
  87. package/src/structure/arith-ops.ts +23 -0
  88. package/src/structure/globalaccess.ts +111 -9
  89. package/src/structure/hazards.ts +16 -64
  90. package/src/structure/local-statics.ts +145 -9
  91. package/src/structure/namecoalesce.ts +1 -1
  92. package/src/structure/pointer-spelling.ts +1106 -0
  93. package/src/structure/redundant-test.ts +1 -1
  94. package/src/structure/structure.ts +2180 -1054
  95. package/src/structure/switch-recover.ts +244 -100
  96. package/src/symbols.ts +6 -0
  97. package/src/target.ts +765 -52
  98. package/src/trace.ts +26 -8
  99. package/src/variation-definitions.ts +126 -14
  100. package/src/variation-gates.ts +2 -0
  101. package/src/variation-tokens.ts +5 -1
@@ -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,20 @@ 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
23
  declaredWidth,
24
24
  declaresAggregateReturn,
25
+ declaresVoidReturn,
25
26
  isFloatingSpelling,
27
+ paramClassOf,
26
28
  statesNoReturn,
27
29
  symbolPrototype,
28
30
  validatePrototypes,
29
31
  } from './proto';
30
- import type { SymbolMap } from './symbols';
32
+ import type { SymbolMap, SymbolTypeFacts } from './symbols';
31
33
  import type { TargetDescription } from './target';
32
34
 
33
35
  // asmlift — callee prototypes read out of a DECLARATION CONTEXT: the preprocessed headers a
@@ -68,6 +70,379 @@ export function prototypesFromContext(src: string, language: Language): Prototyp
68
70
  return functionPrototypes(ctx, table, layoutReader(ctx, table));
69
71
  }
70
72
 
73
+ /** What a preprocessed declaration context states about the globals it declares, beyond their
74
+ * prototypes. The sets travel together: the aggregate spelling `*(T *)&g` is a cast, which drops
75
+ * the declaration's qualifiers, so a reader handed the aggregates without the volatile objects
76
+ * would print a plain read of a volatile one. */
77
+ export interface ContextGlobals {
78
+ /** The objects declared as a struct, a union or an array. Such an object has no value a bare name
79
+ * reads: `v = gInfo;` over `extern struct Info gInfo;` is a compile error, and over
80
+ * `extern u8 gBuf[];` it reads the address. A project whose symbol map does not describe a
81
+ * global (one its linker script places has no DWARF) states it here, so the structurer reads
82
+ * this set where it would read the map's `shape`. A type this cannot resolve — an unexpanded
83
+ * macro — is left out, which keeps the usage-inferred spelling. */
84
+ aggregates: ReadonlySet<string>;
85
+ /** The objects any byte of which is volatile (`hasVolatileStorage`): the object, an array's
86
+ * element, a typedef in its chain, or a member of a struct or union in it, nested or anonymous.
87
+ * A body this cannot read counts as volatile. The set is not per byte: a read of the plain member
88
+ * of `struct { vu16 a; u16 b; }` is placed too, which costs at most a read the compiler kept that
89
+ * the source did not ask it to, never one it may drop. raise/declared-volatile.ts stamps every
90
+ * access to such an object the map does not lay out, and the structurer then puts the qualifier
91
+ * on the cast (`*(volatile u16 *)&gV`). Scalars are in it too: the header already qualifies
92
+ * `gReg` where it is spelled bare, and the stamp keeps the raising passes from merging two of its
93
+ * reads. */
94
+ volatiles: ReadonlySet<string>;
95
+ /** The objects declared as a pointer whose pointee has a volatile byte, read as `volatiles` reads
96
+ * an object: `extern volatile u16 *gP;`, `extern volatile struct V *gQ;`. The pointer itself is
97
+ * plain, so a read of `gP` is not placed; an access through the value it holds is, and the
98
+ * structurer puts the qualifier on the cast it is spelled through (`*(volatile u16 *)gP`).
99
+ * Without it, `while (*gP == 0);` lifts as a plain read the compiler hoists out of the loop. */
100
+ pointeeVolatiles: ReadonlySet<string>;
101
+ /** Every object the context declares, volatile or not; a function is no object. Read beside
102
+ * `volatiles`: an object here and not there is declared with no volatile byte, which is what the
103
+ * dead-read pin's `declared-plain` gate reads (frontend/dead-reads.ts) — the source spelled a
104
+ * read of it plain, so a read the target keeps that nothing uses is not the source's `volatile`. */
105
+ declared: ReadonlySet<string>;
106
+ /** The type each object of `declared` that is no array, pointer or function is declared as, as
107
+ * written (`Q_t`, `struct Q`). An object passed bare as a struct by value must be declared as
108
+ * that struct (frontend/call-plan.ts `foldByValueArgs`): `gQs` over `extern struct Q gQs[4];` or
109
+ * `gO2` over `extern struct O2 gO2;` names the right address and is a type error as an argument
110
+ * of `struct Q`. Compared by the struct it names (`structNamed`, which follows the typedefs),
111
+ * not by spelling. */
112
+ objectTypes: ReadonlyMap<string, string>;
113
+ /** The struct or union each typedef name stands for, as far as its chain goes with no
114
+ * derivation: by its tag (`struct Q`), or, for an untagged body, by the typedef name that binds
115
+ * it. Read through `structNamed`, which is how two spellings of one struct are told to be one:
116
+ * `typedef struct Q {…} Q;` spells the struct `Q`, `struct Q` and every other alias of it. */
117
+ structTypes: ReadonlyMap<string, string>;
118
+ }
119
+
120
+ /** The struct or union `spelling` names, qualifiers aside, by the identity `structTypes` keys it
121
+ * under: `struct Tag` as written, or what a typedef name stands for. Undefined for any other
122
+ * spelling, and for a name the context does not resolve to one — which then names no struct this
123
+ * can compare, and the spellings are compared as written. */
124
+ export function structNamed(
125
+ spelling: string,
126
+ context: Pick<ContextGlobals, 'structTypes'> | undefined,
127
+ ): string | undefined {
128
+ const s = spelling
129
+ .replace(/\b(?:const|volatile)\b/g, ' ')
130
+ .trim()
131
+ .replace(/\s+/g, ' ');
132
+ return /^(?:struct|union) [A-Za-z_$][\w$]*$/.test(s) ? s : context?.structTypes.get(s);
133
+ }
134
+
135
+ /** The globals a preprocessed declaration context declares, read for {@link ContextGlobals}. */
136
+ export function globalsFromContext(src: string, language: Language): ContextGlobals {
137
+ const ctx = parseDeclarations(src, language);
138
+ const table = typeTable(ctx);
139
+ const scope: VolatileScope = { table, typedefs: typedefTypes(ctx), bodies: tagBodies(ctx), ctx, read: new Map() };
140
+ const aggregates = new Set<string>();
141
+ const volatiles = new Set<string>();
142
+ const pointeeVolatiles = new Set<string>();
143
+ const declared = new Set<string>();
144
+ const objectTypes = new Map<string, string>();
145
+ for (const d of ctx.declarations) {
146
+ if (d.specifiers.typedef || d.body !== undefined) {
147
+ continue;
148
+ }
149
+ for (const x of d.declarators) {
150
+ if (x.name === undefined || !WORD.test(x.name)) {
151
+ continue;
152
+ }
153
+ const t = declaredType(d.specifiers, x.derivations);
154
+ if (t.derivations[0]?.kind !== 'function') {
155
+ declared.add(x.name);
156
+ }
157
+ if (t.derivations.length === 0) {
158
+ objectTypes.set(x.name, t.spelling);
159
+ }
160
+ if (isAggregate(t, table, scope.typedefs)) {
161
+ aggregates.add(x.name);
162
+ }
163
+ if (hasVolatileStorage(t, scope, 0)) {
164
+ volatiles.add(x.name);
165
+ }
166
+ // derivations run outermost first: the object is a pointer, and what it points at is the rest
167
+ if (t.derivations[0]?.kind === 'pointer') {
168
+ const pointee = { ...t, derivations: t.derivations.slice(1) };
169
+ if (hasVolatileStorage(pointee, scope, 0)) {
170
+ pointeeVolatiles.add(x.name);
171
+ }
172
+ }
173
+ }
174
+ }
175
+ const structTypes = new Map<string, string>();
176
+ for (const name of scope.typedefs.keys()) {
177
+ const named = structOfTypedef(name, scope.typedefs);
178
+ if (named !== undefined) {
179
+ structTypes.set(name, named);
180
+ }
181
+ }
182
+ return { aggregates, volatiles, pointeeVolatiles, declared, objectTypes, structTypes };
183
+ }
184
+
185
+ /** The struct or union the typedef `name` stands for (`ContextGlobals.structTypes`): its tag where
186
+ * the chain ends at one, else the last name in the chain, which binds the untagged body. */
187
+ function structOfTypedef(name: string, typedefs: ReadonlyMap<string, DeclaredType>): string | undefined {
188
+ let binder = name;
189
+ let at = typedefs.get(name);
190
+ for (let hops = 0; hops < 16 && at !== undefined && at.derivations.length === 0; hops++) {
191
+ const t = at.type;
192
+ if (t.kind === 'tag') {
193
+ return t.keyword === 'struct' || t.keyword === 'union'
194
+ ? t.tag === undefined
195
+ ? binder
196
+ : `${t.keyword} ${t.tag}`
197
+ : undefined;
198
+ }
199
+ if (t.words.length !== 1 || at.unknownWords.length > 0) {
200
+ return undefined;
201
+ }
202
+ binder = t.words[0];
203
+ at = typedefs.get(binder);
204
+ }
205
+ return undefined;
206
+ }
207
+
208
+ /** Every struct, union and class body the context defines, by `keyword Tag` (and, in C++, by the bare
209
+ * tag, as C++ spells one), attributed or not, at the top level and nested in another body. */
210
+ function tagBodies(ctx: ParsedContext): Map<string, Range> {
211
+ const out = new Map<string, Range>();
212
+ const visit = (ds: readonly Declaration[], depth: number): void => {
213
+ for (const d of ds) {
214
+ const t = d.specifiers.type;
215
+ if (t.kind !== 'tag' || t.keyword === 'enum' || t.body === undefined) {
216
+ continue;
217
+ }
218
+ if (t.tag !== undefined && WORD.test(t.tag)) {
219
+ out.set(`${t.keyword} ${t.tag}`, t.body);
220
+ if (ctx.language === 'c++') {
221
+ out.set(t.tag, t.body);
222
+ }
223
+ }
224
+ const members = depth < 8 ? memberDeclarations(ctx, t.body) : undefined;
225
+ if (members !== undefined) {
226
+ visit(members, depth + 1);
227
+ }
228
+ }
229
+ };
230
+ visit(ctx.declarations, 0);
231
+ return out;
232
+ }
233
+
234
+ /** Every typedef name the context declares, and the type it stands for as written: with the body a
235
+ * typedef defines (`typedef struct { vu8 a; } S;`), its qualifiers (`typedef volatile struct {…} S;`)
236
+ * and an array's bounds (`typedef vu8 Buf[4];`), none of which the type table keeps — it binds a
237
+ * body's name to the name itself, and it reads no typedef of an array. */
238
+ function typedefTypes(ctx: ParsedContext): Map<string, DeclaredType> {
239
+ const out = new Map<string, DeclaredType>();
240
+ for (const d of ctx.declarations) {
241
+ if (!d.specifiers.typedef) {
242
+ continue;
243
+ }
244
+ for (const x of d.declarators) {
245
+ const t = declaredType(d.specifiers, x.derivations);
246
+ // `typedef S S;` restates a name and keeps the type it already stands for
247
+ const restates = t.type.kind === 'words' && t.type.words.length === 1 && t.type.words[0] === x.name;
248
+ if (x.name !== undefined && WORD.test(x.name) && !(restates && t.derivations.length === 0)) {
249
+ out.set(x.name, t);
250
+ }
251
+ }
252
+ }
253
+ return out;
254
+ }
255
+
256
+ /** What `hasVolatileStorage` reads a type through, and what it has already read: each typedef name
257
+ * and each body by its first token, so a type every member reaches through the same typedefs
258
+ * (`mActor_name_t`, then `u16`) is walked once, and a long chain of them costs no nesting depth. */
259
+ interface VolatileScope {
260
+ table: TypeTable;
261
+ typedefs: ReadonlyMap<string, DeclaredType>;
262
+ bodies: ReadonlyMap<string, Range>;
263
+ ctx: ParsedContext;
264
+ read: Map<string, boolean>;
265
+ }
266
+
267
+ /** Whether any byte of an object of type `t` is volatile: the type or an array's element is
268
+ * qualified, through the typedef names it is built on, or a struct or union in it has such a member.
269
+ * A pointer ends the walk at its own qualifiers, since what it points at is no byte of the object.
270
+ * A body that cannot be read, or bodies nested past the bound, is read as volatile: a qualifier
271
+ * placed where none was declared only keeps a read the compiler may have dropped. A tag with no
272
+ * body here has no member to qualify.
273
+ * A name met again while it is being read is a cycle, read as volatile (`once`). */
274
+ function hasVolatileStorage(t: DeclaredType, scope: VolatileScope, depth: number): boolean {
275
+ if (depth > 32) {
276
+ return true;
277
+ }
278
+ for (const x of t.derivations) {
279
+ if (x.kind === 'pointer') {
280
+ return x.qualifiers.includes('volatile');
281
+ }
282
+ if (x.kind !== 'array') {
283
+ return false;
284
+ }
285
+ }
286
+ // GNU's alternate spellings, which the declaration parser keeps as words it does not know, or as
287
+ // the type's own name when a typedef name follows (`__volatile__ u8 a;`)
288
+ const words = t.type.kind === 'words' ? [...t.type.words, ...t.unknownWords] : t.unknownWords;
289
+ if (t.qualifiers.includes('volatile') || words.some((w) => w === '__volatile' || w === '__volatile__')) {
290
+ return true;
291
+ }
292
+ let body: Range | undefined;
293
+ if (t.type.kind === 'tag') {
294
+ if (t.type.keyword === 'enum') {
295
+ return false;
296
+ }
297
+ body = t.type.body ?? (t.type.tag !== undefined ? scope.bodies.get(`${t.type.keyword} ${t.type.tag}`) : undefined);
298
+ } else if (t.type.words.length === 1) {
299
+ const name = t.type.words[0];
300
+ const next = scope.typedefs.get(name);
301
+ if (next !== undefined) {
302
+ const resolves = next.type.kind === 'words' && next.type.words.length === 1 && next.type.words[0] === name;
303
+ return !resolves && once(scope, `typedef ${name}`, () => hasVolatileStorage(next, scope, depth));
304
+ }
305
+ body = scope.table.named.get(name)?.body ?? scope.bodies.get(name);
306
+ }
307
+ if (body === undefined) {
308
+ return false;
309
+ }
310
+ const members = memberDeclarations(scope.ctx, body);
311
+ const bases = baseClause(scope.ctx, body);
312
+ if (members === undefined || bases === undefined) {
313
+ return true;
314
+ }
315
+ // a C++ member typedef names a type for the members after it, so they are read through it, in a
316
+ // scope of their own. That scope also reads a body defined elsewhere through it, which can only
317
+ // leave more out.
318
+ const local = typedefTypes({ ...scope.ctx, declarations: members });
319
+ const inner: VolatileScope =
320
+ local.size === 0 ? scope : { ...scope, typedefs: new Map([...scope.typedefs, ...local]), read: new Map() };
321
+ return once(
322
+ scope,
323
+ `body ${body.from}`,
324
+ () =>
325
+ // a C++ base's members are bytes of the object too, and a base with no body here (one a
326
+ // namespace defines, which this does not read) is read as volatile
327
+ bases.some((b) => !definesBody(b, inner) || hasVolatileStorage(b, inner, depth + 1)) ||
328
+ members.some((m) =>
329
+ // a C++ member typedef or static member is no byte of the object
330
+ m.specifiers.typedef || m.specifiers.storage.includes('static')
331
+ ? false
332
+ : m.declarators.length === 0
333
+ ? hasVolatileStorage(declaredType(m.specifiers, []), inner, depth + 1)
334
+ : m.declarators.some((x) =>
335
+ hasVolatileStorage(declaredType(m.specifiers, x.derivations), inner, depth + 1),
336
+ ),
337
+ ),
338
+ );
339
+ }
340
+
341
+ /** The C++ base classes ahead of `body`, each as the type its name spells, or undefined when a
342
+ * clause holds anything else (a qualified name, a template argument list, `decltype`), which the
343
+ * caller reads as volatile. Read backwards from the `{` to the clause's `:`; reaching the
344
+ * tag keyword first means there is no clause. */
345
+ function baseClause(ctx: ParsedContext, body: Range): DeclaredType[] | undefined {
346
+ if (ctx.language !== 'c++') {
347
+ return [];
348
+ }
349
+ const t = ctx.tokens;
350
+ let k = body.from - 2;
351
+ for (; k >= 0 && !t.is(k, ':'); k--) {
352
+ if (TAG_KEYWORDS.has(t.text(k))) {
353
+ return [];
354
+ }
355
+ if (t.kind(k) !== 'identifier' && !t.is(k, '::') && !t.is(k, ',')) {
356
+ // an attribute, a template argument list: a `:` between it and the tag keyword is a clause
357
+ // this does not read, and none means there is no clause
358
+ for (let j = k; j >= 0 && !t.is(j, ';') && !t.is(j, '}') && !t.is(j, '{') && !TAG_KEYWORDS.has(t.text(j)); j--) {
359
+ if (t.is(j, ':')) {
360
+ return undefined;
361
+ }
362
+ }
363
+ return [];
364
+ }
365
+ }
366
+ if (k < 0) {
367
+ return [];
368
+ }
369
+ const out: DeclaredType[] = [];
370
+ let last: string | undefined;
371
+ for (let j = k + 1; j < body.from; j++) {
372
+ if (t.is(j, ',') || j === body.from - 1) {
373
+ if (last === undefined) {
374
+ return undefined;
375
+ }
376
+ out.push({
377
+ qualifiers: [],
378
+ type: { kind: 'words', words: [last] },
379
+ unknownWords: [],
380
+ spelling: last,
381
+ derivations: [],
382
+ });
383
+ last = undefined;
384
+ } else if (t.is(j, '::')) {
385
+ return undefined;
386
+ } else if (t.kind(j) === 'identifier' && !BASE_WORDS.has(t.text(j))) {
387
+ last = t.text(j);
388
+ }
389
+ }
390
+ return out;
391
+ }
392
+
393
+ /** Whether the base `b` names a typedef or a body this context defines. */
394
+ function definesBody(b: DeclaredType, scope: VolatileScope): boolean {
395
+ const name = b.spelling;
396
+ return scope.typedefs.has(name) || scope.table.named.get(name)?.body !== undefined || scope.bodies.has(name);
397
+ }
398
+
399
+ const TAG_KEYWORDS: ReadonlySet<string> = new Set(['struct', 'class', 'union', 'enum']);
400
+ const BASE_WORDS: ReadonlySet<string> = new Set(['public', 'protected', 'private', 'virtual']);
401
+
402
+ /** `read()`, remembered under `key`. A key met again while it is being read is a cycle no compiler
403
+ * lays out (`typedef T U; typedef U T;`, which C11 and C++ accept), read as volatile, for the same
404
+ * reason as an unreadable body. */
405
+ function once(scope: VolatileScope, key: string, read: () => boolean): boolean {
406
+ const known = scope.read.get(key);
407
+ if (known !== undefined) {
408
+ return known;
409
+ }
410
+ scope.read.set(key, true);
411
+ const v = read();
412
+ scope.read.set(key, v);
413
+ return v;
414
+ }
415
+
416
+ /** Whether `t` is a struct, union, class or array type, through the typedef names it is built on.
417
+ * A typedef the type table does not read — one of an array, `typedef u16 Arr4[4];` — is followed
418
+ * as written (`typedefTypes`), or `extern Arr4 gTB;` reads as a scalar whose offset-0 read is
419
+ * spelled `gTB`, the array's address. */
420
+ function isAggregate(t: DeclaredType, table: TypeTable, written: ReadonlyMap<string, DeclaredType>): boolean {
421
+ let cur = t;
422
+ for (let hops = 0; hops < 16; hops++) {
423
+ const [first] = cur.derivations;
424
+ if (first !== undefined) {
425
+ return first.kind === 'array';
426
+ }
427
+ if (cur.type.kind === 'tag') {
428
+ return cur.type.keyword !== 'enum';
429
+ }
430
+ const name = nameOf(cur);
431
+ if (name === undefined) {
432
+ return false;
433
+ }
434
+ if (table.named.has(name)) {
435
+ return true;
436
+ }
437
+ const next = table.typedefs.get(name) ?? written.get(name);
438
+ if (next === undefined || nameOf(next) === name) {
439
+ return false;
440
+ }
441
+ cur = next;
442
+ }
443
+ return false;
444
+ }
445
+
71
446
  const WORD = /^[A-Za-z_]\w*$/;
72
447
 
73
448
  /** `mode(…)` in an attribute, in either spelling: it retypes what it is written on */
@@ -165,6 +540,14 @@ function readTag(
165
540
  if ((attributed || wide) && name !== undefined) {
166
541
  table.unsizedEnums.add(`enum ${name}`);
167
542
  }
543
+ // In C++ an enum's tag names its type with no keyword, as a typedef bound to its body does in C
544
+ // (`readTypedefs`), so the bare `Fmt` of `enum Fmt { … }` is that enum, sized as it is.
545
+ if (ctx.language === 'c++' && name !== undefined) {
546
+ table.enums.add(name);
547
+ if (attributed || wide) {
548
+ table.unsizedEnums.add(name);
549
+ }
550
+ }
168
551
  return attributed || wide;
169
552
  }
170
553
 
@@ -580,17 +963,82 @@ function memberShape(
580
963
  return { name: d.name, pointer: stars.length > 0, ...(dims.length > 0 ? { dims } : {}), returns: [] };
581
964
  }
582
965
 
966
+ /** A declaration's reading: the entry as written, the `key` two declarations are compared by, and
967
+ * the `lists` of every declaration it stands for, which say whether a declaration that states no
968
+ * parameters can be one with a declaration that does (`composable`). */
969
+ type Read = { proto: FnProto; key: FnProto; lists: readonly ParameterList[] };
970
+
971
+ /** What a declaration's parentheses say of its parameters, as `composable` needs it: `()` in C,
972
+ * which states none; a list this could not read, with how many FIXED parameters its top-level commas
973
+ * separate and whether it ends in `...`; or a list it read, with whether every parameter keeps its
974
+ * type under the default argument promotions (C99 6.5.2.2p6) — undefined where one may not. */
975
+ type ParameterList =
976
+ | { kind: 'unstated' }
977
+ | { kind: 'unread'; fixed: number; variadic: boolean }
978
+ | { kind: 'read'; fixed: number; variadic: boolean; promotes: boolean };
979
+
583
980
  /** Every function the declarations name by a plain identifier, keyed by it. A declaration that
584
981
  * spells no type is a constructor's, a destructor's or a conversion operator's; one that defines a
585
982
  * body, or spells a `class`, declares no C function. */
586
983
  function functionPrototypes(ctx: ParsedContext, table: TypeTable, layoutOf: LayoutOf): Prototypes {
587
- const found = new Map<string, FnProto | null>();
984
+ const found = new Map<string, Read | null>();
985
+ const written = typedefTypes(ctx);
588
986
  // a function one of whose declarations retypes a parameter with `mode`, which gives it the type
589
987
  // the mode names in place of the one spelled (c-common.c:563): compiled, `int x
590
988
  // __attribute__((mode(DI)))` takes a register pair. Its parameters are not read, in any
591
989
  // declaration of it; its return is.
592
990
  const unreadParams = new Set<string>();
593
- const returnOnly = ({ params: _unread, ...rest }: FnProto): FnProto => rest;
991
+ // Two declarations of one function name one type, and the entry is that type's: the same reading
992
+ // twice, or — in C — a declaration whose parameters are not read (`()`, which states none, or a
993
+ // list this cannot read, `const char *restrict fmt`) beside one whose are, where the return reads
994
+ // alike and the two are COMPATIBLE (`composable`). The C compiler takes the composite of the two
995
+ // (C99 6.2.7p3), which is the one that states the parameters, so the entry is that one: dropping
996
+ // the name would drop the refusals its parameters carry with it, and send the call to the arity
997
+ // guess. Anything else is a conflict (C++ overloads, or a header this misreads), and the name is
998
+ // dropped.
999
+ //
1000
+ // A `()` is read with a list only where it PRECEDES it: one that follows a list is a conflict,
1001
+ // whatever the types (below). And `()` beside a list that is not compatible with it is no
1002
+ // composite: agbcc, gcc2.7.2kmc and
1003
+ // gcc2.7.2 compile `void f(char); void f();` without a word, the later `()` wins, and a call is
1004
+ // compiled UNPROTOTYPED — any arity, no conversion. Read as `f(char)`, `f(1, 2)` would lift as
1005
+ // `f(1)`, and `f(5)` under `f(short, char)` as `f(5, a1)`.
1006
+ const unstated = (p: FnProto) => p.params === undefined && p.variadicParams === undefined;
1007
+ const merged = (kept: Read, a: Read, b: Read): Read => ({ ...kept, lists: [...a.lists, ...b.lists] });
1008
+ const composite = (a: Read, b: Read): Read | null => {
1009
+ if (b.lists.some((l) => l.kind === 'unstated') && a.lists.some((l) => l.kind === 'read')) {
1010
+ // a `()` AFTER a parameter list: mwcc (2.4.2, 2.4.7, 2.3.3) lets it replace the prototype
1011
+ // outright, even where every parameter is a word — `void f(int); void f();` compiles `f(1, 2)`
1012
+ // as two arguments, `f(1.5)` in f1, and `f(1)` under `void f(double)` in r3 — so the list is
1013
+ // no fact about the call. Read as the list, `f(1, 2)` lifts as `f(1)`. gcc and IDO reject what
1014
+ // that call would need, so nothing is lost on them by a conflict.
1015
+ return null;
1016
+ }
1017
+ if (same(a.key, b.key)) {
1018
+ return merged(b, a, b);
1019
+ }
1020
+ // …and where the list follows a `()` that itself followed a list this could not read, the `()`
1021
+ // still replaced it: the two keep their return (`same`, above), and no later list joins them
1022
+ const all = [...a.lists, ...b.lists];
1023
+ if (
1024
+ ctx.language !== 'c' ||
1025
+ all.some((l, i) => l.kind === 'unstated' && all.slice(0, i).some((m) => m.kind !== 'unstated'))
1026
+ ) {
1027
+ return null;
1028
+ }
1029
+ if (unstated(a.key) && same(a.key, returnOnly(b.key)) && composable(a.lists, b.lists)) {
1030
+ return merged(b, a, b);
1031
+ }
1032
+ return unstated(b.key) && same(b.key, returnOnly(a.key)) && composable(b.lists, a.lists) ? merged(a, a, b) : null;
1033
+ };
1034
+ const returnOnly = ({
1035
+ params: _unread,
1036
+ wordParams: _words,
1037
+ byValueParams: _copies,
1038
+ byValueLayouts: _layouts,
1039
+ variadicParams: _fixed,
1040
+ ...rest
1041
+ }: FnProto): FnProto => rest;
594
1042
  for (const d of ctx.declarations) {
595
1043
  const s = d.specifiers;
596
1044
  if (
@@ -614,15 +1062,40 @@ function functionPrototypes(ctx: ParsedContext, table: TypeTable, layoutOf: Layo
614
1062
  if (retyped) {
615
1063
  unreadParams.add(x.name);
616
1064
  }
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;
1065
+ const reading = (qualifiers: 'kept' | 'dropped') =>
1066
+ readSignature(
1067
+ declaredType(s, returns),
1068
+ fn,
1069
+ ctx,
1070
+ table.typedefs,
1071
+ (t) => layoutOf(t, 0),
1072
+ (t) => passingOf(t, table, written),
1073
+ qualifiers,
1074
+ );
1075
+ const read = reading('kept');
1076
+ // what the declaration says of the function's TYPE, which is what two declarations of it are
1077
+ // compared by: a parameter's top-level qualifiers are not part of it (C99 6.7.5.3p15), so
1078
+ // `void takeW(const struct W w);` and `void takeW(struct W w);` declare one function. Read
1079
+ // as a conflict, they would drop the name, and with it every refusal its entry carries
1080
+ // (`byValueRefusal`, `variadicRefusal`): the call would take the arity guess they exist to
1081
+ // refuse. The entry kept is the later declaration's, spelled as written.
1082
+ const typed = reading('dropped');
1083
+ const [proto, key] = unreadParams.has(x.name) ? [returnOnly(read), returnOnly(typed)] : [read, typed];
1084
+ const lists = [parameterList(fn, ctx, table.typedefs, (t) => passingOf(t, table, written))];
619
1085
  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);
1086
+ const had =
1087
+ prior && unreadParams.has(x.name)
1088
+ ? { ...prior, proto: returnOnly(prior.proto), key: returnOnly(prior.key) }
1089
+ : prior;
1090
+ found.set(
1091
+ x.name,
1092
+ had === undefined ? { proto, key, lists } : had === null ? null : composite(had, { proto, key, lists }),
1093
+ );
622
1094
  }
623
1095
  }
624
1096
  const out: Prototypes = {};
625
- for (const [name, p] of found) {
1097
+ for (const [name, read] of found) {
1098
+ const p = read?.proto ?? null;
626
1099
  const valid = p === null ? undefined : admissible(name, p);
627
1100
  if (valid !== undefined) {
628
1101
  out[name] = valid;
@@ -631,6 +1104,101 @@ function functionPrototypes(ctx: ParsedContext, table: TypeTable, layoutOf: Layo
631
1104
  return out;
632
1105
  }
633
1106
 
1107
+ /** Whether declarations that state no parameters (`unstated`, each `()` or a list this could not
1108
+ * read) are compatible with ones that state them (`stated`), as two declarations of one C function
1109
+ * must be for their composite to be the stated one (C99 6.7.5.3p15). `()` is compatible with a
1110
+ * list that is not variadic and whose every parameter keeps its type under the default argument
1111
+ * promotions: not `char`, `short`, `float`, `_Bool`, nor a typedef or an enum that may be one — an
1112
+ * enum's type is the compiler's to pick, and it may be narrow (`-fshort-enums`, mwcc's `-enum
1113
+ * min`). A name the context never resolves is read by its spelling as `declaredWidth` reads it
1114
+ * (`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. */
1115
+ function composable(unstated: readonly ParameterList[], stated: readonly ParameterList[]): boolean {
1116
+ const lists = stated.filter((l) => l.kind === 'read');
1117
+ return (
1118
+ lists.length > 0 &&
1119
+ unstated.every((u) =>
1120
+ lists.every((l) =>
1121
+ u.kind === 'unstated'
1122
+ ? !l.variadic && !l.promotes
1123
+ : u.kind === 'unread' && u.fixed === l.fixed && u.variadic === l.variadic,
1124
+ ),
1125
+ )
1126
+ );
1127
+ }
1128
+
1129
+ /** Whether `fn`'s parentheses are C's empty list: `(void)`, or one parameter whose type is `void`
1130
+ * through a typedef (`typedef void V; int k(V);`, C99 6.7.5.3p10). Read as a list of one
1131
+ * parameter spelled `void`, the second would have the call pass a register nothing set up for it
1132
+ * (`k()` as `k(a0)`). */
1133
+ function isVoidList(
1134
+ fn: Extract<Derivation, { kind: 'function' }>,
1135
+ tokens: Tokens,
1136
+ typedefs: ReadonlyMap<string, DeclaredType>,
1137
+ ): boolean {
1138
+ if (fn.list.to === fn.list.from + 1 && tokens.is(fn.list.from, 'void')) {
1139
+ return true;
1140
+ }
1141
+ const only = fn.params?.length === 1 && !fn.variadic ? fn.params[0] : undefined;
1142
+ return (
1143
+ only !== undefined &&
1144
+ spellType(resolve(declaredType(only.specifiers, only.declarator.derivations), typedefs, tokens), tokens) === 'void'
1145
+ );
1146
+ }
1147
+
1148
+ /** What `fn`'s parentheses say of its parameters (`ParameterList`). */
1149
+ function parameterList(
1150
+ fn: Extract<Derivation, { kind: 'function' }>,
1151
+ { tokens, language }: ParsedContext,
1152
+ typedefs: ReadonlyMap<string, DeclaredType>,
1153
+ passing: (t: DeclaredType) => Passing,
1154
+ ): ParameterList {
1155
+ const { params, list } = fn;
1156
+ if (params === undefined) {
1157
+ // the top-level commas, and a trailing `...`
1158
+ let items = list.to > list.from ? 1 : 0;
1159
+ let variadic = false;
1160
+ for (let i = list.from; i < list.to; i++) {
1161
+ const close = tokens.match(i);
1162
+ if (close > i) {
1163
+ i = close;
1164
+ } else if (tokens.is(i, ',')) {
1165
+ items++;
1166
+ } else if (tokens.is(i, '...')) {
1167
+ variadic = true;
1168
+ }
1169
+ }
1170
+ return { kind: 'unread', fixed: variadic ? items - 1 : items, variadic };
1171
+ }
1172
+ if (params.length === 0 && !fn.variadic && language === 'c') {
1173
+ return { kind: 'unstated' };
1174
+ }
1175
+ if (isVoidList(fn, tokens, typedefs)) {
1176
+ return { kind: 'read', fixed: 0, variadic: false, promotes: false };
1177
+ }
1178
+ const promotes = params.some((p) => {
1179
+ const declared = asParameter(declaredType(p.specifiers, p.declarator.derivations));
1180
+ if (passing(declared) === 'by-value') {
1181
+ // a struct, union or class, which no promotion converts
1182
+ return false;
1183
+ }
1184
+ const t = resolve(declared, typedefs, tokens);
1185
+ if (t.derivations.length > 0) {
1186
+ // a pointer, or an array or a function the parameter adjusts to one
1187
+ return false;
1188
+ }
1189
+ if (t.type.kind === 'tag') {
1190
+ return t.type.keyword === 'enum';
1191
+ }
1192
+ const spelled = spellType(withoutTopQualifiers(t), tokens);
1193
+ if (spelled === 'double' || spelled === 'long double') {
1194
+ return false;
1195
+ }
1196
+ const width = declaredWidth(spelled);
1197
+ return width === undefined || width < 32;
1198
+ });
1199
+ return { kind: 'read', fixed: params.length, variadic: fn.variadic, promotes };
1200
+ }
1201
+
634
1202
  /** The entry as a table `validatePrototypes` accepts, the check every `--proto` table passes, so a
635
1203
  * context's prototypes can travel as one: a `returns` it refuses is dropped — a return wider than a
636
1204
  * register with a parameter list that cannot be printed — and the entry with it if that is not
@@ -649,6 +1217,8 @@ function readSignature(
649
1217
  { tokens, language }: ParsedContext,
650
1218
  typedefs: ReadonlyMap<string, DeclaredType>,
651
1219
  layoutOf: (t: DeclaredType) => AggregateLayout | undefined,
1220
+ passing: (t: DeclaredType) => Passing,
1221
+ qualifiers: 'kept' | 'dropped' = 'kept',
652
1222
  ): FnProto {
653
1223
  const proto: FnProto = {};
654
1224
  const resolved = resolve(ret, typedefs, tokens);
@@ -689,34 +1259,146 @@ function readSignature(
689
1259
  }
690
1260
  return proto;
691
1261
  }
692
- if (fn.list.to === fn.list.from + 1 && tokens.is(fn.list.from, 'void')) {
1262
+ if (isVoidList(fn, tokens, typedefs)) {
693
1263
  proto.params = [];
694
1264
  return proto;
695
1265
  }
1266
+ // Each parameter's type, its name taken off: an array or a function nearest the name is a
1267
+ // pointer, and the typedef names it is built on resolve as a return's do. A spelling that sizes to
1268
+ // nothing and is no floating type is read through those names for what it positively is: a type
1269
+ // that still travels in one register — an enum, or an array or function type behind a typedef
1270
+ // name, which the parameter adjusts to a pointer (`GXTexFmt`, `Mtx`, `__va_list`) — listed in
1271
+ // `FnProto.wordParams`; or a struct, union or class passed BY VALUE, which mwcc passes as the
1272
+ // address of a copy in the caller's frame — listed in `FnProto.byValueParams`. A name the context
1273
+ // never resolves is in neither, and every reader leaves it unsized.
1274
+ //
1275
+ // A VARIADIC declaration's are read alike and kept as its fixed parameters (`variadicParams`): its
1276
+ // arity is the call's, so they are no `params`, and no layout is read for a struct among them,
1277
+ // whose call refuses (`variadicRefusal`).
1278
+ const types = params.map((p) => asParameter(declaredType(p.specifiers, p.declarator.derivations)));
1279
+ const spelled = types.map((t) => {
1280
+ const r = resolve(t, typedefs, tokens);
1281
+ return spellType(qualifiers === 'kept' ? r : withoutTopQualifiers(r), tokens);
1282
+ });
696
1283
  if (fn.variadic) {
697
- return proto;
1284
+ proto.variadicParams = spelled;
1285
+ } else {
1286
+ proto.params = spelled;
1287
+ }
1288
+ const unsized = (k: number) => declaredWidth(spelled[k]) === undefined && !isFloatingSpelling(spelled[k]);
1289
+ const listed = (as: Passing) => types.flatMap((t, k) => (unsized(k) && passing(t) === as ? [k] : []));
1290
+ const words = listed('word');
1291
+ const copies = listed('by-value');
1292
+ if (words.length > 0) {
1293
+ proto.wordParams = words;
1294
+ }
1295
+ if (copies.length > 0) {
1296
+ proto.byValueParams = copies;
1297
+ }
1298
+ if (copies.length > 0 && !fn.variadic) {
1299
+ // …and the members of each one the context defines, which size it (`FnProto.byValueLayouts`)
1300
+ const layouts = Object.fromEntries(
1301
+ copies.flatMap((k) => {
1302
+ const l = layoutOf(resolve(types[k], typedefs, tokens));
1303
+ return l === undefined ? [] : [[k, l] as const];
1304
+ }),
1305
+ );
1306
+ if (Object.keys(layouts).length > 0) {
1307
+ proto.byValueLayouts = layouts;
1308
+ }
698
1309
  }
699
- proto.params = params.map((p) => parameterType(p, tokens, typedefs));
700
1310
  return proto;
701
1311
  }
702
1312
 
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);
1313
+ /** `t` less its top-level qualifiers: the derivation nearest the name's (`char *const`), or the
1314
+ * base's where there is none (`const struct W`). A pointer's target keeps its own (`const char *`). */
1315
+ function withoutTopQualifiers(t: DeclaredType): DeclaredType {
1316
+ const [first, ...rest] = t.derivations;
1317
+ if (first === undefined) {
1318
+ return { ...t, qualifiers: [] };
1319
+ }
1320
+ return first.kind === 'pointer' ? { ...t, derivations: [{ ...first, qualifiers: [] }, ...rest] } : t;
1321
+ }
1322
+
1323
+ /** How a parameter of type `t` is passed, read through the typedef names it is built on: in ONE
1324
+ * WORD — an enum the target sizes; a pointer; an array or a function, which a parameter adjusts to
1325
+ * a pointer — or BY VALUE, a struct, union or class (a tag, or a name bound to a body). Undefined
1326
+ * where nothing here says: a name this never saw, or an enum the target does not size
1327
+ * (`unsizedEnums`: `packed`, or a value an int cannot hold), which may take two words. */
1328
+ type Passing = 'word' | 'by-value' | undefined;
1329
+
1330
+ function passingOf(t: DeclaredType, table: TypeTable, written: ReadonlyMap<string, DeclaredType>): Passing {
1331
+ let cur = t;
1332
+ for (let hops = 0; hops < 16; hops++) {
1333
+ const [first] = cur.derivations;
1334
+ if (first !== undefined) {
1335
+ return first.kind === 'array' || first.kind === 'function' || first.kind === 'reference' || isPointer(first)
1336
+ ? 'word'
1337
+ : undefined;
1338
+ }
1339
+ if (cur.type.kind === 'tag') {
1340
+ const { keyword, tag } = cur.type;
1341
+ if (keyword !== 'enum') {
1342
+ return 'by-value';
1343
+ }
1344
+ return tag !== undefined && table.unsizedEnums.has(`enum ${tag}`) ? undefined : 'word';
1345
+ }
1346
+ const name = nameOf(cur);
1347
+ if (name === undefined) {
1348
+ return undefined;
1349
+ }
1350
+ if (table.named.has(name)) {
1351
+ return 'by-value';
1352
+ }
1353
+ if (table.enums.has(name)) {
1354
+ return table.unsizedEnums.has(name) ? undefined : 'word';
1355
+ }
1356
+ const next = table.typedefs.get(name) ?? written.get(name);
1357
+ if (next === undefined || nameOf(next) === name) {
1358
+ return undefined;
1359
+ }
1360
+ cur = next;
1361
+ }
1362
+ return undefined;
708
1363
  }
709
1364
 
710
1365
  /** The prototypes a lift of `own` reads when a context is in hand: `stated` — a caller's own
711
1366
  * 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.
1367
+ * states better (`contextPrototypesUnder`). `own`'s declaration is left out of the table, as the
1368
+ * map's is (`asIfUndecompiled`): a header's signature for the function being decompiled was written
1369
+ * with no compiler checking it against a definition while the function was still `INCLUDE_ASM`, so
1370
+ * it is not stated, and only what the caller states about it bounds the lift HARD.
1371
+ *
1372
+ * IT BOUNDS THE ARGUMENT REGISTERS SOFTLY INSTEAD ({@link ownContextEntry}, pipeline.ts
1373
+ * `liftWithinContextBound`). A hard bound puts the risk on the very functions a user decompiles:
1374
+ * an entry too short turns a real argument into `uninit_<reg>` — wrong C, where every other error
1375
+ * of the bound only keeps a parameter — and sbk2 has 16 `void f(void);` task callbacks whose
1376
+ * bodies read a0. The soft bound drops only the registers past the entry that the finished lift
1377
+ * never reads; one it reads (or one the machine homes to a slot, as IDO -g3 does) stays a
1378
+ * parameter, with an `[own-arity]` note naming the declaration and the register. (On kleod, whose
1379
+ * every header its compiler checked, the entry stated as a hard bound takes the lifts with more
1380
+ * parameters than their header from 52 of 377 to 1 of 373.) Only a typed list the bound can lay
1381
+ * out is handed over; every other shape abstains. A struct the entry returns through memory is
1382
+ * read on the same soft footing (pipeline.ts `returnOwnStruct`): lifted as that struct where the
1383
+ * machine proves the hidden pointer is only its storage, and kept as the pointer function with an
1384
+ * `[own-return]` note where it does not.
1385
+ *
1386
+ * The benchmark hands each row the same entry (`withOwnContextMark`, apps/benchmark
1387
+ * context-proto.ts `rowPrototypes`) where its manifest `proto` states none for the row's own
1388
+ * function, as m2c's vendored context already carries it; not every real row restates its own
1389
+ * header entry as `proto`. A user who trusts the entry outright restates it with `--proto`, which
1390
+ * bounds hard.
715
1391
  *
716
1392
  * 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. */
1393
+ * false` as nothing) keeps the context's return by `contextReturnUnder`'s rule: a struct return
1394
+ * always — it is what says argument 0 may be a hidden pointer, and an entry stating only the arity
1395
+ * would otherwise hand that pointer to the call as its first argument; a register return only
1396
+ * where the stated `params` number as many as the header's parameters. A list is parameters; a
1397
+ * COUNT is argument registers (`FnProto.params`), and it is compared with the header's parameter
1398
+ * count all the same, so under a 64-bit parameter a correct register count keeps no return. That
1399
+ * costs a lift, never a wrong one: a return kept is the header's own, and one dropped declines as
1400
+ * an undeclared callee does. `{ returnsVoid: false }` alone states no count, so it keeps no
1401
+ * register return. */
720
1402
  export function withContextPrototypes(
721
1403
  stated: Prototypes | undefined,
722
1404
  context: Prototypes,
@@ -729,21 +1411,131 @@ export function withContextPrototypes(
729
1411
  for (const [name, p] of Object.entries(stated ?? {})) {
730
1412
  const heard = Object.hasOwn(callees, name) ? callees[name] : undefined;
731
1413
  if (p && statesNoReturn(p)) {
732
- out[name] = { ...p, ...aggregateReturnOf(heard) };
1414
+ // `heard` is the entry as the map left it: its return, where it has one, is either the whole
1415
+ // context entry's or one `contextPrototypesUnder` already checked against the map
1416
+ out[name] = { ...p, ...contextReturnUnder(heard ?? undefined, p.params, undefined) };
733
1417
  }
734
1418
  }
735
1419
  return out;
736
1420
  }
737
1421
 
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 {};
1422
+ /** THE CONTEXT'S OWN ENTRY FOR `own`, where it is a TYPED LIST a lift may bound its argument
1423
+ * registers by — the entry `withContextPrototypes` leaves out — and `undefined` everywhere else.
1424
+ *
1425
+ * It has two readers, and both are SOFT. The context bound (pipeline.ts `liftWithinContextBound`):
1426
+ * a register past it that the lift goes on to read stays a parameter, so a header entry too short
1427
+ * costs a diagnostic, never `uninit_<reg>`. And the entry's struct return (pipeline.ts
1428
+ * `ownStructReturnThroughMemory`), lifted as that struct only where the machine proves the hidden
1429
+ * pointer is its storage, and kept as the pointer function with an `[own-return]` note where it
1430
+ * does not. An entry whose every parameter the bound can lay out is handed over, and so is one
1431
+ * that returns a struct or union by value whatever its parameters say, since what the return reads
1432
+ * is the return alone: a hidden pointer comes first in every parameter list, `...` or a struct
1433
+ * passed by value included (kleod's `struct Unk_0800BEF0_2 sub_0800BEF0(struct Unk_0800BEF0)`).
1434
+ * The bound reads such an entry through `ownArgumentWordsOn`, which lays out none of those lists
1435
+ * and so bounds nothing. Where the entry returns no struct, every other shape ABSTAINS:
1436
+ * - no entry, or one the context dropped (two declarations that conflict: C++ overloads);
1437
+ * - a variadic declaration (`variadicParams`, no `params`): its arity is each caller's;
1438
+ * - `()` (no `params` at all): it states none, and a K&R definition may take any;
1439
+ * - a struct passed by value, or a spelling nothing here sizes: either may fill any number of
1440
+ * words;
1441
+ * - a symbol that names a C++ member function or a mangled free function, whose declaration the
1442
+ * context does not key by the symbol; a context naming one the same is not its declaration, so
1443
+ * this abstains on it whatever it returns.
1444
+ * A bare count is no context entry (`prototypesFromContext` reads lists), and abstains as well. */
1445
+ export function ownContextEntry(context: Prototypes, own: string): FnProto | undefined {
1446
+ const p = Object.hasOwn(context, own) ? context[own] : undefined;
1447
+ if (p === undefined || p === null || typeof p.params === 'number') {
1448
+ return undefined;
742
1449
  }
743
- return {
744
- ...(p.returns !== undefined ? { returns: p.returns } : {}),
745
- ...(p.returnLayout !== undefined ? { returnLayout: p.returnLayout } : {}),
746
- };
1450
+ if (demangledName(own) !== null || implicitArgumentWords(own) !== 0) {
1451
+ return undefined;
1452
+ }
1453
+ if (declaresAggregateReturn(p)) {
1454
+ return p;
1455
+ }
1456
+ if (!Array.isArray(p.params) || p.variadicParams !== undefined) {
1457
+ return undefined;
1458
+ }
1459
+ const params = p.params;
1460
+ const laidOut = params.every((_, k) => {
1461
+ const cls = paramClassOf(p, params, k);
1462
+ return cls === 'word' || cls === 'wide' || cls === 'float';
1463
+ });
1464
+ return laidOut ? p : undefined;
1465
+ }
1466
+
1467
+ /** A TABLE'S OWN CONTEXT ENTRY, TAKEN OUT OF IT: where `prototypes`' entry for `own` carries the
1468
+ * mark (`FnProto.fromContext`), the table without it, and the entry as the soft bound reads it
1469
+ * ({@link ownContextEntry}, which abstains on any shape it cannot lay out); otherwise the table as
1470
+ * it is. `given` — an entry the caller hands over beside the table — wins over a marked one.
1471
+ *
1472
+ * Every entry path asks this before any reader looks at the table, so a marked entry is never
1473
+ * read as a stated one: a stated entry bounds hard, and a context entry a body reads past would
1474
+ * turn a real argument into `uninit_<reg>`. */
1475
+ export function splitOwnContext(
1476
+ prototypes: Prototypes | undefined,
1477
+ own: string,
1478
+ given?: FnProto,
1479
+ ): { prototypes: Prototypes | undefined; ownContextProto: FnProto | undefined } {
1480
+ const p = prototypes !== undefined && Object.hasOwn(prototypes, own) ? prototypes[own] : undefined;
1481
+ if (p?.fromContext !== true) {
1482
+ return { prototypes, ownContextProto: given };
1483
+ }
1484
+ const { [own]: _marked, ...rest } = prototypes!;
1485
+ const { fromContext: _mark, ...entry } = p;
1486
+ return { prototypes: rest, ownContextProto: given ?? ownContextEntry({ [own]: entry }, own) };
1487
+ }
1488
+
1489
+ /** `prototypes` with the context's own entry for `own` carried in it under the mark
1490
+ * ({@link splitOwnContext} reads it back), where the context has one {@link ownContextEntry}
1491
+ * hands over and the table states none: how a benchmark row publishes the entry its lift read, so
1492
+ * that its `--proto` replay lifts as it did. */
1493
+ export function withOwnContextMark(
1494
+ prototypes: Prototypes | undefined,
1495
+ own: string,
1496
+ context: Prototypes,
1497
+ ): Prototypes | undefined {
1498
+ if (prototypes !== undefined && Object.hasOwn(prototypes, own)) {
1499
+ return prototypes;
1500
+ }
1501
+ const entry = ownContextEntry(context, own);
1502
+ return entry === undefined ? prototypes : { ...prototypes, [own]: { ...entry, fromContext: true } };
1503
+ }
1504
+
1505
+ /** The caller's stated entry for `own`, with the struct or union its stated `returns` names laid out
1506
+ * by the context — `struct S`, or a typedef of one, `T8` over `typedef struct {u32 a, b;} T8;` —
1507
+ * read as the context reads a declaration's return (`readSignature`, through a declaration of that
1508
+ * return the context is handed for the purpose). A `--proto` table spells a type and lays nothing
1509
+ * out, and `withContextPrototypes` leaves the context's own entry for `own` out, so without this
1510
+ * the lifted function's own struct return is a spelling: `struct S1` sized by nothing, where the
1511
+ * rule that decides whether it comes back through a hidden pointer reads a size
1512
+ * (`returnsInMemory`), and `T8`, which reads as no aggregate at all and lifts as the pointer
1513
+ * function its hidden pointer makes it (pipeline.ts `ownStructReturnThroughMemory`).
1514
+ * A return already laid out, a scalar, a pointer or a floating type is left as stated, and so is
1515
+ * a name the context does not lay out. */
1516
+ export function withOwnReturnLayout(
1517
+ stated: Prototypes | undefined,
1518
+ own: string,
1519
+ src: string,
1520
+ language: Language,
1521
+ ): Prototypes | undefined {
1522
+ const p = stated !== undefined && Object.hasOwn(stated, own) ? stated[own] : undefined;
1523
+ const r = p?.returns?.trim();
1524
+ if (
1525
+ p === undefined ||
1526
+ p === null ||
1527
+ r === undefined ||
1528
+ p.returnLayout !== undefined ||
1529
+ r === 'void' ||
1530
+ r.includes('*') ||
1531
+ declaredWidth(r) !== undefined ||
1532
+ isFloatingSpelling(r)
1533
+ ) {
1534
+ return stated;
1535
+ }
1536
+ const probe = '__asmlift_own_return';
1537
+ const layout = prototypesFromContext(`${src}\n${r} ${probe}(void);\n`, language)[probe]?.returnLayout;
1538
+ return layout === undefined ? stated : { ...stated, [own]: { ...p, returnLayout: layout } };
747
1539
  }
748
1540
 
749
1541
  const same = (a: FnProto | null, b: FnProto): boolean => a !== null && JSON.stringify(a) === JSON.stringify(b);
@@ -752,9 +1544,28 @@ const same = (a: FnProto | null, b: FnProto): boolean => a !== null && JSON.stri
752
1544
  * lets any entry it is handed shadow the map's signature for that name, so what reaches it decides
753
1545
  * which source wins, per name: a context entry that sizes every parameter wins — it is the
754
1546
  * 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. */
1547
+ * need; it is taken whole, its return included, and the map is not consulted for it. An entry that
1548
+ * cannot size one yields to a map signature that can be spelled, which sizes by byte count what a
1549
+ * declaration names (a by-value struct of a register's width); and where the map spells nothing
1550
+ * either, the context entry stays.
1551
+ *
1552
+ * AN ENTRY THAT YIELDS ITS PARAMETERS KEEPS THE RETURN IT STATES where `contextReturnUnder` says
1553
+ * it may, because the map's signature carries no `returns`. DWARF does say that a pointer, or a
1554
+ * signed or unsigned base type, comes back in a register; `symbolPrototype` chooses not to spell it
1555
+ * (`FnProto.returns`: a header states it and nothing derives it), because a spelled return also
1556
+ * types the call's result and prints in the candidate's declarations, and the map's `void *` is not
1557
+ * the header's `u8 *` — C++ does not convert it implicitly. So a map-only lift (no context) still
1558
+ * declines this shape, and a context entry dropped whole leaves every non-void call to that name
1559
+ * undeclared — and a frame address handed to an undeclared callee may be its hidden struct-return
1560
+ * pointer (`returnsWithoutHiddenPointer`). pokeemerald's `u8 *ConvertIntToDecimalStringN(u8 *,
1561
+ * s32, enum StringConvertMode, u8)` is the case: its enum parameter has no width its SPELLING
1562
+ * states, so the entry yields its parameters and keeps its `u8 *`.
1563
+ *
1564
+ * THE QUESTION HERE IS ASKED OF THE SPELLINGS, the context's own reading of the names behind them
1565
+ * (`wordParams`, `byValueParams`) set aside: which source states the declaration better is not
1566
+ * which one a lowering can place. The map sizes an enum or an array typedef by its byte count and
1567
+ * spells it, which the context's reading of the name only classifies; so an entry that sizes such
1568
+ * 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
1569
  function contextPrototypesUnder(
759
1570
  context: Prototypes,
760
1571
  symbols: SymbolMap | undefined,
@@ -763,26 +1574,75 @@ function contextPrototypesUnder(
763
1574
  if (symbols === undefined) {
764
1575
  return context;
765
1576
  }
766
- const mapped = new Map<string, FnProto>();
1577
+ const mapped = new Map<string, { signed: FnProto; returned: SymbolTypeFacts | null }>();
767
1578
  for (const infos of symbols.values()) {
768
1579
  for (const info of infos) {
769
1580
  const signed = symbolPrototype(info);
770
- if (signed?.params !== undefined && !mapped.has(info.name)) {
771
- mapped.set(info.name, signed);
1581
+ if (signed?.params !== undefined && info.signature !== undefined && !mapped.has(info.name)) {
1582
+ mapped.set(info.name, { signed, returned: info.signature.returns });
772
1583
  }
773
1584
  }
774
1585
  }
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
1586
  return Object.fromEntries(
778
1587
  Object.entries(context).flatMap(([name, p]): [string, FnProto][] => {
779
- const signed = mapped.get(name);
780
- if (declaredCallArgs(p, target) !== undefined || signed === undefined) {
1588
+ const map = mapped.get(name);
1589
+ if (declaredCallArgs({ params: p.params }, target) !== undefined || map === undefined) {
781
1590
  return [[name, p]];
782
1591
  }
783
- return declaresAggregateReturn(p) && signed.returnsVoid !== true
784
- ? [[name, { ...signed, ...aggregateReturnOf(p) }]]
785
- : [];
1592
+ const kept = contextReturnUnder(p, map.signed.params, map.returned);
1593
+ return kept === undefined ? [] : [[name, { ...map.signed, ...kept }]];
786
1594
  }),
787
1595
  );
788
1596
  }
1597
+
1598
+ /** The return a context entry `p` keeps under parameters another source states (`under`: the
1599
+ * map's signature, or a stated entry that says nothing of the return) — or `undefined` when it
1600
+ * keeps none. `returned` is the map's DWARF return where the map's signature is the other source,
1601
+ * `null` for a map that says `void`, and `undefined` where the map is not consulted.
1602
+ *
1603
+ * A `void` return is not kept: whether the callee returns is the map's to state.
1604
+ *
1605
+ * A STRUCT RETURN is kept as stated: DWARF names no struct, so the map states one only by its size
1606
+ * (`symbolPrototype`), and never its members; and keeping it is the safe direction, since it only
1607
+ * says argument 0 may be a hidden pointer.
1608
+ *
1609
+ * A REGISTER RETURN says argument 0 is NOT one, so it is the direction that lifts, and it is kept
1610
+ * only where it is the same fact as the parameters it sits under:
1611
+ * - the context states every parameter, and as many as `under` does — a count `under` is read as
1612
+ * a number of parameters here, though it states registers (`withContextPrototypes`). A variadic
1613
+ * declaration, and a K&R one, state none (`readSignature`), and the map's DWARF parameters
1614
+ * leave the `...` out — so `int pr(char *, const char *, ...)` under a two-parameter map
1615
+ * signature would lower `pr(buf, s, a, b)` as `pr(buf, s)`. Dropped whole, the call declines;
1616
+ * - the return has a width (`declaredWidth`). `float` and `double` have none here, cannot say
1617
+ * anything about a hidden pointer the map's word does not, and on a target with an FPU make
1618
+ * call-plan refuse every call to the name (`$f0`, `f1`), whose result is used or not;
1619
+ * - the map, where it is consulted, does not contradict it: a return the map calls `void`, or a
1620
+ * 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
1621
+ * return the header's, and the two are one fact a table must not hold half of. A context entry
1622
+ * taken whole (it sizes every parameter) never meets the map, and keeps its return unchecked.
1623
+ * Each refusal leaves the call undeclared (`FnProto.returns`). */
1624
+ function contextReturnUnder(
1625
+ p: FnProto | undefined,
1626
+ under: FnProto['params'],
1627
+ returned: SymbolTypeFacts | null | undefined,
1628
+ ): Pick<FnProto, 'returns' | 'returnLayout'> | undefined {
1629
+ if (p === undefined || returned === null || statesNoReturn(p) || declaresVoidReturn(p)) {
1630
+ return undefined;
1631
+ }
1632
+ const kept = {
1633
+ ...(p.returns !== undefined ? { returns: p.returns } : {}),
1634
+ ...(p.returnLayout !== undefined ? { returnLayout: p.returnLayout } : {}),
1635
+ };
1636
+ if (declaresAggregateReturn(p)) {
1637
+ return kept;
1638
+ }
1639
+ const arity = typeof under === 'number' ? under : under?.length;
1640
+ if (!Array.isArray(p.params) || p.params.length !== arity) {
1641
+ return undefined;
1642
+ }
1643
+ const width = p.returns === undefined ? undefined : declaredWidth(p.returns);
1644
+ if (width === undefined) {
1645
+ return undefined;
1646
+ }
1647
+ return returned !== undefined && returned.size !== null && returned.size * 8 !== width ? undefined : kept;
1648
+ }