@asmlift/core 0.2.0 → 0.4.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 +5 -3
- package/package.json +1 -1
- package/src/backend/cfamily.ts +154 -5
- package/src/backend/cpp.ts +3 -1
- package/src/backend/pascal.ts +11 -0
- package/src/contracts.ts +37 -5
- package/src/declare.ts +251 -0
- package/src/frontend/frontend.ts +12 -2
- package/src/frontend/mips.ts +24 -23
- package/src/frontend/opaque.ts +39 -2
- package/src/frontend/ssa.ts +32 -53
- package/src/frontend/thumb.ts +420 -32
- package/src/ir/opcodes.ts +44 -0
- package/src/ir/simplify.ts +72 -0
- package/src/l3/argbase.ts +216 -0
- package/src/l3/ast.ts +126 -6
- package/src/l3/basecse.ts +3 -40
- package/src/l3/coalesce.ts +146 -0
- package/src/l3/dce.ts +2 -23
- package/src/l3/hoist.ts +65 -0
- package/src/l3/reindex.ts +7 -0
- package/src/l3/scopebase.ts +436 -0
- package/src/l3/symbol-refs.ts +61 -0
- package/src/l3/tailmerge.ts +120 -0
- package/src/l3/typing.ts +4 -0
- package/src/macros.ts +335 -0
- package/src/pattern/engine.ts +99 -6
- package/src/pipeline.ts +20 -6
- package/src/proto.ts +55 -0
- package/src/raise/divpow2.ts +226 -0
- package/src/raise/gvn.ts +141 -0
- package/src/raise/pre-recovery.ts +37 -3
- package/src/raise/recover.ts +24 -7
- package/src/raise/retsink.ts +36 -7
- package/src/raise/shortcircuit.ts +264 -22
- package/src/raise/structs.ts +12 -2
- package/src/rank.ts +370 -79
- package/src/structure/analysis.ts +42 -1
- package/src/structure/structure.ts +852 -67
- package/src/structure/switch-recover.ts +21 -3
- package/src/symbols.ts +541 -0
- package/src/target.ts +4 -2
- package/src/trace.ts +17 -2
package/src/frontend/frontend.ts
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
// for target→frontend dispatch.
|
|
5
5
|
import type { Fn } from '../ir/core';
|
|
6
6
|
import type { Prototypes } from '../proto';
|
|
7
|
+
import type { SymbolMap } from '../symbols';
|
|
7
8
|
import type { TargetDescription } from '../target';
|
|
8
9
|
import type { AsmData } from './asmdata';
|
|
9
10
|
import type { AsmTextFormat } from './format';
|
|
@@ -17,6 +18,15 @@ export interface Frontend {
|
|
|
17
18
|
/** decode one function's assembly into an L1 Fn. `prototypes` supplies callee arities
|
|
18
19
|
* (and any other header facts the frontend needs); an empty map is valid. `asmData` is the
|
|
19
20
|
* OPTIONAL Regime-B side-table (data-section jump tables + relocations); absent ⇒ a
|
|
20
|
-
* dense-switch dispatch declines/loud-fails.
|
|
21
|
-
|
|
21
|
+
* dense-switch dispatch declines/loud-fails. `symbols` is the OPTIONAL address→symbol map
|
|
22
|
+
* (symbols.ts); today only the Thumb frontend consumes it (numeric-pool promotion) — the
|
|
23
|
+
* MIPS/PPC objdump dialect already carries symbol names in the asm text. */
|
|
24
|
+
lift(
|
|
25
|
+
name: string,
|
|
26
|
+
asm: string,
|
|
27
|
+
target: TargetDescription,
|
|
28
|
+
prototypes: Prototypes,
|
|
29
|
+
asmData?: AsmData,
|
|
30
|
+
symbols?: SymbolMap,
|
|
31
|
+
): Fn;
|
|
22
32
|
}
|
package/src/frontend/mips.ts
CHANGED
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
// compares (`sltu`/`sltiu`) lower to `icmp_ult`; recover types their operands u32 so the backend
|
|
20
20
|
// re-emits `sltu` (the operator is the same `<` — the signedness lives in the operand types).
|
|
21
21
|
import { Fn, Op, Successor, Value, mkOp, mkValue } from '../ir/core';
|
|
22
|
-
import type
|
|
22
|
+
import { NEGATED_ICMP, type Opcode } from '../ir/opcodes';
|
|
23
23
|
import { T } from '../ir/types';
|
|
24
24
|
import type { Prototypes } from '../proto';
|
|
25
25
|
import type { TargetDescription } from '../target';
|
|
@@ -47,19 +47,6 @@ const COND_Z: Record<string, Opcode> = {
|
|
|
47
47
|
bgez: 'icmp_sge',
|
|
48
48
|
};
|
|
49
49
|
const COND_RR: Record<string, Opcode> = { beq: 'icmp_eq', bne: 'icmp_ne' };
|
|
50
|
-
// Negated icmp opcode (for the `slt …; beqz` "branch when false" fold).
|
|
51
|
-
const NEG_ICMP: Record<string, Opcode> = {
|
|
52
|
-
icmp_slt: 'icmp_sge',
|
|
53
|
-
icmp_sge: 'icmp_slt',
|
|
54
|
-
icmp_sgt: 'icmp_sle',
|
|
55
|
-
icmp_sle: 'icmp_sgt',
|
|
56
|
-
icmp_ult: 'icmp_uge',
|
|
57
|
-
icmp_uge: 'icmp_ult',
|
|
58
|
-
icmp_ugt: 'icmp_ule',
|
|
59
|
-
icmp_ule: 'icmp_ugt',
|
|
60
|
-
icmp_eq: 'icmp_ne',
|
|
61
|
-
icmp_ne: 'icmp_eq',
|
|
62
|
-
};
|
|
63
50
|
|
|
64
51
|
const isZero = (r: string) => r === 'zero' || r === '$0';
|
|
65
52
|
// The stack pointer (`$29`). A `sw/lw` through it is not a store/load through a data pointer — it
|
|
@@ -604,6 +591,18 @@ export function lift(
|
|
|
604
591
|
}
|
|
605
592
|
return readVar(r, bi);
|
|
606
593
|
};
|
|
594
|
+
// `slt`-family results, so a following `beqz`/`bnez` can fold into one compare.
|
|
595
|
+
//
|
|
596
|
+
// Keyed by the SSA VALUE the compare produced, never by its register. Keying by register is the
|
|
597
|
+
// bug: `slt v0,a0,a1; xori v0,v0,1; beqz v0,L` (a materialised `a0 >= a1` that a branch then
|
|
598
|
+
// tests — IDO's spelling, and a live benchmark row) redefines v0, and a register-keyed record
|
|
599
|
+
// folds the branch against the DEAD `slt`, silently emitting the INVERTED condition. A value
|
|
600
|
+
// cannot go stale that way: `condValue` resolves the branch's register to whatever value reaches
|
|
601
|
+
// it and looks THAT up, so a redefinition simply misses and the honest `icmp_eq(rX, 0)` is
|
|
602
|
+
// emitted. It also needs no invalidation discipline to be maintained by every future writer —
|
|
603
|
+
// which matters, because `write` is NOT the only path that redefines a register (`lui
|
|
604
|
+
// rD,%hi(SYM)` deliberately reassigns rD's meaning without it).
|
|
605
|
+
const cmpDef = new Map<Value, { opcode: string; lhs: Value; rhs: Value }>();
|
|
607
606
|
const write = (r: string, v: Value) => {
|
|
608
607
|
// Writing a register clears any pending `%hi` it held — the high-half address is gone once the
|
|
609
608
|
// register is reassigned (e.g. `lw rHi, %lo(SYM)(rHi)` reuses the base as the load dest). A
|
|
@@ -626,13 +625,11 @@ export function lift(
|
|
|
626
625
|
// DELIBERATELY (unlike divState): a cross-block `mult`/`mflo` pair has no observed inhabitant,
|
|
627
626
|
// and the miss degrades to a LOUD opaque, never silence.
|
|
628
627
|
let mulState: { rs: Value; rt: Value; signed: boolean } | null = null;
|
|
629
|
-
// `slt`-family results, so a following `beqz`/`bnez` can fold into one compare.
|
|
630
|
-
const cmpDef = new Map<string, { value: Value; opcode: string; lhs: Value; rhs: Value }>();
|
|
631
628
|
const emitCmp = (opc: Opcode, d: string, lhs: Value, rhs: Value) => {
|
|
632
629
|
const v = mkValue(T.unk(32));
|
|
633
630
|
ops.push(mkOp(opc, { operands: [lhs, rhs], results: [v] }));
|
|
634
631
|
write(d, v);
|
|
635
|
-
cmpDef.set(
|
|
632
|
+
cmpDef.set(v, { opcode: opc, lhs, rhs });
|
|
636
633
|
};
|
|
637
634
|
|
|
638
635
|
const decode = (ins: Instr) => {
|
|
@@ -1053,7 +1050,7 @@ function condValue(
|
|
|
1053
1050
|
ops: Op[],
|
|
1054
1051
|
read: (r: string) => Value,
|
|
1055
1052
|
constVal: (n: number) => Value,
|
|
1056
|
-
cmpDef: Map<
|
|
1053
|
+
cmpDef: Map<Value, { opcode: string; lhs: Value; rhs: Value }>,
|
|
1057
1054
|
): Value {
|
|
1058
1055
|
const mk = (opc: Opcode, l: Value, r: Value): Value => {
|
|
1059
1056
|
const v = mkValue(T.unk(32));
|
|
@@ -1064,15 +1061,19 @@ function condValue(
|
|
|
1064
1061
|
return mk(COND_RR[br.mnemonic], read(br.ops[0]), read(br.ops[1]));
|
|
1065
1062
|
}
|
|
1066
1063
|
// *z forms compare a register against zero — except beqz/bnez may fold a preceding `slt`.
|
|
1067
|
-
|
|
1068
|
-
|
|
1064
|
+
// Resolve the register to its reaching VALUE first: that is the fold's key (so a redefinition
|
|
1065
|
+
// between the compare and the branch misses instead of folding stale), and it is also what
|
|
1066
|
+
// routes the operand through `read`'s loud guards (sp / `%hi` / `gp` used as data) on every
|
|
1067
|
+
// path — the fold used to bypass them by never reading the register at all.
|
|
1068
|
+
const rsv = read(br.ops[0]);
|
|
1069
|
+
const folded = cmpDef.get(rsv);
|
|
1069
1070
|
if (folded && br.mnemonic === 'bnez') {
|
|
1070
|
-
return
|
|
1071
|
+
return rsv;
|
|
1071
1072
|
} // branch when slt is true
|
|
1072
1073
|
if (folded && br.mnemonic === 'beqz') {
|
|
1073
|
-
return mk(
|
|
1074
|
+
return mk(NEGATED_ICMP[folded.opcode], folded.lhs, folded.rhs);
|
|
1074
1075
|
} // …when false
|
|
1075
|
-
return mk(COND_Z[br.mnemonic],
|
|
1076
|
+
return mk(COND_Z[br.mnemonic], rsv, constVal(0));
|
|
1076
1077
|
}
|
|
1077
1078
|
|
|
1078
1079
|
/** The MIPS-II / IDO frontend, registered for the `mips` target. */
|
package/src/frontend/opaque.ts
CHANGED
|
@@ -7,6 +7,37 @@
|
|
|
7
7
|
// This module owns only the POLICY (which token is the destination, which are register sources).
|
|
8
8
|
// The frontend still owns the SSA plumbing (how to `read` a source and `write`/emit the result),
|
|
9
9
|
// because that is block-local state the policy must not touch.
|
|
10
|
+
//
|
|
11
|
+
// ─── A NOTE ON ALTERNATIVE MNEMONIC SPELLINGS ──────────────────────────────────────────────────
|
|
12
|
+
//
|
|
13
|
+
// Every ISA here accepts more than one spelling for some instructions, and the frontends handle
|
|
14
|
+
// that in two DIFFERENT ways on purpose. Which one is right is decided by the operands, not by
|
|
15
|
+
// taste:
|
|
16
|
+
//
|
|
17
|
+
// * PURE SYNONYM — same operands, same semantics, different name. Normalise it in a name→name
|
|
18
|
+
// table at the parse site, so every consumer of the mnemonic sees one name. Thumb does this
|
|
19
|
+
// for `ldsh`/`ldrsh`, `ldsb`/`ldrsb`, `ldm`/`ldmfd`/`ldmia`, `stm`/`stmea`/`stmia`, which
|
|
20
|
+
// ARM DDI 0029G Figure 1-6 gives a single encoding apiece.
|
|
21
|
+
//
|
|
22
|
+
// The table is the right shape THERE because the mnemonic is read by more than the decode
|
|
23
|
+
// switch — Thumb's `classifyXfer` matches it to tell a return from an indirect jump, the
|
|
24
|
+
// `storeClass` below matches it to decide whether an unmodelled op may be skipped, and the
|
|
25
|
+
// instruction-size walk tests it for `bl`. An alias arm on one `case` fixes one of those.
|
|
26
|
+
//
|
|
27
|
+
// * EXTENDED MNEMONIC / PSEUDO-INSTRUCTION — different operand GRAMMAR, so no rename can
|
|
28
|
+
// express it. Give it its own decode arm. MIPS `move rD,rS` (2 operands) is `addu rD,rS,zero`
|
|
29
|
+
// (3); PPC `mr rD,rS` is `or rD,rS,rS`; PPC `slwi rD,rS,n` (3) is `rlwinm rD,rS,n,mb,me` (5,
|
|
30
|
+
// with mask fields computed from n). Routing these through a table would be a category error.
|
|
31
|
+
//
|
|
32
|
+
// There is deliberately NO shared alias helper. As of writing, MIPS and PPC have no pure-synonym
|
|
33
|
+
// gap at all — every `unmodelled instruction` decline they produce across the whole benchmark is
|
|
34
|
+
// a genuinely unmodelled opcode (`lwc1`, `fmuls`, `fctiwz`, `subfe`, …), not a spelling — so such
|
|
35
|
+
// a helper would have exactly one caller. The bar for extracting one is the bar this module itself
|
|
36
|
+
// met: several frontends hand-copying the same policy AND observed drift between the copies.
|
|
37
|
+
//
|
|
38
|
+
// One thing that IS shared, and should stay shared: `display` below. A frontend that normalises
|
|
39
|
+
// spellings must still REPORT the one its input actually used, or a decline sends the reader
|
|
40
|
+
// looking for an instruction their disassembly does not contain.
|
|
10
41
|
import { FrontendUnsupportedError } from './errors';
|
|
11
42
|
|
|
12
43
|
export interface OpaquePolicy {
|
|
@@ -27,6 +58,11 @@ export interface OpaquePolicy {
|
|
|
27
58
|
storeClass?: RegExp;
|
|
28
59
|
/** attribution for thrown declines: the function being lifted (optionally "+ site"). */
|
|
29
60
|
context?: string;
|
|
61
|
+
/** How to SPELL the mnemonic in messages, when that differs from the name used to classify it.
|
|
62
|
+
* A frontend that normalises legacy spellings (Thumb `ldsh` -> `ldrsh`) classifies on the
|
|
63
|
+
* canonical name but must report the one the input file actually contains — otherwise a decline
|
|
64
|
+
* names an instruction the reader cannot find in their own .s. Defaults to `mnemonic`. */
|
|
65
|
+
display?: string;
|
|
30
66
|
/** Mnemonics PROVABLY effect-free — or deliberately transparent (Thumb push/pop frame ops) —
|
|
31
67
|
* in this ISA: the ONLY unmodelled no-destination instructions that may be skipped. Any other
|
|
32
68
|
* no-destination unmodelled instruction THROWS: a side-effect-only instruction (swi, syscall,
|
|
@@ -54,13 +90,14 @@ export interface OpaqueDest {
|
|
|
54
90
|
* reaches structuring as the sentinel `?` and trips `assertResolved` — the loud failure the
|
|
55
91
|
* contract requires, instead of a stale/absent value surfacing as confidently-wrong source. */
|
|
56
92
|
export function opaqueDest(mnemonic: string, ops: string[], policy: OpaquePolicy): OpaqueDest | null {
|
|
93
|
+
const shown = policy.display ?? mnemonic;
|
|
57
94
|
if (policy.storeClass?.test(mnemonic)) {
|
|
58
95
|
// `context` names the function (and, where the ISA has addresses, the site) — this message
|
|
59
96
|
// lands verbatim in annotate-mode stub headers, where an un-attributed decline is
|
|
60
97
|
// unactionable in a multi-function run.
|
|
61
98
|
const where = policy.context ? `cannot lift '${policy.context}': ` : '';
|
|
62
99
|
throw new FrontendUnsupportedError(
|
|
63
|
-
`${where}unmodelled store-class instruction '${
|
|
100
|
+
`${where}unmodelled store-class instruction '${shown}' — a memory write cannot be skipped or degraded to a register opaque`,
|
|
64
101
|
);
|
|
65
102
|
}
|
|
66
103
|
const norm = policy.normalize ?? ((s) => s);
|
|
@@ -71,7 +108,7 @@ export function opaqueDest(mnemonic: string, ops: string[], policy: OpaquePolicy
|
|
|
71
108
|
} // explicitly transparent for this ISA
|
|
72
109
|
const where = policy.context ? `cannot lift '${policy.context}': ` : '';
|
|
73
110
|
throw new FrontendUnsupportedError(
|
|
74
|
-
`${where}unmodelled effect instruction '${
|
|
111
|
+
`${where}unmodelled effect instruction '${shown}' — no register destination to degrade, and skipping it would silently delete its effect`,
|
|
75
112
|
);
|
|
76
113
|
}
|
|
77
114
|
if (policy.isZero?.(dst)) {
|
package/src/frontend/ssa.ts
CHANGED
|
@@ -3,11 +3,20 @@
|
|
|
3
3
|
// CFG (predecessors per block) and, per block, emits ops through `readVar`/`writeVar`; this
|
|
4
4
|
// module materialises block-argument phis at joins and back-edges.
|
|
5
5
|
//
|
|
6
|
+
// `preds` is an EDGE list, not a block list: it carries one entry per CFG edge, so a `switch_br`
|
|
7
|
+
// with several case values reaching one block appears there several times. Both readings are
|
|
8
|
+
// needed and they are not interchangeable — phi wiring wants the distinct predecessor BLOCKS (one
|
|
9
|
+
// value each), while the args it appends belong to the EDGES (every one of them). `distinctPreds`
|
|
10
|
+
// names the first; `appendSuccessorArg` walks the second. (ir/core.ts `predecessors` and
|
|
11
|
+
// structure.ts `predecessorBlocks` have the same duality, and structure.ts already dedups ad hoc
|
|
12
|
+
// at its two join sites.)
|
|
13
|
+
//
|
|
6
14
|
// Protocol: create the builder, then fill blocks in index order. For each block, emit its
|
|
7
15
|
// computation via read/writeVar, push its terminator op last (successors referencing
|
|
8
16
|
// `irBlocks`, args left empty — phi wiring appends them), then call `markFilled(b)`. When all
|
|
9
17
|
// blocks are filled, call `finish()` to remove trivial phis.
|
|
10
|
-
import { Block, Fn,
|
|
18
|
+
import { Block, Fn, Value, mkValue } from '../ir/core';
|
|
19
|
+
import { simplifyTrivialPhis } from '../ir/simplify';
|
|
11
20
|
import { T } from '../ir/types';
|
|
12
21
|
|
|
13
22
|
export interface SsaBuilder {
|
|
@@ -27,6 +36,7 @@ export interface SsaBuilder {
|
|
|
27
36
|
finish(): void;
|
|
28
37
|
}
|
|
29
38
|
|
|
39
|
+
/** `preds` is per-EDGE (see the module header): one entry per CFG edge into each block. */
|
|
30
40
|
export function makeSsaBuilder(name: string, blockCount: number, preds: number[][]): SsaBuilder {
|
|
31
41
|
const irBlocks: Block[] = Array.from({ length: blockCount }, () => ({ params: [] as Value[], ops: [] }));
|
|
32
42
|
const fn: Fn = { name, blocks: irBlocks };
|
|
@@ -38,6 +48,9 @@ export function makeSsaBuilder(name: string, blockCount: number, preds: number[]
|
|
|
38
48
|
const phiBlock = new Map<Value, number>();
|
|
39
49
|
const paramReg = new Map<Value, string>();
|
|
40
50
|
|
|
51
|
+
// `preds` lists an entry per CFG EDGE; these are the distinct predecessor BLOCKS.
|
|
52
|
+
const distinctPreds = (b: number): number[] => [...new Set(preds[b])];
|
|
53
|
+
|
|
41
54
|
const writeVar = (reg: string, b: number, v: Value) => defs[b].set(reg, v);
|
|
42
55
|
const readVar = (reg: string, b: number): Value => defs[b].get(reg) ?? readRecursive(reg, b);
|
|
43
56
|
|
|
@@ -55,7 +68,10 @@ export function makeSsaBuilder(name: string, blockCount: number, preds: number[]
|
|
|
55
68
|
incompletePhis[b].set(reg, phi);
|
|
56
69
|
return phi;
|
|
57
70
|
}
|
|
58
|
-
|
|
71
|
+
// DISTINCT predecessor blocks: a switch_br reaching this block on several case values is one
|
|
72
|
+
// predecessor with several edges, and it supplies ONE value — counting the edges instead would
|
|
73
|
+
// manufacture a join (and a phi) where there is none.
|
|
74
|
+
const ps = distinctPreds(b);
|
|
59
75
|
if (ps.length === 0) {
|
|
60
76
|
// live-in with no predecessor: an incoming argument register → function parameter.
|
|
61
77
|
const p = mkValue(T.unk(32));
|
|
@@ -75,16 +91,24 @@ export function makeSsaBuilder(name: string, blockCount: number, preds: number[]
|
|
|
75
91
|
return phi;
|
|
76
92
|
};
|
|
77
93
|
const addPhiOperands = (reg: string, b: number) => {
|
|
78
|
-
for (const p of
|
|
94
|
+
for (const p of distinctPreds(b)) {
|
|
79
95
|
appendSuccessorArg(p, b, readVar(reg, p));
|
|
80
96
|
}
|
|
81
97
|
};
|
|
82
|
-
// Append `arg` to predecessor p
|
|
98
|
+
// Append `arg` to EVERY successor edge of predecessor p that targets block b.
|
|
99
|
+
//
|
|
100
|
+
// A predecessor normally has one edge to a given successor, but a `switch_br` has as many as it
|
|
101
|
+
// has case values, and two cases sharing a body (`case 1: case 2:`) is ordinary C. Block args
|
|
102
|
+
// belong to the EDGE, so each of those edges needs its own copy: appending to just the first (a
|
|
103
|
+
// `find`) left the others short, while `preds` listing the block once per edge made the loop run
|
|
104
|
+
// k times and pile k copies onto that same first edge. Both halves of that — every edge, once per
|
|
105
|
+
// predecessor BLOCK — have to hold together, which is why they are fixed in one place.
|
|
83
106
|
const appendSuccessorArg = (p: number, b: number, arg: Value) => {
|
|
84
107
|
const term = irBlocks[p].ops[irBlocks[p].ops.length - 1];
|
|
85
|
-
const s
|
|
86
|
-
|
|
87
|
-
|
|
108
|
+
for (const s of term.successors) {
|
|
109
|
+
if (s.block === irBlocks[b]) {
|
|
110
|
+
s.args.push(arg);
|
|
111
|
+
}
|
|
88
112
|
}
|
|
89
113
|
};
|
|
90
114
|
const sealBlock = (b: number) => {
|
|
@@ -128,7 +152,7 @@ export function makeSsaBuilder(name: string, blockCount: number, preds: number[]
|
|
|
128
152
|
filled[b] = true;
|
|
129
153
|
sealReadyBlocks();
|
|
130
154
|
},
|
|
131
|
-
finish: () => simplifyTrivialPhis(fn, phiBlock),
|
|
155
|
+
finish: () => simplifyTrivialPhis(fn, (p) => phiBlock.delete(p)),
|
|
132
156
|
};
|
|
133
157
|
}
|
|
134
158
|
|
|
@@ -167,48 +191,3 @@ export function abiSortEntryParams(
|
|
|
167
191
|
}
|
|
168
192
|
entry.params.sort((x, y) => rank(x) - rank(y));
|
|
169
193
|
}
|
|
170
|
-
|
|
171
|
-
// Remove block-parameters that are really trivial phis: those whose incoming operands (across
|
|
172
|
-
// every predecessor edge, ignoring self-references from a back-edge) are all the same single
|
|
173
|
-
// value. Such a parameter carries no join information — a loop-invariant register or a value
|
|
174
|
-
// defined before the join — so it is replaced by that value and the corresponding argument
|
|
175
|
-
// dropped from each predecessor's terminator. Iterated to fixpoint because removing one phi
|
|
176
|
-
// can make another trivial.
|
|
177
|
-
function simplifyTrivialPhis(fn: Fn, phiBlock: Map<Value, number>): void {
|
|
178
|
-
const edgesTo = (b: Block): Successor[] => {
|
|
179
|
-
const out: Successor[] = [];
|
|
180
|
-
for (const pb of fn.blocks) {
|
|
181
|
-
for (const op of pb.ops) {
|
|
182
|
-
for (const s of op.successors) {
|
|
183
|
-
if (s.block === b) {
|
|
184
|
-
out.push(s);
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
return out;
|
|
190
|
-
};
|
|
191
|
-
let changed = true;
|
|
192
|
-
while (changed) {
|
|
193
|
-
changed = false;
|
|
194
|
-
for (const b of fn.blocks) {
|
|
195
|
-
const incoming = edgesTo(b);
|
|
196
|
-
for (let i = b.params.length - 1; i >= 0; i--) {
|
|
197
|
-
const param = b.params[i];
|
|
198
|
-
const operands = incoming.map((s) => s.args[i]);
|
|
199
|
-
const distinct = [...new Set(operands.filter((v) => v !== param))];
|
|
200
|
-
if (distinct.length !== 1) {
|
|
201
|
-
continue;
|
|
202
|
-
} // a genuine join (or unreachable) — keep it
|
|
203
|
-
const v = distinct[0];
|
|
204
|
-
replaceAllUsesWith(fn, param, v);
|
|
205
|
-
b.params.splice(i, 1);
|
|
206
|
-
for (const s of incoming) {
|
|
207
|
-
s.args.splice(i, 1);
|
|
208
|
-
}
|
|
209
|
-
phiBlock.delete(param);
|
|
210
|
-
changed = true;
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
}
|