[vm,dart2wasm,tfa] Support dynamic interface in TFA

TEST=pkg/vm/testcases/transformations/type_flow/transformer/dynamic_module_extendable.dart

Change-Id: I6ef43053d86b1a12259f1cb4d0b542c9519591f8
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/370143
Reviewed-by: Slava Egorov <vegorov@google.com>
Commit-Queue: Alexander Markov <alexmarkov@google.com>
diff --git a/pkg/dart2wasm/lib/target.dart b/pkg/dart2wasm/lib/target.dart
index 1f8fa1f..dbd169e 100644
--- a/pkg/dart2wasm/lib/target.dart
+++ b/pkg/dart2wasm/lib/target.dart
@@ -480,7 +480,8 @@
   }
 
   @override
-  bool isSupportedPragma(String pragmaName) => pragmaName.startsWith("wasm:");
+  bool isSupportedPragma(String pragmaName) =>
+      pragmaName.startsWith("wasm:") || pragmaName.startsWith("dyn-module:");
 
   late final Map<RecordShape, Class> recordClasses;
 
diff --git a/pkg/vm/lib/metadata/unboxing_info.dart b/pkg/vm/lib/metadata/unboxing_info.dart
index 235891b..d447822 100644
--- a/pkg/vm/lib/metadata/unboxing_info.dart
+++ b/pkg/vm/lib/metadata/unboxing_info.dart
@@ -140,6 +140,12 @@
     return false;
   }
 
+  void setFullyBoxed() {
+    argsInfo.length = 0;
+    returnInfo = UnboxingType.kBoxed;
+    mustUseStackCallingConvention = true;
+  }
+
   // Returns `true` if this [UnboxingInfoMetadata] matches default one:
   // all arguments and the return value are boxed, the method is not
   // forced to use stack based calling convention and there is no override
diff --git a/pkg/vm/lib/modular/target/vm.dart b/pkg/vm/lib/modular/target/vm.dart
index e2addb9..d4b5ef0 100644
--- a/pkg/vm/lib/modular/target/vm.dart
+++ b/pkg/vm/lib/modular/target/vm.dart
@@ -527,5 +527,6 @@
       : const CustomizedDartLibrarySupport(unsupported: {'mirrors'});
 
   @override
-  bool isSupportedPragma(String pragmaName) => pragmaName.startsWith("vm:");
+  bool isSupportedPragma(String pragmaName) =>
+      pragmaName.startsWith("vm:") || pragmaName.startsWith("dyn-module:");
 }
diff --git a/pkg/vm/lib/transformations/pragma.dart b/pkg/vm/lib/transformations/pragma.dart
index 5930f6b..e89ed45 100644
--- a/pkg/vm/lib/transformations/pragma.dart
+++ b/pkg/vm/lib/transformations/pragma.dart
@@ -30,7 +30,14 @@
 
 abstract class ParsedPragma {}
 
-enum PragmaEntryPointType { Default, GetterOnly, SetterOnly, CallOnly }
+enum PragmaEntryPointType {
+  Default,
+  Extendable,
+  CanBeOverridden,
+  GetterOnly,
+  SetterOnly,
+  CallOnly
+}
 
 enum PragmaRecognizedType { AsmIntrinsic, GraphIntrinsic, Other }
 
@@ -72,6 +79,10 @@
   const ParsedPlatformConstPragma();
 }
 
+class ParsedDynModuleEntryPointPragma implements ParsedPragma {
+  const ParsedDynModuleEntryPointPragma();
+}
+
 abstract class PragmaAnnotationParser {
   /// May return 'null' if the annotation does not represent a recognized
   /// @pragma.
@@ -177,20 +188,29 @@
       case kVmDisableUnboxedParametersPragmaName:
         return const ParsedDisableUnboxedParameters();
       case kVmKeepNamePragmaName:
-        return ParsedKeepNamePragma();
+        return const ParsedKeepNamePragma();
       case kVmPlatformConstPragmaName:
-        return ParsedPlatformConstPragma();
+        return const ParsedPlatformConstPragma();
       case kVmPlatformConstIfPragmaName:
         if (options is! BoolConstant) {
           throw "ERROR: Non-boolean option to '$kVmPlatformConstIfPragmaName' "
               "pragma: $options";
         }
-        return options.value ? ParsedPlatformConstPragma() : null;
+        return options.value ? const ParsedPlatformConstPragma() : null;
       case kWasmEntryPointPragmaName:
-        return ParsedEntryPointPragma(PragmaEntryPointType.Default);
+        return const ParsedEntryPointPragma(PragmaEntryPointType.Default);
       case kWasmExportPragmaName:
         // Exports are treated as entry points.
-        return ParsedEntryPointPragma(PragmaEntryPointType.Default);
+        return const ParsedEntryPointPragma(PragmaEntryPointType.Default);
+      case kDynModuleExtendablePragmaName:
+        return const ParsedEntryPointPragma(PragmaEntryPointType.Extendable);
+      case kDynModuleCanBeOverriddenPragmaName:
+        return const ParsedEntryPointPragma(
+            PragmaEntryPointType.CanBeOverridden);
+      case kDynModuleCallablePragmaName:
+        return const ParsedEntryPointPragma(PragmaEntryPointType.Default);
+      case kDynModuleEntryPointPragmaName:
+        return const ParsedDynModuleEntryPointPragma();
       default:
         return null;
     }
diff --git a/pkg/vm/lib/transformations/type_flow/analysis.dart b/pkg/vm/lib/transformations/type_flow/analysis.dart
index a25e3c1..f23ec89 100644
--- a/pkg/vm/lib/transformations/type_flow/analysis.dart
+++ b/pkg/vm/lib/transformations/type_flow/analysis.dart
@@ -469,7 +469,7 @@
   }
 
   /// Marker for noSuchMethod() invocation in the map of invocation targets.
-  static final Member kNoSuchMethodMarker = new Procedure(
+  static final Member noSuchMethodMarker = new Procedure(
       new Name('noSuchMethod&&'), ProcedureKind.Method, new FunctionNode(null),
       fileUri: dummyUri);
 
@@ -486,6 +486,8 @@
     // along with more accurate receiver types for each target.
     final targets = <Member, _ReceiverTypeBuilder>{};
     final selector = this.selector;
+    Type result = emptyType;
+    bool hasUnknownTargets = false;
     if (selector is FunctionSelector) {
       if (!_collectTargetsForFunctionCall(
           args.receiver, targets, typeFlowAnalysis)) {
@@ -494,19 +496,25 @@
         return selector.staticResultType;
       }
     } else {
-      _collectTargetsForReceiverType(args.receiver, targets, typeFlowAnalysis);
+      if (!_collectTargetsForReceiverType(
+          args.receiver, targets, typeFlowAnalysis)) {
+        // Set of targets is not fully known at compilation time.
+        hasUnknownTargets = true;
+        _setPolymorphic();
+        result = typeFlowAnalysis.hierarchyCache
+            .fromStaticType(selector.staticReturnType, true);
+      }
     }
 
     // Calculate result as a union of results of direct invocations
     // corresponding to each target.
-    Type result = emptyType;
 
     if (targets.isEmpty) {
       tracePrint("No targets...");
     } else {
       if (targets.length == 1) {
         final target = targets.keys.single;
-        if (target != kNoSuchMethodMarker) {
+        if (!identical(target, noSuchMethodMarker) && !hasUnknownTargets) {
           _setMonomorphicTarget(target);
         } else {
           _setPolymorphic();
@@ -520,7 +528,7 @@
         Type receiver = receiverTypeBuilder.toType();
         Type type;
 
-        if (target == kNoSuchMethodMarker) {
+        if (identical(target, noSuchMethodMarker)) {
           // Non-dynamic call-sites must hit NSM-forwarders in Dart 2.
           assert(selector is DynamicSelector);
           type = _processNoSuchMethod(receiver, typeFlowAnalysis);
@@ -582,7 +590,8 @@
     return result;
   }
 
-  void _collectTargetsForReceiverType(
+  // Returns true if set of targets is known at compilation time.
+  bool _collectTargetsForReceiverType(
       Type receiver,
       Map<Member, _ReceiverTypeBuilder> targets,
       TypeFlowAnalysis typeFlowAnalysis) {
@@ -608,7 +617,11 @@
       }
     }
 
+    ConeType? dynamicallyExtendableReceiver;
     if (receiver is ConeType) {
+      if (receiver.cls.hasDynamicallyExtendableSubtypes) {
+        dynamicallyExtendableReceiver = receiver;
+      }
       // Specialization of type cone will add dependency of the current
       // invocation to the receiver class. A new allocated class discovered
       // in the receiver cone will invalidate this invocation.
@@ -633,6 +646,13 @@
     if (isNullableReceiver) {
       _collectTargetsForNull(targets, typeFlowAnalysis);
     }
+
+    if (dynamicallyExtendableReceiver != null) {
+      return _collectTargetsForDynamicallyExtendableType(
+          dynamicallyExtendableReceiver, targets, typeFlowAnalysis);
+    }
+
+    return true;
   }
 
   void _collectTargetsForNull(Map<Member, _ReceiverTypeBuilder> targets,
@@ -672,14 +692,14 @@
       if (kPrintTrace) {
         tracePrint("Found non-trivial noSuchMethod for receiver $receiver");
       }
-      _getReceiverTypeBuilder(targets, kNoSuchMethodMarker)
+      _getReceiverTypeBuilder(targets, noSuchMethodMarker)
           .addConcreteType(receiver);
     } else if (selector is DynamicSelector) {
       if (kPrintTrace) {
         tracePrint(
             "Dynamic selector - adding noSuchMethod for receiver $receiver");
       }
-      _getReceiverTypeBuilder(targets, kNoSuchMethodMarker)
+      _getReceiverTypeBuilder(targets, noSuchMethodMarker)
           .addConcreteType(receiver);
     } else {
       if (kPrintTrace) {
@@ -713,7 +733,7 @@
     // Conservatively include noSuchMethod if selector is not from Object,
     // as class might miss the implementation.
     if (!dynamicTargetSet.isObjectMember) {
-      _getReceiverTypeBuilder(targets, kNoSuchMethodMarker).addType(receiver);
+      _getReceiverTypeBuilder(targets, noSuchMethodMarker).addType(receiver);
     }
   }
 
@@ -736,6 +756,38 @@
     return false;
   }
 
+  bool _collectTargetsForDynamicallyExtendableType(
+      ConeType receiver,
+      Map<Member, _ReceiverTypeBuilder> targets,
+      TypeFlowAnalysis typeFlowAnalysis) {
+    final cls = receiver.cls as _TFClassImpl;
+    // Collect possible targets among dynamically extendable
+    // subtypes as they may have allocated subtypes at run time.
+    final receiverTypeBuilder = _ReceiverTypeBuilder();
+    receiverTypeBuilder.addType(receiver);
+    bool isDynamicallyOverridden = false;
+    for (final extendableSubtype in cls._dynamicallyExtendableSubtypes) {
+      Member? target = extendableSubtype.getDispatchTarget(selector);
+      if (target != null) {
+        if (areArgumentsValidFor(target)) {
+          // Overwrite previously added receiver type builder.
+          targets[target] = receiverTypeBuilder;
+          isDynamicallyOverridden = isDynamicallyOverridden ||
+              typeFlowAnalysis.nativeCodeOracle
+                  .isDynamicallyOverriddenMember(target);
+        } else {
+          assert(selector is DynamicSelector);
+          _recordMismatchedDynamicInvocation(target, typeFlowAnalysis);
+        }
+      }
+    }
+    if (selector is DynamicSelector) {
+      targets[noSuchMethodMarker] = receiverTypeBuilder;
+      isDynamicallyOverridden = true;
+    }
+    return isDynamicallyOverridden && !selector.name.isPrivate;
+  }
+
   void _recordMismatchedDynamicInvocation(
       Member target, TypeFlowAnalysis typeFlowAnalysis) {
     // Although target is not going to be called because of
@@ -1164,21 +1216,75 @@
 class _TFClassImpl extends TFClass {
   final _TFClassImpl? superclass;
   final Set<_TFClassImpl> _allocatedSubtypes = new Set<_TFClassImpl>();
+  final Set<_TFClassImpl> _dynamicallyExtendableSubtypes =
+      new Set<_TFClassImpl>();
   late final Map<Name, Member> _dispatchTargetsSetters =
       _initDispatchTargets(true);
   late final Map<Name, Member> _dispatchTargetsNonSetters =
       _initDispatchTargets(false);
   final _DependencyTracker dependencyTracker = new _DependencyTracker();
 
-  /// Flag indicating if this class has a noSuchMethod() method not inherited
-  /// from Object.
-  /// Lazy initialized by ClassHierarchyCache.hasNonTrivialNoSuchMethod().
-  bool? hasNonTrivialNoSuchMethod;
+  // Flag indicating if this class has a noSuchMethod() method not inherited
+  // from Object.
+  // Lazy initialized by ClassHierarchyCache.hasNonTrivialNoSuchMethod().
+  static const int flagHasNonTrivialNoSuchMethod = 1 << 0;
+
+  // Flag indicating if flagHasNonTrivialNoSuchMethod was initialized.
+  static const int flagHasNonTrivialNoSuchMethodInitialized = 1 << 1;
+
+  // This class can be extended by a dynamically loaded class
+  // (unknown at compilation time).
+  static const int flagIsDynamicallyExtendable = 1 << 2;
+
+  // This class has a subtype which can be extended by a
+  // dynamically loaded class (unknown at compilation time).
+  static const int flagHasDynamicallyExtendableSubtypes = 1 << 3;
+
+  int _flags = 0;
 
   _TFClassImpl(int id, Class classNode, this.superclass,
       Set<TFClass> supertypes, RecordShape? recordShape)
       : super(id, classNode, supertypes, recordShape);
 
+  bool get hasNonTrivialNoSuchMethodInitialized =>
+      (_flags & flagHasNonTrivialNoSuchMethodInitialized) != 0;
+
+  bool get hasNonTrivialNoSuchMethod =>
+      (_flags & flagHasNonTrivialNoSuchMethod) != 0;
+
+  set hasNonTrivialNoSuchMethod(bool value) {
+    if (value) {
+      _flags = _flags |
+          flagHasNonTrivialNoSuchMethod |
+          flagHasNonTrivialNoSuchMethodInitialized;
+    } else {
+      _flags = (_flags & ~flagHasNonTrivialNoSuchMethod) |
+          flagHasNonTrivialNoSuchMethodInitialized;
+    }
+  }
+
+  bool get isDynamicallyExtendable =>
+      (_flags & flagIsDynamicallyExtendable) != 0;
+
+  set isDynamicallyExtendable(bool value) {
+    if (value) {
+      _flags |= flagIsDynamicallyExtendable;
+    } else {
+      _flags &= ~flagIsDynamicallyExtendable;
+    }
+  }
+
+  bool get hasDynamicallyExtendableSubtypes =>
+      (_flags & flagHasDynamicallyExtendableSubtypes) != 0;
+
+  set hasDynamicallyExtendableSubtypes(bool value) {
+    if (value) {
+      _flags |= flagHasDynamicallyExtendableSubtypes;
+    } else {
+      _flags &= ~flagHasDynamicallyExtendableSubtypes;
+    }
+  }
+
   Type? _specializedConeType;
   Type get specializedConeType =>
       _specializedConeType ??= _calculateConeTypeSpecialization();
@@ -1431,12 +1537,21 @@
     return cls._dispatchTargetsNonSetters[Name(name)] as Field;
   }
 
+  void addDynamicallyExtendableClass(_TFClassImpl cls) {
+    cls.isDynamicallyExtendable = true;
+    for (final supertype in cls.supertypes) {
+      final supertypeImpl = supertype as _TFClassImpl;
+      supertypeImpl.hasDynamicallyExtendableSubtypes = true;
+      supertypeImpl._dynamicallyExtendableSubtypes.add(cls);
+    }
+  }
+
   void seal() {
     _sealed = true;
   }
 
   @override
-  Type specializeTypeCone(TFClass baseClass, {bool allowWideCone = false}) {
+  Type specializeTypeCone(TFClass baseClass, {required bool allowWideCone}) {
     if (kPrintTrace) {
       tracePrint("specializeTypeCone for $baseClass");
     }
@@ -1476,17 +1591,17 @@
 
   bool _hasWideCone(_TFClassImpl cls) =>
       cls._allocatedSubtypes.length >
-      _typeFlowAnalysis.config.maxAllocatedTypesInSetSpecialization;
+          _typeFlowAnalysis.config.maxAllocatedTypesInSetSpecialization ||
+      cls.hasDynamicallyExtendableSubtypes;
 
   bool hasNonTrivialNoSuchMethod(TFClass c) {
     final classImpl = c as _TFClassImpl;
-    bool? value = classImpl.hasNonTrivialNoSuchMethod;
-    if (value == null) {
-      classImpl.hasNonTrivialNoSuchMethod = value =
-          (classImpl._dispatchTargetsNonSetters[noSuchMethodName] !=
-              objectNoSuchMethod);
+    if (classImpl.hasNonTrivialNoSuchMethodInitialized) {
+      return classImpl.hasNonTrivialNoSuchMethod;
     }
-    return value;
+    return classImpl.hasNonTrivialNoSuchMethod =
+        (classImpl._dispatchTargetsNonSetters[noSuchMethodName] !=
+            objectNoSuchMethod);
   }
 
   _DynamicTargetSet getDynamicTargetSet(DynamicSelector selector) {
@@ -2017,6 +2132,14 @@
     return callMethod;
   }
 
+  @override
+  void addDynamicallyExtendableClass(Class c) {
+    if (kPrintDebug) {
+      debugPrint("ADD DYNAMICALLY EXTENDABLE CLASS: $c");
+    }
+    hierarchyCache.addDynamicallyExtendableClass(hierarchyCache.getTFClass(c));
+  }
+
   /// ---- Implementation of [SharedVariableBuilder] interface. ----
 
   @override
diff --git a/pkg/vm/lib/transformations/type_flow/native_code.dart b/pkg/vm/lib/transformations/type_flow/native_code.dart
index a6d6fb0..fe4f1ba 100644
--- a/pkg/vm/lib/transformations/type_flow/native_code.dart
+++ b/pkg/vm/lib/transformations/type_flow/native_code.dart
@@ -42,6 +42,10 @@
 
   /// Artificial call method corresponding to the given [closure].
   Procedure getClosureCallMethod(Closure closure);
+
+  /// Add class which can be extended by a dynamically loaded class
+  /// (unknown at compilation time).
+  void addDynamicallyExtendableClass(Class c);
 }
 
 class PragmaEntryPointsVisitor extends RecursiveVisitor {
@@ -65,14 +69,18 @@
   visitClass(Class klass) {
     final type = _annotationsDefineRoot(klass.annotations);
     if (type != null) {
-      if (type != PragmaEntryPointType.Default) {
+      if (type == PragmaEntryPointType.Default) {
+        if (!klass.isAbstract) {
+          entryPoints.addAllocatedClass(klass);
+        }
+        nativeCodeOracle.addClassReferencedFromNativeCode(klass);
+      } else if (type == PragmaEntryPointType.Extendable) {
+        entryPoints.addDynamicallyExtendableClass(klass);
+        nativeCodeOracle.addClassReferencedFromNativeCode(klass);
+      } else {
         throw "Error: pragma entry-point definition on a class must evaluate "
             "to null, true or false. See entry_points_pragma.md.";
       }
-      if (!klass.isAbstract) {
-        entryPoints.addAllocatedClass(klass);
-      }
-      nativeCodeOracle.addClassReferencedFromNativeCode(klass);
     }
     klass.visitChildren(this);
   }
@@ -116,6 +124,12 @@
         if (!proc.isSetter && !proc.isGetter && !proc.isFactory) {
           addSelector(CallKind.PropertyGet);
         }
+        break;
+      case PragmaEntryPointType.Extendable:
+        throw "Error: only class can be extendable";
+      case PragmaEntryPointType.CanBeOverridden:
+        nativeCodeOracle.addDynamicallyOverriddenMember(proc);
+        break;
     }
 
     nativeCodeOracle.setMemberReferencedFromNativeCode(proc);
@@ -168,6 +182,11 @@
       case PragmaEntryPointType.CallOnly:
         throw "Error: can't generate invocation dispatcher for field $field"
             "through @pragma('vm:entry-point')";
+      case PragmaEntryPointType.Extendable:
+        throw "Error: only class can be extendable";
+      case PragmaEntryPointType.CanBeOverridden:
+        nativeCodeOracle.addDynamicallyOverriddenMember(field);
+        break;
     }
 
     nativeCodeOracle.setMemberReferencedFromNativeCode(field);
@@ -178,6 +197,7 @@
 class NativeCodeOracle {
   final LibraryIndex _libraryIndex;
   final Set<Member> _membersReferencedFromNativeCode = new Set<Member>();
+  final Set<Member> _dynamicallyOverriddenMembers = new Set<Member>();
   final Set<Class> _classesReferencedFromNativeCode = new Set<Class>();
   final PragmaAnnotationParser _matcher;
 
@@ -197,6 +217,13 @@
   bool isMemberReferencedFromNativeCode(Member member) =>
       _membersReferencedFromNativeCode.contains(member);
 
+  void addDynamicallyOverriddenMember(Member member) {
+    _dynamicallyOverriddenMembers.add(member);
+  }
+
+  bool isDynamicallyOverriddenMember(Member member) =>
+      _dynamicallyOverriddenMembers.contains(member);
+
   PragmaRecognizedType? recognizedType(Member member) {
     for (var annotation in member.annotations) {
       ParsedPragma? pragma = _matcher.parsePragma(annotation);
diff --git a/pkg/vm/lib/transformations/type_flow/rta.dart b/pkg/vm/lib/transformations/type_flow/rta.dart
index a8a6de8..0627aa2 100644
--- a/pkg/vm/lib/transformations/type_flow/rta.dart
+++ b/pkg/vm/lib/transformations/type_flow/rta.dart
@@ -559,6 +559,9 @@
   ConcreteType addAllocatedClass(Class c) => rta.addAllocatedClass(c);
 
   @override
+  void addDynamicallyExtendableClass(Class c) {}
+
+  @override
   Field getRecordPositionalField(RecordShape shape, int pos) =>
       throw 'Unsupported operation';
 
diff --git a/pkg/vm/lib/transformations/type_flow/types.dart b/pkg/vm/lib/transformations/type_flow/types.dart
index 2355726..9a329c3 100644
--- a/pkg/vm/lib/transformations/type_flow/types.dart
+++ b/pkg/vm/lib/transformations/type_flow/types.dart
@@ -75,6 +75,8 @@
     return result;
   }
 
+  bool get hasDynamicallyExtendableSubtypes => false;
+
   @override
   int get hashCode => id;
 
@@ -290,7 +292,7 @@
   /// It is correct (although less accurate) for [specializeTypeCone] to return
   /// a larger set. In such case analysis would admit that a larger set of
   /// values can flow through the program.
-  Type specializeTypeCone(TFClass base, {bool allowWideCone = false});
+  Type specializeTypeCone(TFClass base, {required bool allowWideCone});
 
   /// Returns true if [cls] has allocated subtypes.
   bool hasAllocatedSubtypes(TFClass cls);
@@ -870,6 +872,7 @@
 
   @override
   bool hasEmptySpecialization(TypeHierarchy typeHierarchy) =>
+      !cls.hasDynamicallyExtendableSubtypes &&
       !typeHierarchy.hasAllocatedSubtypes(cls);
 
   @override
@@ -927,7 +930,8 @@
   }
 }
 
-/// Type representing a subtype cone which has too many concrete classes.
+/// Type representing a subtype cone which has too many concrete classes
+/// or may contain dynamically loaded subtypes (unknown at compilation time).
 /// It contains instances of all Dart types which extend, mix-in or implement
 /// certain class.
 class WideConeType extends ConeType {
diff --git a/pkg/vm/lib/transformations/type_flow/unboxing_info.dart b/pkg/vm/lib/transformations/type_flow/unboxing_info.dart
index c1feb30..d51953e 100644
--- a/pkg/vm/lib/transformations/type_flow/unboxing_info.dart
+++ b/pkg/vm/lib/transformations/type_flow/unboxing_info.dart
@@ -127,14 +127,17 @@
 
   void _updateUnboxingInfoOfMember(Member member,
       TypeFlowAnalysis typeFlowAnalysis, UnboxingInfoMetadata unboxingInfo) {
+    if (_nativeCodeOracle.isDynamicallyOverriddenMember(member)) {
+      unboxingInfo.setFullyBoxed();
+      return;
+    }
+
     if (!typeFlowAnalysis.isMemberUsed(member)) {
       return;
     }
 
     if (_cannotUnbox(member)) {
-      unboxingInfo.argsInfo.length = 0;
-      unboxingInfo.returnInfo = UnboxingType.kBoxed;
-      unboxingInfo.mustUseStackCallingConvention = true;
+      unboxingInfo.setFullyBoxed();
       return;
     }
 
diff --git a/pkg/vm/test/transformations/type_flow/summary_collector_test.dart b/pkg/vm/test/transformations/type_flow/summary_collector_test.dart
index e495b8e..e0fc9c8 100644
--- a/pkg/vm/test/transformations/type_flow/summary_collector_test.dart
+++ b/pkg/vm/test/transformations/type_flow/summary_collector_test.dart
@@ -74,6 +74,9 @@
 
   @override
   Procedure getClosureCallMethod(Closure closure) => closure.createCallMethod();
+
+  @override
+  void addDynamicallyExtendableClass(Class c) {}
 }
 
 class FakeSharedVariable implements SharedVariable {
diff --git a/pkg/vm/test/transformations/type_flow/types_test.dart b/pkg/vm/test/transformations/type_flow/types_test.dart
index 36b9906..3a9afb2 100644
--- a/pkg/vm/test/transformations/type_flow/types_test.dart
+++ b/pkg/vm/test/transformations/type_flow/types_test.dart
@@ -22,13 +22,13 @@
       : super(coreTypes, target);
 
   @override
-  Type specializeTypeCone(TFClass base, {bool allowWideCone = false}) {
+  Type specializeTypeCone(TFClass base, {required bool allowWideCone}) {
     return specializations[base.classNode]!;
   }
 
   @override
   bool hasAllocatedSubtypes(TFClass cls) =>
-      specializeTypeCone(cls) is! EmptyType;
+      specializeTypeCone(cls, allowWideCone: true) is! EmptyType;
 
   @override
   TFClass getTFClass(Class c) =>
diff --git a/pkg/vm/testcases/transformations/type_flow/transformer/dynamic_module_extendable.dart b/pkg/vm/testcases/transformations/type_flow/transformer/dynamic_module_extendable.dart
new file mode 100644
index 0000000..02e0328
--- /dev/null
+++ b/pkg/vm/testcases/transformations/type_flow/transformer/dynamic_module_extendable.dart
@@ -0,0 +1,56 @@
+// Copyright (c) 2024, 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.
+
+// Test for @pragma('dyn-module:extendable').
+
+abstract class A1 {
+  Object foo(); // Unboxed.
+}
+
+class B1 implements A1 {
+  int foo() => 42;
+}
+
+abstract class C1 implements A1 {} // Unused, eliminated.
+
+abstract class A2 {
+  @pragma('dyn-module:can-be-overridden')
+  Object foo(); // Not unboxed.
+}
+
+class B2 implements A2 {
+  int foo() => 42;
+}
+
+@pragma('dyn-module:extendable')
+abstract class C2 implements A2 {} // Not eliminated.
+
+void call1(A1 obj) {
+  print(obj.foo()); // Devirtualized, constant result.
+}
+
+void call2(A2 obj) {
+  print(obj.foo()); // Not devirtualized, no constant result.
+}
+
+void cast1(A1 obj) {
+  print(obj is B1); // Eliminated.
+  print(obj as B1); // Eliminated.
+}
+
+void cast2(A2 obj) {
+  print(obj is B2); // Not eliminated.
+  print(obj as B2); // Not eliminated.
+}
+
+List opaque = []
+  ..add(B1())
+  ..add(B2());
+
+main() {
+  call1(opaque[0]);
+  call2(opaque[1]);
+  cast1(opaque[0]);
+  cast2(opaque[1]);
+}
diff --git a/pkg/vm/testcases/transformations/type_flow/transformer/dynamic_module_extendable.dart.expect b/pkg/vm/testcases/transformations/type_flow/transformer/dynamic_module_extendable.dart.expect
new file mode 100644
index 0000000..4f2386b
--- /dev/null
+++ b/pkg/vm/testcases/transformations/type_flow/transformer/dynamic_module_extendable.dart.expect
@@ -0,0 +1,86 @@
+library #lib;
+import self as self;
+import "dart:core" as core;
+import "dart:_internal" as _in;
+
+abstract class A1 extends core::Object {
+
+  [@vm.procedure-attributes.metadata=methodOrSetterCalledDynamically:false,getterCalledDynamically:false,hasThisUses:false,hasNonThisUses:false,hasTearOffUses:false,methodOrSetterSelectorId:1,getterSelectorId:2]
+  [@vm.unboxing-info.metadata=()->i]
+  abstract method foo() → core::Object;
+}
+class B1 extends core::Object implements self::A1 {
+  synthetic constructor •() → self::B1
+    : super core::Object::•()
+    ;
+
+  [@vm.inferred-return-type.metadata=dart.core::_Smi (value: 42)]
+  [@vm.procedure-attributes.metadata=methodOrSetterCalledDynamically:false,getterCalledDynamically:false,hasThisUses:false,hasTearOffUses:false,methodOrSetterSelectorId:1,getterSelectorId:2]
+  [@vm.unboxing-info.metadata=()->i]
+  method foo() → core::int
+    return 42;
+}
+abstract class A2 extends core::Object {
+
+  [@vm.procedure-attributes.metadata=methodOrSetterCalledDynamically:false,getterCalledDynamically:false,hasThisUses:false,hasNonThisUses:false,hasTearOffUses:false,methodOrSetterSelectorId:3,getterSelectorId:4]
+  [@vm.unboxing-info.metadata=[!regcc]]
+  @#C3
+  abstract method foo() → core::Object;
+}
+class B2 extends core::Object implements self::A2 {
+  synthetic constructor •() → self::B2
+    : super core::Object::•()
+    ;
+
+  [@vm.inferred-return-type.metadata=dart.core::_Smi (value: 42)]
+  [@vm.procedure-attributes.metadata=methodOrSetterCalledDynamically:false,getterCalledDynamically:false,hasThisUses:false,hasTearOffUses:false,methodOrSetterSelectorId:3,getterSelectorId:4]
+  [@vm.unboxing-info.metadata=[!regcc]]
+  method foo() → core::int
+    return 42;
+}
+@#C5
+abstract class C2 extends core::Object implements self::A2 {
+}
+
+[@vm.inferred-type.metadata=dart.core::_GrowableList<dynamic>]
+static field core::List<dynamic> opaque = let final core::List<dynamic> #t1 = [@vm.inferred-type.metadata=dart.core::_GrowableList<dynamic>] core::_GrowableList::•<dynamic>(0) in block {
+  [@vm.call-site-attributes.metadata=receiverType:dart.core::List<dynamic>] [@vm.direct-call.metadata=dart.core::_GrowableList.add] [@vm.inferred-type.metadata=!? (skip check)] #t1.{core::List::add}(new self::B1::•()){(dynamic) → void};
+  [@vm.call-site-attributes.metadata=receiverType:dart.core::List<dynamic>] [@vm.direct-call.metadata=dart.core::_GrowableList.add] [@vm.inferred-type.metadata=!? (skip check)] #t1.{core::List::add}(new self::B2::•()){(dynamic) → void};
+} =>#t1;
+
+[@vm.inferred-return-type.metadata=dart.core::Null? (value: null)]
+static method call1([@vm.inferred-arg-type.metadata=#lib::B1] self::A1 obj) → void {
+  core::print([@vm.direct-call.metadata=#lib::B1.foo] [@vm.inferred-type.metadata=dart.core::_Smi (skip check) (value: 42)] obj.{self::A1::foo}(){() → core::Object});
+}
+
+[@vm.inferred-return-type.metadata=dart.core::Null? (value: null)]
+static method call2([@vm.inferred-arg-type.metadata=!] self::A2 obj) → void {
+  core::print([@vm.inferred-type.metadata=!] obj.{self::A2::foo}(){() → core::Object});
+}
+
+[@vm.inferred-return-type.metadata=dart.core::Null? (value: null)]
+static method cast1([@vm.inferred-arg-type.metadata=#lib::B1] self::A1 obj) → void {
+  core::print(true);
+  core::print(_in::unsafeCast<self::B1>(obj));
+}
+
+[@vm.inferred-return-type.metadata=dart.core::Null? (value: null)]
+static method cast2([@vm.inferred-arg-type.metadata=!] self::A2 obj) → void {
+  core::print(obj is self::B2);
+  core::print(obj as self::B2);
+}
+
+[@vm.inferred-return-type.metadata=dart.core::Null? (value: null)]
+static method main() → dynamic {
+  self::call1([@vm.direct-call.metadata=dart.core::_GrowableList.[]] [@vm.inferred-type.metadata=!? (skip check)] [@vm.inferred-type.metadata=dart.core::_GrowableList<dynamic>] self::opaque.{core::List::[]}(0){(core::int) → dynamic} as{TypeError,ForDynamic} self::A1);
+  self::call2([@vm.direct-call.metadata=dart.core::_GrowableList.[]] [@vm.inferred-type.metadata=!? (skip check)] [@vm.inferred-type.metadata=dart.core::_GrowableList<dynamic>] self::opaque.{core::List::[]}(1){(core::int) → dynamic} as{TypeError,ForDynamic} self::A2);
+  self::cast1([@vm.direct-call.metadata=dart.core::_GrowableList.[]] [@vm.inferred-type.metadata=!? (skip check)] [@vm.inferred-type.metadata=dart.core::_GrowableList<dynamic>] self::opaque.{core::List::[]}(0){(core::int) → dynamic} as{TypeError,ForDynamic} self::A1);
+  self::cast2([@vm.direct-call.metadata=dart.core::_GrowableList.[]] [@vm.inferred-type.metadata=!? (skip check)] [@vm.inferred-type.metadata=dart.core::_GrowableList<dynamic>] self::opaque.{core::List::[]}(1){(core::int) → dynamic} as{TypeError,ForDynamic} self::A2);
+}
+constants  {
+  #C1 = "dyn-module:can-be-overridden"
+  #C2 = null
+  #C3 = core::pragma {name:#C1, options:#C2}
+  #C4 = "dyn-module:extendable"
+  #C5 = core::pragma {name:#C4, options:#C2}
+}