@asmlift/core 0.2.0 → 0.4.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 +5 -3
- package/package.json +1 -1
- package/src/backend/cfamily.ts +154 -5
- package/src/backend/cpp.ts +3 -1
- package/src/backend/pascal.ts +11 -0
- package/src/contracts.ts +37 -5
- package/src/declare.ts +251 -0
- package/src/frontend/frontend.ts +12 -2
- package/src/frontend/mips.ts +24 -23
- package/src/frontend/opaque.ts +39 -2
- package/src/frontend/ssa.ts +32 -53
- package/src/frontend/thumb.ts +420 -32
- package/src/ir/opcodes.ts +44 -0
- package/src/ir/simplify.ts +72 -0
- package/src/l3/argbase.ts +216 -0
- package/src/l3/ast.ts +126 -6
- package/src/l3/basecse.ts +3 -40
- package/src/l3/coalesce.ts +146 -0
- package/src/l3/dce.ts +2 -23
- package/src/l3/hoist.ts +65 -0
- package/src/l3/reindex.ts +7 -0
- package/src/l3/scopebase.ts +436 -0
- package/src/l3/symbol-refs.ts +61 -0
- package/src/l3/tailmerge.ts +120 -0
- package/src/l3/typing.ts +4 -0
- package/src/macros.ts +335 -0
- package/src/pattern/engine.ts +99 -6
- package/src/pipeline.ts +20 -6
- package/src/proto.ts +55 -0
- package/src/raise/divpow2.ts +226 -0
- package/src/raise/gvn.ts +141 -0
- package/src/raise/pre-recovery.ts +37 -3
- package/src/raise/recover.ts +24 -7
- package/src/raise/retsink.ts +36 -7
- package/src/raise/shortcircuit.ts +264 -22
- package/src/raise/structs.ts +12 -2
- package/src/rank.ts +370 -79
- package/src/structure/analysis.ts +42 -1
- package/src/structure/structure.ts +852 -67
- package/src/structure/switch-recover.ts +21 -3
- package/src/symbols.ts +541 -0
- package/src/target.ts +4 -2
- package/src/trace.ts +17 -2
|
@@ -37,11 +37,23 @@
|
|
|
37
37
|
// whose exit copies would clobber, switch fall-through, and mixed-entry self-loops (a guarded
|
|
38
38
|
// header also entered by a plain br).
|
|
39
39
|
import { Block, Fn, Op, Value, defOpMap, successorsOf } from '../ir/core';
|
|
40
|
-
import { type IrType, T } from '../ir/types';
|
|
41
|
-
import { BinOp, Expr, SFn, Stmt, SwitchCase, exprChildren, mapExprChildren } from '../l3/ast';
|
|
40
|
+
import { type IrType, T, scalarTypeForAccess, typeEquals } from '../ir/types';
|
|
41
|
+
import { BinOp, Expr, SFn, Stmt, SwitchCase, exprChildren, mapExprChildren, negateCond } from '../l3/ast';
|
|
42
42
|
import { exprCType, ptrElemBytes } from '../l3/typing';
|
|
43
43
|
import { returnType } from '../raise/recover';
|
|
44
44
|
import { collectStructs } from '../raise/structs';
|
|
45
|
+
import {
|
|
46
|
+
type DeclaredField,
|
|
47
|
+
type SymbolInfo,
|
|
48
|
+
type SymbolStructField,
|
|
49
|
+
arrayInnerExtents,
|
|
50
|
+
declaredFields,
|
|
51
|
+
isArrayField,
|
|
52
|
+
isBitfieldField,
|
|
53
|
+
isScalarCellSize,
|
|
54
|
+
pointeeFields,
|
|
55
|
+
scalarCellType,
|
|
56
|
+
} from '../symbols';
|
|
45
57
|
import { analyze } from './analysis';
|
|
46
58
|
import { makeLoopHazards, updateWriteSet } from './hazards';
|
|
47
59
|
import { analyzeLoops, dominators } from './loops';
|
|
@@ -59,44 +71,295 @@ import { makeSwitchRecovery } from './switch-recover';
|
|
|
59
71
|
// TODAY — carrying the struct name (resolved against SFn.structs) is the same move as width and
|
|
60
72
|
// the named follow-up; until then no backend pays a tax for the tree cast (Pascal loud-fails
|
|
61
73
|
// `field` regardless, C++ falls through its leaf hook to the shared C spelling).
|
|
74
|
+
// `&gSym`, possibly wearing the value-context integer cast the additive lowering adds
|
|
75
|
+
// (`(u32)&gSym` — see lowerDef's addr-intify): both spell the same link-time constant, so the
|
|
76
|
+
// fold rules match through the cast and every access that CAN spell a named element still does.
|
|
77
|
+
// WIDTH 32 ONLY — a NARROWING cast (`(u8)&gSym`, from a zext/sext lowering) is a different
|
|
78
|
+
// VALUE (`addr & 0xFF`), and folding through it would read the named global at a wrong address
|
|
79
|
+
// (the adversarial round's probe: `*(u8*)(u8)&gSym` must keep its truncation, never become
|
|
80
|
+
// `*(u8*)&gSym` — let alone a confidently-named `gSym.field`).
|
|
81
|
+
function addrIn(e: Expr): Extract<Expr, { k: 'addr' }> | null {
|
|
82
|
+
if (e.k === 'addr') {
|
|
83
|
+
return e;
|
|
84
|
+
}
|
|
85
|
+
if (e.k === 'cast' && e.to.kind === 'int' && e.to.width === 32 && e.e.k === 'addr') {
|
|
86
|
+
return e.e;
|
|
87
|
+
}
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
|
|
62
91
|
// If `e` is a global address `&gSym` (optionally `+ index`), return the global name and the
|
|
63
92
|
// element index (byte residual divided by the access width). `&gSym` alone → idx const 0;
|
|
64
93
|
// `&gSym + i` → idx `i / width` (exact division only — a non-multiple residual is a mid-element
|
|
65
94
|
// access this whole-global spelling can't express, so it declines to null and the caller casts).
|
|
66
95
|
function globalOf(e: Expr, width: number): { name: string; idx: Expr } | null {
|
|
67
|
-
|
|
68
|
-
|
|
96
|
+
const top = addrIn(e);
|
|
97
|
+
if (top) {
|
|
98
|
+
return { name: top.name, idx: { k: 'const', value: 0 } };
|
|
69
99
|
}
|
|
70
100
|
if (e.k === 'bin' && e.op === '+') {
|
|
71
|
-
for (const [
|
|
101
|
+
for (const [side, other] of [
|
|
72
102
|
[e.l, e.r],
|
|
73
103
|
[e.r, e.l],
|
|
74
104
|
] as const) {
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
105
|
+
const addrSide = addrIn(side);
|
|
106
|
+
if (addrSide) {
|
|
107
|
+
const idx = elementIndex(other, width);
|
|
108
|
+
return idx ? { name: addrSide.name, idx } : null;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// THE one gate on the BARE-NAME array-global spelling (`gSym[i]` rather than `((T *)&gSym)[i]`),
|
|
116
|
+
// shared by the constant-offset and variable-index access paths so the two cannot disagree.
|
|
117
|
+
// Returns the `index` node's `lead` fragment when the bare form is spellable, or null to fall
|
|
118
|
+
// through to the always-valid `&gSym` cast form.
|
|
119
|
+
//
|
|
120
|
+
// Two facts are required, not one. The element WIDTH must match, as it always has. And the RANK
|
|
121
|
+
// must be SPELLABLE, because one subscript reaches an element only on a rank-1 array: on `u16
|
|
122
|
+
// g[4][0x400]`, `g[i]` is a ROW. Against the project's own header that is usually a type error,
|
|
123
|
+
// but where the row address flows into an integer context it is merely a warning and the emitted C
|
|
124
|
+
// then addresses a different object than the asm did — silently.
|
|
125
|
+
//
|
|
126
|
+
// A rank > 1 pins the leading dimensions at 0 and puts the whole flat element index in the last
|
|
127
|
+
// subscript (`g[0][i]`) — the same address arithmetic, and the idiom decomp sources themselves use
|
|
128
|
+
// when the split is not observable in the asm either (`gBgTilemapBufs[0][…]` in kleod,
|
|
129
|
+
// `gNatureStatTable[nature][…]` in pokeemerald). A rank the map states but cannot spell (an unknown
|
|
130
|
+
// inner extent) gets no bare form at all; `((T *)&gSym)[i]` is byte-identical and valid under ANY
|
|
131
|
+
// declaration, which is why it is the safe fallback. See symbols.ts arrayInnerExtents for why an
|
|
132
|
+
// ABSENT rank is read as 1 rather than as unknown.
|
|
133
|
+
function bareArrayLead(si: SymbolInfo, width: number): { lead?: number[] } | null {
|
|
134
|
+
if (si.shape !== 'array' || si.elemSize !== width) {
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
const inner = arrayInnerExtents(si);
|
|
138
|
+
return inner === null ? null : inner.length === 0 ? {} : { lead: new Array<number>(inner.length).fill(0) };
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// A BYTE residual read as an ELEMENT index of `elemSize`-wide elements, or null when it is not one
|
|
142
|
+
// — the residual then addresses mid-element and no whole-element spelling can express it, so the
|
|
143
|
+
// caller falls through to the honest cast forms. THE one copy of the rule, indexing the
|
|
144
|
+
// `&gSym`-based array spelling: width 1 → the byte residual IS the index; wider → a constant
|
|
145
|
+
// residual must divide exactly, and a non-constant one must already be element-scaled
|
|
146
|
+
// (`i * elemSize` / `i << log2(elemSize)`), which is exactly what the asm's own index scaling
|
|
147
|
+
// produced.
|
|
148
|
+
function elementIndex(residual: Expr, elemSize: number): Expr | null {
|
|
149
|
+
if (elemSize === 1) {
|
|
150
|
+
return residual;
|
|
151
|
+
}
|
|
152
|
+
if (residual.k === 'const') {
|
|
153
|
+
return residual.value % elemSize === 0 ? { k: 'const', value: residual.value / elemSize } : null;
|
|
154
|
+
}
|
|
155
|
+
if (residual.k === 'bin' && (residual.op === '*' || residual.op === '<<')) {
|
|
156
|
+
const factor =
|
|
157
|
+
residual.op === '<<'
|
|
158
|
+
? residual.r.k === 'const'
|
|
159
|
+
? 1 << residual.r.value
|
|
160
|
+
: 0
|
|
161
|
+
: residual.r.k === 'const'
|
|
162
|
+
? residual.r.value
|
|
163
|
+
: 0;
|
|
164
|
+
if (factor === elemSize) {
|
|
165
|
+
return residual.l;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
return null;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/** The symbol-map rendering context threaded into memAccess/arrayAccess: shape facts per
|
|
172
|
+
* global name, plus a callback registering a global's env type (so the bare `gSym[i]` spelling,
|
|
173
|
+
* which must pass the stride check uncast, does). Absent ⇒ today's spellings. */
|
|
174
|
+
interface SymRenderCtx {
|
|
175
|
+
info(name: string): SymbolInfo | undefined;
|
|
176
|
+
noteGlobal(name: string, type: IrType): void;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// ── interior spelling through a POINTER-shaped global ────────────────────────────────────────
|
|
180
|
+
// A pointer global's VALUE is the address of the object the project's header says it points at.
|
|
181
|
+
// With that pointee's layout in the map, an access at a known offset is a NAMED member of it —
|
|
182
|
+
// `gPtr->member` — which is the source spelling; without it the access is byte arithmetic on the
|
|
183
|
+
// loaded cell (`((u8 *)gPtr + i)[16]`), honest but opaque. The address computed is IDENTICAL
|
|
184
|
+
// either way: `->member` adds the member's DWARF offset, which is the same constant the arithmetic
|
|
185
|
+
// added, and an index into an array member scales by the member's own element size, which the
|
|
186
|
+
// rules below require to equal the access width. Everything not provably that — a partial overlap,
|
|
187
|
+
// a width mismatch, an unnamed offset, a member whose declared signedness differs from the
|
|
188
|
+
// access's (an s8 read is ldrb+lsl+asr where u8 is ldrb alone), a missing layout — falls THROUGH
|
|
189
|
+
// to the cast forms. A member is never guessed.
|
|
190
|
+
|
|
191
|
+
/** The global named by a pointer global's VALUE as the additive lowering spells it: the bare
|
|
192
|
+
* `gPtr`, or that value wearing the byte-pointer / u32 cast that lowering adds (cast-then-add,
|
|
193
|
+
* see the `needsIntSpelling` / pointer-global arithmetic rules below). Both denote the same
|
|
194
|
+
* address and add BYTES to it, so both fold here; a cast to any other pointer type is NOT looked
|
|
195
|
+
* through — a `(u16 *)` base would re-scale everything added after it. */
|
|
196
|
+
function ptrGlobalValueName(x: Expr): string | null {
|
|
197
|
+
if (x.k === 'var') {
|
|
198
|
+
return x.name;
|
|
199
|
+
}
|
|
200
|
+
if (x.k === 'cast' && x.e.k === 'var') {
|
|
201
|
+
const t = x.to;
|
|
202
|
+
const bytePtr = t.kind === 'ptr' && t.to.kind === 'int' && t.to.width === 8;
|
|
203
|
+
return bytePtr || (t.kind === 'int' && t.width === 32) ? x.e.name : null;
|
|
204
|
+
}
|
|
205
|
+
return null;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/** A pointer global's value, the constant bytes added to it, and the at-most-one variable term. */
|
|
209
|
+
interface PtrGlobalBase {
|
|
210
|
+
name: string;
|
|
211
|
+
byte: number;
|
|
212
|
+
idx: Expr | null;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/** Decompose an access base into "the VALUE of a map-declared POINTER global + a constant byte
|
|
216
|
+
* offset + at most ONE variable term": `gPtr`, `gPtr + K`, `(u8 *)gPtr + i`, `(u8 *)gPtr + (i <<
|
|
217
|
+
* 2) + K`. Null for anything else — two variable terms, no such global, a non-`+` operator —
|
|
218
|
+
* because only a single residual can be read as one member's index. */
|
|
219
|
+
function ptrGlobalBase(e: Expr, isPtrGlobal: (n: string) => boolean): PtrGlobalBase | null {
|
|
220
|
+
let name: string | null = null;
|
|
221
|
+
let byte = 0;
|
|
222
|
+
let idx: Expr | null = null;
|
|
223
|
+
let ok = true;
|
|
224
|
+
const visit = (x: Expr): void => {
|
|
225
|
+
if (!ok) {
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
if (x.k === 'bin' && x.op === '+') {
|
|
229
|
+
visit(x.l);
|
|
230
|
+
visit(x.r);
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
const global = ptrGlobalValueName(x);
|
|
234
|
+
if (global !== null && name === null && isPtrGlobal(global)) {
|
|
235
|
+
name = global;
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
if (x.k === 'const') {
|
|
239
|
+
byte += x.value;
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
if (idx !== null) {
|
|
243
|
+
ok = false;
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
idx = x;
|
|
247
|
+
};
|
|
248
|
+
visit(e);
|
|
249
|
+
return ok && name !== null ? { name, byte, idx } : null;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/** Does a member declared at signedness `declared` read as EXACTLY the type the cast spelling this
|
|
253
|
+
* replaces would have produced — `scalarTypeForAccess(width, signed)`? That is the whole
|
|
254
|
+
* byte-exactness argument for naming a member instead of casting: same address, same read width,
|
|
255
|
+
* same C type, so every operator downstream compiles identically. A 4-byte access renders s32
|
|
256
|
+
* whatever the load said (the ISA has one word load), so only a SIGNED member may take its place;
|
|
257
|
+
* narrower accesses carry their own signedness and must match it. An undeclared signedness (the
|
|
258
|
+
* member is not a base type — a nested struct, an enum, a pointer) is never assumed to match.
|
|
259
|
+
* Compared against THE one copy of that rule (ir/types.ts) rather than restating it. */
|
|
260
|
+
function spellsAccessType(declared: boolean | undefined, width: number, signed: boolean): boolean {
|
|
261
|
+
if (declared === undefined) {
|
|
262
|
+
return false;
|
|
263
|
+
}
|
|
264
|
+
return typeEquals(T.int(width * 8, declared), scalarTypeForAccess(width, signed));
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/** May a member be NAMED by an access of this direction, given the qualifiers on its declaration?
|
|
268
|
+
* The named spelling REPLACES a cast through `(u8 *)`, which carries no qualifier at all, so a
|
|
269
|
+
* qualifier the name reintroduces changes what the compiler emits:
|
|
270
|
+
* • `volatile` makes the access observable — the load may no longer be folded or reordered,
|
|
271
|
+
* which is a different instruction sequence (measured: 6 insns where the cast form was 5);
|
|
272
|
+
* • `const` under a STORE is a hard error, where the cast form merely cast the qualifier away.
|
|
273
|
+
* Either way the honest spelling is the cast form, so the member simply is not nameable here. */
|
|
274
|
+
function memberQualsAllow(f: SymbolStructField, containerConst: boolean | undefined, isStore: boolean): boolean {
|
|
275
|
+
if (f.volatile) {
|
|
276
|
+
return false;
|
|
277
|
+
}
|
|
278
|
+
return !(isStore && (f.const || containerConst));
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// WHY THERE IS NO INDEXED `gPtr->arr[i]` SPELLING.
|
|
282
|
+
//
|
|
283
|
+
// Naming a member is only allowed where it is byte-identical to the cast form it replaces, and
|
|
284
|
+
// for the INDEXED form that was measured to be false. Against agbcc, `gPtr->arr[i]` and
|
|
285
|
+
// `((u8 *)gPtr + i)[K]` differ at EVERY nonzero K and for every width and direction — agbcc does
|
|
286
|
+
// not reassociate `(base + K) + i` into `(base + i) + K`, so it materialises the offset instead of
|
|
287
|
+
// folding it into the load (`adds r1, #16`; +2 code bytes at width 1, +4 at widths 2 and 4). At
|
|
288
|
+
// K = 0 the two still differ for widths 2 and 4, where the commutative `adds` picks a different
|
|
289
|
+
// destination register. The single agbcc case that did measure identical — width 1 at K = 0 —
|
|
290
|
+
// survives only a BARE index in a function with ONE such access: `i & 255`, `i + 1`, `i >> 2` and
|
|
291
|
+
// a second access that lets the cast side CSE its base all break it. A spelling rule decides one
|
|
292
|
+
// expression at a time and cannot see the neighbouring access that changes the answer, so there is
|
|
293
|
+
// no local gate that makes this form safe. (Both MIPS targets accept it freely, but core is
|
|
294
|
+
// target-agnostic — it cannot condition on the compiler it is emitting for.)
|
|
295
|
+
//
|
|
296
|
+
// The CONSTANT-offset form below is the opposite case, and is emitted unconditionally: measured
|
|
297
|
+
// identical on all three targets for widths 1/2/4, loads and stores, at every offset tested up to
|
|
298
|
+
// 4096 — including offsets past Thumb's immediate range, and under multi-member, across-a-call
|
|
299
|
+
// and in-a-loop shapes. It is one load whose member offset becomes the same immediate the cast
|
|
300
|
+
// form used, and unlike the indexed form it composes.
|
|
301
|
+
|
|
302
|
+
/** The pointee a global's value may be spelled through: the members the declaration synthesis
|
|
303
|
+
* DECLARES. Null when nothing may be named through it — THE shared gate (symbols.ts
|
|
304
|
+
* pointeeFields), so core never names a member that synthesis would not declare, and never sees a
|
|
305
|
+
* member synthesis drops (a union alias behind the first view at that offset). A VOLATILE pointee
|
|
306
|
+
* declines outright: every named access through it would be a volatile access where the cast form
|
|
307
|
+
* it replaces was plain. */
|
|
308
|
+
function spellablePointee(
|
|
309
|
+
name: string,
|
|
310
|
+
sym: SymRenderCtx,
|
|
311
|
+
): { fields: DeclaredField[]; const: boolean | undefined } | null {
|
|
312
|
+
const pointee = sym.info(name)?.pointee;
|
|
313
|
+
const fields = pointeeFields(pointee);
|
|
314
|
+
if (fields === null || pointee!.volatile) {
|
|
315
|
+
return null;
|
|
316
|
+
}
|
|
317
|
+
return { fields, const: pointee!.const };
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/** `gPtr->member` for an access through a pointer global's value, or null when the offset is not
|
|
321
|
+
* provably ONE member's (see the block comment above). A VARIABLE index declines whatever it
|
|
322
|
+
* lands on — the indexed form is not byte-neutral and has no spelling here. */
|
|
323
|
+
function pointeeAccess(
|
|
324
|
+
pg: PtrGlobalBase,
|
|
325
|
+
off: number,
|
|
326
|
+
width: number,
|
|
327
|
+
signed: boolean,
|
|
328
|
+
isStore: boolean,
|
|
329
|
+
sym: SymRenderCtx,
|
|
330
|
+
): Expr | null {
|
|
331
|
+
if (pg.idx !== null) {
|
|
332
|
+
return null;
|
|
333
|
+
}
|
|
334
|
+
const total = pg.byte + off;
|
|
335
|
+
// Constant offset: the member must match EXACTLY — offset, read width, and the SPELLED type
|
|
336
|
+
// (spellsAccessType). An ARRAY member is excluded whatever its size: `u8 x[1]` would match a
|
|
337
|
+
// byte access by (offset, size) and spell `->x`, which is not an lvalue of that width at all.
|
|
338
|
+
// A BITFIELD member likewise: its `size` is the byte span its bits touch, so a 7-bit field
|
|
339
|
+
// would match a plain u16 read and spell a 7-bit lvalue for a 16-bit access.
|
|
340
|
+
const p = spellablePointee(pg.name, sym);
|
|
341
|
+
const f = p?.fields.find((m) => m.offset === total && m.size === width && !isArrayField(m) && !isBitfieldField(m));
|
|
342
|
+
return p && f && spellsAccessType(f.signed, width, signed) && memberQualsAllow(f, p.const, isStore)
|
|
343
|
+
? { k: 'field', base: { k: 'var', name: pg.name }, name: f.name }
|
|
344
|
+
: null;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// The (name, byte offset) of a global access with a CONSTANT total offset — `&gSym` → off,
|
|
348
|
+
// `&gSym + K` → K + off. The exact byte is what a struct-layout field lookup needs; a variable
|
|
349
|
+
// residual returns null (no field spelling — falls through to the index/cast forms).
|
|
350
|
+
function globalConstByte(baseExpr: Expr, off: number): { name: string; byte: number } | null {
|
|
351
|
+
const top = addrIn(baseExpr);
|
|
352
|
+
if (top) {
|
|
353
|
+
return { name: top.name, byte: off };
|
|
354
|
+
}
|
|
355
|
+
if (baseExpr.k === 'bin' && baseExpr.op === '+') {
|
|
356
|
+
for (const [a, b] of [
|
|
357
|
+
[baseExpr.l, baseExpr.r],
|
|
358
|
+
[baseExpr.r, baseExpr.l],
|
|
359
|
+
] as const) {
|
|
360
|
+
const a2 = addrIn(a);
|
|
361
|
+
if (a2 && b.k === 'const') {
|
|
362
|
+
return { name: a2.name, byte: b.value + off };
|
|
100
363
|
}
|
|
101
364
|
}
|
|
102
365
|
}
|
|
@@ -111,6 +374,8 @@ function memAccess(
|
|
|
111
374
|
signed: boolean,
|
|
112
375
|
ctype: (e: Expr) => IrType | undefined,
|
|
113
376
|
scalarGlobals: Set<string>,
|
|
377
|
+
sym?: SymRenderCtx,
|
|
378
|
+
isStore = false,
|
|
114
379
|
): Expr {
|
|
115
380
|
// A deref of a global's address collapses to the bare global: `*(&gSym)` at off 0 is `gSym`;
|
|
116
381
|
// at off N the global is an array — `gSym[N/width]` (a C global name decays to a pointer, so
|
|
@@ -118,6 +383,39 @@ function memAccess(
|
|
|
118
383
|
// `*(&gSym + i)` → `gSym[i + off/width]` (byte offset `i` peeled from the tree; for a u8 global
|
|
119
384
|
// the residual IS the index). This is what makes an agbcc `.word gSym` pool access a named
|
|
120
385
|
// global read/element rather than a phantom-pointer deref.
|
|
386
|
+
// Declaration-shape spellings (symbol map): a STRUCT global's constant-offset access is the
|
|
387
|
+
// named field (`gSym.field` — the source spelling a folded literal can never match); an ARRAY
|
|
388
|
+
// global indexes its BARE name (`gSym[i]`, see below). Exact field match only (offset AND
|
|
389
|
+
// width) — anything else falls through to the honest cast forms, never a guessed field.
|
|
390
|
+
if (sym) {
|
|
391
|
+
const gb = globalConstByte(baseExpr, off);
|
|
392
|
+
const si = gb ? sym.info(gb.name) : undefined;
|
|
393
|
+
if (gb && si?.shape === 'struct') {
|
|
394
|
+
// THE shared spellability predicate (symbols.ts), the same call declare.ts gates its struct
|
|
395
|
+
// declaration on: a layout it declines whole is a layout with no nameable members, and a
|
|
396
|
+
// union alias it drops for the first view at that offset is a name no declaration carries.
|
|
397
|
+
// An ARRAY member is excluded for the same reason as in pointeeAccess: `u8 x[1]` would match
|
|
398
|
+
// a byte access by (offset, size) and spell `.x`, which is not an lvalue of that width. A
|
|
399
|
+
// BITFIELD member likewise — a plain read of its bytes is not a read of its bits (the named
|
|
400
|
+
// bitfield spelling has its own recognizer, on the extract shape: see lowerDef).
|
|
401
|
+
const fld = declaredFields(si.layout)?.find(
|
|
402
|
+
(f) => f.offset === gb.byte && f.size === width && !isArrayField(f) && !isBitfieldField(f),
|
|
403
|
+
);
|
|
404
|
+
if (fld && memberQualsAllow(fld, si.const, isStore)) {
|
|
405
|
+
return { k: 'field', base: { k: 'var', name: gb.name }, name: fld.name, dot: true };
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
// …and the same idea one indirection down: an access at a CONSTANT offset through a POINTER
|
|
409
|
+
// global's VALUE is a named member of what it points at (`gPtr->member`) when the map knows
|
|
410
|
+
// the pointee's layout — see pointeeAccess for the guards.
|
|
411
|
+
const pg = ptrGlobalBase(baseExpr, (n) => sym.info(n)?.shape === 'pointer');
|
|
412
|
+
if (pg) {
|
|
413
|
+
const spelled = pointeeAccess(pg, off, width, signed, isStore, sym);
|
|
414
|
+
if (spelled) {
|
|
415
|
+
return spelled;
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
}
|
|
121
419
|
const g = globalOf(baseExpr, width);
|
|
122
420
|
if (g) {
|
|
123
421
|
const idxVal = g.idx;
|
|
@@ -136,6 +434,15 @@ function memAccess(
|
|
|
136
434
|
: idxVal.k === 'const'
|
|
137
435
|
? { k: 'const', value: idxVal.value + off / width }
|
|
138
436
|
: { k: 'bin', op: '+', l: idxVal, r: { k: 'const', value: off / width } };
|
|
437
|
+
// ARRAY-declared global (symbol map): index the bare name — `gSym[i]`, the spelling the
|
|
438
|
+
// dogfood proved agbcc needs for ROM tables — with the element type registered in the env
|
|
439
|
+
// so the stride check passes and no cast is added. Element-width match only.
|
|
440
|
+
const siArr = sym?.info(g.name);
|
|
441
|
+
const lead = siArr === undefined ? null : bareArrayLead(siArr, width);
|
|
442
|
+
if (lead !== null) {
|
|
443
|
+
sym!.noteGlobal(g.name, T.ptr(T.int(width * 8, siArr!.elemSigned ?? false)));
|
|
444
|
+
return { k: 'index', base: { k: 'var', name: g.name }, idx, width, signed, ...lead };
|
|
445
|
+
}
|
|
139
446
|
return { k: 'index', base: { k: 'addr', name: g.name }, idx, width, signed };
|
|
140
447
|
}
|
|
141
448
|
const bt = base.type;
|
|
@@ -169,11 +476,19 @@ function arrayAccess(
|
|
|
169
476
|
elemSize: number,
|
|
170
477
|
signed: boolean,
|
|
171
478
|
ctype: (e: Expr) => IrType | undefined,
|
|
479
|
+
sym?: SymRenderCtx,
|
|
172
480
|
): Expr {
|
|
173
481
|
// A variable-index access off a global's address indexes the ADDRESS `&gSym` (the cast form
|
|
174
482
|
// `((T *)&gSym)[i]` — valid for a struct global too, unlike casting the bare value). A
|
|
175
483
|
// struct-array-of-globals (fieldOff) through `&gSym` is out of scope — fall through.
|
|
176
484
|
if (baseExpr.k === 'addr' && fieldOff === undefined) {
|
|
485
|
+
// ARRAY-declared global (symbol map): the bare-name spelling, same rule as memAccess.
|
|
486
|
+
const si = sym?.info(baseExpr.name);
|
|
487
|
+
const lead = si === undefined ? null : bareArrayLead(si, elemSize);
|
|
488
|
+
if (lead !== null) {
|
|
489
|
+
sym!.noteGlobal(baseExpr.name, T.ptr(T.int(elemSize * 8, si!.elemSigned ?? false)));
|
|
490
|
+
return { k: 'index', base: { k: 'var', name: baseExpr.name }, idx: idxExpr, width: elemSize, signed, ...lead };
|
|
491
|
+
}
|
|
177
492
|
return { k: 'index', base: baseExpr, idx: idxExpr, width: elemSize, signed };
|
|
178
493
|
}
|
|
179
494
|
const bt = base.type;
|
|
@@ -243,12 +558,11 @@ const ARITH_TO_BIN: Record<string, BinOp> = {
|
|
|
243
558
|
and: '&',
|
|
244
559
|
xor: '^',
|
|
245
560
|
shl: '<<',
|
|
246
|
-
shr_u: '
|
|
561
|
+
shr_u: '>>>', // the LOGICAL right shift; the C backend spells it `>>` over an unsigned operand
|
|
247
562
|
shr_s: '>>',
|
|
248
563
|
logic_and: '&&',
|
|
249
564
|
logic_or: '||', // short-circuit connectives (raise/shortcircuit.ts)
|
|
250
565
|
};
|
|
251
|
-
const NEGATE: Record<string, BinOp> = { '<': '>=', '>=': '<', '>': '<=', '<=': '>', '==': '!=', '!=': '==' };
|
|
252
566
|
|
|
253
567
|
// Recovered info for a self-loop header: its exit block and the per-parameter back-edge
|
|
254
568
|
// arg it feeds (the value on the header→header edge). The back-edge arg is the "next"
|
|
@@ -300,12 +614,32 @@ export interface StructureOptions {
|
|
|
300
614
|
// body). GCC freely uses `!=`; IDO prefers `==`/`<`. A per-compiler DATA lever, not an `arch ==`
|
|
301
615
|
// branch — default true (permissive; the decline path keeps it sound either way).
|
|
302
616
|
switchAllowsNeqCase?: boolean;
|
|
617
|
+
// Anchor a constant merge copy at its const op's ORIGINAL position instead of at the CFG edge:
|
|
618
|
+
// `movs r9, #0` at entry ahead of a single-armed overwrite emits as a pre-initialization above
|
|
619
|
+
// the `if`, not as its else-arm. A differ-refereed candidate axis (rank.ts `/defsite`), never a
|
|
620
|
+
// default — see the refusal conditions where it is computed.
|
|
621
|
+
anchorConstCopies?: boolean;
|
|
622
|
+
// HARDWARE fact from TargetDescription.capabilities.endianness, threaded by structureOptionsFor:
|
|
623
|
+
// the bitfield extract recognizer solves an LSB-first equation, so it only runs on little-endian
|
|
624
|
+
// data. The provider already refuses to EMIT bitfield facts for a big-endian ELF; this is the
|
|
625
|
+
// same boundary enforced on core's side, against a hand-built map that never went through it.
|
|
626
|
+
littleEndian?: boolean;
|
|
627
|
+
// Spell `(x << a) >> b` extracts of a struct global as the map's named bitfield member. On by
|
|
628
|
+
// default; rank.ts enumerates the OFF spelling as the `/no-bitfield` axis, because the named
|
|
629
|
+
// read recompiles at the DECLARATION's access width — where that diverges from the asm's load
|
|
630
|
+
// width the honest shift spelling is the one that matches, and the differ referees.
|
|
631
|
+
spellBitfieldMembers?: boolean;
|
|
303
632
|
// How an unresolvable VALUE degrades (a live `opaque`, an unlowered transient op, a dropped def):
|
|
304
633
|
// "strict" (default) — the `"?"` sentinel, tripping assertResolved at the boundary (loud in
|
|
305
634
|
// the PROCESS);
|
|
306
635
|
// "annotate" — a `marker` node that spells as the undefined ASMLIFT_ERROR(...) symbol (loud in
|
|
307
636
|
// the ARTIFACT: the function emits complete, but cannot compile un-acknowledged).
|
|
308
637
|
onGap?: 'strict' | 'annotate';
|
|
638
|
+
/** NAME-keyed project symbol facts (symbols.ts `symbolsByName`) — drives the byte-sensitive
|
|
639
|
+
* declaration-shape spellings: `shape:'array'` forces the aggregate classification and the
|
|
640
|
+
* bare `gSym[i]` form; `shape:'struct'`+layout spells interiors as `gSym.field`. Absent (or
|
|
641
|
+
* a symbol not in the map) ⇒ today's usage-inferred behavior, byte-identical. */
|
|
642
|
+
symbols?: Map<string, SymbolInfo>;
|
|
309
643
|
}
|
|
310
644
|
|
|
311
645
|
export function structure(fn: Fn, opts: StructureOptions = {}): SFn {
|
|
@@ -315,7 +649,11 @@ export function structure(fn: Fn, opts: StructureOptions = {}): SFn {
|
|
|
315
649
|
preserveDivergentBranchSense = true,
|
|
316
650
|
orderArgCopiesByComputation = true,
|
|
317
651
|
switchAllowsNeqCase = true,
|
|
652
|
+
anchorConstCopies = false,
|
|
653
|
+
littleEndian = true,
|
|
654
|
+
spellBitfieldMembers = true,
|
|
318
655
|
onGap = 'strict',
|
|
656
|
+
symbols,
|
|
319
657
|
} = opts;
|
|
320
658
|
const defs = defOpMap(fn);
|
|
321
659
|
const preds = predecessorBlocks(fn);
|
|
@@ -323,7 +661,10 @@ export function structure(fn: Fn, opts: StructureOptions = {}): SFn {
|
|
|
323
661
|
const dom = dominators(fn);
|
|
324
662
|
|
|
325
663
|
// ── analysis phase (structure/analysis.ts): use registry, liveness, materialization ──
|
|
326
|
-
const { useSitesOf, opIndex, opBlock, liveIn, materialize, reachFrom } = analyze(
|
|
664
|
+
const { useSitesOf, opIndex, opBlock, liveIn, materialize, reachFrom, emitPos, memWriteBetween } = analyze(
|
|
665
|
+
fn,
|
|
666
|
+
returnsVoid,
|
|
667
|
+
);
|
|
327
668
|
|
|
328
669
|
// SCALAR-vs-AGGREGATE globals: a `gaddr` symbol accessed EXCLUSIVELY at offset 0 is a scalar
|
|
329
670
|
// global → the bare name `gSym` (byte-exact, matches the source). A symbol accessed at any
|
|
@@ -347,14 +688,16 @@ export function structure(fn: Fn, opts: StructureOptions = {}): SFn {
|
|
|
347
688
|
if (s) {
|
|
348
689
|
(offsets.get(s) ?? offsets.set(s, new Set()).get(s)!).add(op.attrs.off as number);
|
|
349
690
|
}
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
691
|
+
} else if (op.opcode === 'add' || op.opcode === 'sub') {
|
|
692
|
+
// ANY arithmetic on the symbol's address is interior addressing ⇒ aggregate — even when
|
|
693
|
+
// the sum only reaches memory through a copy/phi (a pointer-walk loop `p = &g + 2;
|
|
694
|
+
// do { *p++ … }` never makes the add a DIRECT load/store base, which is all the old
|
|
695
|
+
// check saw; the symbol then classified scalar and emitted the bare `g = 0` spelling,
|
|
696
|
+
// which a project declaring `extern u16 g[]` rejects as an incomplete-type assignment).
|
|
697
|
+
for (const o of op.operands) {
|
|
698
|
+
const s2 = gaddrSym(o);
|
|
699
|
+
if (s2) {
|
|
700
|
+
bumpAgg(s2);
|
|
358
701
|
}
|
|
359
702
|
}
|
|
360
703
|
} else if (op.opcode === 'aload' || op.opcode === 'astore') {
|
|
@@ -370,8 +713,38 @@ export function structure(fn: Fn, opts: StructureOptions = {}): SFn {
|
|
|
370
713
|
scalarGlobals.add(sym);
|
|
371
714
|
}
|
|
372
715
|
}
|
|
716
|
+
// Declaration-shape OVERRIDE (symbol map): a project-declared array/struct global is an
|
|
717
|
+
// AGGREGATE whatever the usage inference saw — a lone off-0 access to `extern u16 tbl[]`
|
|
718
|
+
// must still spell through the aggregate/array forms, never the bare scalar `tbl`.
|
|
719
|
+
if (symbols) {
|
|
720
|
+
for (const [n, si] of symbols) {
|
|
721
|
+
if (si.shape === 'array' || si.shape === 'struct') {
|
|
722
|
+
scalarGlobals.delete(n);
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
}
|
|
373
726
|
}
|
|
374
727
|
|
|
728
|
+
// Symbol-map rendering context (memAccess/arrayAccess): shape lookups + the env registry for
|
|
729
|
+
// array-shaped globals actually referenced (they surface as SFn.globals — typed, undeclared).
|
|
730
|
+
const shapedGlobalTypes = new Map<string, IrType>();
|
|
731
|
+
const symCtx: SymRenderCtx | undefined = symbols
|
|
732
|
+
? { info: (n) => symbols.get(n), noteGlobal: (n, t) => shapedGlobalTypes.set(n, t) }
|
|
733
|
+
: undefined;
|
|
734
|
+
|
|
735
|
+
/** A bare `gSym` naming a map-declared POINTER global — the VALUE of a pointer cell. Load,
|
|
736
|
+
* store and compare of that 4-byte cell are identical for any object-pointer type, so the
|
|
737
|
+
* declared pointee never matters to THEM; arithmetic on the loaded value is the opposite case,
|
|
738
|
+
* where the pointee's size scales what is added and every stride must therefore be made
|
|
739
|
+
* explicit (`(u8 *)gPtr + K`). `ctype` cannot see any of this: it types only params/locals, so
|
|
740
|
+
* a pointer global renders `undefined` there. */
|
|
741
|
+
const isPtrGlobal = (x: Expr): boolean => x.k === 'var' && symCtx?.info(x.name)?.shape === 'pointer';
|
|
742
|
+
|
|
743
|
+
/** Operands `-`/`~` cannot take as spelled: a rendered pointer, a bare `&gSym`, a pointer
|
|
744
|
+
* global's value. All three are ill-formed C under a unary arithmetic operator — the asm did
|
|
745
|
+
* 32-bit integer math on the address, so that is what gets spelled. */
|
|
746
|
+
const needsIntSpelling = (x: Expr): boolean => ctype(x)?.kind === 'ptr' || x.k === 'addr' || isPtrGlobal(x);
|
|
747
|
+
|
|
375
748
|
// --- loop discovery (loops.ts): natural loops via dominator back-edges + the nesting forest ---
|
|
376
749
|
const forest = analyzeLoops(fn, dom);
|
|
377
750
|
|
|
@@ -580,6 +953,50 @@ export function structure(fn: Fn, opts: StructureOptions = {}): SFn {
|
|
|
580
953
|
// The C static type of a rendered expression, over the declared variable types — what decides
|
|
581
954
|
// whether a memory access's base may be dereferenced as spelled (memAccess/arrayAccess).
|
|
582
955
|
const ctype = (e0: Expr): IrType | undefined => exprCType(e0, (n) => varType.get(n));
|
|
956
|
+
|
|
957
|
+
/** `&gSym` assigned to a `T *` local: the address of an AGGREGATE is not a pointer to its
|
|
958
|
+
* element. `&gArr` is `T (*)[n]`, `&gStruct` is `struct S *`, and neither is assignable to
|
|
959
|
+
* `T *` — yet the IR's `gaddr` value legitimately has type `T *`, because that is what the asm
|
|
960
|
+
* loaded. The bare spelling therefore states a type the project's own header contradicts.
|
|
961
|
+
*
|
|
962
|
+
* It survived because agbcc only WARNS ("assignment from incompatible pointer type") and
|
|
963
|
+
* computes the right address anyway. That leniency is not something to rely on: the Klonoa
|
|
964
|
+
* project's own build template treats these as fatal, so the row's emitted C does not build
|
|
965
|
+
* where its author would put it. The cast is the always-valid spelling — the same fallback
|
|
966
|
+
* `bareArrayLead` documents for the indexed form — and it is byte-identical (measured on
|
|
967
|
+
* kleod:UpdateHUDCounterDisplay: 81 with and without).
|
|
968
|
+
*
|
|
969
|
+
* The test is whether `&gSym`'s rendered type PROVABLY equals the destination's, not whether the
|
|
970
|
+
* symbol looks like an aggregate. A shape enumeration got this wrong three ways, each a real
|
|
971
|
+
* miss: `shape:'pointer'` declares a pointer cell (`void *gSym`, or `struct Tag *gSym` when the
|
|
972
|
+
* pointee has a declarable layout), so `&gSym` is a pointer-to-pointer either way; a `shape:'scalar'`
|
|
973
|
+
* whose width differs from the destination's pointee gives `s32 *` for a `u16 *` slot; and a
|
|
974
|
+
* NAME-ONLY symbol is synthesized as `extern u32 gSym;` (declare.ts), which is `u32 *` — not the
|
|
975
|
+
* `T *` the older comment here claimed. So the default is to CAST, and the cast is omitted only
|
|
976
|
+
* where the declared cell type is known and matches exactly. Byte-identical either way, so the
|
|
977
|
+
* cost of casting one time too many is a redundant `(T *)`, never a wrong address. */
|
|
978
|
+
const castAggregateAddr = (name: string, value: Expr): Expr => {
|
|
979
|
+
const t = varType.get(name);
|
|
980
|
+
if (t?.kind !== 'ptr' || value.k !== 'addr') {
|
|
981
|
+
return value;
|
|
982
|
+
}
|
|
983
|
+
// The only provably-redundant case: a NON-VOLATILE scalar cell whose DECLARED type is the
|
|
984
|
+
// destination's pointee, where `&gSym` already denotes exactly `T *`.
|
|
985
|
+
//
|
|
986
|
+
// `scalarCellType` and not `scalarTypeForAccess`: the latter answers what an ACCESS of that
|
|
987
|
+
// width reads and collapses every 4-byte access to `s32`, so it called a `u32` cell equal to an
|
|
988
|
+
// `s32 *` destination and let the incompatible assignment through. And a `volatile` cell makes
|
|
989
|
+
// `&gSym` a `volatile T *`, so omitting the cast would DISCARD the qualifier — the same class of
|
|
990
|
+
// fatal-under-a-strict-build defect this rule exists to remove.
|
|
991
|
+
const si = symCtx?.info(value.name);
|
|
992
|
+
if (si?.shape === 'scalar' && !si.volatile && isScalarCellSize(si.size)) {
|
|
993
|
+
if (typeEquals(scalarCellType(si.size, si.signed), t.to)) {
|
|
994
|
+
return value;
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
return { k: 'cast', to: t, e: value };
|
|
998
|
+
};
|
|
999
|
+
|
|
583
1000
|
let fresh = 0;
|
|
584
1001
|
// Materialized defs are named FIRST: the temp is the register the compiler held the
|
|
585
1002
|
// value in, so downstream coalescing (loop inits, merge params) may adopt it — subject to the
|
|
@@ -786,11 +1203,270 @@ export function structure(fn: Fn, opts: StructureOptions = {}): SFn {
|
|
|
786
1203
|
}
|
|
787
1204
|
}
|
|
788
1205
|
|
|
789
|
-
//
|
|
790
|
-
//
|
|
791
|
-
//
|
|
792
|
-
|
|
793
|
-
|
|
1206
|
+
// ── def-site anchoring of constant merge copies (anchorConstCopies) ──────────────────────────
|
|
1207
|
+
// An edge copy `v = K` places the constant where the EDGE is, but the asm often materialized K
|
|
1208
|
+
// earlier: `movs r9, #0` at entry ahead of a single-armed overwrite, `movs r5, #1` at the top
|
|
1209
|
+
// of an arm ahead of a nested if. Anchoring the copy at the const op's own program position
|
|
1210
|
+
// reproduces that placement — the write is emitted as a statement there (sideEffects reads
|
|
1211
|
+
// `anchoredAt`) and the edge copies it replaces are suppressed (argAssignsFor reads
|
|
1212
|
+
// `suppressedArgs`). Where the surviving arm then empties, mkIf's empty-then peephole yields
|
|
1213
|
+
// the single-armed positive `if` the source wrote.
|
|
1214
|
+
//
|
|
1215
|
+
// REFUSAL CONDITIONS — each keeps the edge placement, never producing a different write:
|
|
1216
|
+
// - the arg is not an UNNAMED `const` op (only a rematerializable constant carries
|
|
1217
|
+
// unambiguous placement evidence; a named value's position is its materialized def's);
|
|
1218
|
+
// - the merge is a loop header (loop copies have their own placement discipline);
|
|
1219
|
+
// - the const's block does not dominate every edge source passing it (the anchored write
|
|
1220
|
+
// must precede the edge on every path);
|
|
1221
|
+
// - the const's block or any edge source sits inside ANY loop. Block-level dominance does
|
|
1222
|
+
// not give per-ITERATION precedence — a path may pass the def in iteration 1 and take the
|
|
1223
|
+
// suppressed edge in iteration 2 with the variable overwritten in between, the /preinit
|
|
1224
|
+
// sticky-arm failure class (PR #13) — so in-loop shapes are declined outright;
|
|
1225
|
+
// - the merge variable names any OTHER SSA value (a shared name has readers and writers
|
|
1226
|
+
// between the def site and the edge that edge placement respects and anchoring would not);
|
|
1227
|
+
// - another anchored const of the same variable lies on a path from this one to this one's
|
|
1228
|
+
// edge (the later write would clobber this arg's value; both stay at their edges instead).
|
|
1229
|
+
const anchoredAt = new Map<Op, { name: string; arg: Value }[]>();
|
|
1230
|
+
const suppressedArgs = new Map<object, Set<number>>();
|
|
1231
|
+
if (anchorConstCopies) {
|
|
1232
|
+
const nameCount = new Map<string, number>();
|
|
1233
|
+
for (const n of varName.values()) {
|
|
1234
|
+
nameCount.set(n, (nameCount.get(n) ?? 0) + 1);
|
|
1235
|
+
}
|
|
1236
|
+
const inLoop = (b: Block): boolean => {
|
|
1237
|
+
for (const nl of forest.byHeader.values()) {
|
|
1238
|
+
if (nl.body.has(b)) {
|
|
1239
|
+
return true;
|
|
1240
|
+
}
|
|
1241
|
+
}
|
|
1242
|
+
return false;
|
|
1243
|
+
};
|
|
1244
|
+
// conservative "a write in `a` may execute between one in `b` and `b`'s terminator": same
|
|
1245
|
+
// block counts (op order refined by the caller where it matters), else CFG reachability
|
|
1246
|
+
const mayFollow = (a: Block, b: Block): boolean => a === b || reachFrom(a).has(b);
|
|
1247
|
+
for (const M of fn.blocks) {
|
|
1248
|
+
if (M === entry || M.params.length === 0 || forest.byHeader.has(M)) {
|
|
1249
|
+
continue;
|
|
1250
|
+
}
|
|
1251
|
+
M.params.forEach((p, i) => {
|
|
1252
|
+
const name = varName.get(p)!;
|
|
1253
|
+
if (nameCount.get(name) !== 1) {
|
|
1254
|
+
return;
|
|
1255
|
+
}
|
|
1256
|
+
// every in-edge record into M, grouped by the SSA value it passes for param i
|
|
1257
|
+
const groups = new Map<Value, { rec: { block: Block; args: Value[] }; src: Block }[]>();
|
|
1258
|
+
for (const pr of new Set(preds.get(M) ?? [])) {
|
|
1259
|
+
for (const s of pr.ops[pr.ops.length - 1].successors) {
|
|
1260
|
+
if (s.block === M) {
|
|
1261
|
+
const g = groups.get(s.args[i]);
|
|
1262
|
+
if (g) {
|
|
1263
|
+
g.push({ rec: s, src: pr });
|
|
1264
|
+
} else {
|
|
1265
|
+
groups.set(s.args[i], [{ rec: s, src: pr }]);
|
|
1266
|
+
}
|
|
1267
|
+
}
|
|
1268
|
+
}
|
|
1269
|
+
}
|
|
1270
|
+
const candidates: { arg: Value; def: Op; defBlock: Block; edges: { rec: object; src: Block }[] }[] = [];
|
|
1271
|
+
for (const [arg, edges] of groups) {
|
|
1272
|
+
const def = defs.get(arg);
|
|
1273
|
+
if (!def || def.opcode !== 'const' || varName.has(arg)) {
|
|
1274
|
+
continue;
|
|
1275
|
+
}
|
|
1276
|
+
const defBlock = opBlock.get(def)!;
|
|
1277
|
+
if (inLoop(defBlock) || edges.some(({ src }) => inLoop(src))) {
|
|
1278
|
+
continue;
|
|
1279
|
+
}
|
|
1280
|
+
if (edges.some(({ src }) => !dom.get(src)!.has(defBlock))) {
|
|
1281
|
+
continue;
|
|
1282
|
+
}
|
|
1283
|
+
candidates.push({ arg, def, defBlock, edges });
|
|
1284
|
+
}
|
|
1285
|
+
// pairwise clobber check: candidate `c` is unsafe when another candidate's write can lie
|
|
1286
|
+
// between c's def and one of c's edges (def_c → def_o → edge_c); both then keep their edges
|
|
1287
|
+
const safe = candidates.filter((c) =>
|
|
1288
|
+
candidates.every((o) => {
|
|
1289
|
+
if (o === c) {
|
|
1290
|
+
return true;
|
|
1291
|
+
}
|
|
1292
|
+
const oAfterC =
|
|
1293
|
+
c.defBlock === o.defBlock ? opIndex.get(o.def)! > opIndex.get(c.def)! : mayFollow(c.defBlock, o.defBlock);
|
|
1294
|
+
return !(oAfterC && c.edges.some(({ src }) => mayFollow(o.defBlock, src)));
|
|
1295
|
+
}),
|
|
1296
|
+
);
|
|
1297
|
+
for (const c of safe) {
|
|
1298
|
+
const at = anchoredAt.get(c.def);
|
|
1299
|
+
if (at) {
|
|
1300
|
+
at.push({ name, arg: c.arg });
|
|
1301
|
+
} else {
|
|
1302
|
+
anchoredAt.set(c.def, [{ name, arg: c.arg }]);
|
|
1303
|
+
}
|
|
1304
|
+
for (const { rec } of c.edges) {
|
|
1305
|
+
const sup = suppressedArgs.get(rec);
|
|
1306
|
+
if (sup) {
|
|
1307
|
+
sup.add(i);
|
|
1308
|
+
} else {
|
|
1309
|
+
suppressedArgs.set(rec, new Set([i]));
|
|
1310
|
+
}
|
|
1311
|
+
}
|
|
1312
|
+
}
|
|
1313
|
+
});
|
|
1314
|
+
}
|
|
1315
|
+
}
|
|
1316
|
+
|
|
1317
|
+
// ── BITFIELD member reads (symbol map) ──────────────────────────────────────────────────────
|
|
1318
|
+
// The `(x << a) >> b` extract of a struct global's loaded bytes IS a bitfield access when the
|
|
1319
|
+
// map declares a bitfield at exactly those bits: spelled `gSym.field`, the source form, whose
|
|
1320
|
+
// declared `u32 field : n` then makes C's own integer promotion reproduce the signedness every
|
|
1321
|
+
// downstream operator compiled with (a 7-bit unsigned field promotes to signed int — sdiv
|
|
1322
|
+
// renders `/` and recompiles to __divsi3, where the raw-shift spelling stays u32).
|
|
1323
|
+
//
|
|
1324
|
+
// Semantically EXACT, never approximate: the window must lie inside the loaded bytes (so the
|
|
1325
|
+
// load's extension bits cannot reach it), the field's position, width and signedness must all
|
|
1326
|
+
// match the extract (a logical shift is an unsigned read, an arithmetic one a signed read —
|
|
1327
|
+
// a signless field never matches), and the member must be nameable at all (memberQualsAllow;
|
|
1328
|
+
// the map only carries bitfield facts for little-endian ELFs — see SymbolStructField). Any
|
|
1329
|
+
// mismatch keeps the honest shift spelling.
|
|
1330
|
+
//
|
|
1331
|
+
// Precomputed over the ops (not folded during rendering) for the load's sake: a load whose
|
|
1332
|
+
// EVERY use is a spelled extract chain must not also emit its materialized `v = *(u16 *)&g;`
|
|
1333
|
+
// temp — the compiler CSEs the repeated member reads back to one load, but the leftover temp
|
|
1334
|
+
// would be a second one. A VOLATILE container refuses the whole fold: N member reads are N
|
|
1335
|
+
// volatile accesses where the asm did one load. (Byte-level residual, differ-refereed: a load
|
|
1336
|
+
// only PARTIALLY absorbed — one extract spelled, another use kept — emits both the temp and
|
|
1337
|
+
// the named reads, one load more than the asm; semantics hold, the score decides.)
|
|
1338
|
+
//
|
|
1339
|
+
// ORDERING GATE (adversarial round, CRITICAL 1 — twice): the named spelling replaces a
|
|
1340
|
+
// REGISTER value — the bits captured at the load's program position — with a fresh memory
|
|
1341
|
+
// read at each render position. Every other memory read in this file goes through the
|
|
1342
|
+
// materialization model (analysis.ts) for exactly that hazard, so the fold clears the SAME
|
|
1343
|
+
// bar with the SAME machinery: `emitPos` resolves where each extract actually renders
|
|
1344
|
+
// (transitively through its inlining consumers — an unresolvable position refuses), and
|
|
1345
|
+
// `memWriteBetween` walks every def-avoiding load→render path for a call, an opaque, or a
|
|
1346
|
+
// store not provably to a DIFFERENT named global. Path-based on purpose: the second audit
|
|
1347
|
+
// pass broke the first fix's linear-position scan with a block laid out AFTER the render in
|
|
1348
|
+
// address order but executing between load and render on the taken path — fn.blocks order is
|
|
1349
|
+
// address order, not topological order.
|
|
1350
|
+
const bitfieldSpelling = new Map<Op, { global: string; field: string }>();
|
|
1351
|
+
const absorbedLoads = new Set<Op>();
|
|
1352
|
+
if (symCtx && littleEndian && spellBitfieldMembers) {
|
|
1353
|
+
// the (name, byte) of a load's address when it resolves through defs alone — `gaddr` or
|
|
1354
|
+
// `add(gaddr, const)`; anything else (a materialized base, a variable index) declines
|
|
1355
|
+
const loadTargets = new Map<Op, { name: string; byte: number }>();
|
|
1356
|
+
const addrOf = (v: Value, off: number): { name: string; byte: number } | null => {
|
|
1357
|
+
const d0 = defs.get(v);
|
|
1358
|
+
if (d0?.opcode === 'gaddr') {
|
|
1359
|
+
return { name: d0.attrs.sym as string, byte: off };
|
|
1360
|
+
}
|
|
1361
|
+
if (d0?.opcode === 'add' && d0.operands.length === 2) {
|
|
1362
|
+
for (const [x, y] of [
|
|
1363
|
+
[d0.operands[0], d0.operands[1]],
|
|
1364
|
+
[d0.operands[1], d0.operands[0]],
|
|
1365
|
+
] as const) {
|
|
1366
|
+
const g0 = defs.get(x);
|
|
1367
|
+
const c0 = defs.get(y);
|
|
1368
|
+
if (g0?.opcode === 'gaddr' && c0?.opcode === 'const') {
|
|
1369
|
+
return { name: g0.attrs.sym as string, byte: (c0.attrs.value as number) + off };
|
|
1370
|
+
}
|
|
1371
|
+
}
|
|
1372
|
+
}
|
|
1373
|
+
return null;
|
|
1374
|
+
};
|
|
1375
|
+
// A write for the fold's purposes: calls and opaques always; a store/astore unless its base
|
|
1376
|
+
// resolves to a global PROVABLY different from the folded one. (Name comparison suffices:
|
|
1377
|
+
// the pool promotion picks one canonical name per address, so one cell cannot appear under
|
|
1378
|
+
// two names within a function.)
|
|
1379
|
+
const mayWrite =
|
|
1380
|
+
(sym: string) =>
|
|
1381
|
+
(x: Op): boolean => {
|
|
1382
|
+
if (x.opcode === 'call' || x.opcode === 'opaque') {
|
|
1383
|
+
return true;
|
|
1384
|
+
}
|
|
1385
|
+
if (x.opcode !== 'store' && x.opcode !== 'astore') {
|
|
1386
|
+
return false;
|
|
1387
|
+
}
|
|
1388
|
+
const t = addrOf(x.operands[0], 0);
|
|
1389
|
+
return !(t && t.name !== sym);
|
|
1390
|
+
};
|
|
1391
|
+
for (const blk of fn.blocks) {
|
|
1392
|
+
for (const op of blk.ops) {
|
|
1393
|
+
if ((op.opcode !== 'shr_u' && op.opcode !== 'shr_s') || op.operands.length !== 1) {
|
|
1394
|
+
continue;
|
|
1395
|
+
}
|
|
1396
|
+
const b = op.attrs.imm as number | undefined;
|
|
1397
|
+
const inner = defs.get(op.operands[0]);
|
|
1398
|
+
if (typeof b !== 'number' || b <= 0 || b >= 32 || inner?.opcode !== 'shl' || inner.operands.length !== 1) {
|
|
1399
|
+
continue;
|
|
1400
|
+
}
|
|
1401
|
+
const a = inner.attrs.imm as number | undefined;
|
|
1402
|
+
if (typeof a !== 'number' || a < 0 || b < a) {
|
|
1403
|
+
continue;
|
|
1404
|
+
}
|
|
1405
|
+
const w = 32 - b; // extract width
|
|
1406
|
+
const lo = b - a; // low bit within the loaded value
|
|
1407
|
+
const load = defs.get(inner.operands[0]);
|
|
1408
|
+
if (load?.opcode !== 'load' || lo + w > (load.attrs.width as number) * 8) {
|
|
1409
|
+
continue;
|
|
1410
|
+
}
|
|
1411
|
+
// a materialized shl would still emit its `v = x << a` temp reading the load — the fold
|
|
1412
|
+
// would then ADD member reads on top of it; rare, refuse
|
|
1413
|
+
if (materialize.has(inner)) {
|
|
1414
|
+
continue;
|
|
1415
|
+
}
|
|
1416
|
+
const gb = addrOf(load.operands[0], load.attrs.off as number);
|
|
1417
|
+
const si = gb ? symCtx.info(gb.name) : undefined;
|
|
1418
|
+
if (!gb || si?.shape !== 'struct' || si.volatile) {
|
|
1419
|
+
continue;
|
|
1420
|
+
}
|
|
1421
|
+
// where does the member read RENDER? at the extract's own position when materialized,
|
|
1422
|
+
// else wherever each of its consumers ultimately renders (emitPos, transitively —
|
|
1423
|
+
// unresolvable refuses); every load→render path must be write-free
|
|
1424
|
+
const renders = materialize.has(op)
|
|
1425
|
+
? [{ blk: opBlock.get(op)!, idx: opIndex.get(op)! }]
|
|
1426
|
+
: [...new Set((useSitesOf.get(op.results[0]) ?? []).map((s) => s.op))].map((c) => emitPos(c));
|
|
1427
|
+
const writes = mayWrite(gb.name);
|
|
1428
|
+
if (renders.some((r) => r === null) || renders.some((r) => memWriteBetween(load, r!, writes))) {
|
|
1429
|
+
continue;
|
|
1430
|
+
}
|
|
1431
|
+
const signedRead = op.opcode === 'shr_s';
|
|
1432
|
+
const fld = declaredFields(si.layout)?.find(
|
|
1433
|
+
(f) => f.bitWidth === w && f.offset * 8 + f.bitOffset! === gb.byte * 8 + lo && f.signed === signedRead,
|
|
1434
|
+
);
|
|
1435
|
+
if (fld && memberQualsAllow(fld, si.const, false)) {
|
|
1436
|
+
bitfieldSpelling.set(op, { global: gb.name, field: fld.name });
|
|
1437
|
+
loadTargets.set(load, gb);
|
|
1438
|
+
}
|
|
1439
|
+
}
|
|
1440
|
+
}
|
|
1441
|
+
// a load is ABSORBED when every use is an shl whose every use is a spelled extract
|
|
1442
|
+
for (const load of loadTargets.keys()) {
|
|
1443
|
+
const shls = useSitesOf.get(load.results[0]) ?? [];
|
|
1444
|
+
const absorbed =
|
|
1445
|
+
shls.length > 0 &&
|
|
1446
|
+
shls.every(
|
|
1447
|
+
(u) =>
|
|
1448
|
+
u.op.opcode === 'shl' && (useSitesOf.get(u.op.results[0]) ?? []).every((v) => bitfieldSpelling.has(v.op)),
|
|
1449
|
+
);
|
|
1450
|
+
if (absorbed) {
|
|
1451
|
+
absorbedLoads.add(load);
|
|
1452
|
+
}
|
|
1453
|
+
}
|
|
1454
|
+
}
|
|
1455
|
+
|
|
1456
|
+
// An unresolvable value: strict mode keeps the `"?"` sentinel AND records the reason — the
|
|
1457
|
+
// decline thrown below names the actual gaps ("unmodelled instruction 'adde'"), the same
|
|
1458
|
+
// reasons annotate mode's markers carry, instead of the anonymous `?` that assertResolved
|
|
1459
|
+
// would report at the boundary (assertResolved stays as the backstop for any other producer).
|
|
1460
|
+
// Annotate mode emits a marker (the undefined ASMLIFT_ERROR symbol — loud in the ARTIFACT,
|
|
1461
|
+
// function still complete).
|
|
1462
|
+
const strictGaps: string[] = [];
|
|
1463
|
+
const mkGap = (reason: string, args: Expr[]): Expr => {
|
|
1464
|
+
if (onGap === 'annotate') {
|
|
1465
|
+
return { k: 'marker', reason, args };
|
|
1466
|
+
}
|
|
1467
|
+
strictGaps.push(reason);
|
|
1468
|
+
return { k: 'var', name: '?' };
|
|
1469
|
+
};
|
|
794
1470
|
|
|
795
1471
|
// Lower ONE def's operation to an Expr, rendering operands through `e`. Shared between the
|
|
796
1472
|
// inline-at-use path (exprWith) and the materialized-temp path (sideEffects), so both spell a
|
|
@@ -799,8 +1475,37 @@ export function structure(fn: Fn, opts: StructureOptions = {}): SFn {
|
|
|
799
1475
|
if (d.opcode === 'const') {
|
|
800
1476
|
return { k: 'const', value: d.attrs.value as number };
|
|
801
1477
|
}
|
|
1478
|
+
// a bitfield extract recognized over the ops (see the precompute above): the member read,
|
|
1479
|
+
// not the shift pair
|
|
1480
|
+
const bf = bitfieldSpelling.get(d);
|
|
1481
|
+
if (bf) {
|
|
1482
|
+
return { k: 'field', base: { k: 'var', name: bf.global }, name: bf.field, dot: true };
|
|
1483
|
+
}
|
|
802
1484
|
if (CMP_TO_BIN[d.opcode]) {
|
|
803
|
-
|
|
1485
|
+
// A bare global address `&gSym` as a COMPARISON operand is the same unspelled escape as the
|
|
1486
|
+
// arithmetic case below (see intifyAddr): its C type comes from the PROJECT's own
|
|
1487
|
+
// declaration, unknowable here. Worse, the compare's SIGNEDNESS lives in the operand types
|
|
1488
|
+
// (CMP_TO_BIN maps icmp_ult and icmp_slt to the same '<'), so leaving `&gSym` untyped lets
|
|
1489
|
+
// the project's declaration pick the compare the compiler emits — silently byte-inexact
|
|
1490
|
+
// whenever it disagrees with the asm. The honest spelling is integer math on the address
|
|
1491
|
+
// with the cast AGREEING with the opcode's signedness: unsigned compares (and the
|
|
1492
|
+
// sign-agnostic ==/!=) spell `(u32)&gSym`, signed compares `(s32)&gSym` — exactly the
|
|
1493
|
+
// compare the asm did. The deref folds never see a compare operand, so no named spelling is
|
|
1494
|
+
// lost; a NARROWING cast (`(u8)&gSym`) is not a bare `addr` and keeps its truncation.
|
|
1495
|
+
// SCOPE (adversarial review): this closes the hole for BARE addr operands only. An
|
|
1496
|
+
// addr-carrying arithmetic tree (`(u32)&gSym + 4`, spelled by intifyAddr below) under an
|
|
1497
|
+
// icmp_s* still compares unsigned in C (u32 wins the usual-arithmetic-conversions) — the
|
|
1498
|
+
// same pre-existing wrongness the old ptr-vs-int spelling had, surfacing as a scoring
|
|
1499
|
+
// nonmatch, never a silent regression of a formerly-correct compare. Rare shape; an outer
|
|
1500
|
+
// signed cast on addr-carrying trees is the follow-up if it ever costs a row.
|
|
1501
|
+
const t = /^icmp_s/.test(d.opcode) ? T.s(32) : T.u(32);
|
|
1502
|
+
const intifyAddrCmp = (x: Expr): Expr => (x.k === 'addr' ? { k: 'cast', to: t, e: x } : x);
|
|
1503
|
+
return {
|
|
1504
|
+
k: 'bin',
|
|
1505
|
+
op: CMP_TO_BIN[d.opcode],
|
|
1506
|
+
l: intifyAddrCmp(e(d.operands[0])),
|
|
1507
|
+
r: intifyAddrCmp(e(d.operands[1])),
|
|
1508
|
+
};
|
|
804
1509
|
}
|
|
805
1510
|
if (ARITH_TO_BIN[d.opcode]) {
|
|
806
1511
|
let l = e(d.operands[0]);
|
|
@@ -844,6 +1549,53 @@ export function structure(fn: Fn, opts: StructureOptions = {}): SFn {
|
|
|
844
1549
|
} else if (op === '-' && ctype(l)?.kind !== 'ptr' && ctype(r)?.kind === 'ptr') {
|
|
845
1550
|
r = intify(r); // int - ptr is not C
|
|
846
1551
|
}
|
|
1552
|
+
// A bare global address `&gSym` under ANY of these operators is never emitted as-is: its C
|
|
1553
|
+
// type comes from the PROJECT's own declaration (unknowable here — exprCType types `addr`
|
|
1554
|
+
// undefined, so the ptr-keyed intify above never fires on it), which makes `&gSym + K`
|
|
1555
|
+
// byte-INEXACT (C scales K by sizeof(gSym)) and `&gSym & K` ill-formed. The honest spelling
|
|
1556
|
+
// is integer math on the address — `(u32)&gSym + K`, exactly the arithmetic the asm did.
|
|
1557
|
+
// The deref folds (globalOf / globalConstByte, via addrIn) look through this cast, so every
|
|
1558
|
+
// access that CAN spell a named element/field still does; only a genuine value-context
|
|
1559
|
+
// escape (a call argument, a stored address, a compare) keeps it — previously such an
|
|
1560
|
+
// escape tripped assertDerefsTyped's interior-pointer rule and declined the whole function.
|
|
1561
|
+
const intifyAddr = (x: Expr): Expr => (x.k === 'addr' ? { k: 'cast', to: T.u(32), e: x } : x);
|
|
1562
|
+
l = intifyAddr(l);
|
|
1563
|
+
r = intifyAddr(r);
|
|
1564
|
+
// The SAME hazard one level down, for a POINTER-shaped global's VALUE (`gPtr`, isPtrGlobal):
|
|
1565
|
+
// C scales `gPtr + K` by sizeof(*gPtr) — 1 under the map's synthesized `void *`, but
|
|
1566
|
+
// whatever the PROJECT's header declares (a 0x5C-byte struct, say) in the world a user
|
|
1567
|
+
// actually recompiles in. The asm added BYTES, so the honest spelling makes the stride
|
|
1568
|
+
// explicit: CAST-THEN-ADD, `(u8 *)gPtr + K`, the same address in EVERY world. Add-then-cast
|
|
1569
|
+
// (`(u8 *)(gPtr + K)`, what the backend's deref legalization would otherwise produce) is
|
|
1570
|
+
// byte-correct in exactly one of them — a silent wrongness, the class this project refuses.
|
|
1571
|
+
// NOT foldable into the deref index either: `((u8 *)gPtr)[K + off]` re-scales K by the
|
|
1572
|
+
// ACCESS width, a different address whenever that width is not 1.
|
|
1573
|
+
// Under the non-additive operators C rejects a pointer outright, so there the honest
|
|
1574
|
+
// spelling is integer math on the cell — exactly intifyAddr's `(u32)&gSym` rule.
|
|
1575
|
+
const bytePtr = (x: Expr): Expr => ({ k: 'cast', to: T.ptr(T.u(8)), e: x });
|
|
1576
|
+
const intifyPtrGlobal = (x: Expr): Expr => ({ k: 'cast', to: T.u(32), e: x });
|
|
1577
|
+
if (op === '+' || op === '-') {
|
|
1578
|
+
// `ptr ± int` and `ptr - ptr` are byte arithmetic once both sides are byte pointers;
|
|
1579
|
+
// `ptr + ptr` and `int - ptr` are not C at all, so the second pointer goes integer.
|
|
1580
|
+
const bothPtr = isPtrGlobal(l) && isPtrGlobal(r);
|
|
1581
|
+
if (isPtrGlobal(l)) {
|
|
1582
|
+
l = bytePtr(l);
|
|
1583
|
+
}
|
|
1584
|
+
if (isPtrGlobal(r)) {
|
|
1585
|
+
r = bothPtr && op === '-' ? bytePtr(r) : op === '+' && !bothPtr ? bytePtr(r) : intifyPtrGlobal(r);
|
|
1586
|
+
}
|
|
1587
|
+
} else if (op !== '&&' && op !== '||') {
|
|
1588
|
+
// (`&&`/`||` take a pointer operand legally — a truth test, no arithmetic.)
|
|
1589
|
+
l = isPtrGlobal(l) ? intifyPtrGlobal(l) : l;
|
|
1590
|
+
r = isPtrGlobal(r) ? intifyPtrGlobal(r) : r;
|
|
1591
|
+
}
|
|
1592
|
+
// (The two right shifts stay DISTINCT ops here — `>>>` logical, `>>` arithmetic. Which token
|
|
1593
|
+
// a language spells each with, and what cast pins the choice, is a BACKEND decision; see
|
|
1594
|
+
// l3/ast.ts BinOp and backend/cfamily.ts's shift rule.)
|
|
1595
|
+
// SCOPE: this and intifyAddr cover the ARITHMETIC escapes. A pointer global under a
|
|
1596
|
+
// COMPARISON (`gPtr < K` — C compares unsigned whatever the asm's icmp_s* said) is the same
|
|
1597
|
+
// class as intifyAddrCmp's `addr` rule and is deliberately left alone here: it is valid C
|
|
1598
|
+
// today, so closing it would churn spellings for a signedness case no row exercises.
|
|
847
1599
|
return { k: 'bin', op, l, r };
|
|
848
1600
|
}
|
|
849
1601
|
// `-`/`~` on a pointer rendering is equally not C — same honest integer cast as above.
|
|
@@ -851,8 +1603,9 @@ export function structure(fn: Fn, opts: StructureOptions = {}): SFn {
|
|
|
851
1603
|
// The C rotate idiom — `x >> n | x << (32 - n)` (mirrored for rotl). Byte-exact round-trip
|
|
852
1604
|
// on agbcc (thumb ror) and mwcc (rotlw/rotlwi), verified against both toolchains before the
|
|
853
1605
|
// ops landed. `x` and `n` render twice — both pure by construction (SSA values; the rotate's
|
|
854
|
-
// operands are register reads)
|
|
855
|
-
//
|
|
1606
|
+
// operands are register reads). The right half is the LOGICAL shift `>>>` — the idiom is
|
|
1607
|
+
// wrong with an arithmetic one — stated on the node rather than left to the rotated value's
|
|
1608
|
+
// recovered unsignedness, which is a property of recovery rather than of the idiom.
|
|
856
1609
|
//
|
|
857
1610
|
// (The PPC mirror fold — `rotl(x, 32 - m)` ⇒ rotr(x, m) — lives in the PATTERN layer,
|
|
858
1611
|
// engine.ts ROTL_MIRROR: it is a compiler-spelling idiom, mwcc-gated there, not a
|
|
@@ -869,7 +1622,7 @@ export function structure(fn: Fn, opts: StructureOptions = {}): SFn {
|
|
|
869
1622
|
n.k === 'const'
|
|
870
1623
|
? { k: 'const', value: 32 - n.value }
|
|
871
1624
|
: { k: 'bin', op: '-', l: { k: 'const', value: 32 }, r: n };
|
|
872
|
-
const [near, far] = dir === 'rotr' ? (['
|
|
1625
|
+
const [near, far] = dir === 'rotr' ? (['>>>', '<<'] as const) : (['<<', '>>>'] as const);
|
|
873
1626
|
return {
|
|
874
1627
|
k: 'bin',
|
|
875
1628
|
op: '|',
|
|
@@ -879,11 +1632,11 @@ export function structure(fn: Fn, opts: StructureOptions = {}): SFn {
|
|
|
879
1632
|
}
|
|
880
1633
|
if (d.opcode === 'neg') {
|
|
881
1634
|
const x = e(d.operands[0]);
|
|
882
|
-
return { k: 'un', op: '-', e:
|
|
1635
|
+
return { k: 'un', op: '-', e: needsIntSpelling(x) ? { k: 'cast', to: T.s(32), e: x } : x };
|
|
883
1636
|
}
|
|
884
1637
|
if (d.opcode === 'not') {
|
|
885
1638
|
const x = e(d.operands[0]);
|
|
886
|
-
return { k: 'un', op: '~', e:
|
|
1639
|
+
return { k: 'un', op: '~', e: needsIntSpelling(x) ? { k: 'cast', to: T.s(32), e: x } : x };
|
|
887
1640
|
}
|
|
888
1641
|
// Width-narrowing casts: `zext`/`sext` widen a `width`-bit value back to 32 → C `(u8)e`/`(s8)e`.
|
|
889
1642
|
if (d.opcode === 'zext') {
|
|
@@ -896,6 +1649,12 @@ export function structure(fn: Fn, opts: StructureOptions = {}): SFn {
|
|
|
896
1649
|
return { k: 'call', fn: d.attrs.target as string, args: d.operands.map(e) };
|
|
897
1650
|
}
|
|
898
1651
|
if (d.opcode === 'gaddr') {
|
|
1652
|
+
// A promoted CODE symbol (frontend `code: true`) is a function pointer stored as an
|
|
1653
|
+
// integer: spelled `(u32)Name` — the source idiom — never `&Name` (defect G of the
|
|
1654
|
+
// dogfood report; the & form compiles but is a different, non-matching spelling).
|
|
1655
|
+
if (d.attrs.code === true) {
|
|
1656
|
+
return { k: 'cast', to: T.int(32, false), e: { k: 'var', name: d.attrs.sym as string } };
|
|
1657
|
+
}
|
|
899
1658
|
return { k: 'addr', name: d.attrs.sym as string };
|
|
900
1659
|
}
|
|
901
1660
|
if (d.opcode === 'load') {
|
|
@@ -907,6 +1666,7 @@ export function structure(fn: Fn, opts: StructureOptions = {}): SFn {
|
|
|
907
1666
|
(d.attrs.signed as boolean) ?? false,
|
|
908
1667
|
ctype,
|
|
909
1668
|
scalarGlobals,
|
|
1669
|
+
symCtx,
|
|
910
1670
|
);
|
|
911
1671
|
}
|
|
912
1672
|
// aload carries a runtime index operand (variable-index array access) — `base[index]`, or
|
|
@@ -920,6 +1680,7 @@ export function structure(fn: Fn, opts: StructureOptions = {}): SFn {
|
|
|
920
1680
|
d.attrs.elemSize as number,
|
|
921
1681
|
(d.attrs.signed as boolean) ?? false,
|
|
922
1682
|
ctype,
|
|
1683
|
+
symCtx,
|
|
923
1684
|
);
|
|
924
1685
|
}
|
|
925
1686
|
return d.opcode === 'opaque'
|
|
@@ -983,13 +1744,17 @@ export function structure(fn: Fn, opts: StructureOptions = {}): SFn {
|
|
|
983
1744
|
const target = succ.block;
|
|
984
1745
|
const argExpr = sub ? exprWith(sub) : expr;
|
|
985
1746
|
const copies: { name: string; value: Expr; arg: Value }[] = [];
|
|
1747
|
+
const suppressed = suppressedArgs.get(succ);
|
|
986
1748
|
target.params.forEach((p, i) => {
|
|
1749
|
+
if (suppressed?.has(i)) {
|
|
1750
|
+
return;
|
|
1751
|
+
} // anchored at its const's def site — the write already ran before this edge
|
|
987
1752
|
const name = varName.get(p)!;
|
|
988
1753
|
const arg = succ.args[i];
|
|
989
1754
|
if ((sub?.get(arg) ?? varName.get(arg)) === name) {
|
|
990
1755
|
return;
|
|
991
1756
|
} // identity copy — coalesced away
|
|
992
|
-
copies.push({ name, value: argExpr(arg), arg });
|
|
1757
|
+
copies.push({ name, value: castAggregateAddr(name, argExpr(arg)), arg });
|
|
993
1758
|
});
|
|
994
1759
|
// Emit in the order the args are COMPUTED in `pred` — a compiler that lays the defining ops
|
|
995
1760
|
// (and thus the copies that read them) out in that order matches with no spurious arg-swap.
|
|
@@ -1035,6 +1800,8 @@ export function structure(fn: Fn, opts: StructureOptions = {}): SFn {
|
|
|
1035
1800
|
width === 4,
|
|
1036
1801
|
ctype,
|
|
1037
1802
|
scalarGlobals,
|
|
1803
|
+
symCtx,
|
|
1804
|
+
true, // an lvalue: a member whose declaration is const cannot be NAMED as the target
|
|
1038
1805
|
);
|
|
1039
1806
|
if (lval0.k === 'var') {
|
|
1040
1807
|
globalNames.add(lval0.name);
|
|
@@ -1056,13 +1823,21 @@ export function structure(fn: Fn, opts: StructureOptions = {}): SFn {
|
|
|
1056
1823
|
elemSize,
|
|
1057
1824
|
elemSize === 4,
|
|
1058
1825
|
ctype,
|
|
1826
|
+
symCtx,
|
|
1059
1827
|
),
|
|
1060
1828
|
value: expr(op.operands[2]),
|
|
1061
1829
|
});
|
|
1062
1830
|
} else if (op.opcode === 'call' && op.results.length && !useSitesOf.has(op.results[0])) {
|
|
1063
1831
|
out.push({ k: 'exprstmt', value: expr(op.results[0]) });
|
|
1064
|
-
} else if (materialize.has(op)) {
|
|
1065
|
-
|
|
1832
|
+
} else if (materialize.has(op) && !absorbedLoads.has(op)) {
|
|
1833
|
+
// (an absorbed load's every consumer spells a named bitfield read — emitting its temp
|
|
1834
|
+
// here would recompile to a second load the asm does not have)
|
|
1835
|
+
const nm = varName.get(op.results[0])!;
|
|
1836
|
+
out.push({ k: 'assign', name: nm, value: castAggregateAddr(nm, lowerDef(op, expr)) });
|
|
1837
|
+
}
|
|
1838
|
+
// a merge copy anchored at this const's original position (anchorConstCopies, above)
|
|
1839
|
+
for (const a of anchoredAt.get(op) ?? []) {
|
|
1840
|
+
out.push({ k: 'assign', name: a.name, value: expr(a.arg) });
|
|
1066
1841
|
}
|
|
1067
1842
|
}
|
|
1068
1843
|
return out;
|
|
@@ -1122,6 +1897,7 @@ export function structure(fn: Fn, opts: StructureOptions = {}): SFn {
|
|
|
1122
1897
|
isNamed: (v) => varName.has(v),
|
|
1123
1898
|
isCmpOpcode: (opcode) => !!CMP_TO_BIN[opcode],
|
|
1124
1899
|
switchAllowsNeqCase,
|
|
1900
|
+
emitsAnchoredWrite: (blk) => blk.ops.some((o) => anchoredAt.has(o)),
|
|
1125
1901
|
expr: (v) => expr(v),
|
|
1126
1902
|
structureRegion: (b, stop) => structureRegion(b, stop),
|
|
1127
1903
|
});
|
|
@@ -1323,7 +2099,7 @@ export function structure(fn: Fn, opts: StructureOptions = {}): SFn {
|
|
|
1323
2099
|
out.push(...updateCopies); // the loop update, RAW (i++, p>>=1, …)
|
|
1324
2100
|
let leaveCond = exprWith(sub)(term.operands[0]);
|
|
1325
2101
|
if (contIsTaken) {
|
|
1326
|
-
leaveCond =
|
|
2102
|
+
leaveCond = negateCond(leaveCond);
|
|
1327
2103
|
} // continue is `taken` → leave when NOT it
|
|
1328
2104
|
const exitArm = isBreak
|
|
1329
2105
|
? [...argAssigns(b, loopCtx.exit, sub), { k: 'break' } as Stmt] // break to the loop exit
|
|
@@ -1357,7 +2133,7 @@ export function structure(fn: Fn, opts: StructureOptions = {}): SFn {
|
|
|
1357
2133
|
// IDO/MIPS; agbcc/GCC canonicalise either way, so it is safe there too. A compiler that
|
|
1358
2134
|
// inverts branch canonicalization sets preserveDivergentBranchSense false and falls through
|
|
1359
2135
|
// to the positive form below.
|
|
1360
|
-
out.push({ k: 'if', cond:
|
|
2136
|
+
out.push({ k: 'if', cond: negateCond(cond), then: elseS, else: thenS });
|
|
1361
2137
|
return out;
|
|
1362
2138
|
}
|
|
1363
2139
|
out.push(mkIf(cond, thenS, elseS));
|
|
@@ -1386,7 +2162,7 @@ export function structure(fn: Fn, opts: StructureOptions = {}): SFn {
|
|
|
1386
2162
|
const term = li.header.ops[li.header.ops.length - 1];
|
|
1387
2163
|
let cond = exprWith(loopSub(li))(term.operands[0]);
|
|
1388
2164
|
if (term.successors[0].block !== li.header) {
|
|
1389
|
-
cond =
|
|
2165
|
+
cond = negateCond(cond);
|
|
1390
2166
|
} // loop-continue must be `taken`
|
|
1391
2167
|
const body = [...sideEffects(li.header), ...(updates ?? argAssigns(li.header, li.header))];
|
|
1392
2168
|
return { k: 'while', cond, body };
|
|
@@ -1400,7 +2176,7 @@ export function structure(fn: Fn, opts: StructureOptions = {}): SFn {
|
|
|
1400
2176
|
const term = wl.header.ops[wl.header.ops.length - 1];
|
|
1401
2177
|
let cond = expr(term.operands[0]);
|
|
1402
2178
|
if (term.successors[1].block === wl.bodyEntry) {
|
|
1403
|
-
cond =
|
|
2179
|
+
cond = negateCond(cond);
|
|
1404
2180
|
}
|
|
1405
2181
|
// The header→bodyEntry edge may carry non-identity phi args (a value the header COMPUTED and passes
|
|
1406
2182
|
// into the body). Those copies must open the body — dropping them reads an uninitialised local.
|
|
@@ -1462,7 +2238,7 @@ export function structure(fn: Fn, opts: StructureOptions = {}): SFn {
|
|
|
1462
2238
|
const body = [...inner, ...sideEffects(dw.latch), ...updates];
|
|
1463
2239
|
let cond = exprWith(sub)(lterm.operands[0]);
|
|
1464
2240
|
if (lterm.successors[1].block === dw.header) {
|
|
1465
|
-
cond =
|
|
2241
|
+
cond = negateCond(cond);
|
|
1466
2242
|
} // continue edge must be `taken`
|
|
1467
2243
|
const out: Stmt[] = [{ k: 'dowhile', cond, body }];
|
|
1468
2244
|
// The exit region reads latch back-edge values under `sub` (post-loop they live in the loop vars).
|
|
@@ -1471,6 +2247,14 @@ export function structure(fn: Fn, opts: StructureOptions = {}): SFn {
|
|
|
1471
2247
|
};
|
|
1472
2248
|
|
|
1473
2249
|
const body = recognizeForLoops(structureRegion(entry, null));
|
|
2250
|
+
// Strict-mode gaps decline HERE, naming the reasons — the same text annotate's markers
|
|
2251
|
+
// carry, so the two mode surfaces report the same decline (the reproduction scripts run
|
|
2252
|
+
// strict; the benchmark rows store annotate markers — fidelity holds them against each
|
|
2253
|
+
// other). Without this, the `?` sentinels reach assertResolved and decline anonymously.
|
|
2254
|
+
if (strictGaps.length > 0) {
|
|
2255
|
+
const reasons = [...new Set(strictGaps)].join('; ');
|
|
2256
|
+
throw new StructureError(`${strictGaps.length} unresolvable value(s) in '${fn.name}' — ${reasons}`);
|
|
2257
|
+
}
|
|
1474
2258
|
// v* = coalesced/materialized locals; t* = sequentialize's swap-cycle temps (varType-only —
|
|
1475
2259
|
// they have no Value, so they are collected from varType, not varName).
|
|
1476
2260
|
const localNames = [...new Set([...varName.values(), ...[...varType.keys()].filter((n) => /^t\d+$/.test(n))])].filter(
|
|
@@ -1481,6 +2265,13 @@ export function structure(fn: Fn, opts: StructureOptions = {}): SFn {
|
|
|
1481
2265
|
name: fn.name,
|
|
1482
2266
|
params: entry.params.map((p, i) => ({ name: `a${i}`, type: p.type })),
|
|
1483
2267
|
locals: localNames.map((n) => ({ name: n, type: varType.get(n)! })),
|
|
2268
|
+
...(shapedGlobalTypes.size
|
|
2269
|
+
? {
|
|
2270
|
+
globals: [...shapedGlobalTypes]
|
|
2271
|
+
.map(([name, type]) => ({ name, type }))
|
|
2272
|
+
.sort((a, b) => a.name.localeCompare(b.name)),
|
|
2273
|
+
}
|
|
2274
|
+
: {}),
|
|
1484
2275
|
retType: returnsVoid ? T.void() : returnType(fn),
|
|
1485
2276
|
body,
|
|
1486
2277
|
...(structs.length ? { structs } : {}),
|
|
@@ -1630,16 +2421,10 @@ function substVar(e: Expr, from: string, to: string): Expr {
|
|
|
1630
2421
|
// empty-then peephole: `if (c) {} else { S }` → `if (!c) { S }`
|
|
1631
2422
|
function mkIf(cond: Expr, thenS: Stmt[], elseS: Stmt[]): Stmt {
|
|
1632
2423
|
if (thenS.length === 0 && elseS.length > 0) {
|
|
1633
|
-
return { k: 'if', cond:
|
|
2424
|
+
return { k: 'if', cond: negateCond(cond), then: elseS, else: [] };
|
|
1634
2425
|
}
|
|
1635
2426
|
return { k: 'if', cond, then: thenS, else: elseS };
|
|
1636
2427
|
}
|
|
1637
|
-
function negate(e: Expr): Expr {
|
|
1638
|
-
if (e.k === 'bin' && NEGATE[e.op]) {
|
|
1639
|
-
return { ...e, op: NEGATE[e.op] };
|
|
1640
|
-
}
|
|
1641
|
-
return { k: 'un', op: '!', e };
|
|
1642
|
-
}
|
|
1643
2428
|
|
|
1644
2429
|
// --- CFG utilities ---
|
|
1645
2430
|
function predecessorBlocks(fn: Fn): Map<Block, Block[]> {
|