main
md 90 lines 4.44 KB
Rendered Raw
1 # eliminateRedundantPhi
2
3 ## File
4 `src/SSA/EliminateRedundantPhi.ts`
5
6 ## Purpose
7 Eliminates phi nodes whose operands are trivially the same, replacing all usages of the phi's output identifier with the single source identifier. This simplifies the HIR by removing unnecessary join points that do not actually merge distinct values.
8
9 ## Input Invariants
10 - The function must be in SSA form (i.e., `enterSSA` has already run)
11 - Blocks are in reverse postorder (guaranteed by the HIR structure)
12 - Phi nodes exist at the start of blocks where control flow merges
13
14 ## Output Guarantees
15 - All redundant phi nodes are removed from the HIR
16 - All references to eliminated phi identifiers are rewritten to the source identifier
17 - Non-redundant phi nodes (those merging two or more distinct values) are preserved
18 - Nested function expressions (FunctionExpression, ObjectMethod) also have their redundant phis eliminated and contexts rewritten
19
20 ## Algorithm
21 A phi node is considered redundant when:
22 1. **All operands are the same identifier**: e.g., `x2 = phi(x1, x1, x1)` - the phi is replaced with `x1`
23 2. **All operands are either the same identifier OR the phi's output**: e.g., `x2 = phi(x1, x2, x1, x2)` - this handles loop back-edges where the phi references itself
24
25 The algorithm works as follows:
26 1. Visit blocks in reverse postorder, building a rewrite table (`Map<Identifier, Identifier>`)
27 2. For each phi node in a block:
28 - First rewrite operands using any existing rewrites (to handle cascading eliminations)
29 - Check if all operands (excluding self-references) point to the same identifier
30 - If so, add a mapping from the phi's output to that identifier and delete the phi
31 3. After processing phis, rewrite all instruction lvalues, operands, and terminal operands
32 4. For nested functions, recursively call `eliminateRedundantPhi` with shared rewrites
33 5. If the CFG has back-edges (loops) and new rewrites were added, repeat the entire process
34
35 The loop termination condition `rewrites.size > size && hasBackEdge` ensures:
36 - Without loops: completes in a single pass (reverse postorder guarantees forward propagation)
37 - With loops: repeats until no new rewrites are found (fixpoint)
38
39 ## Key Data Structures
40 - **`Phi`** (from `src/HIR/HIR.ts`): Represents a phi node with:
41 - `place: Place` - the output identifier
42 - `operands: Map<BlockId, Place>` - maps predecessor block IDs to source places
43 - **`rewrites: Map<Identifier, Identifier>`**: Maps eliminated phi outputs to their replacement identifier
44 - **`visited: Set<BlockId>`**: Tracks visited blocks to detect back-edges (loops)
45
46 ## Edge Cases
47 - **Loop back-edges**: When a block has a predecessor that hasn't been visited yet (in reverse postorder), that predecessor is a back-edge. The algorithm handles self-referential phis like `x2 = phi(x1, x2)` by ignoring operands equal to the phi's output.
48 - **Cascading eliminations**: When one phi's output is used in another phi's operands, the algorithm rewrites operands before checking redundancy, enabling transitive elimination in a single pass (for non-loop cases).
49 - **Nested functions**: FunctionExpression and ObjectMethod values contain nested HIR that may have their own phis. The algorithm recursively processes these with a shared rewrite table, ensuring context captures are also rewritten.
50 - **Empty phi check**: The algorithm includes an invariant check that phi operands are never empty (which would be invalid HIR).
51
52 ## TODOs
53 (None found in the source code)
54
55 ## Example
56
57 Consider this fixture from `rewrite-phis-in-lambda-capture-context.js`:
58
59 ```javascript
60 function Component() {
61 const x = 4;
62 const get4 = () => {
63 while (bar()) {
64 if (baz) { bar(); }
65 }
66 return () => x;
67 };
68 return get4;
69 }
70 ```
71
72 **After SSA pass**, the inner function has redundant phis due to the loop:
73
74 ```
75 bb2 (loop):
76 predecessor blocks: bb1 bb5
77 x$29: phi(bb1: x$21, bb5: x$30) // Loop header phi
78 ...
79 bb5 (block):
80 predecessor blocks: bb6 bb4
81 x$30: phi(bb6: x$29, bb4: x$29) // Redundant: both operands are x$29
82 ...
83 ```
84
85 **After EliminateRedundantPhi**:
86 - `x$30 = phi(x$29, x$29)` is eliminated because both operands are `x$29`
87 - `x$29 = phi(x$21, x$30)` becomes `x$29 = phi(x$21, x$29)` after rewriting, which is also redundant (one operand is the phi itself, the other is `x$21`)
88 - Both phis are eliminated, and all uses of `x$29` and `x$30` are rewritten to `x$21`
89
90 The result: the context capture `@context[x$29]` becomes `@context[x$21]`, correctly propagating that `x` is never modified inside the loop.