[js_runtime] Use marker interface to signal trusted .runtimeType overrides

Use the `TrustedGetRuntimeType` interface to mark implementation
classes that have a `get runtimeType` that should be used for
reporting the types of instances in errors and `Record.runtimeType`.

Streamline the various more general `get runtimeType` implementations
by specializing them to the context. By explictly implementing
`Closure.runtimeType` we avoid the need to test for closures in the
'ordinary' object path. This should help the Flutter pattern of
testing `a.runtimeType == b.runtimeType`.

Change-Id: Ib49a6d89ff23e752beed8753aafceff40457433d
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/291741
Reviewed-by: Mayank Patke <fishythefish@google.com>
Commit-Queue: Stephen Adams <sra@google.com>
diff --git a/pkg/compiler/test/inference/type_combination_test.dart b/pkg/compiler/test/inference/type_combination_test.dart
index ee8246b..8d772b0 100644
--- a/pkg/compiler/test/inference/type_combination_test.dart
+++ b/pkg/compiler/test/inference/type_combination_test.dart
@@ -32,6 +32,7 @@
 late TypeMask potentialString;
 late TypeMask jsInterceptor;
 late TypeMask jsInterceptorOrComparable;
+late TypeMask jsTrustedGetRuntimeType;
 
 late TypeMask jsIndexable;
 late TypeMask jsReadableArray;
@@ -49,6 +50,7 @@
 late TypeMask jsIndexableOrNull;
 late TypeMask jsInterceptorOrNull;
 late TypeMask jsInterceptorOrComparableOrNull;
+late TypeMask jsTrustedGetRuntimeTypeOrNull;
 
 class Pair {
   final first;
@@ -168,10 +170,10 @@
 
   rule(jsBoolean, jsBoolean, jsBoolean);
   rule(jsBoolean, jsNumber, jsInterceptor);
-  rule(jsBoolean, jsInteger, jsInterceptor);
-  rule(jsBoolean, jsNumNotInt, jsInterceptor);
+  rule(jsBoolean, jsInteger, jsTrustedGetRuntimeType);
+  rule(jsBoolean, jsNumNotInt, jsTrustedGetRuntimeType);
   rule(jsBoolean, jsIndexable, objectType);
-  rule(jsBoolean, jsString, jsInterceptor);
+  rule(jsBoolean, jsString, jsTrustedGetRuntimeType);
   rule(jsBoolean, jsReadableArray, jsInterceptor);
   rule(jsBoolean, jsMutableArray, jsInterceptor);
   rule(jsBoolean, jsExtendableArray, jsInterceptor);
@@ -182,9 +184,9 @@
   rule(jsBoolean, potentialString, dynamicType);
   rule(jsBoolean, jsBooleanOrNull, jsBooleanOrNull);
   rule(jsBoolean, jsNumberOrNull, jsInterceptorOrNull);
-  rule(jsBoolean, jsIntegerOrNull, jsInterceptorOrNull);
-  rule(jsBoolean, jsNumNotIntOrNull, jsInterceptorOrNull);
-  rule(jsBoolean, jsStringOrNull, jsInterceptorOrNull);
+  rule(jsBoolean, jsIntegerOrNull, jsTrustedGetRuntimeTypeOrNull);
+  rule(jsBoolean, jsNumNotIntOrNull, jsTrustedGetRuntimeTypeOrNull);
+  rule(jsBoolean, jsStringOrNull, jsTrustedGetRuntimeTypeOrNull);
   rule(jsBoolean, nullType, jsBooleanOrNull);
   rule(jsBoolean, jsFixedArray, jsInterceptor);
 
@@ -221,7 +223,7 @@
   rule(jsInteger, nonPrimitive2, objectType);
   rule(jsInteger, potentialArray, dynamicType);
   rule(jsInteger, potentialString, dynamicType);
-  rule(jsInteger, jsBooleanOrNull, jsInterceptorOrNull);
+  rule(jsInteger, jsBooleanOrNull, jsTrustedGetRuntimeTypeOrNull);
   rule(jsInteger, jsNumberOrNull, jsNumberOrNull);
   rule(jsInteger, jsIntegerOrNull, jsIntegerOrNull);
   rule(jsInteger, jsNumNotIntOrNull, jsNumberOrNull);
@@ -240,7 +242,7 @@
   rule(jsNumNotInt, nonPrimitive2, objectType);
   rule(jsNumNotInt, potentialArray, dynamicType);
   rule(jsNumNotInt, potentialString, dynamicType);
-  rule(jsNumNotInt, jsBooleanOrNull, jsInterceptorOrNull);
+  rule(jsNumNotInt, jsBooleanOrNull, jsTrustedGetRuntimeTypeOrNull);
   rule(jsNumNotInt, jsNumberOrNull, jsNumberOrNull);
   rule(jsNumNotInt, jsIntegerOrNull, jsNumberOrNull);
   rule(jsNumNotInt, jsNumNotIntOrNull, jsNumNotIntOrNull);
@@ -275,7 +277,7 @@
   rule(jsString, nonPrimitive2, objectType);
   rule(jsString, potentialArray, dynamicType);
   rule(jsString, potentialString, potentialString);
-  rule(jsString, jsBooleanOrNull, jsInterceptorOrNull);
+  rule(jsString, jsBooleanOrNull, jsTrustedGetRuntimeTypeOrNull);
   rule(jsString, jsNumberOrNull, jsInterceptorOrComparableOrNull);
   rule(jsString, jsIntegerOrNull, jsInterceptorOrComparableOrNull);
   rule(jsString, jsNumNotIntOrNull, jsInterceptorOrComparableOrNull);
@@ -383,9 +385,9 @@
 
   rule(jsBooleanOrNull, jsBooleanOrNull, jsBooleanOrNull);
   rule(jsBooleanOrNull, jsNumberOrNull, jsInterceptorOrNull);
-  rule(jsBooleanOrNull, jsIntegerOrNull, jsInterceptorOrNull);
-  rule(jsBooleanOrNull, jsNumNotIntOrNull, jsInterceptorOrNull);
-  rule(jsBooleanOrNull, jsStringOrNull, jsInterceptorOrNull);
+  rule(jsBooleanOrNull, jsIntegerOrNull, jsTrustedGetRuntimeTypeOrNull);
+  rule(jsBooleanOrNull, jsNumNotIntOrNull, jsTrustedGetRuntimeTypeOrNull);
+  rule(jsBooleanOrNull, jsStringOrNull, jsTrustedGetRuntimeTypeOrNull);
   rule(jsBooleanOrNull, nullType, jsBooleanOrNull);
   rule(jsBooleanOrNull, jsFixedArray, jsInterceptorOrNull);
 
@@ -782,6 +784,9 @@
   LibraryEntity coreLibrary = commonElements.coreLibrary;
   patternClass = elementEnvironment.lookupClass(coreLibrary, 'Pattern');
 
+  final trustedGetRuntimeTypeInterface = elementEnvironment.lookupClass(
+      commonElements.rtiLibrary, 'TrustedGetRuntimeType')!;
+
   nonPrimitive1 =
       TypeMask.nonNullSubtype(closedWorld.commonElements.mapClass, closedWorld);
   nonPrimitive2 = TypeMask.nonNullSubtype(
@@ -791,6 +796,8 @@
   potentialString = TypeMask.subtype(patternClass, closedWorld);
   jsInterceptor = TypeMask.nonNullSubclass(
       closedWorld.commonElements.jsInterceptorClass, closedWorld);
+  jsTrustedGetRuntimeType =
+      TypeMask.nonNullSubtype(trustedGetRuntimeTypeInterface, closedWorld);
   jsArrayOrNull =
       TypeMask.subclass(closedWorld.commonElements.jsArrayClass, closedWorld);
   jsReadableArray = TypeMask.nonNullSubclass(
@@ -817,6 +824,8 @@
       closedWorld.commonElements.jsIndexableClass, closedWorld);
   jsInterceptorOrNull = TypeMask.subclass(
       closedWorld.commonElements.jsInterceptorClass, closedWorld);
+  jsTrustedGetRuntimeTypeOrNull =
+      TypeMask.subtype(trustedGetRuntimeTypeInterface, closedWorld);
   jsStringOrNull =
       TypeMask.exact(closedWorld.commonElements.jsStringClass, closedWorld);
   jsString = TypeMask.nonNullExact(
diff --git a/pkg/compiler/test/rti/emission/map_literal.dart b/pkg/compiler/test/rti/emission/map_literal.dart
index 7803471..f3ec553 100644
--- a/pkg/compiler/test/rti/emission/map_literal.dart
+++ b/pkg/compiler/test/rti/emission/map_literal.dart
@@ -12,7 +12,7 @@
 
 /*spec.class: global#double:checkedInstance,instance,typeArgument*/
 
-/*class: global#JSNumNotInt:checks=[],instance*/
+/*class: global#JSNumNotInt:checks=[$isTrustedGetRuntimeType],instance*/
 
 main() {
   <int, double>{}[0] = 0.5;
diff --git a/sdk/lib/_internal/js_runtime/lib/core_patch.dart b/sdk/lib/_internal/js_runtime/lib/core_patch.dart
index 116cad6..ae38294 100644
--- a/sdk/lib/_internal/js_runtime/lib/core_patch.dart
+++ b/sdk/lib/_internal/js_runtime/lib/core_patch.dart
@@ -24,11 +24,12 @@
         getTraceFromException,
         RuntimeError,
         wrapException,
-        wrapZoneUnaryCallback;
+        wrapZoneUnaryCallback,
+        TrustedGetRuntimeType;
 
 import 'dart:_foreign_helper' show JS;
 import 'dart:_native_typed_data' show NativeUint8List;
-import 'dart:_rti' show getRuntimeType;
+import 'dart:_rti' show getRuntimeTypeOfDartObject;
 
 import 'dart:convert' show Encoding, utf8;
 import 'dart:typed_data' show Endian, Uint8List, Uint16List;
@@ -69,7 +70,7 @@
   }
 
   @patch
-  Type get runtimeType => getRuntimeType(this);
+  Type get runtimeType => getRuntimeTypeOfDartObject(this);
 }
 
 @patch
diff --git a/sdk/lib/_internal/js_runtime/lib/interceptors.dart b/sdk/lib/_internal/js_runtime/lib/interceptors.dart
index bb91148..7f91159 100644
--- a/sdk/lib/_internal/js_runtime/lib/interceptors.dart
+++ b/sdk/lib/_internal/js_runtime/lib/interceptors.dart
@@ -8,7 +8,7 @@
     show DISPATCH_PROPERTY_NAME, TYPE_TO_INTERCEPTOR_MAP;
 
 import 'dart:collection' hide LinkedList, LinkedListEntry;
-import 'dart:_foreign_helper' show JS_GET_FLAG;
+import 'dart:_foreign_helper' show JS_GET_FLAG, TYPE_REF;
 import 'dart:_internal' hide Symbol;
 import "dart:_internal" as _symbol_dev show Symbol;
 import 'dart:_js_helper'
@@ -52,7 +52,12 @@
         JS_INTERCEPTOR_CONSTANT,
         JS_STRING_CONCAT;
 
-import 'dart:_rti' show getRuntimeType;
+import 'dart:_rti'
+    show
+        createRuntimeType,
+        getRuntimeTypeOfArray,
+        getRuntimeTypeOfInterceptorNotArray,
+        TrustedGetRuntimeType;
 
 import 'dart:math' show Random, ln2;
 
@@ -338,11 +343,12 @@
     throw new NoSuchMethodError.withInvocation(this, invocation);
   }
 
-  Type get runtimeType => getRuntimeType(this);
+  Type get runtimeType =>
+      getRuntimeTypeOfInterceptorNotArray(getInterceptor(this), this);
 }
 
 /// The interceptor class for [bool].
-final class JSBool extends Interceptor implements bool {
+final class JSBool extends Interceptor implements bool, TrustedGetRuntimeType {
   const JSBool();
 
   // Note: if you change this, also change the function [S].
@@ -358,7 +364,8 @@
   // positions, including the low bit, so they are different mod 2^k.
   int get hashCode => this ? (2 * 3 * 23 * 3761) : (269 * 811);
 
-  Type get runtimeType => bool;
+  // Same as `=> bool`, but without a constant-pool object.
+  Type get runtimeType => createRuntimeType(TYPE_REF<bool>());
 }
 
 /// The interceptor class for [Null].
@@ -366,7 +373,7 @@
 /// This class defines implementations for *all* methods on [Object] since
 /// the methods on Object assume the receiver is non-null.  This means that
 /// JSNull will always be in the interceptor set for methods defined on Object.
-final class JSNull extends Interceptor implements Null {
+final class JSNull extends Interceptor implements Null, TrustedGetRuntimeType {
   const JSNull();
 
   external bool operator ==(Object other);
@@ -376,10 +383,8 @@
 
   int get hashCode => 0;
 
-  // The spec guarantees that `null` is the singleton instance of the `Null`
-  // class. In the mirrors library we also have to patch the `type` getter to
-  // special case `null`.
-  Type get runtimeType => Null;
+  // Same as `=> Null`, but without a constant-pool object.
+  Type get runtimeType => createRuntimeType(TYPE_REF<Null>());
 
   dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation);
 }
diff --git a/sdk/lib/_internal/js_runtime/lib/js_array.dart b/sdk/lib/_internal/js_runtime/lib/js_array.dart
index f18f169..3a3fb27 100644
--- a/sdk/lib/_internal/js_runtime/lib/js_array.dart
+++ b/sdk/lib/_internal/js_runtime/lib/js_array.dart
@@ -762,6 +762,10 @@
     if (this.isEmpty) throw IterableElementError.noElement();
     this[this.length - 1] = element;
   }
+
+  // Specialized version of `get runtimeType` is needed here so that
+  // `Interceptor.runtimeType` can avoid testing for `JSArray`.
+  Type get runtimeType => getRuntimeTypeOfArray(this);
 }
 
 /// Dummy subclasses that allow the backend to track more precise
diff --git a/sdk/lib/_internal/js_runtime/lib/js_helper.dart b/sdk/lib/_internal/js_runtime/lib/js_helper.dart
index a3f7ae4..e351e13 100644
--- a/sdk/lib/_internal/js_runtime/lib/js_helper.dart
+++ b/sdk/lib/_internal/js_runtime/lib/js_helper.dart
@@ -66,6 +66,7 @@
         evalInInstance,
         evaluateRtiForRecord,
         getRuntimeType,
+        getRuntimeTypeOfClosure,
         getRuntimeTypeOfRecord,
         getTypeFromTypesTable,
         instanceTypeName,
@@ -2419,6 +2420,8 @@
     if (name == null) name = 'unknown';
     return "Closure '${unminifyOrTag(name)}'";
   }
+
+  Type get runtimeType => newRti.getRuntimeTypeOfClosure(this);
 }
 
 /// This is called by the fragment emitter.
diff --git a/sdk/lib/_internal/js_runtime/lib/js_number.dart b/sdk/lib/_internal/js_runtime/lib/js_number.dart
index 288c0bb..27dc231 100644
--- a/sdk/lib/_internal/js_runtime/lib/js_number.dart
+++ b/sdk/lib/_internal/js_runtime/lib/js_number.dart
@@ -477,7 +477,8 @@
     return JS('bool', '# >= #', this, other);
   }
 
-  Type get runtimeType => num;
+  // Same as `=> num;`, but without a constant-pool object.
+  Type get runtimeType => createRuntimeType(TYPE_REF<num>());
 }
 
 /// The interceptor class for [int]s.
@@ -486,7 +487,7 @@
 /// JavaScript all numbers are doubles, so while we want to treat `2.0` as an
 /// integer for some operations, its interceptor should answer `true` to `is
 /// double`.
-final class JSInt extends JSNumber implements int {
+final class JSInt extends JSNumber implements int, TrustedGetRuntimeType {
   const JSInt();
 
   @override
@@ -683,15 +684,19 @@
     return _binaryGcd(x, y, false);
   }
 
-  Type get runtimeType => int;
+  // Same as `=> int;`, but without a constant-pool object.
+  Type get runtimeType => createRuntimeType(TYPE_REF<int>());
 
   int operator ~() => JS('JSUInt32', r'(~#) >>> 0', this);
 }
 
 /// Interceptor for JavaScript values that are not a subclass of [JSInt].
-final class JSNumNotInt extends JSNumber implements double {
+final class JSNumNotInt extends JSNumber
+    implements double, TrustedGetRuntimeType {
   const JSNumNotInt();
-  Type get runtimeType => double;
+
+  // Same as `=> double;`, but without a constant-pool object.
+  Type get runtimeType => createRuntimeType(TYPE_REF<double>());
 }
 
 final class JSPositiveInt extends JSInt {}
diff --git a/sdk/lib/_internal/js_runtime/lib/js_string.dart b/sdk/lib/_internal/js_runtime/lib/js_string.dart
index 14ce9c3..0c09013 100644
--- a/sdk/lib/_internal/js_runtime/lib/js_string.dart
+++ b/sdk/lib/_internal/js_runtime/lib/js_string.dart
@@ -8,7 +8,8 @@
 /// class as an interceptor, and changes references to [:this:] to
 /// actually use the receiver of the method, which is generated as an extra
 /// argument added to each member.
-final class JSString extends Interceptor implements String, JSIndexable {
+final class JSString extends Interceptor
+    implements String, JSIndexable, TrustedGetRuntimeType {
   const JSString();
 
   @pragma('dart2js:noInline')
@@ -192,8 +193,10 @@
         case 0x0D:
         case 0x20:
         case 0x85:
-        case 0xA0: return true;
-        default: return false;
+        case 0xA0:
+          return true;
+        default:
+          return false;
       }
     }
     switch (codeUnit) {
@@ -214,8 +217,10 @@
       case 0x202F:
       case 0x205F:
       case 0x3000:
-      case 0xFEFF: return true;
-      default: return false;
+      case 0xFEFF:
+        return true;
+      default:
+        return false;
     }
   }
 
@@ -455,7 +460,8 @@
     return 0x1fffffff & (hash + ((0x00003fff & hash) << 15));
   }
 
-  Type get runtimeType => String;
+  // Same as `=> String;`, but without a constant-pool object.
+  Type get runtimeType => createRuntimeType(TYPE_REF<String>());
 
   int get length => JS('int', r'#.length', this);
 
diff --git a/sdk/lib/_internal/js_runtime/lib/native_typed_data.dart b/sdk/lib/_internal/js_runtime/lib/native_typed_data.dart
index e54e7b7..011e68f 100644
--- a/sdk/lib/_internal/js_runtime/lib/native_typed_data.dart
+++ b/sdk/lib/_internal/js_runtime/lib/native_typed_data.dart
@@ -21,12 +21,15 @@
         diagnoseIndexError,
         diagnoseRangeError;
 import 'dart:_foreign_helper' show JS;
+import 'dart:_rti' show TrustedGetRuntimeType;
+
 import 'dart:math' as Math;
 
 import 'dart:typed_data';
 
 @Native('ArrayBuffer')
-final class NativeByteBuffer extends JavaScriptObject implements ByteBuffer {
+final class NativeByteBuffer extends JavaScriptObject
+    implements ByteBuffer, TrustedGetRuntimeType {
   @JSName('byteLength')
   int get lengthInBytes native;
 
@@ -104,7 +107,7 @@
 /// space- and time-efficient than the default [List] implementation.
 final class NativeFloat32x4List extends Object
     with ListMixin<Float32x4>, FixedLengthListMixin<Float32x4>
-    implements Float32x4List {
+    implements Float32x4List, TrustedGetRuntimeType {
   final Float32List _storage;
 
   /// Creates a [Float32x4List] of the specified length (in elements),
@@ -176,7 +179,7 @@
 /// space- and time-efficient than the default [List] implementation.
 final class NativeInt32x4List extends Object
     with ListMixin<Int32x4>, FixedLengthListMixin<Int32x4>
-    implements Int32x4List {
+    implements Int32x4List, TrustedGetRuntimeType {
   final Int32List _storage;
 
   /// Creates a [Int32x4List] of the specified length (in elements),
@@ -248,7 +251,7 @@
 /// space- and time-efficient than the default [List] implementation.
 final class NativeFloat64x2List extends Object
     with ListMixin<Float64x2>, FixedLengthListMixin<Float64x2>
-    implements Float64x2List {
+    implements Float64x2List, TrustedGetRuntimeType {
   final Float64List _storage;
 
   /// Creates a [Float64x2List] of the specified length (in elements),
@@ -382,7 +385,8 @@
 }
 
 @Native('DataView')
-final class NativeByteData extends NativeTypedData implements ByteData {
+final class NativeByteData extends NativeTypedData
+    implements ByteData, TrustedGetRuntimeType {
   /// Creates a [ByteData] of the specified length (in elements), all of
   /// whose elements are initially zero.
   factory NativeByteData(int length) => _create1(_checkLength(length));
@@ -739,7 +743,7 @@
 
 @Native('Float32Array')
 final class NativeFloat32List extends NativeTypedArrayOfDouble
-    implements Float32List {
+    implements Float32List, TrustedGetRuntimeType {
   factory NativeFloat32List(int length) => _createLength(_checkLength(length));
 
   factory NativeFloat32List.fromList(List<double> elements) =>
@@ -775,7 +779,7 @@
 
 @Native('Float64Array')
 final class NativeFloat64List extends NativeTypedArrayOfDouble
-    implements Float64List {
+    implements Float64List, TrustedGetRuntimeType {
   factory NativeFloat64List(int length) => _createLength(_checkLength(length));
 
   factory NativeFloat64List.fromList(List<double> elements) =>
@@ -810,7 +814,8 @@
 }
 
 @Native('Int16Array')
-final class NativeInt16List extends NativeTypedArrayOfInt implements Int16List {
+final class NativeInt16List extends NativeTypedArrayOfInt
+    implements Int16List, TrustedGetRuntimeType {
   factory NativeInt16List(int length) => _createLength(_checkLength(length));
 
   factory NativeInt16List.fromList(List<int> elements) =>
@@ -850,7 +855,8 @@
 }
 
 @Native('Int32Array')
-final class NativeInt32List extends NativeTypedArrayOfInt implements Int32List {
+final class NativeInt32List extends NativeTypedArrayOfInt
+    implements Int32List, TrustedGetRuntimeType {
   factory NativeInt32List(int length) => _createLength(_checkLength(length));
 
   factory NativeInt32List.fromList(List<int> elements) =>
@@ -890,7 +896,8 @@
 }
 
 @Native('Int8Array')
-final class NativeInt8List extends NativeTypedArrayOfInt implements Int8List {
+final class NativeInt8List extends NativeTypedArrayOfInt
+    implements Int8List, TrustedGetRuntimeType {
   factory NativeInt8List(int length) => _createLength(_checkLength(length));
 
   factory NativeInt8List.fromList(List<int> elements) =>
@@ -934,7 +941,7 @@
 
 @Native('Uint16Array')
 final class NativeUint16List extends NativeTypedArrayOfInt
-    implements Uint16List {
+    implements Uint16List, TrustedGetRuntimeType {
   factory NativeUint16List(int length) => _createLength(_checkLength(length));
 
   factory NativeUint16List.fromList(List<int> list) =>
@@ -975,7 +982,7 @@
 
 @Native('Uint32Array')
 final class NativeUint32List extends NativeTypedArrayOfInt
-    implements Uint32List {
+    implements Uint32List, TrustedGetRuntimeType {
   factory NativeUint32List(int length) => _createLength(_checkLength(length));
 
   factory NativeUint32List.fromList(List<int> elements) =>
@@ -1016,7 +1023,7 @@
 
 @Native('Uint8ClampedArray,CanvasPixelArray')
 final class NativeUint8ClampedList extends NativeTypedArrayOfInt
-    implements Uint8ClampedList {
+    implements Uint8ClampedList, TrustedGetRuntimeType {
   factory NativeUint8ClampedList(int length) =>
       _createLength(_checkLength(length));
 
@@ -1071,7 +1078,8 @@
 // the potential for Uint8ClampedArray to 'accidentally' pick up the
 // dispatch record for Uint8List.
 @Native('Uint8Array,!nonleaf')
-final class NativeUint8List extends NativeTypedArrayOfInt implements Uint8List {
+final class NativeUint8List extends NativeTypedArrayOfInt
+    implements Uint8List, TrustedGetRuntimeType {
   factory NativeUint8List(int length) => _createLength(_checkLength(length));
 
   factory NativeUint8List.fromList(List<int> elements) =>
diff --git a/sdk/lib/_internal/js_shared/lib/rti.dart b/sdk/lib/_internal/js_shared/lib/rti.dart
index 88cce4b..7a1b76a 100644
--- a/sdk/lib/_internal/js_shared/lib/rti.dart
+++ b/sdk/lib/_internal/js_shared/lib/rti.dart
@@ -27,6 +27,36 @@
 import 'dart:_js_shared_embedded_names';
 import 'dart:_recipe_syntax';
 
+/// A marker interface for classes with 'trustworthy' implementations of `get
+/// runtimeType`.
+///
+/// Generally, overrides of `get runtimeType` are not used in displaying the
+/// types of irritants in TypeErrors or computing the structural `runtimeType`
+/// of records. Instead the Rti (aka 'true') type is used.
+///
+/// The 'true' type is sometimes confusing because it shows implementation
+/// details, e.g. the true type of `42` is `JSInt` and `2.1` is `JSNumNotInt`.
+///
+/// For a limited number of implementation classes we tell a 'white lie' that
+/// the value is of another type, e.g. that `42` is an `int` and `2.1` is
+/// `double`. This is achieved by overriding `get runtimeType` to return the
+/// desired type, and marking the implementation class type with `implements
+/// [TrustedGetRuntimeType]`.
+///
+/// [TrustedGetRuntimeType] is not exposed outside the `dart:` libraries so
+/// users cannot tell lies.
+///
+/// The `Type` returned by a trusted `get runtimeType` must be an instance of
+/// the system `Type`, which is guaranteed by using a type literal. Type
+/// literals can be generic and dependent on type variables, e.g. `List<E>`.
+///
+/// Care needs to taken to ensure that the runtime does not get caught telling
+/// lies. Generally, a class's `runtimeType` lies by returning an abstract
+/// supertype of the class.  Since both the the marker interface and `get
+/// runtimeType` are inherited, there should be no way in which a user can
+/// extend the class or implement interface of the class.
+abstract class TrustedGetRuntimeType {}
+
 /// The name of a property on the constructor function of Dart Object
 /// and interceptor types, used for caching Rti types.
 const constructorRtiCachePropertyName = r'$ccache';
@@ -834,13 +864,64 @@
   return _Utils.asRti(type);
 }
 
-/// Called from [Object.runtimeType] and [Interceptor.runtimeType].
-Type getRuntimeType(Object? object) {
-  Rti rti = _instanceFunctionType(object) ?? instanceType(object);
+/// Called from [Object.runtimeType].
+///
+/// [Object.runtimeType] is shadowed by overrides so that [object] is always an
+/// ordinary object and never an Array, Closure or Record.
+@pragma('dart2js:never-inline')
+Type getRuntimeTypeOfDartObject(Object? object) {
+  Rti rti = _instanceType(object);
   return createRuntimeType(rti);
 }
 
-/// Called from [_Record.runtimeType]
+/// Called from [JSArray.runtimeType].
+Type getRuntimeTypeOfArray(Object? array) {
+  Rti rti = _getRuntimeTypeOfArrayAsRti(array);
+  return createRuntimeType(rti);
+}
+
+Rti _getRuntimeTypeOfArrayAsRti(Object? array) {
+  Rti rti = _arrayInstanceType(array);
+
+  // TODO(http://dartbug.com/51894):
+  //
+  // There are two reasonable types: `JSArray<E>` and `List<E>`.
+  //
+  // Either could be achieved by making JSArray implement TrustedGetRuntimeType
+  // and changing the definition of JSArray.runtimeType:
+  //
+  //     Type get runtimeType => JSArray<E>;
+  //     Type get runtimeType => List<E>;
+  //
+  // - `JSArray<E>`, the internal type, is stored on the array. There is an
+  //    SSA-level optimization that recognizes that type expression `JSArray<E>`
+  //    is just reconstructing the value, so we get the same operations as this
+  //    method.
+  //
+  // - `List<E>` would construct a derived type. This would be a little slower,
+  //    but not terrible, since in the steady state, the `List<E>` constructed
+  //    via a recipe is cached in a map on the stored Rti.
+  //
+  // The reason we don't just define a plain and understandable method is that
+  // the presence of type variable `E` defeats the type-erasure optimization
+  // when `.runtimeType` is used.
+  return rti;
+}
+
+/// Called from [Closure.runtimeType].
+Type getRuntimeTypeOfClosure(Object? closure) {
+  // If there is no function type, use the interface type.
+  Rti rti = closureFunctionType(closure) ?? instanceType(closure);
+  return createRuntimeType(rti);
+}
+
+/// Called from [Interceptor.runtimeType].
+Type getRuntimeTypeOfInterceptorNotArray(Object? interceptor, Object? object) {
+  Rti rti = _instanceTypeFromConstructor(interceptor);
+  return createRuntimeType(rti);
+}
+
+/// Called from [_Record.runtimeType].
 Type getRuntimeTypeOfRecord(Object record) {
   Rti recordRti = records.getRtiForRecord(record);
   return createRuntimeType(recordRti);
@@ -850,7 +931,14 @@
 /// or interface type of [object].
 Rti _structuralTypeOf(Object? object) {
   if (object is Record) return records.getRtiForRecord(object);
-  return _instanceFunctionType(object) ?? instanceType(object);
+  final functionRti = _instanceFunctionType(object);
+  if (functionRti != null) return functionRti;
+  if (object is TrustedGetRuntimeType) {
+    final type = object.runtimeType;
+    return _Utils.as_Type(type)._rti;
+  }
+  if (_Utils.isArray(object)) return _getRuntimeTypeOfArrayAsRti(object);
+  return instanceType(object);
 }
 
 /// Called from generated code.
@@ -3409,6 +3497,7 @@
   static String asString(Object? o) => JS('String', '#', o);
   static Rti asRti(Object? s) => JS('Rti', '#', s);
   static Rti? asRtiOrNull(Object? s) => JS('Rti|Null', '#', s);
+  static _Type as_Type(Object? o) => JS('_Type', '#', o);
 
   static bool isString(Object? o) => JS('bool', 'typeof # == "string"', o);
   static bool isNum(Object? o) => JS('bool', 'typeof # == "number"', o);