@asmlift/core 0.10.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/package.json +1 -1
- package/src/backend/c.ts +1 -0
- package/src/backend/cfamily.ts +33 -12
- package/src/backend/cpp.ts +1 -0
- package/src/backend/lines.ts +10 -0
- package/src/backend/pascal.ts +17 -5
- package/src/cdecl/parse.ts +21 -0
- package/src/contracts.ts +73 -20
- package/src/frontend/arm-body.ts +155 -0
- package/src/frontend/asmdata.ts +2 -1
- package/src/frontend/call-plan.ts +197 -45
- package/src/frontend/callee-body.ts +370 -0
- package/src/frontend/device-pins.ts +18 -8
- package/src/frontend/frame-objects.ts +504 -61
- package/src/frontend/frontend.ts +20 -0
- package/src/frontend/jump-table.ts +35 -20
- package/src/frontend/mips.ts +226 -8
- package/src/frontend/ppc.ts +26 -2
- package/src/frontend/register-struct-call.ts +1 -86
- package/src/frontend/ssa.ts +263 -1
- package/src/frontend/stackargs.ts +95 -17
- package/src/frontend/thumb-entry.ts +340 -0
- package/src/frontend/thumb.ts +410 -61
- package/src/ir/alias.ts +29 -8
- package/src/ir/bits.ts +1081 -66
- package/src/ir/core.ts +154 -4
- package/src/ir/entry.ts +3 -5
- package/src/ir/opcodes.ts +28 -0
- package/src/ir/parse.ts +1 -0
- package/src/ir/simplify.ts +61 -10
- package/src/ir/verify.ts +16 -0
- package/src/l3/argbase.ts +2 -0
- package/src/l3/ast.ts +294 -2
- package/src/l3/basecse.ts +35 -10
- package/src/l3/caseendgoto.ts +223 -0
- package/src/l3/coalesce.ts +42 -14
- package/src/l3/constinit.ts +34 -6
- package/src/l3/dce.ts +52 -8
- package/src/l3/ptrcell.ts +2 -0
- package/src/l3/regspell.ts +2 -0
- package/src/l3/reindex.ts +13 -4
- package/src/l3/scopebase.ts +2 -0
- package/src/l3/tailmerge.ts +2 -0
- package/src/pipeline.ts +507 -23
- package/src/proto-context.ts +135 -1
- package/src/raise/const.ts +25 -0
- package/src/raise/declared-volatile.ts +1 -1
- package/src/raise/divpow2.ts +192 -0
- package/src/raise/offsetnames.ts +412 -10
- package/src/raise/pre-recovery.ts +8 -5
- package/src/raise/pre-structure.ts +24 -5
- package/src/raise/reroll.ts +4 -24
- package/src/raise/shortcircuit.ts +192 -23
- package/src/raise/unpre.ts +594 -0
- package/src/rank-variations.ts +97 -4
- package/src/rank.ts +258 -43
- package/src/structure/analysis.ts +56 -10
- package/src/structure/cross-jumped-tail.ts +216 -0
- package/src/structure/globalaccess.ts +46 -1
- package/src/structure/structure.ts +4221 -368
- package/src/structure/switch-recover.ts +345 -26
- package/src/structure/unfolded-branch-constant.ts +90 -0
- package/src/symbols.ts +52 -15
- package/src/target.ts +269 -8
- package/src/trace.ts +28 -14
- package/src/variation-definitions.ts +207 -6
- package/src/variation-gates.ts +2 -0
- package/src/variation-tokens.ts +63 -2
|
@@ -28,6 +28,7 @@ import {
|
|
|
28
28
|
defOpMap,
|
|
29
29
|
dominators,
|
|
30
30
|
mergeClasses,
|
|
31
|
+
opShape,
|
|
31
32
|
predecessors,
|
|
32
33
|
successorsOf,
|
|
33
34
|
} from '../ir/core';
|
|
@@ -1006,12 +1007,18 @@ function makeMemWriteBetween(deps: {
|
|
|
1006
1007
|
if (wDirty(b.ops, oi + 1, b.ops.length) || wDirty(render.blk.ops, 0, render.idx)) {
|
|
1007
1008
|
return true;
|
|
1008
1009
|
}
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1010
|
+
// The between-blocks: every `x` the def's block reaches without re-crossing it from which the
|
|
1011
|
+
// render block is reached again in at least one step, by the same def-avoiding walk. The render
|
|
1012
|
+
// block itself counts only where it reaches itself so (its head is checked above); any other
|
|
1013
|
+
// block only where it lies on a def→render path.
|
|
1014
|
+
const forward = reachAvoiding(b, b);
|
|
1015
|
+
if (!forward.has(render.blk)) {
|
|
1016
|
+
return false; // every block on a def→render path is one the def's block reaches
|
|
1017
|
+
}
|
|
1018
|
+
const onPath = blocksReaching(forward, render.blk, b);
|
|
1019
|
+
for (const x of forward) {
|
|
1020
|
+
if (!successorsOf(x).some((s) => s !== b && onPath.has(s))) {
|
|
1021
|
+
continue; // not on a def→render path (or, for the render block, acyclic: head checked)
|
|
1015
1022
|
}
|
|
1016
1023
|
if (wDirty(x.ops, 0, x.ops.length)) {
|
|
1017
1024
|
return true;
|
|
@@ -1021,6 +1028,49 @@ function makeMemWriteBetween(deps: {
|
|
|
1021
1028
|
};
|
|
1022
1029
|
}
|
|
1023
1030
|
|
|
1031
|
+
/** The blocks of `within` that reach `target` in zero or more steps without passing `avoid`,
|
|
1032
|
+
* `target` included: one backward walk over `within`'s own edges.
|
|
1033
|
+
*
|
|
1034
|
+
* `makeMemWriteBetween` asks, for each block `x` the def's block reaches, whether `target` is in
|
|
1035
|
+
* `reachAvoiding(x, avoid)`. That is a forward walk per `x`, quadratic in the function's size per
|
|
1036
|
+
* question, and the question is asked per op `analyze` names (`nameAccess`): Klonoa's
|
|
1037
|
+
* sub_0800D188 (1,783 blocks) spent most of its structuring in those walks. `target` is in
|
|
1038
|
+
* `reachAvoiding(x, avoid)` exactly where some successor of `x` other than `avoid` is in this set,
|
|
1039
|
+
* since every block on such a path after `x` is in `within` (`within` being everything the def's
|
|
1040
|
+
* block reaches without passing `avoid`, from which `x` was drawn). */
|
|
1041
|
+
function blocksReaching(within: ReadonlySet<Block>, target: Block, avoid: Block): Set<Block> {
|
|
1042
|
+
const into = new Map<Block, Block[]>();
|
|
1043
|
+
for (const y of within) {
|
|
1044
|
+
if (y === avoid) {
|
|
1045
|
+
continue;
|
|
1046
|
+
}
|
|
1047
|
+
for (const s of successorsOf(y)) {
|
|
1048
|
+
if (s !== avoid && within.has(s)) {
|
|
1049
|
+
const list = into.get(s);
|
|
1050
|
+
if (list === undefined) {
|
|
1051
|
+
into.set(s, [y]);
|
|
1052
|
+
} else {
|
|
1053
|
+
list.push(y);
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
const out = new Set<Block>();
|
|
1059
|
+
if (target === avoid) {
|
|
1060
|
+
return out;
|
|
1061
|
+
}
|
|
1062
|
+
out.add(target);
|
|
1063
|
+
for (const work = [target]; work.length;) {
|
|
1064
|
+
for (const y of into.get(work.pop()!) ?? []) {
|
|
1065
|
+
if (!out.has(y)) {
|
|
1066
|
+
out.add(y);
|
|
1067
|
+
work.push(y);
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1071
|
+
return out;
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1024
1074
|
/** INTERDEPENDENT PARALLEL-COPY ARGS. A terminator's successor args are ONE parallel copy
|
|
1025
1075
|
* (argAssigns), whose every read means the PRE-copy value. An arg whose def-tree reads a SIBLING
|
|
1026
1076
|
* arg of the same edge cannot read it by name there — so the sibling's whole expression is
|
|
@@ -2284,10 +2334,6 @@ function isBarrier(rules: MaterializeRules, materialize: ReadonlySet<Op>, access
|
|
|
2284
2334
|
|
|
2285
2335
|
const COMMUTATIVE_OPS: ReadonlySet<string> = new Set(['add', 'mul', 'and', 'or', 'xor']);
|
|
2286
2336
|
|
|
2287
|
-
/** One op's identity apart from its operands: opcode, result count and attributes, keys sorted. */
|
|
2288
|
-
const opShape = (op: Op): string =>
|
|
2289
|
-
`${op.opcode}/${op.results.length}/${JSON.stringify(Object.entries(op.attrs).sort(([a], [b]) => a.localeCompare(b)))}`;
|
|
2290
|
-
|
|
2291
2337
|
/** Do two values name the same address: one value, or two `gaddr`s of one symbol? */
|
|
2292
2338
|
function sameAddress(defOf: Map<Value, Op>, a: Value, b: Value): boolean {
|
|
2293
2339
|
if (a === b) {
|
|
@@ -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
|
+
}
|
|
@@ -75,6 +75,42 @@ export function globalByteBase(e: Expr): { name: string; residual: Expr } | null
|
|
|
75
75
|
return null;
|
|
76
76
|
}
|
|
77
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
|
+
|
|
78
114
|
// THE one gate on the BARE-NAME array-global spelling (`gSym[i]` rather than `((T *)&gSym)[i]`),
|
|
79
115
|
// shared by the constant-offset and variable-index access paths so the two cannot disagree.
|
|
80
116
|
// Returns the `index` node's `lead` fragment when the bare form is spellable, or null to fall
|
|
@@ -93,10 +129,19 @@ export function globalByteBase(e: Expr): { name: string; residual: Expr } | null
|
|
|
93
129
|
// inner extent) gets no bare form at all; `((T *)&gSym)[i]` is byte-identical and valid under ANY
|
|
94
130
|
// declaration, which is why it is the safe fallback. See symbols.ts arrayInnerExtents for why an
|
|
95
131
|
// ABSENT rank is read as 1 rather than as unknown.
|
|
96
|
-
|
|
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 {
|
|
97
138
|
if (!bareArrayElement(si, width, signed)) {
|
|
98
139
|
return null;
|
|
99
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
|
+
}
|
|
100
145
|
const inner = arrayInnerExtents(si);
|
|
101
146
|
return inner === null ? null : inner.length === 0 ? {} : { lead: inner.map((): Expr => ({ k: 'const', value: 0 })) };
|
|
102
147
|
}
|