@asmlift/core 0.10.0 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/backend/c.ts +1 -0
- package/src/backend/cfamily.ts +33 -12
- package/src/backend/cpp.ts +1 -0
- package/src/backend/lines.ts +10 -0
- package/src/backend/pascal.ts +17 -5
- package/src/cdecl/parse.ts +21 -0
- package/src/contracts.ts +73 -20
- package/src/frontend/arm-body.ts +155 -0
- package/src/frontend/asmdata.ts +2 -1
- package/src/frontend/call-plan.ts +197 -45
- package/src/frontend/callee-body.ts +370 -0
- package/src/frontend/device-pins.ts +18 -8
- package/src/frontend/frame-objects.ts +504 -61
- package/src/frontend/frontend.ts +20 -0
- package/src/frontend/jump-table.ts +35 -20
- package/src/frontend/mips.ts +226 -8
- package/src/frontend/ppc.ts +26 -2
- package/src/frontend/register-struct-call.ts +1 -86
- package/src/frontend/ssa.ts +263 -1
- package/src/frontend/stackargs.ts +95 -17
- package/src/frontend/thumb-entry.ts +340 -0
- package/src/frontend/thumb.ts +410 -61
- package/src/ir/alias.ts +29 -8
- package/src/ir/bits.ts +1081 -66
- package/src/ir/core.ts +154 -4
- package/src/ir/entry.ts +3 -5
- package/src/ir/opcodes.ts +28 -0
- package/src/ir/parse.ts +1 -0
- package/src/ir/simplify.ts +61 -10
- package/src/ir/verify.ts +16 -0
- package/src/l3/argbase.ts +2 -0
- package/src/l3/ast.ts +294 -2
- package/src/l3/basecse.ts +35 -10
- package/src/l3/caseendgoto.ts +223 -0
- package/src/l3/coalesce.ts +42 -14
- package/src/l3/constinit.ts +34 -6
- package/src/l3/dce.ts +52 -8
- package/src/l3/ptrcell.ts +2 -0
- package/src/l3/regspell.ts +2 -0
- package/src/l3/reindex.ts +13 -4
- package/src/l3/scopebase.ts +2 -0
- package/src/l3/tailmerge.ts +2 -0
- package/src/pipeline.ts +507 -23
- package/src/proto-context.ts +135 -1
- package/src/raise/const.ts +25 -0
- package/src/raise/declared-volatile.ts +1 -1
- package/src/raise/divpow2.ts +192 -0
- package/src/raise/offsetnames.ts +412 -10
- package/src/raise/pre-recovery.ts +8 -5
- package/src/raise/pre-structure.ts +24 -5
- package/src/raise/reroll.ts +4 -24
- package/src/raise/shortcircuit.ts +192 -23
- package/src/raise/unpre.ts +594 -0
- package/src/rank-variations.ts +97 -4
- package/src/rank.ts +258 -43
- package/src/structure/analysis.ts +56 -10
- package/src/structure/cross-jumped-tail.ts +216 -0
- package/src/structure/globalaccess.ts +46 -1
- package/src/structure/structure.ts +4221 -368
- package/src/structure/switch-recover.ts +345 -26
- package/src/structure/unfolded-branch-constant.ts +90 -0
- package/src/symbols.ts +52 -15
- package/src/target.ts +269 -8
- package/src/trace.ts +28 -14
- package/src/variation-definitions.ts +207 -6
- package/src/variation-gates.ts +2 -0
- package/src/variation-tokens.ts +63 -2
|
@@ -40,7 +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
|
+
import { constDisplacementOf, disjointConstSlots } from '../ir/alias';
|
|
44
44
|
import {
|
|
45
45
|
type AttrVal,
|
|
46
46
|
Block,
|
|
@@ -49,7 +49,7 @@ import {
|
|
|
49
49
|
Successor,
|
|
50
50
|
Value,
|
|
51
51
|
defOpMap,
|
|
52
|
-
|
|
52
|
+
dominance,
|
|
53
53
|
foldWriteOrder,
|
|
54
54
|
forwardingTarget,
|
|
55
55
|
mkOp,
|
|
@@ -59,8 +59,15 @@ import {
|
|
|
59
59
|
replaceAllUsesWith,
|
|
60
60
|
successorsOf,
|
|
61
61
|
} from '../ir/core';
|
|
62
|
-
import {
|
|
63
|
-
|
|
62
|
+
import {
|
|
63
|
+
effectful,
|
|
64
|
+
forgetHelperPlacement,
|
|
65
|
+
orderSensitive,
|
|
66
|
+
placedAt,
|
|
67
|
+
reevalUnsafe,
|
|
68
|
+
speculationUnsafe,
|
|
69
|
+
} from '../ir/discipline';
|
|
70
|
+
import { MEM_BASE_OPS, NEGATED_FCMP, NEGATED_ICMP } from '../ir/opcodes';
|
|
64
71
|
import { T } from '../ir/types';
|
|
65
72
|
import { type Gate, firstRejection } from '../l3/gates';
|
|
66
73
|
|
|
@@ -371,11 +378,14 @@ export function recognizeShortCircuit(fn: Fn): boolean {
|
|
|
371
378
|
// (`af:adds:ido7.1` and the `divv`/`gcd`/`modv` rows).
|
|
372
379
|
// - ^g holds a side effect — its ops move into ^h, which runs UNCONDITIONALLY. A store in `b`
|
|
373
380
|
// would then execute even when `a` already decided the branch. (`a || (*p = 1)`.)
|
|
374
|
-
// - a value defined in ^g is used more than once inside ^g
|
|
375
|
-
// into a local, which renders as a statement BEFORE
|
|
376
|
-
// computation into an unconditional one. Single-use-and-
|
|
377
|
-
// analysis.ts inlines into the connective's right operand, where
|
|
378
|
-
// re-guards it.
|
|
381
|
+
// - a value defined in ^g is used more than once inside ^g, and `SHARED_VALUE_GATES` refuses it.
|
|
382
|
+
// Such a value the structurer MATERIALIZES into a local, which renders as a statement BEFORE
|
|
383
|
+
// the `if` — turning `b`'s conditional computation into an unconditional one. Single-use-and-
|
|
384
|
+
// local is precisely the shape analysis.ts inlines into the connective's right operand, where
|
|
385
|
+
// C's own short-circuit re-guards it. The table is what keeps a load in `b` from being hoisted
|
|
386
|
+
// across the guard in `a`; what it admits is an ADDRESS (`&gS`, `&gS + k`), which reads nothing,
|
|
387
|
+
// so wherever it is spelled it runs nothing the source did not (l3/basecse.ts's
|
|
388
|
+
// `guarded-operand` keeps the default spelling inline under the operand).
|
|
379
389
|
// - a value defined in ^g is ALSO read past ^g, and a rule in `ARM_REREAD_GATES` refuses
|
|
380
390
|
// re-deriving it in the arm. A read past ^g is otherwise not a refusal: on verified IR it can
|
|
381
391
|
// only sit under the arm ^g's non-shared edge enters (^g dominates nothing else — the shared
|
|
@@ -545,6 +555,9 @@ export interface BranchShortCircuitOptions {
|
|
|
545
555
|
* in {@link ARM_REREAD_GATES} reads — `read-behind-effect`. Threaded by raise/pre-recovery.ts from
|
|
546
556
|
* the target it already holds, the way `narrowlocal`'s `hoistsSingleSetArm` is. Absent ⇒ false. */
|
|
547
557
|
reloadsLocalReread?: boolean;
|
|
558
|
+
/** The USE-COUNT refusals — {@link SHARED_VALUE_GATES} when absent. A parameter for the same two
|
|
559
|
+
* readers as `armReread`: a census (`bench gates --pass shared-value`) and a test's ablation. */
|
|
560
|
+
sharedValue?: readonly Gate<SharedValueSite>[];
|
|
548
561
|
}
|
|
549
562
|
|
|
550
563
|
export function recognizeBranchShortCircuit(fn: Fn, opts: BranchShortCircuitOptions = {}): boolean {
|
|
@@ -640,7 +653,7 @@ export function recognizeBranchShortCircuit(fn: Fn, opts: BranchShortCircuitOpti
|
|
|
640
653
|
// `r0` values. The verdict is a conjunction of pure tests, so the order moves no fold. And
|
|
641
654
|
// BEFORE tree ownership, so `onTreeOwned` keeps its meaning — the one thing in the way.
|
|
642
655
|
const reread = armRereadCone(fn, g, otherEdge.block, preds, opts);
|
|
643
|
-
if (!reread || !definedValuesStayLocal(fn, g, reread)) {
|
|
656
|
+
if (!reread || !definedValuesStayLocal(fn, g, reread, defs, opts.sharedValue ?? SHARED_VALUE_GATES)) {
|
|
644
657
|
continue;
|
|
645
658
|
}
|
|
646
659
|
// LAST refusal, so `onTreeOwned` reports a site where tree ownership is the ONE thing in the
|
|
@@ -1071,10 +1084,28 @@ function armRereadSite(
|
|
|
1071
1084
|
/** Whether leaving ^g for `arm` leaves a natural loop ^g is in — some back-edge `t→h` (h dominates
|
|
1072
1085
|
* t) whose body, h plus everything reaching t without passing h, holds ^g and not the arm. */
|
|
1073
1086
|
function leavesALoop(fn: Fn, g: Block, arm: Block, preds: ReadonlyMap<Block, readonly Block[]>): boolean {
|
|
1074
|
-
|
|
1087
|
+
return naturalLoopBodies(fn, preds).some((body) => body.has(g) && !body.has(arm));
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
/** The natural loop bodies of `fn`'s CFG as `preds` describes it, one per back-edge `t→h` (h
|
|
1091
|
+
* dominates t): h plus everything reaching t without passing h.
|
|
1092
|
+
*
|
|
1093
|
+
* MEMOISED ON THE PREDECESSOR MAP. `recognizeBranchShortCircuit` builds one `preds` per scan of the
|
|
1094
|
+
* function and rescans after every fold, so one map stands for one CFG: every site that scan asks
|
|
1095
|
+
* `loop-exit` about shares one dominator computation instead of paying one each. That was the cost
|
|
1096
|
+
* of a large function's plain lift: Klonoa's sub_0800D188 (1,783 blocks) spent most of its first
|
|
1097
|
+
* five minutes rebuilding these dominator sets per site. A caller that mutates the CFG under a map
|
|
1098
|
+
* it keeps would read stale loops, as it already reads stale predecessors. */
|
|
1099
|
+
function naturalLoopBodies(fn: Fn, preds: ReadonlyMap<Block, readonly Block[]>): readonly Set<Block>[] {
|
|
1100
|
+
const cached = LOOP_BODIES.get(preds);
|
|
1101
|
+
if (cached !== undefined && cached.fn === fn) {
|
|
1102
|
+
return cached.bodies;
|
|
1103
|
+
}
|
|
1104
|
+
const dominates = dominance(fn);
|
|
1105
|
+
const bodies: Set<Block>[] = [];
|
|
1075
1106
|
for (const t of fn.blocks) {
|
|
1076
1107
|
for (const h of successorsOf(t)) {
|
|
1077
|
-
if (!
|
|
1108
|
+
if (!dominates(h, t)) {
|
|
1078
1109
|
continue;
|
|
1079
1110
|
}
|
|
1080
1111
|
const body = new Set<Block>([h]);
|
|
@@ -1085,13 +1116,13 @@ function leavesALoop(fn: Fn, g: Block, arm: Block, preds: ReadonlyMap<Block, rea
|
|
|
1085
1116
|
work.push(...(preds.get(b) ?? []));
|
|
1086
1117
|
}
|
|
1087
1118
|
}
|
|
1088
|
-
|
|
1089
|
-
return true;
|
|
1090
|
-
}
|
|
1119
|
+
bodies.push(body);
|
|
1091
1120
|
}
|
|
1092
1121
|
}
|
|
1093
|
-
|
|
1122
|
+
LOOP_BODIES.set(preds, { fn, bodies });
|
|
1123
|
+
return bodies;
|
|
1094
1124
|
}
|
|
1125
|
+
const LOOP_BODIES = new WeakMap<ReadonlyMap<Block, readonly Block[]>, { fn: Fn; bodies: readonly Set<Block>[] }>();
|
|
1095
1126
|
|
|
1096
1127
|
/** Whether analysis.ts would spell the arm's copy of a READ as a LOCAL rather than inline — a
|
|
1097
1128
|
* MIRROR of analysis.ts's placement and barrier rules (structure/analysis.ts: `emitPos`, the
|
|
@@ -1391,9 +1422,10 @@ function rereadInArm(fn: Fn, g: Block, arm: Block, { copy, drop }: ArmReread): v
|
|
|
1391
1422
|
g.ops = g.ops.filter((op) => !drop.has(op));
|
|
1392
1423
|
}
|
|
1393
1424
|
|
|
1394
|
-
/** True when
|
|
1395
|
-
* `
|
|
1396
|
-
* dropped original leave with it. The use-count half is the residue
|
|
1425
|
+
/** True when no value `g` defines escapes it un-re-derived, and every value `g` reads more than
|
|
1426
|
+
* once itself passes {@link SHARED_VALUE_GATES} (or `gates`) — reads outside `g` move to the arm's
|
|
1427
|
+
* copy, and reads BY a dropped original leave with it. The use-count half is the residue
|
|
1428
|
+
* ARM_REREAD_GATES names, and the table below is that half.
|
|
1397
1429
|
*
|
|
1398
1430
|
* The VALUE form above needs no such check, and the asymmetry is real rather than drift: its feeder
|
|
1399
1431
|
* ends in `br M`, so the feeder has no successor of its own to dominate and every value it defines
|
|
@@ -1406,7 +1438,13 @@ function rereadInArm(fn: Fn, g: Block, arm: Block, { copy, drop }: ArmReread): v
|
|
|
1406
1438
|
* 'the ENTRY block is never folded away' for the branch form. They are deliberately NOT routed
|
|
1407
1439
|
* through one shared `isEntry` helper — a helper enforces nothing, and it is the two tests that
|
|
1408
1440
|
* hold each fold to the refusal. When changing either fold, check the other. */
|
|
1409
|
-
function definedValuesStayLocal(
|
|
1441
|
+
function definedValuesStayLocal(
|
|
1442
|
+
fn: Fn,
|
|
1443
|
+
g: Block,
|
|
1444
|
+
{ copy, drop }: ArmReread,
|
|
1445
|
+
defs: ReadonlyMap<Value, Op>,
|
|
1446
|
+
gates: readonly Gate<SharedValueSite>[],
|
|
1447
|
+
): boolean {
|
|
1410
1448
|
const defined = new Set<Value>(g.ops.flatMap((op) => op.results));
|
|
1411
1449
|
if (defined.size === 0) {
|
|
1412
1450
|
return true;
|
|
@@ -1433,11 +1471,142 @@ function definedValuesStayLocal(fn: Fn, g: Block, { copy, drop }: ArmReread): bo
|
|
|
1433
1471
|
}
|
|
1434
1472
|
}
|
|
1435
1473
|
// ZERO uses is fine — a dead op renders nothing at all, so it cannot escape the short circuit.
|
|
1436
|
-
//
|
|
1437
|
-
|
|
1438
|
-
return [...defined].every(
|
|
1474
|
+
// ONE inlines into the connective's operand. TWO or more is the table's question.
|
|
1475
|
+
const defOf = new Map<Value, Op>(g.ops.flatMap((op) => op.results.map((r): [Value, Op] => [r, op])));
|
|
1476
|
+
return [...defined].every(
|
|
1477
|
+
(value) => (uses.get(value) ?? 0) <= 1 || firstRejection(gates, { g, value, defOf, defs }) === null,
|
|
1478
|
+
);
|
|
1479
|
+
}
|
|
1480
|
+
|
|
1481
|
+
/** One value ^g reads more than once itself — what analysis.ts would otherwise spell as a local. */
|
|
1482
|
+
export interface SharedValueSite {
|
|
1483
|
+
/** ^g, the second condition block the fold would merge into ^h */
|
|
1484
|
+
readonly g: Block;
|
|
1485
|
+
/** the value, defined in ^g and read there 2+ times */
|
|
1486
|
+
readonly value: Value;
|
|
1487
|
+
/** the op in ^g defining each value ^g defines */
|
|
1488
|
+
readonly defOf: ReadonlyMap<Value, Op>;
|
|
1489
|
+
/** the defining op of every value in the function — an address's symbol and offsets are often
|
|
1490
|
+
* defined ABOVE ^g, where GVN numbered them with an earlier use */
|
|
1491
|
+
readonly defs: ReadonlyMap<Value, Op>;
|
|
1492
|
+
}
|
|
1493
|
+
|
|
1494
|
+
/** The part of `v`'s operand cone ^g computes — the ops a hoisted local would carry above the `if`. */
|
|
1495
|
+
function gConeOf(c: SharedValueSite): Op[] {
|
|
1496
|
+
const out = new Set<Op>();
|
|
1497
|
+
for (const work = [c.value]; work.length;) {
|
|
1498
|
+
const d = c.defOf.get(work.pop()!);
|
|
1499
|
+
if (d && !out.has(d)) {
|
|
1500
|
+
out.add(d);
|
|
1501
|
+
work.push(...d.operands);
|
|
1502
|
+
}
|
|
1503
|
+
}
|
|
1504
|
+
return [...out];
|
|
1505
|
+
}
|
|
1506
|
+
|
|
1507
|
+
/** Is `v` constant offsets over a symbol or frame address (`&gS + 0x35`), or over a literal address
|
|
1508
|
+
* or a value ^g did not compute (`p - 0x35` off a pointer parameter) that ^g reads only as a
|
|
1509
|
+
* memory ADDRESS? The chain is ir/alias.ts `constDisplacementOf`, the one reading of address
|
|
1510
|
+
* arithmetic, so Thumb's `sub r0, r0, #0x35` for a negative member offset ends where `add` does.
|
|
1511
|
+
* Asked over the WHOLE function's defs, not ^g's: agbcc's `add r0, r2, #0x35` off an `&gS` an
|
|
1512
|
+
* earlier block loaded is GVN's one `gaddr` (`sub_0800D188`'s site). A root ^g did not compute (a
|
|
1513
|
+
* parameter, an earlier block's result) is, by dominance, already computed when the first test
|
|
1514
|
+
* runs, so the cone spelled above the guard (as a local, or l3/basecse.ts's hoisted base) adds only
|
|
1515
|
+
* `± k` arithmetic, which reads nothing and cannot trap. That root is admitted only when `v` is
|
|
1516
|
+
* read as an address alone (`readAsAddress`): the pointer-parameter base is measured (agbcc's
|
|
1517
|
+
* `add r0, r4, #0x35; ldrb; add r0, r0, #0x22` off `struct T *p` folds to the target's bytes,
|
|
1518
|
+
* synthetic:scbase_pfar), while a counter's `i + 1` read twice by a latch's compare is not an
|
|
1519
|
+
* address, and folding it turns synthetic:retpost_notail on gcc2.7.2 and gcc2.7.2kmc into a
|
|
1520
|
+
* structure decline. */
|
|
1521
|
+
function constOffsetCone(c: SharedValueSite): boolean {
|
|
1522
|
+
const disp = constDisplacementOf(c.defs, c.value);
|
|
1523
|
+
const root = disp?.root ?? c.value;
|
|
1524
|
+
const leaf = c.defs.get(root);
|
|
1525
|
+
if (leaf?.opcode === 'gaddr' || leaf?.opcode === 'laddr') {
|
|
1526
|
+
return true;
|
|
1527
|
+
}
|
|
1528
|
+
return (readsNothing(leaf) || (disp !== null && !c.defOf.has(root))) && readAsAddress(c.g, c.value);
|
|
1529
|
+
}
|
|
1530
|
+
|
|
1531
|
+
/** Is `leaf` an operand-free op that reads nothing and cannot trap — a value spelled the same at
|
|
1532
|
+
* any program point? The property the root argument above rests on, asked of the op rather than
|
|
1533
|
+
* of a list of opcodes: agbcc's `ldr r0, =0x03005220` under `#define gL (*(struct S *)0x03005220)`
|
|
1534
|
+
* is a `const` the second test reads three members off, exactly as it reads them off `&gS`. Unlike
|
|
1535
|
+
* a symbol it is admitted only where read as an address (`readAsAddress`): a mask or a compare
|
|
1536
|
+
* constant read twice is not a base, and its spelling is unmeasured. */
|
|
1537
|
+
function readsNothing(leaf: Op | undefined): boolean {
|
|
1538
|
+
return leaf !== undefined && leaf.operands.length === 0 && !reevalUnsafe(leaf);
|
|
1539
|
+
}
|
|
1540
|
+
|
|
1541
|
+
/** Does every read ^g makes of `v` end in a memory access's ADDRESS: the base of a load or store,
|
|
1542
|
+
* or the pointer side of `v + x` / `v - x` whose result is in turn read only that way? An index
|
|
1543
|
+
* may be a variable (`p->arr[i]` is `add(p + 0x35, i)`), since it is the sum, not `v`, that it
|
|
1544
|
+
* joins. A SUCCESSOR ARGUMENT is a value read — a loop-carried `p + 1` is the next iteration's
|
|
1545
|
+
* pointer, a latch's `i + 1` its counter — and so is a compare, a mask, or a stored value. */
|
|
1546
|
+
function readAsAddress(g: Block, v: Value): boolean {
|
|
1547
|
+
return g.ops.every((op) => {
|
|
1548
|
+
if (!readsOf(op).includes(v)) {
|
|
1549
|
+
return true;
|
|
1550
|
+
}
|
|
1551
|
+
if (op.successors.some((s) => s.args.includes(v))) {
|
|
1552
|
+
return false;
|
|
1553
|
+
}
|
|
1554
|
+
const [first, ...rest] = op.operands;
|
|
1555
|
+
if (MEM_BASE_OPS.has(op.opcode)) {
|
|
1556
|
+
return first === v && !rest.includes(v);
|
|
1557
|
+
}
|
|
1558
|
+
const once = op.operands.length === 2 && op.operands.filter((x) => x === v).length === 1;
|
|
1559
|
+
if (once && (op.opcode === 'add' || (op.opcode === 'sub' && first === v))) {
|
|
1560
|
+
return op.results.every((r) => readAsAddress(g, r));
|
|
1561
|
+
}
|
|
1562
|
+
return false;
|
|
1563
|
+
});
|
|
1439
1564
|
}
|
|
1440
1565
|
|
|
1566
|
+
// ── THE USE-COUNT REFUSALS, AS DATA ──────────────────────────────────────────────────────────
|
|
1567
|
+
//
|
|
1568
|
+
// A value ^g reads twice itself is one analysis.ts materializes into a local, and a local is a
|
|
1569
|
+
// statement BEFORE the `if`: `b`'s computation runs whether or not `a` decided the branch. Taken as
|
|
1570
|
+
// an OPTIONAL parameter (`BranchShortCircuitOptions.sharedValue`) and censused from outside core by
|
|
1571
|
+
// `pnpm bench gates --pass shared-value`, through the same `PRE_RECOVERY_PASSES` seam as
|
|
1572
|
+
// `arm-reread`. Asked once per such value, below every other refusal of the fold but tree ownership.
|
|
1573
|
+
//
|
|
1574
|
+
// What the table ADMITS is a shared ADDRESS: agbcc loads `&gS` into one register and reads three
|
|
1575
|
+
// members off it (`(gS.a | gS.d | gS.c) != 0`), so the address is the one value with three readers.
|
|
1576
|
+
// Refused, the `||` splits into a nest and the structurer copies the shared arm AND everything after
|
|
1577
|
+
// it into both branches, once per such site (kleod's `sub_0800D188`, whose `&gUnk_03005220 + 0x35`
|
|
1578
|
+
// is the shape synthetic:scbase_far pins). The base may also be a literal address, a pointer
|
|
1579
|
+
// parameter (`p->a | p->d`) or another value computed before ^g, when ^g reads it only as an
|
|
1580
|
+
// address: a constant-offset cone over it reads nothing and cannot trap, so hoisting it changes no
|
|
1581
|
+
// behaviour.
|
|
1582
|
+
//
|
|
1583
|
+
// RESIDUE, not in the table: the ESCAPE half of `definedValuesStayLocal` (a value read past ^g that
|
|
1584
|
+
// the arm does not re-derive). `armRereadCone` copies the cone of every outside read, so on the
|
|
1585
|
+
// inputs it accepts that half has nothing left to see; it stays as the check that keeps it so.
|
|
1586
|
+
export const SHARED_VALUE_GATES: readonly Gate<SharedValueSite>[] = [
|
|
1587
|
+
{
|
|
1588
|
+
// `if (p == 0 || (*p | *p >> 4) != 0)`: the read, materialized, runs above the null test.
|
|
1589
|
+
// `if (b == 0 || (q = a / b) + q != 0)`: so does the divide, on the path the test kept it off.
|
|
1590
|
+
// A named local is an UNCONDITIONAL statement, so the predicate is `reevalUnsafe` (effects,
|
|
1591
|
+
// reads AND traps), not `orderSensitive`, which has no trap half.
|
|
1592
|
+
id: 'shared-read',
|
|
1593
|
+
why: 'a read or trapping op the second test makes twice would be spelled as a local, run before the first test',
|
|
1594
|
+
sound: true,
|
|
1595
|
+
guardedBy: 'branch-shortcircuit.test.ts: REFUSED: a READ the second test consumes twice would run above the first',
|
|
1596
|
+
rejects: (c) => gConeOf(c).some(reevalUnsafe),
|
|
1597
|
+
},
|
|
1598
|
+
{
|
|
1599
|
+
// A pure, non-trapping value that is not an address `constOffsetCone` admits — `(x * x) != 0`
|
|
1600
|
+
// over a computed `x`. Hoisting it is correct C; what the target spells there is unmeasured, so
|
|
1601
|
+
// it keeps the nest. A pure SPELLING rule: disjoint from `shared-read` (a read or a trap in the cone
|
|
1602
|
+
// is that rule's), so each ablates alone and ablating this one admits nothing unsafe.
|
|
1603
|
+
id: 'shared-pure',
|
|
1604
|
+
why: 'a computed value the second test reuses is kept in the nest until its spelling is measured',
|
|
1605
|
+
sound: false,
|
|
1606
|
+
rejects: (c) => !constOffsetCone(c) && !gConeOf(c).some(reevalUnsafe),
|
|
1607
|
+
},
|
|
1608
|
+
];
|
|
1609
|
+
|
|
1441
1610
|
/** Two successor argument lists that a fold may collapse into one: same values, same order. */
|
|
1442
1611
|
function sameArgs(a: Value[], b: Value[]): boolean {
|
|
1443
1612
|
return a.length === b.length && a.every((v, i) => v === b[i]);
|