@asmlift/core 0.9.0 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (117) hide show
  1. package/README.md +25 -24
  2. package/package.json +1 -1
  3. package/src/aggregate.ts +149 -0
  4. package/src/backend/c.ts +2 -0
  5. package/src/backend/cfamily.ts +197 -61
  6. package/src/backend/cpp.ts +3 -1
  7. package/src/backend/lines.ts +10 -0
  8. package/src/backend/pascal.ts +47 -16
  9. package/src/cdecl/parse.ts +21 -0
  10. package/src/contracts.ts +246 -36
  11. package/src/declare.ts +69 -20
  12. package/src/frontend/arm-body.ts +155 -0
  13. package/src/frontend/asmdata.ts +9 -2
  14. package/src/frontend/call-plan.ts +1117 -61
  15. package/src/frontend/callee-body.ts +370 -0
  16. package/src/frontend/dead-reads.ts +628 -0
  17. package/src/frontend/device-pins.ts +33 -4
  18. package/src/frontend/emit.ts +4 -3
  19. package/src/frontend/fpu.ts +224 -17
  20. package/src/frontend/frame-objects.ts +1689 -206
  21. package/src/frontend/frontend.ts +84 -3
  22. package/src/frontend/high-half.ts +53 -4
  23. package/src/frontend/jump-table.ts +545 -0
  24. package/src/frontend/local-object.ts +158 -32
  25. package/src/frontend/mips.ts +2296 -217
  26. package/src/frontend/own-struct-return.ts +500 -0
  27. package/src/frontend/ppc.ts +2748 -245
  28. package/src/frontend/register-struct-call.ts +399 -0
  29. package/src/frontend/reloc-symbol.ts +22 -7
  30. package/src/frontend/ssa.ts +1721 -43
  31. package/src/frontend/stackargs.ts +199 -72
  32. package/src/frontend/thumb-entry.ts +340 -0
  33. package/src/frontend/thumb.ts +2400 -325
  34. package/src/ir/alias.ts +65 -2
  35. package/src/ir/bits.ts +1699 -4
  36. package/src/ir/core.ts +254 -9
  37. package/src/ir/discipline.ts +86 -21
  38. package/src/ir/entry.ts +157 -0
  39. package/src/{structure → ir}/loops.ts +6 -2
  40. package/src/ir/opcodes.ts +137 -9
  41. package/src/ir/parse.ts +1 -0
  42. package/src/ir/simplify.ts +61 -10
  43. package/src/ir/types.ts +86 -2
  44. package/src/ir/verify.ts +96 -11
  45. package/src/l3/argbase.ts +2 -0
  46. package/src/l3/ast.ts +582 -18
  47. package/src/l3/basecse.ts +68 -15
  48. package/src/l3/caseendgoto.ts +223 -0
  49. package/src/l3/coalesce.ts +42 -14
  50. package/src/l3/constinit.ts +410 -0
  51. package/src/l3/dce.ts +53 -9
  52. package/src/l3/initfirst.ts +42 -11
  53. package/src/l3/intcell.ts +2 -1
  54. package/src/l3/narrowdecl.ts +77 -36
  55. package/src/l3/pollguard.ts +8 -2
  56. package/src/l3/ptrcell.ts +33 -34
  57. package/src/l3/ptrfield.ts +123 -8
  58. package/src/l3/regspell.ts +11 -4
  59. package/src/l3/reindex.ts +295 -99
  60. package/src/l3/scopebase.ts +2 -0
  61. package/src/l3/storage.ts +98 -1
  62. package/src/l3/symbol-refs.ts +84 -6
  63. package/src/l3/tailmerge.ts +6 -2
  64. package/src/l3/typing.ts +108 -15
  65. package/src/l3/unreduce.ts +6 -5
  66. package/src/l3/volatileptr.ts +17 -54
  67. package/src/mangle.ts +30 -0
  68. package/src/pattern/engine.ts +361 -73
  69. package/src/pipeline.ts +1039 -38
  70. package/src/proto-context.ts +1039 -45
  71. package/src/proto.ts +555 -40
  72. package/src/raise/aggmembers.ts +290 -0
  73. package/src/raise/arrays.ts +5 -1
  74. package/src/raise/const.ts +25 -0
  75. package/src/raise/declared-volatile.ts +207 -31
  76. package/src/raise/divpow2.ts +192 -0
  77. package/src/raise/floathelpers.ts +254 -33
  78. package/src/raise/gvn.ts +3 -3
  79. package/src/raise/latch.ts +162 -6
  80. package/src/raise/memberarrays.ts +10 -4
  81. package/src/raise/offsetnames.ts +422 -36
  82. package/src/raise/pairparams.ts +33 -0
  83. package/src/raise/paramwidth.ts +196 -6
  84. package/src/raise/pre-recovery.ts +107 -21
  85. package/src/raise/pre-structure.ts +73 -0
  86. package/src/raise/recover.ts +12 -5
  87. package/src/raise/reroll.ts +674 -0
  88. package/src/raise/shortcircuit.ts +217 -34
  89. package/src/raise/softdiv.ts +1 -1
  90. package/src/raise/storesign.ts +153 -0
  91. package/src/raise/struct-arrays.ts +30 -11
  92. package/src/raise/structs.ts +396 -33
  93. package/src/raise/truncload.ts +4 -3
  94. package/src/raise/unpre.ts +594 -0
  95. package/src/raise/unthread.ts +400 -0
  96. package/src/rank-declare.ts +118 -12
  97. package/src/rank-variations.ts +186 -35
  98. package/src/rank.ts +536 -68
  99. package/src/runtime-helpers.ts +140 -22
  100. package/src/structure/analysis.ts +217 -8
  101. package/src/structure/arith-ops.ts +23 -0
  102. package/src/structure/cross-jumped-tail.ts +216 -0
  103. package/src/structure/globalaccess.ts +157 -10
  104. package/src/structure/hazards.ts +16 -64
  105. package/src/structure/local-statics.ts +145 -9
  106. package/src/structure/namecoalesce.ts +1 -1
  107. package/src/structure/pointer-spelling.ts +1106 -0
  108. package/src/structure/redundant-test.ts +1 -1
  109. package/src/structure/structure.ts +6192 -1213
  110. package/src/structure/switch-recover.ts +583 -120
  111. package/src/structure/unfolded-branch-constant.ts +90 -0
  112. package/src/symbols.ts +58 -15
  113. package/src/target.ts +1028 -54
  114. package/src/trace.ts +49 -17
  115. package/src/variation-definitions.ts +333 -20
  116. package/src/variation-gates.ts +4 -0
  117. package/src/variation-tokens.ts +68 -3
package/src/rank.ts CHANGED
@@ -20,7 +20,11 @@
20
20
  // names, and which of those names a declaration must refuse to claim.
21
21
  import { cBackend } from './backend/c';
22
22
  import {
23
+ assertCaseGotosNarrow,
23
24
  assertDerefsTyped,
25
+ assertEffectsPreserved,
26
+ assertFrameUnionsViewed,
27
+ assertLocalStorageStaysInteger,
24
28
  assertLocalsWritten,
25
29
  assertNoOrphanedLocals,
26
30
  assertPlacementSurvives,
@@ -28,6 +32,7 @@ import {
28
32
  assertResolved,
29
33
  } from './contracts';
30
34
  import type { AsmData } from './frontend/asmdata';
35
+ import type { CalleeBodies } from './frontend/callee-body';
31
36
  import { hasSetupArgsNarrowing, narrowToSetupArgs, setupArgsDiscardsPassedResult } from './frontend/ssa';
32
37
  import { Fn, Value, defOpMap } from './ir/core';
33
38
  import { T } from './ir/types';
@@ -53,14 +58,24 @@ import { reindexWalks } from './l3/reindex';
53
58
  import { hoistScopedBases } from './l3/scopebase';
54
59
  import { sinkInitsToFirstUse } from './l3/sinkinit';
55
60
  import type { SymbolRef } from './l3/symbol-refs';
61
+ import { signedOverUnsignedCompare } from './l3/typing';
56
62
  import { type UnreduceResult, unreduceAccumulators } from './l3/unreduce';
57
63
  import { deviceVolatileClaims, volatilePtrLocals, volatileSubsetCandidates } from './l3/volatileptr';
58
64
  import { volatileValueLocals } from './l3/volatileval';
59
65
  import { volatileDeviceStores } from './l3/volstore';
60
66
  import { zeroSubNegates } from './l3/zerosub';
61
67
  import { RewritePattern } from './pattern/engine';
62
- import { applyIdiomPatterns, liftStamped, raiseRecovered, structureChecked } from './pipeline';
63
- import { type Prototypes, declaresVoidReturn, prototypesFromSymbols } from './proto';
68
+ import {
69
+ applyIdiomPatterns,
70
+ liftStamped,
71
+ offsetNameBehaviorsFor,
72
+ raiseRecovered,
73
+ sourceLineCount,
74
+ structureChecked,
75
+ structureReturnTailReading,
76
+ } from './pipeline';
77
+ import { type FnProto, type Prototypes, declaresVoidReturn, prototypesFromSymbols } from './proto';
78
+ import { type ContextGlobals, splitOwnContext } from './proto-context';
64
79
  import { inferGlobalArrays, orderLicensedGlobals, sameDerivedShape } from './raise/globalshape';
65
80
  import { OFFSET_NAME_PASS } from './raise/offsetnames';
66
81
  import { runPreRecovery } from './raise/pre-recovery';
@@ -70,6 +85,7 @@ import {
70
85
  type RefusedDeclarationReason,
71
86
  bareGlobalAccessFacts,
72
87
  bareGlobalSymbols,
88
+ initAddressSymbols,
73
89
  makeRefCollector,
74
90
  } from './rank-declare';
75
91
  import {
@@ -82,14 +98,22 @@ import {
82
98
  SIGNEDNESS,
83
99
  STACKED_SUBSETS,
84
100
  STRUCTURE_VARIATIONS,
101
+ StackedMemberThrew,
85
102
  type StructureVariation,
86
103
  UNFOLDED_HOISTS,
87
104
  applyStacked,
88
105
  createdLocals,
106
+ firstFired,
89
107
  sameBases,
90
108
  } from './rank-variations';
91
109
  import { type Stillborn, stillbornVerdict } from './stillborn';
92
- import { hasDivergentSharedRet } from './structure/structure';
110
+ import {
111
+ DefaultAcceptsMemo,
112
+ DefaultAcceptsMemoMisuse,
113
+ FOR_BOUND_GATES,
114
+ StructureError,
115
+ hasDivergentSharedRet,
116
+ } from './structure/structure';
93
117
  import {
94
118
  type SymbolInfo,
95
119
  type SymbolMap,
@@ -159,6 +183,12 @@ export interface EnumerateOptions {
159
183
  asmData?: AsmData;
160
184
  /** address→symbol map (symbols.ts) — same contract as DecompileOptions.symbols */
161
185
  symbols?: SymbolMap;
186
+ /** same contract as DecompileOptions.contextGlobals */
187
+ contextGlobals?: ContextGlobals;
188
+ /** same contract as DecompileOptions.ownContextProto */
189
+ ownContextProto?: FnProto;
190
+ /** same contract as DecompileOptions.calleeBodies */
191
+ calleeBodies?: CalleeBodies;
162
192
  /** Called when a respell variation THROWS or fails a boundary contract, so the failure is visible
163
193
  * instead of the candidate silently not existing. Enumeration continues either way — the default
164
194
  * candidate is unaffected — but a variation that never fires because it always throws is a defect,
@@ -255,16 +285,41 @@ export interface Candidate {
255
285
  * • `l3/intcell.ts` spells a global as the integer a project may have declared it, where a
256
286
  * pointer declaration scales the offset it adds, and no declaration is in the asm.
257
287
  *
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
288
+ * A third obligation is judged on the finished tree rather than declared by a variation
289
+ * (`signedUnsignedCompare` below):
290
+ *
291
+ * • `/signed-cmp`, or a respell over its tree, spells an unsigned compare over signed-rendering
292
+ * operands, which compiles to a signed compare unless the operands are non-negative, and the
293
+ * compiler's proof of that is not in the asm.
294
+ *
295
+ * What settles all three instead is the object: a candidate that assembles to the target's own
296
+ * bytes IS the program, whatever a gate could have proved. So the spelling is offered, scored, and then
260
297
  * either wins on proof or is WITHHELD — never shown as a best-effort answer. Both ranking drivers
261
298
  * ask `withheldReason`, so neither can publish what the other would not. */
262
299
  matchOnly?: true;
300
+ /** The tree this source was emitted from spells some unsigned compare so that C evaluates it
301
+ * signed (l3/typing.ts `signedOverUnsignedCompare`, judged on the finished tree), which makes it
302
+ * `matchOnly` and names the fact it owes. Set for whichever variation got it there: emission
303
+ * under `/signed-cmp`, or a respell that substituted into a compare emission had pinned. It is
304
+ * the only place this obligation is decided: a pass that rebuilds a compare spreads the node to
305
+ * keep its `unsignedCmp` evidence (l3/regspell.ts), a merge that keeps one of two equal nodes
306
+ * keeps the evidence of both (l3/ast.ts `unionEvidence`), or drops it only where its own gate proved
307
+ * the new spelling compares the same values (l3/initfirst.ts `swapSide`, l3/reindex.ts). */
308
+ signedUnsignedCompare?: true;
263
309
  /** This candidate's lift is `/setup-args` and drops an earlier callee's result that a call through
264
310
  * a register is passed (frontend/ssa.ts `setupArgsDiscardsPassedResult`). It loses every score
265
311
  * tie (compareScored): a tie refutes neither reading, and a shorter spelling must not win by
266
312
  * dropping a value the callee may read. */
267
313
  discardsPassedResult?: true;
314
+ /** This candidate's lift is `/narrow-param` and declares a parameter narrow that no prototype
315
+ * declares (raise/pre-recovery.ts `onUndeclaredWidth`). It loses every score tie
316
+ * (compareScored): a tie refutes neither declaration, and the narrow one changes the bytes of
317
+ * every prototyped caller, which this function's score cannot see. */
318
+ guessesParamWidth?: true;
319
+ /** This candidate's lift is `/far-cast` and spells a far member store through a cast of its
320
+ * base. It loses every score tie (compareScored): a tie means the compiler built the member's
321
+ * object too, and the member is the spelling that names the field. */
322
+ farStoreCast?: true;
268
323
  }
269
324
  /** A candidate paired with its score `S` (the injected scorer's result shape — must carry `.score`). */
270
325
  export interface Scored<S> extends Candidate {
@@ -356,7 +411,7 @@ export interface RankedResult<S> {
356
411
  candidates: Scored<S>[]; // sorted best (lowest) first
357
412
  /** candidates whose scoreFn threw — empty when every spelling built */
358
413
  dropped: DroppedCandidate[];
359
- /** candidates withheld for want of a byte-exact proof — empty unless a `matchOnly` variation fired */
414
+ /** candidates withheld for want of a byte-exact proof — empty unless a `matchOnly` candidate scored a nonmatch */
360
415
  withheld: WithheldCandidate[];
361
416
  }
362
417
 
@@ -371,6 +426,9 @@ export function withheldReason<S extends { score: number }>(c: Candidate, score:
371
426
  return null;
372
427
  }
373
428
  const facts = c.variations.flatMap((v) => PROOF_OBLIGATIONS.get(v) ?? []);
429
+ if (c.signedUnsignedCompare === true) {
430
+ facts.push(SIGNED_COMPARE_OBLIGATION);
431
+ }
374
432
  const fact = facts.length > 0 ? facts.join(' and on ') : 'a fact no gate over the C can settle';
375
433
  return `this spelling rests on ${fact}; only a byte-exact score proves it`;
376
434
  }
@@ -381,6 +439,11 @@ const PROOF_OBLIGATIONS: ReadonlyMap<string, string> = new Map<VariationName, st
381
439
  ['int-cell', 'the project declaring the global an integer, which the assembly does not show'],
382
440
  ]);
383
441
 
442
+ /** The fact `Candidate.signedUnsignedCompare` rests on. Keyed by the flag, not by `/signed-cmp`:
443
+ * that name is on candidates that owe nothing (a compare over a `u32` operand) and missing from
444
+ * respells that do. */
445
+ const SIGNED_COMPARE_OBLIGATION = "the compiler having proved every unsigned compare's operands non-negative";
446
+
384
447
  /** What a respell variation hands `respell`: its tree, or — when the variation cannot establish the
385
448
  * candidate's semantics from inside the pass — the tree paired with that fact. `undefined`/`null`
386
449
  * is a decline. */
@@ -444,6 +507,29 @@ interface TreeSource {
444
507
  deviceVolatile?: number;
445
508
  /** see `Candidate.matchOnly` — set by a variation that cannot establish its own semantics */
446
509
  matchOnly?: true;
510
+ /** see `Candidate.signedUnsignedCompare` */
511
+ signedUnsignedCompare?: true;
512
+ }
513
+
514
+ /** Did `bound-plus-one-counter-wraps` CAUSE this structuring's decline? The rule fires from every
515
+ * attempt `forPlanAt` makes, committed or not, and on loops the thrown refusal never reaches
516
+ * (`synthetic:sw_gotocase_inner:ido7.1` fires it and declines on its switches' cases), so its firing
517
+ * alone says nothing about the decline. The question is asked the one way that answers it: the
518
+ * same structuring with that rule off. Where that one structures, the refusal was the rule's, and
519
+ * the other pin, whose counter the rule admits, can take the loop; where it still throws, the
520
+ * decline is the function's and the row aborts as any other default decline does. The tree the
521
+ * ablated run builds is discarded — it is the `for` the rule refuses as wrong under this pin.
522
+ * Not holding every pinnable decline instead: that would let the signed pin past any structure
523
+ * refusal that reads a declared type, not only this one, and re-lift every declined row. */
524
+ function declinesOnCounterSign(fn: Fn, opts: Parameters<typeof structureChecked>[1]): boolean {
525
+ try {
526
+ structureChecked(fn, opts, {
527
+ forBoundGates: FOR_BOUND_GATES.filter((g) => g.id !== 'bound-plus-one-counter-wraps'),
528
+ });
529
+ return true;
530
+ } catch {
531
+ return false;
532
+ }
447
533
  }
448
534
 
449
535
  /** Emit the DISTINCT type/branch-sense candidates for `name` — PURE, no scoring.
@@ -469,16 +555,31 @@ export function enumerateCandidates(
469
555
  /** The last refusal from a backend asked to spell a tree — what the empty-enumeration check
470
556
  * below reports, so "this backend can spell nothing here" names its reason. */
471
557
  let lastEmitError: unknown = null;
558
+ /** THE UNSIGNED PIN'S DECLINE, HELD FOR THE SIGNED ONE. The default candidate's structuring aborts
559
+ * the row (`abortsRow`) on the premise that a structure refusal does not depend on the pin. One
560
+ * does: `forPlanAt`'s `bound-plus-one-counter-wraps` reads the counter's declared type, so IDO's
561
+ * counted loop (`synthetic:sw_gotocase2:ido7.1`) takes its `for` under the `s32` pin and not the
562
+ * `u32` one, and without the `for` the switch's loop exit cannot be spelled at all. The decline is
563
+ * held only where that rule CAUSED it (`declinesOnCounterSign`), and then the rest of the unsigned
564
+ * pin is skipped: its other settings would run behind a declined default, which `abortsRow` exists
565
+ * to forbid. The signed pin runs as it would have; a row that then produces no candidate declines
566
+ * with the held error. Nothing the signed pin builds is new: it is the same pass, under its own
567
+ * default setting and its own gates. */
568
+ let heldPinDecline: unknown = undefined;
472
569
  // Same merge as `decompile`: the project's DWARF signatures fill in what the caller did not
473
570
  // state, so both the annotate pass and the ranked candidates reason about one prototype table.
474
- const prototypes = prototypesFromSymbols(opts.symbols, opts.prototypes ?? {});
571
+ const ownContext = splitOwnContext(opts.prototypes, name, opts.ownContextProto);
572
+ const prototypes = prototypesFromSymbols(opts.symbols, ownContext.prototypes ?? {});
475
573
  const baseOpts = {
476
574
  ...structureOptionsFor(target, declaresVoidReturn(prototypes[name]), prototypes),
477
575
  // See the same line in pipeline.ts: a backend that cannot print switch fall-through must not
478
576
  // be handed a tree carrying one, because its refusal costs the whole candidate (and, when
479
577
  // every candidate carries it, the whole row).
480
578
  spellSwitchFallthrough: backend.spellsSwitchFallthrough,
579
+ spellSwitchBreak: backend.spellsSwitchBreak,
580
+ spellEarlyReturns: backend.spellsEarlyReturn,
481
581
  ...(opts.symbols ? { symbols: symbolsByName(opts.symbols) } : {}),
582
+ ...(opts.contextGlobals ? { contextGlobals: opts.contextGlobals } : {}),
482
583
  };
483
584
  // Branch-sense is a differ-ranked VARIATION, the same class as param signedness: a divergent `if`
484
585
  // can be spelled with either sense (`if (c) A else B` vs `if (!c) B else A`), and which one the
@@ -600,11 +701,23 @@ export function enumerateCandidates(
600
701
  // it buys is that a lift-time change which splits them enumerates both settings rather than
601
702
  // silently dropping one, the failure nothing reports.
602
703
  let sharedLiftTreeOwned = false;
704
+ let sharedLiftExtendedReaders = false;
705
+ let sharedLiftFoldedStore = false;
603
706
  // Shared lift: recover ONCE with no signedness pin, to learn which entry params are
604
707
  // pointers/aggregates so they are excluded from the signedness variation (see NO_PIN_KINDS). One
605
708
  // extra lift+recover, no compile. (The shared lift deliberately stops after recoverTypes — it only reads the param KINDS, so
606
709
  // the totality contract / return-sinking of the full spine are not run on it.)
607
- const sharedLift = liftStamped(name, asm, target, prototypes, opts.asmData, opts.symbols);
710
+ const sharedLift = liftStamped(
711
+ name,
712
+ asm,
713
+ target,
714
+ prototypes,
715
+ opts.asmData,
716
+ opts.symbols,
717
+ opts.contextGlobals,
718
+ { calleeBodies: opts.calleeBodies },
719
+ ownContext.ownContextProto,
720
+ );
608
721
  // The ARRAY SHAPES the input assembly evidences (raise/globalshape.ts), for the DECLARATION
609
722
  // half. Read off the shared lift's LIFTED form — before the fold below and the tower rewrite it —
610
723
  // because the base-materialization order the derivation's licence reads does not survive them.
@@ -619,6 +732,16 @@ export function enumerateCandidates(
619
732
  sharedLiftTreeOwned = true;
620
733
  },
621
734
  },
735
+ paramWidth: {
736
+ onExtendedReaders: () => {
737
+ sharedLiftExtendedReaders = true;
738
+ },
739
+ },
740
+ displaced: {
741
+ onFoldedStore: () => {
742
+ sharedLiftFoldedStore = true;
743
+ },
744
+ },
622
745
  });
623
746
  recoverTypes(sharedLift);
624
747
  const ptrIdx = new Set<number>(
@@ -643,17 +766,47 @@ export function enumerateCandidates(
643
766
  // shared lift's `onTreeOwned` hook has had its chance to fire. Called any earlier it would report a confident
644
767
  // `false` for a function that owns a tree. As a `const` below that call, an early call is a TDZ
645
768
  // ReferenceError instead — a wrong answer traded for a loud one.
646
- const treeOwnedIn = (symbols: typeof opts.symbols): boolean => {
769
+ //
770
+ // `/narrow-param`'s gate is read off the same lift the same way: the width pass reports a
771
+ // parameter the variation's lift would narrow (raise/pre-recovery.ts `ParamWidthOptions`). So is
772
+ // `/far-cast`'s: the displaced-access fold reports a store it moved into a struct.
773
+ const liftReportsIn = (
774
+ symbols: typeof opts.symbols,
775
+ ): { treeOwned: boolean; extendedReaders: boolean; foldedStore: boolean } => {
647
776
  if (symbols === opts.symbols) {
648
- return sharedLiftTreeOwned;
777
+ return {
778
+ treeOwned: sharedLiftTreeOwned,
779
+ extendedReaders: sharedLiftExtendedReaders,
780
+ foldedStore: sharedLiftFoldedStore,
781
+ };
649
782
  }
650
- const p = liftStamped(name, asm, target, prototypes, opts.asmData, symbols);
783
+ const p = liftStamped(
784
+ name,
785
+ asm,
786
+ target,
787
+ prototypes,
788
+ opts.asmData,
789
+ symbols,
790
+ opts.contextGlobals,
791
+ { calleeBodies: opts.calleeBodies },
792
+ ownContext.ownContextProto,
793
+ );
651
794
  applyIdiomPatterns(p, target, opts.patterns);
652
- let seen = false;
795
+ const seen = { treeOwned: false, extendedReaders: false, foldedStore: false };
653
796
  runPreRecovery(p, target, () => verify(p), prototypes[name], {
654
797
  shortCircuit: {
655
798
  onTreeOwned: () => {
656
- seen = true;
799
+ seen.treeOwned = true;
800
+ },
801
+ },
802
+ paramWidth: {
803
+ onExtendedReaders: () => {
804
+ seen.extendedReaders = true;
805
+ },
806
+ },
807
+ displaced: {
808
+ onFoldedStore: () => {
809
+ seen.foldedStore = true;
657
810
  },
658
811
  },
659
812
  });
@@ -916,7 +1069,11 @@ export function enumerateCandidates(
916
1069
  // spelling needs one, and the declaration a candidate spelling `gTbl[i]` cannot compile
917
1070
  // without), and the project map, which knows more than either.
918
1071
  const mapSymbols = baseOpts.symbols;
919
- const targetSymbols = new Map<string, SymbolInfo>([...bareGlobalSymbols(sharedLift), ...sharedLiftShapes]);
1072
+ const targetSymbols = new Map<string, SymbolInfo>([
1073
+ ...initAddressSymbols(sharedLift),
1074
+ ...bareGlobalSymbols(sharedLift),
1075
+ ...sharedLiftShapes,
1076
+ ]);
920
1077
  const declSymbols = new Map<string, SymbolInfo>([...targetSymbols, ...(mapSymbols ?? [])]);
921
1078
  // The four per-enumeration constants named at the seam rather than captured across 60 lines of
922
1079
  // closure (rank-declare.ts states why they belong on one object).
@@ -930,6 +1087,7 @@ export function enumerateCandidates(
930
1087
  prototypes,
931
1088
  target,
932
1089
  refuse,
1090
+ own: Object.hasOwn(prototypes, name) ? prototypes[name] : ownContext.ownContextProto,
933
1091
  });
934
1092
  // THE RESPELL SET, as a function whose PARAMETER LIST is the invariant the tree skip below
935
1093
  // rests on: every source here is a pure function of the structured tree and this call's own
@@ -953,6 +1111,14 @@ export function enumerateCandidates(
953
1111
  // of `/volatile` — a candidate that did not fail and is still in the fan. The order
954
1112
  // is the candidates' own (`[pf.name, ...sp.variations]`), so a reported name and an enumerated
955
1113
  // candidate's variations name the same candidate the same way.
1114
+ //
1115
+ // THE COMPARE-SIGNEDNESS PROOF, asked of every tree a source is emitted from rather than of the
1116
+ // passes that build them. Emission pins each unsigned compare it lowers (structure.ts), but a
1117
+ // respell that substitutes into a compare — `/unmerge` putting an arm's `s16` global where the
1118
+ // pinned `u32` local stood, against an `s32` parameter — re-renders it signed. So the finished
1119
+ // tree is judged here, and a signed rendering of an `icmp_u*` is `matchOnly` whoever produced it.
1120
+ const cmpProof = (tree: SFn): { matchOnly?: true; signedUnsignedCompare?: true } =>
1121
+ signedOverUnsignedCompare(tree) ? { matchOnly: true, signedUnsignedCompare: true } : {};
956
1122
  const respellTree = (sfn: SFn, preRespellVariations: readonly Variation[] = []): TreeSources => {
957
1123
  // The walk→index respell variation (l3/reindex.ts) is a THIRD variation on the same footing as
958
1124
  // signedness and branch sense: whether the source spelled `*p; p++` or `arr[i]` is
@@ -970,7 +1136,7 @@ export function enumerateCandidates(
970
1136
  // Pascal backend loud-declines). Refusing EVERY tree is still loud — the empty-enumeration
971
1137
  // check at the end raises the last refusal.
972
1138
  try {
973
- sources.push({ variations: [], source: backend.emit(sfn), ...refsOf(sfn), ...volOf(sfn) });
1139
+ sources.push({ variations: [], source: backend.emit(sfn), ...refsOf(sfn), ...volOf(sfn), ...cmpProof(sfn) });
974
1140
  } catch (e) {
975
1141
  reportThrow(preRespellVariations, e);
976
1142
  return { sources, emit: { error: e } };
@@ -1050,6 +1216,7 @@ export function enumerateCandidates(
1050
1216
  // `assertEffectsPreserved` is the fourth and is NOT here: it needs the L1 `fn`, and
1051
1217
  // `respellTree`'s parameter list is the invariant the tree-dedup skip rests on (see its header).
1052
1218
  // Widening it for a contract is a defensible change and an argued one — not a silent import.
1219
+ // The PRE-RESPELL loop below has `fn` in scope and no such invariant, so it runs it there.
1053
1220
  // A respell variation returns its tree, or `{ sfn, needsProof }` when it cannot establish its own
1054
1221
  // semantics from inside the pass (Candidate.matchOnly carries the argument).
1055
1222
  const respell = (variations: readonly Variation[], make: () => RespellResult, alreadyShaped = false): void => {
@@ -1068,7 +1235,17 @@ export function enumerateCandidates(
1068
1235
  assertLocalsWritten(alt);
1069
1236
  assertPostIncrUnshared(alt);
1070
1237
  assertNoOrphanedLocals(sfn, alt);
1071
- sources.push({ variations, source: backend.emit(alt), ...refsOf(alt), ...volOf(alt), ...proof });
1238
+ assertLocalStorageStaysInteger(sfn, alt);
1239
+ assertFrameUnionsViewed(alt);
1240
+ assertCaseGotosNarrow(alt);
1241
+ sources.push({
1242
+ variations,
1243
+ source: backend.emit(alt),
1244
+ ...refsOf(alt),
1245
+ ...volOf(alt),
1246
+ ...proof,
1247
+ ...cmpProof(alt),
1248
+ });
1072
1249
  // STACKED variations, derived onto EVERY source (the POLICY note above carries the
1073
1250
  // admission argument). Each is a statement-order/shape or declaration-width fact
1074
1251
  // orthogonal to the respell variations outside the table; subsets compose in the fixed
@@ -1095,19 +1272,25 @@ export function enumerateCandidates(
1095
1272
  // which moves only const or pure-read assigns and so cannot lift a read of a base local
1096
1273
  // above its init.
1097
1274
  const minted = createdLocals(sfn, alt);
1275
+ const firedSets = new Set<string>();
1098
1276
  for (const subset of STACKED_SUBSETS) {
1099
1277
  // ONE TRY PER SHAPE — a shape is its own candidate and fails as its own candidate.
1100
1278
  // Sharing the respell variation's outer try would let a throw deriving one subset
1101
1279
  // discard every later one, under that variation's name, which names no shape.
1102
- const shapeVariations = subset.map((x) => x.name);
1280
+ // A throw past applyStacked is reported under the fired set, the candidate's own name.
1281
+ let shapeVariations: readonly Variation[] = [];
1103
1282
  try {
1104
1283
  const shaped = applyStacked(subset, alt);
1105
- if (shaped !== null) {
1284
+ if (shaped !== null && firstFired(firedSets, shaped.variations)) {
1285
+ shapeVariations = shaped.variations;
1106
1286
  assertResolved(shaped.out);
1107
1287
  assertDerefsTyped(shaped.out);
1108
1288
  assertLocalsWritten(shaped.out);
1109
1289
  assertPostIncrUnshared(shaped.out);
1110
1290
  assertNoOrphanedLocals(alt, shaped.out);
1291
+ assertLocalStorageStaysInteger(alt, shaped.out);
1292
+ assertFrameUnionsViewed(shaped.out);
1293
+ assertCaseGotosNarrow(shaped.out);
1111
1294
  assertPlacementSurvives(alt, shaped.out, minted);
1112
1295
  sources.push({
1113
1296
  variations: [...variations, ...shaped.variations],
@@ -1116,10 +1299,12 @@ export function enumerateCandidates(
1116
1299
  ...volOf(shaped.out),
1117
1300
  // a shape derived from a proof-gated spelling inherits the requirement
1118
1301
  ...proof,
1302
+ ...cmpProof(shaped.out),
1119
1303
  });
1120
1304
  }
1121
1305
  } catch (e) {
1122
- reportThrow([...preRespellVariations, ...variations, ...shapeVariations], e);
1306
+ const step = e instanceof StackedMemberThrew ? e.variations : shapeVariations;
1307
+ reportThrow([...preRespellVariations, ...variations, ...step], e);
1123
1308
  }
1124
1309
  }
1125
1310
  }
@@ -1135,19 +1320,21 @@ export function enumerateCandidates(
1135
1320
  // `/argbase` — name a call's argument bases before the call (l3/argbase.ts). A variation on the
1136
1321
  // same footing as the others: the default inline spelling stays in the list, so the differ
1137
1322
  // referees and this can never cost a match.
1323
+ const firedSets = new Set<string>();
1138
1324
  for (const subset of STACKED_SUBSETS) {
1139
1325
  // the truthful variations need the pass to RUN first, so this bypasses respell's
1140
1326
  // variations-then-thunk shape: same try posture, variations from the fired members
1141
1327
  try {
1142
1328
  const shaped = applyStacked(subset, sfn);
1143
- if (shaped !== null) {
1329
+ if (shaped !== null && firstFired(firedSets, shaped.variations)) {
1144
1330
  // the ONE call whose variations already name shapes — say so, rather than making `respell`
1145
1331
  // read them back out of the variations it was handed
1146
1332
  respell(shaped.variations, () => shaped.out, true);
1147
1333
  }
1148
1334
  } catch (e) {
1149
- // the report names the full subset — the fired set is unknown mid-throw
1150
- reportThrow([...preRespellVariations, ...subset.map((x) => x.name)], e);
1335
+ // `respell` reports its own throws, so only a member can throw here
1336
+ const step = e instanceof StackedMemberThrew ? e.variations : subset.map((x) => x.name);
1337
+ reportThrow([...preRespellVariations, ...step], e);
1151
1338
  }
1152
1339
  }
1153
1340
  respell(['argbase'], () => materializeArgBases(sfn));
@@ -1828,8 +2015,10 @@ export function enumerateCandidates(
1828
2015
  // audit cannot bound. This is the setting's first lift, and every later one is the same lift
1829
2016
  // again.
1830
2017
  let treeOwnedFold: boolean;
2018
+ let extendedReaders: boolean;
2019
+ let foldedStore: boolean;
1831
2020
  try {
1832
- treeOwnedFold = treeOwnedIn(symbolSetting.symbols);
2021
+ ({ treeOwned: treeOwnedFold, extendedReaders, foldedStore } = liftReportsIn(symbolSetting.symbols));
1833
2022
  } catch (e) {
1834
2023
  if (abortsRow(symbolSetting.variations)) {
1835
2024
  throw e;
@@ -1848,11 +2037,27 @@ export function enumerateCandidates(
1848
2037
  // Read off the pin's OWN call, per symbol-map setting — the `/raw-globals` setting lifts without
1849
2038
  // the map and answers for itself, so no lift is governed by a fact measured on a different one.
1850
2039
  let pinnable = false;
1851
- for (const cand of SIGNEDNESS) {
2040
+ pins: for (const cand of SIGNEDNESS) {
1852
2041
  if (cand.signed && !pinnable) {
1853
2042
  break;
1854
2043
  }
1855
- const base = liftStamped(name, asm, target, prototypes, opts.asmData, symbolSetting.symbols);
2044
+ let tailSlot = false;
2045
+ const base = liftStamped(
2046
+ name,
2047
+ asm,
2048
+ target,
2049
+ prototypes,
2050
+ opts.asmData,
2051
+ symbolSetting.symbols,
2052
+ opts.contextGlobals,
2053
+ {
2054
+ onTailSlot: () => {
2055
+ tailSlot = true;
2056
+ },
2057
+ calleeBodies: opts.calleeBodies,
2058
+ },
2059
+ ownContext.ownContextProto,
2060
+ );
1856
2061
  // `/setup-args` — pass a prototype-less callee only what the CALLING BLOCK set up; which of
1857
2062
  // the two readings the source spelled is genuinely ambiguous, and frontend/ssa.ts
1858
2063
  // narrowToSetupArgs carries the argument for why the differ is what settles it.
@@ -1918,12 +2123,66 @@ export function enumerateCandidates(
1918
2123
  { variations: ['setup-args'], narrow: true },
1919
2124
  ]
1920
2125
  : [{ variations: [], narrow: false }];
2126
+ // `/narrow-param` — declare a parameter every reader of which is an extension at the widest
2127
+ // reader's width (raise/paramwidth.ts, EVERY READER AN EXTENSION). A lift variation for the
2128
+ // reason `/connective` is: the raise mutates in place, and the parameter's width is what
2129
+ // recovery and every later stage read. Enumerated only where this symbol-map setting's lift
2130
+ // reports such a parameter; crossed with the other two, and the dedup collapses what agrees.
2131
+ const paramSettings: { variations: readonly Variation[]; extendedReaders: boolean }[] = extendedReaders
2132
+ ? [
2133
+ { variations: [], extendedReaders: false },
2134
+ { variations: ['narrow-param'], extendedReaders: true },
2135
+ ]
2136
+ : [{ variations: [], extendedReaders: false }];
2137
+ // `/kept-slot` — keep the delay slot of a MIPS `b`/`j` whose slot ends a common tail both arms
2138
+ // spell, where the default lift retargets it to the copy before its target and merges the
2139
+ // arms (frontend/mips.ts `normaliseBranchLikely`: the two are one object, and which the source
2140
+ // wrote is the differ's to say). A LIFT variation because the rewrite is the CFG every later
2141
+ // stage reads. Enumerated only where this setting's default lift reports such a slot; crossed
2142
+ // with the others, and the dedup collapses what agrees. An IDO reading: GCC 2.7.2 and KMC
2143
+ // cross-jump a tail both arms spell before they fill a slot, so they leave no such tail.
2144
+ const tailSettings: { variations: readonly Variation[]; keepTailSlots: boolean }[] = tailSlot
2145
+ ? [
2146
+ { variations: [], keepTailSlots: false },
2147
+ { variations: ['kept-slot'], keepTailSlots: true },
2148
+ ]
2149
+ : [{ variations: [], keepTailSlots: false }];
2150
+ // `/far-cast` — keep each store the displaced-access fold would move into a struct on the
2151
+ // address it was reached through (raise/structs.ts `foldDisplacedAccesses`). On agbcc the
2152
+ // member store and the cast store are two objects (target.ts `farMemberStoreOrder`), and a
2153
+ // hoisted pointer the source held builds the cast's. Elsewhere a folded store stops aliasing
2154
+ // the loads beside it and a held pointer is spelled as members, which KMC, GCC 2.7.2 and mwcc
2155
+ // build into other objects. So which one the source wrote is the differ's to say, on every
2156
+ // target. A LIFT variation because the fold decides which struct the stores join, which
2157
+ // recovery and every later stage read. Enumerated only where this setting's lift folded a
2158
+ // store.
2159
+ const castSettings: { variations: readonly Variation[]; keepStores: boolean }[] =
2160
+ foldedStore && offeredOn(target, ['far-cast'])
2161
+ ? [
2162
+ { variations: [], keepStores: false },
2163
+ { variations: ['far-cast'], keepStores: true },
2164
+ ]
2165
+ : [{ variations: [], keepStores: false }];
1921
2166
  const liftSettings = narrowSettings.flatMap((l) =>
1922
- connectiveSettings.map((c) => ({ ...l, ...c, variations: [...l.variations, ...c.variations] })),
2167
+ connectiveSettings.flatMap((c) =>
2168
+ paramSettings.flatMap((w) =>
2169
+ tailSettings.flatMap((k) =>
2170
+ castSettings.map((f) => ({
2171
+ ...l,
2172
+ ...c,
2173
+ ...w,
2174
+ ...k,
2175
+ ...f,
2176
+ variations: [...l.variations, ...c.variations, ...w.variations, ...k.variations, ...f.variations],
2177
+ })),
2178
+ ),
2179
+ ),
2180
+ ),
1923
2181
  );
1924
2182
  for (const liftSetting of liftSettings) {
1925
2183
  let fn: Fn;
1926
2184
  let discardsPassedResult = false;
2185
+ let guessesParamWidth = false;
1927
2186
  let inferredSymbols = new Map<string, SymbolInfo>();
1928
2187
  let orderLicensed: ReadonlySet<string> = new Set<string>();
1929
2188
  try {
@@ -1932,7 +2191,17 @@ export function enumerateCandidates(
1932
2191
  fn =
1933
2192
  liftSetting.variations.length === 0
1934
2193
  ? base
1935
- : liftStamped(name, asm, target, prototypes, opts.asmData, symbolSetting.symbols);
2194
+ : liftStamped(
2195
+ name,
2196
+ asm,
2197
+ target,
2198
+ prototypes,
2199
+ opts.asmData,
2200
+ symbolSetting.symbols,
2201
+ opts.contextGlobals,
2202
+ { keepTailSlots: liftSetting.keepTailSlots, calleeBodies: opts.calleeBodies },
2203
+ ownContext.ownContextProto,
2204
+ );
1936
2205
  if (liftSetting.narrow && !narrowToSetupArgs(fn)) {
1937
2206
  continue; // nothing to cut after all — the default lift's own candidates already cover it
1938
2207
  }
@@ -1968,9 +2237,10 @@ export function enumerateCandidates(
1968
2237
  // …and the NAMES the map holds for the addresses this lift built by arithmetic off a
1969
2238
  // named one (raise/offsetnames.ts). Per lift setting, off that setting's own map: the
1970
2239
  // `/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.
2240
+ // the staleness that leaves them is priced in pipeline.ts's copy of this note, which also
2241
+ // states the one-spelling-per-cell decline the pass throws from here.
1972
2242
  if (symbolSetting.symbols) {
1973
- OFFSET_NAME_PASS.run(fn, symbolSetting.symbols, target.compilerBehaviors);
2243
+ OFFSET_NAME_PASS.run(fn, symbolSetting.symbols, offsetNameBehaviorsFor(target, opts.contextGlobals));
1974
2244
  }
1975
2245
  for (const [n, si] of [...inferredSymbols]) {
1976
2246
  if (baseOpts.symbols?.has(n) === true || !sameDerivedShape(declSymbols.get(n), si)) {
@@ -1992,7 +2262,16 @@ export function enumerateCandidates(
1992
2262
  },
1993
2263
  },
1994
2264
  prototypes[name],
1995
- { shortCircuit: { foldTreeOwned: liftSetting.connective } },
2265
+ {
2266
+ shortCircuit: { foldTreeOwned: liftSetting.connective },
2267
+ paramWidth: {
2268
+ extendedReaders: liftSetting.extendedReaders,
2269
+ onUndeclaredWidth: () => {
2270
+ guessesParamWidth = true;
2271
+ },
2272
+ },
2273
+ displaced: { keepStores: liftSetting.keepStores },
2274
+ },
1996
2275
  );
1997
2276
  } catch (e) {
1998
2277
  // THE DEFAULT CARRIES NO VARIATION, by construction: every lift variation appends one, so
@@ -2007,6 +2286,33 @@ export function enumerateCandidates(
2007
2286
  reportThrow(settingVariations, e);
2008
2287
  continue;
2009
2288
  }
2289
+ // THE OTHER RETURN-TAIL READING, `/ret-copy` or `/ret-join`, in its own `reading` pass
2290
+ // between the follow and the sink. The default pass structures each setting both ways where
2291
+ // the join past early returns took a join somewhere (`onJoinPastReturns`), and publishes one
2292
+ // (pipeline.ts `structureReturnTailReading`). On a target that declares the join its default
2293
+ // (`joinPastReturnsByDefault`: mwcc 2.4.2 and 2.3.3) it structures the join first
2294
+ // and publishes the copies (`copyReturnTails`) only where they spell the function in fewer
2295
+ // lines, the layout shows a cross-jumped return tail, or an early return keeps a
2296
+ // branch-proven constant unfolded. On every other target it structures the copies first and publishes them, and the
2297
+ // join only where the copies refuse. This pass
2298
+ // enumerates the tree it did not publish, so neither reading is structured twice:
2299
+ // - `/ret-copy`, the copies, where the join was published. A chain of `if`s each holding
2300
+ // an early `return;` has no CFG join (`synthetic:retchain`), and where the early returns
2301
+ // cross-jump into one return tail the follow lands on that tail
2302
+ // (`synthetic:retchain_xj`); without the join both copy the code below every such `if`
2303
+ // into its arms, and a switch with a `case` that returns early likewise. The copies are
2304
+ // kept as a candidate because some rows' bytes are them (`synthetic:sw_fall:ido7.1`);
2305
+ // - `/ret-join`, the join, where the copies were published: the join copies a return tail
2306
+ // into each early return in its turn, and where that is the longer spelling it is kept
2307
+ // as a candidate beside the copies.
2308
+ // A setting whose structuring never took the join has one reading, and pays no candidate.
2309
+ // With both in the fan, the bound moves a ranked score nowhere: `compareScored` orders a
2310
+ // score tie by goto, cast and line counts before enumeration order.
2311
+ //
2312
+ // The shared-tail passes below structure with the copies too: their follow and sink were
2313
+ // ranked against the copies, their winners hold those trees, and crossing them with the join
2314
+ // is a reading nothing has measured.
2315
+ //
2010
2316
  // THE SHARED-TAIL VARIATIONS: the same raised fn, structured again with `followEarlyReturns`,
2011
2317
  // in two passes after the default one.
2012
2318
  // - `/shared-ret` is the follow ALONE, on the fn as raised. Some divergent `if` shares a
@@ -2049,14 +2355,30 @@ export function enumerateCandidates(
2049
2355
  // `droppedDefault` is the DEFAULT pass's drops. Each shared-tail pass reads it and keeps its
2050
2356
  // own drops in a copy, so one pass's structuring failure never removes the other's candidate.
2051
2357
  const droppedDefault = new Set<string>();
2052
- for (const pass of ['default', 'follow', 'sink'] as const) {
2358
+ // Per DEFAULT-pass setting whose structuring was a choice between the join past early
2359
+ // returns and the return-tail copies (pipeline.ts `structureReturnTailReading`), the reading
2360
+ // it did not publish: the only trees the `reading` pass enumerates, as `/ret-copy` where the
2361
+ // join was published and `/ret-join` where the copies were.
2362
+ const unpublishedDefault = new Map<string, { readonly reading: 'copies' | 'join'; readonly sfn: SFn }>();
2363
+ // DEFAULT-pass settings whose copies were dropped (they declined or failed a boundary
2364
+ // contract) or barred: the strip closure over them, as `dropped` is over whole settings, so
2365
+ // `X/merge-names` never publishes or offers the copies where `X`'s copies failed.
2366
+ const copiesDropped = new Set<string>();
2367
+ // What `assertDefaultAccepts` learned about this fn, shared by every setting structured from it.
2368
+ let acceptsMemo = new DefaultAcceptsMemo(fn);
2369
+ for (const pass of ['default', 'follow', 'reading', 'sink'] as const) {
2053
2370
  if (pass === 'follow' && !hasDivergentSharedRet(fn)) {
2054
2371
  continue;
2055
2372
  }
2373
+ if (pass === 'reading' && unpublishedDefault.size === 0) {
2374
+ continue;
2375
+ }
2056
2376
  if (pass === 'sink') {
2057
2377
  let sunk: boolean;
2058
2378
  try {
2059
2379
  sunk = sinkStoreTails(fn);
2380
+ // The sink rewrites fn in place, so nothing the memo learned before it holds after it.
2381
+ acceptsMemo = new DefaultAcceptsMemo(fn);
2060
2382
  if (sunk) {
2061
2383
  verify(fn);
2062
2384
  }
@@ -2084,6 +2406,8 @@ export function enumerateCandidates(
2084
2406
  const settingsForLift = usableStructureSettings.filter((s) =>
2085
2407
  offForThisLift.every((variation) => !s[variation.flag]),
2086
2408
  );
2409
+ // Per setting key structured in THIS pass, the `offWitness` hooks its structuring fired.
2410
+ const witnessed = new Map<string, Set<NonNullable<StructureVariation['offWitness']>>>();
2087
2411
  // `/merge-names` combinations whose un-merged sibling was DROPPED. `structure()` already
2088
2412
  // refuses to let the variation unlock a function the default declines, but it can only see its own
2089
2413
  // refusals — a boundary contract fails out here, in `structureChecked`. Without this a
@@ -2100,14 +2424,50 @@ export function enumerateCandidates(
2100
2424
  if (alternative && droppedDefault.has(key)) {
2101
2425
  continue;
2102
2426
  }
2103
- if (
2427
+ // THE OTHER RETURN-TAIL READING: the tree the default pass structured and did not publish,
2428
+ // enumerated as it is. It is the same setting's, so the witness and the strip closure
2429
+ // below already answered for it there.
2430
+ const otherReading = pass === 'reading' ? unpublishedDefault.get(key) : undefined;
2431
+ if (pass === 'reading' && otherReading === undefined) {
2432
+ continue;
2433
+ }
2434
+ const settingLiftVariations: readonly Variation[] =
2435
+ otherReading === undefined
2436
+ ? liftVariations
2437
+ : [...liftSetting.variations, otherReading.reading === 'copies' ? 'ret-copy' : 'ret-join'];
2438
+ // THE OFF-SIBLING WITNESS (`StructureVariation.offWitness`): a setting whose variation's
2439
+ // OFF sibling structured without firing the witness would rebuild that sibling's tree, so
2440
+ // it is skipped and reported as the tree dedup it is. A sibling not structured in this
2441
+ // pass (dropped, or skipped above) witnessed nothing, and the setting is structured.
2442
+ //
2443
+ // A setting skipped HERE builds its sibling's structuring, so it fires the hooks the
2444
+ // sibling fired, and it is recorded with the sibling's set. Without that the gate does
2445
+ // not compose: a setting carrying two witnessed variations (`tail-copies/for-continue`)
2446
+ // finds each sibling unrecorded and is structured however inert both are. A setting
2447
+ // skipped here in the default pass has no reading of its own in the `reading` pass:
2448
+ // the sibling's fired set is the union of both its readings' (`structureReturnTailReading`),
2449
+ // so both of this setting's readings are the sibling's trees, which the sibling enumerates.
2450
+ const sameAs = STRUCTURE_VARIATIONS.flatMap((variation) => {
2451
+ if (otherReading !== undefined || variation.offWitness === undefined || !s[variation.flag]) {
2452
+ return [];
2453
+ }
2454
+ const sibling = s.variations.filter((v) => v !== variation.name).join('/');
2455
+ const seen = witnessed.get(sibling);
2456
+ return seen !== undefined && !seen.has(variation.offWitness) ? [seen] : [];
2457
+ });
2458
+ if (sameAs.length > 0) {
2459
+ witnessed.set(key, sameAs[0]);
2460
+ opts.onTreeDeduped?.();
2461
+ continue;
2462
+ }
2463
+ const stripped = (set: ReadonlySet<string>): boolean =>
2104
2464
  STRUCTURE_VARIATIONS.some(
2105
2465
  (variation) =>
2106
2466
  variation.strip &&
2107
2467
  s[variation.flag] &&
2108
- dropped.has(s.variations.filter((v) => v !== variation.name).join('/')),
2109
- )
2110
- ) {
2468
+ set.has(s.variations.filter((v) => v !== variation.name).join('/')),
2469
+ );
2470
+ if (otherReading === undefined && stripped(dropped)) {
2111
2471
  // A SKIPPED setting is recorded exactly like a dropped one, or the closure would not be
2112
2472
  // transitive: with plain X dropped and X/inplace skipped-but-unrecorded,
2113
2473
  // X/inplace/merge-names would find neither stripped key and run — shipping a
@@ -2119,28 +2479,97 @@ export function enumerateCandidates(
2119
2479
  // which it re-derives from its map on every call, so both branch senses structure the same
2120
2480
  // recovered function without re-lifting.
2121
2481
  let sfn: SFn;
2482
+ const fired = new Set<NonNullable<StructureVariation['offWitness']>>();
2483
+ const witnessHooks = Object.fromEntries(
2484
+ STRUCTURE_VARIATIONS.flatMap((variation) =>
2485
+ variation.offWitness !== undefined && !s[variation.flag]
2486
+ ? [[variation.offWitness, () => fired.add(variation.offWitness!)]]
2487
+ : [],
2488
+ ),
2489
+ );
2490
+ let counterSignRefused = false;
2491
+ const structOpts = {
2492
+ ...symbolSettingOpts,
2493
+ ...(inferredSymbols.size ? { inferredSymbols } : {}),
2494
+ ...(orderLicensed.size ? { orderLicensedGlobals: orderLicensed } : {}),
2495
+ preserveDivergentBranchSense: s.sense,
2496
+ negateJoinedBranchSense: s.join ? !defSense : defSense,
2497
+ ...(s.flipSites ? { branchSenseFlipSites: s.flipSites } : {}),
2498
+ anchorConstCopies: s.anchor,
2499
+ anchorLoopEntryConsts: s.entry,
2500
+ spellBitfieldMembers: s.bitfields,
2501
+ spellPtrMemberElements: s.ptrElems,
2502
+ spellDeclaredSubscripts: s.declRank,
2503
+ ...STRUCTURE_VARIATIONS.reduce(
2504
+ (acc, variation) => ({ ...acc, ...variation.options(s[variation.flag]) }),
2505
+ {},
2506
+ ),
2507
+ ...(alternative ? { followEarlyReturns: true, copyReturnTails: true } : {}),
2508
+ };
2509
+ const hooks = {
2510
+ ...witnessHooks,
2511
+ onCounterSignRefused: () => {
2512
+ counterSignRefused = true;
2513
+ },
2514
+ };
2122
2515
  try {
2123
- sfn = structureChecked(fn, {
2124
- ...symbolSettingOpts,
2125
- ...(inferredSymbols.size ? { inferredSymbols } : {}),
2126
- ...(orderLicensed.size ? { orderLicensedGlobals: orderLicensed } : {}),
2127
- preserveDivergentBranchSense: s.sense,
2128
- negateJoinedBranchSense: s.join ? !defSense : defSense,
2129
- ...(s.flipSites ? { branchSenseFlipSites: s.flipSites } : {}),
2130
- anchorConstCopies: s.anchor,
2131
- anchorLoopEntryConsts: s.entry,
2132
- spellBitfieldMembers: s.bitfields,
2133
- spellPtrMemberElements: s.ptrElems,
2134
- spellDeclaredSubscripts: s.declRank,
2135
- ...STRUCTURE_VARIATIONS.reduce(
2136
- (acc, variation) => ({ ...acc, ...variation.options(s[variation.flag]) }),
2137
- {},
2138
- ),
2139
- ...(alternative ? { followEarlyReturns: true } : {}),
2140
- });
2516
+ if (otherReading !== undefined) {
2517
+ sfn = otherReading.sfn;
2518
+ } else if (pass === 'default') {
2519
+ // THE RETURN-TAIL READING, bounded by size and read off the layout (pipeline.ts
2520
+ // `structureReturnTailReading`): on a target that declares the join past early
2521
+ // returns its default, the join, or the copies where they spell this setting's
2522
+ // function shorter or the asm layout or an unfolded return marks them; on every other
2523
+ // target the copies, structured first, and the join only where they refuse, with
2524
+ // their refusal reported under `ret-copy` (the reading the fan then cannot hold). A
2525
+ // setting fails only where both refuse, and that failure is handled below as any
2526
+ // structuring failure. The losing tree is kept for the `reading` pass, so neither
2527
+ // reading is structured twice.
2528
+ const copiesBarred = stripped(copiesDropped);
2529
+ const read = structureReturnTailReading(
2530
+ fn,
2531
+ { ...structOpts, joinPastReturns: copiesBarred },
2532
+ (t) => backend.emit(t),
2533
+ hooks,
2534
+ acceptsMemo,
2535
+ );
2536
+ sfn = read.sfn;
2537
+ if (read.unpublished === undefined) {
2538
+ if (copiesBarred) {
2539
+ copiesDropped.add(key);
2540
+ }
2541
+ } else if ('error' in read.unpublished) {
2542
+ copiesDropped.add(key);
2543
+ reportThrow(
2544
+ [...liftVariations, 'ret-copy', ...s.variations, ...symbolSetting.variations],
2545
+ read.unpublished.error,
2546
+ );
2547
+ } else {
2548
+ unpublishedDefault.set(key, read.unpublished);
2549
+ }
2550
+ witnessed.set(key, fired);
2551
+ } else {
2552
+ sfn = structureChecked(fn, structOpts, hooks, acceptsMemo);
2553
+ witnessed.set(key, fired);
2554
+ }
2141
2555
  } catch (e) {
2556
+ if (e instanceof DefaultAcceptsMemoMisuse) {
2557
+ throw e; // a memo wired to the wrong fn is a defect here, never a dropped variation
2558
+ }
2142
2559
  if (abortsRow([...liftVariations, ...symbolSetting.variations], s)) {
2143
- throw e; // the default lift's default setting keeps its behavior: a failure aborts the row
2560
+ if (!(
2561
+ counterSignRefused &&
2562
+ !cand.signed &&
2563
+ pinnable &&
2564
+ e instanceof StructureError &&
2565
+ declinesOnCounterSign(fn, structOpts)
2566
+ )) {
2567
+ throw e; // the default lift's default setting keeps its behavior: a failure aborts the row
2568
+ }
2569
+ // THE UNSIGNED PIN'S DECLINE, HELD FOR THE SIGNED ONE — and the rest of the unsigned
2570
+ // pin is not built: every other setting of it would run behind a declined default.
2571
+ heldPinDecline ??= e;
2572
+ continue pins;
2144
2573
  }
2145
2574
  // Recorded for EVERY dropped setting: a candidate with more variations on looks its siblings
2146
2575
  // up by stripping one variation at a time, and the stripped key can itself carry the other.
@@ -2191,7 +2620,13 @@ export function enumerateCandidates(
2191
2620
  // The PRE-RESPELL variations (PRE_RESPELL_VARIATIONS, the last composition the POLICY note
2192
2621
  // names): rewrite the TREE, then run the whole respell set over the result, so every
2193
2622
  // respell variation derives from the rewrite instead of composing onto it. The gate is the pass's
2194
- // own decline; the contracts are `respell`'s three, for `respell`'s reasons.
2623
+ // own decline; the contracts are `respell`'s, for `respell`'s reasons, plus the effect count
2624
+ // `respell` cannot run (its note above): a pre-respell rewrite moves control, not just
2625
+ // spelling — `/unmerge` copies a join into its arms, `/case-end-goto` jumps over the code
2626
+ // between a loop and its arm's end — and the per-path count is what refuses a rewrite that
2627
+ // runs a call twice on one path or drops one. It is also the only production reader of
2628
+ // that count's case-end-goto paths (contracts.ts `PathCounts.end`): structuring never
2629
+ // writes the goto, so without it those paths are read by unit tests alone.
2195
2630
  for (const pf of PRE_RESPELL_VARIATIONS) {
2196
2631
  try {
2197
2632
  const made = pf.apply(sfn);
@@ -2214,6 +2649,9 @@ export function enumerateCandidates(
2214
2649
  assertLocalsWritten(made);
2215
2650
  assertPostIncrUnshared(made);
2216
2651
  assertNoOrphanedLocals(sfn, made);
2652
+ assertLocalStorageStaysInteger(sfn, made);
2653
+ assertFrameUnionsViewed(made);
2654
+ assertEffectsPreserved(fn, made);
2217
2655
  // A backend refusal on this REWRITTEN tree is not a refusal of the row's own
2218
2656
  // tree, so it never becomes the row's stated cause: `TreeSources.emit` is dropped
2219
2657
  // here and only the call over the row's own tree above records one.
@@ -2274,15 +2712,22 @@ export function enumerateCandidates(
2274
2712
  // whichever route the enumeration happened to walk first.
2275
2713
  if (sp.matchOnly === undefined) {
2276
2714
  delete dup.matchOnly;
2715
+ delete dup.signedUnsignedCompare;
2277
2716
  }
2278
2717
  if (!discardsPassedResult) {
2279
2718
  delete dup.discardsPassedResult;
2280
2719
  }
2720
+ if (!guessesParamWidth) {
2721
+ delete dup.guessesParamWidth;
2722
+ }
2723
+ if (!liftSetting.keepStores) {
2724
+ delete dup.farStoreCast;
2725
+ }
2281
2726
  continue;
2282
2727
  }
2283
2728
  const variations: readonly Variation[] = [
2284
2729
  cand.variation,
2285
- ...liftVariations,
2730
+ ...settingLiftVariations,
2286
2731
  ...s.variations,
2287
2732
  ...sp.variations,
2288
2733
  ...symbolSetting.variations,
@@ -2299,8 +2744,11 @@ export function enumerateCandidates(
2299
2744
  preference: symbolIndex,
2300
2745
  ...(sp.symbolRefs ? { symbolRefs: sp.symbolRefs } : {}),
2301
2746
  ...(sp.deviceVolatile ? { deviceVolatile: sp.deviceVolatile } : {}),
2302
- ...(sp.matchOnly ? { matchOnly: sp.matchOnly } : {}),
2747
+ ...(sp.matchOnly ? { matchOnly: true as const } : {}),
2748
+ ...(sp.signedUnsignedCompare ? { signedUnsignedCompare: true as const } : {}),
2303
2749
  ...(discardsPassedResult ? { discardsPassedResult: true as const } : {}),
2750
+ ...(guessesParamWidth ? { guessesParamWidth: true as const } : {}),
2751
+ ...(liftSetting.keepStores ? { farStoreCast: true as const } : {}),
2304
2752
  };
2305
2753
  seen.set(source, made);
2306
2754
  out.push(made);
@@ -2313,6 +2761,9 @@ export function enumerateCandidates(
2313
2761
  // Every tree the fan produced was refused by the backend. Each refusal on its own is a dropped
2314
2762
  // candidate; all of them together is the row, and it stays LOUD — the alternative is a caller
2315
2763
  // ranking an empty list and reporting no match for a function nothing ever tried to spell.
2764
+ if (out.length === 0 && heldPinDecline !== undefined) {
2765
+ throw heldPinDecline;
2766
+ }
2316
2767
  if (out.length === 0) {
2317
2768
  throw new NoSpellableCandidateError(
2318
2769
  `no spellable candidate for '${name}': ${firstLine(lastEmitError ?? 'no candidate produced')}`,
@@ -2440,7 +2891,8 @@ function stillbornNote(stillborn: Stillborn, fan: number): string {
2440
2891
  *
2441
2892
  * A DISCARDED PASSED RESULT next (`Candidate.discardsPassedResult`): equal bytes refute neither
2442
2893
  * reading, so the one that drops a value the callee may read wins only where it scores strictly
2443
- * better.
2894
+ * better. A GUESSED PARAMETER WIDTH (`Candidate.guessesParamWidth`) likewise: a narrow declaration
2895
+ * no prototype supplied changes its callers' bytes, which no score here reads.
2444
2896
  *
2445
2897
  * PREFERENCE next: a named symbol-map spelling beats its `/raw-globals` sibling at equal bytes.
2446
2898
  *
@@ -2463,6 +2915,11 @@ function stillbornNote(stillborn: Stillborn, fan: number): string {
2463
2915
  * below does the same for `/narrow-decl`, which drops one cast per local it narrows and so wins
2464
2916
  * every tie against the candidate it is derived onto.
2465
2917
  *
2918
+ * A FAR STORE SPELLED THROUGH A CAST (`Candidate.farStoreCast`) next: where `/far-cast` ties the
2919
+ * member spelling, the compiler built the member's object too, and the member names the field. Its
2920
+ * `(u8 *)` base is no scalar cast and its line count is no higher, so neither term below would
2921
+ * prefer the member on its own.
2922
+ *
2466
2923
  * CAST COUNT next, and only WITHIN a preference. A wrong signedness pin is what manufactures casts —
2467
2924
  * the C backend has to cast a shift operand back to the signedness the machine op needs, so
2468
2925
  * pinning `u32` on a genuinely-signed parameter buys `s32 f(u32 a0) { return (s32)a0 >> a1; }`
@@ -2480,12 +2937,11 @@ function stillbornNote(stillborn: Stillborn, fan: number): string {
2480
2937
  * result deterministic. Spelled explicitly rather than leaning on Array#sort's stability, which
2481
2938
  * would make each preference an accident of two unrelated decisions.
2482
2939
  *
2483
- * WHAT NO TERM HERE WEIGHS: a comparison's rendered SIGNEDNESS. `/uns-cmp`'s whole output is
2484
- * that polarity, and it carries in a DECLARED TYPE rather than a cast — `castCount` reads 0 on
2485
- * both sides of the tie it loses — so at equal bytes enumeration order decides, and a rival
2486
- * variation's candidate can publish a signed compare where the asm's is unsigned. Weighing it needs
2487
- * the candidate's own SFn and the icmp facts it was structured from, neither of which this
2488
- * comparator carries; `deviceVolatile` is the shape such a term would take. */
2940
+ * WHAT NO TERM HERE WEIGHS: a comparison's rendered SIGNEDNESS. It needs none: every tree a
2941
+ * source is emitted from is judged for an unsigned compare that renders signed
2942
+ * (`Candidate.signedUnsignedCompare`), whichever variation spelled it so, and such a source is
2943
+ * `matchOnly` — on a nonmatch it is withheld before this comparator sees it, whatever it scores,
2944
+ * and on a match both spellings are the program. */
2489
2945
  export function compareScored<S extends { score: number }>(
2490
2946
  a: Candidate & { score: S; order: number },
2491
2947
  b: Candidate & { score: S; order: number },
@@ -2493,20 +2949,32 @@ export function compareScored<S extends { score: number }>(
2493
2949
  return (
2494
2950
  a.score.score - b.score.score ||
2495
2951
  (a.discardsPassedResult ? 1 : 0) - (b.discardsPassedResult ? 1 : 0) ||
2952
+ (a.guessesParamWidth ? 1 : 0) - (b.guessesParamWidth ? 1 : 0) ||
2496
2953
  a.preference - b.preference ||
2497
2954
  (b.deviceVolatile ?? 0) - (a.deviceVolatile ?? 0) ||
2955
+ (a.farStoreCast ? 1 : 0) - (b.farStoreCast ? 1 : 0) ||
2956
+ gotoCount(a.source) - gotoCount(b.source) ||
2498
2957
  castCount(a.source) - castCount(b.source) ||
2499
2958
  lineCount(a.source) - lineCount(b.source) ||
2500
2959
  a.order - b.order
2501
2960
  );
2502
2961
  }
2503
2962
 
2504
- /** Non-blank lines in a candidate's rendered source — the compactness tie-break above, counted
2505
- * exactly as `quality.ts` counts `lines`. Deterministic, and total on any string. */
2506
- function lineCount(source: string): number {
2507
- return source.split('\n').filter((l) => l.trim().length > 0).length;
2963
+ /** `goto` statements in a candidate's rendered source — the readability tie-break above, asked
2964
+ * before the casts. Each goto asmlift writes (l3/ast.ts: a jump to a case label of the switch it
2965
+ * sits in, or to the `break;` that ends its arm) is a ranked spelling against a goto-free one —
2966
+ * copies (`/case-goto`) or a flag (`/case-end-goto`) — and where the two score alike the goto-free
2967
+ * one is published: a goto is the narrow exception the differ has to earn, never a tie's winner.
2968
+ * Deterministic, and total on any string. */
2969
+ export function gotoCount(source: string): number {
2970
+ return source.match(/\bgoto [A-Za-z_]\w*;/g)?.length ?? 0;
2508
2971
  }
2509
2972
 
2973
+ /** Non-blank lines in a candidate's rendered source — the compactness tie-break above, counted
2974
+ * exactly as `quality.ts` counts `lines`, and the one count the default's return-tail reading is
2975
+ * bounded by (pipeline.ts `sourceLineCount`). Deterministic, and total on any string. */
2976
+ const lineCount = sourceLineCount;
2977
+
2510
2978
  /** Scalar casts in a candidate's rendered source — the readability tie-break above.
2511
2979
  *
2512
2980
  * A WITHIN-PREFERENCE tie-break over two spellings of ONE function, and deliberately NARROWER than