@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,374 @@
1
+ // asmlift — the argument ABI at a function's boundary, both register files, read once for the two
2
+ // frontends that lift hardware floating point (frontend/mips.ts, frontend/ppc.ts). The homes are
3
+ // target data (`TargetDescription.fpu`); this is where a lift is held to them.
4
+ //
5
+ // WHY A FRONTEND NEEDS THIS AT ALL, and why decoding the arithmetic alone is not a smaller version
6
+ // of it: a register of the FPU's file read before any write mints an entry parameter like any other
7
+ // key (frontend/ssa.ts), and a value left in the float return register is not the integer one. With
8
+ // the decode and neither of these, `float fadd(float a, float b){ return a + b; }` lifts on every
9
+ // FPU target as `void fadd(s32 a0, s32 a1) { return; }` — two phantom integer parameters and a dead
10
+ // add. `docs/floating-point.md` §2 measures that.
11
+ import type { Fn, Op, Value } from '../ir/core';
12
+ import { FLOAT_OPS, FLOAT_TO_INT, INT_TO_FLOAT, NEGATED_FCMP } from '../ir/opcodes';
13
+ import { pruneDeadParams } from '../ir/simplify';
14
+ import { type IrType, T } from '../ir/types';
15
+ import { declaresAggregateReturn, declaresVoidReturn, floatingSpelling, isFloatingSpelling } from '../proto';
16
+ import type { TargetDescription } from '../target';
17
+ import { computedOnlyToReturn } from './dead-reads';
18
+ import { FrontendUnsupportedError } from './errors';
19
+ import type { OwnDeclaration } from './frontend';
20
+ import type { ArgSlots, SsaBuilder } from './ssa';
21
+
22
+ export type Fpu = NonNullable<TargetDescription['fpu']>;
23
+
24
+ /** The one float type a function's FPU registers hold: double where it decodes a double op, single
25
+ * where it decodes a single one, of no stated precision (`T.fUnstated`) where every op it decodes
26
+ * carries none, and null where it decodes both. `widthOf` is the frontend's reading of a mnemonic
27
+ * (null for one that carries no precision, undefined for one it does not decode). */
28
+ export function fpPrecision(
29
+ instrs: readonly { mnemonic: string }[],
30
+ widthOf: (mnemonic: string) => 32 | 64 | null | undefined,
31
+ ): IrType | null {
32
+ const widths = new Set(instrs.map((ins) => widthOf(ins.mnemonic)).filter((w) => w !== null && w !== undefined));
33
+ return widths.size > 1 ? null : widths.has(64) ? T.f64() : widths.has(32) ? T.f32() : T.fUnstated();
34
+ }
35
+
36
+ /** The precision a function's OWN DECLARATION states for the floats it computes on, where none of
37
+ * its instructions states one (`T.fUnstated`): `double` where every floating parameter and a
38
+ * floating return are spelled `double`. PowerPC's `fcmpo`, `fctiwz`, `fmr` and `fneg` are one
39
+ * object over a float and a double, so `int d2i(double x){ return (int)x; }` is the bytes of the
40
+ * `float` function too, and only a declaration that may prove (`OwnDeclaration.proves`) tells them
41
+ * apart. A declaration spelling both is a function in both precisions, null, which refuses where a
42
+ * float instruction is decoded (`refuseBothPrecisions`). Any other leaves the precision unstated,
43
+ * which C spells `float`: a `float` parameter of an old-style definition arrives as a double. */
44
+ export function declaredPrecision(lifted: IrType | null, own: OwnDeclaration | undefined): IrType | null {
45
+ if (lifted?.kind !== 'float' || lifted.width !== null || !own?.proves) {
46
+ return lifted;
47
+ }
48
+ const { params, returns } = own.proto;
49
+ const spelled = [...(Array.isArray(params) ? params : []), ...(returns === undefined ? [] : [returns])]
50
+ .map(floatingSpelling)
51
+ .filter((f) => f !== undefined);
52
+ if (spelled.length === 0 || spelled.some((f) => f === 'long double')) {
53
+ return lifted;
54
+ }
55
+ return spelled.every((f) => f === 'double') ? T.f64() : spelled.every((f) => f === 'float') ? lifted : null;
56
+ }
57
+
58
+ /** Refuse a function computing in both precisions, where the frontend DECODES a float instruction of
59
+ * it, so a refusal the stream reaches first keeps its own reason. The SSA builder types a register
60
+ * per key, not per value, and the rounding between the precisions (a conversion, PowerPC's `frsp`,
61
+ * or a single op over a double operand) is not modelled. It throws here, so its throw site is the
62
+ * one `declines.test.ts` harvests. */
63
+ export function refuseBothPrecisions(name: string, mnemonic: string): never {
64
+ throw new FrontendUnsupportedError(
65
+ `cannot lift '${name}': '${mnemonic}' is in a function that computes in ` +
66
+ 'both single and double precision, and the rounding between them is not modelled',
67
+ );
68
+ }
69
+
70
+ /** THE ARGUMENT SLOTS OF BOTH REGISTER FILES, as the one `ArgSlots` the MIPS and PowerPC frontends
71
+ * hand to `mintArgSlotHoles` and `abiSortEntryParams` (frontend/ssa.ts). Naming is positional
72
+ * (`a0`, `a1`, … in sort order), so the ORDER `slotOf` gives and the HOLES `holes` fills are two
73
+ * uses of one reading — kept in one place because under `'leading'` the files share a slot
74
+ * sequence, and a hole filled from the wrong file is a signature that compiles and binds its
75
+ * caller's arguments one slot off.
76
+ *
77
+ * `slotOf` is the slot, with one exception: under `'separate'` a float argument ranks after every
78
+ * integer one. THAT ORDER IS A SPELLING, NOT A READING. Under the PowerPC EABI the two files count
79
+ * independently, so `float g(int *p, float b)` and `float g(float b, int *p)` compile to one object
80
+ * and nothing in it says which the source wrote. Either spelling reproduces the bytes in a unit
81
+ * that does not declare the function; this one is fixed so the output is deterministic. KNOWN GAP:
82
+ * a unit that does declare it — a project context — decides the order, and only the C++ backend
83
+ * reads that declaration (`bindSpecParams`). In C the int-first spelling of
84
+ * `float mix(float a, int n);` is a redeclaration mwcc refuses: a noncompile, not a wrong program.
85
+ * Ranking by the function's own declared parameters here would close it, once `proto.ts` can
86
+ * size a float. A key in neither file is no slot (null), and sorts after every argument.
87
+ *
88
+ * `holes` applies FOUR RULES to the keys the entry reads, each a refusal or a parameter the ABI
89
+ * proves:
90
+ * 1. A register of the FPU file that arrives at the entry is a float ARGUMENT, or the lift is
91
+ * refused. Nothing else in that file carries a caller's value: a read of `$f4` before any write
92
+ * is uninitialised storage or code this frontend has not followed, and minting a parameter for
93
+ * it would fabricate an argument.
94
+ * 2. Every slot below the highest one read is a parameter too, read or not. Arguments take their
95
+ * registers in order, so `r5` is the third argument whatever became of the first two, and a
96
+ * signature holding only the registers read would bind every later argument one slot low.
97
+ * 3. …and it is minted from the file the ABI puts that slot in. Under `'separate'` each file fills
98
+ * its own holes. Under `'leading'` (MIPS o32) a float argument is in `argRegs[k]` only while
99
+ * arguments 0..k are all floating, so a hole below the highest float read is a float, and one
100
+ * above it an integer — `float f(float a, float b, int c, int d){ return d ? a : b; }` reads
101
+ * `$f12`, `$f14` and `a3`, and its third argument is `a2`. Which file a hole comes from thus
102
+ * depends on what the function reads, which is why `ArgSlots` asks for the holes of a read
103
+ * set rather than for the key of a slot: `int ib(int a, int b){ return b; }` reads only `a1`,
104
+ * and its first argument is `a0`, not `$f12`.
105
+ * 4. Under `'leading'` a float argument still takes the INTEGER slots it shadows — one for a
106
+ * single, two for a double (`floatType`) — so an integer argument register read in one of those
107
+ * slots is a contradiction the ABI does not produce, and is refused rather than ranked. */
108
+ export function fpuArgSlots(
109
+ name: string,
110
+ fpu: Fpu | undefined,
111
+ argRegs: readonly string[],
112
+ isFpKey: (key: string) => boolean,
113
+ floatType: IrType | null = T.f32(),
114
+ ): ArgSlots {
115
+ // How many integer slots a float argument takes under `'leading'`: IDO reads the integer after a
116
+ // double two slots on (`double di(double a, int b)` reads b from a2).
117
+ const words = floatType?.kind === 'float' && floatType.width === 64 ? 2 : 1;
118
+ const slotOf = (key: string): { slot: number; float: boolean } | null => {
119
+ const gpr = argRegs.indexOf(key);
120
+ if (gpr >= 0) {
121
+ return { slot: gpr, float: false };
122
+ }
123
+ const fp = fpu?.argRegs.indexOf(key) ?? -1;
124
+ return fp >= 0 ? { slot: fpu?.slots === 'leading' ? fp * words : fp, float: true } : null;
125
+ };
126
+ return {
127
+ slotOf: (key) => {
128
+ const s = slotOf(key);
129
+ return s === null ? null : s.float && fpu?.slots === 'separate' ? argRegs.length + s.slot : s.slot;
130
+ },
131
+ holes: (readKeys) => {
132
+ for (const k of readKeys.filter(isFpKey)) {
133
+ if (!fpu?.argRegs.includes(k)) {
134
+ throw new FrontendUnsupportedError(
135
+ `cannot lift '${name}': ${k} is read before this function writes it, and no floating-point argument ` +
136
+ `arrives there — not modelled`,
137
+ );
138
+ }
139
+ }
140
+ const read = readKeys.map(slotOf).filter((s) => s !== null);
141
+ const top = (float: boolean) => Math.max(-1, ...read.filter((s) => s.float === float).map((s) => s.slot));
142
+ const floatTop = top(true);
143
+ const intTop = top(false);
144
+ if (fpu?.slots === 'leading') {
145
+ // The last integer slot the floats read shadow; a double shadows two.
146
+ const floatEnd = floatTop < 0 ? -1 : floatTop + words - 1;
147
+ const shadowed = read.find((s) => !s.float && s.slot <= floatEnd);
148
+ if (shadowed) {
149
+ const k = shadowed.slot;
150
+ throw new FrontendUnsupportedError(
151
+ `cannot lift '${name}': ${argRegs[k]} and ${fpu.argRegs[Math.floor(k / words)]} both carry argument ` +
152
+ `slot ${k} — a floating-point argument takes the integer slots it shadows, so the ABI does not ` +
153
+ 'produce this — not modelled',
154
+ );
155
+ }
156
+ return [
157
+ ...fpu.argRegs.slice(0, Math.max(0, floatTop / words)),
158
+ ...argRegs.slice(floatEnd + 1, Math.max(floatEnd + 1, intTop)),
159
+ ];
160
+ }
161
+ return [...(fpu?.argRegs.slice(0, Math.max(0, floatTop)) ?? []), ...argRegs.slice(0, Math.max(0, intTop))];
162
+ },
163
+ };
164
+ }
165
+
166
+ /** Whether a function RETURNS a float, where no float reaches the integer file: some instruction the
167
+ * frontend decodes writes the float return register, in a block the entry REACHES. Decided over the
168
+ * whole function, before any `ret` is emitted, because a function has one return type on every
169
+ * path — a path that leaves the register untouched returns the float argument that arrived there
170
+ * (PowerPC's `f1` is both), or refuses by `fpuArgSlots`' rule 1. Reachable blocks only: `addi
171
+ * r3,r3,1; blr; fmr f1,f2` returns `r3`, and the `fmr` past the `blr` is on no path of it.
172
+ *
173
+ * And then the INTEGER return register is scratch, even where the function writes it. Both halves
174
+ * rest on one fact: in such a function a float reaches nothing but another float op or the return
175
+ * — every move to memory is refused. A write to the float return register that did not feed the
176
+ * return would therefore be dead in the source, and the compiler does not emit dead arithmetic.
177
+ * IDO's unrolled `float pw(float a, int n)` counts in `v0`.
178
+ *
179
+ * THE FACT FAILS ONCE A FLOAT REACHES THE INTEGER FILE through a compare or a conversion: a float
180
+ * temporary in the home then feeds the compare, not the return (`void vb(float a, float b, int *p)
181
+ * { float c = a * b; if (c < a) *p = 1; }` multiplies into `$f0` on KMC). Such a function is
182
+ * settled once lifted (`settleReturnHome`), never by this. A layer that lets a float reach MEMORY
183
+ * removes the fact for every function, and must settle them all that way (`fpu-lift.test.ts` pins
184
+ * the function that breaks it, `docs/floating-point.md` §6). */
185
+ export function writesFloatReturn(
186
+ instrs: readonly { mnemonic: string; ops: string[] }[],
187
+ decodes: ReadonlySet<string>,
188
+ destKey: (tok: string) => string | null,
189
+ fpu: Fpu | undefined,
190
+ ): boolean {
191
+ return (
192
+ fpu !== undefined &&
193
+ instrs.some((ins) => decodes.has(ins.mnemonic) && ins.ops[0] !== undefined && destKey(ins.ops[0]) === fpu.returnReg)
194
+ );
195
+ }
196
+
197
+ /** Which home a function returns in, as far as can be said BEFORE its blocks are filled: the float
198
+ * home, the integer one (which covers a `void` function), or `undecided`, which `settleReturnHome`
199
+ * answers once the lift is done. The function's OWN DECLARATION decides first, where it may prove
200
+ * (`OwnDeclaration.proves`): a floating return type is the float home, any other return the integer
201
+ * one. Without it, a function where no float reaches the integer file (`crosses` false) is
202
+ * decided by `writesFloatReturn`. One where a float does is undecided: a write to the float home
203
+ * no longer says it is the result, and a value in the integer home may be one computed from a
204
+ * float. */
205
+ export interface ReturnHomeRule {
206
+ readonly home: 'float' | 'integer' | 'undecided';
207
+ /** whether the own declaration decided it */
208
+ readonly declared: boolean;
209
+ }
210
+ export function returnHomeRule(
211
+ own: OwnDeclaration | undefined,
212
+ crosses: boolean,
213
+ writesFloat: () => boolean,
214
+ ): ReturnHomeRule {
215
+ const p = own?.proves ? own.proto : undefined;
216
+ if (p !== undefined && (declaresVoidReturn(p) || declaresAggregateReturn(p) || p.returns !== undefined)) {
217
+ return { home: p.returns !== undefined && isFloatingSpelling(p.returns) ? 'float' : 'integer', declared: true };
218
+ }
219
+ return { home: crosses ? 'undecided' : writesFloat() ? 'float' : 'integer', declared: false };
220
+ }
221
+
222
+ /** What the float return home holds at a return, read WITHOUT a read: every definition that reaches
223
+ * it, and whether one does on every path (a path that brings none brings the caller's register).
224
+ * A read would leave a definition behind, and where the entry reaches it an entry parameter. Asked
225
+ * once every block is filled. */
226
+ export interface HomeAtReturn {
227
+ readonly every: boolean;
228
+ readonly defs: readonly Value[];
229
+ }
230
+ export function floatHomeAt(ssa: Pick<SsaBuilder, 'everyReachingDef'>, home: string, bi: number): HomeAtReturn {
231
+ const defs: Value[] = [];
232
+ const every = ssa.everyReachingDef(home, bi, (v) => {
233
+ defs.push(v);
234
+ return true;
235
+ });
236
+ return { every, defs };
237
+ }
238
+
239
+ /** Whether an undecided function's float home holds a FLOAT on every path into every return, so
240
+ * that `settleReturnHome` may weigh it: then the frontend reads it at each return before the SSA is
241
+ * finished, and hands it on as the `ret`'s LAST operand, where the SSA's own clean-up keeps it
242
+ * current (a join it resolves away is replaced there, and one nothing read would be pruned). */
243
+ export function carriesFloatHome(rule: ReturnHomeRule, homes: readonly HomeAtReturn[]): boolean {
244
+ return rule.home === 'undecided' && homes.every((h) => h.every && h.defs.every((v) => v.type.kind === 'float'));
245
+ }
246
+
247
+ /** SETTLE THE RETURN HOME of a lifted function, and refuse where the bytes do not decide it.
248
+ * `sites` are its returns, each reading what the INTEGER home rule gave it (the operand a `ret`
249
+ * reads where the function returns a word), plus, where `carriesFloat`, the float home's value
250
+ * last; `homes` is what the float home holds at each (`floatHomeAt`); `movedIn` the words a move
251
+ * from the integer file put in an FPU register. Run after the SSA is finished.
252
+ *
253
+ * An INTEGER WORD in the float home at a return refuses, unless the integer home proves itself
254
+ * (below) or the own declaration says the return is no float: `mtc1 $zero,$f0` is how IDO, KMC and
255
+ * GCC 2.7.2 return `0.0f`, and `mtc1 a1,$f0` how they return a float that arrived in a GPR, and
256
+ * either is the word's bits as a float, which is not modelled. KMC's `trunc.w.s $f0,$f12; mfc1
257
+ * v0,$f0` leaves a word there too, and returns it in `v0`. A word an `mtc1` moved in refuses even
258
+ * where the integer home proves itself, since that proof reads the same value. A float return whose
259
+ * `ret` reads a word refuses as well.
260
+ *
261
+ * An UNDECIDED function returns the home whose value at every return was computed only to be
262
+ * returned (frontend/dead-reads.ts `computedOnlyToReturn`): an optimizing compiler emits no write
263
+ * whose value nothing reads, so a value that survives with only a return to read it is the
264
+ * result. `int vi2(float a, float b){ float c = a + b; if (c < a) return 3; … }` adds into `$f0`,
265
+ * which the compares read, and returns the `li` in `v0`. Where the float home holds no float at
266
+ * some return, the function returns none — unless the home is an argument register (`f1`), whose
267
+ * float may be the caller's, and then the integer home must prove itself. Where neither home
268
+ * proves, or both do, the lift refuses: `void vb(float a, float b, int *p){ float c = a * b; if (c
269
+ * < a) *p = 1; }` and `float vb(…){ …; return c; }` are one object, and so are `float mx(float a,
270
+ * float b, int *p){ if (a < b) *p = 1; return a; }` and the same function returning `p`. The own
271
+ * declaration decides those (`returnHomeRule`). */
272
+ export function settleReturnHome(
273
+ name: string,
274
+ fn: Fn,
275
+ rule: ReturnHomeRule,
276
+ sites: readonly { op: Op }[],
277
+ homes: readonly HomeAtReturn[],
278
+ carriesFloat: boolean,
279
+ fpu: Fpu,
280
+ intReg: string,
281
+ movedIn: ReadonlySet<Value> = new Set(),
282
+ ): void {
283
+ const floatAt = carriesFloat ? sites.map(({ op }) => op.operands.pop()!) : undefined;
284
+ const isFloat = (v: Value) => v.type.kind === 'float';
285
+ if (rule.home === 'float') {
286
+ if (sites.some(({ op }) => !op.operands.every(isFloat))) {
287
+ throw new FrontendUnsupportedError(
288
+ `cannot lift '${name}': a return reads an integer word from ${fpu.returnReg}, the float return register`,
289
+ );
290
+ }
291
+ return;
292
+ }
293
+ // A word MOVED IN from the integer file (`mtc1 v0,$f0`) is one value with its source in the IR, so
294
+ // the integer home would seem to prove itself: `float u(float a, float b){ U u; u.i = a < b ? 1 :
295
+ // 2; return u.f; }` computes into `v0` and returns it in `$f0`. KMC's `f2i` leaves the word by
296
+ // `trunc.w` and moves it OUT, which is the other direction.
297
+ if (!rule.declared && homes.some((h) => h.defs.some((v) => movedIn.has(v)))) {
298
+ throw new FrontendUnsupportedError(
299
+ `cannot lift '${name}': ${fpu.returnReg}, the float return register, holds an integer word at a return ` +
300
+ "that an 'mtc1' moved in from the integer file — the word's bits as a float are not modelled",
301
+ );
302
+ }
303
+ const computed = computedOnlyToReturn(fn.blocks);
304
+ const intProven = sites.every(({ op }) => op.operands.length === 1 && computed(op.operands[0]));
305
+ if (!rule.declared && !intProven && homes.some((h) => !h.defs.every(isFloat))) {
306
+ throw new FrontendUnsupportedError(
307
+ `cannot lift '${name}': ${fpu.returnReg}, the float return register, holds an integer word at a return, ` +
308
+ `and ${intReg} holds no value computed only to be returned — the word's bits as a float are not modelled`,
309
+ );
310
+ }
311
+ if (rule.home === 'integer' || (floatAt === undefined && !fpu.argRegs.includes(fpu.returnReg))) {
312
+ if (floatAt !== undefined) {
313
+ pruneDeadParams(fn);
314
+ }
315
+ return;
316
+ }
317
+ const floatProven = floatAt !== undefined && floatAt.every(computed);
318
+ if (floatProven !== intProven) {
319
+ if (floatProven) {
320
+ sites.forEach(({ op }, i) => {
321
+ op.operands = [floatAt![i]];
322
+ });
323
+ }
324
+ // the home not returned may have left a join nothing reads now
325
+ pruneDeadParams(fn);
326
+ return;
327
+ }
328
+ throw new FrontendUnsupportedError(
329
+ `cannot lift '${name}': a float reaches the integer file (a compare or a conversion), and then whether it ` +
330
+ `returns ${fpu.returnReg}, ${intReg} or nothing is not decided by the bytes — ` +
331
+ (floatProven
332
+ ? 'each holds a value computed only to be returned'
333
+ : 'neither holds a value computed only to be returned') +
334
+ ', and no declaration of its own says',
335
+ );
336
+ }
337
+
338
+ /** Refuse a function where an FPU register's INTEGER WORD meets a float, or its float meets an
339
+ * integer, across a block boundary. Within a block each read is checked as it is decoded; a read
340
+ * that reaches an earlier block comes back as a block parameter, which the builder types as the
341
+ * file's float before it knows what flows in (`makeSsaBuilder`'s `keyType`). So the check runs
342
+ * again over the finished function: a float op, a compare or a conversion out of a float over
343
+ * anything but a float, a conversion into one over a float, and an edge passing a word to a float
344
+ * parameter or the reverse. `float g(float a, float b, float c)` on O32 hands `c` over in `a2`,
345
+ * which `mtc1` moves in as a word: computing on it as a float is a bit cast this does not model. */
346
+ export function refuseFpWordCrossings(name: string, fn: Fn): void {
347
+ const fail = (what: string): never => {
348
+ throw new FrontendUnsupportedError(
349
+ `cannot lift '${name}': ${what} — an FPU register holds a float or an integer word, and this one meets the other`,
350
+ );
351
+ };
352
+ const isFloat = (v: Value) => v.type.kind === 'float';
353
+ for (const b of fn.blocks) {
354
+ for (const op of b.ops) {
355
+ const takesFloats =
356
+ (FLOAT_OPS.has(op.opcode) && op.opcode !== 'fconst') ||
357
+ FLOAT_TO_INT.has(op.opcode) ||
358
+ NEGATED_FCMP[op.opcode] !== undefined;
359
+ if (takesFloats && !op.operands.every(isFloat)) {
360
+ fail(`'${op.opcode}' is handed an integer word`);
361
+ }
362
+ if (INT_TO_FLOAT.has(op.opcode) && op.operands.some(isFloat)) {
363
+ fail(`'${op.opcode}' is handed a float`);
364
+ }
365
+ for (const s of op.successors) {
366
+ s.args.forEach((a, i) => {
367
+ if (isFloat(a) !== (s.block.params[i]?.type.kind === 'float')) {
368
+ fail('a block boundary passes one where the other arrives');
369
+ }
370
+ });
371
+ }
372
+ }
373
+ }
374
+ }