| Index: pkg/kernel/lib/transformations/insert_covariance_checks.dart
|
| diff --git a/pkg/kernel/lib/transformations/insert_covariance_checks.dart b/pkg/kernel/lib/transformations/insert_covariance_checks.dart
|
| new file mode 100644
|
| index 0000000000000000000000000000000000000000..b4e1869830dbe3e57e28671fe510cd38a90f986e
|
| --- /dev/null
|
| +++ b/pkg/kernel/lib/transformations/insert_covariance_checks.dart
|
| @@ -0,0 +1,516 @@
|
| +// Copyright (c) 2016, 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.
|
| +library kernel.transformations.insert_covariance_checks;
|
| +
|
| +import '../class_hierarchy.dart';
|
| +import '../clone.dart';
|
| +import '../core_types.dart';
|
| +import '../kernel.dart';
|
| +import '../log.dart';
|
| +import '../type_algebra.dart';
|
| +import '../type_environment.dart';
|
| +
|
| +// TODO: Should helper be removed?
|
| +DartType substituteBounds(DartType type, Map<TypeParameter, DartType> upper,
|
| + Map<TypeParameter, DartType> lower) {
|
| + return Substitution
|
| + .fromUpperAndLowerBounds(upper, lower)
|
| + .substituteType(type);
|
| +}
|
| +
|
| +/// Inserts checked entry points for methods in order to enforce type safety
|
| +/// in face on covariant subtyping.
|
| +///
|
| +/// An 'unsafe parameter' is a parameter whose type mentions a class type
|
| +/// parameter T, but is not contravariant in T. For instance, the argument
|
| +/// to `List.add` is unsafe, whereas the function parameter to `List.forEach`
|
| +/// is safe:
|
| +///
|
| +/// class List<T> {
|
| +/// ...
|
| +/// void add(T x) {...} // unsafe
|
| +/// void forEach(void action(T x)) {...} // safe
|
| +/// }
|
| +///
|
| +/// For every method with unsafe parameters, a checked entry point suffixed
|
| +/// with `$cc` is inserted, which casts the unsafe parameters to their expected
|
| +/// types and calls the actual implementation:
|
| +///
|
| +/// class List<T> {
|
| +/// ...
|
| +/// void add$cc(Object x) => this.add(x as T);
|
| +/// }
|
| +///
|
| +/// Calls whose interface target declares unsafe parameters are then rewritten
|
| +/// to target the `$cc` entry point instead, unless it can be determined that
|
| +/// the type argument is exact. For example:
|
| +///
|
| +/// void foo(List<num> numbers) {
|
| +/// numbers.add(3.5); // before
|
| +/// numbers.add$cc(3.5); // after
|
| +/// }
|
| +///
|
| +/// Currently, we only deduce that the type arguments are exact when the
|
| +/// receiver is `this`.
|
| +class InsertCovarianceChecks {
|
| + ClassHierarchy hierarchy;
|
| + CoreTypes coreTypes;
|
| + TypeEnvironment types;
|
| +
|
| + /// Maps unsafe members to their checked entry point, to be used at call sites
|
| + /// where the arguments cannot be guaranteed to satisfy the generic parameter
|
| + /// types of the actual target.
|
| + final Map<Member, Procedure> unsafeMemberEntryPoint = <Member, Procedure>{};
|
| +
|
| + /// Members that may be invoked through a checked entry point.
|
| + ///
|
| + /// Note that these members are not necessarily unsafe, because a safe member
|
| + /// can override an unsafe member, and thereby be invoked through a checked
|
| + /// entry point. This set is not therefore not the same as the set of keys
|
| + /// in [unsafeMemberEntryPoint].
|
| + final Set<Member> membersWithCheckedEntryPoint = new Set<Member>();
|
| +
|
| + InsertCovarianceChecks({this.hierarchy, this.coreTypes});
|
| +
|
| + void transformProgram(Program program) {
|
| + hierarchy ??= new ClassHierarchy(program);
|
| + coreTypes ??= new CoreTypes(program);
|
| + types = new TypeEnvironment(coreTypes, hierarchy);
|
| + // We transform every class before their subtypes.
|
| + // This ensures that transitive overrides are taken into account.
|
| + hierarchy.classes.forEach(transformClass);
|
| +
|
| + program.accept(new _CallTransformer(this));
|
| + }
|
| +
|
| + void transformClass(Class class_) {
|
| + new _ClassTransformer(class_, this).transformClass();
|
| + }
|
| +}
|
| +
|
| +class _ClassTransformer {
|
| + final Class host;
|
| + final ClassHierarchy hierarchy;
|
| + final TypeEnvironment types;
|
| + final InsertCovarianceChecks global;
|
| +
|
| + final Map<Field, VariableDeclaration> fieldSetterParameter =
|
| + <Field, VariableDeclaration>{};
|
| +
|
| + final Map<VariableDeclaration, List<DartType>> unsafeParameterTypes =
|
| + new Map<VariableDeclaration, List<DartType>>();
|
| +
|
| + // The following four maps translate types from the context of a supertype
|
| + // into the context of the current class.
|
| + //
|
| + // When analyzing an override relation "ownMember <: superMember", the two
|
| + // "own" maps translate types from the context of the ownMember, while the
|
| + // "super" maps translate types from the context of superMember.
|
| + //
|
| + // The "substitution" maps translate type parameters to their exact type,
|
| + // while the "upper bound" maps translate type parameters to their erased
|
| + // upper bounds.
|
| + Map<TypeParameter, DartType> ownSubstitution;
|
| + Map<TypeParameter, DartType> ownUpperBounds;
|
| + Map<TypeParameter, DartType> superSubstitution;
|
| + Map<TypeParameter, DartType> superUpperBounds;
|
| +
|
| + /// Members for which a checked entry point must be created in this current
|
| + /// class.
|
| + Set<Member> membersNeedingCheckedEntryPoint = new Set<Member>();
|
| +
|
| + _ClassTransformer(this.host, InsertCovarianceChecks global)
|
| + : hierarchy = global.hierarchy,
|
| + types = global.types,
|
| + this.global = global;
|
| +
|
| + /// Mark [parameter] unsafe, with [type] as a potential argument type.
|
| + void addUnsafeParameter(
|
| + VariableDeclaration parameter, DartType type, Member member) {
|
| + unsafeParameterTypes.putIfAbsent(parameter, () => <DartType>[]).add(type);
|
| + requireLocalCheckedEntryPoint(member);
|
| + }
|
| +
|
| + /// Get a parameter representing the argument to the implicit setter
|
| + /// for [field].
|
| + VariableDeclaration getFieldSetterParameter(Field field) {
|
| + return fieldSetterParameter.putIfAbsent(field, () {
|
| + return new VariableDeclaration('${field.name.name}_', type: field.type);
|
| + });
|
| + }
|
| +
|
| + /// Mark [field] as unsafe, with [type] as a potential argument to its setter.
|
| + void addUnsafeField(Field field, DartType type) {
|
| + addUnsafeParameter(getFieldSetterParameter(field), type, field);
|
| + }
|
| +
|
| + /// True if [member] can be invoked through a checked entry point.
|
| + ///
|
| + /// This does not imply that the member has unsafe parameters.
|
| + bool hasCheckedEntryPoint(Member member, {bool setter: false}) {
|
| + if (!setter && member is Field) {
|
| + return false; // Field getters never have checked entry points.
|
| + }
|
| + return global.membersWithCheckedEntryPoint.contains(member);
|
| + }
|
| +
|
| + /// Ensures that a checked entry point for [member] will be emitted in the
|
| + /// current class.
|
| + void requireLocalCheckedEntryPoint(Member member) {
|
| + if (membersNeedingCheckedEntryPoint.add(member)) {
|
| + global.membersWithCheckedEntryPoint.add(member);
|
| + }
|
| + }
|
| +
|
| + void transformClass() {
|
| + if (host.isMixinApplication) {
|
| + // TODO(asgerf): We need a way to support mixin applications with unsafe
|
| + // overrides. This version assumes mixins have been resolved by cloning.
|
| + // We could generate a subclass of the mixin application containing the
|
| + // checked entry points.
|
| + throw 'Mixin applications must be resolved before inserting covariance '
|
| + 'checks';
|
| + }
|
| + // Find parameters with an unsafe reference to a class type parameter.
|
| + if (host.typeParameters.isNotEmpty) {
|
| + var upperBounds = getUpperBoundSubstitutionMap(host);
|
| + for (var field in host.fields) {
|
| + if (field.hasImplicitSetter) {
|
| + var rawType = substituteBounds(field.type, upperBounds, {});
|
| + if (!identical(rawType, field.type)) {
|
| + requireLocalCheckedEntryPoint(field);
|
| + addUnsafeField(field, rawType);
|
| + }
|
| + }
|
| + }
|
| + for (var procedure in host.procedures) {
|
| + if (procedure.isStatic) continue;
|
| + void handleParameter(VariableDeclaration parameter) {
|
| + var rawType = substituteBounds(parameter.type, upperBounds, {});
|
| + if (!identical(rawType, parameter.type)) {
|
| + requireLocalCheckedEntryPoint(procedure);
|
| + addUnsafeParameter(parameter, rawType, procedure);
|
| + }
|
| + }
|
| +
|
| + procedure.function.positionalParameters.forEach(handleParameter);
|
| + procedure.function.namedParameters.forEach(handleParameter);
|
| + }
|
| + }
|
| +
|
| + // Find (possibly inherited) members that override a method that has
|
| + // unsafe parameters.
|
| + hierarchy.forEachOverridePair(host,
|
| + (Member ownMember, Member superMember, bool isSetter) {
|
| + if (hasCheckedEntryPoint(superMember, setter: isSetter)) {
|
| + requireLocalCheckedEntryPoint(ownMember);
|
| + }
|
| + if (superMember.enclosingClass.typeParameters.isEmpty) return;
|
| + ownSubstitution = getSubstitutionMap(
|
| + hierarchy.getClassAsInstanceOf(host, ownMember.enclosingClass));
|
| + ownUpperBounds = getUpperBoundSubstitutionMap(ownMember.enclosingClass);
|
| + superSubstitution = getSubstitutionMap(
|
| + hierarchy.getClassAsInstanceOf(host, superMember.enclosingClass));
|
| + superUpperBounds =
|
| + getUpperBoundSubstitutionMap(superMember.enclosingClass);
|
| + if (ownMember is Procedure) {
|
| + if (superMember is Procedure) {
|
| + checkProcedureOverride(ownMember, superMember);
|
| + } else if (superMember is Field && isSetter) {
|
| + checkSetterFieldOverride(ownMember, superMember);
|
| + }
|
| + } else if (isSetter) {
|
| + checkFieldOverride(ownMember, superMember);
|
| + }
|
| + });
|
| +
|
| + for (Member member in membersNeedingCheckedEntryPoint) {
|
| + ownSubstitution = getSubstitutionMap(
|
| + hierarchy.getClassAsInstanceOf(host, member.enclosingClass));
|
| + ownSubstitution = ensureMutable(ownSubstitution);
|
| + generateCheckedEntryPoint(member);
|
| + }
|
| + }
|
| +
|
| + /// Compute an upper bound of the types in [inputTypes].
|
| + ///
|
| + /// We use this to compute a trustworthy type for a parameter, given a list
|
| + /// of types that may actually be passed into the parameter.
|
| + DartType getSafeType(List<DartType> inputTypes) {
|
| + var safeType = inputTypes[0];
|
| + for (int i = 1; i < inputTypes.length; ++i) {
|
| + if (inputTypes[i] != safeType) {
|
| + // Multiple types are being overridden. Fall back to dynamic.
|
| + // There are cases where a better upper bound could be found, but they
|
| + // are quite rare.
|
| + return const DynamicType();
|
| + }
|
| + }
|
| + return safeType;
|
| + }
|
| +
|
| + void fail(String message) {
|
| + log.warning('[unsoundness] $message');
|
| + }
|
| +
|
| + void checkFieldOverride(Field field, Member superMember) {
|
| + var fieldType =
|
| + substituteBounds(field.type, ownUpperBounds, ownSubstitution);
|
| + var superType = substituteBounds(
|
| + superMember.setterType, superUpperBounds, superSubstitution);
|
| + if (!types.isSubtypeOf(superType, fieldType)) {
|
| + addUnsafeField(field, superType);
|
| + }
|
| + }
|
| +
|
| + void checkSetterFieldOverride(Procedure ownMember, Field superMember) {
|
| + assert(ownMember.isSetter);
|
| + var ownParameter = ownMember.function.positionalParameters[0];
|
| + var ownType =
|
| + substituteBounds(ownParameter.type, ownUpperBounds, ownSubstitution);
|
| + var superType = substituteBounds(
|
| + superMember.setterType, superUpperBounds, superSubstitution);
|
| + if (!types.isSubtypeOf(superType, ownType)) {
|
| + addUnsafeParameter(ownParameter, superType, ownMember);
|
| + }
|
| + }
|
| +
|
| + void checkProcedureOverride(Procedure ownMember, Procedure superMember) {
|
| + var ownFunction = ownMember.function;
|
| + var superFunction = superMember.function;
|
| + // We perform some checks here to avoid crashing, but the frontend is
|
| + // responsible for generating IR that does not violate these restrictions.
|
| + if (ownFunction.requiredParameterCount >
|
| + superFunction.requiredParameterCount) {
|
| + fail('$ownMember requires more parameters than $superMember');
|
| + return;
|
| + }
|
| + if (ownFunction.positionalParameters.length <
|
| + superFunction.positionalParameters.length) {
|
| + fail('$ownMember allows fewer parameters than $superMember');
|
| + return;
|
| + }
|
| + if (ownFunction.typeParameters.length !=
|
| + superFunction.typeParameters.length) {
|
| + fail('$ownMember declares a different number of type parameters '
|
| + 'than $superMember');
|
| + return;
|
| + }
|
| + if (superFunction.typeParameters.isNotEmpty) {
|
| + // Ensure these maps are not constant, so we can add bindings for the
|
| + // function type parameters.
|
| + superSubstitution = ensureMutable(superSubstitution);
|
| + superUpperBounds = ensureMutable(superUpperBounds);
|
| + }
|
| + for (int i = 0; i < superFunction.typeParameters.length; ++i) {
|
| + var ownTypeParameter = ownFunction.typeParameters[i];
|
| + var superTypeParameter = superFunction.typeParameters[i];
|
| + var type = new TypeParameterType(ownTypeParameter);
|
| + superSubstitution[superTypeParameter] = type;
|
| + superUpperBounds[superTypeParameter] = type;
|
| + }
|
| + void checkParameterPair(
|
| + VariableDeclaration ownParameter, VariableDeclaration superParameter) {
|
| + var ownType = substitute(ownParameter.type, ownSubstitution);
|
| + var superType = substituteBounds(
|
| + superParameter.type, superUpperBounds, superSubstitution);
|
| + if (!types.isSubtypeOf(superType, ownType)) {
|
| + addUnsafeParameter(ownParameter, superType, ownMember);
|
| + }
|
| + }
|
| +
|
| + for (int i = 0; i < superFunction.positionalParameters.length; ++i) {
|
| + checkParameterPair(ownFunction.positionalParameters[i],
|
| + superFunction.positionalParameters[i]);
|
| + }
|
| + for (int i = 0; i < superFunction.namedParameters.length; ++i) {
|
| + var superParameter = superFunction.namedParameters[i];
|
| + bool found = false;
|
| + for (int j = 0; j < ownFunction.namedParameters.length; ++j) {
|
| + var ownParameter = ownFunction.namedParameters[j];
|
| + if (ownParameter.name == superParameter.name) {
|
| + found = true;
|
| + checkParameterPair(ownParameter, superParameter);
|
| + break;
|
| + }
|
| + }
|
| + if (!found) {
|
| + fail('$ownMember is missing the named parameter '
|
| + '${superParameter.name} from $superMember');
|
| + }
|
| + }
|
| + }
|
| +
|
| + void generateCheckedEntryPoint(Member member) {
|
| + // TODO(asgerf): It may be worthwhile to try to reuse a checked entry
|
| + // point from the supertype when the same checks are needed and the
|
| + // dispatch target is the same.
|
| + if (member is Procedure) {
|
| + generateCheckedProcedure(member);
|
| + } else {
|
| + generateCheckedFieldSetter(member);
|
| + }
|
| + }
|
| +
|
| + void generateCheckedProcedure(Procedure procedure) {
|
| + var function = procedure.function;
|
| +
|
| + // Clone the function without its body.
|
| + var body = function.body;
|
| + function.body = null;
|
| + var cloner = new CloneVisitor(typeSubstitution: ownSubstitution);
|
| + Procedure checkedProcedure = cloner.clone(procedure);
|
| + FunctionNode checkedFunction = checkedProcedure.function;
|
| + function.body = body;
|
| +
|
| + checkedFunction.asyncMarker = AsyncMarker.Sync;
|
| + checkedProcedure.isExternal = false;
|
| +
|
| + Expression getParameter(VariableDeclaration parameter) {
|
| + var cloneParameter = cloner.variables[parameter];
|
| + var unsafeInputs = unsafeParameterTypes[parameter];
|
| + if (unsafeInputs == null) {
|
| + return new VariableGet(cloneParameter); // No check needed.
|
| + }
|
| + // Change the actual parameter type to the safe type, and cast to the
|
| + // type declared on the original parameter.
|
| + // Use the cloner to map function type parameters to the cloned
|
| + // function type parameters (in case the function is generic).
|
| + var targetType = cloneParameter.type;
|
| + cloneParameter.type = cloner.visitType(getSafeType(unsafeInputs));
|
| + return new AsExpression(new VariableGet(cloneParameter), targetType);
|
| + }
|
| +
|
| + // TODO: Insert checks for type parameter bounds.
|
| + var types = checkedFunction.typeParameters
|
| + .map((p) => new TypeParameterType(p))
|
| + .toList();
|
| + var positional = function.positionalParameters.map(getParameter).toList();
|
| + var named = function.namedParameters
|
| + .map((p) => new NamedExpression(p.name, getParameter(p)))
|
| + .toList();
|
| +
|
| + checkedProcedure.name = covariantCheckedName(procedure.name);
|
| + host.addMember(checkedProcedure);
|
| +
|
| + // Only generate a body if the original method had one.
|
| + if (!procedure.isAbstract && !procedure.isInExternalLibrary) {
|
| + var call = procedure.isSetter
|
| + ? new DirectPropertySet(
|
| + new ThisExpression(), procedure, positional[0])
|
| + : new DirectMethodInvocation(new ThisExpression(), procedure,
|
| + new Arguments(positional, named: named, types: types));
|
| + var checkedBody = function.returnType is VoidType
|
| + ? new ExpressionStatement(call)
|
| + : new ReturnStatement(call);
|
| + checkedFunction.body = checkedBody..parent = checkedFunction;
|
| + }
|
| +
|
| + if (procedure.enclosingClass == host) {
|
| + global.unsafeMemberEntryPoint[procedure] = checkedProcedure;
|
| + }
|
| + }
|
| +
|
| + void generateCheckedFieldSetter(Field field) {
|
| + var parameter = getFieldSetterParameter(field);
|
| + var unsafeTypes = unsafeParameterTypes[parameter];
|
| + Expression argument = new VariableGet(parameter);
|
| + if (unsafeTypes != null) {
|
| + var castType = substitute(field.type, ownSubstitution);
|
| + argument = new AsExpression(argument, castType);
|
| + var inputType = substitute(getSafeType(unsafeTypes), ownSubstitution);
|
| + parameter.type = inputType;
|
| + }
|
| +
|
| + Statement body = field.isInExternalLibrary
|
| + ? null
|
| + : new ExpressionStatement(
|
| + new DirectPropertySet(new ThisExpression(), field, argument));
|
| +
|
| + var setter = new Procedure(
|
| + covariantCheckedName(field.name),
|
| + ProcedureKind.Setter,
|
| + new FunctionNode(body, positionalParameters: [parameter]));
|
| + host.addMember(setter);
|
| +
|
| + if (field.enclosingClass == host) {
|
| + global.unsafeMemberEntryPoint[field] = setter;
|
| + }
|
| + }
|
| +
|
| + /// Generates a synthetic name representing the covariant-checked entry point
|
| + /// to a method.
|
| + static Name covariantCheckedName(Name name) {
|
| + return new Name('${name.name}\$cc', name.library);
|
| + }
|
| +
|
| + static Map<TypeParameter, DartType> ensureMutable(
|
| + Map<TypeParameter, DartType> map) {
|
| + if (map.isEmpty) return <TypeParameter, DartType>{};
|
| + return map;
|
| + }
|
| +}
|
| +
|
| +// TODO(asgerf): We should be able to avoid checked calls in a lot more cases:
|
| +// - the arguments to every unsafe parameter is null or is omitted
|
| +// - allocation site of receiver can easily be seen statically
|
| +class _CallTransformer extends RecursiveVisitor {
|
| + final InsertCovarianceChecks global;
|
| + final TypeEnvironment types;
|
| + final Map<Member, Procedure> checkedInterfaceMethod;
|
| +
|
| + _CallTransformer(InsertCovarianceChecks global)
|
| + : checkedInterfaceMethod = global.unsafeMemberEntryPoint,
|
| + types = global.types,
|
| + this.global = global;
|
| +
|
| + Member getChecked(Expression receiver, Member member) {
|
| + var checked = checkedInterfaceMethod[member];
|
| + if (checked == null) return member;
|
| + if (!receiverNeedsChecks(receiver)) return member;
|
| + return checked;
|
| + }
|
| +
|
| + bool receiverNeedsChecks(Expression node) {
|
| + if (node is ThisExpression) return false;
|
| + var type = node.getStaticType(types);
|
| + if (type is InterfaceType && type.typeArguments.every(isSealedType)) {
|
| + return false;
|
| + }
|
| + return true;
|
| + }
|
| +
|
| + bool isSealedType(DartType type) {
|
| + return type is InterfaceType && types.isSealedClass(type.classNode);
|
| + }
|
| +
|
| + bool isTrustedLibrary(Library node) {
|
| + return node.importUri.scheme == 'dart';
|
| + }
|
| +
|
| + @override
|
| + visitClass(Class node) {
|
| + types.thisType = node.thisType;
|
| + node.visitChildren(this);
|
| + }
|
| +
|
| + @override
|
| + visitLibrary(Library node) {
|
| + if (!isTrustedLibrary(node)) {
|
| + node.visitChildren(this);
|
| + }
|
| + }
|
| +
|
| + @override
|
| + visitMethodInvocation(MethodInvocation node) {
|
| + node.interfaceTarget = getChecked(node.receiver, node.interfaceTarget);
|
| + node.visitChildren(this);
|
| + }
|
| +
|
| + @override
|
| + visitPropertySet(PropertySet node) {
|
| + node.interfaceTarget = getChecked(node.receiver, node.interfaceTarget);
|
| + node.visitChildren(this);
|
| + }
|
| +}
|
|
|