| // 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. |
| |
| import 'package:js_runtime/synced/array_flags.dart' show ArrayFlags; |
| |
| 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 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/codegen_inputs.dart' show CodegenInputs; |
| import '../js_backend/native_data.dart' show NativeData; |
| import '../js_model/js_world.dart' show JClosedWorld; |
| import '../js_model/type_recipe.dart' |
| show |
| TypeEnvironmentStructure, |
| TypeExpressionRecipe, |
| TypeRecipeDomain, |
| TypeRecipeDomainImpl; |
| import '../js_backend/specialized_checks.dart'; |
| import '../native/behavior.dart'; |
| import '../options.dart'; |
| import '../universe/call_structure.dart'; |
| import '../universe/selector.dart' show Selector; |
| import '../universe/side_effects.dart' show SideEffects; |
| import '../universe/use.dart' show StaticUse; |
| import '../util/bitset.dart'; |
| import '../util/util.dart'; |
| 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 = {}; |
| |
| final Map<MemberEntity, OptimizationTestLog> loggersForTesting = {}; |
| |
| SsaOptimizerTask(super.measurer, this._options); |
| |
| @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 satisfy phase postcondition after ${phase.name}', |
| ); |
| } |
| |
| SsaCodeMotion codeMotion; |
| SsaLoadElimination loadElimination; |
| |
| TypeRecipeDomain typeRecipeDomain = TypeRecipeDomainImpl( |
| closedWorld.dartTypes, |
| ); |
| |
| OptimizationTestLog? log; |
| if (retainDataForTesting) { |
| log = |
| loggersForTesting[member] = 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, |
| beforeTypePropagation: true, |
| ), |
| 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, codegen.tracer), |
| // 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, codegen.tracer), |
| 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; |
| final last = block.last; |
| if (last is HGoto) { |
| final 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<HInstruction> |
| 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 maxSharedConstantFoldedStringLength = 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; |
| |
| /// Most simplifications become enabled when the types are refined by type |
| /// propagation. Some simplifications remove code that helps type progagation |
| /// produce a better result. These simplifications are inhibited when |
| /// [beforeTypePropagation] is `true` to ensure they are seeing the propagated |
| /// types. |
| final bool beforeTypePropagation; |
| late final HGraph _graph; |
| |
| SsaInstructionSimplifier( |
| this._globalInferenceResults, |
| this._options, |
| this._closedWorld, |
| this._typeRecipeDomain, |
| this._registry, |
| this._log, |
| this._metrics, { |
| this.beforeTypePropagation = false, |
| }); |
| |
| JCommonElements get commonElements => _closedWorld.commonElements; |
| |
| AbstractValueDomain get _abstractValueDomain => |
| _closedWorld.abstractValueDomain; |
| |
| NativeData get _nativeData => _closedWorld.nativeData; |
| |
| @override |
| void visitGraph(HGraph visitee) { |
| _graph = visitee; |
| visitDominatorTree(visitee); |
| finalizeArrayFlagEffects(); |
| } |
| |
| @override |
| bool validPostcondition(HGraph graph) => true; |
| |
| @override |
| void visitBasicBlock(HBasicBlock node) { |
| simplifyPhis(node); |
| HInstruction? instruction = node.first; |
| while (instruction != null) { |
| HInstruction? next = instruction.next; |
| HInstruction replacement = instruction.accept(this); |
| if (replacement != instruction) { |
| node.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. |
| node.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]; |
| final 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. |
| void simplifyPhis(HBasicBlock block) { |
| // Do 'statement' simplifications first, as they might reduce the number of |
| // phis to one, enabling an 'expression' simplification. |
| var phi = block.phis.firstPhi; |
| while (phi != null) { |
| final next = phi.nextPhi; |
| simplifyStatementPhi(block, phi); |
| phi = next; |
| } |
| |
| if (block.predecessors.length != 2) return; |
| phi = block.phis.firstPhi; |
| if (phi != null && phi.next == null) { |
| simplifyExpressionPhi(block, phi); |
| } |
| } |
| |
| /// Simplify a single phi when there are possibly other phis (i.e. the result |
| /// might not be an expression). |
| void simplifyStatementPhi(HBasicBlock block, HPhi phi) { |
| if (simplifyStatementPhiToCommonInput(block, phi)) return; |
| |
| if (block.predecessors.length != 2) return; |
| |
| HBasicBlock dominator = block.dominator!; |
| |
| // Extract the controlling condition. |
| final controlFlow = dominator.last; |
| if (controlFlow is! HIf) return; |
| HInstruction condition = controlFlow.inputs.single; |
| |
| if (condition.isBoolean(_abstractValueDomain).isPotentiallyFalse) return; |
| |
| // For the condition to be 'controlling', there must be no way to reach the |
| // 'else' join from the 'then' branch and vice versa. |
| if (!dominator.successors[0].dominates(block.predecessors[0])) return; |
| if (!dominator.successors[1].dominates(block.predecessors[1])) return; |
| |
| // condition ? true : false --> condition |
| // condition ? condition : false --> condition |
| // condition ? true : condition --> condition |
| final left = phi.inputs[0]; |
| final right = phi.inputs[1]; |
| if ((_isBoolConstant(left, true) || left == condition) && |
| (_isBoolConstant(right, false) || right == condition)) { |
| block.rewrite(phi, condition); |
| block.removePhi(phi); |
| condition.sourceElement ??= phi.sourceElement; |
| return; |
| } |
| |
| // condition ? false : true --> !condition |
| if (_isBoolConstant(left, false) && _isBoolConstant(right, true)) { |
| HInstruction replacement = |
| HNot(condition, _abstractValueDomain.boolType) |
| ..sourceElement = phi.sourceElement |
| ..sourceInformation = phi.sourceInformation; |
| block.addAtEntry(replacement); |
| block.rewrite(phi, replacement); |
| block.removePhi(phi); |
| return; |
| } |
| } |
| |
| bool simplifyStatementPhiToCommonInput(HBasicBlock block, HPhi phi) { |
| // Replace phis that produce the same value on all arms. The test(s) for |
| // control flow often results in a refinement instruction (HTypeKnown), so |
| // we recognize that, allowing, e.g., |
| // |
| // condition ? HTypeKnown(x) : x --> x |
| // condition ? x : HTypeKnown(x) --> x |
| // |
| // We don't remove loop phis here. SsaRedundantPhiEliminator will eliminate |
| // redundant phis without HTypeKnown refinements, including loop phis. |
| |
| // There may be control flow that exits early, leaving refinements that |
| // cause the type of the phi to be stronger than the source. Don't attempt |
| // this simplification until the type of the phi is calculated. |
| if (beforeTypePropagation) return false; |
| |
| HBasicBlock dominator = block.dominator!; |
| |
| /// Find the input, skipping refinements that do not dominate the condition, |
| /// e.g., skipping refinements in the arm of the if-then-else. |
| HInstruction? dominatingRefinementInput(HInstruction input) { |
| while (true) { |
| if (input.block!.dominates(dominator)) return input; |
| if (input is! HTypeKnown) return null; |
| input = input.checkedInput; |
| } |
| } |
| |
| final commonInput = dominatingRefinementInput(phi.inputs.first); |
| if (commonInput == null) return false; |
| |
| for (int i = 1; i < phi.inputs.length; i++) { |
| final next = dominatingRefinementInput(phi.inputs[i]); |
| if (!identical(next, commonInput)) return false; |
| } |
| |
| HTypeKnown replacement = HTypeKnown.pinned( |
| phi.instructionType, |
| commonInput, |
| ); |
| block.addBefore(block.first, replacement); |
| block.rewrite(phi, replacement); |
| block.removePhi(phi); |
| return true; |
| } |
| |
| /// Simplify some CFG diamonds to equivalent expressions. |
| void simplifyExpressionPhi(HBasicBlock block, HPhi phi) { |
| // Is [block] the join point for a simple diamond? |
| 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. |
| final 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 instruction) { |
| return instruction; |
| } |
| |
| ConstantValue? getConstantFromType(HInstruction node) { |
| if (node.isValue(_abstractValueDomain) && |
| node.isNull(_abstractValueDomain).isDefinitelyFalse && |
| node.isLateSentinel(_abstractValueDomain).isDefinitelyFalse) { |
| final 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; |
| final 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 as HLocalGet); |
| } |
| |
| for (HLocalGet user in loads) { |
| final block = user.block!; |
| block.rewrite(user, value); |
| block.remove(user); |
| } |
| if (checkedLoad != null) { |
| final block = checkedLoad.block!; |
| block.rewrite(checkedLoad, node); |
| 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) { |
| final folded = foldUnary(node.operation(), node.operand); |
| return folded ?? node; |
| } |
| |
| HInstruction? foldUnary( |
| constant_system.UnaryOperation operation, |
| HInstruction operand, |
| ) { |
| if (operand is HConstant) { |
| HConstant receiver = operand; |
| final 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 is HConstant) { |
| ConstantValue constant = actualReceiver.constant; |
| if (constant is StringConstantValue) { |
| return _graph.addConstantInt(constant.length, _closedWorld); |
| } |
| if (constant is ListConstantValue) { |
| return _graph.addConstantInt(constant.length, _closedWorld); |
| } |
| if (constant is MapConstantValue) { |
| return _graph.addConstantInt(constant.length, _closedWorld); |
| } |
| } |
| |
| if (actualReceiver |
| .isIndexablePrimitive(_abstractValueDomain) |
| .isDefinitelyTrue) { |
| 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 |
| .isInstanceOf(actualType, commonElements.jsUInt31Class) |
| .isDefinitelyTrue) { |
| resultType = _abstractValueDomain.uint31Type; |
| } else if (_abstractValueDomain |
| .isInstanceOf(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; |
| } |
| return null; |
| } |
| |
| HInstruction handleInterceptedCall(HInvokeDynamic node) { |
| // Try constant folding the instruction. |
| final operation = node.specializer.operation(); |
| if (operation != null) { |
| HInstruction? folded; |
| if (operation is constant_system.UnaryOperation) { |
| assert(node.inputs.length == 2); |
| folded = foldUnary(operation, node.inputs[1]); |
| } else if (operation is constant_system.BinaryOperation) { |
| assert(node.inputs.length == 3); |
| folded = foldBinary(operation, node.inputs[1], node.inputs[2]); |
| } |
| if (folded != null) { |
| _metrics.countOperationFolded.add(); |
| return folded; |
| } |
| } |
| |
| // Try converting the instruction to a builtin instruction. |
| final 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) && |
| _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)) { |
| final 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; |
| final 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)) { |
| // `.isFunction` implies FunctionEntity, but not vice-versa. |
| FunctionEntity method = element as FunctionEntity; |
| |
| 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 is FieldEntity && 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; |
| } |
| HConstant constant = _graph.addConstant( |
| fieldData.constantValue!, |
| _closedWorld, |
| sourceInformation: node.sourceInformation, |
| ); |
| _log?.registerConstantFieldCall(node, field, constant); |
| load = constant; |
| } else { |
| AbstractValue type = AbstractValueFactory.inferredTypeForMember( |
| field, |
| _globalInferenceResults, |
| ); |
| HFieldGet fieldGet = HFieldGet( |
| field, |
| receiver, |
| type, |
| node.sourceInformation, |
| isAssignable: field.isAssignable, |
| ); |
| _log?.registerFieldCall(node, fieldGet); |
| node.block!.addBefore(node, fieldGet); |
| insertionPoint = fieldGet; |
| load = fieldGet; |
| } |
| 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).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); |
| final result = HInvokeExternal( |
| method, |
| [receiver], |
| returnType, |
| nativeBehavior, |
| sourceInformation: node.sourceInformation, |
| ); |
| _registry.registerStaticUse(StaticUse.methodInlining(method, null)); |
| // Assume Native getters effect-free as an approximation 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, |
| HInvokeExternal invocation, |
| FunctionEntity method, |
| ) { |
| HInstruction replacement = invocation; |
| if (_options.nativeNullAssertions && memberEntityIsInWebLibrary(method)) { |
| FunctionType type = _closedWorld.elementEnvironment.getFunctionType( |
| method, |
| ); |
| if (_closedWorld.dartTypes.isNonNullable(type.returnType)) { |
| node.block!.addBefore(node, invocation); |
| replacement = HNullCheck( |
| invocation, |
| _abstractValueDomain.excludeNull(invocation.instructionType), |
| sticky: true, |
| ); |
| } |
| } else if (_options.interopNullAssertions) { |
| final name = PublicName( |
| _nativeData.computeUnescapedJSInteropName(method.name!), |
| ); |
| final selector = |
| method.isGetter |
| ? Selector.getter(name) |
| : Selector.call( |
| name, |
| CallStructure.unnamed(invocation.inputs.length), |
| ); |
| if (_nativeData.interopNullChecks.containsKey(selector)) { |
| FunctionType type = _closedWorld.elementEnvironment.getFunctionType( |
| method, |
| ); |
| if (_closedWorld.dartTypes.isNonNullable(type.returnType)) { |
| node.block!.addBefore(node, invocation); |
| replacement = HInvokeStatic( |
| commonElements.interopNullAssertion, |
| [invocation], |
| _abstractValueDomain.excludeNull(invocation.instructionType), |
| const <DartType>[], |
| ); |
| } |
| } |
| } |
| 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; |
| } |
| |
| final 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); |
| final 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); |
| } |
| |
| HConstant? foldBinary( |
| constant_system.BinaryOperation operation, |
| HInstruction left, |
| HInstruction right, |
| ) { |
| if (left is HConstant && right is HConstant) { |
| ConstantValue? folded = operation.fold(left.constant, right.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 visitSubtract(HSubtract node) { |
| HInstruction left = node.left; |
| HInstruction right = node.right; |
| if (right is HConstant) { |
| final constant = right.constant; |
| // Rewrite `a - 0` to `a`, provided the zero is not negative zero. |
| if (constant.isZero && !constant.isMinusZero) return left; |
| } |
| return super.visitSubtract(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(); |
| final folded = foldBinary(operation, left, right); |
| if (folded != null) return folded; |
| return 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. |
| // TODO(sra): The above is probably no longer true. |
| 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(); |
| } |
| |
| // `x == true` --> `x` |
| // `x == false` --> `!x` |
| HInstruction? tryCompareConstant( |
| HConstant constantInput, |
| HInstruction otherInput, |
| ) { |
| final constant = constantInput.constant; |
| if (constant is BoolConstantValue && |
| otherInput.isBoolean(_abstractValueDomain).isDefinitelyTrue) { |
| return constant.boolValue |
| ? otherInput |
| : HNot(otherInput, _abstractValueDomain.boolType); |
| } |
| return null; |
| } |
| |
| HInstruction? replacement; |
| if (left is HConstant) { |
| replacement = tryCompareConstant(left, right); |
| } else if (right is HConstant) { |
| replacement = tryCompareConstant(right, left); |
| } |
| if (replacement != null) return replacement; |
| |
| 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) { |
| // We expect the enum class to extend `_Enum`, which has an `index` field, |
| // but that might be shadowed by an abstract getter from the class or a |
| // mixin. |
| ClassEntity? cls = enumClass; |
| MemberEntity? foundMember; |
| while (cls != null) { |
| final member = _closedWorld.elementEnvironment.lookupClassMember( |
| cls, |
| const PublicName('index'), |
| ); |
| if (member == null) break; // should never happen. |
| foundMember = member; |
| if (member is FieldEntity) return member; |
| if (!member.isAbstract) break; |
| cls = _closedWorld.elementEnvironment.getSuperClass(cls); |
| } |
| throw StateError('$enumClass should have index field, found $foundMember'); |
| } |
| |
| 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) { |
| final 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 is HConstant) 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; |
| HBasicBlock? elseContinuation; |
| 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), |
| ); |
| for (var hoisted in dominating) { |
| 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 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) { |
| final value = constant.fields[node.element]; |
| if (value != null) { |
| _metrics.countFieldGetFolded.add(); |
| return _graph.addConstant(value, _closedWorld); |
| } |
| } |
| if (constant is RecordConstantValue) { |
| final recordData = _closedWorld.recordData; |
| final shape = constant.shape; |
| final representation = recordData.representationForShape(shape); |
| if (representation != null) { |
| // The `representation` does not have a method to convert a field into |
| // a record-index, so look at all the possible access paths to find |
| // one that matches the field. Although this is 'slow' (1) only short |
| // records have direct fields (longer ones use arrays), and (2) we |
| // should always find a matching path, so the search will not be |
| // repeated in later phases. |
| for (int i = 0; i < constant.shape.fieldCount; i++) { |
| final path = recordData.pathForAccess(shape, i); |
| if (path.field == node.element && path.index == null) { |
| return _graph.addConstant(constant.values[i], _closedWorld); |
| } |
| } |
| } |
| } |
| } |
| |
| return node; |
| } |
| |
| @override |
| HInstruction visitGetLength(HGetLength node) { |
| HInstruction receiver = node.receiver; |
| |
| if (receiver is HConstant) { |
| ConstantValue constant = receiver.constant; |
| if (constant is ListConstantValue) { |
| _metrics.countGetLengthFolded.add(); |
| return _graph.addConstantInt(constant.length, _closedWorld); |
| } |
| if (constant is StringConstantValue) { |
| _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; |
| |
| SCAN: |
| while (!_graph.allocatedFixedLists.contains(potentialAllocation)) { |
| switch (potentialAllocation) { |
| case HInvokeStatic(:final element) |
| when element == commonElements.setArrayType: |
| // Look through `setArrayType(new Array(), ...)` |
| potentialAllocation = potentialAllocation.inputs.first; |
| case HArrayFlagsCheck(:final array) || HArrayFlagsSet(:final array): |
| potentialAllocation = array; |
| default: |
| break SCAN; |
| } |
| } |
| |
| 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 elimination. |
| 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) { |
| switch (node) { |
| case HIndex( |
| receiver: HConstant(:final constant), |
| index: HConstant(constant: final constantIndex), |
| ): |
| final foldedValue = constant_system.index.fold(constant, constantIndex); |
| if (foldedValue != null) { |
| _metrics.countIndexFolded.add(); |
| return _graph.addConstant(foldedValue, _closedWorld); |
| } |
| |
| // Match the access path `(constant_record._values)[i]` for 'long' records |
| // where the record fields are stored in an Array (the `_RecordN` family |
| // of representations). |
| case HIndex( |
| receiver: HFieldGet( |
| receiver: HConstant( |
| constant: RecordConstantValue(:final shape, :final values), |
| ), |
| element: final field, |
| ), |
| index: HConstant( |
| constant: IntConstantValue(:final intValue) && |
| final constantIndex, |
| ), |
| ) |
| when constantIndex.isUInt31(): |
| int indexValue = intValue.toInt(); |
| final recordData = _closedWorld.recordData; |
| final representation = recordData.representationForShape(shape); |
| if (representation != null) { |
| // We assume that the record index is going to be the same as the |
| // HIndex index. If not (for example, we put the shape in the first |
| // slot of the array, offsetting the record field indexes), the |
| // codegen test will fail. |
| final path = recordData.pathForAccess(shape, indexValue); |
| if (path.field == field && path.index == indexValue) { |
| return _graph.addConstant(values[indexValue], _closedWorld); |
| } |
| } |
| } |
| |
| return node; |
| } |
| |
| @override |
| HInstruction visitCharCodeAt(HCharCodeAt node) { |
| final folded = foldBinary( |
| constant_system.codeUnitAt, |
| node.receiver, |
| node.index, |
| ); |
| if (folded != null) return folded; |
| 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; |
| final 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; |
| } |
| |
| HFieldGet _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; |
| final member = |
| node.element ??= _closedWorld.locateSingleMember( |
| node.selector, |
| receiverType, |
| ); |
| if (member == null) return node; |
| |
| if (member is FieldEntity) { |
| if (!member.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(member).isElided) { |
| _log?.registerFieldSet(node); |
| _metrics.countSettersElided.add(); |
| return value; |
| } else { |
| HFieldSet result = HFieldSet(member, receiver, value) |
| ..sourceInformation = node.sourceInformation; |
| _log?.registerFieldSet(node, result); |
| _metrics.countSettersInlined.add(); |
| return result; |
| } |
| } |
| |
| if (node.isInvariant && |
| !_closedWorld.elementEnvironment.isFieldCovariantByDeclaration( |
| member, |
| )) { |
| return assignField(); |
| } |
| |
| if (!_closedWorld.annotationsData |
| .getParameterCheckPolicy(member) |
| .isEmitted) { |
| return assignField(); |
| } |
| |
| DartType fieldType = _closedWorld.elementEnvironment.getFieldType(member); |
| |
| AbstractValueWithPrecision checkedType = _abstractValueDomain |
| .createFromStaticType(fieldType); |
| 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 { |
| final isAssertHelper = commonElements.isAssertHelper(element); |
| final isAssertTest = commonElements.isAssertTest(element); |
| if (isAssertHelper || isAssertTest) { |
| 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 (isAssertTest) { |
| // `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 is HConstant) { |
| final constant = instruction.constant; |
| if (constant is StringConstantValue) return constant; |
| } |
| return null; |
| } |
| |
| final left = node.left; |
| final right = node.right; |
| StringConstantValue? leftString = getString(left); |
| if (leftString != null && leftString.stringValue.isEmpty) { |
| return right; |
| } |
| |
| final rightString = getString(right); |
| if (rightString == null) return node; |
| if (rightString.stringValue.isEmpty) return left; |
| |
| HInstruction? prefix; |
| if (leftString == null) { |
| if (left is! HStringConcat) return node; |
| HStringConcat leftConcat = 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 > |
| maxSharedConstantFoldedStringLength) { |
| 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 is HConstant) { |
| ConstantValue value = input.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 workaround 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) { |
| final 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...) |
| final 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) { |
| final 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) { |
| final environment = node.inputs[0]; |
| final argument = node.inputs[1]; |
| if (environment is HTypeEval && |
| argument is HTypeEval && |
| environment.inputs.single == argument.inputs.single && |
| // Should always be true, but checking is safe: |
| TypeEnvironmentStructure.same( |
| environment.envStructure, |
| argument.envStructure, |
| )) { |
| // env.eval(X).bind(env.eval(Y)) --> env.eval(...X...Y...) |
| final result = _typeRecipeDomain.foldEvalBindEvalWithSharedEnvironment( |
| environment.envStructure, |
| environment.typeExpression, |
| argument.typeExpression, |
| ); |
| if (result != null) { |
| return HTypeEval( |
| environment.inputs.single, |
| result.environmentStructure, |
| result.recipe, |
| node.instructionType, |
| ); |
| } |
| } |
| 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) { |
| // Always a TypeExpressionRecipe, otherwise 'as' is malformed. |
| TypeExpressionRecipe recipe = |
| typeInput.typeExpression as TypeExpressionRecipe; |
| node.checkedTypeExpression = recipe.type; |
| } |
| |
| if (node.isRedundant(_closedWorld, _options)) { |
| return node.checkedInput; |
| } |
| |
| // See if this check can be lowered to a simple one. |
| final specializedCheck = SpecializedChecks.findAsCheck( |
| node.checkedTypeExpression, |
| _closedWorld.commonElements, |
| ); |
| if (specializedCheck != null) { |
| AbstractValueWithPrecision checkedType = _abstractValueDomain |
| .createFromStaticType(node.checkedTypeExpression); |
| 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) { |
| // Always a TypeExpressionRecipe, otherwise 'is' is malformed. |
| TypeExpressionRecipe recipe = |
| typeInput.typeExpression as TypeExpressionRecipe; |
| 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); |
| } |
| |
| final specialization = SpecializedChecks.findIsTestSpecialization( |
| node.dartType, |
| _graph.element, |
| _closedWorld, |
| ); |
| |
| if (specialization == SimpleIsTestSpecialization.isNull || |
| specialization == SimpleIsTestSpecialization.isNotNull) { |
| HInstruction nullTest = HIdentity( |
| node.checkedInput, |
| _graph.addConstantNull(_closedWorld), |
| _abstractValueDomain.boolType, |
| ); |
| if (specialization == SimpleIsTestSpecialization.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); |
| _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.nonCheck(); |
| |
| // 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.isEmpty) { |
| 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) |
| final 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) |
| final 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) { |
| final 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; |
| |
| final 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) { |
| final folded = foldBinary( |
| constant_system.shiftLeft, |
| operand2, |
| count, |
| ); |
| if (folded != null) { |
| return HBitAnd(a, folded, node.instructionType); |
| } |
| } |
| } |
| } |
| } |
| } |
| return node; |
| } |
| |
| @override |
| HInstruction visitArrayFlagsCheck(HArrayFlagsCheck node) { |
| // TODO(sra): Implement removal on basis of type, an 'isRedundant' check. |
| |
| final array = node.array; |
| final arrayFlags = node.arrayFlags; |
| final checkFlags = node.checkFlags; |
| |
| if (arrayFlags is HConstant && arrayFlags.constant.isZero) return array; |
| |
| if (array is HArrayFlagsCheck) { |
| // Dependent check. Checks become dependent during types_propagation. |
| if (arrayFlags == array.arrayFlags && checkFlags == array.checkFlags) { |
| // Check is redundant, even if the `node.operation` is different |
| // (different operations are not picked up by GVN). |
| // |
| // TODO(sra): If a stronger check dominates a weaker check (e.g. check |
| // for immutable before check for fixed length), we can match that with |
| // different flags. |
| return array; |
| } |
| } |
| |
| // The 'operation' and 'verb' strings can be replaced with an index into a |
| // small table to known operations or verbs. This makes the call-sites |
| // smaller, so is worthwhile for calls to HArrayFlagsCheck that are inlined |
| // into multiple places. |
| // |
| // A trailing zero index (verb, or verb and operation) can be omitted from |
| // the instruction. |
| // |
| // When both indexes are replaced by indexes, the indexes are combined into |
| // a single value. |
| // |
| // finalIndex = verbIndex * numberOfOperationIndexes + operationIndex |
| |
| int? verbIndex; // Verb index if nonzero. |
| |
| if (node.hasVerb) { |
| if (node.verb case HConstant( |
| constant: StringConstantValue(:final stringValue), |
| )) { |
| final index = ArrayFlags.verbToIndex[stringValue]; |
| if (index != null) { |
| if (index == 0) { |
| node.removeInput(4); |
| } else { |
| final replacement = _graph.addConstantInt(index, _closedWorld); |
| node.replaceInput(4, replacement); |
| verbIndex = index; |
| } |
| } |
| } |
| } |
| |
| if (node.hasOperation) { |
| if (node.operation case HConstant( |
| constant: StringConstantValue(:final stringValue), |
| )) { |
| var index = ArrayFlags.operationNameToIndex[stringValue]; |
| if (index != null) { |
| if (index == 0 && !node.hasVerb) { |
| node.removeInput(3); |
| } else { |
| if (verbIndex != null) { |
| // Encode combined indexes and remove 'verb' input. |
| index += verbIndex * ArrayFlags.operationNameToIndex.length; |
| node.removeInput(4); |
| } |
| final replacement = _graph.addConstantInt(index, _closedWorld); |
| node.replaceInput(3, replacement); |
| } |
| } |
| } |
| } |
| |
| return node; |
| } |
| |
| /// All HArrayFlagsGet instructions that depend on something. Used to promote |
| /// `HArrayFlagsGet` instructions to side-effect insensitive. See |
| /// [finalizeArrayFlagEffects] for details. |
| List<HArrayFlagsGet>? _arrayFlagsGets; |
| bool _arrayFlagsEffect = false; |
| |
| @override |
| HInstruction visitArrayFlagsSet(HArrayFlagsSet node) { |
| _arrayFlagsEffect = true; |
| return node; |
| } |
| |
| @override |
| HInstruction visitArrayFlagsGet(HArrayFlagsGet node) { |
| if (node.sideEffects.dependsOnSomething()) { |
| (_arrayFlagsGets ??= []).add(node); |
| } else { |
| // If the HArrayFlagsGet is pure and the source is visible, then there is |
| // no HArrayFlagsSet instruction that changes the flags, so the flags are |
| // `0`. This can remove checks on allocations in the same method. To do |
| // this for typed arrays, we need to recognize the allocation. |
| |
| final array = node.inputs.single; |
| |
| if (array is HForeignCode) { |
| final behavior = array.nativeBehavior; |
| if (behavior != null && behavior.isAllocation) { |
| return _graph.addConstantInt(ArrayFlags.none, _closedWorld); |
| } |
| } |
| } |
| |
| // The following store-forwarding of the flags is valid only because all |
| // code in the SDK has a 'linear' pattern where the original value is never |
| // accessed after it is 'tagged' with the flags. |
| HInstruction array = node.inputs.single; |
| while (array is HArrayFlagsCheck) { |
| array = array.array; |
| } |
| if (array case HArrayFlagsSet(:final flags)) return flags; |
| |
| return node; |
| } |
| |
| void finalizeArrayFlagEffects() { |
| // HArrayFlagsGet operations must not be moved past HArrayFlagsSet |
| // operations on the same Array or typed data view. Initially we prevent |
| // this by making HArrayFlagsSet have a changes-property side effect, and |
| // making HArrayFlagsGet depend on that effect. |
| // |
| // This turns out to be rather restrictive and a general 'depends on |
| // property' dependency inhibits important optimizations like hoisting |
| // HArrayFlagsGet out of loops. We could try an add a new effect, but since |
| // the effect analysis is not aware of (non)aliasing, the new effect would |
| // largely have the same problem. |
| // |
| // Instead we notice that HArrayFlagsSet is rare: it is used to implement |
| // constructors that initialize the data, and then mark it as unmodifiable |
| // or fixed-length. If we invoke a callee that does a HArrayFlagsSet |
| // operation, the target of that operation is not visible to the caller. |
| // |
| // Therefore we assume that if we can't see any HArrayFlagsSet operations in |
| // the current method, they cannot change the value observed by |
| // HArrayFlagsGet, and we can pretent the HArrayFlagsGets are pure. |
| |
| if (_arrayFlagsGets == null || _arrayFlagsEffect) return; |
| |
| for (final instruction in _arrayFlagsGets!) { |
| // Instruction may have been removed from the CFG, but that is harmless. |
| instruction.sideEffects.clearAllDependencies(); |
| } |
| } |
| } |
| |
| class SsaDeadCodeEliminator extends HGraphVisitor implements OptimizationPhase { |
| @override |
| final String name = "SsaDeadCodeEliminator"; |
| |
| final JClosedWorld closedWorld; |
| final SsaOptimizerTask optimizer; |
| late final 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. |
| late final HInstruction 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; |
| |
| final 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.isNotEmpty) 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; |
| _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); |
| updateEmptyRegions(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)) { |
| final phiBlock = phi.block!; |
| phiBlock.rewrite(phi, replacement); |
| phiBlock.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; |
| 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) { |
| final block = phi.block!; |
| block.rewrite(phi, base!); |
| block.removePhi(phi); |
| } |
| } |
| |
| void simplifyControlFlow(HBasicBlock block) { |
| HInstruction? instruction = block.last; |
| if (instruction is HIf) { |
| HInstruction condition = instruction.condition; |
| if (condition is HConstant) 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 and a candidate |
| // for being dead code. |
| // |
| // TODO(http://dartbug.com/29475): Remove empty blocks so that recognizing |
| // no-op control flow is trivial. |
| |
| final thenBlock = block.successors[0]; |
| final (thenContinuation, thenSize) = _emptyRegion[thenBlock]!; |
| |
| final elseBlock = block.successors[1]; |
| final (elseContinuation, elseSize) = _emptyRegion[elseBlock]!; |
| |
| if (thenContinuation != elseContinuation) return; |
| if (!thenContinuation.phis.isEmpty) return; |
| |
| // Pick the 'live' branch to be the smallest subgraph. |
| bool value = thenSize <= elseSize; |
| HInstruction newCondition = _graph.addConstantBool(value, closedWorld); |
| instruction.replaceInput(0, newCondition); |
| } |
| } |
| |
| /// Map from block to the continuation, and, if all paths to the continuation |
| /// are empty, the number of blocks in that region. The block is either (1) |
| /// the continuation of the block (a join point of a single-entry, single-exit |
| /// region) or (2a) an earlier block if some the path to the continuation is |
| /// not empty, or (2b) there is path that avoids the continuation (like a loop |
| /// exit or a return). A block `B` that is non-empty has the entry `(B, 0)`. A |
| /// block that dominates a region that has an exit or a path that is non-empty |
| /// is also marked as `(B, 0)`. |
| final Map<HBasicBlock, (HBasicBlock, int)> _emptyRegion = {}; |
| |
| void updateEmptyRegions(HBasicBlock block) { |
| // Default is to consider this block to be the entry to an non-empty region. |
| _emptyRegion[block] = (block, 0); |
| |
| // To be empty, this block should have nothing except a single terminating |
| // HControlFlow instruction. |
| final instruction = block.first; |
| if (instruction == null || instruction.next != null) return; |
| if (!block.phis.isEmpty) return; |
| |
| if (!(instruction is HGoto || |
| instruction is HIf || |
| instruction is HBreak || |
| instruction is HSwitch)) { |
| // Other control flow instructions either generate code (e.g. HReturn), |
| // or are part of some structure like a loop or try-catch. |
| return; |
| } |
| |
| // If all paths from this block reach the same continuation, we have an |
| // empty single-entry / single-exit region. Create a new empty interval |
| // from this block to the continuation, and extend it to the continuation of |
| // the continuation. |
| |
| if (block.successors.isEmpty) return; |
| |
| int size = 1; // Size of empty region includes this block. |
| |
| HBasicBlock? continuation; |
| if (block.successors.length == 1) { |
| continuation = block.successors.single; |
| } else { |
| for (final successor in block.successors) { |
| if (successor.id < block.id) return; // Back-edge |
| final (successorContinuation, successorSize) = _emptyRegion[successor]!; |
| size += successorSize; |
| if (continuation == null) { |
| continuation = successorContinuation; |
| } else if (continuation != successorContinuation) { |
| // This happens when (1) some successor is not empty, (2) some |
| // successor is, or contains, an edge that exits the nested |
| // forward-edge single-entry-single-exit regions. |
| return; |
| } |
| } |
| } |
| |
| if (continuation!.dominator == block) { |
| final int continuationSize; |
| (continuation, continuationSize) = _emptyRegion[continuation]!; |
| size += continuationSize; |
| } |
| |
| _emptyRegion[block] = (continuation, size); |
| } |
| |
| /// 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; |
| HInstruction? branch = block.last; |
| if (branch is HIf) { |
| if (branch.thenBlock.isLive == branch.elseBlock.isLive) return; |
| assert(branch.condition is HConstant); |
| 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) { |
| for (var user in instruction.usedBy) { |
| 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<void> { |
| 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.isNotEmpty) { |
| 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 node) { |
| node.block!.successors.forEach(markBlockLive); |
| } |
| |
| @override |
| void visitIf(HIf node) { |
| HInstruction condition = node.condition; |
| if (condition is HConstant) { |
| if (condition.isConstantTrue()) { |
| markBlockLive(node.thenBlock); |
| } else { |
| markBlockLive(node.elseBlock); |
| } |
| } else { |
| visitControlFlow(node); |
| } |
| } |
| |
| @override |
| void visitSwitch(HSwitch node) { |
| if (node.expression.isInteger(_abstractValueDomain).isDefinitelyTrue) { |
| final switchRange = ranges[node.expression]; |
| if (switchRange != null) { |
| final lowerValue = switchRange.lower; |
| final upperValue = switchRange.upper; |
| if (lowerValue is IntValue && upperValue is IntValue) { |
| 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] as HConstant; |
| final constant = input.constant; |
| if (constant is! IntConstantValue) { |
| // The switch expression is an integer but the constant is not, so |
| // the target block is unreachable. |
| continue; |
| } |
| 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.isNotEmpty) { |
| 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) { |
| for (final input in phi.inputs) { |
| input.removeUser(phi); |
| } |
| phi.inputs.clear(); |
| } |
| |
| // Two-phase removal, phase 2: remove the now-disconnected phis. |
| for (final phi in deadPhis) { |
| assert(phi.inputs.isEmpty); |
| assert(phi.usedBy.isEmpty); |
| phi.block!.removePhi(phi); |
| } |
| } |
| |
| @override |
| bool validPostcondition(HGraph graph) { |
| return NoUnusedPhiValidator.containsNoUnusedPhis(graph); |
| } |
| } |
| |
| class SsaRedundantPhiEliminator implements OptimizationPhase { |
| @override |
| final String name = "SsaRedundantPhiEliminator"; |
| |
| @override |
| void visitGraph(HGraph graph) { |
| final List<HPhi> worklist = []; |
| |
| // Add all phis in the worklist. |
| for (final block in graph.blocks) { |
| block.forEachPhi((HPhi phi) => worklist.add(phi)); |
| } |
| |
| while (worklist.isNotEmpty) { |
| HPhi phi = worklist.removeLast(); |
| |
| // If the phi has already been processed, continue. |
| if (!phi.isInBasicBlock()) continue; |
| |
| // Find if the inputs of the phi are the same instruction. |
| // The builder ensures that phi.inputs[0] cannot be the phi |
| // itself. |
| assert(!identical(phi.inputs[0], phi)); |
| HInstruction? candidate = phi.inputs[0]; |
| for (int i = 1; i < phi.inputs.length; i++) { |
| HInstruction input = phi.inputs[i]; |
| // If the input is the phi, the phi is still candidate for |
| // elimination. |
| if (!identical(input, candidate) && !identical(input, phi)) { |
| candidate = null; |
| break; |
| } |
| } |
| |
| // If the inputs are not the same, continue. |
| if (candidate == null) continue; |
| |
| // Because we're updating the users of this phi, we may have new |
| // phis candidate for elimination. Add phis that used this phi |
| // to the worklist. |
| for (final user in phi.usedBy) { |
| if (user is HPhi) worklist.add(user); |
| } |
| final phiBlock = phi.block!; |
| phiBlock.rewrite(phi, candidate); |
| phiBlock.removePhi(phi); |
| if (candidate.sourceElement == null && |
| phi.sourceElement != null && |
| candidate is! HThis) { |
| candidate.sourceElement = phi.sourceElement; |
| } |
| } |
| } |
| |
| @override |
| bool validPostcondition(HGraph graph) => true; |
| } |
| |
| class GvnWorkItem { |
| final HBasicBlock block; |
| final ValueSet valueSet; |
| GvnWorkItem(this.block, this.valueSet); |
| } |
| |
| class SsaGlobalValueNumberer implements OptimizationPhase { |
| final AbstractValueDomain _abstractValueDomain; |
| @override |
| final String name = "SsaGlobalValueNumberer"; |
| final Set<int> visited = {}; |
| |
| late final List<Bitset> blockChangesFlags; |
| late final List<Bitset> loopChangesFlags; |
| |
| SsaGlobalValueNumberer(this._abstractValueDomain); |
| |
| @override |
| void visitGraph(HGraph graph) { |
| computeChangesFlags(graph); |
| moveLoopInvariantCode(graph); |
| List<GvnWorkItem> workQueue = [GvnWorkItem(graph.entry, ValueSet())]; |
| do { |
| GvnWorkItem item = workQueue.removeLast(); |
| visitBasicBlock(item.block, item.valueSet, workQueue); |
| } while (workQueue.isNotEmpty); |
| } |
| |
| @override |
| bool validPostcondition(HGraph graph) => true; |
| |
| void moveLoopInvariantCode(HGraph graph) { |
| for (int i = graph.blocks.length - 1; i >= 0; i--) { |
| HBasicBlock block = graph.blocks[i]; |
| if (block.isLoopHeader()) { |
| final changesFlags = loopChangesFlags[block.id]; |
| final info = block.loopInformation!; |
| // Iterate over all blocks of this loop. Note that blocks in |
| // inner loops are not visited here, but we know they |
| // were visited before because we are iterating in post-order. |
| // So instructions that are GVN'ed in an inner loop are in their |
| // loop entry, and [info.blocks] contains this loop entry. |
| moveLoopInvariantCodeFromBlock(block, block, changesFlags); |
| for (HBasicBlock other in info.blocks) { |
| moveLoopInvariantCodeFromBlock(other, block, changesFlags); |
| } |
| } |
| } |
| } |
| |
| void moveLoopInvariantCodeFromBlock( |
| HBasicBlock block, |
| HBasicBlock loopHeader, |
| Bitset changesFlags, |
| ) { |
| assert(block.parentLoopHeader == loopHeader || block == loopHeader); |
| HBasicBlock preheader = loopHeader.predecessors[0]; |
| var dependsFlags = SideEffects.computeDependsOnFlags(changesFlags); |
| HInstruction? instruction = block.first; |
| bool isLoopAlwaysTaken() { |
| final instruction = loopHeader.last!; |
| assert(instruction is HGoto || instruction is HLoopBranch); |
| return instruction is HGoto || instruction.inputs[0].isConstantTrue(); |
| } |
| |
| bool firstInstructionInLoop = |
| block == loopHeader |
| // Compensate for lack of code motion. |
| || |
| (blockChangesFlags[loopHeader.id].isEmpty && |
| isLoopAlwaysTaken() && |
| loopHeader.successors[0] == block); |
| while (instruction != null) { |
| final next = instruction.next; |
| if (instruction.useGvn() && |
| instruction.isMovable && |
| (!instruction.canThrow(_abstractValueDomain) || |
| firstInstructionInLoop) && |
| !instruction.sideEffects.dependsOn(dependsFlags)) { |
| bool loopInvariantInputs = true; |
| List<HInstruction> inputs = instruction.inputs; |
| for (int i = 0, length = inputs.length; i < length; i++) { |
| if (isInputDefinedAfterDominator(inputs[i], preheader)) { |
| loopInvariantInputs = false; |
| break; |
| } |
| } |
| |
| // If the inputs are loop invariant, we can move the |
| // instruction from the current block to the pre-header block. |
| if (loopInvariantInputs) { |
| block.detach(instruction); |
| preheader.moveAtExit(instruction); |
| } else { |
| firstInstructionInLoop = false; |
| } |
| } |
| final oldChangesFlags = changesFlags; |
| changesFlags = changesFlags.union( |
| instruction.sideEffects.getChangesFlags(), |
| ); |
| if (oldChangesFlags != changesFlags) { |
| dependsFlags = SideEffects.computeDependsOnFlags(changesFlags); |
| } |
| instruction = next; |
| } |
| } |
| |
| bool isInputDefinedAfterDominator(HInstruction input, HBasicBlock dominator) { |
| return input.block!.id > dominator.id; |
| } |
| |
| void visitBasicBlock( |
| HBasicBlock block, |
| ValueSet values, |
| List<GvnWorkItem> workQueue, |
| ) { |
| HInstruction? instruction = block.first; |
| if (block.isLoopHeader()) { |
| final flags = loopChangesFlags[block.id]; |
| values.kill(flags); |
| } |
| while (instruction != null) { |
| final next = instruction.next; |
| final flags = instruction.sideEffects.getChangesFlags(); |
| assert(flags.isEmpty || !instruction.useGvn()); |
| // TODO(sra): Is the above assertion too strong? We should be able to |
| // reuse the values generated by idempotent operations that have |
| // effects. Would it be correct to make the kill below be conditional on |
| // not replacing the instruction? |
| values.kill(flags); |
| if (instruction.useGvn()) { |
| final other = values.lookup(instruction); |
| if (other != null) { |
| assert(other.gvnEquals(instruction) && instruction.gvnEquals(other)); |
| block.rewriteWithBetterUser(instruction, other); |
| block.remove(instruction); |
| } else { |
| values.add(instruction); |
| } |
| } |
| instruction = next; |
| } |
| |
| List<HBasicBlock> dominatedBlocks = block.dominatedBlocks; |
| for (int i = 0, length = dominatedBlocks.length; i < length; i++) { |
| HBasicBlock dominated = dominatedBlocks[i]; |
| // No need to copy the value set for the last child. |
| ValueSet successorValues = (i == length - 1) ? values : values.copy(); |
| // If we have no values in our set, we do not have to kill |
| // anything. Also, if the range of block ids from the current |
| // block to the dominated block is empty, there is no blocks on |
| // any path from the current block to the dominated block so we |
| // don't have to do anything either. |
| assert(block.id < dominated.id); |
| if (!successorValues.isEmpty && block.id + 1 < dominated.id) { |
| visited.clear(); |
| List<HBasicBlock> workQueue = <HBasicBlock>[dominated]; |
| var changesFlags = Bitset.empty(); |
| do { |
| HBasicBlock current = workQueue.removeLast(); |
| changesFlags = changesFlags.union( |
| getChangesFlagsForDominatedBlock(block, current, workQueue), |
| ); |
| } while (workQueue.isNotEmpty); |
| successorValues.kill(changesFlags); |
| } |
| workQueue.add(GvnWorkItem(dominated, successorValues)); |
| } |
| } |
| |
| void computeChangesFlags(HGraph graph) { |
| // Create the changes flags lists. Make sure to initialize the |
| // loop changes flags list to zero so we can use bitwise-or when |
| // propagating loop changes upwards. |
| final int length = graph.blocks.length; |
| blockChangesFlags = List<Bitset>.filled(length, SideEffects.allChanges); |
| loopChangesFlags = List<Bitset>.filled(length, Bitset.empty()); |
| |
| // Run through all the basic blocks in the graph and fill in the |
| // changes flags lists. |
| for (int i = length - 1; i >= 0; i--) { |
| final HBasicBlock block = graph.blocks[i]; |
| final int id = block.id; |
| |
| // Compute block changes flags for the block. |
| var changesFlags = Bitset.empty(); |
| HInstruction? instruction = block.first; |
| while (instruction != null) { |
| changesFlags = changesFlags.union( |
| instruction.sideEffects.getChangesFlags(), |
| ); |
| instruction = instruction.next; |
| } |
| assert(blockChangesFlags[id] == SideEffects.allChanges); |
| blockChangesFlags[id] = changesFlags; |
| |
| // Loop headers are part of their loop, so update the loop |
| // changes flags accordingly. |
| if (block.isLoopHeader()) { |
| loopChangesFlags[id] = loopChangesFlags[id].union(changesFlags); |
| } |
| |
| // Propagate loop changes flags upwards. |
| final parentLoopHeader = block.parentLoopHeader; |
| if (parentLoopHeader != null) { |
| loopChangesFlags[parentLoopHeader |
| .id] = loopChangesFlags[parentLoopHeader.id].union( |
| (block.isLoopHeader()) ? loopChangesFlags[id] : changesFlags, |
| ); |
| } |
| } |
| } |
| |
| Bitset getChangesFlagsForDominatedBlock( |
| HBasicBlock dominator, |
| HBasicBlock dominated, |
| List<HBasicBlock> workQueue, |
| ) { |
| var changesFlags = Bitset.empty(); |
| List<HBasicBlock> predecessors = dominated.predecessors; |
| for (int i = 0, length = predecessors.length; i < length; i++) { |
| HBasicBlock block = predecessors[i]; |
| int id = block.id; |
| // If the current predecessor block is on the path from the |
| // dominator to the dominated, it must have an id that is in the |
| // range from the dominator to the dominated. |
| if (dominator.id < id && id < dominated.id && !visited.contains(id)) { |
| visited.add(id); |
| changesFlags = changesFlags.union(blockChangesFlags[id]); |
| // Loop bodies might not be on the path from dominator to dominated, |
| // but they can invalidate values. |
| changesFlags = changesFlags.union(loopChangesFlags[id]); |
| workQueue.add(block); |
| } |
| } |
| return changesFlags; |
| } |
| } |
| |
| // This phase merges equivalent instructions on different paths into |
| // one instruction in a dominator block. It runs through the graph |
| // post dominator order and computes a ValueSet for each block of |
| // instructions that can be moved to a dominator block. These |
| // instructions are the ones that: |
| // 1) can be used for GVN, and |
| // 2) do not use definitions of their own block. |
| // |
| // A basic block looks at its successors and finds the intersection of |
| // these computed ValueSet. It moves all instructions of the |
| // intersection into its own list of instructions. |
| class SsaCodeMotion extends HBaseVisitor<void> implements OptimizationPhase { |
| final AbstractValueDomain _abstractValueDomain; |
| |
| @override |
| final String name = "SsaCodeMotion"; |
| |
| bool movedCode = false; |
| late final List<ValueSet> values; |
| |
| SsaCodeMotion(this._abstractValueDomain); |
| |
| @override |
| void visitGraph(HGraph graph) { |
| values = List.generate(graph.blocks.length, (_) => ValueSet()); |
| visitPostDominatorTree(graph); |
| } |
| |
| @override |
| bool validPostcondition(HGraph graph) => true; |
| |
| @override |
| void visitBasicBlock(HBasicBlock node) { |
| List<HBasicBlock> successors = node.successors; |
| |
| // Phase 1: get the ValueSet of all successors (if there are more than one), |
| // compute the intersection and move the instructions of the intersection |
| // into this block. |
| if (successors.length > 1) { |
| ValueSet instructions = values[successors[0].id]; |
| for (int i = 1; i < successors.length; i++) { |
| ValueSet other = values[successors[i].id]; |
| instructions = instructions.intersection(other); |
| } |
| |
| if (!instructions.isEmpty) { |
| List<HInstruction> list = instructions.toList(); |
| // Sort by instruction 'id' for more stable ordering under changes to |
| // unrelated source code. 'id' is a function of the operations of |
| // compiling the current method, whereas the ValueSet order is dependent |
| // hashCodes that are a function of the whole program. |
| list.sort((insn1, insn2) => insn1.id.compareTo(insn2.id)); |
| for (HInstruction instruction in list) { |
| // Move the instruction to the current block. |
| instruction.block!.detach(instruction); |
| node.moveAtExit(instruction); |
| // Go through all successors and rewrite their instruction |
| // to the shared one. |
| for (final successor in successors) { |
| final toRewrite = values[successor.id].lookup(instruction); |
| if (toRewrite != instruction) { |
| successor.rewriteWithBetterUser(toRewrite, instruction); |
| successor.remove(toRewrite!); |
| movedCode = true; |
| } |
| } |
| } |
| } |
| // TODO(sra): There are some non-gvn-able instructions that we could move, |
| // e.g. allocations. We should probably not move instructions that can |
| // directly or indirectly throw since the reported location might be in |
| // the 'wrong' branch. |
| } |
| |
| // Don't try to merge instructions to a dominator if we have |
| // multiple predecessors. |
| if (node.predecessors.length != 1) return; |
| |
| // Phase 2: Go through all instructions of this block and find |
| // which instructions can be moved to a dominator block. |
| ValueSet set_ = values[node.id]; |
| HInstruction? instruction = node.first; |
| var flags = Bitset.empty(); |
| while (instruction != null) { |
| final dependsFlags = SideEffects.computeDependsOnFlags(flags); |
| flags = flags.union(instruction.sideEffects.getChangesFlags()); |
| |
| HInstruction current = instruction; |
| instruction = instruction.next; |
| if (!current.useGvn() || !current.isMovable) continue; |
| // TODO(sra): We could move throwing instructions provided we keep the |
| // exceptions in the same order. This requires they are in the same order |
| // in all successors, which is not tracked by the ValueSet. |
| if (current.canThrow(_abstractValueDomain)) continue; |
| if (current.sideEffects.dependsOn(dependsFlags)) continue; |
| |
| bool canBeMoved = true; |
| for (final HInstruction input in current.inputs) { |
| if (input.block == node) { |
| canBeMoved = false; |
| break; |
| // TODO(sra): We could move trees of instructions provided we move the |
| // roots before the leaves. |
| } |
| } |
| if (!canBeMoved) continue; |
| |
| final existing = set_.lookup(current); |
| if (existing == null) { |
| set_.add(current); |
| } else { |
| node.rewriteWithBetterUser(current, existing); |
| node.remove(current); |
| movedCode = true; |
| } |
| } |
| } |
| } |
| |
| class SsaTypeConversionInserter extends HBaseVisitor<void> |
| implements OptimizationPhase { |
| @override |
| final String name = "SsaTypeconversionInserter"; |
| final JClosedWorld closedWorld; |
| |
| SsaTypeConversionInserter(this.closedWorld); |
| |
| AbstractValueDomain get _abstractValueDomain => |
| closedWorld.abstractValueDomain; |
| |
| @override |
| void visitGraph(HGraph graph) { |
| visitDominatorTree(graph); |
| } |
| |
| @override |
| bool validPostcondition(HGraph graph) => true; |
| |
| // Update users of [input] that are dominated by [:dominator.first:] |
| // to use [TypeKnown] of [input] instead. As the type information depends |
| // on the control flow, we mark the inserted [HTypeKnown] nodes as |
| // non-movable. |
| void insertTypePropagationForDominatedUsers( |
| HBasicBlock dominator, |
| HInstruction input, |
| AbstractValue convertedType, |
| ) { |
| DominatedUses dominatedUses = DominatedUses.of(input, dominator.first!); |
| if (dominatedUses.isEmpty) return; |
| |
| // Check to avoid adding a duplicate HTypeKnown node. |
| if (dominatedUses.isSingleton) { |
| HInstruction user = dominatedUses.single; |
| if (user is HTypeKnown && |
| user.isPinned && |
| user.knownType == convertedType && |
| user.checkedInput == input) { |
| return; |
| } |
| } |
| |
| HTypeKnown newInput = HTypeKnown.pinned(convertedType, input); |
| dominator.addBefore(dominator.first, newInput); |
| dominatedUses.replaceWith(newInput); |
| } |
| |
| @override |
| void visitIsTest(HIsTest node) { |
| List<HBasicBlock> trueTargets = []; |
| List<HBasicBlock> falseTargets = []; |
| |
| collectTargets(node, trueTargets, falseTargets); |
| |
| if (trueTargets.isEmpty && falseTargets.isEmpty) return; |
| |
| AbstractValue convertedType = node.checkedAbstractValue.abstractValue; |
| HInstruction input = node.checkedInput; |
| |
| for (HBasicBlock block in trueTargets) { |
| insertTypePropagationForDominatedUsers(block, input, convertedType); |
| } |
| // TODO(sra): Also strengthen uses for when the condition is precise and |
| // known false (e.g. int? x; ... if (x is! int) use(x)). Avoid strengthening |
| // to `null`. |
| } |
| |
| @override |
| void visitIsTestSimple(HIsTestSimple node) { |
| List<HBasicBlock> trueTargets = []; |
| List<HBasicBlock> falseTargets = []; |
| |
| collectTargets(node, trueTargets, falseTargets); |
| |
| if (trueTargets.isEmpty && falseTargets.isEmpty) return; |
| |
| AbstractValue convertedType = node.checkedAbstractValue.abstractValue; |
| HInstruction input = node.checkedInput; |
| |
| for (HBasicBlock block in trueTargets) { |
| insertTypePropagationForDominatedUsers(block, input, convertedType); |
| } |
| // TODO(sra): Also strengthen uses for when the condition is precise and |
| // known false (e.g. int? x; ... if (x is! int) use(x)). Avoid strengthening |
| // to `null`. |
| } |
| |
| @override |
| void visitIdentity(HIdentity node) { |
| // At HIf(HIdentity(x, null)) strengthens x to non-null on else branch. |
| HInstruction left = node.left; |
| HInstruction right = node.right; |
| HInstruction input; |
| |
| if (left.isConstantNull()) { |
| input = right; |
| } else if (right.isConstantNull()) { |
| input = left; |
| } else { |
| return; |
| } |
| |
| if (_abstractValueDomain.isNull(input.instructionType).isDefinitelyFalse) { |
| return; |
| } |
| |
| List<HBasicBlock> trueTargets = []; |
| List<HBasicBlock> falseTargets = []; |
| |
| collectTargets(node, trueTargets, falseTargets); |
| |
| if (trueTargets.isEmpty && falseTargets.isEmpty) return; |
| |
| AbstractValue nonNullType = _abstractValueDomain.excludeNull( |
| input.instructionType, |
| ); |
| |
| for (HBasicBlock block in falseTargets) { |
| insertTypePropagationForDominatedUsers(block, input, nonNullType); |
| } |
| // We don't strengthen the known-true references. It doesn't happen often |
| // and we don't want "if (x==null) return x;" to convert between JavaScript |
| // 'null' and 'undefined'. |
| } |
| |
| void collectTargets( |
| HInstruction instruction, |
| List<HBasicBlock>? trueTargets, |
| List<HBasicBlock>? falseTargets, |
| ) { |
| if (trueTargets == null && falseTargets == null) return; |
| |
| for (HInstruction user in instruction.usedBy) { |
| if (user is HIf) { |
| trueTargets?.add(user.thenBlock); |
| falseTargets?.add(user.elseBlock); |
| |
| final joinBlock = user.joinBlock; |
| if (joinBlock != null) { |
| final joinPredecessors = joinBlock.predecessors; |
| if (joinPredecessors.length == 2) { |
| if (hasUnreachableExit(joinPredecessors[0])) { |
| // The then-branch does not reach the join block, so the join |
| // block is reached only if condition is false. |
| falseTargets?.add(joinBlock); |
| } else if (hasUnreachableExit(joinPredecessors[1])) { |
| // The else-branch does not reach the join block, so the join |
| // block is reached only if condition is true. |
| trueTargets?.add(joinBlock); |
| } else { |
| final phi = joinBlock.phis.firstPhi; |
| if (phi != null && phi.next == null) { |
| assert(phi.inputs.length == 2); |
| |
| // This is a single phi controlled by `user`. |
| // |
| // Collect the targets of the phi. The phi is in effectively a |
| // conditional `user ? left : right`. |
| |
| final right = phi.inputs[1]; |
| if (right.isConstantFalse()) { |
| // When `c ? x : false` is true, `c` must be true. |
| // So pass `c`'s trueTargets as the phi's trueTargets. |
| collectTargets(phi, trueTargets, null); |
| } else if (right.isConstantTrue()) { |
| // When `c ? x : true` is false, `c` must be true. |
| // So pass `c`'s trueTargets as the phi's falseTargets. |
| collectTargets(phi, null, trueTargets); |
| } |
| |
| final left = phi.inputs[0]; |
| if (left.isConstantFalse()) { |
| // When `c ? false : x` is true, `c` must be false. |
| // So pass `c`'s falseTargets as the phi's trueTargets. |
| collectTargets(phi, falseTargets, null); |
| } else if (left.isConstantTrue()) { |
| // When `c ? true : x` is false, `c` must be false. |
| // So pass `c`'s falseTargets as the phi's falseTargets. |
| collectTargets(phi, null, falseTargets); |
| } |
| |
| // Sanity checks: |
| // |
| // For `c ? true : false`, we pass both `c`'s trueTargets and |
| // falseTargets as the same targets of the phi. |
| // |
| // For `c ? false : true`, we pass the targets reversed, like we |
| // for `HNot`. |
| // |
| // For `c ? false : false`, we pass both `c`'s trueTargets and |
| // falseTargets to the unreachable trueTargets of the phi. We |
| // might insert contradictory strengthenings, which might refine |
| // a value to Never, i.e. we potentially 'prove' the code is |
| // unreachable. |
| } |
| } |
| } |
| } |
| } else if (user is HLoopBranch) { |
| trueTargets?.add(user.block!.successors.first); |
| // Don't insert refinements on else-branch - may be a critical edge |
| // block which we currently need to keep empty (except for phis). |
| } else if (user is HNot) { |
| collectTargets(user, falseTargets, trueTargets); |
| } else if (user is HPhi) { |
| List<HInstruction> inputs = user.inputs; |
| if (inputs.length == 2) { |
| assert(inputs.contains(instruction)); |
| HInstruction other = inputs[(inputs[0] == instruction) ? 1 : 0]; |
| if (other.isConstantTrue()) { |
| // The condition flows to `HPhi(true, user)` or `HPhi(user, true)`, |
| // which means that a downstream HIf has true-branch control flow |
| // that does not depend on the original instruction, so stop |
| // collecting [trueTargets]. |
| collectTargets(user, null, falseTargets); |
| } else if (other.isConstantFalse()) { |
| // Ditto for false. |
| collectTargets(user, trueTargets, null); |
| } |
| } |
| } |
| } |
| } |
| } |
| |
| /// Optimization phase that tries to eliminate memory loads (for example |
| /// [HFieldGet]), when it knows the value stored in that memory location, and |
| /// stores that overwrite with the same value. |
| class SsaLoadElimination extends HBaseVisitor<void> |
| implements OptimizationPhase { |
| final JClosedWorld _closedWorld; |
| final JFieldAnalysis _fieldAnalysis; |
| @override |
| final String name = "SsaLoadElimination"; |
| late MemorySet memorySet; |
| late final List<MemorySet?> memories; |
| bool newGvnCandidates = false; |
| late final HGraph _graph; |
| |
| // Blocks that can be reached via control flow not expressed by the basic |
| // block CFG. These are catch and finally blocks that are reached from some |
| // mid-block instruction that throws in the CFG region corresponding to the |
| // Dart language try-block. The value stored in the map is the HTry that owns |
| // the catch or finally block. This map is filled in on-the-fly since the HTry |
| // dominates the catch and finally so is visited first. |
| Map<HBasicBlock, HTry>? _blocksWithImprecisePredecessors; |
| |
| SsaLoadElimination(this._closedWorld) |
| : _fieldAnalysis = _closedWorld.fieldAnalysis; |
| |
| AbstractValueDomain get _abstractValueDomain => |
| _closedWorld.abstractValueDomain; |
| |
| @override |
| void visitGraph(HGraph graph) { |
| _graph = graph; |
| memories = List<MemorySet?>.filled(graph.blocks.length, null); |
| List<HBasicBlock> blocks = graph.blocks; |
| for (int i = 0; i < blocks.length; i++) { |
| HBasicBlock block = blocks[i]; |
| visitBasicBlock(block); |
| if (block.successors.isNotEmpty && block.successors[0].isLoopHeader()) { |
| // We've reached the ending block of a loop. Iterate over the |
| // blocks of the loop again to take values that flow from that |
| // ending block into account. |
| for (int j = block.successors[0].id; j <= block.id; j++) { |
| visitBasicBlock(blocks[j]); |
| } |
| } |
| } |
| } |
| |
| @override |
| bool validPostcondition(HGraph graph) => true; |
| |
| @override |
| void visitBasicBlock(HBasicBlock node) { |
| final predecessors = node.predecessors; |
| final indegree = predecessors.length; |
| if (indegree == 0) { |
| // Entry block. |
| memorySet = MemorySet(_closedWorld); |
| } else if (indegree == 1 && predecessors[0].successors.length == 1) { |
| // No need to clone, there is no other successor for |
| // `block.predecessors[0]`, and this block has only one predecessor. Since |
| // we are not going to visit `block.predecessors[0]` again, we can just |
| // re-use its [memorySet]. |
| memorySet = memories[predecessors[0].id]!; |
| } else if (indegree == 1) { |
| // Clone the memorySet of the predecessor, because it is also used by |
| // other successors of it. |
| memorySet = memories[predecessors[0].id]!.clone(); |
| } else { |
| // Compute the intersection of all predecessors. |
| // |
| // If a predecessor does not have a reachable exit, the kills on that path |
| // can be ignored. Since the usual case is conditional diamond flow with |
| // two predecessors, this is done by detecting a single non-dead |
| // predecessor and cloning the memory-set, but removing expressions that |
| // are not valid in the current block (invalid instructions would be in |
| // one arm of the diamond). |
| |
| int firstLiveIndex = -1; |
| int otherLiveIndex = -1; |
| int firstDeadIndex = -1; |
| bool pendingBackEdge = false; |
| |
| List<MemorySet?> inputs = List.filled(indegree, null); |
| |
| for (int i = 0; i < indegree; i++) { |
| final predecessor = predecessors[i]; |
| final input = inputs[i] = memories[predecessor.id]; |
| if (input == null) pendingBackEdge = true; |
| if (hasUnreachableExit(predecessor)) { |
| if (firstDeadIndex == -1) firstDeadIndex = i; |
| } else { |
| if (firstLiveIndex == -1) { |
| firstLiveIndex = i; |
| } else if (otherLiveIndex == -1) { |
| otherLiveIndex = i; |
| } |
| } |
| } |
| |
| if (firstLiveIndex != -1 && |
| otherLiveIndex == -1 && |
| firstDeadIndex != -1 && |
| !pendingBackEdge) { |
| // Single live input intersection. |
| memorySet = memories[predecessors[firstLiveIndex].id]! |
| .cloneIfDominatesBlock(node); |
| } else { |
| // Standard intersection over all predecessors. |
| memorySet = inputs[0]!; |
| for (int i = 1; i < inputs.length; i++) { |
| memorySet = memorySet.intersectionFor(inputs[i], node, i); |
| } |
| } |
| } |
| |
| // If the current block is a catch or finally block, it is reachable from |
| // any instruction in the try region that can generate an exception. |
| if (_blocksWithImprecisePredecessors != null) { |
| final tryInstruction = _blocksWithImprecisePredecessors![node]; |
| if (tryInstruction != null) { |
| memorySet.killLocationsForExceptionEdge(); |
| } |
| } |
| |
| memories[node.id] = memorySet; |
| HInstruction? instruction = node.first; |
| while (instruction != null) { |
| final next = instruction.next; |
| instruction.accept(this); |
| instruction = next; |
| } |
| } |
| |
| @override |
| void visitTry(HTry node) { |
| final impreciseBlocks = _blocksWithImprecisePredecessors ??= {}; |
| if (node.catchBlock != null) { |
| impreciseBlocks[node.catchBlock!] = node; |
| } |
| if (node.finallyBlock != null) { |
| impreciseBlocks[node.finallyBlock!] = node; |
| } |
| } |
| |
| void checkNewGvnCandidates(HInstruction instruction, HInstruction existing) { |
| if (newGvnCandidates) return; |
| bool hasUseGvn(HInstruction insn) => insn.nonCheck().useGvn(); |
| if (instruction.usedBy.any(hasUseGvn) && existing.usedBy.any(hasUseGvn)) { |
| newGvnCandidates = true; |
| } |
| } |
| |
| @override |
| void visitFieldGet(HFieldGet node) { |
| FieldEntity element = node.element; |
| HInstruction receiver = node.getDartReceiver(_closedWorld).nonCheck(); |
| _visitFieldGet(element, receiver, node); |
| } |
| |
| @override |
| void visitGetLength(HGetLength node) { |
| HInstruction receiver = node.receiver.nonCheck(); |
| final existing = memorySet.lookupFieldValue(MemoryFeature.length, receiver); |
| if (existing != null) { |
| checkNewGvnCandidates(node, existing); |
| final block = node.block!; |
| block.rewriteWithBetterUser(node, existing); |
| block.remove(node); |
| } else { |
| memorySet.registerFieldValue(MemoryFeature.length, receiver, node); |
| } |
| } |
| |
| void _visitFieldGet( |
| FieldEntity element, |
| HInstruction receiver, |
| HInstruction instruction, |
| ) { |
| final existing = memorySet.lookupFieldValue(element, receiver); |
| if (existing != null) { |
| checkNewGvnCandidates(instruction, existing); |
| final block = instruction.block!; |
| block.rewriteWithBetterUser(instruction, existing); |
| block.remove(instruction); |
| } else { |
| memorySet.registerFieldValue(element, receiver, instruction); |
| } |
| } |
| |
| @override |
| void visitFieldSet(HFieldSet node) { |
| FieldEntity element = node.element; |
| HInstruction receiver = node.getDartReceiver(_closedWorld).nonCheck(); |
| if (memorySet.registerFieldValueUpdate(element, receiver, node.value)) { |
| node.block!.remove(node); |
| } |
| } |
| |
| @override |
| void visitCreate(HCreate node) { |
| memorySet.registerAllocation(node); |
| if (shouldTrackInitialValues(node)) { |
| int argumentIndex = 0; |
| _closedWorld.elementEnvironment.forEachInstanceField(node.element, ( |
| _, |
| FieldEntity member, |
| ) { |
| FieldAnalysisData fieldData = _fieldAnalysis.getFieldData(member); |
| if (fieldData.isElided) return; |
| if (fieldData.isInitializedInAllocator) { |
| // TODO(sra): Can we avoid calling HGraph.addConstant? |
| final value = fieldData.initialValue!; |
| HConstant constant = _graph.addConstant(value, _closedWorld); |
| memorySet.registerFieldValue(member, node, constant); |
| } else { |
| memorySet.registerFieldValue( |
| member, |
| node, |
| node.inputs[argumentIndex++], |
| ); |
| } |
| }); |
| } |
| // In case this instruction has as input non-escaping objects, we |
| // need to mark these objects as escaping. |
| memorySet.killAffectedBy(node); |
| } |
| |
| bool shouldTrackInitialValues(HCreate instruction) { |
| // Don't track initial field values of an allocation that are |
| // unprofitable. We search the chain of single uses in allocations for a |
| // limited depth. |
| |
| const maxHeapDepth = 5; |
| |
| bool interestingUse(HInstruction instruction, int heapDepth) { |
| // Heuristic: if the allocation is too deep in heap it is unlikely we will |
| // recover a field by load-elimination. |
| // TODO(sra): We can measure this depth by looking at load chains. |
| if (heapDepth == maxHeapDepth) return false; |
| // There are multiple uses so do the full store analysis. |
| if (instruction.usedBy.length != 1) return true; |
| HInstruction use = instruction.usedBy.single; |
| // When the only use is an allocation, the allocation becomes the only |
| // heap alias for the current instruction. |
| if (use is HCreate) return interestingUse(use, heapDepth + 1); |
| if (use is HLiteralList) return interestingUse(use, heapDepth + 1); |
| if (use is HInvokeStatic) { |
| // Assume the argument escapes. All we do with our initial allocation is |
| // have it escape or store it into an object that escapes. |
| return false; |
| // TODO(sra): Handle library functions that we know do not modify or |
| // leak the inputs. For example `setArrayType` is used to mark |
| // list literals with type information. |
| } |
| if (use is HPhi) { |
| // The initial allocation (it's only alias) gets merged out of the model |
| // of the heap before load. |
| return false; |
| } |
| return true; |
| } |
| |
| return interestingUse(instruction, 0); |
| } |
| |
| @override |
| void visitInstruction(HInstruction instruction) { |
| if (instruction.isAllocation(_abstractValueDomain)) { |
| memorySet.registerAllocation(instruction); |
| } |
| memorySet.killAffectedBy(instruction); |
| } |
| |
| @override |
| void visitLazyStatic(HLazyStatic node) { |
| FieldEntity field = node.element; |
| handleStaticLoad(field, node); |
| } |
| |
| void handleStaticLoad(MemberEntity element, HInstruction instruction) { |
| final existing = memorySet.lookupFieldValue(element, null); |
| if (existing != null) { |
| checkNewGvnCandidates(instruction, existing); |
| final block = instruction.block!; |
| block.rewriteWithBetterUser(instruction, existing); |
| block.remove(instruction); |
| } else { |
| memorySet.registerFieldValue(element, null, instruction); |
| } |
| } |
| |
| @override |
| void visitStatic(HStatic node) { |
| handleStaticLoad(node.element, node); |
| } |
| |
| @override |
| void visitStaticStore(HStaticStore node) { |
| if (memorySet.registerFieldValueUpdate( |
| node.element, |
| null, |
| node.inputs.last, |
| )) { |
| node.block!.remove(node); |
| } |
| } |
| |
| @override |
| void visitLiteralList(HLiteralList node) { |
| memorySet.registerAllocation(node); |
| memorySet.killAffectedBy(node); |
| // TODO(sra): Set initial keyed values. |
| // TODO(sra): Set initial length. |
| } |
| |
| @override |
| void visitIndex(HIndex node) { |
| HInstruction receiver = node.receiver.nonCheck(); |
| HInstruction index = node.index.nonCheck(); |
| final existing = memorySet.lookupKeyedValue(receiver, index); |
| if (existing != null) { |
| checkNewGvnCandidates(node, existing); |
| final block = node.block!; |
| block.rewriteWithBetterUser(node, existing); |
| block.remove(node); |
| } else { |
| memorySet.registerKeyedValue(receiver, index, node); |
| } |
| } |
| |
| @override |
| void visitIndexAssign(HIndexAssign node) { |
| HInstruction receiver = node.receiver.nonCheck(); |
| memorySet.registerKeyedValueUpdate( |
| receiver, |
| node.index.nonCheck(), |
| node.value, |
| ); |
| } |
| |
| // Pure operations that do not escape their inputs. |
| @override |
| void visitBinaryArithmetic(HBinaryArithmetic node) {} |
| @override |
| void visitBoundsCheck(HBoundsCheck node) {} |
| @override |
| void visitConstant(HConstant node) {} |
| @override |
| void visitIf(HIf node) {} |
| @override |
| void visitInterceptor(HInterceptor node) {} |
| @override |
| void visitNot(HNot node) {} |
| @override |
| void visitNullCheck(HNullCheck node) {} |
| @override |
| void visitLateReadCheck(HLateReadCheck node) {} |
| @override |
| void visitParameterValue(HParameterValue node) {} |
| @override |
| void visitRelational(HRelational node) {} |
| @override |
| void visitStringConcat(HStringConcat node) {} |
| @override |
| void visitTypeKnown(HTypeKnown node) {} |
| } |
| |
| /// A non-field based feature of an object. |
| enum MemoryFeature { |
| // Access to the `length` property of a `JSIndexable`. |
| length, |
| } |
| |
| /// Holds values of memory places. |
| /// |
| /// Generally, values that name a place (a receiver) have type refinements and |
| /// other checks removed to ensure that checks and type refinements do not |
| /// confuse aliasing. Values stored into a memory place keep the type |
| /// refinements to help further optimizations. |
| class MemorySet { |
| final JClosedWorld closedWorld; |
| |
| /// Maps a field to a map of receivers to their current field values. |
| /// |
| /// The field is either a [FieldEntity], a [FunctionEntity] in case of |
| /// instance methods, or a [MemoryFeature] for `length` access on |
| /// `JSIndexable`. |
| /// |
| /// The receiver is `null` for static and top-level fields. |
| /// |
| /// Sometimes `null` is stored in the map instead of deleting an entry to |
| /// avoid ConcurrentModificationError. |
| // TODO(25544): Split length effects from other effects and model lengths |
| // separately. |
| final Map< |
| Object /*MemberEntity|MemoryFeature*/, |
| Map<HInstruction?, HInstruction?> |
| > |
| fieldValues = {}; |
| |
| /// Maps a receiver to a map of keys to value. |
| final Map<HInstruction, Map<HInstruction, HInstruction?>> keyedValues = {}; |
| |
| /// Set of objects that we know don't escape (or have not yet escaped) the |
| /// current function. |
| final Setlet<HInstruction> nonEscapingReceivers = Setlet(); |
| |
| MemorySet(this.closedWorld); |
| |
| AbstractValueDomain get _abstractValueDomain => |
| closedWorld.abstractValueDomain; |
| |
| /// Returns whether [first] and [second] always alias to the same object. |
| bool mustAlias(HInstruction? first, HInstruction? second) { |
| return first == second; |
| } |
| |
| /// Returns whether [first] and [second] may alias to the same object. |
| bool mayAlias(HInstruction? first, HInstruction? second) { |
| if (mustAlias(first, second)) return true; |
| if (isConcrete(first) && isConcrete(second)) return false; |
| if (nonEscapingReceivers.contains(first)) return false; |
| if (nonEscapingReceivers.contains(second)) return false; |
| // Typed arrays of different types might have a shared buffer. |
| if (couldBeTypedArray(first!) && couldBeTypedArray(second!)) return true; |
| return _abstractValueDomain |
| .areDisjoint(first.instructionType, second!.instructionType) |
| .isPotentiallyFalse; |
| } |
| |
| bool isFinal(Object element) { |
| return element is MemberEntity && closedWorld.fieldNeverChanges(element); |
| } |
| |
| bool isConcrete(HInstruction? instruction) { |
| return instruction is HCreate || |
| instruction is HConstant || |
| instruction is HLiteralList; |
| } |
| |
| bool couldBeTypedArray(HInstruction receiver) { |
| return closedWorld.abstractValueDomain |
| .couldBeTypedArray(receiver.instructionType) |
| .isPotentiallyTrue; |
| } |
| |
| /// Returns whether [receiver] escapes the current function. |
| bool escapes(HInstruction? receiver) { |
| assert(receiver == null || receiver == receiver.nonCheck()); |
| return !nonEscapingReceivers.contains(receiver); |
| } |
| |
| /// Kills locations that are imprecise due to many possible edges from |
| /// instructions in the try region that can throw. |
| void killLocationsForExceptionEdge() { |
| // Ideally we would treat each strong (must) update in the try region as a |
| // weak (may) update at the catch block, but we don't track this. The |
| // conservative approximation is to kill everything. |
| // |
| // Aliases can be retained because they are not updated - they are generated |
| // by an allocation and are killed by escaping. There is an edge from any |
| // exit from the try region to the catch block which accounts for the kills |
| // via escapes. Similarly, array lengths don't have updates (set:length is |
| // modeled as an escape which kills the length on some path), so lengths |
| // don't need to be killed here. |
| // |
| // TODO(sra): A more precise accounting of the effects in the try region |
| // might improve precision. |
| fieldValues.forEach((key, map) { |
| if (key != MemoryFeature.length) { |
| map.clear(); |
| } |
| }); |
| keyedValues.clear(); |
| } |
| |
| void registerAllocation(HInstruction instruction) { |
| assert(instruction == instruction.nonCheck()); |
| nonEscapingReceivers.add(instruction); |
| } |
| |
| /// Sets the [field] on [receiver] to contain [value]. Kills all potential |
| /// places that may be affected by this update. Returns `true` if the update |
| /// is redundant. |
| bool registerFieldValueUpdate( |
| Object field, |
| HInstruction? receiver, |
| HInstruction value, |
| ) { |
| assert( |
| field is MemberEntity || field is MemoryFeature, |
| "Unexpected member/feature: $field", |
| ); |
| assert(receiver == null || receiver == receiver.nonCheck()); |
| if (field is MemberEntity && closedWorld.nativeData.isNativeMember(field)) { |
| return false; // TODO(14955): Remove this restriction? |
| } |
| // [value] is being set in some place in memory, we remove it from the |
| // non-escaping set. |
| nonEscapingReceivers.remove(value.nonCheck()); |
| final map = fieldValues.putIfAbsent(field, () => {}); |
| bool isRedundant = map[receiver] == value; |
| map.forEach((key, value) { |
| if (mayAlias(receiver, key)) map[key] = null; |
| }); |
| map[receiver] = value; |
| return isRedundant; |
| } |
| |
| /// Registers that the [field] on [receiver] is now [value]. |
| void registerFieldValue( |
| Object field, |
| HInstruction? receiver, |
| HInstruction value, |
| ) { |
| assert( |
| field is MemberEntity || field is MemoryFeature, |
| "Unexpected member/feature: $field", |
| ); |
| assert(receiver == null || receiver == receiver.nonCheck()); |
| if (field is MemberEntity && closedWorld.nativeData.isNativeMember(field)) { |
| return; // TODO(14955): Remove this restriction? |
| } |
| final map = fieldValues.putIfAbsent(field, () => {}); |
| map[receiver] = value; |
| } |
| |
| /// Returns the value stored for [field] on [receiver]. Returns `null` if we |
| /// don't know. |
| HInstruction? lookupFieldValue(Object field, HInstruction? receiver) { |
| assert( |
| field is MemberEntity || field is MemoryFeature, |
| "Unexpected member/feature: $field", |
| ); |
| assert(receiver == null || receiver == receiver.nonCheck()); |
| final map = fieldValues[field]; |
| return (map == null) ? null : map[receiver]; |
| } |
| |
| /// Kill all places that may be affected by this [instruction]. Also update |
| /// the set of non-escaping objects in case [instruction] has non-escaping |
| /// objects in its inputs. |
| void killAffectedBy(HInstruction instruction) { |
| // Even if [instruction] does not have side effects, it may use non-escaping |
| // objects and store them in a new object, which make these objects |
| // escaping. |
| for (var input in instruction.inputs) { |
| nonEscapingReceivers.remove(input.nonCheck()); |
| } |
| |
| if (instruction.sideEffects.changesInstanceProperty() || |
| instruction.sideEffects.changesStaticProperty()) { |
| // TODO(sra): Be more precise about removing only instance or static |
| // properties. |
| List<HInstruction?> receiversToRemove = []; |
| |
| List<Object>? fieldsToRemove; |
| fieldValues.forEach((Object element, map) { |
| if (isFinal(element)) return; |
| map.forEach((receiver, value) { |
| if (escapes(receiver)) { |
| receiversToRemove.add(receiver); |
| } |
| }); |
| if (receiversToRemove.length == map.length) { |
| // Remove them all by removing the entire map. |
| (fieldsToRemove ??= []).add(element); |
| } else { |
| receiversToRemove.forEach(map.remove); |
| } |
| receiversToRemove.clear(); |
| }); |
| fieldsToRemove?.forEach(fieldValues.remove); |
| } |
| |
| if (instruction.sideEffects.changesIndex()) { |
| keyedValues.forEach((receiver, map) { |
| if (escapes(receiver)) { |
| map.forEach((index, value) { |
| map[index] = null; |
| }); |
| } |
| }); |
| } |
| } |
| |
| /// Returns the value stored in `receiver[index]`. Returns null if we don't |
| /// know. |
| HInstruction? lookupKeyedValue(HInstruction receiver, HInstruction index) { |
| final map = keyedValues[receiver]; |
| return (map == null) ? null : map[index]; |
| } |
| |
| /// Registers that `receiver[index]` is now [value]. |
| void registerKeyedValue( |
| HInstruction receiver, |
| HInstruction index, |
| HInstruction value, |
| ) { |
| final map = keyedValues.putIfAbsent(receiver, () => {}); |
| map[index] = value; |
| } |
| |
| /// Sets `receiver[index]` to contain [value]. Kills all potential places that |
| /// may be affected by this update. |
| void registerKeyedValueUpdate( |
| HInstruction receiver, |
| HInstruction index, |
| HInstruction value, |
| ) { |
| nonEscapingReceivers.remove(value.nonCheck()); |
| keyedValues.forEach((key, values) { |
| if (mayAlias(receiver, key)) { |
| // Typed arrays that are views of the same buffer may have different |
| // offsets or element sizes, unless they are the same typed array. |
| bool weakIndex = couldBeTypedArray(key) && !mustAlias(receiver, key); |
| values.forEach((otherIndex, otherValue) { |
| if (weakIndex || mayAlias(index, otherIndex)) { |
| values[otherIndex] = null; |
| } |
| }); |
| } |
| }); |
| |
| // Typed arrays may narrow incoming values. |
| if (couldBeTypedArray(receiver)) return; |
| |
| final map = keyedValues.putIfAbsent(receiver, () => {}); |
| map[index] = value; |
| } |
| |
| /// Returns a common instruction for [first] and [second]. |
| /// |
| /// Returns `null` if either [first] or [second] is null. Returns [first] if |
| /// [first] and [second] are equal. Otherwise creates or re-uses a phi in |
| /// [block] that holds [first] and [second]. |
| HInstruction? findCommonInstruction( |
| HInstruction? first, |
| HInstruction? second, |
| HBasicBlock block, |
| int predecessorIndex, |
| ) { |
| if (first == null || second == null) return null; |
| if (first == second) return first; |
| if (second is HGetLength) { |
| // Don't always create phis for HGetLength. The phi confuses array bounds |
| // check elimination and the resulting variable-heavy code probably is |
| // confusing for JavaScript VMs. In practice, this mostly affects the |
| // expansion of for-in loops on Arrays, so we match the expression |
| // |
| // checkConcurrentModificationError(array.length == _end, array) |
| // |
| // starting with the HGetLength of the `array.length`, in the case where |
| // `array.length` is not used elsewhere (i.e. not already optimized to use |
| // a previous use, in the loop condition). |
| // |
| // TODO(sra): Figure out a better way ensure 'nice' loop code. |
| // TODO(22407): The phi would not be so bad if it did not confuse bounds |
| // check elimination. |
| // TODO(25437): We could add a phi if we undid the harmful cases. |
| if (second.usedBy.length == 1) { |
| var user = second.usedBy.single; |
| if (user is HIdentity && user.usedBy.length == 1) { |
| HInstruction user2 = user.usedBy.single; |
| if (user2 is HInvokeStatic && |
| closedWorld.commonElements.isCheckConcurrentModificationError( |
| user2.element, |
| )) { |
| return null; |
| } |
| } |
| } |
| } |
| AbstractValue phiType = _abstractValueDomain.union( |
| second.instructionType, |
| first.instructionType, |
| ); |
| if (first is HPhi && first.block == block) { |
| HPhi phi = first; |
| phi.addInput(second); |
| phi.instructionType = phiType; |
| return phi; |
| } else { |
| HPhi phi = HPhi.noInputs(null, phiType); |
| block.addPhi(phi); |
| // Previous predecessors had the same input. A phi must have |
| // the same number of inputs as its block has predecessors. |
| for (int i = 0; i < predecessorIndex; i++) { |
| phi.addInput(first); |
| } |
| phi.addInput(second); |
| return phi; |
| } |
| } |
| |
| /// Returns the intersection between this [MemorySet] and the [other] memory |
| /// set. |
| MemorySet intersectionFor( |
| MemorySet? other, |
| HBasicBlock block, |
| int predecessorIndex, |
| ) { |
| MemorySet result = MemorySet(closedWorld); |
| if (other == null) { |
| // This is the first visit to a loop header ([other] is `null` because we |
| // have not visited the back edge). Copy the nonEscapingReceivers that are |
| // guaranteed to survive the loop because they are not escaped before |
| // method exit. |
| // TODO(sra): We should do a proper dataflow to find the maximal |
| // nonEscapingReceivers (a variant of Available-Expressions), which must |
| // converge before we edit the program in [findCommonInstruction]. |
| for (HInstruction instruction in nonEscapingReceivers) { |
| bool isNonEscapingUse(HInstruction use) { |
| if (use is HReturn) return true; // Escapes, but so does control. |
| if (use is HFieldGet) return true; |
| if (use is HFieldSet) { |
| return use.value.nonCheck() != instruction; |
| } |
| if (use is HGetLength) return true; |
| if (use is HBoundsCheck) return true; |
| if (use is HIndex) return true; |
| if (use is HIndexAssign) { |
| return use.value.nonCheck() != instruction; |
| } |
| if (use is HInterceptor) return true; |
| if (use is HInvokeDynamicMethod) { |
| final element = use.element; |
| if (element != null) { |
| if (element == closedWorld.commonElements.jsArrayAdd) { |
| return use.inputs.last != instruction; |
| } |
| } |
| } |
| if (use is HInvokeStatic) { |
| if (closedWorld.commonElements.isCheckConcurrentModificationError( |
| use.element, |
| )) { |
| return true; |
| } |
| } |
| |
| return false; |
| } |
| |
| if (instruction.usedBy.every(isNonEscapingUse)) { |
| result.nonEscapingReceivers.add(instruction); |
| } |
| } |
| return result; |
| } |
| |
| fieldValues.forEach((element, values) { |
| var otherValues = other.fieldValues[element]; |
| if (otherValues == null) return; |
| values.forEach((receiver, value) { |
| final instruction = findCommonInstruction( |
| value, |
| otherValues[receiver], |
| block, |
| predecessorIndex, |
| ); |
| if (instruction != null) { |
| result.registerFieldValue(element, receiver, instruction); |
| } |
| }); |
| }); |
| |
| keyedValues.forEach((receiver, values) { |
| var otherValues = other.keyedValues[receiver]; |
| if (otherValues == null) return; |
| values.forEach((index, value) { |
| final instruction = findCommonInstruction( |
| value, |
| otherValues[index], |
| block, |
| predecessorIndex, |
| ); |
| if (instruction != null) { |
| result.registerKeyedValue(receiver, index, instruction); |
| } |
| }); |
| }); |
| |
| for (var receiver in nonEscapingReceivers) { |
| if (other.nonEscapingReceivers.contains(receiver)) { |
| result.nonEscapingReceivers.add(receiver); |
| } |
| } |
| return result; |
| } |
| |
| /// Returns a copy of this [MemorySet]. |
| MemorySet clone() { |
| MemorySet result = MemorySet(closedWorld); |
| |
| fieldValues.forEach((element, values) { |
| result.fieldValues[element] = Map.of(values); |
| }); |
| |
| keyedValues.forEach((receiver, values) { |
| result.keyedValues[receiver] = Map.of(values); |
| }); |
| |
| result.nonEscapingReceivers.addAll(nonEscapingReceivers); |
| return result; |
| } |
| |
| /// Returns a copy of this [MemorySet], removing any expressions that are not |
| /// valid in [block]. |
| MemorySet cloneIfDominatesBlock(HBasicBlock block) { |
| bool instructionDominatesBlock(HInstruction? instruction) { |
| return instruction != null && instruction.block!.dominates(block); |
| } |
| |
| MemorySet result = MemorySet(closedWorld); |
| |
| fieldValues.forEach((element, values) { |
| values.forEach((receiver, value) { |
| if ((receiver == null || instructionDominatesBlock(receiver)) && |
| instructionDominatesBlock(value)) { |
| result.registerFieldValue(element, receiver, value!); |
| } |
| }); |
| }); |
| |
| keyedValues.forEach((receiver, values) { |
| if (instructionDominatesBlock(receiver)) { |
| values.forEach((index, value) { |
| if (instructionDominatesBlock(index) && |
| instructionDominatesBlock(value)) { |
| result.registerKeyedValue(receiver, index, value!); |
| } |
| }); |
| } |
| }); |
| |
| result.nonEscapingReceivers.addAll( |
| nonEscapingReceivers.where(instructionDominatesBlock), |
| ); |
| return result; |
| } |
| } |