@asmlift/core 0.4.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/backend/cfamily.ts +5 -2
- package/src/contracts.ts +166 -2
- package/src/frontend/mips.ts +13 -6
- package/src/frontend/opaque.ts +31 -18
- package/src/frontend/ppc.ts +18 -7
- package/src/frontend/ssa.ts +249 -5
- package/src/frontend/thumb.ts +1082 -72
- package/src/ir/alias.ts +75 -0
- package/src/ir/opcodes.ts +24 -14
- package/src/l3/argbase.ts +6 -1
- package/src/l3/ast.ts +9 -1
- package/src/l3/basecse.ts +57 -24
- package/src/l3/coalesce.ts +107 -38
- package/src/l3/dce.ts +31 -18
- package/src/l3/gates.ts +67 -0
- package/src/l3/scopebase.ts +11 -7
- package/src/l3/tailmerge.ts +8 -4
- package/src/pipeline.ts +60 -4
- package/src/raise/divpow2.ts +2 -1
- package/src/raise/gvn.ts +16 -6
- package/src/raise/pre-recovery.ts +4 -2
- package/src/raise/retsink.ts +5 -4
- package/src/raise/shortcircuit.ts +3 -5
- package/src/raise/struct-arrays.ts +2 -1
- package/src/raise/structs.ts +29 -1
- package/src/rank.ts +26 -2
- package/src/structure/analysis.ts +168 -123
- package/src/structure/structure.ts +228 -63
- package/src/structure/switch-recover.ts +96 -27
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
// interference check in structure.ts;
|
|
6
6
|
// • the effect-ordering model — which call/load defs must MATERIALIZE as named temps at
|
|
7
7
|
// their own program position instead of inlining at their use.
|
|
8
|
+
import { globalCellOf, mayWriteGlobal } from '../ir/alias';
|
|
8
9
|
import { Block, Fn, Op, Value, successorsOf } from '../ir/core';
|
|
9
10
|
|
|
10
11
|
export interface UseSite {
|
|
@@ -32,7 +33,34 @@ export interface StructureAnalysis {
|
|
|
32
33
|
memWriteBetween: (def: Op, render: { blk: Block; idx: number }, isWrite: (x: Op) => boolean) => boolean;
|
|
33
34
|
}
|
|
34
35
|
|
|
35
|
-
export
|
|
36
|
+
export interface AnalyzeOptions {
|
|
37
|
+
/** the fn's def map (`defOpMap`) — the structurer already holds one, so it is passed rather than
|
|
38
|
+
* rebuilt. Absent ⇒ the global-aware alias rule below cannot resolve anything and every write
|
|
39
|
+
* bars, exactly as before it existed. */
|
|
40
|
+
defs?: Map<Value, Op>;
|
|
41
|
+
/** THE value-home axis (rank.ts `/reread-globals`). A read of a named global is barred from
|
|
42
|
+
* rendering at its use by any write in between — even a store to an unrelated global, which
|
|
43
|
+
* cannot possibly change what it sees. That over-conservatism is what invents the locals the
|
|
44
|
+
* round-5 dogfood measured as its highest-cost defect ("hoists what agbcc re-reads"):
|
|
45
|
+
*
|
|
46
|
+
* gA = v; gB = v; with `s32 v = gValue;` where the source said `gA = gValue; gB = gValue;`
|
|
47
|
+
*
|
|
48
|
+
* With this on, the barrier scan for a load whose address resolves to a named global uses THE
|
|
49
|
+
* shared disjointness query (ir/alias.ts) instead of "any write at all". Materializing is always
|
|
50
|
+
* sound, so today's spelling is never wrong — only sometimes not the one the compiler was given.
|
|
51
|
+
* Which side matches is genuinely per-function (the same dogfood watched agbcc go both ways
|
|
52
|
+
* inside ONE function), so this is a differ-refereed candidate axis, never a default. */
|
|
53
|
+
rereadGlobals?: boolean;
|
|
54
|
+
/** "does the project declare this global volatile?" — a read of a volatile object may NOT be
|
|
55
|
+
* duplicated or moved, so the axis above refuses on one. Answers false for a symbol the map
|
|
56
|
+
* does not carry (and for no map at all), which is the same posture the multi-render rule has
|
|
57
|
+
* always had: without a declaration nothing here can know, and the differ referees the extra
|
|
58
|
+
* load. Where the map DOES know, the axis is silent about it rather than wrong. */
|
|
59
|
+
volatileGlobal?: (name: string) => boolean;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function analyze(fn: Fn, returnsVoid: boolean, opts: AnalyzeOptions = {}): StructureAnalysis {
|
|
63
|
+
const { defs, rereadGlobals = false, volatileGlobal } = opts;
|
|
36
64
|
// ── use registry ────────────────────────────────────────────────────────────────────────
|
|
37
65
|
// Every use of a value, POSITIONED: the consuming op and its block/index. Successor args are
|
|
38
66
|
// uses AT the terminator (they render in argAssigns at block end). A void function's `ret`
|
|
@@ -204,29 +232,116 @@ export function analyze(fn: Fn, returnsVoid: boolean): StructureAnalysis {
|
|
|
204
232
|
// terminator, materialized def) it inlines into, transitively through single-use pure ops.
|
|
205
233
|
// null = renders in several places / unresolvable (treated conservatively by the caller).
|
|
206
234
|
const emitPosCache = new Map<Op, { blk: Block; idx: number } | null>();
|
|
235
|
+
/** an op that renders AT ITS OWN position: a statement, a terminator, a materialized or dead def */
|
|
236
|
+
const anchored = (op: Op): boolean =>
|
|
237
|
+
op.successors.length > 0 ||
|
|
238
|
+
op.opcode === 'ret' ||
|
|
239
|
+
op.opcode === 'store' ||
|
|
240
|
+
op.opcode === 'astore' ||
|
|
241
|
+
materialize.has(op) ||
|
|
242
|
+
!op.results.length ||
|
|
243
|
+
!useSitesOf.has(op.results[0]);
|
|
244
|
+
const consumersOf = (op: Op): Op[] => [...new Set((useSitesOf.get(op.results[0]) ?? []).map((s) => s.op))];
|
|
207
245
|
const emitPos = (op: Op): { blk: Block; idx: number } | null => {
|
|
208
246
|
if (emitPosCache.has(op)) {
|
|
209
247
|
return emitPosCache.get(op)!;
|
|
210
248
|
}
|
|
211
|
-
const own = { blk: opBlock.get(op)!, idx: opIndex.get(op)! };
|
|
212
249
|
let res: { blk: Block; idx: number } | null;
|
|
213
|
-
if (
|
|
214
|
-
op.
|
|
215
|
-
op.opcode === 'ret' ||
|
|
216
|
-
op.opcode === 'store' ||
|
|
217
|
-
op.opcode === 'astore' ||
|
|
218
|
-
materialize.has(op) ||
|
|
219
|
-
!op.results.length ||
|
|
220
|
-
!useSitesOf.has(op.results[0])
|
|
221
|
-
) {
|
|
222
|
-
res = own; // statements, terminators, materialized/dead defs
|
|
250
|
+
if (anchored(op)) {
|
|
251
|
+
res = { blk: opBlock.get(op)!, idx: opIndex.get(op)! };
|
|
223
252
|
} else {
|
|
224
|
-
const consumers =
|
|
253
|
+
const consumers = consumersOf(op);
|
|
225
254
|
res = consumers.length === 1 ? emitPos(consumers[0]) : null;
|
|
226
255
|
}
|
|
227
256
|
emitPosCache.set(op, res);
|
|
228
257
|
return res;
|
|
229
258
|
};
|
|
259
|
+
// EVERY position a value's expression renders at — `emitPos` generalized to the whole set (it
|
|
260
|
+
// answers one place or gives up), by following ALL consumers transitively. That matters for
|
|
261
|
+
// the value-home axis: a pure expression with two consumers (`gOut = e; return e;`) has no single
|
|
262
|
+
// emit position, so `emitPos` answers null and every memory read feeding it is forced into a
|
|
263
|
+
// local — even when re-reading at both places is provably equivalent. Null only for a genuine
|
|
264
|
+
// cycle (defensive: SSA use-def is acyclic through ops), which the caller treats as unresolvable.
|
|
265
|
+
//
|
|
266
|
+
// NEVER for a call: two render positions mean two executions, so a call whose consumer renders in
|
|
267
|
+
// several places must keep answering null and materialize.
|
|
268
|
+
const emitPosSetCache = new Map<Op, { blk: Block; idx: number }[] | null>();
|
|
269
|
+
const emitPositions = (op: Op, visiting: Set<Op> = new Set()): { blk: Block; idx: number }[] | null => {
|
|
270
|
+
const hit = emitPosSetCache.get(op);
|
|
271
|
+
if (hit !== undefined) {
|
|
272
|
+
return hit;
|
|
273
|
+
}
|
|
274
|
+
if (visiting.has(op)) {
|
|
275
|
+
return null;
|
|
276
|
+
}
|
|
277
|
+
let res: { blk: Block; idx: number }[] | null;
|
|
278
|
+
if (anchored(op)) {
|
|
279
|
+
res = [{ blk: opBlock.get(op)!, idx: opIndex.get(op)! }];
|
|
280
|
+
} else {
|
|
281
|
+
visiting.add(op);
|
|
282
|
+
const seenPos = new Set<string>();
|
|
283
|
+
const acc: { blk: Block; idx: number }[] = [];
|
|
284
|
+
res = acc;
|
|
285
|
+
for (const c of consumersOf(op)) {
|
|
286
|
+
const sub = emitPositions(c, visiting);
|
|
287
|
+
if (!sub) {
|
|
288
|
+
res = null;
|
|
289
|
+
break;
|
|
290
|
+
}
|
|
291
|
+
for (const p of sub) {
|
|
292
|
+
const key = `${blockPos.get(p.blk)}:${p.idx}`;
|
|
293
|
+
if (!seenPos.has(key)) {
|
|
294
|
+
seenPos.add(key);
|
|
295
|
+
acc.push(p);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
visiting.delete(op);
|
|
300
|
+
}
|
|
301
|
+
emitPosSetCache.set(op, res);
|
|
302
|
+
return res;
|
|
303
|
+
};
|
|
304
|
+
// THE def→render path discipline — one implementation, three callers (the two materialization
|
|
305
|
+
// rules below and structure.ts's bitfield fold, which imports it). May an op `isWrite` accepts
|
|
306
|
+
// execute between `def` and a statement at `render`, on any def-avoiding path? The def block's
|
|
307
|
+
// tail, the render block's head, and every between-block on a path; a path re-crossing the def
|
|
308
|
+
// is the NEXT dynamic instance and does not count. Path-based on purpose: `fn.blocks` is ADDRESS
|
|
309
|
+
// order, so a linear-position scan misses a block laid out after the render that executes
|
|
310
|
+
// between def and render on the taken path (an audit round broke exactly that way).
|
|
311
|
+
const memWriteBetween = (def: Op, render: { blk: Block; idx: number }, isWrite: (x: Op) => boolean): boolean => {
|
|
312
|
+
const b = opBlock.get(def)!;
|
|
313
|
+
const oi = opIndex.get(def)!;
|
|
314
|
+
const wDirty = (list: Op[], from: number, to: number): boolean => {
|
|
315
|
+
for (let k = from; k < to; k++) {
|
|
316
|
+
if (isWrite(list[k])) {
|
|
317
|
+
return true;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
return false;
|
|
321
|
+
};
|
|
322
|
+
// Same block: the only def-avoiding path is the straight line between the two indices
|
|
323
|
+
// (leaving and re-entering the block re-crosses the def). A render BEFORE the def cannot
|
|
324
|
+
// happen — within a block, uses follow defs — and falls through to the path walk, whose
|
|
325
|
+
// answer is the conservative one.
|
|
326
|
+
if (render.blk === b && oi < render.idx) {
|
|
327
|
+
return wDirty(b.ops, oi + 1, render.idx);
|
|
328
|
+
}
|
|
329
|
+
if (wDirty(b.ops, oi + 1, b.ops.length) || wDirty(render.blk.ops, 0, render.idx)) {
|
|
330
|
+
return true;
|
|
331
|
+
}
|
|
332
|
+
for (const x of reachAvoiding(b, b)) {
|
|
333
|
+
if (x === render.blk && !reachAvoiding(render.blk, b).has(render.blk)) {
|
|
334
|
+
continue; // acyclic render block: head checked
|
|
335
|
+
}
|
|
336
|
+
if (x !== render.blk && !reachAvoiding(x, b).has(render.blk)) {
|
|
337
|
+
continue; // not on a def→render path
|
|
338
|
+
}
|
|
339
|
+
if (wDirty(x.ops, 0, x.ops.length)) {
|
|
340
|
+
return true;
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
return false;
|
|
344
|
+
};
|
|
230
345
|
// Decide in REVERSE program order so a consumer's own materialization is settled before any
|
|
231
346
|
// producer asks for its emit position (SSA: uses follow defs in dominance/layout order) — and
|
|
232
347
|
// iterate to a fixpoint for IR whose block layout does not follow dominance (hand-built IR):
|
|
@@ -235,6 +350,7 @@ export function analyze(fn: Fn, returnsVoid: boolean): StructureAnalysis {
|
|
|
235
350
|
for (let sizeBefore = -1; sizeBefore !== materialize.size;) {
|
|
236
351
|
sizeBefore = materialize.size;
|
|
237
352
|
emitPosCache.clear();
|
|
353
|
+
emitPosSetCache.clear(); // both render-position caches read `materialize`, which just grew
|
|
238
354
|
for (let bi = fn.blocks.length - 1; bi >= 0; bi--) {
|
|
239
355
|
const b = fn.blocks[bi];
|
|
240
356
|
for (let oi = b.ops.length - 1; oi >= 0; oi--) {
|
|
@@ -274,6 +390,14 @@ export function analyze(fn: Fn, returnsVoid: boolean): StructureAnalysis {
|
|
|
274
390
|
if (!r || !useSitesOf.has(r)) {
|
|
275
391
|
continue;
|
|
276
392
|
} // dead call → exprstmt (unchanged)
|
|
393
|
+
// Under the value-home axis: which named global cell this op reads, if any. A constant-
|
|
394
|
+
// offset `load` only — an `aload`'s runtime index names no single cell, and a call reads
|
|
395
|
+
// everything. Null ⇒ every write bars, exactly as before.
|
|
396
|
+
const cell =
|
|
397
|
+
rereadGlobals && defs && op.opcode === 'load'
|
|
398
|
+
? globalCellOf(defs, op.operands[0], op.attrs.off as number)
|
|
399
|
+
: null;
|
|
400
|
+
const barsThisRead = cell && defs && !volatileGlobal?.(cell.name) ? mayWriteGlobal(defs, cell.name) : null;
|
|
277
401
|
const sites = useSitesOf.get(r)!;
|
|
278
402
|
const consumers = [...new Set(sites.map((s) => s.op))];
|
|
279
403
|
const isCall = op.opcode === 'call';
|
|
@@ -286,50 +410,32 @@ export function analyze(fn: Fn, returnsVoid: boolean): StructureAnalysis {
|
|
|
286
410
|
// per-use source spelling did (`while (*s != EOS) *d = *s;` reads *s twice per iteration),
|
|
287
411
|
// so it is sound iff every render still sees the def-time memory: NO write anywhere
|
|
288
412
|
// between the def and ANY render (cycle-aware, conservative write set). Otherwise a temp.
|
|
289
|
-
|
|
413
|
+
//
|
|
414
|
+
// WHERE it renders. Without the axis: one position per consumer, and a consumer with no
|
|
415
|
+
// single position (its own value renders in several places) refuses. With the axis a load
|
|
416
|
+
// resolves the whole SET instead — the second half of the value-home defect, where the
|
|
417
|
+
// local is invented not by a barrier but because the pure expression downstream is itself
|
|
418
|
+
// duplicated (`gOut = (gValue << 1) + gValue; return (gValue << 1) + gValue;`). Never for a
|
|
419
|
+
// call: several positions there mean several executions.
|
|
420
|
+
const poss =
|
|
421
|
+
rereadGlobals && !isCall
|
|
422
|
+
? emitPositions(op)
|
|
423
|
+
: consumers.length > 1
|
|
424
|
+
? consumers.map((c) => emitPos(c))
|
|
425
|
+
: [emitPos(consumers[0])];
|
|
426
|
+
if (!poss || poss.some((p) => p === null)) {
|
|
427
|
+
materialize.add(op);
|
|
428
|
+
continue;
|
|
429
|
+
}
|
|
430
|
+
if (poss.length > 1) {
|
|
290
431
|
const MW = new Set(['store', 'astore', 'call', 'opaque']);
|
|
291
|
-
const
|
|
292
|
-
|
|
293
|
-
if (MW.has(list[k].opcode)) {
|
|
294
|
-
return true;
|
|
295
|
-
}
|
|
296
|
-
}
|
|
297
|
-
return false;
|
|
298
|
-
};
|
|
299
|
-
const defToRenderDirty = (q: { blk: Block; idx: number }): boolean => {
|
|
300
|
-
// Same block: the only def-avoiding path is the straight line between the two indices
|
|
301
|
-
// (leaving and re-entering the block re-crosses the def).
|
|
302
|
-
if (q.blk === b && oi < q.idx) {
|
|
303
|
-
return wDirty(b.ops, oi + 1, q.idx);
|
|
304
|
-
}
|
|
305
|
-
if (wDirty(b.ops, oi + 1, b.ops.length) || wDirty(q.blk.ops, 0, q.idx)) {
|
|
306
|
-
return true;
|
|
307
|
-
}
|
|
308
|
-
const between = reachAvoiding(b, b);
|
|
309
|
-
for (const x of between) {
|
|
310
|
-
if (x === q.blk && !reachAvoiding(q.blk, b).has(q.blk)) {
|
|
311
|
-
continue;
|
|
312
|
-
} // acyclic render blk: head checked
|
|
313
|
-
if (x !== q.blk && !reachAvoiding(x, b).has(q.blk)) {
|
|
314
|
-
continue;
|
|
315
|
-
} // not on a def→render path
|
|
316
|
-
if (wDirty(x.ops, 0, x.ops.length)) {
|
|
317
|
-
return true;
|
|
318
|
-
}
|
|
319
|
-
}
|
|
320
|
-
return false;
|
|
321
|
-
};
|
|
322
|
-
const poss = consumers.map((c) => emitPos(c));
|
|
323
|
-
if (poss.some((p) => p === null) || poss.some((p) => defToRenderDirty(p!))) {
|
|
432
|
+
const isWrite = barsThisRead ?? ((x: Op) => MW.has(x.opcode));
|
|
433
|
+
if (poss.some((p) => memWriteBetween(op, p!, isWrite))) {
|
|
324
434
|
materialize.add(op);
|
|
325
435
|
}
|
|
326
436
|
continue;
|
|
327
437
|
}
|
|
328
|
-
const pos =
|
|
329
|
-
if (!pos) {
|
|
330
|
-
materialize.add(op);
|
|
331
|
-
continue;
|
|
332
|
-
}
|
|
438
|
+
const pos = poss[0]!;
|
|
333
439
|
// A between-op is a BARRIER when it renders as a sequenced statement the def would cross:
|
|
334
440
|
// stores/opaque always; a call/load that is dead (statement), materialized (statement), or
|
|
335
441
|
// inlined into a DIFFERENT statement. A sibling effect inlined into the SAME statement is
|
|
@@ -337,6 +443,11 @@ export function analyze(fn: Fn, returnsVoid: boolean): StructureAnalysis {
|
|
|
337
443
|
// exactly as it originally chose to. Loads never bar a load (reads don't conflict).
|
|
338
444
|
const samePos = (q: { blk: Block; idx: number } | null) => q !== null && q.blk === pos.blk && q.idx === pos.idx;
|
|
339
445
|
const isBarrier = (x: Op): boolean => {
|
|
446
|
+
// Value-home axis: a store/astore this read is PROVABLY disjoint from (a different named
|
|
447
|
+
// global) does not sequence against it, so the read may still render at its use.
|
|
448
|
+
if (barsThisRead && (x.opcode === 'store' || x.opcode === 'astore') && !barsThisRead(x)) {
|
|
449
|
+
return false;
|
|
450
|
+
}
|
|
340
451
|
if (x.opcode === 'store') {
|
|
341
452
|
// A store to a PROVABLY-DISJOINT slot of the same base never aliases the load: same
|
|
342
453
|
// base SSA value, both constant offset+width, ranges non-overlapping (the everyday
|
|
@@ -368,84 +479,18 @@ export function analyze(fn: Fn, returnsVoid: boolean): StructureAnalysis {
|
|
|
368
479
|
}
|
|
369
480
|
return false;
|
|
370
481
|
};
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
return true;
|
|
375
|
-
}
|
|
376
|
-
}
|
|
377
|
-
return false;
|
|
378
|
-
};
|
|
379
|
-
if (pos.blk === b) {
|
|
380
|
-
if (gapDirty(b.ops, oi + 1, pos.idx)) {
|
|
381
|
-
materialize.add(op);
|
|
382
|
-
}
|
|
383
|
-
continue;
|
|
384
|
-
}
|
|
385
|
-
// Cross-block: a call's execution would become path-dependent — always materialize. A
|
|
386
|
-
// load may inline only if NO write exists on any DEF-AVOIDING def→render path (a path
|
|
387
|
-
// re-crossing the def is the next dynamic instance): the def block's tail, the render
|
|
388
|
-
// block's head, and every block between; a render block cyclic WITHOUT passing the def
|
|
389
|
-
// (an inner loop around the render) is checked in full.
|
|
390
|
-
if (isCall) {
|
|
482
|
+
// A CROSS-BLOCK call's execution would become path-dependent — always materialize. Within
|
|
483
|
+
// its own block a call is judged like everything else, by the barrier scan below.
|
|
484
|
+
if (isCall && pos.blk !== b) {
|
|
391
485
|
materialize.add(op);
|
|
392
486
|
continue;
|
|
393
487
|
}
|
|
394
|
-
|
|
395
|
-
if (
|
|
396
|
-
for (const x of reachAvoiding(b, b)) {
|
|
397
|
-
if (x === pos.blk && !reachAvoiding(pos.blk, b).has(pos.blk)) {
|
|
398
|
-
continue;
|
|
399
|
-
} // acyclic render block: head checked
|
|
400
|
-
if (x !== pos.blk && !reachAvoiding(x, b).has(pos.blk)) {
|
|
401
|
-
continue;
|
|
402
|
-
} // not on a def→render path
|
|
403
|
-
if (gapDirty(x.ops, 0, x.ops.length)) {
|
|
404
|
-
dirty = true;
|
|
405
|
-
break;
|
|
406
|
-
}
|
|
407
|
-
}
|
|
408
|
-
}
|
|
409
|
-
if (dirty) {
|
|
488
|
+
// Otherwise: inline only if no barrier stands on any def-avoiding def→render path.
|
|
489
|
+
if (memWriteBetween(op, pos, isBarrier)) {
|
|
410
490
|
materialize.add(op);
|
|
411
491
|
}
|
|
412
492
|
}
|
|
413
493
|
}
|
|
414
494
|
}
|
|
415
|
-
// The def→render path discipline, exported for the bitfield fold's ordering gate
|
|
416
|
-
// (structure.ts): may an op `isWrite` accepts execute between `def` and a statement at
|
|
417
|
-
// `render`, on any def-avoiding path? Same cycle-aware rules as the materialize decisions
|
|
418
|
-
// above — the def block's tail, the render block's head, and every between-block on a path;
|
|
419
|
-
// a path re-crossing the def is the next dynamic instance and does not count.
|
|
420
|
-
const memWriteBetween = (def: Op, render: { blk: Block; idx: number }, isWrite: (x: Op) => boolean): boolean => {
|
|
421
|
-
const b = opBlock.get(def)!;
|
|
422
|
-
const oi = opIndex.get(def)!;
|
|
423
|
-
const wDirty = (list: Op[], from: number, to: number): boolean => {
|
|
424
|
-
for (let k = from; k < to; k++) {
|
|
425
|
-
if (isWrite(list[k])) {
|
|
426
|
-
return true;
|
|
427
|
-
}
|
|
428
|
-
}
|
|
429
|
-
return false;
|
|
430
|
-
};
|
|
431
|
-
if (render.blk === b && oi < render.idx) {
|
|
432
|
-
return wDirty(b.ops, oi + 1, render.idx);
|
|
433
|
-
}
|
|
434
|
-
if (wDirty(b.ops, oi + 1, b.ops.length) || wDirty(render.blk.ops, 0, render.idx)) {
|
|
435
|
-
return true;
|
|
436
|
-
}
|
|
437
|
-
for (const x of reachAvoiding(b, b)) {
|
|
438
|
-
if (x === render.blk && !reachAvoiding(render.blk, b).has(render.blk)) {
|
|
439
|
-
continue; // acyclic render block: head checked
|
|
440
|
-
}
|
|
441
|
-
if (x !== render.blk && !reachAvoiding(x, b).has(render.blk)) {
|
|
442
|
-
continue; // not on a def→render path
|
|
443
|
-
}
|
|
444
|
-
if (wDirty(x.ops, 0, x.ops.length)) {
|
|
445
|
-
return true;
|
|
446
|
-
}
|
|
447
|
-
}
|
|
448
|
-
return false;
|
|
449
|
-
};
|
|
450
495
|
return { useSitesOf, opIndex, opBlock, liveIn, materialize, reachFrom, emitPos, memWriteBetween };
|
|
451
496
|
}
|