@asmlift/core 0.8.1 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (105) hide show
  1. package/README.md +34 -30
  2. package/package.json +1 -1
  3. package/src/aggregate.ts +248 -0
  4. package/src/backend/c.ts +5 -2
  5. package/src/backend/cfamily.ts +236 -37
  6. package/src/backend/cpp.ts +114 -9
  7. package/src/backend/pascal.ts +39 -0
  8. package/src/cdecl/constant.ts +59 -0
  9. package/src/cdecl/lex.ts +209 -0
  10. package/src/cdecl/parse.ts +975 -0
  11. package/src/cdecl/spell.ts +69 -0
  12. package/src/codegen-flags.ts +38 -6
  13. package/src/compiler-diagnostics.ts +194 -0
  14. package/src/contracts.ts +615 -79
  15. package/src/declare.ts +75 -5
  16. package/src/frontend/asmdata.ts +19 -5
  17. package/src/frontend/call-plan.ts +686 -0
  18. package/src/frontend/device-pins.ts +277 -0
  19. package/src/frontend/errors.ts +5 -0
  20. package/src/frontend/flags-edge.ts +101 -0
  21. package/src/frontend/fpu.ts +167 -0
  22. package/src/frontend/frame-objects.ts +2131 -0
  23. package/src/frontend/high-half.ts +2 -1
  24. package/src/frontend/local-object.ts +490 -0
  25. package/src/frontend/mips.ts +363 -40
  26. package/src/frontend/opaque.ts +97 -18
  27. package/src/frontend/ppc.ts +520 -129
  28. package/src/frontend/reloc-symbol.ts +94 -13
  29. package/src/frontend/splat.ts +108 -5
  30. package/src/frontend/ssa.ts +669 -76
  31. package/src/frontend/stackargs.ts +120 -39
  32. package/src/frontend/thumb.ts +2764 -2018
  33. package/src/ir/alias.ts +29 -0
  34. package/src/ir/core.ts +101 -3
  35. package/src/ir/discipline.ts +248 -0
  36. package/src/ir/float-bits.ts +47 -0
  37. package/src/ir/opcodes.ts +84 -112
  38. package/src/ir/parse.ts +1 -0
  39. package/src/ir/types.ts +118 -2
  40. package/src/ir/verify.ts +150 -3
  41. package/src/l3/address.ts +5 -0
  42. package/src/l3/argcopy.ts +38 -6
  43. package/src/l3/ast.ts +186 -14
  44. package/src/l3/coalesce.ts +21 -13
  45. package/src/l3/dce.ts +32 -10
  46. package/src/l3/gates.ts +9 -3
  47. package/src/l3/hoist.ts +8 -7
  48. package/src/l3/initfirst.ts +27 -9
  49. package/src/l3/intcell.ts +97 -0
  50. package/src/l3/mentions.ts +10 -4
  51. package/src/l3/narrowdecl.ts +218 -0
  52. package/src/l3/nearbase.ts +10 -5
  53. package/src/l3/parkfirst.ts +8 -5
  54. package/src/l3/pollguard.ts +3 -2
  55. package/src/l3/ptrcell.ts +357 -0
  56. package/src/l3/regspell.ts +17 -3
  57. package/src/l3/reindex.ts +10 -7
  58. package/src/l3/symbol-refs.ts +104 -23
  59. package/src/l3/tailmerge.ts +6 -19
  60. package/src/l3/typing.ts +137 -25
  61. package/src/l3/unreduce.ts +4 -4
  62. package/src/l3/volstore.ts +96 -7
  63. package/src/mangle.ts +11 -1
  64. package/src/pattern/engine.ts +19 -16
  65. package/src/pipeline.ts +57 -12
  66. package/src/proto-context.ts +788 -0
  67. package/src/proto.ts +689 -50
  68. package/src/raise/arrays.ts +4 -3
  69. package/src/raise/declared-volatile.ts +131 -0
  70. package/src/raise/divpow2.ts +2 -2
  71. package/src/raise/errors.ts +17 -0
  72. package/src/raise/floathelpers.ts +243 -0
  73. package/src/raise/globalshape.ts +11 -5
  74. package/src/raise/gvn.ts +26 -5
  75. package/src/raise/narrow.ts +29 -2
  76. package/src/raise/narrowlocal.ts +11 -6
  77. package/src/raise/offsetnames.ts +510 -0
  78. package/src/raise/pairparams.ts +81 -0
  79. package/src/raise/paramwidth.ts +7 -0
  80. package/src/raise/pre-recovery.ts +78 -4
  81. package/src/raise/recover.ts +14 -10
  82. package/src/raise/retsink.ts +1 -1
  83. package/src/raise/shortcircuit.ts +41 -19
  84. package/src/raise/softdiv.ts +21 -23
  85. package/src/raise/struct-arrays.ts +5 -4
  86. package/src/raise/structs.ts +311 -98
  87. package/src/raise/truncload.ts +366 -0
  88. package/src/raise/widehelpers.ts +106 -0
  89. package/src/rank-declare.ts +30 -5
  90. package/src/rank-variations.ts +36 -8
  91. package/src/rank.ts +267 -79
  92. package/src/runtime-helpers.ts +287 -0
  93. package/src/stillborn.ts +289 -0
  94. package/src/structure/analysis.ts +1285 -448
  95. package/src/structure/hazards.ts +633 -58
  96. package/src/structure/local-statics.ts +381 -0
  97. package/src/structure/namecoalesce.ts +1 -1
  98. package/src/structure/redundant-test.ts +99 -0
  99. package/src/structure/structure.ts +1772 -395
  100. package/src/structure/switch-recover.ts +159 -78
  101. package/src/symbols.ts +38 -3
  102. package/src/target.ts +662 -59
  103. package/src/trace.ts +44 -24
  104. package/src/variation-definitions.ts +119 -1
  105. package/src/variation-tokens.ts +20 -6
@@ -29,21 +29,37 @@
29
29
  // coalescing (canTakeName + seeding) and emission (structureBlock + the loop emitters) — they
30
30
  // share varName/backArgName mutation and the activeSub/loopCtx dynamic state.
31
31
  //
32
- // Scope: reducible single-latch natural loops — GUARDED self-loop `while` (the guard-fusion
33
- // un-rotation), UNGUARDED self-loop `do-while` (single block, header === latch), test-at-top
34
- // `while`, bottom-test `do-while`, PROPERLY-nested loops, in-body `break`/early-`return`,
35
- // comparison-tree and jump-table `switch`, and a switch arm that FALLS THROUGH into the next one
36
- // (both regimes — see `ArmExit` in switch-recover.ts). Still DECLINED (loud StructureError, never
37
- // wrong code): multi-latch headers, irreducible/overlapping loops, conditional `continue`, a
38
- // `break` whose exit copies would clobber, and mixed-entry self-loops (a guarded header also
39
- // entered by a plain br). Fall-through carries two REFUSALS of its own rather than a decline: a
40
- // target language whose `case` cannot fall through (`spellSwitchFallthrough` false) sends Regime A
41
- // back to if-recovery, and arms that do not linearize into one chain — two arms falling into the
42
- // same sibling, or a fall into the `default:` — refuse in `chainArms`, which answers null.
43
- import { constAddressOf, globalCellOf } from '../ir/alias';
44
- import { Block, Fn, Op, Successor, Value, defOpMap, dominators, mergeClasses, successorsOf } from '../ir/core';
45
- import { CAST_WIDTHS, EFFECTFUL_OPS, SPELLED_WHEN_DEAD_OPS, opSig } from '../ir/opcodes';
46
- import { type IrType, T, scalarTypeForAccess, typeEquals } from '../ir/types';
32
+ // Scope: reducible natural loops — GUARDED self-loop `while` (the guard-fusion un-rotation),
33
+ // UNGUARDED self-loop `do-while` (single block, header === latch), test-at-top `while`,
34
+ // bottom-test `do-while`, `while (1)` for any other latch set (a conditional `continue`, a
35
+ // mid-tested header), PROPERLY-nested loops, in-body `break`/early-`return`, comparison-tree and
36
+ // jump-table `switch`, and a switch arm that FALLS THROUGH into the next one (both regimes — see
37
+ // `ArmExit` in switch-recover.ts). Still DECLINED (loud StructureError, never wrong code):
38
+ // irreducible/overlapping loops, exits to more than one live merge, a `break` out of a nested loop's
39
+ // body, a `break` whose exit copies would clobber, and mixed-entry self-loops (a guarded header also
40
+ // entered by a plain br).
41
+ // Fall-through carries two REFUSALS of its own rather than a decline: a target language whose
42
+ // `case` cannot fall through (`spellSwitchFallthrough` false) sends Regime A back to if-recovery,
43
+ // and arms that do not linearize into one chain — two arms falling into the same sibling, or a fall
44
+ // into the `default:` — refuse in `chainArms`, which answers null.
45
+ import { constAddressOf, globalBaseOf } from '../ir/alias';
46
+ import {
47
+ Block,
48
+ Fn,
49
+ Op,
50
+ Successor,
51
+ Value,
52
+ callArgs,
53
+ calleeName,
54
+ calleeValue,
55
+ defOpMap,
56
+ dominators,
57
+ mergeClasses,
58
+ successorsOf,
59
+ } from '../ir/core';
60
+ import { effectful, placedAt, qualified, qualifiedBy, spelledWhenDead } from '../ir/discipline';
61
+ import { CAST_WIDTHS, opSig } from '../ir/opcodes';
62
+ import { type IrType, T, intWidth, scalarTypeForAccess, typeEquals, unionViewAt } from '../ir/types';
47
63
  import {
48
64
  BinOp,
49
65
  Expr,
@@ -53,15 +69,21 @@ import {
53
69
  exprChildren,
54
70
  exprHasEffect,
55
71
  gapReasonFor,
72
+ isLoop,
56
73
  mapExprChildren,
57
74
  mapStmtExprs,
75
+ mentionedName,
58
76
  negateCond,
59
77
  stmtChildren,
78
+ stmtExprs,
79
+ stmtsEqual,
60
80
  walkExprs,
61
81
  } from '../l3/ast';
62
82
  import { type Gate, firstRejection } from '../l3/gates';
63
- import { exprCType, provablyNonNegative, ptrElemBytes, renderedIntSignedness } from '../l3/typing';
83
+ import { exprCType, exprIntWidth, provablyNonNegative, ptrElemBytes, renderedIntSignedness } from '../l3/typing';
84
+ import { castOffDeclaration, qualifiedMemoryAccess } from '../l3/volstore';
64
85
  import { foldConstPair, isConstFoldOpcode } from '../raise/const';
86
+ import { stampDeclaredVolatile } from '../raise/declared-volatile';
65
87
  import { returnType } from '../raise/recover';
66
88
  import { collectStructs } from '../raise/structs';
67
89
  import {
@@ -78,7 +100,7 @@ import {
78
100
  scalarCellType,
79
101
  structFieldInnerExtents,
80
102
  } from '../symbols';
81
- import { analyze } from './analysis';
103
+ import { analyze, shortCircuitGuardedValues } from './analysis';
82
104
  import { makeBitfieldSpelling } from './bitfields';
83
105
  import {
84
106
  addOffset,
@@ -90,11 +112,20 @@ import {
90
112
  globalOf,
91
113
  subscriptsFromExtents,
92
114
  } from './globalaccess';
93
- import { makeLoopHazards, sunkCopyOverDroppedUndef, updateWriteSet } from './hazards';
115
+ import {
116
+ PREUPDATE_SINK_GATES,
117
+ type SinkCandidate,
118
+ makeLoopHazards,
119
+ preUpdateCopyHome,
120
+ sunkCopyOverDroppedUndef,
121
+ updateWriteSet,
122
+ } from './hazards';
123
+ import { type StaticLayout, localStaticShapes, nameLocalStatics } from './local-statics';
94
124
  import { type NaturalLoop, analyzeLoops } from './loops';
95
125
  import { type NameMerge, coalesceNames } from './namecoalesce';
126
+ import { testRereadsOnly } from './redundant-test';
96
127
  import { unspelledEpilogues } from './retspell';
97
- import { type ArmExit, makeSwitchRecovery } from './switch-recover';
128
+ import { type ArmExit, type SwitchBoundCase, makeSwitchRecovery } from './switch-recover';
98
129
 
99
130
  // Lower a constant-offset memory access to its lvalue/rvalue Expr. If the base was recovered as a
100
131
  // struct pointer (raise/structs.ts), the byte offset resolves to a NAMED field (`base->field_<off>`);
@@ -229,6 +260,22 @@ function spellsAccessType(declared: boolean | undefined, width: number, signed:
229
260
  return typeEquals(T.int(width * 8, declared), scalarTypeForAccess(width, signed));
230
261
  }
231
262
 
263
+ /** The declared type of a frame-local object, from the three facts its `laddr` carries: `count`
264
+ * elements of `width` bytes, signed or not. ONE element is a SCALAR typed by the access every
265
+ * access agreed on. More is STORAGE the frontend's frame-object audit sized without typing —
266
+ * no access pins an element type there, so the audit stamps bytes and this declares the array
267
+ * that spells them. Both arms are the audit's own facts; neither infers anything here. */
268
+ function laddrType(op: Op): IrType {
269
+ // a call's struct-return storage: the audit typed its address as a pointer to that struct
270
+ const t = op.results[0].type;
271
+ if (op.attrs.aggregate === true && t.kind === 'ptr') {
272
+ return t.to;
273
+ }
274
+ const elem = T.int((op.attrs.width as number) * 8, op.attrs.signed as boolean);
275
+ const count = op.attrs.count as number;
276
+ return count === 1 ? elem : T.array(elem, count);
277
+ }
278
+
232
279
  /** May a member be NAMED by an access of this direction, given the qualifiers on its declaration?
233
280
  * The named spelling REPLACES a cast through `(u8 *)`, which carries no qualifier at all, so a
234
281
  * qualifier the name reintroduces changes what the compiler emits:
@@ -636,6 +683,23 @@ function globalConstByte(baseExpr: Expr, off: number): { name: string; byte: num
636
683
  return null;
637
684
  }
638
685
 
686
+ /** The whole-element subscript a constant byte displacement denotes at a given element width — the
687
+ * `idxVal + off / width` fold every indexed spelling below shares, with the rule
688
+ * structure/globalaccess.ts `elementIndex` applies to a VARIABLE residual applied to the constant
689
+ * one. A displacement that is not a whole number of elements has no subscript spelling at all
690
+ * (`[0.5]` is not C, and agbcc answers `array subscript is not an integer`), so it declines LOUD
691
+ * here, where the fraction would be computed, rather than leaving the layer above to have refused
692
+ * every base that can reach it. */
693
+ function displacementIndex(off: number, width: number): number {
694
+ if (off % width !== 0) {
695
+ throw new StructureError(
696
+ `a ${width}-byte access at byte ${off} of its base is not a whole number of elements from it — ` +
697
+ `no subscript spells it`,
698
+ );
699
+ }
700
+ return off / width;
701
+ }
702
+
639
703
  function memAccess(
640
704
  base: Value,
641
705
  baseExpr: Expr,
@@ -742,7 +806,7 @@ function memAccess(
742
806
  return {
743
807
  k: 'index',
744
808
  base: { k: 'var', name: gbb!.name },
745
- idx: addOffset(multi.idx, off / width),
809
+ idx: addOffset(multi.idx, displacementIndex(off, width)),
746
810
  width,
747
811
  signed,
748
812
  lead: multi.lead,
@@ -761,7 +825,7 @@ function memAccess(
761
825
  if (off === 0 && idxVal.k === 'const' && idxVal.value === 0 && scalarGlobals.has(g.name)) {
762
826
  return { k: 'var', name: g.name };
763
827
  }
764
- const idx = addOffset(idxVal, off / width);
828
+ const idx = addOffset(idxVal, displacementIndex(off, width));
765
829
  // ARRAY-declared global (symbol map): index the bare name — `gSym[i]`, the spelling the
766
830
  // dogfood proved agbcc needs for ROM tables — with the element type registered in the env
767
831
  // so the stride check passes and no cast is added. Element-width match only.
@@ -781,9 +845,33 @@ function memAccess(
781
845
  // the array-element form — wrong for a pointer). Anything else is cast to the recovered
782
846
  // struct pointer type; the cast node prints with `->`.
783
847
  const ok = rt?.kind === 'ptr' && rt.to.kind === 'struct' && rt.to.name === bt.to.name && baseExpr.k !== 'index';
784
- return { k: 'field', base: ok ? baseExpr : { k: 'cast', to: bt, e: baseExpr }, name: `field_${off}` };
848
+ const structBase = ok ? baseExpr : { k: 'cast' as const, to: bt, e: baseExpr };
849
+ // A byte range recovered as a UNION member (raise/structs.ts) is read through the view of the
850
+ // access's own width and extension: `p->field_0.word`, `p->field_0.half[1]`, `p->field_0.shalf`.
851
+ const u = unionViewAt(bt.to, off, width, signed, isStore);
852
+ if (u === undefined) {
853
+ return { k: 'field', base: structBase, name: `field_${off}` };
854
+ }
855
+ // An internal invariant: the builder made a view for every width and narrow extension a union's
856
+ // own base accessed, and a value that inherits the type through recoverTypes carries a pointee
857
+ // of its own instead.
858
+ if (u.view === undefined) {
859
+ throw new StructureError(
860
+ `a ${width}-byte ${isStore ? 'store' : signed ? 'signed load' : 'unsigned load'} at byte ${off} has no view in the union member '${u.member.name}'`,
861
+ );
862
+ }
863
+ const cell: Expr = { k: 'field', base: structBase, name: u.member.name };
864
+ const view: Expr = { k: 'field', base: cell, name: u.view.name, dot: true };
865
+ return u.index === null ? view : { k: 'index', base: view, idx: { k: 'const', value: u.index }, width, signed };
785
866
  }
786
- return { k: 'index', base: baseExpr, idx: { k: 'const', value: off / width }, width, signed, ...addressEvidence };
867
+ return {
868
+ k: 'index',
869
+ base: baseExpr,
870
+ idx: { k: 'const', value: displacementIndex(off, width) },
871
+ width,
872
+ signed,
873
+ ...addressEvidence,
874
+ };
787
875
  }
788
876
 
789
877
  // A variable-index array access `base[index]`; `base[index].field_K` when a `fieldOff` marks an
@@ -932,7 +1020,7 @@ const CMP_TO_BIN: Record<string, BinOp> = {
932
1020
  icmp_eq: '==',
933
1021
  icmp_ne: '!=',
934
1022
  };
935
- const ARITH_TO_BIN: Record<string, BinOp> = {
1023
+ export const ARITH_TO_BIN: Record<string, BinOp> = {
936
1024
  add: '+',
937
1025
  sub: '-',
938
1026
  mul: '*',
@@ -952,10 +1040,48 @@ const ARITH_TO_BIN: Record<string, BinOp> = {
952
1040
  logic_or: '||', // short-circuit connectives (raise/shortcircuit.ts)
953
1041
  };
954
1042
 
1043
+ // The float ops (ir/opcodes.ts) and their L3 operators (l3/ast.ts BinOp). A table of their own and
1044
+ // not entries in ARITH_TO_BIN, because every rule that table's consumers apply — the pointer
1045
+ // stride, the integer legalizations, the constant fold, the commutative re-spelling — is an
1046
+ // integer rule.
1047
+ const FLOAT_TO_BIN: Record<string, BinOp> = { fadd: 'f+', fsub: 'f-', fmul: 'f*', fdiv: 'f/' };
1048
+
955
1049
  // The operators whose operand order the machine does not fix — candidates for the def-order
956
1050
  // re-spelling in lowerDef. `&&`/`||` are excluded: short-circuit order IS semantics.
957
1051
  const COMMUTATIVE_BIN: ReadonlySet<BinOp> = new Set(['+', '*', '&', '|', '^']);
958
1052
 
1053
+ // Spell a loop update at the place the bottom test reads the variable: the one `name` leaf becomes
1054
+ // `name++`. Returns null unless the rendered test MENTIONS `name` exactly once, and that mention is
1055
+ // a readable leaf. `&v1` is a mention the `++` cannot be placed on and cannot be counted out of —
1056
+ // a callee reads the object through it — so a test holding one alongside a bare `v1` is refused
1057
+ // here rather than left to `assertPostIncrUnshared`, which would abort the lift instead.
1058
+ //
1059
+ // `preUpdateCondFold` decided that on the DEF TREE, and this asks the same question of what the
1060
+ // lowering actually produced. The two can disagree — an idiom fold spells a value its operands do
1061
+ // not show, a substitution stops the render short of a leaf the walk reached — and the two
1062
+ // disagreements are not equally bad: a test that named the variable twice would be C89-undefined,
1063
+ // and one that names it not at all would drop the update altogether. Both answer null, and the
1064
+ // caller declines.
1065
+ //
1066
+ // No input reaches either answer today, so it is pinned directly, on hand-built expressions
1067
+ // (test/loop-preupdate-cond.test.ts) — the discipline `sunkCopyOverDroppedUndef` is held to.
1068
+ export function spellUpdateInCond(cond: Expr, name: string, by: 1 | -1): Expr | null {
1069
+ let found = 0;
1070
+ let mentions = 0;
1071
+ const rec = (e: Expr): Expr => {
1072
+ if (mentionedName(e) === name) {
1073
+ mentions++;
1074
+ }
1075
+ if (e.k === 'var' && e.name === name) {
1076
+ found++;
1077
+ return { k: 'postincr', name, by };
1078
+ }
1079
+ return mapExprChildren(e, rec);
1080
+ };
1081
+ const out = rec(cond);
1082
+ return found === 1 && mentions === 1 ? out : null;
1083
+ }
1084
+
959
1085
  // Recovered info for a self-loop header: its exit block and the per-parameter back-edge
960
1086
  // arg it feeds (the value on the header→header edge). The back-edge arg is the "next"
961
1087
  // value of the phi; mapping it back to the phi turns the latch test into the while test.
@@ -980,6 +1106,18 @@ interface LoopArm {
980
1106
  owned: Set<Block>;
981
1107
  }
982
1108
 
1109
+ // A loop with no test of its own, `while (1)`: its latches are conditional continues, and it leaves
1110
+ // by a `break` to `exit` or an early `return`.
1111
+ interface ForeverLoopInfo {
1112
+ header: Block;
1113
+ latches: Block[];
1114
+ forwardPreds: Block[];
1115
+ exit: Block;
1116
+ body: Set<Block>;
1117
+ arms: LoopArm[];
1118
+ breaks: Set<Block>; // the body blocks whose edge to `exit` is a `break`
1119
+ }
1120
+
983
1121
  // A test-at-top multi-block `while`. The header is a pure test whose cond_br enters `bodyEntry`
984
1122
  // (inside the loop) or leaves to `exit` (the single loop exit). Unlike LoopInfo the condition reads
985
1123
  // the header's params directly (top-of-iteration values) — no back-edge substitution.
@@ -991,6 +1129,7 @@ interface WhileLoopInfo {
991
1129
  forwardPreds: Block[]; // header preds outside the loop body (the entry/init side)
992
1130
  body: Set<Block>; // the pure natural-loop body (for in-body vs exit classification)
993
1131
  arms: LoopArm[]; // the early-`return` exits out of the body (`earlyReturnArm`)
1132
+ breaks: Set<Block>; // the body blocks whose edge to `exit` itself is a `break`
994
1133
  }
995
1134
 
996
1135
  // Opcodes whose NUMBER OF EXECUTIONS is observable. Moving one of these out of a loop changes what
@@ -1171,6 +1310,8 @@ function reHomesParamMerge(
1171
1310
  export interface CarrierName {
1172
1311
  /** the merge is a pure alias — every in-edge hands it the same value */
1173
1312
  readonly pureAlias: boolean;
1313
+ /** the name offered is that value's OWN, not a loop variable's name offered through its back edge */
1314
+ readonly ownName: boolean;
1174
1315
  /** the name's declaration and the parameter disagree about carrier width */
1175
1316
  readonly widthDiffers: boolean;
1176
1317
  /** …or, at a sub-word width, about signedness — which at that width is part of the width */
@@ -1185,6 +1326,9 @@ export interface CarrierName {
1185
1326
  readonly carrierWritten: boolean;
1186
1327
  /** a value with no name of its own re-derives the name at a use past the copy */
1187
1328
  readonly reDerivesName: boolean;
1329
+ /** …or reads it through a loop's back-edge argument, which carries the loop variable's name
1330
+ * (`backArgName`) and renders as that name past its loop */
1331
+ readonly backArgReadsName: boolean;
1188
1332
  }
1189
1333
 
1190
1334
  /** `canTakeName`'s admission: may this block parameter be SPELLED with a name that already exists,
@@ -1239,6 +1383,28 @@ export const CARRIER_NAME_GATES: readonly Gate<CarrierName>[] = [
1239
1383
  guardedBy: 'name-clobber.test.ts: the inlined difference keeps reading the value the asm computed it from',
1240
1384
  rejects: (c) => !c.pureAlias && c.reDerivesName,
1241
1385
  },
1386
+ {
1387
+ // A back-edge argument is in no `varName`: it takes the header's name unconditionally
1388
+ // (`seedLoopParams`), and the exit region's substitution renders it as that name after the loop.
1389
+ // So a merge there offered the same name overwrites it on every other in-edge. `int u; do { … u =
1390
+ // …; } while (--d > 0); return (c != 1 ? u : u + t) * u;` at mwcc: the merge took `u`'s name, and
1391
+ // its `else` copy overwrote the `u` the multiply still reads.
1392
+ id: 'back-arg-live',
1393
+ why: "a loop's back-edge value renders under the loop variable's name after the loop, and the copies would overwrite it",
1394
+ sound: true,
1395
+ guardedBy: 'name-clobber.test.ts: a back-edge argument live across a merge reads the name it carries',
1396
+ rejects: (c) => !c.pureAlias && c.backArgReadsName,
1397
+ },
1398
+ {
1399
+ // `carrier-live` and `re-derives` waive a pure alias because the alias and its value are equal on
1400
+ // every path under the value's own name. A loop variable's name holds the value only once the back
1401
+ // edge that carries it has run.
1402
+ id: 'alias-under-its-own-name',
1403
+ why: "a loop variable's name holds the value it is offered for only after the back edge that carries it",
1404
+ sound: true,
1405
+ guardedBy: 'carrier-name.test.ts: a redundant merge shares only its value’s own name, not a later loop variable’s',
1406
+ rejects: (c) => c.pureAlias && !c.ownName && (c.carrierLive || c.reDerivesName),
1407
+ },
1242
1408
  ];
1243
1409
 
1244
1410
  /** A nested loop's header parameter offered its ENCLOSING loop's name, as
@@ -1390,11 +1556,11 @@ export interface StructureOptions {
1390
1556
  // body). GCC freely uses `!=`; IDO prefers `==`/`<`. A compiler behavior, not an `arch ==`
1391
1557
  // branch — default true (permissive; the decline path keeps it sound either way).
1392
1558
  switchAllowsNeqCase?: boolean;
1393
- // Comparison-tree switch recovery: treat a relational test whose BRANCH admits exactly one
1394
- // scrutinee value as that case rather than as navigation. A compiler behavior declared in
1395
- // TargetDescription.compilerBehaviors — a compiler opts in on evidence that its dispatch jumps
1396
- // straight to a bounded subtree's body. Default false: absent, every relational edge navigates.
1397
- switchAllowsBoundCase?: boolean;
1559
+ // Comparison-tree switch recovery: treat a relational test as a case where the values that can
1560
+ // reach it leave exactly one on a side its compiler's dispatch lands a body on — the BRANCH
1561
+ // (`'taken'`) or either side (`'either'`). A compiler behavior declared in
1562
+ // TargetDescription.compilerBehaviors. Absent: every relational edge navigates.
1563
+ switchBoundCase?: SwitchBoundCase;
1398
1564
  // Comparison-tree switch recovery: emit the case arms in the order the ASSEMBLY lays their
1399
1565
  // bodies out, rather than sorted by ascending case value. A compiler behavior declared in
1400
1566
  // TargetDescription.compilerBehaviors — a compiler opts in on evidence that it neither reorders
@@ -1440,6 +1606,13 @@ export interface StructureOptions {
1440
1606
  // spell "no" on a public option type. The translation happens once, in `structureOptionsFor`
1441
1607
  // (target.ts), which is where the target-to-structurer mapping lives.
1442
1608
  spillSlotOrder?: 'ascending' | 'descending';
1609
+ // Each declared callee's parameter types, carried to the backend on `SFn.declaredArgs` (see
1610
+ // there). Built by `structureOptionsFor` from the prototype table.
1611
+ declaredArgs?: Readonly<Record<string, readonly (string | undefined)[]>>;
1612
+ // Each declared callee's return type, carried on `SFn.declaredReturns` the same way.
1613
+ declaredReturns?: Readonly<Record<string, string>>;
1614
+ // The emitted C is compiled as C++ (`SFn.dialect`).
1615
+ dialect?: 'c++';
1443
1616
  // Commutative load pairs re-spell in def (evaluation) order — see the swap in lowerDef. Default
1444
1617
  // true; verified byte-exact on agbcc and IDO. A compiler behavior declared in
1445
1618
  // TargetDescription.compilerBehaviors: the first compiler whose scheduler is shown re-ordering
@@ -1485,6 +1658,10 @@ export interface StructureOptions {
1485
1658
  // `deviceMemoryWriters`, which no structurer rule reads). Used as a REFUSAL: absent, a dead read
1486
1659
  // at a literal address is dropped.
1487
1660
  deviceRegisters?: readonly [number, number];
1661
+ // Does the compiler extend a `volatile` narrow signed read in a register, never in the load? The
1662
+ // `declared` stamp structuring re-derives reads it (TargetDescription.compilerBehaviors'
1663
+ // `volatileReadsExtendInRegister`), so it leaves unplaced the read the lift's stamp did.
1664
+ volatileReadsExtendInRegister?: boolean;
1488
1665
  // Spell `(x << a) >> b` extracts of a struct global as the map's named bitfield member. On by
1489
1666
  // default; rank.ts enumerates the OFF spelling as the `/no-bitfield` variation, because the named
1490
1667
  // read recompiles at the DECLARATION's access width — where that diverges from the asm's load
@@ -1552,11 +1729,24 @@ export interface StructureOptions {
1552
1729
  // computation. Off by default; rank.ts enumerates the ON spelling as the `/derived-home` variation —
1553
1730
  // see analysis.ts AnalyzeOptions.
1554
1731
  homeDerivedReads?: boolean;
1732
+ // Can this compiler FUSE a float multiply into the add or subtract that reads it, when both are
1733
+ // written in one expression? Then every such product is named (analysis.ts), so the spelling
1734
+ // compiles to the unfused pair the object holds. A compiler opts in; the target says which
1735
+ // (`compilerBehaviors.contractsFloatProducts`).
1736
+ contractsFloatProducts?: boolean;
1737
+ // How this compiler lays out a function-scope static (`compilerBehaviors.staticLayout`), which
1738
+ // decides the definition that puts one where the target has it. Absent ⇒ unmeasured, and a
1739
+ // function that defines a static declines.
1740
+ staticLayout?: StaticLayout;
1555
1741
  // Materialize a pure value that one join's incoming edges render into the SAME parameter slot
1556
1742
  // from 2+ places — the value the source computed once above the branch and the copy machinery
1557
1743
  // sinks into every arm. Off by default; rank.ts enumerates the ON spelling as the `/merge-home`
1558
1744
  // variation — see analysis.ts AnalyzeOptions.
1559
1745
  homeMergeFeeds?: boolean;
1746
+ // Materialize a `zext`/`sext` with 2+ consumers, none of them in its own block — the register the
1747
+ // asm narrowed into once and every later block read. Off by default; rank.ts enumerates the ON
1748
+ // spelling as the `/escape-home` variation — see analysis.ts AnalyzeOptions.
1749
+ homeEscapingExtensions?: boolean;
1560
1750
  // Emit a memory read as a named temp in ITS OWN block when every place it renders sits in a
1561
1751
  // block that block strictly dominates. A compiler behavior (TargetDescription
1562
1752
  // .compilerBehaviors), not a differ-refereed variation: where the compiler has neither a scheduler
@@ -1621,6 +1811,12 @@ export interface StructureOptions {
1621
1811
  * A SUPERSET of `inferredSymbols`' names, and its own field for exactly that reason: a struct
1622
1812
  * element is licensed here and has no shape there. */
1623
1813
  orderLicensedGlobals?: ReadonlySet<string>;
1814
+ /** The globals the project map declares POINTERS, handed to a structuring that runs without the
1815
+ * map (rank.ts `/raw-globals`). Spelling a global raw does not make it an integer: C still scales
1816
+ * arithmetic on its value by the declared pointee, so the byte-arithmetic rules read this where
1817
+ * they would read the map's `shape:'pointer'`. The operand-order rules do not read it: no element
1818
+ * or field spelling folds from a name with no pointee. */
1819
+ pointerGlobals?: ReadonlySet<string>;
1624
1820
  }
1625
1821
 
1626
1822
  /** The named global an `index` node's base denotes DIRECTLY, or undefined: the bare `&gSym` an
@@ -1676,12 +1872,17 @@ export interface StructureHooks {
1676
1872
  /** `enclosingCarrierName`'s admission rules (`ENCLOSING_CARRIER_GATES`), ablatable the same way —
1677
1873
  * and, wrapped in `tallying`, the census of which rule refuses a nest. */
1678
1874
  enclosingCarrierGates?: readonly Gate<EnclosingCarrier>[];
1875
+ /** `sinkablePreUpdateSlots`'s per-slot rules (`PREUPDATE_SINK_GATES`), ablatable the same way. */
1876
+ preUpdateSinkGates?: readonly Gate<SinkCandidate>[];
1679
1877
  /** Every branch-sense site this structuring reached, in emission order: the block index
1680
1878
  * `StructureOptions.branchSenseFlipSites` names, whether the site is JOINED or divergent, and
1681
1879
  * which sense it actually emitted. The enumeration domain — a site only exists once structuring
1682
1880
  * has decided both arms are real, so it cannot be computed ahead of the pass. */
1683
1881
  onBranchSenseSite?: (site: { block: number; ordinal: number; joined: boolean; negated: boolean }) => void;
1684
- /** Every divergent `if` `followEarlyReturns` gave a follow: the `if`'s block and the follow's. */
1882
+ /** Every branch `followOverReturns` gave a follow: the branch's block and the follow's. Both
1883
+ * callers fire it — a divergent `if` under `followEarlyReturns`, and EVERY jump table whose arms
1884
+ * all return, unconditionally — so the sites it reports are not the enumeration domain of the
1885
+ * shared-tail twins (that domain is `hasDivergentSharedRet`, which asks for a `cond_br`). */
1685
1886
  onEarlyReturnFollow?: (site: { block: number; follow: number }) => void;
1686
1887
  }
1687
1888
 
@@ -1709,6 +1910,7 @@ function assertDefaultAccepts(fn: Fn, opts: StructureOptions, hooks: StructureHo
1709
1910
  homeLoopExprs: false,
1710
1911
  homeDerivedReads: false,
1711
1912
  homeMergeFeeds: false,
1913
+ homeEscapingExtensions: false,
1712
1914
  anchorConstCopies: false,
1713
1915
  anchorLoopEntryConsts: false,
1714
1916
  followEarlyReturns: false,
@@ -1727,22 +1929,37 @@ interface EarlyReturnArmDeps {
1727
1929
  function isRet(blk: Block): boolean {
1728
1930
  return blk.ops[blk.ops.length - 1]?.opcode === 'ret';
1729
1931
  }
1730
- /** The `ret`s reachable from BOTH successors of `b` — the region `followEarlyReturns` keeps. Empty
1731
- * when there is none, and when `b` does not branch two ways. `reachFrom` is forward reachability,
1732
- * the start block excluded. */
1932
+ /** The `ret`s reachable from EVERY successor of `b` — the region `followOverReturns` keeps. Empty
1933
+ * when there is none, and when `b` does not branch at all. `reachFrom` is forward reachability,
1934
+ * the start block excluded.
1935
+ *
1936
+ * THE QUESTION IS ABOUT A BRANCH, not about an `if`: "the arms all return, so post-dominance gives
1937
+ * this block no join — where does the construct end?" is asked by a jump table exactly as by a
1938
+ * two-way branch, and the answer is derived the same way. The `if`-shaped reading belongs to the
1939
+ * CALLER that wants it (`hasDivergentSharedRet`), not here.
1940
+ *
1941
+ * THE DEDUP IS DEFENSIVE AND NO CALLER CAN REACH IT TODAY. It is what makes "two successors that
1942
+ * are the same block" answer `[]` rather than "a branch with a shared `ret`". Both callers ask
1943
+ * only when `ipdom.get(b)` is null, and a branch whose successors are all ONE block has that block
1944
+ * as its post-dominator, so they never ask it of that shape. It stays because the answer belongs
1945
+ * to this function rather than to its callers' guards, and dropping it is a no-op on the corpus:
1946
+ * the reader over every row of the committed artifact (`decompile` per row, no compiling) emits
1947
+ * byte-identical source with and without it. */
1733
1948
  function sharedRetsOf(b: Block, reachFrom: (x: Block) => ReadonlySet<Block>): Block[] {
1734
- const [s1, s2] = b.ops[b.ops.length - 1]?.opcode === 'cond_br' ? successorsOf(b) : [];
1735
- if (s1 === undefined || s2 === undefined || s1 === s2) {
1949
+ const succs = [...new Set(successorsOf(b))];
1950
+ if (succs.length < 2) {
1736
1951
  return [];
1737
1952
  }
1738
1953
  const retsFrom = (x: Block): Block[] => [x, ...reachFrom(x)].filter(isRet);
1739
- const fromS2 = new Set(retsFrom(s2));
1740
- return retsFrom(s1).filter((r) => fromS2.has(r));
1954
+ return succs.map(retsFrom).reduce((shared, rets) => {
1955
+ const here = new Set(rets);
1956
+ return shared.filter((r) => here.has(r));
1957
+ });
1741
1958
  }
1742
1959
  /** Is there an `if` whose arms reach no common block before EXIT but share a `ret` — the only shape
1743
1960
  * `followEarlyReturns` changes? The enumeration gate of both shared-tail twins (rank.ts), asked of
1744
1961
  * the fn as raised and again of the sunk fn; a superset, since it asks `sharedRetsOf` and none of
1745
- * the follow's three later refusals. */
1962
+ * the follow's three later refusals. An `if`, specifically: see the restriction at the `some`. */
1746
1963
  export function hasDivergentSharedRet(fn: Fn): boolean {
1747
1964
  const ipdom = postDominators(fn);
1748
1965
  const reach = new Map<Block, Set<Block>>();
@@ -1761,7 +1978,13 @@ export function hasDivergentSharedRet(fn: Fn): boolean {
1761
1978
  }
1762
1979
  return out;
1763
1980
  };
1764
- return fn.blocks.some((b) => ipdom.get(b) === null && sharedRetsOf(b, reachFrom).length > 0);
1981
+ // `cond_br` HERE, not in `sharedRetsOf`: this is the enumeration gate of the shared-tail twins,
1982
+ // and those twins are `if`-shaped variations. A jump table with a shared `ret` has the same
1983
+ // no-join shape and no such twin, so asking for it would spend fan on a duplicate candidate.
1984
+ return fn.blocks.some(
1985
+ (b) =>
1986
+ b.ops[b.ops.length - 1]?.opcode === 'cond_br' && ipdom.get(b) === null && sharedRetsOf(b, reachFrom).length > 0,
1987
+ );
1765
1988
  }
1766
1989
  /** does a path from `from` reach `to` without passing through `avoid`? */
1767
1990
  function reachesAvoiding(from: Block, to: Block, avoid: Block): boolean {
@@ -1822,7 +2045,7 @@ function earlyReturnArm(
1822
2045
  } // re-enters the loop → not an exit
1823
2046
  if (entryOwned && dom.get(bb)!.has(to)) {
1824
2047
  owned.add(bb);
1825
- } else if (bb.ops.some((op) => EFFECTFUL_OPS.has(op.opcode))) {
2048
+ } else if (bb.ops.some(effectful)) {
1826
2049
  return null;
1827
2050
  }
1828
2051
  const t = bb.ops[bb.ops.length - 1];
@@ -1860,11 +2083,14 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
1860
2083
  orderArgCopiesByWriteOrder = true,
1861
2084
  preferDefPosCopyOrder = false,
1862
2085
  switchAllowsNeqCase = true,
1863
- switchAllowsBoundCase = false,
2086
+ switchBoundCase,
1864
2087
  switchArmsFollowLayout = false,
1865
2088
  switchRequiresFrontLoadedTests = false,
1866
2089
  spellSwitchFallthrough = true,
1867
2090
  spillSlotOrder,
2091
+ declaredArgs,
2092
+ declaredReturns,
2093
+ dialect,
1868
2094
  defOrderLoadPairs = true,
1869
2095
  anchorConstCopies = false,
1870
2096
  anchorLoopEntryConsts = false,
@@ -1878,7 +2104,10 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
1878
2104
  homeLoopExprs = false,
1879
2105
  homeDerivedReads = false,
1880
2106
  homeMergeFeeds = false,
2107
+ homeEscapingExtensions = false,
1881
2108
  readsStayWhereWritten = false,
2109
+ contractsFloatProducts = false,
2110
+ staticLayout,
1882
2111
  unsignedCompareSpelling = false,
1883
2112
  coalesceMergeNames = false,
1884
2113
  freshParamMerge = false,
@@ -1887,16 +2116,43 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
1887
2116
  symbols: mapSymbols,
1888
2117
  inferredSymbols,
1889
2118
  orderLicensedGlobals,
2119
+ pointerGlobals,
1890
2120
  } = opts;
2121
+ // A read this stamp is the first to place was plain to every raising pass, and in a `&&`/`||`'s
2122
+ // guarded operand a short-circuit fold may have lifted it out of the arm it ran in: which side of
2123
+ // the branch the asm read it on is lost. The analysis names a counted op it finds there above the
2124
+ // branch, which is right only for one no fold could move, so this read declines instead.
2125
+ const stampedLate = stampDeclaredVolatile(fn, mapSymbols, opts);
2126
+ if (stampedLate.length > 0) {
2127
+ const defsNow = defOpMap(fn);
2128
+ const guarded = shortCircuitGuardedValues(fn, defsNow);
2129
+ const moved = stampedLate.find((op) => op.results.some((r) => guarded.has(r)));
2130
+ if (moved !== undefined) {
2131
+ throw new StructureError(
2132
+ `cannot structure '${fn.name}': a '&&'/'||' would guard a read of the volatile object ` +
2133
+ `'${globalBaseOf(defsNow, moved.operands[0]) ?? 'a volatile object'}', and which side of the branch the asm read it on is not recoverable`,
2134
+ );
2135
+ }
2136
+ }
1891
2137
  // THE shape dictionary the rendering context asks, map-first. Built as a lookup rather than a
1892
2138
  // merged Map because the map is the PROJECT's and is asked by name for a whole project's worth
1893
2139
  // of symbols — copying it per structuring is work proportional to the project, and a ranked run
1894
2140
  // structures one function thousands of times (the same argument the `laddr` name minter makes).
2141
+ //
2142
+ // A function-scope static is asked by its linker name, which no map holds, and comes FIRST: its
2143
+ // shape is its definition's, never the one raise/globalshape.ts infers from its accesses.
2144
+ const localStatics = localStaticShapes(fn, staticLayout);
2145
+ if ('refused' in localStatics) {
2146
+ throw new StructureError(
2147
+ `cannot structure '${fn.name}': names a function-scope static ('${localStatics.symbol}') ${localStatics.refused}`,
2148
+ );
2149
+ }
2150
+ const staticInfo = localStatics.infos;
1895
2151
  const symbols: SymbolLookup | undefined =
1896
- mapSymbols !== undefined || (inferredSymbols !== undefined && inferredSymbols.size > 0)
2152
+ mapSymbols !== undefined || (inferredSymbols !== undefined && inferredSymbols.size > 0) || staticInfo.size > 0
1897
2153
  ? {
1898
- get: (n) => mapSymbols?.get(n) ?? inferredSymbols?.get(n),
1899
- has: (n) => mapSymbols?.has(n) === true || inferredSymbols?.has(n) === true,
2154
+ get: (n) => staticInfo.get(n) ?? mapSymbols?.get(n) ?? inferredSymbols?.get(n),
2155
+ has: (n) => staticInfo.has(n) || mapSymbols?.has(n) === true || inferredSymbols?.has(n) === true,
1900
2156
  }
1901
2157
  : undefined;
1902
2158
  // Only the MAP makes the named bitfield spelling available, so with no map this is not a question.
@@ -1913,8 +2169,12 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
1913
2169
  // IS this target's compiler behaviors, so resetting one would probe a spelling asmlift never emits
1914
2170
  // here.
1915
2171
  //
1916
- // FOUR OF THE ELEVEN `STRUCTURE_VARIATIONS` ENTRIES (rank-variations.ts) ARE DELIBERATE NON-MEMBERS,
1917
- // each for its own reason, and the list here is the half of the split this side owns:
2172
+ // THE STRUCTURE VARIATIONS `assertDefaultAccepts` DOES NOT RESET (rank-variations.ts) ARE
2173
+ // DELIBERATE NON-MEMBERS, each for its own reason, and this is the whole of that list — the one
2174
+ // place it is stated, derived from the guard's own reset list and held to it by
2175
+ // test/variation-offers.test.ts. Every variation NOT named here is one the guard resets, so the
2176
+ // invariant it protects — a candidate spelling never unlocks a function the default declines —
2177
+ // rests on the guard for those and on the argument beside each name for these:
1918
2178
  // - `/site-sense` (senseFromFoldEvidence) decides which way each folded branch is written. It
1919
2179
  // negates a branch sense per site and touches no copy, like the per-function sense booleans;
1920
2180
  // - `/reread-globals` (rereadGlobals) is an ANALYSIS option, and it only ever RELAXES: it
@@ -1934,6 +2194,7 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
1934
2194
  homeLoopExprs ||
1935
2195
  homeDerivedReads ||
1936
2196
  homeMergeFeeds ||
2197
+ homeEscapingExtensions ||
1937
2198
  anchorConstCopies ||
1938
2199
  followEarlyReturns
1939
2200
  ) {
@@ -1953,10 +2214,8 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
1953
2214
  };
1954
2215
 
1955
2216
  // ── analysis phase (structure/analysis.ts): use registry, liveness, materialization ──
1956
- const { useSitesOf, opIndex, opBlock, liveIn, materialize, reachFrom, emitPos, memWriteBetween } = analyze(
1957
- fn,
1958
- returnsVoid,
1959
- {
2217
+ const { useSitesOf, opIndex, opBlock, liveIn, materialize, preUpdateHomes, reachFrom, emitPos, memWriteBetween } =
2218
+ analyze(fn, returnsVoid, {
1960
2219
  defs,
1961
2220
  dom,
1962
2221
  rereadGlobals,
@@ -1965,14 +2224,10 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
1965
2224
  homeLoopExprs,
1966
2225
  homeDerivedReads,
1967
2226
  homeMergeFeeds,
2227
+ homeEscapingExtensions,
1968
2228
  readsStayWhereWritten,
1969
- // the map's own declaration truth: a volatile object's read may not be duplicated or moved
1970
- volatileGlobal: (n) => {
1971
- const si = symbols?.get(n);
1972
- return si?.volatile === true || (si?.layout ?? []).some((f) => f.volatile === true);
1973
- },
1974
- },
1975
- );
2229
+ contractsFloatProducts,
2230
+ });
1976
2231
 
1977
2232
  // THE FOLLOW OF A DIVERGENT `if`, over the paths that do not return early. Post-dominance gives
1978
2233
  // such an `if` no join — its arms reach two different `ret`s, and EXIT is the only block on every
@@ -1983,21 +2238,36 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
1983
2238
  // rank.ts's `/shared-ret` twin, and as its `/shared-tail` twin after the store-tail sink: as a
1984
2239
  // default it costs rows, measured there.
1985
2240
  //
1986
- // WHICH REGION: the `ret`s reachable from BOTH successors. Every block that cannot reach one of
2241
+ // WHICH REGION: the `ret`s reachable from EVERY successor. Every block that cannot reach one of
1987
2242
  // them is an early-return region and is deleted; the follow is `b`'s post-dominator over what is
1988
2243
  // left. Sound for the same reason an ordinary follow is: every kept path from `b` passes it, and a
1989
2244
  // deleted block reaches no kept `ret` and so never the follow, so each arm structures up to the
1990
- // follow or into a `return`. And no deleted region is reachable from both arms — its `ret` would
1991
- // then be one of the shared ones — so the rule duplicates nothing across the arms.
2245
+ // follow or into a `return`.
2246
+ //
2247
+ // IT CAN DUPLICATE ACROSS THE ARMS, AND ABOVE TWO SUCCESSORS IT DOES. With two, it cannot: a
2248
+ // deleted region both arms reach would have its `ret` in the intersection, and so would not have
2249
+ // been deleted. With three or more that argument does not carry — a `ret` reachable from two of
2250
+ // three arms is NOT in the intersection, so its region is deleted from `keep` and re-emitted in
2251
+ // each arm that reaches it. The copies sit on mutually exclusive paths, so the program stays
2252
+ // correct; the cost is bytes, which is the only currency here. The `switch_br` caller pays it
2253
+ // knowingly: its alternative is not a shorter spelling but a decline (see that caller).
1992
2254
  //
1993
2255
  // PER `if`, never function-wide: the deletion set is a function of that `if`'s own shared `ret`s
1994
2256
  // (memoized on them), because a nested `if` shares a different set, or none.
1995
2257
  //
1996
- // REFUSES (keeping the divergent arms) when no `ret` is reachable from both successors, when the
2258
+ // REFUSES (keeping the divergent arms) when no `ret` is reachable from every successor, when the
1997
2259
  // kept graph gives `b` no post-dominator but EXIT, and when the enclosing region's `stop` is
1998
2260
  // reachable from `b` without passing the follow — that path must reach `stop`, and structuring
1999
- // the arms towards a different follow would emit `stop`'s region inside an arm. The caller asks
2000
- // only outside a loop body.
2261
+ // the arms towards a different follow would emit `stop`'s region inside an arm.
2262
+ //
2263
+ // THE TWO CALLERS DIFFER ON LOOPS AND ONLY ONE OF THEM GUARDS IT. The `if` caller asks outside a
2264
+ // loop body only (`followEarlyReturns && loopCtx === null`, with `clampToLoop` as its second
2265
+ // net), for the reason written there. The `switch_br` caller does not consult `loopCtx` at all,
2266
+ // and no input has been found that reaches this from inside a loop body: every `ret` the arms of
2267
+ // an in-loop branch reach is reachable from the latch too, so the intersection keeps the whole
2268
+ // graph, `pd` is `ipdom` itself — and the caller only asks when `ipdom.get(b)` is already null,
2269
+ // so the answer comes back null and the `?? stop` fallback stands. That is an observation about
2270
+ // the shape rather than a check, which is why it is written down here.
2001
2271
  const followsByShared = new Map<string, Map<Block, Block | null>>();
2002
2272
  const followOverReturns = (b: Block, stop: Block | null): Block | null => {
2003
2273
  const shared = sharedRetsOf(b, reachFrom);
@@ -2028,10 +2298,21 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
2028
2298
  // `((T *)&gSym)[i]` address-cast form (a struct value does not decay, so casting the bare name is
2029
2299
  // invalid C; casting the address is valid and byte-exact). Computed once here.
2030
2300
  //
2031
- // Only the offset set is tracked, not width: a single symbol read at off-0 with two DIFFERENT
2032
- // widths is a union/type-pun, which the downstream struct-layout recovery rejects LOUD
2033
- // ("overlapping fields ... unions not modelled") before this classification is consumed — so a
2034
- // width collision at off-0 declines honestly rather than reaching a wrong bare-`gSym` emission.
2301
+ // THE ACCESS SPELLING IS PART OF THE QUESTION, not only the offset set. The bare name is ONE
2302
+ // declaration, so it spells one width and one signedness; a symbol read at off-0 two different
2303
+ // ways has no bare spelling at all, and emitting one loses the second access entirely
2304
+ // (`gState + gState` for a halfword plus a byte). Such a symbol is an AGGREGATE here, so every
2305
+ // access takes the address-cast form at its OWN width — `*(u16 *)&gState + *(u8 *)&gState`,
2306
+ // which is what the source wrote and what raise/structs.ts's constant-address sibling emits for
2307
+ // the same shape at a literal address.
2308
+ //
2309
+ // TWO QUESTIONS, AND THEY ARE ASKED OF DIFFERENT ACCESSES. WIDTH is asked of every load and store: a
2310
+ // `u16` declaration cannot carry a word store either. SIGNEDNESS is asked of LOADS ONLY — a store
2311
+ // writes the same bytes through either declaration (`*(s16 *)&g = x` and `*(u16 *)&g = x` are one
2312
+ // instruction), while a load does not: `lh` and `lhu` on one cell are two declarations, and
2313
+ // spelling both `g` would lose the zero-extension. Unreachable in Thumb, where `ldrsh`/`ldrsb`
2314
+ // need a register offset and the `add` that forms it already marks the symbol aggregate; live on
2315
+ // MIPS, where `%hi/%lo` addressing carries no such `add`.
2035
2316
  // FRAME-LOCAL OBJECT NAMES (laddr). Minted HERE, not in the frontend, because identifiers live
2036
2317
  // in this layer's namespace: params, locals, every gaddr symbol, and the project's symbol map —
2037
2318
  // none of which the frontend can see. A frontend-chosen `sp0` silently shadowed a project global
@@ -2050,10 +2331,10 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
2050
2331
  for (const op of b.ops) {
2051
2332
  if (op.opcode === 'gaddr') {
2052
2333
  taken.add(op.attrs.sym as string);
2053
- } else if (op.opcode === 'call') {
2334
+ } else if (calleeName(op) !== undefined) {
2054
2335
  // a CALLEE's name is in this namespace too: a function really named sp0 would be
2055
2336
  // shadowed by the minted local, and `sp0()` on a u16 object is a compile error
2056
- taken.add(op.attrs.target as string);
2337
+ taken.add(calleeName(op)!);
2057
2338
  }
2058
2339
  }
2059
2340
  }
@@ -2090,11 +2371,27 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
2090
2371
  })();
2091
2372
 
2092
2373
  const scalarGlobals = new Set<string>();
2374
+ /** The scalar GLOBALS (never a frame object) the IR loads as a pointer at least once: one load
2375
+ * typed a pointer is the value used as an address somewhere in the function, while its other
2376
+ * loads may type an integer only because they reach a call argument or a compare. */
2377
+ const pointerLoadedGlobals = new Set<string>();
2378
+ /** Every scalar GLOBAL (never a frame object) whose value the function loads as a word, the
2379
+ * only width a pointer has: a `u8`/`u16` global is an integer whatever it is added to. */
2380
+ const wordLoadedGlobals = new Set<string>();
2093
2381
  {
2094
2382
  const offsets = new Map<string, Set<number>>();
2383
+ const widths = new Map<string, Set<number>>();
2384
+ const loadSigns = new Map<string, Set<boolean>>();
2385
+ const loadsPointer = new Map<string, boolean>();
2095
2386
  const bumpAgg = (sym: string) => offsets.set(sym, new Set([-1])); // -1 marks "variable index"
2096
2387
  for (const b of fn.blocks) {
2097
2388
  for (const op of b.ops) {
2389
+ // A frame object declared as an aggregate (`laddrType`) is one whatever its accesses agree
2390
+ // on — the frame-side twin of the symbol map's declaration-shape override. Spelled
2391
+ // scalar, a store assigns to the array and a load reads its address.
2392
+ if (op.opcode === 'laddr' && laddrType(op).kind !== 'int') {
2393
+ bumpAgg(laddrName.get(op)!);
2394
+ }
2098
2395
  const gaddrSym = (v: Value) => {
2099
2396
  const dv = defs.get(v);
2100
2397
  // laddr participates identically: `sp0` is scalar-spelled at off 0 and cast-spelled
@@ -2110,6 +2407,13 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
2110
2407
  const s = gaddrSym(op.operands[0]);
2111
2408
  if (s) {
2112
2409
  (offsets.get(s) ?? offsets.set(s, new Set()).get(s)!).add(op.attrs.off as number);
2410
+ (widths.get(s) ?? widths.set(s, new Set()).get(s)!).add(op.attrs.width as number);
2411
+ if (op.opcode === 'load') {
2412
+ (loadSigns.get(s) ?? loadSigns.set(s, new Set()).get(s)!).add(op.attrs.signed as boolean);
2413
+ if (defs.get(op.operands[0])?.opcode === 'gaddr') {
2414
+ loadsPointer.set(s, loadsPointer.get(s) === true || op.results[0].type.kind === 'ptr');
2415
+ }
2416
+ }
2113
2417
  }
2114
2418
  } else if (op.opcode === 'add' || op.opcode === 'sub') {
2115
2419
  // ANY arithmetic on the symbol's address is interior addressing ⇒ aggregate — even when
@@ -2132,10 +2436,20 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
2132
2436
  }
2133
2437
  }
2134
2438
  for (const [sym, offs] of offsets) {
2135
- if (offs.size === 1 && offs.has(0)) {
2439
+ if (offs.size === 1 && offs.has(0) && widths.get(sym)?.size === 1 && (loadSigns.get(sym)?.size ?? 1) === 1) {
2136
2440
  scalarGlobals.add(sym);
2137
2441
  }
2138
2442
  }
2443
+ for (const [sym, ptr] of loadsPointer) {
2444
+ if (scalarGlobals.has(sym)) {
2445
+ if (widths.get(sym)?.has(4)) {
2446
+ wordLoadedGlobals.add(sym);
2447
+ }
2448
+ if (ptr) {
2449
+ pointerLoadedGlobals.add(sym);
2450
+ }
2451
+ }
2452
+ }
2139
2453
  // Declaration-shape OVERRIDE (symbol map): a project-declared array/struct global is an
2140
2454
  // AGGREGATE whatever the usage inference saw — a lone off-0 access to `extern u16 tbl[]`
2141
2455
  // must still spell through the aggregate/array forms, never the bare scalar `tbl`.
@@ -2179,15 +2493,128 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
2179
2493
  }
2180
2494
  : undefined;
2181
2495
 
2182
- /** A value the MAP declares a pointer: a bare `gSym` naming a pointer global (the VALUE of a
2183
- * pointer cell), or a named MEMBER whose declaration is a pointer (`gSym.pBuf`, `gPtr->pBuf`).
2184
- * Load, store and compare of such a 4-byte cell are identical for any object-pointer type, so
2185
- * the declared pointee never matters to THEM; arithmetic on the loaded value is the opposite
2186
- * case, where the pointee's size scales what is added and every stride must therefore be made
2187
- * explicit (`(u8 *)gPtr + K`). `ctype` cannot see any of this: it types only params/locals, so
2188
- * both spellings render `undefined` there. */
2189
- const isPtrValue = (x: Expr): boolean =>
2190
- (x.k === 'var' && symCtx?.info(x.name)?.shape === 'pointer') || ptrMemberDecl(x, symCtx) !== null;
2496
+ /** The declared shape of a global as the pointer-value rules below read it: the map's, or a
2497
+ * pointer the map declares to a structuring that does not spell from it (`pointerGlobals`). */
2498
+ const declaredShape = (name: string): SymbolInfo['shape'] =>
2499
+ symCtx?.info(name)?.shape ?? (pointerGlobals?.has(name) ? 'pointer' : undefined);
2500
+ /** Whether no declaration a spelling folds from types the global: the map's is the only one.
2501
+ * The operand-order rules read this, not declaredShape: a declared pointer keeps `x + (u8 *)p`
2502
+ * for the element and field spellings to read, and a `pointerGlobals` name has no pointee for
2503
+ * them, so its sum is spelled in the asm's order like any other undeclared global's. */
2504
+ const mapUndeclared = (name: string): boolean => symCtx?.info(name)?.shape === undefined;
2505
+
2506
+ /** A POINTER VALUE whose type the project's header owns: a bare `gSym` naming a pointer global
2507
+ * (the VALUE of a pointer cell), or a named MEMBER whose declaration is a pointer (`gSym.pBuf`,
2508
+ * `gPtr->pBuf`). Load, store and compare of such a 4-byte cell are identical for any
2509
+ * object-pointer type, so the declared pointee never matters to THEM; arithmetic on the loaded
2510
+ * value is the opposite case, where the pointee's size scales what is added and every stride
2511
+ * must therefore be made explicit (`(u8 *)gPtr + K`). `ctype` cannot see any of this: it types
2512
+ * only params/locals, so both spellings render `undefined` there.
2513
+ *
2514
+ * A bare global is one when its declared shape is `'pointer'` (declaredShape), or when no
2515
+ * declaration this pass can read says anything (no map, a symtab-only name, entries that
2516
+ * disagree and were dropped to the bare name, a name the map lacks) and the IR loads it as a
2517
+ * pointer at least once. The project still declares it, typically `struct S *`. A map
2518
+ * `shape:'scalar'` is a declaration and is excluded; so is a global the IR never loads as a
2519
+ * pointer (`gCount + 1`, or the `u8` index in `gIdx + gItems`), where casting would make a
2520
+ * pointer of an integer the source added as one. */
2521
+ const isPtrValue = (x: Expr): boolean => {
2522
+ if (x.k !== 'var') {
2523
+ return ptrMemberDecl(x, symCtx) !== null;
2524
+ }
2525
+ const shape = declaredShape(x.name);
2526
+ return shape === 'pointer' || (shape === undefined && pointerLoadedGlobals.has(x.name));
2527
+ };
2528
+
2529
+ /** A global's word VALUE that no declaration this pass can read types, whatever the IR loaded it
2530
+ * as. */
2531
+ const isUndeclaredGlobalValue = (x: Expr): boolean =>
2532
+ x.k === 'var' && wordLoadedGlobals.has(x.name) && declaredShape(x.name) === undefined;
2533
+
2534
+ /** A pointer value or a global's word as the integer the asm added. A global no declaration
2535
+ * types and the IR never loads as a pointer may be declared a float, whose value `(u32)g`
2536
+ * converts, so it goes through `(u8 *)` first: `(u32)(u8 *)g` is `(u32)g`'s bytes under every
2537
+ * integer and pointer declaration, and no C under a float one. KNOWN GAP: under an array or a
2538
+ * function declaration `g` decays to its address, which this spells with no diagnostic; only a
2539
+ * pun reads the word there, and intoDeclaredTemp's KNOWN GAP says why none is spelled. */
2540
+ const globalWord = (x: Expr): Expr => ({
2541
+ k: 'cast',
2542
+ to: T.u(32),
2543
+ e:
2544
+ x.k === 'var' && declaredShape(x.name) === undefined && !pointerLoadedGlobals.has(x.name)
2545
+ ? { k: 'cast', to: T.ptr(T.u(8)), e: x }
2546
+ : x,
2547
+ });
2548
+
2549
+ /** An integer operand with every undeclared global it adds or subtracts made a word as well:
2550
+ * bare, `a0 + g` is pointer arithmetic under a pointer declaration of g. Other operators reject
2551
+ * a pointer operand, and a load or a cast types its own value. */
2552
+ const intWords = (x: Expr): Expr =>
2553
+ isUndeclaredGlobalValue(x)
2554
+ ? globalWord(x)
2555
+ : x.k === 'bin' && (x.op === '+' || x.op === '-')
2556
+ ? { ...x, l: intWords(x.l), r: intWords(x.r) }
2557
+ : x;
2558
+
2559
+ /** A byte sum the pointer-value arithmetic rule below made of a global's value (`(u8 *)g + K`),
2560
+ * and the same sum as the integer it also is (`(u32)g + K`), which an integer added in front of
2561
+ * it takes. KNOWN GAP: an integer READER of the byte sum converts a pointer to an integer. The
2562
+ * backend casts it where the reader's type is known (an assignment, a store through a typed
2563
+ * slot, a return: cfamily `legalizePointerWrites`), and a compare against an integer compares
2564
+ * it as a `u32`. A call argument and a global no declaration types keep the pointer, which agbcc
2565
+ * and KMC gcc warn about and CodeWarrior rejects; only a pointer cell's sum passed to a parameter
2566
+ * the prototype declares an integer is converted (l3/ptrcell.ts). The integer sum is no fix: gcc
2567
+ * orders a pointer sum's operands and an integer sum's differently, so the bytes differ. */
2568
+ const castGlobal = (x: Expr, undeclared = false): x is Extract<Expr, { k: 'cast' }> =>
2569
+ x.k === 'cast' &&
2570
+ typeEquals(x.to, T.ptr(T.u(8))) &&
2571
+ x.e.k === 'var' &&
2572
+ (isPtrValue(x.e) || isUndeclaredGlobalValue(x.e)) &&
2573
+ (!undeclared || mapUndeclared(x.e.name));
2574
+ /** The integer sum the rule spells in the asm's order instead (`(u32)g + x`), cast back to the
2575
+ * byte pointer it stands for: `(u8 *)((u32)g + x)`. A byte sum converted whole (byteSumAsInt's
2576
+ * `(u32)((u8 *)g + x - gB)`) is one of its words. */
2577
+ const intGlobalWord = (x: Expr, undeclared: boolean): boolean =>
2578
+ x.k === 'cast' &&
2579
+ typeEquals(x.to, T.u(32)) &&
2580
+ (castGlobal(x.e, undeclared) ||
2581
+ (x.e.k === 'bin' && isByteGlobalSum(x.e, undeclared)) ||
2582
+ (x.e.k === 'var' &&
2583
+ (isPtrValue(x.e) || isUndeclaredGlobalValue(x.e)) &&
2584
+ (!undeclared || mapUndeclared(x.e.name))));
2585
+ const isIntGlobalSum = (x: Expr, undeclared: boolean): boolean =>
2586
+ x.k === 'bin' &&
2587
+ (x.op === '+' || x.op === '-') &&
2588
+ (intGlobalWord(x.l, undeclared) ||
2589
+ intGlobalWord(x.r, undeclared) ||
2590
+ isIntGlobalSum(x.l, undeclared) ||
2591
+ isIntGlobalSum(x.r, undeclared));
2592
+ const restoredIntSum = (x: Expr, undeclared: boolean): Expr | undefined =>
2593
+ x.k === 'cast' && typeEquals(x.to, T.ptr(T.u(8))) && isIntGlobalSum(x.e, undeclared) ? x.e : undefined;
2594
+ const isByteGlobalSum = (x: Expr, undeclared = false): boolean =>
2595
+ restoredIntSum(x, undeclared) !== undefined ||
2596
+ (x.k === 'bin' &&
2597
+ (x.op === '+' || x.op === '-') &&
2598
+ (castGlobal(x.l, undeclared) ||
2599
+ castGlobal(x.r, undeclared) ||
2600
+ isByteGlobalSum(x.l, undeclared) ||
2601
+ isByteGlobalSum(x.r, undeclared)));
2602
+ /** The integer a byte sum is. Every global it adds bare goes a word with it. A lone global it
2603
+ * subtracts stays the pointer difference, `(u32)((u8 *)g + x - gB)`: the asm's integer under an
2604
+ * integer or a byte-pointer declaration of `gB`, and no C under a wider pointer or array, a
2605
+ * function or a float one, where the word `(u32)(u8 *)gB` would subtract an address. Under a byte
2606
+ * array it subtracts the array's address: globalWord's KNOWN GAP. A difference has one operand
2607
+ * order, so the asm's needs no integer sum. A sum of globals it subtracts goes words: bare,
2608
+ * `gB2 - gB3` is an element count under a wider pointer declaration. */
2609
+ const byteSumAsInt = (x: Expr): Expr =>
2610
+ restoredIntSum(x, false) ??
2611
+ (castGlobal(x)
2612
+ ? globalWord(x.e)
2613
+ : x.k === 'bin' && isByteGlobalSum(x)
2614
+ ? x.op === '-' && isUndeclaredGlobalValue(x.r)
2615
+ ? { k: 'cast', to: T.u(32), e: x }
2616
+ : { ...x, l: byteSumAsInt(x.l), r: byteSumAsInt(x.r) }
2617
+ : intWords(x));
2191
2618
 
2192
2619
  /** Operands `-`/`~` cannot take as spelled: a rendered pointer, a bare `&gSym`, a pointer
2193
2620
  * global's value. All three are ill-formed C under a unary arithmetic operator — the asm did
@@ -2244,7 +2671,7 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
2244
2671
  P !== b &&
2245
2672
  pt?.opcode === 'br' &&
2246
2673
  P.params.length === 0 &&
2247
- P.ops.every((o) => !EFFECTFUL_OPS.has(o.opcode) && !materialize.has(o)) &&
2674
+ P.ops.every((o) => !effectful(o) && !materialize.has(o)) &&
2248
2675
  // at least one def the LOOP BODY reads — the loop-invariant-motion shape this claim
2249
2676
  // exists for. A block that only computes the init args is the do-while path's
2250
2677
  // ordinary entry chain, and that path's sink machinery handles it better.
@@ -2270,7 +2697,24 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
2270
2697
  // reducible single-entry body, and a SINGLE real (non-ret) exit — early returns (ret-terminated
2271
2698
  // targets) are allowed in-body. The shape then splits on WHERE the exit lives: the HEADER exits
2272
2699
  // (pure test-at-top) → `while`; the LATCH exits (body-first) → `do-while`. Anything that fails
2273
- // declines to plain if-recovery, which re-enters the header and fails loud via `onStack`.
2700
+ // is left to `while (1)` (below), or declines to plain if-recovery, which re-enters the header and
2701
+ // fails loud via `onStack`.
2702
+ //
2703
+ // What every loop form asks first (`admissible`): each inner loop whose header sits in this body
2704
+ // is PROPERLY nested — its ENTIRE body is contained in ours (a forest descendant). Structuring
2705
+ // then recurses naturally: when the outer body reaches the inner header, structureBlock
2706
+ // dispatches to the inner's own emitter. An OVERLAPPING loop (shared blocks, neither containing
2707
+ // the other → irreducible) DECLINES. If a contained inner is itself unstructurable, the outer's
2708
+ // body structuring loud-fails at the inner back-edge (onStack) — a safe decline, not a
2709
+ // miscompile. And the body is single-entry: every block but the header is entered ONLY from
2710
+ // inside it — no jump into the loop interior. A `break` edge must also not leave from an inner
2711
+ // loop's body (`innerBodyHas`), which would be a two-level exit.
2712
+ const innerBodyHas = (nl: NaturalLoop, b: Block): boolean =>
2713
+ [...forest.byHeader.values()].some((l2) => l2.header !== nl.header && nl.body.has(l2.header) && l2.body.has(b));
2714
+ const admissible = (nl: NaturalLoop): boolean =>
2715
+ [...forest.byHeader.values()].every(
2716
+ (l2) => l2.header === nl.header || !nl.body.has(l2.header) || [...l2.body].every((b) => nl.body.has(b)),
2717
+ ) && [...nl.body].every((bb) => bb === nl.header || (preds.get(bb) ?? []).every((p) => nl.body.has(p)));
2274
2718
  const whileLoops = new Map<Block, WhileLoopInfo>();
2275
2719
  const doWhileLoops = new Map<Block, DoWhileInfo>();
2276
2720
  for (const nl of forest.byHeader.values()) {
@@ -2282,32 +2726,7 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
2282
2726
  continue;
2283
2727
  } // single latch only
2284
2728
  const latch = nl.selfLoop ? h : nl.latches[0];
2285
- // Nested loops: an inner loop whose header sits in this body is fine ONLY if it is PROPERLY
2286
- // nested — its ENTIRE body is contained in ours (a forest descendant). Structuring then recurses
2287
- // naturally: when the outer body reaches the inner header, structureBlock dispatches to the inner's
2288
- // own emitWhile/emitDoWhile. An OVERLAPPING loop (shared blocks, neither containing the other →
2289
- // irreducible) DECLINES. If a contained inner is itself unstructurable, the outer's body
2290
- // structuring loud-fails at the inner back-edge (onStack) — a safe decline, not a miscompile.
2291
- if (
2292
- [...forest.byHeader.keys()].some(
2293
- (h2) => h2 !== h && nl.body.has(h2) && ![...forest.byHeader.get(h2)!.body].every((b) => nl.body.has(b)),
2294
- )
2295
- ) {
2296
- continue;
2297
- }
2298
- // Reducible entry (single-entry): every body block except the header is entered ONLY from
2299
- // inside the body — no jump into the loop interior.
2300
- let reducible = true;
2301
- for (const bb of nl.body) {
2302
- if (bb === h) {
2303
- continue;
2304
- }
2305
- if ((preds.get(bb) ?? []).some((p) => !nl.body.has(p))) {
2306
- reducible = false;
2307
- break;
2308
- }
2309
- }
2310
- if (!reducible) {
2729
+ if (!admissible(nl)) {
2311
2730
  continue;
2312
2731
  }
2313
2732
 
@@ -2326,7 +2745,7 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
2326
2745
  // whose result also feeds the body would be evaluated twice per iteration). A `load` is fine —
2327
2746
  // but NOT a materialized one: its temp assignment renders only via sideEffects(), which a
2328
2747
  // condition-only header never emits, so its uses would read an unassigned variable.
2329
- const headerPure = !h.ops.some((op) => EFFECTFUL_OPS.has(op.opcode) || materialize.has(op));
2748
+ const headerPure = !h.ops.some((op) => effectful(op) || materialize.has(op));
2330
2749
 
2331
2750
  let exitFrom: Block,
2332
2751
  exit: Block,
@@ -2347,11 +2766,20 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
2347
2766
  continue; // neither a clean pre-tested nor bottom-tested single-exit shape
2348
2767
  }
2349
2768
  // Single loop exit (ret-aware): the chosen exit is the ONE real exit; every OTHER edge leaving
2350
- // the body must be an early `return` (`earlyReturnArm`) or a ret-terminated target. A second exit
2351
- // that lands on a LIVE non-return merge is a genuine `break`/second structured exit → decline.
2352
- // The arms are kept: emission needs to know which edges out of the body end an iteration rather
2353
- // than continue it.
2769
+ // the body must be an early `return` (`earlyReturnArm`), a ret-terminated target, or — for a
2770
+ // `while` — a `break`: an edge landing on the header's own exit, which C spells by leaving the
2771
+ // loop and running the exit region once, after it. A second exit that lands on any OTHER live
2772
+ // merge has no single-level spelling → decline, as does a `break` edge out of a NESTED loop's
2773
+ // body (a two-level exit). A `do-while` takes no break: an edge into its exit that no arm owns
2774
+ // leaves the loop to `while (1)`, whose breaks carry their own copies, where the do-while's
2775
+ // would fall into the latch's. An edge to the header's exit is a break whatever the exit holds:
2776
+ // the asm runs ONE copy of the exit region for both edges, and agbcc and mwcc keep a
2777
+ // source-duplicated return tail duplicated, so copying the tail into the arm spells another
2778
+ // object. Where that exit ends in a return, the edge is also an arm or a ret target, and emission
2779
+ // copies the tail where the break spelling refuses. The arms and breaks are kept: emission needs
2780
+ // to know which edges out of the body end an iteration rather than continue it.
2354
2781
  const arms: LoopArm[] = [];
2782
+ const breaks = new Set<Block>();
2355
2783
  let singleExit = true;
2356
2784
  for (const e of nl.exitEdges) {
2357
2785
  if (e.from === exitFrom && e.to === exit) {
@@ -2360,7 +2788,10 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
2360
2788
  const owned = earlyReturnArm({ dom, reachFrom }, e.from, e.to, nl.body, exit);
2361
2789
  if (owned) {
2362
2790
  arms.push({ from: e.from, to: e.to, owned });
2363
- } else if (!isRet(e.to)) {
2791
+ }
2792
+ if (kind === 'while' && e.to === exit && !innerBodyHas(nl, e.from)) {
2793
+ breaks.add(e.from);
2794
+ } else if (!owned && (!isRet(e.to) || (kind === 'dowhile' && e.to === exit))) {
2364
2795
  singleExit = false;
2365
2796
  break;
2366
2797
  }
@@ -2378,12 +2809,67 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
2378
2809
  forwardPreds: nl.forwardPreds,
2379
2810
  body: nl.body,
2380
2811
  arms,
2812
+ breaks,
2381
2813
  });
2382
2814
  } else {
2383
2815
  doWhileLoops.set(h, { header: h, latch, exit, forwardPreds: nl.forwardPreds, body: nl.body, arms });
2384
2816
  }
2385
2817
  }
2386
2818
 
2819
+ // A loop none of the shapes above spells — several latches with a conditional `continue` in
2820
+ // mid-body beside the bottom one, or one latch under a header that computes before it tests (a
2821
+ // mid-tested loop) — is `while (1)`: every edge back to the header is the
2822
+ // implicit continue at the foot of its region, and every edge out is a `break` to the one exit
2823
+ // chosen below or an early `return`. Same fail-closed preconditions as above: `admissible`, and
2824
+ // no break out of an inner loop's body. The exit is the first target under which every other edge
2825
+ // out is an early-return arm or lands on a `ret` block with no effect in it, trying first, in
2826
+ // block order, the targets laid out after the loop's last body block: a compiler places the code
2827
+ // after a loop there and an early `return` inside the loop's span, so block order alone would take
2828
+ // that `return` for the break and the code after the loop for an early return.
2829
+ const foreverLoops = new Map<Block, ForeverLoopInfo>();
2830
+ const foreverShape = (nl: NaturalLoop): ForeverLoopInfo | null => {
2831
+ const last = Math.max(...[...nl.body].map((b) => fn.blocks.indexOf(b)));
2832
+ const rank = (x: Block) => fn.blocks.indexOf(x) + (fn.blocks.indexOf(x) > last ? 0 : fn.blocks.length);
2833
+ const targets = [...new Set(nl.exitEdges.map((e) => e.to))].sort((x, y) => rank(x) - rank(y));
2834
+ for (const exit of targets) {
2835
+ const arms: LoopArm[] = [];
2836
+ const breaks = new Set<Block>();
2837
+ const fits = nl.exitEdges.every((e) => {
2838
+ if (e.to === exit) {
2839
+ breaks.add(e.from);
2840
+ return !innerBodyHas(nl, e.from);
2841
+ }
2842
+ const owned = earlyReturnArm({ dom, reachFrom }, e.from, e.to, nl.body, exit);
2843
+ if (owned) {
2844
+ arms.push({ from: e.from, to: e.to, owned });
2845
+ }
2846
+ // a `ret` target the edge does not own is copied into the body where the edge leaves, so it
2847
+ // must hold nothing a second copy would run twice in the source
2848
+ return owned !== null || (isRet(e.to) && !e.to.ops.some(effectful));
2849
+ });
2850
+ if (fits) {
2851
+ return {
2852
+ header: nl.header,
2853
+ latches: nl.latches,
2854
+ forwardPreds: nl.forwardPreds,
2855
+ exit,
2856
+ body: nl.body,
2857
+ arms,
2858
+ breaks,
2859
+ };
2860
+ }
2861
+ }
2862
+ return null;
2863
+ };
2864
+ for (const nl of forest.byHeader.values()) {
2865
+ const h = nl.header;
2866
+ const shape =
2867
+ !loops.has(h) && !whileLoops.has(h) && !doWhileLoops.has(h) && admissible(nl) ? foreverShape(nl) : null;
2868
+ if (shape !== null) {
2869
+ foreverLoops.set(h, shape);
2870
+ }
2871
+ }
2872
+
2387
2873
  // --- coalesce SSA values to variable names ---
2388
2874
  const varName = new Map<Value, string>();
2389
2875
  const varType = new Map<string, IrType>();
@@ -2407,6 +2893,8 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
2407
2893
  * which is the chain-rooted widening rather than this rule. */
2408
2894
  const paramSeededMerges = new Set<Value>();
2409
2895
  const backArgName = new Map<Value, string>();
2896
+ /** The loop header each `backArgName` entry was seeded by. */
2897
+ const backArgHeader = new Map<Value, Block>();
2410
2898
  // The C static type of a rendered expression, over the declared variable types — what decides
2411
2899
  // whether a memory access's base may be dereferenced as spelled (memAccess/arrayAccess).
2412
2900
  const vtEnv = (n: string): IrType | undefined => varType.get(n);
@@ -2442,18 +2930,34 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
2442
2930
  if (t === undefined) {
2443
2931
  return value;
2444
2932
  }
2445
- // ── the MAP's pointer values, whose type this side of the program does not own ──────────────
2933
+ // ── pointer values whose type this side of the program does not own ────────────────────────
2446
2934
  // A `gaddr` at least states a type the IR knows. `gSym.pBuf` and a pointer global's own value
2447
- // state one only the MAP knows, and `ctype` — which types params and locals — reads them as
2448
- // `undefined`, so the test above cannot see them at all. Assigning one bare declares that the
2449
- // temp's type and the project's declaration of that pointer are the same type, which nothing
2450
- // here established: the project's header says `struct Unk_03005284 *` where the recovered temp
2451
- // says `struct Struct0 *`, and `-Werror` makes the mismatch fatal in the tree the source is
2452
- // pasted into. The destination's type is the one this pass DID choose, so unlike the
2453
- // map-declared cells below it can be named exactly rather than defused through `void *`. That
2454
- // holds for an INTEGER temp too, and there the diagnostic is the mirror one, `assignment makes
2455
- // integer from pointer without a cast` — same site, same argument, same `(T)` answer.
2456
- if (isPtrValue(value)) {
2935
+ // (isPtrValue) state one only the project's header knows, and `ctype` — which types params and
2936
+ // locals — reads them as `undefined`, so the test above cannot see them at all. Assigning one
2937
+ // bare declares that the temp's type and the project's declaration of that pointer are the
2938
+ // same type, which nothing here established: the project's header says
2939
+ // `struct Unk_03005284 *` where the recovered temp says `struct Struct0 *`, and `-Werror` makes
2940
+ // the mismatch fatal in the tree the source is pasted into. The destination's type is the one
2941
+ // this pass DID choose, so unlike the map-declared cells below it can be named exactly rather
2942
+ // than defused through `void *`. That holds for an INTEGER temp too, and there the diagnostic
2943
+ // is the mirror one, `assignment makes integer from pointer without a cast` — same site, same
2944
+ // argument, same `(T)` answer.
2945
+ //
2946
+ // A value that RENDERS a pointer of another type is the same assignment with its type in plain
2947
+ // sight: the byte arithmetic below spells `(u8 *)gPtr + 3544` and `(u8 *)a0 + 2` for the
2948
+ // address alone, and a `u16 *` or `s32 *` temp takes neither without the cast.
2949
+ //
2950
+ // KNOWN GAP: a global no declaration types, read at offset 0 only, may be declared an ARRAY
2951
+ // (`u16 *gArr[4]`, the asm reading `gArr[0]`). Its value spelling is then the array's address,
2952
+ // and this cast compiles that wrong value with no diagnostic, where the bare value keeps
2953
+ // agbcc's `incompatible pointer type`, fatal under -Werror. The cast is load-bearing all the
2954
+ // same: a substitution variation (`/unmerge`) carries the temp's value into the arithmetic it
2955
+ // feeds, and bare, that arithmetic scales by the declared pointee. A pun, `*(T *)&gArr`, is no
2956
+ // fix: where the declaration is not exactly T it loses gcc's fold of the read (agbcc
2957
+ // c-typeck.c), it drops a `volatile` declaration's qualifier, and agbcc's strict aliasing at
2958
+ // -O2 moves it past a store through the declared type.
2959
+ const vt = ctype(value);
2960
+ if (isPtrValue(value) || (vt?.kind === 'ptr' && !typeEquals(vt, t))) {
2457
2961
  return { k: 'cast', to: t, e: value };
2458
2962
  }
2459
2963
  if (t.kind !== 'ptr' || value.k !== 'addr') {
@@ -2580,7 +3084,7 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
2580
3084
  }
2581
3085
  return { carrierLive, carrierWritten };
2582
3086
  };
2583
- const canTakeName = (p: Value, B: Block, name: string, pureAlias = false): boolean => {
3087
+ const canTakeName = (p: Value, B: Block, name: string, pureAlias = false, ownName = true): boolean => {
2584
3088
  const lin = liveIn.get(B)!;
2585
3089
  let scan: ReturnType<typeof carrierScan> | undefined;
2586
3090
  const scanned = (): ReturnType<typeof carrierScan> => (scan ??= carrierScan(p, B, name));
@@ -2593,24 +3097,67 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
2593
3097
  // computes 10 — agbcc emits exactly that asm, so this is not a generated-IR curiosity. The
2594
3098
  // walk stops at any value with a name of its own (it reads THAT name) and at a materialized
2595
3099
  // def (it is assigned at its own position, which the clause above already judges).
2596
- const reDerives = (w: Value, seen: Set<Value>): boolean => {
3100
+ //
3101
+ // `viaBackArg` asks the `back-arg-live` question instead: the walk is the same, and what it
3102
+ // looks for is a loop's back-edge argument carrying `name` through `backArgName`, so that the two
3103
+ // gates' evidence stays disjoint and each one's ablation is its own claim.
3104
+ // The exit regions that render a back-edge argument AS its loop variable's name are the ones
3105
+ // under the loop's substitution — a self-loop's and a do-while's (`withSub`). A test-at-top
3106
+ // `while` installs none: past it the argument is re-derived from its operands, which the plain
3107
+ // walk judges.
3108
+ const rendersAsBackArgName = (w: Value): boolean => {
3109
+ const h = backArgHeader.get(w);
3110
+ return h !== undefined && (loops.has(h) || doWhileLoops.has(h));
3111
+ };
3112
+ const reDerives = (w: Value, seen: Set<Value>, viaBackArg: boolean): boolean => {
2597
3113
  if (w === p || seen.has(w)) {
2598
3114
  return false;
2599
3115
  }
2600
3116
  seen.add(w);
2601
3117
  const nm = varName.get(w);
2602
3118
  if (nm !== undefined) {
2603
- return nm === name;
3119
+ return !viaBackArg && nm === name;
3120
+ }
3121
+ if (viaBackArg && backArgName.get(w) === name && rendersAsBackArgName(w)) {
3122
+ return true;
2604
3123
  }
2605
3124
  const d = defs.get(w);
2606
3125
  if (!d || materialize.has(d)) {
2607
3126
  return false;
2608
3127
  }
2609
- return d.operands.some((o) => reDerives(o, seen));
3128
+ return d.operands.some((o) => reDerives(o, seen, viaBackArg));
3129
+ };
3130
+ // `back-arg-live` looks where the in-edge copies LAND, which is more than `B`'s entry when the
3131
+ // edge leaves a loop from its latch: the do-while and self-loop emitters sink that exit copy into
3132
+ // the body, so what is live into that loop's header is live at the copy. A predecessor all of
3133
+ // whose edges hand the slot a value already under `name` writes nothing. The sink gate
3134
+ // (`dest-free-inside-loop`) refuses an unnamed value that renders the name through `varName`, but
3135
+ // not one that renders it as an earlier loop's back-edge argument, so this rule is that case's
3136
+ // only guard. `re-derives` is the sink gate's there, and reads `B`'s entry alone.
3137
+ let landed: Set<Value> | undefined;
3138
+ const landing = (): Set<Value> => {
3139
+ if (landed) {
3140
+ return landed;
3141
+ }
3142
+ landed = new Set(lin);
3143
+ const slot = B.params.indexOf(p);
3144
+ const writes = new Map<Block, boolean>();
3145
+ for (const { pred, succ } of inEdgeRecords(preds, B)) {
3146
+ writes.set(pred, (writes.get(pred) ?? false) || varName.get(succ.args[slot]) !== name);
3147
+ }
3148
+ for (const [pr, w] of writes) {
3149
+ for (const s of w ? successorsOf(pr) : []) {
3150
+ if (forest.byHeader.get(s)?.body.has(pr)) {
3151
+ liveIn.get(s)!.forEach((v) => landed!.add(v));
3152
+ }
3153
+ }
3154
+ }
3155
+ return landed;
2610
3156
  };
2611
3157
  return (
2612
3158
  firstRejection(hooks.carrierNameGates ?? CARRIER_NAME_GATES, {
2613
3159
  pureAlias,
3160
+ ownName,
2614
3161
  widthDiffers: carrierWidth(varType.get(name)) !== carrierWidth(p.type),
2615
3162
  signDiffers: carrierSign(varType.get(name)) !== carrierSign(p.type),
2616
3163
  siblingHolds: B.params.some((q) => q !== p && varName.get(q) === name),
@@ -2624,7 +3171,10 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
2624
3171
  // NAMED live values are `carrier-live`'s, so the two rules stay disjoint and each one's
2625
3172
  // ablation is its own claim. The walk itself still crosses into a named OPERAND, which
2626
3173
  // is the whole point of it.
2627
- return [...lin].some((w) => !varName.has(w) && reDerives(w, new Set()));
3174
+ return [...lin].some((w) => !varName.has(w) && reDerives(w, new Set(), false));
3175
+ },
3176
+ get backArgReadsName() {
3177
+ return [...landing()].some((w) => !varName.has(w) && reDerives(w, new Set(), true));
2628
3178
  },
2629
3179
  }) === null
2630
3180
  );
@@ -2790,6 +3340,42 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
2790
3340
  const nm = a === undefined ? undefined : varName.get(a);
2791
3341
  return refused === null && nm !== undefined && canTakeName(p, header, nm) ? nm : undefined;
2792
3342
  };
3343
+ /** Where in `header` an op reading a loop variable RENDERS, for the in-place adoption below: at
3344
+ * its own index when it is a statement, at the consumer it inlines into otherwise (`emitPos`) —
3345
+ * `%t = v1 - v0` ahead of the adopted def, inlined into a call after it, reads the name after
3346
+ * the assignment has overwritten it. A tree the header's TERMINATOR carries renders there, at the
3347
+ * bottom, with one exception: a self-loop's EXIT arg renders at its ROOT's own index, which is
3348
+ * where a sunk exit copy is rebuilt (`preUpdateCopyHome`), while a kept one renders after the
3349
+ * loop, where the hazard checks already count the in-place write (`loopWriteSet`). A tree the
3350
+ * back edge also carries is an update copy, and renders at the bottom. So a read never renders
3351
+ * EARLIER than its SSA position says, and the rule only ever refuses more. Null — refuse — where
3352
+ * the read renders in several places or outside the header, or in an exit arg with no home. */
3353
+ const readRenderIdx = (op: Op, header: Block): number | null => {
3354
+ const pos = emitPos(op);
3355
+ if (pos === null || pos.blk !== header) {
3356
+ return null;
3357
+ }
3358
+ const term = header.ops[header.ops.length - 1];
3359
+ if (pos.idx !== opIndex.get(term) || !term.successors.some((sc) => sc.block === header)) {
3360
+ return pos.idx;
3361
+ }
3362
+ for (let cur = op; cur !== term;) {
3363
+ const cons = [...new Set((useSitesOf.get(cur.results[0]) ?? []).map((u) => u.op))];
3364
+ if (cons.length !== 1) {
3365
+ return null;
3366
+ }
3367
+ if (cons[0] === term) {
3368
+ const root = cur.results[0];
3369
+ if (term.successors.some((sc) => sc.block === header && sc.args.includes(root))) {
3370
+ return pos.idx;
3371
+ }
3372
+ const home = preUpdateCopyHome(defs, opBlock, root, header);
3373
+ return home === null ? null : opIndex.get(home)!;
3374
+ }
3375
+ cur = cons[0];
3376
+ }
3377
+ return pos.idx;
3378
+ };
2793
3379
  // ONE seeding routine for self-loop and structured-loop headers. On a coalesceLoopInit target,
2794
3380
  // keep the induction variable in its entry (forward-edge) value's register — reproducing a
2795
3381
  // compiler that mutates the arg register across the loop instead of copying to a fresh local,
@@ -2823,7 +3409,8 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
2823
3409
  // param sits at-or-before the def in the header (the def's own operands included): the
2824
3410
  // assignment splits the iteration into old-value-before / new-value-after, and a later
2825
3411
  // read of the OLD value would silently get the new one — position-granular, because
2826
- // liveness is block-granular and the param is killed from its own block's liveIn.
3412
+ // liveness is block-granular and the param is killed from its own block's liveIn. And the
3413
+ // position is where the read RENDERS, not where SSA put it (`readRenderIdx`).
2827
3414
  if (name === undefined) {
2828
3415
  const ba = backArgs?.[i];
2829
3416
  const d = ba !== undefined ? defs.get(ba) : undefined;
@@ -2834,7 +3421,10 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
2834
3421
  d !== undefined &&
2835
3422
  materialize.has(d) &&
2836
3423
  opBlock.get(d) === header &&
2837
- (useSitesOf.get(p) ?? []).every((s) => s.blk === header && s.idx <= opIndex.get(d)!) &&
3424
+ (useSitesOf.get(p) ?? []).every((s) => {
3425
+ const at = s.op === d ? s.idx : readRenderIdx(s.op, header);
3426
+ return at !== null && at <= opIndex.get(d)!;
3427
+ }) &&
2838
3428
  canTakeName(p, header, nm)
2839
3429
  ) {
2840
3430
  name = nm;
@@ -2858,6 +3448,7 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
2858
3448
  // agbcc compiles to an unconditional branch.
2859
3449
  if (backArgs) {
2860
3450
  backArgName.set(backArgs[i], varName.get(p)!);
3451
+ backArgHeader.set(backArgs[i], header);
2861
3452
  }
2862
3453
  });
2863
3454
  };
@@ -2875,16 +3466,24 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
2875
3466
  // (agbcc copies the init to a new reg). Never reuse a name already taken by a SIMULTANEOUSLY-LIVE
2876
3467
  // sibling header param — two loop-carried values seeded from one source must not collapse (a silent
2877
3468
  // clobber). The latch's back-edge arg carries the param's name so the loop update assigns it.
3469
+ // A `while (1)` is seeded the same way, and its back-edge args are its latches' when they all hand
3470
+ // the header the same ones: latches that differ leave the param a name of its own, and each
3471
+ // back edge a copy into it. Seeding is what keeps the merge naming below from handing the param a
3472
+ // value the body computes while the param is still read.
3473
+ const onlyBackArgs = (header: Block, latches: readonly Block[]): readonly Value[] | null => {
3474
+ const [first, ...rest] = latches.map((l) => successorTo(l, header)?.args ?? null);
3475
+ return first !== null && rest.every((a) => a !== null && a.every((v, k) => v === first[k])) ? first : null;
3476
+ };
2878
3477
  const structuredLoops = [
2879
- ...[...whileLoops.values()].map((l) => ({
3478
+ ...[...whileLoops.values(), ...doWhileLoops.values()].map((l) => ({
2880
3479
  header: l.header,
2881
- latch: l.latch,
3480
+ backArgs: successorTo(l.latch, l.header)?.args ?? null,
2882
3481
  forwardPreds: l.forwardPreds,
2883
3482
  body: l.body,
2884
3483
  })),
2885
- ...[...doWhileLoops.values()].map((l) => ({
3484
+ ...[...foreverLoops.values()].map((l) => ({
2886
3485
  header: l.header,
2887
- latch: l.latch,
3486
+ backArgs: onlyBackArgs(l.header, l.latches),
2888
3487
  forwardPreds: l.forwardPreds,
2889
3488
  body: l.body,
2890
3489
  })),
@@ -2913,9 +3512,8 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
2913
3512
  return names;
2914
3513
  };
2915
3514
  for (const l of structuredLoops) {
2916
- const back = successorTo(l.latch, l.header);
2917
3515
  // exclusion seeded with enclosing-loop names → never coalesce onto them
2918
- seedLoopParams(l.header, l.forwardPreds, back ? back.args : null, enclosingNames(l));
3516
+ seedLoopParams(l.header, l.forwardPreds, l.backArgs, enclosingNames(l));
2919
3517
  }
2920
3518
  let changed = true;
2921
3519
  while (changed) {
@@ -2933,8 +3531,8 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
2933
3531
  incoming.push({ v: succ.args[i], pr: pred });
2934
3532
  }
2935
3533
  // A redundant phi (every edge passes the SAME value) is a pure alias of it — sharing the
2936
- // name is sound even while the value stays live (they are equal on every path). This
2937
- // waives only the LIVENESS half of canTakeName; the sibling-param check always applies.
3534
+ // value's own name is sound even while the value stays live (they are equal on every path).
3535
+ // This waives `carrier-live` and `re-derives`; the sibling-param check always applies.
2938
3536
  const allSame = incoming.length > 0 && incoming.every((c) => c.v === incoming[0].v);
2939
3537
  // prefer a carrier that already has a name; then a loop var whose update this receives —
2940
3538
  // but only one whose name survives the C3 interference check (else the edge copies into
@@ -2949,7 +3547,7 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
2949
3547
  ...incoming.filter((c) => backArgName.has(c.v)),
2950
3548
  ]) {
2951
3549
  const nm = varName.get(c.v) ?? backArgName.get(c.v)!;
2952
- if (carriesPreUpdate(c.v, c.pr, b) || !canTakeName(p, b, nm, allSame)) {
3550
+ if (carriesPreUpdate(c.v, c.pr, b) || !canTakeName(p, b, nm, allSame, varName.has(c.v))) {
2953
3551
  continue;
2954
3552
  }
2955
3553
  // `freshParamMerge` (the `/fresh-merge` variation): this merge takes its own home rather
@@ -3288,6 +3886,26 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
3288
3886
  return { k: 'var', name: '?' };
3289
3887
  };
3290
3888
 
3889
+ // A qualified access is one the recompile must make where the machine did, so its spelling
3890
+ // carries the qualifier or the function declines: a plain spelling is one agbcc may delete or
3891
+ // hoist. It carries it where ir/discipline.ts `qualifiedBy` says: on its cast, or through the
3892
+ // object's declaration and on its cast where it reaches the object through one, since a cast's
3893
+ // pointee drops the declaration's qualifiers.
3894
+ const pinnedAccess = (op: Op, access: Expr): Expr => {
3895
+ const by = qualifiedBy(op);
3896
+ if (by === null || (by === 'declaration' && !castOffDeclaration(access, ctype))) {
3897
+ return access;
3898
+ }
3899
+ const q = qualifiedMemoryAccess(access, ctype);
3900
+ if (q === null) {
3901
+ throw new StructureError(
3902
+ `cannot structure '${fn.name}': a ${placedAt(op)} ${op.opcode} that must stay volatile is reached through a ` +
3903
+ 'pointer with no type to put the qualifier on',
3904
+ );
3905
+ }
3906
+ return q;
3907
+ };
3908
+
3291
3909
  // Lower ONE def's operation to an Expr, rendering operands through `e`. Shared between the
3292
3910
  // inline-at-use path (exprWith) and the materialized-temp path (sideEffects), so both spell a
3293
3911
  // given op identically.
@@ -3295,12 +3913,21 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
3295
3913
  if (d.opcode === 'const') {
3296
3914
  return { k: 'const', value: d.attrs.value as number };
3297
3915
  }
3916
+ if (d.opcode === 'fconst') {
3917
+ return { k: 'fconst', bits: d.attrs.bits as string };
3918
+ }
3298
3919
  // a bitfield extract recognized over the ops (see the precompute above): the member read,
3299
3920
  // not the shift pair
3300
3921
  const bf = bitfieldSpelling.get(d);
3301
3922
  if (bf) {
3302
3923
  return { k: 'field', base: { k: 'var', name: bf.global }, name: bf.field, dot: true };
3303
3924
  }
3925
+ if (FLOAT_TO_BIN[d.opcode]) {
3926
+ return { k: 'bin', op: FLOAT_TO_BIN[d.opcode], l: e(d.operands[0]), r: e(d.operands[1]) };
3927
+ }
3928
+ if (d.opcode === 'fneg') {
3929
+ return { k: 'un', op: 'f-', e: e(d.operands[0]) };
3930
+ }
3304
3931
  if (CMP_TO_BIN[d.opcode]) {
3305
3932
  // A bare global address `&gSym` as a COMPARISON operand is the same unspelled escape as the
3306
3933
  // arithmetic case below (see intifyAddr): its C type comes from the PROJECT's own
@@ -3347,10 +3974,13 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
3347
3974
  !(provablyNonNegative(l, vtEnv) && provablyNonNegative(r, vtEnv))
3348
3975
  ) {
3349
3976
  const irUnsigned = (v: Value): boolean => v.type.kind === 'int' && !v.type.signed;
3977
+ // AT THE OPERAND'S OWN RANK: `(u32)a` over a 64-bit `a` pins nothing, it truncates the
3978
+ // compare to its low half. `exprIntWidth` is total and answers 64 only where it proves it.
3979
+ const pinUnsigned = (x: Expr): Expr => ({ k: 'cast', to: T.u(exprIntWidth(x, vtEnv)), e: x });
3350
3980
  if (!irUnsigned(d.operands[0]) && irUnsigned(d.operands[1])) {
3351
- r = { k: 'cast', to: T.u(32), e: r };
3981
+ r = pinUnsigned(r);
3352
3982
  } else {
3353
- l = { k: 'cast', to: T.u(32), e: l };
3983
+ l = pinUnsigned(l);
3354
3984
  }
3355
3985
  }
3356
3986
  // The SIGNED direction of the same hole, and a DEFAULT rather than an alternative of that variation —
@@ -3369,14 +3999,26 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
3369
3999
  //
3370
4000
  // `undefined` takes the cast exactly as a definite `false` does (see
3371
4001
  // renderedIntSignedness): a call's signedness is the project header's, not this function's.
3372
- // A POINTER-rendered side is the one operand left alone, and not out of caution — `p < q`
3373
- // is already the unsigned compare C gives two addresses, where `(s32)p < (s32)q` would
3374
- // compare them signed.
4002
+ // A POINTER-rendered side takes it too. C compares an address unsigned (`p < q` is agbcc
4003
+ // `bcc`, IDO `sltu`), so a signed opcode over one is a source that compared the address as
4004
+ // an integer, and the byte arithmetic's `(u8 *)g + K` left bare against an `int` compiles
4005
+ // `bcs` where the asm says `blt`.
3375
4006
  if (/^icmp_s/.test(d.opcode)) {
3376
4007
  const pinSigned = (x: Expr): Expr =>
3377
- renderedIntSignedness(x, vtEnv) === true || ptrSide(x) ? x : { k: 'cast', to: T.s(32), e: x };
4008
+ renderedIntSignedness(x, vtEnv) === true ? x : { k: 'cast', to: T.s(exprIntWidth(x, vtEnv)), e: x };
3378
4009
  l = pinSigned(l);
3379
4010
  r = pinSigned(r);
4011
+ } else {
4012
+ // A pointer-rendered side against an INTEGER one (the byte arithmetic's `(u8 *)g + K`
4013
+ // against an `int`) is a constraint violation IDO 7.1 and CodeWarrior reject and agbcc
4014
+ // warns about. The asm compared two words, and unsigned, as C compares a pointer, so the
4015
+ // pointer side is compared as the `u32` it is. A literal 0 is the null pointer constant.
4016
+ const intSide = (x: Expr): boolean => ctype(x)?.kind === 'int' && !(x.k === 'const' && x.value === 0);
4017
+ if (ptrSide(l) && intSide(r)) {
4018
+ l = { k: 'cast', to: T.u(32), e: l };
4019
+ } else if (ptrSide(r) && intSide(l)) {
4020
+ r = { k: 'cast', to: T.u(32), e: r };
4021
+ }
3380
4022
  }
3381
4023
  return { k: 'bin', op: CMP_TO_BIN[d.opcode], l, r };
3382
4024
  }
@@ -3393,7 +4035,13 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
3393
4035
  // out of source order — an ldmia-fed add reads def-reordered; cross-block positions do not
3394
4036
  // order evaluation), neither stamped `listOrder` (an ldmia-expanded load's own position is
3395
4037
  // LIST order), both operand VALUES un-named (see below), no pointer side (load-bearing for
3396
- // the stride rules below), and no effect moves (call, marker).
4038
+ // the stride rules below), and no effect moves (call, marker). A global's value that the
4039
+ // pointer-value rule below spells as a base is a pointer side too, though `ctype` types no
4040
+ // global: that rule keeps the add's operand order, which a swap here would undo.
4041
+ const ptrGlobalSide = (x: Expr): boolean =>
4042
+ x.k === 'var' &&
4043
+ mapUndeclared(x.name) &&
4044
+ (pointerLoadedGlobals.has(x.name) || (d.results[0]?.type.kind === 'ptr' && wordLoadedGlobals.has(x.name)));
3397
4045
  if (COMMUTATIVE_BIN.has(ARITH_TO_BIN[d.opcode]) && d.operands.length === 2) {
3398
4046
  const [da, db] = [defs.get(d.operands[0]), defs.get(d.operands[1])];
3399
4047
  if (
@@ -3416,6 +4064,8 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
3416
4064
  activeSub?.has(d.operands[1]) !== true &&
3417
4065
  ctype(l)?.kind !== 'ptr' &&
3418
4066
  ctype(r)?.kind !== 'ptr' &&
4067
+ !ptrGlobalSide(l) &&
4068
+ !ptrGlobalSide(r) &&
3419
4069
  !exprHasEffect(l) &&
3420
4070
  !exprHasEffect(r) &&
3421
4071
  opBlock.get(da) === opBlock.get(db) &&
@@ -3470,17 +4120,14 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
3470
4120
  // computed offset addresses TWICE the intended byte, and nothing downstream can see the
3471
4121
  // error — in the sa3 decomp that address is what a `CpuSet` call writes THROUGH.
3472
4122
  //
3473
- // KNOWN GAP: `ptr ± ptr` is excluded, and the intify rules below do not make it right
3474
- // either — `ptr + ptr` becomes `l + (s32)r`, which C scales, and a same-pointee `ptr - ptr`
3475
- // is C's ELEMENT difference where the asm subtracted bytes. Both want the same cast-then-add
3476
- // treatment; both are byte-identical to what this emitted before, and three functions in the
3477
- // agbcc corpus carry one.
4123
+ // `ptr - ptr` is C's ELEMENT difference, where the asm subtracted bytes: agbcc compiles `(q -
4124
+ // p) + m` on an `int *` to `sub; asr #2; add`, and spelled `(v0 - a0) >> 2` the lift divided
4125
+ // by 4 twice. Both sides go byte pointers, `(u8 *)v0 - (u8 *)a0`, the byte count in every
4126
+ // world — unless both already are.
3478
4127
  //
3479
- // KNOWN GAP: the inexact-CONSTANT branch above casts its base and does NOT cast the sum
3480
- // back, so `v1 = (u8 *)a0 + 2` still lands in an `s32 *` slot. Copying the restore up churns
3481
- // the common case, where the sum is consumed by a deref that supplies its own cast — the
3482
- // real predicate is the CONSUMER, and deciding it here at the producer is what these two
3483
- // branches would have to stop doing.
4128
+ // The inexact-CONSTANT branch above casts its base and does not cast the sum back: a deref
4129
+ // supplies its own cast, and a temp of another pointer type takes the sum through
4130
+ // intoDeclaredTemp's cast.
3484
4131
  const walkVar = (x: Expr): IrType | undefined => {
3485
4132
  const t = ctype(x);
3486
4133
  return t?.kind === 'ptr' && ptrElemBytes(t.to) !== 1 ? t : undefined;
@@ -3495,7 +4142,12 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
3495
4142
  // the shape from them.
3496
4143
  const lp = ctype(l)?.kind === 'ptr';
3497
4144
  const rp = d.operands.length === 2 && ctype(r)?.kind === 'ptr';
3498
- if (lp && !rp) {
4145
+ if (lp && rp && d.opcode === 'sub') {
4146
+ if (walkVar(l) || walkVar(r)) {
4147
+ l = bytePtr(l);
4148
+ r = bytePtr(r);
4149
+ }
4150
+ } else if (lp && !rp) {
3499
4151
  restoreTo = walkVar(l);
3500
4152
  l = restoreTo ? bytePtr(l) : l;
3501
4153
  } else if (rp && !lp && d.opcode === 'add') {
@@ -3509,14 +4161,27 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
3509
4161
  // DEFINITELY-pointer rendering is cast (same conservative direction as memAccess); the
3510
4162
  // additive ops keep C's legal pointer arithmetic untouched.
3511
4163
  const op = ARITH_TO_BIN[d.opcode];
3512
- const intify = (x: Expr): Expr => (ctype(x)?.kind === 'ptr' ? { k: 'cast', to: T.s(32), e: x } : x);
4164
+ const intify = (x: Expr): Expr =>
4165
+ restoredIntSum(x, false) ?? (ctype(x)?.kind === 'ptr' ? { k: 'cast', to: T.s(32), e: x } : x);
3513
4166
  if (!['+', '-', '&&', '||'].includes(op)) {
3514
4167
  l = intify(l);
3515
4168
  r = intify(r);
3516
4169
  } else if (op === '+' && ctype(l)?.kind === 'ptr' && ctype(r)?.kind === 'ptr') {
3517
- r = intify(r); // ptr + ptr is not C; ptr + (s32)ptr is, with the same bytes
4170
+ // ptr + ptr is not C; ptr + (s32)ptr is, and C scales it by the left pointee. So the left
4171
+ // side is the base, and the sum goes back to its type as the walk's does.
4172
+ r = intify(r);
4173
+ if (!restoreTo) {
4174
+ restoreTo = walkVar(l);
4175
+ l = restoreTo ? bytePtr(l) : l;
4176
+ }
3518
4177
  } else if (op === '-' && ctype(l)?.kind !== 'ptr' && ctype(r)?.kind === 'ptr') {
3519
- r = intify(r); // int - ptr is not C
4178
+ // int - ptr is not C. A byte sum this rule made integer leaves a global it is taken from a
4179
+ // pointer under a pointer declaration, which the integer would scale; a word, it is the
4180
+ // asm's under any integer or pointer one.
4181
+ if (restoredIntSum(r, false) !== undefined) {
4182
+ l = intWords(l);
4183
+ }
4184
+ r = intify(r);
3520
4185
  }
3521
4186
  // A bare global address `&gSym` under ANY of these operators is never emitted as-is: its C
3522
4187
  // type comes from the PROJECT's own declaration (unknowable here — exprCType types `addr`
@@ -3530,10 +4195,10 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
3530
4195
  const intifyAddr = (x: Expr): Expr => (x.k === 'addr' ? { k: 'cast', to: T.u(32), e: x } : x);
3531
4196
  l = intifyAddr(l);
3532
4197
  r = intifyAddr(r);
3533
- // The SAME hazard one level down, for a value the MAP declares a pointer (`gPtr`, `gSym.pBuf`
3534
- // — isPtrValue): C scales `gPtr + K` by sizeof(*gPtr) — 1 under the map's synthesized
3535
- // `void *`, but whatever the PROJECT's header declares (a `u16 *` member, a 0x5C-byte
3536
- // struct) in the world a user actually recompiles in. The asm added BYTES, so the honest
4198
+ // The SAME hazard one level down, for a pointer VALUE (`gPtr`, `gSym.pBuf` — isPtrValue):
4199
+ // C scales `gPtr + K` by sizeof(*gPtr) — 1 under the map's synthesized `void *`, but
4200
+ // whatever the PROJECT's header declares (a `u16 *` member, a 0x5C-byte struct) in the
4201
+ // world a user actually recompiles in. The asm added BYTES, so the honest
3537
4202
  // spelling makes the stride explicit: CAST-THEN-ADD, `(u8 *)gPtr + K`, the same address in
3538
4203
  // EVERY world. Add-then-cast (`(u8 *)(gPtr + K)`, what the backend's deref legalization
3539
4204
  // would otherwise produce) is byte-correct in exactly one of them — a silent wrongness, the
@@ -3544,17 +4209,97 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
3544
4209
  // ACCESS width, a different address whenever that width is not 1.
3545
4210
  // Under the non-additive operators C rejects a pointer outright, so there the honest
3546
4211
  // spelling is integer math on the cell — exactly intifyAddr's `(u32)&gSym` rule.
3547
- const intifyPtrValue = (x: Expr): Expr => ({ k: 'cast', to: T.u(32), e: x });
4212
+ const intifyPtrValue = globalWord;
3548
4213
  if (op === '+' || op === '-') {
3549
4214
  // `ptr ± int` and `ptr - ptr` are byte arithmetic once both sides are byte pointers;
3550
- // `ptr + ptr` and `int - ptr` are not C at all, so the second pointer goes integer.
3551
- const bothPtr = isPtrValue(l) && isPtrValue(r);
3552
- if (isPtrValue(l)) {
3553
- l = bytePtr(l);
4215
+ // `ptr + ptr` and `int - ptr` are not C at all, so the second pointer goes integer. The
4216
+ // other side is a pointer when it is a pointer value too or when it already renders one
4217
+ // (a pointer temp, the walk's `(u8 *)v1`), and then the global's value is the side that
4218
+ // goes integer.
4219
+ //
4220
+ // A global's value no declaration types, added to an integer, is a pointer value too
4221
+ // when the IR types the SUM a pointer: `(u8 *)g + x` is the asm's address under every
4222
+ // declaration of `g` once `x` renders an integer, whichever operand the source held as
4223
+ // the pointer. An integer side that IS an address (`(u32)&gArr + gIdx`) says the address
4224
+ // is the base and the global its index, so that sum stays as it is. An address under a
4225
+ // load (`((u16 *)&gTbl)[a0]`) is a loaded value, and that side is an integer. A global
4226
+ // that is no pointer value renders an integer too, though `ctype` types no global.
4227
+ const isAddr = (x: Expr): boolean =>
4228
+ x.k === 'addr' ||
4229
+ (x.k === 'cast' && isAddr(x.e)) ||
4230
+ (x.k === 'bin' && (x.op === '+' || x.op === '-') && (isAddr(x.l) || isAddr(x.r)));
4231
+ const intSide = (x: Expr): boolean =>
4232
+ !isAddr(x) && (ctype(x)?.kind === 'int' || (x.k === 'var' && ctype(x) === undefined && !isPtrValue(x)));
4233
+ const sumBase =
4234
+ d.results[0]?.type.kind === 'ptr' && !isPtrValue(l) && !isPtrValue(r)
4235
+ ? isUndeclaredGlobalValue(l) && intSide(r)
4236
+ ? l
4237
+ : op === '+' && isUndeclaredGlobalValue(r) && intSide(l)
4238
+ ? r
4239
+ : undefined
4240
+ : undefined;
4241
+ const ptrValue = (x: Expr): boolean => isPtrValue(x) || x === sumBase;
4242
+ // A global no declaration types in the base's partner is added as an integer, which it is
4243
+ // under an integer declaration and a pointer one alike.
4244
+ if (sumBase !== undefined) {
4245
+ l = l !== sumBase ? intWords(l) : l;
4246
+ r = r !== sumBase ? intWords(r) : r;
4247
+ }
4248
+ const rendersPtr = (x: Expr): boolean => ptrValue(x) || ctype(x)?.kind === 'ptr';
4249
+ const bothPtr = rendersPtr(l) && rendersPtr(r);
4250
+ // `x + (u8 *)g` is pointer arithmetic, and gcc makes the pointer the first operand of the
4251
+ // add, which swaps the asm's. So under an integer left side, the value of a global no
4252
+ // declaration types, or a byte sum this rule made of one, is added as an integer in the
4253
+ // asm's order, and the cast keeps the sum the byte pointer it would have been. A declared
4254
+ // pointer keeps `x + (u8 *)p`, the operand the element and field spellings read.
4255
+ const undeclaredPtr = (x: Expr): boolean => x.k === 'var' && mapUndeclared(x.name);
4256
+ // The same global LEFT of a runtime offset is added as an integer too: CodeWarrior at -O4
4257
+ // puts the index first in every pointer sum, where an integer sum keeps the source's
4258
+ // order, so `(u8 *)g + x` is the asm's order on agbcc, KMC gcc and IDO only. The partner
4259
+ // goes integer with it, or a pointer partner would scale the sum. A constant offset folds
4260
+ // into the access and keeps `(u8 *)g + K`.
4261
+ const constValued = (x: Expr): boolean =>
4262
+ x.k === 'const' ||
4263
+ (x.k === 'cast' && constValued(x.e)) ||
4264
+ (x.k === 'bin' && constValued(x.l) && constValued(x.r));
4265
+ const intSum = op === '+' && !constValued(r) && ptrValue(l) && undeclaredPtr(l);
4266
+ if (intSum) {
4267
+ l = intifyPtrValue(l);
4268
+ r = ptrValue(r)
4269
+ ? intifyPtrValue(r)
4270
+ : ctype(r)?.kind === 'ptr'
4271
+ ? isByteGlobalSum(r)
4272
+ ? byteSumAsInt(r)
4273
+ : { k: 'cast', to: T.u(32), e: r }
4274
+ : intWords(r);
4275
+ restoreTo ??= T.ptr(T.u(8));
4276
+ } else if (ptrValue(l)) {
4277
+ l = op === '+' && bothPtr && !ptrValue(r) ? intifyPtrValue(l) : bytePtr(l);
3554
4278
  }
3555
- if (isPtrValue(r)) {
4279
+ if (intSum) {
4280
+ // both operands are integers now
4281
+ } else if (
4282
+ op === '+' &&
4283
+ !bothPtr &&
4284
+ !restoreTo &&
4285
+ ((ptrValue(r) && undeclaredPtr(r)) || (ctype(r)?.kind === 'ptr' && isByteGlobalSum(r, true)))
4286
+ ) {
4287
+ r = ptrValue(r) ? intifyPtrValue(r) : byteSumAsInt(r);
4288
+ // A left global no declaration types may be a pointer in the project's header, which
4289
+ // would scale the integer sum; as an integer it is the asm's word under any declaration.
4290
+ l = intWords(l);
4291
+ restoreTo = T.ptr(T.u(8));
4292
+ } else if (ptrValue(r)) {
3556
4293
  r = bothPtr && op === '-' ? bytePtr(r) : op === '+' && !bothPtr ? bytePtr(r) : intifyPtrValue(r);
3557
4294
  }
4295
+ // A byte sum less an integer sum of globals no declaration types: bare, `gB2 - gB3` is an
4296
+ // element count under a wider pointer declaration of them, where the asm subtracted bytes.
4297
+ if (op === '-' && r.k === 'bin' && !rendersPtr(r) && isByteGlobalSum(l)) {
4298
+ r = intWords(r);
4299
+ }
4300
+ if (op === '-' && ctype(l)?.kind === 'ptr' && ctype(r)?.kind === 'ptr') {
4301
+ restoreTo = undefined; // a byte pointer less a byte pointer is the byte count, an integer
4302
+ }
3558
4303
  } else if (op !== '&&' && op !== '||') {
3559
4304
  // (`&&`/`||` take a pointer operand legally — a truth test, no arithmetic.)
3560
4305
  l = isPtrValue(l) ? intifyPtrValue(l) : l;
@@ -3563,10 +4308,9 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
3563
4308
  // (The signedness-carrying pairs stay DISTINCT ops here — `>>>`/`>>` and `/u` `%u`/`/` `%`.
3564
4309
  // Which token a language spells each with, and what cast pins the choice, is a BACKEND
3565
4310
  // decision; see l3/ast.ts BinOp and backend/cfamily.ts's C_SPELLING.)
3566
- // SCOPE: this and intifyAddr cover the ARITHMETIC escapes. A pointer global under a
3567
- // COMPARISON (`gPtr < K` — C compares unsigned whatever the asm's icmp_s* said) is the same
3568
- // class as intifyAddrCmp's `addr` rule and is deliberately left alone here: it is valid C
3569
- // today, so closing it would churn spellings for a signedness case no row exercises.
4311
+ // SCOPE: this and intifyAddr cover the ARITHMETIC escapes. A pointer value under a
4312
+ // COMPARISON (`gPtr < K`, `(u8 *)gPtr + K < a0`) needs nothing here: an icmp_s* pins every
4313
+ // operand that does not provably render signed, a pointer included (pinSigned above).
3570
4314
  //
3571
4315
  // A `+`/`|` over two IR `const`s that both RENDER as literals is the literal it is. After
3572
4316
  // pre-recovery there is only one way such an op still exists: `raise/const.ts` refuses to fold
@@ -3666,13 +4410,122 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
3666
4410
  ? { k: 'cast', to: T.int(w, d.opcode === 'sext'), e: e(d.operands[0]) }
3667
4411
  : mkGap(`no C type for a ${w}-bit '${d.opcode}'`, [e(d.operands[0])]);
3668
4412
  }
4413
+ // The recovered integer type of a value, where it has one. `lo32`/`hi32`/`concat` all cast to
4414
+ // one, and a value recovery left as anything else has no cast to spell.
4415
+ const asInt = (t: IrType) => (t.kind === 'int' ? t : undefined);
4416
+ // THE TWO HALVES OF A 64-BIT VALUE, which C spells exactly: the low half is a truncating cast,
4417
+ // the high half a shift and then the same cast. The RESULT's own recovered type is what the
4418
+ // cast is to, and the shift's kind comes from the 64-bit operand's — `>>` over a signed value
4419
+ // and `>>>` over an unsigned one is what the machine did, and what the C backend's operand pin
4420
+ // is spelled in terms of.
4421
+ if (d.opcode === 'lo32' || d.opcode === 'hi32') {
4422
+ const src = e(d.operands[0]);
4423
+ const half = asInt(d.results[0].type) ?? T.u(32);
4424
+ // The LOW half is a truncating cast and needs nothing of its operand's rank: `(u32)x` and
4425
+ // `(u32)(s64)x` are the same value whatever `x` renders as.
4426
+ if (d.opcode === 'lo32') {
4427
+ return { k: 'cast', to: half, e: src };
4428
+ }
4429
+ // THE HIGH HALF SHIFTS BY 32, AND `x >> 32` IS UNDEFINED IN C WHERE `x` RENDERS 32 BITS
4430
+ // WIDE — the shift count must be below the promoted left operand's width. The 64-bit VALUE
4431
+ // is what this op projects, but the rendered EXPRESSION is a separate question and the two
4432
+ // can part, so the operand is made to carry the rank the shift reads by a cast to the
4433
+ // value's own recovered 64-bit type.
4434
+ //
4435
+ // THE SIGNEDNESS IS A SEPARATE PIN AND IS NOT MADE HERE. `>>` versus `>>>` is read off the
4436
+ // 64-bit operand's recovered type, and what the printed operand renders as is decided one
4437
+ // level down by backend/cfamily.ts `pinnedOperands` — which casts whenever the operand does
4438
+ // not PROVABLY render as the operator needs, and a call never does. So the cast this arm may
4439
+ // add carries the rank alone; claiming it carries both would be a claim about a decision
4440
+ // this file does not make.
4441
+ const whole = asInt(d.operands[0].type);
4442
+ const wide: Expr | undefined =
4443
+ exprIntWidth(src, vtEnv) === 64 ? src : whole === undefined ? undefined : { k: 'cast', to: whole, e: src };
4444
+ // A 64-BIT VALUE WHOSE RECOVERY IS NOT AN INTEGER, AND WHOSE RENDERING DOES NOT ALREADY
4445
+ // CARRY THE RANK, has no 64-bit type to cast to and no legal shift to spell, so it is a GAP
4446
+ // rather than the undefined C it would otherwise print — the same loud floor the un-nameable
4447
+ // `sext` width gets two arms above. The condition is the CONJUNCTION and the refusal states
4448
+ // it as one: neither half alone is a reason. Nothing in the corpus reaches it: every `hi32`
4449
+ // is built beside a `concat` or a pair return whose recovery is an integer, and a recovery
4450
+ // that is not one stops at the `concat` gap below before a projection of it is rendered.
4451
+ if (wide === undefined) {
4452
+ // "UPPER HALF", NOT "HIGH HALF", for the reason the split-pair refusal in thumb.ts gives at
4453
+ // its own throw: `apps/web`'s `reloc-halves` decline class holds `/high half/`, its list is
4454
+ // ORDERED, and a 64-bit gap published as a relocation gap because one entry comes first is
4455
+ // a classification nothing states. `wide-call-arg` claims this phrase BY NAME
4456
+ // (`/no upper half to shift out/`) — a 64-bit gap with no class at all renders as
4457
+ // "Other / unclassified", which is the same wrong answer arrived at from the other side.
4458
+ return mkGap(
4459
+ 'a 64-bit value that neither renders 64 bits wide nor has an integer type has no upper half to shift out',
4460
+ [src],
4461
+ );
4462
+ }
4463
+ return {
4464
+ k: 'cast',
4465
+ to: half,
4466
+ e: { k: 'bin', op: whole?.signed ? '>>' : '>>>', l: wide, r: { k: 'const', value: 32 } },
4467
+ };
4468
+ }
4469
+ // …AND THE WIDEN THAT BUILDS ONE, which the machine spells as a PAIR rather than as a cast —
4470
+ // `asr rN,rM,#31` for the signed extension of a word, `mov rN,#0` for the unsigned one. So the
4471
+ // shape is what says HOW the half was widened, and the cast this renders is to whatever 64-bit
4472
+ // type recovery gave the value.
4473
+ //
4474
+ // THE WORD'S OWN SIGNEDNESS IS WHAT THE WIDEN MEANS, and it is pinned the way every other
4475
+ // signedness-carrying operand in this function is: cast only where the operand does not
4476
+ // already render that way. Leaving the signed arm to whatever `x` happens to render as reads
4477
+ // the widen off the DECLARATION rather than off the `asr` — under `/unsigned` the parameter is
4478
+ // declared `u32` and `(s64)x` then says zero-extend, against an instruction that says
4479
+ // otherwise.
4480
+ //
4481
+ // A `concat` of anything else is a 64-bit value this pipeline has no C spelling for, and it
4482
+ // falls through to the loud gap at the bottom — which is the whole safety story for the
4483
+ // representation. A pair that tried to cross a join matches nothing here.
4484
+ if (d.opcode === 'concat') {
4485
+ const [lo, hi] = d.operands;
4486
+ const hiDef = defs.get(hi);
4487
+ const whole = asInt(d.results[0].type);
4488
+ const signedWiden = hiDef?.opcode === 'shr_s' && hiDef.operands[0] === lo && hiDef.attrs.imm === 31;
4489
+ const unsignedWiden = hiDef?.opcode === 'const' && hiDef.attrs.value === 0;
4490
+ if (whole && (signedWiden || unsignedWiden)) {
4491
+ const word = e(lo);
4492
+ const half =
4493
+ renderedIntSignedness(word, vtEnv) === signedWiden
4494
+ ? word
4495
+ : { k: 'cast' as const, to: T.int(32, signedWiden), e: word };
4496
+ return { k: 'cast', to: whole, e: half };
4497
+ }
4498
+ }
3669
4499
  if (d.opcode === 'call') {
3670
- return { k: 'call', fn: d.attrs.target as string, args: d.operands.map(e) };
4500
+ // THE RANK THE RETURN RENDERS WITH TRAVELS ON THE NODE, because no later reader can recover
4501
+ // it: the width is a fact the frontend was TOLD (a runtime helper's signature, or a project's
4502
+ // `FnProto.returns`) and it is gone from the tree by the time the C backend pins an operand.
4503
+ // Stamping it here — at the one place a call becomes an expression — is what keeps every
4504
+ // reader of `exprIntWidth` agreeing. `grep -rn exprIntWidth packages/core/src` is the
4505
+ // enumeration, and it lists SIX call sites in three files: the `hi32` shift and the two
4506
+ // compare pins in this file, the shift rank and the divide rank in backend/cfamily.ts
4507
+ // `pinnedOperands`, and `l3/initfirst.ts`'s `meaningPreserved`.
4508
+ //
4509
+ // THE LAST ONE IS A BEHAVIOUR CHANGE THE STAMP MAKES, and it is named because a reader who
4510
+ // trusts an enumeration is owed a complete one: a `wide64` call answered 32 before the stamp
4511
+ // and answers 64 now, so `meaningPreserved` refuses to fold such a result into a 32-bit
4512
+ // local where it used to admit it. That is what its own reason asks for — a 64-bit value
4513
+ // assigned to a 32-bit one truncates — but it is a pass this branch did not set out to move.
4514
+ const wide64 = d.results[0] !== undefined && intWidth(d.results[0].type) === 64;
4515
+ const through = calleeValue(d);
4516
+ return {
4517
+ k: 'call',
4518
+ fn: through === undefined ? (d.attrs.target as string) : e(through),
4519
+ args: callArgs(d).map(e),
4520
+ ...(wide64 ? { wide64: true as const } : {}),
4521
+ ...(d.attrs.sret === true ? { sret: d.results[0].type } : {}),
4522
+ };
3671
4523
  }
3672
4524
  if (d.opcode === 'laddr') {
3673
4525
  // gaddr's local twin: the address of the frame-local object the Thumb frontend PROVED
3674
4526
  // (frame-object audit — width/signed are stamped machine facts). The NAME is this layer's:
3675
- // see laddrName. Renders `&sp0`; the object itself is declared in `locals`.
4527
+ // see laddrName. Renders `&sp0`; the object itself is declared in `locals`, and an ARRAY
4528
+ // one renders by decay, which the C printer reads off that declaration.
3676
4529
  return { k: 'addr', name: laddrName.get(d)! };
3677
4530
  }
3678
4531
  if (d.opcode === 'undef') {
@@ -3691,7 +4544,7 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
3691
4544
  return { k: 'addr', name: d.attrs.sym as string };
3692
4545
  }
3693
4546
  if (d.opcode === 'load') {
3694
- return memAccess(
4547
+ const access = memAccess(
3695
4548
  d.operands[0],
3696
4549
  e(d.operands[0]),
3697
4550
  d.attrs.off as number,
@@ -3703,11 +4556,12 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
3703
4556
  false,
3704
4557
  advanceStepOf(d.operands[0]),
3705
4558
  );
4559
+ return pinnedAccess(d, access);
3706
4560
  }
3707
4561
  // aload carries a runtime index operand (variable-index array access) — `base[index]`, or
3708
4562
  // `base[index].field_K` when it carries a `fieldOff` (array-of-STRUCT element access).
3709
4563
  if (d.opcode === 'aload') {
3710
- return arrayAccess(
4564
+ const access = arrayAccess(
3711
4565
  d.operands[0],
3712
4566
  e(d.operands[0]),
3713
4567
  e(d.operands[1]),
@@ -3718,16 +4572,28 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
3718
4572
  ctype,
3719
4573
  symCtx,
3720
4574
  );
4575
+ return pinnedAccess(d, access);
3721
4576
  }
3722
4577
  return d.opcode === 'opaque'
3723
- ? mkGap(gapReasonFor(d.attrs.mnemonic), d.operands.map(e))
4578
+ ? mkGap(gapReasonFor(d.attrs), d.operands.map(e))
3724
4579
  : mkGap(`no lowering for op '${d.opcode}'`, d.operands.map(e));
3725
4580
  };
3726
4581
 
4582
+ // A map EXTENDS the ambient post-loop substitution (`activeSub`, below) and wins where both map a
4583
+ // value: a loop's own reading is layered over the enclosing exit region's, which still holds
4584
+ // inside it. Replacing the ambient one would re-derive a value a previous loop left in its
4585
+ // variable's name from that name, which already holds it (`v1 + 1` for `v1`). `null` is no
4586
+ // substitution at all.
3727
4587
  const exprWith = (sub: Map<Value, string> | null) => {
3728
4588
  const e = (v: Value): Expr => {
3729
- const subbed = sub?.get(v);
4589
+ const subbed = sub === null ? undefined : (sub.get(v) ?? activeSub?.get(v));
3730
4590
  if (subbed) {
4591
+ if (zeroTripStale?.get(v) === subbed) {
4592
+ throw new StructureError(
4593
+ `cannot structure '${fn.name}': a read after the loop spells a value computed before it by a ` +
4594
+ `loop variable's name, which holds its initial value on a zero-trip run`,
4595
+ );
4596
+ }
3731
4597
  return { k: 'var', name: subbed };
3732
4598
  }
3733
4599
  if (varName.has(v)) {
@@ -3748,7 +4614,17 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
3748
4614
  // naming walk and the coalescing that follows it, and these checks only read it — so today the
3749
4615
  // two readings coincide; the reference is what keeps them coinciding if a write ever moves down
3750
4616
  // here.
3751
- const { readsClobbered, loopUpdateHazard, sinkablePreUpdateSlots, sameAtEntry, loopWriteSet } = makeLoopHazards({
4617
+ const {
4618
+ readsClobbered,
4619
+ loopEscapeHazard,
4620
+ loopUpdateHazard,
4621
+ testSkipsAnEffect,
4622
+ preUpdateCondFold,
4623
+ sinkablePreUpdateSlots,
4624
+ sameAtEntry,
4625
+ loopWriteSet,
4626
+ loopWriteSetAhead,
4627
+ } = makeLoopHazards({
3752
4628
  defs,
3753
4629
  varName,
3754
4630
  useSitesOf,
@@ -3756,6 +4632,7 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
3756
4632
  opBlock,
3757
4633
  materialize,
3758
4634
  respelledDefs: bitfieldSpelling,
4635
+ emitPos,
3759
4636
  });
3760
4637
 
3761
4638
  // A POST-LOOP substitution active while structuring a loop's exit region: a loop-carried value (a
@@ -3763,6 +4640,11 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
3763
4640
  // read the name, not re-inline the computation (which would double-count, e.g. `(u8)(v1+1)` instead
3764
4641
  // of `v1`). `expr` consults it; `withSub` installs/merges it around the exit region. Null normally.
3765
4642
  let activeSub: Map<Value, string> | null = null;
4643
+ // The entries of a guarded self-loop's substitution its exit region may not render (that site's
4644
+ // `stale`), merged over every enclosing guarded loop's like `activeSub` is: consulted where ANY
4645
+ // substitution hit renders in scope, so it judges what the region spells, dead copies included —
4646
+ // which over-declines, the refusing direction.
4647
+ let zeroTripStale: Map<Value, string> | null = null;
3766
4648
  const expr = (v: Value): Expr => exprWith(activeSub)(v);
3767
4649
  const withSub = <R>(sub: Map<Value, string>, run: () => R): R => {
3768
4650
  const prev = activeSub;
@@ -3802,9 +4684,9 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
3802
4684
  // stores whatever the arm left — the same substitution the parameter case makes, one step over.
3803
4685
  //
3804
4686
  // THE SECOND RELOCATION goes the other way, and this test cannot see it at all. A SUNK pre-update
3805
- // exit copy (preUpdateCopies) writes the loop EXIT's param at the top of the loop BODY, so the
3806
- // home this reads for it — `paramBlock`, the exit block — sits strictly LATER in the CFG than
3807
- // where the copy lands. What keeps the two apart is not this test but `dest-free-inside-loop`
4687
+ // exit copy (preUpdateCopies) writes the loop EXIT's param INSIDE the loop BODY, so the home this
4688
+ // reads for it — `paramBlock`, the exit block — sits strictly LATER in the CFG than where the copy
4689
+ // lands. What keeps the two apart is not this test but `dest-free-inside-loop`
3808
4690
  // (hazards.ts): a merge inside the body under the exit param's name is a block param
3809
4691
  // `definedInBody` sees, so the slot is never sunk in the first place. That gate carries its own
3810
4692
  // KNOWN GAP, so the pair is a conjecture rather than a proof — `sunkCopyOverDroppedUndef` re-checks
@@ -3818,10 +4700,16 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
3818
4700
  }
3819
4701
  /** Every copy `undefCarriesNothing` dropped, with the edge it was dropped from. */
3820
4702
  const droppedUndefCopies: { name: string; pred: Block }[] = [];
3821
- /** Every sunk pre-update exit copy, homed where it LANDS (the loop header whose body opens with
3822
- * it) rather than where its destination param lives. These two lists are the postcondition's
3823
- * whole input. */
4703
+ /** Every sunk pre-update exit copy, homed where it LANDS (the loop header, which dominates every
4704
+ * point inside the body a copy is rebuilt at) rather than where its destination param lives.
4705
+ * These two lists are the postcondition's whole input. */
3824
4706
  const sunkCopyHomes: { name: string; home: Block }[] = [];
4707
+ /** A loop's sunk pre-update exit copies: those rebuilt at the op that computed their value, and
4708
+ * those with no such op, which open the body. */
4709
+ type SunkCopies = { leading: Stmt[]; atDef: Map<Op, Stmt[]> };
4710
+ const NO_COPIES_AT_A_DEF: ReadonlyMap<Op, Stmt[]> = new Map();
4711
+ /** The `atDef` copies `sideEffects` has spelled — `assertSunkCopiesPlaced`'s input. */
4712
+ const sunkCopiesEmitted = new Set<Stmt>();
3825
4713
  const undefCarriesNothing = (arg: Value, name: string, pred: Block): boolean => {
3826
4714
  if (defs.get(arg)?.opcode !== 'undef') {
3827
4715
  return false;
@@ -4064,24 +4952,28 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
4064
4952
  // EFFECTFUL op whose result nothing consumes (a void/discarded call, and an `opaque` standing for
4065
4953
  // an instruction asmlift could not model); and MATERIALIZED defs — a call/load whose value cannot
4066
4954
  // soundly render at its use is assigned to its named temp here, at its own program position.
4067
- /** A pointer VALUE assigned into a map-declared pointer CELL, spelled so the assignment is legal
4068
- * against ANY declaration of that cell. The byte-arithmetic guard renders such a right-hand
4069
- * side `(u8 *)gS.pBuf + K` — the right ADDRESS in every world, which is the whole point of it,
4070
- * and a `u8 *` where the declaration says `u16 *`. READING one is fine (C converts an object
4071
- * pointer freely under a deref, a call argument or a compare); ASSIGNING one is `warning:
4072
- * assignment from incompatible pointer type`, and this project's own `-Werror` compiler
4073
- * template makes that FATAL — so a source that scores clean here fails to build in the tree a
4074
- * user pastes it into, which no score gate can observe.
4955
+ /** A pointer VALUE assigned into a pointer CELL (isPtrValue), spelled so the assignment is legal
4956
+ * against ANY pointer declaration of that cell. The byte-arithmetic guard renders such a
4957
+ * right-hand side `(u8 *)gS.pBuf + K` — the right ADDRESS in every world, which is the whole
4958
+ * point of it, and a `u8 *` where the declaration says `u16 *`. READING one is fine (C converts
4959
+ * an object pointer freely under a deref, a call argument or a compare); ASSIGNING one is
4960
+ * `warning: assignment from incompatible pointer type`, and this project's own `-Werror`
4961
+ * compiler template makes that FATAL — so a source that scores clean here fails to build in the
4962
+ * tree a user pastes it into, which no score gate can observe.
4075
4963
  *
4076
4964
  * `void *`, NOT the map's declared pointee: it is the one target assignment-compatible with any
4077
4965
  * object-pointer declaration, the same "same answer in every world" property the guard that
4078
4966
  * made the `u8 *` exists for. Trusting the map's pointee would put the spelling back in one
4079
4967
  * world. It costs nothing: agbcc compiles `p = (void *)((u8 *)p + 4)` and the warning-carrying
4080
4968
  * `p = (u8 *)p + 4` to BYTE-IDENTICAL objects (`-mthumb-interwork -Wimplicit -O2 -fhex-asm
4081
- * -fprologue-bugfix`), so the fix is invisible to the differ and visible to the compiler. */
4969
+ * -fprologue-bugfix`), so the fix is invisible to the differ and visible to the compiler.
4970
+ *
4971
+ * A project may still declare a cell no map types an INTEGER (`u32 gSym;`), where `(void *)`
4972
+ * makes an integer from a pointer: agbcc warns, CodeWarrior rejects it. The candidate's own world
4973
+ * declares the cell `void *` (l3/symbol-refs.ts), and `/int-cell` (l3/intcell.ts) spells the
4974
+ * integer declaration's source, published only at a byte-exact score. */
4082
4975
  const intoPtrCell = (lval: Expr, value: Expr): Expr => {
4083
- const cell = lval.k === 'var' ? symCtx?.info(lval.name)?.shape === 'pointer' : ptrMemberDecl(lval, symCtx) !== null;
4084
- if (!cell) {
4976
+ if (!isPtrValue(lval)) {
4085
4977
  return value;
4086
4978
  }
4087
4979
  const vt = ctype(value);
@@ -4093,80 +4985,49 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
4093
4985
  return isPtr ? { k: 'cast', to: T.ptr(T.void()), e: value } : value;
4094
4986
  };
4095
4987
 
4096
- /** THE REFUSAL for the read half of `unreadResult`. TWO questions, and the statement is spelled
4097
- * only where BOTH answer yes; the second is not implied by the first.
4988
+ /** THE REFUSAL for the read half of `unreadResult`: can a `volatile` reach this dead read's
4989
+ * spelling, here or in some candidate enumerated from this tree?
4990
+ *
4991
+ * A QUALIFIED read (ir/discipline.ts `qualified`) answers yes: `pinnedAccess` puts the qualifier
4992
+ * on its spelling, from the device pin or from the map's declaration of the object it reads. Every
4993
+ * other read is plain in this tree, and only a variation can qualify it, so the statement is
4994
+ * spelled only where both of these hold:
4098
4995
  *
4099
4996
  * 1. EVIDENCE — would a source plausibly have declared this access `volatile`? The answer has
4100
4997
  * to come from DATA: the target's declared device-register window
4101
- * (`capabilities.deviceRegisters`) or the symbol map's own `volatile` on the named global.
4102
- * `volatile` is a CORRECTNESS claim about an address, not a spelling preference, and
4103
- * asserting one about ordinary RAM is a wrong answer rather than a wrong spelling.
4104
- * 2. REACHABILITY — will the spelling this access gets CARRY a qualifier, here or in some
4105
- * candidate enumerated from this tree? The payoff of spelling a dead read is that a
4106
- * qualifier can land on it and the differ can referee the pair; where none can, the
4107
- * statement is a permanent bare deref in the DEFAULT source, which is what the playground
4108
- * pins and what a decomp author copies. This question refuses far less than question 1:
4109
- * `/volatile` qualifies an EWRAM or ROM address quite happily.
4998
+ * (`capabilities.deviceRegisters`). `volatile` is a CORRECTNESS claim about an address, not a
4999
+ * spelling preference, and asserting one about ordinary RAM is a wrong answer rather than a
5000
+ * wrong spelling.
5001
+ * 2. REACHABILITY — the base value has a use OTHER than this read: l3/volatileptr.ts qualifies a
5002
+ * pointer LOCAL, and l3/basecse.ts only mints that local for a base something else also
5003
+ * touches. A single-access read — `*(s32 *)0x04000200;`, the `REG_IF` acknowledge idiom, and
5004
+ * the shape a WRONG `returnsVoid` on a register accessor produces — has no local to qualify
5005
+ * and never will, however plainly its address is a device register. "Some other use" is
5006
+ * NECESSARY for that local, not sufficient; the gate states the necessary half. Where no
5007
+ * qualifier can land, the statement would be a permanent bare deref in the DEFAULT source,
5008
+ * which is what the playground pins and what a decomp author copies.
4110
5009
  *
4111
- * THE MAP ARM needs the read spelled through the global's own NAME, which is where the map's
4112
- * qualifier lands — memAccess's two name-carrying arms for a global, the bare scalar
4113
- * (`gStatus;`) and the declared struct MEMBER (`gState.ctl;`). A CAST spelling
4114
- * (`((s32 *)&REG_DMA3SAD)[2]`) has thrown the qualifier away in the spelling itself, whatever
4115
- * the declaration says. The member arm asks the CONTAINER's qualifier and not the member's own
4116
- * (`SymbolStructField.volatile`), which looks backwards and is not: `memberQualsAllow` above
4117
- * refuses to NAME a volatile member at all, so a `vu16` member is spelled `((s32 *)&gSym)[k]`
4118
- * with nothing in the spelling for a variation to hold, while `volatile struct S gSym;` qualifies
4119
- * every member and `gSym.ctl;` really is an observable read. Every other map spelling refuses —
4120
- * `gPtr->member`, a bare-name array element, a multidimensional subscript — because
4121
- * over-refusing costs a SPELLING and admitting wrongly costs an ANSWER, this file's standing
4122
- * asymmetry.
5010
+ * The address is a LITERAL, which inhabits only `/raw-globals`: with a symbol map the frontend
5011
+ * spells a pool word as `gaddr`, so `constAddressOf` returns null, and a read of a named object
5012
+ * is plain because the map does not declare it volatile.
4123
5013
  *
4124
- * THE LITERAL ARM needs the base value to have a use OTHER than this read: l3/volatileptr.ts
4125
- * qualifies a pointer LOCAL, and l3/basecse.ts only mints that local for a base something else
4126
- * also touches. A single-access read — `*(s32 *)0x04000200;`, the `REG_IF` acknowledge idiom,
4127
- * and the shape a WRONG `returnsVoid` on a register accessor produces — has no local to qualify
4128
- * and never will, however plainly its address is a device register. "Some other use" is
4129
- * NECESSARY for that local, not sufficient; the gate states the necessary half.
4130
- *
4131
- * ONLY `load` REACHES EITHER ARM. `aload` carries its index in `operands[1]` and has no `off`
4132
- * attr at all, so both address queries would answer for the BARE BASE — `globalCellOf` resolves
4133
- * a base and discards the index by construction (ir/alias.ts), `constAddressOf` sees the literal
5014
+ * ONLY `load` REACHES THE ARM. `aload` carries its index in `operands[1]` and has no `off` attr at
5015
+ * all, so the address query would answer for the BARE BASE — `constAddressOf` sees the literal
4134
5016
  * with `off` defaulted to 0 — which admits `volatile s32 *p0 = (s32 *)0x04000000; p0[a0];`, a
4135
5017
  * qualified access at an address the declared window does not cover. The whitelist is by OPCODE
4136
- * so a read op added later refuses until someone answers both questions for it.
4137
- *
4138
- * THE ARMS DO NOT SHARE A POPULATION. With a symbol map the frontend spells a pool word as
4139
- * `gaddr`, so `constAddressOf` returns null and the literal arm inhabits only `/raw-globals`,
4140
- * which re-structures with NO map. The map arm is the default-source one, and a map-fed DMA
4141
- * function spells no read at all — its wait-read is cast-spelled (see `BASECSE_GATES`'
4142
- * `repeated-const-offset`, whose base local this refusal hands back). */
5018
+ * so a read op added later refuses until someone answers both questions for it. */
4143
5019
  const volatileQualifiable = (op: Op): boolean => {
5020
+ if (qualified(op)) {
5021
+ return true;
5022
+ }
4144
5023
  if (op.opcode !== 'load') {
4145
5024
  return false;
4146
5025
  }
4147
- const off = typeof op.attrs.off === 'number' ? op.attrs.off : 0;
4148
- const width = op.attrs.width as number;
4149
- const cell = globalCellOf(defs, op.operands[0], off);
4150
- if (cell) {
4151
- const si = symbols?.get(cell.name);
4152
- if (si === undefined) {
4153
- return false;
4154
- }
4155
- if (si.shape === 'struct') {
4156
- // the SAME find memAccess's struct arm makes, so the two cannot disagree about which
4157
- // accesses reach the `gSym.field` spelling this arm's qualifier rides on
4158
- const fld = symCtx
4159
- ?.fieldsOf(cell.name)
4160
- ?.find((f) => f.offset === cell.byte && f.size === width && !isArrayField(f) && !isBitfieldField(f));
4161
- return fld !== undefined && memberQualsAllow(fld, si.const, false) && si.volatile === true;
4162
- }
4163
- return cell.byte === 0 && scalarGlobals.has(cell.name) && si.volatile === true;
4164
- }
4165
5026
  const window = opts.deviceRegisters;
4166
5027
  if (!window) {
4167
5028
  return false;
4168
5029
  }
4169
- const addr = constAddressOf(defs, op.operands[0], off);
5030
+ const addr = constAddressOf(defs, op.operands[0], typeof op.attrs.off === 'number' ? op.attrs.off : 0);
4170
5031
  if (addr === null || addr < window[0] || addr >= window[1]) {
4171
5032
  return false;
4172
5033
  }
@@ -4174,11 +5035,11 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
4174
5035
  };
4175
5036
 
4176
5037
  /** Ops the `sideEffects` walk must SPELL even though nothing consumes their result — the
4177
- * registry's own derived set (`SPELLED_WHEN_DEAD_OPS`), so the next op to acquire the property
4178
- * needs no edit here, plus the address refusal above for the memory-read half.
5038
+ * question ir/discipline.ts asks of every op (`spelledWhenDead`), so the next op to acquire the
5039
+ * property needs no edit here, plus the address refusal above for the memory-read half.
4179
5040
  *
4180
- * A memory READ is not in `EFFECTFUL_OPS`, deliberately: ir/opcodes.ts calls a load deletable
4181
- * when dead, because nothing observes a read nobody reads. That is the C claim. The COMPILER
5041
+ * A memory READ is not `effectful`, deliberately: ir/discipline.ts calls a load deletable when
5042
+ * dead, because nothing observes a read nobody reads. That is the C claim. The COMPILER
4182
5043
  * claim points the other way — an optimizing compiler deletes every dead read it is allowed to
4183
5044
  * delete, so one still in the target is evidence the source's access was `volatile`, and
4184
5045
  * dropping it deletes an instruction the machine executed.
@@ -4203,9 +5064,8 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
4203
5064
  * the other preimage is a function whose `returnsVoid` fact is WRONG, so its return value
4204
5065
  * arrived here as a dead read. The second-use clause removes the common shape of that (a bare
4205
5066
  * register accessor), leaving a function that both STORES to a device register and reads one
4206
- * back — narrow, but not proven empty. The MAP arm has no such problem: `gStatus;` under
4207
- * `extern volatile u32 gStatus;` compiles differently from its own absence, so the differ can
4208
- * referee it. The clean fix — spell the read only in candidates that also qualify it, paired
5067
+ * back — narrow, but not proven empty. A qualified read has no such problem: `gStatus;` under
5068
+ * `extern volatile u32 gStatus;` compiles differently from its own absence. The clean fix — spell the read only in candidates that also qualify it, paired
4209
5069
  * the way `/livebase/volatile` already pairs — moves every device row's fan shape and wants
4210
5070
  * its own round and zero-flip gate over BOTH tiers.
4211
5071
  * 2. THE REFUSAL IS SILENT. When this returns false for a `load`, the machine performed a read
@@ -4268,7 +5128,7 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
4268
5128
  const hasSpelledUse = (v: Value): boolean => (useSitesOf.get(v) ?? []).some((s) => isSpelled(s.op));
4269
5129
 
4270
5130
  const unreadResult = (op: Op): boolean =>
4271
- SPELLED_WHEN_DEAD_OPS.has(op.opcode) &&
5131
+ spelledWhenDead(op) &&
4272
5132
  op.results.length > 0 &&
4273
5133
  // NO EXEMPTION FOR A MATERIALIZED DEF HERE, and it is not missing. Such a def already renders
4274
5134
  // at its own position, as `v = f(…)`, which spells the effect as surely as a bare statement
@@ -4281,7 +5141,7 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
4281
5141
  !hasSpelledUse(op.results[0]) &&
4282
5142
  (opSig(op.opcode)?.reads !== true || volatileQualifiable(op));
4283
5143
 
4284
- const sideEffects = (b: Block): Stmt[] => {
5144
+ const sideEffects = (b: Block, atDef: ReadonlyMap<Op, Stmt[]> = NO_COPIES_AT_A_DEF): Stmt[] => {
4285
5145
  const out: Stmt[] = [];
4286
5146
  for (const op of b.ops) {
4287
5147
  if (op.opcode === 'store') {
@@ -4320,24 +5180,26 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
4320
5180
  }
4321
5181
  // signedness mirrors recoverTypes' store seed (word ⇒ signed, narrow ⇒ unsigned), so an
4322
5182
  // inserted cast declares the same scalar the recovered pointee would have.
4323
- out.push({ k: 'store', lval: lval0, value: intoPtrCell(lval0, expr(op.operands[1])) });
5183
+ const lval = pinnedAccess(op, lval0);
5184
+ out.push({ k: 'store', lval, value: intoPtrCell(lval, expr(op.operands[1])) });
4324
5185
  } else if (op.opcode === 'astore') {
4325
5186
  const elemSize = op.attrs.elemSize as number;
5187
+ const lval = arrayAccess(
5188
+ op.operands[0],
5189
+ expr(op.operands[0]),
5190
+ expr(op.operands[1]),
5191
+ op.attrs.fieldOff as number | undefined,
5192
+ op.attrs.memberOff as number | undefined,
5193
+ elemSize,
5194
+ // a member array's store spells through the member's OWN declaration, which the
5195
+ // recognizer records; every other astore keeps the width===4 convention
5196
+ (op.attrs.signed as boolean | undefined) ?? elemSize === 4,
5197
+ ctype,
5198
+ symCtx,
5199
+ );
4326
5200
  out.push({
4327
5201
  k: 'store',
4328
- lval: arrayAccess(
4329
- op.operands[0],
4330
- expr(op.operands[0]),
4331
- expr(op.operands[1]),
4332
- op.attrs.fieldOff as number | undefined,
4333
- op.attrs.memberOff as number | undefined,
4334
- elemSize,
4335
- // a member array's store spells through the member's OWN declaration, which the
4336
- // recognizer records; every other astore keeps the width===4 convention
4337
- (op.attrs.signed as boolean | undefined) ?? elemSize === 4,
4338
- ctype,
4339
- symCtx,
4340
- ),
5202
+ lval: pinnedAccess(op, lval),
4341
5203
  value: expr(op.operands[2]),
4342
5204
  });
4343
5205
  } else if (rendersAtOwnPosition(op)) {
@@ -4364,6 +5226,16 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
4364
5226
  out.push({ k: 'assign', name: a.name, value: expr(a.arg) });
4365
5227
  anchorsEmitted.add(a);
4366
5228
  }
5229
+ // a sunk pre-update exit copy, rebuilt at the op that computed its value (preUpdateCopies).
5230
+ // Reached for the same reason the anchored copies are: every key is an op with a RESULT, so
5231
+ // none of them is the `store`/`astore` this walk `continue`s out of above. A PARAMETER, where
5232
+ // the anchored copies read a module-level map: a sunk copy belongs to ONE loop's body, and a
5233
+ // map would let the generic `sideEffects(b)` calls emit it in an unrelated context — silently,
5234
+ // because `assertSunkCopiesPlaced` would see it placed.
5235
+ for (const st of atDef.get(op) ?? []) {
5236
+ out.push(st);
5237
+ sunkCopiesEmitted.add(st);
5238
+ }
4367
5239
  }
4368
5240
  return out;
4369
5241
  };
@@ -4380,7 +5252,7 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
4380
5252
  // to `header` is a conditional continue; its other edge, when it leaves the loop, is an early exit
4381
5253
  // (a `break` to `exit`, or an early `return` through a trampoline). Null outside any loop body —
4382
5254
  // the early-exit branch in structureBlock is inert there.
4383
- type LoopFrame = { header: Block; exit: Block; body: Set<Block>; arms: LoopArm[] };
5255
+ type LoopFrame = { header: Block; exit: Block; body: Set<Block>; arms: LoopArm[]; breaks: ReadonlySet<Block> };
4384
5256
  let loopCtx: LoopFrame | null = null;
4385
5257
  const withLoop = <R>(frame: LoopFrame, run: () => R): R => {
4386
5258
  const prev = loopCtx;
@@ -4392,6 +5264,88 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
4392
5264
  }
4393
5265
  };
4394
5266
 
5267
+ // THE IMPLICIT CONTINUE. A branch with one edge back to the loop header spells that edge as
5268
+ // nothing at all: control falls to the bottom of the body and the loop goes round. That holds only
5269
+ // where the region being structured ends at the loop bottom (`stop === frame.header`). An `if` in
5270
+ // the body whose join lies outside it, at the loop's exit, hands its arms that exit as `stop` and
5271
+ // renders the exit region after itself, still inside the body: an implicit continue there falls
5272
+ // into that region and the loop runs at most once. Both spellings of a branch that leaves the loop
5273
+ // beside such an edge consult this.
5274
+ const continueFallsToBottom = (frame: LoopFrame, stop: Block | null): boolean => stop === frame.header;
5275
+
5276
+ // THE BREAK RULE, for a `break` from block `from` out of `frame`'s body wherever the edge sits. A
5277
+ // `break` lands after the loop, where the header→exit copies render and then the exit region, raw.
5278
+ // Refused:
5279
+ // • a `do-while`: its exit copies live post-loop too, but are judged differently;
5280
+ // • the header→exit edge has copies the break cannot share: the break path runs them too, and
5281
+ // they would overwrite what the break carried;
5282
+ // • the exit region reads a loop value under a loop variable's name that `writes` holds —
5283
+ // the names this iteration has rewritten by the time the break leaves.
5284
+ //
5285
+ // A BREAK THAT CARRIES WHAT THE HEADER CARRIES shares them. When every argument the break edge
5286
+ // hands the exit is the very value the header's exit edge hands it, the break needs no copies of
5287
+ // its own: it leaves `bare` and falls into the header's copies after the loop, which compute the
5288
+ // same values. They compute them from the names they read, at the loop's end instead of at the
5289
+ // header, so those names must still hold what the header read: none may be one `writes` holds.
5290
+ // Without that clause a latch break spelled after its update runs the update twice —
5291
+ // `v = v + 1; break;`, then the header's copy `x = v + 1` after the loop. The clause is judged
5292
+ // over every argument the header hands the exit, at the value level, and not only over the copies
5293
+ // that render: an exit parameter that takes its argument's own name renders no copy, yet the exit
5294
+ // still reads that name, and a latch update that wrote it hands the exit the updated value.
5295
+ const breakRule = (frame: LoopFrame, from: Block, writes: Set<string>): { refusal: string } | { bare: boolean } => {
5296
+ // A `while (1)` has no copies of its own after the loop, so a break carries its own, computed
5297
+ // where it leaves; what is left is the exit region's re-derivations. An inner loop's value the
5298
+ // region reads under its name (`emitForever`) is no re-derivation.
5299
+ const fl = foreverLoops.get(frame.header);
5300
+ if (fl !== undefined) {
5301
+ const exitRegion = new Set([frame.exit, ...reachFrom(frame.exit)].filter((x) => !frame.body.has(x)));
5302
+ const { sub } = latchInnerSub(fl.header, fl.body, [...fl.breaks], exitRegion);
5303
+ return loopEscapeHazard(frame.body, sub, writes, exitRegion)
5304
+ ? { refusal: 'reaches an exit region that reads a loop value under a name this iteration already rewrote' }
5305
+ : { bare: false };
5306
+ }
5307
+ if (!whileLoops.has(frame.header)) {
5308
+ return { refusal: 'leaves a do-while' };
5309
+ }
5310
+ const headerCopies = argAssigns(frame.header, frame.exit);
5311
+ let bare = false;
5312
+ if (headerCopies.length !== 0) {
5313
+ const headerArgs = successorTo(frame.header, frame.exit)!.args;
5314
+ const breakArgs = successorTo(from, frame.exit)!.args;
5315
+ const reads = new Set<string>();
5316
+ for (const s of headerCopies) {
5317
+ stmtExprs(s).forEach((e) => exprVars(e, reads));
5318
+ }
5319
+ bare =
5320
+ breakArgs.every((v, k) => v === headerArgs[k]) &&
5321
+ ![...reads].some((n) => writes.has(n)) &&
5322
+ !headerArgs.some((a) => readsClobbered(a, new Map(), writes));
5323
+ if (!bare) {
5324
+ return { refusal: 'would run the copies the loop header hands its exit' };
5325
+ }
5326
+ }
5327
+ const exitRegion = new Set([frame.exit, ...reachFrom(frame.exit)].filter((x) => !frame.body.has(x)));
5328
+ if (loopEscapeHazard(frame.body, new Map(), writes, exitRegion)) {
5329
+ return { refusal: 'reaches an exit region that reads a loop value under a name this iteration already rewrote' };
5330
+ }
5331
+ return { bare };
5332
+ };
5333
+ // The loop-variable names an iteration has written on its way from the header to `b`, before any
5334
+ // update copy (`loopWriteSetAhead`).
5335
+ const writtenAhead = (frame: LoopFrame, b: Block): Set<string> => {
5336
+ const ahead = new Set<Block>([b]);
5337
+ for (const stack = [b]; stack.length;) {
5338
+ for (const p of preds.get(stack.pop()!) ?? []) {
5339
+ if (p !== frame.header && frame.body.has(p) && !ahead.has(p)) {
5340
+ ahead.add(p);
5341
+ stack.push(p);
5342
+ }
5343
+ }
5344
+ }
5345
+ const incoming = (x: Block) => [...inEdgeRecords(preds, x)].map(({ succ }) => succ.args);
5346
+ return loopWriteSetAhead(ahead, incoming, frame.header);
5347
+ };
5348
+
4395
5349
  // Branch-sense sites, numbered as the walk below first reaches them (`branchSenseFlipSites`).
4396
5350
  const senseOrdinal = new Map<number, number>();
4397
5351
 
@@ -4403,11 +5357,16 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
4403
5357
  if (b === stop) {
4404
5358
  return [];
4405
5359
  }
5360
+ // Back to a `while (1)` header from a region that ends short of the loop bottom: the edge's
5361
+ // copies are already out, and `continue` is the jump.
5362
+ if (loopCtx !== null && b === loopCtx.header && foreverLoops.has(b)) {
5363
+ return [{ k: 'continue' }];
5364
+ }
4406
5365
  if (onStack.has(b)) {
4407
5366
  throw new StructureError(
4408
5367
  `cannot structure '${fn.name}': unrecovered back-edge into block #${fn.blocks.indexOf(b)} ` +
4409
- `(loop-recovery declined this shape: multi-latch, irreducible/overlapping loops, ` +
4410
- `a conditional continue, or an unsafe break)`,
5368
+ `(loop-recovery declined this shape: irreducible/overlapping loops, exits to more than one ` +
5369
+ `live merge, a break out of a nested loop, or an unsafe break or continue)`,
4411
5370
  );
4412
5371
  }
4413
5372
  onStack.add(b);
@@ -4437,7 +5396,7 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
4437
5396
  isNamed: (v) => varName.has(v),
4438
5397
  isCmpOpcode: (opcode) => !!CMP_TO_BIN[opcode],
4439
5398
  switchAllowsNeqCase,
4440
- switchAllowsBoundCase,
5399
+ switchBoundCase: switchBoundCase ?? null,
4441
5400
  switchArmsFollowLayout,
4442
5401
  switchRequiresFrontLoadedTests,
4443
5402
  spellSwitchFallthrough,
@@ -4456,6 +5415,10 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
4456
5415
  if (dw && !dwActive.has(b)) {
4457
5416
  return emitDoWhile(dw, stop);
4458
5417
  }
5418
+ const forever = foreverLoops.get(b);
5419
+ if (forever && !dwActive.has(b)) {
5420
+ return emitForever(forever, stop);
5421
+ }
4459
5422
 
4460
5423
  const out: Stmt[] = [...sideEffects(b)];
4461
5424
  const term = b.ops[b.ops.length - 1];
@@ -4480,7 +5443,34 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
4480
5443
  // region flows into the NEXT arm is C fall-through (no `break`). Any other shape needs a `goto` and
4481
5444
  // fails LOUD rather than being duplicated or silently closed.
4482
5445
  if (term.opcode === 'switch_br') {
4483
- const merge = ipdom.get(b) ?? stop;
5446
+ // WHERE THE SWITCH ENDS. Post-dominance answers it whenever the arms reconverge. When they
5447
+ // do not — some arm `return`s, so EXIT is the only block on every path — the answer is the
5448
+ // one `followOverReturns` derives for a divergent `if`: keep the blocks that reach a `ret`
5449
+ // EVERY successor reaches, and post-dominate on what is left.
5450
+ //
5451
+ // UNCONDITIONAL, where the `if` asks it behind `followEarlyReturns` — and NOT because a jump
5452
+ // table has only one reading. It has the same two an `if` has, and `ipdom.get(b) ?? stop` is
5453
+ // the other one: for a table whose arms reach a shared `ret` and also a shared tail, that
5454
+ // reading structures each arm to its own end and emits legal, complete C with the tail
5455
+ // duplicated into the arms. Both are legal C for one CFG, so the asm underdetermines the
5456
+ // source — which is the definition of a ranked variation (docs/level-tower.md).
5457
+ //
5458
+ // WHAT MAKES THIS ONE THE DEFAULT is which of the two can decline. With a null merge the
5459
+ // default edge's target joins `siblings`, so an arm's plain `break` classifies as a
5460
+ // fall-through into it, and Regime B has no fallback recovery — `synthetic:sw_jtret` is that
5461
+ // shape and declines outright. A variation could not rescue it, and the reason is an
5462
+ // authoring rule rather than a blanket one: `assertDefaultAccepts` re-runs the default
5463
+ // structuring, and lets its refusal stand, for the options it RESETS — which is every
5464
+ // structure variation but the four named as non-members beside it. `followEarlyReturns`, the
5465
+ // `if`-side twin of this very mechanism, is one it resets, because moving a merge point
5466
+ // changes which edge copies elide as identities; a switch-side twin moves the same merge
5467
+ // point and would be reset for the same reason. So the divergent reading cannot come back as
5468
+ // a candidate that rescues `sw_jtret`. The reading that lifts strictly
5469
+ // more functions therefore has to BE the default, and it is the DIVERGENT reading that a
5470
+ // variation would have to offer back — a candidate nothing mints today, and the one thing
5471
+ // this line costs. `?? stop` keeps the shape where no `ret` is reachable from every
5472
+ // successor.
5473
+ const merge = ipdom.get(b) ?? followOverReturns(b, stop) ?? stop;
4484
5474
  const succ = term.successors;
4485
5475
  const caseVals = term.attrs.cases as number[];
4486
5476
  // Switch edges CARRY phi args (frontend/ssa.ts appends them terminator-generically) — each
@@ -4724,14 +5714,24 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
4724
5714
  // BEFORE the body ever ran — reading a temp uninitialized on the first test. The kept-guard
4725
5715
  // `do-while` tests after the body, so it carries no such restriction.
4726
5716
  const fused = guardProven && !li.header.ops.some((o) => materialize.has(o));
4727
- // A pre-update exit copy is repairable rather than fatal: emitted at the TOP of the body it
4728
- // captures the value one iteration before the update, which is what the exit edge carries.
5717
+ // A pre-update exit copy is repairable rather than fatal: emitted INSIDE the body, ahead of
5718
+ // the update, it captures the value the exit edge carries rather than the updated one.
4729
5719
  // Its post-loop copy is then dropped, so the ZERO-TRIP path needs the guard→exit edge as a
4730
5720
  // seed — the only edge holding the never-entered value. That is why the sink demands the
4731
5721
  // proof above: it makes the fused zero-trip path load-bearing.
4732
5722
  const sunk = guardProven
4733
- ? sinkablePreUpdateSlots(li.header, li.exit, hexitArgs, new Set([li.header]), sub, updateWrites)
4734
- : new Set<number>();
5723
+ ? sinkablePreUpdateSlots(
5724
+ li.header,
5725
+ li.exit,
5726
+ hexitArgs,
5727
+ new Set([li.header]),
5728
+ li.header,
5729
+ sub,
5730
+ updateWrites,
5731
+ hooks.preUpdateSinkGates ?? PREUPDATE_SINK_GATES,
5732
+ (j) => !sameAtEntry(hexitArgs[j], guardExit.args[j] as Value, entryVals),
5733
+ )
5734
+ : new Map<number, Op | null>();
4735
5735
  // (2) Every exit copy the fused form KEEPS renders after the loop, on the zero-trip path
4736
5736
  // too — where the loop variables still hold their init values. It must therefore produce
4737
5737
  // what the guard→exit edge carries, since that edge is dropped. A sunk slot is exempt: its
@@ -4757,6 +5757,12 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
4757
5757
  null,
4758
5758
  )
4759
5759
  ) {
5760
+ // NO `n++` REPAIR HERE, and the SHAPE is why rather than an oversight. The fold
5761
+ // (`preUpdateCondFold`) trades the update copy at the foot of the body for a `++` at the
5762
+ // leaf, which is the same program only for a BOTTOM test: it runs once per iteration, in
5763
+ // the position the copy stood. This test renders at the TOP, ahead of the body, and runs
5764
+ // one more time than the body does — `while (n++ < k)` over a body with no update is a
5765
+ // different loop. Reaching this site is a rotation, not a wider gate.
4760
5766
  throw new StructureError(
4761
5767
  `cannot structure '${fn.name}': loop condition or a post-loop value reads a pre-update loop variable`,
4762
5768
  );
@@ -4867,13 +5873,68 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
4867
5873
  } // entering the loop must be `taken`
4868
5874
  out.push(mkIf(gcond, [loopStmt], []));
4869
5875
  }
5876
+ // A POST-LOOP READ THAT HOLDS THE INIT INSTEAD. The exit copies and the exit region render
5877
+ // under the un-rotation substitution, which spells a back-edge arg as its loop variable's
5878
+ // name. Both run on the zero-trip path too, where that name still holds its init — so an arg
5879
+ // computed BEFORE the loop reads right there only if the init is that very value (`sub` is
5880
+ // last-wins over params sharing an arg, so the init is read off the last one). `staleExit`
5881
+ // proves a copy through `sameAtEntry`, which answers true for one value on both exit edges
5882
+ // without looking inside it — so such a copy is judged here through its inlined expression,
5883
+ // and the region wherever its render hits such an entry (`zeroTripStale`).
5884
+ //
5885
+ // KNOWN GAP, wrong before this check existed too: two DIFFERENT exit values sharing a subtree
5886
+ // that reaches a stale entry pass `staleExit` for the same reason, and so do two calls whose
5887
+ // operands map to each other through the entry values.
5888
+ const stale = new Map(
5889
+ [...sub].filter(([x]) => {
5890
+ const d = defs.get(x);
5891
+ return (
5892
+ !li.header.params.includes(x) &&
5893
+ (d === undefined || opBlock.get(d) !== li.header) &&
5894
+ initArgs[li.backArgOfParam.lastIndexOf(x)] !== x
5895
+ );
5896
+ }),
5897
+ );
5898
+ const holdsInitInstead = (root: Value): boolean => {
5899
+ const seen = new Set<Value>();
5900
+ const walk = (x: Value): boolean => {
5901
+ if (seen.has(x)) {
5902
+ return false;
5903
+ }
5904
+ seen.add(x);
5905
+ if (sub.has(x)) {
5906
+ return stale.has(x);
5907
+ }
5908
+ if (varName.has(x)) {
5909
+ return false;
5910
+ }
5911
+ const d = defs.get(x);
5912
+ return d !== undefined && d.operands.some(walk);
5913
+ };
5914
+ return walk(root);
5915
+ };
5916
+ if (hexitArgs.some((a, j) => keptSlot(j) && a === guardExit.args[j] && holdsInitInstead(a))) {
5917
+ throw new StructureError(
5918
+ `cannot structure '${fn.name}': a read after the loop spells a value computed before it by a ` +
5919
+ `loop variable's name, which holds its initial value on a zero-trip run`,
5920
+ );
5921
+ }
4870
5922
  // The header→exit edge may carry non-identity phi args (the exit param merges the guard-false
4871
5923
  // value with the loop's final value). Emit those copies after the loop — dropping them returns
4872
5924
  // a stale value. Read under the un-rotation substitution (post-loop the params hold their
4873
5925
  // updated values), and structure the exit region under the same substitution so a post-loop
4874
5926
  // use of a loop value reads its name.
4875
5927
  out.push(
4876
- ...withSub(sub, () => [...argAssigns(li.header, li.exit, sub, keptSlot), ...structureRegion(li.exit, stop)]),
5928
+ ...withSub(sub, () => {
5929
+ const copies = argAssigns(li.header, li.exit, sub, keptSlot);
5930
+ const prevStale = zeroTripStale;
5931
+ zeroTripStale = prevStale ? new Map([...prevStale, ...stale]) : stale;
5932
+ try {
5933
+ return [...copies, ...structureRegion(li.exit, stop)];
5934
+ } finally {
5935
+ zeroTripStale = prevStale;
5936
+ }
5937
+ }),
4877
5938
  );
4878
5939
  return out;
4879
5940
  }
@@ -4892,19 +5953,13 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
4892
5953
  // back-edge arm is the implicit continue (control falls to the loop bottom). Guarded to leaving
4893
5954
  // edges only (`!body.has(exitB)` AND a break/return target); an in-body conditional continue still
4894
5955
  // declines (falls through → the header re-entry trips `onStack`, an honest loud fail).
4895
- if (loopCtx && (takenB === loopCtx.header || fallB === loopCtx.header)) {
5956
+ //
5957
+ // Taken only where the implicit continue holds (`continueFallsToBottom`). Where it does not, a
5958
+ // break edge is refused by name below, and an arm re-enters the header and declines loud.
5959
+ if (loopCtx && continueFallsToBottom(loopCtx, stop) && (takenB === loopCtx.header || fallB === loopCtx.header)) {
4896
5960
  const contIsTaken = takenB === loopCtx.header;
4897
5961
  const exitB = contIsTaken ? fallB : takenB;
4898
5962
  const isBreak = exitB === loopCtx.exit;
4899
- // SOUNDNESS (break-clobber): a structured `break` jumps to AFTER the loop, where the header→exit
4900
- // phi copies are emitted (emitTestAtTopWhile / emitDoWhile). If those copies are NON-identity, the
4901
- // break path would fall through them and CLOBBER the value the break carried into the exit param.
4902
- // Only emit `break` for a WHILE header whose header→exit copy is empty (the exit param already
4903
- // coalesces across both edges); otherwise decline (fall through → honest loud fail). A do-while
4904
- // `break` declines here (its exit copies live post-loop too, but the check differs) — the
4905
- // return-trampoline path below still serves both. Trampolines are immune (the `return` terminates
4906
- // the arm, so nothing falls through).
4907
- const breakSafe = whileLoops.has(loopCtx.header) && argAssigns(loopCtx.header, loopCtx.exit).length === 0;
4908
5963
  // The back-edge substitution: each header param's back-edge arg → the param's name, so a test that
4909
5964
  // reads a POST-update value (the value carried to the header) shows the header var name.
4910
5965
  const sub = subFor(loopCtx.header.params, successorTo(b, loopCtx.header)!.args);
@@ -4928,21 +5983,75 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
4928
5983
  // `if (found) { *out = i; return; }` would store `i + 1`. Handing the arm's own region makes
4929
5984
  // the escape check judge exactly those reads.
4930
5985
  const exitRegion = new Set([exitB, ...reachFrom(exitB)].filter((x) => !loopCtx!.body.has(x)));
5986
+ // A `break` is judged by the break rule, with the update among the writes ahead of it: the
5987
+ // exit region renders raw after the loop, so a header value re-derived there from a name the
5988
+ // update wrote is computed once more — `do { b = t / (b + t); if (c > 2) break; } while
5989
+ // (--d > 0); return b;` renders `return t / (v + t)` after `v = t / (v + t)` already ran. An
5990
+ // early `return` needs none of it: its `return` ends the arm, so nothing after the loop runs.
4931
5991
  const hazard = loopUpdateHazard(term.operands[0], exitArgs, loopCtx.body, sub, updateWrites, exitRegion);
4932
- if (!hazard && !loopCtx.body.has(exitB) && ((isBreak && breakSafe) || (!isBreak && isArm(exitB)))) {
5992
+ const rule = isBreak ? breakRule(loopCtx, b, new Set([...updateWrites, ...writtenAhead(loopCtx, b)])) : null;
5993
+ if (!hazard && !loopCtx.body.has(exitB) && (rule ? 'bare' in rule : isArm(exitB))) {
4933
5994
  out.push(...updateCopies); // the loop update, RAW (i++, p>>=1, …)
4934
5995
  let leaveCond = exprWith(sub)(term.operands[0]);
4935
5996
  if (contIsTaken) {
4936
5997
  leaveCond = negateCond(leaveCond);
4937
5998
  } // continue is `taken` → leave when NOT it
4938
- const exitArm = isBreak
4939
- ? [...argAssigns(b, loopCtx.exit, sub), { k: 'break' } as Stmt] // break to the loop exit
4940
- : withSub(sub, () => [...argAssigns(b, exitB, sub), ...structureRegion(exitB, stop)]); // early return
5999
+ const exitArm =
6000
+ rule && 'bare' in rule
6001
+ ? [...(rule.bare ? [] : argAssigns(b, loopCtx.exit, sub)), { k: 'break' } as Stmt] // break to the loop exit
6002
+ : withSub(sub, () => [...argAssigns(b, exitB, sub), ...structureRegion(exitB, stop)]); // early return
4941
6003
  out.push(mkIf(leaveCond, exitArm, []));
4942
6004
  return out;
4943
6005
  }
4944
6006
  }
4945
6007
 
6008
+ // A `break` from INSIDE the body: one edge of this cond_br is an admitted break edge (it lands on
6009
+ // the `while`'s own exit). Spelled `if (c) { <edge copies>; break; }` and then what the other edge
6010
+ // runs, which is what the asm runs: the exit region renders once, after the loop, for the
6011
+ // header's exit and this edge alike. The break leaves before any update copy on this path, so
6012
+ // the loop variables still hold what the header read, unless a block between the header and here
6013
+ // wrote one of their names.
6014
+ //
6015
+ // The other edge may be the BACK edge, where the latch path above refused (its update-first
6016
+ // spelling would let the test or the exit read a value the update overwrites). The update copies
6017
+ // then follow the `if`, where the break has already left — the same C as when the update sits in
6018
+ // a block of its own, which is a layout accident.
6019
+ //
6020
+ // Refused: by the break rule over the names written ahead of this edge; where the other edge
6021
+ // leaves the body too; and where the other edge is the back edge but the implicit continue does
6022
+ // not hold (`continueFallsToBottom`) — every route to the header re-enters it, so that refusal is
6023
+ // always loud. The others are loud, since the loop was admitted on the promise of this spelling,
6024
+ // unless an arm owns the edge or the exit ends in a `ret`: then if-recovery below copies the
6025
+ // exit's tail into the arm and returns.
6026
+ // A `switch` case body never holds one: the exit is a block no switch in the body dominates,
6027
+ // and both switch regimes refuse an arm that reaches such a block (`analyzeArmExit`).
6028
+ if (loopCtx && loopCtx.breaks.has(b)) {
6029
+ const frame = loopCtx;
6030
+ const breakIsTaken = takenB === frame.exit;
6031
+ const stayB = breakIsTaken ? fallB : takenB;
6032
+ const continueRefused = stayB === frame.header && !continueFallsToBottom(frame, stop);
6033
+ const rule = continueRefused
6034
+ ? { refusal: 'would continue the loop from a region that ends past the loop bottom' }
6035
+ : frame.body.has(stayB)
6036
+ ? breakRule(frame, b, writtenAhead(frame, b))
6037
+ : { refusal: 'has no edge beside it that stays in the loop' };
6038
+ if ('bare' in rule) {
6039
+ let leaveCond = expr(term.operands[0]);
6040
+ if (!breakIsTaken) {
6041
+ leaveCond = negateCond(leaveCond);
6042
+ }
6043
+ const [breakEdge, stayEdge] = breakIsTaken ? term.successors : [term.successors[1], term.successors[0]];
6044
+ out.push(mkIf(leaveCond, [...(rule.bare ? [] : argAssignsFor(b, breakEdge)), { k: 'break' }], []));
6045
+ out.push(...argAssignsFor(b, stayEdge), ...structureRegion(stayB, stop));
6046
+ return out;
6047
+ }
6048
+ if (continueRefused || (!isArm(frame.exit) && !isRet(frame.exit))) {
6049
+ throw new StructureError(
6050
+ `cannot structure '${fn.name}': a break out of block #${fn.blocks.indexOf(b)} ${rule.refusal}`,
6051
+ );
6052
+ }
6053
+ }
6054
+
4946
6055
  // Regime-A switch: if this cond_br roots a comparison tree over a single scrutinee, emit a
4947
6056
  // `switch`. A pre-check here — mirroring the guard-fused-loop check above — so it sees the raw
4948
6057
  // tree before if-recovery claims the diamonds. Declines (null) fall through to plain if-recovery.
@@ -4979,7 +6088,12 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
4979
6088
  !loopCtx.body.has(ipd) &&
4980
6089
  term.successors.some((sc) => isArm(sc.block) || sc.block === stop) &&
4981
6090
  term.successors.every((sc) => loopCtx!.body.has(sc.block) || isArm(sc.block));
4982
- const merge = clampToLoop ? stop : (ipd ?? stop);
6091
+ // In a `while (1)` body the join is the body's own, which the CFG's post-dominance cannot see
6092
+ // (`foreverJoin`): a branch whose one arm continues is post-dominated, in the CFG, by wherever
6093
+ // the other arm leaves the loop from.
6094
+ const inForever = loopCtx === null ? undefined : foreverLoops.get(loopCtx.header);
6095
+ const bodyJoin = inForever === undefined ? undefined : foreverJoin(inForever, b);
6096
+ const merge = bodyJoin !== undefined ? (bodyJoin ?? stop) : clampToLoop ? stop : (ipd ?? stop);
4983
6097
  // Per-successor records, NOT successorTo(b, block): a cond_br whose two edges reach the SAME
4984
6098
  // block with different args would otherwise give both arms the first edge's copies.
4985
6099
  const thenS = [...argAssignsFor(b, term.successors[0]), ...structureRegion(takenB, merge)];
@@ -5088,7 +6202,14 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
5088
6202
  negateHere = branchSenseFlipSites?.has(ord) ? !siteDefault : siteDefault;
5089
6203
  hooks.onBranchSenseSite?.({ block: bi, ordinal: ord, joined: ipd !== null, negated: negateHere });
5090
6204
  }
5091
- out.push(negateHere ? { k: 'if', cond: negateCond(cond), then: elseS, else: thenS } : mkIf(cond, thenS, elseS));
6205
+ if (
6206
+ stmtsEqual(thenS, elseS) &&
6207
+ testRereadsOnly(term, b, { defs, useSitesOf, dom, ipdom, loops: forest.byHeader.values() })
6208
+ ) {
6209
+ out.push(...thenS);
6210
+ } else {
6211
+ out.push(negateHere ? { k: 'if', cond: negateCond(cond), then: elseS, else: thenS } : mkIf(cond, thenS, elseS));
6212
+ }
5092
6213
  if (merge && merge !== stop) {
5093
6214
  out.push(...structureRegion(merge, stop));
5094
6215
  }
@@ -5105,10 +6226,13 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
5105
6226
  };
5106
6227
  const loopSub = (li: LoopInfo): Map<Value, string> => subFor(li.header.params, li.backArgOfParam);
5107
6228
 
5108
- // The sunk exit copies, as body statements: `dest = <the arg, rebuilt here>`. The sink's gates
5109
- // are stated against `exprWith(null)` — every name it stops at holds, at the top of the body,
5110
- // what it held where the edge read it — so the arg is spelled with no substitution. Slot order
5111
- // keeps it deterministic.
6229
+ // The sunk exit copies, as body statements: `dest = <the arg, rebuilt here>`, SPLIT by where in
6230
+ // the body each one belongs. WHERE is not recomputed here: `sinkablePreUpdateSlots` cleared each
6231
+ // slot at one position and hands it back, so `atDef` keys the ones homed at a latch op by that op,
6232
+ // for that block's `sideEffects` walk, and `leading` opens the body with the rest. The sink's gates
6233
+ // are stated against `exprWith(null)` — every name the walk stops at holds, anywhere in the body
6234
+ // ahead of the update, what it held where the edge read it — so the arg is spelled with no
6235
+ // substitution. Slot order keeps it deterministic.
5112
6236
  //
5113
6237
  // NOT `expr`: an ambient `activeSub` is an ENCLOSING loop's post-loop naming, which the sink's
5114
6238
  // walk does not model, so a REBUILT tree here would be spelled under names that substitution has
@@ -5119,27 +6243,63 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
5119
6243
  // The destination name is read as an invariant, not checked: every block param carries one.
5120
6244
  // `assertResolved` is what catches a widening that breaks that.
5121
6245
  //
5122
- // `home` is the loop HEADER — the block whose body these copies open, and so the block from which
5123
- // the write is reachable. Recorded rather than inferred because `exit` says the opposite (see
5124
- // `sunkCopyHomes`).
5125
- const preUpdateCopies = (exit: Block, exitArgs: readonly Value[], sunk: Set<number>, home: Block): Stmt[] =>
5126
- [...sunk]
5127
- .sort((x, y) => x - y)
5128
- .map((j) => {
5129
- if (activeSub !== null && !varName.has(exitArgs[j])) {
5130
- throw new StructureError(
5131
- `cannot structure '${fn.name}': a pre-update exit copy would rebuild a computed value ` +
5132
- `inside a loop nested in another loop's post-loop naming`,
5133
- );
5134
- }
5135
- const name = varName.get(exit.params[j])!;
5136
- sunkCopyHomes.push({ name, home });
5137
- return {
5138
- k: 'assign' as const,
5139
- name,
5140
- value: exprWith(null)(exitArgs[j]),
5141
- };
5142
- });
6246
+ // `home` is the loop HEADER, which dominates every point inside the body a copy is rebuilt at, and
6247
+ // so the block from which every one of these writes is reachable. Recorded rather than inferred
6248
+ // because `exit` says the opposite (see `sunkCopyHomes`).
6249
+ const preUpdateCopies = (
6250
+ exit: Block,
6251
+ exitArgs: readonly Value[],
6252
+ sunk: ReadonlyMap<number, Op | null>,
6253
+ home: Block,
6254
+ ): SunkCopies => {
6255
+ const out: SunkCopies = { leading: [], atDef: new Map() };
6256
+ for (const [j, at] of [...sunk].sort(([x], [y]) => x - y)) {
6257
+ if (activeSub !== null && !varName.has(exitArgs[j])) {
6258
+ throw new StructureError(
6259
+ `cannot structure '${fn.name}': a pre-update exit copy would rebuild a computed value ` +
6260
+ `inside a loop nested in another loop's post-loop naming`,
6261
+ );
6262
+ }
6263
+ const name = varName.get(exit.params[j])!;
6264
+ sunkCopyHomes.push({ name, home });
6265
+ // A value the body already names under the destination writes it where it is computed, so
6266
+ // its copy would be `dest = dest`: the slot needs only the seed and the dropped post-loop copy.
6267
+ if (varName.get(exitArgs[j]) === name) {
6268
+ continue;
6269
+ }
6270
+ const st = { k: 'assign' as const, name, value: exprWith(null)(exitArgs[j]) };
6271
+ if (at === null) {
6272
+ out.leading.push(st);
6273
+ } else {
6274
+ (out.atDef.get(at) ?? out.atDef.set(at, []).get(at)!).push(st);
6275
+ }
6276
+ }
6277
+ return out;
6278
+ };
6279
+
6280
+ /** Every copy `preUpdateCopies` placed at a def, once the body that was meant to hold it is
6281
+ * assembled. FORWARD DEFENCE WITH NO REACH TODAY: both emitters hand `atDef` to `sideEffects` on
6282
+ * the very block the gate homed those copies in, and that walk visits every op the block holds,
6283
+ * so no key can go unvisited. It is stated rather than assumed because what it names is silent —
6284
+ * a body assembled without that block's side effects, or handing the copies to another block's
6285
+ * walk, drops a copy, and a dropped copy is a wrong value rather than a gap.
6286
+ *
6287
+ * STRICT, where the anchored-copy obligation below tests OBSERVABILITY instead, and the
6288
+ * difference is which position is guaranteed. An anchor's position is a const's def block, which
6289
+ * may legitimately render no statement at all — a loop preheader the guarded-loop emitter never
6290
+ * structures is one — so a copy that vanishes there can still leave a consistent program. A sunk
6291
+ * copy's position is in a block both emitters render whatever else they do, because that block's
6292
+ * side effects carry the loop's stores; a copy missing from it is a mistake in assembling the
6293
+ * body and has no second reading. */
6294
+ const assertSunkCopiesPlaced = (copies: SunkCopies): void => {
6295
+ for (const st of [...copies.atDef.values()].flat()) {
6296
+ if (!sunkCopiesEmitted.has(st)) {
6297
+ throw new StructureError(
6298
+ `cannot structure '${fn.name}': a pre-update exit copy has no position in the loop body it belongs to`,
6299
+ );
6300
+ }
6301
+ }
6302
+ };
5143
6303
 
5144
6304
  // A guarded self-loop's body and latch test. The test reads the header's own params (back-edge
5145
6305
  // args substituted back), and the body is any sunk trailing copies, then the header's SIDE
@@ -5154,16 +6314,21 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
5154
6314
  // source spelling means.
5155
6315
  const emitWhile = (
5156
6316
  li: LoopInfo,
5157
- updates?: Stmt[],
5158
- sunkCopies: Stmt[] = [],
5159
- kind: 'while' | 'dowhile' = 'while',
6317
+ updates: Stmt[] | undefined,
6318
+ sunkCopies: SunkCopies,
6319
+ kind: 'while' | 'dowhile',
5160
6320
  ): Stmt => {
5161
6321
  const term = li.header.ops[li.header.ops.length - 1];
5162
6322
  let cond = exprWith(loopSub(li))(term.operands[0]);
5163
6323
  if (term.successors[0].block !== li.header) {
5164
6324
  cond = negateCond(cond);
5165
6325
  } // loop-continue must be `taken`
5166
- const body = [...sunkCopies, ...sideEffects(li.header), ...(updates ?? argAssigns(li.header, li.header))];
6326
+ const body = [
6327
+ ...sunkCopies.leading,
6328
+ ...sideEffects(li.header, sunkCopies.atDef),
6329
+ ...(updates ?? argAssigns(li.header, li.header)),
6330
+ ];
6331
+ assertSunkCopiesPlaced(sunkCopies);
5167
6332
  return kind === 'while' ? { k: 'while', cond, body } : { k: 'dowhile', cond, body };
5168
6333
  };
5169
6334
 
@@ -5182,7 +6347,7 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
5182
6347
  // Mirror the br/cond_br cases: argAssigns then structureRegion. Structure the body under this
5183
6348
  // loop's frame so an in-body conditional exit (break / early return) is recognised instead of
5184
6349
  // tripping the header-re-entry `onStack` guard.
5185
- const body = withLoop({ header: wl.header, exit: wl.exit, body: wl.body, arms: wl.arms }, () => [
6350
+ const body = withLoop({ header: wl.header, exit: wl.exit, body: wl.body, arms: wl.arms, breaks: wl.breaks }, () => [
5186
6351
  ...argAssigns(wl.header, wl.bodyEntry),
5187
6352
  ...structureRegion(wl.bodyEntry, wl.header),
5188
6353
  ]);
@@ -5192,6 +6357,105 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
5192
6357
  return out;
5193
6358
  };
5194
6359
 
6360
+ // THE JOIN OF A BRANCH IN A `while (1)` BODY, over the body with its back edges and exits deleted.
6361
+ // A block that only jumps back to the header or out of the loop, rendering nothing but that edge's
6362
+ // copies, ENDS its path — a `continue`, a `break` — so it constrains no join; the join of `b` is
6363
+ // where the paths that go on meet: its nearest post-dominator over the rest. Null when none goes
6364
+ // on, and the region's own `stop` ends it. Without the deletion, a branch whose one arm continues
6365
+ // joins at the loop bottom, and whatever the other arm runs before its own continue is copied into
6366
+ // both arms. A block that ends every path but TESTS first is still where its predecessors meet,
6367
+ // and so is a latch two body paths reach: the foot of `for (…) { if (c) {…} }`, where deleting it
6368
+ // moves the `if`'s join into one arm, which then copies the latch and adds a `continue` the
6369
+ // source never wrote. A break block two paths share stays an end, each path its own `break`.
6370
+ const foreverJoins = new Map<Block, Map<Block, Block | null>>();
6371
+ const foreverJoin = (fl: ForeverLoopInfo, b: Block): Block | null => {
6372
+ let ipdom = foreverJoins.get(fl.header);
6373
+ if (ipdom === undefined) {
6374
+ const inBody = (n: Block): Block[] =>
6375
+ [...new Set(successorsOf(n))].filter((x) => fl.body.has(x) && x !== fl.header);
6376
+ const ends = (n: Block): boolean =>
6377
+ inBody(n).length === 0 &&
6378
+ n.ops[n.ops.length - 1].opcode === 'br' &&
6379
+ n.ops.every((op) => !effectful(op) && !materialize.has(op)) &&
6380
+ !(successorsOf(n)[0] === fl.header && new Set(preds.get(n)?.filter((q) => fl.body.has(q))).size > 1);
6381
+ ipdom = postDominators(fn, fl.body, (n) => inBody(n).filter((x) => !ends(x)));
6382
+ foreverJoins.set(fl.header, ipdom);
6383
+ }
6384
+ return ipdom.get(b) ?? null;
6385
+ };
6386
+
6387
+ // Does rendering `root` under `map` re-derive one of `targets`: reach it through the def tree
6388
+ // without passing a mapped or a named value, either of which renders as a name?
6389
+ const rendersThrough = (
6390
+ root: Value,
6391
+ map: ReadonlyMap<Value, string> | null,
6392
+ targets: ReadonlySet<Value>,
6393
+ ): boolean => {
6394
+ const seen = new Set<Value>();
6395
+ const walk = (x: Value): boolean => {
6396
+ if (seen.has(x) || map?.has(x) === true || varName.has(x)) {
6397
+ return false;
6398
+ }
6399
+ if (targets.has(x)) {
6400
+ return true;
6401
+ }
6402
+ seen.add(x);
6403
+ return defs.get(x)?.operands.some(walk) ?? false;
6404
+ };
6405
+ return walk(root);
6406
+ };
6407
+ // What a loop's exit region renders after the loop, before `stop`, and the values it reads.
6408
+ const exitRegion = (exit: Block, loopBody: ReadonlySet<Block>, stop: Block | null): Set<Block> => {
6409
+ const region = new Set<Block>();
6410
+ for (const work = [exit]; work.length > 0;) {
6411
+ const x = work.pop()!;
6412
+ if (x !== stop && !loopBody.has(x) && !region.has(x)) {
6413
+ region.add(x);
6414
+ work.push(...successorsOf(x));
6415
+ }
6416
+ }
6417
+ return region;
6418
+ };
6419
+ const rootsOf = (region: Iterable<Block>): Value[] =>
6420
+ [...region].flatMap((x) => x.ops.flatMap((op) => [...op.operands, ...op.successors.flatMap((sc) => sc.args)]));
6421
+
6422
+ // A `break` of the loop whose body `ss` is — not of a loop or `switch` inside it.
6423
+ const breaksOut = (ss: Stmt[]): boolean =>
6424
+ ss.some((s) => s.k === 'break' || (!isLoop(s) && s.k !== 'switch' && breaksOut(stmtChildren(s))));
6425
+ // `while (1) { body }`: the header structured as an ordinary block up to itself (its do-while-style
6426
+ // hook masked via dwActive), under a frame whose breaks leave for `exit`, which renders after it.
6427
+ // Where no edge out rendered as a `break` — each one an early `return`, the exit's tail copied
6428
+ // into it — nothing reaches the end of the loop, and the exit rendered after it would be a dead
6429
+ // second copy of that tail.
6430
+ //
6431
+ // The exit region runs after every break, so an inner loop that runs before each of them has left
6432
+ // its last value in its variable's name, and the region reads it there (`latchInnerSub`, with the
6433
+ // breaks as its readers). Read raw it would re-derive that value from the name that holds it. Where
6434
+ // neither reading is the value, decline LOUD, as the do-while latch does.
6435
+ const emitForever = (fl: ForeverLoopInfo, stop: Block | null): Stmt[] => {
6436
+ dwActive.add(fl.header);
6437
+ const body = withLoop({ header: fl.header, exit: fl.exit, body: fl.body, arms: fl.arms, breaks: fl.breaks }, () =>
6438
+ structureBlock(fl.header, fl.header),
6439
+ );
6440
+ dwActive.delete(fl.header);
6441
+ const loop: Stmt = { k: 'while', cond: { k: 'const', value: 1 }, body };
6442
+ if (!breaksOut(body)) {
6443
+ return [loop];
6444
+ }
6445
+ const region = exitRegion(fl.exit, fl.body, stop);
6446
+ const { sub: innerSub, unreadable } = latchInnerSub(fl.header, fl.body, [...fl.breaks], region);
6447
+ if (unreadable.size > 0 && rootsOf(region).some((r) => rendersThrough(r, innerSub, unreadable))) {
6448
+ throw new StructureError(
6449
+ `cannot structure '${fn.name}': the code after a while (1) reads an inner loop's value whose name was ` +
6450
+ `rewritten after the inner loop, and re-deriving it reads a name the inner loop wrote`,
6451
+ );
6452
+ }
6453
+ return [
6454
+ loop,
6455
+ ...(innerSub.size > 0 ? withSub(innerSub, () => structureRegion(fl.exit, stop)) : structureRegion(fl.exit, stop)),
6456
+ ];
6457
+ };
6458
+
5195
6459
  // The latch back-edge substitution (do-while) — subFor over the latch's back-edge args.
5196
6460
  const latchSub = (dw: DoWhileInfo): Map<Value, string> =>
5197
6461
  subFor(dw.header.params, successorTo(dw.latch, dw.header)!.args);
@@ -5262,16 +6526,24 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
5262
6526
  const ins = [...inEdgeRecords(preds, b)];
5263
6527
  return ins.length > 0 && ins.every(({ succ }) => aliasOf(succ.args[k], v, seen));
5264
6528
  };
6529
+ // `readers` are the blocks the substitution is read after: the latch, or every block a `while (1)`
6530
+ // breaks from, whose exit region renders after the loop. `after` adds the blocks of that region,
6531
+ // whose writes land before some of its reads.
5265
6532
  const latchInnerSub = (
5266
- dw: DoWhileInfo,
6533
+ header: Block,
6534
+ loopBody: ReadonlySet<Block>,
6535
+ readers: Block[],
6536
+ after: ReadonlySet<Block> = new Set(),
5267
6537
  ): { sub: Map<Value, string>; unreadable: Set<Value>; writtenAfter: Map<Value, Set<string>> } => {
5268
6538
  const out = new Map<Value, string>();
5269
6539
  const refused = new Set<Value>();
5270
6540
  const writtenAfter = new Map<Value, Set<string>>();
5271
- const latchDoms = dom.get(dw.latch)!;
5272
6541
  const kids = [...forest.byHeader.values()]
5273
6542
  .filter(
5274
- (l) => l.header !== dw.header && dw.body.has(l.header) && !l.body.has(dw.latch) && latchDoms.has(l.header),
6543
+ (l) =>
6544
+ l.header !== header &&
6545
+ loopBody.has(l.header) &&
6546
+ readers.every((r) => !l.body.has(r) && dom.get(r)!.has(l.header)),
5275
6547
  )
5276
6548
  .sort((x, y) => dom.get(x.header)!.size - dom.get(y.header)!.size);
5277
6549
  for (const l of kids) {
@@ -5286,7 +6558,7 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
5286
6558
  for (const [v, n] of varName) {
5287
6559
  const d = defs.get(v);
5288
6560
  const home = paramBlock.get(v) ?? (d !== undefined && materialize.has(d) ? opBlock.get(d) : undefined);
5289
- if (home === undefined || !dw.body.has(home) || home === dw.header) {
6561
+ if (home === undefined || !(loopBody.has(home) || after.has(home)) || home === header) {
5290
6562
  continue;
5291
6563
  }
5292
6564
  written.add(n);
@@ -5332,30 +6604,28 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
5332
6604
  // then every line below spells what it did before the substitution existed — unless an unnamed
5333
6605
  // value an inner loop computed could reach the latch.
5334
6606
  //
5335
- // The update copies take it MERGED with `activeSub`, because a map passed to `argAssigns`
5336
- // replaces the ambient `expr` it would otherwise render with: without the merge, a copy reading
5337
- // an ENCLOSING loop's post-loop value would re-derive it (`ACTIVE_SUB` in
5338
- // `latch-inner-sub.test.ts`). One thing a map changes that `expr` does not: identity elision
5339
- // consults it, so a copy that `activeSub` spells `n = n` is dropped rather than written. The
5340
- // two programs are the same, and it is left conditional so an empty `innerSub` keeps the line
5341
- // exactly as it was (the corpus census is byte-identical either side of this commit's parent).
5342
- const { sub: innerSub, unreadable, writtenAfter } = latchInnerSub(dw);
6607
+ // The update copies take it MERGED with `activeSub`. A map extends the ambient naming where it
6608
+ // renders (`exprWith`), but identity elision consults the map alone, so the merge is what drops
6609
+ // a copy that `activeSub` spells `n = n` rather than writing it. The two programs are the same,
6610
+ // and it is left conditional so an empty `innerSub` takes the map-less path.
6611
+ const { sub: innerSub, unreadable, writtenAfter } = latchInnerSub(dw.header, dw.body, [dw.latch]);
5343
6612
  const latchMap = innerSub.size > 0 ? new Map([...(activeSub ?? []), ...innerSub]) : null;
5344
6613
  const updates = argAssigns(dw.latch, dw.header, latchMap);
5345
6614
  const updateWrites = loopWriteSet(updates, dw.body, dw.header);
5346
6615
  const lterm = dw.latch.ops[dw.latch.ops.length - 1];
5347
- // KNOWN GAP, and the reason the sink stands down rather than repairing anything. A body
5348
- // block's param may adopt a LOOP VARIABLE's name (canTakeName waives the liveness half for a
5349
- // pure alias, an argument that does not carry when the name came from `backArgName` — it is
5350
- // then a different value's). The arm's copy into it is a real write partway through the body,
5351
- // and everything rendered after it reads the name RAW: the update, the bottom test, the
5352
- // header's own ops, the latch's side effects.
6616
+ // A DEFENSIVE GUARD, and the reason the sink stands down rather than repairing anything. A body
6617
+ // block's param that holds a LOOP VARIABLE's name makes the arm's copy into it a real write
6618
+ // partway through the body, and everything rendered after it reads the name RAW: the update,
6619
+ // the bottom test, the header's own ops, the latch's side effects. The pure-alias route to such
6620
+ // a name is closed (`alias-under-its-own-name`); a param may still take it through `varName`
6621
+ // once the header param is dead, and no fixture reaches that today, so this guard is defensive.
5353
6622
  //
5354
6623
  // The refusal is on the NAME, not on who reads it: the readers are every statement the loop
5355
6624
  // emits, which is not a set worth enumerating when the name alone is the whole signal.
5356
6625
  //
5357
- // The emitted C is wrong whenever this shape occurs, sink or no sink — it is a naming-pipeline
5358
- // defect, not this pass's, and repairing the exit copy does not touch it. What IS this pass's
6626
+ // The emitted C can be wrong wherever this shape occurs, sink or no sink (the predicate is on
6627
+ // names, so it also holds on loops that emit correctly) — a naming-pipeline defect, not this
6628
+ // pass's, and repairing the exit copy does not touch it. What IS this pass's
5359
6629
  // is not to UNLOCK such a loop: with no sink these functions decline on the pre-update hazard,
5360
6630
  // so standing down keeps them loud rather than trading a decline for a silent wrong answer.
5361
6631
  const headerNames = new Set(dw.header.params.map((p) => varName.get(p)));
@@ -5380,12 +6650,22 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
5380
6650
  }
5381
6651
  const rebindHazard = [...bodyRebinds].some((n) => headerNames.has(n));
5382
6652
  const exitArgs = (successorTo(dw.latch, dw.exit)?.args ?? []) as Value[];
5383
- // A pre-update exit copy moves to the TOP of the body, where the loop variables still hold
5384
- // their top-of-iteration values. No zero-trip seed here: a `do-while` always runs its body, so
5385
- // any other predecessor of the exit is an ordinary edge some enclosing `if` already emits.
6653
+ // A pre-update exit copy moves INSIDE the body, ahead of the update, where the loop variables
6654
+ // still hold their top-of-iteration values. No zero-trip seed here: a `do-while` always runs
6655
+ // its body, so any other predecessor of the exit is an ordinary edge some enclosing `if`
6656
+ // already emits.
5386
6657
  const sunk = rebindHazard
5387
- ? new Set<number>()
5388
- : sinkablePreUpdateSlots(dw.header, dw.exit, exitArgs, dw.body, sub, updateWrites);
6658
+ ? new Map<number, Op | null>()
6659
+ : sinkablePreUpdateSlots(
6660
+ dw.header,
6661
+ dw.exit,
6662
+ exitArgs,
6663
+ dw.body,
6664
+ dw.latch,
6665
+ sub,
6666
+ updateWrites,
6667
+ hooks.preUpdateSinkGates ?? PREUPDATE_SINK_GATES,
6668
+ );
5389
6669
  // The post-loop region the escaped-value check judges: everything the loop does not emit itself.
5390
6670
  // An early-`return` arm the loop OWNS renders inside the body, ahead of the update, so a read of
5391
6671
  // a loop variable there is the pre-update value it wants — counting it as post-loop would decline
@@ -5393,7 +6673,48 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
5393
6673
  // post-loop where the other path really does read them after the update.
5394
6674
  const owned = new Set(dw.arms.flatMap((a) => [...a.owned]));
5395
6675
  const postLoop = new Set(fn.blocks.filter((bb) => !dw.body.has(bb) && !owned.has(bb)));
6676
+ // AN EFFECT THE RENDERED TEST MAY SKIP. `movedEffect` below says this about the exit copies; the
6677
+ // bottom test is the other place an effect can change how often it runs, and here it is the
6678
+ // `&&`/`||` that moves it rather than a copy: the right operand is evaluated only where the left
6679
+ // one let it be, while the asm's branch did not guard the op at all. Nothing can re-place it —
6680
+ // the connective is what the test IS — so decline LOUD, the same answer for the same reason.
6681
+ //
6682
+ // Asked of every `do-while`, not only of a folded one: the arm holds the same call whichever way
6683
+ // the counter is spelled.
6684
+ if (testSkipsAnEffect(lterm.operands[0], sub)) {
6685
+ throw new StructureError(
6686
+ `cannot structure '${fn.name}': the bottom test may evaluate an effect behind a '&&'/'||' ` +
6687
+ `that the asm ran on every iteration`,
6688
+ );
6689
+ }
6690
+ // The test's OWN pre-update read has a spelling the other two disjuncts do not: `n++` at the
6691
+ // leaf, with the update copy dropped from the foot of the body. Asked only where the test is
6692
+ // already a hazard, so a loop that structures today enumerates exactly the candidates it did.
6693
+ // The polarity is the one applied to `cond` below — the continue edge must be the taken one.
6694
+ const condAnswer = readsClobbered(lterm.operands[0], sub, updateWrites)
6695
+ ? preUpdateCondFold(
6696
+ lterm.operands[0],
6697
+ lterm.successors[1].block === dw.header,
6698
+ dw.header,
6699
+ successorTo(dw.latch, dw.header)?.args ?? [],
6700
+ exitArgs,
6701
+ dw.body,
6702
+ sub,
6703
+ updates,
6704
+ updateWrites,
6705
+ )
6706
+ : null;
6707
+ const condFold = condAnswer === null || 'refused' in condAnswer ? null : condAnswer;
6708
+ // A PRE-UPDATE HOME DOES NOT UNLOCK A LOOP whose variable's NAME a block after it also holds.
6709
+ // The exit region reads the loop's back-edge values under those names (`sub`), and such a
6710
+ // param's copies write the same name first — the merge naming's `carrier-live` sees live NAMED
6711
+ // values, not ones the substitution spells, so it does not refuse them. The spelling is wrong
6712
+ // wherever it occurs; it is the home's to keep from unlocking one, which without the name would
6713
+ // decline on this hazard.
6714
+ const homed = [...dw.body].some((bb) => bb.ops.some((o) => preUpdateHomes.has(o)));
6715
+ const sharesALoopName = homed && [...postLoop].some((bb) => bb.params.some((p) => headerNames.has(varName.get(p))));
5396
6716
  if (
6717
+ sharesALoopName ||
5397
6718
  loopUpdateHazard(
5398
6719
  lterm.operands[0],
5399
6720
  exitArgs.filter((_, j) => !sunk.has(j)),
@@ -5401,10 +6722,16 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
5401
6722
  sub,
5402
6723
  updateWrites,
5403
6724
  postLoop,
6725
+ condFold !== null,
5404
6726
  )
5405
6727
  ) {
6728
+ // Three reasons share this refusal — the test, an exit slot, an escaped body value — and the
6729
+ // one that was asked in detail names the gate that answered.
5406
6730
  throw new StructureError(
5407
- `cannot structure '${fn.name}': do-while condition or a post-loop value reads a pre-update loop variable`,
6731
+ `cannot structure '${fn.name}': do-while condition or a post-loop value reads a pre-update loop variable` +
6732
+ (condAnswer !== null && 'refused' in condAnswer
6733
+ ? ` (no '++' for the test's own read: ${condAnswer.refused})`
6734
+ : ''),
5408
6735
  );
5409
6736
  }
5410
6737
  // The exit copies render AFTER the `dowhile` statement, but the analysis judged where each
@@ -5437,7 +6764,7 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
5437
6764
  const inner =
5438
6765
  dw.header === dw.latch
5439
6766
  ? [] // single-block self-loop: the header IS the latch — its ops render via sideEffects below
5440
- : withLoop({ header: dw.header, exit: dw.exit, body: dw.body, arms: dw.arms }, () =>
6767
+ : withLoop({ header: dw.header, exit: dw.exit, body: dw.body, arms: dw.arms, breaks: new Set() }, () =>
5441
6768
  structureBlock(dw.header, dw.latch),
5442
6769
  ); // header..latch (exclusive of latch)
5443
6770
  dwActive.delete(dw.header);
@@ -5446,17 +6773,27 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
5446
6773
  // already holds the next value, so the latch-computed test reads `v`, not `v - 1`). `updates`
5447
6774
  // reuses the hazard check's computation — a second argAssigns call would burn a spurious
5448
6775
  // swap-cycle temp number.
6776
+ const sunkCopies = preUpdateCopies(dw.exit, exitArgs, sunk, dw.header);
6777
+ const latchEffects = () => sideEffects(dw.latch, sunkCopies.atDef);
6778
+ // The folded update is spelled by the test's own `n++`, so it leaves the foot of the body.
6779
+ // `updates` itself is untouched: every hazard above, and `writtenByUpdate` below, ask what one
6780
+ // iteration WRITES, and the fold moves that write without removing it.
6781
+ const bodyUpdates =
6782
+ condFold === null ? updates : updates.filter((st) => !(st.k === 'assign' && st.name === condFold.name));
5449
6783
  const body = [
5450
- ...preUpdateCopies(dw.exit, exitArgs, sunk, dw.header),
6784
+ ...sunkCopies.leading,
5451
6785
  ...inner,
5452
- ...(innerSub.size > 0 ? withSub(innerSub, () => sideEffects(dw.latch)) : sideEffects(dw.latch)),
5453
- ...updates,
6786
+ ...(innerSub.size > 0 ? withSub(innerSub, latchEffects) : latchEffects()),
6787
+ ...bodyUpdates,
5454
6788
  ];
6789
+ assertSunkCopiesPlaced(sunkCopies);
5455
6790
  // The test reads this loop's own update under `sub`, and anything else an inner loop left under
5456
6791
  // `innerSub` — the outer loop's own reading wins where a value is both. The test runs AFTER the
5457
6792
  // update copies, so an inner name one of them really writes no longer holds the inner value, and
5458
6793
  // that entry keeps the raw reading — the same refusal `latchInnerSub` makes for a name written
5459
6794
  // before the latch, with the update's writes added to what the re-derivation must not read.
6795
+ // The exit copies and the exit region run after the test and read under the same map: an inner
6796
+ // loop's value is still in its name there.
5460
6797
  const writtenByUpdate = innerSub.size > 0 ? updateWriteSet(updates) : new Set<string>();
5461
6798
  const condInner = [...innerSub].filter(([, n]) => !writtenByUpdate.has(n));
5462
6799
  const condMap = condInner.length > 0 ? new Map([...condInner, ...sub]) : sub;
@@ -5474,31 +6811,16 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
5474
6811
  }
5475
6812
  }
5476
6813
  if (condUnreadable.size > 0) {
5477
- const needs = (root: Value, stop: ReadonlyMap<Value, string> | null, targets: ReadonlySet<Value>): boolean => {
5478
- const seen = new Set<Value>();
5479
- const walk = (x: Value): boolean => {
5480
- if (seen.has(x) || stop?.has(x) === true || varName.has(x)) {
5481
- return false;
5482
- }
5483
- if (targets.has(x)) {
5484
- return true;
5485
- }
5486
- seen.add(x);
5487
- return defs.get(x)?.operands.some(walk) ?? false;
5488
- };
5489
- return walk(root);
5490
- };
5491
6814
  const bodyMap = latchMap ?? activeSub;
5492
6815
  const effectRoots = dw.latch.ops
5493
6816
  .slice(0, -1)
5494
- .filter(
5495
- (op) => op.results.length === 0 || materialize.has(op) || EFFECTFUL_OPS.has(op.opcode) || unreadResult(op),
5496
- )
6817
+ .filter((op) => op.results.length === 0 || materialize.has(op) || effectful(op) || unreadResult(op))
5497
6818
  .flatMap((op) => op.operands);
5498
6819
  const updateRoots = successorTo(dw.latch, dw.header)!.args;
6820
+ const regionRoots = rootsOf(exitRegion(dw.exit, dw.body, stop));
5499
6821
  if (
5500
- [...effectRoots, ...updateRoots].some((r) => needs(r, bodyMap, unreadable)) ||
5501
- needs(lterm.operands[0], condMap, condUnreadable)
6822
+ [...effectRoots, ...updateRoots].some((r) => rendersThrough(r, bodyMap, unreadable)) ||
6823
+ [lterm.operands[0], ...exitArgs, ...regionRoots].some((r) => rendersThrough(r, condMap, condUnreadable))
5502
6824
  ) {
5503
6825
  throw new StructureError(
5504
6826
  `cannot structure '${fn.name}': a loop latch reads an inner loop's value whose name was rewritten ` +
@@ -5510,11 +6832,25 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
5510
6832
  if (lterm.successors[1].block === dw.header) {
5511
6833
  cond = negateCond(cond);
5512
6834
  } // continue edge must be `taken`
6835
+ if (condFold !== null) {
6836
+ // AFTER the negation, so the leaf is placed in the tree that is emitted. `negateCond` rewrites
6837
+ // the connectives by De Morgan, which moves the leaf between arms without duplicating it — and
6838
+ // a rewrite that did duplicate it is exactly what `spellUpdateInCond` refuses.
6839
+ const folded = spellUpdateInCond(cond, condFold.name, condFold.by);
6840
+ if (folded === null) {
6841
+ throw new StructureError(
6842
+ `cannot structure '${fn.name}': the do-while test does not spell '${condFold.name}' exactly once, ` +
6843
+ `so its update has no place inside the test`,
6844
+ );
6845
+ }
6846
+ cond = folded;
6847
+ }
5513
6848
  const out: Stmt[] = [{ k: 'dowhile', cond, body }];
5514
- // The exit region reads latch back-edge values under `sub` (post-loop they live in the loop vars).
6849
+ // The exit region reads latch back-edge values under `sub` (post-loop they live in the loop vars),
6850
+ // and inner loops' values as the test does.
5515
6851
  out.push(
5516
- ...withSub(sub, () => [
5517
- ...argAssigns(dw.latch, dw.exit, sub, (j) => !sunk.has(j)),
6852
+ ...withSub(condMap, () => [
6853
+ ...argAssigns(dw.latch, dw.exit, condMap, (j) => !sunk.has(j)),
5518
6854
  ...structureRegion(dw.exit, stop),
5519
6855
  ]),
5520
6856
  );
@@ -5544,8 +6880,9 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
5544
6880
  if (owed.length > 0) {
5545
6881
  const mentioned = new Set<string>(globalNames);
5546
6882
  for (const e of walkExprs(body)) {
5547
- if (e.k === 'var' || e.k === 'addr') {
5548
- mentioned.add(e.name);
6883
+ const n = mentionedName(e);
6884
+ if (n !== undefined) {
6885
+ mentioned.add(n);
5549
6886
  }
5550
6887
  }
5551
6888
  const assignTargets = (ss: Stmt[]): void => {
@@ -5673,7 +7010,7 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
5673
7010
  lastLaddrOf.set(laddrName.get(op)!, op);
5674
7011
  }
5675
7012
  const structs = collectStructs(fn);
5676
- return {
7013
+ const tree: SFn = {
5677
7014
  name: fn.name,
5678
7015
  params: entry.params.map((p, i) => ({ name: `a${i}`, type: p.type })),
5679
7016
  locals: [
@@ -5697,7 +7034,7 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
5697
7034
  // spill, is the referee — stamp them then, and `l3/slotorder.ts` needs no change to use it.
5698
7035
  ...[...lastLaddrOf].map(([name, op]) => ({
5699
7036
  name,
5700
- type: T.int((op.attrs.width as number) * 8, op.attrs.signed as boolean),
7037
+ type: laddrType(op),
5701
7038
  // the asm materialized this slot's address, and this is how many times it
5702
7039
  // loaded and stored through it — both asm facts, and the gate the
5703
7040
  // l3/volatileval.ts variation reads (see the SFn.locals doc)
@@ -5752,13 +7089,48 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
5752
7089
  .sort((a, b) => a.name.localeCompare(b.name)),
5753
7090
  }
5754
7091
  : {}),
7092
+ ...(localStatics.statics.length ? { statics: localStatics.statics } : {}),
5755
7093
  retType: returnsVoid ? T.void() : returnType(fn),
5756
7094
  body,
5757
7095
  ...(structs.length ? { structs } : {}),
5758
7096
  // absent stays absent: the backend asks "is a direction known?", and there is no third state
5759
7097
  // at this boundary — `structureOptionsFor` dropped the target's `'unknown'` already.
5760
7098
  ...(spillSlotOrder !== undefined ? { slotOrder: spillSlotOrder } : {}),
7099
+ ...calledOnly({ declaredArgs, declaredReturns }, body),
7100
+ ...(dialect !== undefined ? { dialect } : {}),
5761
7101
  };
7102
+ if (fn.localObjects === undefined) {
7103
+ return tree;
7104
+ }
7105
+ const named = nameLocalStatics(tree, fn.localObjects);
7106
+ if ('refused' in named) {
7107
+ throw new StructureError(
7108
+ `cannot structure '${fn.name}': names a function-scope static ('${named.symbol}') ${named.refused}`,
7109
+ );
7110
+ }
7111
+ return named;
7112
+ }
7113
+
7114
+ /** Of each table of declared callees, the entries this body calls — every tree the ranked pass
7115
+ * structures carries its own, and is keyed by its whole text. A table this body calls nothing in is
7116
+ * absent. */
7117
+ function calledOnly(
7118
+ tables: Pick<StructureOptions, 'declaredArgs' | 'declaredReturns'>,
7119
+ body: Stmt[],
7120
+ ): Pick<StructureOptions, 'declaredArgs' | 'declaredReturns'> {
7121
+ const callees = new Set<string>();
7122
+ for (const e of walkExprs(body)) {
7123
+ if (e.k === 'call' && typeof e.fn === 'string') {
7124
+ callees.add(e.fn);
7125
+ }
7126
+ }
7127
+ const of = <T>(table: Readonly<Record<string, T>> | undefined): Record<string, T> | undefined => {
7128
+ const called = Object.fromEntries(Object.entries(table ?? {}).filter(([n]) => callees.has(n)));
7129
+ return Object.keys(called).length > 0 ? called : undefined;
7130
+ };
7131
+ const declaredArgs = of(tables.declaredArgs);
7132
+ const declaredReturns = of(tables.declaredReturns);
7133
+ return { ...(declaredArgs ? { declaredArgs } : {}), ...(declaredReturns ? { declaredReturns } : {}) };
5762
7134
  }
5763
7135
 
5764
7136
  // Does any statement CONTINUE this loop (vs. a nested one)? A `continue` inside a nested while/dowhile/
@@ -5984,11 +7356,16 @@ function* inEdgeRecords(preds: Map<Block, Block[]>, b: Block): Generator<{ pred:
5984
7356
  }
5985
7357
 
5986
7358
  // Immediate post-dominators. EXIT is represented as `null`; ret-blocks post-lead to it. Over the
5987
- // subgraph `keep` induces when given, every member of which must still reach a `ret` inside it.
5988
- function postDominators(fn: Fn, keep?: ReadonlySet<Block>): Map<Block, Block | null> {
7359
+ // subgraph `keep` induces when given, every member of which must still reach a `ret` inside it — or,
7360
+ // with `onward`, over only the edges it names between members of `keep`: a block it names no edge
7361
+ // from ends its paths, post-dominated by nothing but itself.
7362
+ function postDominators(fn: Fn, keep?: ReadonlySet<Block>, onward?: (b: Block) => Block[]): Map<Block, Block | null> {
5989
7363
  const blocks = keep ? fn.blocks.filter((b) => keep.has(b)) : fn.blocks;
5990
7364
  const nodes: (Block | null)[] = [null, ...blocks];
5991
7365
  const succ = (b: Block): (Block | null)[] => {
7366
+ if (onward !== undefined) {
7367
+ return onward(b);
7368
+ }
5992
7369
  const term = b.ops[b.ops.length - 1];
5993
7370
  return term.opcode === 'ret' ? [null] : successorsOf(b).filter((s) => keep === undefined || keep.has(s));
5994
7371
  };