| // 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 'dart:collection' show Queue; | |
| // ignore: implementation_imports | |
| import 'package:front_end/src/api_prototype/lowering_predicates.dart' | |
| show | |
| isExtensionMemberTearOff, | |
| getExtensionMemberImplementation, | |
| isExtensionThisName, | |
| isTearOffLowering, | |
| getConstructorTearOffLoweringTarget; | |
| // ignore: implementation_imports | |
| import 'package:front_end/src/api_unstable/dart2js.dart' show Link; | |
| import 'package:kernel/ast.dart' as ir; | |
| import '../common.dart'; | |
| import '../common/codegen.dart' show CodegenRegistry; | |
| import '../common/elements.dart' show JCommonElements; | |
| import '../common/metrics.dart'; | |
| import '../common/names.dart'; | |
| import '../common/tasks.dart' show CompilerTask; | |
| import '../constants/constant_system.dart' as constant_system; | |
| import '../constants/values.dart'; | |
| import '../elements/entities.dart'; | |
| import '../elements/jumps.dart'; | |
| import '../elements/types.dart'; | |
| import '../inferrer/abstract_value_domain.dart'; | |
| import '../io/source_information.dart'; | |
| import '../js/js.dart' as js; | |
| import '../js_backend/codegen_inputs.dart' show CodegenInputs; | |
| import '../js_backend/interceptor_data.dart'; | |
| import '../js_backend/namer.dart' show ModularNamer; | |
| import '../js_backend/native_data.dart'; | |
| import '../js_backend/runtime_types_codegen.dart'; | |
| import '../js_backend/runtime_types_new.dart' | |
| show RecipeEncoder, RecipeEncoding, indexTypeVariable; | |
| import '../js_backend/specialized_checks.dart'; | |
| import '../js_backend/type_reference.dart' show TypeReference; | |
| import '../js_emitter/js_emitter.dart' show ModularEmitter; | |
| import '../js_model/elements.dart' show JGeneratorBody; | |
| import '../js_model/js_world.dart' show JClosedWorld; | |
| import '../js_model/records.dart' show JRecordClass; | |
| import '../js_model/type_recipe.dart'; | |
| import '../native/behavior.dart'; | |
| import '../options.dart'; | |
| import '../tracer.dart' show Tracer; | |
| import '../universe/call_structure.dart' show CallStructure; | |
| import '../universe/recorded_use.dart'; | |
| import '../universe/selector.dart' show Selector; | |
| import '../universe/use.dart' show ConstantUse, DynamicUse, StaticUse, TypeUse; | |
| import 'codegen_helpers.dart'; | |
| import 'nodes.dart'; | |
| import 'variable_allocator.dart'; | |
| mixin CodegenPhase { | |
| String get name; | |
| void visitGraph(HGraph graph); | |
| } | |
| class SsaCodeGeneratorTask extends CompilerTask { | |
| final CompilerOptions _options; | |
| final SourceInformationStrategy sourceInformationStrategy; | |
| CodegenMetrics? _codegenMetrics; | |
| CodegenMetrics get codegenMetrics => _codegenMetrics ??= CodegenMetrics(); | |
| /// Set of constants that have already been recorded for uses. Shard-global to | |
| /// avoid redundant traversal of recurring deep constants. | |
| /// | |
| /// Similar to the VM's `_ConstantCollector` in | |
| /// `pkg/vm/lib/transformations/record_use/constant_collector.dart`. | |
| final Map<ConstantValue, _RecordedUseSequence> _recordedConstantUsesVisited = | |
| {}; | |
| SsaCodeGeneratorTask( | |
| super.measurer, | |
| this._options, | |
| this.sourceInformationStrategy, | |
| ); | |
| @override | |
| String get name => 'SSA code generator'; | |
| @override | |
| Metrics get metrics => _codegenMetrics ?? Metrics.none(); | |
| js.Fun buildJavaScriptFunction( | |
| bool needsAsyncRewrite, | |
| FunctionEntity element, | |
| List<js.Parameter> parameters, | |
| js.Block body, | |
| ) { | |
| js.Fun finish(js.AsyncModifier asyncModifier) { | |
| return js.Fun( | |
| parameters, | |
| body, | |
| asyncModifier: asyncModifier, | |
| ).withSourceInformation( | |
| sourceInformationStrategy | |
| .createBuilderForContext(element) | |
| .buildDeclaration(element), | |
| ) | |
| as js.Fun; | |
| } | |
| if (needsAsyncRewrite) { | |
| return finish( | |
| element.asyncMarker.isAsync | |
| ? (element.asyncMarker.isYielding | |
| ? js.AsyncModifier.asyncStar | |
| : js.AsyncModifier.async) | |
| : (element.asyncMarker.isYielding | |
| ? js.AsyncModifier.syncStar | |
| : js.AsyncModifier.sync), | |
| ); | |
| } else { | |
| return finish(js.AsyncModifier.sync); | |
| } | |
| } | |
| js.Expression generateCode( | |
| MemberEntity member, | |
| HGraph graph, | |
| CodegenInputs codegen, | |
| JClosedWorld closedWorld, | |
| CodegenRegistry registry, | |
| ModularNamer namer, | |
| ModularEmitter emitter, | |
| ) { | |
| js.Expression code; | |
| if (member is FieldEntity) { | |
| code = generateLazyInitializer( | |
| member, | |
| graph, | |
| codegen, | |
| closedWorld, | |
| registry, | |
| namer, | |
| emitter, | |
| ); | |
| } else if (member is FunctionEntity) { | |
| code = generateMethod( | |
| member, | |
| graph, | |
| codegen, | |
| closedWorld, | |
| registry, | |
| namer, | |
| emitter, | |
| ); | |
| } else { | |
| failedAt(member, 'Cannot generate JavaScript for $member'); | |
| } | |
| codegen.tracer.traceJavaScriptText('JavaScript', code.debugPrint); | |
| return code; | |
| } | |
| js.Expression generateLazyInitializer( | |
| FieldEntity field, | |
| HGraph graph, | |
| CodegenInputs codegen, | |
| JClosedWorld closedWorld, | |
| CodegenRegistry registry, | |
| ModularNamer namer, | |
| ModularEmitter emitter, | |
| ) { | |
| return measure(() { | |
| SourceInformation? sourceInformation = sourceInformationStrategy | |
| .createBuilderForContext(field) | |
| .buildDeclaration(field); | |
| SsaCodeGenerator codeGenerator = SsaCodeGenerator( | |
| this, | |
| _options, | |
| codegenMetrics, | |
| emitter, | |
| codegen.rtiSubstitutions, | |
| codegen.rtiRecipeEncoder, | |
| namer, | |
| codegen.tracer, | |
| closedWorld, | |
| registry, | |
| field, | |
| _recordedConstantUsesVisited, | |
| ); | |
| codeGenerator.visitGraph(graph); | |
| codegen.tracer.traceGraph("codegen", graph); | |
| return js.Fun( | |
| codeGenerator.parameters, | |
| codeGenerator.body, | |
| ).withSourceInformation(sourceInformation); | |
| }); | |
| } | |
| js.Expression generateMethod( | |
| FunctionEntity method, | |
| HGraph graph, | |
| CodegenInputs codegen, | |
| JClosedWorld closedWorld, | |
| CodegenRegistry registry, | |
| ModularNamer namer, | |
| ModularEmitter emitter, | |
| ) { | |
| return measure(() { | |
| if (method.asyncMarker != AsyncMarker.sync) { | |
| registry.registerAsyncMarker(method.asyncMarker); | |
| } | |
| SsaCodeGenerator codeGenerator = SsaCodeGenerator( | |
| this, | |
| _options, | |
| codegenMetrics, | |
| emitter, | |
| codegen.rtiSubstitutions, | |
| codegen.rtiRecipeEncoder, | |
| namer, | |
| codegen.tracer, | |
| closedWorld, | |
| registry, | |
| method, | |
| _recordedConstantUsesVisited, | |
| ); | |
| codeGenerator.visitGraph(graph); | |
| codegen.tracer.traceGraph("codegen", graph); | |
| return buildJavaScriptFunction( | |
| graph.needsAsyncRewrite, | |
| method, | |
| codeGenerator.parameters, | |
| codeGenerator.body, | |
| ); | |
| }); | |
| } | |
| } | |
| class CodegenMetrics extends MetricsBase { | |
| int countHIf = 0; | |
| int countHIfConstant = 0; | |
| int countHIsTest = 0; | |
| int countHIsTestSimple = 0; | |
| int countHIsLateSentinel = 0; | |
| int countHGetLength = 0; | |
| int countHIndex = 0; | |
| int countHFieldGet = 0; | |
| int countSingleTargetInstanceCalls = 0; | |
| final countHInterceptor = CountMetric('count.HInterceptor'); | |
| final countHInterceptorGet = CountMetric('count.HInterceptor.getInterceptor'); | |
| final countHInterceptorOneshot = CountMetric('count.HInterceptor.oneShot'); | |
| final countHInterceptorConditionalConstant = CountMetric( | |
| 'count.HInterceptor.conditionalConstant', | |
| ); | |
| CodegenMetrics(); | |
| @override | |
| String get namespace => 'codegen'; | |
| @override | |
| Iterable<Metric> get primary => []; | |
| @override | |
| Iterable<Metric> get secondary => [ | |
| CountMetric('count.HIf')..add(countHIf), | |
| CountMetric('count.HIf.constant')..add(countHIfConstant), | |
| CountMetric('count.HIsTest')..add(countHIsTest), | |
| CountMetric('count.HIsTestSimple')..add(countHIsTestSimple), | |
| CountMetric('count.HIsLateSentinel')..add(countHIsLateSentinel), | |
| CountMetric('count.HGetLength')..add(countHGetLength), | |
| CountMetric('count.HIndex')..add(countHIndex), | |
| CountMetric('count.HFieldGet')..add(countHFieldGet), | |
| CountMetric('count.SingleTargetInstance') | |
| ..add(countSingleTargetInstanceCalls), | |
| countHInterceptor, | |
| countHInterceptorGet, | |
| countHInterceptorConditionalConstant, | |
| countHInterceptorOneshot, | |
| ]; | |
| } | |
| /// Returned by [_expressionType] to tell how code can be generated for | |
| /// a subgraph. | |
| enum _ExpressionCodegenType { | |
| /// The graph must be generated as a statement, which is always possible. | |
| statement, | |
| /// The graph can be generated as an expression, or possibly several | |
| /// comma-separated expressions. | |
| expression, | |
| /// The graph can be generated as an expression, and it only generates | |
| /// expressions of the form | |
| /// variable = expression | |
| /// which are also valid as parts of a "var" declaration. | |
| declaration, | |
| } | |
| class SsaCodeGenerator implements HVisitor<void>, HBlockInformationVisitor { | |
| /// Whether we are currently generating expressions instead of statements. | |
| /// This includes declarations, which are generated as expressions. | |
| bool isGeneratingExpression = false; | |
| final CompilerTask _codegenTask; | |
| final CompilerOptions _options; | |
| final ModularEmitter _emitter; | |
| final RuntimeTypesSubstitutions _rtiSubstitutions; | |
| final RecipeEncoder _rtiRecipeEncoder; | |
| final ModularNamer _namer; | |
| final Tracer _tracer; | |
| final JClosedWorld _closedWorld; | |
| final CodegenRegistry _registry; | |
| final CodegenMetrics _metrics; | |
| final MemberEntity _member; | |
| /// Set of constants that have already been recorded for uses. Shard-global. | |
| /// | |
| /// Similar to the VM's `_ConstantCollector` in | |
| /// `pkg/vm/lib/transformations/record_use/constant_collector.dart`. | |
| final Map<ConstantValue, _RecordedUseSequence> _recordedConstantUsesVisited; | |
| final Set<HInstruction> generateAtUseSite = {}; | |
| final Set<HIf> controlFlowOperators = {}; | |
| final Set<JumpTarget> breakAction = {}; | |
| final Set<LabelDefinition> continueAction = {}; | |
| final Set<JumpTarget> implicitContinueAction = {}; | |
| final List<js.Parameter> parameters = []; | |
| // Using a Block as the current container allows a statement tree to be | |
| // constructed that contains the block, and then have the block filled in | |
| // later. | |
| // TODO(sra): It would be cleaner if the [js.Block] could be unmodifiable but | |
| // that would require deferring the construction of the containing AST. | |
| js.Block currentContainer = js.Block.empty(); | |
| js.Block get body => currentContainer; | |
| List<js.Expression> expressionStack = []; | |
| List<js.Block> oldContainerStack = []; | |
| /// Contains the names of the instructions, as well as the parallel | |
| /// copies to perform on block transitioning. | |
| late VariableNames variableNames; | |
| /// `true` when we need to generate a `var` declaration at function entry, | |
| /// `false` if we can generate a `var` declaration at first assignment in the | |
| /// middle of the function. | |
| bool shouldGroupVarDeclarations = false; | |
| /// While generating expressions, we can't insert variable declarations. | |
| /// Instead we declare them at the start of the function. When minifying | |
| /// we do this most of the time, because it reduces the size unless there | |
| /// is only one variable. | |
| final Set<String> collectedVariableDeclarations = {}; | |
| /// Set of variables and parameters that have already been declared. | |
| final Set<String> declaredLocals = {}; | |
| late HGraph currentGraph; | |
| // Records a block-information that is being handled specially. | |
| // Used to break bad recursion. | |
| HBlockInformation? currentBlockInformation; | |
| // The subgraph is used to delimit traversal for some constructions, e.g., | |
| // if branches. | |
| SubGraph? subGraph; | |
| // Pending blocks than need to be visited as part of current subgraph. | |
| Queue<HBasicBlock>? blockQueue; | |
| SsaCodeGenerator( | |
| this._codegenTask, | |
| this._options, | |
| this._metrics, | |
| this._emitter, | |
| this._rtiSubstitutions, | |
| this._rtiRecipeEncoder, | |
| this._namer, | |
| this._tracer, | |
| this._closedWorld, | |
| this._registry, | |
| this._member, | |
| this._recordedConstantUsesVisited, | |
| ); | |
| JCommonElements get _commonElements => _closedWorld.commonElements; | |
| NativeData get _nativeData => _closedWorld.nativeData; | |
| InterceptorData get _interceptorData => _closedWorld.interceptorData; | |
| AbstractValueDomain get _abstractValueDomain => | |
| _closedWorld.abstractValueDomain; | |
| bool isGenerateAtUseSite(HInstruction instruction) { | |
| return generateAtUseSite.contains(instruction); | |
| } | |
| /// If the [instruction] is not `null` it will be used to attach the position | |
| /// to the [statement]. | |
| void pushStatement(js.Statement statement) { | |
| assert(expressionStack.isEmpty); | |
| currentContainer.statements.add(statement); | |
| } | |
| void insertStatementAtStart(js.Statement statement) { | |
| currentContainer.statements.insert(0, statement); | |
| } | |
| /// If the [instruction] is not `null` it will be used to attach the position | |
| /// to the [expression]. | |
| void pushExpressionAsStatement( | |
| js.Expression expression, | |
| SourceInformation? sourceInformation, | |
| ) { | |
| pushStatement( | |
| js.ExpressionStatement(expression) | |
| .withSourceInformation(sourceInformation), | |
| ); | |
| } | |
| /// If the [instruction] is not `null` it will be used to attach the position | |
| /// to the [expression]. | |
| void push(js.Expression expression) { | |
| expressionStack.add(expression); | |
| } | |
| js.Expression pop() { | |
| return expressionStack.removeLast(); | |
| } | |
| void preGenerateMethod(HGraph graph) { | |
| void runPhase(CodegenPhase phase, {bool traceGraph = true}) { | |
| _codegenTask.measureSubtask(phase.name, () => phase.visitGraph(graph)); | |
| if (traceGraph) { | |
| _tracer.traceGraph(phase.name, graph); | |
| } | |
| assert(graph.isValid(), 'Graph not valid after ${phase.name}'); | |
| } | |
| // Remove trusted late checks first to uncover read-modify-write patterns in | |
| // instruction selection. | |
| runPhase(SsaTrustedLateCheckRemover(_abstractValueDomain)); | |
| runPhase(SsaInstructionSelection(_options, _closedWorld)); | |
| runPhase(SsaTypeKnownRemover()); | |
| runPhase(SsaTrustedPrimitiveCheckRemover(_options)); | |
| runPhase(SsaAssignmentChaining(_closedWorld)); | |
| runPhase(SsaInstructionMerger(_abstractValueDomain, generateAtUseSite)); | |
| runPhase(SsaConditionMerger(generateAtUseSite, controlFlowOperators)); | |
| runPhase(SsaPhiConditioning(generateAtUseSite, controlFlowOperators)); | |
| runPhase(SsaShareRegionConstants()); | |
| SsaLiveIntervalBuilder intervalBuilder = SsaLiveIntervalBuilder( | |
| generateAtUseSite, | |
| controlFlowOperators, | |
| ); | |
| runPhase(intervalBuilder, traceGraph: false); | |
| SsaVariableAllocator allocator = SsaVariableAllocator( | |
| _namer, | |
| intervalBuilder.liveInstructions, | |
| intervalBuilder.liveIntervals, | |
| generateAtUseSite, | |
| ); | |
| runPhase(allocator, traceGraph: false); | |
| variableNames = allocator.names; | |
| shouldGroupVarDeclarations = allocator.names.numberOfVariables > 1; | |
| } | |
| void handleDelayedVariableDeclarations(SourceInformation? sourceInformation) { | |
| // Create 'var' list at the start of function. Move assignment statements | |
| // from the top of the body into the variable initializers. | |
| if (collectedVariableDeclarations.isEmpty) return; | |
| List<js.VariableInitialization> declarations = []; | |
| List<js.Statement> statements = currentContainer.statements; | |
| int nextStatement = 0; | |
| while (nextStatement < statements.length) { | |
| if (collectedVariableDeclarations.isEmpty) break; | |
| js.Statement statement = statements[nextStatement]; | |
| if (statement is js.ExpressionStatement) { | |
| js.Expression expression = statement.expression; | |
| if (expression is js.Assignment && !expression.isCompound) { | |
| js.Expression left = expression.leftHandSide; | |
| if (left is js.VariableReference) { | |
| String name = left.name; | |
| js.Expression value = expression.value; | |
| if (_safeInInitializer(value) && | |
| collectedVariableDeclarations.remove(name)) { | |
| var initialization = js.VariableInitialization( | |
| js.VariableDeclaration(name), | |
| value, | |
| sourceInformation: expression.sourceInformation, | |
| ); | |
| declarations.add(initialization); | |
| ++nextStatement; | |
| continue; | |
| } | |
| } | |
| } | |
| } | |
| break; | |
| } | |
| List<js.VariableInitialization> uninitialized = []; | |
| for (String name in collectedVariableDeclarations) { | |
| uninitialized.add( | |
| js.VariableInitialization(js.VariableDeclaration(name), null), | |
| ); | |
| } | |
| var declarationList = js.VariableDeclarationList( | |
| uninitialized + declarations, | |
| ).withSourceInformation(sourceInformation); | |
| statements.replaceRange(0, nextStatement, [ | |
| js.ExpressionStatement(declarationList), | |
| ]); | |
| } | |
| // An expression is safe to be pulled into a 'var' initializer if it does not | |
| // contain assignments to locals. We don't generate assignments to locals | |
| // inside expressions. | |
| bool _safeInInitializer(js.Expression node) => true; | |
| void visitGraph(HGraph graph) { | |
| preGenerateMethod(graph); | |
| currentGraph = graph; | |
| visitSubGraph(SubGraph(graph.entry, graph.exit)); | |
| handleDelayedVariableDeclarations(graph.sourceInformation); | |
| } | |
| void visitSubGraph(SubGraph? newSubGraph) { | |
| final oldSubGraph = subGraph; | |
| final oldBlockQueue = blockQueue; | |
| subGraph = newSubGraph; | |
| blockQueue = Queue(); | |
| enterSubGraph(subGraph!.start); | |
| blockQueue = oldBlockQueue; | |
| subGraph = oldSubGraph; | |
| } | |
| /// Check whether a sub-graph can be generated as an expression, or even | |
| /// as a declaration, or if it has to fall back to being generated as | |
| /// a statement. | |
| /// Expressions are anything that doesn't generate control flow constructs. | |
| /// Declarations must only generate assignments on the form "id = expression", | |
| /// and not, e.g., expressions where the value isn't assigned, or where it's | |
| /// assigned to something that's not a simple variable. | |
| _ExpressionCodegenType _expressionType(HExpressionInformation info) { | |
| // The only HExpressionInformation used as part of a HBlockInformation is | |
| // current HSubExpressionBlockInformation, so it's the only one reaching | |
| // here. If we start using the other HExpressionInformation types too, | |
| // this code should be generalized. | |
| assert(info is HSubExpressionBlockInformation); | |
| info as HSubExpressionBlockInformation; | |
| HSubExpressionBlockInformation expressionInfo = info; | |
| SubGraph limits = expressionInfo.subExpression!; | |
| // Start assuming that we can generate declarations for simple local | |
| // variables. If we find a counter-example, we degrade our assumption to | |
| // either expression or statement, and in the latter case, we can return | |
| // immediately since it can't get any worse. E.g., a function call where the | |
| // return value isn't used can't be in a declaration. | |
| var result = _ExpressionCodegenType.declaration; | |
| HBasicBlock basicBlock = limits.start; | |
| do { | |
| HInstruction current = basicBlock.first!; | |
| while (current != basicBlock.last) { | |
| // E.g, bounds check. | |
| if (current.isJsStatement()) { | |
| return _ExpressionCodegenType.statement; | |
| } | |
| // HFieldSet generates code on the form "x.y = ...", which isn't valid | |
| // in a declaration. | |
| if (current.usedBy.isEmpty || current is HFieldSet) { | |
| result = _ExpressionCodegenType.expression; | |
| } | |
| current = current.next!; | |
| } | |
| if (current is HGoto) { | |
| basicBlock = basicBlock.successors[0]; | |
| } else if (current is HConditionalBranch) { | |
| if (generateAtUseSite.contains(current)) { | |
| // Short-circuit control flow operator trickery. | |
| // Check the second half, which will continue into the join. | |
| // (The first half is [inputs[0]], the second half is [successors[0]], | |
| // and [successors[1]] is the join-block). | |
| basicBlock = basicBlock.successors[0]; | |
| } else { | |
| // We allow an expression to end on an HIf (a condition expression). | |
| return identical(basicBlock, limits.end) | |
| ? result | |
| : _ExpressionCodegenType.statement; | |
| } | |
| } else { | |
| // Expression-incompatible control flow. | |
| return _ExpressionCodegenType.statement; | |
| } | |
| } while (limits.contains(basicBlock)); | |
| return result; | |
| } | |
| bool isJSExpression(HExpressionInformation info) { | |
| return !identical(_expressionType(info), _ExpressionCodegenType.statement); | |
| } | |
| bool isJSCondition(HExpressionInformation? info) { | |
| // Currently we only handle sub-expression graphs. | |
| info as HSubExpressionBlockInformation; | |
| SubExpression? limits = info.subExpression; | |
| return !identical( | |
| _expressionType(info), | |
| _ExpressionCodegenType.statement, | |
| ) && | |
| (limits!.end.last is HConditionalBranch); | |
| } | |
| /// Generate statements from block information. | |
| /// If the block information contains expressions, generate only | |
| /// assignments, and if it ends in a conditional branch, don't generate | |
| /// the condition. | |
| void generateStatements(HBlockInformation? block) { | |
| if (block is HStatementInformation) { | |
| block.accept(this); | |
| } else if (block is HSubExpressionBlockInformation) { | |
| visitSubGraph(block.subExpression); | |
| } else { | |
| failedAt(currentElementSpannable, 'Unexpected block: $block'); | |
| } | |
| } | |
| js.Block generateStatementsInNewBlock(HBlockInformation? block) { | |
| js.Block result = js.Block.empty(); | |
| js.Block oldContainer = currentContainer; | |
| currentContainer = result; | |
| generateStatements(block); | |
| currentContainer = oldContainer; | |
| return result; | |
| } | |
| /// If the [block] only contains one statement returns that statement. If the | |
| /// that statement itself is a block, recursively calls this method. | |
| /// | |
| /// If the block is empty, returns a new instance of [js.NOP]. | |
| js.Statement unwrapStatement(js.Block block) { | |
| int len = block.statements.length; | |
| if (len == 0) return js.EmptyStatement(); | |
| if (len == 1) { | |
| js.Statement result = block.statements[0]; | |
| if (result is js.Block) return unwrapStatement(result); | |
| return result; | |
| } | |
| return block; | |
| } | |
| /// Generate expressions from block information. | |
| js.Expression? generateExpression(HExpressionInformation expression) { | |
| // Currently we only handle sub-expression graphs. | |
| expression as HSubExpressionBlockInformation; | |
| bool oldIsGeneratingExpression = isGeneratingExpression; | |
| isGeneratingExpression = true; | |
| List<js.Expression> oldExpressionStack = expressionStack; | |
| List<js.Expression> sequenceElements = []; | |
| expressionStack = sequenceElements; | |
| HSubExpressionBlockInformation expressionSubGraph = expression; | |
| visitSubGraph(expressionSubGraph.subExpression); | |
| expressionStack = oldExpressionStack; | |
| isGeneratingExpression = oldIsGeneratingExpression; | |
| if (sequenceElements.isEmpty) { | |
| // Happens when the initializer, condition or update of a loop is empty. | |
| return null; | |
| } else if (sequenceElements.length == 1) { | |
| return sequenceElements[0]; | |
| } else { | |
| js.Expression result = sequenceElements.removeLast(); | |
| while (sequenceElements.isNotEmpty) { | |
| result = js.Binary(',', sequenceElements.removeLast(), result); | |
| } | |
| return result; | |
| } | |
| } | |
| /// Only visits the arguments starting at inputs[HInvoke.argumentsOffset]. | |
| List<js.Expression> visitArguments( | |
| List<HInstruction> inputs, { | |
| int start = HInvoke.argumentsOffset, | |
| }) { | |
| assert(inputs.length >= start); | |
| return List.generate(inputs.length - start, (i) { | |
| use(inputs[i + start]); | |
| return pop(); | |
| }, growable: false); | |
| } | |
| bool isVariableDeclared(String variableName) { | |
| return declaredLocals.contains(variableName) || | |
| collectedVariableDeclarations.contains(variableName); | |
| } | |
| js.Expression generateExpressionAssignment( | |
| String variableName, | |
| js.Expression value, | |
| SourceInformation? sourceInformation, | |
| ) { | |
| // TODO(johnniwinther): Introduce a DeferredVariableUse to handle this | |
| // in the SSA codegen or let the JS printer handle it fully and remove it | |
| // here. | |
| if (value is js.Binary) { | |
| js.Binary binary = value; | |
| String op = binary.op; | |
| if (op == '+' || | |
| op == '-' || | |
| op == '/' || | |
| op == '*' || | |
| op == '%' || | |
| op == '^' || | |
| op == '&' || | |
| op == '|') { | |
| js.Expression left = binary.left; | |
| if (left is js.VariableUse && left.name == variableName) { | |
| // We know now, that we can shorten x = x + y into x += y. | |
| // Also check for the shortcut where y equals 1: x++ and x--. | |
| js.Expression right = binary.right; | |
| if ((op == '+' || op == '-') && | |
| right is js.LiteralNumber && | |
| right.value == "1") { | |
| return js.Prefix(op == '+' ? '++' : '--', left); | |
| } | |
| return js.Assignment.compound(binary.left, op, binary.right); | |
| } | |
| } | |
| } | |
| return js.Assignment( | |
| js.VariableUse(variableName), | |
| value, | |
| ).withSourceInformation(value.sourceInformation ?? sourceInformation); | |
| } | |
| void assignVariable( | |
| String variableName, | |
| js.Expression value, | |
| SourceInformation? sourceInformation, | |
| ) { | |
| if (isGeneratingExpression) { | |
| // If we are in an expression then we can't declare the variable here. | |
| // We have no choice, but to use it and then declare it separately. | |
| if (!isVariableDeclared(variableName)) { | |
| collectedVariableDeclarations.add(variableName); | |
| } | |
| push( | |
| generateExpressionAssignment(variableName, value, sourceInformation), | |
| ); | |
| // Otherwise if we are trying to declare inline and we are in a statement | |
| // then we declare (unless it was already declared). | |
| } else if (!shouldGroupVarDeclarations && | |
| !declaredLocals.contains(variableName)) { | |
| // It may be necessary to remove it from the ones to be declared later. | |
| collectedVariableDeclarations.remove(variableName); | |
| declaredLocals.add(variableName); | |
| js.VariableDeclaration decl = js.VariableDeclaration(variableName); | |
| js.VariableInitialization initialization = js.VariableInitialization( | |
| decl, | |
| value, | |
| ); | |
| pushExpressionAsStatement( | |
| js.VariableDeclarationList([initialization]), | |
| sourceInformation, | |
| ); | |
| } else { | |
| // Otherwise we are just going to use it. If we have not already declared | |
| // it then we make sure we will declare it later. | |
| if (!declaredLocals.contains(variableName)) { | |
| collectedVariableDeclarations.add(variableName); | |
| } | |
| pushExpressionAsStatement( | |
| generateExpressionAssignment(variableName, value, sourceInformation), | |
| sourceInformation, | |
| ); | |
| } | |
| } | |
| void define(HInstruction instruction) { | |
| // For simple type checks like i = intTypeCheck(i), we don't have to | |
| // emit an assignment, because the intTypeCheck just returns its | |
| // argument. | |
| bool needsAssignment = true; | |
| if (instruction is HOutputConstrainedToAnInput) { | |
| if (instruction is HPrimitiveCheck || | |
| instruction is HAsCheck || | |
| instruction is HAsCheckSimple || | |
| instruction is HNullCheck || | |
| instruction is HLateReadCheck || | |
| instruction is HArrayFlagsSet) { | |
| String? inputName = variableNames.getName(instruction.constrainedInput); | |
| if (variableNames.getName(instruction) == inputName) { | |
| needsAssignment = false; | |
| } | |
| } | |
| } | |
| if (instruction is HLocalValue) { | |
| needsAssignment = false; | |
| } | |
| if (needsAssignment && | |
| !instruction.isJsStatement() && | |
| variableNames.hasName(instruction)) { | |
| visitExpression(instruction); | |
| assignVariable( | |
| variableNames.getName(instruction)!, | |
| pop(), | |
| instruction.sourceInformation, | |
| ); | |
| return; | |
| } | |
| if (isGeneratingExpression) { | |
| visitExpression(instruction); | |
| } else { | |
| visitStatement(instruction); | |
| } | |
| } | |
| HInstruction skipGenerateAtUseCheckInputs(HOutputConstrainedToAnInput check) { | |
| HInstruction input = check.constrainedInput; | |
| if (input is HOutputConstrainedToAnInput && isGenerateAtUseSite(input)) { | |
| return skipGenerateAtUseCheckInputs(input); | |
| } | |
| return input; | |
| } | |
| void use(HInstruction argument) { | |
| if (isGenerateAtUseSite(argument)) { | |
| visitExpression(argument); | |
| } else if (argument is HOutputConstrainedToAnInput && | |
| !variableNames.hasName(argument)) { | |
| // We have a check that is not generate-at-use and has no name, yet is a | |
| // subexpression (we are in 'use'). This happens when we have a chain of | |
| // checks on an available unnamed value (e.g. a constant). The checks are | |
| // generated as a statement, so we need to skip the generate-at-use check | |
| // tree to find the underlying value. | |
| // TODO(sra): We should ensure that this invariant holds: "every | |
| // instruction has a name or is generate-at-use". This would require | |
| // naming the input or output of the chain-of-checks. | |
| // This can only happen if the checked node also does not have a name. | |
| assert(!variableNames.hasName(argument.constrainedInput)); | |
| use(skipGenerateAtUseCheckInputs(argument)); | |
| } else { | |
| assert(variableNames.hasName(argument)); | |
| push(js.VariableUse(variableNames.getName(argument)!)); | |
| } | |
| } | |
| void visit(HInstruction node) { | |
| node.accept(this); | |
| } | |
| void visitExpression(HInstruction node) { | |
| bool oldIsGeneratingExpression = isGeneratingExpression; | |
| isGeneratingExpression = true; | |
| visit(node); | |
| isGeneratingExpression = oldIsGeneratingExpression; | |
| } | |
| void visitStatement(HInstruction node) { | |
| assert(!isGeneratingExpression); | |
| visit(node); | |
| if (expressionStack.isNotEmpty) { | |
| assert(expressionStack.length == 1); | |
| js.Expression expression = pop(); | |
| pushExpressionAsStatement(expression, node.sourceInformation); | |
| } | |
| } | |
| void continueAsBreak(LabelDefinition target) { | |
| pushStatement(js.Break(_namer.continueLabelName(target))); | |
| } | |
| void implicitContinueAsBreak(JumpTarget target) { | |
| pushStatement(js.Break(_namer.implicitContinueLabelName(target))); | |
| } | |
| void implicitBreakWithLabel(JumpTarget target) { | |
| pushStatement(js.Break(_namer.implicitBreakLabelName(target))); | |
| } | |
| js.Statement wrapIntoLabels( | |
| js.Statement result, | |
| List<LabelDefinition> labels, | |
| ) { | |
| for (LabelDefinition label in labels) { | |
| if (label.isTarget) { | |
| String breakLabelString = _namer.breakLabelName(label); | |
| result = js.LabeledStatement(breakLabelString, result); | |
| } | |
| } | |
| return result; | |
| } | |
| // The regular [visitIf] method implements the needed logic. | |
| @override | |
| bool visitIfInfo(HIfBlockInformation info) => false; | |
| @override | |
| bool visitSwitchInfo(HSwitchBlockInformation info) { | |
| bool isExpression = isJSExpression(info.expression); | |
| if (!isExpression) { | |
| generateStatements(info.expression); | |
| } | |
| if (isExpression) { | |
| push(generateExpression(info.expression)!); | |
| } else { | |
| use(info.expression.conditionExpression!); | |
| } | |
| js.Expression key = pop(); | |
| bool handledDefault = false; | |
| List<js.SwitchClause> cases = []; | |
| HSwitch switchInstruction = info.expression.end.last as HSwitch; | |
| List<HInstruction> inputs = switchInstruction.inputs; | |
| List<HBasicBlock> successors = switchInstruction.block!.successors; | |
| js.Block oldContainer = currentContainer; | |
| for ( | |
| int inputIndex = 1, statementIndex = 0; | |
| inputIndex < inputs.length; | |
| statementIndex++ | |
| ) { | |
| HBasicBlock successor = successors[inputIndex - 1]; | |
| // If liveness analysis has figured out that this case is dead, | |
| // omit the code for it. | |
| if (successor.isLive) { | |
| do { | |
| final input = inputs[inputIndex]; | |
| visit(input); | |
| currentContainer = js.Block.empty(); | |
| cases.add(js.Case(pop(), currentContainer)); | |
| if (input is HConstant && input.constant is NullConstantValue) { | |
| // JavaScript case expressions match on `===`, which means that the | |
| // just emitted `case null:` will not catch `undefined`. | |
| // Add `case void 0:` to catch `undefined`. | |
| currentContainer = js.Block.empty(); | |
| cases.add( | |
| js.Case(js.Prefix('void', js.number(0)), currentContainer), | |
| ); | |
| } | |
| inputIndex++; | |
| } while ((successors[inputIndex - 1] == successor) && | |
| (inputIndex < inputs.length)); | |
| // If this is the last statement, then these cases also belong to the | |
| // default block. | |
| if (statementIndex == info.statements.length - 1) { | |
| currentContainer = js.Block.empty(); | |
| cases.add(js.Default(currentContainer)); | |
| handledDefault = true; | |
| } | |
| generateStatements(info.statements[statementIndex]); | |
| } else { | |
| // Skip all the case statements that belong to this | |
| // block. | |
| while ((successors[inputIndex - 1] == successor) && | |
| (inputIndex < inputs.length)) { | |
| ++inputIndex; | |
| } | |
| } | |
| } | |
| // If the default case is dead, we omit it. Likewise, if it is an | |
| // empty block, we omit it, too. | |
| if (info.statements.last.start.isLive && !handledDefault) { | |
| currentContainer = js.Block.empty(); | |
| generateStatements(info.statements.last); | |
| if (currentContainer.statements.isNotEmpty) { | |
| cases.add(js.Default(currentContainer)); | |
| } | |
| } | |
| currentContainer = oldContainer; | |
| js.Statement result = js.Switch( | |
| key, | |
| cases, | |
| ).withSourceInformation(info.sourceInformation); | |
| pushStatement(wrapIntoLabels(result, info.labels)); | |
| return true; | |
| } | |
| @override | |
| bool visitSequenceInfo(HStatementSequenceInformation info) { | |
| return false; | |
| } | |
| @override | |
| bool visitSubGraphInfo(HSubGraphBlockInformation info) { | |
| visitSubGraph(info.subGraph); | |
| return true; | |
| } | |
| @override | |
| bool visitSubExpressionInfo(HSubExpressionBlockInformation info) { | |
| return false; | |
| } | |
| @override | |
| bool visitTryInfo(HTryBlockInformation info) { | |
| js.Block body = generateStatementsInNewBlock(info.body); | |
| js.Catch? catchPart; | |
| js.Block? finallyPart; | |
| if (info.catchBlock != null) { | |
| void register(ClassEntity classElement) { | |
| _registry | |
| // ignore:deprecated_member_use_from_same_package | |
| .registerInstantiatedClass(classElement); | |
| } | |
| register(_commonElements.jsPlainJavaScriptObjectClass); | |
| register(_commonElements.jsUnknownJavaScriptObjectClass); | |
| HLocalValue? exception = info.catchVariable; | |
| String name = variableNames.getName(exception)!; | |
| js.VariableDeclaration decl = js.VariableDeclaration(name); | |
| js.Block catchBlock = generateStatementsInNewBlock(info.catchBlock); | |
| catchPart = js.Catch(decl, catchBlock); | |
| } | |
| if (info.finallyBlock != null) { | |
| finallyPart = generateStatementsInNewBlock(info.finallyBlock); | |
| } | |
| pushStatement(js.Try(body, catchPart, finallyPart)); | |
| return true; | |
| } | |
| void visitBodyIgnoreLabels(HLoopBlockInformation info) { | |
| if (info.body!.start.isLabeledBlock()) { | |
| HBlockInformation? oldInfo = currentBlockInformation; | |
| currentBlockInformation = info.body!.start.blockFlow!.body; | |
| generateStatements(info.body); | |
| currentBlockInformation = oldInfo; | |
| } else { | |
| generateStatements(info.body); | |
| } | |
| } | |
| @override | |
| bool visitLoopInfo(HLoopBlockInformation info) { | |
| HExpressionInformation? condition = info.condition; | |
| bool isConditionExpression = isJSCondition(condition); | |
| late js.Loop loop; | |
| switch (info.kind) { | |
| // Treat all three "test-first" loops the same way. | |
| case LoopBlockInformationKind.forLoop: | |
| case LoopBlockInformationKind.whileLoop: | |
| case LoopBlockInformationKind.forInLoop: | |
| case LoopBlockInformationKind.switchContinueLoop: | |
| HExpressionInformation? initialization = info.initializer; | |
| var initializationType = _ExpressionCodegenType.statement; | |
| if (initialization != null) { | |
| initializationType = _expressionType(initialization); | |
| if (initializationType == _ExpressionCodegenType.statement) { | |
| generateStatements(initialization); | |
| initialization = null; | |
| } | |
| } | |
| // We inserted a basic block to avoid critical edges. This block is | |
| // part of the LoopBlockInformation and must therefore be handled here. | |
| js.Block oldContainer = currentContainer; | |
| js.Block avoidContainer = js.Block.empty(); | |
| currentContainer = avoidContainer; | |
| assignPhisOfSuccessors(condition!.end.successors.last); | |
| bool hasPhiUpdates = avoidContainer.statements.isNotEmpty; | |
| currentContainer = oldContainer; | |
| if (isConditionExpression && | |
| !hasPhiUpdates && | |
| info.updates != null && | |
| isJSExpression(info.updates!)) { | |
| // If we have an updates graph, and it's expressible as an | |
| // expression, generate a for-loop. | |
| js.Expression? jsInitialization; | |
| if (initialization != null) { | |
| int delayedVariablesCount = collectedVariableDeclarations.length; | |
| jsInitialization = generateExpression(initialization); | |
| if (!shouldGroupVarDeclarations && | |
| delayedVariablesCount < collectedVariableDeclarations.length) { | |
| // We just added a new delayed variable-declaration. See if we can | |
| // put in a 'var' in front of the initialization to make it go | |
| // away. We walk the 'tree' of comma-operators to find the | |
| // expressions and see if they are all assignments that can be | |
| // converted into declarations. | |
| List<js.Assignment>? assignments; | |
| bool allSimpleAssignments(js.Expression expression) { | |
| if (expression is js.Assignment) { | |
| js.Assignment assignment = expression; | |
| if (assignment.leftHandSide is js.VariableUse && | |
| !assignment.isCompound) { | |
| (assignments ??= []).add(expression); | |
| return true; | |
| } | |
| } else if (expression is js.Binary && | |
| expression.isCommaOperator) { | |
| return allSimpleAssignments(expression.left) && | |
| allSimpleAssignments(expression.right); | |
| } | |
| return false; | |
| } | |
| if (jsInitialization != null && | |
| allSimpleAssignments(jsInitialization)) { | |
| List<js.VariableInitialization> inits = []; | |
| for (js.Assignment assignment in assignments!) { | |
| final id = (assignment.leftHandSide as js.VariableUse).name; | |
| final declaration = js.VariableDeclaration(id); | |
| inits.add( | |
| js.VariableInitialization(declaration, assignment.value), | |
| ); | |
| collectedVariableDeclarations.remove(id); | |
| declaredLocals.add(id); | |
| } | |
| jsInitialization = js.VariableDeclarationList(inits); | |
| } | |
| } | |
| } | |
| js.Expression? jsCondition = generateExpression(condition); | |
| js.Expression? jsUpdates = generateExpression(info.updates!); | |
| // The body might be labeled. Ignore this when recursing on the | |
| // subgraph. | |
| // TODO(lrn): Remove this extra labeling when handling all loops | |
| // using subgraphs. | |
| oldContainer = currentContainer; | |
| js.Block body = js.Block.empty(); | |
| currentContainer = body; | |
| visitBodyIgnoreLabels(info); | |
| currentContainer = oldContainer; | |
| loop = newLoop( | |
| init: jsInitialization, | |
| condition: jsCondition, | |
| update: jsUpdates, | |
| body: unwrapStatement(body), | |
| sourceInformation: info.sourceInformation, | |
| ); | |
| } else { | |
| // We have either no update graph, or it's too complex to | |
| // put in an expression. | |
| if (initialization != null) { | |
| generateStatements(initialization); | |
| } | |
| js.Expression? jsCondition; | |
| js.Block oldContainer = currentContainer; | |
| js.Block body = js.Block.empty(); | |
| if (isConditionExpression && !hasPhiUpdates) { | |
| jsCondition = generateExpression(condition); | |
| currentContainer = body; | |
| } else { | |
| currentContainer = body; | |
| generateStatements(condition); | |
| use(condition.conditionExpression!); | |
| js.Expression ifTest = js.Prefix("!", pop()); | |
| js.Statement jsBreak = js.Break(null); | |
| js.Statement exitLoop; | |
| if (avoidContainer.statements.isEmpty) { | |
| exitLoop = jsBreak; | |
| } else { | |
| avoidContainer.statements.add(jsBreak); | |
| exitLoop = avoidContainer; | |
| } | |
| pushStatement(js.If.noElse(ifTest, exitLoop)); | |
| } | |
| if (info.updates != null) { | |
| wrapLoopBodyForContinue(info); | |
| generateStatements(info.updates); | |
| } else { | |
| visitBodyIgnoreLabels(info); | |
| } | |
| currentContainer = oldContainer; | |
| loop = newLoop( | |
| condition: jsCondition, | |
| body: unwrapStatement(body), | |
| sourceInformation: info.sourceInformation, | |
| ); | |
| } | |
| break; | |
| case LoopBlockInformationKind.doWhileLoop: | |
| if (info.initializer != null) { | |
| generateStatements(info.initializer); | |
| } | |
| // We inserted a basic block to avoid critical edges. This block is | |
| // part of the LoopBlockInformation and must therefore be handled here. | |
| js.Block oldContainer = currentContainer; | |
| js.Block exitAvoidContainer = js.Block.empty(); | |
| currentContainer = exitAvoidContainer; | |
| assignPhisOfSuccessors(condition!.end.successors.last); | |
| bool hasExitPhiUpdates = exitAvoidContainer.statements.isNotEmpty; | |
| currentContainer = oldContainer; | |
| oldContainer = currentContainer; | |
| js.Block body = js.Block.empty(); | |
| // If there are phi copies in the block that jumps to the | |
| // loop entry, we must emit the condition like this: | |
| // do { | |
| // body; | |
| // if (condition) { | |
| // phi updates; | |
| // continue; | |
| // } else { | |
| // break; | |
| // } | |
| // } while (true); | |
| HBasicBlock avoidEdge = info.end.successors[0]; | |
| js.Block updateBody = js.Block.empty(); | |
| currentContainer = updateBody; | |
| assignPhisOfSuccessors(avoidEdge); | |
| bool hasPhiUpdates = updateBody.statements.isNotEmpty; | |
| currentContainer = body; | |
| visitBodyIgnoreLabels(info); | |
| if (info.updates != null) { | |
| generateStatements(info.updates); | |
| } | |
| js.Expression? jsCondition; | |
| if (isConditionExpression) { | |
| jsCondition = generateExpression(condition); | |
| } else { | |
| generateStatements(condition); | |
| use(condition.conditionExpression!); | |
| jsCondition = pop(); | |
| } | |
| if (jsCondition == null) { | |
| // If the condition is dead code, we turn the do-while into | |
| // a simpler while because we will never reach the condition | |
| // at the end of the loop anyway. | |
| loop = newLoop( | |
| body: unwrapStatement(body), | |
| sourceInformation: info.sourceInformation, | |
| ); | |
| } else { | |
| if (hasPhiUpdates || hasExitPhiUpdates) { | |
| updateBody.statements.add(js.Continue(null)); | |
| js.Statement jsBreak = js.Break(null); | |
| js.Statement exitLoop; | |
| if (exitAvoidContainer.statements.isEmpty) { | |
| exitLoop = jsBreak; | |
| } else { | |
| exitAvoidContainer.statements.add(jsBreak); | |
| exitLoop = exitAvoidContainer; | |
| } | |
| body.statements.add(js.If(jsCondition, updateBody, exitLoop)); | |
| jsCondition = newLiteralBool(true, info.sourceInformation); | |
| } | |
| loop = js.Do( | |
| unwrapStatement(body), | |
| jsCondition, | |
| sourceInformation: info.sourceInformation, | |
| ); | |
| } | |
| currentContainer = oldContainer; | |
| break; | |
| case LoopBlockInformationKind.notALoop: | |
| failedAt( | |
| condition!.conditionExpression!, | |
| 'Unexpected loop kind: ${info.kind}.', | |
| ); | |
| } | |
| js.Statement result = loop; | |
| if (info.kind == LoopBlockInformationKind.switchContinueLoop) { | |
| String continueLabelString = _namer.implicitContinueLabelName( | |
| info.target!, | |
| ); | |
| result = js.LabeledStatement(continueLabelString, result); | |
| } | |
| pushStatement(wrapIntoLabels(result, info.labels)); | |
| return true; | |
| } | |
| @override | |
| bool visitLabeledBlockInfo(HLabeledBlockInformation labeledBlockInfo) { | |
| Link<Entity> continueOverrides = const Link<Entity>(); | |
| js.Block oldContainer = currentContainer; | |
| js.Block body = js.Block.empty(); | |
| js.Statement result = body; | |
| currentContainer = body; | |
| // If [labeledBlockInfo.isContinue], the block is an artificial | |
| // block around the body of a loop with an update block, so that | |
| // continues of the loop can be written as breaks of the body | |
| // block. | |
| if (labeledBlockInfo.isContinue) { | |
| for (LabelDefinition label in labeledBlockInfo.labels) { | |
| if (label.isContinueTarget) { | |
| String labelName = _namer.continueLabelName(label); | |
| result = js.LabeledStatement(labelName, result); | |
| continueAction.add(label); | |
| continueOverrides = continueOverrides.prepend(label); | |
| } | |
| } | |
| // For handling unlabeled continues from the body of a loop. | |
| // TODO(lrn): Consider recording whether the target is in fact | |
| // a target of an unlabeled continue, and not generate this if it isn't. | |
| JumpTarget target = labeledBlockInfo.target!; | |
| String labelName = _namer.implicitContinueLabelName(target); | |
| result = js.LabeledStatement(labelName, result); | |
| implicitContinueAction.add(target); | |
| continueOverrides = continueOverrides.prepend(target); | |
| } else { | |
| for (LabelDefinition label in labeledBlockInfo.labels) { | |
| if (label.isBreakTarget) { | |
| String labelName = _namer.breakLabelName(label); | |
| result = js.LabeledStatement(labelName, result); | |
| } | |
| } | |
| } | |
| JumpTarget target = labeledBlockInfo.target!; | |
| if (target.isSwitch) { | |
| // This is an extra block around a switch that is generated | |
| // as a nested if/else chain. We add an extra break target | |
| // so that case code can break. | |
| String labelName = _namer.implicitBreakLabelName(target); | |
| result = js.LabeledStatement(labelName, result); | |
| breakAction.add(target); | |
| } | |
| currentContainer = body; | |
| generateStatements(labeledBlockInfo.body); | |
| if (labeledBlockInfo.isContinue) { | |
| while (continueOverrides.isNotEmpty) { | |
| continueAction.remove(continueOverrides.head); | |
| implicitContinueAction.remove(continueOverrides.head); | |
| continueOverrides = continueOverrides.tail!; | |
| } | |
| } else { | |
| breakAction.remove(labeledBlockInfo.target); | |
| } | |
| currentContainer = oldContainer; | |
| if (body.statements.isNotEmpty) { | |
| pushStatement(result); | |
| } | |
| return true; | |
| } | |
| // Wraps a loop body in a block to make continues have a target to break | |
| // to (if necessary). | |
| void wrapLoopBodyForContinue(HLoopBlockInformation info) { | |
| JumpTarget? target = info.target; | |
| if (target != null && target.isContinueTarget) { | |
| js.Block oldContainer = currentContainer; | |
| js.Block body = js.Block.empty(); | |
| currentContainer = body; | |
| js.Statement result = body; | |
| for (LabelDefinition label in info.labels) { | |
| if (label.isContinueTarget) { | |
| String labelName = _namer.continueLabelName(label); | |
| result = js.LabeledStatement(labelName, result); | |
| continueAction.add(label); | |
| } | |
| } | |
| String labelName = _namer.implicitContinueLabelName(target); | |
| result = js.LabeledStatement(labelName, result); | |
| implicitContinueAction.add(target); | |
| visitBodyIgnoreLabels(info); | |
| implicitContinueAction.remove(target); | |
| for (LabelDefinition label in info.labels) { | |
| if (label.isContinueTarget) { | |
| continueAction.remove(label); | |
| } | |
| } | |
| currentContainer = oldContainer; | |
| pushStatement(result); | |
| } else { | |
| // Loop body contains no continues, so we don't need a break target. | |
| generateStatements(info.body); | |
| } | |
| } | |
| bool handleBlockFlow(HBlockFlow block) { | |
| HBlockInformation info = block.body; | |
| // If we reach here again while handling the attached information, | |
| // e.g., because we call visitSubGraph on a subgraph starting on | |
| // the same block, don't handle it again. | |
| // When the structure graph is complete, we will be able to have | |
| // different structures starting on the same basic block (e.g., an | |
| // "if" and its condition). | |
| if (identical(info, currentBlockInformation)) return false; | |
| HBlockInformation? oldBlockInformation = currentBlockInformation; | |
| currentBlockInformation = info; | |
| bool success = info.accept(this); | |
| currentBlockInformation = oldBlockInformation; | |
| if (success) { | |
| HBasicBlock? continuation = block.continuation; | |
| if (continuation != null) { | |
| continueSubGraph(continuation); | |
| } | |
| } | |
| return success; | |
| } | |
| void enterSubGraph(HBasicBlock node) { | |
| assert(blockQueue!.isEmpty); | |
| continueSubGraph(node); | |
| while (blockQueue!.isNotEmpty) { | |
| node = blockQueue!.removeFirst(); | |
| assert(node.isLive); | |
| assert(subGraph!.contains(node)); | |
| // If this node has block-structure based information attached, | |
| // try using that to traverse from here. | |
| if (node.blockFlow != null && handleBlockFlow(node.blockFlow!)) { | |
| continue; | |
| } | |
| iterateBasicBlock(node); | |
| } | |
| } | |
| void continueSubGraph(HBasicBlock node) { | |
| if (!node.isLive) return; | |
| // Don't follow edges out of the current sub-graph. | |
| if (!subGraph!.contains(node)) return; | |
| blockQueue!.add(node); | |
| } | |
| void emitAssignment( | |
| String destination, | |
| String source, | |
| SourceInformation? sourceInformation, | |
| ) { | |
| assignVariable(destination, js.VariableUse(source), sourceInformation); | |
| } | |
| /// Sequentialize a list of conceptually parallel copies. Parallel | |
| /// copies may contain cycles, that this method breaks. | |
| void sequentializeCopies( | |
| Iterable<Copy<HInstruction>> instructionCopies, | |
| String tempName, | |
| void Function( | |
| String target, | |
| String source, | |
| SourceInformation? sourceInformation, | |
| ) | |
| doAssignment, | |
| ) { | |
| Map<String, SourceInformation?> sourceInformationMap = {}; | |
| // Map the instructions to strings. | |
| Iterable<Copy<String>> copies = instructionCopies.map(( | |
| Copy<HInstruction> copy, | |
| ) { | |
| String sourceName = variableNames.getName(copy.source)!; | |
| sourceInformationMap[sourceName] = copy.source.sourceInformation; | |
| String destinationName = variableNames.getName(copy.destination)!; | |
| sourceInformationMap[sourceName] = copy.destination.sourceInformation; | |
| return Copy<String>(sourceName, destinationName); | |
| }); | |
| // Map to keep track of the current location (ie the variable that | |
| // holds the initial value) of a variable. | |
| Map<String, String> currentLocation = {}; | |
| // Map to keep track of the initial value of a variable. | |
| Map<String, String> initialValue = {}; | |
| // List of variables to assign a value. | |
| List<String> worklist = []; | |
| // List of variables that we can assign a value to (ie are not | |
| // being used anymore). | |
| List<String> ready = []; | |
| // Prune [copies] by removing self-copies. | |
| List<Copy<String>> prunedCopies = []; | |
| for (Copy<String> copy in copies) { | |
| if (copy.source != copy.destination) { | |
| prunedCopies.add(copy); | |
| } | |
| } | |
| copies = prunedCopies; | |
| // For each copy, set the current location of the source to | |
| // itself, and the initial value of the destination to the source. | |
| // Add the destination to the list of copies to make. | |
| for (Copy<String> copy in copies) { | |
| currentLocation[copy.source] = copy.source; | |
| initialValue[copy.destination] = copy.source; | |
| worklist.add(copy.destination); | |
| } | |
| // For each copy, if the destination does not have a current | |
| // location, then we can safely assign to it. | |
| for (Copy<String> copy in copies) { | |
| if (currentLocation[copy.destination] == null) { | |
| ready.add(copy.destination); | |
| } | |
| } | |
| while (worklist.isNotEmpty) { | |
| while (ready.isNotEmpty) { | |
| String destination = ready.removeLast(); | |
| String source = initialValue[destination]!; | |
| // Since [source] might have been updated, use the current | |
| // location of [source] | |
| String copy = currentLocation[source]!; | |
| doAssignment( | |
| destination, | |
| copy, | |
| sourceInformationMap[copy] ?? sourceInformationMap[destination], | |
| ); | |
| // Now [destination] is the current location of [source]. | |
| currentLocation[source] = destination; | |
| // If [source] hasn't been updated and needs to have a value, | |
| // add it to the list of variables that can be updated. Copies | |
| // of [source] will now use [destination]. | |
| if (source == copy && initialValue[source] != null) { | |
| ready.add(source); | |
| } | |
| } | |
| // Check if we have a cycle. | |
| String current = worklist.removeLast(); | |
| // If [current] is used as a source, and the assignment has been | |
| // done, we are done with this variable. Otherwise there is a | |
| // cycle that we break by using a temporary name. | |
| if (currentLocation[current] != null && | |
| current != currentLocation[initialValue[current]]) { | |
| doAssignment(tempName, current, sourceInformationMap[current]); | |
| currentLocation[current] = tempName; | |
| // [current] can now be safely updated. Copies of [current] | |
| // will now use [tempName]. | |
| ready.add(current); | |
| } | |
| } | |
| } | |
| void assignPhisOfSuccessors(HBasicBlock node) { | |
| CopyHandler? handler = variableNames.getCopyHandler(node); | |
| if (handler == null) return; | |
| sequentializeCopies( | |
| handler.copies, | |
| variableNames.getSwapTemp(), | |
| emitAssignment, | |
| ); | |
| for (Copy<HInstruction> copy in handler.assignments) { | |
| String name = variableNames.getName(copy.destination)!; | |
| use(copy.source); | |
| assignVariable( | |
| name, | |
| pop(), | |
| copy.source.sourceInformation ?? copy.destination.sourceInformation, | |
| ); | |
| } | |
| } | |
| void iterateBasicBlock(HBasicBlock node) { | |
| HInstruction instruction = node.first!; | |
| while (!identical(instruction, node.last)) { | |
| if (!isGenerateAtUseSite(instruction)) { | |
| define(instruction); | |
| } | |
| instruction = instruction.next!; | |
| } | |
| assignPhisOfSuccessors(node); | |
| visit(instruction); | |
| } | |
| void handleInvokeBinary( | |
| HInvokeBinary node, | |
| String op, | |
| SourceInformation? sourceInformation, | |
| ) { | |
| use(node.left); | |
| js.Expression jsLeft = pop(); | |
| use(node.right); | |
| push(js.Binary(op, jsLeft, pop()).withSourceInformation(sourceInformation)); | |
| } | |
| @override | |
| void visitLateValue(HLateValue node) { | |
| use(node.target); | |
| } | |
| void visitInvokeBinary(HInvokeBinary node, String op) { | |
| handleInvokeBinary(node, op, node.sourceInformation); | |
| } | |
| void visitRelational(HRelational node, String op) { | |
| handleInvokeBinary(node, op, node.sourceInformation); | |
| } | |
| // We want the outcome of bit-operations to be positive. We use the unsigned | |
| // shift operator to achieve this. | |
| void convertBitOpResultToUnsigned(HInstruction node) { | |
| push( | |
| js.Binary( | |
| ">>>", | |
| pop(), | |
| js.LiteralNumber("0"), | |
| ).withSourceInformation(node.sourceInformation), | |
| ); | |
| } | |
| void visitBitInvokeBinary(HBinaryBitOp node, String op) { | |
| visitInvokeBinary(node, op); | |
| if (node.requiresUintConversion) convertBitOpResultToUnsigned(node); | |
| } | |
| void visitInvokeUnary(HInvokeUnary node, String op) { | |
| use(node.operand); | |
| push(js.Prefix(op, pop()).withSourceInformation(node.sourceInformation)); | |
| } | |
| void emitIdentityComparison( | |
| HIdentity instruction, | |
| SourceInformation? sourceInformation, { | |
| bool inverse = false, | |
| }) { | |
| String? op = instruction.singleComparisonOp; | |
| HInstruction left = instruction.left; | |
| HInstruction right = instruction.right; | |
| if (op != null) { | |
| use(left); | |
| js.Expression jsLeft = pop(); | |
| use(right); | |
| push( | |
| js.Binary( | |
| mapRelationalOperator(op, inverse), | |
| jsLeft, | |
| pop(), | |
| ).withSourceInformation(sourceInformation), | |
| ); | |
| } else { | |
| assert(NullConstantValue.jsNull == 'null'); | |
| use(left); | |
| js.Binary leftEqualsNull = js.Binary("==", pop(), js.LiteralNull()); | |
| use(right); | |
| js.Binary rightEqualsNull = js.Binary( | |
| mapRelationalOperator("==", inverse), | |
| pop(), | |
| js.LiteralNull(), | |
| ); | |
| use(right); | |
| use(left); | |
| js.Binary tripleEq = js.Binary( | |
| mapRelationalOperator("===", inverse), | |
| pop(), | |
| pop(), | |
| ); | |
| push( | |
| js.Conditional( | |
| leftEqualsNull, | |
| rightEqualsNull, | |
| tripleEq, | |
| ).withSourceInformation(sourceInformation), | |
| ); | |
| } | |
| } | |
| @override | |
| void visitIdentity(HIdentity node) { | |
| emitIdentityComparison(node, node.sourceInformation, inverse: false); | |
| } | |
| @override | |
| void visitAdd(HAdd node) => visitInvokeBinary(node, '+'); | |
| @override | |
| void visitDivide(HDivide node) => visitInvokeBinary(node, '/'); | |
| @override | |
| void visitMultiply(HMultiply node) => visitInvokeBinary(node, '*'); | |
| @override | |
| void visitSubtract(HSubtract node) => visitInvokeBinary(node, '-'); | |
| @override | |
| void visitBitAnd(HBitAnd node) => visitBitInvokeBinary(node, '&'); | |
| @override | |
| void visitBitOr(HBitOr node) => visitBitInvokeBinary(node, '|'); | |
| @override | |
| void visitBitXor(HBitXor node) => visitBitInvokeBinary(node, '^'); | |
| @override | |
| void visitShiftLeft(HShiftLeft node) => visitBitInvokeBinary(node, '<<'); | |
| @override | |
| void visitShiftRight(HShiftRight node) => visitBitInvokeBinary(node, '>>>'); | |
| @override | |
| void visitBitNot(HBitNot node) { | |
| visitInvokeUnary(node, '~'); | |
| if (node.requiresUintConversion) convertBitOpResultToUnsigned(node); | |
| } | |
| @override | |
| void visitTruncatingDivide(HTruncatingDivide node) { | |
| assert(node.isUInt31(_abstractValueDomain).isDefinitelyTrue); | |
| // TODO(karlklose): Enable this assertion again when type propagation is | |
| // fixed. Issue 23555. | |
| // assert(node.left.isUInt32(compiler)); | |
| assert(node.right.isPositiveInteger(_abstractValueDomain).isDefinitelyTrue); | |
| use(node.left); | |
| js.Expression jsLeft = pop(); | |
| use(node.right); | |
| push( | |
| js.Binary( | |
| '/', | |
| jsLeft, | |
| pop(), | |
| ).withSourceInformation(node.sourceInformation), | |
| ); | |
| push( | |
| js.Binary( | |
| '|', | |
| pop(), | |
| js.LiteralNumber("0"), | |
| ).withSourceInformation(node.sourceInformation), | |
| ); | |
| } | |
| @override | |
| void visitRemainder(HRemainder node) { | |
| return visitInvokeBinary(node, '%'); | |
| } | |
| @override | |
| void visitNegate(HNegate node) => visitInvokeUnary(node, '-'); | |
| @override | |
| void visitAbs(HAbs node) { | |
| use(node.operand); | |
| push( | |
| js.js('Math.abs(#)', pop()).withSourceInformation(node.sourceInformation), | |
| ); | |
| } | |
| @override | |
| void visitLess(HLess node) => visitRelational(node, '<'); | |
| @override | |
| void visitLessEqual(HLessEqual node) => visitRelational(node, '<='); | |
| @override | |
| void visitGreater(HGreater node) => visitRelational(node, '>'); | |
| @override | |
| void visitGreaterEqual(HGreaterEqual node) => visitRelational(node, '>='); | |
| @override | |
| void visitExit(HExit node) { | |
| // Don't do anything. | |
| } | |
| @override | |
| void visitGoto(HGoto node) { | |
| HBasicBlock block = node.block!; | |
| assert(block.successors.length == 1); | |
| List<HBasicBlock> dominated = block.dominatedBlocks; | |
| // With the exception of the entry-node which dominates its successor | |
| // and the exit node, no block finishing with a 'goto' can have more than | |
| // one dominated block (since it has only one successor). | |
| // If the successor is dominated by another block, then the other block | |
| // is responsible for visiting the successor. | |
| if (dominated.isEmpty) return; | |
| if (dominated.length > 2) { | |
| failedAt(node, 'dominated.length = ${dominated.length}'); | |
| } | |
| if (dominated.length == 2 && block != currentGraph.entry) { | |
| failedAt(node, 'node.block != currentGraph.entry'); | |
| } | |
| assert(dominated[0] == block.successors[0]); | |
| continueSubGraph(dominated.first); | |
| } | |
| @override | |
| void visitLoopBranch(HLoopBranch node) { | |
| assert(node.block == subGraph!.end); | |
| // We are generating code for a loop condition. | |
| // If we are generating the subgraph as an expression, the | |
| // condition will be generated as the expression. | |
| // Otherwise, we don't generate the expression, and leave that | |
| // to the code that called [visitSubGraph]. | |
| if (isGeneratingExpression) { | |
| use(node.inputs[0]); | |
| } | |
| } | |
| @override | |
| void visitBreak(HBreak node) { | |
| assert(node.block!.successors.length == 1); | |
| if (node.label != null) { | |
| LabelDefinition label = node.label!; | |
| if (breakAction.contains(label.target)) { | |
| implicitBreakWithLabel(label.target); | |
| } else { | |
| pushStatement( | |
| js.Break(_namer.breakLabelName(label)) | |
| .withSourceInformation(node.sourceInformation), | |
| ); | |
| } | |
| } else { | |
| JumpTarget target = node.target; | |
| if (breakAction.contains(target)) { | |
| implicitBreakWithLabel(target); | |
| } else { | |
| if (node.breakSwitchContinueLoop) { | |
| pushStatement( | |
| js.Break(_namer.implicitContinueLabelName(target)) | |
| .withSourceInformation(node.sourceInformation), | |
| ); | |
| } else { | |
| pushStatement( | |
| js.Break(null).withSourceInformation(node.sourceInformation), | |
| ); | |
| } | |
| } | |
| } | |
| } | |
| @override | |
| void visitContinue(HContinue node) { | |
| assert(node.block!.successors.length == 1); | |
| if (node.label != null) { | |
| LabelDefinition label = node.label!; | |
| if (continueAction.contains(label)) { | |
| continueAsBreak(label); | |
| } else { | |
| // TODO(floitsch): should this really be the breakLabelName? | |
| pushStatement( | |
| js.Continue(_namer.breakLabelName(label)) | |
| .withSourceInformation(node.sourceInformation), | |
| ); | |
| } | |
| } else { | |
| JumpTarget target = node.target; | |
| if (implicitContinueAction.contains(target)) { | |
| implicitContinueAsBreak(target); | |
| } else { | |
| if (target.isSwitch) { | |
| pushStatement( | |
| js.Continue(_namer.implicitContinueLabelName(target)) | |
| .withSourceInformation(node.sourceInformation), | |
| ); | |
| } else { | |
| pushStatement( | |
| js.Continue(null).withSourceInformation(node.sourceInformation), | |
| ); | |
| } | |
| } | |
| } | |
| } | |
| @override | |
| void visitExitTry(HExitTry node) { | |
| // An [HExitTry] is used to represent the control flow graph of a | |
| // try/catch block, ie the try body is always a predecessor | |
| // of the catch and finally. Here, we continue visiting the try | |
| // body by visiting the block that contains the user-level control | |
| // flow instruction. | |
| continueSubGraph(node.bodyTrySuccessor); | |
| } | |
| @override | |
| void visitTry(HTry node) { | |
| // We should never get here. Try/catch/finally is always handled using block | |
| // information in [visitTryInfo]. | |
| failedAt(node, 'visitTry should not be called.'); | |
| } | |
| bool tryControlFlowOperation(HIf node) { | |
| if (!controlFlowOperators.contains(node)) return false; | |
| final phi = node.joinBlock!.phis.first!; | |
| bool atUseSite = isGenerateAtUseSite(phi); | |
| // Don't generate a conditional operator in this situation: | |
| // i = condition ? bar() : i; | |
| // But generate this instead: | |
| // if (condition) i = bar(); | |
| // Usually, the variable name is longer than 'if' and it takes up | |
| // more space to duplicate the name. | |
| if (!atUseSite && | |
| variableNames.getName(phi) == variableNames.getName(phi.inputs[1])) { | |
| return false; | |
| } | |
| if (!atUseSite) define(phi); | |
| continueSubGraph(node.joinBlock!); | |
| return true; | |
| } | |
| void generateIf(HIf node, HIfBlockInformation info) { | |
| HStatementInformation? thenGraph = info.thenGraph; | |
| HStatementInformation? elseGraph = info.elseGraph; | |
| HInstruction condition = node.inputs.single; | |
| js.Expression test; | |
| js.Statement thenPart; | |
| js.Statement elsePart; | |
| HBasicBlock thenBlock = node.block!.successors[0]; | |
| // If we believe we will generate S1 as empty, instead of | |
| // | |
| // if (e) S1; else S2; | |
| // | |
| // try to generate | |
| // | |
| // if (!e) S2; else S1; | |
| // | |
| // It is better to generate `!e` rather than try and negate it later. | |
| // Recognize a single then-block with no code and no controlled phis. | |
| if (isGenerateAtUseSite(condition) && | |
| thenBlock.successors.length == 1 && | |
| thenBlock.successors.single == node.joinBlock && | |
| node.joinBlock!.phis.isEmpty && | |
| thenBlock.first is HGoto) { | |
| generateNot(condition, condition.sourceInformation); | |
| test = pop(); | |
| // Swap branches but visit in same order as register allocator. | |
| elsePart = unwrapStatement(generateStatementsInNewBlock(thenGraph)); | |
| thenPart = unwrapStatement(generateStatementsInNewBlock(elseGraph)); | |
| assert(elsePart is js.EmptyStatement); | |
| } else { | |
| use(condition); | |
| test = pop(); | |
| thenPart = unwrapStatement(generateStatementsInNewBlock(thenGraph)); | |
| elsePart = unwrapStatement(generateStatementsInNewBlock(elseGraph)); | |
| } | |
| js.Statement code = _assembleIfThenElse(test, thenPart, elsePart); | |
| pushStatement(code.withSourceInformation(node.sourceInformation)); | |
| } | |
| js.Statement _assembleIfThenElse( | |
| js.Expression test, | |
| js.Statement thenPart, | |
| js.Statement elsePart, | |
| ) { | |
| // Peephole rewrites: | |
| // | |
| // if (e); else S; --> if (!e) S; | |
| // | |
| // if (e); --> e; | |
| // | |
| // TODO(sra): We might be able to do better with reshaping the CFG. | |
| if (thenPart is js.EmptyStatement) { | |
| if (elsePart is js.EmptyStatement) { | |
| return js.ExpressionStatement(test); | |
| } | |
| test = js.Prefix('!', test); | |
| js.Statement temp = thenPart; | |
| thenPart = elsePart; | |
| elsePart = temp; | |
| } | |
| if (_options.experimentToBoolean) { | |
| if (elsePart is js.EmptyStatement && | |
| thenPart is js.ExpressionStatement && | |
| thenPart.expression is js.Call) { | |
| return js.ExpressionStatement( | |
| js.Binary('&&', test, thenPart.expression), | |
| ); | |
| } | |
| } | |
| return js.If(test, thenPart, elsePart); | |
| } | |
| @override | |
| void visitIf(HIf node) { | |
| _metrics.countHIf++; | |
| HInstruction condition = node.inputs[0]; | |
| if (condition is HConstant) _metrics.countHIfConstant++; | |
| if (tryControlFlowOperation(node)) return; | |
| HIfBlockInformation info = | |
| node.blockInformation!.body as HIfBlockInformation; | |
| if (condition is HConstant) { | |
| if (condition.constant is TrueConstantValue) { | |
| generateStatements(info.thenGraph); | |
| } else { | |
| generateStatements(info.elseGraph); | |
| } | |
| } else { | |
| generateIf(node, info); | |
| } | |
| HBasicBlock? joinBlock = node.joinBlock; | |
| if (joinBlock != null && !identical(joinBlock.dominator, node.block)) { | |
| // The join block is dominated by a block in one of the branches. | |
| // The subgraph traversal never reached it, so we visit it here | |
| // instead. | |
| continueSubGraph(joinBlock); | |
| } | |
| // Visit all the dominated blocks that are not part of the then or else | |
| // branches, and is not the join block. | |
| // Depending on how the then/else branches terminate | |
| // (e.g., return/throw/break) there can be any number of these. | |
| List<HBasicBlock> dominated = node.block!.dominatedBlocks; | |
| for (int i = 2; i < dominated.length; i++) { | |
| continueSubGraph(dominated[i]); | |
| } | |
| } | |
| @override | |
| void visitInterceptor(HInterceptor node) { | |
| _metrics.countHInterceptor.add(); | |
| if (node.isConditionalConstantInterceptor) { | |
| _metrics.countHInterceptorConditionalConstant.add(); | |
| assert(node.inputs.length == 2); | |
| use(node.receiver); | |
| js.Expression receiverExpression = pop(); | |
| use(node.conditionalConstantInterceptor); | |
| js.Expression constant = pop(); | |
| push(js.js('# && #', [receiverExpression, constant])); | |
| } else { | |
| _metrics.countHInterceptorGet.add(); | |
| assert(node.inputs.length == 1); | |
| _registry.registerSpecializedGetInterceptor(node.interceptedClasses!); | |
| js.Name name = _namer.nameForGetInterceptor(node.interceptedClasses!); | |
| js.Expression isolate = _namer.readGlobalObjectForInterceptors(); | |
| use(node.receiver); | |
| List<js.Expression> arguments = [pop()]; | |
| push( | |
| js | |
| .propertyCall(isolate, name, arguments) | |
| .withSourceInformation(node.sourceInformation), | |
| ); | |
| _registry.registerUseInterceptor(); | |
| } | |
| } | |
| @override | |
| void visitInvokeDynamicMethod(HInvokeDynamicMethod node) { | |
| _updateInvokeMetrics(node); | |
| use(node.receiver); | |
| js.Expression object = pop(); | |
| String? methodName; | |
| List<js.Expression> arguments = visitArguments(node.inputs); | |
| MemberEntity? target = node.element; | |
| // TODO(herhut): The namer should return the appropriate backend name here. | |
| if (target != null && !node.isInterceptedCall) { | |
| if (target == _commonElements.jsArrayAdd) { | |
| methodName = 'push'; | |
| } else if (target == _commonElements.jsArrayRemoveLast) { | |
| methodName = 'pop'; | |
| } else if (_commonElements.isJsStringSplit(target)) { | |
| methodName = 'split'; | |
| // Split returns a List, so we make sure the backend knows the | |
| // list class is instantiated. | |
| _registry | |
| // ignore:deprecated_member_use_from_same_package | |
| .registerInstantiatedClass(_commonElements.listClass); | |
| } | |
| } | |
| js.Name methodLiteral; | |
| if (methodName == null) { | |
| methodLiteral = _namer.invocationName(node.selector); | |
| registerMethodInvoke(node); | |
| } else { | |
| methodLiteral = _namer.asName(methodName); | |
| } | |
| push( | |
| js | |
| .propertyCall(object, methodLiteral, arguments) | |
| .withSourceInformation(node.sourceInformation), | |
| ); | |
| } | |
| @override | |
| void visitInvokeConstructorBody(HInvokeConstructorBody node) { | |
| final element = node.element as ConstructorBodyEntity; | |
| use(node.inputs[0]); | |
| js.Expression object = pop(); | |
| js.Name methodName = _namer.instanceMethodName(element); | |
| List<js.Expression> arguments = visitArguments(node.inputs); | |
| push( | |
| js | |
| .propertyCall(object, methodName, arguments) | |
| .withSourceInformation(node.sourceInformation), | |
| ); | |
| _registry.registerStaticUse( | |
| StaticUse.constructorBodyInvoke( | |
| element, | |
| CallStructure.unnamed(arguments.length), | |
| ), | |
| ); | |
| } | |
| @override | |
| void visitInvokeGeneratorBody(HInvokeGeneratorBody node) { | |
| // TODO(sra): Refactor HInvokeGeneratorBody so that `node.element` has this | |
| // type. | |
| JGeneratorBody element = node.element as JGeneratorBody; | |
| if (element.isInstanceMember) { | |
| use(node.inputs[0]); | |
| js.Expression object = pop(); | |
| List<js.Expression> arguments = visitArguments(node.inputs); | |
| js.Name methodName = _namer.instanceMethodName(element); | |
| push( | |
| js | |
| .propertyCall(object, methodName, arguments) | |
| .withSourceInformation(node.sourceInformation), | |
| ); | |
| } else { | |
| push(_emitter.staticFunctionAccess(element)); | |
| List<js.Expression> arguments = visitArguments(node.inputs, start: 0); | |
| push( | |
| js.Call(pop(), arguments, sourceInformation: node.sourceInformation), | |
| ); | |
| } | |
| _registry.registerStaticUse(StaticUse.generatorBodyInvoke(element)); | |
| } | |
| @override | |
| void visitOneShotInterceptor(HOneShotInterceptor node) { | |
| _metrics.countHInterceptor.add(); | |
| _metrics.countHInterceptorOneshot.add(); | |
| List<js.Expression> arguments = visitArguments(node.inputs); | |
| js.Expression isolate = _namer.readGlobalObjectForInterceptors(); | |
| Selector selector = node.selector; | |
| Set<ClassEntity> classes = _interceptorData.getInterceptedClassesOn( | |
| selector.name, | |
| _closedWorld, | |
| ); | |
| _registry.registerOneShotInterceptor(selector); | |
| js.Name methodName = _namer.nameForOneShotInterceptor(selector, classes); | |
| push( | |
| js | |
| .propertyCall(isolate, methodName, arguments) | |
| .withSourceInformation(node.sourceInformation), | |
| ); | |
| if (selector.isGetter) { | |
| registerGetter(node); | |
| } else if (selector.isSetter) { | |
| registerSetter(node); | |
| } else { | |
| registerMethodInvoke(node); | |
| } | |
| _registry.registerUseInterceptor(); | |
| } | |
| AbstractValue getOptimizedSelectorFor( | |
| HInvokeDynamic node, | |
| Selector selector, | |
| AbstractValue mask, | |
| ) { | |
| if (node.element != null) { | |
| // Create an artificial type mask to make sure only | |
| // [node.element] will be enqueued. We're not using the receiver | |
| // type because our optimizations might end up in a state where the | |
| // invoke dynamic knows more than the receiver. | |
| ClassEntity enclosing = node.element!.enclosingClass!; | |
| if (_closedWorld.classHierarchy.isInstantiated(enclosing)) { | |
| return _abstractValueDomain.createNonNullExact(enclosing); | |
| } else { | |
| // The element is mixed in so a non-null subtype mask is the most | |
| // precise we have. | |
| assert( | |
| _closedWorld.isUsedAsMixin(enclosing), | |
| failedAt( | |
| node, | |
| "Element ${node.element} from $enclosing expected " | |
| "to be mixed in.", | |
| ), | |
| ); | |
| return _abstractValueDomain.createNonNullSubtype(enclosing); | |
| } | |
| } | |
| return mask; | |
| } | |
| void registerMethodInvoke(HInvokeDynamic node) { | |
| Selector selector = node.selector; | |
| // If we don't know what we're calling or if we are calling a getter, | |
| // we need to register that fact that we may be calling a closure | |
| // with the same arguments. | |
| MemberEntity? target = node.element; | |
| if ((target == null || target.isGetter) && | |
| // TODO(johnniwinther): Remove this when kernel adds an `isFunctionCall` | |
| // flag to [ir.MethodInvocation]. Currently we can't tell the difference | |
| // between a dynamic call and a function call, but we at least know that | |
| // toString is not a getter (a potential function call should otherwise | |
| // have been register for string concatenation). | |
| selector != Selectors.toString_) { | |
| // TODO(kasperl): If we have a typed selector for the call, we | |
| // may know something about the types of closures that need | |
| // the specific closure call method. | |
| Selector call = Selector.callClosureFrom(selector); | |
| _registry.registerDynamicUse(DynamicUse(call, null, node.typeArguments)); | |
| } | |
| if (target != null) { | |
| // This is a dynamic invocation which we have found to have a single | |
| // target but for some reason haven't inlined. We are _still_ accessing | |
| // the target dynamically but we don't need to enqueue more than target | |
| // for this to work. | |
| assert( | |
| selector.applies(target), | |
| failedAt(node, '$selector does not apply to $target'), | |
| ); | |
| assert( | |
| !selector.isGetter && !selector.isSetter, | |
| "Unexpected direct invocation selector: $selector.", | |
| ); | |
| target as FunctionEntity; // TODO(sra): Make node.element have this type. | |
| _registry.registerStaticUse( | |
| StaticUse.directInvoke( | |
| target, | |
| selector.callStructure, | |
| node.typeArguments, | |
| ), | |
| ); | |
| } else { | |
| AbstractValue mask = getOptimizedSelectorFor( | |
| node, | |
| selector, | |
| node.receiverType, | |
| ); | |
| _registry.registerDynamicUse( | |
| DynamicUse(selector, mask, node.typeArguments), | |
| ); | |
| } | |
| } | |
| void registerSetter(HInvokeDynamic node, {bool needsCheck = false}) { | |
| final element = node.element; | |
| if (element is FieldEntity && !needsCheck) { | |
| // This is a dynamic update which we have found to have a single | |
| // target but for some reason haven't inlined. We are _still_ accessing | |
| // the target dynamically but we don't need to enqueue more than target | |
| // for this to work. | |
| _registry.registerStaticUse(StaticUse.directSet(element)); | |
| } else { | |
| Selector selector = node.selector; | |
| AbstractValue mask = getOptimizedSelectorFor( | |
| node, | |
| selector, | |
| node.receiverType, | |
| ); | |
| _registry.registerDynamicUse( | |
| DynamicUse(selector, mask, node.typeArguments), | |
| ); | |
| } | |
| } | |
| void registerGetter(HInvokeDynamic node) { | |
| final element = node.element; | |
| if (element != null && (element.isGetter || element is FieldEntity)) { | |
| // This is a dynamic read which we have found to have a single target but | |
| // for some reason haven't inlined. We are _still_ accessing the target | |
| // dynamically but we don't need to enqueue more than target for this to | |
| // work. The test above excludes non-getter functions since the element | |
| // represents two targets - a tearoff getter and the torn-off method. | |
| _registry.registerStaticUse(StaticUse.directGet(element)); | |
| } else { | |
| Selector selector = node.selector; | |
| AbstractValue mask = getOptimizedSelectorFor( | |
| node, | |
| selector, | |
| node.receiverType, | |
| ); | |
| _registry.registerDynamicUse( | |
| DynamicUse(selector, mask, node.typeArguments), | |
| ); | |
| } | |
| } | |
| void _updateInvokeMetrics(HInvokeDynamic node) { | |
| if (node.element != null) _metrics.countSingleTargetInstanceCalls++; | |
| } | |
| @override | |
| void visitInvokeDynamicSetter(HInvokeDynamicSetter node) { | |
| _updateInvokeMetrics(node); | |
| use(node.receiver); | |
| js.Name name = _namer.invocationName(node.selector); | |
| push( | |
| js | |
| .propertyCall(pop(), name, visitArguments(node.inputs)) | |
| .withSourceInformation(node.sourceInformation), | |
| ); | |
| registerSetter(node, needsCheck: node.needsCheck); | |
| } | |
| @override | |
| void visitInvokeDynamicGetter(HInvokeDynamicGetter node) { | |
| _updateInvokeMetrics(node); | |
| use(node.receiver); | |
| js.Name name = _namer.invocationName(node.selector); | |
| push( | |
| js | |
| .propertyCall(pop(), name, visitArguments(node.inputs)) | |
| .withSourceInformation(node.sourceInformation), | |
| ); | |
| registerGetter(node); | |
| } | |
| @override | |
| void visitInvokeClosure(HInvokeClosure node) { | |
| Selector call = Selector.callClosureFrom(node.selector); | |
| use(node.receiver); | |
| push( | |
| js | |
| .propertyCall( | |
| pop(), | |
| _namer.invocationName(call), | |
| visitArguments(node.inputs), | |
| ) | |
| .withSourceInformation(node.sourceInformation), | |
| ); | |
| // TODO(kasperl): If we have a typed selector for the call, we | |
| // may know something about the types of closures that need | |
| // the specific closure call method. | |
| _registry.registerDynamicUse(DynamicUse(call, null, node.typeArguments)); | |
| } | |
| @override | |
| void visitInvokeStatic(HInvokeStatic node) { | |
| // TODO(48820): Refactor HInvokeStatic so that the element has static type | |
| // FunctionEntity (`element` can be a FieldEntity in subclass HInvokeSuper, | |
| // so possibly make HInvokeSuper and HInvokeStatic extend a common | |
| // superclass, or have a different node for super-field accesses). | |
| FunctionEntity element = node.element as FunctionEntity; | |
| node.instantiatedTypes?.forEach(_registry.registerInstantiation); | |
| List<js.Expression> arguments = visitArguments(node.inputs, start: 0); | |
| if (element == _commonElements.jsAllowInterop) { | |
| _nativeData.registerAllowInterop(); | |
| } | |
| if (_commonElements.isCheckConcurrentModificationError(element)) { | |
| // Manually inline the [checkConcurrentModificationError] function. This | |
| // function is only called from a for-loop update. Ideally we would just | |
| // generate the conditional control flow in the builder but it adds basic | |
| // blocks in the loop update that interfere with other optimizations and | |
| // confuses loop recognition. | |
| assert(arguments.length == 2); | |
| FunctionEntity throwFunction = | |
| _commonElements.throwConcurrentModificationError; | |
| _registry.registerStaticUse( | |
| StaticUse.staticInvoke(throwFunction, CallStructure.oneArg), | |
| ); | |
| // Calling using `(0, #)(#)` instead of `#(#)` separates the property load | |
| // of the static function access from the call. For some reason this | |
| // helps V8 see that the call never happens so V8 makes the call a | |
| // deoptimization. This removes the call from the optimized loop, making | |
| // more optimizations available to the loop. This form is 50% faster on | |
| // some small loop, almost as fast as loops with no concurrent | |
| // modification check. | |
| // Create [right] as a separate JS node to give the call a source | |
| // location. | |
| js.Expression right = js | |
| .js('(0, #)(#)', [ | |
| _emitter.staticFunctionAccess(throwFunction), | |
| arguments[1], | |
| ]) | |
| .withSourceInformation(node.sourceInformation); | |
| push( | |
| js | |
| .js('# || #', [arguments[0], right]) | |
| .withSourceInformation(node.sourceInformation), | |
| ); | |
| } else { | |
| StaticUse staticUse; | |
| RecordedUse? recordedMethodUses; | |
| if (element is ConstructorEntity) { | |
| CallStructure callStructure = CallStructure.unnamed( | |
| arguments.length, | |
| node.typeArguments.length, | |
| ); | |
| staticUse = StaticUse.constructorInvoke(element, callStructure); | |
| if (_shouldRecordConstructor(element)) { | |
| recordedMethodUses = _recordConstructorUses(element, node.inputs); | |
| } | |
| } else if (element.isGetter) { | |
| staticUse = StaticUse.staticGet(element); | |
| if (_shouldRecordMethodUses(element)) { | |
| recordedMethodUses = _recordMethodUses(element, node.inputs); | |
| } | |
| } else if (element.isSetter) { | |
| staticUse = StaticUse.staticSet(element); | |
| if (_shouldRecordMethodUses(element)) { | |
| recordedMethodUses = _recordMethodUses(element, node.inputs); | |
| } | |
| } else { | |
| assert(element.isFunction); | |
| CallStructure callStructure = CallStructure.unnamed( | |
| arguments.length, | |
| node.typeArguments.length, | |
| ); | |
| staticUse = StaticUse.staticInvoke( | |
| element, | |
| callStructure, | |
| node.typeArguments, | |
| ); | |
| if (_shouldRecordMethodUses(element)) { | |
| recordedMethodUses = _recordMethodUses(element, node.inputs); | |
| } | |
| if (_shouldRecordConstructor(element)) { | |
| final elementNode = _closedWorld.elementMap | |
| .getMemberDefinition(element) | |
| .node; | |
| if (elementNode is ir.Procedure && | |
| getConstructorTearOffLoweringTarget(elementNode) != null) { | |
| recordedMethodUses = _recordConstructorTearOff(element); | |
| } else { | |
| recordedMethodUses = _recordConstructorUses(element, node.inputs); | |
| } | |
| } | |
| } | |
| _registry.registerStaticUse(staticUse); | |
| push(_emitter.staticFunctionAccess(element)); | |
| push( | |
| js.Call(pop(), arguments, sourceInformation: node.sourceInformation), | |
| ); | |
| if (recordedMethodUses != null) { | |
| push(pop().withAnnotation(recordedMethodUses)); | |
| } | |
| } | |
| } | |
| bool _shouldRecordMethodUses(FunctionEntity element) { | |
| if (_closedWorld.annotationsData.shouldRecordMethodUses(element)) { | |
| final currentElement = currentGraph.element; | |
| if (currentElement is FunctionEntity) { | |
| final currentNode = _closedWorld.elementMap | |
| .getMemberDefinition(currentElement) | |
| .node; | |
| if (currentNode is ir.Procedure) { | |
| final implementation = getExtensionMemberImplementation(currentNode); | |
| if (implementation != null && | |
| _closedWorld.elementMap.containsMethod(implementation)) { | |
| final implementationEntity = _closedWorld.elementMap.getMember( | |
| implementation, | |
| ); | |
| if (element == implementationEntity) return false; | |
| } | |
| } | |
| // If we are currently compiling a tear-off closure's call method, | |
| // we should also skip recording the call to the implementation method. | |
| final enclosingMember = _closedWorld.closureDataLookup | |
| .getEnclosingMember(currentElement); | |
| if (enclosingMember != currentElement && | |
| enclosingMember is FunctionEntity) { | |
| final enclosingNode = _closedWorld.elementMap | |
| .getMemberDefinition(enclosingMember) | |
| .node; | |
| if (enclosingNode is ir.Procedure) { | |
| final implementation = getExtensionMemberImplementation( | |
| enclosingNode, | |
| ); | |
| if (implementation != null && | |
| _closedWorld.elementMap.containsMethod(implementation)) { | |
| final implementationEntity = _closedWorld.elementMap.getMember( | |
| implementation, | |
| ); | |
| if (element == implementationEntity) return false; | |
| } | |
| } | |
| } | |
| } | |
| return true; | |
| } | |
| return _shouldRecordExtensionTearOff(element); | |
| } | |
| /// Returns `true` if [element] is an extension member tear-off that should | |
| /// be recorded. | |
| /// | |
| /// When an extension member is annotated with `@RecordUse`, we want to | |
| /// record whenever it is torn off. | |
| bool _shouldRecordExtensionTearOff(FunctionEntity element) { | |
| final node = _closedWorld.elementMap.getMemberDefinition(element).node; | |
| if (node is ir.Procedure && isExtensionMemberTearOff(node)) { | |
| final implementation = getExtensionMemberImplementation(node); | |
| if (implementation != null && | |
| _closedWorld.elementMap.containsMethod(implementation)) { | |
| return _closedWorld.annotationsData.shouldRecordMethodUses( | |
| _closedWorld.elementMap.getMember(implementation) as FunctionEntity, | |
| ); | |
| } | |
| } | |
| return false; | |
| } | |
| bool _shouldRecordConstructor(MemberEntity element) { | |
| final callerNode = _closedWorld.elementMap | |
| .getMemberDefinition(_member) | |
| .node; | |
| if (callerNode is ir.Procedure && | |
| (isTearOffLowering(callerNode) || callerNode.isRedirectingFactory)) { | |
| // If we are currently compiling a constructor tear-off lowering, we | |
| // don't want to record the generative constructor calls inside it. | |
| return false; | |
| } | |
| final node = _closedWorld.elementMap.getMemberDefinition(element).node; | |
| MemberEntity resolved = element; | |
| bool isTearOff = false; | |
| if (node is ir.Procedure) { | |
| final target = getConstructorTearOffLoweringTarget(node); | |
| if (target != null) { | |
| resolved = _closedWorld.elementMap.getMember(target); | |
| isTearOff = true; | |
| } | |
| } | |
| final irResolved = _closedWorld.elementMap | |
| .getMemberDefinition(resolved) | |
| .node; | |
| if (irResolved is ir.Procedure && irResolved.isRedirectingFactory) { | |
| final ultimate = _getConstructorEffectiveTarget(irResolved); | |
| if (ultimate == null) return false; | |
| resolved = _closedWorld.elementMap.getMember(ultimate); | |
| } | |
| if (resolved is! ConstructorEntity) return false; | |
| final constructor = resolved; | |
| final classAnnotated = _closedWorld.annotationsData | |
| .shouldRecordConstInstances(constructor.enclosingClass); | |
| if (isTearOff) { | |
| return classAnnotated; | |
| } | |
| if (constructor.isGenerativeConstructor) { | |
| return classAnnotated; | |
| } | |
| // Regular factory call. Treat as static call. Don't record instances. | |
| return false; | |
| } | |
| ir.Member? _getConstructorEffectiveTarget(ir.Member node) { | |
| ir.Member? current = node; | |
| while (current is ir.Procedure && current.isRedirectingFactory) { | |
| current = current.function.redirectingFactoryTarget?.target; | |
| } | |
| return current; | |
| } | |
| RecordedUse _recordConstructorUses( | |
| MemberEntity element, | |
| List<HInstruction> arguments, | |
| ) { | |
| ir.Member node = | |
| _closedWorld.elementMap.getMemberDefinition(element).node as ir.Member; | |
| node = _getConstructorEffectiveTarget(node)!; | |
| final constructor = | |
| _closedWorld.elementMap.getMember(node) as ConstructorEntity; | |
| final positionalArguments = <ConstantValue?>[]; | |
| final namedArguments = <String, ConstantValue?>{}; | |
| var argumentIndex = 0; | |
| _closedWorld.elementEnvironment.forEachParameter(constructor, ( | |
| DartType type, | |
| String? name, | |
| ConstantValue? defaultValue, | |
| ) { | |
| final value = argumentIndex < arguments.length | |
| ? _findConstant(arguments[argumentIndex]) | |
| : defaultValue; | |
| if (argumentIndex < constructor.parameterStructure.positionalParameters) { | |
| positionalArguments.add(value); | |
| } else { | |
| namedArguments[name!] = value; | |
| } | |
| argumentIndex++; | |
| }); | |
| return RecordedInstanceCreation( | |
| constructor: constructor, | |
| positionalArguments: positionalArguments, | |
| namedArguments: namedArguments, | |
| ); | |
| } | |
| RecordedUse _recordConstructorTearOff(FunctionEntity element) { | |
| final node = _closedWorld.elementMap.getMemberDefinition(element).node; | |
| ir.Member target = node as ir.Member; | |
| if (node is ir.Procedure) { | |
| target = getConstructorTearOffLoweringTarget(node) ?? target; | |
| } | |
| target = _getConstructorEffectiveTarget(target)!; | |
| final constructor = _closedWorld.elementMap.getMember(target); | |
| return RecordedConstructorTearOff( | |
| constructor: constructor as ConstructorEntity, | |
| ); | |
| } | |
| RecordedUse _recordMethodUses( | |
| FunctionEntity element, | |
| List<HInstruction> arguments, | |
| ) { | |
| final node = _closedWorld.elementMap.getMemberDefinition(element).node; | |
| var definitionHasReceiver = false; | |
| if (node is ir.Procedure) { | |
| if (node.isExtensionMember || node.isExtensionTypeMember) { | |
| definitionHasReceiver = | |
| (node.function.positionalParameters.isNotEmpty && | |
| isExtensionThisName( | |
| node.function.positionalParameters[0].parameterName, | |
| )) || | |
| isExtensionMemberTearOff(node); | |
| } | |
| } | |
| final originalParameterStructure = element.parameterStructure; | |
| ConstantValue? constantReceiver; | |
| final positionalArguments = <ConstantValue?>[]; | |
| final namedArguments = <String, ConstantValue?>{}; | |
| var argumentIndex = 0; | |
| // Loop over arguments in namedOrdering or in nativeOrdering. | |
| _closedWorld.elementEnvironment.forEachParameter(element, ( | |
| DartType type, | |
| String? name, | |
| ConstantValue? defaultValue, | |
| ) { | |
| if (argumentIndex == 0 && definitionHasReceiver) { | |
| constantReceiver = _findConstant(arguments[argumentIndex++]); | |
| } else { | |
| final value = argumentIndex < arguments.length | |
| ? _findConstant(arguments[argumentIndex]) | |
| : defaultValue; | |
| if (argumentIndex < originalParameterStructure.positionalParameters) { | |
| positionalArguments.add(value); | |
| } else { | |
| namedArguments[name!] = value; | |
| } | |
| argumentIndex++; | |
| } | |
| }); | |
| if (_shouldRecordExtensionTearOff(element)) { | |
| return RecordedTearOff( | |
| function: element, | |
| definitionHasReceiver: definitionHasReceiver, | |
| constantReceiver: constantReceiver, | |
| ); | |
| } | |
| return RecordedCallWithArguments( | |
| function: element, | |
| definitionHasReceiver: definitionHasReceiver, | |
| constantReceiver: constantReceiver, | |
| positionalArguments: positionalArguments, | |
| namedArguments: namedArguments, | |
| ); | |
| } | |
| /// Records a tear-off of a static method. | |
| /// | |
| /// Since this is a tear-off of a static method, it has no receiver. | |
| /// Extension member tear-offs are recorded as calls to their tear-off | |
| /// implementation in [_recordMethodUses] instead. | |
| RecordedUse _recordTearOff(FunctionEntity element) { | |
| return RecordedTearOff( | |
| function: element, | |
| definitionHasReceiver: false, | |
| constantReceiver: null, | |
| ); | |
| } | |
| ConstantValue? _findConstant(HInstruction node) { | |
| while (node is HLateValue) { | |
| node = node.target; | |
| } | |
| return node is HConstant ? node.constant : null; | |
| } | |
| @override | |
| void visitInvokeSuper(HInvokeSuper node) { | |
| MemberEntity superElement = node.element; | |
| Selector selector = node.selector; | |
| bool useAliasedSuper = canUseAliasedSuperMember(superElement, selector); | |
| if (selector.isGetter) { | |
| if (superElement is FieldEntity || superElement.isGetter) { | |
| _registry.registerStaticUse(StaticUse.superGet(superElement)); | |
| } else { | |
| superElement as FunctionEntity; // Not a field so must be a function. | |
| _registry.registerStaticUse(StaticUse.superTearOff(superElement)); | |
| } | |
| } else if (selector.isSetter) { | |
| if (superElement is FieldEntity) { | |
| _registry.registerStaticUse(StaticUse.superFieldSet(superElement)); | |
| } else { | |
| assert(superElement.isSetter); | |
| superElement as FunctionEntity; // Not a field so must be a function. | |
| _registry.registerStaticUse(StaticUse.superSetterSet(superElement)); | |
| } | |
| } else { | |
| superElement as FunctionEntity; // Not a field so must be a function. | |
| if (useAliasedSuper) { | |
| _registry.registerStaticUse( | |
| StaticUse.superInvoke( | |
| superElement, | |
| CallStructure.unnamed(node.inputs.length), | |
| ), | |
| ); | |
| } else { | |
| _registry.registerStaticUse( | |
| StaticUse.superInvoke( | |
| superElement, | |
| CallStructure.unnamed(node.inputs.length - 1), | |
| ), | |
| ); | |
| } | |
| } | |
| if (superElement is FieldEntity) { | |
| // TODO(sra): We can lower these in the simplifier. | |
| js.Name fieldName = _namer.instanceFieldPropertyName(superElement); | |
| use(node.getDartReceiver()); | |
| js.PropertyAccess access = js.PropertyAccess( | |
| pop(), | |
| fieldName, | |
| ).withSourceInformation(node.sourceInformation) as js.PropertyAccess; | |
| if (node.isSetter) { | |
| use(node.value); | |
| push( | |
| js.Assignment( | |
| access, | |
| pop(), | |
| ).withSourceInformation(node.sourceInformation), | |
| ); | |
| } else { | |
| push(access); | |
| } | |
| } else if (superElement is FunctionEntity) { | |
| if (!useAliasedSuper) { | |
| js.Name methodName; | |
| if (selector.isGetter && !superElement.isGetter) { | |
| // If this is a tear-off, register the fact that a tear-off closure | |
| // will be created, and that this tear-off must bypass ordinary | |
| // dispatch to ensure the super method is invoked. | |
| FunctionEntity helper = _commonElements.closureFromTearOff; | |
| _registry.registerStaticUse( | |
| StaticUse.staticInvoke( | |
| helper, | |
| CallStructure.unnamed( | |
| node.inputs.length, | |
| node.typeArguments.length, | |
| ), | |
| node.typeArguments, | |
| ), | |
| ); | |
| methodName = _namer.invocationName(selector); | |
| } else { | |
| methodName = _namer.instanceMethodName(superElement); | |
| } | |
| ClassEntity superClass = superElement.enclosingClass!; | |
| push( | |
| js | |
| .js('#.#.call(#)', [ | |
| _emitter.prototypeAccess(superClass), | |
| methodName, | |
| visitArguments(node.inputs, start: 0), | |
| ]) | |
| .withSourceInformation(node.sourceInformation), | |
| ); | |
| } else { | |
| use(node.receiver); | |
| push( | |
| js | |
| .js('#.#(#)', [ | |
| pop(), | |
| _namer.aliasedSuperMemberPropertyName(superElement), | |
| visitArguments(node.inputs, start: 1), | |
| ]) // Skip receiver argument. | |
| .withSourceInformation(node.sourceInformation), | |
| ); | |
| } | |
| } else { | |
| failedAt(node, 'node.element must be FieldEntity or FunctionEntity'); | |
| } | |
| } | |
| js.Expression _loadField( | |
| js.Expression receiver, | |
| FieldEntity field, | |
| SourceInformation? sourceInformation, | |
| ) { | |
| _registry.registerStaticUse(StaticUse.fieldGet(field)); | |
| js.Name name = _namer.instanceFieldPropertyName(field); | |
| return js.PropertyAccess( | |
| receiver, | |
| name, | |
| ).withSourceInformation(sourceInformation); | |
| } | |
| @override | |
| void visitFieldGet(HFieldGet node) { | |
| _metrics.countHFieldGet++; | |
| use(node.receiver); | |
| push(_loadField(pop(), node.element, node.sourceInformation)); | |
| } | |
| @override | |
| void visitFieldSet(HFieldSet node) { | |
| FieldEntity element = node.element; | |
| _registry.registerStaticUse(StaticUse.fieldSet(element)); | |
| js.Name name = _namer.instanceFieldPropertyName(element); | |
| use(node.receiver); | |
| js.Expression receiver = pop(); | |
| use(node.value); | |
| push( | |
| js.Assignment( | |
| js.PropertyAccess( | |
| receiver, | |
| name, | |
| ).withSourceInformation(node.sourceInformation), | |
| pop(), | |
| ).withSourceInformation(node.sourceInformation), | |
| ); | |
| } | |
| @override | |
| void visitGetLength(HGetLength node) { | |
| _metrics.countHGetLength++; | |
| use(node.receiver); | |
| push( | |
| js.PropertyAccess.field( | |
| pop(), | |
| 'length', | |
| ).withSourceInformation(node.sourceInformation), | |
| ); | |
| } | |
| @override | |
| void visitReadModifyWrite(HReadModifyWrite node) { | |
| FieldEntity element = node.element; | |
| _registry.registerStaticUse(StaticUse.fieldGet(element)); | |
| _registry.registerStaticUse(StaticUse.fieldSet(element)); | |
| js.Name name = _namer.instanceFieldPropertyName(element); | |
| use(node.receiver); | |
| js.Expression fieldReference = js.PropertyAccess(pop(), name); | |
| switch (node.opKind) { | |
| case ReadModifyWriteKind.prefix: | |
| push( | |
| js.Prefix( | |
| node.jsOp, | |
| fieldReference, | |
| ).withSourceInformation(node.sourceInformation), | |
| ); | |
| case ReadModifyWriteKind.postfix: | |
| push( | |
| js.Postfix( | |
| node.jsOp, | |
| fieldReference, | |
| ).withSourceInformation(node.sourceInformation), | |
| ); | |
| case ReadModifyWriteKind.assign: | |
| use(node.value); | |
| push( | |
| js.Assignment.compound( | |
| fieldReference, | |
| node.jsOp, | |
| pop(), | |
| ).withSourceInformation(node.sourceInformation), | |
| ); | |
| } | |
| } | |
| @override | |
| void visitFunctionReference(HFunctionReference node) { | |
| FunctionEntity element = node.element; | |
| _registry.registerStaticUse(StaticUse.implicitInvoke(element)); | |
| push(_emitter.staticFunctionAccess(element)); | |
| } | |
| @override | |
| void visitLocalGet(HLocalGet node) { | |
| use(node.receiver); | |
| } | |
| @override | |
| void visitLocalSet(HLocalSet node) { | |
| use(node.value); | |
| assignVariable( | |
| variableNames.getName(node.receiver)!, | |
| pop(), | |
| node.sourceInformation, | |
| ); | |
| } | |
| @override | |
| void visitInvokeExternal(HInvokeExternal node) { | |
| FunctionEntity target = node.element; | |
| List<HInstruction> inputs = node.inputs; | |
| assert(_nativeData.isNativeMember(target), 'non-native target: $node'); | |
| String? targetName = _nativeData.hasFixedBackendName(target) | |
| ? _nativeData.getFixedBackendName(target) | |
| : target.name; | |
| Object? recordedMethodUses; | |
| if (_shouldRecordMethodUses(target)) { | |
| recordedMethodUses = _recordMethodUses(target, inputs); | |
| } | |
| void invokeWithJavaScriptReceiver(js.Expression receiverExpression) { | |
| // JS-interop target names can be paths ("a.b"), so we parse them to | |
| // re-associate the property accesses ("#.a.b" is `dot(dot(#,'a'),'b')`). | |
| // | |
| // Native target names are simple identifiers, so re-parsing is not | |
| // necessary, but it is simpler to use the same code. | |
| String template; | |
| List<Object> templateInputs; | |
| if (target.isGetter) { | |
| template = '#.$targetName'; | |
| templateInputs = [receiverExpression]; | |
| } else if (target.isSetter) { | |
| assert(inputs.length == (target.isInstanceMember ? 2 : 1)); | |
| use(inputs.last); | |
| template = '#.$targetName = #'; | |
| templateInputs = [receiverExpression, pop()]; | |
| } else { | |
| var arguments = visitArguments( | |
| inputs, | |
| start: target.isInstanceMember ? 1 : 0, | |
| ); | |
| template = target is ConstructorEntity | |
| ? 'new #.$targetName(#)' | |
| : '#.$targetName(#)'; | |
| templateInputs = [receiverExpression, arguments]; | |
| } | |
| js.Expression expression = js.js | |
| .uncachedExpressionTemplate(template) | |
| .instantiateExpression(templateInputs); | |
| if (recordedMethodUses != null) { | |
| expression = expression.withAnnotation(recordedMethodUses); | |
| } | |
| push(expression.withSourceInformation(node.sourceInformation)); | |
| _registry.registerNativeMethod(target); | |
| } | |
| if (_nativeData.isJsInteropMember(target)) { | |
| if (target.isStatic || target.isTopLevel || target is ConstructorEntity) { | |
| String path = _nativeData.getFixedBackendMethodPath(target)!; | |
| js.Expression pathExpression = js.js | |
| .uncachedExpressionTemplate(path) | |
| .instantiateExpression([]); | |
| invokeWithJavaScriptReceiver(pathExpression); | |
| return; | |
| } | |
| } | |
| if (_nativeData.isNativeMember(target)) { | |
| _registry.registerNativeBehavior(node.nativeBehavior!); | |
| if (target.isInstanceMember) { | |
| HInstruction receiver = inputs.first; | |
| use(receiver); | |
| invokeWithJavaScriptReceiver(pop()); | |
| return; | |
| } | |
| if (target.isStatic || target.isTopLevel) { | |
| var arguments = visitArguments(inputs, start: 0); | |
| js.Expression targetExpression = js.js | |
| .uncachedExpressionTemplate(targetName!) | |
| .instantiateExpression([]); | |
| js.Expression expression; | |
| if (target.isGetter) { | |
| expression = targetExpression; | |
| } else if (target.isSetter) { | |
| expression = js.js('# = #', [targetExpression, inputs.single]); | |
| } else { | |
| assert(target.isFunction); | |
| expression = js.js('#(#)', [targetExpression, arguments]); | |
| } | |
| if (recordedMethodUses != null) { | |
| expression = expression.withAnnotation(recordedMethodUses); | |
| } | |
| push(expression.withSourceInformation(node.sourceInformation)); | |
| _registry.registerNativeMethod(target); | |
| return; | |
| } | |
| failedAt(node, 'codegen not implemented (non-instance-member): $node'); | |
| } | |
| failedAt(node, 'unexpected target: $node'); | |
| } | |
| void registerForeignTypes(HForeign node) { | |
| NativeBehavior? nativeBehavior = node.nativeBehavior; | |
| if (nativeBehavior == null) return; | |
| _registry.registerNativeBehavior(nativeBehavior); | |
| } | |
| @override | |
| void visitForeignCode(HForeignCode node) { | |
| List<HInstruction> inputs = node.inputs; | |
| if (node.isJsStatement()) { | |
| List<js.Expression> interpolatedExpressions = []; | |
| for (int i = 0; i < inputs.length; i++) { | |
| use(inputs[i]); | |
| interpolatedExpressions.add(pop()); | |
| } | |
| pushStatement( | |
| node.codeTemplate | |
| .instantiateStatement(interpolatedExpressions) | |
| .withSourceInformation(node.sourceInformation), | |
| ); | |
| } else { | |
| List<js.Expression> interpolatedExpressions = []; | |
| for (int i = 0; i < inputs.length; i++) { | |
| use(inputs[i]); | |
| interpolatedExpressions.add(pop()); | |
| } | |
| push( | |
| node.codeTemplate | |
| .instantiateExpression(interpolatedExpressions) | |
| .withSourceInformation(node.sourceInformation), | |
| ); | |
| } | |
| // TODO(sra): Tell world.nativeEnqueuer about the types created here. | |
| registerForeignTypes(node); | |
| } | |
| @override | |
| void visitEmbeddedGlobalGet(HEmbeddedGlobalGet node) { | |
| push(_emitter.generateEmbeddedGlobalAccess(node.name)); | |
| } | |
| @override | |
| void visitCreate(HCreate node) { | |
| js.Expression jsClassReference = _emitter.constructorAccess(node.element); | |
| List<js.Expression> arguments = visitArguments(node.inputs, start: 0); | |
| push( | |
| js.New( | |
| jsClassReference, | |
| arguments, | |
| ).withSourceInformation(node.sourceInformation), | |
| ); | |
| // We also use HCreate to instantiate closure classes that belong to | |
| // function expressions. We have to register their use here, as otherwise | |
| // code for them might not be emitted. | |
| if (node.element.isClosure) { | |
| _registry | |
| // ignore:deprecated_member_use_from_same_package | |
| .registerInstantiatedClass(node.element); | |
| } | |
| if (node.element is JRecordClass) { | |
| _registry | |
| // ignore:deprecated_member_use_from_same_package | |
| .registerInstantiatedClass(node.element); | |
| } | |
| node.instantiatedTypes?.forEach(_registry.registerInstantiation); | |
| final callMethod = node.callMethod; | |
| if (callMethod != null) { | |
| _registry.registerStaticUse(StaticUse.implicitInvoke(callMethod)); | |
| _registry.registerInstantiatedClosure(callMethod); | |
| } | |
| } | |
| @override | |
| void visitCreateBox(HCreateBox node) { | |
| push(js.ObjectInitializer([])); | |
| } | |
| js.Expression newLiteralBool( | |
| bool value, | |
| SourceInformation? sourceInformation, | |
| ) { | |
| if (_options.enableMinification) { | |
| // Use !0 for true, !1 for false. | |
| return js.Prefix( | |
| "!", | |
| js.LiteralNumber(value ? "0" : "1"), | |
| ).withSourceInformation(sourceInformation); | |
| } else { | |
| return js.LiteralBool(value).withSourceInformation(sourceInformation); | |
| } | |
| } | |
| /// Returns a 'for' loop or 'while' loop depending on which parts of a 'for' | |
| /// loop are present. In effect, choose the loop kind by applying these | |
| /// reductions: | |
| /// | |
| /// for(init;true;update) --> for(init;;update) | |
| /// for(;cond;) --> while(cond) | |
| /// while(true) --> for(;;) | |
| js.Loop newLoop({ | |
| js.Expression? init, | |
| js.Expression? condition, // `null` means indefinite, i.e. `true`. | |
| js.Expression? update, | |
| required js.Statement body, | |
| SourceInformation? sourceInformation, | |
| }) { | |
| // `condition` with `true` replaced with `null`: | |
| final conditionOrNull = switch (condition) { | |
| js.LiteralBool(value: true) => null, | |
| js.LiteralNumber(value: '1') => null, | |
| // Minified `true` is `!0`: | |
| js.Prefix(op: '!', argument: js.LiteralNumber(value: '0')) => null, | |
| _ => condition, | |
| }; | |
| if (init == null && update == null && conditionOrNull != null) { | |
| return js.While( | |
| conditionOrNull, | |
| body, | |
| sourceInformation: sourceInformation, | |
| ); | |
| } | |
| return js.For( | |
| init, | |
| conditionOrNull, | |
| update, | |
| body, | |
| sourceInformation: sourceInformation, | |
| ); | |
| } | |
| void generateConstant( | |
| ConstantValue constant, | |
| SourceInformation? sourceInformation, | |
| ) { | |
| js.Expression expression = _emitter.constantReference(constant); | |
| if (!constant.isDummy) { | |
| // TODO(johnniwinther): Support source information on synthetic constants. | |
| expression = expression.withSourceInformation(sourceInformation); | |
| } | |
| for (final recordedUse in _recordConstantUses(constant)) { | |
| expression = expression.withAnnotation(recordedUse); | |
| } | |
| push(expression); | |
| } | |
| _RecordedUseSequence _recordConstantUses(ConstantValue constant) { | |
| if (_recordedConstantUsesVisited.containsKey(constant)) { | |
| return _recordedConstantUsesVisited[constant]!; | |
| } | |
| final resultList = <RecordedUse>[]; | |
| switch (constant) { | |
| case FunctionConstantValue(): | |
| final element = constant.element; | |
| if (_shouldRecordMethodUses(element)) { | |
| resultList.add(_recordTearOff(element)); | |
| } | |
| if (_shouldRecordConstructor(element)) { | |
| resultList.add(_recordConstructorTearOff(element)); | |
| } | |
| case ConstructedConstantValue(): | |
| final element = constant.type.element; | |
| if (_closedWorld.annotationsData.shouldRecordConstInstances(element)) { | |
| resultList.add(RecordedConstInstance(constant: constant)); | |
| } | |
| default: | |
| break; | |
| } | |
| var result = _RecordedUseSequence.fromList(resultList); | |
| // Cover nested constants. | |
| for (final dependency in constant.getDependencies()) { | |
| result += _recordConstantUses(dependency); | |
| } | |
| _recordedConstantUsesVisited[constant] = result; | |
| return result; | |
| } | |
| @override | |
| void visitConstant(HConstant node) { | |
| assert(isGenerateAtUseSite(node)); | |
| generateConstant(node.constant, node.sourceInformation); | |
| _registry.registerConstantUse(ConstantUse.literal(node.constant)); | |
| ConstantValue constant = node.constant; | |
| if (constant is TypeConstantValue) { | |
| _registry.registerTypeUse( | |
| TypeUse.constTypeLiteral(constant.representedType), | |
| ); | |
| } | |
| } | |
| @override | |
| void visitNot(HNot node) { | |
| assert(node.inputs.length == 1); | |
| generateNot(node.inputs[0], node.sourceInformation); | |
| } | |
| static String mapRelationalOperator(String op, bool inverse) { | |
| Map<String, String> inverseOperator = const { | |
| "==": "!=", | |
| "!=": "==", | |
| "===": "!==", | |
| "!==": "===", | |
| "<": ">=", | |
| "<=": ">", | |
| ">": "<=", | |
| ">=": "<", | |
| }; | |
| return inverse ? inverseOperator[op]! : op; | |
| } | |
| void generateNot(HInstruction input, SourceInformation? sourceInformation) { | |
| bool canGenerateOptimizedComparison(HRelational relational) { | |
| HInstruction left = relational.left; | |
| HInstruction right = relational.right; | |
| if (left.isStringOrNull(_abstractValueDomain).isDefinitelyTrue && | |
| right.isStringOrNull(_abstractValueDomain).isDefinitelyTrue) { | |
| return true; | |
| } | |
| // This optimization doesn't work for NaN, so we only do it if the | |
| // type is known to be an integer. | |
| return left.isInteger(_abstractValueDomain).isDefinitelyTrue && | |
| right.isInteger(_abstractValueDomain).isDefinitelyTrue; | |
| } | |
| bool handledBySpecialCase = false; | |
| if (isGenerateAtUseSite(input)) { | |
| handledBySpecialCase = true; | |
| if (input is HIsTestSimple) { | |
| _emitIsTestSimple(input, negative: true); | |
| } else if (input is HNot) { | |
| use(input.inputs[0]); | |
| } else if (input is HIdentity) { | |
| emitIdentityComparison(input, sourceInformation, inverse: true); | |
| } else if (input is HIsLateSentinel) { | |
| _emitIsLateSentinel(input, sourceInformation, inverse: true); | |
| } else if (input is HRelational && | |
| canGenerateOptimizedComparison(input)) { | |
| constant_system.BinaryOperation operation = input.operation(); | |
| String op = mapRelationalOperator(operation.name, true); | |
| handleInvokeBinary(input, op, sourceInformation); | |
| } else { | |
| handledBySpecialCase = false; | |
| } | |
| } | |
| if (!handledBySpecialCase) { | |
| use(input); | |
| push(js.Prefix("!", pop()).withSourceInformation(sourceInformation)); | |
| } | |
| } | |
| @override | |
| void visitParameterValue(HParameterValue node) { | |
| assert(!isGenerateAtUseSite(node)); | |
| String name = variableNames.getName(node)!; | |
| parameters.add(js.Parameter(name)); | |
| declaredLocals.add(name); | |
| } | |
| @override | |
| void visitLocalValue(HLocalValue node) { | |
| assert(!isGenerateAtUseSite(node)); | |
| String name = variableNames.getName(node)!; | |
| collectedVariableDeclarations.add(name); | |
| } | |
| @override | |
| void visitPhi(HPhi node) { | |
| // This method is only called for phis that are generated at use | |
| // site. A phi can be generated at use site only if it is the | |
| // result of a control flow operation. | |
| HBasicBlock ifBlock = node.block!.dominator!; | |
| assert(controlFlowOperators.contains(ifBlock.last)); | |
| HInstruction input = ifBlock.last!.inputs[0]; | |
| if (input.isConstantFalse()) { | |
| use(node.inputs[1]); | |
| } else if (input.isConstantTrue()) { | |
| use(node.inputs[0]); | |
| } else if (node.inputs[1].isConstantBoolean()) { | |
| String operation = node.inputs[1].isConstantFalse() ? '&&' : '||'; | |
| if (operation == '||') { | |
| generateNot(input, input.sourceInformation); | |
| } else { | |
| use(input); | |
| } | |
| js.Expression left = pop(); | |
| use(node.inputs[0]); | |
| push(js.Binary(operation, left, pop())); | |
| } else { | |
| use(input); | |
| js.Expression test = pop(); | |
| use(node.inputs[0]); | |
| js.Expression then = pop(); | |
| use(node.inputs[1]); | |
| push(js.Conditional(test, then, pop())); | |
| } | |
| } | |
| @override | |
| void visitReturn(HReturn node) { | |
| if (node.inputs.isEmpty) { | |
| pushStatement(js.Return().withSourceInformation(node.sourceInformation)); | |
| } else { | |
| use(node.inputs.single); | |
| pushStatement( | |
| js.Return(pop()).withSourceInformation(node.sourceInformation), | |
| ); | |
| } | |
| } | |
| @override | |
| void visitThis(HThis node) { | |
| push(js.This()); | |
| } | |
| @override | |
| void visitThrow(HThrow node) { | |
| SourceInformation? sourceInformation = node.sourceInformation; | |
| if (node.isRethrow) { | |
| use(node.inputs[0]); | |
| pushStatement(js.Throw(pop()).withSourceInformation(sourceInformation)); | |
| } else { | |
| use(node.inputs[0]); | |
| if (node.withoutHelperFrame) { | |
| _pushCallStatic(_commonElements.initializeExceptionWrapper, [ | |
| pop(), | |
| _newErrorObject(sourceInformation), | |
| ], sourceInformation); | |
| } else { | |
| _pushCallStatic(_commonElements.wrapExceptionHelper, [ | |
| pop(), | |
| ], sourceInformation); | |
| } | |
| pushStatement(js.Throw(pop()).withSourceInformation(sourceInformation)); | |
| } | |
| } | |
| js.Expression _newErrorObject(SourceInformation? sourceInformation) { | |
| return js.js('new Error()').withSourceInformation(sourceInformation); | |
| } | |
| @override | |
| void visitAwait(HAwait node) { | |
| use(node.inputs[0]); | |
| push(js.Await(pop()).withSourceInformation(node.sourceInformation)); | |
| } | |
| @override | |
| void visitYield(HYield node) { | |
| use(node.inputs[0]); | |
| pushStatement( | |
| js.DartYield( | |
| pop(), | |
| node.hasStar, | |
| ).withSourceInformation(node.sourceInformation), | |
| ); | |
| } | |
| @override | |
| void visitRangeConversion(HRangeConversion node) { | |
| // Range conversion instructions are removed by the value range | |
| // analyzer. | |
| assert(false); | |
| } | |
| @override | |
| void visitBoundsCheck(HBoundsCheck node) { | |
| // TODO(ngeoffray): Separate the two checks of the bounds check, so, | |
| // e.g., the zero checks can be shared if possible. | |
| // If the checks always succeeds, we would have removed the bounds check | |
| // completely. | |
| assert(node.staticChecks != StaticBoundsChecks.alwaysTrue); | |
| if (node.staticChecks == StaticBoundsChecks.alwaysFalse) { | |
| _pushThrowWithHelper(_commonElements.throwIndexOutOfRangeException, [ | |
| node.array, | |
| node.reportedIndex, | |
| ], sourceInformation: node.sourceInformation); | |
| return; | |
| } | |
| HInstruction index = node.index; | |
| // Generate a test for out-of-bounds, either under or over the range. NaN | |
| // values can creep in, and comparisons on NaN are false, so | |
| // | |
| // if (i < 0) throw ... | |
| // | |
| // will fail to throw if `i` is NaN. The test | |
| // | |
| // if (!(i >= 0)) ... | |
| // | |
| // is 'NaN-safe'. | |
| // TODO(sra): Better analysis of possible NaN input. | |
| bool indexCanBeNaN = !_isDefinitelyNotNaN(index); | |
| js.Expression? under; | |
| js.Expression? over; | |
| if (index.isInteger(_abstractValueDomain).isPotentiallyFalse) { | |
| // Combined domain check and low bound check. `a >>> 0 !== a` is true for | |
| // `null`, `undefined`, `NaN`, and non-integral number and any integral | |
| // number outside the 32-bit unsigned range. | |
| use(index); | |
| js.Expression jsIndex = pop(); | |
| // This test is 'NaN-safe' since `a!==b` is the same as `!(a===b)`. | |
| under = js.js("# >>> 0 !== #", [jsIndex, jsIndex]); | |
| indexCanBeNaN = false; | |
| } else if (node.staticChecks != StaticBoundsChecks.alwaysAboveZero) { | |
| use(index); | |
| // The index must be an `int`, otherwise we could have used the combined | |
| // check above. | |
| if (indexCanBeNaN) { | |
| under = js.js('!(# >= 0)', pop()); | |
| } else { | |
| under = js.js('# < 0', pop()); | |
| } | |
| } | |
| if (node.staticChecks != StaticBoundsChecks.alwaysBelowLength) { | |
| use(index); | |
| js.Expression jsIndex = pop(); | |
| use(node.length); | |
| js.Expression jsLength = pop(); | |
| if (indexCanBeNaN) { | |
| over = js.js('!(# < #)', [jsIndex, jsLength]); | |
| } else { | |
| over = js.js('# >= #', [jsIndex, jsLength]); | |
| } | |
| } | |
| assert(over != null || under != null); | |
| js.Expression underOver; | |
| if (under == null) { | |
| underOver = over!; | |
| } else if (over == null) { | |
| underOver = under; | |
| } else { | |
| if (under is js.Prefix && | |
| under.op == '!' && | |
| over is js.Prefix && | |
| over.op == '!') { | |
| // De Morgans law: !(a) || !(b) <-> !(a && b) | |
| underOver = js.js('!(# && #)', [under.argument, over.argument]); | |
| } else { | |
| underOver = js.Binary('||', under, over); | |
| } | |
| } | |
| // Generate the call to the 'throw' helper in a block in case it needs | |
| // multiple statements. | |
| js.Block thenBody = js.Block.empty(); | |
| js.Block oldContainer = currentContainer; | |
| currentContainer = thenBody; | |
| _pushThrowWithHelper(_commonElements.throwIndexOutOfRangeException, [ | |
| node.array, | |
| node.reportedIndex, | |
| ], sourceInformation: node.sourceInformation); | |
| currentContainer = oldContainer; | |
| pushStatement( | |
| js.If.noElse( | |
| underOver, | |
| unwrapStatement(thenBody), | |
| ).withSourceInformation(node.sourceInformation), | |
| ); | |
| } | |
| bool _isDefinitelyNotNaN(HInstruction node) { | |
| if (node is HConstant) { | |
| if (node.isInteger(_abstractValueDomain).isDefinitelyTrue) return true; | |
| return false; | |
| } | |
| // TODO(sra): Use some form of dataflow. Starting from a small number you | |
| // can add or subtract a small number any number of times and still have a | |
| // finite number. Many operations, produce small numbers (some constants, | |
| // HGetLength, HBitAnd). This could be used to determine that most loop | |
| // indexes are finite and thus not NaN. | |
| return false; | |
| } | |
| void _pushThrowWithHelper( | |
| FunctionEntity helper, | |
| List<HInstruction> inputs, { | |
| SourceInformation? sourceInformation, | |
| }) { | |
| List<js.Expression> arguments = []; | |
| for (final input in inputs) { | |
| use(input); | |
| arguments.add(pop()); | |
| } | |
| _pushCallStatic(helper, arguments, sourceInformation); | |
| // BUG(4906): Using throw/return here adds to the size of the generated code | |
| // but it has the advantage of explicitly telling the JS engine that | |
| // this code path will terminate abruptly. Needs more work. | |
| pushStatement(js.Return(pop()).withSourceInformation(sourceInformation)); | |
| } | |
| void _pushCallStatic( | |
| FunctionEntity target, | |
| List<js.Expression> arguments, | |
| SourceInformation? sourceInformation, | |
| ) { | |
| _registry.registerStaticUse( | |
| StaticUse.staticInvoke(target, CallStructure.unnamed(arguments.length)), | |
| ); | |
| js.Expression jsTarget = _emitter.staticFunctionAccess(target); | |
| js.Call call = js.Call( | |
| jsTarget, | |
| List.of(arguments, growable: false), | |
| sourceInformation: sourceInformation, | |
| ); | |
| push(call); | |
| } | |
| @override | |
| void visitThrowExpression(HThrowExpression node) { | |
| use(node.inputs[0]); | |
| _pushCallStatic(_commonElements.throwExpressionHelper, [ | |
| pop(), | |
| if (node.withoutHelperFrame) _newErrorObject(node.sourceInformation), | |
| ], node.sourceInformation); | |
| } | |
| @override | |
| void visitSwitch(HSwitch node) { | |
| // Switches are handled using [visitSwitchInfo]. | |
| } | |
| @override | |
| void visitStatic(HStatic node) { | |
| MemberEntity element = node.element; | |
| if (element is FunctionEntity) { | |
| // TODO(sra): Static tear-offs should be constants. | |
| js.Expression expression = _emitter | |
| .staticClosureAccess(element) | |
| .withSourceInformation(node.sourceInformation); | |
| if (_shouldRecordConstructor(element)) { | |
| expression = expression.withAnnotation( | |
| _recordConstructorTearOff(element), | |
| ); | |
| } | |
| push(expression); | |
| _registry.registerStaticUse(StaticUse.staticTearOff(element)); | |
| } else if (element is FieldEntity) { | |
| push( | |
| _emitter | |
| .staticFieldAccess(element) | |
| .withSourceInformation(node.sourceInformation), | |
| ); | |
| _registry.registerStaticUse(StaticUse.staticGet(element)); | |
| } else { | |
| failedAt(node, 'HStatic must be a FieldEntity or FunctionEntity'); | |
| } | |
| } | |
| @override | |
| void visitLazyStatic(HLazyStatic node) { | |
| FieldEntity element = node.element; | |
| _registry.registerStaticUse(StaticUse.staticInit(element)); | |
| js.Expression lazyGetter = _emitter.isolateLazyInitializerAccess(element); | |
| js.Call call = js.Call( | |
| lazyGetter, | |
| [], | |
| sourceInformation: node.sourceInformation, | |
| ); | |
| push(call); | |
| } | |
| @override | |
| void visitStaticStore(HStaticStore node) { | |
| _registry.registerStaticUse(StaticUse.staticSet(node.element)); | |
| js.Expression variable = _emitter.staticFieldAccess(node.element); | |
| use(node.inputs[0]); | |
| push( | |
| js.Assignment( | |
| variable, | |
| pop(), | |
| ).withSourceInformation(node.sourceInformation), | |
| ); | |
| } | |
| @override | |
| void visitStringConcat(HStringConcat node) { | |
| use(node.left); | |
| js.Expression jsLeft = pop(); | |
| use(node.right); | |
| push( | |
| js.Binary( | |
| '+', | |
| jsLeft, | |
| pop(), | |
| ).withSourceInformation(node.sourceInformation), | |
| ); | |
| } | |
| @override | |
| void visitStringify(HStringify node) { | |
| HInstruction input = node.inputs.first; | |
| if (input.isString(_abstractValueDomain).isDefinitelyTrue) { | |
| use(input); | |
| } else if (input.isInteger(_abstractValueDomain).isDefinitelyTrue || | |
| input.isBoolean(_abstractValueDomain).isDefinitelyTrue) { | |
| // JavaScript's + operator with a string for the left operand will convert | |
| // the right operand to a string, and the conversion result is correct. | |
| use(input); | |
| if (node.usedBy.length == 1 && | |
| node.usedBy[0] is HStringConcat && | |
| node.usedBy[0].inputs[1] == node) { | |
| // The context is already <string> + value. | |
| } else { | |
| // Force an empty string for the first operand. | |
| push( | |
| js.Binary( | |
| '+', | |
| js.string(""), | |
| pop(), | |
| ).withSourceInformation(node.sourceInformation), | |
| ); | |
| } | |
| } else { | |
| FunctionEntity convertToString = | |
| _commonElements.stringInterpolationHelper; | |
| _registry.registerStaticUse( | |
| StaticUse.staticInvoke(convertToString, CallStructure.oneArg), | |
| ); | |
| js.Expression jsHelper = _emitter.staticFunctionAccess(convertToString); | |
| use(input); | |
| push( | |
| js.Call(jsHelper, [pop()], sourceInformation: node.sourceInformation), | |
| ); | |
| } | |
| } | |
| @override | |
| void visitLiteralList(HLiteralList node) { | |
| _registry | |
| // ignore:deprecated_member_use_from_same_package | |
| .registerInstantiatedClass(_commonElements.listClass); | |
| generateArrayLiteral(node); | |
| } | |
| void generateArrayLiteral(HLiteralList node) { | |
| List<js.Expression> elements = node.inputs.map((HInstruction input) { | |
| use(input); | |
| return pop(); | |
| }).toList(); | |
| push( | |
| js.ArrayInitializer(elements) | |
| .withSourceInformation(node.sourceInformation), | |
| ); | |
| } | |
| @override | |
| void visitIndex(HIndex node) { | |
| _metrics.countHIndex++; | |
| use(node.receiver); | |
| js.Expression receiver = pop(); | |
| use(node.index); | |
| push( | |
| js.PropertyAccess( | |
| receiver, | |
| pop(), | |
| ).withSourceInformation(node.sourceInformation), | |
| ); | |
| } | |
| @override | |
| void visitIndexAssign(HIndexAssign node) { | |
| use(node.receiver); | |
| js.Expression receiver = pop(); | |
| use(node.index); | |
| js.Expression index = pop(); | |
| use(node.value); | |
| push( | |
| js.Assignment( | |
| js.PropertyAccess(receiver, index), | |
| pop(), | |
| ).withSourceInformation(node.sourceInformation), | |
| ); | |
| } | |
| @override | |
| void visitCharCodeAt(HCharCodeAt node) { | |
| use(node.receiver); | |
| js.Expression receiver = pop(); | |
| use(node.index); | |
| push( | |
| js | |
| .js('#.charCodeAt(#)', [receiver, pop()]) | |
| .withSourceInformation(node.sourceInformation), | |
| ); | |
| } | |
| void checkTypeOf( | |
| HInstruction input, | |
| String cmp, | |
| String typeName, | |
| SourceInformation? sourceInformation, | |
| ) { | |
| use(input); | |
| js.Expression typeOf = js.Prefix("typeof", pop()); | |
| push( | |
| js.Binary( | |
| cmp, | |
| typeOf, | |
| js.string(typeName), | |
| ).withSourceInformation(sourceInformation), | |
| ); | |
| } | |
| void checkNum( | |
| HInstruction input, | |
| String cmp, | |
| SourceInformation? sourceInformation, | |
| ) { | |
| return checkTypeOf(input, cmp, 'number', sourceInformation); | |
| } | |
| void checkBool( | |
| HInstruction input, | |
| String cmp, | |
| SourceInformation? sourceInformation, | |
| ) { | |
| return checkTypeOf(input, cmp, 'boolean', sourceInformation); | |
| } | |
| @override | |
| void visitPrimitiveCheck(HPrimitiveCheck node) { | |
| js.Expression test = _generateReceiverOrArgumentTypeTest(node); | |
| js.Block oldContainer = currentContainer; | |
| js.Block body = currentContainer = js.Block.empty(); | |
| final sourceInformation = node.sourceInformation; | |
| switch (node.kind) { | |
| case PrimitiveCheckKind.argumentType: | |
| use(node.checkedInput); | |
| _pushCallStatic(_commonElements.throwIllegalArgumentException, [ | |
| pop(), | |
| ], node.sourceInformation); | |
| pushStatement( | |
| js.Return(pop()).withSourceInformation(sourceInformation), | |
| ); | |
| case PrimitiveCheckKind.receiverType: | |
| use(node.checkedInput); | |
| js.Name methodName = _namer.invocationName( | |
| node.receiverTypeCheckSelector!, | |
| ); | |
| js.Expression call = js | |
| .propertyCall(pop(), methodName, []) | |
| .withSourceInformation(sourceInformation); | |
| pushStatement(js.Return(call).withSourceInformation(sourceInformation)); | |
| } | |
| currentContainer = oldContainer; | |
| final then = unwrapStatement(body); | |
| pushStatement( | |
| js.If.noElse(test, then).withSourceInformation(sourceInformation), | |
| ); | |
| } | |
| js.Expression _generateReceiverOrArgumentTypeTest(HPrimitiveCheck node) { | |
| DartType type = node.typeExpression; | |
| HInstruction input = node.checkedInput; | |
| AbstractValue checkedType = node.checkedType; | |
| // This path is no longer used for indexable primitive types. | |
| assert(_abstractValueDomain.isJsIndexable(checkedType).isPotentiallyFalse); | |
| // Figure out if it is beneficial to use a null check. V8 generally prefers | |
| // 'typeof' checks, but for integers we cannot compile this test into a | |
| // single typeof check so the null check is cheaper. | |
| if (type == _commonElements.numType) { | |
| // input is !num | |
| checkNum(input, '!==', input.sourceInformation); | |
| return pop(); | |
| } | |
| if (type == _commonElements.boolType) { | |
| // input is !bool | |
| checkBool(input, '!==', input.sourceInformation); | |
| return pop(); | |
| } | |
| throw failedAt(input, 'Unexpected check: $type.'); | |
| } | |
| @override | |
| void visitNullCheck(HNullCheck node) { | |
| use(node.checkedInput); | |
| // We access a JavaScript member 'toString' as all objects besides `null` | |
| // and `undefined` have it. | |
| // TODO(35996): Pick a shorter field. The instruction has a selector and | |
| // field that could be used here to pick the 'right' field. The 'field' | |
| // might need to be propagated to an earlier HNullCheck. JSArray and | |
| // JSString have 'length'. | |
| pushStatement( | |
| js.ExpressionStatement( | |
| js.PropertyAccess.field( | |
| pop(), | |
| 'toString', | |
| ).withSourceInformation(node.sourceInformation), | |
| ), | |
| ); | |
| } | |
| @override | |
| void visitLateReadCheck(HLateReadCheck node) { | |
| // We generate code roughly equivalent to invoking: | |
| // | |
| // T _lateReadCheck<T>(T value, String name) { | |
| // if (isSentinel(value)) throw LateError.fieldNI(name); | |
| // return value; | |
| // } | |
| assert(!node.isRedundant(_closedWorld)); | |
| final sourceInformation = node.sourceInformation; | |
| _emitIsLateSentinel(node.checkedInput, sourceInformation); | |
| final condition = pop(); | |
| if (node.hasName) { | |
| use(node.name); | |
| _pushCallStatic(_commonElements.throwLateFieldNI, [ | |
| pop(), | |
| ], sourceInformation); | |
| } else { | |
| _pushCallStatic( | |
| _commonElements.throwUnnamedLateFieldNI, | |
| const [], | |
| sourceInformation, | |
| ); | |
| } | |
| // `condition && helper();` is smaller than `if (condition) helper();`. | |
| pushStatement( | |
| js.js | |
| .statement('# && #;', [condition, pop()]) | |
| .withSourceInformation(sourceInformation), | |
| ); | |
| } | |
| @override | |
| void visitLateWriteOnceCheck(HLateWriteOnceCheck node) { | |
| // We generate code roughly equivalent to invoking: | |
| // | |
| // void _lateWriteOnceCheck(Object? value, String name) { | |
| // if (!isSentinel(value)) throw LateError.fieldAI(name); | |
| // } | |
| assert(!node.isRedundant(_closedWorld)); | |
| final sourceInformation = node.sourceInformation; | |
| _emitIsLateSentinel(node.checkedInput, sourceInformation, inverse: true); | |
| final condition = pop(); | |
| if (node.hasName) { | |
| use(node.name); | |
| _pushCallStatic(_commonElements.throwLateFieldAI, [ | |
| pop(), | |
| ], sourceInformation); | |
| } else { | |
| _pushCallStatic( | |
| _commonElements.throwUnnamedLateFieldAI, | |
| [], | |
| sourceInformation, | |
| ); | |
| } | |
| // `condition && helper();` is smaller than `if (condition) helper();`. | |
| pushStatement( | |
| js.js | |
| .statement('# && #;', [condition, pop()]) | |
| .withSourceInformation(sourceInformation), | |
| ); | |
| } | |
| @override | |
| void visitLateInitializeOnceCheck(HLateInitializeOnceCheck node) { | |
| // We generate code roughly equivalent to invoking: | |
| // | |
| // void _lateInitializeOnceCheck(Object? value, String name) { | |
| // if (!isSentinel(value)) throw LateError.fieldADI(name); | |
| // } | |
| assert(!node.isRedundant(_closedWorld)); | |
| final sourceInformation = node.sourceInformation; | |
| _emitIsLateSentinel(node.checkedInput, sourceInformation, inverse: true); | |
| final condition = pop(); | |
| if (node.hasName) { | |
| use(node.name); | |
| _pushCallStatic(_commonElements.throwLateFieldADI, [ | |
| pop(), | |
| ], sourceInformation); | |
| } else { | |
| _pushCallStatic( | |
| _commonElements.throwUnnamedLateFieldADI, | |
| [], | |
| sourceInformation, | |
| ); | |
| } | |
| // `condition && helper();` is smaller than `if (condition) helper();`. | |
| pushStatement( | |
| js.js | |
| .statement('# && #;', [condition, pop()]) | |
| .withSourceInformation(sourceInformation), | |
| ); | |
| } | |
| @override | |
| void visitTypeKnown(HTypeKnown node) { | |
| // [HTypeKnown] instructions are removed before generating code. | |
| assert(false); | |
| } | |
| @override | |
| void visitRef(HRef node) { | |
| visit(node.value); | |
| } | |
| @override | |
| void visitIsTest(HIsTest node) { | |
| _metrics.countHIsTest++; | |
| _registry.registerTypeUse(TypeUse.isCheck(node.dartType)); | |
| use(node.typeInput); | |
| js.Expression first = pop(); | |
| use(node.checkedInput); | |
| js.Expression second = pop(); | |
| FieldEntity field = _commonElements.rtiIsField; | |
| js.Name name = _namer.instanceFieldPropertyName(field); | |
| push( | |
| js | |
| .js('#.#(#)', [first, name, second]) | |
| .withSourceInformation(node.sourceInformation), | |
| ); | |
| } | |
| @override | |
| void visitIsTestSimple(HIsTestSimple node) { | |
| _metrics.countHIsTestSimple++; | |
| _emitIsTestSimple(node); | |
| } | |
| void _emitIsTestSimple(HIsTestSimple node, {bool negative = false}) { | |
| use(node.checkedInput); | |
| js.Expression value = pop(); | |
| String relation = negative ? '!=' : '=='; | |
| js.Expression handleNegative(js.Expression test) => | |
| negative ? js.Prefix('!', test) : test; | |
| js.Expression typeof(String type) => | |
| js.Binary(relation, js.Prefix('typeof', value), js.string(type)); | |
| js.Expression isTest(FunctionEntity helper) { | |
| _registry.registerStaticUse( | |
| StaticUse.staticInvoke(helper, CallStructure.oneArg), | |
| ); | |
| js.Expression test = js.Call(_emitter.staticFunctionAccess(helper), [ | |
| value, | |
| ]); | |
| return handleNegative(test); | |
| } | |
| late js.Expression test; | |
| switch (node.specialization) { | |
| case SimpleIsTestSpecialization.isNull: | |
| case SimpleIsTestSpecialization.isNotNull: | |
| // These cases should be lowered using [HIdentity] during optimization. | |
| failedAt(node, 'Missing lowering'); | |
| case SimpleIsTestSpecialization.isString: | |
| test = typeof("string"); | |
| break; | |
| case SimpleIsTestSpecialization.isBool: | |
| test = isTest(_commonElements.specializedIsBool); | |
| break; | |
| case SimpleIsTestSpecialization.isNum: | |
| test = typeof("number"); | |
| break; | |
| case SimpleIsTestSpecialization.isInt: | |
| test = isTest(_commonElements.specializedIsInt); | |
| break; | |
| case SimpleIsTestSpecialization.isArrayTop: | |
| test = handleNegative(js.js('Array.isArray(#)', [value])); | |
| break; | |
| case InstanceOfIsTestSpecialization(interfaceType: final type): | |
| _registry.registerTypeUse(TypeUse.constructorReference(type)); | |
| test = handleNegative( | |
| js.js('# instanceof #', [ | |
| value, | |
| _emitter.constructorAccess(type.element), | |
| ]), | |
| ); | |
| } | |
| push(test.withSourceInformation(node.sourceInformation)); | |
| } | |
| @override | |
| void visitAsCheck(HAsCheck node) { | |
| use(node.typeInput); | |
| js.Expression first = pop(); | |
| use(node.checkedInput); | |
| js.Expression second = pop(); | |
| _registry.registerTypeUse(TypeUse.isCheck(node.checkedTypeExpression)); | |
| FieldEntity field = _commonElements.rtiAsField; | |
| js.Name name = _namer.instanceFieldPropertyName(field); | |
| push( | |
| js | |
| .js('#.#(#)', [first, name, second]) | |
| .withSourceInformation(node.sourceInformation), | |
| ); | |
| } | |
| @override | |
| void visitAsCheckSimple(HAsCheckSimple node) { | |
| use(node.checkedInput); | |
| FunctionEntity method = node.method; | |
| _registry.registerStaticUse( | |
| StaticUse.staticInvoke(method, CallStructure.oneArg), | |
| ); | |
| js.Expression methodAccess = _emitter.staticFunctionAccess(method); | |
| push( | |
| js | |
| .js(r'#(#)', [methodAccess, pop()]) | |
| .withSourceInformation(node.sourceInformation), | |
| ); | |
| } | |
| @override | |
| Never visitSubtypeCheck(HSubtypeCheck node) { | |
| throw UnimplementedError('SsaCodeGenerator.visitSubtypeCheck $node'); | |
| } | |
| @override | |
| void visitLoadType(HLoadType node) { | |
| // 'findType' will be called somewhere to initialize the type reference. | |
| _registry.registerStaticUse( | |
| StaticUse.staticInvoke(_commonElements.findType, CallStructure.oneArg), | |
| ); | |
| TypeReference reference = TypeReference(node.typeExpression); | |
| reference.forLazyInitializer = currentGraph.isLazyInitializer; | |
| push(reference); | |
| } | |
| @override | |
| void visitInstanceEnvironment(HInstanceEnvironment node) { | |
| HInstruction input = node.inputs.single; | |
| use(input); | |
| js.Expression receiver = pop(); | |
| void useRtiField() { | |
| push(js.js(r'#.#', [receiver, _namer.rtiFieldJsName])); | |
| } | |
| void useHelper(FunctionEntity helper) { | |
| _registry.registerStaticUse( | |
| StaticUse.staticInvoke(helper, CallStructure.oneArg), | |
| ); | |
| js.Expression helperAccess = _emitter.staticFunctionAccess(helper); | |
| push( | |
| js | |
| .js(r'#(#)', [helperAccess, receiver]) | |
| .withSourceInformation(node.sourceInformation), | |
| ); | |
| } | |
| // Try to use the 'rti' field, or a specialization of 'instanceType'. | |
| AbstractValue receiverMask = node.codegenInputType; | |
| AbstractBool isArray = _abstractValueDomain.isInstanceOf( | |
| receiverMask, | |
| _commonElements.jsArrayClass, | |
| ); | |
| if (isArray.isDefinitelyTrue) { | |
| useHelper(_commonElements.arrayInstanceType); | |
| return; | |
| } | |
| if (isArray.isDefinitelyFalse) { | |
| // See if the receiver type narrows the set of classes to ones that all | |
| // have a stored type field. | |
| // TODO(sra): Currently the only convenient query is [getExactClass]. We | |
| // should have a (cached) query to iterate over all the concrete classes | |
| // in [receiverMask]. | |
| // TODO(sra): Store the context class on the HInstanceEnvironment. This | |
| // would allow the subtype classes to be iterated. | |
| ClassEntity? receiverClass = _abstractValueDomain.getExactClass( | |
| receiverMask, | |
| ); | |
| if (receiverClass != null) { | |
| if (_closedWorld.rtiNeed.classNeedsTypeArguments(receiverClass)) { | |
| useRtiField(); | |
| return; | |
| } | |
| } | |
| // If the type is not intercepted and is not a closure, use the 'simple' | |
| // helper. | |
| if (_abstractValueDomain.isInterceptor(receiverMask).isDefinitelyFalse) { | |
| if (_abstractValueDomain | |
| .isInstanceOf(receiverMask, _commonElements.closureClass) | |
| .isDefinitelyFalse) { | |
| useHelper(_commonElements.simpleInstanceType); | |
| return; | |
| } | |
| } | |
| } | |
| useHelper(_commonElements.instanceType); | |
| } | |
| @override | |
| void visitTypeEval(HTypeEval node) { | |
| // Call `env._eval("recipe")`. | |
| use(node.inputs[0]); | |
| js.Expression environment = pop(); | |
| // Instead of generating `env._eval("$n")`, generate appropriate field | |
| // accesses where possible. | |
| TypeEnvironmentStructure envStructure = node.envStructure; | |
| TypeRecipe typeExpression = node.typeExpression; | |
| if (envStructure is FullTypeEnvironmentStructure && | |
| typeExpression is TypeExpressionRecipe) { | |
| final type = typeExpression.type; | |
| if (type is TypeVariableType) { | |
| int? index = indexTypeVariable( | |
| _closedWorld, | |
| _rtiSubstitutions, | |
| envStructure, | |
| type, | |
| ); | |
| if (index != null) { | |
| assert(index >= 1); | |
| List<TypeVariableType> bindings = envStructure.bindings; | |
| if (bindings.isNotEmpty) { | |
| // If the environment is a binding RTI, we should never index past | |
| // its length (i.e. into its base), since in that case, we could | |
| // eval against the base directly. | |
| assert(index <= bindings.length); | |
| } else { | |
| // If the environment is an interface RTI, use precomputed fields | |
| // for common accesses. | |
| if (index == 1) { | |
| push( | |
| _loadField( | |
| environment, | |
| _commonElements.rtiPrecomputed1Field, | |
| node.sourceInformation, | |
| ), | |
| ); | |
| return; | |
| } | |
| } | |
| js.Expression rest = _loadField( | |
| environment, | |
| _commonElements.rtiRestField, | |
| node.sourceInformation, | |
| ); | |
| push( | |
| js.PropertyAccess.indexed( | |
| rest, | |
| index - 1, | |
| ).withSourceInformation(node.sourceInformation), | |
| ); | |
| return; | |
| } | |
| } | |
| } | |
| RecipeEncoding encoding = _rtiRecipeEncoder.encodeRecipe( | |
| _emitter, | |
| node.envStructure, | |
| node.typeExpression, | |
| ); | |
| js.Expression recipe = encoding.recipe; | |
| for (TypeVariableType typeVariable in encoding.typeVariables) { | |
| _registry.registerTypeUse(TypeUse.namedTypeVariable(typeVariable)); | |
| } | |
| final method = _commonElements.rtiEvalMethod; | |
| Selector selector = Selector.fromElement(method); | |
| js.Name methodLiteral = _namer.invocationName(selector); | |
| push( | |
| js | |
| .js('#.#(#)', [environment, methodLiteral, recipe]) | |
| .withSourceInformation(node.sourceInformation), | |
| ); | |
| _registry.registerStaticUse( | |
| StaticUse.directInvoke(method, selector.callStructure, null), | |
| ); | |
| } | |
| @override | |
| void visitTypeBind(HTypeBind node) { | |
| // Call `env1._bind(env2)`. | |
| assert(node.inputs.length == 2); | |
| use(node.inputs[0]); | |
| js.Expression environment = pop(); | |
| use(node.inputs[1]); | |
| js.Expression extensions = pop(); | |
| final method = _commonElements.rtiBindMethod; | |
| Selector selector = Selector.fromElement(method); | |
| js.Name methodLiteral = _namer.invocationName(selector); | |
| push( | |
| js | |
| .js('#.#(#)', [environment, methodLiteral, extensions]) | |
| .withSourceInformation(node.sourceInformation), | |
| ); | |
| _registry.registerStaticUse( | |
| StaticUse.directInvoke(method, selector.callStructure, null), | |
| ); | |
| } | |
| void _emitIsLateSentinel( | |
| HInstruction input, | |
| SourceInformation? sourceInformation, { | |
| bool inverse = false, | |
| }) { | |
| use(input); | |
| js.Expression value = pop(); | |
| js.Expression sentinel = _emitter.constantReference( | |
| LateSentinelConstantValue(), | |
| ); | |
| push( | |
| js.Binary( | |
| mapRelationalOperator('===', inverse), | |
| value, | |
| sentinel, | |
| ).withSourceInformation(sourceInformation), | |
| ); | |
| } | |
| @override | |
| void visitIsLateSentinel(HIsLateSentinel node) { | |
| _metrics.countHIsLateSentinel++; | |
| _emitIsLateSentinel(node.inputs.single, node.sourceInformation); | |
| } | |
| @override | |
| void visitArrayFlagsGet(HArrayFlagsGet node) { | |
| use(node.inputs.single); | |
| js.Expression array = pop(); | |
| js.Expression flags = js.js(r'#.#', [ | |
| array, | |
| _namer.fixedNames.arrayFlagsPropertyName, | |
| ]); | |
| if (isGenerateAtUseSite(node) && node.usedBy.single is HArrayFlagsCheck) { | |
| // The enclosing expression will be an immediate `& mask`. | |
| push(flags); | |
| } else { | |
| // The flags are reused, possibly hoisted, so force an `undefined` to be a | |
| // small integer once rather than at each check. | |
| push(js.js(r'# | 0', flags)); | |
| } | |
| } | |
| @override | |
| void visitArrayFlagsSet(HArrayFlagsSet node) { | |
| use(node.inputs[0]); | |
| js.Expression array = pop(); | |
| use(node.inputs[1]); | |
| js.Expression arrayFlags = pop(); | |
| pushStatement( | |
| js.js | |
| .statement(r'#.# = #;', [ | |
| array, | |
| _namer.fixedNames.arrayFlagsPropertyName, | |
| arrayFlags, | |
| ]) | |
| .withSourceInformation(node.sourceInformation), | |
| ); | |
| } | |
| @override | |
| void visitArrayFlagsCheck(HArrayFlagsCheck node) { | |
| use(node.array); | |
| js.Expression array = pop(); | |
| js.Expression? test; | |
| if (!node.alwaysThrows()) { | |
| use(node.arrayFlags); | |
| js.Expression arrayFlags = pop(); | |
| use(node.checkFlags); | |
| js.Expression checkFlags = pop(); | |
| test = js.js('# & #', [arrayFlags, checkFlags]); | |
| } | |
| List<js.Expression> arguments = [array]; | |
| if (node.hasOperation) { | |
| use(node.operation); | |
| arguments.add(pop()); | |
| } | |
| if (node.hasVerb) { | |
| use(node.verb); | |
| arguments.add(pop()); | |
| } | |
| _pushCallStatic( | |
| _commonElements.throwUnsupportedOperation, | |
| arguments, | |
| node.sourceInformation, | |
| ); | |
| js.Statement check; | |
| if (test == null) { | |
| check = js.js.statement('#;', pop()); | |
| } else { | |
| check = js.js.statement('# && #;', [test, pop()]); | |
| } | |
| pushStatement(check.withSourceInformation(node.sourceInformation)); | |
| } | |
| } | |
| /// A sequence of [RecordedUse] that supports O(1) composition and | |
| /// linear iteration safe for deep, unbalanced trees. | |
| /// | |
| /// Specialized for recording constant uses in [SsaCodeGenerator]. | |
| abstract class _RecordedUseSequence extends Iterable<RecordedUse> { | |
| static const _RecordedUseSequence _empty = _RecordedUseLeaf([], 0); | |
| @override | |
| final int length; | |
| @override | |
| bool get isEmpty => length == 0; | |
| const _RecordedUseSequence._(this.length); | |
| factory _RecordedUseSequence.fromList(List<RecordedUse> list) { | |
| if (list.isEmpty) return _empty; | |
| return _RecordedUseLeaf(list, list.length); | |
| } | |
| _RecordedUseSequence operator +(_RecordedUseSequence other) { | |
| if (isEmpty) return other; | |
| if (other.isEmpty) return this; | |
| return _RecordedUseComposite(this, other); | |
| } | |
| @override | |
| Iterator<RecordedUse> get iterator => _RecordedUseStackIterator(this); | |
| } | |
| class _RecordedUseLeaf extends _RecordedUseSequence { | |
| final List<RecordedUse> items; | |
| const _RecordedUseLeaf(this.items, int length) : super._(length); | |
| } | |
| class _RecordedUseComposite extends _RecordedUseSequence { | |
| final _RecordedUseSequence left; | |
| final _RecordedUseSequence right; | |
| _RecordedUseComposite(this.left, this.right) | |
| : super._(left.length + right.length); | |
| } | |
| class _RecordedUseStackIterator implements Iterator<RecordedUse> { | |
| final List<_RecordedUseSequence> _stack = []; | |
| Iterator<RecordedUse>? _currentLeafIterator; | |
| RecordedUse? _currentValue; | |
| _RecordedUseStackIterator(_RecordedUseSequence root) { | |
| _pushLeft(root); | |
| } | |
| void _pushLeft(_RecordedUseSequence node) { | |
| _RecordedUseSequence current = node; | |
| while (current is _RecordedUseComposite) { | |
| _stack.add(current.right); | |
| current = current.left; | |
| } | |
| _currentLeafIterator = (current as _RecordedUseLeaf).items.iterator; | |
| } | |
| @override | |
| RecordedUse get current => _currentValue!; | |
| @override | |
| bool moveNext() { | |
| if (_currentLeafIterator != null && _currentLeafIterator!.moveNext()) { | |
| _currentValue = _currentLeafIterator!.current; | |
| return true; | |
| } | |
| if (_stack.isNotEmpty) { | |
| _pushLeft(_stack.removeLast()); | |
| return moveNext(); | |
| } | |
| _currentValue = null; | |
| return false; | |
| } | |
| } |