@asmlift/core 0.9.0 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (101) hide show
  1. package/README.md +25 -24
  2. package/package.json +1 -1
  3. package/src/aggregate.ts +149 -0
  4. package/src/backend/c.ts +1 -0
  5. package/src/backend/cfamily.ts +165 -50
  6. package/src/backend/cpp.ts +2 -1
  7. package/src/backend/pascal.ts +31 -12
  8. package/src/contracts.ts +173 -16
  9. package/src/declare.ts +69 -20
  10. package/src/frontend/asmdata.ts +7 -1
  11. package/src/frontend/call-plan.ts +964 -60
  12. package/src/frontend/dead-reads.ts +628 -0
  13. package/src/frontend/device-pins.ts +23 -4
  14. package/src/frontend/emit.ts +4 -3
  15. package/src/frontend/fpu.ts +224 -17
  16. package/src/frontend/frame-objects.ts +1232 -192
  17. package/src/frontend/frontend.ts +64 -3
  18. package/src/frontend/high-half.ts +53 -4
  19. package/src/frontend/jump-table.ts +530 -0
  20. package/src/frontend/local-object.ts +158 -32
  21. package/src/frontend/mips.ts +2076 -215
  22. package/src/frontend/own-struct-return.ts +500 -0
  23. package/src/frontend/ppc.ts +2711 -232
  24. package/src/frontend/register-struct-call.ts +484 -0
  25. package/src/frontend/reloc-symbol.ts +22 -7
  26. package/src/frontend/ssa.ts +1460 -44
  27. package/src/frontend/stackargs.ts +106 -57
  28. package/src/frontend/thumb.ts +2044 -318
  29. package/src/ir/alias.ts +42 -0
  30. package/src/ir/bits.ts +681 -1
  31. package/src/ir/core.ts +100 -5
  32. package/src/ir/discipline.ts +86 -21
  33. package/src/ir/entry.ts +159 -0
  34. package/src/{structure → ir}/loops.ts +6 -2
  35. package/src/ir/opcodes.ts +109 -9
  36. package/src/ir/types.ts +86 -2
  37. package/src/ir/verify.ts +80 -11
  38. package/src/l3/ast.ts +290 -18
  39. package/src/l3/basecse.ts +33 -5
  40. package/src/l3/constinit.ts +382 -0
  41. package/src/l3/dce.ts +2 -2
  42. package/src/l3/initfirst.ts +42 -11
  43. package/src/l3/intcell.ts +2 -1
  44. package/src/l3/narrowdecl.ts +77 -36
  45. package/src/l3/pollguard.ts +8 -2
  46. package/src/l3/ptrcell.ts +31 -34
  47. package/src/l3/ptrfield.ts +123 -8
  48. package/src/l3/regspell.ts +9 -4
  49. package/src/l3/reindex.ts +283 -96
  50. package/src/l3/storage.ts +98 -1
  51. package/src/l3/symbol-refs.ts +84 -6
  52. package/src/l3/tailmerge.ts +4 -2
  53. package/src/l3/typing.ts +108 -15
  54. package/src/l3/unreduce.ts +6 -5
  55. package/src/l3/volatileptr.ts +17 -54
  56. package/src/mangle.ts +30 -0
  57. package/src/pattern/engine.ts +361 -73
  58. package/src/pipeline.ts +541 -24
  59. package/src/proto-context.ts +905 -45
  60. package/src/proto.ts +555 -40
  61. package/src/raise/aggmembers.ts +290 -0
  62. package/src/raise/arrays.ts +5 -1
  63. package/src/raise/declared-volatile.ts +207 -31
  64. package/src/raise/floathelpers.ts +254 -33
  65. package/src/raise/gvn.ts +3 -3
  66. package/src/raise/latch.ts +162 -6
  67. package/src/raise/memberarrays.ts +10 -4
  68. package/src/raise/offsetnames.ts +10 -26
  69. package/src/raise/pairparams.ts +33 -0
  70. package/src/raise/paramwidth.ts +196 -6
  71. package/src/raise/pre-recovery.ts +102 -19
  72. package/src/raise/pre-structure.ts +54 -0
  73. package/src/raise/recover.ts +12 -5
  74. package/src/raise/reroll.ts +694 -0
  75. package/src/raise/shortcircuit.ts +26 -12
  76. package/src/raise/softdiv.ts +1 -1
  77. package/src/raise/storesign.ts +153 -0
  78. package/src/raise/struct-arrays.ts +30 -11
  79. package/src/raise/structs.ts +396 -33
  80. package/src/raise/truncload.ts +4 -3
  81. package/src/raise/unthread.ts +400 -0
  82. package/src/rank-declare.ts +118 -12
  83. package/src/rank-variations.ts +89 -31
  84. package/src/rank.ts +307 -54
  85. package/src/runtime-helpers.ts +140 -22
  86. package/src/structure/analysis.ts +165 -2
  87. package/src/structure/arith-ops.ts +23 -0
  88. package/src/structure/globalaccess.ts +111 -9
  89. package/src/structure/hazards.ts +16 -64
  90. package/src/structure/local-statics.ts +145 -9
  91. package/src/structure/namecoalesce.ts +1 -1
  92. package/src/structure/pointer-spelling.ts +1106 -0
  93. package/src/structure/redundant-test.ts +1 -1
  94. package/src/structure/structure.ts +2180 -1054
  95. package/src/structure/switch-recover.ts +244 -100
  96. package/src/symbols.ts +6 -0
  97. package/src/target.ts +765 -52
  98. package/src/trace.ts +26 -8
  99. package/src/variation-definitions.ts +126 -14
  100. package/src/variation-gates.ts +2 -0
  101. package/src/variation-tokens.ts +5 -1
@@ -33,9 +33,10 @@
33
33
  *
34
34
  * A FUNCTION ACCEPTED OBJECT BY OBJECT pins every device read, and of its device stores only those
35
35
  * of the first kind (`overwritten`). A read is pinned because a plain one is lost either way:
36
- * nothing uses the `dmaRegs[2];` that ends a DMA macro, so the lift drops it outright, and a poll
37
- * is hoisted as above. Its other device stores stay plain, their qualified spelling left to
38
- * `/vol-store`'s candidate (l3/volstore.ts): pinned in the structured tree they are pinned in every
36
+ * nothing uses the `dmaRegs[2];` that ends a DMA macro, so the lift would drop it outright (the
37
+ * dead-read pin, frontend/dead-reads.ts, keeps such a read in every function), and a poll is
38
+ * hoisted as above, which only this pin prevents. Its other device stores stay plain, their
39
+ * qualified spelling left to `/vol-store`'s candidate (l3/volstore.ts): pinned in the structured tree they are pinned in every
39
40
  * variation, and the ones that home the base or un-reduce a loop refuse a qualified base, which
40
41
  * costs `synthetic:dmastride` and `synthetic:dmaptrsrc` their matches. That is also why the two
41
42
  * policies stay two rows: the one-object row's every-store pin is what keeps `threeFills`' nine
@@ -53,6 +54,7 @@
53
54
  * (target.ts `readCouldBeVolatile`): on a compiler that sign-extends a qualified narrow read in a
54
55
  * register, a sign-extending load is a plain read in the source, and qualified it recompiles to
55
56
  * another instruction sequence. */
57
+ import { type RangeCtx, type StridedRange, incomingArgs, stridedRange } from '../ir/bits';
56
58
  import type { Block, Op, Value } from '../ir/core';
57
59
  import type { SymbolMap } from '../symbols';
58
60
  import { type TargetDescription, readCouldBeVolatile } from '../target';
@@ -64,6 +66,9 @@ export interface LiteralAddresses {
64
66
  constOf(v: Value): number | undefined;
65
67
  /** the literal address `v` denotes, or undefined when this cannot say */
66
68
  literalAddrOf(v: Value): number | undefined;
69
+ /** every address `v` may denote, as a strided unsigned interval, or undefined when this cannot
70
+ * bound it */
71
+ addressRangeOf(v: Value): StridedRange | undefined;
67
72
  }
68
73
 
69
74
  /** `literalAddrOf`: `const` is the bare pool word, `gaddr` is the same word after the symbol map
@@ -128,7 +133,21 @@ export function literalAddresses(irBlocks: readonly Block[], symbols: SymbolMap
128
133
  }
129
134
  return undefined;
130
135
  };
131
- return { defOf, constOf, literalAddrOf: (v) => literalAddrOf(v) };
136
+ // `addressRangeOf`: where `literalAddrOf` names one address, this names every address a value
137
+ // may hold (ir/bits.ts `stridedRange`), a placed name being the one leaf only this module reads.
138
+ const rangeCtx: RangeCtx = {
139
+ defs: defOf,
140
+ constOf,
141
+ incoming: incomingArgs(irBlocks),
142
+ entry: new Set(irBlocks[0]?.params ?? []),
143
+ pointOf: (v) => (defOf.get(v)?.opcode === 'gaddr' ? literalAddrOf(v) : undefined),
144
+ };
145
+ return {
146
+ defOf,
147
+ constOf,
148
+ literalAddrOf: (v) => literalAddrOf(v),
149
+ addressRangeOf: (v) => stridedRange(rangeCtx, v),
150
+ };
132
151
  }
133
152
 
134
153
  /** Which device stores a policy pins, beside every device read. */
@@ -45,13 +45,14 @@ export function mkEmitKit(ops: Op[], write: (reg: string, v: Value) => void): Em
45
45
 
46
46
  /** Emit a recovered jump-table dispatch: N case successors followed by the default (LAST), with
47
47
  * the dense 0..N-1 `cases` list derived here — the single home of the case/successor
48
- * alignment invariant. */
49
- export function pushSwitchBr(ops: Op[], scrut: Value, successors: Successor[]): void {
48
+ * alignment invariant. `exhaustive`: the scrutinee is already known to be in 0..N-1 here (see
49
+ * the opcode's attr). */
50
+ export function pushSwitchBr(ops: Op[], scrut: Value, successors: Successor[], exhaustive = false): void {
50
51
  ops.push(
51
52
  mkOp('switch_br', {
52
53
  operands: [scrut],
53
54
  successors,
54
- attrs: { cases: successors.slice(0, -1).map((_, k) => k) },
55
+ attrs: { cases: successors.slice(0, -1).map((_, k) => k), ...(exhaustive ? { exhaustive: true } : {}) },
55
56
  }),
56
57
  );
57
58
  }
@@ -8,10 +8,16 @@
8
8
  // the decode and neither of these, `float fadd(float a, float b){ return a + b; }` lifts on every
9
9
  // FPU target as `void fadd(s32 a0, s32 a1) { return; }` — two phantom integer parameters and a dead
10
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';
11
14
  import { type IrType, T } from '../ir/types';
15
+ import { declaresAggregateReturn, declaresVoidReturn, floatingSpelling, isFloatingSpelling } from '../proto';
12
16
  import type { TargetDescription } from '../target';
17
+ import { computedOnlyToReturn } from './dead-reads';
13
18
  import { FrontendUnsupportedError } from './errors';
14
- import type { ArgSlots } from './ssa';
19
+ import type { OwnDeclaration } from './frontend';
20
+ import type { ArgSlots, SsaBuilder } from './ssa';
15
21
 
16
22
  export type Fpu = NonNullable<TargetDescription['fpu']>;
17
23
 
@@ -27,6 +33,28 @@ export function fpPrecision(
27
33
  return widths.size > 1 ? null : widths.has(64) ? T.f64() : widths.has(32) ? T.f32() : T.fUnstated();
28
34
  }
29
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
+
30
58
  /** Refuse a function computing in both precisions, where the frontend DECODES a float instruction of
31
59
  * it, so a refusal the stream reaches first keeps its own reason. The SSA builder types a register
32
60
  * per key, not per value, and the rounding between the precisions (a conversion, PowerPC's `frsp`,
@@ -135,25 +163,25 @@ export function fpuArgSlots(
135
163
  };
136
164
  }
137
165
 
138
- /** Whether a function RETURNS a float: some instruction the frontend decodes writes the float return
139
- * register, in a block the entry REACHES. Decided over the whole function, before any `ret` is
140
- * emitted, because a function has one return type on every path — a path that leaves the register
141
- * untouched returns the float argument that arrived there (PowerPC's `f1` is both), or refuses by
142
- * `fpuArgSlots`' rule 1. Reachable blocks only: `addi r3,r3,1; blr; fmr f1,f2` returns `r3`, and
143
- * the `fmr` past the `blr` is on no path of it.
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.
144
172
  *
145
173
  * And then the INTEGER return register is scratch, even where the function writes it. Both halves
146
- * rest on one fact: the arithmetic is ALL the decode admits, so a float reaches nothing but another
147
- * float op or the return — every move to memory, to the integer file or through a compare or a
148
- * conversion is refused. A write to the float return register that did not feed the return would
149
- * therefore be dead in the source, and the compiler does not emit dead arithmetic; an integer
150
- * result, meanwhile, could only have come from a float by a conversion, which refuses. IDO's
151
- * unrolled `float pw(float a, int n)` is the witness for the second half: it counts in `v0`.
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`.
152
178
  *
153
- * THAT FACT IS WHAT HOLDS THIS RULE UP, and the next layer removes it: a layer that lets a float
154
- * reach memory, the integer file or a compare must first decide the return from the value that
155
- * reaches each `ret`, not from a mnemonic scan (`fpu-lift.test.ts` pins the function that breaks
156
- * it, `docs/floating-point.md` §6). */
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). */
157
185
  export function writesFloatReturn(
158
186
  instrs: readonly { mnemonic: string; ops: string[] }[],
159
187
  decodes: ReadonlySet<string>,
@@ -165,3 +193,182 @@ export function writesFloatReturn(
165
193
  instrs.some((ins) => decodes.has(ins.mnemonic) && ins.ops[0] !== undefined && destKey(ins.ops[0]) === fpu.returnReg)
166
194
  );
167
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
+ }