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