Version 3.9.0-190.0.dev

Merge c3496701e4ad0f8ec26050982aab860d25bbe8f3 into dev
diff --git a/DEPS b/DEPS
index b73c1ed..77a1e36 100644
--- a/DEPS
+++ b/DEPS
@@ -78,13 +78,13 @@
   "clang_version": "git_revision:8c7a2ce01a77c96028fe2c8566f65c45ad9408d3",
 
   # https://chrome-infra-packages.appspot.com/p/gn/gn
-  "gn_version": "git_revision:ebc8f16ca7b0d36a3e532ee90896f9eb48e5423b",
+  "gn_version": "git_revision:afd24ed11bc5fbef775a3ffe46c72e6bdca0fa60",
 
   "reclient_version": "re_client_version:0.178.0.5ee9d3e8-gomaip",
   "download_reclient": True,
 
   # Update from https://chrome-infra-packages.appspot.com/p/fuchsia/sdk/core
-  "fuchsia_sdk_version": "version:28.20250519.5.1",
+  "fuchsia_sdk_version": "version:28.20250528.4.1",
   "download_fuchsia_deps": False,
 
   # Ninja, runs the build based on files generated by GN.
@@ -577,7 +577,7 @@
     "packages": [
       {
       "package": "chromium/fuchsia/test-scripts",
-      "version": "L6SMsr0eTyQlN6KR4VWrJ1Kx7DLrq0yWL1u8XbmfzuAC",
+      "version": "DG11ez0PRGu8loi2mYSO_7_iW5KrUS9IEvwM9ZynXH4C",
       }
     ],
     "condition": 'download_fuchsia_deps',
diff --git a/pkg/compiler/lib/src/kernel/transformations/modular/late_lowering.dart b/pkg/compiler/lib/src/kernel/transformations/modular/late_lowering.dart
index 4dd98cb..1d3ee75 100644
--- a/pkg/compiler/lib/src/kernel/transformations/modular/late_lowering.dart
+++ b/pkg/compiler/lib/src/kernel/transformations/modular/late_lowering.dart
@@ -76,6 +76,10 @@
 
   final ExtensionTable _extensionTable = ExtensionTable();
 
+  late final InstanceConstant pragmaAllowCSE = _pragmaConstant(
+    'dart2js:allow-cse',
+  );
+
   LateLowering(this._coreTypes, CompilerOptions? _options)
     : _omitLateNames = _options?.omitLateNames ?? false,
       _readLocal = _Reader(_coreTypes.cellReadLocal),
@@ -674,6 +678,12 @@
     // transformer flags to reflect whether the getter contains super calls.
     getter.transformerFlags = field.transformerFlags;
     _copyAnnotations(getter, field);
+    if (initializer != null && field.isFinal) {
+      getter.addAnnotation(
+        ConstantExpression(pragmaAllowCSE, _coreTypes.pragmaNonNullableRawType)
+          ..fileOffset = field.fileOffset,
+      );
+    }
     enclosingClass.addProcedure(getter);
 
     VariableDeclaration setterValue = VariableDeclaration(
@@ -750,6 +760,13 @@
     }
   }
 
+  InstanceConstant _pragmaConstant(String pragmaName) {
+    return InstanceConstant(_coreTypes.pragmaClass.reference, [], {
+      _coreTypes.pragmaName.fieldReference: StringConstant(pragmaName),
+      _coreTypes.pragmaOptions.fieldReference: NullConstant(),
+    });
+  }
+
   TreeNode transformField(Field field, Member contextMember) {
     _contextMember = contextMember;
 
diff --git a/pkg/compiler/lib/src/ssa/builder.dart b/pkg/compiler/lib/src/ssa/builder.dart
index 56ece5e..fe211a7 100644
--- a/pkg/compiler/lib/src/ssa/builder.dart
+++ b/pkg/compiler/lib/src/ssa/builder.dart
@@ -7873,11 +7873,19 @@
       return false;
     }
 
-    // Don't inline functions marked with 'allow-cse' and 'allow-dce' since we
-    // need the call instruction to do these optimizations. We might be able to
+    // Don't inline functions marked with 'allow-dce' since we need the call
+    // instruction to recognize the whole call as unused. We might be able to
     // inline simple methods afterwards.
-    if (closedWorld.annotationsData.allowCSE(function) ||
-        closedWorld.annotationsData.allowDCE(function)) {
+    if (closedWorld.annotationsData.allowDCE(function)) {
+      return false;
+    }
+
+    // Don't inline functions marked with 'allow-cse' since we need the call
+    // instructions to recognize repeated calls. We might be able to inline
+    // simple methods afterwards. If this is the only call site, we will never
+    // find the repeated call, so we should consider inlining here.
+    if (closedWorld.annotationsData.allowCSE(function) &&
+        !_isCalledOnce(function)) {
       return false;
     }
 
diff --git a/pkg/compiler/lib/src/ssa/nodes.dart b/pkg/compiler/lib/src/ssa/nodes.dart
index 636be70..ee060bd 100644
--- a/pkg/compiler/lib/src/ssa/nodes.dart
+++ b/pkg/compiler/lib/src/ssa/nodes.dart
@@ -1344,6 +1344,9 @@
     bool hasSameType = typeEquals(other);
     assert(hasSameType == (_gvnType == other._gvnType));
     if (!hasSameType) return false;
+    // Check the data first to ensure we are considering the same element or
+    // selector.
+    if (!dataEquals(other)) return false;
     assert((useGvn() && other.useGvn()) || (allowCSE && other.allowCSE));
     if (sideEffects != other.sideEffects) return false;
     // Check that the inputs match.
@@ -1355,8 +1358,7 @@
         return false;
       }
     }
-    // Check that the data in the instruction matches.
-    return dataEquals(other);
+    return true;
   }
 
   int gvnHashCode() {
@@ -2092,13 +2094,8 @@
 
   @override
   bool dataEquals(HInvokeDynamic other) {
-    // Use the name and the kind instead of [Selector.operator==]
-    // because we don't need to check the arity (already checked in
-    // [gvnEquals]), and the receiver types may not be in sync.
-    // TODO(sra): If we GVN calls with named (optional) arguments then the
-    // selector needs a deeper check for the same subset of named arguments.
-    return selector.name == other.selector.name &&
-        selector.kind == other.selector.kind;
+    return selector == other.selector &&
+        (useGvn() == other.useGvn() || allowCSE == other.allowCSE);
   }
 }
 
diff --git a/pkg/compiler/lib/src/ssa/optimize.dart b/pkg/compiler/lib/src/ssa/optimize.dart
index 7eebfbf..792d797 100644
--- a/pkg/compiler/lib/src/ssa/optimize.dart
+++ b/pkg/compiler/lib/src/ssa/optimize.dart
@@ -2,6 +2,8 @@
 // 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.
 
+import 'dart:collection' show Queue;
+
 import 'package:js_runtime/synced/array_flags.dart' show ArrayFlags;
 
 import '../common.dart';
@@ -4150,15 +4152,11 @@
     HInstruction input = node.checkedInput;
     if (input.usedBy.length <= 1) return; // No other uses to refine.
 
-    List<HBasicBlock> trueTargets = [];
-    List<HBasicBlock> falseTargets = [];
-
-    collectTargets(node, trueTargets, falseTargets);
-
-    if (trueTargets.isEmpty && falseTargets.isEmpty) return;
+    final targets = ConditionTargets(node);
+    if (targets.isEmpty) return;
 
     AbstractValue whenTrueType = node.checkedAbstractValue.abstractValue;
-    insertTypeRefinements(trueTargets, input, whenTrueType);
+    insertTypeRefinements(targets.whenTrue, input, whenTrueType);
     // TODO(sra): Also strengthen uses for when the condition is precise and
     // known false (e.g. int? x; ... if (x is! int) use(x)). Avoid strengthening
     // to `null`.
@@ -4169,15 +4167,11 @@
     HInstruction input = node.checkedInput;
     if (input.usedBy.length <= 1) return; // No other uses to refine.
 
-    List<HBasicBlock> trueTargets = [];
-    List<HBasicBlock> falseTargets = [];
-
-    collectTargets(node, trueTargets, falseTargets);
-
-    if (trueTargets.isEmpty && falseTargets.isEmpty) return;
+    final targets = ConditionTargets(node);
+    if (targets.isEmpty) return;
 
     AbstractValue whenTrueType = node.checkedAbstractValue.abstractValue;
-    insertTypeRefinements(trueTargets, input, whenTrueType);
+    insertTypeRefinements(targets.whenTrue, input, whenTrueType);
     // TODO(sra): Also strengthen uses for when the condition is precise and
     // known false (e.g. int? x; ... if (x is! int) use(x)). Avoid strengthening
     // to `null`.
@@ -4204,17 +4198,13 @@
       return;
     }
 
-    List<HBasicBlock> trueTargets = [];
-    List<HBasicBlock> falseTargets = [];
-
-    collectTargets(node, trueTargets, falseTargets);
-
-    if (trueTargets.isEmpty && falseTargets.isEmpty) return;
+    final targets = ConditionTargets(node);
+    if (targets.isEmpty) return;
 
     AbstractValue nonNullType = _abstractValueDomain.excludeNull(
       input.instructionType,
     );
-    insertTypeRefinements(falseTargets, input, nonNullType);
+    insertTypeRefinements(targets.whenFalse, input, nonNullType);
     // We don't strengthen the known-true references. It doesn't happen often
     // and we don't want "if (x==null) return x;" to convert between JavaScript
     // 'null' and 'undefined'.
@@ -4225,26 +4215,92 @@
     final input = node.inputs.single;
     if (input.usedBy.length <= 1) return; // No other uses to refine.
 
-    List<HBasicBlock> trueTargets = [];
-    List<HBasicBlock> falseTargets = [];
-
-    collectTargets(node, trueTargets, falseTargets);
-
-    if (trueTargets.isEmpty && falseTargets.isEmpty) return;
+    final targets = ConditionTargets(node);
+    if (targets.isEmpty) return;
 
     final sentinelType = _abstractValueDomain.lateSentinelType;
     final nonSentinelType = _abstractValueDomain.excludeLateSentinel(
       input.instructionType,
     );
-    insertTypeRefinements(trueTargets, input, sentinelType);
-    insertTypeRefinements(falseTargets, input, nonSentinelType);
+    insertTypeRefinements(targets.whenTrue, input, sentinelType);
+    insertTypeRefinements(targets.whenFalse, input, nonSentinelType);
+  }
+}
+
+typedef _Step = (HInstruction, {_Targets? whenTrue, _Targets? whenFalse});
+
+/// [ConditionTargets] collects the target blocks for a condition. The target
+/// blocks are blocks reachable only when the condition is true (`whenTrue`) and
+/// blocks reachable only when the condition is false (`whenFalse`).
+///
+/// The algorithm starts with an initial condition instruction and two empty
+/// sets of targets, [_trueTargets] and [_falseTargets]. If the condition is
+/// used in HIf control flow, we know the condition is true in the then-block
+/// and false in the else-block.
+///
+/// If the condition is used by HNot, we can infer that when the HNot is true,
+/// the condition was false and the trueTargets of the HNot are the falseTargets
+/// of the original condition. The HNot is added to a work queue with flipped
+/// targets.
+///
+/// If the condition is used in a phi, or a HIf controlling a phi, depending on
+/// how the condition is used, sometimes when the phi is true or false, we can
+/// infer something about the original condition's targets.
+///
+/// `whenTrue:` and `whenFalse` each have three possible values, (1)
+/// _trueTargets, (2) _falseTargets, and (3) `null`.
+class ConditionTargets {
+  /// For the visited set, only one of `whenTrue:` and `whenFalse:` used, the
+  /// other is `null`. It is unusual, but possible to visit both of:
+  ///
+  ///     (cond, whenTrue: _trueTargets, whenFalse: null)
+  ///     (cond, whenTrue: _falseTargets, whenFalse: null)
+  ///
+  /// This is a contradition (cond both must be true and also must be
+  /// false). Contradictions happen only in unreachable code.
+  final Set<_Step> _visited = {};
+
+  final Queue<_Step> _queue = Queue();
+
+  final _Targets _trueTargets = _Targets();
+  final _Targets _falseTargets = _Targets();
+
+  ConditionTargets(HInstruction condition) {
+    _add(condition, _trueTargets, _falseTargets);
+    while (_queue.isNotEmpty) {
+      _step();
+    }
   }
 
-  void collectTargets(
-    HInstruction instruction,
-    List<HBasicBlock>? trueTargets,
-    List<HBasicBlock>? falseTargets,
-  ) {
+  bool get isEmpty => _trueTargets.isEmpty && _falseTargets.isEmpty;
+
+  List<HBasicBlock> get whenTrue => _trueTargets.blocks;
+  List<HBasicBlock> get whenFalse => _falseTargets.blocks;
+
+  void _add(HInstruction node, _Targets? trueTargets, _Targets? falseTargets) {
+    if (trueTargets == null && falseTargets == null) return;
+    _queue.add((node, whenTrue: trueTargets, whenFalse: falseTargets));
+  }
+
+  void _step() {
+    var (instruction, whenTrue: trueTargets, whenFalse: falseTargets) = _queue
+        .removeFirst();
+
+    // The same instruction (I) can be reached on different paths with different
+    // combinations of trueTargets (T) and falseTargets (F).  Filling in the
+    // targets for problem (I,T,F) is separable into processing (I,T,null) and
+    // (I,null,F), so we check if we have visited this instruction before by
+    // querying trueTargets and falseTargets independently, and remove the
+    // separable subproblem that already has been solved.
+    if (trueTargets != null &&
+        !_visited.add((instruction, whenTrue: trueTargets, whenFalse: null))) {
+      trueTargets = null;
+    }
+    if (falseTargets != null &&
+        !_visited.add((instruction, whenTrue: null, whenFalse: falseTargets))) {
+      falseTargets = null;
+    }
+
     if (trueTargets == null && falseTargets == null) return;
 
     for (HInstruction user in instruction.usedBy) {
@@ -4278,22 +4334,22 @@
                 if (right.isConstantFalse()) {
                   // When `c ? x : false` is true, `c` must be true.
                   // So pass `c`'s trueTargets as the phi's trueTargets.
-                  collectTargets(phi, trueTargets, null);
+                  _add(phi, trueTargets, null);
                 } else if (right.isConstantTrue()) {
                   // When `c ? x : true` is false, `c` must be true.
                   // So pass `c`'s trueTargets as the phi's falseTargets.
-                  collectTargets(phi, null, trueTargets);
+                  _add(phi, null, trueTargets);
                 }
 
                 final left = phi.inputs[0];
                 if (left.isConstantFalse()) {
                   // When `c ? false : x` is true, `c` must be false.
                   // So pass `c`'s falseTargets as the phi's trueTargets.
-                  collectTargets(phi, falseTargets, null);
+                  _add(phi, falseTargets, null);
                 } else if (left.isConstantTrue()) {
                   // When `c ? true : x` is false, `c` must be false.
                   // So pass `c`'s falseTargets as the phi's falseTargets.
-                  collectTargets(phi, null, falseTargets);
+                  _add(phi, null, falseTargets);
                 }
 
                 // Sanity checks:
@@ -4301,14 +4357,14 @@
                 // For `c ? true : false`, we pass both `c`'s trueTargets and
                 // falseTargets as the same targets of the phi.
                 //
-                // For `c ? false : true`, we pass the targets reversed, like we
-                // for `HNot`.
+                // For `c ? false : true`, we pass the targets reversed, like
+                // we do for `HNot`.
                 //
                 // For `c ? false : false`, we pass both `c`'s trueTargets and
                 // falseTargets to the unreachable trueTargets of the phi. We
-                // might insert contradictory strengthenings, which might refine
-                // a value to Never, i.e. we potentially 'prove' the code is
-                // unreachable.
+                // might insert contradictory strengthenings, which might
+                // refine a value to Never, i.e. we potentially 'prove' the
+                // code is unreachable.
               }
             }
           }
@@ -4318,21 +4374,21 @@
         // Don't insert refinements on else-branch - may be a critical edge
         // block which we currently need to keep empty (except for phis).
       } else if (user is HNot) {
-        collectTargets(user, falseTargets, trueTargets);
+        _add(user, falseTargets, trueTargets);
       } else if (user is HPhi) {
         List<HInstruction> inputs = user.inputs;
         if (inputs.length == 2) {
           assert(inputs.contains(instruction));
           HInstruction other = inputs[(inputs[0] == instruction) ? 1 : 0];
           if (other.isConstantTrue()) {
-            // The condition flows to `HPhi(true, user)` or `HPhi(user, true)`,
+            // The condition flows to `HPhi(true,user)` or `HPhi(user,true)`,
             // which means that a downstream HIf has true-branch control flow
             // that does not depend on the original instruction, so stop
             // collecting [trueTargets].
-            collectTargets(user, null, falseTargets);
+            _add(user, null, falseTargets);
           } else if (other.isConstantFalse()) {
             // Ditto for false.
-            collectTargets(user, trueTargets, null);
+            _add(user, trueTargets, null);
           }
         }
       }
@@ -4340,6 +4396,13 @@
   }
 }
 
+class _Targets {
+  final List<HBasicBlock> _blocks = [];
+  bool get isEmpty => _blocks.isEmpty;
+  void add(HBasicBlock block) => _blocks.add(block);
+  List<HBasicBlock> get blocks => _blocks;
+}
+
 /// Optimization phase that tries to eliminate memory loads (for example
 /// [HFieldGet]), when it knows the value stored in that memory location, and
 /// stores that overwrite with the same value.
diff --git a/pkg/compiler/test/codegen/data/promotion2.dart b/pkg/compiler/test/codegen/data/promotion2.dart
new file mode 100644
index 0000000..bbf30a6
--- /dev/null
+++ b/pkg/compiler/test/codegen/data/promotion2.dart
@@ -0,0 +1,30 @@
+// 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.
+
+// Examples where null checks should be removed.
+
+/*member: test1:function(a) {
+  var b, b0, i;
+  for (b = a != null, b0 = false, i = 0; ++i, i < 3; b0 = b)
+    if (b0)
+      B.JSArray_methods.get$first(a);
+}*/
+void test1(List<Object>? a) {
+  bool b = false;
+  int i = 0;
+  // The null check is guarded by `b = false;` in the initial iteration.
+  while (++i < 3) {
+    if (b) sink = a!.first;
+    b = a != null;
+  }
+}
+
+Object? sink;
+
+/*member: main:ignore*/
+main() {
+  test1(null);
+  test1([1, 2]);
+  test1(['x']);
+}
diff --git a/pkg/front_end/testcases/dart2js/late_fields.dart.strong.transformed.expect b/pkg/front_end/testcases/dart2js/late_fields.dart.strong.transformed.expect
index cabb345..590ece1 100644
--- a/pkg/front_end/testcases/dart2js/late_fields.dart.strong.transformed.expect
+++ b/pkg/front_end/testcases/dart2js/late_fields.dart.strong.transformed.expect
@@ -32,6 +32,7 @@
   }
   set c(synthesized core::int value) → void
     this.{self::C::_#C#c#AI} = value;
+  @#C3
   get d() → core::int {
     synthesized core::int value = this.{self::C::_#C#d#FI}{core::int};
     if(_in::isSentinel(value)) {
@@ -68,6 +69,11 @@
   core::print(self::c.{self::C::d}{core::int});
 }
 
+constants  {
+  #C1 = "dart2js:allow-cse"
+  #C2 = null
+  #C3 = core::pragma {name:#C1, options:#C2}
+}
 
 Extra constant evaluation status:
 Evaluated: InstanceInvocation @ org-dartlang-testcase:///late_fields.dart:15:16 -> DoubleConstant(-1.0)
diff --git a/pkg/front_end/testcases/dart2js/late_fields_with_annotation.dart.strong.transformed.expect b/pkg/front_end/testcases/dart2js/late_fields_with_annotation.dart.strong.transformed.expect
index 1228214..cb5c17d 100644
--- a/pkg/front_end/testcases/dart2js/late_fields_with_annotation.dart.strong.transformed.expect
+++ b/pkg/front_end/testcases/dart2js/late_fields_with_annotation.dart.strong.transformed.expect
@@ -42,6 +42,7 @@
     this.{self::C::_#C#c#AI} = value;
   @#C5
   @#C9
+  @#C11
   get d() → core::int {
     synthesized core::int value = this.{self::C::_#C#d#FI}{core::int};
     if(_in::isSentinel(value)) {
@@ -88,6 +89,8 @@
   #C7 = core::pragma {name:#C6, options:#C2}
   #C8 = "dart2js:noInline"
   #C9 = core::pragma {name:#C8, options:#C2}
+  #C10 = "dart2js:allow-cse"
+  #C11 = core::pragma {name:#C10, options:#C2}
 }
 
 Extra constant evaluation status:
diff --git a/pkg/front_end/testcases/dart2js/late_from_dill/main.dart.strong.expect b/pkg/front_end/testcases/dart2js/late_from_dill/main.dart.strong.expect
index 2d935e1..053ab4b 100644
--- a/pkg/front_end/testcases/dart2js/late_from_dill/main.dart.strong.expect
+++ b/pkg/front_end/testcases/dart2js/late_from_dill/main.dart.strong.expect
@@ -119,6 +119,7 @@
   }
   set c(synthesized core::int value) → void
     this.{mai::C::_#C#c#AI} = value;
+  @#C3
   get d() → core::int {
     synthesized core::int value = this.{mai::C::_#C#d#FI}{core::int};
     if(_in::isSentinel(value)) {
@@ -212,3 +213,9 @@
   return mai2::_#b.{_la::_Cell::readField}<core::int>(){() → core::int};
 static set b(synthesized core::int value) → void
   return mai2::_#b.{_la::_Cell::finalFieldValue} = value;
+
+constants  {
+  #C1 = "dart2js:allow-cse"
+  #C2 = null
+  #C3 = core::pragma {name:#C1, options:#C2}
+}
diff --git a/pkg/front_end/testcases/dart2js/late_from_dill/main.dart.strong.transformed.expect b/pkg/front_end/testcases/dart2js/late_from_dill/main.dart.strong.transformed.expect
index 4128801..0d24073 100644
--- a/pkg/front_end/testcases/dart2js/late_from_dill/main.dart.strong.transformed.expect
+++ b/pkg/front_end/testcases/dart2js/late_from_dill/main.dart.strong.transformed.expect
@@ -119,6 +119,7 @@
   }
   set c(synthesized core::int value) → void
     this.{mai::C::_#C#c#AI} = value;
+  @#C3
   get d() → core::int {
     synthesized core::int value = this.{mai::C::_#C#d#FI}{core::int};
     if(_in::isSentinel(value)) {
@@ -213,6 +214,11 @@
 static set b(synthesized core::int value) → void
   return mai2::_#b.{_la::_Cell::finalFieldValue} = value;
 
+constants  {
+  #C1 = "dart2js:allow-cse"
+  #C2 = null
+  #C3 = core::pragma {name:#C1, options:#C2}
+}
 
 Extra constant evaluation status:
 Evaluated: InstanceInvocation @ org-dartlang-testcase:///main_lib1.dart:8:16 -> DoubleConstant(-1.0)
diff --git a/tools/VERSION b/tools/VERSION
index 91c446d..bcce01c 100644
--- a/tools/VERSION
+++ b/tools/VERSION
@@ -27,5 +27,5 @@
 MAJOR 3
 MINOR 9
 PATCH 0
-PRERELEASE 189
+PRERELEASE 190
 PRERELEASE_PATCH 0