@asmlift/core 0.5.0 → 0.6.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 +22 -16
- package/package.json +1 -1
- package/src/backend/c.ts +1 -0
- package/src/backend/cfamily.ts +238 -167
- package/src/backend/cpp.ts +1 -0
- package/src/backend/pascal.ts +26 -12
- package/src/contracts.ts +194 -39
- package/src/declare.ts +41 -4
- package/src/frontend/mips.ts +11 -0
- package/src/frontend/ppc.ts +43 -7
- package/src/frontend/ssa.ts +404 -29
- package/src/frontend/thumb.ts +2176 -686
- package/src/ir/alias.ts +54 -0
- package/src/ir/bits.ts +75 -0
- package/src/ir/core.ts +337 -2
- package/src/ir/opcodes.ts +140 -21
- package/src/ir/parse.ts +19 -2
- package/src/ir/print.ts +27 -2
- package/src/ir/simplify.ts +190 -3
- package/src/ir/struct-names.ts +42 -0
- package/src/ir/verify.ts +43 -49
- package/src/l3/address.ts +62 -0
- package/src/l3/argbase.ts +2 -1
- package/src/l3/ast.ts +464 -57
- package/src/l3/basecse.ts +664 -76
- package/src/l3/coalesce.ts +429 -43
- package/src/l3/dce.ts +31 -9
- package/src/l3/gates.ts +21 -0
- package/src/l3/hoist.ts +293 -14
- package/src/l3/homesplit.ts +285 -0
- package/src/l3/initfirst.ts +301 -0
- package/src/l3/inlinebase.ts +193 -0
- package/src/l3/mentions.ts +113 -0
- package/src/l3/mulfirst.ts +42 -0
- package/src/l3/nearbase.ts +152 -0
- package/src/l3/offmember.ts +371 -0
- package/src/l3/parkfirst.ts +96 -0
- package/src/l3/pollguard.ts +154 -0
- package/src/l3/ptrfield.ts +227 -0
- package/src/l3/regspell.ts +110 -85
- package/src/l3/reindex.ts +715 -78
- package/src/l3/scopebase.ts +644 -218
- package/src/l3/sinkinit.ts +40 -0
- package/src/l3/slotorder.ts +123 -0
- package/src/l3/storage.ts +48 -0
- package/src/l3/symbol-refs.ts +41 -8
- package/src/l3/tailmerge.ts +15 -0
- package/src/l3/typing.ts +198 -9
- package/src/l3/unmerge.ts +263 -0
- package/src/l3/unreduce.ts +971 -0
- package/src/l3/volatileptr.ts +207 -0
- package/src/l3/volatileval.ts +130 -0
- package/src/l3/volstore.ts +229 -0
- package/src/l3/zerosub.ts +62 -0
- package/src/pattern/engine.ts +236 -13
- package/src/pipeline.ts +157 -56
- package/src/proto.ts +112 -14
- package/src/raise/arrays.ts +6 -1
- package/src/raise/divpow2.ts +2 -2
- package/src/raise/globalshape.ts +1038 -0
- package/src/raise/gvn.ts +33 -18
- package/src/raise/latch.ts +126 -0
- package/src/raise/memberarrays.ts +594 -0
- package/src/raise/narrow.ts +124 -0
- package/src/raise/narrowlocal.ts +556 -0
- package/src/raise/paramwidth.ts +179 -0
- package/src/raise/pre-recovery.ts +97 -14
- package/src/raise/recover.ts +56 -23
- package/src/raise/retsink.ts +210 -10
- package/src/raise/shortcircuit.ts +474 -74
- package/src/raise/struct-arrays.ts +19 -2
- package/src/raise/structs.ts +33 -3
- package/src/rank-axes.ts +630 -0
- package/src/rank-declare.ts +256 -0
- package/src/rank.ts +1723 -272
- package/src/structure/analysis.ts +1392 -141
- package/src/structure/bitfields.ts +332 -0
- package/src/structure/globalaccess.ts +274 -0
- package/src/structure/hazards.ts +411 -20
- package/src/structure/loops.ts +2 -49
- package/src/structure/namecoalesce.ts +435 -0
- package/src/structure/structure.ts +2678 -526
- package/src/structure/switch-recover.ts +616 -144
- package/src/symbols.ts +62 -1
- package/src/target.ts +367 -24
- package/src/trace.ts +111 -32
|
@@ -36,14 +36,45 @@
|
|
|
36
36
|
// folds bottom-up: the innermost diamond becomes a bare condition, which the next diamond consumes as its
|
|
37
37
|
// Vb, and so on. SCOPE: the shared-arm must be reachable as a single-predecessor `br` feeder; the `||`
|
|
38
38
|
// form where the const-1 "true" block has TWO predecessors (`return a || b`) is not folded.
|
|
39
|
-
// Guards stay conservative: the CONST is exactly 0/1, Vb is a bool op or 0/1 const, the head condition
|
|
40
|
-
//
|
|
41
|
-
|
|
39
|
+
// Guards stay conservative: the CONST is exactly 0/1, Vb is a bool op or 0/1 const, and the head condition
|
|
40
|
+
// is NEGATABLE whenever the orientation inverts it — an icmp by opcode swap or a `logic_and`/`logic_or` by
|
|
41
|
+
// De Morgan, both via `negateCondOps`, which the control-flow form below shares. Any deviation falls
|
|
42
|
+
// through untouched (a miss, never a miscompile).
|
|
43
|
+
import {
|
|
44
|
+
Block,
|
|
45
|
+
Fn,
|
|
46
|
+
Op,
|
|
47
|
+
Successor,
|
|
48
|
+
Value,
|
|
49
|
+
defOpMap,
|
|
50
|
+
foldWriteOrder,
|
|
51
|
+
forwardingTarget,
|
|
52
|
+
mkOp,
|
|
53
|
+
mkValue,
|
|
54
|
+
predecessors,
|
|
55
|
+
reachableBlocks,
|
|
56
|
+
replaceAllUsesWith,
|
|
57
|
+
} from '../ir/core';
|
|
42
58
|
import { HOIST_UNSAFE_OPS, NEGATED_ICMP } from '../ir/opcodes';
|
|
43
59
|
import { T } from '../ir/types';
|
|
44
60
|
|
|
45
61
|
const BOOL_OPS = new Set([...Object.keys(NEGATED_ICMP), 'logic_and', 'logic_or']);
|
|
46
62
|
|
|
63
|
+
/** Run `step` until it stops rewriting, and answer whether it ever did.
|
|
64
|
+
*
|
|
65
|
+
* Both folds in this file are driven the same way and have to be: `defOpMap` and `predecessors`
|
|
66
|
+
* are stale the moment a block is spliced or dropped, so a scan performs at most ONE rewrite and
|
|
67
|
+
* then starts over. `step` is that scan — it recomputes everything it reads, does at most one
|
|
68
|
+
* rewrite, and returns whether it did — which makes the rescan point a function boundary instead
|
|
69
|
+
* of a `break` to a label. */
|
|
70
|
+
function untilFixpoint(step: () => boolean): boolean {
|
|
71
|
+
let changed = false;
|
|
72
|
+
while (step()) {
|
|
73
|
+
changed = true;
|
|
74
|
+
}
|
|
75
|
+
return changed;
|
|
76
|
+
}
|
|
77
|
+
|
|
47
78
|
/** Fold `(-x | x) >> 31` (logical shift) → `x != 0`, in place. agbcc's branchless is-nonzero idiom. */
|
|
48
79
|
// NOT exported: it must run before the diamond fold, an ordering only recognizeShortCircuit's
|
|
49
80
|
// internal call preserves.
|
|
@@ -86,7 +117,6 @@ function recognizeBoolNormalize(fn: Fn): boolean {
|
|
|
86
117
|
|
|
87
118
|
/** Collapse a simple boolean short-circuit diamond into one `logic_and`/`logic_or`, in place. */
|
|
88
119
|
export function recognizeShortCircuit(fn: Fn): boolean {
|
|
89
|
-
let changed = recognizeBoolNormalize(fn);
|
|
90
120
|
const term = (b: Block) => b.ops[b.ops.length - 1];
|
|
91
121
|
const constOf = (defs: Map<Value, Op>, v: Value): number | null => {
|
|
92
122
|
const d = defs.get(v);
|
|
@@ -97,12 +127,15 @@ export function recognizeShortCircuit(fn: Fn): boolean {
|
|
|
97
127
|
return !!d && BOOL_OPS.has(d.opcode);
|
|
98
128
|
};
|
|
99
129
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
130
|
+
// TWO STATEMENTS, not `recognizeBoolNormalize(fn) || untilFixpoint(…)`: the normalisation must
|
|
131
|
+
// run BEFORE the fold (it is what turns agbcc's branchless is-nonzero into the `icmp_ne` the
|
|
132
|
+
// diamond's second operand has to be), and `||` would skip the fold whenever it reported a
|
|
133
|
+
// change. Both results are returned, because either one is a change to the IR.
|
|
134
|
+
const normalized = recognizeBoolNormalize(fn);
|
|
135
|
+
const folded = untilFixpoint(() => {
|
|
103
136
|
const defs = defOpMap(fn);
|
|
104
137
|
const preds = predecessors(fn);
|
|
105
|
-
|
|
138
|
+
for (const m of fn.blocks) {
|
|
106
139
|
if (m.params.length !== 1) {
|
|
107
140
|
continue;
|
|
108
141
|
}
|
|
@@ -124,9 +157,8 @@ export function recognizeShortCircuit(fn: Fn): boolean {
|
|
|
124
157
|
// below: `predecessors()` walks successor edges only, so an entry block that is also a loop
|
|
125
158
|
// header shows one predecessor while actually running BEFORE it on the first iteration.
|
|
126
159
|
// Hoisting its body then reorders it and deleting it moves `fn.blocks[0]`. Silent — verify,
|
|
127
|
-
// assertResolved and assertDerefsTyped all pass.
|
|
128
|
-
//
|
|
129
|
-
// note used to assert this one was safe.
|
|
160
|
+
// assertResolved and assertDerefsTyped all pass. Pinned by 'a feeder that is the entry block
|
|
161
|
+
// is not folded away'; the branch form has the same refusal and its own test.
|
|
130
162
|
if (bfeed === fn.blocks[0]) {
|
|
131
163
|
continue;
|
|
132
164
|
}
|
|
@@ -162,10 +194,24 @@ export function recognizeShortCircuit(fn: Fn): boolean {
|
|
|
162
194
|
continue;
|
|
163
195
|
}
|
|
164
196
|
const cond = ht.operands[0];
|
|
165
|
-
|
|
166
|
-
|
|
197
|
+
// The head condition must be a C BOOLEAN computed here — a negatable comparison or a
|
|
198
|
+
// `logic_and`/`logic_or`, which is exactly `BOOL_OPS` and exactly the set `negateCondOps`
|
|
199
|
+
// accepts at top level, so a fused connective head still folds. Mere def-existence is NOT
|
|
200
|
+
// the test — `call` declares `results: 1` (ir/opcodes.ts), so `defOpMap` maps a call result
|
|
201
|
+
// like any other — because the const/const reduction below hands `cond` ITSELF on as the
|
|
202
|
+
// merge value (`res = condSide`), replacing a phi that was `cond ? 1 : 0`. A non-boolean
|
|
203
|
+
// head there is a silent WRONG VALUE, not a missed fold: `and(6, 3)` emitted where the
|
|
204
|
+
// program yields 1. The branch form carries no such obligation — its result only ever feeds
|
|
205
|
+
// a `cond_br`, which reads truthiness — which is why this gate lives here and not in the
|
|
206
|
+
// shared helper, whose own result is boolean by construction and so covers the negated case.
|
|
207
|
+
//
|
|
208
|
+
// Instrumented over the whole benchmark under BOTH lift configurations, it refuses
|
|
209
|
+
// NOTHING — every head reaching it is a negatable icmp — while the pass folds 6 value-form
|
|
210
|
+
// diamonds per configuration, 3 of them through the const/const reduction. An invariant's
|
|
211
|
+
// guard, not a filter any row depends on.
|
|
212
|
+
if (!isBool(defs, cond)) {
|
|
167
213
|
continue;
|
|
168
|
-
}
|
|
214
|
+
}
|
|
169
215
|
|
|
170
216
|
// The `cond`-side operand is negated iff `cond` guards the short-circuit (taken+0 / fall+1).
|
|
171
217
|
const wantNeg = (c === 0 && mIsTaken) || (c === 1 && mIsFall);
|
|
@@ -180,11 +226,26 @@ export function recognizeShortCircuit(fn: Fn): boolean {
|
|
|
180
226
|
if (bfeed.ops.slice(0, -1).some((op) => HOIST_UNSAFE_OPS.has(op.opcode))) {
|
|
181
227
|
continue;
|
|
182
228
|
}
|
|
229
|
+
// NEGATABLE only when the orientation actually inverts the head — asked here rather than up
|
|
230
|
+
// with the other head checks, so a fused `logic_and`/`logic_or` head (what the branch form
|
|
231
|
+
// below leaves behind, and what an earlier round of THIS pass leaves behind in a chain) is
|
|
232
|
+
// not refused when nothing needs inverting. `negateCondOps` is the branch form's helper too,
|
|
233
|
+
// so both siblings refuse the same three shapes. Its dominance precondition holds here for a
|
|
234
|
+
// different reason than there: `cond` is ^h's OWN terminator operand, so its whole cone
|
|
235
|
+
// dominates the point `before` splices into.
|
|
236
|
+
//
|
|
237
|
+
// Minted below every other refusal and above the first mutation: a site refused for any
|
|
238
|
+
// other reason pays nothing, and a refusal below the hoist would leave ^h half-rewritten.
|
|
239
|
+
const negation = wantNeg ? negateCondOps(defs, cond, NEGATE_BUDGET) : null;
|
|
240
|
+
if (wantNeg && !negation) {
|
|
241
|
+
continue;
|
|
242
|
+
}
|
|
183
243
|
bfeed.ops.slice(0, -1).forEach(before); // hoist B's pure body (defines Vb; harmless if a dead const)
|
|
244
|
+
foldWriteOrder(fn.writeOrder, bfeed, h); // …and its writes now follow H's (ir/core.ts)
|
|
184
245
|
let condSide = cond;
|
|
185
|
-
if (
|
|
186
|
-
|
|
187
|
-
|
|
246
|
+
if (negation) {
|
|
247
|
+
negation.ops.forEach((op) => before(op));
|
|
248
|
+
condSide = negation.result;
|
|
188
249
|
}
|
|
189
250
|
// Vb const → the phi reduces to the (possibly negated) condition; Vb bool → a && / || connective.
|
|
190
251
|
let res = condSide;
|
|
@@ -203,13 +264,12 @@ export function recognizeShortCircuit(fn: Fn): boolean {
|
|
|
203
264
|
m.params = [];
|
|
204
265
|
}
|
|
205
266
|
fn.blocks = fn.blocks.filter((x) => x !== bfeed);
|
|
206
|
-
|
|
207
|
-
progress = true;
|
|
208
|
-
break outer; // defs/preds are stale after mutation — recompute on the next iteration
|
|
267
|
+
return true; // defs/preds are stale after mutation — the driver rescans
|
|
209
268
|
}
|
|
210
269
|
}
|
|
211
|
-
|
|
212
|
-
|
|
270
|
+
return false;
|
|
271
|
+
});
|
|
272
|
+
return normalized || folded;
|
|
213
273
|
}
|
|
214
274
|
|
|
215
275
|
// ── the CONTROL-FLOW form ───────────────────────────────────────────────────────────────────────
|
|
@@ -253,10 +313,51 @@ export function recognizeShortCircuit(fn: Fn): boolean {
|
|
|
253
313
|
// not a hand-written `||`. switch-recover.ts requires every test's `cond_br` operand to be an
|
|
254
314
|
// `icmp` (its `isCmpOpcode` gate), and a `logic_or` is not one, so folding first PERMANENTLY
|
|
255
315
|
// disqualifies the recovery and a clean `switch (x) { case 1: case 2: … }` degrades to a chain
|
|
256
|
-
// of nested `if`s. The
|
|
257
|
-
//
|
|
258
|
-
//
|
|
259
|
-
//
|
|
316
|
+
// of nested `if`s. The two spellings are mutually exclusive within one raise and BOTH are
|
|
317
|
+
// legitimate C, and which of them the asm rules out depends on the shape: `x == 0 || x == 2`
|
|
318
|
+
// and `switch (x) { case 0: case 2: }` are ONE object only where the switch has a single case
|
|
319
|
+
// group plus `default:` (agbcc 12 instructions each, one md5; IDO 64 bytes each, one md5), and
|
|
320
|
+
// part as soon as there is a second group to balance a dispatch against (agbcc 20 against 16,
|
|
321
|
+
// IDO 80 bytes and different bytes). So this is a DEFAULT rather than a decision: the switch
|
|
322
|
+
// is the more specific recovery and wins the shape here, while
|
|
323
|
+
// `foldTreeOwned` spells the connective instead and the differ referees (rank.ts's
|
|
324
|
+
// `/connective`). ONLY THIS CLAUSE. Its notion of "same scrutinee" is switch-recover.ts's own
|
|
325
|
+
// PRE1 — a NECESSARY condition for recovery, never a sufficient one, so what it refuses is
|
|
326
|
+
// "a switch could not be ruled out here" and it is a proxy too, just a far tighter one than
|
|
327
|
+
// the relayed clause. Priced over the set it REFUSES rather than the set it admits: it fires on
|
|
328
|
+
// 6 rows, protects 2 (`pokeemerald:IsStringLengthAtLeast`,
|
|
329
|
+
// `pokeemerald:TrySetCantSelectMoveBattleScript`), and on the other 4 the published winner
|
|
330
|
+
// 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 axis, not the clause,
|
|
332
|
+
// that keeps those 4. A structural discriminator is L1-visible and would be strictly better —
|
|
333
|
+
// is the shared block the entry of a region with dispatch-shaped in-edges, is the scrutinee
|
|
334
|
+
// defined by the enclosing loop header — and is UNBUILT.
|
|
335
|
+
// The relayed clause below is a different statement (see its own note: a blunt proxy that
|
|
336
|
+
// fires on an ordinary loop counter), it has NO inhabitant anywhere in the benchmark, and a
|
|
337
|
+
// candidate born there would carry a `/connective` label for a fold that answers
|
|
338
|
+
// no connective-vs-tree question. It stays absolute.
|
|
339
|
+
// - the shared block was reached through a RELAY, and either test's scrutinee is compared against
|
|
340
|
+
// constants more than once in the function. This one is ABSOLUTE — `foldTreeOwned` does not
|
|
341
|
+
// widen it. Same reason as the bullet above, widened because the reach is: a relay is what
|
|
342
|
+
// agbcc puts on a tree's default edge, so resolving one walks this fold into a dispatch chain,
|
|
343
|
+
// where the sibling that gives the tree away may be neither test in
|
|
344
|
+
// hand — the split node is RELATIONAL and only its children are equalities, which is precisely
|
|
345
|
+
// what the pairwise test cannot see. Without it `sub_807BD88` and `sub_808491C` each lose a
|
|
346
|
+
// `switch` (sa3).
|
|
347
|
+
//
|
|
348
|
+
// It is BLUNT, and that is why it is held to the relayed case. The count does not distinguish a
|
|
349
|
+
// dispatch chain from any variable tested against constants at two sites: on `sub_8080AD4` it
|
|
350
|
+
// fires on `v2 > 2` and `v2 != 2` — one ordinary loop counter — and refusing there costs the
|
|
351
|
+
// function its whole decompilation, because the only fold it has is the `do…while` exit. Its
|
|
352
|
+
// notion of "same scrutinee" is SSA-value identity, which is switch-recover.ts's own PRE1, so it
|
|
353
|
+
// cannot refuse a tree that recovery could not have taken either — but it is a proxy for "am I
|
|
354
|
+
// inside a dispatch chain", not a test of it. A direct edge onto a relational split node still
|
|
355
|
+
// escapes: `sub_807F334` folds `x > 1 && x == 2` and loses a `switch` on main and here alike.
|
|
356
|
+
// - BOTH of ^g's edges rejoin the shared block, with NEITHER of them landing on it directly. Then
|
|
357
|
+
// there is no "other" arm and nothing decides which side the connective guards. Only resolution
|
|
358
|
+
// creates this — an edge that arrives directly is preferred exactly so the MIPS divide-guard
|
|
359
|
+
// idiom, whose emptied trap block forwards to the same place, keeps folding as it always has
|
|
360
|
+
// (`af:adds:ido7.1` and the `divv`/`gcd`/`modv` rows).
|
|
260
361
|
// - ^g holds a side effect — its ops move into ^h, which runs UNCONDITIONALLY. A store in `b`
|
|
261
362
|
// would then execute even when `a` already decided the branch. (`a || (*p = 1)`.)
|
|
262
363
|
// - a value defined in ^g is used outside ^g, or used more than once. Then the structurer
|
|
@@ -267,21 +368,133 @@ export function recognizeShortCircuit(fn: Fn): boolean {
|
|
|
267
368
|
// - the two edges into the shared block carry DIFFERENT args. Only one edge survives the fold,
|
|
268
369
|
// so it can only carry one argument list; picking either would silently drop the other path's
|
|
269
370
|
// phi input.
|
|
270
|
-
// - ^g's two successors are the same block, or ^g's condition
|
|
271
|
-
// orientation needs negating.
|
|
371
|
+
// - ^g's two successors are the same block, or ^g's condition cannot be NEGATED when the
|
|
372
|
+
// orientation needs negating. `negateCondOps` decides that, over two shapes: a negatable
|
|
373
|
+
// `icmp_*` (the swapped opcode) and a `logic_and`/`logic_or` (De Morgan — the dual connective
|
|
374
|
+
// over recursively negated operands). The connective case is what lets a chain fold past its
|
|
375
|
+
// FIRST level: this pass is iterative, so by the time it tries an outer diamond the inner one is
|
|
376
|
+
// already fused and ^g's condition is a connective, which `NEGATED_ICMP` — a table over
|
|
377
|
+
// comparison opcodes — has no entry for. Refuse it and `a || !(b || c)` stops after one level at
|
|
378
|
+
// all 13 sites this fires on (`synthetic:llcmp:agbcc`, `:gcc2.7.2kmc`, and 11 real agbcc
|
|
379
|
+
// functions in klonoa+sa3); on the two benchmark rows the structurer then tail-duplicates the
|
|
380
|
+
// shared return into both arms. The helper's own refusals — no def, a non-negatable leaf
|
|
381
|
+
// ANYWHERE in the cone (no partial De Morgan), a cone over the node budget — are on the helper.
|
|
382
|
+
//
|
|
383
|
+
// De Morgan DUPLICATES leaf comparisons, so a duplicated leaf could gain a second consumer that
|
|
384
|
+
// analysis.ts renders as a statement BEFORE the `if` — the hazard the single-icmp negation
|
|
385
|
+
// always carried. Measured over those 11 BRANCH-form sites: NO site gains a local (one,
|
|
386
|
+
// `sub_80B7CD0`, loses one, 8 → 7) and every site that already decompiled gets SHORTER. Two
|
|
387
|
+
// (`sub_80930B8`, `sub_80932E0`) go from DECLINED to a full decompilation — folding a loop-exit
|
|
388
|
+
// connective removes the back-edge loop recovery was refusing — so they get LONGER (1 → 112 and
|
|
389
|
+
// 1 → 55 lines) and are the only sites whose local count rises at all, 0 → 22 and 0 → 7. A
|
|
390
|
+
// decompilation appearing, not a leaf escaping.
|
|
391
|
+
//
|
|
392
|
+
// That number is SCOPED to this fold and does not carry to the value form, which shares the
|
|
393
|
+
// helper but no use-count condition: `definedValuesStayLocal` (bottom of this file)
|
|
394
|
+
// independently forbids a ^g-defined value with a second consumer HERE, while the value form
|
|
395
|
+
// relies on the original cone dying to the pass list's own `dce: true`. There is nothing to
|
|
396
|
+
// transfer the number to yet either — instrumenting the value form over the whole benchmark
|
|
397
|
+
// under both lift configurations counts 6 folds per configuration, every head a single icmp.
|
|
398
|
+
//
|
|
399
|
+
// The `/connective` LIFT AXIS is a separate question from the default lift, and is unwidened:
|
|
400
|
+
// `onTreeOwned` below is what tells rank.ts the axis exists for a row, and this check sits ABOVE
|
|
401
|
+
// it. Over the whole benchmark under BOTH configurations rank.ts lifts with (`foldTreeOwned`
|
|
402
|
+
// false and true), against the same rows with the connective case ablated: the recovered IR
|
|
403
|
+
// moves on the same 2 rows under each, `onTreeOwned` fires on the same rows either way, and
|
|
404
|
+
// nothing new throws.
|
|
272
405
|
//
|
|
273
406
|
// Every refusal falls through untouched, leaving the tail-duplicated spelling — a miss, never a
|
|
274
407
|
// miscompile. Applied ITERATIVELY, so `a || b || c` folds left-to-right, each round consuming one
|
|
275
408
|
// more condition block.
|
|
276
|
-
|
|
277
|
-
|
|
409
|
+
//
|
|
410
|
+
// WHICH SPELLING, and why the fold alone does not decide it. `if (a && b) X else Y` and its dual
|
|
411
|
+
// `if (!a || !b) Y else X` are the same program and NOT the same bytes — agbcc lays the arms out in
|
|
412
|
+
// source order, so which was written is recorded in the branch senses. This rewrite keeps ^h's
|
|
413
|
+
// unchanged successor slot, so the connective comes out in the orientation those senses spell, and
|
|
414
|
+
// 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` axis
|
|
416
|
+
// is what asks for it on a RECONVERGING if — the default spells the layout reading and the axis
|
|
417
|
+
// 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 axis).
|
|
419
|
+
//
|
|
420
|
+
// What neither reaches is the MIXED spelling. `negateJoinedBranchSense` is a per-FUNCTION boolean,
|
|
421
|
+
// so the axis negates every joined `if` at once — and of the 28 real rows carrying the
|
|
422
|
+
// `short-circuit` tag, 16 hold two or more TWO-ARMED ifs (counted by `else`, which is what the
|
|
423
|
+
// axis's own `thenS.length && elseS.length` gate needs) and 12 hold two or more conditions
|
|
424
|
+
// carrying a connective. TWO-ARMED is the count that matters: both sense booleans exclude a
|
|
425
|
+
// one-armed `if` by construction, so a tally of `if (` of any kind is the wrong denominator.
|
|
426
|
+
// A per-SITE negation is the open lever; a gate on whether to ENUMERATE the axis does not reach
|
|
427
|
+
// it, and removes a spelling the differ would referee.
|
|
428
|
+
//
|
|
429
|
+
// The De Morgan negation below forecloses a third spelling, at a measured price: it DISTRIBUTES, so
|
|
430
|
+
// the leaves come out negated (`a || (!b && !c)`) and `a || !(b || c)` has no
|
|
431
|
+
// candidate — the IR has no `logic_not` to build one from (ir/opcodes.ts). Compiled both ways on
|
|
432
|
+
// the `a || (b && c)` guard shape at agbcc's default flags, the two source spellings assemble to
|
|
433
|
+
// 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 axis, not a bug in this fold.
|
|
435
|
+
//
|
|
436
|
+
// WHICH slot ^g lands in is decided by the asm's branch POLARITY, and on Thumb the branch RANGE
|
|
437
|
+
// decides the polarity — so the same source `&&` reaches this pass two different ways:
|
|
438
|
+
//
|
|
439
|
+
// short branch `beq shared` ^g is ^h's FALL → logic_or
|
|
440
|
+
// long branch `bne ^g / b shared` ^g is ^h's TAKEN → logic_and
|
|
441
|
+
//
|
|
442
|
+
// agbcc inverts a conditional it cannot reach, so past ±256 bytes it emits the second form, and the
|
|
443
|
+
// trampoline it leaves on the `b` sits on the edge into the SHARED block — which `forwardingTarget`
|
|
444
|
+
// (ir/core.ts) looks through. Only that edge needs it: the INVERTED branch is the one that still
|
|
445
|
+
// reaches, so `bne ^g` always arrives at ^g directly and no relay can sit between them.
|
|
446
|
+
//
|
|
447
|
+
// SO THE CONNECTIVE THIS FOLD MINTS IS THE RANGE'S, NEVER THE SOURCE'S: both `synthetic:ifand_near`
|
|
448
|
+
// (source `&&`) and `synthetic:ifor_near` (source `||`) are short-branch rows and both come out
|
|
449
|
+
// `logic_or`. Which SPELLING then wins is the joined-sense default's question, one layer up, and
|
|
450
|
+
// BOTH halves have a dual candidate there — `ifand_near:agbcc` matches at the default (`unsigned`),
|
|
451
|
+
// while `ifor_near:agbcc` and the long-branch `ifand_far:agbcc` each match on `/flip-join`.
|
|
452
|
+
//
|
|
453
|
+
// `gIsFall` IS NOT THE CARRIER FOR A PER-SITE SENSE, and that was measured rather than argued. It
|
|
454
|
+
// reads the branch RANGE, exactly as the table above says — so in any function small enough for
|
|
455
|
+
// every branch to be short it is the SAME at every site, including sites whose sources wrote
|
|
456
|
+
// opposite connectives. Instrumented at the fold and run through the bench: a two-site row whose
|
|
457
|
+
// first site wrote `&&` and whose second wrote its dual reads `gIsFall=true` at BOTH
|
|
458
|
+
// (`synthetic:joinsense`), and a four-site ladder row with two sites inverted in the source reads
|
|
459
|
+
// `true` at all four (`synthetic:mixsense`). The positive control reads the other way —
|
|
460
|
+
// `synthetic:ifand_far`, the long-branch row, gives `false` against
|
|
461
|
+
// `synthetic:ifand_near`'s `true`. So carrying this boolean to L3
|
|
462
|
+
// as a node stamp (the `#144` `Expr.baseOrdered` shape) would hand every site of such a function
|
|
463
|
+
// one answer and reach exactly the two configurations `negateJoinedBranchSense` already reaches.
|
|
464
|
+
//
|
|
465
|
+
// Every refusal falls through untouched — a miss, never a miscompile.
|
|
466
|
+
/** Per-call options for `recognizeBranchShortCircuit` — the tree-ownership refusal's two ends. */
|
|
467
|
+
export interface BranchShortCircuitOptions {
|
|
468
|
+
/** 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` axis; see the
|
|
470
|
+
* REFUSALS note. It widens the SHAPE the fold accepts and nothing about what the fold may move —
|
|
471
|
+
* every other refusal still applies, the RELAYED clause included.
|
|
472
|
+
*
|
|
473
|
+
* A NEW REFUSAL CONDITION, stated because it is one: this is per-FUNCTION and the question is
|
|
474
|
+
* per-SITE. Every tree-owned site in a function flips together, so a function with two of them
|
|
475
|
+
* wanting OPPOSITE spellings has no candidate that spells the mix, and nothing reports the gap.
|
|
476
|
+
* A per-FUNCTION predicate cannot decide a per-SITE question — the same shape the joined-if
|
|
477
|
+
* default hit — and here it costs completeness rather than correctness. The alternative is a
|
|
478
|
+
* fork per site: `kleod:CheckWorldCompletion` refuses at 10 and goes 96 → 192 candidates as one
|
|
479
|
+
* boolean, where a per-site fork would be 1024×. That is why the boolean, not an oversight. */
|
|
480
|
+
foldTreeOwned?: boolean;
|
|
481
|
+
/** Called at each site the pairwise tree-ownership refusal is the ONE thing stopping the fold —
|
|
482
|
+
* how rank.ts learns the axis has an inhabitant here without re-running the matcher. Asked LAST,
|
|
483
|
+
* after `sameArgs` and the negatability check, so a report means a `/connective` candidate that
|
|
484
|
+
* differs from its sibling: reporting a refusal merely REACHED would double the row's whole
|
|
485
|
+
* 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 lever CHANGE anything.)
|
|
487
|
+
* The pass re-scans after every rewrite, so one site can report more than once; read it as a
|
|
488
|
+
* boolean. */
|
|
489
|
+
onTreeOwned?: () => void;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
export function recognizeBranchShortCircuit(fn: Fn, opts: BranchShortCircuitOptions = {}): boolean {
|
|
278
493
|
const term = (b: Block) => b.ops[b.ops.length - 1];
|
|
279
|
-
|
|
280
|
-
while (progress) {
|
|
281
|
-
progress = false;
|
|
494
|
+
return untilFixpoint(() => {
|
|
282
495
|
const defs = defOpMap(fn);
|
|
283
496
|
const preds = predecessors(fn);
|
|
284
|
-
|
|
497
|
+
for (const h of fn.blocks) {
|
|
285
498
|
const ht = term(h);
|
|
286
499
|
if (ht.opcode !== 'cond_br') {
|
|
287
500
|
continue;
|
|
@@ -312,10 +525,6 @@ export function recognizeBranchShortCircuit(fn: Fn): boolean {
|
|
|
312
525
|
if (gTaken.block === gFall.block) {
|
|
313
526
|
continue;
|
|
314
527
|
}
|
|
315
|
-
// A comparison TREE over one scrutinee belongs to switch recovery, not to this fold.
|
|
316
|
-
if (sameScrutineeConstTests(defs, ht.operands[0], gt.operands[0])) {
|
|
317
|
-
continue;
|
|
318
|
-
}
|
|
319
528
|
// ^g's body must be pure, and every value it defines must be consumed only by ^g itself —
|
|
320
529
|
// see the REFUSALS note: an escaping or reused value becomes a statement hoisted out of the
|
|
321
530
|
// short circuit.
|
|
@@ -329,12 +538,32 @@ export function recognizeBranchShortCircuit(fn: Fn): boolean {
|
|
|
329
538
|
if (!definedValuesStayLocal(fn, g)) {
|
|
330
539
|
continue;
|
|
331
540
|
}
|
|
332
|
-
// Which of ^g's edges rejoins ^h's other successor? That is the shared block.
|
|
333
|
-
|
|
334
|
-
|
|
541
|
+
// Which of ^g's edges rejoins ^h's other successor? That is the shared block. A DIRECT edge
|
|
542
|
+
// wins, so resolution only ever adds reach — it never re-picks an edge this fold already had.
|
|
543
|
+
const sharedTarget = forwardingTarget(sharedFromH.block);
|
|
544
|
+
const rejoins = (e: Successor): boolean => forwardingTarget(e.block) === sharedTarget;
|
|
545
|
+
const direct = gTaken.block === sharedFromH.block ? gTaken : gFall.block === sharedFromH.block ? gFall : null;
|
|
546
|
+
// With neither edge direct, both may resolve onto the shared block — and then there is no
|
|
547
|
+
// "other" arm left and nothing decides which side the connective guards.
|
|
548
|
+
if (direct === null && rejoins(gTaken) && rejoins(gFall)) {
|
|
549
|
+
continue;
|
|
550
|
+
}
|
|
551
|
+
const sharedEdge = direct ?? (rejoins(gTaken) ? gTaken : rejoins(gFall) ? gFall : null);
|
|
335
552
|
if (!sharedEdge) {
|
|
336
553
|
continue;
|
|
337
554
|
}
|
|
555
|
+
// A comparison TREE over one scrutinee belongs to switch recovery, not to this fold. A relay
|
|
556
|
+
// is what agbcc puts on a tree's default edge, so a shared block reached through one is
|
|
557
|
+
// searched function-wide; a direct edge keeps the pairwise test. See the REFUSALS note —
|
|
558
|
+
// the wider test is blunt, and that is why it is not asked everywhere.
|
|
559
|
+
const throughRelay = sharedEdge.block !== sharedFromH.block;
|
|
560
|
+
if (
|
|
561
|
+
throughRelay &&
|
|
562
|
+
(inComparisonTree(fn, defs, ht.operands[0]) || inComparisonTree(fn, defs, gt.operands[0]))
|
|
563
|
+
) {
|
|
564
|
+
continue; // the relayed clause is absolute — `foldTreeOwned` does not widen it
|
|
565
|
+
}
|
|
566
|
+
const treeOwned = !throughRelay && sameScrutineeConstTests(defs, ht.operands[0], gt.operands[0]);
|
|
338
567
|
const otherEdge = sharedEdge === gTaken ? gFall : gTaken;
|
|
339
568
|
if (!sameArgs(sharedFromH.args, sharedEdge.args)) {
|
|
340
569
|
continue;
|
|
@@ -343,16 +572,27 @@ export function recognizeBranchShortCircuit(fn: Fn): boolean {
|
|
|
343
572
|
// reach the SHARED block", `logic_and` asks "does ^g reach the OTHER block".
|
|
344
573
|
const wantEdge = gIsFall ? sharedEdge : otherEdge;
|
|
345
574
|
const c2 = gt.operands[0];
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
575
|
+
// ^g's condition may itself be a CONNECTIVE — the fold is iterative, so an inner diamond is
|
|
576
|
+
// already fused by the time the outer one is tried, and negating one is De Morgan rather
|
|
577
|
+
// than an opcode swap. `negateCondOps` does both, and returns null on anything else.
|
|
578
|
+
const negation = wantEdge !== gTaken ? negateCondOps(defs, c2, NEGATE_BUDGET) : null;
|
|
579
|
+
if (wantEdge !== gTaken && !negation) {
|
|
580
|
+
continue;
|
|
581
|
+
}
|
|
582
|
+
// LAST refusal, so `onTreeOwned` reports a site where tree ownership is the ONE thing in the
|
|
583
|
+
// way — which is why the negatability check stays above it even though it MINTS the negated
|
|
584
|
+
// cone (up to NEGATE_BUDGET ops) and discards it whenever this gate refuses. That discard is
|
|
585
|
+
// free rather than merely cheap: `mkValue` is `{ type }` with no identity counter
|
|
586
|
+
// (ir/core.ts) and nothing is spliced until below, so a site refused here leaves the CFG
|
|
587
|
+
// byte-identical. See the option's doc for why the position is the gate's meaning.
|
|
588
|
+
if (treeOwned) {
|
|
589
|
+
opts.onTreeOwned?.();
|
|
590
|
+
if (!opts.foldTreeOwned) {
|
|
351
591
|
continue;
|
|
352
592
|
}
|
|
353
|
-
second = mkValue(T.unk(32));
|
|
354
|
-
negated.push(mkOp(NEGATED_ICMP[c2Def.opcode], { operands: [...c2Def.operands], results: [second] }));
|
|
355
593
|
}
|
|
594
|
+
const second = negation ? negation.result : c2;
|
|
595
|
+
const negated: Op[] = negation ? negation.ops : [];
|
|
356
596
|
const res = mkValue(T.unk(32));
|
|
357
597
|
const connective = mkOp(gIsFall ? 'logic_or' : 'logic_and', {
|
|
358
598
|
operands: [ht.operands[0], second],
|
|
@@ -372,35 +612,195 @@ export function recognizeBranchShortCircuit(fn: Fn): boolean {
|
|
|
372
612
|
{ block: sharedEdge.block, args: [...sharedEdge.args] },
|
|
373
613
|
],
|
|
374
614
|
});
|
|
615
|
+
foldWriteOrder(fn.writeOrder, g, h); // ^g's writes now follow ^h's own (ir/core.ts)
|
|
375
616
|
fn.blocks = fn.blocks.filter((x) => x !== g);
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
617
|
+
// ^h's old shared edge is gone, so the relay it pointed at may have become unreachable —
|
|
618
|
+
// and once that link goes, so may the next, all the way down the chain. `dominators`
|
|
619
|
+
// (ir/core.ts) gives a block with no in-edges only ITSELF, which empties the intersection at
|
|
620
|
+
// everything it still branches to, so a half-dropped chain declines two passes later with a
|
|
621
|
+
// def-does-not-dominate-use out of `verify`.
|
|
622
|
+
//
|
|
623
|
+
// REACHABILITY, not predecessor count: in-edges from blocks that are themselves unreachable
|
|
624
|
+
// leave a block just as orphaned, and the thumb frontend does hand over unreachable blocks
|
|
625
|
+
// (see raise/gvn.ts). Only relays are dropped — the chain ends at the first block that does
|
|
626
|
+
// real work, and that one stays whatever its in-edges look like.
|
|
627
|
+
//
|
|
628
|
+
// The relay test below is `isBodyless` (ir/core.ts) MINUS its parameter clause, and the
|
|
629
|
+
// omission is what the reachability guard above buys: an unreachable block binds nothing, so
|
|
630
|
+
// a param on one says nothing about a live edge, while refusing it would leave the chain
|
|
631
|
+
// HALF-DROPPED — and a half-dropped chain fails `verify` two passes later with a
|
|
632
|
+
// def-does-not-dominate-use. `isBodyless` itself is unchanged and right for its own three
|
|
633
|
+
// callers, every one of which asks about a block that is still reached.
|
|
634
|
+
for (let link = sharedFromH.block; link.ops.length === 1 && link.ops[0].opcode === 'br';) {
|
|
635
|
+
const dead = link;
|
|
636
|
+
if (dead === fn.blocks[0] || reachableBlocks(fn).has(dead)) {
|
|
637
|
+
break;
|
|
638
|
+
}
|
|
639
|
+
link = dead.ops[0].successors[0].block;
|
|
640
|
+
fn.blocks = fn.blocks.filter((x) => x !== dead);
|
|
641
|
+
}
|
|
642
|
+
return true; // defs/preds are stale after the mutation — the driver rescans
|
|
379
643
|
}
|
|
380
644
|
}
|
|
381
|
-
|
|
382
|
-
|
|
645
|
+
return false;
|
|
646
|
+
});
|
|
383
647
|
}
|
|
384
648
|
|
|
385
|
-
/**
|
|
386
|
-
*
|
|
387
|
-
*
|
|
388
|
-
*
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
649
|
+
/** How many ops `negateCondOps` may KEEP for one negation. De Morgan rebuilds the cone PER PATH and
|
|
650
|
+
* shares nothing — deliberately, see the helper's note — so a fold's cost is linear in the cone's
|
|
651
|
+
* nodes and this is the cheap stop on it.
|
|
652
|
+
*
|
|
653
|
+
* What 8 decides, said as the shape rather than as headroom, because "8" reads like more room than
|
|
654
|
+
* it is. A negation mints one op per cone node and a binary expansion has leaves = internal + 1, so
|
|
655
|
+
* a minted count is always ODD: 1, 3, 5, 7, 9. At 8 a FOUR-clause inner conjunct (7 ops) folds and
|
|
656
|
+
* a FIVE-clause one (9 ops) does not; 7 and 8 are therefore one gate, and so is 9 over everything
|
|
657
|
+
* 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 axis either: a FLAT `a || b || c || …`
|
|
659
|
+
* chain pays nothing at all, because ^g's condition is never a connective in that shape.
|
|
660
|
+
*
|
|
661
|
+
* The bound is on ops KEPT, and the frontier is a NODE COUNT — not a shape. Pinned as such rather
|
|
662
|
+
* than argued: branch-shortcircuit.test.ts's "the fold's frontier is the cone's NODE COUNT, not its
|
|
663
|
+
* shape" enumerates all 23,714 binary cone shapes with up to 10 internal nodes (21 nodes) and
|
|
664
|
+
* asserts the PUBLIC fold takes one exactly when `2k + 1 <= budget`.
|
|
665
|
+
* Shape decides only WHICH guard refuses and how many ops had been
|
|
666
|
+
* minted when it did, neither of them visible to a caller — at budget 8 a 15-node cone is caught by
|
|
667
|
+
* the ENTRY guard at 9 (left chain) or 8 (balanced cone) or by the POST-check at 15 (right chain).
|
|
668
|
+
* Because `go` pushes a parent only after its children, a refused walk transiently mints more than
|
|
669
|
+
* the budget before unwinding — `2 * budget - 1` at the worst shape, that right chain, at every
|
|
670
|
+
* budget measured. Bounded, and nothing escapes it.
|
|
671
|
+
*
|
|
672
|
+
* A refusal here is SILENT, unlike the `onTreeOwned` gate above which exists so a sweep need not
|
|
673
|
+
* re-instrument. So raising this constant is not free advice: finding a corpus site that wants it
|
|
674
|
+
* means patching a hook back into this file. No callback is added because no consumer has asked for
|
|
675
|
+
* one; test/branch-shortcircuit.test.ts pins both sides of the frontier instead. */
|
|
676
|
+
const NEGATE_BUDGET = 8;
|
|
677
|
+
|
|
678
|
+
/** The ops computing `!v`, or `null` when `v` cannot be negated.
|
|
679
|
+
*
|
|
680
|
+
* Two cases, and no third:
|
|
681
|
+
* - an `icmp_*` in `NEGATED_ICMP` → the swapped-opcode comparison over the SAME operands.
|
|
682
|
+
* - a `logic_and`/`logic_or` → the DUAL connective over its two recursively negated operands.
|
|
683
|
+
* De Morgan: `!(a || b)` is `!a && !b`. This is the only reason the helper is recursive, and
|
|
684
|
+
* it is what lets a chain fold past its first level (`a || !(b || c)`).
|
|
685
|
+
*
|
|
686
|
+
* REFUSALS — each returns `null`, which the caller turns into its existing `continue`, so the CFG
|
|
687
|
+
* is left exactly as it was and the structurer emits today's tail-duplicated spelling. A bytes
|
|
688
|
+
* miss, never a wrong answer:
|
|
689
|
+
* - the value has no def in this function, which means a block param — a call RESULT has one,
|
|
690
|
+
* `call` declaring `results: 1` (ir/opcodes.ts), and is refused by the next bullet instead;
|
|
691
|
+
* - the def is neither a negatable icmp nor a connective (a call, any arithmetic) — there is no
|
|
692
|
+
* sound inverse to build, and `!x` as `x == 0` is a DIFFERENT spelling, not this fold's
|
|
693
|
+
* business;
|
|
694
|
+
* - ANY leaf anywhere in the cone is non-negatable ⇒ the WHOLE negation is refused. Half a
|
|
695
|
+
* De Morgan is not a conservative approximation of one;
|
|
696
|
+
* - the cone exceeds `NEGATE_BUDGET` minted ops.
|
|
697
|
+
*
|
|
698
|
+
* Ops are MINTED, never mutated: the originals stay where the caller hoisted them and die to the
|
|
699
|
+
* pass list's own `dce: true` (raise/pre-recovery.ts). Only `icmp_*`/`logic_and`/`logic_or` are
|
|
700
|
+
* ever rebuilt, all of them pure, so no effect can be duplicated or reordered by construction.
|
|
701
|
+
*
|
|
702
|
+
* Nothing is SHARED between paths, and that is the point rather than a limitation. Memoizing `go`
|
|
703
|
+
* in a `Map<Value, Value>` is three lines and would make the budget unnecessary — and it would
|
|
704
|
+
* create exactly the hazard this fold exists to remove: a shared negated sub-condition has two
|
|
705
|
+
* consumers, and analysis.ts renders a value with two consumers as a statement BEFORE the `if`.
|
|
706
|
+
* Nothing collapses the duplicates later either — `numberPureValues` runs as `addrnum`, far ahead
|
|
707
|
+
* of both folds. So the per-path rebuild is the mechanism and NEGATE_BUDGET is its price.
|
|
708
|
+
*
|
|
709
|
+
* What it GUARANTEES about its result, which one caller leans on: every op it mints is an `icmp_*`
|
|
710
|
+
* or a `logic_and`/`logic_or`, so the returned value is always a C boolean. The value form hands an
|
|
711
|
+
* un-negated head straight on as the merge VALUE, which is why that caller checks booleanness for
|
|
712
|
+
* itself before deciding whether to call here at all (see its head gate); the branch form needs no
|
|
713
|
+
* such check, because its result only ever feeds a `cond_br`.
|
|
714
|
+
*
|
|
715
|
+
* PRECONDITION, earned by the caller and not checked here: every value in the cone must dominate
|
|
716
|
+
* the point the caller splices `ops` into — the minted comparisons reuse the ORIGINAL leaf
|
|
717
|
+
* operands. The branch fold gets it free from ^g's single-predecessor gate: with ^h ^g's only
|
|
718
|
+
* predecessor, every def in the cone either sits in ^g's body (spliced in ahead of these ops) or
|
|
719
|
+
* dominates ^h. A caller without that invariant emits a def that does not dominate its use — loud
|
|
720
|
+
* at `verify`, but this helper does not look.
|
|
721
|
+
*
|
|
722
|
+
* It stays in this file rather than joining `NEGATED_ICMP` in ir/opcodes.ts: that table is a fact
|
|
723
|
+
* about opcodes, this MINTS ops, and both consumers are the two folds above — the value form and
|
|
724
|
+
* the branch form, which is the whole reason it is a helper and not inline. */
|
|
725
|
+
function negateCondOps(defs: Map<Value, Op>, v: Value, budget: number): { ops: Op[]; result: Value } | null {
|
|
726
|
+
const ops: Op[] = [];
|
|
727
|
+
const go = (x: Value): Value | null => {
|
|
728
|
+
if (ops.length >= budget) {
|
|
729
|
+
return null;
|
|
730
|
+
}
|
|
731
|
+
const d = defs.get(x);
|
|
732
|
+
if (!d) {
|
|
393
733
|
return null;
|
|
394
734
|
}
|
|
395
|
-
const
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
735
|
+
const out = mkValue(T.unk(32));
|
|
736
|
+
if (NEGATED_ICMP[d.opcode]) {
|
|
737
|
+
ops.push(mkOp(NEGATED_ICMP[d.opcode], { operands: [...d.operands], results: [out] }));
|
|
738
|
+
return out;
|
|
739
|
+
}
|
|
740
|
+
if (d.opcode === 'logic_and' || d.opcode === 'logic_or') {
|
|
741
|
+
const a = go(d.operands[0]);
|
|
742
|
+
if (a === null) {
|
|
743
|
+
return null;
|
|
744
|
+
}
|
|
745
|
+
const b = go(d.operands[1]);
|
|
746
|
+
if (b === null) {
|
|
747
|
+
return null;
|
|
748
|
+
}
|
|
749
|
+
// Operands are pushed BEFORE the connective, so the op list is already in dominating order.
|
|
750
|
+
ops.push(mkOp(d.opcode === 'logic_and' ? 'logic_or' : 'logic_and', { operands: [a, b], results: [out] }));
|
|
751
|
+
return out;
|
|
752
|
+
}
|
|
753
|
+
return null;
|
|
400
754
|
};
|
|
401
|
-
const
|
|
402
|
-
|
|
403
|
-
|
|
755
|
+
const result = go(v);
|
|
756
|
+
return result === null || ops.length > budget ? null : { ops, result };
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
/** Do `c1` and `c2` compare the SAME value against CONSTANTS? The signature of a comparison-tree
|
|
760
|
+
* `switch`, which switch-recover.ts owns — see the REFUSALS note. Equality tests only: a switch
|
|
761
|
+
* dispatches on `==`/`!=`, while a RELATIONAL pair (`x >= lo && x <= hi`, the range check) is a
|
|
762
|
+
* genuine connective this fold should still take. */
|
|
763
|
+
function sameScrutineeConstTests(defs: Map<Value, Op>, c1: Value, c2: Value): boolean {
|
|
764
|
+
const s1 = constTestScrutinee(defs, c1);
|
|
765
|
+
const isEq = (v: Value): boolean => {
|
|
766
|
+
const op = defs.get(v)?.opcode;
|
|
767
|
+
return op === 'icmp_eq' || op === 'icmp_ne';
|
|
768
|
+
};
|
|
769
|
+
return s1 !== null && s1 === constTestScrutinee(defs, c2) && isEq(c1) && isEq(c2);
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
/** Is `c`'s scrutinee compared against constants by MORE THAN ONE `cond_br` in the function?
|
|
773
|
+
*
|
|
774
|
+
* The function-wide question, for the case where the shared block was reached through a relay. A
|
|
775
|
+
* tree's split node is RELATIONAL and its children are equalities (`if (x > 10) { if (x == 20) }`),
|
|
776
|
+
* so the two tests in hand need not look alike, and the one that would give the tree away may be
|
|
777
|
+
* neither of them. Counting every constant test on the scrutinee catches the split either way. */
|
|
778
|
+
function inComparisonTree(fn: Fn, defs: Map<Value, Op>, c: Value): boolean {
|
|
779
|
+
const scrutinee = constTestScrutinee(defs, c);
|
|
780
|
+
if (scrutinee === null) {
|
|
781
|
+
return false;
|
|
782
|
+
}
|
|
783
|
+
let seen = 0;
|
|
784
|
+
for (const b of fn.blocks) {
|
|
785
|
+
const t = b.ops[b.ops.length - 1];
|
|
786
|
+
if (t?.opcode === 'cond_br' && constTestScrutinee(defs, t.operands[0]) === scrutinee && ++seen > 1) {
|
|
787
|
+
return true;
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
return false;
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
/** The value an `icmp_* <value>, <const>` tests, or null when `c` is not one. */
|
|
794
|
+
function constTestScrutinee(defs: Map<Value, Op>, c: Value): Value | null {
|
|
795
|
+
const d = defs.get(c);
|
|
796
|
+
if (!d || NEGATED_ICMP[d.opcode] === undefined) {
|
|
797
|
+
return null;
|
|
798
|
+
}
|
|
799
|
+
const [x, y] = d.operands;
|
|
800
|
+
const xc = defs.get(x)?.opcode === 'const';
|
|
801
|
+
const yc = defs.get(y)?.opcode === 'const';
|
|
802
|
+
// exactly one side constant — `x == y` between two variables is no switch test
|
|
803
|
+
return xc === yc ? null : xc ? y : x;
|
|
404
804
|
}
|
|
405
805
|
|
|
406
806
|
/** True when every value `g` defines is read at most once, and any read is inside `g`.
|
|
@@ -411,11 +811,11 @@ function sameScrutineeConstTests(defs: Map<Value, Op>, c1: Value, c2: Value): bo
|
|
|
411
811
|
* ends in `cond_br` and its `other` successor IS ^g-dominated, so a ^g-defined value genuinely can
|
|
412
812
|
* escape, and only this check stops it.
|
|
413
813
|
*
|
|
414
|
-
*
|
|
415
|
-
*
|
|
416
|
-
*
|
|
417
|
-
*
|
|
418
|
-
*
|
|
814
|
+
* What the two folds genuinely DO share is the `fn.blocks[0]` refusal, and each has a test that
|
|
815
|
+
* pins its own half: 'a feeder that is the entry block is not folded away' for the value form, and
|
|
816
|
+
* 'the ENTRY block is never folded away' for the branch form. They are deliberately NOT routed
|
|
817
|
+
* through one shared `isEntry` helper — a helper enforces nothing, and it is the two tests that
|
|
818
|
+
* hold each fold to the refusal. When changing either fold, check the other. */
|
|
419
819
|
function definedValuesStayLocal(fn: Fn, g: Block): boolean {
|
|
420
820
|
const defined = new Set<Value>(g.ops.flatMap((op) => op.results));
|
|
421
821
|
if (defined.size === 0) {
|