[ddc] Add support for new generic interface types

- Introduce type environments in the compiler used for evaluating type
  parameters from generic classes and generic function types.

Issue: https://github.com/dart-lang/sdk/issues/48585
Change-Id: Ib5641eb666527acc3b7f13a4a00dea34e0122b52
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/266540
Reviewed-by: Mark Zhou <markzipan@google.com>
diff --git a/pkg/dev_compiler/lib/src/compiler/js_names.dart b/pkg/dev_compiler/lib/src/compiler/js_names.dart
index 3c48dc8..97921d9 100644
--- a/pkg/dev_compiler/lib/src/compiler/js_names.dart
+++ b/pkg/dev_compiler/lib/src/compiler/js_names.dart
@@ -19,10 +19,6 @@
   static const operatorIsPrefix = r'$is_';
   static const operatorSignature = r'$signature';
   static const rtiName = r'$ti';
-  // TODO(48585) Fix this name.
-  static const futureClassName = 'TODO';
-  // TODO(48585) Fix this name.
-  static const listClassName = 'TODO';
   static const rtiAsField = '_as';
   static const rtiIsField = '_is';
 }
diff --git a/pkg/dev_compiler/lib/src/kernel/compiler.dart b/pkg/dev_compiler/lib/src/kernel/compiler.dart
index b9a3646..7827185 100644
--- a/pkg/dev_compiler/lib/src/kernel/compiler.dart
+++ b/pkg/dev_compiler/lib/src/kernel/compiler.dart
@@ -41,6 +41,7 @@
 import 'nullable_inference.dart';
 import 'property_model.dart';
 import 'target.dart' show allowedNativeTest;
+import 'type_environment.dart';
 import 'type_recipe_generator.dart';
 import 'type_table.dart';
 
@@ -131,6 +132,12 @@
   /// True when a class is emitting a deferred class hierarchy.
   bool _emittingDeferredType = false;
 
+  /// The current type environment of type parameters introduced to the scope
+  /// via generic classes and functions.
+  DDCTypeEnvironment _currentTypeEnvironment = const EmptyTypeEnvironment();
+
+  final TypeRecipeGenerator _typeRecipeGenerator;
+
   /// The current element being loaded.
   /// We can use this to determine if we're loading top-level code or not:
   ///
@@ -188,6 +195,9 @@
 
   final _superHelpers = <String, js_ast.Method>{};
 
+  /// Cache for the results of calling [_typeParametersInHierarchy].
+  final _typeParametersInHierarchyCache = <Class, bool>{};
+
   // Compilation of Kernel's [BreakStatement].
   //
   // Kernel represents Dart's `break` and `continue` uniformly as
@@ -345,7 +355,8 @@
         _asyncStarImplClass = sdk.getClass('dart:async', '_AsyncStarImpl'),
         _assertInteropMethod = sdk.getTopLevelMember(
             'dart:_runtime', 'assertInterop') as Procedure,
-        _futureOrNormalizer = FutureOrNormalizer(_coreTypes);
+        _futureOrNormalizer = FutureOrNormalizer(_coreTypes),
+        _typeRecipeGenerator = TypeRecipeGenerator(_coreTypes);
 
   @override
   Library? get currentLibrary => _currentLibrary;
@@ -682,6 +693,12 @@
     _currentClass = c;
     _currentLibrary = c.enclosingLibrary;
     _currentUri = c.fileUri;
+    var savedTypeEnvironment = _currentTypeEnvironment;
+    // When compiling the type heritage of the class we can't reference an rti
+    // object attached to an instance. Instead we construct a type environment
+    // manually when needed. Later we use the rti attached to an instance for
+    // a simpler representation within instance members of the class.
+    _currentTypeEnvironment = _currentTypeEnvironment.extend(c.typeParameters);
 
     // Mixins are unrolled in _defineClass.
     // If this class is annotated with `@JS`, then there is nothing to emit.
@@ -697,6 +714,7 @@
     _currentClass = savedClass;
     _currentLibrary = savedLibrary;
     _currentUri = savedUri;
+    _currentTypeEnvironment = savedTypeEnvironment;
   }
 
   /// To emit top-level classes, we sometimes need to reorder them.
@@ -759,7 +777,15 @@
     });
 
     var jsCtors = _defineConstructors(c, className);
+    // 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);
+    }
     var jsMethods = _emitClassMethods(c);
+    _currentTypeEnvironment = savedTypeEnvironment;
 
     _emitSuperHelperSymbols(body);
     // Deferred supertypes must be evaluated lazily while emitting classes to
@@ -785,7 +811,7 @@
 
     // Attach caches on all canonicalized types.
     if (_options.newRuntimeTypes) {
-      var name = TypeRecipeGenerator.interfaceTypeRecipe(c);
+      var name = _typeRecipeGenerator.interfaceTypeRecipe(c);
       body.add(runtimeStatement(
           'addRtiResources(#, #)', [className, js.string(name)]));
     }
@@ -1687,6 +1713,18 @@
     // multiple constructors, but it needs to be balanced against readability.
     body.add(_initializeFields(fields, node));
 
+    // Instances of classes with type arguments need an rti object attached to
+    // them since the type arguments could be instantiated differently for
+    // each instance.
+    if (_options.newRuntimeTypes && _typeParametersInHierarchy(cls)) {
+      var type = cls.getThisType(_coreTypes, Nullability.nonNullable);
+      // Only set the rti if there isn't one already. This avoids superclasses
+      // from overwriting the value already set by subclass.
+      var rtiProperty = propertyName(js_ast.FixedNames.rtiName);
+      body.add(js.statement(
+          'this.# = this.# || #', [rtiProperty, rtiProperty, _emitType(type)]));
+    }
+
     // If no superinitializer is provided, an implicit superinitializer of the
     // form `super()` is added at the end of the initializer list, unless the
     // enclosing class is class Object.
@@ -1700,6 +1738,19 @@
     return body;
   }
 
+  /// Returns `true` if [cls] or any of its transitive super classes has
+  /// generic type parameters.
+  bool _typeParametersInHierarchy(Class? cls) {
+    if (cls == null) return false;
+    var cachedResult = _typeParametersInHierarchyCache[cls];
+    if (cachedResult != null) return cachedResult;
+    var hasTypeParameters = cls.typeParameters.isNotEmpty
+        ? true
+        : _typeParametersInHierarchy(cls.superclass);
+    _typeParametersInHierarchyCache[cls] = hasTypeParameters;
+    return hasTypeParameters;
+  }
+
   js_ast.LiteralString _constructorName(String name) {
     if (name == '') {
       // Default constructors (factory or not) use `new` as their name.
@@ -2182,12 +2233,16 @@
     /// own type parameters, this will need to be changed to call
     /// [_emitFunction] instead.
     var name = node.name.text;
+    var savedTypeEnvironment = _currentTypeEnvironment;
+    _currentTypeEnvironment =
+        _currentTypeEnvironment.extend(function.typeParameters);
     var jsBody = _emitSyncFunctionBody(function, name);
     var jsName = _constructorName(name);
     memberNames[node] = jsName.valueWithoutQuotes;
+    var jsParams = _emitParameters(function);
+    _currentTypeEnvironment = savedTypeEnvironment;
 
-    return js_ast.Method(jsName, js_ast.Fun(_emitParameters(function), jsBody),
-        isStatic: true)
+    return js_ast.Method(jsName, js_ast.Fun(jsParams, jsBody), isStatic: true)
       ..sourceInformation = _nodeEnd(node.fileEndOffset);
   }
 
@@ -2945,10 +3000,79 @@
   /// Returns an expression that evaluates to the rti object from the dart:_rti
   /// library that represents [type].
   js_ast.Expression _newEmitType(DartType type) {
+    /// Returns an expression that evaluates a type [recipe] within the type
+    /// [environment].
+    ///
+    /// At runtime the expression will evaluate to an rti object.
+    js_ast.Expression emitRtiEval(
+            js_ast.Expression environment, String recipe) =>
+        js.call('#.#("$recipe")',
+            [environment, _emitMemberName('_eval', memberClass: rtiClass)]);
+
+    /// Returns an expression that binds a type [parameter] within the type
+    /// [environment].
+    ///
+    /// At runtime the expression will evaluate to an rti object that has been
+    /// extended to include the provided [parameter].
+    js_ast.Expression emitRtiBind(
+            js_ast.Expression environment, TypeParameter parameter) =>
+        js.call('#.#(#)', [
+          environment,
+          _emitMemberName('_bind', memberClass: rtiClass),
+          _emitTypeParameter(parameter)
+        ]);
+
+    /// Returns an expression that evaluates a type [recipe] in a type
+    /// [environment] resulting in an rti object.
+    js_ast.Expression evalInEnvironment(
+        DDCTypeEnvironment environment, String recipe) {
+      if (environment is EmptyTypeEnvironment) {
+        return js.call('#.findType("$recipe")', [emitLibraryName(rtiLibrary)]);
+      } else if (environment is BindingTypeEnvironment) {
+        js_ast.Expression env;
+        if (environment.isSingleTypeParameter) {
+          // An environment with a single type parameter can be simplified to
+          // just that parameter.
+          env = _emitTypeParameter(environment.parameters.single);
+        } else {
+          var environmentTypes = environment.parameters;
+          // Create a dummy interface type to "hold" type arguments.
+          env = emitRtiEval(_emitTypeParameter(environmentTypes.first), '@<0>');
+          // Bind remaining type arguments.
+          for (var i = 1; i < environmentTypes.length; i++) {
+            env = emitRtiBind(env, environmentTypes[i]);
+          }
+        }
+        return emitRtiEval(env, recipe);
+      } 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}');
+      } 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}');
+        // Bind extra type parameters.
+        for (var parameter in environment.extendedParameters) {
+          env = emitRtiBind(env, parameter);
+        }
+        return emitRtiEval(env, recipe);
+      } else {
+        _typeCompilationError(type,
+            'Unexpected DDCTypeEnvironment type (${environment.runtimeType}).');
+      }
+    }
+
+    // TODO(nshahan) Avoid calling _emitType when we actually want a
+    // reference to an rti that already exists in scope.
+    if (type is TypeParameterType && type.isPotentiallyNonNullable) {
+      return _emitTypeParameterType(type, emitNullability: false);
+    }
     var normalizedType = _futureOrNormalizer.normalize(type);
     try {
-      var recipe = normalizedType.accept(TypeRecipeGenerator());
-      return js.call('#.findType("$recipe")', [emitLibraryName(rtiLibrary)]);
+      var result = _typeRecipeGenerator.recipeInEnvironment(
+          normalizedType, _currentTypeEnvironment);
+      return evalInEnvironment(result.requiredEnvironment, result.recipe);
     } on UnsupportedError catch (e) {
       _typeCompilationError(normalizedType, e.message ?? 'Unknown Error');
     }
@@ -3443,6 +3567,8 @@
   }
 
   js_ast.Fun _emitFunction(FunctionNode f, String? name) {
+    var savedTypeEnvironment = _currentTypeEnvironment;
+    _currentTypeEnvironment = _currentTypeEnvironment.extend(f.typeParameters);
     // normal function (sync), vs (sync*, async, async*)
     var isSync = f.asyncMarker == AsyncMarker.Sync;
     var formals = _emitParameters(f);
@@ -3462,6 +3588,7 @@
         : _emitGeneratorFunctionBody(f, name);
 
     block = super.exitFunction(formals, block);
+    _currentTypeEnvironment = savedTypeEnvironment;
     return js_ast.Fun(formals, block);
   }
 
@@ -5965,9 +6092,11 @@
       case JsGetName.RTI_NAME:
         return js.string(js_ast.FixedNames.rtiName);
       case JsGetName.FUTURE_CLASS_TYPE_NAME:
-        return js.string(js_ast.FixedNames.futureClassName);
+        return js.string(
+            _typeRecipeGenerator.interfaceTypeRecipe(_coreTypes.futureClass));
       case JsGetName.LIST_CLASS_TYPE_NAME:
-        return js.string(js_ast.FixedNames.listClassName);
+        return js.string(
+            _typeRecipeGenerator.interfaceTypeRecipe(_coreTypes.listClass));
       case JsGetName.RTI_FIELD_AS:
         return _emitMemberName(js_ast.FixedNames.rtiAsField,
             memberClass: rtiClass);
@@ -6907,12 +7036,16 @@
       return js_ast.Property(symbol, constant);
     }
 
-    var type = _emitInterfaceType(
-        node.getType(_staticTypeContext) as InterfaceType,
-        emitNullability: false);
-    var prototype = js.call('#.prototype', [type]);
+    var type = node.getType(_staticTypeContext);
+    var classRef =
+        _emitInterfaceType(type as InterfaceType, emitNullability: false);
+    var prototype = js.call('#.prototype', [classRef]);
     var properties = [
       js_ast.Property(propertyName('__proto__'), prototype),
+      if (_options.newRuntimeTypes && type.typeArguments.isNotEmpty)
+        // Generic interface type instances require a type information tag.
+        js_ast.Property(
+            propertyName(js_ast.FixedNames.rtiName), _emitType(type)),
       for (var e in node.fieldValues.entries.toList().reversed)
         entryToProperty(e),
     ];
diff --git a/pkg/dev_compiler/lib/src/kernel/kernel_helpers.dart b/pkg/dev_compiler/lib/src/kernel/kernel_helpers.dart
index 76a4b61..a97f328 100644
--- a/pkg/dev_compiler/lib/src/kernel/kernel_helpers.dart
+++ b/pkg/dev_compiler/lib/src/kernel/kernel_helpers.dart
@@ -371,3 +371,12 @@
 
 bool _isDartInternal(Uri uri) =>
     uri.isScheme('dart') && uri.path == '_internal';
+
+/// Collects all `TypeParameter`s from the `TypeParameterType`s present in the
+/// visited `DartType`.
+class TypeParameterTypeFinder extends RecursiveVisitor<void> {
+  final found = <TypeParameter>{};
+  @override
+  void visitTypeParameterType(TypeParameterType node) =>
+      found.add(node.parameter);
+}
diff --git a/pkg/dev_compiler/lib/src/kernel/type_environment.dart b/pkg/dev_compiler/lib/src/kernel/type_environment.dart
new file mode 100644
index 0000000..c8d5fd5
--- /dev/null
+++ b/pkg/dev_compiler/lib/src/kernel/type_environment.dart
@@ -0,0 +1,178 @@
+// Copyright (c) 2022, 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';
+
+import 'package:kernel/ast.dart';
+
+/// Type environments made of type parameters.
+///
+/// At runtime, type recipes are evaluated in an environment to produce an rti.
+/// These compile time representations are used to calculate the indices of type
+/// parameters in type recipes.
+abstract class DDCTypeEnvironment {
+  /// Creates a new environment by adding [parameters] to those already in this
+  /// environment.
+  DDCTypeEnvironment extend(List<TypeParameter> parameters);
+
+  /// Reduces this environment down to an environment that will contain
+  /// [requiredTypes] and be output in a compact representation.
+  DDCTypeEnvironment prune(Iterable<TypeParameter> requiredParameters);
+
+  /// Returns the index of [parameter] in this environment for use in a type
+  /// recipe or a negative value if the parameter was not found.
+  int recipeIndexOf(TypeParameter parameter);
+}
+
+/// An empty environment that signals no type parameters are present or needed.
+///
+/// Facilitates building other environments.
+class EmptyTypeEnvironment implements DDCTypeEnvironment {
+  const EmptyTypeEnvironment();
+
+  @override
+  DDCTypeEnvironment extend(List<TypeParameter> parameters) =>
+      parameters.isEmpty ? this : BindingTypeEnvironment(parameters);
+
+  @override
+  DDCTypeEnvironment prune(Iterable<TypeParameter> requiredParameters) => this;
+
+  @override
+  int recipeIndexOf(TypeParameter parameter) => -1;
+}
+
+/// A type environment introduced by a class with one or more generic type
+/// parameters.
+class ClassTypeEnvironment implements DDCTypeEnvironment {
+  final List<TypeParameter> _typeParameters;
+  ClassTypeEnvironment(this._typeParameters);
+
+  @override
+  DDCTypeEnvironment extend(List<TypeParameter> parameters) =>
+      parameters.isEmpty
+          ? this
+          : ExtendedClassTypeEnvironment(this, [...parameters]);
+
+  @override
+  DDCTypeEnvironment prune(Iterable<TypeParameter> requiredParameters) =>
+      // If any parameters are required, the class type environment already
+      // exists and a reference to it is suitably compact.
+      requiredParameters.any(_typeParameters.contains)
+          ? this
+          : const EmptyTypeEnvironment();
+
+  @override
+  int recipeIndexOf(TypeParameter parameter) {
+    var i = _typeParameters.indexOf(parameter);
+    if (i < 0) return i;
+    // Index for class type parameters is one based. Zero refers to the full
+    // class rti with all type arguments.
+    return i + 1;
+  }
+}
+
+/// A type environment containing multiple type parameters.
+class BindingTypeEnvironment implements DDCTypeEnvironment {
+  final List<TypeParameter> _typeParameters;
+
+  BindingTypeEnvironment(this._typeParameters);
+
+  @override
+  DDCTypeEnvironment extend(List<TypeParameter> parameters) =>
+      parameters.isEmpty
+          ? this
+          : BindingTypeEnvironment([
+              // Place new parameters first so they can effectively shadow
+              // parameters already in the environment.
+              ...parameters, ..._typeParameters
+            ]);
+
+  @override
+  DDCTypeEnvironment prune(Iterable<TypeParameter> requiredParameters) {
+    var foundParameters = requiredParameters.where(_typeParameters.contains);
+    if (foundParameters.isEmpty) return const EmptyTypeEnvironment();
+    if (foundParameters.length == _typeParameters.length) return this;
+    return BindingTypeEnvironment(foundParameters.toList());
+  }
+
+  @override
+  int recipeIndexOf(TypeParameter parameter) {
+    var i = _typeParameters.indexOf(parameter);
+    if (i < 0) return i;
+    // Environments containing a single parameter can have a more compact
+    // representation with a zero based index.
+    if (isSingleTypeParameter) return i;
+    return i + 1;
+  }
+
+  /// The type parameters in this environment.
+  List<TypeParameter> get parameters => UnmodifiableListView(_typeParameters);
+
+  /// Returns `true` if this environment only contains a single type parameter.
+  bool get isSingleTypeParameter => _typeParameters.length == 1;
+}
+
+/// A type environment based on one introduced by a generic class but extended
+/// with additional parameters from methods with generic type parameters.
+class ExtendedClassTypeEnvironment implements DDCTypeEnvironment {
+  final ClassTypeEnvironment _classEnvironment;
+  final List<TypeParameter> _typeParameters;
+
+  ExtendedClassTypeEnvironment(this._classEnvironment, this._typeParameters);
+
+  @override
+  DDCTypeEnvironment extend(List<TypeParameter> parameters) =>
+      parameters.isEmpty
+          ? this
+          : ExtendedClassTypeEnvironment(_classEnvironment, [
+              // Place new parameters first so they can effectively shadow
+              // parameters already in the environment.
+              ...parameters, ..._typeParameters
+            ]);
+
+  @override
+  DDCTypeEnvironment prune(Iterable<TypeParameter> requiredParameters) {
+    var classEnvironmentNeeded =
+        requiredParameters.any(_classEnvironment._typeParameters.contains);
+    var additionalParameters =
+        requiredParameters.where(_typeParameters.contains);
+    if (additionalParameters.isEmpty) {
+      return classEnvironmentNeeded
+          // Simply using the class environment has a compact representation
+          // and is already constructed.
+          ? _classEnvironment
+          // No type parameters are needed from this environment.
+          : const EmptyTypeEnvironment();
+    }
+    if (!classEnvironmentNeeded) {
+      // None of the parameters are needed from the class environment.
+      return BindingTypeEnvironment(additionalParameters.toList());
+    }
+    // This is already the exact environment needed.
+    if (additionalParameters.length == _typeParameters.length) return this;
+    // An extended class environment with fewer additional parameters is needed.
+    return ExtendedClassTypeEnvironment(
+        _classEnvironment, additionalParameters.toList());
+  }
+
+  @override
+  int recipeIndexOf(TypeParameter parameter) {
+    // Search in the extended type parameters first. They can shadow parameters
+    // from the class.
+    var i = _typeParameters.indexOf(parameter);
+    // Type parameter was found and should be a one based index. Zero refers to
+    // the original class rti that is the basis on this environment.
+    if (i >= 0) return i + 1;
+    i = _classEnvironment.recipeIndexOf(parameter);
+    // The type parameter bindings with the closest scope have the lowest
+    // indices. Since the parameter was found in the class binding the index
+    // must be offset by the number of extended parameters.
+    if (i > 0) return i + _typeParameters.length;
+    // Type parameter not found.
+    return -1;
+  }
+
+  List<TypeParameter> get extendedParameters =>
+      UnmodifiableListView(_typeParameters);
+}
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 719b6c7..c633ba1 100644
--- a/pkg/dev_compiler/lib/src/kernel/type_recipe_generator.dart
+++ b/pkg/dev_compiler/lib/src/kernel/type_recipe_generator.dart
@@ -4,15 +4,48 @@
 
 import 'package:js_shared/synced/recipe_syntax.dart' show Recipe;
 import 'package:kernel/ast.dart';
+import 'package:kernel/core_types.dart';
 import 'package:path/path.dart' as p;
 
 import '../compiler/js_names.dart';
+import 'kernel_helpers.dart';
+import 'type_environment.dart';
+
+class TypeRecipeGenerator {
+  final _TypeRecipeVisitor _recipeVisitor;
+
+  TypeRecipeGenerator(CoreTypes coreTypes)
+      : _recipeVisitor =
+            _TypeRecipeVisitor(const EmptyTypeEnvironment(), coreTypes);
+
+  GeneratedRecipe recipeInEnvironment(
+      DartType type, DDCTypeEnvironment environment) {
+    var typeParameterFinder = TypeParameterTypeFinder();
+    type.accept(typeParameterFinder);
+    _recipeVisitor._typeEnvironment =
+        environment.prune(typeParameterFinder.found);
+    return GeneratedRecipe(
+        type.accept(_recipeVisitor), _recipeVisitor._typeEnvironment);
+  }
+
+  String interfaceTypeRecipe(Class node) =>
+      _recipeVisitor._interfaceTypeRecipe(node);
+}
 
 /// A visitor to generate type recipe strings from a [DartType].
 ///
 /// The recipes are used by the 'dart:_rti' library while running the compiled
 /// application.
-class TypeRecipeGenerator extends DartTypeVisitor<String> {
+class _TypeRecipeVisitor extends DartTypeVisitor<String> {
+  /// The type environment to evaluate recipes in.
+  ///
+  /// Used to determine the indices for type variables.
+  DDCTypeEnvironment _typeEnvironment;
+
+  final CoreTypes _coreTypes;
+
+  _TypeRecipeVisitor(this._typeEnvironment, this._coreTypes);
+
   @override
   String defaultDartType(DartType node) {
     return 'TODO';
@@ -26,11 +59,19 @@
 
   @override
   String visitInterfaceType(InterfaceType node) {
-    var recipe = interfaceTypeRecipe(node.classNode);
-    if (node.typeArguments.isEmpty) {
-      return '$recipe${_nullabilityRecipe(node)}';
+    // Generate the interface type recipe.
+    var recipeBuffer = StringBuffer(_interfaceTypeRecipe(node.classNode));
+    // Generate the recipes for all type arguments.
+    if (node.typeArguments.isNotEmpty) {
+      recipeBuffer.write(Recipe.startTypeArgumentsString);
+      recipeBuffer.writeAll(
+          node.typeArguments.map((typeArgument) => typeArgument.accept(this)),
+          Recipe.separatorString);
+      recipeBuffer.write(Recipe.endTypeArgumentsString);
     }
-    return 'TODO';
+    // Add nullability.
+    recipeBuffer.write(_nullabilityRecipe(node));
+    return recipeBuffer.toString();
   }
 
   @override
@@ -43,8 +84,10 @@
   String visitFunctionType(FunctionType node) => defaultDartType(node);
 
   @override
-  String visitTypeParameterType(TypeParameterType node) =>
-      defaultDartType(node);
+  String visitTypeParameterType(TypeParameterType node) {
+    var i = _typeEnvironment.recipeIndexOf(node.parameter);
+    return '$i${_nullabilityRecipe(node)}';
+  }
 
   @override
   String visitTypedefType(TypedefType node) => defaultDartType(node);
@@ -59,7 +102,8 @@
               '${_nullabilityRecipe(node)}';
 
   @override
-  String visitNullType(NullType node) => defaultDartType(node);
+  String visitNullType(NullType node) =>
+      _interfaceTypeRecipe(_coreTypes.deprecatedNullClass);
 
   @override
   String visitExtensionType(ExtensionType node) => defaultDartType(node);
@@ -69,14 +113,14 @@
       visitTypeParameterType(node.left);
 
   /// Returns the recipe for the interface type introduced by [cls].
-  static String interfaceTypeRecipe(Class cls) {
+  String _interfaceTypeRecipe(Class cls) {
     var path = p.withoutExtension(cls.enclosingLibrary.importUri.path);
     var library = pathToJSIdentifier(path);
     return '$library${Recipe.librarySeparatorString}${cls.name}';
   }
 
   /// Returns the recipe representation of [nullability].
-  static String _nullabilityRecipe(DartType type) {
+  String _nullabilityRecipe(DartType type) {
     switch (type.declaredNullability) {
       case Nullability.undetermined:
         if (type is TypeParameterType) {
@@ -96,3 +140,23 @@
     }
   }
 }
+
+/// Packages the type recipe and the environment needed to evaluate it at
+/// runtime.
+///
+/// Returned from [TypeRecipeGenerator.recipeInEnvironment].
+class GeneratedRecipe {
+  /// A type recipe that can be used to evaluate a type at runtime in the
+  /// [requiredEnvironment].
+  ///
+  /// For use with the dart:_rti library.
+  final String recipe;
+
+  /// The environment required to properly evaluate [recipe] within.
+  ///
+  /// This environment has been reduced down to be represented compactly with
+  /// only the necessary type parameters.
+  final DDCTypeEnvironment requiredEnvironment;
+
+  GeneratedRecipe(this.recipe, this.requiredEnvironment);
+}
diff --git a/sdk/lib/_internal/js_dev_runtime/private/foreign_helper.dart b/sdk/lib/_internal/js_dev_runtime/private/foreign_helper.dart
index 8eef711..a6d1bec 100644
--- a/sdk/lib/_internal/js_dev_runtime/private/foreign_helper.dart
+++ b/sdk/lib/_internal/js_dev_runtime/private/foreign_helper.dart
@@ -4,6 +4,7 @@
 
 library dart._foreign_helper;
 
+import 'dart:_interceptors' show JSArray;
 import 'dart:_js_helper' show notNull;
 import 'dart:_js_shared_embedded_names' show JsBuiltin, JsGetName;
 import 'dart:_rti' show Rti;
@@ -298,12 +299,12 @@
 /// Returns the interceptor for [object].
 ///
 // TODO(nshahan) Replace calls at compile time?
-external getInterceptor(object);
+Object getInterceptor(object) => object;
 
 /// Returns the Rti object for the type for JavaScript arrays via JS-interop.
 ///
 // TODO(nshahan) Replace calls at compile time?
-external Object getJSArrayInteropRti();
+Object getJSArrayInteropRti() => TYPE_REF<JSArray>();
 
 /// Returns a raw reference to the JavaScript function which implements
 /// [function].