Files
sdk/pkg/dart2wasm/lib/code_generator.dart
T
Johnni Winther e30cd0322c [cfe][Contexts] Split VariableDeclaration and VariableStatement
This separates VariableDeclaration from Statement. VariableDeclaration no longer implements Statement and variable declared in a block or in a for-statement are now wrapped by a VariableStatement.

Currently there are two VariableStatement implementations; LegacyVariableStatement for variables in the current model, called LegacyVariable, and VariableInitialization for variables used in the new, still experimental, encoding that supports scope computation.

This CL is a step towards realigning the AST nodes to the new model in which each kind of variable has its own distinct subclass. (LocalVariable, PositionalParameter, NamedParameter, SyntheticVariable, etc.)

Note that it is not the intent to use VariableStatement in ForStatement going forward but that will be handled in a follow-up.

TEST=existing.

Change-Id: I5b309cd62c9b138f95b74fb054686edffa49a393
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/502681
Reviewed-by: Chloe Stefantsova <cstefantsova@google.com>
Reviewed-by: Nicholas Shahan <nshahan@google.com>
Reviewed-by: Alexander Markov <alexmarkov@google.com>
Reviewed-by: Martin Kustermann <kustermann@google.com>
Commit-Queue: Johnni Winther <johnniwinther@google.com>
2026-05-18 05:49:28 -07:00

6209 lines
192 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<VariableDeclaration, 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,
VariableDeclaration 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 (VariableDeclaration param in functionNode.positionalParameters) {
locals[param] = paramLocals[paramIndex++];
}
for (VariableDeclaration 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 {
visitVariableDeclaration(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 visitVariableDeclaration(VariableDeclaration 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 [visitVariableDeclaration] but it gives more control
/// over how the variable is initialized.
void initializeVariable(
VariableDeclaration 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 VariableDeclaration? 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 VariableDeclaration? 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.
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 (VariableStatement variable in node.variables) {
translateStatement(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 (VariableStatement 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) {
translateVariableDeclaration(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.jsStringInterpolate1;
} else if (expressions.length == 2) {
target = translator.jsStringInterpolate2;
} else if (expressions.length == 3) {
target = translator.jsStringInterpolate3;
} else {
assert(expressions.length == 4);
target = translator.jsStringInterpolate4;
}
} else {
final nullableObjectType = translator.coreTypes.objectNullableRawType;
makeArrayFromExpressions(expressions, nullableObjectType);
target = translator.jsStringInterpolate;
}
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(VariableDeclaration 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<VariableDeclaration> parameters,
) {
List<w.Local> constructorArgs = [];
for (int i = 0; i < typeParameters.length; i++) {
constructorArgs.add(typeLocals[typeParameters[i]]!);
}
for (VariableDeclaration param in parameters) {
constructorArgs.add(locals[param]!);
}
return constructorArgs;
}
int _setupConstructorParameters(
List<TypeParameter> typeParameters,
List<VariableDeclaration> 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) {
translateVariableDeclaration(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.jsStringEquals;
} 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.jsStringEquals.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) {
return translator.typeEnvironment.isSubtypeOf(
InterfaceType(translator.jsValueClass, Nullability.nonNullable),
guard.extensionTypeErasure,
);
}