@asmlift/core 0.6.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +48 -24
- package/package.json +1 -1
- package/src/backend/cfamily.ts +39 -11
- package/src/backend/pascal.ts +2 -2
- package/src/codegen-flags.ts +640 -0
- package/src/contracts.ts +60 -11
- package/src/frontend/disasm.ts +141 -11
- package/src/frontend/high-half.ts +149 -0
- package/src/frontend/mips.ts +458 -209
- package/src/frontend/ppc.ts +332 -67
- package/src/frontend/reloc-symbol.ts +109 -0
- package/src/frontend/splat.ts +56 -18
- package/src/frontend/ssa.ts +127 -30
- package/src/frontend/stackargs.ts +420 -0
- package/src/frontend/thumb.ts +209 -232
- package/src/ir/alias.ts +24 -0
- package/src/ir/core.ts +70 -3
- package/src/ir/opcodes.ts +52 -7
- package/src/ir/parse.ts +7 -1
- package/src/ir/simplify.ts +1 -1
- package/src/l3/address.ts +2 -2
- package/src/l3/advance.ts +373 -0
- package/src/l3/argbase.ts +6 -6
- package/src/l3/argcopy.ts +269 -0
- package/src/l3/ast.ts +110 -22
- package/src/l3/basecse.ts +50 -30
- package/src/l3/coalesce.ts +118 -61
- package/src/l3/gates.ts +75 -1
- package/src/l3/hoist.ts +1 -1
- package/src/l3/homesplit.ts +13 -13
- package/src/l3/initfirst.ts +3 -3
- package/src/l3/inlinebase.ts +16 -16
- package/src/l3/mentions.ts +68 -5
- package/src/l3/mulfirst.ts +3 -3
- package/src/l3/nearbase.ts +4 -4
- package/src/l3/offmember.ts +5 -5
- package/src/l3/parkfirst.ts +6 -6
- package/src/l3/pollguard.ts +3 -3
- package/src/l3/ptrfield.ts +4 -4
- package/src/l3/regspell.ts +8 -8
- package/src/l3/reindex.ts +22 -17
- package/src/l3/scopebase.ts +32 -29
- package/src/l3/sinkinit.ts +7 -7
- package/src/l3/slotorder.ts +3 -3
- package/src/l3/storage.ts +1 -1
- package/src/l3/tailmerge.ts +2 -2
- package/src/l3/tailret.ts +70 -0
- package/src/l3/typing.ts +3 -3
- package/src/l3/unmerge.ts +483 -59
- package/src/l3/unreduce.ts +15 -14
- package/src/l3/volatileptr.ts +11 -11
- package/src/l3/volatileval.ts +11 -11
- package/src/l3/volstore.ts +16 -16
- package/src/l3/zerosub.ts +6 -6
- package/src/mangle.ts +49 -0
- package/src/pattern/engine.ts +132 -17
- package/src/pipeline.ts +39 -16
- package/src/proto.ts +2 -2
- package/src/raise/const.ts +203 -3
- package/src/raise/divpow2.ts +2 -2
- package/src/raise/extscale.ts +345 -0
- package/src/raise/globalshape.ts +32 -12
- package/src/raise/gvn.ts +2 -2
- package/src/raise/magicdiv.ts +2 -2
- package/src/raise/memberarrays.ts +4 -4
- package/src/raise/narrowlocal.ts +18 -2
- package/src/raise/paramwidth.ts +133 -3
- package/src/raise/pre-recovery.ts +100 -25
- package/src/raise/retsink.ts +389 -19
- package/src/raise/shortcircuit.ts +595 -34
- package/src/raise/structs.ts +4 -4
- package/src/raise/tailsink.ts +141 -0
- package/src/rank-declare.ts +21 -13
- package/src/{rank-axes.ts → rank-variations.ts} +319 -189
- package/src/rank.ts +1176 -805
- package/src/structure/analysis.ts +87 -90
- package/src/structure/bitfields.ts +130 -30
- package/src/structure/globalaccess.ts +30 -4
- package/src/structure/namecoalesce.ts +32 -13
- package/src/structure/retspell.ts +95 -0
- package/src/structure/structure.ts +1425 -201
- package/src/structure/switch-recover.ts +101 -8
- package/src/symbols.ts +127 -6
- package/src/target.ts +374 -44
- package/src/trace.ts +28 -19
- package/src/variation-definitions.ts +1590 -0
- package/src/variation-gates.ts +92 -0
- package/src/variation-tokens.ts +356 -0
|
@@ -6,9 +6,9 @@
|
|
|
6
6
|
// • the effect-ordering model — which defs must MATERIALIZE as named temps at their own
|
|
7
7
|
// program position instead of inlining at their use (calls/loads for effect order, plus
|
|
8
8
|
// the pure defs the homing rules claim);
|
|
9
|
-
// • the HOMING-
|
|
10
|
-
// this function hold a value the
|
|
11
|
-
// candidate would only duplicate the default. Each mirrors its
|
|
9
|
+
// • the HOMING-VARIATION ENUMERATION GATES — one export per structure variation, each answering
|
|
10
|
+
// "does this function hold a value the variation would home at all" so rank.ts can skip a
|
|
11
|
+
// variation whose candidate would only duplicate the default. Each mirrors its variation's scope inside `analyze`
|
|
12
12
|
// and states where it DIVERGES from it, in which direction, and what that costs.
|
|
13
13
|
import { disjointConstSlots, globalCellOf, mayWriteGlobal } from '../ir/alias';
|
|
14
14
|
import {
|
|
@@ -23,7 +23,7 @@ import {
|
|
|
23
23
|
predecessors,
|
|
24
24
|
successorsOf,
|
|
25
25
|
} from '../ir/core';
|
|
26
|
-
import { EFFECTFUL_OPS, ORDER_SENSITIVE_OPS, REEVAL_UNSAFE_OPS } from '../ir/opcodes';
|
|
26
|
+
import { EFFECTFUL_OPS, MEM_BASE_OPS, ORDER_SENSITIVE_OPS, REEVAL_UNSAFE_OPS } from '../ir/opcodes';
|
|
27
27
|
|
|
28
28
|
export interface UseSite {
|
|
29
29
|
blk: Block;
|
|
@@ -31,9 +31,6 @@ export interface UseSite {
|
|
|
31
31
|
op: Op;
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
-
/** the ops whose operands[0] is a memory-access BASE — the address-home axis's slot model */
|
|
35
|
-
const MEM_BASE_OPS = new Set(['load', 'store', 'aload', 'astore']);
|
|
36
|
-
|
|
37
34
|
/** Is the op's own value an ADDRESS — a pointer or array whose standalone rendering must carry a
|
|
38
35
|
* cast? Homed, `add(p, 8)` renders `(u16 *)(gPtr + 8)`: the cast lands outside the sum, so a byte
|
|
39
36
|
* offset becomes element arithmetic and the address moves. The VALUE-side counterpart of
|
|
@@ -78,7 +75,7 @@ function shortCircuitGuardedValues(fn: Fn, defOf: Map<Value, Op>): Set<Value> {
|
|
|
78
75
|
* scopebase.ts and nearbase.ts serves those bases instead). The walk deliberately crosses loads —
|
|
79
76
|
* a gaddr reachable only through a load's address keeps its cast at that load's own deref, so
|
|
80
77
|
* over-refusal there costs a candidate, never soundness. That over-refusal is why the
|
|
81
|
-
* derived-read-home
|
|
78
|
+
* derived-read-home variation asks its own pair instead (an address in the cone OUTSIDE a read, plus
|
|
82
79
|
* `rendersAsAddress` on the value): reads over named globals are its whole clientele. */
|
|
83
80
|
function coneHoldsAddr(op0: Op, defOf: Map<Value, Op>): boolean {
|
|
84
81
|
const seen = new Set<Value>();
|
|
@@ -101,10 +98,10 @@ function coneHoldsAddr(op0: Op, defOf: Map<Value, Op>): boolean {
|
|
|
101
98
|
return false;
|
|
102
99
|
}
|
|
103
100
|
|
|
104
|
-
/** THE ADDRESS-HOME
|
|
101
|
+
/** THE ADDRESS-HOME VARIATION'S SCOPE: every value whose MERGE CLASS (ir/core.ts `mergeClasses`) is
|
|
105
102
|
* used only as the base of memory accesses, and at 2+ of them.
|
|
106
103
|
*
|
|
107
|
-
* WHY THE CLASS AND NOT THE VALUE. The question the
|
|
104
|
+
* WHY THE CLASS AND NOT THE VALUE. The question the variation asks is about a REGISTER — did the
|
|
108
105
|
* machine derive one address and dereference it at several sites — and a register that survives a
|
|
109
106
|
* branch merge is spelled in functional-form SSA as an edge argument plus a block parameter. So a
|
|
110
107
|
* base each arm derives and the join then reads is N+1 SSA values with one base use apiece, none
|
|
@@ -142,7 +139,7 @@ function coneHoldsAddr(op0: Op, defOf: Map<Value, Op>): boolean {
|
|
|
142
139
|
*
|
|
143
140
|
* The FIRST is about which classes the widening adds. Of the 25+8 functions above exactly one
|
|
144
141
|
* carries a class reaching a loop-header parameter (marioparty3
|
|
145
|
-
* `func_80112508_523648_filesel`, 2 values, map-less), and neither value is one the
|
|
142
|
+
* `func_80112508_523648_filesel`, 2 values, map-less), and neither value is one the variation could
|
|
146
143
|
* materialize; `addr-home.test.ts` pins both halves. A loop-carried pointer INDUCTION is refused
|
|
147
144
|
* by its own increment, a non-base use of a class member. The loop-header class that does survive
|
|
148
145
|
* has a back-edge value READ FROM MEMORY, so its only def is a `load` and the enumeration gate
|
|
@@ -155,7 +152,7 @@ function coneHoldsAddr(op0: Op, defOf: Map<Value, Op>): boolean {
|
|
|
155
152
|
* in. It is load-bearing and measurable: replace that clause with `true` and marioparty3
|
|
156
153
|
* `func_8010923C_18E46C_cosmic_coaster` stops structuring at all — it throws the
|
|
157
154
|
* `carriesPreUpdate` StructureError this paragraph is about — while `func_80033910_34510` and
|
|
158
|
-
* `func_80033970_34570` go from
|
|
155
|
+
* `func_80033970_34570` go from variation-inert to changing their source. So the hazard is real; it is
|
|
159
156
|
* answered by WHERE a home may sit rather than by WHICH class may have one, which is a refusal
|
|
160
157
|
* the widening does not touch and the plan's class guard would have restated. Note what that
|
|
161
158
|
* implies about the enumeration gate: it deliberately
|
|
@@ -217,7 +214,7 @@ export function sharedBaseClasses(fn: Fn, ignoreRet: boolean): Set<Value> {
|
|
|
217
214
|
return out;
|
|
218
215
|
}
|
|
219
216
|
|
|
220
|
-
/** Is `d` a def one of the homing
|
|
217
|
+
/** Is `d` a def one of the homing variations could seat in a local — a def at all, and a PURE
|
|
221
218
|
* non-memory one that is not a `const`? The three enumeration gates below share this filter and
|
|
222
219
|
* each then adds its own cone/address/shape refusals.
|
|
223
220
|
*
|
|
@@ -261,14 +258,14 @@ function readCone(op0: Op, defOf: Map<Value, Op>): Op[] | null {
|
|
|
261
258
|
return reads;
|
|
262
259
|
}
|
|
263
260
|
|
|
264
|
-
/** rank.ts's enumeration gate for the `/addr-home`
|
|
265
|
-
* would home — a non-const pure def whose merge class is a shared base, with no gaddr/laddr in
|
|
266
|
-
* its cone? Mirrors the
|
|
261
|
+
/** rank.ts's enumeration gate for the `/addr-home` variation: does the function HAVE a value the
|
|
262
|
+
* variation would home — a non-const pure def whose merge class is a shared base, with no gaddr/laddr in
|
|
263
|
+
* its cone? Mirrors the variation's scope rule in `analyze` (the same `sharedBaseClasses` call), minus
|
|
267
264
|
* the loop-header seat refusal (that needs the loop model; a false positive costs one
|
|
268
265
|
* duplicate-collapsed candidate, never a wrong one).
|
|
269
266
|
*
|
|
270
267
|
* `ignoreRet` is true here: a `ret` operand may be a void phantom, which `analyze` skips under
|
|
271
|
-
* `returnsVoid` and this gate cannot know. For a genuinely returned base the
|
|
268
|
+
* `returnsVoid` and this gate cannot know. For a genuinely returned base the variation's own rule
|
|
272
269
|
* still refuses, costing one duplicate-collapsed candidate — the same trade as the loop-header
|
|
273
270
|
* divergence. */
|
|
274
271
|
export function hasHomeableSharedAddress(fn: Fn): boolean {
|
|
@@ -282,17 +279,17 @@ export function hasHomeableSharedAddress(fn: Fn): boolean {
|
|
|
282
279
|
return false;
|
|
283
280
|
}
|
|
284
281
|
|
|
285
|
-
/** rank.ts's enumeration gate for the `/expr-home`
|
|
286
|
-
*
|
|
282
|
+
/** rank.ts's enumeration gate for the `/expr-home` variation: does the function HAVE a value the
|
|
283
|
+
* variation would home — a pure non-const def with 2+ distinct consumers, at least one of them inside
|
|
287
284
|
* a loop the def sits outside, cone-free? Loops here are LAYOUT ranges (a successor at an
|
|
288
|
-
* equal-or-earlier block position closes one) where the
|
|
285
|
+
* equal-or-earlier block position closes one) where the variation's own rule uses the dominator model,
|
|
289
286
|
* and consumers here come from op operands only, where the rule counts `useSitesOf` and so counts
|
|
290
287
|
* branch args too — unlike hasHomeableSharedAddress this therefore diverges in BOTH directions. A
|
|
291
288
|
* false positive costs one duplicate-collapsed candidate. A false negative silently skips the arm:
|
|
292
289
|
* on IR whose block layout does not follow dominance, which every frontend avoids by laying blocks
|
|
293
290
|
* out in address order (a natural loop's back edge points backward), and on a value EITHER of
|
|
294
291
|
* whose two consumers is a branch arg, which the rule would home and this never enumerates. The
|
|
295
|
-
* second is unwitnessed over the 856-row bench the
|
|
292
|
+
* second is unwitnessed over the 856-row bench the variation was measured on (#97), and costs a
|
|
296
293
|
* missing candidate, never a wrong one. */
|
|
297
294
|
export function hasLoopSharedPureValue(fn: Fn): boolean {
|
|
298
295
|
const defOf = defOpMap(fn);
|
|
@@ -337,8 +334,8 @@ export function hasLoopSharedPureValue(fn: Fn): boolean {
|
|
|
337
334
|
return false;
|
|
338
335
|
}
|
|
339
336
|
|
|
340
|
-
/** rank.ts's enumeration gate for the `/derived-home`
|
|
341
|
-
*
|
|
337
|
+
/** rank.ts's enumeration gate for the `/derived-home` variation: does the function HAVE a value the
|
|
338
|
+
* variation would home — a pure non-const, non-pointer def with 2+ consumers standing on a same-block
|
|
342
339
|
* memory read that is used nowhere else and reaches it with no write in between, and with no call
|
|
343
340
|
* or standalone address in the cone? A TRUE here DOUBLES the whole structuring cross for the
|
|
344
341
|
* function — not one candidate — so every refusal cheap enough to state without the positioned
|
|
@@ -350,8 +347,8 @@ export function hasLoopSharedPureValue(fn: Fn): boolean {
|
|
|
350
347
|
* cross whose every candidate the source dedup then collapses. Under: use counting here is by SLOT
|
|
351
348
|
* over operands and successor args, where `analyze` drops a void function's `ret` operand — so a
|
|
352
349
|
* read whose second use is a suppressed return is refused here and admitted there, silently
|
|
353
|
-
* skipping the
|
|
354
|
-
* caller can observe; the
|
|
350
|
+
* skipping the alternative. That shape is a void function returning the very halfword it read, which no
|
|
351
|
+
* caller can observe; the variation's clientele reads to compute, not to return. */
|
|
355
352
|
export function hasDerivedReadHome(fn: Fn): boolean {
|
|
356
353
|
const defOf = defOpMap(fn);
|
|
357
354
|
const consumers = new Map<Value, Set<Op>>();
|
|
@@ -434,7 +431,7 @@ function naturalLoops(
|
|
|
434
431
|
return loops;
|
|
435
432
|
}
|
|
436
433
|
|
|
437
|
-
/** THE MERGE-FEED-HOME
|
|
434
|
+
/** THE MERGE-FEED-HOME variation's scope (rank.ts `/merge-home`, AnalyzeOptions.homeMergeFeeds): the
|
|
438
435
|
* pure defs it materializes.
|
|
439
436
|
*
|
|
440
437
|
* A merge parameter whose incoming edges render ONE value's expression twice or more is a value
|
|
@@ -447,7 +444,7 @@ function naturalLoops(
|
|
|
447
444
|
* clientele (a value re-derived at two ordinary uses) is a value the compiler DOES re-materialize
|
|
448
445
|
* for free. What a merge slot adds is that the two renders are mutually exclusive ARMS of one
|
|
449
446
|
* branch. Which side the source spelled is still not derivable (nothing stops a source from
|
|
450
|
-
* writing the expression twice), so this is a differ-refereed candidate
|
|
447
|
+
* writing the expression twice), so this is a differ-refereed candidate variation, never a default.
|
|
451
448
|
*
|
|
452
449
|
* Refusals:
|
|
453
450
|
* • a value whose cone holds a gaddr/laddr (`coneHoldsAddr`) — rendered standalone an `&g + i`
|
|
@@ -458,13 +455,13 @@ function naturalLoops(
|
|
|
458
455
|
* in their cone — derived from, or loaded from, a pointer param — and there the backend does
|
|
459
456
|
* re-derive the byte cast (`v0 = (u8 *)*a1 + 6;` then `v0 = (u8 *)v0 + 2;`). Address-shaped
|
|
460
457
|
* bases belong to the cast-aware machinery in l3/basecse.ts, scopebase.ts and nearbase.ts;
|
|
461
|
-
* this
|
|
458
|
+
* this variation does not offer a second spelling of them.
|
|
462
459
|
* • an op whose answer depends on WHERE it runs, or that can TRAP — `REEVAL_UNSAFE_OPS`, read
|
|
463
460
|
* from the registry rather than re-listed. Both halves carry: for a read WHERE it happens is
|
|
464
461
|
* the read rules' question, and a homed divide becomes an unconditional statement at a def
|
|
465
462
|
* block raise/shortcircuit.ts may have made, on paths C's own `&&` would have re-guarded —
|
|
466
463
|
* the KNOWN GAP `ir/opcodes.ts` books against `HOIST_UNSAFE_OPS`.
|
|
467
|
-
* • an `undef` — the
|
|
464
|
+
* • an `undef` — the variation's premise is a value the source COMPUTED once above the branch, and
|
|
468
465
|
* an uninitialised register was never computed at all: homed, it spells `v0 = uninit_r5;`,
|
|
469
466
|
* a copy of a value nothing wrote, which no asm can have.
|
|
470
467
|
* • a value in a `&&`/`||`'s guarded cone WHOSE CONE HOLDS a re-evaluation-unsafe op —
|
|
@@ -484,7 +481,7 @@ function naturalLoops(
|
|
|
484
481
|
* joins with params) is refused by this clause and no other, and lifting it takes that
|
|
485
482
|
* function's map-less enumeration 75264 → 150528 candidates, 102s → 207s. A null there is
|
|
486
483
|
* this refusal, not an absent idiom.
|
|
487
|
-
* The `analyze` scope adds nothing to this list — the whole predicate is here, so the
|
|
484
|
+
* The `analyze` scope adds nothing to this list — the whole predicate is here, so the variation's
|
|
488
485
|
* enumeration gate can run it rather than approximate it. */
|
|
489
486
|
function mergeFeedHomes(fn: Fn, dom: Map<Block, Set<Block>>, defOf: Map<Value, Op>, inLoop: Set<Block>): Set<Op> {
|
|
490
487
|
// Every block's incoming COPY SITES — the places its edge assignments render, each once. Neither
|
|
@@ -581,7 +578,7 @@ function mergeFeedHomes(fn: Fn, dom: Map<Block, Set<Block>>, defOf: Map<Value, O
|
|
|
581
578
|
const d = defOf.get(x);
|
|
582
579
|
return d !== undefined && REEVAL_UNSAFE_OPS.has(d.opcode);
|
|
583
580
|
});
|
|
584
|
-
/** may this op's result be materialized at its def — and is this
|
|
581
|
+
/** may this op's result be materialized at its def — and is this variation the one to do it? */
|
|
585
582
|
const eligible = (op: Op): boolean => {
|
|
586
583
|
const v = op.results[0];
|
|
587
584
|
return (
|
|
@@ -650,8 +647,8 @@ function mergeFeedHomes(fn: Fn, dom: Map<Block, Set<Block>>, defOf: Map<Value, O
|
|
|
650
647
|
return out;
|
|
651
648
|
}
|
|
652
649
|
|
|
653
|
-
/** rank.ts's enumeration gate for the `/merge-home`
|
|
654
|
-
* would home? The scope itself rather than a restatement of it, so the gate and the rule cannot
|
|
650
|
+
/** rank.ts's enumeration gate for the `/merge-home` variation: does the function HAVE a value the
|
|
651
|
+
* variation would home? The scope itself rather than a restatement of it, so the gate and the rule cannot
|
|
655
652
|
* drift apart — admitting still says only that the rule fires, not that the tree changes. */
|
|
656
653
|
export function hasMergeFeedHome(fn: Fn): boolean {
|
|
657
654
|
const dom = dominators(fn);
|
|
@@ -688,7 +685,7 @@ export interface AnalyzeOptions {
|
|
|
688
685
|
/** Dominator sets, when the caller already holds them (structure() does) — consumed by the
|
|
689
686
|
* live-across-a-loop rule's back-edge detection. Absent ⇒ that rule stands down. */
|
|
690
687
|
dom?: Map<Block, Set<Block>>;
|
|
691
|
-
/** THE value-home
|
|
688
|
+
/** THE value-home variation (rank.ts `/reread-globals`). A read of a named global is barred from
|
|
692
689
|
* rendering at its use by any write in between — even a store to an unrelated global, which
|
|
693
690
|
* cannot possibly change what it sees. That over-conservatism is what invents the locals the
|
|
694
691
|
* round-5 dogfood measured as its highest-cost defect ("hoists what agbcc re-reads"):
|
|
@@ -702,38 +699,38 @@ export interface AnalyzeOptions {
|
|
|
702
699
|
* scoped), so today's spelling is never wrong — only sometimes not the one the compiler was
|
|
703
700
|
* given.
|
|
704
701
|
* Which side matches is genuinely per-function (the same dogfood watched agbcc go both ways
|
|
705
|
-
* inside ONE function), so this is a differ-refereed candidate
|
|
702
|
+
* inside ONE function), so this is a differ-refereed candidate variation, never a default.
|
|
706
703
|
*
|
|
707
704
|
* SCOPE, since `readsStayWhereWritten` below now answers the same question — read once and
|
|
708
|
-
* reuse, or read per use — with the opposite default: this
|
|
705
|
+
* reuse, or read per use — with the opposite default: this variation owns renders in the read's OWN
|
|
709
706
|
* block, where nothing about placement is in evidence and both spellings really do compile.
|
|
710
707
|
* A read whose every render sits in a STRICTLY DOMINATED block is the default's, and on a
|
|
711
|
-
* target that declares it the
|
|
708
|
+
* target that declares it the variation cannot produce the re-read spelling there (the rule
|
|
712
709
|
* materializes first). That is a pre-emption, not a conflict: a per-arm source read compiles to
|
|
713
710
|
* a per-arm load on that compiler, so the sunk spelling is one it did not emit from this asm. */
|
|
714
711
|
rereadGlobals?: boolean;
|
|
715
712
|
/** "does the project declare this global volatile?" — a read of a volatile object may NOT be
|
|
716
|
-
* duplicated or moved, so the
|
|
713
|
+
* duplicated or moved, so the variation above refuses on one. Answers false for a symbol the map
|
|
717
714
|
* does not carry (and for no map at all), which is the same posture the multi-render rule has
|
|
718
715
|
* always had: without a declaration nothing here can know, and the differ referees the extra
|
|
719
|
-
* load. Where the map DOES know, the
|
|
716
|
+
* load. Where the map DOES know, the variation is silent about it rather than wrong. */
|
|
720
717
|
volatileGlobal?: (name: string) => boolean;
|
|
721
|
-
/** The in-place-join
|
|
718
|
+
/** The in-place-join variation (rank.ts `/inplace`). A load whose result is a `cond_br` successor
|
|
722
719
|
* ARG feeds a merge: rendered inline it has no name, so the merge param mints a fresh variable
|
|
723
720
|
* and BOTH arms must assign it. Materialized, the naming walk can home the merge in the load's
|
|
724
721
|
* own variable, the identity arm elides, and the `if` renders one-sided — `v = *p; if (v > 31)
|
|
725
722
|
* v = 32;` — which is also what reuses the load's register when recompiled. Whether the source
|
|
726
723
|
* spelled the temp or the overwrite is not derivable from the asm, so this is a differ-refereed
|
|
727
|
-
* candidate
|
|
724
|
+
* candidate variation, never a default. Plain `br` args (loop-carried values) are out of scope:
|
|
728
725
|
* their homes are the loop-param machinery's question. */
|
|
729
726
|
materializeJoinFeeds?: boolean;
|
|
730
|
-
/** The address-home
|
|
727
|
+
/** The address-home variation (rank.ts `/addr-home`). A pure computed address the asm derived ONCE
|
|
731
728
|
* and dereferenced at 2+ sites renders, by default, re-derived at each use — and the loads
|
|
732
729
|
* through it re-read per use — where the original source may have spelled a pointer local plus
|
|
733
730
|
* scalar temps (`u8 *entry = (u8 *)((a0 << 2) + base); type = entry[1]; idx = entry[0];`).
|
|
734
731
|
* The two spellings are codegen-visible (the re-derive folds each deref offset into its OWN
|
|
735
732
|
* pool literal; the home shares one base register across `[rN, #k]` accesses) and which one
|
|
736
|
-
* the source used is not derivable from asm, so this is a differ-refereed candidate
|
|
733
|
+
* the source used is not derivable from asm, so this is a differ-refereed candidate variation,
|
|
737
734
|
* never a default. With it on: a non-const pure value whose MERGE CLASS is consumed ONLY as
|
|
738
735
|
* the base operand of 2+ memory accesses materializes (`sharedBaseClasses` — the class is what
|
|
739
736
|
* makes a base the arms derive and the join dereferences one register rather than three
|
|
@@ -743,11 +740,11 @@ export interface AnalyzeOptions {
|
|
|
743
740
|
* standalone, an address cone's value changes: the byte-stride cast lives at the use — see
|
|
744
741
|
* coneHoldsAddr; those bases stay with l3/basecse.ts, scopebase.ts and nearbase.ts), and the
|
|
745
742
|
* multi-block-loop-header seat refusal is the decline-avoidance half (same as
|
|
746
|
-
* liveAcrossLoop's). An L3
|
|
743
|
+
* liveAcrossLoop's). An L3 respell variation could not host this one: by structuring's end the loads
|
|
747
744
|
* have already rendered per-use inside separate arms, and only this phase's positioned
|
|
748
745
|
* memory model can merge them into pre-branch temps. */
|
|
749
746
|
homeSharedAddresses?: boolean;
|
|
750
|
-
/** The loop-expression-home
|
|
747
|
+
/** The loop-expression-home variation (rank.ts `/expr-home`). A pure computed value defined outside
|
|
751
748
|
* a loop and consumed by 2+ distinct ops, at least one of them inside that loop, is one the
|
|
752
749
|
* compiler holds in a (callee-saved) register across the iterations — it never re-derives per
|
|
753
750
|
* use in a loop —
|
|
@@ -756,10 +753,10 @@ export interface AnalyzeOptions {
|
|
|
756
753
|
* declared type is the IR value's recovered type, so a u32 value's compares stay unsigned
|
|
757
754
|
* through the local. Straight-line multi-use values stay OUT (the small-constant class the
|
|
758
755
|
* const-across-call scope's note records); shared memory-access bases are `/addr-home`'s.
|
|
759
|
-
* Same refusals as that
|
|
756
|
+
* Same refusals as that variation: gaddr/laddr cones and multi-block-loop-header seats. Adding
|
|
760
757
|
* materialization preserves semantics for the admitted values, exactly as above. */
|
|
761
758
|
homeLoopExprs?: boolean;
|
|
762
|
-
/** The derived-read-home
|
|
759
|
+
/** The derived-read-home variation (rank.ts `/derived-home`). A memory read whose value is not used
|
|
763
760
|
* directly but through a pure computation with 2+ consumers puts the home on the WRONG node:
|
|
764
761
|
* the read materializes (its consumer resolves no single render position) and the computation
|
|
765
762
|
* then re-derives from that local at every use, where the asm computed it ONCE — the read's
|
|
@@ -768,7 +765,7 @@ export interface AnalyzeOptions {
|
|
|
768
765
|
* non-const value with 2+ consumers whose operand cone bottoms out at a memory read
|
|
769
766
|
* materializes instead; the read then renders exactly once, inside the home.
|
|
770
767
|
*
|
|
771
|
-
* A differ-refereed
|
|
768
|
+
* A differ-refereed variation, not a fix: agbcc CSEs a re-derived expression back to one
|
|
772
769
|
* instruction often enough that both spellings do compile, and which one the source spelled is
|
|
773
770
|
* not derivable. What the cone's read supplies is the evidence the straight-line case otherwise
|
|
774
771
|
* lacks — `/expr-home` takes a loop as proof the value stayed in a register, and a
|
|
@@ -782,23 +779,23 @@ export interface AnalyzeOptions {
|
|
|
782
779
|
* changes which paths read, how often, and in what order). A cone crossing a `call` (homing
|
|
783
780
|
* would move a side effect). A standalone gaddr/laddr in the cone, or a value that is ITSELF an
|
|
784
781
|
* address (rendered standalone the byte-stride cast lands outside the sum — see
|
|
785
|
-
* `rendersAsAddress`). And the multi-block-loop-header seat the sibling
|
|
782
|
+
* `rendersAsAddress`). And the multi-block-loop-header seat the sibling variations refuse. */
|
|
786
783
|
homeDerivedReads?: boolean;
|
|
787
|
-
/** The merge-feed-home
|
|
784
|
+
/** The merge-feed-home variation (rank.ts `/merge-home`). A pure value one join's incoming edges
|
|
788
785
|
* render into the SAME parameter slot from 2+ places materializes at its def: the copy machinery
|
|
789
786
|
* has no name to reference, so the default re-derives the whole expression per arm
|
|
790
787
|
* (`m = (-(b & 1) | b & 1) >> 31 & 0x400;` in both) and agbcc if-converts each copy. The scope
|
|
791
788
|
* and every refusal it states are `mergeFeedHomes` above, which the enumeration gate also runs. */
|
|
792
789
|
homeMergeFeeds?: boolean;
|
|
793
790
|
/** DEF-BLOCK PLACEMENT for memory reads — WHERE the read happens, not where the value lives.
|
|
794
|
-
* The sibling of the homing
|
|
791
|
+
* The sibling of the homing variations above: there the question is which register or offset holds a
|
|
795
792
|
* value, here which BLOCK performs the read. A read whose every render sits in a block its own
|
|
796
|
-
* block STRICTLY DOMINATES has no rule at all above — those
|
|
793
|
+
* block STRICTLY DOMINATES has no rule at all above — those variations want 2+ consumers or a shared
|
|
797
794
|
* base — so it sinks and each arm re-reads it: a second load either way, plus a second pool
|
|
798
795
|
* literal when the address folded to a constant.
|
|
799
796
|
*
|
|
800
|
-
* A
|
|
801
|
-
* a differ-refereed
|
|
797
|
+
* A compiler behavior (TargetDescription.compilerBehaviors `readsStayWhereWritten`), not
|
|
798
|
+
* a differ-refereed variation, and that distinction is the argument: a variation exists where the asm
|
|
802
799
|
* UNDERDETERMINES the source, some pass having collapsed two spellings onto one output
|
|
803
800
|
* (`/uns-cmp`'s non-negativity proof is the type case). Where the compiler emits a read in the
|
|
804
801
|
* block the source spelled it in, re-spelling it at the block the asm read in reproduces that
|
|
@@ -1033,7 +1030,7 @@ function blockLiveIn(fn: Fn, returnsVoid: boolean): Map<Block, Set<Value>> {
|
|
|
1033
1030
|
* `reachFrom` is the plain successors-transitive set, excluding the start block itself, cached per
|
|
1034
1031
|
* factory — which is why this is a factory: the cache belongs to one `analyze` call.
|
|
1035
1032
|
*
|
|
1036
|
-
* `reachAvoiding` never passes THROUGH `avoid` — the def-block-avoiding
|
|
1033
|
+
* `reachAvoiding` never passes THROUGH `avoid` — the def-block-avoiding form for per-iteration
|
|
1037
1034
|
* path checks: a path that re-enters the def's block re-executes the def, so writes on it belong
|
|
1038
1035
|
* to the NEXT dynamic instance (which re-renders anyway) and must not count against this one.
|
|
1039
1036
|
* Uncached (per-decision graphs are small). */
|
|
@@ -1060,7 +1057,7 @@ function makeReach(): {
|
|
|
1060
1057
|
reachCache.set(b, r);
|
|
1061
1058
|
return r;
|
|
1062
1059
|
};
|
|
1063
|
-
// Reachability that never passes THROUGH `avoid` — the def-block-avoiding
|
|
1060
|
+
// Reachability that never passes THROUGH `avoid` — the def-block-avoiding form for
|
|
1064
1061
|
// per-iteration path checks: a path that re-enters the def's block re-executes the def, so
|
|
1065
1062
|
// writes on it belong to the NEXT dynamic instance (which re-renders anyway) and must not
|
|
1066
1063
|
// count against this one. Uncached (per-decision graphs are small).
|
|
@@ -1144,7 +1141,7 @@ export function analyze(fn: Fn, returnsVoid: boolean, opts: AnalyzeOptions = {})
|
|
|
1144
1141
|
// because their edge copy is on the one path that reaches the successor, so nothing conditional
|
|
1145
1142
|
// happens to them.
|
|
1146
1143
|
//
|
|
1147
|
-
// `condBrArgFed` is the `/inplace`
|
|
1144
|
+
// `condBrArgFed` is the `/inplace` variation's narrower reading — a preference about where a load's
|
|
1148
1145
|
// value is homed, whose own scope note is on AnalyzeOptions.materializeJoinFeeds.
|
|
1149
1146
|
const branchArgFed = new Set<Value>();
|
|
1150
1147
|
const condBrArgFed = new Set<Value>();
|
|
@@ -1238,7 +1235,7 @@ export function analyze(fn: Fn, returnsVoid: boolean, opts: AnalyzeOptions = {})
|
|
|
1238
1235
|
};
|
|
1239
1236
|
// EVERY position a value's expression renders at — `emitPos` generalized to the whole set (it
|
|
1240
1237
|
// answers one place or gives up), by following ALL consumers transitively. That matters for
|
|
1241
|
-
// the value-home
|
|
1238
|
+
// the value-home variation: a pure expression with two consumers (`gOut = e; return e;`) has no single
|
|
1242
1239
|
// emit position, so `emitPos` answers null and every memory read feeding it is forced into a
|
|
1243
1240
|
// local — even when re-reading at both places is provably equivalent. Null only for a genuine
|
|
1244
1241
|
// cycle (defensive: SSA use-def is acyclic through ops), which the caller treats as unresolvable.
|
|
@@ -1312,16 +1309,16 @@ export function analyze(fn: Fn, returnsVoid: boolean, opts: AnalyzeOptions = {})
|
|
|
1312
1309
|
* holds the loaded value and the address stays inline at the deref, which is why the load rules
|
|
1313
1310
|
* (live-across-a-loop, join feeds, /addr-home's, def-block placement) do not ask it. */
|
|
1314
1311
|
const addressCone = (op0: Op): boolean => coneHoldsAddr(op0, defOf);
|
|
1315
|
-
// ── the address-home
|
|
1312
|
+
// ── the address-home variation's scope ────────────────────────────────────────────────────
|
|
1316
1313
|
// Over the MERGE CLASS, so a base the arms derive and the join dereferences counts as the one
|
|
1317
|
-
// register it is — see `sharedBaseClasses`. Computed once, and only under the
|
|
1314
|
+
// register it is — see `sharedBaseClasses`. Computed once, and only under the variation.
|
|
1318
1315
|
const sharedBaseClass = homeSharedAddresses ? sharedBaseClasses(fn, returnsVoid) : new Set<Value>();
|
|
1319
|
-
// The same question asked of the VALUE ALONE, which is what the two
|
|
1316
|
+
// The same question asked of the VALUE ALONE, which is what the two variations below need: their
|
|
1320
1317
|
// "shared bases stay the address-home scope's" exclusion is a hand-off between scopes, and
|
|
1321
1318
|
// widening it to the class would make them refuse values the address-home scope only claims
|
|
1322
|
-
// when its own
|
|
1319
|
+
// when its own variation is ON — a candidate lost with no candidate gained. Where both variations run the
|
|
1323
1320
|
// two scopes may claim one value, which is a no-op: `materialize` is a set and the address-home
|
|
1324
|
-
// scope, running first, is what registers `
|
|
1321
|
+
// scope, running first, is what registers `addressHomedBases`.
|
|
1325
1322
|
const usedOnlyAsSharedBase = (v: Value): boolean => {
|
|
1326
1323
|
const sites = useSitesOf.get(v) ?? [];
|
|
1327
1324
|
const consumers = new Set(sites.map((s) => s.op));
|
|
@@ -1332,7 +1329,7 @@ export function analyze(fn: Fn, returnsVoid: boolean, opts: AnalyzeOptions = {})
|
|
|
1332
1329
|
)
|
|
1333
1330
|
);
|
|
1334
1331
|
};
|
|
1335
|
-
// The merge-feed-home
|
|
1332
|
+
// The merge-feed-home variation's ops, settled BEFORE the fixpoint: the scope reads the IR alone (no
|
|
1336
1333
|
// render positions, no `materialize`), so it cannot change as the set grows. Unlike the rules
|
|
1337
1334
|
// that stand down without the caller's `dom`, this one computes its own: rank.ts has already
|
|
1338
1335
|
// admitted the candidate on `hasMergeFeedHome`, which runs the same scope, so standing down here
|
|
@@ -1343,9 +1340,9 @@ export function analyze(fn: Fn, returnsVoid: boolean, opts: AnalyzeOptions = {})
|
|
|
1343
1340
|
const mloops = mdom === dom ? loopBodies : naturalLoops(fn, mdom, predsOf);
|
|
1344
1341
|
mergeFeedOps = mergeFeedHomes(fn, mdom, defOf, new Set(mloops.flatMap((L) => [...L.body])));
|
|
1345
1342
|
}
|
|
1346
|
-
/** result values the address-home
|
|
1347
|
-
const
|
|
1348
|
-
/** The loop-expression-home
|
|
1343
|
+
/** result values the address-home variation materialized — the load rule's admission key */
|
|
1344
|
+
const addressHomedBases = new Set<Value>();
|
|
1345
|
+
/** The loop-expression-home variation's scope: 2+ distinct consumers of the value, at least one of
|
|
1349
1346
|
* them inside a loop the def's block is outside (loop model = the caller's dominators; absent ⇒
|
|
1350
1347
|
* never).
|
|
1351
1348
|
*
|
|
@@ -1363,7 +1360,7 @@ export function analyze(fn: Fn, returnsVoid: boolean, opts: AnalyzeOptions = {})
|
|
|
1363
1360
|
loopBodies.some((L) => !L.body.has(defBlk) && consumers.some((c) => L.body.has(opBlock.get(c)!)))
|
|
1364
1361
|
);
|
|
1365
1362
|
};
|
|
1366
|
-
/** The derived-read-home
|
|
1363
|
+
/** The derived-read-home variation's scope: does `op0` stand on a memory READ that may render at
|
|
1367
1364
|
* `op0`'s own position?
|
|
1368
1365
|
*
|
|
1369
1366
|
* The cone walk STOPS at a read — its address stays inline at the deref, so nothing below it is
|
|
@@ -1371,7 +1368,7 @@ export function analyze(fn: Fn, returnsVoid: boolean, opts: AnalyzeOptions = {})
|
|
|
1371
1368
|
* (a side effect), and a gaddr/laddr reached OUTSIDE a read's address (rendered standalone an
|
|
1372
1369
|
* `&g + i` loses the memAccess's byte-stride cast). `coneHoldsAddr` cannot answer this: it
|
|
1373
1370
|
* crosses reads, so it refuses every value standing on a named global's load — which is this
|
|
1374
|
-
*
|
|
1371
|
+
* variation's whole clientele.
|
|
1375
1372
|
*
|
|
1376
1373
|
* At least one read is required. A value derivable from locals and constants alone is one the
|
|
1377
1374
|
* compiler re-materializes for free, and homing it only adds copies — the small-constant class
|
|
@@ -1384,7 +1381,7 @@ export function analyze(fn: Fn, returnsVoid: boolean, opts: AnalyzeOptions = {})
|
|
|
1384
1381
|
* A read OUTSIDE the cone bars it too. The default's "loads never bar a load" holds for reads
|
|
1385
1382
|
* the compiler leaves unsequenced inside ONE expression, which is what the cone's own reads
|
|
1386
1383
|
* become; a foreign read renders in a different statement, so moving past it reorders two
|
|
1387
|
-
* accesses — for two MMIO cells (this
|
|
1384
|
+
* accesses — for two MMIO cells (this variation's clientele) an observable swap, as when `A`'s
|
|
1388
1385
|
* derived value sits below `B`'s and homing both puts `B`'s read first.
|
|
1389
1386
|
*
|
|
1390
1387
|
* And each read's value must go NOWHERE BUT the cone: exactly one use site. Homing resolves a
|
|
@@ -1393,9 +1390,9 @@ export function analyze(fn: Fn, returnsVoid: boolean, opts: AnalyzeOptions = {})
|
|
|
1393
1390
|
* `ldrh`, which for a volatile cell is precisely the duplication `volatileGlobal` refuses. Two
|
|
1394
1391
|
* homed values over one read is the same shape from the other side (each is the other's second
|
|
1395
1392
|
* use), so one test covers both. This is what makes "renders once, inside the home" a property
|
|
1396
|
-
* rather than an aspiration: without it the
|
|
1393
|
+
* rather than an aspiration: without it the variation silently doubles a hardware read.
|
|
1397
1394
|
*
|
|
1398
|
-
* The same block is what makes the
|
|
1395
|
+
* The same block is what makes the variation's claim true at all: the register handoff it reproduces
|
|
1399
1396
|
* is one straight-line run of the asm, `ldrh` into `eor` into three uses. Across blocks WHICH
|
|
1400
1397
|
* BLOCK reads is `readsStayWhereWritten`'s question, not this one, and answering it here goes
|
|
1401
1398
|
* wrong in both directions — a value below a branch pulls the read into an arm that may not
|
|
@@ -1516,10 +1513,10 @@ export function analyze(fn: Fn, returnsVoid: boolean, opts: AnalyzeOptions = {})
|
|
|
1516
1513
|
} else if (op.opcode !== 'const' && pr && copyInterdependent.has(pr) && !addressCone(op)) {
|
|
1517
1514
|
materialize.add(op);
|
|
1518
1515
|
}
|
|
1519
|
-
// Folding the FOUR
|
|
1516
|
+
// Folding the FOUR VARIATION scopes below into one predicate-parameterized scope is BOOKED
|
|
1520
1517
|
// in docs/level-tower.md and deliberately unpaid; what it cannot absorb is named there,
|
|
1521
1518
|
// along with the price the fourth one added to it (the gate duplication).
|
|
1522
|
-
// Third scope, under the address-home
|
|
1519
|
+
// Third scope, under the address-home variation only (see AnalyzeOptions.homeSharedAddresses):
|
|
1523
1520
|
// a non-const pure value whose MERGE CLASS is used only as the base of 2+ memory
|
|
1524
1521
|
// accesses.
|
|
1525
1522
|
if (
|
|
@@ -1531,9 +1528,9 @@ export function analyze(fn: Fn, returnsVoid: boolean, opts: AnalyzeOptions = {})
|
|
|
1531
1528
|
!multiBlockHeaders.has(b)
|
|
1532
1529
|
) {
|
|
1533
1530
|
materialize.add(op);
|
|
1534
|
-
|
|
1531
|
+
addressHomedBases.add(pr);
|
|
1535
1532
|
}
|
|
1536
|
-
// Fourth scope, under the loop-expression-home
|
|
1533
|
+
// Fourth scope, under the loop-expression-home variation (AnalyzeOptions.homeLoopExprs): a
|
|
1537
1534
|
// pure non-const value with 2+ distinct consumers, at least one of them inside a loop the
|
|
1538
1535
|
// def sits outside. Shared bases stay the previous scope's (its load rule needs the
|
|
1539
1536
|
// registration).
|
|
@@ -1548,7 +1545,7 @@ export function analyze(fn: Fn, returnsVoid: boolean, opts: AnalyzeOptions = {})
|
|
|
1548
1545
|
) {
|
|
1549
1546
|
materialize.add(op);
|
|
1550
1547
|
}
|
|
1551
|
-
// Fifth scope, under the derived-read-home
|
|
1548
|
+
// Fifth scope, under the derived-read-home variation (AnalyzeOptions.homeDerivedReads): a
|
|
1552
1549
|
// pure non-const value with 2+ consumers standing on a memory read. Shared bases stay
|
|
1553
1550
|
// the third scope's and values consumed across a loop the fourth's; what this one adds
|
|
1554
1551
|
// is the straight-line case, which neither reaches.
|
|
@@ -1564,10 +1561,10 @@ export function analyze(fn: Fn, returnsVoid: boolean, opts: AnalyzeOptions = {})
|
|
|
1564
1561
|
) {
|
|
1565
1562
|
materialize.add(op);
|
|
1566
1563
|
}
|
|
1567
|
-
// Sixth scope, under the merge-feed-home
|
|
1568
|
-
// `mergeFeedHomes` above. The only one of the FOUR
|
|
1564
|
+
// Sixth scope, under the merge-feed-home variation (AnalyzeOptions.homeMergeFeeds) —
|
|
1565
|
+
// `mergeFeedHomes` above. The only one of the FOUR VARIATION scopes that admits a `const`; the
|
|
1569
1566
|
// other const clientele is the first scope, a const live across a call. For the sibling
|
|
1570
|
-
//
|
|
1567
|
+
// variations a re-derived const is re-materialization, the compiler's own behavior, while a
|
|
1571
1568
|
// const the arms of a branch merge is one it held in a register across them
|
|
1572
1569
|
// (`mov r5, #0` once, not per arm).
|
|
1573
1570
|
if (homeMergeFeeds && mergeFeedOps.has(op)) {
|
|
@@ -1579,7 +1576,7 @@ export function analyze(fn: Fn, returnsVoid: boolean, opts: AnalyzeOptions = {})
|
|
|
1579
1576
|
if (!r || !useSitesOf.has(r)) {
|
|
1580
1577
|
continue;
|
|
1581
1578
|
} // dead call → exprstmt (unchanged)
|
|
1582
|
-
// Under the value-home
|
|
1579
|
+
// Under the value-home variation: which named global cell this op reads, if any. A constant-
|
|
1583
1580
|
// offset `load` only — an `aload`'s runtime index names no single cell, and a call reads
|
|
1584
1581
|
// everything. Null ⇒ every write bars, exactly as before.
|
|
1585
1582
|
const cell =
|
|
@@ -1647,19 +1644,19 @@ export function analyze(fn: Fn, returnsVoid: boolean, opts: AnalyzeOptions = {})
|
|
|
1647
1644
|
continue;
|
|
1648
1645
|
}
|
|
1649
1646
|
}
|
|
1650
|
-
// Address-home
|
|
1647
|
+
// Address-home variation: a multi-render load THROUGH a base this variation homed re-reads what the
|
|
1651
1648
|
// asm read once into the register the home just reproduced — home the value too, at the
|
|
1652
|
-
// load's own position. Only through
|
|
1649
|
+
// load's own position. Only through variation-homed bases (the fixpoint's later sweep sees them
|
|
1653
1650
|
// even though reverse order visits the load first); the general multi-render re-read stays
|
|
1654
|
-
// the default rule below.
|
|
1655
|
-
// base — safe because no other rule can pre-empt a value the
|
|
1656
|
-
// means no successor-arg use (outside copyInterdependent's read set), and the
|
|
1651
|
+
// the default rule below. addressHomedBases registers on the same sweep that materializes the
|
|
1652
|
+
// base — safe because no other rule can pre-empt a value the variation would home: base-slot-only
|
|
1653
|
+
// means no successor-arg use (outside copyInterdependent's read set), and the variation's scope
|
|
1657
1654
|
// excludes consts, the const arm's only clientele.
|
|
1658
1655
|
if (
|
|
1659
1656
|
homeSharedAddresses &&
|
|
1660
1657
|
op.opcode === 'load' &&
|
|
1661
1658
|
consumers.length > 1 &&
|
|
1662
|
-
|
|
1659
|
+
addressHomedBases.has(op.operands[0]) &&
|
|
1663
1660
|
!multiBlockHeaders.has(b)
|
|
1664
1661
|
) {
|
|
1665
1662
|
materialize.add(op);
|
|
@@ -1670,8 +1667,8 @@ export function analyze(fn: Fn, returnsVoid: boolean, opts: AnalyzeOptions = {})
|
|
|
1670
1667
|
// so it is sound iff every render still sees the def-time memory: NO write anywhere
|
|
1671
1668
|
// between the def and ANY render (cycle-aware, conservative write set). Otherwise a temp.
|
|
1672
1669
|
//
|
|
1673
|
-
// WHERE it renders. Without the
|
|
1674
|
-
// single position (its own value renders in several places) refuses. With the
|
|
1670
|
+
// WHERE it renders. Without the variation: one position per consumer, and a consumer with no
|
|
1671
|
+
// single position (its own value renders in several places) refuses. With the variation a load
|
|
1675
1672
|
// resolves the whole SET instead — the second half of the value-home defect, where the
|
|
1676
1673
|
// local is invented not by a barrier but because the pure expression downstream is itself
|
|
1677
1674
|
// duplicated (`gOut = (gValue << 1) + gValue; return (gValue << 1) + gValue;`). Never for a
|
|
@@ -1701,7 +1698,7 @@ export function analyze(fn: Fn, returnsVoid: boolean, opts: AnalyzeOptions = {})
|
|
|
1701
1698
|
// exactly as it originally chose to. Loads never bar a load (reads don't conflict).
|
|
1702
1699
|
const samePos = (q: { blk: Block; idx: number } | null) => q !== null && q.blk === pos.blk && q.idx === pos.idx;
|
|
1703
1700
|
const isBarrier = (x: Op): boolean => {
|
|
1704
|
-
// Value-home
|
|
1701
|
+
// Value-home variation: a store/astore this read is PROVABLY disjoint from (a different named
|
|
1705
1702
|
// global) does not sequence against it, so the read may still render at its use.
|
|
1706
1703
|
if (barsThisRead && (x.opcode === 'store' || x.opcode === 'astore') && !barsThisRead(x)) {
|
|
1707
1704
|
return false;
|