Version 3.9.0-159.0.dev

Merge b00c777f795f89677cb899a59ba3aef586aa749f into dev
diff --git a/pkg/compiler/doc/pragmas.md b/pkg/compiler/doc/pragmas.md
index d3261f3..649241f 100644
--- a/pkg/compiler/doc/pragmas.md
+++ b/pkg/compiler/doc/pragmas.md
@@ -35,6 +35,8 @@
 | `dart2js:parameter:trust` | TBD |
 | `dart2js:types:check` | TBD |
 | `dart2js:types:trust` | TBD |
+| `dart2js:allow-cse` | [Allow common subexpression elimination (CSE)](#allow-cse) |
+| `dart2js:allow-dce` | [Allow dead code elimination (DCE)](#allow-dce) |
 
 ## Pragmas for internal use
 
@@ -45,8 +47,8 @@
 | --- | --- |
 | `dart2js:assumeDynamic` | TBD |
 | `dart2js:disableFinal` | TBD |
-| `dart2js:noSideEffects` | Requires `dart2js:noInline` to work properly |
-| `dart2js:noThrows` | Requires `dart2js:noInline` to work properly |
+| `dart2js:noSideEffects` | Requires `dart2js:never-inline` to work properly |
+| `dart2js:noThrows` | Requires `dart2js:never-inline` to work properly |
 
 ## Detailed descriptions
 
@@ -125,7 +127,7 @@
 
 Function inlining is disabled at call sites within the annotated function.
 Inlining is disabled even when the call site has a viable inlining candidate
-that is annotated with `@pragma('dart2js:tryInline')`.
+that is annotated with `@pragma('dart2js:prefer-inline')`.
 
 
 ### Annotations related to run-time checks
@@ -157,13 +159,13 @@
 One use of `dart2js:as:trust` is to construct an `unsafeCast` method.
 
 ```dart
-@pragma('dart2js:tryInline')
+@pragma('dart2js:prefer-inline')
 @pragma('dart2js:as:trust')
 T unsafeCast<T>(Object? o) => o as T;
 ```
 
-The `tryInline` pragma ensures that the function is inlined, removing the cost
-of the call and passing the type parameter `T`, and the `as:trust` pragma
+The `prefer-inline` pragma ensures that the function is inlined, removing the
+cost of the call and passing the type parameter `T`, and the `as:trust` pragma
 removes the code that does the check.
 
 #### Downcasts
@@ -184,7 +186,7 @@
 implicit downcasts.
 
 ```dart
-@pragma('dart2js:tryInline')
+@pragma('dart2js:prefer-inline')
 @pragma('dart2js:downcast:trust')
 T unsafeCast<T>(dynamic o) => o; // implicit downcast `as T`.
 ```
@@ -230,6 +232,41 @@
 
 This annotation can be placed on a method, class or library.
 
+### Annotations related to compiler optimizations
+
+#### Allow CSE
+
+**EXPERIMENTAL**: This annotation may be removed without notice.
+
+This annotation may be placed on a method or getter. If, after some
+optimization, there are two calls to the same annotated method or getter, and
+the calls have the same input values, and the first call will always have
+happened by the time control flow reaches the second call, the compiler may
+choose to remove the second call and use the value returned by the first call
+instead.
+
+```dart
+@pragma('dart2js:allow-cse')
+```
+
+This annotation is intended for an `external` getter or method that always
+refers to the same thing.
+
+#### Allow DCE
+
+**EXPERIMENTAL**: This annotated may be removed without notice.
+
+This annotation may be placed on a method or getter. If the result of the call
+to the method or getter is unused (or becomes unused due to other
+optimizations), the compiler may choose to remove the call.
+
+```dart
+@pragma('dart2js:allow-dce')
+```
+
+This annotation is intended for and `external` getter or method that makes no
+observable change.
+
 ### Annotations related to deferred library loading
 
 #### Load priority
diff --git a/pkg/compiler/lib/src/js_backend/annotations.dart b/pkg/compiler/lib/src/js_backend/annotations.dart
index 1719160..19e7556 100644
--- a/pkg/compiler/lib/src/js_backend/annotations.dart
+++ b/pkg/compiler/lib/src/js_backend/annotations.dart
@@ -93,7 +93,10 @@
   loadLibraryPriority('load-priority', hasOption: true),
   resourceIdentifier('resource-identifier'),
 
-  throwWithoutHelperFrame('stack-starts-at-throw');
+  throwWithoutHelperFrame('stack-starts-at-throw'),
+
+  allowCSE('allow-cse'),
+  allowDCE('allow-dce');
 
   final String name;
   final bool forFunctionsOnly;
@@ -364,6 +367,14 @@
   /// expression generates extra code to avoid having a runtime helper on the
   /// stack?
   bool throwWithoutHelperFrame(ir.TreeNode node);
+
+  /// Returns `true` if [member] has a `@pragma('dart2js:allow-cse')`
+  /// annotation.
+  bool allowCSE(MemberEntity member);
+
+  /// Returns `true` if [member] has a `@pragma('dart2js:allow-dce')`
+  /// annotation.
+  bool allowDCE(MemberEntity member);
 }
 
 class AnnotationsDataImpl implements AnnotationsData {
@@ -668,6 +679,14 @@
     }
     return false;
   }
+
+  @override
+  bool allowCSE(MemberEntity member) =>
+      _hasPragma(member, PragmaAnnotation.allowCSE);
+
+  @override
+  bool allowDCE(MemberEntity member) =>
+      _hasPragma(member, PragmaAnnotation.allowDCE);
 }
 
 class AnnotationsDataBuilder {
diff --git a/pkg/compiler/lib/src/ssa/builder.dart b/pkg/compiler/lib/src/ssa/builder.dart
index c710227..4cd0421 100644
--- a/pkg/compiler/lib/src/ssa/builder.dart
+++ b/pkg/compiler/lib/src/ssa/builder.dart
@@ -6804,6 +6804,8 @@
       );
     }
     instruction.sideEffects = _inferredData.getSideEffectsOfElement(target);
+    instruction.allowCSE = closedWorld.annotationsData.allowCSE(target);
+    instruction.allowDCE = closedWorld.annotationsData.allowDCE(target);
     push(instruction);
   }
 
@@ -6973,6 +6975,11 @@
       }
     }
 
+    if (element != null) {
+      invoke.allowCSE = closedWorld.annotationsData.allowCSE(element);
+      invoke.allowDCE = closedWorld.annotationsData.allowDCE(element);
+    }
+
     if (node is ir.InstanceInvocation ||
         node is ir.FunctionInvocation ||
         node is ir.InstanceGet) {
@@ -7893,6 +7900,14 @@
       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
+    // inline simple methods afterwards.
+    if (closedWorld.annotationsData.allowCSE(function) ||
+        closedWorld.annotationsData.allowDCE(function)) {
+      return false;
+    }
+
     bool insideLoop = loopDepth > 0 || graph.calledInLoop;
 
     // Bail out early if the inlining decision is in the cache and we can't
diff --git a/pkg/compiler/lib/src/ssa/nodes.dart b/pkg/compiler/lib/src/ssa/nodes.dart
index ea5586d..9e6cbd2 100644
--- a/pkg/compiler/lib/src/ssa/nodes.dart
+++ b/pkg/compiler/lib/src/ssa/nodes.dart
@@ -1175,6 +1175,11 @@
   SideEffects sideEffects = SideEffects.empty();
   bool _useGvn = false;
 
+  // TODO(sra): Consider whether to reduce instruction size by collecting all
+  // these instruction flags into a bitmask.
+  bool _allowCSE = false;
+  bool _allowDCE = false;
+
   // Main constructor copies the list of inputs to ensure ownership.
   HInstruction(List<HInstruction> initialInputs, this.instructionType)
     : inputs = [...initialInputs];
@@ -1219,6 +1224,26 @@
         !canThrow(domain);
   }
 
+  /// `true` if an instruction can be eliminated as a common subexpression -
+  /// when the instruction is equivalent to an earlier instruction may be
+  /// replaced by the value of the earlier instruction. Equivalent means that
+  /// the instruction has exactly the same inputs.
+  ///
+  /// This property is set on function invocations on the basis of annotations
+  /// and program analysis.  Usually, `allowCSE` means that the instruction is
+  /// idempotent - a second equivalent instruction returns the same value as the
+  /// first instruction without additional observable effects.
+  ///
+  ///  If an instruction is pure, it should be marked as `useGvn` instead.
+  bool get allowCSE => _allowCSE;
+
+  /// `true` if the instruction may be removed when the value is unused.
+  ///
+  /// This property is set on function invocations on the basis of annotations
+  /// and program analysis.  Usually `allowDCE` means that the instruction has
+  /// no observable effect.
+  bool get allowDCE => _allowDCE;
+
   /// An instruction is an 'allocation' is it is the sole alias for an object.
   /// This applies to instructions that allocate new objects and can be extended
   /// to methods that return other allocations without escaping them.
@@ -1315,11 +1340,11 @@
   bool isInBasicBlock() => block != null;
 
   bool gvnEquals(HInstruction other) {
-    assert(useGvn() && other.useGvn());
     // Check that the type and the sideEffects match.
     bool hasSameType = typeEquals(other);
     assert(hasSameType == (_gvnType == other._gvnType));
     if (!hasSameType) return false;
+    assert((useGvn() && other.useGvn()) || (allowCSE && other.allowCSE));
     if (sideEffects != other.sideEffects) return false;
     // Check that the inputs match.
     final int inputsLength = inputs.length;
@@ -1344,7 +1369,7 @@
   }
 
   // These methods should be overwritten by instructions that
-  // participate in global value numbering.
+  // participate in global value numbering or allowCSE.
   _GvnType get _gvnType => _GvnType.undefined;
   bool typeEquals(covariant HInstruction other) => false;
   bool dataEquals(covariant HInstruction other) => false;
@@ -1496,6 +1521,18 @@
   String toString() => '$runtimeType()';
 }
 
+mixin HasSettableAllowCSE on HInstruction {
+  set allowCSE(bool value) {
+    _allowCSE = value;
+  }
+}
+
+mixin HasSettableAllowDCE on HInstruction {
+  set allowDCE(bool value) {
+    _allowDCE = value;
+  }
+}
+
 /// An interface implemented by certain kinds of [HInstruction]. This makes it
 /// possible to discover which annotations were in force in the code from which
 /// the instruction originated.
@@ -1868,7 +1905,8 @@
   String toString() => 'HCreateBox()';
 }
 
-abstract class HInvoke extends HInstruction {
+abstract class HInvoke extends HInstruction
+    with HasSettableAllowCSE, HasSettableAllowDCE {
   bool _isAllocation = false;
 
   /// [isInterceptedCall] is true if this invocation uses the interceptor
@@ -2207,6 +2245,10 @@
 
   @override
   _GvnType get _gvnType => _GvnType.invokeStatic;
+  @override
+  bool typeEquals(other) => other is HInvokeStatic;
+  @override
+  bool dataEquals(HInvokeStatic other) => element == other.element;
 
   @override
   String toString() => 'invoke static: $element';
@@ -3938,9 +3980,10 @@
 
   @override
   String toString() {
+    String stickyString = sticky ? 'sticky, ' : '';
     String fieldString = field == null ? '' : ', $field';
     String selectorString = selector == null ? '' : ', $selector';
-    return 'HNullCheck($checkedInput$fieldString$selectorString)';
+    return 'HNullCheck($stickyString$checkedInput$fieldString$selectorString)';
   }
 }
 
diff --git a/pkg/compiler/lib/src/ssa/optimize.dart b/pkg/compiler/lib/src/ssa/optimize.dart
index 97494c1..11ddba2 100644
--- a/pkg/compiler/lib/src/ssa/optimize.dart
+++ b/pkg/compiler/lib/src/ssa/optimize.dart
@@ -1070,6 +1070,14 @@
               node.selector.callStructure.typeArgumentCount) {
         node.element = method;
       }
+
+      if (_closedWorld.annotationsData.allowCSE(element)) {
+        node.allowCSE = true;
+      }
+      if (_closedWorld.annotationsData.allowDCE(element)) {
+        node.allowDCE = true;
+      }
+
       return node;
     }
 
@@ -1986,7 +1994,12 @@
     Selector selector = node.selector;
     final member =
         node.element ?? _closedWorld.locateSingleMember(selector, receiverType);
-    if (member == null) return node;
+    if (member == null) {
+      // TODO(sra): Consider properties of the target set. If all potential
+      // targets, say, allow CSE, or return fresh allocations, then so does this
+      // getter call.
+      return node;
+    }
 
     if (member is FieldEntity) {
       FieldEntity field = member;
@@ -2015,13 +2028,19 @@
     }
 
     if (member is FunctionEntity) {
-      // If the member is not a getter, this could be a property extraction
-      // getter or legacy `noSuchMethod`.
+      // Consider only getters - if the member is not a getter, this could be a
+      // property extraction getter or legacy `noSuchMethod`.
       if (member.isGetter && member.name == selector.name) {
         node.element = member;
         if (_nativeData.isNativeMember(member)) {
           return tryInlineNativeGetter(node, member) ?? node;
         }
+        if (_closedWorld.annotationsData.allowCSE(member)) {
+          node.allowCSE = true;
+        }
+        if (_closedWorld.annotationsData.allowDCE(member)) {
+          node.allowDCE = true;
+        }
       }
     }
 
@@ -3217,6 +3236,7 @@
   bool isDeadCode(HInstruction instruction) {
     if (instruction.usedBy.isNotEmpty) return false;
     if (isTrivialDeadStore(instruction)) return true;
+    if (instruction.allowDCE) return true;
     if (instruction.sideEffects.hasSideEffects()) return false;
     if (instruction.canThrow(_abstractValueDomain)) {
       if (canFoldIntoFollowingInstruction(instruction)) {
@@ -3825,21 +3845,33 @@
     while (instruction != null) {
       final next = instruction.next;
       final flags = instruction.sideEffects.getChangesFlags();
-      assert(flags.isEmpty || !instruction.useGvn());
-      // TODO(sra): Is the above assertion too strong? We should be able to
-      // reuse the values generated by idempotent operations that have
-      // effects. Would it be correct to make the kill below be conditional on
-      // not replacing the instruction?
-      values.kill(flags);
-      if (instruction.useGvn()) {
+      if (instruction.allowCSE) {
         final other = values.lookup(instruction);
         if (other != null) {
           assert(other.gvnEquals(instruction) && instruction.gvnEquals(other));
           block.rewriteWithBetterUser(instruction, other);
           block.remove(instruction);
         } else {
+          // We didn't replace the instruction with a previous value,
+          values.kill(flags);
           values.add(instruction);
         }
+      } else {
+        assert(flags.isEmpty || !instruction.useGvn());
+        values.kill(flags);
+
+        if (instruction.useGvn()) {
+          final other = values.lookup(instruction);
+          if (other != null) {
+            assert(
+              other.gvnEquals(instruction) && instruction.gvnEquals(other),
+            );
+            block.rewriteWithBetterUser(instruction, other);
+            block.remove(instruction);
+          } else {
+            values.add(instruction);
+          }
+        }
       }
       instruction = next;
     }
@@ -4430,7 +4462,11 @@
 
   void checkNewGvnCandidates(HInstruction instruction, HInstruction existing) {
     if (newGvnCandidates) return;
-    bool hasUseGvn(HInstruction insn) => insn.nonCheck().useGvn();
+    bool hasUseGvn(HInstruction insn) {
+      final nonCheck = insn.nonCheck();
+      return nonCheck.useGvn() || nonCheck.allowCSE;
+    }
+
     if (instruction.usedBy.any(hasUseGvn) && existing.usedBy.any(hasUseGvn)) {
       newGvnCandidates = true;
     }
diff --git a/pkg/compiler/lib/src/ssa/tracer.dart b/pkg/compiler/lib/src/ssa/tracer.dart
index 0574278..f4df682 100644
--- a/pkg/compiler/lib/src/ssa/tracer.dart
+++ b/pkg/compiler/lib/src/ssa/tracer.dart
@@ -428,13 +428,14 @@
     String target = '$receiver.$name';
     int offset = HInvoke.argumentsOffset;
     List<HInstruction> arguments = invoke.inputs.sublist(offset);
-    final attributes = {
+    final attributes = [
       if (invoke.isInvariant) 'Invariant',
       if (invoke.isBoundsSafe) 'BoundSafe',
-    };
-    String attributesText = attributes.isEmpty ? '' : ' $attributes';
+    ];
 
-    return "${handleGenericInvoke(kind, target, arguments)}(${invoke.receiverType})$attributesText";
+    return "${handleGenericInvoke(kind, target, arguments)}"
+        "(${invoke.receiverType})"
+        "${generalAttributes(invoke, attributes)}";
   }
 
   @override
@@ -488,9 +489,11 @@
   @override
   String visitForeignCode(HForeignCode node) {
     var template = node.codeTemplate;
-    String code = '${template.ast}';
+    String code = template.source == null
+        ? '${template.ast}'
+        : '"${template.source}"';
     var inputs = node.inputs.map(temporaryId).join(', ');
-    return "ForeignCode: $code ($inputs)";
+    return "ForeignCode: $code ($inputs)${generalAttributes(node)}";
   }
 
   @override
@@ -679,6 +682,7 @@
   String visitNullCheck(HNullCheck node) {
     String checkedInput = temporaryId(node.checkedInput);
     var comments = [
+      if (node.sticky) 'sticky',
       if (node.selector != null) 'for ${node.selector!}',
       if (node.field != null) 'for ${node.field!}',
     ].join(', ');
@@ -752,7 +756,17 @@
   String visitAsCheck(HAsCheck node) {
     var inputs = node.inputs.map(temporaryId).join(', ');
     String error = node.isTypeError ? 'TypeError' : 'CastError';
-    return "AsCheck: $error $inputs";
+    return "AsCheck: $error $inputs${generalAttributes(node)}";
+  }
+
+  String generalAttributes(HInstruction node, [List<String>? inputAttributes]) {
+    final attributes = [
+      ...?inputAttributes,
+      if (node.allowCSE) 'allowCSE',
+      if (node.allowDCE) 'allowDCE',
+    ];
+    if (attributes.isEmpty) return '';
+    return ' $attributes';
   }
 
   @override
diff --git a/pkg/compiler/lib/src/ssa/value_set.dart b/pkg/compiler/lib/src/ssa/value_set.dart
index dbbdf34..5018653 100644
--- a/pkg/compiler/lib/src/ssa/value_set.dart
+++ b/pkg/compiler/lib/src/ssa/value_set.dart
@@ -53,12 +53,16 @@
   void kill(Bitset flags) {
     if (flags.isEmpty) return;
     final depends = SideEffects.computeDependsOnFlags(flags);
-    // Kill in the hash table.
+    // Remove entries from the hash table that depend on the 'killed' effect
+    // flags. Keep idempotent (allowCSE) entries.
     for (int index = 0, length = table.length; index < length; index++) {
       HInstruction? instruction = table[index];
-      if (instruction != null && instruction.sideEffects.dependsOn(depends)) {
-        table[index] = null;
-        size--;
+      if (instruction != null) {
+        if (!instruction.allowCSE &&
+            instruction.sideEffects.dependsOn(depends)) {
+          table[index] = null;
+          size--;
+        }
       }
     }
     // Kill in the collisions list.
@@ -67,7 +71,7 @@
     while (current != null) {
       ValueSetNode? next = current.next;
       HInstruction cached = current.value;
-      if (cached.sideEffects.dependsOn(depends)) {
+      if (!cached.allowCSE && cached.sideEffects.dependsOn(depends)) {
         if (previous == null) {
           collisions = next;
         } else {
diff --git a/pkg/compiler/test/codegen/data/allow_cse_dce.dart b/pkg/compiler/test/codegen/data/allow_cse_dce.dart
new file mode 100644
index 0000000..a6d3338
--- /dev/null
+++ b/pkg/compiler/test/codegen/data/allow_cse_dce.dart
@@ -0,0 +1,306 @@
+// 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.
+
+/*member: main:ignore*/
+void main() {
+  for (final functions in [
+    [cse_000, cse_001, cse_010, cse_011, cse_100, cse_101, cse_110, cse_111],
+    [dce_000, dce_001, dce_010, dce_011, dce_100, dce_101, dce_110, dce_111],
+    [cse_dce_000, cse_dce_001, cse_dce_010, cse_dce_011],
+    [cse_dce_100, cse_dce_101, cse_dce_110, cse_dce_111],
+  ]) {
+    for (final f in functions) {
+      f(123);
+      f('x');
+    }
+  }
+  print([sink1, sink2]);
+}
+
+Object? sink1;
+Object? sink2;
+int gI = 0;
+
+/// [source] is not idempotent so would normally not be eligible for CSE or DCE.
+/*member: source:ignore*/
+@pragma('dart2js:never-inline')
+Object? source(Object? o) {
+  ++gI;
+  sink1 = o;
+  return o is num ? gI : o;
+}
+
+/// Basic 'f' functions are the same function body that calls [source] and casts
+/// the result, but with all combinations of inlining and allow-cse and
+/// allow-dce annotations.
+///
+/// fABC:
+///  A:    1: inlined (0 not inlined)
+///   B:    1: has allow-dce
+///    C:    1: has allow-cse
+///
+/// So f000 and f100 serve as a baseline for comparing the effects of the
+/// allow-cse and allow-dce annotations.
+///
+/// These functions are used in three ways:
+///
+///        cse_ABC calls fABC twice and uses the results
+///        dce_ABC calls fABC once and ignores the result
+///    cse_dce_ABC calls fABC twice and ignores both results
+
+@pragma('dart2js:never-inline')
+/*member: f000:function(o) {
+  return A._asInt(A.source(o));
+}*/
+int f000(o) => source(o) as int;
+
+@pragma('dart2js:never-inline')
+@pragma('dart2js:allow-cse')
+/*member: f001:function(o) {
+  return A._asInt(A.source(o));
+}*/
+int f001(o) => source(o) as int;
+
+@pragma('dart2js:never-inline')
+@pragma('dart2js:allow-dce')
+/*member: f010:function(o) {
+  return A._asInt(A.source(o));
+}*/
+int f010(o) => source(o) as int;
+
+@pragma('dart2js:never-inline')
+@pragma('dart2js:allow-cse')
+@pragma('dart2js:allow-dce')
+/*member: f011:function(o) {
+  return A._asInt(A.source(o));
+}*/
+int f011(o) => source(o) as int;
+
+@pragma('dart2js:prefer-inline')
+int f100(o) => source(o) as int;
+
+@pragma('dart2js:prefer-inline')
+@pragma('dart2js:allow-cse')
+/*member: f101:function(o) {
+  return A._asInt(A.source(o));
+}*/
+int f101(o) => source(o) as int;
+
+@pragma('dart2js:prefer-inline')
+@pragma('dart2js:allow-dce')
+/*member: f110:function(o) {
+  return A._asInt(A.source(o));
+}*/
+int f110(o) => source(o) as int;
+
+@pragma('dart2js:prefer-inline')
+@pragma('dart2js:allow-cse')
+@pragma('dart2js:allow-dce')
+/*member: f111:function(o) {
+  return A._asInt(A.source(o));
+}*/
+int f111(o) => source(o) as int;
+
+/*member: cse_000:function(x) {
+  $.sink1 = A.f000(x);
+  $.sink2 = A.f000(x);
+}*/
+void cse_000(x) {
+  // Expect two uninlined calls.
+  sink1 = f000(x);
+  sink2 = f000(x);
+}
+
+/*member: cse_001:function(x) {
+  $.sink2 = $.sink1 = A.f001(x);
+}*/
+void cse_001(x) {
+  // Expect one call that is reused.
+  sink1 = f001(x);
+  sink2 = f001(x);
+}
+
+/*member: cse_010:function(x) {
+  $.sink1 = A.f010(x);
+  $.sink2 = A.f010(x);
+}*/
+void cse_010(x) {
+  sink1 = f010(x);
+  sink2 = f010(x);
+}
+
+/*member: cse_011:function(x) {
+  $.sink2 = $.sink1 = A.f011(x);
+}*/
+void cse_011(x) {
+  // Expect one call that is reused.
+  sink1 = f011(x);
+  sink2 = f011(x);
+}
+
+/*member: cse_100:function(x) {
+  $.sink1 = A._asInt(A.source(x));
+  $.sink2 = A._asInt(A.source(x));
+}*/
+void cse_100(x) {
+  // Expect two inlined calls with no shared subexpressions.
+  sink1 = f100(x);
+  sink2 = f100(x);
+}
+
+/*member: cse_101:function(x) {
+  $.sink2 = $.sink1 = A.f101(x);
+}*/
+void cse_101(x) {
+  // Expect one call, possibly inlined.
+  sink1 = f101(x);
+  sink2 = f101(x);
+}
+
+/*member: cse_110:function(x) {
+  $.sink1 = A.f110(x);
+  $.sink2 = A.f110(x);
+}*/
+void cse_110(x) {
+  // Expect two calls; possibly inlined, with no shared subexpressions.
+  sink1 = f110(x);
+  sink2 = f110(x);
+}
+
+/*member: cse_111:function(x) {
+  $.sink2 = $.sink1 = A.f111(x);
+}*/
+void cse_111(x) {
+  // Expect one call.
+  sink1 = f111(x);
+  sink2 = f111(x);
+}
+
+/*member: dce_000:function(x) {
+  A.f000(x);
+}*/
+void dce_000(x) {
+  f000(x);
+}
+
+/*member: dce_001:function(x) {
+  A.f001(x);
+}*/
+void dce_001(x) {
+  f001(x);
+}
+
+/*member: dce_010:function(x) {
+}*/
+void dce_010(x) {
+  // Expect deleted call.
+  f010(x);
+}
+
+/*member: dce_011:function(x) {
+}*/
+void dce_011(x) {
+  // Expect deleted call.
+  f011(x);
+}
+
+/*member: dce_100:function(x) {
+  A._asInt(A.source(x));
+}*/
+void dce_100(x) {
+  // Expect inlined call.
+  f100(x);
+}
+
+/*member: dce_101:function(x) {
+  A.f101(x);
+}*/
+void dce_101(x) {
+  // Expect one call, possibly inlined.
+  f101(x);
+}
+
+/*member: dce_110:function(x) {
+}*/
+void dce_110(x) {
+  // Expect deleted call.
+  f110(x);
+}
+
+/*member: dce_111:function(x) {
+}*/
+void dce_111(x) {
+  // Expect deleted call.
+  f111(x);
+}
+
+/*member: cse_dce_000:function(x) {
+  A.f000(x);
+  A.f000(x);
+}*/
+void cse_dce_000(x) {
+  // Expect two calls.
+  f000(x);
+  f000(x);
+}
+
+/*member: cse_dce_001:function(x) {
+  A.f001(x);
+}*/
+void cse_dce_001(x) {
+  // Expect one call.
+  f001(x);
+  f001(x);
+}
+
+/*member: cse_dce_010:function(x) {
+}*/
+void cse_dce_010(x) {
+  // Expect empty body - both calls deleted.
+  f010(x);
+  f010(x);
+}
+
+/*member: cse_dce_011:function(x) {
+}*/
+void cse_dce_011(x) {
+  // Expect empty body - both calls deleted.
+  f011(x);
+  f011(x);
+}
+
+/*member: cse_dce_100:function(x) {
+  A._asInt(A.source(x));
+  A._asInt(A.source(x));
+}*/
+void cse_dce_100(x) {
+  // Expect both calls inlined.
+  f100(x);
+  f100(x);
+}
+
+/*member: cse_dce_101:function(x) {
+  A.f101(x);
+}*/
+void cse_dce_101(x) {
+  // Expect call or one copy of inlined function.
+  f101(x);
+  f101(x);
+}
+
+/*member: cse_dce_110:function(x) {
+}*/
+void cse_dce_110(x) {
+  // Expect empty body - both calls deleted.
+  f110(x);
+  f110(x);
+}
+
+/*member: cse_dce_111:function(x) {
+}*/
+void cse_dce_111(x) {
+  // Expect empty body - both calls deleted.
+  f111(x);
+  f111(x);
+}
diff --git a/sdk/lib/_internal/js_runtime/lib/js_helper.dart b/sdk/lib/_internal/js_runtime/lib/js_helper.dart
index d340376..593a662 100644
--- a/sdk/lib/_internal/js_runtime/lib/js_helper.dart
+++ b/sdk/lib/_internal/js_runtime/lib/js_helper.dart
@@ -662,6 +662,7 @@
     return result;
   }
 
+  @pragma('dart2js:allow-cse')
   static String stringFromCharCode(int charCode) {
     if (0 <= charCode) {
       if (charCode <= 0xffff) {
diff --git a/tools/VERSION b/tools/VERSION
index 3afd39f..f3b3922 100644
--- a/tools/VERSION
+++ b/tools/VERSION
@@ -27,5 +27,5 @@
 MAJOR 3
 MINOR 9
 PATCH 0
-PRERELEASE 158
+PRERELEASE 159
 PRERELEASE_PATCH 0