@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
|
@@ -1,4 +1,16 @@
|
|
|
1
|
-
// asmlift —
|
|
1
|
+
// asmlift — short-circuit connective recovery (F-CFG; successor-aware, agbcc-class).
|
|
2
|
+
//
|
|
3
|
+
// A `&&`/`||` reaches the IR in two shapes, and this module recovers BOTH into the SAME pair of
|
|
4
|
+
// opcodes (`logic_and`/`logic_or`) the backend already prints as `&&`/`||`:
|
|
5
|
+
//
|
|
6
|
+
// - the VALUE form (`return a && b`) — a diamond whose merge phi is the boolean. That is
|
|
7
|
+
// `recognizeShortCircuit`, described below.
|
|
8
|
+
// - the CONTROL-FLOW form (`if (a || b) X else Y`) — no value at all, just two `cond_br` blocks
|
|
9
|
+
// that share a target. That is `recognizeBranchShortCircuit`, at the bottom of this file.
|
|
10
|
+
//
|
|
11
|
+
// They are one concept and stay in one file, but they are separate passes because their inputs do
|
|
12
|
+
// not overlap: the value form needs the second block to end in `br` carrying a phi argument, the
|
|
13
|
+
// branch form needs it to end in `cond_br`.
|
|
2
14
|
//
|
|
3
15
|
// `return a && b` compiles (agbcc) to a value-producing diamond: `if (a==0) result=0; else result=(b!=0)`,
|
|
4
16
|
// where the merge block returns the phi. The structurer lowers that as `if (a==0){v0=0}else{v0=(-b|b)>>31}
|
|
@@ -27,26 +39,10 @@
|
|
|
27
39
|
// Guards stay conservative: the CONST is exactly 0/1, Vb is a bool op or 0/1 const, the head condition is a
|
|
28
40
|
// negatable icmp, and any deviation falls through untouched (a miss, never a miscompile).
|
|
29
41
|
import { Block, Fn, Op, Value, defOpMap, mkOp, mkValue, predecessors, replaceAllUsesWith } from '../ir/core';
|
|
30
|
-
import
|
|
31
|
-
import { EFFECTFUL_OPS } from '../ir/opcodes';
|
|
42
|
+
import { HOIST_UNSAFE_OPS, NEGATED_ICMP } from '../ir/opcodes';
|
|
32
43
|
import { T } from '../ir/types';
|
|
33
44
|
|
|
34
|
-
const
|
|
35
|
-
icmp_eq: 'icmp_ne',
|
|
36
|
-
icmp_ne: 'icmp_eq',
|
|
37
|
-
icmp_slt: 'icmp_sge',
|
|
38
|
-
icmp_sge: 'icmp_slt',
|
|
39
|
-
icmp_sgt: 'icmp_sle',
|
|
40
|
-
icmp_sle: 'icmp_sgt',
|
|
41
|
-
icmp_ult: 'icmp_uge',
|
|
42
|
-
icmp_uge: 'icmp_ult',
|
|
43
|
-
icmp_ugt: 'icmp_ule',
|
|
44
|
-
icmp_ule: 'icmp_ugt',
|
|
45
|
-
};
|
|
46
|
-
const BOOL_OPS = new Set([...Object.keys(NEGATE_ICMP), 'logic_and', 'logic_or']);
|
|
47
|
-
// Ops with an observable side effect — unsafe to HOIST out of a short-circuit's conditional arm
|
|
48
|
-
// (they would run unconditionally). Derived from the ONE effect table in ir/opcodes.ts.
|
|
49
|
-
const SIDE_EFFECT = EFFECTFUL_OPS;
|
|
45
|
+
const BOOL_OPS = new Set([...Object.keys(NEGATED_ICMP), 'logic_and', 'logic_or']);
|
|
50
46
|
|
|
51
47
|
/** Fold `(-x | x) >> 31` (logical shift) → `x != 0`, in place. agbcc's branchless is-nonzero idiom. */
|
|
52
48
|
// NOT exported: it must run before the diamond fold, an ordering only recognizeShortCircuit's
|
|
@@ -124,6 +120,16 @@ export function recognizeShortCircuit(fn: Fn): boolean {
|
|
|
124
120
|
if (bp.length !== 1) {
|
|
125
121
|
continue;
|
|
126
122
|
}
|
|
123
|
+
// The ENTRY block is never a feeder, for the same reason it is never ^g in the branch form
|
|
124
|
+
// below: `predecessors()` walks successor edges only, so an entry block that is also a loop
|
|
125
|
+
// header shows one predecessor while actually running BEFORE it on the first iteration.
|
|
126
|
+
// Hoisting its body then reorders it and deleting it moves `fn.blocks[0]`. Silent — verify,
|
|
127
|
+
// assertResolved and assertDerefsTyped all pass. PRE-EXISTING (this fold predates the branch
|
|
128
|
+
// form and `main` miscompiles the same MIPS input); fixed here because the branch form's
|
|
129
|
+
// note used to assert this one was safe.
|
|
130
|
+
if (bfeed === fn.blocks[0]) {
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
127
133
|
const h = bp[0];
|
|
128
134
|
const ht = term(h);
|
|
129
135
|
if (ht.opcode !== 'cond_br') {
|
|
@@ -157,7 +163,7 @@ export function recognizeShortCircuit(fn: Fn): boolean {
|
|
|
157
163
|
}
|
|
158
164
|
const cond = ht.operands[0];
|
|
159
165
|
const condDef = defs.get(cond);
|
|
160
|
-
if (!condDef || !
|
|
166
|
+
if (!condDef || !NEGATED_ICMP[condDef.opcode]) {
|
|
161
167
|
continue;
|
|
162
168
|
} // head condition must be a negatable icmp
|
|
163
169
|
|
|
@@ -171,14 +177,14 @@ export function recognizeShortCircuit(fn: Fn): boolean {
|
|
|
171
177
|
// expression, where C's own short-circuit re-guards them. Any side effect ⇒ DECLINE the fold — the
|
|
172
178
|
// merge-variable spelling the fall-through leaves is correct (the side effect stays in B's block),
|
|
173
179
|
// just possibly non-matching.
|
|
174
|
-
if (bfeed.ops.slice(0, -1).some((op) =>
|
|
180
|
+
if (bfeed.ops.slice(0, -1).some((op) => HOIST_UNSAFE_OPS.has(op.opcode))) {
|
|
175
181
|
continue;
|
|
176
182
|
}
|
|
177
183
|
bfeed.ops.slice(0, -1).forEach(before); // hoist B's pure body (defines Vb; harmless if a dead const)
|
|
178
184
|
let condSide = cond;
|
|
179
185
|
if (wantNeg) {
|
|
180
186
|
condSide = mkValue(T.unk(32));
|
|
181
|
-
before(mkOp(
|
|
187
|
+
before(mkOp(NEGATED_ICMP[condDef.opcode], { operands: [...condDef.operands], results: [condSide] }));
|
|
182
188
|
}
|
|
183
189
|
// Vb const → the phi reduces to the (possibly negated) condition; Vb bool → a && / || connective.
|
|
184
190
|
let res = condSide;
|
|
@@ -205,3 +211,237 @@ export function recognizeShortCircuit(fn: Fn): boolean {
|
|
|
205
211
|
}
|
|
206
212
|
return changed;
|
|
207
213
|
}
|
|
214
|
+
|
|
215
|
+
// ── the CONTROL-FLOW form ───────────────────────────────────────────────────────────────────────
|
|
216
|
+
//
|
|
217
|
+
// `if (a || b) X else Y` produces no value: it is two `cond_br` blocks that SHARE a target.
|
|
218
|
+
//
|
|
219
|
+
// ^h: cond_br c1, ^X, ^g <- `a`
|
|
220
|
+
// ^g: … ; cond_br c2, ^Y, ^X <- `b` (sole predecessor ^h)
|
|
221
|
+
//
|
|
222
|
+
// Nothing in the tower recognizes that today, so the structurer reaches ^X from two arms and
|
|
223
|
+
// TAIL-DUPLICATES it — `if (a) X else { if (b') Y else X }`. The duplicate is correct C but it is
|
|
224
|
+
// not the C the compiler compiled, and the duplicated tail costs every byte it contains.
|
|
225
|
+
//
|
|
226
|
+
// The fold rewrites ^h's terminator to one `cond_br` over a connective and drops ^g. Which
|
|
227
|
+
// connective, and which successor slot, follows from WHICH of ^h's edges leads to ^g:
|
|
228
|
+
//
|
|
229
|
+
// ^g is ^h's FALL → ^g runs iff !c1, so the SHARED block is taken iff `c1 || cShared`
|
|
230
|
+
// ⇒ cond_br(logic_or(c1, cShared))[shared, other]
|
|
231
|
+
// ^g is ^h's TAKEN → ^g runs iff c1, so the OTHER block is taken iff `c1 && cOther`
|
|
232
|
+
// ⇒ cond_br(logic_and(c1, cOther))[other, shared]
|
|
233
|
+
//
|
|
234
|
+
// where `cShared`/`cOther` is ^g's own condition ORIENTED at that block — ^g's `cond_br` operand
|
|
235
|
+
// when it already branches there, otherwise its negation (so ^g's condition must be a negatable
|
|
236
|
+
// icmp, exactly as in the value form). Both spellings keep ^h's original successor ORDER for the
|
|
237
|
+
// edge that did not change, so the branch sense the frontend read out of the asm is preserved.
|
|
238
|
+
//
|
|
239
|
+
// REFUSALS (each one a real way this could be wrong, not a hypothetical):
|
|
240
|
+
//
|
|
241
|
+
// - ^g is the ENTRY block. `predecessors()` walks successor edges only — it does not model the
|
|
242
|
+
// implicit edge into `fn.blocks[0]` — so an entry block that is ALSO a loop header (its one
|
|
243
|
+
// real predecessor being its own latch) passes the sole-predecessor test below while the whole
|
|
244
|
+
// soundness argument fails for it: on the first iteration ^g runs BEFORE ^h, so hoisting ^g's
|
|
245
|
+
// body into ^h reorders it, and deleting ^g moves the entry to another block entirely. That
|
|
246
|
+
// turns an entry-guarded `while` into a `do…while` whose body runs once unconditionally —
|
|
247
|
+
// silent wrong code, caught by no contract (verify, assertResolved and assertDerefsTyped all
|
|
248
|
+
// pass). MIPS and PPC reach this: only thumb.ts inserts a synthetic preheader that would give
|
|
249
|
+
// the header a second predecessor. `retsink.ts` guards the same way (`fn.blocks[0] !== m`).
|
|
250
|
+
// - ^g has a predecessor other than ^h — folding would delete a block still reachable elsewhere.
|
|
251
|
+
// - ^g has block params — ^h's edge binds them, and dropping the edge drops the binding.
|
|
252
|
+
// - ^h and ^g both test the SAME value against CONSTANTS — that is a comparison-tree `switch`,
|
|
253
|
+
// not a hand-written `||`. switch-recover.ts requires every test's `cond_br` operand to be an
|
|
254
|
+
// `icmp` (its `isCmpOpcode` gate), and a `logic_or` is not one, so folding first PERMANENTLY
|
|
255
|
+
// disqualifies the recovery and a clean `switch (x) { case 1: case 2: … }` degrades to a chain
|
|
256
|
+
// of nested `if`s. The switch is the better recovery and it is the more specific one, so it
|
|
257
|
+
// wins the shape. Cost: a genuine source-level `x == 1 || x == 2` that is NOT part of a wider
|
|
258
|
+
// tree also declines — the same conservative trade loops.ts makes when it refuses to infer a
|
|
259
|
+
// header from `cond_br` shape.
|
|
260
|
+
// - ^g holds a side effect — its ops move into ^h, which runs UNCONDITIONALLY. A store in `b`
|
|
261
|
+
// would then execute even when `a` already decided the branch. (`a || (*p = 1)`.)
|
|
262
|
+
// - a value defined in ^g is used outside ^g, or used more than once. Then the structurer
|
|
263
|
+
// MATERIALIZES it into a local, which renders as a statement BEFORE the `if` — turning `b`'s
|
|
264
|
+
// conditional computation into an unconditional one. Single-use-and-local is precisely the
|
|
265
|
+
// shape analysis.ts inlines into the connective's right operand, where C's own short-circuit
|
|
266
|
+
// re-guards it. This is what keeps a load in `b` from being hoisted across the guard in `a`.
|
|
267
|
+
// - the two edges into the shared block carry DIFFERENT args. Only one edge survives the fold,
|
|
268
|
+
// so it can only carry one argument list; picking either would silently drop the other path's
|
|
269
|
+
// phi input.
|
|
270
|
+
// - ^g's two successors are the same block, or ^g's condition is not a negatable icmp when the
|
|
271
|
+
// orientation needs negating.
|
|
272
|
+
//
|
|
273
|
+
// Every refusal falls through untouched, leaving the tail-duplicated spelling — a miss, never a
|
|
274
|
+
// miscompile. Applied ITERATIVELY, so `a || b || c` folds left-to-right, each round consuming one
|
|
275
|
+
// more condition block.
|
|
276
|
+
export function recognizeBranchShortCircuit(fn: Fn): boolean {
|
|
277
|
+
let changed = false;
|
|
278
|
+
const term = (b: Block) => b.ops[b.ops.length - 1];
|
|
279
|
+
let progress = true;
|
|
280
|
+
while (progress) {
|
|
281
|
+
progress = false;
|
|
282
|
+
const defs = defOpMap(fn);
|
|
283
|
+
const preds = predecessors(fn);
|
|
284
|
+
outer: for (const h of fn.blocks) {
|
|
285
|
+
const ht = term(h);
|
|
286
|
+
if (ht.opcode !== 'cond_br') {
|
|
287
|
+
continue;
|
|
288
|
+
}
|
|
289
|
+
const [taken, fall] = ht.successors;
|
|
290
|
+
// Try ^g = the fall edge, then ^g = the taken edge. `gIsFall` picks the connective.
|
|
291
|
+
for (const gIsFall of [true, false]) {
|
|
292
|
+
const gEdge = gIsFall ? fall : taken;
|
|
293
|
+
const sharedFromH = gIsFall ? taken : fall;
|
|
294
|
+
const g = gEdge.block;
|
|
295
|
+
if (g === h || g === sharedFromH.block || g.params.length > 0) {
|
|
296
|
+
continue;
|
|
297
|
+
}
|
|
298
|
+
// The ENTRY block is never ^g — see the REFUSALS note. `predecessors()` cannot see the
|
|
299
|
+
// implicit entry edge, so this is the only thing standing between an entry-block loop
|
|
300
|
+
// header and a silently reordered function body.
|
|
301
|
+
if (g === fn.blocks[0]) {
|
|
302
|
+
continue;
|
|
303
|
+
}
|
|
304
|
+
if ((preds.get(g) ?? []).length !== 1) {
|
|
305
|
+
continue;
|
|
306
|
+
}
|
|
307
|
+
const gt = term(g);
|
|
308
|
+
if (gt.opcode !== 'cond_br') {
|
|
309
|
+
continue;
|
|
310
|
+
}
|
|
311
|
+
const [gTaken, gFall] = gt.successors;
|
|
312
|
+
if (gTaken.block === gFall.block) {
|
|
313
|
+
continue;
|
|
314
|
+
}
|
|
315
|
+
// A comparison TREE over one scrutinee belongs to switch recovery, not to this fold.
|
|
316
|
+
if (sameScrutineeConstTests(defs, ht.operands[0], gt.operands[0])) {
|
|
317
|
+
continue;
|
|
318
|
+
}
|
|
319
|
+
// ^g's body must be pure, and every value it defines must be consumed only by ^g itself —
|
|
320
|
+
// see the REFUSALS note: an escaping or reused value becomes a statement hoisted out of the
|
|
321
|
+
// short circuit.
|
|
322
|
+
// HOIST_UNSAFE_OPS includes `opaque`: an instruction asmlift could not model, and moving it
|
|
323
|
+
// out of the arm that guards it is the reordering this refuses. Loud either way today — a
|
|
324
|
+
// decline under `onGap: 'strict'`, an ASMLIFT_ERROR marker under `annotate`.
|
|
325
|
+
const body = g.ops.slice(0, -1);
|
|
326
|
+
if (body.some((op) => HOIST_UNSAFE_OPS.has(op.opcode))) {
|
|
327
|
+
continue;
|
|
328
|
+
}
|
|
329
|
+
if (!definedValuesStayLocal(fn, g)) {
|
|
330
|
+
continue;
|
|
331
|
+
}
|
|
332
|
+
// Which of ^g's edges rejoins ^h's other successor? That is the shared block.
|
|
333
|
+
const sharedEdge =
|
|
334
|
+
gTaken.block === sharedFromH.block ? gTaken : gFall.block === sharedFromH.block ? gFall : null;
|
|
335
|
+
if (!sharedEdge) {
|
|
336
|
+
continue;
|
|
337
|
+
}
|
|
338
|
+
const otherEdge = sharedEdge === gTaken ? gFall : gTaken;
|
|
339
|
+
if (!sameArgs(sharedFromH.args, sharedEdge.args)) {
|
|
340
|
+
continue;
|
|
341
|
+
}
|
|
342
|
+
// The second operand, oriented at the block whose slot it decides: `logic_or` asks "does ^g
|
|
343
|
+
// reach the SHARED block", `logic_and` asks "does ^g reach the OTHER block".
|
|
344
|
+
const wantEdge = gIsFall ? sharedEdge : otherEdge;
|
|
345
|
+
const c2 = gt.operands[0];
|
|
346
|
+
const c2Def = defs.get(c2);
|
|
347
|
+
let second = c2;
|
|
348
|
+
const negated: Op[] = [];
|
|
349
|
+
if (wantEdge !== gTaken) {
|
|
350
|
+
if (!c2Def || !NEGATED_ICMP[c2Def.opcode]) {
|
|
351
|
+
continue;
|
|
352
|
+
}
|
|
353
|
+
second = mkValue(T.unk(32));
|
|
354
|
+
negated.push(mkOp(NEGATED_ICMP[c2Def.opcode], { operands: [...c2Def.operands], results: [second] }));
|
|
355
|
+
}
|
|
356
|
+
const res = mkValue(T.unk(32));
|
|
357
|
+
const connective = mkOp(gIsFall ? 'logic_or' : 'logic_and', {
|
|
358
|
+
operands: [ht.operands[0], second],
|
|
359
|
+
results: [res],
|
|
360
|
+
});
|
|
361
|
+
// ^g's body moves ahead of ^h's terminator; ^h keeps the successor SLOT that did not change
|
|
362
|
+
// (taken=shared for `||`, taken=other for `&&`), so the frontend's branch sense survives.
|
|
363
|
+
h.ops.splice(h.ops.length - 1, 1, ...body, ...negated, connective, {
|
|
364
|
+
...mkOp('cond_br', { operands: [res] }),
|
|
365
|
+
successors: gIsFall
|
|
366
|
+
? [
|
|
367
|
+
{ block: sharedEdge.block, args: [...sharedEdge.args] },
|
|
368
|
+
{ block: otherEdge.block, args: [...otherEdge.args] },
|
|
369
|
+
]
|
|
370
|
+
: [
|
|
371
|
+
{ block: otherEdge.block, args: [...otherEdge.args] },
|
|
372
|
+
{ block: sharedEdge.block, args: [...sharedEdge.args] },
|
|
373
|
+
],
|
|
374
|
+
});
|
|
375
|
+
fn.blocks = fn.blocks.filter((x) => x !== g);
|
|
376
|
+
changed = true;
|
|
377
|
+
progress = true;
|
|
378
|
+
break outer; // defs/preds are stale after the mutation — recompute on the next round
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
return changed;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/** Do `c1` and `c2` compare the SAME value against CONSTANTS? That is the signature of a
|
|
386
|
+
* comparison-tree `switch`, which switch-recover.ts owns — see the REFUSALS note. Equality tests
|
|
387
|
+
* only: a switch tree dispatches on `==`/`!=`, while a RELATIONAL pair (`x >= lo && x <= hi`, the
|
|
388
|
+
* range check) is a genuine connective this fold should still take. */
|
|
389
|
+
function sameScrutineeConstTests(defs: Map<Value, Op>, c1: Value, c2: Value): boolean {
|
|
390
|
+
const eqTest = (v: Value): { scrutinee: Value } | null => {
|
|
391
|
+
const d = defs.get(v);
|
|
392
|
+
if (!d || (d.opcode !== 'icmp_eq' && d.opcode !== 'icmp_ne')) {
|
|
393
|
+
return null;
|
|
394
|
+
}
|
|
395
|
+
const [x, y] = d.operands;
|
|
396
|
+
const xc = defs.get(x)?.opcode === 'const';
|
|
397
|
+
const yc = defs.get(y)?.opcode === 'const';
|
|
398
|
+
// exactly one side constant — `x == y` between two variables is no switch test
|
|
399
|
+
return xc === yc ? null : { scrutinee: xc ? y : x };
|
|
400
|
+
};
|
|
401
|
+
const a = eqTest(c1);
|
|
402
|
+
const b = eqTest(c2);
|
|
403
|
+
return a !== null && b !== null && a.scrutinee === b.scrutinee;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/** True when every value `g` defines is read at most once, and any read is inside `g`.
|
|
407
|
+
*
|
|
408
|
+
* The VALUE form above needs no such check, and the asymmetry is real rather than drift: its feeder
|
|
409
|
+
* ends in `br M`, so the feeder has no successor of its own to dominate and every value it defines
|
|
410
|
+
* is either read in the feeder or carried to `M` as the phi argument the fold consumes. Here ^g
|
|
411
|
+
* ends in `cond_br` and its `other` successor IS ^g-dominated, so a ^g-defined value genuinely can
|
|
412
|
+
* escape, and only this check stops it.
|
|
413
|
+
*
|
|
414
|
+
* An earlier version of this note justified the asymmetry by "the feeder dominates nothing but
|
|
415
|
+
* itself because M has 2+ predecessors", and told the reader not to unify the guards. That was
|
|
416
|
+
* WRONG — the entry block dominates every block whatever M's predecessor count — and it was wrong
|
|
417
|
+
* about the one guard the two folds genuinely DO share, the `fn.blocks[0]` refusal, which the value
|
|
418
|
+
* form was missing entirely. Both now have it. When changing either fold, check the other. */
|
|
419
|
+
function definedValuesStayLocal(fn: Fn, g: Block): boolean {
|
|
420
|
+
const defined = new Set<Value>(g.ops.flatMap((op) => op.results));
|
|
421
|
+
if (defined.size === 0) {
|
|
422
|
+
return true;
|
|
423
|
+
}
|
|
424
|
+
const uses = new Map<Value, number>();
|
|
425
|
+
for (const b of fn.blocks) {
|
|
426
|
+
for (const op of b.ops) {
|
|
427
|
+
for (const v of [...op.operands, ...op.successors.flatMap((s) => s.args)]) {
|
|
428
|
+
if (!defined.has(v)) {
|
|
429
|
+
continue;
|
|
430
|
+
}
|
|
431
|
+
if (b !== g) {
|
|
432
|
+
return false; // escapes ^g — the structurer would render it before the `if`
|
|
433
|
+
}
|
|
434
|
+
uses.set(v, (uses.get(v) ?? 0) + 1);
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
// ZERO uses is fine — a dead op renders nothing at all, so it cannot escape the short circuit.
|
|
439
|
+
// TWO or more is not: analysis.ts materializes a multi-consumer value into a local, which is a
|
|
440
|
+
// statement, and a statement lands before the `if`.
|
|
441
|
+
return [...defined].every((v) => (uses.get(v) ?? 0) <= 1);
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
/** Two successor argument lists that a fold may collapse into one: same values, same order. */
|
|
445
|
+
function sameArgs(a: Value[], b: Value[]): boolean {
|
|
446
|
+
return a.length === b.length && a.every((v, i) => v === b[i]);
|
|
447
|
+
}
|
|
@@ -87,7 +87,8 @@ function withPadding(dataFields: StructField[], stride: number): StructField[] {
|
|
|
87
87
|
* rematerializes the same element address (several `add(base, i*stride)` ops for one logical
|
|
88
88
|
* array), and recovering them one-by-one would let the first claim the base and force its
|
|
89
89
|
* twins to decline — a mixed spelling that is worse than either pure form (found live on
|
|
90
|
-
* pokeemerald:
|
|
90
|
+
* pokeemerald:GetGenderFromSpeciesAndPersonality, whose address is materialized twice). A base
|
|
91
|
+
* whose element pointers
|
|
91
92
|
* disagree on stride declines entirely: two strides over one base is a reinterpreted view or
|
|
92
93
|
* a 2D layout, genuinely ambiguous — decline over guess. */
|
|
93
94
|
export function recognizeStructArrays(fn: Fn): number {
|
package/src/raise/structs.ts
CHANGED
|
@@ -178,6 +178,17 @@ export function recognizeStructs(fn: Fn): number {
|
|
|
178
178
|
}
|
|
179
179
|
}
|
|
180
180
|
|
|
181
|
+
// Which values are the address of a NAMED global (`gaddr`)? Consulted only when synthesis
|
|
182
|
+
// DECLINES: see the catch below.
|
|
183
|
+
const namedGlobal = new Set<Value>();
|
|
184
|
+
for (const b of fn.blocks) {
|
|
185
|
+
for (const op of b.ops as Op[]) {
|
|
186
|
+
if (op.opcode === 'gaddr') {
|
|
187
|
+
namedGlobal.add(op.results[0]);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
181
192
|
let count = 0;
|
|
182
193
|
for (const base of order) {
|
|
183
194
|
if (arrayBases.has(base)) {
|
|
@@ -186,18 +197,45 @@ export function recognizeStructs(fn: Fn): number {
|
|
|
186
197
|
if (base.type.kind !== 'unknown') {
|
|
187
198
|
continue;
|
|
188
199
|
} // already typed (not a bare recovery target)
|
|
200
|
+
|
|
189
201
|
const accesses = accessesOf.get(base)!;
|
|
190
202
|
if (isArray(accesses)) {
|
|
191
203
|
continue;
|
|
192
204
|
} // uniform stride / single aligned access → array
|
|
193
|
-
|
|
205
|
+
try {
|
|
206
|
+
base.type = T.ptr(buildStruct(`Struct${count}`, accesses));
|
|
207
|
+
} catch (e) {
|
|
208
|
+
// A NAMED global whose accesses synthesis cannot reconcile is not a reason to decline the
|
|
209
|
+
// function: its declaration belongs to the project's own headers, and its constant-offset
|
|
210
|
+
// accesses render at L3 through the symbol context (member spelling when the map knows the
|
|
211
|
+
// layout, the honest cast spelling when it does not). The inhabitant is agbcc FUSING two
|
|
212
|
+
// adjacent u8 compares into one ldrh — `s.level == 8 && s.world == 6` reads offset 12 at
|
|
213
|
+
// widths 1 AND 2, which is not a union, just two spellings of declared bytes. An ANONYMOUS
|
|
214
|
+
// base (a loaded pointer, a parameter) has no other source of truth, so for it the decline
|
|
215
|
+
// stands exactly as before — this catch narrows nothing for the shapes that already worked,
|
|
216
|
+
// because a base synthesis succeeds on takes the same path it always took.
|
|
217
|
+
if (e instanceof RaiseUnsupportedError && namedGlobal.has(base)) {
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
220
|
+
throw e;
|
|
221
|
+
}
|
|
194
222
|
count++;
|
|
195
223
|
}
|
|
196
224
|
return count;
|
|
197
225
|
}
|
|
198
226
|
|
|
199
|
-
/** The distinct struct types this function
|
|
200
|
-
* deduped by name and sorted, for the backend to declare above the function.
|
|
227
|
+
/** The distinct struct types this function's L2 GRAPH mentions (unwrapping struct pointers on every
|
|
228
|
+
* value), deduped by name and sorted, for the backend to declare above the function.
|
|
229
|
+
*
|
|
230
|
+
* "Mentions", not "references": this walks `fn.blocks` at the moment structuring runs, and the
|
|
231
|
+
* result is CACHED on the SFn (`structure.ts`) and carried by every later `{...sfn}` pass, so a
|
|
232
|
+
* struct whose last use a subsequent L3 pass removed would still be declared. Sibling `locals` is
|
|
233
|
+
* reference-pruned after dead-store elimination (`l3/dce.ts`) for exactly that reason; this list is
|
|
234
|
+
* not. No pass drops such a use today — the IR-level DCE runs long before recognition, and l3/dce's
|
|
235
|
+
* `mustKeep` never drops a `field`/`index` — so the staleness is LATENT, not live, which is why it
|
|
236
|
+
* is recorded here rather than fixed speculatively. The fix, if a pass ever makes it reachable, is
|
|
237
|
+
* the one l3/symbol-refs.ts already applies to symbol references: derive at the consumption point
|
|
238
|
+
* (`backend/cfamily.ts` structDecls) instead of caching. */
|
|
201
239
|
export function collectStructs(fn: Fn): StructType[] {
|
|
202
240
|
const seen = new Map<string, StructType>();
|
|
203
241
|
const consider = (t: IrType) => {
|