@asmlift/core 0.8.1 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (130) hide show
  1. package/README.md +36 -31
  2. package/package.json +1 -1
  3. package/src/aggregate.ts +397 -0
  4. package/src/backend/c.ts +6 -2
  5. package/src/backend/cfamily.ts +370 -56
  6. package/src/backend/cpp.ts +116 -10
  7. package/src/backend/pascal.ts +69 -11
  8. package/src/cdecl/constant.ts +59 -0
  9. package/src/cdecl/lex.ts +209 -0
  10. package/src/cdecl/parse.ts +975 -0
  11. package/src/cdecl/spell.ts +69 -0
  12. package/src/codegen-flags.ts +38 -6
  13. package/src/compiler-diagnostics.ts +194 -0
  14. package/src/contracts.ts +777 -84
  15. package/src/declare.ts +132 -13
  16. package/src/frontend/asmdata.ts +26 -6
  17. package/src/frontend/call-plan.ts +1590 -0
  18. package/src/frontend/dead-reads.ts +628 -0
  19. package/src/frontend/device-pins.ts +296 -0
  20. package/src/frontend/emit.ts +4 -3
  21. package/src/frontend/errors.ts +5 -0
  22. package/src/frontend/flags-edge.ts +101 -0
  23. package/src/frontend/fpu.ts +374 -0
  24. package/src/frontend/frame-objects.ts +3171 -0
  25. package/src/frontend/frontend.ts +64 -3
  26. package/src/frontend/high-half.ts +55 -5
  27. package/src/frontend/jump-table.ts +530 -0
  28. package/src/frontend/local-object.ts +616 -0
  29. package/src/frontend/mips.ts +2392 -208
  30. package/src/frontend/opaque.ts +97 -18
  31. package/src/frontend/own-struct-return.ts +500 -0
  32. package/src/frontend/ppc.ts +3137 -267
  33. package/src/frontend/register-struct-call.ts +484 -0
  34. package/src/frontend/reloc-symbol.ts +109 -13
  35. package/src/frontend/splat.ts +108 -5
  36. package/src/frontend/ssa.ts +2098 -89
  37. package/src/frontend/stackargs.ts +172 -42
  38. package/src/frontend/thumb.ts +4563 -2091
  39. package/src/ir/alias.ts +71 -0
  40. package/src/ir/bits.ts +681 -1
  41. package/src/ir/core.ts +199 -6
  42. package/src/ir/discipline.ts +313 -0
  43. package/src/ir/entry.ts +159 -0
  44. package/src/ir/float-bits.ts +47 -0
  45. package/src/{structure → ir}/loops.ts +6 -2
  46. package/src/ir/opcodes.ts +193 -121
  47. package/src/ir/parse.ts +1 -0
  48. package/src/ir/types.ts +202 -2
  49. package/src/ir/verify.ts +219 -3
  50. package/src/l3/address.ts +5 -0
  51. package/src/l3/argcopy.ts +38 -6
  52. package/src/l3/ast.ts +467 -23
  53. package/src/l3/basecse.ts +33 -5
  54. package/src/l3/coalesce.ts +21 -13
  55. package/src/l3/constinit.ts +382 -0
  56. package/src/l3/dce.ts +34 -12
  57. package/src/l3/gates.ts +9 -3
  58. package/src/l3/hoist.ts +8 -7
  59. package/src/l3/initfirst.ts +68 -19
  60. package/src/l3/intcell.ts +98 -0
  61. package/src/l3/mentions.ts +10 -4
  62. package/src/l3/narrowdecl.ts +259 -0
  63. package/src/l3/nearbase.ts +10 -5
  64. package/src/l3/parkfirst.ts +8 -5
  65. package/src/l3/pollguard.ts +11 -4
  66. package/src/l3/ptrcell.ts +354 -0
  67. package/src/l3/ptrfield.ts +123 -8
  68. package/src/l3/regspell.ts +26 -7
  69. package/src/l3/reindex.ts +293 -103
  70. package/src/l3/storage.ts +98 -1
  71. package/src/l3/symbol-refs.ts +182 -23
  72. package/src/l3/tailmerge.ts +9 -20
  73. package/src/l3/typing.ts +237 -32
  74. package/src/l3/unreduce.ts +10 -9
  75. package/src/l3/volatileptr.ts +17 -54
  76. package/src/l3/volstore.ts +96 -7
  77. package/src/mangle.ts +41 -1
  78. package/src/pattern/engine.ts +373 -82
  79. package/src/pipeline.ts +589 -27
  80. package/src/proto-context.ts +1648 -0
  81. package/src/proto.ts +1203 -49
  82. package/src/raise/aggmembers.ts +290 -0
  83. package/src/raise/arrays.ts +8 -3
  84. package/src/raise/declared-volatile.ts +307 -0
  85. package/src/raise/divpow2.ts +2 -2
  86. package/src/raise/errors.ts +17 -0
  87. package/src/raise/floathelpers.ts +464 -0
  88. package/src/raise/globalshape.ts +11 -5
  89. package/src/raise/gvn.ts +29 -8
  90. package/src/raise/latch.ts +162 -6
  91. package/src/raise/memberarrays.ts +10 -4
  92. package/src/raise/narrow.ts +29 -2
  93. package/src/raise/narrowlocal.ts +11 -6
  94. package/src/raise/offsetnames.ts +494 -0
  95. package/src/raise/pairparams.ts +114 -0
  96. package/src/raise/paramwidth.ts +203 -6
  97. package/src/raise/pre-recovery.ts +174 -17
  98. package/src/raise/pre-structure.ts +54 -0
  99. package/src/raise/recover.ts +26 -15
  100. package/src/raise/reroll.ts +694 -0
  101. package/src/raise/retsink.ts +1 -1
  102. package/src/raise/shortcircuit.ts +66 -30
  103. package/src/raise/softdiv.ts +21 -23
  104. package/src/raise/storesign.ts +153 -0
  105. package/src/raise/struct-arrays.ts +34 -14
  106. package/src/raise/structs.ts +674 -98
  107. package/src/raise/truncload.ts +367 -0
  108. package/src/raise/unthread.ts +400 -0
  109. package/src/raise/widehelpers.ts +106 -0
  110. package/src/rank-declare.ts +143 -12
  111. package/src/rank-variations.ts +117 -31
  112. package/src/rank.ts +563 -122
  113. package/src/runtime-helpers.ts +405 -0
  114. package/src/stillborn.ts +289 -0
  115. package/src/structure/analysis.ts +1449 -449
  116. package/src/structure/arith-ops.ts +23 -0
  117. package/src/structure/globalaccess.ts +111 -9
  118. package/src/structure/hazards.ts +641 -114
  119. package/src/structure/local-statics.ts +517 -0
  120. package/src/structure/namecoalesce.ts +2 -2
  121. package/src/structure/pointer-spelling.ts +1106 -0
  122. package/src/structure/redundant-test.ts +99 -0
  123. package/src/structure/structure.ts +3478 -975
  124. package/src/structure/switch-recover.ts +403 -178
  125. package/src/symbols.ts +44 -3
  126. package/src/target.ts +1387 -71
  127. package/src/trace.ts +66 -28
  128. package/src/variation-definitions.ts +243 -13
  129. package/src/variation-gates.ts +2 -0
  130. package/src/variation-tokens.ts +25 -7
@@ -0,0 +1,1590 @@
1
+ // asmlift — WHAT A CALL IS, read off its callee's declaration: the one place a frontend reads one.
2
+ //
3
+ // A declaration is the project's prototype, the compiler's runtime table or a signature the C
4
+ // standard fixes, and every frontend asks the same questions of it: how wide each argument is,
5
+ // whether argument 0 is a hidden struct-return pointer, whether the result comes back as a pair,
6
+ // and which registers the call leaves holding nothing the caller can name. The frontend states what
7
+ // its own lowering can carry out (`CallLowering`) and refuses in its own error class (`fail`).
8
+ // Every answer is read through the generic `TargetDescription` surface. WHERE each argument word
9
+ // travels is the frontend's own placement (Thumb's outgoing block, O32's `16(sp)`): a width list
10
+ // here counts words as `wordsOf` does, which is agbcc's placement, and a lowering that builds no
11
+ // pair (`CallLowering.pairs`) refuses a 64-bit width before any count is taken. The call a plan
12
+ // lowers to is built here too (`CallDeclarations.lower`), and asks the frontend (`CallSite`) only
13
+ // what its ISA alone knows: how it reads and writes a register, where an argument word past the
14
+ // registers lives, and how it builds a pair.
15
+ import {
16
+ type ByValueArg,
17
+ aggregateType,
18
+ byValueCallArgs,
19
+ byValueRefusal,
20
+ ownArgumentWordsOn,
21
+ ownLeadingWordParamsOn,
22
+ returnedAggregate,
23
+ returnsInMemory,
24
+ returnsWithoutHiddenPointer,
25
+ } from '../aggregate';
26
+ import { type Block, type Op, REGISTER_STRUCT, type Value, calleeName, mkOp, mkValue } from '../ir/core';
27
+ import { qualified } from '../ir/discipline';
28
+ import { OPCODES, type OpSig } from '../ir/opcodes';
29
+ import { type IrType, T, structSpelling, typeToString } from '../ir/types';
30
+ import {
31
+ type FnProto,
32
+ type ParamClass,
33
+ type Prototypes,
34
+ calleeDeclaration,
35
+ declaredCallArgs,
36
+ declaredReturnWidth,
37
+ declaresAggregateReturn,
38
+ declaresParams,
39
+ declaresVoidReturn,
40
+ floatingSpelling,
41
+ isFloatingSpelling,
42
+ ownDeclaresFloatingParam,
43
+ paramClassOf,
44
+ spellableProto,
45
+ variadicRefusal,
46
+ wordsOf,
47
+ } from '../proto';
48
+ import { type ContextGlobals, structNamed } from '../proto-context';
49
+ import { floatSidesOf, helperPrototypes, isFloatHelper, isWideHelper, lookupHelper } from '../runtime-helpers';
50
+ import { type SymbolMap, symbolsByName } from '../symbols';
51
+ import { type TargetDescription, argRegisterHoles } from '../target';
52
+ import { type FrontendRefusal, FrontendUnsupportedError } from './errors';
53
+ import { type OwnDeclaration, ownDeclarationOf } from './frontend';
54
+ import type { HighHalves } from './high-half';
55
+ import { unspellableReason } from './reloc-symbol';
56
+ import { type SsaBuilder, clobberedByCall, fallbackArgc } from './ssa';
57
+
58
+ /** What the frontend's own call lowering can carry out. */
59
+ export interface CallLowering {
60
+ /** builds a 64-bit value out of two argument words, and splits a 64-bit return */
61
+ readonly pairs: boolean;
62
+ /** passes a hidden struct-return pointer in argument 0 and types the call's value as the struct */
63
+ readonly memoryReturn: boolean;
64
+ /** reads argument words past the registers off its own frame */
65
+ readonly stackArgs: boolean;
66
+ /** writes no value for a callee declared void, and its own return reads the return register only
67
+ * where a value reaches it (`SsaBuilder.holdsValue`), so the register the call destroyed is no
68
+ * return value */
69
+ readonly voidReturn: boolean;
70
+ /** a call through argument register rN passes r0..r(N-1). The compiler's arguments fill the
71
+ * registers from r0 up and the address cannot share one, which holds for agbcc: soft-float, no
72
+ * pair alignment. o32 refutes it twice — a leading float takes slot a0 and leaves the register
73
+ * free (`jalr a0` passing $f12 and a1), and a 64-bit argument's even pair leaves a1 empty
74
+ * (`jalr a1` passing a0, a2 and a3) — so a lowering that does not state it guesses. */
75
+ readonly argRegisterBoundsArity: boolean;
76
+ }
77
+
78
+ /** A call's struct return through memory: the declared struct, laid out on this target. */
79
+ interface StructReturn {
80
+ type: IrType;
81
+ }
82
+
83
+ /** What one callee's declaration states about a call (`CallDeclarations.declaredCall`). */
84
+ export interface DeclaredCall {
85
+ /** each argument's width, in bits, a hidden struct-return pointer first */
86
+ readonly widths: readonly number[];
87
+ /** the indices into `widths` that are a `double` */
88
+ readonly doubles: ReadonlySet<number>;
89
+ /** the declared parameter count, a hidden pointer aside */
90
+ readonly params: number;
91
+ readonly returned?: StructReturn | 'register';
92
+ /** each struct passed by value, by its first word in `widths` (a hidden pointer counted) */
93
+ readonly byValue?: readonly ByValueArg[];
94
+ }
95
+
96
+ /** WHAT A DECLARED PARAMETER IS, as far as where the caller places it goes (`paramClassOf`,
97
+ * proto.ts, which `declaredCall`'s widths read too). */
98
+ export type { ParamClass };
99
+
100
+ /** One declared parameter: its class, and the spelling it was read from (empty for a count). */
101
+ export interface DeclaredParam {
102
+ readonly kind: ParamClass;
103
+ readonly spelling: string;
104
+ }
105
+
106
+ /** What one call is, decided from its callee's declaration before any instruction is read. */
107
+ export interface CallPlan {
108
+ /** each argument's width, a hidden pointer first; null when nothing sizes them, so `lower`
109
+ * guesses */
110
+ readonly widths: readonly number[] | null;
111
+ /** the indices into `widths` its declaration types `double` */
112
+ readonly doubles: ReadonlySet<number>;
113
+ /** the structs its declaration passes by value, by their words in `widths` */
114
+ readonly byValue?: readonly ByValueArg[];
115
+ readonly returns:
116
+ | { readonly kind: 'word' }
117
+ | { readonly kind: 'void' }
118
+ | { readonly kind: 'pair' }
119
+ | {
120
+ readonly kind: 'register-struct';
121
+ /** the return register holds the struct's image, which a local of it is read off
122
+ * (`registerStructLocal`); false, it holds nothing the caller may read */
123
+ readonly image: boolean;
124
+ }
125
+ | { readonly kind: 'memory-struct'; readonly type: IrType };
126
+ /** what the call leaves holding nothing this function can name, for `SsaBuilder.noteCall` */
127
+ readonly clobbers: readonly string[];
128
+ /** the callee is declared void, whether or not this lowering writes no value for it (`returns`
129
+ * is `void` only where `CallLowering.voidReturn` says so) */
130
+ readonly declaredVoid: boolean;
131
+ /** the floating type an ordinary callee is declared to return where the target has no FPU, so
132
+ * the return register holds its bits (`raise/floathelpers.ts` holds every read of them) */
133
+ readonly floatReturn?: 'float' | 'double' | 'long double';
134
+ }
135
+
136
+ /** WHAT EACH CALLEE'S DECLARATION SAYS, and what a call leaves holding nothing this function can
137
+ * name. A declaration is the project's prototype or the compiler's runtime table, and a frontend
138
+ * reads either only here: its outgoing-argument analysis, its call lowering (`lower`) and the
139
+ * frame-object audit (`FrameObjectAudit.returnsWithoutHiddenPointer`) ask these questions. */
140
+ export interface CallDeclarations {
141
+ /** what any call leaves holding nothing this function can name (`clobberedByCall`) */
142
+ readonly callClobbers: readonly string[];
143
+ /** whether a declaration rules out a struct returned through a hidden pointer at argument 0
144
+ * (`returnsWithoutHiddenPointer`, aggregate.ts) */
145
+ returnsWithoutHiddenPointer(callee: string): boolean;
146
+ /** the callee's declared argument widths, for a frontend that places them before it lowers the
147
+ * call; null where no declaration sizes them */
148
+ declaredCall(callee: string): DeclaredCall | null;
149
+ /** what each parameter the callee's declaration spells is (`ParamClass`), read through the tiers
150
+ * `declaredCall` reads; null where no declaration states the parameters */
151
+ declaredParams(callee: string): readonly DeclaredParam[] | null;
152
+ /** `lower`'s first half, the call decided from the declaration alone: a value its tests compare
153
+ * whole, where `lower` leaves IR */
154
+ plan(callee: string): CallPlan;
155
+ /** the call `site` makes, planned and lowered: its `call` op, its result and its clobbers */
156
+ lower(site: CallSite): void;
157
+ /** every value an argument register may hold at a call `lower` lowered at an UNDECLARED arity — a
158
+ * guess, or the bound a call through an argument register sets — gathered as each such call is
159
+ * lowered. One the call does not pass is unused by the LIFT, not proved unused by the source: the
160
+ * dead-read pin refuses it (frontend/dead-reads.ts `held-at-undeclared-call`). */
161
+ readonly heldAtUndeclaredCall: ReadonlySet<Value>;
162
+ /** THE #351 GAPLESS GUESS, NOT LET IN THROUGH A JUMP TABLE'S CASES. Once every call is lowered:
163
+ * where the function lifts only because of what an elided or kept dispatch left in its registers
164
+ * — a dispatch's marker or scaled index reaching some guessed call's argument registers
165
+ * (`CallSite.parked`, `CallSite.scaledIndex`; a PowerPC pair's r4 too), which stands where the
166
+ * pre-dispatch value had made a gap refuse, or `caseStateRead`, a read of the scaled index a case
167
+ * target is entered with — refuse the first guessed arity the bytes cannot tell from another:
168
+ * - its last counted register holds a value some path from the dispatch's block to the call
169
+ * did not write (`SsaBuilder.writtenOnEveryPath`; a cross-jumped call's block writes nothing,
170
+ * its cases do), a value passed on unchanged (`g0(a, 9)` lifted `g0(a0, 9, a0)`, agbcc's pre-dispatch copy of
171
+ * a0 still in r2; mwcc `return g()` lifted `return g(a0)`);
172
+ * - a counted register holds a value an effect of its block uses, a store's address or value
173
+ * left in the register (`gc = ga; g()` lifted `g(&gc, v0)`);
174
+ * - a register it leaves out below that marker still holds its incoming value, an argument
175
+ * passed on or not (mwcc `g(a)` with the table base in r4 lifted `g()`).
176
+ * Elsewhere the guess takes these, an open gap (`g()` and `g(a0)` are the same bytes); there the
177
+ * function declined before the dispatch's registers were modelled, and a decline is not turned
178
+ * into that guess. */
179
+ refuseDispatchUnlockedPassThrough(caseStateRead: boolean): void;
180
+ }
181
+
182
+ /** WHAT ONE ISA ALONE KNOWS ABOUT A CALL: everything `CallDeclarations.lower` asks a frontend. The
183
+ * argument and return registers are the target's. */
184
+ export interface CallSite {
185
+ /** the callee's name, or the address of the function a call through a register calls */
186
+ readonly callee: string | IndirectCallee;
187
+ readonly ssa: SsaBuilder;
188
+ /** the block the call is made in */
189
+ readonly bi: number;
190
+ /** an argument register the plan states, read as this frontend reads a register */
191
+ read(reg: string): Value;
192
+ /** the call's result, written as this frontend writes a register */
193
+ write(reg: string, v: Value): void;
194
+ /** argument word `k` past the registers, where this ABI places it (`CallLowering.stackArgs`) */
195
+ stackWord?(k: number): Value;
196
+ /** how this frontend builds and splits a pair (`CallLowering.pairs`) */
197
+ readonly pairs?: CallPairs;
198
+ /** what this frontend defines registers with that is no value and so no argument — a pending
199
+ * high half (frontend/high-half.ts), a jump table's base (frontend/jump-table.ts) */
200
+ readonly placeholders?: Pick<HighHalves, 'has' | 'guardRead'>;
201
+ /** whether `v`, in argument register `reg`, is what a jump-table dispatch left in that very
202
+ * register (frontend/jump-table.ts `parkedIn`): no argument, as a table base is none, so a guessed
203
+ * arity stops below the lowest register only it reaches (`fallbackArgc`'s `parked`), except one
204
+ * this ABI may skip (an O32 double's a1, a PowerPC pair's r4, a leading float's a0), which stays a
205
+ * gap. A copy of one below that ceiling is counted, and its read declines; a copy above it is
206
+ * dropped with everything else there (`mov r2,r1` ahead of `bl g` lifts `g(…)` without it), which
207
+ * only hand asm meets: no compiler copies a marker. */
208
+ readonly parked?: (v: Value, reg: string) => boolean;
209
+ /** whether `v`, in argument register `reg`, is the scaled index a jump-table dispatch left in that
210
+ * very register (frontend/jump-table.ts `scaledIndexIn`): a call handed it and one not compile
211
+ * alike, so a guessed arity that counts it refuses */
212
+ readonly scaledIndex?: (v: Value, reg: string) => boolean;
213
+ /** what a guessed arity refuses on. Required, as the gap refusal it feeds (`fallbackArgc`) has
214
+ * no opt-out: a count that stopped at the first register nothing reaches would drop every
215
+ * argument past an incoming one the call is handed unchanged (`logv(s, 1)` as `logv()`). */
216
+ readonly guess: {
217
+ /** where the call is, for the gap refusal: its address, or — for an ISA whose instructions
218
+ * carry none (Thumb's `.s` text) — a phrase that names it, printed as written */
219
+ readonly at: number | string;
220
+ /** refuses a guess of `argc` words this ABI's outgoing area could extend; absent where the
221
+ * frontend's frame model already refuses every word such a guess could leave out */
222
+ refuse?(argc: number): void;
223
+ };
224
+ /** handed the argument registers an UNDECLARED arity leaves out although a value reaches them —
225
+ * past a guessed count, or past the bound of a call through an argument register — and the ones
226
+ * it passes. The call drops what the left-out ones hold, so a frontend that knows one to be an
227
+ * argument all the same refuses here. */
228
+ readonly leftOut?: (regs: readonly string[], passed: readonly string[]) => void;
229
+ /** whether the frontend withholds the shorter `/setup-args` reading from THIS CALL whatever it
230
+ * cuts; the frontend says why where it sets it. A thunk: it is asked once every block is lowered. */
231
+ readonly withholdsSetupArgs?: () => boolean;
232
+ /** whether the shorter `/setup-args` reading of this call asks `leftOut` of EVERY register it
233
+ * cuts, not only of those the call's own block wrote (`GuessedCallSite.asksEveryCut`); the
234
+ * frontend says why where it sets it. A thunk: it is asked once every block is lowered. */
235
+ readonly asksEveryCut?: () => boolean;
236
+ }
237
+
238
+ /** A call through a register. It names no callee, so no declaration plans it: an argument
239
+ * register holding the address bounds its arity where the lowering states it
240
+ * (`CallLowering.argRegisterBoundsArity`), and otherwise it is guessed. */
241
+ export interface IndirectCallee {
242
+ readonly address: Value;
243
+ /** the register the address is in */
244
+ readonly reg: string;
245
+ }
246
+
247
+ /** How a frontend builds a 64-bit value out of two words and splits one. */
248
+ export interface CallPairs {
249
+ fuse(lo: Value, hi: Value): Value;
250
+ project(whole: Value, half: 'lo' | 'hi'): Value;
251
+ /** the pair each projected half was split from */
252
+ readonly halfOf: ReadonlyMap<Value, { readonly whole: Value; readonly half: 'lo' | 'hi' }>;
253
+ }
254
+
255
+ export function callDeclarations(
256
+ name: string,
257
+ target: TargetDescription,
258
+ prototypes: Prototypes,
259
+ lowering: CallLowering,
260
+ fail: FrontendRefusal,
261
+ /** the function's own declaration, where one binds its body — asked as a call is lowered */
262
+ ownDeclaration: () => OwnDeclaration | undefined = () => ownDeclarationOf(name, prototypes, undefined),
263
+ ): CallDeclarations {
264
+ // What a call leaves holding nothing this function can name — checked against `argRegs` there.
265
+ const callClobbers = clobberedByCall(target);
266
+ // `refuseDispatchUnlockedPassThrough`'s two facts, gathered as each call is lowered: whether a
267
+ // dispatch's marker reached a guessed call's argument registers, and the first guessed count
268
+ // the bytes cannot tell from another, as its refusal
269
+ let parkedInArgs = false;
270
+ let passThrough: { call: string; reg: string; why: string } | undefined;
271
+ // …and what a call to a PAIR-RETURNING helper leaves: the same set minus the high half, because
272
+ // the low half is the return register (already excluded) and `lower` writes the high one itself
273
+ // from the callee's own result. The two arms of one rule: where the callee hands a register back,
274
+ // the lowering names it; where it does not, nobody can.
275
+ //
276
+ // `argRegs[1]` IS THE PAIR'S HIGH REGISTER ONLY WHERE THE ABI ALIASES THE FIRST ARGUMENT ONTO
277
+ // THE RETURN REGISTER, which makes the returned pair occupy the first two argument registers.
278
+ // True on Thumb (r0:r1) and PowerPC (r3:r4); FALSE on MIPS o32, which returns in v0:v1 and passes
279
+ // in a0:a1, so reading a high half out of `argRegs[1]` there would name an argument register. A
280
+ // lowering that builds pairs on a target without the identity is a target bug.
281
+ if (lowering.pairs && target.returnReg !== target.argRegs[0]) {
282
+ throw new Error(
283
+ `target '${target.id}': a lowering that builds pairs needs the return register to be the first ` +
284
+ `argument register, and ${target.returnReg} is not ${target.argRegs[0]}`,
285
+ );
286
+ }
287
+ const pairReturnClobbers = callClobbers.filter((r) => r !== target.argRegs[1]);
288
+ const heldAtUndeclaredCall = new Set<Value>();
289
+ const isFloatHelperName = (callee: string): boolean => {
290
+ const h = lookupHelper(target.runtimeHelpers, callee);
291
+ return h !== undefined && isFloatHelper(h) && floatSidesOf(h).result;
292
+ };
293
+ /** the callee that handed back each pair a call returned, by the pair's value */
294
+ const pairCallee = new Map<Value, string>();
295
+ /** the value each call to a callee declared void left in the return register */
296
+ const voidResults = new Set<Value>();
297
+ // The compiler's own runtime, off the TARGET (runtime-helpers.ts): which helpers a compiler
298
+ // emits is a compiler fact, and reading one table for every ISA is how a scan for `__*di3`
299
+ // reports zero on a compiler whose runtime spells them `__ll_*`.
300
+ const helperProtos = helperPrototypes(target);
301
+ const wideHelper = (callee: string) => {
302
+ // Through the table's one reader (`lookupHelper`): a bare index answers with a member of
303
+ // `Object.prototype` for a callee named `toString`, which is truthy and has no `params` for
304
+ // `isWideHelper` to read. The `prototypes` read below needs no such guard because
305
+ // `declaresParams` is that table's designated safe reader — it answers "nothing is declared"
306
+ // for an entry that is not an `FnProto`, whatever it is.
307
+ const h = lookupHelper(target.runtimeHelpers, callee);
308
+ if (!h || !isWideHelper(h)) {
309
+ return null;
310
+ }
311
+ // A PROJECT RE-DECLARATION DISABLES THE CAPABILITY, it does not redirect it. A runtime helper's
312
+ // signature is its COMPILER's — `proto.ts` holds the signatures the C standard fixes, which this is precisely
313
+ // not — so a header declaring `__muldi3` is not a better source for the same fact; it is a
314
+ // claim that the name is the project's own function. Neither reading can be honoured: as the
315
+ // table's helper it would contradict the header, and as an ordinary call it becomes the
316
+ // pass-through that MATCHES for free. So no pair is built and `raise/widehelpers.ts` gaps the
317
+ // call, at whichever arity was declared.
318
+ if (declaresParams(prototypes[callee])) {
319
+ return null;
320
+ }
321
+ return h;
322
+ };
323
+ /** Whether the target's own runtime table claims this name — asked of the TABLE, not of what
324
+ * `wideHelper` made of it.
325
+ *
326
+ * A PROJECT'S `returns` NEVER BUILDS A PAIR FOR A RUNTIME HELPER'S NAME: a re-declaration
327
+ * disables the capability for the result as it does for the arguments. `wideHelper` answers
328
+ * null for a re-declared helper, which stops the ARGUMENT pair; the RESULT pair comes from a
329
+ * separate source, and `raise/widehelpers.ts` folds on the result alone (`arrivesAsDeclared`
330
+ * needs `results[0]` 64 bits wide). A pair built from the project's `returns` would fold the
331
+ * call into the compiler's own operation — `__muldi3` printed as `a0 * a1` — for a name the
332
+ * project says is its own function.
333
+ *
334
+ * A NON-WIDE ENTRY IS COVERED TOO, and deliberately: a `returns` on `__divsi3` states a width
335
+ * about a function whose signature is its compiler's, and `refuseUnmodelledHelpers` is going to
336
+ * gap the call whatever this answers. The table is the authority for every name in it. */
337
+ const isRuntimeHelperName = (callee: string): boolean => lookupHelper(target.runtimeHelpers, callee) !== undefined;
338
+ // WHETHER THE CALLEE HANDS BACK A PAIR — two sources for one ABI fact, and they answer the same
339
+ // question about the same two registers. A runtime helper's signature is its compiler's and needs
340
+ // no header; a project's callee needs one, and `returns` is where a header states it. Silence
341
+ // means a word — the callee then defines the return register alone and `frontend/ssa.ts` refuses
342
+ // a read of the other, because in that reading it is right to.
343
+ //
344
+ // AND THEY ARE ASKED IN THAT ORDER, never unioned: a name the runtime table carries is answered
345
+ // by the table or by nothing (`isRuntimeHelperName`), so a header that re-declares a helper
346
+ // disables the capability rather than restoring it through the other key.
347
+ const returnsPair = (callee: string): boolean => {
348
+ const wide = wideHelper(callee);
349
+ return (
350
+ (wide
351
+ ? wide.returns
352
+ : isRuntimeHelperName(callee)
353
+ ? undefined
354
+ : declaredReturnWidth(prototypes[callee], target)) === 64
355
+ );
356
+ };
357
+ // A callee the project declares to return a struct in registers, asked where `declaredCall` has
358
+ // no arity to answer with: the return is the declaration's, and needs none.
359
+ const registerStructReturn = (callee: string | undefined): 'register' | undefined => {
360
+ const own = callee !== undefined && Object.hasOwn(prototypes, callee) ? prototypes[callee] : undefined;
361
+ return declaresAggregateReturn(own) && structReturnOf(name, target, callee!, own!, fail) === 'register'
362
+ ? 'register'
363
+ : undefined;
364
+ };
365
+ // WHAT ONE CALLEE'S DECLARATION SAYS — the ONE place that reads it. A frontend's outgoing-argument
366
+ // analysis and its call lowering both come through here, so the arity that LICENSED a block and
367
+ // the arity that CONSUMES it cannot drift apart; a disagreement between two spellings of this
368
+ // lookup would read an argument register's neighbour as argument 5 or throw a slot-model error
369
+ // naming the wrong thing.
370
+ //
371
+ // THE WIDTHS ARE WHAT IS ASKED FOR, not a C parameter count, which would be the wrong number the
372
+ // moment one parameter is wider than a word: a `long long` adds a word AND moves every later
373
+ // argument's home, in the registers and on the stack alike. `proto.ts` converts the declaration
374
+ // once and every reader of it takes the same answer.
375
+ //
376
+ // WHAT THE CONVERSION CANNOT DO ON ITS OWN. `declaredWidth` answers for every type asmlift can
377
+ // spell, `long long` included, and `undefined` for a project typedef, a by-value struct or a
378
+ // floating type. `declaredCallArgs` sizes a `double` from the target
379
+ // (`TargetDescription.doubleArgWords`) and a typedef the declaration lists in `wordParams` (an
380
+ // enum, an array or a function type: `proto.ts` `paramClassOf`) at one word; one other such
381
+ // spelling and it states no layout at all, because the question here is not how wide that
382
+ // parameter is but whether it occupies one argument register or two, and the choice moves every
383
+ // later argument's home. So the declaration licenses no outgoing block and this returns `null`:
384
+ // the call is lifted at the arg-register guess, exactly as a callee the project never declared
385
+ // is, and the guess retracts the registers a call destroyed where a stated width would assert
386
+ // them.
387
+ //
388
+ // The COUNT form (`{ params: 5 }`) carries no spellings at all: it is the user's word for how
389
+ // many argument REGISTERS the call takes, and a count that lies is garbage in —
390
+ // `validatePrototypes` can no more check it than it can check `returnsVoid`. The machine-derived
391
+ // side upholds the premise at its source: `prototypesFromSymbols` drops a whole entry rather
392
+ // than spell a parameter that is not 1, 2 or 4 bytes (test/proto.test.ts).
393
+ //
394
+ // A frontend that places the widths before it lowers the call runs this over every call, so a
395
+ // refusal thrown here reaches the caller ahead of every other slot-model refusal, which is right
396
+ // because it is the most specific thing that was seen.
397
+ const known = (t: Prototypes, callee: string) => (Object.hasOwn(t, callee) ? t[callee] : undefined);
398
+ /** the declaration a callee is read at: the project's, then the runtime table's, then the C
399
+ * standard's (`proto.ts` `calleeDeclaration`; `declaredCall` gives the reasons for the order) */
400
+ const declarationOf = (callee: string): FnProto | undefined => calleeDeclaration(callee, prototypes, helperProtos);
401
+ const declaredParams = (callee: string): readonly DeclaredParam[] | null => {
402
+ const params = declarationOf(callee)?.params;
403
+ if (typeof params === 'number') {
404
+ return Array.from({ length: params }, () => ({ kind: 'word', spelling: '' }));
405
+ }
406
+ if (!Array.isArray(params)) {
407
+ return null;
408
+ }
409
+ const declared = declarationOf(callee)!;
410
+ return params.map((spelling, k) => ({ kind: paramClassOf(declared, params, k), spelling }));
411
+ };
412
+ // A STRUCT OR UNION PASSED BY VALUE THAT NO RULE PLACES REFUSES; IT IS NOT GUESSED. The arity
413
+ // guess counts argument registers, and a struct is not one: it takes as many words as its size
414
+ // (agbcc, O32), starts at an even word when it is 8-aligned (O32), or travels as the address of
415
+ // a copy (mwcc). A guess over it drops the struct (agbcc `takeQ(k, gQ)` with no layout for
416
+ // `struct Q` guesses `takeQ();`) or reads its words as separate arguments (O32 `takeW(gW)` as
417
+ // `takeW(*(s32 *)&gW)`), and either is C that compiles. This is
418
+ // not the abstention `declaredCallArgs` documents for an unsized typedef, which may still be one
419
+ // word: a parameter the declaration names a struct or union is known not to be.
420
+ //
421
+ // A VARIADIC DECLARATION REFUSES THE SAME GUESS over a fixed parameter that is not one word: it
422
+ // states a layout only where each fixed parameter is one word (`variadicFloor`), and otherwise its
423
+ // call is placed by the guess (`FnProto.variadicParams`), which splits a struct, a 64-bit value or
424
+ // a `double` into words. Its parameters are the project's fact, read off its own entry,
425
+ // which `declarationOf` reads past because it states no `params`.
426
+ const refuseUnplacedByValue = (callee: string, hidden: number): void => {
427
+ const own = known(prototypes, callee);
428
+ const fixed = variadicRefusal(own);
429
+ if (fixed !== undefined) {
430
+ const spelling = own!.variadicParams![fixed.param];
431
+ const byValue = paramClassOf(own!, own!.variadicParams!, fixed.param) === 'by-value';
432
+ fail(
433
+ `cannot lift '${name}': \`${callee}\` takes '${spelling}'${byValue ? ' by value' : ''} as its parameter ` +
434
+ `${fixed.param + 1}, and ${fixed.refused} — a variadic call is lifted only where each fixed parameter is one argument word`,
435
+ );
436
+ }
437
+ const p = declarationOf(callee);
438
+ const why = byValueRefusal(p, target, hidden);
439
+ if (why !== undefined) {
440
+ fail(
441
+ `cannot lift '${name}': \`${callee}\` takes '${(p!.params as readonly string[])[why.param]}' by value as its ` +
442
+ `parameter ${why.param + 1}, and ${why.refused} — a struct passed by value is lifted only where its argument words are known`,
443
+ );
444
+ }
445
+ };
446
+ // A FLOATING PARAMETER THE DECLARATION STATES BUT NOTHING LAYS OUT REFUSES; IT IS NOT GUESSED.
447
+ // Where the target has an FPU, `plan` refuses every floating parameter first (it travels in the
448
+ // FPU's registers). Where it has none — agbcc — a `double` is laid out in general words
449
+ // (`TargetDescription.doubleArgWords`) and a `float` or `long double` is not (`proto.ts`
450
+ // `declaredCallArgs`), so the call would fall to the arity guess, which is wrong both ways: a
451
+ // trailing argument passed through unchanged is dropped (`int g(int, float)`: `g(a + 1, b)` as
452
+ // `g(a0 + 1)`), and a register left over from the caller is counted (`float h(float)`: `h(v)` as
453
+ // `h(a1, a1)`). Neither compiles against the header. The unsized-typedef abstention does not
454
+ // apply: a floating spelling is known, and what is missing is a layout, not a width.
455
+ const refuseUnplacedFloat = (callee: string): void => {
456
+ const p = declarationOf(callee);
457
+ const params = p?.params;
458
+ if (target.fpu !== undefined || !Array.isArray(params)) {
459
+ return;
460
+ }
461
+ const at = params.findIndex((_, k) => paramClassOf(p!, params, k) === 'float');
462
+ if (at >= 0) {
463
+ fail(
464
+ `cannot lift '${name}': \`${callee}\` is declared to take '${params[at]}' as its parameter ${at + 1}, ` +
465
+ 'which this target passes in general argument words that are not laid out here — the arity guess ' +
466
+ 'in its place drops an argument passed through unchanged, or counts a leftover register as one',
467
+ );
468
+ }
469
+ };
470
+ const declaredCall = (callee: string): DeclaredCall | null => {
471
+ // Three tiers, narrowing: the project's own headers, then the compiler's runtime helpers,
472
+ // then the signatures the C standard fixes (proto.ts). A project that re-declares one of the
473
+ // last two is read at its own declaration: it may be building against it. On a runtime
474
+ // helper's name that disables the helper rather than redirecting it (`wideHelper`).
475
+ //
476
+ // THE TIER QUESTION IS WHETHER THE PROJECT DECLARED THE CALLEE, not whether the declaration
477
+ // could be sized. A header that spells `memcpy`'s third parameter through a project typedef
478
+ // has still re-declared `memcpy`, and reading the standard's signature past it would answer
479
+ // for a different function — which is the one reading this tier order exists to prevent.
480
+ //
481
+ // `Object.hasOwn` on all three tables: a callee named `toString` or `valueOf` would otherwise
482
+ // read a `Function` off `Object.prototype` as its prototype entry.
483
+ const own = known(prototypes, callee);
484
+ const returned = declaresAggregateReturn(own) ? structReturnOf(name, target, callee, own!, fail) : undefined;
485
+ const hidden = returned === undefined || returned === 'register' ? 0 : 1;
486
+ const declared:
487
+ { widths: readonly number[]; doubles: ReadonlySet<number>; byValue?: readonly ByValueArg[] } | undefined =
488
+ declaredCallArgs(declarationOf(callee), target) ?? byValueCallArgs(declarationOf(callee), target, hidden);
489
+ const params = declared?.widths;
490
+ if (params === undefined) {
491
+ refuseUnplacedByValue(callee, hidden);
492
+ refuseUnplacedFloat(callee);
493
+ }
494
+ if (params === undefined && returned !== undefined && returned !== 'register') {
495
+ // a guessed arity reads argument registers from the first, which holds the hidden pointer
496
+ fail(
497
+ `cannot lift '${name}': \`${callee}\` returns ${returned.type.kind === 'struct' ? returned.type.declared : typeToString(returned.type)} through a hidden ` +
498
+ `pointer in ${target.argRegs[0]}, and its parameters are not all sized, so which registers carry its arguments is not known`,
499
+ );
500
+ }
501
+ if (params === undefined) {
502
+ return null;
503
+ }
504
+ // THE STRUCT'S LOCAL NEEDS THE CALLEE PRINTED. A self-declared candidate defines the struct only
505
+ // beside the callee's printed prototype (`declare.ts`), so a declaration the printer cannot spell
506
+ // (`double *`, `size_t`, or a bare parameter count, which states no type) lifts to a candidate
507
+ // that does not compile. Asked of the printer's own predicate, so the two cannot disagree; a
508
+ // headers world, which would declare the callee itself, loses the lift with it.
509
+ const byValueParams = new Set((declared?.byValue ?? []).map((b) => b.param));
510
+ if (
511
+ returned !== undefined &&
512
+ returned !== 'register' &&
513
+ spellableProto(own, target, returned.type, byValueParams) === undefined
514
+ ) {
515
+ const via = `\`${callee}\` returns ${returned.type.kind === 'struct' ? returned.type.declared : typeToString(returned.type)} through a hidden pointer in ${target.argRegs[0]}`;
516
+ if (typeof own!.params === 'number') {
517
+ fail(
518
+ `cannot lift '${name}': ${via}, and its declaration states only a count of parameters, no type the lifted source can declare it with`,
519
+ );
520
+ }
521
+ fail(
522
+ `cannot lift '${name}': ${via}, and a parameter type of its declaration has no spelling the lifted source can declare it with`,
523
+ );
524
+ }
525
+ const widths = hidden === 0 ? params : [32, ...params];
526
+ const doubles = new Set([...declared!.doubles].map((i) => i + hidden));
527
+ const byValue = (declared!.byValue ?? []).map((b) => ({
528
+ ...b,
529
+ word: b.word + hidden,
530
+ }));
531
+ // a struct partly in the registers and partly on the stack (FUNCTION_ARG_PARTIAL_NREGS) is
532
+ // one value read from two places, which no lowering here builds
533
+ const past = byValue.find((b) => wordsOf(widths.slice(0, b.word), target) + b.words > target.argRegs.length);
534
+ if (past !== undefined) {
535
+ fail(
536
+ `cannot lift '${name}': \`${callee}\` takes ${structSpelling(past.type)} by value as its parameter ${past.param + 1}, ` +
537
+ 'and it does not fit in the argument registers — a struct passed on the stack is not modelled',
538
+ );
539
+ }
540
+ return {
541
+ widths,
542
+ doubles,
543
+ params: declared!.widths.length - byValue.reduce((n, b) => n + b.words - 1, 0),
544
+ ...(returned === undefined ? {} : { returned }),
545
+ ...(byValue.length === 0 ? {} : { byValue }),
546
+ };
547
+ };
548
+ // ONE CALL, IN THE ORDER ITS QUESTIONS REFUSE: a struct the lowering cannot receive and a float,
549
+ // then the runtime helper, whose signature is its compiler's, so no declaration is asked for one;
550
+ // the declaration's own refusals come next, then the return.
551
+ const plan = (callee: string): CallPlan => {
552
+ const own = Object.hasOwn(prototypes, callee) ? prototypes[callee] : undefined;
553
+ // One returned through memory is handed a hidden pointer in argument 0, with every argument one
554
+ // register up. One returned in registers takes its arguments where they are declared.
555
+ if (
556
+ !lowering.memoryReturn &&
557
+ declaresAggregateReturn(own) &&
558
+ returnsInMemory(returnedAggregate(own!), target) !== false
559
+ ) {
560
+ fail(
561
+ `cannot lift '${name}': '${callee}' is declared to return ${own?.returns ?? 'a struct or union'} by value — ` +
562
+ 'a struct returned through a hidden pointer, or one nothing here can size, is not modelled',
563
+ );
564
+ }
565
+ // A FLOAT CROSSES A CALL IN THE FPU'S REGISTERS where the target has one (`TargetDescription.fpu`),
566
+ // and a call here passes and reads general registers alone.
567
+ if (target.fpu !== undefined && own !== undefined) {
568
+ if (typeof own.returns === 'string' && isFloatingSpelling(own.returns)) {
569
+ fail(
570
+ `cannot lift '${name}': '${callee}' is declared to return ${own.returns}, which comes back in ` +
571
+ `${target.fpu.returnReg} — the floating-point registers a call passes and returns in are not modelled`,
572
+ );
573
+ }
574
+ const params = Array.isArray(own.params) ? own.params : [];
575
+ const at = params.findIndex(isFloatingSpelling);
576
+ if (at >= 0) {
577
+ fail(
578
+ `cannot lift '${name}': '${callee}' is declared to take ${params[at]} as its ` +
579
+ `parameter ${at + 1}, which travels in the FPU's registers — the floating-point registers a call ` +
580
+ 'passes and returns in are not modelled',
581
+ );
582
+ }
583
+ }
584
+ const helper = wideHelper(callee);
585
+ // WITHOUT PAIRS A 64-BIT HELPER IS AN UNDECLARED CALL. Its table signature passes a pair, which
586
+ // such a lowering cannot build; guessed, the call reaches `raise/widehelpers.ts`, which gaps it
587
+ // by name.
588
+ const wide = lowering.pairs ? helper : null;
589
+ const declared = helper ? null : declaredCall(callee);
590
+ if (!lowering.pairs && declared !== null) {
591
+ // A PARAMETER WIDER THAN A REGISTER TRAVELS IN A PAIR. A lowering that passes every argument
592
+ // register as its own value would honour the declaration by handing the callee one half.
593
+ const wideAt = declared.widths.findIndex((w) => w > 32);
594
+ if (wideAt >= 0) {
595
+ // `widths` holds argument words — a hidden struct-return pointer ahead of the declared
596
+ // list, and each word of a struct passed by value — and the text names the parameter
597
+ const hidden = declared.returned !== undefined && declared.returned !== 'register' ? 1 : 0;
598
+ const param =
599
+ wideAt -
600
+ hidden -
601
+ (declared.byValue ?? []).filter((b) => b.word < wideAt).reduce((n, b) => n + b.words - 1, 0);
602
+ fail(
603
+ `cannot lift '${name}': one half of a 64-bit value would be handed to '${callee}' — its parameter ` +
604
+ `${param + 1} is declared wider than a register, and this frontend passes each argument ` +
605
+ 'register as its own value rather than building the pair the ABI passes it in',
606
+ );
607
+ }
608
+ }
609
+ if (!lowering.stackArgs && declared !== null && wordsOf(declared.widths, target) > target.argRegs.length) {
610
+ fail(
611
+ `cannot lift '${name}': outgoing stack arguments not modelled — '${callee}' is declared with ` +
612
+ `${declared.widths.length} parameters and the argument registers carry ${target.argRegs.length}, so the rest ` +
613
+ `travel in its parameter area on the stack`,
614
+ );
615
+ }
616
+ const pair = helper !== null && !lowering.pairs ? false : returnsPair(callee);
617
+ // THE SAME RULE ON THE WAY BACK. The declaration reaches the candidate whether or not the
618
+ // lowering can act on it (`l3/symbol-refs.ts` prints `long long g(void);`), so reading the
619
+ // return register alone would lift `return g();` off one half under a declaration that makes
620
+ // it mean the whole value.
621
+ if (pair && !lowering.pairs) {
622
+ fail(
623
+ `cannot lift '${name}': '${callee}' would hand back one half of a 64-bit value — its return is ` +
624
+ 'declared wider than a register, and this frontend reads the return register as the whole ' +
625
+ 'value rather than building the pair the ABI hands back',
626
+ );
627
+ }
628
+ // a struct returned through memory is the call's value, and argument 0 is where it lands; one
629
+ // returned in registers is their bytes, whether or not anything states the call's arity
630
+ const returned = declared?.returned ?? (declared === null && !wide ? registerStructReturn(callee) : undefined);
631
+ // two answers to which registers hold the result, and to whether argument 0 is a hidden pointer
632
+ if (pair && returned !== undefined) {
633
+ fail(`cannot lift '${name}': \`${callee}\` is declared to return both a struct or union and a 64-bit value`);
634
+ }
635
+ // a runtime helper's return is its table's, whatever a project declares on its name
636
+ const declaredVoid = !isRuntimeHelperName(callee) && declaresVoidReturn(own);
637
+ const voided = lowering.voidReturn && declaredVoid;
638
+ const image = returned === 'register' && registerStructLocal(known(prototypes, callee), target) !== undefined;
639
+ // A FLOAT RETURNED WITHOUT AN FPU IS BITS IN THE RETURN REGISTER. The candidate prints the call
640
+ // against the declaration, so `v = f()` into anything but a float converts the value (agbcc:
641
+ // `bl f; bl __fixsfsi`), where the bytes moved the bits. The call names the type, and
642
+ // `raise/floathelpers.ts` reads the result as that float or refuses. A target with an FPU
643
+ // refused the call above; a runtime helper's return is its table's.
644
+ const returnSpelling = own?.returns ?? declarationOf(callee)?.returns;
645
+ const floatReturn =
646
+ target.fpu === undefined && !isRuntimeHelperName(callee) && !voided && typeof returnSpelling === 'string'
647
+ ? floatingSpelling(returnSpelling)
648
+ : undefined;
649
+ return {
650
+ ...(floatReturn === undefined ? {} : { floatReturn }),
651
+ widths: wide?.params ?? declared?.widths ?? null,
652
+ doubles: declared?.doubles ?? new Set(),
653
+ ...(declared?.byValue === undefined ? {} : { byValue: declared.byValue }),
654
+ returns: voided
655
+ ? { kind: 'void' }
656
+ : pair
657
+ ? { kind: 'pair' }
658
+ : returned === undefined
659
+ ? { kind: 'word' }
660
+ : returned === 'register'
661
+ ? { kind: 'register-struct', image }
662
+ : { kind: 'memory-struct', type: returned.type },
663
+ // a struct leaves the return register holding nothing the caller may read as a value, save
664
+ // the image of one a local can be read off, and a void callee leaves it holding whatever the
665
+ // callee did
666
+ clobbers: pair
667
+ ? pairReturnClobbers
668
+ : (returned === undefined && !voided) || image
669
+ ? callClobbers
670
+ : [...callClobbers, target.returnReg],
671
+ declaredVoid,
672
+ };
673
+ };
674
+ // A VARIADIC DECLARATION WHOSE FIXED PARAMETERS ARE EACH ONE WORD STATES ITS FIRST ARGUMENTS:
675
+ // they are the first that many argument words, exactly, so they are read as stated words and only
676
+ // what the `...` takes is guessed, above them (`fallbackArgc`'s and the trim's `floor`). Read the
677
+ // way a call through argument register rN reads the registers below N (`lower`): one this
678
+ // function never wrote is its own argument passed on, and one a call destroyed names nothing and
679
+ // refuses (`SsaBuilder.finish`). So `int logv(const char *, int, ...)` lifts `logv(s + 1, c)` as
680
+ // `logv(a0 + 1, a1)`, where a guess from r0 sees r1 untouched and refuses the gap. A fixed list
681
+ // holding an UNSIZED spelling states no such words — the one may be two — and gets no floor; a
682
+ // fixed parameter known not to be one word refuses (`refuseUnplacedByValue`, in `declaredCall`).
683
+ const variadicFloor = (callee: string | undefined): number => {
684
+ const own = callee === undefined ? undefined : known(prototypes, callee);
685
+ const fixed = own?.params === undefined ? own?.variadicParams : undefined;
686
+ return fixed !== undefined && fixed.every((_, k) => paramClassOf(own!, fixed, k) === 'word') ? fixed.length : 0;
687
+ };
688
+ // A GUESS SHORTER THAN THE DECLARATION REFUSES. A declaration the lowering cannot lay out (an
689
+ // UNSIZED spelling: `proto.ts` `declaredCallArgs`), or a variadic one whose fixed parameters
690
+ // include one, still states how many parameters it has — fixed ones, for a variadic — and every
691
+ // one of them is at least one argument word. A guess that counts fewer registers has dropped
692
+ // arguments: the last one passed through unchanged leaves its register with no definition, so
693
+ // `void h(int, Foo)` with `Foo` unresolved lifts `h(n + 1, v)` as `h(a0 + 1)`. The count bounds
694
+ // the guess from below only — an unsized parameter may be two words, or a struct of several. A
695
+ // variadic call with a `variadicFloor` never reaches the bound: its guess starts there.
696
+ //
697
+ // KNOWN GAP: this bounds the count `fallbackArgc` takes, not what `SsaBuilder.finish` later
698
+ // retracts from it (a register a call between the setup and this one destroyed).
699
+ const refuseShortOfDeclared = (callee: string | undefined, argc: number): void => {
700
+ if (callee === undefined) {
701
+ return;
702
+ }
703
+ const p = declarationOf(callee);
704
+ const own = known(prototypes, callee);
705
+ const stated = Array.isArray(p?.params)
706
+ ? { n: p.params.length, what: 'parameters' }
707
+ : own?.variadicParams !== undefined && own.params === undefined
708
+ ? { n: own.variadicParams.length, what: 'fixed parameters before its `...`' }
709
+ : undefined;
710
+ if (stated !== undefined && argc < stated.n) {
711
+ fail(
712
+ `cannot lift '${name}': \`${callee}\` is declared with ${stated.n} ${stated.what}, which no layout here ` +
713
+ `places, and the arity guess in its place finds ${argc} argument register${argc === 1 ? '' : 's'} set up — ` +
714
+ 'an argument passed through unchanged leaves its register with nothing the guess can see',
715
+ );
716
+ }
717
+ };
718
+ // A GAP IN A GUESSED ARITY IS DECIDED WHERE THE REGISTER ABOVE IT IS SURELY AN ARGUMENT
719
+ // (`fallbackArgc`'s `decide`). The gap refuses because an argument register left at its incoming
720
+ // value and one the call does not pass look the same — but only while nothing says the call takes
721
+ // an argument above it. A register the call's own block wrote with a value that no memory read
722
+ // produced and that nothing but the call reads says exactly that: an optimising compiler deletes a
723
+ // dead computation, so the value exists only to be passed. And where argument words fill in order,
724
+ // every word below an argument is an argument too, so the gap's registers are passed on unchanged.
725
+ // Measured on 2026-10-18 at every toolchain's canonical flags: `logv(s, 1)` sets r1/a1/r4 alone,
726
+ // and at -O0 no gap arises at all (every argument is reloaded from its home first).
727
+ //
728
+ // EACH CLAUSE IS A MEASURED COUNTEREXAMPLE, so each refuses:
729
+ // - the value must be the call's own block's. Where a discarded read may land in an argument
730
+ // register (`compilerBehaviors.discardedReadMayFillArgRegister`), it must also be something no
731
+ // memory read produced: agbcc puts a DISCARDED volatile read in the lowest free argument
732
+ // register (`*(volatile int *)K; g(s)` is `ldr r1,[r1]; bl g`, at -O0 too), the passgap shape
733
+ // exactly. gcc2.7.2kmc, gcc2.7.2 and the three mwcc builds do the same once their usual
734
+ // discard register (v0/v1, r0) is busy: `int a = ga * 3; *(volatile int *)&gx; ...; logv(s)`
735
+ // is `lw a1,0(v0); ... jal logv` on KMC and `lwz r4,gx` on mwcc, the bytes of the twin that
736
+ // passes the read. Only ido7.1 discards into $zero every time, so only there a load only the
737
+ // call reads is an argument: `logv(s, gx)` lifts. Nothing else may read the value either
738
+ // (`SsaBuilder.requireSoleReader`, asked of the finished function): `gx = 1; logv(s)` and
739
+ // `gx = 1; logv(s, 1)` are one agbcc object, the stored constant left in r1.
740
+ // - the value must be no instruction's side effect (`SsaBuilder.noteIncidentalWrite`): a base
741
+ // writeback is left in its register whether anything wants it or not. agbcc copies a struct of
742
+ // 3+ words as `ldmia r3!, {r4, r5, r6}; stmia r2!, {r4, r5, r6}` and leaves both writebacks
743
+ // dead, so `*p = *q; return logv(s, n) + 1;` would lift `logv(a0, a1, a2 + 3, a3 + 3)`.
744
+ // - where a caller forwards its own destination as a callee's hidden pointer
745
+ // (`compilerBehaviors.forwardsStructReturnDestination`, ido7.1), a gap at the first argument
746
+ // register is decided only where the call's result is read (`SsaBuilder.requireReader`,
747
+ // owed by `lower`), by anything but a `ret`. `int b3(struct S *p){ *p = sg(1); return 0; }` is
748
+ // `jal sg; li a1,1` with a0 untouched, and would lift `sg(a0, 1)` even with `int b3(struct S *)`
749
+ // declared: the hidden pointer stated as a C argument, and the `*p =` store dropped. IDO reads
750
+ // no forwarded struct return's result register (five shapes, at the target.ts field), but a
751
+ // `ret` reads it in a function ending in the call, `void b7(struct S *p){ *p = sg(1); }`.
752
+ // - each gap register must hold the function's own incoming value on every path
753
+ // (`holdsEntryValue`): one a call destroyed is the trim's, and holds nothing.
754
+ // - where the ABI can leave a register empty below a filled one (target.ts `argRegisterHoles`)
755
+ // the gap may be that hole. An argument aligned to 8 skips an odd word (O32 a1, PowerPC
756
+ // r4/r6/r8), and the hole is ruled out only where that argument's second word holds nothing:
757
+ // no value reaches it and the own declaration fills fewer words. A leading float shadows the
758
+ // first general words on O32. It is ruled out only where the own declaration surely puts an
759
+ // integer word in each gap register. No register value can rule it out: the MIPS frontend
760
+ // refuses every instruction naming the FPU's file in a function that calls
761
+ // (frontend/mips.ts), so a float here is one passed on untouched in $f12, which nothing
762
+ // reads. A gap that starts at a1 under an a0 that only a read set up (`gx = a; gp(d, 5)`) is
763
+ // the 8-byte skip's case and refuses through it: a1 is odd, so it needs the own declaration's
764
+ // word bound, and `ownLeadingWordParamsOn` stops at the double.
765
+ // - where the own declaration binds the body, each gap register must surely hold one of its
766
+ // integer word parameters (`ownLeadingWordParamsOn`): no hidden struct pointer, no float.
767
+ // - at flags that keep a read nothing uses (`compilerBehaviors.keepsUnusedReads`, the -O0
768
+ // levels) the premise is suspended, as frontend/ppc.ts `computedPairHalf` suspends it. mwcc -O0
769
+ // keeps a dead `li r31,5`. The gap was measured never to arise there (mwcc -O0,p on all three
770
+ // builds, IDO -O1 and agbcc -O0 reload every argument from its home first), so this costs no
771
+ // lift.
772
+ // What the decision leaves open above the proven register is the call's own check
773
+ // (`decidedCallRefusal`). The proven register's value is returned, for the sole-reader
774
+ // obligation the call then takes on.
775
+ const decideGap = (
776
+ ssa: SsaBuilder,
777
+ bi: number,
778
+ gap: readonly string[],
779
+ set: string,
780
+ accept: (v: Value) => boolean,
781
+ ): Value | undefined => {
782
+ const regs = target.argRegs;
783
+ if (target.compilerBehaviors.keepsUnusedReads === true || !ssa.writtenSinceCall(set, bi)) {
784
+ return undefined;
785
+ }
786
+ const v = ssa.readVar(set, bi);
787
+ const def = ssa.irBlocks[bi].ops.find((op) => op.results.includes(v));
788
+ const sig = def === undefined ? undefined : (OPCODES[def.opcode as keyof typeof OPCODES] as OpSig | undefined);
789
+ const readsRefuse = target.compilerBehaviors.discardedReadMayFillArgRegister !== false;
790
+ if (
791
+ !accept(v) ||
792
+ ssa.isIncidentalWrite(v) ||
793
+ sig === undefined ||
794
+ sig.effects ||
795
+ (sig.reads && readsRefuse) ||
796
+ sig.terminator ||
797
+ def!.opcode === 'undef'
798
+ ) {
799
+ return undefined;
800
+ }
801
+ if (gap.some((g) => ssa.writtenSinceCall(g, bi) || !ssa.holdsEntryValue(g, bi))) {
802
+ return undefined;
803
+ }
804
+ const own = ownDeclaration();
805
+ const surelyOwn = own === undefined ? undefined : ownLeadingWordParamsOn(name, own.proto, target);
806
+ if (surelyOwn !== undefined && gap.some((g) => regs.indexOf(g) >= surelyOwn)) {
807
+ return undefined;
808
+ }
809
+ const holes = argRegisterHoles(target);
810
+ if (holes.leadingFloatShadow && regs.indexOf(gap[0]) === 0 && own?.proves !== true) {
811
+ return undefined;
812
+ }
813
+ const ownWords = own?.proves !== true ? undefined : ownArgumentWordsOn(name, own.proto, target);
814
+ for (const g of gap.filter((r) => holes.skippable.has(r))) {
815
+ const j = regs.indexOf(g);
816
+ const second = regs[j + 2];
817
+ if (
818
+ regs[j + 1] !== set ||
819
+ second === undefined ||
820
+ ssa.hasReachingDef(second, bi, accept) ||
821
+ ownWords === undefined ||
822
+ j + 2 < ownWords
823
+ ) {
824
+ return undefined;
825
+ }
826
+ }
827
+ return v;
828
+ };
829
+ // WHAT A DECIDED GAP LEAVES OPEN ABOVE THE COUNT: whether the call takes more than the `argc`
830
+ // words counted. The decision proves that the gap registers are this function's own arguments
831
+ // passed on. Past the count, the guess reads an unwritten register as no argument, as it does at
832
+ // every gapless call, and three things say otherwise. Each refuses (returns true):
833
+ // - an own parameter that may sit past the count (`ownArgumentWordsOn`, an upper bound). It is
834
+ // just as likely passed on as the gap below it was. On gcc2.7.2kmc and ido7.1, `int ll6(int a,
835
+ // int b, int c, int d){ return g(a, 2, c, d, 5) + 1; }` leaves c and d in a2/a3 and would lift
836
+ // `g(a0, 2)`.
837
+ // - a word of the outgoing area that reaches the call (`guess.refuse`, asked as if the count
838
+ // filled every argument register). It is a fifth argument, so every register below it is an
839
+ // argument too: the same `ll6`'s `sw v0,16(sp)`.
840
+ // - a floating argument this ABI passes apart from the general words (`fpu.slots: 'separate'`,
841
+ // PowerPC), which no guess counts. `int fl2(int b, float x){ return g(b, x, 3) + 1; }` leaves
842
+ // x in f1 on all three mwcc builds and would lift `g(a0, 3)`. A float in f1 that nothing reads is
843
+ // invisible, so on such an ABI only an own declaration whose parameters are all integers rules
844
+ // it out. Without a declaration this refuses. A value reaching an FPU argument register refuses
845
+ // on every ABI.
846
+ const decidedCallRefusal = (ssa: SsaBuilder, bi: number, argc: number, site: CallSite): boolean => {
847
+ const own = ownDeclaration();
848
+ if (own !== undefined) {
849
+ const words = ownArgumentWordsOn(name, own.proto, target);
850
+ if (words === undefined || words > argc) {
851
+ return true;
852
+ }
853
+ }
854
+ const fpu = target.fpu;
855
+ if (fpu !== undefined) {
856
+ if (fpu.argRegs.some((r) => ssa.hasReachingDef(r, bi, (v) => !site.placeholders?.has(v)))) {
857
+ return true;
858
+ }
859
+ const floats = own === undefined ? undefined : ownDeclaresFloatingParam(name, own.proto);
860
+ if (floats === true || (fpu.slots === 'separate' && !(own?.proves === true && floats === false))) {
861
+ return true;
862
+ }
863
+ }
864
+ try {
865
+ site.guess.refuse?.(target.argRegs.length);
866
+ } catch (e) {
867
+ if (e instanceof FrontendUnsupportedError) {
868
+ return true;
869
+ }
870
+ throw e;
871
+ }
872
+ return false;
873
+ };
874
+ // THE CALL A PLAN LOWERS TO, in one order on every ISA: the arity, each argument word, the
875
+ // `call` op, the guessed arity's record, the result and the clobbers.
876
+ const lower = (site: CallSite): void => {
877
+ const { ssa, bi, pairs, guess, placeholders } = site;
878
+ // whether `v`, reaching argument register `r`, may be an argument the caller set up
879
+ const mayPass = (v: Value, r: string) => !placeholders?.has(v) && site.parked?.(v, r) !== true;
880
+ const holes = argRegisterHoles(target);
881
+ if (lowering.pairs !== (pairs !== undefined) || lowering.stackArgs !== (site.stackWord !== undefined)) {
882
+ throw new Error(`target '${target.id}': a call site's pairs and stack words must be what its lowering states`);
883
+ }
884
+ const indirect = typeof site.callee === 'string' ? undefined : site.callee;
885
+ const callee = typeof site.callee === 'string' ? site.callee : undefined;
886
+ // A CALL THROUGH ARGUMENT REGISTER rN PASSES r0..r(N-1) where the lowering states it
887
+ // (`CallLowering.argRegisterBoundsArity`): N bounds the arity, and reading every register below
888
+ // it passes what the machine passes. An untouched one is this function's own argument passed
889
+ // on; one a call destroyed names nothing, and its read refuses (`SsaBuilder.finish`). Through
890
+ // any other register, or on a lowering that does not state it, the arity is guessed.
891
+ //
892
+ // KNOWN GAP: N is only a bound. agbcc's address lands above r(argc) when an argument register
893
+ // still holds a live temp, and a pointer that arrived as an argument stays where it arrived
894
+ // (`void f(int x, void (*g)(void)) { g(); }` calls through r1, the bytes `g(x)` compiles to), so
895
+ // the registers in between read as arguments the source never passed. Only the pointer's
896
+ // declared type decides the arity, and nothing reads one.
897
+ const bound =
898
+ indirect === undefined || !lowering.argRegisterBoundsArity ? -1 : target.argRegs.indexOf(indirect.reg);
899
+ const p: CallPlan =
900
+ indirect === undefined
901
+ ? plan(callee!)
902
+ : {
903
+ widths: bound < 0 ? null : Array.from({ length: bound }, () => 32),
904
+ doubles: new Set(),
905
+ returns: { kind: 'word' },
906
+ clobbers: callClobbers,
907
+ declaredVoid: false,
908
+ };
909
+ // ONE LIST OF PARAMETER WIDTHS, FROM WHICHEVER SOURCE STATES THEM — the compiler's own
910
+ // runtime table or the project's headers. Both answer the same question, so the walk that
911
+ // reads argument registers off the answer is written once; two walks would be two chances
912
+ // for the pairing rule and the arity rule to disagree.
913
+ //
914
+ // A DECLARATION HOLDING A SPELLING NOTHING CAN SIZE STATES NO LAYOUT (`proto.ts`
915
+ // `declaredCallArgs`), so `widths` is null for it and this falls to the guess below —
916
+ // the same answer the callee would get with no prototype at all.
917
+ const { widths, returns } = p;
918
+ // the leading arguments a variadic declaration states, where the rest is guessed
919
+ const floor = widths === null ? variadicFloor(callee) : 0;
920
+ // the gaps `decideGap` decided: the value that proved each, and the gap's own refusal, owed if
921
+ // that proof fails once the function is lifted
922
+ const decided: Array<{ value: Value; refuse: () => void }> = [];
923
+ // the refusals of the decided gaps at the first argument register that the call's result must
924
+ // be read to keep (`compilerBehaviors.forwardsStructReturnDestination`, at `decideGap`)
925
+ const resultOwed: Array<() => void> = [];
926
+ // how many argument words the decided gaps put beyond doubt
927
+ let decidedWords = 0;
928
+ const argc =
929
+ widths === null
930
+ ? fallbackArgc(ssa, target.argRegs, bi, {
931
+ accept: mayPass,
932
+ gap: { name, at: guess.at, fail },
933
+ floor,
934
+ parked: (r) =>
935
+ !holes.skippable.has(r) &&
936
+ !(holes.leadingFloatShadow && r === target.argRegs[0]) &&
937
+ (ssa.holdsValue(r, bi, (v) => site.parked?.(v, r) === true) ||
938
+ // a call in the case destroyed it: the hole is that call's, and refuses only where a
939
+ // register above holds a value written since (`fallbackArgc`'s `parked`)
940
+ (ssa.hasReachingDef(r, bi, (v) => site.parked?.(v, r) === true) &&
941
+ !target.argRegs
942
+ .slice(target.argRegs.indexOf(r) + 1)
943
+ .some((a) => ssa.holdsValue(a, bi, (v) => mayPass(v, a))))),
944
+ decide: (gap, set, refuse) => {
945
+ const value = decideGap(ssa, bi, gap, set, (v) => mayPass(v, set));
946
+ if (value === undefined) {
947
+ return false;
948
+ }
949
+ decided.push({ value, refuse });
950
+ if (gap[0] === target.argRegs[0] && target.compilerBehaviors.forwardsStructReturnDestination !== false) {
951
+ resultOwed.push(refuse);
952
+ }
953
+ decidedWords = target.argRegs.indexOf(set) + 1;
954
+ return true;
955
+ },
956
+ entry: (reg) =>
957
+ !ssa.writtenSinceCall(reg, bi) &&
958
+ ssa.holdsValue(
959
+ reg,
960
+ bi,
961
+ (v) => mayPass(v, reg) && ssa.paramReg.get(v) === reg && ssa.irBlocks[0].params.includes(v),
962
+ ),
963
+ })
964
+ : wordsOf(widths, target);
965
+ if (widths === null) {
966
+ const index = target.argRegs
967
+ .slice(floor, argc)
968
+ .find((r) => ssa.hasReachingDef(r, bi, (v) => site.scaledIndex?.(v, r) === true));
969
+ if (index !== undefined) {
970
+ fail(
971
+ `cannot lift '${name}': the call ${typeof guess.at === 'number' ? `at 0x${guess.at.toString(16)}` : guess.at} ` +
972
+ `has no prototype, and ${index}, which the arity guessed without one counts, still holds the index the ` +
973
+ "jump table's dispatch scaled — a call handed it and one that is not compile to the same bytes, so the " +
974
+ 'argument count is not decidable',
975
+ );
976
+ }
977
+ // the lowest argument register past the stated ones that a marker or the scaled index a
978
+ // dispatch left there may reach, parked or not (a PowerPC pair's r4 never parks): where the
979
+ // pre-dispatch value stood
980
+ const markedAt = target.argRegs.findIndex(
981
+ (r, k) =>
982
+ k >= floor &&
983
+ ssa.hasReachingDef(r, bi, (v) => site.parked?.(v, r) === true || site.scaledIndex?.(v, r) === true),
984
+ );
985
+ parkedInArgs ||= markedAt >= 0;
986
+ if (passThrough === undefined) {
987
+ const call = typeof guess.at === 'number' ? `at 0x${guess.at.toString(16)}` : guess.at;
988
+ const last = target.argRegs[argc - 1];
989
+ // the blocks since the dispatch on the way to the call: a path stops at a `switch_br`'s block
990
+ const since = new Set<number>([bi]);
991
+ const dispatch = (at: number) => ssa.irBlocks[at].ops.some((op) => op.opcode === 'switch_br');
992
+ const lastWritten = argc <= floor || ssa.writtenOnEveryPath(last, bi, dispatch, (at) => since.add(at));
993
+ // what an effect those blocks made so far reads
994
+ const effectReads = new Set(
995
+ [...since].flatMap((at) =>
996
+ ssa.irBlocks[at].ops.flatMap((op) =>
997
+ (OPCODES[op.opcode as keyof typeof OPCODES] as OpSig | undefined)?.effects ? op.operands : [],
998
+ ),
999
+ ),
1000
+ );
1001
+ const stored = target.argRegs
1002
+ .slice(floor, argc)
1003
+ .find((r) => ssa.holdsValue(r, bi, (v) => mayPass(v, r) && effectReads.has(v)));
1004
+ const leftIncoming =
1005
+ markedAt < 0 ? undefined : target.argRegs.slice(argc, markedAt).find((r) => ssa.holdsEntryValue(r, bi));
1006
+ if (!lastWritten) {
1007
+ passThrough = {
1008
+ call,
1009
+ reg: last,
1010
+ why: 'the last register the count takes, holds a value some path since the dispatch did not write',
1011
+ };
1012
+ } else if (stored !== undefined) {
1013
+ passThrough = { call, reg: stored, why: 'which the count takes, holds a value an effect of its block uses' };
1014
+ } else if (leftIncoming !== undefined) {
1015
+ passThrough = {
1016
+ call,
1017
+ reg: leftIncoming,
1018
+ why: "which the count leaves out below a dispatch's register, holds its incoming value",
1019
+ };
1020
+ }
1021
+ }
1022
+ guess.refuse?.(argc);
1023
+ refuseShortOfDeclared(callee, argc);
1024
+ if (decided.length > 0 && decidedCallRefusal(ssa, bi, argc, site)) {
1025
+ decided[0].refuse();
1026
+ }
1027
+ }
1028
+ // WHAT EVERY ARGUMENT REGISTER HOLDS HERE, where no declaration stated the arity: a value the
1029
+ // count leaves out (or a later trim cuts) may be an argument the source passed, so the dead-read
1030
+ // pin may not call it unused. Asked through `holdsValue`, which reads no register.
1031
+ if (widths === null || bound >= 0) {
1032
+ for (const r of target.argRegs) {
1033
+ ssa.holdsValue(r, bi, (v) => {
1034
+ heldAtUndeclaredCall.add(v);
1035
+ return false; // keep walking: every path's value, not the first
1036
+ });
1037
+ }
1038
+ }
1039
+ // WHAT THE ARITY LEFT OUT, where no declaration stated it: an argument register past a guessed
1040
+ // count, or past the bound a call through an argument register sets, that a value reaches.
1041
+ // The gap refusal has already fired on any such register past a guess; past the bound, the
1042
+ // register leaves the call silently, and the frontend says whether it knows that one to be an
1043
+ // argument all the same.
1044
+ if (site.leftOut !== undefined && (widths === null || bound >= 0)) {
1045
+ site.leftOut(
1046
+ target.argRegs
1047
+ .slice(argc)
1048
+ .filter((r) => r !== indirect?.reg && ssa.hasReachingDef(r, bi, (v) => mayPass(v, r))),
1049
+ target.argRegs.slice(0, argc),
1050
+ );
1051
+ }
1052
+ // ARGUMENT WORD `k`: a register, or past the registers the word the frontend's ABI places
1053
+ // there. A guess never exceeds `argRegs.length`, so a stack word can only come from a stated
1054
+ // width.
1055
+ const word = (k: number): Value => (k < target.argRegs.length ? site.read(target.argRegs[k]) : site.stackWord!(k));
1056
+ // A GUESSED arity reads argument registers to ASK whether the caller set them up, and
1057
+ // `finish()` answers by dropping the ones a call has been through; a STATED width asserts
1058
+ // they exist, so a destroyed register read for it is a wrong value nothing retracts. A
1059
+ // guess is a list of single words by construction — `fallbackArgc` counts registers.
1060
+ const guessed = (k: number): Value => {
1061
+ const r = target.argRegs[k];
1062
+ const v = ssa.readGuessedArg(r, bi);
1063
+ return placeholders ? placeholders.guardRead(name, r, v) : v;
1064
+ };
1065
+ const args: Value[] = [];
1066
+ let k = 0;
1067
+ for (const w of widths ?? Array.from({ length: argc }, () => 32)) {
1068
+ // A 64-BIT PARAMETER IS TWO ARGUMENT WORDS AND ONE VALUE, so the pair is built here
1069
+ // rather than recovered from two 32-bit arguments later — `contracts.ts` would fire on
1070
+ // the second reading anyway, since the structurer materialises an effectful call once
1071
+ // per result. Its words are wherever the frontend's ABI placed them.
1072
+ args.push(w > 32 ? pairs!.fuse(word(k), word(k + 1)) : widths === null && k >= floor ? guessed(k) : word(k));
1073
+ k += w > 32 ? 2 : 1;
1074
+ }
1075
+ // A 64-BIT VALUE MAY NOT LEAVE AS A WORD WHERE NOTHING SAYS HOW WIDE THE PARAMETER IS,
1076
+ // and this is the refusal — GUARDED ON `widths === null`, which is the whole of the
1077
+ // condition. A guessed arity counts argument registers, so a caller that computes a pair
1078
+ // and a caller that computes two words set up the same two registers: passing the low
1079
+ // half alone invents a truncation the asm never wrote, passing both halves as two words
1080
+ // invents an argument. Both recompile to the very call being lifted, so the differ
1081
+ // scores them exactly as it scores the right answer and nothing downstream can referee
1082
+ // either.
1083
+ //
1084
+ // A STATED WIDTH IS THE DISAMBIGUATION AND IT IS ONE WHETHER IT SAYS 64 OR 32. A stated
1085
+ // 64 built the pair in the walk above. A stated 32 says the callee takes a word, so
1086
+ // handing it a half is the narrowing the header authorises — `void sink(int)` against
1087
+ // `sink((int)(a * b))` — and refusing it here would contradict a fact the user supplied.
1088
+ //
1089
+ // SO THE MESSAGE IS ABOUT THE WIDTH AND NOT ABOUT A PROTOTYPE, because a supplied one
1090
+ // reaches here too: a typed list holding a spelling `declaredWidth` cannot size states
1091
+ // no layout at all (`proto.ts` `declaredCallArgs`), and a bare COUNT states argument
1092
+ // registers rather than widths. Both leave `widths` null with a `--proto` on the command
1093
+ // line, and blaming an absent prototype would be false about its own input.
1094
+ if (widths === null && pairs !== undefined) {
1095
+ // no prototype keys a call through a register, so the hint names one only for a named callee
1096
+ const theCall = indirect === undefined ? `the call to '${callee}'` : `the call through ${indirect.reg}`;
1097
+ const itsParams = indirect === undefined ? `'${callee}'s parameters` : "the function pointer's parameters";
1098
+ // a stated fixed word (`variadicFloor`) takes a half as the narrowing its declaration authorises
1099
+ for (const [j, v] of args.entries()) {
1100
+ const half = j < floor ? undefined : pairs.halfOf.get(v);
1101
+ // A DOUBLE THE RUNTIME RETURNED IS NO long long, so the hint below would send the
1102
+ // reader to the wrong declaration: a double leaves a soft-float helper only into
1103
+ // another one, the return, or a parameter declared `double` (`raise/floathelpers.ts`),
1104
+ // and a pair built here for a callee declared to take a `long long` is refused there.
1105
+ const producer = half && pairCallee.get(half.whole);
1106
+ if (half && producer && isFloatHelperName(producer)) {
1107
+ fail(
1108
+ `cannot lift '${name}': argument ${j + 1} of ${theCall} is the ` +
1109
+ `${half.half === 'lo' ? 'low' : 'high'} half of a 64-bit value, the double '${producer}' ` +
1110
+ `returned, and nothing states how wide ${itsParams} are. A double is ` +
1111
+ "modelled into the runtime's arithmetic helpers, the return and a parameter a prototype " +
1112
+ 'declares `double`, so a runtime compare or conversion declines' +
1113
+ (indirect === undefined
1114
+ ? '; a callee that takes a double, or fewer arguments than its registers suggest, lifts ' +
1115
+ `once a prototype states them (\`{"${callee}": {"params": [...]}}\`)`
1116
+ : ''),
1117
+ );
1118
+ }
1119
+ if (half) {
1120
+ fail(
1121
+ `cannot lift '${name}': argument ${j + 1} of ${theCall} is the ` +
1122
+ `${half.half === 'lo' ? 'low' : 'high'} half of a 64-bit value, and nothing states ` +
1123
+ `how wide ${itsParams} are, so a pair cannot be told from two ordinary arguments` +
1124
+ (indirect === undefined
1125
+ ? `. A typed prototype states it (\`{"${callee}": {"params": ` +
1126
+ '["long long", …]}}`); a count, or a list holding a spelling asmlift cannot size, does not'
1127
+ : ' — not modelled'),
1128
+ );
1129
+ }
1130
+ }
1131
+ }
1132
+ const sret = returns.kind === 'memory-struct' ? returns.type : undefined;
1133
+ const res = mkValue(sret ?? T.unk(returns.kind === 'pair' ? 64 : 32));
1134
+ // A DECLARED `double` IS TWO WORDS THAT ARE NOT A `long long`: its first word holds the
1135
+ // sign and exponent (`TargetDescription.doubleArgWords`), so the pair read as an integer
1136
+ // spells a different number — `g(1.5)` as `g(1073217536, 0)`. The call names the operands
1137
+ // that are doubles, and `raise/floathelpers.ts` retypes each or refuses it.
1138
+ const doubles = p.doubles.size ? [...p.doubles] : undefined;
1139
+ // A STRUCT PASSED BY VALUE IS ITS WORDS HERE, one operand each, and the call names them as
1140
+ // (first operand, words) pairs: `foldByValueArgs` folds them into the one object passed whole
1141
+ // once the function is lifted, or refuses.
1142
+ const byValue = p.byValue?.flatMap((b) => [b.word, b.words]);
1143
+ const call = mkOp('call', {
1144
+ operands: indirect === undefined ? args : [...args, indirect.address],
1145
+ results: [res],
1146
+ attrs: {
1147
+ ...(callee !== undefined ? { target: callee } : { indirect: true }),
1148
+ ...(sret === undefined ? {} : { sret: true }),
1149
+ ...(returns.kind === 'register-struct' && returns.image ? { [REGISTER_STRUCT]: true } : {}),
1150
+ ...(doubles === undefined ? {} : { doubles }),
1151
+ ...(p.floatReturn === undefined ? {} : { floatReturn: p.floatReturn }),
1152
+ ...(byValue === undefined ? {} : { byValueWords: byValue }),
1153
+ },
1154
+ });
1155
+ ssa.irBlocks[bi].ops.push(call);
1156
+ for (const d of decided) {
1157
+ ssa.requireSoleReader(d.value, call, d.refuse);
1158
+ }
1159
+ for (const refuse of resultOwed) {
1160
+ if (returns.kind === 'word') {
1161
+ ssa.requireReader(res, refuse);
1162
+ } else {
1163
+ refuse();
1164
+ }
1165
+ }
1166
+ // A GUESSED arity is revisited in `finish()`: only once the whole function is lifted is it
1167
+ // known whether every path to here passes through another call, which would have clobbered
1168
+ // the argument registers this guess just read.
1169
+ //
1170
+ // A CALL THROUGH A REGISTER IS PASSED AN EARLIER CALLEE'S RESULT its guess reads, which the trim
1171
+ // drops from a named call (`trimClobberedCallArgs`): no declaration checks a call through a cast
1172
+ // to an unprototyped type, so a dropped argument would never be refused, and `p(g())` would lift
1173
+ // as `g(); p();`, which compiles to the same bytes. Kept, it lifts as `p(g())`, which passes what
1174
+ // the machine passes — unless `g` is declared void, and its result names nothing.
1175
+ //
1176
+ // …AND WHAT THE TRIM CUTS IS LEFT OUT TOO, so the frontend is asked the `leftOut` question of it
1177
+ // as well: a guess of four the check above passed can still lose a frame address in r2 when
1178
+ // the trim cuts it back to the run below an unfresh r1 (`v = f64(); g(v, &s)` with neither
1179
+ // declared, which otherwise lifts as `g(v)`).
1180
+ if (widths === null) {
1181
+ const leftOut = site.leftOut;
1182
+ ssa.recordGuessedCall(
1183
+ call,
1184
+ bi,
1185
+ target,
1186
+ indirect === undefined ? undefined : (r) => !voidResults.has(r),
1187
+ leftOut === undefined
1188
+ ? undefined
1189
+ : (cut, kept) =>
1190
+ leftOut(
1191
+ cut.filter((r) => r !== indirect?.reg),
1192
+ kept,
1193
+ ),
1194
+ site.withholdsSetupArgs,
1195
+ site.asksEveryCut,
1196
+ // a decided gap puts its words beyond doubt, so neither reading cuts below them
1197
+ Math.max(floor, decidedWords),
1198
+ );
1199
+ }
1200
+ if (p.declaredVoid) {
1201
+ voidResults.add(res);
1202
+ }
1203
+ if (returns.kind === 'pair') {
1204
+ // A PAIR RETURN IS ONE VALUE, SPLIT. The callee defines BOTH registers, so both are
1205
+ // named here and neither is in the clobber set — which is the acceptance arm of the
1206
+ // very rule whose refusal arm `frontend/ssa.ts` applies to every other register.
1207
+ site.write(target.returnReg, pairs!.project(res, 'lo'));
1208
+ site.write(target.argRegs[1], pairs!.project(res, 'hi'));
1209
+ if (callee !== undefined) {
1210
+ pairCallee.set(res, callee);
1211
+ }
1212
+ } else if (returns.kind === 'word' || (returns.kind === 'register-struct' && returns.image)) {
1213
+ site.write(target.returnReg, res); // the callee defines the return register …
1214
+ }
1215
+ // … and the clobber is recorded after it, so that def is the CALLEE's; a struct, the memory at
1216
+ // argument 0 or the return register's bytes, leaves that register holding nothing the caller
1217
+ // may read as a value, save the image of one a local is declared for, which a guessed arity
1218
+ // still does not count (`SsaBuilder.noteStructImage`)
1219
+ ssa.noteCall(bi, p.clobbers);
1220
+ if (returns.kind === 'register-struct' && returns.image) {
1221
+ ssa.noteStructImage(bi, target.returnReg, res);
1222
+ }
1223
+ };
1224
+ return {
1225
+ callClobbers,
1226
+ returnsWithoutHiddenPointer: (callee) => returnsWithoutHiddenPointer(callee, prototypes, target),
1227
+ declaredCall,
1228
+ declaredParams,
1229
+ plan,
1230
+ lower,
1231
+ heldAtUndeclaredCall,
1232
+ refuseDispatchUnlockedPassThrough: (caseStateRead) => {
1233
+ if ((parkedInArgs || caseStateRead) && passThrough !== undefined) {
1234
+ fail(
1235
+ `cannot lift '${name}': the call ${passThrough.call} has no prototype, and ${passThrough.reg}, ` +
1236
+ `${passThrough.why}, in a function that lifts only through what a jump-table dispatch left in its ` +
1237
+ 'registers — the arity guessed without a prototype and another compile to the same bytes, so the ' +
1238
+ 'argument count is not decidable',
1239
+ );
1240
+ }
1241
+ },
1242
+ };
1243
+ }
1244
+
1245
+ // A CALLEE DECLARED TO RETURN A STRUCT OR UNION BY VALUE, where the target returns it through
1246
+ // memory: the caller hands it the storage in argument 0 and every declared argument one register
1247
+ // up (on agbcc, thumb.h:644-645, 672), so the call's first word is that pointer. What it returns in
1248
+ // the return register is not a value the caller reads (calls.c: the struct is the memory at the
1249
+ // address). One the target returns in registers takes its arguments where they are declared, and
1250
+ // its registers hold the struct's bytes: the call is `'register'`, and the return register is the
1251
+ // struct's image where a local of it can be declared (`registerStructLocal`) and holds nothing the
1252
+ // caller may read otherwise. Every other struct-returning call refuses, naming why.
1253
+ function structReturnOf(
1254
+ name: string,
1255
+ target: TargetDescription,
1256
+ callee: string,
1257
+ own: FnProto,
1258
+ fail: FrontendRefusal,
1259
+ ): StructReturn | 'register' {
1260
+ const read = returnedStruct(own, target);
1261
+ if (typeof read === 'object' && 'refused' in read) {
1262
+ fail(
1263
+ `cannot lift '${name}': \`${callee}\` is declared to return ${own.returns ?? 'a struct or union'} by value, and ${read.refused}`,
1264
+ );
1265
+ }
1266
+ return read as StructReturn | 'register';
1267
+ }
1268
+
1269
+ /** HOW A FUNCTION DECLARED TO RETURN A STRUCT OR UNION BY VALUE HANDS IT BACK on this target: the
1270
+ * struct laid out as a local of it is declared (`type`) where the target returns it through a
1271
+ * hidden pointer in argument 0, `'register'` where it returns it in registers, and the reason
1272
+ * otherwise. Both halves of the return read this: a call to such a callee (`structReturnOf`) and a
1273
+ * function that itself returns one (pipeline.ts `ownStructReturnThroughMemory`). */
1274
+ export function returnedStruct(
1275
+ own: FnProto,
1276
+ target: TargetDescription,
1277
+ ): { type: Extract<IrType, { kind: 'struct' }> } | 'register' | { refused: string } {
1278
+ const layout = returnedAggregate(own);
1279
+ const inMemory = returnsInMemory(layout, target);
1280
+ if (inMemory === undefined) {
1281
+ return {
1282
+ refused:
1283
+ 'nothing here says whether it comes back through a hidden pointer — its members are not all known, this target does not ' +
1284
+ 'say how it lays one of them out (an enum, a bitfield), or it states no rule ' +
1285
+ '(a prototype states them as `returnLayout`, a context by defining the struct)',
1286
+ };
1287
+ }
1288
+ if (inMemory === false) {
1289
+ return 'register';
1290
+ }
1291
+ const type = structLocalType(own, target);
1292
+ return 'refused' in type ? type : { type };
1293
+ }
1294
+
1295
+ /** The struct a declaration returns by value as a local of it is declared, or why it has none. */
1296
+ export function structLocalType(
1297
+ own: FnProto,
1298
+ target: TargetDescription,
1299
+ ): Extract<IrType, { kind: 'struct' }> | { refused: string } {
1300
+ // the local it lands in is declared as the header spells the type, qualifiers aside; the tag
1301
+ // is what the declarations block defines it by, and a typedef name is that tag too
1302
+ const spelling = (own.returns ?? '')
1303
+ .replace(/\b(?:const|volatile)\b/g, ' ')
1304
+ .trim()
1305
+ .replace(/\s+/g, ' ');
1306
+ const tag = /^(?:(?:struct|union)\s+)?([A-Za-z_]\w*)$/.exec(spelling)?.[1];
1307
+ if (tag === undefined) {
1308
+ return { refused: 'it names no type a local of it could be declared with' };
1309
+ }
1310
+ const type = aggregateType(tag, spelling, returnedAggregate(own), target);
1311
+ if (type?.kind !== 'struct') {
1312
+ return {
1313
+ refused:
1314
+ 'it is a union, or its members are not all known or this target does not say how it lays one of them out ' +
1315
+ '(an enum, a bitfield) — the local it lands in has no type here',
1316
+ };
1317
+ }
1318
+ return type;
1319
+ }
1320
+
1321
+ // A STRUCT RETURNED IN THE RETURN REGISTER IS READ OFF A LOCAL OF IT (frontend/register-struct-call.ts).
1322
+ // The register holds the struct's bytes as the target lays them in a word: from bit 0 up where
1323
+ // the target is little-endian (agbcc's `{u16}` is `strh r0` off the call), from bit 31 down where it
1324
+ // is big-endian (mwcc's is `srwi r0,r3,16`). So the register is the struct's image, which the call
1325
+ // writes and `liftRegisterStructCalls` re-reads as a load of the local the call assigns, and the
1326
+ // member a later read takes out of it is that local's member: `s = mk(x); gV = s.h;`, which is
1327
+ // what every such compiler compiles to these bytes. Only the declaration says the call returns a
1328
+ // struct at all, since the bytes are a scalar callee's, so only a callee the project declares
1329
+ // gets one, and only where the local can be:
1330
+ // - declared with a type (`structLocalType`): a union or a bitfield has none here;
1331
+ // - one, two or four bytes, the widths one load reads whole — three bytes, or the second
1332
+ // register of an eight-byte struct, is an image no load here builds;
1333
+ // - printed beside its callee's prototype, which the declarations block defines it by;
1334
+ // - all integers: a floating member has no spelling here that reads its bits as the float they
1335
+ // are (structure.ts `frameStructMember` spells an integer field), and `*(s32 *)&s` stored to a
1336
+ // `float` converts the value (agbcc: `bl __floatsisf`).
1337
+ // Otherwise the register still holds nothing the caller may read. The ONE derivation of that type:
1338
+ // the call plan marks the call by it, and `liftRegisterStructCalls` declares the local by it.
1339
+ export function registerStructLocal(
1340
+ own: FnProto | undefined,
1341
+ target: TargetDescription,
1342
+ ): Extract<IrType, { kind: 'struct' }> | undefined {
1343
+ if (own === undefined) {
1344
+ return undefined;
1345
+ }
1346
+ const type = structLocalType(own, target);
1347
+ if ('refused' in type || ![1, 2, 4].includes(type.size ?? 0) || hasFloatingMember(type)) {
1348
+ return undefined;
1349
+ }
1350
+ return spellableProto(own, target, type) === undefined ? undefined : type;
1351
+ }
1352
+
1353
+ function hasFloatingMember(t: IrType): boolean {
1354
+ switch (t.kind) {
1355
+ case 'float':
1356
+ return true;
1357
+ case 'struct':
1358
+ return t.fields.some((f) => hasFloatingMember(f.type));
1359
+ case 'union':
1360
+ return t.members.some((f) => hasFloatingMember(f.type));
1361
+ case 'array':
1362
+ return hasFloatingMember(t.elem);
1363
+ default:
1364
+ return false;
1365
+ }
1366
+ }
1367
+
1368
+ /** A STRUCT PASSED BY VALUE IS THE ONE OBJECT ITS ARGUMENT WORDS HOLD AT THE CALL. The lowering
1369
+ * hands its words over one operand each (`byValueWords`, pairs of first operand and word count);
1370
+ * C passes the struct, so the words must be one object's, whole, and that object is what the call
1371
+ * is handed. Two objects are:
1372
+ *
1373
+ * - A FRAME OBJECT the words are loaded off — word loads at consecutive words from one `laddr`
1374
+ * offset. The operand becomes that offset's address, and the frame-object audit judges the
1375
+ * object as the struct (frame-objects.ts `objectShapes`).
1376
+ * - A GLOBAL the words are loaded off, or were just stored to — word accesses at consecutive words
1377
+ * from the symbol's own address (`gaddr`, one or one per word of the same symbol), loaded off it
1378
+ * or stored there as the very values passed. The operand becomes the symbol's address, spelled
1379
+ * bare (`take(gS)`), which compiles only where the declarations type the global as the struct.
1380
+ * So a symbol C cannot name (a section symbol, `.bss`; a function-scope static's `sQ.3`)
1381
+ * refuses, and so does one the declaration context or the symbol map types as anything else:
1382
+ * the struct at offset 0 of `gQs[4]` or of `struct O2 { struct Q q; … } gO2` sits at the
1383
+ * symbol's own address too, and `take(gQs)` is a type error against the header that declares
1384
+ * it (`bareStructRefusal`).
1385
+ *
1386
+ * Either way every access is unqualified and in the call's own block, and no store or call lies
1387
+ * between the earliest of them and the call but those accesses themselves, so the object holds at
1388
+ * the call what the words carry. Every other source — registers computed in place, words off two
1389
+ * objects, off a pointer or into a global's interior, a store the call may not see last — refuses.
1390
+ *
1391
+ * The operand's value is typed a pointer to the struct, and a global's `gaddr` is a fresh one that
1392
+ * names the struct too (`passedAs`), so value numbering never merges it with a plain address of
1393
+ * the symbol. The call then names which operands it passes by value (`byValue`, pairs of operand
1394
+ * and declared parameter, read by ir/core.ts `byValueOperands`). */
1395
+ export function foldByValueArgs(
1396
+ name: string,
1397
+ irBlocks: Block[],
1398
+ calls: Pick<CallDeclarations, 'declaredCall'>,
1399
+ declarations: { symbols: SymbolMap | undefined; context: ContextGlobals | undefined },
1400
+ ): void {
1401
+ const defs = new Map<Value, { op: Op; blk: Block }>();
1402
+ for (const blk of irBlocks) {
1403
+ for (const op of blk.ops) {
1404
+ op.results.forEach((r) => defs.set(r, { op, blk }));
1405
+ }
1406
+ }
1407
+ for (const blk of irBlocks) {
1408
+ for (const call of [...blk.ops]) {
1409
+ const words = call.attrs.byValueWords as number[] | undefined;
1410
+ if (call.opcode !== 'call' || words === undefined) {
1411
+ continue;
1412
+ }
1413
+ const callee = calleeName(call);
1414
+ const declared = callee === undefined ? undefined : calls.declaredCall(callee)?.byValue;
1415
+ const pairs = Array.from({ length: words.length / 2 }, (_, i) => {
1416
+ const arg = declared?.find((b) => b.word === words[2 * i]);
1417
+ const type = arg?.type;
1418
+ if (type?.kind !== 'struct' || type.size === undefined) {
1419
+ throw new Error(`internal: a by-value operand of a call to '${callee}' has no declared struct`);
1420
+ }
1421
+ return { first: words[2 * i], n: words[2 * i + 1], type, param: arg!.param };
1422
+ });
1423
+ // highest first, so folding one leaves the operand places of those below it as they were
1424
+ for (const { first, n, type } of [...pairs].sort((x, y) => y.first - x.first)) {
1425
+ const at = blk.ops.indexOf(call);
1426
+ const passed = call.operands.slice(first, first + n);
1427
+ // the earliest of `accesses` and every op after it up to the call writes nothing else
1428
+ const settled = (accesses: readonly Op[]): boolean => {
1429
+ const earliest = Math.min(...accesses.map((a) => blk.ops.indexOf(a)));
1430
+ return blk.ops
1431
+ .slice(earliest, at)
1432
+ .every((op) => (op.opcode !== 'store' && op.opcode !== 'call') || accesses.includes(op));
1433
+ };
1434
+ // a word access at byte `start + 4 * i` from one base, unqualified, in the call's block
1435
+ // before the call
1436
+ const wordAt = (op: Op | undefined, i: number, base: Value | undefined, start = 0): op is Op =>
1437
+ op !== undefined &&
1438
+ blk.ops.indexOf(op) >= 0 &&
1439
+ blk.ops.indexOf(op) < at &&
1440
+ (op.attrs.width as number) === 4 &&
1441
+ !qualified(op) &&
1442
+ op.operands[0] === base &&
1443
+ (op.attrs.off as number) === start + 4 * i;
1444
+ const loads = passed.map((v) => defs.get(v)?.op);
1445
+ const loaded = loads.every((d) => d?.opcode === 'load');
1446
+ const base0 = loaded ? loads[0]!.operands[0] : undefined;
1447
+ const baseDef = base0 === undefined ? undefined : defs.get(base0)?.op;
1448
+ let object: Op | undefined;
1449
+ if (baseDef?.opcode === 'laddr') {
1450
+ // off the frame, through one `laddr` or several of the same offset (the slot model mints
1451
+ // one per access): the displacement is past the `laddr`'s own offset
1452
+ const frameAt = loads.map((d) => {
1453
+ const b = defs.get(d!.operands[0])?.op;
1454
+ return b?.opcode === 'laddr' && wordAt(d, 0, d!.operands[0], d!.attrs.off as number)
1455
+ ? (b.attrs.off as number) + (d!.attrs.off as number)
1456
+ : undefined;
1457
+ });
1458
+ const from = frameAt[0];
1459
+ if (from !== undefined && frameAt.every((o, i) => o === from + 4 * i) && settled(loads as Op[])) {
1460
+ object = mkOp('laddr', { results: [mkValue(T.ptr(type))], attrs: { off: from } });
1461
+ }
1462
+ } else {
1463
+ const symbol = (v: Value | undefined): string | undefined => {
1464
+ const d = v === undefined ? undefined : defs.get(v)?.op;
1465
+ return d?.opcode === 'gaddr' && d.attrs.code !== true ? (d.attrs.sym as string) : undefined;
1466
+ };
1467
+ // ONE SYMBOL, NOT ONE VALUE: kmc addresses each word through a `lui`/`lw` pair of its own,
1468
+ // so the words come off several `gaddr`s of the same symbol rather than one base. Each
1469
+ // access is still at its word's offset from the symbol's own address.
1470
+ const ofSymbol = (op: Op | undefined, i: number, sym0: string): op is Op =>
1471
+ op !== undefined && symbol(op.operands[0]) === sym0 && wordAt(op, i, op.operands[0]);
1472
+ const sym0 = symbol(base0);
1473
+ let sym: string | undefined;
1474
+ if (loaded && sym0 !== undefined && loads.every((d, i) => ofSymbol(d, i, sym0)) && settled(loads as Op[])) {
1475
+ sym = sym0;
1476
+ } else {
1477
+ // the words stored to a global just before the call: the last store of each, in order
1478
+ const stores = passed.map((v) =>
1479
+ blk.ops
1480
+ .slice(0, at)
1481
+ .filter((op) => op.opcode === 'store' && op.operands[1] === v)
1482
+ .at(-1),
1483
+ );
1484
+ const sink = symbol(stores[0]?.operands[0]);
1485
+ if (sink !== undefined && stores.every((st, i) => ofSymbol(st, i, sink)) && settled(stores as Op[])) {
1486
+ sym = sink;
1487
+ }
1488
+ }
1489
+ const misnamed = sym === undefined ? undefined : bareStructRefusal(sym, type, declarations);
1490
+ if (misnamed !== undefined) {
1491
+ throw new FrontendUnsupportedError(
1492
+ `cannot lift '${name}': the call to \`${callee ?? 'a function pointer'}\` passes ${structSpelling(type)} ` +
1493
+ `by value off '${sym}', and ${misnamed} — the struct would be spelled as that symbol bare`,
1494
+ );
1495
+ }
1496
+ if (sym !== undefined) {
1497
+ object = mkOp('gaddr', {
1498
+ results: [mkValue(T.ptr(type))],
1499
+ attrs: { sym, passedAs: structSpelling(type) },
1500
+ });
1501
+ }
1502
+ }
1503
+ if (object === undefined) {
1504
+ throw new FrontendUnsupportedError(
1505
+ `cannot lift '${name}': the call to \`${callee ?? 'a function pointer'}\` passes ${structSpelling(type)} ` +
1506
+ 'by value, and its words are not one frame object or one global, read whole just before the call — ' +
1507
+ 'a struct passed from anywhere else is not modelled',
1508
+ );
1509
+ }
1510
+ blk.ops.splice(at, 0, object);
1511
+ call.operands.splice(first, n, object.results[0]);
1512
+ // the word loads, and a frame address only they read, are dead now, and would otherwise be
1513
+ // judged as accesses of their own
1514
+ for (const d of loads) {
1515
+ if (d?.opcode === 'load') {
1516
+ dropIfUnread(irBlocks, d);
1517
+ const b = defs.get(d.operands[0])?.op;
1518
+ if (b?.opcode === 'laddr') {
1519
+ dropIfUnread(irBlocks, b);
1520
+ }
1521
+ }
1522
+ }
1523
+ }
1524
+ // each folded struct is one operand now: its place is its first word less the words folded
1525
+ // below it
1526
+ const sorted = [...pairs].sort((x, y) => x.first - y.first);
1527
+ const { byValueWords: _, ...attrs } = call.attrs;
1528
+ call.attrs = {
1529
+ ...attrs,
1530
+ byValue: sorted.flatMap(({ first, param }) => [
1531
+ first - pairs.filter((q) => q.first < first).reduce((m, q) => m + q.n - 1, 0),
1532
+ param,
1533
+ ]),
1534
+ };
1535
+ }
1536
+ }
1537
+ }
1538
+
1539
+ /** Why the global `sym` cannot be spelled bare as the struct `type` a call passes by value, or
1540
+ * undefined where it can: a C name, declared as exactly that struct where a declaration context or
1541
+ * a symbol map types it at all. A symbol neither types is declared from the call (declare.ts
1542
+ * `passedAs`). */
1543
+ function bareStructRefusal(
1544
+ sym: string,
1545
+ type: IrType,
1546
+ { symbols, context }: { symbols: SymbolMap | undefined; context: ContextGlobals | undefined },
1547
+ ): string | undefined {
1548
+ // the linker's name for the object, where it is one no C source spells: a static at its
1549
+ // section's start relocated against the section (`.bss`), or a function-scope static's name and
1550
+ // counter (`sQ.3`), which only the static's definition spells (frontend/reloc-symbol.ts)
1551
+ const unspellable = unspellableReason(sym);
1552
+ if (unspellable !== null) {
1553
+ return `that is no name C can spell: it ${unspellable}`;
1554
+ }
1555
+ // ONE STRUCT, MANY SPELLINGS: `typedef struct Q {…} Q_t;` spells it `Q_t` and `struct Q`, and a
1556
+ // header passes one and declares the other. Each side is resolved to the struct it names through
1557
+ // the context's typedefs (`structNamed`), and a spelling that resolves to none matches only as
1558
+ // written.
1559
+ const spelling = structSpelling(type);
1560
+ const passed = structNamed(spelling, context);
1561
+ const isPassed = (other: string): boolean =>
1562
+ other === spelling || (passed !== undefined && structNamed(other, context) === passed);
1563
+ if (context?.declared.has(sym) && !isPassed(context.objectTypes.get(sym) ?? '')) {
1564
+ return `the declaration context does not declare it as ${spelling} (an array, an enclosing struct, another type)`;
1565
+ }
1566
+ const info = symbols === undefined ? undefined : symbolsByName(symbols, new Set([sym])).get(sym);
1567
+ // the map names a struct by its tag, or by the typedef name that binds an untagged one
1568
+ // (symbols.ts `structName`)
1569
+ const mapped = info?.structName;
1570
+ if (
1571
+ info?.shape !== undefined &&
1572
+ !(info.shape === 'struct' && mapped !== undefined && (isPassed(`struct ${mapped}`) || passed === mapped))
1573
+ ) {
1574
+ return `the symbol map types it as ${info.shape === 'struct' ? `struct ${info.structName ?? '(untagged)'}` : `${info.shape === 'array' ? 'an' : 'a'} ${info.shape}`}`;
1575
+ }
1576
+ return undefined;
1577
+ }
1578
+
1579
+ /** Removes `op` where nothing reads its result. */
1580
+ function dropIfUnread(irBlocks: Block[], op: Op): void {
1581
+ const r = op.results[0];
1582
+ const read = irBlocks.some((b) =>
1583
+ b.ops.some((x) => x.operands.includes(r) || x.successors.some((sc) => sc.args.includes(r))),
1584
+ );
1585
+ if (!read) {
1586
+ for (const b of irBlocks) {
1587
+ b.ops = b.ops.filter((x) => x !== op);
1588
+ }
1589
+ }
1590
+ }