@asmlift/core 0.1.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/LICENSE +21 -0
- package/README.md +148 -0
- package/package.json +14 -0
- package/src/backend/c.ts +20 -0
- package/src/backend/cfamily.ts +352 -0
- package/src/backend/cpp.ts +145 -0
- package/src/backend/pascal.ts +279 -0
- package/src/contracts.ts +131 -0
- package/src/detect.ts +12 -0
- package/src/frontend/asmdata.ts +170 -0
- package/src/frontend/disasm.ts +102 -0
- package/src/frontend/emit.ts +57 -0
- package/src/frontend/errors.ts +14 -0
- package/src/frontend/format.ts +47 -0
- package/src/frontend/frontend.ts +22 -0
- package/src/frontend/mips.ts +875 -0
- package/src/frontend/opaque.ts +82 -0
- package/src/frontend/ppc.ts +990 -0
- package/src/frontend/registry.ts +34 -0
- package/src/frontend/ssa.ts +214 -0
- package/src/frontend/thumb.ts +1419 -0
- package/src/ir/core.ts +104 -0
- package/src/ir/opcodes.ts +143 -0
- package/src/ir/parse.ts +221 -0
- package/src/ir/print.ts +77 -0
- package/src/ir/types.ts +106 -0
- package/src/ir/verify.ts +221 -0
- package/src/l3/ast.ts +301 -0
- package/src/l3/basecse.ts +218 -0
- package/src/l3/dce.ts +256 -0
- package/src/l3/regspell.ts +331 -0
- package/src/l3/reindex.ts +447 -0
- package/src/l3/typing.ts +145 -0
- package/src/mangle.ts +135 -0
- package/src/pattern/engine.ts +392 -0
- package/src/pipeline.ts +272 -0
- package/src/proto.ts +42 -0
- package/src/raise/arrays.ts +84 -0
- package/src/raise/const.ts +52 -0
- package/src/raise/errors.ts +10 -0
- package/src/raise/magicdiv.ts +386 -0
- package/src/raise/pre-recovery.ts +71 -0
- package/src/raise/recover.ts +215 -0
- package/src/raise/retsink.ts +72 -0
- package/src/raise/shortcircuit.ts +207 -0
- package/src/raise/softdiv.ts +62 -0
- package/src/raise/struct-arrays.ts +257 -0
- package/src/raise/structs.ts +223 -0
- package/src/rank.ts +208 -0
- package/src/structure/analysis.ts +410 -0
- package/src/structure/hazards.ts +142 -0
- package/src/structure/loops.ts +169 -0
- package/src/structure/structure.ts +1726 -0
- package/src/structure/switch-recover.ts +410 -0
- package/src/target.ts +140 -0
- package/src/trace.ts +233 -0
|
@@ -0,0 +1,1726 @@
|
|
|
1
|
+
// asmlift — L2→L3 structuring: CFG + block-argument SSA → a structured AST.
|
|
2
|
+
//
|
|
3
|
+
// Three jobs:
|
|
4
|
+
// 1. SSA destruction WITH COALESCING — a merge block-argument that already carries a
|
|
5
|
+
// variable's value on one path is coalesced to that variable, so only the
|
|
6
|
+
// non-identity paths emit an assignment (reproducing agbcc's register allocation:
|
|
7
|
+
// NOTE the coupled INVERSE: l3/regspell.ts re-derives the UN-coalesced copy-carrying
|
|
8
|
+
// spelling as a ranked candidate — its R1 template matches THIS pass's diamond output
|
|
9
|
+
// shape, so a change to coalescing here can silently stop that lever firing (the
|
|
10
|
+
// matching-suite regspell gate is what makes the coupling loud).
|
|
11
|
+
// the clamp0 diamond becomes `if (x < 0) x = 0; return x;` rather than a temp copy).
|
|
12
|
+
// Coalescing is INTERFERENCE-CHECKED against per-block value liveness, and
|
|
13
|
+
// inline-at-use rendering carries an effect-ordering model: a call/load that cannot
|
|
14
|
+
// soundly render at its use is MATERIALIZED as a named temp at its own program
|
|
15
|
+
// position. Where no correct spelling exists, the structurer declines loud
|
|
16
|
+
// (StructureError) — never silent wrong code.
|
|
17
|
+
// 2. If-recovery over the CFG using immediate post-dominators as merge points, with an
|
|
18
|
+
// empty-then peephole (negate + swap).
|
|
19
|
+
// 3. Loop-recovery: a back-edge (edge into a dominating header) is recognised, and the
|
|
20
|
+
// gcc "guard + do-while" lowering is un-rotated back into a `while` — the loop
|
|
21
|
+
// condition is the latch test read on the header's OWN parameters (back-edge args
|
|
22
|
+
// substituted back to the phi they feed), and the loop body is the header's
|
|
23
|
+
// parallel block-argument update, sequentialised so an assignment never clobbers a
|
|
24
|
+
// value a later one still needs.
|
|
25
|
+
//
|
|
26
|
+
// Module layout: loop DISCOVERY is loops.ts; the pure ANALYSIS phase (use registry, liveness,
|
|
27
|
+
// materialization) is analysis.ts; comparison-tree switch recovery is switch-recover.ts
|
|
28
|
+
// (explicit-deps factory). THIS file keeps the mutually-entangled remainder: SSA-destruction
|
|
29
|
+
// coalescing (canTakeName + seeding) and emission (structureBlock + the loop emitters) — they
|
|
30
|
+
// share varName/backArgName mutation and the activeSub/loopCtx dynamic state.
|
|
31
|
+
//
|
|
32
|
+
// Scope: reducible single-latch natural loops — GUARDED self-loop `while` (the guard-fusion
|
|
33
|
+
// un-rotation), UNGUARDED self-loop `do-while` (single block, header === latch), test-at-top
|
|
34
|
+
// `while`, bottom-test `do-while`, PROPERLY-nested loops, in-body `break`/early-`return`,
|
|
35
|
+
// comparison-tree and jump-table `switch`. Still DECLINED (loud StructureError, never wrong
|
|
36
|
+
// code): multi-latch headers, irreducible/overlapping loops, conditional `continue`, a `break`
|
|
37
|
+
// whose exit copies would clobber, switch fall-through, and mixed-entry self-loops (a guarded
|
|
38
|
+
// header also entered by a plain br).
|
|
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';
|
|
42
|
+
import { exprCType, ptrElemBytes } from '../l3/typing';
|
|
43
|
+
import { returnType } from '../raise/recover';
|
|
44
|
+
import { collectStructs } from '../raise/structs';
|
|
45
|
+
import { analyze } from './analysis';
|
|
46
|
+
import { makeLoopHazards, updateWriteSet } from './hazards';
|
|
47
|
+
import { analyzeLoops, dominators } from './loops';
|
|
48
|
+
import { makeSwitchRecovery } from './switch-recover';
|
|
49
|
+
|
|
50
|
+
// Lower a constant-offset memory access to its lvalue/rvalue Expr. If the base was recovered as a
|
|
51
|
+
// struct pointer (raise/structs.ts), the byte offset resolves to a NAMED field (`base->field_<off>`);
|
|
52
|
+
// otherwise it stays the width-scaled array index (`base[off/width]`, `*base` for offset 0).
|
|
53
|
+
//
|
|
54
|
+
// The scalar path builds a WIDTH-CARRYING `index` node and inserts NO cast: each backend
|
|
55
|
+
// legalizes the base itself from the node's width (the C family inserts the reinterpret cast at
|
|
56
|
+
// print time when the base's rendered type does not stride the width — derefStrideOk, l3/typing —
|
|
57
|
+
// and Pascal loud-declines instead). The struct path still bakes its cast into the tree: the
|
|
58
|
+
// legalization target is the RECOVERED struct pointer type, which the `field` node does not carry
|
|
59
|
+
// TODAY — carrying the struct name (resolved against SFn.structs) is the same move as width and
|
|
60
|
+
// the named follow-up; until then no backend pays a tax for the tree cast (Pascal loud-fails
|
|
61
|
+
// `field` regardless, C++ falls through its leaf hook to the shared C spelling).
|
|
62
|
+
// If `e` is a global address `&gSym` (optionally `+ index`), return the global name and the
|
|
63
|
+
// element index (byte residual divided by the access width). `&gSym` alone → idx const 0;
|
|
64
|
+
// `&gSym + i` → idx `i / width` (exact division only — a non-multiple residual is a mid-element
|
|
65
|
+
// access this whole-global spelling can't express, so it declines to null and the caller casts).
|
|
66
|
+
function globalOf(e: Expr, width: number): { name: string; idx: Expr } | null {
|
|
67
|
+
if (e.k === 'addr') {
|
|
68
|
+
return { name: e.name, idx: { k: 'const', value: 0 } };
|
|
69
|
+
}
|
|
70
|
+
if (e.k === 'bin' && e.op === '+') {
|
|
71
|
+
for (const [addrSide, other] of [
|
|
72
|
+
[e.l, e.r],
|
|
73
|
+
[e.r, e.l],
|
|
74
|
+
] as const) {
|
|
75
|
+
if (addrSide.k === 'addr') {
|
|
76
|
+
// width 1 → the byte residual IS the index; width>1 → a constant residual divides, a
|
|
77
|
+
// non-constant residual must already be element-scaled (`i * width`) to divide exactly.
|
|
78
|
+
if (width === 1) {
|
|
79
|
+
return { name: addrSide.name, idx: other };
|
|
80
|
+
}
|
|
81
|
+
if (other.k === 'const') {
|
|
82
|
+
return other.value % width === 0
|
|
83
|
+
? { name: addrSide.name, idx: { k: 'const', value: other.value / width } }
|
|
84
|
+
: null;
|
|
85
|
+
}
|
|
86
|
+
if (other.k === 'bin' && (other.op === '*' || other.op === '<<')) {
|
|
87
|
+
const factor =
|
|
88
|
+
other.op === '<<'
|
|
89
|
+
? other.r.k === 'const'
|
|
90
|
+
? 1 << other.r.value
|
|
91
|
+
: 0
|
|
92
|
+
: other.r.k === 'const'
|
|
93
|
+
? other.r.value
|
|
94
|
+
: 0;
|
|
95
|
+
if (factor === width) {
|
|
96
|
+
return { name: addrSide.name, idx: other.l };
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return null; // a non-element-aligned residual — decline the global-array spelling
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function memAccess(
|
|
107
|
+
base: Value,
|
|
108
|
+
baseExpr: Expr,
|
|
109
|
+
off: number,
|
|
110
|
+
width: number,
|
|
111
|
+
signed: boolean,
|
|
112
|
+
ctype: (e: Expr) => IrType | undefined,
|
|
113
|
+
scalarGlobals: Set<string>,
|
|
114
|
+
): Expr {
|
|
115
|
+
// A deref of a global's address collapses to the bare global: `*(&gSym)` at off 0 is `gSym`;
|
|
116
|
+
// at off N the global is an array — `gSym[N/width]` (a C global name decays to a pointer, so
|
|
117
|
+
// the index reproduces the offset). A `+`-tree base holding `&gSym` is a global-ARRAY element
|
|
118
|
+
// `*(&gSym + i)` → `gSym[i + off/width]` (byte offset `i` peeled from the tree; for a u8 global
|
|
119
|
+
// the residual IS the index). This is what makes an agbcc `.word gSym` pool access a named
|
|
120
|
+
// global read/element rather than a phantom-pointer deref.
|
|
121
|
+
const g = globalOf(baseExpr, width);
|
|
122
|
+
if (g) {
|
|
123
|
+
const idxVal = g.idx;
|
|
124
|
+
// off-0 access of a SCALAR global (accessed only at offset 0, per scalarGlobals) → the BARE
|
|
125
|
+
// global `gSym` (byte-exact, matches the source spelling). Any other access — a non-zero
|
|
126
|
+
// offset, a variable index, or an AGGREGATE global (accessed at multiple offsets) — indexes
|
|
127
|
+
// the global's ADDRESS `&gSym`, NOT the bare value: a struct global does not decay, so
|
|
128
|
+
// `((s32 *)gSym)[i]` is invalid C, but `((s32 *)&gSym)[i]` reinterpret-casts the address and
|
|
129
|
+
// strides correctly for BOTH a struct and an array global.
|
|
130
|
+
if (off === 0 && idxVal.k === 'const' && idxVal.value === 0 && scalarGlobals.has(g.name)) {
|
|
131
|
+
return { k: 'var', name: g.name };
|
|
132
|
+
}
|
|
133
|
+
const idx: Expr =
|
|
134
|
+
off === 0
|
|
135
|
+
? idxVal
|
|
136
|
+
: idxVal.k === 'const'
|
|
137
|
+
? { k: 'const', value: idxVal.value + off / width }
|
|
138
|
+
: { k: 'bin', op: '+', l: idxVal, r: { k: 'const', value: off / width } };
|
|
139
|
+
return { k: 'index', base: { k: 'addr', name: g.name }, idx, width, signed };
|
|
140
|
+
}
|
|
141
|
+
const bt = base.type;
|
|
142
|
+
if (bt.kind === 'ptr' && bt.to.kind === 'struct') {
|
|
143
|
+
const rt = ctype(baseExpr);
|
|
144
|
+
// `->` requires the base to render as a pointer to THIS struct (field names resolve against
|
|
145
|
+
// its declaration) AND as a non-`index` node (the printer spells an index-node base with `.`,
|
|
146
|
+
// the array-element form — wrong for a pointer). Anything else is cast to the recovered
|
|
147
|
+
// struct pointer type; the cast node prints with `->`.
|
|
148
|
+
const ok = rt?.kind === 'ptr' && rt.to.kind === 'struct' && rt.to.name === bt.to.name && baseExpr.k !== 'index';
|
|
149
|
+
return { k: 'field', base: ok ? baseExpr : { k: 'cast', to: bt, e: baseExpr }, name: `field_${off}` };
|
|
150
|
+
}
|
|
151
|
+
return { k: 'index', base: baseExpr, idx: { k: 'const', value: off / width }, width, signed };
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// A variable-index array access `base[index]`, or `base[index].field_K` when a `fieldOff` marks an
|
|
155
|
+
// array-of-STRUCT element (raise/struct-arrays.ts). The `.field` on an array element prints
|
|
156
|
+
// with `.` (the printer decides dot-vs-arrow from the base being an `index` node).
|
|
157
|
+
//
|
|
158
|
+
// Scalar path: a width-carrying `index` node, no cast — the backend legalizes (see memAccess).
|
|
159
|
+
// Struct-array path: like memAccess's struct path, the recovered struct pointer type is an L2
|
|
160
|
+
// fact the AST cannot carry, so a base that does not render as THAT struct pointer is cast here
|
|
161
|
+
// (C then scales the index by the struct size, exactly the aload/astore element stride); the
|
|
162
|
+
// `index` node's width is the struct size only nominally — strideOk never fires on struct
|
|
163
|
+
// pointees, so the C backend leaves a struct-typed base uncast and the tree-level cast governs.
|
|
164
|
+
function arrayAccess(
|
|
165
|
+
base: Value,
|
|
166
|
+
baseExpr: Expr,
|
|
167
|
+
idxExpr: Expr,
|
|
168
|
+
fieldOff: number | undefined,
|
|
169
|
+
elemSize: number,
|
|
170
|
+
signed: boolean,
|
|
171
|
+
ctype: (e: Expr) => IrType | undefined,
|
|
172
|
+
): Expr {
|
|
173
|
+
// A variable-index access off a global's address indexes the ADDRESS `&gSym` (the cast form
|
|
174
|
+
// `((T *)&gSym)[i]` — valid for a struct global too, unlike casting the bare value). A
|
|
175
|
+
// struct-array-of-globals (fieldOff) through `&gSym` is out of scope — fall through.
|
|
176
|
+
if (baseExpr.k === 'addr' && fieldOff === undefined) {
|
|
177
|
+
return { k: 'index', base: baseExpr, idx: idxExpr, width: elemSize, signed };
|
|
178
|
+
}
|
|
179
|
+
const bt = base.type;
|
|
180
|
+
if (fieldOff !== undefined) {
|
|
181
|
+
const structTo = bt.kind === 'ptr' && bt.to.kind === 'struct' ? bt.to : null;
|
|
182
|
+
const rt = ctype(baseExpr);
|
|
183
|
+
const ok = structTo !== null && rt?.kind === 'ptr' && rt.to.kind === 'struct' && rt.to.name === structTo.name;
|
|
184
|
+
// an ill-typed struct-array base with no recovered struct type has no derivable cast target:
|
|
185
|
+
// left as rendered — assertDerefsTyped's FIELD rule flags the definite violations at the
|
|
186
|
+
// stage boundary (the dot-form field types non-struct there).
|
|
187
|
+
const b = ok || structTo === null ? baseExpr : { k: 'cast' as const, to: T.ptr(structTo), e: baseExpr };
|
|
188
|
+
const index: Expr = { k: 'index', base: b, idx: idxExpr, width: elemSize, signed };
|
|
189
|
+
return { k: 'field', base: index, name: `field_${fieldOff}` };
|
|
190
|
+
}
|
|
191
|
+
return { k: 'index', base: baseExpr, idx: idxExpr, width: elemSize, signed };
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Raised when the CFG contains control flow the structurer cannot recover (see the module scope
|
|
195
|
+
// note above for what IS recovered vs declined). It is an explicit, catchable "out of scope"
|
|
196
|
+
// signal — NOT a bug — so callers fail loud with a diagnostic instead of stack-overflowing.
|
|
197
|
+
export class StructureError extends Error {
|
|
198
|
+
constructor(message: string) {
|
|
199
|
+
super(message);
|
|
200
|
+
this.name = 'StructureError';
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// THE one copy of the guard shape (like fieldSpellsDot/derefStrideOk): a predecessor whose
|
|
205
|
+
// cond_br decides "enter `header` vs its `exit`". The self-loop DISCOVERY classifies ownership
|
|
206
|
+
// with it, and the guard-FUSION site consumes the same shape (its `takenB/fallB === li.exit`
|
|
207
|
+
// check is this predicate from the branch's own viewpoint) — a drift between the two is
|
|
208
|
+
// fail-safe (traced: either an onStack decline or an unfused `if (g) do…while` spelling, never
|
|
209
|
+
// wrong code) but wastes capability, so both keep pointing here.
|
|
210
|
+
function isGuardShapedPred(pred: Block, header: Block, exit: Block): boolean {
|
|
211
|
+
if (pred === header) {
|
|
212
|
+
return false;
|
|
213
|
+
}
|
|
214
|
+
const t = pred.ops[pred.ops.length - 1];
|
|
215
|
+
return (
|
|
216
|
+
t.opcode === 'cond_br' &&
|
|
217
|
+
t.successors.some((sx) => sx.block === header) &&
|
|
218
|
+
t.successors.some((sx) => sx.block === exit)
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const CMP_TO_BIN: Record<string, BinOp> = {
|
|
223
|
+
icmp_slt: '<',
|
|
224
|
+
icmp_sle: '<=',
|
|
225
|
+
icmp_sgt: '>',
|
|
226
|
+
icmp_sge: '>=',
|
|
227
|
+
icmp_ult: '<',
|
|
228
|
+
icmp_ule: '<=',
|
|
229
|
+
icmp_ugt: '>',
|
|
230
|
+
icmp_uge: '>=', // unsignedness is in the operand types
|
|
231
|
+
icmp_eq: '==',
|
|
232
|
+
icmp_ne: '!=',
|
|
233
|
+
};
|
|
234
|
+
const ARITH_TO_BIN: Record<string, BinOp> = {
|
|
235
|
+
add: '+',
|
|
236
|
+
sub: '-',
|
|
237
|
+
mul: '*',
|
|
238
|
+
sdiv: '/',
|
|
239
|
+
udiv: '/',
|
|
240
|
+
smod: '%',
|
|
241
|
+
umod: '%',
|
|
242
|
+
or: '|',
|
|
243
|
+
and: '&',
|
|
244
|
+
xor: '^',
|
|
245
|
+
shl: '<<',
|
|
246
|
+
shr_u: '>>',
|
|
247
|
+
shr_s: '>>',
|
|
248
|
+
logic_and: '&&',
|
|
249
|
+
logic_or: '||', // short-circuit connectives (raise/shortcircuit.ts)
|
|
250
|
+
};
|
|
251
|
+
const NEGATE: Record<string, BinOp> = { '<': '>=', '>=': '<', '>': '<=', '<=': '>', '==': '!=', '!=': '==' };
|
|
252
|
+
|
|
253
|
+
// Recovered info for a self-loop header: its exit block and the per-parameter back-edge
|
|
254
|
+
// arg it feeds (the value on the header→header edge). The back-edge arg is the "next"
|
|
255
|
+
// value of the phi; mapping it back to the phi turns the latch test into the while test.
|
|
256
|
+
interface LoopInfo {
|
|
257
|
+
header: Block;
|
|
258
|
+
exit: Block;
|
|
259
|
+
backArgOfParam: Value[]; // index-aligned with header.params
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// A test-at-top multi-block `while`. The header is a pure test whose cond_br enters `bodyEntry`
|
|
263
|
+
// (inside the loop) or leaves to `exit` (the single loop exit). Unlike LoopInfo the condition reads
|
|
264
|
+
// the header's params directly (top-of-iteration values) — no back-edge substitution.
|
|
265
|
+
interface WhileLoopInfo {
|
|
266
|
+
header: Block;
|
|
267
|
+
bodyEntry: Block;
|
|
268
|
+
exit: Block;
|
|
269
|
+
latch: Block; // the single block with the back-edge to header (its args = the update)
|
|
270
|
+
forwardPreds: Block[]; // header preds outside the loop body (the entry/init side)
|
|
271
|
+
body: Set<Block>; // the pure natural-loop body (for in-body vs exit classification)
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// A bottom-tested `do { body } while(cond)`. The header is the body entry (entered before any
|
|
275
|
+
// test); the LATCH holds the loop condition and the single exit. Body = header..latch structured, then
|
|
276
|
+
// the latch's own ops + the loop-update; the latch test is the do-while condition. The condition is
|
|
277
|
+
// read under the latch back-edge substitution (post-update the params hold their next-iteration value).
|
|
278
|
+
interface DoWhileInfo {
|
|
279
|
+
header: Block;
|
|
280
|
+
latch: Block;
|
|
281
|
+
exit: Block;
|
|
282
|
+
forwardPreds: Block[];
|
|
283
|
+
body: Set<Block>; // the pure natural-loop body (for in-body vs exit classification)
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// Structuring levers, threaded as DATA so a new one is a field here + its consumer, not a new
|
|
287
|
+
// positional boolean widened across every call site:
|
|
288
|
+
// returnsVoid — from the function's own prototype (suppress phantom r0 return);
|
|
289
|
+
// coalesceLoopInit — keep the induction var in its arg register;
|
|
290
|
+
// preserveDivergentBranchSense — reproduce source branch direction on divergent ifs;
|
|
291
|
+
// orderArgCopiesByComputation — order edge copies by computation order in the predecessor.
|
|
292
|
+
// The last three are `compilerBehaviors` (target.ts) — this pass stays target-AGNOSTIC: it reads
|
|
293
|
+
// booleans, never a compiler name.
|
|
294
|
+
export interface StructureOptions {
|
|
295
|
+
returnsVoid?: boolean;
|
|
296
|
+
coalesceLoopInit?: boolean;
|
|
297
|
+
preserveDivergentBranchSense?: boolean;
|
|
298
|
+
orderArgCopiesByComputation?: boolean;
|
|
299
|
+
// Comparison-tree switch recovery: treat an `x != K` test as a case (the EQUAL side is a case
|
|
300
|
+
// body). GCC freely uses `!=`; IDO prefers `==`/`<`. A per-compiler DATA lever, not an `arch ==`
|
|
301
|
+
// branch — default true (permissive; the decline path keeps it sound either way).
|
|
302
|
+
switchAllowsNeqCase?: boolean;
|
|
303
|
+
// How an unresolvable VALUE degrades (a live `opaque`, an unlowered transient op, a dropped def):
|
|
304
|
+
// "strict" (default) — the `"?"` sentinel, tripping assertResolved at the boundary (loud in
|
|
305
|
+
// the PROCESS);
|
|
306
|
+
// "annotate" — a `marker` node that spells as the undefined ASMLIFT_ERROR(...) symbol (loud in
|
|
307
|
+
// the ARTIFACT: the function emits complete, but cannot compile un-acknowledged).
|
|
308
|
+
onGap?: 'strict' | 'annotate';
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
export function structure(fn: Fn, opts: StructureOptions = {}): SFn {
|
|
312
|
+
const {
|
|
313
|
+
returnsVoid = false,
|
|
314
|
+
coalesceLoopInit = false,
|
|
315
|
+
preserveDivergentBranchSense = true,
|
|
316
|
+
orderArgCopiesByComputation = true,
|
|
317
|
+
switchAllowsNeqCase = true,
|
|
318
|
+
onGap = 'strict',
|
|
319
|
+
} = opts;
|
|
320
|
+
const defs = defOpMap(fn);
|
|
321
|
+
const preds = predecessorBlocks(fn);
|
|
322
|
+
const ipdom = postDominators(fn);
|
|
323
|
+
const dom = dominators(fn);
|
|
324
|
+
|
|
325
|
+
// ── analysis phase (structure/analysis.ts): use registry, liveness, materialization ──
|
|
326
|
+
const { useSitesOf, opIndex, opBlock, liveIn, materialize, reachFrom } = analyze(fn, returnsVoid);
|
|
327
|
+
|
|
328
|
+
// SCALAR-vs-AGGREGATE globals: a `gaddr` symbol accessed EXCLUSIVELY at offset 0 is a scalar
|
|
329
|
+
// global → the bare name `gSym` (byte-exact, matches the source). A symbol accessed at any
|
|
330
|
+
// non-zero offset (or via a variable index) is an array/struct global → EVERY access uses the
|
|
331
|
+
// `((T *)&gSym)[i]` address-cast form (a struct value does not decay, so casting the bare name is
|
|
332
|
+
// invalid C; casting the address is valid and byte-exact). Computed once here.
|
|
333
|
+
//
|
|
334
|
+
// Only the offset set is tracked, not width: a single symbol read at off-0 with two DIFFERENT
|
|
335
|
+
// widths is a union/type-pun, which the downstream struct-layout recovery rejects LOUD
|
|
336
|
+
// ("overlapping fields ... unions not modelled") before this classification is consumed — so a
|
|
337
|
+
// width collision at off-0 declines honestly rather than reaching a wrong bare-`gSym` emission.
|
|
338
|
+
const scalarGlobals = new Set<string>();
|
|
339
|
+
{
|
|
340
|
+
const offsets = new Map<string, Set<number>>();
|
|
341
|
+
const bumpAgg = (sym: string) => offsets.set(sym, new Set([-1])); // -1 marks "variable index"
|
|
342
|
+
for (const b of fn.blocks) {
|
|
343
|
+
for (const op of b.ops) {
|
|
344
|
+
const gaddrSym = (v: Value) => (defs.get(v)?.opcode === 'gaddr' ? (defs.get(v)!.attrs.sym as string) : null);
|
|
345
|
+
if (op.opcode === 'load' || op.opcode === 'store') {
|
|
346
|
+
const s = gaddrSym(op.operands[0]);
|
|
347
|
+
if (s) {
|
|
348
|
+
(offsets.get(s) ?? offsets.set(s, new Set()).get(s)!).add(op.attrs.off as number);
|
|
349
|
+
}
|
|
350
|
+
// a `+`-tree base holding a gaddr (global array element) is aggregate
|
|
351
|
+
const d = defs.get(op.operands[0]);
|
|
352
|
+
if (d?.opcode === 'add') {
|
|
353
|
+
for (const o of d.operands) {
|
|
354
|
+
const s2 = gaddrSym(o);
|
|
355
|
+
if (s2) {
|
|
356
|
+
bumpAgg(s2);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
} else if (op.opcode === 'aload' || op.opcode === 'astore') {
|
|
361
|
+
const s = gaddrSym(op.operands[0]);
|
|
362
|
+
if (s) {
|
|
363
|
+
bumpAgg(s);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
for (const [sym, offs] of offsets) {
|
|
369
|
+
if (offs.size === 1 && offs.has(0)) {
|
|
370
|
+
scalarGlobals.add(sym);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// --- loop discovery (loops.ts): natural loops via dominator back-edges + the nesting forest ---
|
|
376
|
+
const forest = analyzeLoops(fn, dom);
|
|
377
|
+
|
|
378
|
+
// GUARDED self-loop headers (a block that is its own successor, entered through a guard-shaped
|
|
379
|
+
// cond_br) — recovered by the guard-fusion + emitWhile un-rotation path below (the gcc "guard +
|
|
380
|
+
// do-while" → `while` shape; countdown/shifts). UNGUARDED self-loops register as single-block
|
|
381
|
+
// do-whiles in the structured-loop discovery instead. A self-loop's
|
|
382
|
+
// test and update live in ONE block, so the latch test reads the UPDATED value → emitWhile substitutes
|
|
383
|
+
// the back-edge arg back to the header param. This is DISTINCT from a test-at-top multi-block `while`
|
|
384
|
+
// (whileLoops, below) whose header is a pure test read on entry values.
|
|
385
|
+
const loops = new Map<Block, LoopInfo>();
|
|
386
|
+
for (const nl of forest.byHeader.values()) {
|
|
387
|
+
if (!nl.selfLoop) {
|
|
388
|
+
continue;
|
|
389
|
+
} // multi-block loops go through whileLoops
|
|
390
|
+
const b = nl.header;
|
|
391
|
+
const term = b.ops[b.ops.length - 1];
|
|
392
|
+
if (term.opcode !== 'cond_br') {
|
|
393
|
+
continue;
|
|
394
|
+
} // a many-way self-terminator has no single "exit"
|
|
395
|
+
const exit = term.successors.find((s) => s.block !== b)?.block;
|
|
396
|
+
if (!exit) {
|
|
397
|
+
continue;
|
|
398
|
+
}
|
|
399
|
+
// GUARDED self-loops only: some forward pred's cond_br decides "enter b vs its exit" — the
|
|
400
|
+
// shape the guard-fusion + emitWhile un-rotation below consumes. An UNGUARDED self-loop
|
|
401
|
+
// (entered by a plain br / fall-through) is a bottom-tested loop whose body always runs
|
|
402
|
+
// once — a single-block do-while — and is claimed by the structured-loop discovery below
|
|
403
|
+
// instead (each header lives in exactly ONE map, so seeding stays single-pass).
|
|
404
|
+
if (!(preds.get(b) ?? []).some((pr) => isGuardShapedPred(pr, b, exit))) {
|
|
405
|
+
continue;
|
|
406
|
+
}
|
|
407
|
+
const back = successorTo(b, b)!;
|
|
408
|
+
loops.set(b, { header: b, exit, backArgOfParam: b.params.map((_, i) => back.args[i]) });
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// --- structured natural loops (test-at-top `while` / bottom-test `do-while`) ---
|
|
412
|
+
// Both share the fail-closed preconditions: single latch, properly-nested inner loops only,
|
|
413
|
+
// reducible single-entry body, and a SINGLE real (non-ret) exit — early returns (ret-terminated
|
|
414
|
+
// targets) are allowed in-body. The shape then splits on WHERE the exit lives: the HEADER exits
|
|
415
|
+
// (pure test-at-top) → `while`; the LATCH exits (body-first) → `do-while`. Anything that fails
|
|
416
|
+
// declines to plain if-recovery, which re-enters the header and fails loud via `onStack`.
|
|
417
|
+
const isRet = (blk: Block) => blk.ops[blk.ops.length - 1]?.opcode === 'ret';
|
|
418
|
+
// A pure "return trampoline" out of the loop: forward-walking from `start` WITHOUT re-entering the
|
|
419
|
+
// loop `body`, every path terminates in a `ret` and no block on the way carries an OBSERVABLE side
|
|
420
|
+
// effect (store/astore/call/opaque). agbcc/gcc merge every `return` into ONE epilogue block and each
|
|
421
|
+
// return site just sets the return register and branches there — so a second body exit that lands on
|
|
422
|
+
// such a chain is an early RETURN, not a break to a live merge. Structuring it on more than one exit
|
|
423
|
+
// path is sound precisely because it is side-effect-free (a duplicated `return v` is harmless).
|
|
424
|
+
// This lets two returns merged through a shared `bx lr` recover as a `while` with an in-body early
|
|
425
|
+
// `return` instead of declining as "multi-exit".
|
|
426
|
+
const leadsToReturnOnly = (start: Block, body: Set<Block>): boolean => {
|
|
427
|
+
const seen = new Set<Block>();
|
|
428
|
+
const stack = [start];
|
|
429
|
+
while (stack.length) {
|
|
430
|
+
const bb = stack.pop()!;
|
|
431
|
+
if (seen.has(bb)) {
|
|
432
|
+
continue;
|
|
433
|
+
}
|
|
434
|
+
seen.add(bb);
|
|
435
|
+
if (body.has(bb)) {
|
|
436
|
+
return false;
|
|
437
|
+
} // re-enters the loop → not a pure exit
|
|
438
|
+
if (
|
|
439
|
+
bb.ops.some(
|
|
440
|
+
(op) => op.opcode === 'store' || op.opcode === 'astore' || op.opcode === 'call' || op.opcode === 'opaque',
|
|
441
|
+
)
|
|
442
|
+
) {
|
|
443
|
+
return false;
|
|
444
|
+
}
|
|
445
|
+
const t = bb.ops[bb.ops.length - 1];
|
|
446
|
+
if (t.opcode === 'ret') {
|
|
447
|
+
continue;
|
|
448
|
+
}
|
|
449
|
+
if (t.opcode === 'br' || t.opcode === 'cond_br') {
|
|
450
|
+
for (const s of t.successors) {
|
|
451
|
+
stack.push(s.block);
|
|
452
|
+
}
|
|
453
|
+
continue;
|
|
454
|
+
}
|
|
455
|
+
return false; // switch_br / unknown terminator → decline
|
|
456
|
+
}
|
|
457
|
+
return true;
|
|
458
|
+
};
|
|
459
|
+
const whileLoops = new Map<Block, WhileLoopInfo>();
|
|
460
|
+
const doWhileLoops = new Map<Block, DoWhileInfo>();
|
|
461
|
+
for (const nl of forest.byHeader.values()) {
|
|
462
|
+
const h = nl.header;
|
|
463
|
+
if (nl.selfLoop && loops.has(h)) {
|
|
464
|
+
continue;
|
|
465
|
+
} // guarded self-loops use emitWhile (above); UNGUARDED ones are single-block do-whiles
|
|
466
|
+
if (!nl.selfLoop && nl.latches.length !== 1) {
|
|
467
|
+
continue;
|
|
468
|
+
} // single latch only
|
|
469
|
+
const latch = nl.selfLoop ? h : nl.latches[0];
|
|
470
|
+
// Nested loops: an inner loop whose header sits in this body is fine ONLY if it is PROPERLY
|
|
471
|
+
// nested — its ENTIRE body is contained in ours (a forest descendant). Structuring then recurses
|
|
472
|
+
// naturally: when the outer body reaches the inner header, structureBlock dispatches to the inner's
|
|
473
|
+
// own emitWhile/emitDoWhile. An OVERLAPPING loop (shared blocks, neither containing the other →
|
|
474
|
+
// irreducible) DECLINES. If a contained inner is itself unstructurable, the outer's body
|
|
475
|
+
// structuring loud-fails at the inner back-edge (onStack) — a safe decline, not a miscompile.
|
|
476
|
+
if (
|
|
477
|
+
[...forest.byHeader.keys()].some(
|
|
478
|
+
(h2) => h2 !== h && nl.body.has(h2) && ![...forest.byHeader.get(h2)!.body].every((b) => nl.body.has(b)),
|
|
479
|
+
)
|
|
480
|
+
) {
|
|
481
|
+
continue;
|
|
482
|
+
}
|
|
483
|
+
// Reducible entry (single-entry): every body block except the header is entered ONLY from
|
|
484
|
+
// inside the body — no jump into the loop interior.
|
|
485
|
+
let reducible = true;
|
|
486
|
+
for (const bb of nl.body) {
|
|
487
|
+
if (bb === h) {
|
|
488
|
+
continue;
|
|
489
|
+
}
|
|
490
|
+
if ((preds.get(bb) ?? []).some((p) => !nl.body.has(p))) {
|
|
491
|
+
reducible = false;
|
|
492
|
+
break;
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
if (!reducible) {
|
|
496
|
+
continue;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
// Identify the loop's single STRUCTURAL exit — where the loop-condition sends control when it
|
|
500
|
+
// fails. It may itself be a ret block (a loop ending in `return`), so it CANNOT be found by
|
|
501
|
+
// filtering ret targets (that would hide the exit of every `while(*p){} return q;`). It is the
|
|
502
|
+
// header's non-body edge (test-at-top `while`) or the latch's non-body edge (bottom-test
|
|
503
|
+
// `do-while`). `while` is tried first; `do-while` only when the header keeps BOTH edges in-body.
|
|
504
|
+
const hTerm = h.ops[h.ops.length - 1];
|
|
505
|
+
const lTerm = latch.ops[latch.ops.length - 1];
|
|
506
|
+
const hInBody = hTerm.opcode === 'cond_br' ? hTerm.successors.filter((s) => nl.body.has(s.block)) : [];
|
|
507
|
+
const hOut = hTerm.opcode === 'cond_br' ? hTerm.successors.filter((s) => !nl.body.has(s.block)) : [];
|
|
508
|
+
const lOut = lTerm.opcode === 'cond_br' ? lTerm.successors.filter((s) => !nl.body.has(s.block)) : [];
|
|
509
|
+
// Header purity — the header block is KEPT as the re-evaluated `while` condition, so no
|
|
510
|
+
// store/astore/opaque, and no `call` (expr() inlines a result at every use with no CSE → a call
|
|
511
|
+
// whose result also feeds the body would be evaluated twice per iteration). A `load` is fine —
|
|
512
|
+
// but NOT a materialized one: its temp assignment renders only via sideEffects(), which a
|
|
513
|
+
// condition-only header never emits, so its uses would read an unassigned variable.
|
|
514
|
+
const headerPure = !h.ops.some(
|
|
515
|
+
(op) =>
|
|
516
|
+
op.opcode === 'store' ||
|
|
517
|
+
op.opcode === 'astore' ||
|
|
518
|
+
op.opcode === 'opaque' ||
|
|
519
|
+
op.opcode === 'call' ||
|
|
520
|
+
materialize.has(op),
|
|
521
|
+
);
|
|
522
|
+
|
|
523
|
+
let exitFrom: Block,
|
|
524
|
+
exit: Block,
|
|
525
|
+
kind: 'while' | 'dowhile',
|
|
526
|
+
bodyEntry: Block | null = null;
|
|
527
|
+
if (!nl.selfLoop && hTerm.opcode === 'cond_br' && hInBody.length === 1 && hOut.length === 1 && headerPure) {
|
|
528
|
+
kind = 'while';
|
|
529
|
+
exitFrom = h;
|
|
530
|
+
exit = hOut[0].block;
|
|
531
|
+
bodyEntry = hInBody[0].block;
|
|
532
|
+
} else if (lTerm.opcode === 'cond_br' && lOut.length === 1 && lTerm.successors.some((s) => s.block === h)) {
|
|
533
|
+
// a SELF-loop always lands here: its ops run before its bottom test (body-first), so the
|
|
534
|
+
// faithful spelling is `do { ops; updates } while (cond)` with header === latch
|
|
535
|
+
kind = 'dowhile';
|
|
536
|
+
exitFrom = latch;
|
|
537
|
+
exit = lOut[0].block;
|
|
538
|
+
} else {
|
|
539
|
+
continue; // neither a clean pre-tested nor bottom-tested single-exit shape
|
|
540
|
+
}
|
|
541
|
+
// Single loop exit (ret-aware): the chosen exit is the ONE real exit; every OTHER edge leaving
|
|
542
|
+
// the body must be an early `return` — a ret-terminated target OR a pure return-trampoline chain
|
|
543
|
+
// (agbcc's merged epilogue; `leadsToReturnOnly`). A second exit that lands on a LIVE non-return
|
|
544
|
+
// merge is a genuine `break`/second structured exit → decline.
|
|
545
|
+
if (
|
|
546
|
+
nl.exitEdges.some(
|
|
547
|
+
(e) => !(e.from === exitFrom && e.to === exit) && !isRet(e.to) && !leadsToReturnOnly(e.to, nl.body),
|
|
548
|
+
)
|
|
549
|
+
) {
|
|
550
|
+
continue;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
if (kind === 'while') {
|
|
554
|
+
whileLoops.set(h, {
|
|
555
|
+
header: h,
|
|
556
|
+
bodyEntry: bodyEntry!,
|
|
557
|
+
exit,
|
|
558
|
+
latch,
|
|
559
|
+
forwardPreds: nl.forwardPreds,
|
|
560
|
+
body: nl.body,
|
|
561
|
+
});
|
|
562
|
+
} else {
|
|
563
|
+
doWhileLoops.set(h, { header: h, latch, exit, forwardPreds: nl.forwardPreds, body: nl.body });
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
// --- coalesce SSA values to variable names ---
|
|
568
|
+
const varName = new Map<Value, string>();
|
|
569
|
+
const varType = new Map<string, IrType>();
|
|
570
|
+
// Global symbols referenced by name (agbcc pool `.word gSym`, lowered by the global read/write
|
|
571
|
+
// paths below). They print as bare `gSym`, declared by the project headers — so they are
|
|
572
|
+
// EXCLUDED from the emitted local declarations (localNames below).
|
|
573
|
+
const globalNames = new Set<string>();
|
|
574
|
+
const entry = fn.blocks[0];
|
|
575
|
+
entry.params.forEach((p, i) => {
|
|
576
|
+
varName.set(p, `a${i}`);
|
|
577
|
+
varType.set(`a${i}`, p.type);
|
|
578
|
+
});
|
|
579
|
+
const backArgName = new Map<Value, string>();
|
|
580
|
+
// The C static type of a rendered expression, over the declared variable types — what decides
|
|
581
|
+
// whether a memory access's base may be dereferenced as spelled (memAccess/arrayAccess).
|
|
582
|
+
const ctype = (e0: Expr): IrType | undefined => exprCType(e0, (n) => varType.get(n));
|
|
583
|
+
let fresh = 0;
|
|
584
|
+
// Materialized defs are named FIRST: the temp is the register the compiler held the
|
|
585
|
+
// value in, so downstream coalescing (loop inits, merge params) may adopt it — subject to the
|
|
586
|
+
// same interference check as any other name.
|
|
587
|
+
for (const b of fn.blocks) {
|
|
588
|
+
for (const op of b.ops) {
|
|
589
|
+
if (materialize.has(op)) {
|
|
590
|
+
const r = op.results[0];
|
|
591
|
+
const name = `v${fresh++}`;
|
|
592
|
+
varName.set(r, name);
|
|
593
|
+
varType.set(name, r.type);
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
// Interference check: may block-param `p` of block B adopt `name`? The in-edge copies into
|
|
598
|
+
// `name` execute just before B, and inside/after B the name means p — so it is a silent
|
|
599
|
+
// clobber if ANY other value already under that name is still LIVE at B's entry (the textbook
|
|
600
|
+
// "two live values merged into one variable"), or if a SIBLING param of B claimed it (one
|
|
601
|
+
// edge would then write the name twice).
|
|
602
|
+
//
|
|
603
|
+
// AND THE CONVERSE: the name must not be WRITTEN anywhere `p` itself is live. Every other
|
|
604
|
+
// block param under the name is such a write — its in-edge copies execute at each
|
|
605
|
+
// predecessor's end, and a LOOP header's update copy is emitted inside the loop body, where it
|
|
606
|
+
// also runs on the final (exiting) iteration — so the test is `p` live into the writer's block
|
|
607
|
+
// OR live out of any of its predecessors (the conservative union covers that placement). A
|
|
608
|
+
// materialized def under the name writes at its own block. This applies even to a
|
|
609
|
+
// redundant-phi alias (`pureAlias` waives only the value-at-B check: aliasing is sound at B's
|
|
610
|
+
// entry, but a later write to the shared name still splits them — e.g. a saved pre-increment
|
|
611
|
+
// `i` read post-loop).
|
|
612
|
+
const paramBlock = new Map<Value, Block>();
|
|
613
|
+
for (const blk of fn.blocks) {
|
|
614
|
+
for (const pv of blk.params) {
|
|
615
|
+
paramBlock.set(pv, blk);
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
const canTakeName = (p: Value, B: Block, name: string, pureAlias = false): boolean => {
|
|
619
|
+
if (B.params.some((q) => q !== p && varName.get(q) === name)) {
|
|
620
|
+
return false;
|
|
621
|
+
}
|
|
622
|
+
const lin = liveIn.get(B)!;
|
|
623
|
+
for (const [v, n] of varName) {
|
|
624
|
+
if (n !== name || v === p) {
|
|
625
|
+
continue;
|
|
626
|
+
}
|
|
627
|
+
if (!pureAlias && lin.has(v)) {
|
|
628
|
+
return false;
|
|
629
|
+
} // v still live at B → p's copies clobber it
|
|
630
|
+
const wblk = paramBlock.get(v);
|
|
631
|
+
if (wblk && wblk !== entry) {
|
|
632
|
+
// v is a param → `name` written at wblk's edges
|
|
633
|
+
if (liveIn.get(wblk)!.has(p)) {
|
|
634
|
+
return false;
|
|
635
|
+
}
|
|
636
|
+
for (const pr of preds.get(wblk) ?? []) {
|
|
637
|
+
for (const s of successorsOf(pr)) {
|
|
638
|
+
if (liveIn.get(s)!.has(p)) {
|
|
639
|
+
return false;
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
const d = defs.get(v);
|
|
645
|
+
if (d && materialize.has(d) && liveIn.get(opBlock.get(d)!)!.has(p)) {
|
|
646
|
+
return false;
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
return true;
|
|
650
|
+
};
|
|
651
|
+
// ONE seeding routine for self-loop and structured-loop headers. On a coalesceLoopInit target,
|
|
652
|
+
// keep the induction variable in its entry (forward-edge) value's register — reproducing a
|
|
653
|
+
// compiler that mutates the arg register across the loop instead of copying to a fresh local,
|
|
654
|
+
// so the init copy vanishes. The loop mutates the adopted name every iteration — canTakeName
|
|
655
|
+
// declines it when any value under it is still live at the header. `exclude` are names never to
|
|
656
|
+
// adopt (enclosing loops' induction vars — the cross-level collision below); every seeded
|
|
657
|
+
// param's name is ADDED to it, so sibling params can't collapse.
|
|
658
|
+
const seedLoopParams = (
|
|
659
|
+
header: Block,
|
|
660
|
+
forwardPreds: Block[],
|
|
661
|
+
backArgs: readonly Value[] | null,
|
|
662
|
+
exclude: Set<string>,
|
|
663
|
+
): void => {
|
|
664
|
+
header.params.forEach((p, i) => {
|
|
665
|
+
if (!varName.has(p)) {
|
|
666
|
+
let name: string | undefined;
|
|
667
|
+
if (coalesceLoopInit) {
|
|
668
|
+
for (const fp of forwardPreds) {
|
|
669
|
+
const nm = varName.get(successorTo(fp, header)?.args[i] as Value);
|
|
670
|
+
if (nm && !exclude.has(nm) && canTakeName(p, header, nm)) {
|
|
671
|
+
name = nm;
|
|
672
|
+
break;
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
name ??= `v${fresh++}`;
|
|
677
|
+
varName.set(p, name);
|
|
678
|
+
if (!varType.has(name)) {
|
|
679
|
+
varType.set(name, p.type);
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
exclude.add(varName.get(p)!);
|
|
683
|
+
if (backArgs) {
|
|
684
|
+
backArgName.set(backArgs[i], varName.get(p)!);
|
|
685
|
+
}
|
|
686
|
+
});
|
|
687
|
+
};
|
|
688
|
+
// Self-loop headers seed FIRST, with an EMPTY exclusion set. KNOWN GAP: a nested self-loop
|
|
689
|
+
// (a guard-fused inner loop inside a structured loop DOES structure) seeds before the
|
|
690
|
+
// enclosing loop's induction name exists to exclude — the outermost-first discipline below
|
|
691
|
+
// does not cover this ordering. canTakeName's liveness/write-site checks are the only guard
|
|
692
|
+
// against a cross-level name adoption here.
|
|
693
|
+
for (const li of loops.values()) {
|
|
694
|
+
const fwdPreds = (preds.get(li.header) ?? []).filter((pr) => pr !== li.header);
|
|
695
|
+
seedLoopParams(li.header, fwdPreds, li.backArgOfParam, new Set());
|
|
696
|
+
}
|
|
697
|
+
// Seed structured-loop (`while`/`do-while`) header params: same discipline as self-loops. On
|
|
698
|
+
// coalesceLoopInit, keep the loop variable in its forward-edge (init) register; else a fresh local
|
|
699
|
+
// (agbcc copies the init to a new reg). Never reuse a name already taken by a SIMULTANEOUSLY-LIVE
|
|
700
|
+
// sibling header param — two loop-carried values seeded from one source must not collapse (a silent
|
|
701
|
+
// clobber). The latch's back-edge arg carries the param's name so the loop update assigns it.
|
|
702
|
+
const structuredLoops = [
|
|
703
|
+
...[...whileLoops.values()].map((l) => ({
|
|
704
|
+
header: l.header,
|
|
705
|
+
latch: l.latch,
|
|
706
|
+
forwardPreds: l.forwardPreds,
|
|
707
|
+
body: l.body,
|
|
708
|
+
})),
|
|
709
|
+
...[...doWhileLoops.values()].map((l) => ({
|
|
710
|
+
header: l.header,
|
|
711
|
+
latch: l.latch,
|
|
712
|
+
forwardPreds: l.forwardPreds,
|
|
713
|
+
body: l.body,
|
|
714
|
+
})),
|
|
715
|
+
];
|
|
716
|
+
// Cross-level collision: with nesting, an outer loop's induction variable is LIVE across the inner
|
|
717
|
+
// loop (the outer latch reads it after). If the inner var is coalesced onto the outer var's name
|
|
718
|
+
// (its init reads the outer var), the inner loop would MUTATE the outer variable — a silent
|
|
719
|
+
// miscompile. Process OUTERMOST-first (so an enclosing loop is named first) and, per loop, exclude
|
|
720
|
+
// the names of every enclosing loop's header params from the coalescing candidates.
|
|
721
|
+
// `enclosingNames(l)` = names of params of headers whose natural body strictly contains `l.header`.
|
|
722
|
+
structuredLoops.sort((a, b) => b.body.size - a.body.size); // outermost first
|
|
723
|
+
const enclosingNames = (l: { header: Block; body: Set<Block> }): Set<string> => {
|
|
724
|
+
const names = new Set<string>();
|
|
725
|
+
for (const nl2 of forest.byHeader.values()) {
|
|
726
|
+
if (nl2.header !== l.header && nl2.body.has(l.header)) {
|
|
727
|
+
// nl2 strictly encloses l
|
|
728
|
+
for (const p of nl2.header.params) {
|
|
729
|
+
const nm = varName.get(p);
|
|
730
|
+
if (nm) {
|
|
731
|
+
names.add(nm);
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
return names;
|
|
737
|
+
};
|
|
738
|
+
for (const l of structuredLoops) {
|
|
739
|
+
const back = successorTo(l.latch, l.header);
|
|
740
|
+
// exclusion seeded with enclosing-loop names → never coalesce onto them
|
|
741
|
+
seedLoopParams(l.header, l.forwardPreds, back ? back.args : null, enclosingNames(l));
|
|
742
|
+
}
|
|
743
|
+
let changed = true;
|
|
744
|
+
while (changed) {
|
|
745
|
+
changed = false;
|
|
746
|
+
for (const b of fn.blocks) {
|
|
747
|
+
if (b === entry) {
|
|
748
|
+
continue;
|
|
749
|
+
}
|
|
750
|
+
b.params.forEach((p, i) => {
|
|
751
|
+
if (varName.has(p)) {
|
|
752
|
+
return;
|
|
753
|
+
}
|
|
754
|
+
// EVERY in-edge record, not successorTo (which returns only the FIRST record to `b` — a
|
|
755
|
+
// terminator with two edges to the same block would hide the second edge's args here).
|
|
756
|
+
const incoming: Value[] = [];
|
|
757
|
+
for (const pr of new Set(preds.get(b) ?? [])) {
|
|
758
|
+
for (const s of pr.ops[pr.ops.length - 1].successors) {
|
|
759
|
+
if (s.block === b) {
|
|
760
|
+
incoming.push(s.args[i]);
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
// A redundant phi (every edge passes the SAME value) is a pure alias of it — sharing the
|
|
765
|
+
// name is sound even while the value stays live (they are equal on every path). This
|
|
766
|
+
// waives only the LIVENESS half of canTakeName; the sibling-param check always applies.
|
|
767
|
+
const allSame = incoming.length > 0 && incoming.every((v) => v === incoming[0]);
|
|
768
|
+
// prefer a carrier that already has a name; then a loop var whose update this receives —
|
|
769
|
+
// but only one whose name survives the C3 interference check (else the edge copies into
|
|
770
|
+
// the name would clobber a still-live value).
|
|
771
|
+
let name: string | undefined;
|
|
772
|
+
for (const c of [...incoming.filter((v) => varName.has(v)), ...incoming.filter((v) => backArgName.has(v))]) {
|
|
773
|
+
const nm = varName.get(c) ?? backArgName.get(c)!;
|
|
774
|
+
if (canTakeName(p, b, nm, allSame)) {
|
|
775
|
+
name = nm;
|
|
776
|
+
break;
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
name ??= `v${fresh++}`;
|
|
780
|
+
varName.set(p, name);
|
|
781
|
+
if (!varType.has(name)) {
|
|
782
|
+
varType.set(name, p.type);
|
|
783
|
+
}
|
|
784
|
+
changed = true;
|
|
785
|
+
});
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
// An unresolvable value: strict mode keeps the `"?"` sentinel (assertResolved trips at the
|
|
790
|
+
// boundary — loud in the PROCESS); annotate mode emits a marker (the undefined ASMLIFT_ERROR
|
|
791
|
+
// symbol — loud in the ARTIFACT, function still complete).
|
|
792
|
+
const mkGap = (reason: string, args: Expr[]): Expr =>
|
|
793
|
+
onGap === 'annotate' ? { k: 'marker', reason, args } : { k: 'var', name: '?' };
|
|
794
|
+
|
|
795
|
+
// Lower ONE def's operation to an Expr, rendering operands through `e`. Shared between the
|
|
796
|
+
// inline-at-use path (exprWith) and the materialized-temp path (sideEffects), so both spell a
|
|
797
|
+
// given op identically.
|
|
798
|
+
const lowerDef = (d: Op, e: (v: Value) => Expr): Expr => {
|
|
799
|
+
if (d.opcode === 'const') {
|
|
800
|
+
return { k: 'const', value: d.attrs.value as number };
|
|
801
|
+
}
|
|
802
|
+
if (CMP_TO_BIN[d.opcode]) {
|
|
803
|
+
return { k: 'bin', op: CMP_TO_BIN[d.opcode], l: e(d.operands[0]), r: e(d.operands[1]) };
|
|
804
|
+
}
|
|
805
|
+
if (ARITH_TO_BIN[d.opcode]) {
|
|
806
|
+
let l = e(d.operands[0]);
|
|
807
|
+
let r = d.operands.length === 2 ? e(d.operands[1]) : ({ k: 'const', value: d.attrs.imm as number } as Expr);
|
|
808
|
+
// Pointer stride: C pointer arithmetic is ELEMENT-scaled, but the asm added a BYTE
|
|
809
|
+
// constant — `addi p,4` on an `s32*` walks 1 element, yet C `p + 4` walks 4. Divide the byte
|
|
810
|
+
// constant by the pointee size so the walk recompiles to the same address math.
|
|
811
|
+
//
|
|
812
|
+
// Keyed on the operand's RENDERED C type, never the IR value's recovered type: C scales by
|
|
813
|
+
// the type of the expression it actually sees, and the two diverge exactly like memAccess's
|
|
814
|
+
// deref bases (a value recovered `s32*` can render as an int-typed tree — C then does NO
|
|
815
|
+
// element scaling, so pre-dividing the constant would bake in a WRONG address that the
|
|
816
|
+
// deref cast downstream turns into silently-wrong bytes; found by the adversarial round).
|
|
817
|
+
// An int-rendered walk keeps its raw byte constant and derefs through the access-width cast.
|
|
818
|
+
// Fires only for a rendered pointer whose element size (>1) DIVIDES the constant exactly;
|
|
819
|
+
// otherwise raw (a misaligned/struct-array stride is left as-is; a `u8*` is size 1 so
|
|
820
|
+
// unchanged). Since C `(K/es) + p == p + (K/es)`, scaling the const on whichever side it
|
|
821
|
+
// sits fixes the bytes: `add` is commutative so the pointer may be either operand; `sub` is
|
|
822
|
+
// not, so only operand[0] (the minuend) may be the pointer.
|
|
823
|
+
const scale = (t: IrType | undefined, c: Extract<Expr, { k: 'const' }>): Expr => {
|
|
824
|
+
const es = t?.kind === 'ptr' ? ptrElemBytes(t.to) : 0;
|
|
825
|
+
return es > 1 && c.value % es === 0 ? { k: 'const', value: c.value / es } : c;
|
|
826
|
+
};
|
|
827
|
+
if ((d.opcode === 'add' || d.opcode === 'sub') && r.k === 'const') {
|
|
828
|
+
r = scale(ctype(l), r);
|
|
829
|
+
} else if (d.opcode === 'add' && d.operands.length === 2 && l.k === 'const') {
|
|
830
|
+
l = scale(ctype(r), l); // commuted `const + ptr`
|
|
831
|
+
}
|
|
832
|
+
// C rejects a pointer operand outright under the non-additive operators (& | ^ << >> * / %),
|
|
833
|
+
// under `ptr + ptr`, and as the subtrahend of `int - ptr` — the asm just does 32-bit integer
|
|
834
|
+
// math on the address, so the honest spelling is the value cast to its integer self. Only a
|
|
835
|
+
// DEFINITELY-pointer rendering is cast (same conservative direction as memAccess); the
|
|
836
|
+
// additive ops keep C's legal pointer arithmetic untouched.
|
|
837
|
+
const op = ARITH_TO_BIN[d.opcode];
|
|
838
|
+
const intify = (x: Expr): Expr => (ctype(x)?.kind === 'ptr' ? { k: 'cast', to: T.s(32), e: x } : x);
|
|
839
|
+
if (!['+', '-', '&&', '||'].includes(op)) {
|
|
840
|
+
l = intify(l);
|
|
841
|
+
r = intify(r);
|
|
842
|
+
} else if (op === '+' && ctype(l)?.kind === 'ptr' && ctype(r)?.kind === 'ptr') {
|
|
843
|
+
r = intify(r); // ptr + ptr is not C; ptr + (s32)ptr is, with the same bytes
|
|
844
|
+
} else if (op === '-' && ctype(l)?.kind !== 'ptr' && ctype(r)?.kind === 'ptr') {
|
|
845
|
+
r = intify(r); // int - ptr is not C
|
|
846
|
+
}
|
|
847
|
+
return { k: 'bin', op, l, r };
|
|
848
|
+
}
|
|
849
|
+
// `-`/`~` on a pointer rendering is equally not C — same honest integer cast as above.
|
|
850
|
+
if (d.opcode === 'rotr' || d.opcode === 'rotl') {
|
|
851
|
+
// The C rotate idiom — `x >> n | x << (32 - n)` (mirrored for rotl). Byte-exact round-trip
|
|
852
|
+
// on agbcc (thumb ror) and mwcc (rotlw/rotlwi), verified against both toolchains before the
|
|
853
|
+
// ops landed. `x` and `n` render twice — both pure by construction (SSA values; the rotate's
|
|
854
|
+
// operands are register reads), and recovery seeds the rotated value unsigned so `>>`
|
|
855
|
+
// spells the logical shift the idiom requires.
|
|
856
|
+
//
|
|
857
|
+
// (The PPC mirror fold — `rotl(x, 32 - m)` ⇒ rotr(x, m) — lives in the PATTERN layer,
|
|
858
|
+
// engine.ts ROTL_MIRROR: it is a compiler-spelling idiom, mwcc-gated there, not a
|
|
859
|
+
// structurer concern.)
|
|
860
|
+
const dir: 'rotr' | 'rotl' = d.opcode;
|
|
861
|
+
const n: Expr = d.operands.length === 2 ? e(d.operands[1]) : { k: 'const', value: d.attrs.imm as number };
|
|
862
|
+
const x = e(d.operands[0]);
|
|
863
|
+
// constant-amount edges: 0 and 32 are the IDENTITY (the C idiom would spell the UB
|
|
864
|
+
// shift-by-32); otherwise a constant folds the complement (`a0 >> 24`, not `a0 >> 32 - 8`).
|
|
865
|
+
if (n.k === 'const' && (n.value === 0 || n.value === 32)) {
|
|
866
|
+
return x;
|
|
867
|
+
}
|
|
868
|
+
const w: Expr =
|
|
869
|
+
n.k === 'const'
|
|
870
|
+
? { k: 'const', value: 32 - n.value }
|
|
871
|
+
: { k: 'bin', op: '-', l: { k: 'const', value: 32 }, r: n };
|
|
872
|
+
const [near, far] = dir === 'rotr' ? (['>>', '<<'] as const) : (['<<', '>>'] as const);
|
|
873
|
+
return {
|
|
874
|
+
k: 'bin',
|
|
875
|
+
op: '|',
|
|
876
|
+
l: { k: 'bin', op: near, l: x, r: n },
|
|
877
|
+
r: { k: 'bin', op: far, l: x, r: w },
|
|
878
|
+
};
|
|
879
|
+
}
|
|
880
|
+
if (d.opcode === 'neg') {
|
|
881
|
+
const x = e(d.operands[0]);
|
|
882
|
+
return { k: 'un', op: '-', e: ctype(x)?.kind === 'ptr' ? { k: 'cast', to: T.s(32), e: x } : x };
|
|
883
|
+
}
|
|
884
|
+
if (d.opcode === 'not') {
|
|
885
|
+
const x = e(d.operands[0]);
|
|
886
|
+
return { k: 'un', op: '~', e: ctype(x)?.kind === 'ptr' ? { k: 'cast', to: T.s(32), e: x } : x };
|
|
887
|
+
}
|
|
888
|
+
// Width-narrowing casts: `zext`/`sext` widen a `width`-bit value back to 32 → C `(u8)e`/`(s8)e`.
|
|
889
|
+
if (d.opcode === 'zext') {
|
|
890
|
+
return { k: 'cast', to: T.int(d.attrs.width as number, false), e: e(d.operands[0]) };
|
|
891
|
+
}
|
|
892
|
+
if (d.opcode === 'sext') {
|
|
893
|
+
return { k: 'cast', to: T.int(d.attrs.width as number, true), e: e(d.operands[0]) };
|
|
894
|
+
}
|
|
895
|
+
if (d.opcode === 'call') {
|
|
896
|
+
return { k: 'call', fn: d.attrs.target as string, args: d.operands.map(e) };
|
|
897
|
+
}
|
|
898
|
+
if (d.opcode === 'gaddr') {
|
|
899
|
+
return { k: 'addr', name: d.attrs.sym as string };
|
|
900
|
+
}
|
|
901
|
+
if (d.opcode === 'load') {
|
|
902
|
+
return memAccess(
|
|
903
|
+
d.operands[0],
|
|
904
|
+
e(d.operands[0]),
|
|
905
|
+
d.attrs.off as number,
|
|
906
|
+
d.attrs.width as number,
|
|
907
|
+
(d.attrs.signed as boolean) ?? false,
|
|
908
|
+
ctype,
|
|
909
|
+
scalarGlobals,
|
|
910
|
+
);
|
|
911
|
+
}
|
|
912
|
+
// aload carries a runtime index operand (variable-index array access) — `base[index]`, or
|
|
913
|
+
// `base[index].field_K` when it carries a `fieldOff` (array-of-STRUCT element access).
|
|
914
|
+
if (d.opcode === 'aload') {
|
|
915
|
+
return arrayAccess(
|
|
916
|
+
d.operands[0],
|
|
917
|
+
e(d.operands[0]),
|
|
918
|
+
e(d.operands[1]),
|
|
919
|
+
d.attrs.fieldOff as number | undefined,
|
|
920
|
+
d.attrs.elemSize as number,
|
|
921
|
+
(d.attrs.signed as boolean) ?? false,
|
|
922
|
+
ctype,
|
|
923
|
+
);
|
|
924
|
+
}
|
|
925
|
+
return d.opcode === 'opaque'
|
|
926
|
+
? mkGap(`unmodelled instruction '${(d.attrs.mnemonic as string) ?? '?'}'`, d.operands.map(e))
|
|
927
|
+
: mkGap(`no lowering for op '${d.opcode}'`, d.operands.map(e));
|
|
928
|
+
};
|
|
929
|
+
|
|
930
|
+
const exprWith = (sub: Map<Value, string> | null) => {
|
|
931
|
+
const e = (v: Value): Expr => {
|
|
932
|
+
const subbed = sub?.get(v);
|
|
933
|
+
if (subbed) {
|
|
934
|
+
return { k: 'var', name: subbed };
|
|
935
|
+
}
|
|
936
|
+
if (varName.has(v)) {
|
|
937
|
+
return { k: 'var', name: varName.get(v)! };
|
|
938
|
+
}
|
|
939
|
+
const d = defs.get(v);
|
|
940
|
+
if (!d) {
|
|
941
|
+
return mkGap('value has no reaching definition (dropped def)', []);
|
|
942
|
+
}
|
|
943
|
+
return lowerDef(d, e);
|
|
944
|
+
};
|
|
945
|
+
return e;
|
|
946
|
+
};
|
|
947
|
+
// The loop-emission hazard checks (readsClobbered / loopEscapeHazard / loopUpdateHazard) —
|
|
948
|
+
// pure decline-or-emit predicates, extracted to hazards.ts behind the explicit-deps factory.
|
|
949
|
+
// `varName` is captured as a live reference: it is still being populated here in the naming
|
|
950
|
+
// pipeline, and each check reads the names that exist when EMISSION calls it.
|
|
951
|
+
const { loopUpdateHazard } = makeLoopHazards({ defs, varName, useSitesOf });
|
|
952
|
+
|
|
953
|
+
// A POST-LOOP substitution active while structuring a loop's exit region: a loop-carried value (a
|
|
954
|
+
// latch back-edge arg) is held in its loop-variable NAME after the loop, so any post-loop use must
|
|
955
|
+
// read the name, not re-inline the computation (which would double-count, e.g. `(u8)(v1+1)` instead
|
|
956
|
+
// of `v1`). `expr` consults it; `withSub` installs/merges it around the exit region. Null normally.
|
|
957
|
+
let activeSub: Map<Value, string> | null = null;
|
|
958
|
+
const expr = (v: Value): Expr => exprWith(activeSub)(v);
|
|
959
|
+
const withSub = <R>(sub: Map<Value, string>, run: () => R): R => {
|
|
960
|
+
const prev = activeSub;
|
|
961
|
+
activeSub = prev ? new Map([...prev, ...sub]) : sub;
|
|
962
|
+
try {
|
|
963
|
+
return run();
|
|
964
|
+
} finally {
|
|
965
|
+
activeSub = prev;
|
|
966
|
+
}
|
|
967
|
+
};
|
|
968
|
+
|
|
969
|
+
// Assignments a predecessor must perform when branching into `target`, as a PARALLEL
|
|
970
|
+
// copy (skip identities), then sequentialised so no assignment clobbers a still-needed
|
|
971
|
+
// value. `sub` (used for the emitWhile un-rotation's exit copies) substitutes back-edge args to
|
|
972
|
+
// their header-param NAMES — post-loop the params already hold their updated values, so a merged
|
|
973
|
+
// exit value is read as `v` not `v-1`.
|
|
974
|
+
const tempCounter = { n: 0 }; // per-function swap-cycle temp names (sequentialize)
|
|
975
|
+
// The copies for ONE specific successor record — the workhorse behind argAssigns, taken
|
|
976
|
+
// directly by the switch_br path, whose duplicate case targets successorTo cannot
|
|
977
|
+
// disambiguate.
|
|
978
|
+
const argAssignsFor = (
|
|
979
|
+
pred: Block,
|
|
980
|
+
succ: { block: Block; args: Value[] },
|
|
981
|
+
sub: Map<Value, string> | null = null,
|
|
982
|
+
): Stmt[] => {
|
|
983
|
+
const target = succ.block;
|
|
984
|
+
const argExpr = sub ? exprWith(sub) : expr;
|
|
985
|
+
const copies: { name: string; value: Expr; arg: Value }[] = [];
|
|
986
|
+
target.params.forEach((p, i) => {
|
|
987
|
+
const name = varName.get(p)!;
|
|
988
|
+
const arg = succ.args[i];
|
|
989
|
+
if ((sub?.get(arg) ?? varName.get(arg)) === name) {
|
|
990
|
+
return;
|
|
991
|
+
} // identity copy — coalesced away
|
|
992
|
+
copies.push({ name, value: argExpr(arg), arg });
|
|
993
|
+
});
|
|
994
|
+
// Emit in the order the args are COMPUTED in `pred` — a compiler that lays the defining ops
|
|
995
|
+
// (and thus the copies that read them) out in that order matches with no spurious arg-swap.
|
|
996
|
+
// This is a per-compiler behavior (orderArgCopiesByComputation), not a universal: a compiler
|
|
997
|
+
// that emits copies in source/param order sets it false. Dependency ordering (sequentialize)
|
|
998
|
+
// still has the final say regardless.
|
|
999
|
+
if (orderArgCopiesByComputation) {
|
|
1000
|
+
// opIndex is only valid for a def IN this block; a def elsewhere keeps indexOf's -1 (sorts first).
|
|
1001
|
+
const pos = (v: Value) => {
|
|
1002
|
+
const d = defs.get(v);
|
|
1003
|
+
return d && opBlock.get(d) === pred ? opIndex.get(d)! : -1;
|
|
1004
|
+
};
|
|
1005
|
+
copies.sort((a, b) => pos(a.arg) - pos(b.arg));
|
|
1006
|
+
}
|
|
1007
|
+
return sequentialize(
|
|
1008
|
+
copies.map(({ name, value }) => ({ name, value })),
|
|
1009
|
+
varType,
|
|
1010
|
+
tempCounter,
|
|
1011
|
+
fn.name,
|
|
1012
|
+
);
|
|
1013
|
+
};
|
|
1014
|
+
const argAssigns = (pred: Block, target: Block, sub: Map<Value, string> | null = null): Stmt[] => {
|
|
1015
|
+
const succ = successorTo(pred, target);
|
|
1016
|
+
return succ ? argAssignsFor(pred, succ, sub) : [];
|
|
1017
|
+
};
|
|
1018
|
+
|
|
1019
|
+
// Side-effecting ops of a block, emitted as statements in program order: memory stores,
|
|
1020
|
+
// calls whose return value nothing consumes (a void/discarded call), and MATERIALIZED defs
|
|
1021
|
+
// — a call/load whose value cannot soundly render at its use is assigned to its named
|
|
1022
|
+
// temp here, at its own program position.
|
|
1023
|
+
const sideEffects = (b: Block): Stmt[] => {
|
|
1024
|
+
const out: Stmt[] = [];
|
|
1025
|
+
for (const op of b.ops) {
|
|
1026
|
+
if (op.opcode === 'store') {
|
|
1027
|
+
// A store whose lvalue is a bare global (`gSym = v`, from an `&gSym` base at off 0) emits
|
|
1028
|
+
// as an ASSIGN, not a store — memAccess returns a `var` node for that case.
|
|
1029
|
+
const width = op.attrs.width as number;
|
|
1030
|
+
const lval0 = memAccess(
|
|
1031
|
+
op.operands[0],
|
|
1032
|
+
expr(op.operands[0]),
|
|
1033
|
+
op.attrs.off as number,
|
|
1034
|
+
width,
|
|
1035
|
+
width === 4,
|
|
1036
|
+
ctype,
|
|
1037
|
+
scalarGlobals,
|
|
1038
|
+
);
|
|
1039
|
+
if (lval0.k === 'var') {
|
|
1040
|
+
globalNames.add(lval0.name);
|
|
1041
|
+
out.push({ k: 'assign', name: lval0.name, value: expr(op.operands[1]) });
|
|
1042
|
+
continue;
|
|
1043
|
+
}
|
|
1044
|
+
// signedness mirrors recoverTypes' store seed (word ⇒ signed, narrow ⇒ unsigned), so an
|
|
1045
|
+
// inserted cast declares the same scalar the recovered pointee would have.
|
|
1046
|
+
out.push({ k: 'store', lval: lval0, value: expr(op.operands[1]) });
|
|
1047
|
+
} else if (op.opcode === 'astore') {
|
|
1048
|
+
const elemSize = op.attrs.elemSize as number;
|
|
1049
|
+
out.push({
|
|
1050
|
+
k: 'store',
|
|
1051
|
+
lval: arrayAccess(
|
|
1052
|
+
op.operands[0],
|
|
1053
|
+
expr(op.operands[0]),
|
|
1054
|
+
expr(op.operands[1]),
|
|
1055
|
+
op.attrs.fieldOff as number | undefined,
|
|
1056
|
+
elemSize,
|
|
1057
|
+
elemSize === 4,
|
|
1058
|
+
ctype,
|
|
1059
|
+
),
|
|
1060
|
+
value: expr(op.operands[2]),
|
|
1061
|
+
});
|
|
1062
|
+
} else if (op.opcode === 'call' && op.results.length && !useSitesOf.has(op.results[0])) {
|
|
1063
|
+
out.push({ k: 'exprstmt', value: expr(op.results[0]) });
|
|
1064
|
+
} else if (materialize.has(op)) {
|
|
1065
|
+
out.push({ k: 'assign', name: varName.get(op.results[0])!, value: lowerDef(op, expr) });
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
return out;
|
|
1069
|
+
};
|
|
1070
|
+
|
|
1071
|
+
// Blocks currently on the recursion stack. A well-formed reducible CFG structures each
|
|
1072
|
+
// block at most once per active path, so re-entering a block already on the stack means an
|
|
1073
|
+
// unrecovered back-edge (a cycle loop-recovery didn't lower) — which would recurse forever.
|
|
1074
|
+
// Bail explicitly instead. This also bounds recursion depth by the block count.
|
|
1075
|
+
const onStack = new Set<Block>();
|
|
1076
|
+
// do-while headers currently being emitted — so structuring the do-while's own body (which re-enters
|
|
1077
|
+
// the header block to structure its ops up to the latch) does not re-trigger the do-while hook.
|
|
1078
|
+
const dwActive = new Set<Block>();
|
|
1079
|
+
// The innermost loop whose BODY is currently being structured. A body cond_br with one edge back
|
|
1080
|
+
// to `header` is a conditional continue; its other edge, when it leaves the loop, is an early exit
|
|
1081
|
+
// (a `break` to `exit`, or an early `return` through a trampoline). Null outside any loop body —
|
|
1082
|
+
// the early-exit branch in structureBlock is inert there.
|
|
1083
|
+
type LoopFrame = { header: Block; exit: Block; body: Set<Block> };
|
|
1084
|
+
let loopCtx: LoopFrame | null = null;
|
|
1085
|
+
const withLoop = <R>(frame: LoopFrame, run: () => R): R => {
|
|
1086
|
+
const prev = loopCtx;
|
|
1087
|
+
loopCtx = frame;
|
|
1088
|
+
try {
|
|
1089
|
+
return run();
|
|
1090
|
+
} finally {
|
|
1091
|
+
loopCtx = prev;
|
|
1092
|
+
}
|
|
1093
|
+
};
|
|
1094
|
+
|
|
1095
|
+
const structureRegion = (b: Block, stop: Block | null): Stmt[] => {
|
|
1096
|
+
if (b === stop) {
|
|
1097
|
+
return [];
|
|
1098
|
+
}
|
|
1099
|
+
if (onStack.has(b)) {
|
|
1100
|
+
throw new StructureError(
|
|
1101
|
+
`cannot structure '${fn.name}': unrecovered back-edge into block #${fn.blocks.indexOf(b)} ` +
|
|
1102
|
+
`(loop-recovery declined this shape: multi-latch, irreducible/overlapping loops, ` +
|
|
1103
|
+
`a conditional continue, or an unsafe break)`,
|
|
1104
|
+
);
|
|
1105
|
+
}
|
|
1106
|
+
onStack.add(b);
|
|
1107
|
+
try {
|
|
1108
|
+
return structureBlock(b, stop);
|
|
1109
|
+
} finally {
|
|
1110
|
+
onStack.delete(b);
|
|
1111
|
+
}
|
|
1112
|
+
};
|
|
1113
|
+
|
|
1114
|
+
// ── Regime-A switch recovery (structure/switch-recover.ts): the recognizer's case bodies call
|
|
1115
|
+
// back into structureRegion, and Regime B (switch_br, below) shares its fall-through predicate.
|
|
1116
|
+
const { recognizeSwitch, caseRegionReachesSibling } = makeSwitchRecovery({
|
|
1117
|
+
fn,
|
|
1118
|
+
defs,
|
|
1119
|
+
dom,
|
|
1120
|
+
ipdom,
|
|
1121
|
+
opBlock,
|
|
1122
|
+
isNamed: (v) => varName.has(v),
|
|
1123
|
+
isCmpOpcode: (opcode) => !!CMP_TO_BIN[opcode],
|
|
1124
|
+
switchAllowsNeqCase,
|
|
1125
|
+
expr: (v) => expr(v),
|
|
1126
|
+
structureRegion: (b, stop) => structureRegion(b, stop),
|
|
1127
|
+
});
|
|
1128
|
+
|
|
1129
|
+
const structureBlock = (b: Block, stop: Block | null): Stmt[] => {
|
|
1130
|
+
// Bottom-test `do-while`: this block is a do-while header (the body-first loop entry). Emit the
|
|
1131
|
+
// do-while — its body includes `b`'s own ops (structured via structureRegion with the hook masked),
|
|
1132
|
+
// so do NOT emit sideEffects(b) here. The init was already emitted by the predecessor's argAssigns.
|
|
1133
|
+
const dw = doWhileLoops.get(b);
|
|
1134
|
+
if (dw && !dwActive.has(b)) {
|
|
1135
|
+
return emitDoWhile(dw, stop);
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
const out: Stmt[] = [...sideEffects(b)];
|
|
1139
|
+
const term = b.ops[b.ops.length - 1];
|
|
1140
|
+
if (term.opcode === 'ret') {
|
|
1141
|
+
// A void function's `bx lr` leaves whatever in r0; suppress that phantom return value.
|
|
1142
|
+
out.push({ k: 'return', value: returnsVoid || !term.operands.length ? undefined : expr(term.operands[0]) });
|
|
1143
|
+
return out;
|
|
1144
|
+
}
|
|
1145
|
+
if (term.opcode === 'br') {
|
|
1146
|
+
const target = term.successors[0].block;
|
|
1147
|
+
out.push(...argAssignsFor(b, term.successors[0]));
|
|
1148
|
+
out.push(...structureRegion(target, stop));
|
|
1149
|
+
return out;
|
|
1150
|
+
}
|
|
1151
|
+
// Regime B: a `switch_br` (jump-table dispatch) lowers directly to the `switch` node — scrutinee,
|
|
1152
|
+
// per-successor case value, last successor = default. Case bodies delegate to structureRegion (as in
|
|
1153
|
+
// Regime A). Fall-through between jump-table cases is not yet handled: if a case body reaches another
|
|
1154
|
+
// case/default block inside the region, fail LOUD rather than duplicate it.
|
|
1155
|
+
if (term.opcode === 'switch_br') {
|
|
1156
|
+
const merge = ipdom.get(b) ?? stop;
|
|
1157
|
+
const succ = term.successors;
|
|
1158
|
+
const caseVals = term.attrs.cases as number[];
|
|
1159
|
+
const targets = new Set<Block>(succ.map((s) => s.block));
|
|
1160
|
+
if (caseRegionReachesSibling(targets, b, merge)) {
|
|
1161
|
+
throw new StructureError(
|
|
1162
|
+
`cannot structure '${fn.name}': fall-through between jump-table cases is not yet supported`,
|
|
1163
|
+
);
|
|
1164
|
+
}
|
|
1165
|
+
// Switch edges CARRY phi args (frontend/ssa.ts appends them terminator-generically) — each
|
|
1166
|
+
// case/default body must open with its edge's copies, exactly as cond_br edges do; dropping
|
|
1167
|
+
// them leaves the target's params uninitialized on the switch path. Two case values sharing
|
|
1168
|
+
// a target must agree on their args (else the copies are ambiguous → loud decline); the
|
|
1169
|
+
// shared body is then structured per case entry.
|
|
1170
|
+
const argsSeen = new Map<Block, Value[]>();
|
|
1171
|
+
for (const s of succ) {
|
|
1172
|
+
const prev = argsSeen.get(s.block);
|
|
1173
|
+
if (prev && (prev.length !== s.args.length || prev.some((v, i) => v !== s.args[i]))) {
|
|
1174
|
+
throw new StructureError(
|
|
1175
|
+
`cannot structure '${fn.name}': jump-table cases share a target block with differing phi args`,
|
|
1176
|
+
);
|
|
1177
|
+
}
|
|
1178
|
+
argsSeen.set(s.block, s.args as Value[]);
|
|
1179
|
+
}
|
|
1180
|
+
const outCases: SwitchCase[] = succ.slice(0, -1).map((s, i) => ({
|
|
1181
|
+
values: [caseVals[i]],
|
|
1182
|
+
body: [...argAssignsFor(b, s), ...structureRegion(s.block, merge)],
|
|
1183
|
+
fallsThrough: false,
|
|
1184
|
+
}));
|
|
1185
|
+
const sw: Stmt = {
|
|
1186
|
+
k: 'switch',
|
|
1187
|
+
scrutinee: expr(term.operands[0]),
|
|
1188
|
+
cases: outCases,
|
|
1189
|
+
default: [...argAssignsFor(b, succ[succ.length - 1]), ...structureRegion(succ[succ.length - 1].block, merge)],
|
|
1190
|
+
};
|
|
1191
|
+
out.push(sw);
|
|
1192
|
+
if (merge && merge !== stop) {
|
|
1193
|
+
out.push(...structureRegion(merge, stop));
|
|
1194
|
+
}
|
|
1195
|
+
return out;
|
|
1196
|
+
}
|
|
1197
|
+
// Everything below assumes a 2-way `cond_br`. Any OTHER terminator (a malformed op) must fail LOUD
|
|
1198
|
+
// here — otherwise it is silently read as a `cond_br` and every successor past the second is dropped,
|
|
1199
|
+
// a silent control-flow miscompile at the structuring seam.
|
|
1200
|
+
if (term.opcode !== 'cond_br') {
|
|
1201
|
+
throw new StructureError(
|
|
1202
|
+
`cannot structure '${fn.name}': unsupported terminator '${term.opcode}' in block #${fn.blocks.indexOf(b)} ` +
|
|
1203
|
+
`(only ret / br / cond_br / switch_br are structured today)`,
|
|
1204
|
+
);
|
|
1205
|
+
}
|
|
1206
|
+
// cond_br: successors = [taken, fallthrough]
|
|
1207
|
+
const takenB = term.successors[0].block;
|
|
1208
|
+
const fallB = term.successors[1].block;
|
|
1209
|
+
|
|
1210
|
+
// Test-at-top `while`: this block IS a loop header whose pure test decides body-vs-exit. The
|
|
1211
|
+
// header test is the loop condition (read on entry values); the body is a region that stops at the
|
|
1212
|
+
// header (= continue). The init was already emitted by the predecessor's argAssigns into `b`.
|
|
1213
|
+
const wl = whileLoops.get(b);
|
|
1214
|
+
if (wl) {
|
|
1215
|
+
out.push(...emitTestAtTopWhile(wl, stop));
|
|
1216
|
+
return out;
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1219
|
+
// guard-fused loop: this cond_br decides "enter loop header h vs its exit". Emit the
|
|
1220
|
+
// inits unconditionally, then a while whose own test subsumes this guard. Never fuse when `b`
|
|
1221
|
+
// is itself the header (a guard-LESS single-block do-while would emit the update once before a
|
|
1222
|
+
// wrongly-`while` loop) — require a DISTINCT dominating guard block.
|
|
1223
|
+
for (const h of [takenB, fallB]) {
|
|
1224
|
+
const li = loops.get(h);
|
|
1225
|
+
if (li && h !== b && (takenB === li.exit || fallB === li.exit)) {
|
|
1226
|
+
// A MATERIALIZED header op's temp is assigned only inside the body (sideEffects), but the
|
|
1227
|
+
// un-rotated `while` condition renders BEFORE the body ever ran — reading the temp
|
|
1228
|
+
// uninitialized on the first test. Mirror of the headerPure gate: decline loud.
|
|
1229
|
+
if (li.header.ops.some((o) => materialize.has(o))) {
|
|
1230
|
+
throw new StructureError(
|
|
1231
|
+
`cannot structure '${fn.name}': loop header holds a materialized def its condition would read uninitialized`,
|
|
1232
|
+
);
|
|
1233
|
+
}
|
|
1234
|
+
// Self-loop emitter hazards: the while condition, the header→exit args, and every
|
|
1235
|
+
// post-loop use of a header-computed value render under the un-rotation sub — sound only
|
|
1236
|
+
// when their loop-variable reads go through sub-mapped back-edge args (post-update). A
|
|
1237
|
+
// direct read of an updated variable is a PRE-update value the emitted name no longer
|
|
1238
|
+
// holds → decline LOUD, never emit wrong code.
|
|
1239
|
+
const sub = loopSub(li);
|
|
1240
|
+
const updates = argAssigns(li.header, li.header);
|
|
1241
|
+
const updateWrites = updateWriteSet(updates);
|
|
1242
|
+
const hterm = li.header.ops[li.header.ops.length - 1];
|
|
1243
|
+
const hexitArgs = (successorTo(li.header, li.exit)?.args ?? []) as Value[];
|
|
1244
|
+
if (
|
|
1245
|
+
loopUpdateHazard(
|
|
1246
|
+
hterm.operands[0],
|
|
1247
|
+
hexitArgs,
|
|
1248
|
+
new Set([li.header]),
|
|
1249
|
+
sub,
|
|
1250
|
+
updateWrites,
|
|
1251
|
+
null,
|
|
1252
|
+
new Set(li.header.params),
|
|
1253
|
+
)
|
|
1254
|
+
) {
|
|
1255
|
+
throw new StructureError(
|
|
1256
|
+
`cannot structure '${fn.name}': loop condition or a post-loop value reads a pre-update loop variable`,
|
|
1257
|
+
);
|
|
1258
|
+
}
|
|
1259
|
+
out.push(...argAssigns(b, h)); // loop-variable initialisation
|
|
1260
|
+
out.push(emitWhile(li, updates));
|
|
1261
|
+
// The header→exit edge may carry non-identity phi args (the exit param merges the guard-false
|
|
1262
|
+
// value with the loop's final value). Emit those copies after the loop — dropping them returns
|
|
1263
|
+
// a stale value. Read under the un-rotation substitution (post-loop the params hold their
|
|
1264
|
+
// updated values), and structure the exit region under the same substitution so a post-loop
|
|
1265
|
+
// use of a loop value reads its name.
|
|
1266
|
+
out.push(...withSub(sub, () => [...argAssigns(li.header, li.exit, sub), ...structureRegion(li.exit, stop)]));
|
|
1267
|
+
return out;
|
|
1268
|
+
}
|
|
1269
|
+
}
|
|
1270
|
+
|
|
1271
|
+
// Conditional latch / in-body early exit: one edge of this cond_br is the loop back-edge (a
|
|
1272
|
+
// continue to `loopCtx.header`); the other LEAVES the loop. When the leaving edge lands on the
|
|
1273
|
+
// loop's own exit block it is a `break`; when it trampolines to a `return` it is an early `return`.
|
|
1274
|
+
// Emit the loop update (the back-edge args, RAW) then a single `if (leaveCond) { <exit arm> }` — the
|
|
1275
|
+
// back-edge arm is the implicit continue (control falls to the loop bottom). Guarded to leaving
|
|
1276
|
+
// edges only (`!body.has(exitB)` AND a break/return target); an in-body conditional continue still
|
|
1277
|
+
// declines (falls through → the header re-entry trips `onStack`, an honest loud fail).
|
|
1278
|
+
if (loopCtx && (takenB === loopCtx.header || fallB === loopCtx.header)) {
|
|
1279
|
+
const contIsTaken = takenB === loopCtx.header;
|
|
1280
|
+
const exitB = contIsTaken ? fallB : takenB;
|
|
1281
|
+
const isBreak = exitB === loopCtx.exit;
|
|
1282
|
+
// SOUNDNESS (break-clobber): a structured `break` jumps to AFTER the loop, where the header→exit
|
|
1283
|
+
// phi copies are emitted (emitTestAtTopWhile / emitDoWhile). If those copies are NON-identity, the
|
|
1284
|
+
// break path would fall through them and CLOBBER the value the break carried into the exit param.
|
|
1285
|
+
// Only emit `break` for a WHILE header whose header→exit copy is empty (the exit param already
|
|
1286
|
+
// coalesces across both edges); otherwise decline (fall through → honest loud fail). A do-while
|
|
1287
|
+
// `break` declines here (its exit copies live post-loop too, but the check differs) — the
|
|
1288
|
+
// return-trampoline path below still serves both. Trampolines are immune (the `return` terminates
|
|
1289
|
+
// the arm, so nothing falls through).
|
|
1290
|
+
const breakSafe = whileLoops.has(loopCtx.header) && argAssigns(loopCtx.header, loopCtx.exit).length === 0;
|
|
1291
|
+
// The back-edge substitution: each header param's back-edge arg → the param's name, so a test that
|
|
1292
|
+
// reads a POST-update value (the value carried to the header) shows the header var name.
|
|
1293
|
+
const sub = subFor(loopCtx.header.params, successorTo(b, loopCtx.header)!.args);
|
|
1294
|
+
// SOUNDNESS (pre-update-read hazard): the loop update (`argAssigns(b, header)`) is emitted
|
|
1295
|
+
// BEFORE the exit test/args. If the test or an exit arg reads a PRE-update value of an
|
|
1296
|
+
// induction variable (a body/header param, NOT a back-edge arg) whose coalesced name the
|
|
1297
|
+
// update overwrites, the emitted C would read the post-update value → a silent miscompile
|
|
1298
|
+
// (break/return fires on the wrong value; e.g. a test on `v0` where the update did
|
|
1299
|
+
// `v0 = v0 - 1`). Back-edge args (in `sub`) are the INTENDED post-update reads and are safe.
|
|
1300
|
+
// `readsClobbered` distinguishes the two at the VALUE level; on a hazard, decline (fall
|
|
1301
|
+
// through → honest loud fail) rather than emit wrong code.
|
|
1302
|
+
const updateCopies = argAssigns(b, loopCtx.header);
|
|
1303
|
+
const updateWrites = updateWriteSet(updateCopies);
|
|
1304
|
+
const exitArgs = (successorTo(b, exitB)?.args ?? []) as Value[];
|
|
1305
|
+
// The exit ARM may also read loop-body-computed values directly (an exitB dominated by `b`
|
|
1306
|
+
// — e.g. its `ret` operand), not just through edge args: apply the same escape test to the
|
|
1307
|
+
// arm's region (blocks reachable from exitB outside the loop body).
|
|
1308
|
+
const exitRegion = new Set([exitB, ...reachFrom(exitB)].filter((x) => !loopCtx!.body.has(x)));
|
|
1309
|
+
const hazard = loopUpdateHazard(
|
|
1310
|
+
term.operands[0],
|
|
1311
|
+
exitArgs,
|
|
1312
|
+
loopCtx.body,
|
|
1313
|
+
sub,
|
|
1314
|
+
updateWrites,
|
|
1315
|
+
exitRegion,
|
|
1316
|
+
new Set(loopCtx.header.params),
|
|
1317
|
+
);
|
|
1318
|
+
if (
|
|
1319
|
+
!hazard &&
|
|
1320
|
+
!loopCtx.body.has(exitB) &&
|
|
1321
|
+
((isBreak && breakSafe) || (!isBreak && leadsToReturnOnly(exitB, loopCtx.body)))
|
|
1322
|
+
) {
|
|
1323
|
+
out.push(...updateCopies); // the loop update, RAW (i++, p>>=1, …)
|
|
1324
|
+
let leaveCond = exprWith(sub)(term.operands[0]);
|
|
1325
|
+
if (contIsTaken) {
|
|
1326
|
+
leaveCond = negate(leaveCond);
|
|
1327
|
+
} // continue is `taken` → leave when NOT it
|
|
1328
|
+
const exitArm = isBreak
|
|
1329
|
+
? [...argAssigns(b, loopCtx.exit, sub), { k: 'break' } as Stmt] // break to the loop exit
|
|
1330
|
+
: withSub(sub, () => [...argAssigns(b, exitB, sub), ...structureRegion(exitB, stop)]); // early return
|
|
1331
|
+
out.push(mkIf(leaveCond, exitArm, []));
|
|
1332
|
+
return out;
|
|
1333
|
+
}
|
|
1334
|
+
}
|
|
1335
|
+
|
|
1336
|
+
// Regime-A switch: if this cond_br roots a comparison tree over a single scrutinee, emit a
|
|
1337
|
+
// `switch`. A pre-check here — mirroring the guard-fused-loop check above — so it sees the raw
|
|
1338
|
+
// tree before if-recovery claims the diamonds. Declines (null) fall through to plain if-recovery.
|
|
1339
|
+
const asSwitch = recognizeSwitch(b, stop);
|
|
1340
|
+
if (asSwitch) {
|
|
1341
|
+
out.push(...asSwitch);
|
|
1342
|
+
return out;
|
|
1343
|
+
}
|
|
1344
|
+
|
|
1345
|
+
const cond = expr(term.operands[0]);
|
|
1346
|
+
const ipd = ipdom.get(b) ?? null; // null ⇒ the arms diverge (both reach EXIT), no join
|
|
1347
|
+
const merge = ipd ?? stop;
|
|
1348
|
+
// Per-successor records, NOT successorTo(b, block): a cond_br whose two edges reach the SAME
|
|
1349
|
+
// block with different args would otherwise give both arms the first edge's copies.
|
|
1350
|
+
const thenS = [...argAssignsFor(b, term.successors[0]), ...structureRegion(takenB, merge)];
|
|
1351
|
+
const elseS = [...argAssignsFor(b, term.successors[1]), ...structureRegion(fallB, merge)];
|
|
1352
|
+
if (ipd === null && thenS.length && elseS.length && preserveDivergentBranchSense) {
|
|
1353
|
+
// Divergent arms (both terminate — no reconvergence). The asm branched forward to the
|
|
1354
|
+
// `taken` block and fell through to `fall`; a compiler that PRESERVES source branch direction
|
|
1355
|
+
// re-emits that as a forward branch on the NEGATED condition to the else-arm, so putting the
|
|
1356
|
+
// taken arm as `else` (and negating) reproduces the original branch sense. Byte-exact on
|
|
1357
|
+
// IDO/MIPS; agbcc/GCC canonicalise either way, so it is safe there too. A compiler that
|
|
1358
|
+
// inverts branch canonicalization sets preserveDivergentBranchSense false and falls through
|
|
1359
|
+
// to the positive form below.
|
|
1360
|
+
out.push({ k: 'if', cond: negate(cond), then: elseS, else: thenS });
|
|
1361
|
+
return out;
|
|
1362
|
+
}
|
|
1363
|
+
out.push(mkIf(cond, thenS, elseS));
|
|
1364
|
+
if (merge && merge !== stop) {
|
|
1365
|
+
out.push(...structureRegion(merge, stop));
|
|
1366
|
+
}
|
|
1367
|
+
return out;
|
|
1368
|
+
};
|
|
1369
|
+
|
|
1370
|
+
// The un-rotation / back-edge substitution: each header param's back-edge arg → the param's
|
|
1371
|
+
// name, so a latch test (and exit copies) reads the post-update value under the param's name.
|
|
1372
|
+
// ONE builder for all three loop emitters (self-loop, do-while, early-exit).
|
|
1373
|
+
const subFor = (params: Value[], backArgs: Value[]): Map<Value, string> => {
|
|
1374
|
+
const sub = new Map<Value, string>();
|
|
1375
|
+
params.forEach((p, i) => sub.set(backArgs[i], varName.get(p)!));
|
|
1376
|
+
return sub;
|
|
1377
|
+
};
|
|
1378
|
+
const loopSub = (li: LoopInfo): Map<Value, string> => subFor(li.header.params, li.backArgOfParam);
|
|
1379
|
+
|
|
1380
|
+
// Un-rotate a header's do-while latch into a `while`: the test reads the header's own
|
|
1381
|
+
// params (back-edge args substituted back), and the body is the header's SIDE EFFECTS in
|
|
1382
|
+
// program order followed by its parallel update. The side effects are required — a copies-only
|
|
1383
|
+
// body would silently delete every store/discarded call in the header. Effect order is right by
|
|
1384
|
+
// construction: statements read pre-update names, the updates land after.
|
|
1385
|
+
const emitWhile = (li: LoopInfo, updates?: Stmt[]): Stmt => {
|
|
1386
|
+
const term = li.header.ops[li.header.ops.length - 1];
|
|
1387
|
+
let cond = exprWith(loopSub(li))(term.operands[0]);
|
|
1388
|
+
if (term.successors[0].block !== li.header) {
|
|
1389
|
+
cond = negate(cond);
|
|
1390
|
+
} // loop-continue must be `taken`
|
|
1391
|
+
const body = [...sideEffects(li.header), ...(updates ?? argAssigns(li.header, li.header))];
|
|
1392
|
+
return { k: 'while', cond, body };
|
|
1393
|
+
};
|
|
1394
|
+
|
|
1395
|
+
// Test-at-top `while`: the header's cond_br is the loop condition. The body is a region that stops
|
|
1396
|
+
// at the header (the back-edge = end-of-iteration; the latch's argAssigns emit the loop update where
|
|
1397
|
+
// it structurally lands). Polarity: the continue edge is the body-entry; negate iff body-entry
|
|
1398
|
+
// is the FALL-THROUGH (successors[1]) — NOT the self-loop-relative test emitWhile uses.
|
|
1399
|
+
const emitTestAtTopWhile = (wl: WhileLoopInfo, stop: Block | null): Stmt[] => {
|
|
1400
|
+
const term = wl.header.ops[wl.header.ops.length - 1];
|
|
1401
|
+
let cond = expr(term.operands[0]);
|
|
1402
|
+
if (term.successors[1].block === wl.bodyEntry) {
|
|
1403
|
+
cond = negate(cond);
|
|
1404
|
+
}
|
|
1405
|
+
// The header→bodyEntry edge may carry non-identity phi args (a value the header COMPUTED and passes
|
|
1406
|
+
// into the body). Those copies must open the body — dropping them reads an uninitialised local.
|
|
1407
|
+
// Mirror the br/cond_br cases: argAssigns then structureRegion. Structure the body under this
|
|
1408
|
+
// loop's frame so an in-body conditional exit (break / early return) is recognised instead of
|
|
1409
|
+
// tripping the header-re-entry `onStack` guard.
|
|
1410
|
+
const body = withLoop({ header: wl.header, exit: wl.exit, body: wl.body }, () => [
|
|
1411
|
+
...argAssigns(wl.header, wl.bodyEntry),
|
|
1412
|
+
...structureRegion(wl.bodyEntry, wl.header),
|
|
1413
|
+
]);
|
|
1414
|
+
const out: Stmt[] = [{ k: 'while', cond, body }];
|
|
1415
|
+
out.push(...argAssigns(wl.header, wl.exit)); // phi args carried on the header→exit edge, if any
|
|
1416
|
+
out.push(...structureRegion(wl.exit, stop));
|
|
1417
|
+
return out;
|
|
1418
|
+
};
|
|
1419
|
+
|
|
1420
|
+
// The latch back-edge substitution (do-while) — subFor over the latch's back-edge args.
|
|
1421
|
+
const latchSub = (dw: DoWhileInfo): Map<Value, string> =>
|
|
1422
|
+
subFor(dw.header.params, successorTo(dw.latch, dw.header)!.args);
|
|
1423
|
+
|
|
1424
|
+
// Bottom-test `do-while`: the body runs header..latch (structured, with `b`'s do-while hook masked
|
|
1425
|
+
// via dwActive), then the latch's own side-effects + the loop update; the latch's cond_br test is the
|
|
1426
|
+
// do-while condition, read under `latchSub` (post-update the params hold their next value). Polarity:
|
|
1427
|
+
// the loop-CONTINUE edge is the back-edge to the header; negate iff that is the FALL-THROUGH.
|
|
1428
|
+
const emitDoWhile = (dw: DoWhileInfo, stop: Block | null): Stmt[] => {
|
|
1429
|
+
// The bottom test and everything post-loop render under `latchSub` — the update copies have
|
|
1430
|
+
// ALREADY run by then, so a condition/exit-arg/escaped-value read of an updated loop variable
|
|
1431
|
+
// that does NOT go through a sub-mapped back-edge arg means the PRE-update value (the
|
|
1432
|
+
// `i++ < n` shape: `icmp %i, %n` reads the pre-increment %i) and would render as the
|
|
1433
|
+
// post-update name — one iteration off, silently. Same readsClobbered guard the early-exit
|
|
1434
|
+
// path applies; on a hazard, decline LOUD.
|
|
1435
|
+
const sub = latchSub(dw);
|
|
1436
|
+
const updates = argAssigns(dw.latch, dw.header);
|
|
1437
|
+
const updateWrites = updateWriteSet(updates);
|
|
1438
|
+
const lterm = dw.latch.ops[dw.latch.ops.length - 1];
|
|
1439
|
+
const exitArgs = (successorTo(dw.latch, dw.exit)?.args ?? []) as Value[];
|
|
1440
|
+
if (loopUpdateHazard(lterm.operands[0], exitArgs, dw.body, sub, updateWrites, null, new Set(dw.header.params))) {
|
|
1441
|
+
throw new StructureError(
|
|
1442
|
+
`cannot structure '${fn.name}': do-while condition or a post-loop value reads a pre-update loop variable`,
|
|
1443
|
+
);
|
|
1444
|
+
}
|
|
1445
|
+
dwActive.add(dw.header);
|
|
1446
|
+
// structure the header's own block up to the latch. Call structureBlock DIRECTLY (not
|
|
1447
|
+
// structureRegion): the header is already on `onStack` from the caller's structureRegion, so
|
|
1448
|
+
// re-entering it via structureRegion would trip the back-edge guard. dwActive masks the do-while
|
|
1449
|
+
// hook so this pass structures `header` as an ordinary block (its ifs reconverge at the latch=stop).
|
|
1450
|
+
// Structure the header..latch body under this loop's frame so an in-body conditional exit
|
|
1451
|
+
// (break / early return before the bottom test) is recognised rather than declining.
|
|
1452
|
+
const inner =
|
|
1453
|
+
dw.header === dw.latch
|
|
1454
|
+
? [] // single-block self-loop: the header IS the latch — its ops render via sideEffects below
|
|
1455
|
+
: withLoop({ header: dw.header, exit: dw.exit, body: dw.body }, () => structureBlock(dw.header, dw.latch)); // header..latch (exclusive of latch)
|
|
1456
|
+
dwActive.delete(dw.header);
|
|
1457
|
+
// The UPDATE is RAW (`v = v - 1`) — it IS the decrement; applying `sub` would make it look like the
|
|
1458
|
+
// identity `v = v` and drop it. Only the CONDITION and EXIT copies use `sub` (post-update the param
|
|
1459
|
+
// already holds the next value, so the latch-computed test reads `v`, not `v - 1`). `updates`
|
|
1460
|
+
// reuses the hazard check's computation — a second argAssigns call would burn a spurious
|
|
1461
|
+
// swap-cycle temp number.
|
|
1462
|
+
const body = [...inner, ...sideEffects(dw.latch), ...updates];
|
|
1463
|
+
let cond = exprWith(sub)(lterm.operands[0]);
|
|
1464
|
+
if (lterm.successors[1].block === dw.header) {
|
|
1465
|
+
cond = negate(cond);
|
|
1466
|
+
} // continue edge must be `taken`
|
|
1467
|
+
const out: Stmt[] = [{ k: 'dowhile', cond, body }];
|
|
1468
|
+
// The exit region reads latch back-edge values under `sub` (post-loop they live in the loop vars).
|
|
1469
|
+
out.push(...withSub(sub, () => [...argAssigns(dw.latch, dw.exit, sub), ...structureRegion(dw.exit, stop)]));
|
|
1470
|
+
return out;
|
|
1471
|
+
};
|
|
1472
|
+
|
|
1473
|
+
const body = recognizeForLoops(structureRegion(entry, null));
|
|
1474
|
+
// v* = coalesced/materialized locals; t* = sequentialize's swap-cycle temps (varType-only —
|
|
1475
|
+
// they have no Value, so they are collected from varType, not varName).
|
|
1476
|
+
const localNames = [...new Set([...varName.values(), ...[...varType.keys()].filter((n) => /^t\d+$/.test(n))])].filter(
|
|
1477
|
+
(n) => /^[vt]\d+$/.test(n) && !globalNames.has(n),
|
|
1478
|
+
);
|
|
1479
|
+
const structs = collectStructs(fn);
|
|
1480
|
+
return {
|
|
1481
|
+
name: fn.name,
|
|
1482
|
+
params: entry.params.map((p, i) => ({ name: `a${i}`, type: p.type })),
|
|
1483
|
+
locals: localNames.map((n) => ({ name: n, type: varType.get(n)! })),
|
|
1484
|
+
retType: returnsVoid ? T.void() : returnType(fn),
|
|
1485
|
+
body,
|
|
1486
|
+
...(structs.length ? { structs } : {}),
|
|
1487
|
+
};
|
|
1488
|
+
}
|
|
1489
|
+
|
|
1490
|
+
// Does any statement CONTINUE this loop (vs. a nested one)? A `continue` inside a nested while/dowhile/
|
|
1491
|
+
// for targets THAT loop, so we do not descend into them; but `if`/`switch` do not capture `continue`,
|
|
1492
|
+
// so we scan through those. DEFENSIVE: the structurer does not currently emit an explicit `continue`
|
|
1493
|
+
// node (the in-body early-exit is an IMPLICIT continue — a fall-through to the loop bottom — and a
|
|
1494
|
+
// conditional continue DECLINES), so this never fires today; it guards the one case where the while→for
|
|
1495
|
+
// re-bracketing would change semantics (a `continue` runs `inc` under `for`, skips it under `while`).
|
|
1496
|
+
function hasEnclosingContinue(stmts: Stmt[]): boolean {
|
|
1497
|
+
const scan = (s: Stmt): boolean => {
|
|
1498
|
+
switch (s.k) {
|
|
1499
|
+
case 'continue':
|
|
1500
|
+
return true;
|
|
1501
|
+
case 'if':
|
|
1502
|
+
return s.then.some(scan) || s.else.some(scan);
|
|
1503
|
+
case 'switch':
|
|
1504
|
+
return s.cases.some((c) => c.body.some(scan)) || (s.default ?? []).some(scan);
|
|
1505
|
+
case 'while':
|
|
1506
|
+
case 'dowhile':
|
|
1507
|
+
case 'for':
|
|
1508
|
+
return false; // nested loop captures its own continue
|
|
1509
|
+
default:
|
|
1510
|
+
return false;
|
|
1511
|
+
}
|
|
1512
|
+
};
|
|
1513
|
+
return stmts.some(scan);
|
|
1514
|
+
}
|
|
1515
|
+
|
|
1516
|
+
// Re-spell an eligible test-at-top `while` as a `for` (quality only). PURELY cosmetic — the C
|
|
1517
|
+
// desugaring `for(init;cond;inc){body}` compiles identically to `init; while(cond){body; inc}`, so it
|
|
1518
|
+
// NEVER changes a byte-exact match. Conservative preconditions (a Ghidra `findLoopVariable`-style
|
|
1519
|
+
// recognition that moves NO op — the init already precedes the loop, the increment is already the
|
|
1520
|
+
// body's last statement):
|
|
1521
|
+
// • the `while` is IMMEDIATELY preceded by `assign(iv, e0)` (the init literally precedes it);
|
|
1522
|
+
// • the `while` cond references `iv`;
|
|
1523
|
+
// • the body's LAST statement is `assign(iv, e1)` with `e1` referencing `iv` (a genuine self-update —
|
|
1524
|
+
// the increment is literally the body's last op, not an unrelated trailing assign);
|
|
1525
|
+
// • the body EXCLUDING that increment has no `continue` targeting THIS loop — a `continue` RUNS `inc`
|
|
1526
|
+
// under `for` but SKIPS it under `while`, so folding the increment into the header would change
|
|
1527
|
+
// semantics. This is the one hazard of the transform; all others are pure re-bracketing.
|
|
1528
|
+
// Any precondition failing leaves the `while` untouched. Runs bottom-up so inner loops convert first.
|
|
1529
|
+
function recognizeForLoops(stmts: Stmt[]): Stmt[] {
|
|
1530
|
+
const out: Stmt[] = [];
|
|
1531
|
+
for (const s0 of stmts) {
|
|
1532
|
+
const s: Stmt =
|
|
1533
|
+
s0.k === 'if'
|
|
1534
|
+
? { ...s0, then: recognizeForLoops(s0.then), else: recognizeForLoops(s0.else) }
|
|
1535
|
+
: s0.k === 'while' || s0.k === 'dowhile'
|
|
1536
|
+
? { ...s0, body: recognizeForLoops(s0.body) }
|
|
1537
|
+
: s0.k === 'for'
|
|
1538
|
+
? { ...s0, body: recognizeForLoops(s0.body) }
|
|
1539
|
+
: s0.k === 'switch'
|
|
1540
|
+
? {
|
|
1541
|
+
...s0,
|
|
1542
|
+
cases: s0.cases.map((c) => ({ ...c, body: recognizeForLoops(c.body) })),
|
|
1543
|
+
...(s0.default ? { default: recognizeForLoops(s0.default) } : {}),
|
|
1544
|
+
}
|
|
1545
|
+
: s0;
|
|
1546
|
+
|
|
1547
|
+
const prev = out[out.length - 1];
|
|
1548
|
+
if (s.k === 'while' && s.body.length >= 1 && prev && prev.k === 'assign') {
|
|
1549
|
+
const iv = prev.name;
|
|
1550
|
+
const inc = s.body[s.body.length - 1];
|
|
1551
|
+
if (
|
|
1552
|
+
inc.k === 'assign' &&
|
|
1553
|
+
inc.name === iv &&
|
|
1554
|
+
exprVars(inc.value).has(iv) &&
|
|
1555
|
+
exprVars(s.cond).has(iv) &&
|
|
1556
|
+
!hasEnclosingContinue(s.body.slice(0, -1))
|
|
1557
|
+
) {
|
|
1558
|
+
out.pop(); // the init assign moves into the for-header
|
|
1559
|
+
out.push({ k: 'for', init: prev, cond: s.cond, inc, body: s.body.slice(0, -1) });
|
|
1560
|
+
continue;
|
|
1561
|
+
}
|
|
1562
|
+
}
|
|
1563
|
+
out.push(s);
|
|
1564
|
+
}
|
|
1565
|
+
return out;
|
|
1566
|
+
}
|
|
1567
|
+
|
|
1568
|
+
// Sequentialise a parallel copy: order the assignments so none writes a variable that a
|
|
1569
|
+
// still-pending assignment reads; break a cycle by spilling one destination to a temp.
|
|
1570
|
+
// `tmp` is the per-FUNCTION temp counter (threaded from structure()) so two independent
|
|
1571
|
+
// parallel copies never reuse a temp name against conflicting types.
|
|
1572
|
+
function sequentialize(
|
|
1573
|
+
copies: { name: string; value: Expr }[],
|
|
1574
|
+
varType: Map<string, IrType>,
|
|
1575
|
+
tmp: { n: number },
|
|
1576
|
+
fnName: string,
|
|
1577
|
+
): Stmt[] {
|
|
1578
|
+
// Two writes to one destination have no correct order — that is two phi params coalesced onto
|
|
1579
|
+
// one name, which canTakeName prevents upstream. Fail loud, never pick one.
|
|
1580
|
+
const dests = new Set<string>();
|
|
1581
|
+
for (const c of copies) {
|
|
1582
|
+
if (dests.has(c.name)) {
|
|
1583
|
+
throw new StructureError(`cannot structure '${fnName}': parallel copy writes '${c.name}' twice (coalescing bug)`);
|
|
1584
|
+
}
|
|
1585
|
+
dests.add(c.name);
|
|
1586
|
+
}
|
|
1587
|
+
const pending = copies.map((c) => ({ ...c, reads: exprVars(c.value) }));
|
|
1588
|
+
const out: Stmt[] = [];
|
|
1589
|
+
while (pending.length) {
|
|
1590
|
+
const i = pending.findIndex((a) => !pending.some((b) => b !== a && b.reads.has(a.name)));
|
|
1591
|
+
if (i >= 0) {
|
|
1592
|
+
const a = pending.splice(i, 1)[0];
|
|
1593
|
+
out.push({ k: 'assign', name: a.name, value: a.value });
|
|
1594
|
+
continue;
|
|
1595
|
+
}
|
|
1596
|
+
// All remaining form a cycle: spill one destination into a temp, rewrite its readers, and
|
|
1597
|
+
// RECOMPUTE their read-sets — with stale sets the spilled copy never becomes emittable and
|
|
1598
|
+
// the loop mints fresh temps forever.
|
|
1599
|
+
const a = pending[0];
|
|
1600
|
+
const t = `t${tmp.n++}`;
|
|
1601
|
+
varType.set(t, varType.get(a.name)!);
|
|
1602
|
+
out.push({ k: 'assign', name: t, value: { k: 'var', name: a.name } });
|
|
1603
|
+
for (const b of pending) {
|
|
1604
|
+
if (b !== a) {
|
|
1605
|
+
b.value = substVar(b.value, a.name, t);
|
|
1606
|
+
b.reads = exprVars(b.value);
|
|
1607
|
+
}
|
|
1608
|
+
}
|
|
1609
|
+
}
|
|
1610
|
+
return out;
|
|
1611
|
+
}
|
|
1612
|
+
// Read-set / rewrite walkers over the FULL Expr union — a walker that misses a node kind (e.g.
|
|
1613
|
+
// call/index/field reads) sequentializes a copy keyed by an array index in the wrong order.
|
|
1614
|
+
function exprVars(e: Expr, acc: Set<string> = new Set()): Set<string> {
|
|
1615
|
+
if (e.k === 'var') {
|
|
1616
|
+
acc.add(e.name);
|
|
1617
|
+
}
|
|
1618
|
+
for (const c of exprChildren(e)) {
|
|
1619
|
+
exprVars(c, acc);
|
|
1620
|
+
}
|
|
1621
|
+
return acc;
|
|
1622
|
+
}
|
|
1623
|
+
function substVar(e: Expr, from: string, to: string): Expr {
|
|
1624
|
+
if (e.k === 'var') {
|
|
1625
|
+
return e.name === from ? { k: 'var', name: to } : e;
|
|
1626
|
+
}
|
|
1627
|
+
return mapExprChildren(e, (c) => substVar(c, from, to));
|
|
1628
|
+
}
|
|
1629
|
+
|
|
1630
|
+
// empty-then peephole: `if (c) {} else { S }` → `if (!c) { S }`
|
|
1631
|
+
function mkIf(cond: Expr, thenS: Stmt[], elseS: Stmt[]): Stmt {
|
|
1632
|
+
if (thenS.length === 0 && elseS.length > 0) {
|
|
1633
|
+
return { k: 'if', cond: negate(cond), then: elseS, else: [] };
|
|
1634
|
+
}
|
|
1635
|
+
return { k: 'if', cond, then: thenS, else: elseS };
|
|
1636
|
+
}
|
|
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
|
+
|
|
1644
|
+
// --- CFG utilities ---
|
|
1645
|
+
function predecessorBlocks(fn: Fn): Map<Block, Block[]> {
|
|
1646
|
+
const m = new Map<Block, Block[]>();
|
|
1647
|
+
for (const b of fn.blocks) {
|
|
1648
|
+
m.set(b, []);
|
|
1649
|
+
}
|
|
1650
|
+
for (const b of fn.blocks) {
|
|
1651
|
+
for (const s of successorsOf(b)) {
|
|
1652
|
+
m.get(s)!.push(b);
|
|
1653
|
+
}
|
|
1654
|
+
}
|
|
1655
|
+
return m;
|
|
1656
|
+
}
|
|
1657
|
+
function successorTo(pred: Block, target: Block) {
|
|
1658
|
+
const term = pred.ops[pred.ops.length - 1];
|
|
1659
|
+
return term.successors.find((s) => s.block === target);
|
|
1660
|
+
}
|
|
1661
|
+
|
|
1662
|
+
// Immediate post-dominators. EXIT is represented as `null`; ret-blocks post-lead to it.
|
|
1663
|
+
function postDominators(fn: Fn): Map<Block, Block | null> {
|
|
1664
|
+
const nodes: (Block | null)[] = [null, ...fn.blocks];
|
|
1665
|
+
const succ = (b: Block): (Block | null)[] => {
|
|
1666
|
+
const term = b.ops[b.ops.length - 1];
|
|
1667
|
+
return term.opcode === 'ret' ? [null] : successorsOf(b);
|
|
1668
|
+
};
|
|
1669
|
+
const pdom = new Map<Block | null, Set<Block | null>>();
|
|
1670
|
+
pdom.set(null, new Set([null]));
|
|
1671
|
+
for (const b of fn.blocks) {
|
|
1672
|
+
pdom.set(b, new Set(nodes));
|
|
1673
|
+
}
|
|
1674
|
+
let changed = true;
|
|
1675
|
+
while (changed) {
|
|
1676
|
+
changed = false;
|
|
1677
|
+
for (const b of fn.blocks) {
|
|
1678
|
+
const ss = succ(b);
|
|
1679
|
+
let inter: Set<Block | null> | null = null;
|
|
1680
|
+
for (const s of ss) {
|
|
1681
|
+
const ps = pdom.get(s)!;
|
|
1682
|
+
if (inter === null) {
|
|
1683
|
+
inter = new Set(ps);
|
|
1684
|
+
continue;
|
|
1685
|
+
}
|
|
1686
|
+
for (const x of inter) {
|
|
1687
|
+
if (!ps.has(x)) {
|
|
1688
|
+
inter.delete(x);
|
|
1689
|
+
}
|
|
1690
|
+
} // intersect in place (spec-safe delete-in-iter)
|
|
1691
|
+
}
|
|
1692
|
+
const next = new Set<Block | null>(inter ?? []);
|
|
1693
|
+
next.add(b);
|
|
1694
|
+
if (!setEq(next, pdom.get(b)!)) {
|
|
1695
|
+
pdom.set(b, next);
|
|
1696
|
+
changed = true;
|
|
1697
|
+
}
|
|
1698
|
+
}
|
|
1699
|
+
}
|
|
1700
|
+
// ipdom(b) = the strict post-dom c with (strictPostDoms(b) \ {c}) ⊆ pdom(c)
|
|
1701
|
+
const ipdom = new Map<Block, Block | null>();
|
|
1702
|
+
for (const b of fn.blocks) {
|
|
1703
|
+
const strict = [...pdom.get(b)!].filter((c) => c !== b);
|
|
1704
|
+
let chosen: Block | null = null;
|
|
1705
|
+
for (const c of strict) {
|
|
1706
|
+
const others = strict.filter((x) => x !== c);
|
|
1707
|
+
if (others.every((x) => pdom.get(c)!.has(x))) {
|
|
1708
|
+
chosen = c;
|
|
1709
|
+
break;
|
|
1710
|
+
}
|
|
1711
|
+
}
|
|
1712
|
+
ipdom.set(b, chosen);
|
|
1713
|
+
}
|
|
1714
|
+
return ipdom;
|
|
1715
|
+
}
|
|
1716
|
+
function setEq<X>(a: Set<X>, b: Set<X>): boolean {
|
|
1717
|
+
if (a.size !== b.size) {
|
|
1718
|
+
return false;
|
|
1719
|
+
}
|
|
1720
|
+
for (const x of a) {
|
|
1721
|
+
if (!b.has(x)) {
|
|
1722
|
+
return false;
|
|
1723
|
+
}
|
|
1724
|
+
}
|
|
1725
|
+
return true;
|
|
1726
|
+
}
|