@asmlift/core 0.8.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (105) hide show
  1. package/README.md +34 -30
  2. package/package.json +1 -1
  3. package/src/aggregate.ts +248 -0
  4. package/src/backend/c.ts +5 -2
  5. package/src/backend/cfamily.ts +236 -37
  6. package/src/backend/cpp.ts +114 -9
  7. package/src/backend/pascal.ts +39 -0
  8. package/src/cdecl/constant.ts +59 -0
  9. package/src/cdecl/lex.ts +209 -0
  10. package/src/cdecl/parse.ts +975 -0
  11. package/src/cdecl/spell.ts +69 -0
  12. package/src/codegen-flags.ts +38 -6
  13. package/src/compiler-diagnostics.ts +194 -0
  14. package/src/contracts.ts +615 -79
  15. package/src/declare.ts +75 -5
  16. package/src/frontend/asmdata.ts +19 -5
  17. package/src/frontend/call-plan.ts +686 -0
  18. package/src/frontend/device-pins.ts +277 -0
  19. package/src/frontend/errors.ts +5 -0
  20. package/src/frontend/flags-edge.ts +101 -0
  21. package/src/frontend/fpu.ts +167 -0
  22. package/src/frontend/frame-objects.ts +2131 -0
  23. package/src/frontend/high-half.ts +2 -1
  24. package/src/frontend/local-object.ts +490 -0
  25. package/src/frontend/mips.ts +363 -40
  26. package/src/frontend/opaque.ts +97 -18
  27. package/src/frontend/ppc.ts +520 -129
  28. package/src/frontend/reloc-symbol.ts +94 -13
  29. package/src/frontend/splat.ts +108 -5
  30. package/src/frontend/ssa.ts +669 -76
  31. package/src/frontend/stackargs.ts +120 -39
  32. package/src/frontend/thumb.ts +2764 -2018
  33. package/src/ir/alias.ts +29 -0
  34. package/src/ir/core.ts +101 -3
  35. package/src/ir/discipline.ts +248 -0
  36. package/src/ir/float-bits.ts +47 -0
  37. package/src/ir/opcodes.ts +84 -112
  38. package/src/ir/parse.ts +1 -0
  39. package/src/ir/types.ts +118 -2
  40. package/src/ir/verify.ts +150 -3
  41. package/src/l3/address.ts +5 -0
  42. package/src/l3/argcopy.ts +38 -6
  43. package/src/l3/ast.ts +186 -14
  44. package/src/l3/coalesce.ts +21 -13
  45. package/src/l3/dce.ts +32 -10
  46. package/src/l3/gates.ts +9 -3
  47. package/src/l3/hoist.ts +8 -7
  48. package/src/l3/initfirst.ts +27 -9
  49. package/src/l3/intcell.ts +97 -0
  50. package/src/l3/mentions.ts +10 -4
  51. package/src/l3/narrowdecl.ts +218 -0
  52. package/src/l3/nearbase.ts +10 -5
  53. package/src/l3/parkfirst.ts +8 -5
  54. package/src/l3/pollguard.ts +3 -2
  55. package/src/l3/ptrcell.ts +357 -0
  56. package/src/l3/regspell.ts +17 -3
  57. package/src/l3/reindex.ts +10 -7
  58. package/src/l3/symbol-refs.ts +104 -23
  59. package/src/l3/tailmerge.ts +6 -19
  60. package/src/l3/typing.ts +137 -25
  61. package/src/l3/unreduce.ts +4 -4
  62. package/src/l3/volstore.ts +96 -7
  63. package/src/mangle.ts +11 -1
  64. package/src/pattern/engine.ts +19 -16
  65. package/src/pipeline.ts +57 -12
  66. package/src/proto-context.ts +788 -0
  67. package/src/proto.ts +689 -50
  68. package/src/raise/arrays.ts +4 -3
  69. package/src/raise/declared-volatile.ts +131 -0
  70. package/src/raise/divpow2.ts +2 -2
  71. package/src/raise/errors.ts +17 -0
  72. package/src/raise/floathelpers.ts +243 -0
  73. package/src/raise/globalshape.ts +11 -5
  74. package/src/raise/gvn.ts +26 -5
  75. package/src/raise/narrow.ts +29 -2
  76. package/src/raise/narrowlocal.ts +11 -6
  77. package/src/raise/offsetnames.ts +510 -0
  78. package/src/raise/pairparams.ts +81 -0
  79. package/src/raise/paramwidth.ts +7 -0
  80. package/src/raise/pre-recovery.ts +78 -4
  81. package/src/raise/recover.ts +14 -10
  82. package/src/raise/retsink.ts +1 -1
  83. package/src/raise/shortcircuit.ts +41 -19
  84. package/src/raise/softdiv.ts +21 -23
  85. package/src/raise/struct-arrays.ts +5 -4
  86. package/src/raise/structs.ts +311 -98
  87. package/src/raise/truncload.ts +366 -0
  88. package/src/raise/widehelpers.ts +106 -0
  89. package/src/rank-declare.ts +30 -5
  90. package/src/rank-variations.ts +36 -8
  91. package/src/rank.ts +276 -80
  92. package/src/runtime-helpers.ts +287 -0
  93. package/src/stillborn.ts +289 -0
  94. package/src/structure/analysis.ts +1285 -448
  95. package/src/structure/hazards.ts +633 -58
  96. package/src/structure/local-statics.ts +381 -0
  97. package/src/structure/namecoalesce.ts +1 -1
  98. package/src/structure/redundant-test.ts +99 -0
  99. package/src/structure/structure.ts +1772 -395
  100. package/src/structure/switch-recover.ts +159 -78
  101. package/src/symbols.ts +38 -3
  102. package/src/target.ts +662 -59
  103. package/src/trace.ts +44 -24
  104. package/src/variation-definitions.ts +119 -1
  105. package/src/variation-tokens.ts +20 -6
@@ -0,0 +1,686 @@
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 { aggregateType, returnedAggregate, returnsInMemory, returnsWithoutHiddenPointer } from '../aggregate';
16
+ import { type Value, mkOp, mkValue } from '../ir/core';
17
+ import { type IrType, T, typeToString } from '../ir/types';
18
+ import {
19
+ type FnProto,
20
+ type Prototypes,
21
+ STANDARD_SIGNATURES,
22
+ declaredCallArgs,
23
+ declaredReturnWidth,
24
+ declaresAggregateReturn,
25
+ declaresParams,
26
+ declaresVoidReturn,
27
+ isFloatingSpelling,
28
+ spellableProto,
29
+ wordsOf,
30
+ } from '../proto';
31
+ import { helperPrototypes, isFloatHelper, isWideHelper, lookupHelper } from '../runtime-helpers';
32
+ import type { TargetDescription } from '../target';
33
+ import type { FrontendRefusal } from './errors';
34
+ import type { HighHalves } from './high-half';
35
+ import { type SsaBuilder, clobberedByCall, fallbackArgc } from './ssa';
36
+
37
+ /** What the frontend's own call lowering can carry out. */
38
+ export interface CallLowering {
39
+ /** builds a 64-bit value out of two argument words, and splits a 64-bit return */
40
+ readonly pairs: boolean;
41
+ /** passes a hidden struct-return pointer in argument 0 and types the call's value as the struct */
42
+ readonly memoryReturn: boolean;
43
+ /** reads argument words past the registers off its own frame */
44
+ readonly stackArgs: boolean;
45
+ /** writes no value for a callee declared void, and its own return reads the return register only
46
+ * where a value reaches it (`SsaBuilder.holdsValue`), so the register the call destroyed is no
47
+ * return value */
48
+ readonly voidReturn: boolean;
49
+ /** a call through argument register rN passes r0..r(N-1). The compiler's arguments fill the
50
+ * registers from r0 up and the address cannot share one, which holds for agbcc: soft-float, no
51
+ * pair alignment. o32 refutes it twice — a leading float takes slot a0 and leaves the register
52
+ * free (`jalr a0` passing $f12 and a1), and a 64-bit argument's even pair leaves a1 empty
53
+ * (`jalr a1` passing a0, a2 and a3) — so a lowering that does not state it guesses. */
54
+ readonly argRegisterBoundsArity: boolean;
55
+ }
56
+
57
+ /** A call's struct return through memory: the declared struct, laid out on this target. */
58
+ interface StructReturn {
59
+ type: IrType;
60
+ }
61
+
62
+ /** What one callee's declaration states about a call (`CallDeclarations.declaredCall`). */
63
+ export interface DeclaredCall {
64
+ /** each argument's width, in bits, a hidden struct-return pointer first */
65
+ readonly widths: readonly number[];
66
+ /** the indices into `widths` that are a `double` */
67
+ readonly doubles: ReadonlySet<number>;
68
+ /** the declared parameter count, a hidden pointer aside */
69
+ readonly params: number;
70
+ readonly returned?: StructReturn | 'register';
71
+ }
72
+
73
+ /** What one call is, decided from its callee's declaration before any instruction is read. */
74
+ export interface CallPlan {
75
+ /** each argument's width, a hidden pointer first; null when nothing sizes them, so `lower`
76
+ * guesses */
77
+ readonly widths: readonly number[] | null;
78
+ /** the indices into `widths` its declaration types `double` */
79
+ readonly doubles: ReadonlySet<number>;
80
+ readonly returns:
81
+ | { readonly kind: 'word' }
82
+ | { readonly kind: 'void' }
83
+ | { readonly kind: 'pair' }
84
+ | { readonly kind: 'register-struct' }
85
+ | { readonly kind: 'memory-struct'; readonly type: IrType };
86
+ /** what the call leaves holding nothing this function can name, for `SsaBuilder.noteCall` */
87
+ readonly clobbers: readonly string[];
88
+ /** the callee is declared void, whether or not this lowering writes no value for it (`returns`
89
+ * is `void` only where `CallLowering.voidReturn` says so) */
90
+ readonly declaredVoid: boolean;
91
+ }
92
+
93
+ /** WHAT EACH CALLEE'S DECLARATION SAYS, and what a call leaves holding nothing this function can
94
+ * name. A declaration is the project's prototype or the compiler's runtime table, and a frontend
95
+ * reads either only here: its outgoing-argument analysis, its call lowering (`lower`) and the
96
+ * frame-object audit (`FrameObjectAudit.returnsWithoutHiddenPointer`) ask these questions. */
97
+ export interface CallDeclarations {
98
+ /** what any call leaves holding nothing this function can name (`clobberedByCall`) */
99
+ readonly callClobbers: readonly string[];
100
+ /** whether a declaration rules out a struct returned through a hidden pointer at argument 0
101
+ * (`returnsWithoutHiddenPointer`, aggregate.ts) */
102
+ returnsWithoutHiddenPointer(callee: string): boolean;
103
+ /** the callee's declared argument widths, for a frontend that places them before it lowers the
104
+ * call; null where no declaration sizes them */
105
+ declaredCall(callee: string): DeclaredCall | null;
106
+ /** `lower`'s first half, the call decided from the declaration alone: a value its tests compare
107
+ * whole, where `lower` leaves IR */
108
+ plan(callee: string): CallPlan;
109
+ /** the call `site` makes, planned and lowered: its `call` op, its result and its clobbers */
110
+ lower(site: CallSite): void;
111
+ }
112
+
113
+ /** WHAT ONE ISA ALONE KNOWS ABOUT A CALL: everything `CallDeclarations.lower` asks a frontend. The
114
+ * argument and return registers are the target's. */
115
+ export interface CallSite {
116
+ /** the callee's name, or the address of the function a call through a register calls */
117
+ readonly callee: string | IndirectCallee;
118
+ readonly ssa: SsaBuilder;
119
+ /** the block the call is made in */
120
+ readonly bi: number;
121
+ /** an argument register the plan states, read as this frontend reads a register */
122
+ read(reg: string): Value;
123
+ /** the call's result, written as this frontend writes a register */
124
+ write(reg: string, v: Value): void;
125
+ /** argument word `k` past the registers, where this ABI places it (`CallLowering.stackArgs`) */
126
+ stackWord?(k: number): Value;
127
+ /** how this frontend builds and splits a pair (`CallLowering.pairs`) */
128
+ readonly pairs?: CallPairs;
129
+ /** the high halves this frontend defines registers with, which are no argument */
130
+ readonly highHalves?: HighHalves;
131
+ /** what a guessed arity refuses on; without it a gap in the argument registers ends the count */
132
+ readonly guess?: {
133
+ /** the call's address, for the gap refusal */
134
+ readonly at: number;
135
+ /** refuses a guess of `argc` words this ABI's outgoing area could extend */
136
+ refuse(argc: number): void;
137
+ };
138
+ }
139
+
140
+ /** A call through a register. It names no callee, so no declaration plans it: an argument
141
+ * register holding the address bounds its arity where the lowering states it
142
+ * (`CallLowering.argRegisterBoundsArity`), and otherwise it is guessed. */
143
+ export interface IndirectCallee {
144
+ readonly address: Value;
145
+ /** the register the address is in */
146
+ readonly reg: string;
147
+ }
148
+
149
+ /** How a frontend builds a 64-bit value out of two words and splits one. */
150
+ export interface CallPairs {
151
+ fuse(lo: Value, hi: Value): Value;
152
+ project(whole: Value, half: 'lo' | 'hi'): Value;
153
+ /** the pair each projected half was split from */
154
+ readonly halfOf: ReadonlyMap<Value, { readonly whole: Value; readonly half: 'lo' | 'hi' }>;
155
+ }
156
+
157
+ export function callDeclarations(
158
+ name: string,
159
+ target: TargetDescription,
160
+ prototypes: Prototypes,
161
+ lowering: CallLowering,
162
+ fail: FrontendRefusal,
163
+ ): CallDeclarations {
164
+ // What a call leaves holding nothing this function can name — checked against `argRegs` there.
165
+ const callClobbers = clobberedByCall(target);
166
+ // …and what a call to a PAIR-RETURNING helper leaves: the same set minus the high half, because
167
+ // the low half is the return register (already excluded) and `lower` writes the high one itself
168
+ // from the callee's own result. The two arms of one rule: where the callee hands a register back,
169
+ // the lowering names it; where it does not, nobody can.
170
+ //
171
+ // `argRegs[1]` IS THE PAIR'S HIGH REGISTER ONLY WHERE THE ABI ALIASES THE FIRST ARGUMENT ONTO
172
+ // THE RETURN REGISTER, which makes the returned pair occupy the first two argument registers.
173
+ // True on Thumb (r0:r1) and PowerPC (r3:r4); FALSE on MIPS o32, which returns in v0:v1 and passes
174
+ // in a0:a1, so reading a high half out of `argRegs[1]` there would name an argument register. A
175
+ // lowering that builds pairs on a target without the identity is a target bug.
176
+ if (lowering.pairs && target.returnReg !== target.argRegs[0]) {
177
+ throw new Error(
178
+ `target '${target.id}': a lowering that builds pairs needs the return register to be the first ` +
179
+ `argument register, and ${target.returnReg} is not ${target.argRegs[0]}`,
180
+ );
181
+ }
182
+ const pairReturnClobbers = callClobbers.filter((r) => r !== target.argRegs[1]);
183
+ const isFloatHelperName = (callee: string): boolean => {
184
+ const h = lookupHelper(target.runtimeHelpers, callee);
185
+ return h !== undefined && isFloatHelper(h);
186
+ };
187
+ /** the callee that handed back each pair a call returned, by the pair's value */
188
+ const pairCallee = new Map<Value, string>();
189
+ /** the value each call to a callee declared void left in the return register */
190
+ const voidResults = new Set<Value>();
191
+ // The compiler's own runtime, off the TARGET (runtime-helpers.ts): which helpers a compiler
192
+ // emits is a compiler fact, and reading one table for every ISA is how a scan for `__*di3`
193
+ // reports zero on a compiler whose runtime spells them `__ll_*`.
194
+ const helperProtos = helperPrototypes(target.runtimeHelpers);
195
+ const wideHelper = (callee: string) => {
196
+ // Through the table's one reader (`lookupHelper`): a bare index answers with a member of
197
+ // `Object.prototype` for a callee named `toString`, which is truthy and has no `params` for
198
+ // `isWideHelper` to read. The `prototypes` read below needs no such guard because
199
+ // `declaresParams` is that table's designated safe reader — it answers "nothing is declared"
200
+ // for an entry that is not an `FnProto`, whatever it is.
201
+ const h = lookupHelper(target.runtimeHelpers, callee);
202
+ if (!h || !isWideHelper(h)) {
203
+ return null;
204
+ }
205
+ // A PROJECT RE-DECLARATION DISABLES THE CAPABILITY, it does not redirect it. A runtime helper's
206
+ // signature is its COMPILER's — `proto.ts` holds the signatures the C standard fixes, which this is precisely
207
+ // not — so a header declaring `__muldi3` is not a better source for the same fact; it is a
208
+ // claim that the name is the project's own function. Neither reading can be honoured: as the
209
+ // table's helper it would contradict the header, and as an ordinary call it becomes the
210
+ // pass-through that MATCHES for free. So no pair is built and `raise/widehelpers.ts` gaps the
211
+ // call, at whichever arity was declared.
212
+ if (declaresParams(prototypes[callee])) {
213
+ return null;
214
+ }
215
+ return h;
216
+ };
217
+ /** Whether the target's own runtime table claims this name — asked of the TABLE, not of what
218
+ * `wideHelper` made of it.
219
+ *
220
+ * A PROJECT'S `returns` NEVER BUILDS A PAIR FOR A RUNTIME HELPER'S NAME: a re-declaration
221
+ * disables the capability for the result as it does for the arguments. `wideHelper` answers
222
+ * null for a re-declared helper, which stops the ARGUMENT pair; the RESULT pair comes from a
223
+ * separate source, and `raise/widehelpers.ts` folds on the result alone (`arrivesAsDeclared`
224
+ * needs `results[0]` 64 bits wide). A pair built from the project's `returns` would fold the
225
+ * call into the compiler's own operation — `__muldi3` printed as `a0 * a1` — for a name the
226
+ * project says is its own function.
227
+ *
228
+ * A NON-WIDE ENTRY IS COVERED TOO, and deliberately: a `returns` on `__divsi3` states a width
229
+ * about a function whose signature is its compiler's, and `refuseUnmodelledHelpers` is going to
230
+ * gap the call whatever this answers. The table is the authority for every name in it. */
231
+ const isRuntimeHelperName = (callee: string): boolean => lookupHelper(target.runtimeHelpers, callee) !== undefined;
232
+ // WHETHER THE CALLEE HANDS BACK A PAIR — two sources for one ABI fact, and they answer the same
233
+ // question about the same two registers. A runtime helper's signature is its compiler's and needs
234
+ // no header; a project's callee needs one, and `returns` is where a header states it. Silence
235
+ // means a word — the callee then defines the return register alone and `frontend/ssa.ts` refuses
236
+ // a read of the other, because in that reading it is right to.
237
+ //
238
+ // AND THEY ARE ASKED IN THAT ORDER, never unioned: a name the runtime table carries is answered
239
+ // by the table or by nothing (`isRuntimeHelperName`), so a header that re-declares a helper
240
+ // disables the capability rather than restoring it through the other key.
241
+ const returnsPair = (callee: string): boolean => {
242
+ const wide = wideHelper(callee);
243
+ return (
244
+ (wide
245
+ ? wide.returns
246
+ : isRuntimeHelperName(callee)
247
+ ? undefined
248
+ : declaredReturnWidth(prototypes[callee], target)) === 64
249
+ );
250
+ };
251
+ // A callee the project declares to return a struct in registers, asked where `declaredCall` has
252
+ // no arity to answer with: the return is the declaration's, and needs none.
253
+ const registerStructReturn = (callee: string | undefined): 'register' | undefined => {
254
+ const own = callee !== undefined && Object.hasOwn(prototypes, callee) ? prototypes[callee] : undefined;
255
+ return declaresAggregateReturn(own) && structReturnOf(name, target, callee!, own!, fail) === 'register'
256
+ ? 'register'
257
+ : undefined;
258
+ };
259
+ // WHAT ONE CALLEE'S DECLARATION SAYS — the ONE place that reads it. A frontend's outgoing-argument
260
+ // analysis and its call lowering both come through here, so the arity that LICENSED a block and
261
+ // the arity that CONSUMES it cannot drift apart; a disagreement between two spellings of this
262
+ // lookup would read an argument register's neighbour as argument 5 or throw a slot-model error
263
+ // naming the wrong thing.
264
+ //
265
+ // THE WIDTHS ARE WHAT IS ASKED FOR, not a C parameter count, which would be the wrong number the
266
+ // moment one parameter is wider than a word: a `long long` adds a word AND moves every later
267
+ // argument's home, in the registers and on the stack alike. `proto.ts` converts the declaration
268
+ // once and every reader of it takes the same answer.
269
+ //
270
+ // WHAT THE CONVERSION CANNOT DO ON ITS OWN. `declaredWidth` answers for every type asmlift can
271
+ // spell, `long long` included, and `undefined` for a project typedef, a by-value struct or a
272
+ // floating type. `declaredCallArgs` sizes a `double` from the target
273
+ // (`TargetDescription.doubleArgWords`); one other such spelling and it states no layout at all,
274
+ // because the question here is not how wide that parameter is but whether it occupies one
275
+ // argument register or two, and the choice moves every later argument's home. So the declaration licenses no outgoing block and this returns `null`:
276
+ // the call is lifted at the arg-register guess, exactly as a callee the project never declared
277
+ // is, and the guess retracts the registers a call destroyed where a stated width would assert
278
+ // them.
279
+ //
280
+ // The COUNT form (`{ params: 5 }`) carries no spellings at all: it is the user's word for how
281
+ // many argument REGISTERS the call takes, and a count that lies is garbage in —
282
+ // `validatePrototypes` can no more check it than it can check `returnsVoid`. The machine-derived
283
+ // side upholds the premise at its source: `prototypesFromSymbols` drops a whole entry rather
284
+ // than spell a parameter that is not 1, 2 or 4 bytes (test/proto.test.ts).
285
+ //
286
+ // A frontend that places the widths before it lowers the call runs this over every call, so a
287
+ // refusal thrown here reaches the caller ahead of every other slot-model refusal, which is right
288
+ // because it is the most specific thing that was seen.
289
+ const declaredCall = (callee: string): DeclaredCall | null => {
290
+ // Three tiers, narrowing: the project's own headers, then the compiler's runtime helpers,
291
+ // then the signatures the C standard fixes (proto.ts). A project that re-declares one of the
292
+ // last two is read at its own declaration: it may be building against it. On a runtime
293
+ // helper's name that disables the helper rather than redirecting it (`wideHelper`).
294
+ //
295
+ // THE TIER QUESTION IS WHETHER THE PROJECT DECLARED THE CALLEE, not whether the declaration
296
+ // could be sized. A header that spells `memcpy`'s third parameter through a project typedef
297
+ // has still re-declared `memcpy`, and reading the standard's signature past it would answer
298
+ // for a different function — which is the one reading this tier order exists to prevent.
299
+ //
300
+ // `Object.hasOwn` on all three tables: a callee named `toString` or `valueOf` would otherwise
301
+ // read a `Function` off `Object.prototype` as its prototype entry.
302
+ const known = (t: Prototypes) => (Object.hasOwn(t, callee) ? t[callee] : undefined);
303
+ const own = known(prototypes);
304
+ const returned = declaresAggregateReturn(own) ? structReturnOf(name, target, callee, own!, fail) : undefined;
305
+ const proto = declaresParams(own) ? own : (known(helperProtos) ?? known(STANDARD_SIGNATURES));
306
+ const declared = declaredCallArgs(proto, target);
307
+ const params = declared?.widths;
308
+ if (params === undefined && returned !== undefined && returned !== 'register') {
309
+ // a guessed arity reads argument registers from the first, which holds the hidden pointer
310
+ fail(
311
+ `cannot lift '${name}': \`${callee}\` returns ${returned.type.kind === 'struct' ? returned.type.declared : typeToString(returned.type)} through a hidden ` +
312
+ `pointer in ${target.argRegs[0]}, and its parameters are not all sized, so which registers carry its arguments is not known`,
313
+ );
314
+ }
315
+ if (params === undefined) {
316
+ return null;
317
+ }
318
+ // THE STRUCT'S LOCAL NEEDS THE CALLEE PRINTED. A self-declared candidate defines the struct only
319
+ // beside the callee's printed prototype (`declare.ts`), so a declaration the printer cannot spell
320
+ // (`double *`, `size_t`, or a bare parameter count, which states no type) lifts to a candidate
321
+ // that does not compile. Asked of the printer's own predicate, so the two cannot disagree; a
322
+ // headers world, which would declare the callee itself, loses the lift with it.
323
+ if (returned !== undefined && returned !== 'register' && spellableProto(own, target, returned.type) === undefined) {
324
+ const via = `\`${callee}\` returns ${returned.type.kind === 'struct' ? returned.type.declared : typeToString(returned.type)} through a hidden pointer in ${target.argRegs[0]}`;
325
+ if (typeof own!.params === 'number') {
326
+ fail(
327
+ `cannot lift '${name}': ${via}, and its declaration states only a count of parameters, no type the lifted source can declare it with`,
328
+ );
329
+ }
330
+ fail(
331
+ `cannot lift '${name}': ${via}, and a parameter type of its declaration has no spelling the lifted source can declare it with`,
332
+ );
333
+ }
334
+ const hidden = returned === undefined || returned === 'register' ? 0 : 1;
335
+ const widths = hidden === 0 ? params : [32, ...params];
336
+ const doubles = new Set([...declared!.doubles].map((i) => i + hidden));
337
+ return {
338
+ widths,
339
+ doubles,
340
+ params: params.length,
341
+ ...(returned === undefined ? {} : { returned }),
342
+ };
343
+ };
344
+ // ONE CALL, IN THE ORDER ITS QUESTIONS REFUSE: a struct the lowering cannot receive and a float,
345
+ // then the runtime helper, whose signature is its compiler's, so no declaration is asked for one;
346
+ // the declaration's own refusals come next, then the return.
347
+ const plan = (callee: string): CallPlan => {
348
+ const own = Object.hasOwn(prototypes, callee) ? prototypes[callee] : undefined;
349
+ // One returned through memory is handed a hidden pointer in argument 0, with every argument one
350
+ // register up. One returned in registers takes its arguments where they are declared.
351
+ if (
352
+ !lowering.memoryReturn &&
353
+ declaresAggregateReturn(own) &&
354
+ returnsInMemory(returnedAggregate(own!), target) !== false
355
+ ) {
356
+ fail(
357
+ `cannot lift '${name}': '${callee}' is declared to return ${own?.returns ?? 'a struct or union'} by value — ` +
358
+ 'a struct returned through a hidden pointer, or one nothing here can size, is not modelled',
359
+ );
360
+ }
361
+ // A FLOAT CROSSES A CALL IN THE FPU'S REGISTERS where the target has one (`TargetDescription.fpu`),
362
+ // and a call here passes and reads general registers alone.
363
+ if (target.fpu !== undefined && own !== undefined) {
364
+ if (typeof own.returns === 'string' && isFloatingSpelling(own.returns)) {
365
+ fail(
366
+ `cannot lift '${name}': '${callee}' is declared to return ${own.returns}, which comes back in ` +
367
+ `${target.fpu.returnReg} — the floating-point registers a call passes and returns in are not modelled`,
368
+ );
369
+ }
370
+ const params = Array.isArray(own.params) ? own.params : [];
371
+ const at = params.findIndex(isFloatingSpelling);
372
+ if (at >= 0) {
373
+ fail(
374
+ `cannot lift '${name}': '${callee}' is declared to take ${params[at]} as its ` +
375
+ `parameter ${at + 1}, which travels in the FPU's registers — the floating-point registers a call ` +
376
+ 'passes and returns in are not modelled',
377
+ );
378
+ }
379
+ }
380
+ const helper = wideHelper(callee);
381
+ // WITHOUT PAIRS A 64-BIT HELPER IS AN UNDECLARED CALL. Its table signature passes a pair, which
382
+ // such a lowering cannot build; guessed, the call reaches `raise/widehelpers.ts`, which gaps it
383
+ // by name.
384
+ const wide = lowering.pairs ? helper : null;
385
+ const declared = helper ? null : declaredCall(callee);
386
+ if (!lowering.pairs && declared !== null) {
387
+ // A PARAMETER WIDER THAN A REGISTER TRAVELS IN A PAIR. A lowering that passes every argument
388
+ // register as its own value would honour the declaration by handing the callee one half.
389
+ const wideAt = declared.widths.findIndex((w) => w > 32);
390
+ if (wideAt >= 0) {
391
+ fail(
392
+ `cannot lift '${name}': one half of a 64-bit value would be handed to '${callee}' — its parameter ` +
393
+ `${wideAt + 1} is declared wider than a register, and this frontend passes each argument ` +
394
+ 'register as its own value rather than building the pair the ABI passes it in',
395
+ );
396
+ }
397
+ }
398
+ if (!lowering.stackArgs && declared !== null && wordsOf(declared.widths) > target.argRegs.length) {
399
+ fail(
400
+ `cannot lift '${name}': outgoing stack arguments not modelled — '${callee}' is declared with ` +
401
+ `${declared.widths.length} parameters and the argument registers carry ${target.argRegs.length}, so the rest ` +
402
+ `travel in its parameter area on the stack`,
403
+ );
404
+ }
405
+ const pair = helper !== null && !lowering.pairs ? false : returnsPair(callee);
406
+ // THE SAME RULE ON THE WAY BACK. The declaration reaches the candidate whether or not the
407
+ // lowering can act on it (`l3/symbol-refs.ts` prints `long long g(void);`), so reading the
408
+ // return register alone would lift `return g();` off one half under a declaration that makes
409
+ // it mean the whole value.
410
+ if (pair && !lowering.pairs) {
411
+ fail(
412
+ `cannot lift '${name}': '${callee}' would hand back one half of a 64-bit value — its return is ` +
413
+ 'declared wider than a register, and this frontend reads the return register as the whole ' +
414
+ 'value rather than building the pair the ABI hands back',
415
+ );
416
+ }
417
+ // a struct returned through memory is the call's value, and argument 0 is where it lands; one
418
+ // returned in registers is their bytes, whether or not anything states the call's arity
419
+ const returned = declared?.returned ?? (declared === null && !wide ? registerStructReturn(callee) : undefined);
420
+ // two answers to which registers hold the result, and to whether argument 0 is a hidden pointer
421
+ if (pair && returned !== undefined) {
422
+ fail(`cannot lift '${name}': \`${callee}\` is declared to return both a struct or union and a 64-bit value`);
423
+ }
424
+ // a runtime helper's return is its table's, whatever a project declares on its name
425
+ const declaredVoid = !isRuntimeHelperName(callee) && declaresVoidReturn(own);
426
+ const voided = lowering.voidReturn && declaredVoid;
427
+ return {
428
+ widths: wide?.params ?? declared?.widths ?? null,
429
+ doubles: declared?.doubles ?? new Set(),
430
+ returns: voided
431
+ ? { kind: 'void' }
432
+ : pair
433
+ ? { kind: 'pair' }
434
+ : returned === undefined
435
+ ? { kind: 'word' }
436
+ : returned === 'register'
437
+ ? { kind: 'register-struct' }
438
+ : { kind: 'memory-struct', type: returned.type },
439
+ // a struct leaves the return register holding nothing the caller may read as a value, and a
440
+ // void callee leaves it holding whatever the callee did
441
+ clobbers: pair
442
+ ? pairReturnClobbers
443
+ : returned === undefined && !voided
444
+ ? callClobbers
445
+ : [...callClobbers, target.returnReg],
446
+ declaredVoid,
447
+ };
448
+ };
449
+ // THE CALL A PLAN LOWERS TO, in one order on every ISA: the arity, each argument word, the
450
+ // `call` op, the guessed arity's record, the result and the clobbers.
451
+ const lower = (site: CallSite): void => {
452
+ const { ssa, bi, pairs, guess, highHalves } = site;
453
+ if (lowering.pairs !== (pairs !== undefined) || lowering.stackArgs !== (site.stackWord !== undefined)) {
454
+ throw new Error(`target '${target.id}': a call site's pairs and stack words must be what its lowering states`);
455
+ }
456
+ const indirect = typeof site.callee === 'string' ? undefined : site.callee;
457
+ const callee = typeof site.callee === 'string' ? site.callee : undefined;
458
+ // A CALL THROUGH ARGUMENT REGISTER rN PASSES r0..r(N-1) where the lowering states it
459
+ // (`CallLowering.argRegisterBoundsArity`): N bounds the arity, and reading every register below
460
+ // it passes what the machine passes. An untouched one is this function's own argument passed
461
+ // on; one a call destroyed names nothing, and its read refuses (`SsaBuilder.finish`). Through
462
+ // any other register, or on a lowering that does not state it, the arity is guessed.
463
+ //
464
+ // KNOWN GAP: N is only a bound. agbcc's address lands above r(argc) when an argument register
465
+ // still holds a live temp, and a pointer that arrived as an argument stays where it arrived
466
+ // (`void f(int x, void (*g)(void)) { g(); }` calls through r1, the bytes `g(x)` compiles to), so
467
+ // the registers in between read as arguments the source never passed. Only the pointer's
468
+ // declared type decides the arity, and nothing reads one.
469
+ const bound =
470
+ indirect === undefined || !lowering.argRegisterBoundsArity ? -1 : target.argRegs.indexOf(indirect.reg);
471
+ const p: CallPlan =
472
+ indirect === undefined
473
+ ? plan(callee!)
474
+ : {
475
+ widths: bound < 0 ? null : Array.from({ length: bound }, () => 32),
476
+ doubles: new Set(),
477
+ returns: { kind: 'word' },
478
+ clobbers: callClobbers,
479
+ declaredVoid: false,
480
+ };
481
+ // ONE LIST OF PARAMETER WIDTHS, FROM WHICHEVER SOURCE STATES THEM — the compiler's own
482
+ // runtime table or the project's headers. Both answer the same question, so the walk that
483
+ // reads argument registers off the answer is written once; two walks would be two chances
484
+ // for the pairing rule and the arity rule to disagree.
485
+ //
486
+ // A DECLARATION HOLDING A SPELLING NOTHING CAN SIZE STATES NO LAYOUT (`proto.ts`
487
+ // `declaredCallArgs`), so `widths` is null for it and this falls to the guess below —
488
+ // the same answer the callee would get with no prototype at all.
489
+ const { widths, returns } = p;
490
+ const argc =
491
+ widths === null
492
+ ? fallbackArgc(ssa, target.argRegs, bi, {
493
+ accept: (v) => !highHalves?.has(v),
494
+ gap: guess && { name, at: guess.at, fail },
495
+ })
496
+ : wordsOf(widths);
497
+ if (widths === null) {
498
+ guess?.refuse(argc);
499
+ }
500
+ // ARGUMENT WORD `k`: a register, or past the registers the word the frontend's ABI places
501
+ // there. A guess never exceeds `argRegs.length`, so a stack word can only come from a stated
502
+ // width.
503
+ const word = (k: number): Value => (k < target.argRegs.length ? site.read(target.argRegs[k]) : site.stackWord!(k));
504
+ // A GUESSED arity reads argument registers to ASK whether the caller set them up, and
505
+ // `finish()` answers by dropping the ones a call has been through; a STATED width asserts
506
+ // they exist, so a destroyed register read for it is a wrong value nothing retracts. A
507
+ // guess is a list of single words by construction — `fallbackArgc` counts registers.
508
+ const guessed = (k: number): Value => {
509
+ const r = target.argRegs[k];
510
+ const v = ssa.readGuessedArg(r, bi);
511
+ return highHalves ? highHalves.guardRead(name, r, v) : v;
512
+ };
513
+ const args: Value[] = [];
514
+ let k = 0;
515
+ for (const w of widths ?? Array.from({ length: argc }, () => 32)) {
516
+ // A 64-BIT PARAMETER IS TWO ARGUMENT WORDS AND ONE VALUE, so the pair is built here
517
+ // rather than recovered from two 32-bit arguments later — `contracts.ts` would fire on
518
+ // the second reading anyway, since the structurer materialises an effectful call once
519
+ // per result. Its words are wherever the frontend's ABI placed them.
520
+ args.push(w > 32 ? pairs!.fuse(word(k), word(k + 1)) : widths === null ? guessed(k) : word(k));
521
+ k += w > 32 ? 2 : 1;
522
+ }
523
+ // A 64-BIT VALUE MAY NOT LEAVE AS A WORD WHERE NOTHING SAYS HOW WIDE THE PARAMETER IS,
524
+ // and this is the refusal — GUARDED ON `widths === null`, which is the whole of the
525
+ // condition. A guessed arity counts argument registers, so a caller that computes a pair
526
+ // and a caller that computes two words set up the same two registers: passing the low
527
+ // half alone invents a truncation the asm never wrote, passing both halves as two words
528
+ // invents an argument. Both recompile to the very call being lifted, so the differ
529
+ // scores them exactly as it scores the right answer and nothing downstream can referee
530
+ // either.
531
+ //
532
+ // A STATED WIDTH IS THE DISAMBIGUATION AND IT IS ONE WHETHER IT SAYS 64 OR 32. A stated
533
+ // 64 built the pair in the walk above. A stated 32 says the callee takes a word, so
534
+ // handing it a half is the narrowing the header authorises — `void sink(int)` against
535
+ // `sink((int)(a * b))` — and refusing it here would contradict a fact the user supplied.
536
+ //
537
+ // SO THE MESSAGE IS ABOUT THE WIDTH AND NOT ABOUT A PROTOTYPE, because a supplied one
538
+ // reaches here too: a typed list holding a spelling `declaredWidth` cannot size states
539
+ // no layout at all (`proto.ts` `declaredCallArgs`), and a bare COUNT states argument
540
+ // registers rather than widths. Both leave `widths` null with a `--proto` on the command
541
+ // line, and blaming an absent prototype would be false about its own input.
542
+ if (widths === null && pairs !== undefined) {
543
+ // no prototype keys a call through a register, so the hint names one only for a named callee
544
+ const theCall = indirect === undefined ? `the call to '${callee}'` : `the call through ${indirect.reg}`;
545
+ const itsParams = indirect === undefined ? `'${callee}'s parameters` : "the function pointer's parameters";
546
+ for (const [j, v] of args.entries()) {
547
+ const half = pairs.halfOf.get(v);
548
+ // A DOUBLE THE RUNTIME RETURNED IS NO long long, so the hint below would send the
549
+ // reader to the wrong declaration: a double leaves a soft-float helper only into
550
+ // another one, the return, or a parameter declared `double` (`raise/floathelpers.ts`),
551
+ // and a pair built here for a callee declared to take a `long long` is refused there.
552
+ const producer = half && pairCallee.get(half.whole);
553
+ if (half && producer && isFloatHelperName(producer)) {
554
+ fail(
555
+ `cannot lift '${name}': argument ${j + 1} of ${theCall} is the ` +
556
+ `${half.half === 'lo' ? 'low' : 'high'} half of a 64-bit value, the double '${producer}' ` +
557
+ `returned, and nothing states how wide ${itsParams} are. A double is ` +
558
+ "modelled into the runtime's arithmetic helpers, the return and a parameter a prototype " +
559
+ 'declares `double`, so a runtime compare or conversion declines' +
560
+ (indirect === undefined
561
+ ? '; a callee that takes a double, or fewer arguments than its registers suggest, lifts ' +
562
+ `once a prototype states them (\`{"${callee}": {"params": [...]}}\`)`
563
+ : ''),
564
+ );
565
+ }
566
+ if (half) {
567
+ fail(
568
+ `cannot lift '${name}': argument ${j + 1} of ${theCall} is the ` +
569
+ `${half.half === 'lo' ? 'low' : 'high'} half of a 64-bit value, and nothing states ` +
570
+ `how wide ${itsParams} are, so a pair cannot be told from two ordinary arguments` +
571
+ (indirect === undefined
572
+ ? `. A typed prototype states it (\`{"${callee}": {"params": ` +
573
+ '["long long", …]}}`); a count, or a list holding a spelling asmlift cannot size, does not'
574
+ : ' — not modelled'),
575
+ );
576
+ }
577
+ }
578
+ }
579
+ const sret = returns.kind === 'memory-struct' ? returns.type : undefined;
580
+ const res = mkValue(sret ?? T.unk(returns.kind === 'pair' ? 64 : 32));
581
+ // A DECLARED `double` IS TWO WORDS THAT ARE NOT A `long long`: its first word holds the
582
+ // sign and exponent (`TargetDescription.doubleArgWords`), so the pair read as an integer
583
+ // spells a different number — `g(1.5)` as `g(1073217536, 0)`. The call names the operands
584
+ // that are doubles, and `raise/floathelpers.ts` retypes each or refuses it.
585
+ const doubles = p.doubles.size ? [...p.doubles] : undefined;
586
+ const call = mkOp('call', {
587
+ operands: indirect === undefined ? args : [...args, indirect.address],
588
+ results: [res],
589
+ attrs: {
590
+ ...(callee !== undefined ? { target: callee } : { indirect: true }),
591
+ ...(sret === undefined ? {} : { sret: true }),
592
+ ...(doubles === undefined ? {} : { doubles }),
593
+ },
594
+ });
595
+ ssa.irBlocks[bi].ops.push(call);
596
+ // A GUESSED arity is revisited in `finish()`: only once the whole function is lifted is it
597
+ // known whether every path to here passes through another call, which would have clobbered
598
+ // the argument registers this guess just read.
599
+ //
600
+ // A CALL THROUGH A REGISTER IS PASSED AN EARLIER CALLEE'S RESULT its guess reads, which the trim
601
+ // drops from a named call (`trimClobberedCallArgs`): no declaration checks a call through a cast
602
+ // to an unprototyped type, so a dropped argument would never be refused, and `p(g())` would lift
603
+ // as `g(); p();`, which compiles to the same bytes. Kept, it lifts as `p(g())`, which passes what
604
+ // the machine passes — unless `g` is declared void, and its result names nothing.
605
+ if (widths === null) {
606
+ ssa.recordGuessedCall(call, bi, target, indirect === undefined ? undefined : (r) => !voidResults.has(r));
607
+ }
608
+ if (p.declaredVoid) {
609
+ voidResults.add(res);
610
+ }
611
+ if (returns.kind === 'pair') {
612
+ // A PAIR RETURN IS ONE VALUE, SPLIT. The callee defines BOTH registers, so both are
613
+ // named here and neither is in the clobber set — which is the acceptance arm of the
614
+ // very rule whose refusal arm `frontend/ssa.ts` applies to every other register.
615
+ site.write(target.returnReg, pairs!.project(res, 'lo'));
616
+ site.write(target.argRegs[1], pairs!.project(res, 'hi'));
617
+ if (callee !== undefined) {
618
+ pairCallee.set(res, callee);
619
+ }
620
+ } else if (returns.kind === 'word') {
621
+ site.write(target.returnReg, res); // the callee defines the return register …
622
+ }
623
+ // … and the clobber is recorded after it, so that def is the CALLEE's; a struct, the memory at
624
+ // argument 0 or the return register's bytes, leaves that register holding nothing the caller
625
+ // may read as a value
626
+ ssa.noteCall(bi, p.clobbers);
627
+ };
628
+ return {
629
+ callClobbers,
630
+ returnsWithoutHiddenPointer: (callee) => returnsWithoutHiddenPointer(callee, prototypes, target),
631
+ declaredCall,
632
+ plan,
633
+ lower,
634
+ };
635
+ }
636
+
637
+ // A CALLEE DECLARED TO RETURN A STRUCT OR UNION BY VALUE, where the target returns it through
638
+ // memory: the caller hands it the storage in argument 0 and every declared argument one register
639
+ // up (on agbcc, thumb.h:644-645, 672), so the call's first word is that pointer. What it returns in
640
+ // the return register is not a value the caller reads (calls.c: the struct is the memory at the
641
+ // address). One the target returns in registers takes its arguments where they are declared, and
642
+ // its registers hold the struct's bytes, which nothing here reads as a struct: the call is
643
+ // `'register'` and a read of the return register after it refuses. Every other struct-returning
644
+ // call refuses, naming why.
645
+ function structReturnOf(
646
+ name: string,
647
+ target: TargetDescription,
648
+ callee: string,
649
+ own: FnProto,
650
+ fail: FrontendRefusal,
651
+ ): StructReturn | 'register' {
652
+ const refuse = (why: string): never =>
653
+ fail(
654
+ `cannot lift '${name}': \`${callee}\` is declared to return ${own.returns ?? 'a struct or union'} by value, and ${why}`,
655
+ );
656
+ const layout = returnedAggregate(own);
657
+ const inMemory = returnsInMemory(layout, target);
658
+ if (inMemory === undefined) {
659
+ refuse(
660
+ 'nothing here says whether it comes back through a hidden pointer — its members are not all known, this target does not ' +
661
+ 'say how it lays one of them out (an enum, a bitfield), or it states no rule ' +
662
+ '(a prototype states them as `returnLayout`, a context by defining the struct)',
663
+ );
664
+ }
665
+ if (inMemory === false) {
666
+ return 'register';
667
+ }
668
+ // the local it lands in is declared as the header spells the type, qualifiers aside; the tag
669
+ // is what the declarations block defines it by, and a typedef name is that tag too
670
+ const spelling = (own.returns ?? '')
671
+ .replace(/\b(?:const|volatile)\b/g, ' ')
672
+ .trim()
673
+ .replace(/\s+/g, ' ');
674
+ const tag = /^(?:(?:struct|union)\s+)?([A-Za-z_]\w*)$/.exec(spelling)?.[1];
675
+ if (tag === undefined) {
676
+ refuse('it names no type a local of it could be declared with');
677
+ }
678
+ const type = aggregateType(tag!, spelling, layout, target);
679
+ if (type === undefined) {
680
+ refuse(
681
+ 'it is a union, or its members are not all known or this target does not say how it lays one of them out ' +
682
+ '(an enum, a bitfield) — the local it lands in has no type here',
683
+ );
684
+ }
685
+ return { type: type! };
686
+ }