@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,287 @@
1
+ // asmlift — the COMPILER RUNTIME HELPERS a target's codegen calls, and what each one computes.
2
+ //
3
+ // A compiler lowers an operation its ISA cannot do to a call into its own runtime: agbcc emits
4
+ // `bl __divsi3` for `a / b` because ARMv4T has no divider, and `bl __muldi3` for a 64-bit multiply
5
+ // because ARMv4T has no 64-bit anything. Two things are needed to recover either, and they are
6
+ // different things:
7
+ // • the SIGNATURE, so the call's arguments are recovered at all (the frontend's arity lookup
8
+ // falls back to this behind any caller-supplied prototype);
9
+ // • the OPERATION, so the call is rewritten to the op it computes and the structurer prints
10
+ // `a / b` rather than `__divsi3(a, b)`.
11
+ //
12
+ // AN ENTRY HERE IS ALSO A REFUSAL. Where no recogniser folds a call to a name in this table,
13
+ // `raise/widehelpers.ts` turns it into a gap instead of letting the backend spell it. Re-emitting
14
+ // the compiler's own runtime call as source is not a recovery — it is the one shape that MATCHES
15
+ // for free, because a compiler handed `__div2i(a, b)` emits the `bl __div2i` it was lifted from.
16
+ // A helper nothing here can fold therefore declines; a helper nothing here NAMES still passes
17
+ // through, which is what makes adding a name a decision rather than a note.
18
+ //
19
+ // WHICH HELPERS A COMPILER EMITS IS A COMPILER FACT, so the table hangs off `TargetDescription`
20
+ // rather than off a pass. `proto.ts` holds signatures fixed by the C STANDARD, which a runtime
21
+ // helper is precisely not — agbcc calls `__muldi3`, CodeWarrior calls `__div2i`, IDO calls
22
+ // `__ll_mul`, and nothing about the C standard predicts any of them. A scan that assumes one
23
+ // family reports ZERO on the others.
24
+ //
25
+ // `params` is a list of C parameter WIDTHS in bits, not a word count, and the difference is the
26
+ // whole point of the field: `__ashrdi3` takes a 64-bit value and a 32-bit count — two C parameters
27
+ // occupying THREE argument registers. A stated width is read two ways and both conversions live
28
+ // here: `wordsOf` (proto.ts, because a C declaration asks the same question) counts the argument
29
+ // REGISTERS the list occupies, and `irWidthOf` gives the IR width one parameter ARRIVES at once a
30
+ // frontend has paired those registers up.
31
+ import { type Op, type Value, mkOp } from './ir/core';
32
+ import { FLOAT_OPS, type Opcode, WIDE_BITS } from './ir/opcodes';
33
+ import { type IrType, intWidth } from './ir/types';
34
+ import { type Prototypes, wordsOf } from './proto';
35
+
36
+ export interface RuntimeHelper {
37
+ /** the op this helper computes, where asmlift has one. Absent for a helper it can only SIGN. */
38
+ op?: Opcode;
39
+ /** each C parameter's width in bits, in argument order. */
40
+ params: readonly number[];
41
+ /** the returned width: 0 (void), 32, or 64 (a register PAIR). */
42
+ returns: 0 | 32 | 64;
43
+ }
44
+
45
+ /** The entry a target's table holds for `callee`, or undefined. THE ONE READER, because the table
46
+ * is an object literal and a C function may be named `toString`: a bare index or an `in` answers
47
+ * with a member of `Object.prototype` for eight names, and what each caller then does with that
48
+ * `Function` differs — one reads `params` off it and throws a TypeError, another refuses while
49
+ * naming a runtime helper the target does not have. `Object.hasOwn` is stated here so that no
50
+ * caller can be the one that forgot it. (`proto.ts` states the same rule for the prototype
51
+ * tables, which are read the same way for the same reason.) */
52
+ export function lookupHelper(
53
+ table: Readonly<Record<string, RuntimeHelper>> | undefined,
54
+ callee: string,
55
+ ): RuntimeHelper | undefined {
56
+ return table && Object.hasOwn(table, callee) ? table[callee] : undefined;
57
+ }
58
+
59
+ /** The IR width a C parameter of `bits` arrives at: one 64-bit value for anything wider than a
60
+ * register, because a frontend that reads such a parameter fuses its registers into a single
61
+ * value; a machine word for everything else, whatever narrower width the C type carries. The
62
+ * counterpart of `wordsOf` — same threshold, different vocabulary. */
63
+ export function irWidthOf(bits: number): number {
64
+ return bits > 32 ? WIDE_BITS : 32;
65
+ }
66
+
67
+ /** Whether a call ARRIVED in the shape this helper's signature states: one result at the returned
68
+ * width, and each operand at the IR width its C parameter carries.
69
+ *
70
+ * A COUNT IS NOT A WIDTH, and the gap between them is where a 64-bit operation gets invented over
71
+ * two 32-bit words. Two ordinary shapes reach a fold with the right operand count and the wrong
72
+ * contents: a call whose arity came off a caller-supplied prototype rather than off this table, so
73
+ * no frontend ever built the pair; and a call on a target whose frontend does not pair registers
74
+ * at all, where an argument register that is not live at the call gets trimmed and the survivors
75
+ * happen to number what the table states. Either one folded publishes an operation over one half
76
+ * of each value, at exit 0 and with no gap — so the pair construction IS the precondition, and a
77
+ * shape that lacks it belongs to `refuseUnmodelledHelpers`.
78
+ *
79
+ * AN OPERAND THAT CARRIES NO INTEGER WIDTH REFUSES, which is `intWidth` returning null for a
80
+ * pointer or an aggregate. `ir/verify.ts` reads that same null as "does not take part in the width
81
+ * rule"; here it means there is no width to check the claim against, and a fold may not proceed on
82
+ * a claim it cannot check.
83
+ *
84
+ * A `returns: 0` entry can never satisfy this, because no result carries width 0. A void helper
85
+ * computing on a 64-bit value is a shape there is no operation to fold it into. */
86
+ export function arrivesAsDeclared(
87
+ helper: RuntimeHelper,
88
+ operands: readonly IrType[],
89
+ results: readonly IrType[],
90
+ ): boolean {
91
+ if (results.length !== 1 || intWidth(results[0]) !== helper.returns) {
92
+ return false;
93
+ }
94
+ return (
95
+ operands.length === helper.params.length && operands.every((t, i) => intWidth(t) === irWidthOf(helper.params[i]))
96
+ );
97
+ }
98
+
99
+ /** Whether a helper computes on a value wider than a register — the ones the 64-bit representation
100
+ * is for, and the ones no hardware capability can make unnecessary. */
101
+ export function isWideHelper(h: RuntimeHelper): boolean {
102
+ return h.returns > 32 || h.params.some((w) => w > 32);
103
+ }
104
+
105
+ /** The op a recognised helper call is rewritten to, STAMPED with the helper it was: `helper` names
106
+ * the callee. An integer helper's op takes the call's own operands and the SAME result value, so
107
+ * every use already points at it; a float helper's takes them re-typed as floats
108
+ * (`raise/floathelpers.ts`). `raise/softdiv.ts`, `raise/widehelpers.ts` and `raise/floathelpers.ts`
109
+ * build every one through here.
110
+ *
111
+ * The stamp carries the one fact the op's opcode loses: the asm CALLED something here. A
112
+ * `sdiv` is a `divw` on mwcc and a `bl __divsi3` on agbcc, and an `shr_s` is a shift everywhere
113
+ * but a `bl __ashrdi3` where it is 64-bit on agbcc — which ops a compiler calls a helper for is a
114
+ * compiler fact, not an ISA one, and it was known exactly once, at the rewrite. What reads it is
115
+ * every rule that places a call: a call runs once, where the asm ran it (ir/discipline.ts
116
+ * `placedAt`), and a pass that moves the op elsewhere drops the stamp (`forgetHelperPlacement`). */
117
+ export function helperOp(opcode: Opcode, helper: string, operands: readonly Value[], result: Value): Op {
118
+ return mkOp(opcode, { operands: [...operands], results: [result], attrs: { helper } });
119
+ }
120
+
121
+ /** Whether a helper computes on floats: its op is a float op, so its operands and result are floats
122
+ * of the widths its signature states, carried in integer registers by a soft-float runtime. Every
123
+ * reader of the table asks this and not the width, because the two families fold into different
124
+ * IR: an integer helper into its op over the same values, a float helper into a float op over
125
+ * values re-typed as floats (`raise/floathelpers.ts`). */
126
+ export function isFloatHelper(h: RuntimeHelper): boolean {
127
+ return h.op !== undefined && FLOAT_OPS.has(h.op);
128
+ }
129
+
130
+ /** Signatures for a target's helpers, in the WORD arity the frontend's prototype lookup speaks.
131
+ * Consumed behind any caller-supplied prototype — the project's own headers win. */
132
+ export function helperPrototypes(table: Readonly<Record<string, RuntimeHelper>> | undefined): Prototypes {
133
+ return Object.fromEntries(Object.entries(table ?? {}).map(([sym, h]) => [sym, { params: wordsOf(h.params) }]));
134
+ }
135
+
136
+ /** agbcc's runtime, which is GCC 2.9's libgcc for ARM.
137
+ *
138
+ * THE 64-BIT MULTIPLY IS SIGNEDNESS-BLIND, and that is a fact about this libgcc rather than a
139
+ * simplification here: `optabs.c` initialises the only integer multiply libfunc from the MODE
140
+ * alone, and `expmed.c` routes every `MULT` through it whatever the operands' signedness, so
141
+ * `__umuldi3` does not exist and both spellings emit `bl __muldi3`. Signedness comes from the
142
+ * operand SET-UP instead — `asr rN,rM,#31` per half for signed, `mov rN,#0` for unsigned. The
143
+ * DIVISIONS do split, and their entries say so.
144
+ *
145
+ * THE DOUBLE ARITHMETIC IS HERE, AND NOTHING ELSE OF THE SOFT FLOAT. `optabs.c:4022`
146
+ * `init_floating_libfuncs` names DFmode's add, sub, mul, div (4129-4140) and neg (4155), and
147
+ * `thumb.md` has no DF pattern but the move, so every one of them is a call; the ABI is a long
148
+ * long's (thumb.h:632, 655). `raise/floathelpers.ts` folds them to the float ops over a `double`.
149
+ *
150
+ * NAMING THEM IS A TRADE, and these five make it: a call the fold refuses declines where an
151
+ * unnamed helper would publish as a pass-through that matches for free. So a function whose
152
+ * double arithmetic feeds a compare (`__gtdf2`…), a conversion (`__fixdfsi`…) or any other call
153
+ * declines, where unnamed it would print `__muldf3(); return __fixdfsi();`. The compares and
154
+ * conversions of both precisions stay out, because the IR has no int<->float op to fold one into;
155
+ * the single-precision arithmetic stays out because its pass-throughs are what the float rows
156
+ * score on (`synthetic:fadd:agbcc`), and declining them is the same trade, to be made with a
157
+ * measurement of those rows. Making it is a table row: the fold reads each value's width off the
158
+ * signature. */
159
+ export const AGBCC_RUNTIME_HELPERS: Readonly<Record<string, RuntimeHelper>> = {
160
+ // 32-bit software division — the ops `raise/softdiv.ts` rewrites, gated on the target having no
161
+ // hardware divider, which is what those four are about.
162
+ __divsi3: { op: 'sdiv', params: [32, 32], returns: 32 },
163
+ __udivsi3: { op: 'udiv', params: [32, 32], returns: 32 },
164
+ __modsi3: { op: 'smod', params: [32, 32], returns: 32 },
165
+ __umodsi3: { op: 'umod', params: [32, 32], returns: 32 },
166
+ // 64-bit arithmetic. NOT gated on a hardware capability: no ISA this repo targets has 64-bit
167
+ // integer arithmetic at all, so a call here is software on every one of them.
168
+ __muldi3: { op: 'mul', params: [64, 64], returns: 64 },
169
+ __divdi3: { op: 'sdiv', params: [64, 64], returns: 64 },
170
+ __udivdi3: { op: 'udiv', params: [64, 64], returns: 64 },
171
+ __moddi3: { op: 'smod', params: [64, 64], returns: 64 },
172
+ __umoddi3: { op: 'umod', params: [64, 64], returns: 64 },
173
+ __ashldi3: { op: 'shl', params: [64, 32], returns: 64 },
174
+ __ashrdi3: { op: 'shr_s', params: [64, 32], returns: 64 },
175
+ __lshrdi3: { op: 'shr_u', params: [64, 32], returns: 64 },
176
+ __negdi2: { op: 'neg', params: [64], returns: 64 },
177
+ // Double arithmetic, which the same pair carries (`raise/floathelpers.ts`).
178
+ __adddf3: { op: 'fadd', params: [64, 64], returns: 64 },
179
+ __subdf3: { op: 'fsub', params: [64, 64], returns: 64 },
180
+ __muldf3: { op: 'fmul', params: [64, 64], returns: 64 },
181
+ __divdf3: { op: 'fdiv', params: [64, 64], returns: 64 },
182
+ __negdf2: { op: 'fneg', params: [64], returns: 64 },
183
+ };
184
+
185
+ /** CodeWarrior's PowerPC runtime (`Runtime.PPCEABI.H`), as the GameCube projects vendor it.
186
+ *
187
+ * A DIFFERENT FAMILY ENTIRELY, which is the reason this table is per target and not per repo: a
188
+ * scan for `__*di3` reports zero on every one of these. These seven are the integer set, agreed
189
+ * on by all three PPC checkouts' own vendored `runtime.c` (each at a different path), where every
190
+ * one is an `ASM` function whose body states its register contract: `__div2i` takes r3:r4 and
191
+ * r5:r6 and returns r3:r4, `__shl2i` takes r3:r4 and a count in r5.
192
+ *
193
+ * ONE SHIFT LEFT AND TWO RIGHT, which is the same signedness rule agbcc's `__ashldi3` follows:
194
+ * the bits a left shift brings in are zero whatever the operand is, so there is nothing for a
195
+ * `__shl2u` to do differently.
196
+ *
197
+ * NO MULTIPLY, and that is a fact about this compiler rather than a hole here: mwcc open-codes a
198
+ * 64-bit multiply. `synthetic:llmul:mwcc_242_81` is the evidence and it is in the corpus — its
199
+ * target is 28 bytes of `mulhwu` and two `mullw` with no relocation at all, against 32 bytes and
200
+ * an `R_PPC_REL24` to `__div2i` for `lldivs` beside it. The float and decimal helpers of the same
201
+ * runtime (`__cvt_sll_flt`, `__num2dec`) are left out for the reason the agbcc table leaves out
202
+ * its own.
203
+ *
204
+ * ⚠ WHAT THIS TABLE DOES ON THIS TARGET TODAY IS REFUSE, and the `op` column is not reachable
205
+ * from a lift. `frontend/ppc.ts` reads guessed argument registers and fuses none of them into a
206
+ * pair, so no value on this target is ever 64 bits wide, so `arrivesAsDeclared` can never be
207
+ * satisfied here and `recognizeWideHelpers` cannot fold. THAT absent pairing is the gate bounding
208
+ * the path — not anything in this file, and not `refuseUnmodelledHelpers`, which is what these
209
+ * names reach instead. The ops below are measured off the vendored `runtime.c` and asserted
210
+ * directly by `runtime-helper-refusal.test.ts`, because a claim no lift can reach is a claim no
211
+ * lift can check either; naming which operation each helper computes is the half of the frontend
212
+ * work that is done. */
213
+ export const PPC_MWCC_RUNTIME_HELPERS: Readonly<Record<string, RuntimeHelper>> = {
214
+ __div2i: { op: 'sdiv', params: [64, 64], returns: 64 },
215
+ __div2u: { op: 'udiv', params: [64, 64], returns: 64 },
216
+ __mod2i: { op: 'smod', params: [64, 64], returns: 64 },
217
+ __mod2u: { op: 'umod', params: [64, 64], returns: 64 },
218
+ __shl2i: { op: 'shl', params: [64, 32], returns: 64 },
219
+ __shr2i: { op: 'shr_s', params: [64, 32], returns: 64 },
220
+ __shr2u: { op: 'shr_u', params: [64, 32], returns: 64 },
221
+ };
222
+
223
+ /** IDO 7.1's runtime for the 64-bit operations MIPS II has no instruction for, a family of its own
224
+ * (`__ll_*` for signed, `__ull_*` for unsigned): a scan for `__*di3` reports zero on it.
225
+ *
226
+ * Compiled at `-mips2 -O2 -32 -non_shared -G 0` (compiled evidence only): `a * b` calls `__ll_mul`
227
+ * for both signednesses, the divisions and remainders split, and a left shift is one helper while
228
+ * the right shift splits. A SHIFT'S COUNT TRAVELS AS A PAIR: `a << n` with `int n` builds
229
+ * `a2 = n >> 31, a3 = n` before `jal __ll_lshift`, so both parameters are 64 bits wide.
230
+ *
231
+ * EVERY CONVERSION BETWEEN A 64-BIT INTEGER AND A FLOAT IS A CALL TOO, one per direction, precision
232
+ * and signedness. The float side travels in the FPU's registers (`$f12` in, `$f0` out), so these
233
+ * carry no op: a guessed call to one passes and returns nothing it can see.
234
+ *
235
+ * SO IS A 64-BIT BITFIELD: a read hands the word's address, start bit and length to
236
+ * `__ll_bit_extract` (`__ull_bit_extract` unsigned), and a write of either signedness calls
237
+ * `__ll_bit_insert` with the value as a fourth parameter. No op folds either.
238
+ *
239
+ * THE NAMES ARE THE COMPILER'S OWN: these are every `__ll_*`, `__ull_*` and `__*_to_*ll` that a pass
240
+ * of IDO 7.1 carries in its string table (`cfe` carries them all). Its libc defines more
241
+ * (`__ll_mod`, `__ull_divremi`, `__ull_divrem_*`), which no pass names, so no compiled code calls them.
242
+ *
243
+ * ⚠ WHAT THIS TABLE DOES ON THIS TARGET IS REFUSE, as `PPC_MWCC_RUNTIME_HELPERS` does and for the
244
+ * same reason: `frontend/mips.ts` fuses no register pair, so a call to one of these arrives with
245
+ * word operands and `refuseUnmodelledHelpers` gaps it by name. */
246
+ export const IDO_RUNTIME_HELPERS: Readonly<Record<string, RuntimeHelper>> = {
247
+ __ll_mul: { op: 'mul', params: [64, 64], returns: 64 },
248
+ __ll_div: { op: 'sdiv', params: [64, 64], returns: 64 },
249
+ __ull_div: { op: 'udiv', params: [64, 64], returns: 64 },
250
+ __ll_rem: { op: 'smod', params: [64, 64], returns: 64 },
251
+ __ull_rem: { op: 'umod', params: [64, 64], returns: 64 },
252
+ __ll_lshift: { op: 'shl', params: [64, 64], returns: 64 },
253
+ __ll_rshift: { op: 'shr_s', params: [64, 64], returns: 64 },
254
+ __ull_rshift: { op: 'shr_u', params: [64, 64], returns: 64 },
255
+ __ll_to_d: { params: [64], returns: 64 },
256
+ __ll_to_f: { params: [64], returns: 32 },
257
+ __ull_to_d: { params: [64], returns: 64 },
258
+ __ull_to_f: { params: [64], returns: 32 },
259
+ __d_to_ll: { params: [64], returns: 64 },
260
+ __f_to_ll: { params: [32], returns: 64 },
261
+ __d_to_ull: { params: [64], returns: 64 },
262
+ __f_to_ull: { params: [32], returns: 64 },
263
+ __ll_bit_extract: { params: [32, 32, 32], returns: 64 },
264
+ __ull_bit_extract: { params: [32, 32, 32], returns: 64 },
265
+ __ll_bit_insert: { params: [32, 32, 32, 64], returns: 64 },
266
+ };
267
+
268
+ /** GCC's libgcc for MIPS, as KMC GCC and GCC 2.7.2 call it at the N64 projects' `-mips3 -mgp32`
269
+ * (compiled evidence only, both toolchains agreeing): the four divisions are calls, while a
270
+ * 64-bit multiply is `mult`/`multu` and a shift is open-coded, so neither has an entry. The
271
+ * conversions between a 64-bit integer and a float are calls, with no `__floatundi*`: an unsigned
272
+ * one converts as signed and corrects the result when `__cmpdi2` finds the value negative. The
273
+ * conversions carry no op, for the reason `IDO_RUNTIME_HELPERS` gives. It refuses on this target
274
+ * for that table's reason too. */
275
+ export const MIPS_GCC_RUNTIME_HELPERS: Readonly<Record<string, RuntimeHelper>> = {
276
+ __divdi3: { op: 'sdiv', params: [64, 64], returns: 64 },
277
+ __udivdi3: { op: 'udiv', params: [64, 64], returns: 64 },
278
+ __moddi3: { op: 'smod', params: [64, 64], returns: 64 },
279
+ __umoddi3: { op: 'umod', params: [64, 64], returns: 64 },
280
+ __floatdidf: { params: [64], returns: 64 },
281
+ __floatdisf: { params: [64], returns: 32 },
282
+ __fixdfdi: { params: [64], returns: 64 },
283
+ __fixsfdi: { params: [32], returns: 64 },
284
+ __fixunsdfdi: { params: [64], returns: 64 },
285
+ __fixunssfdi: { params: [32], returns: 64 },
286
+ __cmpdi2: { params: [64, 64], returns: 32 },
287
+ };
@@ -0,0 +1,289 @@
1
+ // asmlift — THE STILLBORN FAN: when to stop compiling a fan that cannot compile.
2
+ //
3
+ // A fan is a product of variations over ONE function body, and every variation only re-spells
4
+ // statements. When the body holds a statement the compiler refuses for a reason no variation
5
+ // reaches — a call with more arguments than the project's header declares, a struct-typed global
6
+ // read as a scalar — every candidate is refused for it, and the fan's size is the number of times
7
+ // the same sentence gets printed: 30,240 on `kleod:PauseMenuScreenHandler`.
8
+ //
9
+ // THE RULE. The DEFAULT candidate (first in enumeration order) is compiled first. Only if the
10
+ // compiler REJECTS it is anything else asked: a PROBE per variation name — the candidate with the
11
+ // fewest variations that carries it, per signedness half (HALF BY HALF) — is compiled, and the fan
12
+ // is stillborn only when the default holds an error that SURVIVES every probe: a message the probe
13
+ // was rejected with too, exactly as many times as the default was (compiler-diagnostics.ts
14
+ // `errorMessages`, positions normalised away). Then the remaining candidates are NOT COMPILED, and
15
+ // are reported as exactly that.
16
+ //
17
+ // WHAT IT BETS ON: that a statement no probe re-spells is re-spelled by no product of the probes'
18
+ // variations either, so its error is in every candidate. That is a bet, not a theorem — a product
19
+ // is not the sum of its variations (THE RESIDUALS, below) — and the rule never compiles a product
20
+ // to check it.
21
+ //
22
+ // One survivor is enough; the probes may reach the default's other errors. In
23
+ // `ac-decomp:aINS_destruct:mwcc_242_81`'s vendored-context attempt `/setup-args` cures the call to
24
+ // `mPlib_Get_item_net_catch_label`, both `illegal implicit conversion` errors stay, and the fan of
25
+ // 12 stops after 8 compiles.
26
+ //
27
+ // EXACTLY as many times: a probe that prints the survivor more often has re-spelled a statement
28
+ // that raises it (`/reread-globals` re-reading a global whose type is refused), and a probe that
29
+ // prints it fewer times has cured one. Either one reached it, so neither vouches that a product
30
+ // leaves it alone.
31
+ //
32
+ // PER ATTEMPT. A rejection may hold several compiles (compiler-diagnostics.ts `attemptsOf`: the
33
+ // benchmark's real tier tries every candidate in a ladder of contexts and two dialects), and a
34
+ // candidate compiles when ANY attempt does. So a survivor is asked of EACH attempt, and one attempt
35
+ // without a survivor keeps the fan alive: an attempt no candidate can compile in — a context that
36
+ // cannot open its own headers — prints the same sentences for every candidate, and those survive
37
+ // every probe while another attempt's product compiles. Every probe must have been tried in the
38
+ // same attempts, under the same labels, as the default.
39
+ //
40
+ // HALF BY HALF. Every fan is enumerated at BOTH signednesses (variation-tokens.ts, kind
41
+ // `signedness`), and a signedness re-types the whole body, so the survivor is worded once per half:
42
+ // aINS_destruct's `illegal implicit conversion` to `struct game_s *` names `unsigned long` in one
43
+ // half and `long` in the other, and asked over the whole fan nothing in its vendored-context
44
+ // attempt survives the `signed` probe. So each half is asked as a fan of its own — its first
45
+ // candidate is its default, its probes are the smallest carrier of each variation name INSIDE it —
46
+ // and the fan is stillborn only when every half is. A half is found by the registry's kind, never
47
+ // by a name, and a fan with no signedness in its names is one half. A stopped fan pays both halves'
48
+ // probes.
49
+ //
50
+ // KNOWN GAP: a variation that re-words the survivor in PART of the fan is not split on, so its
51
+ // probe leaves that attempt without a survivor and the fan is ranked whole —
52
+ // `kleod:WorldMapScreenUnlockNewWorld:agbcc`'s only vendored-context error, `incompatible types in
53
+ // assignment`, reads `invalid operands to binary &` under `/derived-home` and `/setup-args`.
54
+ //
55
+ // Anything else ranks the whole fan. A probe that compiles or is withheld: the fan is alive. An
56
+ // attempt with no survivor: a product of variations may cure what no single one does. An attempt
57
+ // whose errors are not read — none recognised, or the compiler stopped reporting — or an attempt
58
+ // list that differs from the default's: an unread sentence equals nothing. A throw that is not a
59
+ // `CompilerRejection`: a timeout or a killed compiler says nothing about the candidate.
60
+ //
61
+ // THE RESIDUALS, which this rule does NOT close: a product that cures the survivor while every
62
+ // probe keeps it. One survivor being enough, a stop may rest its whole bet on a single message in
63
+ // the one attempt a candidate could compile in. Two shapes are known.
64
+ //
65
+ // JOINT CURE: ONE error MESSAGE that needs TWO variations together. `a & b` is `invalid operands
66
+ // to binary &` while either operand is a struct; if one variation re-types `a` and another re-types
67
+ // `b`, each probe leaves the message where it was, and only their product compiles. The same shape
68
+ // arises without any re-typing wherever the compiler prints ONE message for a statement holding TWO
69
+ // defects: mwcc and IDO report `y = g(1, 2) + h(3, 4)` as a single message, and agbcc, kmc and IDO
70
+ // report an undeclared name once however many times it is used. Two DIFFERENT messages each cured
71
+ // by its own variation are not this case: each probe removes its message, and neither survives.
72
+ // Signedness re-types operands and can cure half a statement — against a declared
73
+ // `CARDGetSectorSize(long, unsigned long *)`, `signed` turns a call's `(unsigned long, long *)` into
74
+ // `(long, long *)`, one message either way — and HALF BY HALF is what closes it there: a half holds
75
+ // its signedness fixed. Any OTHER variation that re-types an operand or cures half a statement
76
+ // must revisit this rule.
77
+ //
78
+ // INTERACTION: a variation whose reach depends on another. A respell runs over whatever tree the
79
+ // structure variations built, so a structure variation × respell product re-spells statements
80
+ // neither of its probes touched: `/unmerge` copies a join's call into both arms only on the
81
+ // `/flip-join` tree, so their product can print a message more often than the default and either
82
+ // probe do. A product that moves a survivor UP keeps it an error; one that moved it to zero, leaving
83
+ // no other error, would be a candidate that compiles and that the stop never compiles.
84
+ //
85
+ // ONE COPY. Probe selection and the verdict are pure functions over indices, so the sync driver
86
+ // (rank.ts `rankBy`), the pooled CLI driver and the webapp's async loop all sequence their own
87
+ // compiles and ask the same question. Every driver finishes the probe phase before compiling
88
+ // anything else, which is what keeps the verdict independent of a scheduler.
89
+ import { CompilerRejection, attemptsOf, verdictMessages } from './compiler-diagnostics';
90
+ import { VARIATION_TOKENS } from './variation-tokens';
91
+
92
+ /** What compiling one candidate came to, as far as this rule reads it. `compiled` covers scored
93
+ * AND withheld: either way the compiler accepted the text. */
94
+ export type ProbeOutcome = 'compiled' | { thrown: unknown };
95
+
96
+ /** The signedness names, read off the registry by kind. */
97
+ const SIGNEDNESS = new Set<string>(VARIATION_TOKENS.filter((t) => t.variationKind === 'signedness').map((t) => t.name));
98
+
99
+ /** The fan's signedness HALVES: candidates grouped by the signedness their names carry, each in
100
+ * enumeration order, the halves in the order of their first candidate. Index 0 opens the first. */
101
+ function halvesOf(candidates: readonly { variations: readonly string[] }[]): number[][] {
102
+ const halves = new Map<string, number[]>();
103
+ candidates.forEach((c, i) => {
104
+ const key = c.variations.filter((v) => SIGNEDNESS.has(v)).join('/');
105
+ const half = halves.get(key);
106
+ if (half === undefined) {
107
+ halves.set(key, [i]);
108
+ } else {
109
+ half.push(i);
110
+ }
111
+ });
112
+ return [...halves.values()];
113
+ }
114
+
115
+ /** A half's probes: for every variation name in the half, the index of the candidate IN THE HALF
116
+ * with the FEWEST variations that carries it, enumeration order breaking a tie. Ascending, and
117
+ * without the half's own first candidate, which is its default.
118
+ *
119
+ * A NAME is the whole part as the candidate carries it, subject included: `argcopy-a0@1.0` and
120
+ * `argcopy-a0@2.0` are two probes, because each re-spells a different statement and the rule asks
121
+ * whether ANY re-spelling reaches the failing one. Collapsing them to the registered name would
122
+ * probe one region and answer for both. */
123
+ function probesOf(candidates: readonly { variations: readonly string[] }[], half: readonly number[]): number[] {
124
+ const smallest = new Map<string, number>();
125
+ for (const i of half) {
126
+ for (const v of candidates[i].variations) {
127
+ const held = smallest.get(v);
128
+ if (held === undefined || candidates[i].variations.length < candidates[held].variations.length) {
129
+ smallest.set(v, i);
130
+ }
131
+ }
132
+ }
133
+ return [...new Set(smallest.values())].filter((i) => i !== half[0]).sort((a, b) => a - b);
134
+ }
135
+
136
+ /** The candidates to compile once the default was rejected: every half's default and probes
137
+ * (`halvesOf`, `probesOf`). Ascending, and without the default itself. */
138
+ export function probeIndices(candidates: readonly { variations: readonly string[] }[]): number[] {
139
+ const probes = new Set<number>();
140
+ for (const half of halvesOf(candidates)) {
141
+ for (const i of [half[0], ...probesOf(candidates, half)]) {
142
+ probes.add(i);
143
+ }
144
+ }
145
+ probes.delete(0);
146
+ return [...probes].sort((a, b) => a - b);
147
+ }
148
+
149
+ /** A fan declared stillborn. */
150
+ export interface Stillborn {
151
+ /** each half's default's errors that survived every probe of that half, as the compiler worded
152
+ * them in that half: a message named at the most times one attempt printed it */
153
+ messages: string[];
154
+ /** indices that WERE compiled — the default and the probes, ascending */
155
+ compiled: number[];
156
+ /** indices that were not, ascending: the rest of the fan */
157
+ notCompiled: number[];
158
+ }
159
+
160
+ /** A rejection read attempt by attempt: each attempt's label and the multiset of its error
161
+ * messages. Null for anything the rule cannot read as a verdict — a compile, a throw that is no
162
+ * `CompilerRejection`, or any attempt whose errors are unread. */
163
+ interface Reading {
164
+ labels: string[];
165
+ errors: Map<string, number>[];
166
+ }
167
+
168
+ /** Whether a throw is the compiler REFUSING the text, read as the rule reads a probe: a
169
+ * `CompilerRejection` whose every attempt prints an error the rule reads. Any other throw — a
170
+ * killed compiler, a timeout — says nothing about the candidate, and neither does a rejection
171
+ * that prints no such error: a Docker CLI that cannot reach its daemon exits 1, as a refusing
172
+ * compiler does. Whatever checks the rule's bet by compiling a stopped fan's rest (the
173
+ * benchmark's `bench fan --whole`) counts a refusal through this test, never a second one. */
174
+ export const readableRefusal = (thrown: unknown): boolean => readingOf({ thrown }) !== null;
175
+
176
+ function readingOf(outcome: ProbeOutcome): Reading | null {
177
+ if (outcome === 'compiled' || !(outcome.thrown instanceof CompilerRejection)) {
178
+ return null;
179
+ }
180
+ const attempts = attemptsOf(outcome.thrown.diagnostic);
181
+ const errors: Map<string, number>[] = [];
182
+ for (const attempt of attempts) {
183
+ const messages = verdictMessages(attempt.diagnostic);
184
+ if (messages === null) {
185
+ return null;
186
+ }
187
+ const tally = new Map<string, number>();
188
+ for (const m of messages) {
189
+ tally.set(m, (tally.get(m) ?? 0) + 1);
190
+ }
191
+ errors.push(tally);
192
+ }
193
+ return { labels: attempts.map((a) => a.label), errors };
194
+ }
195
+
196
+ /** The default's errors that survive `probes`, per attempt — null as soon as one attempt has none
197
+ * left, or a probe cannot be read against the default. Asks `outcomeOf` for the probes in order
198
+ * and for none past the first that ends it. */
199
+ function survivors(
200
+ anchor: number,
201
+ probes: readonly number[],
202
+ outcomeOf: (index: number) => ProbeOutcome,
203
+ ): Map<string, number>[] | null {
204
+ const anchored = readingOf(outcomeOf(anchor));
205
+ if (anchored === null) {
206
+ return null;
207
+ }
208
+ const left = anchored.errors.map((tally) => new Map(tally));
209
+ for (const i of probes) {
210
+ const probe = readingOf(outcomeOf(i));
211
+ if (
212
+ probe === null ||
213
+ probe.labels.length !== anchored.labels.length ||
214
+ probe.labels.some((label, k) => label !== anchored.labels[k])
215
+ ) {
216
+ return null;
217
+ }
218
+ for (const [k, tally] of left.entries()) {
219
+ for (const [message, times] of tally) {
220
+ if (probe.errors[k].get(message) !== times) {
221
+ tally.delete(message);
222
+ }
223
+ }
224
+ if (tally.size === 0) {
225
+ return null;
226
+ }
227
+ }
228
+ }
229
+ return left;
230
+ }
231
+
232
+ /** The verdict over a fan whose default and probes have all been compiled — null for "rank the
233
+ * whole fan", which is every case but one (the module header states the rule).
234
+ *
235
+ * `outcomeOf` is asked for index 0 and then, only while the answer can still be "stillborn", for
236
+ * the probes of its half in order, then for each further half's default and probes — so a
237
+ * synchronous driver may compile on demand inside it and pays for no probe past the first that
238
+ * ends it. It must answer whenever asked: a driver that skipped a
239
+ * probe has not run the rule, and "stillborn" over the probes it happened to compile would be a
240
+ * different, weaker rule. */
241
+ export function stillbornVerdict(
242
+ candidates: readonly { variations: readonly string[] }[],
243
+ outcomeOf: (index: number) => ProbeOutcome | undefined,
244
+ ): Stillborn | null {
245
+ if (candidates.length === 0) {
246
+ return null;
247
+ }
248
+ const asked = (i: number): ProbeOutcome => {
249
+ const outcome = outcomeOf(i);
250
+ if (outcome === undefined) {
251
+ throw new Error(`internal: the stillborn verdict was asked before candidate ${i} was compiled`);
252
+ }
253
+ return outcome;
254
+ };
255
+ const perHalf: Map<string, number>[][] = [];
256
+ for (const half of halvesOf(candidates)) {
257
+ const left = survivors(half[0], probesOf(candidates, half), asked);
258
+ if (left === null) {
259
+ return null;
260
+ }
261
+ perHalf.push(left);
262
+ }
263
+ const compiled = [0, ...probeIndices(candidates)];
264
+ const tried = new Set(compiled);
265
+ return {
266
+ messages: namedOnce(perHalf.flat()),
267
+ compiled,
268
+ notCompiled: candidates.map((_, i) => i).filter((i) => !tried.has(i)),
269
+ };
270
+ }
271
+
272
+ /** Every half's and attempt's survivors as one list: a message in the order it first appears, as
273
+ * many times as the attempt that printed it most. */
274
+ function namedOnce(perAttempt: readonly Map<string, number>[]): string[] {
275
+ const most = new Map<string, number>();
276
+ for (const tally of perAttempt) {
277
+ for (const [message, times] of tally) {
278
+ most.set(message, Math.max(most.get(message) ?? 0, times));
279
+ }
280
+ }
281
+ return [...most].flatMap(([message, times]) => Array.from({ length: times }, () => message));
282
+ }
283
+
284
+ /** Whether a driver that has compiled ONLY the default should now compile the probes — i.e.
285
+ * whether the default was rejected in a way the rule can read. A driver may skip this and compile
286
+ * the probes regardless; asking first is what keeps an ordinary fan's compile order untouched. */
287
+ export function defaultIsReadableRejection(outcome: ProbeOutcome): boolean {
288
+ return readingOf(outcome) !== null;
289
+ }