@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.
- package/README.md +25 -24
- package/package.json +1 -1
- package/src/aggregate.ts +149 -0
- package/src/backend/c.ts +2 -0
- package/src/backend/cfamily.ts +197 -61
- package/src/backend/cpp.ts +3 -1
- package/src/backend/lines.ts +10 -0
- package/src/backend/pascal.ts +47 -16
- package/src/cdecl/parse.ts +21 -0
- package/src/contracts.ts +246 -36
- package/src/declare.ts +69 -20
- package/src/frontend/arm-body.ts +155 -0
- package/src/frontend/asmdata.ts +9 -2
- package/src/frontend/call-plan.ts +1117 -61
- package/src/frontend/callee-body.ts +370 -0
- package/src/frontend/dead-reads.ts +628 -0
- package/src/frontend/device-pins.ts +33 -4
- package/src/frontend/emit.ts +4 -3
- package/src/frontend/fpu.ts +224 -17
- package/src/frontend/frame-objects.ts +1689 -206
- package/src/frontend/frontend.ts +84 -3
- package/src/frontend/high-half.ts +53 -4
- package/src/frontend/jump-table.ts +545 -0
- package/src/frontend/local-object.ts +158 -32
- package/src/frontend/mips.ts +2296 -217
- package/src/frontend/own-struct-return.ts +500 -0
- package/src/frontend/ppc.ts +2748 -245
- package/src/frontend/register-struct-call.ts +399 -0
- package/src/frontend/reloc-symbol.ts +22 -7
- package/src/frontend/ssa.ts +1721 -43
- package/src/frontend/stackargs.ts +199 -72
- package/src/frontend/thumb-entry.ts +340 -0
- package/src/frontend/thumb.ts +2400 -325
- package/src/ir/alias.ts +65 -2
- package/src/ir/bits.ts +1699 -4
- package/src/ir/core.ts +254 -9
- package/src/ir/discipline.ts +86 -21
- package/src/ir/entry.ts +157 -0
- package/src/{structure → ir}/loops.ts +6 -2
- package/src/ir/opcodes.ts +137 -9
- package/src/ir/parse.ts +1 -0
- package/src/ir/simplify.ts +61 -10
- package/src/ir/types.ts +86 -2
- package/src/ir/verify.ts +96 -11
- package/src/l3/argbase.ts +2 -0
- package/src/l3/ast.ts +582 -18
- package/src/l3/basecse.ts +68 -15
- package/src/l3/caseendgoto.ts +223 -0
- package/src/l3/coalesce.ts +42 -14
- package/src/l3/constinit.ts +410 -0
- package/src/l3/dce.ts +53 -9
- package/src/l3/initfirst.ts +42 -11
- package/src/l3/intcell.ts +2 -1
- package/src/l3/narrowdecl.ts +77 -36
- package/src/l3/pollguard.ts +8 -2
- package/src/l3/ptrcell.ts +33 -34
- package/src/l3/ptrfield.ts +123 -8
- package/src/l3/regspell.ts +11 -4
- package/src/l3/reindex.ts +295 -99
- package/src/l3/scopebase.ts +2 -0
- package/src/l3/storage.ts +98 -1
- package/src/l3/symbol-refs.ts +84 -6
- package/src/l3/tailmerge.ts +6 -2
- package/src/l3/typing.ts +108 -15
- package/src/l3/unreduce.ts +6 -5
- package/src/l3/volatileptr.ts +17 -54
- package/src/mangle.ts +30 -0
- package/src/pattern/engine.ts +361 -73
- package/src/pipeline.ts +1039 -38
- package/src/proto-context.ts +1039 -45
- package/src/proto.ts +555 -40
- package/src/raise/aggmembers.ts +290 -0
- package/src/raise/arrays.ts +5 -1
- package/src/raise/const.ts +25 -0
- package/src/raise/declared-volatile.ts +207 -31
- package/src/raise/divpow2.ts +192 -0
- package/src/raise/floathelpers.ts +254 -33
- package/src/raise/gvn.ts +3 -3
- package/src/raise/latch.ts +162 -6
- package/src/raise/memberarrays.ts +10 -4
- package/src/raise/offsetnames.ts +422 -36
- package/src/raise/pairparams.ts +33 -0
- package/src/raise/paramwidth.ts +196 -6
- package/src/raise/pre-recovery.ts +107 -21
- package/src/raise/pre-structure.ts +73 -0
- package/src/raise/recover.ts +12 -5
- package/src/raise/reroll.ts +674 -0
- package/src/raise/shortcircuit.ts +217 -34
- package/src/raise/softdiv.ts +1 -1
- package/src/raise/storesign.ts +153 -0
- package/src/raise/struct-arrays.ts +30 -11
- package/src/raise/structs.ts +396 -33
- package/src/raise/truncload.ts +4 -3
- package/src/raise/unpre.ts +594 -0
- package/src/raise/unthread.ts +400 -0
- package/src/rank-declare.ts +118 -12
- package/src/rank-variations.ts +186 -35
- package/src/rank.ts +536 -68
- package/src/runtime-helpers.ts +140 -22
- package/src/structure/analysis.ts +217 -8
- package/src/structure/arith-ops.ts +23 -0
- package/src/structure/cross-jumped-tail.ts +216 -0
- package/src/structure/globalaccess.ts +157 -10
- package/src/structure/hazards.ts +16 -64
- package/src/structure/local-statics.ts +145 -9
- package/src/structure/namecoalesce.ts +1 -1
- package/src/structure/pointer-spelling.ts +1106 -0
- package/src/structure/redundant-test.ts +1 -1
- package/src/structure/structure.ts +6192 -1213
- package/src/structure/switch-recover.ts +583 -120
- package/src/structure/unfolded-branch-constant.ts +90 -0
- package/src/symbols.ts +58 -15
- package/src/target.ts +1028 -54
- package/src/trace.ts +49 -17
- package/src/variation-definitions.ts +333 -20
- package/src/variation-gates.ts +4 -0
- 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`,
|
|
4
|
-
// naming walk, no loop context. It answers three questions the access spellings in
|
|
5
|
-
// ask over and over — is this address `&gSym` plus something; does that something
|
|
6
|
-
// whole elements; and do the terms of it name the DECLARED subscripts of a
|
|
7
|
-
// — and it answers them the same way for every caller, which is the point
|
|
8
|
-
// rank-pinning fallback and the declared-subscript recovery share
|
|
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 {
|
|
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 `
|
|
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
|
-
|
|
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
|
+
}
|
package/src/structure/hazards.ts
CHANGED
|
@@ -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
|
|
8
|
-
// the loop's entry state so a guard about to be
|
|
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 {
|
|
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
|
-
|
|
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` (
|
|
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
|
|
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
|
};
|