| // 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. | |
| import 'dart:collection' show HashMap, HashSet, Queue; | |
| import 'package:kernel/core_types.dart'; | |
| import 'package:kernel/kernel.dart'; | |
| import 'package:kernel/type_environment.dart'; | |
| import '../compiler/js_names.dart' as js_ast; | |
| import 'kernel_helpers.dart'; | |
| import 'native_types.dart'; | |
| /// Dart allows all fields to be overridden. | |
| /// | |
| /// To prevent a performance/code size penalty for allowing this, we analyze | |
| /// private classes within each library that is being compiled to determine | |
| /// if those fields should be virtual or not. In effect, we devirtualize fields | |
| /// when possible by analyzing the class hierarchy and using knowledge of | |
| /// which members are private and thus, could not be overridden outside of the | |
| /// current library. | |
| class VirtualFieldModel { | |
| final _modelForLibrary = HashMap<Library, _LibraryVirtualFieldModel>(); | |
| _LibraryVirtualFieldModel _getModel(Library library) => _modelForLibrary | |
| .putIfAbsent(library, () => _LibraryVirtualFieldModel.build(library)); | |
| /// Returns true if a field is virtual. | |
| bool isVirtual(Field field) => | |
| _getModel(field.enclosingLibrary).isVirtual(field); | |
| } | |
| /// This is a building block of [VirtualFieldModel], used to track information | |
| /// about a single library that has been analyzed. | |
| class _LibraryVirtualFieldModel { | |
| /// Fields that are private (or public fields of a private class) and | |
| /// overridden in this library. | |
| /// | |
| /// This means we must generate them as virtual fields using a property pair | |
| /// in JavaScript. | |
| final _overriddenPrivateFields = HashSet<Field>(); | |
| /// Private classes that can be extended outside of this library. | |
| /// | |
| /// Normally private classes cannot be accessed outside this library, however, | |
| /// this can happen if they are extended by a public class or exposed by a | |
| /// public typedef, for example: | |
| /// | |
| /// class _A { int x = 42; } | |
| /// class _B { int x = 42; } | |
| /// class _C { int x = 42; } | |
| /// | |
| /// // _A and _B are now effectively public for the purpose of overrides. | |
| /// class D extends _A {} | |
| /// typedef E = _B; | |
| /// | |
| /// The classes _A and _B must treat is "x" as virtual, however _C does not. | |
| final _extensiblePrivateClasses = HashSet<Class>(); | |
| _LibraryVirtualFieldModel.build(Library library) { | |
| var allClasses = library.classes; | |
| for (var typedef in library.typedefs) { | |
| // Ignore private typedefs. | |
| if (typedef.name.startsWith('_')) continue; | |
| var type = typedef.type; | |
| if (type is InterfaceType && type.classNode.name.startsWith('_')) { | |
| // Public typedefs of private classes expose those classes for | |
| // extension. | |
| _extensiblePrivateClasses.add(type.classNode); | |
| } | |
| } | |
| // The set of public types is our initial extensible type set. | |
| // From there, visit all immediate private types in this library, and so on | |
| // from those private types, marking them as extensible. | |
| var classesToVisit = Queue<Class>.from( | |
| allClasses.where((c) => !c.name.startsWith('_')), | |
| ); | |
| while (classesToVisit.isNotEmpty) { | |
| var c = classesToVisit.removeFirst(); | |
| // For each supertype of a public type in this library, | |
| // if we encounter a private class, we mark it as being extended, and | |
| // add it to our work set if this is the first time we've visited it. | |
| for (var superclass in getImmediateSuperclasses(c)) { | |
| if (superclass.name.startsWith('_') && | |
| superclass.enclosingLibrary == library) { | |
| if (_extensiblePrivateClasses.add(superclass)) { | |
| classesToVisit.add(superclass); | |
| } | |
| } | |
| } | |
| } | |
| // Class can only look up inherited members with an O(N) scan through | |
| // the class, so we build up a mapping of all fields in the library ahead of | |
| // time. | |
| Map<String, Field> getInstanceFieldMap(Class c) { | |
| var instanceFields = c.fields.where((f) => !f.isStatic); | |
| return HashMap.fromIterables( | |
| instanceFields.map((f) => f.name.text), | |
| instanceFields, | |
| ); | |
| } | |
| var allFields = HashMap.fromIterables( | |
| allClasses, | |
| allClasses.map(getInstanceFieldMap), | |
| ); | |
| for (var class_ in allClasses) { | |
| Set<Class>? superclasses; | |
| // Visit accessors in the current class, and see if they override an | |
| // otherwise private field. | |
| for (var member in class_.members) { | |
| // Ignore abstract/static accessors, methods, constructors. | |
| if (member.isAbstract || | |
| member is Procedure && (!member.isAccessor || member.isStatic) || | |
| member is Constructor) { | |
| continue; | |
| } | |
| assert(member is Field || member is Procedure && member.isAccessor); | |
| // Ignore public accessors in extensible classes. | |
| if (!member.name.isPrivate && | |
| (!class_.name.startsWith('_') || | |
| _extensiblePrivateClasses.contains(class_))) { | |
| continue; | |
| } | |
| if (superclasses == null) { | |
| superclasses = <Class>{}; | |
| void collectSupertypes(Class c, Set<Class> supers) { | |
| if (!supers.add(c)) return; | |
| var s = c.superclass; | |
| if (s != null) collectSupertypes(s, supers); | |
| var m = c.mixedInClass; | |
| if (m != null) collectSupertypes(m, supers); | |
| } | |
| collectSupertypes(class_, superclasses); | |
| superclasses.remove(class_); | |
| superclasses.removeWhere((s) => s.enclosingLibrary != library); | |
| } | |
| // Look in all super classes to see if we're overriding a field in our | |
| // library, if so mark that field as overridden. | |
| var name = member.name.text; | |
| _overriddenPrivateFields.addAll( | |
| superclasses.map((c) => allFields[c]![name]).nonNulls, | |
| ); | |
| } | |
| } | |
| } | |
| /// Returns true if a field inside this library is virtual. | |
| bool isVirtual(Field field) { | |
| // If the field was marked non-virtual, we know for sure. | |
| if (field.isStatic) return false; | |
| var class_ = field.enclosingClass!; | |
| if (class_.isEnum) { | |
| // Enums are not extensible. | |
| return false; | |
| } | |
| if (!field.name.isPrivate) { | |
| // Public fields in public classes (or extensible private classes) | |
| // are always virtual. | |
| // They could be overridden by someone using our library. | |
| if (!class_.name.startsWith('_')) return true; | |
| if (_extensiblePrivateClasses.contains(class_)) return true; | |
| } | |
| if (class_.constructors.any((c) => c.isConst)) { | |
| // Always virtualize fields of a (might be) non-enum (see above) const | |
| // class. The way these are lowered by the CFE, they need to be | |
| // writable from different modules even if overridden. | |
| return true; | |
| } | |
| // Otherwise, the field is effectively private and we only need to make it | |
| // virtual if it's overridden. | |
| return _overriddenPrivateFields.contains(field); | |
| } | |
| } | |
| /// Tracks how fields, getters and setters are represented when emitting JS. | |
| /// | |
| /// Dart classes have implicit features that must be made explicit: | |
| /// | |
| /// - virtual fields induce a getter and setter pair. | |
| /// - getters and setters are independent. | |
| /// - getters and setters can be overridden. | |
| /// | |
| class ClassPropertyModel { | |
| final NativeTypeSet extensionTypes; | |
| final TypeEnvironment types; | |
| /// Fields that are virtual, that is, they must be generated as a property | |
| /// pair in JavaScript. | |
| /// | |
| /// The value property stores the symbol used for the field's storage slot. | |
| final virtualFields = <Field, js_ast.ScopedId>{}; | |
| /// The set of inherited getters, used because JS getters/setters are paired, | |
| /// so if we're generating a setter we may need to emit a getter that calls | |
| /// super. | |
| final inheritedGetters = HashSet<String>(); | |
| /// The set of inherited setters, used because JS getters/setters are paired, | |
| /// so if we're generating a getter we may need to emit a setter that calls | |
| /// super. | |
| final inheritedSetters = HashSet<String>(); | |
| final extensionMethods = <String>{}; | |
| final extensionAccessors = <String>{}; | |
| ClassPropertyModel.build( | |
| this.types, | |
| this.extensionTypes, | |
| VirtualFieldModel fieldModel, | |
| Class class_, | |
| ) { | |
| // Visit superclasses to collect information about their fields/accessors. | |
| // This is expensive so we try to collect everything in one pass. | |
| var superclasses = [class_, ...getSuperclasses(class_)]; | |
| for (var base in superclasses) { | |
| for (var member in base.members) { | |
| // Note, we treat noSuchMethodForwarders in the current class as | |
| // inherited / potentially virtual. Skip all other members of the | |
| // current class. | |
| if (base == class_ && | |
| (member is Field || | |
| (member is Procedure && !member.isNoSuchMethodForwarder))) { | |
| continue; | |
| } | |
| if (member is Constructor || | |
| member is Procedure && (!member.isAccessor || member.isStatic)) { | |
| continue; | |
| } | |
| // Ignore private names from other libraries. | |
| if (member.name.isPrivate && | |
| member.enclosingLibrary != class_.enclosingLibrary) { | |
| continue; | |
| } | |
| var name = member.name.text; | |
| if (member is Field) { | |
| inheritedGetters.add(name); | |
| if (!member.isFinal) inheritedSetters.add(name); | |
| } else { | |
| var accessor = member as Procedure; | |
| assert(accessor.isAccessor); | |
| (accessor.isGetter ? inheritedGetters : inheritedSetters).add(name); | |
| } | |
| } | |
| } | |
| _collectExtensionMembers(class_); | |
| var virtualAccessorNames = HashSet<String>() | |
| ..addAll(inheritedGetters) | |
| ..addAll(inheritedSetters) | |
| ..addAll(extensionAccessors); | |
| // Visit accessors in the current class, and see if they need to be | |
| // generated differently based on the inherited fields/accessors. | |
| for (var field in class_.fields) { | |
| // Also ignore abstract fields. | |
| if (field.isAbstract || field.isStatic) continue; | |
| var name = field.name.text; | |
| if (virtualAccessorNames.contains(name) || | |
| fieldModel.isVirtual(field) || | |
| field.isCovariantByDeclaration || | |
| field.isCovariantByClass) { | |
| virtualFields[field] = js_ast.ScopedId(js_ast.toJSIdentifier(name)); | |
| } | |
| } | |
| } | |
| CoreTypes get coreTypes => extensionTypes.coreTypes; | |
| void _collectExtensionMembers(Class class_) { | |
| if (extensionTypes.isNativeClass(class_)) return; | |
| // Find all generic interfaces that could be used to call into members of | |
| // this class. This will help us identify which parameters need checks | |
| // for soundness. | |
| var allNatives = HashSet<String>(); | |
| _collectNativeMembers(class_, allNatives); | |
| if (allNatives.isEmpty) return; | |
| // For members on this class, check them against all generic interfaces. | |
| var seenConcreteMembers = HashSet<String>(); | |
| _findExtensionMembers(class_, seenConcreteMembers, allNatives); | |
| // For members of the superclass, we may need to add checks because this | |
| // class adds a new unsafe interface. Collect those checks. | |
| var visited = HashSet<Class>()..add(class_); | |
| var existingMembers = HashSet<String>(); | |
| void visitImmediateSuper(Class c) { | |
| // For members of mixins/supertypes, check them against new interfaces, | |
| // and also record any existing checks they already had. | |
| var oldCovariant = HashSet<String>(); | |
| _collectNativeMembers(c, oldCovariant); | |
| var newCovariant = allNatives.difference(oldCovariant); | |
| if (newCovariant.isEmpty) return; | |
| existingMembers.addAll(oldCovariant); | |
| void visitSuper(Class c) { | |
| if (visited.add(c)) { | |
| _findExtensionMembers(c, seenConcreteMembers, newCovariant); | |
| var m = c.mixedInClass; | |
| if (m != null) visitSuper(m); | |
| var s = c.superclass; | |
| if (s != null) visitSuper(s); | |
| } | |
| } | |
| visitSuper(c); | |
| } | |
| if (class_.superclass != null) { | |
| var mixins = <Class>[]; | |
| var superclass = getSuperclassAndMixins(class_, mixins); | |
| mixins.forEach(visitImmediateSuper); | |
| visitImmediateSuper(superclass); | |
| } | |
| } | |
| /// Searches all concrete instance members declared on this type, skipping | |
| /// already [seenConcreteMembers], and adds them to [extensionMembers] if | |
| /// needed. | |
| /// | |
| /// By tracking the set of seen members, we can visit superclasses and mixins | |
| /// and ultimately collect every most-derived member exposed by a given type. | |
| void _findExtensionMembers( | |
| Class c, | |
| HashSet<String> seenConcreteMembers, | |
| Set<String> allNatives, | |
| ) { | |
| // We only visit each most derived concrete member. | |
| // To avoid visiting an overridden superclass member, we skip members | |
| // we've seen, and visit starting from the class, then mixins in | |
| // reverse order, then superclasses. | |
| for (var m in c.members) { | |
| var name = m.name.text; | |
| if (m.isAbstract || m is Constructor) continue; | |
| if (m is Procedure) { | |
| if (m.isStatic) continue; | |
| if (seenConcreteMembers.add(name) && allNatives.contains(name)) { | |
| (m.isAccessor ? extensionAccessors : extensionMethods).add(name); | |
| } | |
| } else if (m is Field) { | |
| if (m.isStatic) continue; | |
| if (seenConcreteMembers.add(name) && allNatives.contains(name)) { | |
| extensionAccessors.add(name); | |
| } | |
| } | |
| } | |
| } | |
| /// Collects all supertypes that may themselves contain native subtypes, | |
| /// excluding [Object], for example `List` is implemented by several native | |
| /// types. | |
| void _collectNativeMembers(Class c, Set<String> members) { | |
| _collectNativeMembersFrom(c, members, HashSet<Class>()); | |
| } | |
| /// Helper for [_collectNativeMembers] that skips classes in [visited]. | |
| void _collectNativeMembersFrom( | |
| Class c, | |
| Set<String> members, | |
| HashSet<Class> visited, | |
| ) { | |
| if (!visited.add(c)) return; | |
| if (extensionTypes.hasNativeSubtype(c)) { | |
| for (var m in c.members) { | |
| if (!m.name.isPrivate && | |
| (m is Procedure && !m.isStatic || m is Field && !m.isStatic)) { | |
| members.add(m.name.text); | |
| } | |
| } | |
| } | |
| var m = c.mixedInClass; | |
| if (m != null) _collectNativeMembersFrom(m, members, visited); | |
| for (var i in c.implementedTypes) { | |
| _collectNativeMembersFrom(i.classNode, members, visited); | |
| } | |
| var s = c.superclass; | |
| if (s != null) _collectNativeMembersFrom(s, members, visited); | |
| } | |
| } |