// 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 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 with ExpressionVisitor1DefaultMixin, ExpressionVisitor1ExperimentExclusionMixin, StatementVisitorExperimentExclusionMixin implements InitializerVisitor, StatementVisitor, CodeGenerator { final Translator translator; final w.FunctionType functionType; final Member enclosingMember; // To be initialized in `generate()` late w.InstructionsBuilder b; late final List 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 locals = {}; w.Local? thisLocal; w.Local? preciseThisLocal; w.Local? returnValueLocal; final Map typeLocals = {}; /// Finalizers to run on `return`. final List 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> breakFinalizers = LinkedHashMap(); final List<({w.Local exceptionLocal, w.Local stackTraceLocal})> tryBlockLocals = []; final Map 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 switchBackwardJumpInfos = {}; /// Create a code generator for a member or one of its lambdas. AstCodeGenerator(this.translator, this.functionType, this.enclosingMember); List 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 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 paramLocals, w.Label? returnLabel, ) { this.b = b; this.paramLocals = paramLocals; this.returnLabel = returnLabel; translator.membersBeingGenerated.add(enclosingMember); generateInternal(); translator.membersBeingGenerated.remove(enclosingMember); } // Generate the body. void generateInternal(); void _setupLocalParameters( Member member, ParameterInfo paramInfo, int parameterOffset, int implicitParams, { bool isForwarder = false, bool canSafelyOmitImplicitChecks = false, }) { final memberFunction = member.function!; final ( :typeParameters, :typeParametersToTypeCheck, :positional, :positionalToTypeCheck, :named, :namedToTypeCheck, ) = translator.getParametersToCheck( member, ); for (int i = 0; i < typeParameters.length; i++) { final typeParameter = typeParameters[i]; typeLocals[typeParameter] = paramLocals[parameterOffset + i]; } final mayNeedToCheckTypes = translator.needToCheckTypesFor(member); if (mayNeedToCheckTypes) { for (int i = 0; i < typeParameters.length; i++) { final typeParameter = typeParameters[i]; if (translator.needToCheckTypeParameter(typeParameter)) { final typeParameterToCheck = typeParametersToTypeCheck[i]; _generateTypeArgumentBoundCheck( typeParameter.name!, typeLocals[typeParameter]!, typeParameterToCheck, ); } } } void setupParamLocal( DartType variableTypeToCheck, Variable variable, int index, Constant? defaultValue, bool isRequired, ) { final localIndex = implicitParams + index; w.Local local = paramLocals[localIndex]; final variableName = variable.name; if (variableName != null && variableName.isNotEmpty) { b.localNames[local.index] = variableName; } if (defaultValue == ParameterInfo.defaultValueSentinel) { // The default value for this parameter differs between implementations // within the same selector. This means that callers will pass the // default value sentinel to indicate that the parameter is not given. // The callee must check for the sentinel value and substitute the // actual default value. // // NOTE: The default sentinel is a dummy instance of the wasm type of // the parameter in the function signature. This type may be a super // type of the kind of arguments we actually see in practice. // (e.g. we may know that only nullable one byte strings can flow into // the argument, but the wasm type may be of object type). So we first // have to handle sentinel before we can downcast the value. b.local_get(local); translator.constants.instantiateConstant( b, ParameterInfo.defaultValueSentinel, local.type, ); b.ref_eq(); b.if_(); translateExpression(variable.initializer!, local.type); b.local_set(local); b.end(); } if (!isForwarder) { // TFA may have inferred a very precise type for the incoming arguments, // but the wasm function parameter type may not reflect this (e.g. due // to upper-bounding in dispatch table row building) // => This means, we may need to do a downcast here. final incomingArgumentType = translator.translateTypeOfParameter( variable, isRequired, ); if (!local.type.isSubtypeOf(incomingArgumentType)) { final newLocal = addLocal(incomingArgumentType); b.local_get(local); translator.convertType(b, local.type, newLocal.type); b.local_set(newLocal); local = newLocal; } } if (mayNeedToCheckTypes) { if (translator.needToCheckParameter( variable, uncheckedEntry: canSafelyOmitImplicitChecks, )) { final boxedType = variable.type.isPotentiallyNullable ? translator.topType : translator.topTypeNonNullable; w.Local operand = local; if (!operand.type.isSubtypeOf(boxedType)) { final boxedOperand = addLocal(boxedType); b.local_get(operand); translator.convertType(b, operand.type, boxedOperand.type); b.local_set(boxedOperand); operand = boxedOperand; } b.local_get(operand); _generateArgumentTypeCheck( variable.name!, operand.type as w.RefType, variableTypeToCheck, ); } } if (!isForwarder && !variable.isFinal) { // We now have a precise local that can contain the values passed by // callers, but the body may assign less precise types to this variable, // so we may introduce another local variable that is less precise. // => Binaryen will simplify the above downcast and this upcast. final variableType = translator.translateTypeOfLocalVariable(variable); if (!variableType.isSubtypeOf(local.type)) { w.Local newLocal = addLocal(variableType); b.local_get(local); translator.convertType(b, local.type, newLocal.type); b.local_set(newLocal); local = newLocal; } } locals[variable] = local; } for (int i = 0; i < positional.length; i++) { final bool isRequired = i < memberFunction.requiredParameterCount; final typeToCheck = positionalToTypeCheck[i]; setupParamLocal( typeToCheck, positional[i], i, paramInfo.positional[i], isRequired, ); } for (int i = 0; i < named.length; i++) { final param = named[i]; final typeToCheck = namedToTypeCheck[i]; setupParamLocal( typeToCheck, param, paramInfo.nameIndex[param.name]!, paramInfo.named[param.name], param.isRequired, ); } // For all parameters whose Wasm type has been forced to `externref` due to // this function being an export, internalize and cast the parameter to the // canonical representation type for its Dart type. locals.forEach((parameter, local) { DartType parameterType = parameter.type; if (local.type == w.RefType.extern(nullable: true) && !(parameterType is InterfaceType && parameterType.classNode == translator.wasmExternRefClass)) { w.Local newLocal = addLocal( translateType(parameterType), name: parameter.name, ); b.local_get(local); translator.convertType(b, local.type, newLocal.type); b.local_set(newLocal); locals[parameter] = newLocal; } }); } void setupParameters( Reference reference, { bool isForwarder = false, bool canSafelyOmitImplicitChecks = false, }) { Member member = reference.asMember; ParameterInfo paramInfo = translator.paramInfoForDirectCall(reference); int parameterOffset = _initializeThis(reference); int implicitParams = parameterOffset + paramInfo.typeParamCount; _setupLocalParameters( member, paramInfo, parameterOffset, implicitParams, isForwarder: isForwarder, canSafelyOmitImplicitChecks: canSafelyOmitImplicitChecks, ); } void setupParametersForNormalEntry(Member member) { setupParameters( member.reference, canSafelyOmitImplicitChecks: !translator.needToCheckTypesFor(member), ); } void setupParametersForCheckedEntry(Member member) { assert(member.isInstanceMember); assert(translator.needToCheckTypesFor(member)); setupParameters( member.checkedEntryReference, canSafelyOmitImplicitChecks: false, ); } void setupParametersForUncheckedEntry(Member member) { assert(member.isInstanceMember); assert(translator.needToCheckTypesFor(member)); setupParameters( member.uncheckedEntryReference, canSafelyOmitImplicitChecks: true, ); } void setupContexts(Member member) { allocateContext(member.function!); captureParameters(member.function!); } void setupLambdaParametersAndContexts(Lambda lambda) { FunctionNode functionNode = lambda.functionNode; _initializeContextLocals(functionNode); int paramIndex = 1; for (TypeParameter typeParam in functionNode.typeParameters) { typeLocals[typeParam] = paramLocals[paramIndex++]; } for (Variable param in functionNode.positionalParameters) { locals[param] = paramLocals[paramIndex++]; } for (Variable param in functionNode.namedParameters) { locals[param] = paramLocals[paramIndex++]; } allocateContext(functionNode); captureParameters(functionNode); } /// Initialize locals containing `this` in constructors and instance members. /// Returns the number of parameter locals taken up by the receiver parameter, /// i.e. the parameter offset for the first type parameter (or the first /// parameter if there are no type parameters). int _initializeThis(Reference reference) { Member member = reference.asMember; final hasThis = member.isInstanceMember || reference.isConstructorBodyReference; if (hasThis) { thisLocal = paramLocals[0]; b.localNames[thisLocal!.index] = "this"; final preciseThisType = translator.preciseThisFor(member); if (translator.needsConversion(thisLocal!.type, preciseThisType)) { preciseThisLocal = addLocal(preciseThisType, name: "preciseThis"); b.local_get(thisLocal!); translator.convertType(b, thisLocal!.type, preciseThisType); b.local_set(preciseThisLocal!); } else { preciseThisLocal = thisLocal!; } return 1; } return 0; } /// Initialize locals pointing to every context in the context chain of a /// closure, plus the locals containing `this` if `this` is captured by the /// closure. void _initializeContextLocals(TreeNode node, {int contextParamIndex = 0}) { Context? context; if (node is Constructor) { // The context parameter is for the constructor context. context = closures.contexts[node]; } else { assert(node is FunctionNode); // The context parameter is for the parent context. context = closures.contexts[node]?.parent; } if (context != null) { assert(!context.isEmpty); w.RefType contextType = w.RefType.def(context.struct, nullable: false); b.local_get(paramLocals[contextParamIndex]); b.ref_cast(contextType); while (true) { w.Local contextLocal = addLocal(contextType); context!.currentLocal = contextLocal; if (context.parent != null || context.containsThis) { b.local_tee(contextLocal); } else { b.local_set(contextLocal); } if (context.containsThis) { thisLocal = addLocal( context.struct.fields[context.thisFieldIndex].type.unpacked .withNullability(false), name: "this", ); preciseThisLocal = thisLocal; b.struct_get(context.struct, context.thisFieldIndex); b.ref_as_non_null(); b.local_set(thisLocal!); if (context.parent != null) { b.local_get(contextLocal); } } if (context.parent == null) break; b.struct_get(context.struct, context.parentFieldIndex); b.ref_as_non_null(); context = context.parent!; contextType = w.RefType.def(context.struct, nullable: false); } } } void _implicitReturn() { if (outputs.isNotEmpty) { w.ValueType returnType = outputs.single; if (returnType is w.RefType && returnType.nullable) { // Dart body may have an implicit return null. b.ref_null(returnType.heapType.bottomType); } else { b.comment("Unreachable implicit return"); b.unreachable(); } } } void allocateContext(TreeNode node) { Context? context = closures.contexts[node]; if (context == null || context.isEmpty) return; w.Local contextLocal = addLocal( w.RefType.def(context.struct, nullable: true), ); context.currentLocal = contextLocal; b.struct_new_default(context.struct); b.local_set(contextLocal); if (context.containsThis) { b.local_get(contextLocal); b.local_get(preciseThisLocal!); b.struct_set(context.struct, context.thisFieldIndex); } if (context.parent != null) { w.Local parentLocal = context.parent!.currentLocal; b.local_get(contextLocal); b.local_get(parentLocal); b.struct_set(context.struct, context.parentFieldIndex); } } void captureParameters(TreeNode node) { Context? context = closures.contexts[node]; if (context == null || context.isEmpty) return; locals.forEach((variable, local) { Capture? capture = closures.captures[variable]; if (capture != null && capture.context == context) { b.local_get(capture.context.currentLocal); b.local_get(local); translator.convertType(b, local.type, capture.type); b.struct_set(capture.context.struct, capture.fieldIndex); } }); typeLocals.forEach((parameter, local) { Capture? capture = closures.captures[parameter]; if (capture != null && capture.context == context) { b.local_get(capture.context.currentLocal); b.local_get(local); translator.convertType(b, local.type, capture.type); b.struct_set(capture.context.struct, capture.fieldIndex); } }); } /// Helper function to throw a Wasm ref downcast error. void throwWasmRefError(String expected) { _emitString(expected); call(translator.stackTraceCurrent.reference); call(translator.throwWasmRefError.reference); b.unreachable(); } /// Generates code for an expression plus conversion code to convert the /// result to the expected type if needed. All expression code generation goes /// through this method. w.ValueType translateExpression(Expression node, w.ValueType expectedType) { var sourceUpdated = false; Source? oldSource; if (node is FileUriNode) { final source = node.enclosingComponent!.uriToSource[(node as FileUriNode).fileUri]!; oldSource = setSourceMapSource(source); sourceUpdated = true; } final oldFileOffset = setSourceMapFileOffset(node.fileOffset); try { w.ValueType resultType = node.accept1(this, expectedType); translator.convertType(b, resultType, expectedType); return expectedType; } catch (_) { _printLocation(node); rethrow; } finally { if (sourceUpdated) { setSourceMapSource(oldSource); } setSourceMapFileOffset(oldFileOffset); } } void translateVariableDeclaration(VariableDeclaration node) { final oldFileOffset = setSourceMapFileOffset(node.fileOffset); try { visitVariable(node.variable); } catch (_) { _printLocation(node); rethrow; } finally { setSourceMapFileOffset(oldFileOffset); } } void translateVariable(Variable node) { final oldFileOffset = setSourceMapFileOffset(node.fileOffset); try { visitVariable(node); } catch (_) { _printLocation(node); rethrow; } finally { setSourceMapFileOffset(oldFileOffset); } } void translateStatement(Statement node) { final oldFileOffset = setSourceMapFileOffset(node.fileOffset); try { node.accept(this); } catch (_) { _printLocation(node); rethrow; } finally { setSourceMapFileOffset(oldFileOffset); } } void _printLocation(TreeNode node) { if (!exceptionLocationPrinted) { print("Exception in ${node.runtimeType} at ${node.location}"); exceptionLocationPrinted = true; } } List 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] = [label]; translateStatement(node.body); breakFinalizers.remove(node); b.end(); } @override void visitBreakStatement(BreakStatement node) { b.br(breakFinalizers[node.target]!.last); } @override void visitVariable(Variable node) { final w.ValueType type = translator.translateTypeOfLocalVariable(node); w.Local? local; final Capture? capture = closures.captures[node]; if (capture == null || !capture.written) { // Variable is not captured, or never updated after initialization. Keep // the value in a local. local = addLocal(type, name: node.name); locals[node] = local; } // Handle variable initialization. Nullable variables don't get an // initializer in kernel, but they still need to be initialized as `null`, // to reset the variables in loops to the initial value, intead of reusing // the last value from the previous iteration. This is tested in // `tests/language/local_null_initialization.dart`. if (node.initializer != null || node.type.nullability == Nullability.nullable) { Expression initializer = node.initializer ?? ConstantExpression(NullConstant()); if (capture != null) { // Type for the variable in context will always be nullable, to be able // to allocate the context without creating dummy values. Nullability of // the local's type will depend on the Dart type. assert( local == null || local.type.withNullability(true) == capture.type, ); w.ValueType expectedType = local != null ? local.type : capture.type; b.local_get(capture.context.currentLocal); translateExpression(initializer, expectedType); if (local != null) { b.local_tee(local); } b.struct_set(capture.context.struct, capture.fieldIndex); } else { translateExpression(initializer, local!.type); b.local_set(local); } } else if (local != null && !local.type.defaultable) { // Uninitialized variable. We don't need to update the context when the // variable is captured as the context is already initialized. translator .getDummyValuesCollectorForModule(b.moduleBuilder) .instantiateLocalDummyValue(b, local.type); b.local_set(local); } } /// Initialize a variable [node] to an initial value which must be left on /// the stack by [pushInitialValue]. /// /// This is similar to [visitVariable] but it gives more control /// over how the variable is initialized. void initializeVariable(Variable node, void Function() pushInitialValue) { final w.ValueType type = translator.translateTypeOfLocalVariable(node); w.Local? local; final Capture? capture = closures.captures[node]; if (capture == null || !capture.written) { // Variable is not captured, or never updated after initialization. Keep // the value in a local. local = addLocal(type, name: node.name); locals[node] = local; } if (capture != null) { b.local_get(capture.context.currentLocal); pushInitialValue(); if (local != null) { b.local_tee(local); } b.struct_set(capture.context.struct, capture.fieldIndex); } else { pushInitialValue(); b.local_set(local!); } } @override void visitEmptyStatement(EmptyStatement node) {} @override void visitAssertStatement(AssertStatement node) { if (options.enableAsserts) { w.Label assertBlock = b.block(); translateExpression(node.condition, w.NumType.i32); b.br_if(assertBlock); Expression? message = node.message; if (message != null) { translateExpression(message, translator.topType); } else { b.ref_null(w.HeapType.none); } final Location? location = node.location; final w.RefType stringRefType = translator.stringTypeNullable; if (location != null) { instantiateConstant( StringConstant(location.file.toString()), stringRefType, ); b.i64_const(location.line); b.i64_const(location.column); final String sourceString = node.enclosingComponent!.uriToSource[location.file]!.text; final String conditionString = sourceString.substring( node.conditionStartOffset, node.conditionEndOffset, ); instantiateConstant(StringConstant(conditionString), stringRefType); } else { b.ref_null(stringRefType.heapType); b.i64_const(0); b.i64_const(0); b.ref_null(stringRefType.heapType); } call(translator.throwAssertionError.reference); b.unreachable(); b.end(); } } @override void visitAssertBlock(AssertBlock node) { if (!options.enableAsserts) return; for (Statement statement in node.statements) { translateStatement(statement); } } @override void visitTryCatch(TryCatch node) { // It is not valid Dart to have a try without a catch. assert(node.catches.isNotEmpty); final w.RefType exceptionType = translator.topTypeNonNullable; final w.RefType stackTraceType = translator.stackTraceType; final w.Label wrapperBlock = b.block(); // Create a block target for each Dart `catch` block, to be able to share // code when generating a `catch` and `catch_all` for the same Dart `catch` // block, when the block can catch both Dart and JS exceptions. // The `end` for the Wasm `try` block works as the first exception handler // target. List catchBlockLabels = List.generate( node.catches.length - 1, (i) => b.block([], [exceptionType, stackTraceType]), growable: true, ); w.Label try_ = b.try_legacy([], [exceptionType, stackTraceType]); catchBlockLabels.add(try_); catchBlockLabels = catchBlockLabels.reversed.toList(); translateStatement(node.body); b.br(wrapperBlock); // Stash the original exception in a local so we can push it back onto the // stack after each type test. Also, store the stack trace in a local. w.Local thrownException = addLocal(exceptionType); w.Local thrownStackTrace = addLocal(stackTraceType); tryBlockLocals.add(( exceptionLocal: thrownException, stackTraceLocal: thrownStackTrace, )); void emitCatchBlock( w.Label catchBlockTarget, Catch catch_, bool emitGuard, ) { // For each catch node: // 1) Create a block for the catch. // 2) Push the caught exception onto the stack. // 3) Add a type test based on the guard of the catch. // 4) If the test fails, we jump to the next catch. Otherwise, we // jump to the block for the body of the catch. w.Label catchBlock = b.block(); DartType guard = catch_.guard; // Only emit the type test if the guard is not [Object]. if (emitGuard) { b.local_get(thrownException); types.emitIsTest( this, guard, translator.coreTypes.objectNonNullableRawType, catch_.location, ); b.i32_eqz(); b.br_if(catchBlock); } b.local_get(thrownException); b.local_get(thrownStackTrace); b.br(catchBlockTarget); b.end(); // end catchBlock. } // Insert a catch instruction which will catch any thrown Dart // exceptions. b.catch_legacy(translator.getDartExceptionTag(b.moduleBuilder)); b.local_set(thrownStackTrace); b.local_set(thrownException); for ( int catchBlockIndex = 0; catchBlockIndex < node.catches.length; catchBlockIndex += 1 ) { final catch_ = node.catches[catchBlockIndex]; // Only insert type checks if the guard is not `Object` final bool shouldEmitGuard = catch_.guard != translator.coreTypes.objectNonNullableRawType; emitCatchBlock( catchBlockLabels[catchBlockIndex], catch_, shouldEmitGuard, ); if (!shouldEmitGuard) { // If we didn't emit a guard, we won't ever fall through to the // following catch blocks. break; } } // Rethrow if all the catch blocks fall through b.rethrow_(try_); if (node.catches.any( (c) => guardCanMatchJSException(translator, c.guard), )) { b.catch_legacy(translator.getJsExceptionTag(b.moduleBuilder)); final jsExceptionLocal = addLocal(w.RefType.extern(nullable: true)); b.local_tee(jsExceptionLocal); call(translator.boxJsException.reference); b.local_tee(thrownException); // ref null #Top b.local_get(jsExceptionLocal); call(translator.jsExceptionStackTrace.reference); b.local_set(thrownStackTrace); for ( int catchBlockIndex = 0; catchBlockIndex < node.catches.length; catchBlockIndex += 1 ) { final catch_ = node.catches[catchBlockIndex]; if (!guardCanMatchJSException(translator, catch_.guard)) { continue; } // Type guards based on a type parameter are special, in that we cannot // statically determine whether a JavaScript error will always satisfy // the guard, so we should emit the type checking code for it. All // other guards will always match a JavaScript error, however, so no // need to emit type checks for those. final bool shouldEmitGuard = catch_.guard is TypeParameterType; emitCatchBlock( catchBlockLabels[catchBlockIndex], catch_, shouldEmitGuard, ); if (!shouldEmitGuard) { // If we didn't emit a guard, we won't ever fall through to the // following catch blocks. break; } } // Rethrow if the catch block falls through b.rethrow_(try_); } for (Catch catch_ in node.catches) { b.end(); b.local_set(thrownStackTrace); b.local_set(thrownException); final Variable? exceptionDeclaration = catch_.exception; if (exceptionDeclaration != null) { initializeVariable(exceptionDeclaration, () { b.local_get(thrownException); // Type test passed, downcast the exception to the expected type. translator.convertType( b, thrownException.type, translator.translateType(exceptionDeclaration.type), ); }); } final Variable? stackTraceDeclaration = catch_.stackTrace; if (stackTraceDeclaration != null) { initializeVariable( stackTraceDeclaration, () => b.local_get(thrownStackTrace), ); } translateStatement(catch_.body); b.br(wrapperBlock); } tryBlockLocals.removeLast(); b.end(); // end tryWrapper } @override void visitTryFinally(TryFinally node) { // We lower a [TryFinally] to a number of nested blocks, depending on how // many different code paths we have that run the finally block. // // We emit the finalizer once in a catch, to handle the case where the try // throws. Once outside of the catch, to handle the case where the try does // not throw. If there is a return within the try block, then we emit the // finalizer one more time along with logic to continue walking up the // stack. // // A `break L` can run more than one finalizer, and each of those // finalizers will need to be run in a different `try` block. So for each // wrapping label we generate a block to run the finalizer on `break` and // then branch to the right Wasm block to either run the next finalizer or // break. // The block for the try-finally statement. Used as `br` target in normal // execution after the finalizer (no throws, returns, or breaks). w.Label tryFinallyBlock = b.block(); // Create one block for each wrapping label. for (final labelBlocks in breakFinalizers.values.toList().reversed) { labelBlocks.add(b.block()); } // Continuation of this block runs the finalizer and returns (or jumps to // the next finalizer block). Used as `br` target on `return`. w.Label returnFinalizerBlock = b.block(); returnFinalizers.add(TryBlockFinalizer(returnFinalizerBlock)); w.Label tryBlock = b.try_legacy(); translateStatement(node.body); final bool mustHandleReturn = returnFinalizers .removeLast() .mustHandleReturn; // `break` statements in the current finalizer and the rest will not run // the current finalizer, update the `break` targets. final removedBreakTargets = {}; for (final breakFinalizerEntry in breakFinalizers.entries) { removedBreakTargets[breakFinalizerEntry.key] = breakFinalizerEntry.value .removeLast(); } // Handle Dart exceptions. b.catch_legacy(translator.getDartExceptionTag(b.moduleBuilder)); translateStatement(node.finalizer); b.rethrow_(tryBlock); // Handle JS exceptions. if (!translator.options.standalone) { b.catch_legacy(translator.getJsExceptionTag(b.moduleBuilder)); translateStatement(node.finalizer); b.rethrow_(tryBlock); } b.end(); // tryBlock // Run finalizer on normal execution (no breaks, throws, or returns). translateStatement(node.finalizer); b.br(tryFinallyBlock); b.end(); // returnFinalizerBlock // Run the finalizer on `return`. if (mustHandleReturn) { translateStatement(node.finalizer); if (returnFinalizers.isNotEmpty) { b.br(returnFinalizers.last.label); } else { if (returnValueLocal != null) { b.local_get(returnValueLocal!); translator.convertType(b, returnValueLocal!.type, returnType); } _returnFromFunction(); } } // Generate finalizers for `break`s in the `try` block. for (final removedBreakTargetEntry in removedBreakTargets.entries) { b.end(); translateStatement(node.finalizer); b.br(breakFinalizers[removedBreakTargetEntry.key]!.last); } b.end(); // tryFinallyBlock } @override void visitExpressionStatement(ExpressionStatement node) { translateExpression(node.expression, voidMarker); } bool _hasLogicalOperator(Expression condition) { while (condition is Not) { condition = condition.operand; } return condition is LogicalExpression; } void branchIf( Expression? condition, w.Label target, { required bool negated, }) { if (condition == null) { if (!negated) b.br(target); return; } while (condition is Not) { negated = !negated; condition = condition.operand; } if (condition is LogicalExpression) { bool isConjunctive = (condition.operatorEnum == LogicalExpressionOperator.AND) ^ negated; if (isConjunctive) { w.Label conditionBlock = b.block(); branchIf(condition.left, conditionBlock, negated: !negated); branchIf(condition.right, target, negated: negated); b.end(); } else { branchIf(condition.left, target, negated: negated); branchIf(condition.right, target, negated: negated); } } else { translateExpression(condition!, w.NumType.i32); if (negated) { b.i32_eqz(); } b.br_if(target); } } void _conditional( Expression condition, void Function() then, void Function()? otherwise, List result, ) { if (!_hasLogicalOperator(condition)) { // Simple condition translateExpression(condition, w.NumType.i32); b.if_(const [], result); then(); if (otherwise != null) { b.else_(); otherwise(); } b.end(); } else { // Complex condition w.Label ifBlock = b.block(const [], result); if (otherwise != null) { w.Label elseBlock = b.block(); branchIf(condition, elseBlock, negated: true); then(); b.br(ifBlock); b.end(); otherwise(); } else { branchIf(condition, ifBlock, negated: true); then(); } b.end(); } } @override void visitIfStatement(IfStatement node) { _conditional( node.condition, () => translateStatement(node.then), node.otherwise != null ? () => translateStatement(node.otherwise!) : null, const [], ); } @override void visitDoStatement(DoStatement node) { w.Label loop = b.loop(); allocateContext(node); translateStatement(node.body); branchIf(node.condition, loop, negated: false); b.end(); } @override void visitWhileStatement(WhileStatement node) { w.Label block = b.block(); w.Label loop = b.loop(); allocateContext(node); branchIf(node.condition, block, negated: true); translateStatement(node.body); b.br(loop); b.end(); b.end(); } @override void visitForStatement(ForStatement node) { allocateContext(node); for (VariableDeclaration variable in node.variables) { translateVariableDeclaration(variable); } w.Label block = b.block(); w.Label loop = b.loop(); branchIf(node.condition, block, negated: true); translateStatement(node.body); emitForStatementUpdate(node); b.br(loop); b.end(); b.end(); } void emitForStatementUpdate(ForStatement node) { Context? context = closures.contexts[node]; if (context != null && !context.isEmpty) { // Create a new context for each iteration of the loop. w.Local oldContext = context.currentLocal; allocateContext(node); w.Local newContext = context.currentLocal; // Copy the values of captured loop variables to the new context. for (VariableDeclaration variableDeclaration in node.variables) { Capture? capture = closures.captures[variableDeclaration.variable]; if (capture != null) { assert(capture.context == context); b.local_get(newContext); b.local_get(oldContext); b.struct_get(context.struct, capture.fieldIndex); b.struct_set(context.struct, capture.fieldIndex); } } // Update the context local to point to the new context. b.local_get(newContext); b.local_set(oldContext); } for (Expression update in node.updates) { translateExpression(update, voidMarker); } } @override void visitForInStatement(ForInStatement node) { throw "ForInStatement should have been desugared: $node"; } /// Handle the return from this function, either by jumping to [returnLabel] /// in the case this function was inlined or just inserting a return /// instruction. void _returnFromFunction() { if (returnLabel != null) { b.br(returnLabel!); } else { b.return_(); } } @override void visitReturnStatement(ReturnStatement node) { Expression? expression = node.expression; if (expression != null) { translateExpression(expression, returnType); } else { _implicitReturn(); } // If we are wrapped in a [TryFinally] node then we have to run finalizers // as the stack unwinds. When we get to the top of the finalizer stack, we // will handle the return using [returnValueLocal] if this function returns // a value. if (returnFinalizers.isNotEmpty) { for (TryBlockFinalizer finalizer in returnFinalizers) { finalizer.mustHandleReturn = true; } if (returnType != voidMarker) { // Since the flow of the return value through the returnValueLocal // crosses control-flow constructs, the local needs to always have a // defaultable type in order for the Wasm code to validate. returnValueLocal ??= addLocal( returnType.withNullability(true), name: "returnValue", ); b.local_set(returnValueLocal!); } b.br(returnFinalizers.last.label); } else { _returnFromFunction(); } } @override void visitSwitchStatement(SwitchStatement node) { // If we have an empty switch, just evaluate the expression for any // potential side effects. In this case, the return type does not matter. if (node.cases.isEmpty) { translateExpression(node.expression, voidMarker); return; } final switchInfo = SwitchInfo(this, node); bool isNullable = dartTypeOf(node.expression).isPotentiallyNullable; // When the type is nullable we use two variables: one for the nullable // value, one after the null check, with non-nullable type. w.Local switchValueNonNullableLocal = addLocal(switchInfo.nonNullableType); w.Local? switchValueNullableLocal = isNullable ? addLocal(switchInfo.nullableType) : null; // Initialize switch value local translateExpression( node.expression, isNullable ? switchInfo.nullableType : switchInfo.nonNullableType, ); b.local_set( isNullable ? switchValueNullableLocal! : switchValueNonNullableLocal, ); // Special cases SwitchCase? defaultCase = switchInfo.defaultCase; SwitchCase? nullCase = switchInfo.nullCase; // Create `loop` for backward jumps w.Label loopLabel = b.loop(); // Set `switchValueLocal` for backward jumps w.Local switchValueLocal = isNullable ? switchValueNullableLocal! : switchValueNonNullableLocal; // Add backward jump info switchBackwardJumpInfos[node] = SwitchBackwardJumpInfo( switchValueLocal, loopLabel, ); // Set up blocks, in reverse order of cases so they end in forward order w.Label doneLabel = b.block(); for (SwitchCase c in node.cases.reversed) { switchLabels[c] = b.block(); } // Compute value and handle null if (isNullable) { w.Label nullLabel = nullCase != null ? switchLabels[nullCase]! : defaultCase != null ? switchLabels[defaultCase]! : doneLabel; b.local_get(switchValueNullableLocal!); b.br_on_null(nullLabel); translator.convertType( b, switchInfo.nullableType.withNullability(false), switchInfo.nonNullableType, ); b.local_set(switchValueNonNullableLocal); } final dynamicTypeGuard = switchInfo.dynamicTypeGuard; if (dynamicTypeGuard != null) { final success = b.block(const [], [translator.topTypeNonNullable]); dynamicTypeGuard(switchValueNonNullableLocal, success); b.br(switchLabels[defaultCase] ?? doneLabel); b.end(); } final brTable = switchInfo.brTable; if (brTable != null) { // Map each entry in the range to the appropriate jump table entry. final indexBlocks = []; 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), exp, ); b.br_if(switchLabels[c]!); } } } // No explicit cases matched if (node.isExplicitlyExhaustive) { b.unreachable(); } else { w.Label defaultLabel = defaultCase != null ? switchLabels[defaultCase]! : doneLabel; b.br(defaultLabel); } } // Emit case bodies for (SwitchCase c in node.cases) { b.end(); // Remove backward jump target from forward jump labels switchLabels.remove(c); // Create a `loop` in default case to allow backward jumps to it if (c.isDefault) { switchBackwardJumpInfos[node]!.defaultLoopLabel = b.loop(); } translateStatement(c.body); if (c.isDefault) { b.end(); // defaultLoopLabel } b.br(doneLabel); } b.end(); // doneLabel b.end(); // loopLabel // Remove backward jump info final removed = switchBackwardJumpInfos.remove(node); assert(removed != null); } @override void visitContinueSwitchStatement(ContinueSwitchStatement node) { w.Label? label = switchLabels[node.target]; if (label != null) { b.br(label); } else { // Backward jump. Find the case literal in jump target, set the switched // values to the jump target's value, and loop. final SwitchCase targetSwitchCase = node.target; final SwitchStatement targetSwitch = targetSwitchCase.parent! as SwitchStatement; final SwitchBackwardJumpInfo targetInfo = switchBackwardJumpInfos[targetSwitch]!; if (targetSwitchCase.expressions.isEmpty) { // Default case assert(targetSwitchCase.isDefault); b.br(targetInfo.defaultLoopLabel!); return; } final Expression targetValue = targetSwitchCase.expressions[0]; // pick any of the values translateExpression(targetValue, targetInfo.switchValueLocal.type); b.local_set(targetInfo.switchValueLocal); b.br(targetInfo.loopLabel); } } @override void visitYieldStatement(YieldStatement node) { unimplemented(node, node.runtimeType, const []); } @override w.ValueType visitAwaitExpression( AwaitExpression node, w.ValueType expectedType, ) { throw 'Await expression in code generator: $node (${node.location})'; } @override w.ValueType visitBlockExpression( BlockExpression node, w.ValueType expectedType, ) { translateStatement(node.body); return translateExpression(node.value, expectedType); } @override w.ValueType visitLet(Let node, w.ValueType expectedType) { translateVariable(node.variable); return translateExpression(node.body, expectedType); } @override w.ValueType visitThisExpression( ThisExpression node, w.ValueType expectedType, ) { return visitThis(expectedType); } w.ValueType visitThis(w.ValueType expectedType) { w.ValueType thisType = thisLocal!.type; w.ValueType preciseThisType = preciseThisLocal!.type; assert(!thisType.nullable); assert(!preciseThisType.nullable); if (thisType.isSubtypeOf(expectedType)) { b.local_get(thisLocal!); return thisType; } if (preciseThisType.isSubtypeOf(expectedType)) { b.local_get(preciseThisLocal!); return preciseThisType; } // A user of `this` may have more precise type information, in which case // we downcast it here. b.local_get(thisLocal!); translator.convertType(b, thisType, expectedType); return expectedType; } @override w.ValueType visitConstructorInvocation( ConstructorInvocation node, w.ValueType expectedType, ) { w.ValueType? intrinsicResult = intrinsifier.generateConstructorIntrinsic( node, ); if (intrinsicResult != null) return intrinsicResult; ClassInfo info = translator.classInfo[node.target.enclosingClass]!; final target = node.targetReference; _visitArguments( node.arguments, translator.signatureForDirectCall(target), translator.paramInfoForDirectCall(target), 0, ); if (info.isCyclic) { // Cyclic types cannot be instantiated. Any code that tries to instantiate // them will fail with stack overflow, which is a trap in Wasm. Here we // replace one trap with another. b.unreachable(); return expectedType; } return call(target).single; } @override w.ValueType visitStaticInvocation( StaticInvocation node, w.ValueType expectedType, ) { if (ExternalEffect.isExternalEffect(node)) { return voidMarker; } w.ValueType? intrinsicResult = intrinsifier.generateStaticIntrinsic(node); if (intrinsicResult != null) return intrinsicResult; final target = node.targetReference; _visitArguments( node.arguments, translator.signatureForDirectCall(target), translator.paramInfoForDirectCall(target), 0, ); return translator.outputOrVoid(call(target)); } Member _lookupSuperTarget(Member interfaceTarget, {required bool setter}) { return translator.hierarchy.getDispatchTarget( enclosingMember.enclosingClass!.superclass!, interfaceTarget.name, setter: setter, )!; } @override w.ValueType visitSuperMethodInvocation( SuperMethodInvocation node, w.ValueType expectedType, ) { Reference target = translator.getFunctionEntry( _lookupSuperTarget(node.interfaceTarget, setter: false).reference, uncheckedEntry: true, ); w.FunctionType targetFunctionType = translator.signatureForDirectCall( target, ); final w.ValueType receiverType = translator.preciseThisFor(target.asMember); // When calling `==` and the argument is potentially nullable, check if the // argument is `null`. if (node.name == equalsName) { assert(node.arguments.positional.length == 1); assert(node.arguments.named.isEmpty); final argument = node.arguments.positional[0]; if (dartTypeOf(argument).isPotentiallyNullable) { w.Label resultBlock = b.block(const [], const [w.NumType.i32]); w.ValueType argumentType = targetFunctionType.inputs[1]; // `==` arguments are non-nullable. assert(argumentType.nullable == false); final argumentNullBlock = b.block(const [], const []); visitThis(receiverType); translateExpression(argument, argumentType.withNullability(true)); b.br_on_null(argumentNullBlock); final resultType = translator.outputOrVoid(call(target)); // `super ==` should return bool. assert(resultType == w.NumType.i32); b.br(resultBlock); b.end(); // argumentNullBlock b.i32_const(0); // false b.br(resultBlock); b.end(); // resultBlock return w.NumType.i32; } } visitThis(receiverType); _visitArguments( node.arguments, translator.signatureForDirectCall(target), translator.paramInfoForDirectCall(target), 1, ); return translator.outputOrVoid(call(target)); } @override w.ValueType visitInstanceInvocation( InstanceInvocation node, w.ValueType expectedType, ) { w.ValueType? intrinsicResult = intrinsifier.generateInstanceIntrinsic(node); if (intrinsicResult != null) return intrinsicResult; final useUncheckedEntry = translator.canUseUncheckedEntry( node.receiver, node, ); w.ValueType callWithNullCheck( Procedure target, void Function(w.ValueType) onNull, ) { late w.Label done; final w.ValueType resultType = _virtualCall( node, target, _VirtualCallKind.Call, (signature) { done = b.block(const [], signature.outputs); final w.Label nullReceiver = b.block(); translateExpression(node.receiver, translator.topType); b.br_on_null(nullReceiver); }, (w.FunctionType signature, ParameterInfo paramInfo) { _visitArguments(node.arguments, signature, paramInfo, 1); }, useUncheckedEntry: useUncheckedEntry, ); b.br(done); b.end(); // end nullReceiver onNull(resultType); b.end(); return resultType; } final Procedure target = node.interfaceTarget; if (node.kind == InstanceAccessKind.Object) { switch (target.name.text) { case "toString": return callWithNullCheck( target, (resultType) => translateExpression(StringLiteral("null"), resultType), ); case "noSuchMethod": return callWithNullCheck(target, (resultType) { final target = node.interfaceTargetReference; final signature = translator.signatureForDirectCall(target); final paramInfo = translator.paramInfoForDirectCall(target); // Object? receiver b.ref_null(translator.topType.heapType); // Invocation invocation _visitArguments(node.arguments, signature, paramInfo, 1); call(translator.invokeNoSuchMethod.reference); }); default: unimplemented(node, "Nullable invocation of ${target.name.text}", [ if (expectedType != voidMarker) expectedType, ]); return expectedType; } } Member? singleTarget = translator.singleTarget(node); // Custom devirtualization because TFA doesn't correctly devirtualize index // accesses on constant lists (see https://dartbug.com/60313) if (singleTarget == null && target.kind == ProcedureKind.Operator && target.name == indexGetName) { final receiver = node.receiver; if (receiver is ConstantExpression && receiver.constant is ListConstant) { singleTarget = translator.listBaseIndexOperator; } } if (singleTarget != null) { final target = translator.getFunctionEntry( singleTarget.reference, uncheckedEntry: useUncheckedEntry, ); final signature = translator.signatureForDirectCall(target); final paramInfo = translator.paramInfoForDirectCall(target); translateExpression(node.receiver, signature.inputs.first); _visitArguments(node.arguments, signature, paramInfo, 1); return translator.outputOrVoid(call(target)); } return _virtualCall( node, target, _VirtualCallKind.Call, (signature) => translateExpression(node.receiver, signature.inputs.first), (w.FunctionType signature, ParameterInfo paramInfo) { _visitArguments(node.arguments, signature, paramInfo, 1); }, useUncheckedEntry: useUncheckedEntry, ); } @override w.ValueType visitDynamicInvocation( DynamicInvocation node, w.ValueType expectedType, ) { return _handleDynamicInvocation(node.receiver, node.arguments, node.name); } w.ValueType _handleDynamicInvocation( Expression receiver, Arguments arguments, Name memberName, ) { // Call dynamic invocation forwarder final typeArguments = arguments.types; final positionalArguments = arguments.positional; final namedArguments = arguments.named; final callShape = MethodCallShape( memberName, typeArguments.length, positionalArguments.length, namedArguments.map((n) => n.name).toList()..sort(), ); final forwarder = translator .getDynamicDispatchersForModule(b.moduleBuilder) .getDispatcher(callShape); // Evaluate receiver translateExpression(receiver, translator.topType); // Evaluate type arguments. for (final typeArgument in typeArguments) { translator.types.makeType(this, typeArgument); } // Evaluate positional arguments for (final argument in positionalArguments) { translateExpression(argument, translator.topType); } // Evaluate named arguments. The arguments need to be evaluated in the // order they appear in the AST, but need to be sorted based on names in // the argument list passed to the dynamic forwarder. Create a local for // each argument to allow adding values to the list in expected order. final namedArgumentLocals = {}; 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); b.unreachable(); return voidMarker; } 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 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 argNames = node.arguments.named.map((a) => a.name).toList()..sort(); final Map 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 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 namedLocals = {}; for (var namedArg in node.named) { final w.ValueType type = signature .inputs[signatureOffset + paramInfo.nameIndex[namedArg.name]!]; final w.Local namedLocal = addLocal(type); namedLocals[namedArg.name] = namedLocal; translateExpression(namedArg.value, namedLocal.type); b.local_set(namedLocal); } // Re-order named arguments and push default values for optional named // parameters. for (String name in paramInfo.names) { w.Local? namedLocal = namedLocals[name]; final w.ValueType type = signature.inputs[signatureOffset + paramInfo.nameIndex[name]!]; if (namedLocal != null) { b.local_get(namedLocal); } else { instantiateConstant(paramInfo.named[name]!, type); } } } @override w.ValueType visitStringConcatenation( StringConcatenation node, w.ValueType expectedType, ) { bool isConstantString(Expression expr) => expr is StringLiteral || (expr is ConstantExpression && expr.constant is StringConstant); String extractConstantString(Expression expr) { if (expr is StringLiteral) { return expr.value; } else { return ((expr as ConstantExpression).constant as StringConstant).value; } } final expressions = node.expressions; if (expressions.every(isConstantString)) { StringBuffer result = StringBuffer(); for (final expr in expressions) { result.write(extractConstantString(expr)); } final expr = StringLiteral(result.toString()); return visitStringLiteral(expr, expectedType); } late final Procedure target; // We have special cases for 1/2/3/4 arguments. if (expressions.length <= 4) { final nullableObjectType = translator.translateType( translator.coreTypes.objectNullableRawType, ); for (final expression in expressions) { translateExpression(expression, nullableObjectType); } if (expressions.length == 1) { target = translator.stringImplInterpolate1; } else if (expressions.length == 2) { target = translator.stringImplInterpolate2; } else if (expressions.length == 3) { target = translator.stringImplInterpolate3; } else { assert(expressions.length == 4); target = translator.stringImplInterpolate4; } } else { final nullableObjectType = translator.coreTypes.objectNullableRawType; makeArrayFromExpressions(expressions, nullableObjectType); target = translator.stringImplInterpolate; } return translator.outputOrVoid(call(target.reference)); } @override w.ValueType visitThrow(Throw node, w.ValueType expectedType) { // Front-end wraps the argument with `as Object` when necessary, so we can // assume non-nullable here. assert(!dartTypeOf(node.expression).isPotentiallyNullable); translateExpression(node.expression, translator.topTypeNonNullable); call(translator.errorThrowWithCurrentStackTrace.reference); b.unreachable(); return expectedType; } @override w.ValueType visitRethrow(Rethrow node, w.ValueType expectedType) { final exceptionLocals = tryBlockLocals.last; b.local_get(exceptionLocals.exceptionLocal); b.local_get(exceptionLocals.stackTraceLocal); b.throw_(translator.getDartExceptionTag(b.moduleBuilder)); return expectedType; } @override w.ValueType visitConstantExpression( ConstantExpression node, w.ValueType expectedType, ) { instantiateConstant(node.constant, expectedType); return expectedType; } @override w.ValueType visitNullLiteral(NullLiteral node, w.ValueType expectedType) { instantiateConstant(NullConstant(), expectedType); return expectedType; } @override w.ValueType visitStringLiteral(StringLiteral node, w.ValueType expectedType) { instantiateConstant(StringConstant(node.value), expectedType); return expectedType; } @override w.ValueType visitBoolLiteral(BoolLiteral node, w.ValueType expectedType) { instantiateConstant(BoolConstant(node.value), expectedType); return expectedType; } @override w.ValueType visitIntLiteral(IntLiteral node, w.ValueType expectedType) { instantiateConstant(IntConstant(node.value), expectedType); return expectedType; } @override w.ValueType visitDoubleLiteral(DoubleLiteral node, w.ValueType expectedType) { instantiateConstant(DoubleConstant(node.value), expectedType); return expectedType; } @override w.ValueType visitListLiteral(ListLiteral node, w.ValueType expectedType) { final useSharedCreator = types.isTypeConstant(node.typeArgument); final passType = !useSharedCreator; final passArray = node.expressions.isNotEmpty; final targetReference = passArray ? translator.growableListFromWasmArray.reference : translator.growableListEmpty.reference; final target = useSharedCreator ? translator .getPartialInstantiatorForModule(b.moduleBuilder) .getOneTypeArgumentForwarder( targetReference, node.typeArgument, 'create${passArray ? '' : 'Empty'}List<${node.typeArgument}>', ) : translator.functions.getFunction(targetReference); if (passType) { types.makeType(this, node.typeArgument); } if (passArray) { makeArrayFromExpressions( node.expressions, translator.coreTypes.objectRawType(Nullability.nullable), ); } translator.callFunction(target, b); return target.type.outputs.single; } w.ValueType makeArrayFromExpressions( List expressions, InterfaceType elementType, ) { return makeArray( translator.arrayTypeForDartType(elementType, mutable: true), expressions.length, (w.ValueType type, int i) { translateExpression(expressions[i], type); }, ); } w.ValueType makeArray( w.ArrayType arrayType, int length, void Function(w.ValueType, int) generateItem, ) { return translator.makeArray(b, arrayType, length, generateItem); } @override w.ValueType visitMapLiteral(MapLiteral node, w.ValueType expectedType) { final useSharedCreator = types.isTypeConstant(node.keyType) && types.isTypeConstant(node.valueType); final passTypes = !useSharedCreator; final passArray = node.entries.isNotEmpty; final targetReference = passArray ? translator.mapFromWasmArray.reference : translator.mapFactory.reference; final target = useSharedCreator ? translator .getPartialInstantiatorForModule(b.moduleBuilder) .getTwoTypeArgumentForwarder( targetReference, node.keyType, node.valueType, 'create${passArray ? '' : 'Empty'}' 'Map<${node.keyType}, ${node.valueType}>', ) : translator.functions.getFunction(targetReference); if (passTypes) { types.makeType(this, node.keyType); types.makeType(this, node.valueType); } if (passArray) { makeArray(translator.nullableObjectArrayType, 2 * node.entries.length, ( elementType, elementIndex, ) { final index = elementIndex ~/ 2; final entry = node.entries[index]; if (elementIndex % 2 == 0) { translateExpression(entry.key, elementType); } else { translateExpression(entry.value, elementType); } }); } translator.callFunction(target, b); return target.type.outputs.single; } @override w.ValueType visitSetLiteral(SetLiteral node, w.ValueType expectedType) { final useSharedCreator = types.isTypeConstant(node.typeArgument); final passType = !useSharedCreator; final passArray = node.expressions.isNotEmpty; final targetReference = passArray ? translator.setFromWasmArray.reference : translator.setFactory.reference; final target = useSharedCreator ? translator .getPartialInstantiatorForModule(b.moduleBuilder) .getOneTypeArgumentForwarder( targetReference, node.typeArgument, 'create${passArray ? '' : 'Empty'}Set<${node.typeArgument}>', ) : translator.functions.getFunction(targetReference); if (passType) { types.makeType(this, node.typeArgument); } if (passArray) { makeArrayFromExpressions( node.expressions, translator.coreTypes.objectRawType(Nullability.nullable), ); } translator.callFunction(target, b); return target.type.outputs.single; } @override w.ValueType visitTypeLiteral(TypeLiteral node, w.ValueType expectedType) { return types.makeType(this, node.type); } @override w.ValueType visitIsExpression(IsExpression node, w.ValueType expectedType) { final operandType = dartTypeOf(node.operand); final boxedOperandType = operandType.isPotentiallyNullable ? translator.topType : translator.topTypeNonNullable; translateExpression(node.operand, boxedOperandType); types.emitIsTest(this, node.type, operandType, node.location); return w.NumType.i32; } @override w.ValueType visitAsExpression(AsExpression node, w.ValueType expectedType) { final isImplicitCheck = (node.isTypeError || node.isCovarianceCheck || node.isForDynamic); if (node.isUnchecked || (translator.options.omitImplicitTypeChecks && isImplicitCheck) || (translator.options.omitExplicitTypeChecks && !isImplicitCheck)) { return translateExpression(node.operand, expectedType); } final operandType = dartTypeOf(node.operand); final boxedOperandType = operandType.isPotentiallyNullable ? translator.topType : translator.topTypeNonNullable; translateExpression(node.operand, boxedOperandType); return types.emitAsCheck( this, node.isCovarianceCheck, node.type, operandType, boxedOperandType, node.location, ); } @override w.ValueType visitLoadLibrary(LoadLibrary node, w.ValueType expectedType) { throw UnsupportedError( 'LoadLibrary should be lowered by modular transformer.', ); } @override w.ValueType visitCheckLibraryIsLoaded( CheckLibraryIsLoaded node, w.ValueType expectedType, ) { throw UnsupportedError( 'CheckLibraryIsLoaded should be lowered by modular transformer.', ); } /// Pushes the `_Type` object for a function or class type parameter to the /// stack and returns the value type of the object. w.ValueType instantiateTypeParameter(TypeParameter parameter) { w.ValueType resultType; w.Local? local = typeLocals[parameter]; Capture? capture = closures.captures[parameter]; if (local != null) { b.local_get(local); resultType = local.type; } else if (capture != null) { Capture capture = closures.captures[parameter]!; b.local_get(capture.context.currentLocal); b.struct_get(capture.context.struct, capture.fieldIndex); resultType = capture.type; } else { Class cls = parameter.declaration as Class; ClassInfo info = translator.classInfo[cls]!; int fieldIndex = translator.typeParameterIndex[parameter]!; visitThis(info.nonNullableType); b.struct_get(info.struct, fieldIndex); resultType = info.struct.fields[fieldIndex].type.unpacked; } translator.convertType(b, resultType, types.nonNullableTypeType); return types.nonNullableTypeType; } @override w.ValueType visitRecordLiteral(RecordLiteral node, w.ValueType expectedType) { final ClassInfo recordClassInfo = translator.getRecordClassInfo( node.recordType, ); translator.functions.recordClassAllocation(recordClassInfo.classId); b.pushObjectHeaderFields(translator, recordClassInfo); for (Expression positional in node.positional) { translateExpression(positional, translator.topType); } for (NamedExpression named in node.named) { translateExpression(named.value, translator.topType); } b.struct_new(recordClassInfo.struct); return recordClassInfo.nonNullableType; } @override w.ValueType visitRecordIndexGet( RecordIndexGet node, w.ValueType expectedType, ) { final RecordShape recordShape = RecordShape.fromType(node.receiverType); final ClassInfo recordClassInfo = translator.getRecordClassInfo( node.receiverType, ); translateExpression(node.receiver, translator.topTypeNonNullable); b.ref_cast(w.RefType(recordClassInfo.struct, nullable: false)); b.struct_get( recordClassInfo.struct, recordShape.getPositionalIndex(node.index), ); return translator.topType; } @override w.ValueType visitRecordNameGet(RecordNameGet node, w.ValueType expectedType) { final RecordShape recordShape = RecordShape.fromType(node.receiverType); final ClassInfo recordClassInfo = translator.getRecordClassInfo( node.receiverType, ); translateExpression(node.receiver, translator.topTypeNonNullable); b.ref_cast(w.RefType(recordClassInfo.struct, nullable: false)); b.struct_get(recordClassInfo.struct, recordShape.getNameIndex(node.name)); return translator.topType; } @override w.ValueType visitFileUriExpression( FileUriExpression node, w.ValueType expectedType, ) { return translateExpression(node.expression, expectedType); } /// Generate code that checks type of an argument against an expected type /// and throws a `TypeError` on failure. /// /// Expects a boxed object (whose type is to be checked) on the stack. /// /// [argName] is used in the type error as the name of the argument that /// doesn't match the expected type. void _generateArgumentTypeCheck( String argName, w.RefType argumentType, DartType testedAgainstType, ) { if (translator.options.minify) { // We don't need to include the name in the error message, so we can use // the optimized `as` checks. types.emitAsCheck( this, false, testedAgainstType, translator.coreTypes.objectNullableRawType, argumentType, ); b.drop(); } else { final argLocal = b.addLocal(argumentType); b.local_tee(argLocal); types.emitIsTest( this, testedAgainstType, translator.coreTypes.objectNullableRawType, ); b.i32_eqz(); b.if_(); b.local_get(argLocal); types.makeType(this, testedAgainstType); _emitString(argName); call(translator.stackTraceCurrent.reference); call(translator.throwArgumentTypeCheckError.reference); b.unreachable(); b.end(); } } void _generateTypeArgumentBoundCheck( String argName, w.Local typeLocal, DartType bound, ) { b.local_get(typeLocal); final boundLocal = b.addLocal(translator.runtimeTypeType); types.makeType(this, bound); b.local_tee(boundLocal); call(translator.isTypeSubtype.reference); b.i32_eqz(); b.if_(); // Type check failed b.local_get(typeLocal); b.local_get(boundLocal); _emitString(argName); call(translator.stackTraceCurrent.reference); call(translator.throwTypeArgumentBoundCheckError.reference); b.unreachable(); b.end(); } void _emitString(String str) => translateExpression( StringLiteral(str), translator.translateType(translator.coreTypes.stringNonNullableRawType), ); @override void visitPatternSwitchStatement(PatternSwitchStatement node) { // This node is internal to the front end and removed by the constant // evaluator. throw UnsupportedError("CodeGenerator.visitPatternSwitchStatement"); } @override void visitPatternVariableDeclaration(PatternVariableDeclaration node) { // This node is internal to the front end and removed by the constant // evaluator. throw UnsupportedError("CodeGenerator.visitPatternVariableDeclaration"); } @override void visitIfCaseStatement(IfCaseStatement node) { // This node is internal to the front end and removed by the constant // evaluator. throw UnsupportedError("CodeGenerator.visitIfCaseStatement"); } void debugRuntimePrint(String s) { final printFunction = translator.functions.getFunction( translator.printToConsole.reference, ); translator.constants.instantiateConstant( b, StringConstant(s), printFunction.type.inputs[0], ); translator.callFunction(printFunction, b); } @override void visitAuxiliaryStatement(AuxiliaryStatement node) { throw UnsupportedError( "Unsupported auxiliary statement $node (${node.runtimeType}).", ); } @override void visitAuxiliaryInitializer(AuxiliaryInitializer node) { throw UnsupportedError( "Unsupported auxiliary initializer $node (${node.runtimeType}).", ); } void emitUnimplementedExternalError(Member member) { b.comment("Unimplemented external member $member at ${member.location}"); if (member.isInstanceMember) { b.local_get(paramLocals[0]); } else { b.ref_null(w.HeapType.none); } translator.constants.instantiateConstant( b, translator.symbols.methodSymbolFromName(member.name), translator.classInfo[translator.symbolClass]!.nonNullableType, ); call( translator .noSuchMethodErrorThrowUnimplementedExternalMemberError .reference, ); b.unreachable(); } void instantiateConstant(Constant constant, w.ValueType expectedType) { translator.constants.instantiateConstant( b, constant, expectedType, deferredModuleGuard: translator.moduleForConstant(constant), ); } } CodeGenerator getMemberCodeGenerator( Translator translator, w.FunctionBuilder functionBuilder, Reference memberReference, ) { final member = memberReference.asMember; final asyncMarker = member.function?.asyncMarker ?? AsyncMarker.Sync; final codeGen = getInlinableMemberCodeGenerator( translator, asyncMarker, functionBuilder.type, memberReference, ); if (codeGen != null) return codeGen; final Class? memberClass = member.enclosingClass; if (memberClass != null && translator.classInfo[memberClass]!.isCyclic) { return UnreachableCodeGenerator(translator, functionBuilder.type, member); } final procedure = member as Procedure; if (asyncMarker == AsyncMarker.SyncStar) { return SyncStarProcedureCodeGenerator( translator, procedure, functionBuilder.type, functionBuilder.name, ); } assert(asyncMarker == AsyncMarker.Async); return AsyncProcedureCodeGenerator( translator, procedure, functionBuilder.type, functionBuilder.name, ); } CodeGenerator getLambdaCodeGenerator(Translator translator, Lambda lambda) { final enclosingMember = lambda.enclosingMember; final enclosingClass = enclosingMember.enclosingClass; if (enclosingClass != null && translator.classInfo[enclosingClass]!.isCyclic) { return UnreachableCodeGenerator( translator, lambda.callTarget.signature, enclosingMember, ); } final asyncMarker = lambda.functionNode.asyncMarker; if (asyncMarker == AsyncMarker.Async) { return AsyncLambdaCodeGenerator(translator, lambda); } if (asyncMarker == AsyncMarker.SyncStar) { return SyncStarLambdaCodeGenerator(translator, lambda); } assert(asyncMarker == AsyncMarker.Sync); return SynchronousLambdaCodeGenerator(translator, lambda); } /// Returns a [CodeGenerator] for the given member iff that member can be /// inlined. CodeGenerator? getInlinableMemberCodeGenerator( Translator translator, AsyncMarker asyncMarker, w.FunctionType functionType, Reference reference, ) { final Member member = reference.asMember; final Class? memberClass = member.enclosingClass; if (memberClass != null && translator.classInfo[memberClass]!.isCyclic) { return UnreachableCodeGenerator(translator, functionType, member); } if (reference.isTearOffReference) { return TearOffCodeGenerator(translator, functionType, member); } if (member is Constructor) { if (reference.isConstructorBodyReference) { return ConstructorBodyCodeGenerator(translator, functionType, member); } else if (reference.isInitializerReference) { return ConstructorInitializerCodeGenerator( translator, functionType, member, ); } else { return ConstructorAllocatorCodeGenerator( translator, functionType, member, ); } } if (member is Field) { if (member.isStatic) { if (reference.isImplicitGetter || reference.isImplicitSetter) { return StaticFieldImplicitAccessorCodeGenerator( translator, functionType, member, reference.isImplicitGetter, ); } return StaticFieldInitializerCodeGenerator( translator, functionType, member, ); } final useUncheckedEntry = reference.isUncheckedEntryReference; return ImplicitFieldAccessorCodeGenerator( translator, functionType, member, reference.isImplicitGetter, useUncheckedEntry, ); } if (member is Procedure && asyncMarker == AsyncMarker.Sync) { return SynchronousProcedureCodeGenerator( translator, functionType, member, reference, reference.entryKind, ); } assert( asyncMarker == AsyncMarker.SyncStar || asyncMarker == AsyncMarker.Async, ); return null; } class SynchronousProcedureCodeGenerator extends AstCodeGenerator { final Procedure member; final Reference reference; final EntryPoint kind; SynchronousProcedureCodeGenerator( Translator translator, w.FunctionType functionType, this.member, this.reference, this.kind, ) : super(translator, functionType, member) { assert( !translator.needToCheckTypesFor(member) || kind != EntryPoint.normal, ); } @override void generateInternal() { final source = member.enclosingComponent!.uriToSource[member.fileUri]!; setSourceMapSourceAndFileOffset(source, member.fileOffset); if (intrinsifier.generateMemberIntrinsic( member.reference, functionType, paramLocals, returnLabel, )) { b.end(); return; } if (member.isExternal) { emitUnimplementedExternalError(member); b.end(); return; } closures = translator.getClosures(member); switch (kind) { case EntryPoint.normal: b.comment('Normal Entry'); _makeNonMultiEntryPointFunction(); case EntryPoint.checked: b.comment('Checked Entry'); _makeMultipleEntryPoint(true); case EntryPoint.unchecked: b.comment('Unchecked Entry'); _makeMultipleEntryPoint(false); case EntryPoint.body: b.comment('Body for Checked & Unchecked Entry'); _makeMultipleEntryPointSharedBody(); break; } } void _makeMultipleEntryPoint(bool checked) { final function = member.function; final signature = translator.signatureForDirectCall(member.bodyReference); if (checked) { setupParametersForCheckedEntry(member); } else { setupParametersForUncheckedEntry(member); } int arg = 0; visitThis(signature.inputs[arg++]); for (final parameter in function.typeParameters) { final r = instantiateTypeParameter(parameter); translator.convertType(b, r, signature.inputs[arg++]); } for (final parameter in function.positionalParameters) { final local = locals[parameter]!; b.local_get(local); translator.convertType(b, local.type, signature.inputs[arg++]); } for (final parameter in function.namedParameters) { final local = locals[parameter]!; b.local_get(local); translator.convertType(b, local.type, signature.inputs[arg++]); } final outputs = call(member.bodyReference); if (outputs.isNotEmpty) { translator.convertType(b, outputs.single, returnType); } _returnFromFunction(); b.end(); } void _makeMultipleEntryPointSharedBody() { final function = member.function; final typeParameters = function.typeParameters; final positionals = function.positionalParameters; final named = function.namedParameters; int param = _initializeThis(member.reference); for (int i = 0; i < typeParameters.length; i++) { final typeParameter = typeParameters[i]; typeLocals[typeParameter] = paramLocals[param++]; } void setupParameter(Variable parameter) { // The body may assign less precise types to the parameter variable than // what the caller provides. w.Local local = paramLocals[param++]; if (translator.typeOfCheckedParameterVariable(parameter) != parameter.type) { final newLocal = addLocal(translator.translateType(parameter.type)); b.local_get(local); translator.convertType(b, local.type, newLocal.type); b.local_set(newLocal); local = newLocal; } locals[parameter] = local; } for (int i = 0; i < positionals.length; i++) { setupParameter(positionals[i]); } for (int i = 0; i < named.length; i++) { setupParameter(named[i]); } setupContexts(member); Statement? body = member.function.body; if (body != null) { translateStatement(body); } _implicitReturn(); b.end(); } void _makeNonMultiEntryPointFunction() { setupParametersForNormalEntry(member); setupContexts(member); Statement? body = member.function.body; if (body != null) { translateStatement(body); } _implicitReturn(); b.end(); return; } } class TearOffCodeGenerator extends AstCodeGenerator { final Member member; TearOffCodeGenerator( Translator translator, w.FunctionType functionType, this.member, ) : super(translator, functionType, member); @override void generateInternal() { // Initialize [Closures] without [Closures.captures]: [Closures.captures] is // used by `makeType` below, when generating runtime types of type // parameters of the function type, but the type parameters are not // captured, always loaded from the `this` struct. closures = translator.getClosures(member, findCaptures: false); _initializeThis(member.reference); Procedure procedure = member as Procedure; DartType functionType = translator.getTearOffType(procedure); ClosureImplementation closure = translator.getTearOffClosure( procedure, b.moduleBuilder, ); w.StructType struct = closure.representation.closureStruct; ClassInfo info = translator.closureInfo; translator.functions.recordClassAllocation(info.classId); b.pushObjectHeaderFields(translator, info); b.local_get(paramLocals[0]); // `this` as context // The closure requires a struct value so box `this` if necessary. translator.convertType( b, paramLocals[0].type, struct.fields[FieldIndex.closureContext].type.unpacked, ); translator.globals.readGlobal(b, closure.vtable); types.makeType(this, functionType); b.struct_new(struct); b.end(); } } /// Generates code for a dynamic forwarder function. /// /// Dynamic forwarders are functions that /// * may have to populate default type arguments /// * may have to check argument types /// * may have to unbox arguments /// * call the normal getter/setter/method target /// * may have to box the result value /// /// We generate them for each [CallShape] and the caller guarantees that the /// [CallShape] is valid for the given target [reference]. The signature for /// such a forwarder function is determined by [makeDynamicForwarderSignature]. class DynamicForwarderCodeGenerator extends AstCodeGenerator { final Reference reference; final CallShape callShape; DynamicForwarderCodeGenerator( Translator translator, w.FunctionType functionType, this.reference, this.callShape, ) : super(translator, functionType, reference.asMember); @override void generateInternal() { final member = reference.asMember; // Initialize [Closures] without [Closures.captures]: Similar to // [TearOffCodeGenerator], type parameters will be loaded from the `this` // struct. closures = translator.getClosures(member, findCaptures: false); if (reference.isGetter) { _generateDynamicGetterForwarder(); } else if (reference.isSetter) { _generateDynamicSetterForwarder(); } else if (reference.isTearOffReference) { _generateDynamicTearOffForwarder(); } else { member as Procedure; _generateDynamicMethodForwarder(); } } void _generateDynamicTearOffForwarder() { final targetSignature = translator.signatureForDirectCall(reference); assert( targetSignature.inputs.length == 1 && targetSignature.outputs.length == 1, ); // The only real thing the dynamic forwarder for a tear off does is // converting the receiver type: The receiver type from the dynamic call // site is a non-nullable top type. But the actual tear off getter from the // target may require a more precise receiver type. b.local_get(paramLocals[0]); translator.convertType( b, paramLocals[0].type, targetSignature.inputs.single, ); translator.callReference(reference, b); b.return_(); b.end(); } void _generateDynamicMethodForwarder() { final callShape = this.callShape as MethodCallShape; // The offsets of arguments passed by the caller. const int argReceiverOffset = 0; const int argTypesOffset = argReceiverOffset + 1; final int argPositionalsOffset = argTypesOffset + callShape.typeCount; final int argNamedOffset = argPositionalsOffset + callShape.positionalCount; final targetProcedure = reference.asMember as Procedure; final targetFunction = targetProcedure.function; assert(callShape.matchesTarget(targetFunction)); final target = translator.getFunctionEntry( targetProcedure.reference, uncheckedEntry: false, ); final callTarget = translator.directCallTarget(target); _initializeThis(reference); final typeType = translator.classInfo[translator.typeClass]!.nonNullableType; final targetParamInfo = translator.paramInfoForDirectCall(target); // Load the receiver final receiverLocal = paramLocals[argReceiverOffset]; b.local_get(receiverLocal); translator.convertType( b, receiverLocal.type, callTarget.signature.inputs[0], ); // Load type parameters for target. final targetTypeParams = targetFunction.typeParameters; assert(targetTypeParams.length == targetParamInfo.typeParamCount); if (targetTypeParams.isNotEmpty) { if (callShape.typeCount != 0) { // Provided by caller. for (int i = 0; i < targetTypeParams.length; ++i) { final param = targetTypeParams[i]; final paramValue = paramLocals[argTypesOffset + i]; b.local_get(paramValue); typeLocals[param] = paramValue; } } else { // Use default-to-bounds. for (int i = 0; i < targetTypeParams.length; ++i) { final param = targetTypeParams[i]; types.makeType(this, param.defaultType); final paramValue = b.addLocal(typeType); b.local_tee(paramValue); typeLocals[param] = paramValue; } } } // Check type parameter bounds. if (!translator.options.omitImplicitTypeChecks) { for (int i = 0; i < targetTypeParams.length; ++i) { final param = targetTypeParams[i]; if (param.bound != translator.coreTypes.objectNullableRawType) { final paramValue = typeLocals[param]!; _generateTypeArgumentBoundCheck(param.name!, paramValue, param.bound); } } } // Load positional parameters for the target (and check types if needed). final targetPositionalParams = targetFunction.positionalParameters; for (int i = 0; i < targetParamInfo.positional.length; i++) { final targetParamType = callTarget.signature.inputs[1 + targetParamInfo.typeParamCount + i]; if (i < callShape.positionalCount) { // Provided by the caller. final paramValue = paramLocals[argPositionalsOffset + i]; b.local_get(paramValue); if (!translator.options.omitImplicitTypeChecks) { final param = targetPositionalParams[i]; b.local_get(paramValue); _generateArgumentTypeCheck( param.name!, translator.topType, param.type, ); } translator.convertType(b, paramValue.type, targetParamType); } else { // Default to use if the callee has the `i` parameter. final defaultFunctionValue = i < targetPositionalParams.length ? (targetPositionalParams[i].initializer as ConstantExpression?) ?.constant : null; // Default to use if callee doesn't have the `i` parameter. final defaultValue = targetParamInfo.positional[i]; // The target wasm function corresponding to an instance method may have // a selector signature (which is based on all implementations of a // selector) and therefore may have more parameters than the actual // target needs (the others are ignored in the callee). final value = defaultFunctionValue ?? defaultValue!; translator.constants.instantiateConstant(b, value, targetParamType); } } // Load named arguments (and check types if needed). final targetNamedParams = targetFunction.namedParameters; for (int i = 0; i < targetParamInfo.names.length; ++i) { final targetParamType = callTarget.signature.inputs[1 + targetParamInfo.typeParamCount + targetParamInfo.positional.length + i]; final name = targetParamInfo.names[i]; final namedParam = targetNamedParams.firstWhereOrNull( (n) => n.name == name, ); final callerIndex = callShape.named.indexOf(name); if (0 <= callerIndex) { // Provided by the caller. final paramValue = paramLocals[argNamedOffset + callerIndex]; b.local_get(paramValue); if (!translator.options.omitImplicitTypeChecks) { b.local_get(paramValue); _generateArgumentTypeCheck( name, translator.topType, namedParam!.type, ); } translator.convertType(b, paramValue.type, targetParamType); } else { // Default to use if callee has the `name` parameter. final defaultFunctionValue = (namedParam?.initializer as ConstantExpression?)?.constant; // Default to use if callee doesn't have `name` parameter. final defaultValue = targetParamInfo.named[name]; // The target wasm function corresponding to an instance method may have // a selector signature (which is based on all implementations of a // selector) and therefore may have more parameters than the actual // target needs (the others are ignored in the callee). final value = (defaultFunctionValue ?? defaultValue)!; translator.constants.instantiateConstant(b, value, targetParamType); } } final outputs = translator.callTarget(callTarget, b); if (outputs.isNotEmpty) { translator.convertType(b, outputs.single, returnType); } b.return_(); b.end(); } void _generateDynamicGetterForwarder() { final receiverLocal = paramLocals[0]; _initializeThis(reference); final member = reference.asMember; final info = translator.classInfo[member.enclosingClass]!; if (member is Field) { int fieldIndex = translator.fieldIndex[member]!; b.local_get(receiverLocal); translator.convertType(b, receiverLocal.type, info.nonNullableType); b.struct_get(info.struct, fieldIndex); translator.convertType( b, info.struct.fields[fieldIndex].type.unpacked, translator.topType, ); } else { final target = translator.getFunctionEntry( reference, uncheckedEntry: false, ); final getterWasmType = translator.signatureForDirectCall(target); final getterInputs = getterWasmType.inputs; final getterOutputs = getterWasmType.outputs; b.local_get(receiverLocal); translator.convertType(b, receiverLocal.type, getterInputs.single); call(target); translator.convertType(b, getterOutputs.single, translator.topType); } b.end(); // end function } void _generateDynamicSetterForwarder() { final receiverLocal = paramLocals[0]; final positionalArgLocal = paramLocals[1]; _initializeThis(reference); final member = reference.asMember; DartType paramType; if (member is Field) { paramType = member.type; } else { paramType = (member as Procedure).setterType; } if (!translator.options.omitImplicitTypeChecks) { b.local_get(positionalArgLocal); _generateArgumentTypeCheck( member.name.text, positionalArgLocal.type as w.RefType, paramType, ); } ClassInfo info = translator.classInfo[member.enclosingClass]!; if (member is Field) { int fieldIndex = translator.fieldIndex[member]!; b.local_get(receiverLocal); translator.convertType(b, receiverLocal.type, info.nonNullableType); b.local_get(positionalArgLocal); translator.convertType( b, positionalArgLocal.type, info.struct.fields[fieldIndex].type.unpacked, ); b.struct_set(info.struct, fieldIndex); } else { final target = translator.getFunctionEntry( reference, uncheckedEntry: false, ); final setterWasmType = translator.signatureForDirectCall(target); final setterInputs = setterWasmType.inputs; assert(setterInputs.length == 2); b.local_get(receiverLocal); translator.convertType(b, receiverLocal.type, setterInputs[0]); b.local_get(positionalArgLocal); translator.convertType(b, positionalArgLocal.type, setterInputs[1]); call(target); b.drop(); // Drop `null` from setter call. } b.end(); // end function } } /// Creates [Invocation] object based on a [CallShape]. class InvocationCreationStubGenerator implements CodeGenerator { final Translator translator; final MethodCallShape callShape; InvocationCreationStubGenerator(this.translator, this.callShape); @override void generate( w.InstructionsBuilder b, List 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 _getConstructorArgumentLocals( List typeParameters, List parameters, ) { List constructorArgs = []; for (int i = 0; i < typeParameters.length; i++) { constructorArgs.add(typeLocals[typeParameters[i]]!); } for (Variable param in parameters) { constructorArgs.add(locals[param]!); } return constructorArgs; } int _setupConstructorParameters( List typeParameters, List 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 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 ? [] : 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 = []; final superBodyArgsReversed = []; 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 getTypeFields(Class cls, ClassInfo info) { final typeFields = []; for (final typeParam in cls.typeParameters) { final match = info.typeParameterMatch[typeParam]; if (match == null) { // Type is not contained in super class' fields typeFields.add(typeLocals[typeParam]!); } } return typeFields; } void _setupDefaultFieldValues(ClassInfo info) { fieldLocals.clear(); for (Field field in info.cls!.fields) { if (field.isInstanceMember && field.initializer != null) { final source = field.enclosingComponent!.uriToSource[field.fileUri]!; final (oldSource, oldFileOffset) = setSourceMapSourceAndFileOffset( source, field.fileOffset, ); int fieldIndex = translator.fieldIndex[field]!; w.Local local = addLocal(info.struct.fields[fieldIndex].type.unpacked); translateExpression( field.initializer!, info.struct.fields[fieldIndex].type.unpacked, ); b.local_set(local); fieldLocals[field] = local; setSourceMapSourceAndFileOffset(oldSource, oldFileOffset); } } } @override void visitInvalidInitializer(InvalidInitializer node) {} @override void visitAssertInitializer(AssertInitializer node) { translateStatement(node.statement); } @override void visitLocalInitializer(LocalInitializer node) { translateVariable(node.variable); } @override void visitFieldInitializer(FieldInitializer node) { Class cls = (node.parent as Constructor).enclosingClass; w.StructType struct = translator.classInfo[cls]!.struct; Field field = node.field; int fieldIndex = translator.fieldIndex[field]!; w.Local? local = fieldLocals[field]; local ??= addLocal(struct.fields[fieldIndex].type.unpacked); translateExpression(node.value, struct.fields[fieldIndex].type.unpacked); b.local_set(local); fieldLocals[field] = local; } @override void visitRedirectingInitializer(RedirectingInitializer node) { Class cls = (node.parent as Constructor).enclosingClass; for (TypeParameter typeParam in cls.typeParameters) { types.makeType( this, TypeParameterType(typeParam, Nullability.nonNullable), ); } final targetMember = node.targetReference.asMember; final target = targetMember.initializerReference; _visitArguments( node.arguments, translator.signatureForDirectCall(target), translator.paramInfoForDirectCall(target), cls.typeParameters.length, ); b.comment("Direct call of '$targetMember Redirected Initializer'"); call(target); } @override void visitSuperInitializer(SuperInitializer node) { Supertype? supertype = (node.parent as Constructor).enclosingClass.supertype; Supertype? supersupertype = node.target.enclosingClass.supertype; // Skip calls to the constructor for Object, as this is empty if (supersupertype != null) { for (DartType typeArg in supertype!.typeArguments) { types.makeType(this, typeArg); } final targetMember = node.targetReference.asMember; final target = targetMember.initializerReference; _visitArguments( node.arguments, translator.signatureForDirectCall(target), translator.paramInfoForDirectCall(target), supertype.typeArguments.length, ); b.comment("Direct call of '$targetMember Initializer'"); call(target); } } @override w.ValueType visitVariableGet(VariableGet node, w.ValueType expectedType) { final capture = closures.captures[node.variable]; if (capture == null) { return super.visitVariableGet(node, expectedType); } // If the parameter was captured in the initializer we can load it from // initializer context. But if it will be captured in body, then we use // normal parameter here. if (capture.isInInitializer) { assert(capture.isInInitializer); return super.visitVariableGet(node, expectedType); } // Even though the parameter is captured, it's only captured in the body and // the body will put it in a newly allocated context. The initializer uses // the normal variable. final local = locals[node.variable]!; b.local_get(local); return local.type; } @override w.ValueType visitVariableSet(VariableSet node, w.ValueType expectedType) { final capture = closures.captures[node.variable]; if (capture == null) { return super.visitVariableSet(node, expectedType); } // If the parameter was captured in the initializer we can load it from // initializer context. But if it will be captured in body, then we use // normal parameter here. if (capture.isInInitializer) { return super.visitVariableSet(node, expectedType); } // Even though the parameter is captured, it's only captured in the body and // the body will put it in a newly allocated context. The initializer uses // the normal variable. final local = locals[node.variable]!; translateExpression(node.value, local.type); if (expectedType == voidMarker) { b.local_set(local); return voidMarker; } b.local_tee(local); return local.type; } } class ConstructorAllocatorCodeGenerator extends ConstructorCodeGeneratorBase { ConstructorAllocatorCodeGenerator( super.translator, super.functionType, super.member, ); @override void generateInternal() { // Closures are built when constructor functions are added to worklist. closures = translator.constructorClosures[member.reference]!; final source = member.enclosingComponent!.uriToSource[member.fileUri]!; setSourceMapSourceAndFileOffset(source, member.fileOffset); generateConstructorAllocator(); } // Generates a function for allocating an object. This calls the separate // initializer list and constructor body methods, and allocates a struct for // the object. void generateConstructorAllocator() { int parameterOffset = 0; final constructorInfo = translator.getConstructorInfo(member); _setupConstructorParameters( member.enclosingClass.typeParameters, constructorInfo.allParameters, parameterOffset, ); w.FunctionType initializerMethodType = translator.signatureForDirectCall( member.initializerReference, ); final info = translator.classInfo[member.enclosingClass]!; final List 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 = []; 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 = []; 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 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 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 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, Expression caseExpr, ) 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() => 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)), ); bool isEqualityPrimitive(Expression e) => e is ConstantExpression && (e.constant is StringConstant || e.constant is SymbolConstant) || e is StringLiteral || e is SymbolLiteral; // 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 shouldUseEquality(Expression caseExpr) => translator.typeEnvironment.isSubtypeOf( codeGen.dartTypeOf(caseExpr), translator.coreTypes.typeNonNullableRawType, ) || isEqualityPrimitive(caseExpr); void addTopTypeCompare() { compare = (switchExprLocal, pushCaseExpr, caseExpr) { if (shouldUseEquality(caseExpr)) { // Virtual call to `Object.==` for primitive types. codeGen._virtualCall( node, translator.coreTypes.objectEquals, _VirtualCallKind.Call, (functionType) { pushCaseExpr(); }, (functionType, paramInfo) { codeGen.b.local_get(switchExprLocal); }, useUncheckedEntry: false, ); } else { // Use `identical` for non-primitive types. codeGen.b.local_get(switchExprLocal); pushCaseExpr(); codeGen.call(translator.coreTypes.identicalProcedure.reference); } }; } 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 (switchExprType is DynamicType) { // Per spec, compare with ` == `. 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()) { equalsMember = translator.boxedBoolEquals; } else if (check()) { equalsMember = translator.boxedIntEquals; } else if (check()) { equalsMember = translator.stringImplEquals; } else { addTopTypeCompare(); _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()) { // 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()) { // 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 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()) { // String switch nonNullableType = translator.stringType; nullableType = translator.stringTypeNullable; compare = (switchExprLocal, pushCaseExpr, _) { codeGen.b.local_get(switchExprLocal); pushCaseExpr(); codeGen.call(translator.stringImplEquals.reference); }; } else if (switchExprClass.isEnum) { // If this is an applicable switch over enums, create a jump table. bool isValid = true; var caseMap = {}; int? 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; addTopTypeCompare(); } _initializeSpecialCases(node); } void _initializeSpecialCases(SwitchStatement node) { // Special cases defaultCase = node.cases.cast().firstWhere( (c) => c!.isDefault, orElse: () => null, ); nullCase = node.cases.cast().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 emitUnreachableIfNoResult(List 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 outputs, int n, void Function() pushBranchExpression, void Function(int) handleCase, void Function() handleDefault, ) { final done = block([], outputs); final defaultCase = block(); final labelStack = []; 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 ranges) { final rangeValues = ranges.map((r) => (range: r, value: null)).toList(); classIdSearch( 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( List<({Range range, T value})> ranges, List 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(ranges, outputs, match, miss); } else { _binaryClassIdSearch(ranges, outputs, match, miss); } } void _binaryClassIdSearch( List<({Range range, T value})> ranges, List 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( List<({Range range, T value})> ranges, List 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 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 inlineCallTo(CallTarget target) { assert(target.supportsInlining); final List inlinedLocals = target.signature.inputs .map((t) => addLocal(t)) .toList(); for (w.Local local in inlinedLocals.reversed) { local_set(local); } final w.Label callBlock = block(const [], target.signature.outputs); return withInlinedFrame(target.name, () { target.inliningCodeGen.generate(this, inlinedLocals, callBlock); return emitUnreachableIfNoResult(target.signature.outputs); }); } /// Pushes fields common to all Dart objects (class id, id hash). void pushObjectHeaderFields(Translator translator, ClassInfo classInfo) { var classId = classInfo.classId; i32_const(classId); i32_const(initialIdentityHash); } void loadClassId(Translator translator, w.ValueType receiverType) { assert(!receiverType.nullable); assert(receiverType.isSubtypeOf(translator.topTypeNonNullable)); struct_get( translator.classInfoCollector.topInfo.struct, FieldIndex.classId, ); } void fillTableRange( w.Table table, int start, int strideWidth, w.BaseFunction fun, ) { i32_const(start); ref_func(fun); i32_const(strideWidth); table_fill(table); } void fillTableRangeWithIncreasingIntegers( w.Table table, int start, int strideWidth, int startValue, ) { // index = start final index = addLocal(w.NumType.i32); i32_const(start); local_set(index); // value = startValue final value = addLocal(w.NumType.i32); i32_const(startValue); local_set(value); // while (index < (start + strideWidth)) { ... } final done = block(); final next = loop(); local_get(index); i32_const(start + strideWidth); i32_ge_u(); br_if(done); // table[index] = value local_get(index); local_get(value); i31_new(); table_set(table); // index++ local_get(index); i32_const(1); i32_add(); local_set(index); // value++ local_get(value); i32_const(1); i32_add(); local_set(value); br(next); end(); // next end(); // done } } /// A call target that may be called with a direct call or may be inlined. abstract class CallTarget { /// The wasm signature of the call target (that may be called or inlined). final w.FunctionType signature; CallTarget(this.signature); /// Whether callers should synthesize a `null` return value. bool get synthesizeNullReturnValue => false; /// Whether callee never returns and callers should emit `unreachable`. bool get synthesizeNoReturn => false; /// Whether this call target supports inlining. bool get supportsInlining => false; /// Whether we should inline (different call targets may have semantic /// knowledge about how big the body would be and whether we should inline or /// not). InliningDecision get shouldInline => InliningDecision(false, 'no CallTarget support'); /// The code generator to use for inlining the body. CodeGenerator get inliningCodeGen => throw 'No inlining support (yet).'; /// The name of this target /// /// The inliner can use this to emit comments for the inlined target. String get name; /// The wasm target function to call. /// /// This should only be accessed if caller intents to call it, as it will /// enqueue the function in the compilation queue. w.BaseFunction get function; } class AstCallTarget extends CallTarget { final Translator _translator; final Reference _reference; AstCallTarget(super.signature, this._translator, this._reference); @override bool get synthesizeNullReturnValue => _translator.synthesizeNullReturnValue(_reference); @override bool get synthesizeNoReturn => _translator.synthesizeNoReturn(_reference); @override String get name => _translator.functions.getFunctionName(_reference); @override bool get supportsInlining => _translator.supportsInlining(_reference); @override InliningDecision get shouldInline => _translator.shouldInline(_reference, signature); @override CodeGenerator get inliningCodeGen => getInlinableMemberCodeGenerator( _translator, AsyncMarker.Sync, signature, _reference, )!; @override w.BaseFunction get function => _translator.functions.getFunction(_reference); } class LambdaCallTarget extends CallTarget { final Translator _translator; final Lambda _lambda; LambdaCallTarget(super.signature, this._translator, this._lambda); @override late final String name = _translator.functions.getLambdaFunctionName(_lambda); @override late final w.BaseFunction function = _translator.functions.getLambdaFunction( _lambda, ); } /// Whether a `catch` guard has the right type to catch JS exceptions. /// /// JS exceptions are only caught as: `dynamic`, `Object`, an extension of /// `JSValue` like `JSObject`. /// /// Note that the guard type can be nullable, but the value for the exception /// needs to be non-null regardless of the guard type, as per Dart semantics. bool guardCanMatchJSException(Translator translator, DartType guard) { if (translator.options.standalone) { // Standalone mode doesn't run in a JavaScript context and doesn't have JS // exceptions. return false; } return translator.typeEnvironment.isSubtypeOf( InterfaceType(translator.jsValueClass, Nullability.nonNullable), guard.extensionTypeErasure, ); }