diff --git a/pkg/dev_compiler/lib/js/ddc/ddc_module_loader.js b/pkg/dev_compiler/lib/js/ddc/ddc_module_loader.js index c219f4ef8f1..09b5c8f7d34 100644 --- a/pkg/dev_compiler/lib/js/ddc/ddc_module_loader.js +++ b/pkg/dev_compiler/lib/js/ddc/ddc_module_loader.js @@ -1560,6 +1560,12 @@ if (!self.deferred_loader) { * application. */ hotReloadEnd() { + // Clear RTI subtype caches before initializing libraries. + // These needs to be done before hot reload completes (and any new + // libraries initialize) in case subtype hierarchies updated. + let dartRtiLibrary = this.importLibrary('dart:_rti'); + dartRtiLibrary.resetRtiSubtypeCaches(); + // On a hot reload, we reuse the existing library objects to ensure all // references remain valid and continue to be unique. We track in // `previouslyLoaded` which libraries already exist in the system, so we diff --git a/pkg/dev_compiler/lib/src/kernel/compiler_new.dart b/pkg/dev_compiler/lib/src/kernel/compiler_new.dart index f782234cacf..449bebefa2d 100644 --- a/pkg/dev_compiler/lib/src/kernel/compiler_new.dart +++ b/pkg/dev_compiler/lib/src/kernel/compiler_new.dart @@ -812,6 +812,24 @@ class LibraryCompiler extends ComputeOnceConstantVisitor ]).toStatement(); _typeRuleLinks.add(addRulesStatement); } + // Reset type rules for all classes with empty type hierarchies. These + // classes implicitly extend `Object` and may have been updated after a + // hot reload. This is unnecessary for types in `liveInterfaceTypeRules`, + // as `addRules` overrides the old rules. + // TODO(57049): Only do this after a hot reload. + var emptyInterfaceTypeRecipes = + Set.from(_typeRecipeGenerator.visitedInterfaceTypeRecipes) + ..removeAll(typeRules.keys); + if (emptyInterfaceTypeRecipes.isNotEmpty) { + var template = '#._Universe.#(#, JSON.parse(#))'; + var deleteRulesStatement = js.call(template, [ + _emitLibraryName(_rtiLibrary), + _emitMemberName('deleteRules', memberClass: universeClass), + _runtimeCall('typeUniverse'), + js.string(jsonEncode(emptyInterfaceTypeRecipes.toList()), "'") + ]).toStatement(); + _typeRuleLinks.add(deleteRulesStatement); + } // Update type rules for `LegacyJavaScriptObject` to add all interop // types in this module as a supertype. var updateRules = _typeRecipeGenerator.updateLegacyJavaScriptObjectRules; diff --git a/pkg/dev_compiler/lib/src/kernel/type_recipe_generator.dart b/pkg/dev_compiler/lib/src/kernel/type_recipe_generator.dart index 3f8cd6b1139..741ad0d28d2 100644 --- a/pkg/dev_compiler/lib/src/kernel/type_recipe_generator.dart +++ b/pkg/dev_compiler/lib/src/kernel/type_recipe_generator.dart @@ -71,6 +71,13 @@ class TypeRecipeGenerator { void addLiveTypeAncestries(InterfaceType type) => _recipeVisitor.addLiveTypeAncestries(type); + /// Returns all recipes for [InterfaceType]s that have appeared in type + /// recipes. + List get visitedInterfaceTypeRecipes => [ + for (var type in _recipeVisitor.visitedInterfaceTypes) + interfaceTypeRecipe(type.classNode) + ]; + /// Returns a mapping of type hierarchies for all [InterfaceType]s that have /// appeared in type recipes. /// diff --git a/sdk/lib/_internal/js_shared/lib/rti.dart b/sdk/lib/_internal/js_shared/lib/rti.dart index df28aad75ad..65152c760a5 100644 --- a/sdk/lib/_internal/js_shared/lib/rti.dart +++ b/sdk/lib/_internal/js_shared/lib/rti.dart @@ -202,6 +202,9 @@ class Rti { Object? _isSubtypeCache; + static Object? _getRawIsSubtypeCache(Rti rti) => rti._isSubtypeCache; + + /// Same as [_getRawIsSubtypeCache] but also initializes the cache. static Object? _getIsSubtypeCache(Rti rti) => rti._isSubtypeCache ??= JS('', 'new Map()'); @@ -567,6 +570,24 @@ Rti _rtiBind(Rti environment, Rti types) { return _Universe.bind(_theUniverse(), environment, types); } +/// Resets subtype caches on all evaluated RTIs. +/// +/// This operation is used when subtyping relationships change in a running +/// app (such as after a hot reload). +void resetRtiSubtypeCaches() { + var universe = _theUniverse(); + var cache = _Universe.evalCache(universe); + var values = _Utils.mapValues(cache); + var length = _Utils.arrayLength(values); + for (int i = 0; i < length; i++) { + Rti rti = _Utils.asRti(_Utils.arrayAt(values, i)); + var sCache = Rti._getRawIsSubtypeCache(rti); + if (sCache != null) { + _Utils.mapClear(sCache); + } + } +} + /// Evaluate a ground-term type. /// Called from generated code. Rti findType(String recipe) { @@ -2309,6 +2330,15 @@ class _Universe { static void addRules(Object? universe, Object? rules) => _Utils.objectAssign(typeRules(universe), rules); + static void deleteRules(Object? universe, Object? types) { + var universeTypeRules = typeRules(universe); + var typeCount = _Utils.arrayLength(types); + for (int i = 0; i < typeCount; i++) { + var type = _Utils.asString(_Utils.arrayAt(types, i)); + _Utils.objectDelete(universeTypeRules, type); + } + } + /// Adds or updates existing type rules in the type [universe]. /// /// This update is intended to add new rules to the set of rules that exist @@ -4344,6 +4374,9 @@ class _Utils { } } + static void objectDelete(Object? o, Object? property) => + JS('', 'delete #[#]', o, property); + static Object? newArrayOrEmpty(int length) => length > 0 ? JS('', 'new Array(#)', length) @@ -4380,6 +4413,11 @@ class _Utils { static bool stringLessThan(String s1, String s2) => JS('bool', '# < #', s1, s2); + static JSArray mapValues(Object? map) => + JS('JSArray', 'Array.from(#.values())', map); + + static void mapClear(Object? map) => JS('', '#.clear()', map); + static Object? mapGet(Object? cache, Object? key) => JS('', '#.get(#)', cache, key); diff --git a/tests/hot_reload/existing_field_changes_type_indirect_function/main.0.dart b/tests/hot_reload/existing_field_changes_type_indirect_function/main.0.dart index ca9c35cc64d..a72c787e5fb 100644 --- a/tests/hot_reload/existing_field_changes_type_indirect_function/main.0.dart +++ b/tests/hot_reload/existing_field_changes_type_indirect_function/main.0.dart @@ -33,7 +33,9 @@ Future main() async { await hotReload(); // B is no longer a subtype of A. - Expect.equals( - "type '(A) => bool' is not a subtype of type '(B) => bool'", helper()); + Expect.throws( + () => helper(), + (error) => '$error'.contains( + "type '(A) => bool' is not a subtype of type '(B) => bool'")); Expect.equals(1, hotReloadGeneration); } diff --git a/tests/hot_reload/existing_field_changes_type_indirect_function/main.1.dart b/tests/hot_reload/existing_field_changes_type_indirect_function/main.1.dart index d3250f854f3..1f91ea56db7 100644 --- a/tests/hot_reload/existing_field_changes_type_indirect_function/main.1.dart +++ b/tests/hot_reload/existing_field_changes_type_indirect_function/main.1.dart @@ -22,11 +22,7 @@ class Foo { late Foo value; helper() { - try { - return value.x.toString(); - } catch (e) { - return e.toString(); - } + return value.x.toString(); } Future main() async { @@ -36,8 +32,10 @@ Future main() async { await hotReload(); // B is no longer a subtype of A. - Expect.equals( - "type '(A) => bool' is not a subtype of type '(B) => bool'", helper()); + Expect.throws( + () => helper(), + (error) => '$error'.contains( + "type '(A) => bool' is not a subtype of type '(B) => bool'")); Expect.equals(1, hotReloadGeneration); } /** DIFF **/ @@ -51,17 +49,13 @@ Future main() async { typedef bool Predicate(B b); -@@ -22,8 +22,11 @@ +@@ -22,8 +22,7 @@ late Foo value; helper() { - value = Foo((A a) => true); - return 'okay'; -+ try { -+ return value.x.toString(); -+ } catch (e) { -+ return e.toString(); -+ } ++ return value.x.toString(); } Future main() async {