@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.
- package/README.md +34 -30
- package/package.json +1 -1
- package/src/aggregate.ts +248 -0
- package/src/backend/c.ts +5 -2
- package/src/backend/cfamily.ts +236 -37
- package/src/backend/cpp.ts +114 -9
- package/src/backend/pascal.ts +39 -0
- package/src/cdecl/constant.ts +59 -0
- package/src/cdecl/lex.ts +209 -0
- package/src/cdecl/parse.ts +975 -0
- package/src/cdecl/spell.ts +69 -0
- package/src/codegen-flags.ts +38 -6
- package/src/compiler-diagnostics.ts +194 -0
- package/src/contracts.ts +615 -79
- package/src/declare.ts +75 -5
- package/src/frontend/asmdata.ts +19 -5
- package/src/frontend/call-plan.ts +686 -0
- package/src/frontend/device-pins.ts +277 -0
- package/src/frontend/errors.ts +5 -0
- package/src/frontend/flags-edge.ts +101 -0
- package/src/frontend/fpu.ts +167 -0
- package/src/frontend/frame-objects.ts +2131 -0
- package/src/frontend/high-half.ts +2 -1
- package/src/frontend/local-object.ts +490 -0
- package/src/frontend/mips.ts +363 -40
- package/src/frontend/opaque.ts +97 -18
- package/src/frontend/ppc.ts +520 -129
- package/src/frontend/reloc-symbol.ts +94 -13
- package/src/frontend/splat.ts +108 -5
- package/src/frontend/ssa.ts +669 -76
- package/src/frontend/stackargs.ts +120 -39
- package/src/frontend/thumb.ts +2764 -2018
- package/src/ir/alias.ts +29 -0
- package/src/ir/core.ts +101 -3
- package/src/ir/discipline.ts +248 -0
- package/src/ir/float-bits.ts +47 -0
- package/src/ir/opcodes.ts +84 -112
- package/src/ir/parse.ts +1 -0
- package/src/ir/types.ts +118 -2
- package/src/ir/verify.ts +150 -3
- package/src/l3/address.ts +5 -0
- package/src/l3/argcopy.ts +38 -6
- package/src/l3/ast.ts +186 -14
- package/src/l3/coalesce.ts +21 -13
- package/src/l3/dce.ts +32 -10
- package/src/l3/gates.ts +9 -3
- package/src/l3/hoist.ts +8 -7
- package/src/l3/initfirst.ts +27 -9
- package/src/l3/intcell.ts +97 -0
- package/src/l3/mentions.ts +10 -4
- package/src/l3/narrowdecl.ts +218 -0
- package/src/l3/nearbase.ts +10 -5
- package/src/l3/parkfirst.ts +8 -5
- package/src/l3/pollguard.ts +3 -2
- package/src/l3/ptrcell.ts +357 -0
- package/src/l3/regspell.ts +17 -3
- package/src/l3/reindex.ts +10 -7
- package/src/l3/symbol-refs.ts +104 -23
- package/src/l3/tailmerge.ts +6 -19
- package/src/l3/typing.ts +137 -25
- package/src/l3/unreduce.ts +4 -4
- package/src/l3/volstore.ts +96 -7
- package/src/mangle.ts +11 -1
- package/src/pattern/engine.ts +19 -16
- package/src/pipeline.ts +57 -12
- package/src/proto-context.ts +788 -0
- package/src/proto.ts +689 -50
- package/src/raise/arrays.ts +4 -3
- package/src/raise/declared-volatile.ts +131 -0
- package/src/raise/divpow2.ts +2 -2
- package/src/raise/errors.ts +17 -0
- package/src/raise/floathelpers.ts +243 -0
- package/src/raise/globalshape.ts +11 -5
- package/src/raise/gvn.ts +26 -5
- package/src/raise/narrow.ts +29 -2
- package/src/raise/narrowlocal.ts +11 -6
- package/src/raise/offsetnames.ts +510 -0
- package/src/raise/pairparams.ts +81 -0
- package/src/raise/paramwidth.ts +7 -0
- package/src/raise/pre-recovery.ts +78 -4
- package/src/raise/recover.ts +14 -10
- package/src/raise/retsink.ts +1 -1
- package/src/raise/shortcircuit.ts +41 -19
- package/src/raise/softdiv.ts +21 -23
- package/src/raise/struct-arrays.ts +5 -4
- package/src/raise/structs.ts +311 -98
- package/src/raise/truncload.ts +366 -0
- package/src/raise/widehelpers.ts +106 -0
- package/src/rank-declare.ts +30 -5
- package/src/rank-variations.ts +36 -8
- package/src/rank.ts +276 -80
- package/src/runtime-helpers.ts +287 -0
- package/src/stillborn.ts +289 -0
- package/src/structure/analysis.ts +1285 -448
- package/src/structure/hazards.ts +633 -58
- package/src/structure/local-statics.ts +381 -0
- package/src/structure/namecoalesce.ts +1 -1
- package/src/structure/redundant-test.ts +99 -0
- package/src/structure/structure.ts +1772 -395
- package/src/structure/switch-recover.ts +159 -78
- package/src/symbols.ts +38 -3
- package/src/target.ts +662 -59
- package/src/trace.ts +44 -24
- package/src/variation-definitions.ts +119 -1
- 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
|
+
};
|
package/src/stillborn.ts
ADDED
|
@@ -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
|
+
}
|