@asmlift/core 0.4.0 → 0.6.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 +22 -16
- package/package.json +1 -1
- package/src/backend/c.ts +1 -0
- package/src/backend/cfamily.ts +238 -164
- package/src/backend/cpp.ts +1 -0
- package/src/backend/pascal.ts +26 -12
- package/src/contracts.ts +341 -22
- package/src/declare.ts +41 -4
- package/src/frontend/mips.ts +24 -6
- package/src/frontend/opaque.ts +31 -18
- package/src/frontend/ppc.ts +54 -7
- package/src/frontend/ssa.ts +632 -13
- package/src/frontend/thumb.ts +2786 -286
- package/src/ir/alias.ts +129 -0
- package/src/ir/bits.ts +75 -0
- package/src/ir/core.ts +337 -2
- package/src/ir/opcodes.ts +156 -27
- package/src/ir/parse.ts +19 -2
- package/src/ir/print.ts +27 -2
- package/src/ir/simplify.ts +190 -3
- package/src/ir/struct-names.ts +42 -0
- package/src/ir/verify.ts +43 -49
- package/src/l3/address.ts +62 -0
- package/src/l3/argbase.ts +8 -2
- package/src/l3/ast.ts +464 -49
- package/src/l3/basecse.ts +709 -88
- package/src/l3/coalesce.ts +521 -66
- package/src/l3/dce.ts +54 -19
- package/src/l3/gates.ts +88 -0
- package/src/l3/hoist.ts +293 -14
- package/src/l3/homesplit.ts +285 -0
- package/src/l3/initfirst.ts +301 -0
- package/src/l3/inlinebase.ts +193 -0
- package/src/l3/mentions.ts +113 -0
- package/src/l3/mulfirst.ts +42 -0
- package/src/l3/nearbase.ts +152 -0
- package/src/l3/offmember.ts +371 -0
- package/src/l3/parkfirst.ts +96 -0
- package/src/l3/pollguard.ts +154 -0
- package/src/l3/ptrfield.ts +227 -0
- package/src/l3/regspell.ts +110 -85
- package/src/l3/reindex.ts +715 -78
- package/src/l3/scopebase.ts +649 -219
- package/src/l3/sinkinit.ts +40 -0
- package/src/l3/slotorder.ts +123 -0
- package/src/l3/storage.ts +48 -0
- package/src/l3/symbol-refs.ts +41 -8
- package/src/l3/tailmerge.ts +23 -4
- package/src/l3/typing.ts +198 -9
- package/src/l3/unmerge.ts +263 -0
- package/src/l3/unreduce.ts +971 -0
- package/src/l3/volatileptr.ts +207 -0
- package/src/l3/volatileval.ts +130 -0
- package/src/l3/volstore.ts +229 -0
- package/src/l3/zerosub.ts +62 -0
- package/src/pattern/engine.ts +236 -13
- package/src/pipeline.ts +206 -49
- package/src/proto.ts +112 -14
- package/src/raise/arrays.ts +6 -1
- package/src/raise/divpow2.ts +4 -3
- package/src/raise/globalshape.ts +1038 -0
- package/src/raise/gvn.ts +44 -19
- package/src/raise/latch.ts +126 -0
- package/src/raise/memberarrays.ts +594 -0
- package/src/raise/narrow.ts +124 -0
- package/src/raise/narrowlocal.ts +556 -0
- package/src/raise/paramwidth.ts +179 -0
- package/src/raise/pre-recovery.ts +101 -16
- package/src/raise/recover.ts +56 -23
- package/src/raise/retsink.ts +215 -14
- package/src/raise/shortcircuit.ts +477 -79
- package/src/raise/struct-arrays.ts +21 -3
- package/src/raise/structs.ts +61 -3
- package/src/rank-axes.ts +630 -0
- package/src/rank-declare.ts +256 -0
- package/src/rank.ts +1726 -251
- package/src/structure/analysis.ts +1516 -220
- package/src/structure/bitfields.ts +332 -0
- package/src/structure/globalaccess.ts +274 -0
- package/src/structure/hazards.ts +411 -20
- package/src/structure/loops.ts +2 -49
- package/src/structure/namecoalesce.ts +435 -0
- package/src/structure/structure.ts +2850 -533
- package/src/structure/switch-recover.ts +688 -147
- package/src/symbols.ts +62 -1
- package/src/target.ts +367 -24
- package/src/trace.ts +111 -32
|
@@ -0,0 +1,594 @@
|
|
|
1
|
+
// asmlift — STRUCT-MEMBER ARRAY recovery (L1 → a struct whose member is an ARRAY).
|
|
2
|
+
//
|
|
3
|
+
// THE SHAPE. `d->name[i]` — a variable-index walk over an array that lives at a constant byte
|
|
4
|
+
// offset INSIDE a struct — reaches this pass as an `aload`/`astore` whose base is a materialized
|
|
5
|
+
// `add(P, K)`:
|
|
6
|
+
//
|
|
7
|
+
// add r5, r0, #0x4 %4 = add %0, {value=4} (the member's address, loop-invariant)
|
|
8
|
+
// ldrh r0, [r1, r0] %9 = aload %6, %8 {elemSize=2}
|
|
9
|
+
//
|
|
10
|
+
// raise/arrays.ts legalizes the access, but the `+K` stays an ordinary add, so the base is an
|
|
11
|
+
// untyped word and the backend spells the whole thing `((u16 *)(a0 + 4))[i]`. This pass reads the
|
|
12
|
+
// add as a MEMBER SELECTION instead: `P` points at a struct whose byte-K member is an array of
|
|
13
|
+
// `elemSize` elements, and the access is `P->field_K[i]`.
|
|
14
|
+
//
|
|
15
|
+
// A BYTE member is outside that shape in both directions, and the bound is arrays.ts's rather than
|
|
16
|
+
// this pass's: only the shift-scaled address is legalized, so a `u8[]` walk keeps its `add(base,
|
|
17
|
+
// index)` and mints no site here — and that surviving add is then an escaping use of the base,
|
|
18
|
+
// which refuses the WIDER members beside it too (`escaping-use`). A struct that mixes a byte member
|
|
19
|
+
// with u16/s32 members is therefore not reached at all; legalizing the unscaled form is what would
|
|
20
|
+
// reach it.
|
|
21
|
+
//
|
|
22
|
+
// WHY THE SPELLING IS NOT COSMETIC. agbcc puts the `+K` in a different place for each of the three
|
|
23
|
+
// C spellings of this access, and only one of them is the target's. Compiled with this benchmark's
|
|
24
|
+
// own agbcc and scored against `synthetic:membnarrow`'s own target object:
|
|
25
|
+
//
|
|
26
|
+
// `(a0 + 2)[i]` `+4` in the LOAD DISPLACEMENT, `ldrh r0,[r0,#0x4]`, 2 saves 11
|
|
27
|
+
// `u16 *v = a0 + 2; v[i]` preheader `add r4,r0,#0x4`, INDEX-first `add r2,r0,r4`, 2 saves 11
|
|
28
|
+
// `a0->field_4[i]` preheader `add r5,r0,#0x4`, BASE-first `add r2,r5,r0`, 3 saves 0
|
|
29
|
+
//
|
|
30
|
+
// So the evidence this pass reads is the materialized add itself: the cast spelling materializes no
|
|
31
|
+
// member address at all, and `arrays.ts` legalizes only an `off === 0` access, so that spelling
|
|
32
|
+
// produces no site here. `synthetic:basefold` is that control — its reference IS the cast spelling
|
|
33
|
+
// — and it MATCHES today, untouched.
|
|
34
|
+
//
|
|
35
|
+
// A DEFAULT, NOT A RANKED AXIS, and the reason is a measurement rather than a preference. An axis
|
|
36
|
+
// exists where two source spellings collapse onto one asm, so nothing but the differ can separate
|
|
37
|
+
// them. Here the asm separates them itself — line one folds `+K` into the memory operand and
|
|
38
|
+
// produces NO site — so `add(P, K)` feeding an off-0 scaled access is evidence AGAINST the spelling
|
|
39
|
+
// asmlift emits today, not a coin flip. And an axis at this level is a LIFT variant (`/setup-args`'s
|
|
40
|
+
// position in rank.ts), doubling the whole candidate product for every function with a site: swept
|
|
41
|
+
// over 2288 sa3 and 412 klonoa functions in both symbol-map configurations, the gates below admit
|
|
42
|
+
// two bases, in one function, which declines for an unrelated reason — so the price would be paid
|
|
43
|
+
// to referee a question the corpus never poses. What is NOT settled is line two, and that is a
|
|
44
|
+
// different question: where a base LOCAL goes is the L3 base-local levers' business, taken over
|
|
45
|
+
// whatever type the recovery mints, not a second answer to the TYPE this pass decides.
|
|
46
|
+
//
|
|
47
|
+
// WHY THE TRAILING COUNT IS A GATE RATHER THAN A GUESS, and it is the struct's SIZE that makes it
|
|
48
|
+
// one. The count itself is nearly invisible: on `synthetic:membnarrow`'s own target, declaring the
|
|
49
|
+
// walked member `u16 field_4[N]` scores 11 at N = 1 and 2 and MATCHES at every N from 3 to 100.
|
|
50
|
+
// What moved was `sizeof` — hold the count at 1 and pad the struct instead and the same threshold
|
|
51
|
+
// appears (size 6 and 8 score 11, size 10 and up match), because agbcc hoists the loop base out
|
|
52
|
+
// only once the object is big enough. So a count is not a fact to derive but a free parameter with
|
|
53
|
+
// a byte-observable LOWER BOUND, and inventing one invents a size. The count is therefore read off
|
|
54
|
+
// the loop that walks the member — `boundedCount` below — and the whole base DECLINES when no
|
|
55
|
+
// counted loop states one. An INTERIOR member's count is not a choice at all: it is forced by the
|
|
56
|
+
// member that follows it.
|
|
57
|
+
//
|
|
58
|
+
// THE SIBLING PASSES, and why this is a third one rather than a case inside either.
|
|
59
|
+
// raise/structs.ts recovers a struct from CONSTANT-offset accesses and leaves every variable-index
|
|
60
|
+
// base alone (its `arrayBases` set); raise/struct-arrays.ts recovers an ARRAY OF STRUCTS, where the
|
|
61
|
+
// index scales the stride and the constant offset is the field. This pass is the remaining corner —
|
|
62
|
+
// the constant offset selects the member and the index strides inside it.
|
|
63
|
+
//
|
|
64
|
+
// The three shapes are NOT disjoint, and the overlap is per-ACCESS rather than per-base:
|
|
65
|
+
// `P->tbl[i].f` is an array of structs living at a member offset, so it carries both this pass's
|
|
66
|
+
// address shape and struct-arrays'. Ordering alone does not deconflict them, because the two passes
|
|
67
|
+
// claim different VALUES — struct-arrays types the materialized `add(P, K)`, this pass groups on
|
|
68
|
+
// `P` — so each refuses what another has marked: `claimed-access` and `base-typed` here,
|
|
69
|
+
// `arrayBases` and the `unknown` check there. `field_K[i].f` is a layout this pass does not declare;
|
|
70
|
+
// the loud half of that refusal is in structure.ts's `arrayAccess`.
|
|
71
|
+
import { type Fn, type Op, type Value, defOpMap } from '../ir/core';
|
|
72
|
+
import { type IrType, type StructField, T, scalarTypeForAccess } from '../ir/types';
|
|
73
|
+
import { type Gate, firstRejection } from '../l3/gates';
|
|
74
|
+
import { firstFreeStructIndex } from './structs';
|
|
75
|
+
|
|
76
|
+
/** One variable-index access, resolved onto (base value, member offset). */
|
|
77
|
+
interface MemberAccess {
|
|
78
|
+
op: Op;
|
|
79
|
+
off: number;
|
|
80
|
+
elemSize: number;
|
|
81
|
+
/** loads only; a store carries structs.ts's `width === 4` convention */
|
|
82
|
+
signed: boolean;
|
|
83
|
+
isLoad: boolean;
|
|
84
|
+
index: Value;
|
|
85
|
+
/** the `add(base, off)` result, or null when the access sits straight on the base */
|
|
86
|
+
addr: Value | null;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** One member of the synthesized struct: an offset, an element type, and the counts the loops
|
|
90
|
+
* walking it reach. */
|
|
91
|
+
interface Member {
|
|
92
|
+
off: number;
|
|
93
|
+
elemSize: number;
|
|
94
|
+
signed: boolean;
|
|
95
|
+
/** element counts derived from the walking loops' bounds — empty when none was derivable */
|
|
96
|
+
bounds: number[];
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** What the gates below judge: one base value and the member set its accesses describe. */
|
|
100
|
+
export interface MemberArrayCandidate {
|
|
101
|
+
/** the base is still untyped, so no earlier recovery has claimed it */
|
|
102
|
+
untypedBase: boolean;
|
|
103
|
+
/** no access, nor the member address it is taken off, was already claimed by an earlier recovery */
|
|
104
|
+
unclaimedAccesses: boolean;
|
|
105
|
+
/** the base reaches the address of a NAMED global, whose declaration is the project's own */
|
|
106
|
+
namedGlobalBase: boolean;
|
|
107
|
+
/** some access sits at a NONZERO member offset — a base walked only at offset 0 is a plain array */
|
|
108
|
+
hasNonZeroMember: boolean;
|
|
109
|
+
/** every member offset is small enough to BE an offset rather than an absolute address */
|
|
110
|
+
offsetsInRange: boolean;
|
|
111
|
+
/** no constant-offset `load`/`store` reads the base or a member address (raise/structs.ts's shape) */
|
|
112
|
+
noDirectAccess: boolean;
|
|
113
|
+
/** every use of the base and of every member address is a variable-index access BASE */
|
|
114
|
+
noEscape: boolean;
|
|
115
|
+
/** every member offset is non-negative and a multiple of its own element size */
|
|
116
|
+
aligned: boolean;
|
|
117
|
+
/** accesses sharing an offset agree on element size and on load signedness */
|
|
118
|
+
consistent: boolean;
|
|
119
|
+
/** natural C alignment seats every member at exactly its observed offset */
|
|
120
|
+
seatable: boolean;
|
|
121
|
+
/** the TRAILING member's element count is stated by a counted loop */
|
|
122
|
+
trailingBounded: boolean;
|
|
123
|
+
/** no member's walk runs past the member that follows it */
|
|
124
|
+
inBounds: boolean;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export const MEMBER_ARRAY_GATES: readonly Gate<MemberArrayCandidate>[] = [
|
|
128
|
+
{
|
|
129
|
+
id: 'base-typed',
|
|
130
|
+
why: 'an earlier recovery already typed this base from evidence of its own',
|
|
131
|
+
sound: false,
|
|
132
|
+
guardedBy: 'member-arrays.test.ts: an array-of-struct base declines',
|
|
133
|
+
rejects: (c) => !c.untypedBase,
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
// The evidence is per-ACCESS and the refusal is per-BASE. raise/struct-arrays.ts types the
|
|
137
|
+
// MATERIALIZED member address `add(P, K)` and marks the accesses it mints with its own
|
|
138
|
+
// `fieldOff`, while the base `P` this pass groups on stays untyped — so `P->tbl[i].f` reaches
|
|
139
|
+
// the gates as an ordinary member walk, and claiming it drops the field offset (an array of
|
|
140
|
+
// structs at a member offset is not the layout this pass declares) and re-spells the element
|
|
141
|
+
// STRIDE as a scalar width. One marked access refuses the whole base, which costs the members
|
|
142
|
+
// beside it: a struct walked at a member offset AND holding an array of structs elsewhere is
|
|
143
|
+
// refused entirely (measured at 4 points on such a shape). Claiming only the unmarked accesses
|
|
144
|
+
// would retype `P` underneath the accesses struct-arrays owns, so the narrower rule is a
|
|
145
|
+
// change to both passes rather than to this predicate.
|
|
146
|
+
id: 'claimed-access',
|
|
147
|
+
why: 'an access another recovery has already claimed carries an offset this pass would drop',
|
|
148
|
+
sound: true,
|
|
149
|
+
guardedBy: 'member-arrays.test.ts: an array-of-struct member declines',
|
|
150
|
+
rejects: (c) => !c.unclaimedAccesses,
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
// A named global's layout belongs to the project's headers, and the member spelling is the one
|
|
154
|
+
// spelling that cannot consult them: structure.ts's `arrayAccess` renders `&gSym` through the
|
|
155
|
+
// symbol context — the map's own array declaration, `gSym[i]` — only while no member offset is
|
|
156
|
+
// set, and takes the `(struct StructN *)&gSym` cast once one is. So claiming a global trades a
|
|
157
|
+
// declaration asmlift has evidence for against one it invents. The base is resolved through
|
|
158
|
+
// block parameters because a global merged onto a `?:` is still a global.
|
|
159
|
+
id: 'global-base',
|
|
160
|
+
why: "a named global's layout is the project's own declaration, not one to synthesize",
|
|
161
|
+
sound: false,
|
|
162
|
+
guardedBy: 'member-arrays.test.ts: a named global base declines',
|
|
163
|
+
rejects: (c) => c.namedGlobalBase,
|
|
164
|
+
},
|
|
165
|
+
{
|
|
166
|
+
id: 'no-member-offset',
|
|
167
|
+
why: 'a base walked only at offset 0 is a plain array — nothing states a struct around it',
|
|
168
|
+
sound: false,
|
|
169
|
+
guardedBy: 'member-arrays.test.ts: a bare array walk declines',
|
|
170
|
+
rejects: (c) => !c.hasNonZeroMember,
|
|
171
|
+
},
|
|
172
|
+
{
|
|
173
|
+
// A MATERIALIZED addend is not bounded the way a memory operand's immediate is. `add(P, K)`
|
|
174
|
+
// with K an absolute address — `ldr r0, =0x03000004` then `add r0, r0, rX` — is base-plus-index
|
|
175
|
+
// with the roles reversed, and reading it as a member selection declares a 48 MB struct. Nothing
|
|
176
|
+
// in the IR separates the two: raise/const.ts folds a pool-loaded literal and an add-immediate
|
|
177
|
+
// into the same `const`. So the separation is STATED here rather than derived, and the number is
|
|
178
|
+
// chosen where the two populations do not overlap: below every mapped GBA region base
|
|
179
|
+
// (0x02000000), and above the offsets this recognizer admits — 26 is the largest on the sa3 and
|
|
180
|
+
// klonoa corpora, and a hand-built struct is admitted at 40, 100 and 200. A target whose objects
|
|
181
|
+
// run past it is what would earn a declared field to read instead.
|
|
182
|
+
id: 'member-offset-range',
|
|
183
|
+
why: 'a materialized addend that large is an absolute address, not a member offset',
|
|
184
|
+
sound: false,
|
|
185
|
+
guardedBy: 'member-arrays.test.ts: an absolute-address addend declines',
|
|
186
|
+
rejects: (c) => !c.offsetsInRange,
|
|
187
|
+
},
|
|
188
|
+
{
|
|
189
|
+
id: 'direct-access',
|
|
190
|
+
why: 'a constant-offset access alongside the walk is the struct-POINTER shape raise/structs.ts owns',
|
|
191
|
+
sound: false,
|
|
192
|
+
guardedBy: 'member-arrays.test.ts: a base read at a constant offset declines',
|
|
193
|
+
rejects: (c) => !c.noDirectAccess,
|
|
194
|
+
},
|
|
195
|
+
{
|
|
196
|
+
// The byte member of the coverage bound above lands here too: an unlegalized `add(base, index)`
|
|
197
|
+
// is a use of the base that is not a variable-index access, so one `u8[]` member refuses every
|
|
198
|
+
// wider member of its struct.
|
|
199
|
+
id: 'escaping-use',
|
|
200
|
+
why: 'a use this rewrite cannot see keeps reading the base as a word the retype no longer spells',
|
|
201
|
+
sound: false,
|
|
202
|
+
guardedBy: 'member-arrays.test.ts: a member address forwarded on a branch declines',
|
|
203
|
+
rejects: (c) => !c.noEscape,
|
|
204
|
+
},
|
|
205
|
+
{
|
|
206
|
+
id: 'member-conflict',
|
|
207
|
+
why: 'one offset read at two widths or two extensions is a union view, not a member',
|
|
208
|
+
sound: false,
|
|
209
|
+
guardedBy: 'member-arrays.test.ts: two widths at one offset decline',
|
|
210
|
+
rejects: (c) => !c.consistent,
|
|
211
|
+
},
|
|
212
|
+
{
|
|
213
|
+
// SOUND, and it is the address that is at stake rather than the spelling: C aligns a member to
|
|
214
|
+
// its own element type, so declaring an `s32` member at byte 2 would seat it at byte 4 and
|
|
215
|
+
// every access through it would read four bytes off the observed address. A NEGATIVE offset is
|
|
216
|
+
// unseatable for a different reason — the base points into the middle of an object rather than
|
|
217
|
+
// at a struct — and the modulo alone does not catch it, because JS `-4 % 2` is `-0`.
|
|
218
|
+
id: 'member-align',
|
|
219
|
+
why: 'a member C cannot seat at its observed offset addresses different bytes than the asm did',
|
|
220
|
+
sound: true,
|
|
221
|
+
guardedBy: 'member-arrays.test.ts: a word member at byte 2 declines',
|
|
222
|
+
rejects: (c) => !c.aligned,
|
|
223
|
+
},
|
|
224
|
+
{
|
|
225
|
+
// SOUND for the same reason: the interior member counts are what place every later member, so
|
|
226
|
+
// a run that overlaps its successor or does not divide by its element size mislays it.
|
|
227
|
+
id: 'member-seat',
|
|
228
|
+
why: 'a member run that overlaps the next one shifts every later member off its observed offset',
|
|
229
|
+
sound: true,
|
|
230
|
+
guardedBy: 'member-arrays.test.ts: overlapping member runs decline',
|
|
231
|
+
rejects: (c) => !c.seatable,
|
|
232
|
+
},
|
|
233
|
+
{
|
|
234
|
+
id: 'trailing-unbounded',
|
|
235
|
+
why: "nothing but the walking loop's bound states the last member's element count, and a count invents a sizeof",
|
|
236
|
+
sound: false,
|
|
237
|
+
guardedBy: 'member-arrays.test.ts: a member walked to a runtime bound declines',
|
|
238
|
+
rejects: (c) => !c.trailingBounded,
|
|
239
|
+
},
|
|
240
|
+
{
|
|
241
|
+
id: 'member-overrun',
|
|
242
|
+
why: 'a walk running past the next member is evidence of a layout other than the one being declared',
|
|
243
|
+
sound: false,
|
|
244
|
+
guardedBy: 'member-arrays.test.ts: a walk overrunning its successor declines',
|
|
245
|
+
rejects: (c) => !c.inBounds,
|
|
246
|
+
},
|
|
247
|
+
];
|
|
248
|
+
|
|
249
|
+
/** The envelope `member-offset-range` states — see that gate. */
|
|
250
|
+
const MAX_MEMBER_OFFSET = 0x10000;
|
|
251
|
+
|
|
252
|
+
/** Strip `sext`/`zext` wrappers — a narrow counter reaches its uses through them. */
|
|
253
|
+
function stripExt(v: Value, defs: Map<Value, Op>): Value {
|
|
254
|
+
for (;;) {
|
|
255
|
+
const d = defs.get(v);
|
|
256
|
+
if (d === undefined || (d.opcode !== 'sext' && d.opcode !== 'zext')) {
|
|
257
|
+
return v;
|
|
258
|
+
}
|
|
259
|
+
v = d.operands[0];
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/** `add(x, const)` read as (other operand, constant), or null. The add is commutative. */
|
|
264
|
+
function constAddend(op: Op, defs: Map<Value, Op>): { of: Value; k: number } | null {
|
|
265
|
+
if (op.opcode !== 'add' || op.operands.length !== 2) {
|
|
266
|
+
return null;
|
|
267
|
+
}
|
|
268
|
+
for (const [ci, oi] of [
|
|
269
|
+
[0, 1],
|
|
270
|
+
[1, 0],
|
|
271
|
+
] as const) {
|
|
272
|
+
const c = defs.get(op.operands[ci]);
|
|
273
|
+
if (c?.opcode === 'const') {
|
|
274
|
+
return { of: op.operands[oi], k: c.attrs.value as number };
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
return null;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/** How many elements the loop walking `idx` reaches, or null when the shape is not a counted loop.
|
|
281
|
+
*
|
|
282
|
+
* THE ONE SHAPE THIS READS, and everything else declines: `idx` is a block parameter (through any
|
|
283
|
+
* extensions) of a header with exactly two in-edges, one carrying a non-negative constant and one
|
|
284
|
+
* the TRUE arm of a `cond_br` whose condition compares `param + 1` against a constant bound. The
|
|
285
|
+
* count is then the bound plus one for `<=`, the bound itself for `<`. A `>`/`>=` test, a step
|
|
286
|
+
* other than one, a bound that is not a constant, or a third edge into the header all decline —
|
|
287
|
+
* the answer is a DECLARED element count, so a shape whose last index is not read off the asm has
|
|
288
|
+
* no honest one. */
|
|
289
|
+
function boundedCount(fn: Fn, idx: Value, defs: Map<Value, Op>): number | null {
|
|
290
|
+
const param = stripExt(idx, defs);
|
|
291
|
+
const header = fn.blocks.find((b) => b.params.includes(param));
|
|
292
|
+
if (header === undefined) {
|
|
293
|
+
return null;
|
|
294
|
+
}
|
|
295
|
+
const slot = header.params.indexOf(param);
|
|
296
|
+
const edges: { op: Op; index: number }[] = [];
|
|
297
|
+
for (const b of fn.blocks) {
|
|
298
|
+
for (const op of b.ops) {
|
|
299
|
+
op.successors.forEach((s, i) => {
|
|
300
|
+
if (s.block === header) {
|
|
301
|
+
edges.push({ op, index: i });
|
|
302
|
+
}
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
if (edges.length !== 2) {
|
|
307
|
+
return null;
|
|
308
|
+
}
|
|
309
|
+
const entry = edges.find((e) => defs.get(e.op.successors[e.index].args[slot])?.opcode === 'const');
|
|
310
|
+
const back = edges.find((e) => e !== entry);
|
|
311
|
+
if (entry === undefined || back === undefined || back.op.opcode !== 'cond_br' || back.index !== 0) {
|
|
312
|
+
return null;
|
|
313
|
+
}
|
|
314
|
+
if ((defs.get(entry.op.successors[entry.index].args[slot])!.attrs.value as number) < 0) {
|
|
315
|
+
return null;
|
|
316
|
+
}
|
|
317
|
+
// the back edge carries `param + 1`, possibly re-extended by the counter's own truncation
|
|
318
|
+
const next = stripExt(back.op.successors[0].args[slot], defs);
|
|
319
|
+
const step = defs.get(next) === undefined ? null : constAddend(defs.get(next)!, defs);
|
|
320
|
+
if (step === null || step.k !== 1 || stripExt(step.of, defs) !== param) {
|
|
321
|
+
return null;
|
|
322
|
+
}
|
|
323
|
+
// …and the exit test compares that same value against a constant bound
|
|
324
|
+
const cond = defs.get(back.op.operands[0]);
|
|
325
|
+
if (cond === undefined || cond.operands.length !== 2) {
|
|
326
|
+
return null;
|
|
327
|
+
}
|
|
328
|
+
const inclusive = cond.opcode === 'icmp_sle' || cond.opcode === 'icmp_ule';
|
|
329
|
+
if (!inclusive && cond.opcode !== 'icmp_slt' && cond.opcode !== 'icmp_ult') {
|
|
330
|
+
return null;
|
|
331
|
+
}
|
|
332
|
+
if (stripExt(cond.operands[0], defs) !== next) {
|
|
333
|
+
return null;
|
|
334
|
+
}
|
|
335
|
+
const bound = defs.get(cond.operands[1]);
|
|
336
|
+
if (bound?.opcode !== 'const') {
|
|
337
|
+
return null;
|
|
338
|
+
}
|
|
339
|
+
const count = (bound.attrs.value as number) + (inclusive ? 1 : 0);
|
|
340
|
+
return count >= 1 ? count : null;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/** Every value that reaches the address of a NAMED global. A `gaddr` result is one; so is a block
|
|
344
|
+
* parameter ANY of whose in-edges carries one, because a base merged from `&gA` and `&gB` — or
|
|
345
|
+
* from `&gA` and something else — still spells at least one global's layout. */
|
|
346
|
+
function namedGlobals(fn: Fn, defs: Map<Value, Op>): Set<Value> {
|
|
347
|
+
const global = new Set<Value>();
|
|
348
|
+
const incoming = new Map<Value, Value[]>();
|
|
349
|
+
for (const b of fn.blocks) {
|
|
350
|
+
for (const op of b.ops) {
|
|
351
|
+
if (op.opcode === 'gaddr') {
|
|
352
|
+
global.add(op.results[0]);
|
|
353
|
+
}
|
|
354
|
+
for (const s of op.successors) {
|
|
355
|
+
s.block.params.forEach((param, i) => {
|
|
356
|
+
incoming.set(param, [...(incoming.get(param) ?? []), s.args[i]]);
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
for (let grew = true; grew;) {
|
|
362
|
+
grew = false;
|
|
363
|
+
for (const [param, args] of incoming) {
|
|
364
|
+
if (!global.has(param) && args.some((a) => global.has(a) || defs.get(a)?.opcode === 'gaddr')) {
|
|
365
|
+
global.add(param);
|
|
366
|
+
grew = true;
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
return global;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/** Group every variable-index access by the base it is taken off, resolving a `add(P, K)` address
|
|
374
|
+
* into (P, K). An offset-0 access contributes its own base. */
|
|
375
|
+
function accessesByBase(fn: Fn, defs: Map<Value, Op>): Map<Value, MemberAccess[]> {
|
|
376
|
+
const out = new Map<Value, MemberAccess[]>();
|
|
377
|
+
for (const b of fn.blocks) {
|
|
378
|
+
for (const op of b.ops) {
|
|
379
|
+
if (op.opcode !== 'aload' && op.opcode !== 'astore') {
|
|
380
|
+
continue;
|
|
381
|
+
}
|
|
382
|
+
const isLoad = op.opcode === 'aload';
|
|
383
|
+
const elemSize = op.attrs.elemSize as number;
|
|
384
|
+
const addrDef = defs.get(op.operands[0]);
|
|
385
|
+
const add = addrDef === undefined ? null : constAddend(addrDef, defs);
|
|
386
|
+
const base = add === null ? op.operands[0] : add.of;
|
|
387
|
+
const list = out.get(base) ?? [];
|
|
388
|
+
list.push({
|
|
389
|
+
op,
|
|
390
|
+
off: add === null ? 0 : add.k,
|
|
391
|
+
elemSize,
|
|
392
|
+
signed: isLoad ? (op.attrs.signed as boolean) : elemSize === 4,
|
|
393
|
+
isLoad,
|
|
394
|
+
index: op.operands[1],
|
|
395
|
+
addr: add === null ? null : op.operands[0],
|
|
396
|
+
});
|
|
397
|
+
out.set(base, list);
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
return out;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/** One candidate base, as the gates read it, plus the members the rewrite would declare. */
|
|
404
|
+
export interface MemberArrayGroup {
|
|
405
|
+
base: Value;
|
|
406
|
+
c: MemberArrayCandidate;
|
|
407
|
+
/** Non-empty: `accessesByBase` only keys a base it recorded an access for, and every access puts
|
|
408
|
+
* an entry in `byOff`, whose values these are. The type carries it so `fieldsOf` and the
|
|
409
|
+
* trailing-member gate can index the ends without a guard the checker cannot see through. */
|
|
410
|
+
members: [Member, ...Member[]];
|
|
411
|
+
accesses: MemberAccess[];
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/** Every base a variable-index access is taken off, with the member set its accesses describe.
|
|
415
|
+
* Exported so a corpus sweep can price the gate table over the bases it REFUSES. */
|
|
416
|
+
export function memberArrayCandidates(fn: Fn): MemberArrayGroup[] {
|
|
417
|
+
const defs = defOpMap(fn);
|
|
418
|
+
const global = namedGlobals(fn, defs);
|
|
419
|
+
const out: MemberArrayGroup[] = [];
|
|
420
|
+
const bounds = new Map<Value, number | null>();
|
|
421
|
+
for (const [base, accesses] of accessesByBase(fn, defs)) {
|
|
422
|
+
// one loop-bound read per index value — the two ends of a copy loop share it
|
|
423
|
+
const bound = (v: Value): number | null => {
|
|
424
|
+
if (!bounds.has(v)) {
|
|
425
|
+
bounds.set(v, boundedCount(fn, v, defs));
|
|
426
|
+
}
|
|
427
|
+
return bounds.get(v)!;
|
|
428
|
+
};
|
|
429
|
+
const addrs = new Set<Value>(accesses.flatMap((a) => (a.addr === null ? [] : [a.addr])));
|
|
430
|
+
// Every use of the base and of every member address must be a variable-index access base: the
|
|
431
|
+
// member addresses disappear in the rewrite, and the base takes a struct-pointer type every
|
|
432
|
+
// other use would have to spell.
|
|
433
|
+
let noEscape = true;
|
|
434
|
+
let noDirectAccess = true;
|
|
435
|
+
for (const b of fn.blocks) {
|
|
436
|
+
for (const op of b.ops) {
|
|
437
|
+
const isVarIndex = op.opcode === 'aload' || op.opcode === 'astore';
|
|
438
|
+
op.operands.forEach((o, k) => {
|
|
439
|
+
if (o !== base && !addrs.has(o)) {
|
|
440
|
+
return;
|
|
441
|
+
}
|
|
442
|
+
if (op.opcode === 'load' || op.opcode === 'store') {
|
|
443
|
+
noDirectAccess = false;
|
|
444
|
+
} else if (isVarIndex ? k !== 0 : !(o === base && addrs.has(op.results[0]))) {
|
|
445
|
+
noEscape = false;
|
|
446
|
+
}
|
|
447
|
+
});
|
|
448
|
+
for (const s of op.successors) {
|
|
449
|
+
if (s.args.some((a) => a === base || addrs.has(a))) {
|
|
450
|
+
noEscape = false;
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
// one member per distinct offset; a load's extension wins over a store's width convention
|
|
456
|
+
const byOff = new Map<number, Member>();
|
|
457
|
+
const loadSigned = new Map<number, boolean>();
|
|
458
|
+
let consistent = true;
|
|
459
|
+
let aligned = true;
|
|
460
|
+
for (const a of accesses) {
|
|
461
|
+
if (a.off < 0 || a.off % a.elemSize !== 0) {
|
|
462
|
+
aligned = false;
|
|
463
|
+
}
|
|
464
|
+
const prev = byOff.get(a.off);
|
|
465
|
+
const m = prev ?? { off: a.off, elemSize: a.elemSize, signed: a.signed, bounds: [] };
|
|
466
|
+
if (prev !== undefined && prev.elemSize !== a.elemSize) {
|
|
467
|
+
consistent = false;
|
|
468
|
+
}
|
|
469
|
+
if (a.isLoad) {
|
|
470
|
+
const seen = loadSigned.get(a.off);
|
|
471
|
+
if (seen !== undefined && seen !== a.signed) {
|
|
472
|
+
consistent = false;
|
|
473
|
+
}
|
|
474
|
+
loadSigned.set(a.off, a.signed);
|
|
475
|
+
m.signed = a.signed;
|
|
476
|
+
}
|
|
477
|
+
const n = bound(a.index);
|
|
478
|
+
if (n !== null) {
|
|
479
|
+
m.bounds.push(n);
|
|
480
|
+
}
|
|
481
|
+
byOff.set(a.off, m);
|
|
482
|
+
}
|
|
483
|
+
// `accesses` is non-empty (accessesByBase keys no base it recorded nothing for) and every
|
|
484
|
+
// access writes a `byOff` entry, so the assertion below is the loop's postcondition.
|
|
485
|
+
const members = [...byOff.values()].sort((x, y) => x.off - y.off) as [Member, ...Member[]];
|
|
486
|
+
// Natural C alignment must seat every member at its observed offset. An INTERIOR member's run
|
|
487
|
+
// reaches exactly the offset of the member that follows it, so the only pad this layout ever
|
|
488
|
+
// needs is the leading one and the only way to mislay a member is a span its element size does
|
|
489
|
+
// not divide.
|
|
490
|
+
let seatable = true;
|
|
491
|
+
let inBounds = true;
|
|
492
|
+
for (const [i, m] of members.entries()) {
|
|
493
|
+
const next = members[i + 1];
|
|
494
|
+
if (next === undefined) {
|
|
495
|
+
break;
|
|
496
|
+
}
|
|
497
|
+
const span = next.off - m.off;
|
|
498
|
+
if (span % m.elemSize !== 0) {
|
|
499
|
+
seatable = false;
|
|
500
|
+
} else if (m.bounds.some((n) => n > span / m.elemSize)) {
|
|
501
|
+
inBounds = false;
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
out.push({
|
|
505
|
+
base,
|
|
506
|
+
accesses,
|
|
507
|
+
members,
|
|
508
|
+
c: {
|
|
509
|
+
untypedBase: base.type.kind === 'unknown',
|
|
510
|
+
unclaimedAccesses: accesses.every(
|
|
511
|
+
(a) => a.op.attrs.fieldOff === undefined && (a.addr === null || a.addr.type.kind === 'unknown'),
|
|
512
|
+
),
|
|
513
|
+
namedGlobalBase: global.has(base) || defs.get(base)?.opcode === 'gaddr',
|
|
514
|
+
hasNonZeroMember: members.some((m) => m.off > 0),
|
|
515
|
+
offsetsInRange: members.every((m) => m.off < MAX_MEMBER_OFFSET),
|
|
516
|
+
noDirectAccess,
|
|
517
|
+
noEscape,
|
|
518
|
+
aligned,
|
|
519
|
+
consistent,
|
|
520
|
+
seatable,
|
|
521
|
+
trailingBounded: members[members.length - 1].bounds.length > 0,
|
|
522
|
+
inBounds,
|
|
523
|
+
},
|
|
524
|
+
});
|
|
525
|
+
}
|
|
526
|
+
return out;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
/** The declared field list for an admitted base: a leading `u8[N]` pad when the first member does
|
|
530
|
+
* not start at 0, then one array member per offset — interior counts forced by the member that
|
|
531
|
+
* follows, the trailing count read off its walking loop. */
|
|
532
|
+
function fieldsOf(members: Member[]): StructField[] {
|
|
533
|
+
const fields: StructField[] = [];
|
|
534
|
+
if (members[0].off > 0) {
|
|
535
|
+
fields.push({ off: 0, type: T.array(T.u(8), members[0].off), name: '_pad0' });
|
|
536
|
+
}
|
|
537
|
+
for (const [i, m] of members.entries()) {
|
|
538
|
+
const next = members[i + 1];
|
|
539
|
+
// `trailing-unbounded` is what makes the trailing member's `bounds` non-empty; the `1` is what
|
|
540
|
+
// an ABLATION of that gate declares rather than a count of nothing. Two loops walking one
|
|
541
|
+
// member to two different extents are not a contradiction the way two WIDTHS at one offset are
|
|
542
|
+
// (`member-conflict`): the member has to hold both, so the larger is the smallest count that
|
|
543
|
+
// seats them.
|
|
544
|
+
const count = next === undefined ? Math.max(1, ...m.bounds) : (next.off - m.off) / m.elemSize;
|
|
545
|
+
fields.push({
|
|
546
|
+
off: m.off,
|
|
547
|
+
type: T.array(scalarTypeForAccess(m.elemSize, m.signed), count),
|
|
548
|
+
name: `field_${m.off}`,
|
|
549
|
+
});
|
|
550
|
+
}
|
|
551
|
+
return fields;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
/** Recover a struct whose MEMBER is an array from the variable-index accesses taken off a constant
|
|
555
|
+
* byte offset of one base. Runs after array legalization and array-of-struct recovery, before
|
|
556
|
+
* struct-pointer recovery. Returns the number of bases recovered.
|
|
557
|
+
*
|
|
558
|
+
* Bases sharing a layout share one declared struct — the two ends of a copy loop are one type,
|
|
559
|
+
* and two identical declarations under different names would be an artifact of the walk order. */
|
|
560
|
+
export function recognizeMemberArrays(
|
|
561
|
+
fn: Fn,
|
|
562
|
+
gates: readonly Gate<MemberArrayCandidate>[] = MEMBER_ARRAY_GATES,
|
|
563
|
+
): number {
|
|
564
|
+
const admitted = memberArrayCandidates(fn).filter((g) => firstRejection(gates, g.c) === null);
|
|
565
|
+
if (admitted.length === 0) {
|
|
566
|
+
return 0;
|
|
567
|
+
}
|
|
568
|
+
let next = firstFreeStructIndex(fn);
|
|
569
|
+
const byLayout = new Map<string, IrType>();
|
|
570
|
+
for (const g of admitted) {
|
|
571
|
+
const fields = fieldsOf(g.members);
|
|
572
|
+
const key = JSON.stringify(fields);
|
|
573
|
+
let type = byLayout.get(key);
|
|
574
|
+
if (type === undefined) {
|
|
575
|
+
type = T.struct(`Struct${next++}`, fields);
|
|
576
|
+
byLayout.set(key, type);
|
|
577
|
+
}
|
|
578
|
+
g.base.type = T.ptr(type);
|
|
579
|
+
for (const a of g.accesses) {
|
|
580
|
+
// An ADDRESS operand, so no slot home moves with it (ir/core.ts `SlotHomes`): a home is
|
|
581
|
+
// stamped on the value a `str [sp,#k]` STORED, and what is swapped in here is the access's
|
|
582
|
+
// base pointer, which never reaches the builder under a slot key.
|
|
583
|
+
a.op.operands[0] = g.base;
|
|
584
|
+
a.op.attrs.memberOff = a.off;
|
|
585
|
+
if (!a.isLoad) {
|
|
586
|
+
// an astore carries no signedness of its own — the member's decides how the index node
|
|
587
|
+
// types, so the store spells through the same declaration the load does
|
|
588
|
+
a.op.attrs.signed = g.members.find((m) => m.off === a.off)!.signed;
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
// the now-dead `add`/`const` member addresses are reaped by the DRIVER's dce
|
|
593
|
+
return admitted.length;
|
|
594
|
+
}
|