@asmlift/core 0.9.0 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (101) hide show
  1. package/README.md +25 -24
  2. package/package.json +1 -1
  3. package/src/aggregate.ts +149 -0
  4. package/src/backend/c.ts +1 -0
  5. package/src/backend/cfamily.ts +165 -50
  6. package/src/backend/cpp.ts +2 -1
  7. package/src/backend/pascal.ts +31 -12
  8. package/src/contracts.ts +173 -16
  9. package/src/declare.ts +69 -20
  10. package/src/frontend/asmdata.ts +7 -1
  11. package/src/frontend/call-plan.ts +964 -60
  12. package/src/frontend/dead-reads.ts +628 -0
  13. package/src/frontend/device-pins.ts +23 -4
  14. package/src/frontend/emit.ts +4 -3
  15. package/src/frontend/fpu.ts +224 -17
  16. package/src/frontend/frame-objects.ts +1232 -192
  17. package/src/frontend/frontend.ts +64 -3
  18. package/src/frontend/high-half.ts +53 -4
  19. package/src/frontend/jump-table.ts +530 -0
  20. package/src/frontend/local-object.ts +158 -32
  21. package/src/frontend/mips.ts +2076 -215
  22. package/src/frontend/own-struct-return.ts +500 -0
  23. package/src/frontend/ppc.ts +2711 -232
  24. package/src/frontend/register-struct-call.ts +484 -0
  25. package/src/frontend/reloc-symbol.ts +22 -7
  26. package/src/frontend/ssa.ts +1460 -44
  27. package/src/frontend/stackargs.ts +106 -57
  28. package/src/frontend/thumb.ts +2044 -318
  29. package/src/ir/alias.ts +42 -0
  30. package/src/ir/bits.ts +681 -1
  31. package/src/ir/core.ts +100 -5
  32. package/src/ir/discipline.ts +86 -21
  33. package/src/ir/entry.ts +159 -0
  34. package/src/{structure → ir}/loops.ts +6 -2
  35. package/src/ir/opcodes.ts +109 -9
  36. package/src/ir/types.ts +86 -2
  37. package/src/ir/verify.ts +80 -11
  38. package/src/l3/ast.ts +290 -18
  39. package/src/l3/basecse.ts +33 -5
  40. package/src/l3/constinit.ts +382 -0
  41. package/src/l3/dce.ts +2 -2
  42. package/src/l3/initfirst.ts +42 -11
  43. package/src/l3/intcell.ts +2 -1
  44. package/src/l3/narrowdecl.ts +77 -36
  45. package/src/l3/pollguard.ts +8 -2
  46. package/src/l3/ptrcell.ts +31 -34
  47. package/src/l3/ptrfield.ts +123 -8
  48. package/src/l3/regspell.ts +9 -4
  49. package/src/l3/reindex.ts +283 -96
  50. package/src/l3/storage.ts +98 -1
  51. package/src/l3/symbol-refs.ts +84 -6
  52. package/src/l3/tailmerge.ts +4 -2
  53. package/src/l3/typing.ts +108 -15
  54. package/src/l3/unreduce.ts +6 -5
  55. package/src/l3/volatileptr.ts +17 -54
  56. package/src/mangle.ts +30 -0
  57. package/src/pattern/engine.ts +361 -73
  58. package/src/pipeline.ts +541 -24
  59. package/src/proto-context.ts +905 -45
  60. package/src/proto.ts +555 -40
  61. package/src/raise/aggmembers.ts +290 -0
  62. package/src/raise/arrays.ts +5 -1
  63. package/src/raise/declared-volatile.ts +207 -31
  64. package/src/raise/floathelpers.ts +254 -33
  65. package/src/raise/gvn.ts +3 -3
  66. package/src/raise/latch.ts +162 -6
  67. package/src/raise/memberarrays.ts +10 -4
  68. package/src/raise/offsetnames.ts +10 -26
  69. package/src/raise/pairparams.ts +33 -0
  70. package/src/raise/paramwidth.ts +196 -6
  71. package/src/raise/pre-recovery.ts +102 -19
  72. package/src/raise/pre-structure.ts +54 -0
  73. package/src/raise/recover.ts +12 -5
  74. package/src/raise/reroll.ts +694 -0
  75. package/src/raise/shortcircuit.ts +26 -12
  76. package/src/raise/softdiv.ts +1 -1
  77. package/src/raise/storesign.ts +153 -0
  78. package/src/raise/struct-arrays.ts +30 -11
  79. package/src/raise/structs.ts +396 -33
  80. package/src/raise/truncload.ts +4 -3
  81. package/src/raise/unthread.ts +400 -0
  82. package/src/rank-declare.ts +118 -12
  83. package/src/rank-variations.ts +89 -31
  84. package/src/rank.ts +307 -54
  85. package/src/runtime-helpers.ts +140 -22
  86. package/src/structure/analysis.ts +165 -2
  87. package/src/structure/arith-ops.ts +23 -0
  88. package/src/structure/globalaccess.ts +111 -9
  89. package/src/structure/hazards.ts +16 -64
  90. package/src/structure/local-statics.ts +145 -9
  91. package/src/structure/namecoalesce.ts +1 -1
  92. package/src/structure/pointer-spelling.ts +1106 -0
  93. package/src/structure/redundant-test.ts +1 -1
  94. package/src/structure/structure.ts +2180 -1054
  95. package/src/structure/switch-recover.ts +244 -100
  96. package/src/symbols.ts +6 -0
  97. package/src/target.ts +765 -52
  98. package/src/trace.ts +26 -8
  99. package/src/variation-definitions.ts +126 -14
  100. package/src/variation-gates.ts +2 -0
  101. package/src/variation-tokens.ts +5 -1
@@ -23,8 +23,8 @@
23
23
  // parallel block-argument update, sequentialised so an assignment never clobbers a
24
24
  // value a later one still needs.
25
25
  //
26
- // Module layout: loop DISCOVERY is loops.ts; the pure ANALYSIS phase (use registry, liveness,
27
- // materialization) is analysis.ts; comparison-tree switch recovery is switch-recover.ts
26
+ // Module layout: loop DISCOVERY is ../ir/loops.ts (raise/ reads it too); the pure ANALYSIS phase
27
+ // (use registry, liveness, materialization) is analysis.ts; comparison-tree switch recovery is switch-recover.ts
28
28
  // (explicit-deps factory). THIS file keeps the mutually-entangled remainder: SSA-destruction
29
29
  // coalescing (canTakeName + seeding) and emission (structureBlock + the loop emitters) — they
30
30
  // share varName/backArgName mutation and the activeSub/loopCtx dynamic state.
@@ -34,7 +34,8 @@
34
34
  // bottom-test `do-while`, `while (1)` for any other latch set (a conditional `continue`, a
35
35
  // mid-tested header), PROPERLY-nested loops, in-body `break`/early-`return`, comparison-tree and
36
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):
37
+ // `ArmExit` in switch-recover.ts), on some paths only, leaving the switch by a `break;` on the
38
+ // others (`switchFrames`). Still DECLINED (loud StructureError, never wrong code):
38
39
  // irreducible/overlapping loops, exits to more than one live merge, a `break` out of a nested loop's
39
40
  // body, a `break` whose exit copies would clobber, and mixed-entry self-loops (a guarded header also
40
41
  // entered by a plain br).
@@ -42,24 +43,44 @@
42
43
  // `case` cannot fall through (`spellSwitchFallthrough` false) sends Regime A back to if-recovery,
43
44
  // and arms that do not linearize into one chain — two arms falling into the same sibling, or a fall
44
45
  // into the `default:` — refuse in `chainArms`, which answers null.
45
- import { constAddressOf, globalBaseOf } from '../ir/alias';
46
+ import { globalBaseOf } from '../ir/alias';
47
+ import { type WordCtx, incomingArgs, mayAlwaysHold, overwrittenReads, storeForwards } from '../ir/bits';
46
48
  import {
47
49
  Block,
50
+ CALL_RETURN_OBJECT,
48
51
  Fn,
52
+ OWN_RETURN_OBJECT,
49
53
  Op,
54
+ RETURNS_OBJECT,
50
55
  Successor,
51
56
  Value,
57
+ byValueOperands,
52
58
  callArgs,
53
59
  calleeName,
54
60
  calleeValue,
55
61
  defOpMap,
56
62
  dominators,
57
63
  mergeClasses,
64
+ reachForward,
58
65
  successorsOf,
59
66
  } 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';
67
+ import { effectful, orderSensitive, placedAt, qualified, qualifiedBy, spelledWhenDead } from '../ir/discipline';
68
+ import { type NaturalLoop, analyzeLoops } from '../ir/loops';
69
+ import { CAST_WIDTHS, FLOAT_RESIZE, FLOAT_TO_INT, INT_TO_FLOAT, NEGATED_FCMP, WIDE_BITS, opSig } from '../ir/opcodes';
70
+ import {
71
+ type IrType,
72
+ type StructField,
73
+ T,
74
+ UNION_VIEW_NAMES,
75
+ intWidth,
76
+ isCastView,
77
+ scalarTypeForAccess,
78
+ spellsAccessType,
79
+ structSpelling,
80
+ typeEquals,
81
+ unionView,
82
+ unionViewAt,
83
+ } from '../ir/types';
63
84
  import {
64
85
  BinOp,
65
86
  Expr,
@@ -70,37 +91,46 @@ import {
70
91
  exprHasEffect,
71
92
  gapReasonFor,
72
93
  isLoop,
94
+ keepsHelperRel,
73
95
  mapExprChildren,
74
96
  mapStmtExprs,
75
97
  mentionedName,
76
98
  negateCond,
99
+ regainsHelperRel,
100
+ splitHelperBreak,
77
101
  stmtChildren,
78
102
  stmtExprs,
79
103
  stmtsEqual,
104
+ unionStmtsEvidence,
80
105
  walkExprs,
81
106
  } from '../l3/ast';
107
+ import { constantImageRefusal } from '../l3/constinit';
82
108
  import { type Gate, firstRejection } from '../l3/gates';
83
- import { exprCType, exprIntWidth, provablyNonNegative, ptrElemBytes, renderedIntSignedness } from '../l3/typing';
109
+ import {
110
+ exprCType,
111
+ exprIntWidth,
112
+ renderedIntSignedness,
113
+ rendersSignedOverUnsigned,
114
+ spelledNonNegative,
115
+ } from '../l3/typing';
84
116
  import { castOffDeclaration, qualifiedMemoryAccess } from '../l3/volstore';
117
+ import type { ContextGlobals } from '../proto-context';
85
118
  import { foldConstPair, isConstFoldOpcode } from '../raise/const';
86
119
  import { stampDeclaredVolatile } from '../raise/declared-volatile';
87
120
  import { returnType } from '../raise/recover';
121
+ import { storeSigned } from '../raise/storesign';
88
122
  import { collectStructs } from '../raise/structs';
89
123
  import {
90
124
  type DeclaredField,
91
125
  type SymbolInfo,
92
- type SymbolStructField,
93
126
  declaredArrayShape,
94
- declaredFields,
95
127
  isArrayField,
96
128
  isBitfieldField,
97
- isPtrField,
98
- isScalarCellSize,
99
129
  pointeeFields,
100
- scalarCellType,
101
130
  structFieldInnerExtents,
102
131
  } from '../symbols';
103
132
  import { analyze, shortCircuitGuardedValues } from './analysis';
133
+ import { ARITH_TO_BIN } from './arith-ops';
104
134
  import { makeBitfieldSpelling } from './bitfields';
105
135
  import {
106
136
  addOffset,
@@ -110,6 +140,10 @@ import {
110
140
  elementIndex,
111
141
  globalByteBase,
112
142
  globalOf,
143
+ memberQualsAllow,
144
+ memoFieldsOf,
145
+ ptrMemberDecl,
146
+ structMemberAt,
113
147
  subscriptsFromExtents,
114
148
  } from './globalaccess';
115
149
  import {
@@ -121,8 +155,8 @@ import {
121
155
  updateWriteSet,
122
156
  } from './hazards';
123
157
  import { type StaticLayout, localStaticShapes, nameLocalStatics } from './local-statics';
124
- import { type NaturalLoop, analyzeLoops } from './loops';
125
158
  import { type NameMerge, coalesceNames } from './namecoalesce';
159
+ import { type ArithCompilerFacts, declaresBytePointer, makePointerSpelling } from './pointer-spelling';
126
160
  import { testRereadsOnly } from './redundant-test';
127
161
  import { unspelledEpilogues } from './retspell';
128
162
  import { type ArmExit, type SwitchBoundCase, makeSwitchRecovery } from './switch-recover';
@@ -162,11 +196,7 @@ interface SymRenderCtx {
162
196
  * the byte residual — the `/flat-rank` arm's OFF switch, off the `spellDeclaredSubscripts`
163
197
  * structure option. */
164
198
  declRank: boolean;
165
- /** The members a symbol's declaration seats — a struct global's own, or a pointer global's
166
- * pointee's — MEMOIZED per symbol. `declaredFields` validates every member and returns a fresh
167
- * sorted copy on every call, and `isPtrValue` asks it for both operands of every binary node
168
- * lowered, so an uncached lookup is an O(n log n) allocation on a hot path — inside a
169
- * `structure()` a ranked run repeats once per candidate, 17,856 times on the largest fan. */
199
+ /** {@link memoFieldsOf} over `info`. */
170
200
  fieldsOf(name: string): DeclaredField[] | null;
171
201
  }
172
202
 
@@ -184,9 +214,9 @@ interface SymRenderCtx {
184
214
 
185
215
  /** The global named by a pointer global's VALUE as the additive lowering spells it: the bare
186
216
  * `gPtr`, or that value wearing the byte-pointer / u32 cast that lowering adds (cast-then-add,
187
- * see the `needsIntSpelling` / pointer-global arithmetic rules below). Both denote the same
188
- * address and add BYTES to it, so both fold here; a cast to any other pointer type is NOT looked
189
- * through — a `(u16 *)` base would re-scale everything added after it. */
217
+ * see the `needsIntSpelling` / pointer-global arithmetic rules in pointer-spelling.ts). Both
218
+ * denote the same address and add BYTES to it, so both fold here; a cast to any other pointer
219
+ * type is NOT looked through — a `(u16 *)` base would re-scale everything added after it. */
190
220
  function ptrGlobalValueName(x: Expr): string | null {
191
221
  if (x.k === 'var') {
192
222
  return x.name;
@@ -245,82 +275,40 @@ function ptrGlobalBase(e: Expr, isPtrGlobal: (n: string) => boolean): PtrGlobalB
245
275
  return name !== null ? { name, byte, idx } : null;
246
276
  }
247
277
 
248
- /** Does a member declared at signedness `declared` read as EXACTLY the type the cast spelling this
249
- * replaces would have produced — `scalarTypeForAccess(width, signed)`? That is the whole
250
- * byte-exactness argument for naming a member instead of casting: same address, same read width,
251
- * same C type, so every operator downstream compiles identically. A 4-byte access renders s32
252
- * whatever the load said (the ISA has one word load), so only a SIGNED member may take its place;
253
- * narrower accesses carry their own signedness and must match it. An undeclared signedness (the
254
- * member is not a base type — a nested struct, an enum, a pointer) is never assumed to match.
255
- * Compared against THE one copy of that rule (ir/types.ts) rather than restating it. */
256
- function spellsAccessType(declared: boolean | undefined, width: number, signed: boolean): boolean {
257
- if (declared === undefined) {
258
- return false;
259
- }
260
- return typeEquals(T.int(width * 8, declared), scalarTypeForAccess(width, signed));
261
- }
262
-
263
278
  /** The declared type of a frame-local object, from the three facts its `laddr` carries: `count`
264
279
  * elements of `width` bytes, signed or not. ONE element is a SCALAR typed by the access every
265
280
  * access agreed on. More is STORAGE the frontend's frame-object audit sized without typing —
266
281
  * 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. */
282
+ * that spells them. Both arms are the audit's own facts; neither infers anything here.
283
+ *
284
+ * …and storage the audit found accessed at two widths over one byte carries the VIEWS those
285
+ * accesses use (`views`, frame-objects.ts `unionViews`), and is declared the union of them —
286
+ * each view an array tiling the whole storage, `union { s32 word[3]; u16 half[6]; u8 byte[12]; }`
287
+ * — because a union member access is the one spelling of two widths agbcc never reads stale. */
268
288
  function laddrType(op: Op): IrType {
269
289
  // a call's struct-return storage: the audit typed its address as a pointer to that struct
270
290
  const t = op.results[0].type;
271
291
  if (op.attrs.aggregate === true && t.kind === 'ptr') {
272
292
  return t.to;
273
293
  }
294
+ const views = op.attrs.views as number[] | undefined;
295
+ if (views !== undefined) {
296
+ const count = op.attrs.count as number;
297
+ const widths = views.map(Math.abs);
298
+ const members = views.map((v): StructField => {
299
+ const w = Math.abs(v);
300
+ const elem = scalarTypeForAccess(w, v < 0);
301
+ const name =
302
+ widths.filter((x) => x === w).length > 1 ? `${v < 0 ? 's' : 'u'}${UNION_VIEW_NAMES[w]}` : UNION_VIEW_NAMES[w];
303
+ return { off: 0, type: count === w ? elem : T.array(elem, count / w), name };
304
+ });
305
+ return { kind: 'union', members, size: count };
306
+ }
274
307
  const elem = T.int((op.attrs.width as number) * 8, op.attrs.signed as boolean);
275
308
  const count = op.attrs.count as number;
276
309
  return count === 1 ? elem : T.array(elem, count);
277
310
  }
278
311
 
279
- /** May a member be NAMED by an access of this direction, given the qualifiers on its declaration?
280
- * The named spelling REPLACES a cast through `(u8 *)`, which carries no qualifier at all, so a
281
- * qualifier the name reintroduces changes what the compiler emits:
282
- * • `volatile` makes the access observable — the load may no longer be folded or reordered,
283
- * which is a different instruction sequence (measured: 6 insns where the cast form was 5);
284
- * • `const` under a STORE is a hard error, where the cast form merely cast the qualifier away.
285
- * Either way the honest spelling is the cast form, so the member simply is not nameable here. */
286
- function memberQualsAllow(f: SymbolStructField, containerConst: boolean | undefined, isStore: boolean): boolean {
287
- if (f.volatile) {
288
- return false;
289
- }
290
- return !(isStore && (f.const || containerConst));
291
- }
292
-
293
- /** The map member a `field` node NAMES, or null when it names none — THE one resolver for "what
294
- * does the declaration say about this member", for rules that must reason about a member's type
295
- * after the access rules have already spelled it. Both named spellings resolve, through the same
296
- * shared gate their spelling passed: `gSym.member` off a struct global's {@link declaredFields},
297
- * `gPtr->member` off the pointee's ({@link pointeeFields}). A synthesized `field_K` — the
298
- * recovered-struct spelling, which no map declares — resolves to null, and so does any base that
299
- * is not a map-shaped global, which is what makes every caller refuse rather than guess.
300
- *
301
- * The pointee arm reaches only what the MAP lets it: `pointeeAccess` gates every `gPtr->member`
302
- * spelling on `spellsAccessType(f.signed, …)`, so a pointer field declaring no signedness — which
303
- * is every one in the corpus's vendored maps — is never named, and a pointer read one indirection
304
- * down spells `((s32 *)gQ)[1]`. That is a fact about those maps, not about this code: `SymbolMap`
305
- * is a caller-supplied input, and one field flips it (`signed: true` on a 4-byte pointer member
306
- * of a pointee yields `(u8 *)gQ->pInner`, cast and all, pinned in pointer-members.test.ts). So
307
- * the arm is live and tested, and the byte-arithmetic rule that reads this answer is correct for
308
- * it — where resolving a pointee member to null would reopen the double-scaling hole silently. */
309
- function declaredMemberOf(x: Expr, sym: SymRenderCtx | undefined): DeclaredField | null {
310
- if (x.k !== 'field' || x.base.k !== 'var' || sym === undefined) {
311
- return null;
312
- }
313
- return sym.fieldsOf(x.base.name)?.find((f) => f.name === x.name) ?? null;
314
- }
315
-
316
- /** The map member a `field` node names when the declaration makes it a POINTER — {@link
317
- * isPtrField} being the shared two-fact test, so this and the synthesized declaration cannot
318
- * disagree about what a member is. */
319
- function ptrMemberDecl(x: Expr, sym: SymRenderCtx | undefined): DeclaredField | null {
320
- const f = declaredMemberOf(x, sym);
321
- return f !== null && isPtrField(f) ? f : null;
322
- }
323
-
324
312
  /** A map-declared POINTER MEMBER as the additive lowering renders its VALUE: the bare `gSym.pBuf`,
325
313
  * or that member wearing the byte-pointer / u32 cast the arithmetic guard adds. Both denote the
326
314
  * same address and add BYTES to it, so both fold here; a cast to any OTHER pointer type is not
@@ -465,6 +453,23 @@ function ptrMemberElement(
465
453
  // 4096 — including offsets past Thumb's immediate range, and under multi-member, across-a-call
466
454
  // and in-a-loop shapes. It is one load whose member offset becomes the same immediate the cast
467
455
  // form used, and unlike the indexed form it composes.
456
+ //
457
+ // ONE SHAPE IS NOT IDENTICAL: on agbcc, a STORE to an offset past Thumb's
458
+ // immediate range. expand_assignment (gcc/expr.c) expands only the base of a COMPONENT_REF target
459
+ // (:2535) and evaluates the right-hand side first in store_field (:2624), while it expands a cast
460
+ // (INDIRECT_REF) target whole (:2689) before the right-hand side (:2747). So the member form loads
461
+ // a loaded value before it materialises `base + K` and the cast form after it, and with a constant
462
+ // or a parameter on the right the two still allocate registers differently. Neither spelling is
463
+ // the source's by default: a hoisted `u8 *d = &p->f38; *d = v;` builds the cast's object, and the
464
+ // order in which the address and the value are computed is the evidence between them. The member
465
+ // is emitted here; raise/structs.ts `foldDisplacedAccesses` folds such a store into its struct by
466
+ // default and the `/far-cast` lift keeps it on its address, so the differ referees the two
467
+ // (`synthetic:sfar_b` pins the member). A LOAD's member and cast forms compile alike, but agbcc
468
+ // gives each member access its own `base + K`, so one materialised base read at two offsets is a
469
+ // held pointer, and the fold leaves it on its address (`held-pointer-offsets`, `synthetic:sfar_held`
470
+ // pins it); so does ido past its `addiu` reach (target.ts `farMemberBasePerAccess`). On ido, KMC,
471
+ // GCC 2.7.2 and mwcc one access's two forms compile alike past a 16-bit displacement, a loaded
472
+ // value included, and `/far-cast` is offered there for the neighbours a folded store respells.
468
473
 
469
474
  /** The pointee a global's value may be spelled through: the members the declaration synthesis
470
475
  * DECLARES. Null when nothing may be named through it — THE shared gate (symbols.ts
@@ -700,6 +705,39 @@ function displacementIndex(off: number, width: number): number {
700
705
  return off / width;
701
706
  }
702
707
 
708
+ /** A stamped narrow store (raise/storesign.ts) spelled through a MEMBER — a struct field, a union
709
+ * member's view, a struct-array element's field — is converted by the compiler to the member's
710
+ * DECLARED type, so a declaration whose sign is not the stamp's loses it: `a0->field_6 = -104`
711
+ * under `u16 field_6` recompiles to `li 0xff98`. Such a store is spelled instead as the flat cell
712
+ * at the same address, `((s16 *)a0)[3] = -104`, or `((s16 *)(a0 + a1))[1] = -104` for an
713
+ * element. The member's type is read off the spelled lvalue (`ctype`), so a member declared by a
714
+ * load, a stamp or a union's view is judged alike.
715
+ *
716
+ * `storeWidth` is `undefined` for an `astore`, which does not carry it: raise/struct-arrays.ts
717
+ * declares each field at the one width its accesses share, so the member's width is the store's.
718
+ *
719
+ * `lval` comes back unchanged for an unstamped store, an lvalue that is not a member, no
720
+ * `flatBase`, a member that is not a narrow integer of the store's width or agrees with the stamp,
721
+ * and an offset that is not a whole number of cells (no flat subscript spells it). */
722
+ function stampedStoreLval(
723
+ lval: Expr,
724
+ stamp: boolean | undefined,
725
+ storeWidth: number | undefined,
726
+ flatBase: Expr | null,
727
+ off: number,
728
+ ctype: (e: Expr) => IrType | undefined,
729
+ ): Expr {
730
+ if (stamp === undefined || lval.k !== 'field' || flatBase === null) {
731
+ return lval;
732
+ }
733
+ const t = ctype(lval);
734
+ const width = storeWidth ?? (t?.kind === 'int' ? t.width / 8 : 0);
735
+ if (t?.kind !== 'int' || (width !== 1 && width !== 2) || t.width !== width * 8 || t.signed === stamp || off % width) {
736
+ return lval;
737
+ }
738
+ return { k: 'index', base: flatBase, idx: { k: 'const', value: off / width }, width, signed: stamp };
739
+ }
740
+
703
741
  function memAccess(
704
742
  base: Value,
705
743
  baseExpr: Expr,
@@ -711,6 +749,7 @@ function memAccess(
711
749
  sym?: SymRenderCtx,
712
750
  isStore = false,
713
751
  advancedBy?: number,
752
+ castQualified = false,
714
753
  ): Expr {
715
754
  // A deref of a global's address collapses to the bare global: `*(&gSym)` at off 0 is `gSym`;
716
755
  // at off N the global is an array — `gSym[N/width]` (a C global name decays to a pointer, so
@@ -762,18 +801,18 @@ function memAccess(
762
801
  if (sym) {
763
802
  const gb = globalConstByte(baseExpr, off);
764
803
  const si = gb ? sym.info(gb.name) : undefined;
765
- if (gb && si?.shape === 'struct') {
804
+ // NOT FOR AN ACCESS THE LIFT QUALIFIED BY A CAST (`castQualified`, ir/discipline.ts `qualifiedBy`):
805
+ // a member's name reaches the object through its declaration, which the map states plain — had
806
+ // it qualified the object the access would be placed `declared` — so `gState.ctl;` would compile
807
+ // to no read at all. The access falls to the cast forms below, where `pinnedAccess` puts the
808
+ // qualifier: `((volatile s32 *)&gState)[2];`.
809
+ if (gb && si?.shape === 'struct' && !castQualified) {
766
810
  // THE shared spellability predicate (symbols.ts), the same call declare.ts gates its struct
767
811
  // declaration on: a layout it declines whole is a layout with no nameable members, and a
768
812
  // union alias it drops for the first view at that offset is a name no declaration carries.
769
- // An ARRAY member is excluded for the same reason as in pointeeAccess: `u8 x[1]` would match
770
- // a byte access by (offset, size) and spell `.x`, which is not an lvalue of that width. A
771
- // BITFIELD member likewise — a plain read of its bytes is not a read of its bits (the named
772
- // bitfield spelling has its own recognizer, on the extract shape: see lowerDef).
773
- const fld = declaredFields(si.layout)?.find(
774
- (f) => f.offset === gb.byte && f.size === width && !isArrayField(f) && !isBitfieldField(f),
775
- );
776
- if (fld && memberQualsAllow(fld, si.const, isStore)) {
813
+ // The named bitfield spelling has its own recognizer, on the extract shape: see lowerDef.
814
+ const fld = structMemberAt(si, gb.byte, width, isStore);
815
+ if (fld) {
777
816
  return { k: 'field', base: { k: 'var', name: gb.name }, name: fld.name, dot: true };
778
817
  }
779
818
  }
@@ -838,7 +877,10 @@ function memAccess(
838
877
  return { k: 'index', base: { k: 'addr', name: g.name }, idx, width, signed, ...addressEvidence };
839
878
  }
840
879
  const bt = base.type;
841
- if (bt.kind === 'ptr' && bt.to.kind === 'struct') {
880
+ // A field declared at its cell's widest view (raise/structs.ts `castViewCell`) is read at any
881
+ // other view as the flat cell at the same address, `((u8 *)p)[1]`: the cast the union it stands
882
+ // for would have spelled as a member.
883
+ if (bt.kind === 'ptr' && bt.to.kind === 'struct' && !isCastView(bt.to, off, width, signed, isStore)) {
842
884
  const rt = ctype(baseExpr);
843
885
  // `->` requires the base to render as a pointer to THIS struct (field names resolve against
844
886
  // its declaration) AND as a non-`index` node (the printer spells an index-node base with `.`,
@@ -1020,32 +1062,25 @@ const CMP_TO_BIN: Record<string, BinOp> = {
1020
1062
  icmp_eq: '==',
1021
1063
  icmp_ne: '!=',
1022
1064
  };
1023
- export const ARITH_TO_BIN: Record<string, BinOp> = {
1024
- add: '+',
1025
- sub: '-',
1026
- mul: '*',
1027
- sdiv: '/',
1028
- // the UNSIGNED quotient/remainder — the C backend spells them `/`/`%` over an operand it casts
1029
- // unsigned (l3/ast.ts BinOp, backend/cfamily.ts C_SPELLING)
1030
- udiv: '/u',
1031
- smod: '%',
1032
- umod: '%u',
1033
- or: '|',
1034
- and: '&',
1035
- xor: '^',
1036
- shl: '<<',
1037
- shr_u: '>>>', // the LOGICAL right shift; the C backend spells it `>>` over an unsigned operand
1038
- shr_s: '>>',
1039
- logic_and: '&&',
1040
- logic_or: '||', // short-circuit connectives (raise/shortcircuit.ts)
1041
- };
1042
1065
 
1043
1066
  // 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.
1067
+ // not entries in ARITH_TO_BIN (arith-ops.ts), because every rule that table's consumers apply —
1068
+ // the pointer stride, the integer legalizations, the constant fold, the commutative re-spelling —
1069
+ // is an integer rule.
1047
1070
  const FLOAT_TO_BIN: Record<string, BinOp> = { fadd: 'f+', fsub: 'f-', fmul: 'f*', fdiv: 'f/' };
1048
1071
 
1072
+ // The float compares (ir/opcodes.ts) and their L3 operators. An ORDERED compare and `fcmp_une` are
1073
+ // C's relation; an unordered one is the logical NOT of the ordered relation it negates
1074
+ // (`NEGATED_FCMP`), and is spelled as that `!`, never as the integer negation of the relation.
1075
+ const FCMP_TO_BIN: Record<string, BinOp> = {
1076
+ fcmp_olt: 'f<',
1077
+ fcmp_ole: 'f<=',
1078
+ fcmp_ogt: 'f>',
1079
+ fcmp_oge: 'f>=',
1080
+ fcmp_oeq: 'f==',
1081
+ fcmp_une: 'f!=',
1082
+ };
1083
+
1049
1084
  // The operators whose operand order the machine does not fix — candidates for the def-order
1050
1085
  // re-spelling in lowerDef. `&&`/`||` are excluded: short-circuit order IS semantics.
1051
1086
  const COMMUTATIVE_BIN: ReadonlySet<BinOp> = new Set(['+', '*', '&', '|', '^']);
@@ -1098,24 +1133,39 @@ interface LoopInfo {
1098
1133
 
1099
1134
  // One early-`return` exit out of a loop body, and the blocks of it the loop emits inside its body
1100
1135
  // (`earlyReturnArm` decides which). Per-EDGE rather than flattened into per-loop sets: ownership is
1101
- // decided against `from`, so a second edge into the same target is a separate question. `owned` has
1102
- // one reader, `emitDoWhile` — a `while` hoists no update, so it has no pre-update reads to exempt.
1136
+ // decided against `from`, so a second edge into the same target is a separate question. Two arms'
1137
+ // `owned` sets may OVERLAP: an effectful tail only the loop's exit arms run (`armsOnly`, the
1138
+ // cross-jumped `h(i); return;` two arms end in) is owned by every arm that reaches it, and written
1139
+ // once in each. `owned` has one reader, `emitDoWhile`, which unions the arms' sets for its
1140
+ // pre-update hazard check — an overlap only repeats a block in that union — and a `while` hoists no
1141
+ // update, so it has no pre-update reads to exempt.
1103
1142
  interface LoopArm {
1104
1143
  from: Block;
1105
1144
  to: Block;
1106
1145
  owned: Set<Block>;
1107
1146
  }
1108
1147
 
1148
+ // A `break` whose arm RUNS CODE first: `if (t[i] == 7) { gHit = 1; break; }`. The arm's blocks never
1149
+ // reach the latch, so they sit outside the natural loop as a second exit target; `codeBreakArm`
1150
+ // says when they are one exit with code on the way.
1151
+ interface BreakArm {
1152
+ from: Block;
1153
+ to: Block;
1154
+ region: Set<Block>;
1155
+ }
1156
+
1109
1157
  // 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`.
1158
+ // by a `break` to `exit` or an early `return`. `exit` is null for a loop no edge leaves at all — the
1159
+ // main loop that never returns — which has no break, no arm, and nothing after it.
1111
1160
  interface ForeverLoopInfo {
1112
1161
  header: Block;
1113
1162
  latches: Block[];
1114
1163
  forwardPreds: Block[];
1115
- exit: Block;
1164
+ exit: Block | null;
1116
1165
  body: Set<Block>;
1117
1166
  arms: LoopArm[];
1118
- breaks: Set<Block>; // the body blocks whose edge to `exit` is a `break`
1167
+ breaks: Set<Block>; // the body blocks whose edge to `exit` is a `break`, or whose edge enters a `breakArms` arm
1168
+ breakArms: BreakArm[];
1119
1169
  }
1120
1170
 
1121
1171
  // A test-at-top multi-block `while`. The header is a pure test whose cond_br enters `bodyEntry`
@@ -1129,7 +1179,8 @@ interface WhileLoopInfo {
1129
1179
  forwardPreds: Block[]; // header preds outside the loop body (the entry/init side)
1130
1180
  body: Set<Block>; // the pure natural-loop body (for in-body vs exit classification)
1131
1181
  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`
1182
+ breaks: Set<Block>; // the body blocks whose edge to `exit` itself is a `break`, or enters a `breakArms` arm
1183
+ breakArms: BreakArm[];
1133
1184
  }
1134
1185
 
1135
1186
  // Opcodes whose NUMBER OF EXECUTIONS is observable. Moving one of these out of a loop changes what
@@ -1481,14 +1532,16 @@ export const ENCLOSING_CARRIER_GATES: readonly Gate<EnclosingCarrier>[] = [
1481
1532
  // orderArgCopiesByWriteOrder — order edge copies by the order the pred WROTE their
1482
1533
  // destinations.
1483
1534
  // The last three are `compilerBehaviors` (target.ts) — this pass stays target-AGNOSTIC: it reads
1484
- // booleans, never a compiler name.
1535
+ // booleans, never a compiler name. So are the arithmetic table's (`ArithCompilerFacts`), which this
1536
+ // type extends and hands to `makePointerSpelling` whole; the byte-pointer temp step below reads
1537
+ // `keepsPointerSumAddend` from it too.
1485
1538
  //
1486
1539
  // WHAT A FIELD DOC BELOW HOLDS, narrowly: what the option MEANS to `structure()`, and the suffix of
1487
1540
  // the variation that enumerates it. A VARIATION's rationale, and any figure pricing its marginal value, live
1488
1541
  // ONCE at its `STRUCTURE_VARIATIONS` entry in rank-variations.ts — restated here the two copies rot
1489
1542
  // separately, and only that one is the entry the enumeration reads. Figures pricing a
1490
1543
  // DEFAULT this pass owns (the edge-copy ordering, `spellDeclaredSubscripts`) do belong here.
1491
- export interface StructureOptions {
1544
+ export interface StructureOptions extends ArithCompilerFacts {
1492
1545
  returnsVoid?: boolean;
1493
1546
  coalesceLoopInit?: boolean;
1494
1547
  preserveDivergentBranchSense?: boolean;
@@ -1593,6 +1646,13 @@ export interface StructureOptions {
1593
1646
  // that a fall-through switch has a SECOND, behaviourally identical recovery. Regime A declines
1594
1647
  // to if-recovery when it is false; Regime B, having no fallback, fails loud. Default true.
1595
1648
  spellSwitchFallthrough?: boolean;
1649
+ // Does the TARGET LANGUAGE spell a `break` that leaves a `switch` arm before its end? Set from
1650
+ // the caller's LanguageBackend (`spellsSwitchBreak`), for the same reason as the line above. When
1651
+ // false, a CLOSED arm gets no region (`structureArm`), so `switchBreakAt` never mints the
1652
+ // statement and the arm structures to the CFG's joins, copying the tail its early exit skips into
1653
+ // each path. A FALLING arm needs no gate of its own: a language without a mid-arm `break` has no
1654
+ // fall-through either (Pascal), and `spellSwitchFallthrough` keeps those out. Default true.
1655
+ spellSwitchBreak?: boolean;
1596
1656
  // Which way this compiler hands out frame slots against DECLARATION RANK: `ascending` = the
1597
1657
  // earlier-declared spilled local takes the LOWER `[sp,#k]`. A compiler behavior declared
1598
1658
  // in TargetDescription.compilerBehaviors, carried to the backend on `SFn.slotOrder` and applied
@@ -1613,6 +1673,12 @@ export interface StructureOptions {
1613
1673
  declaredReturns?: Readonly<Record<string, string>>;
1614
1674
  // The emitted C is compiled as C++ (`SFn.dialect`).
1615
1675
  dialect?: 'c++';
1676
+ // The compiler parses a designated initializer element (`SFn.designators`; the compiler behavior
1677
+ // of the same name says which do).
1678
+ designatedInitializers?: boolean;
1679
+ // The compiler's C refuses C's implicit pointer conversions (`SFn.strictConversions`; the compiler
1680
+ // behavior `strictPointerConversions` says which does).
1681
+ strictPointerConversions?: boolean;
1616
1682
  // Commutative load pairs re-spell in def (evaluation) order — see the swap in lowerDef. Default
1617
1683
  // true; verified byte-exact on agbcc and IDO. A compiler behavior declared in
1618
1684
  // TargetDescription.compilerBehaviors: the first compiler whose scheduler is shown re-ordering
@@ -1650,14 +1716,6 @@ export interface StructureOptions {
1650
1716
  // data. The provider already refuses to EMIT bitfield facts for a big-endian ELF; this is the
1651
1717
  // same boundary enforced on core's side, against a hand-built map that never went through it.
1652
1718
  littleEndian?: boolean;
1653
- // HARDWARE fact from TargetDescription.capabilities.deviceRegisters, threaded by
1654
- // `structureOptionsFor` like `littleEndian` above: the half-open byte window whose cells are
1655
- // hardware registers rather than objects a source declares. The structurer asks it the same
1656
- // question its four other readers ask — "would a source have spelled this address `volatile`" —
1657
- // so it may be approximate: it decides a SPELLING, not a memory model (that is
1658
- // `deviceMemoryWriters`, which no structurer rule reads). Used as a REFUSAL: absent, a dead read
1659
- // at a literal address is dropped.
1660
- deviceRegisters?: readonly [number, number];
1661
1719
  // Does the compiler extend a `volatile` narrow signed read in a register, never in the load? The
1662
1720
  // `declared` stamp structuring re-derives reads it (TargetDescription.compilerBehaviors'
1663
1721
  // `volatileReadsExtendInRegister`), so it leaves unplaced the read the lift's stamp did.
@@ -1756,10 +1814,13 @@ export interface StructureOptions {
1756
1814
  readsStayWhereWritten?: boolean;
1757
1815
  // Spell unsigned compares unsigned: cast an icmp_u* operand where the rendered operands do not
1758
1816
  // guarantee it, and reconcile a mixed-claimant declaration to u32 when nothing under the name
1759
- // needs signed. Off by default: a signed spelling that byte-matched was PROVED non-negative by
1760
- // the compiler (it emits the unsigned branch from signed compares only then), so which spelling
1761
- // the source used is genuinely ambiguous at emission — rank.ts enumerates the ON spelling as
1762
- // the `/uns-cmp` variation and the differ referees.
1817
+ // needs signed. ON by default, because it is the only spelling that is value-faithful without a
1818
+ // proof: an unsigned compare spelled over two signed-rendering operands compiles to a SIGNED
1819
+ // compare, which differs from the machine's wherever an operand's top bit is set. The OFF spelling
1820
+ // is still a real source: a signed spelling that byte-matched was PROVED non-negative by the
1821
+ // compiler (it emits the unsigned branch from signed compares only then), and emission's provable
1822
+ // set is smaller than the compiler's. So rank.ts enumerates it as the `/signed-cmp` variation and
1823
+ // publishes it only on a byte-exact score (`Candidate.matchOnly`).
1763
1824
  unsignedCompareSpelling?: boolean;
1764
1825
  // Merge two variables that a merge copy would join, when the values under them never interfere
1765
1826
  // (structure/namecoalesce.ts). Off by default; rank.ts enumerates the ON spelling as the
@@ -1817,6 +1878,11 @@ export interface StructureOptions {
1817
1878
  * they would read the map's `shape:'pointer'`. The operand-order rules do not read it: no element
1818
1879
  * or field spelling folds from a name with no pointee. */
1819
1880
  pointerGlobals?: ReadonlySet<string>;
1881
+ /** What the declaration context states about its globals (proto-context.ts `globalsFromContext`):
1882
+ * which are structs, unions or arrays, and which are volatile, read by the stamp this structuring
1883
+ * re-derives. Read with the map setting and without it: a context declaration is what every
1884
+ * candidate compiles against, whichever names it spells. */
1885
+ contextGlobals?: ContextGlobals;
1820
1886
  }
1821
1887
 
1822
1888
  /** The named global an `index` node's base denotes DIRECTLY, or undefined: the bare `&gSym` an
@@ -1880,9 +1946,11 @@ export interface StructureHooks {
1880
1946
  * has decided both arms are real, so it cannot be computed ahead of the pass. */
1881
1947
  onBranchSenseSite?: (site: { block: number; ordinal: number; joined: boolean; negated: boolean }) => void;
1882
1948
  /** 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`). */
1949
+ * callers fire it — a divergent `if` under `followEarlyReturns`, EVERY jump table whose arms all
1950
+ * return, unconditionally, and a jump table whose default-anchored RETRY is adopted (some arms
1951
+ * return, the rest reach the code after it) — so the sites it reports are not the enumeration
1952
+ * domain of the shared-tail twins (that domain is `hasDivergentSharedRet`, which asks for a
1953
+ * `cond_br`). */
1886
1954
  onEarlyReturnFollow?: (site: { block: number; follow: number }) => void;
1887
1955
  }
1888
1956
 
@@ -1897,26 +1965,181 @@ export interface StructureHooks {
1897
1965
  * It rests on `structure()` not mutating `fn`, which `structure-purity.test.ts` pins.
1898
1966
  *
1899
1967
  * SCOPE: refusals thrown by `structure()` itself. A decline can also come from `structureChecked`'s
1900
- * boundary contracts, which run OUTSIDE it — `rank.ts` closes that half, where the contracts are. */
1901
- function assertDefaultAccepts(fn: Fn, opts: StructureOptions, hooks: StructureHooks): void {
1902
- structure(
1903
- fn,
1904
- {
1905
- ...opts,
1906
- coalesceMergeNames: false,
1907
- freshParamMerge: false,
1908
- materializeJoinFeeds: false,
1909
- homeSharedAddresses: false,
1910
- homeLoopExprs: false,
1911
- homeDerivedReads: false,
1912
- homeMergeFeeds: false,
1913
- homeEscapingExtensions: false,
1914
- anchorConstCopies: false,
1915
- anchorLoopEntryConsts: false,
1916
- followEarlyReturns: false,
1917
- },
1918
- hooks,
1919
- );
1968
+ * boundary contracts, which run OUTSIDE it — `rank.ts` closes that half, where the contracts are.
1969
+ *
1970
+ * With a `memo`, a reset already structured for this fn is answered from the memo. */
1971
+ function assertDefaultAccepts(
1972
+ fn: Fn,
1973
+ opts: StructureOptions,
1974
+ hooks: StructureHooks,
1975
+ memo: DefaultAcceptsMemo | undefined,
1976
+ ): void {
1977
+ const reset: StructureOptions = {
1978
+ ...opts,
1979
+ coalesceMergeNames: false,
1980
+ freshParamMerge: false,
1981
+ materializeJoinFeeds: false,
1982
+ homeSharedAddresses: false,
1983
+ homeLoopExprs: false,
1984
+ homeDerivedReads: false,
1985
+ homeMergeFeeds: false,
1986
+ homeEscapingExtensions: false,
1987
+ anchorConstCopies: false,
1988
+ anchorLoopEntryConsts: false,
1989
+ followEarlyReturns: false,
1990
+ };
1991
+ if (memo === undefined) {
1992
+ structure(fn, reset, hooks);
1993
+ } else {
1994
+ memo.check(fn, hooks, reset, () => structure(fn, reset, hooks));
1995
+ }
1996
+ }
1997
+
1998
+ /** A {@link DefaultAcceptsMemo} asked a question it cannot answer. */
1999
+ export class DefaultAcceptsMemoMisuse extends Error {
2000
+ constructor(message: string) {
2001
+ super(`DefaultAcceptsMemo: ${message}`);
2002
+ this.name = 'DefaultAcceptsMemoMisuse';
2003
+ }
2004
+ }
2005
+
2006
+ /** Every block, block parameter, op, opcode, operand, result, successor and edge argument of `fn`,
2007
+ * in order and by identity, each list after its length. A pass that rewrites the graph in place
2008
+ * changes it; attributes and side tables are not in it. */
2009
+ function graphShape(fn: Fn): unknown[] {
2010
+ const shape: unknown[] = [fn.blocks.length];
2011
+ for (const b of fn.blocks) {
2012
+ shape.push(b, b.params.length, ...b.params, b.ops.length);
2013
+ for (const op of b.ops) {
2014
+ shape.push(op, op.opcode, op.operands.length, ...op.operands, op.results.length, ...op.results);
2015
+ shape.push(op.successors.length);
2016
+ for (const s of op.successors) {
2017
+ shape.push(s.block, s.args.length, ...s.args);
2018
+ }
2019
+ }
2020
+ }
2021
+ return shape;
2022
+ }
2023
+
2024
+ /** A set or array of at most this many primitives is keyed by its members. Keying costs a pass over
2025
+ * the members on every structuring, and the maps and sets a project hands every function (`symbols`,
2026
+ * `pointerGlobals`) are as large as the project, so a bigger one is keyed by identity. */
2027
+ const MEMO_CONTENT_KEY_LIMIT = 64;
2028
+
2029
+ /** What `assertDefaultAccepts` learned about ONE fn: for each reset it structured, whether
2030
+ * `structure()` accepted it or the error it threw. Settings that differ only in the flags the
2031
+ * guard resets share one reset, so an enumeration that structures a fn under many settings asks
2032
+ * the same question of it many times.
2033
+ *
2034
+ * VALID WHILE THE FN AND THE OPTIONS' CONTAINERS ARE UNCHANGED. A fn rewritten in place
2035
+ * (`sinkStoreTails`) needs a new memo, and a `Map`, an object, or a set or array over
2036
+ * `MEMO_CONTENT_KEY_LIMIT` members is keyed by identity, so one mutated between two calls would
2037
+ * share a key with its old contents. The `declared` stamps are not a change: `structure()`
2038
+ * re-derives them from the options' map before it reaches the guard, so the reset run finds them as
2039
+ * the first run of the same key did.
2040
+ *
2041
+ * Asked about another fn, or about its own fn once a block, op or value of it was added, removed,
2042
+ * replaced or reordered (`graphShape`), it throws {@link DefaultAcceptsMemoMisuse} rather than
2043
+ * answer. That is a wiring defect, not a decline, so `rank.ts` lets it end the enumeration.
2044
+ *
2045
+ * Only a call with no hooks is answered: a hook can change the outcome, or must observe the run.
2046
+ * An option holding a function or a symbol has no key, and its reset always runs. */
2047
+ export class DefaultAcceptsMemo {
2048
+ readonly #outcomes = new Map<string, { error: unknown } | 'accepted'>();
2049
+ readonly #ids = new WeakMap<object, number>();
2050
+ readonly #shape: readonly unknown[];
2051
+ #nextId = 0;
2052
+
2053
+ constructor(readonly fn: Fn) {
2054
+ this.#shape = graphShape(fn);
2055
+ }
2056
+
2057
+ /** Returns or throws what `run` did for this reset, running it only when the memo has no
2058
+ * outcome for it yet. */
2059
+ check(fn: Fn, hooks: StructureHooks, reset: StructureOptions, run: () => void): void {
2060
+ if (fn !== this.fn) {
2061
+ throw new DefaultAcceptsMemoMisuse('asked about a fn it was not made for');
2062
+ }
2063
+ const shape = graphShape(fn);
2064
+ if (shape.length !== this.#shape.length || shape.some((x, i) => !Object.is(x, this.#shape[i]))) {
2065
+ throw new DefaultAcceptsMemoMisuse(`asked about ${fn.name} after its graph was rewritten`);
2066
+ }
2067
+ const key = Object.values(hooks).every((h) => h === undefined) ? this.#keyOf(reset) : undefined;
2068
+ if (key === undefined) {
2069
+ run();
2070
+ return;
2071
+ }
2072
+ const known = this.#outcomes.get(key);
2073
+ if (known === 'accepted') {
2074
+ return;
2075
+ }
2076
+ if (known !== undefined) {
2077
+ throw known.error;
2078
+ }
2079
+ try {
2080
+ run();
2081
+ } catch (error) {
2082
+ this.#outcomes.set(key, { error });
2083
+ throw error;
2084
+ }
2085
+ this.#outcomes.set(key, 'accepted');
2086
+ }
2087
+
2088
+ /** Every option that is set, by name and value; `undefined` when one has no key. */
2089
+ #keyOf(opts: StructureOptions): string | undefined {
2090
+ const parts: string[] = [];
2091
+ for (const name of Object.keys(opts).sort()) {
2092
+ const value: unknown = (opts as Record<string, unknown>)[name];
2093
+ if (value === undefined) {
2094
+ continue;
2095
+ }
2096
+ const part = this.#valueKey(value);
2097
+ if (part === undefined) {
2098
+ return undefined;
2099
+ }
2100
+ parts.push(`${name}=${part}`);
2101
+ }
2102
+ return parts.join(';');
2103
+ }
2104
+
2105
+ /** A prefix-free spelling of one value, so two option sets share a key only when every value
2106
+ * agrees. */
2107
+ #valueKey(value: unknown): string | undefined {
2108
+ switch (typeof value) {
2109
+ case 'undefined':
2110
+ return 'u';
2111
+ case 'boolean':
2112
+ return value ? 't' : 'f';
2113
+ case 'number':
2114
+ return Object.is(value, -0) ? 'n-0' : `n${value}`;
2115
+ case 'bigint':
2116
+ return `b${value}`;
2117
+ case 'string':
2118
+ return JSON.stringify(value);
2119
+ case 'function':
2120
+ case 'symbol':
2121
+ return undefined;
2122
+ }
2123
+ if (value === null) {
2124
+ return 'null';
2125
+ }
2126
+ const members = value instanceof Set ? [...value] : Array.isArray(value) ? value : undefined;
2127
+ if (
2128
+ members !== undefined &&
2129
+ members.length <= MEMO_CONTENT_KEY_LIMIT &&
2130
+ members.every((m) => m === null || typeof m !== 'object')
2131
+ ) {
2132
+ const keys = members.map((m) => this.#valueKey(m));
2133
+ return keys.every((k) => k !== undefined) ? `${value instanceof Set ? 'S' : 'A'}[${keys.join(',')}]` : undefined;
2134
+ }
2135
+ const container = value as object;
2136
+ let id = this.#ids.get(container);
2137
+ if (id === undefined) {
2138
+ id = this.#nextId++;
2139
+ this.#ids.set(container, id);
2140
+ }
2141
+ return `#${id}`;
2142
+ }
1920
2143
  }
1921
2144
 
1922
2145
  /** What {@link earlyReturnArm} reads beyond its arguments: block dominance and forward
@@ -1924,20 +2147,33 @@ function assertDefaultAccepts(fn: Fn, opts: StructureOptions, hooks: StructureHo
1924
2147
  interface EarlyReturnArmDeps {
1925
2148
  dom: Map<Block, Set<Block>>;
1926
2149
  reachFrom: (b: Block) => Set<Block>;
2150
+ preds: ReadonlyMap<Block, Block[]>;
1927
2151
  }
1928
2152
 
1929
2153
  function isRet(blk: Block): boolean {
1930
2154
  return blk.ops[blk.ops.length - 1]?.opcode === 'ret';
1931
2155
  }
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.
2156
+ /** The `ret`s reachable from EVERY successor of `b` — the region `followOverReturns` keeps — or,
2157
+ * given an `anchor`, from every successor that shares a `ret` with the anchor. Empty when there
2158
+ * is none, and when `b` does not branch at all. `reachFrom` is forward reachability, the start
2159
+ * block excluded.
1935
2160
  *
1936
2161
  * THE QUESTION IS ABOUT A BRANCH, not about an `if`: "the arms all return, so post-dominance gives
1937
2162
  * this block no join — where does the construct end?" is asked by a jump table exactly as by a
1938
2163
  * two-way branch, and the answer is derived the same way. The `if`-shaped reading belongs to the
1939
2164
  * CALLER that wants it (`hasDivergentSharedRet`), not here.
1940
2165
  *
2166
+ * THE ANCHOR names a successor taken to reach the end the question is about, and leaves out
2167
+ * every successor that shares no `ret` with it (`leftOutOf`). The `switch_br` caller anchors on
2168
+ * the dispatch's DEFAULT edge, whose target is the `default:` arm or the code after the switch: a
2169
+ * `case` that `return`s on its own, or falls into one that does, then no longer empties the
2170
+ * intersection for the arms that `break`. Leaving those successors out is sound for the follow the
2171
+ * caller derives from the answer: the answer is a subset of the anchor's `ret`s, so a successor
2172
+ * left out reaches none of them, hence no block that reaches one — in particular not the follow —
2173
+ * and it is structured to its own `return`. It is a candidate and not evidence that the source
2174
+ * ended the construct there: a `default:` that `return`s on its own is an anchor too, and the
2175
+ * caller states what it reads to tell the two apart. The `if` caller never anchors.
2176
+ *
1941
2177
  * THE DEDUP IS DEFENSIVE AND NO CALLER CAN REACH IT TODAY. It is what makes "two successors that
1942
2178
  * are the same block" answer `[]` rather than "a branch with a shared `ret`". Both callers ask
1943
2179
  * only when `ipdom.get(b)` is null, and a branch whose successors are all ONE block has that block
@@ -1945,23 +2181,33 @@ function isRet(blk: Block): boolean {
1945
2181
  * to this function rather than to its callers' guards, and dropping it is a no-op on the corpus:
1946
2182
  * the reader over every row of the committed artifact (`decompile` per row, no compiling) emits
1947
2183
  * byte-identical source with and without it. */
1948
- function sharedRetsOf(b: Block, reachFrom: (x: Block) => ReadonlySet<Block>): Block[] {
1949
- const succs = [...new Set(successorsOf(b))];
2184
+ function sharedRetsOf(b: Block, reachFrom: (x: Block) => ReadonlySet<Block>, anchor: Block | null = null): Block[] {
2185
+ const left = anchor === null ? [] : leftOutOf(b, reachFrom, anchor);
2186
+ const succs = [...new Set(successorsOf(b))].filter((x) => !left.includes(x));
1950
2187
  if (succs.length < 2) {
1951
2188
  return [];
1952
2189
  }
1953
- const retsFrom = (x: Block): Block[] => [x, ...reachFrom(x)].filter(isRet);
1954
- return succs.map(retsFrom).reduce((shared, rets) => {
1955
- const here = new Set(rets);
1956
- return shared.filter((r) => here.has(r));
1957
- });
2190
+ return succs
2191
+ .map((x) => retsFrom(x, reachFrom))
2192
+ .reduce((shared, rets) => {
2193
+ const here = new Set(rets);
2194
+ return shared.filter((r) => here.has(r));
2195
+ });
2196
+ }
2197
+ function retsFrom(x: Block, reachFrom: (x: Block) => ReadonlySet<Block>): Block[] {
2198
+ return [x, ...reachFrom(x)].filter(isRet);
2199
+ }
2200
+ /** The successors of `b` that share no `ret` with `anchor` — the ones `sharedRetsOf` leaves out. */
2201
+ function leftOutOf(b: Block, reachFrom: (x: Block) => ReadonlySet<Block>, anchor: Block): Block[] {
2202
+ const anchored = new Set(retsFrom(anchor, reachFrom));
2203
+ return [...new Set(successorsOf(b))].filter((x) => !retsFrom(x, reachFrom).some((r) => anchored.has(r)));
1958
2204
  }
1959
2205
  /** Is there an `if` whose arms reach no common block before EXIT but share a `ret` — the only shape
1960
2206
  * `followEarlyReturns` changes? The enumeration gate of both shared-tail twins (rank.ts), asked of
1961
2207
  * the fn as raised and again of the sunk fn; a superset, since it asks `sharedRetsOf` and none of
1962
2208
  * the follow's three later refusals. An `if`, specifically: see the restriction at the `some`. */
1963
2209
  export function hasDivergentSharedRet(fn: Fn): boolean {
1964
- const ipdom = postDominators(fn);
2210
+ const ipdom = functionPostDominators(fn);
1965
2211
  const reach = new Map<Block, Set<Block>>();
1966
2212
  const reachFrom = (b: Block): Set<Block> => {
1967
2213
  let out = reach.get(b);
@@ -2002,6 +2248,141 @@ function reachesAvoiding(from: Block, to: Block, avoid: Block): boolean {
2002
2248
  }
2003
2249
  return false;
2004
2250
  }
2251
+ /** The arm `from -> to` breaks to `exit` after running code: the region `to` heads re-enters no
2252
+ * `body` block and leaves only for `exit` or by a `ret` of its own. Returns that region, or null.
2253
+ * The region is written inside the loop, ahead of the `break` — a `ret` among it as the `return` it
2254
+ * is: `if (t[i] == 7) { if (k) return; gHit = 1; break; }`.
2255
+ *
2256
+ * UNSHARED (the default, and the header-exit caller), `to` is entered by that edge alone and the
2257
+ * region admits no edge from outside itself, so each region block is written once and the region
2258
+ * owns its `ret`s: the `return` is written where the asm holds its one copy.
2259
+ *
2260
+ * A TAIL SEVERAL BREAKS SHARE, with `shared` (what `exitArmOf` passes for every in-body code arm):
2261
+ * `if (a) f |= 1; else if (b) f |= 2; else continue; gHit = i; break;` compiles to two arms that
2262
+ * each run their own code and jump to one `gHit = i` block on the way out (agbcc and gcc2.7.2
2263
+ * cross-jump arms written with a tail each into that same shape, jump.c). The region is then
2264
+ * admitted when the arm's head is entered only from the body (a body block, or a block every path
2265
+ * into which comes from the body without passing `exit`: the other arm's own blocks, whose exit
2266
+ * edge the classifier must admit as an arm of its own, or the loop declines), every other block an
2267
+ * edge from outside enters is one the loop's own exit path does not run (`offExitPath`), and the
2268
+ * region holds no cycle. Each arm writes the shared blocks again ahead of its own `break`, a shared
2269
+ * `ret` as one `return` per arm, and a path from outside the loop (an `else` before it) structures
2270
+ * its own copy where the block sits. Those paths are exclusive — a path that left the body by an arm
2271
+ * reaches a region block again only through `exit`, and the exit does not reach it — so every path
2272
+ * runs the tail once, as the asm does, whatever the tail does; where the compiler cross-jumps, the
2273
+ * copies are the same code (gcc2.7.2kmc byte-identical, agbcc up to register allocation).
2274
+ *
2275
+ * THE COST where it does not: ido keeps one tail, and only the source's single copy at the arms'
2276
+ * join recompiles to it. `synthetic:brmerge:ido7.1`'s row source is byte-identical to its target;
2277
+ * the per-arm spelling compiles to 52 instructions against 50 (an extra `lui` and `sb`), so that
2278
+ * row cannot MATCH this way. The single copy needs a `continue` that runs the latch's update — a
2279
+ * `for` whose `inc` is that update, or a `do`/`while` whose test the `continue` reaches — and
2280
+ * structure writes `continue` only to a `while (1)` header, while the L3 `for` respell refuses a
2281
+ * body holding one (l3/ast.ts, the `for` node). */
2282
+ function codeBreakArm(
2283
+ preds: ReadonlyMap<Block, Block[]>,
2284
+ dom: ReadonlyMap<Block, ReadonlySet<Block>>,
2285
+ from: Block,
2286
+ to: Block,
2287
+ body: ReadonlySet<Block>,
2288
+ exit: Block,
2289
+ shared = false,
2290
+ ): Set<Block> | null {
2291
+ if (to === exit || body.has(to)) {
2292
+ return null;
2293
+ }
2294
+ const region = new Set<Block>();
2295
+ let leaves = false;
2296
+ for (const stack = [to]; stack.length > 0;) {
2297
+ const x = stack.pop()!;
2298
+ if (x === exit) {
2299
+ leaves = true;
2300
+ continue;
2301
+ }
2302
+ if (region.has(x)) {
2303
+ continue;
2304
+ }
2305
+ const t = x.ops[x.ops.length - 1];
2306
+ if (body.has(x) || (t.opcode !== 'br' && t.opcode !== 'cond_br' && t.opcode !== 'ret')) {
2307
+ return null; // re-enters the loop, or dispatches
2308
+ }
2309
+ region.add(x);
2310
+ stack.push(...successorsOf(x));
2311
+ }
2312
+ // Shared: the arm's head is entered from another arm when every path into the entering block
2313
+ // comes from the body without passing `exit` — the body block itself, or the other arm's own
2314
+ // blocks. Never from outside the loop: an edge straight from the body into a block others enter
2315
+ // too is a `break` to it, which `while (1)` writes once, after the loop.
2316
+ const armOfBody = onlyFromBody(preds, body, exit);
2317
+ // Shared, past the head: a block may be entered from anywhere — another arm, or outside the loop —
2318
+ // when the loop's own exit path does not run it (`offExitPathOf`). agbcc cross-jumps a `break`
2319
+ // that assigns a `u8` with an `else` before the loop that assigns the same `u8` into one `lsr`
2320
+ // truncation block, and a `gLast = id;` both write into one `strb` (jump.c); gcc2.7.2kmc keeps one
2321
+ // `jal note` the arm's `beql` and the `else` both enter. The per-arm copy is the source's spelling.
2322
+ const offExitPath = offExitPathOf(exit, dom);
2323
+ let entersShared = false;
2324
+ const entered = (x: Block) =>
2325
+ (preds.get(x) ?? []).every(
2326
+ (p) =>
2327
+ region.has(p) ||
2328
+ (x === to && p === from) ||
2329
+ (shared && (x === to ? armOfBody(p) : offExitPath(x)) && (entersShared = true)),
2330
+ );
2331
+ if (!leaves || ![...region].every(entered)) {
2332
+ return null;
2333
+ }
2334
+ // a region another arm enters is written once per arm, so it must be one a walk writes once: no
2335
+ // cycle. One only its own edge enters is written once wherever it sits.
2336
+ return !entersShared || [...region].every((x) => acyclicThrough(x, region)) ? region : null;
2337
+ }
2338
+ /** The duplication policy's admission (written at `earlyReturnArm`): is a block one the loop's own
2339
+ * exit path does not run? Forward only (`reachForward`): round an enclosing loop's back-edge the exit
2340
+ * reaches the inner loop again, which is the next iteration running the block again in the asm too,
2341
+ * not the fall-through path running it after the loop. Computed on first use. */
2342
+ function offExitPathOf(exit: Block, dom: ReadonlyMap<Block, ReadonlySet<Block>>): (b: Block) => boolean {
2343
+ let fromExit: Set<Block> | undefined;
2344
+ return (b) => !(fromExit ??= reachForward(exit, dom)).has(b);
2345
+ }
2346
+ /** Does every path into a block come from `body` without passing `exit`? True of a body block, and
2347
+ * of a block outside whose every predecessor is one — a loop exit arm's own blocks, and a tail only
2348
+ * those arms enter. Memoised per call; a cycle back to a block is no path from the body. */
2349
+ function onlyFromBody(
2350
+ preds: ReadonlyMap<Block, Block[]>,
2351
+ body: ReadonlySet<Block>,
2352
+ exit: Block,
2353
+ ): (b: Block) => boolean {
2354
+ const fromBody = new Map<Block, boolean>();
2355
+ const armOfBody = (p: Block): boolean => {
2356
+ if (body.has(p)) {
2357
+ return true;
2358
+ }
2359
+ const known = fromBody.get(p);
2360
+ if (known !== undefined) {
2361
+ return known;
2362
+ }
2363
+ fromBody.set(p, false);
2364
+ const into = preds.get(p) ?? [];
2365
+ const ok = p !== exit && into.length > 0 && into.every(armOfBody);
2366
+ fromBody.set(p, ok);
2367
+ return ok;
2368
+ };
2369
+ return armOfBody;
2370
+ }
2371
+ /** Does no path inside `region` lead from `x` back to `x`? */
2372
+ function acyclicThrough(x: Block, region: ReadonlySet<Block>): boolean {
2373
+ const seen = new Set<Block>();
2374
+ for (const stack = successorsOf(x); stack.length > 0;) {
2375
+ const y = stack.pop()!;
2376
+ if (y === x) {
2377
+ return false;
2378
+ }
2379
+ if (region.has(y) && !seen.has(y)) {
2380
+ seen.add(y);
2381
+ stack.push(...successorsOf(y));
2382
+ }
2383
+ }
2384
+ return true;
2385
+ }
2005
2386
  // An early `return` out of the loop: forward-walking from `to` WITHOUT re-entering the loop `body`,
2006
2387
  // every path terminates in a `ret`. agbcc/gcc merge every `return` into ONE epilogue block and each
2007
2388
  // return site just sets the return register and branches there, so a second body exit that lands on
@@ -2011,26 +2392,67 @@ function reachesAvoiding(from: Block, to: Block, avoid: Block): boolean {
2011
2392
  //
2012
2393
  // The arm is structured AT the edge, so any block in it that a second path also reaches is emitted
2013
2394
  // twice. A duplicated `return v` is harmless — both copies sit on mutually exclusive paths, so this
2014
- // is a FIDELITY rule, not a soundness one: a store or call written twice is source no compiler
2015
- // would have produced from this asm, and the region it drags along is unbounded. A block escapes
2016
- // that on two counts. `from` dominates `to` and `to` dominates the block, so every path reaching it
2395
+ // is a FIDELITY rule, not a soundness one.
2396
+ //
2397
+ // ONE DUPLICATION POLICY for loop exit arms, here and in `codeBreakArm`'s shared mode. A block with
2398
+ // effects may be written once per arm when the loop's own exit path does not run it
2399
+ // (`offExitPathOf`) and it holds no cycle, whoever else enters it — another arm, or a path from
2400
+ // before the loop (an `else` beside it). A path that left the body by an arm reaches the block only
2401
+ // through that arm, because the exit does not reach it; a path from before the loop never ran an arm.
2402
+ // The paths are exclusive, so each runs one copy once, the arm's or the one written where the block
2403
+ // sits; on a compiler that cross-jumps (agbcc, gcc2.7.2: jump.c) those copies are the source the one
2404
+ // block came from. Whether the block has an effect does not enter into it. A block the loop's own
2405
+ // exit path also runs is written once, after the loop: copying it into an arm writes a store or call
2406
+ // twice where the asm holds it once on the fall-through path, and drags the rest of the function
2407
+ // along. `codeBreakArm`'s shared region stops at `exit`, so the loop's own arms bound it and it
2408
+ // takes no size limit. This walk applies the same policy to a tail that ends in its own `return`:
2409
+ // an effectful block is admitted only on one of the two counts below, and refused otherwise.
2410
+ //
2411
+ // THE EDGE OWNS IT. `from` dominates `to` and `to` dominates the block, so every path reaching it
2017
2412
  // runs this edge's predecessor and then this region — and only one edge into `to` can satisfy the
2018
2413
  // first half, since two predecessors cannot both dominate it. And the region must not be reachable
2019
- // from the loop's own exit, which dominance does NOT rule out: an arm landing straight on the
2020
- // post-loop join dominates itself, and claiming it would emit the epilogue on both paths. Testing
2021
- // `to` covers the whole region — a block dominated by `to` that the exit reached would mean the
2022
- // exit reached `to`. The shared epilogue an arm branches to still has to be pure.
2414
+ // forward from the loop's own exit (`offExitPathOf`), which dominance does NOT rule out: an arm
2415
+ // landing straight on the post-loop join dominates itself, and claiming it would emit the epilogue
2416
+ // on both paths. Testing `to` covers the whole region — a block dominated by `to` that the exit
2417
+ // reached would mean the exit reached `to`.
2418
+ //
2419
+ // ONLY THE LOOP'S EXIT ARMS RUN IT (`armsOnly`): the policy's count, plus every path into it comes
2420
+ // from the body without passing `exit` (`onlyFromBody`). Two arms ending in the same `h(i); return;`,
2421
+ // which agbcc and gcc2.7.2kmc cross-jump into one block no edge dominates, write it once each: the
2422
+ // arms run on exclusive paths, so each path still runs it once. The `while` reading that takes one
2423
+ // such arm for the exit stays refused — the other arm's tail is then reachable from it — so the loop
2424
+ // falls to the reading whose exit runs nothing of the tail. A shared epilogue the exit also reaches
2425
+ // still has to be pure.
2023
2426
  //
2024
- // Returns the blocks the arm OWNS — its exclusive part, which the loop emits inside its body, ahead
2025
- // of the update — or null when this is not an early-`return` arm.
2427
+ // WHY A RETURN TAIL ASKS MORE THAN THE POLICY. An entry from before the loop is sound to copy past —
2428
+ // the paths are exclusive — but into a tail that returns it is, in the common shape, the loop's own
2429
+ // zero-trip guard landing on the post-loop epilogue: `for (i = 0; i < n; i++) { if (t[i] == 7) {
2430
+ // ping(i); return; } } out(0);`. Admitting it lets the `while` reading take the early-return arm for
2431
+ // the exit and copy `out(0); return;` into the body, where the asm holds it once after the loop
2432
+ // (ido7.1 and mwcc_242_81 re-spell `while (1)` that way, equivalent but not the source's shape).
2433
+ // What it would buy is an `else` before the loop that cross-jumps into the arms' tail (agbcc's
2434
+ // `else { pong(9); hit(a); return; }`), and that lifts only through the same re-spelling.
2435
+ //
2436
+ // Returns the blocks the arm OWNS — the ones the loop emits inside its body for this arm, ahead of
2437
+ // the update — or null when this is not an early-`return` arm. Not exclusive to the arm: a block the
2438
+ // `armsOnly` count admits is owned by every arm that reaches it, so two arms' sets can share it.
2026
2439
  function earlyReturnArm(
2027
- { dom, reachFrom }: EarlyReturnArmDeps,
2440
+ { dom, reachFrom, preds }: EarlyReturnArmDeps,
2028
2441
  from: Block,
2029
2442
  to: Block,
2030
2443
  body: Set<Block>,
2031
2444
  exit: Block,
2032
2445
  ): Set<Block> | null {
2033
- const entryOwned = dom.get(to)!.has(from) && to !== exit && !reachFrom(exit).has(to);
2446
+ // does the loop's own exit path run it? forward only (`offExitPathOf`)
2447
+ const offExitPath = offExitPathOf(exit, dom);
2448
+ const entryOwned = dom.get(to)!.has(from) && to !== exit && offExitPath(to);
2449
+ // the duplication policy's admission: the loop's own exit path does not run it, and no cycle holds
2450
+ // it — and, for a tail that returns, only the loop's exit arms enter it (`armsOnly`, below). Never
2451
+ // the arm's own head: an edge straight from the body into a block others enter too is a `break` to
2452
+ // it, which `while (1)` writes once, after the loop.
2453
+ const armOfBody = onlyFromBody(preds, body, exit);
2454
+ const armsOnly = (bb: Block) =>
2455
+ bb !== to && bb !== exit && armOfBody(bb) && offExitPath(bb) && !reachFrom(bb).has(bb);
2034
2456
  const owned = new Set<Block>();
2035
2457
  const seen = new Set<Block>();
2036
2458
  const stack = [to];
@@ -2043,7 +2465,7 @@ function earlyReturnArm(
2043
2465
  if (body.has(bb)) {
2044
2466
  return null;
2045
2467
  } // re-enters the loop → not an exit
2046
- if (entryOwned && dom.get(bb)!.has(to)) {
2468
+ if ((entryOwned && dom.get(bb)!.has(to)) || (bb.ops.some(effectful) && armsOnly(bb))) {
2047
2469
  owned.add(bb);
2048
2470
  } else if (bb.ops.some(effectful)) {
2049
2471
  return null;
@@ -2072,7 +2494,12 @@ function carrierSign(t: IrType | undefined): boolean | undefined {
2072
2494
  return t?.kind === 'int' && t.width < 32 ? t.signed : undefined;
2073
2495
  }
2074
2496
 
2075
- export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureHooks = {}): SFn {
2497
+ export function structure(
2498
+ fn: Fn,
2499
+ opts: StructureOptions = {},
2500
+ hooks: StructureHooks = {},
2501
+ memo?: DefaultAcceptsMemo,
2502
+ ): SFn {
2076
2503
  const {
2077
2504
  returnsVoid = false,
2078
2505
  coalesceLoopInit = false,
@@ -2087,10 +2514,13 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
2087
2514
  switchArmsFollowLayout = false,
2088
2515
  switchRequiresFrontLoadedTests = false,
2089
2516
  spellSwitchFallthrough = true,
2517
+ spellSwitchBreak = true,
2090
2518
  spillSlotOrder,
2091
2519
  declaredArgs,
2092
2520
  declaredReturns,
2093
2521
  dialect,
2522
+ designatedInitializers = false,
2523
+ strictPointerConversions = false,
2094
2524
  defOrderLoadPairs = true,
2095
2525
  anchorConstCopies = false,
2096
2526
  anchorLoopEntryConsts = false,
@@ -2107,8 +2537,9 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
2107
2537
  homeEscapingExtensions = false,
2108
2538
  readsStayWhereWritten = false,
2109
2539
  contractsFloatProducts = false,
2540
+ keepsPointerSumAddend = false,
2110
2541
  staticLayout,
2111
- unsignedCompareSpelling = false,
2542
+ unsignedCompareSpelling = true,
2112
2543
  coalesceMergeNames = false,
2113
2544
  freshParamMerge = false,
2114
2545
  followEarlyReturns = false,
@@ -2117,12 +2548,16 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
2117
2548
  inferredSymbols,
2118
2549
  orderLicensedGlobals,
2119
2550
  pointerGlobals,
2551
+ contextGlobals,
2120
2552
  } = opts;
2121
2553
  // A read this stamp is the first to place was plain to every raising pass, and in a `&&`/`||`'s
2122
2554
  // guarded operand a short-circuit fold may have lifted it out of the arm it ran in: which side of
2123
2555
  // the branch the asm read it on is lost. The analysis names a counted op it finds there above the
2124
2556
  // branch, which is right only for one no fold could move, so this read declines instead.
2125
- const stampedLate = stampDeclaredVolatile(fn, mapSymbols, opts);
2557
+ const stampedLate = stampDeclaredVolatile(fn, mapSymbols, {
2558
+ volatileReadsExtendInRegister: opts.volatileReadsExtendInRegister,
2559
+ contextGlobals,
2560
+ });
2126
2561
  if (stampedLate.length > 0) {
2127
2562
  const defsNow = defOpMap(fn);
2128
2563
  const guarded = shortCircuitGuardedValues(fn, defsNow);
@@ -2143,6 +2578,11 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
2143
2578
  // shape is its definition's, never the one raise/globalshape.ts infers from its accesses.
2144
2579
  const localStatics = localStaticShapes(fn, staticLayout);
2145
2580
  if ('refused' in localStatics) {
2581
+ if (fn.localObjects?.get(localStatics.symbol)?.anonymous) {
2582
+ throw new StructureError(
2583
+ `cannot structure '${fn.name}': reads the compiler's anonymous constant ('${localStatics.symbol}') ${localStatics.refused}`,
2584
+ );
2585
+ }
2146
2586
  throw new StructureError(
2147
2587
  `cannot structure '${fn.name}': names a function-scope static ('${localStatics.symbol}') ${localStatics.refused}`,
2148
2588
  );
@@ -2181,8 +2621,9 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
2181
2621
  // widens a load's render positions and narrows the write set that bars it, so it removes
2182
2622
  // materializations rather than minting them. Extra materialization is what this guard is
2183
2623
  // about (see above), and this variation adds none;
2184
- // - `/uns-cmp` (unsignedCompareSpelling) writes `varType` and inserts casts at compares. It
2185
- // touches no name and no copy, so no edge copy changes its elision under it;
2624
+ // - `/signed-cmp` (unsignedCompareSpelling OFF) drops the casts at compares and the u32
2625
+ // reconciliation of `varType`. It touches no name and no copy, so no edge copy changes its
2626
+ // elision under it;
2186
2627
  // - `/copy-defpos` (preferDefPosCopyOrder) REORDERS the copies of one edge and adds or drops
2187
2628
  // none. rank.ts states the same thing from the variation side, in the terms that matter there: a
2188
2629
  // reordering cannot rescue a spelling whose OFF sibling failed the boundary contracts.
@@ -2198,12 +2639,16 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
2198
2639
  anchorConstCopies ||
2199
2640
  followEarlyReturns
2200
2641
  ) {
2201
- assertDefaultAccepts(fn, opts, hooks);
2642
+ assertDefaultAccepts(fn, opts, hooks, memo);
2202
2643
  }
2203
2644
  const defs = defOpMap(fn);
2645
+ // The word evaluator's view of `fn`, built at the first `neg_s` (no other rule asks it).
2646
+ let wordCtx: WordCtx | undefined;
2204
2647
  const preds = predecessorBlocks(fn);
2205
- const ipdom = postDominators(fn);
2206
2648
  const dom = dominators(fn);
2649
+ // --- loop discovery (ir/loops.ts): natural loops via dominator back-edges + the nesting forest ---
2650
+ const forest = analyzeLoops(fn, dom);
2651
+ const ipdom = functionPostDominators(fn, forest);
2207
2652
 
2208
2653
  /** The byte step `raise/const.ts` recorded on the literal that feeds this access's address, or
2209
2654
  * `undefined` where the address was not reached by advancing a register. Read at the two
@@ -2213,6 +2658,20 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
2213
2658
  return typeof step === 'number' ? step : undefined;
2214
2659
  };
2215
2660
 
2661
+ /** THE view a `store` (`width` bytes) or `astore` (`elemSize`) lvalue is spelled through, for
2662
+ * both branches below: `storeSigned`, the scalar the recovered pointee was seeded at, so an
2663
+ * inserted cast declares the same type. An UNSTAMPED narrow store of a constant both views store
2664
+ * alike (narrow sign bit clear) takes its base's own int pointee instead and asks for no cast,
2665
+ * since nothing witnessed either type. */
2666
+ const storeView = (op: Op, width: number): boolean => {
2667
+ const t = op.operands[0].type;
2668
+ const own = t.kind === 'ptr' && t.to.kind === 'int' && t.to.width === width * 8 ? t.to.signed : undefined;
2669
+ const v = defs.get(op.operands[op.opcode === 'astore' ? 2 : 1]);
2670
+ const k = v?.opcode === 'const' ? v.attrs.value : undefined;
2671
+ const neutral = typeof k === 'number' && Number.isInteger(k) && k >= 0 && k < 2 ** (width * 8 - 1) && width < 4;
2672
+ return op.attrs.signed === undefined && neutral && own !== undefined ? own : storeSigned(op, width);
2673
+ };
2674
+
2216
2675
  // ── analysis phase (structure/analysis.ts): use registry, liveness, materialization ──
2217
2676
  const { useSitesOf, opIndex, opBlock, liveIn, materialize, preUpdateHomes, reachFrom, emitPos, memWriteBetween } =
2218
2677
  analyze(fn, returnsVoid, {
@@ -2238,7 +2697,9 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
2238
2697
  // rank.ts's `/shared-ret` twin, and as its `/shared-tail` twin after the store-tail sink: as a
2239
2698
  // default it costs rows, measured there.
2240
2699
  //
2241
- // WHICH REGION: the `ret`s reachable from EVERY successor. Every block that cannot reach one of
2700
+ // WHICH REGION: the `ret`s reachable from EVERY successor — or, given an `anchor`, from every
2701
+ // successor that shares a `ret` with the anchor (`sharedRetsOf`; only the `switch_br` caller's
2702
+ // retry passes one, the dispatch's default edge). Every block that cannot reach one of
2242
2703
  // them is an early-return region and is deleted; the follow is `b`'s post-dominator over what is
2243
2704
  // left. Sound for the same reason an ordinary follow is: every kept path from `b` passes it, and a
2244
2705
  // deleted block reaches no kept `ret` and so never the follow, so each arm structures up to the
@@ -2255,7 +2716,8 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
2255
2716
  // PER `if`, never function-wide: the deletion set is a function of that `if`'s own shared `ret`s
2256
2717
  // (memoized on them), because a nested `if` shares a different set, or none.
2257
2718
  //
2258
- // REFUSES (keeping the divergent arms) when no `ret` is reachable from every successor, when the
2719
+ // REFUSES (keeping the divergent arms) when that reading leaves no `ret` — none reachable from
2720
+ // every successor, or from every successor the anchor keeps — when the
2259
2721
  // kept graph gives `b` no post-dominator but EXIT, and when the enclosing region's `stop` is
2260
2722
  // reachable from `b` without passing the follow — that path must reach `stop`, and structuring
2261
2723
  // the arms towards a different follow would emit `stop`'s region inside an arm.
@@ -2267,10 +2729,14 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
2267
2729
  // an in-loop branch reach is reachable from the latch too, so the intersection keeps the whole
2268
2730
  // graph, `pd` is `ipdom` itself — and the caller only asks when `ipdom.get(b)` is already null,
2269
2731
  // 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.
2732
+ // the shape rather than a check, which is why it is written down here. The anchored RETRY that
2733
+ // caller adds is no exception in practice: inside a loop the latch carries every arm to every
2734
+ // `ret`, so the arm that returns shares one with the default edge and is kept, and nothing is
2735
+ // left out (measured on five in-loop switches with one returning arm, compiled with agbcc and
2736
+ // IDO: every one declines). That is a reach this does not have, not a guard.
2271
2737
  const followsByShared = new Map<string, Map<Block, Block | null>>();
2272
- const followOverReturns = (b: Block, stop: Block | null): Block | null => {
2273
- const shared = sharedRetsOf(b, reachFrom);
2738
+ const followOverReturns = (b: Block, stop: Block | null, anchor: Block | null = null): Block | null => {
2739
+ const shared = sharedRetsOf(b, reachFrom, anchor);
2274
2740
  if (shared.length === 0) {
2275
2741
  return null;
2276
2742
  }
@@ -2288,7 +2754,9 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
2288
2754
  if (follow === null || (stop !== null && follow !== stop && reachesAvoiding(b, stop, follow))) {
2289
2755
  return null;
2290
2756
  }
2291
- hooks.onEarlyReturnFollow?.({ block: fn.blocks.indexOf(b), follow: fn.blocks.indexOf(follow) });
2757
+ if (anchor === null) {
2758
+ hooks.onEarlyReturnFollow?.({ block: fn.blocks.indexOf(b), follow: fn.blocks.indexOf(follow) });
2759
+ }
2292
2760
  return follow;
2293
2761
  };
2294
2762
 
@@ -2349,9 +2817,16 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
2349
2817
  const byOff = new Map<number, string>();
2350
2818
  const names = new Map<Op, string>();
2351
2819
  const undefNames = new Map<Op, string>();
2820
+ const undefByKey = new Map<string, string>();
2352
2821
  for (const b of fn.blocks) {
2353
2822
  for (const op of b.ops) {
2354
- if (op.opcode === 'laddr') {
2823
+ if (op.opcode === 'laddr' && op.attrs[OWN_RETURN_OBJECT] === true) {
2824
+ // the function's own return object, which no frame offset names
2825
+ names.set(op, mint('ret'));
2826
+ } else if (op.opcode === 'laddr' && typeof op.attrs[CALL_RETURN_OBJECT] === 'string') {
2827
+ // the struct a call returned in a register, which no frame offset names either
2828
+ names.set(op, mint(`${op.attrs[CALL_RETURN_OBJECT]}_ret`));
2829
+ } else if (op.opcode === 'laddr') {
2355
2830
  const off = op.attrs.off as number;
2356
2831
  let n = byOff.get(off);
2357
2832
  if (n === undefined) {
@@ -2363,7 +2838,23 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
2363
2838
  // Named from the key, like laddr's `sp<off>`: `uninit_sp8` says which frame slot to look
2364
2839
  // at in the assembly, and it stays put where a running counter would renumber every local
2365
2840
  // when an unrelated edit changed the order ops are minted in.
2366
- undefNames.set(op, mint(`uninit_${String(op.attrs.key).replace('@', '')}`));
2841
+ // A `copied` undef is the same storage's content as the undef its key names, so it
2842
+ // shares that one's name (and declaration) once both are minted, below.
2843
+ if (op.attrs.copied !== true) {
2844
+ const n = mint(`uninit_${String(op.attrs.key).replace('@', '')}`);
2845
+ undefNames.set(op, n);
2846
+ undefByKey.set(String(op.attrs.key), n);
2847
+ }
2848
+ }
2849
+ }
2850
+ }
2851
+ for (const b of fn.blocks) {
2852
+ for (const op of b.ops) {
2853
+ if (op.opcode === 'undef' && op.attrs.copied === true) {
2854
+ const key = String(op.attrs.key);
2855
+ const n = undefByKey.get(key) ?? mint(`uninit_${key.replace('@', '')}`);
2856
+ undefByKey.set(key, n);
2857
+ undefNames.set(op, n);
2367
2858
  }
2368
2859
  }
2369
2860
  }
@@ -2450,17 +2941,16 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
2450
2941
  }
2451
2942
  }
2452
2943
  }
2453
- // Declaration-shape OVERRIDE (symbol map): a project-declared array/struct global is an
2454
- // AGGREGATE whatever the usage inference saw — a lone off-0 access to `extern u16 tbl[]`
2455
- // must still spell through the aggregate/array forms, never the bare scalar `tbl`.
2944
+ // Declaration-shape OVERRIDE (symbol map, or the declaration context for a global the map does
2945
+ // not describe): a project-declared array/struct global is an AGGREGATE whatever the usage
2946
+ // inference saw — a lone off-0 access to `extern u16 tbl[]` must still spell through the
2947
+ // aggregate/array forms, never the bare scalar `tbl`.
2456
2948
  // Driven from the FUNCTION's own globals, for the same reason the name minter above consults
2457
2949
  // the map instead of copying it: a name outside `scalarGlobals` has nothing to override.
2458
- if (symbols) {
2459
- for (const n of [...scalarGlobals]) {
2460
- const shape = symbols.get(n)?.shape;
2461
- if (shape === 'array' || shape === 'struct') {
2462
- scalarGlobals.delete(n);
2463
- }
2950
+ for (const n of [...scalarGlobals]) {
2951
+ const shape = symbols?.get(n)?.shape;
2952
+ if (shape === 'array' || shape === 'struct' || contextGlobals?.aggregates.has(n) === true) {
2953
+ scalarGlobals.delete(n);
2464
2954
  }
2465
2955
  }
2466
2956
  }
@@ -2468,162 +2958,16 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
2468
2958
  // Symbol-map rendering context (memAccess/arrayAccess): shape lookups + the env registry for
2469
2959
  // array-shaped globals actually referenced (they surface as SFn.globals — typed, undeclared).
2470
2960
  const shapedGlobalTypes = new Map<string, IrType>();
2471
- const declaredFieldCache = new Map<string, DeclaredField[] | null>();
2472
2961
  const symCtx: SymRenderCtx | undefined = symbols
2473
2962
  ? {
2474
2963
  info: (n) => symbols.get(n),
2475
2964
  noteGlobal: (n, t) => shapedGlobalTypes.set(n, t),
2476
2965
  ptrElements: ptrElementSpellingWanted,
2477
2966
  declRank: declRankSpellingWanted,
2478
- fieldsOf: (n) => {
2479
- const hit = declaredFieldCache.get(n);
2480
- if (hit !== undefined) {
2481
- return hit;
2482
- }
2483
- const si = symbols.get(n);
2484
- const fields =
2485
- si?.shape === 'struct'
2486
- ? declaredFields(si.layout)
2487
- : si?.shape === 'pointer'
2488
- ? pointeeFields(si.pointee)
2489
- : null;
2490
- declaredFieldCache.set(n, fields);
2491
- return fields;
2492
- },
2967
+ fieldsOf: memoFieldsOf((n) => symbols.get(n)),
2493
2968
  }
2494
2969
  : undefined;
2495
2970
 
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));
2618
-
2619
- /** Operands `-`/`~` cannot take as spelled: a rendered pointer, a bare `&gSym`, a pointer
2620
- * global's value. All three are ill-formed C under a unary arithmetic operator — the asm did
2621
- * 32-bit integer math on the address, so that is what gets spelled. */
2622
- const needsIntSpelling = (x: Expr): boolean => ctype(x)?.kind === 'ptr' || x.k === 'addr' || isPtrValue(x);
2623
-
2624
- // --- loop discovery (loops.ts): natural loops via dominator back-edges + the nesting forest ---
2625
- const forest = analyzeLoops(fn, dom);
2626
-
2627
2971
  // GUARDED self-loop headers (a block that is its own successor, entered through a guard-shaped
2628
2972
  // cond_br) — recovered by the guard-fusion + emitWhile un-rotation path below (the gcc "guard +
2629
2973
  // do-while" → `while` shape; countdown/shifts). UNGUARDED self-loops register as single-block
@@ -2715,13 +3059,29 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
2715
3059
  [...forest.byHeader.values()].every(
2716
3060
  (l2) => l2.header === nl.header || !nl.body.has(l2.header) || [...l2.body].every((b) => nl.body.has(b)),
2717
3061
  ) && [...nl.body].every((bb) => bb === nl.header || (preds.get(bb) ?? []).every((p) => nl.body.has(p)));
3062
+ // An edge out of `nl` that does not land on `exit` itself, read the one way both the `while` and the
3063
+ // `while (1)` classifiers read it: a `break` that runs `region` first, or an early return whose
3064
+ // tail the loop emits (`owned`). The break arm is asked first: where the code past `exit` is a pure
3065
+ // `return`, `earlyReturnArm` would also take the arm and copy that post-loop `return` into it, where
3066
+ // the asm holds one copy after the loop (two code arms ahead of `return 3;` would lift as `gHit = i;
3067
+ // return 3;` twice, not a `break` each and one `return 3`). The break arm copies nothing past
3068
+ // `exit` — the duplication policy written at `earlyReturnArm`.
3069
+ // `codeArms` is the classifier's own admission of a code-running break; never out of an inner body.
3070
+ const exitArmOf = (nl: NaturalLoop, e: { from: Block; to: Block }, exit: Block, codeArms: boolean) => {
3071
+ const region =
3072
+ codeArms && !innerBodyHas(nl, e.from) ? codeBreakArm(preds, dom, e.from, e.to, nl.body, exit, true) : null;
3073
+ const owned = region === null ? earlyReturnArm({ dom, reachFrom, preds }, e.from, e.to, nl.body, exit) : null;
3074
+ return { region, owned };
3075
+ };
2718
3076
  const whileLoops = new Map<Block, WhileLoopInfo>();
2719
3077
  const doWhileLoops = new Map<Block, DoWhileInfo>();
3078
+ const keptGuardDoWhiles = new Map<Block, DoWhileInfo>();
2720
3079
  for (const nl of forest.byHeader.values()) {
2721
3080
  const h = nl.header;
2722
- if (nl.selfLoop && loops.has(h)) {
2723
- continue;
2724
- } // guarded self-loops use emitWhile (above); UNGUARDED ones are single-block do-whiles
3081
+ // Guarded self-loops use emitWhile (above); UNGUARDED ones are single-block do-whiles. A
3082
+ // guarded one's do-while reading is kept aside all the same (`keptGuardDoWhiles`), for the
3083
+ // guard that emitWhile cannot fuse and then declines.
3084
+ const guarded = nl.selfLoop && loops.has(h);
2725
3085
  if (!nl.selfLoop && nl.latches.length !== 1) {
2726
3086
  continue;
2727
3087
  } // single latch only
@@ -2751,7 +3111,36 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
2751
3111
  exit: Block,
2752
3112
  kind: 'while' | 'dowhile',
2753
3113
  bodyEntry: Block | null = null;
2754
- if (!nl.selfLoop && hTerm.opcode === 'cond_br' && hInBody.length === 1 && hOut.length === 1 && headerPure) {
3114
+ // The header's way out is not the loop's exit when it enters an arm that runs code and then
3115
+ // joins another exit edge (`codeBreakArm`) whose tail has an effect: that arm is a `break`, and
3116
+ // the loop a `while (1)`. Where the tail only returns, the `while` reading copies it into the
3117
+ // other edge as an early `return`, and keeps that spelling — unless the tail is a bare `return`
3118
+ // and the arm itself runs code. gcc2.7.2's `for (i = 0; i < n; i++) { if (t[i] == 7) { gHit = 1;
3119
+ // break; } }` keeps the arm inside a guarded bottom-tested loop, so the arm is the one in the
3120
+ // loop, and a bare `return` copies no code: `while (t[i] != 7) { ...; if (i >= n) return; }
3121
+ // gHit = 1;` is the spelling the bytes do not hold (breaknend_code 21/26 against 4/18).
3122
+ // The arm is asked in shared mode, as `exitArmOf` asks it for `while (1)`: a break whose tail
3123
+ // another arm or an `else` before the loop also enters is a break all the same, and the `while`
3124
+ // reading that took it for the exit would leave the other exit edge with no spelling.
3125
+ const runsEffect = (x: Block) => [x, ...reachFrom(x)].some((y) => !nl.body.has(y) && y.ops.some(effectful));
3126
+ const hOutIsBreakArm =
3127
+ hOut.length === 1 &&
3128
+ nl.exitEdges.some((e) => {
3129
+ if (e.to === hOut[0].block) {
3130
+ return false;
3131
+ }
3132
+ const region = codeBreakArm(preds, dom, h, hOut[0].block, nl.body, e.to, true);
3133
+ const bareReturn = e.to.ops.length === 1 && e.to.ops[0].opcode === 'ret' && e.to.ops[0].operands.length === 0;
3134
+ return region !== null && (runsEffect(e.to) || (bareReturn && [...region].some((y) => y.ops.some(effectful))));
3135
+ });
3136
+ if (
3137
+ !nl.selfLoop &&
3138
+ hTerm.opcode === 'cond_br' &&
3139
+ hInBody.length === 1 &&
3140
+ hOut.length === 1 &&
3141
+ headerPure &&
3142
+ !hOutIsBreakArm
3143
+ ) {
2755
3144
  kind = 'while';
2756
3145
  exitFrom = h;
2757
3146
  exit = hOut[0].block;
@@ -2780,17 +3169,23 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
2780
3169
  // to know which edges out of the body end an iteration rather than continue it.
2781
3170
  const arms: LoopArm[] = [];
2782
3171
  const breaks = new Set<Block>();
3172
+ const breakArms: BreakArm[] = [];
2783
3173
  let singleExit = true;
2784
3174
  for (const e of nl.exitEdges) {
2785
3175
  if (e.from === exitFrom && e.to === exit) {
2786
3176
  continue;
2787
3177
  }
2788
- const owned = earlyReturnArm({ dom, reachFrom }, e.from, e.to, nl.body, exit);
3178
+ // a `while` takes a code-running break only into an exit that takes no copies: the header's exit
3179
+ // copies are what `breakRule` shares with a break, and an arm's edge into the exit is not one
3180
+ const { region, owned } = exitArmOf(nl, e, exit, kind === 'while' && exit.params.length === 0);
2789
3181
  if (owned) {
2790
3182
  arms.push({ from: e.from, to: e.to, owned });
2791
3183
  }
2792
3184
  if (kind === 'while' && e.to === exit && !innerBodyHas(nl, e.from)) {
2793
3185
  breaks.add(e.from);
3186
+ } else if (region !== null) {
3187
+ breaks.add(e.from);
3188
+ breakArms.push({ from: e.from, to: e.to, region });
2794
3189
  } else if (!owned && (!isRet(e.to) || (kind === 'dowhile' && e.to === exit))) {
2795
3190
  singleExit = false;
2796
3191
  break;
@@ -2810,9 +3205,17 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
2810
3205
  body: nl.body,
2811
3206
  arms,
2812
3207
  breaks,
3208
+ breakArms,
2813
3209
  });
2814
3210
  } else {
2815
- doWhileLoops.set(h, { header: h, latch, exit, forwardPreds: nl.forwardPreds, body: nl.body, arms });
3211
+ (guarded ? keptGuardDoWhiles : doWhileLoops).set(h, {
3212
+ header: h,
3213
+ latch,
3214
+ exit,
3215
+ forwardPreds: nl.forwardPreds,
3216
+ body: nl.body,
3217
+ arms,
3218
+ });
2816
3219
  }
2817
3220
  }
2818
3221
 
@@ -2828,21 +3231,42 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
2828
3231
  // that `return` for the break and the code after the loop for an early return.
2829
3232
  const foreverLoops = new Map<Block, ForeverLoopInfo>();
2830
3233
  const foreverShape = (nl: NaturalLoop): ForeverLoopInfo | null => {
3234
+ // NO EDGE LEAVES: the loop never ends, and C spells that `while (1)` with no `break` and nothing
3235
+ // after it. A block laid out past the loop is reached from elsewhere or not at all, and the
3236
+ // region holding the loop ends at the loop either way (`emitForever`).
3237
+ if (nl.exitEdges.length === 0) {
3238
+ return {
3239
+ header: nl.header,
3240
+ latches: nl.latches,
3241
+ forwardPreds: nl.forwardPreds,
3242
+ exit: null,
3243
+ body: nl.body,
3244
+ arms: [],
3245
+ breaks: new Set(),
3246
+ breakArms: [],
3247
+ };
3248
+ }
2831
3249
  const last = Math.max(...[...nl.body].map((b) => fn.blocks.indexOf(b)));
2832
3250
  const rank = (x: Block) => fn.blocks.indexOf(x) + (fn.blocks.indexOf(x) > last ? 0 : fn.blocks.length);
2833
3251
  const targets = [...new Set(nl.exitEdges.map((e) => e.to))].sort((x, y) => rank(x) - rank(y));
2834
3252
  for (const exit of targets) {
2835
3253
  const arms: LoopArm[] = [];
2836
3254
  const breaks = new Set<Block>();
3255
+ const breakArms: BreakArm[] = [];
2837
3256
  const fits = nl.exitEdges.every((e) => {
2838
3257
  if (e.to === exit) {
2839
3258
  breaks.add(e.from);
2840
3259
  return !innerBodyHas(nl, e.from);
2841
3260
  }
2842
- const owned = earlyReturnArm({ dom, reachFrom }, e.from, e.to, nl.body, exit);
3261
+ const { region, owned } = exitArmOf(nl, e, exit, true);
2843
3262
  if (owned) {
2844
3263
  arms.push({ from: e.from, to: e.to, owned });
2845
3264
  }
3265
+ if (region !== null) {
3266
+ breaks.add(e.from);
3267
+ breakArms.push({ from: e.from, to: e.to, region });
3268
+ return true;
3269
+ }
2846
3270
  // a `ret` target the edge does not own is copied into the body where the edge leaves, so it
2847
3271
  // must hold nothing a second copy would run twice in the source
2848
3272
  return owned !== null || (isRet(e.to) && !e.to.ops.some(effectful));
@@ -2856,6 +3280,7 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
2856
3280
  body: nl.body,
2857
3281
  arms,
2858
3282
  breaks,
3283
+ breakArms,
2859
3284
  };
2860
3285
  }
2861
3286
  }
@@ -2900,85 +3325,33 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
2900
3325
  const vtEnv = (n: string): IrType | undefined => varType.get(n);
2901
3326
  const ctype = (e0: Expr): IrType | undefined => exprCType(e0, vtEnv);
2902
3327
 
2903
- /** A value assigned into a TEMP THIS PASS DECLARES, spelled so the assignment is legal against
2904
- * that declaration. Two inhabitants, one argument: the value's type comes from somewhere this
2905
- * pass does not control, and the temp's comes from here.
2906
- *
2907
- * `&gSym` assigned to a `T *` local: the address of an AGGREGATE is not a pointer to its
2908
- * element. `&gArr` is `T (*)[n]`, `&gStruct` is `struct S *`, and neither is assignable to
2909
- * `T *` — yet the IR's `gaddr` value legitimately has type `T *`, because that is what the asm
2910
- * loaded. The bare spelling therefore states a type the project's own header contradicts.
2911
- *
2912
- * It survived because agbcc only WARNS ("assignment from incompatible pointer type") and
2913
- * computes the right address anyway. That leniency is not something to rely on: the Klonoa
2914
- * project's own build template treats these as fatal, so the row's emitted C does not build
2915
- * where its author would put it. The cast is the always-valid spelling — the same fallback
2916
- * `bareArrayLead` documents for the indexed form — and it is byte-identical, so no benchmark row
2917
- * moves either way and the rule that decides it is pinned in test/deref-typing.test.ts instead.
2918
- *
2919
- * The test is whether `&gSym`'s rendered type PROVABLY equals the destination's, not whether the
2920
- * symbol looks like an aggregate. A SHAPE ENUMERATION MISSES THREE WAYS, each real:
2921
- * `shape:'pointer'` declares a pointer cell (`void *gSym`, or `struct Tag *gSym` when the pointee
2922
- * has a declarable layout), so `&gSym` is a pointer-to-pointer either way; a `shape:'scalar'`
2923
- * whose width differs from the destination's pointee gives `s32 *` for a `u16 *` slot; and a
2924
- * NAME-ONLY symbol is synthesized as `extern u32 gSym;` (declare.ts), which is `u32 *`. So the
2925
- * default is to CAST, and the cast is omitted only
2926
- * where the declared cell type is known and matches exactly. Byte-identical either way, so the
2927
- * cost of casting one time too many is a redundant `(T *)`, never a wrong address. */
2928
- const intoDeclaredTemp = (name: string, value: Expr): Expr => {
2929
- const t = varType.get(name);
2930
- if (t === undefined) {
2931
- return value;
2932
- }
2933
- // ── pointer values whose type this side of the program does not own ────────────────────────
2934
- // A `gaddr` at least states a type the IR knows. `gSym.pBuf` and a pointer global's own 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))) {
2961
- return { k: 'cast', to: t, e: value };
2962
- }
2963
- if (t.kind !== 'ptr' || value.k !== 'addr') {
2964
- return value;
2965
- }
2966
- // The only provably-redundant case: a NON-VOLATILE scalar cell whose DECLARED type is the
2967
- // destination's pointee, where `&gSym` already denotes exactly `T *`.
2968
- //
2969
- // `scalarCellType` and not `scalarTypeForAccess`: the latter answers what an ACCESS of that
2970
- // width reads and collapses every 4-byte access to `s32`, so it called a `u32` cell equal to an
2971
- // `s32 *` destination and let the incompatible assignment through. And a `volatile` cell makes
2972
- // `&gSym` a `volatile T *`, so omitting the cast would DISCARD the qualifier — the same class of
2973
- // fatal-under-a-strict-build defect this rule exists to remove.
2974
- const si = symCtx?.info(value.name);
2975
- if (si?.shape === 'scalar' && !si.volatile && isScalarCellSize(si.size)) {
2976
- if (typeEquals(scalarCellType(si.size, si.signed), t.to)) {
2977
- return value;
2978
- }
2979
- }
2980
- return { k: 'cast', to: t, e: value };
2981
- };
3328
+ // ── POINTER/INTEGER spelling (structure/pointer-spelling.ts) ────────────────────────────────
3329
+ // Whether an operand, a temp's value or a pointer cell's value is written as a pointer or as an
3330
+ // integer, and through which cast. `varType` and `varName` are read live: the naming below is
3331
+ // still declaring temps.
3332
+ const {
3333
+ pointerGlobal,
3334
+ castsIntoGlobal,
3335
+ needsIntSpelling,
3336
+ intoDeclaredTemp,
3337
+ intoPtrCell,
3338
+ arith: spellArith,
3339
+ } = makePointerSpelling({
3340
+ sym: symCtx,
3341
+ pointerGlobals,
3342
+ pointerLoadedGlobals,
3343
+ wordLoadedGlobals,
3344
+ scalarGlobals,
3345
+ varType,
3346
+ compiler: opts,
3347
+ strictPointerConversions,
3348
+ ...(dialect !== undefined ? { dialect } : {}),
3349
+ refuse: (why) => {
3350
+ throw new StructureError(`cannot structure '${fn.name}': ${why}`);
3351
+ },
3352
+ useSitesOf,
3353
+ isNamed: (v) => varName.has(v),
3354
+ });
2982
3355
 
2983
3356
  let fresh = 0;
2984
3357
  // Materialized defs are named FIRST: the temp is the register the compiler held the
@@ -3688,6 +4061,36 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
3688
4061
  }
3689
4062
  }
3690
4063
 
4064
+ // ── a temp holding a pointer global's value, summed with `x + K`, is a byte pointer ──────────
4065
+ // (pointer-spelling.ts `declaresBytePointer` says when.) Decided over every value under the name
4066
+ // once the names are settled, so a name that also holds anything else keeps its integer.
4067
+ if (keepsPointerSumAddend) {
4068
+ const inArgs = (v: Value): Value[] | undefined => {
4069
+ const b = paramBlock.get(v);
4070
+ if (b === undefined || entryParams.has(v)) {
4071
+ return undefined;
4072
+ }
4073
+ const i = b.params.indexOf(v);
4074
+ return [...inEdgeRecords(preds, b)].map(({ succ }) => succ.args[i]);
4075
+ };
4076
+ const ir = {
4077
+ defOf: (v: Value) => defs.get(v),
4078
+ inArgs,
4079
+ usesOf: (v: Value) => (useSitesOf.get(v) ?? []).map((u) => u.op),
4080
+ isNamed: (v: Value) => varName.has(v),
4081
+ };
4082
+ const holders = new Map<string, Value[]>();
4083
+ for (const [v, n] of [...varName, ...backArgName]) {
4084
+ (holders.get(n) ?? holders.set(n, []).get(n)!).push(v);
4085
+ }
4086
+ for (const [n, vs] of holders) {
4087
+ const t = varType.get(n);
4088
+ if (t?.kind === 'int' && t.width === 32 && declaresBytePointer(vs, ir, { pointerGlobal, castsIntoGlobal })) {
4089
+ varType.set(n, T.ptr(T.u(8)));
4090
+ }
4091
+ }
4092
+ }
4093
+
3691
4094
  // ── def-site anchoring of constant merge copies (anchorConstCopies) ──────────────────────────
3692
4095
  // An edge copy `v = K` places the constant where the EDGE is, but the asm often materialized K
3693
4096
  // earlier: `movs r9, #0` at entry ahead of a single-armed overwrite, `movs r5, #1` at the top
@@ -3762,7 +4165,7 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
3762
4165
  // entry consts anchored at their own def sites would still order correctly, and this refuses
3763
4166
  // the shape rather than reasoning about it. The other half of "entered only through its
3764
4167
  // header" needs no test — `analyzeLoops` builds the body as the backward closure from the
3765
- // latches with the header already in it (loops.ts), so a body block's predecessors are all in
4168
+ // latches with the header already in it (ir/loops.ts), so a body block's predecessors are all in
3766
4169
  // the body by construction.
3767
4170
  const singleEntry = (nl: NaturalLoop): boolean => nl.forwardPreds.length === 1;
3768
4171
  for (const M of fn.blocks) {
@@ -3891,12 +4294,70 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
3891
4294
  // hoist. It carries it where ir/discipline.ts `qualifiedBy` says: on its cast, or through the
3892
4295
  // object's declaration and on its cast where it reaches the object through one, since a cast's
3893
4296
  // pointee drops the declaration's qualifiers.
4297
+ //
4298
+ // Whether a cast stands between the access and the object is the PRINTER's question
4299
+ // (backend/cfamily.ts `legalizedIndexBase`), so it is asked of the env the printer judges by:
4300
+ // the declared variables over the globals this structuring typed for the tree (`SFn.globals`,
4301
+ // l3/typing.ts `declaredTypes`). Asked of the variables alone, a global subscript the printer
4302
+ // spells through its own name (`gVA[i]`, already volatile by its declaration) reads as one it
4303
+ // casts, and the qualified cast agbcc compiles differently is put on a name that needed none.
4304
+ const printedType = (n: string): IrType | undefined => varType.get(n) ?? shapedGlobalTypes.get(n);
4305
+ const printedCType = (e0: Expr): IrType | undefined => exprCType(e0, printedType);
4306
+ // A declared access reaches its object through the declaration only where its address is spelled
4307
+ // from the object's own name. Through a local or a parameter — a pointer one of two addresses of
4308
+ // the object joined into, or one a loop steps through it — the declaration qualifies nothing, and
4309
+ // the qualifier goes on a cast.
4310
+ const throughLocal = (access: Expr): boolean => {
4311
+ let b: Expr | undefined = access.k === 'index' || access.k === 'field' ? access.base : undefined;
4312
+ while (b !== undefined) {
4313
+ if (b.k === 'var') {
4314
+ return varType.has(b.name);
4315
+ }
4316
+ if (b.k === 'index' || b.k === 'field') {
4317
+ b = b.base;
4318
+ } else if (b.k === 'cast') {
4319
+ b = b.e;
4320
+ } else if (b.k === 'bin' && (b.op === '+' || b.op === '-')) {
4321
+ b = b.l.k === 'const' ? b.r : b.l;
4322
+ } else {
4323
+ return false;
4324
+ }
4325
+ }
4326
+ return false;
4327
+ };
3894
4328
  const pinnedAccess = (op: Op, access: Expr): Expr => {
3895
4329
  const by = qualifiedBy(op);
3896
- if (by === null || (by === 'declaration' && !castOffDeclaration(access, ctype))) {
4330
+ if (by === null || (by === 'declaration' && !castOffDeclaration(access, printedCType) && !throughLocal(access))) {
3897
4331
  return access;
3898
4332
  }
3899
- const q = qualifiedMemoryAccess(access, ctype);
4333
+ // A bare global's declaration owns its qualifiers. Where the project declares the name — the map
4334
+ // knows it, or a declaration context is in play — a declaration that qualified it would have
4335
+ // placed the access `declared`, so one the lift qualified by a CAST (`device`:
4336
+ // frontend/dead-reads.ts, frontend/device-pins.ts) is declared plain there, and `gR;` compiles
4337
+ // to no read at all: the cast goes on the object's address. A name nothing declares gets the
4338
+ // synthesized declaration, which carries the qualifier (rank-declare.ts `bareGlobalSymbols`),
4339
+ // and keeps the bare spelling — on gcc 2.7.2 the cast compiles to another address sequence.
4340
+ if (
4341
+ by === 'cast' &&
4342
+ access.k === 'var' &&
4343
+ (mapSymbols?.has(access.name) === true || contextGlobals !== undefined)
4344
+ ) {
4345
+ const width = op.attrs.width as number;
4346
+ const signed = op.attrs.signed === true;
4347
+ return {
4348
+ k: 'index',
4349
+ base: {
4350
+ k: 'cast',
4351
+ to: T.ptr(scalarTypeForAccess(width, signed)),
4352
+ volatile: true,
4353
+ e: { k: 'addr', name: access.name },
4354
+ },
4355
+ idx: { k: 'const', value: 0 },
4356
+ width,
4357
+ signed,
4358
+ };
4359
+ }
4360
+ const q = qualifiedMemoryAccess(access, printedCType);
3900
4361
  if (q === null) {
3901
4362
  throw new StructureError(
3902
4363
  `cannot structure '${fn.name}': a ${placedAt(op)} ${op.opcode} that must stay volatile is reached through a ` +
@@ -3906,46 +4367,248 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
3906
4367
  return q;
3907
4368
  };
3908
4369
 
3909
- // Lower ONE def's operation to an Expr, rendering operands through `e`. Shared between the
3910
- // inline-at-use path (exprWith) and the materialized-temp path (sideEffects), so both spell a
3911
- // given op identically.
3912
- const lowerDef = (d: Op, e: (v: Value) => Expr): Expr => {
3913
- if (d.opcode === 'const') {
3914
- return { k: 'const', value: d.attrs.value as number };
4370
+ // A FRAME OBJECT DECLARED AS A UNION (`laddrType`) is read and written through the view of each
4371
+ // access's own width and extension — `sp0.half[1]`, `sp0.word[0]` — on the object itself, never
4372
+ // through a cast on its storage: a cast there is the spelling agbcc may read stale, and the
4373
+ // union is declared because it does not. The audit admitted only accesses at a fixed offset on
4374
+ // the object and aligned to their own width (frame-objects.ts `unionRefusal`), so a view names
4375
+ // each one; an access with none is an internal error, not a decline to fall back from.
4376
+ const frameUnionAccess = (base: Value, off: number, width: number, signed: boolean, isStore: boolean) => {
4377
+ const d = defs.get(base);
4378
+ if (d?.opcode !== 'laddr' || d.attrs.views === undefined) {
4379
+ return null;
3915
4380
  }
3916
- if (d.opcode === 'fconst') {
3917
- return { k: 'fconst', bits: d.attrs.bits as string };
4381
+ const u = laddrType(d);
4382
+ const { view, index } =
4383
+ u.kind === 'union' ? unionView(u, off, width, signed, isStore) : { view: undefined, index: null };
4384
+ if (view === undefined || off % width !== 0) {
4385
+ throw new Error(
4386
+ `internal: a ${width}-byte ${isStore ? 'store' : 'load'} at byte ${off} of the frame union '${laddrName.get(d)}' has no view`,
4387
+ );
3918
4388
  }
3919
- // a bitfield extract recognized over the ops (see the precompute above): the member read,
3920
- // not the shift pair
3921
- const bf = bitfieldSpelling.get(d);
3922
- if (bf) {
3923
- return { k: 'field', base: { k: 'var', name: bf.global }, name: bf.field, dot: true };
4389
+ const cell: Expr = { k: 'field', base: { k: 'var', name: laddrName.get(d)! }, name: view.name, dot: true };
4390
+ return index === null
4391
+ ? cell
4392
+ : ({ k: 'index', base: cell, idx: { k: 'const', value: index }, width, signed, subscript: true } as const);
4393
+ };
4394
+ // A FRAME OBJECT DECLARED AS A STRUCT (a call's return storage or a struct passed by value,
4395
+ // `laddrType`) is read and written through the member an access names exactly — its offset, its
4396
+ // width, and the type the cast it replaces would spell (`spellsAccessType`, a load's alone: a
4397
+ // store truncates to the member's bytes whatever their type); any other access keeps the cast.
4398
+ // Unqualified only: a qualified access keeps the cast its qualifier goes on.
4399
+ const frameStructMember = (op: Op, off: number, width: number, signed: boolean | null): Expr | null => {
4400
+ const d = defs.get(op.operands[0]);
4401
+ const t = d?.results[0].type;
4402
+ if (d?.opcode !== 'laddr' || d.attrs.aggregate !== true || t?.kind !== 'ptr' || t.to.kind !== 'struct') {
4403
+ return null;
3924
4404
  }
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]) };
4405
+ const f = t.to.fields.find((m) => m.off === off);
4406
+ if (f === undefined || f.type.kind !== 'int' || f.type.width !== width * 8 || qualifiedBy(op) !== null) {
4407
+ return null;
3927
4408
  }
3928
- if (d.opcode === 'fneg') {
3929
- return { k: 'un', op: 'f-', e: e(d.operands[0]) };
4409
+ return signed === null || spellsAccessType(f.type.signed, width, signed)
4410
+ ? { k: 'field', base: { k: 'var', name: laddrName.get(d)! }, name: f.name, dot: true }
4411
+ : null;
4412
+ };
4413
+ // A PART OF THE ONE FRAME OBJECT a copy proved one object of the source's (frame-objects.ts
4414
+ // `provenParts`, stamped `part`) is read and written through the element an access names exactly
4415
+ // — at a whole element, inside the array, of the element's width, and of the type the cast it
4416
+ // replaces would spell (a load's alone, as for a struct member) — `sp16[5]`, not
4417
+ // `((u8 *)sp16)[5]`. The two are one C value, and not one compile: with a second array at sp+16
4418
+ // whose address agbcc already holds for its `memcpy`, `b[5]` reads `ldrb [r4, #0x5]` through it
4419
+ // while the cast re-forms `sp + 0x15`. Only a part: the one object's bytes are members and spills
4420
+ // of any type, which the cast spells honestly, and a lone array at sp+0 compiles the same either
4421
+ // way. Unqualified only, like the member.
4422
+ const frameArrayElement = (op: Op, off: number, width: number, signed: boolean | null): Expr | null => {
4423
+ const d = defs.get(op.operands[0]);
4424
+ if (d?.opcode !== 'laddr' || d.attrs.part !== true || d.attrs.views !== undefined) {
4425
+ return null;
3930
4426
  }
3931
- if (CMP_TO_BIN[d.opcode]) {
3932
- // A bare global address `&gSym` as a COMPARISON operand is the same unspelled escape as the
3933
- // arithmetic case below (see intifyAddr): its C type comes from the PROJECT's own
3934
- // declaration, unknowable here. Worse, the compare's SIGNEDNESS lives in the operand types
3935
- // (CMP_TO_BIN maps icmp_ult and icmp_slt to the same '<'), so leaving `&gSym` untyped lets
3936
- // the project's declaration pick the compare the compiler emits — silently byte-inexact
3937
- // whenever it disagrees with the asm. The honest spelling is integer math on the address
3938
- // with the cast AGREEING with the opcode's signedness: unsigned compares (and the
3939
- // sign-agnostic ==/!=) spell `(u32)&gSym`, signed compares `(s32)&gSym` — exactly the
3940
- // compare the asm did. The deref folds never see a compare operand, so no named spelling is
3941
- // lost; a NARROWING cast (`(u8)&gSym`) is not a bare `addr` and keeps its truncation.
3942
- // SCOPE: this handles BARE addr operands. An addr-carrying arithmetic tree
3943
- // (`(u32)&gSym + 4`, spelled by intifyAddr below) renders unsigned and would compare
3944
- // unsigned under an icmp_s*; the signed operand pin at the end of this block catches it as
3945
- // one case of the general rule, needing no addr-specific reasoning.
3946
- const t = /^icmp_s/.test(d.opcode) ? T.s(32) : T.u(32);
3947
- const intifyAddrCmp = (x: Expr): Expr => (x.k === 'addr' ? { k: 'cast', to: t, e: x } : x);
3948
- let l = intifyAddrCmp(e(d.operands[0]));
4427
+ const t = laddrType(d);
4428
+ if (t.kind !== 'array' || t.elem.kind !== 'int' || t.elem.width !== width * 8 || qualifiedBy(op) !== null) {
4429
+ return null;
4430
+ }
4431
+ if (off < 0 || off % width !== 0 || off / width >= (d.attrs.count as number)) {
4432
+ return null;
4433
+ }
4434
+ return signed === null || spellsAccessType(t.elem.signed, width, signed)
4435
+ ? {
4436
+ k: 'index',
4437
+ base: { k: 'var', name: laddrName.get(d)! },
4438
+ idx: { k: 'const', value: off / width },
4439
+ width,
4440
+ signed: t.elem.signed,
4441
+ subscript: true,
4442
+ }
4443
+ : null;
4444
+ };
4445
+ // A FLOATING MEMBER READ AS ITS INTEGER BITS is spelled `*(s32 *)&s`, which C converts wherever
4446
+ // a float is what the reader takes (`gF = *(s32 *)&s;` against `float gF` is `bl __floatsisf` on
4447
+ // agbcc, a `cvt` on MIPS), which the bytes never did. So such a read is spelled only where every
4448
+ // reader is a store into an lvalue whose integer type this lift states (`floatBitsStoreTarget`):
4449
+ // a whole-struct copy lowered to words, `((s32 *)&gV3)[1] = ((s32 *)&s)[1];`, passes the bits
4450
+ // through. Any other reader (a named global, a declared member, a call, a return, a join)
4451
+ // declines. A store of bits into such a member is the bits, and keeps the cast.
4452
+ const floatMemberBits = (op: Op): { name: string; width: number } | null => {
4453
+ if (op.opcode !== 'load') {
4454
+ return null;
4455
+ }
4456
+ const width = op.attrs.width as number;
4457
+ const t = defs.get(op.operands[0])?.results[0].type;
4458
+ const f = t?.kind === 'ptr' && t.to.kind === 'struct' ? t.to.fields.find((m) => m.off === op.attrs.off) : undefined;
4459
+ return f?.type.kind === 'float' && f.type.width === width * 8 ? { name: f.name, width } : null;
4460
+ };
4461
+ const refuseFloatBits = (m: { name: string; width: number }): never => {
4462
+ throw new StructureError(
4463
+ `a ${m.width}-byte read of the floating member '${m.name}' of a struct local is an integer here, and C ` +
4464
+ 'would convert it where a float is read — not modelled',
4465
+ );
4466
+ };
4467
+ const refuseFloatMemberRead = (op: Op): void => {
4468
+ const m = floatMemberBits(op);
4469
+ const v = op.results[0];
4470
+ const storedOnly = (useSitesOf.get(v) ?? []).every(
4471
+ ({ op: u }) =>
4472
+ (u.opcode === 'store' && u.operands[1] === v && u.operands[0] !== v) ||
4473
+ (u.opcode === 'astore' && u.operands[2] === v && u.operands[0] !== v && u.operands[1] !== v),
4474
+ );
4475
+ if (m !== null && !storedOnly) {
4476
+ refuseFloatBits(m);
4477
+ }
4478
+ };
4479
+ // …and the store side: the lvalue must be one whose type the lift spells, an integer of the
4480
+ // read's width: a cast cell (`((s32 *)&g)[1]`), or a local's or a parameter's cell. A bare global
4481
+ // (`gF = …`, `gA[i] = …`) has the project's type, whatever that is.
4482
+ const floatBitsStoreTarget = (value: Value, lval: Expr): void => {
4483
+ const d = defs.get(value);
4484
+ const m = d === undefined ? null : floatMemberBits(d);
4485
+ if (m === null) {
4486
+ return;
4487
+ }
4488
+ let root: Expr = lval;
4489
+ while (root.k === 'index' || root.k === 'field') {
4490
+ root = root.base;
4491
+ }
4492
+ const t = ctype(lval);
4493
+ const own = root.k !== 'var' || varType.has(root.name);
4494
+ if (!own || (lval.k !== 'index' && lval.k !== 'field') || t?.kind !== 'int' || t.width !== m.width * 8) {
4495
+ refuseFloatBits(m);
4496
+ }
4497
+ };
4498
+ // A STRUCT LOCAL'S BYTES STORED INTO A BARE ARRAY ELEMENT keep the cast cell. The bare `gA[i]`
4499
+ // (globalaccess.ts `bareArrayLead`) checks the element's width, not its kind, and the bytes of
4500
+ // `gA[i] = mk(x);` over `struct S gA[4]` are the bytes of a scalar array's store: `gA[a1] =
4501
+ // *(s32 *)&mk_ret;` assigns an int to the struct element (agbcc: incompatible types in
4502
+ // assignment). `((s32 *)&gA)[a1]` addresses the same bytes under any declaration. A recovered row
4503
+ // subscript (`gA[r][i]`) has no flat index here to put in the cell, so it declines.
4504
+ // ONLY an array-declared symbol: a store through a POINTER global's value (`gIP[i] = s.x`) has
4505
+ // the same `var` base, and `&gIP` would address the pointer variable, not its pointee.
4506
+ const structBytesCell = (value: Value, lval: Expr): Expr => {
4507
+ const d = defs.get(value);
4508
+ const local = d?.opcode === 'load' ? defs.get(d.operands[0]) : undefined;
4509
+ const t = local?.results[0].type;
4510
+ const fromStruct = local?.opcode === 'laddr' && t?.kind === 'ptr' && t.to.kind === 'struct';
4511
+ if (
4512
+ !fromStruct ||
4513
+ lval.k !== 'index' ||
4514
+ lval.base.k !== 'var' ||
4515
+ varType.has(lval.base.name) ||
4516
+ symCtx?.info(lval.base.name)?.shape !== 'array'
4517
+ ) {
4518
+ return lval;
4519
+ }
4520
+ if (lval.lead?.some((l) => l.k !== 'const' || l.value !== 0)) {
4521
+ throw new StructureError(
4522
+ `a struct local's bytes stored into an element of '${lval.base.name}' by a row subscript have no cast cell to spell`,
4523
+ );
4524
+ }
4525
+ const { lead: _lead, ...cell } = lval;
4526
+ return { ...cell, base: { k: 'addr', name: lval.base.name } };
4527
+ };
4528
+ // …and with no qualifier: `pinnedAccess` would put one on through a cast, the very spelling, and
4529
+ // the audit refuses the union where its accesses would need one (`unionRefusal`)
4530
+ const unqualifiedView = (op: Op, viewed: Expr): Expr => {
4531
+ if (qualifiedBy(op) !== null) {
4532
+ throw new Error(
4533
+ `internal: '${fn.name}': a ${placedAt(op)} ${op.opcode} on a frame union must stay volatile, ` +
4534
+ 'which no view of the union carries',
4535
+ );
4536
+ }
4537
+ return viewed;
4538
+ };
4539
+
4540
+ // Lower ONE def's operation to an Expr, rendering operands through `e`. Shared between the
4541
+ // inline-at-use path (exprWith) and the materialized-temp path (sideEffects), so both spell a
4542
+ // given op identically.
4543
+ const lowerDef = (d: Op, e: (v: Value) => Expr): Expr => {
4544
+ if (d.opcode === 'const') {
4545
+ return { k: 'const', value: d.attrs.value as number };
4546
+ }
4547
+ if (d.opcode === 'fconst') {
4548
+ return { k: 'fconst', bits: d.attrs.bits as string };
4549
+ }
4550
+ // a bitfield extract recognized over the ops (see the precompute above): the member read,
4551
+ // not the shift pair
4552
+ const bf = bitfieldSpelling.get(d);
4553
+ if (bf) {
4554
+ return { k: 'field', base: { k: 'var', name: bf.global }, name: bf.field, dot: true };
4555
+ }
4556
+ if (FLOAT_TO_BIN[d.opcode]) {
4557
+ return { k: 'bin', op: FLOAT_TO_BIN[d.opcode], l: e(d.operands[0]), r: e(d.operands[1]) };
4558
+ }
4559
+ if (d.opcode === 'fneg') {
4560
+ return { k: 'un', op: 'f-', e: e(d.operands[0]) };
4561
+ }
4562
+ if (FCMP_TO_BIN[d.opcode] || FCMP_TO_BIN[NEGATED_FCMP[d.opcode] ?? '']) {
4563
+ const op = FCMP_TO_BIN[d.opcode] ?? FCMP_TO_BIN[NEGATED_FCMP[d.opcode]];
4564
+ const spelled = typeof d.attrs.relation === 'string' ? FCMP_TO_BIN[d.attrs.relation] : undefined;
4565
+ const rel: Expr = {
4566
+ k: 'bin',
4567
+ op,
4568
+ l: e(d.operands[0]),
4569
+ r: e(d.operands[1]),
4570
+ ...(spelled !== undefined ? { helperRel: spelled } : {}),
4571
+ };
4572
+ return FCMP_TO_BIN[d.opcode] ? rel : { k: 'un', op: '!', e: rel };
4573
+ }
4574
+ // A conversion is C's cast to the type it converts to. Converting an integer, the cast's
4575
+ // operand must BE signed, because C converts an unsigned one with the unsigned conversion,
4576
+ // which agbcc open-codes and an FPU computes differently: the pin `icmp_s*` takes below. A cast
4577
+ // does not promote its operand, so a `u8` or `u16` name is unsigned here, where it would
4578
+ // promote to `int` in arithmetic: agbcc compiles `(float)c` over an `unsigned char` as the
4579
+ // unsigned conversion and `(float)(s32)c` as `__floatsisf`.
4580
+ if (FLOAT_TO_INT.has(d.opcode)) {
4581
+ const w = intWidth(d.results[0].type) === WIDE_BITS ? WIDE_BITS : 32;
4582
+ return { k: 'cast', to: T.int(w, d.opcode === 'fptosi'), e: e(d.operands[0]) };
4583
+ }
4584
+ if (INT_TO_FLOAT.has(d.opcode)) {
4585
+ const x = e(d.operands[0]);
4586
+ const own = ['var', 'postincr', 'cast', 'index', 'field'].includes(x.k) ? exprCType(x, vtEnv) : undefined;
4587
+ const isSigned = own?.kind === 'int' && own.width < 32 ? own.signed : renderedIntSignedness(x, vtEnv) === true;
4588
+ const signed: Expr = isSigned ? x : { k: 'cast', to: T.s(exprIntWidth(x, vtEnv)), e: x };
4589
+ return { k: 'cast', to: d.results[0].type, e: signed };
4590
+ }
4591
+ if (FLOAT_RESIZE[d.opcode]) {
4592
+ return { k: 'cast', to: d.results[0].type, e: e(d.operands[0]) };
4593
+ }
4594
+ if (CMP_TO_BIN[d.opcode]) {
4595
+ // A bare global address `&gSym` as a COMPARISON operand is the same unspelled escape as the
4596
+ // arithmetic case (pointer-spelling.ts ARITH_ROWS): its C type comes from the PROJECT's own
4597
+ // declaration, unknowable here. Worse, the compare's SIGNEDNESS lives in the operand types
4598
+ // (CMP_TO_BIN maps icmp_ult and icmp_slt to the same '<'), so leaving `&gSym` untyped lets
4599
+ // the project's declaration pick the compare the compiler emits — silently byte-inexact
4600
+ // whenever it disagrees with the asm. The honest spelling is integer math on the address
4601
+ // with the cast AGREEING with the opcode's signedness: unsigned compares (and the
4602
+ // sign-agnostic ==/!=) spell `(u32)&gSym`, signed compares `(s32)&gSym` — exactly the
4603
+ // compare the asm did. The deref folds never see a compare operand, so no named spelling is
4604
+ // lost; a NARROWING cast (`(u8)&gSym`) is not a bare `addr` and keeps its truncation.
4605
+ // SCOPE: this handles BARE addr operands. An addr-carrying arithmetic tree
4606
+ // (`(u32)&gSym + 4`, spelled by the arithmetic table) renders unsigned and would compare
4607
+ // unsigned under an icmp_s*; the signed operand pin at the end of this block catches it as
4608
+ // one case of the general rule, needing no addr-specific reasoning.
4609
+ const t = /^icmp_s/.test(d.opcode) ? T.s(32) : T.u(32);
4610
+ const intifyAddrCmp = (x: Expr): Expr => (x.k === 'addr' ? { k: 'cast', to: t, e: x } : x);
4611
+ let l = intifyAddrCmp(e(d.operands[0]));
3949
4612
  let r = intifyAddrCmp(e(d.operands[1]));
3950
4613
  // The same signedness hole for ORDINARY operands: an icmp_u* whose operands both render
3951
4614
  // as signed-promoting C (an s32-declared var carrying a u32 value — declarations take the
@@ -3964,15 +4627,14 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
3964
4627
  const t2 = ctype(x);
3965
4628
  return t2?.kind === 'ptr' || t2?.kind === 'array';
3966
4629
  };
3967
- if (
3968
- unsignedCompareSpelling &&
3969
- /^icmp_u/.test(d.opcode) &&
3970
- !ptrSide(l) &&
3971
- !ptrSide(r) &&
3972
- renderedIntSignedness(l, vtEnv) !== false &&
3973
- renderedIntSignedness(r, vtEnv) !== false &&
3974
- !(provablyNonNegative(l, vtEnv) && provablyNonNegative(r, vtEnv))
3975
- ) {
4630
+ // `pinned` places the cast (l3/typing.ts `spelledNonNegative`). Whether an uncast spelling
4631
+ // compares different values is not decided here: the compare carries `unsignedCmp` evidence,
4632
+ // and rank.ts asks `signedOverUnsignedCompare` of the finished tree, after every pass that
4633
+ // could substitute into it. The two differ on an `|`/`^` of non-negative sides, which stays
4634
+ // pinned (the pin is codegen-visible) and owes no proof uncast.
4635
+ const unsignedOrdering = /^icmp_u(lt|le|gt|ge)$/.test(d.opcode);
4636
+ const pinned = /^icmp_u/.test(d.opcode) && rendersSignedOverUnsigned(l, r, vtEnv, spelledNonNegative);
4637
+ if (pinned && unsignedCompareSpelling) {
3976
4638
  const irUnsigned = (v: Value): boolean => v.type.kind === 'int' && !v.type.signed;
3977
4639
  // AT THE OPERAND'S OWN RANK: `(u32)a` over a 64-bit `a` pins nothing, it truncates the
3978
4640
  // compare to its low half. `exprIntWidth` is total and answers 64 only where it proves it.
@@ -4020,298 +4682,11 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
4020
4682
  r = { k: 'cast', to: T.u(32), e: r };
4021
4683
  }
4022
4684
  }
4023
- return { k: 'bin', op: CMP_TO_BIN[d.opcode], l, r };
4685
+ return { k: 'bin', op: CMP_TO_BIN[d.opcode], l, r, ...(unsignedOrdering ? { unsignedCmp: true as const } : {}) };
4024
4686
  }
4025
4687
  if (ARITH_TO_BIN[d.opcode]) {
4026
- let l = e(d.operands[0]);
4027
- let r = d.operands.length === 2 ? e(d.operands[1]) : ({ k: 'const', value: d.attrs.imm as number } as Expr);
4028
- // Commutative LOAD-PAIR operands re-spell in EVALUATION order. A commutative instruction's
4029
- // operand order is an allocator artifact (`mul r0, r0, r2` reads dst-first, so the lift's
4030
- // l/r is whichever load landed in the dst), but the order the compiler EVALUATED the
4031
- // operands is still visible: their defs' order in the instruction stream. gcc 2.9 and IDO
4032
- // both emit `w * h`'s loads w-first, so def order IS source order — verified byte-identical
4033
- // on both (the bg_area rows). Scope, one gate per way the signal fails: BOTH root defs must
4034
- // be same-block memory reads (a const already has its side; arithmetic defs get combined
4035
- // out of source order — an ldmia-fed add reads def-reordered; cross-block positions do not
4036
- // order evaluation), neither stamped `listOrder` (an ldmia-expanded load's own position is
4037
- // LIST order), both operand VALUES un-named (see below), no pointer side (load-bearing for
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)));
4045
- if (COMMUTATIVE_BIN.has(ARITH_TO_BIN[d.opcode]) && d.operands.length === 2) {
4046
- const [da, db] = [defs.get(d.operands[0]), defs.get(d.operands[1])];
4047
- if (
4048
- defOrderLoadPairs &&
4049
- da &&
4050
- db &&
4051
- (da.opcode === 'load' || da.opcode === 'aload') &&
4052
- (db.opcode === 'load' || db.opcode === 'aload') &&
4053
- da.attrs.listOrder !== true &&
4054
- db.attrs.listOrder !== true &&
4055
- // NAMED values decline: a value that renders as a name here — a materialized def
4056
- // (varName), a loop-carried value in a post-loop region (activeSub) — was evaluated at
4057
- // its def statement, so re-ordering the reference re-orders nothing and only churns the
4058
- // spelling away from the machine order the allocator saw. An inlined def — a deref, a
4059
- // field, a bare scalar global (whose `var` node lowerDef itself mints) — evaluates at
4060
- // THIS site, wherever recovery spells it.
4061
- !varName.has(d.operands[0]) &&
4062
- !varName.has(d.operands[1]) &&
4063
- activeSub?.has(d.operands[0]) !== true &&
4064
- activeSub?.has(d.operands[1]) !== true &&
4065
- ctype(l)?.kind !== 'ptr' &&
4066
- ctype(r)?.kind !== 'ptr' &&
4067
- !ptrGlobalSide(l) &&
4068
- !ptrGlobalSide(r) &&
4069
- !exprHasEffect(l) &&
4070
- !exprHasEffect(r) &&
4071
- opBlock.get(da) === opBlock.get(db) &&
4072
- opIndex.get(da)! > opIndex.get(db)!
4073
- ) {
4074
- [l, r] = [r, l];
4075
- }
4076
- }
4077
- // Pointer stride: C pointer arithmetic is ELEMENT-scaled, but the asm added a BYTE
4078
- // constant — `addi p,4` on an `s32*` walks 1 element, yet C `p + 4` walks 4. Divide the byte
4079
- // constant by the pointee size so the walk recompiles to the same address math.
4080
- //
4081
- // Keyed on the operand's RENDERED C type, never the IR value's recovered type: C scales by
4082
- // the type of the expression it actually sees, and the two diverge exactly like memAccess's
4083
- // deref bases (a value recovered `s32*` can render as an int-typed tree — C then does NO
4084
- // element scaling, so pre-dividing the constant would bake in a WRONG address that the
4085
- // deref cast downstream turns into silently-wrong bytes).
4086
- // An int-rendered walk keeps its raw byte constant and derefs through the access-width cast.
4087
- // Fires only for a rendered pointer whose element size (>1) DIVIDES the constant exactly;
4088
- // otherwise raw (a misaligned/struct-array stride is left as-is; a `u8*` is size 1 so
4089
- // unchanged). Since C `(K/es) + p == p + (K/es)`, scaling the const on whichever side it
4090
- // sits fixes the bytes: `add` is commutative so the pointer may be either operand; `sub` is
4091
- // not, so only operand[0] (the minuend) may be the pointer.
4092
- // A rendered pointer that CANNOT express the byte constant as a whole number of elements —
4093
- // an inexact residual, or a pointee whose size is not knowable here (a struct) — has no
4094
- // scaled spelling at all, and leaving the raw byte count is the silent wrongness the
4095
- // pointer-global rule below refuses in the same words: C multiplies it back, so `p + 62` on
4096
- // an `s32 *` addresses byte 248 and `p + 38` on a `struct S *` addresses byte 38 * sizeof(S).
4097
- // Same answer as there — CAST THEN ADD, `(u8 *)p + 62`, the same address in every world.
4098
- const bytePtr = (x: Expr): Expr => ({ k: 'cast', to: T.ptr(T.u(8)), e: x });
4099
- // Set when a byte-pointer cast was applied for the ADDRESS math alone: the sum is cast back
4100
- // to the pointer type it started as, so the walk changes the arithmetic and nothing else. A
4101
- // bare `u8 *` sum would be a different C type from the slot it lands in (`v4 = (u8 *)a0 +
4102
- // (v1 << 2)` into an `s32 *` — an mwcc error) and from the bases the deref rules read.
4103
- let restoreTo: IrType | undefined;
4104
- const walk = (base: Expr, c: Extract<Expr, { k: 'const' }>): { base: Expr; off: Expr } => {
4105
- const t = ctype(base);
4106
- if (t?.kind !== 'ptr') {
4107
- return { base, off: c }; // an int-rendered walk: C scales nothing, the bytes are right
4108
- }
4109
- const es = ptrElemBytes(t.to);
4110
- if (es === 1) {
4111
- return { base, off: c }; // already a byte pointer
4112
- }
4113
- return es > 1 && c.value % es === 0
4114
- ? { base, off: { k: 'const', value: c.value / es } }
4115
- : { base: bytePtr(base), off: c };
4116
- };
4117
- // A RUNTIME byte offset has no element spelling at all — not even an inexact one to reject,
4118
- // since the residual is unknown until the program runs. The asm added bytes, so the same
4119
- // answer as the inexact constant above: cast then add. Without it a `u16 *` walked by a
4120
- // computed offset addresses TWICE the intended byte, and nothing downstream can see the
4121
- // error — in the sa3 decomp that address is what a `CpuSet` call writes THROUGH.
4122
- //
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.
4127
- //
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.
4131
- const walkVar = (x: Expr): IrType | undefined => {
4132
- const t = ctype(x);
4133
- return t?.kind === 'ptr' && ptrElemBytes(t.to) !== 1 ? t : undefined;
4134
- };
4135
- if ((d.opcode === 'add' || d.opcode === 'sub') && r.k === 'const') {
4136
- ({ base: l, off: r } = walk(l, r));
4137
- } else if (d.opcode === 'add' && d.operands.length === 2 && l.k === 'const') {
4138
- ({ base: r, off: l } = walk(r, l)); // commuted `const + ptr`
4139
- } else if (d.opcode === 'add' || d.opcode === 'sub') {
4140
- // ONE side only. `ptr - ptr` is C's element difference and `ptr + ptr` is not C at all;
4141
- // both are the intify rules' business below, and casting both operands here would hide
4142
- // the shape from them.
4143
- const lp = ctype(l)?.kind === 'ptr';
4144
- const rp = d.operands.length === 2 && ctype(r)?.kind === 'ptr';
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) {
4151
- restoreTo = walkVar(l);
4152
- l = restoreTo ? bytePtr(l) : l;
4153
- } else if (rp && !lp && d.opcode === 'add') {
4154
- restoreTo = walkVar(r);
4155
- r = restoreTo ? bytePtr(r) : r;
4156
- }
4157
- }
4158
- // C rejects a pointer operand outright under the non-additive operators (& | ^ << >> * / %),
4159
- // under `ptr + ptr`, and as the subtrahend of `int - ptr` — the asm just does 32-bit integer
4160
- // math on the address, so the honest spelling is the value cast to its integer self. Only a
4161
- // DEFINITELY-pointer rendering is cast (same conservative direction as memAccess); the
4162
- // additive ops keep C's legal pointer arithmetic untouched.
4163
- const op = ARITH_TO_BIN[d.opcode];
4164
- const intify = (x: Expr): Expr =>
4165
- restoredIntSum(x, false) ?? (ctype(x)?.kind === 'ptr' ? { k: 'cast', to: T.s(32), e: x } : x);
4166
- if (!['+', '-', '&&', '||'].includes(op)) {
4167
- l = intify(l);
4168
- r = intify(r);
4169
- } else if (op === '+' && ctype(l)?.kind === 'ptr' && ctype(r)?.kind === 'ptr') {
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
- }
4177
- } else if (op === '-' && ctype(l)?.kind !== 'ptr' && ctype(r)?.kind === 'ptr') {
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);
4185
- }
4186
- // A bare global address `&gSym` under ANY of these operators is never emitted as-is: its C
4187
- // type comes from the PROJECT's own declaration (unknowable here — exprCType types `addr`
4188
- // undefined, so the ptr-keyed intify above never fires on it), which makes `&gSym + K`
4189
- // byte-INEXACT (C scales K by sizeof(gSym)) and `&gSym & K` ill-formed. The honest spelling
4190
- // is integer math on the address — `(u32)&gSym + K`, exactly the arithmetic the asm did.
4191
- // The deref folds (globalOf / globalConstByte, via addrIn) look through this cast, so every
4192
- // access that CAN spell a named element/field still does; only a genuine value-context
4193
- // escape (a call argument, a stored address, a compare) keeps it — previously such an
4194
- // escape tripped assertDerefsTyped's interior-pointer rule and declined the whole function.
4195
- const intifyAddr = (x: Expr): Expr => (x.k === 'addr' ? { k: 'cast', to: T.u(32), e: x } : x);
4196
- l = intifyAddr(l);
4197
- r = intifyAddr(r);
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
4202
- // spelling makes the stride explicit: CAST-THEN-ADD, `(u8 *)gPtr + K`, the same address in
4203
- // EVERY world. Add-then-cast (`(u8 *)(gPtr + K)`, what the backend's deref legalization
4204
- // would otherwise produce) is byte-correct in exactly one of them — a silent wrongness, the
4205
- // class this project refuses. A MEMBER is the case with no world in which the raw spelling
4206
- // is right: the map declares the pointee width, so `bytes + gSym.pBuf` on a `u16 *` scales
4207
- // the residual a SECOND time and addresses twice the byte the asm did.
4208
- // NOT foldable into the deref index either: `((u8 *)gPtr)[K + off]` re-scales K by the
4209
- // ACCESS width, a different address whenever that width is not 1.
4210
- // Under the non-additive operators C rejects a pointer outright, so there the honest
4211
- // spelling is integer math on the cell — exactly intifyAddr's `(u32)&gSym` rule.
4212
- const intifyPtrValue = globalWord;
4213
- if (op === '+' || op === '-') {
4214
- // `ptr ± int` and `ptr - ptr` are byte arithmetic once both sides are byte pointers;
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);
4278
- }
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)) {
4293
- r = bothPtr && op === '-' ? bytePtr(r) : op === '+' && !bothPtr ? bytePtr(r) : intifyPtrValue(r);
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
- }
4303
- } else if (op !== '&&' && op !== '||') {
4304
- // (`&&`/`||` take a pointer operand legally — a truth test, no arithmetic.)
4305
- l = isPtrValue(l) ? intifyPtrValue(l) : l;
4306
- r = isPtrValue(r) ? intifyPtrValue(r) : r;
4307
- }
4308
- // (The signedness-carrying pairs stay DISTINCT ops here — `>>>`/`>>` and `/u` `%u`/`/` `%`.
4309
- // Which token a language spells each with, and what cast pins the choice, is a BACKEND
4310
- // decision; see l3/ast.ts BinOp and backend/cfamily.ts's C_SPELLING.)
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).
4314
- //
4688
+ const l = e(d.operands[0]);
4689
+ const r = d.operands.length === 2 ? e(d.operands[1]) : ({ k: 'const', value: d.attrs.imm as number } as Expr);
4315
4690
  // A `+`/`|` over two IR `const`s that both RENDER as literals is the literal it is. After
4316
4691
  // pre-recovery there is only one way such an op still exists: `raise/const.ts` refuses to fold
4317
4692
  // one shape — a register the compiler held live across a branch, whose value on this path is a
@@ -4323,7 +4698,8 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
4323
4698
  // stake, so `const-fold.test.ts` pins this on the emitted STRING. Re-folding HERE rather than
4324
4699
  // back in the IR is the point: the pair must survive pre-recovery for the enumeration gate to
4325
4700
  // see the merge feed, and only at rendering is it settled that this candidate named neither
4326
- // half.
4701
+ // half. Two literals are spelled as they are by the arithmetic table, so the fold reads them
4702
+ // before it.
4327
4703
  //
4328
4704
  // `foldsFromIrConsts` keeps the reach honest and is not redundant with `l`/`r` being `const`
4329
4705
  // Exprs: a BLOCK PARAMETER resolved to a constant on this arm also renders as a literal, and
@@ -4351,12 +4727,52 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
4351
4727
  if (l.k === 'const' && r.k === 'const' && d.operands.every((o) => foldsFromIrConsts(o))) {
4352
4728
  const value = foldConstPair(d.opcode, l.value, r.value);
4353
4729
  if (value !== null) {
4354
- const folded: Expr = { k: 'const', value };
4355
- return restoreTo ? { k: 'cast', to: restoreTo, e: folded } : folded;
4730
+ return { k: 'const', value };
4356
4731
  }
4357
4732
  }
4358
- const sum: Expr = { k: 'bin', op, l, r };
4359
- return restoreTo ? { k: 'cast', to: restoreTo, e: sum } : sum;
4733
+ // Commutative LOAD-PAIR operands re-spell in EVALUATION order. A commutative instruction's
4734
+ // operand order is an allocator artifact (`mul r0, r0, r2` reads dst-first, so the lift's
4735
+ // l/r is whichever load landed in the dst), but the order the compiler EVALUATED the
4736
+ // operands is still visible: their defs' order in the instruction stream. gcc 2.9 and IDO
4737
+ // both emit `w * h`'s loads w-first, so def order IS source order — verified byte-identical
4738
+ // on both (the bg_area rows). Scope, one gate per way the signal fails: BOTH root defs must
4739
+ // be same-block memory reads (a const already has its side; arithmetic defs get combined
4740
+ // out of source order — an ldmia-fed add reads def-reordered; cross-block positions do not
4741
+ // order evaluation), neither stamped `listOrder` (an ldmia-expanded load's own position is
4742
+ // LIST order), both operand VALUES un-named (see below), and no effect moves (call, marker).
4743
+ // Which sides may move is the arithmetic's own decision (structure/pointer-spelling.ts
4744
+ // `pointerSide`, load-bearing for the stride rules), so the pair is handed to it.
4745
+ const [da, db] = [defs.get(d.operands[0]), defs.get(d.operands[1])];
4746
+ const loadPairReversed =
4747
+ COMMUTATIVE_BIN.has(ARITH_TO_BIN[d.opcode]) &&
4748
+ d.operands.length === 2 &&
4749
+ defOrderLoadPairs &&
4750
+ da !== undefined &&
4751
+ db !== undefined &&
4752
+ (da.opcode === 'load' || da.opcode === 'aload') &&
4753
+ (db.opcode === 'load' || db.opcode === 'aload') &&
4754
+ da.attrs.listOrder !== true &&
4755
+ db.attrs.listOrder !== true &&
4756
+ // NAMED values decline: a value that renders as a name here — a materialized def
4757
+ // (varName), a loop-carried value in a post-loop region (activeSub) — was evaluated at
4758
+ // its def statement, so re-ordering the reference re-orders nothing and only churns the
4759
+ // spelling away from the machine order the allocator saw. An inlined def — a deref, a
4760
+ // field, a bare scalar global (whose `var` node lowerDef itself mints) — evaluates at
4761
+ // THIS site, wherever recovery spells it.
4762
+ !varName.has(d.operands[0]) &&
4763
+ !varName.has(d.operands[1]) &&
4764
+ activeSub?.has(d.operands[0]) !== true &&
4765
+ activeSub?.has(d.operands[1]) !== true &&
4766
+ !exprHasEffect(l) &&
4767
+ !exprHasEffect(r) &&
4768
+ opBlock.get(da) === opBlock.get(db) &&
4769
+ opIndex.get(da)! > opIndex.get(db)!;
4770
+ // The operand order, the pointer stride, the integer legalizations and the pointer-value
4771
+ // byte arithmetic: structure/pointer-spelling.ts `arith`. (The signedness-carrying pairs
4772
+ // stay DISTINCT ops — `>>>`/`>>` and `/u` `%u`/`/` `%`. Which token a language spells each
4773
+ // with, and what cast pins the choice, is a BACKEND decision; see l3/ast.ts BinOp and
4774
+ // backend/cfamily.ts's C_SPELLING.)
4775
+ return spellArith(d, l, r, loadPairReversed);
4360
4776
  }
4361
4777
  // `-`/`~` on a pointer rendering is equally not C — same honest integer cast as above.
4362
4778
  if (d.opcode === 'rotr' || d.opcode === 'rotl') {
@@ -4390,6 +4806,37 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
4390
4806
  r: { k: 'bin', op: far, l: x, r: w },
4391
4807
  };
4392
4808
  }
4809
+ if (d.opcode === 'neg_s') {
4810
+ // The MATHEMATICAL negation (ir/opcodes.ts), so it must not be spelled over an unsigned 32-bit
4811
+ // operand: `-(u32)b` wraps, and at b == 0x80000000 a signed compare reads `(s32)-b` as INT_MIN
4812
+ // where the machine's sum was +2^31. Negating the operand as s32 instead is exact wherever C
4813
+ // defines it — it is undefined at exactly that one input. The operand keeps its own spelling
4814
+ // where it already renders signed (`renderedIntSignedness`, the rule the icmp_s* operand
4815
+ // pin uses); a cast it did not need costs no byte.
4816
+ //
4817
+ // That one input is a value the operand MIGHT hold. An operand that ALWAYS holds it is a
4818
+ // different thing: every call then lands on the undefined input, and the C a compiler makes
4819
+ // of `-(s32)0x80000000` (INT_MIN again) is not the machine's +2^31. Whether it always does is
4820
+ // what the compiler can FOLD, not which opcodes built it — a pool word, `1 << 31`, a
4821
+ // `sub`/`orr`/`eor` of constants, a param both arms of a diamond set, a reload of a store
4822
+ // nothing can have overwritten, a reload of memory this function wrote on every path through
4823
+ // an address it cannot match — so the question goes to the shared word evaluator, and fails
4824
+ // closed: a constant it cannot compute refuses too.
4825
+ // An operand that is INT_MIN on some paths only still lifts; that is the "might hold" case.
4826
+ // Only hand-written asm reaches the refusal — agbcc folds the negation of a constant.
4827
+ const x = e(d.operands[0]);
4828
+ wordCtx ??= {
4829
+ defs,
4830
+ incoming: incomingArgs(fn.blocks),
4831
+ forwarded: storeForwards(fn.blocks),
4832
+ overwritten: overwrittenReads(fn.blocks),
4833
+ };
4834
+ if (mayAlwaysHold(wordCtx, d.operands[0], 0x80000000)) {
4835
+ return mkGap('the mathematical negation of the constant 0x80000000 has no int spelling', [x]);
4836
+ }
4837
+ const cast = needsIntSpelling(x) || renderedIntSignedness(x, vtEnv) !== true;
4838
+ return { k: 'un', op: '-', e: cast ? { k: 'cast', to: T.s(32), e: x } : x };
4839
+ }
4393
4840
  if (d.opcode === 'neg') {
4394
4841
  const x = e(d.operands[0]);
4395
4842
  return { k: 'un', op: '-', e: needsIntSpelling(x) ? { k: 'cast', to: T.s(32), e: x } : x };
@@ -4513,12 +4960,36 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
4513
4960
  // assigned to a 32-bit one truncates — but it is a pass this branch did not set out to move.
4514
4961
  const wide64 = d.results[0] !== undefined && intWidth(d.results[0].type) === 64;
4515
4962
  const through = calleeValue(d);
4963
+ // a struct passed by value is the object its operand addresses — a frame object stamped as
4964
+ // that struct, or a global — spelled bare (frontend/call-plan.ts `foldByValueArgs`)
4965
+ const byValue = byValueOperands(d).map(({ at, param }) => {
4966
+ const o = defs.get(d.operands[at]);
4967
+ const t = d.operands[at].type;
4968
+ const type = t.kind === 'ptr' && t.to.kind === 'struct' ? t.to : undefined;
4969
+ const object =
4970
+ type !== undefined &&
4971
+ (o?.opcode === 'laddr'
4972
+ ? o.attrs.aggregate === true
4973
+ : o?.opcode === 'gaddr' && o.attrs.passedAs === structSpelling(type));
4974
+ if (!object) {
4975
+ throw new Error(
4976
+ `internal: a by-value argument of '${d.attrs.target}' is not the address of an object of the struct it passes`,
4977
+ );
4978
+ }
4979
+ return { at, param, type };
4980
+ });
4981
+ const args = callArgs(d).map((v, i): Expr => {
4982
+ const x = e(v);
4983
+ return byValue.some((b) => b.at === i) && x.k === 'addr' ? { k: 'var', name: x.name } : x;
4984
+ });
4516
4985
  return {
4517
4986
  k: 'call',
4518
4987
  fn: through === undefined ? (d.attrs.target as string) : e(through),
4519
- args: callArgs(d).map(e),
4988
+ args,
4520
4989
  ...(wide64 ? { wide64: true as const } : {}),
4521
4990
  ...(d.attrs.sret === true ? { sret: d.results[0].type } : {}),
4991
+ ...(d.attrs.sret !== true && d.results[0]?.type.kind === 'struct' ? { returns: d.results[0].type } : {}),
4992
+ ...(byValue.length > 0 ? { byValue } : {}),
4522
4993
  };
4523
4994
  }
4524
4995
  if (d.opcode === 'laddr') {
@@ -4544,6 +5015,35 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
4544
5015
  return { k: 'addr', name: d.attrs.sym as string };
4545
5016
  }
4546
5017
  if (d.opcode === 'load') {
5018
+ const viewed = frameUnionAccess(
5019
+ d.operands[0],
5020
+ d.attrs.off as number,
5021
+ d.attrs.width as number,
5022
+ (d.attrs.signed as boolean) ?? false,
5023
+ false,
5024
+ );
5025
+ if (viewed !== null) {
5026
+ return unqualifiedView(d, viewed);
5027
+ }
5028
+ const member = frameStructMember(
5029
+ d,
5030
+ d.attrs.off as number,
5031
+ d.attrs.width as number,
5032
+ (d.attrs.signed as boolean) ?? false,
5033
+ );
5034
+ if (member !== null) {
5035
+ return member;
5036
+ }
5037
+ const element = frameArrayElement(
5038
+ d,
5039
+ d.attrs.off as number,
5040
+ d.attrs.width as number,
5041
+ (d.attrs.signed as boolean) ?? false,
5042
+ );
5043
+ if (element !== null) {
5044
+ return element;
5045
+ }
5046
+ refuseFloatMemberRead(d);
4547
5047
  const access = memAccess(
4548
5048
  d.operands[0],
4549
5049
  e(d.operands[0]),
@@ -4555,6 +5055,7 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
4555
5055
  symCtx,
4556
5056
  false,
4557
5057
  advanceStepOf(d.operands[0]),
5058
+ qualifiedBy(d) === 'cast',
4558
5059
  );
4559
5060
  return pinnedAccess(d, access);
4560
5061
  }
@@ -4646,6 +5147,12 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
4646
5147
  // which over-declines, the refusing direction.
4647
5148
  let zeroTripStale: Map<Value, string> | null = null;
4648
5149
  const expr = (v: Value): Expr => exprWith(activeSub)(v);
5150
+ // A return that hands back the function's own struct return object (frontend/own-struct-return.ts)
5151
+ // returns that local by value, `return ret;`, where its address would print `&ret`.
5152
+ const returnedValue = (term: Op): Expr => {
5153
+ const value = expr(term.operands[0]);
5154
+ return term.attrs[RETURNS_OBJECT] === true && value.k === 'addr' ? { k: 'var', name: value.name } : value;
5155
+ };
4649
5156
  const withSub = <R>(sub: Map<Value, string>, run: () => R): R => {
4650
5157
  const prev = activeSub;
4651
5158
  activeSub = prev ? new Map([...prev, ...sub]) : sub;
@@ -4710,10 +5217,32 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
4710
5217
  const NO_COPIES_AT_A_DEF: ReadonlyMap<Op, Stmt[]> = new Map();
4711
5218
  /** The `atDef` copies `sideEffects` has spelled — `assertSunkCopiesPlaced`'s input. */
4712
5219
  const sunkCopiesEmitted = new Set<Stmt>();
4713
- const undefCarriesNothing = (arg: Value, name: string, pred: Block): boolean => {
4714
- if (defs.get(arg)?.opcode !== 'undef') {
5220
+ const undefCarriesNothing = (arg: Value, name: string, pred: Block, coalescing = false): boolean => {
5221
+ // A `copied` undef is another storage's content moved into this one (`x = y` of two
5222
+ // uninitialised locals): the machine ran that move, so the copy is spelled.
5223
+ if (defs.get(arg)?.opcode !== 'undef' || defs.get(arg)!.attrs.copied === true) {
4715
5224
  return false;
4716
5225
  }
5226
+ // AN UNDEFINED VALUE READ UNDER ANOTHER NAME IS A RELATION. Dropping the copy leaves `name`
5227
+ // holding whatever it held, which equals the undefined value only where nothing else reads that
5228
+ // value: `int x, z; z = x; if (c) x = 5; return x * 2 + z;` at gcc 2.7.2 keeps x in v0 and
5229
+ // reads it as z too, so on `c == 0` the join's local IS z, and an unassigned `v0` beside
5230
+ // `uninit_v0` loses that. So the copy is dropped only where every other read of the value goes
5231
+ // through `name` itself: the coalesced undef below (which the coalescing test asks before it
5232
+ // has renamed it), or edge copies into joins of the same name.
5233
+ if (!coalescing) {
5234
+ const op = defs.get(arg)!;
5235
+ for (const site of useSitesOf.get(arg) ?? []) {
5236
+ if (site.op.operands.includes(arg) && undefName.get(op) !== name) {
5237
+ return false;
5238
+ }
5239
+ for (const s of site.op.successors) {
5240
+ if (s.args.some((a, i) => a === arg && varName.get(s.block.params[i]) !== name)) {
5241
+ return false;
5242
+ }
5243
+ }
5244
+ }
5245
+ }
4717
5246
  const writesBefore = (home: Block | undefined): boolean =>
4718
5247
  home === undefined || home === pred || reachFrom(home).has(pred);
4719
5248
  for (const [v, n] of varName) {
@@ -4723,6 +5252,67 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
4723
5252
  }
4724
5253
  return !(anchoredHome.get(name) ?? []).some(writesBefore);
4725
5254
  };
5255
+ // AN UNDEF ONE JOIN TAKES IS THAT JOIN'S LOCAL, READ BEFORE ITS ASSIGNMENT. `int t; if (k > 5)
5256
+ // g = t; switch (k) { case 0: t = …; } … t …` reads one source variable on both sides of the
5257
+ // switch, and the asm reads one register: the entry value directly, and again through the join
5258
+ // the arms assign. Named from its key it would be two declarations, `uninit_t7` beside the join's
5259
+ // `v9`, which the compiler allocates as two. So the undef takes the join's name when that name is
5260
+ // never written where any of its reads happen: every edge copy into the join is one
5261
+ // `undefCarriesNothing` drops, and every block that reads it — through the ops rendered inline
5262
+ // over it — passes the same test, so each read sees the name before anything assigned it.
5263
+ // A merge that adopted a parameter's name is never one (`v<n>` only). The reads are recorded with
5264
+ // the dropped copies, for the sunk-copy postcondition below to check them the same way.
5265
+ const coalescedUndefs = new Set<Op>();
5266
+ for (const op of fn.blocks.flatMap((b) => b.ops)) {
5267
+ if (op.opcode !== 'undef') {
5268
+ continue;
5269
+ }
5270
+ const u = op.results[0];
5271
+ const joins = new Set<string>();
5272
+ const intoJoin: Block[] = [];
5273
+ const readIn = new Set<Block>();
5274
+ let unnamed = false;
5275
+ for (const site of useSitesOf.get(u) ?? []) {
5276
+ for (const s of site.op.successors) {
5277
+ s.args.forEach((a, i) => {
5278
+ if (a === u) {
5279
+ const n = varName.get(s.block.params[i]);
5280
+ if (n === undefined) {
5281
+ unnamed = true;
5282
+ } else {
5283
+ joins.add(n);
5284
+ }
5285
+ intoJoin.push(site.blk);
5286
+ }
5287
+ });
5288
+ }
5289
+ if (site.op.operands.includes(u)) {
5290
+ const work = [site];
5291
+ for (let w = work.pop(); w !== undefined; w = work.pop()) {
5292
+ readIn.add(w.blk);
5293
+ if (!materialize.has(w.op)) {
5294
+ work.push(...w.op.results.flatMap((r) => useSitesOf.get(r) ?? []));
5295
+ }
5296
+ }
5297
+ }
5298
+ }
5299
+ const [name] = joins;
5300
+ if (
5301
+ unnamed ||
5302
+ joins.size !== 1 ||
5303
+ readIn.size === 0 ||
5304
+ !/^v\d+$/.test(name) ||
5305
+ globalNames.has(name) ||
5306
+ ![...intoJoin, ...readIn].every((b) => undefCarriesNothing(u, name, b, true))
5307
+ ) {
5308
+ continue;
5309
+ }
5310
+ undefName.set(op, name);
5311
+ coalescedUndefs.add(op);
5312
+ for (const b of readIn) {
5313
+ droppedUndefCopies.push({ name, pred: b });
5314
+ }
5315
+ }
4726
5316
  const tempCounter = { n: 0 }; // per-function swap-cycle temp names (sequentialize)
4727
5317
  /** One record per parameter slot an edge ACTUALLY carries, in the order the copies are to be
4728
5318
  * laid out — everything `argAssignsFor` does except `sequentialize`. Split out because Regime-A
@@ -4952,127 +5542,32 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
4952
5542
  // EFFECTFUL op whose result nothing consumes (a void/discarded call, and an `opaque` standing for
4953
5543
  // an instruction asmlift could not model); and MATERIALIZED defs — a call/load whose value cannot
4954
5544
  // soundly render at its use is assigned to its named temp here, at its own program position.
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.
4963
- *
4964
- * `void *`, NOT the map's declared pointee: it is the one target assignment-compatible with any
4965
- * object-pointer declaration, the same "same answer in every world" property the guard that
4966
- * made the `u8 *` exists for. Trusting the map's pointee would put the spelling back in one
4967
- * world. It costs nothing: agbcc compiles `p = (void *)((u8 *)p + 4)` and the warning-carrying
4968
- * `p = (u8 *)p + 4` to BYTE-IDENTICAL objects (`-mthumb-interwork -Wimplicit -O2 -fhex-asm
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. */
4975
- const intoPtrCell = (lval: Expr, value: Expr): Expr => {
4976
- if (!isPtrValue(lval)) {
4977
- return value;
4978
- }
4979
- const vt = ctype(value);
4980
- // BOTH ways a pointer value reaches here. `ctype` types params and locals, so it sees the
4981
- // guard's own `(u8 *)…` and nothing else: a bare `gSym.pBuf` or a pointer global's value —
4982
- // the population this rule exists for — reads `undefined` there. An already-`void *` value is
4983
- // assignable as it stands.
4984
- const isPtr = isPtrValue(value) || (vt?.kind === 'ptr' && vt.to.kind !== 'void');
4985
- return isPtr ? { k: 'cast', to: T.ptr(T.void()), e: value } : value;
4986
- };
4987
5545
 
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:
4995
- *
4996
- * 1. EVIDENCE — would a source plausibly have declared this access `volatile`? The answer has
4997
- * to come from DATA: the target's declared device-register window
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.
5009
- *
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.
5546
+ /** THE REFUSAL for the read half of `unreadResult`: is this dead read QUALIFIED (ir/discipline.ts
5547
+ * `qualified`)? `pinnedAccess` then puts the qualifier on its spelling, from the dead-read pin
5548
+ * (frontend/dead-reads.ts), the device pin, or the map's declaration of the object it reads, and
5549
+ * `gStatus;` under `extern volatile u32 gStatus;` compiles differently from its own absence. A
5550
+ * plain read is dropped: an unqualified `p[2];` compiles to the same bytes as no statement at all
5551
+ * (measured on agbcc, IDO and mwcc), so spelling it claims an access no candidate makes.
5013
5552
  *
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
5016
- * with `off` defaulted to 0 — which admits `volatile s32 *p0 = (s32 *)0x04000000; p0[a0];`, a
5017
- * qualified access at an address the declared window does not cover. The whitelist is by OPCODE
5018
- * so a read op added later refuses until someone answers both questions for it. */
5019
- const volatileQualifiable = (op: Op): boolean => {
5020
- if (qualified(op)) {
5021
- return true;
5022
- }
5023
- if (op.opcode !== 'load') {
5024
- return false;
5025
- }
5026
- const window = opts.deviceRegisters;
5027
- if (!window) {
5028
- return false;
5029
- }
5030
- const addr = constAddressOf(defs, op.operands[0], typeof op.attrs.off === 'number' ? op.attrs.off : 0);
5031
- if (addr === null || addr < window[0] || addr >= window[1]) {
5032
- return false;
5033
- }
5034
- return (useSitesOf.get(op.operands[0]) ?? []).some((s) => s.op !== op);
5035
- };
5553
+ * ONE DECIDER. Whether a read nobody uses was `volatile` is the dead-read pin's question, asked
5554
+ * once of the raw lift, at any address, with its refusals tabled and reported. The read that
5555
+ * reaches this walk unused because a DECLARED void return suppressed the `ret` operand is the
5556
+ * same question, and the pin answers it too: it does not count that operand as a use. So no
5557
+ * address window, base-local or second-access rule is asked here; a read the pin refused is
5558
+ * plain, and dropped. */
5559
+ const volatileQualifiable = (op: Op): boolean => qualified(op);
5036
5560
 
5037
5561
  /** Ops the `sideEffects` walk must SPELL even though nothing consumes their result — the
5038
5562
  * 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.
5563
+ * property needs no edit here, plus the qualifier refusal above for the memory-read half.
5040
5564
  *
5041
5565
  * A memory READ is not `effectful`, deliberately: ir/discipline.ts calls a load deletable when
5042
5566
  * dead, because nothing observes a read nobody reads. That is the C claim. The COMPILER
5043
5567
  * claim points the other way — an optimizing compiler deletes every dead read it is allowed to
5044
5568
  * delete, so one still in the target is evidence the source's access was `volatile`, and
5045
- * dropping it deletes an instruction the machine executed.
5046
- *
5047
- * The statement earns nothing by itself: an unqualified `p[2];` compiles to the same bytes as
5048
- * no statement at all (measured on agbcc, IDO and mwcc). What it does is put the access where a
5049
- * qualifier can reach it — and `volatileQualifiable` is the condition under which one can.
5050
- *
5051
- * WHICH VARIATION REACHES IT, because "a qualifier" is two variations and only one of them does:
5052
- * l3/volatileptr.ts's `/volatile` qualifies the pointer LOCAL the read is spelled through, and
5053
- * that is the arm every match here rides. l3/volstore.ts's `/vol-store` mints `volatile` at an
5054
- * inline cast STORE and never visits an `exprstmt`, so on a tree with no base local it emits the
5055
- * cell qualified for its writes and plain for this read — a candidate that cannot reproduce the
5056
- * surviving `ldr`. Wasted rather than wrong (the differ refuses it); teaching that variation the
5057
- * read is a widening with its own window census to pay for, priced in its header.
5058
- *
5059
- * TWO THINGS DELIBERATELY NOT DONE HERE, priced rather than left for a reader to rediscover.
5060
- *
5061
- * 1. THE LITERAL ARM IS A BACKWARDS DEFAULT AND ITS PREIMAGE IS NOT EMPTY. docs/level-tower.md
5062
- * admits a default that reads the map backwards only when the backwards mapping is ITSELF a
5063
- * function, and "a surviving dead read implies the source said `volatile`" is not quite one:
5064
- * the other preimage is a function whose `returnsVoid` fact is WRONG, so its return value
5065
- * arrived here as a dead read. The second-use clause removes the common shape of that (a bare
5066
- * register accessor), leaving a function that both STORES to a device register and reads one
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
5069
- * the way `/livebase/volatile` already pairs — moves every device row's fan shape and wants
5070
- * its own round and zero-flip gate over BOTH tiers.
5071
- * 2. THE REFUSAL IS SILENT. When this returns false for a `load`, the machine performed a read
5072
- * that no statement stands for and nothing records the decision — against this project's own
5073
- * "instrument the refusal" rule; `structure()` has no diagnostic sink to write into. What
5074
- * exists instead is the ADMISSION side's zero point, `synthetic:dmareadback`, which fails
5075
- * loudly if the rule stops firing. */
5569
+ * dropping it deletes an instruction the machine executed. The pin makes that claim; this walk
5570
+ * spells what it stamped. */
5076
5571
  /** Does `op` render AT ITS OWN POSITION, as `v = f(…)`?
5077
5572
  *
5078
5573
  * ONE DEFINITION, TWO READERS, on purpose: `isSpelled`'s base case is a MODEL of what
@@ -5161,42 +5656,84 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
5161
5656
  // A store whose lvalue is a bare global (`gSym = v`, from an `&gSym` base at off 0) emits
5162
5657
  // as an ASSIGN, not a store — memAccess returns a `var` node for that case.
5163
5658
  const width = op.attrs.width as number;
5164
- const lval0 = memAccess(
5165
- op.operands[0],
5166
- expr(op.operands[0]),
5167
- op.attrs.off as number,
5168
- width,
5169
- width === 4,
5170
- ctype,
5171
- scalarGlobals,
5172
- symCtx,
5173
- true, // an lvalue: a member whose declaration is const cannot be NAMED as the target
5174
- advanceStepOf(op.operands[0]),
5659
+ const off = op.attrs.off as number;
5660
+ const stamp = op.attrs.signed as boolean | undefined;
5661
+ const viewed = frameUnionAccess(op.operands[0], off, width, storeView(op, width), true);
5662
+ if (viewed !== null) {
5663
+ const lval = unqualifiedView(op, viewed);
5664
+ floatBitsStoreTarget(op.operands[1], lval);
5665
+ out.push({ k: 'store', lval, value: intoPtrCell(lval, expr(op.operands[1])) });
5666
+ continue;
5667
+ }
5668
+ const member = frameStructMember(op, off, width, null) ?? frameArrayElement(op, off, width, null);
5669
+ if (member !== null) {
5670
+ floatBitsStoreTarget(op.operands[1], member);
5671
+ out.push({ k: 'store', lval: member, value: intoPtrCell(member, expr(op.operands[1])) });
5672
+ continue;
5673
+ }
5674
+ const baseExpr = expr(op.operands[0]);
5675
+ const lval0 = structBytesCell(
5676
+ op.operands[1],
5677
+ memAccess(
5678
+ op.operands[0],
5679
+ baseExpr,
5680
+ off,
5681
+ width,
5682
+ storeView(op, width),
5683
+ ctype,
5684
+ scalarGlobals,
5685
+ symCtx,
5686
+ true, // an lvalue: a member whose declaration is const cannot be NAMED as the target
5687
+ advanceStepOf(op.operands[0]),
5688
+ qualifiedBy(op) === 'cast',
5689
+ ),
5175
5690
  );
5691
+ floatBitsStoreTarget(op.operands[1], lval0);
5176
5692
  if (lval0.k === 'var') {
5177
5693
  globalNames.add(lval0.name);
5178
5694
  out.push({ k: 'assign', name: lval0.name, value: intoPtrCell(lval0, expr(op.operands[1])) });
5179
5695
  continue;
5180
5696
  }
5181
- // signedness mirrors recoverTypes' store seed (word ⇒ signed, narrow ⇒ unsigned), so an
5182
- // inserted cast declares the same scalar the recovered pointee would have.
5183
- const lval = pinnedAccess(op, lval0);
5697
+ // a member whose declared sign is not the stamp's is spelled as the flat cell (stampedStoreLval)
5698
+ const bt = op.operands[0].type;
5699
+ const lval = pinnedAccess(
5700
+ op,
5701
+ stampedStoreLval(
5702
+ lval0,
5703
+ stamp,
5704
+ width,
5705
+ bt.kind === 'ptr' && bt.to.kind === 'struct' ? baseExpr : null,
5706
+ off,
5707
+ ctype,
5708
+ ),
5709
+ );
5184
5710
  out.push({ k: 'store', lval, value: intoPtrCell(lval, expr(op.operands[1])) });
5185
5711
  } else if (op.opcode === 'astore') {
5186
5712
  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,
5713
+ const fieldOff = op.attrs.fieldOff as number | undefined;
5714
+ const stamp = op.attrs.signed as boolean | undefined;
5715
+ const lval0 = structBytesCell(
5716
+ op.operands[2],
5717
+ arrayAccess(
5718
+ op.operands[0],
5719
+ expr(op.operands[0]),
5720
+ expr(op.operands[1]),
5721
+ fieldOff,
5722
+ op.attrs.memberOff as number | undefined,
5723
+ elemSize,
5724
+ storeView(op, elemSize),
5725
+ ctype,
5726
+ symCtx,
5727
+ ),
5199
5728
  );
5729
+ // a struct-array element's field: `base[i].field_K`, whose flat cell is
5730
+ // `(base + i)[K / width]` — C scales `base + i` by the element, exactly as `base[i]` does
5731
+ const el = lval0.k === 'field' && lval0.base.k === 'index' ? lval0.base : null;
5732
+ const lval =
5733
+ el === null || fieldOff === undefined
5734
+ ? lval0
5735
+ : stampedStoreLval(lval0, stamp, undefined, { k: 'bin', op: '+', l: el.base, r: el.idx }, fieldOff, ctype);
5736
+ floatBitsStoreTarget(op.operands[2], lval);
5200
5737
  out.push({
5201
5738
  k: 'store',
5202
5739
  lval: pinnedAccess(op, lval),
@@ -5252,7 +5789,14 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
5252
5789
  // to `header` is a conditional continue; its other edge, when it leaves the loop, is an early exit
5253
5790
  // (a `break` to `exit`, or an early `return` through a trampoline). Null outside any loop body —
5254
5791
  // the early-exit branch in structureBlock is inert there.
5255
- type LoopFrame = { header: Block; exit: Block; body: Set<Block>; arms: LoopArm[]; breaks: ReadonlySet<Block> };
5792
+ type LoopFrame = {
5793
+ header: Block;
5794
+ exit: Block | null;
5795
+ body: Set<Block>;
5796
+ arms: LoopArm[];
5797
+ breaks: ReadonlySet<Block>;
5798
+ breakArms?: readonly BreakArm[];
5799
+ };
5256
5800
  let loopCtx: LoopFrame | null = null;
5257
5801
  const withLoop = <R>(frame: LoopFrame, run: () => R): R => {
5258
5802
  const prev = loopCtx;
@@ -5264,6 +5808,87 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
5264
5808
  }
5265
5809
  };
5266
5810
 
5811
+ // THE SWITCH ARMS being structured, innermost last — one frame per arm, in both regimes, so a
5812
+ // `break;` can be checked against what C binds it to. A C `break` leaves the innermost enclosing
5813
+ // loop OR switch, so a switch-scoped one is only spelled where this arm's frame is the innermost
5814
+ // switch frame AND no loop was entered since the arm began (`loop` is the loop frame the arm sits
5815
+ // in). `region` is the arm's own blocks (`ArmExit`), closed or falling; null on an arm the exit
5816
+ // analysis gave none (one that leaves sideways), which structures to the CFG's joins and stops AT
5817
+ // the merge without the statement. A frame with no region still matters: it is what tells a
5818
+ // `break;` reached inside a nested switch that it would bind the inner one, and the loop-exit
5819
+ // rules that a loop's `break` cannot be minted here. `fallsTo` is the sibling a falling arm runs
5820
+ // into (`armJoin` reads it).
5821
+ type SwitchFrame = {
5822
+ merge: Block | null;
5823
+ loop: LoopFrame | null;
5824
+ region: ReadonlySet<Block> | null;
5825
+ fallsTo: Block | null;
5826
+ };
5827
+ const switchFrames: SwitchFrame[] = [];
5828
+ // Is `b` the merge of an arm with a region, reached before that arm's end, and may `break;` say so
5829
+ // from here? Undefined when no such arm ends at `b`. (At an arm's own end, `stop` is the merge and
5830
+ // `structureRegion` returns before asking.) Throws when one does but a nested loop or switch would
5831
+ // capture the statement.
5832
+ const switchBreakAt = (b: Block): Stmt[] | undefined => {
5833
+ let i = switchFrames.length - 1;
5834
+ while (i >= 0 && (switchFrames[i].region === null || switchFrames[i].merge !== b)) {
5835
+ i--;
5836
+ }
5837
+ if (i < 0) {
5838
+ return undefined;
5839
+ }
5840
+ if (i !== switchFrames.length - 1 || switchFrames[i].loop !== loopCtx) {
5841
+ throw new StructureError(
5842
+ `cannot structure '${fn.name}': a case body leaves its switch from inside a nested ` +
5843
+ `${i !== switchFrames.length - 1 ? 'switch' : 'loop'}, where \`break\` would leave that instead`,
5844
+ );
5845
+ }
5846
+ return [{ k: 'break', ofSwitch: true }];
5847
+ };
5848
+ // THE JOIN OF A BRANCH INSIDE AN ARM. The CFG's join for `if (c) break;` lies past the switch —
5849
+ // in a falling arm the fall-through path reaches the merge too, through the next arm, and in a
5850
+ // closed arm the merge IS the join — so structuring to it would copy the next arm, or the rest of
5851
+ // this one, into both sides. Inside the arm, an edge to the merge is a `break;` and a `ret` a
5852
+ // `return`: jumps, which end their path wherever they sit, so those edges are DELETED, as
5853
+ // `loopJoin` deletes a loop body's exits (`regionJoins`). The edge into the sibling the arm falls
5854
+ // into is no jump: its path runs to the arm's END, so it goes to EXIT, the caller's `stop`. Sent
5855
+ // to EXIT too, a guard whose one side breaks would be post-dominated by EXIT alone and every
5856
+ // statement after it would nest in its `else`; deleted, a guard whose one side falls would join
5857
+ // on the other side's path, which the falling path cannot jump over. Null ⇒ the paths meet only
5858
+ // at the arm's end. Asked only in the innermost arm frame and outside any loop entered since,
5859
+ // which is where `switchBreakAt` would spell the `break;`.
5860
+ const armJoins = new Map<ReadonlySet<Block>, Map<Block, Block | null>>();
5861
+ const armJoin = (b: Block, stop: Block | null): Block | null | undefined => {
5862
+ const f = switchFrames[switchFrames.length - 1];
5863
+ if (f === undefined || f.region === null || !f.region.has(b) || f.loop !== loopCtx) {
5864
+ return undefined;
5865
+ }
5866
+ const region = f.region;
5867
+ let pd = armJoins.get(region);
5868
+ if (pd === undefined) {
5869
+ pd = regionJoins(region, null, false, f.fallsTo);
5870
+ armJoins.set(region, pd);
5871
+ }
5872
+ const join = pd.get(b) ?? null;
5873
+ // IN A CLOSED ARM, AN EDGE TO THE MERGE IS USUALLY THE ARM'S OWN END, not a `break;`: in
5874
+ // `case 7: s *= 2; if (s > 100) s = 1; break;` the guard's edge past `s = 1` is where the arm
5875
+ // runs off, and deleting it makes `s = 1` the join and spells `if (!(s > 100)) break; s = 1;`.
5876
+ // What the deletion is for is a TAIL two of the arm's paths share behind a mid-arm `break;`
5877
+ // (`if (a) { if (b) break; g(1); } h(2); break;`, where `h(2)` is the join). So a closed arm
5878
+ // takes its own join only where the join is such a meeting point, two of the arm's blocks
5879
+ // entering it, or where the region being structured already ends INSIDE the arm (`stop` is one
5880
+ // of its blocks: an enclosing guard found such a tail, and a path to the merge from here jumps
5881
+ // over it). Otherwise it reads the CFG.
5882
+ if (
5883
+ f.fallsTo === null &&
5884
+ !(stop !== null && region.has(stop)) &&
5885
+ (join === null || new Set(preds.get(join)?.filter((p) => region.has(p))).size < 2)
5886
+ ) {
5887
+ return undefined;
5888
+ }
5889
+ return join;
5890
+ };
5891
+
5267
5892
  // THE IMPLICIT CONTINUE. A branch with one edge back to the loop header spells that edge as
5268
5893
  // nothing at all: control falls to the bottom of the body and the loop goes round. That holds only
5269
5894
  // where the region being structured ends at the loop bottom (`stop === frame.header`). An `if` in
@@ -5293,12 +5918,16 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
5293
5918
  // that render: an exit parameter that takes its argument's own name renders no copy, yet the exit
5294
5919
  // still reads that name, and a latch update that wrote it hands the exit the updated value.
5295
5920
  const breakRule = (frame: LoopFrame, from: Block, writes: Set<string>): { refusal: string } | { bare: boolean } => {
5921
+ const exit = frame.exit;
5922
+ if (exit === null) {
5923
+ return { refusal: 'leaves a loop that has no exit' }; // DEFENSIVE: no edge leaves such a loop
5924
+ }
5296
5925
  // A `while (1)` has no copies of its own after the loop, so a break carries its own, computed
5297
5926
  // where it leaves; what is left is the exit region's re-derivations. An inner loop's value the
5298
5927
  // region reads under its name (`emitForever`) is no re-derivation.
5299
5928
  const fl = foreverLoops.get(frame.header);
5300
5929
  if (fl !== undefined) {
5301
- const exitRegion = new Set([frame.exit, ...reachFrom(frame.exit)].filter((x) => !frame.body.has(x)));
5930
+ const exitRegion = new Set([exit, ...reachFrom(exit)].filter((x) => !frame.body.has(x)));
5302
5931
  const { sub } = latchInnerSub(fl.header, fl.body, [...fl.breaks], exitRegion);
5303
5932
  return loopEscapeHazard(frame.body, sub, writes, exitRegion)
5304
5933
  ? { refusal: 'reaches an exit region that reads a loop value under a name this iteration already rewrote' }
@@ -5307,11 +5936,11 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
5307
5936
  if (!whileLoops.has(frame.header)) {
5308
5937
  return { refusal: 'leaves a do-while' };
5309
5938
  }
5310
- const headerCopies = argAssigns(frame.header, frame.exit);
5939
+ const headerCopies = argAssigns(frame.header, exit);
5311
5940
  let bare = false;
5312
5941
  if (headerCopies.length !== 0) {
5313
- const headerArgs = successorTo(frame.header, frame.exit)!.args;
5314
- const breakArgs = successorTo(from, frame.exit)!.args;
5942
+ const headerArgs = successorTo(frame.header, exit)!.args;
5943
+ const breakArgs = successorTo(from, exit)!.args;
5315
5944
  const reads = new Set<string>();
5316
5945
  for (const s of headerCopies) {
5317
5946
  stmtExprs(s).forEach((e) => exprVars(e, reads));
@@ -5324,7 +5953,7 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
5324
5953
  return { refusal: 'would run the copies the loop header hands its exit' };
5325
5954
  }
5326
5955
  }
5327
- const exitRegion = new Set([frame.exit, ...reachFrom(frame.exit)].filter((x) => !frame.body.has(x)));
5956
+ const exitRegion = new Set([exit, ...reachFrom(exit)].filter((x) => !frame.body.has(x)));
5328
5957
  if (loopEscapeHazard(frame.body, new Map(), writes, exitRegion)) {
5329
5958
  return { refusal: 'reaches an exit region that reads a loop value under a name this iteration already rewrote' };
5330
5959
  }
@@ -5357,6 +5986,12 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
5357
5986
  if (b === stop) {
5358
5987
  return [];
5359
5988
  }
5989
+ // The merge of the switch a falling arm also leaves: the edge copies are already out, and
5990
+ // `break;` is the jump.
5991
+ const brk = switchBreakAt(b);
5992
+ if (brk !== undefined) {
5993
+ return brk;
5994
+ }
5360
5995
  // Back to a `while (1)` header from a region that ends short of the loop bottom: the edge's
5361
5996
  // copies are already out, and `continue` is the jump.
5362
5997
  if (loopCtx !== null && b === loopCtx.header && foreverLoops.has(b)) {
@@ -5387,6 +6022,21 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
5387
6022
  // two places the map is handed to (`coalesceNames`, `makeLoopHazards`) only read it. So the
5388
6023
  // recognizer sees one settled naming however late a case body calls back into `structureRegion`,
5389
6024
  // and nothing here has to be rebuilt against a naming that moves underneath it.
6025
+ // One switch arm, from `entry` to `stop`, under its frame (`switchFrames`).
6026
+ const structureArm = (entry: Block, stop: Block | null, merge: Block | null, exit: ArmExit | undefined): Stmt[] => {
6027
+ // A backend with no mid-arm `break` (`spellSwitchBreak`) gets no region for a closed arm, and
6028
+ // so no `break;` from `switchBreakAt` and no arm-local join: it structures to the CFG's.
6029
+ const region =
6030
+ exit === undefined || exit.kind === 'unstructurable' || (exit.kind === 'break' && !spellSwitchBreak)
6031
+ ? null
6032
+ : (exit.region ?? null);
6033
+ switchFrames.push({ merge, loop: loopCtx, region, fallsTo: exit?.kind === 'fallthrough' ? exit.to : null });
6034
+ try {
6035
+ return structureRegion(entry, stop);
6036
+ } finally {
6037
+ switchFrames.pop();
6038
+ }
6039
+ };
5390
6040
  const { recognizeSwitch, analyzeArmExit, layoutIndex, defaultLayoutPos, chainArms } = makeSwitchRecovery({
5391
6041
  fn,
5392
6042
  defs,
@@ -5402,16 +6052,20 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
5402
6052
  spellSwitchFallthrough,
5403
6053
  emitsOwnStatement: (blk) => blk.ops.some((o) => anchoredAt.has(o) || materialize.has(o)),
5404
6054
  blockOf,
6055
+ enclosingLoop: () => loopCtx,
5405
6056
  hoistDispatchCopies: (edges, liveAt) => hoistedDispatchAssigns(edges, liveAt),
5406
6057
  expr: (v) => expr(v),
5407
6058
  structureRegion: (b, stop) => structureRegion(b, stop),
6059
+ structureArm,
5408
6060
  });
5409
6061
 
5410
6062
  const structureBlock = (b: Block, stop: Block | null): Stmt[] => {
5411
6063
  // Bottom-test `do-while`: this block is a do-while header (the body-first loop entry). Emit the
5412
6064
  // do-while — its body includes `b`'s own ops (structured via structureRegion with the hook masked),
5413
6065
  // so do NOT emit sideEffects(b) here. The init was already emitted by the predecessor's argAssigns.
5414
- const dw = doWhileLoops.get(b);
6066
+ // A guarded self-loop its guard did not claim (`keptGuardDoWhiles`) is a do-while like any
6067
+ // other: read as a plain block, its back edge would re-enter it and decline.
6068
+ const dw = doWhileLoops.get(b) ?? keptGuardDoWhiles.get(b);
5415
6069
  if (dw && !dwActive.has(b)) {
5416
6070
  return emitDoWhile(dw, stop);
5417
6071
  }
@@ -5424,7 +6078,7 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
5424
6078
  const term = b.ops[b.ops.length - 1];
5425
6079
  if (term.opcode === 'ret') {
5426
6080
  // A void function's `bx lr` leaves whatever in r0; suppress that phantom return value.
5427
- const value = returnsVoid || !term.operands.length ? undefined : expr(term.operands[0]);
6081
+ const value = returnsVoid || !term.operands.length ? undefined : returnedValue(term);
5428
6082
  // The `value === undefined` half is a GUARD: `l3/tailret.ts` re-checks it before deleting
5429
6083
  // anything, so marking a value-carrying return would change no output. The mark claims the asm
5430
6084
  // shows no `return;` STATEMENT here, which is a claim about a VOID return only.
@@ -5469,8 +6123,9 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
5469
6123
  // more functions therefore has to BE the default, and it is the DIVERGENT reading that a
5470
6124
  // variation would have to offer back — a candidate nothing mints today, and the one thing
5471
6125
  // 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;
6126
+ // successor — and where that reading declines, the default-anchored RETRY below gets one try.
6127
+ const joined = ipdom.get(b) ?? followOverReturns(b, stop);
6128
+ let merge = joined ?? stop;
5474
6129
  const succ = term.successors;
5475
6130
  const caseVals = term.attrs.cases as number[];
5476
6131
  // Switch edges CARRY phi args (frontend/ssa.ts appends them terminator-generically) — each
@@ -5503,13 +6158,6 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
5503
6158
  }
5504
6159
  a.values.push(caseVals[i]);
5505
6160
  });
5506
- // The blocks an arm could fall INTO. The default block counts only when it is a block of its
5507
- // own: when it IS the merge, "the default" is just where the switch ends, and an arm reaching
5508
- // it is a plain `break`.
5509
- const siblings = new Set<Block>(arms.map((a) => a.entry));
5510
- if (defEdge.block !== merge) {
5511
- siblings.add(defEdge.block);
5512
- }
5513
6161
  // ARM ORDER. Grouping the table's slots by target walks them in TABLE order, which for a dense
5514
6162
  // table is ascending case value — the neutral spelling, and the source's order only by
5515
6163
  // accident. Where the compiler has declared that its block layout IS the order the arms were
@@ -5521,39 +6169,208 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
5521
6169
  // The policy orders the chain HEADS. A FALLING arm's position is not free — emission order is
5522
6170
  // load-bearing for correctness (the l3/ast.ts non-neutrality note), so it must sit directly
5523
6171
  // above the arm it falls into — and `chainArms` below re-threads it for exactly those, which
5524
- // is the same per-SITE reading Regime A makes of the same declaration. `analyzeArmExit` does
5525
- // not depend on emission order, so the exits can be settled first and reused below. Grouping
5526
- // by target already gives every arm a DISTINCT entry block, so no two sort keys can be equal
5527
- // and the tie-break Regime A needs on shared bodies has nothing to decide here.
5528
- const exitOf = new Map<Block, ArmExit>();
5529
- for (const entry of [...arms.map((a) => a.entry), defEdge.block]) {
5530
- if (!exitOf.has(entry)) {
5531
- exitOf.set(entry, analyzeArmExit(entry, b, merge, siblings));
6172
+ // is the same per-SITE reading Regime A makes of the same declaration.
6173
+ if (switchArmsFollowLayout) {
6174
+ arms.sort((x, y) => layoutIndex(x.entry) - layoutIndex(y.entry));
6175
+ }
6176
+ // …and then the CHAIN re-threads that order for the falling arms, exactly as it does for the
6177
+ // comparison tree: ONE definition (`chainArms`) for the linearization, the adjacency it
6178
+ // guarantees, and the three shapes no linear order spells. The arm-order policy above is read
6179
+ // for the chain HEADS only — the same per-SITE reading Regime A makes of the same
6180
+ // declaration, rather than a whole-switch gate on every arm being closed.
6181
+ const preChain = arms.map((a) => a.entry);
6182
+ // Everything below that depends on WHERE THE SWITCH ENDS, read for one candidate merge: the
6183
+ // arms' exits, their linearization, and whether the emission below would decline them. Read
6184
+ // twice at most — see the retry after it.
6185
+ // A default edge the dispatch never takes: a frontend whose dispatch is entered only past a
6186
+ // bounds test that already sent every other value elsewhere says so with `exhaustive`.
6187
+ const deadDefaultEdge = term.attrs.exhaustive === true;
6188
+ const readArms = (at: Block | null) => {
6189
+ // The blocks an arm could fall INTO. The default block counts only when it is a block of
6190
+ // its own: when it IS the merge, "the default" is just where the switch ends, and an arm
6191
+ // reaching it is a plain `break`.
6192
+ const siblings = new Set<Block>(arms.map((a) => a.entry));
6193
+ if (defEdge.block !== at) {
6194
+ siblings.add(defEdge.block);
6195
+ }
6196
+ // `analyzeArmExit` does not depend on emission order, so the exits can be settled before
6197
+ // the chain and reused below. Grouping by target already gives every arm a DISTINCT entry
6198
+ // block, so no two sort keys can be equal and the tie-break Regime A needs on shared bodies
6199
+ // has nothing to decide here.
6200
+ const exits = new Map<Block, ArmExit>();
6201
+ for (const entry of [...arms.map((a) => a.entry), defEdge.block]) {
6202
+ if (!exits.has(entry)) {
6203
+ exits.set(entry, analyzeArmExit(entry, b, at, siblings));
6204
+ }
6205
+ }
6206
+ const falls = [...exits.values()].some((e) => e.kind === 'fallthrough');
6207
+ // A table whose default block is ALSO a case target is that case's arm, not a separate
6208
+ // one: it is already in `preChain`, and `defaultLayoutPos` withholds the label's position.
6209
+ const dflt = defEdge.block !== at && !armOf.has(defEdge.block) ? defEdge.block : null;
6210
+ const chain = !spellSwitchFallthrough && falls ? null : chainArms(preChain, dflt, exits);
6211
+ const refusal = chain === null ? null : armRefusal(exits, chain);
6212
+ return { exits, falls, chain, refusal, declines: chain === null || refusal !== null };
6213
+ };
6214
+ // THE EMISSION'S PER-ARM REFUSALS, the one definition of them. Every refusal the emission
6215
+ // below throws is a null chain (which a fall-through in a language without one forces too —
6216
+ // `readArms` reads that one) or one of three asked here per arm, in emission order, without
6217
+ // emitting anything: an arm whose exit is unstructurable, a fall-through into an arm that is
6218
+ // not the next one emitted, and a fall-through into an arm that opens with switch-edge
6219
+ // copies. The answer is the arm it refuses at and a `raise` that throws the refusal, and the
6220
+ // emission calls exactly the one `readArms` carries for the reading it adopted — so the
6221
+ // retry's gate and the emission cannot disagree about what declines, and a reading that
6222
+ // declines fails with the message it always did. Each refusal stays a literal
6223
+ // `throw new StructureError(…)` so the decline-message harvest (apps/web declines.test.ts)
6224
+ // still reads it.
6225
+ //
6226
+ // `edgeCopyRecords` stands in for `argAssignsFor` because it mints no swap-cycle temp — a
6227
+ // name minted here would change the output of every arm emitted after it — and it answers
6228
+ // the same question: `sequentialize` emits at least one statement per record and none for
6229
+ // none. It is not a pure query either: an `undef` copy it drops is recorded in
6230
+ // `droppedUndefCopies`, which a postcondition reads. So the records this question leaves are
6231
+ // rolled back, as the speculative dispatch hoist does with its own.
6232
+ const armRefusal = (exits: Map<Block, ArmExit>, chain: Block[]): { at: number; raise: () => never } | null => {
6233
+ const order: { entry: Block; edge: (typeof succ)[number]; values: number[] | null }[] = [
6234
+ ...chain.map((e) => armOf.get(e)!),
6235
+ { entry: defEdge.block, edge: defEdge, values: null },
6236
+ ];
6237
+ const auditMark = droppedUndefCopies.length;
6238
+ try {
6239
+ for (const [i, a] of order.entries()) {
6240
+ const exit = exits.get(a.entry)!;
6241
+ if (exit.kind === 'unstructurable') {
6242
+ const { why } = exit;
6243
+ return {
6244
+ at: i,
6245
+ raise: () => {
6246
+ throw new StructureError(`cannot structure '${fn.name}': ${why}`);
6247
+ },
6248
+ };
6249
+ }
6250
+ if (exit.kind !== 'fallthrough') {
6251
+ continue;
6252
+ }
6253
+ const next = order[i + 1];
6254
+ if (next?.entry !== exit.to) {
6255
+ const { values } = a;
6256
+ return {
6257
+ at: i,
6258
+ raise: () => {
6259
+ throw new StructureError(
6260
+ `cannot structure '${fn.name}': ${values ? `case ${values.join('/')}` : 'the default arm'} falls ` +
6261
+ `through into an arm that is not the next one emitted — C fall-through only reaches the arm below`,
6262
+ );
6263
+ },
6264
+ };
6265
+ }
6266
+ // The arm fallen INTO opens with its own switch-edge copies, which are how the dispatch
6267
+ // hands it its block parameters. On the fall-through path those copies would RE-RUN and
6268
+ // overwrite what the falling arm just computed (`case 0: v=7; case 1: v=5;` — the case-0
6269
+ // path calling with 5). They cannot simply be dropped either: entering that arm by its
6270
+ // own case value needs them. Hoisting them above the switch is possible but not always
6271
+ // safe (another arm may read the same name first), so this shape declines LOUD;
6272
+ // recovering it is future work. The one edge that needs no copies is a default edge the
6273
+ // dispatch never takes (the op's `exhaustive` attr): nothing enters the default by it,
6274
+ // so its copies are not emitted (`edgeCopies` below) and cannot re-run.
6275
+ if (!(deadDefaultEdge && next.edge === defEdge) && edgeCopyRecords(b, next.edge).length > 0) {
6276
+ return {
6277
+ at: i,
6278
+ raise: () => {
6279
+ throw new StructureError(
6280
+ `cannot structure '${fn.name}': the case fallen into takes a value from the switch edge, ` +
6281
+ `which the fall-through path would re-run`,
6282
+ );
6283
+ },
6284
+ };
6285
+ }
6286
+ }
6287
+ return null;
6288
+ } finally {
6289
+ droppedUndefCopies.length = auditMark;
6290
+ }
6291
+ };
6292
+ let read = readArms(merge);
6293
+ // THE RETRY: A SWITCH WHOSE ARMS DO NOT ALL REACH THE CODE AFTER IT. When neither
6294
+ // post-dominance nor the every-successor reading gives the table an end, the `?? stop`
6295
+ // reading above puts the default block among the siblings, so every plain `break` reads as
6296
+ // falling INTO it — and the function declines wherever that does not spell. One arm that
6297
+ // `return`s on its own is enough to cause it (`case 4: …; return TRUE;` beside four `break`s
6298
+ // and a `return FALSE;` after the switch): its `ret` is reached by no other successor, so
6299
+ // the every-successor intersection is empty although the rest plainly share an end. So is a
6300
+ // `case 1:` that falls into a returning `case 2:`, and so are four returning arms beside one
6301
+ // `break` that carries a value to the code after the switch.
6302
+ //
6303
+ // The DEFAULT-ANCHORED reading (`sharedRetsOf`) names that end by the dispatch's DEFAULT
6304
+ // edge, which goes to the `default:` arm or, where the source wrote none, straight to the
6305
+ // code after the switch. It keeps the successors that share a `ret` with the default edge's
6306
+ // target, and derives the follow from them alone. That is a candidate, not evidence: a
6307
+ // `default:` that `return`s on its own has a `ret` of its own, and the follow it derives
6308
+ // then sits inside that arm, with every `break` spelled as a `return` — the same program,
6309
+ // and not the one the source wrote (KMC and gcc 2.7.2 recompile it to different code).
6310
+ //
6311
+ // THE LAYOUT TELLS THEM APART, and nothing in the graph does: `case 0: …; break;` beside
6312
+ // returning arms and a returning `default:` is the same CFG as returning arms beside one
6313
+ // arm that carries a value past the switch to a store the default edge shares. What
6314
+ // differs is where the code the anchor leaves out sits. The code after a switch is laid
6315
+ // out after every arm, so an arm the anchor leaves out — one that returns on its own, or
6316
+ // reaches the switch's real end — sits above that end only if the candidate is the real
6317
+ // end. Where any block a left-out successor reaches is laid out past the candidate, the
6318
+ // candidate is inside an arm and the retry is refused.
6319
+ //
6320
+ // That reads `layoutIndex` at a strength of its own: only that no block of an arm was moved
6321
+ // past the code after the switch — not the arm order `switchArmsFollowLayout` places, so it
6322
+ // is not behind that declaration. Where it fails — PPC's synthetic return blocks sort after
6323
+ // every real one — it can only refuse, so its cost is a decline the `stop` reading already
6324
+ // made, never a spelling.
6325
+ //
6326
+ // A RETRY, NOT A THIRD LINK IN THE `??` CHAIN. Where the `stop` reading spells, it is legal
6327
+ // C already, and asking the anchored reading first re-spells three shapes whose `stop`
6328
+ // spelling is published (a real fall-through moved after the switch, a value-carrying
6329
+ // fall-through edge lifted where it declined, a Pascal decline re-worded) with no row asking
6330
+ // for it. So it is read only where the `stop` reading DECLINES — any refusal `readArms`
6331
+ // reports, which is every refusal the emission below throws — and adopted only where it
6332
+ // declines nothing, so it can turn a decline into a lift and never one lift into another,
6333
+ // nor one decline's stated reason into another's.
6334
+ //
6335
+ // NEVER AT A CASE ARM. Where the arms that return on their own are the many and the arms that
6336
+ // share an end are one arm and the arm it falls into (`case 0: …` running on into `case 1:`,
6337
+ // every other arm its own `return`), the follow the anchored reading can derive is that
6338
+ // second arm's ENTRY. Ending the switch there spells `case 1:` as an empty arm and moves its
6339
+ // body after the switch — the same program, but a spelling no source writes and the one the
6340
+ // `stop` reading's own refusal is the honest answer for. So the retry ends the switch only
6341
+ // at the default block or past every arm.
6342
+ //
6343
+ // THE DEFAULT BLOCK STAYS ELIGIBLE WHEN THE TABLE'S HOLES MAKE IT AN ARM. A slot no `case`
6344
+ // names holds the default edge's target, so on a table with a hole that block is an arm by
6345
+ // construction, but not one with a body: a hole runs what an unmatched value runs, and ending
6346
+ // the switch there spells it `case 1: break;`. The rule above refuses an arm that is NOT the
6347
+ // default edge's target, whose body would move after the switch, and only that arm.
6348
+ if (joined === null && read.declines) {
6349
+ const found = followOverReturns(b, stop, defEdge.block);
6350
+ const pastEnd = (end: Block): boolean =>
6351
+ leftOutOf(b, reachFrom, defEdge.block).some((s) =>
6352
+ [s, ...reachFrom(s)].some((x) => x !== b && layoutIndex(x) > layoutIndex(end)),
6353
+ );
6354
+ const over = found !== null && (found === defEdge.block || !armOf.has(found)) && !pastEnd(found) ? found : null;
6355
+ const retry = over === null ? null : readArms(over);
6356
+ if (retry !== null && !retry.declines) {
6357
+ hooks.onEarlyReturnFollow?.({ block: fn.blocks.indexOf(b), follow: fn.blocks.indexOf(over!) });
6358
+ merge = over;
6359
+ read = retry;
5532
6360
  }
5533
6361
  }
6362
+ const exitOf = read.exits;
5534
6363
  // A language whose `case` cannot fall through (Pascal) has no spelling for this shape, and
5535
6364
  // Regime B has no second recovery to fall back on — so it fails LOUD, as it does for every
5536
6365
  // other unspellable jump table. (Regime A, which does have a fallback, declines to
5537
6366
  // if-recovery instead: switch-recover.ts's PRE2.)
5538
- if (!spellSwitchFallthrough && [...exitOf.values()].some((e) => e.kind === 'fallthrough')) {
6367
+ if (!spellSwitchFallthrough && read.falls) {
5539
6368
  throw new StructureError(
5540
6369
  `cannot structure '${fn.name}': a jump-table case runs on into the next case, and the target ` +
5541
6370
  `language has no fall-through in its case statement`,
5542
6371
  );
5543
6372
  }
5544
- if (switchArmsFollowLayout) {
5545
- arms.sort((x, y) => layoutIndex(x.entry) - layoutIndex(y.entry));
5546
- }
5547
- // …and then the CHAIN re-threads that order for the falling arms, exactly as it does for the
5548
- // comparison tree: ONE definition (`chainArms`) for the linearization, the adjacency it
5549
- // guarantees, and the three shapes no linear order spells. The arm-order policy above is read
5550
- // for the chain HEADS only — the same per-SITE reading Regime A makes of the same
5551
- // declaration, rather than a whole-switch gate on every arm being closed.
5552
- const preChain = arms.map((a) => a.entry);
5553
- // A table whose default block is ALSO a case target is that case's arm, not a separate one:
5554
- // it is already in `preChain`, and `defaultLayoutPos` withholds the label's position for it.
5555
- const dfltArm = defEdge.block !== merge && !armOf.has(defEdge.block) ? defEdge.block : null;
5556
- const chained = chainArms(preChain, dfltArm, exitOf);
6373
+ const chained = read.chain;
5557
6374
  if (chained === null) {
5558
6375
  throw new StructureError(
5559
6376
  `cannot structure '${fn.name}': the jump table's case arms do not linearize — two arms fall ` +
@@ -5583,38 +6400,34 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
5583
6400
  // Each arm's switch-edge copies, computed ONCE and in emission order: `argAssignsFor` mints
5584
6401
  // swap-cycle temp names, so calling it twice for one edge burns a temp number and changes the
5585
6402
  // output (the same reason emitDoWhile reuses its `updates`).
5586
- const edgeCopies = emitOrder.map((a) => argAssignsFor(b, a.edge));
6403
+ // A dead default edge carries none: the default runs only when a case falls into it, and that
6404
+ // case's own edge copies hand it its values.
6405
+ const edgeCopies = emitOrder.map((a) => (deadDefaultEdge && a.edge === defEdge ? [] : argAssignsFor(b, a.edge)));
6406
+ // A default edge the dispatch never takes (the op's `exhaustive` attr) gets no arm: its block
6407
+ // runs under whatever the dispatch path left in the registers, a state the machine never gives
6408
+ // it there. Unless a case runs on into that block — then the label is where that case goes.
6409
+ // A table whose hole names that block already gives it a case arm (`dflt` is null in
6410
+ // `readArms`), and a fall into the block lands under that arm's labels, never the default's.
6411
+ const deadDefault =
6412
+ deadDefaultEdge &&
6413
+ (armOf.has(defEdge.block) ||
6414
+ ![...exitOf.values()].some((e) => e.kind === 'fallthrough' && e.to === defEdge.block));
5587
6415
  const bodies = emitOrder.map((a, i) => {
5588
- const exit = exitOf.get(a.entry)!;
5589
- if (exit.kind === 'unstructurable') {
5590
- throw new StructureError(`cannot structure '${fn.name}': ${exit.why}`);
5591
- }
5592
- const ft = exit.kind === 'fallthrough';
5593
- const next = emitOrder[i + 1];
5594
- // Re-read off the EMISSION array rather than trusted from `chainArms` — the seam where a
5595
- // position acquires control-flow meaning, checked on the same side of it as Regime A's.
5596
- if (ft && next?.entry !== exit.to) {
5597
- throw new StructureError(
5598
- `cannot structure '${fn.name}': ${a.values ? `case ${a.values.join('/')}` : 'the default arm'} falls ` +
5599
- `through into an arm that is not the next one emitted — C fall-through only reaches the arm below`,
5600
- );
6416
+ // The refusal `armRefusal` found for this reading, thrown at the arm it names, so an
6417
+ // earlier arm's own refusal speaks first. Its order is this emission array, so "the next
6418
+ // arm" is read off the seam where a position acquires control-flow meaning, as Regime A's is.
6419
+ if (read.refusal !== null && read.refusal.at === i) {
6420
+ read.refusal.raise();
5601
6421
  }
5602
- // The arm fallen INTO opens with its own switch-edge copies, which are how the dispatch hands
5603
- // it its block parameters. On the fall-through path those copies would RE-RUN and overwrite
5604
- // what the falling arm just computed (`case 0: v=7; case 1: v=5;` — the case-0 path calling
5605
- // with 5). They cannot simply be dropped either: entering that arm by its own case value
5606
- // needs them. Hoisting them above the switch is possible but not always safe (another arm may
5607
- // read the same name first), so this shape declines LOUD; recovering it is future work.
5608
- if (ft && edgeCopies[i + 1].length) {
5609
- throw new StructureError(
5610
- `cannot structure '${fn.name}': the case fallen into takes a value from the switch edge, ` +
5611
- `which the fall-through path would re-run`,
5612
- );
6422
+ if (deadDefault && i === emitOrder.length - 1) {
6423
+ return { body: [], fallsThrough: false };
5613
6424
  }
6425
+ const exit = exitOf.get(a.entry)!;
6426
+ const ft = exit.kind === 'fallthrough';
5614
6427
  return {
5615
6428
  // A falling-through arm stops AT its successor arm, which then emits that body once under
5616
6429
  // its own labels; a closed arm runs to the merge as before.
5617
- body: [...edgeCopies[i], ...structureRegion(a.entry, ft ? exit.to : merge)],
6430
+ body: [...edgeCopies[i], ...structureArm(a.entry, ft ? exit.to : merge, merge, exit)],
5618
6431
  fallsThrough: ft,
5619
6432
  };
5620
6433
  });
@@ -5696,24 +6509,64 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
5696
6509
  li.header.params.forEach(
5697
6510
  (_, i) => initArgs[i] !== undefined && entryVals.set(li.backArgOfParam[i], initArgs[i]),
5698
6511
  );
6512
+ // A CALL OR A LOAD IS ONE EVALUATION, AT ONE POINT. Read through, it equals another of one
6513
+ // callee or one address on the same operands only where each runs at its own test. The
6514
+ // guard's runs there when it sits in the guard block with no effect after it. The loop's
6515
+ // runs there when it sits in the header or its pure preheader: the fused `while` renders the
6516
+ // header's test at its top, so its first evaluation runs right after the guard's, and the
6517
+ // `while`'s first test is the evaluation that replaces the guard's — a store or call ahead
6518
+ // of it in the header is the previous iteration's body, at the top of this one. Read through
6519
+ // anywhere else, a cached result would equal a fresh one: a call the function made once
6520
+ // ahead of both (`if (v < f(a)) do {} while (v < v)` would lose the `f(a)`), or a load ahead
6521
+ // of a call or store that changes what it reads (`v = *p; g(); if (i < *p) do {} while
6522
+ // (i < v)` would lose the guard and its reload).
6523
+ const evaluatedAtTest = (v: Value): boolean => {
6524
+ const d = defs.get(v);
6525
+ if (d === undefined || !orderSensitive(d)) {
6526
+ return true;
6527
+ }
6528
+ const at = opBlock.get(d);
6529
+ if (at === b) {
6530
+ return !b.ops.slice(opIndex.get(d)! + 1).some(effectful);
6531
+ }
6532
+ return at !== undefined && (at === li.header || at === li.preheader);
6533
+ };
5699
6534
  // (1) Is the guard PROVABLY the loop's own test? Fusing DELETES it and lets the `while`
5700
6535
  // re-test, so an unproven fuse would lose the `if` outright and run a loop the source
5701
6536
  // skipped. Structural equality is a sufficient proof, never a necessary one: `str[0]` and
5702
6537
  // `str[i]` at `i = 0`, or a signed `e <= 0` beside an unsigned `e != 0`, are the same test
5703
- // spelled differently, and normalising those needs a canonical form this pass does not
5704
- // have. An unproven guard therefore KEEPS its `if`, with the loop as a `do-while` inside it
5705
- // — every test the asm performs is emitted as itself. The proof still gates the SINK below:
5706
- // a sunk copy runs once per iteration, and only the proof (fused form) or the kept `if`
5707
- // makes the zero-trip path skip it. The kept `if` would widen the sink's key to unproven
5708
- // guards too; no row needs that yet, so it stays keyed on the proof.
6538
+ // spelled differently. The first is one of the identities `sameAtEntry` (ir/entry.ts) reads
6539
+ // under `normalize`, which the raise re-roll turns on; this caller keeps it off, since it
6540
+ // re-spells rows (agbcc `while (s[i]) i++` fuses into a `for`). The second has no reading.
6541
+ // An unproven guard KEEPS its `if`, with the loop as a `do-while` inside it — every test
6542
+ // the asm performs is emitted as itself. The proof still gates the SINK below: a sunk copy
6543
+ // runs once per iteration, and only the proof (fused form) or the kept `if` makes the
6544
+ // zero-trip path skip it. The kept `if` would widen the sink's key to unproven guards too;
6545
+ // no row needs that yet, so it stays keyed on the proof.
5709
6546
  const enterIsTaken = takenB === h;
5710
6547
  const contIsTaken = hterm.successors[0].block === li.header;
5711
- const guardProven = sameAtEntry(hterm.operands[0], term.operands[0], entryVals, enterIsTaken !== contIsTaken);
6548
+ const guardProven = sameAtEntry(
6549
+ hterm.operands[0],
6550
+ term.operands[0],
6551
+ entryVals,
6552
+ enterIsTaken !== contIsTaken,
6553
+ evaluatedAtTest,
6554
+ );
5712
6555
  // Fused `while` additionally requires a header free of MATERIALIZED defs: their temps are
5713
6556
  // assigned only inside the body (sideEffects), and an un-rotated `while` condition renders
5714
6557
  // BEFORE the body ever ran — reading a temp uninitialized on the first test. The kept-guard
5715
6558
  // `do-while` tests after the body, so it carries no such restriction.
5716
6559
  const fused = guardProven && !li.header.ops.some((o) => materialize.has(o));
6560
+ // A KEPT GUARD IS AN ORDINARY `if` AROUND AN ORDINARY `do-while`, and emitWhile's hazards
6561
+ // below are the price of merging the guard's exit edge into the loop's post-loop copies,
6562
+ // which only the fused `while` needs, since it drops that edge. So where a kept guard meets
6563
+ // one of them, it is not claimed here: the `if` recovery below takes the guard, carrying each
6564
+ // exit edge's copies on that edge, and the loop reaches `structureBlock` as the do-while it
6565
+ // is (`keptGuardDoWhiles`), with the do-while's repairs — a `++` for the test's own
6566
+ // pre-update read, the exit-copy sink, the pre-update homes. Asked only where emitWhile
6567
+ // declines, so a guarded loop that structures here keeps its spelling. IDO's stolen-slot
6568
+ // latch reaches it (`strlen1`: `if (*s) do { t = s[1]; n++; s++; } while (t);`).
6569
+ const handBack = (): boolean => !fused && keptGuardDoWhiles.has(li.header);
5717
6570
  // A pre-update exit copy is repairable rather than fatal: emitted INSIDE the body, ahead of
5718
6571
  // the update, it captures the value the exit edge carries rather than the updated one.
5719
6572
  // Its post-loop copy is then dropped, so the ZERO-TRIP path needs the guard→exit edge as a
@@ -5729,7 +6582,7 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
5729
6582
  sub,
5730
6583
  updateWrites,
5731
6584
  hooks.preUpdateSinkGates ?? PREUPDATE_SINK_GATES,
5732
- (j) => !sameAtEntry(hexitArgs[j], guardExit.args[j] as Value, entryVals),
6585
+ (j) => !sameAtEntry(hexitArgs[j], guardExit.args[j] as Value, entryVals, false, evaluatedAtTest),
5733
6586
  )
5734
6587
  : new Map<number, Op | null>();
5735
6588
  // (2) Every exit copy the fused form KEEPS renders after the loop, on the zero-trip path
@@ -5737,9 +6590,12 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
5737
6590
  // what the guard→exit edge carries, since that edge is dropped. A sunk slot is exempt: its
5738
6591
  // seed and its body copy cover the two paths separately.
5739
6592
  const staleExit = hexitArgs.findIndex(
5740
- (a, j) => !sunk.has(j) && !sameAtEntry(a, guardExit.args[j] as Value, entryVals),
6593
+ (a, j) => !sunk.has(j) && !sameAtEntry(a, guardExit.args[j] as Value, entryVals, false, evaluatedAtTest),
5741
6594
  );
5742
6595
  if (staleExit >= 0) {
6596
+ if (handBack()) {
6597
+ break;
6598
+ }
5743
6599
  throw new StructureError(
5744
6600
  `cannot structure '${fn.name}': the fused guard's exit edge carries a value the post-loop ` +
5745
6601
  `copies do not reproduce on a zero-trip run`,
@@ -5757,6 +6613,9 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
5757
6613
  null,
5758
6614
  )
5759
6615
  ) {
6616
+ if (handBack()) {
6617
+ break;
6618
+ }
5760
6619
  // NO `n++` REPAIR HERE, and the SHAPE is why rather than an oversight. The fold
5761
6620
  // (`preUpdateCondFold`) trades the update copy at the foot of the body for a `++` at the
5762
6621
  // leaf, which is the same program only for a BOTTOM test: it runs once per iteration, in
@@ -5774,6 +6633,9 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
5774
6633
  const seed = argAssigns(b, li.exit, null, sunkSlot);
5775
6634
  const seedWrites = new Set(seed.filter((st) => st.k === 'assign').map((st) => st.name));
5776
6635
  if (initArgs.some((a) => readsClobbered(a, new Map(), seedWrites))) {
6636
+ if (handBack()) {
6637
+ break;
6638
+ }
5777
6639
  throw new StructureError(
5778
6640
  `cannot structure '${fn.name}': seeding the zero-trip value would overwrite a value the ` +
5779
6641
  `loop initialisation reads`,
@@ -5826,6 +6688,9 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
5826
6688
  hexitArgs.some((a, j) => !sunk.has(j) && reachesBodyTemp(a)) ||
5827
6689
  [...(liveIn.get(li.exit) ?? [])].some(bodyTemp)
5828
6690
  ) {
6691
+ if (handBack()) {
6692
+ break;
6693
+ }
5829
6694
  throw new StructureError(
5830
6695
  `cannot structure '${fn.name}': a post-loop read reaches a temp the guarded body may never assign`,
5831
6696
  );
@@ -5857,6 +6722,9 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
5857
6722
  // `gsub`, whose mapped values the inits deliberately hold.
5858
6723
  const writes = new Set([...seedWrites, ...updateWriteSet(inits)]);
5859
6724
  if (readsClobbered(term.operands[0], gsub, writes)) {
6725
+ if (handBack()) {
6726
+ break;
6727
+ }
5860
6728
  throw new StructureError(
5861
6729
  `cannot structure '${fn.name}': the kept guard's condition reads a name the loop initialisation overwrites`,
5862
6730
  );
@@ -5956,7 +6824,16 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
5956
6824
  //
5957
6825
  // Taken only where the implicit continue holds (`continueFallsToBottom`). Where it does not, a
5958
6826
  // 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)) {
6827
+ // A switch arm opened in this loop, with no loop entered since: C binds a `break;` here to the
6828
+ // switch, so neither rule below may mint the loop's (see the second).
6829
+ const armFrame = switchFrames.length > 0 ? switchFrames[switchFrames.length - 1] : undefined;
6830
+ const armCapturesBreak = loopCtx !== null && armFrame !== undefined && armFrame.loop === loopCtx;
6831
+ if (
6832
+ loopCtx &&
6833
+ continueFallsToBottom(loopCtx, stop) &&
6834
+ (takenB === loopCtx.header || fallB === loopCtx.header) &&
6835
+ !(armCapturesBreak && (takenB === loopCtx.exit || fallB === loopCtx.exit))
6836
+ ) {
5960
6837
  const contIsTaken = takenB === loopCtx.header;
5961
6838
  const exitB = contIsTaken ? fallB : takenB;
5962
6839
  const isBreak = exitB === loopCtx.exit;
@@ -5998,7 +6875,7 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
5998
6875
  } // continue is `taken` → leave when NOT it
5999
6876
  const exitArm =
6000
6877
  rule && 'bare' in rule
6001
- ? [...(rule.bare ? [] : argAssigns(b, loopCtx.exit, sub)), { k: 'break' } as Stmt] // break to the loop exit
6878
+ ? [...(rule.bare ? [] : argAssigns(b, exitB, sub)), { k: 'break' } as Stmt] // break to the loop exit
6002
6879
  : withSub(sub, () => [...argAssigns(b, exitB, sub), ...structureRegion(exitB, stop)]); // early return
6003
6880
  out.push(mkIf(leaveCond, exitArm, []));
6004
6881
  return out;
@@ -6023,11 +6900,29 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
6023
6900
  // always loud. The others are loud, since the loop was admitted on the promise of this spelling,
6024
6901
  // unless an arm owns the edge or the exit ends in a `ret`: then if-recovery below copies the
6025
6902
  // 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)) {
6903
+ // Inside a switch arm opened in THIS loop (no loop entered since), C binds `break;` to the
6904
+ // switch, so the loop's `break` cannot be spelled here. An arm spells an edge to the loop's exit
6905
+ // only where that exit is the switch's own merge: the code after the switch then IS the exit
6906
+ // region, and the edge structures as any other edge to the merge — the arm's `break;`
6907
+ // (`switchBreakAt`), or nothing in a closed arm, which stops there. An exit whose tail returns
6908
+ // (or that an early-return arm owns) needs no `break`: if-recovery below copies the tail into the
6909
+ // arm, as it does outside a switch when the break rule refuses. Any other exit is refused here,
6910
+ // loud, rather than handed to the printer as a `break` it would rebind.
6911
+ if (loopCtx && loopCtx.breaks.has(b) && armCapturesBreak) {
6912
+ const exit = loopCtx.exit;
6913
+ const tailReturns = exit !== null && (isArm(exit) || isRet(exit));
6914
+ if ((exit === null || armFrame!.merge !== exit) && !tailReturns) {
6915
+ throw new StructureError(
6916
+ `cannot structure '${fn.name}': a case body leaves its switch for a loop exit past the ` +
6917
+ `switch's end (block #${fn.blocks.indexOf(b)}), where \`break\` would leave only the switch`,
6918
+ );
6919
+ }
6920
+ } else if (loopCtx && loopCtx.breaks.has(b)) {
6029
6921
  const frame = loopCtx;
6030
- const breakIsTaken = takenB === frame.exit;
6922
+ // An arm that runs code before it breaks (`codeBreakArm`) is rendered inside the `if`, ahead of
6923
+ // the `break`, and its last block's copies into the exit are the break's.
6924
+ const arm = frame.breakArms?.find((a) => a.from === b && (a.to === takenB || a.to === fallB));
6925
+ const breakIsTaken = takenB === (arm?.to ?? frame.exit);
6031
6926
  const stayB = breakIsTaken ? fallB : takenB;
6032
6927
  const continueRefused = stayB === frame.header && !continueFallsToBottom(frame, stop);
6033
6928
  const rule = continueRefused
@@ -6041,11 +6936,21 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
6041
6936
  leaveCond = negateCond(leaveCond);
6042
6937
  }
6043
6938
  const [breakEdge, stayEdge] = breakIsTaken ? term.successors : [term.successors[1], term.successors[0]];
6044
- out.push(mkIf(leaveCond, [...(rule.bare ? [] : argAssignsFor(b, breakEdge)), { k: 'break' }], []));
6939
+ out.push(
6940
+ mkIf(
6941
+ leaveCond,
6942
+ [
6943
+ ...(rule.bare ? [] : argAssignsFor(b, breakEdge)),
6944
+ ...(arm !== undefined ? structureRegion(arm.to, frame.exit) : []),
6945
+ { k: 'break' },
6946
+ ],
6947
+ [],
6948
+ ),
6949
+ );
6045
6950
  out.push(...argAssignsFor(b, stayEdge), ...structureRegion(stayB, stop));
6046
6951
  return out;
6047
6952
  }
6048
- if (continueRefused || (!isArm(frame.exit) && !isRet(frame.exit))) {
6953
+ if (continueRefused || frame.exit === null || (!isArm(frame.exit) && !isRet(frame.exit))) {
6049
6954
  throw new StructureError(
6050
6955
  `cannot structure '${fn.name}': a break out of block #${fn.blocks.indexOf(b)} ${rule.refusal}`,
6051
6956
  );
@@ -6078,10 +6983,9 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
6078
6983
  // `stop` stands in for the real join, so it has to BE the real join: one side must already end
6079
6984
  // this region — an admitted arm (its `return` terminates that path) or `stop` itself. Two sides
6080
6985
  // meeting somewhere further down instead would each re-emit everything from there to the bottom,
6081
- // doubling per nesting level; those keep the CFG join and the old loud decline. Lifting that
6082
- // needs a post-dominator computed over the body alone with the arms DELETED — treating them as
6083
- // exits does not work, since post-dominance cannot express "where the paths that did not
6084
- // return meet".
6986
+ // doubling per nesting level; those take the post-dominator computed over the body alone with
6987
+ // the back edges and the arms DELETED (`loopJoin`, below) — treating the arms as exits does
6988
+ // not work, since post-dominance cannot express "where the paths that did not return meet".
6085
6989
  const clampToLoop =
6086
6990
  loopCtx !== null &&
6087
6991
  ipd !== null &&
@@ -6089,11 +6993,44 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
6089
6993
  term.successors.some((sc) => isArm(sc.block) || sc.block === stop) &&
6090
6994
  term.successors.every((sc) => loopCtx!.body.has(sc.block) || isArm(sc.block));
6091
6995
  // 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
6996
+ // (`loopJoin`): a branch whose one arm continues is post-dominated, in the CFG, by wherever
6093
6997
  // the other arm leaves the loop from.
6094
6998
  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);
6999
+ // A break arm's own blocks join as any code outside the loop does: they are not the body's.
7000
+ const inArm = loopCtx?.breakArms?.some((a) => a.region.has(b)) === true;
7001
+ const bodyJoin = inForever === undefined || inArm ? undefined : loopJoin(inForever, b, true);
7002
+ // THE SAME QUESTION IN A TESTED LOOP. A branch whose one way leads out of the body by a `break` —
7003
+ // the `else break;` of an if/else-if chain — has the loop's exit as its CFG join, and its in-body
7004
+ // arm walked from there through the latch back into the header (`onStack`). Its join is where its
7005
+ // paths that stay in the body meet: the post-dominator over the body with the back edges and the
7006
+ // exits deleted (`loopJoin`). Asked only where the CFG join leaves the body, no clamp applies,
7007
+ // one successor stays in the body, and every way out of the body from here is one the loop
7008
+ // renders in place (`leavesByOwnedEdges`).
7009
+ const tested =
7010
+ loopCtx !== null &&
7011
+ inForever === undefined &&
7012
+ !inArm &&
7013
+ ipd !== null &&
7014
+ !loopCtx.body.has(ipd) &&
7015
+ !clampToLoop &&
7016
+ loopCtx.body.has(b) &&
7017
+ term.successors.some((sc) => loopCtx!.body.has(sc.block) && sc.block !== loopCtx!.header) &&
7018
+ leavesByOwnedEdges(loopCtx, b)
7019
+ ? loopJoin(loopCtx, b, false)
7020
+ : undefined;
7021
+ // Inside a switch arm, the arm's own join wins over every reading above: each of them reads the
7022
+ // CFG, where the arm's paths meet past the switch.
7023
+ const inArmJoin = armJoin(b, stop);
7024
+ const merge =
7025
+ inArmJoin !== undefined
7026
+ ? (inArmJoin ?? stop)
7027
+ : bodyJoin !== undefined
7028
+ ? (bodyJoin ?? stop)
7029
+ : tested !== undefined
7030
+ ? (tested ?? stop)
7031
+ : clampToLoop
7032
+ ? stop
7033
+ : (ipd ?? stop);
6097
7034
  // Per-successor records, NOT successorTo(b, block): a cond_br whose two edges reach the SAME
6098
7035
  // block with different args would otherwise give both arms the first edge's copies.
6099
7036
  const thenS = [...argAssignsFor(b, term.successors[0]), ...structureRegion(takenB, merge)];
@@ -6206,9 +7143,21 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
6206
7143
  stmtsEqual(thenS, elseS) &&
6207
7144
  testRereadsOnly(term, b, { defs, useSitesOf, dom, ipdom, loops: forest.byHeader.values() })
6208
7145
  ) {
6209
- out.push(...thenS);
7146
+ out.push(...unionStmtsEvidence(thenS, elseS));
6210
7147
  } else {
6211
- out.push(negateHere ? { k: 'if', cond: negateCond(cond), then: elseS, else: thenS } : mkIf(cond, thenS, elseS));
7148
+ // A compare whose runtime helper names the relation the source spelled DECIDES its own sense,
7149
+ // one-armed or two: the arm order is the one that spells that relation, because no spelling
7150
+ // of its negation calls the same helper (l3/ast.ts `keepsHelperRel`). `if (a == b) {} else`
7151
+ // is how the source wrote agbcc's `bl __eqsf2; cmp r0, #0; beq` over the arm.
7152
+ const helperSense = keepsHelperRel(cond) ? false : regainsHelperRel(cond) ? true : undefined;
7153
+ const negate = helperSense ?? negateHere;
7154
+ out.push(
7155
+ negate
7156
+ ? { k: 'if', cond: negateCond(cond), then: elseS, else: thenS }
7157
+ : helperSense === false
7158
+ ? { k: 'if', cond, then: thenS, else: elseS }
7159
+ : mkIf(cond, thenS, elseS),
7160
+ );
6212
7161
  }
6213
7162
  if (merge && merge !== stop) {
6214
7163
  out.push(...structureRegion(merge, stop));
@@ -6347,42 +7296,139 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
6347
7296
  // Mirror the br/cond_br cases: argAssigns then structureRegion. Structure the body under this
6348
7297
  // loop's frame so an in-body conditional exit (break / early return) is recognised instead of
6349
7298
  // tripping the header-re-entry `onStack` guard.
6350
- const body = withLoop({ header: wl.header, exit: wl.exit, body: wl.body, arms: wl.arms, breaks: wl.breaks }, () => [
6351
- ...argAssigns(wl.header, wl.bodyEntry),
6352
- ...structureRegion(wl.bodyEntry, wl.header),
6353
- ]);
7299
+ const body = withLoop(
7300
+ { header: wl.header, exit: wl.exit, body: wl.body, arms: wl.arms, breaks: wl.breaks, breakArms: wl.breakArms },
7301
+ () => [...argAssigns(wl.header, wl.bodyEntry), ...structureRegion(wl.bodyEntry, wl.header)],
7302
+ );
7303
+ // A test whose tail is a float compare the stay sense moves off its runtime helper's relation
7304
+ // (l3/ast.ts `splitHelperBreak`) leaves through a break guard, ahead of the edge's copies: the
7305
+ // header evaluated it before them.
7306
+ const split = splitHelperBreak(cond);
7307
+ if (split !== null) {
7308
+ cond = split.head;
7309
+ body.unshift({ k: 'if', cond: split.guard, then: [{ k: 'break' }], else: [] });
7310
+ }
6354
7311
  const out: Stmt[] = [{ k: 'while', cond, body }];
6355
7312
  out.push(...argAssigns(wl.header, wl.exit)); // phi args carried on the header→exit edge, if any
6356
7313
  out.push(...structureRegion(wl.exit, stop));
6357
7314
  return out;
6358
7315
  };
6359
7316
 
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);
7317
+ // MAY A BRANCH IN A TESTED LOOP JOIN INSIDE THE BODY? Only where every way out of the body from `b`
7318
+ // is one the loop's own spelling renders in place: a `break` (an edge of `breaks`, or the edge the
7319
+ // loop tests at its header or latch) or an early-`return` arm the loop owns. Any other edge out
7320
+ // leaves for code that is copied wherever it is reached, and a join inside the body would write
7321
+ // that tail into the arm as well.
7322
+ const leavesByOwnedEdges = (frame: LoopFrame, b: Block): boolean => {
7323
+ const exitFrom = whileLoops.get(frame.header)?.header ?? doWhileLoops.get(frame.header)?.latch;
7324
+ const seen = new Set<Block>();
7325
+ for (const stack = [b]; stack.length > 0;) {
7326
+ const x = stack.pop()!;
7327
+ if (x === frame.header || seen.has(x)) {
7328
+ continue;
7329
+ }
7330
+ seen.add(x);
7331
+ for (const sx of successorsOf(x)) {
7332
+ if (frame.body.has(sx)) {
7333
+ stack.push(sx);
7334
+ } else if (
7335
+ !frame.arms.some((a) => a.from === x && a.to === sx) &&
7336
+ !(sx === frame.exit && (frame.breaks.has(x) || x === exitFrom))
7337
+ ) {
7338
+ return false;
7339
+ }
7340
+ }
7341
+ }
7342
+ return true;
7343
+ };
7344
+ // THE JOIN OF A BRANCH IN A LOOP BODY, over the body with its back edges and exits deleted: where
7345
+ // the paths from `b` that stay in the body meet, its nearest post-dominator over the rest. Null
7346
+ // when none goes on, and the region's own `stop` ends it. Without the deletion, a branch whose one
7347
+ // arm continues joins at the loop bottom, and whatever the other arm runs before its own continue
7348
+ // is copied into both arms; in a tested loop, the `else break;` of an if/else-if chain joins at
7349
+ // the loop's exit.
7350
+ //
7351
+ // `endsCut` is the `while (1)` rule, and only `emitForever`'s frames take it: a block that only
7352
+ // jumps back to the header or out of the loop, rendering nothing but that edge's copies, ENDS its
7353
+ // path — a `continue`, a `break` — so it constrains no join either. A block that ends every path
7354
+ // but TESTS first is still where its predecessors meet, and so is a latch two body paths reach: the
7355
+ // foot of `for (…) { if (c) {…} }`, where deleting it moves the `if`'s join into one arm, which then
7356
+ // copies the latch and adds a `continue` the source never wrote. A break block two paths share
7357
+ // stays an end, each path its own `break`. A tested loop's latch is where its test renders, so
7358
+ // there it constrains the join like any block.
7359
+ /** per header, one cache for each value of `endsCut` */
7360
+ const loopJoins = [new Map<Block, Map<Block, Block | null>>(), new Map<Block, Map<Block, Block | null>>()];
7361
+ const loopJoin = (loop: { header: Block; body: Set<Block> }, b: Block, endsCut: boolean): Block | null => {
7362
+ const cache = loopJoins[endsCut ? 1 : 0];
7363
+ let ipdom = cache.get(loop.header);
6373
7364
  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);
7365
+ ipdom = regionJoins(loop.body, loop.header, endsCut);
7366
+ cache.set(loop.header, ipdom);
6383
7367
  }
6384
7368
  return ipdom.get(b) ?? null;
6385
7369
  };
7370
+ // The post-dominators `loopJoin` reads, over `body` with every edge out of it and every edge back
7371
+ // to `header` (when there is one) deleted — save an edge to `toExit`, which goes to EXIT. Also
7372
+ // the join inside a switch arm (`armJoin`), whose `toExit` is the sibling a falling arm runs into
7373
+ // and null for a closed one.
7374
+ function regionJoins(
7375
+ body: ReadonlySet<Block>,
7376
+ header: Block | null,
7377
+ endsCut: boolean,
7378
+ toExit: Block | null = null,
7379
+ ): Map<Block, Block | null> {
7380
+ const inBody = (n: Block): Block[] => [...new Set(successorsOf(n))].filter((x) => body.has(x) && x !== header);
7381
+ const ends = (n: Block): boolean =>
7382
+ endsCut &&
7383
+ inBody(n).length === 0 &&
7384
+ n.ops[n.ops.length - 1].opcode === 'br' &&
7385
+ n.ops.every((op) => !effectful(op) && !materialize.has(op)) &&
7386
+ !(successorsOf(n)[0] === header && new Set(preds.get(n)?.filter((q) => body.has(q))).size > 1);
7387
+ const exits = (n: Block): boolean => toExit !== null && successorsOf(n).includes(toExit);
7388
+ // IN A FALLING ARM, A PATH THAT CANNOT FALL ENDS WHERE IT LEAVES THE FALLING PATHS. Deleting the
7389
+ // edge to the merge or a `ret` covers only a jump straight off the branch (`if (b) break;`).
7390
+ // When the leaving path runs code first — `if (b) { g(7); break; }`, or a `return k` through a
7391
+ // shared epilogue — its block keeps no edge on, and a block with none is post-dominated only by
7392
+ // itself, so the guard above it would find no join and copy the rest of the arm into both sides.
7393
+ // Such a block is a jump all the same: an edge into a block that can no longer reach the
7394
+ // fall-through is deleted too.
7395
+ let falls: Set<Block> | null = null;
7396
+ if (toExit !== null) {
7397
+ const live = new Set<Block>();
7398
+ for (let grew = true; grew;) {
7399
+ grew = false;
7400
+ for (const n of body) {
7401
+ if (!live.has(n) && (exits(n) || inBody(n).some((x) => live.has(x) && !ends(x)))) {
7402
+ live.add(n);
7403
+ grew = true;
7404
+ }
7405
+ }
7406
+ }
7407
+ falls = live;
7408
+ }
7409
+ const onward = (n: Block): Block[] => inBody(n).filter((x) => !ends(x) && (falls === null || falls.has(x)));
7410
+ // A CYCLE NO PATH LEAVES. An inner loop at the foot of the body — the vblank wait closing a
7411
+ // `while (1)` main loop, `while (q() == 3);` — keeps only its own back edge once the edges
7412
+ // to the header and to the ends are deleted, so its paths never end and post-dominance
7413
+ // over it is undefined: no branch above it would find a join, and each would re-emit the rest
7414
+ // of the body, doubling per `if`. Such a block ends its path at the edge it leaves the cycle
7415
+ // by. A block that reaches an end keeps its edges as they are, so no defined join moves.
7416
+ const reachesEnd = new Set<Block>();
7417
+ for (let grew = true; grew;) {
7418
+ grew = false;
7419
+ for (const n of body) {
7420
+ if (!reachesEnd.has(n) && (onward(n).length === 0 || exits(n) || onward(n).some((x) => reachesEnd.has(x)))) {
7421
+ reachesEnd.add(n);
7422
+ grew = true;
7423
+ }
7424
+ }
7425
+ }
7426
+ return postDominators(fn, body, (n) =>
7427
+ !exits(n) && (reachesEnd.has(n) || successorsOf(n).every((x) => onward(n).includes(x)))
7428
+ ? onward(n)
7429
+ : [...onward(n), null],
7430
+ );
7431
+ }
6386
7432
 
6387
7433
  // Does rendering `root` under `map` re-derive one of `targets`: reach it through the def tree
6388
7434
  // without passing a mapped or a named value, either of which renders as a name?
@@ -6434,15 +7480,22 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
6434
7480
  // neither reading is the value, decline LOUD, as the do-while latch does.
6435
7481
  const emitForever = (fl: ForeverLoopInfo, stop: Block | null): Stmt[] => {
6436
7482
  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
- );
7483
+ const frame = {
7484
+ header: fl.header,
7485
+ exit: fl.exit,
7486
+ body: fl.body,
7487
+ arms: fl.arms,
7488
+ breaks: fl.breaks,
7489
+ breakArms: fl.breakArms,
7490
+ };
7491
+ const body = withLoop(frame, () => structureBlock(fl.header, fl.header));
6440
7492
  dwActive.delete(fl.header);
6441
7493
  const loop: Stmt = { k: 'while', cond: { k: 'const', value: 1 }, body };
6442
- if (!breaksOut(body)) {
7494
+ const exit = fl.exit;
7495
+ if (exit === null || !breaksOut(body)) {
6443
7496
  return [loop];
6444
7497
  }
6445
- const region = exitRegion(fl.exit, fl.body, stop);
7498
+ const region = exitRegion(exit, fl.body, stop);
6446
7499
  const { sub: innerSub, unreadable } = latchInnerSub(fl.header, fl.body, [...fl.breaks], region);
6447
7500
  if (unreadable.size > 0 && rootsOf(region).some((r) => rendersThrough(r, innerSub, unreadable))) {
6448
7501
  throw new StructureError(
@@ -6452,7 +7505,7 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
6452
7505
  }
6453
7506
  return [
6454
7507
  loop,
6455
- ...(innerSub.size > 0 ? withSub(innerSub, () => structureRegion(fl.exit, stop)) : structureRegion(fl.exit, stop)),
7508
+ ...(innerSub.size > 0 ? withSub(innerSub, () => structureRegion(exit, stop)) : structureRegion(exit, stop)),
6456
7509
  ];
6457
7510
  };
6458
7511
 
@@ -6967,11 +8020,19 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
6967
8020
  // `synthetic:dma_fill_uninit` loses its record here, and so did `kleod:ProcessInputAndUpdateEntities`
6968
8021
  // before that row was retired (2026-09-13).
6969
8022
  const frameRecord = (at: Op): { frame?: { loads: number; stores: number } } => {
8023
+ if (at.attrs[OWN_RETURN_OBJECT] === true || at.attrs[CALL_RETURN_OBJECT] !== undefined) {
8024
+ return {};
8025
+ }
6970
8026
  const off = at.attrs.off as number;
6971
8027
  const roots = new Set<Value>();
6972
8028
  for (const b of fn.blocks) {
6973
8029
  for (const op of b.ops) {
6974
- if (op.opcode === 'laddr' && (op.attrs.off as number) === off) {
8030
+ if (
8031
+ op.opcode === 'laddr' &&
8032
+ (op.attrs.off as number) === off &&
8033
+ op.attrs[OWN_RETURN_OBJECT] !== true &&
8034
+ op.attrs[CALL_RETURN_OBJECT] === undefined
8035
+ ) {
6975
8036
  roots.add(op.results[0]);
6976
8037
  }
6977
8038
  }
@@ -6988,8 +8049,16 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
6988
8049
  stores++;
6989
8050
  }
6990
8051
  }
8052
+ // a struct passed by value is read whole at the call: the word loads the asm made of it
8053
+ const passed = byValueOperands(op)
8054
+ .map((b) => b.at)
8055
+ .filter((at) => roots.has(op.operands[at]));
8056
+ passed.forEach((at) => {
8057
+ const t = op.operands[at].type;
8058
+ loads += t.kind === 'ptr' && t.to.kind === 'struct' ? Math.ceil((t.to.size ?? 4) / 4) : 1;
8059
+ });
6991
8060
  // every OTHER mention of the address, operands and branch arguments alike
6992
- const elsewhere = op.operands.some((v, i) => roots.has(v) && !(access && i === 0));
8061
+ const elsewhere = op.operands.some((v, i) => roots.has(v) && !(access && i === 0) && !passed.includes(i));
6993
8062
  if (elsewhere || op.successors.some((sx) => sx.args.some((v) => roots.has(v)))) {
6994
8063
  return {};
6995
8064
  }
@@ -7075,7 +8144,8 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
7075
8144
  // halves, so a change to either fails there.
7076
8145
  ...fn.blocks
7077
8146
  .flatMap((b) => b.ops)
7078
- .filter((op) => op.opcode === 'undef')
8147
+ .filter((op) => op.opcode === 'undef' && !coalescedUndefs.has(op))
8148
+ .filter((op, i, all) => all.findIndex((o) => undefName.get(o) === undefName.get(op)) === i)
7079
8149
  .map((op) => ({
7080
8150
  name: undefName.get(op)!,
7081
8151
  type: varType.get(undefName.get(op)!) ?? op.results[0].type,
@@ -7090,6 +8160,7 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
7090
8160
  }
7091
8161
  : {}),
7092
8162
  ...(localStatics.statics.length ? { statics: localStatics.statics } : {}),
8163
+ ...(localStatics.images.length ? { images: localStatics.images } : {}),
7093
8164
  retType: returnsVoid ? T.void() : returnType(fn),
7094
8165
  body,
7095
8166
  ...(structs.length ? { structs } : {}),
@@ -7098,6 +8169,8 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
7098
8169
  ...(spillSlotOrder !== undefined ? { slotOrder: spillSlotOrder } : {}),
7099
8170
  ...calledOnly({ declaredArgs, declaredReturns }, body),
7100
8171
  ...(dialect !== undefined ? { dialect } : {}),
8172
+ ...(designatedInitializers ? { designators: true as const } : {}),
8173
+ ...(strictPointerConversions ? { strictConversions: true as const } : {}),
7101
8174
  };
7102
8175
  if (fn.localObjects === undefined) {
7103
8176
  return tree;
@@ -7108,9 +8181,30 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
7108
8181
  `cannot structure '${fn.name}': names a function-scope static ('${named.symbol}') ${named.refused}`,
7109
8182
  );
7110
8183
  }
8184
+ // The compiler's anonymous constant is spelled only as the initializer of the locals its copies
8185
+ // fill, which the emitter folds (l3/constinit.ts says why there). Asked here, so a function
8186
+ // whose image cannot fold declines loudly rather than reaching an emitter that cannot print it.
8187
+ const image = constantImageRefusal(named);
8188
+ if (image !== null) {
8189
+ throw new StructureError(
8190
+ `cannot structure '${fn.name}': reads the compiler's anonymous constant ('${image.image}') ${image.refused}`,
8191
+ );
8192
+ }
7111
8193
  return named;
7112
8194
  }
7113
8195
 
8196
+ /** Each declared-callee table's own key order, name → position, computed once per table object: the
8197
+ * ranked pass structures every candidate against the same tables. */
8198
+ const keyOrders = new WeakMap<object, ReadonlyMap<string, number>>();
8199
+ function keyOrder(table: object): ReadonlyMap<string, number> {
8200
+ let order = keyOrders.get(table);
8201
+ if (order === undefined) {
8202
+ order = new Map(Object.keys(table).map((n, i) => [n, i]));
8203
+ keyOrders.set(table, order);
8204
+ }
8205
+ return order;
8206
+ }
8207
+
7114
8208
  /** Of each table of declared callees, the entries this body calls — every tree the ranked pass
7115
8209
  * structures carries its own, and is keyed by its whole text. A table this body calls nothing in is
7116
8210
  * absent. */
@@ -7124,9 +8218,17 @@ function calledOnly(
7124
8218
  callees.add(e.fn);
7125
8219
  }
7126
8220
  }
8221
+ // Look up the few names this body calls, not every entry of the table: a project's table holds
8222
+ // every function its ELF declares (thousands for pokeemerald), and this runs on each structuring
8223
+ // of each candidate. The result keeps the TABLE's key order, as a whole-table filter would,
8224
+ // because the tree is keyed by its whole text.
7127
8225
  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;
8226
+ if (table === undefined || callees.size === 0) {
8227
+ return undefined;
8228
+ }
8229
+ const order = keyOrder(table);
8230
+ const names = [...callees].filter((n) => order.has(n)).sort((a, b) => order.get(a)! - order.get(b)!);
8231
+ return names.length > 0 ? Object.fromEntries(names.map((n) => [n, table[n]])) : undefined;
7130
8232
  };
7131
8233
  const declaredArgs = of(tables.declaredArgs);
7132
8234
  const declaredReturns = of(tables.declaredReturns);
@@ -7295,7 +8397,7 @@ function substVar(e: Expr, from: string, to: string): Expr {
7295
8397
 
7296
8398
  // empty-then peephole: `if (c) {} else { S }` → `if (!c) { S }`
7297
8399
  function mkIf(cond: Expr, thenS: Stmt[], elseS: Stmt[]): Stmt {
7298
- if (thenS.length === 0 && elseS.length > 0) {
8400
+ if (thenS.length === 0 && elseS.length > 0 && !keepsHelperRel(cond)) {
7299
8401
  return { k: 'if', cond: negateCond(cond), then: elseS, else: [] };
7300
8402
  }
7301
8403
  return { k: 'if', cond, then: thenS, else: elseS };
@@ -7355,11 +8457,31 @@ function* inEdgeRecords(preds: Map<Block, Block[]>, b: Block): Generator<{ pred:
7355
8457
  }
7356
8458
  }
7357
8459
 
7358
- // Immediate post-dominators. EXIT is represented as `null`; ret-blocks post-lead to it. Over the
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
8460
+ /** THE function-level immediate post-dominators, the one definition `structure()` and its
8461
+ * enumeration gate `hasDivergentSharedRet` both read, so the gate's join for an `if` is the join
8462
+ * the follow sees. A loop no edge leaves reaches no `ret`, so plain post-dominance would hand
8463
+ * every block before it every block as a post-dominator, and an `if` ahead of it an arbitrary
8464
+ * join. Its header ENDS paths as a `ret` does: the region holding the loop ends there
8465
+ * (`emitForever`). `forest` is the caller's, when it already has one. */
8466
+ export function functionPostDominators(
8467
+ fn: Fn,
8468
+ forest: { byHeader: ReadonlyMap<Block, NaturalLoop> } = analyzeLoops(fn, dominators(fn)),
8469
+ ): Map<Block, Block | null> {
8470
+ const exitless = [...forest.byHeader.values()].filter((nl) => nl.exitEdges.length === 0).map((nl) => nl.header);
8471
+ return postDominators(fn, undefined, undefined, new Set(exitless));
8472
+ }
8473
+
8474
+ // Immediate post-dominators. EXIT is represented as `null`; ret-blocks post-lead to it, and so do
8475
+ // the blocks of `ends` besides their own successors. Over the subgraph `keep` induces when given,
8476
+ // every member of which must still reach a `ret` inside it — or, with `onward`, over only the edges
8477
+ // it names between members of `keep` (and to EXIT, where it names `null`): a block it names no edge
7361
8478
  // 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> {
8479
+ function postDominators(
8480
+ fn: Fn,
8481
+ keep?: ReadonlySet<Block>,
8482
+ onward?: (b: Block) => (Block | null)[],
8483
+ ends?: ReadonlySet<Block>,
8484
+ ): Map<Block, Block | null> {
7363
8485
  const blocks = keep ? fn.blocks.filter((b) => keep.has(b)) : fn.blocks;
7364
8486
  const nodes: (Block | null)[] = [null, ...blocks];
7365
8487
  const succ = (b: Block): (Block | null)[] => {
@@ -7367,7 +8489,11 @@ function postDominators(fn: Fn, keep?: ReadonlySet<Block>, onward?: (b: Block) =
7367
8489
  return onward(b);
7368
8490
  }
7369
8491
  const term = b.ops[b.ops.length - 1];
7370
- return term.opcode === 'ret' ? [null] : successorsOf(b).filter((s) => keep === undefined || keep.has(s));
8492
+ if (term.opcode === 'ret') {
8493
+ return [null];
8494
+ }
8495
+ const onwards = successorsOf(b).filter((s) => keep === undefined || keep.has(s));
8496
+ return ends?.has(b) === true ? [...onwards, null] : onwards;
7371
8497
  };
7372
8498
  const pdom = new Map<Block | null, Set<Block | null>>();
7373
8499
  pdom.set(null, new Set([null]));