blob: a8b11e081a03ac3b60cabb6013a709e9301d833e [file] [log] [blame]
// Copyright (c) 2012, 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.
// @dart = 2.10
import '../common.dart';
import '../common/codegen.dart' show CodegenRegistry;
import '../common/elements.dart' show JCommonElements;
import '../common/names.dart' show Selectors;
import '../common/tasks.dart' show Measurer, CompilerTask;
import '../constants/constant_system.dart' as constant_system;
import '../constants/values.dart';
import '../elements/entities.dart';
import '../elements/names.dart';
import '../elements/types.dart';
import '../inferrer/abstract_value_domain.dart';
import '../inferrer/types.dart';
import '../ir/util.dart';
import '../js_backend/field_analysis.dart'
show FieldAnalysisData, JFieldAnalysis;
import '../js_backend/backend.dart' show CodegenInputs;
import '../js_backend/native_data.dart' show NativeData;
import '../js_model/type_recipe.dart'
show
TypeRecipe,
TypeExpressionRecipe,
TypeRecipeAndEnvironmentStructure,
TypeRecipeDomain,
TypeRecipeDomainImpl;
import '../js_backend/specialized_checks.dart';
import '../native/behavior.dart';
import '../options.dart';
import '../universe/selector.dart' show Selector;
import '../universe/side_effects.dart' show SideEffects;
import '../universe/use.dart' show StaticUse;
import '../util/util.dart';
import '../world.dart' show JClosedWorld;
import 'interceptor_simplifier.dart';
import 'interceptor_finalizer.dart';
import 'late_field_optimizer.dart';
import 'logging.dart';
import 'nodes.dart';
import 'metrics.dart';
import 'types.dart';
import 'types_propagation.dart';
import 'validate.dart' show NoUnusedPhiValidator;
import 'value_range_analyzer.dart';
import 'value_set.dart';
abstract class OptimizationPhase {
String get name;
void visitGraph(HGraph graph);
bool validPostcondition(HGraph graph);
}
class SsaOptimizerTask extends CompilerTask {
final CompilerOptions _options;
Map<HInstruction, Range> ranges = {};
Map<MemberEntity, OptimizationTestLog> loggersForTesting;
SsaOptimizerTask(Measurer measurer, this._options) : super(measurer);
@override
String get name => 'SSA optimizer';
void optimize(
MemberEntity member,
HGraph graph,
CodegenInputs codegen,
JClosedWorld closedWorld,
GlobalTypeInferenceResults globalInferenceResults,
CodegenRegistry registry,
SsaMetrics metrics) {
void runPhase(OptimizationPhase phase) {
measureSubtask(phase.name, () => phase.visitGraph(graph));
codegen.tracer.traceGraph(phase.name, graph);
assert(graph.isValid(), 'Graph not valid after ${phase.name}');
assert(phase.validPostcondition(graph),
'Graph does not satify phase postcondition after ${phase.name}');
}
SsaCodeMotion codeMotion;
SsaLoadElimination loadElimination;
TypeRecipeDomain typeRecipeDomain =
TypeRecipeDomainImpl(closedWorld.dartTypes);
OptimizationTestLog log;
if (retainDataForTesting) {
loggersForTesting ??= {};
loggersForTesting[member] =
log = OptimizationTestLog(closedWorld.dartTypes);
}
measure(() {
List<OptimizationPhase> phases = [
// Run trivial instruction simplification first to optimize
// some patterns useful for type conversion.
SsaInstructionSimplifier(globalInferenceResults, _options, closedWorld,
typeRecipeDomain, registry, log, metrics),
SsaTypeConversionInserter(closedWorld),
SsaRedundantPhiEliminator(),
SsaDeadPhiEliminator(),
SsaTypePropagator(globalInferenceResults, closedWorld.commonElements,
closedWorld, log),
// After type propagation, more instructions can be
// simplified.
SsaInstructionSimplifier(globalInferenceResults, _options, closedWorld,
typeRecipeDomain, registry, log, metrics),
SsaInstructionSimplifier(globalInferenceResults, _options, closedWorld,
typeRecipeDomain, registry, log, metrics),
SsaTypePropagator(globalInferenceResults, closedWorld.commonElements,
closedWorld, log),
// Run a dead code eliminator before LICM because dead
// interceptors are often in the way of LICM'able instructions.
SsaDeadCodeEliminator(closedWorld, this),
SsaGlobalValueNumberer(closedWorld.abstractValueDomain),
// After GVN, some instructions might need their type to be
// updated because they now have different inputs.
SsaTypePropagator(globalInferenceResults, closedWorld.commonElements,
closedWorld, log),
codeMotion = SsaCodeMotion(closedWorld.abstractValueDomain),
loadElimination = SsaLoadElimination(closedWorld),
SsaRedundantPhiEliminator(),
SsaDeadPhiEliminator(),
SsaLateFieldOptimizer(closedWorld, log),
// After GVN and load elimination the same value may be used in code
// controlled by a test on the value, so redo 'conversion insertion' to
// learn from the refined type.
SsaTypeConversionInserter(closedWorld),
SsaTypePropagator(globalInferenceResults, closedWorld.commonElements,
closedWorld, log),
SsaValueRangeAnalyzer(closedWorld, this),
// Previous optimizations may have generated new
// opportunities for instruction simplification.
SsaInstructionSimplifier(globalInferenceResults, _options, closedWorld,
typeRecipeDomain, registry, log, metrics),
];
phases.forEach(runPhase);
// Simplifying interceptors is just an optimization, it is required for
// implementation correctness because the code generator assumes it is
// always performed to compute the intercepted classes sets.
runPhase(SsaSimplifyInterceptors(closedWorld, member.enclosingClass));
SsaDeadCodeEliminator dce = SsaDeadCodeEliminator(closedWorld, this);
runPhase(dce);
if (codeMotion.movedCode ||
dce.eliminatedSideEffects ||
dce.newGvnCandidates ||
loadElimination.newGvnCandidates) {
phases = [
SsaTypePropagator(globalInferenceResults, closedWorld.commonElements,
closedWorld, log),
SsaGlobalValueNumberer(closedWorld.abstractValueDomain),
SsaCodeMotion(closedWorld.abstractValueDomain),
SsaValueRangeAnalyzer(closedWorld, this),
SsaInstructionSimplifier(globalInferenceResults, _options,
closedWorld, typeRecipeDomain, registry, log, metrics),
SsaSimplifyInterceptors(closedWorld, member.enclosingClass),
SsaDeadCodeEliminator(closedWorld, this),
];
} else {
phases = [
SsaTypePropagator(globalInferenceResults, closedWorld.commonElements,
closedWorld, log),
// Run the simplifier to remove unneeded type checks inserted by
// type propagation.
SsaInstructionSimplifier(globalInferenceResults, _options,
closedWorld, typeRecipeDomain, registry, log, metrics),
];
}
phases.forEach(runPhase);
});
// SsaFinalizeInterceptors must always be run to ensure consistent calling
// conventions between SSA-generated code and other code fragments generated
// by the emitter.
// TODO(sra): Generate these other fragments via SSA, then this phase
// becomes an opt-in optimization.
runPhase(SsaFinalizeInterceptors(closedWorld));
}
}
/// Returns `true` if [mask] represents only types that have a length that
/// cannot change. The current implementation is conservative for the purpose
/// of identifying gvn-able lengths and mis-identifies some unions of fixed
/// length indexables (see TODO) as not fixed length.
bool isFixedLength(AbstractValue mask, JClosedWorld closedWorld) {
AbstractValueDomain abstractValueDomain = closedWorld.abstractValueDomain;
if (abstractValueDomain.isContainer(mask) &&
abstractValueDomain.getContainerLength(mask) != null) {
// A container on which we have inferred the length.
return true;
}
// TODO(sra): Recognize any combination of fixed length indexables.
if (abstractValueDomain.isFixedArray(mask).isDefinitelyTrue ||
abstractValueDomain.isStringOrNull(mask).isDefinitelyTrue ||
abstractValueDomain.isTypedArray(mask).isDefinitelyTrue) {
return true;
}
return false;
}
/// Returns `true` if the end of [block] is unreachable, e.g. due to a `throw`
/// expression.
bool hasUnreachableExit(HBasicBlock block) {
if (!block.isLive) return false;
HInstruction last = block.last;
if (last is HGoto) {
HInstruction previous = last.previous;
if (previous is HThrowExpression) return true;
// TODO(sra): Match other signs of unreachability, e.g. a call to a method
// that returns `[empty]`.
}
return false;
}
/// If both inputs to known operations are available execute the operation at
/// compile-time.
class SsaInstructionSimplifier extends HBaseVisitor
implements OptimizationPhase {
// We don't produce constant-folded strings longer than this unless they have
// a single use. This protects against exponentially large constant folded
// strings.
static const MAX_SHARED_CONSTANT_FOLDED_STRING_LENGTH = 512;
@override
final String name = "SsaInstructionSimplifier";
final GlobalTypeInferenceResults _globalInferenceResults;
final CompilerOptions _options;
final JClosedWorld _closedWorld;
final TypeRecipeDomain _typeRecipeDomain;
final CodegenRegistry _registry;
final OptimizationTestLog _log;
final SsaMetrics _metrics;
HGraph _graph;
SsaInstructionSimplifier(
this._globalInferenceResults,
this._options,
this._closedWorld,
this._typeRecipeDomain,
this._registry,
this._log,
this._metrics);
JCommonElements get commonElements => _closedWorld.commonElements;
AbstractValueDomain get _abstractValueDomain =>
_closedWorld.abstractValueDomain;
NativeData get _nativeData => _closedWorld.nativeData;
@override
void visitGraph(HGraph visitee) {
_graph = visitee;
visitDominatorTree(visitee);
}
@override
bool validPostcondition(HGraph graph) => true;
@override
visitBasicBlock(HBasicBlock block) {
simplifyPhis(block);
HInstruction instruction = block.first;
while (instruction != null) {
HInstruction next = instruction.next;
HInstruction replacement = instruction.accept(this);
if (replacement != instruction) {
block.rewrite(instruction, replacement);
// The intersection of double and int return conflicting, and
// because of our number implementation for JavaScript, it
// might be that an operation thought to return double, can be
// simplified to an int. For example:
// `2.5 * 10`.
if (!(replacement
.isNumberOrNull(_abstractValueDomain)
.isDefinitelyTrue &&
instruction
.isNumberOrNull(_abstractValueDomain)
.isDefinitelyTrue)) {
// If we can replace [instruction] with [replacement], then
// [replacement]'s type can be narrowed.
AbstractValue newType = _abstractValueDomain.intersection(
replacement.instructionType, instruction.instructionType);
replacement.instructionType = newType;
}
// If the replacement instruction does not know its
// source element, use the source element of the
// instruction.
replacement.sourceElement ??= instruction.sourceElement;
replacement.sourceInformation ??= instruction.sourceInformation;
if (!replacement.isInBasicBlock()) {
// The constant folding can return an instruction that is already
// part of the graph (like an input), so we only add the replacement
// if necessary.
block.addAfter(instruction, replacement);
// Visit the replacement as the next instruction in case it
// can also be constant folded away.
next = replacement;
}
_removeInstructionAndPureDeadInputs(instruction);
}
instruction = next;
}
}
/// Removes [instruction] and the DAG of pure instructions that are transitive
/// inputs to [instruction].
///
/// It is worth doing the cleanup of removing the instructions in the
/// simplifier as it reduces the `usedBy` count, which enables rewrites that
/// are sensitive to the use count. Such rewrites can occur much earlier with
/// 'online' usedBy counting. The cost of visiting all inputs of a rewritten
/// instruction is regained by avoiding visiting the dead instructions in
/// subsequent passes.
void _removeInstructionAndPureDeadInputs(HInstruction instruction) {
List<HInstruction> worklist = instruction.inputs.toList();
instruction.block.remove(instruction);
for (int i = 0; i < worklist.length; i++) {
HInstruction next = worklist[i];
HBasicBlock block = next.block;
if (block == null) continue; // Already removed.
if (next.usedBy.isEmpty && next.isPure(_abstractValueDomain)) {
// Special cases that are removed properly by other phases.
if (next is HParameterValue) continue;
if (next is HLocalValue) continue;
if (next is HConstant) continue;
if (next is HPhi) continue;
worklist.addAll(next.inputs);
block.remove(next);
}
}
}
// Simplify some CFG diamonds to equivalent expressions.
simplifyPhis(HBasicBlock block) {
// Is [block] the join point for a simple diamond that generates a single
// phi node?
if (block.phis.isEmpty) return;
HPhi phi = block.phis.first;
if (phi.next != null) return;
if (block.predecessors.length != 2) return;
assert(phi.inputs.length == 2);
HBasicBlock b1 = block.predecessors[0];
HBasicBlock b2 = block.predecessors[1];
HBasicBlock dominator = block.dominator;
if (!(b1.dominator == dominator && b2.dominator == dominator)) return;
// Extract the controlling condition.
HInstruction controlFlow = dominator.last;
if (controlFlow is! HIf) return;
HInstruction test = controlFlow.inputs.single;
if (test.usedBy.length > 1) return;
bool negated = false;
while (test is HNot) {
test = test.inputs.single;
if (test.usedBy.length > 1) return;
negated = !negated;
}
if (test is! HIdentity) return;
HInstruction tested;
if (test.inputs[0].isNull(_abstractValueDomain).isDefinitelyTrue) {
tested = test.inputs[1];
} else if (test.inputs[1].isNull(_abstractValueDomain).isDefinitelyTrue) {
tested = test.inputs[0];
} else {
return;
}
HInstruction whenNullValue = phi.inputs[negated ? 1 : 0];
HInstruction whenNotNullValue = phi.inputs[negated ? 0 : 1];
HBasicBlock whenNullBlock = block.predecessors[negated ? 1 : 0];
HBasicBlock whenNotNullBlock = block.predecessors[negated ? 0 : 1];
// If 'x' is nullable boolean,
//
// x == null ? false : x ---> x == true
//
// This ofen comes from the dart code `x ?? false`.
if (_sameOrRefinementOf(tested, whenNotNullValue) &&
_isBoolConstant(whenNullValue, false) &&
whenNotNullValue
.isBooleanOrNull(_abstractValueDomain)
.isDefinitelyTrue &&
_mostlyEmpty(whenNullBlock) &&
_mostlyEmpty(whenNotNullBlock)) {
HInstruction trueConstant = _graph.addConstantBool(true, _closedWorld);
HInstruction replacement =
HIdentity(tested, trueConstant, _abstractValueDomain.boolType)
..sourceElement = phi.sourceElement
..sourceInformation = phi.sourceInformation;
block.rewrite(phi, replacement);
block.addAtEntry(replacement);
block.removePhi(phi);
return;
}
// If 'x'is nullable boolean,
//
// x == null ? true : x ---> !(x == false)
//
// This ofen comes from the dart code `x ?? true`.
if (_sameOrRefinementOf(tested, whenNotNullValue) &&
_isBoolConstant(whenNullValue, true) &&
whenNotNullValue
.isBooleanOrNull(_abstractValueDomain)
.isDefinitelyTrue &&
_mostlyEmpty(whenNullBlock) &&
_mostlyEmpty(whenNotNullBlock)) {
HInstruction falseConstant = _graph.addConstantBool(false, _closedWorld);
HInstruction compare =
HIdentity(tested, falseConstant, _abstractValueDomain.boolType);
block.addAtEntry(compare);
HInstruction replacement = HNot(compare, _abstractValueDomain.boolType)
..sourceElement = phi.sourceElement
..sourceInformation = phi.sourceInformation;
block.rewrite(phi, replacement);
block.addAfter(compare, replacement);
block.removePhi(phi);
return;
}
// TODO(sra): Consider other simple diamonds, e.g. with the addition of a
// special instruction,
//
// s == null ? "" : s ---> s || "".
return;
}
bool _isBoolConstant(HInstruction node, bool value) {
if (node is HConstant) {
ConstantValue c = node.constant;
if (c is BoolConstantValue) {
return c.boolValue == value;
}
}
return false;
}
bool _sameOrRefinementOf(HInstruction base, HInstruction insn) {
if (base == insn) return true;
if (insn is HTypeKnown) return _sameOrRefinementOf(base, insn.checkedInput);
return false;
}
bool _mostlyEmpty(HBasicBlock block) {
for (HInstruction insn = block.first; insn != null; insn = insn.next) {
if (insn is HTypeKnown) continue;
if (insn is HGoto) return true;
return false;
}
return true;
}
@override
HInstruction visitInstruction(HInstruction node) {
return node;
}
ConstantValue getConstantFromType(HInstruction node) {
if (node.isValue(_abstractValueDomain) &&
node.isNull(_abstractValueDomain).isDefinitelyFalse &&
node.isLateSentinel(_abstractValueDomain).isDefinitelyFalse) {
ConstantValue value =
_abstractValueDomain.getPrimitiveValue(node.instructionType);
if (value is BoolConstantValue) {
return value;
}
// TODO(het): consider supporting other values (short strings?)
}
return null;
}
void propagateConstantValueToUses(HInstruction node) {
if (node.usedBy.isEmpty) return;
ConstantValue value = getConstantFromType(node);
if (value != null) {
HConstant constant = _graph.addConstant(value, _closedWorld);
for (HInstruction user in node.usedBy.toList()) {
user.changeUse(node, constant);
}
}
}
@override
HInstruction visitParameterValue(HParameterValue node) {
// [HParameterValue]s are either the value of the parameter (in fully SSA
// converted code), or the mutable variable containing the value (in
// incompletely SSA converted code, e.g. methods containing exceptions).
//
// If the parameter is used as a mutable variable we cannot replace the
// variable with a value.
//
// If the parameter is used as a mutable variable but never written, first
// convert to a value parameter.
if (node.usedAsVariable()) {
// Trivial SSA-conversion. Replace all HLocalGet instructions with the
// parameter.
//
// If there is a single assignment, it could dominate all the reads, or
// dominate all the reads except one read, which was used to 'check' the
// value at entry, like this:
//
// t1 = HLocalGet(param);
// t2 = check(t1); // replace t1 with param
// HLocalSet(param, t2);
// ....
// t3 = HLocalGet(param);
// ... t3 ... // replace t3 with t2
//
HLocalSet assignment;
for (HInstruction user in node.usedBy) {
if (user is HLocalSet) {
assert(user.local == node);
if (assignment != null) return node; // Two or more assignments.
assignment = user;
continue;
}
if (user is HLocalGet) continue;
// There should not really be anything else but there are Phis which
// have not been cleaned up.
return node;
}
HLocalGet checkedLoad;
HInstruction value = node;
if (assignment != null) {
value = assignment.value;
HInstruction source = value.nonCheck();
if (source is HLocalGet && source.local == node) {
if (source.usedBy.length != 1) return node;
checkedLoad = source;
}
}
// If there is a single assignment it will dominate all other uses.
List<HLocalGet> loads = [];
for (HInstruction user in node.usedBy) {
if (user == assignment) continue;
if (user == checkedLoad) continue;
assert(user is HLocalGet && user.local == node);
if (assignment != null && !assignment.dominates(user)) return node;
loads.add(user);
}
for (HLocalGet user in loads) {
user.block.rewrite(user, value);
user.block.remove(user);
}
if (checkedLoad != null) {
checkedLoad.block.rewrite(checkedLoad, node);
checkedLoad.block.remove(checkedLoad);
}
if (assignment != null) {
assignment.block.remove(assignment);
}
}
propagateConstantValueToUses(node);
return node;
}
@override
HInstruction visitNot(HNot node) {
List<HInstruction> inputs = node.inputs;
assert(inputs.length == 1);
HInstruction input = inputs[0];
if (input is HConstant) {
return _graph.addConstantBool(
input.constant is! TrueConstantValue, _closedWorld);
} else if (input is HNot) {
return input.inputs[0];
}
return node;
}
@override
HInstruction visitInvokeUnary(HInvokeUnary node) {
HInstruction folded = foldUnary(node.operation(), node.operand);
return folded ?? node;
}
HInstruction foldUnary(
constant_system.UnaryOperation operation, HInstruction operand) {
if (operand is HConstant) {
HConstant receiver = operand;
ConstantValue folded = operation.fold(receiver.constant);
if (folded != null) return _graph.addConstant(folded, _closedWorld);
}
return null;
}
HInstruction tryOptimizeLengthInterceptedGetter(HInvokeDynamic node) {
HInstruction actualReceiver = node.inputs[1];
if (actualReceiver
.isIndexablePrimitive(_abstractValueDomain)
.isDefinitelyTrue) {
if (actualReceiver.isConstantString()) {
HConstant constantInput = actualReceiver;
StringConstantValue constant = constantInput.constant;
return _graph.addConstantInt(constant.length, _closedWorld);
} else if (actualReceiver.isConstantList()) {
HConstant constantInput = actualReceiver;
ListConstantValue constant = constantInput.constant;
return _graph.addConstantInt(constant.length, _closedWorld);
}
bool isFixed =
isFixedLength(actualReceiver.instructionType, _closedWorld);
AbstractValue actualType = node.instructionType;
AbstractValue resultType = _abstractValueDomain.positiveIntType;
// If we already have computed a more specific type, keep that type.
if (_abstractValueDomain
.isInstanceOfOrNull(actualType, commonElements.jsUInt31Class)
.isDefinitelyTrue) {
resultType = _abstractValueDomain.uint31Type;
} else if (_abstractValueDomain
.isInstanceOfOrNull(actualType, commonElements.jsUInt32Class)
.isDefinitelyTrue) {
resultType = _abstractValueDomain.uint32Type;
}
HInstruction checkedReceiver = actualReceiver;
if (actualReceiver.isNull(_abstractValueDomain).isPotentiallyTrue) {
// The receiver is potentially `null` so we insert a null receiver
// check. This will be folded into the length access later.
HNullCheck check = HNullCheck(actualReceiver,
_abstractValueDomain.excludeNull(actualReceiver.instructionType))
..selector = node.selector
..sourceInformation = node.sourceInformation;
_log?.registerNullCheck(node, check);
node.block.addBefore(node, check);
checkedReceiver = check;
}
HGetLength result =
HGetLength(checkedReceiver, resultType, isAssignable: !isFixed);
return result;
} else if (actualReceiver.isConstantMap()) {
HConstant constantInput = actualReceiver;
MapConstantValue constant = constantInput.constant;
return _graph.addConstantInt(constant.length, _closedWorld);
}
return null;
}
HInstruction handleInterceptedCall(HInvokeDynamic node) {
// Try constant folding the instruction.
constant_system.Operation operation = node.specializer.operation();
if (operation != null) {
HInstruction instruction = node.inputs.length == 2
? foldUnary(operation, node.inputs[1])
: foldBinary(operation, node.inputs[1], node.inputs[2]);
if (instruction != null) {
_metrics.countOperationFolded.add();
return instruction;
}
}
// Try converting the instruction to a builtin instruction.
HInstruction instruction = node.specializer.tryConvertToBuiltin(node,
_graph, _globalInferenceResults, commonElements, _closedWorld, _log);
if (instruction != null) {
_metrics.countSpecializations.add();
return instruction;
}
Selector selector = node.selector;
AbstractValue mask = node.receiverType;
HInstruction input = node.inputs[1];
bool applies(MemberEntity element) {
return selector.applies(element) &&
(mask == null ||
_abstractValueDomain
.isTargetingMember(mask, element, selector.memberName)
.isPotentiallyTrue);
}
if (selector.isCall || selector.isOperator) {
FunctionEntity target;
if (input.isExtendableArray(_abstractValueDomain).isDefinitelyTrue) {
if (applies(commonElements.jsArrayAdd)) {
// Codegen special cases array calls to `Array.push`, but does not
// inline argument type checks. We lower if the check always passes
// (due to invariance or being a top-type), or if the check is not
// emitted.
if (node.isInvariant ||
input is HLiteralList ||
!_closedWorld.annotationsData
.getParameterCheckPolicy(commonElements.jsArrayAdd)
.isEmitted) {
target = commonElements.jsArrayAdd;
}
}
} else if (input.isStringOrNull(_abstractValueDomain).isDefinitelyTrue) {
if (commonElements.appliesToJsStringSplit(
selector, mask, _abstractValueDomain)) {
return handleStringSplit(node);
} else if (applies(commonElements.jsStringOperatorAdd)) {
// `operator+` is turned into a JavaScript '+' so we need to
// make sure the receiver and the argument are not null.
// TODO(sra): Do this via [node.specializer].
HInstruction argument = node.inputs[2];
if (argument.isString(_abstractValueDomain).isDefinitelyTrue &&
input.isNull(_abstractValueDomain).isDefinitelyFalse) {
return HStringConcat(input, argument, node.instructionType);
}
} else if (applies(commonElements.jsStringToString) &&
input.isNull(_abstractValueDomain).isDefinitelyFalse) {
return input;
}
}
if (target != null) {
// TODO(ngeoffray): There is a strong dependency between codegen
// and this optimization that the dynamic invoke does not need an
// interceptor. We currently need to keep a
// HInvokeDynamicMethod and not create a HForeign because
// HForeign is too opaque for the SsaCheckInserter (that adds a
// bounds check on removeLast). Once we start inlining, the
// bounds check will become explicit, so we won't need this
// optimization.
// TODO(sra): Fix comment - SsaCheckInserter is deleted.
HInvokeDynamicMethod result = HInvokeDynamicMethod(
node.selector,
node.receiverType,
node.inputs.sublist(1), // Drop interceptor.
node.instructionType,
node.typeArguments,
node.sourceInformation,
isIntercepted: false);
result.element = target;
return result;
}
} else if (selector.isGetter) {
if (commonElements.appliesToJsIndexableLength(selector)) {
HInstruction optimized = tryOptimizeLengthInterceptedGetter(node);
if (optimized != null) {
_metrics.countLengthOptimized.add();
return optimized;
}
}
}
return node;
}
HInstruction handleStringSplit(HInvokeDynamic node) {
HInstruction argument = node.inputs[2];
if (!argument.isString(_abstractValueDomain).isDefinitelyTrue) {
return node;
}
// Replace `s.split$1(pattern)` with
//
// t1 = s.split(pattern);
// t2 = String;
// t3 = JSArray<t2>;
// t4 = setArrayType(t1, t3);
//
AbstractValue resultMask = _abstractValueDomain.growableListType;
HInvokeDynamicMethod splitInstruction = HInvokeDynamicMethod(
node.selector,
node.receiverType,
node.inputs.sublist(1), // Drop interceptor.
resultMask,
const <DartType>[],
node.sourceInformation,
isIntercepted: false)
..element = commonElements.jsStringSplit
..setAllocation(true);
if (!_closedWorld.rtiNeed
.classNeedsTypeArguments(commonElements.jsArrayClass)) {
return splitInstruction;
}
node.block.addBefore(node, splitInstruction);
HInstruction typeInfo = HLoadType.type(
_closedWorld.elementEnvironment.createInterfaceType(
commonElements.jsArrayClass, [commonElements.stringType]),
_abstractValueDomain.dynamicType);
node.block.addBefore(node, typeInfo);
HInvokeStatic tagInstruction = HInvokeStatic(commonElements.setArrayType,
[splitInstruction, typeInfo], resultMask, const <DartType>[]);
// 'Linear typing' trick: [tagInstruction] is the only use of the
// [splitInstruction], so it becomes the sole alias.
// TODO(sra): Build this knowledge into alias analysis.
tagInstruction.setAllocation(true);
return tagInstruction;
}
@override
HInstruction visitInvokeDynamicMethod(HInvokeDynamicMethod node) {
propagateConstantValueToUses(node);
if (node.isInterceptedCall) {
HInstruction folded = handleInterceptedCall(node);
if (folded != node) return folded;
}
HInstruction receiver = node.getDartReceiver(_closedWorld);
AbstractValue receiverType = receiver.instructionType;
MemberEntity element =
_closedWorld.locateSingleMember(node.selector, receiverType);
if (element == null) return node;
// TODO(ngeoffray): Also fold if it's a getter or variable.
if (element.isFunction &&
// If we found out that the only target is an implicitly called
// `noSuchMethod` we just ignore it.
node.selector.applies(element)) {
FunctionEntity method = element;
if (_nativeData.isNativeMember(method)) {
return tryInlineNativeMethod(node, method) ?? node;
}
// TODO(ngeoffray): If the method has optional parameters, we should pass
// the default values.
ParameterStructure parameters = method.parameterStructure;
if (parameters.totalParameters == node.selector.argumentCount &&
parameters.typeParameters ==
node.selector.callStructure.typeArgumentCount) {
node.element = method;
}
return node;
}
// Replace method calls through fields with a closure call on the value of
// the field. This usually removes the demand for the call-through stub and
// makes the field load available to further optimization, e.g. LICM.
if (element.isField && element.name == node.selector.name) {
FieldEntity field = element;
if (!_nativeData.isNativeMember(field) &&
!node.isCallOnInterceptor(_closedWorld)) {
// Insertion point for the closure call.
HInstruction insertionPoint = node;
HInstruction load;
FieldAnalysisData fieldData =
_closedWorld.fieldAnalysis.getFieldData(field);
if (fieldData.isEffectivelyConstant) {
// The field is elided and replace it with its constant value.
if (_abstractValueDomain.isNull(receiverType).isPotentiallyTrue) {
// The receiver is potentially `null` so we insert a null receiver
// check to trigger a null pointer exception. Insert check
// conditionally to avoid the work of removing it later.
HNullCheck check = HNullCheck(
receiver, _abstractValueDomain.excludeNull(receiverType))
..selector = node.selector
..sourceInformation = node.sourceInformation;
_log?.registerNullCheck(node, check);
node.block.addBefore(node, check);
insertionPoint = check;
}
ConstantValue value = fieldData.constantValue;
load = _graph.addConstant(value, _closedWorld,
sourceInformation: node.sourceInformation);
_log?.registerConstantFieldCall(node, field, load);
} else {
AbstractValue type = AbstractValueFactory.inferredTypeForMember(
field, _globalInferenceResults);
load = HFieldGet(field, receiver, type, node.sourceInformation);
_log?.registerFieldCall(node, load);
node.block.addBefore(node, load);
insertionPoint = load;
}
Selector callSelector = Selector.callClosureFrom(node.selector);
List<HInstruction> inputs = [
load,
...node.inputs.skip(node.isInterceptedCall ? 2 : 1)
];
DartType fieldType =
_closedWorld.elementEnvironment.getFieldType(field);
HInstruction closureCall = HInvokeClosure(
callSelector,
_abstractValueDomain
.createFromStaticType(fieldType, nullable: true)
.abstractValue,
inputs,
node.instructionType,
node.typeArguments)
..sourceInformation = node.sourceInformation;
node.block.addAfter(insertionPoint, closureCall);
return closureCall;
}
}
return node;
}
bool _avoidInliningNativeMethod(HInvokeDynamic node, FunctionEntity method) {
assert(_nativeData.isNativeMember(method));
if (_options.disableInlining) return true;
if (_closedWorld.annotationsData.hasNoInline(method)) {
return true;
}
return false;
}
// Try to 'inline' an instance getter call to a known native or js-interop
// getter. This replaces the call to the getter on the Dart interceptor with a
// direct call to the external method.
HInstruction tryInlineNativeGetter(
HInvokeDynamicGetter node, FunctionEntity method) {
if (_avoidInliningNativeMethod(node, method)) return null;
// Strengthen instruction type from annotations to help optimize dependent
// instructions.
NativeBehavior nativeBehavior = _nativeData.getNativeMethodBehavior(method);
AbstractValue returnType =
AbstractValueFactory.fromNativeBehavior(nativeBehavior, _closedWorld);
HInstruction receiver = node.inputs.last; // Drop interceptor.
receiver = maybeGuardWithNullCheck(receiver, node, null);
HInstruction result = HInvokeExternal(
method, [receiver], returnType, nativeBehavior,
sourceInformation: node.sourceInformation);
_registry.registerStaticUse(StaticUse.methodInlining(method, null));
// Assume Native getters effect-free as an approximantion to being
// idempotent.
// TODO(sra): [native.BehaviorBuilder.buildMethodBehavior] should do this
// for us.
result.sideEffects.setDependsOnSomething();
result.sideEffects.clearAllSideEffects();
result.setUseGvn();
return maybeAddNativeReturnNullCheck(node, result, method);
}
HInstruction maybeAddNativeReturnNullCheck(
HInstruction node, HInstruction replacement, FunctionEntity method) {
if (_options.nativeNullAssertions) {
if (method.library.isNonNullableByDefault) {
FunctionType type =
_closedWorld.elementEnvironment.getFunctionType(method);
if (_closedWorld.dartTypes.isNonNullableIfSound(type.returnType) &&
memberEntityIsInWebLibrary(method)) {
node.block.addBefore(node, replacement);
replacement = HNullCheck(replacement,
_abstractValueDomain.excludeNull(replacement.instructionType),
sticky: true);
}
}
}
return replacement;
}
// Try to 'inline' an instance setter call to a known native or js-interop
// getter. This replaces the call to the setter on the Dart interceptor with a
// direct call to the external method.
HInstruction tryInlineNativeSetter(
HInvokeDynamicSetter node, FunctionEntity method) {
if (_avoidInliningNativeMethod(node, method)) return null;
assert(node.inputs.length == 3);
HInstruction receiver = node.inputs[1];
HInstruction value = node.inputs[2];
FunctionType type = _closedWorld.elementEnvironment.getFunctionType(method);
assert(type.optionalParameterTypes.isEmpty);
assert(type.namedParameterTypes.isEmpty);
DartType parameterType = type.parameterTypes.single;
if (_nativeArgumentNeedsCheckOrConversion(method, parameterType, value)) {
return null;
}
NativeBehavior nativeBehavior = _nativeData.getNativeMethodBehavior(method);
receiver = maybeGuardWithNullCheck(receiver, node, null);
HInvokeExternal result = HInvokeExternal(
method, [receiver, value], value.instructionType, nativeBehavior,
sourceInformation: node.sourceInformation);
_registry.registerStaticUse(StaticUse.methodInlining(method, null));
return result;
}
// TODO(sra): Refactor this code so that we can decide to inline the method
// with a few checks or conversions. We would want to do this if there was a
// single call site to [method], or most arguments do not require a check.
bool _nativeArgumentNeedsCheckOrConversion(
FunctionEntity method, DartType parameterType, HInstruction argument) {
// TODO(sra): JS-interop *instance* methods don't check their arguments
// since the forwarding stub is shared by all JS-interop methods with the
// same name, regardless of parameter types. We could 'inline' js-interop
// calls even when the types of the arguments are incorrect.
if (!_nativeData.isJsInteropMember(method)) {
// @Native methods have conversion code for function arguments. Rather
// than insert that code at the inlined call site, call the target on the
// interceptor.
if (parameterType.withoutNullability is FunctionType) return true;
}
if (!_closedWorld.annotationsData
.getParameterCheckPolicy(method)
.isEmitted) {
// If the target has no checks we can inline.
return false;
}
AbstractValue parameterAbstractValue = _abstractValueDomain
.getAbstractValueForNativeMethodParameterType(parameterType);
if (parameterAbstractValue == null ||
_abstractValueDomain
.isIn(argument.instructionType, parameterAbstractValue)
.isPotentiallyFalse) {
return true;
}
return false;
}
// Try to 'inline' an instance method call to a known native or js-interop
// method. This replaces the call to the method on the Dart interceptor with a
// direct call to the external method.
HInstruction tryInlineNativeMethod(
HInvokeDynamicMethod node, FunctionEntity method) {
if (_avoidInliningNativeMethod(node, method)) return null;
// We can replace the call to the native class interceptor method (target)
// if the target does no conversions or useful type checks.
FunctionType type = _closedWorld.elementEnvironment.getFunctionType(method);
if (type.namedParameters.isNotEmpty) return null;
// The call site might omit optional arguments. The inlined code must
// preserve the number of arguments, so check only the actual arguments.
bool canInline = true;
List<HInstruction> inputs = node.inputs;
int inputPosition = 2; // Skip interceptor and receiver.
void checkParameterType(DartType parameterType) {
if (!canInline) return;
if (inputPosition >= inputs.length) return;
HInstruction input = inputs[inputPosition++];
if (_nativeArgumentNeedsCheckOrConversion(method, parameterType, input)) {
canInline = false;
}
}
type.parameterTypes.forEach(checkParameterType);
type.optionalParameterTypes.forEach(checkParameterType);
assert(type.namedParameterTypes.isEmpty);
if (!canInline) return null;
// Strengthen instruction type from annotations to help optimize
// dependent instructions.
NativeBehavior nativeBehavior = _nativeData.getNativeMethodBehavior(method);
AbstractValue returnType =
AbstractValueFactory.fromNativeBehavior(nativeBehavior, _closedWorld);
HInstruction receiver = inputs[1];
receiver = maybeGuardWithNullCheck(receiver, node, null);
HInstruction result = HInvokeExternal(
method,
[receiver, ...inputs.skip(2)], // '2': Drop interceptor and receiver.
returnType,
nativeBehavior,
sourceInformation: node.sourceInformation);
_registry.registerStaticUse(StaticUse.methodInlining(method, null));
return maybeAddNativeReturnNullCheck(node, result, method);
}
@override
HInstruction visitBoundsCheck(HBoundsCheck node) {
HInstruction index = node.index;
if (index.isInteger(_abstractValueDomain).isDefinitelyTrue) {
return node;
}
if (index.isConstant()) {
HConstant constantInstruction = index;
assert(constantInstruction.constant is! IntConstantValue);
if (!constant_system.isInt(constantInstruction.constant)) {
// -0.0 is a double but will pass the runtime integer check.
node.staticChecks = HBoundsCheck.ALWAYS_FALSE;
}
}
return node;
}
HInstruction foldBinary(constant_system.BinaryOperation operation,
HInstruction left, HInstruction right) {
if (left is HConstant && right is HConstant) {
HConstant op1 = left;
HConstant op2 = right;
ConstantValue folded = operation.fold(op1.constant, op2.constant);
if (folded != null) return _graph.addConstant(folded, _closedWorld);
}
return null;
}
@override
HInstruction visitAdd(HAdd node) {
HInstruction left = node.left;
HInstruction right = node.right;
// We can only perform this rewriting on Integer, as it is not
// valid for -0.0.
if (left.isInteger(_abstractValueDomain).isDefinitelyTrue &&
right.isInteger(_abstractValueDomain).isDefinitelyTrue) {
if (left is HConstant && left.constant.isZero) return right;
if (right is HConstant && right.constant.isZero) return left;
}
return super.visitAdd(node);
}
@override
HInstruction visitMultiply(HMultiply node) {
HInstruction left = node.left;
HInstruction right = node.right;
if (left.isNumber(_abstractValueDomain).isDefinitelyTrue &&
right.isNumber(_abstractValueDomain).isDefinitelyTrue) {
if (left is HConstant && left.constant.isOne) return right;
if (right is HConstant && right.constant.isOne) return left;
}
return super.visitMultiply(node);
}
@override
HInstruction visitInvokeBinary(HInvokeBinary node) {
HInstruction left = node.left;
HInstruction right = node.right;
constant_system.BinaryOperation operation = node.operation();
HConstant folded = foldBinary(operation, left, right);
if (folded != null) return folded;
return node;
}
@override
HInstruction visitRelational(HRelational node) {
return super.visitRelational(node);
}
HInstruction handleIdentityCheck(HRelational node) {
HInstruction left = node.left;
HInstruction right = node.right;
AbstractValue leftType = left.instructionType;
AbstractValue rightType = right.instructionType;
HInstruction makeTrue() => _graph.addConstantBool(true, _closedWorld);
HInstruction makeFalse() => _graph.addConstantBool(false, _closedWorld);
// Intersection of int and double return conflicting, so
// we don't optimize on numbers to preserve the runtime semantics.
if (!(left.isNumberOrNull(_abstractValueDomain).isDefinitelyTrue &&
right.isNumberOrNull(_abstractValueDomain).isDefinitelyTrue)) {
if (_abstractValueDomain
.areDisjoint(leftType, rightType)
.isDefinitelyTrue) {
return makeFalse();
}
}
if (left.isNull(_abstractValueDomain).isDefinitelyTrue &&
right.isNull(_abstractValueDomain).isDefinitelyTrue) {
return makeTrue();
}
HInstruction compareConstant(HConstant constant, HInstruction input) {
if (constant.constant is TrueConstantValue) {
return input;
} else {
return HNot(input, _abstractValueDomain.boolType);
}
}
if (left.isConstantBoolean() &&
right.isBoolean(_abstractValueDomain).isDefinitelyTrue) {
return compareConstant(left, right);
}
if (right.isConstantBoolean() &&
left.isBoolean(_abstractValueDomain).isDefinitelyTrue) {
return compareConstant(right, left);
}
if (identical(left.nonCheck(), right.nonCheck())) {
// Avoid constant-folding `identical(x, x)` when `x` might be double. The
// dart2js runtime has not always been consistent with the Dart
// specification (section 16.0.1), which makes distinctions on NaNs and
// -0.0 that are hard to implement efficiently.
if (left.isIntegerOrNull(_abstractValueDomain).isDefinitelyTrue) {
return makeTrue();
}
if (left.isPrimitiveNumber(_abstractValueDomain).isDefinitelyFalse) {
return makeTrue();
}
}
return null;
}
FieldEntity _indexFieldOfEnumClass(ClassEntity enumClass) {
MemberEntity member = _closedWorld.elementEnvironment
.lookupClassMember(enumClass, const PublicName('index'));
if (member is FieldEntity) return member;
throw StateError('$enumClass should have index field, found $member');
}
IntConstantValue _indexValueOfEnumConstant(
ConstantValue constant, ClassEntity enumClass, FieldEntity field) {
if (constant is ConstructedConstantValue) {
if (constant.type.element == enumClass) {
final value = constant.fields[field];
if (value is IntConstantValue) return value;
}
}
throw StateError(
'Enum constant ${constant.toStructuredText(_closedWorld.dartTypes)}'
' should have $field');
}
// The `enum` class of the [node], or `null` if [node] does not have an
// non-nullable enum type.
ClassEntity _enumClass(HInstruction node) {
final cls = _abstractValueDomain.getExactClass(node.instructionType);
if (cls == null) return null;
if (_closedWorld.elementEnvironment.isEnumClass(cls)) return cls;
return null;
}
// Try to replace enum labels in a switch with the index values. This is
// usually smaller, faster, and might allow the JavaScript engine to compile
// the switch to a jump-table.
void _tryReduceEnumsInSwitch(HSwitch node) {
final enumClass = _enumClass(node.expression);
if (enumClass == null) return;
FieldEntity indexField = _indexFieldOfEnumClass(enumClass);
// In some cases the `index` field is elided. We can't load an elided field.
//
// TODO(sra): The ConstantValue has the index value, so we can still reduce
// the enum to the index value. We could handle an elided `index` field in
// some cases by doing this optimization more like scalar replacement or
// unboxing, replacing all enums in the method at once, possibly reducing
// phis of enums to phis of indexes.
if (_closedWorld.fieldAnalysis.getFieldData(indexField).isElided) return;
// All the label expressions should be of the same enum class as the switch
// expression.
for (final label in node.inputs.skip(1)) {
if (label is! HConstant) return;
if (_enumClass(label) != enumClass) return;
}
final loadIndex = _directFieldGet(node.expression, indexField, node);
node.block.addBefore(node, loadIndex);
node.replaceInput(0, loadIndex);
for (int i = 1; i < node.inputs.length; i++) {
ConstantValue labelConstant = (node.inputs[i] as HConstant).constant;
node.replaceInput(
i,
_graph.addConstant(
_indexValueOfEnumConstant(labelConstant, enumClass, indexField),
_closedWorld));
}
}
@override
HInstruction visitSwitch(HSwitch node) {
_tryReduceEnumsInSwitch(node);
return node;
}
@override
HInstruction visitIdentity(HIdentity node) {
HInstruction newInstruction = handleIdentityCheck(node);
return newInstruction ?? super.visitIdentity(node);
}
@override
HInstruction visitIsLateSentinel(HIsLateSentinel node) {
HInstruction value = node.inputs[0];
AbstractBool isLateSentinel = value.isLateSentinel(_abstractValueDomain);
if (isLateSentinel.isDefinitelyTrue) {
_metrics.countLateSentinelCheckDecided.add();
return _graph.addConstantBool(true, _closedWorld);
} else if (isLateSentinel.isDefinitelyFalse) {
_metrics.countLateSentinelCheckDecided.add();
return _graph.addConstantBool(false, _closedWorld);
}
return super.visitIsLateSentinel(node);
}
void simplifyCondition(
HBasicBlock block, HInstruction condition, bool value, String tag) {
if (block == null) return;
// `excludePhiOutEdges: true` prevents replacing a partially dominated phi
// node input with a constant. This tends to add unnecessary assignments, by
// transforming the following, which has phi(false, x),
//
// if (x) { init(); x = false; }
//
// into this, which has phi(false, false)
//
// if (x) { init(); x = false; } else { x = false; }
//
// which is further simplified to:
//
// if (x) { init(); }
// ...
// x = false;
//
// This is mostly harmless (if a little confusing) but does cause a lot of
// `x = false;` copies to be inserted when a loop body has many continue
// statements or ends with a switch.
DominatedUses uses =
DominatedUses.of(condition, block.first, excludePhiOutEdges: true);
if (uses.isEmpty) return;
uses.replaceWith(_graph.addConstantBool(value, _closedWorld));
_log?.registerConditionValue(condition, value, tag, uses.length);
}
@override
HInstruction visitIf(HIf node) {
HInstruction condition = node.condition;
if (condition.isConstant()) return node;
AbstractBool isTruthy =
_abstractValueDomain.isTruthy(condition.instructionType);
if (isTruthy.isDefinitelyTrue) {
_metrics.countConditionDecided.add();
return _replaceHIfCondition(
node, _graph.addConstantBool(true, _closedWorld));
} else if (isTruthy.isDefinitelyFalse) {
_metrics.countConditionDecided.add();
return _replaceHIfCondition(
node, _graph.addConstantBool(false, _closedWorld));
}
HBasicBlock thenBlock = node.thenBlock;
HBasicBlock elseBlock = node.elseBlock;
// For diamond control flow, if the end of the then- or else-block is not
// reachable, the other block dynamically dominates the join, so the join
// acts as a continuation of the else- or then- branch.
HBasicBlock thenContinuation = null;
HBasicBlock elseContinuation = null;
if (node.joinBlock != null) {
final joinPredecessors = node.joinBlock.predecessors;
if (joinPredecessors.length == 2) {
if (hasUnreachableExit(joinPredecessors[0])) {
elseContinuation = node.joinBlock;
} else if (hasUnreachableExit(joinPredecessors[1])) {
thenContinuation = node.joinBlock;
}
}
}
simplifyCondition(thenBlock, condition, true, 'then');
simplifyCondition(thenContinuation, condition, true, 'then-join');
simplifyCondition(elseBlock, condition, false, 'else');
simplifyCondition(elseContinuation, condition, false, 'else-join');
if (condition is HNot) {
// if (!t1) ... t1 ...
HInstruction negated = condition.inputs[0];
simplifyCondition(thenBlock, negated, false, 'then');
simplifyCondition(thenContinuation, negated, false, 'then-join');
simplifyCondition(elseBlock, negated, true, 'else');
simplifyCondition(elseContinuation, negated, true, 'else-join');
} else {
// It is possible for LICM to move a negated version of the condition out
// of the loop where it used. We still want to simplify the nested use of
// the condition in that case, so we look for all dominating negated
// conditions and replace nested uses of them with true or false.
//
// t1 = ...
// t2 = !t1
// loop
// if (t1)
// t2 // replace with `false`
//
Iterable<HInstruction> dominating = condition.usedBy
.where((user) => user is HNot && user.dominates(node));
dominating.forEach((hoisted) {
simplifyCondition(thenBlock, hoisted, false, 'hoisted-then');
simplifyCondition(
thenContinuation, hoisted, false, 'hoisted-then-join');
simplifyCondition(elseBlock, hoisted, true, 'hoisted-else');
simplifyCondition(elseContinuation, hoisted, true, 'hoisted-else-join');
});
}
return node;
}
/// Returns [node] after replacing condition.
HInstruction _replaceHIfCondition(HIf node, HInstruction newCondition) {
node.replaceInput(0, newCondition);
return node;
}
@override
HInstruction visitPrimitiveCheck(HPrimitiveCheck node) {
if (node.isRedundant(_closedWorld)) return node.checkedInput;
return node;
}
@override
HInstruction visitBoolConversion(HBoolConversion node) {
if (node.isRedundant(_closedWorld)) return node.checkedInput;
return node;
}
@override
HInstruction visitNullCheck(HNullCheck node) {
if (node.isRedundant(_closedWorld)) return node.checkedInput;
return node;
}
@override
HInstruction visitLateReadCheck(HLateReadCheck node) {
if (node.isRedundant(_closedWorld)) return node.checkedInput;
return node;
}
@override
HInstruction visitLateWriteOnceCheck(HLateWriteOnceCheck node) {
if (node.isRedundant(_closedWorld)) return node.checkedInput;
return node;
}
@override
HInstruction visitLateInitializeOnceCheck(HLateInitializeOnceCheck node) {
if (node.isRedundant(_closedWorld)) return node.checkedInput;
return node;
}
@override
HInstruction visitTypeKnown(HTypeKnown node) {
if (node.isRedundant(_closedWorld)) return node.checkedInput;
return node;
}
@override
HInstruction visitFieldGet(HFieldGet node) {
var receiver = node.receiver;
// HFieldGet of a constructed constant can be replaced with the constant's
// field.
if (receiver is HConstant) {
ConstantValue constant = receiver.constant;
if (constant is ConstructedConstantValue) {
ConstantValue value = constant.fields[node.element];
if (value != null) {
_metrics.countFieldGetFolded.add();
return _graph.addConstant(value, _closedWorld);
}
}
}
return node;
}
@override
HInstruction visitGetLength(HGetLength node) {
HInstruction receiver = node.receiver;
if (receiver.isConstantList()) {
HConstant constantReceiver = receiver;
ListConstantValue constant = constantReceiver.constant;
_metrics.countGetLengthFolded.add();
return _graph.addConstantInt(constant.length, _closedWorld);
}
if (receiver.isConstantString()) {
HConstant constantReceiver = receiver;
StringConstantValue constant = constantReceiver.constant;
_metrics.countGetLengthFolded.add();
return _graph.addConstantInt(constant.length, _closedWorld);
}
AbstractValue receiverType = receiver.instructionType;
if (_abstractValueDomain.isContainer(receiverType)) {
int /*?*/ length = _abstractValueDomain.getContainerLength(receiverType);
if (length != null) {
HInstruction constant = _graph.addConstantInt(length, _closedWorld);
_metrics.countGetLengthFolded.add();
if (_abstractValueDomain.isNull(receiverType).isPotentiallyTrue) {
// If the container can be null, we update all uses of the length
// access to use the constant instead, but keep the length access in
// the graph, to ensure we still have a null check.
node.block.rewrite(node, constant);
return node;
}
return constant;
}
}
// Can we find the length as an input to an allocation?
HInstruction potentialAllocation = receiver;
if (receiver is HInvokeStatic &&
receiver.element == commonElements.setArrayType) {
// Look through `setArrayType(new Array(), ...)`
potentialAllocation = receiver.inputs.first;
}
if (_graph.allocatedFixedLists.contains(potentialAllocation)) {
// TODO(sra): How do we keep this working if we lower/inline the receiver
// in an optimization?
HInstruction lengthInput = potentialAllocation.inputs.first;
// We don't expect a non-integer first input to the fixed-size allocation,
// but checking the input is an integer ensures we do not replace a
// HGetlength with a reference to something with a type that will confuse
// bounds check eliminiation.
if (lengthInput.isInteger(_abstractValueDomain).isDefinitelyTrue) {
// TODO(sra). HGetLength may have a better type than [lengthInput] as
// the allocation may throw on an out-of-range input. Typically the
// input is an unconstrained `int` and the length is non-negative. We
// may have done some optimizations with the better type that we won't
// be able to do with the broader type of [lengthInput]. We should
// insert a HTypeKnown witnessed by the allocation to narrow the
// lengthInput.
_metrics.countGetLengthFolded.add();
return lengthInput;
}
}
if (node.isAssignable &&
isFixedLength(receiver.instructionType, _closedWorld)) {
// The input type has changed to fixed-length so change to an unassignable
// HGetLength to allow more GVN optimizations.
_metrics.countGetLengthFolded.add();
return HGetLength(receiver, node.instructionType, isAssignable: false);
}
return node;
}
@override
HInstruction visitIndex(HIndex node) {
HInstruction receiver = node.receiver;
if (receiver is HConstant) {
HInstruction index = node.index;
if (index is HConstant) {
ConstantValue foldedValue =
constant_system.index.fold(receiver.constant, index.constant);
if (foldedValue != null) {
_metrics.countIndexFolded.add();
return _graph.addConstant(foldedValue, _closedWorld);
}
}
}
return node;
}
/// Returns the guarded receiver.
HInstruction maybeGuardWithNullCheck(
HInstruction receiver, HInvokeDynamic node, FieldEntity /*?*/ field) {
AbstractValue receiverType = receiver.instructionType;
if (_abstractValueDomain.isNull(receiverType).isPotentiallyTrue) {
HNullCheck check =
HNullCheck(receiver, _abstractValueDomain.excludeNull(receiverType))
..selector = node.selector
..field = field
..sourceInformation = node.sourceInformation;
_log?.registerNullCheck(node, check);
node.block.addBefore(node, check);
return check;
}
return receiver;
}
@override
HInstruction visitInvokeDynamicGetter(HInvokeDynamicGetter node) {
propagateConstantValueToUses(node);
if (node.isInterceptedCall) {
HInstruction folded = handleInterceptedCall(node);
if (folded != node) return folded;
}
HInstruction receiver = node.getDartReceiver(_closedWorld);
AbstractValue receiverType = receiver.instructionType;
Selector selector = node.selector;
MemberEntity member =
node.element ?? _closedWorld.locateSingleMember(selector, receiverType);
if (member == null) return node;
if (member is FieldEntity) {
FieldEntity field = member;
FieldAnalysisData fieldData =
_closedWorld.fieldAnalysis.getFieldData(field);
if (fieldData.isEffectivelyConstant) {
// The field is elided and replace it with its constant value.
maybeGuardWithNullCheck(receiver, node, null);
ConstantValue constant = fieldData.constantValue;
HConstant result = _graph.addConstant(constant, _closedWorld,
sourceInformation: node.sourceInformation);
_metrics.countGettersElided.add();
_log?.registerConstantFieldGet(node, field, result);
return result;
} else {
receiver = maybeGuardWithNullCheck(receiver, node, field);
HFieldGet result = _directFieldGet(receiver, field, node);
_metrics.countGettersInlined.add();
_log?.registerFieldGet(node, result);
return result;
}
}
if (member is FunctionEntity) {
// 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 (member.isFunction && member.name == selector.name) {
// A property extraction getter, aka a tear-off.
node.element = member;
node.sideEffects.clearAllDependencies();
node.sideEffects.clearAllSideEffects();
node.setUseGvn(); // We don't care about identity of tear-offs.
}
return node;
}
HInstruction _directFieldGet(
HInstruction receiver, FieldEntity field, HInstruction node) {
bool isAssignable = !_closedWorld.fieldNeverChanges(field);
AbstractValue type;
if (_nativeData.isNativeClass(field.enclosingClass)) {
type = AbstractValueFactory.fromNativeBehavior(
_nativeData.getNativeFieldLoadBehavior(field), _closedWorld);
} else {
// TODO(johnniwinther): Use the potentially more precise type of the
// node + find a test that shows its usefulness.
// type = _abstractValueDomain.intersection(
// node.instructionType,
// AbstractValueFactory.inferredTypeForMember(
// field, _globalInferenceResults));
type = AbstractValueFactory.inferredTypeForMember(
field, _globalInferenceResults);
}
return HFieldGet(field, receiver, type, node.sourceInformation,
isAssignable: isAssignable);
}
@override
HInstruction visitInvokeDynamicSetter(HInvokeDynamicSetter node) {
if (node.isInterceptedCall) {
HInstruction folded = handleInterceptedCall(node);
if (folded != node) return folded;
}
HInstruction receiver = node.getDartReceiver(_closedWorld);
AbstractValue receiverType = receiver.instructionType;
MemberEntity member = node.element ??=
_closedWorld.locateSingleMember(node.selector, receiverType);
if (member == null) return node;
if (member is FieldEntity) {
FieldEntity field = member;
if (field == null || !field.isAssignable) return node;
// Use `node.inputs.last` in case the call follows the interceptor calling
// convention, but is not a call on an interceptor.
HInstruction value = node.inputs.last;
HInstruction assignField() {
if (_closedWorld.fieldAnalysis.getFieldData(field).isElided) {
_log?.registerFieldSet(node);
_metrics.countSettersElided.add();
return value;
} else {
HFieldSet result =
HFieldSet(_abstractValueDomain, field, receiver, value)
..sourceInformation = node.sourceInformation;
_log?.registerFieldSet(node, result);
_metrics.countSettersInlined.add();
return result;
}
}
if (!_closedWorld.annotationsData
.getParameterCheckPolicy(field)
.isEmitted) {
return assignField();
}
DartType fieldType = _closedWorld.elementEnvironment.getFieldType(field);
AbstractValueWithPrecision checkedType =
_abstractValueDomain.createFromStaticType(fieldType, nullable: true);
if (checkedType.isPrecise &&
_abstractValueDomain
.isIn(value.instructionType, checkedType.abstractValue)
.isDefinitelyTrue) {
return assignField();
}
// TODO(sra): Implement inlining of setters with checks for new rti. The
// check and field assignment for the setter should be inlined if this is
// the only call to the setter, or the current function already computes
// the type of the field.
node.needsCheck = true;
return node;
}
if (member is FunctionEntity) {
// If the member is not a setter is could be legacy `noSuchMethod`.
if (member.isSetter && member.name == node.selector.name) {
if (_nativeData.isNativeMember(member)) {
return tryInlineNativeSetter(node, member) ?? node;
}
}
}
return node;
}
@override
HInstruction visitInvokeClosure(HInvokeClosure node) {
HInstruction closure = node.getDartReceiver(_closedWorld);
// Replace indirect call to static method tear-off closure with direct call
// to static method.
if (closure is HConstant) {
ConstantValue constant = closure.constant;
if (constant is FunctionConstantValue) {
FunctionEntity target = constant.element;
ParameterStructure parameterStructure = target.parameterStructure;
if (parameterStructure.callStructure == node.selector.callStructure) {
// TODO(sra): Handle adding optional arguments default values.
assert(!node.isInterceptedCall);
return HInvokeStatic(target, node.inputs.skip(1).toList(),
node.instructionType, node.typeArguments)
..sourceInformation = node.sourceInformation;
}
}
}
return node;
}
@override
HInstruction visitInvokeStatic(HInvokeStatic node) {
propagateConstantValueToUses(node);
MemberEntity element = node.element;
if (element == commonElements.identicalFunction) {
if (node.inputs.length == 2) {
return HIdentity(
node.inputs[0], node.inputs[1], _abstractValueDomain.boolType)
..sourceInformation = node.sourceInformation;
}
} else if (element == commonElements.setArrayType) {
if (node.inputs.length == 2) {
return handleArrayTypeInfo(node);
}
} else if (commonElements.isCheckConcurrentModificationError(element)) {
if (node.inputs.length == 2) {
HInstruction firstArgument = node.inputs[0];
if (firstArgument is HConstant) {
HConstant constant = firstArgument;
if (constant.constant is TrueConstantValue) return constant;
}
}
} else if (commonElements.isCheckInt(element)) {
if (node.inputs.length == 1) {
HInstruction argument = node.inputs[0];
if (argument.isInteger(_abstractValueDomain).isDefinitelyTrue) {
return argument;
}
}
} else if (commonElements.isCheckNum(element)) {
if (node.inputs.length == 1) {
HInstruction argument = node.inputs[0];
if (argument.isNumber(_abstractValueDomain).isDefinitelyTrue) {
return argument;
}
}
} else if (commonElements.isCheckString(element)) {
if (node.inputs.length == 1) {
HInstruction argument = node.inputs[0];
if (argument.isString(_abstractValueDomain).isDefinitelyTrue) {
return argument;
}
}
} else if (element == commonElements.assertHelper ||
element == commonElements.assertTest) {
if (node.inputs.length == 1) {
HInstruction argument = node.inputs[0];
if (argument is HConstant) {
ConstantValue constant = argument.constant;
if (constant is BoolConstantValue) {
bool value = constant is TrueConstantValue;
if (element == commonElements.assertTest) {
// `assertTest(argument)` effectively negates the argument.
return _graph.addConstantBool(!value, _closedWorld);
}
// `assertHelper(true)` is a no-op, other values throw.
if (value) return argument;
}
}
}
}
// TODO(sra): [element] could be a native or js-interop method, in which
// case we could 'inline' the call to the Dart-convention wrapper code,
// replacing it with a HInvokeExternal instruction. Many of these static
// methods are already 'inlined' by the CFG builder.
return node;
}
HInstruction handleArrayTypeInfo(HInvokeStatic node) {
// If type information is not needed, use the raw Array.
HInstruction source = node.inputs[0];
if (source.usedBy.length != 1) return node;
if (source.isArray(_abstractValueDomain).isPotentiallyFalse) {
return node;
}
for (HInstruction user in node.usedBy) {
if (user is HGetLength) continue;
if (user is HIndex) continue;
// Bounds check escapes the array, but we don't care.
if (user is HBoundsCheck) continue;
// Interceptor only escapes the Array if array passed to an intercepted
// method.
if (user is HInterceptor) continue;
if (user is HInvokeStatic) {
MemberEntity element = user.element;
if (commonElements.isCheckConcurrentModificationError(element)) {
// CME check escapes the array, but we don't care.
continue;
}
}
if (user is HInvokeDynamicGetter) {
String name = user.selector.name;
// These getters don't use the Array type.
if (name == 'last') continue;
if (name == 'first') continue;
}
// TODO(sra): Implement a more general algorithm - many methods don't use
// the element type.
return node;
}
return source;
}
@override
HInstruction visitStringConcat(HStringConcat node) {
// Simplify string concat:
//
// "" + R -> R
// L + "" -> L
// "L" + "R" -> "LR"
// (prefix + "L") + "R" -> prefix + "LR"
//
StringConstantValue getString(HInstruction instruction) {
if (!instruction.isConstantString()) return null;
HConstant constant = instruction;
return constant.constant;
}
StringConstantValue leftString = getString(node.left);
if (leftString != null && leftString.stringValue.length == 0) {
return node.right;
}
StringConstantValue rightString = getString(node.right);
if (rightString == null) return node;
if (rightString.stringValue.length == 0) return node.left;
HInstruction prefix = null;
if (leftString == null) {
if (node.left is! HStringConcat) return node;
HStringConcat leftConcat = node.left;
// Don't undo CSE.
if (leftConcat.usedBy.length != 1) return node;
prefix = leftConcat.left;
leftString = getString(leftConcat.right);
if (leftString == null) return node;
}
if (leftString.stringValue.length + rightString.stringValue.length >
MAX_SHARED_CONSTANT_FOLDED_STRING_LENGTH) {
if (node.usedBy.length > 1) return node;
}
HInstruction folded = _graph.addConstant(
constant_system
.createString(leftString.stringValue + rightString.stringValue),
_closedWorld);
if (prefix == null) return folded;
return HStringConcat(prefix, folded, _abstractValueDomain.stringType);
}
@override
HInstruction visitStringify(HStringify node) {
HInstruction input = node.inputs[0];
if (input.isString(_abstractValueDomain).isDefinitelyTrue) {
return input;
}
HInstruction asString(String string) =>
_graph.addConstant(constant_system.createString(string), _closedWorld);
HInstruction tryConstant() {
if (!input.isConstant()) return null;
HConstant constant = input;
ConstantValue value = constant.constant;
if (value is IntConstantValue) {
// Only constant-fold int.toString() when Dart and JS results the same.
// TODO(18103): We should be able to remove this work-around when issue
// 18103 is resolved by providing the correct string.
if (!value.isUInt32()) return null;
return asString('${value.intValue}');
}
if (value is BoolConstantValue) {
return asString(value.boolValue ? 'true' : 'false');
}
if (value is NullConstantValue) {
return asString('null');
}
if (value is DoubleConstantValue) {
// TODO(sra): It seems unlikely that all dart2js host implementations
// produce exactly the same characters as all JavaScript targets.
return asString('${value.doubleValue}');
}
return null;
}
HInstruction tryToString() {
// If the `toString` method is guaranteed to return a string we can call
// it directly. Keep the stringifier for primitives (since they have fast
// path code in the stringifier) and for classes requiring interceptors
// (since SsaInstructionSimplifier runs after SsaSimplifyInterceptors).
if (input.isPrimitive(_abstractValueDomain).isPotentiallyTrue) {
return null;
}
if (input.isNull(_abstractValueDomain).isPotentiallyTrue) {
return null;
}
Selector selector = Selectors.toString_;
AbstractValue toStringType =
AbstractValueFactory.inferredResultTypeForSelector(
selector, input.instructionType, _globalInferenceResults);
if (_abstractValueDomain
.containsOnlyType(
toStringType, _closedWorld.commonElements.jsStringClass)
.isPotentiallyFalse) {
return null;
}
// All intercepted classes extend `Interceptor`, so if the receiver can't
// be a class extending `Interceptor` then it can be called directly.
if (_abstractValueDomain
.isInterceptor(input.instructionType)
.isDefinitelyFalse) {
List<HInstruction> inputs = [input, input]; // [interceptor, receiver].
HInstruction result = HInvokeDynamicMethod(
selector,
input.instructionType, // receiver type.
inputs,
toStringType,
const <DartType>[],
node.sourceInformation,
isIntercepted: true);
return result;
}
return null;
}
// For primitives we know the stringifier is effect-free and never throws.
if (input.isNumberOrNull(_abstractValueDomain).isDefinitelyTrue ||
input.isStringOrNull(_abstractValueDomain).isDefinitelyTrue ||
input.isBooleanOrNull(_abstractValueDomain).isDefinitelyTrue) {
node.setPure();
}
return tryConstant() ?? tryToString() ?? node;
}
@override
HInstruction visitOneShotInterceptor(HOneShotInterceptor node) {
throw StateError('Should not see HOneShotInterceptor in simplifier: $node');
}
@override
HInstruction visitTypeEval(HTypeEval node) {
HInstruction environment = node.inputs.single;
if (_typeRecipeDomain.isIdentity(node.typeExpression, node.envStructure)) {
return environment;
}
if (environment is HLoadType) {
TypeRecipe result = _typeRecipeDomain.foldLoadEval(
environment.typeExpression, node.envStructure, node.typeExpression);
if (result != null) return HLoadType(result, node.instructionType);
return node;
}
if (environment is HTypeBind) {
HInstruction bindings = environment.inputs.last;
if (bindings is HLoadType) {
// env.bind(LoadType(T)).eval(...1...) --> env.eval(...T...)
TypeRecipeAndEnvironmentStructure result =
_typeRecipeDomain.foldBindLoadEval(bindings.typeExpression,
node.envStructure, node.typeExpression);
if (result != null) {
HInstruction previousEnvironment = environment.inputs.first;
return HTypeEval(previousEnvironment, result.environmentStructure,
result.recipe, node.instructionType);
}
}
// TODO(sra): LoadType(T).bind(E).eval(...1...) --> E.eval(...0...)
return node;
}
if (environment is HTypeEval) {
TypeRecipeAndEnvironmentStructure result = _typeRecipeDomain.foldEvalEval(
environment.envStructure,
environment.typeExpression,
node.envStructure,
node.typeExpression);
if (result != null) {
HInstruction previousEnvironment = environment.inputs.first;
return HTypeEval(previousEnvironment, result.environmentStructure,
result.recipe, node.instructionType);
}
return node;
}
if (environment is HInstanceEnvironment) {
HInstruction instance = environment.inputs.single;
AbstractValue instanceAbstractValue = instance.instructionType;
ClassEntity instanceClass;
// All the subclasses of JSArray are JSArray at runtime.
ClassEntity jsArrayClass = _closedWorld.commonElements.jsArrayClass;
if (_abstractValueDomain
.isInstanceOf(instanceAbstractValue, jsArrayClass)
.isDefinitelyTrue) {
instanceClass = jsArrayClass;
} else {
instanceClass =
_abstractValueDomain.getExactClass(instanceAbstractValue);
}
if (instanceClass != null) {
if (_typeRecipeDomain.isReconstruction(
instanceClass, node.envStructure, node.typeExpression)) {
return environment;
}
}
}
return node;
}
@override
HInstruction visitTypeBind(HTypeBind node) {
// TODO(sra): env1.eval(X).bind(env1.eval(Y)) --> env1.eval(...X...Y...)
return node;
}
@override
HInstruction visitAsCheck(HAsCheck node) {
// TODO(fishythefish): Correctly constant fold `null as T` (also in
// [visitAsCheckSimple]) when running with sound null safety. We might get
// this for free if nullability is precisely propagated to the typemasks.
HInstruction typeInput = node.typeInput;
if (typeInput is HLoadType) {
TypeExpressionRecipe recipe = typeInput.typeExpression;
node.checkedTypeExpression = recipe.type;
}
if (node.isRedundant(_closedWorld, _options)) {
return node.checkedInput;
}
// See if this check can be lowered to a simple one.
MemberEntity specializedCheck = SpecializedChecks.findAsCheck(
node.checkedTypeExpression,
_closedWorld.commonElements,
_options.useLegacySubtyping);
if (specializedCheck != null) {
AbstractValueWithPrecision checkedType = _abstractValueDomain
.createFromStaticType(node.checkedTypeExpression, nullable: true);
return HAsCheckSimple(
node.checkedInput,
node.checkedTypeExpression,
checkedType,
node.isTypeError,
specializedCheck,
node.instructionType);
}
return node;
}
@override
HInstruction visitAsCheckSimple(HAsCheckSimple node) {
if (node.isRedundant(_closedWorld, _options)) {
return node.checkedInput;
}
return node;
}
@override
HInstruction visitIsTest(HIsTest node) {
HInstruction typeInput = node.typeInput;
if (typeInput is HLoadType) {
TypeExpressionRecipe recipe = typeInput.typeExpression;
node.dartType = recipe.type;
}
AbstractBool result = node.evaluate(_closedWorld, _options);
if (result.isDefinitelyFalse) {
_metrics.countIsTestDecided.add();
return _graph.addConstantBool(false, _closedWorld);
}
if (result.isDefinitelyTrue) {
_metrics.countIsTestDecided.add();
return _graph.addConstantBool(true, _closedWorld);
}
IsTestSpecialization specialization =
SpecializedChecks.findIsTestSpecialization(
node.dartType, _graph.element, _closedWorld);
if (specialization == IsTestSpecialization.isNull ||
specialization == IsTestSpecialization.notNull) {
HInstruction nullTest = HIdentity(node.checkedInput,
_graph.addConstantNull(_closedWorld), _abstractValueDomain.boolType);
if (specialization == IsTestSpecialization.isNull) return nullTest;
nullTest.sourceInformation = node.sourceInformation;
node.block.addBefore(node, nullTest);
return HNot(nullTest, _abstractValueDomain.boolType);
}
if (specialization != null) {
AbstractValueWithPrecision checkedType = _abstractValueDomain
.createFromStaticType(node.dartType, nullable: false);
_metrics.countIsTestSimplified.add();
return HIsTestSimple(node.dartType, checkedType, specialization,
node.checkedInput, _abstractValueDomain.boolType);
}
// TODO(fishythefish): Prune now-unneeded is-tests from the metadata.
return node;
}
@override
HInstruction visitIsTestSimple(HIsTestSimple node) {
AbstractBool result = node.evaluate(_closedWorld, _options);
if (result.isDefinitelyFalse) {
_metrics.countIsTestDecided.add();
return _graph.addConstantBool(false, _closedWorld);
}
if (result.isDefinitelyTrue) {
_metrics.countIsTestDecided.add();
return _graph.addConstantBool(true, _closedWorld);
}
return node;
}
@override
HInstruction visitInstanceEnvironment(HInstanceEnvironment node) {
HInstruction instance = node.inputs.single;
// Store-forward instance types of created instances and constant instances.
//
// Forwarding the type might cause the instance (HCreate, constant etc) to
// become dead. This might cause us to lose track of that fact that there
// are type expressions from within the instance's class scope, so breaking
// the algorithm for generating the per-type runtime type information. The
// fix is to register the classes as created here in case the instance
// becomes dead.
//
// TODO(sra): It would be cleaner to track on HLoadType, HTypeEval, etc
// which class scope(s) they originated from. If the type expressions become
// dead, the references to the scope type variables become dead.
if (instance is HCreate) {
if (instance.hasRtiInput) {
instance.instantiatedTypes?.forEach(_registry.registerInstantiation);
return instance.rtiInput;
}
InterfaceType instanceType =
_closedWorld.elementEnvironment.getThisType(instance.element);
if (instanceType.typeArguments.length == 0) {
instance.instantiatedTypes?.forEach(_registry.registerInstantiation);
return HLoadType.type(instanceType, instance.instructionType);
}
return node;
}
if (instance is HConstant) {
ConstantValue constantValue = instance.constant;
if (constantValue is ConstructedConstantValue) {
_registry.registerInstantiation(constantValue.type);
return HLoadType.type(constantValue.type, instance.instructionType);
}
if (constantValue is ListConstantValue) {
InterfaceType type = constantValue.type;
_registry.registerInstantiation(type);
return HLoadType.type(type, instance.instructionType);
}
return node;
}
if (instance is HInvokeStatic &&
instance.element == commonElements.setArrayType) {
// TODO(sra): What is the 'instantiated type' we should be registering as
// discussed above? Perhaps it should be carried on HLiteralList.
return instance.inputs.last;
}
return node;
}
@override
HInstruction visitBitAnd(HBitAnd node) {
HInstruction left = node.left;
HInstruction right = node.right;
if (left is HConstant) {
if (right is HConstant) {
return foldBinary(node.operation(), left, right) ?? node;
}
// c1 & a --> a & c1
return HBitAnd(right, left, node.instructionType);
}
if (right is HConstant) {
ConstantValue constant = right.constant;
if (constant.isZero) return right; // a & 0 --> 0
if (_isFull32BitMask(constant)) {
if (left.isUInt32(_abstractValueDomain).isDefinitelyTrue) {
// Mask of all '1's has no effect.
return left;
// TODO(sra): A more advanced version of this would be to see if the
// input might have any bits that would be cleared by the mask. Thus
// `a >> 24 & 255` is a no-op since `a >> 24` can have only the low 8
// bits non-zero. If a bit is must-zero, we can remove it from
// mask. e.g. if a is Uint31, then `a >> 24 & 0xF0` becomes `a >> 24 &
// 0x70`.
}
// Ensure that the result is still canonicalized to an unsigned 32-bit
// integer using `left >>> 0`. This shift is often removed or combined
// in subsequent optimization.
HConstant zero = _graph.addConstantInt(0, _closedWorld);
return HShiftRight(left, zero, node.instructionType);
}
if (left is HBitAnd && left.usedBy.length == 1) {
HInstruction operand1 = left.left;
HInstruction operand2 = left.right;
if (operand2 is HConstant) {
// (a & c1) & c2 --> a & (c1 & c2)
HInstruction folded =
foldBinary(constant_system.bitAnd, operand2, right);
if (folded == null) return node;
return HBitAnd(operand1, folded, node.instructionType);
}
// TODO(sra): We don't rewrite (a & c1) & b --> (a & b) & c1. I suspect
// that the JavaScript VM might benefit from reducing the value early
// (e.g. to a 'Smi'). We could do that, but we should also consider a
// variation of the above rule where:
//
// a & c1 & ... & b & c2 --> a & (c1 & c2) & ... & b
//
// This would probably be best deferred until after GVN in case (a & c1)
// is reused.
}
}
return node;
}
bool _isFull32BitMask(ConstantValue constant) {
return constant is IntConstantValue && constant.intValue == _mask32;
}
static final _mask32 = BigInt.parse('FFFFFFFF', radix: 16);
@override
HInstruction visitBitOr(HBitOr node) {
HInstruction left = node.left;
HInstruction right = node.right;
if (left is HConstant) {
if (right is HConstant) {
return foldBinary(node.operation(), left, right) ?? node;
}
// c1 | a --> a | c1
return HBitOr(right, left, node.instructionType);
}
// (a | b) | c
if (left is HBitOr && left.usedBy.length == 1) {
HInstruction operand1 = left.left;
HInstruction operand2 = left.right;
if (operand2 is HConstant) {
if (right is HConstant) {
// (a | c1) | c2 --> a | (c1 | c2)
HInstruction folded =
foldBinary(constant_system.bitOr, operand2, right);
if (folded == null) return node;
return HBitOr(operand1, folded, node.instructionType);
} else {
// (a | c1) | b --> (a | b) | c1
HInstruction or1 = _makeBitOr(operand1, right)
..sourceInformation = left.sourceInformation;
node.block.addBefore(node, or1);
// TODO(sra): Restart simplification at 'or1'.
return _makeBitOr(or1, operand2);
}
}
}
// (a & c1) | (a & c2) --> a & (c1 | c2)
if (left is HBitAnd &&
left.usedBy.length == 1 &&
right is HBitAnd &&
right.usedBy.length == 1) {
HInstruction a1 = left.left;
HInstruction a2 = right.left;
if (a1 == a2) {
HInstruction c1 = left.right;
HInstruction c2 = right.right;
if (c1 is HConstant && c2 is HConstant) {
HInstruction folded = foldBinary(constant_system.bitOr, c1, c2);
if (folded != null) {
return HBitAnd(a1, folded, node.instructionType);
}
}
}
}
// TODO(sra):
//
// (e | (a & c1)) | (a & c2) --> e | (a & (c1 | c2))
return node;
}
HBitOr _makeBitOr(HInstruction operand1, HInstruction operand2) {
AbstractValue instructionType = _abstractValueDomainBitOr(
operand1.instructionType, operand2.instructionType);
return HBitOr(operand1, operand2, instructionType);
}
// TODO(sra): Use a common definition of primitive operations in the
// AbstractValueDomain.
AbstractValue _abstractValueDomainBitOr(AbstractValue a, AbstractValue b) {
return (_abstractValueDomain.isUInt31(a).isDefinitelyTrue &&
_abstractValueDomain.isUInt31(b).isDefinitelyTrue)
? _abstractValueDomain.uint31Type
: _abstractValueDomain.uint32Type;
}
@override
HInstruction visitShiftRight(HShiftRight node) {
HInstruction left = node.left;
HInstruction count = node.right;
if (count is HConstant) {
if (left is HConstant) {
return foldBinary(node.operation(), left, count) ?? node;
}
ConstantValue countValue = count.constant;
// Shift by zero can convert to 32 bit unsigned, so remove only if no-op.
// a >> 0 --> a
if (countValue.isZero &&
left.isUInt32(_abstractValueDomain).isDefinitelyTrue) {
return left;
}
if (left is HBitAnd && left.usedBy.length == 1) {
// TODO(sra): Should this be postponed to after GVN?
HInstruction operand = left.left;
HInstruction mask = left.right;
if (mask is HConstant) {
// Reduce mask constant size.
// (a & mask) >> count --> (a >> count) & (mask >> count)
ConstantValue maskValue = mask.constant;
ConstantValue shiftedMask =
constant_system.shiftRight.fold(maskValue, countValue);
if (shiftedMask is IntConstantValue && shiftedMask.isUInt32()) {
// TODO(sra): The shift type should be available from the abstract
// value domain.
AbstractValue shiftType = shiftedMask.isZero
? _abstractValueDomain.uint32Type
: _abstractValueDomain.uint31Type;
var shift = HShiftRight(operand, count, shiftType)
..sourceInformation = node.sourceInformation;
node.block.addBefore(node, shift);
HConstant shiftedMaskInstruction =
_graph.addConstant(shiftedMask, _closedWorld);
return HBitAnd(shift, shiftedMaskInstruction, node.instructionType);
}
}
return node;
}
}
return node;
}
@override
HInstruction visitShiftLeft(HShiftLeft node) {
HInstruction left = node.left;
HInstruction count = node.right;
if (count is HConstant) {
if (left is HConstant) {
return foldBinary(node.operation(), left, count) ?? node;
}
// Shift by zero can convert to 32 bit unsigned, so remove only if no-op.
// a << 0 --> a
if (count.constant.isZero &&
left.isUInt32(_abstractValueDomain).isDefinitelyTrue) {
return left;
}
// Shift-mask-unshift reduction.
// ((a >> c1) & c2) << c1 --> a & (c2 << c1);
if (left is HBitAnd && left.usedBy.length == 1) {
HInstruction operand1 = left.left;
HInstruction operand2 = left.right;
if (operand2 is HConstant) {
if (operand1 is HShiftRight && operand1.usedBy.length == 1) {
HInstruction a = operand1.left;
HInstruction count2 = operand1.right;
if (count2 == count) {
HInstruction folded =
foldBinary(constant_system.shiftLeft, operand2, count);
if (folded != null) {
return HBitAnd(a, folded, node.instructionType);
}
}
}
}
}
}
return node;
}
}
class SsaDeadCodeEliminator extends HGraphVisitor implements OptimizationPhase {
@override
final String name = "SsaDeadCodeEliminator";
final JClosedWorld closedWorld;
final SsaOptimizerTask optimizer;
HGraph _graph;
Map<HInstruction, bool> trivialDeadStoreReceivers = Maplet();
bool eliminatedSideEffects = false;
bool newGvnCandidates = false;
SsaDeadCodeEliminator(this.closedWorld, this.optimizer);
AbstractValueDomain get _abstractValueDomain =>
closedWorld.abstractValueDomain;
// A constant with no type does not pollute types at phi nodes.
HInstruction _zapInstruction;
HInstruction get zapInstruction =>
_zapInstruction ??= _graph.addConstantUnreachable(closedWorld);
/// Determines whether we can delete [instruction] because the only thing it
/// does is throw the same exception as the next instruction that throws or
/// has an effect.
bool canFoldIntoFollowingInstruction(HInstruction instruction) {
assert(instruction.usedBy.isEmpty);
assert(instruction.canThrow(_abstractValueDomain));
if (!instruction.onlyThrowsNSM()) return false;
HInstruction receiver = instruction.getDartReceiver(closedWorld);
HInstruction current = instruction.next;
do {
if ((current.getDartReceiver(closedWorld) == receiver) &&
current.canThrow(_abstractValueDomain)) {
return true;
}
if (current is HForeignCode && current.isNullGuardFor(receiver)) {
return true;
}
if (current.canThrow(_abstractValueDomain) ||
current.sideEffects.hasSideEffects()) {
return false;
}
HInstruction next = current.next;
if (next == null) {
// We do not merge blocks in our SSA graph, so if this block just jumps
// to a single successor, visit the successor, avoiding back-edges.
HBasicBlock successor;
if (current is HGoto) {
successor = current.block.successors.single;
} else if (current is HIf) {
// We also leave HIf nodes in place when one branch is dead.
HInstruction condition = current.inputs.first;
if (condition is HConstant) {
successor = condition.constant is TrueConstantValue
? current.thenBlock
: current.elseBlock;
assert(successor.isLive);
}
}
if (successor != null && successor.id > current.block.id) {
next = successor.first;
}
}
current = next;
} while (current != null);
return false;
}
bool isTrivialDeadStoreReceiver(HInstruction instruction) {
// For an allocation, if all the loads are dead (awaiting removal after
// SsaLoadElimination) and the only other uses are stores, then the
// allocation does not escape which makes all the stores dead too.
bool isDeadUse(HInstruction use) {
if (use is HFieldSet) {
// The use must be the receiver. Even if the use is also the argument,
// i.e. a.x = a, the store is still dead if all other uses are dead.
if (use.getDartReceiver(closedWorld) == instruction) return true;
} else if (use is HFieldGet) {
assert(use.getDartReceiver(closedWorld) == instruction);
if (isDeadCode(use)) return true;
}
return false;
}
return instruction.isAllocation(_abstractValueDomain) &&
instruction.isPure(_abstractValueDomain) &&
trivialDeadStoreReceivers.putIfAbsent(
instruction, () => instruction.usedBy.every(isDeadUse));
}
bool isTrivialDeadStore(HInstruction instruction) {
return instruction is HFieldSet &&
isTrivialDeadStoreReceiver(instruction.getDartReceiver(closedWorld));
}
bool isDeadCode(HInstruction instruction) {
if (!instruction.usedBy.isEmpty) return false;
if (isTrivialDeadStore(instruction)) return true;
if (instruction.sideEffects.hasSideEffects()) return false;
if (instruction.canThrow(_abstractValueDomain)) {
if (canFoldIntoFollowingInstruction(instruction)) {
// TODO(35996): If we remove [instruction], the source location of the
// 'equivalent' instruction should be updated.
return true;
}
return false;
}
if (instruction is HParameterValue) return false;
if (instruction is HLocalSet) return false;
return true;
}
@override
void visitGraph(HGraph graph) {
_graph = graph;
_zapInstruction = null;
_computeLiveness();
visitPostDominatorTree(graph);
}
void _computeLiveness() {
var analyzer = SsaLiveBlockAnalyzer(_graph, closedWorld, optimizer);
analyzer.analyze();
for (HBasicBlock block in _graph.blocks) {
block.isLive = analyzer.isLiveBlock(block);
}
}
@override
bool validPostcondition(HGraph graph) {
return NoUnusedPhiValidator.containsNoUnusedPhis(graph);
}
@override
void visitBasicBlock(HBasicBlock block) {
simplifyControlFlow(block);
// Start from the last non-control flow instruction in the block.
HInstruction instruction = block.last.previous;
while (instruction != null) {
var previous = instruction.previous;
if (!block.isLive) {
eliminatedSideEffects =
eliminatedSideEffects || instruction.sideEffects.hasSideEffects();
removeUsers(instruction);
block.remove(instruction);
} else if (isDeadCode(instruction)) {
block.remove(instruction);
}
instruction = previous;
}
block.forEachPhi(simplifyPhi);
evacuateTakenBranch(block);
}
void simplifyPhi(HPhi phi) {
// Remove an unused HPhi so that the inputs can become potentially dead.
if (!phi.block.isLive) {
removeUsers(phi);
}
if (phi.usedBy.isEmpty) {
phi.block.removePhi(phi);
return;
}
// Run through the phis of the block and replace them with their input that
// comes from the only live predecessor if that dominates the phi.
//
// TODO(sra): If the input is directly in the only live predecessor, it
// might be possible to move it into [block] (e.g. all its inputs are
// dominating.)
// Find the index of the single live predecessor if it exists.
List<HBasicBlock> predecessors = phi.block.predecessors;
int indexOfLive = -1;
for (int i = 0; i < predecessors.length; i++) {
if (predecessors[i].isLive) {
if (indexOfLive >= 0) {
indexOfLive = -1;
break;
}
indexOfLive = i;
}
}
if (indexOfLive >= 0) {
HInstruction replacement = phi.inputs[indexOfLive];
if (replacement.dominates(phi)) {
phi.block.rewrite(phi, replacement);
phi.block.removePhi(phi);
if (replacement.sourceElement == null &&
phi.sourceElement != null &&
replacement is! HThis) {
replacement.sourceElement = phi.sourceElement;
}
return;
}
}
// If the phi is of the form `phi(x, HTypeKnown(x))`, it does not strengthen
// `x`. We can replace the phi with `x` to potentially make the HTypeKnown
// refinement node dead and potentially make a HIf control no HPhis.
// TODO(sra): Implement version of this test that works for a subgraph of
// HPhi nodes.
HInstruction base = null;
bool seenUnrefinedBase = false;
for (HInstruction input in phi.inputs) {
HInstruction value = input;
while (value is HTypeKnown) {
HTypeKnown refinement = value;
value = refinement.checkedInput;
}
if (value == input) seenUnrefinedBase = true;
base ??= value;
if (base != value) return;
}
if (seenUnrefinedBase) {
HBasicBlock block = phi.block;
block.rewrite(phi, base);
block.removePhi(phi);
}
}
void simplifyControlFlow(HBasicBlock block) {
HControlFlow instruction = block.last;
if (instruction is HIf) {
HInstruction condition = instruction.condition;
if (condition.isConstant()) return;
// We want to remove an if-then-else diamond when the then- and else-
// branches are empty and the condition does not control a HPhi. We cannot
// change the CFG structure so we replace the HIf condition with a
// constant. This may leave the original condition unused. i.e. a
// candidate for being dead code.
List<HBasicBlock> dominated = block.dominatedBlocks;
// Diamond-like control flow dominates the then-, else- and join- blocks.
if (dominated.length != 3) return;
HBasicBlock join = dominated.last;
if (!join.phis.isEmpty) return; // condition controls a phi.
// Ignore exit block - usually the join in `if (...) return ...`
if (join.isExitBlock()) return;
int thenSize = measureEmptyInterval(instruction.thenBlock, join);
if (thenSize == null) return;
int elseSize = measureEmptyInterval(instruction.elseBlock, join);
if (elseSize == null) return;
// Pick the 'live' branch to be the smallest subgraph.
bool value = thenSize <= elseSize;
HInstruction newCondition = _graph.addConstantBool(value, closedWorld);
instruction.replaceInput(0, newCondition);
}
}
/// Returns the number of blocks from [start] up to but not including [end].
/// Returns `null` if any of the blocks is non-empty (other than control
/// flow). Returns `null` if there is an exit from the region other than
/// [end] or via control flow other than HGoto and HIf.
int measureEmptyInterval(HBasicBlock start, HBasicBlock end) {
if (start.first != start.last) return null; // start is not empty.
// Do a simple single-block region test first.
if (start.last is HGoto &&
start.successors.length == 1 &&
start.successors.single == end) {
return 1;
}
// TODO(sra): Implement fuller test.
return null;
}
/// If [block] is an always-taken branch, move the code from the taken branch
/// into [block]. This has the effect of making the instructions available for
/// further optimizations by moving them to a position that dominates the join
/// point of the if-then-else.
// TODO(29475): Delete dead blocks instead.
void evacuateTakenBranch(HBasicBlock block) {
if (!block.isLive) return;
HControlFlow branch = block.last;
if (branch is HIf) {
if (branch.thenBlock.isLive == branch.elseBlock.isLive) return;
assert(branch.condition.isConstant());
HBasicBlock liveSuccessor =
branch.thenBlock.isLive ? branch.thenBlock : branch.elseBlock;
HInstruction instruction = liveSuccessor.first;
// Move instructions up until the final control flow instruction or pinned
// HTypeKnown.
while (instruction.next != null) {
if (instruction is HTypeKnown && instruction.isPinned) break;
HInstruction next = instruction.next;
// It might be worth re-running GVN optimizations if we hoisted a
// GVN-able instructions from [target] into [block].
newGvnCandidates = newGvnCandidates || instruction.useGvn();
instruction.block.detach(instruction);
block.moveAtExit(instruction);
instruction = next;
}
}
}
void removeUsers(HInstruction instruction) {
instruction.usedBy.forEach((user) {
removeInput(user, instruction);
});
instruction.usedBy.clear();
}
void removeInput(HInstruction user, HInstruction input) {
List<HInstruction> inputs = user.inputs;
for (int i = 0, length = inputs.length; i < length; i++) {
if (input == inputs[i]) {
user.inputs[i] = zapInstruction;
zapInstruction.usedBy.add(user);
}
}
}
}
class SsaLiveBlockAnalyzer extends HBaseVisitor {
final HGraph graph;
final Set<HBasicBlock> live = {};
final List<HBasicBlock> worklist = [];
final SsaOptimizerTask optimizer;
final JClosedWorld closedWorld;
SsaLiveBlockAnalyzer(this.graph, this.closedWorld, this.optimizer);
AbstractValueDomain get _abstractValueDomain =>
closedWorld.abstractValueDomain;
Map<HInstruction, Range> get ranges => optimizer.ranges;
bool isLiveBlock(HBasicBlock block) => live.contains(block);
void analyze() {
markBlockLive(graph.entry);
while (!worklist.isEmpty) {
HBasicBlock live = worklist.removeLast();
live.last.accept(this);
}
}
void markBlockLive(HBasicBlock block) {
if (!live.contains(block)) {
worklist.add(block);
live.add(block);
}
}
@override
void visitControlFlow(HControlFlow instruction) {
instruction.block.successors.forEach(markBlockLive);
}
@override
void visitIf(HIf instruction) {
HInstruction condition = instruction.condition;
if (condition.isConstant()) {
if (condition.isConstantTrue()) {
markBlockLive(instruction.thenBlock);
} else {
markBlockLive(instruction.elseBlock);
}
} else {
visitControlFlow(instruction);
}
}
@override
void visitSwitch(HSwitch node) {
if (node.expression.isInteger(_abstractValueDomain).isDefinitelyTrue) {
Range switchRange = ranges[node.expression];
if (switchRange != null &&
switchRange.lower is IntValue &&
switchRange.upper is IntValue) {
IntValue lowerValue = switchRange.lower;
IntValue upperValue = switchRange.upper;
BigInt lower = lowerValue.value;
BigInt upper = upperValue.value;
Set<BigInt> liveLabels = {};
for (int pos = 1; pos < node.inputs.length; pos++) {
HConstant input = node.inputs[pos];
if (!input.isConstantInteger()) continue;
IntConstantValue constant = input.constant;
BigInt label = constant.intValue;
if (!liveLabels.contains(label) && label <= upper && label >= lower) {
markBlockLive(node.block.successors[pos - 1]);
liveLabels.add(label);
}
}
if (BigInt.from(liveLabels.length) != upper - lower + BigInt.one) {
markBlockLive(node.defaultTarget);
}
return;
}
}
visitControlFlow(node);
}
}
class SsaDeadPhiEliminator implements OptimizationPhase {
@override
final String name = "SsaDeadPhiEliminator";
@override
void visitGraph(HGraph graph) {
final List<HPhi> worklist = [];
// A set to keep track of the live phis that we found.
final Set<HPhi> livePhis = {};
// Add to the worklist all live phis: phis referenced by non-phi
// instructions.
for (final block in graph.blocks) {
block.forEachPhi((HPhi phi) {
for (final user in phi.usedBy) {
if (user is! HPhi) {
worklist.add(phi);
livePhis.add(phi);
break;
}
}
});
}
// Process the worklist by propagating liveness to phi inputs.
while (!worklist.isEmpty) {
HPhi phi = worklist.removeLast();
for (final input in phi.inputs) {
if (input is HPhi && !livePhis.contains(input)) {
worklist.add(input);
livePhis.add(input);
}
}
}
// Collect dead phis.
List<HPhi> deadPhis = [];
for (final block in graph.blocks) {
block.forEachPhi((phi) {
if (!livePhis.contains(phi)) deadPhis.add(phi);
});
}
// Two-phase removal, phase 1: unlink all the input nodes.
for (final phi in deadPhis) {