@asmlift/core 0.6.0 → 0.8.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 (88) hide show
  1. package/README.md +48 -24
  2. package/package.json +1 -1
  3. package/src/backend/cfamily.ts +39 -11
  4. package/src/backend/pascal.ts +2 -2
  5. package/src/codegen-flags.ts +640 -0
  6. package/src/contracts.ts +60 -11
  7. package/src/frontend/disasm.ts +141 -11
  8. package/src/frontend/high-half.ts +149 -0
  9. package/src/frontend/mips.ts +458 -209
  10. package/src/frontend/ppc.ts +332 -67
  11. package/src/frontend/reloc-symbol.ts +109 -0
  12. package/src/frontend/splat.ts +56 -18
  13. package/src/frontend/ssa.ts +127 -30
  14. package/src/frontend/stackargs.ts +420 -0
  15. package/src/frontend/thumb.ts +209 -232
  16. package/src/ir/alias.ts +24 -0
  17. package/src/ir/core.ts +70 -3
  18. package/src/ir/opcodes.ts +52 -7
  19. package/src/ir/parse.ts +7 -1
  20. package/src/ir/simplify.ts +1 -1
  21. package/src/l3/address.ts +2 -2
  22. package/src/l3/advance.ts +373 -0
  23. package/src/l3/argbase.ts +6 -6
  24. package/src/l3/argcopy.ts +269 -0
  25. package/src/l3/ast.ts +110 -22
  26. package/src/l3/basecse.ts +50 -30
  27. package/src/l3/coalesce.ts +118 -61
  28. package/src/l3/gates.ts +75 -1
  29. package/src/l3/hoist.ts +1 -1
  30. package/src/l3/homesplit.ts +13 -13
  31. package/src/l3/initfirst.ts +3 -3
  32. package/src/l3/inlinebase.ts +16 -16
  33. package/src/l3/mentions.ts +68 -5
  34. package/src/l3/mulfirst.ts +3 -3
  35. package/src/l3/nearbase.ts +4 -4
  36. package/src/l3/offmember.ts +5 -5
  37. package/src/l3/parkfirst.ts +6 -6
  38. package/src/l3/pollguard.ts +3 -3
  39. package/src/l3/ptrfield.ts +4 -4
  40. package/src/l3/regspell.ts +8 -8
  41. package/src/l3/reindex.ts +22 -17
  42. package/src/l3/scopebase.ts +32 -29
  43. package/src/l3/sinkinit.ts +7 -7
  44. package/src/l3/slotorder.ts +3 -3
  45. package/src/l3/storage.ts +1 -1
  46. package/src/l3/tailmerge.ts +2 -2
  47. package/src/l3/tailret.ts +70 -0
  48. package/src/l3/typing.ts +3 -3
  49. package/src/l3/unmerge.ts +483 -59
  50. package/src/l3/unreduce.ts +15 -14
  51. package/src/l3/volatileptr.ts +11 -11
  52. package/src/l3/volatileval.ts +11 -11
  53. package/src/l3/volstore.ts +16 -16
  54. package/src/l3/zerosub.ts +6 -6
  55. package/src/mangle.ts +49 -0
  56. package/src/pattern/engine.ts +132 -17
  57. package/src/pipeline.ts +39 -16
  58. package/src/proto.ts +2 -2
  59. package/src/raise/const.ts +203 -3
  60. package/src/raise/divpow2.ts +2 -2
  61. package/src/raise/extscale.ts +345 -0
  62. package/src/raise/globalshape.ts +32 -12
  63. package/src/raise/gvn.ts +2 -2
  64. package/src/raise/magicdiv.ts +2 -2
  65. package/src/raise/memberarrays.ts +4 -4
  66. package/src/raise/narrowlocal.ts +18 -2
  67. package/src/raise/paramwidth.ts +133 -3
  68. package/src/raise/pre-recovery.ts +100 -25
  69. package/src/raise/retsink.ts +389 -19
  70. package/src/raise/shortcircuit.ts +595 -34
  71. package/src/raise/structs.ts +4 -4
  72. package/src/raise/tailsink.ts +141 -0
  73. package/src/rank-declare.ts +21 -13
  74. package/src/{rank-axes.ts → rank-variations.ts} +319 -189
  75. package/src/rank.ts +1176 -805
  76. package/src/structure/analysis.ts +87 -90
  77. package/src/structure/bitfields.ts +130 -30
  78. package/src/structure/globalaccess.ts +30 -4
  79. package/src/structure/namecoalesce.ts +32 -13
  80. package/src/structure/retspell.ts +95 -0
  81. package/src/structure/structure.ts +1425 -201
  82. package/src/structure/switch-recover.ts +101 -8
  83. package/src/symbols.ts +127 -6
  84. package/src/target.ts +374 -44
  85. package/src/trace.ts +28 -19
  86. package/src/variation-definitions.ts +1590 -0
  87. package/src/variation-gates.ts +92 -0
  88. package/src/variation-tokens.ts +356 -0
@@ -7,7 +7,7 @@
7
7
  // the clamp0 diamond becomes `if (x < 0) x = 0; return x;` rather than a temp copy).
8
8
  // NOTE the coupled INVERSE: l3/regspell.ts re-derives the UN-coalesced copy-carrying
9
9
  // spelling as a ranked candidate — its R1 template matches THIS pass's diamond output
10
- // shape, so a change to coalescing here can silently stop that lever firing (the
10
+ // shape, so a change to coalescing here can silently stop that respell variation firing (the
11
11
  // matching-suite regspell gate is what makes the coupling loud).
12
12
  // Coalescing is INTERFERENCE-CHECKED against per-block value liveness, and
13
13
  // inline-at-use rendering carries an effect-ordering model: a call/load that cannot
@@ -40,8 +40,9 @@
40
40
  // target language whose `case` cannot fall through (`spellSwitchFallthrough` false) sends Regime A
41
41
  // back to if-recovery, and arms that do not linearize into one chain — two arms falling into the
42
42
  // same sibling, or a fall into the `default:` — refuse in `chainArms`, which answers null.
43
+ import { constAddressOf, globalCellOf } from '../ir/alias';
43
44
  import { Block, Fn, Op, Successor, Value, defOpMap, dominators, mergeClasses, successorsOf } from '../ir/core';
44
- import { CAST_WIDTHS, EFFECTFUL_OPS } from '../ir/opcodes';
45
+ import { CAST_WIDTHS, EFFECTFUL_OPS, SPELLED_WHEN_DEAD_OPS, opSig } from '../ir/opcodes';
45
46
  import { type IrType, T, scalarTypeForAccess, typeEquals } from '../ir/types';
46
47
  import {
47
48
  BinOp,
@@ -60,12 +61,14 @@ import {
60
61
  } from '../l3/ast';
61
62
  import { type Gate, firstRejection } from '../l3/gates';
62
63
  import { exprCType, provablyNonNegative, ptrElemBytes, renderedIntSignedness } from '../l3/typing';
64
+ import { foldConstPair, isConstFoldOpcode } from '../raise/const';
63
65
  import { returnType } from '../raise/recover';
64
66
  import { collectStructs } from '../raise/structs';
65
67
  import {
66
68
  type DeclaredField,
67
69
  type SymbolInfo,
68
70
  type SymbolStructField,
71
+ declaredArrayShape,
69
72
  declaredFields,
70
73
  isArrayField,
71
74
  isBitfieldField,
@@ -73,6 +76,7 @@ import {
73
76
  isScalarCellSize,
74
77
  pointeeFields,
75
78
  scalarCellType,
79
+ structFieldInnerExtents,
76
80
  } from '../symbols';
77
81
  import { analyze } from './analysis';
78
82
  import { makeBitfieldSpelling } from './bitfields';
@@ -84,10 +88,12 @@ import {
84
88
  elementIndex,
85
89
  globalByteBase,
86
90
  globalOf,
91
+ subscriptsFromExtents,
87
92
  } from './globalaccess';
88
93
  import { makeLoopHazards, sunkCopyOverDroppedUndef, updateWriteSet } from './hazards';
89
94
  import { type NaturalLoop, analyzeLoops } from './loops';
90
95
  import { type NameMerge, coalesceNames } from './namecoalesce';
96
+ import { unspelledEpilogues } from './retspell';
91
97
  import { type ArmExit, makeSwitchRecovery } from './switch-recover';
92
98
 
93
99
  // Lower a constant-offset memory access to its lvalue/rvalue Expr. If the base was recovered as a
@@ -162,7 +168,7 @@ function ptrGlobalValueName(x: Expr): string | null {
162
168
  return null;
163
169
  }
164
170
 
165
- /** A pointer global's value, the constant bytes added to it, and the at-most-one variable term. */
171
+ /** A pointer global's value, the constant bytes added to it, and the variable byte residual. */
166
172
  interface PtrGlobalBase {
167
173
  name: string;
168
174
  byte: number;
@@ -170,18 +176,23 @@ interface PtrGlobalBase {
170
176
  }
171
177
 
172
178
  /** Decompose an access base into "the VALUE of a map-declared POINTER global + a constant byte
173
- * offset + at most ONE variable term": `gPtr`, `gPtr + K`, `(u8 *)gPtr + i`, `(u8 *)gPtr + (i <<
174
- * 2) + K`. Null for anything else — two variable terms, no such global, a non-`+` operator —
175
- * because only a single residual can be read as one member's index. */
179
+ * offset + the variable residual": `gPtr`, `gPtr + K`, `(u8 *)gPtr + i`, `(u8 *)gPtr + (i << 2)
180
+ * + K`, `(u8 *)gPtr + K + j + (i << 3)`. Null for anything else — no such global, a non-`+`
181
+ * operator.
182
+ *
183
+ * SEVERAL variable terms re-associate into ONE residual rather than refusing the decomposition,
184
+ * because a rank-2 member's index is two of them (`->x[i][j]` computes `j + i*8`) and the caller
185
+ * that splits them back apart ({@link pointeeElement}) needs to see both. The `+` tree's own
186
+ * LEFT-TO-RIGHT visit order is preserved and never sorted: `j + (i * 8)` and `(i * 8) + j` are
187
+ * different agbcc objects, so the order is part of the answer.
188
+ *
189
+ * Every consumer still decides for itself what a residual it cannot explain means: the
190
+ * constant-offset member spelling ({@link pointeeAccess}) refuses any residual at all. */
176
191
  function ptrGlobalBase(e: Expr, isPtrGlobal: (n: string) => boolean): PtrGlobalBase | null {
177
192
  let name: string | null = null;
178
193
  let byte = 0;
179
- let idx: Expr | null = null;
180
- let ok = true;
194
+ const terms: Expr[] = [];
181
195
  const visit = (x: Expr): void => {
182
- if (!ok) {
183
- return;
184
- }
185
196
  if (x.k === 'bin' && x.op === '+') {
186
197
  visit(x.l);
187
198
  visit(x.r);
@@ -196,14 +207,11 @@ function ptrGlobalBase(e: Expr, isPtrGlobal: (n: string) => boolean): PtrGlobalB
196
207
  byte += x.value;
197
208
  return;
198
209
  }
199
- if (idx !== null) {
200
- ok = false;
201
- return;
202
- }
203
- idx = x;
210
+ terms.push(x);
204
211
  };
205
212
  visit(e);
206
- return ok && name !== null ? { name, byte, idx } : null;
213
+ const idx = terms.length === 0 ? null : terms.reduce((l, r): Expr => ({ k: 'bin', op: '+', l, r }));
214
+ return name !== null ? { name, byte, idx } : null;
207
215
  }
208
216
 
209
217
  /** Does a member declared at signedness `declared` read as EXACTLY the type the cast spelling this
@@ -358,7 +366,7 @@ function ptrMemberBase(e: Expr, sym: SymRenderCtx): PtrMemberBase | null {
358
366
  * rather than widened, because this rule is not byte-neutral (see `spellPtrMemberElements`) and
359
367
  * widening a non-neutral spelling's reach is a separate question a row has to ask.
360
368
  *
361
- * And the whole rule refuses when `/no-ptr-elem` turns it off — the axis, not a preference. */
369
+ * And the whole rule refuses when `/no-ptr-elem` turns it off — the variation, not a preference. */
362
370
  function ptrMemberElement(
363
371
  baseExpr: Expr,
364
372
  carried: Expr | null,
@@ -429,9 +437,159 @@ function spellablePointee(
429
437
  return { fields, const: pointee!.const };
430
438
  }
431
439
 
440
+ /** `gPtr->arr[i]` — a VARIABLE-index access into an ARRAY member of a pointer global's pointee.
441
+ *
442
+ * Unlike the constant-offset member spelling below, this one is NOT byte-neutral. The member form
443
+ * materialises the member's own base (`add r1, r1, #0x8` then `ldrb r0, [r1]`) where the cast form
444
+ * folds the constant into the load (`add r0, r0, r1` then `ldrb r0, [r0, #0x8]`), so the constant
445
+ * reaches this rule down two distinguishable channels: the base tree (`pg.byte`, a separate add)
446
+ * or the instruction's own displacement (`off`). The gate is `off === 0` — the constant was
447
+ * materialised — and where the displacement carries it the cast spelling stands.
448
+ *
449
+ * THAT CHANNEL DOES NOT SAY THE SOURCE NAMED THE MEMBER: a HOISTED BASE LOCAL materialises the
450
+ * same constant. Compiled at agbcc's canonical flags against `u8 unk8[6][8]`, all three of
451
+ * `gBlob->unk8[0][i]`, `u8 *p = (u8 *)gBlob->unk8; p[i]` and `u8 *p = (u8 *)gBlob + 8; p[i]` emit
452
+ * the identical `add r1, #0x8` · `add r1, r1, r0` · `ldrb r0, [r1]`, while `*((u8 *)gBlob + 8 + i)`
453
+ * and `((u8 *)gBlob + i)[8]` take the displacement — and the base-local form is a spelling three
454
+ * shipped passes exist to emit (basecse/nearbase/scopebase). It is a per-site DEFAULT and not a
455
+ * ranked variation for the OTHER reason of the two docs/level-tower.md gives: those two forms TIE in
456
+ * bytes, so a variation would enumerate a candidate that can never win. Contrast the GLOBAL rank
457
+ * recovery one indirection up (`cli/test/matching/array-rank-variation.test.ts`, `/flat-rank`), which
458
+ * IS a variation because its two spellings do not tie. Do not read this gate as "the asm decided" and
459
+ * carry that reading to a case where the alternatives differ in bytes.
460
+ *
461
+ * THE RANK IS PART OF THE SPELLING, not a later fidelity polish. The declaration this access has
462
+ * to type-check against belongs to the PROJECT, not to asmlift, and `->x[k]` on a `u8 x[6][8]`
463
+ * is a ROW: agbcc strides it a second time and truncates the resulting pointer to `u8`, which is
464
+ * a different program that happens to compile. So a member whose map states no rank is REFUSED
465
+ * (absence means "the map could not say" — see SymbolStructField.dims), and a stated rank is
466
+ * spelled out in full (`->x[i][j]`, or `->x[0][i]` where the asm merged the row into one flat
467
+ * counter, which is the same address).
468
+ *
469
+ * `->x[0][i]` TYPE-CHECKS AND IS OUT OF BOUNDS, and both halves of that are meant: `i` runs the
470
+ * member's whole element count through a declared row of `8`. There is no in-bounds alternative —
471
+ * the flat `->x[i]` is a different program (above) and the cast form is what this rule replaces —
472
+ * so it is the only spelling that both type-checks and keeps the bytes, which is a narrower claim
473
+ * than "the only spelling that type-checks". The `pmarrrow` synthetic row referees it (pass
474
+ * `subscriptsFromExtents` a `needRecovered` of true, as the global path does, and the row takes
475
+ * the cast form: MATCH → diff:5); `kleod:CheckWorldCompletion:agbcc` was its real-tier inhabitant
476
+ * until kleod's rows were retired (2026-09-13);
477
+ * at rank 3 the same merge spells `->x[0][0][k]`.
478
+ *
479
+ * A VARIABLE SUBSCRIPT IS NEVER BOUNDED, here or anywhere. The member lookup bounds only the
480
+ * CONSTANT part (`pg.byte` inside `[offset, offset+size)`), so `->grid[i]` can address the member
481
+ * after `grid` exactly as `->x[0][i]` can run past a row. Not this rule's defect to fix: the cast
482
+ * form it replaces is unbounded in the same way, the asm supplies no bound, and refusing every
483
+ * unbounded index would refuse the capability whole (`pmarr1`'s `gBlob->unk8[i]` included). It is
484
+ * recorded so the constant-side check is not mistaken for a bounds check on the access.
485
+ *
486
+ * REFUSES, and each one keeps the honest cast form rather than guessing. Named with the test that
487
+ * fails when it is removed (`packages/core/test/symbols.test.ts`), because a refusal nothing
488
+ * ablates is a claim rather than a rule:
489
+ * • any load/store displacement at all (`off !== 0`) — "named only where the member BASE was
490
+ * materialised"
491
+ * • a pointee nothing may be named through, a qualifier the name would reintroduce, a width
492
+ * mismatch, an element signedness the access contradicts (an s8 read is ldrb+lsl+asr where u8
493
+ * is ldrb alone) — "honours every gate the constant-offset one does", "a WIDER indexed
494
+ * member is not named either", "a WIDTH mismatch falls back to the cast spelling"
495
+ * • no array member of exactly this element width covering the accessed byte — same three
496
+ * • a member with NO stated rank — "absence is not read as rank 1"
497
+ * • a rank the DECLARATION does not spell: `dims` and `length` are independent map facts, and
498
+ * symbolFieldType declares the flat byte array whenever the bound is missing — "a rank with
499
+ * NO `length` DECLARES flat, so the access may not subscript it — as a pair"
500
+ * • a rank whose subscripts cannot be split out of the residual — "a rank the residual cannot
501
+ * be split along still spells every subscript, never a row"
502
+ * • a byte offset into the member that does not land on an element boundary
503
+ *
504
+ * NOT a `Gate` table (l3/gates.ts), unlike `FRESH_MERGE_GATES` and `CARRIER_NAME_GATES` in this
505
+ * same file: those refusals are predicates over ONE prepared context, while these interleave with
506
+ * the computations that produce the values the later ones test (the member `find`,
507
+ * `structFieldInnerExtents`, `declaredArrayShape`, `subscriptsFromExtents`), so building that Ctx
508
+ * would run all of it on inputs the earlier gates reject.
509
+ *
510
+ * A THIRD ROUTE TO THE SAME LEGALIZATION, deliberately not taken: peel `lead.length` array levels
511
+ * off the WALKED base type before the stride check, needing neither `baseElem` nor the global
512
+ * path's env lie (`noteGlobal(name, T.ptr(elem))`) — both of which exist only because
513
+ * `derefStrideOk` asks a SINGLE-subscript question of a node carrying `lead.length + 1`
514
+ * subscripts. It needs `gPtr` typed in the print env, a bigger change than this gap should
515
+ * carry. */
516
+ function pointeeElement(
517
+ pg: PtrGlobalBase,
518
+ off: number,
519
+ width: number,
520
+ signed: boolean,
521
+ isStore: boolean,
522
+ sym: SymRenderCtx,
523
+ ): Expr | null {
524
+ // `pg.idx === null` narrows the type; the sole caller reaches here only for a variable index.
525
+ if (off !== 0 || pg.idx === null) {
526
+ return null;
527
+ }
528
+ const p = spellablePointee(pg.name, sym);
529
+ const f = p?.fields.find(
530
+ (m) => isArrayField(m) && m.elemSize === width && m.offset <= pg.byte && pg.byte < m.offset + m.size,
531
+ );
532
+ if (!p || !f || !spellsAccessType(f.elemSigned, width, signed) || !memberQualsAllow(f, p.const, isStore)) {
533
+ return null;
534
+ }
535
+ // The map must have STATED the rank. Absence is not rank 1 here: every vendored map predates the
536
+ // provider reading a member's rank, and each one flattens a member its project's header declares
537
+ // multidimensional (SymbolStructField.dims).
538
+ if (f.dims === undefined) {
539
+ return null;
540
+ }
541
+ const inner = structFieldInnerExtents(f);
542
+ const rel = pg.byte - f.offset;
543
+ if (inner === null || rel % width !== 0) {
544
+ return null;
545
+ }
546
+ // …AND THE DECLARATION HAS TO SPELL THAT SAME RANK. `dims` is not the declaration: symbolFieldType
547
+ // needs facts `dims` does not supply (a `length`, a base-type `elemSigned`) and declares the FLAT
548
+ // byte array without them, while `dims` and `length` are independent — a member whose outermost
549
+ // subrange is unbounded (`u8 data[][8]`, legal C; @gba-kit/debug-info reports `length` absent and
550
+ // `dims` regardless) carries `dims: [null, 8]` and no `length`. Reading the rank off `dims` alone
551
+ // therefore emitted `gPtr->grid[0][a0];` against `struct Save { u8 grid[48]; };` — the exact
552
+ // decl-vs-access divergence declaredFields' header calls non-compiling C. Asked of
553
+ // declaredArrayShape, which reads the answer back OUT of the declaration, so no gate that
554
+ // function grows later can reopen it. The element test is the one `baseElem` below asserts: the
555
+ // declaration must really give this base the element type the node is about to state for it.
556
+ const decl = declaredArrayShape(f);
557
+ if (decl.extents.length !== inner.length + 1 || !typeEquals(decl.elem, T.int(width * 8, f.elemSigned!))) {
558
+ return null;
559
+ }
560
+ // The residual is BYTES from the member's start — the accessed byte's own offset into it plus
561
+ // whatever the asm computed. `subscriptsFromExtents` splits the declared rows back out of it,
562
+ // and answers zero leading subscripts for a rank-1 member.
563
+ const residual: Expr = rel === 0 ? pg.idx : { k: 'bin', op: '+', l: pg.idx, r: { k: 'const', value: rel } };
564
+ const split =
565
+ inner.length === 0
566
+ ? (() => {
567
+ const i = elementIndex(residual, width);
568
+ return i === null ? null : { lead: [] as Expr[], idx: i };
569
+ })()
570
+ : subscriptsFromExtents(inner, residual, width, false);
571
+ if (split === null) {
572
+ return null;
573
+ }
574
+ return {
575
+ k: 'index',
576
+ base: { k: 'field', base: { k: 'var', name: pg.name }, name: f.name },
577
+ idx: split.idx,
578
+ width,
579
+ signed,
580
+ ...(split.lead.length ? { lead: split.lead } : {}),
581
+ // The element type the MAP declares for this member — the base is a `field` node off an
582
+ // untyped `var`, which the C type walk types `undefined`, and without this the backend would
583
+ // legalize the base through `((u8 *)gPtr->arr)[i]`, which is the cast form's object again.
584
+ // Stated from the same two facts the member gate above just checked (see l3/ast.ts baseElem).
585
+ baseElem: T.int(width * 8, f.elemSigned!),
586
+ };
587
+ }
588
+
432
589
  /** `gPtr->member` for an access through a pointer global's value, or null when the offset is not
433
- * provably ONE member's (see the block comment above). A VARIABLE index declines whatever it
434
- * lands on the indexed form is not byte-neutral and has no spelling here. */
590
+ * provably ONE member's (see the block comment above). A VARIABLE index is not this rule's
591
+ * constant-offset question at all and is handed to {@link pointeeElement}, which decides it on
592
+ * its own terms. */
435
593
  function pointeeAccess(
436
594
  pg: PtrGlobalBase,
437
595
  off: number,
@@ -441,7 +599,7 @@ function pointeeAccess(
441
599
  sym: SymRenderCtx,
442
600
  ): Expr | null {
443
601
  if (pg.idx !== null) {
444
- return null;
602
+ return pointeeElement(pg, off, width, signed, isStore, sym);
445
603
  }
446
604
  const total = pg.byte + off;
447
605
  // Constant offset: the member must match EXACTLY — offset, read width, and the SPELLED type
@@ -488,6 +646,7 @@ function memAccess(
488
646
  scalarGlobals: Set<string>,
489
647
  sym?: SymRenderCtx,
490
648
  isStore = false,
649
+ advancedBy?: number,
491
650
  ): Expr {
492
651
  // A deref of a global's address collapses to the bare global: `*(&gSym)` at off 0 is `gSym`;
493
652
  // at off N the global is an array — `gSym[N/width]` (a C global name decays to a pointer, so
@@ -504,7 +663,38 @@ function memAccess(
504
663
  // carried is already inside `baseExpr` — and folding them into one subscript below
505
664
  // (`idxVal + off / width`) is what makes the two indistinguishable at L3, so the displacement
506
665
  // is recorded before the fold destroys it (see the `operandOff` note in l3/ast.ts).
507
- const fromOperand = off !== 0 ? ({ operandOff: off } as const) : {};
666
+ // …and the second evidence field this function carries: the byte step by which the machine advanced
667
+ // an address register to reach this access (raise/const.ts `advancedBy`, recorded before its own
668
+ // fold destroyed it). It rides beside `operandOff` because both are facts about how the address
669
+ // was computed rather than about which cell it names, and both are lost at L3 otherwise.
670
+ //
671
+ // `off === 0` IS A NARROWING, AND ONE SIDE OF IT IS WHAT THE ASM SHOWS. That side is a base
672
+ // value used as an address at TWO offsets — `ldr r3,=X; strh [r3]; adds r3,#2;
673
+ // strh [r3]; strh [r3,#2]`, where the third access carries `{adv 2, off 2}`. Stamping that one
674
+ // makes `l3/advance.ts` spell a second `p = p + 1;` where the target performed no second `add`:
675
+ // the distance still lands (the displacement was folded into the cell address, so
676
+ // `prev.addr + step` matches), so nothing downstream can refuse it, and the byte-exact spelling
677
+ // — one advance and then a subscript — is not in the fan at all. Pinned on that three-access
678
+ // shape in test/advance.test.ts, a COMPILED repro and not a row.
679
+ //
680
+ // WHAT IT IS NOT is "a non-zero `off` says this access was reached by a displacement RATHER THAN
681
+ // by the advance", which is false wherever ONE displacement rides EVERY member —
682
+ // `ldr r3,=X; strh [r3,#4]; adds r3,#2; strh [r3,#4]` is a real chain at X+4 and X+6, and this
683
+ // term refuses it — both index nodes arrive `{off 4, adv undefined}` and `advancedBases`
684
+ // declines, pinned by test/advance.test.ts's `one displacement on BOTH members is a real chain,
685
+ // and the stamp rule declines it`. The discriminating fact is the same base VALUE used at more
686
+ // than one `off`, where only the smallest is the link — not `off` alone, and reaching it takes a
687
+ // per-value census of address offsets this seam does not have.
688
+ //
689
+ // PRICED AT 0 ROWS EITHER WAY, which is why the narrow term ships: instrumenting this seam over
690
+ // `bench sweep --fan --map-modes harness,nomap` (2,126 records, 1,063 rows) counts 4,361 `advancedBy`
691
+ // stamps, ALL of them at `off === 0` and none at `off !== 0`, over ten producing functions. So
692
+ // the shape above has no corpus inhabitant to pay for the census, and the capability it costs is
693
+ // recorded here rather than spent.
694
+ const addressEvidence = {
695
+ ...(off !== 0 ? ({ operandOff: off } as const) : {}),
696
+ ...(advancedBy !== undefined && off === 0 ? ({ baseAdvanced: advancedBy } as const) : {}),
697
+ };
508
698
  if (sym) {
509
699
  const gb = globalConstByte(baseExpr, off);
510
700
  const si = gb ? sym.info(gb.name) : undefined;
@@ -537,7 +727,7 @@ function memAccess(
537
727
  // buffer that member points AT (see ptrMemberElement).
538
728
  const elem = ptrMemberElement(baseExpr, null, off, width, signed, sym);
539
729
  if (elem) {
540
- return { ...elem, ...fromOperand };
730
+ return { ...elem, ...addressEvidence };
541
731
  }
542
732
  }
543
733
  // …and the MULTIDIMENSIONAL bare-name spelling, which needs the byte terms globalOf's division
@@ -556,7 +746,7 @@ function memAccess(
556
746
  width,
557
747
  signed,
558
748
  lead: multi.lead,
559
- ...fromOperand,
749
+ ...addressEvidence,
560
750
  };
561
751
  }
562
752
  const g = globalOf(baseExpr, width);
@@ -579,9 +769,9 @@ function memAccess(
579
769
  const lead = siArr === undefined ? null : bareArrayLead(siArr, width, signed);
580
770
  if (lead !== null) {
581
771
  sym!.noteGlobal(g.name, T.ptr(T.int(width * 8, siArr!.elemSigned ?? false)));
582
- return { k: 'index', base: { k: 'var', name: g.name }, idx, width, signed, ...lead, ...fromOperand };
772
+ return { k: 'index', base: { k: 'var', name: g.name }, idx, width, signed, ...lead, ...addressEvidence };
583
773
  }
584
- return { k: 'index', base: { k: 'addr', name: g.name }, idx, width, signed, ...fromOperand };
774
+ return { k: 'index', base: { k: 'addr', name: g.name }, idx, width, signed, ...addressEvidence };
585
775
  }
586
776
  const bt = base.type;
587
777
  if (bt.kind === 'ptr' && bt.to.kind === 'struct') {
@@ -593,7 +783,7 @@ function memAccess(
593
783
  const ok = rt?.kind === 'ptr' && rt.to.kind === 'struct' && rt.to.name === bt.to.name && baseExpr.k !== 'index';
594
784
  return { k: 'field', base: ok ? baseExpr : { k: 'cast', to: bt, e: baseExpr }, name: `field_${off}` };
595
785
  }
596
- return { k: 'index', base: baseExpr, idx: { k: 'const', value: off / width }, width, signed, ...fromOperand };
786
+ return { k: 'index', base: baseExpr, idx: { k: 'const', value: off / width }, width, signed, ...addressEvidence };
597
787
  }
598
788
 
599
789
  // A variable-index array access `base[index]`; `base[index].field_K` when a `fieldOff` marks an
@@ -650,11 +840,11 @@ function arrayAccess(
650
840
  // input asm.
651
841
  // u32 g[6][9] — a NON-power-of-two row stride: `g[a][b]` and `g[0][a*9+b]` are BYTE-
652
842
  // IDENTICAL, because agbcc reassociates `(a*36)+(b*4)` into `((a*9)+b)*4` itself. Nothing
653
- // referees the choice, which is the same reason `scaledBy` refuses a CONSTANT row term.
843
+ // referees the question, which is the same reason `scaledBy` refuses a CONSTANT row term.
654
844
  //
655
845
  // Both cases say decline: in the first the evidence points the other way, in the second there
656
846
  // is none. The recovery therefore lives on the byte residual alone (memAccess), where the two
657
- // spellings are still distinguishable. Pinned by matching/array-rank-axis.test.ts.
847
+ // spellings are still distinguishable. Pinned by matching/array-rank-variation.test.ts.
658
848
  const lead = si === undefined ? null : bareArrayLead(si, elemSize, signed);
659
849
  if (lead !== null) {
660
850
  sym!.noteGlobal(baseExpr.name, T.ptr(T.int(elemSize * 8, si!.elemSigned ?? false)));
@@ -824,8 +1014,8 @@ const NO_WRITTEN_DESTINATIONS: ReadonlyMap<Value, number> = new Map<Value, numbe
824
1014
  * same tree and the fan does not grow.
825
1015
  *
826
1016
  * A SUPERSET of the real sort, on purpose and in the safe direction — and the direction only
827
- * holds because rank asks this of the SAME fn it then structures (a `variantGate`, evaluated on
828
- * the variant's own fully-raised fn). Two gaps remain, both of which only say YES where the sort
1017
+ * holds because rank asks this of the SAME fn it then structures (a `perLiftGate`, evaluated on
1018
+ * that lift's own fully-raised fn). Two gaps remain, both of which only say YES where the sort
829
1019
  * says nothing: `keepSlot`/`suppressedArgs` drop copies this still counts, and this asks of every
830
1020
  * edge where `preferDefPosCopyOrder` reorders only the acyclic ones (`copySetIsCyclic` needs the
831
1021
  * built copy list, which is not available here). So it can answer true for a pair that later
@@ -841,7 +1031,7 @@ const NO_WRITTEN_DESTINATIONS: ReadonlyMap<Value, number> = new Map<Value, numbe
841
1031
  * A SECOND, HAND-WRITTEN SPELLING of `edgeCopyRecords`' two comparators — including the stability
842
1032
  * that decides ties — not a call into them, and nothing forces the two to agree. A change to that
843
1033
  * sort has to be mirrored here BY HAND, or this gate keeps answering about an ordering the pass no
844
- * longer produces. Unified deliberately not: this is `/copy-defpos`'s variantGate, so it decides
1034
+ * longer produces. Unified deliberately not: this is `/copy-defpos`'s perLiftGate, so it decides
845
1035
  * which candidates are ENUMERATED and its predicate cannot move without moving rows. */
846
1036
  export function edgeCopyOrdersDiffer(fn: Fn): boolean {
847
1037
  const order = fn.writeOrder;
@@ -912,12 +1102,12 @@ export interface FreshMergeCarrier {
912
1102
  * `max3` (agbcc -O2, scored against its own target) only the SECOND of the two chained merges is
913
1103
  * load-bearing: re-homing just the first is byte-identical to re-homing neither (score 5), and
914
1104
  * re-homing just the second is byte-identical to re-homing both (score 0, MATCH). Splitting the
915
- * choice per slot would be a fan of 2^slots, so the axis offers the whole-function spelling.
1105
+ * decision per slot would be a fan of 2^slots, so the variation offers the whole-function spelling.
916
1106
  *
917
1107
  * `param-rooted` is a SCOPE, not a derivation. A chain rooted in an ordinary merge home is left
918
- * alone, and widening to one is a different, unmeasured axis: 293 of 721 map-less corpus rows
1108
+ * alone, and widening to one is a different, unmeasured variation: 293 of 721 map-less corpus rows
919
1109
  * carry at least one conditional merge slot (925 slots), of which the param rooting admits 109 —
920
- * `LoadBGTilemapData` is one of the other 184, which is why the axis has no reach there — a RECORD
1110
+ * `LoadBGTilemapData` is one of the other 184, which is why the variation has no reach there — a RECORD
921
1111
  * of a measurement, not a live check: that function is in no corpus row and no fixture here (its
922
1112
  * attribution evidence is docs/lbg-attribution.md), so it cannot be re-run from this repo.
923
1113
  *
@@ -929,14 +1119,14 @@ export interface FreshMergeCarrier {
929
1119
  export const FRESH_MERGE_GATES: readonly Gate<FreshMergeCarrier>[] = [
930
1120
  {
931
1121
  id: 'redundant-phi',
932
- why: 'a merge every edge feeds the same value overwrites nothing, so its own home buys a copy',
1122
+ why: 'a merge every edge feeds the same value overwrites nothing, so a local of its own only adds a copy',
933
1123
  sound: false,
934
1124
  guardedBy: 'fresh-merge.test.ts: a redundant phi over one parameter keeps the parameter',
935
1125
  rejects: (c) => c.allSame,
936
1126
  },
937
1127
  {
938
1128
  id: 'param-rooted',
939
- why: "the rule's scope a chain rooted in an ordinary merge home is a separate, unmeasured axis",
1129
+ why: 'a merge fed by a chain that does not start at a parameter keeps its ordinary local',
940
1130
  sound: false,
941
1131
  guardedBy: 'fresh-merge.test.ts: a merge over ordinary locals is untouched',
942
1132
  rejects: (c) => !c.paramRooted,
@@ -970,7 +1160,154 @@ function reHomesParamMerge(
970
1160
  return firstRejection(gates, c) === null;
971
1161
  }
972
1162
 
973
- // Structuring levers, threaded as DATA so a new one is a field here + its consumer, not a new
1163
+ /** One name offered to one block parameter, as `CARRIER_NAME_GATES` judges it.
1164
+ *
1165
+ * THE FIELDS ARE LAZY, and that is a cost decision rather than a style one. `canTakeName` runs per
1166
+ * merge slot per carrier over every named value in the function, and `NAME_COALESCE_GATES`' eager
1167
+ * record is affordable only because its pass is an opt-in variation. Laziness changes nothing about
1168
+ * BLAME: `firstRejection` reports the first gate in TABLE order that rejects, whichever fields
1169
+ * were computed to get there. What it does mean is that the table's order also decides what runs,
1170
+ * so the two whole-function walks sit at the bottom. */
1171
+ export interface CarrierName {
1172
+ /** the merge is a pure alias — every in-edge hands it the same value */
1173
+ readonly pureAlias: boolean;
1174
+ /** the name's declaration and the parameter disagree about carrier width */
1175
+ readonly widthDiffers: boolean;
1176
+ /** …or, at a sub-word width, about signedness — which at that width is part of the width */
1177
+ readonly signDiffers: boolean;
1178
+ /** another parameter of the SAME block already holds the name */
1179
+ readonly siblingHolds: boolean;
1180
+ /** a value under the name is still live where the parameter's in-edge copies land */
1181
+ readonly carrierLive: boolean;
1182
+ /** the name is written somewhere the parameter is still live — at a materialized definition
1183
+ * under it, or at any block a predecessor of another parameter's block reaches, which is that
1184
+ * block itself plus wherever a loop emitter could move the copy to */
1185
+ readonly carrierWritten: boolean;
1186
+ /** a value with no name of its own re-derives the name at a use past the copy */
1187
+ readonly reDerivesName: boolean;
1188
+ }
1189
+
1190
+ /** `canTakeName`'s admission: may this block parameter be SPELLED with a name that already exists,
1191
+ * so the in-edge copies into it disappear? EVERY rule here is sound — a refusal costs one copy in
1192
+ * the emitted C, and an admission this table gets wrong is a program that computes something else.
1193
+ * The argument for each is in the block comment above `canTakeName`, which has room for it.
1194
+ *
1195
+ * `carrier-write` carries the one approximation in the table. A loop emitter rotates the header's
1196
+ * update copy to the BOTTOM of the body, where it also runs on the exiting iteration, so a write
1197
+ * nominally on a back edge lands on the exit path too. Liveness cannot see a placement, so the
1198
+ * rule takes the conservative union over every block the writing edge's predecessor reaches. */
1199
+ export const CARRIER_NAME_GATES: readonly Gate<CarrierName>[] = [
1200
+ {
1201
+ id: 'carrier-width',
1202
+ why: 'the copies into the name are assignments through its declaration, which would truncate',
1203
+ sound: true,
1204
+ guardedBy: 'fresh-merge.test.ts: a merge WIDER than its parameter carrier takes a fresh home',
1205
+ rejects: (c) => c.widthDiffers,
1206
+ },
1207
+ {
1208
+ id: 'carrier-sign',
1209
+ why: 'a narrow declaration IS the extension it replaced, and u8 re-applies a different one',
1210
+ sound: true,
1211
+ guardedBy: 'carrier-name.test.ts: ablating carrier-sign reads an s8 carrier through a u8 name',
1212
+ rejects: (c) => c.signDiffers,
1213
+ },
1214
+ {
1215
+ id: 'sibling-param',
1216
+ why: 'two parameters of one block share every in-edge, so one edge would write the name twice',
1217
+ sound: true,
1218
+ guardedBy: 'carrier-name-fuzz.test.ts: every SOUND gate of CARRIER_NAME_GATES is load-bearing',
1219
+ rejects: (c) => c.siblingHolds,
1220
+ },
1221
+ {
1222
+ id: 'carrier-live',
1223
+ why: 'a value under the name still live here is what the in-edge copies would overwrite',
1224
+ sound: true,
1225
+ guardedBy: 'carrier-name-fuzz.test.ts: every SOUND gate of CARRIER_NAME_GATES is load-bearing',
1226
+ rejects: (c) => !c.pureAlias && c.carrierLive,
1227
+ },
1228
+ {
1229
+ id: 'carrier-write',
1230
+ why: 'the converse — the name must not be written anywhere the parameter is still live',
1231
+ sound: true,
1232
+ guardedBy: 'carrier-name.test.ts: ablating carrier-write reads a loop variable past its sunk update',
1233
+ rejects: (c) => c.carrierWritten,
1234
+ },
1235
+ {
1236
+ id: 're-derives',
1237
+ why: 'an unnamed live value is re-rendered at its use, and would read the name written here',
1238
+ sound: true,
1239
+ guardedBy: 'name-clobber.test.ts: the inlined difference keeps reading the value the asm computed it from',
1240
+ rejects: (c) => !c.pureAlias && c.reDerivesName,
1241
+ },
1242
+ ];
1243
+
1244
+ /** A nested loop's header parameter offered its ENCLOSING loop's name, as
1245
+ * `ENCLOSING_CARRIER_GATES` judges it — the admission `enclosingCarrierName` makes before
1246
+ * `canTakeName` (`CARRIER_NAME_GATES`) is asked. `E` is the inner header's first forward
1247
+ * predecessor and `a` the argument `E` hands the parameter.
1248
+ *
1249
+ * Lazy for the same reason `CarrierName` is: `carriedByBoth` walks the in-edges of both headers,
1250
+ * so it sits last and runs only when every cheaper rule admits. Every field is TOTAL — false when
1251
+ * `E` or `a` does not exist — so a test can drop any one gate and the rest still answer. */
1252
+ export interface EnclosingCarrier {
1253
+ /** every forward predecessor of the inner header is the one block `E`, and `E` is not the header */
1254
+ readonly oneForwardEntry: boolean;
1255
+ /** `E` heads a loop whose body holds the inner header */
1256
+ readonly entryEncloses: boolean;
1257
+ /** `a` is one of `E`'s own parameters — `E`'s loop-carried value, which `E` does not compute */
1258
+ readonly argIsEnclosingParam: boolean;
1259
+ /** the frontend measured `E`, and `E` wrote nothing into the parameter's key */
1260
+ readonly keyUnwritten: boolean;
1261
+ /** the value is carried by BOTH loops (`carriedByBothLoops`). False whenever `entryEncloses` or
1262
+ * `argIsEnclosingParam` is, since it needs `E`'s loop and `a`'s slot in `E` — which is why no
1263
+ * fixture can separate those two from this one. Independent of `keyUnwritten`, which no part of
1264
+ * the walk consults */
1265
+ readonly carriedByBoth: boolean;
1266
+ }
1267
+
1268
+ /** `enclosingCarrierName`'s own admission. The argument for each rule is the block comment above
1269
+ * `enclosingCarrierName`; the ids are here so a refusal can be named and a rule dropped by a test.
1270
+ *
1271
+ * ONE rule is sound, and it is the last. The four before it bound what the write-order record can
1272
+ * be EVIDENCE for — a refusal keeps the two-variable spelling, which is correct by construction —
1273
+ * so each is `sound: false`: dropping one spells a different, still-correct program (the fixtures
1274
+ * in `nested-carrier.test.ts` show what each keeps). `carried-by-one-loop` is the collision
1275
+ * `canTakeName` cannot see, and dropping it emits another program. */
1276
+ export const ENCLOSING_CARRIER_GATES: readonly Gate<EnclosingCarrier>[] = [
1277
+ {
1278
+ id: 'one-forward-entry',
1279
+ why: 'the record is per predecessor, so it cannot vouch for a second edge into the inner header',
1280
+ sound: false,
1281
+ rejects: (c) => !c.oneForwardEntry,
1282
+ },
1283
+ {
1284
+ id: 'enclosing-header',
1285
+ why: 'only the enclosing header itself has a record that can say the entry copy was not made',
1286
+ sound: false,
1287
+ rejects: (c) => !c.entryEncloses,
1288
+ },
1289
+ {
1290
+ id: 'enclosing-param',
1291
+ why: 'only a value the enclosing header did not compute can have reached the inner loop uncopied',
1292
+ sound: false,
1293
+ rejects: (c) => !c.argIsEnclosingParam,
1294
+ },
1295
+ {
1296
+ id: 'key-written',
1297
+ why: 'the enclosing header wrote the key (or was not measured): the source spelled the copy',
1298
+ sound: false,
1299
+ rejects: (c) => !c.keyUnwritten,
1300
+ },
1301
+ {
1302
+ id: 'carried-by-one-loop',
1303
+ why: 'a value the outer back edge replaces would be overwritten under a name the inner loop reads',
1304
+ sound: true,
1305
+ guardedBy: 'nested-carrier.test.ts: ablating carried-by-one-loop lets the outer update clobber the inner value',
1306
+ rejects: (c) => !c.carriedByBoth,
1307
+ },
1308
+ ];
1309
+
1310
+ // Structuring options, threaded as DATA so a new one is a field here + its consumer, not a new
974
1311
  // positional boolean widened across every call site:
975
1312
  // returnsVoid — from the function's own prototype (suppress phantom r0 return);
976
1313
  // coalesceLoopInit — keep the induction var in its arg register;
@@ -981,9 +1318,9 @@ function reHomesParamMerge(
981
1318
  // booleans, never a compiler name.
982
1319
  //
983
1320
  // WHAT A FIELD DOC BELOW HOLDS, narrowly: what the option MEANS to `structure()`, and the suffix of
984
- // the axis that enumerates it. An AXIS's rationale, and any figure pricing its marginal value, live
985
- // ONCE at its `STRUCTURING_AXES` entry in rank.ts — restated here the two copies rot separately,
986
- // and only the rank.ts one sits next to the enumeration that could refute it. Figures pricing a
1321
+ // the variation that enumerates it. A VARIATION's rationale, and any figure pricing its marginal value, live
1322
+ // ONCE at its `STRUCTURE_VARIATIONS` entry in rank-variations.ts — restated here the two copies rot
1323
+ // separately, and only that one is the entry the enumeration reads. Figures pricing a
987
1324
  // DEFAULT this pass owns (the edge-copy ordering, `spellDeclaredSubscripts`) do belong here.
988
1325
  export interface StructureOptions {
989
1326
  returnsVoid?: boolean;
@@ -996,9 +1333,9 @@ export interface StructureOptions {
996
1333
  // both arms reconverge. Defaults to preserveDivergentBranchSense rather than to a constant, so a
997
1334
  // target that opts out of the divergent claim opts out of this one; target.ts says how.
998
1335
  //
999
- // This is the ZERO POINT of rank.ts's `/flip-join` axis, not a per-compiler fact that closes
1336
+ // This is the ZERO POINT of rank.ts's `/flip-join` variation, not a per-compiler fact that closes
1000
1337
  // the question — docs/level-tower.md wants a default only where the mapping is a FUNCTION, and
1001
- // benchmark rows still reach their winning spelling through the axis rather than through this
1338
+ // benchmark rows still reach their winning spelling through the variation rather than through this
1002
1339
  // default. Read it forward only: it says which sense to emit ABSENT evidence of an inversion,
1003
1340
  // never that the asm's layout WAS the source's sense. What agbcc contributes is the refusals —
1004
1341
  // its gcc Makefile SRCS compiles neither sched.c nor reorg.c and toplev.c never sets
@@ -1006,14 +1343,41 @@ export interface StructureOptions {
1006
1343
  // toplev.c sets for -Os alone — so no scheduler and no hoister moves an arm's body across the
1007
1344
  // branch after stmt.c laid the arms out in source order.
1008
1345
  //
1009
- // Three mechanisms DO invert the sense, and each is per-SITE where this lever is per-function,
1346
+ // Three mechanisms DO invert the sense, and each is per-SITE where this option is per-function,
1010
1347
  // so no value here is right in every `if` of a function that holds several: a short-circuit
1011
1348
  // fold picks which successor is `taken` from the asm's branch polarity, which on Thumb the
1012
1349
  // branch RANGE decides (raise/shortcircuit.ts); a relay past a branch's reach inverts to jump
1013
1350
  // around the long form; and a rotated loop's zero-trip guard is an `if` no source wrote at all
1014
1351
  // (`synthetic:fib`, `for(i=0;i<n;i++)`, emits `if (0 >= a0) … else do{…}while`), so there no
1015
- // spelling is the faithful one and only the differ can choose.
1352
+ // spelling is the faithful one and only the differ can choose. `senseFromFoldEvidence` below
1353
+ // answers the FIRST of the three per site, off the fold's own record.
1016
1354
  negateJoinedBranchSense?: boolean;
1355
+ /** Spell a branch-sense site from the SHORT-CIRCUIT FOLD'S own orientation evidence, where the
1356
+ * fold left some, instead of from the two booleans above. `raise/shortcircuit.ts` stamps the
1357
+ * fused branch with three facts: `scSharedOnFall` — whether the arm both tests reach was FALLEN
1358
+ * INTO rather than branched to, and so, since gcc lays a condition's arms out in source order,
1359
+ * whether it is the source's `then` — `scSharedIsTaken`, which successor slot that arm landed in
1360
+ * here, and `scEdgeRelayed`, whether a long-branch trampoline sat on either edge, which is the
1361
+ * layout where the source-order premise is inverted and therefore must not be consulted. The
1362
+ * site NEGATES in one cell only: the shared arm in the TAKEN slot, branched to, no relay — the
1363
+ * taken slot holding the source's `else`. Per SITE, which is the point: the booleans are per
1364
+ * function, and a function whose `if`s were written in opposite senses has no right value for
1365
+ * either.
1366
+ *
1367
+ * A site the fold did not touch carries no stamp and keeps its boolean, so this changes nothing
1368
+ * on a function with no short-circuit chain. rank.ts's `/site-sense` variation; it is a VARIATION and not
1369
+ * a default because the source-order premise under `scSharedOnFall` is a claim about gcc's
1370
+ * layout that the differ referees per row — and because one cell of the table is genuinely
1371
+ * undecided by these three facts (see the census at the read). */
1372
+ senseFromFoldEvidence?: boolean;
1373
+ /** PER-SITE override of whatever decided a site's sense — the boolean, or `senseFromFoldEvidence`
1374
+ * where that is on: the ORDINALS of the branch-sense sites to spell the OTHER way round. A
1375
+ * site's ordinal is its
1376
+ * position among the distinct blocks that turn out to BE sense sites, in first-visit order —
1377
+ * a numbering only this pass can hand out, since a site exists only once structuring has
1378
+ * decided both of its arms are real. Absent ⇒ the booleans alone decide, which is every caller
1379
+ * but the enumerating one; an ordinal past the last site is inert. */
1380
+ branchSenseFlipSites?: ReadonlySet<number>;
1017
1381
  orderArgCopiesByWriteOrder?: boolean;
1018
1382
  /** Order a measured edge's ACYCLIC copy set by the def-position proxy instead — the
1019
1383
  * `/copy-defpos` ranked sibling of the write-order spelling (rank.ts). A CYCLIC set keeps the
@@ -1023,20 +1387,40 @@ export interface StructureOptions {
1023
1387
  * on an unmeasured pred, where the proxy is already what runs. */
1024
1388
  preferDefPosCopyOrder?: boolean;
1025
1389
  // Comparison-tree switch recovery: treat an `x != K` test as a case (the EQUAL side is a case
1026
- // body). GCC freely uses `!=`; IDO prefers `==`/`<`. A per-compiler DATA lever, not an `arch ==`
1390
+ // body). GCC freely uses `!=`; IDO prefers `==`/`<`. A compiler behavior, not an `arch ==`
1027
1391
  // branch — default true (permissive; the decline path keeps it sound either way).
1028
1392
  switchAllowsNeqCase?: boolean;
1029
1393
  // Comparison-tree switch recovery: treat a relational test whose BRANCH admits exactly one
1030
- // scrutinee value as that case rather than as navigation. A per-compiler DATA lever declared in
1394
+ // scrutinee value as that case rather than as navigation. A compiler behavior declared in
1031
1395
  // TargetDescription.compilerBehaviors — a compiler opts in on evidence that its dispatch jumps
1032
1396
  // straight to a bounded subtree's body. Default false: absent, every relational edge navigates.
1033
1397
  switchAllowsBoundCase?: boolean;
1034
1398
  // Comparison-tree switch recovery: emit the case arms in the order the ASSEMBLY lays their
1035
- // bodies out, rather than sorted by ascending case value. A per-compiler DATA lever declared in
1399
+ // bodies out, rather than sorted by ascending case value. A compiler behavior declared in
1036
1400
  // TargetDescription.compilerBehaviors — a compiler opts in on evidence that it neither reorders
1037
1401
  // basic blocks nor schedules across them, so the layout it produced IS the order the source
1038
1402
  // wrote. Default false: absent, the arms keep the ascending spelling.
1039
1403
  switchArmsFollowLayout?: boolean;
1404
+ // Comparison-tree switch recovery: DECLINE a tree whose own layout interleaves a test block with
1405
+ // a case body, on the reading that the source wrote an if/else-if LADDER there and not a
1406
+ // `switch`. Regime A otherwise recovers a `switch` from any comparison tree it can, so a ladder
1407
+ // and a `switch` over the same values produce the SAME candidate and the differ never sees the
1408
+ // ladder — there is nothing in the fan for it to prefer.
1409
+ //
1410
+ // The two spellings are different objects. Compiled at agbcc's canonical flags the same two-case
1411
+ // body is 20 bytes either way (0x14, ten Thumb instructions — the pair is committed as
1412
+ // `corpus/agbcc-sw{frontload,ladder}.s`) and disagrees instruction for instruction: the `switch` emits
1413
+ // `cmp #0x1e; beq` then `cmp #0x64; bne` — both tests ahead of both bodies, and sorted ASCENDING,
1414
+ // which is the reverse of the order the source writes them in — while the ladder emits
1415
+ // `cmp #0x64; bne` directly above its own body and reaches `cmp #0x1e` only after it.
1416
+ //
1417
+ // Read PER SITE off the recovery's own blocks — a function may hold one of each — and it
1418
+ // inherits `layoutIndex`'s frontend premise (see switch-recover.ts PRE5, which also states what
1419
+ // the gate costs when the premise fails: the `switch` spelling, never correctness — and what a
1420
+ // lost spelling actually looks like, which on a NESTED tree is an `if` nest around a `switch`
1421
+ // over some of the arms, not a clean ladder).
1422
+ // Default false: absent, every recoverable tree is still spelled as a `switch`.
1423
+ switchRequiresFrontLoadedTests?: boolean;
1040
1424
  // Does the TARGET LANGUAGE spell a `switch` arm that runs on into the next one? Set from the
1041
1425
  // caller's LanguageBackend (`spellsSwitchFallthrough`), not from the compiler target: it is a
1042
1426
  // property of what the emitted source may say, and the only reason the structurer needs it is
@@ -1044,7 +1428,7 @@ export interface StructureOptions {
1044
1428
  // to if-recovery when it is false; Regime B, having no fallback, fails loud. Default true.
1045
1429
  spellSwitchFallthrough?: boolean;
1046
1430
  // Which way this compiler hands out frame slots against DECLARATION RANK: `ascending` = the
1047
- // earlier-declared spilled local takes the LOWER `[sp,#k]`. A per-compiler DATA lever declared
1431
+ // earlier-declared spilled local takes the LOWER `[sp,#k]`. A compiler behavior declared
1048
1432
  // in TargetDescription.compilerBehaviors, carried to the backend on `SFn.slotOrder` and applied
1049
1433
  // by `l3/slotorder.ts` at emit time. ABSENT means the ordering refuses — there is no default
1050
1434
  // direction, because a wrong one reorders declarations for no reason.
@@ -1057,26 +1441,26 @@ export interface StructureOptions {
1057
1441
  // (target.ts), which is where the target-to-structurer mapping lives.
1058
1442
  spillSlotOrder?: 'ascending' | 'descending';
1059
1443
  // Commutative load pairs re-spell in def (evaluation) order — see the swap in lowerDef. Default
1060
- // true; verified byte-exact on agbcc and IDO. A per-compiler DATA lever declared in
1444
+ // true; verified byte-exact on agbcc and IDO. A compiler behavior declared in
1061
1445
  // TargetDescription.compilerBehaviors: the first compiler whose scheduler is shown re-ordering
1062
1446
  // independent loads flips it there, not in a code branch. A per-FUNCTION machine-order fallback
1063
1447
  // candidate is deliberately deferred until a row demands it.
1064
1448
  defOrderLoadPairs?: boolean;
1065
1449
  // Anchor a constant merge copy at its const op's ORIGINAL position instead of at the CFG edge:
1066
1450
  // `movs r9, #0` at entry ahead of a single-armed overwrite emits as a pre-initialization above
1067
- // the `if`, not as its else-arm. A differ-refereed candidate axis (rank.ts `/defsite`), never a
1451
+ // the `if`, not as its else-arm. A differ-refereed candidate variation (rank.ts `/defsite`), never a
1068
1452
  // default — see the refusal conditions where it is computed.
1069
1453
  anchorConstCopies?: boolean;
1070
1454
  // WIDEN `anchorConstCopies` to a LOOP HEADER's entry constant — `int s = 0;` hoisted above the
1071
1455
  // `if` that guards the loop, rather than written on the edge into it. A second placement
1072
- // decision, so a second axis point (rank.ts `/defsite/loop-entry`) rather than a widening of
1456
+ // decision, so a second structure setting (rank.ts `/defsite/loop-entry`) rather than a widening of
1073
1457
  // the first: on a function carrying both kinds of anchorable const, folding them into one flag
1074
1458
  // would make "anchor the plain ones, leave the loop's at its edge" — a spelling `/defsite`
1075
1459
  // emits today — unreachable. Inert unless `anchorConstCopies` is also on.
1076
1460
  //
1077
- // AN AXIS RATHER THAN AN EXTENSION OF `l3/initfirst.ts`, whose header opens on the same rewrite
1461
+ // A VARIATION RATHER THAN AN EXTENSION OF `l3/initfirst.ts`, whose header opens on the same rewrite
1078
1462
  // (`if (0 < n) { v = 0; … }` → `v = 0; if (v < n) { … }`) at a fraction of the price: a
1079
- // re-spelling adds candidates only where it fires, while this multiplies every candidate below
1463
+ // respell variation adds candidates only where it fires, while this multiplies every candidate below
1080
1464
  // it. The fork is REACH, and it is a hard one. `initfirst` MOVES A STATEMENT, and an edge copy
1081
1465
  // is not a statement — structuring mints it, choosing between the edge and the const op's own
1082
1466
  // position, and the IR that holds those positions is gone by the time L3 runs. So the shapes
@@ -1084,26 +1468,34 @@ export interface StructureOptions {
1084
1468
  // re-spelling wants an ELSE-LESS `if` and rewrites the condition to read the hoisted variable
1085
1469
  // (`if (v < n)`), where anchoring leaves the condition alone, so the two emit different sources.
1086
1470
  // Measured, not argued: `/initfirst` rides every spelling rank.ts enumerates, so it is scored on
1087
- // every benchmark row already, and on each row this axis wins its `/initfirst`-only sibling is
1471
+ // every benchmark row already, and on each row this variation wins its `/initfirst`-only sibling is
1088
1472
  // either not enumerated at all or not byte-identical to the anchored source — the substitution
1089
- // reaches none of them. Take the axis only while rows demand that; the price is in rank.ts.
1473
+ // reaches none of them. Take the variation only while rows demand that; the price is in rank.ts.
1090
1474
  anchorLoopEntryConsts?: boolean;
1091
1475
  // HARDWARE fact from TargetDescription.capabilities.endianness, threaded by structureOptionsFor:
1092
1476
  // the bitfield extract recognizer solves an LSB-first equation, so it only runs on little-endian
1093
1477
  // data. The provider already refuses to EMIT bitfield facts for a big-endian ELF; this is the
1094
1478
  // same boundary enforced on core's side, against a hand-built map that never went through it.
1095
1479
  littleEndian?: boolean;
1480
+ // HARDWARE fact from TargetDescription.capabilities.deviceRegisters, threaded by
1481
+ // `structureOptionsFor` like `littleEndian` above: the half-open byte window whose cells are
1482
+ // hardware registers rather than objects a source declares. The structurer asks it the same
1483
+ // question its four other readers ask — "would a source have spelled this address `volatile`" —
1484
+ // so it may be approximate: it decides a SPELLING, not a memory model (that is
1485
+ // `deviceMemoryWriters`, which no structurer rule reads). Used as a REFUSAL: absent, a dead read
1486
+ // at a literal address is dropped.
1487
+ deviceRegisters?: readonly [number, number];
1096
1488
  // Spell `(x << a) >> b` extracts of a struct global as the map's named bitfield member. On by
1097
- // default; rank.ts enumerates the OFF spelling as the `/no-bitfield` axis, because the named
1489
+ // default; rank.ts enumerates the OFF spelling as the `/no-bitfield` variation, because the named
1098
1490
  // read recompiles at the DECLARATION's access width — where that diverges from the asm's load
1099
1491
  // width the honest shift spelling is the one that matches, and the differ referees. Only the map
1100
1492
  // carries the names, so with no `symbols` this is normalized to false whatever a caller passes.
1101
1493
  spellBitfieldMembers?: boolean;
1102
1494
  // Spell an element-scaled offset through a map-declared POINTER MEMBER as a whole-element
1103
1495
  // subscript of it (`gBg.pMap[i + 157]`) rather than as the byte arithmetic it replaces. On by
1104
- // default; rank.ts enumerates the OFF spelling as the `/no-ptr-elem` axis.
1496
+ // default; rank.ts enumerates the OFF spelling as the `/no-ptr-elem` variation.
1105
1497
  //
1106
- // IT IS AN AXIS AND NOT A DEFAULT BECAUSE IT IS NOT BYTE-NEUTRAL — the bar the block comment
1498
+ // IT IS A VARIATION AND NOT A DEFAULT BECAUSE IT IS NOT BYTE-NEUTRAL — the bar the block comment
1107
1499
  // above `spellablePointee` sets for a member spelling. Compiled against agbcc (`-mthumb-interwork -Wimplicit -O2 -fhex-asm
1108
1500
  // -fprologue-bugfix`), `((u16 *)gB.pMap)[i + K]` and `*(u16 *)((i << 1) + (u8 *)gB.pMap + 2K)`
1109
1501
  // are the same address and the same instruction COUNT at K = 0, 1 and 157 — and different
@@ -1114,9 +1506,9 @@ export interface StructureOptions {
1114
1506
  // Recover a multidimensional array global's DECLARED subscripts (`g[r][i]`) from a byte residual
1115
1507
  // carrying a term at the declared ROW stride, rather than spelling the whole residual as the
1116
1508
  // `*(T *)(… + (u32)&g)` cast it replaces. On by default; rank.ts enumerates the OFF spelling as
1117
- // the `/flat-rank` axis.
1509
+ // the `/flat-rank` variation.
1118
1510
  //
1119
- // IT IS AN AXIS AND NOT A DEFAULT BECAUSE THE ASM DOES NOT DETERMINE IT, and the evidence that
1511
+ // IT IS A VARIATION AND NOT A DEFAULT BECAUSE THE ASM DOES NOT DETERMINE IT, and the evidence that
1120
1512
  // it does not is the same compile the recovery's own premise rests on, read against the spelling
1121
1513
  // the recovery DISPLACES rather than against the flat one it refuses. For `u16 g[4][0x400]`:
1122
1514
  //
@@ -1131,7 +1523,7 @@ export interface StructureOptions {
1131
1523
  // own premise is false and the flat spelling reaches it too.
1132
1524
  //
1133
1525
  // WHICH ROW OF THAT TABLE IS PINNED: the agbcc pair, by
1134
- // packages/cli/test/matching/array-rank-axis.test.ts, which compiles both spellings through the
1526
+ // packages/cli/test/matching/array-rank-variation.test.ts, which compiles both spellings through the
1135
1527
  // klonoa checkout's own template. The other three were measured by hand and nothing re-runs
1136
1528
  // them, so treat them as the record of a measurement rather than as a live check.
1137
1529
  //
@@ -1140,34 +1532,34 @@ export interface StructureOptions {
1140
1532
  spellDeclaredSubscripts?: boolean;
1141
1533
  // Let a read of a named global render at its use across writes that PROVABLY cannot reach it
1142
1534
  // (a store to a different named global), instead of caching it in a local. Off by default;
1143
- // rank.ts enumerates the ON spelling as the `/reread-globals` axis — see analysis.ts
1144
- // AnalyzeOptions for why this is a differ-refereed lever and not a fix.
1535
+ // rank.ts enumerates the ON spelling as the `/reread-globals` variation — see analysis.ts
1536
+ // AnalyzeOptions for why this is a differ-refereed variation and not a fix.
1145
1537
  rereadGlobals?: boolean;
1146
1538
  // Materialize a load that feeds a `cond_br` join arg, so the naming walk can home the join in
1147
1539
  // it and the identity arm elides to a one-sided in-place `if`. Off by default; rank.ts
1148
- // enumerates the ON spelling as the `/inplace` axis — see analysis.ts AnalyzeOptions.
1540
+ // enumerates the ON spelling as the `/inplace` variation — see analysis.ts AnalyzeOptions.
1149
1541
  materializeJoinFeeds?: boolean;
1150
1542
  // Materialize a pure computed address shared by 2+ memory accesses, and the multi-render loads
1151
1543
  // through it, reproducing the source's pointer-local + scalar-temp spelling. Off by default;
1152
- // rank.ts enumerates the ON spelling as the `/addr-home` axis — see analysis.ts AnalyzeOptions.
1544
+ // rank.ts enumerates the ON spelling as the `/addr-home` variation — see analysis.ts AnalyzeOptions.
1153
1545
  homeSharedAddresses?: boolean;
1154
1546
  // Materialize a pure value with 2+ distinct consumers, at least one of them inside a loop the
1155
1547
  // def sits outside — the register the compiler holds across the iterations. Off by default;
1156
- // rank.ts enumerates the ON spelling as the `/expr-home` axis — see analysis.ts AnalyzeOptions.
1548
+ // rank.ts enumerates the ON spelling as the `/expr-home` variation — see analysis.ts AnalyzeOptions.
1157
1549
  homeLoopExprs?: boolean;
1158
1550
  // Materialize a pure value with 2+ consumers standing on a memory read — the register the asm
1159
1551
  // carried the DERIVED value in, where the read's own home is a register that died at the
1160
- // computation. Off by default; rank.ts enumerates the ON spelling as the `/derived-home` axis
1552
+ // computation. Off by default; rank.ts enumerates the ON spelling as the `/derived-home` variation
1161
1553
  // see analysis.ts AnalyzeOptions.
1162
1554
  homeDerivedReads?: boolean;
1163
1555
  // Materialize a pure value that one join's incoming edges render into the SAME parameter slot
1164
1556
  // from 2+ places — the value the source computed once above the branch and the copy machinery
1165
1557
  // sinks into every arm. Off by default; rank.ts enumerates the ON spelling as the `/merge-home`
1166
- // axis — see analysis.ts AnalyzeOptions.
1558
+ // variation — see analysis.ts AnalyzeOptions.
1167
1559
  homeMergeFeeds?: boolean;
1168
1560
  // Emit a memory read as a named temp in ITS OWN block when every place it renders sits in a
1169
- // block that block strictly dominates. A per-compiler DATA lever (TargetDescription
1170
- // .compilerBehaviors), not a differ-refereed axis: where the compiler has neither a scheduler
1561
+ // block that block strictly dominates. A compiler behavior (TargetDescription
1562
+ // .compilerBehaviors), not a differ-refereed variation: where the compiler has neither a scheduler
1171
1563
  // nor a code hoister, the sunk spelling is one it could not have emitted from this asm, so there
1172
1564
  // is nothing to referee. Absent ⇒ off — the target field carries the evidence a compiler owes,
1173
1565
  // analysis.ts AnalyzeOptions the refusals.
@@ -1177,23 +1569,27 @@ export interface StructureOptions {
1177
1569
  // needs signed. Off by default: a signed spelling that byte-matched was PROVED non-negative by
1178
1570
  // the compiler (it emits the unsigned branch from signed compares only then), so which spelling
1179
1571
  // the source used is genuinely ambiguous at emission — rank.ts enumerates the ON spelling as
1180
- // the `/uns-cmp` axis and the differ referees.
1572
+ // the `/uns-cmp` variation and the differ referees.
1181
1573
  unsignedCompareSpelling?: boolean;
1182
1574
  // Merge two variables that a merge copy would join, when the values under them never interfere
1183
1575
  // (structure/namecoalesce.ts). Off by default; rank.ts enumerates the ON spelling as the
1184
- // `/merge-names` axis. Which variables the compiler's own coalescer shared is not derivable from
1576
+ // `/merge-names` variation. Which variables the compiler's own coalescer shared is not derivable from
1185
1577
  // the naming, and removing a copy is worth less than it looks — the compiler coalesces most of
1186
1578
  // them itself. What moves the score is which values share a register, and that splits per
1187
1579
  // function.
1188
1580
  coalesceMergeNames?: boolean;
1189
1581
  // Give a merge whose carrier is a FUNCTION PARAMETER its own local, instead of assigning back
1190
1582
  // into the parameter's name. Off by default; rank.ts enumerates the ON spelling as the
1191
- // `/fresh-merge` axis, and `FRESH_MERGE_GATES` holds the admission and the argument for it.
1583
+ // `/fresh-merge` variation, and `FRESH_MERGE_GATES` holds the admission and the argument for it.
1192
1584
  //
1193
- // NOT `materializeJoinFeeds` widened to parameters. That axis reaches its shape by giving the
1585
+ // NOT `materializeJoinFeeds` widened to parameters. That variation reaches its shape by giving the
1194
1586
  // join's feed a NAME to adopt, and materialization is keyed on the defining `Op` — a parameter
1195
1587
  // has none, so there is nothing to key.
1196
1588
  freshParamMerge?: boolean;
1589
+ // Give a DIVERGENT `if` — one whose arms reach no common block before EXIT — the follow its
1590
+ // non-returning paths share: `followOverReturns` below. Absent, such an `if` keeps its arms
1591
+ // divergent and a region both of them reach is emitted in each.
1592
+ followEarlyReturns?: boolean;
1197
1593
  // How an unresolvable VALUE degrades (a live `opaque`, an unlowered transient op, a dropped def):
1198
1594
  // "strict" (default) — the `"?"` sentinel, tripping assertResolved at the boundary (loud in
1199
1595
  // the PROCESS);
@@ -1213,7 +1609,7 @@ export interface StructureOptions {
1213
1609
  * A SEPARATE FIELD rather than a pre-merged map, and the separation is load-bearing twice.
1214
1610
  * `spellBitfieldMembers` is normalized against `symbols` alone, so a derived shape can never
1215
1611
  * switch the named-bitfield spelling on for a map-less row (which would silently delete the
1216
- * `/no-bitfield` axis's decline — see bitfield-members.test.ts). And it keeps the derivation
1612
+ * `/no-bitfield` variation's decline — see bitfield-members.test.ts). And it keeps the derivation
1217
1613
  * ATTRIBUTABLE: everything the map does stays keyed on the map. */
1218
1614
  inferredSymbols?: Map<string, SymbolInfo>;
1219
1615
  /** THE ORDER HALF of the same derivation (raise/globalshape.ts `orderLicensedGlobals`): the
@@ -1273,13 +1669,27 @@ export interface StructureHooks {
1273
1669
  nameCoalesceGates?: readonly Gate<NameMerge>[];
1274
1670
  /** `freshParamMerge`'s admission rules, ablatable the same way. */
1275
1671
  freshMergeGates?: readonly Gate<FreshMergeCarrier>[];
1672
+ /** `canTakeName`'s admission rules — which name a block parameter may be spelled with. Every
1673
+ * entry is sound, so this exists for the differential test that drops one and watches the
1674
+ * emitted program change, never for a shipped ablation. */
1675
+ carrierNameGates?: readonly Gate<CarrierName>[];
1676
+ /** `enclosingCarrierName`'s admission rules (`ENCLOSING_CARRIER_GATES`), ablatable the same way —
1677
+ * and, wrapped in `tallying`, the census of which rule refuses a nest. */
1678
+ enclosingCarrierGates?: readonly Gate<EnclosingCarrier>[];
1679
+ /** Every branch-sense site this structuring reached, in emission order: the block index
1680
+ * `StructureOptions.branchSenseFlipSites` names, whether the site is JOINED or divergent, and
1681
+ * which sense it actually emitted. The enumeration domain — a site only exists once structuring
1682
+ * has decided both arms are real, so it cannot be computed ahead of the pass. */
1683
+ onBranchSenseSite?: (site: { block: number; ordinal: number; joined: boolean; negated: boolean }) => void;
1684
+ /** Every divergent `if` `followEarlyReturns` gave a follow: the `if`'s block and the follow's. */
1685
+ onEarlyReturnFollow?: (site: { block: number; follow: number }) => void;
1276
1686
  }
1277
1687
 
1278
1688
  /** A CANDIDATE SPELLING MUST NEVER UNLOCK A FUNCTION THE PRIMARY DECLINES. `varName` is not only
1279
1689
  * how values are spelled — the loop emitters' hazard predicates read it, and several ask "does
1280
1690
  * this edge copy survive identity elision", which merging two names quietly answers `no`. A pass
1281
1691
  * that made a hazard invisible would trade a loud decline for a silent wrong answer, so the
1282
- * lever-less structuring runs first and its refusal stands. That is the whole invariant, rather
1692
+ * structuring with no variation on runs first and its refusal stands. That is the whole invariant, rather
1283
1693
  * than a list of individually patched guards, and it costs one extra structuring — nothing next to
1284
1694
  * the compile the candidate exists to feed.
1285
1695
  *
@@ -1287,7 +1697,7 @@ export interface StructureHooks {
1287
1697
  *
1288
1698
  * SCOPE: refusals thrown by `structure()` itself. A decline can also come from `structureChecked`'s
1289
1699
  * boundary contracts, which run OUTSIDE it — `rank.ts` closes that half, where the contracts are. */
1290
- function assertPrimaryAccepts(fn: Fn, opts: StructureOptions, hooks: StructureHooks): void {
1700
+ function assertDefaultAccepts(fn: Fn, opts: StructureOptions, hooks: StructureHooks): void {
1291
1701
  structure(
1292
1702
  fn,
1293
1703
  {
@@ -1301,6 +1711,7 @@ function assertPrimaryAccepts(fn: Fn, opts: StructureOptions, hooks: StructureHo
1301
1711
  homeMergeFeeds: false,
1302
1712
  anchorConstCopies: false,
1303
1713
  anchorLoopEntryConsts: false,
1714
+ followEarlyReturns: false,
1304
1715
  },
1305
1716
  hooks,
1306
1717
  );
@@ -1316,6 +1727,58 @@ interface EarlyReturnArmDeps {
1316
1727
  function isRet(blk: Block): boolean {
1317
1728
  return blk.ops[blk.ops.length - 1]?.opcode === 'ret';
1318
1729
  }
1730
+ /** The `ret`s reachable from BOTH successors of `b` — the region `followEarlyReturns` keeps. Empty
1731
+ * when there is none, and when `b` does not branch two ways. `reachFrom` is forward reachability,
1732
+ * the start block excluded. */
1733
+ function sharedRetsOf(b: Block, reachFrom: (x: Block) => ReadonlySet<Block>): Block[] {
1734
+ const [s1, s2] = b.ops[b.ops.length - 1]?.opcode === 'cond_br' ? successorsOf(b) : [];
1735
+ if (s1 === undefined || s2 === undefined || s1 === s2) {
1736
+ return [];
1737
+ }
1738
+ const retsFrom = (x: Block): Block[] => [x, ...reachFrom(x)].filter(isRet);
1739
+ const fromS2 = new Set(retsFrom(s2));
1740
+ return retsFrom(s1).filter((r) => fromS2.has(r));
1741
+ }
1742
+ /** Is there an `if` whose arms reach no common block before EXIT but share a `ret` — the only shape
1743
+ * `followEarlyReturns` changes? The enumeration gate of both shared-tail twins (rank.ts), asked of
1744
+ * the fn as raised and again of the sunk fn; a superset, since it asks `sharedRetsOf` and none of
1745
+ * the follow's three later refusals. */
1746
+ export function hasDivergentSharedRet(fn: Fn): boolean {
1747
+ const ipdom = postDominators(fn);
1748
+ const reach = new Map<Block, Set<Block>>();
1749
+ const reachFrom = (b: Block): Set<Block> => {
1750
+ let out = reach.get(b);
1751
+ if (out === undefined) {
1752
+ out = new Set<Block>();
1753
+ for (const stack = successorsOf(b); stack.length;) {
1754
+ const x = stack.pop()!;
1755
+ if (!out.has(x)) {
1756
+ out.add(x);
1757
+ stack.push(...successorsOf(x));
1758
+ }
1759
+ }
1760
+ reach.set(b, out);
1761
+ }
1762
+ return out;
1763
+ };
1764
+ return fn.blocks.some((b) => ipdom.get(b) === null && sharedRetsOf(b, reachFrom).length > 0);
1765
+ }
1766
+ /** does a path from `from` reach `to` without passing through `avoid`? */
1767
+ function reachesAvoiding(from: Block, to: Block, avoid: Block): boolean {
1768
+ const seen = new Set<Block>([avoid]);
1769
+ const stack = successorsOf(from);
1770
+ while (stack.length) {
1771
+ const x = stack.pop()!;
1772
+ if (x === to) {
1773
+ return true;
1774
+ }
1775
+ if (!seen.has(x)) {
1776
+ seen.add(x);
1777
+ stack.push(...successorsOf(x));
1778
+ }
1779
+ }
1780
+ return false;
1781
+ }
1319
1782
  // An early `return` out of the loop: forward-walking from `to` WITHOUT re-entering the loop `body`,
1320
1783
  // every path terminates in a `ret`. agbcc/gcc merge every `return` into ONE epilogue block and each
1321
1784
  // return site just sets the return register and branches there, so a second body exit that lands on
@@ -1392,11 +1855,14 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
1392
1855
  coalesceLoopInit = false,
1393
1856
  preserveDivergentBranchSense = true,
1394
1857
  negateJoinedBranchSense = preserveDivergentBranchSense,
1858
+ branchSenseFlipSites,
1859
+ senseFromFoldEvidence = false,
1395
1860
  orderArgCopiesByWriteOrder = true,
1396
1861
  preferDefPosCopyOrder = false,
1397
1862
  switchAllowsNeqCase = true,
1398
1863
  switchAllowsBoundCase = false,
1399
1864
  switchArmsFollowLayout = false,
1865
+ switchRequiresFrontLoadedTests = false,
1400
1866
  spellSwitchFallthrough = true,
1401
1867
  spillSlotOrder,
1402
1868
  defOrderLoadPairs = true,
@@ -1416,6 +1882,7 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
1416
1882
  unsignedCompareSpelling = false,
1417
1883
  coalesceMergeNames = false,
1418
1884
  freshParamMerge = false,
1885
+ followEarlyReturns = false,
1419
1886
  onGap = 'strict',
1420
1887
  symbols: mapSymbols,
1421
1888
  inferredSymbols,
@@ -1432,29 +1899,32 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
1432
1899
  has: (n) => mapSymbols?.has(n) === true || inferredSymbols?.has(n) === true,
1433
1900
  }
1434
1901
  : undefined;
1435
- // Only the MAP makes the named bitfield spelling available, so with no map this is not a choice.
1902
+ // Only the MAP makes the named bitfield spelling available, so with no map this is not a question.
1436
1903
  // Normalized once here rather than left to each reader's own `symCtx &&` guard, because rank.ts's
1437
1904
  // `/no-bitfield` decline rests on both arms structuring the IDENTICAL tree without a map — a
1438
1905
  // second reader added outside that guard would otherwise delete a candidate silently, and nothing
1439
1906
  // reports a candidate that was never enumerated (bitfield-members.test.ts).
1440
1907
  // Against the PROJECT MAP alone, never the derived shapes: only a map carries bitfield members,
1441
- // and keying this on the union would flip the `/no-bitfield` axis's zero point on a map-less row.
1908
+ // and keying this on the union would flip the `/no-bitfield` variation's zero point on a map-less row.
1442
1909
  const spellBitfieldMembers = mapSymbols !== undefined && bitfieldSpellingWanted;
1443
- // These levers all change which edge copies elide as identities (extra materialization does
1910
+ // These options all change which edge copies elide as identities (extra materialization does
1444
1911
  // too), which the loop emitters' hazard predicates read — so the invariant above covers each.
1445
- // A per-compiler DEFAULT is not among them, however much it materializes: the primary IS this
1446
- // target's defaults, so resetting one would probe a spelling asmlift never emits here.
1912
+ // A compiler behavior is not among them, however much it materializes: the default structuring
1913
+ // IS this target's compiler behaviors, so resetting one would probe a spelling asmlift never emits
1914
+ // here.
1447
1915
  //
1448
- // THREE OF rank.ts's TEN `STRUCTURING_AXES` ARE DELIBERATE NON-MEMBERS, each for its own reason,
1449
- // and the list here is the half of the split this side owns:
1916
+ // FOUR OF THE ELEVEN `STRUCTURE_VARIATIONS` ENTRIES (rank-variations.ts) ARE DELIBERATE NON-MEMBERS,
1917
+ // each for its own reason, and the list here is the half of the split this side owns:
1918
+ // - `/site-sense` (senseFromFoldEvidence) decides which way each folded branch is written. It
1919
+ // negates a branch sense per site and touches no copy, like the per-function sense booleans;
1450
1920
  // - `/reread-globals` (rereadGlobals) is an ANALYSIS option, and it only ever RELAXES: it
1451
1921
  // widens a load's render positions and narrows the write set that bars it, so it removes
1452
1922
  // materializations rather than minting them. Extra materialization is what this guard is
1453
- // about (see above), and this axis adds none;
1923
+ // about (see above), and this variation adds none;
1454
1924
  // - `/uns-cmp` (unsignedCompareSpelling) writes `varType` and inserts casts at compares. It
1455
1925
  // touches no name and no copy, so no edge copy changes its elision under it;
1456
1926
  // - `/copy-defpos` (preferDefPosCopyOrder) REORDERS the copies of one edge and adds or drops
1457
- // none. rank.ts states the same thing from the axis side, in the terms that matter there: a
1927
+ // none. rank.ts states the same thing from the variation side, in the terms that matter there: a
1458
1928
  // reordering cannot rescue a spelling whose OFF sibling failed the boundary contracts.
1459
1929
  if (
1460
1930
  coalesceMergeNames ||
@@ -1464,15 +1934,24 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
1464
1934
  homeLoopExprs ||
1465
1935
  homeDerivedReads ||
1466
1936
  homeMergeFeeds ||
1467
- anchorConstCopies
1937
+ anchorConstCopies ||
1938
+ followEarlyReturns
1468
1939
  ) {
1469
- assertPrimaryAccepts(fn, opts, hooks);
1940
+ assertDefaultAccepts(fn, opts, hooks);
1470
1941
  }
1471
1942
  const defs = defOpMap(fn);
1472
1943
  const preds = predecessorBlocks(fn);
1473
1944
  const ipdom = postDominators(fn);
1474
1945
  const dom = dominators(fn);
1475
1946
 
1947
+ /** The byte step `raise/const.ts` recorded on the literal that feeds this access's address, or
1948
+ * `undefined` where the address was not reached by advancing a register. Read at the two
1949
+ * `memAccess` call sites, which are where an IR value becomes an L3 access node. */
1950
+ const advanceStepOf = (base: Value): number | undefined => {
1951
+ const step = defs.get(base)?.attrs.advancedBy;
1952
+ return typeof step === 'number' ? step : undefined;
1953
+ };
1954
+
1476
1955
  // ── analysis phase (structure/analysis.ts): use registry, liveness, materialization ──
1477
1956
  const { useSitesOf, opIndex, opBlock, liveIn, materialize, reachFrom, emitPos, memWriteBetween } = analyze(
1478
1957
  fn,
@@ -1495,6 +1974,54 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
1495
1974
  },
1496
1975
  );
1497
1976
 
1977
+ // THE FOLLOW OF A DIVERGENT `if`, over the paths that do not return early. Post-dominance gives
1978
+ // such an `if` no join — its arms reach two different `ret`s, and EXIT is the only block on every
1979
+ // path — so each arm is structured to its end and a region both arms reach is emitted in both.
1980
+ // When the compiler emitted that region ONCE, the source can have written it once, after the
1981
+ // `if`, with every other path into a `ret` an early `return;` — `synthetic:gcseinner`, where
1982
+ // agbcc keeps the `fnA` arm's own `ret` and stores the default once. An option, enumerated as
1983
+ // rank.ts's `/shared-ret` twin, and as its `/shared-tail` twin after the store-tail sink: as a
1984
+ // default it costs rows, measured there.
1985
+ //
1986
+ // WHICH REGION: the `ret`s reachable from BOTH successors. Every block that cannot reach one of
1987
+ // them is an early-return region and is deleted; the follow is `b`'s post-dominator over what is
1988
+ // left. Sound for the same reason an ordinary follow is: every kept path from `b` passes it, and a
1989
+ // deleted block reaches no kept `ret` and so never the follow, so each arm structures up to the
1990
+ // follow or into a `return`. And no deleted region is reachable from both arms — its `ret` would
1991
+ // then be one of the shared ones — so the rule duplicates nothing across the arms.
1992
+ //
1993
+ // PER `if`, never function-wide: the deletion set is a function of that `if`'s own shared `ret`s
1994
+ // (memoized on them), because a nested `if` shares a different set, or none.
1995
+ //
1996
+ // REFUSES (keeping the divergent arms) when no `ret` is reachable from both successors, when the
1997
+ // kept graph gives `b` no post-dominator but EXIT, and when the enclosing region's `stop` is
1998
+ // reachable from `b` without passing the follow — that path must reach `stop`, and structuring
1999
+ // the arms towards a different follow would emit `stop`'s region inside an arm. The caller asks
2000
+ // only outside a loop body.
2001
+ const followsByShared = new Map<string, Map<Block, Block | null>>();
2002
+ const followOverReturns = (b: Block, stop: Block | null): Block | null => {
2003
+ const shared = sharedRetsOf(b, reachFrom);
2004
+ if (shared.length === 0) {
2005
+ return null;
2006
+ }
2007
+ const key = shared
2008
+ .map((r) => fn.blocks.indexOf(r))
2009
+ .sort((x, y) => x - y)
2010
+ .join(',');
2011
+ let pd = followsByShared.get(key);
2012
+ if (pd === undefined) {
2013
+ const keep = new Set(fn.blocks.filter((x) => shared.some((r) => r === x || reachFrom(x).has(r))));
2014
+ pd = postDominators(fn, keep);
2015
+ followsByShared.set(key, pd);
2016
+ }
2017
+ const follow = pd.get(b) ?? null;
2018
+ if (follow === null || (stop !== null && follow !== stop && reachesAvoiding(b, stop, follow))) {
2019
+ return null;
2020
+ }
2021
+ hooks.onEarlyReturnFollow?.({ block: fn.blocks.indexOf(b), follow: fn.blocks.indexOf(follow) });
2022
+ return follow;
2023
+ };
2024
+
1498
2025
  // SCALAR-vs-AGGREGATE globals: a `gaddr` symbol accessed EXCLUSIVELY at offset 0 is a scalar
1499
2026
  // global → the bare name `gSym` (byte-exact, matches the source). A symbol accessed at any
1500
2027
  // non-zero offset (or via a variable index) is an array/struct global → EVERY access uses the
@@ -1972,12 +2499,16 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
1972
2499
  // AND THE CONVERSE: the name must not be WRITTEN anywhere `p` itself is live. Every other
1973
2500
  // block param under the name is such a write — its in-edge copies execute at each
1974
2501
  // predecessor's end, and a LOOP header's update copy is emitted inside the loop body, where it
1975
- // also runs on the final (exiting) iteration — so the test is `p` live into the writer's block
1976
- // OR live out of any of its predecessors (the conservative union covers that placement). A
1977
- // materialized def under the name writes at its own block. This applies even to a
1978
- // redundant-phi alias (`pureAlias` waives only the value-at-B check: aliasing is sound at B's
1979
- // entry, but a later write to the shared name still splits them — e.g. a saved pre-increment
1980
- // `i` read post-loop).
2502
+ // also runs on the final (exiting) iteration — so the test is `p` live out of any of the writing
2503
+ // block's predecessors, the conservative union that covers that placement. A materialized def
2504
+ // under the name writes at its own block. This applies even to a redundant-phi alias
2505
+ // (`pureAlias` waives only the value-at-B check: aliasing is sound at B's entry, but a later
2506
+ // write to the shared name still splits them — e.g. a saved pre-increment `i` read post-loop).
2507
+ //
2508
+ // EXCEPT WHERE THE WRITE STORES `p`. A predecessor all of whose edges hand that slot `p` itself
2509
+ // writes `name = name` once the two share the name, so it stores nothing to clobber and
2510
+ // relocating it reaches nowhere. That predecessor is a loop's own accumulator update, and
2511
+ // without the exception the rule calls a value a clobber of itself.
1981
2512
  const paramBlock = new Map<Value, Block>();
1982
2513
  for (const blk of fn.blocks) {
1983
2514
  for (const pv of blk.params) {
@@ -2006,44 +2537,54 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
2006
2537
  // admitted it — a silent wrong answer, not a worse score. At 32 bits the two spellings ARE the
2007
2538
  // same bytes at a read, which is the mismatch `structure/namecoalesce.ts`'s header names and this
2008
2539
  // rule deliberately still tolerates.
2009
- const canTakeName = (p: Value, B: Block, name: string, pureAlias = false): boolean => {
2010
- if (carrierWidth(varType.get(name)) !== carrierWidth(p.type)) {
2011
- return false;
2012
- }
2013
- if (carrierSign(varType.get(name)) !== carrierSign(p.type)) {
2014
- return false;
2015
- }
2016
- if (B.params.some((q) => q !== p && varName.get(q) === name)) {
2017
- return false;
2018
- }
2540
+ //
2541
+ // THE RULES ARE A TABLE (`CARRIER_NAME_GATES`), so each one can be dropped and the pass re-run on
2542
+ // real input — see docs/level-tower.md. What is computed here is the EVIDENCE; which evidence
2543
+ // refuses is the table's to say.
2544
+ const carrierScan = (p: Value, B: Block, name: string): { carrierLive: boolean; carrierWritten: boolean } => {
2019
2545
  const lin = liveIn.get(B)!;
2546
+ let carrierLive = false;
2547
+ let carrierWritten = false;
2020
2548
  for (const [v, n] of varName) {
2021
2549
  if (n !== name || v === p) {
2022
2550
  continue;
2023
2551
  }
2024
- if (!pureAlias && lin.has(v)) {
2025
- return false;
2026
- } // v still live at B → p's copies clobber it
2552
+ carrierLive ||= lin.has(v); // v still live at B → p's copies clobber it
2027
2553
  const wblk = paramBlock.get(v);
2028
2554
  if (wblk && wblk !== entry) {
2029
- // v is a param → `name` written at wblk's edges
2030
- if (liveIn.get(wblk)!.has(p)) {
2031
- return false;
2555
+ // v is a param → `name` is written by the in-edge copies into wblk, and by any relocation
2556
+ // of one of them. AN EDGE THAT HANDS THIS SLOT `p` ITSELF IS NOT A WRITE: once the two
2557
+ // share the name the copy reads `name = name`, so it stores nothing to clobber and
2558
+ // relocating it reaches nowhere. Per PREDECESSOR rather than per edge, because the
2559
+ // relocation is a property of the predecessor's placement — a terminator with two edges
2560
+ // into wblk is exempt only if BOTH hand it `p`.
2561
+ const slot = wblk.params.indexOf(v);
2562
+ const storesOther = new Map<Block, boolean>();
2563
+ for (const { pred, succ } of inEdgeRecords(preds, wblk)) {
2564
+ storesOther.set(pred, (storesOther.get(pred) ?? false) || succ.args[slot] !== p);
2032
2565
  }
2033
- for (const pr of preds.get(wblk) ?? []) {
2566
+ for (const [pr, other] of storesOther) {
2567
+ if (!other) {
2568
+ continue;
2569
+ }
2034
2570
  for (const s of successorsOf(pr)) {
2035
- if (liveIn.get(s)!.has(p)) {
2036
- return false;
2037
- }
2571
+ carrierWritten ||= liveIn.get(s)!.has(p);
2038
2572
  }
2039
2573
  }
2040
2574
  }
2041
2575
  const d = defs.get(v);
2042
- if (d && materialize.has(d) && liveIn.get(opBlock.get(d)!)!.has(p)) {
2043
- return false;
2576
+ carrierWritten ||= !!d && materialize.has(d) && liveIn.get(opBlock.get(d)!)!.has(p);
2577
+ if (carrierLive && carrierWritten) {
2578
+ break;
2044
2579
  }
2045
2580
  }
2046
- // AND A VALUE NOBODY NAMED IS STILL A READER OF THIS NAME. The loop above asks which NAMED
2581
+ return { carrierLive, carrierWritten };
2582
+ };
2583
+ const canTakeName = (p: Value, B: Block, name: string, pureAlias = false): boolean => {
2584
+ const lin = liveIn.get(B)!;
2585
+ let scan: ReturnType<typeof carrierScan> | undefined;
2586
+ const scanned = (): ReturnType<typeof carrierScan> => (scan ??= carrierScan(p, B, name));
2587
+ // AND A VALUE NOBODY NAMED IS STILL A READER OF THIS NAME. `carrier-live` asks which NAMED
2047
2588
  // values are live at `B`; an unnamed one is not stored anywhere, it is RE-DERIVED at its use
2048
2589
  // from whatever its operands are called then — so a value live into `B` whose inlined
2049
2590
  // expression mentions `name` reads the merge's assignment instead of what it was defined from.
@@ -2052,29 +2593,41 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
2052
2593
  // computes 10 — agbcc emits exactly that asm, so this is not a generated-IR curiosity. The
2053
2594
  // walk stops at any value with a name of its own (it reads THAT name) and at a materialized
2054
2595
  // def (it is assigned at its own position, which the clause above already judges).
2055
- if (!pureAlias) {
2056
- const reDerives = (w: Value, seen: Set<Value>): boolean => {
2057
- if (w === p || seen.has(w)) {
2058
- return false;
2059
- }
2060
- seen.add(w);
2061
- const nm = varName.get(w);
2062
- if (nm !== undefined) {
2063
- return nm === name;
2064
- }
2065
- const d = defs.get(w);
2066
- if (!d || materialize.has(d)) {
2067
- return false;
2068
- }
2069
- return d.operands.some((o) => reDerives(o, seen));
2070
- };
2071
- for (const w of lin) {
2072
- if (reDerives(w, new Set())) {
2073
- return false;
2074
- }
2596
+ const reDerives = (w: Value, seen: Set<Value>): boolean => {
2597
+ if (w === p || seen.has(w)) {
2598
+ return false;
2075
2599
  }
2076
- }
2077
- return true;
2600
+ seen.add(w);
2601
+ const nm = varName.get(w);
2602
+ if (nm !== undefined) {
2603
+ return nm === name;
2604
+ }
2605
+ const d = defs.get(w);
2606
+ if (!d || materialize.has(d)) {
2607
+ return false;
2608
+ }
2609
+ return d.operands.some((o) => reDerives(o, seen));
2610
+ };
2611
+ return (
2612
+ firstRejection(hooks.carrierNameGates ?? CARRIER_NAME_GATES, {
2613
+ pureAlias,
2614
+ widthDiffers: carrierWidth(varType.get(name)) !== carrierWidth(p.type),
2615
+ signDiffers: carrierSign(varType.get(name)) !== carrierSign(p.type),
2616
+ siblingHolds: B.params.some((q) => q !== p && varName.get(q) === name),
2617
+ get carrierLive() {
2618
+ return scanned().carrierLive;
2619
+ },
2620
+ get carrierWritten() {
2621
+ return scanned().carrierWritten;
2622
+ },
2623
+ get reDerivesName() {
2624
+ // NAMED live values are `carrier-live`'s, so the two rules stay disjoint and each one's
2625
+ // ablation is its own claim. The walk itself still crosses into a named OPERAND, which
2626
+ // is the whole point of it.
2627
+ return [...lin].some((w) => !varName.has(w) && reDerives(w, new Set()));
2628
+ },
2629
+ }) === null
2630
+ );
2078
2631
  };
2079
2632
  // Does the edge `pr -> b` hand `c` over as a loop variable's PRE-update value? True when `c` is a
2080
2633
  // loop header's own param and the edge leaves the loop from a latch of an emitter that places
@@ -2105,13 +2658,146 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
2105
2658
  const k = header!.params.indexOf(c);
2106
2659
  return !!back && k >= 0 && back.args[k] !== c && !back.args.includes(c);
2107
2660
  };
2661
+ // A NESTED LOOP'S CARRIED VALUE KEEPS THE NAME ITS ENCLOSING LOOP GAVE IT. `p`, a param of the
2662
+ // inner header, is entered straight from the enclosing loop's header `E` with `E`'s own param —
2663
+ // an accumulator (or any loop-carried value) crossing into the inner loop. Minting `p` a fresh
2664
+ // name spells `v3 = v1; do { … v3 … } while (…); v1 = v3;`, and on agbcc that pair is two `mov`s
2665
+ // the target does not contain. Whether the source had ONE variable or TWO is what the frontend's
2666
+ // write-order record answers: `E` wrote nothing into `p`'s key, so the machine carried the value
2667
+ // into the inner loop in the register it already had — there is no copy to reproduce.
2668
+ //
2669
+ // THE NARROW FORM OF A PROXY THAT WAS MEASURED AND LOST. `target.ts`'s MIPS_GCC note records the
2670
+ // wide form — adopt the entry value's name whenever the forward predecessor did not write the
2671
+ // param's key — moving 36 of 736 synthetic rows for four matches lost net, because a predecessor
2672
+ // that COMPUTES the initial value into the param's own register writes the key and still
2673
+ // coalesces. Here the argument is `E`'s own parameter, which `E` does not compute: `E` can write
2674
+ // `p`'s key only by moving that value into it, which is the copy the two-variable spelling spells.
2675
+ // So in this scope the record answers the question exactly, and the measured reach is 4 rows over
2676
+ // the 1,036 of the corpus, all four moved toward the target.
2677
+ //
2678
+ // The argument above is an agbcc one (two `mov`s). The rule has no compiler gate, and it reaches
2679
+ // no corpus row on any other toolchain (0 of the MIPS and PPC rows change), so for those the claim
2680
+ // is UNMEASURED rather than established.
2681
+ //
2682
+ // THE REFUSALS ARE A TABLE (`ENCLOSING_CARRIER_GATES`, in table order), and then `canTakeName`'s.
2683
+ // `p` keeps the seeding below when:
2684
+ // • `one-forward-entry` / `enclosing-header`: `p` has more than one forward predecessor, or its
2685
+ // one forward predecessor is not the header of a loop that strictly encloses `p`'s header.
2686
+ // That is a limit of the DATUM, not of the hazard: the record is keyed by successor params,
2687
+ // and a single-predecessor block has none, so it cannot say whether a block between `E` and
2688
+ // the inner header made the copy. It is also what holds the rule to the unguarded,
2689
+ // constant-trip nest: nestacc1 with its inner bound `j < 7` made `j < n` is entered through
2690
+ // its guard and keeps both copies (measured, agbcc). The register-key identity the MIPS_GCC
2691
+ // note names (`frontend/ssa.ts` `phiKey`) is the datum that would lift it;
2692
+ // • `enclosing-param`: the argument is not one of `E`'s own params (it is not `E`'s
2693
+ // loop-carried value);
2694
+ // • `key-written`: the frontend did not measure `E`, or measured it WRITING `p`'s key — the
2695
+ // copy the source spelled, which the fresh name reproduces;
2696
+ // • `carried-by-one-loop`: the value is not carried by BOTH loops (`carriedByBothLoops`, below).
2697
+ // The one SOUND rule of the five; the four above bound the evidence, and dropping one spells
2698
+ // a different program that is still correct;
2699
+ // • `canTakeName` refuses. With the clause above, these are the guards against the collision
2700
+ // `enclosingNames` excludes wholesale: a value of `E` still read after the inner loop is
2701
+ // `carrier-live`, and an unnamed one re-derived from it there (the outer update
2702
+ // `(u8)(v0 + 1)`) is `re-derives`. `canTakeName` alone is NOT enough — it reads `varName`
2703
+ // only, and sharing the name reaches two readers that are not in it.
2704
+ //
2705
+ // THE VALUE MUST BE CARRIED BY BOTH LOOPS — the per-site form of `structure/namecoalesce.ts`'s
2706
+ // `loop-escape` premise. Sharing the name hands it to more values than `p`: the inner back edge's
2707
+ // argument takes it through `backArgName` (unconditionally, in `seedLoopParams`), and `E`'s own
2708
+ // back-edge argument for `a`'s slot takes it as the outer loop's un-rotation alias. If the outer
2709
+ // back edge hands `a`'s slot something the inner loop did not produce, the outer update copy
2710
+ // overwrites the name the inner loop's value is read under, and a merge after the loop that
2711
+ // adopts that value's `backArgName` reads the outer value instead. That is the frozen
2712
+ // `INNER_CLOBBERS_OUTER` pair (`fz5104`, `fz6437`, in `test/loop-escape-witnesses.ts`, replayed
2713
+ // against this rule by `nested-carrier.test.ts`): given ONE realistic record fact — `E`
2714
+ // did not write `p`'s key, nestacc1's own shape — the rule without this clause emits both as a
2715
+ // different program, and nothing throws. So every in-edge of `E` from inside its loop must hand
2716
+ // `a`'s slot either `a` (then `a` is live across the inner loop, and `carrier-live` refuses), `p`
2717
+ // (what the name holds when a test-at-top `while` exits from its header; after a bottom-tested
2718
+ // loop it is a pre-update read, the inner emitter's own hazard and the same one whichever name
2719
+ // `p` has), an inner back-edge argument for `p`, or a merge every in-edge of which hands it one of
2720
+ // those. One level of merge, not a closure: a deeper chain refuses, which costs reach and never
2721
+ // soundness.
2722
+ const carriedByBothLoops = (p: Value, i: number, header: Block, E: Block, a: Value): boolean => {
2723
+ const outer = forest.byHeader.get(E);
2724
+ const inner = forest.byHeader.get(header);
2725
+ if (!outer || !inner) {
2726
+ return false;
2727
+ }
2728
+ const carried = new Set<Value>([a, p]);
2729
+ for (const { pred, succ } of inEdgeRecords(preds, header)) {
2730
+ if (inner.body.has(pred)) {
2731
+ carried.add(succ.args[i]);
2732
+ }
2733
+ }
2734
+ const k = E.params.indexOf(a);
2735
+ if (k < 0) {
2736
+ return false; // not `E`'s loop-carried value — `enclosing-param`'s question, asked again
2737
+ }
2738
+ let backEdges = 0;
2739
+ for (const { pred, succ } of inEdgeRecords(preds, E)) {
2740
+ if (!outer.body.has(pred)) {
2741
+ continue;
2742
+ }
2743
+ backEdges++;
2744
+ const r = succ.args[k];
2745
+ if (carried.has(r)) {
2746
+ continue;
2747
+ }
2748
+ const rb = paramBlock.get(r);
2749
+ if (rb === undefined || rb === entry) {
2750
+ return false;
2751
+ }
2752
+ const j = rb.params.indexOf(r);
2753
+ let ins = 0;
2754
+ for (const { succ: s } of inEdgeRecords(preds, rb)) {
2755
+ ins++;
2756
+ if (!carried.has(s.args[j])) {
2757
+ return false;
2758
+ }
2759
+ }
2760
+ if (ins === 0) {
2761
+ return false;
2762
+ }
2763
+ }
2764
+ return backEdges > 0;
2765
+ };
2766
+ const enclosingCarrierName = (
2767
+ p: Value,
2768
+ i: number,
2769
+ header: Block,
2770
+ forwardPreds: readonly Block[],
2771
+ ): string | undefined => {
2772
+ const [E] = forwardPreds;
2773
+ const a = E === undefined ? undefined : successorTo(E, header)?.args[i];
2774
+ const order = fn.writeOrder;
2775
+ const refused = firstRejection(hooks.enclosingCarrierGates ?? ENCLOSING_CARRIER_GATES, {
2776
+ oneForwardEntry: E !== undefined && E !== header && forwardPreds.every((fp) => fp === E),
2777
+ get entryEncloses() {
2778
+ return E !== undefined && forest.byHeader.get(E)?.body.has(header) === true;
2779
+ },
2780
+ get argIsEnclosingParam() {
2781
+ return a !== undefined && E!.params.includes(a);
2782
+ },
2783
+ get keyUnwritten() {
2784
+ return E !== undefined && order?.writes.has(E) === true && order.lastWrite.get(E)?.has(p) !== true;
2785
+ },
2786
+ get carriedByBoth() {
2787
+ return E !== undefined && a !== undefined && carriedByBothLoops(p, i, header, E, a);
2788
+ },
2789
+ });
2790
+ const nm = a === undefined ? undefined : varName.get(a);
2791
+ return refused === null && nm !== undefined && canTakeName(p, header, nm) ? nm : undefined;
2792
+ };
2108
2793
  // ONE seeding routine for self-loop and structured-loop headers. On a coalesceLoopInit target,
2109
2794
  // keep the induction variable in its entry (forward-edge) value's register — reproducing a
2110
2795
  // compiler that mutates the arg register across the loop instead of copying to a fresh local,
2111
2796
  // so the init copy vanishes. The loop mutates the adopted name every iteration — canTakeName
2112
2797
  // declines it when any value under it is still live at the header. `exclude` are names never to
2113
- // adopt (enclosing loops' induction vars — the cross-level collision below); every seeded
2114
- // param's name is ADDED to it, so sibling params can't collapse.
2798
+ // adopt (enclosing loops' induction vars — the cross-level collision below) except through
2799
+ // `enclosingCarrierName`, which measures the collision instead; every seeded param's name is
2800
+ // ADDED to it, so sibling params can't collapse.
2115
2801
  const seedLoopParams = (
2116
2802
  header: Block,
2117
2803
  forwardPreds: Block[],
@@ -2130,6 +2816,7 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
2130
2816
  }
2131
2817
  }
2132
2818
  }
2819
+ name ??= enclosingCarrierName(p, i, header, forwardPreds);
2133
2820
  // A MATERIALIZED back-edge arg is this variable's in-place update (`add r4, r4, r0`
2134
2821
  // mutates the same register the param lives in) — adopt its name so the def assigns the
2135
2822
  // loop variable directly and the update copy elides. Sound only when every read of the
@@ -2206,7 +2893,8 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
2206
2893
  // loop (the outer latch reads it after). If the inner var is coalesced onto the outer var's name
2207
2894
  // (its init reads the outer var), the inner loop would MUTATE the outer variable — a silent
2208
2895
  // miscompile. Process OUTERMOST-first (so an enclosing loop is named first) and, per loop, exclude
2209
- // the names of every enclosing loop's header params from the coalescing candidates.
2896
+ // the names of every enclosing loop's header params from the coalescing candidates — all but the
2897
+ // one `enclosingCarrierName` hands over with the evidence and the `canTakeName` check above.
2210
2898
  // `enclosingNames(l)` = names of params of headers whose natural body strictly contains `l.header`.
2211
2899
  structuredLoops.sort((a, b) => b.body.size - a.body.size); // outermost first
2212
2900
  const enclosingNames = (l: { header: Block; body: Set<Block> }): Set<string> => {
@@ -2264,7 +2952,7 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
2264
2952
  if (carriesPreUpdate(c.v, c.pr, b) || !canTakeName(p, b, nm, allSame)) {
2265
2953
  continue;
2266
2954
  }
2267
- // `freshParamMerge` (the `/fresh-merge` axis): this merge takes its own home rather
2955
+ // `freshParamMerge` (the `/fresh-merge` variation): this merge takes its own home rather
2268
2956
  // than the carrier's name — `FRESH_MERGE_GATES` above holds the admission and the
2269
2957
  // argument for it. Absent the option nothing changes.
2270
2958
  if (
@@ -2382,7 +3070,7 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
2382
3070
  }
2383
3071
  // Params never reconcile: their declarations come from p.type, not varType, so a flip here
2384
3072
  // would only desync the cast site's view from the emitted declaration — and param signedness
2385
- // is the sign-pin axis's dimension.
3073
+ // is the signedness variation's dimension.
2386
3074
  const paramNames = new Set(entry.params.map((_, i) => `a${i}`));
2387
3075
  const claimants = new Map<string, Value[]>();
2388
3076
  for (const [v, n] of [...varName, ...backArgName]) {
@@ -2665,13 +3353,13 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
2665
3353
  l = { k: 'cast', to: T.u(32), e: l };
2666
3354
  }
2667
3355
  }
2668
- // The SIGNED direction of the same hole, and a DEFAULT rather than an arm of that axis
3356
+ // The SIGNED direction of the same hole, and a DEFAULT rather than an alternative of that variation
2669
3357
  // not because nothing underdetermines, but because the underdetermination is INERT. An
2670
3358
  // unsigned source compare can reach a signed opcode when the compiler proves the test is
2671
3359
  // the sign bit (`u32 a; a < 0x80000000` compiles to `cmp r0, #0; bge`; kmc-gcc and gcc
2672
3360
  // 2.7.2 fold it to `slti`), so an icmp_s* has more than one source — but the pinned
2673
3361
  // spelling reproduces that branch too (`(s32)a >= 0` is the same `cmp r0, #0; bge`), so
2674
- // both sources reach ONE candidate and an axis would have doubled the fan to referee a
3362
+ // both sources reach ONE candidate and a variation would have doubled the fan to referee a
2675
3363
  // question with one answer. Where the spellings genuinely diverge they diverge the way the
2676
3364
  // opcode says, on every toolchain: an operand that renders unsigned makes C compare
2677
3365
  // unsigned (agbcc `bls`, IDO/kmc-gcc/gcc 2.7.2 `sltu`/`sltiu` against `slt`/`slti`, mwcc
@@ -2879,6 +3567,50 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
2879
3567
  // COMPARISON (`gPtr < K` — C compares unsigned whatever the asm's icmp_s* said) is the same
2880
3568
  // class as intifyAddrCmp's `addr` rule and is deliberately left alone here: it is valid C
2881
3569
  // today, so closing it would churn spellings for a signedness case no row exercises.
3570
+ //
3571
+ // A `+`/`|` over two IR `const`s that both RENDER as literals is the literal it is. After
3572
+ // pre-recovery there is only one way such an op still exists: `raise/const.ts` refuses to fold
3573
+ // one shape — a register the compiler held live across a branch, whose value on this path is a
3574
+ // constant — so that `/merge-home` can enumerate the hoisted init (`v = 0; if (c) v = v + 1;`).
3575
+ // A candidate that does NOT home the register inlines both operands and would ship
3576
+ // `v = 0 + 1;`: `synthetic:fib:gcc2.7.2kmc` emits exactly that without this fold and scores
3577
+ // diff:12 either way, because the target compiler folds the constant expression and no score
3578
+ // gate can see the difference. The artifact a decomp author pastes into a repo is what is at
3579
+ // stake, so `const-fold.test.ts` pins this on the emitted STRING. Re-folding HERE rather than
3580
+ // back in the IR is the point: the pair must survive pre-recovery for the enumeration gate to
3581
+ // see the merge feed, and only at rendering is it settled that this candidate named neither
3582
+ // half.
3583
+ //
3584
+ // `foldsFromIrConsts` keeps the reach honest and is not redundant with `l`/`r` being `const`
3585
+ // Exprs: a BLOCK PARAMETER resolved to a constant on this arm also renders as a literal, and
3586
+ // folding those is a different and unmeasured decision. Instrumented over the real pipeline it
3587
+ // fires on `synthetic:sinkacc:agbcc` and on ZERO renders of the 16 MATCH rows whose emitted
3588
+ // source carries a literal pair today — theirs are address trees whose IR defs are not consts.
3589
+ // It reads through the fold's OWN opcodes, and recursively, because the residue is not flat:
3590
+ // `add(add(const 0, const 1), const 2)` would otherwise ship `1 + 2`. That cannot widen the
3591
+ // reach past the refusal's residue — any other const/const `add`/`or` `raise/const.ts` already
3592
+ // folded, and nothing outside `frontend/` constructs one afterwards. The fold itself is
3593
+ // `foldConstPair`, not a copy: which opcodes fold and how the result is normalised to int32 is
3594
+ // ONE decision, and a third `FOLD` entry must not silently leave its residue unrepaired here.
3595
+ const foldsFromIrConsts = (v: Value): boolean => {
3596
+ const dv = defs.get(v);
3597
+ if (!dv) {
3598
+ return false; // a block parameter: not this refusal's residue
3599
+ }
3600
+ if (dv.opcode === 'const') {
3601
+ return true;
3602
+ }
3603
+ return (
3604
+ isConstFoldOpcode(dv.opcode) && dv.operands.length === 2 && dv.operands.every((o) => foldsFromIrConsts(o))
3605
+ );
3606
+ };
3607
+ if (l.k === 'const' && r.k === 'const' && d.operands.every((o) => foldsFromIrConsts(o))) {
3608
+ const value = foldConstPair(d.opcode, l.value, r.value);
3609
+ if (value !== null) {
3610
+ const folded: Expr = { k: 'const', value };
3611
+ return restoreTo ? { k: 'cast', to: restoreTo, e: folded } : folded;
3612
+ }
3613
+ }
2882
3614
  const sum: Expr = { k: 'bin', op, l, r };
2883
3615
  return restoreTo ? { k: 'cast', to: restoreTo, e: sum } : sum;
2884
3616
  }
@@ -2968,6 +3700,8 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
2968
3700
  ctype,
2969
3701
  scalarGlobals,
2970
3702
  symCtx,
3703
+ false,
3704
+ advanceStepOf(d.operands[0]),
2971
3705
  );
2972
3706
  }
2973
3707
  // aload carries a runtime index operand (variable-index array access) — `base[index]`, or
@@ -3065,7 +3799,7 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
3065
3799
  // written at the const's own def site instead of on the edges (anchorConstCopies, above), and
3066
3800
  // that site dominates them. Its block is a write site for the name like any other, and without it
3067
3801
  // `v0 = 0; if (c) { } store v0;` drops the undefined arm's copy and stores 0 where the machine
3068
- // stores whatever the arm left — the same substitution the parameter case makes, one axis over.
3802
+ // stores whatever the arm left — the same substitution the parameter case makes, one step over.
3069
3803
  //
3070
3804
  // THE SECOND RELOCATION goes the other way, and this test cannot see it at all. A SUNK pre-update
3071
3805
  // exit copy (preUpdateCopies) writes the loop EXIT's param at the top of the loop BODY, so the
@@ -3221,7 +3955,7 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
3221
3955
  * PER NAME the dispatch binds — which is not once per machine write: `sw_fall`'s three arms
3222
3956
  * each take the accumulator under a name of their own, so this tree emits `v0 = 0; v1 = 0;
3223
3957
  * v2 = 0;` where agbcc has a single `mov r1, #0`, and the one-local spelling that byte-matches
3224
- * comes from the `/merge-home` ranked axis, not from here. What the position is for is the
3958
+ * comes from the `/merge-home` ranked variation, not from here. What the position is for is the
3225
3959
  * fall-through chain: per-arm copies RE-RUN on the fall path and overwrite what the falling arm
3226
3960
  * computed, the hazard Regime B states at its own `switch_br` refusal.
3227
3961
  *
@@ -3359,6 +4093,194 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
3359
4093
  return isPtr ? { k: 'cast', to: T.ptr(T.void()), e: value } : value;
3360
4094
  };
3361
4095
 
4096
+ /** THE REFUSAL for the read half of `unreadResult`. TWO questions, and the statement is spelled
4097
+ * only where BOTH answer yes; the second is not implied by the first.
4098
+ *
4099
+ * 1. EVIDENCE — would a source plausibly have declared this access `volatile`? The answer has
4100
+ * to come from DATA: the target's declared device-register window
4101
+ * (`capabilities.deviceRegisters`) or the symbol map's own `volatile` on the named global.
4102
+ * `volatile` is a CORRECTNESS claim about an address, not a spelling preference, and
4103
+ * asserting one about ordinary RAM is a wrong answer rather than a wrong spelling.
4104
+ * 2. REACHABILITY — will the spelling this access gets CARRY a qualifier, here or in some
4105
+ * candidate enumerated from this tree? The payoff of spelling a dead read is that a
4106
+ * qualifier can land on it and the differ can referee the pair; where none can, the
4107
+ * statement is a permanent bare deref in the DEFAULT source, which is what the playground
4108
+ * pins and what a decomp author copies. This question refuses far less than question 1:
4109
+ * `/volatile` qualifies an EWRAM or ROM address quite happily.
4110
+ *
4111
+ * THE MAP ARM needs the read spelled through the global's own NAME, which is where the map's
4112
+ * qualifier lands — memAccess's two name-carrying arms for a global, the bare scalar
4113
+ * (`gStatus;`) and the declared struct MEMBER (`gState.ctl;`). A CAST spelling
4114
+ * (`((s32 *)&REG_DMA3SAD)[2]`) has thrown the qualifier away in the spelling itself, whatever
4115
+ * the declaration says. The member arm asks the CONTAINER's qualifier and not the member's own
4116
+ * (`SymbolStructField.volatile`), which looks backwards and is not: `memberQualsAllow` above
4117
+ * refuses to NAME a volatile member at all, so a `vu16` member is spelled `((s32 *)&gSym)[k]`
4118
+ * with nothing in the spelling for a variation to hold, while `volatile struct S gSym;` qualifies
4119
+ * every member and `gSym.ctl;` really is an observable read. Every other map spelling refuses —
4120
+ * `gPtr->member`, a bare-name array element, a multidimensional subscript — because
4121
+ * over-refusing costs a SPELLING and admitting wrongly costs an ANSWER, this file's standing
4122
+ * asymmetry.
4123
+ *
4124
+ * THE LITERAL ARM needs the base value to have a use OTHER than this read: l3/volatileptr.ts
4125
+ * qualifies a pointer LOCAL, and l3/basecse.ts only mints that local for a base something else
4126
+ * also touches. A single-access read — `*(s32 *)0x04000200;`, the `REG_IF` acknowledge idiom,
4127
+ * and the shape a WRONG `returnsVoid` on a register accessor produces — has no local to qualify
4128
+ * and never will, however plainly its address is a device register. "Some other use" is
4129
+ * NECESSARY for that local, not sufficient; the gate states the necessary half.
4130
+ *
4131
+ * ONLY `load` REACHES EITHER ARM. `aload` carries its index in `operands[1]` and has no `off`
4132
+ * attr at all, so both address queries would answer for the BARE BASE — `globalCellOf` resolves
4133
+ * a base and discards the index by construction (ir/alias.ts), `constAddressOf` sees the literal
4134
+ * with `off` defaulted to 0 — which admits `volatile s32 *p0 = (s32 *)0x04000000; p0[a0];`, a
4135
+ * qualified access at an address the declared window does not cover. The whitelist is by OPCODE
4136
+ * so a read op added later refuses until someone answers both questions for it.
4137
+ *
4138
+ * THE ARMS DO NOT SHARE A POPULATION. With a symbol map the frontend spells a pool word as
4139
+ * `gaddr`, so `constAddressOf` returns null and the literal arm inhabits only `/raw-globals`,
4140
+ * which re-structures with NO map. The map arm is the default-source one, and a map-fed DMA
4141
+ * function spells no read at all — its wait-read is cast-spelled (see `BASECSE_GATES`'
4142
+ * `repeated-const-offset`, whose base local this refusal hands back). */
4143
+ const volatileQualifiable = (op: Op): boolean => {
4144
+ if (op.opcode !== 'load') {
4145
+ return false;
4146
+ }
4147
+ const off = typeof op.attrs.off === 'number' ? op.attrs.off : 0;
4148
+ const width = op.attrs.width as number;
4149
+ const cell = globalCellOf(defs, op.operands[0], off);
4150
+ if (cell) {
4151
+ const si = symbols?.get(cell.name);
4152
+ if (si === undefined) {
4153
+ return false;
4154
+ }
4155
+ if (si.shape === 'struct') {
4156
+ // the SAME find memAccess's struct arm makes, so the two cannot disagree about which
4157
+ // accesses reach the `gSym.field` spelling this arm's qualifier rides on
4158
+ const fld = symCtx
4159
+ ?.fieldsOf(cell.name)
4160
+ ?.find((f) => f.offset === cell.byte && f.size === width && !isArrayField(f) && !isBitfieldField(f));
4161
+ return fld !== undefined && memberQualsAllow(fld, si.const, false) && si.volatile === true;
4162
+ }
4163
+ return cell.byte === 0 && scalarGlobals.has(cell.name) && si.volatile === true;
4164
+ }
4165
+ const window = opts.deviceRegisters;
4166
+ if (!window) {
4167
+ return false;
4168
+ }
4169
+ const addr = constAddressOf(defs, op.operands[0], off);
4170
+ if (addr === null || addr < window[0] || addr >= window[1]) {
4171
+ return false;
4172
+ }
4173
+ return (useSitesOf.get(op.operands[0]) ?? []).some((s) => s.op !== op);
4174
+ };
4175
+
4176
+ /** Ops the `sideEffects` walk must SPELL even though nothing consumes their result — the
4177
+ * registry's own derived set (`SPELLED_WHEN_DEAD_OPS`), so the next op to acquire the property
4178
+ * needs no edit here, plus the address refusal above for the memory-read half.
4179
+ *
4180
+ * A memory READ is not in `EFFECTFUL_OPS`, deliberately: ir/opcodes.ts calls a load deletable
4181
+ * when dead, because nothing observes a read nobody reads. That is the C claim. The COMPILER
4182
+ * claim points the other way — an optimizing compiler deletes every dead read it is allowed to
4183
+ * delete, so one still in the target is evidence the source's access was `volatile`, and
4184
+ * dropping it deletes an instruction the machine executed.
4185
+ *
4186
+ * The statement earns nothing by itself: an unqualified `p[2];` compiles to the same bytes as
4187
+ * no statement at all (measured on agbcc, IDO and mwcc). What it does is put the access where a
4188
+ * qualifier can reach it — and `volatileQualifiable` is the condition under which one can.
4189
+ *
4190
+ * WHICH VARIATION REACHES IT, because "a qualifier" is two variations and only one of them does:
4191
+ * l3/volatileptr.ts's `/volatile` qualifies the pointer LOCAL the read is spelled through, and
4192
+ * that is the arm every match here rides. l3/volstore.ts's `/vol-store` mints `volatile` at an
4193
+ * inline cast STORE and never visits an `exprstmt`, so on a tree with no base local it emits the
4194
+ * cell qualified for its writes and plain for this read — a candidate that cannot reproduce the
4195
+ * surviving `ldr`. Wasted rather than wrong (the differ refuses it); teaching that variation the
4196
+ * read is a widening with its own window census to pay for, priced in its header.
4197
+ *
4198
+ * TWO THINGS DELIBERATELY NOT DONE HERE, priced rather than left for a reader to rediscover.
4199
+ *
4200
+ * 1. THE LITERAL ARM IS A BACKWARDS DEFAULT AND ITS PREIMAGE IS NOT EMPTY. docs/level-tower.md
4201
+ * admits a default that reads the map backwards only when the backwards mapping is ITSELF a
4202
+ * function, and "a surviving dead read implies the source said `volatile`" is not quite one:
4203
+ * the other preimage is a function whose `returnsVoid` fact is WRONG, so its return value
4204
+ * arrived here as a dead read. The second-use clause removes the common shape of that (a bare
4205
+ * register accessor), leaving a function that both STORES to a device register and reads one
4206
+ * back — narrow, but not proven empty. The MAP arm has no such problem: `gStatus;` under
4207
+ * `extern volatile u32 gStatus;` compiles differently from its own absence, so the differ can
4208
+ * referee it. The clean fix — spell the read only in candidates that also qualify it, paired
4209
+ * the way `/livebase/volatile` already pairs — moves every device row's fan shape and wants
4210
+ * its own round and zero-flip gate over BOTH tiers.
4211
+ * 2. THE REFUSAL IS SILENT. When this returns false for a `load`, the machine performed a read
4212
+ * that no statement stands for and nothing records the decision — against this project's own
4213
+ * "instrument the refusal" rule; `structure()` has no diagnostic sink to write into. What
4214
+ * exists instead is the ADMISSION side's zero point, `synthetic:dmareadback`, which fails
4215
+ * loudly if the rule stops firing. */
4216
+ /** Does `op` render AT ITS OWN POSITION, as `v = f(…)`?
4217
+ *
4218
+ * ONE DEFINITION, TWO READERS, on purpose: `isSpelled`'s base case is a MODEL of what
4219
+ * `sideEffects` emits, and a predictor and an emitter that disagree spell a call zero times or
4220
+ * twice. There is no third copy in `unreadResult`, and none is needed — it falls out of position
4221
+ * instead, because `sideEffects` tests the assign branch BEFORE the `unreadResult` branch and
4222
+ * `isSpelled` returns at its base case without ever asking `unreadResult`. */
4223
+ const rendersAtOwnPosition = (op: Op): boolean => materialize.has(op) && !absorbedLoads.has(op);
4224
+ const spelledMemo = new Map<Op, boolean>();
4225
+ /** Will the tree SPELL `op` anywhere — as a statement of its own, or inlined into one?
4226
+ *
4227
+ * `useSitesOf` is SYNTACTIC, and that is not the question `unreadResult` is asking. An op whose
4228
+ * result feeds one pure op that is itself never rendered HAS a use site, so the syntactic test
4229
+ * reads it as consumed; nothing then renders the consumer either, and an effectful op the
4230
+ * machine executed disappears with no statement and no diagnostic — the one outcome the
4231
+ * `sideEffects` walk exists to prevent. The analysis registry carries a HAND-WRITTEN instance of
4232
+ * the same correction one level up (a void function's `ret` operand is left out of the registry,
4233
+ * so a call feeding only the suppressed return reads as dead); this is its transitive form, which
4234
+ * the registry cannot express because "is it rendered" is a question about the TREE. Measured by
4235
+ * the IR oracle rather than by a reader: without this walk `irTraceOf` disagrees with the emitted
4236
+ * tree on 433 of `generateSsaFn`'s 4,000 acyclic seeds, every one a call the IR performed and the
4237
+ * tree did not, and both naming fuzzes' REFERENCE spellings share the defect exactly.
4238
+ *
4239
+ * A TERMINATOR, a `store`/`astore`/`ret` and a materialized def each render at their own
4240
+ * position, so they are the base cases (the first three have no results and fall out of the
4241
+ * same test). Everything else is spelled iff something spelled reads it — or iff it is effectful
4242
+ * and nothing does, which is this walk. No cycle is reachable: op→op edges follow SSA def-use,
4243
+ * and a cycle can only close through a block PARAM, which is not an op result. The memo is still
4244
+ * seeded `true` before recursing, so a malformed function degrades to the over-admitting answer
4245
+ * rather than recursing forever.
4246
+ *
4247
+ * WHAT THIS DELIBERATELY DOES NOT MODEL: an edge argument into a block param NOTHING READS is
4248
+ * also spelled nowhere, so in principle the walk should stop at a dead param rather than at the
4249
+ * terminator. That clause moves the residual by ZERO — d0 48/4,000, d1 31/2,508, d2 6/1,556 with
4250
+ * it and the identical counts without — because `argAssigns` writes a copy into every NAMED param
4251
+ * whether or not the param is read, and the naming walk names the params of these shapes. Holding
4252
+ * that draw takes two further clauses (skip the copy test for a named param, or `fz735`'s call is
4253
+ * spelled twice), so it is three rules that buy nothing and it is not here. */
4254
+ const isSpelled = (op: Op): boolean => {
4255
+ const memo = spelledMemo.get(op);
4256
+ if (memo !== undefined) {
4257
+ return memo;
4258
+ }
4259
+ if (op.results.length === 0 || rendersAtOwnPosition(op)) {
4260
+ return true;
4261
+ }
4262
+ spelledMemo.set(op, true);
4263
+ const r = hasSpelledUse(op.results[0]) || unreadResult(op);
4264
+ spelledMemo.set(op, r);
4265
+ return r;
4266
+ };
4267
+ /** Does any op the tree spells READ `v`? */
4268
+ const hasSpelledUse = (v: Value): boolean => (useSitesOf.get(v) ?? []).some((s) => isSpelled(s.op));
4269
+
4270
+ const unreadResult = (op: Op): boolean =>
4271
+ SPELLED_WHEN_DEAD_OPS.has(op.opcode) &&
4272
+ op.results.length > 0 &&
4273
+ // NO EXEMPTION FOR A MATERIALIZED DEF HERE, and it is not missing. Such a def already renders
4274
+ // at its own position, as `v = f(…)`, which spells the effect as surely as a bare statement
4275
+ // does; taking the `exprstmt` branch for one instead emits `expr(result)` — the NAME, so `v3;`
4276
+ // replaces `v3 = f0(…)` and the call is gone. Both callers rule that out BEFORE asking:
4277
+ // `isSpelled` returns at its `rendersAtOwnPosition` base case, and `sideEffects` tests the
4278
+ // assign branch first. Under a transitive use test the two sets overlap constantly — a
4279
+ // materialized def's name is often read by nothing spelled — so that ordering is load-bearing,
4280
+ // and `dead-effect.test.ts`'s `MATERIALIZED` pins it.
4281
+ !hasSpelledUse(op.results[0]) &&
4282
+ (opSig(op.opcode)?.reads !== true || volatileQualifiable(op));
4283
+
3362
4284
  const sideEffects = (b: Block): Stmt[] => {
3363
4285
  const out: Stmt[] = [];
3364
4286
  for (const op of b.ops) {
@@ -3370,7 +4292,9 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
3370
4292
  out.push({
3371
4293
  k: 'store',
3372
4294
  lval: { k: 'field', base: { k: 'var', name: bfs.global }, name: bfs.field, dot: true },
3373
- value: expr(bfs.value),
4295
+ // `zero` is the ALL-ZERO form: the asm carries no value at all, because agbcc emits
4296
+ // only the clearing `and` when the assigned value is 0 (structure/bitfields.ts).
4297
+ value: bfs.value.k === 'zero' ? { k: 'const', value: 0 } : expr(bfs.value.v),
3374
4298
  });
3375
4299
  continue;
3376
4300
  }
@@ -3387,6 +4311,7 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
3387
4311
  scalarGlobals,
3388
4312
  symCtx,
3389
4313
  true, // an lvalue: a member whose declaration is const cannot be NAMED as the target
4314
+ advanceStepOf(op.operands[0]),
3390
4315
  );
3391
4316
  if (lval0.k === 'var') {
3392
4317
  globalNames.add(lval0.name);
@@ -3415,23 +4340,24 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
3415
4340
  ),
3416
4341
  value: expr(op.operands[2]),
3417
4342
  });
3418
- } else if (EFFECTFUL_OPS.has(op.opcode) && op.results.length && !useSitesOf.has(op.results[0])) {
3419
- // An effectful op whose result nobody reads is still an execution. `store`/`astore` have no
3420
- // result and were handled above, so what reaches here is `call` and `opaque` — and an
3421
- // `opaque` missing from this walk is an instruction the frontend could not model
3422
- // disappearing with no diagnostic, which is the one thing this project refuses to do.
3423
- //
3424
- // Keyed on EFFECTFUL_OPS rather than the two opcode names: the deciding property is "has an
3425
- // effect the result does not account for", which is what the flag already means, so the next
3426
- // op to acquire it needs no edit here. Statement, not expression — `expr` on the result
3427
- // routes through `lowerDef`, already where `opaque` becomes the gap, so this reuses the SAME
3428
- // degradation a live opaque gets rather than inventing a second way to be loud.
3429
- out.push({ k: 'exprstmt', value: expr(op.results[0]) });
3430
- } else if (materialize.has(op) && !absorbedLoads.has(op)) {
4343
+ } else if (rendersAtOwnPosition(op)) {
4344
+ // FIRST, and that order is the rule rather than a restatement of it: this branch is what
4345
+ // "renders at its own position" MEANS, so an op it claims can never reach the `exprstmt`
4346
+ // branch below and be spelled as a bare `v3;` with the call dropped.
3431
4347
  // (an absorbed load's every consumer spells a named bitfield read — emitting its temp
3432
4348
  // here would recompile to a second load the asm does not have)
3433
4349
  const nm = varName.get(op.results[0])!;
3434
4350
  out.push({ k: 'assign', name: nm, value: intoDeclaredTemp(nm, lowerDef(op, expr)) });
4351
+ } else if (unreadResult(op)) {
4352
+ // An op whose result nobody reads is still an execution. `store`/`astore` have no result and
4353
+ // were handled above, so what reaches here is `call`, `opaque` and a memory READ — and an
4354
+ // `opaque` missing from this walk is an instruction the frontend could not model
4355
+ // disappearing with no diagnostic, which is the one thing this project refuses to do.
4356
+ //
4357
+ // Statement, not expression — `expr` on the result routes through `lowerDef`, already where
4358
+ // `opaque` becomes the gap, so this reuses the SAME degradation a live opaque gets rather
4359
+ // than inventing a second way to be loud.
4360
+ out.push({ k: 'exprstmt', value: expr(op.results[0]) });
3435
4361
  }
3436
4362
  // a merge copy anchored at this const's original position (anchorConstCopies, above)
3437
4363
  for (const a of anchoredAt.get(op) ?? []) {
@@ -3466,6 +4392,13 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
3466
4392
  }
3467
4393
  };
3468
4394
 
4395
+ // Branch-sense sites, numbered as the walk below first reaches them (`branchSenseFlipSites`).
4396
+ const senseOrdinal = new Map<number, number>();
4397
+
4398
+ // Which `return;` statements the SOURCE wrote — the reading, and why it is decidable, live in
4399
+ // `structure/retspell.ts`. `l3/tailret.ts` owns whether a marked return is safe to delete.
4400
+ const unspelledRets = unspelledEpilogues(fn);
4401
+
3469
4402
  const structureRegion = (b: Block, stop: Block | null): Stmt[] => {
3470
4403
  if (b === stop) {
3471
4404
  return [];
@@ -3506,6 +4439,7 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
3506
4439
  switchAllowsNeqCase,
3507
4440
  switchAllowsBoundCase,
3508
4441
  switchArmsFollowLayout,
4442
+ switchRequiresFrontLoadedTests,
3509
4443
  spellSwitchFallthrough,
3510
4444
  emitsOwnStatement: (blk) => blk.ops.some((o) => anchoredAt.has(o) || materialize.has(o)),
3511
4445
  blockOf,
@@ -3527,7 +4461,11 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
3527
4461
  const term = b.ops[b.ops.length - 1];
3528
4462
  if (term.opcode === 'ret') {
3529
4463
  // A void function's `bx lr` leaves whatever in r0; suppress that phantom return value.
3530
- out.push({ k: 'return', value: returnsVoid || !term.operands.length ? undefined : expr(term.operands[0]) });
4464
+ const value = returnsVoid || !term.operands.length ? undefined : expr(term.operands[0]);
4465
+ // The `value === undefined` half is a GUARD: `l3/tailret.ts` re-checks it before deleting
4466
+ // anything, so marking a value-carrying return would change no output. The mark claims the asm
4467
+ // shows no `return;` STATEMENT here, which is a claim about a VOID return only.
4468
+ out.push({ k: 'return', value, ...(value === undefined && unspelledRets.has(b) ? { unspelled: true } : {}) });
3531
4469
  return out;
3532
4470
  }
3533
4471
  if (term.opcode === 'br') {
@@ -4015,7 +4953,12 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
4015
4953
  }
4016
4954
 
4017
4955
  const cond = expr(term.operands[0]);
4018
- const ipd = ipdom.get(b) ?? null; // null ⇒ the arms diverge (both reach EXIT), no join
4956
+ // null ⇒ the arms diverge (both reach EXIT) and no follow over early returns applies. Asked
4957
+ // outside loop bodies only: inside one, `clampToLoop` below owns the question. DEFENSIVE — no
4958
+ // input it changes: ablated, the follow fires on no more of 40,000 generated functions (random
4959
+ // structuring options, sunk and unsunk), and every row of both tiers enumerates a byte-identical
4960
+ // variations-and-source set (810 synthetic, 251 real, `ProcessInputAndUpdateEntities` excepted).
4961
+ const ipd = ipdom.get(b) ?? (followEarlyReturns && loopCtx === null ? followOverReturns(b, stop) : null);
4019
4962
  // Inside a loop body, a join OUTSIDE that body is not this `if`'s join: an arm that leaves the
4020
4963
  // loop `return`s and never comes back, so what is left reconverges at the loop's own
4021
4964
  // continuation. Post-dominance cannot see that — agbcc/gcc merge every `return` into one
@@ -4041,29 +4984,111 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
4041
4984
  // block with different args would otherwise give both arms the first edge's copies.
4042
4985
  const thenS = [...argAssignsFor(b, term.successors[0]), ...structureRegion(takenB, merge)];
4043
4986
  const elseS = [...argAssignsFor(b, term.successors[1]), ...structureRegion(fallB, merge)];
4044
- if (ipd === null && thenS.length && elseS.length && preserveDivergentBranchSense) {
4045
- // Divergent arms (both terminate no reconvergence). The asm branched forward to the
4046
- // `taken` block and fell through to `fall`; a compiler that PRESERVES source branch direction
4047
- // re-emits that as a forward branch on the NEGATED condition to the else-arm, so putting the
4048
- // taken arm as `else` (and negating) reproduces the original branch sense. Byte-exact on
4049
- // IDO/MIPS; agbcc/GCC canonicalise either way, so it is safe there too. A compiler that
4050
- // inverts branch canonicalization sets preserveDivergentBranchSense false and falls through
4051
- // to the positive form below.
4052
- out.push({ k: 'if', cond: negateCond(cond), then: elseS, else: thenS });
4053
- return out;
4054
- }
4055
- if (negateJoinedBranchSense && ipd !== null && thenS.length && elseS.length) {
4056
- // JOINED arms only (`ipd !== null` a divergent if belongs to preserveDivergentBranchSense
4057
- // above, and without the check a /flip-branch variant would fall through here and get
4058
- // flipped BACK, collapsing the {divergent flipped × joined flipped} combination), and both
4059
- // arms real: the flipped spelling is a genuine sibling, not noise on a one-armed if
4060
- out.push({ k: 'if', cond: negateCond(cond), then: elseS, else: thenS });
4061
- if (merge && merge !== stop) {
4062
- out.push(...structureRegion(merge, stop));
4987
+ // A BRANCH-SENSE SITE: both arms real, so the swapped-and-negated spelling is a genuine
4988
+ // sibling rather than noise on a one-armed if. Which boolean owns it is `ipd`: divergent arms
4989
+ // (both terminate, no reconvergence) belong to preserveDivergentBranchSense, a reconverging
4990
+ // pair to negateJoinedBranchSense and the split has to stay, or a /flip-branch candidate would
4991
+ // fall into the joined case and get flipped BACK, collapsing the {divergent × joined}
4992
+ // combination. Sense TRUE = the asm branched forward to the `taken` block and fell through to
4993
+ // `fall`, so a compiler that PRESERVES source branch direction saw the FALL-THROUGH arm as
4994
+ // `then`: putting the taken arm as `else` (and negating) reproduces the original. Byte-exact
4995
+ // on IDO/MIPS; agbcc/GCC canonicalise either way, so it is safe there too. A compiler that
4996
+ // inverts branch canonicalization sets the boolean false and gets the positive form.
4997
+ const senseSite = thenS.length > 0 && elseS.length > 0;
4998
+ // The fold's evidence where there is any, the function-wide boolean where there is not
4999
+ // (`senseFromFoldEvidence`). THREE stamps, and exactly ONE of the eight cells negates.
5000
+ //
5001
+ // onFall isTaken relayed spelling inhabitant (each measured from its own asm)
5002
+ // ------ ------- ------- -------- --------------------------------------------------
5003
+ // false true false NEGATE `synthetic:ifand_near` the short-branch `&&`
5004
+ // true true false positive `synthetic:ifor_near` — the short-branch `||`
5005
+ // true false true positive `synthetic:ifand_far` — the long-branch `&&`
5006
+ // false true TRUE positive `synthetic:ifor_far` — the long-branch `||`
5007
+ // false false false positive `synthetic:chainsense`, the chained fold
5008
+ // (the other three cells are uninhabited over the committed corpus at both settings)
5009
+ //
5010
+ // `scSharedIsTaken` says where the shared arm went — TRUE, the arm both tests reach is this
5011
+ // branch's TAKEN successor, is the `||` fold. `scSharedOnFall` says which source arm it is:
5012
+ // true = the last test FELL INTO it, so under gcc's source-order layout it is the source's
5013
+ // `then`. A shared arm in the FALL slot leaves the source's `then` in the taken slot whatever
5014
+ // the layout — an `&&`'s shared arm is where its FAILING tests go — so the `&&` half is
5015
+ // positive at both values of `scSharedOnFall` and the premise is only consulted where it
5016
+ // decides something. That is what admits the CHAINED fold, `scSharedOnFall` false, whose inner
5017
+ // fold left the head's taken edge pointing at the next test: reading the source arm alone
5018
+ // NEGATES it, `synthetic:chainsense` is 4/44 that way and MATCH this way, and the inner-loop
5019
+ // site of `kleod:CountCollectedGems:agbcc` is the real-row inhabitant (39/352 → 18/344).
5020
+ //
5021
+ // `scEdgeRelayed` is the LONG BRANCH, and it is the stamp that keeps the premise honest rather
5022
+ // than absorbing it. agbcc inverts a conditional it cannot reach in ±256 bytes, so the layout
5023
+ // claim `scSharedOnFall` rests on is simply false there — and the inversion moves the shared arm
5024
+ // to the OTHER successor slot, which is why it is not confined to one quadrant. Measured: the
5025
+ // long `||` (`ifor_far`) stamps the IDENTICAL pair as the short `&&` (`ifand_near`), false/true,
5026
+ // and wants the opposite spelling. Two booleans cannot separate them; without this one the
5027
+ // reading spells `ifor_far` as its own dual. Both long rows are pinned by test/site-sense.test.ts
5028
+ // and by nothing else: each MATCHes on `/flip-join` whatever the fold spelled (0/140 and 0/139).
5029
+ //
5030
+ // WHAT IS STILL UNDECIDED, and it is a cell of this table rather than a hole beside it: at
5031
+ // `(onFall=true, isTaken=false)` the corpus holds a function with TWO sites of OPPOSITE source
5032
+ // sense — `kleod:CheckWorldCompletion:agbcc`, whose own `refSource` wants the positive spelling
5033
+ // at one and the negated one at the other. No CONSTANT is right for that cell; this table's
5034
+ // `positive` is right at one of the two and wrong at the other, and the row scores 45/191 with
5035
+ // the same winner either way because `/site-sense` is not its winner. If the cell is ever to be
5036
+ // decided it needs a SITE fact the fold does not carry yet; if it cannot be, the honest shape is
5037
+ // `rank.ts`'s per-site mask (`branchSenseFlipSites`, the `/sense-N` probe), which enumerates a
5038
+ // site both ways instead of picking. Do not read the table above as the mapping being a function.
5039
+ //
5040
+ // CENSUS over the 1037 rows of the committed artifact — which predates `synthetic:ifor_far` and
5041
+ // `synthetic:chainsense`, the two rows added for the cells below. Each row's own `targetAsm`
5042
+ // lifted at both `/connective` settings: stamped `cond_br`s at the PRODUCER, then this
5043
+ // consumer's own reads at real two-armed sense sites, each summed over the two settings:
5044
+ //
5045
+ // stamped sites `/connective` off: 82 taken-slot, 4 long-branch, 0 chained
5046
+ // `/connective` on: 85 taken-slot, 4 long-branch, 2 chained
5047
+ // consumer reads 99 taken-slot, 8 long-branch, 2 chained
5048
+ //
5049
+ // The chained cell has ZERO inhabitants with `/connective` off, so that half of the capability
5050
+ // is reachable only through the `/connective` lift variation — both real inhabitants and
5051
+ // `chainsense` carry it in their winner. And `scEdgeRelayed`'s own cell `(false,true,true)`
5052
+ // carries 9 stamped sites at each setting and **0 consumer reads** there: all 9 are MIPS rows
5053
+ // (`ido7.1`/`gcc2.7.2kmc`, where no ±256 range exists and a relay means something else), and
5054
+ // none is a two-armed sense site, so those rows emit byte-identical C with the stamp and
5055
+ // without it — the cell's one reader is `synthetic:ifor_far`, measured from its own asm. That
5056
+ // MIPS firing is this stamp's known over-reach — a PROXY firing where the structure it proxies
5057
+ // is absent — priced at 0 today and worth re-censusing whenever a MIPS row starts reading it.
5058
+ //
5059
+ // REFUSES unless ALL THREE stamps are present. DEFENSIVE — no input reaches it: the one producer
5060
+ // (raise/shortcircuit.ts) writes all three in a single object literal, and instrumenting this
5061
+ // read over that same corpus counts 0 partially-stamped sites. A site the fold never touched
5062
+ // carries none and keeps its function-wide boolean.
5063
+ const foldEvidence = term.attrs.scSharedOnFall;
5064
+ const sharedIsTaken = term.attrs.scSharedIsTaken;
5065
+ const edgeRelayed = term.attrs.scEdgeRelayed;
5066
+ const siteDefault =
5067
+ senseFromFoldEvidence &&
5068
+ typeof foldEvidence === 'boolean' &&
5069
+ typeof sharedIsTaken === 'boolean' &&
5070
+ typeof edgeRelayed === 'boolean'
5071
+ ? sharedIsTaken && !foldEvidence && !edgeRelayed
5072
+ : ipd === null
5073
+ ? preserveDivergentBranchSense
5074
+ : negateJoinedBranchSense;
5075
+ let negateHere = false;
5076
+ if (senseSite) {
5077
+ // Keyed by BLOCK, numbered by first visit. Both halves matter: a region the structurer
5078
+ // emits twice (a tail duplicated into two arms) reaches the same block twice and must get
5079
+ // the SAME sense both times, and the ordinal has to mean the same site under every mask —
5080
+ // which it does because both arms are structured ABOVE, before any sense is chosen, so the
5081
+ // visit order does not depend on the choice.
5082
+ const bi = fn.blocks.indexOf(b);
5083
+ let ord = senseOrdinal.get(bi);
5084
+ if (ord === undefined) {
5085
+ ord = senseOrdinal.size;
5086
+ senseOrdinal.set(bi, ord);
4063
5087
  }
4064
- return out;
5088
+ negateHere = branchSenseFlipSites?.has(ord) ? !siteDefault : siteDefault;
5089
+ hooks.onBranchSenseSite?.({ block: bi, ordinal: ord, joined: ipd !== null, negated: negateHere });
4065
5090
  }
4066
- out.push(mkIf(cond, thenS, elseS));
5091
+ out.push(negateHere ? { k: 'if', cond: negateCond(cond), then: elseS, else: thenS } : mkIf(cond, thenS, elseS));
4067
5092
  if (merge && merge !== stop) {
4068
5093
  out.push(...structureRegion(merge, stop));
4069
5094
  }
@@ -4171,6 +5196,126 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
4171
5196
  const latchSub = (dw: DoWhileInfo): Map<Value, string> =>
4172
5197
  subFor(dw.header.params, successorTo(dw.latch, dw.header)!.args);
4173
5198
 
5199
+ // THE LATCH IS POST-LOOP FOR EVERY INNER LOOP THAT RUNS BEFORE IT. A bottom-tested loop renders
5200
+ // its latch — side effects, update copies, test — HERE, outside the body region, so none of it is
5201
+ // under the substitution an inner loop's exit region installs (`withSub` in the self-loop and
5202
+ // do-while emitters). Yet the latch runs after those inner loops exactly as the rest of their exit
5203
+ // region does, and the inner do-while's own hazard check judged it that way (its `postLoop` holds
5204
+ // every block outside the inner body). Read raw, an inner back-edge value is RE-DERIVED from the inner
5205
+ // variable's name — which by then already holds that value — so the latch counts the last
5206
+ // iteration twice: `a += gT[i][j] * 2` over a nest whose inner loop is one block emits
5207
+ // `do { … v4 = v4 + (v0 << 1); … } while (…); v2 = v4 + (v0 << 1);` — a silent wrong answer that
5208
+ // main's default candidate reaches on agbcc's own output (the `acc += a[i][j]` nest).
5209
+ //
5210
+ // So the latch reads such a value under the name its inner loop left it in: the back-edge
5211
+ // substitution of every loop INSIDE this one that dominates the latch and does not contain it.
5212
+ // Every depth, not only children: a grandchild's value reaches the latch raw whenever the loop
5213
+ // between them does not carry it (`for i { for j { s = j; for k { s += …; } } gO[i] = s; }` —
5214
+ // `latch-inner-sub.test.ts`'s `GRANDCHILD`, 64 of 64 inputs wrong when only children counted).
5215
+ // Where that middle loop DOES carry it, both loops map the value to the one name the carrying
5216
+ // shares, and the inner one wins. Dominating loops apply outermost first, so a later one wins, as
5217
+ // the nested `withSub`s do. A test-at-top `while` installs no substitution (its latch-computed
5218
+ // values never reach past its header exit), so it contributes nothing. Dominance is not a
5219
+ // soundness term — a value of a loop that does not dominate the latch cannot be read there — it
5220
+ // is what keeps the map EMPTY when no loop can hand the latch a value, and `emitDoWhile` then
5221
+ // spells every line as it did before the map existed.
5222
+ //
5223
+ // NARROWER THAN THE EXIT REGION'S `withSub`, in two ways:
5224
+ // • only an UNNAMED value DEFINED INSIDE the inner loop. A named value renders under its own
5225
+ // name, and one defined outside the loop (an entry value handed round the back edge
5226
+ // unchanged) re-derives from operands the loop never wrote. The IR oracle found the second
5227
+ // (generated seed 16501, `fz16501`): substituting an entry value `a1 - a1` by its inner name
5228
+ // after the exit copy `v2 = v1` had rewritten it. That witness is held by EITHER narrowing
5229
+ // (measured, dropping one at a time); this one also keeps the refusal below from judging
5230
+ // values whose re-derivation the loop cannot have made stale;
5231
+ // • only while the name still HOLDS it. A name written between the inner loop and the latch —
5232
+ // a block param of the exit region, of an enclosing loop's header or of the latch itself, or
5233
+ // a materialized def there — no longer holds the loop's last value, unless what it wrote IS
5234
+ // that value (`aliasOf`: a merge every arm of which hands it the inner value,
5235
+ // `IDENTITY_MERGE`). This loop's own header is exempt: its params are written by the update
5236
+ // copies below, which read under this very substitution, and `enclosingCarrierName` hands
5237
+ // an inner value exactly that name (`LATCH_SUM`, measured).
5238
+ // Such an entry is not substituted, and then the latch re-derives it, which is right only if
5239
+ // the re-derivation reads no name written after the value was computed — the inner loop's own
5240
+ // and that stretch's. `unreadable` holds the ones for which it does: no spelling at the latch
5241
+ // is that value, and `emitDoWhile` declines LOUD if a latch reader needs one. `T2_MERGE` and
5242
+ // `T2_INVARIANT` in `latch-inner-sub.test.ts` are the two sides: a merge after the inner loop
5243
+ // that took the inner name, with an inner value the raw reading re-derives wrong (neither
5244
+ // reading is right) and right (the raw reading is). The refusal is per VALUE and per
5245
+ // stretch, not per name: an entry another loop's substitution still covers (a middle loop
5246
+ // that carries the grandchild's value under the name they share) is read through that one.
5247
+ //
5248
+ // `writtenAfter` is that set of names per entry, for the test's own refusal in `emitDoWhile`.
5249
+ // Does `w` hold `v` — `v` itself, or a block param every in-edge of which hands it `v` (or such a
5250
+ // param)? The write of a param like that stores what the name already held. A cycle of such
5251
+ // params is `v` too, which is why a revisit answers yes.
5252
+ const aliasOf = (w: Value, v: Value, seen: Set<Value>): boolean => {
5253
+ if (w === v || seen.has(w)) {
5254
+ return true;
5255
+ }
5256
+ seen.add(w);
5257
+ const b = paramBlock.get(w);
5258
+ if (b === undefined || b === entry) {
5259
+ return false;
5260
+ }
5261
+ const k = b.params.indexOf(w);
5262
+ const ins = [...inEdgeRecords(preds, b)];
5263
+ return ins.length > 0 && ins.every(({ succ }) => aliasOf(succ.args[k], v, seen));
5264
+ };
5265
+ const latchInnerSub = (
5266
+ dw: DoWhileInfo,
5267
+ ): { sub: Map<Value, string>; unreadable: Set<Value>; writtenAfter: Map<Value, Set<string>> } => {
5268
+ const out = new Map<Value, string>();
5269
+ const refused = new Set<Value>();
5270
+ const writtenAfter = new Map<Value, Set<string>>();
5271
+ const latchDoms = dom.get(dw.latch)!;
5272
+ const kids = [...forest.byHeader.values()]
5273
+ .filter(
5274
+ (l) => l.header !== dw.header && dw.body.has(l.header) && !l.body.has(dw.latch) && latchDoms.has(l.header),
5275
+ )
5276
+ .sort((x, y) => dom.get(x.header)!.size - dom.get(y.header)!.size);
5277
+ for (const l of kids) {
5278
+ const self = loops.get(l.header);
5279
+ const nested = doWhileLoops.get(l.header);
5280
+ const s = self ? loopSub(self) : nested ? latchSub(nested) : null;
5281
+ if (s === null) {
5282
+ continue;
5283
+ }
5284
+ const rewrittenBy = new Map<string, Value[]>();
5285
+ const written = new Set<string>();
5286
+ for (const [v, n] of varName) {
5287
+ const d = defs.get(v);
5288
+ const home = paramBlock.get(v) ?? (d !== undefined && materialize.has(d) ? opBlock.get(d) : undefined);
5289
+ if (home === undefined || !dw.body.has(home) || home === dw.header) {
5290
+ continue;
5291
+ }
5292
+ written.add(n);
5293
+ if (!l.body.has(home)) {
5294
+ rewrittenBy.set(n, [...(rewrittenBy.get(n) ?? []), v]);
5295
+ }
5296
+ }
5297
+ for (const [v, n] of s) {
5298
+ const d = defs.get(v);
5299
+ if (varName.has(v) || d === undefined || !l.body.has(opBlock.get(d)!)) {
5300
+ continue;
5301
+ }
5302
+ writtenAfter.set(v, written);
5303
+ if (rewrittenBy.get(n)?.some((w) => !aliasOf(w, v, new Set())) === true) {
5304
+ refused.add(v);
5305
+ } else {
5306
+ out.set(v, n);
5307
+ }
5308
+ }
5309
+ }
5310
+ const unreadable = new Set<Value>();
5311
+ for (const v of refused) {
5312
+ if (!out.has(v) && readsClobbered(v, out, writtenAfter.get(v)!)) {
5313
+ unreadable.add(v);
5314
+ }
5315
+ }
5316
+ return { sub: out, unreadable, writtenAfter };
5317
+ };
5318
+
4174
5319
  // Bottom-test `do-while`: the body runs header..latch (structured, with `b`'s do-while hook masked
4175
5320
  // via dwActive), then the latch's own side-effects + the loop update; the latch's cond_br test is the
4176
5321
  // do-while condition, read under `latchSub` (post-update the params hold their next value). Polarity:
@@ -4183,7 +5328,20 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
4183
5328
  // post-update name — one iteration off, silently. Same readsClobbered guard the early-exit
4184
5329
  // path applies; on a hazard, decline LOUD.
4185
5330
  const sub = latchSub(dw);
4186
- const updates = argAssigns(dw.latch, dw.header);
5331
+ // The inner loops' post-loop substitution the latch reads under (`latchInnerSub`). Empty — and
5332
+ // then every line below spells what it did before the substitution existed — unless an unnamed
5333
+ // value an inner loop computed could reach the latch.
5334
+ //
5335
+ // The update copies take it MERGED with `activeSub`, because a map passed to `argAssigns`
5336
+ // replaces the ambient `expr` it would otherwise render with: without the merge, a copy reading
5337
+ // an ENCLOSING loop's post-loop value would re-derive it (`ACTIVE_SUB` in
5338
+ // `latch-inner-sub.test.ts`). One thing a map changes that `expr` does not: identity elision
5339
+ // consults it, so a copy that `activeSub` spells `n = n` is dropped rather than written. The
5340
+ // two programs are the same, and it is left conditional so an empty `innerSub` keeps the line
5341
+ // exactly as it was (the corpus census is byte-identical either side of this commit's parent).
5342
+ const { sub: innerSub, unreadable, writtenAfter } = latchInnerSub(dw);
5343
+ const latchMap = innerSub.size > 0 ? new Map([...(activeSub ?? []), ...innerSub]) : null;
5344
+ const updates = argAssigns(dw.latch, dw.header, latchMap);
4187
5345
  const updateWrites = loopWriteSet(updates, dw.body, dw.header);
4188
5346
  const lterm = dw.latch.ops[dw.latch.ops.length - 1];
4189
5347
  // KNOWN GAP, and the reason the sink stands down rather than repairing anything. A body
@@ -4291,10 +5449,64 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
4291
5449
  const body = [
4292
5450
  ...preUpdateCopies(dw.exit, exitArgs, sunk, dw.header),
4293
5451
  ...inner,
4294
- ...sideEffects(dw.latch),
5452
+ ...(innerSub.size > 0 ? withSub(innerSub, () => sideEffects(dw.latch)) : sideEffects(dw.latch)),
4295
5453
  ...updates,
4296
5454
  ];
4297
- let cond = exprWith(sub)(lterm.operands[0]);
5455
+ // The test reads this loop's own update under `sub`, and anything else an inner loop left under
5456
+ // `innerSub` — the outer loop's own reading wins where a value is both. The test runs AFTER the
5457
+ // update copies, so an inner name one of them really writes no longer holds the inner value, and
5458
+ // that entry keeps the raw reading — the same refusal `latchInnerSub` makes for a name written
5459
+ // before the latch, with the update's writes added to what the re-derivation must not read.
5460
+ const writtenByUpdate = innerSub.size > 0 ? updateWriteSet(updates) : new Set<string>();
5461
+ const condInner = [...innerSub].filter(([, n]) => !writtenByUpdate.has(n));
5462
+ const condMap = condInner.length > 0 ? new Map([...condInner, ...sub]) : sub;
5463
+ // NEITHER READING IS THE VALUE — decline LOUD. An inner value whose name was rewritten before a
5464
+ // latch reader runs is not substituted, and its re-derivation reads a name written after it
5465
+ // was computed: the name holds something else and the re-derivation computes something else.
5466
+ // Which readers these are is exactly what the lines above render under each map: the latch's
5467
+ // side effects and update copies under `latchMap` (or the ambient `activeSub`), the test under
5468
+ // `condMap`. Materialized and effectful ops are the side effects' roots; a pure op renders at
5469
+ // its use, which is one of the other two roots or outside the latch.
5470
+ const condUnreadable = new Set(unreadable);
5471
+ for (const [v] of innerSub) {
5472
+ if (!condMap.has(v) && readsClobbered(v, condMap, new Set([...writtenAfter.get(v)!, ...writtenByUpdate]))) {
5473
+ condUnreadable.add(v);
5474
+ }
5475
+ }
5476
+ if (condUnreadable.size > 0) {
5477
+ const needs = (root: Value, stop: ReadonlyMap<Value, string> | null, targets: ReadonlySet<Value>): boolean => {
5478
+ const seen = new Set<Value>();
5479
+ const walk = (x: Value): boolean => {
5480
+ if (seen.has(x) || stop?.has(x) === true || varName.has(x)) {
5481
+ return false;
5482
+ }
5483
+ if (targets.has(x)) {
5484
+ return true;
5485
+ }
5486
+ seen.add(x);
5487
+ return defs.get(x)?.operands.some(walk) ?? false;
5488
+ };
5489
+ return walk(root);
5490
+ };
5491
+ const bodyMap = latchMap ?? activeSub;
5492
+ const effectRoots = dw.latch.ops
5493
+ .slice(0, -1)
5494
+ .filter(
5495
+ (op) => op.results.length === 0 || materialize.has(op) || EFFECTFUL_OPS.has(op.opcode) || unreadResult(op),
5496
+ )
5497
+ .flatMap((op) => op.operands);
5498
+ const updateRoots = successorTo(dw.latch, dw.header)!.args;
5499
+ if (
5500
+ [...effectRoots, ...updateRoots].some((r) => needs(r, bodyMap, unreadable)) ||
5501
+ needs(lterm.operands[0], condMap, condUnreadable)
5502
+ ) {
5503
+ throw new StructureError(
5504
+ `cannot structure '${fn.name}': a loop latch reads an inner loop's value whose name was rewritten ` +
5505
+ `after the inner loop, and re-deriving it reads a name the inner loop wrote`,
5506
+ );
5507
+ }
5508
+ }
5509
+ let cond = exprWith(condMap)(lterm.operands[0]);
4298
5510
  if (lterm.successors[1].block === dw.header) {
4299
5511
  cond = negateCond(cond);
4300
5512
  } // continue edge must be `taken`
@@ -4415,8 +5627,8 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
4415
5627
  // computation, a call. The count is then a floor rather than the access set, and it is read as
4416
5628
  // the access set (the l3/volatileval.ts gate), so it refuses instead of reporting a number that
4417
5629
  // undercounts. Reached rather than theoretical: an address-escaped frame scratch takes it —
4418
- // `synthetic:dma_fill_uninit` and `kleod:ProcessInputAndUpdateEntities` both lose their record
4419
- // here.
5630
+ // `synthetic:dma_fill_uninit` loses its record here, and so did `kleod:ProcessInputAndUpdateEntities`
5631
+ // before that row was retired (2026-09-13).
4420
5632
  const frameRecord = (at: Op): { frame?: { loads: number; stores: number } } => {
4421
5633
  const off = at.attrs.off as number;
4422
5634
  const roots = new Set<Value>();
@@ -4488,7 +5700,7 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
4488
5700
  type: T.int((op.attrs.width as number) * 8, op.attrs.signed as boolean),
4489
5701
  // the asm materialized this slot's address, and this is how many times it
4490
5702
  // loaded and stored through it — both asm facts, and the gate the
4491
- // l3/volatileval.ts lever reads (see the SFn.locals doc)
5703
+ // l3/volatileval.ts variation reads (see the SFn.locals doc)
4492
5704
  ...frameRecord(op),
4493
5705
  // an ESCAPED address makes every store observable (the DMA hardware reads it), and
4494
5706
  // the source spells the scratch volatile for that reason — see the stamp site in
@@ -4718,14 +5930,24 @@ function mkIf(cond: Expr, thenS: Stmt[], elseS: Stmt[]): Stmt {
4718
5930
  }
4719
5931
 
4720
5932
  // --- CFG utilities ---
5933
+ /** Every block's in-edges. THROWS BY NAME on a successor that is not a block of `fn` — the one
5934
+ * state that makes this map lie, and the one the whole structurer reads positions and dominance
5935
+ * out of. `ir/verify.ts` already rejects that state with a name and the tower runs it after the
5936
+ * lift and after every raising pass, so reaching HERE means a pass built it after the last verify:
5937
+ * exactly when a named error beats a `TypeError` from a CFG utility 3000 lines from anything the
5938
+ * reader recognises. switch-recover.ts PRE5 and its test cite this throw as a loud invariant. */
4721
5939
  function predecessorBlocks(fn: Fn): Map<Block, Block[]> {
4722
5940
  const m = new Map<Block, Block[]>();
4723
5941
  for (const b of fn.blocks) {
4724
5942
  m.set(b, []);
4725
5943
  }
4726
- for (const b of fn.blocks) {
5944
+ for (const [i, b] of fn.blocks.entries()) {
4727
5945
  for (const s of successorsOf(b)) {
4728
- m.get(s)!.push(b);
5946
+ const preds = m.get(s);
5947
+ if (!preds) {
5948
+ throw new Error(`successor of block ${i} is not a block of this fn (fn '${fn.name}', predecessorBlocks)`);
5949
+ }
5950
+ preds.push(b);
4729
5951
  }
4730
5952
  }
4731
5953
  return m;
@@ -4761,22 +5983,24 @@ function* inEdgeRecords(preds: Map<Block, Block[]>, b: Block): Generator<{ pred:
4761
5983
  }
4762
5984
  }
4763
5985
 
4764
- // Immediate post-dominators. EXIT is represented as `null`; ret-blocks post-lead to it.
4765
- function postDominators(fn: Fn): Map<Block, Block | null> {
4766
- const nodes: (Block | null)[] = [null, ...fn.blocks];
5986
+ // Immediate post-dominators. EXIT is represented as `null`; ret-blocks post-lead to it. Over the
5987
+ // subgraph `keep` induces when given, every member of which must still reach a `ret` inside it.
5988
+ function postDominators(fn: Fn, keep?: ReadonlySet<Block>): Map<Block, Block | null> {
5989
+ const blocks = keep ? fn.blocks.filter((b) => keep.has(b)) : fn.blocks;
5990
+ const nodes: (Block | null)[] = [null, ...blocks];
4767
5991
  const succ = (b: Block): (Block | null)[] => {
4768
5992
  const term = b.ops[b.ops.length - 1];
4769
- return term.opcode === 'ret' ? [null] : successorsOf(b);
5993
+ return term.opcode === 'ret' ? [null] : successorsOf(b).filter((s) => keep === undefined || keep.has(s));
4770
5994
  };
4771
5995
  const pdom = new Map<Block | null, Set<Block | null>>();
4772
5996
  pdom.set(null, new Set([null]));
4773
- for (const b of fn.blocks) {
5997
+ for (const b of blocks) {
4774
5998
  pdom.set(b, new Set(nodes));
4775
5999
  }
4776
6000
  let changed = true;
4777
6001
  while (changed) {
4778
6002
  changed = false;
4779
- for (const b of fn.blocks) {
6003
+ for (const b of blocks) {
4780
6004
  const ss = succ(b);
4781
6005
  let inter: Set<Block | null> | null = null;
4782
6006
  for (const s of ss) {
@@ -4801,7 +6025,7 @@ function postDominators(fn: Fn): Map<Block, Block | null> {
4801
6025
  }
4802
6026
  // ipdom(b) = the strict post-dom c with (strictPostDoms(b) \ {c}) ⊆ pdom(c)
4803
6027
  const ipdom = new Map<Block, Block | null>();
4804
- for (const b of fn.blocks) {
6028
+ for (const b of blocks) {
4805
6029
  const strict = [...pdom.get(b)!].filter((c) => c !== b);
4806
6030
  let chosen: Block | null = null;
4807
6031
  for (const c of strict) {