@asmlift/core 0.10.0 → 0.11.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 (68) hide show
  1. package/package.json +1 -1
  2. package/src/backend/c.ts +1 -0
  3. package/src/backend/cfamily.ts +33 -12
  4. package/src/backend/cpp.ts +1 -0
  5. package/src/backend/lines.ts +10 -0
  6. package/src/backend/pascal.ts +17 -5
  7. package/src/cdecl/parse.ts +21 -0
  8. package/src/contracts.ts +73 -20
  9. package/src/frontend/arm-body.ts +155 -0
  10. package/src/frontend/asmdata.ts +2 -1
  11. package/src/frontend/call-plan.ts +197 -45
  12. package/src/frontend/callee-body.ts +370 -0
  13. package/src/frontend/device-pins.ts +18 -8
  14. package/src/frontend/frame-objects.ts +504 -61
  15. package/src/frontend/frontend.ts +20 -0
  16. package/src/frontend/jump-table.ts +35 -20
  17. package/src/frontend/mips.ts +226 -8
  18. package/src/frontend/ppc.ts +26 -2
  19. package/src/frontend/register-struct-call.ts +1 -86
  20. package/src/frontend/ssa.ts +263 -1
  21. package/src/frontend/stackargs.ts +95 -17
  22. package/src/frontend/thumb-entry.ts +340 -0
  23. package/src/frontend/thumb.ts +410 -61
  24. package/src/ir/alias.ts +29 -8
  25. package/src/ir/bits.ts +1081 -66
  26. package/src/ir/core.ts +154 -4
  27. package/src/ir/entry.ts +3 -5
  28. package/src/ir/opcodes.ts +28 -0
  29. package/src/ir/parse.ts +1 -0
  30. package/src/ir/simplify.ts +61 -10
  31. package/src/ir/verify.ts +16 -0
  32. package/src/l3/argbase.ts +2 -0
  33. package/src/l3/ast.ts +294 -2
  34. package/src/l3/basecse.ts +35 -10
  35. package/src/l3/caseendgoto.ts +223 -0
  36. package/src/l3/coalesce.ts +42 -14
  37. package/src/l3/constinit.ts +34 -6
  38. package/src/l3/dce.ts +52 -8
  39. package/src/l3/ptrcell.ts +2 -0
  40. package/src/l3/regspell.ts +2 -0
  41. package/src/l3/reindex.ts +13 -4
  42. package/src/l3/scopebase.ts +2 -0
  43. package/src/l3/tailmerge.ts +2 -0
  44. package/src/pipeline.ts +507 -23
  45. package/src/proto-context.ts +135 -1
  46. package/src/raise/const.ts +25 -0
  47. package/src/raise/declared-volatile.ts +1 -1
  48. package/src/raise/divpow2.ts +192 -0
  49. package/src/raise/offsetnames.ts +412 -10
  50. package/src/raise/pre-recovery.ts +8 -5
  51. package/src/raise/pre-structure.ts +24 -5
  52. package/src/raise/reroll.ts +4 -24
  53. package/src/raise/shortcircuit.ts +192 -23
  54. package/src/raise/unpre.ts +594 -0
  55. package/src/rank-variations.ts +97 -4
  56. package/src/rank.ts +258 -43
  57. package/src/structure/analysis.ts +56 -10
  58. package/src/structure/cross-jumped-tail.ts +216 -0
  59. package/src/structure/globalaccess.ts +46 -1
  60. package/src/structure/structure.ts +4221 -368
  61. package/src/structure/switch-recover.ts +345 -26
  62. package/src/structure/unfolded-branch-constant.ts +90 -0
  63. package/src/symbols.ts +52 -15
  64. package/src/target.ts +269 -8
  65. package/src/trace.ts +28 -14
  66. package/src/variation-definitions.ts +207 -6
  67. package/src/variation-gates.ts +2 -0
  68. package/src/variation-tokens.ts +63 -2
@@ -264,6 +264,7 @@ export interface VariationDefinition {
264
264
  const CALLS =
265
265
  'void A(); void B(); void C(); void D(); void X(); void Y(); void P(); void Q(); s32 f(); s32 g(); s32 h(); void use();\n';
266
266
 
267
+ const PIPELINE = 'packages/core/src/pipeline.ts';
267
268
  const RANK = 'packages/core/src/rank.ts';
268
269
  const RANK_VARIATIONS = 'packages/core/src/rank-variations.ts';
269
270
  const SYMBOLS = 'packages/core/src/symbols.ts';
@@ -400,10 +401,14 @@ export const VARIATION_DEFINITIONS: { readonly [N in VariationName]: VariationDe
400
401
  title: 'Shared code after an early return',
401
402
  summary: 'code both arms of an `if` reach before returning is written once, after the `if`',
402
403
  detail:
403
- 'An `if` whose arms never meet again before the function returns has no follow, so by default a ' +
404
- 'region both arms reach is written in each of them. This structures the same raised function again ' +
405
- "with that shared region as the `if`'s follow: it is written once after the `if`, and every other " +
406
- 'path leaves through an early `return`.',
404
+ 'An `if` whose arms never meet again before the function returns has no follow, so under the ' +
405
+ 'return-tail copies (`ret-copy`) a region both arms reach is written in each of them. This structures ' +
406
+ "the same raised function again with the copies and that shared region as the `if`'s follow: it is " +
407
+ 'written once after the `if`, and every other path leaves through an early `return`. On agbcc, ' +
408
+ "this example's compiler, the copies are the default, so this is the step from them to the region " +
409
+ 'written once. Where the join past early returns is the default instead (CodeWarrior ' +
410
+ "2.4.2 and 2.3.3), it already writes this example's region once, since the paths that do not " +
411
+ 'return meet at it: the variation is a reading of the copies, ranked beside them.',
407
412
  offeredWhen: {
408
413
  when: 'Some `if` has arms that reach a common `return` block and no common block before it.',
409
414
  decidedBy: { symbol: 'hasDivergentSharedRet', file: STRUCTURE },
@@ -415,7 +420,68 @@ export const VARIATION_DEFINITIONS: { readonly [N in VariationName]: VariationDe
415
420
  after: 'if (c) { A(); } else { if (x) { C(); return; } } B();',
416
421
  },
417
422
  implementedIn: STRUCTURE,
418
- seeAlso: ['shared-tail'],
423
+ seeAlso: ['shared-tail', 'ret-copy'],
424
+ },
425
+ 'ret-copy': {
426
+ title: 'Return tails copied',
427
+ summary: 'code below an `if` whose arms only meet at the `return` is written into each arm',
428
+ detail:
429
+ 'An `if` with an early `return` in one arm has no block both arms reach before the function ' +
430
+ 'returns. Where the join past early returns is the default (CodeWarrior 2.3.3, and 2.4.2, ' +
431
+ "this example's compiler), asmlift gives it the block where the paths that do not return meet as its " +
432
+ 'join, so the code from there is written once and every other path leaves through its early ' +
433
+ '`return`; that also reads early returns the compiler merged into the epilogue or cross-jumped ' +
434
+ 'into one return tail, and a `switch` with a `case` that returns early. This structures the same ' +
435
+ 'raised function again with no such join: everything below the `if` is written into each arm, ' +
436
+ 'once more per such `if` of a chain, which is the source some functions were written as. Where ' +
437
+ 'the copies spell the whole function in fewer lines than the join, where the compiler is one that ' +
438
+ 'cross-jumps return tails and the layout shows it did, or where an early return keeps a constant ' +
439
+ 'its branch proved unfolded (gcc 2.x below -O2), they are the default instead, and the join is ' +
440
+ 'offered as `ret-join`. On agbcc and IDO 7.1, measured against held-out functions where the join ' +
441
+ 'loses matches, on GCC 2.7.2 and KMC, measured against generated functions where it does, and on CodeWarrior 2.4.7, where no held-out function was measured, the copies are ' +
442
+ 'the default outright, and this variation is never offered there. Past the copy budget the copies ' +
443
+ 'are not offered.',
444
+ offeredWhen: {
445
+ when: 'Some `if` or `switch` outside every loop took a join past early returns that the default kept.',
446
+ decidedBy: { symbol: 'structureReturnTailReading', file: PIPELINE },
447
+ },
448
+ example: {
449
+ compiler: 'mwcc',
450
+ unit: CALLS + 'extern s32 gT; void example(s32 a, s32 b) { @ }',
451
+ before: 'if (a > 3) { if (b == 1) { A(); gT = 0; return; } B(); } else { C(); } X(); Y();',
452
+ after: 'if (a > 3) { if (b == 1) { A(); gT = 0; return; } B(); X(); Y(); } else { C(); X(); Y(); }',
453
+ note: 'the shape of `synthetic:retchain_one`',
454
+ },
455
+ implementedIn: STRUCTURE,
456
+ seeAlso: ['shared-ret', 'ret-join'],
457
+ },
458
+ 'ret-join': {
459
+ title: 'Join past early returns, where the copies are the default',
460
+ summary: 'the join past early returns, where it writes a return tail into early returns',
461
+ detail:
462
+ 'The join past early returns writes the code below a divergent `if` once, but where the paths that ' +
463
+ 'do not return meet at a block only some of them reach, every other path becomes an early `return` ' +
464
+ 'that takes its own copy of the tail below. Where that spells the whole function in more lines than ' +
465
+ 'the return-tail copies, or where the layout shows the compiler cross-jumped a return tail the ' +
466
+ 'source wrote into every arm, or where an early return keeps a constant its branch proved unfolded ' +
467
+ '(gcc 2.x below -O2), the copies are the default and this is the join, offered beside them, which is ' +
468
+ "the source some functions were written as. On agbcc, this example's compiler, and on IDO 7.1, GCC 2.7.2, KMC and " +
469
+ 'CodeWarrior 2.4.7, the copies are the default outright: the join is this variation ' +
470
+ 'wherever it is taken.',
471
+ offeredWhen: {
472
+ when: 'Some `if` or `switch` outside every loop took a join past early returns that the default gave up for the copies.',
473
+ decidedBy: { symbol: 'structureReturnTailReading', file: PIPELINE },
474
+ },
475
+ example: {
476
+ compiler: 'agbcc',
477
+ unit: CALLS + 'void example(s32 a) { @ }',
478
+ before: 'if (a == 1) { A(); B(); } else if (a == 2 || a == 3) { A(); } else { A(); B(); } X(); Y(); C();',
479
+ after:
480
+ 'if (a == 1) { A(); } else { if (a == 2 || a == 3) { A(); X(); Y(); C(); return; } A(); } B(); X(); Y(); C();',
481
+ note: 'the shape of sa3 `sub_809A808`',
482
+ },
483
+ implementedIn: PIPELINE,
484
+ seeAlso: ['ret-copy'],
419
485
  },
420
486
  'shared-tail': {
421
487
  title: 'Store tail copied back',
@@ -423,7 +489,7 @@ export const VARIATION_DEFINITIONS: { readonly [N in VariationName]: VariationDe
423
489
  detail:
424
490
  "agbcc's global common-subexpression pass can move a trailing store into every predecessor of a join, " +
425
491
  'and the paths then jump into one `store; return` tail. This lift copies that tail back into each ' +
426
- 'path that branches to it, and then structures with the shared follow `shared-ret` uses, so the ' +
492
+ 'path that branches to it, and then structures with the copies and the shared follow `shared-ret` uses, so the ' +
427
493
  'store the source wrote once is written after the `if` and the others before an early `return`. It is ' +
428
494
  'enumerated apart from `shared-ret` because the sink can delete the shared `return` that variation needs.',
429
495
  compilerBehavior:
@@ -957,6 +1023,107 @@ export const VARIATION_DEFINITIONS: { readonly [N in VariationName]: VariationDe
957
1023
  implementedIn: STRUCTURE,
958
1024
  seeAlso: ['flip-join', 'flip-branch', 'sense', 'connective'],
959
1025
  },
1026
+ 'tail-copies': {
1027
+ title: 'Shared switch tail copied into every arm',
1028
+ summary: 'every `case` that runs into the tail a cross-jump kept gets its own copy of it',
1029
+ detail:
1030
+ 'When several `case` arms end in the same code, the compiler keeps one copy and the other arms jump ' +
1031
+ "into it, so they all enter one arm's body. By default the arm placed directly above that body keeps " +
1032
+ 'falling into it and the others get a copy. This gives every one of them its own copy.',
1033
+ compilerBehavior:
1034
+ 'Which arm the source wrote the shared code under is not in the assembly. On KMC and gcc 2.7.2, an ' +
1035
+ 'arm written above its target in case order wants the fall-through, and arms written out of case ' +
1036
+ 'order want the copies.',
1037
+ offeredWhen: {
1038
+ when: "Structuring this lift without it left a jump-table arm falling into a sibling arm's body that another arm also enters.",
1039
+ decidedBy: { symbol: 'STRUCTURE_VARIATIONS', file: RANK_VARIATIONS },
1040
+ },
1041
+ example: {
1042
+ compiler: 'gcc',
1043
+ unit:
1044
+ 'extern s32 gOut; void f(s32); void g(s32); void h(s32); void m(s32); void r(s32);\n' +
1045
+ 'void example(s32 id, s32 v) { switch (id) { @ } r(0); }',
1046
+ before:
1047
+ 'case 0: f(1); break; case 1: g(v); case 2: gOut = v; break; case 3: h(v); gOut = v; break; case 4: m(5); break;',
1048
+ after:
1049
+ 'case 0: f(1); break; case 1: g(v); gOut = v; break; case 2: gOut = v; break; case 3: h(v); gOut = v; break; case 4: m(5); break;',
1050
+ note: 'cases 1 and 3 both enter case 2',
1051
+ },
1052
+ implementedIn: STRUCTURE,
1053
+ seeAlso: ['unmerge', 'shared-tail'],
1054
+ },
1055
+ 'for-continue': {
1056
+ title: 'Counted loop spelled as a `for`',
1057
+ summary: 'a `while (1)` whose last block steps and tests a counter is written `for (init; test; step)`',
1058
+ detail:
1059
+ 'A loop whose last block adds to a counter and tests it is written by default as `while (1)` with ' +
1060
+ '`i++; if (!(test)) break;` at the foot of its body, and a jump-table arm inside it that leaves the ' +
1061
+ 'loop copies the code after the loop and returns. This writes it as `for (i = init; test; i++)` instead, ' +
1062
+ 'so an arm that reaches the step is `continue;`, an arm that leaves the loop ends the switch, and a ' +
1063
+ '`break;` after the switch leaves the loop. Only a loop whose entry test is the same test (or folds to ' +
1064
+ 'true) and whose last block does nothing but step one counter and test it is written this way.',
1065
+ compilerBehavior:
1066
+ 'agbcc and gcc 2.7.2 lay a `for` out as a guard, the body and a latch that steps and tests, which is ' +
1067
+ 'also how they lay out the `while (1)` spelling of the same loop. Which one the source wrote decides ' +
1068
+ 'how the arms that leave the loop are laid out, and that is not in the assembly before it is compiled.',
1069
+ offeredWhen: {
1070
+ when: 'Structuring this lift without it met a `while (1)` loop whose last block steps one counter and tests it.',
1071
+ decidedBy: { symbol: 'STRUCTURE_VARIATIONS', file: RANK_VARIATIONS },
1072
+ },
1073
+ example: {
1074
+ compiler: 'agbcc',
1075
+ unit:
1076
+ 'struct E { s32 x; u8 id; u8 u; }; extern struct E gE[]; void k(s32);\n' +
1077
+ 'void example(s32 lo, s32 hi) { s32 i; @ k(99); }',
1078
+ before:
1079
+ 'if (lo <= hi) { i = lo; while (1) { switch (gE[i].id) { case 1: if (gE[i].u != 1) { k(4); k(99); return; } break; ' +
1080
+ 'case 2: if (gE[i].x <= 20) { k(5); k(99); return; } break; case 3: if (gE[i].x >= 0) { k(6); k(99); return; } break; ' +
1081
+ 'case 4: if (gE[i].x > 2) { k(7); k(99); return; } break; case 5: if (gE[i].x != 9) { k(8); k(99); return; } break; } ' +
1082
+ 'i++; if (i > hi) break; } }',
1083
+ after:
1084
+ 'for (i = lo; i <= hi; i++) { switch (gE[i].id) { case 1: if (gE[i].u == 1) continue; k(4); break; ' +
1085
+ 'case 2: if (gE[i].x > 20) continue; k(5); break; case 3: if (gE[i].x < 0) continue; k(6); break; ' +
1086
+ 'case 4: if (gE[i].x <= 2) continue; k(7); break; case 5: if (gE[i].x == 9) continue; k(8); break; ' +
1087
+ 'default: continue; } break; }',
1088
+ note: 'every arm either goes round the loop or calls `k` and leaves it; five arms, so agbcc dispatches through a jump table',
1089
+ },
1090
+ implementedIn: STRUCTURE,
1091
+ seeAlso: ['tail-copies'],
1092
+ },
1093
+ 'case-goto': {
1094
+ title: 'Shared case body entered by `goto`',
1095
+ summary:
1096
+ "every `case` that runs into another case's body the default copies it into ends `goto` to that case instead",
1097
+ detail:
1098
+ "When several `case` arms run on into one other arm's body, by default the arm placed directly above " +
1099
+ 'that body keeps falling into it and the others get a copy of it. This leaves the copies out: those ' +
1100
+ 'arms end in `goto` to a label on that arm, after its `case` labels. It is one of the two kinds of `goto` asmlift writes.',
1101
+ compilerBehavior:
1102
+ 'A compiler that cross-jumps keeps one copy of code that several arms end in, so a copy and a `goto` can ' +
1103
+ 'build the same object. agbcc stops that merge short where an arm already formed an address the copy ' +
1104
+ 'forms again, and the copy then compiles to more code than the `goto`.',
1105
+ offeredWhen: {
1106
+ when: "Structuring this lift without it copied a case arm's body into another jump-table arm that runs on into it.",
1107
+ decidedBy: { symbol: 'STRUCTURE_VARIATIONS', file: RANK_VARIATIONS },
1108
+ },
1109
+ example: {
1110
+ compiler: 'agbcc',
1111
+ unit:
1112
+ 'struct E { s32 x; u8 id; u8 f; u8 u; u8 v; }; extern struct E gE[]; extern s32 gOut; void k(s32);\n' +
1113
+ 'void example(s32 lo, s32 hi) { s32 i; for (i = lo; i <= hi; i++) { switch (gE[i].id) { @ default: continue; } break; } }',
1114
+ before:
1115
+ 'case 1: if (gE[i].u == 1) continue; if (gE[i].x > 20) continue; gOut = 1; k(i); break; ' +
1116
+ 'case 2: if (gE[i + 1].v != 0) continue; case 3: case 4: if (gE[i].x > 20) continue; gOut = 1; k(i); break; ' +
1117
+ 'case 7: k(7); break;',
1118
+ after:
1119
+ 'case 1: if (gE[i].u == 1) continue; goto case_3; ' +
1120
+ 'case 2: if (gE[i + 1].v != 0) continue; case 3: case 4: case_3: if (gE[i].x > 20) continue; gOut = 1; k(i); ' +
1121
+ 'break; case 7: k(7); break;',
1122
+ note: 'cases 1 and 2 both run on into the body of cases 3 and 4',
1123
+ },
1124
+ implementedIn: STRUCTURE,
1125
+ seeAlso: ['tail-copies'],
1126
+ },
960
1127
 
961
1128
  // ── respell ─────────────────────────────────────────────────────────────────────────────────
962
1129
  unmerge: {
@@ -990,6 +1157,40 @@ export const VARIATION_DEFINITIONS: { readonly [N in VariationName]: VariationDe
990
1157
  implementedIn: l3('unmerge'),
991
1158
  seeAlso: ['initfirst', 'regionbase'],
992
1159
  },
1160
+ 'case-end-goto': {
1161
+ title: 'Search loop leaving its case by `goto`',
1162
+ summary: "a search loop's hit that skips the rest of its `case` jumps to a label on the case's closing `break;`",
1163
+ detail:
1164
+ 'When a loop inside a `case` has a second way out that skips the code after the loop and ends the ' +
1165
+ '`case`, the default sets a flag before it leaves the loop and runs that code under `if (flag == 0)` ' +
1166
+ 'after it. This leaves the flag out: the second way out is `goto` to a label on the `break;` that ' +
1167
+ 'ends the case, and the code after the loop runs unguarded before that label. It is one of the two ' +
1168
+ 'kinds of `goto` asmlift writes.',
1169
+ compilerBehavior:
1170
+ 'agbcc sends a `goto` to a `break;` straight to the end of the `switch`, and past a loop the switch ' +
1171
+ 'ends, so only the `goto` builds the bytes the source did; the flag adds a variable and its tests.',
1172
+ offeredWhen: {
1173
+ judges: "each flag set at a loop's own `break` and tested right after that loop",
1174
+ gates: ['CASE_END_GOTO_GATES'],
1175
+ },
1176
+ example: {
1177
+ compiler: 'agbcc',
1178
+ unit:
1179
+ 'struct E { s32 x; u8 id; u8 f; u8 u; u8 v; }; extern struct E gE[]; extern s32 gLo, gHi; void k(s32);\n' +
1180
+ 'void example(s32 lo, s32 hi) { s32 i, j, h, a = 0; for (i = lo; i <= hi; i++) { if (gE[i].f <= 26) { ' +
1181
+ 'switch (gE[i].id) { @ default: continue; } break; } } k(a); }',
1182
+ before:
1183
+ 'case 1: case 2: if (gE[i].x > 5) continue; h = 0; for (j = gLo; j <= gHi; j++) { if (i != j && gE[j].f == 28) ' +
1184
+ '{ a = 1; k(j); h = 1; break; } } if (h == 0) { gE[i].f = 19; k(i); } break; case 4: k(i + 4); break;',
1185
+ after:
1186
+ 'case 1: case 2: if (gE[i].x > 5) continue; for (j = gLo; j <= gHi; j++) { if (i != j && gE[j].f == 28) ' +
1187
+ '{ a = 1; k(j); goto hit; } } gE[i].f = 19; k(i); hit: break; case 4: k(i + 4); break;',
1188
+ note: 'the search loop in cases 1 and 2 either finds a hit, which skips the rest of the case, or runs out into it',
1189
+ },
1190
+ implementedIn: l3('caseendgoto'),
1191
+ seeAlso: ['case-goto'],
1192
+ },
1193
+
993
1194
  argbase: {
994
1195
  title: 'Argument bases named before the call',
995
1196
  summary: "the fixed addresses a call's arguments read are loaded into pointer locals first",
@@ -11,6 +11,7 @@
11
11
  import { ADVANCE_HEAD_GATES, ADVANCE_MEMBER_GATES } from './l3/advance';
12
12
  import { ARGCOPY_GATES, ARGCOPY_REGION_GATES } from './l3/argcopy';
13
13
  import { BASEFOLD_GATES, LIVEBASE_BLOCK_GATES, LIVEBASE_GATES, ORDERBASE_GATES, UNFOLDED_GATES } from './l3/basecse';
14
+ import { CASE_END_GOTO_GATES } from './l3/caseendgoto';
14
15
  import { ARM_DISJOINT_GATES, COALESCE_GATES } from './l3/coalesce';
15
16
  import type { Gate } from './l3/gates';
16
17
  import { HOMESPLIT_FAN_GATES, HOMESPLIT_GATES } from './l3/homesplit';
@@ -49,6 +50,7 @@ export const VARIATION_GATE_TABLES = {
49
50
  ARM_DISJOINT_GATES,
50
51
  ARM_REREAD_GATES,
51
52
  BASEFOLD_GATES,
53
+ CASE_END_GOTO_GATES,
52
54
  COALESCE_GATES,
53
55
  COUNTDOWN_GATES,
54
56
  FRESH_MERGE_GATES,
@@ -69,6 +69,8 @@ const TOKENS = [
69
69
  { name: 'connective', variationKind: 'lift' },
70
70
  { name: 'narrow-param', variationKind: 'lift' },
71
71
  { name: 'shared-ret', variationKind: 'lift' },
72
+ { name: 'ret-copy', variationKind: 'lift' },
73
+ { name: 'ret-join', variationKind: 'lift' },
72
74
  { name: 'shared-tail', variationKind: 'lift' },
73
75
  { name: 'kept-slot', variationKind: 'lift' },
74
76
  { name: 'far-cast', variationKind: 'lift' },
@@ -93,8 +95,12 @@ const TOKENS = [
93
95
  { name: 'fresh-merge', variationKind: 'structure' },
94
96
  { name: 'copy-defpos', variationKind: 'structure' },
95
97
  { name: 'site-sense', variationKind: 'structure' },
98
+ { name: 'tail-copies', variationKind: 'structure' },
99
+ { name: 'for-continue', variationKind: 'structure' },
100
+ { name: 'case-goto', variationKind: 'structure' },
96
101
  // respell: the structured tree rewritten
97
102
  { name: 'unmerge', variationKind: 'respell' },
103
+ { name: 'case-end-goto', variationKind: 'respell' },
98
104
  { name: 'argbase', variationKind: 'respell' },
99
105
  { name: 'zerosub', variationKind: 'respell' },
100
106
  { name: 'volatile', variationKind: 'respell', subject: LOCALS },
@@ -293,6 +299,40 @@ export function splitVariations(name: string): string[] {
293
299
  return variations;
294
300
  }
295
301
 
302
+ /** What is wrong with one candidate's name, or `undefined`: no variation at all, a variation the
303
+ * registry does not hold, a first variation that is not a signedness, or a variation after one of
304
+ * a later kind. The mint sites are typed, so none of these is a `tsc` error; enumeration data is
305
+ * the only place one shows, and `tallyFanVariations` asks this of every candidate it counts. */
306
+ export function variationsDefect(variations: readonly string[]): string | undefined {
307
+ try {
308
+ return orderDefect(variations, (part) => kindIndex(parseVariation(part).name));
309
+ } catch (e) {
310
+ return `${variations.join('/')}: ${e instanceof Error ? e.message : String(e)}`;
311
+ }
312
+ }
313
+
314
+ const kindIndex = (name: string): number => VARIATION_KINDS.indexOf(variationToken(name).variationKind);
315
+
316
+ /** `variationsDefect` past the registry lookup, which `kindOf` does (and throws from) so the tally
317
+ * can serve it from its own cache. */
318
+ function orderDefect(variations: readonly string[], kindOf: (part: string) => number): string | undefined {
319
+ if (variations.length === 0) {
320
+ return 'a candidate with no variations: it applies at least its signedness';
321
+ }
322
+ let previous = -1;
323
+ for (const [i, part] of variations.entries()) {
324
+ const kind = kindOf(part);
325
+ if (i === 0 && VARIATION_KINDS[kind] !== 'signedness') {
326
+ return `${variations.join('/')}: the first variation is '${part}', not a signedness`;
327
+ }
328
+ if (kind < previous) {
329
+ return `${variations.join('/')}: '${part}' (${VARIATION_KINDS[kind]}) after a ${VARIATION_KINDS[previous]} variation`;
330
+ }
331
+ previous = kind;
332
+ }
333
+ return undefined;
334
+ }
335
+
296
336
  /** How many of one fan's candidates carry one variation. `candidates` counts the whole fan —
297
337
  * scored, dropped, withheld and never compiled alike — and `dropped`, `withheld` and
298
338
  * `notCompiled` are the parts of that count the ranking did not score, each absent when 0.
@@ -319,7 +359,15 @@ interface NamedCandidate {
319
359
  *
320
360
  * A tally, not a factorisation: enumeration gates prune the fan, so the counts do not multiply to
321
361
  * its size. Keys run in kind order, then by name, so two tallies of one fan serialize to the same
322
- * bytes whatever order the fan was listed in. Throws on a variation the registry does not hold. */
362
+ * bytes whatever order the fan was listed in.
363
+ *
364
+ * Throws on any candidate whose name `variationsDefect` refuses — an unregistered variation, a
365
+ * first variation that is not a signedness, parts out of kind order. The bench tallies every fan
366
+ * it ranks to a verdict or refuses whole, every candidate of it, on both tiers, so this is where a
367
+ * malformed mint is caught: the artifact keeps only the winner's and the refused candidates' names,
368
+ * and a test that re-enumerated the synthetic tier to read the rest cost minutes and never saw the
369
+ * real one. A rank a scorer dies in leaves a `noncompile` row with no fan, so nothing is tallied
370
+ * there. */
323
371
  export function tallyFanVariations(fan: {
324
372
  candidates: readonly NamedCandidate[];
325
373
  dropped: readonly NamedCandidate[];
@@ -335,9 +383,23 @@ export function tallyFanVariations(fan: {
335
383
  }
336
384
  return name;
337
385
  };
386
+ const kindOfName = new Map<string, number>();
387
+ const kindOf = (part: string): number => {
388
+ const name = nameOf(part);
389
+ let kind = kindOfName.get(name);
390
+ if (kind === undefined) {
391
+ kind = kindIndex(name);
392
+ kindOfName.set(name, kind);
393
+ }
394
+ return kind;
395
+ };
338
396
  const counts = new Map<string, { candidates: number; dropped: number; withheld: number; notCompiled: number }>();
339
397
  const add = (list: readonly NamedCandidate[], refusal: 'dropped' | 'withheld' | 'notCompiled' | undefined): void => {
340
398
  for (const c of list) {
399
+ const defect = orderDefect(c.variations, kindOf);
400
+ if (defect !== undefined) {
401
+ throw new Error(`a fan candidate's name is malformed — ${defect} (packages/core/src/variation-tokens.ts)`);
402
+ }
341
403
  for (const name of new Set(c.variations.map(nameOf))) {
342
404
  let n = counts.get(name);
343
405
  if (n === undefined) {
@@ -355,7 +417,6 @@ export function tallyFanVariations(fan: {
355
417
  add(fan.dropped, 'dropped');
356
418
  add(fan.withheld, 'withheld');
357
419
  add(fan.notCompiled, 'notCompiled');
358
- const kindIndex = (name: string): number => VARIATION_KINDS.indexOf(variationToken(name).variationKind);
359
420
  const names = [...counts.keys()].sort((a, b) => kindIndex(a) - kindIndex(b) || (a < b ? -1 : a > b ? 1 : 0));
360
421
  return Object.fromEntries(
361
422
  names.map((name) => {