6b693e00bc
This adds a VariableDeclaration node which is used in ForStatement instead of VariableStatement. This is a step towards removing the initializer from Variable. Long term, VariableDeclaration will own the initializer expression for variables and function parameters will have a defaultValue property instead of using the initializer property for the default value. TEST=existing Change-Id: I4a663eeb6006a0f9f098fb2b3e3b502d2ae583b0 Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/505681 Reviewed-by: Stephen Adams <sra@google.com> Reviewed-by: Martin Kustermann <kustermann@google.com> Reviewed-by: Chloe Stefantsova <cstefantsova@google.com> Reviewed-by: Nicholas Shahan <nshahan@google.com>
6226 lines
193 KiB
Dart
6226 lines
193 KiB
Dart
// Copyright (c) 2022, 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 LinkedHashMap;
|
|
|
|
import 'package:collection/collection.dart';
|
|
import 'package:front_end/src/api_prototype/external_effect.dart'
|
|
show ExternalEffect;
|
|
import 'package:kernel/ast.dart';
|
|
import 'package:kernel/names.dart';
|
|
import 'package:kernel/type_environment.dart';
|
|
import 'package:wasm_builder/wasm_builder.dart' as w;
|
|
|
|
import 'async.dart';
|
|
import 'class_info.dart';
|
|
import 'closures.dart';
|
|
import 'functions.dart';
|
|
import 'globals.dart';
|
|
import 'intrinsics.dart';
|
|
import 'param_info.dart';
|
|
import 'records.dart';
|
|
import 'reference_extensions.dart';
|
|
import 'sync_star.dart';
|
|
import 'translator.dart';
|
|
import 'types.dart';
|
|
|
|
abstract class CodeGenerator {
|
|
// The two parameters here are used for inlining:
|
|
//
|
|
// If the user
|
|
//
|
|
// * inlines the code, it will provide locals and a return label
|
|
//
|
|
// * doesn't inline (i.e. makes new function with this code) it will provide
|
|
// the parameters of the function and no return label.
|
|
//
|
|
void generate(
|
|
w.InstructionsBuilder b,
|
|
List<w.Local> paramLocals,
|
|
w.Label? returnLabel,
|
|
);
|
|
}
|
|
|
|
/// Main code generator for member bodies.
|
|
///
|
|
/// The [generate] method first collects all local functions and function
|
|
/// expressions in the body and then generates code for the body. Code for the
|
|
/// local functions and function expressions must be generated separately by
|
|
/// calling the [generateLambda] method on all lambdas in [closures].
|
|
///
|
|
/// A new [CodeGenerator] object must be created for each new member or lambda.
|
|
///
|
|
/// Every visitor method for an expression takes in the Wasm type that it is
|
|
/// expected to leave on the stack (or the special [voidMarker] to indicate that
|
|
/// it should leave nothing). It returns what it actually left on the stack. The
|
|
/// code generation for every expression or subexpression is done via the
|
|
/// [translateExpression] method, which emits appropriate conversion code if the
|
|
/// produced type is not a subtype of the expected type.
|
|
abstract class AstCodeGenerator
|
|
extends ExpressionVisitor1<w.ValueType, w.ValueType>
|
|
with
|
|
ExpressionVisitor1DefaultMixin<w.ValueType, w.ValueType>,
|
|
ExpressionVisitor1ExperimentExclusionMixin<w.ValueType, w.ValueType>,
|
|
StatementVisitorExperimentExclusionMixin<void>
|
|
implements InitializerVisitor<void>, StatementVisitor<void>, CodeGenerator {
|
|
final Translator translator;
|
|
final w.FunctionType functionType;
|
|
final Member enclosingMember;
|
|
|
|
// To be initialized in `generate()`
|
|
late w.InstructionsBuilder b;
|
|
late final List<w.Local> paramLocals;
|
|
late final w.Label? returnLabel;
|
|
|
|
late final Intrinsifier intrinsifier = Intrinsifier(this);
|
|
late final StaticTypeContext typeContext = StaticTypeContext(
|
|
enclosingMember,
|
|
translator.typeEnvironment,
|
|
);
|
|
|
|
late final Closures closures;
|
|
|
|
bool exceptionLocationPrinted = false;
|
|
|
|
final Map<Variable, w.Local> locals = {};
|
|
w.Local? thisLocal;
|
|
w.Local? preciseThisLocal;
|
|
w.Local? returnValueLocal;
|
|
final Map<TypeParameter, w.Local> typeLocals = {};
|
|
|
|
/// Finalizers to run on `return`.
|
|
final List<TryBlockFinalizer> returnFinalizers = [];
|
|
|
|
/// Finalizers to run on a `break`. `breakFinalizers[L].last` (which should
|
|
/// always be present) is the `br` target for the label `L` that will run the
|
|
/// finalizers, or break out of the loop.
|
|
final LinkedHashMap<LabeledStatement, List<w.Label>> breakFinalizers =
|
|
LinkedHashMap();
|
|
|
|
final List<({w.Local exceptionLocal, w.Local stackTraceLocal})>
|
|
tryBlockLocals = [];
|
|
|
|
final Map<SwitchCase, w.Label> switchLabels = {};
|
|
|
|
/// Maps a switch statement to the information used when doing a backward
|
|
/// jump to one of the cases in the switch statement
|
|
final Map<SwitchStatement, SwitchBackwardJumpInfo> switchBackwardJumpInfos =
|
|
{};
|
|
|
|
/// Create a code generator for a member or one of its lambdas.
|
|
AstCodeGenerator(this.translator, this.functionType, this.enclosingMember);
|
|
|
|
List<w.ValueType> get outputs => functionType.outputs;
|
|
|
|
w.ValueType get returnType => translator.outputOrVoid(outputs);
|
|
|
|
TranslatorOptions get options => translator.options;
|
|
|
|
w.ValueType get voidMarker => translator.voidMarker;
|
|
|
|
Types get types => translator.types;
|
|
|
|
w.ValueType translateType(DartType type) => translator.translateType(type);
|
|
|
|
w.Local addLocal(w.ValueType type, {String? name}) =>
|
|
b.addLocal(type, name: name);
|
|
|
|
DartType dartTypeOf(Expression exp) {
|
|
if (exp is ConstantExpression) {
|
|
// For constant expressions `getStaticType` returns often `DynamicType`
|
|
// instead of a more precise type. See http://dartbug.com/60368
|
|
return exp.constant.getType(typeContext);
|
|
}
|
|
return exp.getStaticType(typeContext);
|
|
}
|
|
|
|
void unimplemented(
|
|
TreeNode node,
|
|
Object message,
|
|
List<w.ValueType> expectedTypes,
|
|
) {
|
|
final text = "Not implemented: $message at ${node.location}";
|
|
print(text);
|
|
b.comment(text);
|
|
b.block(const [], expectedTypes);
|
|
b.unreachable();
|
|
b.end();
|
|
}
|
|
|
|
@override
|
|
w.ValueType defaultExpression(Expression node, w.ValueType expectedType) {
|
|
unimplemented(node, node.runtimeType, [
|
|
if (expectedType != voidMarker) expectedType,
|
|
]);
|
|
return expectedType;
|
|
}
|
|
|
|
Source? _sourceMapSource;
|
|
int _sourceMapFileOffset = TreeNode.noOffset;
|
|
|
|
/// Update the [Source] for the AST nodes being compiled.
|
|
///
|
|
/// The [Source] is used to resolve [TreeNode.fileOffset]s to file URI, line,
|
|
/// and column numbers, to be able to generate source mappings, in
|
|
/// [setSourceMapFileOffset].
|
|
///
|
|
/// Setting this `null` disables source mapping for the instructions being
|
|
/// generated.
|
|
///
|
|
/// This should be called before [setSourceMapFileOffset] as the file offset
|
|
/// passed to that function is resolved using the [Source].
|
|
///
|
|
/// Returns the old [Source], which can be used to restore the source mapping
|
|
/// after visiting a sub-tree.
|
|
Source? setSourceMapSource(Source? source) {
|
|
final old = _sourceMapSource;
|
|
_sourceMapSource = source;
|
|
return old;
|
|
}
|
|
|
|
/// Update the source location of the AST nodes being compiled in the source
|
|
/// map.
|
|
///
|
|
/// When the offset is [TreeNode.noOffset], this disables mapping the
|
|
/// generated instructions.
|
|
///
|
|
/// Returns the old file offset, which can be used to restore the source
|
|
/// mapping after vising a sub-tree.
|
|
int setSourceMapFileOffset(int fileOffset) {
|
|
if (!b.recordSourceMaps) {
|
|
final old = _sourceMapFileOffset;
|
|
_sourceMapFileOffset = fileOffset;
|
|
return old;
|
|
}
|
|
if (fileOffset == TreeNode.noOffset) {
|
|
b.stopSourceMapping();
|
|
final old = _sourceMapFileOffset;
|
|
_sourceMapFileOffset = fileOffset;
|
|
return old;
|
|
}
|
|
final source = _sourceMapSource!;
|
|
final fileUri = source.fileUri!;
|
|
final location = source.getLocation(fileUri, fileOffset);
|
|
final old = _sourceMapFileOffset;
|
|
_sourceMapFileOffset = fileOffset;
|
|
b.startSourceMapping(
|
|
fileUri,
|
|
location.line - 1,
|
|
location.column - 1,
|
|
enclosingMember.name.text,
|
|
);
|
|
return old;
|
|
}
|
|
|
|
/// Calls [setSourceMapSource] and [setSourceMapFileOffset].
|
|
(Source?, int) setSourceMapSourceAndFileOffset(
|
|
Source? source,
|
|
int fileOffset,
|
|
) {
|
|
final oldSource = setSourceMapSource(source);
|
|
final oldFileOffset = setSourceMapFileOffset(fileOffset);
|
|
return (oldSource, oldFileOffset);
|
|
}
|
|
|
|
/// Generate code while preventing recursive inlining.
|
|
@override
|
|
void generate(
|
|
w.InstructionsBuilder b,
|
|
List<w.Local> paramLocals,
|
|
w.Label? returnLabel,
|
|
) {
|
|
this.b = b;
|
|
this.paramLocals = paramLocals;
|
|
this.returnLabel = returnLabel;
|
|
|
|
translator.membersBeingGenerated.add(enclosingMember);
|
|
generateInternal();
|
|
translator.membersBeingGenerated.remove(enclosingMember);
|
|
}
|
|
|
|
// Generate the body.
|
|
void generateInternal();
|
|
|
|
void _setupLocalParameters(
|
|
Member member,
|
|
ParameterInfo paramInfo,
|
|
int parameterOffset,
|
|
int implicitParams, {
|
|
bool isForwarder = false,
|
|
bool canSafelyOmitImplicitChecks = false,
|
|
}) {
|
|
final memberFunction = member.function!;
|
|
|
|
final (
|
|
:typeParameters,
|
|
:typeParametersToTypeCheck,
|
|
:positional,
|
|
:positionalToTypeCheck,
|
|
:named,
|
|
:namedToTypeCheck,
|
|
) = translator.getParametersToCheck(
|
|
member,
|
|
);
|
|
|
|
for (int i = 0; i < typeParameters.length; i++) {
|
|
final typeParameter = typeParameters[i];
|
|
typeLocals[typeParameter] = paramLocals[parameterOffset + i];
|
|
}
|
|
final mayNeedToCheckTypes = translator.needToCheckTypesFor(member);
|
|
if (mayNeedToCheckTypes) {
|
|
for (int i = 0; i < typeParameters.length; i++) {
|
|
final typeParameter = typeParameters[i];
|
|
if (translator.needToCheckTypeParameter(typeParameter)) {
|
|
final typeParameterToCheck = typeParametersToTypeCheck[i];
|
|
_generateTypeArgumentBoundCheck(
|
|
typeParameter.name!,
|
|
typeLocals[typeParameter]!,
|
|
typeParameterToCheck,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
void setupParamLocal(
|
|
DartType variableTypeToCheck,
|
|
Variable variable,
|
|
int index,
|
|
Constant? defaultValue,
|
|
bool isRequired,
|
|
) {
|
|
final localIndex = implicitParams + index;
|
|
w.Local local = paramLocals[localIndex];
|
|
final variableName = variable.name;
|
|
if (variableName != null && variableName.isNotEmpty) {
|
|
b.localNames[local.index] = variableName;
|
|
}
|
|
if (defaultValue == ParameterInfo.defaultValueSentinel) {
|
|
// The default value for this parameter differs between implementations
|
|
// within the same selector. This means that callers will pass the
|
|
// default value sentinel to indicate that the parameter is not given.
|
|
// The callee must check for the sentinel value and substitute the
|
|
// actual default value.
|
|
//
|
|
// NOTE: The default sentinel is a dummy instance of the wasm type of
|
|
// the parameter in the function signature. This type may be a super
|
|
// type of the kind of arguments we actually see in practice.
|
|
// (e.g. we may know that only nullable one byte strings can flow into
|
|
// the argument, but the wasm type may be of object type). So we first
|
|
// have to handle sentinel before we can downcast the value.
|
|
b.local_get(local);
|
|
translator.constants.instantiateConstant(
|
|
b,
|
|
ParameterInfo.defaultValueSentinel,
|
|
local.type,
|
|
);
|
|
b.ref_eq();
|
|
b.if_();
|
|
translateExpression(variable.initializer!, local.type);
|
|
b.local_set(local);
|
|
b.end();
|
|
}
|
|
if (!isForwarder) {
|
|
// TFA may have inferred a very precise type for the incoming arguments,
|
|
// but the wasm function parameter type may not reflect this (e.g. due
|
|
// to upper-bounding in dispatch table row building)
|
|
// => This means, we may need to do a downcast here.
|
|
final incomingArgumentType = translator.translateTypeOfParameter(
|
|
variable,
|
|
isRequired,
|
|
);
|
|
if (!local.type.isSubtypeOf(incomingArgumentType)) {
|
|
final newLocal = addLocal(incomingArgumentType);
|
|
b.local_get(local);
|
|
translator.convertType(b, local.type, newLocal.type);
|
|
b.local_set(newLocal);
|
|
local = newLocal;
|
|
}
|
|
}
|
|
if (mayNeedToCheckTypes) {
|
|
if (translator.needToCheckParameter(
|
|
variable,
|
|
uncheckedEntry: canSafelyOmitImplicitChecks,
|
|
)) {
|
|
final boxedType = variable.type.isPotentiallyNullable
|
|
? translator.topType
|
|
: translator.topTypeNonNullable;
|
|
w.Local operand = local;
|
|
if (!operand.type.isSubtypeOf(boxedType)) {
|
|
final boxedOperand = addLocal(boxedType);
|
|
b.local_get(operand);
|
|
translator.convertType(b, operand.type, boxedOperand.type);
|
|
b.local_set(boxedOperand);
|
|
operand = boxedOperand;
|
|
}
|
|
b.local_get(operand);
|
|
_generateArgumentTypeCheck(
|
|
variable.name!,
|
|
operand.type as w.RefType,
|
|
variableTypeToCheck,
|
|
);
|
|
}
|
|
}
|
|
if (!isForwarder && !variable.isFinal) {
|
|
// We now have a precise local that can contain the values passed by
|
|
// callers, but the body may assign less precise types to this variable,
|
|
// so we may introduce another local variable that is less precise.
|
|
// => Binaryen will simplify the above downcast and this upcast.
|
|
final variableType = translator.translateTypeOfLocalVariable(variable);
|
|
if (!variableType.isSubtypeOf(local.type)) {
|
|
w.Local newLocal = addLocal(variableType);
|
|
b.local_get(local);
|
|
translator.convertType(b, local.type, newLocal.type);
|
|
b.local_set(newLocal);
|
|
local = newLocal;
|
|
}
|
|
}
|
|
|
|
locals[variable] = local;
|
|
}
|
|
|
|
for (int i = 0; i < positional.length; i++) {
|
|
final bool isRequired = i < memberFunction.requiredParameterCount;
|
|
final typeToCheck = positionalToTypeCheck[i];
|
|
setupParamLocal(
|
|
typeToCheck,
|
|
positional[i],
|
|
i,
|
|
paramInfo.positional[i],
|
|
isRequired,
|
|
);
|
|
}
|
|
for (int i = 0; i < named.length; i++) {
|
|
final param = named[i];
|
|
final typeToCheck = namedToTypeCheck[i];
|
|
setupParamLocal(
|
|
typeToCheck,
|
|
param,
|
|
paramInfo.nameIndex[param.name]!,
|
|
paramInfo.named[param.name],
|
|
param.isRequired,
|
|
);
|
|
}
|
|
|
|
// For all parameters whose Wasm type has been forced to `externref` due to
|
|
// this function being an export, internalize and cast the parameter to the
|
|
// canonical representation type for its Dart type.
|
|
locals.forEach((parameter, local) {
|
|
DartType parameterType = parameter.type;
|
|
if (local.type == w.RefType.extern(nullable: true) &&
|
|
!(parameterType is InterfaceType &&
|
|
parameterType.classNode == translator.wasmExternRefClass)) {
|
|
w.Local newLocal = addLocal(
|
|
translateType(parameterType),
|
|
name: parameter.name,
|
|
);
|
|
b.local_get(local);
|
|
translator.convertType(b, local.type, newLocal.type);
|
|
b.local_set(newLocal);
|
|
locals[parameter] = newLocal;
|
|
}
|
|
});
|
|
}
|
|
|
|
void setupParameters(
|
|
Reference reference, {
|
|
bool isForwarder = false,
|
|
bool canSafelyOmitImplicitChecks = false,
|
|
}) {
|
|
Member member = reference.asMember;
|
|
ParameterInfo paramInfo = translator.paramInfoForDirectCall(reference);
|
|
|
|
int parameterOffset = _initializeThis(reference);
|
|
int implicitParams = parameterOffset + paramInfo.typeParamCount;
|
|
|
|
_setupLocalParameters(
|
|
member,
|
|
paramInfo,
|
|
parameterOffset,
|
|
implicitParams,
|
|
isForwarder: isForwarder,
|
|
canSafelyOmitImplicitChecks: canSafelyOmitImplicitChecks,
|
|
);
|
|
}
|
|
|
|
void setupParametersForNormalEntry(Member member) {
|
|
setupParameters(
|
|
member.reference,
|
|
canSafelyOmitImplicitChecks: !translator.needToCheckTypesFor(member),
|
|
);
|
|
}
|
|
|
|
void setupParametersForCheckedEntry(Member member) {
|
|
assert(member.isInstanceMember);
|
|
assert(translator.needToCheckTypesFor(member));
|
|
setupParameters(
|
|
member.checkedEntryReference,
|
|
canSafelyOmitImplicitChecks: false,
|
|
);
|
|
}
|
|
|
|
void setupParametersForUncheckedEntry(Member member) {
|
|
assert(member.isInstanceMember);
|
|
assert(translator.needToCheckTypesFor(member));
|
|
setupParameters(
|
|
member.uncheckedEntryReference,
|
|
canSafelyOmitImplicitChecks: true,
|
|
);
|
|
}
|
|
|
|
void setupContexts(Member member) {
|
|
allocateContext(member.function!);
|
|
captureParameters(member.function!);
|
|
}
|
|
|
|
void setupLambdaParametersAndContexts(Lambda lambda) {
|
|
FunctionNode functionNode = lambda.functionNode;
|
|
_initializeContextLocals(functionNode);
|
|
|
|
int paramIndex = 1;
|
|
for (TypeParameter typeParam in functionNode.typeParameters) {
|
|
typeLocals[typeParam] = paramLocals[paramIndex++];
|
|
}
|
|
for (Variable param in functionNode.positionalParameters) {
|
|
locals[param] = paramLocals[paramIndex++];
|
|
}
|
|
for (Variable param in functionNode.namedParameters) {
|
|
locals[param] = paramLocals[paramIndex++];
|
|
}
|
|
|
|
allocateContext(functionNode);
|
|
captureParameters(functionNode);
|
|
}
|
|
|
|
/// Initialize locals containing `this` in constructors and instance members.
|
|
/// Returns the number of parameter locals taken up by the receiver parameter,
|
|
/// i.e. the parameter offset for the first type parameter (or the first
|
|
/// parameter if there are no type parameters).
|
|
int _initializeThis(Reference reference) {
|
|
Member member = reference.asMember;
|
|
final hasThis =
|
|
member.isInstanceMember || reference.isConstructorBodyReference;
|
|
if (hasThis) {
|
|
thisLocal = paramLocals[0];
|
|
b.localNames[thisLocal!.index] = "this";
|
|
final preciseThisType = translator.preciseThisFor(member);
|
|
if (translator.needsConversion(thisLocal!.type, preciseThisType)) {
|
|
preciseThisLocal = addLocal(preciseThisType, name: "preciseThis");
|
|
b.local_get(thisLocal!);
|
|
translator.convertType(b, thisLocal!.type, preciseThisType);
|
|
b.local_set(preciseThisLocal!);
|
|
} else {
|
|
preciseThisLocal = thisLocal!;
|
|
}
|
|
return 1;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
/// Initialize locals pointing to every context in the context chain of a
|
|
/// closure, plus the locals containing `this` if `this` is captured by the
|
|
/// closure.
|
|
void _initializeContextLocals(TreeNode node, {int contextParamIndex = 0}) {
|
|
Context? context;
|
|
|
|
if (node is Constructor) {
|
|
// The context parameter is for the constructor context.
|
|
context = closures.contexts[node];
|
|
} else {
|
|
assert(node is FunctionNode);
|
|
// The context parameter is for the parent context.
|
|
context = closures.contexts[node]?.parent;
|
|
}
|
|
|
|
if (context != null) {
|
|
assert(!context.isEmpty);
|
|
w.RefType contextType = w.RefType.def(context.struct, nullable: false);
|
|
|
|
b.local_get(paramLocals[contextParamIndex]);
|
|
b.ref_cast(contextType);
|
|
|
|
while (true) {
|
|
w.Local contextLocal = addLocal(contextType);
|
|
context!.currentLocal = contextLocal;
|
|
|
|
if (context.parent != null || context.containsThis) {
|
|
b.local_tee(contextLocal);
|
|
} else {
|
|
b.local_set(contextLocal);
|
|
}
|
|
|
|
if (context.containsThis) {
|
|
thisLocal = addLocal(
|
|
context.struct.fields[context.thisFieldIndex].type.unpacked
|
|
.withNullability(false),
|
|
name: "this",
|
|
);
|
|
preciseThisLocal = thisLocal;
|
|
|
|
b.struct_get(context.struct, context.thisFieldIndex);
|
|
b.ref_as_non_null();
|
|
b.local_set(thisLocal!);
|
|
|
|
if (context.parent != null) {
|
|
b.local_get(contextLocal);
|
|
}
|
|
}
|
|
|
|
if (context.parent == null) break;
|
|
|
|
b.struct_get(context.struct, context.parentFieldIndex);
|
|
b.ref_as_non_null();
|
|
context = context.parent!;
|
|
contextType = w.RefType.def(context.struct, nullable: false);
|
|
}
|
|
}
|
|
}
|
|
|
|
void _implicitReturn() {
|
|
if (outputs.isNotEmpty) {
|
|
w.ValueType returnType = outputs.single;
|
|
if (returnType is w.RefType && returnType.nullable) {
|
|
// Dart body may have an implicit return null.
|
|
b.ref_null(returnType.heapType.bottomType);
|
|
} else {
|
|
b.comment("Unreachable implicit return");
|
|
b.unreachable();
|
|
}
|
|
}
|
|
}
|
|
|
|
void allocateContext(TreeNode node) {
|
|
Context? context = closures.contexts[node];
|
|
if (context == null || context.isEmpty) return;
|
|
|
|
w.Local contextLocal = addLocal(
|
|
w.RefType.def(context.struct, nullable: true),
|
|
);
|
|
context.currentLocal = contextLocal;
|
|
b.struct_new_default(context.struct);
|
|
b.local_set(contextLocal);
|
|
if (context.containsThis) {
|
|
b.local_get(contextLocal);
|
|
b.local_get(preciseThisLocal!);
|
|
b.struct_set(context.struct, context.thisFieldIndex);
|
|
}
|
|
if (context.parent != null) {
|
|
w.Local parentLocal = context.parent!.currentLocal;
|
|
b.local_get(contextLocal);
|
|
b.local_get(parentLocal);
|
|
b.struct_set(context.struct, context.parentFieldIndex);
|
|
}
|
|
}
|
|
|
|
void captureParameters(TreeNode node) {
|
|
Context? context = closures.contexts[node];
|
|
if (context == null || context.isEmpty) return;
|
|
|
|
locals.forEach((variable, local) {
|
|
Capture? capture = closures.captures[variable];
|
|
if (capture != null && capture.context == context) {
|
|
b.local_get(capture.context.currentLocal);
|
|
b.local_get(local);
|
|
translator.convertType(b, local.type, capture.type);
|
|
b.struct_set(capture.context.struct, capture.fieldIndex);
|
|
}
|
|
});
|
|
typeLocals.forEach((parameter, local) {
|
|
Capture? capture = closures.captures[parameter];
|
|
if (capture != null && capture.context == context) {
|
|
b.local_get(capture.context.currentLocal);
|
|
b.local_get(local);
|
|
translator.convertType(b, local.type, capture.type);
|
|
b.struct_set(capture.context.struct, capture.fieldIndex);
|
|
}
|
|
});
|
|
}
|
|
|
|
/// Helper function to throw a Wasm ref downcast error.
|
|
void throwWasmRefError(String expected) {
|
|
_emitString(expected);
|
|
call(translator.stackTraceCurrent.reference);
|
|
call(translator.throwWasmRefError.reference);
|
|
b.unreachable();
|
|
}
|
|
|
|
/// Generates code for an expression plus conversion code to convert the
|
|
/// result to the expected type if needed. All expression code generation goes
|
|
/// through this method.
|
|
w.ValueType translateExpression(Expression node, w.ValueType expectedType) {
|
|
var sourceUpdated = false;
|
|
Source? oldSource;
|
|
if (node is FileUriNode) {
|
|
final source =
|
|
node.enclosingComponent!.uriToSource[(node as FileUriNode).fileUri]!;
|
|
oldSource = setSourceMapSource(source);
|
|
sourceUpdated = true;
|
|
}
|
|
final oldFileOffset = setSourceMapFileOffset(node.fileOffset);
|
|
try {
|
|
w.ValueType resultType = node.accept1(this, expectedType);
|
|
translator.convertType(b, resultType, expectedType);
|
|
return expectedType;
|
|
} catch (_) {
|
|
_printLocation(node);
|
|
rethrow;
|
|
} finally {
|
|
if (sourceUpdated) {
|
|
setSourceMapSource(oldSource);
|
|
}
|
|
setSourceMapFileOffset(oldFileOffset);
|
|
}
|
|
}
|
|
|
|
void translateVariableDeclaration(VariableDeclaration node) {
|
|
final oldFileOffset = setSourceMapFileOffset(node.fileOffset);
|
|
try {
|
|
visitVariable(node.variable);
|
|
} catch (_) {
|
|
_printLocation(node);
|
|
rethrow;
|
|
} finally {
|
|
setSourceMapFileOffset(oldFileOffset);
|
|
}
|
|
}
|
|
|
|
void translateVariable(Variable node) {
|
|
final oldFileOffset = setSourceMapFileOffset(node.fileOffset);
|
|
try {
|
|
visitVariable(node);
|
|
} catch (_) {
|
|
_printLocation(node);
|
|
rethrow;
|
|
} finally {
|
|
setSourceMapFileOffset(oldFileOffset);
|
|
}
|
|
}
|
|
|
|
void translateStatement(Statement node) {
|
|
final oldFileOffset = setSourceMapFileOffset(node.fileOffset);
|
|
try {
|
|
node.accept(this);
|
|
} catch (_) {
|
|
_printLocation(node);
|
|
rethrow;
|
|
} finally {
|
|
setSourceMapFileOffset(oldFileOffset);
|
|
}
|
|
}
|
|
|
|
void _printLocation(TreeNode node) {
|
|
if (!exceptionLocationPrinted) {
|
|
print("Exception in ${node.runtimeType} at ${node.location}");
|
|
exceptionLocationPrinted = true;
|
|
}
|
|
}
|
|
|
|
List<w.ValueType> call(Reference target) {
|
|
return translator.callReference(target, b);
|
|
}
|
|
|
|
@override
|
|
void visitInvalidInitializer(InvalidInitializer node) {
|
|
throw StateError('Should be handled in InitializerListCodeGenerator.');
|
|
}
|
|
|
|
@override
|
|
void visitAssertInitializer(AssertInitializer node) {
|
|
throw StateError('Should be handled in InitializerListCodeGenerator.');
|
|
}
|
|
|
|
@override
|
|
void visitLocalInitializer(LocalInitializer node) {
|
|
throw StateError('Should be handled in InitializerListCodeGenerator.');
|
|
}
|
|
|
|
@override
|
|
void visitFieldInitializer(FieldInitializer node) {
|
|
throw StateError('Should be handled in InitializerListCodeGenerator.');
|
|
}
|
|
|
|
@override
|
|
void visitRedirectingInitializer(RedirectingInitializer node) {
|
|
throw StateError('Should be handled in InitializerListCodeGenerator.');
|
|
}
|
|
|
|
@override
|
|
void visitSuperInitializer(SuperInitializer node) {
|
|
throw StateError('Should be handled in InitializerListCodeGenerator.');
|
|
}
|
|
|
|
@override
|
|
void visitBlock(Block node) {
|
|
for (Statement statement in node.statements) {
|
|
translateStatement(statement);
|
|
}
|
|
}
|
|
|
|
@override
|
|
void visitLabeledStatement(LabeledStatement node) {
|
|
w.Label label = b.block();
|
|
breakFinalizers[node] = <w.Label>[label];
|
|
translateStatement(node.body);
|
|
breakFinalizers.remove(node);
|
|
b.end();
|
|
}
|
|
|
|
@override
|
|
void visitBreakStatement(BreakStatement node) {
|
|
b.br(breakFinalizers[node.target]!.last);
|
|
}
|
|
|
|
@override
|
|
void visitVariable(Variable node) {
|
|
final w.ValueType type = translator.translateTypeOfLocalVariable(node);
|
|
w.Local? local;
|
|
final Capture? capture = closures.captures[node];
|
|
if (capture == null || !capture.written) {
|
|
// Variable is not captured, or never updated after initialization. Keep
|
|
// the value in a local.
|
|
local = addLocal(type, name: node.name);
|
|
locals[node] = local;
|
|
}
|
|
|
|
// Handle variable initialization. Nullable variables don't get an
|
|
// initializer in kernel, but they still need to be initialized as `null`,
|
|
// to reset the variables in loops to the initial value, intead of reusing
|
|
// the last value from the previous iteration. This is tested in
|
|
// `tests/language/local_null_initialization.dart`.
|
|
if (node.initializer != null ||
|
|
node.type.nullability == Nullability.nullable) {
|
|
Expression initializer =
|
|
node.initializer ?? ConstantExpression(NullConstant());
|
|
if (capture != null) {
|
|
// Type for the variable in context will always be nullable, to be able
|
|
// to allocate the context without creating dummy values. Nullability of
|
|
// the local's type will depend on the Dart type.
|
|
assert(
|
|
local == null || local.type.withNullability(true) == capture.type,
|
|
);
|
|
w.ValueType expectedType = local != null ? local.type : capture.type;
|
|
b.local_get(capture.context.currentLocal);
|
|
translateExpression(initializer, expectedType);
|
|
if (local != null) {
|
|
b.local_tee(local);
|
|
}
|
|
b.struct_set(capture.context.struct, capture.fieldIndex);
|
|
} else {
|
|
translateExpression(initializer, local!.type);
|
|
b.local_set(local);
|
|
}
|
|
} else if (local != null && !local.type.defaultable) {
|
|
// Uninitialized variable. We don't need to update the context when the
|
|
// variable is captured as the context is already initialized.
|
|
translator
|
|
.getDummyValuesCollectorForModule(b.moduleBuilder)
|
|
.instantiateLocalDummyValue(b, local.type);
|
|
b.local_set(local);
|
|
}
|
|
}
|
|
|
|
/// Initialize a variable [node] to an initial value which must be left on
|
|
/// the stack by [pushInitialValue].
|
|
///
|
|
/// This is similar to [visitVariable] but it gives more control
|
|
/// over how the variable is initialized.
|
|
void initializeVariable(Variable node, void Function() pushInitialValue) {
|
|
final w.ValueType type = translator.translateTypeOfLocalVariable(node);
|
|
w.Local? local;
|
|
final Capture? capture = closures.captures[node];
|
|
if (capture == null || !capture.written) {
|
|
// Variable is not captured, or never updated after initialization. Keep
|
|
// the value in a local.
|
|
local = addLocal(type, name: node.name);
|
|
locals[node] = local;
|
|
}
|
|
|
|
if (capture != null) {
|
|
b.local_get(capture.context.currentLocal);
|
|
pushInitialValue();
|
|
if (local != null) {
|
|
b.local_tee(local);
|
|
}
|
|
b.struct_set(capture.context.struct, capture.fieldIndex);
|
|
} else {
|
|
pushInitialValue();
|
|
b.local_set(local!);
|
|
}
|
|
}
|
|
|
|
@override
|
|
void visitEmptyStatement(EmptyStatement node) {}
|
|
|
|
@override
|
|
void visitAssertStatement(AssertStatement node) {
|
|
if (options.enableAsserts) {
|
|
w.Label assertBlock = b.block();
|
|
translateExpression(node.condition, w.NumType.i32);
|
|
b.br_if(assertBlock);
|
|
|
|
Expression? message = node.message;
|
|
if (message != null) {
|
|
translateExpression(message, translator.topType);
|
|
} else {
|
|
b.ref_null(w.HeapType.none);
|
|
}
|
|
final Location? location = node.location;
|
|
final w.RefType stringRefType = translator.stringTypeNullable;
|
|
if (location != null) {
|
|
instantiateConstant(
|
|
StringConstant(location.file.toString()),
|
|
stringRefType,
|
|
);
|
|
b.i64_const(location.line);
|
|
b.i64_const(location.column);
|
|
final String sourceString =
|
|
node.enclosingComponent!.uriToSource[location.file]!.text;
|
|
final String conditionString = sourceString.substring(
|
|
node.conditionStartOffset,
|
|
node.conditionEndOffset,
|
|
);
|
|
instantiateConstant(StringConstant(conditionString), stringRefType);
|
|
} else {
|
|
b.ref_null(stringRefType.heapType);
|
|
b.i64_const(0);
|
|
b.i64_const(0);
|
|
b.ref_null(stringRefType.heapType);
|
|
}
|
|
|
|
call(translator.throwAssertionError.reference);
|
|
|
|
b.unreachable();
|
|
b.end();
|
|
}
|
|
}
|
|
|
|
@override
|
|
void visitAssertBlock(AssertBlock node) {
|
|
if (!options.enableAsserts) return;
|
|
|
|
for (Statement statement in node.statements) {
|
|
translateStatement(statement);
|
|
}
|
|
}
|
|
|
|
@override
|
|
void visitTryCatch(TryCatch node) {
|
|
// It is not valid Dart to have a try without a catch.
|
|
assert(node.catches.isNotEmpty);
|
|
|
|
final w.RefType exceptionType = translator.topTypeNonNullable;
|
|
final w.RefType stackTraceType = translator.stackTraceType;
|
|
|
|
final w.Label wrapperBlock = b.block();
|
|
|
|
// Create a block target for each Dart `catch` block, to be able to share
|
|
// code when generating a `catch` and `catch_all` for the same Dart `catch`
|
|
// block, when the block can catch both Dart and JS exceptions.
|
|
// The `end` for the Wasm `try` block works as the first exception handler
|
|
// target.
|
|
List<w.Label> catchBlockLabels = List.generate(
|
|
node.catches.length - 1,
|
|
(i) => b.block([], [exceptionType, stackTraceType]),
|
|
growable: true,
|
|
);
|
|
|
|
w.Label try_ = b.try_legacy([], [exceptionType, stackTraceType]);
|
|
catchBlockLabels.add(try_);
|
|
|
|
catchBlockLabels = catchBlockLabels.reversed.toList();
|
|
|
|
translateStatement(node.body);
|
|
b.br(wrapperBlock);
|
|
|
|
// Stash the original exception in a local so we can push it back onto the
|
|
// stack after each type test. Also, store the stack trace in a local.
|
|
w.Local thrownException = addLocal(exceptionType);
|
|
w.Local thrownStackTrace = addLocal(stackTraceType);
|
|
|
|
tryBlockLocals.add((
|
|
exceptionLocal: thrownException,
|
|
stackTraceLocal: thrownStackTrace,
|
|
));
|
|
|
|
void emitCatchBlock(
|
|
w.Label catchBlockTarget,
|
|
Catch catch_,
|
|
bool emitGuard,
|
|
) {
|
|
// For each catch node:
|
|
// 1) Create a block for the catch.
|
|
// 2) Push the caught exception onto the stack.
|
|
// 3) Add a type test based on the guard of the catch.
|
|
// 4) If the test fails, we jump to the next catch. Otherwise, we
|
|
// jump to the block for the body of the catch.
|
|
w.Label catchBlock = b.block();
|
|
DartType guard = catch_.guard;
|
|
|
|
// Only emit the type test if the guard is not [Object].
|
|
if (emitGuard) {
|
|
b.local_get(thrownException);
|
|
types.emitIsTest(
|
|
this,
|
|
guard,
|
|
translator.coreTypes.objectNonNullableRawType,
|
|
catch_.location,
|
|
);
|
|
b.i32_eqz();
|
|
b.br_if(catchBlock);
|
|
}
|
|
|
|
b.local_get(thrownException);
|
|
b.local_get(thrownStackTrace);
|
|
b.br(catchBlockTarget);
|
|
|
|
b.end(); // end catchBlock.
|
|
}
|
|
|
|
// Insert a catch instruction which will catch any thrown Dart
|
|
// exceptions.
|
|
b.catch_legacy(translator.getDartExceptionTag(b.moduleBuilder));
|
|
|
|
b.local_set(thrownStackTrace);
|
|
b.local_set(thrownException);
|
|
for (
|
|
int catchBlockIndex = 0;
|
|
catchBlockIndex < node.catches.length;
|
|
catchBlockIndex += 1
|
|
) {
|
|
final catch_ = node.catches[catchBlockIndex];
|
|
// Only insert type checks if the guard is not `Object`
|
|
final bool shouldEmitGuard =
|
|
catch_.guard != translator.coreTypes.objectNonNullableRawType;
|
|
emitCatchBlock(
|
|
catchBlockLabels[catchBlockIndex],
|
|
catch_,
|
|
shouldEmitGuard,
|
|
);
|
|
if (!shouldEmitGuard) {
|
|
// If we didn't emit a guard, we won't ever fall through to the
|
|
// following catch blocks.
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Rethrow if all the catch blocks fall through
|
|
b.rethrow_(try_);
|
|
|
|
if (node.catches.any(
|
|
(c) => guardCanMatchJSException(translator, c.guard),
|
|
)) {
|
|
b.catch_legacy(translator.getJsExceptionTag(b.moduleBuilder));
|
|
|
|
final jsExceptionLocal = addLocal(w.RefType.extern(nullable: true));
|
|
b.local_tee(jsExceptionLocal);
|
|
|
|
call(translator.boxJsException.reference);
|
|
b.local_tee(thrownException); // ref null #Top
|
|
|
|
b.local_get(jsExceptionLocal);
|
|
call(translator.jsExceptionStackTrace.reference);
|
|
b.local_set(thrownStackTrace);
|
|
|
|
for (
|
|
int catchBlockIndex = 0;
|
|
catchBlockIndex < node.catches.length;
|
|
catchBlockIndex += 1
|
|
) {
|
|
final catch_ = node.catches[catchBlockIndex];
|
|
if (!guardCanMatchJSException(translator, catch_.guard)) {
|
|
continue;
|
|
}
|
|
// Type guards based on a type parameter are special, in that we cannot
|
|
// statically determine whether a JavaScript error will always satisfy
|
|
// the guard, so we should emit the type checking code for it. All
|
|
// other guards will always match a JavaScript error, however, so no
|
|
// need to emit type checks for those.
|
|
final bool shouldEmitGuard = catch_.guard is TypeParameterType;
|
|
emitCatchBlock(
|
|
catchBlockLabels[catchBlockIndex],
|
|
catch_,
|
|
shouldEmitGuard,
|
|
);
|
|
if (!shouldEmitGuard) {
|
|
// If we didn't emit a guard, we won't ever fall through to the
|
|
// following catch blocks.
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Rethrow if the catch block falls through
|
|
b.rethrow_(try_);
|
|
}
|
|
|
|
for (Catch catch_ in node.catches) {
|
|
b.end();
|
|
b.local_set(thrownStackTrace);
|
|
b.local_set(thrownException);
|
|
|
|
final Variable? exceptionDeclaration = catch_.exception;
|
|
if (exceptionDeclaration != null) {
|
|
initializeVariable(exceptionDeclaration, () {
|
|
b.local_get(thrownException);
|
|
// Type test passed, downcast the exception to the expected type.
|
|
translator.convertType(
|
|
b,
|
|
thrownException.type,
|
|
translator.translateType(exceptionDeclaration.type),
|
|
);
|
|
});
|
|
}
|
|
|
|
final Variable? stackTraceDeclaration = catch_.stackTrace;
|
|
if (stackTraceDeclaration != null) {
|
|
initializeVariable(
|
|
stackTraceDeclaration,
|
|
() => b.local_get(thrownStackTrace),
|
|
);
|
|
}
|
|
|
|
translateStatement(catch_.body);
|
|
b.br(wrapperBlock);
|
|
}
|
|
|
|
tryBlockLocals.removeLast();
|
|
b.end(); // end tryWrapper
|
|
}
|
|
|
|
@override
|
|
void visitTryFinally(TryFinally node) {
|
|
// We lower a [TryFinally] to a number of nested blocks, depending on how
|
|
// many different code paths we have that run the finally block.
|
|
//
|
|
// We emit the finalizer once in a catch, to handle the case where the try
|
|
// throws. Once outside of the catch, to handle the case where the try does
|
|
// not throw. If there is a return within the try block, then we emit the
|
|
// finalizer one more time along with logic to continue walking up the
|
|
// stack.
|
|
//
|
|
// A `break L` can run more than one finalizer, and each of those
|
|
// finalizers will need to be run in a different `try` block. So for each
|
|
// wrapping label we generate a block to run the finalizer on `break` and
|
|
// then branch to the right Wasm block to either run the next finalizer or
|
|
// break.
|
|
|
|
// The block for the try-finally statement. Used as `br` target in normal
|
|
// execution after the finalizer (no throws, returns, or breaks).
|
|
w.Label tryFinallyBlock = b.block();
|
|
|
|
// Create one block for each wrapping label.
|
|
for (final labelBlocks in breakFinalizers.values.toList().reversed) {
|
|
labelBlocks.add(b.block());
|
|
}
|
|
|
|
// Continuation of this block runs the finalizer and returns (or jumps to
|
|
// the next finalizer block). Used as `br` target on `return`.
|
|
w.Label returnFinalizerBlock = b.block();
|
|
returnFinalizers.add(TryBlockFinalizer(returnFinalizerBlock));
|
|
|
|
w.Label tryBlock = b.try_legacy();
|
|
translateStatement(node.body);
|
|
|
|
final bool mustHandleReturn = returnFinalizers
|
|
.removeLast()
|
|
.mustHandleReturn;
|
|
|
|
// `break` statements in the current finalizer and the rest will not run
|
|
// the current finalizer, update the `break` targets.
|
|
final removedBreakTargets = <LabeledStatement, w.Label>{};
|
|
for (final breakFinalizerEntry in breakFinalizers.entries) {
|
|
removedBreakTargets[breakFinalizerEntry.key] = breakFinalizerEntry.value
|
|
.removeLast();
|
|
}
|
|
|
|
// Handle Dart exceptions.
|
|
b.catch_legacy(translator.getDartExceptionTag(b.moduleBuilder));
|
|
translateStatement(node.finalizer);
|
|
b.rethrow_(tryBlock);
|
|
|
|
// Handle JS exceptions.
|
|
if (!translator.options.standalone) {
|
|
b.catch_legacy(translator.getJsExceptionTag(b.moduleBuilder));
|
|
translateStatement(node.finalizer);
|
|
b.rethrow_(tryBlock);
|
|
}
|
|
|
|
b.end(); // tryBlock
|
|
|
|
// Run finalizer on normal execution (no breaks, throws, or returns).
|
|
translateStatement(node.finalizer);
|
|
b.br(tryFinallyBlock);
|
|
b.end(); // returnFinalizerBlock
|
|
|
|
// Run the finalizer on `return`.
|
|
if (mustHandleReturn) {
|
|
translateStatement(node.finalizer);
|
|
if (returnFinalizers.isNotEmpty) {
|
|
b.br(returnFinalizers.last.label);
|
|
} else {
|
|
if (returnValueLocal != null) {
|
|
b.local_get(returnValueLocal!);
|
|
translator.convertType(b, returnValueLocal!.type, returnType);
|
|
}
|
|
_returnFromFunction();
|
|
}
|
|
}
|
|
|
|
// Generate finalizers for `break`s in the `try` block.
|
|
for (final removedBreakTargetEntry in removedBreakTargets.entries) {
|
|
b.end();
|
|
translateStatement(node.finalizer);
|
|
b.br(breakFinalizers[removedBreakTargetEntry.key]!.last);
|
|
}
|
|
|
|
b.end(); // tryFinallyBlock
|
|
}
|
|
|
|
@override
|
|
void visitExpressionStatement(ExpressionStatement node) {
|
|
translateExpression(node.expression, voidMarker);
|
|
}
|
|
|
|
bool _hasLogicalOperator(Expression condition) {
|
|
while (condition is Not) {
|
|
condition = condition.operand;
|
|
}
|
|
return condition is LogicalExpression;
|
|
}
|
|
|
|
void branchIf(
|
|
Expression? condition,
|
|
w.Label target, {
|
|
required bool negated,
|
|
}) {
|
|
if (condition == null) {
|
|
if (!negated) b.br(target);
|
|
return;
|
|
}
|
|
while (condition is Not) {
|
|
negated = !negated;
|
|
condition = condition.operand;
|
|
}
|
|
if (condition is LogicalExpression) {
|
|
bool isConjunctive =
|
|
(condition.operatorEnum == LogicalExpressionOperator.AND) ^ negated;
|
|
if (isConjunctive) {
|
|
w.Label conditionBlock = b.block();
|
|
branchIf(condition.left, conditionBlock, negated: !negated);
|
|
branchIf(condition.right, target, negated: negated);
|
|
b.end();
|
|
} else {
|
|
branchIf(condition.left, target, negated: negated);
|
|
branchIf(condition.right, target, negated: negated);
|
|
}
|
|
} else {
|
|
translateExpression(condition!, w.NumType.i32);
|
|
if (negated) {
|
|
b.i32_eqz();
|
|
}
|
|
b.br_if(target);
|
|
}
|
|
}
|
|
|
|
void _conditional(
|
|
Expression condition,
|
|
void Function() then,
|
|
void Function()? otherwise,
|
|
List<w.ValueType> result,
|
|
) {
|
|
if (!_hasLogicalOperator(condition)) {
|
|
// Simple condition
|
|
translateExpression(condition, w.NumType.i32);
|
|
b.if_(const [], result);
|
|
then();
|
|
if (otherwise != null) {
|
|
b.else_();
|
|
otherwise();
|
|
}
|
|
b.end();
|
|
} else {
|
|
// Complex condition
|
|
w.Label ifBlock = b.block(const [], result);
|
|
if (otherwise != null) {
|
|
w.Label elseBlock = b.block();
|
|
branchIf(condition, elseBlock, negated: true);
|
|
then();
|
|
b.br(ifBlock);
|
|
b.end();
|
|
otherwise();
|
|
} else {
|
|
branchIf(condition, ifBlock, negated: true);
|
|
then();
|
|
}
|
|
b.end();
|
|
}
|
|
}
|
|
|
|
@override
|
|
void visitIfStatement(IfStatement node) {
|
|
_conditional(
|
|
node.condition,
|
|
() => translateStatement(node.then),
|
|
node.otherwise != null ? () => translateStatement(node.otherwise!) : null,
|
|
const [],
|
|
);
|
|
}
|
|
|
|
@override
|
|
void visitDoStatement(DoStatement node) {
|
|
w.Label loop = b.loop();
|
|
allocateContext(node);
|
|
translateStatement(node.body);
|
|
branchIf(node.condition, loop, negated: false);
|
|
b.end();
|
|
}
|
|
|
|
@override
|
|
void visitWhileStatement(WhileStatement node) {
|
|
w.Label block = b.block();
|
|
w.Label loop = b.loop();
|
|
allocateContext(node);
|
|
branchIf(node.condition, block, negated: true);
|
|
translateStatement(node.body);
|
|
b.br(loop);
|
|
b.end();
|
|
b.end();
|
|
}
|
|
|
|
@override
|
|
void visitForStatement(ForStatement node) {
|
|
allocateContext(node);
|
|
for (VariableDeclaration variable in node.variables) {
|
|
translateVariableDeclaration(variable);
|
|
}
|
|
w.Label block = b.block();
|
|
w.Label loop = b.loop();
|
|
branchIf(node.condition, block, negated: true);
|
|
translateStatement(node.body);
|
|
|
|
emitForStatementUpdate(node);
|
|
|
|
b.br(loop);
|
|
b.end();
|
|
b.end();
|
|
}
|
|
|
|
void emitForStatementUpdate(ForStatement node) {
|
|
Context? context = closures.contexts[node];
|
|
if (context != null && !context.isEmpty) {
|
|
// Create a new context for each iteration of the loop.
|
|
w.Local oldContext = context.currentLocal;
|
|
allocateContext(node);
|
|
w.Local newContext = context.currentLocal;
|
|
|
|
// Copy the values of captured loop variables to the new context.
|
|
for (VariableDeclaration variableDeclaration in node.variables) {
|
|
Capture? capture = closures.captures[variableDeclaration.variable];
|
|
if (capture != null) {
|
|
assert(capture.context == context);
|
|
b.local_get(newContext);
|
|
b.local_get(oldContext);
|
|
b.struct_get(context.struct, capture.fieldIndex);
|
|
b.struct_set(context.struct, capture.fieldIndex);
|
|
}
|
|
}
|
|
|
|
// Update the context local to point to the new context.
|
|
b.local_get(newContext);
|
|
b.local_set(oldContext);
|
|
}
|
|
|
|
for (Expression update in node.updates) {
|
|
translateExpression(update, voidMarker);
|
|
}
|
|
}
|
|
|
|
@override
|
|
void visitForInStatement(ForInStatement node) {
|
|
throw "ForInStatement should have been desugared: $node";
|
|
}
|
|
|
|
/// Handle the return from this function, either by jumping to [returnLabel]
|
|
/// in the case this function was inlined or just inserting a return
|
|
/// instruction.
|
|
void _returnFromFunction() {
|
|
if (returnLabel != null) {
|
|
b.br(returnLabel!);
|
|
} else {
|
|
b.return_();
|
|
}
|
|
}
|
|
|
|
@override
|
|
void visitReturnStatement(ReturnStatement node) {
|
|
Expression? expression = node.expression;
|
|
if (expression != null) {
|
|
translateExpression(expression, returnType);
|
|
} else {
|
|
_implicitReturn();
|
|
}
|
|
|
|
// If we are wrapped in a [TryFinally] node then we have to run finalizers
|
|
// as the stack unwinds. When we get to the top of the finalizer stack, we
|
|
// will handle the return using [returnValueLocal] if this function returns
|
|
// a value.
|
|
if (returnFinalizers.isNotEmpty) {
|
|
for (TryBlockFinalizer finalizer in returnFinalizers) {
|
|
finalizer.mustHandleReturn = true;
|
|
}
|
|
if (returnType != voidMarker) {
|
|
// Since the flow of the return value through the returnValueLocal
|
|
// crosses control-flow constructs, the local needs to always have a
|
|
// defaultable type in order for the Wasm code to validate.
|
|
returnValueLocal ??= addLocal(
|
|
returnType.withNullability(true),
|
|
name: "returnValue",
|
|
);
|
|
b.local_set(returnValueLocal!);
|
|
}
|
|
b.br(returnFinalizers.last.label);
|
|
} else {
|
|
_returnFromFunction();
|
|
}
|
|
}
|
|
|
|
@override
|
|
void visitSwitchStatement(SwitchStatement node) {
|
|
// If we have an empty switch, just evaluate the expression for any
|
|
// potential side effects. In this case, the return type does not matter.
|
|
if (node.cases.isEmpty) {
|
|
translateExpression(node.expression, voidMarker);
|
|
return;
|
|
}
|
|
|
|
final switchInfo = SwitchInfo(this, node);
|
|
|
|
bool isNullable = dartTypeOf(node.expression).isPotentiallyNullable;
|
|
|
|
// When the type is nullable we use two variables: one for the nullable
|
|
// value, one after the null check, with non-nullable type.
|
|
w.Local switchValueNonNullableLocal = addLocal(switchInfo.nonNullableType);
|
|
w.Local? switchValueNullableLocal = isNullable
|
|
? addLocal(switchInfo.nullableType)
|
|
: null;
|
|
|
|
// Initialize switch value local
|
|
translateExpression(
|
|
node.expression,
|
|
isNullable ? switchInfo.nullableType : switchInfo.nonNullableType,
|
|
);
|
|
b.local_set(
|
|
isNullable ? switchValueNullableLocal! : switchValueNonNullableLocal,
|
|
);
|
|
|
|
// Special cases
|
|
SwitchCase? defaultCase = switchInfo.defaultCase;
|
|
SwitchCase? nullCase = switchInfo.nullCase;
|
|
|
|
// Create `loop` for backward jumps
|
|
w.Label loopLabel = b.loop();
|
|
|
|
// Set `switchValueLocal` for backward jumps
|
|
w.Local switchValueLocal = isNullable
|
|
? switchValueNullableLocal!
|
|
: switchValueNonNullableLocal;
|
|
|
|
// Add backward jump info
|
|
switchBackwardJumpInfos[node] = SwitchBackwardJumpInfo(
|
|
switchValueLocal,
|
|
loopLabel,
|
|
);
|
|
|
|
// Set up blocks, in reverse order of cases so they end in forward order
|
|
w.Label doneLabel = b.block();
|
|
for (SwitchCase c in node.cases.reversed) {
|
|
switchLabels[c] = b.block();
|
|
}
|
|
|
|
// Compute value and handle null
|
|
if (isNullable) {
|
|
w.Label nullLabel = nullCase != null
|
|
? switchLabels[nullCase]!
|
|
: defaultCase != null
|
|
? switchLabels[defaultCase]!
|
|
: doneLabel;
|
|
b.local_get(switchValueNullableLocal!);
|
|
b.br_on_null(nullLabel);
|
|
translator.convertType(
|
|
b,
|
|
switchInfo.nullableType.withNullability(false),
|
|
switchInfo.nonNullableType,
|
|
);
|
|
b.local_set(switchValueNonNullableLocal);
|
|
}
|
|
|
|
final dynamicTypeGuard = switchInfo.dynamicTypeGuard;
|
|
if (dynamicTypeGuard != null) {
|
|
final success = b.block(const [], [translator.topTypeNonNullable]);
|
|
dynamicTypeGuard(switchValueNonNullableLocal, success);
|
|
b.br(switchLabels[defaultCase] ?? doneLabel);
|
|
b.end();
|
|
}
|
|
|
|
final brTable = switchInfo.brTable;
|
|
if (brTable != null) {
|
|
// Map each entry in the range to the appropriate jump table entry.
|
|
final indexBlocks = <w.Label>[];
|
|
final defaultLabel = defaultCase != null
|
|
? switchLabels[defaultCase]!
|
|
: doneLabel;
|
|
for (int i = brTable.minValue; i <= brTable.maxValue; ++i) {
|
|
final c = brTable.caseMap[i];
|
|
indexBlocks.add(c == null ? defaultLabel : switchLabels[c]!);
|
|
}
|
|
|
|
brTable.emitBrTableExpr(b, switchValueNonNullableLocal);
|
|
|
|
b.br_table(indexBlocks, defaultLabel);
|
|
} else {
|
|
// Compare against all case values
|
|
for (SwitchCase c in node.cases) {
|
|
for (Expression exp in c.expressions) {
|
|
if (exp is NullLiteral ||
|
|
exp is ConstantExpression && exp.constant is NullConstant) {
|
|
// Null already checked, skip
|
|
} else {
|
|
switchInfo.compare(
|
|
switchValueNonNullableLocal,
|
|
() => translateExpression(exp, switchInfo.nonNullableType),
|
|
);
|
|
b.br_if(switchLabels[c]!);
|
|
}
|
|
}
|
|
}
|
|
|
|
// No explicit cases matched
|
|
if (node.isExplicitlyExhaustive) {
|
|
b.unreachable();
|
|
} else {
|
|
w.Label defaultLabel = defaultCase != null
|
|
? switchLabels[defaultCase]!
|
|
: doneLabel;
|
|
b.br(defaultLabel);
|
|
}
|
|
}
|
|
|
|
// Emit case bodies
|
|
for (SwitchCase c in node.cases) {
|
|
b.end();
|
|
// Remove backward jump target from forward jump labels
|
|
switchLabels.remove(c);
|
|
|
|
// Create a `loop` in default case to allow backward jumps to it
|
|
if (c.isDefault) {
|
|
switchBackwardJumpInfos[node]!.defaultLoopLabel = b.loop();
|
|
}
|
|
|
|
translateStatement(c.body);
|
|
|
|
if (c.isDefault) {
|
|
b.end(); // defaultLoopLabel
|
|
}
|
|
|
|
b.br(doneLabel);
|
|
}
|
|
b.end(); // doneLabel
|
|
b.end(); // loopLabel
|
|
|
|
// Remove backward jump info
|
|
final removed = switchBackwardJumpInfos.remove(node);
|
|
assert(removed != null);
|
|
}
|
|
|
|
@override
|
|
void visitContinueSwitchStatement(ContinueSwitchStatement node) {
|
|
w.Label? label = switchLabels[node.target];
|
|
if (label != null) {
|
|
b.br(label);
|
|
} else {
|
|
// Backward jump. Find the case literal in jump target, set the switched
|
|
// values to the jump target's value, and loop.
|
|
final SwitchCase targetSwitchCase = node.target;
|
|
final SwitchStatement targetSwitch =
|
|
targetSwitchCase.parent! as SwitchStatement;
|
|
final SwitchBackwardJumpInfo targetInfo =
|
|
switchBackwardJumpInfos[targetSwitch]!;
|
|
if (targetSwitchCase.expressions.isEmpty) {
|
|
// Default case
|
|
assert(targetSwitchCase.isDefault);
|
|
b.br(targetInfo.defaultLoopLabel!);
|
|
return;
|
|
}
|
|
final Expression targetValue =
|
|
targetSwitchCase.expressions[0]; // pick any of the values
|
|
translateExpression(targetValue, targetInfo.switchValueLocal.type);
|
|
b.local_set(targetInfo.switchValueLocal);
|
|
b.br(targetInfo.loopLabel);
|
|
}
|
|
}
|
|
|
|
@override
|
|
void visitYieldStatement(YieldStatement node) {
|
|
unimplemented(node, node.runtimeType, const []);
|
|
}
|
|
|
|
@override
|
|
w.ValueType visitAwaitExpression(
|
|
AwaitExpression node,
|
|
w.ValueType expectedType,
|
|
) {
|
|
throw 'Await expression in code generator: $node (${node.location})';
|
|
}
|
|
|
|
@override
|
|
w.ValueType visitBlockExpression(
|
|
BlockExpression node,
|
|
w.ValueType expectedType,
|
|
) {
|
|
translateStatement(node.body);
|
|
return translateExpression(node.value, expectedType);
|
|
}
|
|
|
|
@override
|
|
w.ValueType visitLet(Let node, w.ValueType expectedType) {
|
|
translateVariable(node.variable);
|
|
return translateExpression(node.body, expectedType);
|
|
}
|
|
|
|
@override
|
|
w.ValueType visitThisExpression(
|
|
ThisExpression node,
|
|
w.ValueType expectedType,
|
|
) {
|
|
return visitThis(expectedType);
|
|
}
|
|
|
|
w.ValueType visitThis(w.ValueType expectedType) {
|
|
w.ValueType thisType = thisLocal!.type;
|
|
w.ValueType preciseThisType = preciseThisLocal!.type;
|
|
assert(!thisType.nullable);
|
|
assert(!preciseThisType.nullable);
|
|
if (thisType.isSubtypeOf(expectedType)) {
|
|
b.local_get(thisLocal!);
|
|
return thisType;
|
|
}
|
|
if (preciseThisType.isSubtypeOf(expectedType)) {
|
|
b.local_get(preciseThisLocal!);
|
|
return preciseThisType;
|
|
}
|
|
// A user of `this` may have more precise type information, in which case
|
|
// we downcast it here.
|
|
b.local_get(thisLocal!);
|
|
translator.convertType(b, thisType, expectedType);
|
|
return expectedType;
|
|
}
|
|
|
|
@override
|
|
w.ValueType visitConstructorInvocation(
|
|
ConstructorInvocation node,
|
|
w.ValueType expectedType,
|
|
) {
|
|
w.ValueType? intrinsicResult = intrinsifier.generateConstructorIntrinsic(
|
|
node,
|
|
);
|
|
if (intrinsicResult != null) return intrinsicResult;
|
|
|
|
ClassInfo info = translator.classInfo[node.target.enclosingClass]!;
|
|
|
|
final target = node.targetReference;
|
|
_visitArguments(
|
|
node.arguments,
|
|
translator.signatureForDirectCall(target),
|
|
translator.paramInfoForDirectCall(target),
|
|
0,
|
|
);
|
|
|
|
if (info.isCyclic) {
|
|
// Cyclic types cannot be instantiated. Any code that tries to instantiate
|
|
// them will fail with stack overflow, which is a trap in Wasm. Here we
|
|
// replace one trap with another.
|
|
b.unreachable();
|
|
return expectedType;
|
|
}
|
|
|
|
return call(target).single;
|
|
}
|
|
|
|
@override
|
|
w.ValueType visitStaticInvocation(
|
|
StaticInvocation node,
|
|
w.ValueType expectedType,
|
|
) {
|
|
if (ExternalEffect.isExternalEffect(node)) {
|
|
return voidMarker;
|
|
}
|
|
w.ValueType? intrinsicResult = intrinsifier.generateStaticIntrinsic(node);
|
|
if (intrinsicResult != null) return intrinsicResult;
|
|
|
|
final target = node.targetReference;
|
|
_visitArguments(
|
|
node.arguments,
|
|
translator.signatureForDirectCall(target),
|
|
translator.paramInfoForDirectCall(target),
|
|
0,
|
|
);
|
|
return translator.outputOrVoid(call(target));
|
|
}
|
|
|
|
Member _lookupSuperTarget(Member interfaceTarget, {required bool setter}) {
|
|
return translator.hierarchy.getDispatchTarget(
|
|
enclosingMember.enclosingClass!.superclass!,
|
|
interfaceTarget.name,
|
|
setter: setter,
|
|
)!;
|
|
}
|
|
|
|
@override
|
|
w.ValueType visitSuperMethodInvocation(
|
|
SuperMethodInvocation node,
|
|
w.ValueType expectedType,
|
|
) {
|
|
Reference target = translator.getFunctionEntry(
|
|
_lookupSuperTarget(node.interfaceTarget, setter: false).reference,
|
|
uncheckedEntry: true,
|
|
);
|
|
w.FunctionType targetFunctionType = translator.signatureForDirectCall(
|
|
target,
|
|
);
|
|
final w.ValueType receiverType = translator.preciseThisFor(target.asMember);
|
|
|
|
// When calling `==` and the argument is potentially nullable, check if the
|
|
// argument is `null`.
|
|
if (node.name == equalsName) {
|
|
assert(node.arguments.positional.length == 1);
|
|
assert(node.arguments.named.isEmpty);
|
|
final argument = node.arguments.positional[0];
|
|
if (dartTypeOf(argument).isPotentiallyNullable) {
|
|
w.Label resultBlock = b.block(const [], const [w.NumType.i32]);
|
|
|
|
w.ValueType argumentType = targetFunctionType.inputs[1];
|
|
// `==` arguments are non-nullable.
|
|
assert(argumentType.nullable == false);
|
|
|
|
final argumentNullBlock = b.block(const [], const []);
|
|
|
|
visitThis(receiverType);
|
|
translateExpression(argument, argumentType.withNullability(true));
|
|
b.br_on_null(argumentNullBlock);
|
|
|
|
final resultType = translator.outputOrVoid(call(target));
|
|
// `super ==` should return bool.
|
|
assert(resultType == w.NumType.i32);
|
|
b.br(resultBlock);
|
|
|
|
b.end(); // argumentNullBlock
|
|
|
|
b.i32_const(0); // false
|
|
b.br(resultBlock);
|
|
|
|
b.end(); // resultBlock
|
|
return w.NumType.i32;
|
|
}
|
|
}
|
|
|
|
visitThis(receiverType);
|
|
_visitArguments(
|
|
node.arguments,
|
|
translator.signatureForDirectCall(target),
|
|
translator.paramInfoForDirectCall(target),
|
|
1,
|
|
);
|
|
return translator.outputOrVoid(call(target));
|
|
}
|
|
|
|
@override
|
|
w.ValueType visitInstanceInvocation(
|
|
InstanceInvocation node,
|
|
w.ValueType expectedType,
|
|
) {
|
|
w.ValueType? intrinsicResult = intrinsifier.generateInstanceIntrinsic(node);
|
|
if (intrinsicResult != null) return intrinsicResult;
|
|
|
|
final useUncheckedEntry = translator.canUseUncheckedEntry(
|
|
node.receiver,
|
|
node,
|
|
);
|
|
|
|
w.ValueType callWithNullCheck(
|
|
Procedure target,
|
|
void Function(w.ValueType) onNull,
|
|
) {
|
|
late w.Label done;
|
|
final w.ValueType resultType = _virtualCall(
|
|
node,
|
|
target,
|
|
_VirtualCallKind.Call,
|
|
(signature) {
|
|
done = b.block(const [], signature.outputs);
|
|
final w.Label nullReceiver = b.block();
|
|
translateExpression(node.receiver, translator.topType);
|
|
b.br_on_null(nullReceiver);
|
|
},
|
|
(w.FunctionType signature, ParameterInfo paramInfo) {
|
|
_visitArguments(node.arguments, signature, paramInfo, 1);
|
|
},
|
|
useUncheckedEntry: useUncheckedEntry,
|
|
);
|
|
b.br(done);
|
|
b.end(); // end nullReceiver
|
|
onNull(resultType);
|
|
b.end();
|
|
return resultType;
|
|
}
|
|
|
|
final Procedure target = node.interfaceTarget;
|
|
if (node.kind == InstanceAccessKind.Object) {
|
|
switch (target.name.text) {
|
|
case "toString":
|
|
return callWithNullCheck(
|
|
target,
|
|
(resultType) =>
|
|
translateExpression(StringLiteral("null"), resultType),
|
|
);
|
|
case "noSuchMethod":
|
|
return callWithNullCheck(target, (resultType) {
|
|
final target = node.interfaceTargetReference;
|
|
final signature = translator.signatureForDirectCall(target);
|
|
final paramInfo = translator.paramInfoForDirectCall(target);
|
|
|
|
// Object? receiver
|
|
b.ref_null(translator.topType.heapType);
|
|
// Invocation invocation
|
|
_visitArguments(node.arguments, signature, paramInfo, 1);
|
|
call(translator.invokeNoSuchMethod.reference);
|
|
});
|
|
default:
|
|
unimplemented(node, "Nullable invocation of ${target.name.text}", [
|
|
if (expectedType != voidMarker) expectedType,
|
|
]);
|
|
return expectedType;
|
|
}
|
|
}
|
|
|
|
Member? singleTarget = translator.singleTarget(node);
|
|
|
|
// Custom devirtualization because TFA doesn't correctly devirtualize index
|
|
// accesses on constant lists (see https://dartbug.com/60313)
|
|
if (singleTarget == null &&
|
|
target.kind == ProcedureKind.Operator &&
|
|
target.name == indexGetName) {
|
|
final receiver = node.receiver;
|
|
if (receiver is ConstantExpression && receiver.constant is ListConstant) {
|
|
singleTarget = translator.listBaseIndexOperator;
|
|
}
|
|
}
|
|
|
|
if (singleTarget != null) {
|
|
final target = translator.getFunctionEntry(
|
|
singleTarget.reference,
|
|
uncheckedEntry: useUncheckedEntry,
|
|
);
|
|
final signature = translator.signatureForDirectCall(target);
|
|
final paramInfo = translator.paramInfoForDirectCall(target);
|
|
translateExpression(node.receiver, signature.inputs.first);
|
|
_visitArguments(node.arguments, signature, paramInfo, 1);
|
|
|
|
return translator.outputOrVoid(call(target));
|
|
}
|
|
return _virtualCall(
|
|
node,
|
|
target,
|
|
_VirtualCallKind.Call,
|
|
(signature) => translateExpression(node.receiver, signature.inputs.first),
|
|
(w.FunctionType signature, ParameterInfo paramInfo) {
|
|
_visitArguments(node.arguments, signature, paramInfo, 1);
|
|
},
|
|
useUncheckedEntry: useUncheckedEntry,
|
|
);
|
|
}
|
|
|
|
@override
|
|
w.ValueType visitDynamicInvocation(
|
|
DynamicInvocation node,
|
|
w.ValueType expectedType,
|
|
) {
|
|
return _handleDynamicInvocation(node.receiver, node.arguments, node.name);
|
|
}
|
|
|
|
w.ValueType _handleDynamicInvocation(
|
|
Expression receiver,
|
|
Arguments arguments,
|
|
Name memberName,
|
|
) {
|
|
// Call dynamic invocation forwarder
|
|
final typeArguments = arguments.types;
|
|
final positionalArguments = arguments.positional;
|
|
final namedArguments = arguments.named;
|
|
final callShape = MethodCallShape(
|
|
memberName,
|
|
typeArguments.length,
|
|
positionalArguments.length,
|
|
namedArguments.map((n) => n.name).toList()..sort(),
|
|
);
|
|
final forwarder = translator
|
|
.getDynamicDispatchersForModule(b.moduleBuilder)
|
|
.getDispatcher(callShape);
|
|
|
|
// Evaluate receiver
|
|
translateExpression(receiver, translator.topType);
|
|
|
|
// Evaluate type arguments.
|
|
for (final typeArgument in typeArguments) {
|
|
translator.types.makeType(this, typeArgument);
|
|
}
|
|
|
|
// Evaluate positional arguments
|
|
for (final argument in positionalArguments) {
|
|
translateExpression(argument, translator.topType);
|
|
}
|
|
|
|
// Evaluate named arguments. The arguments need to be evaluated in the
|
|
// order they appear in the AST, but need to be sorted based on names in
|
|
// the argument list passed to the dynamic forwarder. Create a local for
|
|
// each argument to allow adding values to the list in expected order.
|
|
final namedArgumentLocals = <String, w.Local>{};
|
|
for (final namedArgument in namedArguments) {
|
|
translateExpression(namedArgument.value, translator.topType);
|
|
final argumentLocal = addLocal(translator.topType);
|
|
b.local_set(argumentLocal);
|
|
namedArgumentLocals[namedArgument.name] = argumentLocal;
|
|
}
|
|
|
|
// Load named arguments in sorted order.
|
|
for (final name in callShape.named) {
|
|
b.local_get(namedArgumentLocals[name]!);
|
|
}
|
|
|
|
translator.callFunction(forwarder.function, b);
|
|
if (callShape.isIndexSet) {
|
|
b.ref_null(w.HeapType.none);
|
|
}
|
|
|
|
return translator.topType;
|
|
}
|
|
|
|
@override
|
|
w.ValueType visitEqualsCall(EqualsCall node, w.ValueType expectedType) {
|
|
w.ValueType? intrinsicResult = intrinsifier.generateEqualsIntrinsic(node);
|
|
if (intrinsicResult != null) return intrinsicResult;
|
|
|
|
final leftType = translator.translateType(dartTypeOf(node.left));
|
|
Member? singleTarget = translator.singleTarget(node);
|
|
if (singleTarget == translator.coreTypes.objectEquals ||
|
|
// If leftType is not a Dart type (or builtin value type) then use
|
|
// reference equality (e.g. the vtable type is not a subtype of
|
|
// topType).
|
|
(leftType is w.RefType && !leftType.isSubtypeOf(translator.topType))) {
|
|
// Plain reference comparison
|
|
translateExpression(node.left, w.RefType.eq(nullable: true));
|
|
translateExpression(node.right, w.RefType.eq(nullable: true));
|
|
b.ref_eq();
|
|
} else {
|
|
// Check operands for null, then call implementation
|
|
bool leftNullable = dartTypeOf(node.left).isPotentiallyNullable;
|
|
bool rightNullable = dartTypeOf(node.right).isPotentiallyNullable;
|
|
w.RefType leftType = translator.topType.withNullability(leftNullable);
|
|
w.RefType rightType = translator.topType.withNullability(rightNullable);
|
|
w.Local leftLocal = addLocal(leftType);
|
|
w.Local rightLocal = addLocal(rightType);
|
|
w.Label? operandNull;
|
|
w.Label? done;
|
|
if (leftNullable || rightNullable) {
|
|
done = b.block(const [], const [w.NumType.i32]);
|
|
operandNull = b.block();
|
|
}
|
|
translateExpression(node.left, leftLocal.type);
|
|
b.local_set(leftLocal);
|
|
translateExpression(node.right, rightLocal.type);
|
|
if (rightNullable) {
|
|
b.local_tee(rightLocal);
|
|
b.br_on_null(operandNull!);
|
|
b.drop();
|
|
} else {
|
|
b.local_set(rightLocal);
|
|
}
|
|
|
|
void left([_]) {
|
|
b.local_get(leftLocal);
|
|
if (leftNullable) {
|
|
b.br_on_null(operandNull!);
|
|
}
|
|
}
|
|
|
|
void right([_, _]) {
|
|
b.local_get(rightLocal);
|
|
if (rightNullable) {
|
|
b.ref_as_non_null();
|
|
}
|
|
}
|
|
|
|
final useUncheckedEntry = translator.canUseUncheckedEntry(
|
|
node.left,
|
|
node,
|
|
);
|
|
if (singleTarget != null) {
|
|
left();
|
|
right();
|
|
call(
|
|
translator.getFunctionEntry(
|
|
singleTarget.reference,
|
|
uncheckedEntry: useUncheckedEntry,
|
|
),
|
|
);
|
|
} else {
|
|
_virtualCall(
|
|
node,
|
|
node.interfaceTarget,
|
|
_VirtualCallKind.Call,
|
|
left,
|
|
right,
|
|
useUncheckedEntry: useUncheckedEntry,
|
|
);
|
|
}
|
|
if (leftNullable || rightNullable) {
|
|
b.br(done!);
|
|
b.end(); // operandNull
|
|
if (leftNullable && rightNullable) {
|
|
// Both sides nullable - compare references
|
|
b.local_get(leftLocal);
|
|
b.local_get(rightLocal);
|
|
b.ref_eq();
|
|
} else {
|
|
// Only one side nullable - not equal if one is null
|
|
b.i32_const(0);
|
|
}
|
|
b.end(); // done
|
|
}
|
|
}
|
|
return w.NumType.i32;
|
|
}
|
|
|
|
@override
|
|
w.ValueType visitEqualsNull(EqualsNull node, w.ValueType expectedType) {
|
|
translateExpression(node.expression, const w.RefType.any(nullable: true));
|
|
b.ref_is_null();
|
|
return w.NumType.i32;
|
|
}
|
|
|
|
w.ValueType _virtualCall(
|
|
TreeNode node,
|
|
Member interfaceTarget,
|
|
_VirtualCallKind kind,
|
|
void Function(w.FunctionType signature) pushReceiver,
|
|
void Function(w.FunctionType signature, ParameterInfo) pushArguments, {
|
|
required bool useUncheckedEntry,
|
|
}) {
|
|
assert(kind != _VirtualCallKind.Get || !useUncheckedEntry);
|
|
final reference = interfaceTarget.referenceAs(
|
|
getter: kind.isGetter,
|
|
setter: kind.isSetter,
|
|
);
|
|
final selector = translator.dispatchTable.selectorForTarget(reference);
|
|
final signature = selector.signature;
|
|
final name = selector.entryPointName(useUncheckedEntry);
|
|
assert(selector.name == interfaceTarget.name.text);
|
|
|
|
pushReceiver(signature);
|
|
|
|
final targets = selector.targets(unchecked: useUncheckedEntry);
|
|
List<({Range range, Reference target})> targetRanges =
|
|
targets.allTargetRanges;
|
|
List<({Range range, Reference target})> staticDispatchRanges =
|
|
targets.staticDispatchRanges;
|
|
|
|
// NOTE: Keep this in sync with
|
|
// `dynamic_dispatchers.dart:generateNoSuchMethodCall`.
|
|
final bool noTarget = targetRanges.isEmpty;
|
|
final bool directCall =
|
|
targetRanges.length == 1 && staticDispatchRanges.length == 1;
|
|
final callPolymorphicDispatcher =
|
|
!directCall && staticDispatchRanges.isNotEmpty;
|
|
|
|
if (noTarget) {
|
|
// Unreachable call
|
|
b.comment(
|
|
"Virtual call of $name with no targets"
|
|
" at ${node.location}",
|
|
);
|
|
pushArguments(signature, selector.paramInfo);
|
|
for (int i = 0; i < signature.inputs.length; ++i) {
|
|
b.drop();
|
|
}
|
|
b.block(const [], signature.outputs);
|
|
b.unreachable();
|
|
b.end();
|
|
return translator.outputOrVoid(signature.outputs);
|
|
}
|
|
if (directCall) {
|
|
final target = translator.getFunctionEntry(
|
|
targetRanges[0].target,
|
|
uncheckedEntry: useUncheckedEntry,
|
|
);
|
|
final directCallSignature = translator.signatureForDirectCall(target);
|
|
final paramInfo = translator.paramInfoForDirectCall(target);
|
|
pushArguments(directCallSignature, paramInfo);
|
|
return translator.outputOrVoid(call(target));
|
|
}
|
|
|
|
// Receiver is already on stack.
|
|
w.Local receiverVar = addLocal(signature.inputs.first);
|
|
assert(!receiverVar.type.nullable);
|
|
b.local_tee(receiverVar);
|
|
if (callPolymorphicDispatcher) {
|
|
b.loadClassId(translator, receiverVar.type);
|
|
b.local_get(receiverVar);
|
|
}
|
|
pushArguments(signature, selector.paramInfo);
|
|
|
|
if (callPolymorphicDispatcher) {
|
|
b.invoke(
|
|
translator
|
|
.getPolymorphicDispatchersForModule(b.moduleBuilder)
|
|
.getPolymorphicDispatcher(
|
|
selector,
|
|
useUncheckedEntry: useUncheckedEntry,
|
|
),
|
|
);
|
|
} else {
|
|
b.comment("Instance $kind of '$name'");
|
|
b.local_get(receiverVar);
|
|
translator.callDispatchTable(
|
|
b,
|
|
selector,
|
|
interfaceTarget: reference,
|
|
useUncheckedEntry: useUncheckedEntry,
|
|
);
|
|
}
|
|
if (selector.synthesizeNullReturnValue) {
|
|
assert(selector.signature.outputs.isEmpty);
|
|
b.ref_null(w.HeapType.none);
|
|
return w.RefType(w.HeapType.none, nullable: true);
|
|
}
|
|
if (selector.synthesizeNoReturn) {
|
|
assert(selector.signature.outputs.isEmpty);
|
|
b.unreachable();
|
|
return voidMarker;
|
|
}
|
|
|
|
return translator.outputOrVoid(signature.outputs);
|
|
}
|
|
|
|
@override
|
|
w.ValueType visitVariableGet(VariableGet node, w.ValueType expectedType) {
|
|
w.Local? local = locals[node.variable];
|
|
Capture? capture = closures.captures[node.variable];
|
|
if (capture != null) {
|
|
if (!capture.written && local != null) {
|
|
b.local_get(local);
|
|
return local.type;
|
|
} else {
|
|
b.local_get(capture.context.currentLocal);
|
|
b.struct_get(capture.context.struct, capture.fieldIndex);
|
|
return capture.type;
|
|
}
|
|
} else {
|
|
if (local == null) {
|
|
throw "Read of undefined variable ${node.variable}";
|
|
}
|
|
b.local_get(local);
|
|
return local.type;
|
|
}
|
|
}
|
|
|
|
@override
|
|
w.ValueType visitVariableSet(VariableSet node, w.ValueType expectedType) {
|
|
w.Local? local = locals[node.variable];
|
|
Capture? capture = closures.captures[node.variable];
|
|
bool preserved = expectedType != voidMarker;
|
|
if (capture != null) {
|
|
assert(capture.written);
|
|
b.local_get(capture.context.currentLocal);
|
|
translateExpression(node.value, capture.type);
|
|
if (preserved) {
|
|
w.Local temp = addLocal(capture.type);
|
|
b.local_tee(temp);
|
|
b.struct_set(capture.context.struct, capture.fieldIndex);
|
|
b.local_get(temp);
|
|
return temp.type;
|
|
} else {
|
|
b.struct_set(capture.context.struct, capture.fieldIndex);
|
|
return voidMarker;
|
|
}
|
|
} else {
|
|
if (local == null) {
|
|
throw "Write of undefined variable ${node.variable}";
|
|
}
|
|
translateExpression(node.value, local.type);
|
|
if (preserved) {
|
|
b.local_tee(local);
|
|
return local.type;
|
|
} else {
|
|
b.local_set(local);
|
|
return voidMarker;
|
|
}
|
|
}
|
|
}
|
|
|
|
@override
|
|
w.ValueType visitStaticGet(StaticGet node, w.ValueType expectedType) {
|
|
w.ValueType? intrinsicResult = intrinsifier.generateStaticGetterIntrinsic(
|
|
node,
|
|
);
|
|
if (intrinsicResult != null) return intrinsicResult;
|
|
|
|
return translator.outputOrVoid(call(node.targetReference));
|
|
}
|
|
|
|
@override
|
|
w.ValueType visitStaticTearOff(StaticTearOff node, w.ValueType expectedType) {
|
|
instantiateConstant(StaticTearOffConstant(node.target), expectedType);
|
|
return expectedType;
|
|
}
|
|
|
|
@override
|
|
w.ValueType visitStaticSet(StaticSet node, w.ValueType expectedType) {
|
|
bool preserved = expectedType != voidMarker;
|
|
Member target = node.target;
|
|
final reference = target is Field
|
|
? target.setterReference!
|
|
: target.reference;
|
|
w.ValueType paramType = translator
|
|
.signatureForDirectCall(reference)
|
|
.inputs
|
|
.single;
|
|
translateExpression(node.value, paramType);
|
|
if (!preserved) {
|
|
call(node.targetReference);
|
|
b.drop(); // Drop `null` from setter call.
|
|
return voidMarker;
|
|
}
|
|
w.Local temp = addLocal(paramType);
|
|
b.local_tee(temp);
|
|
|
|
call(reference);
|
|
b.drop(); // Drop `null` from setter call.
|
|
b.local_get(temp);
|
|
return temp.type;
|
|
}
|
|
|
|
@override
|
|
w.ValueType visitSuperPropertyGet(
|
|
SuperPropertyGet node,
|
|
w.ValueType expectedType,
|
|
) {
|
|
Member target = _lookupSuperTarget(node.interfaceTarget, setter: false);
|
|
if (target is Procedure && !target.isGetter) {
|
|
// Super tear-off
|
|
w.StructType closureStruct = _pushClosure(
|
|
translator.getTearOffClosure(target, b.moduleBuilder),
|
|
translator.getTearOffType(target),
|
|
() => visitThis(w.RefType.struct(nullable: false)),
|
|
);
|
|
return w.RefType.def(closureStruct, nullable: false);
|
|
}
|
|
return _directGet(target, ThisExpression());
|
|
}
|
|
|
|
@override
|
|
w.ValueType visitSuperPropertySet(
|
|
SuperPropertySet node,
|
|
w.ValueType expectedType,
|
|
) {
|
|
Member target = _lookupSuperTarget(node.interfaceTarget, setter: true);
|
|
return _directSet(
|
|
target,
|
|
ThisExpression(),
|
|
node.value,
|
|
preserved: expectedType != voidMarker,
|
|
useUncheckedEntry: true,
|
|
);
|
|
}
|
|
|
|
@override
|
|
w.ValueType visitInstanceGet(InstanceGet node, w.ValueType expectedType) {
|
|
Member target = node.interfaceTarget;
|
|
if (node.kind == InstanceAccessKind.Object) {
|
|
late w.Label doneLabel;
|
|
w.ValueType resultType = _virtualCall(
|
|
node,
|
|
target,
|
|
_VirtualCallKind.Get,
|
|
(signature) {
|
|
doneLabel = b.block(const [], signature.outputs);
|
|
w.Label nullLabel = b.block();
|
|
translateExpression(node.receiver, translator.topType);
|
|
b.br_on_null(nullLabel);
|
|
},
|
|
(_, _) {},
|
|
useUncheckedEntry: false,
|
|
);
|
|
b.br(doneLabel);
|
|
b.end(); // nullLabel
|
|
switch (target.name.text) {
|
|
case "hashCode":
|
|
b.i64_const(2011);
|
|
break;
|
|
case "runtimeType":
|
|
translateExpression(
|
|
ConstantExpression(TypeLiteralConstant(NullType())),
|
|
resultType,
|
|
);
|
|
break;
|
|
default:
|
|
unimplemented(node, "Nullable get of ${target.name.text}", [
|
|
resultType,
|
|
]);
|
|
break;
|
|
}
|
|
b.end(); // doneLabel
|
|
return resultType;
|
|
}
|
|
|
|
Member? singleTarget = translator.singleTarget(node);
|
|
if (singleTarget != null) {
|
|
final intrinsic = intrinsifier.generateInstanceGetterIntrinsic(node);
|
|
if (intrinsic != null) return intrinsic;
|
|
|
|
return _directGet(singleTarget, node.receiver);
|
|
} else {
|
|
return _virtualCall(
|
|
node,
|
|
target,
|
|
_VirtualCallKind.Get,
|
|
(signature) =>
|
|
translateExpression(node.receiver, signature.inputs.first),
|
|
(_, _) {},
|
|
useUncheckedEntry: false,
|
|
);
|
|
}
|
|
}
|
|
|
|
@override
|
|
w.ValueType visitDynamicGet(DynamicGet node, w.ValueType expectedType) {
|
|
final receiver = node.receiver;
|
|
final memberName = node.name;
|
|
final callShape = GetterCallShape(memberName);
|
|
final dispatcher = translator
|
|
.getDynamicDispatchersForModule(b.moduleBuilder)
|
|
.getDispatcher(callShape);
|
|
|
|
// Evaluate receiver
|
|
translateExpression(receiver, translator.topType);
|
|
|
|
// Call get dispatcher
|
|
translator.callFunction(dispatcher.function, b);
|
|
|
|
return translator.topType;
|
|
}
|
|
|
|
@override
|
|
w.ValueType visitDynamicSet(DynamicSet node, w.ValueType expectedType) {
|
|
final receiver = node.receiver;
|
|
final value = node.value;
|
|
final memberName = node.name;
|
|
final callShape = SetterCallShape(memberName);
|
|
final dispatcher = translator
|
|
.getDynamicDispatchersForModule(b.moduleBuilder)
|
|
.getDispatcher(callShape);
|
|
|
|
final savedValue = b.addLocal(translator.topType);
|
|
translateExpression(receiver, translator.topType);
|
|
translateExpression(value, translator.topType);
|
|
b.local_tee(savedValue);
|
|
translator.callFunction(dispatcher.function, b);
|
|
b.local_get(savedValue);
|
|
return savedValue.type;
|
|
}
|
|
|
|
w.ValueType _directGet(Member target, Expression receiver) {
|
|
if (target is Field) {
|
|
ClassInfo info = translator.classInfo[target.enclosingClass]!;
|
|
int fieldIndex = translator.fieldIndex[target]!;
|
|
w.ValueType receiverType = info.nonNullableType;
|
|
w.ValueType fieldType = info.struct.fields[fieldIndex].type.unpacked;
|
|
translateExpression(receiver, receiverType);
|
|
b.struct_get(info.struct, fieldIndex);
|
|
return fieldType;
|
|
} else {
|
|
// Instance call of getter
|
|
assert(target is Procedure && target.isGetter);
|
|
w.FunctionType targetFunctionType = translator.signatureForDirectCall(
|
|
target.reference,
|
|
);
|
|
translateExpression(receiver, targetFunctionType.inputs.single);
|
|
return translator.outputOrVoid(call(target.reference));
|
|
}
|
|
}
|
|
|
|
@override
|
|
w.ValueType visitInstanceTearOff(
|
|
InstanceTearOff node,
|
|
w.ValueType expectedType,
|
|
) {
|
|
Member target = node.interfaceTarget;
|
|
|
|
if (node.kind == InstanceAccessKind.Object) {
|
|
late w.Label doneLabel;
|
|
w.ValueType resultType = _virtualCall(
|
|
node,
|
|
target,
|
|
_VirtualCallKind.Get,
|
|
(signature) {
|
|
doneLabel = b.block(const [], signature.outputs);
|
|
w.Label nullLabel = b.block();
|
|
translateExpression(node.receiver, translator.topType);
|
|
b.br_on_null(nullLabel);
|
|
translator.convertType(b, translator.topType, signature.inputs[0]);
|
|
},
|
|
(_, _) {},
|
|
useUncheckedEntry: false,
|
|
);
|
|
b.br(doneLabel);
|
|
b.end(); // nullLabel
|
|
switch (target.name.text) {
|
|
case "toString":
|
|
translateExpression(
|
|
ConstantExpression(StaticTearOffConstant(translator.nullToString)),
|
|
resultType,
|
|
);
|
|
break;
|
|
case "noSuchMethod":
|
|
translateExpression(
|
|
ConstantExpression(
|
|
StaticTearOffConstant(translator.invokeNullNoSuchMethod),
|
|
),
|
|
resultType,
|
|
);
|
|
break;
|
|
default:
|
|
unimplemented(node, "Nullable tear-off of ${target.name.text}", [
|
|
resultType,
|
|
]);
|
|
break;
|
|
}
|
|
b.end(); // doneLabel
|
|
return resultType;
|
|
}
|
|
|
|
return _virtualCall(
|
|
node,
|
|
target,
|
|
_VirtualCallKind.Get,
|
|
(signature) => translateExpression(node.receiver, signature.inputs.first),
|
|
(_, _) {},
|
|
useUncheckedEntry: false,
|
|
);
|
|
}
|
|
|
|
@override
|
|
w.ValueType visitInstanceSet(InstanceSet node, w.ValueType expectedType) {
|
|
bool preserved = expectedType != voidMarker;
|
|
w.Local? temp;
|
|
Member? singleTarget = translator.singleTarget(node);
|
|
final useUncheckedEntry = translator.canUseUncheckedEntry(
|
|
node.receiver,
|
|
node,
|
|
);
|
|
if (singleTarget != null) {
|
|
return _directSet(
|
|
singleTarget,
|
|
node.receiver,
|
|
node.value,
|
|
preserved: preserved,
|
|
useUncheckedEntry: useUncheckedEntry,
|
|
);
|
|
} else {
|
|
_virtualCall(
|
|
node,
|
|
node.interfaceTarget,
|
|
_VirtualCallKind.Set,
|
|
(signature) =>
|
|
translateExpression(node.receiver, signature.inputs.first),
|
|
(signature, _) {
|
|
w.ValueType paramType = signature.inputs.last;
|
|
translateExpression(node.value, paramType);
|
|
if (preserved) {
|
|
temp = addLocal(paramType);
|
|
b.local_tee(temp!);
|
|
}
|
|
},
|
|
useUncheckedEntry: useUncheckedEntry,
|
|
);
|
|
b.drop(); // Drop `null` from setter call.
|
|
if (preserved) {
|
|
b.local_get(temp!);
|
|
return temp!.type;
|
|
} else {
|
|
return voidMarker;
|
|
}
|
|
}
|
|
}
|
|
|
|
w.ValueType _directSet(
|
|
Member target,
|
|
Expression receiver,
|
|
Expression value, {
|
|
required bool preserved,
|
|
required bool useUncheckedEntry,
|
|
}) {
|
|
w.Local? temp;
|
|
final Reference reference = translator.getFunctionEntry(
|
|
(target is Field)
|
|
? target.setterReference!
|
|
: (target as Procedure).reference,
|
|
uncheckedEntry: useUncheckedEntry,
|
|
);
|
|
final w.FunctionType targetFunctionType = translator.signatureForDirectCall(
|
|
reference,
|
|
);
|
|
final w.ValueType paramType = targetFunctionType.inputs.last;
|
|
translateExpression(receiver, targetFunctionType.inputs.first);
|
|
translateExpression(value, paramType);
|
|
if (preserved) {
|
|
temp = addLocal(paramType);
|
|
b.local_tee(temp);
|
|
}
|
|
call(reference);
|
|
b.drop(); // Drop `null` from setter call.
|
|
if (preserved) {
|
|
b.local_get(temp!);
|
|
return temp.type;
|
|
} else {
|
|
return voidMarker;
|
|
}
|
|
}
|
|
|
|
@override
|
|
void visitFunctionDeclaration(FunctionDeclaration node) {
|
|
Capture? capture = closures.captures[node.variable];
|
|
bool locallyClosurized = closures.closurizedFunctions.contains(node);
|
|
if (capture != null || locallyClosurized) {
|
|
if (capture != null) {
|
|
b.local_get(capture.context.currentLocal);
|
|
}
|
|
w.StructType struct = _instantiateClosure(node.function);
|
|
if (locallyClosurized) {
|
|
w.Local local = addLocal(w.RefType.def(struct, nullable: false));
|
|
locals[node.variable] = local;
|
|
if (capture != null) {
|
|
b.local_tee(local);
|
|
} else {
|
|
b.local_set(local);
|
|
}
|
|
}
|
|
if (capture != null) {
|
|
b.struct_set(capture.context.struct, capture.fieldIndex);
|
|
}
|
|
}
|
|
}
|
|
|
|
@override
|
|
w.ValueType visitFunctionExpression(
|
|
FunctionExpression node,
|
|
w.ValueType expectedType,
|
|
) {
|
|
w.StructType struct = _instantiateClosure(node.function);
|
|
return w.RefType.def(struct, nullable: false);
|
|
}
|
|
|
|
w.StructType _instantiateClosure(FunctionNode functionNode) {
|
|
Lambda lambda = closures.lambdas[functionNode]!;
|
|
ClosureImplementation closure = translator.getClosure(
|
|
functionNode,
|
|
lambda.callTarget,
|
|
b.moduleBuilder,
|
|
ParameterInfo.fromLocalFunction(functionNode),
|
|
"closure wrapper at ${functionNode.location}",
|
|
);
|
|
return _pushClosure(
|
|
closure,
|
|
functionNode.computeFunctionType(Nullability.nonNullable),
|
|
() => _pushContext(functionNode),
|
|
);
|
|
}
|
|
|
|
w.StructType _pushClosure(
|
|
ClosureImplementation closure,
|
|
DartType functionType,
|
|
void Function() pushContext,
|
|
) {
|
|
w.StructType struct = closure.representation.closureStruct;
|
|
|
|
ClassInfo info = translator.closureInfo;
|
|
translator.functions.recordClassAllocation(info.classId);
|
|
|
|
b.pushObjectHeaderFields(translator, info);
|
|
pushContext();
|
|
translator.globals.readGlobal(b, closure.vtable);
|
|
types.makeType(this, functionType);
|
|
b.struct_new(struct);
|
|
|
|
return struct;
|
|
}
|
|
|
|
void _pushContext(FunctionNode functionNode) {
|
|
Context? context = closures.contexts[functionNode]?.parent;
|
|
if (context != null) {
|
|
assert(!context.isEmpty);
|
|
b.local_get(context.currentLocal);
|
|
if (context.currentLocal.type.nullable) {
|
|
b.ref_as_non_null();
|
|
}
|
|
} else {
|
|
translator
|
|
.getDummyValuesCollectorForModule(b.moduleBuilder)
|
|
.instantiateLocalDummyValue(
|
|
b,
|
|
const w.RefType.struct(nullable: false),
|
|
); // Dummy context
|
|
}
|
|
}
|
|
|
|
@override
|
|
w.ValueType visitFunctionInvocation(
|
|
FunctionInvocation node,
|
|
w.ValueType expectedType,
|
|
) {
|
|
w.ValueType? intrinsicResult = intrinsifier.generateFunctionCallIntrinsic(
|
|
node,
|
|
);
|
|
if (intrinsicResult != null) return intrinsicResult;
|
|
|
|
if (node.kind == FunctionAccessKind.Function) {
|
|
// Type of function is `Function`, without the argument types.
|
|
return _handleDynamicInvocation(node.receiver, node.arguments, node.name);
|
|
}
|
|
|
|
List<String> argNames = node.arguments.named.map((a) => a.name).toList()
|
|
..sort();
|
|
ClosureRepresentation? representation = translator.closureLayouter
|
|
.getClosureRepresentation(
|
|
node.arguments.types.length,
|
|
node.arguments.positional.length,
|
|
argNames,
|
|
);
|
|
if (representation == null) {
|
|
// This is a dynamic function call with a signature that matches no
|
|
// functions in the program.
|
|
b.unreachable();
|
|
return translator.topType;
|
|
}
|
|
|
|
final SingleClosureTarget? directClosureCall = translator
|
|
.singleClosureTarget(node, representation, typeContext);
|
|
|
|
if (directClosureCall != null) {
|
|
return _generateDirectClosureCall(
|
|
node,
|
|
representation,
|
|
directClosureCall,
|
|
);
|
|
}
|
|
|
|
return _generateClosureInvocation(node, representation);
|
|
}
|
|
|
|
w.ValueType _generateDirectClosureCall(
|
|
FunctionInvocation node,
|
|
ClosureRepresentation representation,
|
|
SingleClosureTarget closureTarget,
|
|
) {
|
|
final closureStruct = representation.closureStruct;
|
|
final closureStructRef = w.RefType.def(closureStruct, nullable: false);
|
|
final callTarget = closureTarget.callTarget;
|
|
assert(callTarget is AstCallTarget || callTarget is LambdaCallTarget);
|
|
final signature = callTarget.signature;
|
|
final paramInfo = closureTarget.paramInfo;
|
|
|
|
if (callTarget is AstCallTarget) {
|
|
if (paramInfo.takesContextOrReceiver) {
|
|
translateExpression(node.receiver, closureStructRef);
|
|
b.struct_get(closureStruct, FieldIndex.closureContext);
|
|
translator.convertType(b, closureContextFieldType, signature.inputs[0]);
|
|
_visitArguments(node.arguments, signature, paramInfo, 1);
|
|
} else {
|
|
_visitArguments(node.arguments, signature, paramInfo, 0);
|
|
}
|
|
return translator.outputOrVoid(translator.callTarget(callTarget, b));
|
|
}
|
|
|
|
assert(paramInfo.takesContextOrReceiver);
|
|
translateExpression(node.receiver, closureStructRef);
|
|
b.struct_get(closureStruct, FieldIndex.closureContext);
|
|
translator.convertType(b, closureContextFieldType, signature.inputs[0]);
|
|
_visitArguments(node.arguments, signature, paramInfo, 1);
|
|
return translator.outputOrVoid(translator.callTarget(callTarget, b));
|
|
}
|
|
|
|
w.ValueType _generateClosureInvocation(
|
|
FunctionInvocation node,
|
|
ClosureRepresentation representation,
|
|
) {
|
|
final closureStruct = representation.closureStruct;
|
|
|
|
// Evaluate receiver
|
|
w.Local closureLocal = addLocal(
|
|
w.RefType.def(closureStruct, nullable: false),
|
|
);
|
|
translateExpression(node.receiver, closureLocal.type);
|
|
b.local_tee(closureLocal);
|
|
b.struct_get(closureStruct, FieldIndex.closureContext);
|
|
|
|
// Type arguments
|
|
for (DartType typeArg in node.arguments.types) {
|
|
types.makeType(this, typeArg);
|
|
}
|
|
|
|
// Positional arguments
|
|
for (Expression arg in node.arguments.positional) {
|
|
translateExpression(arg, translator.topType);
|
|
}
|
|
|
|
// Named arguments
|
|
final List<String> argNames =
|
|
node.arguments.named.map((a) => a.name).toList()..sort();
|
|
final Map<String, w.Local> namedLocals = {};
|
|
for (final namedArg in node.arguments.named) {
|
|
final w.Local namedLocal = addLocal(translator.topType);
|
|
namedLocals[namedArg.name] = namedLocal;
|
|
translateExpression(namedArg.value, namedLocal.type);
|
|
b.local_set(namedLocal);
|
|
}
|
|
for (String name in argNames) {
|
|
b.local_get(namedLocals[name]!);
|
|
}
|
|
|
|
final int vtableFieldIndex = representation.fieldIndexForSignature(
|
|
node.arguments.positional.length,
|
|
argNames,
|
|
);
|
|
final w.FunctionType functionType = representation.vtableStruct
|
|
.getVtableEntryAt(vtableFieldIndex);
|
|
|
|
// Call entry point in vtable
|
|
b.local_get(closureLocal);
|
|
b.struct_get(closureStruct, FieldIndex.closureVtable);
|
|
b.struct_get(representation.vtableStruct, vtableFieldIndex);
|
|
b.call_ref(functionType);
|
|
|
|
return translator.topType;
|
|
}
|
|
|
|
@override
|
|
w.ValueType visitLocalFunctionInvocation(
|
|
LocalFunctionInvocation node,
|
|
w.ValueType expectedType,
|
|
) {
|
|
var decl = node.variable.parent as FunctionDeclaration;
|
|
Lambda lambda = closures.lambdas[decl.function]!;
|
|
_pushContext(decl.function);
|
|
Arguments arguments = node.arguments;
|
|
_visitArguments(
|
|
arguments,
|
|
lambda.callTarget.signature,
|
|
ParameterInfo.fromLocalFunction(decl.function),
|
|
1,
|
|
);
|
|
b.comment("Local call of ${decl.variable.name}");
|
|
return translator.outputOrVoid(translator.callTarget(lambda.callTarget, b));
|
|
}
|
|
|
|
@override
|
|
w.ValueType visitInstantiation(Instantiation node, w.ValueType expectedType) {
|
|
DartType type = dartTypeOf(node.expression);
|
|
if (type is FunctionType) {
|
|
int typeCount = type.typeParameters.length;
|
|
int posArgCount = type.positionalParameters.length;
|
|
List<String> argNames = type.namedParameters.map((a) => a.name).toList();
|
|
ClosureRepresentation representation = translator.closureLayouter
|
|
.getClosureRepresentation(typeCount, posArgCount, argNames)!;
|
|
|
|
// Operand closure
|
|
w.RefType closureType = w.RefType.def(
|
|
representation.closureStruct,
|
|
nullable: false,
|
|
);
|
|
w.Local closureTemp = addLocal(closureType);
|
|
translateExpression(node.expression, closureType);
|
|
b.local_tee(closureTemp);
|
|
|
|
// Type arguments
|
|
for (DartType typeArg in node.typeArguments) {
|
|
types.makeType(this, typeArg);
|
|
}
|
|
|
|
// Instantiation function
|
|
final vtableIndex =
|
|
translator.closureLayouter.vtableInstantiationFunctionIndex;
|
|
final instantiationFunctionType = representation.vtableStruct
|
|
.getVtableEntryAt(vtableIndex);
|
|
|
|
b.local_get(closureTemp);
|
|
b.struct_get(representation.closureStruct, FieldIndex.closureVtable);
|
|
b.struct_get(representation.vtableStruct, vtableIndex);
|
|
|
|
// Call instantiation function
|
|
b.call_ref(instantiationFunctionType);
|
|
return instantiationFunctionType.outputs.single;
|
|
} else {
|
|
// Only other alternative is `NeverType`.
|
|
assert(type is NeverType);
|
|
b.unreachable();
|
|
return voidMarker;
|
|
}
|
|
}
|
|
|
|
@override
|
|
w.ValueType visitLogicalExpression(
|
|
LogicalExpression node,
|
|
w.ValueType expectedType,
|
|
) {
|
|
_conditional(node, () => b.i32_const(1), () => b.i32_const(0), const [
|
|
w.NumType.i32,
|
|
]);
|
|
return w.NumType.i32;
|
|
}
|
|
|
|
@override
|
|
w.ValueType visitNot(Not node, w.ValueType expectedType) {
|
|
translateExpression(node.operand, w.NumType.i32);
|
|
b.i32_eqz();
|
|
return w.NumType.i32;
|
|
}
|
|
|
|
@override
|
|
w.ValueType visitConditionalExpression(
|
|
ConditionalExpression node,
|
|
w.ValueType expectedType,
|
|
) {
|
|
_conditional(
|
|
node.condition,
|
|
() => translateExpression(node.then, expectedType),
|
|
() => translateExpression(node.otherwise, expectedType),
|
|
[if (expectedType != voidMarker) expectedType],
|
|
);
|
|
return expectedType;
|
|
}
|
|
|
|
@override
|
|
w.ValueType visitNullCheck(NullCheck node, w.ValueType expectedType) {
|
|
w.ValueType operandType = translator.translateType(
|
|
dartTypeOf(node.operand),
|
|
);
|
|
w.ValueType nonNullOperandType = operandType.withNullability(false);
|
|
|
|
// In rare cases the operand is non-nullable but TFA doesn't optimize away
|
|
// the null check. If the operand is an unboxed type, the br_on_non_null
|
|
// would fail to compile.
|
|
if (!operandType.nullable) {
|
|
translateExpression(node.operand, operandType);
|
|
return nonNullOperandType;
|
|
}
|
|
w.Label nullCheckBlock = b.block(const [], [nonNullOperandType]);
|
|
translateExpression(node.operand, operandType);
|
|
|
|
// We lower a null check to a br_on_non_null, throwing a [TypeError] in
|
|
// the null case.
|
|
b.br_on_non_null(nullCheckBlock);
|
|
call(translator.throwNullCheckErrorWithCurrentStack.reference);
|
|
b.unreachable();
|
|
b.end();
|
|
return nonNullOperandType;
|
|
}
|
|
|
|
void _visitArguments(
|
|
Arguments node,
|
|
w.FunctionType signature,
|
|
ParameterInfo paramInfo,
|
|
int signatureOffset,
|
|
) {
|
|
// Type arguments
|
|
for (int i = 0; i < node.types.length; i++) {
|
|
types.makeType(this, node.types[i]);
|
|
}
|
|
signatureOffset += node.types.length;
|
|
|
|
// Positional arguments
|
|
for (int i = 0; i < node.positional.length; i++) {
|
|
translateExpression(
|
|
node.positional[i],
|
|
signature.inputs[signatureOffset + i],
|
|
);
|
|
}
|
|
// Push default values for optional positional parameters.
|
|
for (int i = node.positional.length; i < paramInfo.positional.length; i++) {
|
|
final w.ValueType type = signature.inputs[signatureOffset + i];
|
|
instantiateConstant(paramInfo.positional[i]!, type);
|
|
}
|
|
|
|
// Named arguments. Store evaluated arguments in locals to be able to
|
|
// re-order them based on the `ParameterInfo`.
|
|
final Map<String, w.Local> namedLocals = {};
|
|
for (var namedArg in node.named) {
|
|
final w.ValueType type = signature
|
|
.inputs[signatureOffset + paramInfo.nameIndex[namedArg.name]!];
|
|
final w.Local namedLocal = addLocal(type);
|
|
namedLocals[namedArg.name] = namedLocal;
|
|
translateExpression(namedArg.value, namedLocal.type);
|
|
b.local_set(namedLocal);
|
|
}
|
|
// Re-order named arguments and push default values for optional named
|
|
// parameters.
|
|
for (String name in paramInfo.names) {
|
|
w.Local? namedLocal = namedLocals[name];
|
|
final w.ValueType type =
|
|
signature.inputs[signatureOffset + paramInfo.nameIndex[name]!];
|
|
if (namedLocal != null) {
|
|
b.local_get(namedLocal);
|
|
} else {
|
|
instantiateConstant(paramInfo.named[name]!, type);
|
|
}
|
|
}
|
|
}
|
|
|
|
@override
|
|
w.ValueType visitStringConcatenation(
|
|
StringConcatenation node,
|
|
w.ValueType expectedType,
|
|
) {
|
|
bool isConstantString(Expression expr) =>
|
|
expr is StringLiteral ||
|
|
(expr is ConstantExpression && expr.constant is StringConstant);
|
|
|
|
String extractConstantString(Expression expr) {
|
|
if (expr is StringLiteral) {
|
|
return expr.value;
|
|
} else {
|
|
return ((expr as ConstantExpression).constant as StringConstant).value;
|
|
}
|
|
}
|
|
|
|
final expressions = node.expressions;
|
|
if (expressions.every(isConstantString)) {
|
|
StringBuffer result = StringBuffer();
|
|
for (final expr in expressions) {
|
|
result.write(extractConstantString(expr));
|
|
}
|
|
final expr = StringLiteral(result.toString());
|
|
return visitStringLiteral(expr, expectedType);
|
|
}
|
|
|
|
late final Procedure target;
|
|
|
|
// We have special cases for 1/2/3/4 arguments.
|
|
if (expressions.length <= 4) {
|
|
final nullableObjectType = translator.translateType(
|
|
translator.coreTypes.objectNullableRawType,
|
|
);
|
|
for (final expression in expressions) {
|
|
translateExpression(expression, nullableObjectType);
|
|
}
|
|
if (expressions.length == 1) {
|
|
target = translator.stringImplInterpolate1;
|
|
} else if (expressions.length == 2) {
|
|
target = translator.stringImplInterpolate2;
|
|
} else if (expressions.length == 3) {
|
|
target = translator.stringImplInterpolate3;
|
|
} else {
|
|
assert(expressions.length == 4);
|
|
target = translator.stringImplInterpolate4;
|
|
}
|
|
} else {
|
|
final nullableObjectType = translator.coreTypes.objectNullableRawType;
|
|
makeArrayFromExpressions(expressions, nullableObjectType);
|
|
target = translator.stringImplInterpolate;
|
|
}
|
|
return translator.outputOrVoid(call(target.reference));
|
|
}
|
|
|
|
@override
|
|
w.ValueType visitThrow(Throw node, w.ValueType expectedType) {
|
|
// Front-end wraps the argument with `as Object` when necessary, so we can
|
|
// assume non-nullable here.
|
|
assert(!dartTypeOf(node.expression).isPotentiallyNullable);
|
|
translateExpression(node.expression, translator.topTypeNonNullable);
|
|
call(translator.errorThrowWithCurrentStackTrace.reference);
|
|
b.unreachable();
|
|
return expectedType;
|
|
}
|
|
|
|
@override
|
|
w.ValueType visitRethrow(Rethrow node, w.ValueType expectedType) {
|
|
final exceptionLocals = tryBlockLocals.last;
|
|
b.local_get(exceptionLocals.exceptionLocal);
|
|
b.local_get(exceptionLocals.stackTraceLocal);
|
|
b.throw_(translator.getDartExceptionTag(b.moduleBuilder));
|
|
return expectedType;
|
|
}
|
|
|
|
@override
|
|
w.ValueType visitConstantExpression(
|
|
ConstantExpression node,
|
|
w.ValueType expectedType,
|
|
) {
|
|
instantiateConstant(node.constant, expectedType);
|
|
return expectedType;
|
|
}
|
|
|
|
@override
|
|
w.ValueType visitNullLiteral(NullLiteral node, w.ValueType expectedType) {
|
|
instantiateConstant(NullConstant(), expectedType);
|
|
return expectedType;
|
|
}
|
|
|
|
@override
|
|
w.ValueType visitStringLiteral(StringLiteral node, w.ValueType expectedType) {
|
|
instantiateConstant(StringConstant(node.value), expectedType);
|
|
return expectedType;
|
|
}
|
|
|
|
@override
|
|
w.ValueType visitBoolLiteral(BoolLiteral node, w.ValueType expectedType) {
|
|
instantiateConstant(BoolConstant(node.value), expectedType);
|
|
return expectedType;
|
|
}
|
|
|
|
@override
|
|
w.ValueType visitIntLiteral(IntLiteral node, w.ValueType expectedType) {
|
|
instantiateConstant(IntConstant(node.value), expectedType);
|
|
return expectedType;
|
|
}
|
|
|
|
@override
|
|
w.ValueType visitDoubleLiteral(DoubleLiteral node, w.ValueType expectedType) {
|
|
instantiateConstant(DoubleConstant(node.value), expectedType);
|
|
return expectedType;
|
|
}
|
|
|
|
@override
|
|
w.ValueType visitListLiteral(ListLiteral node, w.ValueType expectedType) {
|
|
final useSharedCreator = types.isTypeConstant(node.typeArgument);
|
|
|
|
final passType = !useSharedCreator;
|
|
final passArray = node.expressions.isNotEmpty;
|
|
|
|
final targetReference = passArray
|
|
? translator.growableListFromWasmArray.reference
|
|
: translator.growableListEmpty.reference;
|
|
|
|
final target = useSharedCreator
|
|
? translator
|
|
.getPartialInstantiatorForModule(b.moduleBuilder)
|
|
.getOneTypeArgumentForwarder(
|
|
targetReference,
|
|
node.typeArgument,
|
|
'create${passArray ? '' : 'Empty'}List<${node.typeArgument}>',
|
|
)
|
|
: translator.functions.getFunction(targetReference);
|
|
|
|
if (passType) {
|
|
types.makeType(this, node.typeArgument);
|
|
}
|
|
if (passArray) {
|
|
makeArrayFromExpressions(
|
|
node.expressions,
|
|
translator.coreTypes.objectRawType(Nullability.nullable),
|
|
);
|
|
}
|
|
|
|
translator.callFunction(target, b);
|
|
|
|
return target.type.outputs.single;
|
|
}
|
|
|
|
w.ValueType makeArrayFromExpressions(
|
|
List<Expression> expressions,
|
|
InterfaceType elementType,
|
|
) {
|
|
return makeArray(
|
|
translator.arrayTypeForDartType(elementType, mutable: true),
|
|
expressions.length,
|
|
(w.ValueType type, int i) {
|
|
translateExpression(expressions[i], type);
|
|
},
|
|
);
|
|
}
|
|
|
|
w.ValueType makeArray(
|
|
w.ArrayType arrayType,
|
|
int length,
|
|
void Function(w.ValueType, int) generateItem,
|
|
) {
|
|
return translator.makeArray(b, arrayType, length, generateItem);
|
|
}
|
|
|
|
@override
|
|
w.ValueType visitMapLiteral(MapLiteral node, w.ValueType expectedType) {
|
|
final useSharedCreator =
|
|
types.isTypeConstant(node.keyType) &&
|
|
types.isTypeConstant(node.valueType);
|
|
|
|
final passTypes = !useSharedCreator;
|
|
final passArray = node.entries.isNotEmpty;
|
|
|
|
final targetReference = passArray
|
|
? translator.mapFromWasmArray.reference
|
|
: translator.mapFactory.reference;
|
|
|
|
final target = useSharedCreator
|
|
? translator
|
|
.getPartialInstantiatorForModule(b.moduleBuilder)
|
|
.getTwoTypeArgumentForwarder(
|
|
targetReference,
|
|
node.keyType,
|
|
node.valueType,
|
|
'create${passArray ? '' : 'Empty'}'
|
|
'Map<${node.keyType}, ${node.valueType}>',
|
|
)
|
|
: translator.functions.getFunction(targetReference);
|
|
|
|
if (passTypes) {
|
|
types.makeType(this, node.keyType);
|
|
types.makeType(this, node.valueType);
|
|
}
|
|
if (passArray) {
|
|
makeArray(translator.nullableObjectArrayType, 2 * node.entries.length, (
|
|
elementType,
|
|
elementIndex,
|
|
) {
|
|
final index = elementIndex ~/ 2;
|
|
final entry = node.entries[index];
|
|
if (elementIndex % 2 == 0) {
|
|
translateExpression(entry.key, elementType);
|
|
} else {
|
|
translateExpression(entry.value, elementType);
|
|
}
|
|
});
|
|
}
|
|
translator.callFunction(target, b);
|
|
|
|
return target.type.outputs.single;
|
|
}
|
|
|
|
@override
|
|
w.ValueType visitSetLiteral(SetLiteral node, w.ValueType expectedType) {
|
|
final useSharedCreator = types.isTypeConstant(node.typeArgument);
|
|
|
|
final passType = !useSharedCreator;
|
|
final passArray = node.expressions.isNotEmpty;
|
|
|
|
final targetReference = passArray
|
|
? translator.setFromWasmArray.reference
|
|
: translator.setFactory.reference;
|
|
|
|
final target = useSharedCreator
|
|
? translator
|
|
.getPartialInstantiatorForModule(b.moduleBuilder)
|
|
.getOneTypeArgumentForwarder(
|
|
targetReference,
|
|
node.typeArgument,
|
|
'create${passArray ? '' : 'Empty'}Set<${node.typeArgument}>',
|
|
)
|
|
: translator.functions.getFunction(targetReference);
|
|
|
|
if (passType) {
|
|
types.makeType(this, node.typeArgument);
|
|
}
|
|
if (passArray) {
|
|
makeArrayFromExpressions(
|
|
node.expressions,
|
|
translator.coreTypes.objectRawType(Nullability.nullable),
|
|
);
|
|
}
|
|
translator.callFunction(target, b);
|
|
|
|
return target.type.outputs.single;
|
|
}
|
|
|
|
@override
|
|
w.ValueType visitTypeLiteral(TypeLiteral node, w.ValueType expectedType) {
|
|
return types.makeType(this, node.type);
|
|
}
|
|
|
|
@override
|
|
w.ValueType visitIsExpression(IsExpression node, w.ValueType expectedType) {
|
|
final operandType = dartTypeOf(node.operand);
|
|
final boxedOperandType = operandType.isPotentiallyNullable
|
|
? translator.topType
|
|
: translator.topTypeNonNullable;
|
|
translateExpression(node.operand, boxedOperandType);
|
|
types.emitIsTest(this, node.type, operandType, node.location);
|
|
return w.NumType.i32;
|
|
}
|
|
|
|
@override
|
|
w.ValueType visitAsExpression(AsExpression node, w.ValueType expectedType) {
|
|
final isImplicitCheck =
|
|
(node.isTypeError || node.isCovarianceCheck || node.isForDynamic);
|
|
if (node.isUnchecked ||
|
|
(translator.options.omitImplicitTypeChecks && isImplicitCheck) ||
|
|
(translator.options.omitExplicitTypeChecks && !isImplicitCheck)) {
|
|
return translateExpression(node.operand, expectedType);
|
|
}
|
|
|
|
final operandType = dartTypeOf(node.operand);
|
|
final boxedOperandType = operandType.isPotentiallyNullable
|
|
? translator.topType
|
|
: translator.topTypeNonNullable;
|
|
translateExpression(node.operand, boxedOperandType);
|
|
return types.emitAsCheck(
|
|
this,
|
|
node.isCovarianceCheck,
|
|
node.type,
|
|
operandType,
|
|
boxedOperandType,
|
|
node.location,
|
|
);
|
|
}
|
|
|
|
@override
|
|
w.ValueType visitLoadLibrary(LoadLibrary node, w.ValueType expectedType) {
|
|
throw UnsupportedError(
|
|
'LoadLibrary should be lowered by modular transformer.',
|
|
);
|
|
}
|
|
|
|
@override
|
|
w.ValueType visitCheckLibraryIsLoaded(
|
|
CheckLibraryIsLoaded node,
|
|
w.ValueType expectedType,
|
|
) {
|
|
throw UnsupportedError(
|
|
'CheckLibraryIsLoaded should be lowered by modular transformer.',
|
|
);
|
|
}
|
|
|
|
/// Pushes the `_Type` object for a function or class type parameter to the
|
|
/// stack and returns the value type of the object.
|
|
w.ValueType instantiateTypeParameter(TypeParameter parameter) {
|
|
w.ValueType resultType;
|
|
|
|
w.Local? local = typeLocals[parameter];
|
|
Capture? capture = closures.captures[parameter];
|
|
if (local != null) {
|
|
b.local_get(local);
|
|
resultType = local.type;
|
|
} else if (capture != null) {
|
|
Capture capture = closures.captures[parameter]!;
|
|
b.local_get(capture.context.currentLocal);
|
|
b.struct_get(capture.context.struct, capture.fieldIndex);
|
|
resultType = capture.type;
|
|
} else {
|
|
Class cls = parameter.declaration as Class;
|
|
ClassInfo info = translator.classInfo[cls]!;
|
|
int fieldIndex = translator.typeParameterIndex[parameter]!;
|
|
visitThis(info.nonNullableType);
|
|
b.struct_get(info.struct, fieldIndex);
|
|
resultType = info.struct.fields[fieldIndex].type.unpacked;
|
|
}
|
|
|
|
translator.convertType(b, resultType, types.nonNullableTypeType);
|
|
return types.nonNullableTypeType;
|
|
}
|
|
|
|
@override
|
|
w.ValueType visitRecordLiteral(RecordLiteral node, w.ValueType expectedType) {
|
|
final ClassInfo recordClassInfo = translator.getRecordClassInfo(
|
|
node.recordType,
|
|
);
|
|
translator.functions.recordClassAllocation(recordClassInfo.classId);
|
|
|
|
b.pushObjectHeaderFields(translator, recordClassInfo);
|
|
for (Expression positional in node.positional) {
|
|
translateExpression(positional, translator.topType);
|
|
}
|
|
for (NamedExpression named in node.named) {
|
|
translateExpression(named.value, translator.topType);
|
|
}
|
|
b.struct_new(recordClassInfo.struct);
|
|
|
|
return recordClassInfo.nonNullableType;
|
|
}
|
|
|
|
@override
|
|
w.ValueType visitRecordIndexGet(
|
|
RecordIndexGet node,
|
|
w.ValueType expectedType,
|
|
) {
|
|
final RecordShape recordShape = RecordShape.fromType(node.receiverType);
|
|
final ClassInfo recordClassInfo = translator.getRecordClassInfo(
|
|
node.receiverType,
|
|
);
|
|
|
|
translateExpression(node.receiver, translator.topTypeNonNullable);
|
|
b.ref_cast(w.RefType(recordClassInfo.struct, nullable: false));
|
|
b.struct_get(
|
|
recordClassInfo.struct,
|
|
recordShape.getPositionalIndex(node.index),
|
|
);
|
|
|
|
return translator.topType;
|
|
}
|
|
|
|
@override
|
|
w.ValueType visitRecordNameGet(RecordNameGet node, w.ValueType expectedType) {
|
|
final RecordShape recordShape = RecordShape.fromType(node.receiverType);
|
|
final ClassInfo recordClassInfo = translator.getRecordClassInfo(
|
|
node.receiverType,
|
|
);
|
|
|
|
translateExpression(node.receiver, translator.topTypeNonNullable);
|
|
b.ref_cast(w.RefType(recordClassInfo.struct, nullable: false));
|
|
b.struct_get(recordClassInfo.struct, recordShape.getNameIndex(node.name));
|
|
|
|
return translator.topType;
|
|
}
|
|
|
|
@override
|
|
w.ValueType visitFileUriExpression(
|
|
FileUriExpression node,
|
|
w.ValueType expectedType,
|
|
) {
|
|
return translateExpression(node.expression, expectedType);
|
|
}
|
|
|
|
/// Generate code that checks type of an argument against an expected type
|
|
/// and throws a `TypeError` on failure.
|
|
///
|
|
/// Expects a boxed object (whose type is to be checked) on the stack.
|
|
///
|
|
/// [argName] is used in the type error as the name of the argument that
|
|
/// doesn't match the expected type.
|
|
void _generateArgumentTypeCheck(
|
|
String argName,
|
|
w.RefType argumentType,
|
|
DartType testedAgainstType,
|
|
) {
|
|
if (translator.options.minify) {
|
|
// We don't need to include the name in the error message, so we can use
|
|
// the optimized `as` checks.
|
|
types.emitAsCheck(
|
|
this,
|
|
false,
|
|
testedAgainstType,
|
|
translator.coreTypes.objectNullableRawType,
|
|
argumentType,
|
|
);
|
|
b.drop();
|
|
} else {
|
|
final argLocal = b.addLocal(argumentType);
|
|
b.local_tee(argLocal);
|
|
types.emitIsTest(
|
|
this,
|
|
testedAgainstType,
|
|
translator.coreTypes.objectNullableRawType,
|
|
);
|
|
b.i32_eqz();
|
|
b.if_();
|
|
b.local_get(argLocal);
|
|
types.makeType(this, testedAgainstType);
|
|
_emitString(argName);
|
|
call(translator.stackTraceCurrent.reference);
|
|
call(translator.throwArgumentTypeCheckError.reference);
|
|
b.unreachable();
|
|
b.end();
|
|
}
|
|
}
|
|
|
|
void _generateTypeArgumentBoundCheck(
|
|
String argName,
|
|
w.Local typeLocal,
|
|
DartType bound,
|
|
) {
|
|
b.local_get(typeLocal);
|
|
final boundLocal = b.addLocal(translator.runtimeTypeType);
|
|
types.makeType(this, bound);
|
|
b.local_tee(boundLocal);
|
|
call(translator.isTypeSubtype.reference);
|
|
|
|
b.i32_eqz();
|
|
b.if_();
|
|
// Type check failed
|
|
b.local_get(typeLocal);
|
|
b.local_get(boundLocal);
|
|
_emitString(argName);
|
|
call(translator.stackTraceCurrent.reference);
|
|
call(translator.throwTypeArgumentBoundCheckError.reference);
|
|
b.unreachable();
|
|
b.end();
|
|
}
|
|
|
|
void _emitString(String str) => translateExpression(
|
|
StringLiteral(str),
|
|
translator.translateType(translator.coreTypes.stringNonNullableRawType),
|
|
);
|
|
|
|
@override
|
|
void visitPatternSwitchStatement(PatternSwitchStatement node) {
|
|
// This node is internal to the front end and removed by the constant
|
|
// evaluator.
|
|
throw UnsupportedError("CodeGenerator.visitPatternSwitchStatement");
|
|
}
|
|
|
|
@override
|
|
void visitPatternVariableDeclaration(PatternVariableDeclaration node) {
|
|
// This node is internal to the front end and removed by the constant
|
|
// evaluator.
|
|
throw UnsupportedError("CodeGenerator.visitPatternVariableDeclaration");
|
|
}
|
|
|
|
@override
|
|
void visitIfCaseStatement(IfCaseStatement node) {
|
|
// This node is internal to the front end and removed by the constant
|
|
// evaluator.
|
|
throw UnsupportedError("CodeGenerator.visitIfCaseStatement");
|
|
}
|
|
|
|
void debugRuntimePrint(String s) {
|
|
final printFunction = translator.functions.getFunction(
|
|
translator.printToConsole.reference,
|
|
);
|
|
translator.constants.instantiateConstant(
|
|
b,
|
|
StringConstant(s),
|
|
printFunction.type.inputs[0],
|
|
);
|
|
translator.callFunction(printFunction, b);
|
|
}
|
|
|
|
@override
|
|
void visitAuxiliaryStatement(AuxiliaryStatement node) {
|
|
throw UnsupportedError(
|
|
"Unsupported auxiliary statement $node (${node.runtimeType}).",
|
|
);
|
|
}
|
|
|
|
@override
|
|
void visitAuxiliaryInitializer(AuxiliaryInitializer node) {
|
|
throw UnsupportedError(
|
|
"Unsupported auxiliary initializer $node (${node.runtimeType}).",
|
|
);
|
|
}
|
|
|
|
void emitUnimplementedExternalError(Member member) {
|
|
b.comment("Unimplemented external member $member at ${member.location}");
|
|
if (member.isInstanceMember) {
|
|
b.local_get(paramLocals[0]);
|
|
} else {
|
|
b.ref_null(w.HeapType.none);
|
|
}
|
|
translator.constants.instantiateConstant(
|
|
b,
|
|
translator.symbols.methodSymbolFromName(member.name),
|
|
translator.classInfo[translator.symbolClass]!.nonNullableType,
|
|
);
|
|
call(
|
|
translator
|
|
.noSuchMethodErrorThrowUnimplementedExternalMemberError
|
|
.reference,
|
|
);
|
|
b.unreachable();
|
|
}
|
|
|
|
void instantiateConstant(Constant constant, w.ValueType expectedType) {
|
|
translator.constants.instantiateConstant(
|
|
b,
|
|
constant,
|
|
expectedType,
|
|
deferredModuleGuard: translator.moduleForConstant(constant),
|
|
);
|
|
}
|
|
}
|
|
|
|
CodeGenerator getMemberCodeGenerator(
|
|
Translator translator,
|
|
w.FunctionBuilder functionBuilder,
|
|
Reference memberReference,
|
|
) {
|
|
final member = memberReference.asMember;
|
|
final asyncMarker = member.function?.asyncMarker ?? AsyncMarker.Sync;
|
|
final codeGen = getInlinableMemberCodeGenerator(
|
|
translator,
|
|
asyncMarker,
|
|
functionBuilder.type,
|
|
memberReference,
|
|
);
|
|
if (codeGen != null) return codeGen;
|
|
|
|
final Class? memberClass = member.enclosingClass;
|
|
if (memberClass != null && translator.classInfo[memberClass]!.isCyclic) {
|
|
return UnreachableCodeGenerator(translator, functionBuilder.type, member);
|
|
}
|
|
|
|
final procedure = member as Procedure;
|
|
|
|
if (asyncMarker == AsyncMarker.SyncStar) {
|
|
return SyncStarProcedureCodeGenerator(
|
|
translator,
|
|
procedure,
|
|
functionBuilder.type,
|
|
functionBuilder.name,
|
|
);
|
|
}
|
|
assert(asyncMarker == AsyncMarker.Async);
|
|
return AsyncProcedureCodeGenerator(
|
|
translator,
|
|
procedure,
|
|
functionBuilder.type,
|
|
functionBuilder.name,
|
|
);
|
|
}
|
|
|
|
CodeGenerator getLambdaCodeGenerator(Translator translator, Lambda lambda) {
|
|
final enclosingMember = lambda.enclosingMember;
|
|
final enclosingClass = enclosingMember.enclosingClass;
|
|
if (enclosingClass != null &&
|
|
translator.classInfo[enclosingClass]!.isCyclic) {
|
|
return UnreachableCodeGenerator(
|
|
translator,
|
|
lambda.callTarget.signature,
|
|
enclosingMember,
|
|
);
|
|
}
|
|
|
|
final asyncMarker = lambda.functionNode.asyncMarker;
|
|
|
|
if (asyncMarker == AsyncMarker.Async) {
|
|
return AsyncLambdaCodeGenerator(translator, lambda);
|
|
}
|
|
if (asyncMarker == AsyncMarker.SyncStar) {
|
|
return SyncStarLambdaCodeGenerator(translator, lambda);
|
|
}
|
|
assert(asyncMarker == AsyncMarker.Sync);
|
|
return SynchronousLambdaCodeGenerator(translator, lambda);
|
|
}
|
|
|
|
/// Returns a [CodeGenerator] for the given member iff that member can be
|
|
/// inlined.
|
|
CodeGenerator? getInlinableMemberCodeGenerator(
|
|
Translator translator,
|
|
AsyncMarker asyncMarker,
|
|
w.FunctionType functionType,
|
|
Reference reference,
|
|
) {
|
|
final Member member = reference.asMember;
|
|
|
|
final Class? memberClass = member.enclosingClass;
|
|
if (memberClass != null && translator.classInfo[memberClass]!.isCyclic) {
|
|
return UnreachableCodeGenerator(translator, functionType, member);
|
|
}
|
|
|
|
if (reference.isTearOffReference) {
|
|
return TearOffCodeGenerator(translator, functionType, member);
|
|
}
|
|
|
|
if (member is Constructor) {
|
|
if (reference.isConstructorBodyReference) {
|
|
return ConstructorBodyCodeGenerator(translator, functionType, member);
|
|
} else if (reference.isInitializerReference) {
|
|
return ConstructorInitializerCodeGenerator(
|
|
translator,
|
|
functionType,
|
|
member,
|
|
);
|
|
} else {
|
|
return ConstructorAllocatorCodeGenerator(
|
|
translator,
|
|
functionType,
|
|
member,
|
|
);
|
|
}
|
|
}
|
|
|
|
if (member is Field) {
|
|
if (member.isStatic) {
|
|
if (reference.isImplicitGetter || reference.isImplicitSetter) {
|
|
return StaticFieldImplicitAccessorCodeGenerator(
|
|
translator,
|
|
functionType,
|
|
member,
|
|
reference.isImplicitGetter,
|
|
);
|
|
}
|
|
return StaticFieldInitializerCodeGenerator(
|
|
translator,
|
|
functionType,
|
|
member,
|
|
);
|
|
}
|
|
final useUncheckedEntry = reference.isUncheckedEntryReference;
|
|
return ImplicitFieldAccessorCodeGenerator(
|
|
translator,
|
|
functionType,
|
|
member,
|
|
reference.isImplicitGetter,
|
|
useUncheckedEntry,
|
|
);
|
|
}
|
|
|
|
if (member is Procedure && asyncMarker == AsyncMarker.Sync) {
|
|
return SynchronousProcedureCodeGenerator(
|
|
translator,
|
|
functionType,
|
|
member,
|
|
reference,
|
|
reference.entryKind,
|
|
);
|
|
}
|
|
|
|
assert(
|
|
asyncMarker == AsyncMarker.SyncStar || asyncMarker == AsyncMarker.Async,
|
|
);
|
|
return null;
|
|
}
|
|
|
|
class SynchronousProcedureCodeGenerator extends AstCodeGenerator {
|
|
final Procedure member;
|
|
final Reference reference;
|
|
final EntryPoint kind;
|
|
|
|
SynchronousProcedureCodeGenerator(
|
|
Translator translator,
|
|
w.FunctionType functionType,
|
|
this.member,
|
|
this.reference,
|
|
this.kind,
|
|
) : super(translator, functionType, member) {
|
|
assert(
|
|
!translator.needToCheckTypesFor(member) || kind != EntryPoint.normal,
|
|
);
|
|
}
|
|
|
|
@override
|
|
void generateInternal() {
|
|
final source = member.enclosingComponent!.uriToSource[member.fileUri]!;
|
|
setSourceMapSourceAndFileOffset(source, member.fileOffset);
|
|
|
|
if (intrinsifier.generateMemberIntrinsic(
|
|
member.reference,
|
|
functionType,
|
|
paramLocals,
|
|
returnLabel,
|
|
)) {
|
|
b.end();
|
|
return;
|
|
}
|
|
|
|
if (member.isExternal) {
|
|
emitUnimplementedExternalError(member);
|
|
b.end();
|
|
return;
|
|
}
|
|
|
|
closures = translator.getClosures(member);
|
|
|
|
switch (kind) {
|
|
case EntryPoint.normal:
|
|
b.comment('Normal Entry');
|
|
_makeNonMultiEntryPointFunction();
|
|
case EntryPoint.checked:
|
|
b.comment('Checked Entry');
|
|
_makeMultipleEntryPoint(true);
|
|
case EntryPoint.unchecked:
|
|
b.comment('Unchecked Entry');
|
|
_makeMultipleEntryPoint(false);
|
|
case EntryPoint.body:
|
|
b.comment('Body for Checked & Unchecked Entry');
|
|
_makeMultipleEntryPointSharedBody();
|
|
break;
|
|
}
|
|
}
|
|
|
|
void _makeMultipleEntryPoint(bool checked) {
|
|
final function = member.function;
|
|
final signature = translator.signatureForDirectCall(member.bodyReference);
|
|
if (checked) {
|
|
setupParametersForCheckedEntry(member);
|
|
} else {
|
|
setupParametersForUncheckedEntry(member);
|
|
}
|
|
|
|
int arg = 0;
|
|
visitThis(signature.inputs[arg++]);
|
|
for (final parameter in function.typeParameters) {
|
|
final r = instantiateTypeParameter(parameter);
|
|
translator.convertType(b, r, signature.inputs[arg++]);
|
|
}
|
|
for (final parameter in function.positionalParameters) {
|
|
final local = locals[parameter]!;
|
|
b.local_get(local);
|
|
translator.convertType(b, local.type, signature.inputs[arg++]);
|
|
}
|
|
for (final parameter in function.namedParameters) {
|
|
final local = locals[parameter]!;
|
|
b.local_get(local);
|
|
translator.convertType(b, local.type, signature.inputs[arg++]);
|
|
}
|
|
|
|
final outputs = call(member.bodyReference);
|
|
if (outputs.isNotEmpty) {
|
|
translator.convertType(b, outputs.single, returnType);
|
|
}
|
|
_returnFromFunction();
|
|
b.end();
|
|
}
|
|
|
|
void _makeMultipleEntryPointSharedBody() {
|
|
final function = member.function;
|
|
final typeParameters = function.typeParameters;
|
|
final positionals = function.positionalParameters;
|
|
final named = function.namedParameters;
|
|
|
|
int param = _initializeThis(member.reference);
|
|
|
|
for (int i = 0; i < typeParameters.length; i++) {
|
|
final typeParameter = typeParameters[i];
|
|
typeLocals[typeParameter] = paramLocals[param++];
|
|
}
|
|
void setupParameter(Variable parameter) {
|
|
// The body may assign less precise types to the parameter variable than
|
|
// what the caller provides.
|
|
w.Local local = paramLocals[param++];
|
|
if (translator.typeOfCheckedParameterVariable(parameter) !=
|
|
parameter.type) {
|
|
final newLocal = addLocal(translator.translateType(parameter.type));
|
|
b.local_get(local);
|
|
translator.convertType(b, local.type, newLocal.type);
|
|
b.local_set(newLocal);
|
|
local = newLocal;
|
|
}
|
|
locals[parameter] = local;
|
|
}
|
|
|
|
for (int i = 0; i < positionals.length; i++) {
|
|
setupParameter(positionals[i]);
|
|
}
|
|
for (int i = 0; i < named.length; i++) {
|
|
setupParameter(named[i]);
|
|
}
|
|
|
|
setupContexts(member);
|
|
Statement? body = member.function.body;
|
|
if (body != null) {
|
|
translateStatement(body);
|
|
}
|
|
|
|
_implicitReturn();
|
|
b.end();
|
|
}
|
|
|
|
void _makeNonMultiEntryPointFunction() {
|
|
setupParametersForNormalEntry(member);
|
|
setupContexts(member);
|
|
Statement? body = member.function.body;
|
|
if (body != null) {
|
|
translateStatement(body);
|
|
}
|
|
_implicitReturn();
|
|
b.end();
|
|
return;
|
|
}
|
|
}
|
|
|
|
class TearOffCodeGenerator extends AstCodeGenerator {
|
|
final Member member;
|
|
|
|
TearOffCodeGenerator(
|
|
Translator translator,
|
|
w.FunctionType functionType,
|
|
this.member,
|
|
) : super(translator, functionType, member);
|
|
|
|
@override
|
|
void generateInternal() {
|
|
// Initialize [Closures] without [Closures.captures]: [Closures.captures] is
|
|
// used by `makeType` below, when generating runtime types of type
|
|
// parameters of the function type, but the type parameters are not
|
|
// captured, always loaded from the `this` struct.
|
|
closures = translator.getClosures(member, findCaptures: false);
|
|
|
|
_initializeThis(member.reference);
|
|
Procedure procedure = member as Procedure;
|
|
DartType functionType = translator.getTearOffType(procedure);
|
|
ClosureImplementation closure = translator.getTearOffClosure(
|
|
procedure,
|
|
b.moduleBuilder,
|
|
);
|
|
w.StructType struct = closure.representation.closureStruct;
|
|
|
|
ClassInfo info = translator.closureInfo;
|
|
translator.functions.recordClassAllocation(info.classId);
|
|
|
|
b.pushObjectHeaderFields(translator, info);
|
|
b.local_get(paramLocals[0]); // `this` as context
|
|
// The closure requires a struct value so box `this` if necessary.
|
|
translator.convertType(
|
|
b,
|
|
paramLocals[0].type,
|
|
struct.fields[FieldIndex.closureContext].type.unpacked,
|
|
);
|
|
translator.globals.readGlobal(b, closure.vtable);
|
|
types.makeType(this, functionType);
|
|
b.struct_new(struct);
|
|
b.end();
|
|
}
|
|
}
|
|
|
|
/// Generates code for a dynamic forwarder function.
|
|
///
|
|
/// Dynamic forwarders are functions that
|
|
/// * may have to populate default type arguments
|
|
/// * may have to check argument types
|
|
/// * may have to unbox arguments
|
|
/// * call the normal getter/setter/method target
|
|
/// * may have to box the result value
|
|
///
|
|
/// We generate them for each [CallShape] and the caller guarantees that the
|
|
/// [CallShape] is valid for the given target [reference]. The signature for
|
|
/// such a forwarder function is determined by [makeDynamicForwarderSignature].
|
|
class DynamicForwarderCodeGenerator extends AstCodeGenerator {
|
|
final Reference reference;
|
|
final CallShape callShape;
|
|
|
|
DynamicForwarderCodeGenerator(
|
|
Translator translator,
|
|
w.FunctionType functionType,
|
|
this.reference,
|
|
this.callShape,
|
|
) : super(translator, functionType, reference.asMember);
|
|
|
|
@override
|
|
void generateInternal() {
|
|
final member = reference.asMember;
|
|
|
|
// Initialize [Closures] without [Closures.captures]: Similar to
|
|
// [TearOffCodeGenerator], type parameters will be loaded from the `this`
|
|
// struct.
|
|
closures = translator.getClosures(member, findCaptures: false);
|
|
if (reference.isGetter) {
|
|
_generateDynamicGetterForwarder();
|
|
} else if (reference.isSetter) {
|
|
_generateDynamicSetterForwarder();
|
|
} else if (reference.isTearOffReference) {
|
|
_generateDynamicTearOffForwarder();
|
|
} else {
|
|
member as Procedure;
|
|
_generateDynamicMethodForwarder();
|
|
}
|
|
}
|
|
|
|
void _generateDynamicTearOffForwarder() {
|
|
final targetSignature = translator.signatureForDirectCall(reference);
|
|
assert(
|
|
targetSignature.inputs.length == 1 && targetSignature.outputs.length == 1,
|
|
);
|
|
|
|
// The only real thing the dynamic forwarder for a tear off does is
|
|
// converting the receiver type: The receiver type from the dynamic call
|
|
// site is a non-nullable top type. But the actual tear off getter from the
|
|
// target may require a more precise receiver type.
|
|
b.local_get(paramLocals[0]);
|
|
translator.convertType(
|
|
b,
|
|
paramLocals[0].type,
|
|
targetSignature.inputs.single,
|
|
);
|
|
translator.callReference(reference, b);
|
|
b.return_();
|
|
b.end();
|
|
}
|
|
|
|
void _generateDynamicMethodForwarder() {
|
|
final callShape = this.callShape as MethodCallShape;
|
|
|
|
// The offsets of arguments passed by the caller.
|
|
const int argReceiverOffset = 0;
|
|
const int argTypesOffset = argReceiverOffset + 1;
|
|
final int argPositionalsOffset = argTypesOffset + callShape.typeCount;
|
|
final int argNamedOffset = argPositionalsOffset + callShape.positionalCount;
|
|
|
|
final targetProcedure = reference.asMember as Procedure;
|
|
final targetFunction = targetProcedure.function;
|
|
assert(callShape.matchesTarget(targetFunction));
|
|
|
|
final target = translator.getFunctionEntry(
|
|
targetProcedure.reference,
|
|
uncheckedEntry: false,
|
|
);
|
|
final callTarget = translator.directCallTarget(target);
|
|
|
|
_initializeThis(reference);
|
|
|
|
final typeType =
|
|
translator.classInfo[translator.typeClass]!.nonNullableType;
|
|
final targetParamInfo = translator.paramInfoForDirectCall(target);
|
|
|
|
// Load the receiver
|
|
final receiverLocal = paramLocals[argReceiverOffset];
|
|
b.local_get(receiverLocal);
|
|
translator.convertType(
|
|
b,
|
|
receiverLocal.type,
|
|
callTarget.signature.inputs[0],
|
|
);
|
|
|
|
// Load type parameters for target.
|
|
final targetTypeParams = targetFunction.typeParameters;
|
|
assert(targetTypeParams.length == targetParamInfo.typeParamCount);
|
|
if (targetTypeParams.isNotEmpty) {
|
|
if (callShape.typeCount != 0) {
|
|
// Provided by caller.
|
|
for (int i = 0; i < targetTypeParams.length; ++i) {
|
|
final param = targetTypeParams[i];
|
|
final paramValue = paramLocals[argTypesOffset + i];
|
|
b.local_get(paramValue);
|
|
typeLocals[param] = paramValue;
|
|
}
|
|
} else {
|
|
// Use default-to-bounds.
|
|
for (int i = 0; i < targetTypeParams.length; ++i) {
|
|
final param = targetTypeParams[i];
|
|
types.makeType(this, param.defaultType);
|
|
final paramValue = b.addLocal(typeType);
|
|
b.local_tee(paramValue);
|
|
typeLocals[param] = paramValue;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Check type parameter bounds.
|
|
if (!translator.options.omitImplicitTypeChecks) {
|
|
for (int i = 0; i < targetTypeParams.length; ++i) {
|
|
final param = targetTypeParams[i];
|
|
if (param.bound != translator.coreTypes.objectNullableRawType) {
|
|
final paramValue = typeLocals[param]!;
|
|
_generateTypeArgumentBoundCheck(param.name!, paramValue, param.bound);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Load positional parameters for the target (and check types if needed).
|
|
final targetPositionalParams = targetFunction.positionalParameters;
|
|
for (int i = 0; i < targetParamInfo.positional.length; i++) {
|
|
final targetParamType =
|
|
callTarget.signature.inputs[1 + targetParamInfo.typeParamCount + i];
|
|
if (i < callShape.positionalCount) {
|
|
// Provided by the caller.
|
|
final paramValue = paramLocals[argPositionalsOffset + i];
|
|
b.local_get(paramValue);
|
|
if (!translator.options.omitImplicitTypeChecks) {
|
|
final param = targetPositionalParams[i];
|
|
b.local_get(paramValue);
|
|
_generateArgumentTypeCheck(
|
|
param.name!,
|
|
translator.topType,
|
|
param.type,
|
|
);
|
|
}
|
|
translator.convertType(b, paramValue.type, targetParamType);
|
|
} else {
|
|
// Default to use if the callee has the `i` parameter.
|
|
final defaultFunctionValue = i < targetPositionalParams.length
|
|
? (targetPositionalParams[i].initializer as ConstantExpression?)
|
|
?.constant
|
|
: null;
|
|
// Default to use if callee doesn't have the `i` parameter.
|
|
final defaultValue = targetParamInfo.positional[i];
|
|
// The target wasm function corresponding to an instance method may have
|
|
// a selector signature (which is based on all implementations of a
|
|
// selector) and therefore may have more parameters than the actual
|
|
// target needs (the others are ignored in the callee).
|
|
final value = defaultFunctionValue ?? defaultValue!;
|
|
translator.constants.instantiateConstant(b, value, targetParamType);
|
|
}
|
|
}
|
|
|
|
// Load named arguments (and check types if needed).
|
|
final targetNamedParams = targetFunction.namedParameters;
|
|
for (int i = 0; i < targetParamInfo.names.length; ++i) {
|
|
final targetParamType =
|
|
callTarget.signature.inputs[1 +
|
|
targetParamInfo.typeParamCount +
|
|
targetParamInfo.positional.length +
|
|
i];
|
|
final name = targetParamInfo.names[i];
|
|
final namedParam = targetNamedParams.firstWhereOrNull(
|
|
(n) => n.name == name,
|
|
);
|
|
final callerIndex = callShape.named.indexOf(name);
|
|
if (0 <= callerIndex) {
|
|
// Provided by the caller.
|
|
final paramValue = paramLocals[argNamedOffset + callerIndex];
|
|
b.local_get(paramValue);
|
|
if (!translator.options.omitImplicitTypeChecks) {
|
|
b.local_get(paramValue);
|
|
_generateArgumentTypeCheck(
|
|
name,
|
|
translator.topType,
|
|
namedParam!.type,
|
|
);
|
|
}
|
|
translator.convertType(b, paramValue.type, targetParamType);
|
|
} else {
|
|
// Default to use if callee has the `name` parameter.
|
|
final defaultFunctionValue =
|
|
(namedParam?.initializer as ConstantExpression?)?.constant;
|
|
// Default to use if callee doesn't have `name` parameter.
|
|
final defaultValue = targetParamInfo.named[name];
|
|
// The target wasm function corresponding to an instance method may have
|
|
// a selector signature (which is based on all implementations of a
|
|
// selector) and therefore may have more parameters than the actual
|
|
// target needs (the others are ignored in the callee).
|
|
final value = (defaultFunctionValue ?? defaultValue)!;
|
|
translator.constants.instantiateConstant(b, value, targetParamType);
|
|
}
|
|
}
|
|
|
|
final outputs = translator.callTarget(callTarget, b);
|
|
if (outputs.isNotEmpty) {
|
|
translator.convertType(b, outputs.single, returnType);
|
|
}
|
|
b.return_();
|
|
b.end();
|
|
}
|
|
|
|
void _generateDynamicGetterForwarder() {
|
|
final receiverLocal = paramLocals[0];
|
|
_initializeThis(reference);
|
|
|
|
final member = reference.asMember;
|
|
final info = translator.classInfo[member.enclosingClass]!;
|
|
if (member is Field) {
|
|
int fieldIndex = translator.fieldIndex[member]!;
|
|
b.local_get(receiverLocal);
|
|
translator.convertType(b, receiverLocal.type, info.nonNullableType);
|
|
b.struct_get(info.struct, fieldIndex);
|
|
translator.convertType(
|
|
b,
|
|
info.struct.fields[fieldIndex].type.unpacked,
|
|
translator.topType,
|
|
);
|
|
} else {
|
|
final target = translator.getFunctionEntry(
|
|
reference,
|
|
uncheckedEntry: false,
|
|
);
|
|
final getterWasmType = translator.signatureForDirectCall(target);
|
|
final getterInputs = getterWasmType.inputs;
|
|
final getterOutputs = getterWasmType.outputs;
|
|
b.local_get(receiverLocal);
|
|
translator.convertType(b, receiverLocal.type, getterInputs.single);
|
|
call(target);
|
|
translator.convertType(b, getterOutputs.single, translator.topType);
|
|
}
|
|
|
|
b.end(); // end function
|
|
}
|
|
|
|
void _generateDynamicSetterForwarder() {
|
|
final receiverLocal = paramLocals[0];
|
|
final positionalArgLocal = paramLocals[1];
|
|
|
|
_initializeThis(reference);
|
|
|
|
final member = reference.asMember;
|
|
DartType paramType;
|
|
if (member is Field) {
|
|
paramType = member.type;
|
|
} else {
|
|
paramType = (member as Procedure).setterType;
|
|
}
|
|
|
|
if (!translator.options.omitImplicitTypeChecks) {
|
|
b.local_get(positionalArgLocal);
|
|
_generateArgumentTypeCheck(
|
|
member.name.text,
|
|
positionalArgLocal.type as w.RefType,
|
|
paramType,
|
|
);
|
|
}
|
|
|
|
ClassInfo info = translator.classInfo[member.enclosingClass]!;
|
|
if (member is Field) {
|
|
int fieldIndex = translator.fieldIndex[member]!;
|
|
b.local_get(receiverLocal);
|
|
translator.convertType(b, receiverLocal.type, info.nonNullableType);
|
|
b.local_get(positionalArgLocal);
|
|
translator.convertType(
|
|
b,
|
|
positionalArgLocal.type,
|
|
info.struct.fields[fieldIndex].type.unpacked,
|
|
);
|
|
b.struct_set(info.struct, fieldIndex);
|
|
} else {
|
|
final target = translator.getFunctionEntry(
|
|
reference,
|
|
uncheckedEntry: false,
|
|
);
|
|
final setterWasmType = translator.signatureForDirectCall(target);
|
|
final setterInputs = setterWasmType.inputs;
|
|
assert(setterInputs.length == 2);
|
|
b.local_get(receiverLocal);
|
|
translator.convertType(b, receiverLocal.type, setterInputs[0]);
|
|
b.local_get(positionalArgLocal);
|
|
translator.convertType(b, positionalArgLocal.type, setterInputs[1]);
|
|
call(target);
|
|
b.drop(); // Drop `null` from setter call.
|
|
}
|
|
|
|
b.end(); // end function
|
|
}
|
|
}
|
|
|
|
/// Creates [Invocation] object based on a [CallShape].
|
|
class InvocationCreationStubGenerator implements CodeGenerator {
|
|
final Translator translator;
|
|
final MethodCallShape callShape;
|
|
|
|
InvocationCreationStubGenerator(this.translator, this.callShape);
|
|
|
|
@override
|
|
void generate(
|
|
w.InstructionsBuilder b,
|
|
List<w.Local> paramLocals,
|
|
w.Label? returnLabel,
|
|
) {
|
|
assert(returnLabel == null);
|
|
|
|
int argumentIterator = 0;
|
|
|
|
final typeArgs = b.addLocal(
|
|
w.RefType(translator.typeArrayType, nullable: false),
|
|
);
|
|
for (int i = 0; i < callShape.typeCount; ++i) {
|
|
b.local_get(paramLocals[argumentIterator++]);
|
|
}
|
|
b.array_new_fixed(translator.typeArrayType, callShape.typeCount);
|
|
b.local_set(typeArgs);
|
|
|
|
final posArgs = b.addLocal(
|
|
w.RefType(translator.nullableObjectArrayType, nullable: false),
|
|
);
|
|
for (int i = 0; i < callShape.positionalCount; ++i) {
|
|
b.local_get(paramLocals[argumentIterator++]);
|
|
}
|
|
b.array_new_fixed(
|
|
translator.nullableObjectArrayType,
|
|
callShape.positionalCount,
|
|
);
|
|
b.local_set(posArgs);
|
|
|
|
final namedArgs = b.addLocal(
|
|
w.RefType(translator.nullableObjectArrayType, nullable: false),
|
|
);
|
|
for (int i = 0; i < callShape.named.length; ++i) {
|
|
final name = callShape.named[i];
|
|
translator.constants.instantiateConstant(
|
|
b,
|
|
translator.symbols.symbolForNamedParameter(name),
|
|
translator.topType,
|
|
);
|
|
b.local_get(paramLocals[argumentIterator++]);
|
|
}
|
|
b.array_new_fixed(
|
|
translator.nullableObjectArrayType,
|
|
callShape.named.length * 2,
|
|
);
|
|
b.local_set(namedArgs);
|
|
|
|
createInvocationObject(
|
|
translator,
|
|
b,
|
|
callShape.name,
|
|
typeArgs,
|
|
posArgs,
|
|
namedArgs,
|
|
);
|
|
b.return_();
|
|
b.end();
|
|
}
|
|
}
|
|
|
|
void createInvocationObject(
|
|
Translator translator,
|
|
w.InstructionsBuilder b,
|
|
Name memberName,
|
|
w.Local typeArgsLocal,
|
|
w.Local positionalArgsLocal,
|
|
w.Local namedArgsLocal,
|
|
) {
|
|
translator.constants.instantiateConstant(
|
|
b,
|
|
translator.symbols.methodSymbolFromName(memberName),
|
|
translator.classInfo[translator.symbolClass]!.nonNullableType,
|
|
);
|
|
|
|
b.local_get(typeArgsLocal);
|
|
translator.callReference(translator.typeArgumentsToList.reference, b);
|
|
b.local_get(positionalArgsLocal);
|
|
translator.callReference(translator.positionalParametersToList.reference, b);
|
|
b.local_get(namedArgsLocal);
|
|
translator.callReference(translator.namedParametersToMap.reference, b);
|
|
translator.callReference(
|
|
translator.invocationGenericMethodFactory.reference,
|
|
b,
|
|
);
|
|
}
|
|
|
|
abstract class ConstructorCodeGeneratorBase extends AstCodeGenerator {
|
|
final Constructor member;
|
|
|
|
ConstructorCodeGeneratorBase(
|
|
Translator translator,
|
|
w.FunctionType functionType,
|
|
this.member,
|
|
) : super(translator, functionType, member);
|
|
|
|
List<w.Local> _getConstructorArgumentLocals(
|
|
List<TypeParameter> typeParameters,
|
|
List<Variable> parameters,
|
|
) {
|
|
List<w.Local> constructorArgs = [];
|
|
|
|
for (int i = 0; i < typeParameters.length; i++) {
|
|
constructorArgs.add(typeLocals[typeParameters[i]]!);
|
|
}
|
|
|
|
for (Variable param in parameters) {
|
|
constructorArgs.add(locals[param]!);
|
|
}
|
|
|
|
return constructorArgs;
|
|
}
|
|
|
|
int _setupConstructorParameters(
|
|
List<TypeParameter> typeParameters,
|
|
List<Variable> parameters,
|
|
int parameterOffset,
|
|
) {
|
|
for (int i = 0; i < typeParameters.length; i++) {
|
|
typeLocals[typeParameters[i]] = paramLocals[parameterOffset++];
|
|
}
|
|
|
|
for (int i = 0; i < parameters.length; i++) {
|
|
final variable = parameters[i];
|
|
final local = paramLocals[parameterOffset++];
|
|
final variableName = variable.name;
|
|
if (variableName != null && variableName.isNotEmpty) {
|
|
b.localNames[local.index] = variableName;
|
|
}
|
|
locals[variable] = local;
|
|
}
|
|
return parameterOffset;
|
|
}
|
|
}
|
|
|
|
class ConstructorInitializerCodeGenerator extends ConstructorCodeGeneratorBase {
|
|
// Maps a classes' fields to corresponding locals so that we can update the
|
|
// local directly if a field has both a default value and a FieldInitializer.
|
|
final Map<Field, w.Local> fieldLocals = {};
|
|
|
|
ConstructorInitializerCodeGenerator(
|
|
super.translator,
|
|
super.functionType,
|
|
super.member,
|
|
);
|
|
|
|
@override
|
|
void generateInternal() {
|
|
// Closures are built when constructor functions are added to worklist.
|
|
closures = translator.constructorClosures[member.reference]!;
|
|
|
|
final source = member.enclosingComponent!.uriToSource[member.fileUri]!;
|
|
setSourceMapSourceAndFileOffset(source, member.fileOffset);
|
|
|
|
if (member.isExternal) {
|
|
emitUnimplementedExternalError(member);
|
|
} else {
|
|
final lastInit = member.initializers.lastOrNull;
|
|
if (lastInit is RedirectingInitializer) {
|
|
generateRedirectingInitializerList();
|
|
} else {
|
|
generateInitializerList();
|
|
}
|
|
}
|
|
b.end();
|
|
}
|
|
|
|
void generateRedirectingInitializerList() {
|
|
_setupInitializerListParametersAndContexts();
|
|
for (final init in member.initializers) {
|
|
assert(init is! SuperInitializer);
|
|
init.accept(this);
|
|
}
|
|
// Redirecting generative constructors don't have a body.
|
|
assert(member.function.body is EmptyStatement);
|
|
assert(translator.getConstructorInfo(member).bodyParameters.isEmpty);
|
|
|
|
// The last was a call to the redirectee which means we have
|
|
// [redirectee-body-args, fields] already on the stack.
|
|
assert(member.initializers.last is RedirectingInitializer);
|
|
}
|
|
|
|
/// Code for the initializer function.
|
|
///
|
|
/// For a class hierarchy like
|
|
///
|
|
/// class Object { class-id, identity-hashcode }
|
|
/// class A<...> extends Object { ...A-fields }
|
|
/// class B<...> extends A<...> { ...B-fields }
|
|
/// class C<...> extends C<...> { ...C-fields }
|
|
///
|
|
/// the resulting stack looks like:
|
|
///
|
|
/// [
|
|
/// C-initializer-context?
|
|
/// ...C-body-args
|
|
/// B-initializer-context?
|
|
/// ...B-body-args
|
|
/// A-initializer-context?
|
|
/// ...A-body-args
|
|
/// // NOTE: No Object-fields (class-id, hashcode) here.
|
|
/// ...A-type-fields
|
|
/// ...A-fields
|
|
/// ...B-type-fields
|
|
/// ...B-fields
|
|
/// ...C-type-fields
|
|
/// ...C-fields
|
|
/// ]
|
|
///
|
|
void generateInitializerList() {
|
|
_setupInitializerListParametersAndContexts();
|
|
|
|
Class cls = member.enclosingClass;
|
|
ClassInfo info = translator.classInfo[cls]!;
|
|
final context = closures.contexts[member];
|
|
final constructorInfo = translator.getConstructorInfo(member);
|
|
final lastInit = member.initializers.lastOrNull;
|
|
|
|
_setupDefaultFieldValues(info);
|
|
for (final initializer in member.initializers) {
|
|
initializer.accept(this);
|
|
}
|
|
|
|
if (cls.superclass != null) {
|
|
// checks if a SuperInitializer was dropped because the constructor body
|
|
// throws an error
|
|
if (lastInit is! SuperInitializer) {
|
|
b.unreachable();
|
|
return;
|
|
}
|
|
|
|
// checks if a FieldInitializer was dropped because the constructor body
|
|
// throws an error
|
|
for (Field field in info.cls!.fields) {
|
|
if (field.isInstanceMember && !fieldLocals.containsKey(field)) {
|
|
b.unreachable();
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
final superInit = lastInit as SuperInitializer?;
|
|
final superInfo = info.superInfo!;
|
|
final superClassFields = superInfo.getClassFieldTypes();
|
|
|
|
final superInitOutputs = superInit == null
|
|
? <w.ValueType>[]
|
|
: translator
|
|
.signatureForDirectCall(superInit.target.initializerReference)
|
|
.outputs;
|
|
|
|
final extraTypeFields = getTypeFields(cls, info);
|
|
final extraFields = Map.fromEntries(
|
|
fieldLocals.entries.toList()..sort(
|
|
(x, y) => translator.fieldIndex[x.key]!.compareTo(
|
|
translator.fieldIndex[y.key]!,
|
|
),
|
|
),
|
|
).values.toList();
|
|
|
|
// The last evaluation was a super initializer call that pushed
|
|
// [...superBodyArgs, ...superFields].
|
|
//
|
|
// If we don't have anything to pass to our body constructor, then we can
|
|
// simply add a few more fields of our class and return.
|
|
if (context == null && constructorInfo.bodyParameters.isEmpty) {
|
|
for (final local in [...extraTypeFields, ...extraFields]) {
|
|
b.local_get(local);
|
|
}
|
|
// Now we have
|
|
// [...superBodyArgs, ...superFields, ...extraTypeFields, ...extraFields].
|
|
// i.e.
|
|
// [...superBodyArgs, ...thisFields].
|
|
} else {
|
|
// We have to inject extra args for our body, so pop, inject, push
|
|
|
|
// Pop
|
|
final superclassFieldsReversed = <w.Local>[];
|
|
final superBodyArgsReversed = <w.Local>[];
|
|
if (superInitOutputs.isNotEmpty) {
|
|
// Pop super fields.
|
|
for (final type in superClassFields.reversed) {
|
|
final local = addLocal(type);
|
|
b.local_set(local);
|
|
superclassFieldsReversed.add(local);
|
|
}
|
|
|
|
// Pop args to super bodies.
|
|
final superBodyArgs = superInitOutputs.sublist(
|
|
0,
|
|
superInitOutputs.length - superClassFields.length,
|
|
);
|
|
for (final type in superBodyArgs.reversed) {
|
|
w.Local local = addLocal(type);
|
|
b.local_set(local);
|
|
superBodyArgsReversed.add(local);
|
|
}
|
|
}
|
|
|
|
// Push things for our constructor body
|
|
if (context != null) {
|
|
assert(!context.isEmpty);
|
|
b.local_get(context.currentLocal);
|
|
}
|
|
for (final param in constructorInfo.bodyParameters) {
|
|
b.local_get(locals[param]!);
|
|
}
|
|
// Push things for super constructor bodies.
|
|
for (final local in superBodyArgsReversed.reversed) {
|
|
b.local_get(local);
|
|
}
|
|
// push super fields
|
|
for (final local in superclassFieldsReversed.reversed) {
|
|
b.local_get(local);
|
|
}
|
|
// push our fields
|
|
for (final local in [...extraTypeFields, ...extraFields]) {
|
|
b.local_get(local);
|
|
}
|
|
}
|
|
}
|
|
|
|
void _setupInitializerListParametersAndContexts() {
|
|
int parameterOffset = 0;
|
|
final info = translator.getConstructorInfo(member);
|
|
_setupConstructorParameters(
|
|
info.initializerTypeParameters,
|
|
info.initializerParameters,
|
|
parameterOffset,
|
|
);
|
|
allocateContext(member);
|
|
captureParameters(member);
|
|
}
|
|
|
|
/// The locals for type parameter values which end up in fields (i.e. type
|
|
/// parameters that aren't shared with super class).
|
|
List<w.Local> getTypeFields(Class cls, ClassInfo info) {
|
|
final typeFields = <w.Local>[];
|
|
for (final typeParam in cls.typeParameters) {
|
|
final match = info.typeParameterMatch[typeParam];
|
|
if (match == null) {
|
|
// Type is not contained in super class' fields
|
|
typeFields.add(typeLocals[typeParam]!);
|
|
}
|
|
}
|
|
return typeFields;
|
|
}
|
|
|
|
void _setupDefaultFieldValues(ClassInfo info) {
|
|
fieldLocals.clear();
|
|
|
|
for (Field field in info.cls!.fields) {
|
|
if (field.isInstanceMember && field.initializer != null) {
|
|
final source = field.enclosingComponent!.uriToSource[field.fileUri]!;
|
|
final (oldSource, oldFileOffset) = setSourceMapSourceAndFileOffset(
|
|
source,
|
|
field.fileOffset,
|
|
);
|
|
|
|
int fieldIndex = translator.fieldIndex[field]!;
|
|
w.Local local = addLocal(info.struct.fields[fieldIndex].type.unpacked);
|
|
|
|
translateExpression(
|
|
field.initializer!,
|
|
info.struct.fields[fieldIndex].type.unpacked,
|
|
);
|
|
b.local_set(local);
|
|
fieldLocals[field] = local;
|
|
|
|
setSourceMapSourceAndFileOffset(oldSource, oldFileOffset);
|
|
}
|
|
}
|
|
}
|
|
|
|
@override
|
|
void visitInvalidInitializer(InvalidInitializer node) {}
|
|
|
|
@override
|
|
void visitAssertInitializer(AssertInitializer node) {
|
|
translateStatement(node.statement);
|
|
}
|
|
|
|
@override
|
|
void visitLocalInitializer(LocalInitializer node) {
|
|
translateVariable(node.variable);
|
|
}
|
|
|
|
@override
|
|
void visitFieldInitializer(FieldInitializer node) {
|
|
Class cls = (node.parent as Constructor).enclosingClass;
|
|
w.StructType struct = translator.classInfo[cls]!.struct;
|
|
Field field = node.field;
|
|
int fieldIndex = translator.fieldIndex[field]!;
|
|
|
|
w.Local? local = fieldLocals[field];
|
|
|
|
local ??= addLocal(struct.fields[fieldIndex].type.unpacked);
|
|
|
|
translateExpression(node.value, struct.fields[fieldIndex].type.unpacked);
|
|
b.local_set(local);
|
|
fieldLocals[field] = local;
|
|
}
|
|
|
|
@override
|
|
void visitRedirectingInitializer(RedirectingInitializer node) {
|
|
Class cls = (node.parent as Constructor).enclosingClass;
|
|
|
|
for (TypeParameter typeParam in cls.typeParameters) {
|
|
types.makeType(
|
|
this,
|
|
TypeParameterType(typeParam, Nullability.nonNullable),
|
|
);
|
|
}
|
|
|
|
final targetMember = node.targetReference.asMember;
|
|
final target = targetMember.initializerReference;
|
|
_visitArguments(
|
|
node.arguments,
|
|
translator.signatureForDirectCall(target),
|
|
translator.paramInfoForDirectCall(target),
|
|
cls.typeParameters.length,
|
|
);
|
|
|
|
b.comment("Direct call of '$targetMember Redirected Initializer'");
|
|
call(target);
|
|
}
|
|
|
|
@override
|
|
void visitSuperInitializer(SuperInitializer node) {
|
|
Supertype? supertype =
|
|
(node.parent as Constructor).enclosingClass.supertype;
|
|
Supertype? supersupertype = node.target.enclosingClass.supertype;
|
|
|
|
// Skip calls to the constructor for Object, as this is empty
|
|
if (supersupertype != null) {
|
|
for (DartType typeArg in supertype!.typeArguments) {
|
|
types.makeType(this, typeArg);
|
|
}
|
|
|
|
final targetMember = node.targetReference.asMember;
|
|
final target = targetMember.initializerReference;
|
|
_visitArguments(
|
|
node.arguments,
|
|
translator.signatureForDirectCall(target),
|
|
translator.paramInfoForDirectCall(target),
|
|
supertype.typeArguments.length,
|
|
);
|
|
|
|
b.comment("Direct call of '$targetMember Initializer'");
|
|
call(target);
|
|
}
|
|
}
|
|
|
|
@override
|
|
w.ValueType visitVariableGet(VariableGet node, w.ValueType expectedType) {
|
|
final capture = closures.captures[node.variable];
|
|
if (capture == null) {
|
|
return super.visitVariableGet(node, expectedType);
|
|
}
|
|
|
|
// If the parameter was captured in the initializer we can load it from
|
|
// initializer context. But if it will be captured in body, then we use
|
|
// normal parameter here.
|
|
if (capture.isInInitializer) {
|
|
assert(capture.isInInitializer);
|
|
return super.visitVariableGet(node, expectedType);
|
|
}
|
|
|
|
// Even though the parameter is captured, it's only captured in the body and
|
|
// the body will put it in a newly allocated context. The initializer uses
|
|
// the normal variable.
|
|
final local = locals[node.variable]!;
|
|
b.local_get(local);
|
|
return local.type;
|
|
}
|
|
|
|
@override
|
|
w.ValueType visitVariableSet(VariableSet node, w.ValueType expectedType) {
|
|
final capture = closures.captures[node.variable];
|
|
if (capture == null) {
|
|
return super.visitVariableSet(node, expectedType);
|
|
}
|
|
|
|
// If the parameter was captured in the initializer we can load it from
|
|
// initializer context. But if it will be captured in body, then we use
|
|
// normal parameter here.
|
|
if (capture.isInInitializer) {
|
|
return super.visitVariableSet(node, expectedType);
|
|
}
|
|
|
|
// Even though the parameter is captured, it's only captured in the body and
|
|
// the body will put it in a newly allocated context. The initializer uses
|
|
// the normal variable.
|
|
final local = locals[node.variable]!;
|
|
translateExpression(node.value, local.type);
|
|
if (expectedType == voidMarker) {
|
|
b.local_set(local);
|
|
return voidMarker;
|
|
}
|
|
b.local_tee(local);
|
|
return local.type;
|
|
}
|
|
}
|
|
|
|
class ConstructorAllocatorCodeGenerator extends ConstructorCodeGeneratorBase {
|
|
ConstructorAllocatorCodeGenerator(
|
|
super.translator,
|
|
super.functionType,
|
|
super.member,
|
|
);
|
|
|
|
@override
|
|
void generateInternal() {
|
|
// Closures are built when constructor functions are added to worklist.
|
|
closures = translator.constructorClosures[member.reference]!;
|
|
|
|
final source = member.enclosingComponent!.uriToSource[member.fileUri]!;
|
|
setSourceMapSourceAndFileOffset(source, member.fileOffset);
|
|
|
|
generateConstructorAllocator();
|
|
}
|
|
|
|
// Generates a function for allocating an object. This calls the separate
|
|
// initializer list and constructor body methods, and allocates a struct for
|
|
// the object.
|
|
void generateConstructorAllocator() {
|
|
int parameterOffset = 0;
|
|
final constructorInfo = translator.getConstructorInfo(member);
|
|
_setupConstructorParameters(
|
|
member.enclosingClass.typeParameters,
|
|
constructorInfo.allParameters,
|
|
parameterOffset,
|
|
);
|
|
|
|
w.FunctionType initializerMethodType = translator.signatureForDirectCall(
|
|
member.initializerReference,
|
|
);
|
|
final info = translator.classInfo[member.enclosingClass]!;
|
|
final List<w.FieldType> fieldTypes = info.struct.fields
|
|
.sublist(FieldIndex.objectFieldBase)
|
|
.toList();
|
|
final bodyCallArgTypes = initializerMethodType.outputs.sublist(
|
|
0,
|
|
initializerMethodType.outputs.length - fieldTypes.length,
|
|
);
|
|
|
|
final initializerArgs = _getConstructorArgumentLocals(
|
|
constructorInfo.initializerTypeParameters,
|
|
constructorInfo.initializerParameters,
|
|
);
|
|
|
|
final bodyCallArgsReversed = <w.Local>[];
|
|
if (bodyCallArgTypes.isEmpty) {
|
|
// Common situation: Initializers don't capture constructor parameters,
|
|
// initializers don't modify constructor parameters, constructor
|
|
// parameters end up in fields
|
|
// => No arguments to the body function.
|
|
b.comment('Allocate wasm struct optimized');
|
|
b.pushObjectHeaderFields(translator, info);
|
|
b.comment('Calling $member initializer function');
|
|
for (final local in initializerArgs) {
|
|
b.local_get(local);
|
|
}
|
|
call(member.initializerReference);
|
|
b.struct_new(info.struct);
|
|
} else {
|
|
b.comment('Calling $member initializer function');
|
|
for (final local in initializerArgs) {
|
|
b.local_get(local);
|
|
}
|
|
call(member.initializerReference);
|
|
|
|
b.comment('Pop all field values to locals');
|
|
final fieldValuesReversed = <w.Local>[];
|
|
for (final field in fieldTypes.reversed) {
|
|
final local = addLocal(field.type.unpacked);
|
|
fieldValuesReversed.add(local);
|
|
b.local_set(local);
|
|
}
|
|
|
|
b.comment('Pop body and super/redirect body args to locals');
|
|
for (final argType in bodyCallArgTypes.reversed) {
|
|
final local = addLocal(argType);
|
|
b.local_set(local);
|
|
bodyCallArgsReversed.add(local);
|
|
}
|
|
|
|
b.comment('Allocate wasm struct');
|
|
b.pushObjectHeaderFields(translator, info);
|
|
for (final local in fieldValuesReversed.reversed) {
|
|
b.local_get(local);
|
|
}
|
|
b.struct_new(info.struct);
|
|
}
|
|
|
|
// Mark the class as allocated now, which enqueues those methods of the
|
|
// class that could be targeted by already emitted instance calls.
|
|
translator.functions.recordClassAllocation(info.classId);
|
|
|
|
b.comment('Push receiver');
|
|
final receiverVar = addLocal(info.nonNullableType);
|
|
b.local_tee(receiverVar);
|
|
b.comment('Push constructor body and super arguments');
|
|
for (final arg in bodyCallArgsReversed.reversed) {
|
|
b.local_get(arg);
|
|
}
|
|
|
|
b.comment("Direct call of $member Constructor Body");
|
|
call(member.constructorBodyReference);
|
|
|
|
b.local_get(receiverVar);
|
|
b.end();
|
|
}
|
|
}
|
|
|
|
class ConstructorBodyCodeGenerator extends ConstructorCodeGeneratorBase {
|
|
ConstructorBodyCodeGenerator(
|
|
super.translator,
|
|
super.functionType,
|
|
super.member,
|
|
);
|
|
|
|
@override
|
|
void generateInternal() {
|
|
// Closures are built when constructor functions are added to worklist.
|
|
closures = translator.constructorClosures[member.reference]!;
|
|
|
|
final source = member.enclosingComponent!.uriToSource[member.fileUri]!;
|
|
setSourceMapSourceAndFileOffset(source, member.fileOffset);
|
|
|
|
if (member.isExternal) {
|
|
// Currently all external constructors are throwing NSM in the initializer
|
|
// function (see [ConstructorInitializerCodeGenerator.generateInternal]).
|
|
// So the body function should be unreachable.
|
|
b.unreachable();
|
|
b.end();
|
|
return;
|
|
}
|
|
|
|
generateConstructorBody();
|
|
}
|
|
|
|
// Generates a function for a constructor's body, where the allocated struct
|
|
// object is passed to this function.
|
|
void generateConstructorBody() {
|
|
final parameterOffset = _setupConstructorBodyParametersAndContexts();
|
|
|
|
// Call super class' constructor body, or redirected constructor
|
|
for (final initializer in member.initializers) {
|
|
if (initializer is SuperInitializer ||
|
|
initializer is RedirectingInitializer) {
|
|
final target = initializer is SuperInitializer
|
|
? initializer.target
|
|
: (initializer as RedirectingInitializer).target;
|
|
if (target.enclosingClass.supertype == null) break;
|
|
|
|
w.Local object = thisLocal!;
|
|
b.local_get(object);
|
|
for (w.Local local in paramLocals.sublist(parameterOffset)) {
|
|
b.local_get(local);
|
|
}
|
|
call(target.constructorBodyReference);
|
|
break;
|
|
}
|
|
}
|
|
|
|
Statement? body = member.function.body;
|
|
if (body != null) {
|
|
translateStatement(body);
|
|
}
|
|
|
|
b.end();
|
|
}
|
|
|
|
int _setupConstructorBodyParametersAndContexts() {
|
|
// Setup [thisLocal] / [preciseThisLocal].
|
|
int parameterOffset = _initializeThis(member.constructorBodyReference);
|
|
assert(parameterOffset == 1);
|
|
|
|
// Setup context variables
|
|
// Redirecting constructors don't have a real body and don't need the
|
|
// context in the body.
|
|
final isRedirectInitializer =
|
|
member.initializers.lastOrNull is RedirectingInitializer;
|
|
if (!isRedirectInitializer) {
|
|
if (closures.contexts[member] case var context?) {
|
|
assert(!context.isEmpty);
|
|
_initializeContextLocals(member, contextParamIndex: parameterOffset);
|
|
parameterOffset++;
|
|
}
|
|
}
|
|
|
|
final info = translator.getConstructorInfo(member);
|
|
parameterOffset = _setupConstructorParameters(
|
|
const [],
|
|
info.bodyParameters,
|
|
parameterOffset,
|
|
);
|
|
|
|
// Populate type parameters and normal parameters from fields if we emitted
|
|
// them from the body function signature.
|
|
final classInfo = translator.classInfo[member.enclosingClass]!;
|
|
for (final typeParam in member.enclosingClass.typeParameters) {
|
|
if (!typeLocals.containsKey(typeParam)) {
|
|
final fieldIndex = translator.typeParameterIndex[typeParam]!;
|
|
final typeType =
|
|
translator.classInfo[translator.typeClass]!.nonNullableType;
|
|
final local = addLocal(typeType);
|
|
b.local_get(preciseThisLocal!);
|
|
b.struct_get(classInfo.struct, fieldIndex);
|
|
b.local_set(local);
|
|
typeLocals[typeParam] = local;
|
|
}
|
|
}
|
|
info.parameterToField.forEach((variable, field) {
|
|
if (!locals.containsKey(variable)) {
|
|
final fieldIndex = translator.fieldIndex[field]!;
|
|
final wasmType = translator.translateTypeOfField(field);
|
|
final local = addLocal(wasmType);
|
|
b.local_get(preciseThisLocal!);
|
|
b.struct_get(classInfo.struct, fieldIndex);
|
|
b.local_set(local);
|
|
locals[variable] = local;
|
|
}
|
|
});
|
|
|
|
allocateContext(member.function);
|
|
captureParameters(member.function);
|
|
|
|
return parameterOffset;
|
|
}
|
|
}
|
|
|
|
class StaticFieldInitializerCodeGenerator extends AstCodeGenerator {
|
|
final Field field;
|
|
|
|
StaticFieldInitializerCodeGenerator(
|
|
Translator translator,
|
|
w.FunctionType functionType,
|
|
this.field,
|
|
) : super(translator, functionType, field);
|
|
|
|
@override
|
|
void generateInternal() {
|
|
final source = field.enclosingComponent!.uriToSource[field.fileUri]!;
|
|
setSourceMapSourceAndFileOffset(source, field.fileOffset);
|
|
|
|
// Static field initializer function
|
|
closures = translator.getClosures(field);
|
|
|
|
final globalDefinition = translator.dartGlobals.getDefinitionForStaticField(
|
|
field,
|
|
);
|
|
final flag = globalDefinition.initializedFlag;
|
|
|
|
final local = b.addLocal(globalDefinition.type);
|
|
globalDefinition.write(translator, b, (b) {
|
|
translateExpression(field.initializer!, local.type);
|
|
b.local_tee(local);
|
|
});
|
|
b.local_get(local);
|
|
translator.convertType(b, local.type, outputs.single);
|
|
if (flag != null) {
|
|
b.i32_const(1);
|
|
translator.globals.writeGlobal(b, flag);
|
|
}
|
|
b.end();
|
|
}
|
|
}
|
|
|
|
/// Will eagerly initialize a static field as part of the module's start
|
|
/// function.
|
|
class EagerStaticFieldInitializerCodeGenerator extends AstCodeGenerator {
|
|
final Field field;
|
|
final w.Global global;
|
|
|
|
EagerStaticFieldInitializerCodeGenerator(
|
|
Translator translator,
|
|
this.field,
|
|
this.global,
|
|
) : super(translator, w.FunctionType([], []), field);
|
|
|
|
@override
|
|
void generateInternal() {
|
|
final source = field.enclosingComponent!.uriToSource[field.fileUri]!;
|
|
setSourceMapSourceAndFileOffset(source, field.fileOffset);
|
|
|
|
translateExpression(field.initializer!, global.type.type);
|
|
translator.globals.writeGlobal(b, global);
|
|
}
|
|
}
|
|
|
|
class StaticFieldImplicitAccessorCodeGenerator extends AstCodeGenerator {
|
|
final Field field;
|
|
final bool isImplicitGetter;
|
|
|
|
StaticFieldImplicitAccessorCodeGenerator(
|
|
Translator translator,
|
|
w.FunctionType functionType,
|
|
this.field,
|
|
this.isImplicitGetter,
|
|
) : super(translator, functionType, field);
|
|
|
|
@override
|
|
void generateInternal() {
|
|
final globalDefinition = translator.dartGlobals.getDefinitionForStaticField(
|
|
field,
|
|
);
|
|
if (isImplicitGetter) {
|
|
final initFunction = translator.functions.getExistingFunction(
|
|
field.staticFieldInitializer,
|
|
);
|
|
_generateGetter(globalDefinition, initFunction);
|
|
} else {
|
|
_generateSetter(globalDefinition);
|
|
}
|
|
b.end();
|
|
}
|
|
|
|
void _generateGetter(
|
|
DartGlobalDefinition definition,
|
|
w.BaseFunction? initFunction,
|
|
) {
|
|
final flag = definition.initializedFlag;
|
|
|
|
if (initFunction == null) {
|
|
// Statically initialized
|
|
definition.read(translator, b);
|
|
translator.convertType(b, definition.type, returnType);
|
|
} else {
|
|
if (flag != null) {
|
|
// Explicit initialization flag
|
|
translator.globals.readGlobal(b, flag);
|
|
b.if_(const [], [definition.type]);
|
|
definition.read(translator, b);
|
|
b.else_();
|
|
translator.callFunction(initFunction, b);
|
|
b.end();
|
|
translator.convertType(b, definition.type, returnType);
|
|
} else {
|
|
// Null signals uninitialized
|
|
w.Label block = b.block(const [], [initFunction.type.outputs.single]);
|
|
definition.read(translator, b);
|
|
b.br_on_non_null(block);
|
|
translator.callFunction(initFunction, b);
|
|
b.end();
|
|
translator.convertType(b, initFunction.type.outputs.single, returnType);
|
|
}
|
|
}
|
|
}
|
|
|
|
void _generateSetter(DartGlobalDefinition definition) {
|
|
definition.write(translator, b, (b) {
|
|
b.local_get(paramLocals.single);
|
|
});
|
|
final flag = definition.initializedFlag;
|
|
if (flag != null) {
|
|
b.i32_const(1); // true
|
|
translator.globals.writeGlobal(b, flag);
|
|
}
|
|
}
|
|
}
|
|
|
|
class ImplicitFieldAccessorCodeGenerator extends AstCodeGenerator {
|
|
final Field field;
|
|
final bool isImplicitGetter;
|
|
final bool useUncheckedEntry;
|
|
|
|
ImplicitFieldAccessorCodeGenerator(
|
|
Translator translator,
|
|
w.FunctionType functionType,
|
|
this.field,
|
|
this.isImplicitGetter,
|
|
this.useUncheckedEntry,
|
|
) : super(translator, functionType, field);
|
|
|
|
@override
|
|
void generateInternal() {
|
|
thisLocal = preciseThisLocal = paramLocals[0];
|
|
|
|
// Conceptually not needed for implicit accessors, but currently the code
|
|
// that instantiates types uses closure information to see whether a type
|
|
// parameter was captured (and loads it from context chain) or not (and
|
|
// loads it directly from `this`).
|
|
closures = translator.getClosures(field, findCaptures: false);
|
|
|
|
final source = field.enclosingComponent!.uriToSource[field.fileUri]!;
|
|
setSourceMapSourceAndFileOffset(source, field.fileOffset);
|
|
|
|
// Implicit getter or setter
|
|
w.StructType struct = translator.classInfo[field.enclosingClass!]!.struct;
|
|
w.RefType structType = w.RefType.def(struct, nullable: false);
|
|
int fieldIndex = translator.fieldIndex[field]!;
|
|
w.ValueType fieldType = struct.fields[fieldIndex].type.unpacked;
|
|
|
|
void getThis() {
|
|
w.Local thisLocal = paramLocals[0];
|
|
b.local_get(thisLocal);
|
|
translator.convertType(b, thisLocal.type, structType);
|
|
}
|
|
|
|
if (isImplicitGetter) {
|
|
// Implicit getter
|
|
getThis();
|
|
b.struct_get(struct, fieldIndex);
|
|
translator.convertType(b, fieldType, returnType);
|
|
} else {
|
|
// Implicit setter
|
|
w.Local valueLocal = paramLocals[1];
|
|
getThis();
|
|
|
|
if (translator.needToCheckTypesFor(field) &&
|
|
translator.needToCheckImplicitSetterValue(
|
|
field,
|
|
uncheckedEntry: useUncheckedEntry,
|
|
)) {
|
|
final boxedType = field.type.isPotentiallyNullable
|
|
? translator.topType
|
|
: translator.topTypeNonNullable;
|
|
w.Local operand = valueLocal;
|
|
if (!operand.type.isSubtypeOf(boxedType)) {
|
|
final boxedOperand = addLocal(boxedType);
|
|
b.local_get(operand);
|
|
translator.convertType(b, operand.type, boxedOperand.type);
|
|
b.local_set(boxedOperand);
|
|
operand = boxedOperand;
|
|
}
|
|
b.local_get(operand);
|
|
_generateArgumentTypeCheck(
|
|
field.name.text,
|
|
operand.type as w.RefType,
|
|
field.type,
|
|
);
|
|
}
|
|
|
|
b.local_get(valueLocal);
|
|
translator.convertType(b, valueLocal.type, fieldType);
|
|
b.struct_set(struct, fieldIndex);
|
|
assert(functionType.outputs.isEmpty);
|
|
}
|
|
b.end();
|
|
}
|
|
}
|
|
|
|
class SynchronousLambdaCodeGenerator extends AstCodeGenerator {
|
|
final Lambda lambda;
|
|
|
|
SynchronousLambdaCodeGenerator(Translator translator, this.lambda)
|
|
: super(translator, lambda.callTarget.signature, lambda.enclosingMember);
|
|
|
|
@override
|
|
void generateInternal() {
|
|
closures = lambda.enclosingMemberClosures;
|
|
|
|
setSourceMapSource(lambda.functionNodeSource);
|
|
|
|
assert(lambda.functionNode.asyncMarker != AsyncMarker.Async);
|
|
|
|
setupLambdaParametersAndContexts(lambda);
|
|
|
|
translateStatement(lambda.functionNode.body!);
|
|
_implicitReturn();
|
|
b.end();
|
|
}
|
|
}
|
|
|
|
class UnreachableCodeGenerator extends AstCodeGenerator {
|
|
UnreachableCodeGenerator(super.translator, super.functionType, super.member);
|
|
|
|
@override
|
|
void generateInternal() {
|
|
b.unreachable();
|
|
b.end();
|
|
}
|
|
}
|
|
|
|
class TryBlockFinalizer {
|
|
/// `br` target to run the finalizer
|
|
final w.Label label;
|
|
|
|
/// Whether the last finalizer in the chain should return. When this is
|
|
/// `false` the block won't be used, as the block is for running finalizers
|
|
/// when returning.
|
|
bool mustHandleReturn = false;
|
|
|
|
TryBlockFinalizer(this.label);
|
|
}
|
|
|
|
/// Holds information of a switch statement, to be used when doing a backward
|
|
/// jump to it
|
|
class SwitchBackwardJumpInfo {
|
|
/// Wasm local for the value of the switched expression. For example, in a
|
|
/// `switch` like:
|
|
///
|
|
/// ```
|
|
/// switch (expr) {
|
|
/// ...
|
|
/// }
|
|
/// ```
|
|
///
|
|
/// This local holds the value of `expr`.
|
|
///
|
|
/// This local is updated with a new value when doing backward jumps.
|
|
final w.Local switchValueLocal;
|
|
|
|
/// Label of the `loop` to use when doing backward jumps
|
|
final w.Label loopLabel;
|
|
|
|
/// When compiling a `default` case, label of the `loop` in the case body, to
|
|
/// use when doing backward jumps to the same case.
|
|
w.Label? defaultLoopLabel;
|
|
|
|
SwitchBackwardJumpInfo(this.switchValueLocal, this.loopLabel)
|
|
: defaultLoopLabel = null;
|
|
}
|
|
|
|
/// Info needed to represent a switch statement using a br_table instruction.
|
|
///
|
|
/// This is used for switches on integers and enums (using their indicies). We
|
|
/// map each option to an index in the jump table and then jump directly to the
|
|
/// target case. This is much faster than iteratively comparing each cases's
|
|
/// expression to the switch expression.
|
|
///
|
|
/// Sometimes switches over ranges of ints are sparse enough that a table would
|
|
/// bloat the code compared to the iterative comparison.
|
|
class BrTableInfo {
|
|
// At least 50% of the [min, max] must be occupied for us to use `br_table`.
|
|
static const double _minimumTableOccupancy = 0.5;
|
|
// This is the maximum size where it's always worth it to use a br_table.
|
|
// If the br_table is bigger than this then we start to check sparseness.
|
|
// Below this point we always accept the potential code size hit.
|
|
static const int _maxSparseSize = 50;
|
|
|
|
int get rangeSize => _rangeSize(minValue, maxValue);
|
|
final int minValue;
|
|
final int maxValue;
|
|
final void Function(w.Local switchExprLocal) _brTableExpr;
|
|
final Map<int, SwitchCase> caseMap;
|
|
|
|
BrTableInfo._(this.minValue, this.maxValue, this.caseMap, this._brTableExpr)
|
|
: assert(!_isTooSparse(minValue, maxValue, caseMap));
|
|
|
|
/// Heuristically validate whether the provided table would be too sparse and
|
|
/// if so return null. Otherwise return the expected table.
|
|
static BrTableInfo? build(
|
|
Map<int, SwitchCase> caseMap,
|
|
void Function(w.Local switchExprLocal) brTableExpr, {
|
|
required int minValue,
|
|
required int maxValue,
|
|
}) {
|
|
// Validate the table density and size is worth putting into a br_table.
|
|
if (_isTooSparse(minValue, maxValue, caseMap)) return null;
|
|
|
|
return BrTableInfo._(minValue, maxValue, caseMap, brTableExpr);
|
|
}
|
|
|
|
static bool _isTooSparse(int min, int max, Map<int, SwitchCase> caseMap) {
|
|
int rangeSize = _rangeSize(min, max);
|
|
return (caseMap.length / rangeSize) < _minimumTableOccupancy &&
|
|
rangeSize > _maxSparseSize;
|
|
}
|
|
|
|
static int _rangeSize(int min, int max) => max - min + 1;
|
|
|
|
void emitBrTableExpr(w.InstructionsBuilder b, w.Local switchExprLocal) {
|
|
_brTableExpr(switchExprLocal);
|
|
// Normalize on 0.
|
|
if (minValue != 0) {
|
|
b.i64_const(minValue);
|
|
b.i64_sub();
|
|
}
|
|
// Now that we've normalized on 0 it should be safe to switch to i32.
|
|
b.i32_wrap_i64();
|
|
}
|
|
}
|
|
|
|
class SwitchInfo {
|
|
/// Non-nullable Wasm type of the `switch` expression. Used when the
|
|
/// expression is not nullable, and after the null check.
|
|
late final w.ValueType nullableType;
|
|
|
|
/// Nullable Wasm type of the `switch` expression. Only used when the
|
|
/// expression is nullable.
|
|
late final w.ValueType nonNullableType;
|
|
|
|
/// Generates code that will br on [successLabel] if [switchExprLocal] has the
|
|
/// correct type for the case checks on this switch. Only set for switches
|
|
/// where the switch expression is dynamic.
|
|
void Function(w.Local switchExprLocal, w.Label successLabel)?
|
|
dynamicTypeGuard;
|
|
|
|
/// Generates code that compares value of a `case` expression with the
|
|
/// `switch` expression's value. Calls [pushCaseExpr] once.
|
|
late final void Function(
|
|
w.Local switchExprLocal,
|
|
w.ValueType Function() pushCaseExpr,
|
|
)
|
|
compare;
|
|
|
|
/// The `default: ...` case, if exists.
|
|
late final SwitchCase? defaultCase;
|
|
|
|
/// The `null: ...` case, if exists.
|
|
late final SwitchCase? nullCase;
|
|
|
|
/// Info needed to compile this switch statement into a wasm br_table. If null
|
|
/// this switch statement should not use a br_table and should use comparison
|
|
/// based case matching instead.
|
|
BrTableInfo? brTable;
|
|
|
|
SwitchInfo(AstCodeGenerator codeGen, SwitchStatement node) {
|
|
final translator = codeGen.translator;
|
|
|
|
final switchExprType = codeGen.dartTypeOf(node.expression);
|
|
|
|
final switchExprClass = translator.classForType(switchExprType);
|
|
|
|
bool check<L extends Expression, C extends Constant>() => node.cases
|
|
.expand((c) => c.expressions)
|
|
.every(
|
|
(e) =>
|
|
e is L ||
|
|
e is NullLiteral ||
|
|
(e is ConstantExpression &&
|
|
((e.constant is C &&
|
|
(translator.hierarchy.isSubInterfaceOf(
|
|
translator.classForType(codeGen.dartTypeOf(e)),
|
|
switchExprClass,
|
|
))) ||
|
|
e.constant is NullConstant)),
|
|
);
|
|
|
|
// Type objects should be compared using `==` rather than identity even
|
|
// though the specification is not very clear about it. In language versions
|
|
// >=3.0 CFE would desugar such switches to a sequence of `if` statements
|
|
// using `==`, but for language versions <3.0 it would simply emit
|
|
// `SwitchStatement` and expect back-end to handle types specially if
|
|
// required. See #60375 for more details.
|
|
bool canInvokeTypeEquality() =>
|
|
translator.typeEnvironment.isSubtypeOf(
|
|
switchExprType,
|
|
translator.coreTypes.typeNullableRawType,
|
|
) ||
|
|
node.cases
|
|
.expand((c) => c.expressions)
|
|
.any(
|
|
(e) => translator.typeEnvironment.isSubtypeOf(
|
|
codeGen.dartTypeOf(e),
|
|
translator.coreTypes.typeNonNullableRawType,
|
|
),
|
|
);
|
|
|
|
if (node.cases.every(
|
|
(c) =>
|
|
c.expressions.isEmpty && c.isDefault ||
|
|
c.expressions.every(
|
|
(e) =>
|
|
e is NullLiteral ||
|
|
e is ConstantExpression && e.constant is NullConstant,
|
|
),
|
|
)) {
|
|
// default-only switch
|
|
nonNullableType = w.RefType.eq(nullable: false);
|
|
nullableType = w.RefType.eq(nullable: true);
|
|
compare = (switchExprLocal, pushCaseExpr) =>
|
|
throw "Comparison in default-only switch";
|
|
} else if (canInvokeTypeEquality()) {
|
|
nonNullableType = translator.runtimeTypeType;
|
|
nullableType = translator.runtimeTypeTypeNullable;
|
|
compare = (switchExprLocal, pushCaseExpr) {
|
|
// Virtual call to `Type.==`.
|
|
codeGen._virtualCall(
|
|
node,
|
|
translator.coreTypes.objectEquals,
|
|
_VirtualCallKind.Call,
|
|
(functionType) {
|
|
codeGen.b.local_get(switchExprLocal);
|
|
},
|
|
(functionType, paramInfo) {
|
|
pushCaseExpr();
|
|
},
|
|
useUncheckedEntry: false,
|
|
);
|
|
};
|
|
} else if (switchExprType is DynamicType) {
|
|
// Per spec, compare with `<case expr> == <switch expr>`. For performance,
|
|
// if we know that the cases all have the same type, we call the case
|
|
// expression's `==` implementation directly (instead of virtually calling
|
|
// `Object.==`).
|
|
//
|
|
// Note: this could be improved by directly calling a different `==` in
|
|
// each of the cases based on the case value. For now we only directly
|
|
// call a `==` if all of the cases have a compatible type.
|
|
nonNullableType = translator.topTypeNonNullable;
|
|
nullableType = translator.topType;
|
|
final Member equalsMember;
|
|
if (check<BoolLiteral, BoolConstant>()) {
|
|
equalsMember = translator.boxedBoolEquals;
|
|
} else if (check<IntLiteral, IntConstant>()) {
|
|
equalsMember = translator.boxedIntEquals;
|
|
} else if (check<StringLiteral, StringConstant>()) {
|
|
equalsMember = translator.stringImplEquals;
|
|
} else {
|
|
compare = (switchExprLocal, pushCaseExpr) {
|
|
// Virtual call to `Object.==`.
|
|
codeGen._virtualCall(
|
|
node,
|
|
codeGen.translator.coreTypes.objectEquals,
|
|
_VirtualCallKind.Call,
|
|
(functionType) {
|
|
codeGen.b.local_get(switchExprLocal);
|
|
},
|
|
(functionType, paramInfo) {
|
|
pushCaseExpr();
|
|
},
|
|
useUncheckedEntry: false,
|
|
);
|
|
};
|
|
_initializeSpecialCases(node);
|
|
return;
|
|
}
|
|
|
|
final equalsMemberSignature = translator.signatureForDirectCall(
|
|
equalsMember.reference,
|
|
);
|
|
|
|
// Per spec, `==` can't have type, or extra (optional) positional and
|
|
// named arguments. So we don't have to check `ParamInfo` for it and
|
|
// add missing optional parameters.
|
|
assert(equalsMemberSignature.inputs.length == 2);
|
|
|
|
dynamicTypeGuard = (switchExprLocal, successLabel) {
|
|
codeGen.b.local_get(switchExprLocal);
|
|
codeGen.b.br_on_cast(
|
|
successLabel,
|
|
switchExprLocal.type as w.RefType,
|
|
equalsMemberSignature.inputs[0].withNullability(
|
|
switchExprLocal.type.nullable,
|
|
)
|
|
as w.RefType,
|
|
);
|
|
codeGen.b.drop();
|
|
};
|
|
|
|
compare = (switchExprLocal, pushCaseExpr) {
|
|
final caseExprType = pushCaseExpr();
|
|
translator.convertType(
|
|
codeGen.b,
|
|
caseExprType,
|
|
equalsMemberSignature.inputs[0],
|
|
);
|
|
|
|
codeGen.b.local_get(switchExprLocal);
|
|
translator.convertType(
|
|
codeGen.b,
|
|
switchExprLocal.type,
|
|
equalsMemberSignature.inputs[1],
|
|
);
|
|
|
|
codeGen.call(equalsMember.reference);
|
|
};
|
|
} else if (check<BoolLiteral, BoolConstant>()) {
|
|
// bool switch
|
|
nonNullableType = w.NumType.i32;
|
|
nullableType =
|
|
translator.classInfo[translator.boxedBoolClass]!.nullableType;
|
|
compare = (switchExprLocal, pushCaseExpr) {
|
|
codeGen.b.local_get(switchExprLocal);
|
|
pushCaseExpr();
|
|
codeGen.b.i32_eq();
|
|
};
|
|
} else if (check<IntLiteral, IntConstant>()) {
|
|
// int switch
|
|
nonNullableType = w.NumType.i64;
|
|
nullableType =
|
|
translator.classInfo[translator.boxedIntClass]!.nullableType;
|
|
|
|
// Calculate the range covered by the cases and create the jump table.
|
|
int? minValue;
|
|
int? maxValue;
|
|
Map<int, SwitchCase> caseMap = {};
|
|
for (final c in node.cases) {
|
|
for (final e in c.expressions) {
|
|
if (e is NullLiteral ||
|
|
(e is ConstantExpression && e.constant is NullConstant)) {
|
|
// Null already handled above.
|
|
continue;
|
|
}
|
|
|
|
final value = e is IntLiteral
|
|
? e.value
|
|
: ((e as ConstantExpression).constant as IntConstant).value;
|
|
|
|
caseMap[value] = c;
|
|
if (minValue == null || value < minValue) minValue = value;
|
|
if (maxValue == null || value > maxValue) maxValue = value;
|
|
}
|
|
}
|
|
if (maxValue != null) {
|
|
brTable = BrTableInfo.build(
|
|
minValue: minValue!,
|
|
maxValue: maxValue,
|
|
caseMap,
|
|
(switchExprLocal) {
|
|
codeGen.b.local_get(switchExprLocal);
|
|
},
|
|
);
|
|
}
|
|
|
|
// Provide a compare as a fallback in case the range is too sparse.
|
|
compare = (switchExprLocal, pushCaseExpr) {
|
|
codeGen.b.local_get(switchExprLocal);
|
|
pushCaseExpr();
|
|
codeGen.b.i64_eq();
|
|
};
|
|
} else if (check<StringLiteral, StringConstant>()) {
|
|
// String switch
|
|
nonNullableType = translator.stringType;
|
|
nullableType = translator.stringTypeNullable;
|
|
compare = (switchExprLocal, pushCaseExpr) {
|
|
codeGen.b.local_get(switchExprLocal);
|
|
pushCaseExpr();
|
|
codeGen.call(translator.stringImplEquals.reference);
|
|
};
|
|
} else if (switchExprClass.isEnum) {
|
|
// If this is an applicable switch over enums, create a jump table.
|
|
bool isValid = true;
|
|
var caseMap = <int, SwitchCase>{};
|
|
int? minIndex;
|
|
int? maxIndex;
|
|
outer:
|
|
for (final c in node.cases) {
|
|
for (final e in c.expressions) {
|
|
if (e is NullLiteral ||
|
|
(e is ConstantExpression && e.constant is NullConstant)) {
|
|
// Null already handled above.
|
|
continue;
|
|
}
|
|
if (e is! ConstantExpression) {
|
|
isValid = false;
|
|
break outer;
|
|
}
|
|
final constant = e.constant;
|
|
if (constant is! InstanceConstant) {
|
|
isValid = false;
|
|
break outer;
|
|
}
|
|
if (constant.classNode != switchExprClass) {
|
|
isValid = false;
|
|
break outer;
|
|
}
|
|
final enumIndex =
|
|
(constant.fieldValues[translator.enumIndexField.fieldReference]
|
|
as IntConstant)
|
|
.value;
|
|
caseMap[enumIndex] = c;
|
|
if (maxIndex == null || enumIndex > maxIndex) maxIndex = enumIndex;
|
|
if (minIndex == null || enumIndex < minIndex) minIndex = enumIndex;
|
|
}
|
|
}
|
|
|
|
if (isValid && maxIndex != null) {
|
|
brTable = BrTableInfo.build(
|
|
minValue: minIndex!,
|
|
maxValue: maxIndex,
|
|
caseMap,
|
|
(switchExprLocal) {
|
|
codeGen.b.local_get(switchExprLocal);
|
|
codeGen.call(translator.enumIndexField.getterReference);
|
|
},
|
|
);
|
|
}
|
|
|
|
if (brTable == null) {
|
|
// Object identity switch
|
|
nonNullableType = translator.topTypeNonNullable;
|
|
nullableType = translator.topType;
|
|
} else {
|
|
nonNullableType =
|
|
translator.classInfo[switchExprClass]!.nonNullableType;
|
|
nullableType = translator.classInfo[switchExprClass]!.nullableType;
|
|
}
|
|
|
|
// Set compare anyway for state machine handling
|
|
compare = (switchExprLocal, pushCaseExpr) {
|
|
codeGen.b.local_get(switchExprLocal);
|
|
pushCaseExpr();
|
|
codeGen.call(translator.coreTypes.identicalProcedure.reference);
|
|
};
|
|
} else {
|
|
// Object identity switch
|
|
nonNullableType = translator.topTypeNonNullable;
|
|
nullableType = translator.topType;
|
|
compare = (switchExprLocal, pushCaseExpr) {
|
|
codeGen.b.local_get(switchExprLocal);
|
|
pushCaseExpr();
|
|
codeGen.call(translator.coreTypes.identicalProcedure.reference);
|
|
};
|
|
}
|
|
|
|
_initializeSpecialCases(node);
|
|
}
|
|
|
|
void _initializeSpecialCases(SwitchStatement node) {
|
|
// Special cases
|
|
defaultCase = node.cases.cast<SwitchCase?>().firstWhere(
|
|
(c) => c!.isDefault,
|
|
orElse: () => null,
|
|
);
|
|
|
|
nullCase = node.cases.cast<SwitchCase?>().firstWhere(
|
|
(c) => c!.expressions.any(
|
|
(e) =>
|
|
e is NullLiteral ||
|
|
e is ConstantExpression && e.constant is NullConstant,
|
|
),
|
|
orElse: () => null,
|
|
);
|
|
}
|
|
}
|
|
|
|
enum _VirtualCallKind {
|
|
Get,
|
|
Set,
|
|
Call;
|
|
|
|
@override
|
|
String toString() {
|
|
return switch (this) {
|
|
_VirtualCallKind.Get => "get",
|
|
_VirtualCallKind.Set => "set",
|
|
_VirtualCallKind.Call => "call",
|
|
};
|
|
}
|
|
|
|
bool get isGetter => this == _VirtualCallKind.Get;
|
|
|
|
bool get isSetter => this == _VirtualCallKind.Set;
|
|
}
|
|
|
|
extension MacroAssembler on w.InstructionsBuilder {
|
|
/// If the given [outputs] of a call contain bottom types then we will emit an
|
|
/// `unreachable` instruction.
|
|
///
|
|
/// This can help wasm compilers / wasm runtimes to optimize things more as
|
|
/// they know control flow has ended here.
|
|
List<w.ValueType> emitUnreachableIfNoResult(List<w.ValueType> outputs) {
|
|
for (int i = 0; i < outputs.length; ++i) {
|
|
final output = outputs[i];
|
|
if (output is w.RefType &&
|
|
output.heapType == w.HeapType.none &&
|
|
!output.nullable) {
|
|
unreachable();
|
|
break;
|
|
}
|
|
}
|
|
return outputs;
|
|
}
|
|
|
|
/// Switches on the [pushBranchExpression] and calls [handleCase] for each
|
|
/// case or [handleDefault] for the default case.
|
|
///
|
|
/// Assumes that [handleCase] and [handleDefault] will push [outputs] on the
|
|
/// stack.
|
|
///
|
|
/// Leaves [outputs] on the stack.
|
|
void emitDenseTableBranch(
|
|
List<w.ValueType> outputs,
|
|
int n,
|
|
void Function() pushBranchExpression,
|
|
void Function(int) handleCase,
|
|
void Function() handleDefault,
|
|
) {
|
|
final done = block([], outputs);
|
|
final defaultCase = block();
|
|
|
|
final labelStack = <w.Label>[];
|
|
for (int i = 0; i < n; ++i) {
|
|
labelStack.add(block());
|
|
}
|
|
pushBranchExpression();
|
|
br_table(labelStack, defaultCase);
|
|
for (int i = n - 1; i >= 0; --i) {
|
|
end();
|
|
handleCase(i);
|
|
br(done);
|
|
}
|
|
|
|
end(); // defaultCase
|
|
handleDefault();
|
|
end(); // done
|
|
}
|
|
|
|
void incrementingLoop({
|
|
required void Function() pushStart,
|
|
required void Function() pushLimit,
|
|
required void Function(w.Local) genBody,
|
|
int step = 1,
|
|
}) {
|
|
final endLoop = block();
|
|
final limitVar = addLocal(w.NumType.i32);
|
|
final loopVar = addLocal(w.NumType.i32);
|
|
pushLimit();
|
|
local_set(limitVar);
|
|
pushStart();
|
|
local_set(loopVar);
|
|
|
|
final loopLabel = loop();
|
|
local_get(loopVar);
|
|
local_get(limitVar);
|
|
i32_ge_u();
|
|
br_if(endLoop);
|
|
|
|
genBody(loopVar);
|
|
local_get(loopVar);
|
|
i32_const(step);
|
|
i32_add();
|
|
local_set(loopVar);
|
|
br(loopLabel);
|
|
end();
|
|
end();
|
|
}
|
|
|
|
/// [ref Array] [ref Array] -> [ref Array]
|
|
///
|
|
/// Takes the two arrays on the stack and concatenates them into a single
|
|
/// array. They both must have the same type provided as [arrayRefType].
|
|
/// Uses [pushDefaultElement] as the filler element that holds space in the
|
|
/// array until values are copied over.
|
|
void concatenateWasmArrays(
|
|
w.ArrayType arrayType, {
|
|
required void Function(
|
|
w.InstructionsBuilder b,
|
|
w.Local oldArray,
|
|
w.Local newArray,
|
|
)
|
|
pushDefaultElement,
|
|
}) {
|
|
final arrayRefType = w.RefType(arrayType, nullable: false);
|
|
final newArray = addLocal(arrayRefType);
|
|
final oldArray = addLocal(arrayRefType);
|
|
final newArrayLen = addLocal(w.NumType.i32);
|
|
final oldArrayLen = addLocal(w.NumType.i32);
|
|
final joinedArray = addLocal(arrayRefType);
|
|
|
|
local_set(newArray);
|
|
local_set(oldArray);
|
|
pushDefaultElement(this, oldArray, newArray);
|
|
local_get(newArray);
|
|
array_len();
|
|
local_set(newArrayLen);
|
|
local_get(oldArray);
|
|
array_len();
|
|
local_tee(oldArrayLen);
|
|
local_get(newArrayLen);
|
|
i32_add();
|
|
array_new(arrayType);
|
|
local_tee(joinedArray);
|
|
i32_const(0);
|
|
local_get(oldArray);
|
|
i32_const(0);
|
|
local_get(oldArrayLen);
|
|
array_copy(arrayType, arrayType);
|
|
local_get(joinedArray);
|
|
local_get(oldArrayLen);
|
|
local_get(newArray);
|
|
i32_const(0);
|
|
local_get(newArrayLen);
|
|
array_copy(arrayType, arrayType);
|
|
local_get(joinedArray);
|
|
end();
|
|
}
|
|
|
|
/// `[i32] -> [i32]`
|
|
///
|
|
/// Consumes a `i32` class ID, leaves an `i32` as `bool` for whether
|
|
/// the class ID is in the given list of ranges.
|
|
void emitClassIdRangeCheck(List<Range> ranges) {
|
|
final rangeValues = ranges.map((r) => (range: r, value: null)).toList();
|
|
classIdSearch<Null>(
|
|
rangeValues,
|
|
[w.NumType.i32],
|
|
(_) {
|
|
i32_const(1);
|
|
},
|
|
() {
|
|
i32_const(0);
|
|
},
|
|
);
|
|
}
|
|
|
|
/// `[i32] -> [outputs]`
|
|
///
|
|
/// Consumes a `i32` class ID and checks whether it lies within one of the
|
|
/// given [ranges] using a linear or binary search.
|
|
///
|
|
/// The [ranges] have to be non-empty, non-overlapping and sorted.
|
|
///
|
|
/// Calls [match] on a matching value and [miss] if provided and no match was
|
|
/// found.
|
|
///
|
|
/// Assumes [match] and [miss] leave [outputs] on the stack.
|
|
void classIdSearch<T>(
|
|
List<({Range range, T value})> ranges,
|
|
List<w.ValueType> outputs,
|
|
void Function(T) match,
|
|
void Function()? miss,
|
|
) {
|
|
final bool linearSearch = ranges.length <= 3;
|
|
if (traceEnabled) {
|
|
comment('Class id ${linearSearch ? 'linear' : 'binary'} search:');
|
|
for (final (:range, :value) in ranges) {
|
|
comment(' - $range -> $value');
|
|
}
|
|
}
|
|
if (linearSearch) {
|
|
_linearClassIdSearch<T>(ranges, outputs, match, miss);
|
|
} else {
|
|
_binaryClassIdSearch<T>(ranges, outputs, match, miss);
|
|
}
|
|
}
|
|
|
|
void _binaryClassIdSearch<T>(
|
|
List<({Range range, T value})> ranges,
|
|
List<w.ValueType> outputs,
|
|
void Function(T) match,
|
|
void Function()? miss,
|
|
) {
|
|
assert(ranges.isNotEmpty || miss != null);
|
|
if (miss != null && ranges.isEmpty) {
|
|
drop();
|
|
miss();
|
|
return;
|
|
}
|
|
|
|
w.Local classId = addLocal(w.NumType.i32);
|
|
local_set(classId);
|
|
|
|
final done = block([], outputs);
|
|
final fail = block();
|
|
void search(int left, int right, Range searchArea) {
|
|
if (left == right) {
|
|
final entry = ranges[left];
|
|
final range = entry.range;
|
|
assert(searchArea.containsRange(range));
|
|
if (miss == null || range.containsRange(searchArea)) {
|
|
match(entry.value);
|
|
br(done);
|
|
return;
|
|
}
|
|
local_get(classId);
|
|
if (range.length == 1) {
|
|
i32_const(range.start);
|
|
i32_eq();
|
|
} else {
|
|
if (searchArea.end <= range.end) {
|
|
i32_const(range.start);
|
|
i32_ge_u();
|
|
} else if (range.start <= searchArea.start) {
|
|
i32_const(range.end);
|
|
i32_le_u();
|
|
} else {
|
|
i32_const(range.start);
|
|
i32_sub();
|
|
i32_const(range.length);
|
|
i32_lt_u();
|
|
}
|
|
}
|
|
if_();
|
|
match(entry.value);
|
|
br(done);
|
|
end();
|
|
br(fail);
|
|
return;
|
|
}
|
|
final mid = (left + right) ~/ 2;
|
|
final midRange = ranges[mid].range;
|
|
|
|
local_get(classId);
|
|
i32_const(midRange.end);
|
|
i32_le_u();
|
|
if_();
|
|
search(left, mid, Range(searchArea.start, midRange.end));
|
|
end();
|
|
search(mid + 1, right, Range(midRange.end + 1, searchArea.end));
|
|
}
|
|
|
|
search(0, ranges.length - 1, Range(0, 0xffffffff));
|
|
end(); // fail
|
|
if (miss != null) {
|
|
miss();
|
|
br(done);
|
|
} else {
|
|
unreachable();
|
|
}
|
|
end(); // done
|
|
}
|
|
|
|
void _linearClassIdSearch<T>(
|
|
List<({Range range, T value})> ranges,
|
|
List<w.ValueType> outputs,
|
|
void Function(T) match,
|
|
void Function()? miss,
|
|
) {
|
|
assert(ranges.isNotEmpty || miss != null);
|
|
if (miss != null && ranges.isEmpty) {
|
|
drop();
|
|
miss();
|
|
return;
|
|
}
|
|
|
|
w.Local classId = addLocal(w.NumType.i32);
|
|
local_set(classId);
|
|
final done = block([], outputs);
|
|
for (final (:range, :value) in ranges) {
|
|
local_get(classId);
|
|
i32_const(range.start);
|
|
if (range.length == 1) {
|
|
i32_eq();
|
|
} else {
|
|
i32_sub();
|
|
i32_const(range.length);
|
|
i32_lt_u();
|
|
}
|
|
if_();
|
|
match(value);
|
|
br(done);
|
|
end();
|
|
}
|
|
if (miss != null) {
|
|
miss();
|
|
br(done);
|
|
} else {
|
|
unreachable();
|
|
}
|
|
end(); // done
|
|
}
|
|
|
|
/// `[ref _Closure] -> [i32]`
|
|
///
|
|
/// Given a closure reference returns whether the closure is an
|
|
/// instantiation.
|
|
void emitInstantiationClosureCheck(Translator translator) {
|
|
ref_cast(
|
|
w.RefType(translator.closureLayouter.closureBaseStruct, nullable: false),
|
|
);
|
|
struct_get(
|
|
translator.closureLayouter.closureBaseStruct,
|
|
FieldIndex.closureContext,
|
|
);
|
|
ref_test(
|
|
w.RefType(
|
|
translator.closureLayouter.instantiationContextBaseStruct,
|
|
nullable: false,
|
|
),
|
|
);
|
|
}
|
|
|
|
/// `[ref _Closure] -> [ref #ClosureBase]`
|
|
///
|
|
/// Given an instantiation closure returns the instantiated closure.
|
|
void emitGetInstantiatedClosure(Translator translator) {
|
|
// instantiation.context
|
|
ref_cast(
|
|
w.RefType(translator.closureLayouter.closureBaseStruct, nullable: false),
|
|
);
|
|
struct_get(
|
|
translator.closureLayouter.closureBaseStruct,
|
|
FieldIndex.closureContext,
|
|
);
|
|
|
|
// instantiation.context.inner
|
|
ref_cast(
|
|
w.RefType(
|
|
translator.closureLayouter.instantiationContextBaseStruct,
|
|
nullable: false,
|
|
),
|
|
);
|
|
struct_get(
|
|
translator.closureLayouter.instantiationContextBaseStruct,
|
|
FieldIndex.instantiationContextInner,
|
|
);
|
|
}
|
|
|
|
/// `[ref _Closure] -> [i32]`
|
|
///
|
|
/// Given a closure returns whether the closure is a tear-off.
|
|
void emitTearOffCheck(Translator translator) {
|
|
ref_cast(
|
|
w.RefType(translator.closureLayouter.closureBaseStruct, nullable: false),
|
|
);
|
|
struct_get(
|
|
translator.closureLayouter.closureBaseStruct,
|
|
FieldIndex.closureContext,
|
|
);
|
|
ref_test(translator.topTypeNonNullable);
|
|
}
|
|
|
|
/// `[ref _Closure] -> [ref #Top]`
|
|
///
|
|
/// Given a closure returns the receiver of the closure.
|
|
void emitGetTearOffReceiver(Translator translator) {
|
|
ref_cast(
|
|
w.RefType(translator.closureLayouter.closureBaseStruct, nullable: false),
|
|
);
|
|
struct_get(
|
|
translator.closureLayouter.closureBaseStruct,
|
|
FieldIndex.closureContext,
|
|
);
|
|
ref_cast(translator.topTypeNonNullable);
|
|
}
|
|
|
|
/// `[ref _Closure] -> [ref Any]
|
|
///
|
|
/// Given a closure returns the vtable of the closure.
|
|
void emitGetClosureVtable(Translator translator) {
|
|
ref_cast(
|
|
w.RefType(translator.closureLayouter.closureBaseStruct, nullable: false),
|
|
);
|
|
struct_get(
|
|
translator.closureLayouter.closureBaseStruct,
|
|
FieldIndex.closureVtable,
|
|
);
|
|
}
|
|
|
|
/// Will restore all context locals and `this` from a suspend state.
|
|
void restoreSuspendStateContext(
|
|
w.Local suspendStateLocal,
|
|
w.StructType suspendStateStruct,
|
|
int suspendStateContextField,
|
|
Closures closures,
|
|
Context? context,
|
|
w.Local? thisLocal, {
|
|
FunctionNode? cloneContextFor,
|
|
}) {
|
|
if (context != null) {
|
|
assert(!context.isEmpty);
|
|
local_get(suspendStateLocal);
|
|
struct_get(suspendStateStruct, suspendStateContextField);
|
|
ref_cast(context.currentLocal.type as w.RefType);
|
|
local_set(context.currentLocal);
|
|
if (context.owner == cloneContextFor) {
|
|
context.currentLocal = cloneFunctionLevelContext(
|
|
closures,
|
|
context,
|
|
cloneContextFor!,
|
|
);
|
|
}
|
|
restoreThisAndContextChain(context, thisLocal);
|
|
}
|
|
}
|
|
|
|
/// Will restore the parent context chain and `this` (if captured)
|
|
///
|
|
/// Assumes the innermost context is already loaded.
|
|
void restoreThisAndContextChain(
|
|
Context innermostContext,
|
|
w.Local? thisLocal,
|
|
) {
|
|
bool restoredThis = false;
|
|
|
|
Context? context = innermostContext;
|
|
while (context != null) {
|
|
if (context.containsThis) {
|
|
assert(!restoredThis);
|
|
local_get(context.currentLocal);
|
|
struct_get(context.struct, context.thisFieldIndex);
|
|
ref_as_non_null();
|
|
local_set(thisLocal!);
|
|
restoredThis = true;
|
|
}
|
|
|
|
final parent = context.parent;
|
|
if (parent != null) {
|
|
assert(!parent.isEmpty);
|
|
local_get(context.currentLocal);
|
|
struct_get(context.struct, context.parentFieldIndex);
|
|
ref_as_non_null();
|
|
local_set(parent.currentLocal);
|
|
}
|
|
context = parent;
|
|
}
|
|
}
|
|
|
|
/// Clones the [context] and returns a local to the clone it.
|
|
///
|
|
/// It is assumed that the context is a function-level context.
|
|
w.Local cloneFunctionLevelContext(
|
|
Closures closures,
|
|
Context context,
|
|
FunctionNode functionNode,
|
|
) {
|
|
final w.Local srcContext = context.currentLocal;
|
|
final w.Local destContext = addLocal(context.currentLocal.type);
|
|
|
|
struct_new_default(context.struct);
|
|
local_set(destContext);
|
|
|
|
void copyCapture(TreeNode node) {
|
|
Capture? capture = closures.captures[node];
|
|
if (capture != null) {
|
|
assert(capture.context == context);
|
|
local_get(destContext);
|
|
local_get(srcContext);
|
|
struct_get(context.struct, capture.fieldIndex);
|
|
struct_set(context.struct, capture.fieldIndex);
|
|
}
|
|
}
|
|
|
|
if (context.containsThis) {
|
|
local_get(destContext);
|
|
local_get(srcContext);
|
|
struct_get(context.struct, context.thisFieldIndex);
|
|
struct_set(context.struct, context.thisFieldIndex);
|
|
}
|
|
if (context.parent != null) {
|
|
local_get(destContext);
|
|
local_get(srcContext);
|
|
struct_get(context.struct, context.parentFieldIndex);
|
|
struct_set(context.struct, context.parentFieldIndex);
|
|
}
|
|
functionNode.positionalParameters.forEach(copyCapture);
|
|
functionNode.namedParameters.forEach(copyCapture);
|
|
functionNode.typeParameters.forEach(copyCapture);
|
|
|
|
return destContext;
|
|
}
|
|
|
|
List<w.ValueType> invoke(CallTarget target, {bool forceInline = false}) {
|
|
if (target.supportsInlining) {
|
|
if (forceInline) {
|
|
comment('Inlining ${target.name}, reason: forced');
|
|
return inlineCallTo(target);
|
|
}
|
|
final decision = target.shouldInline;
|
|
if (decision.shouldInline) {
|
|
comment('Inlining ${target.name}, reason: ${decision.reason}');
|
|
return inlineCallTo(target);
|
|
} else {
|
|
comment('Not inlining, reason: ${decision.reason}');
|
|
}
|
|
}
|
|
comment('Direct call to ${target.name}');
|
|
call(target.function);
|
|
return emitUnreachableIfNoResult(target.signature.outputs);
|
|
}
|
|
|
|
List<w.ValueType> inlineCallTo(CallTarget target) {
|
|
assert(target.supportsInlining);
|
|
final List<w.Local> inlinedLocals = target.signature.inputs
|
|
.map((t) => addLocal(t))
|
|
.toList();
|
|
for (w.Local local in inlinedLocals.reversed) {
|
|
local_set(local);
|
|
}
|
|
final w.Label callBlock = block(const [], target.signature.outputs);
|
|
return withInlinedFrame(target.name, () {
|
|
target.inliningCodeGen.generate(this, inlinedLocals, callBlock);
|
|
return emitUnreachableIfNoResult(target.signature.outputs);
|
|
});
|
|
}
|
|
|
|
/// Pushes fields common to all Dart objects (class id, id hash).
|
|
void pushObjectHeaderFields(Translator translator, ClassInfo classInfo) {
|
|
var classId = classInfo.classId;
|
|
i32_const(classId);
|
|
i32_const(initialIdentityHash);
|
|
}
|
|
|
|
void loadClassId(Translator translator, w.ValueType receiverType) {
|
|
assert(!receiverType.nullable);
|
|
assert(receiverType.isSubtypeOf(translator.topTypeNonNullable));
|
|
struct_get(
|
|
translator.classInfoCollector.topInfo.struct,
|
|
FieldIndex.classId,
|
|
);
|
|
}
|
|
|
|
void fillTableRange(
|
|
w.Table table,
|
|
int start,
|
|
int strideWidth,
|
|
w.BaseFunction fun,
|
|
) {
|
|
i32_const(start);
|
|
ref_func(fun);
|
|
i32_const(strideWidth);
|
|
table_fill(table);
|
|
}
|
|
|
|
void fillTableRangeWithIncreasingIntegers(
|
|
w.Table table,
|
|
int start,
|
|
int strideWidth,
|
|
int startValue,
|
|
) {
|
|
// index = start
|
|
final index = addLocal(w.NumType.i32);
|
|
i32_const(start);
|
|
local_set(index);
|
|
|
|
// value = startValue
|
|
final value = addLocal(w.NumType.i32);
|
|
i32_const(startValue);
|
|
local_set(value);
|
|
|
|
// while (index < (start + strideWidth)) { ... }
|
|
final done = block();
|
|
final next = loop();
|
|
local_get(index);
|
|
i32_const(start + strideWidth);
|
|
i32_ge_u();
|
|
br_if(done);
|
|
|
|
// table[index] = value
|
|
local_get(index);
|
|
local_get(value);
|
|
i31_new();
|
|
table_set(table);
|
|
|
|
// index++
|
|
local_get(index);
|
|
i32_const(1);
|
|
i32_add();
|
|
local_set(index);
|
|
|
|
// value++
|
|
local_get(value);
|
|
i32_const(1);
|
|
i32_add();
|
|
local_set(value);
|
|
|
|
br(next);
|
|
end(); // next
|
|
end(); // done
|
|
}
|
|
}
|
|
|
|
/// A call target that may be called with a direct call or may be inlined.
|
|
abstract class CallTarget {
|
|
/// The wasm signature of the call target (that may be called or inlined).
|
|
final w.FunctionType signature;
|
|
|
|
CallTarget(this.signature);
|
|
|
|
/// Whether callers should synthesize a `null` return value.
|
|
bool get synthesizeNullReturnValue => false;
|
|
|
|
/// Whether callee never returns and callers should emit `unreachable`.
|
|
bool get synthesizeNoReturn => false;
|
|
|
|
/// Whether this call target supports inlining.
|
|
bool get supportsInlining => false;
|
|
|
|
/// Whether we should inline (different call targets may have semantic
|
|
/// knowledge about how big the body would be and whether we should inline or
|
|
/// not).
|
|
InliningDecision get shouldInline =>
|
|
InliningDecision(false, 'no CallTarget support');
|
|
|
|
/// The code generator to use for inlining the body.
|
|
CodeGenerator get inliningCodeGen => throw 'No inlining support (yet).';
|
|
|
|
/// The name of this target
|
|
///
|
|
/// The inliner can use this to emit comments for the inlined target.
|
|
String get name;
|
|
|
|
/// The wasm target function to call.
|
|
///
|
|
/// This should only be accessed if caller intents to call it, as it will
|
|
/// enqueue the function in the compilation queue.
|
|
w.BaseFunction get function;
|
|
}
|
|
|
|
class AstCallTarget extends CallTarget {
|
|
final Translator _translator;
|
|
final Reference _reference;
|
|
|
|
AstCallTarget(super.signature, this._translator, this._reference);
|
|
|
|
@override
|
|
bool get synthesizeNullReturnValue =>
|
|
_translator.synthesizeNullReturnValue(_reference);
|
|
|
|
@override
|
|
bool get synthesizeNoReturn => _translator.synthesizeNoReturn(_reference);
|
|
|
|
@override
|
|
String get name => _translator.functions.getFunctionName(_reference);
|
|
|
|
@override
|
|
bool get supportsInlining => _translator.supportsInlining(_reference);
|
|
|
|
@override
|
|
InliningDecision get shouldInline =>
|
|
_translator.shouldInline(_reference, signature);
|
|
|
|
@override
|
|
CodeGenerator get inliningCodeGen => getInlinableMemberCodeGenerator(
|
|
_translator,
|
|
AsyncMarker.Sync,
|
|
signature,
|
|
_reference,
|
|
)!;
|
|
|
|
@override
|
|
w.BaseFunction get function => _translator.functions.getFunction(_reference);
|
|
}
|
|
|
|
class LambdaCallTarget extends CallTarget {
|
|
final Translator _translator;
|
|
final Lambda _lambda;
|
|
|
|
LambdaCallTarget(super.signature, this._translator, this._lambda);
|
|
|
|
@override
|
|
late final String name = _translator.functions.getLambdaFunctionName(_lambda);
|
|
|
|
@override
|
|
late final w.BaseFunction function = _translator.functions.getLambdaFunction(
|
|
_lambda,
|
|
);
|
|
}
|
|
|
|
/// Whether a `catch` guard has the right type to catch JS exceptions.
|
|
///
|
|
/// JS exceptions are only caught as: `dynamic`, `Object`, an extension of
|
|
/// `JSValue` like `JSObject`.
|
|
///
|
|
/// Note that the guard type can be nullable, but the value for the exception
|
|
/// needs to be non-null regardless of the guard type, as per Dart semantics.
|
|
bool guardCanMatchJSException(Translator translator, DartType guard) {
|
|
if (translator.options.standalone) {
|
|
// Standalone mode doesn't run in a JavaScript context and doesn't have JS
|
|
// exceptions.
|
|
return false;
|
|
}
|
|
|
|
return translator.typeEnvironment.isSubtypeOf(
|
|
InterfaceType(translator.jsValueClass, Nullability.nonNullable),
|
|
guard.extensionTypeErasure,
|
|
);
|
|
}
|