@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/ir/alias.ts
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
// asmlift — memory DISJOINTNESS: the one place that answers "could this write change what that
|
|
2
|
+
// read sees". A pure query over L2 (typed SSA); no structuring or emission state.
|
|
3
|
+
//
|
|
4
|
+
// It exists because the answer was being given at three different strengths in three places, the
|
|
5
|
+
// weakest one governing the most common case (the materialization model's multi-render load rule,
|
|
6
|
+
// which barred on ANY write). A read that is barred by a store to an unrelated global is spelled
|
|
7
|
+
// as a named local the source never had — the "value home" defect the round-5 dogfood measured as
|
|
8
|
+
// its single highest cost. One predicate, one strength, one place to sharpen.
|
|
9
|
+
//
|
|
10
|
+
// The rule is deliberately NAME-based and deliberately narrow:
|
|
11
|
+
//
|
|
12
|
+
// • two DIFFERENT named globals are different objects, so a store through one can never change
|
|
13
|
+
// what a read of the other sees. That is a C guarantee about distinct declared objects, not a
|
|
14
|
+
// heuristic about what the compiler happened to do.
|
|
15
|
+
// • name comparison suffices because the pool promotion picks ONE canonical name per address,
|
|
16
|
+
// so a single cell cannot appear under two names within one function (frontend/thumb.ts).
|
|
17
|
+
// • anything that does not resolve to a name — a materialized base, a variable index, a pointer
|
|
18
|
+
// parameter — is unknown, and unknown BARS. A call or an `opaque` bars unconditionally: it may
|
|
19
|
+
// write anything.
|
|
20
|
+
//
|
|
21
|
+
// Being conservative here costs at most a match (an extra local the compiler would have folded);
|
|
22
|
+
// being wrong here is a silently wrong read. Every relaxation must keep that asymmetry.
|
|
23
|
+
//
|
|
24
|
+
// A SECOND PREMISE LIVES HERE, on different evidence and for a different clientele:
|
|
25
|
+
// `disjointConstSlots` — the same base VALUE, both accesses at a constant offset and width, and
|
|
26
|
+
// byte ranges that do not overlap. Where the name rule above is about two declared OBJECTS, this
|
|
27
|
+
// one is about two byte ranges of whatever single object one base denotes, so it needs no name and
|
|
28
|
+
// asserts nothing about which object that is: `p->field_0` and `p->field_4` are different cells
|
|
29
|
+
// whether `p` points at a global, a local or a parameter. Narrow in the same direction — a
|
|
30
|
+
// different base value or a runtime index BARS — and it COMPOSES with the name rule rather than
|
|
31
|
+
// widening it: a caller asks whichever question its evidence can answer, and a bar from either
|
|
32
|
+
// stands.
|
|
33
|
+
//
|
|
34
|
+
// ONE OTHER PLACE ANSWERS THE SAME QUESTION, on a different premise, and it is not reachable from
|
|
35
|
+
// here: `l3/unreduce.ts`'s `moved-read-aliasable` gate, which asks whether moving a read down to
|
|
36
|
+
// the point that re-reads it lets the writes it crosses change what it sees. It runs on L3, where there are no `Value`s
|
|
37
|
+
// and no `defs` map to resolve, and the addresses it is about are RAW CONSTANTS — precisely the
|
|
38
|
+
// case in which `globalCellOf` returns null and everything here bars. What it uses instead is the
|
|
39
|
+
// TARGET's declared device-register range (`capabilities.deviceRegisters`): a write to a hardware
|
|
40
|
+
// register is not a write to any object a C program declares, so no STORE THE C PERFORMS over that
|
|
41
|
+
// span can change an ordinary read. That is a fact about the board rather than about C, which is
|
|
42
|
+
// why it is a target capability and not a rule in this file. The asymmetry above is kept in both:
|
|
43
|
+
// everything the range does not place BARS.
|
|
44
|
+
//
|
|
45
|
+
// AND IT IS NOT THE WHOLE ANSWER, which this comment used to claim it was. The sentence above
|
|
46
|
+
// covers the CPU's stores and stops there — a DMA controller reads a control word and then WRITES
|
|
47
|
+
// ORDINARY MEMORY on the program's behalf, so a span whose every write is a "device register"
|
|
48
|
+
// write can still rewrite the cell a moved read reads. Executed with the transfer modelled, the
|
|
49
|
+
// admitted candidate turned a clean destination walk into wild writes. The second half of the
|
|
50
|
+
// claim is therefore a second datum, `capabilities.deviceMemoryWriters`, and what it does not
|
|
51
|
+
// settle is settled by the DIFFER instead (`Candidate.matchOnly`) rather than by a wider licence.
|
|
52
|
+
// The lesson generalises past this file: a premise about the board is still a premise, and one
|
|
53
|
+
// stated as an aside in a comment gets copied rather than checked — this one reached four files
|
|
54
|
+
// before anything executed it.
|
|
55
|
+
import { type Op, type Value } from './core';
|
|
56
|
+
|
|
57
|
+
/** A byte cell of a named global: the symbol plus the byte offset within it. */
|
|
58
|
+
export interface GlobalCell {
|
|
59
|
+
name: string;
|
|
60
|
+
byte: number;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* The named global cell an address value denotes, resolved through defs alone — `gaddr`, or
|
|
65
|
+
* `gaddr + const` in either operand order — plus the access's own `off`. Null when the address
|
|
66
|
+
* does not reduce to a name (a materialized base, a runtime index, a pointer): the caller must
|
|
67
|
+
* then treat it as unknown memory.
|
|
68
|
+
*/
|
|
69
|
+
export function globalCellOf(defs: Map<Value, Op>, addr: Value, off: number): GlobalCell | null {
|
|
70
|
+
const d = defs.get(addr);
|
|
71
|
+
if (d?.opcode === 'gaddr') {
|
|
72
|
+
return { name: d.attrs.sym as string, byte: off };
|
|
73
|
+
}
|
|
74
|
+
if (d?.opcode === 'add' && d.operands.length === 2) {
|
|
75
|
+
for (const [x, y] of [
|
|
76
|
+
[d.operands[0], d.operands[1]],
|
|
77
|
+
[d.operands[1], d.operands[0]],
|
|
78
|
+
] as const) {
|
|
79
|
+
const g = defs.get(x);
|
|
80
|
+
const c = defs.get(y);
|
|
81
|
+
if (g?.opcode === 'gaddr' && c?.opcode === 'const') {
|
|
82
|
+
return { name: g.attrs.sym as string, byte: (c.attrs.value as number) + off };
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Are these two accesses through ONE base provably different byte cells — same base value, both at
|
|
91
|
+
* a constant offset and width, ranges non-overlapping? The everyday struct interleave
|
|
92
|
+
* `… = p->field_0; p->field_4 = …`, where the store cannot change what the load sees even though
|
|
93
|
+
* neither side resolves to a named global.
|
|
94
|
+
*
|
|
95
|
+
* False on anything less certain, which is what the callers need: a different base value, or an
|
|
96
|
+
* access with no constant slot to compare. `off`/`width` are contract-required on `load`/`store`
|
|
97
|
+
* (ir/opcodes.ts), so the casts are defensive; the comparisons are the ones the fused call site in
|
|
98
|
+
* structure/analysis.ts made, NaN behaviour included.
|
|
99
|
+
*/
|
|
100
|
+
export function disjointConstSlots(load: Op, store: Op): boolean {
|
|
101
|
+
if (store.operands[0] !== load.operands[0]) {
|
|
102
|
+
return false;
|
|
103
|
+
}
|
|
104
|
+
const lo = load.attrs.off as number,
|
|
105
|
+
lw = load.attrs.width as number;
|
|
106
|
+
const so = store.attrs.off as number,
|
|
107
|
+
sw = store.attrs.width as number;
|
|
108
|
+
return so + sw <= lo || lo + lw <= so;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* "May op `x` write the global named `sym`?" — the predicate a read of `sym` must clear on every
|
|
113
|
+
* path between its def and each of its render positions (analysis.ts `memWriteBetween`).
|
|
114
|
+
*
|
|
115
|
+
* Calls and opaques always may. A store/astore may unless its base resolves to a DIFFERENT named
|
|
116
|
+
* global. Everything else (pure arithmetic, loads) never writes.
|
|
117
|
+
*/
|
|
118
|
+
export function mayWriteGlobal(defs: Map<Value, Op>, sym: string): (x: Op) => boolean {
|
|
119
|
+
return (x: Op): boolean => {
|
|
120
|
+
if (x.opcode === 'call' || x.opcode === 'opaque') {
|
|
121
|
+
return true;
|
|
122
|
+
}
|
|
123
|
+
if (x.opcode !== 'store' && x.opcode !== 'astore') {
|
|
124
|
+
return false;
|
|
125
|
+
}
|
|
126
|
+
const t = globalCellOf(defs, x.operands[0], 0);
|
|
127
|
+
return !(t && t.name !== sym);
|
|
128
|
+
};
|
|
129
|
+
}
|
package/src/ir/bits.ts
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
// KNOWN BITS over SSA values — an L2 fact about `Fn`, answered from declarations and instructions
|
|
2
|
+
// alone.
|
|
3
|
+
//
|
|
4
|
+
// This is not a spelling rule and it says nothing about C. It answers "how many significant bits
|
|
5
|
+
// can this value have", which is what every rule that needs to know whether a narrower container
|
|
6
|
+
// LOSES something must ask: the mask-and-insert fold's truncation bound (structure.ts), and the
|
|
7
|
+
// same question raise/narrowlocal.ts states for itself as "every value arriving on an in-edge is
|
|
8
|
+
// itself an extension of at most this width, or a constant".
|
|
9
|
+
//
|
|
10
|
+
// It is a function over `Fn` rather than a closure inside a rendering pass so that a test can ask
|
|
11
|
+
// it directly: reachable only through emitted C, a wrong answer for one opcode reads as a silent
|
|
12
|
+
// wrong address in a row nobody is looking at.
|
|
13
|
+
import type { Op, Value } from './core';
|
|
14
|
+
|
|
15
|
+
/** A materialized def emits its own named temp, so its value is a VARIABLE at every use — never a
|
|
16
|
+
* literal this analysis may fold through. Callers that have no materialization model pass none. */
|
|
17
|
+
export interface BitsCtx {
|
|
18
|
+
defs: Map<Value, Op>;
|
|
19
|
+
materialize?: ReadonlySet<Op>;
|
|
20
|
+
/** A bound a CALLER already knows for a def — the escape hatch for facts this layer cannot see,
|
|
21
|
+
* such as a bitfield read a rendering pass has recognized and can price from the declaration.
|
|
22
|
+
* Returning null defers to the rules below. A caller's answer must be a bound on the VALUE, not
|
|
23
|
+
* on the container it came out of: a SIGNED narrow read carries all 32 bits however few the
|
|
24
|
+
* declaration allots it. */
|
|
25
|
+
bound?: (d: Op) => number | null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** A def's value as a compile-time CONSTANT, or null. A thumb `bic` lifts as `and` with `neg`/
|
|
29
|
+
* `not` of a constant, so those two spellings fold; anything else is not a literal. */
|
|
30
|
+
export function constMask(ctx: BitsCtx, v: Value): number | null {
|
|
31
|
+
const d = ctx.defs.get(v);
|
|
32
|
+
if (!d || ctx.materialize?.has(d)) {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
if (d.opcode === 'const') {
|
|
36
|
+
return (d.attrs.value as number) | 0;
|
|
37
|
+
}
|
|
38
|
+
if ((d.opcode === 'neg' || d.opcode === 'not') && d.operands.length === 1) {
|
|
39
|
+
const a = constMask(ctx, d.operands[0]);
|
|
40
|
+
return a === null ? null : (d.opcode === 'neg' ? -a : ~a) | 0;
|
|
41
|
+
}
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** An UPPER BOUND on the significant bits of a value, or 32 when nothing bounds it. Every answer
|
|
46
|
+
* below 32 comes from an instruction or from a caller's declaration-backed `bound`:
|
|
47
|
+
* · a zero-fill shift right by n leaves 32 - n;
|
|
48
|
+
* · a load leaves its width in bits — UNLESS it sign-extends, which leaves all 32;
|
|
49
|
+
* · an `and` with a non-negative constant leaves that constant's top set bit.
|
|
50
|
+
* A SIGNED anything is 32: sign extension sets the high bits, and a bound that ignores it turns a
|
|
51
|
+
* correct refusal into a plausible wrong answer. */
|
|
52
|
+
export function provableBits(ctx: BitsCtx, v: Value): number {
|
|
53
|
+
const d = ctx.defs.get(v);
|
|
54
|
+
if (!d) {
|
|
55
|
+
return 32;
|
|
56
|
+
}
|
|
57
|
+
const given = ctx.bound?.(d);
|
|
58
|
+
if (given !== null && given !== undefined) {
|
|
59
|
+
return given;
|
|
60
|
+
}
|
|
61
|
+
if (d.opcode === 'shr_u' && d.operands.length === 1 && typeof d.attrs.imm === 'number') {
|
|
62
|
+
return 32 - (d.attrs.imm as number);
|
|
63
|
+
}
|
|
64
|
+
if (d.opcode === 'load') {
|
|
65
|
+
return d.attrs.signed === true ? 32 : (d.attrs.width as number) * 8;
|
|
66
|
+
}
|
|
67
|
+
const m =
|
|
68
|
+
d.opcode === 'const'
|
|
69
|
+
? constMask(ctx, v)
|
|
70
|
+
: d.opcode === 'and'
|
|
71
|
+
? (d.operands.map((o) => constMask(ctx, o)).find((x) => x !== null) ??
|
|
72
|
+
(typeof d.attrs.imm === 'number' ? (d.attrs.imm as number) | 0 : null))
|
|
73
|
+
: null;
|
|
74
|
+
return m !== null && m >= 0 ? 32 - Math.clz32(m) : 32;
|
|
75
|
+
}
|
package/src/ir/core.ts
CHANGED
|
@@ -42,6 +42,122 @@ export interface Block {
|
|
|
42
42
|
export interface Fn {
|
|
43
43
|
name: string;
|
|
44
44
|
blocks: Block[];
|
|
45
|
+
/** L1 SIDE DATA (see {@link WriteOrder}); set by the SSA builder, `undefined` on parsed IR.
|
|
46
|
+
* REQUIRED, not optional, and the `| undefined` is the point: every place that builds an `Fn`
|
|
47
|
+
* — the copy in `cli/src/report.ts` most of all — has to say what it does with the record, so
|
|
48
|
+
* a side table added to `Fn` cannot be dropped by a copy that simply never mentions it. */
|
|
49
|
+
writeOrder: WriteOrder | undefined;
|
|
50
|
+
/** L1 SIDE DATA (see {@link SlotHomes}); set by the SSA builder, `undefined` on parsed IR.
|
|
51
|
+
* REQUIRED-but-possibly-undefined for exactly the reason `writeOrder` is: an optional field is
|
|
52
|
+
* silently dropped by a copy that never mentions it (`cli/src/report.ts` structuredCloneFn says
|
|
53
|
+
* so at its own definition), and a fact the structurer reads but the score probe's clone drops
|
|
54
|
+
* makes that probe's delta a fact about a program asmlift does not emit. */
|
|
55
|
+
slotHomes: SlotHomes | undefined;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Which `[sp,#k]` the machine homed a value at — the frame coordinate, carried L1 → L3.
|
|
59
|
+
*
|
|
60
|
+
* The coordinate exists only in the frontends: they record a word sp-relative slot's value in SSA
|
|
61
|
+
* under the key `sp@k` (`frontend/ssa.ts` stackSlotKey) instead of emitting a store through `sp`,
|
|
62
|
+
* so by L2 the value is an ordinary SSA value and `k` is gone. This map is where `k` survives.
|
|
63
|
+
*
|
|
64
|
+
* A KEY IS NOT A FRAME COORDINATE, and the frontends differ on exactly that. `sp@40` is a local
|
|
65
|
+
* on one ABI and the caller's fifth argument on another, so the shared stamp asks the frontend's
|
|
66
|
+
* own `LiveInModel.declaredLocals` and refuses an offset outside it — Thumb declares a range,
|
|
67
|
+
* MIPS and PPC declare no partition and so stamp nothing. An entry here therefore means "storage
|
|
68
|
+
* this function DECLARES, homed at k", not "some sp-relative key".
|
|
69
|
+
*
|
|
70
|
+
* DECLARES, NOT OWNS, AND THE DIFFERENCE IS AN AGBCC FACT WITH A LIVE DEPENDENCY: `ownedLocals`,
|
|
71
|
+
* the partition a def-less READ asks, admits agbcc's outgoing stack-argument area, and an offset
|
|
72
|
+
* there is an ABI position rather than an `expand_decl` rank. Under Thumb the two ranges are
|
|
73
|
+
* written equal, and a decline rather than a proof is what makes that safe — see `declaredLocals`
|
|
74
|
+
* (frontend/ssa.ts) for the dependency and what lifting it obliges.
|
|
75
|
+
*
|
|
76
|
+
* ONE consumer reads it for its content — the structurer, which turns it into
|
|
77
|
+
* `SFn.locals[i].slots`; everything else only carries it (`replaceAllUsesWith`, the report's
|
|
78
|
+
* clone). It exists because a compiler that hands out frame slots by declaration rank makes the
|
|
79
|
+
* source's DECLARATION ORDER observable in the object. Absent entries are the norm: most values
|
|
80
|
+
* are never spilled, and a value with no entry simply has no frame coordinate to order by.
|
|
81
|
+
*
|
|
82
|
+
* A SET, AND NOTHING MERGES IT. Every place two homes meet — this map's own writes, a value that
|
|
83
|
+
* inherits another's uses in `replaceAllUsesWith`, several values under one name in the
|
|
84
|
+
* structurer's naming walk, two named locals absorbed into one by `l3/coalesce.ts` — takes the
|
|
85
|
+
* UNION. None of those four sites picks an offset, because picking one is a question about the
|
|
86
|
+
* TARGET and not one of them holds a target: the SSA builder takes `(name, blockCount, preds,
|
|
87
|
+
* liveInOf)`, `replaceAllUsesWith` takes an `Fn`, and `localsAfterMerge` takes a locals list.
|
|
88
|
+
*
|
|
89
|
+
* The reduction happens ONCE, in `l3/slotorder.ts`, which is the one place that holds
|
|
90
|
+
* `SFn.slotOrder`. Earliest declaration rank is the LOWEST offset under an ascending frame and
|
|
91
|
+
* the HIGHEST under a descending one, so a merge site that took an END of the set would be
|
|
92
|
+
* spelling one direction's answer under a neutral name: right for agbcc, inverted for a target
|
|
93
|
+
* whose measured direction is `descending` (ido7.1). Reducing where the direction is in hand
|
|
94
|
+
* makes it one comparator rather than four copies of a sentence.
|
|
95
|
+
*
|
|
96
|
+
* It is still a POLICY and not a proof: the machine homed one value at two offsets and the source
|
|
97
|
+
* declared one local, so no reading of the asm recovers which rank the source had. What the union
|
|
98
|
+
* buys is that the choice is made where it can be made correctly.
|
|
99
|
+
*
|
|
100
|
+
* NO `ir/verify.ts` RULE, unlike `writeOrder`: that record is a per-FUNCTION measurement with a
|
|
101
|
+
* mixed state to reject (some blocks measured, others not). A slot home is per VALUE and
|
|
102
|
+
* legitimately absent on almost every value, so there is no mixed state for a verifier to catch
|
|
103
|
+
* and a rule would never fire. */
|
|
104
|
+
export type SlotHomes = Map<Value, Set<number>>;
|
|
105
|
+
|
|
106
|
+
/** The order in which each block WROTE the keys its successors' block-params stand for — a
|
|
107
|
+
* measurement the SSA builder makes and nothing downstream can recover, because the value graph
|
|
108
|
+
* keeps no trace of it: a register copy is the same SSA value under a new key, and once a key's
|
|
109
|
+
* value is a successor's edge argument the write that put it there has no op of its own.
|
|
110
|
+
*
|
|
111
|
+
* The structurer needs it to spell a parallel copy in the compiler's own order. The compiler
|
|
112
|
+
* established the block's final register values in SOME order, and in a cyclic copy the register
|
|
113
|
+
* that reached its final value FIRST is the one whose old value had to be saved elsewhere — the
|
|
114
|
+
* temp — because the copies that still read that old value run after it. Sorting an edge's copies
|
|
115
|
+
* by this record puts that copy first, so the sequentializer's first-copy spill reproduces the
|
|
116
|
+
* compiler's temp instead of guessing from def positions, where an in-block def and an incoming
|
|
117
|
+
* param have no common scale. LAST write, not first: a predecessor commonly writes one key several
|
|
118
|
+
* times (1,867 of 5,283 records over three checkouts), and the edge carries what the last left.
|
|
119
|
+
*
|
|
120
|
+
* Keyed by OBJECTS (the predecessor block, the destination param), never by arg position, so the
|
|
121
|
+
* param splices in `ir/simplify.ts` cannot leave it stale. A pass that moves one block's ops into
|
|
122
|
+
* another owes `foldWriteOrder`.
|
|
123
|
+
*
|
|
124
|
+
* MEASUREMENT IS PER FUNCTION, NOT PER BLOCK. The SSA builder measures every block it builds, so
|
|
125
|
+
* `writes` covers every block or the record is absent/empty (parsed or hand-built IR — nobody
|
|
126
|
+
* measured this function); `ir/verify.ts` rejects the mixed state, and says there what that check
|
|
127
|
+
* does and does not reach. A reader still asks per BLOCK (`structure.ts` `predIsMeasured`), which
|
|
128
|
+
* is how a wholly unmeasured fn takes the def-position proxy without a second query — but a
|
|
129
|
+
* missing entry means "no frontend measured this function", never "this block wrote nothing". The
|
|
130
|
+
* latter is an entry whose `lastWrite` holds no destination of the edge, its own case with its own
|
|
131
|
+
* golden. */
|
|
132
|
+
export interface WriteOrder {
|
|
133
|
+
/** pred → (param of a successor → ordinal, among the pred's writes, of its LAST write to the key
|
|
134
|
+
* that param stands for). No entry ⇒ the pred did not write the key; the arg passes through. */
|
|
135
|
+
lastWrite: Map<Block, Map<Value, number>>;
|
|
136
|
+
/** Every measured block → how many writes it made. Membership is the "measured" test. */
|
|
137
|
+
writes: Map<Block, number>;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** `from`'s writes now happen at the END of `into` — the bookkeeping a pass owes when it moves one
|
|
141
|
+
* block's ops (or the edge copies an empty block stood for) into another. Each of `from`'s records
|
|
142
|
+
* lands under `into` at its ordinal plus `into`'s own write count, so a key `into` wrote itself
|
|
143
|
+
* still sorts first and a later fold onto `into` composes the same way. REFUSES when `into` was
|
|
144
|
+
* never measured: a block no builder counted has no place to put `from`'s writes after, and
|
|
145
|
+
* reading it as "wrote nothing of its own" would be a guess. `from`'s own entries stay; a block a
|
|
146
|
+
* pass removes from the CFG is never read again. */
|
|
147
|
+
export function foldWriteOrder(order: WriteOrder | undefined, from: Block, into: Block): void {
|
|
148
|
+
const base = order?.writes.get(into);
|
|
149
|
+
if (order === undefined || base === undefined) {
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
const moved = order.lastWrite.get(from);
|
|
153
|
+
if (moved !== undefined) {
|
|
154
|
+
const rec = order.lastWrite.get(into) ?? new Map<Value, number>();
|
|
155
|
+
for (const [param, at] of moved) {
|
|
156
|
+
rec.set(param, base + at);
|
|
157
|
+
}
|
|
158
|
+
order.lastWrite.set(into, rec);
|
|
159
|
+
}
|
|
160
|
+
order.writes.set(into, base + (order.writes.get(from) ?? 0));
|
|
45
161
|
}
|
|
46
162
|
|
|
47
163
|
export function mkValue(type: IrType): Value {
|
|
@@ -58,10 +174,53 @@ export function mkOp(opcode: Opcode, o: Partial<Op> = {}): Op {
|
|
|
58
174
|
};
|
|
59
175
|
}
|
|
60
176
|
|
|
177
|
+
/** A block's last op — its terminator on well-formed IR, `undefined` on a block with no ops.
|
|
178
|
+
*
|
|
179
|
+
* `ir/verify.ts` rejects an empty block and every entry path verifies before raising, so the
|
|
180
|
+
* `undefined` case is reachable only from hand-built IR: a test, or a pass reading a block it is
|
|
181
|
+
* itself midway through rewriting. A reader that DECIDES something off the terminator therefore
|
|
182
|
+
* spells that case (`terminator(b)?.opcode === 'br'`); one that has already established the shape
|
|
183
|
+
* indexes `b.ops` directly, so a broken invariant surfaces as a TypeError rather than as a silent
|
|
184
|
+
* skip. */
|
|
185
|
+
export function terminator(b: Block): Op | undefined {
|
|
186
|
+
return b.ops[b.ops.length - 1];
|
|
187
|
+
}
|
|
188
|
+
|
|
61
189
|
/** The successor blocks of `b`, read off its terminator. */
|
|
62
190
|
export function successorsOf(b: Block): Block[] {
|
|
63
|
-
|
|
64
|
-
|
|
191
|
+
return terminator(b)?.successors.map((s) => s.block) ?? [];
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/** The blocks reachable from the entry along successor edges — the entry's own reflexive closure,
|
|
195
|
+
* so a function with no blocks yields the empty set rather than a set holding `undefined`.
|
|
196
|
+
*
|
|
197
|
+
* A SET, so the walk order is not observable and no caller can come to depend on it. Three passes
|
|
198
|
+
* ask this question — `contracts.ts` (an unreachable block's call is legitimately never emitted),
|
|
199
|
+
* `pipeline.ts`'s opaque attribution, and `raise/shortcircuit.ts`'s relay drop — and each had its
|
|
200
|
+
* own copy. They agree on every input any of them sees, because `successorsOf` reads the
|
|
201
|
+
* terminator and `verify` rejects successors on a non-terminator.
|
|
202
|
+
*
|
|
203
|
+
* REACHABILITY, not predecessor count: in-edges from blocks that are themselves unreachable leave
|
|
204
|
+
* a block just as orphaned, and the thumb frontend does hand over unreachable blocks. `raise/gvn.ts`
|
|
205
|
+
* keeps a walk of its own rather than calling this one — it descends through EVERY op's successors,
|
|
206
|
+
* which is the same relation on verified IR and a wider one on the hand-built IR its own note is
|
|
207
|
+
* about. */
|
|
208
|
+
export function reachableBlocks(fn: Fn): Set<Block> {
|
|
209
|
+
const seen = new Set<Block>();
|
|
210
|
+
const entry = fn.blocks[0];
|
|
211
|
+
if (entry === undefined) {
|
|
212
|
+
return seen;
|
|
213
|
+
}
|
|
214
|
+
seen.add(entry);
|
|
215
|
+
for (const stack = [entry]; stack.length;) {
|
|
216
|
+
for (const s of successorsOf(stack.pop()!)) {
|
|
217
|
+
if (!seen.has(s)) {
|
|
218
|
+
seen.add(s);
|
|
219
|
+
stack.push(s);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
return seen;
|
|
65
224
|
}
|
|
66
225
|
|
|
67
226
|
/** Predecessor map for the whole function's CFG. */
|
|
@@ -78,6 +237,159 @@ export function predecessors(fn: Fn): Map<Block, Block[]> {
|
|
|
78
237
|
return preds;
|
|
79
238
|
}
|
|
80
239
|
|
|
240
|
+
/** Forward dominators (iterative data-flow). dom(b) = {b} ∪ ⋂ dom(preds).
|
|
241
|
+
*
|
|
242
|
+
* A CFG fact, so it lives beside `predecessors` it is built on rather than with either consumer:
|
|
243
|
+
* `verify` needs it to check def-dominates-use, `structure/loops.ts` to tell a back-edge from a
|
|
244
|
+
* forward one, and `raise/latch.ts` to tell a latch from a preheader. */
|
|
245
|
+
|
|
246
|
+
export function dominators(fn: Fn): Map<Block, Set<Block>> {
|
|
247
|
+
const preds = predecessors(fn);
|
|
248
|
+
const all = new Set(fn.blocks);
|
|
249
|
+
const dom = new Map<Block, Set<Block>>();
|
|
250
|
+
fn.blocks.forEach((b, i) => dom.set(b, i === 0 ? new Set([b]) : new Set(all)));
|
|
251
|
+
for (let changed = true; changed;) {
|
|
252
|
+
changed = false;
|
|
253
|
+
for (const b of fn.blocks.slice(1)) {
|
|
254
|
+
let inter: Set<Block> | null = null;
|
|
255
|
+
for (const p of preds.get(b)!) {
|
|
256
|
+
const dp = dom.get(p)!;
|
|
257
|
+
if (inter === null) {
|
|
258
|
+
inter = new Set(dp);
|
|
259
|
+
continue;
|
|
260
|
+
}
|
|
261
|
+
for (const x of inter) {
|
|
262
|
+
if (!dp.has(x)) {
|
|
263
|
+
inter.delete(x);
|
|
264
|
+
}
|
|
265
|
+
} // intersect in place (spec-safe delete-in-iter)
|
|
266
|
+
}
|
|
267
|
+
const next = new Set<Block>(inter ?? []);
|
|
268
|
+
next.add(b);
|
|
269
|
+
const prev = dom.get(b)!;
|
|
270
|
+
if (next.size !== prev.size || [...next].some((x) => !prev.has(x))) {
|
|
271
|
+
dom.set(b, next);
|
|
272
|
+
changed = true;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
return dom;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/** A block with NO BODY OF ITS OWN: it declares no parameters and holds a single op, which is
|
|
280
|
+
* therefore its terminator. It computes nothing and it binds nothing. Three sites ask this and
|
|
281
|
+
* each adds its own clause (`forwardingTarget` below wants a `br` carrying no args;
|
|
282
|
+
* switch-recover.ts's `isBareExit` admits a `ret` as well; raise/retsink.ts asks it of a
|
|
283
|
+
* predecessor, to tell a decision that RAN OUT from an arm that ran ON), so the shared half is
|
|
284
|
+
* stated once, here.
|
|
285
|
+
*
|
|
286
|
+
* THE PARAMETER CLAUSE IS THE LOAD-BEARING HALF, and the reason this is not spelled
|
|
287
|
+
* `ops.length === 1`: a case ENTRY whose arm is EMPTY is also one op — the jump onwards — but it
|
|
288
|
+
* takes the accumulator as a block parameter, so it is a real arm of a dispatch and not a
|
|
289
|
+
* forwarder. */
|
|
290
|
+
export function isBodyless(blk: Block): boolean {
|
|
291
|
+
return blk.params.length === 0 && blk.ops.length === 1;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/** Where a chain of TRANSPARENT forwarding blocks lands — no params, a lone `br`, no block args.
|
|
295
|
+
*
|
|
296
|
+
* A compiler that cannot reach a target from a conditional branch emits the real branch separately
|
|
297
|
+
* (agbcc past Thumb's ±256-byte range; the binary-search layout of a `switch`), so two sites
|
|
298
|
+
* reaching one block arrive as two DISTINCT forwarding blocks. A recogniser keyed on successor
|
|
299
|
+
* identity needs the destination, not the edge.
|
|
300
|
+
*
|
|
301
|
+
* An edge carrying block ARGUMENTS is not transparent — skipping it would drop the value it
|
|
302
|
+
* supplies — so the walk stops there. Read-only: nothing about the graph changes.
|
|
303
|
+
*
|
|
304
|
+
* Two sites test a similar shape and are deliberately NOT callers, both because they KEEP the
|
|
305
|
+
* args this refuses to walk past: raise/latch.ts's `foldEmptyLatches` rewrites the edge and
|
|
306
|
+
* carries the forwarder's args onto it; structure/switch-recover.ts's `resolveDefault` walks onto
|
|
307
|
+
* a default candidate through a `b .Ldefault(v)` and turns that step into one more dispatch edge,
|
|
308
|
+
* whose copies the hoist re-emits above the `switch`. */
|
|
309
|
+
export function forwardingTarget(b: Block): Block {
|
|
310
|
+
const seen = new Set<Block>();
|
|
311
|
+
let cur = b;
|
|
312
|
+
while (isBodyless(cur) && !seen.has(cur)) {
|
|
313
|
+
seen.add(cur);
|
|
314
|
+
const t = cur.ops[0];
|
|
315
|
+
if (t.opcode !== 'br' || t.successors[0].args.length > 0) {
|
|
316
|
+
break;
|
|
317
|
+
}
|
|
318
|
+
cur = t.successors[0].block;
|
|
319
|
+
}
|
|
320
|
+
return cur;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/** THE MERGE CLASSES: every value that rides a branch edge, grouped with the block parameter it
|
|
324
|
+
* binds — transitively, so a value forwarded across several merges lands in one class.
|
|
325
|
+
*
|
|
326
|
+
* Functional-form SSA has no phi node: a register merge is spelled as an edge ARGUMENT plus a
|
|
327
|
+
* block PARAMETER, two names for the one value a register carried. So a rule that counts what a
|
|
328
|
+
* value is USED for reads one merged value as N values with N use counts, and any threshold it
|
|
329
|
+
* applies (`structure/analysis.ts`'s "base of 2+ accesses") is measured against the wrong
|
|
330
|
+
* denominator. This is the closure that restores it.
|
|
331
|
+
*
|
|
332
|
+
* A value on no edge is its own class and is absent from the map — read it as `get(v) ?? [v]`.
|
|
333
|
+
* Member order is definition order (block order, params before results), so a caller that reports
|
|
334
|
+
* a class reports it deterministically.
|
|
335
|
+
*
|
|
336
|
+
* A CFG/SSA fact rather than a rule of any one pass, so it lives here beside `dominators`. Two
|
|
337
|
+
* callers took it: `structure/analysis.ts`'s shared-base scope, and `structure/structure.ts`'s
|
|
338
|
+
* signed-use cone, which had hand-rolled the same arg↔param map.
|
|
339
|
+
*
|
|
340
|
+
* THE THIRD SITE IS NOT THIS RELATION, and saying so is the point of naming it here.
|
|
341
|
+
* `raise/recover.ts`'s `propagatePointers` runs its own union-find over the edge relation PLUS
|
|
342
|
+
* the const-offset `add`/`sub` (a pointer ± an integer stays the same pointer), which is a
|
|
343
|
+
* strictly larger relation and a typing rule of that pass rather than a fact about the CFG.
|
|
344
|
+
* Rewriting it to start from this map would make it seed a pass-specific union on top, i.e. give
|
|
345
|
+
* a CFG fact a parameter for one caller's extra edges — so it stays where it is, and what this
|
|
346
|
+
* paragraph buys is that the next reader looking for a fourth copy knows which of the two the
|
|
347
|
+
* third one is. */
|
|
348
|
+
export function mergeClasses(fn: Fn): Map<Value, readonly Value[]> {
|
|
349
|
+
const parent = new Map<Value, Value>();
|
|
350
|
+
const find = (v: Value): Value => {
|
|
351
|
+
let r = v;
|
|
352
|
+
while ((parent.get(r) ?? r) !== r) {
|
|
353
|
+
r = parent.get(r)!;
|
|
354
|
+
}
|
|
355
|
+
for (let c = v; (parent.get(c) ?? r) !== r;) {
|
|
356
|
+
const n = parent.get(c)!;
|
|
357
|
+
parent.set(c, r);
|
|
358
|
+
c = n;
|
|
359
|
+
}
|
|
360
|
+
parent.set(r, r);
|
|
361
|
+
return r;
|
|
362
|
+
};
|
|
363
|
+
for (const b of fn.blocks) {
|
|
364
|
+
for (const op of b.ops) {
|
|
365
|
+
for (const s of op.successors) {
|
|
366
|
+
// `verify` pins arg/param arity, but this also runs on hand-built IR in tests — take the
|
|
367
|
+
// overlap rather than index past the end.
|
|
368
|
+
const n = Math.min(s.args.length, s.block.params.length);
|
|
369
|
+
for (let i = 0; i < n; i++) {
|
|
370
|
+
parent.set(find(s.args[i]), find(s.block.params[i]));
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
const byRoot = new Map<Value, Value[]>();
|
|
376
|
+
for (const b of fn.blocks) {
|
|
377
|
+
for (const v of [...b.params, ...b.ops.flatMap((op) => op.results)]) {
|
|
378
|
+
if (parent.has(v)) {
|
|
379
|
+
const root = find(v);
|
|
380
|
+
(byRoot.get(root) ?? byRoot.set(root, []).get(root)!).push(v);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
const out = new Map<Value, readonly Value[]>();
|
|
385
|
+
for (const members of byRoot.values()) {
|
|
386
|
+
for (const v of members) {
|
|
387
|
+
out.set(v, members);
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
return out;
|
|
391
|
+
}
|
|
392
|
+
|
|
81
393
|
/** Every value defined by an op result → its defining op (block params excluded). */
|
|
82
394
|
export function defOpMap(fn: Fn): Map<Value, Op> {
|
|
83
395
|
const m = new Map<Value, Op>();
|
|
@@ -93,6 +405,29 @@ export function defOpMap(fn: Fn): Map<Value, Op> {
|
|
|
93
405
|
|
|
94
406
|
/** Replace every use of `oldV` with `newV` (operands + successor args). No in-place op mutation. */
|
|
95
407
|
export function replaceAllUsesWith(fn: Fn, oldV: Value, newV: Value): void {
|
|
408
|
+
// The frame coordinate follows the value that inherits the uses. Without this the home stays on
|
|
409
|
+
// a value nothing reads any more while the local the structurer names — `newV` — carries none,
|
|
410
|
+
// and the declaration list loses its order for that local. UNION, never a choice: this helper is
|
|
411
|
+
// handed an `Fn` and an `Fn` carries no target, so it cannot know whether the earlier rank is the
|
|
412
|
+
// lower or the higher offset (`SlotHomes`). `l3/slotorder.ts` decides that, once.
|
|
413
|
+
//
|
|
414
|
+
// A GUARD, MEASURED: instrumented over every agbcc case of both benchmark tiers, this fires 14
|
|
415
|
+
// times on 252 lifted synthetic functions and 112 times on 116 lifted real ones — and in every
|
|
416
|
+
// single firing the inheriting value already carried the same offset. So it has never yet SAVED
|
|
417
|
+
// a home, and the union has never yet held two. It ships because the alternative is a home
|
|
418
|
+
// stranded on a retired value, which is silent.
|
|
419
|
+
const homes = fn.slotHomes;
|
|
420
|
+
if (homes !== undefined) {
|
|
421
|
+
const from = homes.get(oldV);
|
|
422
|
+
if (from !== undefined) {
|
|
423
|
+
const to = homes.get(newV);
|
|
424
|
+
if (to === undefined) {
|
|
425
|
+
homes.set(newV, new Set(from));
|
|
426
|
+
} else {
|
|
427
|
+
from.forEach((off) => to.add(off));
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
}
|
|
96
431
|
for (const b of fn.blocks) {
|
|
97
432
|
for (const op of b.ops) {
|
|
98
433
|
op.operands = op.operands.map((v) => (v === oldV ? newV : v));
|