@asmlift/core 0.9.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/README.md +25 -24
- package/package.json +1 -1
- package/src/aggregate.ts +149 -0
- package/src/backend/c.ts +2 -0
- package/src/backend/cfamily.ts +197 -61
- package/src/backend/cpp.ts +3 -1
- package/src/backend/lines.ts +10 -0
- package/src/backend/pascal.ts +47 -16
- package/src/cdecl/parse.ts +21 -0
- package/src/contracts.ts +246 -36
- package/src/declare.ts +69 -20
- package/src/frontend/arm-body.ts +155 -0
- package/src/frontend/asmdata.ts +9 -2
- package/src/frontend/call-plan.ts +1117 -61
- package/src/frontend/callee-body.ts +370 -0
- package/src/frontend/dead-reads.ts +628 -0
- package/src/frontend/device-pins.ts +33 -4
- package/src/frontend/emit.ts +4 -3
- package/src/frontend/fpu.ts +224 -17
- package/src/frontend/frame-objects.ts +1689 -206
- package/src/frontend/frontend.ts +84 -3
- package/src/frontend/high-half.ts +53 -4
- package/src/frontend/jump-table.ts +545 -0
- package/src/frontend/local-object.ts +158 -32
- package/src/frontend/mips.ts +2296 -217
- package/src/frontend/own-struct-return.ts +500 -0
- package/src/frontend/ppc.ts +2748 -245
- package/src/frontend/register-struct-call.ts +399 -0
- package/src/frontend/reloc-symbol.ts +22 -7
- package/src/frontend/ssa.ts +1721 -43
- package/src/frontend/stackargs.ts +199 -72
- package/src/frontend/thumb-entry.ts +340 -0
- package/src/frontend/thumb.ts +2400 -325
- package/src/ir/alias.ts +65 -2
- package/src/ir/bits.ts +1699 -4
- package/src/ir/core.ts +254 -9
- package/src/ir/discipline.ts +86 -21
- package/src/ir/entry.ts +157 -0
- package/src/{structure → ir}/loops.ts +6 -2
- package/src/ir/opcodes.ts +137 -9
- package/src/ir/parse.ts +1 -0
- package/src/ir/simplify.ts +61 -10
- package/src/ir/types.ts +86 -2
- package/src/ir/verify.ts +96 -11
- package/src/l3/argbase.ts +2 -0
- package/src/l3/ast.ts +582 -18
- package/src/l3/basecse.ts +68 -15
- package/src/l3/caseendgoto.ts +223 -0
- package/src/l3/coalesce.ts +42 -14
- package/src/l3/constinit.ts +410 -0
- package/src/l3/dce.ts +53 -9
- package/src/l3/initfirst.ts +42 -11
- package/src/l3/intcell.ts +2 -1
- package/src/l3/narrowdecl.ts +77 -36
- package/src/l3/pollguard.ts +8 -2
- package/src/l3/ptrcell.ts +33 -34
- package/src/l3/ptrfield.ts +123 -8
- package/src/l3/regspell.ts +11 -4
- package/src/l3/reindex.ts +295 -99
- package/src/l3/scopebase.ts +2 -0
- package/src/l3/storage.ts +98 -1
- package/src/l3/symbol-refs.ts +84 -6
- package/src/l3/tailmerge.ts +6 -2
- package/src/l3/typing.ts +108 -15
- package/src/l3/unreduce.ts +6 -5
- package/src/l3/volatileptr.ts +17 -54
- package/src/mangle.ts +30 -0
- package/src/pattern/engine.ts +361 -73
- package/src/pipeline.ts +1039 -38
- package/src/proto-context.ts +1039 -45
- package/src/proto.ts +555 -40
- package/src/raise/aggmembers.ts +290 -0
- package/src/raise/arrays.ts +5 -1
- package/src/raise/const.ts +25 -0
- package/src/raise/declared-volatile.ts +207 -31
- package/src/raise/divpow2.ts +192 -0
- package/src/raise/floathelpers.ts +254 -33
- package/src/raise/gvn.ts +3 -3
- package/src/raise/latch.ts +162 -6
- package/src/raise/memberarrays.ts +10 -4
- package/src/raise/offsetnames.ts +422 -36
- package/src/raise/pairparams.ts +33 -0
- package/src/raise/paramwidth.ts +196 -6
- package/src/raise/pre-recovery.ts +107 -21
- package/src/raise/pre-structure.ts +73 -0
- package/src/raise/recover.ts +12 -5
- package/src/raise/reroll.ts +674 -0
- package/src/raise/shortcircuit.ts +217 -34
- package/src/raise/softdiv.ts +1 -1
- package/src/raise/storesign.ts +153 -0
- package/src/raise/struct-arrays.ts +30 -11
- package/src/raise/structs.ts +396 -33
- package/src/raise/truncload.ts +4 -3
- package/src/raise/unpre.ts +594 -0
- package/src/raise/unthread.ts +400 -0
- package/src/rank-declare.ts +118 -12
- package/src/rank-variations.ts +186 -35
- package/src/rank.ts +536 -68
- package/src/runtime-helpers.ts +140 -22
- package/src/structure/analysis.ts +217 -8
- package/src/structure/arith-ops.ts +23 -0
- package/src/structure/cross-jumped-tail.ts +216 -0
- package/src/structure/globalaccess.ts +157 -10
- package/src/structure/hazards.ts +16 -64
- package/src/structure/local-statics.ts +145 -9
- package/src/structure/namecoalesce.ts +1 -1
- package/src/structure/pointer-spelling.ts +1106 -0
- package/src/structure/redundant-test.ts +1 -1
- package/src/structure/structure.ts +6192 -1213
- package/src/structure/switch-recover.ts +583 -120
- package/src/structure/unfolded-branch-constant.ts +90 -0
- package/src/symbols.ts +58 -15
- package/src/target.ts +1028 -54
- package/src/trace.ts +49 -17
- package/src/variation-definitions.ts +333 -20
- package/src/variation-gates.ts +4 -0
- package/src/variation-tokens.ts +68 -3
package/src/l3/basecse.ts
CHANGED
|
@@ -203,13 +203,16 @@ interface Collected {
|
|
|
203
203
|
* pass has rebuilt one node and dropped the stamp, and requiring all of them makes that a
|
|
204
204
|
* refusal rather than a half-homed base. */
|
|
205
205
|
ordered: Map<string, boolean>;
|
|
206
|
+
/** per key, whether EVERY access so far sits under some connective's right operand — `false`
|
|
207
|
+
* once one access is under none (the input to `guardedOperand`) */
|
|
208
|
+
guard: Map<string, boolean>;
|
|
206
209
|
}
|
|
207
210
|
|
|
208
211
|
/** Every `index` node whose base is a hoistable leaf, tallied by key (the gates' use count) and in
|
|
209
212
|
* first-appearance order (so the hoisted assignments emit in the order the bases are first used,
|
|
210
213
|
* matching the compiler's pool-load order). `loop` marks uses nested in a while/do-while/for. */
|
|
211
214
|
function collect(stmts: Stmt[], c: Collected, loop: boolean): void {
|
|
212
|
-
const visitExpr = (e: Expr, inLoop: boolean): void => {
|
|
215
|
+
const visitExpr = (e: Expr, inLoop: boolean, guard: boolean): void => {
|
|
213
216
|
if (e.k === 'index' && isHoistableBase(e.base)) {
|
|
214
217
|
const k = keyOf(e.base, e.width, e.signed);
|
|
215
218
|
if (!c.count.has(k)) {
|
|
@@ -232,9 +235,11 @@ function collect(stmts: Stmt[], c: Collected, loop: boolean): void {
|
|
|
232
235
|
c.operandOff.add(k);
|
|
233
236
|
}
|
|
234
237
|
c.ordered.set(k, (c.ordered.get(k) ?? true) && e.baseOrdered === true);
|
|
238
|
+
c.guard.set(k, (c.guard.get(k) ?? true) && guard);
|
|
235
239
|
}
|
|
240
|
+
const rhs = e.k === 'bin' && (e.op === '&&' || e.op === '||') ? e.r : null;
|
|
236
241
|
for (const ch of exprChildren(e)) {
|
|
237
|
-
visitExpr(ch, inLoop);
|
|
242
|
+
visitExpr(ch, inLoop, guard || ch === rhs);
|
|
238
243
|
}
|
|
239
244
|
};
|
|
240
245
|
for (const s of stmts) {
|
|
@@ -242,21 +247,41 @@ function collect(stmts: Stmt[], c: Collected, loop: boolean): void {
|
|
|
242
247
|
// there is loop-invariant just like a body use — visit it with `nested`, not the outer flag.
|
|
243
248
|
const nested = loop || isLoop(s);
|
|
244
249
|
for (const e of stmtExprs(s)) {
|
|
245
|
-
visitExpr(e, nested);
|
|
250
|
+
visitExpr(e, nested, false);
|
|
246
251
|
}
|
|
247
252
|
collect(stmtChildren(s), c, nested);
|
|
248
253
|
}
|
|
249
254
|
}
|
|
250
255
|
|
|
251
|
-
/** Rewrite every `index`-of-hoistable-base whose key is hoisted so its base becomes the hoist local.
|
|
252
|
-
|
|
256
|
+
/** Rewrite every `index`-of-hoistable-base whose key is hoisted so its base becomes the hoist local.
|
|
257
|
+
*
|
|
258
|
+
* A `volatile` cast over a hoisted LEAF keeps its cast and takes the local under it:
|
|
259
|
+
* `((volatile s32 *)0x40000D4)[2]` beside a hoisted `p0 = (s32 *)0x40000D4` becomes
|
|
260
|
+
* `((volatile s32 *)p0)[2]`. The cast is never hoisted itself (`isHoistableBase`), since the local
|
|
261
|
+
* would drop its qualifier; under it the local is the same address, so the access keeps its
|
|
262
|
+
* qualifier and its cell, and reads the register the compiler already holds the base in. Inline, a
|
|
263
|
+
* qualified access at a nonzero offset makes agbcc materialize a second literal with the offset
|
|
264
|
+
* folded in (`.word 0x40000dc` + `ldr r0, [r0]`) where the source read `[r4, #0x8]` off the base
|
|
265
|
+
* it had loaded — the `dmaRegs[2];` that ends a DMA macro. `leafLocal` maps a hoisted leaf to its
|
|
266
|
+
* local, whichever key hoisted it. */
|
|
267
|
+
function rewrite(e: Expr, localFor: Map<string, string>, leafLocal: ReadonlyMap<string, string>): Expr {
|
|
253
268
|
if (e.k === 'index' && isHoistableBase(e.base)) {
|
|
254
269
|
const nm = localFor.get(keyOf(e.base, e.width, e.signed));
|
|
255
270
|
if (nm) {
|
|
256
|
-
return { ...e, base: { k: 'var', name: nm }, idx: rewrite(e.idx, localFor) };
|
|
271
|
+
return { ...e, base: { k: 'var', name: nm }, idx: rewrite(e.idx, localFor, leafLocal) };
|
|
257
272
|
}
|
|
258
273
|
}
|
|
259
|
-
|
|
274
|
+
if (e.k === 'index' && e.base.k === 'cast' && e.base.volatile === true && isHoistableLeaf(e.base.e)) {
|
|
275
|
+
const nm = leafLocal.get(leafId(e.base.e));
|
|
276
|
+
if (nm) {
|
|
277
|
+
return {
|
|
278
|
+
...e,
|
|
279
|
+
base: { ...e.base, e: { k: 'var', name: nm } },
|
|
280
|
+
idx: rewrite(e.idx, localFor, leafLocal),
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
return mapExprChildren(e, (c) => rewrite(c, localFor, leafLocal));
|
|
260
285
|
}
|
|
261
286
|
|
|
262
287
|
/** One base under consideration, keyed as `(base, width, signedness)`. */
|
|
@@ -360,6 +385,14 @@ export interface BaseKey {
|
|
|
360
385
|
* false for every base a compiler that has not opted in produced, which is what keeps the variation
|
|
361
386
|
* off those targets. */
|
|
362
387
|
orderLicensed: boolean;
|
|
388
|
+
/** EVERY access sits in the right operand of some `&&`/`||` — the half C evaluates only when the
|
|
389
|
+
* left one did not decide. A head hoist puts the base's load above that guard, where the compiler
|
|
390
|
+
* loaded it inside the operand: `if (gY != 5 || (gS.a | gS.d | gS.c) != 0)` reads `&gS` into a
|
|
391
|
+
* register after the `gY` test, while `p0 = &gS;` ahead of the `if` loads it first. Not one
|
|
392
|
+
* operand: accesses under two different guards (`(gY != 5 || gS.a) && (gA != 1 || gS.c)`, or the
|
|
393
|
+
* two in separate `if`s) are each loaded under their own, so a hoist above both moves every load
|
|
394
|
+
* (agbcc 15/39 and 19/40 hoisted, 9/39 inline). */
|
|
395
|
+
guardedOperand: boolean;
|
|
363
396
|
}
|
|
364
397
|
|
|
365
398
|
/** The admission rules. NONE is sound, and that is a property of the pass rather than an oversight:
|
|
@@ -421,6 +454,16 @@ export const BASECSE_GATES: readonly Gate<BaseKey>[] = [
|
|
|
421
454
|
sound: false,
|
|
422
455
|
rejects: (c) => c.repeatedConstOffset,
|
|
423
456
|
},
|
|
457
|
+
{
|
|
458
|
+
// `if (gY != 5 || (gS.a | gS.d | gS.c) != 0) { … }` on agbcc: the inline spelling MATCHES and
|
|
459
|
+
// `p0 = (u8 *)&gS;` above the `if` does not (7 points over 44), because the literal load moves
|
|
460
|
+
// ahead of the first test. A placement prediction like the two above, ablated with them for
|
|
461
|
+
// `/livebase`, so a hoisted base before a connective stays a candidate the differ can pick.
|
|
462
|
+
id: 'guarded-operand',
|
|
463
|
+
why: 'a base read only under the second half of a condition is loaded there, after the first half',
|
|
464
|
+
sound: false,
|
|
465
|
+
rejects: (c) => c.guardedOperand,
|
|
466
|
+
},
|
|
424
467
|
];
|
|
425
468
|
|
|
426
469
|
/** `/basefold`'s admission (rank.ts): the default rules with `single-use` EXEMPTING a base whose
|
|
@@ -453,15 +496,15 @@ export const BASEFOLD_GATES: readonly Gate<BaseKey>[] = [
|
|
|
453
496
|
...ablateHeuristic(BASECSE_GATES, 'single-use'),
|
|
454
497
|
];
|
|
455
498
|
|
|
456
|
-
/** The `/livebase` variation's admission (rank.ts): the default rules with
|
|
457
|
-
* ablated, keeping only `single-use`. `loop
|
|
458
|
-
* the compiler chose, and
|
|
459
|
-
* while (p[2] & BUSY) {}`) stores and re-reads a fixed offset through ONE register
|
|
460
|
-
* time.
|
|
499
|
+
/** The `/livebase` variation's admission (rank.ts): the default rules with the PLACEMENT heuristics
|
|
500
|
+
* ablated, keeping only `single-use`. `loop`, `repeated-const-offset` and `guarded-operand` predict
|
|
501
|
+
* which spelling the compiler chose, and the first two have a counterexample — an MMIO poll
|
|
502
|
+
* (`p[2] = go; while (p[2] & BUSY) {}`) stores and re-reads a fixed offset through ONE register
|
|
503
|
+
* the whole time. None of the three is `sound`, so ablating them can only change which spelling wins, never
|
|
461
504
|
* what a candidate means; the differ referees. */
|
|
462
505
|
export const LIVEBASE_GATES: readonly Gate<BaseKey>[] = ablateHeuristic(
|
|
463
|
-
ablateHeuristic(BASECSE_GATES, 'loop'),
|
|
464
|
-
'
|
|
506
|
+
ablateHeuristic(ablateHeuristic(BASECSE_GATES, 'loop'), 'repeated-const-offset'),
|
|
507
|
+
'guarded-operand',
|
|
465
508
|
);
|
|
466
509
|
|
|
467
510
|
/** `/livebase-block`'s admission (rank.ts): `/livebase` plus `single-cell`. The two tables differ
|
|
@@ -705,6 +748,7 @@ function collectFrom(sfn: SFn): Collected {
|
|
|
705
748
|
varIndexed: new Set(),
|
|
706
749
|
operandOff: new Set(),
|
|
707
750
|
ordered: new Map(),
|
|
751
|
+
guard: new Map(),
|
|
708
752
|
};
|
|
709
753
|
collect(sfn.body, c, false);
|
|
710
754
|
return c;
|
|
@@ -725,6 +769,7 @@ function admit(sfn: SFn, gates: readonly Gate<BaseKey>[]): { c: Collected; keys:
|
|
|
725
769
|
unfoldedOffset: c.operandOff.has(k),
|
|
726
770
|
castBase: c.meta.get(k)!.base.k === 'cast',
|
|
727
771
|
orderLicensed: c.ordered.get(k) === true,
|
|
772
|
+
guardedOperand: c.guard.get(k) === true,
|
|
728
773
|
}) === null
|
|
729
774
|
);
|
|
730
775
|
});
|
|
@@ -784,7 +829,15 @@ export function hoistBaseLocals(
|
|
|
784
829
|
});
|
|
785
830
|
}
|
|
786
831
|
|
|
787
|
-
const
|
|
832
|
+
const leafLocal = new Map<string, string>();
|
|
833
|
+
for (const k of hoisted) {
|
|
834
|
+
const b = meta.get(k)!.base;
|
|
835
|
+
const leaf = leafId(b.k === 'cast' ? b.e : b);
|
|
836
|
+
if (!leafLocal.has(leaf)) {
|
|
837
|
+
leafLocal.set(leaf, localFor.get(k)!);
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
const rewritten = sfn.body.map((s) => mapStmtExprs(s, (e) => rewrite(e, localFor, leafLocal)));
|
|
788
841
|
// The new inits join the tree's LEADING run of base inits rather than being prepended above it:
|
|
789
842
|
// when rank's /livebase re-runs this pass the head already carries the default run's, and
|
|
790
843
|
// blindly prepending would spell the new base's pool load above locals the compiler loads first.
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
// L3 respell variation `/case-end-goto`: a search loop's hit flag, read once after its loop where the
|
|
2
|
+
// code after the loop ends the switch arm the loop sits in, rewritten as the source's `goto` to that
|
|
3
|
+
// arm's end — the case-end goto (l3/ast.ts `caseEndGoto`, docs/asmlift-101.md §2.7):
|
|
4
|
+
//
|
|
5
|
+
// f = 0; for (j…) {
|
|
6
|
+
// for (j…) { if (hit) { s(j); goto case_end; }
|
|
7
|
+
// if (hit) { s(j); f = 1; break; } }
|
|
8
|
+
// } → k(i);
|
|
9
|
+
// if (f == 0) { k(i); } case_end:
|
|
10
|
+
// break; break;
|
|
11
|
+
//
|
|
12
|
+
// The structurer writes the flag where a two-level exit's second target lies past the code after
|
|
13
|
+
// the loop (structure.ts `TwoLevelExit`, `emitForeverExit`). Where that target is the end of the
|
|
14
|
+
// switch arm, agbcc threads the source's goto past the loop around the switch (jump.c
|
|
15
|
+
// `follow_jumps`), and on agbcc, KMC, gcc 2.7.2 and IDO the goto compiles to the source's bytes
|
|
16
|
+
// where the flag never does (`synthetic:sw_hitexit`); but inside asmlift's own candidates the flag
|
|
17
|
+
// can still score closer (gcc 2.7.2), so both are emitted, the differ referees, and a tie goes to
|
|
18
|
+
// the candidate without the goto (rank.ts `gotoCount`).
|
|
19
|
+
//
|
|
20
|
+
// The structurer is not the only writer the rewrite reaches: it reads the tree, not who built it,
|
|
21
|
+
// so a flag the source declared and the compiler kept is rewritten too where it has the same shape
|
|
22
|
+
// (IDO keeps a `u8 found` a source sets to 2 in a case arm, and lifts it as that flag). That is
|
|
23
|
+
// sound for the reason below, which reads only the tree, and ranked like the rest.
|
|
24
|
+
//
|
|
25
|
+
// SOUND OVER THE TREE ALONE. `f` is a local mentioned only by `f = 0;` right before the loop, by
|
|
26
|
+
// each hit's `f = c; break;` (c a nonzero constant, the `break` the loop's own: reached from the
|
|
27
|
+
// loop's body through `if`s only) and by the test right after the loop (`CASE_END_GOTO_GATES`
|
|
28
|
+
// `flag-elsewhere`). So after the loop `f == 0` exactly when it left by any other way than a hit,
|
|
29
|
+
// and `X` runs exactly then — which is what `goto L;` at each hit and `X` unguarded after the loop
|
|
30
|
+
// run. After `X` both spellings go on to whatever follows the `if`, and that is the arm's end with
|
|
31
|
+
// nothing run on the way (`target-not-arm-end`): a switch `break;` right after the `if`, which takes
|
|
32
|
+
// the label, or the `if` last in a list whose falling off runs nothing before leaving the switch,
|
|
33
|
+
// which gets a labelled `break;` appended. `L: break;` leaves the switch as falling off the arm
|
|
34
|
+
// does. The label sits in the loop's own list, no loop between it and its arm, and
|
|
35
|
+
// the hits' `break`s are reached through `if`s only, so no switch stands between them and it: the
|
|
36
|
+
// form `caseGotoViolation` holds every case-end goto to.
|
|
37
|
+
//
|
|
38
|
+
// A RESPELL, NOT A STRUCTURE VARIATION — the third fork of docs/level-tower.md. The rewrite reads
|
|
39
|
+
// nothing but the flag's tree, so asked of the structured tree it reaches every site a structure
|
|
40
|
+
// option inside `emitForeverExit` would. A structure variation doubles the settings every
|
|
41
|
+
// enumeration walks even where it changes no tree, which level-tower.md measures on this one; the
|
|
42
|
+
// respell costs one walk of each structured tree, and nothing more where no flag is read after its
|
|
43
|
+
// loop. What it gives up is composition with the other pre-respell variation (`/unmerge` runs on
|
|
44
|
+
// the structured tree beside it, not on its output); no row has asked for both.
|
|
45
|
+
//
|
|
46
|
+
// Outer sites first, and an inner site whose `if` the outer rewrite leaves right before the outer
|
|
47
|
+
// label takes that label, so two search loops in sequence into one case end share one `L:`.
|
|
48
|
+
import { type Expr, type Loop, type SFn, type Stmt, type SwitchCase, isLoop, mapStmtLists, stmtChildren } from './ast';
|
|
49
|
+
import { type Gate, firstRejection } from './gates';
|
|
50
|
+
import { localMentions, readsOf } from './mentions';
|
|
51
|
+
|
|
52
|
+
/** A flag read after its loop that the rewrite would replace (`CASE_END_GOTO_GATES`). */
|
|
53
|
+
export interface CaseEndSite {
|
|
54
|
+
/** what runs after the `if (f == 0)` is not the arm's end: no switch `break;` follows it, and it
|
|
55
|
+
* is not last in a list whose falling off leaves the switch with nothing run. Outside a switch
|
|
56
|
+
* arm, or in a loop inside one, neither can hold (a switch `break;` sits only in an arm with no
|
|
57
|
+
* loop between, `refuseMisboundBreak`), so this is also the rule that keeps such a flag */
|
|
58
|
+
readonly targetNotArmEnd: boolean;
|
|
59
|
+
/** `f` is a param or global, or mentioned anywhere but its `f = 0;`, its hits and its one test */
|
|
60
|
+
readonly flagElsewhere: boolean;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** When a flag read after its loop is rewritten as a case-end goto. The argument is the file's. */
|
|
64
|
+
export const CASE_END_GOTO_GATES: readonly Gate<CaseEndSite>[] = [
|
|
65
|
+
{
|
|
66
|
+
id: 'target-not-arm-end',
|
|
67
|
+
why: 'the goto skips only the guarded code, so what follows it must leave the switch with nothing run',
|
|
68
|
+
sound: true,
|
|
69
|
+
guardedBy: [
|
|
70
|
+
'case-end-goto.test.ts: a hit whose target is a join running code keeps its flag',
|
|
71
|
+
'case-end-goto.test.ts: a flag read outside a switch arm, or in a loop inside one, keeps its flag',
|
|
72
|
+
],
|
|
73
|
+
rejects: (c) => c.targetNotArmEnd,
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
id: 'flag-elsewhere',
|
|
77
|
+
why: 'the flag is zero after the loop exactly when no hit ran only if nothing else writes or reads it',
|
|
78
|
+
sound: true,
|
|
79
|
+
guardedBy: 'case-end-goto.test.ts: a flag read or written anywhere else keeps its flag',
|
|
80
|
+
rejects: (c) => c.flagElsewhere,
|
|
81
|
+
},
|
|
82
|
+
];
|
|
83
|
+
|
|
84
|
+
/** `f == 0`, the structurer's test of a flag (structure.ts `emitForeverExit`) */
|
|
85
|
+
const flagTest = (e: Expr): string | null =>
|
|
86
|
+
e.k === 'bin' && e.op === '==' && e.l.k === 'var' && e.r.k === 'const' && e.r.value === 0 ? e.l.name : null;
|
|
87
|
+
|
|
88
|
+
/** The hits in `body`, a loop's body: each `f = c; break;` (c a nonzero constant) reached through
|
|
89
|
+
* `if`s only, so the `break` is that loop's own. */
|
|
90
|
+
function hitsIn(body: readonly Stmt[], flag: string): number {
|
|
91
|
+
return body.reduce((n, s, i) => {
|
|
92
|
+
const next = body[i + 1];
|
|
93
|
+
if (
|
|
94
|
+
s.k === 'assign' &&
|
|
95
|
+
s.name === flag &&
|
|
96
|
+
s.value.k === 'const' &&
|
|
97
|
+
s.value.value !== 0 &&
|
|
98
|
+
next?.k === 'break' &&
|
|
99
|
+
next.ofSwitch !== true &&
|
|
100
|
+
next.label === undefined
|
|
101
|
+
) {
|
|
102
|
+
return n + 1;
|
|
103
|
+
}
|
|
104
|
+
return s.k === 'if' ? n + hitsIn(s.then, flag) + hitsIn(s.else, flag) : n;
|
|
105
|
+
}, 0);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** `body` with each hit `hitsIn` counts written `goto label;` */
|
|
109
|
+
function jumpHits(body: readonly Stmt[], flag: string, label: string): Stmt[] {
|
|
110
|
+
return body.flatMap((s, i): Stmt[] => {
|
|
111
|
+
const prev = body[i - 1];
|
|
112
|
+
if (s.k === 'break' && prev?.k === 'assign' && prev.name === flag && s.ofSwitch !== true) {
|
|
113
|
+
return [];
|
|
114
|
+
}
|
|
115
|
+
if (s.k === 'assign' && s.name === flag) {
|
|
116
|
+
return [{ k: 'caseEndGoto', label }];
|
|
117
|
+
}
|
|
118
|
+
return s.k === 'if' ? [{ ...s, then: jumpHits(s.then, flag, label), else: jumpHits(s.else, flag, label) }] : [s];
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** `f = 0; LOOP; if (f == 0) { X }` starting at `list[i]`, the structurer's flag read after its loop */
|
|
123
|
+
function siteAt(
|
|
124
|
+
list: readonly Stmt[],
|
|
125
|
+
i: number,
|
|
126
|
+
): { flag: string; loop: Loop; test: Extract<Stmt, { k: 'if' }> } | null {
|
|
127
|
+
const [init, loop, test] = [list[i], list[i + 1], list[i + 2]];
|
|
128
|
+
if (
|
|
129
|
+
init?.k !== 'assign' ||
|
|
130
|
+
init.value.k !== 'const' ||
|
|
131
|
+
init.value.value !== 0 ||
|
|
132
|
+
loop === undefined ||
|
|
133
|
+
!isLoop(loop) ||
|
|
134
|
+
test?.k !== 'if' ||
|
|
135
|
+
test.else.length > 0 ||
|
|
136
|
+
flagTest(test.cond) !== init.name
|
|
137
|
+
) {
|
|
138
|
+
return null;
|
|
139
|
+
}
|
|
140
|
+
return { flag: init.name, loop, test };
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** Every flag read after its loop where the code after the loop ends its switch arm, rewritten as
|
|
144
|
+
* a case-end goto, or null where none is (the variation's decline). */
|
|
145
|
+
export function jumpToCaseEnds(sfn: SFn, gates: readonly Gate<CaseEndSite>[] = CASE_END_GOTO_GATES): SFn | null {
|
|
146
|
+
const mentions = localMentions(sfn);
|
|
147
|
+
const labels = new Set<string>();
|
|
148
|
+
const collect = (s: Stmt): void => {
|
|
149
|
+
if (s.k === 'switch') {
|
|
150
|
+
s.cases.forEach((c) => c.label !== undefined && labels.add(c.label));
|
|
151
|
+
} else if (s.k === 'break' && s.label !== undefined) {
|
|
152
|
+
labels.add(s.label);
|
|
153
|
+
}
|
|
154
|
+
stmtChildren(s).forEach(collect);
|
|
155
|
+
};
|
|
156
|
+
sfn.body.forEach(collect);
|
|
157
|
+
const fresh = (): string => {
|
|
158
|
+
let label = 'case_end';
|
|
159
|
+
for (let n = 2; labels.has(label); n++) {
|
|
160
|
+
label = `case_end_${n}`;
|
|
161
|
+
}
|
|
162
|
+
labels.add(label);
|
|
163
|
+
return label;
|
|
164
|
+
};
|
|
165
|
+
const dropped = new Set<string>();
|
|
166
|
+
|
|
167
|
+
/** `inArm`: the list is inside a switch arm with no loop between; `leaves`: falling off its end
|
|
168
|
+
* leaves that switch with nothing run on the way. */
|
|
169
|
+
const rewrite = (list: readonly Stmt[], inArm: boolean, leaves: boolean): Stmt[] => {
|
|
170
|
+
const out = [...list];
|
|
171
|
+
for (let i = 0; i < out.length; i++) {
|
|
172
|
+
const site = siteAt(out, i);
|
|
173
|
+
if (site !== null) {
|
|
174
|
+
const { flag, loop, test } = site;
|
|
175
|
+
const after = out[i + 3];
|
|
176
|
+
const end = after?.k === 'break' && after.ofSwitch === true ? after : undefined;
|
|
177
|
+
const m = mentions.get(flag);
|
|
178
|
+
const hits = hitsIn(loop.body, flag);
|
|
179
|
+
const refused = firstRejection(gates, {
|
|
180
|
+
targetNotArmEnd: end === undefined && !(after === undefined && leaves),
|
|
181
|
+
flagElsewhere:
|
|
182
|
+
m === undefined || hits === 0 || m.assigns !== hits + 1 || readsOf(m) !== 1 || m.addrTaken !== 0,
|
|
183
|
+
});
|
|
184
|
+
if (refused === null) {
|
|
185
|
+
const label = end?.label ?? fresh();
|
|
186
|
+
const jumped: Stmt = { ...loop, body: jumpHits(loop.body, flag, label) };
|
|
187
|
+
out.splice(i, end ? 4 : 3, descend(jumped, false, undefined, false), ...test.then, {
|
|
188
|
+
k: 'break',
|
|
189
|
+
ofSwitch: true,
|
|
190
|
+
label,
|
|
191
|
+
});
|
|
192
|
+
dropped.add(flag);
|
|
193
|
+
// `X` is visited next, in this list, so a site inside it whose `if` now sits right before
|
|
194
|
+
// the label takes that label
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
out[i] = descend(out[i], inArm, out[i + 1], i === out.length - 1 && leaves);
|
|
199
|
+
}
|
|
200
|
+
return out;
|
|
201
|
+
};
|
|
202
|
+
/** `s` with its lists rewritten; `next` follows it in its list, and `last` says it ends a list
|
|
203
|
+
* whose falling off leaves the switch */
|
|
204
|
+
const descend = (s: Stmt, inArm: boolean, next: Stmt | undefined, last: boolean): Stmt => {
|
|
205
|
+
if (s.k === 'switch') {
|
|
206
|
+
const arm = (c: SwitchCase): SwitchCase => ({ ...c, body: rewrite(c.body, true, !c.fallsThrough) });
|
|
207
|
+
return { ...s, cases: s.cases.map(arm), ...(s.default ? { default: rewrite(s.default, true, true) } : {}) };
|
|
208
|
+
}
|
|
209
|
+
if (isLoop(s)) {
|
|
210
|
+
return mapStmtLists(s, (l) => rewrite(l, false, false));
|
|
211
|
+
}
|
|
212
|
+
if (s.k === 'if') {
|
|
213
|
+
const leaves = inArm && ((next?.k === 'break' && next.ofSwitch === true) || (next === undefined && last));
|
|
214
|
+
return { ...s, then: rewrite(s.then, inArm, leaves), else: rewrite(s.else, inArm, leaves) };
|
|
215
|
+
}
|
|
216
|
+
return s;
|
|
217
|
+
};
|
|
218
|
+
const body = rewrite(sfn.body, false, false);
|
|
219
|
+
if (dropped.size === 0) {
|
|
220
|
+
return null;
|
|
221
|
+
}
|
|
222
|
+
return { ...sfn, body, locals: sfn.locals.filter((l) => !dropped.has(l.name)) };
|
|
223
|
+
}
|
package/src/l3/coalesce.ts
CHANGED
|
@@ -12,7 +12,16 @@
|
|
|
12
12
|
// and whether fall-through joins the two arms onto one path. `coalesceCandidates` offers both.
|
|
13
13
|
import { typeToString } from '../ir/types';
|
|
14
14
|
import type { Expr, SFn, Stmt } from './ast';
|
|
15
|
-
import {
|
|
15
|
+
import {
|
|
16
|
+
exprChildren,
|
|
17
|
+
isLoop,
|
|
18
|
+
mapExprChildren,
|
|
19
|
+
mentionedName,
|
|
20
|
+
stmtChildren,
|
|
21
|
+
stmtExprs,
|
|
22
|
+
switchArmSuccessors,
|
|
23
|
+
switchJumpsBack,
|
|
24
|
+
} from './ast';
|
|
16
25
|
import { type Gate, firstRejection } from './gates';
|
|
17
26
|
|
|
18
27
|
function namesIn(e: Expr, out: Set<string>): void {
|
|
@@ -122,8 +131,12 @@ function spans(body: Stmt[]): Map<string, Span> {
|
|
|
122
131
|
};
|
|
123
132
|
/** the loop set a while/do-while's own condition and children run under — its own. A `for` is
|
|
124
133
|
* routed separately: `stmtChildren` hands back its init too, and the init is the one child its
|
|
125
|
-
* loop does not re-run.
|
|
126
|
-
|
|
134
|
+
* loop does not re-run. A switch whose case-label `goto` lands on an arm laid out above it
|
|
135
|
+
* (`switchJumpsBack`) counts too: that jump is a back edge with no loop statement, and the arm
|
|
136
|
+
* it lands on runs after the one that jumps, so a local the lower arm writes can clobber one the
|
|
137
|
+
* upper arm still reads. */
|
|
138
|
+
const under = (s: Stmt, loops: readonly Stmt[]): readonly Stmt[] =>
|
|
139
|
+
isLoop(s) || (s.k === 'switch' && switchJumpsBack(s)) ? [...loops, s] : loops;
|
|
127
140
|
const walk = (list: Stmt[], loops: readonly Stmt[]): void => {
|
|
128
141
|
for (const s of list) {
|
|
129
142
|
// A `for`'s INIT runs ONCE, ahead of the condition and outside the loop; its cond, inc and
|
|
@@ -208,9 +221,11 @@ export interface MergePair {
|
|
|
208
221
|
* earlier one — and a back edge returns only to the head of its OWN loop, whose body is one
|
|
209
222
|
* contiguous preorder range. So the reordering can reach a PAIR only when some loop holds a
|
|
210
223
|
* mention of both locals: there the survivor's write can be followed, on the next iteration, by
|
|
211
|
-
* the absorbed local's read.
|
|
212
|
-
*
|
|
213
|
-
*
|
|
224
|
+
* the absorbed local's read. The one other back edge L3 holds is a case-label `goto` aimed at an
|
|
225
|
+
* arm laid out above it, which returns into its own switch's range the same way, so `spans`
|
|
226
|
+
* records such a switch as a loop and this rule covers it. Where no loop holds both — two sibling
|
|
227
|
+
* loops, or one local living before the loop the other lives in — no back edge connects the two
|
|
228
|
+
* ranges and preorder IS execution order. The ancestor chain is what the span records, not the innermost loop: an outer
|
|
214
229
|
* loop re-runs an inner one's statements, so it can reorder a pair that no inner loop shares.
|
|
215
230
|
*
|
|
216
231
|
* `first-is-write` AND `const-fed` ARE INDEPENDENT, and neither masks the other: a survivor whose
|
|
@@ -332,8 +347,9 @@ export interface ArmPair {
|
|
|
332
347
|
b: string;
|
|
333
348
|
/** the confining branch has a loop ancestor, so it can run more than once */
|
|
334
349
|
ifInLoop: boolean;
|
|
335
|
-
/** control can run from one of the two arms INTO the other — `switch` fall-through
|
|
336
|
-
*
|
|
350
|
+
/** control can run from one of the two arms INTO the other — `switch` fall-through or a
|
|
351
|
+
* case-label `goto`, the two ways two arms of a branch land on a single path. Always false for
|
|
352
|
+
* an `if`. */
|
|
337
353
|
armsJoined: boolean;
|
|
338
354
|
sameType: boolean;
|
|
339
355
|
/** either local is object-volatile or carries a pointee-volatile qualifier (see MergePair) */
|
|
@@ -498,9 +514,10 @@ function mentionIndex(): {
|
|
|
498
514
|
* `switch` are one rule here rather than two walkers.
|
|
499
515
|
*
|
|
500
516
|
* An `if` has exactly two arms and no way to reach one from the other. A `switch`'s arms are its
|
|
501
|
-
* `case` bodies, and
|
|
502
|
-
*
|
|
503
|
-
* TRANSITIVE chain and not merely
|
|
517
|
+
* `case` bodies, and two of them land on a single path where one runs on into the other: by
|
|
518
|
+
* FALL-THROUGH, or by the case-label `goto` that ends an arm (l3/ast.ts), which lands where a
|
|
519
|
+
* fall-through into its target would. The reach is the TRANSITIVE chain of both and not merely
|
|
520
|
+
* the adjacent pair.
|
|
504
521
|
*
|
|
505
522
|
* ITS REACH, MEASURED, because a soundness argument with no corpus witness should say so. Counted
|
|
506
523
|
* 2026-09-19 with a throwaway counter in this walk, over `pnpm bench sweep --fan --map-modes
|
|
@@ -522,10 +539,21 @@ const branchArms = (st: Stmt): { arms: Stmt[][]; joined: (i: number, j: number)
|
|
|
522
539
|
return { arms: [st.then, st.else], joined: () => false };
|
|
523
540
|
}
|
|
524
541
|
if (st.k === 'switch') {
|
|
525
|
-
|
|
542
|
+
// Arm `i` runs on into the arms `switchArmSuccessors` chains it to, by falling through or by the
|
|
543
|
+
// `goto` that ends it (l3/ast.ts) — two arms on one path either way — so `reach[i]` is that
|
|
544
|
+
// chain, the `default` included as a node (a case falling into it and jumping out of it puts
|
|
545
|
+
// both on the path), though never offered as an arm itself.
|
|
546
|
+
const next = switchArmSuccessors(st);
|
|
547
|
+
const reach = next.map((_, i) => {
|
|
548
|
+
const seen = new Set<number>();
|
|
549
|
+
for (let at = next[i]; at !== undefined && !seen.has(at); at = next[at]) {
|
|
550
|
+
seen.add(at);
|
|
551
|
+
}
|
|
552
|
+
return seen;
|
|
553
|
+
});
|
|
526
554
|
return {
|
|
527
|
-
arms: cases.map((c) => c.body),
|
|
528
|
-
joined: (i, j) =>
|
|
555
|
+
arms: st.cases.map((c) => c.body),
|
|
556
|
+
joined: (i, j) => reach[i].has(j) || reach[j].has(i),
|
|
529
557
|
};
|
|
530
558
|
}
|
|
531
559
|
return null;
|