@asmlift/core 0.8.1 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (105) hide show
  1. package/README.md +34 -30
  2. package/package.json +1 -1
  3. package/src/aggregate.ts +248 -0
  4. package/src/backend/c.ts +5 -2
  5. package/src/backend/cfamily.ts +236 -37
  6. package/src/backend/cpp.ts +114 -9
  7. package/src/backend/pascal.ts +39 -0
  8. package/src/cdecl/constant.ts +59 -0
  9. package/src/cdecl/lex.ts +209 -0
  10. package/src/cdecl/parse.ts +975 -0
  11. package/src/cdecl/spell.ts +69 -0
  12. package/src/codegen-flags.ts +38 -6
  13. package/src/compiler-diagnostics.ts +194 -0
  14. package/src/contracts.ts +615 -79
  15. package/src/declare.ts +75 -5
  16. package/src/frontend/asmdata.ts +19 -5
  17. package/src/frontend/call-plan.ts +686 -0
  18. package/src/frontend/device-pins.ts +277 -0
  19. package/src/frontend/errors.ts +5 -0
  20. package/src/frontend/flags-edge.ts +101 -0
  21. package/src/frontend/fpu.ts +167 -0
  22. package/src/frontend/frame-objects.ts +2131 -0
  23. package/src/frontend/high-half.ts +2 -1
  24. package/src/frontend/local-object.ts +490 -0
  25. package/src/frontend/mips.ts +363 -40
  26. package/src/frontend/opaque.ts +97 -18
  27. package/src/frontend/ppc.ts +520 -129
  28. package/src/frontend/reloc-symbol.ts +94 -13
  29. package/src/frontend/splat.ts +108 -5
  30. package/src/frontend/ssa.ts +669 -76
  31. package/src/frontend/stackargs.ts +120 -39
  32. package/src/frontend/thumb.ts +2764 -2018
  33. package/src/ir/alias.ts +29 -0
  34. package/src/ir/core.ts +101 -3
  35. package/src/ir/discipline.ts +248 -0
  36. package/src/ir/float-bits.ts +47 -0
  37. package/src/ir/opcodes.ts +84 -112
  38. package/src/ir/parse.ts +1 -0
  39. package/src/ir/types.ts +118 -2
  40. package/src/ir/verify.ts +150 -3
  41. package/src/l3/address.ts +5 -0
  42. package/src/l3/argcopy.ts +38 -6
  43. package/src/l3/ast.ts +186 -14
  44. package/src/l3/coalesce.ts +21 -13
  45. package/src/l3/dce.ts +32 -10
  46. package/src/l3/gates.ts +9 -3
  47. package/src/l3/hoist.ts +8 -7
  48. package/src/l3/initfirst.ts +27 -9
  49. package/src/l3/intcell.ts +97 -0
  50. package/src/l3/mentions.ts +10 -4
  51. package/src/l3/narrowdecl.ts +218 -0
  52. package/src/l3/nearbase.ts +10 -5
  53. package/src/l3/parkfirst.ts +8 -5
  54. package/src/l3/pollguard.ts +3 -2
  55. package/src/l3/ptrcell.ts +357 -0
  56. package/src/l3/regspell.ts +17 -3
  57. package/src/l3/reindex.ts +10 -7
  58. package/src/l3/symbol-refs.ts +104 -23
  59. package/src/l3/tailmerge.ts +6 -19
  60. package/src/l3/typing.ts +137 -25
  61. package/src/l3/unreduce.ts +4 -4
  62. package/src/l3/volstore.ts +96 -7
  63. package/src/mangle.ts +11 -1
  64. package/src/pattern/engine.ts +19 -16
  65. package/src/pipeline.ts +57 -12
  66. package/src/proto-context.ts +788 -0
  67. package/src/proto.ts +689 -50
  68. package/src/raise/arrays.ts +4 -3
  69. package/src/raise/declared-volatile.ts +131 -0
  70. package/src/raise/divpow2.ts +2 -2
  71. package/src/raise/errors.ts +17 -0
  72. package/src/raise/floathelpers.ts +243 -0
  73. package/src/raise/globalshape.ts +11 -5
  74. package/src/raise/gvn.ts +26 -5
  75. package/src/raise/narrow.ts +29 -2
  76. package/src/raise/narrowlocal.ts +11 -6
  77. package/src/raise/offsetnames.ts +510 -0
  78. package/src/raise/pairparams.ts +81 -0
  79. package/src/raise/paramwidth.ts +7 -0
  80. package/src/raise/pre-recovery.ts +78 -4
  81. package/src/raise/recover.ts +14 -10
  82. package/src/raise/retsink.ts +1 -1
  83. package/src/raise/shortcircuit.ts +41 -19
  84. package/src/raise/softdiv.ts +21 -23
  85. package/src/raise/struct-arrays.ts +5 -4
  86. package/src/raise/structs.ts +311 -98
  87. package/src/raise/truncload.ts +366 -0
  88. package/src/raise/widehelpers.ts +106 -0
  89. package/src/rank-declare.ts +30 -5
  90. package/src/rank-variations.ts +36 -8
  91. package/src/rank.ts +267 -79
  92. package/src/runtime-helpers.ts +287 -0
  93. package/src/stillborn.ts +289 -0
  94. package/src/structure/analysis.ts +1285 -448
  95. package/src/structure/hazards.ts +633 -58
  96. package/src/structure/local-statics.ts +381 -0
  97. package/src/structure/namecoalesce.ts +1 -1
  98. package/src/structure/redundant-test.ts +99 -0
  99. package/src/structure/structure.ts +1772 -395
  100. package/src/structure/switch-recover.ts +159 -78
  101. package/src/symbols.ts +38 -3
  102. package/src/target.ts +662 -59
  103. package/src/trace.ts +44 -24
  104. package/src/variation-definitions.ts +119 -1
  105. package/src/variation-tokens.ts +20 -6
package/src/rank.ts CHANGED
@@ -24,12 +24,12 @@ import {
24
24
  assertLocalsWritten,
25
25
  assertNoOrphanedLocals,
26
26
  assertPlacementSurvives,
27
+ assertPostIncrUnshared,
27
28
  assertResolved,
28
29
  } from './contracts';
29
30
  import type { AsmData } from './frontend/asmdata';
30
- import { frontendFor } from './frontend/registry';
31
- import { hasSetupArgsNarrowing, narrowToSetupArgs } from './frontend/ssa';
32
- import { Fn, defOpMap } from './ir/core';
31
+ import { hasSetupArgsNarrowing, narrowToSetupArgs, setupArgsDiscardsPassedResult } from './frontend/ssa';
32
+ import { Fn, Value, defOpMap } from './ir/core';
33
33
  import { T } from './ir/types';
34
34
  import { verify } from './ir/verify';
35
35
  import { advancedBases } from './l3/advance';
@@ -42,6 +42,7 @@ import type { Gate } from './l3/gates';
42
42
  import type { HoistPlacement } from './l3/hoist';
43
43
  import { homeSplitTag, homeSplitWithholds, splitHomeBases } from './l3/homesplit';
44
44
  import { inlinableConstBases, inlineConstBases } from './l3/inlinebase';
45
+ import { integerCells } from './l3/intcell';
45
46
  import { mulFirstSums } from './l3/mulfirst';
46
47
  import { nearBaseClusters } from './l3/nearbase';
47
48
  import { spellOperandMembers } from './l3/offmember';
@@ -58,9 +59,10 @@ import { volatileValueLocals } from './l3/volatileval';
58
59
  import { volatileDeviceStores } from './l3/volstore';
59
60
  import { zeroSubNegates } from './l3/zerosub';
60
61
  import { RewritePattern } from './pattern/engine';
61
- import { applyIdiomPatterns, raiseRecovered, structureChecked } from './pipeline';
62
- import { type Prototypes, prototypesFromSymbols } from './proto';
62
+ import { applyIdiomPatterns, liftStamped, raiseRecovered, structureChecked } from './pipeline';
63
+ import { type Prototypes, declaresVoidReturn, prototypesFromSymbols } from './proto';
63
64
  import { inferGlobalArrays, orderLicensedGlobals, sameDerivedShape } from './raise/globalshape';
65
+ import { OFFSET_NAME_PASS } from './raise/offsetnames';
64
66
  import { runPreRecovery } from './raise/pre-recovery';
65
67
  import { recoverTypes } from './raise/recover';
66
68
  import { sinkStoreTails } from './raise/tailsink';
@@ -86,6 +88,7 @@ import {
86
88
  createdLocals,
87
89
  sameBases,
88
90
  } from './rank-variations';
91
+ import { type Stillborn, stillbornVerdict } from './stillborn';
89
92
  import { hasDivergentSharedRet } from './structure/structure';
90
93
  import {
91
94
  type SymbolInfo,
@@ -96,7 +99,13 @@ import {
96
99
  symbolsByName,
97
100
  } from './symbols';
98
101
  import { type TargetDescription, structureOptionsFor } from './target';
99
- import { type SubjectVariationName, type Variation, offeredOn, withSubject } from './variation-tokens';
102
+ import {
103
+ type SubjectVariationName,
104
+ type Variation,
105
+ type VariationName,
106
+ offeredOn,
107
+ withSubject,
108
+ } from './variation-tokens';
100
109
 
101
110
  /** Pin every SCALAR entry param (index not in `ptrIdx`) to the candidate signedness, before
102
111
  * recovery. Answers whether any param was PINNABLE — not whether its type moved: which of the
@@ -106,16 +115,33 @@ import { type SubjectVariationName, type Variation, offeredOn, withSubject } fro
106
115
  * A param NARROWED by raise/paramwidth.ts is not pinnable: the extension it was narrowed at states
107
116
  * the signedness as well as the width — agbcc's shift pair by its `asr`/`lsr`, PPC's `extsb`/`extsh`
108
117
  * by the opcode — so there is no question for the signedness variation to put to the differ, and pinning would
109
- * widen it back to 32 bits. */
118
+ * widen it back to 32 bits.
119
+ *
120
+ * Nor is a param NOTHING READS. Its declared signedness reaches no instruction, so the two
121
+ * candidates would differ only in the signature's text and compile to the same object. The frontend
122
+ * mints such params on purpose: an argument register below the highest one read keeps its slot
123
+ * (`frontend/ssa.ts` mintArgSlotHoles). Counting one as pinnable would double the fan of every
124
+ * function whose only scalar params are unread. */
110
125
  function pinScalarParams(fn: Fn, signed: boolean, ptrIdx: Set<number>): boolean {
126
+ const read = new Set<Value>();
127
+ for (const b of fn.blocks) {
128
+ for (const op of b.ops) {
129
+ op.operands.forEach((v) => read.add(v));
130
+ op.successors.forEach((s) => s.args.forEach((v) => read.add(v)));
131
+ }
132
+ }
111
133
  let pinnable = false;
112
134
  fn.blocks[0].params.forEach((p, i) => {
113
- if (ptrIdx.has(i)) {
135
+ if (ptrIdx.has(i) || !read.has(p)) {
114
136
  return;
115
137
  }
116
- if (p.type.kind === 'unknown' || (p.type.kind === 'int' && p.type.width === 32)) {
138
+ // AT THE PARAMETER'S OWN WIDTH. The pin answers a question the asm does not — whether the
139
+ // source declared this parameter signed — and a 64-bit parameter has that question too; what
140
+ // it does not have is a spare 32 bits to lose to a constant here. A width the machine already
141
+ // narrowed (a declared `u8`) is not a scalar this variation re-decides, so it is left alone.
142
+ if (p.type.kind === 'unknown' || (p.type.kind === 'int' && p.type.width >= 32)) {
117
143
  pinnable = true;
118
- p.type = signed ? T.s(32) : T.u(32);
144
+ p.type = T.int(p.type.width, signed);
119
145
  }
120
146
  });
121
147
  return pinnable;
@@ -220,15 +246,25 @@ export interface Candidate {
220
246
  *
221
247
  * The third admission ground, and the narrowest. A respell variation must preserve semantics by
222
248
  * construction (the POLICY note at the respell site), because on a nonmatch row the best
223
- * spelling is what the user is shown. One spelling cannot meet that bar from inside the pass:
224
- * `l3/unreduce.ts` moves a memory read into a loop whose stores are all device registers, and
225
- * on this board a device store can make the DEVICE write ordinary memory (a DMA trigger), which
226
- * no gate over the C can rule out. What settles it instead is the object: a candidate that
227
- * assembles to the target's own bytes IS the program, whatever a gate could have proved. So the
228
- * spelling is offered, scored, and then either wins on proof or is WITHHELD — never shown as a
229
- * best-effort answer. Both ranking drivers ask `withheldReason`, so neither can publish what the
230
- * other would not. */
249
+ * spelling is what the user is shown. Two spellings cannot meet that bar from inside the pass,
250
+ * each for its own fact (`PROOF_OBLIGATIONS` states them):
251
+ *
252
+ * • `l3/unreduce.ts` moves a memory read into a loop whose stores are all device registers, and
253
+ * on this board a device store can make the DEVICE write ordinary memory (a DMA trigger),
254
+ * which no gate over the C can rule out;
255
+ * • `l3/intcell.ts` spells a global as the integer a project may have declared it, where a
256
+ * pointer declaration scales the offset it adds, and no declaration is in the asm.
257
+ *
258
+ * What settles both instead is the object: a candidate that assembles to the target's own bytes
259
+ * IS the program, whatever a gate could have proved. So the spelling is offered, scored, and then
260
+ * either wins on proof or is WITHHELD — never shown as a best-effort answer. Both ranking drivers
261
+ * ask `withheldReason`, so neither can publish what the other would not. */
231
262
  matchOnly?: true;
263
+ /** This candidate's lift is `/setup-args` and drops an earlier callee's result that a call through
264
+ * a register is passed (frontend/ssa.ts `setupArgsDiscardsPassedResult`). It loses every score
265
+ * tie (compareScored): a tie refutes neither reading, and a shorter spelling must not win by
266
+ * dropping a value the callee may read. */
267
+ discardsPassedResult?: true;
232
268
  }
233
269
  /** A candidate paired with its score `S` (the injected scorer's result shape — must carry `.score`). */
234
270
  export interface Scored<S> extends Candidate {
@@ -261,10 +297,20 @@ export interface WithheldCandidate {
261
297
  why: string;
262
298
  }
263
299
 
300
+ /** A candidate that was NEVER COMPILED: the fan was declared stillborn (stillborn.ts) before its
301
+ * turn came. Kept apart from `dropped`, which means "the scorer refused it" — nothing refused
302
+ * this one, and a count that folded the two would report compiles that never ran. */
303
+ export interface NotCompiledCandidate {
304
+ variations: readonly string[];
305
+ /** the text the stop bet the compiler would refuse: a stop is re-placed whenever it changes, and
306
+ * the benchmark keys its STOP report on it (`fanNotCompiledDigest`) */
307
+ source: string;
308
+ }
309
+
264
310
  /** EVERY candidate refused — `rankBy` has no ranked result to return, so it throws this.
265
311
  *
266
- * The two lists ride on the error, and that is the point of having a class at all: on a row where
267
- * nothing scored, `dropped` IS the whole fan, and a bare `Error` discards it. A caller that
312
+ * The lists ride on the error, and that is the point of having a class at all: on a row where
313
+ * nothing scored, the three of them ARE the whole fan, and a bare `Error` discards it. A caller that
268
314
  * prints one candidate's failure (the `cause`) is showing the LAST spelling the scorer refused,
269
315
  * which is neither the first nor a representative one — the benchmark's own noncompile rows have
270
316
  * up to a thousand siblings behind that single line. The MESSAGE is load-bearing and must stay
@@ -273,11 +319,20 @@ export interface WithheldCandidate {
273
319
  export class NoScorableCandidateError extends Error {
274
320
  readonly dropped: DroppedCandidate[];
275
321
  readonly withheld: WithheldCandidate[];
276
- constructor(message: string, dropped: DroppedCandidate[], withheld: WithheldCandidate[], options?: ErrorOptions) {
322
+ /** empty unless the fan was declared stillborn; `dropped` is then only what WAS compiled */
323
+ readonly notCompiled: NotCompiledCandidate[];
324
+ constructor(
325
+ message: string,
326
+ dropped: DroppedCandidate[],
327
+ withheld: WithheldCandidate[],
328
+ notCompiled: NotCompiledCandidate[],
329
+ options?: ErrorOptions,
330
+ ) {
277
331
  super(message, options);
278
332
  this.name = 'NoScorableCandidateError';
279
333
  this.dropped = dropped;
280
334
  this.withheld = withheld;
335
+ this.notCompiled = notCompiled;
281
336
  }
282
337
  }
283
338
 
@@ -309,14 +364,23 @@ export interface RankedResult<S> {
309
364
  * drivers over one enumeration (this module's sync `rankBy` and the webapp's async await-loop),
310
365
  * and a filter written twice is how they come to publish different answers. Null ⇒ publish.
311
366
  *
312
- * `score === 0` is objdiff's byte-exact match (cli objdiff.ts states the equivalence), which is
367
+ * `score === 0` is objdiff's byte-exact match (@match-kit/scoring states the equivalence), which is
313
368
  * why a bare `.score` suffices and the generic needs no `match` field. */
314
369
  export function withheldReason<S extends { score: number }>(c: Candidate, score: S): string | null {
315
- return c.matchOnly === true && score.score !== 0
316
- ? 'this spelling rests on a device-behaviour fact no gate over the C can settle; only a byte-exact score proves it'
317
- : null;
370
+ if (c.matchOnly !== true || score.score === 0) {
371
+ return null;
372
+ }
373
+ const facts = c.variations.flatMap((v) => PROOF_OBLIGATIONS.get(v) ?? []);
374
+ const fact = facts.length > 0 ? facts.join(' and on ') : 'a fact no gate over the C can settle';
375
+ return `this spelling rests on ${fact}; only a byte-exact score proves it`;
318
376
  }
319
377
 
378
+ /** The fact each proof-carrying variation (`needsProof`) rests on, as `withheldReason` publishes it. */
379
+ const PROOF_OBLIGATIONS: ReadonlyMap<string, string> = new Map<VariationName, string>([
380
+ ['unreduce', 'a device-behaviour fact no gate over the C can settle'],
381
+ ['int-cell', 'the project declaring the global an integer, which the assembly does not show'],
382
+ ]);
383
+
320
384
  /** What a respell variation hands `respell`: its tree, or — when the variation cannot establish the
321
385
  * candidate's semantics from inside the pass — the tree paired with that fact. `undefined`/`null`
322
386
  * is a decline. */
@@ -408,9 +472,8 @@ export function enumerateCandidates(
408
472
  // Same merge as `decompile`: the project's DWARF signatures fill in what the caller did not
409
473
  // state, so both the annotate pass and the ranked candidates reason about one prototype table.
410
474
  const prototypes = prototypesFromSymbols(opts.symbols, opts.prototypes ?? {});
411
- const frontend = frontendFor(target);
412
475
  const baseOpts = {
413
- ...structureOptionsFor(target, prototypes[name]?.returnsVoid ?? false),
476
+ ...structureOptionsFor(target, declaresVoidReturn(prototypes[name]), prototypes),
414
477
  // See the same line in pipeline.ts: a backend that cannot print switch fall-through must not
415
478
  // be handed a tree carrying one, because its refusal costs the whole candidate (and, when
416
479
  // every candidate carries it, the whole row).
@@ -541,8 +604,7 @@ export function enumerateCandidates(
541
604
  // pointers/aggregates so they are excluded from the signedness variation (see NO_PIN_KINDS). One
542
605
  // extra lift+recover, no compile. (The shared lift deliberately stops after recoverTypes — it only reads the param KINDS, so
543
606
  // the totality contract / return-sinking of the full spine are not run on it.)
544
- const sharedLift = frontend.lift(name, asm, target, prototypes, opts.asmData, opts.symbols);
545
- verify(sharedLift);
607
+ const sharedLift = liftStamped(name, asm, target, prototypes, opts.asmData, opts.symbols);
546
608
  // The ARRAY SHAPES the input assembly evidences (raise/globalshape.ts), for the DECLARATION
547
609
  // half. Read off the shared lift's LIFTED form — before the fold below and the tower rewrite it —
548
610
  // because the base-materialization order the derivation's licence reads does not survive them.
@@ -585,8 +647,7 @@ export function enumerateCandidates(
585
647
  if (symbols === opts.symbols) {
586
648
  return sharedLiftTreeOwned;
587
649
  }
588
- const p = frontend.lift(name, asm, target, prototypes, opts.asmData, symbols);
589
- verify(p);
650
+ const p = liftStamped(name, asm, target, prototypes, opts.asmData, symbols);
590
651
  applyIdiomPatterns(p, target, opts.patterns);
591
652
  let seen = false;
592
653
  runPreRecovery(p, target, () => verify(p), prototypes[name], {
@@ -800,6 +861,14 @@ export function enumerateCandidates(
800
861
  s.ptrElems &&
801
862
  s.declRank &&
802
863
  STRUCTURE_VARIATIONS.every((variation) => !s[variation.flag]);
864
+ /** Does a failure ABORT THE ROW, rather than drop the setting that threw? Only on the default
865
+ * candidate's own path: no variation named by the symbol-map setting or the lift setting —
866
+ * `settingVariations` is both, as the candidate spells them — and, where a structure setting is
867
+ * named, the default one. Anywhere else the failure is a dropped variation, reported through
868
+ * `onEnumerationError` under the same names. The lift, the raise and the structuring all ask
869
+ * this one predicate, so a setting that names a new variation is seen by all three. */
870
+ const abortsRow = (settingVariations: readonly Variation[], s?: StructureSetting): boolean =>
871
+ settingVariations.length === 0 && (s === undefined || isDefaultSetting(s));
803
872
 
804
873
  const seen = new Map<string, Candidate>();
805
874
  const seenTrees = new Set<string>();
@@ -840,7 +909,7 @@ export function enumerateCandidates(
840
909
  // every OTHER name in the function, so supplying more information made the tool strictly
841
910
  // worse. A union cannot — each name is declared by whichever half knows more about it.
842
911
  // SCOPE: `declSymbols` is used ONLY here. It must never reach `opts.symbols`/`baseOpts.symbols`
843
- // or `frontend.lift` — feeding it to the lift would turn on pool promotion, interior
912
+ // or `liftStamped` — feeding it to the lift would turn on pool promotion, interior
844
913
  // attribution and the `/raw-globals` variation, which is a different (and source-moving) change.
845
914
  // THREE halves now, in increasing authority: the name-only pool/reloc symbols, the array shapes
846
915
  // the asm evidences for them (raise/globalshape.ts — an `extern u16 gTbl[];` where the bare
@@ -856,6 +925,10 @@ export function enumerateCandidates(
856
925
  accessFacts,
857
926
  mapSymbols,
858
927
  targetNames: new Set(targetSymbols.keys()),
928
+ // The MERGED table, the same one the frontend was handed above, so the declaration a candidate
929
+ // carries and the lift that produced it cannot state different things about a callee.
930
+ prototypes,
931
+ target,
859
932
  refuse,
860
933
  });
861
934
  // THE RESPELL SET, as a function whose PARAMETER LIST is the invariant the tree skip below
@@ -923,9 +996,11 @@ export function enumerateCandidates(
923
996
  // numeric-address pointer local — the joint spelling is reachable from neither variation
924
997
  // alone, each composition narrows /volatile to that variation's own locals (volatilePtrLocals'
925
998
  // `only`), and each needed a row to demand it. The STACKED variations (STACKED_VARIATIONS) are
926
- // derived onto EVERY source: statement order/shape is orthogonal to what any other respell
927
- // variation changes — the same kind of independent dimension as signedness —
928
- // so they are crossed with every source rather than paired; a third blanket composition needs
999
+ // derived onto EVERY source: statement order/shape, and the width a narrow local is declared
1000
+ // at, are each orthogonal to what any respell variation outside that table changes — the same
1001
+ // kind of independent dimension as signedness —
1002
+ // so they are crossed with every source rather than paired (two members of the table may
1003
+ // compete for one node, and its order decides; rank-variations.ts); a third blanket composition needs
929
1004
  // the same argument, not just a row. And a specific PAIRING is admitted on one of two
930
1005
  // grounds, never on "it might help". FIRST, a row demands the joint spelling AND that
931
1006
  // spelling is reachable from neither variation alone: /livebase × /indexed, × /sinkinit,
@@ -991,14 +1066,16 @@ export function enumerateCandidates(
991
1066
  assertResolved(alt);
992
1067
  assertDerefsTyped(alt);
993
1068
  assertLocalsWritten(alt);
1069
+ assertPostIncrUnshared(alt);
994
1070
  assertNoOrphanedLocals(sfn, alt);
995
1071
  sources.push({ variations, source: backend.emit(alt), ...refsOf(alt), ...volOf(alt), ...proof });
996
1072
  // STACKED variations, derived onto EVERY source (the POLICY note above carries the
997
- // admission argument). Each is a statement-order/shape fact orthogonal to
998
- // representation; subsets compose in the fixed order below. A stacked variation that
1073
+ // admission argument). Each is a statement-order/shape or declaration-width fact
1074
+ // orthogonal to the respell variations outside the table; subsets compose in the fixed
1075
+ // table order, which decides between two members that compete for one node. A stacked variation that
999
1076
  // never fires declines and costs nothing.
1000
1077
  if (!alreadyShaped) {
1001
- // A shape REORDERS statements, and it is derived after a variation has placed its defs —
1078
+ // A shape may REORDER statements, and it is derived after a variation has placed its defs —
1002
1079
  // so the placement is re-checked on the shaped tree (contracts.ts). Differential: judged
1003
1080
  // only where the unshaped tree already satisfied the walk, so a variation whose placement
1004
1081
  // it never described is not dropped on the strength of a model that does not apply.
@@ -1029,6 +1106,7 @@ export function enumerateCandidates(
1029
1106
  assertResolved(shaped.out);
1030
1107
  assertDerefsTyped(shaped.out);
1031
1108
  assertLocalsWritten(shaped.out);
1109
+ assertPostIncrUnshared(shaped.out);
1032
1110
  assertNoOrphanedLocals(alt, shaped.out);
1033
1111
  assertPlacementSurvives(alt, shaped.out, minted);
1034
1112
  sources.push({
@@ -1143,9 +1221,9 @@ export function enumerateCandidates(
1143
1221
  // only ties `/vol-store`'s 27, and RT's 32 beats the admitted standalone `/unreduce`'s 35, so
1144
1222
  // "worse than an admitted variation" would exclude neither.
1145
1223
  //
1146
- // WHAT THE STANDALONE LINES COST, since neither of the two variations ever wins an artifact row
1147
- // ALONE — every `/unreduce` and `/ptr-field` winner rides inside a `/vol-store` pairing, which
1148
- // is the property `apps/benchmark/test/census.test.ts` asserts. They are kept because a
1224
+ // WHAT THE STANDALONE LINES COST, since the two variations rarely win an artifact row ALONE —
1225
+ // `apps/benchmark/test/census.test.ts` names every `/unreduce` and `/ptr-field` winner outside
1226
+ // a `/vol-store` pairing, and fails on one it does not name. They are kept because a
1149
1227
  // variation has to be able to LOSE on its own terms: the admission posture (compareScored orders
1150
1228
  // by score) is what makes a wrong respelled source harmless, and it is only observable when the
1151
1229
  // single-variation candidate is in the fan —
@@ -1666,6 +1744,17 @@ export function enumerateCandidates(
1666
1744
  // Which placement the source used is not derivable from the asm, so both are emitted and the
1667
1745
  // differ referees.
1668
1746
  respell(['sinkinit'], () => sinkInitsToFirstUse(sfn));
1747
+ // `/int-cell` — every pointer cell spelled as the integer a project may have declared it,
1748
+ // `g = g + K` and no conversions (l3/intcell.ts). The default's `g = (void *)((u8 *)g + K)` is
1749
+ // right under a pointer declaration of g and rejected by CodeWarrior under an integer one, where
1750
+ // this is the source; which the project declared is not in the asm, so it is published only at
1751
+ // a byte-exact score.
1752
+ respell(['int-cell'], () =>
1753
+ integerCells(sfn, (n) => {
1754
+ const shape = mapSymbols?.get(n)?.shape;
1755
+ return shape !== undefined && shape !== 'scalar';
1756
+ }),
1757
+ );
1669
1758
  // the register-copy variation (l3/regspell.ts): 0–3 results (base; tail assign-back reusing
1670
1759
  // the dead value var; tail assign-back into a fresh var — the tail decision is allocator-
1671
1760
  // ambiguous, so both are ranked).
@@ -1707,8 +1796,13 @@ export function enumerateCandidates(
1707
1796
  { variations: ['raw-globals'], symbols: undefined },
1708
1797
  ]
1709
1798
  : [{ variations: [] }];
1799
+ // The map's POINTER declarations go to the raw setting all the same: a raw name is still a
1800
+ // pointer in the project's file, so arithmetic on its value scales unless spelled in bytes.
1801
+ const pointerGlobals = new Set(
1802
+ [...(baseOpts.symbols?.entries() ?? [])].filter(([, info]) => info.shape === 'pointer').map(([n]) => n),
1803
+ );
1710
1804
  for (const [symbolIndex, symbolSetting] of symbolSettings.entries()) {
1711
- const symbolSettingOpts = symbolSetting.symbols ? baseOpts : { ...baseOpts, symbols: undefined };
1805
+ const symbolSettingOpts = symbolSetting.symbols ? baseOpts : { ...baseOpts, symbols: undefined, pointerGlobals };
1712
1806
  // `/no-bitfield` names a spelling the MAP makes available, so it has no inhabitant on the
1713
1807
  // symbol-map setting that structures without one: structure() normalizes `spellBitfieldMembers`
1714
1808
  // to false when `symbols` is absent, so both settings structure the identical tree whatever
@@ -1728,7 +1822,21 @@ export function enumerateCandidates(
1728
1822
  const usableStructureSettings = symbolSetting.symbols
1729
1823
  ? structureSettings
1730
1824
  : structureSettings.filter((s) => s.bitfields && s.ptrElems && (s.declRank || rawDerivesRank));
1731
- const treeOwnedFold = treeOwnedIn(symbolSetting.symbols);
1825
+ // THE `/raw-globals` SETTING IS A VARIATION, so a lift that declines without the map drops it
1826
+ // and keeps the map's candidates (`abortsRow`). The two lifts can disagree on whether a
1827
+ // function lifts at all: a name only the map places is, without it, a pointer the frame-object
1828
+ // audit cannot bound. This is the setting's first lift, and every later one is the same lift
1829
+ // again.
1830
+ let treeOwnedFold: boolean;
1831
+ try {
1832
+ treeOwnedFold = treeOwnedIn(symbolSetting.symbols);
1833
+ } catch (e) {
1834
+ if (abortsRow(symbolSetting.variations)) {
1835
+ throw e;
1836
+ }
1837
+ reportThrow(symbolSetting.variations, e);
1838
+ continue;
1839
+ }
1732
1840
  // The signedness variation DECLINES where the pin has nothing to pin. `pinScalarParams` writes only
1733
1841
  // over an entry param still `unknown`/`int` that is not one of the recovered pointers/
1734
1842
  // aggregates `ptrIdx` excludes; where no param is left, the second pass re-lifts, re-raises and
@@ -1744,7 +1852,7 @@ export function enumerateCandidates(
1744
1852
  if (cand.signed && !pinnable) {
1745
1853
  break;
1746
1854
  }
1747
- const base = frontend.lift(name, asm, target, prototypes, opts.asmData, symbolSetting.symbols);
1855
+ const base = liftStamped(name, asm, target, prototypes, opts.asmData, symbolSetting.symbols);
1748
1856
  // `/setup-args` — pass a prototype-less callee only what the CALLING BLOCK set up; which of
1749
1857
  // the two readings the source spelled is genuinely ambiguous, and frontend/ssa.ts
1750
1858
  // narrowToSetupArgs carries the argument for why the differ is what settles it.
@@ -1815,6 +1923,7 @@ export function enumerateCandidates(
1815
1923
  );
1816
1924
  for (const liftSetting of liftSettings) {
1817
1925
  let fn: Fn;
1926
+ let discardsPassedResult = false;
1818
1927
  let inferredSymbols = new Map<string, SymbolInfo>();
1819
1928
  let orderLicensed: ReadonlySet<string> = new Set<string>();
1820
1929
  try {
@@ -1823,10 +1932,11 @@ export function enumerateCandidates(
1823
1932
  fn =
1824
1933
  liftSetting.variations.length === 0
1825
1934
  ? base
1826
- : frontend.lift(name, asm, target, prototypes, opts.asmData, symbolSetting.symbols);
1935
+ : liftStamped(name, asm, target, prototypes, opts.asmData, symbolSetting.symbols);
1827
1936
  if (liftSetting.narrow && !narrowToSetupArgs(fn)) {
1828
1937
  continue; // nothing to cut after all — the default lift's own candidates already cover it
1829
1938
  }
1939
+ discardsPassedResult = liftSetting.narrow && setupArgsDiscardsPassedResult(fn);
1830
1940
  verify(fn);
1831
1941
  // This lift's OWN array-shape evidence, off its own lifted fn (a symbol map promotes
1832
1942
  // numeric pool words to `gaddr`, so the `/raw-globals` setting genuinely answers differently).
@@ -1855,6 +1965,13 @@ export function enumerateCandidates(
1855
1965
  // declaration. A map that names the symbol an array takes the access to a bare `var`
1856
1966
  // base anyway, which carries no licence: the two never meet.
1857
1967
  orderLicensed = orderLicensedGlobals(fn, target);
1968
+ // …and the NAMES the map holds for the addresses this lift built by arithmetic off a
1969
+ // named one (raise/offsetnames.ts). Per lift setting, off that setting's own map: the
1970
+ // `/raw-globals` sibling has no map and so no name to reach. AFTER the two readings, and
1971
+ // the staleness that leaves them is priced in pipeline.ts's copy of this note.
1972
+ if (symbolSetting.symbols) {
1973
+ OFFSET_NAME_PASS.run(fn, symbolSetting.symbols, target.compilerBehaviors);
1974
+ }
1858
1975
  for (const [n, si] of [...inferredSymbols]) {
1859
1976
  if (baseOpts.symbols?.has(n) === true || !sameDerivedShape(declSymbols.get(n), si)) {
1860
1977
  inferredSymbols.delete(n);
@@ -1879,14 +1996,15 @@ export function enumerateCandidates(
1879
1996
  );
1880
1997
  } catch (e) {
1881
1998
  // THE DEFAULT CARRIES NO VARIATION, by construction: every lift variation appends one, so
1882
- // `variations.length === 0` is the only spelling of "no lift variation is on" that stays correct
1999
+ // an empty list is the only spelling of "no lift variation is on" that stays correct
1883
2000
  // when a fourth is added — the same reason the structure half below reads its table
1884
2001
  // instead of naming its flags.
1885
- if (liftSetting.variations.length === 0) {
2002
+ const settingVariations = [...liftSetting.variations, ...symbolSetting.variations];
2003
+ if (abortsRow(settingVariations)) {
1886
2004
  throw e; // the default lift keeps its behavior: a raising failure aborts the row
1887
2005
  }
1888
2006
  // A dropped variation, never an aborted enumeration — the same posture as `respell`.
1889
- reportThrow(liftSetting.variations, e);
2007
+ reportThrow(settingVariations, e);
1890
2008
  continue;
1891
2009
  }
1892
2010
  // THE SHARED-TAIL VARIATIONS: the same raised fn, structured again with `followEarlyReturns`,
@@ -1894,7 +2012,8 @@ export function enumerateCandidates(
1894
2012
  // - `/shared-ret` is the follow ALONE, on the fn as raised. Some divergent `if` shares a
1895
2013
  // `ret` the compiler left in place (`synthetic:gcseinner`).
1896
2014
  // - `/shared-tail` is the follow after `sinkStoreTails` has rewritten the fn in place, which
1897
- // is safe because `structure()` never mutates `fn`, so the earlier passes are done with it.
2015
+ // is safe because `structure()` writes nothing on `fn` but the `declared` stamps, which it
2016
+ // re-derives from its map on every call, so the earlier passes are done with it.
1898
2017
  // It is enumerated only where the sink changed something AND some divergent `if` of the
1899
2018
  // SUNK fn shares a `ret`. That is the sink's price gate: without it, a tail copied into
1900
2019
  // arms no `if` shares adds a candidate spelling it in each — 204 synthetic candidates on 8
@@ -1942,7 +2061,7 @@ export function enumerateCandidates(
1942
2061
  verify(fn);
1943
2062
  }
1944
2063
  } catch (e) {
1945
- reportThrow([...liftSetting.variations, 'shared-tail'], e);
2064
+ reportThrow([...liftSetting.variations, 'shared-tail', ...symbolSetting.variations], e);
1946
2065
  break;
1947
2066
  }
1948
2067
  // Unsunk, this fn is the `/shared-ret` pass's again.
@@ -1996,8 +2115,9 @@ export function enumerateCandidates(
1996
2115
  dropped.add(key);
1997
2116
  continue;
1998
2117
  }
1999
- // structure() reads `fn` and produces a fresh SFn (it does not mutate `fn`), so both branch
2000
- // senses structure the same recovered function without re-lifting.
2118
+ // structure() produces a fresh SFn and writes nothing on `fn` but the `declared` stamps,
2119
+ // which it re-derives from its map on every call, so both branch senses structure the same
2120
+ // recovered function without re-lifting.
2001
2121
  let sfn: SFn;
2002
2122
  try {
2003
2123
  sfn = structureChecked(fn, {
@@ -2019,7 +2139,7 @@ export function enumerateCandidates(
2019
2139
  ...(alternative ? { followEarlyReturns: true } : {}),
2020
2140
  });
2021
2141
  } catch (e) {
2022
- if (liftVariations.length === 0 && isDefaultSetting(s)) {
2142
+ if (abortsRow([...liftVariations, ...symbolSetting.variations], s)) {
2023
2143
  throw e; // the default lift's default setting keeps its behavior: a failure aborts the row
2024
2144
  }
2025
2145
  // Recorded for EVERY dropped setting: a candidate with more variations on looks its siblings
@@ -2027,7 +2147,7 @@ export function enumerateCandidates(
2027
2147
  dropped.add(key);
2028
2148
  // an anchored setting that fails structuring or its contracts is a dropped variation, never
2029
2149
  // an aborted enumeration — same rule as respell below
2030
- reportThrow([...liftVariations, ...s.variations], e);
2150
+ reportThrow([...liftVariations, ...s.variations, ...symbolSetting.variations], e);
2031
2151
  continue;
2032
2152
  }
2033
2153
  // A TREE another structure setting already produced. `respellTree` reads the tree and this
@@ -2092,6 +2212,7 @@ export function enumerateCandidates(
2092
2212
  assertResolved(made);
2093
2213
  assertDerefsTyped(made);
2094
2214
  assertLocalsWritten(made);
2215
+ assertPostIncrUnshared(made);
2095
2216
  assertNoOrphanedLocals(sfn, made);
2096
2217
  // A backend refusal on this REWRITTEN tree is not a refusal of the row's own
2097
2218
  // tree, so it never becomes the row's stated cause: `TreeSources.emit` is dropped
@@ -2154,6 +2275,9 @@ export function enumerateCandidates(
2154
2275
  if (sp.matchOnly === undefined) {
2155
2276
  delete dup.matchOnly;
2156
2277
  }
2278
+ if (!discardsPassedResult) {
2279
+ delete dup.discardsPassedResult;
2280
+ }
2157
2281
  continue;
2158
2282
  }
2159
2283
  const variations: readonly Variation[] = [
@@ -2176,6 +2300,7 @@ export function enumerateCandidates(
2176
2300
  ...(sp.symbolRefs ? { symbolRefs: sp.symbolRefs } : {}),
2177
2301
  ...(sp.deviceVolatile ? { deviceVolatile: sp.deviceVolatile } : {}),
2178
2302
  ...(sp.matchOnly ? { matchOnly: sp.matchOnly } : {}),
2303
+ ...(discardsPassedResult ? { discardsPassedResult: true as const } : {}),
2179
2304
  };
2180
2305
  seen.set(source, made);
2181
2306
  out.push(made);
@@ -2200,36 +2325,75 @@ export function enumerateCandidates(
2200
2325
  /** Score each candidate with the injected `scoreFn` and rank by score (lowest first). A candidate
2201
2326
  * whose `scoreFn` throws — e.g. its C failed to compile — is SKIPPED so it cannot sink a sibling
2202
2327
  * that compiles and matches; only if EVERY candidate fails is the failure surfaced. Synchronous:
2203
- * the scorer must be sync (the cli/Node objdiff path). The webapp scores asynchronously and does
2204
- * its own await-loop over `enumerateCandidates`, reusing this module's `Candidate`/`RankedResult`
2205
- * types but not this driver. */
2328
+ * the scorer must be sync (the cli/Node objdiff path). An async driver scores on its own schedule
2329
+ * and then ranks through here against its memoized outcomes (the pooled CLI driver, the webapp).
2330
+ *
2331
+ * A STILLBORN fan (stillborn.ts) is not compiled to the end: `scoreFn` is called for the default
2332
+ * candidate and the probes, and the rest ride on the thrown error as `notCompiled`. A `scoreFn`
2333
+ * that wants the rule to read its refusals throws `CompilerRejection`; any other throw ranks the
2334
+ * whole fan. */
2206
2335
  export function rankBy<S extends { score: number; rows?: number }>(
2207
2336
  candidates: Candidate[],
2208
2337
  symbol: string,
2209
2338
  scoreFn: (source: string, symbol: string, candidate: Candidate) => S,
2210
2339
  ): RankedResult<S> {
2340
+ const outcomes = new Map<number, { score: S } | { thrown: unknown }>();
2341
+ const outcomeAt = (i: number): { score: S } | { thrown: unknown } => {
2342
+ let outcome = outcomes.get(i);
2343
+ if (outcome === undefined) {
2344
+ try {
2345
+ outcome = { score: scoreFn(candidates[i].source, symbol, candidates[i]) };
2346
+ } catch (e) {
2347
+ outcome = { thrown: e };
2348
+ }
2349
+ outcomes.set(i, outcome);
2350
+ }
2351
+ return outcome;
2352
+ };
2353
+ const stillborn = stillbornVerdict(candidates, (i) => {
2354
+ const outcome = outcomeAt(i);
2355
+ return 'score' in outcome ? 'compiled' : outcome;
2356
+ });
2357
+ if (stillborn !== null) {
2358
+ const thrownAt = (i: number): unknown => {
2359
+ const outcome = outcomeAt(i);
2360
+ if ('score' in outcome) {
2361
+ throw new Error(`internal: a stillborn fan holds a candidate that scored (${i})`);
2362
+ }
2363
+ return outcome.thrown;
2364
+ };
2365
+ throw new NoScorableCandidateError(
2366
+ `no scorable candidate for '${symbol}': ${stillbornNote(stillborn, candidates.length)}\n` +
2367
+ `The default candidate's compile: ${fullMessage(thrownAt(0))}`,
2368
+ stillborn.compiled.map((i) => ({ variations: candidates[i].variations, error: firstLine(thrownAt(i)) })),
2369
+ [],
2370
+ stillborn.notCompiled.map((i) => ({ variations: candidates[i].variations, source: candidates[i].source })),
2371
+ { cause: thrownAt(0) },
2372
+ );
2373
+ }
2211
2374
  const results: (Scored<S> & { order: number })[] = [];
2212
2375
  const dropped: DroppedCandidate[] = []; // candidates that failed to build; only fatal if ALL do
2213
2376
  const withheld: WithheldCandidate[] = []; // candidates that built but did not earn publication
2214
2377
  let lastScoreErr: unknown = null;
2215
2378
  candidates.forEach((c, order) => {
2216
- try {
2217
- const score = scoreFn(c.source, symbol, c);
2218
- const why = withheldReason(c, score);
2219
- if (why !== null) {
2220
- withheld.push({
2221
- variations: c.variations,
2222
- score: score.score,
2223
- ...(score.rows === undefined ? {} : { rows: score.rows }),
2224
- why,
2225
- });
2226
- return;
2227
- }
2228
- results.push({ ...c, order, score });
2229
- } catch (e) {
2230
- lastScoreErr = e;
2231
- dropped.push({ variations: c.variations, error: firstLine(e) });
2379
+ const outcome = outcomeAt(order);
2380
+ if ('thrown' in outcome) {
2381
+ lastScoreErr = outcome.thrown;
2382
+ dropped.push({ variations: c.variations, error: firstLine(outcome.thrown) });
2383
+ return;
2384
+ }
2385
+ const { score } = outcome;
2386
+ const why = withheldReason(c, score);
2387
+ if (why !== null) {
2388
+ withheld.push({
2389
+ variations: c.variations,
2390
+ score: score.score,
2391
+ ...(score.rows === undefined ? {} : { rows: score.rows }),
2392
+ why,
2393
+ });
2394
+ return;
2232
2395
  }
2396
+ results.push({ ...c, order, score });
2233
2397
  });
2234
2398
  if (results.length === 0) {
2235
2399
  // Naming the withheld count matters here: "no scorable candidate" with a null cause reads as a
@@ -2240,7 +2404,7 @@ export function rankBy<S extends { score: number; rows?: number }>(
2240
2404
  // above stays one line each — it is a roster, not a diagnosis.
2241
2405
  const why =
2242
2406
  lastScoreErr !== null ? fullMessage(lastScoreErr) : `${withheld.length} candidate(s) withheld, none scored`;
2243
- throw new NoScorableCandidateError(`no scorable candidate for '${symbol}': ${why}`, dropped, withheld, {
2407
+ throw new NoScorableCandidateError(`no scorable candidate for '${symbol}': ${why}`, dropped, withheld, [], {
2244
2408
  cause: lastScoreErr,
2245
2409
  });
2246
2410
  }
@@ -2248,15 +2412,36 @@ export function rankBy<S extends { score: number; rows?: number }>(
2248
2412
  return { winner: results[0], candidates: results.map(({ order: _order, ...c }) => c), dropped, withheld };
2249
2413
  }
2250
2414
 
2251
- /** THE candidate ordering — score, then preference, then readability, then enumeration
2252
- * order. Exported because there are TWO drivers over the same enumeration (this module's sync
2253
- * `rankBy` for the Node/objdiff scorer, and the webapp's async await-loop for the wasm one), and
2254
- * a per-driver copy would let the same input produce two different winners.
2415
+ /** The sentence a stillborn fan's error OPENS on: how much of the fan was never compiled, on what
2416
+ * evidence, and the errors each half's compiled candidates kept. The DEFAULT candidate's own
2417
+ * diagnostic — the spelling the rule is anchored on, rather than whichever was refused last —
2418
+ * follows it, so that a printer bounding a long diagnostic keeps the verdict. */
2419
+ function stillbornNote(stillborn: Stillborn, fan: number): string {
2420
+ const tally = new Map<string, number>();
2421
+ for (const m of stillborn.messages) {
2422
+ tally.set(m, (tally.get(m) ?? 0) + 1);
2423
+ }
2424
+ const shared = [...tally].map(([m, n]) => ` ${m}${n > 1 ? ` (x${n})` : ''}`).join('\n');
2425
+ return (
2426
+ `${stillborn.notCompiled.length} of ${fan} candidates were NOT COMPILED: in each signedness half, the default ` +
2427
+ `candidate and one probe per variation (${stillborn.compiled.length} compiled) were all rejected, every probe ` +
2428
+ `keeping one of its default's errors:\n${shared}`
2429
+ );
2430
+ }
2431
+
2432
+ /** THE candidate ordering — score, then semantics, then preference, then readability, then enumeration
2433
+ * order. Exported because it is asked outside `rankBy` too — the benchmark's sweep driver and
2434
+ * fan runner order what they read back — and a second copy would let the same input produce two
2435
+ * different winners.
2255
2436
  *
2256
2437
  * SCORE dominates absolutely: the differ is the fitness function, and a tie means the variation
2257
2438
  * that separates these two candidates did not change the bytes — so everything below only chooses what
2258
2439
  * the READER sees, and can never cost a match.
2259
2440
  *
2441
+ * A DISCARDED PASSED RESULT next (`Candidate.discardsPassedResult`): equal bytes refute neither
2442
+ * reading, so the one that drops a value the callee may read wins only where it scores strictly
2443
+ * better.
2444
+ *
2260
2445
  * PREFERENCE next: a named symbol-map spelling beats its `/raw-globals` sibling at equal bytes.
2261
2446
  *
2262
2447
  * DEVICE VOLATILITY next: at equal bytes, the spelling that qualifies a DEVICE REGISTER
@@ -2274,7 +2459,9 @@ export function rankBy<S extends { score: number; rows?: number }>(
2274
2459
  * with no score and no outcome moving. That is a judgement about the source rather than a
2275
2460
  * measurement of it — the differ never refereed those six — and it is the same judgement this
2276
2461
  * term was declared to make, taken on the same evidence. What it must never do is change WHICH
2277
- * candidates exist; that stays an admission question, one variation at a time.
2462
+ * candidates exist; that stays an admission question, one variation at a time. The CAST COUNT
2463
+ * below does the same for `/narrow-decl`, which drops one cast per local it narrows and so wins
2464
+ * every tie against the candidate it is derived onto.
2278
2465
  *
2279
2466
  * CAST COUNT next, and only WITHIN a preference. A wrong signedness pin is what manufactures casts —
2280
2467
  * the C backend has to cast a shift operand back to the signedness the machine op needs, so
@@ -2305,6 +2492,7 @@ export function compareScored<S extends { score: number }>(
2305
2492
  ): number {
2306
2493
  return (
2307
2494
  a.score.score - b.score.score ||
2495
+ (a.discardsPassedResult ? 1 : 0) - (b.discardsPassedResult ? 1 : 0) ||
2308
2496
  a.preference - b.preference ||
2309
2497
  (b.deviceVolatile ?? 0) - (a.deviceVolatile ?? 0) ||
2310
2498
  castCount(a.source) - castCount(b.source) ||