@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
package/src/proto.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { parseTypeName } from './cdecl/parse';
2
2
  import type { IrType } from './ir/types';
3
+ import { demangle, implicitArgumentWords, spellType } from './mangle';
3
4
  import type { SymbolInfo, SymbolMap, SymbolTypeFacts } from './symbols';
4
5
  import type { TargetDescription } from './target';
5
6
 
@@ -30,11 +31,44 @@ export type ParamType = string;
30
31
  export interface FnProto {
31
32
  /** declared parameters — either the typed parameter list a header extraction produces
32
33
  * (`["u8", "s32"]`), which is a list of C PARAMETERS, or a bare COUNT, which is the user's word
33
- * for how many argument REGISTERS the call occupies. The two are the same number only while
34
- * every parameter fits in a register, and `declaredCallArgs` is what converts the first into
35
- * the second. Omit — or spell a parameter that conversion cannot size — to let the frontend
34
+ * for how many argument REGISTERS the call occupies — read so at a call. On the function's OWN
35
+ * entry a count bounds no argument register (`ownArgumentWords` abstains): a header's count may
36
+ * be of C parameters, and one of those may span any number of words. The two are the
37
+ * same number only while every parameter fits in a register, and `declaredCallArgs` is what
38
+ * converts the first into the second. Omit — or spell a parameter that conversion cannot size — to let the frontend
36
39
  * fall back to its contiguous-arg-register heuristic. */
37
40
  params?: number | ParamType[];
41
+ /** The parameters, by index into a typed `params`, whose spelling `declaredWidth` cannot size and
42
+ * which still travel in one register: an enum, or an array or function type a typedef name
43
+ * stands for, which the parameter adjusts to a pointer. A context states it, reading the types
44
+ * behind the names (`prototypesFromContext`). Every reader sizes such a parameter at one word
45
+ * (`paramClassOf`): the call lowering's widths (`declaredCallArgs`) and a frontend's frame
46
+ * partition alike. */
47
+ wordParams?: readonly number[];
48
+ /** The parameters, by index into a typed `params`, that are a struct, union or class passed BY
49
+ * VALUE: mwcc passes the address of a copy it builds in the caller's own frame, which no source
50
+ * declares (`frontend/ppc.ts` `measurePpcFrame`). A context states it, reading the tag behind the
51
+ * name (`prototypesFromContext`). A spelling neither this nor `wordParams` lists, and that
52
+ * nothing else sizes, is UNSIZED — a name nothing here resolved, not a struct by inference — and
53
+ * every reader abstains on it. */
54
+ byValueParams?: readonly number[];
55
+ /** The members of each struct or union a typed `params` list passes BY VALUE, keyed by the
56
+ * parameter's index: what sizes it, and so how many argument words it takes on a target that
57
+ * passes one in words (`compilerBehaviors.byValueAggregateArgWords`, read by `aggregate.ts`
58
+ * `byValueCallArgs`). A context states it from the struct's definition, as it states
59
+ * `returnLayout`. A by-value parameter with no layout here is unsized, and a call to it refuses
60
+ * (`aggregate.ts` `byValueRefusal`) rather than take the argument-register guess. */
61
+ byValueLayouts?: Readonly<Record<number, AggregateLayout>>;
62
+ /** The FIXED parameters of a VARIADIC declaration, spelled as a typed `params` list spells them.
63
+ * Such a declaration states no `params`, because its arity is the call's and a list would read
64
+ * as the whole of it; `wordParams` and `byValueParams` index into this list instead. A context
65
+ * states it (`prototypesFromContext`). Read for two things, and never as a layout of its own.
66
+ * Where each fixed parameter is one word, they are the call's first argument words, and only
67
+ * what the `...` takes is guessed (frontend/call-plan.ts `variadicFloor`). Where one is not, the
68
+ * call refuses (`variadicRefusal`): the arity guess reads argument words one by one, so a fixed
69
+ * parameter that is not one word is split or dropped: `void takeV(struct W, ...)` would lift
70
+ * `takeV(*(s32 *)&gW, 5)`, which compiles against the header. */
71
+ variadicParams?: ParamType[];
38
72
  /** The declared return type is `void`. Read for the function under decompilation, where a
39
73
  * trailing `bx lr` leaves a meaningless return register that must not surface as a `return`
40
74
  * value — and, since the out-parameter path landed, for a CALLEE, where it is the only thing
@@ -89,6 +123,16 @@ export interface FnProto {
89
123
  * this key also says it of a typedef name, which spells no keyword, and carries the members
90
124
  * whose layout decides whether the target returns it through memory at all (`aggregate.ts`). */
91
125
  returnLayout?: AggregateLayout;
126
+ /** ON THE FUNCTION'S OWN ENTRY, `true` says the entry is its declaration context's header entry
127
+ * (proto-context.ts `ownContextEntry`) and not the caller's word: it bounds the argument
128
+ * registers SOFTLY (pipeline.ts `liftWithinContextBound`), and a struct it returns through
129
+ * memory is lifted as that struct where the lift proves the hidden pointer is only its storage
130
+ * (pipeline.ts `returnOwnStruct`), and kept as the pointer function with a note where it does
131
+ * not; it is read as nothing else — not as a scalar return, not by any reader of a stated own
132
+ * entry. Each entry path takes it out of the table before anything reads it (proto-context.ts `splitOwnContext`). It is how a table carries the
133
+ * entry a `--context` lift was handed: a benchmark row publishes it so its `--proto` replay
134
+ * lifts as the harness did. On a callee's entry it is ignored. */
135
+ fromContext?: boolean;
92
136
  }
93
137
 
94
138
  /** A struct or union a declaration spells: what it is, and its members as the header lists them.
@@ -118,8 +162,10 @@ export interface AggregateMember {
118
162
  /** symbol → prototype. The function under decompilation and its callees share one table. */
119
163
  export type Prototypes = Record<string, FnProto>;
120
164
 
121
- /** How many argument REGISTERS a list of parameter WIDTHS occupies: a parameter wider than a
122
- * machine word travels in a register pair, every other one in a single register.
165
+ /** How many argument WORDS a list of parameter WIDTHS occupies: a parameter wider than a machine
166
+ * word travels in a pair of words, every other one in a single word, and where the target aligns
167
+ * an 8-byte argument (`TargetDescription.evenWordAlignedArgs`) a pair that would start at an odd
168
+ * word skips one.
123
169
  *
124
170
  * The rule lives here rather than beside either caller because it has two, and they are the two
125
171
  * vocabularies a declaration is written in. `runtime-helpers.ts` states a compiler's own helper
@@ -127,17 +173,14 @@ export type Prototypes = Record<string, FnProto>;
127
173
  * `declaredCallArgs` below reads the same widths off a project's C types. One ABI fact, one
128
174
  * copy of it; a second copy is a rule that can disagree with itself.
129
175
  *
130
- * NO EVEN-REGISTER ALIGNMENT, and that is a measured agbcc fact rather than an omission: its
131
- * `thumb.h` computes an argument's register from a plain byte counter with no rounding, so
132
- * `void f(s32, long long)` passes the pair in r1:r2 where AAPCS would pad to r2:r3.
133
- *
134
- * AN ABI THAT DOES ALIGN HAS NO READER HERE YET, which is why the rule is flat rather than a
135
- * knob on `TargetDescription` beside `argRegs` and `stagesOutgoingArgsInFrame`. MIPS o32 aligns a
136
- * 64-bit argument to an even register pair and is the target that will want one, and
137
- * `frontend/mips.ts` refuses a declared 64-bit parameter (`CallLowering.pairs`), so every list it
138
- * counts is one word per parameter. The lowering that builds an o32 pair owes the rule a home. */
139
- export function wordsOf(params: readonly number[]): number {
140
- return params.reduce((n, w) => n + (w > 32 ? 2 : 1), 0);
176
+ * agbcc DOES NOT ALIGN, a measured fact rather than an omission: its `thumb.h` computes an
177
+ * argument's register from a plain byte counter with no rounding, so `void f(s32, long long)`
178
+ * passes the pair in r1:r2 where AAPCS would pad to r2:r3. O32 does, for a scalar and a struct
179
+ * alike, and that is the one fact both read here: `aggregate.ts` `byValueCallArgs` places a struct
180
+ * by this count, and the lowering that builds an o32 pair, which `frontend/mips.ts` does not yet
181
+ * (it refuses a declared 64-bit parameter, `CallLowering.pairs`), counts its words the same way. */
182
+ export function wordsOf(params: readonly number[], target: Pick<TargetDescription, 'evenWordAlignedArgs'>): number {
183
+ return params.reduce((n, w) => (w > 32 ? n + (target.evenWordAlignedArgs === true ? n % 2 : 0) + 2 : n + 1), 0);
141
184
  }
142
185
 
143
186
  /** Whether a proto states a parameter list at all — readably or not. The tier question, asked
@@ -148,25 +191,49 @@ export function declaresParams(p: FnProto | undefined): boolean {
148
191
  return typeof p?.params === 'number' || Array.isArray(p?.params);
149
192
  }
150
193
 
194
+ /** The declaration a call to `callee` is read at — THE TIER ORDER, written once: the project's own
195
+ * entry where it states a parameter list (`declaresParams`), then the compiler's runtime table
196
+ * (`helpers`: `runtime-helpers.ts` `helperPrototypes` of the target), then the signatures the C
197
+ * standard fixes. `frontend/call-plan.ts` lowers every call at it, and the CLI asks it which
198
+ * callees a run guessed, so a callee the lowering reads a signature for is never reported as
199
+ * guessed, and one it guesses for always is. `Object.hasOwn` on all three tables: a callee named
200
+ * `toString` would otherwise read a `Function` off `Object.prototype` as its entry. */
201
+ export function calleeDeclaration(callee: string, prototypes: Prototypes, helpers: Prototypes): FnProto | undefined {
202
+ const known = (t: Prototypes) => (Object.hasOwn(t, callee) ? t[callee] : undefined);
203
+ const own = known(prototypes);
204
+ return declaresParams(own) ? own : (known(helpers) ?? known(STANDARD_SIGNATURES));
205
+ }
206
+
151
207
  /** What a declaration says about the argument registers a call occupies — the width each declared
152
208
  * parameter travels in, in argument order — or `undefined` when it says nothing this can act on.
153
209
  *
154
210
  * THREE WAYS TO SAY NOTHING, AND THEY ARE ONE ANSWER. `params` omitted; `params` malformed (a
155
211
  * bare `"u8"` string, the shape the untyped CLI `--proto` JSON admits); and a list holding a
156
- * spelling `declaredWidth` cannot read — a project typedef (`bool8`, `Direction`, `TaskFunc`), a
157
- * by-value struct, a floating type. The first two are not a reading of the list at all. The third
158
- * is a list read to the end that cannot be CONVERTED: a parameter of unknown width occupies one
159
- * argument register or two, the choice moves every later argument's home, and nothing a
160
- * declaration holds settles it.
212
+ * spelling neither `declaredWidth` reads nor the declaration sizes — a project typedef
213
+ * (`bool8`, `Direction`, `TaskFunc`) that `wordParams` does not list, a by-value struct, a
214
+ * floating type (a by-value struct is the exception below). The first two are not a reading of the
215
+ * list at all. The third is a list read to the end that cannot be CONVERTED: a parameter of
216
+ * unknown width occupies one argument register or two, and the choice moves every later argument's
217
+ * home. Where the declaration does settle it
218
+ * — a typedef'd enum, an array or a function type the context resolved to one word, which
219
+ * `wordParams` lists (`paramClassOf`) — the parameter is 32 bits and the list converts.
161
220
  *
162
221
  * SO IT ABSTAINS, AND ABSTAINING IS NEITHER A REFUSAL NOR A GUESS. The caller falls back to its
163
222
  * own contiguous-arg-register scan — exactly what it does for a callee the project never declared
164
223
  * — and that scan names itself a guess: `SsaBuilder.readGuessedArg` ASKS whether a register was
165
224
  * set up, and `finish()` retracts the ones an intervening call destroyed. Refusing instead would
166
- * be a function that lifts when told nothing and declines when told more. Returning
225
+ * be a function that lifts when told nothing and declines when told more. The list's LENGTH is
226
+ * still read: every parameter is at least one word, so a guess that counts fewer registers has
227
+ * dropped one, and that refuses (frontend/call-plan.ts `refuseShortOfDeclared`). Returning
167
228
  * one-register-each instead would be a layout nothing could know to be right, and the frontend
168
229
  * would then ASSERT those registers rather than ask for them.
169
230
  *
231
+ * A PARAMETER THE DECLARATION PASSES AS A STRUCT OR UNION BY VALUE IS THE EXCEPTION, because it is
232
+ * known not to be one register: the caller asks `aggregate.ts` `byValueCallArgs` to place it, and
233
+ * where no rule does (`byValueRefusal`) the call refuses (frontend/call-plan.ts
234
+ * `refuseUnplacedByValue`). That is a function that declines when told more, on purpose: a guess
235
+ * over a struct drops it or reads its words as separate arguments, and both compile.
236
+ *
170
237
  * THE MACHINE IS NOT A WITNESS FOR A WIDTH NOBODY STATED, which is the reading this replaces. The
171
238
  * contiguous scan is wrong in both directions and neither is recoverable here. It OVER-counts:
172
239
  * it is read before `frontend/ssa.ts` `narrowToSetupArgs` trims the registers a call sat between,
@@ -186,7 +253,9 @@ export function declaresParams(p: FnProto | undefined): boolean {
186
253
  * target that states no such words the list abstains. A `float` states no layout on either: it
187
254
  * is one word on a soft-float target, but that word is a float only to a callee whose declaration
188
255
  * the candidate prints, and `spellableProto` prints no `float` — an undeclared callee is handed a
189
- * `float` promoted to a `double`, which is two words. */
256
+ * `float` promoted to a `double`, which is two words. A known spelling with no layout is not the
257
+ * unsized abstention, so on a target with no FPU a call to such a declaration refuses rather than
258
+ * guess (frontend/call-plan.ts `refuseUnplacedFloat`). */
190
259
  export function declaredCallArgs(
191
260
  p: FnProto | undefined,
192
261
  target: Pick<TargetDescription, 'doubleArgWords'>,
@@ -200,7 +269,7 @@ export function declaredCallArgs(
200
269
  const widths: number[] = [];
201
270
  const doubles = new Set<number>();
202
271
  for (const [i, t] of p.params.entries()) {
203
- const w = declaredWidth(t);
272
+ const w = declaredWidth(t) ?? (paramClassOf(p, p.params, i) === 'word' ? 32 : undefined);
204
273
  if (w !== undefined) {
205
274
  widths.push(w);
206
275
  } else if (target.doubleArgWords !== undefined && isDoubleSpelling(t)) {
@@ -213,19 +282,310 @@ export function declaredCallArgs(
213
282
  return { widths, doubles };
214
283
  }
215
284
 
285
+ /** WHAT ONE DECLARED PARAMETER IS, as far as where a caller places it goes — the one reading every
286
+ * reader of a declaration takes, so the call lowering (`declaredCallArgs`) and a frontend's frame
287
+ * partition (`frontend/call-plan.ts` `declaredParams`) cannot give one declaration two answers: a
288
+ * value of one word (`declaredWidth` sizes it at 32 bits or less, or `wordParams` lists it), one
289
+ * wider than a word, a floating type (`isFloatingSpelling`), a struct or union passed by value
290
+ * (`byValueParams` lists it, or the spelling says so with its keyword: `struct B`), or UNSIZED — a
291
+ * spelling nothing here resolved, which states none of those and is not read as any of them. A
292
+ * count states words. */
293
+ export type ParamClass = 'word' | 'wide' | 'float' | 'by-value' | 'unsized';
294
+
295
+ /** A struct, union or class named by its keyword and tag, with no declarator: passed by value. */
296
+ const BY_VALUE_SPELLING = /^\s*(?:struct|union|class)\s+\w+\s*$/;
297
+
298
+ /** The class of parameter `k` of the typed list `params` that declaration `p` states (`ParamClass`). */
299
+ export function paramClassOf(p: FnProto, params: readonly ParamType[], k: number): ParamClass {
300
+ const spelling = params[k];
301
+ const w = declaredWidth(spelling);
302
+ return w !== undefined
303
+ ? w > 32
304
+ ? 'wide'
305
+ : 'word'
306
+ : isFloatingSpelling(spelling)
307
+ ? 'float'
308
+ : p.wordParams?.includes(k)
309
+ ? 'word'
310
+ : p.byValueParams?.includes(k) || BY_VALUE_SPELLING.test(spelling.replace(/\b(?:const|volatile)\b/g, ' '))
311
+ ? 'by-value'
312
+ : 'unsized';
313
+ }
314
+
315
+ /** Why a call to the VARIADIC declaration `p` cannot be placed by the arity guess: its first fixed
316
+ * parameter (`FnProto.variadicParams`, an index into it) that is not one argument word, and the
317
+ * reason. Undefined where `p` is no variadic declaration, or where every fixed parameter is a word
318
+ * or UNSIZED — a spelling nothing here resolved, which may be one word, and on which every reader
319
+ * abstains (`declaredCallArgs`) rather than refuse. */
320
+ export function variadicRefusal(p: FnProto | undefined): { refused: string; param: number } | undefined {
321
+ const fixed = p?.variadicParams;
322
+ if (fixed === undefined || p?.params !== undefined) {
323
+ return undefined;
324
+ }
325
+ const reasons: Partial<Record<ParamClass, string>> = {
326
+ 'by-value': 'reads a struct or union passed by value as that many separate words',
327
+ wide: 'reads a value wider than a word as two separate arguments',
328
+ float: 'reads a floating-point value as integer words, or misses it in the FPU registers',
329
+ };
330
+ for (const k of fixed.keys()) {
331
+ const why = reasons[paramClassOf(p!, fixed, k)];
332
+ if (why !== undefined) {
333
+ return {
334
+ refused: `its declaration is variadic: the call's arguments are placed by the arity guess, which ${why}`,
335
+ param: k,
336
+ };
337
+ }
338
+ }
339
+ return undefined;
340
+ }
341
+
342
+ /** HOW MANY GENERAL ARGUMENT WORDS THE FUNCTION UNDER DECOMPILATION'S OWN DECLARATION FILLS — an
343
+ * UPPER BOUND, never an exact layout — or `undefined` where the declaration bounds nothing.
344
+ *
345
+ * Its one reader is the live-in model (`frontend/ssa.ts` `registerLiveIns`): an argument register
346
+ * at or past this many words is one no caller fills, so a read of it before any write is an
347
+ * uninitialised local the compiler homed there, never a parameter. KMC and gcc 2.7.2 home a local
348
+ * in `a2`/`a3` past a two-parameter declaration, agbcc in `r3` and mwcc 2.3.3 in `r5`..`r10`, and
349
+ * unbounded, each such read is a made-up parameter (`uninitfirst(a0, *a1, a2, a3)`).
350
+ *
351
+ * AN UPPER BOUND, because the direction of every error decides what it costs. A bound too HIGH
352
+ * leaves a register a parameter, as no bound at all does. A bound
353
+ * too LOW turns a real argument into `uninit_<reg>`, which is wrong C. So:
354
+ * - the declared words are laid out by {@link wordsOf}, the call's own rule: a floating parameter
355
+ * takes no general word where floats count on their own (`fpu.slots: 'separate'`, the PowerPC
356
+ * EABI), and everywhere else a floating parameter takes two, as a `long long` does (O32's
357
+ * leading floats still take their integer slots, and agbcc passes both in general words).
358
+ * A `float` takes two as well: a list cannot say whether the definition is prototyped, and an
359
+ * old-style one (`int k(a, b) float a; int b;`) is handed `a` promoted to `double`, so `b`
360
+ * sits in r2 / a2. Two words are an upper bound for a prototyped `float` too.
361
+ * - UP TO TWO HIDDEN WORDS ARE ADDED to a typed list where they may come first, because no
362
+ * parameter list states them and each pushes every declared parameter one register up. A return the
363
+ * declaration does not show is a scalar or `void` may be a struct handed back through a
364
+ * pointer in argument 0, and a symbol that may name a C++ member function is handed `this`
365
+ * there (`mangle.ts` `implicitArgumentWords`). Both are "may": a static member or a struct
366
+ * returned in registers leaves one register a parameter.
367
+ *
368
+ * THE PARAMETERS ARE THE STATED ONES, or where the declaration states none, the ones a mangled
369
+ * symbol records (`demangle`: `uninitfirst__FiPi` is `(int, int *)`), which is the compiler's own
370
+ * copy of the declaration. A BARE COUNT BOUNDS NOTHING: it may count argument registers
371
+ * (`FnProto.params`) or C parameters, any of which may be a pair or a struct passed by value of
372
+ * any size. IT ABSTAINS where neither states a list, or on one it cannot lay out: an
373
+ * unsized spelling (which may be two words), a struct passed by value (sized by a layout this
374
+ * does not read), a `long double`; and on a constructor or destructor, which
375
+ * may be handed a flag beside `this`. A variadic declaration states no `params` and so
376
+ * abstains: its arity is each caller's.
377
+ *
378
+ * `registerReturn` says the declared struct return is one the target hands back in registers, so
379
+ * no hidden pointer comes ahead of the parameters: the target's rule, which this module cannot
380
+ * read, so every reader asks through `aggregate.ts` `ownArgumentWordsOn`, which reads it as the call
381
+ * lowering does. */
382
+ export function ownArgumentWords(
383
+ name: string,
384
+ p: FnProto | undefined,
385
+ target: Pick<TargetDescription, 'fpu' | 'evenWordAlignedArgs'>,
386
+ registerReturn = false,
387
+ ): number | undefined {
388
+ const own = ownArguments(name, p, target, registerReturn);
389
+ if (own === undefined || own.hidden === undefined) {
390
+ return undefined;
391
+ }
392
+ // The hidden words are laid out WITH the declared ones, not added after them: they come first,
393
+ // so they move where the even-word rule puts a 64-bit parameter. O32's `struct P f(double x)`
394
+ // passes the struct pointer in a0, skips a1 and passes `x` in a2:a3 — 4 words, where `1 +
395
+ // wordsOf([64])` says 3 and turns a3 into `uninit_a3`. The call side counts the same way
396
+ // (`frontend/call-plan.ts`, `[32, ...params]`).
397
+ return wordsOf([...Array.from({ length: own.hidden }, () => 32), ...own.widths], target);
398
+ }
399
+
400
+ /** WHETHER THE OWN DECLARATION SAYS THE CALLER FILLS ARGUMENT 0, asked of a function that RETURNS A
401
+ * WORD — so no hidden struct pointer is in question: `true` where a declared parameter word or
402
+ * `this` comes first, `false` where it states no argument word at all (`g__Fv`, a free
403
+ * function of none), and `undefined` where it does not say (no declaration, one {@link
404
+ * ownArgumentWords} cannot lay out, or no parameters on a symbol that only MAY be a method, which a
405
+ * static member is not). The PowerPC frontend asks it before a bare return hands back r3's entry
406
+ * value (`frontend/ppc.ts` `r3IsAnArgument`), and it reads the declaration the bound reads, so the
407
+ * two cannot give one declaration two answers. */
408
+ export function ownFirstArgumentPassed(
409
+ name: string,
410
+ p: FnProto | undefined,
411
+ target: Pick<TargetDescription, 'fpu' | 'evenWordAlignedArgs'>,
412
+ ): boolean | undefined {
413
+ const own = ownArguments(name, p, target);
414
+ if (own === undefined) {
415
+ return undefined;
416
+ }
417
+ if (own.widths.length > 0 || own.passesThis === true) {
418
+ return true;
419
+ }
420
+ return own.passesThis === false ? false : undefined;
421
+ }
422
+
423
+ /** The own declaration as both readers above read it: the declared parameter words' widths, the
424
+ * hidden words that may come ahead of them (`undefined` where they cannot be bounded: a
425
+ * constructor's or destructor's flag), and whether `this` is surely passed (`true` for a
426
+ * constructor or destructor, `false` for a free function, `undefined` for a symbol that only may
427
+ * be a method).
428
+ *
429
+ * A bare count is no declaration this can lay out, so it reads as none. */
430
+ function ownArguments(
431
+ name: string,
432
+ p: FnProto | undefined,
433
+ target: Pick<TargetDescription, 'fpu' | 'evenWordAlignedArgs'>,
434
+ registerReturn = false,
435
+ ): { widths: number[]; hidden: number | undefined; passesThis: boolean | undefined } | undefined {
436
+ // A COUNT BOUNDS NOTHING: it may mean argument registers or C parameters, and a C parameter
437
+ // may be a pair or a struct passed by value of any size (`f(struct S12 s)` {params: 1} fills 3
438
+ // words), so no layout of it is an upper bound.
439
+ if (typeof p?.params === 'number') {
440
+ return undefined;
441
+ }
442
+ const implicit = implicitArgumentWords(name);
443
+ const widths = ownParamWidths(name, p ?? undefined, target);
444
+ if (widths === undefined) {
445
+ return undefined;
446
+ }
447
+ return {
448
+ widths,
449
+ hidden: implicit === undefined ? undefined : (registerReturn || showsScalarReturn(p) ? 0 : 1) + implicit,
450
+ passesThis: implicit === undefined ? true : implicit === 0 ? false : undefined,
451
+ };
452
+ }
453
+
454
+ /** Whether a declaration shows its return is a scalar or `void`, so no struct pointer is handed
455
+ * in ahead of its parameters: a `void`, or a stated return that is no aggregate and whose width
456
+ * or floating type this can read. A project typedef (`Pt`) shows nothing. */
457
+ function showsScalarReturn(p: FnProto | undefined): boolean {
458
+ return (
459
+ declaresVoidReturn(p) ||
460
+ (p?.returns !== undefined &&
461
+ !declaresAggregateReturn(p) &&
462
+ (declaredWidth(p.returns) !== undefined || isFloatingSpelling(p.returns)))
463
+ );
464
+ }
465
+
466
+ /** The parameter list the own declaration states, or where it states none the one a mangled symbol
467
+ * records (`demangle`), as {@link ownArgumentWords} reads it. */
468
+ function ownParamList(name: string, p: FnProto | undefined): (FnProto & { params: ParamType[] }) | undefined {
469
+ const mangled = Array.isArray(p?.params) ? null : demangle(name);
470
+ const decl: FnProto | undefined = Array.isArray(p?.params)
471
+ ? p
472
+ : mangled === null
473
+ ? undefined
474
+ : { params: mangled.params.map(spellType) };
475
+ return Array.isArray(decl?.params) ? (decl as FnProto & { params: ParamType[] }) : undefined;
476
+ }
477
+
478
+ /** HOW MANY OF THE FIRST ARGUMENT REGISTERS SURELY HOLD ONE OF THE FUNCTION'S OWN PARAMETERS, each a
479
+ * single integer-class word — a LOWER bound, where {@link ownArgumentWords} is an upper one, and 0
480
+ * where the declaration says nothing surely.
481
+ *
482
+ * Its reader is the guessed-arity gap (`frontend/call-plan.ts` `decideGap`), which may read an
483
+ * argument register left at its incoming value as an argument passed on only where that register
484
+ * surely holds a parameter. So every "may" counts against it: a hidden word that may come first (a
485
+ * return the declaration does not show is a scalar, a symbol that may be a method) makes it 0,
486
+ * and the count stops at the first parameter that is not one integer word — a floating one may
487
+ * travel in the FPU's registers and leave its general words holding nothing (O32's leading
488
+ * float), a wide or unsized one takes words this does not place.
489
+ *
490
+ * {@link ownLeadingWordRegisters} asks the other question the same run of parameters answers. */
491
+ export function ownLeadingWordParams(
492
+ name: string,
493
+ p: FnProto | undefined,
494
+ target: Pick<TargetDescription, 'fpu' | 'evenWordAlignedArgs'>,
495
+ registerReturn = false,
496
+ ): number {
497
+ const decl = ownParamList(name, p);
498
+ const own = ownArguments(name, p, target, registerReturn);
499
+ return decl === undefined || own === undefined || own.hidden !== 0 ? 0 : leadingWordRun(decl);
500
+ }
501
+
502
+ /** HOW MANY OF THE FIRST ARGUMENT REGISTERS SURELY HOLD ONE INTEGER WORD, WHOEVER'S, by the own
503
+ * declaration (or the parameter list a mangled symbol records), and 0 where it states no list. A
504
+ * hidden word (a struct return's pointer, `this`, a constructor's flag) is a word too, and only
505
+ * moves the parameters to later registers, so each of the first that-many registers holds a word
506
+ * whichever way it falls, and a later parameter this cannot place does not move the ones before
507
+ * it. Its reader is the Thumb frame's by-value home test (`frontend/thumb.ts` `argumentHomes`): a
508
+ * register that surely holds a word is no by-value struct's home. */
509
+ export function ownLeadingWordRegisters(name: string, p: FnProto | undefined): number {
510
+ const decl = ownParamList(name, p);
511
+ return decl === undefined ? 0 : leadingWordRun(decl);
512
+ }
513
+
514
+ /** The run of leading parameters that are each one integer-class word: it stops at the first
515
+ * floating one or the first {@link paramClassOf} does not call `'word'`. */
516
+ function leadingWordRun(decl: FnProto & { params: ParamType[] }): number {
517
+ let n = 0;
518
+ while (
519
+ n < decl.params.length &&
520
+ !isFloatingSpelling(decl.params[n]) &&
521
+ paramClassOf(decl, decl.params, n) === 'word'
522
+ ) {
523
+ n++;
524
+ }
525
+ return n;
526
+ }
527
+
528
+ /** Whether the own declaration (or the parameter list a mangled symbol records) states a floating
529
+ * parameter, or `undefined` where it states no list. Its reader is the guessed-arity gap
530
+ * (`frontend/call-plan.ts` `decidedCallRefusal`): a float may travel in an FPU register that no
531
+ * guess counts. */
532
+ export function ownDeclaresFloatingParam(name: string, p: FnProto | undefined): boolean | undefined {
533
+ return ownParamList(name, p)?.params.some((t) => isFloatingSpelling(t));
534
+ }
535
+
536
+ /** The widths `ownArgumentWords` lays out: one per declared parameter that takes general words. */
537
+ function ownParamWidths(
538
+ name: string,
539
+ p: FnProto | undefined,
540
+ target: Pick<TargetDescription, 'fpu'>,
541
+ ): number[] | undefined {
542
+ const decl = ownParamList(name, p);
543
+ if (decl === undefined) {
544
+ return undefined;
545
+ }
546
+ const params = decl.params;
547
+ const widths: number[] = [];
548
+ for (const k of params.keys()) {
549
+ const cls = paramClassOf(decl, params, k);
550
+ if (cls === 'word' || cls === 'wide') {
551
+ widths.push(cls === 'word' ? 32 : (declaredWidth(params[k]) ?? 64));
552
+ } else if (cls === 'float') {
553
+ const bare = params[k]
554
+ .replace(/\b(?:const|volatile)\b/g, ' ')
555
+ .trim()
556
+ .replace(/\s+/g, ' ');
557
+ const spelled = FLOAT_TYPEDEFS.get(bare) ?? bare;
558
+ if (spelled === 'long double') {
559
+ return undefined;
560
+ }
561
+ // a `float` may be an old-style definition's, promoted to `double`
562
+ if (target.fpu?.slots !== 'separate') {
563
+ widths.push(64);
564
+ }
565
+ } else {
566
+ return undefined;
567
+ }
568
+ }
569
+ return widths;
570
+ }
571
+
216
572
  /** Whether a declared type is `double`, qualifiers aside. */
217
573
  const isDoubleSpelling = (t: ParamType): boolean => t.replace(/\b(?:const|volatile)\b/g, ' ').trim() === 'double';
218
574
 
219
- /** Whether a declared type is a floating type, qualifiers aside: a C keyword, or a typedef of one
220
- * every project declares (`FLOAT_TYPEDEFS`). */
221
- export const isFloatingSpelling = (t: ParamType): boolean => {
575
+ /** The floating type a declared type is, qualifiers aside — a C keyword, or a typedef of one every
576
+ * project declares (`FLOAT_TYPEDEFS`) — or undefined for any other type. */
577
+ export const floatingSpelling = (t: ParamType): 'float' | 'double' | 'long double' | undefined => {
222
578
  const bare = t
223
579
  .replace(/\b(?:const|volatile)\b/g, ' ')
224
580
  .trim()
225
581
  .replace(/\s+/g, ' ');
226
- return /^(?:float|double|long double)$/.test(FLOAT_TYPEDEFS.get(bare) ?? bare);
582
+ const spelled = FLOAT_TYPEDEFS.get(bare) ?? bare;
583
+ return spelled === 'float' || spelled === 'double' || spelled === 'long double' ? spelled : undefined;
227
584
  };
228
585
 
586
+ /** Whether a declared type is a floating type (`floatingSpelling`). */
587
+ export const isFloatingSpelling = (t: ParamType): boolean => floatingSpelling(t) !== undefined;
588
+
229
589
  /** A signature the C standard fixes is a COMPLETE one, which an `FnProto` is not: a project
230
590
  * prototype is a lower bound assembled from whatever a header extraction could read, and omits
231
591
  * what it could not. The standard omits nothing, so the RETURN is spelled here and is required —
@@ -233,6 +593,16 @@ export const isFloatingSpelling = (t: ParamType): boolean => {
233
593
  interface StandardSignature extends FnProto {
234
594
  /** the return type, as its C spelling (`"void *"`, `"u32"`, `"void"`) */
235
595
  returns: ParamType;
596
+ /** for a BLOCK COPY, the operand each role is: it reads `[0, n)` through `src`, writes `[0, n)`
597
+ * through `dst`, and keeps neither address (`blockCopyRoles`) */
598
+ copies?: BlockCopyRoles;
599
+ }
600
+
601
+ /** The operands of a standard block copy, by role (`StandardSignature.copies`). */
602
+ export interface BlockCopyRoles {
603
+ readonly dst: number;
604
+ readonly src: number;
605
+ readonly n: number;
236
606
  }
237
607
 
238
608
  /** Signatures FIXED BY THE C STANDARD, so they are not a project fact and need no header to be
@@ -251,9 +621,20 @@ interface StandardSignature extends FnProto {
251
621
  * standard and equally addable, and they are absent because nothing measures them — a table
252
622
  * grown by appetite would be a table nobody priced. */
253
623
  export const STANDARD_SIGNATURES: Record<string, StandardSignature> = {
254
- memcpy: { params: ['void *', 'const void *', 'u32'], returns: 'void *' },
624
+ memcpy: { params: ['void *', 'const void *', 'u32'], returns: 'void *', copies: { dst: 0, src: 1, n: 2 } },
255
625
  };
256
626
 
627
+ /** The roles of `callee`'s operands where the C standard fixes it as a BLOCK COPY, or `undefined`.
628
+ * ONE TABLE for every reader: the frame audit bounds the bytes a copy with a literal count reaches
629
+ * (frontend/frame-objects.ts `boundedCopy`), and the constant-initializer fold reads the copy an
630
+ * aggregate initializer compiles to (l3/constinit.ts). `memmove` is equally fixed, and absent for
631
+ * the reason the list above is short. */
632
+ export function blockCopyRoles(callee: unknown): BlockCopyRoles | undefined {
633
+ return typeof callee === 'string' && Object.hasOwn(STANDARD_SIGNATURES, callee)
634
+ ? STANDARD_SIGNATURES[callee].copies
635
+ : undefined;
636
+ }
637
+
257
638
  /** Whether a declaration says the function returns NO VALUE — the one question `returnsVoid` was
258
639
  * added for, asked through both keys that can answer it.
259
640
  *
@@ -271,6 +652,18 @@ export function declaresVoidReturn(p: FnProto | undefined): boolean {
271
652
  return p?.returnsVoid === true || p?.returns?.trim() === 'void';
272
653
  }
273
654
 
655
+ /** The bit width of the scalar the function UNDER DECOMPILATION declares it returns, or `undefined`
656
+ * where its declaration states none this can read: no `returns`, a struct or union by value, a
657
+ * floating type or a project typedef (`declaredWidth`'s "no opinion"). A pointer answers 32,
658
+ * `long long` 64.
659
+ *
660
+ * Not {@link declaredReturnWidth}, which answers for a CALLEE and only where the candidate can
661
+ * print its declaration: the lifted function's own signature is the one the candidate defines,
662
+ * so whether its parameters are spellable says nothing about what it returns. */
663
+ export function ownReturnWidth(p: FnProto | undefined): number | undefined {
664
+ return p?.returns === undefined || declaresAggregateReturn(p) ? undefined : declaredWidth(p.returns);
665
+ }
666
+
274
667
  /** Whether a declaration says nothing any reader acts on about the return — no spelling, no
275
668
  * layout, and no `returnsVoid: true`. `returnsVoid: false` is silence (`declaresVoidReturn`), and
276
669
  * it is how a manifest spells a callee it knows only the parameters of. Such an entry can take a
@@ -370,6 +763,26 @@ export function declaredWidth(t: ParamType): number | undefined {
370
763
  return BASE_WIDTHS.get(base === '' && s !== '' ? 'int' : base);
371
764
  }
372
765
 
766
+ /** Whether one declared parameter type spells a signed integer, or `undefined` where it does not say:
767
+ * a project typedef, a pointer, a plain `char` (whose signedness is the compiler's). */
768
+ export function declaredSigned(t: ParamType): boolean | undefined {
769
+ const s = t
770
+ .replace(/\b(?:const|volatile)\b/g, ' ')
771
+ .trim()
772
+ .replace(/\s+/g, ' ');
773
+ if (isAddress(s)) {
774
+ return undefined;
775
+ }
776
+ const own = /^([su])(8|16|32|64)$/.exec(s);
777
+ if (own) {
778
+ return own[1] === 's';
779
+ }
780
+ if (/\bunsigned\b/.test(s) || /^uint\d+_t$/.test(s)) {
781
+ return false;
782
+ }
783
+ return /\bsigned\b/.test(s) || /^int\d+_t$/.test(s) || /^(?:short|int|long)\b/.test(s) ? true : undefined;
784
+ }
785
+
373
786
  /** Whether the type name `s` is a pointer by its abstract declarator, the derivation nearest the
374
787
  * absent name: `u8 *`, `void (*)(s32)`, `f32 (*)[3]`, `void (**)(void)`. A C++ pointer to a data
375
788
  * member is one too, and a pointer to a member function is not: compiled with mwcc GC/1.2.5n,
@@ -519,15 +932,25 @@ export function spellableProto(
519
932
  p: FnProto | undefined,
520
933
  target: Pick<TargetDescription, 'doubleArgWords'>,
521
934
  returned?: IrType,
935
+ byValue?: ReadonlySet<number>,
522
936
  ): { readonly params: readonly ParamType[]; readonly returns: ParamType } | undefined {
523
937
  // A struct returned through memory prints as the headers spell the struct the lift typed its call
524
938
  // as (`returned`), which the declarations block defines beside this prototype (declare.ts). With
525
939
  // no such call it is a return this cannot print.
526
- const returns = returned?.kind === 'struct' ? (returned.declared ?? `struct ${returned.name}`) : p?.returns;
940
+ // A callee passed a struct by value prints whenever its declaration is complete, `returnsVoid`
941
+ // standing for `void`: the struct is defined only beside it, and the local the lift passes is of
942
+ // that type, so without it the candidate cannot declare its own local.
943
+ const passesStruct = byValue !== undefined && byValue.size > 0;
944
+ const returns =
945
+ returned?.kind === 'struct'
946
+ ? (returned.declared ?? `struct ${returned.name}`)
947
+ : (p?.returns ?? (passesStruct && p?.returnsVoid === true ? 'void' : undefined));
527
948
  if (
528
949
  p === undefined ||
529
950
  returns === undefined ||
530
- (returned === undefined && (!spellableType(returns) || declaredWidth(returns) === undefined))
951
+ (returned === undefined &&
952
+ !(passesStruct && returns.trim() === 'void') &&
953
+ (!spellableType(returns) || declaredWidth(returns) === undefined))
531
954
  ) {
532
955
  return undefined;
533
956
  }
@@ -535,7 +958,19 @@ export function spellableProto(
535
958
  return { params: [], returns };
536
959
  }
537
960
  // A parameter prints where the frontend lays it out (`declaredCallArgs`): a type this spells, or a
538
- // `double` the target gives general words, which as a C keyword needs no typedef.
961
+ // `double` the target gives general words, which as a C keyword needs no typedef — or a struct
962
+ // the lift passed by value (`byValue`, by parameter index), which the declarations block defines
963
+ // beside this prototype from its `byValueLayouts`, as it defines a returned one.
964
+ if (Array.isArray(p.params) && byValue !== undefined && passesStruct) {
965
+ const params = p.params;
966
+ const rest = params.map((t, i) => (byValue.has(i) ? 'u32' : t));
967
+ const laid = declaredCallArgs({ ...p, params: rest }, target);
968
+ return laid !== undefined &&
969
+ [...byValue].every((i) => p.byValueLayouts !== undefined && Object.hasOwn(p.byValueLayouts, i)) &&
970
+ rest.every((t, i) => laid.doubles.has(i) || spellableType(t))
971
+ ? { params, returns }
972
+ : undefined;
973
+ }
539
974
  const laid = declaredCallArgs(p, target);
540
975
  return Array.isArray(p.params) &&
541
976
  laid !== undefined &&
@@ -586,16 +1021,96 @@ export function validatePrototypes(value: unknown): string[] {
586
1021
  continue;
587
1022
  }
588
1023
  for (const key of Object.keys(proto)) {
589
- if (key !== 'params' && key !== 'returnsVoid' && key !== 'returns' && key !== 'returnLayout') {
590
- problems.push(`${sym}: unknown key "${key}" (expected "params", "returnsVoid", "returns" or "returnLayout")`);
1024
+ if (
1025
+ key !== 'params' &&
1026
+ key !== 'returnsVoid' &&
1027
+ key !== 'returns' &&
1028
+ key !== 'returnLayout' &&
1029
+ key !== 'wordParams' &&
1030
+ key !== 'byValueParams' &&
1031
+ key !== 'byValueLayouts' &&
1032
+ key !== 'variadicParams' &&
1033
+ key !== 'fromContext'
1034
+ ) {
1035
+ problems.push(
1036
+ `${sym}: unknown key "${key}" (expected "params", "returnsVoid", "returns", "returnLayout", "wordParams", ` +
1037
+ '"byValueParams", "byValueLayouts", "variadicParams" or "fromContext")',
1038
+ );
1039
+ }
1040
+ }
1041
+ if ('fromContext' in proto && typeof (proto as { fromContext?: unknown }).fromContext !== 'boolean') {
1042
+ problems.push(`${sym}: "fromContext" must be a boolean`);
1043
+ }
1044
+ const { params, returnsVoid, returns, returnLayout, wordParams, byValueParams, byValueLayouts, variadicParams } =
1045
+ proto as {
1046
+ params?: unknown;
1047
+ returnsVoid?: unknown;
1048
+ returns?: unknown;
1049
+ returnLayout?: unknown;
1050
+ wordParams?: unknown;
1051
+ byValueParams?: unknown;
1052
+ byValueLayouts?: unknown;
1053
+ variadicParams?: unknown;
1054
+ };
1055
+ // a variadic declaration's fixed parameters, which it states beside no typed list
1056
+ if (
1057
+ variadicParams !== undefined &&
1058
+ !(Array.isArray(variadicParams) && variadicParams.every((t) => typeof t === 'string') && params === undefined)
1059
+ ) {
1060
+ problems.push(`${sym}: "variadicParams" must be a list of parameter types, beside no "params"`);
1061
+ }
1062
+ const listed = Array.isArray(params) ? params : Array.isArray(variadicParams) ? variadicParams : undefined;
1063
+ for (const [key, list] of [
1064
+ ['wordParams', wordParams],
1065
+ ['byValueParams', byValueParams],
1066
+ ] as const) {
1067
+ if (
1068
+ list !== undefined &&
1069
+ !(
1070
+ listed !== undefined &&
1071
+ Array.isArray(list) &&
1072
+ list.every((k) => Number.isInteger(k) && k >= 0 && k < listed.length)
1073
+ )
1074
+ ) {
1075
+ problems.push(`${sym}: "${key}" must list indices into a typed "params" or "variadicParams" list`);
1076
+ }
1077
+ }
1078
+ // Two answers to one question — whether the parameter travels in a register or as a copy.
1079
+ if (
1080
+ Array.isArray(wordParams) &&
1081
+ Array.isArray(byValueParams) &&
1082
+ wordParams.some((k) => byValueParams.includes(k))
1083
+ ) {
1084
+ problems.push(`${sym}: "wordParams" and "byValueParams" both list one parameter`);
1085
+ }
1086
+ // A layout keyed by a parameter the list does not pass by value would size a word as a struct.
1087
+ if (byValueLayouts !== undefined) {
1088
+ if (typeof byValueLayouts !== 'object' || byValueLayouts === null || Array.isArray(byValueLayouts)) {
1089
+ problems.push(`${sym}: "byValueLayouts" must map a parameter index to its layout`);
1090
+ } else {
1091
+ for (const [key, layout] of Object.entries(byValueLayouts)) {
1092
+ const k = Number(key);
1093
+ if (
1094
+ !Array.isArray(params) ||
1095
+ !params.every((t) => typeof t === 'string') ||
1096
+ !/^\d+$/.test(key) ||
1097
+ k >= params.length ||
1098
+ paramClassOf(
1099
+ {
1100
+ wordParams: Array.isArray(wordParams) ? wordParams : [],
1101
+ byValueParams: Array.isArray(byValueParams) ? byValueParams : [],
1102
+ },
1103
+ params,
1104
+ k,
1105
+ ) !== 'by-value'
1106
+ ) {
1107
+ problems.push(`${sym}: "byValueLayouts" key "${key}" is not a parameter the typed "params" pass by value`);
1108
+ continue;
1109
+ }
1110
+ problems.push(...layoutProblems(`${sym}: "byValueLayouts" ${key}`, layout));
1111
+ }
591
1112
  }
592
1113
  }
593
- const { params, returnsVoid, returns, returnLayout } = proto as {
594
- params?: unknown;
595
- returnsVoid?: unknown;
596
- returns?: unknown;
597
- returnLayout?: unknown;
598
- };
599
1114
  if (returnLayout !== undefined) {
600
1115
  problems.push(...layoutProblems(`${sym}: "returnLayout"`, returnLayout));
601
1116
  // A layout beside a return that says `void` or spells a scalar is two answers to one question