| // 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:math' show max; | |
| import 'package:kernel/ast.dart'; | |
| import 'package:kernel/core_types.dart'; | |
| import 'package:kernel/type_environment.dart' as type_env; | |
| import 'package:wasm_builder/wasm_builder.dart' as w; | |
| import 'class_info.dart'; | |
| import 'code_generator.dart'; | |
| import 'dispatch_table.dart' show Row, buildRowDisplacementTable; | |
| import 'translator.dart'; | |
| /// Values for the `_kind` field in `_TopType`. Must match the definitions in | |
| /// `_TopType`. | |
| class TopTypeKind { | |
| static const int objectKind = 0; | |
| static const int dynamicKind = 1; | |
| static const int voidKind = 2; | |
| } | |
| class InterfaceTypeEnvironment { | |
| final Map<TypeParameter, int> _typeOffsets = {}; | |
| void _add(InterfaceType type) { | |
| Class cls = type.classNode; | |
| int i = 0; | |
| for (TypeParameter typeParameter in cls.typeParameters) { | |
| _typeOffsets[typeParameter] = i++; | |
| } | |
| } | |
| int lookup(TypeParameter typeParameter) => _typeOffsets[typeParameter]!; | |
| } | |
| /// Helper class for building runtime types. | |
| class Types { | |
| final Translator translator; | |
| /// Class info for `_Type` | |
| late final ClassInfo typeClassInfo = | |
| translator.classInfo[translator.typeClass]!; | |
| /// Class info for `_NamedParameter` | |
| late final ClassInfo namedParameterClassInfo = | |
| translator.classInfo[translator.namedParameterClass]!; | |
| /// Wasm value type of `List<_Type>` | |
| late final w.ValueType typeListExpectedType = | |
| translator.classInfo[translator.listBaseClass]!.nonNullableType; | |
| /// Wasm array type of `WasmArray<_Type>` | |
| late final w.ArrayType typeArrayArrayType = translator.arrayTypeForDartType( | |
| typeType, | |
| mutable: true, | |
| ); | |
| /// Wasm value type of `WasmArray<_Type>` | |
| late final w.ValueType typeArrayExpectedType = w.RefType.def( | |
| typeArrayArrayType, | |
| nullable: false, | |
| ); | |
| /// Wasm value type of `WasmArray<_NamedParameter>` | |
| late final w.ValueType namedParametersExpectedType = classAndFieldToType( | |
| translator.functionTypeClass, | |
| FieldIndex.functionTypeNamedParameters, | |
| ); | |
| /// Wasm value type of `_RecordType.names` field. | |
| late final w.ValueType recordTypeNamesFieldExpectedType = classAndFieldToType( | |
| translator.recordTypeClass, | |
| FieldIndex.recordTypeNames, | |
| ); | |
| late final RuntimeTypeInformation rtt = RuntimeTypeInformation(translator); | |
| /// We will build the [interfaceTypeEnvironment] when building the | |
| /// [typeRules]. | |
| final InterfaceTypeEnvironment interfaceTypeEnvironment = | |
| InterfaceTypeEnvironment(); | |
| /// Type parameter offset for function types, specifying the lower end of | |
| /// their index range for type parameter types. | |
| Map<FunctionType, int> functionTypeParameterOffset = Map.identity(); | |
| /// Index value for function type parameter types, indexing into the type | |
| /// parameter index range of their corresponding function type. | |
| Map<StructuralParameter, int> functionTypeParameterIndex = Map.identity(); | |
| final Map<w.ModuleBuilder, AsCheckers> _asCheckers = {}; | |
| final Map<w.ModuleBuilder, IsCheckers> _isCheckers = {}; | |
| AsCheckers asCheckersForModule(w.ModuleBuilder module) { | |
| return _asCheckers[module] ??= AsCheckers(translator, rtt, module); | |
| } | |
| IsCheckers isCheckersForModule(w.ModuleBuilder module) { | |
| return _isCheckers[module] ??= IsCheckers(translator, rtt, module); | |
| } | |
| Types(this.translator); | |
| w.ValueType classAndFieldToType(Class cls, int fieldIndex) => | |
| translator.classInfo[cls]!.struct.fields[fieldIndex].type.unpacked; | |
| /// Wasm value type for non-nullable `_Type` values. | |
| w.RefType get nonNullableTypeType => typeClassInfo.nonNullableType; | |
| InterfaceType get namedParameterType => | |
| InterfaceType(translator.namedParameterClass, Nullability.nonNullable); | |
| InterfaceType get typeType => | |
| InterfaceType(translator.typeClass, Nullability.nonNullable); | |
| CoreTypes get coreTypes => translator.coreTypes; | |
| bool isTypeConstant(DartType type) { | |
| return type is DynamicType || | |
| type is VoidType || | |
| type is NeverType || | |
| type is NullType || | |
| type is FutureOrType && isTypeConstant(type.typeArgument) || | |
| (type is FunctionType && | |
| type.typeParameters.every((p) => isTypeConstant(p.bound)) && | |
| isTypeConstant(type.returnType) && | |
| type.positionalParameters.every(isTypeConstant) && | |
| type.namedParameters.every((n) => isTypeConstant(n.type))) || | |
| type is InterfaceType && type.typeArguments.every(isTypeConstant) || | |
| (type is RecordType && | |
| type.positional.every(isTypeConstant) && | |
| type.named.every((n) => isTypeConstant(n.type))) || | |
| type is StructuralParameterType || | |
| type is ExtensionType && isTypeConstant(type.extensionTypeErasure); | |
| } | |
| Class classForType(DartType type) { | |
| if (type is DynamicType) { | |
| return translator.topTypeClass; | |
| } else if (type is VoidType) { | |
| return translator.topTypeClass; | |
| } else if (type is NeverType) { | |
| return translator.bottomTypeClass; | |
| } else if (type is NullType) { | |
| return translator.bottomTypeClass; | |
| } else if (type is FutureOrType) { | |
| return translator.futureOrTypeClass; | |
| } else if (type is InterfaceType) { | |
| if (type.classNode == coreTypes.objectClass) { | |
| return translator.topTypeClass; | |
| } | |
| if (type.classNode == coreTypes.functionClass) { | |
| return translator.abstractFunctionTypeClass; | |
| } | |
| if (type.classNode == coreTypes.recordClass) { | |
| return translator.abstractRecordTypeClass; | |
| } | |
| return translator.interfaceTypeClass; | |
| } else if (type is FunctionType) { | |
| return translator.functionTypeClass; | |
| } else if (type is TypeParameterType) { | |
| return translator.interfaceTypeParameterTypeClass; | |
| } else if (type is StructuralParameterType) { | |
| return translator.functionTypeParameterTypeClass; | |
| } else if (type is ExtensionType) { | |
| return classForType(type.extensionTypeErasure); | |
| } else if (type is RecordType) { | |
| return translator.recordTypeClass; | |
| } | |
| throw "Unexpected DartType: $type"; | |
| } | |
| bool isSpecializedClass(Class cls) { | |
| return cls == coreTypes.objectClass || | |
| cls == coreTypes.functionClass || | |
| cls == coreTypes.recordClass; | |
| } | |
| int topTypeKind(DartType type) { | |
| return type is VoidType | |
| ? TopTypeKind.voidKind | |
| : type is DynamicType | |
| ? TopTypeKind.dynamicKind | |
| : TopTypeKind.objectKind; | |
| } | |
| /// Allocates a `WasmArray<_Type>` from [types] and pushes it to the | |
| /// stack. | |
| void _makeTypeArray(AstCodeGenerator codeGen, Iterable<DartType> types) { | |
| if (types.every(isTypeConstant)) { | |
| translator.constants.instantiateConstant( | |
| codeGen.b, | |
| translator.constants.makeTypeArray(types), | |
| typeArrayExpectedType, | |
| ); | |
| } else { | |
| for (DartType type in types) { | |
| makeType(codeGen, type); | |
| } | |
| codeGen.b.array_new_fixed(typeArrayArrayType, types.length); | |
| } | |
| } | |
| void _makeInterfaceType(AstCodeGenerator codeGen, InterfaceType type) { | |
| final b = codeGen.b; | |
| b.i32_const(encodedNullability(type)); | |
| b.i32_const(translator.classInfo[type.classNode]!.classId); | |
| _makeTypeArray(codeGen, type.typeArguments); | |
| } | |
| void _makeRecordType(AstCodeGenerator codeGen, RecordType type) { | |
| codeGen.b.i32_const(encodedNullability(type)); | |
| final names = translator.constants.makeArrayOf( | |
| translator.coreTypes.stringNonNullableRawType, | |
| ConstantList.mapped(type.named, (n) => StringConstant(n.name)), | |
| mutable: false, | |
| ); | |
| translator.constants.instantiateConstant( | |
| codeGen.b, | |
| names, | |
| recordTypeNamesFieldExpectedType, | |
| ); | |
| _makeTypeArray( | |
| codeGen, | |
| type.positional.followedBy(type.named.map((t) => t.type)), | |
| ); | |
| } | |
| /// Normalizes a Dart type. Many rules are already applied for us, but we | |
| /// still have to manually turn `Never?` into `Null` and normalize `FutureOr`. | |
| DartType normalize(DartType type) { | |
| if (type is NeverType && type.declaredNullability == Nullability.nullable) { | |
| return const NullType(); | |
| } | |
| if (type is! FutureOrType) return type; | |
| final s = normalize(type.typeArgument); | |
| // `coreTypes.isTop` and `coreTypes.isObject` take into account the | |
| // normalization rules of `FutureOr`. | |
| if (coreTypes.isTop(type) || coreTypes.isObject(type)) { | |
| return type.declaredNullability == Nullability.nullable | |
| ? s.withDeclaredNullability(Nullability.nullable) | |
| : s; | |
| } else if (s is NeverType) { | |
| return InterfaceType( | |
| coreTypes.futureClass, | |
| Nullability.nonNullable, | |
| DartTypeList.never1, | |
| ); | |
| } else if (s is NullType) { | |
| return InterfaceType( | |
| coreTypes.futureClass, | |
| Nullability.nullable, | |
| DartTypeList.null1, | |
| ); | |
| } | |
| // The type is normalized, and remains a `FutureOr` so now we normalize its | |
| // nullability. | |
| // Note: We diverge from the spec here and normalize the type to nullable if | |
| // its type argument is nullable, since this simplifies subtype checking. | |
| // We compensate for this difference when converting the type to a string, | |
| // making the discrepancy invisible to the user. | |
| final declaredNullability = s.nullability == Nullability.nullable | |
| ? Nullability.nullable | |
| : type.declaredNullability; | |
| return FutureOrType(s, declaredNullability); | |
| } | |
| void _makeFutureOrType(AstCodeGenerator codeGen, FutureOrType type) { | |
| final b = codeGen.b; | |
| b.i32_const(encodedNullability(type)); | |
| makeType(codeGen, type.typeArgument); | |
| codeGen.call(translator.createNormalizedFutureOrType.reference); | |
| } | |
| void _makeFunctionType(AstCodeGenerator codeGen, FunctionType type) { | |
| int typeParameterOffset = computeFunctionTypeParameterOffset(type); | |
| final b = codeGen.b; | |
| b.i32_const(encodedNullability(type)); | |
| b.i64_const(typeParameterOffset); | |
| // WasmArray<_Type> typeParameterBounds | |
| _makeTypeArray(codeGen, type.typeParameters.map((p) => p.bound)); | |
| // WasmArray<_Type> typeParameterDefaults | |
| _makeTypeArray(codeGen, type.typeParameters.map((p) => p.defaultType)); | |
| // _Type returnType | |
| makeType(codeGen, type.returnType); | |
| // WasmArray<_Type> positionalParameters | |
| _makeTypeArray(codeGen, type.positionalParameters); | |
| // int requiredParameterCount | |
| b.i64_const(type.requiredParameterCount); | |
| // WasmArray<_NamedParameter> namedParameters | |
| if (type.namedParameters.every((n) => isTypeConstant(n.type))) { | |
| translator.constants.instantiateConstant( | |
| b, | |
| translator.constants.makeNamedParametersArray(type), | |
| namedParametersExpectedType, | |
| ); | |
| } else { | |
| Class namedParameterClass = translator.namedParameterClass; | |
| Constructor namedParameterConstructor = | |
| namedParameterClass.constructors.single; | |
| List<Expression> expressions = []; | |
| for (NamedType n in type.namedParameters) { | |
| expressions.add( | |
| isTypeConstant(n.type) | |
| ? ConstantExpression( | |
| translator.constants.makeNamedParameterConstant(n), | |
| namedParameterType, | |
| ) | |
| : ConstructorInvocation( | |
| namedParameterConstructor, | |
| Arguments( | |
| ExpressionList( | |
| ConstantExpression( | |
| translator.symbols.symbolForNamedParameter(n.name), | |
| ), | |
| TypeLiteral(n.type), | |
| BoolLiteral(n.isRequired), | |
| ), | |
| ), | |
| ), | |
| ); | |
| } | |
| w.ValueType namedParametersListType = codeGen.makeArrayFromExpressions( | |
| expressions, | |
| namedParameterType, | |
| ); | |
| translator.convertType( | |
| b, | |
| namedParametersListType, | |
| namedParametersExpectedType, | |
| ); | |
| } | |
| } | |
| /// Makes a `_Type` object on the stack. | |
| /// TODO(joshualitt): Refactor this logic to remove the dependency on | |
| /// CodeGenerator. | |
| w.ValueType makeType(AstCodeGenerator codeGen, DartType type) { | |
| // Always ensure type is normalized before making a type. | |
| type = normalize(type); | |
| final b = codeGen.b; | |
| if (isTypeConstant(type)) { | |
| translator.constants.instantiateConstant( | |
| b, | |
| TypeLiteralConstant(type), | |
| nonNullableTypeType, | |
| ); | |
| return nonNullableTypeType; | |
| } | |
| // All of the singleton types represented by canonical objects should be | |
| // created const. | |
| assert( | |
| type is TypeParameterType || | |
| type is ExtensionType || | |
| type is InterfaceType || | |
| type is FutureOrType || | |
| type is FunctionType || | |
| type is RecordType, | |
| ); | |
| if (type is TypeParameterType) { | |
| codeGen.instantiateTypeParameter(type.parameter); | |
| if (type.declaredNullability == Nullability.nullable) { | |
| codeGen.call(translator.typeAsNullable.reference); | |
| } | |
| return nonNullableTypeType; | |
| } | |
| if (type is ExtensionType) { | |
| return makeType(codeGen, type.extensionTypeErasure); | |
| } | |
| ClassInfo info = translator.classInfo[classForType(type)]!; | |
| if (type is FutureOrType) { | |
| _makeFutureOrType(codeGen, type); | |
| return nonNullableTypeType; | |
| } | |
| translator.functions.recordClassAllocation(info.classId); | |
| b.pushObjectHeaderFields(translator, info); | |
| if (type is InterfaceType) { | |
| _makeInterfaceType(codeGen, type); | |
| } else if (type is FunctionType) { | |
| _makeFunctionType(codeGen, type); | |
| } else if (type is RecordType) { | |
| _makeRecordType(codeGen, type); | |
| } else { | |
| throw '`$type` should have already been handled.'; | |
| } | |
| b.struct_new(info.struct); | |
| return info.nonNullableType; | |
| } | |
| /// Compute the lower end of the type parameter index range for this function | |
| /// type. This is computed such that it avoids overlap between the index range | |
| /// of this function type and the index ranges of all generic function types | |
| /// nested within it that contain references to the type parameters of this | |
| /// function type. | |
| /// | |
| /// This will also compute the index values for all of the function's type | |
| /// parameters, which can subsequently be queried using | |
| /// [getFunctionTypeParameterIndex]. | |
| int computeFunctionTypeParameterOffset(FunctionType type) { | |
| if (type.typeParameters.isEmpty) return 0; | |
| int? offset = functionTypeParameterOffset[type]; | |
| if (offset != null) return offset; | |
| _FunctionTypeParameterOffsetCollector(this).visitFunctionType(type); | |
| return functionTypeParameterOffset[type]!; | |
| } | |
| /// Get the index value for a function type parameter, indexing into the | |
| /// type parameter index range of its corresponding function type. | |
| int getFunctionTypeParameterIndex(StructuralParameter type) { | |
| assert( | |
| functionTypeParameterIndex.containsKey(type), | |
| "Type parameter offset has not been computed for function type", | |
| ); | |
| return functionTypeParameterIndex[type]!; | |
| } | |
| /// Emit code for testing a value against a Dart type. Expects the value on | |
| /// the stack as a (ref null #Top) and leaves the result on the stack as an | |
| /// i32. | |
| void emitIsTest( | |
| AstCodeGenerator codeGen, | |
| DartType testedAgainstType, | |
| DartType operandType, [ | |
| Location? location, | |
| ]) { | |
| final b = codeGen.b; | |
| b.comment("type check against $testedAgainstType"); | |
| w.Local? operandTemp; | |
| if (translator.options.verifyTypeChecks) { | |
| operandTemp = b.addLocal(translator.topType); | |
| b.local_tee(operandTemp); | |
| } | |
| final checkOnlyNullAssignability = _requiresOnlyNullAssignabilityCheck( | |
| operandType, | |
| testedAgainstType, | |
| ); | |
| final isCheckers = isCheckersForModule(b.moduleBuilder); | |
| final (typeToCheck, :checkArguments) = isCheckers.canUseTypeCheckHelper( | |
| testedAgainstType, | |
| operandType, | |
| ); | |
| if (!checkOnlyNullAssignability && typeToCheck != null) { | |
| if (checkArguments) { | |
| for (final typeArgument in typeToCheck.typeArguments) { | |
| makeType(codeGen, typeArgument); | |
| } | |
| } | |
| b.invoke( | |
| isCheckers.generateIsChecker( | |
| typeToCheck, | |
| checkArguments, | |
| operandType.isPotentiallyNullable, | |
| ), | |
| ); | |
| } else { | |
| if (testedAgainstType is InterfaceType && | |
| classForType(testedAgainstType) == translator.interfaceTypeClass) { | |
| final typeClassInfo = | |
| translator.classInfo[testedAgainstType.classNode]!; | |
| final typeArguments = testedAgainstType.typeArguments; | |
| if (checkOnlyNullAssignability) { | |
| b.i32_const(encodedNullability(testedAgainstType)); | |
| codeGen.call(translator.isNullabilityCheck.reference); | |
| } else { | |
| b.i32_const(encodedNullability(testedAgainstType)); | |
| b.i32_const(typeClassInfo.classId); | |
| if (typeArguments.isEmpty) { | |
| codeGen.call(translator.isInterfaceSubtype0.reference); | |
| } else if (typeArguments.length == 1) { | |
| makeType(codeGen, typeArguments[0]); | |
| codeGen.call(translator.isInterfaceSubtype1.reference); | |
| } else if (typeArguments.length == 2) { | |
| makeType(codeGen, typeArguments[0]); | |
| makeType(codeGen, typeArguments[1]); | |
| codeGen.call(translator.isInterfaceSubtype2.reference); | |
| } else { | |
| _makeTypeArray(codeGen, typeArguments); | |
| codeGen.call(translator.isInterfaceSubtype.reference); | |
| } | |
| } | |
| } else { | |
| makeType(codeGen, testedAgainstType); | |
| if (checkOnlyNullAssignability) { | |
| b.struct_get( | |
| typeClassInfo.struct, | |
| translator.fieldIndex[translator.typeIsDeclaredNullableField]!, | |
| ); | |
| codeGen.call(translator.isNullabilityCheck.reference); | |
| } else { | |
| codeGen.call(translator.isSubtype.reference); | |
| } | |
| } | |
| } | |
| if (translator.options.verifyTypeChecks) { | |
| b.local_get(operandTemp!); | |
| makeType(codeGen, testedAgainstType); | |
| if (location != null) { | |
| w.FunctionType verifyFunctionType = translator.signatureForDirectCall( | |
| translator.verifyOptimizedTypeCheck.reference, | |
| ); | |
| translator.constants.instantiateConstant( | |
| b, | |
| StringConstant('$location'), | |
| verifyFunctionType.inputs.last, | |
| ); | |
| } else { | |
| b.ref_null(w.HeapType.none); | |
| } | |
| codeGen.call(translator.verifyOptimizedTypeCheck.reference); | |
| } | |
| } | |
| w.ValueType emitAsCheck( | |
| AstCodeGenerator codeGen, | |
| DartType testedAgainstType, | |
| DartType operandType, | |
| w.RefType boxedOperandType, [ | |
| Location? location, | |
| ]) { | |
| final b = codeGen.b; | |
| final checkOnlyNullAssignability = _requiresOnlyNullAssignabilityCheck( | |
| operandType, | |
| testedAgainstType, | |
| ); | |
| final asCheckers = asCheckersForModule(b.moduleBuilder); | |
| final (typeToCheck, :checkArguments) = asCheckers.canUseTypeCheckHelper( | |
| testedAgainstType, | |
| operandType, | |
| ); | |
| if (!checkOnlyNullAssignability && typeToCheck != null) { | |
| if (checkArguments) { | |
| for (final typeArgument in typeToCheck.typeArguments) { | |
| makeType(codeGen, typeArgument); | |
| } | |
| } | |
| b.invoke( | |
| asCheckers.generateAsChecker( | |
| typeToCheck, | |
| checkArguments, | |
| operandType.isPotentiallyNullable, | |
| ), | |
| ); | |
| return translator.translateType(testedAgainstType); | |
| } | |
| w.Local operand = b.addLocal(boxedOperandType); | |
| b.local_tee(operand); | |
| late List<w.ValueType> outputsToDrop; | |
| b.i32_const(encodedBool(checkOnlyNullAssignability)); | |
| if (testedAgainstType is InterfaceType && | |
| classForType(testedAgainstType) == translator.interfaceTypeClass) { | |
| final typeClassInfo = translator.classInfo[testedAgainstType.classNode]!; | |
| final typeArguments = testedAgainstType.typeArguments; | |
| b.i32_const(encodedNullability(testedAgainstType)); | |
| b.i32_const(typeClassInfo.classId); | |
| if (typeArguments.isEmpty) { | |
| outputsToDrop = codeGen.call(translator.asInterfaceSubtype0.reference); | |
| } else if (typeArguments.length == 1) { | |
| makeType(codeGen, typeArguments[0]); | |
| outputsToDrop = codeGen.call(translator.asInterfaceSubtype1.reference); | |
| } else if (typeArguments.length == 2) { | |
| makeType(codeGen, typeArguments[0]); | |
| makeType(codeGen, typeArguments[1]); | |
| outputsToDrop = codeGen.call(translator.asInterfaceSubtype2.reference); | |
| } else { | |
| _makeTypeArray(codeGen, typeArguments); | |
| outputsToDrop = codeGen.call(translator.asInterfaceSubtype.reference); | |
| } | |
| } else { | |
| makeType(codeGen, testedAgainstType); | |
| outputsToDrop = codeGen.call(translator.asSubtype.reference); | |
| } | |
| for (final _ in outputsToDrop) { | |
| b.drop(); | |
| } | |
| b.local_get(operand); | |
| return operand.type; | |
| } | |
| bool _requiresOnlyNullAssignabilityCheck( | |
| DartType operandType, | |
| DartType testedAgainstType, | |
| ) { | |
| // We may only need to check that either of these hold: | |
| // * value is non-null | |
| // * value is null and the type is nullable. | |
| return translator.typeEnvironment.isSubtypeOf( | |
| operandType, | |
| testedAgainstType.withDeclaredNullability(Nullability.nullable), | |
| ); | |
| } | |
| } | |
| abstract class _TypeCheckers { | |
| final RuntimeTypeInformation rtt; | |
| final type_env.TypeEnvironment typeEnvironment; | |
| _TypeCheckers(this.typeEnvironment, this.rtt); | |
| // If a type check helper can be used, returns the type the caller has to | |
| // check and whether arguments of the type have to be checked or not. | |
| (InterfaceType?, {bool checkArguments}) canUseTypeCheckHelper( | |
| DartType testedAgainstType, | |
| DartType operandType, | |
| ) { | |
| // The is/as check helpers are for cid-range checks of interface types. | |
| if (testedAgainstType is! InterfaceType) { | |
| return (null, checkArguments: false); | |
| } | |
| if (_hasOnlyDefaultTypeArguments(testedAgainstType)) { | |
| return (testedAgainstType, checkArguments: false); | |
| } | |
| if (operandType is InterfaceType && | |
| _staticTypesEnsureTypeArgumentsMatch(testedAgainstType, operandType)) { | |
| return ( | |
| _getTypeWithDefaultsToBounds(testedAgainstType), | |
| checkArguments: false, | |
| ); | |
| } | |
| if (!rtt.requiresTypeArgumentSubstitution(testedAgainstType.classNode)) { | |
| return (testedAgainstType, checkArguments: true); | |
| } | |
| return (null, checkArguments: false); | |
| } | |
| bool _staticTypesEnsureTypeArgumentsMatch( | |
| InterfaceType testedAgainstType, | |
| InterfaceType operandType, | |
| ) { | |
| assert(testedAgainstType.typeArguments.isNotEmpty); | |
| // If the operand type doesn't have any type arguments it will not be able | |
| // to tell us anything about the type arguments of testedAgainstType. | |
| if (operandType.typeArguments.isEmpty) return false; | |
| final sufficiency = typeEnvironment.computeTypeShapeCheckSufficiency( | |
| expressionStaticType: operandType, | |
| checkTargetType: testedAgainstType.withDeclaredNullability( | |
| Nullability.nullable, | |
| ), | |
| ); | |
| // If `true` the caller only needs to check nullabillity and the actual | |
| // concrete class, no need to check [testedAgainstType] arguments. | |
| return sufficiency == type_env.TypeShapeCheckSufficiency.interfaceShape; | |
| } | |
| bool _hasOnlyDefaultTypeArguments(InterfaceType testedAgainstType) { | |
| if (testedAgainstType.typeArguments.isEmpty) return true; | |
| final parameters = testedAgainstType.classNode.typeParameters; | |
| final arguments = testedAgainstType.typeArguments; | |
| assert(parameters.length == arguments.length); | |
| for (int i = 0; i < arguments.length; ++i) { | |
| if (arguments[i] != parameters[i].defaultType) return false; | |
| } | |
| return true; | |
| } | |
| InterfaceType _getTypeWithDefaultsToBounds(InterfaceType type) { | |
| // We only need to check whether the nullability and the class itself fits | |
| // (the [testedAgainstType] arguments are guaranteed to fit statically) | |
| final parameters = type.classNode.typeParameters; | |
| final args = DartTypeList.mapped(parameters, (p) => p.defaultType); | |
| return InterfaceType(type.classNode, type.nullability, args); | |
| } | |
| } | |
| class IsCheckers extends _TypeCheckers { | |
| final Translator translator; | |
| final w.ModuleBuilder callingModule; | |
| IsCheckers(this.translator, RuntimeTypeInformation rtt, this.callingModule) | |
| : super(translator.typeEnvironment, rtt); | |
| final Map<DartType, IsCheckerCallTarget> _nullableIsCheckers = {}; | |
| final Map<DartType, IsCheckerCallTarget> _isCheckers = {}; | |
| final Map<DartType, IsCheckerCallTarget> | |
| _nullableIsCheckersWithArgumentsCheck = {}; | |
| final Map<DartType, IsCheckerCallTarget> _isCheckersWithArgumentsCheck = {}; | |
| // Currently the is-checker helper functions only check nullability and the | |
| // concrete class (the arguments do not have to be checked). | |
| CallTarget generateIsChecker( | |
| InterfaceType testedAgainstType, | |
| bool checkArguments, | |
| bool operandIsNullable, | |
| ) { | |
| assert(_hasOnlyDefaultTypeArguments(testedAgainstType) || checkArguments); | |
| final interfaceClass = testedAgainstType.classNode; | |
| final Map<DartType, IsCheckerCallTarget> cache; | |
| final int argumentCount; | |
| if (checkArguments) { | |
| testedAgainstType = _getTypeWithDefaultsToBounds(testedAgainstType); | |
| argumentCount = interfaceClass.typeParameters.length; | |
| cache = operandIsNullable | |
| ? _nullableIsCheckersWithArgumentsCheck | |
| : _isCheckersWithArgumentsCheck; | |
| } else { | |
| argumentCount = 0; | |
| cache = operandIsNullable ? _nullableIsCheckers : _isCheckers; | |
| } | |
| return cache.putIfAbsent(testedAgainstType, () { | |
| final typeType = translator.translateType(translator.typeType); | |
| final argumentType = operandIsNullable | |
| ? translator.topType | |
| : translator.topTypeNonNullable; | |
| final signature = w.FunctionType( | |
| [argumentType, for (int i = 0; i < argumentCount; ++i) typeType], | |
| [w.NumType.i32], | |
| ); | |
| return IsCheckerCallTarget( | |
| translator, | |
| callingModule, | |
| signature, | |
| testedAgainstType, | |
| operandIsNullable, | |
| checkArguments, | |
| argumentCount, | |
| ); | |
| }); | |
| } | |
| } | |
| class AsCheckers extends _TypeCheckers { | |
| final Translator translator; | |
| final w.ModuleBuilder callingModule; | |
| AsCheckers(this.translator, RuntimeTypeInformation rtt, this.callingModule) | |
| : super(translator.typeEnvironment, rtt); | |
| final Map<DartType, AsCheckerCallTarget> _nullableAsCheckers = {}; | |
| final Map<DartType, AsCheckerCallTarget> _asCheckers = {}; | |
| final Map<DartType, AsCheckerCallTarget> _asCheckersWithArgumentsCheck = {}; | |
| final Map<DartType, AsCheckerCallTarget> | |
| _nullableAsCheckersWithArgumentsCheck = {}; | |
| CallTarget generateAsChecker( | |
| InterfaceType testedAgainstType, | |
| bool checkArguments, | |
| bool operandIsNullable, | |
| ) { | |
| assert(_hasOnlyDefaultTypeArguments(testedAgainstType) || checkArguments); | |
| final Map<DartType, AsCheckerCallTarget> cache; | |
| final int argumentCount; | |
| if (checkArguments) { | |
| testedAgainstType = _getTypeWithDefaultsToBounds(testedAgainstType); | |
| argumentCount = testedAgainstType.classNode.typeParameters.length; | |
| cache = operandIsNullable | |
| ? _nullableAsCheckersWithArgumentsCheck | |
| : _asCheckersWithArgumentsCheck; | |
| } else { | |
| argumentCount = 0; | |
| cache = operandIsNullable ? _nullableAsCheckers : _asCheckers; | |
| } | |
| return cache.putIfAbsent(testedAgainstType, () { | |
| final returnType = translator.translateType(testedAgainstType); | |
| final argumentType = operandIsNullable | |
| ? translator.topType | |
| : translator.topTypeNonNullable; | |
| final typeType = translator.translateType(translator.typeType); | |
| final signature = w.FunctionType( | |
| [argumentType, for (int i = 0; i < argumentCount; ++i) typeType], | |
| [returnType], | |
| ); | |
| return AsCheckerCallTarget( | |
| translator, | |
| callingModule, | |
| signature, | |
| testedAgainstType, | |
| operandIsNullable, | |
| checkArguments, | |
| argumentCount, | |
| ); | |
| }); | |
| } | |
| } | |
| int encodedNullability(DartType type) => | |
| type.declaredNullability == Nullability.nullable ? 1 : 0; | |
| int encodedBool(bool value) => value ? 1 : 0; | |
| class IsCheckerCallTarget extends CallTarget { | |
| final Translator translator; | |
| final w.ModuleBuilder callingModule; | |
| final InterfaceType testedAgainstType; | |
| final bool operandIsNullable; | |
| final bool checkArguments; | |
| final int argumentCount; | |
| IsCheckerCallTarget( | |
| this.translator, | |
| this.callingModule, | |
| super.signature, | |
| this.testedAgainstType, | |
| this.operandIsNullable, | |
| this.checkArguments, | |
| this.argumentCount, | |
| ); | |
| @override | |
| String get name { | |
| final typeArgumentsName = checkArguments | |
| ? '<${[for (int i = 0; i < argumentCount; ++i) 'T$i'].join(', ')}>' | |
| : ''; | |
| return '<obj> is ${testedAgainstType.classNode.name}$typeArgumentsName'; | |
| } | |
| @override | |
| bool get supportsInlining => true; | |
| @override | |
| InliningDecision get shouldInline { | |
| if (checkArguments) return InliningDecision(false, 'checkArguments'); | |
| final interfaceClass = testedAgainstType.classNode; | |
| // Can emit a single class-id range check for those, so we prefer to inline | |
| // them. | |
| if (interfaceClass == translator.coreTypes.objectClass) { | |
| return InliningDecision(true, 'is Object'); | |
| } | |
| if (interfaceClass == translator.coreTypes.functionClass) { | |
| return InliningDecision(true, 'is Function'); | |
| } | |
| // Checking the receiver for null emits more code (block, save receiver to | |
| // local, conditional branch) - so it's likely to regress size. | |
| if (operandIsNullable) return InliningDecision(false, 'operandIsNullable'); | |
| // Always inline single class-id range checks (no branching, simply loads, | |
| // arithmetic and unsigned compare). | |
| final ranges = translator.classIdNumbering.getConcreteClassIdRange( | |
| interfaceClass, | |
| ); | |
| return InliningDecision(ranges.length <= 1, 'ranges <= 1'); | |
| } | |
| @override | |
| CodeGenerator get inliningCodeGen => IsCheckerCodeGenerator( | |
| translator, | |
| testedAgainstType, | |
| operandIsNullable, | |
| checkArguments, | |
| argumentCount, | |
| ); | |
| @override | |
| late final w.BaseFunction function = (() { | |
| final builder = callingModule.functions.define( | |
| translator.typesBuilder.defineFunction( | |
| signature.inputs, | |
| signature.outputs, | |
| ), | |
| name, | |
| )..isPure = true; | |
| translator.compilationQueue.add(CompilationTask(builder, inliningCodeGen)); | |
| return builder.function; | |
| })(); | |
| } | |
| class IsCheckerCodeGenerator implements CodeGenerator { | |
| final Translator translator; | |
| final InterfaceType testedAgainstType; | |
| final bool operandIsNullable; | |
| final bool checkArguments; | |
| final int argumentCount; | |
| IsCheckerCodeGenerator( | |
| this.translator, | |
| this.testedAgainstType, | |
| this.operandIsNullable, | |
| this.checkArguments, | |
| this.argumentCount, | |
| ); | |
| @override | |
| void generate( | |
| w.InstructionsBuilder b, | |
| List<w.Local> paramLocals, | |
| w.Label? returnLabel, | |
| ) { | |
| final interfaceClass = testedAgainstType.classNode; | |
| w.Local operand = paramLocals[0]; | |
| w.Local boolTemp = b.addLocal(w.NumType.i32); | |
| final w.Label resultLabel = b.block(const [], const [w.NumType.i32]); | |
| if (operandIsNullable) { | |
| w.Label nullLabel = b.block(const [], const []); | |
| b.local_get(operand); | |
| b.br_on_null(nullLabel); | |
| final nonNullableOperand = b.addLocal(translator.topTypeNonNullable); | |
| b.local_get(operand); | |
| b.ref_cast(nonNullableOperand.type as w.RefType); | |
| b.local_set(nonNullableOperand); | |
| operand = nonNullableOperand; | |
| } | |
| if (checkArguments) { | |
| b.local_get(operand); | |
| // We have one is checker helper function per [testedAgainstType]. All `is` | |
| // expressions that test against the same type will use this shared helper. | |
| // => Inline the argument checking code here as it will not cause | |
| // meaningful increases in code size for application but will be good for | |
| // performance. | |
| const bool forceInline = true; | |
| b.invoke( | |
| translator.types | |
| .isCheckersForModule(b.moduleBuilder) | |
| .generateIsChecker(testedAgainstType, false, false), | |
| forceInline: forceInline, | |
| ); | |
| b.local_set(boolTemp); | |
| // If cid ranges fail, we fail | |
| { | |
| final w.Label okBlock = b.block(const [], const []); | |
| b.local_get(boolTemp); | |
| b.i32_const(1); | |
| b.i32_eq(); | |
| b.br_if(okBlock); | |
| b.i32_const(0); | |
| b.br(resultLabel); | |
| b.end(); | |
| } | |
| // Otherwise we have to check each argument. | |
| // Call Object._getArguments() | |
| w.Local typeArguments = b.addLocal( | |
| translator.types.typeArrayExpectedType, | |
| ); | |
| b.local_get(operand); | |
| translator.callReference(translator.objectGetTypeArguments.reference, b); | |
| b.local_set(typeArguments); | |
| for (int i = 0; i < argumentCount; ++i) { | |
| b.local_get(typeArguments); | |
| b.i32_const(i); | |
| b.array_get(translator.types.typeArrayArrayType); | |
| b.local_get(paramLocals[1 + i]); | |
| translator.callReference(translator.isTypeSubtype.reference, b); | |
| { | |
| b.local_set(boolTemp); | |
| final w.Label okBlock = b.block(const [], const []); | |
| b.local_get(boolTemp); | |
| b.i32_const(1); | |
| b.i32_eq(); | |
| b.br_if(okBlock); | |
| b.i32_const(0); | |
| b.br(resultLabel); | |
| b.end(); | |
| } | |
| } | |
| b.i32_const(1); | |
| b.br(resultLabel); | |
| } else { | |
| if (interfaceClass == translator.coreTypes.objectClass) { | |
| b.drop(); | |
| b.i32_const(1); | |
| } else if (interfaceClass == translator.coreTypes.functionClass) { | |
| b.local_get(operand); | |
| b.ref_test(translator.closureInfo.nonNullableType); | |
| } else { | |
| final ranges = translator.classIdNumbering.getConcreteClassIdRange( | |
| interfaceClass, | |
| ); | |
| b.local_get(operand); | |
| b.loadClassId(translator, operand.type); | |
| b.emitClassIdRangeCheck(ranges); | |
| } | |
| b.br(resultLabel); | |
| } | |
| if (operandIsNullable) { | |
| b.end(); // nullLabel | |
| b.i32_const(encodedNullability(testedAgainstType)); | |
| } | |
| b.end(); // resultLabel | |
| if (returnLabel != null) { | |
| b.br(returnLabel); | |
| } else { | |
| b.return_(); | |
| } | |
| b.end(); | |
| } | |
| } | |
| class AsCheckerCallTarget extends CallTarget { | |
| final Translator translator; | |
| final w.ModuleBuilder callingModule; | |
| final InterfaceType testedAgainstType; | |
| final bool operandIsNullable; | |
| final bool checkArguments; | |
| final int argumentCount; | |
| AsCheckerCallTarget( | |
| this.translator, | |
| this.callingModule, | |
| super.signature, | |
| this.testedAgainstType, | |
| this.operandIsNullable, | |
| this.checkArguments, | |
| this.argumentCount, | |
| ); | |
| @override | |
| String get name { | |
| final typeArgumentsName = checkArguments | |
| ? '<${[for (int i = 0; i < argumentCount; ++i) 'T$i'].join(', ')}>' | |
| : ''; | |
| return '<obj> as ${testedAgainstType.classNode.name}$typeArgumentsName'; | |
| } | |
| @override | |
| late final w.BaseFunction function = (() { | |
| final builder = callingModule.functions.define( | |
| translator.typesBuilder.defineFunction( | |
| signature.inputs, | |
| signature.outputs, | |
| ), | |
| name, | |
| ); | |
| translator.compilationQueue.add( | |
| CompilationTask( | |
| builder, | |
| AsCheckerCodeGenerator( | |
| translator, | |
| signature, | |
| testedAgainstType, | |
| operandIsNullable, | |
| checkArguments, | |
| argumentCount, | |
| ), | |
| ), | |
| ); | |
| return builder.function; | |
| })(); | |
| } | |
| class AsCheckerCodeGenerator implements CodeGenerator { | |
| final Translator translator; | |
| final w.FunctionType signature; | |
| final InterfaceType testedAgainstType; | |
| final bool operandIsNullable; | |
| final bool checkArguments; | |
| final int argumentCount; | |
| AsCheckerCodeGenerator( | |
| this.translator, | |
| this.signature, | |
| this.testedAgainstType, | |
| this.operandIsNullable, | |
| this.checkArguments, | |
| this.argumentCount, | |
| ); | |
| @override | |
| void generate( | |
| w.InstructionsBuilder b, | |
| List<w.Local> paramLocals, | |
| w.Label? returnLabel, | |
| ) { | |
| assert(returnLabel == null); | |
| w.Label asCheckBlock = b.block(); | |
| b.local_get(b.locals[0]); | |
| for (int i = 0; i < argumentCount; ++i) { | |
| b.local_get(b.locals[1 + i]); | |
| } | |
| // We have one as checker helper function per [testedAgainstType]. All `as` | |
| // expressions that test against the same type will use this shared helper. | |
| // => Inline the is checking code here as it will not cause meaningful | |
| // increases in code size for the application but will be good for | |
| // performance. | |
| const bool forceInline = true; | |
| b.invoke( | |
| translator.types | |
| .isCheckersForModule(b.moduleBuilder) | |
| .generateIsChecker( | |
| testedAgainstType, | |
| checkArguments, | |
| operandIsNullable, | |
| ), | |
| forceInline: forceInline, | |
| ); | |
| b.br_if(asCheckBlock); | |
| if (translator.options.minify) { | |
| translator.callReference( | |
| translator.throwErrorWithoutDetails.reference, | |
| b, | |
| ); | |
| } else { | |
| if (checkArguments) { | |
| final testedAgainstClassId = | |
| translator.classInfo[testedAgainstType.classNode]!.classId; | |
| b.local_get(b.locals[0]); | |
| b.i32_const(encodedNullability(testedAgainstType)); | |
| b.i32_const(testedAgainstClassId); | |
| if (argumentCount == 1) { | |
| b.local_get(b.locals[1]); | |
| translator.callReference( | |
| translator.throwInterfaceTypeAsCheckError1.reference, | |
| b, | |
| ); | |
| } else if (argumentCount == 2) { | |
| b.local_get(b.locals[1]); | |
| b.local_get(b.locals[2]); | |
| translator.callReference( | |
| translator.throwInterfaceTypeAsCheckError2.reference, | |
| b, | |
| ); | |
| } else { | |
| for (int i = 0; i < argumentCount; ++i) { | |
| b.local_get(b.locals[1 + i]); | |
| } | |
| b.array_new_fixed(translator.types.typeArrayArrayType, argumentCount); | |
| translator.callReference( | |
| translator.throwInterfaceTypeAsCheckError.reference, | |
| b, | |
| ); | |
| } | |
| } else { | |
| b.local_get(b.locals[0]); | |
| translator.constants.instantiateConstant( | |
| b, | |
| TypeLiteralConstant(testedAgainstType), | |
| translator.types.nonNullableTypeType, | |
| ); | |
| translator.callReference(translator.throwAsCheckError.reference, b); | |
| } | |
| } | |
| b.unreachable(); | |
| b.end(); // asCheckBlock | |
| b.local_get(b.locals[0]); | |
| translator.convertType(b, signature.inputs.first, signature.outputs.single); | |
| b.return_(); | |
| b.end(); | |
| } | |
| } | |
| /// Builds up data structures that the Runtime Type System implementation uses. | |
| /// | |
| /// There are 2 data structures: | |
| /// | |
| /// * The name of all classes represented as an wasm array of strings. | |
| /// | |
| /// * A type row-displacement table encoding whether a class is a subclass of | |
| /// another class and if so, how to translate the type arguments from a | |
| /// subclass to a super clss. | |
| /// | |
| /// See sdk/lib/_internal/wasm/lib/type.dart for more information. | |
| class RuntimeTypeInformation { | |
| final Translator translator; | |
| /// This index in the substitution canonicalization table indicates that we do | |
| /// not have to substitute anything. | |
| static const int noSubstitutionIndex = 0; | |
| CoreTypes get coreTypes => translator.coreTypes; | |
| Types get types => translator.types; | |
| late final Map<int, Map<int, int>> _substitutionSubclassToSuperclass; | |
| late final Map<int, Map<int, int>> _substitutionSuperclassToSubclass; | |
| late final Map<InstanceConstant, int> _substitutionTable; | |
| late final List<InstanceConstant> _substitutionTableByIndex; | |
| late final Constant typeRowDisplacementOffsets; | |
| late final Constant typeRowDisplacementTable; | |
| late final Constant typeRowDisplacementSubstTable; | |
| late final Constant canonicalSubstitutionTable; | |
| late final Constant typeNames; | |
| /// Object containing RTT info for the main module. See | |
| /// sdk/lib/_internal/wasm/lib/type.dart for what this contains and how it's | |
| /// used. | |
| final Map<int, bool> _requiresSubstitutionForSubclasses = {}; | |
| RuntimeTypeInformation(this.translator) { | |
| _buildTypeRules(); | |
| _setupRtt(); | |
| } | |
| bool requiresTypeArgumentSubstitution(Class superclass) { | |
| final id = translator.classIdNumbering.classIds[superclass]!; | |
| return _requiresSubstitutionForSubclasses.putIfAbsent(id, () { | |
| final subclassSubstitutions = _substitutionSuperclassToSubclass[id]; | |
| if (subclassSubstitutions == null) return false; | |
| for (final entry in subclassSubstitutions.entries) { | |
| final substitutionIndex = entry.value; | |
| if (substitutionIndex != noSubstitutionIndex) return true; | |
| } | |
| return false; | |
| }); | |
| } | |
| void _buildTypeRules() { | |
| _substitutionSubclassToSuperclass = <int, Map<int, int>>{}; | |
| _substitutionSuperclassToSubclass = <int, Map<int, int>>{}; | |
| _substitutionTable = <InstanceConstant, int>{}; | |
| _substitutionTableByIndex = <InstanceConstant>[]; | |
| assert(noSubstitutionIndex == 0); | |
| assert(_substitutionTable.length == noSubstitutionIndex); | |
| assert(_substitutionTableByIndex.length == noSubstitutionIndex); | |
| final noSubstitution = translator.constants.makeTypeArray([]); | |
| _substitutionTable[noSubstitution] = noSubstitutionIndex; | |
| _substitutionTableByIndex.add(noSubstitution); | |
| for (ClassInfo classInfo in translator.classes) { | |
| ClassInfo superclassInfo = classInfo; | |
| // We don't need type rules for any class without a superclass, or for | |
| // classes whose supertype is [Object]. The latter case will be handled | |
| // directly in the subtype checking algorithm. | |
| if (superclassInfo.cls == null || | |
| superclassInfo.cls == coreTypes.objectClass) { | |
| continue; | |
| } | |
| Class superclass = superclassInfo.cls!; | |
| assert(!superclass.isAnonymousMixin); | |
| // TODO(joshualitt): This includes abstract types that can't be | |
| // instantiated, but might be needed for subtype checks. The majority of | |
| // abstract classes are probably unnecessary though. We should filter | |
| // these cases to reduce the size of the type rules. | |
| Iterable<Class> subclasses = translator.subtypes | |
| .getSubtypesOf(superclass) | |
| .where((cls) => cls != superclass); | |
| Iterable<InterfaceType> subtypes = subclasses.map( | |
| (Class cls) => cls.getThisType(coreTypes, Nullability.nonNullable), | |
| ); | |
| for (InterfaceType subtype in subtypes) { | |
| if (subtype.classNode.isAnonymousMixin) continue; | |
| types.interfaceTypeEnvironment._add(subtype); | |
| final List<DartType>? typeArguments = translator.hierarchy | |
| .getInterfaceTypeArgumentsAsInstanceOfClass(subtype, superclass) | |
| ?.map(types.normalize) | |
| .toList(); | |
| int substitutionIndex; | |
| if (_isIdentitySubstitution(typeArguments)) { | |
| substitutionIndex = noSubstitutionIndex; | |
| } else { | |
| final substitution = translator.constants.makeTypeArray( | |
| typeArguments!, | |
| ); | |
| int? index = _substitutionTable[substitution]; | |
| if (index == null) { | |
| assert( | |
| _substitutionTableByIndex.length == _substitutionTable.length, | |
| ); | |
| index = _substitutionTableByIndex.length; | |
| _substitutionTableByIndex.add(substitution); | |
| _substitutionTable[substitution] = index; | |
| } | |
| substitutionIndex = index; | |
| } | |
| final subclassId = translator.classInfo[subtype.classNode]!.classId; | |
| final superclassId = superclassInfo.classId; | |
| (_substitutionSubclassToSuperclass[subclassId] ??= {})[superclassId] = | |
| substitutionIndex; | |
| (_substitutionSuperclassToSubclass[superclassId] ??= {})[subclassId] = | |
| substitutionIndex; | |
| } | |
| } | |
| } | |
| /// Whether the substitution [typeArguments] would cause a NOP substitution. | |
| /// | |
| /// We have a NOP substitution if one of the following conditions apply: | |
| /// | |
| /// - the classes are unrelated | |
| /// - the super class is not generic | |
| /// - the type arguments from subclass are the same as for the super class | |
| /// | |
| bool _isIdentitySubstitution(List<DartType>? typeArguments) { | |
| // This happen if the classes are not related to each other. | |
| if (typeArguments == null) return true; | |
| for (int i = 0; i < typeArguments.length; ++i) { | |
| final typeArgument = typeArguments[i]; | |
| if (typeArgument is! TypeParameterType) { | |
| return false; | |
| } | |
| if (typeArgument.declaredNullability == Nullability.nullable) { | |
| return false; | |
| } | |
| final int environmentIndex = types.interfaceTypeEnvironment.lookup( | |
| typeArgument.parameter, | |
| ); | |
| if (i != environmentIndex) { | |
| return false; | |
| } | |
| } | |
| return true; | |
| } | |
| void _setupRtt() { | |
| final rowForSuperclass = List<Row?>.filled(translator.classes.length, null); | |
| final rows = <Row<(int, int)>>[]; | |
| final ranges = _buildRanges(_substitutionSuperclassToSubclass); | |
| ranges.forEach((int superId, List<(Range, int)> subs) { | |
| if (subs.isEmpty) return; | |
| final rowEntries = <({int index, (int, int) value})>[]; | |
| for (final (Range range, int substitutionIndex) in subs) { | |
| for (int classId = range.start; classId <= range.end; ++classId) { | |
| rowEntries.add((index: classId, value: (superId, substitutionIndex))); | |
| } | |
| } | |
| final row = Row<(int, int)>(rowEntries); | |
| rows.add(row); | |
| rowForSuperclass[superId] = row; | |
| }); | |
| final typeType = InterfaceType( | |
| translator.typeClass, | |
| Nullability.nonNullable, | |
| ); | |
| final arrayOfType = InterfaceType( | |
| translator.wasmArrayClass, | |
| Nullability.nonNullable, | |
| DartTypeList(typeType), | |
| ); | |
| final wasmI16 = InterfaceType( | |
| translator.wasmI16Class, | |
| Nullability.nonNullable, | |
| ); | |
| final wasmI32 = InterfaceType( | |
| translator.wasmI32Class, | |
| Nullability.nonNullable, | |
| ); | |
| final maxId = translator.classIdNumbering.maxClassId; | |
| int normalize(int value) => (100 * value) ~/ maxId; | |
| int weight(Row row) { | |
| return normalize(row.values.length) + normalize(row.holes); | |
| } | |
| rows.sort((Row a, Row b) => -weight(a).compareTo(weight(b))); | |
| final table = buildRowDisplacementTable(rows, firstAvailable: 1); | |
| const invalidClassId = 0; | |
| final typeRowDisplacementTable = translator.constants.makeArrayOf( | |
| wasmI32, | |
| ConstantList.mapped( | |
| table, | |
| (entry) => translator.constants.makeWasmI32( | |
| entry == null ? invalidClassId : entry.$1, | |
| ), | |
| ), | |
| ); | |
| final typeRowDisplacementSubstTable = translator.constants.makeArrayOf( | |
| wasmI16, | |
| ConstantList.mapped( | |
| table, | |
| (entry) => translator.constants.makeWasmI32( | |
| entry == null ? noSubstitutionIndex : entry.$2, | |
| ), | |
| ), | |
| ); | |
| final canonicalSubstitutionTable = translator.constants.makeArrayOf( | |
| arrayOfType, | |
| ConstantList.from(_substitutionTableByIndex), | |
| ); | |
| final typeRowDisplacementOffsets = translator.constants.makeArrayOf( | |
| wasmI32, | |
| ConstantList.mapped( | |
| rowForSuperclass, | |
| (row) => translator.constants.makeWasmI32(row?.offset ?? -1), | |
| ), | |
| ); | |
| final typeNames = translator.options.minify | |
| ? NullConstant() | |
| : _getTypeNames(); | |
| this.typeRowDisplacementOffsets = typeRowDisplacementOffsets; | |
| this.typeRowDisplacementTable = typeRowDisplacementTable; | |
| this.typeRowDisplacementSubstTable = typeRowDisplacementSubstTable; | |
| this.canonicalSubstitutionTable = canonicalSubstitutionTable; | |
| this.typeNames = typeNames; | |
| } | |
| InstanceConstant _getTypeNames() { | |
| final stringType = translator.coreTypes.stringRawType( | |
| Nullability.nonNullable, | |
| ); | |
| final emptyString = StringConstant(''); | |
| final nameConstants = ConstantList.mapped(translator.classes, (classInfo) { | |
| Class? cls = classInfo.cls; | |
| if (cls == null || cls.isAnonymousMixin) { | |
| return emptyString; | |
| } else { | |
| return StringConstant(cls.name); | |
| } | |
| }); | |
| return translator.constants.makeArrayOf(stringType, nameConstants); | |
| } | |
| Map<int, List<(Range, int)>> _buildRanges(Map<int, Map<int, int>> map) { | |
| final rangeValues = <int, List<(Range, int)>>{}; | |
| map.forEach((int id, Map<int, int> subs) { | |
| final entries = subs.entries | |
| .map((entry) => (Range(entry.key, entry.key), entry.value)) | |
| .toList(); | |
| entries.sort((a, b) => a.$1.start.compareTo(b.$1.start)); | |
| int writeIndex = 0; | |
| for (int readIndex = 1; readIndex < entries.length; ++readIndex) { | |
| final current = entries[writeIndex]; | |
| final next = entries[readIndex]; | |
| if (current.$2 == next.$2 && (current.$1.end + 1) == next.$1.start) { | |
| entries[writeIndex] = ( | |
| Range(current.$1.start, next.$1.end), | |
| current.$2, | |
| ); | |
| continue; | |
| } | |
| entries[++writeIndex] = next; | |
| } | |
| entries.length = writeIndex + 1; | |
| rangeValues[id] = entries; | |
| }); | |
| return rangeValues; | |
| } | |
| } | |
| /// For a function type F = `... Function<X0, ..., Xn-1>(...)` compute offset(F) | |
| /// such that for any function type G = `... Function<Y0, ..., Ym-1>(...)` | |
| /// nested inside F, if G contains a reference to any type parameters of F, then | |
| /// offset(F) >= offset(G) + m. | |
| /// | |
| /// Conceptually, the type parameters of F are indexed from offset(F) inclusive | |
| /// to offset(F) + n exclusive. | |
| /// | |
| /// Also assign to each type parameter Xi the index offset(F) + i such that it | |
| /// indexes the correct type parameter in the conceptual type parameter index | |
| /// range of F. | |
| /// | |
| /// This ensures that for every reference to a type parameter, its corresponding | |
| /// function type is the innermost function type enclosing it for which the | |
| /// index falls within the type parameter index range of the function type. | |
| class _FunctionTypeParameterOffsetCollector extends RecursiveVisitor { | |
| final Types types; | |
| final List<FunctionType> _functionStack = []; | |
| final List<Set<FunctionType>> _functionsContainingParameters = []; | |
| final Map<StructuralParameter, int> _functionForParameter = {}; | |
| _FunctionTypeParameterOffsetCollector(this.types); | |
| @override | |
| void visitFunctionType(FunctionType node) { | |
| int slot = _functionStack.length; | |
| _functionStack.add(node); | |
| _functionsContainingParameters.add({}); | |
| for (int i = 0; i < node.typeParameters.length; i++) { | |
| StructuralParameter parameter = node.typeParameters[i]; | |
| _functionForParameter[parameter] = slot; | |
| } | |
| super.visitFunctionType(node); | |
| int offset = 0; | |
| for (FunctionType inner in _functionsContainingParameters.last) { | |
| offset = max( | |
| offset, | |
| types.functionTypeParameterOffset[inner]! + inner.typeParameters.length, | |
| ); | |
| } | |
| types.functionTypeParameterOffset[node] = offset; | |
| for (int i = 0; i < node.typeParameters.length; i++) { | |
| StructuralParameter parameter = node.typeParameters[i]; | |
| types.functionTypeParameterIndex[parameter] = offset + i; | |
| } | |
| _functionsContainingParameters.removeLast(); | |
| _functionStack.removeLast(); | |
| } | |
| @override | |
| void visitStructuralParameterType(StructuralParameterType node) { | |
| int slot = _functionForParameter[node.parameter]!; | |
| for (int inner = slot + 1; inner < _functionStack.length; inner++) { | |
| _functionsContainingParameters[slot].add(_functionStack[inner]); | |
| } | |
| } | |
| } | |
| extension InstanceConstantInterfaceType on InstanceConstant { | |
| InterfaceType get interfaceType => | |
| InterfaceType(classNode, Nullability.nonNullable, typeArguments); | |
| } |