eliminateRedundantPhi

File

src/SSA/EliminateRedundantPhi.ts

Purpose

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.

Input Invariants

Output Guarantees

Algorithm

A phi node is considered redundant when:

  1. All operands are the same identifier: e.g., x2 = phi(x1, x1, x1) - the phi is replaced with x1
  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

The algorithm works as follows:

  1. Visit blocks in reverse postorder, building a rewrite table (Map<Identifier, Identifier>)
  2. For each phi node in a block:
    • First rewrite operands using any existing rewrites (to handle cascading eliminations)
    • Check if all operands (excluding self-references) point to the same identifier
    • If so, add a mapping from the phi's output to that identifier and delete the phi
  3. After processing phis, rewrite all instruction lvalues, operands, and terminal operands
  4. For nested functions, recursively call eliminateRedundantPhi with shared rewrites
  5. If the CFG has back-edges (loops) and new rewrites were added, repeat the entire process

The loop termination condition rewrites.size > size && hasBackEdge ensures:

Key Data Structures

Edge Cases

TODOs

(None found in the source code)

Example

Consider this fixture from rewrite-phis-in-lambda-capture-context.js:

function Component() {
  const x = 4;
  const get4 = () => {
    while (bar()) {
      if (baz) { bar(); }
    }
    return () => x;
  };
  return get4;
}

After SSA pass, the inner function has redundant phis due to the loop:

bb2 (loop):
  predecessor blocks: bb1 bb5
  x$29: phi(bb1: x$21, bb5: x$30)  // Loop header phi
  ...
bb5 (block):
  predecessor blocks: bb6 bb4
  x$30: phi(bb6: x$29, bb4: x$29)  // Redundant: both operands are x$29
  ...

After EliminateRedundantPhi:

The result: the context capture @context[x$29] becomes @context[x$21], correctly propagating that x is never modified inside the loop.