@asmlift/core 0.8.1 → 0.10.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 (130) hide show
  1. package/README.md +36 -31
  2. package/package.json +1 -1
  3. package/src/aggregate.ts +397 -0
  4. package/src/backend/c.ts +6 -2
  5. package/src/backend/cfamily.ts +370 -56
  6. package/src/backend/cpp.ts +116 -10
  7. package/src/backend/pascal.ts +69 -11
  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 +777 -84
  15. package/src/declare.ts +132 -13
  16. package/src/frontend/asmdata.ts +26 -6
  17. package/src/frontend/call-plan.ts +1590 -0
  18. package/src/frontend/dead-reads.ts +628 -0
  19. package/src/frontend/device-pins.ts +296 -0
  20. package/src/frontend/emit.ts +4 -3
  21. package/src/frontend/errors.ts +5 -0
  22. package/src/frontend/flags-edge.ts +101 -0
  23. package/src/frontend/fpu.ts +374 -0
  24. package/src/frontend/frame-objects.ts +3171 -0
  25. package/src/frontend/frontend.ts +64 -3
  26. package/src/frontend/high-half.ts +55 -5
  27. package/src/frontend/jump-table.ts +530 -0
  28. package/src/frontend/local-object.ts +616 -0
  29. package/src/frontend/mips.ts +2392 -208
  30. package/src/frontend/opaque.ts +97 -18
  31. package/src/frontend/own-struct-return.ts +500 -0
  32. package/src/frontend/ppc.ts +3137 -267
  33. package/src/frontend/register-struct-call.ts +484 -0
  34. package/src/frontend/reloc-symbol.ts +109 -13
  35. package/src/frontend/splat.ts +108 -5
  36. package/src/frontend/ssa.ts +2098 -89
  37. package/src/frontend/stackargs.ts +172 -42
  38. package/src/frontend/thumb.ts +4563 -2091
  39. package/src/ir/alias.ts +71 -0
  40. package/src/ir/bits.ts +681 -1
  41. package/src/ir/core.ts +199 -6
  42. package/src/ir/discipline.ts +313 -0
  43. package/src/ir/entry.ts +159 -0
  44. package/src/ir/float-bits.ts +47 -0
  45. package/src/{structure → ir}/loops.ts +6 -2
  46. package/src/ir/opcodes.ts +193 -121
  47. package/src/ir/parse.ts +1 -0
  48. package/src/ir/types.ts +202 -2
  49. package/src/ir/verify.ts +219 -3
  50. package/src/l3/address.ts +5 -0
  51. package/src/l3/argcopy.ts +38 -6
  52. package/src/l3/ast.ts +467 -23
  53. package/src/l3/basecse.ts +33 -5
  54. package/src/l3/coalesce.ts +21 -13
  55. package/src/l3/constinit.ts +382 -0
  56. package/src/l3/dce.ts +34 -12
  57. package/src/l3/gates.ts +9 -3
  58. package/src/l3/hoist.ts +8 -7
  59. package/src/l3/initfirst.ts +68 -19
  60. package/src/l3/intcell.ts +98 -0
  61. package/src/l3/mentions.ts +10 -4
  62. package/src/l3/narrowdecl.ts +259 -0
  63. package/src/l3/nearbase.ts +10 -5
  64. package/src/l3/parkfirst.ts +8 -5
  65. package/src/l3/pollguard.ts +11 -4
  66. package/src/l3/ptrcell.ts +354 -0
  67. package/src/l3/ptrfield.ts +123 -8
  68. package/src/l3/regspell.ts +26 -7
  69. package/src/l3/reindex.ts +293 -103
  70. package/src/l3/storage.ts +98 -1
  71. package/src/l3/symbol-refs.ts +182 -23
  72. package/src/l3/tailmerge.ts +9 -20
  73. package/src/l3/typing.ts +237 -32
  74. package/src/l3/unreduce.ts +10 -9
  75. package/src/l3/volatileptr.ts +17 -54
  76. package/src/l3/volstore.ts +96 -7
  77. package/src/mangle.ts +41 -1
  78. package/src/pattern/engine.ts +373 -82
  79. package/src/pipeline.ts +589 -27
  80. package/src/proto-context.ts +1648 -0
  81. package/src/proto.ts +1203 -49
  82. package/src/raise/aggmembers.ts +290 -0
  83. package/src/raise/arrays.ts +8 -3
  84. package/src/raise/declared-volatile.ts +307 -0
  85. package/src/raise/divpow2.ts +2 -2
  86. package/src/raise/errors.ts +17 -0
  87. package/src/raise/floathelpers.ts +464 -0
  88. package/src/raise/globalshape.ts +11 -5
  89. package/src/raise/gvn.ts +29 -8
  90. package/src/raise/latch.ts +162 -6
  91. package/src/raise/memberarrays.ts +10 -4
  92. package/src/raise/narrow.ts +29 -2
  93. package/src/raise/narrowlocal.ts +11 -6
  94. package/src/raise/offsetnames.ts +494 -0
  95. package/src/raise/pairparams.ts +114 -0
  96. package/src/raise/paramwidth.ts +203 -6
  97. package/src/raise/pre-recovery.ts +174 -17
  98. package/src/raise/pre-structure.ts +54 -0
  99. package/src/raise/recover.ts +26 -15
  100. package/src/raise/reroll.ts +694 -0
  101. package/src/raise/retsink.ts +1 -1
  102. package/src/raise/shortcircuit.ts +66 -30
  103. package/src/raise/softdiv.ts +21 -23
  104. package/src/raise/storesign.ts +153 -0
  105. package/src/raise/struct-arrays.ts +34 -14
  106. package/src/raise/structs.ts +674 -98
  107. package/src/raise/truncload.ts +367 -0
  108. package/src/raise/unthread.ts +400 -0
  109. package/src/raise/widehelpers.ts +106 -0
  110. package/src/rank-declare.ts +143 -12
  111. package/src/rank-variations.ts +117 -31
  112. package/src/rank.ts +563 -122
  113. package/src/runtime-helpers.ts +405 -0
  114. package/src/stillborn.ts +289 -0
  115. package/src/structure/analysis.ts +1449 -449
  116. package/src/structure/arith-ops.ts +23 -0
  117. package/src/structure/globalaccess.ts +111 -9
  118. package/src/structure/hazards.ts +641 -114
  119. package/src/structure/local-statics.ts +517 -0
  120. package/src/structure/namecoalesce.ts +2 -2
  121. package/src/structure/pointer-spelling.ts +1106 -0
  122. package/src/structure/redundant-test.ts +99 -0
  123. package/src/structure/structure.ts +3478 -975
  124. package/src/structure/switch-recover.ts +403 -178
  125. package/src/symbols.ts +44 -3
  126. package/src/target.ts +1387 -71
  127. package/src/trace.ts +66 -28
  128. package/src/variation-definitions.ts +243 -13
  129. package/src/variation-gates.ts +2 -0
  130. package/src/variation-tokens.ts +25 -7
package/src/rank.ts CHANGED
@@ -21,15 +21,17 @@
21
21
  import { cBackend } from './backend/c';
22
22
  import {
23
23
  assertDerefsTyped,
24
+ assertFrameUnionsViewed,
25
+ assertLocalStorageStaysInteger,
24
26
  assertLocalsWritten,
25
27
  assertNoOrphanedLocals,
26
28
  assertPlacementSurvives,
29
+ assertPostIncrUnshared,
27
30
  assertResolved,
28
31
  } from './contracts';
29
32
  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';
33
+ import { hasSetupArgsNarrowing, narrowToSetupArgs, setupArgsDiscardsPassedResult } from './frontend/ssa';
34
+ import { Fn, Value, defOpMap } from './ir/core';
33
35
  import { T } from './ir/types';
34
36
  import { verify } from './ir/verify';
35
37
  import { advancedBases } from './l3/advance';
@@ -42,6 +44,7 @@ import type { Gate } from './l3/gates';
42
44
  import type { HoistPlacement } from './l3/hoist';
43
45
  import { homeSplitTag, homeSplitWithholds, splitHomeBases } from './l3/homesplit';
44
46
  import { inlinableConstBases, inlineConstBases } from './l3/inlinebase';
47
+ import { integerCells } from './l3/intcell';
45
48
  import { mulFirstSums } from './l3/mulfirst';
46
49
  import { nearBaseClusters } from './l3/nearbase';
47
50
  import { spellOperandMembers } from './l3/offmember';
@@ -52,15 +55,18 @@ import { reindexWalks } from './l3/reindex';
52
55
  import { hoistScopedBases } from './l3/scopebase';
53
56
  import { sinkInitsToFirstUse } from './l3/sinkinit';
54
57
  import type { SymbolRef } from './l3/symbol-refs';
58
+ import { signedOverUnsignedCompare } from './l3/typing';
55
59
  import { type UnreduceResult, unreduceAccumulators } from './l3/unreduce';
56
60
  import { deviceVolatileClaims, volatilePtrLocals, volatileSubsetCandidates } from './l3/volatileptr';
57
61
  import { volatileValueLocals } from './l3/volatileval';
58
62
  import { volatileDeviceStores } from './l3/volstore';
59
63
  import { zeroSubNegates } from './l3/zerosub';
60
64
  import { RewritePattern } from './pattern/engine';
61
- import { applyIdiomPatterns, raiseRecovered, structureChecked } from './pipeline';
62
- import { type Prototypes, prototypesFromSymbols } from './proto';
65
+ import { applyIdiomPatterns, liftStamped, raiseRecovered, stampBehaviorsFor, structureChecked } from './pipeline';
66
+ import { type FnProto, type Prototypes, declaresVoidReturn, prototypesFromSymbols } from './proto';
67
+ import { type ContextGlobals, splitOwnContext } from './proto-context';
63
68
  import { inferGlobalArrays, orderLicensedGlobals, sameDerivedShape } from './raise/globalshape';
69
+ import { OFFSET_NAME_PASS } from './raise/offsetnames';
64
70
  import { runPreRecovery } from './raise/pre-recovery';
65
71
  import { recoverTypes } from './raise/recover';
66
72
  import { sinkStoreTails } from './raise/tailsink';
@@ -68,6 +74,7 @@ import {
68
74
  type RefusedDeclarationReason,
69
75
  bareGlobalAccessFacts,
70
76
  bareGlobalSymbols,
77
+ initAddressSymbols,
71
78
  makeRefCollector,
72
79
  } from './rank-declare';
73
80
  import {
@@ -80,13 +87,16 @@ import {
80
87
  SIGNEDNESS,
81
88
  STACKED_SUBSETS,
82
89
  STRUCTURE_VARIATIONS,
90
+ StackedMemberThrew,
83
91
  type StructureVariation,
84
92
  UNFOLDED_HOISTS,
85
93
  applyStacked,
86
94
  createdLocals,
95
+ firstFired,
87
96
  sameBases,
88
97
  } from './rank-variations';
89
- import { hasDivergentSharedRet } from './structure/structure';
98
+ import { type Stillborn, stillbornVerdict } from './stillborn';
99
+ import { DefaultAcceptsMemo, DefaultAcceptsMemoMisuse, hasDivergentSharedRet } from './structure/structure';
90
100
  import {
91
101
  type SymbolInfo,
92
102
  type SymbolMap,
@@ -96,7 +106,13 @@ import {
96
106
  symbolsByName,
97
107
  } from './symbols';
98
108
  import { type TargetDescription, structureOptionsFor } from './target';
99
- import { type SubjectVariationName, type Variation, offeredOn, withSubject } from './variation-tokens';
109
+ import {
110
+ type SubjectVariationName,
111
+ type Variation,
112
+ type VariationName,
113
+ offeredOn,
114
+ withSubject,
115
+ } from './variation-tokens';
100
116
 
101
117
  /** Pin every SCALAR entry param (index not in `ptrIdx`) to the candidate signedness, before
102
118
  * recovery. Answers whether any param was PINNABLE — not whether its type moved: which of the
@@ -106,16 +122,33 @@ import { type SubjectVariationName, type Variation, offeredOn, withSubject } fro
106
122
  * A param NARROWED by raise/paramwidth.ts is not pinnable: the extension it was narrowed at states
107
123
  * the signedness as well as the width — agbcc's shift pair by its `asr`/`lsr`, PPC's `extsb`/`extsh`
108
124
  * 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. */
125
+ * widen it back to 32 bits.
126
+ *
127
+ * Nor is a param NOTHING READS. Its declared signedness reaches no instruction, so the two
128
+ * candidates would differ only in the signature's text and compile to the same object. The frontend
129
+ * mints such params on purpose: an argument register below the highest one read keeps its slot
130
+ * (`frontend/ssa.ts` mintArgSlotHoles). Counting one as pinnable would double the fan of every
131
+ * function whose only scalar params are unread. */
110
132
  function pinScalarParams(fn: Fn, signed: boolean, ptrIdx: Set<number>): boolean {
133
+ const read = new Set<Value>();
134
+ for (const b of fn.blocks) {
135
+ for (const op of b.ops) {
136
+ op.operands.forEach((v) => read.add(v));
137
+ op.successors.forEach((s) => s.args.forEach((v) => read.add(v)));
138
+ }
139
+ }
111
140
  let pinnable = false;
112
141
  fn.blocks[0].params.forEach((p, i) => {
113
- if (ptrIdx.has(i)) {
142
+ if (ptrIdx.has(i) || !read.has(p)) {
114
143
  return;
115
144
  }
116
- if (p.type.kind === 'unknown' || (p.type.kind === 'int' && p.type.width === 32)) {
145
+ // AT THE PARAMETER'S OWN WIDTH. The pin answers a question the asm does not — whether the
146
+ // source declared this parameter signed — and a 64-bit parameter has that question too; what
147
+ // it does not have is a spare 32 bits to lose to a constant here. A width the machine already
148
+ // narrowed (a declared `u8`) is not a scalar this variation re-decides, so it is left alone.
149
+ if (p.type.kind === 'unknown' || (p.type.kind === 'int' && p.type.width >= 32)) {
117
150
  pinnable = true;
118
- p.type = signed ? T.s(32) : T.u(32);
151
+ p.type = T.int(p.type.width, signed);
119
152
  }
120
153
  });
121
154
  return pinnable;
@@ -133,6 +166,10 @@ export interface EnumerateOptions {
133
166
  asmData?: AsmData;
134
167
  /** address→symbol map (symbols.ts) — same contract as DecompileOptions.symbols */
135
168
  symbols?: SymbolMap;
169
+ /** same contract as DecompileOptions.contextGlobals */
170
+ contextGlobals?: ContextGlobals;
171
+ /** same contract as DecompileOptions.ownContextProto */
172
+ ownContextProto?: FnProto;
136
173
  /** Called when a respell variation THROWS or fails a boundary contract, so the failure is visible
137
174
  * instead of the candidate silently not existing. Enumeration continues either way — the default
138
175
  * candidate is unaffected — but a variation that never fires because it always throws is a defect,
@@ -220,15 +257,50 @@ export interface Candidate {
220
257
  *
221
258
  * The third admission ground, and the narrowest. A respell variation must preserve semantics by
222
259
  * 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. */
260
+ * spelling is what the user is shown. Two spellings cannot meet that bar from inside the pass,
261
+ * each for its own fact (`PROOF_OBLIGATIONS` states them):
262
+ *
263
+ * • `l3/unreduce.ts` moves a memory read into a loop whose stores are all device registers, and
264
+ * on this board a device store can make the DEVICE write ordinary memory (a DMA trigger),
265
+ * which no gate over the C can rule out;
266
+ * • `l3/intcell.ts` spells a global as the integer a project may have declared it, where a
267
+ * pointer declaration scales the offset it adds, and no declaration is in the asm.
268
+ *
269
+ * A third obligation is judged on the finished tree rather than declared by a variation
270
+ * (`signedUnsignedCompare` below):
271
+ *
272
+ * • `/signed-cmp`, or a respell over its tree, spells an unsigned compare over signed-rendering
273
+ * operands, which compiles to a signed compare unless the operands are non-negative, and the
274
+ * compiler's proof of that is not in the asm.
275
+ *
276
+ * What settles all three instead is the object: a candidate that assembles to the target's own
277
+ * bytes IS the program, whatever a gate could have proved. So the spelling is offered, scored, and then
278
+ * either wins on proof or is WITHHELD — never shown as a best-effort answer. Both ranking drivers
279
+ * ask `withheldReason`, so neither can publish what the other would not. */
231
280
  matchOnly?: true;
281
+ /** The tree this source was emitted from spells some unsigned compare so that C evaluates it
282
+ * signed (l3/typing.ts `signedOverUnsignedCompare`, judged on the finished tree), which makes it
283
+ * `matchOnly` and names the fact it owes. Set for whichever variation got it there: emission
284
+ * under `/signed-cmp`, or a respell that substituted into a compare emission had pinned. It is
285
+ * the only place this obligation is decided: a pass that rebuilds a compare spreads the node to
286
+ * keep its `unsignedCmp` evidence (l3/regspell.ts), a merge that keeps one of two equal nodes
287
+ * keeps the evidence of both (l3/ast.ts `unionEvidence`), or drops it only where its own gate proved
288
+ * the new spelling compares the same values (l3/initfirst.ts `swapSide`, l3/reindex.ts). */
289
+ signedUnsignedCompare?: true;
290
+ /** This candidate's lift is `/setup-args` and drops an earlier callee's result that a call through
291
+ * a register is passed (frontend/ssa.ts `setupArgsDiscardsPassedResult`). It loses every score
292
+ * tie (compareScored): a tie refutes neither reading, and a shorter spelling must not win by
293
+ * dropping a value the callee may read. */
294
+ discardsPassedResult?: true;
295
+ /** This candidate's lift is `/narrow-param` and declares a parameter narrow that no prototype
296
+ * declares (raise/pre-recovery.ts `onUndeclaredWidth`). It loses every score tie
297
+ * (compareScored): a tie refutes neither declaration, and the narrow one changes the bytes of
298
+ * every prototyped caller, which this function's score cannot see. */
299
+ guessesParamWidth?: true;
300
+ /** This candidate's lift is `/far-cast` and spells a far member store through a cast of its
301
+ * base. It loses every score tie (compareScored): a tie means the compiler built the member's
302
+ * object too, and the member is the spelling that names the field. */
303
+ farStoreCast?: true;
232
304
  }
233
305
  /** A candidate paired with its score `S` (the injected scorer's result shape — must carry `.score`). */
234
306
  export interface Scored<S> extends Candidate {
@@ -261,10 +333,20 @@ export interface WithheldCandidate {
261
333
  why: string;
262
334
  }
263
335
 
336
+ /** A candidate that was NEVER COMPILED: the fan was declared stillborn (stillborn.ts) before its
337
+ * turn came. Kept apart from `dropped`, which means "the scorer refused it" — nothing refused
338
+ * this one, and a count that folded the two would report compiles that never ran. */
339
+ export interface NotCompiledCandidate {
340
+ variations: readonly string[];
341
+ /** the text the stop bet the compiler would refuse: a stop is re-placed whenever it changes, and
342
+ * the benchmark keys its STOP report on it (`fanNotCompiledDigest`) */
343
+ source: string;
344
+ }
345
+
264
346
  /** EVERY candidate refused — `rankBy` has no ranked result to return, so it throws this.
265
347
  *
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
348
+ * The lists ride on the error, and that is the point of having a class at all: on a row where
349
+ * nothing scored, the three of them ARE the whole fan, and a bare `Error` discards it. A caller that
268
350
  * prints one candidate's failure (the `cause`) is showing the LAST spelling the scorer refused,
269
351
  * which is neither the first nor a representative one — the benchmark's own noncompile rows have
270
352
  * up to a thousand siblings behind that single line. The MESSAGE is load-bearing and must stay
@@ -273,11 +355,20 @@ export interface WithheldCandidate {
273
355
  export class NoScorableCandidateError extends Error {
274
356
  readonly dropped: DroppedCandidate[];
275
357
  readonly withheld: WithheldCandidate[];
276
- constructor(message: string, dropped: DroppedCandidate[], withheld: WithheldCandidate[], options?: ErrorOptions) {
358
+ /** empty unless the fan was declared stillborn; `dropped` is then only what WAS compiled */
359
+ readonly notCompiled: NotCompiledCandidate[];
360
+ constructor(
361
+ message: string,
362
+ dropped: DroppedCandidate[],
363
+ withheld: WithheldCandidate[],
364
+ notCompiled: NotCompiledCandidate[],
365
+ options?: ErrorOptions,
366
+ ) {
277
367
  super(message, options);
278
368
  this.name = 'NoScorableCandidateError';
279
369
  this.dropped = dropped;
280
370
  this.withheld = withheld;
371
+ this.notCompiled = notCompiled;
281
372
  }
282
373
  }
283
374
 
@@ -301,7 +392,7 @@ export interface RankedResult<S> {
301
392
  candidates: Scored<S>[]; // sorted best (lowest) first
302
393
  /** candidates whose scoreFn threw — empty when every spelling built */
303
394
  dropped: DroppedCandidate[];
304
- /** candidates withheld for want of a byte-exact proof — empty unless a `matchOnly` variation fired */
395
+ /** candidates withheld for want of a byte-exact proof — empty unless a `matchOnly` candidate scored a nonmatch */
305
396
  withheld: WithheldCandidate[];
306
397
  }
307
398
 
@@ -309,14 +400,31 @@ export interface RankedResult<S> {
309
400
  * drivers over one enumeration (this module's sync `rankBy` and the webapp's async await-loop),
310
401
  * and a filter written twice is how they come to publish different answers. Null ⇒ publish.
311
402
  *
312
- * `score === 0` is objdiff's byte-exact match (cli objdiff.ts states the equivalence), which is
403
+ * `score === 0` is objdiff's byte-exact match (@match-kit/scoring states the equivalence), which is
313
404
  * why a bare `.score` suffices and the generic needs no `match` field. */
314
405
  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;
406
+ if (c.matchOnly !== true || score.score === 0) {
407
+ return null;
408
+ }
409
+ const facts = c.variations.flatMap((v) => PROOF_OBLIGATIONS.get(v) ?? []);
410
+ if (c.signedUnsignedCompare === true) {
411
+ facts.push(SIGNED_COMPARE_OBLIGATION);
412
+ }
413
+ const fact = facts.length > 0 ? facts.join(' and on ') : 'a fact no gate over the C can settle';
414
+ return `this spelling rests on ${fact}; only a byte-exact score proves it`;
318
415
  }
319
416
 
417
+ /** The fact each proof-carrying variation (`needsProof`) rests on, as `withheldReason` publishes it. */
418
+ const PROOF_OBLIGATIONS: ReadonlyMap<string, string> = new Map<VariationName, string>([
419
+ ['unreduce', 'a device-behaviour fact no gate over the C can settle'],
420
+ ['int-cell', 'the project declaring the global an integer, which the assembly does not show'],
421
+ ]);
422
+
423
+ /** The fact `Candidate.signedUnsignedCompare` rests on. Keyed by the flag, not by `/signed-cmp`:
424
+ * that name is on candidates that owe nothing (a compare over a `u32` operand) and missing from
425
+ * respells that do. */
426
+ const SIGNED_COMPARE_OBLIGATION = "the compiler having proved every unsigned compare's operands non-negative";
427
+
320
428
  /** What a respell variation hands `respell`: its tree, or — when the variation cannot establish the
321
429
  * candidate's semantics from inside the pass — the tree paired with that fact. `undefined`/`null`
322
430
  * is a decline. */
@@ -380,6 +488,8 @@ interface TreeSource {
380
488
  deviceVolatile?: number;
381
489
  /** see `Candidate.matchOnly` — set by a variation that cannot establish its own semantics */
382
490
  matchOnly?: true;
491
+ /** see `Candidate.signedUnsignedCompare` */
492
+ signedUnsignedCompare?: true;
383
493
  }
384
494
 
385
495
  /** Emit the DISTINCT type/branch-sense candidates for `name` — PURE, no scoring.
@@ -407,15 +517,17 @@ export function enumerateCandidates(
407
517
  let lastEmitError: unknown = null;
408
518
  // Same merge as `decompile`: the project's DWARF signatures fill in what the caller did not
409
519
  // state, so both the annotate pass and the ranked candidates reason about one prototype table.
410
- const prototypes = prototypesFromSymbols(opts.symbols, opts.prototypes ?? {});
411
- const frontend = frontendFor(target);
520
+ const ownContext = splitOwnContext(opts.prototypes, name, opts.ownContextProto);
521
+ const prototypes = prototypesFromSymbols(opts.symbols, ownContext.prototypes ?? {});
412
522
  const baseOpts = {
413
- ...structureOptionsFor(target, prototypes[name]?.returnsVoid ?? false),
523
+ ...structureOptionsFor(target, declaresVoidReturn(prototypes[name]), prototypes),
414
524
  // See the same line in pipeline.ts: a backend that cannot print switch fall-through must not
415
525
  // be handed a tree carrying one, because its refusal costs the whole candidate (and, when
416
526
  // every candidate carries it, the whole row).
417
527
  spellSwitchFallthrough: backend.spellsSwitchFallthrough,
528
+ spellSwitchBreak: backend.spellsSwitchBreak,
418
529
  ...(opts.symbols ? { symbols: symbolsByName(opts.symbols) } : {}),
530
+ ...(opts.contextGlobals ? { contextGlobals: opts.contextGlobals } : {}),
419
531
  };
420
532
  // Branch-sense is a differ-ranked VARIATION, the same class as param signedness: a divergent `if`
421
533
  // can be spelled with either sense (`if (c) A else B` vs `if (!c) B else A`), and which one the
@@ -537,12 +649,23 @@ export function enumerateCandidates(
537
649
  // it buys is that a lift-time change which splits them enumerates both settings rather than
538
650
  // silently dropping one, the failure nothing reports.
539
651
  let sharedLiftTreeOwned = false;
652
+ let sharedLiftExtendedReaders = false;
653
+ let sharedLiftFoldedStore = false;
540
654
  // Shared lift: recover ONCE with no signedness pin, to learn which entry params are
541
655
  // pointers/aggregates so they are excluded from the signedness variation (see NO_PIN_KINDS). One
542
656
  // extra lift+recover, no compile. (The shared lift deliberately stops after recoverTypes — it only reads the param KINDS, so
543
657
  // 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);
658
+ const sharedLift = liftStamped(
659
+ name,
660
+ asm,
661
+ target,
662
+ prototypes,
663
+ opts.asmData,
664
+ opts.symbols,
665
+ opts.contextGlobals,
666
+ undefined,
667
+ ownContext.ownContextProto,
668
+ );
546
669
  // The ARRAY SHAPES the input assembly evidences (raise/globalshape.ts), for the DECLARATION
547
670
  // half. Read off the shared lift's LIFTED form — before the fold below and the tower rewrite it —
548
671
  // because the base-materialization order the derivation's licence reads does not survive them.
@@ -557,6 +680,16 @@ export function enumerateCandidates(
557
680
  sharedLiftTreeOwned = true;
558
681
  },
559
682
  },
683
+ paramWidth: {
684
+ onExtendedReaders: () => {
685
+ sharedLiftExtendedReaders = true;
686
+ },
687
+ },
688
+ displaced: {
689
+ onFoldedStore: () => {
690
+ sharedLiftFoldedStore = true;
691
+ },
692
+ },
560
693
  });
561
694
  recoverTypes(sharedLift);
562
695
  const ptrIdx = new Set<number>(
@@ -581,18 +714,47 @@ export function enumerateCandidates(
581
714
  // shared lift's `onTreeOwned` hook has had its chance to fire. Called any earlier it would report a confident
582
715
  // `false` for a function that owns a tree. As a `const` below that call, an early call is a TDZ
583
716
  // ReferenceError instead — a wrong answer traded for a loud one.
584
- const treeOwnedIn = (symbols: typeof opts.symbols): boolean => {
717
+ //
718
+ // `/narrow-param`'s gate is read off the same lift the same way: the width pass reports a
719
+ // parameter the variation's lift would narrow (raise/pre-recovery.ts `ParamWidthOptions`). So is
720
+ // `/far-cast`'s: the displaced-access fold reports a store it moved into a struct.
721
+ const liftReportsIn = (
722
+ symbols: typeof opts.symbols,
723
+ ): { treeOwned: boolean; extendedReaders: boolean; foldedStore: boolean } => {
585
724
  if (symbols === opts.symbols) {
586
- return sharedLiftTreeOwned;
725
+ return {
726
+ treeOwned: sharedLiftTreeOwned,
727
+ extendedReaders: sharedLiftExtendedReaders,
728
+ foldedStore: sharedLiftFoldedStore,
729
+ };
587
730
  }
588
- const p = frontend.lift(name, asm, target, prototypes, opts.asmData, symbols);
589
- verify(p);
731
+ const p = liftStamped(
732
+ name,
733
+ asm,
734
+ target,
735
+ prototypes,
736
+ opts.asmData,
737
+ symbols,
738
+ opts.contextGlobals,
739
+ undefined,
740
+ ownContext.ownContextProto,
741
+ );
590
742
  applyIdiomPatterns(p, target, opts.patterns);
591
- let seen = false;
743
+ const seen = { treeOwned: false, extendedReaders: false, foldedStore: false };
592
744
  runPreRecovery(p, target, () => verify(p), prototypes[name], {
593
745
  shortCircuit: {
594
746
  onTreeOwned: () => {
595
- seen = true;
747
+ seen.treeOwned = true;
748
+ },
749
+ },
750
+ paramWidth: {
751
+ onExtendedReaders: () => {
752
+ seen.extendedReaders = true;
753
+ },
754
+ },
755
+ displaced: {
756
+ onFoldedStore: () => {
757
+ seen.foldedStore = true;
596
758
  },
597
759
  },
598
760
  });
@@ -800,6 +962,14 @@ export function enumerateCandidates(
800
962
  s.ptrElems &&
801
963
  s.declRank &&
802
964
  STRUCTURE_VARIATIONS.every((variation) => !s[variation.flag]);
965
+ /** Does a failure ABORT THE ROW, rather than drop the setting that threw? Only on the default
966
+ * candidate's own path: no variation named by the symbol-map setting or the lift setting —
967
+ * `settingVariations` is both, as the candidate spells them — and, where a structure setting is
968
+ * named, the default one. Anywhere else the failure is a dropped variation, reported through
969
+ * `onEnumerationError` under the same names. The lift, the raise and the structuring all ask
970
+ * this one predicate, so a setting that names a new variation is seen by all three. */
971
+ const abortsRow = (settingVariations: readonly Variation[], s?: StructureSetting): boolean =>
972
+ settingVariations.length === 0 && (s === undefined || isDefaultSetting(s));
803
973
 
804
974
  const seen = new Map<string, Candidate>();
805
975
  const seenTrees = new Set<string>();
@@ -840,14 +1010,18 @@ export function enumerateCandidates(
840
1010
  // every OTHER name in the function, so supplying more information made the tool strictly
841
1011
  // worse. A union cannot — each name is declared by whichever half knows more about it.
842
1012
  // 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
1013
+ // or `liftStamped` — feeding it to the lift would turn on pool promotion, interior
844
1014
  // attribution and the `/raw-globals` variation, which is a different (and source-moving) change.
845
1015
  // THREE halves now, in increasing authority: the name-only pool/reloc symbols, the array shapes
846
1016
  // the asm evidences for them (raise/globalshape.ts — an `extern u16 gTbl[];` where the bare
847
1017
  // spelling needs one, and the declaration a candidate spelling `gTbl[i]` cannot compile
848
1018
  // without), and the project map, which knows more than either.
849
1019
  const mapSymbols = baseOpts.symbols;
850
- const targetSymbols = new Map<string, SymbolInfo>([...bareGlobalSymbols(sharedLift), ...sharedLiftShapes]);
1020
+ const targetSymbols = new Map<string, SymbolInfo>([
1021
+ ...initAddressSymbols(sharedLift),
1022
+ ...bareGlobalSymbols(sharedLift),
1023
+ ...sharedLiftShapes,
1024
+ ]);
851
1025
  const declSymbols = new Map<string, SymbolInfo>([...targetSymbols, ...(mapSymbols ?? [])]);
852
1026
  // The four per-enumeration constants named at the seam rather than captured across 60 lines of
853
1027
  // closure (rank-declare.ts states why they belong on one object).
@@ -856,7 +1030,12 @@ export function enumerateCandidates(
856
1030
  accessFacts,
857
1031
  mapSymbols,
858
1032
  targetNames: new Set(targetSymbols.keys()),
1033
+ // The MERGED table, the same one the frontend was handed above, so the declaration a candidate
1034
+ // carries and the lift that produced it cannot state different things about a callee.
1035
+ prototypes,
1036
+ target,
859
1037
  refuse,
1038
+ own: Object.hasOwn(prototypes, name) ? prototypes[name] : ownContext.ownContextProto,
860
1039
  });
861
1040
  // THE RESPELL SET, as a function whose PARAMETER LIST is the invariant the tree skip below
862
1041
  // rests on: every source here is a pure function of the structured tree and this call's own
@@ -880,6 +1059,14 @@ export function enumerateCandidates(
880
1059
  // of `/volatile` — a candidate that did not fail and is still in the fan. The order
881
1060
  // is the candidates' own (`[pf.name, ...sp.variations]`), so a reported name and an enumerated
882
1061
  // candidate's variations name the same candidate the same way.
1062
+ //
1063
+ // THE COMPARE-SIGNEDNESS PROOF, asked of every tree a source is emitted from rather than of the
1064
+ // passes that build them. Emission pins each unsigned compare it lowers (structure.ts), but a
1065
+ // respell that substitutes into a compare — `/unmerge` putting an arm's `s16` global where the
1066
+ // pinned `u32` local stood, against an `s32` parameter — re-renders it signed. So the finished
1067
+ // tree is judged here, and a signed rendering of an `icmp_u*` is `matchOnly` whoever produced it.
1068
+ const cmpProof = (tree: SFn): { matchOnly?: true; signedUnsignedCompare?: true } =>
1069
+ signedOverUnsignedCompare(tree) ? { matchOnly: true, signedUnsignedCompare: true } : {};
883
1070
  const respellTree = (sfn: SFn, preRespellVariations: readonly Variation[] = []): TreeSources => {
884
1071
  // The walk→index respell variation (l3/reindex.ts) is a THIRD variation on the same footing as
885
1072
  // signedness and branch sense: whether the source spelled `*p; p++` or `arr[i]` is
@@ -897,7 +1084,7 @@ export function enumerateCandidates(
897
1084
  // Pascal backend loud-declines). Refusing EVERY tree is still loud — the empty-enumeration
898
1085
  // check at the end raises the last refusal.
899
1086
  try {
900
- sources.push({ variations: [], source: backend.emit(sfn), ...refsOf(sfn), ...volOf(sfn) });
1087
+ sources.push({ variations: [], source: backend.emit(sfn), ...refsOf(sfn), ...volOf(sfn), ...cmpProof(sfn) });
901
1088
  } catch (e) {
902
1089
  reportThrow(preRespellVariations, e);
903
1090
  return { sources, emit: { error: e } };
@@ -923,9 +1110,11 @@ export function enumerateCandidates(
923
1110
  // numeric-address pointer local — the joint spelling is reachable from neither variation
924
1111
  // alone, each composition narrows /volatile to that variation's own locals (volatilePtrLocals'
925
1112
  // `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
1113
+ // derived onto EVERY source: statement order/shape, and the width a narrow local is declared
1114
+ // at, are each orthogonal to what any respell variation outside that table changes — the same
1115
+ // kind of independent dimension as signedness —
1116
+ // so they are crossed with every source rather than paired (two members of the table may
1117
+ // compete for one node, and its order decides; rank-variations.ts); a third blanket composition needs
929
1118
  // the same argument, not just a row. And a specific PAIRING is admitted on one of two
930
1119
  // grounds, never on "it might help". FIRST, a row demands the joint spelling AND that
931
1120
  // spelling is reachable from neither variation alone: /livebase × /indexed, × /sinkinit,
@@ -991,14 +1180,25 @@ export function enumerateCandidates(
991
1180
  assertResolved(alt);
992
1181
  assertDerefsTyped(alt);
993
1182
  assertLocalsWritten(alt);
1183
+ assertPostIncrUnshared(alt);
994
1184
  assertNoOrphanedLocals(sfn, alt);
995
- sources.push({ variations, source: backend.emit(alt), ...refsOf(alt), ...volOf(alt), ...proof });
1185
+ assertLocalStorageStaysInteger(sfn, alt);
1186
+ assertFrameUnionsViewed(alt);
1187
+ sources.push({
1188
+ variations,
1189
+ source: backend.emit(alt),
1190
+ ...refsOf(alt),
1191
+ ...volOf(alt),
1192
+ ...proof,
1193
+ ...cmpProof(alt),
1194
+ });
996
1195
  // 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
1196
+ // admission argument). Each is a statement-order/shape or declaration-width fact
1197
+ // orthogonal to the respell variations outside the table; subsets compose in the fixed
1198
+ // table order, which decides between two members that compete for one node. A stacked variation that
999
1199
  // never fires declines and costs nothing.
1000
1200
  if (!alreadyShaped) {
1001
- // A shape REORDERS statements, and it is derived after a variation has placed its defs —
1201
+ // A shape may REORDER statements, and it is derived after a variation has placed its defs —
1002
1202
  // so the placement is re-checked on the shaped tree (contracts.ts). Differential: judged
1003
1203
  // only where the unshaped tree already satisfied the walk, so a variation whose placement
1004
1204
  // it never described is not dropped on the strength of a model that does not apply.
@@ -1018,18 +1218,24 @@ export function enumerateCandidates(
1018
1218
  // which moves only const or pure-read assigns and so cannot lift a read of a base local
1019
1219
  // above its init.
1020
1220
  const minted = createdLocals(sfn, alt);
1221
+ const firedSets = new Set<string>();
1021
1222
  for (const subset of STACKED_SUBSETS) {
1022
1223
  // ONE TRY PER SHAPE — a shape is its own candidate and fails as its own candidate.
1023
1224
  // Sharing the respell variation's outer try would let a throw deriving one subset
1024
1225
  // discard every later one, under that variation's name, which names no shape.
1025
- const shapeVariations = subset.map((x) => x.name);
1226
+ // A throw past applyStacked is reported under the fired set, the candidate's own name.
1227
+ let shapeVariations: readonly Variation[] = [];
1026
1228
  try {
1027
1229
  const shaped = applyStacked(subset, alt);
1028
- if (shaped !== null) {
1230
+ if (shaped !== null && firstFired(firedSets, shaped.variations)) {
1231
+ shapeVariations = shaped.variations;
1029
1232
  assertResolved(shaped.out);
1030
1233
  assertDerefsTyped(shaped.out);
1031
1234
  assertLocalsWritten(shaped.out);
1235
+ assertPostIncrUnshared(shaped.out);
1032
1236
  assertNoOrphanedLocals(alt, shaped.out);
1237
+ assertLocalStorageStaysInteger(alt, shaped.out);
1238
+ assertFrameUnionsViewed(shaped.out);
1033
1239
  assertPlacementSurvives(alt, shaped.out, minted);
1034
1240
  sources.push({
1035
1241
  variations: [...variations, ...shaped.variations],
@@ -1038,10 +1244,12 @@ export function enumerateCandidates(
1038
1244
  ...volOf(shaped.out),
1039
1245
  // a shape derived from a proof-gated spelling inherits the requirement
1040
1246
  ...proof,
1247
+ ...cmpProof(shaped.out),
1041
1248
  });
1042
1249
  }
1043
1250
  } catch (e) {
1044
- reportThrow([...preRespellVariations, ...variations, ...shapeVariations], e);
1251
+ const step = e instanceof StackedMemberThrew ? e.variations : shapeVariations;
1252
+ reportThrow([...preRespellVariations, ...variations, ...step], e);
1045
1253
  }
1046
1254
  }
1047
1255
  }
@@ -1057,19 +1265,21 @@ export function enumerateCandidates(
1057
1265
  // `/argbase` — name a call's argument bases before the call (l3/argbase.ts). A variation on the
1058
1266
  // same footing as the others: the default inline spelling stays in the list, so the differ
1059
1267
  // referees and this can never cost a match.
1268
+ const firedSets = new Set<string>();
1060
1269
  for (const subset of STACKED_SUBSETS) {
1061
1270
  // the truthful variations need the pass to RUN first, so this bypasses respell's
1062
1271
  // variations-then-thunk shape: same try posture, variations from the fired members
1063
1272
  try {
1064
1273
  const shaped = applyStacked(subset, sfn);
1065
- if (shaped !== null) {
1274
+ if (shaped !== null && firstFired(firedSets, shaped.variations)) {
1066
1275
  // the ONE call whose variations already name shapes — say so, rather than making `respell`
1067
1276
  // read them back out of the variations it was handed
1068
1277
  respell(shaped.variations, () => shaped.out, true);
1069
1278
  }
1070
1279
  } catch (e) {
1071
- // the report names the full subset — the fired set is unknown mid-throw
1072
- reportThrow([...preRespellVariations, ...subset.map((x) => x.name)], e);
1280
+ // `respell` reports its own throws, so only a member can throw here
1281
+ const step = e instanceof StackedMemberThrew ? e.variations : subset.map((x) => x.name);
1282
+ reportThrow([...preRespellVariations, ...step], e);
1073
1283
  }
1074
1284
  }
1075
1285
  respell(['argbase'], () => materializeArgBases(sfn));
@@ -1143,9 +1353,9 @@ export function enumerateCandidates(
1143
1353
  // only ties `/vol-store`'s 27, and RT's 32 beats the admitted standalone `/unreduce`'s 35, so
1144
1354
  // "worse than an admitted variation" would exclude neither.
1145
1355
  //
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
1356
+ // WHAT THE STANDALONE LINES COST, since the two variations rarely win an artifact row ALONE —
1357
+ // `apps/benchmark/test/census.test.ts` names every `/unreduce` and `/ptr-field` winner outside
1358
+ // a `/vol-store` pairing, and fails on one it does not name. They are kept because a
1149
1359
  // variation has to be able to LOSE on its own terms: the admission posture (compareScored orders
1150
1360
  // by score) is what makes a wrong respelled source harmless, and it is only observable when the
1151
1361
  // single-variation candidate is in the fan —
@@ -1666,6 +1876,17 @@ export function enumerateCandidates(
1666
1876
  // Which placement the source used is not derivable from the asm, so both are emitted and the
1667
1877
  // differ referees.
1668
1878
  respell(['sinkinit'], () => sinkInitsToFirstUse(sfn));
1879
+ // `/int-cell` — every pointer cell spelled as the integer a project may have declared it,
1880
+ // `g = g + K` and no conversions (l3/intcell.ts). The default's `g = (void *)((u8 *)g + K)` is
1881
+ // right under a pointer declaration of g and rejected by CodeWarrior under an integer one, where
1882
+ // this is the source; which the project declared is not in the asm, so it is published only at
1883
+ // a byte-exact score.
1884
+ respell(['int-cell'], () =>
1885
+ integerCells(sfn, (n) => {
1886
+ const shape = mapSymbols?.get(n)?.shape;
1887
+ return shape !== undefined && shape !== 'scalar';
1888
+ }),
1889
+ );
1669
1890
  // the register-copy variation (l3/regspell.ts): 0–3 results (base; tail assign-back reusing
1670
1891
  // the dead value var; tail assign-back into a fresh var — the tail decision is allocator-
1671
1892
  // ambiguous, so both are ranked).
@@ -1707,8 +1928,13 @@ export function enumerateCandidates(
1707
1928
  { variations: ['raw-globals'], symbols: undefined },
1708
1929
  ]
1709
1930
  : [{ variations: [] }];
1931
+ // The map's POINTER declarations go to the raw setting all the same: a raw name is still a
1932
+ // pointer in the project's file, so arithmetic on its value scales unless spelled in bytes.
1933
+ const pointerGlobals = new Set(
1934
+ [...(baseOpts.symbols?.entries() ?? [])].filter(([, info]) => info.shape === 'pointer').map(([n]) => n),
1935
+ );
1710
1936
  for (const [symbolIndex, symbolSetting] of symbolSettings.entries()) {
1711
- const symbolSettingOpts = symbolSetting.symbols ? baseOpts : { ...baseOpts, symbols: undefined };
1937
+ const symbolSettingOpts = symbolSetting.symbols ? baseOpts : { ...baseOpts, symbols: undefined, pointerGlobals };
1712
1938
  // `/no-bitfield` names a spelling the MAP makes available, so it has no inhabitant on the
1713
1939
  // symbol-map setting that structures without one: structure() normalizes `spellBitfieldMembers`
1714
1940
  // to false when `symbols` is absent, so both settings structure the identical tree whatever
@@ -1728,7 +1954,23 @@ export function enumerateCandidates(
1728
1954
  const usableStructureSettings = symbolSetting.symbols
1729
1955
  ? structureSettings
1730
1956
  : structureSettings.filter((s) => s.bitfields && s.ptrElems && (s.declRank || rawDerivesRank));
1731
- const treeOwnedFold = treeOwnedIn(symbolSetting.symbols);
1957
+ // THE `/raw-globals` SETTING IS A VARIATION, so a lift that declines without the map drops it
1958
+ // and keeps the map's candidates (`abortsRow`). The two lifts can disagree on whether a
1959
+ // function lifts at all: a name only the map places is, without it, a pointer the frame-object
1960
+ // audit cannot bound. This is the setting's first lift, and every later one is the same lift
1961
+ // again.
1962
+ let treeOwnedFold: boolean;
1963
+ let extendedReaders: boolean;
1964
+ let foldedStore: boolean;
1965
+ try {
1966
+ ({ treeOwned: treeOwnedFold, extendedReaders, foldedStore } = liftReportsIn(symbolSetting.symbols));
1967
+ } catch (e) {
1968
+ if (abortsRow(symbolSetting.variations)) {
1969
+ throw e;
1970
+ }
1971
+ reportThrow(symbolSetting.variations, e);
1972
+ continue;
1973
+ }
1732
1974
  // The signedness variation DECLINES where the pin has nothing to pin. `pinScalarParams` writes only
1733
1975
  // over an entry param still `unknown`/`int` that is not one of the recovered pointers/
1734
1976
  // aggregates `ptrIdx` excludes; where no param is left, the second pass re-lifts, re-raises and
@@ -1744,7 +1986,22 @@ export function enumerateCandidates(
1744
1986
  if (cand.signed && !pinnable) {
1745
1987
  break;
1746
1988
  }
1747
- const base = frontend.lift(name, asm, target, prototypes, opts.asmData, symbolSetting.symbols);
1989
+ let tailSlot = false;
1990
+ const base = liftStamped(
1991
+ name,
1992
+ asm,
1993
+ target,
1994
+ prototypes,
1995
+ opts.asmData,
1996
+ symbolSetting.symbols,
1997
+ opts.contextGlobals,
1998
+ {
1999
+ onTailSlot: () => {
2000
+ tailSlot = true;
2001
+ },
2002
+ },
2003
+ ownContext.ownContextProto,
2004
+ );
1748
2005
  // `/setup-args` — pass a prototype-less callee only what the CALLING BLOCK set up; which of
1749
2006
  // the two readings the source spelled is genuinely ambiguous, and frontend/ssa.ts
1750
2007
  // narrowToSetupArgs carries the argument for why the differ is what settles it.
@@ -1810,11 +2067,66 @@ export function enumerateCandidates(
1810
2067
  { variations: ['setup-args'], narrow: true },
1811
2068
  ]
1812
2069
  : [{ variations: [], narrow: false }];
2070
+ // `/narrow-param` — declare a parameter every reader of which is an extension at the widest
2071
+ // reader's width (raise/paramwidth.ts, EVERY READER AN EXTENSION). A lift variation for the
2072
+ // reason `/connective` is: the raise mutates in place, and the parameter's width is what
2073
+ // recovery and every later stage read. Enumerated only where this symbol-map setting's lift
2074
+ // reports such a parameter; crossed with the other two, and the dedup collapses what agrees.
2075
+ const paramSettings: { variations: readonly Variation[]; extendedReaders: boolean }[] = extendedReaders
2076
+ ? [
2077
+ { variations: [], extendedReaders: false },
2078
+ { variations: ['narrow-param'], extendedReaders: true },
2079
+ ]
2080
+ : [{ variations: [], extendedReaders: false }];
2081
+ // `/kept-slot` — keep the delay slot of a MIPS `b`/`j` whose slot ends a common tail both arms
2082
+ // spell, where the default lift retargets it to the copy before its target and merges the
2083
+ // arms (frontend/mips.ts `normaliseBranchLikely`: the two are one object, and which the source
2084
+ // wrote is the differ's to say). A LIFT variation because the rewrite is the CFG every later
2085
+ // stage reads. Enumerated only where this setting's default lift reports such a slot; crossed
2086
+ // with the others, and the dedup collapses what agrees. An IDO reading: GCC 2.7.2 and KMC
2087
+ // cross-jump a tail both arms spell before they fill a slot, so they leave no such tail.
2088
+ const tailSettings: { variations: readonly Variation[]; keepTailSlots: boolean }[] = tailSlot
2089
+ ? [
2090
+ { variations: [], keepTailSlots: false },
2091
+ { variations: ['kept-slot'], keepTailSlots: true },
2092
+ ]
2093
+ : [{ variations: [], keepTailSlots: false }];
2094
+ // `/far-cast` — keep each store the displaced-access fold would move into a struct on the
2095
+ // address it was reached through (raise/structs.ts `foldDisplacedAccesses`). On agbcc the
2096
+ // member store and the cast store are two objects (target.ts `farMemberStoreOrder`), and a
2097
+ // hoisted pointer the source held builds the cast's. Elsewhere a folded store stops aliasing
2098
+ // the loads beside it and a held pointer is spelled as members, which KMC, GCC 2.7.2 and mwcc
2099
+ // build into other objects. So which one the source wrote is the differ's to say, on every
2100
+ // target. A LIFT variation because the fold decides which struct the stores join, which
2101
+ // recovery and every later stage read. Enumerated only where this setting's lift folded a
2102
+ // store.
2103
+ const castSettings: { variations: readonly Variation[]; keepStores: boolean }[] =
2104
+ foldedStore && offeredOn(target, ['far-cast'])
2105
+ ? [
2106
+ { variations: [], keepStores: false },
2107
+ { variations: ['far-cast'], keepStores: true },
2108
+ ]
2109
+ : [{ variations: [], keepStores: false }];
1813
2110
  const liftSettings = narrowSettings.flatMap((l) =>
1814
- connectiveSettings.map((c) => ({ ...l, ...c, variations: [...l.variations, ...c.variations] })),
2111
+ connectiveSettings.flatMap((c) =>
2112
+ paramSettings.flatMap((w) =>
2113
+ tailSettings.flatMap((k) =>
2114
+ castSettings.map((f) => ({
2115
+ ...l,
2116
+ ...c,
2117
+ ...w,
2118
+ ...k,
2119
+ ...f,
2120
+ variations: [...l.variations, ...c.variations, ...w.variations, ...k.variations, ...f.variations],
2121
+ })),
2122
+ ),
2123
+ ),
2124
+ ),
1815
2125
  );
1816
2126
  for (const liftSetting of liftSettings) {
1817
2127
  let fn: Fn;
2128
+ let discardsPassedResult = false;
2129
+ let guessesParamWidth = false;
1818
2130
  let inferredSymbols = new Map<string, SymbolInfo>();
1819
2131
  let orderLicensed: ReadonlySet<string> = new Set<string>();
1820
2132
  try {
@@ -1823,10 +2135,21 @@ export function enumerateCandidates(
1823
2135
  fn =
1824
2136
  liftSetting.variations.length === 0
1825
2137
  ? base
1826
- : frontend.lift(name, asm, target, prototypes, opts.asmData, symbolSetting.symbols);
2138
+ : liftStamped(
2139
+ name,
2140
+ asm,
2141
+ target,
2142
+ prototypes,
2143
+ opts.asmData,
2144
+ symbolSetting.symbols,
2145
+ opts.contextGlobals,
2146
+ { keepTailSlots: liftSetting.keepTailSlots },
2147
+ ownContext.ownContextProto,
2148
+ );
1827
2149
  if (liftSetting.narrow && !narrowToSetupArgs(fn)) {
1828
2150
  continue; // nothing to cut after all — the default lift's own candidates already cover it
1829
2151
  }
2152
+ discardsPassedResult = liftSetting.narrow && setupArgsDiscardsPassedResult(fn);
1830
2153
  verify(fn);
1831
2154
  // This lift's OWN array-shape evidence, off its own lifted fn (a symbol map promotes
1832
2155
  // numeric pool words to `gaddr`, so the `/raw-globals` setting genuinely answers differently).
@@ -1855,6 +2178,13 @@ export function enumerateCandidates(
1855
2178
  // declaration. A map that names the symbol an array takes the access to a bare `var`
1856
2179
  // base anyway, which carries no licence: the two never meet.
1857
2180
  orderLicensed = orderLicensedGlobals(fn, target);
2181
+ // …and the NAMES the map holds for the addresses this lift built by arithmetic off a
2182
+ // named one (raise/offsetnames.ts). Per lift setting, off that setting's own map: the
2183
+ // `/raw-globals` sibling has no map and so no name to reach. AFTER the two readings, and
2184
+ // the staleness that leaves them is priced in pipeline.ts's copy of this note.
2185
+ if (symbolSetting.symbols) {
2186
+ OFFSET_NAME_PASS.run(fn, symbolSetting.symbols, stampBehaviorsFor(target, opts.contextGlobals));
2187
+ }
1858
2188
  for (const [n, si] of [...inferredSymbols]) {
1859
2189
  if (baseOpts.symbols?.has(n) === true || !sameDerivedShape(declSymbols.get(n), si)) {
1860
2190
  inferredSymbols.delete(n);
@@ -1875,18 +2205,28 @@ export function enumerateCandidates(
1875
2205
  },
1876
2206
  },
1877
2207
  prototypes[name],
1878
- { shortCircuit: { foldTreeOwned: liftSetting.connective } },
2208
+ {
2209
+ shortCircuit: { foldTreeOwned: liftSetting.connective },
2210
+ paramWidth: {
2211
+ extendedReaders: liftSetting.extendedReaders,
2212
+ onUndeclaredWidth: () => {
2213
+ guessesParamWidth = true;
2214
+ },
2215
+ },
2216
+ displaced: { keepStores: liftSetting.keepStores },
2217
+ },
1879
2218
  );
1880
2219
  } catch (e) {
1881
2220
  // 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
2221
+ // an empty list is the only spelling of "no lift variation is on" that stays correct
1883
2222
  // when a fourth is added — the same reason the structure half below reads its table
1884
2223
  // instead of naming its flags.
1885
- if (liftSetting.variations.length === 0) {
2224
+ const settingVariations = [...liftSetting.variations, ...symbolSetting.variations];
2225
+ if (abortsRow(settingVariations)) {
1886
2226
  throw e; // the default lift keeps its behavior: a raising failure aborts the row
1887
2227
  }
1888
2228
  // A dropped variation, never an aborted enumeration — the same posture as `respell`.
1889
- reportThrow(liftSetting.variations, e);
2229
+ reportThrow(settingVariations, e);
1890
2230
  continue;
1891
2231
  }
1892
2232
  // THE SHARED-TAIL VARIATIONS: the same raised fn, structured again with `followEarlyReturns`,
@@ -1894,7 +2234,8 @@ export function enumerateCandidates(
1894
2234
  // - `/shared-ret` is the follow ALONE, on the fn as raised. Some divergent `if` shares a
1895
2235
  // `ret` the compiler left in place (`synthetic:gcseinner`).
1896
2236
  // - `/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.
2237
+ // is safe because `structure()` writes nothing on `fn` but the `declared` stamps, which it
2238
+ // re-derives from its map on every call, so the earlier passes are done with it.
1898
2239
  // It is enumerated only where the sink changed something AND some divergent `if` of the
1899
2240
  // SUNK fn shares a `ret`. That is the sink's price gate: without it, a tail copied into
1900
2241
  // arms no `if` shares adds a candidate spelling it in each — 204 synthetic candidates on 8
@@ -1930,6 +2271,8 @@ export function enumerateCandidates(
1930
2271
  // `droppedDefault` is the DEFAULT pass's drops. Each shared-tail pass reads it and keeps its
1931
2272
  // own drops in a copy, so one pass's structuring failure never removes the other's candidate.
1932
2273
  const droppedDefault = new Set<string>();
2274
+ // What `assertDefaultAccepts` learned about this fn, shared by every setting structured from it.
2275
+ let acceptsMemo = new DefaultAcceptsMemo(fn);
1933
2276
  for (const pass of ['default', 'follow', 'sink'] as const) {
1934
2277
  if (pass === 'follow' && !hasDivergentSharedRet(fn)) {
1935
2278
  continue;
@@ -1938,11 +2281,13 @@ export function enumerateCandidates(
1938
2281
  let sunk: boolean;
1939
2282
  try {
1940
2283
  sunk = sinkStoreTails(fn);
2284
+ // The sink rewrites fn in place, so nothing the memo learned before it holds after it.
2285
+ acceptsMemo = new DefaultAcceptsMemo(fn);
1941
2286
  if (sunk) {
1942
2287
  verify(fn);
1943
2288
  }
1944
2289
  } catch (e) {
1945
- reportThrow([...liftSetting.variations, 'shared-tail'], e);
2290
+ reportThrow([...liftSetting.variations, 'shared-tail', ...symbolSetting.variations], e);
1946
2291
  break;
1947
2292
  }
1948
2293
  // Unsunk, this fn is the `/shared-ret` pass's again.
@@ -1996,30 +2341,39 @@ export function enumerateCandidates(
1996
2341
  dropped.add(key);
1997
2342
  continue;
1998
2343
  }
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.
2344
+ // structure() produces a fresh SFn and writes nothing on `fn` but the `declared` stamps,
2345
+ // which it re-derives from its map on every call, so both branch senses structure the same
2346
+ // recovered function without re-lifting.
2001
2347
  let sfn: SFn;
2002
2348
  try {
2003
- sfn = structureChecked(fn, {
2004
- ...symbolSettingOpts,
2005
- ...(inferredSymbols.size ? { inferredSymbols } : {}),
2006
- ...(orderLicensed.size ? { orderLicensedGlobals: orderLicensed } : {}),
2007
- preserveDivergentBranchSense: s.sense,
2008
- negateJoinedBranchSense: s.join ? !defSense : defSense,
2009
- ...(s.flipSites ? { branchSenseFlipSites: s.flipSites } : {}),
2010
- anchorConstCopies: s.anchor,
2011
- anchorLoopEntryConsts: s.entry,
2012
- spellBitfieldMembers: s.bitfields,
2013
- spellPtrMemberElements: s.ptrElems,
2014
- spellDeclaredSubscripts: s.declRank,
2015
- ...STRUCTURE_VARIATIONS.reduce(
2016
- (acc, variation) => ({ ...acc, ...variation.options(s[variation.flag]) }),
2017
- {},
2018
- ),
2019
- ...(alternative ? { followEarlyReturns: true } : {}),
2020
- });
2349
+ sfn = structureChecked(
2350
+ fn,
2351
+ {
2352
+ ...symbolSettingOpts,
2353
+ ...(inferredSymbols.size ? { inferredSymbols } : {}),
2354
+ ...(orderLicensed.size ? { orderLicensedGlobals: orderLicensed } : {}),
2355
+ preserveDivergentBranchSense: s.sense,
2356
+ negateJoinedBranchSense: s.join ? !defSense : defSense,
2357
+ ...(s.flipSites ? { branchSenseFlipSites: s.flipSites } : {}),
2358
+ anchorConstCopies: s.anchor,
2359
+ anchorLoopEntryConsts: s.entry,
2360
+ spellBitfieldMembers: s.bitfields,
2361
+ spellPtrMemberElements: s.ptrElems,
2362
+ spellDeclaredSubscripts: s.declRank,
2363
+ ...STRUCTURE_VARIATIONS.reduce(
2364
+ (acc, variation) => ({ ...acc, ...variation.options(s[variation.flag]) }),
2365
+ {},
2366
+ ),
2367
+ ...(alternative ? { followEarlyReturns: true } : {}),
2368
+ },
2369
+ undefined,
2370
+ acceptsMemo,
2371
+ );
2021
2372
  } catch (e) {
2022
- if (liftVariations.length === 0 && isDefaultSetting(s)) {
2373
+ if (e instanceof DefaultAcceptsMemoMisuse) {
2374
+ throw e; // a memo wired to the wrong fn is a defect here, never a dropped variation
2375
+ }
2376
+ if (abortsRow([...liftVariations, ...symbolSetting.variations], s)) {
2023
2377
  throw e; // the default lift's default setting keeps its behavior: a failure aborts the row
2024
2378
  }
2025
2379
  // Recorded for EVERY dropped setting: a candidate with more variations on looks its siblings
@@ -2027,7 +2381,7 @@ export function enumerateCandidates(
2027
2381
  dropped.add(key);
2028
2382
  // an anchored setting that fails structuring or its contracts is a dropped variation, never
2029
2383
  // an aborted enumeration — same rule as respell below
2030
- reportThrow([...liftVariations, ...s.variations], e);
2384
+ reportThrow([...liftVariations, ...s.variations, ...symbolSetting.variations], e);
2031
2385
  continue;
2032
2386
  }
2033
2387
  // A TREE another structure setting already produced. `respellTree` reads the tree and this
@@ -2092,7 +2446,10 @@ export function enumerateCandidates(
2092
2446
  assertResolved(made);
2093
2447
  assertDerefsTyped(made);
2094
2448
  assertLocalsWritten(made);
2449
+ assertPostIncrUnshared(made);
2095
2450
  assertNoOrphanedLocals(sfn, made);
2451
+ assertLocalStorageStaysInteger(sfn, made);
2452
+ assertFrameUnionsViewed(made);
2096
2453
  // A backend refusal on this REWRITTEN tree is not a refusal of the row's own
2097
2454
  // tree, so it never becomes the row's stated cause: `TreeSources.emit` is dropped
2098
2455
  // here and only the call over the row's own tree above records one.
@@ -2153,6 +2510,16 @@ export function enumerateCandidates(
2153
2510
  // whichever route the enumeration happened to walk first.
2154
2511
  if (sp.matchOnly === undefined) {
2155
2512
  delete dup.matchOnly;
2513
+ delete dup.signedUnsignedCompare;
2514
+ }
2515
+ if (!discardsPassedResult) {
2516
+ delete dup.discardsPassedResult;
2517
+ }
2518
+ if (!guessesParamWidth) {
2519
+ delete dup.guessesParamWidth;
2520
+ }
2521
+ if (!liftSetting.keepStores) {
2522
+ delete dup.farStoreCast;
2156
2523
  }
2157
2524
  continue;
2158
2525
  }
@@ -2175,7 +2542,11 @@ export function enumerateCandidates(
2175
2542
  preference: symbolIndex,
2176
2543
  ...(sp.symbolRefs ? { symbolRefs: sp.symbolRefs } : {}),
2177
2544
  ...(sp.deviceVolatile ? { deviceVolatile: sp.deviceVolatile } : {}),
2178
- ...(sp.matchOnly ? { matchOnly: sp.matchOnly } : {}),
2545
+ ...(sp.matchOnly ? { matchOnly: true as const } : {}),
2546
+ ...(sp.signedUnsignedCompare ? { signedUnsignedCompare: true as const } : {}),
2547
+ ...(discardsPassedResult ? { discardsPassedResult: true as const } : {}),
2548
+ ...(guessesParamWidth ? { guessesParamWidth: true as const } : {}),
2549
+ ...(liftSetting.keepStores ? { farStoreCast: true as const } : {}),
2179
2550
  };
2180
2551
  seen.set(source, made);
2181
2552
  out.push(made);
@@ -2200,36 +2571,75 @@ export function enumerateCandidates(
2200
2571
  /** Score each candidate with the injected `scoreFn` and rank by score (lowest first). A candidate
2201
2572
  * whose `scoreFn` throws — e.g. its C failed to compile — is SKIPPED so it cannot sink a sibling
2202
2573
  * 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. */
2574
+ * the scorer must be sync (the cli/Node objdiff path). An async driver scores on its own schedule
2575
+ * and then ranks through here against its memoized outcomes (the pooled CLI driver, the webapp).
2576
+ *
2577
+ * A STILLBORN fan (stillborn.ts) is not compiled to the end: `scoreFn` is called for the default
2578
+ * candidate and the probes, and the rest ride on the thrown error as `notCompiled`. A `scoreFn`
2579
+ * that wants the rule to read its refusals throws `CompilerRejection`; any other throw ranks the
2580
+ * whole fan. */
2206
2581
  export function rankBy<S extends { score: number; rows?: number }>(
2207
2582
  candidates: Candidate[],
2208
2583
  symbol: string,
2209
2584
  scoreFn: (source: string, symbol: string, candidate: Candidate) => S,
2210
2585
  ): RankedResult<S> {
2586
+ const outcomes = new Map<number, { score: S } | { thrown: unknown }>();
2587
+ const outcomeAt = (i: number): { score: S } | { thrown: unknown } => {
2588
+ let outcome = outcomes.get(i);
2589
+ if (outcome === undefined) {
2590
+ try {
2591
+ outcome = { score: scoreFn(candidates[i].source, symbol, candidates[i]) };
2592
+ } catch (e) {
2593
+ outcome = { thrown: e };
2594
+ }
2595
+ outcomes.set(i, outcome);
2596
+ }
2597
+ return outcome;
2598
+ };
2599
+ const stillborn = stillbornVerdict(candidates, (i) => {
2600
+ const outcome = outcomeAt(i);
2601
+ return 'score' in outcome ? 'compiled' : outcome;
2602
+ });
2603
+ if (stillborn !== null) {
2604
+ const thrownAt = (i: number): unknown => {
2605
+ const outcome = outcomeAt(i);
2606
+ if ('score' in outcome) {
2607
+ throw new Error(`internal: a stillborn fan holds a candidate that scored (${i})`);
2608
+ }
2609
+ return outcome.thrown;
2610
+ };
2611
+ throw new NoScorableCandidateError(
2612
+ `no scorable candidate for '${symbol}': ${stillbornNote(stillborn, candidates.length)}\n` +
2613
+ `The default candidate's compile: ${fullMessage(thrownAt(0))}`,
2614
+ stillborn.compiled.map((i) => ({ variations: candidates[i].variations, error: firstLine(thrownAt(i)) })),
2615
+ [],
2616
+ stillborn.notCompiled.map((i) => ({ variations: candidates[i].variations, source: candidates[i].source })),
2617
+ { cause: thrownAt(0) },
2618
+ );
2619
+ }
2211
2620
  const results: (Scored<S> & { order: number })[] = [];
2212
2621
  const dropped: DroppedCandidate[] = []; // candidates that failed to build; only fatal if ALL do
2213
2622
  const withheld: WithheldCandidate[] = []; // candidates that built but did not earn publication
2214
2623
  let lastScoreErr: unknown = null;
2215
2624
  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) });
2625
+ const outcome = outcomeAt(order);
2626
+ if ('thrown' in outcome) {
2627
+ lastScoreErr = outcome.thrown;
2628
+ dropped.push({ variations: c.variations, error: firstLine(outcome.thrown) });
2629
+ return;
2232
2630
  }
2631
+ const { score } = outcome;
2632
+ const why = withheldReason(c, score);
2633
+ if (why !== null) {
2634
+ withheld.push({
2635
+ variations: c.variations,
2636
+ score: score.score,
2637
+ ...(score.rows === undefined ? {} : { rows: score.rows }),
2638
+ why,
2639
+ });
2640
+ return;
2641
+ }
2642
+ results.push({ ...c, order, score });
2233
2643
  });
2234
2644
  if (results.length === 0) {
2235
2645
  // Naming the withheld count matters here: "no scorable candidate" with a null cause reads as a
@@ -2240,7 +2650,7 @@ export function rankBy<S extends { score: number; rows?: number }>(
2240
2650
  // above stays one line each — it is a roster, not a diagnosis.
2241
2651
  const why =
2242
2652
  lastScoreErr !== null ? fullMessage(lastScoreErr) : `${withheld.length} candidate(s) withheld, none scored`;
2243
- throw new NoScorableCandidateError(`no scorable candidate for '${symbol}': ${why}`, dropped, withheld, {
2653
+ throw new NoScorableCandidateError(`no scorable candidate for '${symbol}': ${why}`, dropped, withheld, [], {
2244
2654
  cause: lastScoreErr,
2245
2655
  });
2246
2656
  }
@@ -2248,15 +2658,37 @@ export function rankBy<S extends { score: number; rows?: number }>(
2248
2658
  return { winner: results[0], candidates: results.map(({ order: _order, ...c }) => c), dropped, withheld };
2249
2659
  }
2250
2660
 
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.
2661
+ /** The sentence a stillborn fan's error OPENS on: how much of the fan was never compiled, on what
2662
+ * evidence, and the errors each half's compiled candidates kept. The DEFAULT candidate's own
2663
+ * diagnostic — the spelling the rule is anchored on, rather than whichever was refused last —
2664
+ * follows it, so that a printer bounding a long diagnostic keeps the verdict. */
2665
+ function stillbornNote(stillborn: Stillborn, fan: number): string {
2666
+ const tally = new Map<string, number>();
2667
+ for (const m of stillborn.messages) {
2668
+ tally.set(m, (tally.get(m) ?? 0) + 1);
2669
+ }
2670
+ const shared = [...tally].map(([m, n]) => ` ${m}${n > 1 ? ` (x${n})` : ''}`).join('\n');
2671
+ return (
2672
+ `${stillborn.notCompiled.length} of ${fan} candidates were NOT COMPILED: in each signedness half, the default ` +
2673
+ `candidate and one probe per variation (${stillborn.compiled.length} compiled) were all rejected, every probe ` +
2674
+ `keeping one of its default's errors:\n${shared}`
2675
+ );
2676
+ }
2677
+
2678
+ /** THE candidate ordering — score, then semantics, then preference, then readability, then enumeration
2679
+ * order. Exported because it is asked outside `rankBy` too — the benchmark's sweep driver and
2680
+ * fan runner order what they read back — and a second copy would let the same input produce two
2681
+ * different winners.
2255
2682
  *
2256
2683
  * SCORE dominates absolutely: the differ is the fitness function, and a tie means the variation
2257
2684
  * that separates these two candidates did not change the bytes — so everything below only chooses what
2258
2685
  * the READER sees, and can never cost a match.
2259
2686
  *
2687
+ * A DISCARDED PASSED RESULT next (`Candidate.discardsPassedResult`): equal bytes refute neither
2688
+ * reading, so the one that drops a value the callee may read wins only where it scores strictly
2689
+ * better. A GUESSED PARAMETER WIDTH (`Candidate.guessesParamWidth`) likewise: a narrow declaration
2690
+ * no prototype supplied changes its callers' bytes, which no score here reads.
2691
+ *
2260
2692
  * PREFERENCE next: a named symbol-map spelling beats its `/raw-globals` sibling at equal bytes.
2261
2693
  *
2262
2694
  * DEVICE VOLATILITY next: at equal bytes, the spelling that qualifies a DEVICE REGISTER
@@ -2274,7 +2706,14 @@ export function rankBy<S extends { score: number; rows?: number }>(
2274
2706
  * with no score and no outcome moving. That is a judgement about the source rather than a
2275
2707
  * measurement of it — the differ never refereed those six — and it is the same judgement this
2276
2708
  * 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.
2709
+ * candidates exist; that stays an admission question, one variation at a time. The CAST COUNT
2710
+ * below does the same for `/narrow-decl`, which drops one cast per local it narrows and so wins
2711
+ * every tie against the candidate it is derived onto.
2712
+ *
2713
+ * A FAR STORE SPELLED THROUGH A CAST (`Candidate.farStoreCast`) next: where `/far-cast` ties the
2714
+ * member spelling, the compiler built the member's object too, and the member names the field. Its
2715
+ * `(u8 *)` base is no scalar cast and its line count is no higher, so neither term below would
2716
+ * prefer the member on its own.
2278
2717
  *
2279
2718
  * CAST COUNT next, and only WITHIN a preference. A wrong signedness pin is what manufactures casts —
2280
2719
  * the C backend has to cast a shift operand back to the signedness the machine op needs, so
@@ -2293,20 +2732,22 @@ export function rankBy<S extends { score: number; rows?: number }>(
2293
2732
  * result deterministic. Spelled explicitly rather than leaning on Array#sort's stability, which
2294
2733
  * would make each preference an accident of two unrelated decisions.
2295
2734
  *
2296
- * WHAT NO TERM HERE WEIGHS: a comparison's rendered SIGNEDNESS. `/uns-cmp`'s whole output is
2297
- * that polarity, and it carries in a DECLARED TYPE rather than a cast — `castCount` reads 0 on
2298
- * both sides of the tie it loses — so at equal bytes enumeration order decides, and a rival
2299
- * variation's candidate can publish a signed compare where the asm's is unsigned. Weighing it needs
2300
- * the candidate's own SFn and the icmp facts it was structured from, neither of which this
2301
- * comparator carries; `deviceVolatile` is the shape such a term would take. */
2735
+ * WHAT NO TERM HERE WEIGHS: a comparison's rendered SIGNEDNESS. It needs none: every tree a
2736
+ * source is emitted from is judged for an unsigned compare that renders signed
2737
+ * (`Candidate.signedUnsignedCompare`), whichever variation spelled it so, and such a source is
2738
+ * `matchOnly` — on a nonmatch it is withheld before this comparator sees it, whatever it scores,
2739
+ * and on a match both spellings are the program. */
2302
2740
  export function compareScored<S extends { score: number }>(
2303
2741
  a: Candidate & { score: S; order: number },
2304
2742
  b: Candidate & { score: S; order: number },
2305
2743
  ): number {
2306
2744
  return (
2307
2745
  a.score.score - b.score.score ||
2746
+ (a.discardsPassedResult ? 1 : 0) - (b.discardsPassedResult ? 1 : 0) ||
2747
+ (a.guessesParamWidth ? 1 : 0) - (b.guessesParamWidth ? 1 : 0) ||
2308
2748
  a.preference - b.preference ||
2309
2749
  (b.deviceVolatile ?? 0) - (a.deviceVolatile ?? 0) ||
2750
+ (a.farStoreCast ? 1 : 0) - (b.farStoreCast ? 1 : 0) ||
2310
2751
  castCount(a.source) - castCount(b.source) ||
2311
2752
  lineCount(a.source) - lineCount(b.source) ||
2312
2753
  a.order - b.order