@asmlift/core 0.6.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +48 -24
- package/package.json +1 -1
- package/src/backend/cfamily.ts +39 -11
- package/src/backend/pascal.ts +2 -2
- package/src/codegen-flags.ts +640 -0
- package/src/contracts.ts +60 -11
- package/src/frontend/disasm.ts +141 -11
- package/src/frontend/high-half.ts +149 -0
- package/src/frontend/mips.ts +458 -209
- package/src/frontend/ppc.ts +332 -67
- package/src/frontend/reloc-symbol.ts +109 -0
- package/src/frontend/splat.ts +56 -18
- package/src/frontend/ssa.ts +127 -30
- package/src/frontend/stackargs.ts +420 -0
- package/src/frontend/thumb.ts +209 -232
- package/src/ir/alias.ts +24 -0
- package/src/ir/core.ts +70 -3
- package/src/ir/opcodes.ts +52 -7
- package/src/ir/parse.ts +7 -1
- package/src/ir/simplify.ts +1 -1
- package/src/l3/address.ts +2 -2
- package/src/l3/advance.ts +373 -0
- package/src/l3/argbase.ts +6 -6
- package/src/l3/argcopy.ts +269 -0
- package/src/l3/ast.ts +110 -22
- package/src/l3/basecse.ts +50 -30
- package/src/l3/coalesce.ts +118 -61
- package/src/l3/gates.ts +75 -1
- package/src/l3/hoist.ts +1 -1
- package/src/l3/homesplit.ts +13 -13
- package/src/l3/initfirst.ts +3 -3
- package/src/l3/inlinebase.ts +16 -16
- package/src/l3/mentions.ts +68 -5
- package/src/l3/mulfirst.ts +3 -3
- package/src/l3/nearbase.ts +4 -4
- package/src/l3/offmember.ts +5 -5
- package/src/l3/parkfirst.ts +6 -6
- package/src/l3/pollguard.ts +3 -3
- package/src/l3/ptrfield.ts +4 -4
- package/src/l3/regspell.ts +8 -8
- package/src/l3/reindex.ts +22 -17
- package/src/l3/scopebase.ts +32 -29
- package/src/l3/sinkinit.ts +7 -7
- package/src/l3/slotorder.ts +3 -3
- package/src/l3/storage.ts +1 -1
- package/src/l3/tailmerge.ts +2 -2
- package/src/l3/tailret.ts +70 -0
- package/src/l3/typing.ts +3 -3
- package/src/l3/unmerge.ts +483 -59
- package/src/l3/unreduce.ts +15 -14
- package/src/l3/volatileptr.ts +11 -11
- package/src/l3/volatileval.ts +11 -11
- package/src/l3/volstore.ts +16 -16
- package/src/l3/zerosub.ts +6 -6
- package/src/mangle.ts +49 -0
- package/src/pattern/engine.ts +132 -17
- package/src/pipeline.ts +39 -16
- package/src/proto.ts +2 -2
- package/src/raise/const.ts +203 -3
- package/src/raise/divpow2.ts +2 -2
- package/src/raise/extscale.ts +345 -0
- package/src/raise/globalshape.ts +32 -12
- package/src/raise/gvn.ts +2 -2
- package/src/raise/magicdiv.ts +2 -2
- package/src/raise/memberarrays.ts +4 -4
- package/src/raise/narrowlocal.ts +18 -2
- package/src/raise/paramwidth.ts +133 -3
- package/src/raise/pre-recovery.ts +100 -25
- package/src/raise/retsink.ts +389 -19
- package/src/raise/shortcircuit.ts +595 -34
- package/src/raise/structs.ts +4 -4
- package/src/raise/tailsink.ts +141 -0
- package/src/rank-declare.ts +21 -13
- package/src/{rank-axes.ts → rank-variations.ts} +319 -189
- package/src/rank.ts +1176 -805
- package/src/structure/analysis.ts +87 -90
- package/src/structure/bitfields.ts +130 -30
- package/src/structure/globalaccess.ts +30 -4
- package/src/structure/namecoalesce.ts +32 -13
- package/src/structure/retspell.ts +95 -0
- package/src/structure/structure.ts +1425 -201
- package/src/structure/switch-recover.ts +101 -8
- package/src/symbols.ts +127 -6
- package/src/target.ts +374 -44
- package/src/trace.ts +28 -19
- package/src/variation-definitions.ts +1590 -0
- package/src/variation-gates.ts +92 -0
- package/src/variation-tokens.ts +356 -0
|
@@ -40,6 +40,7 @@
|
|
|
40
40
|
// is NEGATABLE whenever the orientation inverts it — an icmp by opcode swap or a `logic_and`/`logic_or` by
|
|
41
41
|
// De Morgan, both via `negateCondOps`, which the control-flow form below shares. Any deviation falls
|
|
42
42
|
// through untouched (a miss, never a miscompile).
|
|
43
|
+
import { disjointConstSlots } from '../ir/alias';
|
|
43
44
|
import {
|
|
44
45
|
Block,
|
|
45
46
|
Fn,
|
|
@@ -47,6 +48,7 @@ import {
|
|
|
47
48
|
Successor,
|
|
48
49
|
Value,
|
|
49
50
|
defOpMap,
|
|
51
|
+
dominators,
|
|
50
52
|
foldWriteOrder,
|
|
51
53
|
forwardingTarget,
|
|
52
54
|
mkOp,
|
|
@@ -54,9 +56,11 @@ import {
|
|
|
54
56
|
predecessors,
|
|
55
57
|
reachableBlocks,
|
|
56
58
|
replaceAllUsesWith,
|
|
59
|
+
successorsOf,
|
|
57
60
|
} from '../ir/core';
|
|
58
|
-
import { HOIST_UNSAFE_OPS, NEGATED_ICMP } from '../ir/opcodes';
|
|
61
|
+
import { EFFECTFUL_OPS, HOIST_UNSAFE_OPS, NEGATED_ICMP, ORDER_SENSITIVE_OPS } from '../ir/opcodes';
|
|
59
62
|
import { T } from '../ir/types';
|
|
63
|
+
import { type Gate, firstRejection } from '../l3/gates';
|
|
60
64
|
|
|
61
65
|
const BOOL_OPS = new Set([...Object.keys(NEGATED_ICMP), 'logic_and', 'logic_or']);
|
|
62
66
|
|
|
@@ -328,13 +332,13 @@ export function recognizeShortCircuit(fn: Fn): boolean {
|
|
|
328
332
|
// 6 rows, protects 2 (`pokeemerald:IsStringLengthAtLeast`,
|
|
329
333
|
// `pokeemerald:TrySetCantSelectMoveBattleScript`), and on the other 4 the published winner
|
|
330
334
|
// folds THROUGH it — `kleod:CheckWorldCompletion`'s refused site is `v5 == 3 || v5 == 5` on an
|
|
331
|
-
// ordinary inner-loop counter with no dispatch region near it. It is the
|
|
335
|
+
// ordinary inner-loop counter with no dispatch region near it. It is the variation, not the clause,
|
|
332
336
|
// that keeps those 4. A structural discriminator is L1-visible and would be strictly better —
|
|
333
337
|
// is the shared block the entry of a region with dispatch-shaped in-edges, is the scrutinee
|
|
334
338
|
// defined by the enclosing loop header — and is UNBUILT.
|
|
335
339
|
// The relayed clause below is a different statement (see its own note: a blunt proxy that
|
|
336
340
|
// fires on an ordinary loop counter), it has NO inhabitant anywhere in the benchmark, and a
|
|
337
|
-
// candidate born there would carry a `/connective`
|
|
341
|
+
// candidate born there would carry a `/connective` variation for a fold that answers
|
|
338
342
|
// no connective-vs-tree question. It stays absolute.
|
|
339
343
|
// - the shared block was reached through a RELAY, and either test's scrutinee is compared against
|
|
340
344
|
// constants more than once in the function. This one is ABSOLUTE — `foldTreeOwned` does not
|
|
@@ -360,11 +364,24 @@ export function recognizeShortCircuit(fn: Fn): boolean {
|
|
|
360
364
|
// (`af:adds:ido7.1` and the `divv`/`gcd`/`modv` rows).
|
|
361
365
|
// - ^g holds a side effect — its ops move into ^h, which runs UNCONDITIONALLY. A store in `b`
|
|
362
366
|
// would then execute even when `a` already decided the branch. (`a || (*p = 1)`.)
|
|
363
|
-
// - a value defined in ^g is used
|
|
364
|
-
//
|
|
365
|
-
//
|
|
366
|
-
//
|
|
367
|
+
// - a value defined in ^g is used more than once inside ^g. Then the structurer MATERIALIZES it
|
|
368
|
+
// into a local, which renders as a statement BEFORE the `if` — turning `b`'s conditional
|
|
369
|
+
// computation into an unconditional one. Single-use-and-local is precisely the shape
|
|
370
|
+
// analysis.ts inlines into the connective's right operand, where C's own short-circuit
|
|
367
371
|
// re-guards it. This is what keeps a load in `b` from being hoisted across the guard in `a`.
|
|
372
|
+
// - a value defined in ^g is ALSO read past ^g, and a rule in `ARM_REREAD_GATES` refuses
|
|
373
|
+
// re-deriving it in the arm. A read past ^g is otherwise not a refusal: on verified IR it can
|
|
374
|
+
// only sit under the arm ^g's non-shared edge enters (^g dominates nothing else — the shared
|
|
375
|
+
// block is reached from ^h too), so the arm RE-DERIVES it (`armRereadCone`) and ^g keeps only
|
|
376
|
+
// its own use. Refusing every such read splits the condition into a nest whose shared block
|
|
377
|
+
// the structurer duplicates into both negative branches — `synthetic:ladder5` and
|
|
378
|
+
// `synthetic:ladidx2`. The two
|
|
379
|
+
// are different evidence: `ladidx2` is `if (a && (p->f & 0x7F) == 0x7F) { p->f &= 0x80; }`,
|
|
380
|
+
// which names `p->f` twice while agbcc reads it once and carries the register into the arm,
|
|
381
|
+
// so its copy is a LOAD; `ladder5`'s arm reuses an ADDRESS its test computed, a
|
|
382
|
+
// pure cone analysis.ts re-derives at every use anyway. Two of the table's rules are sound
|
|
383
|
+
// (the copy must not run on a path the original did not); the rest are fidelity, and say
|
|
384
|
+
// which bytes they protect.
|
|
368
385
|
// - the two edges into the shared block carry DIFFERENT args. Only one edge survives the fold,
|
|
369
386
|
// so it can only carry one argument list; picking either would silently drop the other path's
|
|
370
387
|
// phi input.
|
|
@@ -396,8 +413,8 @@ export function recognizeShortCircuit(fn: Fn): boolean {
|
|
|
396
413
|
// transfer the number to yet either — instrumenting the value form over the whole benchmark
|
|
397
414
|
// under both lift configurations counts 6 folds per configuration, every head a single icmp.
|
|
398
415
|
//
|
|
399
|
-
// The `/connective` LIFT
|
|
400
|
-
// `onTreeOwned` below is what tells rank.ts the
|
|
416
|
+
// The `/connective` LIFT VARIATION is a separate question from the default lift, and is unwidened:
|
|
417
|
+
// `onTreeOwned` below is what tells rank.ts the variation exists for a row, and this check sits ABOVE
|
|
401
418
|
// it. Over the whole benchmark under BOTH configurations rank.ts lifts with (`foldTreeOwned`
|
|
402
419
|
// false and true), against the same rows with the connective case ablated: the recovered IR
|
|
403
420
|
// moves on the same 2 rows under each, `onTreeOwned` fires on the same rows either way, and
|
|
@@ -412,26 +429,28 @@ export function recognizeShortCircuit(fn: Fn): boolean {
|
|
|
412
429
|
// source order, so which was written is recorded in the branch senses. This rewrite keeps ^h's
|
|
413
430
|
// unchanged successor slot, so the connective comes out in the orientation those senses spell, and
|
|
414
431
|
// which of the two that is depends on the branch RANGE below, not on the source. Reaching the
|
|
415
|
-
// other is `negateCond`'s job (l3/ast.ts distributes `!(a && b)`), and rank.ts's `/flip-join`
|
|
416
|
-
// is what asks for it on a RECONVERGING if — the default spells the layout reading and the
|
|
432
|
+
// other is `negateCond`'s job (l3/ast.ts distributes `!(a && b)`), and rank.ts's `/flip-join` variation
|
|
433
|
+
// is what asks for it on a RECONVERGING if — the default spells the layout reading and the variation
|
|
417
434
|
// spells its dual, so both orientations are compiled and the differ picks (synthetic:ifand_near
|
|
418
|
-
// matches at the default, synthetic:ifor_near on the
|
|
435
|
+
// matches at the default, synthetic:ifor_near on the variation).
|
|
419
436
|
//
|
|
420
437
|
// What neither reaches is the MIXED spelling. `negateJoinedBranchSense` is a per-FUNCTION boolean,
|
|
421
|
-
// so the
|
|
438
|
+
// so the variation negates every joined `if` at once — and of the 28 real rows carrying the
|
|
422
439
|
// `short-circuit` tag, 16 hold two or more TWO-ARMED ifs (counted by `else`, which is what the
|
|
423
|
-
//
|
|
440
|
+
// variation's own `thenS.length && elseS.length` gate needs) and 12 hold two or more conditions
|
|
424
441
|
// carrying a connective. TWO-ARMED is the count that matters: both sense booleans exclude a
|
|
425
442
|
// one-armed `if` by construction, so a tally of `if (` of any kind is the wrong denominator.
|
|
426
|
-
//
|
|
427
|
-
//
|
|
443
|
+
// The per-SITE negation is `/site-sense` (rank-variations.ts), which reads the orientation this fold
|
|
444
|
+
// stamps on the fused branch (`scSharedOnFall`, below) instead of the per-function boolean. A gate
|
|
445
|
+
// on whether to ENUMERATE the variation does not reach the mixed spelling and removes one the differ
|
|
446
|
+
// would referee.
|
|
428
447
|
//
|
|
429
448
|
// The De Morgan negation below forecloses a third spelling, at a measured price: it DISTRIBUTES, so
|
|
430
449
|
// the leaves come out negated (`a || (!b && !c)`) and `a || !(b || c)` has no
|
|
431
450
|
// candidate — the IR has no `logic_not` to build one from (ir/opcodes.ts). Compiled both ways on
|
|
432
451
|
// the `a || (b && c)` guard shape at agbcc's default flags, the two source spellings assemble to
|
|
433
452
|
// the same bytes (12/12 rows, score 0), so the foreclosure costs nothing here. A shape that ever
|
|
434
|
-
// separated them would be a new
|
|
453
|
+
// separated them would be a new variation, not a bug in this fold.
|
|
435
454
|
//
|
|
436
455
|
// WHICH slot ^g lands in is decided by the asm's branch POLARITY, and on Thumb the branch RANGE
|
|
437
456
|
// decides the polarity — so the same source `&&` reaches this pass two different ways:
|
|
@@ -462,11 +481,35 @@ export function recognizeShortCircuit(fn: Fn): boolean {
|
|
|
462
481
|
// as a node stamp (the `#144` `Expr.baseOrdered` shape) would hand every site of such a function
|
|
463
482
|
// one answer and reach exactly the two configurations `negateJoinedBranchSense` already reaches.
|
|
464
483
|
//
|
|
484
|
+
// WHAT IS THE CARRIER is the OTHER boolean this loop computes — whether the SHARED block was
|
|
485
|
+
// reached from ^g by its branch or by falling into it — and the fold stamps it as
|
|
486
|
+
// `scSharedOnFall`. It separates every site the sources above wrote as a dual from every site
|
|
487
|
+
// they did not, at constant branch range, because it reads the source's connective rather than
|
|
488
|
+
// the range: an `&&` sends every failing test AWAY to the shared block, an `||` falls into it.
|
|
489
|
+
//
|
|
490
|
+
// IT IS HALF THE CARRIER. Which SOURCE arm the shared block is only names the spelling once you
|
|
491
|
+
// also know which SUCCESSOR SLOT it lands in here, and the slot is `gIsFall` — stamped beside it
|
|
492
|
+
// as `scSharedIsTaken`. `gIsFall` false puts the shared arm in the FALL slot, which happens at the
|
|
493
|
+
// long-branch `&&` AND at every CHAINED fold, since an inner fold leaves the head's taken edge
|
|
494
|
+
// pointing at the next test. There the source's `then` is in the taken slot however ^g reached the
|
|
495
|
+
// shared block, so BOTH values of `scSharedOnFall` read POSITIVE — measured, not derived: the two
|
|
496
|
+
// inhabited layouts stamp opposite `scSharedOnFall` (`synthetic:chainsense` false,
|
|
497
|
+
// `synthetic:ifand_far` true) and want the same spelling, because the long branch is exactly the
|
|
498
|
+
// layout that falsifies the source-order premise ON that stamp.
|
|
499
|
+
//
|
|
500
|
+
// AND THAT IS STILL NOT ALL OF IT, because the long branch INVERTS the last test and so moves the
|
|
501
|
+
// shared arm between slots: a long `||` puts it back in the TAKEN slot and stamps the pair a short
|
|
502
|
+
// `&&` stamps (`synthetic:ifor_far` against `synthetic:ifand_near`, measured from their own asm),
|
|
503
|
+
// while wanting the opposite spelling. The third stamp is `scEdgeRelayed`, the trampoline the
|
|
504
|
+
// inversion leaves on one of the two edges — see its own note at the stamp. The table the consumer
|
|
505
|
+
// reads is `structure.ts`'s `senseFromFoldEvidence` site default, and one of its cells is
|
|
506
|
+
// undecided by all three facts (`kleod:CheckWorldCompletion:agbcc`, two sites, opposite senses).
|
|
507
|
+
//
|
|
465
508
|
// Every refusal falls through untouched — a miss, never a miscompile.
|
|
466
509
|
/** Per-call options for `recognizeBranchShortCircuit` — the tree-ownership refusal's two ends. */
|
|
467
510
|
export interface BranchShortCircuitOptions {
|
|
468
511
|
/** Take the fold at a site the PAIRWISE comparison-tree refusal owns, spelling the connective
|
|
469
|
-
* where the default leaves the tree for switch-recover.ts. rank.ts's `/connective`
|
|
512
|
+
* where the default leaves the tree for switch-recover.ts. rank.ts's `/connective` variation; see the
|
|
470
513
|
* REFUSALS note. It widens the SHAPE the fold accepts and nothing about what the fold may move —
|
|
471
514
|
* every other refusal still applies, the RELAYED clause included.
|
|
472
515
|
*
|
|
@@ -479,14 +522,22 @@ export interface BranchShortCircuitOptions {
|
|
|
479
522
|
* boolean, where a per-site fork would be 1024×. That is why the boolean, not an oversight. */
|
|
480
523
|
foldTreeOwned?: boolean;
|
|
481
524
|
/** Called at each site the pairwise tree-ownership refusal is the ONE thing stopping the fold —
|
|
482
|
-
* how rank.ts learns the
|
|
525
|
+
* how rank.ts learns the variation has an inhabitant here without re-running the matcher. Asked LAST,
|
|
483
526
|
* after `sameArgs` and the negatability check, so a report means a `/connective` candidate that
|
|
484
527
|
* differs from its sibling: reporting a refusal merely REACHED would double the row's whole
|
|
485
528
|
* candidate cross for a lift that produces duplicates the dedup collapses. (Its sibling gate
|
|
486
|
-
* `hasSetupArgsNarrowing` asks the same question the same way — does the
|
|
529
|
+
* `hasSetupArgsNarrowing` asks the same question the same way — does the variation CHANGE anything.)
|
|
487
530
|
* The pass re-scans after every rewrite, so one site can report more than once; read it as a
|
|
488
531
|
* boolean. */
|
|
489
532
|
onTreeOwned?: () => void;
|
|
533
|
+
/** The RE-READ admission's refusals — {@link ARM_REREAD_GATES} when absent. A parameter so a
|
|
534
|
+
* census can hand in `tallying(ARM_REREAD_GATES).gates` and a test can ablate one rule; the
|
|
535
|
+
* census does, through `bench gates --pass arm-reread` (apps/benchmark/src/run/gate-census.ts). */
|
|
536
|
+
armReread?: readonly Gate<ArmRereadSite>[];
|
|
537
|
+
/** The TARGET's `compilerBehaviors.reloadsLocalReread` (target.ts), which one conjunct of one rule
|
|
538
|
+
* in {@link ARM_REREAD_GATES} reads — `read-behind-effect`. Threaded by raise/pre-recovery.ts from
|
|
539
|
+
* the target it already holds, the way `narrowlocal`'s `hoistsSingleSetArm` is. Absent ⇒ false. */
|
|
540
|
+
reloadsLocalReread?: boolean;
|
|
490
541
|
}
|
|
491
542
|
|
|
492
543
|
export function recognizeBranchShortCircuit(fn: Fn, opts: BranchShortCircuitOptions = {}): boolean {
|
|
@@ -525,17 +576,11 @@ export function recognizeBranchShortCircuit(fn: Fn, opts: BranchShortCircuitOpti
|
|
|
525
576
|
if (gTaken.block === gFall.block) {
|
|
526
577
|
continue;
|
|
527
578
|
}
|
|
528
|
-
// ^g's body must be pure
|
|
529
|
-
// see the REFUSALS note: an escaping or reused value becomes a statement hoisted out of the
|
|
530
|
-
// short circuit.
|
|
579
|
+
// ^g's body must be pure — see the REFUSALS note.
|
|
531
580
|
// HOIST_UNSAFE_OPS includes `opaque`: an instruction asmlift could not model, and moving it
|
|
532
581
|
// out of the arm that guards it is the reordering this refuses. Loud either way today — a
|
|
533
582
|
// decline under `onGap: 'strict'`, an ASMLIFT_ERROR marker under `annotate`.
|
|
534
|
-
|
|
535
|
-
if (body.some((op) => HOIST_UNSAFE_OPS.has(op.opcode))) {
|
|
536
|
-
continue;
|
|
537
|
-
}
|
|
538
|
-
if (!definedValuesStayLocal(fn, g)) {
|
|
583
|
+
if (g.ops.slice(0, -1).some((op) => HOIST_UNSAFE_OPS.has(op.opcode))) {
|
|
539
584
|
continue;
|
|
540
585
|
}
|
|
541
586
|
// Which of ^g's edges rejoins ^h's other successor? That is the shared block. A DIRECT edge
|
|
@@ -579,6 +624,17 @@ export function recognizeBranchShortCircuit(fn: Fn, opts: BranchShortCircuitOpti
|
|
|
579
624
|
if (wantEdge !== gTaken && !negation) {
|
|
580
625
|
continue;
|
|
581
626
|
}
|
|
627
|
+
// Every value ^g defines is consumed only by ^g itself, once, or re-derived in the arm —
|
|
628
|
+
// see the REFUSALS note: an escaping or reused value becomes a statement hoisted out of
|
|
629
|
+
// the short circuit. Asked AFTER `sameArgs` and the negatability check, so a census of
|
|
630
|
+
// `ARM_REREAD_GATES` counts only sites nothing cheaper refuses — above `sameArgs` it would
|
|
631
|
+
// credit `read-behind-effect` with void functions whose shared edges carry different dead
|
|
632
|
+
// `r0` values. The verdict is a conjunction of pure tests, so the order moves no fold. And
|
|
633
|
+
// BEFORE tree ownership, so `onTreeOwned` keeps its meaning — the one thing in the way.
|
|
634
|
+
const reread = armRereadCone(fn, g, otherEdge.block, preds, opts);
|
|
635
|
+
if (!reread || !definedValuesStayLocal(fn, g, reread)) {
|
|
636
|
+
continue;
|
|
637
|
+
}
|
|
582
638
|
// LAST refusal, so `onTreeOwned` reports a site where tree ownership is the ONE thing in the
|
|
583
639
|
// way — which is why the negatability check stays above it even though it MINTS the negated
|
|
584
640
|
// cone (up to NEGATE_BUDGET ops) and discards it whenever this gate refuses. That discard is
|
|
@@ -591,6 +647,8 @@ export function recognizeBranchShortCircuit(fn: Fn, opts: BranchShortCircuitOpti
|
|
|
591
647
|
continue;
|
|
592
648
|
}
|
|
593
649
|
}
|
|
650
|
+
rereadInArm(fn, g, otherEdge.block, reread);
|
|
651
|
+
const body = g.ops.slice(0, -1);
|
|
594
652
|
const second = negation ? negation.result : c2;
|
|
595
653
|
const negated: Op[] = negation ? negation.ops : [];
|
|
596
654
|
const res = mkValue(T.unk(32));
|
|
@@ -601,7 +659,69 @@ export function recognizeBranchShortCircuit(fn: Fn, opts: BranchShortCircuitOpti
|
|
|
601
659
|
// ^g's body moves ahead of ^h's terminator; ^h keeps the successor SLOT that did not change
|
|
602
660
|
// (taken=shared for `||`, taken=other for `&&`), so the frontend's branch sense survives.
|
|
603
661
|
h.ops.splice(h.ops.length - 1, 1, ...body, ...negated, connective, {
|
|
604
|
-
|
|
662
|
+
// THE ORIENTATION EVIDENCE, kept because only this pass can see it. `scSharedOnFall`
|
|
663
|
+
// says the SHARED block — the arm both tests reach — was reached from ^g by
|
|
664
|
+
// FALL-THROUGH rather than by ^g's branch. gcc lays a condition's arms out in source
|
|
665
|
+
// order, so an `&&` sends every failing test AWAY to the shared block (the source's
|
|
666
|
+
// `else`) while an `||` lets the last test FALL INTO it (the source's `then`) — which is
|
|
667
|
+
// the one thing that separates `if (a && b) X else Y` from its dual `if (!a || !b) Y
|
|
668
|
+
// else X`, two different objects that fold to the same connective and the same successor
|
|
669
|
+
// slots. Consumed at L3 by `StructureOptions.senseFromFoldEvidence`.
|
|
670
|
+
//
|
|
671
|
+
// `gIsFall` is NOT that separator and is stamped for a different reason. It reads the
|
|
672
|
+
// branch RANGE: over `synthetic:joinsense` (two sites, opposite source connectives) and
|
|
673
|
+
// `synthetic:mixsense` (four sites, two inverted) it is TRUE at every site of both,
|
|
674
|
+
// where `scSharedOnFall` separates them. What it is, is the SLOT — the successor order
|
|
675
|
+
// below is `gIsFall`'s — so `scSharedIsTaken` says where the shared arm went and
|
|
676
|
+
// `scSharedOnFall` says which source arm it is. The shared arm in the FALL slot means the
|
|
677
|
+
// taken slot holds the source's `then` whatever `scSharedOnFall` reads, so only the TAKEN
|
|
678
|
+
// quadrants can negate; `structure.ts` owns that table and the layout premise under it.
|
|
679
|
+
//
|
|
680
|
+
// `scEdgeRelayed` — the THIRD fact, and the only one of the three that is neither the
|
|
681
|
+
// source's connective nor a slot. It says a long-branch TRAMPOLINE sat on one of this
|
|
682
|
+
// fold's two edges: agbcc inverts a conditional it cannot reach in ±256 bytes and leaves
|
|
683
|
+
// a `br`-only block behind, which is exactly the inversion that breaks `scSharedOnFall`'s
|
|
684
|
+
// source-order premise. Measured on the four orientation rows, lifted from their own
|
|
685
|
+
// compiled asm (`ONLY=synthetic:<row>:agbcc` through the probe in the round's ledger):
|
|
686
|
+
//
|
|
687
|
+
// row source onFall isTaken relayed /site-sense spells
|
|
688
|
+
// ifand_near a && b false true false a && b ✅
|
|
689
|
+
// ifor_near a || b true true false a || b ✅
|
|
690
|
+
// ifand_far a && b true false true a && b ✅
|
|
691
|
+
// ifor_far a || b false true TRUE its DUAL ❌ without this stamp
|
|
692
|
+
//
|
|
693
|
+
// `ifand_near` and `ifor_far` stamp the IDENTICAL pair and want OPPOSITE spellings, so no
|
|
694
|
+
// two of these booleans can decide the site. The relay is what separates them, and
|
|
695
|
+
// `synthetic:ifor_far` is the only row whose spelling it decides — the row exists for this
|
|
696
|
+
// stamp, because its SCORE cannot referee it (MATCH 0/139 on `/flip-join`, like
|
|
697
|
+
// `ifand_far`'s 0/140).
|
|
698
|
+
//
|
|
699
|
+
// A PROXY, and named as one: what decides the spelling is the branch INVERSION, and what
|
|
700
|
+
// is observable here is the trampoline the inversion leaves. They coincide on every input
|
|
701
|
+
// measured, and a relay arriving from some other cause at a taken-slot site would read as
|
|
702
|
+
// an inversion that did not happen. The `&&` long branch relays the SHARED edge and the
|
|
703
|
+
// `||` long branch the OTHER one — measured on `ifand_far` and `ifor_far`, and both
|
|
704
|
+
// layouts pinned in `test/branch-shortcircuit.test.ts` — which is why both are asked.
|
|
705
|
+
//
|
|
706
|
+
// POSITIONAL, unlike its two neighbours: `scSharedOnFall` and `scEdgeRelayed` name arms
|
|
707
|
+
// and survive anything, while `scSharedIsTaken` names a SUCCESSOR SLOT of the very op it
|
|
708
|
+
// rides on. THE INVARIANT IT NEEDS IS NOT "nothing assigns `.successors`" — something
|
|
709
|
+
// does. It is that no pass between here and `structure` may REORDER a stamped `cond_br`'s
|
|
710
|
+
// successor slots. Downstream of this fold `raiseRecovered` (pipeline.ts) runs
|
|
711
|
+
// `sinkReturns` and `foldEmptyLatches`; `raise/latch.ts` holds the one successor rewrite in
|
|
712
|
+
// `packages/core/src` (grep `successors[i] =` / `successors.push|splice`) —
|
|
713
|
+
// `op.successors[i] = { block: onward.block, … }`, which repoints a slot IN PLACE at its
|
|
714
|
+
// own index and therefore keeps the stamp true. `sinkReturns` only replaces `br`
|
|
715
|
+
// terminators. A pass that canonicalised a stamped `cond_br`'s edges would turn the
|
|
716
|
+
// reading over with no refusal firing, and would have to re-stamp.
|
|
717
|
+
...mkOp('cond_br', {
|
|
718
|
+
operands: [res],
|
|
719
|
+
attrs: {
|
|
720
|
+
scSharedOnFall: sharedEdge === gFall,
|
|
721
|
+
scSharedIsTaken: gIsFall,
|
|
722
|
+
scEdgeRelayed: throughRelay || forwardingTarget(otherEdge.block) !== otherEdge.block,
|
|
723
|
+
},
|
|
724
|
+
}),
|
|
605
725
|
successors: gIsFall
|
|
606
726
|
? [
|
|
607
727
|
{ block: sharedEdge.block, args: [...sharedEdge.args] },
|
|
@@ -655,7 +775,7 @@ export function recognizeBranchShortCircuit(fn: Fn, opts: BranchShortCircuitOpti
|
|
|
655
775
|
* a minted count is always ODD: 1, 3, 5, 7, 9. At 8 a FOUR-clause inner conjunct (7 ops) folds and
|
|
656
776
|
* a FIVE-clause one (9 ops) does not; 7 and 8 are therefore one gate, and so is 9 over everything
|
|
657
777
|
* measured here, the deepest cone in the 2,047 lifted klonoa+sa3 functions minting 5 (17
|
|
658
|
-
* connective negations, 0 refused). Clause COUNT is not the
|
|
778
|
+
* connective negations, 0 refused). Clause COUNT is not the measure either: a FLAT `a || b || c || …`
|
|
659
779
|
* chain pays nothing at all, because ^g's condition is never a connective in that shape.
|
|
660
780
|
*
|
|
661
781
|
* The bound is on ops KEPT, and the frontier is a NODE COUNT — not a shape. Pinned as such rather
|
|
@@ -803,32 +923,473 @@ function constTestScrutinee(defs: Map<Value, Op>, c: Value): Value | null {
|
|
|
803
923
|
return xc === yc ? null : xc ? y : x;
|
|
804
924
|
}
|
|
805
925
|
|
|
806
|
-
/**
|
|
926
|
+
/** `armRereadCone`'s answer: the ops the arm copies, and the originals that then leave ^g. */
|
|
927
|
+
interface ArmReread {
|
|
928
|
+
copy: ReadonlySet<Op>;
|
|
929
|
+
drop: ReadonlySet<Op>;
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
const readsOf = (op: Op): Value[] => [...op.operands, ...op.successors.flatMap((s) => s.args)];
|
|
933
|
+
|
|
934
|
+
/** What the arm must RE-DERIVE for the fold to go ahead: `copy` is every `g`-defined value read
|
|
935
|
+
* outside `g` with the part of its operand cone `g` computes, and `drop` the originals in `copy`
|
|
936
|
+
* nothing left in `g` reads — the ones that would MOVE rather than duplicate. Both empty when
|
|
937
|
+
* nothing escapes — then no gate is asked, because there is nothing to re-derive; null when a
|
|
938
|
+
* gate in `opts.armReread` (default {@link ARM_REREAD_GATES}) refuses the site.
|
|
939
|
+
*
|
|
940
|
+
* The copy is exact at the arm's head because nothing runs between: `g`'s body is pure (its
|
|
941
|
+
* caller refuses anything in HOIST_UNSAFE_OPS), so a load copied there reads what `g`'s own load
|
|
942
|
+
* read, and `g` is the arm's ONLY predecessor, so the copy runs exactly when the original's
|
|
943
|
+
* value would have been live there. On verified IR that also places every escaping read under
|
|
944
|
+
* the arm: `g` dominates nothing else, because the shared block is reached from ^h directly.
|
|
945
|
+
* That is the MEANING half, and it holds however analysis.ts spells the copy. It is not the bytes
|
|
946
|
+
* half — see ARM_REREAD_GATES' `read-behind-effect`. */
|
|
947
|
+
function armRereadCone(
|
|
948
|
+
fn: Fn,
|
|
949
|
+
g: Block,
|
|
950
|
+
arm: Block,
|
|
951
|
+
preds: Map<Block, Block[]>,
|
|
952
|
+
opts: BranchShortCircuitOptions,
|
|
953
|
+
): ArmReread | null {
|
|
954
|
+
const defOf = new Map<Value, Op>();
|
|
955
|
+
for (const op of g.ops) {
|
|
956
|
+
for (const r of op.results) {
|
|
957
|
+
defOf.set(r, op);
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
const readers = fn.blocks
|
|
961
|
+
.filter((b) => b !== g)
|
|
962
|
+
.flatMap((b) => b.ops.filter((op) => readsOf(op).some((v) => defOf.has(v))));
|
|
963
|
+
if (readers.length === 0) {
|
|
964
|
+
return { copy: new Set(), drop: new Set() };
|
|
965
|
+
}
|
|
966
|
+
const site = armRereadSite(fn, g, arm, preds, defOf, readers, opts.reloadsLocalReread === true);
|
|
967
|
+
return firstRejection(opts.armReread ?? ARM_REREAD_GATES, site) === null
|
|
968
|
+
? { copy: site.copy, drop: site.drop }
|
|
969
|
+
: null;
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
/** One site the RE-READ admission judges: a condition block ^g that defines a value something past
|
|
973
|
+
* it reads, and the arm ^g's non-shared edge enters — where that value would be re-derived.
|
|
974
|
+
*
|
|
975
|
+
* THE FIELDS ARE LAZY. `copy` walks the operand cone and `drop` the whole of ^g, and the two
|
|
976
|
+
* cheapest gates refuse without asking either. */
|
|
977
|
+
export interface ArmRereadSite {
|
|
978
|
+
readonly fn: Fn;
|
|
979
|
+
/** ^g, the second condition block the fold would merge into ^h */
|
|
980
|
+
readonly g: Block;
|
|
981
|
+
/** the block ^g's non-shared edge enters — where the copy goes */
|
|
982
|
+
readonly arm: Block;
|
|
983
|
+
readonly preds: ReadonlyMap<Block, readonly Block[]>;
|
|
984
|
+
/** the op in ^g defining each value ^g defines */
|
|
985
|
+
readonly defOf: ReadonlyMap<Value, Op>;
|
|
986
|
+
/** every op outside ^g that reads a value ^g defines — never empty (see `armRereadCone`) */
|
|
987
|
+
readonly readers: readonly Op[];
|
|
988
|
+
/** those values and the part of their operand cone ^g computes: what the arm would copy */
|
|
989
|
+
readonly copy: ReadonlySet<Op>;
|
|
990
|
+
/** the originals in `copy` nothing left in ^g reads — deleted from ^g, so they MOVE to the arm.
|
|
991
|
+
* Reverse order, so an original that fed only another dropped original goes too. */
|
|
992
|
+
readonly drop: ReadonlySet<Op>;
|
|
993
|
+
/** the target's compiler loads a LOCAL initialised with a read its test already performed a
|
|
994
|
+
* second time (`BranchShortCircuitOptions.reloadsLocalReread`) */
|
|
995
|
+
readonly targetReloadsLocalReread: boolean;
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
function armRereadSite(
|
|
999
|
+
fn: Fn,
|
|
1000
|
+
g: Block,
|
|
1001
|
+
arm: Block,
|
|
1002
|
+
preds: Map<Block, Block[]>,
|
|
1003
|
+
defOf: Map<Value, Op>,
|
|
1004
|
+
readers: Op[],
|
|
1005
|
+
targetReloadsLocalReread: boolean,
|
|
1006
|
+
): ArmRereadSite {
|
|
1007
|
+
let copy: Set<Op> | undefined;
|
|
1008
|
+
let drop: Set<Op> | undefined;
|
|
1009
|
+
const coneOf = (): Set<Op> => {
|
|
1010
|
+
const out = new Set<Op>();
|
|
1011
|
+
for (const work = readers.flatMap(readsOf).filter((v) => defOf.has(v)); work.length;) {
|
|
1012
|
+
const d = defOf.get(work.pop()!);
|
|
1013
|
+
if (d && !out.has(d)) {
|
|
1014
|
+
out.add(d);
|
|
1015
|
+
work.push(...d.operands);
|
|
1016
|
+
}
|
|
1017
|
+
}
|
|
1018
|
+
return out;
|
|
1019
|
+
};
|
|
1020
|
+
const dropOf = (c: ReadonlySet<Op>): Set<Op> => {
|
|
1021
|
+
const out = new Set<Op>();
|
|
1022
|
+
const live = new Set<Value>();
|
|
1023
|
+
for (let i = g.ops.length - 1; i >= 0; i--) {
|
|
1024
|
+
const op = g.ops[i];
|
|
1025
|
+
if (c.has(op) && op.results.every((r) => !live.has(r))) {
|
|
1026
|
+
out.add(op);
|
|
1027
|
+
} else {
|
|
1028
|
+
readsOf(op).forEach((v) => live.add(v));
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
return out;
|
|
1032
|
+
};
|
|
1033
|
+
return {
|
|
1034
|
+
fn,
|
|
1035
|
+
g,
|
|
1036
|
+
arm,
|
|
1037
|
+
preds,
|
|
1038
|
+
defOf,
|
|
1039
|
+
readers,
|
|
1040
|
+
targetReloadsLocalReread,
|
|
1041
|
+
get copy() {
|
|
1042
|
+
return (copy ??= coneOf());
|
|
1043
|
+
},
|
|
1044
|
+
get drop() {
|
|
1045
|
+
return (drop ??= dropOf(this.copy));
|
|
1046
|
+
},
|
|
1047
|
+
};
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
/** Whether leaving ^g for `arm` leaves a natural loop ^g is in — some back-edge `t→h` (h dominates
|
|
1051
|
+
* t) whose body, h plus everything reaching t without passing h, holds ^g and not the arm. */
|
|
1052
|
+
function leavesALoop(fn: Fn, g: Block, arm: Block, preds: ReadonlyMap<Block, readonly Block[]>): boolean {
|
|
1053
|
+
const dom = dominators(fn);
|
|
1054
|
+
for (const t of fn.blocks) {
|
|
1055
|
+
for (const h of successorsOf(t)) {
|
|
1056
|
+
if (!dom.get(t)?.has(h)) {
|
|
1057
|
+
continue;
|
|
1058
|
+
}
|
|
1059
|
+
const body = new Set<Block>([h]);
|
|
1060
|
+
for (const work = [t]; work.length;) {
|
|
1061
|
+
const b = work.pop()!;
|
|
1062
|
+
if (!body.has(b)) {
|
|
1063
|
+
body.add(b);
|
|
1064
|
+
work.push(...(preds.get(b) ?? []));
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
1067
|
+
if (body.has(g) && !body.has(arm)) {
|
|
1068
|
+
return true;
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
return false;
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
/** Whether analysis.ts would spell the arm's copy of a READ as a LOCAL rather than inline — a
|
|
1076
|
+
* MIRROR of analysis.ts's placement and barrier rules (structure/analysis.ts: `emitPos`, the
|
|
1077
|
+
* single-render `isBarrier`, the multi-render `isWrite`), not a coarser stand-in for them. It is
|
|
1078
|
+
* separate code, so the two can drift: the DIFFERENTIAL in the matching suite
|
|
1079
|
+
* (shortcircuit-branch.test.ts) is what holds them together. The copy sits at the arm's head, so
|
|
1080
|
+
* every question is about the arm block.
|
|
1081
|
+
*
|
|
1082
|
+
* Only the copied READS are asked about. An address member of the cone (`add`, `gaddr`) re-derives
|
|
1083
|
+
* at every use and carries nothing, however the address is spelled — so `p[i]` and `p[5]` get the
|
|
1084
|
+
* same answer. A read answers yes when:
|
|
1085
|
+
*
|
|
1086
|
+
* - it renders nowhere single: some PURE value between it and a statement has two consumers, so
|
|
1087
|
+
* `emitPos` is null and analysis.ts materializes it whatever stands in the way
|
|
1088
|
+
* (`p[2] = p[1] & 0x80; p[3] = p[1] & 0x80;` — one `and`, two stores);
|
|
1089
|
+
* - it has MORE THAN ONE direct reader, and an effect precedes one of their render positions —
|
|
1090
|
+
* the multi-render rule, `isWrite = EFFECTFUL_OPS`, which exempts nothing;
|
|
1091
|
+
* - it has ONE, and an effect precedes its render position that analysis.ts's `isBarrier`
|
|
1092
|
+
* counts: any `astore`/`opaque`, a `call` that is not rendered inside that same statement,
|
|
1093
|
+
* and a `store` UNLESS it is to a provably disjoint slot of the read's own base
|
|
1094
|
+
* (`disjointConstSlots`, ir/alias.ts — the helper `isBarrier` calls).
|
|
1095
|
+
*
|
|
1096
|
+
* A reader in any block other than the arm answers yes — the copy then outlives the arm's first
|
|
1097
|
+
* block. What this does NOT mirror, and which way each gap errs, is ARM_REREAD_GATES' RESIDUE. */
|
|
1098
|
+
function readHeldAcrossEffect(c: ArmRereadSite): boolean {
|
|
1099
|
+
const reads = [...c.copy].filter((op) => ORDER_SENSITIVE_OPS.has(op.opcode));
|
|
1100
|
+
if (reads.length === 0) {
|
|
1101
|
+
return false;
|
|
1102
|
+
}
|
|
1103
|
+
// The arm as analysis.ts will see it: the copied cone at its head (position 0 — nothing runs
|
|
1104
|
+
// before it), then the arm's own ops. A copied op's consumers are the arm ops and the OTHER copied
|
|
1105
|
+
// ops reading it, so a read feeding a copied `add` the arm reads renders where that `add` does.
|
|
1106
|
+
const cone = c.g.ops.filter((op) => c.copy.has(op));
|
|
1107
|
+
const posOf = (op: Op): number => Math.max(0, c.arm.ops.indexOf(op));
|
|
1108
|
+
const escapes = (vs: readonly Value[]): boolean =>
|
|
1109
|
+
c.fn.blocks.some((b) => b !== c.g && b !== c.arm && b.ops.some((op) => readsOf(op).some((v) => vs.includes(v))));
|
|
1110
|
+
const consumersOf = (vs: readonly Value[]): Op[] =>
|
|
1111
|
+
[...cone, ...c.arm.ops].filter((op) => readsOf(op).some((v) => vs.includes(v)));
|
|
1112
|
+
// `emitPos`: a statement renders where it stands, a value where its one consumer renders. A pure
|
|
1113
|
+
// value with two consumers renders in both — null. A load or call with two is one analysis.ts
|
|
1114
|
+
// names (a call executes once), so it renders where it stands. A consumer outside the arm leaves
|
|
1115
|
+
// the position unresolved — null, the other-block clause.
|
|
1116
|
+
const at = (op: Op): number | null => {
|
|
1117
|
+
if (op.successors.length > 0 || op.opcode === 'ret' || op.results.length === 0) {
|
|
1118
|
+
return posOf(op);
|
|
1119
|
+
}
|
|
1120
|
+
if (escapes(op.results)) {
|
|
1121
|
+
return null;
|
|
1122
|
+
}
|
|
1123
|
+
const cs = consumersOf(op.results);
|
|
1124
|
+
if (cs.length === 0) {
|
|
1125
|
+
return posOf(op);
|
|
1126
|
+
}
|
|
1127
|
+
if (cs.length === 1) {
|
|
1128
|
+
return at(cs[0]);
|
|
1129
|
+
}
|
|
1130
|
+
return ORDER_SENSITIVE_OPS.has(op.opcode) ? posOf(op) : null;
|
|
1131
|
+
};
|
|
1132
|
+
for (const l of reads) {
|
|
1133
|
+
if (escapes(l.results)) {
|
|
1134
|
+
return true;
|
|
1135
|
+
}
|
|
1136
|
+
const renders = consumersOf(l.results).map(at);
|
|
1137
|
+
if (renders.some((i) => i === null)) {
|
|
1138
|
+
return true;
|
|
1139
|
+
}
|
|
1140
|
+
const multi = renders.length > 1;
|
|
1141
|
+
const bars = (x: Op, pos: number): boolean => {
|
|
1142
|
+
if (!EFFECTFUL_OPS.has(x.opcode)) {
|
|
1143
|
+
return false;
|
|
1144
|
+
}
|
|
1145
|
+
if (multi) {
|
|
1146
|
+
return true;
|
|
1147
|
+
}
|
|
1148
|
+
if (x.opcode === 'store') {
|
|
1149
|
+
return !(l.opcode === 'load' && disjointConstSlots(l, x));
|
|
1150
|
+
}
|
|
1151
|
+
if (x.opcode === 'call') {
|
|
1152
|
+
return at(x) !== pos;
|
|
1153
|
+
}
|
|
1154
|
+
return true;
|
|
1155
|
+
};
|
|
1156
|
+
if (renders.some((pos) => c.arm.ops.slice(0, pos!).some((x) => bars(x, pos!)))) {
|
|
1157
|
+
return true;
|
|
1158
|
+
}
|
|
1159
|
+
}
|
|
1160
|
+
return false;
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
// ── THE RE-READ ADMISSION'S REFUSALS, AS DATA ─────────────────────────────────────────────────
|
|
1164
|
+
//
|
|
1165
|
+
// A table so its refusals can be counted: taken as an OPTIONAL parameter
|
|
1166
|
+
// (`BranchShortCircuitOptions.armReread`), and censused from outside core by
|
|
1167
|
+
// `pnpm bench gates --pass arm-reread`, which swaps this pass's
|
|
1168
|
+
// entry in `PRE_RECOVERY_PASSES` (apps/benchmark/src/run/gate-census.ts). The table is asked only
|
|
1169
|
+
// at a site `sameArgs` and the negatability check have already passed, so a count is its own.
|
|
1170
|
+
//
|
|
1171
|
+
// SOUND is two rules — `entry-arm` and `second-pred` put the copy on a path the original never
|
|
1172
|
+
// ran. The other five are FIDELITY: each refuses a site where re-deriving costs bytes the nest did
|
|
1173
|
+
// not, and a wrong answer there is a miss, never wrong C, because the copy stays under the guard
|
|
1174
|
+
// either way.
|
|
1175
|
+
//
|
|
1176
|
+
// RESIDUE, named because it is not in the table:
|
|
1177
|
+
//
|
|
1178
|
+
// - the USE-COUNT half of `definedValuesStayLocal` — a value ^g reads twice itself is
|
|
1179
|
+
// materialized before the `if` — which judges ^g's own reads rather than this site's
|
|
1180
|
+
// re-derivation, and runs after the table on every fold, escaping or not.
|
|
1181
|
+
// - what `read-behind-effect` does not mirror of analysis.ts, of two kinds that err opposite
|
|
1182
|
+
// ways. (1) `/reread-globals`. Under that STRUCTURE variation analysis.ts lets a store to a
|
|
1183
|
+
// DIFFERENT named global through (`mayWriteGlobal`, ir/alias.ts), so there the copy inlines —
|
|
1184
|
+
// but this pass runs once per LIFT, and rank.ts structures every `/reread-globals` candidate
|
|
1185
|
+
// from a lift it shares with the others (`liftSettings`), so the verdict cannot follow the
|
|
1186
|
+
// variation without a lift of its own. It errs toward REFUSING, at a measured price:
|
|
1187
|
+
// `if (a && (gQ2[1] & 0x7f) == 0x7f) { gK = 1; gQ2[1] &= 0x80; return; } fnB();` keeps a 3/23
|
|
1188
|
+
// nest where the fold matches on that variation (MATCH 0/21 with this rule ablated), and so does the
|
|
1189
|
+
// same arm over a struct global's member; no corpus row. (2) the scopes analysis.ts
|
|
1190
|
+
// materializes a read in for reasons OTHER than a barrier — `liveAcrossLoop`, the address-home
|
|
1191
|
+
// variation, a load fed to a `cond_br` edge. They err toward ADMITTING: a local, under the guard,
|
|
1192
|
+
// one extra load on agbcc; no inhabitant measured. The DIFFERENTIAL in
|
|
1193
|
+
// packages/cli/test/matching/shortcircuit-branch.test.ts is what notices either one growing.
|
|
1194
|
+
//
|
|
1195
|
+
// MEASURED over the whole corpus, every row's default lift and whole enumerated fan with its
|
|
1196
|
+
// symbol map, both tiers (LBG and ProcessInputAndUpdateEntities default lift only): something
|
|
1197
|
+
// escapes ^g at a site on 10 rows. Against main the default source or the fan moves on 6 —
|
|
1198
|
+
// synthetic `ladder4`/`ladder5`/`ladidx1`/`ladidx2`, kleod `CountCollectedGems`, sa3
|
|
1199
|
+
// `EwramFree` — and on no other row; `nestinit` and `ucmp:mwcc_242_81` are refused here and come
|
|
1200
|
+
// out as main did, and `CheckTileCollisionVertical` and `TrySetCantSelectMoveBattleScript` are
|
|
1201
|
+
// refused at every site and come out as main did too — held
|
|
1202
|
+
// there by `loop-exit`, `moves-a-read` and `read-behind-effect`, whose prices outside the corpus
|
|
1203
|
+
// are on each rule. `read-behind-effect`'s target scoping and its analysis.ts mirror move nothing
|
|
1204
|
+
// in the corpus — default source and fan byte-identical on all 784 synthetic and 252 real rows,
|
|
1205
|
+
// and the same verdict at every site — so what they buy is off the corpus, on the rule.
|
|
1206
|
+
export const ARM_REREAD_GATES: readonly Gate<ArmRereadSite>[] = [
|
|
1207
|
+
{
|
|
1208
|
+
id: 'entry-arm',
|
|
1209
|
+
why: 'an arm that is the function’s first block is also reached on entry, where the copy would run too',
|
|
1210
|
+
sound: true,
|
|
1211
|
+
guardedBy: 'branch-shortcircuit.test.ts: REFUSED: a value the ARM re-reads, when the arm is the entry block',
|
|
1212
|
+
rejects: (c) => c.arm === c.fn.blocks[0],
|
|
1213
|
+
},
|
|
1214
|
+
{
|
|
1215
|
+
// With readers present the second predecessor can only be one ^g's copy dominates — a loop
|
|
1216
|
+
// back into the arm, re-running the re-read against memory the loop has since written.
|
|
1217
|
+
id: 'second-pred',
|
|
1218
|
+
why: 'a second way into the arm would run the copy on a path that never ran the original',
|
|
1219
|
+
sound: true,
|
|
1220
|
+
guardedBy: 'branch-shortcircuit.test.ts: REFUSED: a value the ARM re-reads, when the arm has a second predecessor',
|
|
1221
|
+
rejects: (c) => (c.preds.get(c.arm) ?? []).length !== 1,
|
|
1222
|
+
},
|
|
1223
|
+
{
|
|
1224
|
+
// The loop-guard shape, `for (i = 0; i < n; …)` under an `if`: agbcc tests the register it just
|
|
1225
|
+
// set to the induction's start (`mov r3, #0; cmp r3, r5`) and carries that register into the
|
|
1226
|
+
// loop, so ^g's condition reads a VARIABLE the arm goes on to own. A copy re-derives the
|
|
1227
|
+
// constant and the fold hands the outer `if` a `0 < n` loop recovery needed as its own guard
|
|
1228
|
+
// — `synthetic:nestinit:agbcc` MATCH → 6/49.
|
|
1229
|
+
id: 'edge-arg',
|
|
1230
|
+
why: 'a value an edge carries is a variable the arm owns, not an expression it re-reads',
|
|
1231
|
+
sound: false,
|
|
1232
|
+
guardedBy: 'branch-shortcircuit.test.ts: REFUSED: a value the ARM carries on as an edge ARGUMENT',
|
|
1233
|
+
rejects: (c) => c.readers.some((op) => op.successors.some((s) => s.args.some((v) => c.defOf.has(v)))),
|
|
1234
|
+
},
|
|
1235
|
+
{
|
|
1236
|
+
// A value the SSA builder homed in a frame slot (`fn.slotHomes`) is one the target held in a
|
|
1237
|
+
// DECLARED local and reloads in the arm (`ldr rN, [sp, #k]`). The copy re-derives it instead,
|
|
1238
|
+
// and the copy's result carries no home, so the local and its `[sp, #k]` traffic are spelled
|
|
1239
|
+
// away and the frame order `l3/slotorder.ts` reads loses that name — silently, the failure
|
|
1240
|
+
// `replaceAllUsesWith` (ir/core.ts) ships an equally inhabitant-less guard against. Those are
|
|
1241
|
+
// the bytes it protects. NO INHABITANT in either tier or in any probe written against this
|
|
1242
|
+
// table, and stated rather than argued away: only Thumb stamps homes, and on agbcc a value
|
|
1243
|
+
// spilled in ^g is one live across a call in the arm, which `read-behind-effect` refuses on
|
|
1244
|
+
// its own unless the copy is a pure ADDRESS — the only shape where this rule alone would decide.
|
|
1245
|
+
id: 'slot-home',
|
|
1246
|
+
why: 'the machine kept this value in a stack slot rather than re-deriving it',
|
|
1247
|
+
sound: false,
|
|
1248
|
+
guardedBy: 'branch-shortcircuit.test.ts: REFUSED: a value the ARM re-reads, when the frame homed it',
|
|
1249
|
+
rejects: (c) => [...c.copy].some((op) => op.results.some((r) => c.fn.slotHomes?.has(r))),
|
|
1250
|
+
},
|
|
1251
|
+
{
|
|
1252
|
+
// A loop body's `if (a && … && d) { …; return; }`: the fold fuses the WHOLE condition into the
|
|
1253
|
+
// loop header, whose out-edge is then this early-return arm, and loop recovery reads it as the
|
|
1254
|
+
// loop's exit and declines the latch's (`unrecovered back-edge`). The shape is not this
|
|
1255
|
+
// admission's — the same loop with an arm that re-reads nothing folds the same way and
|
|
1256
|
+
// declines on main too — but this admission is what reached it: a probe of kleod
|
|
1257
|
+
// `CheckTileCollisionVertical`'s shape (not a corpus row) went diff:11/57 → declined, and the
|
|
1258
|
+
// real row's eight `/reread-globals` candidates all threw, which `bench run` does not report.
|
|
1259
|
+
// Refusing the re-read at a loop exit keeps exactly the nest main produced there. No row this
|
|
1260
|
+
// admission matches has its arm outside a loop ^g is in.
|
|
1261
|
+
//
|
|
1262
|
+
// What it protects that no corpus row pins is the ordinary SEARCH LOOP: `for (i = 0; i < 8;
|
|
1263
|
+
// i++) { if (q[i] != 0 && (v = p[i]) > 5) { r[0] = v; return; } } fnB();` matches as the nest
|
|
1264
|
+
// and scores 21/33 fused, and its `p[i] &= 0x80` twin MATCH against 31/37. The first is in the
|
|
1265
|
+
// matching suite (shortcircuit-branch.test.ts), which fails with this rule ablated.
|
|
1266
|
+
id: 'loop-exit',
|
|
1267
|
+
why: "fusing a loop body's whole condition makes this arm the header's exit, which loop recovery misreads",
|
|
1268
|
+
sound: false,
|
|
1269
|
+
guardedBy: 'branch-shortcircuit.test.ts: REFUSED: a value the ARM re-reads, when the arm LEAVES the loop',
|
|
1270
|
+
rejects: (c) => leavesALoop(c.fn, c.g, c.arm, c.preds),
|
|
1271
|
+
},
|
|
1272
|
+
{
|
|
1273
|
+
// `if (a) { v = p->f; if (b) use(v); }`: the target reads `p->f` on BOTH exits of `b`, before
|
|
1274
|
+
// `b`'s own reads; moved, it runs on one, after them. No row this admission matches moves
|
|
1275
|
+
// anything (the census: every fire site's `drop` is empty), and moving the read cost
|
|
1276
|
+
// the probe `if (gA) { v = gP->f[5]; if (gQ == 3) sink(v); }` its MATCH (0/20 → 15/24) and
|
|
1277
|
+
// turned a device-register read into one on one path only. Moving a PURE original stays: it
|
|
1278
|
+
// re-derives at every use whatever this does.
|
|
1279
|
+
id: 'moves-a-read',
|
|
1280
|
+
why: 'the target read it before the second test on both of its exits; moved, it runs on one',
|
|
1281
|
+
sound: false,
|
|
1282
|
+
guardedBy: 'branch-shortcircuit.test.ts: REFUSED: a READ only the arm consumes would move under the second test',
|
|
1283
|
+
rejects: (c) => [...c.drop].some((op) => ORDER_SENSITIVE_OPS.has(op.opcode)),
|
|
1284
|
+
},
|
|
1285
|
+
{
|
|
1286
|
+
// THE BYTES HALF of the copy (see `armRereadCone`), and a claim about ONE compiler. The arm of
|
|
1287
|
+
// `if (a && (p[1] & 0x7f) == 0x7f) { … }` loads `p[1]` again or not by SPELLING — loads of
|
|
1288
|
+
// `p[1]`, compiled at each bench toolchain's flags:
|
|
1289
|
+
//
|
|
1290
|
+
// agbcc ido7.1 gcc2.7.2kmc gcc2.7.2 mwcc_242_81
|
|
1291
|
+
// p[1] &= 0x80; 1 1 1 1 1
|
|
1292
|
+
// q[0] = 5; p[1] &= 0x80; 1 2 2 2 2
|
|
1293
|
+
// p[3] = 5; p[1] &= 0x80; 1 1 2 2 2
|
|
1294
|
+
// fnB(); p[2] = p[1]; 2 2 2 2 2
|
|
1295
|
+
// u8 v = p[1]; p[2] = v; 2 1 1 1 1
|
|
1296
|
+
// u8 v = p[1]; q[0] = 5; p[2] = v; 2 1 1 1 1
|
|
1297
|
+
// u8 v = p[1]; fnB(); p[2] = v; 2 1 1 1 1
|
|
1298
|
+
//
|
|
1299
|
+
// A LOCAL at the arm's head costs agbcc a second load and costs the other four nothing — the
|
|
1300
|
+
// target's `compilerBehaviors.reloadsLocalReread` (target.ts), the conjunct below. analysis.ts
|
|
1301
|
+
// spells the copy inline unless something stands between it and a use, and otherwise
|
|
1302
|
+
// materializes a local at the arm's head: correct, under the guard, and on agbcc a second
|
|
1303
|
+
// load. So on agbcc a read analysis.ts would materialize is refused, and the nest main spells
|
|
1304
|
+
// stays — `{ fnB(); sink(v); }`, `{ sink(v); sink(v); }` and `{ gP->f[3] = 0; sink(v); }` each
|
|
1305
|
+
// match only as the nest. Elsewhere the local IS the target's spelling: on mwcc,
|
|
1306
|
+
// `if (a) { u8 v = p[3]; if ((v & 0x7f) == 0x7f) { fnA(); p[4] = v; return; } } fnB();` was
|
|
1307
|
+
// 3/24 with this rule running there and matches without it.
|
|
1308
|
+
//
|
|
1309
|
+
// WHICH copies analysis.ts materializes is asked with analysis.ts's own rules
|
|
1310
|
+
// (`readHeldAcrossEffect`): only the copied READS, never the address arithmetic beside them,
|
|
1311
|
+
// and a store to a provably disjoint slot is no barrier. The coarser rule — ANY effect bars,
|
|
1312
|
+
// seeded from every copied op — holds 13 agbcc probes at 3 to 8 points that byte-match under
|
|
1313
|
+
// this one (`r->x = 5; r->fl &= 0x80;`, `p[3] = 5; p[1] &= 0x80;`, `p[i] &= 0x80; fnB(); p[i]
|
|
1314
|
+
// = 1;` among them), scores 8 more worse, and scores none of 249 probes over five toolchains
|
|
1315
|
+
// better. What this one does not mirror is the RESIDUE above.
|
|
1316
|
+
id: 'read-behind-effect',
|
|
1317
|
+
why: 'a re-read written as a local costs a second load on a target whose compiler reloads one',
|
|
1318
|
+
sound: false,
|
|
1319
|
+
guardedBy: 'branch-shortcircuit.test.ts: REFUSED: a READ the arm holds across an effect is not re-derived',
|
|
1320
|
+
rejects: (c) => c.targetReloadsLocalReread && readHeldAcrossEffect(c),
|
|
1321
|
+
},
|
|
1322
|
+
];
|
|
1323
|
+
|
|
1324
|
+
/** Copy `copy` to the head of `arm`, point every read outside `g` at the copies, and delete `drop`.
|
|
1325
|
+
* A dropped original is deleted rather than left for the hoist: the arm now performs it, and a
|
|
1326
|
+
* dead READ left in ^h is one the structurer may still spell (SPELLED_WHEN_DEAD_OPS). */
|
|
1327
|
+
function rereadInArm(fn: Fn, g: Block, arm: Block, { copy, drop }: ArmReread): void {
|
|
1328
|
+
const copyOf = new Map<Value, Value>();
|
|
1329
|
+
const copies = g.ops
|
|
1330
|
+
.filter((op) => copy.has(op))
|
|
1331
|
+
.map((op): Op => ({
|
|
1332
|
+
opcode: op.opcode,
|
|
1333
|
+
operands: op.operands.map((v) => copyOf.get(v) ?? v),
|
|
1334
|
+
results: op.results.map((r) => {
|
|
1335
|
+
const c = mkValue(r.type);
|
|
1336
|
+
copyOf.set(r, c);
|
|
1337
|
+
return c;
|
|
1338
|
+
}),
|
|
1339
|
+
attrs: { ...op.attrs },
|
|
1340
|
+
successors: [],
|
|
1341
|
+
}));
|
|
1342
|
+
const redirect = (v: Value): Value => copyOf.get(v) ?? v;
|
|
1343
|
+
for (const b of fn.blocks) {
|
|
1344
|
+
if (b === g) {
|
|
1345
|
+
continue;
|
|
1346
|
+
}
|
|
1347
|
+
for (const op of b.ops) {
|
|
1348
|
+
op.operands = op.operands.map(redirect);
|
|
1349
|
+
for (const s of op.successors) {
|
|
1350
|
+
s.args = s.args.map(redirect);
|
|
1351
|
+
}
|
|
1352
|
+
}
|
|
1353
|
+
}
|
|
1354
|
+
arm.ops.unshift(...copies);
|
|
1355
|
+
g.ops = g.ops.filter((op) => !drop.has(op));
|
|
1356
|
+
}
|
|
1357
|
+
|
|
1358
|
+
/** True when every value `g` defines is read at most once, and any read is inside `g` or is one
|
|
1359
|
+
* `armRereadCone` answered for — reads outside `g` move to the arm's copy, and reads BY a
|
|
1360
|
+
* dropped original leave with it. The use-count half is the residue ARM_REREAD_GATES names.
|
|
807
1361
|
*
|
|
808
1362
|
* The VALUE form above needs no such check, and the asymmetry is real rather than drift: its feeder
|
|
809
1363
|
* ends in `br M`, so the feeder has no successor of its own to dominate and every value it defines
|
|
810
1364
|
* is either read in the feeder or carried to `M` as the phi argument the fold consumes. Here ^g
|
|
811
1365
|
* ends in `cond_br` and its `other` successor IS ^g-dominated, so a ^g-defined value genuinely can
|
|
812
|
-
* escape, and only this check
|
|
1366
|
+
* escape, and only this check and the arm's copy stop it.
|
|
813
1367
|
*
|
|
814
1368
|
* What the two folds genuinely DO share is the `fn.blocks[0]` refusal, and each has a test that
|
|
815
1369
|
* pins its own half: 'a feeder that is the entry block is not folded away' for the value form, and
|
|
816
1370
|
* 'the ENTRY block is never folded away' for the branch form. They are deliberately NOT routed
|
|
817
1371
|
* through one shared `isEntry` helper — a helper enforces nothing, and it is the two tests that
|
|
818
1372
|
* hold each fold to the refusal. When changing either fold, check the other. */
|
|
819
|
-
function definedValuesStayLocal(fn: Fn, g: Block): boolean {
|
|
1373
|
+
function definedValuesStayLocal(fn: Fn, g: Block, { copy, drop }: ArmReread): boolean {
|
|
820
1374
|
const defined = new Set<Value>(g.ops.flatMap((op) => op.results));
|
|
821
1375
|
if (defined.size === 0) {
|
|
822
1376
|
return true;
|
|
823
1377
|
}
|
|
1378
|
+
const copied = new Set<Value>([...copy].flatMap((op) => op.results));
|
|
824
1379
|
const uses = new Map<Value, number>();
|
|
825
1380
|
for (const b of fn.blocks) {
|
|
826
1381
|
for (const op of b.ops) {
|
|
827
|
-
|
|
1382
|
+
if (drop.has(op)) {
|
|
1383
|
+
continue;
|
|
1384
|
+
}
|
|
1385
|
+
for (const v of readsOf(op)) {
|
|
828
1386
|
if (!defined.has(v)) {
|
|
829
1387
|
continue;
|
|
830
1388
|
}
|
|
831
1389
|
if (b !== g) {
|
|
1390
|
+
if (copied.has(v)) {
|
|
1391
|
+
continue; // re-derived in the arm (`rereadInArm`)
|
|
1392
|
+
}
|
|
832
1393
|
return false; // escapes ^g — the structurer would render it before the `if`
|
|
833
1394
|
}
|
|
834
1395
|
uses.set(v, (uses.get(v) ?? 0) + 1);
|