@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,410 @@
|
|
|
1
|
+
// asmlift structurer — the ANALYSIS phase. Pure derivation over the lifted fn — nothing here
|
|
2
|
+
// mutates the IR or depends on naming/emission state:
|
|
3
|
+
// • use-site registry — every use of a value, POSITIONED (op + block + index);
|
|
4
|
+
// • per-block SSA value liveness (backward dataflow) — consumed by the coalescing
|
|
5
|
+
// interference check in structure.ts;
|
|
6
|
+
// • the effect-ordering model — which call/load defs must MATERIALIZE as named temps at
|
|
7
|
+
// their own program position instead of inlining at their use.
|
|
8
|
+
import { Block, Fn, Op, Value, successorsOf } from '../ir/core';
|
|
9
|
+
|
|
10
|
+
export interface UseSite {
|
|
11
|
+
blk: Block;
|
|
12
|
+
idx: number;
|
|
13
|
+
op: Op;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface StructureAnalysis {
|
|
17
|
+
/** every positioned use of a value; a value absent here is dead */
|
|
18
|
+
useSitesOf: Map<Value, UseSite[]>;
|
|
19
|
+
opIndex: Map<Op, number>;
|
|
20
|
+
opBlock: Map<Op, Block>;
|
|
21
|
+
/** SSA values live at each block's entry */
|
|
22
|
+
liveIn: Map<Block, Set<Value>>;
|
|
23
|
+
/** call/load defs that must emit as named temps at their own position */
|
|
24
|
+
materialize: Set<Op>;
|
|
25
|
+
/** cached forward reachability (successors-transitive, excluding the start block itself) */
|
|
26
|
+
reachFrom: (b: Block) => Set<Block>;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function analyze(fn: Fn, returnsVoid: boolean): StructureAnalysis {
|
|
30
|
+
// ── use registry ────────────────────────────────────────────────────────────────────────
|
|
31
|
+
// Every use of a value, POSITIONED: the consuming op and its block/index. Successor args are
|
|
32
|
+
// uses AT the terminator (they render in argAssigns at block end). A void function's `ret`
|
|
33
|
+
// operand is a phantom, not a real use — skipping it lets a call whose result ONLY flows into
|
|
34
|
+
// the suppressed return read as a dead (side-effect) call, so `sideEffects()` emits it.
|
|
35
|
+
// One operand SLOT = one entry (an op reading a value twice records two uses — that count is
|
|
36
|
+
// what decides whether an inlined call would EXECUTE twice).
|
|
37
|
+
const useSitesOf = new Map<Value, UseSite[]>();
|
|
38
|
+
const opIndex = new Map<Op, number>();
|
|
39
|
+
const opBlock = new Map<Op, Block>();
|
|
40
|
+
const blockPos = new Map<Block, number>();
|
|
41
|
+
for (const b of fn.blocks) {
|
|
42
|
+
blockPos.set(b, blockPos.size);
|
|
43
|
+
b.ops.forEach((op, i) => {
|
|
44
|
+
opIndex.set(op, i);
|
|
45
|
+
opBlock.set(op, b);
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
// Linear program position (block order, then op order): a call "between" a def and a use is one
|
|
49
|
+
// whose position lies strictly between them. This is a PROXY for "a call on a def→use path", not
|
|
50
|
+
// the real dataflow: it checks position order only, not reachability. It can therefore FALSE-
|
|
51
|
+
// POSITIVE — a call in a forward SIBLING branch (never traversed on the def→use path) still sits
|
|
52
|
+
// between them by position (e.g. `def; if(c){call;ret} else {…use…use}`), and back-edges are not
|
|
53
|
+
// modelled either. That is SAFE ONLY BECAUSE the caller materializes exactly `const` ops: a const
|
|
54
|
+
// is a relocation-invariant leaf whose def dominates every use, so binding it to a local is
|
|
55
|
+
// UNCONDITIONALLY semantics-preserving on every path — a false positive costs at most a match (an
|
|
56
|
+
// extra `v =` the compiler would have re-inlined), caught by the zero-lost gate, never wrong C.
|
|
57
|
+
// RE-VERIFY this before widening the whitelist to any value that is not path-independent or that
|
|
58
|
+
// carries a use-site cast (an address computation `&g + i`), for which a false positive is unsound.
|
|
59
|
+
const linPos = (op: Op): number => blockPos.get(opBlock.get(op)!)! * 1e6 + opIndex.get(op)!;
|
|
60
|
+
const callPos: number[] = [];
|
|
61
|
+
for (const b of fn.blocks) {
|
|
62
|
+
for (const op of b.ops) {
|
|
63
|
+
if (op.opcode === 'call') {
|
|
64
|
+
callPos.push(linPos(op));
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
/** True if `def`'s value is still needed after a call — a call lies strictly between the def and
|
|
69
|
+
* one of its `consumers`. Such a value survives in a callee-saved register (a local), which is
|
|
70
|
+
* what materializing it reproduces. */
|
|
71
|
+
const liveAcrossCall = (def: Op, consumers: Op[]): boolean => {
|
|
72
|
+
const dp = linPos(def);
|
|
73
|
+
const usePos = consumers.map(linPos);
|
|
74
|
+
return callPos.some((c) => c > dp && usePos.some((u) => u > c));
|
|
75
|
+
};
|
|
76
|
+
for (const b of fn.blocks) {
|
|
77
|
+
b.ops.forEach((op, i) => {
|
|
78
|
+
if (returnsVoid && op.opcode === 'ret') {
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
const site: UseSite = { blk: b, idx: i, op };
|
|
82
|
+
const add = (v: Value) => {
|
|
83
|
+
const arr = useSitesOf.get(v);
|
|
84
|
+
if (arr) {
|
|
85
|
+
arr.push(site);
|
|
86
|
+
} else {
|
|
87
|
+
useSitesOf.set(v, [site]);
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
for (const u of op.operands) {
|
|
91
|
+
add(u);
|
|
92
|
+
}
|
|
93
|
+
for (const s of op.successors) {
|
|
94
|
+
for (const a of s.args) {
|
|
95
|
+
add(a);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// ── per-block liveness of SSA values ──────────────────────────────────────────────────────
|
|
102
|
+
// Backward dataflow. Successor args count as uses at the END of the predecessor (they render
|
|
103
|
+
// in the predecessor's argAssigns), so liveIn(B) means precisely "read at-or-after B's entry".
|
|
104
|
+
// Consumed by the coalescing interference check: merging two values that are ever
|
|
105
|
+
// simultaneously live into one variable name is the textbook silent clobber.
|
|
106
|
+
const liveIn = new Map<Block, Set<Value>>();
|
|
107
|
+
for (const b of fn.blocks) {
|
|
108
|
+
liveIn.set(b, new Set());
|
|
109
|
+
}
|
|
110
|
+
for (let liveChanged = true; liveChanged;) {
|
|
111
|
+
liveChanged = false;
|
|
112
|
+
for (let bi = fn.blocks.length - 1; bi >= 0; bi--) {
|
|
113
|
+
const b = fn.blocks[bi];
|
|
114
|
+
const live = new Set<Value>();
|
|
115
|
+
for (const s of successorsOf(b)) {
|
|
116
|
+
for (const v of liveIn.get(s)!) {
|
|
117
|
+
live.add(v);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
for (let oi = b.ops.length - 1; oi >= 0; oi--) {
|
|
121
|
+
const op = b.ops[oi];
|
|
122
|
+
for (const r of op.results) {
|
|
123
|
+
live.delete(r);
|
|
124
|
+
}
|
|
125
|
+
for (const s of op.successors) {
|
|
126
|
+
for (const a of s.args) {
|
|
127
|
+
live.add(a);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
if (!(returnsVoid && op.opcode === 'ret')) {
|
|
131
|
+
for (const u of op.operands) {
|
|
132
|
+
live.add(u);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
for (const p of b.params) {
|
|
137
|
+
live.delete(p);
|
|
138
|
+
}
|
|
139
|
+
const cur = liveIn.get(b)!;
|
|
140
|
+
if (live.size !== cur.size || ![...live].every((v) => cur.has(v))) {
|
|
141
|
+
liveIn.set(b, live);
|
|
142
|
+
liveChanged = true;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// ── the effect-ordering model — inline-at-use barriers ────────────────────────────────────
|
|
148
|
+
// `expr()` renders a def's computation AT ITS USE, which silently MOVES it: a call executes
|
|
149
|
+
// once per rendered copy (`foo(a0)+foo(a0)`), a load reads memory at the render point (it can
|
|
150
|
+
// textually sink past an aliasing store). The model: a call/load/aload def may inline ONLY
|
|
151
|
+
// when rendering cannot change behavior — exactly one render position, and the program-order
|
|
152
|
+
// gap between def and render crosses no memory write (loads) / no memory access at all (calls,
|
|
153
|
+
// whose own reads+writes must not reorder against anything). Every other case gets a NAMED
|
|
154
|
+
// TEMP assigned at the def's own program position (sideEffects) — which is precisely the
|
|
155
|
+
// register the compiler used.
|
|
156
|
+
const materialize = new Set<Op>();
|
|
157
|
+
const reachCache = new Map<Block, Set<Block>>();
|
|
158
|
+
const reachFrom = (b: Block): Set<Block> => {
|
|
159
|
+
let r = reachCache.get(b);
|
|
160
|
+
if (r) {
|
|
161
|
+
return r;
|
|
162
|
+
}
|
|
163
|
+
r = new Set<Block>();
|
|
164
|
+
const stack = [...successorsOf(b)];
|
|
165
|
+
while (stack.length) {
|
|
166
|
+
const x = stack.pop()!;
|
|
167
|
+
if (r.has(x)) {
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
r.add(x);
|
|
171
|
+
stack.push(...successorsOf(x));
|
|
172
|
+
}
|
|
173
|
+
reachCache.set(b, r);
|
|
174
|
+
return r;
|
|
175
|
+
};
|
|
176
|
+
// Reachability that never passes THROUGH `avoid` — the def-block-avoiding variant for
|
|
177
|
+
// per-iteration path checks: a path that re-enters the def's block re-executes the def, so
|
|
178
|
+
// writes on it belong to the NEXT dynamic instance (which re-renders anyway) and must not
|
|
179
|
+
// count against this one. Uncached (per-decision graphs are small).
|
|
180
|
+
const reachAvoiding = (from: Block, avoid: Block): Set<Block> => {
|
|
181
|
+
const r = new Set<Block>();
|
|
182
|
+
const stack = successorsOf(from).filter((s) => s !== avoid);
|
|
183
|
+
while (stack.length) {
|
|
184
|
+
const x = stack.pop()!;
|
|
185
|
+
if (r.has(x)) {
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
r.add(x);
|
|
189
|
+
for (const s of successorsOf(x)) {
|
|
190
|
+
if (s !== avoid && !r.has(s)) {
|
|
191
|
+
stack.push(s);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
return r;
|
|
196
|
+
};
|
|
197
|
+
// Where a value's expression is ultimately EMITTED: the anchored consumer (statement op,
|
|
198
|
+
// terminator, materialized def) it inlines into, transitively through single-use pure ops.
|
|
199
|
+
// null = renders in several places / unresolvable (treated conservatively by the caller).
|
|
200
|
+
const emitPosCache = new Map<Op, { blk: Block; idx: number } | null>();
|
|
201
|
+
const emitPos = (op: Op): { blk: Block; idx: number } | null => {
|
|
202
|
+
if (emitPosCache.has(op)) {
|
|
203
|
+
return emitPosCache.get(op)!;
|
|
204
|
+
}
|
|
205
|
+
const own = { blk: opBlock.get(op)!, idx: opIndex.get(op)! };
|
|
206
|
+
let res: { blk: Block; idx: number } | null;
|
|
207
|
+
if (
|
|
208
|
+
op.successors.length ||
|
|
209
|
+
op.opcode === 'ret' ||
|
|
210
|
+
op.opcode === 'store' ||
|
|
211
|
+
op.opcode === 'astore' ||
|
|
212
|
+
materialize.has(op) ||
|
|
213
|
+
!op.results.length ||
|
|
214
|
+
!useSitesOf.has(op.results[0])
|
|
215
|
+
) {
|
|
216
|
+
res = own; // statements, terminators, materialized/dead defs
|
|
217
|
+
} else {
|
|
218
|
+
const consumers = [...new Set((useSitesOf.get(op.results[0]) ?? []).map((s) => s.op))];
|
|
219
|
+
res = consumers.length === 1 ? emitPos(consumers[0]) : null;
|
|
220
|
+
}
|
|
221
|
+
emitPosCache.set(op, res);
|
|
222
|
+
return res;
|
|
223
|
+
};
|
|
224
|
+
// Decide in REVERSE program order so a consumer's own materialization is settled before any
|
|
225
|
+
// producer asks for its emit position (SSA: uses follow defs in dominance/layout order) — and
|
|
226
|
+
// iterate to a fixpoint for IR whose block layout does not follow dominance (hand-built IR):
|
|
227
|
+
// materialize only GROWS, and growing it only moves render positions closer / adds barriers,
|
|
228
|
+
// so the loop is monotone and converges.
|
|
229
|
+
for (let sizeBefore = -1; sizeBefore !== materialize.size;) {
|
|
230
|
+
sizeBefore = materialize.size;
|
|
231
|
+
emitPosCache.clear();
|
|
232
|
+
for (let bi = fn.blocks.length - 1; bi >= 0; bi--) {
|
|
233
|
+
const b = fn.blocks[bi];
|
|
234
|
+
for (let oi = b.ops.length - 1; oi >= 0; oi--) {
|
|
235
|
+
const op = b.ops[oi];
|
|
236
|
+
if (materialize.has(op)) {
|
|
237
|
+
continue;
|
|
238
|
+
}
|
|
239
|
+
if (op.opcode !== 'call' && op.opcode !== 'load' && op.opcode !== 'aload') {
|
|
240
|
+
// PURE value-producing op (a constant, an address computation, arithmetic — NOT a
|
|
241
|
+
// memory access). A value with ≥2 distinct-STATEMENT uses in the SSA is one the compiler
|
|
242
|
+
// kept in a register and reused: the frontend never dedups, so multi-use exists ONLY
|
|
243
|
+
// because the asm loaded/computed the value once and read the same register again.
|
|
244
|
+
// Inlining it re-derives the value at each use (a fresh pool load / repeated address
|
|
245
|
+
// arithmetic) — which the compiler did NOT do — so materialize it into a local instead,
|
|
246
|
+
// reproducing that register. Pure ⇒ every render is value-identical, so (unlike a load)
|
|
247
|
+
// no intervening memory write can invalidate a later render: multi-consumer suffices, no
|
|
248
|
+
// barrier scan. Scope: a `const` that is LIVE ACROSS A CALL. A value the compiler needs
|
|
249
|
+
// after a call must survive in a CALLEE-SAVED register — i.e. a local — because the call
|
|
250
|
+
// clobbers the caller-saved ones; the compiler therefore loads it ONCE and keeps it,
|
|
251
|
+
// exactly what materializing into a local reproduces (the base of `((s32 *)C)[i]` reused
|
|
252
|
+
// across `foo(...)` calls). WITHOUT a call in its live range the const is instead cheaply
|
|
253
|
+
// re-materialized at each use (a bare `movs r, #0` per init), so materializing it would
|
|
254
|
+
// ADD pointless copies and MISS — hence the call gate (the small-constant regression).
|
|
255
|
+
// Cheap deref casts still land on the `index` node at the use, preserving byte strides;
|
|
256
|
+
// NON-const pure ops are excluded (an address computation `&g + i` rendered standalone
|
|
257
|
+
// loses the memAccess's inline `(u8 *)` cast — cast-aware base materialization is separate).
|
|
258
|
+
const pr = op.results[0];
|
|
259
|
+
if (op.opcode === 'const' && pr && useSitesOf.has(pr)) {
|
|
260
|
+
const cons = [...new Set((useSitesOf.get(pr) ?? []).map((s) => s.op))];
|
|
261
|
+
if (cons.length > 1 && liveAcrossCall(op, cons)) {
|
|
262
|
+
materialize.add(op);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
continue;
|
|
266
|
+
}
|
|
267
|
+
const r = op.results[0];
|
|
268
|
+
if (!r || !useSitesOf.has(r)) {
|
|
269
|
+
continue;
|
|
270
|
+
} // dead call → exprstmt (unchanged)
|
|
271
|
+
const sites = useSitesOf.get(r)!;
|
|
272
|
+
const consumers = [...new Set(sites.map((s) => s.op))];
|
|
273
|
+
const isCall = op.opcode === 'call';
|
|
274
|
+
// A call must EXECUTE once — any second operand slot duplicates it → named temp.
|
|
275
|
+
if (isCall && sites.length > 1) {
|
|
276
|
+
materialize.add(op);
|
|
277
|
+
continue;
|
|
278
|
+
}
|
|
279
|
+
// A MULTI-RENDER load re-reads memory at each render — which is exactly what the original
|
|
280
|
+
// per-use source spelling did (`while (*s != EOS) *d = *s;` reads *s twice per iteration),
|
|
281
|
+
// so it is sound iff every render still sees the def-time memory: NO write anywhere
|
|
282
|
+
// between the def and ANY render (cycle-aware, conservative write set). Otherwise a temp.
|
|
283
|
+
if (!isCall && consumers.length > 1) {
|
|
284
|
+
const MW = new Set(['store', 'astore', 'call', 'opaque']);
|
|
285
|
+
const wDirty = (list: Op[], from: number, to: number) => {
|
|
286
|
+
for (let k = from; k < to; k++) {
|
|
287
|
+
if (MW.has(list[k].opcode)) {
|
|
288
|
+
return true;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
return false;
|
|
292
|
+
};
|
|
293
|
+
const defToRenderDirty = (q: { blk: Block; idx: number }): boolean => {
|
|
294
|
+
// Same block: the only def-avoiding path is the straight line between the two indices
|
|
295
|
+
// (leaving and re-entering the block re-crosses the def).
|
|
296
|
+
if (q.blk === b && oi < q.idx) {
|
|
297
|
+
return wDirty(b.ops, oi + 1, q.idx);
|
|
298
|
+
}
|
|
299
|
+
if (wDirty(b.ops, oi + 1, b.ops.length) || wDirty(q.blk.ops, 0, q.idx)) {
|
|
300
|
+
return true;
|
|
301
|
+
}
|
|
302
|
+
const between = reachAvoiding(b, b);
|
|
303
|
+
for (const x of between) {
|
|
304
|
+
if (x === q.blk && !reachAvoiding(q.blk, b).has(q.blk)) {
|
|
305
|
+
continue;
|
|
306
|
+
} // acyclic render blk: head checked
|
|
307
|
+
if (x !== q.blk && !reachAvoiding(x, b).has(q.blk)) {
|
|
308
|
+
continue;
|
|
309
|
+
} // not on a def→render path
|
|
310
|
+
if (wDirty(x.ops, 0, x.ops.length)) {
|
|
311
|
+
return true;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
return false;
|
|
315
|
+
};
|
|
316
|
+
const poss = consumers.map((c) => emitPos(c));
|
|
317
|
+
if (poss.some((p) => p === null) || poss.some((p) => defToRenderDirty(p!))) {
|
|
318
|
+
materialize.add(op);
|
|
319
|
+
}
|
|
320
|
+
continue;
|
|
321
|
+
}
|
|
322
|
+
const pos = emitPos(consumers[0]);
|
|
323
|
+
if (!pos) {
|
|
324
|
+
materialize.add(op);
|
|
325
|
+
continue;
|
|
326
|
+
}
|
|
327
|
+
// A between-op is a BARRIER when it renders as a sequenced statement the def would cross:
|
|
328
|
+
// stores/opaque always; a call/load that is dead (statement), materialized (statement), or
|
|
329
|
+
// inlined into a DIFFERENT statement. A sibling effect inlined into the SAME statement is
|
|
330
|
+
// not a reorder — the recompiling compiler orders unsequenced operands of one expression
|
|
331
|
+
// exactly as it originally chose to. Loads never bar a load (reads don't conflict).
|
|
332
|
+
const samePos = (q: { blk: Block; idx: number } | null) => q !== null && q.blk === pos.blk && q.idx === pos.idx;
|
|
333
|
+
const isBarrier = (x: Op): boolean => {
|
|
334
|
+
if (x.opcode === 'store') {
|
|
335
|
+
// A store to a PROVABLY-DISJOINT slot of the same base never aliases the load: same
|
|
336
|
+
// base SSA value, both constant offset+width, ranges non-overlapping (the everyday
|
|
337
|
+
// struct interleave `… = p->field_0; p->field_4 = …`). Anything less certain bars.
|
|
338
|
+
if (!isCall && op.opcode === 'load' && x.operands[0] === op.operands[0]) {
|
|
339
|
+
const lo = op.attrs.off as number,
|
|
340
|
+
lw = op.attrs.width as number;
|
|
341
|
+
const so = x.attrs.off as number,
|
|
342
|
+
sw = x.attrs.width as number;
|
|
343
|
+
if (so + sw <= lo || lo + lw <= so) {
|
|
344
|
+
return false;
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
return true;
|
|
348
|
+
}
|
|
349
|
+
if (x.opcode === 'astore' || x.opcode === 'opaque') {
|
|
350
|
+
return true;
|
|
351
|
+
}
|
|
352
|
+
if (x.opcode === 'call') {
|
|
353
|
+
return !x.results.length || !useSitesOf.has(x.results[0]) || materialize.has(x) || !samePos(emitPos(x));
|
|
354
|
+
}
|
|
355
|
+
if (!isCall) {
|
|
356
|
+
return false;
|
|
357
|
+
} // a load never bars a load
|
|
358
|
+
if (x.opcode === 'load' || x.opcode === 'aload') {
|
|
359
|
+
return !x.results.length || !useSitesOf.has(x.results[0])
|
|
360
|
+
? false // dead load: never emitted at all
|
|
361
|
+
: materialize.has(x) || !samePos(emitPos(x));
|
|
362
|
+
}
|
|
363
|
+
return false;
|
|
364
|
+
};
|
|
365
|
+
const gapDirty = (list: Op[], from: number, to: number) => {
|
|
366
|
+
for (let k = from; k < to; k++) {
|
|
367
|
+
if (isBarrier(list[k])) {
|
|
368
|
+
return true;
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
return false;
|
|
372
|
+
};
|
|
373
|
+
if (pos.blk === b) {
|
|
374
|
+
if (gapDirty(b.ops, oi + 1, pos.idx)) {
|
|
375
|
+
materialize.add(op);
|
|
376
|
+
}
|
|
377
|
+
continue;
|
|
378
|
+
}
|
|
379
|
+
// Cross-block: a call's execution would become path-dependent — always materialize. A
|
|
380
|
+
// load may inline only if NO write exists on any DEF-AVOIDING def→render path (a path
|
|
381
|
+
// re-crossing the def is the next dynamic instance): the def block's tail, the render
|
|
382
|
+
// block's head, and every block between; a render block cyclic WITHOUT passing the def
|
|
383
|
+
// (an inner loop around the render) is checked in full.
|
|
384
|
+
if (isCall) {
|
|
385
|
+
materialize.add(op);
|
|
386
|
+
continue;
|
|
387
|
+
}
|
|
388
|
+
let dirty = gapDirty(b.ops, oi + 1, b.ops.length) || gapDirty(pos.blk.ops, 0, pos.idx);
|
|
389
|
+
if (!dirty) {
|
|
390
|
+
for (const x of reachAvoiding(b, b)) {
|
|
391
|
+
if (x === pos.blk && !reachAvoiding(pos.blk, b).has(pos.blk)) {
|
|
392
|
+
continue;
|
|
393
|
+
} // acyclic render block: head checked
|
|
394
|
+
if (x !== pos.blk && !reachAvoiding(x, b).has(pos.blk)) {
|
|
395
|
+
continue;
|
|
396
|
+
} // not on a def→render path
|
|
397
|
+
if (gapDirty(x.ops, 0, x.ops.length)) {
|
|
398
|
+
dirty = true;
|
|
399
|
+
break;
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
if (dirty) {
|
|
404
|
+
materialize.add(op);
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
return { useSitesOf, opIndex, opBlock, liveIn, materialize, reachFrom };
|
|
410
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
// asmlift structurer — LOOP-EMISSION HAZARD checks: may this loop's updates be emitted before
|
|
2
|
+
// its condition/exit/post-loop reads, or would some read then see a clobbered (post-update)
|
|
3
|
+
// value that the original IR read PRE-update? Every check here is PURE — it reads the analysis
|
|
4
|
+
// maps and decides; nothing mutates — which is what lets the emission sites call it freely
|
|
5
|
+
// before committing to a loop form, and decline loud instead of miscompiling.
|
|
6
|
+
//
|
|
7
|
+
// The factory takes its dependencies EXPLICITLY (`LoopHazardDeps`), the switch-recover pattern.
|
|
8
|
+
// The maps are captured as LIVE REFERENCES, deliberately: `varName` is still being populated by
|
|
9
|
+
// the naming pipeline when the factory is created, and each hazard check reads whatever names
|
|
10
|
+
// exist at CALL time (emission runs after naming completes). Snapshotting them would break this.
|
|
11
|
+
import { Block, Op, Value } from '../ir/core';
|
|
12
|
+
import { Stmt } from '../l3/ast';
|
|
13
|
+
import type { UseSite } from './analysis';
|
|
14
|
+
|
|
15
|
+
export interface LoopHazardDeps {
|
|
16
|
+
/** value → defining op (defOpMap) */
|
|
17
|
+
defs: Map<Value, Op>;
|
|
18
|
+
/** value → adopted variable name — LIVE: populated by the naming pipeline, read at call time */
|
|
19
|
+
varName: Map<Value, string>;
|
|
20
|
+
/** every positioned use of a value (analysis.ts) */
|
|
21
|
+
useSitesOf: Map<Value, UseSite[]>;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface LoopHazards {
|
|
25
|
+
readsClobbered(v: Value, sub: Map<Value, string>, updateWrites: Set<string>): boolean;
|
|
26
|
+
loopEscapeHazard(
|
|
27
|
+
body: Set<Block>,
|
|
28
|
+
sub: Map<Value, string>,
|
|
29
|
+
updateWrites: Set<string>,
|
|
30
|
+
region?: Set<Block> | null,
|
|
31
|
+
loopParams?: Set<Value>,
|
|
32
|
+
): boolean;
|
|
33
|
+
loopUpdateHazard(
|
|
34
|
+
condV: Value,
|
|
35
|
+
exitArgs: Value[],
|
|
36
|
+
body: Set<Block>,
|
|
37
|
+
sub: Map<Value, string>,
|
|
38
|
+
updateWrites: Set<string>,
|
|
39
|
+
region: Set<Block> | null,
|
|
40
|
+
loopParams: Set<Value>,
|
|
41
|
+
): boolean;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** The names a loop update assigns (its non-identity copies) — the write set every loop-emission
|
|
45
|
+
* hazard check tests against. Dependency-free, so a plain function, not a factory member. */
|
|
46
|
+
export const updateWriteSet = (updates: Stmt[]): Set<string> =>
|
|
47
|
+
new Set(updates.filter((st): st is Extract<Stmt, { k: 'assign' }> => st.k === 'assign').map((st) => st.name));
|
|
48
|
+
|
|
49
|
+
export function makeLoopHazards(deps: LoopHazardDeps): LoopHazards {
|
|
50
|
+
const { defs, varName, useSitesOf } = deps;
|
|
51
|
+
|
|
52
|
+
// Does rendering `v` under `sub` read a variable that a pending loop update (`updateWrites`, the
|
|
53
|
+
// names it assigns) overwrites, via a path OTHER than a `sub`-mapped back-edge arg? Such a read is a
|
|
54
|
+
// PRE-update value the update clobbers → a read-after-write hazard when the update is emitted first.
|
|
55
|
+
// Walks the def-tree exactly like `exprWith`, stopping at `sub` values (intended post-update → safe)
|
|
56
|
+
// and named values (a var: hazard iff its name is a write-target). Pure (no mutation), so it is safe
|
|
57
|
+
// to call before emitting.
|
|
58
|
+
const readsClobbered = (v: Value, sub: Map<Value, string>, updateWrites: Set<string>): boolean => {
|
|
59
|
+
const seen = new Set<Value>();
|
|
60
|
+
const walk = (x: Value): boolean => {
|
|
61
|
+
if (seen.has(x)) {
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
seen.add(x);
|
|
65
|
+
if (sub.has(x)) {
|
|
66
|
+
return false;
|
|
67
|
+
} // sub-mapped → post-update, safe
|
|
68
|
+
if (varName.has(x)) {
|
|
69
|
+
return updateWrites.has(varName.get(x)!);
|
|
70
|
+
} // a named var: hazard iff clobbered
|
|
71
|
+
const d = defs.get(x);
|
|
72
|
+
return d ? d.operands.some(walk) : false; // inline (mirrors exprWith's recursion)
|
|
73
|
+
};
|
|
74
|
+
return walk(v);
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
// A value computed INSIDE a loop and used after it renders post-loop under `sub`, where each
|
|
78
|
+
// updated loop variable already holds its FINAL value. That is only correct when every
|
|
79
|
+
// loop-variable read goes through a sub-mapped back-edge arg (the intended post-update read); a
|
|
80
|
+
// direct read of an updated variable meant the LAST-ITERATION PRE-update value, which the
|
|
81
|
+
// post-loop name no longer holds. Scans every value defined in `body` for a use outside it (or,
|
|
82
|
+
// when `region` is given, inside that specific post-loop region) whose rendering readsClobbered
|
|
83
|
+
// flags. Same hazard test the early-exit path applies to its condition and edge args.
|
|
84
|
+
const loopEscapeHazard = (
|
|
85
|
+
body: Set<Block>,
|
|
86
|
+
sub: Map<Value, string>,
|
|
87
|
+
updateWrites: Set<string>,
|
|
88
|
+
region: Set<Block> | null = null,
|
|
89
|
+
loopParams: Set<Value> = new Set(),
|
|
90
|
+
): boolean => {
|
|
91
|
+
// Body-block PARAMS escape too: a non-loop-carried param whose adopted name the update writes
|
|
92
|
+
// reads post-loop as the clobbered value. canTakeName prevents that adoption, so this firing
|
|
93
|
+
// means a naming bug — decline loud, never emit. The loop's own carried params (`loopParams`)
|
|
94
|
+
// are exempt: their post-loop read of the updated name is exactly the intended final value.
|
|
95
|
+
const escaped = (v: Value): boolean => {
|
|
96
|
+
for (const s of useSitesOf.get(v) ?? []) {
|
|
97
|
+
if (region ? region.has(s.blk) : !body.has(s.blk)) {
|
|
98
|
+
return true;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return false;
|
|
102
|
+
};
|
|
103
|
+
for (const bb of body) {
|
|
104
|
+
for (const pv of bb.params) {
|
|
105
|
+
if (loopParams.has(pv)) {
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
if (escaped(pv) && updateWrites.has(varName.get(pv)!)) {
|
|
109
|
+
return true;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
for (const op of bb.ops) {
|
|
113
|
+
for (const r of op.results) {
|
|
114
|
+
if (escaped(r) && readsClobbered(r, sub, updateWrites)) {
|
|
115
|
+
return true;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return false;
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
// The loop-emission hazard check, in ONE place (shared by the guard-fused, early-exit, and
|
|
124
|
+
// do-while sites): the loop condition, the exit-edge args, and every escaped body value must
|
|
125
|
+
// read loop variables ONLY through sub-mapped back-edge args (post-update); any direct read of
|
|
126
|
+
// an updated name is a pre-update value the emitted C no longer holds. Callers keep their
|
|
127
|
+
// distinct decline behavior.
|
|
128
|
+
const loopUpdateHazard = (
|
|
129
|
+
condV: Value,
|
|
130
|
+
exitArgs: Value[],
|
|
131
|
+
body: Set<Block>,
|
|
132
|
+
sub: Map<Value, string>,
|
|
133
|
+
updateWrites: Set<string>,
|
|
134
|
+
region: Set<Block> | null,
|
|
135
|
+
loopParams: Set<Value>,
|
|
136
|
+
): boolean =>
|
|
137
|
+
readsClobbered(condV, sub, updateWrites) ||
|
|
138
|
+
exitArgs.some((a) => readsClobbered(a, sub, updateWrites)) ||
|
|
139
|
+
loopEscapeHazard(body, sub, updateWrites, region, loopParams);
|
|
140
|
+
|
|
141
|
+
return { readsClobbered, loopEscapeHazard, loopUpdateHazard };
|
|
142
|
+
}
|