@asmlift/core 0.8.1 → 0.9.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 +34 -30
- package/package.json +1 -1
- package/src/aggregate.ts +248 -0
- package/src/backend/c.ts +5 -2
- package/src/backend/cfamily.ts +236 -37
- package/src/backend/cpp.ts +114 -9
- package/src/backend/pascal.ts +39 -0
- package/src/cdecl/constant.ts +59 -0
- package/src/cdecl/lex.ts +209 -0
- package/src/cdecl/parse.ts +975 -0
- package/src/cdecl/spell.ts +69 -0
- package/src/codegen-flags.ts +38 -6
- package/src/compiler-diagnostics.ts +194 -0
- package/src/contracts.ts +615 -79
- package/src/declare.ts +75 -5
- package/src/frontend/asmdata.ts +19 -5
- package/src/frontend/call-plan.ts +686 -0
- package/src/frontend/device-pins.ts +277 -0
- package/src/frontend/errors.ts +5 -0
- package/src/frontend/flags-edge.ts +101 -0
- package/src/frontend/fpu.ts +167 -0
- package/src/frontend/frame-objects.ts +2131 -0
- package/src/frontend/high-half.ts +2 -1
- package/src/frontend/local-object.ts +490 -0
- package/src/frontend/mips.ts +363 -40
- package/src/frontend/opaque.ts +97 -18
- package/src/frontend/ppc.ts +520 -129
- package/src/frontend/reloc-symbol.ts +94 -13
- package/src/frontend/splat.ts +108 -5
- package/src/frontend/ssa.ts +669 -76
- package/src/frontend/stackargs.ts +120 -39
- package/src/frontend/thumb.ts +2764 -2018
- package/src/ir/alias.ts +29 -0
- package/src/ir/core.ts +101 -3
- package/src/ir/discipline.ts +248 -0
- package/src/ir/float-bits.ts +47 -0
- package/src/ir/opcodes.ts +84 -112
- package/src/ir/parse.ts +1 -0
- package/src/ir/types.ts +118 -2
- package/src/ir/verify.ts +150 -3
- package/src/l3/address.ts +5 -0
- package/src/l3/argcopy.ts +38 -6
- package/src/l3/ast.ts +186 -14
- package/src/l3/coalesce.ts +21 -13
- package/src/l3/dce.ts +32 -10
- package/src/l3/gates.ts +9 -3
- package/src/l3/hoist.ts +8 -7
- package/src/l3/initfirst.ts +27 -9
- package/src/l3/intcell.ts +97 -0
- package/src/l3/mentions.ts +10 -4
- package/src/l3/narrowdecl.ts +218 -0
- package/src/l3/nearbase.ts +10 -5
- package/src/l3/parkfirst.ts +8 -5
- package/src/l3/pollguard.ts +3 -2
- package/src/l3/ptrcell.ts +357 -0
- package/src/l3/regspell.ts +17 -3
- package/src/l3/reindex.ts +10 -7
- package/src/l3/symbol-refs.ts +104 -23
- package/src/l3/tailmerge.ts +6 -19
- package/src/l3/typing.ts +137 -25
- package/src/l3/unreduce.ts +4 -4
- package/src/l3/volstore.ts +96 -7
- package/src/mangle.ts +11 -1
- package/src/pattern/engine.ts +19 -16
- package/src/pipeline.ts +57 -12
- package/src/proto-context.ts +788 -0
- package/src/proto.ts +689 -50
- package/src/raise/arrays.ts +4 -3
- package/src/raise/declared-volatile.ts +131 -0
- package/src/raise/divpow2.ts +2 -2
- package/src/raise/errors.ts +17 -0
- package/src/raise/floathelpers.ts +243 -0
- package/src/raise/globalshape.ts +11 -5
- package/src/raise/gvn.ts +26 -5
- package/src/raise/narrow.ts +29 -2
- package/src/raise/narrowlocal.ts +11 -6
- package/src/raise/offsetnames.ts +510 -0
- package/src/raise/pairparams.ts +81 -0
- package/src/raise/paramwidth.ts +7 -0
- package/src/raise/pre-recovery.ts +78 -4
- package/src/raise/recover.ts +14 -10
- package/src/raise/retsink.ts +1 -1
- package/src/raise/shortcircuit.ts +41 -19
- package/src/raise/softdiv.ts +21 -23
- package/src/raise/struct-arrays.ts +5 -4
- package/src/raise/structs.ts +311 -98
- package/src/raise/truncload.ts +366 -0
- package/src/raise/widehelpers.ts +106 -0
- package/src/rank-declare.ts +30 -5
- package/src/rank-variations.ts +36 -8
- package/src/rank.ts +267 -79
- package/src/runtime-helpers.ts +287 -0
- package/src/stillborn.ts +289 -0
- package/src/structure/analysis.ts +1285 -448
- package/src/structure/hazards.ts +633 -58
- package/src/structure/local-statics.ts +381 -0
- package/src/structure/namecoalesce.ts +1 -1
- package/src/structure/redundant-test.ts +99 -0
- package/src/structure/structure.ts +1772 -395
- package/src/structure/switch-recover.ts +159 -78
- package/src/symbols.ts +38 -3
- package/src/target.ts +662 -59
- package/src/trace.ts +44 -24
- package/src/variation-definitions.ts +119 -1
- package/src/variation-tokens.ts +20 -6
|
@@ -8,8 +8,16 @@
|
|
|
8
8
|
// the pure defs the homing rules claim);
|
|
9
9
|
// • the HOMING-VARIATION ENUMERATION GATES — one export per structure variation, each answering
|
|
10
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
|
|
11
|
+
// variation whose candidate would only duplicate the default. Each mirrors its variation's scope in `namePureOp`
|
|
12
12
|
// and states where it DIVERGES from it, in which direction, and what that costs.
|
|
13
|
+
//
|
|
14
|
+
// `analyze` runs stages — `analysisStages` wires `indexUses`, `edgeArgUses`, `makeRenderModel`,
|
|
15
|
+
// `makeInlineReach`, `makeLoopRules` and `homeScopes` — and then the rules over them: `namePureOp`,
|
|
16
|
+
// `nameAccess`, `namePreUpdateHelpers`. The fixpoint driver stays in `analyze` because it IS the
|
|
17
|
+
// order: reverse program order, the escape phase once every other rule has settled, the render
|
|
18
|
+
// caches cleared at the head of each sweep. `MaterializeState.materialize` is shared LIVE with the
|
|
19
|
+
// render model, the inline reach and the loop rules: a rule's verdict for one op turns on the names
|
|
20
|
+
// the same sweep has already given the ops after it.
|
|
13
21
|
import { disjointConstSlots, globalCellOf, mayWriteGlobal } from '../ir/alias';
|
|
14
22
|
import {
|
|
15
23
|
Block,
|
|
@@ -23,7 +31,8 @@ import {
|
|
|
23
31
|
predecessors,
|
|
24
32
|
successorsOf,
|
|
25
33
|
} from '../ir/core';
|
|
26
|
-
import {
|
|
34
|
+
import { counted, effectful, orderSensitive, placedAt, reevalUnsafe } from '../ir/discipline';
|
|
35
|
+
import { MEM_BASE_OPS, opSig } from '../ir/opcodes';
|
|
27
36
|
|
|
28
37
|
export interface UseSite {
|
|
29
38
|
blk: Block;
|
|
@@ -47,8 +56,11 @@ function rendersAsAddress(op: Op): boolean {
|
|
|
47
56
|
* branch on the contract that the structurer inlines it back under C's own short circuit, so the
|
|
48
57
|
* def block of anything in it is a FOLD ARTIFACT — a rule that names one of these values there
|
|
49
58
|
* emits it above the guard the source wrote (`p != 0 && *p != 0` becoming `v0 = *p;` above its own
|
|
50
|
-
* null check). Asked by the def-block placement rule and by the merge-feed-home scope
|
|
51
|
-
|
|
59
|
+
* null check). Asked by the def-block placement rule and by the merge-feed-home scope; the
|
|
60
|
+
* guarded-call rule asks the same set for the opposite answer, since a counted op is
|
|
61
|
+
* `speculationUnsafe` and so was never folded into the cone in the first place — and
|
|
62
|
+
* structure.ts asks it for a read placed only as structuring starts, which a fold saw as plain. */
|
|
63
|
+
export function shortCircuitGuardedValues(fn: Fn, defOf: Map<Value, Op>): Set<Value> {
|
|
52
64
|
const guarded = new Set<Value>();
|
|
53
65
|
const work: Value[] = [];
|
|
54
66
|
for (const b of fn.blocks) {
|
|
@@ -260,7 +272,7 @@ function readCone(op0: Op, defOf: Map<Value, Op>): Op[] | null {
|
|
|
260
272
|
|
|
261
273
|
/** rank.ts's enumeration gate for the `/addr-home` variation: does the function HAVE a value the
|
|
262
274
|
* 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 `
|
|
275
|
+
* its cone? Mirrors the variation's scope rule in `namePureOp` (the same `sharedBaseClasses` call), minus
|
|
264
276
|
* the loop-header seat refusal (that needs the loop model; a false positive costs one
|
|
265
277
|
* duplicate-collapsed candidate, never a wrong one).
|
|
266
278
|
*
|
|
@@ -375,8 +387,7 @@ export function hasDerivedReadHome(fn: Fn): boolean {
|
|
|
375
387
|
}
|
|
376
388
|
/** any op that writes memory strictly between two ops of one block — the rule's `memWriteBetween`
|
|
377
389
|
* over the straight line the same-block requirement already pins */
|
|
378
|
-
const writeBetween = (b: Block, lo: number, hi: number): boolean =>
|
|
379
|
-
b.ops.slice(lo + 1, hi).some((x) => EFFECTFUL_OPS.has(x.opcode));
|
|
390
|
+
const writeBetween = (b: Block, lo: number, hi: number): boolean => b.ops.slice(lo + 1, hi).some(effectful);
|
|
380
391
|
const standsOnRead = (op0: Op): boolean => {
|
|
381
392
|
const reads = readCone(op0, defOf);
|
|
382
393
|
if (reads === null) {
|
|
@@ -402,15 +413,73 @@ export function hasDerivedReadHome(fn: Fn): boolean {
|
|
|
402
413
|
return false;
|
|
403
414
|
}
|
|
404
415
|
|
|
416
|
+
/** The `/escape-home` variation's SCOPE, as one definition the variation's rule and its enumeration
|
|
417
|
+
* gate both read: a `zext`/`sext` with 2+ distinct consumers, none of them in its own block. A
|
|
418
|
+
* successor ARG counts as a use in the block whose terminator carries it, which is where the edge
|
|
419
|
+
* copy is emitted, so an edge-fed value is never mistaken for one that escaped.
|
|
420
|
+
*
|
|
421
|
+
* `ignoreRet` is the one fact the gate cannot know, on the same terms as `sharedBaseClasses`':
|
|
422
|
+
* `analyze` drops a void function's `ret` operand as a phantom, and a dropped operand moves this
|
|
423
|
+
* answer in BOTH directions — it can take a value below two consumers, and it can remove the
|
|
424
|
+
* at-home use that disqualified one. Which is why the gate asks under both readings rather than
|
|
425
|
+
* picking the one that happens to admit more.
|
|
426
|
+
*
|
|
427
|
+
* The seat refusal (`multiBlockHeaders`) is NOT here: it needs the loop model, it is a clause of
|
|
428
|
+
* the rule rather than of the scope, and over-admitting at the gate costs one duplicate-collapsed
|
|
429
|
+
* candidate. */
|
|
430
|
+
export function escapingExtensions(fn: Fn, ignoreRet: boolean): Set<Op> {
|
|
431
|
+
const consumers = new Map<Value, Set<Op>>();
|
|
432
|
+
const blockOf = new Map<Op, Block>();
|
|
433
|
+
for (const b of fn.blocks) {
|
|
434
|
+
for (const op of b.ops) {
|
|
435
|
+
blockOf.set(op, b);
|
|
436
|
+
if (ignoreRet && op.opcode === 'ret') {
|
|
437
|
+
continue;
|
|
438
|
+
}
|
|
439
|
+
const use = (v: Value) => (consumers.get(v) ?? consumers.set(v, new Set()).get(v)!).add(op);
|
|
440
|
+
for (const o of op.operands) {
|
|
441
|
+
use(o);
|
|
442
|
+
}
|
|
443
|
+
for (const sx of op.successors) {
|
|
444
|
+
for (const a of sx.args) {
|
|
445
|
+
use(a);
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
const out = new Set<Op>();
|
|
451
|
+
for (const b of fn.blocks) {
|
|
452
|
+
for (const op of b.ops) {
|
|
453
|
+
if (op.opcode !== 'zext' && op.opcode !== 'sext') {
|
|
454
|
+
continue;
|
|
455
|
+
}
|
|
456
|
+
const cs = consumers.get(op.results[0]);
|
|
457
|
+
if (cs !== undefined && cs.size >= 2 && [...cs].every((c) => blockOf.get(c) !== b)) {
|
|
458
|
+
out.add(op);
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
return out;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
/** rank.ts's enumeration gate for `/escape-home`: does the function HAVE a value the variation
|
|
466
|
+
* would home? It RUNS the scope rather than restating it — see `escapingExtensions` for the one
|
|
467
|
+
* divergence left, and `sharedBaseClasses` for the shape this follows. */
|
|
468
|
+
export function hasEscapingExtension(fn: Fn): boolean {
|
|
469
|
+
return escapingExtensions(fn, false).size > 0 || escapingExtensions(fn, true).size > 0;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
interface NaturalLoop {
|
|
473
|
+
header: Block;
|
|
474
|
+
latch: Block;
|
|
475
|
+
body: Set<Block>;
|
|
476
|
+
}
|
|
477
|
+
|
|
405
478
|
/** Natural loops: a back edge is `latch → header` with the header dominating the latch, and the
|
|
406
479
|
* body is the backward closure from the latch. Shared by the rules inside `analyze` and by the
|
|
407
480
|
* `/merge-home` gate below, so the two cannot disagree about what "inside a loop" means. */
|
|
408
|
-
function naturalLoops(
|
|
409
|
-
|
|
410
|
-
dom: Map<Block, Set<Block>>,
|
|
411
|
-
predsOf: Map<Block, Block[]>,
|
|
412
|
-
): { header: Block; body: Set<Block> }[] {
|
|
413
|
-
const loops: { header: Block; body: Set<Block> }[] = [];
|
|
481
|
+
function naturalLoops(fn: Fn, dom: Map<Block, Set<Block>>, predsOf: Map<Block, Block[]>): NaturalLoop[] {
|
|
482
|
+
const loops: NaturalLoop[] = [];
|
|
414
483
|
for (const latch of fn.blocks) {
|
|
415
484
|
for (const header of successorsOf(latch)) {
|
|
416
485
|
if (!dom.get(latch)?.has(header)) {
|
|
@@ -425,7 +494,7 @@ function naturalLoops(
|
|
|
425
494
|
work.push(...(predsOf.get(x) ?? []));
|
|
426
495
|
}
|
|
427
496
|
}
|
|
428
|
-
loops.push({ header, body });
|
|
497
|
+
loops.push({ header, latch, body });
|
|
429
498
|
}
|
|
430
499
|
}
|
|
431
500
|
return loops;
|
|
@@ -456,11 +525,11 @@ function naturalLoops(
|
|
|
456
525
|
* re-derive the byte cast (`v0 = (u8 *)*a1 + 6;` then `v0 = (u8 *)v0 + 2;`). Address-shaped
|
|
457
526
|
* bases belong to the cast-aware machinery in l3/basecse.ts, scopebase.ts and nearbase.ts;
|
|
458
527
|
* this variation does not offer a second spelling of them.
|
|
459
|
-
* • an op whose answer depends on WHERE it runs, or that can TRAP — `
|
|
528
|
+
* • an op whose answer depends on WHERE it runs, or that can TRAP — `reevalUnsafe`, read
|
|
460
529
|
* from the registry rather than re-listed. Both halves carry: for a read WHERE it happens is
|
|
461
530
|
* the read rules' question, and a homed divide becomes an unconditional statement at a def
|
|
462
531
|
* block raise/shortcircuit.ts may have made, on paths C's own `&&` would have re-guarded —
|
|
463
|
-
* the
|
|
532
|
+
* the gap ir/discipline.ts books against `speculationUnsafe`.
|
|
464
533
|
* • an `undef` — the variation's premise is a value the source COMPUTED once above the branch, and
|
|
465
534
|
* an uninitialised register was never computed at all: homed, it spells `v0 = uninit_r5;`,
|
|
466
535
|
* a copy of a value nothing wrote, which no asm can have.
|
|
@@ -481,7 +550,7 @@ function naturalLoops(
|
|
|
481
550
|
* joins with params) is refused by this clause and no other, and lifting it takes that
|
|
482
551
|
* function's map-less enumeration 75264 → 150528 candidates, 102s → 207s. A null there is
|
|
483
552
|
* this refusal, not an absent idiom.
|
|
484
|
-
* The `
|
|
553
|
+
* The `namePureOp` scope adds nothing to this list — the whole predicate is here, so the variation's
|
|
485
554
|
* enumeration gate can run it rather than approximate it. */
|
|
486
555
|
function mergeFeedHomes(fn: Fn, dom: Map<Block, Set<Block>>, defOf: Map<Value, Op>, inLoop: Set<Block>): Set<Op> {
|
|
487
556
|
// Every block's incoming COPY SITES — the places its edge assignments render, each once. Neither
|
|
@@ -537,8 +606,10 @@ function mergeFeedHomes(fn: Fn, dom: Map<Block, Set<Block>>, defOf: Map<Value, O
|
|
|
537
606
|
// Every value an edge ARGUMENT renders — the argument itself plus its inlined operand cone,
|
|
538
607
|
// memoized per value so one walk serves every slot. The walk stops DESCENDING at an
|
|
539
608
|
// order-sensitive def: under a call or a memory read the value renders at that op's own position,
|
|
540
|
-
// never at this edge. It crosses a trapping divide, which
|
|
541
|
-
//
|
|
609
|
+
// never at this edge. It crosses a trapping divide, which renders inline at the copy site with its
|
|
610
|
+
// operands. `analyze` names some divides at their def (the helper clause, the pre-update exit
|
|
611
|
+
// rule), after this walk, so their operands still count as rendered here: an extra render, which
|
|
612
|
+
// costs a candidate that homes a value rendered once. The stopping op is still
|
|
542
613
|
// recorded, so a membership test over the cone sees it.
|
|
543
614
|
const coneCache = new Map<Value, Set<Value>>();
|
|
544
615
|
const coneOf = (root: Value): Set<Value> => {
|
|
@@ -562,7 +633,7 @@ function mergeFeedHomes(fn: Fn, dom: Map<Block, Set<Block>>, defOf: Map<Value, O
|
|
|
562
633
|
continue;
|
|
563
634
|
}
|
|
564
635
|
const d = defOf.get(x);
|
|
565
|
-
if (d && !
|
|
636
|
+
if (d && !orderSensitive(d)) {
|
|
566
637
|
stack.push(...d.operands);
|
|
567
638
|
}
|
|
568
639
|
}
|
|
@@ -576,14 +647,14 @@ function mergeFeedHomes(fn: Fn, dom: Map<Block, Set<Block>>, defOf: Map<Value, O
|
|
|
576
647
|
const coneHoldsReevalUnsafe = (v: Value): boolean =>
|
|
577
648
|
[...coneOf(v)].some((x) => {
|
|
578
649
|
const d = defOf.get(x);
|
|
579
|
-
return d !== undefined &&
|
|
650
|
+
return d !== undefined && reevalUnsafe(d);
|
|
580
651
|
});
|
|
581
652
|
/** may this op's result be materialized at its def — and is this variation the one to do it? */
|
|
582
653
|
const eligible = (op: Op): boolean => {
|
|
583
654
|
const v = op.results[0];
|
|
584
655
|
return (
|
|
585
656
|
v !== undefined &&
|
|
586
|
-
!
|
|
657
|
+
!reevalUnsafe(op) &&
|
|
587
658
|
op.opcode !== 'undef' &&
|
|
588
659
|
!(scGuarded.has(v) && coneHoldsReevalUnsafe(v)) &&
|
|
589
660
|
!rendersAsAddress(op) &&
|
|
@@ -667,6 +738,9 @@ export interface StructureAnalysis {
|
|
|
667
738
|
/** defs that must emit as named temps at their own position — calls/loads for effect order,
|
|
668
739
|
* plus the pure defs the homing rules claim */
|
|
669
740
|
materialize: Set<Op>;
|
|
741
|
+
/** the members of `materialize` that name a pre-update read: the escape rule's
|
|
742
|
+
* (`escapesAheadOfUpdate`), and a helper op or exit divide named over one */
|
|
743
|
+
preUpdateHomes: Set<Op>;
|
|
670
744
|
/** cached forward reachability (successors-transitive, excluding the start block itself) */
|
|
671
745
|
reachFrom: (b: Block) => Set<Block>;
|
|
672
746
|
/** where a value's expression ultimately renders — the anchored consumer it inlines into,
|
|
@@ -709,12 +783,6 @@ export interface AnalyzeOptions {
|
|
|
709
783
|
* materializes first). That is a pre-emption, not a conflict: a per-arm source read compiles to
|
|
710
784
|
* a per-arm load on that compiler, so the sunk spelling is one it did not emit from this asm. */
|
|
711
785
|
rereadGlobals?: boolean;
|
|
712
|
-
/** "does the project declare this global volatile?" — a read of a volatile object may NOT be
|
|
713
|
-
* duplicated or moved, so the variation above refuses on one. Answers false for a symbol the map
|
|
714
|
-
* does not carry (and for no map at all), which is the same posture the multi-render rule has
|
|
715
|
-
* always had: without a declaration nothing here can know, and the differ referees the extra
|
|
716
|
-
* load. Where the map DOES know, the variation is silent about it rather than wrong. */
|
|
717
|
-
volatileGlobal?: (name: string) => boolean;
|
|
718
786
|
/** The in-place-join variation (rank.ts `/inplace`). A load whose result is a `cond_br` successor
|
|
719
787
|
* ARG feeds a merge: rendered inline it has no name, so the merge param mints a fresh variable
|
|
720
788
|
* and BOTH arms must assign it. Materialized, the naming walk can home the merge in the load's
|
|
@@ -781,12 +849,39 @@ export interface AnalyzeOptions {
|
|
|
781
849
|
* address (rendered standalone the byte-stride cast lands outside the sum — see
|
|
782
850
|
* `rendersAsAddress`). And the multi-block-loop-header seat the sibling variations refuse. */
|
|
783
851
|
homeDerivedReads?: boolean;
|
|
852
|
+
/** Name a float product a float add or subtract reads, so a contracting compiler cannot fuse the
|
|
853
|
+
* two (StructureOptions.contractsFloatProducts). */
|
|
854
|
+
contractsFloatProducts?: boolean;
|
|
784
855
|
/** The merge-feed-home variation (rank.ts `/merge-home`). A pure value one join's incoming edges
|
|
785
856
|
* render into the SAME parameter slot from 2+ places materializes at its def: the copy machinery
|
|
786
857
|
* has no name to reference, so the default re-derives the whole expression per arm
|
|
787
858
|
* (`m = (-(b & 1) | b & 1) >> 31 & 0x400;` in both) and agbcc if-converts each copy. The scope
|
|
788
859
|
* and every refusal it states are `mergeFeedHomes` above, which the enumeration gate also runs. */
|
|
789
860
|
homeMergeFeeds?: boolean;
|
|
861
|
+
/** The escaping-extension-home variation (rank.ts `/escape-home`). A `zext`/`sext` with 2+ distinct
|
|
862
|
+
* consumers, none of them in its own block, renders by default re-evaluated at every consumer —
|
|
863
|
+
* `(u16)v` spelled once per use — where the asm narrowed once into a register and every later
|
|
864
|
+
* block read that register. Materialized, the truncation is a local computed at the def, which
|
|
865
|
+
* is where the asm computed it.
|
|
866
|
+
*
|
|
867
|
+
* The MIRROR of `/expr-home`, which wants a def OUTSIDE a loop with a consumer inside: here every
|
|
868
|
+
* consumer is outside the def's block, and neither scope reaches the other's shape. Straight-line
|
|
869
|
+
* values whose def block also consumes them stay with the siblings — a consumer beside the def is
|
|
870
|
+
* what says the compiler could re-derive there. What is left reaches six corpus rows over two
|
|
871
|
+
* toolchains, which `bench diff`'s FAN section names, plus the synthetic row that pins it.
|
|
872
|
+
*
|
|
873
|
+
* A THIRD SPELLING of the shape `raise/narrowlocal.ts` tabulates. That pass reads one extension
|
|
874
|
+
* over raw in-edges and picks between `u16 v` and `s32 v` + `(u16)v` off branch-shape evidence;
|
|
875
|
+
* this one leaves the width alone and gives the truncation a name where it happened. They do not
|
|
876
|
+
* overlap — narrowlocal judges a block PARAMETER, this an extension's result — and they are
|
|
877
|
+
* neighbours rather than alternatives.
|
|
878
|
+
*
|
|
879
|
+
* A differ-refereed variation, never a default: `pokeemerald:AcroBikeHandleInputTurning` is a
|
|
880
|
+
* MATCH inside this scope, so forced on the spelling would be REPLACED across the fan rather than
|
|
881
|
+
* added to it. Adding materialization preserves semantics for the admitted values — an extension's
|
|
882
|
+
* own result is never an address, so `rendersAsAddress` has nothing to refuse here — and the
|
|
883
|
+
* multi-block-loop-header seat refusal is the same decline-avoidance the siblings make. */
|
|
884
|
+
homeEscapingExtensions?: boolean;
|
|
790
885
|
/** DEF-BLOCK PLACEMENT for memory reads — WHERE the read happens, not where the value lives.
|
|
791
886
|
* The sibling of the homing variations above: there the question is which register or offset holds a
|
|
792
887
|
* value, here which BLOCK performs the read. A read whose every render sits in a block its own
|
|
@@ -1057,10 +1152,6 @@ function makeReach(): {
|
|
|
1057
1152
|
reachCache.set(b, r);
|
|
1058
1153
|
return r;
|
|
1059
1154
|
};
|
|
1060
|
-
// Reachability that never passes THROUGH `avoid` — the def-block-avoiding form for
|
|
1061
|
-
// per-iteration path checks: a path that re-enters the def's block re-executes the def, so
|
|
1062
|
-
// writes on it belong to the NEXT dynamic instance (which re-renders anyway) and must not
|
|
1063
|
-
// count against this one. Uncached (per-decision graphs are small).
|
|
1064
1155
|
const reachAvoiding = (from: Block, avoid: Block): Set<Block> => {
|
|
1065
1156
|
const r = new Set<Block>();
|
|
1066
1157
|
const stack = successorsOf(from).filter((s) => s !== avoid);
|
|
@@ -1081,26 +1172,25 @@ function makeReach(): {
|
|
|
1081
1172
|
return { reachFrom, reachAvoiding };
|
|
1082
1173
|
}
|
|
1083
1174
|
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
// what decides whether an inlined call would EXECUTE twice).
|
|
1175
|
+
/** THE USE REGISTRY. Every use of a value, POSITIONED: the consuming op and its block/index.
|
|
1176
|
+
* Successor args are uses AT the terminator (they render in argAssigns at block end). A void
|
|
1177
|
+
* function's `ret` operand is a phantom, not a real use — skipping it lets a call whose result
|
|
1178
|
+
* ONLY flows into the suppressed return read as a dead (side-effect) call, so `sideEffects()`
|
|
1179
|
+
* emits it. One operand SLOT = one entry (an op reading a value twice records two uses — that
|
|
1180
|
+
* count is what decides whether an inlined call would EXECUTE twice). */
|
|
1181
|
+
interface UseIndex {
|
|
1182
|
+
/** every positioned use of a value; a value absent here is dead */
|
|
1183
|
+
useSitesOf: Map<Value, UseSite[]>;
|
|
1184
|
+
opIndex: Map<Op, number>;
|
|
1185
|
+
opBlock: Map<Op, Block>;
|
|
1186
|
+
blockPos: Map<Block, number>;
|
|
1187
|
+
/** True if `def`'s value is still needed after a call — a call lies strictly between the def and
|
|
1188
|
+
* one of its `consumers`. Such a value survives in a callee-saved register (a local), which is
|
|
1189
|
+
* what materializing it reproduces. */
|
|
1190
|
+
liveAcrossCall(def: Op, consumers: Op[]): boolean;
|
|
1191
|
+
}
|
|
1192
|
+
|
|
1193
|
+
function indexUses(fn: Fn, returnsVoid: boolean): UseIndex {
|
|
1104
1194
|
const useSitesOf = new Map<Value, UseSite[]>();
|
|
1105
1195
|
const opIndex = new Map<Op, number>();
|
|
1106
1196
|
const opBlock = new Map<Op, Block>();
|
|
@@ -1132,36 +1222,6 @@ export function analyze(fn: Fn, returnsVoid: boolean, opts: AnalyzeOptions = {})
|
|
|
1132
1222
|
}
|
|
1133
1223
|
}
|
|
1134
1224
|
}
|
|
1135
|
-
// Which values ride a branch edge as a successor ARG, in two scopes with two different readers.
|
|
1136
|
-
//
|
|
1137
|
-
// `branchArgFed` is EVERY multi-successor terminator's, and it is a correctness fact: an edge
|
|
1138
|
-
// argument is emitted INSIDE the arm it belongs to, so a value that renders only there runs only
|
|
1139
|
-
// on that path. For a call that is an effect the IR performs unconditionally and the C performs
|
|
1140
|
-
// sometimes, and a `switch_br` does it as readily as a `cond_br`. Plain `br` args are excluded
|
|
1141
|
-
// because their edge copy is on the one path that reaches the successor, so nothing conditional
|
|
1142
|
-
// happens to them.
|
|
1143
|
-
//
|
|
1144
|
-
// `condBrArgFed` is the `/inplace` variation's narrower reading — a preference about where a load's
|
|
1145
|
-
// value is homed, whose own scope note is on AnalyzeOptions.materializeJoinFeeds.
|
|
1146
|
-
const branchArgFed = new Set<Value>();
|
|
1147
|
-
const condBrArgFed = new Set<Value>();
|
|
1148
|
-
for (const b of fn.blocks) {
|
|
1149
|
-
for (const op of b.ops) {
|
|
1150
|
-
if (op.successors.length > 1) {
|
|
1151
|
-
for (const s of op.successors) {
|
|
1152
|
-
for (const a of s.args) {
|
|
1153
|
-
branchArgFed.add(a);
|
|
1154
|
-
if (op.opcode === 'cond_br') {
|
|
1155
|
-
condBrArgFed.add(a);
|
|
1156
|
-
}
|
|
1157
|
-
}
|
|
1158
|
-
}
|
|
1159
|
-
}
|
|
1160
|
-
}
|
|
1161
|
-
}
|
|
1162
|
-
/** True if `def`'s value is still needed after a call — a call lies strictly between the def and
|
|
1163
|
-
* one of its `consumers`. Such a value survives in a callee-saved register (a local), which is
|
|
1164
|
-
* what materializing it reproduces. */
|
|
1165
1225
|
const liveAcrossCall = (def: Op, consumers: Op[]): boolean => {
|
|
1166
1226
|
const dp = linPos(def);
|
|
1167
1227
|
const usePos = consumers.map(linPos);
|
|
@@ -1191,25 +1251,124 @@ export function analyze(fn: Fn, returnsVoid: boolean, opts: AnalyzeOptions = {})
|
|
|
1191
1251
|
}
|
|
1192
1252
|
});
|
|
1193
1253
|
}
|
|
1254
|
+
return { useSitesOf, opIndex, opBlock, blockPos, liveAcrossCall };
|
|
1255
|
+
}
|
|
1194
1256
|
|
|
1195
|
-
|
|
1257
|
+
/** Which values ride a branch edge as a successor ARG, in two scopes with two different readers. */
|
|
1258
|
+
interface EdgeArgUses {
|
|
1259
|
+
/** EVERY multi-successor terminator's, and it is a correctness fact: an edge argument is emitted
|
|
1260
|
+
* INSIDE the arm it belongs to, so a value that renders only there runs only on that path. For a
|
|
1261
|
+
* call that is an effect the IR performs unconditionally and the C performs sometimes, and a
|
|
1262
|
+
* `switch_br` does it as readily as a `cond_br`. Plain `br` args are excluded because their edge
|
|
1263
|
+
* copy is on the one path that reaches the successor, so nothing conditional happens to them. */
|
|
1264
|
+
branchArgFed: Set<Value>;
|
|
1265
|
+
/** the `/inplace` variation's narrower reading — a preference about where a load's value is
|
|
1266
|
+
* homed, whose own scope note is on AnalyzeOptions.materializeJoinFeeds */
|
|
1267
|
+
condBrArgFed: Set<Value>;
|
|
1268
|
+
/** How many times each value rides a BACK edge (to a block that dominates its own: a loop's
|
|
1269
|
+
* header), which `ridesEdge` weighs apart. Decided by dominance, not layout: a return tail laid
|
|
1270
|
+
* out above the branch into it is still a forward edge, and its copy renders in the arm.
|
|
1271
|
+
*
|
|
1272
|
+
* And only from a latch that is its loop's ONLY exit. Then the latch's test is the bottom test
|
|
1273
|
+
* and its back-edge copy is the update the body runs on every iteration. When another block
|
|
1274
|
+
* leaves the loop too, the structurer may keep that exit as the loop's own test and render the
|
|
1275
|
+
* latch's branch as an `if` in the body, with the copy in one arm: agbcc's `while (n-- > 0) { q
|
|
1276
|
+
* = q - 1; if (u == k) return *q; t = cg(t) + H[n & 3] & cb(q + 1); }` (with `u` 0) runs `bl
|
|
1277
|
+
* cb` ahead of the latch's `bgt`, and inlined into the copy it ran only on the iterations that
|
|
1278
|
+
* went round again. Such an edge weighs as a forward one. */
|
|
1279
|
+
backArgFed: Map<Value, number>;
|
|
1280
|
+
}
|
|
1196
1281
|
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
const
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1282
|
+
function edgeArgUses(fn: Fn, domOf: Map<Block, Set<Block>>): EdgeArgUses {
|
|
1283
|
+
const branchArgFed = new Set<Value>();
|
|
1284
|
+
const condBrArgFed = new Set<Value>();
|
|
1285
|
+
const backArgFed = new Map<Value, number>();
|
|
1286
|
+
const soleExitLatch = new Set<Block>();
|
|
1287
|
+
for (const L of naturalLoops(fn, domOf, predecessors(fn))) {
|
|
1288
|
+
if (![...L.body].some((x) => x !== L.latch && successorsOf(x).some((t) => !L.body.has(t)))) {
|
|
1289
|
+
soleExitLatch.add(L.latch);
|
|
1290
|
+
}
|
|
1291
|
+
}
|
|
1292
|
+
const backEdge = (from: Block, to: Block): boolean => (domOf.get(from)?.has(to) ?? false) && soleExitLatch.has(from);
|
|
1293
|
+
for (const b of fn.blocks) {
|
|
1294
|
+
for (const op of b.ops) {
|
|
1295
|
+
if (op.successors.length > 1) {
|
|
1296
|
+
for (const s of op.successors) {
|
|
1297
|
+
for (const a of s.args) {
|
|
1298
|
+
branchArgFed.add(a);
|
|
1299
|
+
if (op.opcode === 'cond_br') {
|
|
1300
|
+
condBrArgFed.add(a);
|
|
1301
|
+
}
|
|
1302
|
+
if (backEdge(b, s.block)) {
|
|
1303
|
+
backArgFed.set(a, (backArgFed.get(a) ?? 0) + 1);
|
|
1304
|
+
}
|
|
1305
|
+
}
|
|
1306
|
+
}
|
|
1307
|
+
}
|
|
1308
|
+
}
|
|
1309
|
+
}
|
|
1310
|
+
return { branchArgFed, condBrArgFed, backArgFed };
|
|
1311
|
+
}
|
|
1312
|
+
|
|
1313
|
+
/** THE EFFECT-ORDERING MODEL — inline-at-use barriers. `expr()` renders a def's computation AT ITS
|
|
1314
|
+
* USE, which silently MOVES it: a call executes once per rendered copy (`foo(a0)+foo(a0)`), a load
|
|
1315
|
+
* reads memory at the render point (it can textually sink past an aliasing store). The model: a
|
|
1316
|
+
* call/load/aload def may inline ONLY when rendering cannot change behavior — exactly one render
|
|
1317
|
+
* position, and the program-order gap between def and render crosses no memory write (loads) / no
|
|
1318
|
+
* memory access at all (calls, whose own reads+writes must not reorder against anything). Every
|
|
1319
|
+
* other case gets a NAMED TEMP assigned at the def's own program position (sideEffects) — which is
|
|
1320
|
+
* precisely the register the compiler used. */
|
|
1321
|
+
interface MaterializeState {
|
|
1322
|
+
/** defs that must emit as named temps at their own position */
|
|
1323
|
+
materialize: Set<Op>;
|
|
1324
|
+
/** the members of `materialize` that name a pre-update read */
|
|
1325
|
+
preUpdateHomes: Set<Op>;
|
|
1326
|
+
/** the divides the pre-update exit rule named (`rebuiltPast`) */
|
|
1327
|
+
exitDivides: Set<Op>;
|
|
1328
|
+
/** result values the address-home variation materialized — the load rule's admission key */
|
|
1329
|
+
addressHomedBases: Set<Value>;
|
|
1330
|
+
}
|
|
1331
|
+
|
|
1332
|
+
/** an op the asm reached by CALLING a runtime helper */
|
|
1333
|
+
function isHelper(op: Op): boolean {
|
|
1334
|
+
return placedAt(op) === 'helper';
|
|
1335
|
+
}
|
|
1336
|
+
|
|
1337
|
+
/** one the helper clause named: a statement sequenced as a named call is */
|
|
1338
|
+
function namedHelper(materialize: ReadonlySet<Op>, op: Op): boolean {
|
|
1339
|
+
return materialize.has(op) && isHelper(op);
|
|
1340
|
+
}
|
|
1341
|
+
|
|
1342
|
+
/** WHERE A VALUE'S EXPRESSION RENDERS, read off `materialize` as it stands when asked. The two
|
|
1343
|
+
* position caches hold until `invalidate()`. */
|
|
1344
|
+
interface RenderModel {
|
|
1212
1345
|
/** an op that renders AT ITS OWN position: a statement, a terminator, a materialized or dead def */
|
|
1346
|
+
anchored(op: Op): boolean;
|
|
1347
|
+
consumersOf(op: Op): Op[];
|
|
1348
|
+
/** Does `op`'s value reach a float add or subtract, directly or through negations only? */
|
|
1349
|
+
feedsFloatAdd(op: Op): boolean;
|
|
1350
|
+
/** Where a value's expression is ultimately EMITTED: the anchored consumer (statement op,
|
|
1351
|
+
* terminator, materialized def) it inlines into, transitively through single-use pure ops.
|
|
1352
|
+
* null = renders in several places / unresolvable (treated conservatively by the caller). */
|
|
1353
|
+
emitPos(op: Op): { blk: Block; idx: number } | null;
|
|
1354
|
+
/** EVERY position a value's expression renders at — `emitPos` generalized to the whole set (it
|
|
1355
|
+
* answers one place or gives up), by following ALL consumers transitively. That matters for the
|
|
1356
|
+
* value-home variation: a pure expression with two consumers (`gOut = e; return e;`) has no
|
|
1357
|
+
* single emit position, so `emitPos` answers null and every memory read feeding it is forced
|
|
1358
|
+
* into a local — even when re-reading at both places is provably equivalent. Null only for a
|
|
1359
|
+
* genuine cycle (defensive: SSA use-def is acyclic through ops), which the caller treats as
|
|
1360
|
+
* unresolvable.
|
|
1361
|
+
*
|
|
1362
|
+
* NEVER for a call: two render positions mean two executions, so a call whose consumer renders
|
|
1363
|
+
* in several places must keep answering null and materialize. */
|
|
1364
|
+
emitPositions(op: Op): { blk: Block; idx: number }[] | null;
|
|
1365
|
+
/** clears both position caches */
|
|
1366
|
+
invalidate(): void;
|
|
1367
|
+
}
|
|
1368
|
+
|
|
1369
|
+
function makeRenderModel(uses: UseIndex, materialize: ReadonlySet<Op>): RenderModel {
|
|
1370
|
+
const { useSitesOf, opIndex, opBlock, blockPos } = uses;
|
|
1371
|
+
const emitPosCache = new Map<Op, { blk: Block; idx: number } | null>();
|
|
1213
1372
|
const anchored = (op: Op): boolean =>
|
|
1214
1373
|
op.successors.length > 0 ||
|
|
1215
1374
|
op.opcode === 'ret' ||
|
|
@@ -1219,6 +1378,10 @@ export function analyze(fn: Fn, returnsVoid: boolean, opts: AnalyzeOptions = {})
|
|
|
1219
1378
|
!op.results.length ||
|
|
1220
1379
|
!useSitesOf.has(op.results[0]);
|
|
1221
1380
|
const consumersOf = (op: Op): Op[] => [...new Set((useSitesOf.get(op.results[0]) ?? []).map((s) => s.op))];
|
|
1381
|
+
const feedsFloatAdd = (op: Op): boolean =>
|
|
1382
|
+
consumersOf(op).some(
|
|
1383
|
+
(c) => c.opcode === 'fadd' || c.opcode === 'fsub' || (c.opcode === 'fneg' && feedsFloatAdd(c)),
|
|
1384
|
+
);
|
|
1222
1385
|
const emitPos = (op: Op): { blk: Block; idx: number } | null => {
|
|
1223
1386
|
if (emitPosCache.has(op)) {
|
|
1224
1387
|
return emitPosCache.get(op)!;
|
|
@@ -1233,15 +1396,6 @@ export function analyze(fn: Fn, returnsVoid: boolean, opts: AnalyzeOptions = {})
|
|
|
1233
1396
|
emitPosCache.set(op, res);
|
|
1234
1397
|
return res;
|
|
1235
1398
|
};
|
|
1236
|
-
// EVERY position a value's expression renders at — `emitPos` generalized to the whole set (it
|
|
1237
|
-
// answers one place or gives up), by following ALL consumers transitively. That matters for
|
|
1238
|
-
// the value-home variation: a pure expression with two consumers (`gOut = e; return e;`) has no single
|
|
1239
|
-
// emit position, so `emitPos` answers null and every memory read feeding it is forced into a
|
|
1240
|
-
// local — even when re-reading at both places is provably equivalent. Null only for a genuine
|
|
1241
|
-
// cycle (defensive: SSA use-def is acyclic through ops), which the caller treats as unresolvable.
|
|
1242
|
-
//
|
|
1243
|
-
// NEVER for a call: two render positions mean two executions, so a call whose consumer renders in
|
|
1244
|
-
// several places must keep answering null and materialize.
|
|
1245
1399
|
const emitPosSetCache = new Map<Op, { blk: Block; idx: number }[] | null>();
|
|
1246
1400
|
const emitPositions = (op: Op, visiting: Set<Op> = new Set()): { blk: Block; idx: number }[] | null => {
|
|
1247
1401
|
const hit = emitPosSetCache.get(op);
|
|
@@ -1278,21 +1432,116 @@ export function analyze(fn: Fn, returnsVoid: boolean, opts: AnalyzeOptions = {})
|
|
|
1278
1432
|
emitPosSetCache.set(op, res);
|
|
1279
1433
|
return res;
|
|
1280
1434
|
};
|
|
1281
|
-
const
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1435
|
+
const invalidate = (): void => {
|
|
1436
|
+
emitPosCache.clear();
|
|
1437
|
+
emitPosSetCache.clear();
|
|
1438
|
+
};
|
|
1439
|
+
return { anchored, consumersOf, feedsFloatAdd, emitPos, emitPositions, invalidate };
|
|
1440
|
+
}
|
|
1441
|
+
|
|
1442
|
+
interface InlineReach {
|
|
1443
|
+
ridesEdge(call: Op): boolean;
|
|
1444
|
+
spelledTwice(call: Op): boolean;
|
|
1445
|
+
}
|
|
1446
|
+
|
|
1447
|
+
function makeInlineReach(uses: UseIndex, edges: EdgeArgUses, materialize: ReadonlySet<Op>): InlineReach {
|
|
1448
|
+
const { useSitesOf } = uses;
|
|
1449
|
+
const { branchArgFed, backArgFed } = edges;
|
|
1450
|
+
/** Does `hit` hold of `call`'s value, or of any value it reaches through the ops it would be
|
|
1451
|
+
* inlined into? Such an op renders where its consumer does. A load is a read, not an effect, so
|
|
1452
|
+
* the walk passes through it: `*f()` read twice spells `f()` twice. The walk stops at a named op,
|
|
1453
|
+
* which renders at its own position, and at another effect, which these same rules place. That
|
|
1454
|
+
* makes it narrower than `!anchored`: a call whose value is used is not anchored, and the walk
|
|
1455
|
+
* still stops there. */
|
|
1456
|
+
const reachesThroughInline = (call: Op, hit: (x: Value, sites: readonly UseSite[]) => boolean): boolean => {
|
|
1457
|
+
const seen = new Set<Value>();
|
|
1458
|
+
const walk = (x: Value): boolean => {
|
|
1459
|
+
if (seen.has(x)) {
|
|
1460
|
+
return false;
|
|
1461
|
+
}
|
|
1462
|
+
seen.add(x);
|
|
1463
|
+
const sites = useSitesOf.get(x) ?? [];
|
|
1464
|
+
return (
|
|
1465
|
+
hit(x, sites) ||
|
|
1466
|
+
sites.some(
|
|
1467
|
+
(u) =>
|
|
1468
|
+
!effectful(u.op) &&
|
|
1469
|
+
u.op.successors.length === 0 &&
|
|
1470
|
+
!materialize.has(u.op) &&
|
|
1471
|
+
u.op.results.length > 0 &&
|
|
1472
|
+
walk(u.op.results[0]),
|
|
1473
|
+
)
|
|
1474
|
+
);
|
|
1475
|
+
};
|
|
1476
|
+
return walk(call.results[0]);
|
|
1477
|
+
};
|
|
1478
|
+
/** Does `call`'s value reach an edge argument through the ops it would be inlined into, and
|
|
1479
|
+
* so render where that edge copy does? Such an op renders where its consumer does, so a call under
|
|
1480
|
+
* `f(x) + 1` or `*f(x)` rides the copy exactly as a bare `f(x)` does.
|
|
1481
|
+
*
|
|
1482
|
+
* A FORWARD edge's copy renders in an arm or after the loop, and a value two back-edge args carry
|
|
1483
|
+
* renders twice, so reaching either is enough. A value ONE back-edge arg carries renders once, in
|
|
1484
|
+
* the update copy at the foot of the body, and a forward edge carrying it too reads the loop
|
|
1485
|
+
* variable that copy wrote. That copy moves the call only past what the latch runs after it, and
|
|
1486
|
+
* the barrier scan in `nameAccess` already weighs exactly that: a store, a call or a read in
|
|
1487
|
+
* another part of the terminator bars the call and names it, and a read in the same copy renders
|
|
1488
|
+
* after the call, as every compiler evaluates it. So `s = s + g(i)` stays inline (`for (…) s = s
|
|
1489
|
+
* + g(i);`). */
|
|
1490
|
+
const ridesEdge = (call: Op): boolean =>
|
|
1491
|
+
reachesThroughInline(call, (x) => {
|
|
1492
|
+
const carried = backArgFed.get(x) ?? 0;
|
|
1493
|
+
return carried > 1 || (carried === 0 && branchArgFed.has(x));
|
|
1494
|
+
});
|
|
1495
|
+
/** Does `call`'s value, through the ops it would be inlined into, reach ONE op that reads it
|
|
1496
|
+
* twice? That op spells it twice — both operands of `s * s`, or both edge copies of a `br
|
|
1497
|
+
* ^bb3(%9, %9)` — and the call runs once per spelling. A multi-successor terminator's arguments
|
|
1498
|
+
* are `ridesEdge`'s question, which weighs a do-while's exit copy reading the loop variable its
|
|
1499
|
+
* update copy wrote. */
|
|
1500
|
+
const spelledTwice = (call: Op): boolean =>
|
|
1501
|
+
reachesThroughInline(call, (_, sites) =>
|
|
1502
|
+
sites.some((u, i) => u.op.successors.length <= 1 && sites.findIndex((v) => v.op === u.op) !== i),
|
|
1503
|
+
);
|
|
1504
|
+
return { ridesEdge, spelledTwice };
|
|
1505
|
+
}
|
|
1506
|
+
|
|
1507
|
+
/** A natural loop whose latch's terminator branches back to the header: the test sits at the
|
|
1508
|
+
* bottom of the body. */
|
|
1509
|
+
type BottomTestedLoop = NaturalLoop & { term: Op; back: Successor };
|
|
1510
|
+
|
|
1511
|
+
/** The natural loops, from the caller's dominators, and the loop rules over them. With no `dom`
|
|
1512
|
+
* there are no loops and every rule stands down — the same posture as the `defs`-carried rules. */
|
|
1513
|
+
interface LoopRules {
|
|
1514
|
+
loopBodies: NaturalLoop[];
|
|
1515
|
+
/** Never seat a materialized def in a MULTI-BLOCK loop header: a test-at-top `while`'s condition
|
|
1516
|
+
* has no seat for a materialized temp (the structurer's headerPure gate), so materializing there
|
|
1517
|
+
* trades a structuring function for a decline. Self-loop headers stay eligible — their
|
|
1518
|
+
* kept-guard do-while form hosts the temp. */
|
|
1519
|
+
multiBlockHeaders: Set<Block>;
|
|
1520
|
+
bottomTested: BottomTestedLoop[];
|
|
1521
|
+
/** The def's value enters some loop's header live and every consumer sits outside that loop, as
|
|
1522
|
+
* does the def: the value is carried ACROSS the loop, not into it. */
|
|
1523
|
+
liveAcrossLoop(def: Op, r: Value, consumers: Op[]): boolean;
|
|
1524
|
+
/** Does `r`, computed by `op` in `L`'s body, read a loop variable the update overwrites? */
|
|
1525
|
+
readsPreUpdate(L: BottomTestedLoop, op: Op, r: Value): boolean;
|
|
1526
|
+
escapesAheadOfUpdate(op: Op, r: Value, consumers: Op[]): boolean;
|
|
1527
|
+
rebuiltPast(op: Op, r: Value): boolean;
|
|
1528
|
+
/** Is `r` read where the update has run: after the loop, or by its bottom test? */
|
|
1529
|
+
readAfterUpdate(L: BottomTestedLoop, r: Value, consumers: Op[]): boolean;
|
|
1530
|
+
}
|
|
1531
|
+
|
|
1532
|
+
function makeLoopRules(a: {
|
|
1533
|
+
fn: Fn;
|
|
1534
|
+
dom: Map<Block, Set<Block>> | undefined;
|
|
1535
|
+
predsOf: Map<Block, Block[]>;
|
|
1536
|
+
liveIn: Map<Block, Set<Value>>;
|
|
1537
|
+
uses: UseIndex;
|
|
1538
|
+
defOf: Map<Value, Op>;
|
|
1539
|
+
render: RenderModel;
|
|
1540
|
+
materialize: ReadonlySet<Op>;
|
|
1541
|
+
}): LoopRules {
|
|
1542
|
+
const { fn, dom, predsOf, liveIn, defOf, render, materialize } = a;
|
|
1543
|
+
const { opBlock } = a.uses;
|
|
1289
1544
|
const loopBodies = dom ? naturalLoops(fn, dom, predsOf) : [];
|
|
1290
|
-
/** The def's value enters some loop's header live and every consumer sits outside that loop,
|
|
1291
|
-
* as does the def: the value is carried ACROSS the loop, not into it. */
|
|
1292
|
-
// Never for a def in a MULTI-BLOCK loop header: a test-at-top `while`'s condition has no seat
|
|
1293
|
-
// for a materialized temp (the structurer's headerPure gate), so materializing there trades a
|
|
1294
|
-
// structuring function for a decline. Self-loop headers stay eligible — their kept-guard
|
|
1295
|
-
// do-while form hosts the temp.
|
|
1296
1545
|
const multiBlockHeaders = new Set(loopBodies.filter((L) => L.body.size > 1).map((L) => L.header));
|
|
1297
1546
|
const liveAcrossLoop = (def: Op, r: Value, consumers: Op[]): boolean =>
|
|
1298
1547
|
!multiBlockHeaders.has(opBlock.get(def)!) &&
|
|
@@ -1302,46 +1551,203 @@ export function analyze(fn: Fn, returnsVoid: boolean, opts: AnalyzeOptions = {})
|
|
|
1302
1551
|
!L.body.has(opBlock.get(def)!) &&
|
|
1303
1552
|
consumers.every((c) => !L.body.has(opBlock.get(c)!)),
|
|
1304
1553
|
);
|
|
1554
|
+
/** THE PRE-UPDATE ESCAPE. A value a bottom-tested loop computes and the code after it reads,
|
|
1555
|
+
* whose expression reads a loop variable — `do { s = &f->v; f = f->next; } while (--n); *out =
|
|
1556
|
+
* s;`. Inlined at the post-loop read it re-reads that variable's NAME, which by then holds the
|
|
1557
|
+
* value the update wrote, one iteration past the one the value was computed from. agbcc kept the
|
|
1558
|
+
* computed value in its own register instead (`add r3, r0, #4` inside the loop, `str r3, [r2]`
|
|
1559
|
+
* after it), and naming the def at its own position is that register. Without the name the
|
|
1560
|
+
* structurer declines the loop (`loopEscapeHazard`, hazards.ts); an exit-edge ARG carrying the
|
|
1561
|
+
* same value is the sink's.
|
|
1562
|
+
*
|
|
1563
|
+
* Naming a def at its own position never changes a value, and `loopEscapeHazard` stays the
|
|
1564
|
+
* check. What the name does change is which functions structure at all, and a function this rule
|
|
1565
|
+
* lets through is spelled by the rest of the structurer, whose naming has shapes it gets wrong.
|
|
1566
|
+
* So it is kept to the shape the listing has, A SELF-LOOP: in a body of more than one block a
|
|
1567
|
+
* differential fuzz over generated IR found functions it unlocked that were then spelled wrong —
|
|
1568
|
+
* a loop variable's name adopted in place ahead of a read of its old value, the calls of a bottom
|
|
1569
|
+
* test reordered — each wrong on shapes that already structure, too. In a self-loop every reader
|
|
1570
|
+
* outside the body lies past the latch's exit, where the update has run. The structurer adds one
|
|
1571
|
+
* refusal of its own, on names (`preUpdateHomes`, structure.ts).
|
|
1572
|
+
*
|
|
1573
|
+
* Asked only once every other rule has settled (`escapePhase` in `analyze`), so the walk stops at
|
|
1574
|
+
* each def those rules name: a materialized def renders as its name. It stops at a BACK-EDGE ARG
|
|
1575
|
+
* too — reading it is reading the post-update value, which is what the name holds. */
|
|
1576
|
+
const bottomTested = loopBodies.flatMap((L) => {
|
|
1577
|
+
const term = L.latch.ops[L.latch.ops.length - 1];
|
|
1578
|
+
const back = term.successors.find((sc) => sc.block === L.header);
|
|
1579
|
+
return back ? [{ ...L, term, back }] : [];
|
|
1580
|
+
});
|
|
1581
|
+
const escapeLoops = bottomTested.filter((L) => L.body.size === 1);
|
|
1582
|
+
const readsPreUpdate = (L: BottomTestedLoop, op: Op, r: Value): boolean => {
|
|
1583
|
+
const seen = new Set<Value>();
|
|
1584
|
+
const readsUpdated = (x: Value): boolean => {
|
|
1585
|
+
if (seen.has(x) || L.back.args.includes(x)) {
|
|
1586
|
+
return false;
|
|
1587
|
+
}
|
|
1588
|
+
seen.add(x);
|
|
1589
|
+
if (L.header.params.includes(x)) {
|
|
1590
|
+
return true;
|
|
1591
|
+
}
|
|
1592
|
+
const d = defOf.get(x);
|
|
1593
|
+
if (!d || !L.body.has(opBlock.get(d)!) || (d !== op && materialize.has(d))) {
|
|
1594
|
+
return false;
|
|
1595
|
+
}
|
|
1596
|
+
return d.operands.some(readsUpdated);
|
|
1597
|
+
};
|
|
1598
|
+
return readsUpdated(r);
|
|
1599
|
+
};
|
|
1600
|
+
const escapesAheadOfUpdate = (op: Op, r: Value, consumers: Op[]): boolean =>
|
|
1601
|
+
escapeLoops.some(
|
|
1602
|
+
(L) =>
|
|
1603
|
+
L.body.has(opBlock.get(op)!) &&
|
|
1604
|
+
!consumers.every((c) => L.body.has(opBlock.get(c)!)) &&
|
|
1605
|
+
readsPreUpdate(L, op, r),
|
|
1606
|
+
);
|
|
1607
|
+
/** Would a value that reads `op`'s result `r` past the update be rebuilt BEHIND a memory access
|
|
1608
|
+
* or an effect the asm ran after `op`? `op` sits in a block every iteration runs (the latch, or
|
|
1609
|
+
* one dominating it), and `r` reads a loop variable, so where that value renders the variable's
|
|
1610
|
+
* name no longer holds what `op` read, and the value is rebuilt from `op`'s operands:
|
|
1611
|
+
* • an arg of the latch's exit edge, rebuilt at the latch op computing it (`preUpdateCopyHome`,
|
|
1612
|
+
* hazards.ts) — by the sink, or where it is the back edge's arg too, by the update copy at
|
|
1613
|
+
* the foot of the body, which runs later still;
|
|
1614
|
+
* • a latch op read after the loop, rebuilt there. One the escape rule names first
|
|
1615
|
+
* (`escapesAheadOfUpdate`, a self-loop) is a materialized def this walk stops at, so it is
|
|
1616
|
+
* spelled at its own position with the divide inline.
|
|
1617
|
+
* Either way what `op` is carried past is everything the iteration runs between `op` and the
|
|
1618
|
+
* latch op the value is computed at.
|
|
1619
|
+
*
|
|
1620
|
+
* Never for a divide in a multi-block loop header with an edge out of `L`: named, it is a
|
|
1621
|
+
* pre-update home (`preUpdateHomes`), and the structurer refuses a home in a loop whose
|
|
1622
|
+
* variable's name a block after the loop also holds (`sharesALoopName`), where the loop lifts
|
|
1623
|
+
* with the divide unnamed. mwcc's `do { t = k / n; if (*q == 5) break; … } while (--n);` puts
|
|
1624
|
+
* the `divw` ahead of the header's `beq` out. */
|
|
1625
|
+
const rebuiltPast = (op: Op, r: Value): boolean =>
|
|
1626
|
+
bottomTested.some((L) => {
|
|
1627
|
+
const ob = opBlock.get(op)!;
|
|
1628
|
+
if (!L.body.has(ob) || !dom!.get(L.latch)!.has(ob) || !readsPreUpdate(L, op, r)) {
|
|
1629
|
+
return false;
|
|
1630
|
+
}
|
|
1631
|
+
if (ob !== L.latch && multiBlockHeaders.has(ob) && successorsOf(ob).some((s) => !L.body.has(s))) {
|
|
1632
|
+
return false;
|
|
1633
|
+
}
|
|
1634
|
+
const seen = new Set<Value>();
|
|
1635
|
+
const reaches = (x: Value): boolean => {
|
|
1636
|
+
if (x === r) {
|
|
1637
|
+
return true;
|
|
1638
|
+
}
|
|
1639
|
+
if (seen.has(x)) {
|
|
1640
|
+
return false;
|
|
1641
|
+
}
|
|
1642
|
+
seen.add(x);
|
|
1643
|
+
const d = defOf.get(x);
|
|
1644
|
+
return !!d && L.body.has(opBlock.get(d)!) && !materialize.has(d) && d.operands.some(reaches);
|
|
1645
|
+
};
|
|
1646
|
+
// The ops between `op` and latch index `to`: the rest of `op`'s block, then every body block
|
|
1647
|
+
// it reaches ahead of the latch, then the latch's own ops ahead of `to`.
|
|
1648
|
+
const between = (to: number): Op[] => {
|
|
1649
|
+
if (ob === L.latch) {
|
|
1650
|
+
return L.latch.ops.slice(L.latch.ops.indexOf(op) + 1, to);
|
|
1651
|
+
}
|
|
1652
|
+
const mid = new Set<Block>();
|
|
1653
|
+
const stack = successorsOf(ob);
|
|
1654
|
+
while (stack.length > 0) {
|
|
1655
|
+
const b = stack.pop()!;
|
|
1656
|
+
if (b !== L.header && b !== L.latch && L.body.has(b) && !mid.has(b)) {
|
|
1657
|
+
mid.add(b);
|
|
1658
|
+
stack.push(...successorsOf(b));
|
|
1659
|
+
}
|
|
1660
|
+
}
|
|
1661
|
+
return [
|
|
1662
|
+
...ob.ops.slice(ob.ops.indexOf(op) + 1),
|
|
1663
|
+
...[...mid].flatMap((b) => b.ops),
|
|
1664
|
+
...L.latch.ops.slice(0, to),
|
|
1665
|
+
];
|
|
1666
|
+
};
|
|
1667
|
+
const crossesAt = (home: Op): boolean =>
|
|
1668
|
+
home !== op &&
|
|
1669
|
+
opBlock.get(home) === L.latch &&
|
|
1670
|
+
between(L.latch.ops.indexOf(home)).some((x) => orderSensitive(x) || namedHelper(materialize, x));
|
|
1671
|
+
const exitArg = L.term.successors.some(
|
|
1672
|
+
(sc) =>
|
|
1673
|
+
!L.body.has(sc.block) &&
|
|
1674
|
+
sc.args.some((a) => {
|
|
1675
|
+
const home = defOf.get(a);
|
|
1676
|
+
return !!home && reaches(a) && crossesAt(home);
|
|
1677
|
+
}),
|
|
1678
|
+
);
|
|
1679
|
+
return (
|
|
1680
|
+
exitArg ||
|
|
1681
|
+
L.latch.ops.some(
|
|
1682
|
+
(h) =>
|
|
1683
|
+
h.results[0] !== undefined &&
|
|
1684
|
+
render.consumersOf(h).some((c) => !L.body.has(opBlock.get(c)!)) &&
|
|
1685
|
+
reaches(h.results[0]) &&
|
|
1686
|
+
crossesAt(h),
|
|
1687
|
+
)
|
|
1688
|
+
);
|
|
1689
|
+
});
|
|
1690
|
+
const readAfterUpdate = (L: BottomTestedLoop, r: Value, consumers: Op[]): boolean => {
|
|
1691
|
+
if (!consumers.every((c) => L.body.has(opBlock.get(c)!))) {
|
|
1692
|
+
return true;
|
|
1693
|
+
}
|
|
1694
|
+
const seen = new Set<Value>();
|
|
1695
|
+
const inTest = (x: Value): boolean => {
|
|
1696
|
+
if (x === r) {
|
|
1697
|
+
return true;
|
|
1698
|
+
}
|
|
1699
|
+
if (seen.has(x)) {
|
|
1700
|
+
return false;
|
|
1701
|
+
}
|
|
1702
|
+
seen.add(x);
|
|
1703
|
+
const d = defOf.get(x);
|
|
1704
|
+
return !!d && L.body.has(opBlock.get(d)!) && !materialize.has(d) && d.operands.some(inTest);
|
|
1705
|
+
};
|
|
1706
|
+
return L.term.opcode === 'cond_br' && inTest(L.term.operands[0]);
|
|
1707
|
+
};
|
|
1708
|
+
return {
|
|
1709
|
+
loopBodies,
|
|
1710
|
+
multiBlockHeaders,
|
|
1711
|
+
bottomTested,
|
|
1712
|
+
liveAcrossLoop,
|
|
1713
|
+
readsPreUpdate,
|
|
1714
|
+
escapesAheadOfUpdate,
|
|
1715
|
+
rebuiltPast,
|
|
1716
|
+
readAfterUpdate,
|
|
1717
|
+
};
|
|
1718
|
+
}
|
|
1719
|
+
|
|
1720
|
+
/** The variation scopes and the placement sets the rules read, each settled before the fixpoint:
|
|
1721
|
+
* none reads `materialize`. */
|
|
1722
|
+
interface HomeScopes {
|
|
1305
1723
|
/** Would naming THIS op's own result change its value? Yes when the result is an ADDRESS built
|
|
1306
1724
|
* over a gaddr/laddr: rendered standalone an `&g + i` loses the memAccess's inline byte-stride
|
|
1307
1725
|
* cast, and the cast-aware base machinery in l3/ serves those bases instead. Asked by the rules
|
|
1308
1726
|
* that home an address; a rule homing the SCALAR a load returns is not this case — the name
|
|
1309
1727
|
* holds the loaded value and the address stays inline at the deref, which is why the load rules
|
|
1310
1728
|
* (live-across-a-loop, join feeds, /addr-home's, def-block placement) do not ask it. */
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
// render positions, no `materialize`), so it cannot change as the set grows. Unlike the rules
|
|
1334
|
-
// that stand down without the caller's `dom`, this one computes its own: rank.ts has already
|
|
1335
|
-
// admitted the candidate on `hasMergeFeedHome`, which runs the same scope, so standing down here
|
|
1336
|
-
// would be a refusal nothing reports.
|
|
1337
|
-
let mergeFeedOps = new Set<Op>();
|
|
1338
|
-
if (homeMergeFeeds) {
|
|
1339
|
-
const mdom = dom ?? dominators(fn);
|
|
1340
|
-
const mloops = mdom === dom ? loopBodies : naturalLoops(fn, mdom, predsOf);
|
|
1341
|
-
mergeFeedOps = mergeFeedHomes(fn, mdom, defOf, new Set(mloops.flatMap((L) => [...L.body])));
|
|
1342
|
-
}
|
|
1343
|
-
/** result values the address-home variation materialized — the load rule's admission key */
|
|
1344
|
-
const addressHomedBases = new Set<Value>();
|
|
1729
|
+
addressCone(op0: Op): boolean;
|
|
1730
|
+
/** The address-home variation's scope. Over the MERGE CLASS, so a base the arms derive and the
|
|
1731
|
+
* join dereferences counts as the one register it is — see `sharedBaseClasses`. Computed only
|
|
1732
|
+
* under the variation. */
|
|
1733
|
+
sharedBaseClass: Set<Value>;
|
|
1734
|
+
/** The seventh scope's members, read the same way and for the same reason — one definition, run
|
|
1735
|
+
* under this function's own `returnsVoid` where the gate must ask under both. */
|
|
1736
|
+
escapeHomeOps: Set<Op>;
|
|
1737
|
+
/** The address-home question asked of the VALUE ALONE, which is what the loop-expression and
|
|
1738
|
+
* derived-read scopes need: their "shared bases stay the address-home scope's" exclusion is a
|
|
1739
|
+
* hand-off between scopes, and widening it to the class would make them refuse values the
|
|
1740
|
+
* address-home scope only claims when its own variation is ON — a candidate lost with no
|
|
1741
|
+
* candidate gained. Where both variations run the two scopes may claim one value, which is a
|
|
1742
|
+
* no-op: `materialize` is a set and the address-home scope, running first, is what registers
|
|
1743
|
+
* `addressHomedBases`. */
|
|
1744
|
+
usedOnlyAsSharedBase(v: Value): boolean;
|
|
1745
|
+
/** The merge-feed-home variation's ops: the scope reads the IR alone (no render positions, no
|
|
1746
|
+
* `materialize`), so it cannot change as the set grows. Unlike the rules that stand down without
|
|
1747
|
+
* the caller's `dom`, this one computes its own: rank.ts has already admitted the candidate on
|
|
1748
|
+
* `hasMergeFeedHome`, which runs the same scope, so standing down here would be a refusal nothing
|
|
1749
|
+
* reports. */
|
|
1750
|
+
mergeFeedOps: Set<Op>;
|
|
1345
1751
|
/** The loop-expression-home variation's scope: 2+ distinct consumers of the value, at least one of
|
|
1346
1752
|
* them inside a loop the def's block is outside (loop model = the caller's dominators; absent ⇒
|
|
1347
1753
|
* never).
|
|
@@ -1353,13 +1759,7 @@ export function analyze(fn: Fn, returnsVoid: boolean, opts: AnalyzeOptions = {})
|
|
|
1353
1759
|
* single-use value inlines at its one use with the same bytes either way, so homing it can only
|
|
1354
1760
|
* add a copy. Values consumed only OUTSIDE any loop are the straight-line class `/derived-home`
|
|
1355
1761
|
* serves on its own evidence. */
|
|
1356
|
-
|
|
1357
|
-
const consumers = [...new Set((useSitesOf.get(v) ?? []).map((s) => s.op))];
|
|
1358
|
-
return (
|
|
1359
|
-
consumers.length >= 2 &&
|
|
1360
|
-
loopBodies.some((L) => !L.body.has(defBlk) && consumers.some((c) => L.body.has(opBlock.get(c)!)))
|
|
1361
|
-
);
|
|
1362
|
-
};
|
|
1762
|
+
loopSharedConsumers(v: Value, defBlk: Block): boolean;
|
|
1363
1763
|
/** The derived-read-home variation's scope: does `op0` stand on a memory READ that may render at
|
|
1364
1764
|
* `op0`'s own position?
|
|
1365
1765
|
*
|
|
@@ -1387,7 +1787,7 @@ export function analyze(fn: Fn, returnsVoid: boolean, opts: AnalyzeOptions = {})
|
|
|
1387
1787
|
* And each read's value must go NOWHERE BUT the cone: exactly one use site. Homing resolves a
|
|
1388
1788
|
* render position for a read that had none, so a SECOND use resolves a second one, and the
|
|
1389
1789
|
* multi-render load rule then inlines the read at BOTH — two accesses where the asm has one
|
|
1390
|
-
* `ldrh`, which for a volatile cell is precisely the duplication `
|
|
1790
|
+
* `ldrh`, which for a volatile cell is precisely the duplication the `declared` placement refuses. Two
|
|
1391
1791
|
* homed values over one read is the same shape from the other side (each is the other's second
|
|
1392
1792
|
* use), so one test covers both. This is what makes "renders once, inside the home" a property
|
|
1393
1793
|
* rather than an aspiration: without it the variation silently doubles a hardware read.
|
|
@@ -1399,39 +1799,37 @@ export function analyze(fn: Fn, returnsVoid: boolean, opts: AnalyzeOptions = {})
|
|
|
1399
1799
|
* run, a value inside a loop pulls it in to run per iteration. Neither is a write, so no
|
|
1400
1800
|
* barrier sees either; for an ordinary cell they are worse spellings, and for a volatile one
|
|
1401
1801
|
* they are a missing access and a duplicated one. */
|
|
1402
|
-
|
|
1403
|
-
const reads = readCone(op0, defOf);
|
|
1404
|
-
if (reads === null) {
|
|
1405
|
-
return false;
|
|
1406
|
-
}
|
|
1407
|
-
const at = { blk, idx: opIndex.get(op0)! };
|
|
1408
|
-
const coneReads = new Set(reads);
|
|
1409
|
-
const bars = (x: Op): boolean =>
|
|
1410
|
-
EFFECTFUL_OPS.has(x.opcode) || ((x.opcode === 'load' || x.opcode === 'aload') && !coneReads.has(x));
|
|
1411
|
-
return (
|
|
1412
|
-
reads.length > 0 &&
|
|
1413
|
-
reads.every(
|
|
1414
|
-
(r) =>
|
|
1415
|
-
opBlock.get(r) === blk && (useSitesOf.get(r.results[0])?.length ?? 0) === 1 && !memWriteBetween(r, at, bars),
|
|
1416
|
-
)
|
|
1417
|
-
);
|
|
1418
|
-
};
|
|
1802
|
+
standsOnMovableRead(op0: Op, blk: Block): boolean;
|
|
1419
1803
|
/** 2+ distinct consuming ops — the multi-use the pure-op rule reads as a reused register. */
|
|
1420
|
-
|
|
1421
|
-
/**
|
|
1422
|
-
*
|
|
1804
|
+
multiConsumer(v: Value): boolean;
|
|
1805
|
+
/** Values C evaluates only under a `&&`/`||` — the SECOND operand of every
|
|
1806
|
+
* `logic_and`/`logic_or`, and everything it reads. Two rules read it and they read it the
|
|
1807
|
+
* opposite way round, because the fold that built the connective treats a READ and an EFFECT
|
|
1808
|
+
* differently.
|
|
1423
1809
|
*
|
|
1424
1810
|
* raise/shortcircuit.ts recovers a connective by hoisting the guarded arm's whole pure body,
|
|
1425
1811
|
* memory reads included, into the block ABOVE the branch (its value form and its control-flow
|
|
1426
|
-
* form both splice that body into the head). ir/
|
|
1427
|
-
* reason a read is
|
|
1428
|
-
* into the `&&`/`||` right-hand side, where C's own short circuit re-guards it. So for a
|
|
1429
|
-
*
|
|
1430
|
-
*
|
|
1431
|
-
*
|
|
1812
|
+
* form both splice that body into the head). ir/discipline.ts states the safety argument as the
|
|
1813
|
+
* reason a plain read is not `speculationUnsafe`: the structurer inlines it back
|
|
1814
|
+
* into the `&&`/`||` right-hand side, where C's own short circuit re-guards it. So for a READ
|
|
1815
|
+
* the def block is a FOLD ARTIFACT rather than the block the asm read in, and the def-block
|
|
1816
|
+
* placement rule stands down. Naming it also breaks the re-guard: `p != 0 && *p != 0` would
|
|
1817
|
+
* emit `v0 = *p;` above its own null check. That argument is about which SPELLING matches, and
|
|
1818
|
+
* a plain read is the only one it covers.
|
|
1432
1819
|
*
|
|
1433
|
-
*
|
|
1434
|
-
|
|
1820
|
+
* A COUNTED op — a call, a qualified read — is `speculationUnsafe`, so no fold ever lifted one
|
|
1821
|
+
* out of the arm it guards: one that reached this cone ran ABOVE the branch, unconditionally, and
|
|
1822
|
+
* the guarded-call rule materializes it there rather than letting C's short circuit skip it.
|
|
1823
|
+
* agbcc emits `int t = gVolReg; if (a > 0 && t != 0)` with the `ldrh` above the `cmp`, and
|
|
1824
|
+
* `if (a > 0 && gVolReg != 0)` with it past the `ble`, where the fold leaves it in its arm and
|
|
1825
|
+
* no connective is built. The same holds of an op
|
|
1826
|
+
* the asm reached by calling a runtime helper and the fold did not hoist — one it hoists loses
|
|
1827
|
+
* the stamp — and the helper clause names it by the same argument.
|
|
1828
|
+
*
|
|
1829
|
+
* Only the guarded side is SEEDED. A connective's own operand[0] is evaluated whenever the
|
|
1830
|
+
* connective is, so neither rule wants it — but an inner connective sitting under an outer guard
|
|
1831
|
+
* is reached through the outer's cone, operand[0] included, which is what C does with it. */
|
|
1832
|
+
shortCircuitGuarded: Set<Value>;
|
|
1435
1833
|
/** THE def-block placement rule's copy refusal: is every use of the value a successor ARGUMENT,
|
|
1436
1834
|
* i.e. is the value nothing but a block parameter's incoming copy?
|
|
1437
1835
|
*
|
|
@@ -1446,11 +1844,72 @@ export function analyze(fn: Fn, returnsVoid: boolean, opts: AnalyzeOptions = {})
|
|
|
1446
1844
|
* Note what this does NOT claim: those reads really do sit above the loop guard in the asm, so
|
|
1447
1845
|
* the placement inference was right and the SPELLING is what fails. Seating the read above the
|
|
1448
1846
|
* guard AND as the loop variable is loop-init hoisting, a capability this rule does not have. */
|
|
1847
|
+
onlyFeedsBlockParams(v: Value): boolean;
|
|
1848
|
+
}
|
|
1849
|
+
|
|
1850
|
+
function homeScopes(a: {
|
|
1851
|
+
fn: Fn;
|
|
1852
|
+
returnsVoid: boolean;
|
|
1853
|
+
opts: AnalyzeOptions;
|
|
1854
|
+
uses: UseIndex;
|
|
1855
|
+
defOf: Map<Value, Op>;
|
|
1856
|
+
predsOf: Map<Block, Block[]>;
|
|
1857
|
+
loops: LoopRules;
|
|
1858
|
+
memWriteBetween: StructureAnalysis['memWriteBetween'];
|
|
1859
|
+
}): HomeScopes {
|
|
1860
|
+
const { fn, returnsVoid, opts, defOf, predsOf, loops, memWriteBetween } = a;
|
|
1861
|
+
const { useSitesOf, opIndex, opBlock } = a.uses;
|
|
1862
|
+
const { dom } = opts;
|
|
1863
|
+
const addressCone = (op0: Op): boolean => coneHoldsAddr(op0, defOf);
|
|
1864
|
+
const sharedBaseClass = opts.homeSharedAddresses ? sharedBaseClasses(fn, returnsVoid) : new Set<Value>();
|
|
1865
|
+
const escapeHomeOps = opts.homeEscapingExtensions ? escapingExtensions(fn, returnsVoid) : new Set<Op>();
|
|
1866
|
+
const usedOnlyAsSharedBase = (v: Value): boolean => {
|
|
1867
|
+
const sites = useSitesOf.get(v) ?? [];
|
|
1868
|
+
const consumers = new Set(sites.map((s) => s.op));
|
|
1869
|
+
return (
|
|
1870
|
+
consumers.size >= 2 &&
|
|
1871
|
+
[...consumers].every(
|
|
1872
|
+
(c) => MEM_BASE_OPS.has(c.opcode) && c.operands[0] === v && !c.operands.some((o, i) => i > 0 && o === v),
|
|
1873
|
+
)
|
|
1874
|
+
);
|
|
1875
|
+
};
|
|
1876
|
+
let mergeFeedOps = new Set<Op>();
|
|
1877
|
+
if (opts.homeMergeFeeds) {
|
|
1878
|
+
const mdom = dom ?? dominators(fn);
|
|
1879
|
+
const mloops = mdom === dom ? loops.loopBodies : naturalLoops(fn, mdom, predsOf);
|
|
1880
|
+
mergeFeedOps = mergeFeedHomes(fn, mdom, defOf, new Set(mloops.flatMap((L) => [...L.body])));
|
|
1881
|
+
}
|
|
1882
|
+
const loopSharedConsumers = (v: Value, defBlk: Block): boolean => {
|
|
1883
|
+
const consumers = [...new Set((useSitesOf.get(v) ?? []).map((s) => s.op))];
|
|
1884
|
+
return (
|
|
1885
|
+
consumers.length >= 2 &&
|
|
1886
|
+
loops.loopBodies.some((L) => !L.body.has(defBlk) && consumers.some((c) => L.body.has(opBlock.get(c)!)))
|
|
1887
|
+
);
|
|
1888
|
+
};
|
|
1889
|
+
const standsOnMovableRead = (op0: Op, blk: Block): boolean => {
|
|
1890
|
+
const reads = readCone(op0, defOf);
|
|
1891
|
+
if (reads === null) {
|
|
1892
|
+
return false;
|
|
1893
|
+
}
|
|
1894
|
+
const at = { blk, idx: opIndex.get(op0)! };
|
|
1895
|
+
const coneReads = new Set(reads);
|
|
1896
|
+
const bars = (x: Op): boolean =>
|
|
1897
|
+
effectful(x) || ((x.opcode === 'load' || x.opcode === 'aload') && !coneReads.has(x));
|
|
1898
|
+
return (
|
|
1899
|
+
reads.length > 0 &&
|
|
1900
|
+
reads.every(
|
|
1901
|
+
(r) =>
|
|
1902
|
+
opBlock.get(r) === blk && (useSitesOf.get(r.results[0])?.length ?? 0) === 1 && !memWriteBetween(r, at, bars),
|
|
1903
|
+
)
|
|
1904
|
+
);
|
|
1905
|
+
};
|
|
1906
|
+
const multiConsumer = (v: Value): boolean => new Set((useSitesOf.get(v) ?? []).map((s) => s.op)).size >= 2;
|
|
1907
|
+
const shortCircuitGuarded = shortCircuitGuardedValues(fn, defOf);
|
|
1449
1908
|
const argUsedValues = new Set<Value>();
|
|
1450
1909
|
const operandUsedValues = new Set<Value>();
|
|
1451
|
-
// Both sets serve
|
|
1452
|
-
// `condBrArgFed` takes
|
|
1453
|
-
if (readsStayWhereWritten) {
|
|
1910
|
+
// Both sets serve the copy refusal alone, so they are built only where it can fire — the same
|
|
1911
|
+
// posture `condBrArgFed` takes (every other target pays nothing for a behavior it never declares).
|
|
1912
|
+
if (opts.readsStayWhereWritten) {
|
|
1454
1913
|
for (const b of fn.blocks) {
|
|
1455
1914
|
for (const op of b.ops) {
|
|
1456
1915
|
for (const v of op.operands) {
|
|
@@ -1465,15 +1924,605 @@ export function analyze(fn: Fn, returnsVoid: boolean, opts: AnalyzeOptions = {})
|
|
|
1465
1924
|
}
|
|
1466
1925
|
}
|
|
1467
1926
|
const onlyFeedsBlockParams = (v: Value): boolean => argUsedValues.has(v) && !operandUsedValues.has(v);
|
|
1927
|
+
return {
|
|
1928
|
+
addressCone,
|
|
1929
|
+
sharedBaseClass,
|
|
1930
|
+
escapeHomeOps,
|
|
1931
|
+
usedOnlyAsSharedBase,
|
|
1932
|
+
mergeFeedOps,
|
|
1933
|
+
loopSharedConsumers,
|
|
1934
|
+
standsOnMovableRead,
|
|
1935
|
+
multiConsumer,
|
|
1936
|
+
shortCircuitGuarded,
|
|
1937
|
+
onlyFeedsBlockParams,
|
|
1938
|
+
};
|
|
1939
|
+
}
|
|
1940
|
+
|
|
1941
|
+
/** Every stage output the materialization rules read. */
|
|
1942
|
+
interface MaterializeRules {
|
|
1943
|
+
opts: AnalyzeOptions;
|
|
1944
|
+
uses: UseIndex;
|
|
1945
|
+
edges: EdgeArgUses;
|
|
1946
|
+
render: RenderModel;
|
|
1947
|
+
reach: InlineReach;
|
|
1948
|
+
loops: LoopRules;
|
|
1949
|
+
scopes: HomeScopes;
|
|
1950
|
+
defOf: Map<Value, Op>;
|
|
1951
|
+
predsOf: Map<Block, Block[]>;
|
|
1952
|
+
memWriteBetween: StructureAnalysis['memWriteBetween'];
|
|
1953
|
+
copyInterdependent: Set<Value>;
|
|
1954
|
+
}
|
|
1955
|
+
|
|
1956
|
+
/** The rules for a PURE value-producing op (a constant, an address computation, arithmetic — NOT a
|
|
1957
|
+
* memory access), in order. The helper clause and the exit-divide rule end the walk when they name
|
|
1958
|
+
* the op; every rule after them runs whatever an earlier one decided, so the address-home scope
|
|
1959
|
+
* registers `addressHomedBases` even for an op a rule ahead of it already named.
|
|
1960
|
+
*
|
|
1961
|
+
* A value with ≥2 distinct-STATEMENT uses in the SSA is one the compiler kept in a register and
|
|
1962
|
+
* reused: the frontend never dedups, so multi-use exists ONLY because the asm loaded/computed the
|
|
1963
|
+
* value once and read the same register again. Inlining it re-derives the value at each use (a
|
|
1964
|
+
* fresh pool load / repeated address arithmetic) — which the compiler did NOT do — so materialize
|
|
1965
|
+
* it into a local instead, reproducing that register. Pure ⇒ every render is value-identical, so
|
|
1966
|
+
* (unlike a load) no intervening memory write can invalidate a later render: multi-consumer
|
|
1967
|
+
* suffices, no barrier scan. Scope: a `const` that is LIVE ACROSS A CALL. A value the compiler
|
|
1968
|
+
* needs after a call must survive in a CALLEE-SAVED register — i.e. a local — because the call
|
|
1969
|
+
* clobbers the caller-saved ones; the compiler therefore loads it ONCE and keeps it, exactly what
|
|
1970
|
+
* materializing into a local reproduces (the base of `((s32 *)C)[i]` reused across `foo(...)`
|
|
1971
|
+
* calls). WITHOUT a call in its live range the const is instead cheaply re-materialized at each
|
|
1972
|
+
* use (a bare `movs r, #0` per init), so materializing it would ADD pointless copies and MISS —
|
|
1973
|
+
* hence the call gate (the small-constant regression). Cheap deref casts still land on the `index`
|
|
1974
|
+
* node at the use, preserving byte strides. Second scope: a NON-const read by a sibling
|
|
1975
|
+
* parallel-copy arg (`copyInterdependent`) — the one place a pure value's inlining is not
|
|
1976
|
+
* value-identical rendering but a re-derivation the copy machinery is forced into. Address cones
|
|
1977
|
+
* are excluded from it (an `&g + i` rendered standalone loses the memAccess's inline `(u8 *)` cast
|
|
1978
|
+
* — cast-aware base materialization is separate), and consts are not in it at all: a re-derived
|
|
1979
|
+
* const is re-materialization, which is the compiler's own behavior. */
|
|
1980
|
+
function namePureOp(rules: MaterializeRules, state: MaterializeState, op: Op, b: Block, escapePhase: boolean): void {
|
|
1981
|
+
const { opts, uses, render, reach, loops, scopes, memWriteBetween, copyInterdependent } = rules;
|
|
1982
|
+
const { useSitesOf } = uses;
|
|
1983
|
+
const { materialize } = state;
|
|
1984
|
+
const pr = op.results[0];
|
|
1985
|
+
// AN OP THE ASM CALLED A HELPER FOR is placed as the call it was (`isHelper`: `bl
|
|
1986
|
+
// __divsi3` for `k / n` and `k / 5` on agbcc, `bl __ashrdi3` for a 64-bit `>>`, `bl
|
|
1987
|
+
// __div2i` for a 64-bit `/` on mwcc). A call runs once, where the asm ran it, and agbcc,
|
|
1988
|
+
// with no scheduler, EMITS the call where the source spells the operation, so naming it
|
|
1989
|
+
// where the asm ran it recompiles to that asm; inlined at its use it moves there. One
|
|
1990
|
+
// direction only, as target.ts `readsStayWhereWritten` says of reads: the asm's position
|
|
1991
|
+
// is not always where the source computed it — loop.c hoists an invariant `bl __divsi3`
|
|
1992
|
+
// into the loop's guarded preheader, and naming it there is what recompiles. So it is
|
|
1993
|
+
// named at its def when:
|
|
1994
|
+
// • an effect, or another helper named there, lies between the def and a place it
|
|
1995
|
+
// renders — `t = k / n; *q = n; return t + 1;` is `bl; str`, and inlined it comes
|
|
1996
|
+
// back `*q = n; return k / n + 1;`, `str; bl`. A named one bars what the barrier
|
|
1997
|
+
// scan in `nameAccess` places, as a named call does, so a call the asm ran ahead of it
|
|
1998
|
+
// stays ahead of it;
|
|
1999
|
+
// • it renders in a block other than its own, or rides a branch's edge copy
|
|
2000
|
+
// (`ridesEdge`): then it runs on the paths that render it, not where the asm ran it
|
|
2001
|
+
// once. `t = k / n; if (c) return t + 1; return t - 3;` is one `bl`
|
|
2002
|
+
// above the `cmp`, and inlined into both arms it recompiles to one per arm;
|
|
2003
|
+
// • it sits in a `&&`/`||` guarded cone. raise/shortcircuit.ts drops the stamp from a
|
|
2004
|
+
// helper op it hoists out of the arm it guards (`forgetHelperPlacement`), so one that
|
|
2005
|
+
// still carries it ran above the branch, and C's short circuit would skip it (the
|
|
2006
|
+
// guarded-call rule in `nameAccess`).
|
|
2007
|
+
// A divide the ISA computes is none of these: kmc, IDO and mwcc compile `t = k / n; *q
|
|
2008
|
+
// = n;` and `*q = n; … k / n` to the same object, so naming it buys no byte. The rule
|
|
2009
|
+
// after this one names it only where the loop's exit value needs the name.
|
|
2010
|
+
if (isHelper(op) && pr && useSitesOf.has(pr)) {
|
|
2011
|
+
const at = render.emitPositions(op);
|
|
2012
|
+
if (
|
|
2013
|
+
!at ||
|
|
2014
|
+
scopes.shortCircuitGuarded.has(pr) ||
|
|
2015
|
+
reach.ridesEdge(op) ||
|
|
2016
|
+
at.some((p) => p.blk !== b) ||
|
|
2017
|
+
at.some((p) => memWriteBetween(op, p, (x) => effectful(x) || namedHelper(materialize, x)))
|
|
2018
|
+
) {
|
|
2019
|
+
materialize.add(op);
|
|
2020
|
+
return;
|
|
2021
|
+
}
|
|
2022
|
+
}
|
|
2023
|
+
// A DIVIDE THE ISA COMPUTES, in a value read past the loop's update that reads a loop
|
|
2024
|
+
// variable, is named at its def when that value would be rebuilt behind a memory access
|
|
2025
|
+
// or an effect the asm ran after it (`rebuiltPast`) — once every other rule has settled
|
|
2026
|
+
// (`escapePhase`), as the escape rule is. kmc's `do { t = k / n; *q = n; r = t + 1; }
|
|
2027
|
+
// while (--n);` is `div; …; sw; …; addiu v0, v1, 1`. Unnamed, the loop declines —
|
|
2028
|
+
// `arg-safe-to-reevaluate` (hazards.ts) refuses the sink's move, and a read after the loop
|
|
2029
|
+
// re-reads the updated counter — or, where the back edge carries the value too, the
|
|
2030
|
+
// update copy rebuilds the divide behind the store. Named, the value reads the name and
|
|
2031
|
+
// the divide stays where the asm ran it.
|
|
2032
|
+
if (escapePhase && opSig(op.opcode)?.traps && pr && useSitesOf.has(pr) && loops.rebuiltPast(op, pr)) {
|
|
2033
|
+
materialize.add(op);
|
|
2034
|
+
state.exitDivides.add(op);
|
|
2035
|
+
return;
|
|
2036
|
+
}
|
|
2037
|
+
if (op.opcode === 'const' && pr && useSitesOf.has(pr)) {
|
|
2038
|
+
const cons = [...new Set((useSitesOf.get(pr) ?? []).map((s) => s.op))];
|
|
2039
|
+
if (cons.length > 1 && uses.liveAcrossCall(op, cons)) {
|
|
2040
|
+
materialize.add(op);
|
|
2041
|
+
}
|
|
2042
|
+
} else if (op.opcode !== 'const' && pr && copyInterdependent.has(pr) && !scopes.addressCone(op)) {
|
|
2043
|
+
materialize.add(op);
|
|
2044
|
+
}
|
|
2045
|
+
// A float PRODUCT read by a float add or subtract, directly or through a negation, is named
|
|
2046
|
+
// on a compiler that contracts (target.ts `contractsFloatProducts`): it fuses only WITHIN
|
|
2047
|
+
// one expression, so the inline `a * b + c` recompiles to one `fmadds` — one rounding, a
|
|
2048
|
+
// different value — while `t = a * b; t + c` is the unfused pair the object holds.
|
|
2049
|
+
// KNOWN GAP: not at a multi-block loop header, the seat the scopes below refuse too.
|
|
2050
|
+
if (
|
|
2051
|
+
opts.contractsFloatProducts &&
|
|
2052
|
+
op.opcode === 'fmul' &&
|
|
2053
|
+
render.feedsFloatAdd(op) &&
|
|
2054
|
+
!loops.multiBlockHeaders.has(b)
|
|
2055
|
+
) {
|
|
2056
|
+
materialize.add(op);
|
|
2057
|
+
}
|
|
2058
|
+
// Folding the FIVE VARIATION scopes below into one predicate-parameterized scope is BOOKED
|
|
2059
|
+
// in docs/level-tower.md and deliberately unpaid; what it cannot absorb is named there,
|
|
2060
|
+
// along with the gate duplication that is its price.
|
|
2061
|
+
// Third scope, under the address-home variation only (see AnalyzeOptions.homeSharedAddresses):
|
|
2062
|
+
// a non-const pure value whose MERGE CLASS is used only as the base of 2+ memory
|
|
2063
|
+
// accesses.
|
|
2064
|
+
if (
|
|
2065
|
+
opts.homeSharedAddresses &&
|
|
2066
|
+
op.opcode !== 'const' &&
|
|
2067
|
+
pr &&
|
|
2068
|
+
scopes.sharedBaseClass.has(pr) &&
|
|
2069
|
+
!scopes.addressCone(op) &&
|
|
2070
|
+
!loops.multiBlockHeaders.has(b)
|
|
2071
|
+
) {
|
|
2072
|
+
materialize.add(op);
|
|
2073
|
+
state.addressHomedBases.add(pr);
|
|
2074
|
+
}
|
|
2075
|
+
// Fourth scope, under the loop-expression-home variation (AnalyzeOptions.homeLoopExprs): a
|
|
2076
|
+
// pure non-const value with 2+ distinct consumers, at least one of them inside a loop the
|
|
2077
|
+
// def sits outside. Shared bases stay the previous scope's (its load rule needs the
|
|
2078
|
+
// registration).
|
|
2079
|
+
if (
|
|
2080
|
+
opts.homeLoopExprs &&
|
|
2081
|
+
op.opcode !== 'const' &&
|
|
2082
|
+
pr &&
|
|
2083
|
+
!scopes.usedOnlyAsSharedBase(pr) &&
|
|
2084
|
+
scopes.loopSharedConsumers(pr, b) &&
|
|
2085
|
+
!scopes.addressCone(op) &&
|
|
2086
|
+
!loops.multiBlockHeaders.has(b)
|
|
2087
|
+
) {
|
|
2088
|
+
materialize.add(op);
|
|
2089
|
+
}
|
|
2090
|
+
// Fifth scope, under the derived-read-home variation (AnalyzeOptions.homeDerivedReads): a
|
|
2091
|
+
// pure non-const value with 2+ consumers standing on a memory read. Shared bases stay
|
|
2092
|
+
// the third scope's and values consumed across a loop the fourth's; what this one adds
|
|
2093
|
+
// is the straight-line case, which neither reaches.
|
|
2094
|
+
if (
|
|
2095
|
+
opts.homeDerivedReads &&
|
|
2096
|
+
op.opcode !== 'const' &&
|
|
2097
|
+
pr &&
|
|
2098
|
+
!scopes.usedOnlyAsSharedBase(pr) &&
|
|
2099
|
+
!rendersAsAddress(op) &&
|
|
2100
|
+
scopes.multiConsumer(pr) &&
|
|
2101
|
+
scopes.standsOnMovableRead(op, b) &&
|
|
2102
|
+
!loops.multiBlockHeaders.has(b)
|
|
2103
|
+
) {
|
|
2104
|
+
materialize.add(op);
|
|
2105
|
+
}
|
|
2106
|
+
// Sixth scope, under the merge-feed-home variation (AnalyzeOptions.homeMergeFeeds) —
|
|
2107
|
+
// `mergeFeedHomes` above. The only one of the FIVE VARIATION scopes that admits a `const`; the
|
|
2108
|
+
// other const clientele is the first scope, a const live across a call. For the sibling
|
|
2109
|
+
// variations a re-derived const is re-materialization, the compiler's own behavior, while a
|
|
2110
|
+
// const the arms of a branch merge is one it held in a register across them
|
|
2111
|
+
// (`mov r5, #0` once, not per arm).
|
|
2112
|
+
if (opts.homeMergeFeeds && scopes.mergeFeedOps.has(op)) {
|
|
2113
|
+
materialize.add(op);
|
|
2114
|
+
}
|
|
2115
|
+
// Seventh scope, under the escaping-extension-home variation
|
|
2116
|
+
// (AnalyzeOptions.homeEscapingExtensions) — `escapingExtensions` above, which the
|
|
2117
|
+
// enumeration gate also runs. The siblings all admit a value the def block also consumes;
|
|
2118
|
+
// this one admits only the value that left, which is the register the asm carried across
|
|
2119
|
+
// the boundary. The seat refusal is the rule's, not the scope's — and unlike the third
|
|
2120
|
+
// scope's, which `sharedBaseClasses` prices with a named row, it has NO structurable
|
|
2121
|
+
// inhabitant here: four constructions of a header defining a multi-consumer escaping
|
|
2122
|
+
// value decline before any variation is consulted, `xor` included
|
|
2123
|
+
// (`escape-home.test.ts`). It stands because the seat is a property of the block.
|
|
2124
|
+
if (opts.homeEscapingExtensions && scopes.escapeHomeOps.has(op) && !loops.multiBlockHeaders.has(b)) {
|
|
2125
|
+
materialize.add(op);
|
|
2126
|
+
}
|
|
2127
|
+
}
|
|
2128
|
+
|
|
2129
|
+
/** A read or call the barrier scan places at its one render position. */
|
|
2130
|
+
interface PlacedAccess {
|
|
2131
|
+
op: Op;
|
|
2132
|
+
/** `counted(op)`: it EXECUTES at each render */
|
|
2133
|
+
once: boolean;
|
|
2134
|
+
sites: UseSite[];
|
|
2135
|
+
pos: { blk: Block; idx: number };
|
|
2136
|
+
/** which part of its anchor the op renders in (`partOf`) */
|
|
2137
|
+
ownPart: string | null;
|
|
2138
|
+
/** under the value-home variation, the writes that may reach the global cell the op reads */
|
|
2139
|
+
barsThisRead: ((x: Op) => boolean) | null;
|
|
2140
|
+
}
|
|
2141
|
+
|
|
2142
|
+
/** Which part of its anchor `x` renders in: '' when the anchor is not a terminator, null when it
|
|
2143
|
+
* has no one part. A terminator renders in parts — its own operands, then one copy per edge
|
|
2144
|
+
* argument, each a statement of its own. */
|
|
2145
|
+
function partOf(render: RenderModel, x: Op): string | null {
|
|
2146
|
+
for (let cur = x; ;) {
|
|
2147
|
+
const cons = render.consumersOf(cur);
|
|
2148
|
+
if (cons.length !== 1) {
|
|
2149
|
+
return null;
|
|
2150
|
+
}
|
|
2151
|
+
const [c] = cons;
|
|
2152
|
+
if (c.successors.length > 0) {
|
|
2153
|
+
const v = cur.results[0];
|
|
2154
|
+
const parts = [
|
|
2155
|
+
...c.operands.flatMap((o, i) => (o === v ? [`op${i}`] : [])),
|
|
2156
|
+
...c.successors.flatMap((sc, si) => sc.args.flatMap((a, ai) => (a === v ? [`${si}:${ai}`] : []))),
|
|
2157
|
+
];
|
|
2158
|
+
return parts.length === 1 ? parts[0] : null;
|
|
2159
|
+
}
|
|
2160
|
+
if (render.anchored(c)) {
|
|
2161
|
+
return '';
|
|
2162
|
+
}
|
|
2163
|
+
cur = c;
|
|
2164
|
+
}
|
|
2165
|
+
}
|
|
2166
|
+
|
|
2167
|
+
/** Does `x` render in the same statement as the placed access: the same anchor, and the same part
|
|
2168
|
+
* of it? */
|
|
2169
|
+
function sameStatement(render: RenderModel, access: PlacedAccess, x: Op): boolean {
|
|
2170
|
+
const q = render.emitPos(x);
|
|
2171
|
+
return (
|
|
2172
|
+
q !== null &&
|
|
2173
|
+
q.blk === access.pos.blk &&
|
|
2174
|
+
q.idx === access.pos.idx &&
|
|
2175
|
+
access.ownPart !== null &&
|
|
2176
|
+
partOf(render, x) === access.ownPart
|
|
2177
|
+
);
|
|
2178
|
+
}
|
|
2179
|
+
|
|
2180
|
+
/** Is every use of the placed access inside `call`'s arguments, as rendered? */
|
|
2181
|
+
function onlyFeedsCall(defOf: Map<Value, Op>, materialize: ReadonlySet<Op>, access: PlacedAccess, call: Op): boolean {
|
|
2182
|
+
const cone = new Set<Op>([call]);
|
|
2183
|
+
const walk = (v: Value): void => {
|
|
2184
|
+
const d = defOf.get(v);
|
|
2185
|
+
if (d !== undefined && d !== access.op && !cone.has(d) && !materialize.has(d)) {
|
|
2186
|
+
cone.add(d);
|
|
2187
|
+
d.operands.forEach(walk);
|
|
2188
|
+
}
|
|
2189
|
+
};
|
|
2190
|
+
call.operands.forEach(walk);
|
|
2191
|
+
return access.sites.every((u) => cone.has(u.op));
|
|
2192
|
+
}
|
|
2193
|
+
|
|
2194
|
+
/** A between-op is a BARRIER when it renders as a sequenced statement the def would cross:
|
|
2195
|
+
* stores/opaque always; a call/load that is dead (statement), materialized (statement), or
|
|
2196
|
+
* inlined into a DIFFERENT statement. Loads never bar a load (reads don't conflict).
|
|
2197
|
+
*
|
|
2198
|
+
* Inside ONE statement the order is the compiler's, and every compiler the corpus builds
|
|
2199
|
+
* with evaluates a call before the memory reads beside it — agbcc, IDO 7.1, both gcc 2.7.2
|
|
2200
|
+
* builds and mwcc all compile `*p + cb(p)`, `cb(p) - *p`, `(*p ^ 3) * cb(p)` and `*p <<
|
|
2201
|
+
* cb(p)` to the call, then the load. So a CALL ahead of a read it shares a statement with
|
|
2202
|
+
* is the order the recompile gives back, and bars nothing. A READ ahead of such a call is
|
|
2203
|
+
* the order no single expression gives back — `int t = *p; return t + cb(p);` compiles
|
|
2204
|
+
* `ldr; bl; add`, and inlined as `*p + cb(p)` it recompiles `bl; ldr` — so that call bars
|
|
2205
|
+
* the read, which is named where it ran. Unless the read is only the call's argument,
|
|
2206
|
+
* which every compiler evaluates first; read a second time beside the call (`G & cg(G)`
|
|
2207
|
+
* for one `ldr`), that second read comes back after it.
|
|
2208
|
+
*
|
|
2209
|
+
* TWO CALLS in one statement have no such order: agbcc calls `cg(k) - cb(p)` in operand
|
|
2210
|
+
* order and mwcc in its own, so `bl cb; bl cg` inlined as that comes back reversed. A call
|
|
2211
|
+
* ahead of another is named too, unless its value is only the later call's argument.
|
|
2212
|
+
*
|
|
2213
|
+
* AND A TERMINATOR IS NOT ONE STATEMENT. It renders in parts (`partOf`), so a read in one edge
|
|
2214
|
+
* copy and a call in its sibling are sequenced, whatever order the copies come out in: agbcc's
|
|
2215
|
+
* `bl cb; ldr r1, [r5]` into a merge came back `a1 = *a0; a2 = cb(a0) + a2;`. Two ops share a
|
|
2216
|
+
* statement only when they render in the same part. */
|
|
2217
|
+
function isBarrier(rules: MaterializeRules, materialize: ReadonlySet<Op>, access: PlacedAccess, x: Op): boolean {
|
|
2218
|
+
const { op, once, barsThisRead } = access;
|
|
2219
|
+
const isCall = op.opcode === 'call';
|
|
2220
|
+
// Value-home variation: a store/astore this read is PROVABLY disjoint from (a different named
|
|
2221
|
+
// global) does not sequence against it, so the read may still render at its use.
|
|
2222
|
+
if (barsThisRead && (x.opcode === 'store' || x.opcode === 'astore') && !barsThisRead(x)) {
|
|
2223
|
+
return false;
|
|
2224
|
+
}
|
|
2225
|
+
if (x.opcode === 'store') {
|
|
2226
|
+
// A store to a PROVABLY-DISJOINT slot of the same base never aliases the load
|
|
2227
|
+
// (`disjointConstSlots`, ir/alias.ts). Anything less certain bars — and so does every
|
|
2228
|
+
// store to a qualified read: a device register answers by when it is read, not only by
|
|
2229
|
+
// which bytes were last written (REG_IF read after the REG_IE write it preceded).
|
|
2230
|
+
if (!once && op.opcode === 'load' && disjointConstSlots(op, x)) {
|
|
2231
|
+
return false;
|
|
2232
|
+
}
|
|
2233
|
+
return true;
|
|
2234
|
+
}
|
|
2235
|
+
if (x.opcode === 'astore' || x.opcode === 'opaque' || namedHelper(materialize, x)) {
|
|
2236
|
+
return true;
|
|
2237
|
+
}
|
|
2238
|
+
if (x.opcode === 'call') {
|
|
2239
|
+
return (
|
|
2240
|
+
!x.results.length ||
|
|
2241
|
+
!rules.uses.useSitesOf.has(x.results[0]) ||
|
|
2242
|
+
materialize.has(x) ||
|
|
2243
|
+
!sameStatement(rules.render, access, x) ||
|
|
2244
|
+
!onlyFeedsCall(rules.defOf, materialize, access, x)
|
|
2245
|
+
);
|
|
2246
|
+
}
|
|
2247
|
+
// Two qualified reads are two accesses in an order. Rendered in one expression, the
|
|
2248
|
+
// order is the compiler's to choose (`gY = VCOUNT - TM0CNT_L` reads VCOUNT first); a
|
|
2249
|
+
// qualified read spelled as a bare statement because nothing uses it is an access too.
|
|
2250
|
+
if (once && !isCall && (x.opcode === 'load' || x.opcode === 'aload') && counted(x)) {
|
|
2251
|
+
return true;
|
|
2252
|
+
}
|
|
2253
|
+
if (!isCall) {
|
|
2254
|
+
return false;
|
|
2255
|
+
} // a plain load never bars a load
|
|
2256
|
+
if (x.opcode === 'load' || x.opcode === 'aload') {
|
|
2257
|
+
return !x.results.length || !rules.uses.useSitesOf.has(x.results[0])
|
|
2258
|
+
? false // dead load: never emitted at all
|
|
2259
|
+
: materialize.has(x) || !sameStatement(rules.render, access, x);
|
|
2260
|
+
}
|
|
2261
|
+
return false;
|
|
2262
|
+
}
|
|
2263
|
+
|
|
2264
|
+
/** The rules for a memory access — a `call`, `load` or `aload` — in order; the first that names
|
|
2265
|
+
* the op ends the walk. */
|
|
2266
|
+
function nameAccess(rules: MaterializeRules, state: MaterializeState, op: Op, b: Block): void {
|
|
2267
|
+
const { opts, uses, edges, render, reach, loops, scopes, predsOf, memWriteBetween } = rules;
|
|
2268
|
+
const { useSitesOf } = uses;
|
|
2269
|
+
const { materialize } = state;
|
|
2270
|
+
const { defs, dom } = opts;
|
|
2271
|
+
const r = op.results[0];
|
|
2272
|
+
if (!r || !useSitesOf.has(r)) {
|
|
2273
|
+
return;
|
|
2274
|
+
} // dead call → exprstmt (unchanged)
|
|
2275
|
+
// A call, or a read whose spelling is qualified (a device read the lift pinned, a read of an
|
|
2276
|
+
// object the map declares volatile), EXECUTES at each render, where a plain duplicate read is
|
|
2277
|
+
// one agbcc CSEs away. It takes the two-site, edge, guarded-operand and one-position rules
|
|
2278
|
+
// below. And it stays in order against every other qualified access, which `isBarrier` says.
|
|
2279
|
+
const once = counted(op);
|
|
2280
|
+
// Under the value-home variation: which named global cell this op reads, if any. A constant-
|
|
2281
|
+
// offset `load` only — an `aload`'s runtime index names no single cell, and a call reads
|
|
2282
|
+
// everything. Null ⇒ every write bars.
|
|
2283
|
+
const cell =
|
|
2284
|
+
opts.rereadGlobals && defs && op.opcode === 'load' && !once
|
|
2285
|
+
? globalCellOf(defs, op.operands[0], op.attrs.off as number)
|
|
2286
|
+
: null;
|
|
2287
|
+
const barsThisRead = cell && defs ? mayWriteGlobal(defs, cell.name) : null;
|
|
2288
|
+
if (opts.materializeJoinFeeds && op.opcode === 'load' && edges.condBrArgFed.has(r)) {
|
|
2289
|
+
materialize.add(op);
|
|
2290
|
+
return;
|
|
2291
|
+
}
|
|
2292
|
+
const sites = useSitesOf.get(r)!;
|
|
2293
|
+
const consumers = [...new Set(sites.map((s) => s.op))];
|
|
2294
|
+
// A call must EXECUTE once — any second operand slot duplicates it → named temp.
|
|
2295
|
+
if (once && sites.length > 1) {
|
|
2296
|
+
materialize.add(op);
|
|
2297
|
+
return;
|
|
2298
|
+
}
|
|
2299
|
+
// …and ONCE means once on EVERY path the asm runs it on, which the multi-site rule above
|
|
2300
|
+
// does not say. `anchored` calls a terminator a single render position, but a branch's edge
|
|
2301
|
+
// copies are emitted inside the ARMS, so a call whose only consumer is an edge argument
|
|
2302
|
+
// renders in one arm and is skipped on the others. Compiled: `s32 t = f2(a); if (a > 0) {
|
|
2303
|
+
// return t; } return 0;` gives `bl f2` ahead of the `cmp`, and inlined at the edge the
|
|
2304
|
+
// recovered C calls `f2` only in the `else`; a `switch_br` arm hides it the same way.
|
|
2305
|
+
// Materializing puts it back at the position the asm executed it. Sole-use only in
|
|
2306
|
+
// practice — a second use already materialized above.
|
|
2307
|
+
//
|
|
2308
|
+
// And the same under the ops it is inlined into (`ridesEdge`): `f1(a0) - v` riding a
|
|
2309
|
+
// do-while's exit edge renders the call after the loop, once, where the body ran it every
|
|
2310
|
+
// iteration; riding the back edge of a loop that also exits elsewhere it renders in an arm;
|
|
2311
|
+
// riding two edge args it renders twice. And one op reading it twice spells it twice
|
|
2312
|
+
// (`spelledTwice`).
|
|
2313
|
+
if (once && (edges.branchArgFed.has(r) || reach.ridesEdge(op) || reach.spelledTwice(op))) {
|
|
2314
|
+
materialize.add(op);
|
|
2315
|
+
return;
|
|
2316
|
+
}
|
|
2317
|
+
// …and a `&&`/`||` skips its guarded operand the same way, without a branch of its own to
|
|
2318
|
+
// give it away. Two independent facts. A def DOMINATES its uses (ir/verify.ts), so a call
|
|
2319
|
+
// or a qualified read the connective reads ran on every path that evaluates it, while the
|
|
2320
|
+
// inlined C runs it on fewer — agbcc compiles `do { r = cb(p); } while (i++ <= n && r !=
|
|
2321
|
+
// 0);` to a `bl cb` ahead of both compares. And the DEF is where to put it back because a
|
|
2322
|
+
// counted op is `speculationUnsafe`, so no fold lifted one into this cone
|
|
2323
|
+
// (raise/shortcircuit.ts refuses an arm that holds one; structure.ts declines on a read
|
|
2324
|
+
// placed after the folds ran). `opaque`, the other hoist-unsafe
|
|
2325
|
+
// op with a result, needs no placement — neither position spells compilable C — and a
|
|
2326
|
+
// bottom test holding one still declines in `testSkipsAnEffect`, which is that guard's
|
|
2327
|
+
// remaining population.
|
|
2328
|
+
if (once && scopes.shortCircuitGuarded.has(r)) {
|
|
2329
|
+
materialize.add(op);
|
|
2330
|
+
return;
|
|
2331
|
+
}
|
|
2332
|
+
// Live across a LOOP neither side belongs to: the access ran before the loop and the
|
|
2333
|
+
// value crossed it in a callee-saved register (hipress's `keep = p[1]`, homed in r8 and
|
|
2334
|
+
// touched only by `mov`). Rendering at the use would re-schedule the access to the far
|
|
2335
|
+
// side of the loop — the same refusal liveAcrossCall makes for a call, applied to a loop.
|
|
2336
|
+
if (loops.liveAcrossLoop(op, r, consumers)) {
|
|
2337
|
+
materialize.add(op);
|
|
2338
|
+
return;
|
|
2339
|
+
}
|
|
2340
|
+
// ── DEF-BLOCK PLACEMENT (readsStayWhereWritten; see AnalyzeOptions) ──────────────────
|
|
2341
|
+
// Every render in a block this one strictly dominates, with a branch between ⇒ the asm
|
|
2342
|
+
// read once above that branch, and re-spelling the read there reproduces it. ONE render
|
|
2343
|
+
// suffices — the short-circuit-into-a-call shape has exactly one — and an unresolvable
|
|
2344
|
+
// render position refuses, as everywhere else. Both memory reads, spelled positively: a
|
|
2345
|
+
// `call` is the enclosing arm's other member and has its own execute-once rules above.
|
|
2346
|
+
if (
|
|
2347
|
+
(op.opcode === 'load' || op.opcode === 'aload') &&
|
|
2348
|
+
opts.readsStayWhereWritten &&
|
|
2349
|
+
dom &&
|
|
2350
|
+
!loops.multiBlockHeaders.has(b) &&
|
|
2351
|
+
!scopes.shortCircuitGuarded.has(r) &&
|
|
2352
|
+
!scopes.onlyFeedsBlockParams(r)
|
|
2353
|
+
) {
|
|
2354
|
+
const at = consumers.map((c) => render.emitPos(c));
|
|
2355
|
+
const rb = at.some((p) => p === null) ? null : [...new Set(at.map((p) => p!.blk))];
|
|
2356
|
+
if (
|
|
2357
|
+
rb &&
|
|
2358
|
+
rb.length > 0 &&
|
|
2359
|
+
rb.every((x) => x !== b && dom.get(x)!.has(b)) &&
|
|
2360
|
+
!preheaderOfRenderLoop(loops.loopBodies, b, rb) &&
|
|
2361
|
+
!fallThroughSeam(predsOf, b, rb)
|
|
2362
|
+
) {
|
|
2363
|
+
materialize.add(op);
|
|
2364
|
+
return;
|
|
2365
|
+
}
|
|
2366
|
+
}
|
|
2367
|
+
// Address-home variation: a multi-render load THROUGH a base this variation homed re-reads what the
|
|
2368
|
+
// asm read once into the register the home just reproduced — home the value too, at the
|
|
2369
|
+
// load's own position. Only through variation-homed bases (the fixpoint's later sweep sees them
|
|
2370
|
+
// even though reverse order visits the load first); the general multi-render re-read stays
|
|
2371
|
+
// the default rule below. addressHomedBases registers on the same sweep that materializes the
|
|
2372
|
+
// base — safe because no other rule can pre-empt a value the variation would home: base-slot-only
|
|
2373
|
+
// means no successor-arg use (outside copyInterdependent's read set), and the variation's scope
|
|
2374
|
+
// excludes consts, the const arm's only clientele.
|
|
2375
|
+
if (
|
|
2376
|
+
opts.homeSharedAddresses &&
|
|
2377
|
+
op.opcode === 'load' &&
|
|
2378
|
+
consumers.length > 1 &&
|
|
2379
|
+
state.addressHomedBases.has(op.operands[0]) &&
|
|
2380
|
+
!loops.multiBlockHeaders.has(b)
|
|
2381
|
+
) {
|
|
2382
|
+
materialize.add(op);
|
|
2383
|
+
return;
|
|
2384
|
+
}
|
|
2385
|
+
// A MULTI-RENDER load re-reads memory at each render — which is exactly what the original
|
|
2386
|
+
// per-use source spelling did (`while (*s != EOS) *d = *s;` reads *s twice per iteration),
|
|
2387
|
+
// so it is sound iff every render still sees the def-time memory: NO write anywhere
|
|
2388
|
+
// between the def and ANY render (cycle-aware, conservative write set). Otherwise a temp.
|
|
2389
|
+
//
|
|
2390
|
+
// WHERE it renders. Without the variation: one position per consumer, and a consumer with no
|
|
2391
|
+
// single position (its own value renders in several places) refuses. With the variation a load
|
|
2392
|
+
// resolves the whole SET instead — the second half of the value-home defect, where the
|
|
2393
|
+
// local is invented not by a barrier but because the pure expression downstream is itself
|
|
2394
|
+
// duplicated (`gOut = (gValue << 1) + gValue; return (gValue << 1) + gValue;`). Never for a
|
|
2395
|
+
// call or a marked read: several positions there mean several executions.
|
|
2396
|
+
const poss =
|
|
2397
|
+
opts.rereadGlobals && !once
|
|
2398
|
+
? render.emitPositions(op)
|
|
2399
|
+
: consumers.length > 1
|
|
2400
|
+
? consumers.map((c) => render.emitPos(c))
|
|
2401
|
+
: [render.emitPos(consumers[0])];
|
|
2402
|
+
if (!poss || poss.some((p) => p === null)) {
|
|
2403
|
+
materialize.add(op);
|
|
2404
|
+
return;
|
|
2405
|
+
}
|
|
2406
|
+
// A named helper op bars it too, as it bars the single render below: it is the call the asm
|
|
2407
|
+
// made, and a value read before it and used after it was read once and kept across it
|
|
2408
|
+
// (`v = gB; t = v / n; … q[1] = v;` is one `ldr` ahead of the `bl __divsi3`).
|
|
2409
|
+
if (poss.length > 1) {
|
|
2410
|
+
const writes = barsThisRead ?? effectful;
|
|
2411
|
+
const isWrite = (x: Op): boolean => writes(x) || namedHelper(materialize, x);
|
|
2412
|
+
if (poss.some((p) => memWriteBetween(op, p!, isWrite))) {
|
|
2413
|
+
materialize.add(op);
|
|
2414
|
+
}
|
|
2415
|
+
return;
|
|
2416
|
+
}
|
|
2417
|
+
const pos = poss[0]!;
|
|
2418
|
+
const access: PlacedAccess = { op, once, sites, pos, ownPart: partOf(render, op), barsThisRead };
|
|
2419
|
+
// A CROSS-BLOCK call or qualified read would run on the render block's paths instead of
|
|
2420
|
+
// its own — always materialize. Within its own block it is judged like everything else, by the
|
|
2421
|
+
// barrier scan below.
|
|
2422
|
+
if (once && pos.blk !== b) {
|
|
2423
|
+
materialize.add(op);
|
|
2424
|
+
return;
|
|
2425
|
+
}
|
|
2426
|
+
// Otherwise: inline only if no barrier stands on any def-avoiding def→render path.
|
|
2427
|
+
if (memWriteBetween(op, pos, (x) => isBarrier(rules, materialize, access, x))) {
|
|
2428
|
+
materialize.add(op);
|
|
2429
|
+
}
|
|
2430
|
+
}
|
|
2431
|
+
|
|
2432
|
+
/** A helper op the helper clause named, or a divide the exit rule did, is a pre-update home too
|
|
2433
|
+
* when its value reads a loop variable and is read where the update has run. Named, it lifts the
|
|
2434
|
+
* loop the pre-update hazard declines, in a body of any size — so the structurer's refusal on
|
|
2435
|
+
* homes (`preUpdateHomes`) must see it, as it sees the escape rule's.
|
|
2436
|
+
* KNOWN GAP: `readAfterUpdate` asks of the op's own consumers, so a divide whose value leaves the
|
|
2437
|
+
* loop only through a body op it feeds (`t + 1`) is not a home. Walked through that op, it would
|
|
2438
|
+
* be one, and the refusal would decline every such loop over a counter the function takes as a
|
|
2439
|
+
* parameter: its post-loop names include the entry block's. */
|
|
2440
|
+
function namePreUpdateHelpers(rules: MaterializeRules, state: MaterializeState): void {
|
|
2441
|
+
const { uses, render, loops } = rules;
|
|
2442
|
+
for (const op of state.materialize) {
|
|
2443
|
+
const r = op.results[0];
|
|
2444
|
+
if (!r || state.preUpdateHomes.has(op) || !(isHelper(op) || state.exitDivides.has(op))) {
|
|
2445
|
+
continue;
|
|
2446
|
+
}
|
|
2447
|
+
const consumers = render.consumersOf(op);
|
|
2448
|
+
if (
|
|
2449
|
+
loops.bottomTested.some(
|
|
2450
|
+
(L) =>
|
|
2451
|
+
L.body.has(uses.opBlock.get(op)!) && loops.readAfterUpdate(L, r, consumers) && loops.readsPreUpdate(L, op, r),
|
|
2452
|
+
)
|
|
2453
|
+
) {
|
|
2454
|
+
state.preUpdateHomes.add(op);
|
|
2455
|
+
}
|
|
2456
|
+
}
|
|
2457
|
+
}
|
|
2458
|
+
|
|
2459
|
+
/** Every stage `analyze` runs ahead of its fixpoint, over an empty `MaterializeState`. */
|
|
2460
|
+
function analysisStages(
|
|
2461
|
+
fn: Fn,
|
|
2462
|
+
returnsVoid: boolean,
|
|
2463
|
+
opts: AnalyzeOptions,
|
|
2464
|
+
): {
|
|
2465
|
+
liveIn: Map<Block, Set<Value>>;
|
|
2466
|
+
reachFrom: (b: Block) => Set<Block>;
|
|
2467
|
+
state: MaterializeState;
|
|
2468
|
+
rules: MaterializeRules;
|
|
2469
|
+
} {
|
|
2470
|
+
const { dom } = opts;
|
|
2471
|
+
const uses = indexUses(fn, returnsVoid);
|
|
2472
|
+
const edges = edgeArgUses(fn, dom ?? dominators(fn));
|
|
2473
|
+
const liveIn = blockLiveIn(fn, returnsVoid);
|
|
2474
|
+
const state: MaterializeState = {
|
|
2475
|
+
materialize: new Set(),
|
|
2476
|
+
preUpdateHomes: new Set(),
|
|
2477
|
+
exitDivides: new Set(),
|
|
2478
|
+
addressHomedBases: new Set(),
|
|
2479
|
+
};
|
|
2480
|
+
const { materialize } = state;
|
|
2481
|
+
const render = makeRenderModel(uses, materialize);
|
|
2482
|
+
const reach = makeInlineReach(uses, edges, materialize);
|
|
2483
|
+
const { reachFrom, reachAvoiding } = makeReach();
|
|
2484
|
+
const memWriteBetween = makeMemWriteBetween({ opBlock: uses.opBlock, opIndex: uses.opIndex, reachAvoiding });
|
|
2485
|
+
const defOf = opts.defs ?? defOpMap(fn);
|
|
2486
|
+
const predsOf = predecessors(fn);
|
|
2487
|
+
const loops = makeLoopRules({ fn, dom, predsOf, liveIn, uses, defOf, render, materialize });
|
|
2488
|
+
const scopes = homeScopes({ fn, returnsVoid, opts, uses, defOf, predsOf, loops, memWriteBetween });
|
|
2489
|
+
const copyInterdependent = copyInterdependentValues(fn, defOf);
|
|
2490
|
+
const rules = {
|
|
2491
|
+
opts,
|
|
2492
|
+
uses,
|
|
2493
|
+
edges,
|
|
2494
|
+
render,
|
|
2495
|
+
reach,
|
|
2496
|
+
loops,
|
|
2497
|
+
scopes,
|
|
2498
|
+
defOf,
|
|
2499
|
+
predsOf,
|
|
2500
|
+
memWriteBetween,
|
|
2501
|
+
copyInterdependent,
|
|
2502
|
+
};
|
|
2503
|
+
return { liveIn, reachFrom, state, rules };
|
|
2504
|
+
}
|
|
2505
|
+
|
|
2506
|
+
export function analyze(fn: Fn, returnsVoid: boolean, opts: AnalyzeOptions = {}): StructureAnalysis {
|
|
2507
|
+
const { liveIn, reachFrom, state, rules } = analysisStages(fn, returnsVoid, opts);
|
|
2508
|
+
const { materialize, preUpdateHomes } = state;
|
|
2509
|
+
const { uses, render, loops } = rules;
|
|
1468
2510
|
// Decide in REVERSE program order so a consumer's own materialization is settled before any
|
|
1469
2511
|
// producer asks for its emit position (SSA: uses follow defs in dominance/layout order) — and
|
|
1470
2512
|
// iterate to a fixpoint for IR whose block layout does not follow dominance (hand-built IR):
|
|
1471
2513
|
// materialize only GROWS, and growing it only moves render positions closer / adds barriers,
|
|
1472
2514
|
// so the loop is monotone and converges.
|
|
1473
|
-
|
|
2515
|
+
//
|
|
2516
|
+
// Then the same fixpoint again with the pre-update escape rule on. The rule stops its walk at a
|
|
2517
|
+
// materialized def, so it must see every name the other rules give; it adds its homes on the first
|
|
2518
|
+
// sweep, and the rest of the phase is the other rules settling around them.
|
|
2519
|
+
let escapePhase = false;
|
|
2520
|
+
for (let sizeBefore = -1; sizeBefore !== materialize.size || !escapePhase;) {
|
|
2521
|
+
if (sizeBefore === materialize.size) {
|
|
2522
|
+
escapePhase = true;
|
|
2523
|
+
}
|
|
1474
2524
|
sizeBefore = materialize.size;
|
|
1475
|
-
|
|
1476
|
-
emitPosSetCache.clear(); // both render-position caches read `materialize`, which just grew
|
|
2525
|
+
render.invalidate(); // both render-position caches read `materialize`, which just grew
|
|
1477
2526
|
for (let bi = fn.blocks.length - 1; bi >= 0; bi--) {
|
|
1478
2527
|
const b = fn.blocks[bi];
|
|
1479
2528
|
for (let oi = b.ops.length - 1; oi >= 0; oi--) {
|
|
@@ -1481,264 +2530,52 @@ export function analyze(fn: Fn, returnsVoid: boolean, opts: AnalyzeOptions = {})
|
|
|
1481
2530
|
if (materialize.has(op)) {
|
|
1482
2531
|
continue;
|
|
1483
2532
|
}
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
// memory access). A value with ≥2 distinct-STATEMENT uses in the SSA is one the compiler
|
|
1487
|
-
// kept in a register and reused: the frontend never dedups, so multi-use exists ONLY
|
|
1488
|
-
// because the asm loaded/computed the value once and read the same register again.
|
|
1489
|
-
// Inlining it re-derives the value at each use (a fresh pool load / repeated address
|
|
1490
|
-
// arithmetic) — which the compiler did NOT do — so materialize it into a local instead,
|
|
1491
|
-
// reproducing that register. Pure ⇒ every render is value-identical, so (unlike a load)
|
|
1492
|
-
// no intervening memory write can invalidate a later render: multi-consumer suffices, no
|
|
1493
|
-
// barrier scan. Scope: a `const` that is LIVE ACROSS A CALL. A value the compiler needs
|
|
1494
|
-
// after a call must survive in a CALLEE-SAVED register — i.e. a local — because the call
|
|
1495
|
-
// clobbers the caller-saved ones; the compiler therefore loads it ONCE and keeps it,
|
|
1496
|
-
// exactly what materializing into a local reproduces (the base of `((s32 *)C)[i]` reused
|
|
1497
|
-
// across `foo(...)` calls). WITHOUT a call in its live range the const is instead cheaply
|
|
1498
|
-
// re-materialized at each use (a bare `movs r, #0` per init), so materializing it would
|
|
1499
|
-
// ADD pointless copies and MISS — hence the call gate (the small-constant regression).
|
|
1500
|
-
// Cheap deref casts still land on the `index` node at the use, preserving byte strides.
|
|
1501
|
-
// Second scope: a NON-const read by a sibling parallel-copy arg (`copyInterdependent`) —
|
|
1502
|
-
// the one place a pure value's inlining is not value-identical rendering but a
|
|
1503
|
-
// re-derivation the copy machinery is forced into. Address cones are excluded from it
|
|
1504
|
-
// (an `&g + i` rendered standalone loses the memAccess's inline `(u8 *)` cast —
|
|
1505
|
-
// cast-aware base materialization is separate), and consts are not in it at all: a
|
|
1506
|
-
// re-derived const is re-materialization, which is the compiler's own behavior.
|
|
1507
|
-
const pr = op.results[0];
|
|
1508
|
-
if (op.opcode === 'const' && pr && useSitesOf.has(pr)) {
|
|
1509
|
-
const cons = [...new Set((useSitesOf.get(pr) ?? []).map((s) => s.op))];
|
|
1510
|
-
if (cons.length > 1 && liveAcrossCall(op, cons)) {
|
|
1511
|
-
materialize.add(op);
|
|
1512
|
-
}
|
|
1513
|
-
} else if (op.opcode !== 'const' && pr && copyInterdependent.has(pr) && !addressCone(op)) {
|
|
1514
|
-
materialize.add(op);
|
|
1515
|
-
}
|
|
1516
|
-
// Folding the FOUR VARIATION scopes below into one predicate-parameterized scope is BOOKED
|
|
1517
|
-
// in docs/level-tower.md and deliberately unpaid; what it cannot absorb is named there,
|
|
1518
|
-
// along with the price the fourth one added to it (the gate duplication).
|
|
1519
|
-
// Third scope, under the address-home variation only (see AnalyzeOptions.homeSharedAddresses):
|
|
1520
|
-
// a non-const pure value whose MERGE CLASS is used only as the base of 2+ memory
|
|
1521
|
-
// accesses.
|
|
1522
|
-
if (
|
|
1523
|
-
homeSharedAddresses &&
|
|
1524
|
-
op.opcode !== 'const' &&
|
|
1525
|
-
pr &&
|
|
1526
|
-
sharedBaseClass.has(pr) &&
|
|
1527
|
-
!addressCone(op) &&
|
|
1528
|
-
!multiBlockHeaders.has(b)
|
|
1529
|
-
) {
|
|
1530
|
-
materialize.add(op);
|
|
1531
|
-
addressHomedBases.add(pr);
|
|
1532
|
-
}
|
|
1533
|
-
// Fourth scope, under the loop-expression-home variation (AnalyzeOptions.homeLoopExprs): a
|
|
1534
|
-
// pure non-const value with 2+ distinct consumers, at least one of them inside a loop the
|
|
1535
|
-
// def sits outside. Shared bases stay the previous scope's (its load rule needs the
|
|
1536
|
-
// registration).
|
|
1537
|
-
if (
|
|
1538
|
-
homeLoopExprs &&
|
|
1539
|
-
op.opcode !== 'const' &&
|
|
1540
|
-
pr &&
|
|
1541
|
-
!usedOnlyAsSharedBase(pr) &&
|
|
1542
|
-
loopSharedConsumers(pr, b) &&
|
|
1543
|
-
!addressCone(op) &&
|
|
1544
|
-
!multiBlockHeaders.has(b)
|
|
1545
|
-
) {
|
|
1546
|
-
materialize.add(op);
|
|
1547
|
-
}
|
|
1548
|
-
// Fifth scope, under the derived-read-home variation (AnalyzeOptions.homeDerivedReads): a
|
|
1549
|
-
// pure non-const value with 2+ consumers standing on a memory read. Shared bases stay
|
|
1550
|
-
// the third scope's and values consumed across a loop the fourth's; what this one adds
|
|
1551
|
-
// is the straight-line case, which neither reaches.
|
|
1552
|
-
if (
|
|
1553
|
-
homeDerivedReads &&
|
|
1554
|
-
op.opcode !== 'const' &&
|
|
1555
|
-
pr &&
|
|
1556
|
-
!usedOnlyAsSharedBase(pr) &&
|
|
1557
|
-
!rendersAsAddress(op) &&
|
|
1558
|
-
multiConsumer(pr) &&
|
|
1559
|
-
standsOnMovableRead(op, b) &&
|
|
1560
|
-
!multiBlockHeaders.has(b)
|
|
1561
|
-
) {
|
|
1562
|
-
materialize.add(op);
|
|
1563
|
-
}
|
|
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
|
|
1566
|
-
// other const clientele is the first scope, a const live across a call. For the sibling
|
|
1567
|
-
// variations a re-derived const is re-materialization, the compiler's own behavior, while a
|
|
1568
|
-
// const the arms of a branch merge is one it held in a register across them
|
|
1569
|
-
// (`mov r5, #0` once, not per arm).
|
|
1570
|
-
if (homeMergeFeeds && mergeFeedOps.has(op)) {
|
|
1571
|
-
materialize.add(op);
|
|
1572
|
-
}
|
|
1573
|
-
continue;
|
|
1574
|
-
}
|
|
1575
|
-
const r = op.results[0];
|
|
1576
|
-
if (!r || !useSitesOf.has(r)) {
|
|
1577
|
-
continue;
|
|
1578
|
-
} // dead call → exprstmt (unchanged)
|
|
1579
|
-
// Under the value-home variation: which named global cell this op reads, if any. A constant-
|
|
1580
|
-
// offset `load` only — an `aload`'s runtime index names no single cell, and a call reads
|
|
1581
|
-
// everything. Null ⇒ every write bars, exactly as before.
|
|
1582
|
-
const cell =
|
|
1583
|
-
rereadGlobals && defs && op.opcode === 'load'
|
|
1584
|
-
? globalCellOf(defs, op.operands[0], op.attrs.off as number)
|
|
1585
|
-
: null;
|
|
1586
|
-
const barsThisRead = cell && defs && !volatileGlobal?.(cell.name) ? mayWriteGlobal(defs, cell.name) : null;
|
|
1587
|
-
if (materializeJoinFeeds && op.opcode === 'load' && condBrArgFed.has(r)) {
|
|
1588
|
-
materialize.add(op);
|
|
1589
|
-
continue;
|
|
1590
|
-
}
|
|
1591
|
-
const sites = useSitesOf.get(r)!;
|
|
1592
|
-
const consumers = [...new Set(sites.map((s) => s.op))];
|
|
1593
|
-
const isCall = op.opcode === 'call';
|
|
1594
|
-
// A call must EXECUTE once — any second operand slot duplicates it → named temp.
|
|
1595
|
-
if (isCall && sites.length > 1) {
|
|
1596
|
-
materialize.add(op);
|
|
1597
|
-
continue;
|
|
1598
|
-
}
|
|
1599
|
-
// …and ONCE means once on EVERY path the asm runs it on, which the multi-site rule above
|
|
1600
|
-
// does not say. `anchored` calls a terminator a single render position, but a branch's edge
|
|
1601
|
-
// copies are emitted inside the ARMS, so a call whose only consumer is an edge argument
|
|
1602
|
-
// renders in one arm and is skipped on the others. Compiled: `s32 t = f2(a); if (a > 0) {
|
|
1603
|
-
// return t; } return 0;` gives `bl f2` ahead of the `cmp`, and inlined at the edge the
|
|
1604
|
-
// recovered C calls `f2` only in the `else`; a `switch_br` arm hides it the same way.
|
|
1605
|
-
// Materializing puts it back at the position the asm executed it. Sole-use only in
|
|
1606
|
-
// practice — a second use already materialized above — and it is 0 of 2288 sa3 functions,
|
|
1607
|
-
// 2 of 412 klonoa ones.
|
|
1608
|
-
if (isCall && branchArgFed.has(r)) {
|
|
1609
|
-
materialize.add(op);
|
|
1610
|
-
continue;
|
|
1611
|
-
}
|
|
1612
|
-
// Live across a LOOP neither side belongs to: the access ran before the loop and the
|
|
1613
|
-
// value crossed it in a callee-saved register (hipress's `keep = p[1]`, homed in r8 and
|
|
1614
|
-
// touched only by `mov`). Rendering at the use would re-schedule the access to the far
|
|
1615
|
-
// side of the loop — the same refusal liveAcrossCall makes for a call, applied to a loop.
|
|
1616
|
-
if (liveAcrossLoop(op, r, consumers)) {
|
|
1617
|
-
materialize.add(op);
|
|
1618
|
-
continue;
|
|
1619
|
-
}
|
|
1620
|
-
// ── DEF-BLOCK PLACEMENT (readsStayWhereWritten; see AnalyzeOptions) ──────────────────
|
|
1621
|
-
// Every render in a block this one strictly dominates, with a branch between ⇒ the asm
|
|
1622
|
-
// read once above that branch, and re-spelling the read there reproduces it. ONE render
|
|
1623
|
-
// suffices — the short-circuit-into-a-call shape has exactly one — and an unresolvable
|
|
1624
|
-
// render position refuses, as everywhere else. Both memory reads, spelled positively: a
|
|
1625
|
-
// `call` is the enclosing arm's other member and has its own execute-once rules above.
|
|
2533
|
+
// A call needs no rule: read outside its block it is named by `nameAccess`'s cross-block rule.
|
|
2534
|
+
const er = op.results[0];
|
|
1626
2535
|
if (
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
!onlyFeedsBlockParams(r)
|
|
2536
|
+
escapePhase &&
|
|
2537
|
+
op.opcode !== 'call' &&
|
|
2538
|
+
er &&
|
|
2539
|
+
uses.useSitesOf.has(er) &&
|
|
2540
|
+
loops.escapesAheadOfUpdate(op, er, render.consumersOf(op))
|
|
1633
2541
|
) {
|
|
1634
|
-
const at = consumers.map((c) => emitPos(c));
|
|
1635
|
-
const rb = at.some((p) => p === null) ? null : [...new Set(at.map((p) => p!.blk))];
|
|
1636
|
-
if (
|
|
1637
|
-
rb &&
|
|
1638
|
-
rb.length > 0 &&
|
|
1639
|
-
rb.every((x) => x !== b && dom.get(x)!.has(b)) &&
|
|
1640
|
-
!preheaderOfRenderLoop(loopBodies, b, rb) &&
|
|
1641
|
-
!fallThroughSeam(predsOf, b, rb)
|
|
1642
|
-
) {
|
|
1643
|
-
materialize.add(op);
|
|
1644
|
-
continue;
|
|
1645
|
-
}
|
|
1646
|
-
}
|
|
1647
|
-
// Address-home variation: a multi-render load THROUGH a base this variation homed re-reads what the
|
|
1648
|
-
// asm read once into the register the home just reproduced — home the value too, at the
|
|
1649
|
-
// load's own position. Only through variation-homed bases (the fixpoint's later sweep sees them
|
|
1650
|
-
// even though reverse order visits the load first); the general multi-render re-read stays
|
|
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
|
|
1654
|
-
// excludes consts, the const arm's only clientele.
|
|
1655
|
-
if (
|
|
1656
|
-
homeSharedAddresses &&
|
|
1657
|
-
op.opcode === 'load' &&
|
|
1658
|
-
consumers.length > 1 &&
|
|
1659
|
-
addressHomedBases.has(op.operands[0]) &&
|
|
1660
|
-
!multiBlockHeaders.has(b)
|
|
1661
|
-
) {
|
|
1662
|
-
materialize.add(op);
|
|
1663
|
-
continue;
|
|
1664
|
-
}
|
|
1665
|
-
// A MULTI-RENDER load re-reads memory at each render — which is exactly what the original
|
|
1666
|
-
// per-use source spelling did (`while (*s != EOS) *d = *s;` reads *s twice per iteration),
|
|
1667
|
-
// so it is sound iff every render still sees the def-time memory: NO write anywhere
|
|
1668
|
-
// between the def and ANY render (cycle-aware, conservative write set). Otherwise a temp.
|
|
1669
|
-
//
|
|
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
|
|
1672
|
-
// resolves the whole SET instead — the second half of the value-home defect, where the
|
|
1673
|
-
// local is invented not by a barrier but because the pure expression downstream is itself
|
|
1674
|
-
// duplicated (`gOut = (gValue << 1) + gValue; return (gValue << 1) + gValue;`). Never for a
|
|
1675
|
-
// call: several positions there mean several executions.
|
|
1676
|
-
const poss =
|
|
1677
|
-
rereadGlobals && !isCall
|
|
1678
|
-
? emitPositions(op)
|
|
1679
|
-
: consumers.length > 1
|
|
1680
|
-
? consumers.map((c) => emitPos(c))
|
|
1681
|
-
: [emitPos(consumers[0])];
|
|
1682
|
-
if (!poss || poss.some((p) => p === null)) {
|
|
1683
|
-
materialize.add(op);
|
|
1684
|
-
continue;
|
|
1685
|
-
}
|
|
1686
|
-
if (poss.length > 1) {
|
|
1687
|
-
const isWrite = barsThisRead ?? ((x: Op) => EFFECTFUL_OPS.has(x.opcode));
|
|
1688
|
-
if (poss.some((p) => memWriteBetween(op, p!, isWrite))) {
|
|
1689
|
-
materialize.add(op);
|
|
1690
|
-
}
|
|
1691
|
-
continue;
|
|
1692
|
-
}
|
|
1693
|
-
const pos = poss[0]!;
|
|
1694
|
-
// A between-op is a BARRIER when it renders as a sequenced statement the def would cross:
|
|
1695
|
-
// stores/opaque always; a call/load that is dead (statement), materialized (statement), or
|
|
1696
|
-
// inlined into a DIFFERENT statement. A sibling effect inlined into the SAME statement is
|
|
1697
|
-
// not a reorder — the recompiling compiler orders unsequenced operands of one expression
|
|
1698
|
-
// exactly as it originally chose to. Loads never bar a load (reads don't conflict).
|
|
1699
|
-
const samePos = (q: { blk: Block; idx: number } | null) => q !== null && q.blk === pos.blk && q.idx === pos.idx;
|
|
1700
|
-
const isBarrier = (x: Op): boolean => {
|
|
1701
|
-
// Value-home variation: a store/astore this read is PROVABLY disjoint from (a different named
|
|
1702
|
-
// global) does not sequence against it, so the read may still render at its use.
|
|
1703
|
-
if (barsThisRead && (x.opcode === 'store' || x.opcode === 'astore') && !barsThisRead(x)) {
|
|
1704
|
-
return false;
|
|
1705
|
-
}
|
|
1706
|
-
if (x.opcode === 'store') {
|
|
1707
|
-
// A store to a PROVABLY-DISJOINT slot of the same base never aliases the load
|
|
1708
|
-
// (`disjointConstSlots`, ir/alias.ts). Anything less certain bars.
|
|
1709
|
-
if (!isCall && op.opcode === 'load' && disjointConstSlots(op, x)) {
|
|
1710
|
-
return false;
|
|
1711
|
-
}
|
|
1712
|
-
return true;
|
|
1713
|
-
}
|
|
1714
|
-
if (x.opcode === 'astore' || x.opcode === 'opaque') {
|
|
1715
|
-
return true;
|
|
1716
|
-
}
|
|
1717
|
-
if (x.opcode === 'call') {
|
|
1718
|
-
return !x.results.length || !useSitesOf.has(x.results[0]) || materialize.has(x) || !samePos(emitPos(x));
|
|
1719
|
-
}
|
|
1720
|
-
if (!isCall) {
|
|
1721
|
-
return false;
|
|
1722
|
-
} // a load never bars a load
|
|
1723
|
-
if (x.opcode === 'load' || x.opcode === 'aload') {
|
|
1724
|
-
return !x.results.length || !useSitesOf.has(x.results[0])
|
|
1725
|
-
? false // dead load: never emitted at all
|
|
1726
|
-
: materialize.has(x) || !samePos(emitPos(x));
|
|
1727
|
-
}
|
|
1728
|
-
return false;
|
|
1729
|
-
};
|
|
1730
|
-
// A CROSS-BLOCK call's execution would become path-dependent — always materialize. Within
|
|
1731
|
-
// its own block a call is judged like everything else, by the barrier scan below.
|
|
1732
|
-
if (isCall && pos.blk !== b) {
|
|
1733
2542
|
materialize.add(op);
|
|
2543
|
+
preUpdateHomes.add(op);
|
|
1734
2544
|
continue;
|
|
1735
2545
|
}
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
2546
|
+
if (op.opcode !== 'call' && op.opcode !== 'load' && op.opcode !== 'aload') {
|
|
2547
|
+
namePureOp(rules, state, op, b, escapePhase);
|
|
2548
|
+
} else {
|
|
2549
|
+
nameAccess(rules, state, op, b);
|
|
1739
2550
|
}
|
|
1740
2551
|
}
|
|
1741
2552
|
}
|
|
1742
2553
|
}
|
|
1743
|
-
|
|
2554
|
+
namePreUpdateHelpers(rules, state);
|
|
2555
|
+
return {
|
|
2556
|
+
useSitesOf: uses.useSitesOf,
|
|
2557
|
+
opIndex: uses.opIndex,
|
|
2558
|
+
opBlock: uses.opBlock,
|
|
2559
|
+
liveIn,
|
|
2560
|
+
materialize,
|
|
2561
|
+
preUpdateHomes,
|
|
2562
|
+
reachFrom,
|
|
2563
|
+
emitPos: render.emitPos,
|
|
2564
|
+
memWriteBetween: rules.memWriteBetween,
|
|
2565
|
+
};
|
|
1744
2566
|
}
|
|
2567
|
+
|
|
2568
|
+
/** Internal surface for this module's own tests, and for nothing else. `@asmlift/core` exports
|
|
2569
|
+
* every source path under `./*`, so a plain `export` here would put the stages `analyze` runs into the
|
|
2570
|
+
* package's public API, where no consumer should be able to depend on them. The name says what it
|
|
2571
|
+
* is; nothing in `src` reads it. */
|
|
2572
|
+
export const __testing = {
|
|
2573
|
+
analysisStages,
|
|
2574
|
+
blockLiveIn,
|
|
2575
|
+
indexUses,
|
|
2576
|
+
edgeArgUses,
|
|
2577
|
+
makeRenderModel,
|
|
2578
|
+
makeLoopRules,
|
|
2579
|
+
namePureOp,
|
|
2580
|
+
nameAccess,
|
|
2581
|
+
};
|