[ddc] Support more complicated subtype checks

Generate type hierarchy rules for all interface types used in a
module. These rules are utilized by the `isSubtype()` to determine
interface subtypes.

Issue: https://github.com/dart-lang/sdk/issues/48585
Change-Id: I63f64075d4947f234eca5e730ad15588e60b9e1e
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/266542
Reviewed-by: Anna Gringauze <annagrin@google.com>
Reviewed-by: Mark Zhou <markzipan@google.com>
Commit-Queue: Nicholas Shahan <nshahan@google.com>
This commit is contained in:
Nicholas Shahan 2022-12-03 00:37:41 +00:00 committed by Commit Queue
parent bf548e788a
commit a464fc354b
3 changed files with 196 additions and 26 deletions

View file

@ -350,7 +350,7 @@ class ProgramCompiler extends ComputeOnceConstantVisitor<js_ast.Expression>
_assertInteropMethod = sdk.getTopLevelMember(
'dart:_runtime', 'assertInterop') as Procedure,
_futureOrNormalizer = FutureOrNormalizer(_coreTypes),
_typeRecipeGenerator = TypeRecipeGenerator(_coreTypes);
_typeRecipeGenerator = TypeRecipeGenerator(_coreTypes, _hierarchy);
@override
Library? get currentLibrary => _currentLibrary;
@ -484,6 +484,20 @@ class ProgramCompiler extends ComputeOnceConstantVisitor<js_ast.Expression>
moduleItems.addAll(afterClassDefItems);
afterClassDefItems.clear();
// Add all type hierarchy rules for the interface types used in this module.
if (_options.newRuntimeTypes) {
var universeClass =
rtiLibrary.classes.firstWhere((cls) => cls.name == '_Universe');
var typeRules = _typeRecipeGenerator.visitedInterfaceTypeRules;
var template = "#._Universe.#(#, JSON.parse('${jsonEncode(typeRules)}'))";
var addRulesStatement = js.call(template, [
emitLibraryName(rtiLibrary),
_emitMemberName('addRules', memberClass: universeClass),
runtimeCall('typeUniverse')
]).toStatement();
moduleItems.add(addRulesStatement);
}
// Visit directives (for exports)
libraries.forEach(_emitExports);
@ -763,15 +777,15 @@ class ProgramCompiler extends ComputeOnceConstantVisitor<js_ast.Expression>
});
var jsCtors = _defineConstructors(c, className);
// TODO(nshahan) Use ClassTypeEnvironment when the representation of generic
// classes is no longer a closure that defines the class and captures the
// type arguments.
// Emitting class members in a class type environment results in a more
// succinct type representation when referencing class type arguments from
// instance members.
var savedTypeEnvironment = _currentTypeEnvironment;
if (c.typeParameters.isNotEmpty) {
_currentTypeEnvironment = ClassTypeEnvironment(c.typeParameters);
}
// instance members but the type rules must include mappings of all type
// arguments throughout the hierarchy.
var jsMethods = _emitClassMethods(c);
_currentTypeEnvironment = savedTypeEnvironment;
_emitSuperHelperSymbols(body);
// Deferred supertypes must be evaluated lazily while emitting classes to
@ -795,11 +809,34 @@ class ProgramCompiler extends ComputeOnceConstantVisitor<js_ast.Expression>
var finishGenericTypeTest = _emitClassTypeTests(c, className, body);
/// Collects all implemented types in the ancestry of [cls].
Iterable<Supertype> transitiveImplementedTypes(Class cls) {
var allImplementedTypes = <Supertype>{};
var toVisit = ListQueue<Supertype>()..addAll(cls.implementedTypes);
while (toVisit.isNotEmpty) {
var supertype = toVisit.removeFirst();
var superclass = supertype.classNode;
if (allImplementedTypes.contains(supertype) ||
superclass == _coreTypes.objectClass) continue;
toVisit.addAll(superclass.supers);
// Skip encoding the synthetic classes in the type rules because they
// will never be instantiated or appear in type tests.
if (superclass.isAnonymousMixin) continue;
allImplementedTypes.add(supertype);
}
return allImplementedTypes;
}
// Attach caches on all canonicalized types.
if (_options.newRuntimeTypes) {
var name = _typeRecipeGenerator.interfaceTypeRecipe(c);
body.add(runtimeStatement(
'addRtiResources(#, #)', [className, js.string(name)]));
var implementedRecipes = [
name,
for (var type in transitiveImplementedTypes(c))
_typeRecipeGenerator.interfaceTypeRecipe(type.classNode)
];
body.add(runtimeStatement('addRtiResources(#, #)',
[className, js_ast.stringArray(implementedRecipes)]));
}
body.add(runtimeStatement('addTypeCaches(#)', [className]));
@ -981,8 +1018,13 @@ class ProgramCompiler extends ComputeOnceConstantVisitor<js_ast.Expression>
if (t is InterfaceType) {
_declareBeforeUse(t.classNode);
if (t.typeArguments.isNotEmpty) {
var typeRep =
_emitGenericClassType(t, t.typeArguments.map(emitDeferredType));
var typeRep = _emitGenericClassType(
t,
_options.newRuntimeTypes
// No reason to defer type arguments in the new type
// representation.
? t.typeArguments.map(_emitType)
: t.typeArguments.map(emitDeferredType));
return emitNullability
? _emitNullabilityWrapper(typeRep, t.declaredNullability)
: typeRep;
@ -3041,11 +3083,12 @@ class ProgramCompiler extends ComputeOnceConstantVisitor<js_ast.Expression>
} else if (environment is ClassTypeEnvironment) {
// Class type environments are already constructed and attached to the
// instance of a generic class.
return js.call('this.${js_ast.FixedNames.rtiName}');
var env = runtimeCall('getReifiedType(this)');
return emitRtiEval(env, recipe);
} else if (environment is ExtendedClassTypeEnvironment) {
// A generic class instance already stores a reference to a type
// containing all of its type arguments.
var env = js.call('this.${js_ast.FixedNames.rtiName}');
var env = runtimeCall('getReifiedType(this)');
// Bind extra type parameters.
for (var parameter in environment.extendedParameters) {
env = emitRtiBind(env, parameter);
@ -3203,6 +3246,44 @@ class ProgramCompiler extends ComputeOnceConstantVisitor<js_ast.Expression>
return _typeTable.nameType(type, typeRep);
}
/// Emits the a reference to the class described by [type].
///
/// The nullability of [type] is not considered because it is meaningless when
/// describing a reference to the class itself.
///
/// In the case of a generic type, this reference will be a call to the
/// function that defines the class and will pass the type parameters as
/// arguments. The nullability of the type parameters does have meaning so it
/// is encoded.
js_ast.Expression _emitClassRef(InterfaceType type) {
var cls = type.classNode;
_declareBeforeUse(cls);
// Type parameters don't matter as JS interop types cannot be reified.
// package:js types fall under `@JS`, `@anonymous`, or `@staticInterop`
// types. `@JS` types are used to correspond to JS types that exist, but we
// do not use the underlying type for type checks, so they operate virtually
// the same as `@anonymous` types. `@staticInterop` types, however, can be
// casted to other `package:js` types as well as any type that inherits
// `JavaScriptObject`. This is to match the behavior across the other
// backends that use erasure. We represent `@JS` and `@anonymous` types with
// a NonStaticInteropType and `@staticInterop` with a StaticInteropType to
// make this distinction at runtime.
var jsName = isJSAnonymousType(cls)
? getLocalClassName(cls)
: _emitJsNameWithoutGlobal(cls);
if (jsName != null) {
return runtimeCall('packageJSType(#, #)',
[js.escapedString(jsName), js.boolean(isStaticInteropType(cls))]);
}
var args = type.typeArguments;
Iterable<js_ast.Expression>? jsArgs;
if (args.any((a) => a != const DynamicType())) {
jsArgs = args.map(_emitType);
}
if (jsArgs != null) return _emitGenericClassType(type, jsArgs);
return _emitTopLevelNameNoInterop(type.classNode);
}
/// Emits the representation of a FutureOr [type].
js_ast.Expression _emitFutureOrType(FutureOrType type) {
_declareBeforeUse(_coreTypes.deprecatedFutureOrClass);
@ -3420,7 +3501,9 @@ class ProgramCompiler extends ComputeOnceConstantVisitor<js_ast.Expression>
/// Emits an expression that lets you access statics on a [type] from code.
js_ast.Expression _emitConstructorAccess(InterfaceType type) {
return _emitJSInterop(type.classNode) ??
_emitInterfaceType(type, emitNullability: false);
(_options.newRuntimeTypes
? _emitClassRef(type)
: _emitInterfaceType(type, emitNullability: false));
}
js_ast.Expression _emitConstructorName(InterfaceType type, Member c) {

View file

@ -2,8 +2,11 @@
// 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';
import 'package:js_shared/synced/recipe_syntax.dart' show Recipe;
import 'package:kernel/ast.dart';
import 'package:kernel/class_hierarchy.dart';
import 'package:kernel/core_types.dart';
import 'package:path/path.dart' as p;
@ -18,10 +21,13 @@ import 'type_environment.dart';
/// `ProgramCompiler`. It provides the API to interact with the DartType visitor
/// that generates the type recipes.
class TypeRecipeGenerator {
final CoreTypes _coreTypes;
final ClassHierarchy _hierarchy;
final _TypeRecipeVisitor _recipeVisitor;
TypeRecipeGenerator(CoreTypes coreTypes)
: _recipeVisitor =
TypeRecipeGenerator(CoreTypes coreTypes, this._hierarchy)
: _coreTypes = coreTypes,
_recipeVisitor =
_TypeRecipeVisitor(const EmptyTypeEnvironment(), coreTypes);
/// Returns a recipe for the provided [type] packaged with an environment with
@ -36,11 +42,66 @@ class TypeRecipeGenerator {
var minimalEnvironment = environment.prune(parametersInType);
// Set the visitor state, generate the recipe, and package it with the
// environment required to evaluate it.
_recipeVisitor.setState(environment: minimalEnvironment);
_recipeVisitor.setState(
environment: minimalEnvironment, recordInterfaceTypes: true);
var recipe = type.accept(_recipeVisitor);
return GeneratedRecipe(recipe, minimalEnvironment);
}
/// Returns a mapping of type hierarchies for all [InterfaceType]s that have
/// appeared in type recipes.
///
/// This mapping is intended to satisfy the requirements for the
/// `_Universe.addRules()` static method in the runtime rti library.
///
/// The format is compatible to directly encode as the JSON string expected
/// by the runtime library:
///
/// ```
/// '{"type 0": {"supertype 0": ["type argument 0"... "type argument n"],
/// ...
/// "supertype n": ["type argument 0"... "type argument n"]},
/// ...
/// "type n": {...}}'
/// ```
Map<String, Map<String, List<String>>> get visitedInterfaceTypeRules {
var rules = <String, Map<String, List<String>>>{};
for (var type in _recipeVisitor.visitedInterfaceTypes) {
var recipe = interfaceTypeRecipe(type.classNode);
var cls = type.classNode;
// Create a class type environment for calculating type argument indices.
// Avoid recording the types while iterating the visited types.
_recipeVisitor.setState(
environment: ClassTypeEnvironment(cls.typeParameters),
recordInterfaceTypes: false);
var supertypeEntries = <String, List<String>>{};
// Encode type rules for all supers.
var toVisit = ListQueue<Supertype>.from(cls.supers);
var visited = <Supertype>{};
while (toVisit.isNotEmpty) {
var currentClass = toVisit.removeFirst().classNode;
if (currentClass == _coreTypes.objectClass) continue;
var currentType = _hierarchy.getClassAsInstanceOf(cls, currentClass)!;
if (visited.contains(currentType)) continue;
// Add all supers to the visit queue.
toVisit.addAll(currentClass.supers);
// Skip encoding the synthetic classes in the type rules because they
// will never be instantiated or appear in type tests.
if (currentClass.isAnonymousMixin) continue;
// Encode this type rule.
var recipe = interfaceTypeRecipe(currentClass);
var typeArgumentRecipes = [
for (var typeArgument in currentType.typeArguments)
typeArgument.accept(_recipeVisitor)
];
supertypeEntries[recipe] = typeArgumentRecipes;
visited.add(currentType);
}
if (supertypeEntries.isNotEmpty) rules[recipe] = supertypeEntries;
}
return rules;
}
String interfaceTypeRecipe(Class node) =>
_recipeVisitor.interfaceTypeRecipe(node);
}
@ -60,7 +121,21 @@ class _TypeRecipeVisitor extends DartTypeVisitor<String> {
/// Part of the state that should be set before visiting a type.
/// Used to determine the indices for type variables.
DDCTypeEnvironment _typeEnvironment;
/// Type parameters introduced to the environment while visiting generic
/// function types nested within other types.
var _unboundTypeParameters = <String>[];
/// When `true` this visitor will record all of the [InterfaceType]s it
/// visits.
///
/// Part of the state that should be set before visiting a type.
/// These types can be used later to produce runtime type hierarchy rules for
/// all of the visited interface types.
bool _recordInterfaceTypes = true;
/// All of the [InterfaceType]s visited.
final _visitedInterfaceTypes = <InterfaceType>{};
final CoreTypes _coreTypes;
_TypeRecipeVisitor(this._typeEnvironment, this._coreTypes);
@ -70,10 +145,17 @@ class _TypeRecipeVisitor extends DartTypeVisitor<String> {
/// Generally this should be called before visiting a type, but the visitor
/// does not modify the state so if many types need to be evaluated in the
/// same state it can be set once before visiting all of them.
void setState({DDCTypeEnvironment? environment}) {
void setState({DDCTypeEnvironment? environment, bool? recordInterfaceTypes}) {
if (environment != null) _typeEnvironment = environment;
if (recordInterfaceTypes != null) {
_recordInterfaceTypes = recordInterfaceTypes;
}
}
/// The [InterfaceType]s that have been visited.
Iterable<InterfaceType> get visitedInterfaceTypes =>
Set.unmodifiable(_visitedInterfaceTypes);
@override
String defaultDartType(DartType node) {
throw UnimplementedError('Unknown DartType: $node');
@ -87,6 +169,7 @@ class _TypeRecipeVisitor extends DartTypeVisitor<String> {
@override
String visitInterfaceType(InterfaceType node) {
if (_recordInterfaceTypes) _visitedInterfaceTypes.add(node);
// Generate the interface type recipe.
var recipeBuffer = StringBuffer(interfaceTypeRecipe(node.classNode));
// Generate the recipes for all type arguments.

View file

@ -654,23 +654,27 @@ Object typeTagSymbol(String recipe) {
return tagSymbol;
}
/// Attaches the type [recipe] and the type tag for the class to to [classRef].
/// Attaches the class type recipe and the type tags for all implemented
/// [interfaceRecipes] to [classRef].
///
/// The tags are used for simple identification of instances in the dart:rti
/// library.
///
/// The first element of interface [interfaceRecipes] must always be the recipe
/// for the type represented by [classRef].
void addRtiResources(Object classRef, String recipe) {
// Attach the classes own interface type recipe.
/// The first element of [interfaceRecipes] must always be the type recipe for
/// the type represented by [classRef].
void addRtiResources(Object classRef, JSArray<String> interfaceRecipes) {
// Attach the [classRef]'s own interface type recipe.
// The recipe is used in dart:_rti to create an [rti.Rti] instance when
// needed.
JS('', r'#.# = #', classRef, rti.interfaceTypeRecipePropertyName, recipe);
JS('', r'#.# = #[0]', classRef, rti.interfaceTypeRecipePropertyName,
interfaceRecipes);
// Add specialized test resources used for fast interface type checks in
// dart:_rti.
var prototype = JS<Object>('!', '#.prototype', classRef);
var tagSymbol = typeTagSymbol(recipe);
JS('', '#.# = #', prototype, tagSymbol, true);
for (var recipe in interfaceRecipes) {
var tagSymbol = typeTagSymbol(recipe);
JS('', '#.# = #', prototype, tagSymbol, true);
}
}
/// Pre-initializes types with empty type caches.