@asmlift/core 0.3.0 → 0.5.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 +130 -4
- package/src/backend/cpp.ts +3 -1
- package/src/backend/pascal.ts +11 -0
- package/src/contracts.ts +181 -4
- package/src/declare.ts +35 -9
- package/src/frontend/mips.ts +37 -29
- package/src/frontend/opaque.ts +70 -20
- package/src/frontend/ppc.ts +18 -7
- package/src/frontend/ssa.ts +279 -56
- package/src/frontend/thumb.ts +1372 -87
- package/src/ir/alias.ts +75 -0
- package/src/ir/opcodes.ts +57 -3
- package/src/ir/simplify.ts +72 -0
- package/src/l3/argbase.ts +221 -0
- package/src/l3/ast.ts +127 -5
- package/src/l3/basecse.ts +58 -62
- package/src/l3/coalesce.ts +215 -0
- package/src/l3/dce.ts +33 -41
- package/src/l3/gates.ts +67 -0
- package/src/l3/hoist.ts +65 -0
- package/src/l3/reindex.ts +7 -0
- package/src/l3/scopebase.ts +440 -0
- package/src/l3/tailmerge.ts +124 -0
- package/src/macros.ts +222 -13
- package/src/pattern/engine.ts +99 -6
- package/src/pipeline.ts +65 -6
- package/src/raise/divpow2.ts +227 -0
- package/src/raise/gvn.ts +151 -0
- package/src/raise/pre-recovery.ts +39 -3
- package/src/raise/recover.ts +24 -7
- package/src/raise/retsink.ts +37 -7
- package/src/raise/shortcircuit.ts +262 -22
- package/src/raise/struct-arrays.ts +2 -1
- package/src/raise/structs.ts +41 -3
- package/src/rank.ts +196 -20
- package/src/structure/analysis.ts +175 -89
- package/src/structure/structure.ts +588 -55
- package/src/structure/switch-recover.ts +117 -30
- package/src/symbols.ts +128 -13
- package/src/target.ts +4 -2
- package/src/trace.ts +9 -0
|
@@ -18,18 +18,49 @@ export interface SwitchRecoverDeps {
|
|
|
18
18
|
/** is this opcode an integer comparison? */
|
|
19
19
|
isCmpOpcode: (opcode: string) => boolean;
|
|
20
20
|
switchAllowsNeqCase: boolean;
|
|
21
|
+
/** does emitting this block's ops carry a statement beyond the ops themselves? A def-site
|
|
22
|
+
* ANCHORED merge copy (structure.ts anchorConstCopies) is attached to a const op and emitted
|
|
23
|
+
* with the block's side effects — a test block carrying one is not pure however pure its
|
|
24
|
+
* opcodes look, because collapsing it into a `switch` discards the write while the edge copy
|
|
25
|
+
* it replaced stays suppressed. */
|
|
26
|
+
emitsAnchoredWrite: (blk: Block) => boolean;
|
|
21
27
|
expr: (v: Value) => Expr;
|
|
22
28
|
structureRegion: (b: Block, stop: Block | null) => Stmt[];
|
|
23
29
|
}
|
|
24
30
|
|
|
31
|
+
/** Where ONE switch arm's region leaves it — the fact that decides whether the arm can be spelled
|
|
32
|
+
* as C at all, and with or without a `break`.
|
|
33
|
+
*
|
|
34
|
+
* - `break` every path out of the arm reaches the switch's merge (or returns / loops
|
|
35
|
+
* inside the arm). The ordinary closed arm.
|
|
36
|
+
* - `fallthrough` every path out leaves into exactly ONE sibling arm's entry: C's fall-through.
|
|
37
|
+
* Only spellable when that sibling is the arm emitted NEXT (the caller checks
|
|
38
|
+
* emission adjacency — see the l3/ast.ts non-neutrality note).
|
|
39
|
+
* - `unstructurable` anything else: two different siblings, or a mix of "into a sibling" and
|
|
40
|
+
* "out to the merge". C needs a `goto` for those, so callers decline LOUD. */
|
|
41
|
+
export type ArmExit = { kind: 'break' } | { kind: 'fallthrough'; to: Block } | { kind: 'unstructurable'; why: string };
|
|
42
|
+
|
|
25
43
|
export interface SwitchRecovery {
|
|
26
44
|
recognizeSwitch: (b: Block, stop: Block | null) => Stmt[] | null;
|
|
27
|
-
/** shared with the Regime-B (`switch_br`) path in structure.ts, which
|
|
28
|
-
|
|
45
|
+
/** shared with the Regime-B (`switch_br`) path in structure.ts, which recovers the fall-through
|
|
46
|
+
* this returns; Regime A only accepts `break` arms and otherwise declines to if-recovery. */
|
|
47
|
+
analyzeArmExit: (entry: Block, b: Block, merge: Block | null, siblings: Set<Block>) => ArmExit;
|
|
29
48
|
}
|
|
30
49
|
|
|
31
50
|
export function makeSwitchRecovery(deps: SwitchRecoverDeps): SwitchRecovery {
|
|
32
|
-
const {
|
|
51
|
+
const {
|
|
52
|
+
fn,
|
|
53
|
+
defs,
|
|
54
|
+
dom,
|
|
55
|
+
ipdom,
|
|
56
|
+
opBlock,
|
|
57
|
+
isNamed,
|
|
58
|
+
isCmpOpcode,
|
|
59
|
+
switchAllowsNeqCase,
|
|
60
|
+
emitsAnchoredWrite,
|
|
61
|
+
expr,
|
|
62
|
+
structureRegion,
|
|
63
|
+
} = deps;
|
|
33
64
|
|
|
34
65
|
// --- Regime A: comparison-tree switch recovery ----------------------------------------------------
|
|
35
66
|
// Every ambiguity declines. Four preconditions are enforced below, annotated PRE1..PRE4:
|
|
@@ -128,9 +159,9 @@ export function makeSwitchRecovery(deps: SwitchRecoverDeps): SwitchRecovery {
|
|
|
128
159
|
if (!cmp || !isCmpOpcode(cmp.opcode)) {
|
|
129
160
|
return null;
|
|
130
161
|
}
|
|
131
|
-
if (!isRoot && blk.ops.some((op) => SIDE_EFFECTFUL.has(op.opcode))) {
|
|
162
|
+
if (!isRoot && (blk.ops.some((op) => SIDE_EFFECTFUL.has(op.opcode)) || emitsAnchoredWrite(blk))) {
|
|
132
163
|
return null;
|
|
133
|
-
} // PRE4
|
|
164
|
+
} // PRE4 — anchored writes included: discarded with the block, while their edge copies stay suppressed
|
|
134
165
|
// Which operand is the scrutinee, which is the constant?
|
|
135
166
|
const [lo, ro] = cmp.operands;
|
|
136
167
|
const lc = evalConst(lo),
|
|
@@ -180,31 +211,83 @@ export function makeSwitchRecovery(deps: SwitchRecoverDeps): SwitchRecovery {
|
|
|
180
211
|
}
|
|
181
212
|
};
|
|
182
213
|
|
|
183
|
-
//
|
|
184
|
-
//
|
|
185
|
-
//
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
214
|
+
// Where does one arm's region LEAVE? Walk it from `entry`, never stepping THROUGH the merge or a
|
|
215
|
+
// sibling arm's entry, and classify what it steps INTO. `siblings` is every OTHER arm entry the
|
|
216
|
+
// caller can emit a `case`/`default` label for — the merge is deliberately not among them, so a
|
|
217
|
+
// switch whose default block IS the merge (agbcc's usual "the default just leaves") reads as an
|
|
218
|
+
// ordinary `break`, not as falling into the default.
|
|
219
|
+
//
|
|
220
|
+
// Region membership is `dom(blk) ∋ b` as before: a block NOT dominated by the switch is outside
|
|
221
|
+
// this switch's region and is not walked. It IS recorded as an escape, because an arm that can
|
|
222
|
+
// leave sideways does not fall into the next case — but only the fall-through verdict consults
|
|
223
|
+
// that, so no arm that used to be accepted as closed becomes a decline.
|
|
224
|
+
//
|
|
225
|
+
// A CONSEQUENCE, not a hole: a sibling reachable only THROUGH such a block is never seen, so the
|
|
226
|
+
// arm reads as closed and `structureRegion` walks into the sibling's blocks and emits them again
|
|
227
|
+
// under this arm. That is duplication, not a wrong dispatch — the same duplication the structurer
|
|
228
|
+
// already does for any tail two arms share, and how the case bodies agbcc tail-merged are put
|
|
229
|
+
// back. Costly for matching, correct to run.
|
|
230
|
+
const analyzeArmExit = (entry: Block, b: Block, merge: Block | null, siblings: Set<Block>): ArmExit => {
|
|
231
|
+
if (entry === merge) {
|
|
232
|
+
return { kind: 'break' }; // an empty arm (a table slot pointing straight at the switch's end)
|
|
233
|
+
}
|
|
234
|
+
const into = new Set<Block>(); // sibling entries this arm flows into
|
|
235
|
+
let toMerge = false,
|
|
236
|
+
escapes = false;
|
|
237
|
+
const seen = new Set<Block>([entry]);
|
|
238
|
+
const q = [entry];
|
|
239
|
+
while (q.length) {
|
|
240
|
+
const cur = q.pop()!;
|
|
241
|
+
for (const s of successorsOf(cur)) {
|
|
242
|
+
if (s === merge) {
|
|
243
|
+
toMerge = true;
|
|
244
|
+
} else if (s !== entry && siblings.has(s)) {
|
|
245
|
+
into.add(s);
|
|
246
|
+
} else if (s !== entry && !seen.has(s)) {
|
|
247
|
+
if (!dom.get(s)!.has(b)) {
|
|
248
|
+
escapes = true;
|
|
195
249
|
continue;
|
|
196
250
|
}
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
}
|
|
200
|
-
if (inRegion(s) && !rseen.has(s)) {
|
|
201
|
-
rseen.add(s);
|
|
202
|
-
q.push(s);
|
|
203
|
-
}
|
|
251
|
+
seen.add(s);
|
|
252
|
+
q.push(s);
|
|
204
253
|
}
|
|
205
254
|
}
|
|
206
255
|
}
|
|
207
|
-
|
|
256
|
+
if (into.size === 0) {
|
|
257
|
+
return { kind: 'break' };
|
|
258
|
+
}
|
|
259
|
+
if (into.size === 1 && !toMerge && !escapes) {
|
|
260
|
+
return { kind: 'fallthrough', to: [...into][0] };
|
|
261
|
+
}
|
|
262
|
+
// Name what is actually missing. These three are different facts, and only the first is a shape
|
|
263
|
+
// C has no spelling for — the other two are asmlift's own limits, so say so rather than blame C.
|
|
264
|
+
const names = () => [...into].map((x) => `#${fn.blocks.indexOf(x)}`).join(', ');
|
|
265
|
+
if (into.size > 1) {
|
|
266
|
+
return {
|
|
267
|
+
kind: 'unstructurable',
|
|
268
|
+
why: `a case body reaches several sibling cases (${names()}) — C fall-through reaches only one, so this needs a goto`,
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
if (escapes) {
|
|
272
|
+
return {
|
|
273
|
+
kind: 'unstructurable',
|
|
274
|
+
why: `a case body reaches sibling case ${names()} on one path and, on another, a block the switch does not dominate`,
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
return {
|
|
278
|
+
kind: 'unstructurable',
|
|
279
|
+
// `case 0: if (c) { …; break; } /* fall through */ case 1:` is the C for this, and the reason
|
|
280
|
+
// asmlift cannot write it is its own: `{k:'break'}` is emitted only for the innermost LOOP
|
|
281
|
+
// (l3/ast.ts), never switch-scoped. That is the capability this shape is waiting on.
|
|
282
|
+
why: `a case body reaches sibling case ${names()} on one path and the end of the switch on another — a switch-scoped \`break\` inside a case body is not emitted yet`,
|
|
283
|
+
};
|
|
284
|
+
};
|
|
285
|
+
|
|
286
|
+
/** Every arm closed (`break`)? The precondition Regime A needs — it has a behaviourally identical
|
|
287
|
+
* fallback (if-recovery), so it declines on anything else instead of recovering fall-through. */
|
|
288
|
+
const allArmsClosed = (targets: Set<Block>, b: Block, merge: Block | null): boolean => {
|
|
289
|
+
const siblings = new Set([...targets].filter((t) => t !== merge));
|
|
290
|
+
return [...siblings].every((t) => analyzeArmExit(t, b, merge, siblings).kind === 'break');
|
|
208
291
|
};
|
|
209
292
|
|
|
210
293
|
const recognizeSwitch = (b: Block, stop: Block | null): Stmt[] | null => {
|
|
@@ -367,11 +450,12 @@ export function makeSwitchRecovery(deps: SwitchRecoverDeps): SwitchRecovery {
|
|
|
367
450
|
}
|
|
368
451
|
|
|
369
452
|
// PRE2 (fall-through): only NON-fall-through switches are handled — decline if any case body
|
|
370
|
-
// can reach ANOTHER case body (or
|
|
371
|
-
//
|
|
453
|
+
// can reach ANOTHER case body (or a default that has its own block) while staying inside the
|
|
454
|
+
// region. (The SAME analysis serves the Regime-B path, which RECOVERS the adjacent-sibling
|
|
455
|
+
// case as C fall-through instead of declining; A has if-recovery to fall back on, B does not.)
|
|
372
456
|
const merge = ipdom.get(b) ?? stop;
|
|
373
457
|
const targets = new Set<Block>([...caseBlocks, ...(defaultBlk ? [defaultBlk] : [])]);
|
|
374
|
-
if (
|
|
458
|
+
if (!allArmsClosed(targets, b, merge)) {
|
|
375
459
|
return null;
|
|
376
460
|
}
|
|
377
461
|
|
|
@@ -394,11 +478,14 @@ export function makeSwitchRecovery(deps: SwitchRecoverDeps): SwitchRecovery {
|
|
|
394
478
|
body: structureRegion(blk, merge),
|
|
395
479
|
fallsThrough: false,
|
|
396
480
|
}));
|
|
481
|
+
// An empty default arm is not a default (see the Regime-B note in structure.ts): the label
|
|
482
|
+
// would carry no statement, which says nothing and is not valid C89.
|
|
483
|
+
const defBody = defaultBlk ? structureRegion(defaultBlk, merge) : [];
|
|
397
484
|
const sw: Stmt = {
|
|
398
485
|
k: 'switch',
|
|
399
486
|
scrutinee: scrutExpr,
|
|
400
487
|
cases: outCases,
|
|
401
|
-
...(
|
|
488
|
+
...(defBody.length ? { default: defBody } : {}),
|
|
402
489
|
};
|
|
403
490
|
const out: Stmt[] = [sw];
|
|
404
491
|
if (merge && merge !== stop) {
|
|
@@ -406,5 +493,5 @@ export function makeSwitchRecovery(deps: SwitchRecoverDeps): SwitchRecovery {
|
|
|
406
493
|
}
|
|
407
494
|
return out;
|
|
408
495
|
};
|
|
409
|
-
return { recognizeSwitch,
|
|
496
|
+
return { recognizeSwitch, analyzeArmExit };
|
|
410
497
|
}
|
package/src/symbols.ts
CHANGED
|
@@ -52,6 +52,17 @@ export interface SymbolStructField {
|
|
|
52
52
|
/** ARRAY field only: the element count (absent for a flexible array member, which declares a
|
|
53
53
|
* stride but no bound) — types the synthesized `T name[n];` field decl */
|
|
54
54
|
length?: number;
|
|
55
|
+
/** BITFIELD field only: the field's width in BITS. Its PRESENCE is what marks a field a
|
|
56
|
+
* bitfield — `size` above stays the byte span its bits touch (the read width the compiler
|
|
57
|
+
* uses), which is why the exact (offset,size) scalar-field rules must exclude it. The
|
|
58
|
+
* provider only emits these for LITTLE-ENDIAN ELFs: both the extract equation the access
|
|
59
|
+
* recognizer solves and the `u32 name : n` layout model the synthesis verifies are LE-GCC
|
|
60
|
+
* semantics, so a big-endian map carries no bitfield members at all (today's behavior). */
|
|
61
|
+
bitWidth?: number;
|
|
62
|
+
/** BITFIELD field only: the bit position of the field's LOW bit within the byte at `offset`
|
|
63
|
+
* (LSB-first) — the field's absolute low bit is `offset*8 + bitOffset`. Required alongside
|
|
64
|
+
* `bitWidth`; a bitfield missing it is malformed and declines the whole layout. */
|
|
65
|
+
bitOffset?: number;
|
|
55
66
|
}
|
|
56
67
|
|
|
57
68
|
/** What a `shape:'pointer'` global POINTS AT, when the sidecar says its target is a struct/union.
|
|
@@ -118,6 +129,15 @@ export interface SymbolInfo {
|
|
|
118
129
|
elemSize?: number;
|
|
119
130
|
/** element signedness for `shape:'array'` (default unsigned) — types the env entry */
|
|
120
131
|
elemSigned?: boolean;
|
|
132
|
+
/** ARRAY RANK for `shape:'array'` — the per-dimension extents, outermost first (`u16
|
|
133
|
+
* g[4][0x400]` → `[4, 1024]`), `null` for an unbounded one. It is NOT `size`/`elemSize`
|
|
134
|
+
* restated: those size the object, this says how many subscripts reach an ELEMENT. `gSym[i]`
|
|
135
|
+
* on a rank-2 array is a ROW — against the project's own header that is a type error, or,
|
|
136
|
+
* where the row address flows into an integer context, silently the wrong address. So the
|
|
137
|
+
* bare spelling needs the leading subscripts (`gSym[0][i]`), and its ABSENCE is what forbids
|
|
138
|
+
* the bare spelling from being attempted at all (see the provider's dims capability gate:
|
|
139
|
+
* a package that cannot report rank must not be read as "rank 1"). */
|
|
140
|
+
dims?: (number | null)[];
|
|
121
141
|
/** the real struct tag for `shape:'struct'` — names the synthesized struct declaration
|
|
122
142
|
* (absent ⇒ synthesis mints a placeholder tag; the tag is codegen-arbitrary) */
|
|
123
143
|
structName?: string;
|
|
@@ -145,6 +165,33 @@ export interface SymbolInfo {
|
|
|
145
165
|
macroBody?: string;
|
|
146
166
|
}
|
|
147
167
|
|
|
168
|
+
/** THE one reading of {@link SymbolInfo.dims} for spelling C, shared by the access side
|
|
169
|
+
* (structure.ts's bare-name gate) and the declaration side (declare.ts) so the two cannot
|
|
170
|
+
* disagree about an array's shape.
|
|
171
|
+
*
|
|
172
|
+
* Returns the INNER extents — every dimension but the outermost. The outermost is excluded
|
|
173
|
+
* because C lets a declaration omit it, and the inner ones are exactly what scales a leading
|
|
174
|
+
* subscript. `[]` is the rank-1 answer: one subscript, `extern T gSym[];`, the spelling this has
|
|
175
|
+
* always had.
|
|
176
|
+
*
|
|
177
|
+
* An ABSENT `dims` also reads as rank 1, because that is what the author of such a map said: the
|
|
178
|
+
* ELF provider's capability gate refuses a @gba-kit/debug-info that cannot report rank, so
|
|
179
|
+
* absence here can only come from a hand-written map whose `shape:'array'` states a plain array.
|
|
180
|
+
* Absence never means "the package could not say" — that case fails loudly at load.
|
|
181
|
+
*
|
|
182
|
+
* Null means NO consistent pair is available (a stated rank with an unknown inner extent, which
|
|
183
|
+
* neither a declaration nor a subscript can spell). Both sides honour it the same way: the access
|
|
184
|
+
* falls back to `((T *)&gSym)[i]`, the declaration to the flat `extern T gSym[];` — valid
|
|
185
|
+
* together under whatever the project's own header says. */
|
|
186
|
+
export function arrayInnerExtents(info: SymbolInfo): number[] | null {
|
|
187
|
+
const dims = info.shape === 'array' ? info.dims : undefined;
|
|
188
|
+
if (dims === undefined || dims.length <= 1) {
|
|
189
|
+
return [];
|
|
190
|
+
}
|
|
191
|
+
const inner = dims.slice(1);
|
|
192
|
+
return inner.every((d) => typeof d === 'number' && d > 0) ? (inner as number[]) : null;
|
|
193
|
+
}
|
|
194
|
+
|
|
148
195
|
/** address → symbols at that address; `[0]` is the provider's canonical pick. */
|
|
149
196
|
export type SymbolMap = Map<number, SymbolInfo[]>;
|
|
150
197
|
|
|
@@ -155,6 +202,13 @@ export function isArrayField(f: SymbolStructField): boolean {
|
|
|
155
202
|
return f.elemSize !== undefined;
|
|
156
203
|
}
|
|
157
204
|
|
|
205
|
+
/** THE one test for "is this field a bitfield" — the PRESENCE of `bitWidth` (see the field doc).
|
|
206
|
+
* The exact (offset,size) scalar-field rules must exclude these: a 7-bit field whose bits span
|
|
207
|
+
* 2 bytes carries `size: 2` and would otherwise match a plain u16 read at its offset. */
|
|
208
|
+
export function isBitfieldField(f: SymbolStructField): boolean {
|
|
209
|
+
return f.bitWidth !== undefined;
|
|
210
|
+
}
|
|
211
|
+
|
|
158
212
|
/** A layout member that {@link declaredFields} passed: sizable, and seated at an offset no
|
|
159
213
|
* earlier member already covers. */
|
|
160
214
|
export type DeclaredField = SymbolStructField & { size: number };
|
|
@@ -166,12 +220,30 @@ function wellFormedField(f: unknown): f is SymbolStructField {
|
|
|
166
220
|
return false;
|
|
167
221
|
}
|
|
168
222
|
const m = f as Partial<SymbolStructField>;
|
|
169
|
-
|
|
170
|
-
typeof m.name
|
|
171
|
-
typeof m.offset
|
|
172
|
-
Number.isFinite(m.offset)
|
|
173
|
-
(m.size === null || (typeof m.size === 'number' && Number.isFinite(m.size) && m.size >= 0))
|
|
174
|
-
)
|
|
223
|
+
if (
|
|
224
|
+
typeof m.name !== 'string' ||
|
|
225
|
+
typeof m.offset !== 'number' ||
|
|
226
|
+
!Number.isFinite(m.offset) ||
|
|
227
|
+
!(m.size === null || (typeof m.size === 'number' && Number.isFinite(m.size) && m.size >= 0))
|
|
228
|
+
) {
|
|
229
|
+
return false;
|
|
230
|
+
}
|
|
231
|
+
// A bitfield's two facts must be present TOGETHER and internally consistent — a bitWidth with
|
|
232
|
+
// no bitOffset (or bits outside the byte span `size` claims) leaves the field unseatable, so
|
|
233
|
+
// the member is malformed and the layout declines whole like any other malformed member.
|
|
234
|
+
if (m.bitWidth !== undefined) {
|
|
235
|
+
return (
|
|
236
|
+
typeof m.bitWidth === 'number' &&
|
|
237
|
+
Number.isInteger(m.bitWidth) &&
|
|
238
|
+
m.bitWidth > 0 &&
|
|
239
|
+
typeof m.bitOffset === 'number' &&
|
|
240
|
+
Number.isInteger(m.bitOffset) &&
|
|
241
|
+
m.bitOffset >= 0 &&
|
|
242
|
+
typeof m.size === 'number' &&
|
|
243
|
+
m.bitOffset + m.bitWidth <= m.size * 8
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
return true;
|
|
175
247
|
}
|
|
176
248
|
|
|
177
249
|
/**
|
|
@@ -202,15 +274,33 @@ export function declaredFields(layout: SymbolStructField[] | undefined): Declare
|
|
|
202
274
|
return null;
|
|
203
275
|
}
|
|
204
276
|
}
|
|
205
|
-
|
|
277
|
+
// The cursor is in BITS so co-located bitfields seat correctly: `u32 a:2; u32 b:3;` are two
|
|
278
|
+
// members at byte offset 0, not a union alias. For plain members the arithmetic is the old
|
|
279
|
+
// byte cursor times 8 — behavior-identical for every bitfield-free layout.
|
|
280
|
+
const lowBitOf = (m: DeclaredField): number => m.offset * 8 + (m.bitWidth !== undefined ? m.bitOffset! : 0);
|
|
281
|
+
const members = (layout as DeclaredField[])
|
|
282
|
+
.slice()
|
|
283
|
+
.sort((a, b) => lowBitOf(a) - lowBitOf(b) || (a.bitWidth ?? -1) - (b.bitWidth ?? -1));
|
|
206
284
|
const out: DeclaredField[] = [];
|
|
207
|
-
let
|
|
285
|
+
let bitCursor = 0;
|
|
208
286
|
for (const m of members) {
|
|
209
|
-
|
|
287
|
+
const lo = lowBitOf(m);
|
|
288
|
+
if (lo < bitCursor) {
|
|
210
289
|
continue; // an overlapping (union) member: the first view is declared, the alias is not
|
|
211
290
|
}
|
|
212
|
-
|
|
213
|
-
|
|
291
|
+
if (m.bitWidth !== undefined) {
|
|
292
|
+
// The synthesis lays bitfields as LE-GCC `u32 name : n`, whose allocation never straddles
|
|
293
|
+
// a 32-bit unit — a field that would cannot be reproduced, so it is not declared (its bits
|
|
294
|
+
// pad instead) and no access may name it. The cursor does NOT advance: the bits stay a hole.
|
|
295
|
+
if (Math.floor(lo / 32) !== Math.floor((lo + m.bitWidth - 1) / 32)) {
|
|
296
|
+
continue;
|
|
297
|
+
}
|
|
298
|
+
out.push(m);
|
|
299
|
+
bitCursor = lo + m.bitWidth;
|
|
300
|
+
} else {
|
|
301
|
+
out.push(m);
|
|
302
|
+
bitCursor = (m.offset + m.size) * 8;
|
|
303
|
+
}
|
|
214
304
|
}
|
|
215
305
|
return out;
|
|
216
306
|
}
|
|
@@ -247,14 +337,39 @@ export function symbolFieldType(f: DeclaredField): IrType {
|
|
|
247
337
|
const scalarPointee = f.pointeeSize === 1 || f.pointeeSize === 2 || f.pointeeSize === 4;
|
|
248
338
|
return T.ptr(scalarPointee ? T.int(f.pointeeSize! * 8, f.pointeeSigned ?? false) : T.void());
|
|
249
339
|
}
|
|
250
|
-
if (f
|
|
251
|
-
|
|
340
|
+
if (isBitfieldField(f)) {
|
|
341
|
+
// The BASE type of the synthesized `u32 name : n` — the `: n` itself is the declaration
|
|
342
|
+
// renderer's job (StructFieldDecl.bits). 32-bit base always: that is the LE-GCC unit model
|
|
343
|
+
// declaredFields verified the layout against. A signless bitfield declares unsigned — the
|
|
344
|
+
// extract recognizer refuses to NAME one anyway, so the choice only types padding.
|
|
345
|
+
return T.int(32, f.signed ?? false);
|
|
346
|
+
}
|
|
347
|
+
if (isScalarCellSize(f.size)) {
|
|
348
|
+
return scalarCellType(f.size, f.signed);
|
|
252
349
|
}
|
|
253
350
|
return T.array(T.u(8), f.size);
|
|
254
351
|
}
|
|
255
352
|
/** A 4-byte member/scalar with NO base-type signedness is the enum idiom — C89 says int. */
|
|
256
353
|
export const ENUM_IS_SIGNED = true;
|
|
257
354
|
|
|
355
|
+
/** Is this byte size one a base type can spell? */
|
|
356
|
+
export function isScalarCellSize(size: number | undefined): size is 1 | 2 | 4 {
|
|
357
|
+
return size === 1 || size === 2 || size === 4;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/** THE DECLARED type of a 1/2/4-byte scalar cell — what `extern T gSym;` synthesis writes, and
|
|
361
|
+
* therefore what `&gSym` actually points to.
|
|
362
|
+
*
|
|
363
|
+
* Extracted because this rule had drifted into a fourth copy. It is NOT `scalarTypeForAccess`,
|
|
364
|
+
* which answers a different question — the type an ACCESS of that width reads — and collapses
|
|
365
|
+
* every 4-byte access to `s32` whatever the signedness. Using that one to decide "does `&gSym`
|
|
366
|
+
* already have the destination's type" silently answered YES for a `u32` cell reaching an
|
|
367
|
+
* `s32 *`, and the incompatible-pointer assignment survived. Declaration side and access side are
|
|
368
|
+
* separate facts; this is the declaration one. */
|
|
369
|
+
export function scalarCellType(size: 1 | 2 | 4, signed: boolean | undefined): IrType {
|
|
370
|
+
return T.int(size * 8, signed ?? (size === 4 ? ENUM_IS_SIGNED : false));
|
|
371
|
+
}
|
|
372
|
+
|
|
258
373
|
/**
|
|
259
374
|
* THE gate on every spelling through a POINTER global's value: the members a `gPtr->member`
|
|
260
375
|
* spelling may name, or null when nothing may be named through this pointee at all. Null unless
|
package/src/target.ts
CHANGED
|
@@ -34,7 +34,7 @@ export interface TargetDescription {
|
|
|
34
34
|
returnReg: string;
|
|
35
35
|
// HARDWARE / ISA facts — independent of the compiler.
|
|
36
36
|
capabilities: {
|
|
37
|
-
endianness: 'little' | 'big'; //
|
|
37
|
+
endianness: 'little' | 'big'; // consumed by structureOptionsFor (bitfield extract recognition is LSB-first)
|
|
38
38
|
hwDivide: boolean; // consumed by patternApplies (idiom gating)
|
|
39
39
|
hwFloat: boolean; // consumed by patternApplies (idiom gating)
|
|
40
40
|
flags: boolean; // RESERVED — no pass reads it yet (PPC condition regs will)
|
|
@@ -132,7 +132,9 @@ export const PPC_MWCC: TargetDescription = {
|
|
|
132
132
|
* target's compiler behaviors flow into the target-agnostic structurer — a new behavior lever
|
|
133
133
|
* is a field in `compilerBehaviors`, consumed automatically. */
|
|
134
134
|
export function structureOptionsFor(t: TargetDescription, returnsVoid: boolean): StructureOptions {
|
|
135
|
-
|
|
135
|
+
// `littleEndian` is the one HARDWARE capability the structurer consumes (bitfield extract
|
|
136
|
+
// recognition is LSB-first); everything else is a compiler behavior.
|
|
137
|
+
return { returnsVoid, littleEndian: t.capabilities.endianness === 'little', ...t.compilerBehaviors };
|
|
136
138
|
}
|
|
137
139
|
|
|
138
140
|
export const C_TYPEDEFS =
|
package/src/trace.ts
CHANGED
|
@@ -72,12 +72,21 @@ export interface TraceOptions {
|
|
|
72
72
|
// (not in pre-recovery.ts) because these strings are a trace concern — the driver itself is
|
|
73
73
|
// trace-agnostic. `title` is a function so `arrays` can fold its scaled-access count in.
|
|
74
74
|
const PRE_RECOVERY_TRACE: Record<string, { stage: string; title: (result: number | boolean) => string }> = {
|
|
75
|
+
addrnum: {
|
|
76
|
+
stage: 'stage:addrnum',
|
|
77
|
+
title: (r) => `Address numbering (${r} duplicate address def(s) / trivial phi(s) collapsed)`,
|
|
78
|
+
},
|
|
75
79
|
const: { stage: 'stage:const', title: () => 'Const materialize (lui;ori → one 32-bit const)' },
|
|
76
80
|
magicdiv: { stage: 'stage:magicdiv', title: () => 'Magic-number division recovery (mulh/mulhu → sdiv/udiv)' },
|
|
77
81
|
softdiv: { stage: 'stage:softdiv', title: () => 'Soft-division lower (bl __divsi3 → division op)' },
|
|
78
82
|
arrays: { stage: 'stage:legalize', title: (r) => `Array legalize (${r} scaled access(es) → aload/astore)` },
|
|
79
83
|
structs: { stage: 'stage:structs', title: () => 'Struct-pointer recovery (access-pattern evidence)' },
|
|
80
84
|
shortcircuit: { stage: 'stage:shortcircuit', title: () => 'Short-circuit recovery (boolean && / ||)' },
|
|
85
|
+
'branch-shortcircuit': {
|
|
86
|
+
stage: 'stage:branch-shortcircuit',
|
|
87
|
+
title: () => 'Short-circuit recovery (control-flow && / ||)',
|
|
88
|
+
},
|
|
89
|
+
'struct-arrays': { stage: 'stage:struct-arrays', title: () => 'Struct-array recovery (element stride evidence)' },
|
|
81
90
|
};
|
|
82
91
|
|
|
83
92
|
/** Run the tower while recording a TraceReport. Strict mode throws on any gap (like decompile);
|