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