@asmlift/core 0.5.0 → 0.7.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 (94) hide show
  1. package/README.md +22 -16
  2. package/package.json +1 -1
  3. package/src/backend/c.ts +1 -0
  4. package/src/backend/cfamily.ts +270 -171
  5. package/src/backend/cpp.ts +1 -0
  6. package/src/backend/pascal.ts +26 -12
  7. package/src/contracts.ts +243 -39
  8. package/src/declare.ts +41 -4
  9. package/src/frontend/mips.ts +11 -0
  10. package/src/frontend/ppc.ts +43 -7
  11. package/src/frontend/ssa.ts +404 -29
  12. package/src/frontend/thumb.ts +2176 -686
  13. package/src/ir/alias.ts +78 -0
  14. package/src/ir/bits.ts +75 -0
  15. package/src/ir/core.ts +345 -2
  16. package/src/ir/opcodes.ts +176 -21
  17. package/src/ir/parse.ts +19 -2
  18. package/src/ir/print.ts +27 -2
  19. package/src/ir/simplify.ts +190 -3
  20. package/src/ir/struct-names.ts +42 -0
  21. package/src/ir/verify.ts +43 -49
  22. package/src/l3/address.ts +62 -0
  23. package/src/l3/advance.ts +373 -0
  24. package/src/l3/argbase.ts +6 -5
  25. package/src/l3/ast.ts +510 -59
  26. package/src/l3/basecse.ts +686 -78
  27. package/src/l3/coalesce.ts +432 -46
  28. package/src/l3/dce.ts +31 -9
  29. package/src/l3/gates.ts +96 -1
  30. package/src/l3/hoist.ts +293 -14
  31. package/src/l3/homesplit.ts +285 -0
  32. package/src/l3/initfirst.ts +301 -0
  33. package/src/l3/inlinebase.ts +193 -0
  34. package/src/l3/mentions.ts +176 -0
  35. package/src/l3/mulfirst.ts +42 -0
  36. package/src/l3/nearbase.ts +152 -0
  37. package/src/l3/offmember.ts +371 -0
  38. package/src/l3/parkfirst.ts +96 -0
  39. package/src/l3/pollguard.ts +154 -0
  40. package/src/l3/ptrfield.ts +227 -0
  41. package/src/l3/regspell.ts +114 -89
  42. package/src/l3/reindex.ts +722 -80
  43. package/src/l3/scopebase.ts +649 -220
  44. package/src/l3/sinkinit.ts +40 -0
  45. package/src/l3/slotorder.ts +123 -0
  46. package/src/l3/storage.ts +48 -0
  47. package/src/l3/symbol-refs.ts +41 -8
  48. package/src/l3/tailmerge.ts +16 -1
  49. package/src/l3/typing.ts +198 -9
  50. package/src/l3/unmerge.ts +687 -0
  51. package/src/l3/unreduce.ts +971 -0
  52. package/src/l3/volatileptr.ts +207 -0
  53. package/src/l3/volatileval.ts +130 -0
  54. package/src/l3/volstore.ts +229 -0
  55. package/src/l3/zerosub.ts +62 -0
  56. package/src/pattern/engine.ts +239 -16
  57. package/src/pipeline.ts +173 -60
  58. package/src/proto.ts +112 -14
  59. package/src/raise/arrays.ts +6 -1
  60. package/src/raise/const.ts +203 -3
  61. package/src/raise/divpow2.ts +4 -4
  62. package/src/raise/extscale.ts +342 -0
  63. package/src/raise/globalshape.ts +1058 -0
  64. package/src/raise/gvn.ts +33 -18
  65. package/src/raise/latch.ts +126 -0
  66. package/src/raise/magicdiv.ts +2 -2
  67. package/src/raise/memberarrays.ts +594 -0
  68. package/src/raise/narrow.ts +124 -0
  69. package/src/raise/narrowlocal.ts +572 -0
  70. package/src/raise/paramwidth.ts +201 -0
  71. package/src/raise/pre-recovery.ts +169 -21
  72. package/src/raise/recover.ts +56 -23
  73. package/src/raise/retsink.ts +585 -19
  74. package/src/raise/shortcircuit.ts +1050 -89
  75. package/src/raise/struct-arrays.ts +19 -2
  76. package/src/raise/structs.ts +34 -4
  77. package/src/raise/tailsink.ts +126 -0
  78. package/src/rank-declare.ts +256 -0
  79. package/src/rank-variations.ts +760 -0
  80. package/src/rank.ts +2122 -326
  81. package/src/structure/analysis.ts +1398 -150
  82. package/src/structure/bitfields.ts +432 -0
  83. package/src/structure/globalaccess.ts +300 -0
  84. package/src/structure/hazards.ts +411 -20
  85. package/src/structure/loops.ts +2 -49
  86. package/src/structure/namecoalesce.ts +454 -0
  87. package/src/structure/structure.ts +3979 -612
  88. package/src/structure/switch-recover.ts +710 -145
  89. package/src/symbols.ts +188 -6
  90. package/src/target.ts +495 -32
  91. package/src/trace.ts +112 -33
  92. package/src/variation-definitions.ts +1540 -0
  93. package/src/variation-gates.ts +89 -0
  94. package/src/variation-tokens.ts +355 -0
@@ -0,0 +1,89 @@
1
+ // asmlift — the admission tables a variation's definition names, so a reader sees the rules
2
+ // enumeration applies rather than a paraphrase of them.
3
+ //
4
+ // A `VariationDefinition.offeredWhen` names a table by its key here, and the drawer renders each
5
+ // table's own `why` strings. The key IS the exported binding's name (object shorthand), and
6
+ // `packages/core/test/variation-offers.test.ts` holds each key to the export of that name in the
7
+ // file the definition's `implementedIn` names.
8
+ //
9
+ // Kept apart from `variation-definitions.ts`, which stays pure data: that module takes only the
10
+ // TYPE of a key, so a consumer reading a title or a summary does not load the passes.
11
+ import { ADVANCE_HEAD_GATES, ADVANCE_MEMBER_GATES } from './l3/advance';
12
+ import { BASEFOLD_GATES, LIVEBASE_BLOCK_GATES, LIVEBASE_GATES, ORDERBASE_GATES, UNFOLDED_GATES } from './l3/basecse';
13
+ import { ARM_DISJOINT_GATES, COALESCE_GATES } from './l3/coalesce';
14
+ import type { Gate } from './l3/gates';
15
+ import { HOMESPLIT_FAN_GATES, HOMESPLIT_GATES } from './l3/homesplit';
16
+ import { INLINEBASE_GATES } from './l3/inlinebase';
17
+ import { OFFMEMBER_GATES } from './l3/offmember';
18
+ import { PTR_FIELD_GATES } from './l3/ptrfield';
19
+ import { COUNTDOWN_GATES } from './l3/reindex';
20
+ import { REGIONBASE_GATES, SCOPEBASE_ELIGIBILITY, SCOPEBASE_GATES } from './l3/scopebase';
21
+ import {
22
+ UNMERGE_ARM_GATES,
23
+ UNMERGE_RUNG_GATES,
24
+ UNMERGE_SITE_GATES,
25
+ UNMERGE_TOTALITY_GATES,
26
+ UNMERGE_VALUE_GATES,
27
+ } from './l3/unmerge';
28
+ import { UNREDUCE_GATES } from './l3/unreduce';
29
+ import { VOL_SLOT_GATES } from './l3/volatileval';
30
+ import { VOL_STORE_GATES } from './l3/volstore';
31
+ import { ARM_REREAD_GATES } from './raise/shortcircuit';
32
+ import { NAME_COALESCE_GATES } from './structure/namecoalesce';
33
+ import { FRESH_MERGE_GATES } from './structure/structure';
34
+
35
+ /** One rule as a reader sees it: what it refuses and whether removing it would make a candidate wrong. */
36
+ export interface ReaderRule {
37
+ id: string;
38
+ why: string;
39
+ sound: boolean;
40
+ }
41
+
42
+ export const VARIATION_GATE_TABLES = {
43
+ ADVANCE_HEAD_GATES,
44
+ ADVANCE_MEMBER_GATES,
45
+ ARM_DISJOINT_GATES,
46
+ ARM_REREAD_GATES,
47
+ BASEFOLD_GATES,
48
+ COALESCE_GATES,
49
+ COUNTDOWN_GATES,
50
+ FRESH_MERGE_GATES,
51
+ HOMESPLIT_FAN_GATES,
52
+ HOMESPLIT_GATES,
53
+ INLINEBASE_GATES,
54
+ LIVEBASE_BLOCK_GATES,
55
+ LIVEBASE_GATES,
56
+ NAME_COALESCE_GATES,
57
+ OFFMEMBER_GATES,
58
+ ORDERBASE_GATES,
59
+ PTR_FIELD_GATES,
60
+ REGIONBASE_GATES,
61
+ SCOPEBASE_ELIGIBILITY,
62
+ SCOPEBASE_GATES,
63
+ UNFOLDED_GATES,
64
+ UNMERGE_ARM_GATES,
65
+ UNMERGE_RUNG_GATES,
66
+ UNMERGE_SITE_GATES,
67
+ UNMERGE_TOTALITY_GATES,
68
+ UNMERGE_VALUE_GATES,
69
+ UNREDUCE_GATES,
70
+ VOL_SLOT_GATES,
71
+ VOL_STORE_GATES,
72
+ } as const satisfies Record<string, readonly Gate<never>[]>;
73
+
74
+ /** A table a variation's definition can name. */
75
+ export type GateTableName = keyof typeof VARIATION_GATE_TABLES;
76
+
77
+ /** The rules of `tables`, in table order, each once: a rule two tables word alike is listed once. */
78
+ export function readerRules(tables: readonly GateTableName[]): ReaderRule[] {
79
+ const seen = new Set<string>();
80
+ return tables.flatMap((t) =>
81
+ VARIATION_GATE_TABLES[t].flatMap(({ id, why, sound }) => {
82
+ if (seen.has(why)) {
83
+ return [];
84
+ }
85
+ seen.add(why);
86
+ return [{ id, why, sound }];
87
+ }),
88
+ );
89
+ }
@@ -0,0 +1,355 @@
1
+ // asmlift — the VARIATION REGISTRY: every variation a candidate's name can carry, as data.
2
+ //
3
+ // A candidate is named by the variations it applied, an ordered list: its signedness first, then
4
+ // its lift, structure and respell variations, then the symbol-map variation, e.g.
5
+ // `['unsigned', 'defsite', 'raw-globals']`. Joined with `/` — `unsigned/defsite/raw-globals` — the
6
+ // list is how a name is PRINTED and typed back (`bench fan --show`), and nothing else: the list is
7
+ // the name. A few variations are applied to something the entry has to name — `coalesce-v0-v1`
8
+ // merges `v0` into `v1`, `volatile-p1` qualifies `p1` — and that trailing `-…` is the variation's
9
+ // SUBJECT.
10
+ //
11
+ // WHY A CLOSED TABLE. Enumeration (`rank.ts`, `rank-variations.ts`) mints a name as a list of
12
+ // `Variation`s: a `VariationName`, or a subject-taking name applied through `withSubject`. So a
13
+ // variation the table does not hold is a type error at its mint site, and a subject that does not
14
+ // fit its pattern throws where it is minted. The other direction — an entry nothing mints any more
15
+ // — is `packages/core/test/variation-mints.test.ts`, which reads the mint sites off the type
16
+ // checker. What each name means to a reader is `variation-definitions.ts`, keyed by
17
+ // `VariationName` below.
18
+ //
19
+ // WHY A TEST PREDICATE GOES THROUGH HERE. A predicate written as a substring —
20
+ // `name.includes('/regcopy-ret')` inside `.toEqual([])` — keeps passing after the variation is
21
+ // spelled differently, and then asserts nothing. `hasVariation` throws on a name this table does
22
+ // not register, so a stale predicate fails instead. It also compares whole variations, never
23
+ // substrings: `hasVariation(v, 'livebase')` is false on `livebase-block`, so a predicate about a
24
+ // family of variations names each member.
25
+ //
26
+ // WHY A TARGET GATE IS HERE TOO. A variation offered only on some compilers is withheld by
27
+ // `offeredOn`, which enumeration asks before it builds a candidate, and the webapp's drawer reads the
28
+ // same entry. What a reader is told about the target is the rule enumeration applies.
29
+ //
30
+ // Pure data and pure functions: this module stays browser-safe.
31
+ import type { TargetDescription } from './target';
32
+
33
+ /** The variation kinds, in the order their variations appear in a candidate's name. */
34
+ export const VARIATION_KINDS = ['signedness', 'lift', 'structure', 'respell', 'symbol-map'] as const;
35
+ export type VariationKind = (typeof VARIATION_KINDS)[number];
36
+
37
+ /** The compiler behavior a variation's offer depends on: offered only where the target declares
38
+ * `behavior`, or, with `declared: false`, only where it does not. `unlessWith` names the variation
39
+ * whose company lifts the restriction; `RegisteredToken` holds it to a registered name. */
40
+ export interface TargetGate<B extends CompilerBehavior = CompilerBehavior, N extends string = string> {
41
+ behavior: B;
42
+ declared: boolean;
43
+ unlessWith?: N;
44
+ }
45
+
46
+ type CompilerBehavior = keyof TargetDescription['compilerBehaviors'];
47
+
48
+ export interface VariationToken {
49
+ /** the registered spelling; a `-` inside it is part of the name (`livebase-block`, `vol-slot`) */
50
+ name: string;
51
+ variationKind: VariationKind;
52
+ /** what may follow `name-` when the variation names what it was applied to; absent for a
53
+ * variation that takes no subject. Anchored by `parseVariation`, never here. */
54
+ subject?: RegExp;
55
+ target?: TargetGate;
56
+ }
57
+
58
+ /** The local names a multi-result variation's subject lists, `-`-joined: `v0-v1`, `p0-p1-p2`. */
59
+ const LOCALS = /[a-z]+\d+(?:-[a-z]+\d+)*/;
60
+
61
+ /** Every variation, grouped by kind in name order. The order of this table is not published
62
+ * behaviour: enumeration order is decided in `rank.ts` and `rank-variations.ts`, never here. */
63
+ const TOKENS = [
64
+ // signedness: always the first part of a name, because both answers are enumerated
65
+ { name: 'unsigned', variationKind: 'signedness' },
66
+ { name: 'signed', variationKind: 'signedness' },
67
+ // lift: the assembly lifted or raised again
68
+ { name: 'setup-args', variationKind: 'lift' },
69
+ { name: 'connective', variationKind: 'lift' },
70
+ { name: 'shared-ret', variationKind: 'lift' },
71
+ { name: 'shared-tail', variationKind: 'lift' },
72
+ // structure: `structure()` re-run with other options
73
+ { name: 'flip-branch', variationKind: 'structure' },
74
+ { name: 'defsite', variationKind: 'structure' },
75
+ { name: 'loop-entry', variationKind: 'structure' },
76
+ { name: 'flip-join', variationKind: 'structure' },
77
+ { name: 'sense', variationKind: 'structure', subject: /\d+/ },
78
+ { name: 'no-bitfield', variationKind: 'structure' },
79
+ { name: 'no-ptr-elem', variationKind: 'structure' },
80
+ { name: 'flat-rank', variationKind: 'structure' },
81
+ { name: 'reread-globals', variationKind: 'structure' },
82
+ { name: 'inplace', variationKind: 'structure' },
83
+ { name: 'merge-names', variationKind: 'structure' },
84
+ { name: 'addr-home', variationKind: 'structure' },
85
+ { name: 'expr-home', variationKind: 'structure' },
86
+ { name: 'derived-home', variationKind: 'structure' },
87
+ { name: 'merge-home', variationKind: 'structure' },
88
+ { name: 'uns-cmp', variationKind: 'structure' },
89
+ { name: 'fresh-merge', variationKind: 'structure' },
90
+ { name: 'copy-defpos', variationKind: 'structure' },
91
+ { name: 'site-sense', variationKind: 'structure' },
92
+ // respell: the structured tree rewritten
93
+ { name: 'unmerge', variationKind: 'respell' },
94
+ { name: 'argbase', variationKind: 'respell' },
95
+ { name: 'zerosub', variationKind: 'respell' },
96
+ { name: 'volatile', variationKind: 'respell', subject: LOCALS },
97
+ { name: 'vol-slot', variationKind: 'respell' },
98
+ { name: 'vol-store', variationKind: 'respell' },
99
+ { name: 'unreduce', variationKind: 'respell' },
100
+ { name: 'ptr-field', variationKind: 'respell' },
101
+ { name: 'offmember', variationKind: 'respell', target: { behavior: 'foldsConstAddrOffset', declared: true } },
102
+ { name: 'inlinebase', variationKind: 'respell' },
103
+ { name: 'scopebase', variationKind: 'respell' },
104
+ { name: 'regionbase', variationKind: 'respell' },
105
+ { name: 'coalesce', variationKind: 'respell', subject: LOCALS },
106
+ { name: 'indexed', variationKind: 'respell' },
107
+ { name: 'livebase', variationKind: 'respell' },
108
+ { name: 'livebase-block', variationKind: 'respell' },
109
+ { name: 'basefold', variationKind: 'respell', target: { behavior: 'foldsConstAddrOffset', declared: true } },
110
+ { name: 'unfolded', variationKind: 'respell', target: { behavior: 'foldsConstAddrOffset', declared: true } },
111
+ { name: 'orderbase', variationKind: 'respell', target: { behavior: 'arrayShapeFromStride', declared: true } },
112
+ {
113
+ name: 'orderbase-scoped',
114
+ variationKind: 'respell',
115
+ target: { behavior: 'arrayShapeFromStride', declared: true },
116
+ },
117
+ { name: 'homesplit', variationKind: 'respell', subject: /[^/,\s]+/ },
118
+ { name: 'mulfirst', variationKind: 'respell' },
119
+ { name: 'nearbase', variationKind: 'respell', target: { behavior: 'nearBaseSpan', declared: true } },
120
+ {
121
+ name: 'advance',
122
+ variationKind: 'respell',
123
+ target: { behavior: 'foldsPointerAdvance', declared: false, unlessWith: 'volatile' },
124
+ },
125
+ { name: 'parkfirst', variationKind: 'respell' },
126
+ { name: 'sinkinit', variationKind: 'respell' },
127
+ { name: 'regcopy', variationKind: 'respell', subject: /ret|ret-fresh/ },
128
+ { name: 'initfirst', variationKind: 'respell' },
129
+ { name: 'pollguard', variationKind: 'respell' },
130
+ { name: 'pollread', variationKind: 'respell' },
131
+ // symbol map: the map's shaped spellings withheld; always the last part
132
+ { name: 'raw-globals', variationKind: 'symbol-map' },
133
+ ] as const satisfies readonly VariationToken[];
134
+
135
+ /** A registered variation's name. `variation-definitions.ts` keys its definitions by this type, so a
136
+ * registry entry without a definition, or a definition for a name the registry does not hold, is a
137
+ * type error. */
138
+ export type VariationName = (typeof TOKENS)[number]['name'];
139
+
140
+ /** A registry entry: its name is registered, and its target gate names a behavior some entry gates on
141
+ * and, in `unlessWith`, a registered variation. */
142
+ export type RegisteredToken = VariationToken & {
143
+ name: VariationName;
144
+ target?: TargetGate<GatingBehavior, VariationName>;
145
+ };
146
+
147
+ export const VARIATION_TOKENS: readonly RegisteredToken[] = TOKENS;
148
+
149
+ /** A registered variation that names what it was applied to. */
150
+ export type SubjectVariationName = Extract<(typeof TOKENS)[number], { subject: RegExp }>['name'];
151
+
152
+ /** A compiler behavior some registered variation's offer depends on. */
153
+ export type GatingBehavior = Extract<(typeof TOKENS)[number], { target: TargetGate }>['target']['behavior'];
154
+
155
+ declare const subjectFitted: unique symbol;
156
+
157
+ /** A subject-taking variation applied to a subject, `coalesce-v0-v1`. Only `withSubject` makes one,
158
+ * so its subject fits the registered pattern. */
159
+ export type SubjectVariation = `${SubjectVariationName}-${string}` & { readonly [subjectFitted]: true };
160
+
161
+ /** One part of a candidate's name as enumeration mints it. */
162
+ export type Variation = VariationName | SubjectVariation;
163
+
164
+ const BY_NAME = new Map<string, RegisteredToken>(VARIATION_TOKENS.map((t) => [t.name, t]));
165
+
166
+ /** Each subject pattern, anchored. */
167
+ const SUBJECT = new Map<string, RegExp>(
168
+ VARIATION_TOKENS.flatMap((t) => (t.subject === undefined ? [] : [[t.name, new RegExp(`^(?:${t.subject.source})$`)]])),
169
+ );
170
+
171
+ /** `name` applied to `subject`: `withSubject('coalesce', 'v0-v1')` is `coalesce-v0-v1`. Throws on a
172
+ * subject the registered pattern does not fit. */
173
+ export function withSubject(name: SubjectVariationName, subject: string): SubjectVariation {
174
+ if (!SUBJECT.get(name)!.test(subject)) {
175
+ throw new Error(`'${name}' takes no subject '${subject}' (packages/core/src/variation-tokens.ts)`);
176
+ }
177
+ return `${name}-${subject}` as SubjectVariation;
178
+ }
179
+
180
+ /** The registry entry for a name, or a throw naming what is registered. */
181
+ export function variationToken(name: string): RegisteredToken {
182
+ const t = BY_NAME.get(name);
183
+ if (t === undefined) {
184
+ throw new Error(`'${name}' is not a registered variation (packages/core/src/variation-tokens.ts)`);
185
+ }
186
+ return t;
187
+ }
188
+
189
+ /** May enumeration offer a candidate carrying `variations` on `target`? False when one of them has a
190
+ * target gate the target's compiler behaviors fail and no variation its `unlessWith` names is among
191
+ * them. A behavior is declared when present and not `false`. */
192
+ export function offeredOn(target: TargetDescription, variations: readonly Variation[]): boolean {
193
+ const names = variations.map((v) => parseVariation(v).name);
194
+ return names.every((n) => {
195
+ const gate = variationToken(n).target;
196
+ if (gate === undefined || (gate.unlessWith !== undefined && names.includes(gate.unlessWith))) {
197
+ return true;
198
+ }
199
+ const value = target.compilerBehaviors[gate.behavior];
200
+ return (value !== undefined && value !== false) === gate.declared;
201
+ });
202
+ }
203
+
204
+ /** One part of a candidate's name, split into the variation it names and that variation's subject.
205
+ * The longest registered name wins, so `livebase-block` is never `livebase` applied to `block`.
206
+ * Throws on a part no registered variation spells. */
207
+ export function parseVariation(part: string): { name: VariationName; subject?: string } {
208
+ const exact = BY_NAME.get(part);
209
+ if (exact !== undefined) {
210
+ return { name: exact.name };
211
+ }
212
+ let best: { name: VariationName; subject: string } | undefined;
213
+ for (const t of VARIATION_TOKENS) {
214
+ if (t.subject === undefined || !part.startsWith(`${t.name}-`)) {
215
+ continue;
216
+ }
217
+ const subject = part.slice(t.name.length + 1);
218
+ if (SUBJECT.get(t.name)!.test(subject) && (best === undefined || t.name.length > best.name.length)) {
219
+ best = { name: t.name, subject };
220
+ }
221
+ }
222
+ if (best === undefined) {
223
+ throw new Error(`'${part}' names no registered variation (packages/core/src/variation-tokens.ts)`);
224
+ }
225
+ return best;
226
+ }
227
+
228
+ /** Does this candidate apply the variation `name`? An omitted `subject` matches any subject
229
+ * (`hasVariation(v, 'volatile')` is true on `volatile` and on `volatile-p0-p1`); `null` matches
230
+ * only the variation applied with no subject (`regcopy`, not `regcopy-ret`). Throws when `name` is
231
+ * not registered, when a string `subject` is given for a variation that takes none or does not fit
232
+ * its pattern, and when any of the candidate's own variations is unregistered. */
233
+ export function hasVariation(variations: readonly string[], name: string, subject?: string | null): boolean {
234
+ const t = variationToken(name);
235
+ if (typeof subject === 'string' && !(SUBJECT.get(t.name)?.test(subject) ?? false)) {
236
+ throw new Error(`'${name}' takes no subject '${subject}' (packages/core/src/variation-tokens.ts)`);
237
+ }
238
+ return variations.some((part) => {
239
+ const p = parseVariation(part);
240
+ return p.name === name && (subject === undefined || p.subject === (subject ?? undefined));
241
+ });
242
+ }
243
+
244
+ /** Does this candidate apply the variations `names` consecutively, in that order — e.g.
245
+ * `['basefold', 'sinkinit']`? Each entry matches any subject. Throws like `hasVariation`. */
246
+ export function hasVariations(variations: readonly string[], names: readonly string[]): boolean {
247
+ names.forEach(variationToken);
248
+ if (names.length === 0) {
249
+ throw new Error('hasVariations needs at least one name');
250
+ }
251
+ const parsed = variations.map((part) => parseVariation(part).name);
252
+ for (let i = 0; i + names.length <= parsed.length; i++) {
253
+ if (names.every((n, j) => parsed[i + j] === n)) {
254
+ return true;
255
+ }
256
+ }
257
+ return false;
258
+ }
259
+
260
+ /** A candidate's variations as ONE string, `/`-joined: how a name is printed, typed back, hashed
261
+ * and used as a key. Throws on an empty entry and on an entry that contains `/`, so the join is
262
+ * injective — `['a/b']` and `['a', 'b']` can never print, key or hash alike. */
263
+ export function joinVariations(variations: readonly string[]): string {
264
+ if (variations.length === 0) {
265
+ throw new Error('a candidate applies at least its signedness variation');
266
+ }
267
+ for (const v of variations) {
268
+ if (v === '' || v.includes('/')) {
269
+ throw new Error(`'${v}' cannot be one variation: an entry is non-empty and holds no '/'`);
270
+ }
271
+ }
272
+ return variations.join('/');
273
+ }
274
+
275
+ /** The inverse of `joinVariations`: a printed name (`unsigned/defsite`) back to its variations.
276
+ * Throws on an empty entry (`unsigned//defsite`, a leading or trailing `/`). */
277
+ export function splitVariations(name: string): string[] {
278
+ const variations = name.split('/');
279
+ joinVariations(variations);
280
+ return variations;
281
+ }
282
+
283
+ /** How many of one fan's candidates carry one variation. `candidates` counts the whole fan —
284
+ * scored, dropped and withheld alike — and `dropped` and `withheld` are the refused part of that
285
+ * count, each absent when 0. */
286
+ export interface VariationTally {
287
+ candidates: number;
288
+ dropped?: number;
289
+ withheld?: number;
290
+ }
291
+
292
+ interface NamedCandidate {
293
+ variations: readonly string[];
294
+ }
295
+
296
+ /** Every variation a fan carried, keyed by its REGISTERED name, with how many of the fan's
297
+ * candidates carry it. A variation applied to a subject counts under its registered name
298
+ * (`coalesce-v0-v1` and `coalesce-v2-v3` are both `coalesce`), and a candidate counts once under
299
+ * each name it carries however many subjects it applies it to.
300
+ *
301
+ * `fan` is ranking's three-way partition, which puts every enumerated candidate in exactly one
302
+ * list, so each signedness entry's `candidates` sums with the other's to the fan size.
303
+ *
304
+ * A tally, not a factorisation: enumeration gates prune the fan, so the counts do not multiply to
305
+ * its size. Keys run in kind order, then by name, so two tallies of one fan serialize to the same
306
+ * bytes whatever order the fan was listed in. Throws on a variation the registry does not hold. */
307
+ export function tallyFanVariations(fan: {
308
+ candidates: readonly NamedCandidate[];
309
+ dropped: readonly NamedCandidate[];
310
+ withheld: readonly NamedCandidate[];
311
+ }): Record<string, VariationTally> {
312
+ const registeredName = new Map<string, string>();
313
+ const nameOf = (part: string): string => {
314
+ let name = registeredName.get(part);
315
+ if (name === undefined) {
316
+ name = parseVariation(part).name;
317
+ registeredName.set(part, name);
318
+ }
319
+ return name;
320
+ };
321
+ const counts = new Map<string, { candidates: number; dropped: number; withheld: number }>();
322
+ const add = (list: readonly NamedCandidate[], refusal: 'dropped' | 'withheld' | undefined): void => {
323
+ for (const c of list) {
324
+ for (const name of new Set(c.variations.map(nameOf))) {
325
+ let n = counts.get(name);
326
+ if (n === undefined) {
327
+ n = { candidates: 0, dropped: 0, withheld: 0 };
328
+ counts.set(name, n);
329
+ }
330
+ n.candidates++;
331
+ if (refusal !== undefined) {
332
+ n[refusal]++;
333
+ }
334
+ }
335
+ }
336
+ };
337
+ add(fan.candidates, undefined);
338
+ add(fan.dropped, 'dropped');
339
+ add(fan.withheld, 'withheld');
340
+ const kindIndex = (name: string): number => VARIATION_KINDS.indexOf(variationToken(name).variationKind);
341
+ const names = [...counts.keys()].sort((a, b) => kindIndex(a) - kindIndex(b) || (a < b ? -1 : a > b ? 1 : 0));
342
+ return Object.fromEntries(
343
+ names.map((name) => {
344
+ const n = counts.get(name)!;
345
+ return [
346
+ name,
347
+ {
348
+ candidates: n.candidates,
349
+ ...(n.dropped ? { dropped: n.dropped } : {}),
350
+ ...(n.withheld ? { withheld: n.withheld } : {}),
351
+ },
352
+ ];
353
+ }),
354
+ );
355
+ }