@asmlift/core 0.5.0 → 0.7.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 +270 -171
- package/src/backend/cpp.ts +1 -0
- package/src/backend/pascal.ts +26 -12
- package/src/contracts.ts +243 -39
- package/src/declare.ts +41 -4
- package/src/frontend/mips.ts +11 -0
- package/src/frontend/ppc.ts +43 -7
- package/src/frontend/ssa.ts +404 -29
- package/src/frontend/thumb.ts +2176 -686
- package/src/ir/alias.ts +78 -0
- package/src/ir/bits.ts +75 -0
- package/src/ir/core.ts +345 -2
- package/src/ir/opcodes.ts +176 -21
- 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/advance.ts +373 -0
- package/src/l3/argbase.ts +6 -5
- package/src/l3/ast.ts +510 -59
- package/src/l3/basecse.ts +686 -78
- package/src/l3/coalesce.ts +432 -46
- package/src/l3/dce.ts +31 -9
- package/src/l3/gates.ts +96 -1
- 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 +176 -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 +114 -89
- package/src/l3/reindex.ts +722 -80
- package/src/l3/scopebase.ts +649 -220
- 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 +16 -1
- package/src/l3/typing.ts +198 -9
- package/src/l3/unmerge.ts +687 -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 +239 -16
- package/src/pipeline.ts +173 -60
- package/src/proto.ts +112 -14
- package/src/raise/arrays.ts +6 -1
- package/src/raise/const.ts +203 -3
- package/src/raise/divpow2.ts +4 -4
- package/src/raise/extscale.ts +342 -0
- package/src/raise/globalshape.ts +1058 -0
- package/src/raise/gvn.ts +33 -18
- package/src/raise/latch.ts +126 -0
- package/src/raise/magicdiv.ts +2 -2
- package/src/raise/memberarrays.ts +594 -0
- package/src/raise/narrow.ts +124 -0
- package/src/raise/narrowlocal.ts +572 -0
- package/src/raise/paramwidth.ts +201 -0
- package/src/raise/pre-recovery.ts +169 -21
- package/src/raise/recover.ts +56 -23
- package/src/raise/retsink.ts +585 -19
- package/src/raise/shortcircuit.ts +1050 -89
- package/src/raise/struct-arrays.ts +19 -2
- package/src/raise/structs.ts +34 -4
- package/src/raise/tailsink.ts +126 -0
- package/src/rank-declare.ts +256 -0
- package/src/rank-variations.ts +760 -0
- package/src/rank.ts +2122 -326
- package/src/structure/analysis.ts +1398 -150
- package/src/structure/bitfields.ts +432 -0
- package/src/structure/globalaccess.ts +300 -0
- package/src/structure/hazards.ts +411 -20
- package/src/structure/loops.ts +2 -49
- package/src/structure/namecoalesce.ts +454 -0
- package/src/structure/structure.ts +3979 -612
- package/src/structure/switch-recover.ts +710 -145
- package/src/symbols.ts +188 -6
- package/src/target.ts +495 -32
- package/src/trace.ts +112 -33
- package/src/variation-definitions.ts +1540 -0
- package/src/variation-gates.ts +89 -0
- package/src/variation-tokens.ts +355 -0
|
@@ -0,0 +1,432 @@
|
|
|
1
|
+
// asmlift structurer — BITFIELD MEMBER SPELLING off a symbol map. Precomputes, for one function,
|
|
2
|
+
// which extracts read a declared bitfield and which stores write one; structure.ts renders from
|
|
3
|
+
// the three products and never re-derives them. Everything here is a REFUSAL machine: any fact
|
|
4
|
+
// that does not hold exactly leaves the honest shift/mask spelling in place.
|
|
5
|
+
//
|
|
6
|
+
// ── BITFIELD member READS ────────────────────────────────────────────────────────────────────
|
|
7
|
+
// The `(x << a) >> b` extract of a struct global's loaded bytes IS a bitfield access when the
|
|
8
|
+
// map declares a bitfield at exactly those bits: spelled `gSym.field`, the source form, whose
|
|
9
|
+
// declared `u32 field : n` then makes C's own integer promotion reproduce the signedness every
|
|
10
|
+
// downstream operator compiled with (a 7-bit unsigned field promotes to signed int — sdiv
|
|
11
|
+
// renders `/` and recompiles to __divsi3, where the raw-shift spelling stays u32).
|
|
12
|
+
//
|
|
13
|
+
// Semantically EXACT, never approximate: the window must lie inside the loaded bytes (so the
|
|
14
|
+
// load's extension bits cannot reach it), the field's position, width and signedness must all
|
|
15
|
+
// match the extract (a logical shift is an unsigned read, an arithmetic one a signed read —
|
|
16
|
+
// a signless field never matches), and the member must be nameable at all (memberQualsAllow;
|
|
17
|
+
// the map only carries bitfield facts for little-endian ELFs — see SymbolStructField). Any
|
|
18
|
+
// mismatch keeps the honest shift spelling.
|
|
19
|
+
//
|
|
20
|
+
// Precomputed over the ops (not folded during rendering) for the load's sake: a load whose
|
|
21
|
+
// EVERY use is a spelled extract chain must not also emit its materialized `v = *(u16 *)&g;`
|
|
22
|
+
// temp — the compiler CSEs the repeated member reads back to one load, but the leftover temp
|
|
23
|
+
// would be a second one. A VOLATILE container refuses the whole fold: N member reads are N
|
|
24
|
+
// volatile accesses where the asm did one load. (Byte-level residual, differ-refereed: a load
|
|
25
|
+
// only PARTIALLY absorbed — one extract spelled, another use kept — emits both the temp and
|
|
26
|
+
// the named reads, one load more than the asm; semantics hold, the score decides.)
|
|
27
|
+
//
|
|
28
|
+
// ORDERING GATE: the named spelling replaces a
|
|
29
|
+
// REGISTER value — the bits captured at the load's program position — with a fresh memory
|
|
30
|
+
// read at each render position. Every other memory read in this file goes through the
|
|
31
|
+
// materialization model (analysis.ts) for exactly that hazard, so the fold clears the SAME
|
|
32
|
+
// bar with the SAME machinery: `emitPos` resolves where each extract actually renders
|
|
33
|
+
// (transitively through its inlining consumers — an unresolvable position refuses), and
|
|
34
|
+
// `memWriteBetween` walks every def-avoiding load→render path for a call, an opaque, or a
|
|
35
|
+
// store not provably to a DIFFERENT named global. PATH-BASED, never a linear scan over the op
|
|
36
|
+
// list: `fn.blocks` is in ADDRESS order, not topological order, so a block laid out after the
|
|
37
|
+
// render can still execute between the load and the render on the taken path.
|
|
38
|
+
import { type GlobalCell, globalCellOf, mayWriteGlobal } from '../ir/alias';
|
|
39
|
+
import { type BitsCtx, constMask, provableBits } from '../ir/bits';
|
|
40
|
+
import { Block, Fn, Op, Value } from '../ir/core';
|
|
41
|
+
import { type DeclaredField, type SymbolInfo, type SymbolStructField, declaredFields } from '../symbols';
|
|
42
|
+
|
|
43
|
+
/** The slice of structure.ts's symbol-map rendering context this fold reads. Structural on purpose:
|
|
44
|
+
* the owner of that context stays in structure.ts, and nothing here can reach the rest of it. */
|
|
45
|
+
export interface BitfieldSymCtx {
|
|
46
|
+
info(name: string): SymbolInfo | undefined;
|
|
47
|
+
fieldsOf(name: string): DeclaredField[] | null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface BitfieldDeps {
|
|
51
|
+
fn: Fn;
|
|
52
|
+
defs: Map<Value, Op>;
|
|
53
|
+
/** defs that emit as named temps at their own position (structure/analysis.ts). READ ONLY here:
|
|
54
|
+
* a materialized op is what several of the refusals below test for. */
|
|
55
|
+
materialize: Set<Op>;
|
|
56
|
+
useSitesOf: Map<Value, { op: Op }[]>;
|
|
57
|
+
opBlock: Map<Op, Block>;
|
|
58
|
+
opIndex: Map<Op, number>;
|
|
59
|
+
/** where an op's expression ultimately renders, transitively through its inlining consumers;
|
|
60
|
+
* null when there is no single such position. */
|
|
61
|
+
emitPos: (op: Op) => { blk: Block; idx: number } | null;
|
|
62
|
+
/** does any op matching `isWrite` lie on a def-avoiding path from `def` to `render`? */
|
|
63
|
+
memWriteBetween: (def: Op, render: { blk: Block; idx: number }, isWrite: (x: Op) => boolean) => boolean;
|
|
64
|
+
/** absent ⇒ no map, and then nothing here fires. */
|
|
65
|
+
sym: BitfieldSymCtx | undefined;
|
|
66
|
+
/** the map only carries bitfield facts for little-endian ELFs (see SymbolStructField). */
|
|
67
|
+
littleEndian: boolean;
|
|
68
|
+
/** `spellBitfieldMembers`, already normalized against the PROJECT map by the caller. */
|
|
69
|
+
enabled: boolean;
|
|
70
|
+
/** may a member be NAMED by an access of this direction, given its declared qualifiers? Taken as
|
|
71
|
+
* a dependency rather than duplicated: structure.ts owns the one statement of that rule, and
|
|
72
|
+
* both of its named-member spellings pass through the same predicate. */
|
|
73
|
+
memberQualsAllow: (f: SymbolStructField, containerConst: boolean | undefined, isStore: boolean) => boolean;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** What a recognized bitfield store assigns: an inserted value, or the literal 0 the asm does not
|
|
77
|
+
* carry (the ALL-ZERO form — see the note in `makeBitfieldSpelling`). */
|
|
78
|
+
export type BitfieldAssigned = { k: 'zero' } | { k: 'value'; v: Value };
|
|
79
|
+
|
|
80
|
+
export interface BitfieldSpellings {
|
|
81
|
+
/** extract op → the `gSym.field` read it spells */
|
|
82
|
+
spelling: Map<Op, { global: string; field: string }>;
|
|
83
|
+
/** store op → the `gSym.field = …` write it spells */
|
|
84
|
+
stores: Map<Op, { global: string; field: string; value: BitfieldAssigned }>;
|
|
85
|
+
/** loads whose EVERY use is a spelled extract: the fold emits no temp for these */
|
|
86
|
+
absorbed: Set<Op>;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function makeBitfieldSpelling(deps: BitfieldDeps): BitfieldSpellings {
|
|
90
|
+
const {
|
|
91
|
+
fn,
|
|
92
|
+
defs,
|
|
93
|
+
materialize,
|
|
94
|
+
useSitesOf,
|
|
95
|
+
opBlock,
|
|
96
|
+
opIndex,
|
|
97
|
+
emitPos,
|
|
98
|
+
memWriteBetween,
|
|
99
|
+
sym: symCtx,
|
|
100
|
+
littleEndian,
|
|
101
|
+
enabled: spellBitfieldMembers,
|
|
102
|
+
memberQualsAllow,
|
|
103
|
+
} = deps;
|
|
104
|
+
// the READ side: an extract op → the `gSym.field` it spells. Every rule and every refusal behind
|
|
105
|
+
// it is in this module's header.
|
|
106
|
+
const bitfieldSpelling = new Map<Op, { global: string; field: string }>();
|
|
107
|
+
// …and the WRITE side: a store the mask-and-insert idiom recognized (see the block below), with
|
|
108
|
+
// the value the source assigned. THE SECOND inhabitant of "a precomputed member spelling", which
|
|
109
|
+
// is what makes the shape shared rather than anticipated.
|
|
110
|
+
const bitfieldStore = new Map<Op, { global: string; field: string; value: BitfieldAssigned }>();
|
|
111
|
+
const absorbedLoads = new Set<Op>();
|
|
112
|
+
if (symCtx && littleEndian && spellBitfieldMembers) {
|
|
113
|
+
// the (name, byte) of a load's address when it resolves through defs alone — `gaddr` or
|
|
114
|
+
// `add(gaddr, const)`; anything else (a materialized base, a variable index) declines. THE
|
|
115
|
+
// shared L2 disjointness query (ir/alias.ts), which the materialization model consults with
|
|
116
|
+
// the same rule, so the fold and the model cannot disagree about what a store can reach.
|
|
117
|
+
const loadTargets = new Map<Op, GlobalCell>();
|
|
118
|
+
const addrOf = (v: Value, off: number): GlobalCell | null => globalCellOf(defs, v, off);
|
|
119
|
+
// A write for the fold's purposes: calls and opaques always; a store/astore unless its base
|
|
120
|
+
// resolves to a global PROVABLY different from the folded one.
|
|
121
|
+
const mayWrite = (sym: string) => mayWriteGlobal(defs, sym);
|
|
122
|
+
for (const blk of fn.blocks) {
|
|
123
|
+
for (const op of blk.ops) {
|
|
124
|
+
if ((op.opcode !== 'shr_u' && op.opcode !== 'shr_s') || op.operands.length !== 1) {
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
const b = op.attrs.imm as number | undefined;
|
|
128
|
+
const inner = defs.get(op.operands[0]);
|
|
129
|
+
if (typeof b !== 'number' || b <= 0 || b >= 32 || inner?.opcode !== 'shl' || inner.operands.length !== 1) {
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
const a = inner.attrs.imm as number | undefined;
|
|
133
|
+
if (typeof a !== 'number' || a < 0 || b < a) {
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
const w = 32 - b; // extract width
|
|
137
|
+
const lo = b - a; // low bit within the loaded value
|
|
138
|
+
const load = defs.get(inner.operands[0]);
|
|
139
|
+
if (load?.opcode !== 'load' || lo + w > (load.attrs.width as number) * 8) {
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
// a materialized shl would still emit its `v = x << a` temp reading the load — the fold
|
|
143
|
+
// would then ADD member reads on top of it; rare, refuse
|
|
144
|
+
if (materialize.has(inner)) {
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
const gb = addrOf(load.operands[0], load.attrs.off as number);
|
|
148
|
+
const si = gb ? symCtx.info(gb.name) : undefined;
|
|
149
|
+
if (!gb || si?.shape !== 'struct' || si.volatile) {
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
// where does the member read RENDER? at the extract's own position when materialized,
|
|
153
|
+
// else wherever each of its consumers ultimately renders (emitPos, transitively —
|
|
154
|
+
// unresolvable refuses); every load→render path must be write-free
|
|
155
|
+
const renders = materialize.has(op)
|
|
156
|
+
? [{ blk: opBlock.get(op)!, idx: opIndex.get(op)! }]
|
|
157
|
+
: [...new Set((useSitesOf.get(op.results[0]) ?? []).map((s) => s.op))].map((c) => emitPos(c));
|
|
158
|
+
const writes = mayWrite(gb.name);
|
|
159
|
+
if (renders.some((r) => r === null) || renders.some((r) => memWriteBetween(load, r!, writes))) {
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
const signedRead = op.opcode === 'shr_s';
|
|
163
|
+
const fld = declaredFields(si.layout)?.find(
|
|
164
|
+
(f) => f.bitWidth === w && f.offset * 8 + f.bitOffset! === gb.byte * 8 + lo && f.signed === signedRead,
|
|
165
|
+
);
|
|
166
|
+
if (fld && memberQualsAllow(fld, si.const, false)) {
|
|
167
|
+
bitfieldSpelling.set(op, { global: gb.name, field: fld.name });
|
|
168
|
+
loadTargets.set(load, gb);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
// a load is ABSORBED when every use is an shl whose every use is a spelled extract
|
|
173
|
+
for (const load of loadTargets.keys()) {
|
|
174
|
+
const shls = useSitesOf.get(load.results[0]) ?? [];
|
|
175
|
+
const absorbed =
|
|
176
|
+
shls.length > 0 &&
|
|
177
|
+
shls.every(
|
|
178
|
+
(u) =>
|
|
179
|
+
u.op.opcode === 'shl' && (useSitesOf.get(u.op.results[0]) ?? []).every((v) => bitfieldSpelling.has(v.op)),
|
|
180
|
+
);
|
|
181
|
+
if (absorbed) {
|
|
182
|
+
absorbedLoads.add(load);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// ── BITFIELD member WRITES: the mask-and-insert idiom ───────────────────────────────────
|
|
187
|
+
// `store(A, or(and(load(A), ~W), v << lo))` over a struct global's cell IS an assignment to
|
|
188
|
+
// the declared bitfield at bits W — `gSym.field = v;`, one statement where the recovered
|
|
189
|
+
// spelling is a read, a mask, a shift, an or and a store.
|
|
190
|
+
//
|
|
191
|
+
// EXACT, never approximate. The cleared bits must be exactly one declared field's window; the
|
|
192
|
+
// load must address the SAME cell at the same width; the insert must be that value shifted to
|
|
193
|
+
// the window's own position; and the load, the mask, the `and` and the `or` must each be
|
|
194
|
+
// single-use and unmaterialized, because the fold DELETES all of them — a second reader would
|
|
195
|
+
// keep the temp and the emitted C would do the work twice.
|
|
196
|
+
//
|
|
197
|
+
// THE ALL-ZERO FORM HAS NO `or`: agbcc's `expmed.c` skips the insert when the assigned value
|
|
198
|
+
// is all-zero (`:557-558`, `:606-608`), so `gSym.field = 0;` lowers to
|
|
199
|
+
// `store(A, and(load(A), ~W))` and the store's value operand IS the keep.
|
|
200
|
+
//
|
|
201
|
+
// ITS EVIDENCE IS THE MASK, NOT THE VALUE. `= 0` is also spellable raw, as
|
|
202
|
+
// `*(u8 *)&gSym = KEEP & *(u8 *)&gSym`, so keying on "the inserted value is 0" would name a
|
|
203
|
+
// member wherever the two spellings are the SAME OBJECT. Measured, pinned agbcc, `u8 a:4;
|
|
204
|
+
// u8 b:4`: the DECLARED store of the LOW nibble complements in the 32-bit domain — `~0xF` =
|
|
205
|
+
// -16, which no Thumb `mov #imm8` encodes, so `mov #0x10; neg` — where the byte-domain raw
|
|
206
|
+
// spelling narrows the same keep to one encodable `mov #0xF0`. The HIGH nibble's two
|
|
207
|
+
// spellings are byte-identical (`mov #0xf` on both sides). So the fold admits the zero form
|
|
208
|
+
// only where the keep mask is that 32-bit complement; the gate below says what each clause
|
|
209
|
+
// kills.
|
|
210
|
+
//
|
|
211
|
+
// THE MASK'S ENCODING IS NOT ITS DEFINING OP. `mov;neg` is only how the complement is built
|
|
212
|
+
// where it misses `mov #imm8`; agbcc spells the same complement for `u16 a : 12` as a pool
|
|
213
|
+
// `ldr` of `-0x1000` (compiled). A rule keyed on "defined by a `neg`" would refuse that real
|
|
214
|
+
// inhabitant, so the gate reads the mask's VALUE. The price: the Thumb frontend lowers
|
|
215
|
+
// `bic Rd,Rm` to `and(Rd, ~Rm)`, so a hand-written `mov #0xf; bic` arrives as the same IR as
|
|
216
|
+
// the accepted `mov #0x10; neg` and is admitted. agbcc emits no `bic` for this idiom
|
|
217
|
+
// (compiled: the or-form clear is `mov #0x4; neg`), and `/no-bitfield` co-enumerates the raw
|
|
218
|
+
// spelling, so that is one extra candidate for the differ, not a lost one.
|
|
219
|
+
//
|
|
220
|
+
// TARGET COUPLING, stated because the code cannot: every argument here is a THUMB encoding
|
|
221
|
+
// argument, and the fold's only target guard is `littleEndian`. That is sound only because
|
|
222
|
+
// armv4t+agbcc is the one little-endian target in `target.ts`; a second one inherits none of
|
|
223
|
+
// this reasoning.
|
|
224
|
+
//
|
|
225
|
+
// TRUNCATION is what makes an UNMASKED insert legal, and only sometimes: C truncates the
|
|
226
|
+
// assigned value to the field width, while the asm's `or` writes every bit of `v << lo` that
|
|
227
|
+
// the STORE keeps. The two agree when the field ends the stored cell — bits above it are
|
|
228
|
+
// dropped by the store either way — or when `v` provably has no more bits than the field.
|
|
229
|
+
// Anything else keeps the honest mask spelling.
|
|
230
|
+
//
|
|
231
|
+
// ORDERING is NOT this fold's to police, and the difference from the read fold above is the
|
|
232
|
+
// reason. That fold MOVES a read: its extract renders at the consumer, so a write in between
|
|
233
|
+
// changes what the extract sees. This one moves nothing — the spelling it replaces is a single
|
|
234
|
+
// statement AT THE STORE (`*(u8 *)&gS = v | *(u8 *)&gS & ~W;`), which reads the cell in exactly
|
|
235
|
+
// the position `gS.field = v` does. What keeps that read honest is the MATERIALIZATION model,
|
|
236
|
+
// and it is byte-granular where a symbol-wide alias query is not: a call, or a store this load
|
|
237
|
+
// may alias, forces the load to its own temp at its own position, and `!materialize.has(load)`
|
|
238
|
+
// below then refuses. A store to a DISJOINT byte of the same cell's symbol materializes
|
|
239
|
+
// nothing, and refusing there bought no ordering — it only spelled the same read as arithmetic.
|
|
240
|
+
|
|
241
|
+
// THE KNOWN-BITS QUESTION IS L2 AND LIVES THERE (ir/bits.ts) — this fold only supplies the
|
|
242
|
+
// one fact that layer cannot see: a bitfield READ this pass has already recognized, whose
|
|
243
|
+
// bound comes from the DECLARATION. And it supplies it signedness-first, because a signed
|
|
244
|
+
// field's read is sign-extended and carries all 32 bits however few bits the declaration
|
|
245
|
+
// allots it — bounding one by its own `bitWidth` folds `gS.dest = gS.delta` over a value whose
|
|
246
|
+
// high bits the asm's `or` writes and C's truncation does not.
|
|
247
|
+
const bits: BitsCtx = {
|
|
248
|
+
defs,
|
|
249
|
+
materialize,
|
|
250
|
+
bound: (d) => {
|
|
251
|
+
const bf = bitfieldSpelling.get(d);
|
|
252
|
+
if (!bf) {
|
|
253
|
+
return null;
|
|
254
|
+
}
|
|
255
|
+
const f = symCtx.fieldsOf(bf.global)?.find((x) => x.name === bf.field);
|
|
256
|
+
return f?.signed === false ? (f.bitWidth ?? 32) : 32;
|
|
257
|
+
},
|
|
258
|
+
};
|
|
259
|
+
const maskConst = (v: Value): number | null => constMask(bits, v);
|
|
260
|
+
/** The other operand of a 2-operand commutative op, or null when there is none — a
|
|
261
|
+
* 1-operand op carries its constant as `attrs.imm`, which is not a Value the caller can
|
|
262
|
+
* read a mask off, so the caller falls through to `attrs.imm` itself. */
|
|
263
|
+
const otherOperand = (d: Op, keep: Value): Value | null =>
|
|
264
|
+
d.operands.length === 2 ? (d.operands[0] === keep ? d.operands[1] : d.operands[0]) : null;
|
|
265
|
+
|
|
266
|
+
for (const blk of fn.blocks) {
|
|
267
|
+
for (const op of blk.ops) {
|
|
268
|
+
if (op.opcode !== 'store') {
|
|
269
|
+
continue;
|
|
270
|
+
}
|
|
271
|
+
const width = op.attrs.width as number;
|
|
272
|
+
const cell = globalCellOf(defs, op.operands[0], op.attrs.off as number);
|
|
273
|
+
const si = cell ? symCtx.info(cell.name) : undefined;
|
|
274
|
+
const valOp = defs.get(op.operands[1]);
|
|
275
|
+
// the ALL-ZERO form: no `or`, the keep `and` IS the store's value. Single-use and
|
|
276
|
+
// unmaterialized go untested here because the shared `andOp` gate tests the same op.
|
|
277
|
+
const zeroForm = valOp?.opcode === 'and';
|
|
278
|
+
if (
|
|
279
|
+
!cell ||
|
|
280
|
+
si?.shape !== 'struct' ||
|
|
281
|
+
si.volatile ||
|
|
282
|
+
(!zeroForm &&
|
|
283
|
+
(valOp?.opcode !== 'or' ||
|
|
284
|
+
valOp.operands.length !== 2 ||
|
|
285
|
+
materialize.has(valOp) ||
|
|
286
|
+
(useSitesOf.get(valOp.results[0]) ?? []).length !== 1))
|
|
287
|
+
) {
|
|
288
|
+
continue;
|
|
289
|
+
}
|
|
290
|
+
const cellBits = width * 8;
|
|
291
|
+
const cellMask = width >= 4 ? -1 : (1 << cellBits) - 1;
|
|
292
|
+
// The `or` form's two pairs are the commutativity of `|`; the zero form contributes one,
|
|
293
|
+
// with `null` for the insert the asm does not contain.
|
|
294
|
+
const pairs: readonly (readonly [Value, Value | null])[] = zeroForm
|
|
295
|
+
? [[op.operands[1], null]]
|
|
296
|
+
: [
|
|
297
|
+
[valOp!.operands[0], valOp!.operands[1]],
|
|
298
|
+
[valOp!.operands[1], valOp!.operands[0]],
|
|
299
|
+
];
|
|
300
|
+
for (const [keepV, insV] of pairs) {
|
|
301
|
+
const andOp = defs.get(keepV);
|
|
302
|
+
if (
|
|
303
|
+
andOp?.opcode !== 'and' ||
|
|
304
|
+
materialize.has(andOp) ||
|
|
305
|
+
(useSitesOf.get(andOp.results[0]) ?? []).length !== 1
|
|
306
|
+
) {
|
|
307
|
+
continue;
|
|
308
|
+
}
|
|
309
|
+
// `and` is commutative and may carry its constant as an immediate: find the operand that
|
|
310
|
+
// is the SAME cell's load, and read the mask off whatever is left.
|
|
311
|
+
const loadV = andOp.operands.find((o) => {
|
|
312
|
+
const l = defs.get(o);
|
|
313
|
+
const c = l?.opcode === 'load' ? globalCellOf(defs, l.operands[0], l.attrs.off as number) : null;
|
|
314
|
+
return c !== null && c.name === cell.name && c.byte === cell.byte && l!.attrs.width === width;
|
|
315
|
+
});
|
|
316
|
+
const load = loadV === undefined ? undefined : defs.get(loadV)!;
|
|
317
|
+
const maskV = loadV === undefined ? null : otherOperand(andOp, loadV);
|
|
318
|
+
const mask =
|
|
319
|
+
maskV !== null
|
|
320
|
+
? maskConst(maskV)
|
|
321
|
+
: typeof andOp.attrs.imm === 'number'
|
|
322
|
+
? (andOp.attrs.imm as number) | 0
|
|
323
|
+
: null;
|
|
324
|
+
if (
|
|
325
|
+
load === undefined ||
|
|
326
|
+
mask === null ||
|
|
327
|
+
materialize.has(load) ||
|
|
328
|
+
(useSitesOf.get(load.results[0]) ?? []).length !== 1
|
|
329
|
+
) {
|
|
330
|
+
continue;
|
|
331
|
+
}
|
|
332
|
+
// The cleared bits must be ONE contiguous window inside the stored cell.
|
|
333
|
+
const clear = ~mask & cellMask;
|
|
334
|
+
if (clear === 0) {
|
|
335
|
+
continue;
|
|
336
|
+
}
|
|
337
|
+
const lo = 31 - Math.clz32(clear & -clear);
|
|
338
|
+
const w = 32 - Math.clz32(clear >>> lo);
|
|
339
|
+
if ((((w >= 32 ? -1 : (1 << w) - 1) << lo) & cellMask) !== clear) {
|
|
340
|
+
continue;
|
|
341
|
+
}
|
|
342
|
+
// …AND THE STORE'S WIDTH MUST BE THE ACCESS AN ASSIGNMENT TO THOSE BITS COMPILES TO:
|
|
343
|
+
// the NARROWEST aligned 1/2/4-byte cell that contains the whole window. agbcc picks the
|
|
344
|
+
// access from the field's own BITS, not from its container's declared type — measured,
|
|
345
|
+
// pinned agbcc, one compile per row:
|
|
346
|
+
//
|
|
347
|
+
// u16 a : 2 at bit 0 → `ldrb`/`strb` (narrower than the `u16` container)
|
|
348
|
+
// u32 a : 12 at bit 0 → `ldrh`/`strh` (narrower than the `u32` container)
|
|
349
|
+
// u32 x : 8 at bit 12 → `ldr`/`str` (WIDER: bits 12-19 straddle bytes 1-2)
|
|
350
|
+
// u32 a : 17 / a : 20 → `ldr`/`str` (WIDER: no 3-byte access exists)
|
|
351
|
+
//
|
|
352
|
+
// Both directions matter and only one of them is intuitive. Without the narrowing half a
|
|
353
|
+
// `strh` over `gState.hearts` (two bits inside byte 0) spells a member from bytes that
|
|
354
|
+
// spelling cannot reproduce. Without the WIDENING half every field whose bits straddle
|
|
355
|
+
// an aligned pair — the common packed-header shape — is refused, because the compiler
|
|
356
|
+
// had no choice but a word.
|
|
357
|
+
//
|
|
358
|
+
// So the test reads the WINDOW and never the map's `size`. `size` is the field's byte
|
|
359
|
+
// SPAN (@gba-kit/debug-info emits `ceil((bitsIntoByte + bitWidth) / 8)`), which for the
|
|
360
|
+
// two widening rows is 2 and 3 — neither an access this machine has, and 3 an access no
|
|
361
|
+
// machine has. Any bound expressed in `size` is therefore wrong for a whole band of real
|
|
362
|
+
// fields however it rounds. `bitOffset`/`bitWidth` state the same fact exactly.
|
|
363
|
+
const loBit = cell.byte * 8 + lo;
|
|
364
|
+
const cellWidth = [1, 2, 4].find(
|
|
365
|
+
(n) => Math.floor(loBit / (n * 8)) === Math.floor((loBit + w - 1) / (n * 8)),
|
|
366
|
+
);
|
|
367
|
+
if (width !== cellWidth) {
|
|
368
|
+
continue;
|
|
369
|
+
}
|
|
370
|
+
// THE ZERO FORM'S EVIDENCE RULE: accept only a keep mask no RAW spelling of the same
|
|
371
|
+
// clear can produce. Three clauses, each measured with the pinned agbcc:
|
|
372
|
+
//
|
|
373
|
+
// · `mask === ~clear` — the 32-bit complement of the window. `~0xF` = -16 for a low
|
|
374
|
+
// nibble, `-0x1000` for `u16 a : 12`: neither is encodable in the byte domain, so a
|
|
375
|
+
// raw spelling narrows to a different object. A pool word like `0xFFFF00F0` clears
|
|
376
|
+
// the same nibble but zeroes bits ABOVE the cell, so it is some other function whose
|
|
377
|
+
// clear is a coincidence.
|
|
378
|
+
// · `(mask & cellMask) !== mask` — a WORD cell is refused outright. `cellMask` has bit
|
|
379
|
+
// 31 clear at widths 1-2, so `mask === ~clear` already forces an outside bit there
|
|
380
|
+
// and this can only bite at width 4. It costs nothing: compiled, `gS.a = 0;` for
|
|
381
|
+
// `u32 a : 20` and `*(s32 *)&gS &= -1048576;` are byte-identical.
|
|
382
|
+
// · `(mask & cellMask) !== 0` — a mask keeping NO bit of the cell is not a
|
|
383
|
+
// read-modify-write. agbcc compiles `g.f8 = 0;`, a field filling its own byte, to
|
|
384
|
+
// `mov #0x0; strb`, so the candidate could not reproduce its own input.
|
|
385
|
+
//
|
|
386
|
+
// The rule refuses a real population as collateral: agbcc narrows the HIGH nibble's
|
|
387
|
+
// clear to the in-cell `mov #0xf`, byte-identical to the raw spelling, so a declared
|
|
388
|
+
// store does NOT always complement in 32 bits. Only the converse is needed.
|
|
389
|
+
//
|
|
390
|
+
// NONE of this may extend to the `or` form, where the insert seated at `lo` is the
|
|
391
|
+
// evidence and agbcc does spell an end-of-cell clear with the narrow in-cell constant.
|
|
392
|
+
// `zeroForm`, the discriminant that built `pairs`, is what keeps the two apart.
|
|
393
|
+
if (zeroForm && (mask !== ~clear || (mask & cellMask) === mask || (mask & cellMask) === 0)) {
|
|
394
|
+
continue;
|
|
395
|
+
}
|
|
396
|
+
// …and the insert must be exactly that value seated at `lo`. The zero form skips it:
|
|
397
|
+
// no insert to seat, and no bit the (absent) `or` writes for C's truncation to disagree
|
|
398
|
+
// with.
|
|
399
|
+
let assigned: BitfieldAssigned = { k: 'zero' };
|
|
400
|
+
if (insV !== null) {
|
|
401
|
+
const shifted = defs.get(insV);
|
|
402
|
+
const inserted =
|
|
403
|
+
lo === 0
|
|
404
|
+
? insV
|
|
405
|
+
: shifted?.opcode === 'shl' && shifted.operands.length === 1 && shifted.attrs.imm === lo
|
|
406
|
+
? shifted.operands[0]
|
|
407
|
+
: null;
|
|
408
|
+
if (
|
|
409
|
+
inserted === null ||
|
|
410
|
+
(lo !== 0 && (materialize.has(shifted!) || (useSitesOf.get(insV) ?? []).length !== 1))
|
|
411
|
+
) {
|
|
412
|
+
continue;
|
|
413
|
+
}
|
|
414
|
+
if (lo + w !== cellBits && provableBits(bits, inserted) > w) {
|
|
415
|
+
continue; // C would truncate bits the asm's `or` writes
|
|
416
|
+
}
|
|
417
|
+
assigned = { k: 'value', v: inserted };
|
|
418
|
+
}
|
|
419
|
+
// …and a DECLARED field must occupy exactly the window.
|
|
420
|
+
const fld = symCtx
|
|
421
|
+
.fieldsOf(cell.name)
|
|
422
|
+
?.find((f) => f.bitWidth === w && f.offset * 8 + f.bitOffset! === loBit && f.signed !== undefined);
|
|
423
|
+
if (fld && memberQualsAllow(fld, si.const, true)) {
|
|
424
|
+
bitfieldStore.set(op, { global: cell.name, field: fld.name, value: assigned });
|
|
425
|
+
}
|
|
426
|
+
break;
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
return { spelling: bitfieldSpelling, stores: bitfieldStore, absorbed: absorbedLoads };
|
|
432
|
+
}
|