blob: 8a7efbe7f9f654d20174684dad97aa7ea7a3c9e8 [file] [log] [blame]
// Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
import 'dart:collection' show Queue;
// ignore: implementation_imports
import 'package:front_end/src/api_unstable/dart2js.dart' show Link;
import '../common.dart';
import '../common/elements.dart' show JCommonElements;
import '../common/metrics.dart';
import '../common/names.dart';
import '../common/codegen.dart' show CodegenRegistry;
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/interceptor_data.dart';
import '../js_backend/codegen_inputs.dart' show CodegenInputs;
import '../js_backend/native_data.dart';
import '../js_backend/namer.dart' show ModularNamer;
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/resource_identifier.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();
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,
);
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,
);
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 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,
);
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 = js.For(
jsInitialization,
jsCondition,
jsUpdates,
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 {
jsCondition = newLiteralBool(true, info.sourceInformation);
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 = js.While(
jsCondition!,
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 = js.While(
newLiteralBool(true, info.sourceInformation),
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;
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;
Object? resourceIdentifierAnnotation;
if (element is ConstructorEntity) {
CallStructure callStructure = CallStructure.unnamed(
arguments.length,
node.typeArguments.length,
);
staticUse = StaticUse.constructorInvoke(element, callStructure);
} else if (element.isGetter) {
staticUse = StaticUse.staticGet(element);
} else if (element.isSetter) {
staticUse = StaticUse.staticSet(element);
} else {
assert(element.isFunction);
CallStructure callStructure = CallStructure.unnamed(
arguments.length,
node.typeArguments.length,
);
staticUse = StaticUse.staticInvoke(
element,
callStructure,
node.typeArguments,
);
if (_closedWorld.annotationsData.methodIsResourceIdentifier(element)) {
resourceIdentifierAnnotation = _methodResourceIdentifier(
element,
callStructure,
node.inputs,
node.sourceInformation,
);
}
}
_registry.registerStaticUse(staticUse);
push(_emitter.staticFunctionAccess(element));
push(
js.Call(pop(), arguments, sourceInformation: node.sourceInformation),
);
if (resourceIdentifierAnnotation != null) {
push(pop().withAnnotation(resourceIdentifierAnnotation));
}
}
}
ResourceIdentifier _methodResourceIdentifier(
FunctionEntity element,
CallStructure callStructure,
List<HInstruction> arguments,
SourceInformation? sourceInformation,
) {
ConstantValue? findConstant(HInstruction node) {
while (node is HLateValue) {
node = node.target;
}
return node is HConstant ? node.constant : null;
}
final definition = _closedWorld.elementMap.getMemberDefinition(element);
final uri = definition.location.uri;
final builder = ResourceIdentifierBuilder(element.name!, uri);
if (sourceInformation != null) {
_addSourceInformationToResourceIdentiferBuilder(
builder,
sourceInformation,
);
}
for (int i = 0; i < arguments.length; i++) {
builder.add('${i + 1}', findConstant(arguments[i]));
}
return builder.finish();
}
void _addSourceInformationToResourceIdentiferBuilder(
ResourceIdentifierBuilder builder,
SourceInformation sourceInformation,
) {
SourceLocation? location =
sourceInformation.startPosition ??
sourceInformation.innerPosition ??
sourceInformation.endPosition;
if (location != null) {
final sourceUri = location.sourceUri;
if (sourceUri != null) {
// Is [sourceUri] normalized in some way or does that need to be done
// here?
builder.addLocation(sourceUri, location.line, location.column);
}
}
}
@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;
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);
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]);
}
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 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);
}
}
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);
}
push(expression);
}
@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.
push(
_emitter
.staticClosureAccess(element)
.withSourceInformation(node.sourceInformation),
);
_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));
}
}