@asmlift/core 0.9.0 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (117) hide show
  1. package/README.md +25 -24
  2. package/package.json +1 -1
  3. package/src/aggregate.ts +149 -0
  4. package/src/backend/c.ts +2 -0
  5. package/src/backend/cfamily.ts +197 -61
  6. package/src/backend/cpp.ts +3 -1
  7. package/src/backend/lines.ts +10 -0
  8. package/src/backend/pascal.ts +47 -16
  9. package/src/cdecl/parse.ts +21 -0
  10. package/src/contracts.ts +246 -36
  11. package/src/declare.ts +69 -20
  12. package/src/frontend/arm-body.ts +155 -0
  13. package/src/frontend/asmdata.ts +9 -2
  14. package/src/frontend/call-plan.ts +1117 -61
  15. package/src/frontend/callee-body.ts +370 -0
  16. package/src/frontend/dead-reads.ts +628 -0
  17. package/src/frontend/device-pins.ts +33 -4
  18. package/src/frontend/emit.ts +4 -3
  19. package/src/frontend/fpu.ts +224 -17
  20. package/src/frontend/frame-objects.ts +1689 -206
  21. package/src/frontend/frontend.ts +84 -3
  22. package/src/frontend/high-half.ts +53 -4
  23. package/src/frontend/jump-table.ts +545 -0
  24. package/src/frontend/local-object.ts +158 -32
  25. package/src/frontend/mips.ts +2296 -217
  26. package/src/frontend/own-struct-return.ts +500 -0
  27. package/src/frontend/ppc.ts +2748 -245
  28. package/src/frontend/register-struct-call.ts +399 -0
  29. package/src/frontend/reloc-symbol.ts +22 -7
  30. package/src/frontend/ssa.ts +1721 -43
  31. package/src/frontend/stackargs.ts +199 -72
  32. package/src/frontend/thumb-entry.ts +340 -0
  33. package/src/frontend/thumb.ts +2400 -325
  34. package/src/ir/alias.ts +65 -2
  35. package/src/ir/bits.ts +1699 -4
  36. package/src/ir/core.ts +254 -9
  37. package/src/ir/discipline.ts +86 -21
  38. package/src/ir/entry.ts +157 -0
  39. package/src/{structure → ir}/loops.ts +6 -2
  40. package/src/ir/opcodes.ts +137 -9
  41. package/src/ir/parse.ts +1 -0
  42. package/src/ir/simplify.ts +61 -10
  43. package/src/ir/types.ts +86 -2
  44. package/src/ir/verify.ts +96 -11
  45. package/src/l3/argbase.ts +2 -0
  46. package/src/l3/ast.ts +582 -18
  47. package/src/l3/basecse.ts +68 -15
  48. package/src/l3/caseendgoto.ts +223 -0
  49. package/src/l3/coalesce.ts +42 -14
  50. package/src/l3/constinit.ts +410 -0
  51. package/src/l3/dce.ts +53 -9
  52. package/src/l3/initfirst.ts +42 -11
  53. package/src/l3/intcell.ts +2 -1
  54. package/src/l3/narrowdecl.ts +77 -36
  55. package/src/l3/pollguard.ts +8 -2
  56. package/src/l3/ptrcell.ts +33 -34
  57. package/src/l3/ptrfield.ts +123 -8
  58. package/src/l3/regspell.ts +11 -4
  59. package/src/l3/reindex.ts +295 -99
  60. package/src/l3/scopebase.ts +2 -0
  61. package/src/l3/storage.ts +98 -1
  62. package/src/l3/symbol-refs.ts +84 -6
  63. package/src/l3/tailmerge.ts +6 -2
  64. package/src/l3/typing.ts +108 -15
  65. package/src/l3/unreduce.ts +6 -5
  66. package/src/l3/volatileptr.ts +17 -54
  67. package/src/mangle.ts +30 -0
  68. package/src/pattern/engine.ts +361 -73
  69. package/src/pipeline.ts +1039 -38
  70. package/src/proto-context.ts +1039 -45
  71. package/src/proto.ts +555 -40
  72. package/src/raise/aggmembers.ts +290 -0
  73. package/src/raise/arrays.ts +5 -1
  74. package/src/raise/const.ts +25 -0
  75. package/src/raise/declared-volatile.ts +207 -31
  76. package/src/raise/divpow2.ts +192 -0
  77. package/src/raise/floathelpers.ts +254 -33
  78. package/src/raise/gvn.ts +3 -3
  79. package/src/raise/latch.ts +162 -6
  80. package/src/raise/memberarrays.ts +10 -4
  81. package/src/raise/offsetnames.ts +422 -36
  82. package/src/raise/pairparams.ts +33 -0
  83. package/src/raise/paramwidth.ts +196 -6
  84. package/src/raise/pre-recovery.ts +107 -21
  85. package/src/raise/pre-structure.ts +73 -0
  86. package/src/raise/recover.ts +12 -5
  87. package/src/raise/reroll.ts +674 -0
  88. package/src/raise/shortcircuit.ts +217 -34
  89. package/src/raise/softdiv.ts +1 -1
  90. package/src/raise/storesign.ts +153 -0
  91. package/src/raise/struct-arrays.ts +30 -11
  92. package/src/raise/structs.ts +396 -33
  93. package/src/raise/truncload.ts +4 -3
  94. package/src/raise/unpre.ts +594 -0
  95. package/src/raise/unthread.ts +400 -0
  96. package/src/rank-declare.ts +118 -12
  97. package/src/rank-variations.ts +186 -35
  98. package/src/rank.ts +536 -68
  99. package/src/runtime-helpers.ts +140 -22
  100. package/src/structure/analysis.ts +217 -8
  101. package/src/structure/arith-ops.ts +23 -0
  102. package/src/structure/cross-jumped-tail.ts +216 -0
  103. package/src/structure/globalaccess.ts +157 -10
  104. package/src/structure/hazards.ts +16 -64
  105. package/src/structure/local-statics.ts +145 -9
  106. package/src/structure/namecoalesce.ts +1 -1
  107. package/src/structure/pointer-spelling.ts +1106 -0
  108. package/src/structure/redundant-test.ts +1 -1
  109. package/src/structure/structure.ts +6192 -1213
  110. package/src/structure/switch-recover.ts +583 -120
  111. package/src/structure/unfolded-branch-constant.ts +90 -0
  112. package/src/symbols.ts +58 -15
  113. package/src/target.ts +1028 -54
  114. package/src/trace.ts +49 -17
  115. package/src/variation-definitions.ts +333 -20
  116. package/src/variation-gates.ts +4 -0
  117. package/src/variation-tokens.ts +68 -3
@@ -0,0 +1,216 @@
1
+ // THE LAYOUT OF A CROSS-JUMPED RETURN TAIL: does the asm show that the compiler merged a return
2
+ // tail the source wrote into every arm, where the structurer took the join past early returns?
3
+ //
4
+ // A compiler that cross-jumps return tails (target.ts `crossJumpsReturnTails`, gcc 2.x jump.c)
5
+ // leaves the same CFG for `if (a) { x = 1; tail(); return; } ... tail();` and for `if (a) x = 1;
6
+ // ... tail();`: one copy of the tail, which every arm reaches. The two spellings differ in WHERE
7
+ // that copy sits and in WHICH arms became one block, because `do_cross_jump` keeps the copy of
8
+ // the LATER jumping arm and jumps the earlier one to a new label before it. That label stops
9
+ // further merging (`INSN_UID (JUMP_LABEL (insn)) < max_uid`). So the copies spelling leaves one of
10
+ // three marks in the object:
11
+ //
12
+ // (A) THE TAIL IS NOT LAST. The surviving copy ends in an unconditional jump (A1), and a block of
13
+ // the construct that never reaches the tail is laid out after it (A2). That is an arm that
14
+ // did not use the tail, left where the source wrote it. The join writes the tail after the
15
+ // whole construct, where it falls into what follows it. A bare return block does not count
16
+ // for A2: the epilogue an early `return` reaches is laid out last under either spelling.
17
+ // The copy can survive split over two blocks: an arm's own jump enters it part-way, so the
18
+ // join falls into the rest of the tail, and that rest ends in the jump. So A1 reads the end
19
+ // of the join's run of fall-throughs, not the join block alone.
20
+ // (B) IDENTICAL ARMS STAYED APART. Two arms that reach the tail and nothing else, each by an
21
+ // unconditional jump, have the same instructions. Under the join, each such arm jumps to the
22
+ // join's one original label, and jump2's search of the other jumps to that label merges
23
+ // identical bodies. Under the copies, the first cross-jump made a new label, and that search
24
+ // never ran. An arm that FALLS into the join is not one of them: it is tried only against
25
+ // the code before the label, which a call ending it defeats (flow.c's use-nop), so agbcc
26
+ // keeps a falling `g(2)` apart from a jumping `g(2)` under the join too
27
+ // (`probe-rettail-pd-join.c`).
28
+ // (C) A CALL CUT FROM ITS ARGUMENTS. The join starts with a call, and two predecessors that reach
29
+ // only the join each hold one same instruction after their own last call. `do_cross_jump`
30
+ // compares backward from each jump, one instruction at a time, and stops at the first pair
31
+ // that differs; arms that set the call's arguments in different orders, or where one arm
32
+ // finds an argument already in its register, differ just above the call. So the merge cuts
33
+ // the call from its argument setup, and leaves a copy of that setup in each arm. The join
34
+ // writes the setup once, after its label, and an instruction both arms hold in the same
35
+ // place would have been merged up into it.
36
+ //
37
+ // Bodies are compared as the frontend recorded them, the relocated symbol included
38
+ // (frontend/ssa.ts `recordAsmLayout`): in a relocatable MIPS object `gT = c` and `gW = c` are the
39
+ // same two instructions, and only the relocation names the global (`probe-rettail-pc-join.c`).
40
+ //
41
+ // A MARK IS EVIDENCE, NOT PROOF. jump2 cross-jumps any shared code, not only a return tail: two
42
+ // identical arms of a join-written `if`, or a call both arms end in, merge the same way, and leave
43
+ // the merged block ending in a jump with an early-return arm laid out after it (A). Both readings
44
+ // of such a site are verified readings of the asm, and on the hand probes neither matches. So a
45
+ // mark moves which reading is the DEFAULT, never what is lifted, and its worth is measured, not
46
+ // derived.
47
+ //
48
+ // MEASURED. Over the 620 real functions where the join re-spells the default (2026-11-05), each
49
+ // reading really compiled: the bound plus (A1 & A2) | B gives MATCH 44, against 35 for the copies
50
+ // everywhere and 40 for the size bound alone, with no match lost. A and B were chosen on those 620.
51
+ // The split tail of A1 and mark C were read off held-out sources instead: 160 generated functions
52
+ // written WITH the copies, early returns between the arms (2026-11-06). There the size bound with A
53
+ // and B alone loses 7 of the copies' matches on agbcc, 9 on gcc2.7.2kmc and 2 on gcc2.7.2; with the
54
+ // split tail and C it loses 2, 3 and 1, and gains as many as before (5, 5, 2). Over 300 paired
55
+ // functions, half written each way, the two lose no match; 3 join-written ones on each GCC 2.7.2
56
+ // lane move to the copies and score further. On the 620 they move 3 functions, all to the copies'
57
+ // own spelling: 2 closer, 1 further, MATCH still 44. What no layout read recovers: a tie the two
58
+ // spellings leave in the same layout, where only the register allocation differs, and a call merged
59
+ // across arms that pass it different arguments. The probe pairs in cross-jumped-tail.test.ts check
60
+ // each mark and each shape that is no mark.
61
+ //
62
+ // READ BY NO DEFAULT. Over held-out real functions (pokeemerald, 2026-11-06) the size bound with
63
+ // these marks still loses 9 of the copies' matches on agbcc, none of them marked; over the 160
64
+ // generated functions (2026-11-09) it still loses 3 on gcc2.7.2kmc and 1 on gcc2.7.2. So both
65
+ // compilers that cross-jump default to the copies outright (they leave out target.ts
66
+ // `joinPastReturnsByDefault`), and the probe pairs still pin each mark.
67
+ //
68
+ // Read only from the frontend's layout record (ir/core.ts `AsmLayout`). A block with no entry, such
69
+ // as one a raise pass made up, is no evidence either way. A function with no record (parsed IR)
70
+ // has no mark.
71
+ import type { AsmLayout, Block, Fn } from '../ir/core';
72
+ import type { JoinPastReturnsSite } from './structure';
73
+
74
+ const succsOf = (b: Block): Block[] => [...new Set(b.ops[b.ops.length - 1]?.successors.map((s) => s.block) ?? [])];
75
+
76
+ /** Every block reachable from `from`, `from` included, never entering `avoid`. */
77
+ function reach(from: Block, avoid?: Block): Set<Block> {
78
+ const seen = new Set<Block>();
79
+ const stack = [from];
80
+ while (stack.length) {
81
+ const x = stack.pop()!;
82
+ if (x === avoid || seen.has(x)) {
83
+ continue;
84
+ }
85
+ seen.add(x);
86
+ stack.push(...succsOf(x));
87
+ }
88
+ return seen;
89
+ }
90
+
91
+ const isBareReturn = (b: Block): boolean => b.ops.length === 1 && b.ops[0].opcode === 'ret';
92
+
93
+ /** Does `site` show either mark of a cross-jumped return tail in `fn`'s asm layout? */
94
+ export function crossJumpedTail(fn: Fn, site: JoinPastReturnsSite): boolean {
95
+ const layout: AsmLayout | undefined = fn.asmLayout;
96
+ const at = layout?.get(site.join);
97
+ if (layout === undefined || at === undefined) {
98
+ return false;
99
+ }
100
+ return (
101
+ (tailEndsInJump(layout, site.join) && armLaidOutAfter(fn, layout, site, at.index)) ||
102
+ identicalArms(fn, layout, site.join) ||
103
+ callCutFromArguments(fn, layout, site.join)
104
+ );
105
+ }
106
+
107
+ /** (A1): the tail from the join, followed through its fall-throughs, ends in an unconditional jump. */
108
+ function tailEndsInJump(layout: AsmLayout, join: Block): boolean {
109
+ const seen = new Set<Block>();
110
+ for (let b: Block = join; ;) {
111
+ const at = layout.get(b);
112
+ if (at === undefined || seen.has(b)) {
113
+ return false;
114
+ }
115
+ if (at.endsInJump) {
116
+ return true;
117
+ }
118
+ seen.add(b);
119
+ // a fall-through is the next block in the asm; anything else is no run of the tail
120
+ const next = succsOf(b);
121
+ if (next.length !== 1 || layout.get(next[0])?.index !== at.index + 1) {
122
+ return false;
123
+ }
124
+ b = next[0];
125
+ }
126
+ }
127
+
128
+ /** (C): the join's first instruction is a call, and two of its predecessors, each with the join as
129
+ * its only successor, hold a same instruction after their own last call.
130
+ *
131
+ * BLIND TO WHERE IN THE ARM THAT INSTRUCTION SITS, and to what it computes, on purpose. The text
132
+ * compared is the instruction as printed, so a shared statement is not needed: on MIPS the address
133
+ * half of a relocated global (`lui at,0x0 <gT>`) and a store from a register that holds a different
134
+ * value in each arm (`sw v0,0(at) <gT>`) read the same. So any two call-free arms that write one
135
+ * global fire it when the join opens with a call (`if (a == 0) { gV = b; gT = 1; } else if (a ==
136
+ * 1) { gV = c; gT = 2; } ... k(a);`, 26 lines off as copies, 0 as the join, on both MIPS GCC lanes),
137
+ * and it publishes the copies there, which do not match. Counting only
138
+ * an instruction that sets up a register the join reads (an argument of its call, or a saved
139
+ * register read past it) clears that case, and loses two matches the copies have: on
140
+ * `probe-rettail-pc-copies` the arms keep `gV = a` above the cut call, a store that sets up no
141
+ * register, and the join then scores 23 lines off on gcc2.7.2 and 30 on gcc2.7.2kmc, really
142
+ * compiled (2026-11-06). A mark that fires wrongly publishes the copies, the default before the
143
+ * join, and one that misses can lose a match the copies had; so C stays wide. */
144
+ function callCutFromArguments(fn: Fn, layout: AsmLayout, join: Block): boolean {
145
+ if (layout.get(join)?.calls[0] !== 0) {
146
+ return false;
147
+ }
148
+ const seen = new Set<string>();
149
+ for (const p of predecessors(fn).get(join) ?? []) {
150
+ const at = layout.get(p);
151
+ if (at === undefined || succsOf(p).length !== 1) {
152
+ continue;
153
+ }
154
+ const setup = new Set(at.body.slice((at.calls[at.calls.length - 1] ?? -1) + 1));
155
+ if ([...setup].some((ins) => seen.has(ins))) {
156
+ return true;
157
+ }
158
+ for (const ins of setup) {
159
+ seen.add(ins);
160
+ }
161
+ }
162
+ return false;
163
+ }
164
+
165
+ /** (A2): a block of the construct, reached from its branch short of the join, that never reaches
166
+ * the join and is laid out after it. A bare return does not count. */
167
+ function armLaidOutAfter(fn: Fn, layout: AsmLayout, site: JoinPastReturnsSite, joinIndex: number): boolean {
168
+ const fromJoin = reach(site.join);
169
+ const reachesJoin = new Set<Block>([site.join]);
170
+ const preds = predecessors(fn);
171
+ const stack = [site.join];
172
+ while (stack.length) {
173
+ for (const p of preds.get(stack.pop()!) ?? []) {
174
+ if (!reachesJoin.has(p)) {
175
+ reachesJoin.add(p);
176
+ stack.push(p);
177
+ }
178
+ }
179
+ }
180
+ for (const x of reach(site.branch, site.join)) {
181
+ const xi = layout.get(x)?.index;
182
+ if (xi !== undefined && xi > joinIndex && !fromJoin.has(x) && !reachesJoin.has(x) && !isBareReturn(x)) {
183
+ return true;
184
+ }
185
+ }
186
+ return false;
187
+ }
188
+
189
+ /** (B): two predecessors of the join, each with the join as its only successor and each ending in an
190
+ * unconditional jump to it, whose asm bodies are the same non-empty instructions. */
191
+ function identicalArms(fn: Fn, layout: AsmLayout, join: Block): boolean {
192
+ const seen = new Set<string>();
193
+ for (const p of predecessors(fn).get(join) ?? []) {
194
+ const at = layout.get(p);
195
+ if (at === undefined || !at.endsInJump || at.body.length === 0 || succsOf(p).length !== 1) {
196
+ continue;
197
+ }
198
+ const body = at.body;
199
+ const key = body.join('\n');
200
+ if (seen.has(key)) {
201
+ return true;
202
+ }
203
+ seen.add(key);
204
+ }
205
+ return false;
206
+ }
207
+
208
+ function predecessors(fn: Fn): Map<Block, Block[]> {
209
+ const preds = new Map<Block, Block[]>(fn.blocks.map((b) => [b, []]));
210
+ for (const b of fn.blocks) {
211
+ for (const s of succsOf(b)) {
212
+ preds.get(s)?.push(b);
213
+ }
214
+ }
215
+ return preds;
216
+ }
@@ -1,17 +1,29 @@
1
1
  // THE GLOBAL-ACCESS ADDRESS DECOMPOSITION — the pure half of the symbol-map access spellings.
2
2
  //
3
- // Everything here is a function of an `Expr`, a `SymbolInfo` and a width: no structurer state, no
4
- // naming walk, no loop context. It answers three questions the access spellings in structure.ts
5
- // ask over and over — is this address `&gSym` plus something; does that something divide into
6
- // whole elements; and do the terms of it name the DECLARED subscripts of a multidimensional array
7
- // — and it answers them the same way for every caller, which is the point of the split: the
8
- // rank-pinning fallback and the declared-subscript recovery share `bareArrayElement`, so the two
9
- // cannot disagree about what a bare element spelling is.
3
+ // Everything here is a function of an `Expr`, the map's declarations and a width: no structurer
4
+ // state, no naming walk, no loop context. It answers three questions the access spellings in
5
+ // structure.ts ask over and over — is this address `&gSym` plus something; does that something
6
+ // divide into whole elements; and do the terms of it name the DECLARED subscripts of a
7
+ // multidimensional array — and it answers them the same way for every caller, which is the point
8
+ // of the split: the rank-pinning fallback and the declared-subscript recovery share
9
+ // `bareArrayElement`, so the two cannot disagree about what a bare element spelling is. The same
10
+ // holds for what the map declares a named member to be (`ptrMemberDecl`), which the access folds
11
+ // and the pointer spelling (pointer-spelling.ts) both read.
10
12
  import type { Expr } from '../l3/ast';
11
- import { type SymbolInfo, arrayInnerExtents } from '../symbols';
13
+ import {
14
+ type DeclaredField,
15
+ type SymbolInfo,
16
+ type SymbolStructField,
17
+ arrayInnerExtents,
18
+ declaredFields,
19
+ isArrayField,
20
+ isBitfieldField,
21
+ isPtrField,
22
+ pointeeFields,
23
+ } from '../symbols';
12
24
 
13
25
  // `&gSym`, possibly wearing the value-context integer cast the additive lowering adds
14
- // (`(u32)&gSym` — see `intifyAddr` in structure.ts's lowerDef): both spell the same link-time
26
+ // (`(u32)&gSym` — see `ARITH_ROWS` in pointer-spelling.ts): both spell the same link-time
15
27
  // constant, so the fold rules match through the cast and every access that CAN spell a named
16
28
  // element still does.
17
29
  // WIDTH 32 ONLY — a NARROWING cast (`(u8)&gSym`, from a zext/sext lowering) is a different
@@ -63,6 +75,42 @@ export function globalByteBase(e: Expr): { name: string; residual: Expr } | null
63
75
  return null;
64
76
  }
65
77
 
78
+ /** {@link globalOf} with the address any number of `+` down — FOR THE BARE-ARRAY SPELLING ONLY, the
79
+ * one consumer whose declaration says what the residual indexes. An interior address the lift
80
+ * rewrote as the object's plus a constant before the index was added (IDO's `addiu t, sp, 0x58`
81
+ * for the second of two copied arrays reaches the lift as `(u32)sp56 + 32 + v8`) has its object
82
+ * one `+` further down than globalByteBase looks. The sum's terms are flattened, exactly one of
83
+ * them an address, and the rest are the residual: its runtime terms in order and its constants
84
+ * summed into one trailing term (`v8 + 32`). Not for the cast forms, which keep the sum as written
85
+ * (`*(s8 *)((u32)sp0 + 3 + a0)`). `off` is the access's own displacement, summed into the same
86
+ * trailing constant. */
87
+ export function nestedGlobalOf(e: Expr, width: number, off: number): { name: string; idx: Expr } | null {
88
+ const terms: Expr[] = [];
89
+ const flatten = (x: Expr): void => {
90
+ if (x.k === 'bin' && x.op === '+') {
91
+ flatten(x.l);
92
+ flatten(x.r);
93
+ } else {
94
+ terms.push(x);
95
+ }
96
+ };
97
+ flatten(e);
98
+ const at = terms.filter((x) => addrIn(x) !== null);
99
+ if (at.length !== 1) {
100
+ return null;
101
+ }
102
+ const rest = terms.filter((x) => x !== at[0]);
103
+ const bias = rest.reduce((n, x) => (x.k === 'const' ? n + x.value : n), off);
104
+ const runtime = rest
105
+ .filter((x) => x.k !== 'const')
106
+ .reduce<Expr | null>((l, r) => (l === null ? r : { k: 'bin', op: '+', l, r }), null);
107
+ if (bias % width !== 0) {
108
+ return null;
109
+ }
110
+ const idx = runtime === null ? { k: 'const' as const, value: 0 } : elementIndex(runtime, width);
111
+ return idx === null ? null : { name: addrIn(at[0])!.name, idx: addOffset(idx, bias / width) };
112
+ }
113
+
66
114
  // THE one gate on the BARE-NAME array-global spelling (`gSym[i]` rather than `((T *)&gSym)[i]`),
67
115
  // shared by the constant-offset and variable-index access paths so the two cannot disagree.
68
116
  // Returns the `index` node's `lead` fragment when the bare form is spellable, or null to fall
@@ -81,10 +129,19 @@ export function globalByteBase(e: Expr): { name: string; residual: Expr } | null
81
129
  // inner extent) gets no bare form at all; `((T *)&gSym)[i]` is byte-identical and valid under ANY
82
130
  // declaration, which is why it is the safe fallback. See symbols.ts arrayInnerExtents for why an
83
131
  // ABSENT rank is read as 1 rather than as unknown.
84
- export function bareArrayLead(si: SymbolInfo, width: number, signed: boolean): { lead?: Expr[] } | null {
132
+ //
133
+ // …and a CONSTANT subscript must name an element of the declaration, where it states its extent: a
134
+ // subscript past a rank-1 array's end (or before its start) is not an element of it, so it keeps
135
+ // the cast form, which states only the address. A runtime subscript is the access's own business —
136
+ // for a frame part, the audit that split it bounded the reach (frame-objects.ts `provenParts`).
137
+ export function bareArrayLead(si: SymbolInfo, width: number, signed: boolean, idx?: Expr): { lead?: Expr[] } | null {
85
138
  if (!bareArrayElement(si, width, signed)) {
86
139
  return null;
87
140
  }
141
+ const extent = si.dims?.length === 1 ? si.dims[0] : undefined;
142
+ if (idx?.k === 'const' && (idx.value < 0 || (typeof extent === 'number' && idx.value >= extent))) {
143
+ return null;
144
+ }
88
145
  const inner = arrayInnerExtents(si);
89
146
  return inner === null ? null : inner.length === 0 ? {} : { lead: inner.map((): Expr => ({ k: 'const', value: 0 })) };
90
147
  }
@@ -298,3 +355,93 @@ export function addOffset(idx: Expr, n: number): Expr {
298
355
  ? { k: 'const', value: idx.value + n }
299
356
  : { k: 'bin', op: '+', l: idx, r: { k: 'const', value: n } };
300
357
  }
358
+
359
+ /** May a member be NAMED by an access of this direction, given the qualifiers on its declaration?
360
+ * The named spelling REPLACES a cast through `(u8 *)`, which carries no qualifier at all, so a
361
+ * qualifier the name reintroduces changes what the compiler emits:
362
+ * • `volatile` makes the access observable — the load may no longer be folded or reordered,
363
+ * which is a different instruction sequence (measured: 6 insns where the cast form was 5);
364
+ * • `const` under a STORE is a hard error, where the cast form merely cast the qualifier away.
365
+ * Either way the honest spelling is the cast form, so the member simply is not nameable here. */
366
+ export function memberQualsAllow(f: SymbolStructField, containerConst: boolean | undefined, isStore: boolean): boolean {
367
+ if (f.volatile) {
368
+ return false;
369
+ }
370
+ return !(isStore && (f.const || containerConst));
371
+ }
372
+
373
+ /** The member a `width`-byte access at byte `byte` of a struct global names, `gSym.member`: one at
374
+ * that offset and of that size, no array or bitfield, whose qualifiers allow the access. An ARRAY
375
+ * member would match a byte access by (offset, size) and spell `.x`, which is not an lvalue of
376
+ * that width; a plain read of a BITFIELD's bytes is not a read of its bits. */
377
+ export function structMemberAt(
378
+ si: SymbolInfo,
379
+ byte: number,
380
+ width: number,
381
+ isStore: boolean,
382
+ ): DeclaredField | undefined {
383
+ if (si.shape !== 'struct') {
384
+ return undefined;
385
+ }
386
+ const f = declaredFields(si.layout)?.find(
387
+ (x) => x.offset === byte && x.size === width && !isArrayField(x) && !isBitfieldField(x),
388
+ );
389
+ return f !== undefined && memberQualsAllow(f, si.const, isStore) ? f : undefined;
390
+ }
391
+
392
+ /** The members a symbol's declaration seats: a struct global's own, or a pointer global's
393
+ * pointee's. */
394
+ export interface MemberLookup {
395
+ fieldsOf(name: string): DeclaredField[] | null;
396
+ }
397
+
398
+ /** {@link MemberLookup.fieldsOf} over `info`, MEMOIZED per symbol. `declaredFields` validates every
399
+ * member and returns a fresh sorted copy on every call, and pointer-spelling.ts's `roleOf`
400
+ * asks it for every member operand of every arithmetic op lowered, so an uncached lookup is an
401
+ * O(n log n) allocation on a hot path — inside a `structure()` a ranked run repeats once per
402
+ * candidate, 17,856 times on the largest fan. */
403
+ export function memoFieldsOf(info: (name: string) => SymbolInfo | undefined): MemberLookup['fieldsOf'] {
404
+ const cache = new Map<string, DeclaredField[] | null>();
405
+ return (name) => {
406
+ const hit = cache.get(name);
407
+ if (hit !== undefined) {
408
+ return hit;
409
+ }
410
+ const si = info(name);
411
+ const fields =
412
+ si?.shape === 'struct' ? declaredFields(si.layout) : si?.shape === 'pointer' ? pointeeFields(si.pointee) : null;
413
+ cache.set(name, fields);
414
+ return fields;
415
+ };
416
+ }
417
+
418
+ /** The map member a `field` node NAMES, or null when it names none — THE one resolver for "what
419
+ * does the declaration say about this member", for rules that must reason about a member's type
420
+ * after the access rules have already spelled it. Both named spellings resolve, through the same
421
+ * shared gate their spelling passed: `gSym.member` off a struct global's {@link declaredFields},
422
+ * `gPtr->member` off the pointee's ({@link pointeeFields}). A synthesized `field_K` — the
423
+ * recovered-struct spelling, which no map declares — resolves to null, and so does any base that
424
+ * is not a map-shaped global, which is what makes every caller refuse rather than guess.
425
+ *
426
+ * The pointee arm reaches only what the MAP lets it: `pointeeAccess` gates every `gPtr->member`
427
+ * spelling on `spellsAccessType(f.signed, …)`, so a pointer field declaring no signedness — which
428
+ * is every one in the corpus's vendored maps — is never named, and a pointer read one indirection
429
+ * down spells `((s32 *)gQ)[1]`. That is a fact about those maps, not about this code: `SymbolMap`
430
+ * is a caller-supplied input, and one field flips it (`signed: true` on a 4-byte pointer member
431
+ * of a pointee yields `(u8 *)gQ->pInner`, cast and all, pinned in pointer-members.test.ts). So
432
+ * the arm is live and tested, and the byte-arithmetic rule that reads this answer is correct for
433
+ * it — where resolving a pointee member to null would reopen the double-scaling hole silently. */
434
+ export function declaredMemberOf(x: Expr, sym: MemberLookup | undefined): DeclaredField | null {
435
+ if (x.k !== 'field' || x.base.k !== 'var' || sym === undefined) {
436
+ return null;
437
+ }
438
+ return sym.fieldsOf(x.base.name)?.find((f) => f.name === x.name) ?? null;
439
+ }
440
+
441
+ /** The map member a `field` node names when the declaration makes it a POINTER — {@link
442
+ * isPtrField} being the shared two-fact test, so this and the synthesized declaration cannot
443
+ * disagree about what a member is. */
444
+ export function ptrMemberDecl(x: Expr, sym: MemberLookup | undefined): DeclaredField | null {
445
+ const f = declaredMemberOf(x, sym);
446
+ return f !== null && isPtrField(f) ? f : null;
447
+ }
@@ -4,9 +4,9 @@
4
4
  // • may this loop's updates be emitted before its condition/exit/post-loop reads, or would some
5
5
  // read then see a clobbered (post-update) value that the original IR read PRE-update — and
6
6
  // which of those reads can be REPAIRED by moving the copy into the body (`sinkable…`);
7
- // • is a value the emitted form DROPS redundant — `sameAtEntry`, which reads an expression on
8
- // the loop's entry state so a guard about to be fused away can be checked against the test
9
- // that replaces it.
7
+ // • is a value the emitted form DROPS redundant — `sameAtEntry` (ir/entry.ts, shared with the
8
+ // raise re-roll), which reads an expression on the loop's entry state so a guard about to be
9
+ // fused away can be checked against the test that replaces it.
10
10
  //
11
11
  // WHAT MAKES A SUNK COPY LEGAL. The exit edge's value is not moved, it is REBUILT: the copy spells
12
12
  // the arg's def-tree again inside the body, at the point that tree was already computed at
@@ -50,7 +50,7 @@
50
50
  // exist at CALL time (emission runs after naming completes). Snapshotting them would break this.
51
51
  import { Block, Op, Value, successorsOf } from '../ir/core';
52
52
  import { effectful, orderSensitive, reevalUnsafe } from '../ir/discipline';
53
- import { NEGATED_ICMP } from '../ir/opcodes';
53
+ import { sameAtEntry } from '../ir/entry';
54
54
  import { Expr, Stmt } from '../l3/ast';
55
55
  import { type Gate, firstRejection } from '../l3/gates';
56
56
  import type { UseSite } from './analysis';
@@ -120,7 +120,15 @@ export interface LoopHazards {
120
120
  gates?: readonly Gate<SinkCandidate>[],
121
121
  staleOnZeroTrip?: (slot: number) => boolean,
122
122
  ): Map<number, Op | null>;
123
- sameAtEntry(a: Value, b: Value, entry: Map<Value, Value>, negated?: boolean): boolean;
123
+ /** `through`: the values whose defining op may be read through (ir/entry.ts `EntryReading`);
124
+ * every value when absent. */
125
+ sameAtEntry(
126
+ a: Value,
127
+ b: Value,
128
+ entry: Map<Value, Value>,
129
+ negated?: boolean,
130
+ through?: (v: Value) => boolean,
131
+ ): boolean;
124
132
  loopWriteSet(updates: Stmt[], bodyBlocks: Iterable<Block>, header: Block): Set<string>;
125
133
  loopWriteSetAhead(ahead: Iterable<Block>, incoming: (b: Block) => Value[][], header: Block): Set<string>;
126
134
  }
@@ -324,7 +332,7 @@ interface Reach {
324
332
  const ALWAYS: Reach = { onTrue: true, onFalse: true };
325
333
 
326
334
  /** The reach each operand of a SHORT-CIRCUIT op inherits. These two are the whole set: they are the
327
- * ops `ARITH_TO_BIN` (structure.ts) renders as `&&`/`||` — a test holds the two lists together,
335
+ * ops `ARITH_TO_BIN` (arith-ops.ts) renders as `&&`/`||` — a test holds the two lists together,
328
336
  * since a connective missing from here is read as an ordinary op and its arms inherit a reach they
329
337
  * do not have — and `Expr` has no conditional form besides them, no ternary, so every other op
330
338
  * evaluates all of its operands whenever it is itself evaluated and they inherit its reach
@@ -363,63 +371,6 @@ function unitStep(value: Expr, name: string): 1 | -1 | null {
363
371
  return k === null ? null : ((value.op === '-' ? -k : k) as 1 | -1);
364
372
  }
365
373
 
366
- // `x + 0` / `x - 0` / `x | 0` are `x`. Substituting a loop variable by its init constant turns
367
- // ordinary index arithmetic into exactly these, and a guard that spells the same value without
368
- // the arithmetic would otherwise compare unequal.
369
- function fold(defs: Map<Value, Op>, v: Value): Value {
370
- const d = defs.get(v);
371
- if (!d || d.operands.length !== 2 || !['add', 'sub', 'or'].includes(d.opcode)) {
372
- return v;
373
- }
374
- const z = defs.get(d.operands[1]);
375
- return z?.opcode === 'const' && z.attrs?.value === 0 ? fold(defs, d.operands[0]) : v;
376
- }
377
-
378
- // Does `a`, read on a loop's ENTRY values, denote the same thing as `b`? `entry` maps each header
379
- // param and back-edge arg to the arg the forward edge passes, so substituting through it models
380
- // the FIRST iteration — the state a guard in front of the loop tested. `negated` compares against
381
- // b's logical opposite instead, for the usual case where a guard spells the loop's own test the
382
- // other way round (`beq` to the exit vs `bne` to the header).
383
- //
384
- // Structural, not semantic: distinct ops with the same opcode, attributes and operands compare
385
- // equal (two `const 0`s do), anything else does not. A false negative costs a loud decline, which
386
- // is the direction to be wrong in. Memoised like `readsClobbered`'s `seen`: a value its own
387
- // consumer reads twice would otherwise double the work at every level.
388
- function sameAtEntry(defs: Map<Value, Op>, a: Value, b: Value, entry: Map<Value, Value>, negated = false): boolean {
389
- const memo = new Map<Value, Map<Value, boolean>>();
390
- const sameOp = (da: Op, db: Op, opcodeOk: boolean): boolean =>
391
- opcodeOk &&
392
- da.operands.length === db.operands.length &&
393
- JSON.stringify(da.attrs ?? null) === JSON.stringify(db.attrs ?? null) &&
394
- da.operands.every((o, i) => same(o, db.operands[i]));
395
- const same = (x0: Value, y0: Value): boolean => {
396
- const x = fold(defs, entry.get(x0) ?? x0);
397
- const y = fold(defs, y0);
398
- if (x === y) {
399
- return true;
400
- }
401
- let row = memo.get(x);
402
- if (!row) {
403
- memo.set(x, (row = new Map()));
404
- }
405
- const hit = row.get(y);
406
- if (hit !== undefined) {
407
- return hit;
408
- }
409
- const da = defs.get(x);
410
- const db = defs.get(y);
411
- const r = !!da && !!db && sameOp(da, db, da.opcode === db.opcode);
412
- row.set(y, r);
413
- return r;
414
- };
415
- if (!negated) {
416
- return same(a, b);
417
- }
418
- const da = defs.get(fold(defs, entry.get(a) ?? a));
419
- const db = defs.get(fold(defs, b));
420
- return !!da && !!db && sameOp(da, db, NEGATED_ICMP[da.opcode] === db.opcode);
421
- }
422
-
423
374
  // WHERE A SUNK COPY IS REBUILT. The copy is not carried into the body, it is SPELLED AGAIN
424
375
  // there, so the point it belongs at is the one its value was computed at: the arg's own defining
425
376
  // op, when that op is one of `latch`'s — the block whose statements both loop emitters render
@@ -1068,7 +1019,8 @@ export function makeLoopHazards(deps: LoopHazardDeps): LoopHazards {
1068
1019
  testSkipsAnEffect,
1069
1020
  preUpdateCondFold,
1070
1021
  sinkablePreUpdateSlots,
1071
- sameAtEntry: (a, b, entry, negated = false) => sameAtEntry(defs, a, b, entry, negated),
1022
+ sameAtEntry: (a, b, entry, negated = false, through = undefined) =>
1023
+ sameAtEntry(defs, a, b, through ? { entry, through } : { entry }, negated),
1072
1024
  loopWriteSet,
1073
1025
  loopWriteSetAhead,
1074
1026
  };