@asmlift/core 0.4.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 -164
- package/src/backend/cpp.ts +1 -0
- package/src/backend/pascal.ts +26 -12
- package/src/contracts.ts +341 -22
- package/src/declare.ts +41 -4
- package/src/frontend/mips.ts +24 -6
- package/src/frontend/opaque.ts +31 -18
- package/src/frontend/ppc.ts +54 -7
- package/src/frontend/ssa.ts +632 -13
- package/src/frontend/thumb.ts +2786 -286
- package/src/ir/alias.ts +129 -0
- package/src/ir/bits.ts +75 -0
- package/src/ir/core.ts +337 -2
- package/src/ir/opcodes.ts +156 -27
- 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 +8 -2
- package/src/l3/ast.ts +464 -49
- package/src/l3/basecse.ts +709 -88
- package/src/l3/coalesce.ts +521 -66
- package/src/l3/dce.ts +54 -19
- package/src/l3/gates.ts +88 -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 +649 -219
- 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 +23 -4
- 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 +206 -49
- package/src/proto.ts +112 -14
- package/src/raise/arrays.ts +6 -1
- package/src/raise/divpow2.ts +4 -3
- package/src/raise/globalshape.ts +1038 -0
- package/src/raise/gvn.ts +44 -19
- 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 +101 -16
- package/src/raise/recover.ts +56 -23
- package/src/raise/retsink.ts +215 -14
- package/src/raise/shortcircuit.ts +477 -79
- package/src/raise/struct-arrays.ts +21 -3
- package/src/raise/structs.ts +61 -3
- package/src/rank-axes.ts +630 -0
- package/src/rank-declare.ts +256 -0
- package/src/rank.ts +1726 -251
- package/src/structure/analysis.ts +1516 -220
- 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 +2850 -533
- package/src/structure/switch-recover.ts +688 -147
- package/src/symbols.ts +62 -1
- package/src/target.ts +367 -24
- package/src/trace.ts +111 -32
package/src/raise/gvn.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
// asmlift — value numbering for OPERAND-FREE PURE definitions (today
|
|
1
|
+
// asmlift — value numbering for OPERAND-FREE PURE definitions (today `gaddr` and its frame-local
|
|
2
|
+
// twin `laddr`).
|
|
2
3
|
//
|
|
3
4
|
// A compiler materializes a global's address wherever it needs one. Two arms of an `if` that both
|
|
4
5
|
// touch `gTable` each get their own pool load, so the frontend lifts two DISTINCT SSA values that
|
|
@@ -23,13 +24,20 @@
|
|
|
23
24
|
// alone, so two with equal attrs are equal in every execution, on every path, always. Replacing all
|
|
24
25
|
// of them with ONE definition is exact.
|
|
25
26
|
//
|
|
26
|
-
// THE ADMISSION RULE IS
|
|
27
|
-
// safety argument, so do not relax it to the general-sounding version.
|
|
28
|
-
//
|
|
29
|
-
//
|
|
30
|
-
//
|
|
31
|
-
//
|
|
32
|
-
//
|
|
27
|
+
// THE ADMISSION RULE IS A LIST, NOT "operand-free and pure" — and the difference is the whole
|
|
28
|
+
// safety argument, so do not relax it to the general-sounding version. Two more opcodes satisfy the
|
|
29
|
+
// general predicate and each is deliberately out, for a different reason:
|
|
30
|
+
//
|
|
31
|
+
// `const` — numbering consts function-wide would be actively HARMFUL: structure/analysis.ts
|
|
32
|
+
// materializes a multi-use `const` that is live across a call into a named local, and
|
|
33
|
+
// its own comment records that this exact widening ("the small-constant regression")
|
|
34
|
+
// already cost matches once.
|
|
35
|
+
// `undef` — numbering it would be VACUOUS: two `undef`s in one function always carry different
|
|
36
|
+
// `key`s (ir/opcodes.ts says so at the opcode), so "same key, therefore same value"
|
|
37
|
+
// never has two members to collapse.
|
|
38
|
+
//
|
|
39
|
+
// The gate is a MATCHING policy, not a property of the opcode — which is why it is not a flag on
|
|
40
|
+
// the opcode table, where `const` would satisfy it.
|
|
33
41
|
//
|
|
34
42
|
// PLACEMENT. One fresh definition per class is created in the ENTRY block, which dominates every
|
|
35
43
|
// REACHABLE block — so no reachable use can precede it (unreachable blocks are excluded from the
|
|
@@ -39,6 +47,9 @@
|
|
|
39
47
|
// analysis.ts, whose materialize-into-a-local rule covers `const`, `call` and the memory reads, NOT
|
|
40
48
|
// address ops), so the address is re-spelled at each access exactly as the original source did.
|
|
41
49
|
// Hoisting therefore does not create the long live range that hoisting a LOADED value would.
|
|
50
|
+
// That is a promise ANOTHER module keeps, so `test/addr-placement.test.ts` holds it to it: let
|
|
51
|
+
// analysis.ts materialize an address op and the entry hoist becomes a function-top local — the one
|
|
52
|
+
// this pass exists to delete, reintroduced one level up.
|
|
42
53
|
//
|
|
43
54
|
// SCOPE, deliberately narrow: `code: true` symbols (a promoted function pointer, spelled `(u32)Name`
|
|
44
55
|
// rather than `&Name`) are numbered separately from data ones, because the attr is part of what the
|
|
@@ -47,15 +58,31 @@
|
|
|
47
58
|
// THE WIN IS CONTINGENT ON THE SYMBOL MAP, which is worth knowing before relying on it. With a map
|
|
48
59
|
// supplying an array's rank the accesses render as `gSym[0][i]`, a `var` base that
|
|
49
60
|
// `l3/basecse.ts`'s `isHoistableBase` cannot see, so nothing re-creates the local this pass
|
|
50
|
-
// deleted. WITHOUT
|
|
51
|
-
//
|
|
52
|
-
//
|
|
53
|
-
//
|
|
54
|
-
//
|
|
61
|
+
// deleted. WITHOUT one the same accesses spell as `addr`, basecse sees the reuse, and it hoists a
|
|
62
|
+
// function-top `p0 = (u16 *)&gBgTilemapBufs` — the same local, one level up. Both arms are pinned
|
|
63
|
+
// in `test/addr-placement.test.ts`; before that they rested on a run nobody could repeat.
|
|
64
|
+
//
|
|
65
|
+
// Several modules answer "is this address a local?" with independent policies — here: never;
|
|
66
|
+
// basecse: at whichever of the two positions `l3/hoist.ts` is handed (the COMMITTED call states
|
|
67
|
+
// the function top, its roster admissions also offer each init's first use), when the gate table
|
|
68
|
+
// admits the base; l3/scopebase.ts: at the innermost scope holding the uses; l3/argbase.ts:
|
|
69
|
+
// immediately before a call whose arguments share it. The newer placement levers — l3/nearbase.ts,
|
|
70
|
+
// l3/inlinebase.ts, l3/homesplit.ts — answer it too, each with its own.
|
|
71
|
+
// Reconciling them is recorded debt, and the same test pins the two places they actively disagree, because a
|
|
72
|
+
// consolidation has to PICK rather than discover them: a `for`'s init (basecse reads it at loop
|
|
73
|
+
// cadence and refuses, scopebase at the enclosing one and hoists) and a global name shadowed by a
|
|
74
|
+
// local (scopebase must refuse — it re-spells the base as `&g` — while argbase may fire, because it
|
|
75
|
+
// keeps the base expression verbatim).
|
|
55
76
|
import { Block, Fn, Op, Value, mkOp, replaceAllUsesWith } from '../ir/core';
|
|
77
|
+
import type { Opcode } from '../ir/opcodes';
|
|
56
78
|
|
|
57
|
-
/** Ops whose result depends on `attrs` alone — no operands, no memory, no control flow.
|
|
58
|
-
|
|
79
|
+
/** Ops whose result depends on `attrs` alone — no operands, no memory, no control flow. The LIST,
|
|
80
|
+
* not the predicate (see the module note); `satisfies` makes a typo a compile error rather than an
|
|
81
|
+
* entry that silently matches nothing. `laddr` earns its place on the same argument `gaddr` does —
|
|
82
|
+
* operand-free, pure, attr-keyed. */
|
|
83
|
+
const NUMBERABLE = ['gaddr', 'laddr'] as const satisfies readonly Opcode[];
|
|
84
|
+
type NumberableOpcode = (typeof NUMBERABLE)[number];
|
|
85
|
+
const isNumberable = (opcode: string): opcode is NumberableOpcode => (NUMBERABLE as readonly string[]).includes(opcode);
|
|
59
86
|
|
|
60
87
|
/** The value-number key: the opcode plus every attribute, in a stable order. */
|
|
61
88
|
function keyOf(op: Op): string {
|
|
@@ -99,7 +126,7 @@ export function numberPureValues(fn: Fn): number {
|
|
|
99
126
|
return;
|
|
100
127
|
}
|
|
101
128
|
for (const op of b.ops) {
|
|
102
|
-
if (
|
|
129
|
+
if (isNumberable(op.opcode) && op.results.length === 1) {
|
|
103
130
|
const k = keyOf(op);
|
|
104
131
|
groups.set(k, [...(groups.get(k) ?? []), { op, block: bi }]);
|
|
105
132
|
}
|
|
@@ -118,9 +145,7 @@ export function numberPureValues(fn: Fn): number {
|
|
|
118
145
|
// block, so no path is left reading a definition this pass has moved.
|
|
119
146
|
const survivor = dups[0].op;
|
|
120
147
|
const value: Value = { type: survivor.results[0].type };
|
|
121
|
-
hoisted.push(
|
|
122
|
-
mkOp(survivor.opcode as Parameters<typeof mkOp>[0], { results: [value], attrs: { ...survivor.attrs } }),
|
|
123
|
-
);
|
|
148
|
+
hoisted.push(mkOp(survivor.opcode as NumberableOpcode, { results: [value], attrs: { ...survivor.attrs } }));
|
|
124
149
|
for (const d of dups) {
|
|
125
150
|
replaceAllUsesWith(fn, d.op.results[0], value);
|
|
126
151
|
removed++;
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
// asmlift — empty-latch folding (F-CFG-class structural pass; successor-aware, ISA-neutral).
|
|
2
|
+
//
|
|
3
|
+
// A loop whose back-edge carries a register copy — `add r3, r0, #0` then `b .L6` — lifts to a block
|
|
4
|
+
// holding nothing but that branch, because SSA construction turns the copy into an EDGE ARGUMENT.
|
|
5
|
+
// The loop is unchanged; its latch has just become a separate empty block between the exit test and
|
|
6
|
+
// the header. Loop DISCOVERY still finds the loop — `structure/loops.ts` reads the same back-edge —
|
|
7
|
+
// but the do-while emitter requires the latch to end in a `cond_br`, an empty one ends in `br`, and
|
|
8
|
+
// the test is at the bottom so the `while` form is unavailable too. The back-edge survives to the
|
|
9
|
+
// `onStack` refusal and the whole function declines with "unrecovered back-edge".
|
|
10
|
+
//
|
|
11
|
+
// Splicing the block out — every predecessor edge re-pointed at the header, carrying the latch's
|
|
12
|
+
// own edge arguments — restores the single-latch loop, which is a canonicalization the emitter
|
|
13
|
+
// already handles rather than a new case inside it.
|
|
14
|
+
//
|
|
15
|
+
// The arguments move soundly because the block has no params and one op: a value the latch's `br`
|
|
16
|
+
// passes dominates the latch, so it dominates the end of every predecessor of the latch too.
|
|
17
|
+
//
|
|
18
|
+
// WHY DOMINANCE, AND NOT "the target can reach this block". The two disagree on a loop PREHEADER —
|
|
19
|
+
// the same empty forwarding block seen from the other side, which dominates its header instead of
|
|
20
|
+
// the reverse. Reachability alone cannot refuse one, because an INNER loop's preheader sitting
|
|
21
|
+
// inside an OUTER loop is reachable from the inner header round the outer back-edge. Folding a
|
|
22
|
+
// preheader hands the structurer a guard branching straight at the header — the guarded-self-loop
|
|
23
|
+
// shape, where the structurer either fuses under its guard proof or keeps the guard as its own
|
|
24
|
+
// `if` (declining loud on the hazards). The guard therefore survives the fold either way; what the
|
|
25
|
+
// gate preserves is the SHAPE — a forward trampoline is not a latch, and folding one re-casts an
|
|
26
|
+
// unrelated branch as a loop guard — which is the `target-dominates` entry's own note below.
|
|
27
|
+
import { Fn, dominators, foldWriteOrder } from '../ir/core';
|
|
28
|
+
import { type Gate, firstRejection } from '../l3/gates';
|
|
29
|
+
|
|
30
|
+
/** What the gates below judge: one candidate block and the block its `br` goes to. */
|
|
31
|
+
interface LatchCandidate {
|
|
32
|
+
block: Fn['blocks'][number];
|
|
33
|
+
target: Fn['blocks'][number];
|
|
34
|
+
dominatesBlock: boolean;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export const LATCH_GATES: readonly Gate<LatchCandidate>[] = [
|
|
38
|
+
{
|
|
39
|
+
id: 'latch-has-params',
|
|
40
|
+
why: "a param means the block JOINS edges, so its br args are not this one edge's copy",
|
|
41
|
+
sound: false,
|
|
42
|
+
rejects: (c) => c.block.params.length > 0,
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
// WHAT THIS RULE ACTUALLY DECIDES, which is narrower than its id suggests. A block carrying real
|
|
46
|
+
// work never reaches the table at all: `foldEmptyLatches`' pre-check reads `ops[0].successors[0]`,
|
|
47
|
+
// so a first op that is a `store` (or any non-terminator) has no target and is refused there.
|
|
48
|
+
// What is left for this rule is the block whose sole op IS a terminator but not a `br` — a
|
|
49
|
+
// `cond_br`, whose SECOND successor the fold would discard along with the block.
|
|
50
|
+
id: 'latch-does-work',
|
|
51
|
+
why: 'the sole terminator must be an unconditional `br`: folding a `cond_br` block discards its second successor',
|
|
52
|
+
sound: true,
|
|
53
|
+
guardedBy: 'latch.test.ts: a latch whose sole op is a cond_br is refused — folding it would drop its second arm',
|
|
54
|
+
// `br` is a terminator, so a block whose FIRST op is one holds nothing but that branch.
|
|
55
|
+
rejects: (c) => c.block.ops[0]?.opcode !== 'br',
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
id: 'self-branch',
|
|
59
|
+
why: 'every block dominates itself, so this is an infinite loop rather than a trampoline',
|
|
60
|
+
sound: false,
|
|
61
|
+
rejects: (c) => c.target === c.block,
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
id: 'target-dominates',
|
|
65
|
+
why: "a preheader is the same empty block from the other side; folding it re-shapes another block's branch into a loop guard",
|
|
66
|
+
// NOT `sound`, and the burden it used to carry now lives elsewhere: the guarded-self-loop
|
|
67
|
+
// emitter refuses to fuse an unproven guard — it keeps its `if`, or declines loud — and a
|
|
68
|
+
// multi-block loop's guard has no fusion path to lose it to at all. So ablating this gate
|
|
69
|
+
// re-shapes the C without making it wrong, which is a heuristic by the Gate contract. The named
|
|
70
|
+
// test pins that second layer.
|
|
71
|
+
sound: false,
|
|
72
|
+
guardedBy: 'latch.test.ts: ablating the dominance gate hands a guard to the kept-guard loop emitter',
|
|
73
|
+
rejects: (c) => !c.dominatesBlock,
|
|
74
|
+
},
|
|
75
|
+
];
|
|
76
|
+
|
|
77
|
+
/** Splice out every EMPTY LATCH the gates above admit; returns how many were removed.
|
|
78
|
+
*
|
|
79
|
+
* Iterated, because folding one latch can make its predecessor into one: the predecessor's
|
|
80
|
+
* SUCCESSOR changes, so an edge that was not a back-edge becomes one. (Dominator sets themselves
|
|
81
|
+
* only ever shrink — removing a block removes it from every set it was in.)
|
|
82
|
+
*
|
|
83
|
+
* A predecessor that already branches to the target ends up with two edges into one block. That is
|
|
84
|
+
* a block whose every edge continues the loop, so it has no exit and loop recovery declines — the
|
|
85
|
+
* same loud decline it gave before the fold.
|
|
86
|
+
*/
|
|
87
|
+
export function foldEmptyLatches(fn: Fn, gates: readonly Gate<LatchCandidate>[] = LATCH_GATES): number {
|
|
88
|
+
let folded = 0;
|
|
89
|
+
for (;;) {
|
|
90
|
+
const dom = dominators(fn);
|
|
91
|
+
const latch = fn.blocks.find((b) => {
|
|
92
|
+
// THE PRE-CHECK IS WHAT REFUSES A WORK-CARRYING BLOCK. A block whose first op computes
|
|
93
|
+
// something — a `store`, an arithmetic op — is not a terminator and so has no successors, and
|
|
94
|
+
// a candidate with no target is not judged at all. The gates below therefore only ever see a
|
|
95
|
+
// block whose FIRST op is a terminator; `latch-does-work` is the one that then insists it be
|
|
96
|
+
// an unconditional `br`.
|
|
97
|
+
const target = b.ops[0]?.successors[0]?.block;
|
|
98
|
+
return (
|
|
99
|
+
target !== undefined &&
|
|
100
|
+
firstRejection(gates, { block: b, target, dominatesBlock: dom.get(b)!.has(target) }) === null
|
|
101
|
+
);
|
|
102
|
+
});
|
|
103
|
+
if (!latch) {
|
|
104
|
+
return folded;
|
|
105
|
+
}
|
|
106
|
+
const onward = latch.ops[0].successors[0];
|
|
107
|
+
for (const b of fn.blocks) {
|
|
108
|
+
let repointed = false;
|
|
109
|
+
for (const op of b.ops) {
|
|
110
|
+
op.successors.forEach((s, i) => {
|
|
111
|
+
if (s.block === latch) {
|
|
112
|
+
op.successors[i] = { block: onward.block, args: [...onward.args] };
|
|
113
|
+
repointed = true;
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
// The copies the latch stood for now happen at the end of this predecessor — after its own
|
|
118
|
+
// writes, which the write-order record has to keep saying (ir/core.ts `foldWriteOrder`).
|
|
119
|
+
if (repointed) {
|
|
120
|
+
foldWriteOrder(fn.writeOrder, latch, b);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
fn.blocks = fn.blocks.filter((b) => b !== latch);
|
|
124
|
+
folded++;
|
|
125
|
+
}
|
|
126
|
+
}
|