[dart2js] Reduce redundant phis with refinements

Redundant phi elimination removes phis that join the same value.
However, this does not work when one or more of the inputs has a refinement (HTypeKnown). This change adds redundant phi elimination when the phi inputs have refinements.

The need for this optimization shows up when static js_interop needs a dispatch on type for conversion:

```
final JSAny? jsValue;
if (value is String) {
  jsValue = value.toJS;
} else if (value is bool) {
  jsValue = value.toJS;
...
```

(The `.toJS` calls become no-ops since, for dart2js, we are already in JavaScript, and so leave an otherwise pointless if-then-else chain).

Change-Id: If1a94856592163a81ac36c686cee04232c16d197
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/403950
Reviewed-by: Nate Biggs <natebiggs@google.com>
Commit-Queue: Stephen Adams <sra@google.com>
diff --git a/pkg/compiler/lib/src/ssa/optimize.dart b/pkg/compiler/lib/src/ssa/optimize.dart
index 015f650..84bee0b 100644
--- a/pkg/compiler/lib/src/ssa/optimize.dart
+++ b/pkg/compiler/lib/src/ssa/optimize.dart
@@ -103,8 +103,8 @@
 
     measure(() {
       List<OptimizationPhase> phases = [
-        // Run trivial instruction simplification first to optimize
-        // some patterns useful for type conversion.
+        // Run trivial instruction simplification first to optimize some
+        // patterns useful for type conversion.
         SsaInstructionSimplifier(
           globalInferenceResults,
           _options,
@@ -113,6 +113,7 @@
           registry,
           log,
           metrics,
+          beforeTypePropagation: true,
         ),
         SsaTypeConversionInserter(closedWorld),
         SsaRedundantPhiEliminator(),
@@ -123,8 +124,7 @@
           closedWorld,
           log,
         ),
-        // After type propagation, more instructions can be
-        // simplified.
+        // After type propagation, more instructions can be simplified.
         SsaInstructionSimplifier(
           globalInferenceResults,
           _options,
@@ -309,6 +309,13 @@
   final CodegenRegistry _registry;
   final OptimizationTestLog? _log;
   final SsaMetrics _metrics;
+
+  /// Most simplifications become enabled when the types are refined by type
+  /// propagation. Some simplifications remove code that helps type progagation
+  /// produce a better result. These simplifications are inhibited when
+  /// [beforeTypePropagation] is `true` to ensure they are seeing the propagated
+  /// types.
+  final bool beforeTypePropagation;
   late final HGraph _graph;
 
   SsaInstructionSimplifier(
@@ -318,8 +325,9 @@
     this._typeRecipeDomain,
     this._registry,
     this._log,
-    this._metrics,
-  );
+    this._metrics, {
+    this.beforeTypePropagation = false,
+  });
 
   JCommonElements get commonElements => _closedWorld.commonElements;
 
@@ -418,8 +426,6 @@
 
   // Simplify some CFG diamonds to equivalent expressions.
   void simplifyPhis(HBasicBlock block) {
-    if (block.predecessors.length != 2) return;
-
     // Do 'statement' simplifications first, as they might reduce the number of
     // phis to one, enabling an 'expression' simplification.
     var phi = block.phis.firstPhi;
@@ -429,6 +435,7 @@
       phi = next;
     }
 
+    if (block.predecessors.length != 2) return;
     phi = block.phis.firstPhi;
     if (phi != null && phi.next == null) {
       simplifyExpressionPhi(block, phi);
@@ -438,6 +445,10 @@
   /// Simplify a single phi when there are possibly other phis (i.e. the result
   /// might not be an expression).
   void simplifyStatementPhi(HBasicBlock block, HPhi phi) {
+    if (simplifyStatementPhiToCommonInput(block, phi)) return;
+
+    if (block.predecessors.length != 2) return;
+
     HBasicBlock dominator = block.dominator!;
 
     // Extract the controlling condition.
@@ -478,6 +489,52 @@
     }
   }
 
+  bool simplifyStatementPhiToCommonInput(HBasicBlock block, HPhi phi) {
+    // Replace phis that produce the same value on all arms.  The test(s) for
+    // control flow often results in a refinement instruction (HTypeKnown), so
+    // we recognize that, allowing, e.g.,
+    //
+    //     condition ? HTypeKnown(x) : x  -->  x
+    //     condition ? x : HTypeKnown(x)  -->  x
+    //
+    // We don't remove loop phis here. SsaRedundantPhiEliminator will eliminate
+    // redundant phis without HTypeKnown refinements, including loop phis.
+
+    // There may be control flow that exits early, leaving refinements that
+    // cause the type of the phi to be stronger than the source. Don't attempt
+    // this simplification until the type of the phi is calculated.
+    if (beforeTypePropagation) return false;
+
+    HBasicBlock dominator = block.dominator!;
+
+    /// Find the input, skipping refinements that do not dominate the condition,
+    /// e.g., skipping refinements in the arm of the if-then-else.
+    HInstruction? dominatingRefinementInput(HInstruction input) {
+      while (true) {
+        if (input.block!.dominates(dominator)) return input;
+        if (input is! HTypeKnown) return null;
+        input = input.checkedInput;
+      }
+    }
+
+    final commonInput = dominatingRefinementInput(phi.inputs.first);
+    if (commonInput == null) return false;
+
+    for (int i = 1; i < phi.inputs.length; i++) {
+      final next = dominatingRefinementInput(phi.inputs[i]);
+      if (!identical(next, commonInput)) return false;
+    }
+
+    HTypeKnown replacement = HTypeKnown.pinned(
+      phi.instructionType,
+      commonInput,
+    );
+    block.addBefore(block.first, replacement);
+    block.rewrite(phi, replacement);
+    block.removePhi(phi);
+    return true;
+  }
+
   /// Simplify some CFG diamonds to equivalent expressions.
   void simplifyExpressionPhi(HBasicBlock block, HPhi phi) {
     // Is [block] the join point for a simple diamond?
diff --git a/pkg/compiler/test/codegen/data/partial_phi_redundancy.dart b/pkg/compiler/test/codegen/data/partial_phi_redundancy.dart
new file mode 100644
index 0000000..b743c86
--- /dev/null
+++ b/pkg/compiler/test/codegen/data/partial_phi_redundancy.dart
@@ -0,0 +1,33 @@
+// Copyright (c) 2025, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+@pragma('dart2js:never-inline')
+/*member: foo1:function(x) {
+  if (Date.now() > 0) {
+    if (typeof x != "string")
+      return "bad1";
+  } else if (typeof x != "string")
+    return "bad2";
+  return x;
+}*/
+String foo1(Object x) {
+  final Object y;
+  if (DateTime.now().millisecondsSinceEpoch > 0) {
+    if (x is! String) return 'bad1';
+    y = x;
+  } else {
+    if (x is! String) return 'bad2';
+    y = x;
+  }
+  // The phi for y has refinements to String on both branches, so the return
+  // should not need stringification.
+  return '$y';
+}
+
+/*member: main:ignore*/
+main() {
+  print(foo1('a'));
+  print(foo1('b'));
+  print(foo1(123));
+}