[dartdevc] fix #34596, hot restart can now clear field & cache state

This adds a method to the internal SDK runtime library that can be
called to clear state when a hot restart is desired.

This also refactors some of the logic around ignoring type errors,
as we need to ensure we can clear those caches. Also I noticed a bug
where the result of isSubtypeOf was assumed to be non-null, but this
was not the case. The new structure should make this more clear.

(Note: I think we may be able to remove a lot of the code for ignoring
type errors soon.)

Change-Id: Icf5072ffe21aefd85e9816ffc2fc29f679839bc4
Reviewed-on: https://dart-review.googlesource.com/c/78324
Reviewed-by: Jake Macdonald <jakemac@google.com>
Commit-Queue: Jenny Messerly <jmesserly@google.com>
diff --git a/pkg/dev_compiler/lib/js/legacy/dart_library.js b/pkg/dev_compiler/lib/js/legacy/dart_library.js
index 1902d38..8fb0fab 100644
--- a/pkg/dev_compiler/lib/js/legacy/dart_library.js
+++ b/pkg/dev_compiler/lib/js/legacy/dart_library.js
@@ -2,22 +2,23 @@
 // 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.
 
-/* This file defines the module loader for the dart runtime.
-*/
+// This file defines the module loader for the dart runtime.
 var dart_library;
 if (!dart_library) {
-dart_library =
-  typeof module != "undefined" && module.exports || {};
+  dart_library = typeof module != "undefined" && module.exports || {};
 
 (function (dart_library) {
   'use strict';
 
-  /** Note that we cannot use dart_utils.throwInternalError from here. */
+  // Throws an error related to module loading.
+  //
+  // This does not throw a Dart error because the Dart SDK may not have loaded
+  // yet, and module loading errors cannot be caught by Dart code.
   function throwLibraryError(message) {
     // Dispatch event to allow others to react to the load error without
     // capturing the exception.
-    var errorEvent = new CustomEvent('dartLoadException', { detail: message });
-    window.dispatchEvent(errorEvent);
+    window.dispatchEvent(
+      new CustomEvent('dartLoadException', { detail: message }));
     throw Error(message);
   }
 
@@ -31,12 +32,12 @@
   // Returns a proxy that delegates to the underlying loader.
   // This defers loading of a module until a library is actually used.
   const loadedModule = Symbol('loadedModule');
-  dart_library.defer = function(module, name, patch) {
+  dart_library.defer = function (module, name, patch) {
     let done = false;
     function loadDeferred() {
       done = true;
-      var mod = module[loadedModule];
-      var lib = mod[name];
+      let mod = module[loadedModule];
+      let lib = mod[name];
       // Install unproxied module and library in caller's context.
       patch(mod, lib);
     }
@@ -44,11 +45,11 @@
     // library object should be get (to read a top-level variable, method, or
     // Class) or set (to write a top-level variable).
     return new Proxy({}, {
-      get: function(o, p) {
+      get: function (o, p) {
         if (!done) loadDeferred();
         return module[name][p];
       },
-      set: function(o, p, value) {
+      set: function (o, p, value) {
         if (!done) loadDeferred();
         module[name][p] = value;
         return true;
@@ -60,8 +61,8 @@
   class LibraryLoader {
 
     constructor(name, defaultValue, imports, loader) {
-      imports.forEach(function(i) {
-        var deps = _reverseImports.get(i);
+      imports.forEach(function (i) {
+        let deps = _reverseImports.get(i);
         if (!deps) {
           deps = new Set();
           _reverseImports.set(i, deps);
@@ -88,8 +89,7 @@
     load() {
       // Check for cycles
       if (this._state == LibraryLoader.LOADING) {
-        throwLibraryError('Circular dependence on library: '
-                              + this._name);
+        throwLibraryError('Circular dependence on library: ' + this._name);
       } else if (this._state >= LibraryLoader.READY) {
         return this._library;
       }
@@ -113,7 +113,7 @@
         // Load / parse other modules on demand.
         let done = false;
         this._library = new Proxy(library, {
-          get: function(o, name) {
+          get: function (o, name) {
             if (!done) {
               done = true;
               loader._loader.apply(null, args);
@@ -137,9 +137,9 @@
 
   // Map from name to LibraryLoader
   let _libraries = new Map();
-  dart_library.libraries = function() { return _libraries.keys(); };
-  dart_library.debuggerLibraries = function() {
-    var debuggerLibraries = [];
+  dart_library.libraries = function () { return _libraries.keys(); };
+  dart_library.debuggerLibraries = function () {
+    let debuggerLibraries = [];
     _libraries.forEach(function (value, key, map) {
       debuggerLibraries.push(value.load());
     });
@@ -194,37 +194,83 @@
   }
   dart_library.import = import_;
 
-  var _currentIsolate = false;
+  let _debuggerInitialized = false;
 
-  function _restart() {
-    start(_lastModuleName, _lastLibraryName, true);
-  }
+  // Called to initiate a hot restart of the application.
+  //
+  // "Hot restart" means all application state is cleared, the newly compiled
+  // modules are loaded, and `main()` is called.
+  //
+  // Note: `onReloadEnd()` can be provided, and if so will be used instead of
+  // `main()` for hot restart.
+  //
+  // This happens in the following sequence:
+  //
+  // 1. Look for `onReloadStart()` in the same library that has `main()`, and
+  //    call it if present. This function is implemented by the application to
+  //    ensure any global browser/DOM state is cleared, so the application can
+  //    restart.
+  // 2. Wait for `onReloadStart()` to complete (either synchronously, or async
+  //    if it returned a `Future`).
+  // 3. Call dart:_runtime's `hotRestart()` function to clear any state that
+  //    `dartdevc` is tracking, such as initialized static fields and type
+  //    caches.
+  // 4. Call `window.$dartWarmReload()` (provided by the HTML page) to reload
+  //    the relevant JS modules, passing a callback that will invoke `main()`.
+  // 5. `$dartWarmReload` calls the callback to rerun main.
+  //
+  function reload(clearState) {
+    // TODO(jmesserly): once we've rolled out `clearState` make it the default,
+    // and eventually remove the parameter.
+    if (clearState == null) clearState = false;
 
-  function reload() {
+
+    // TODO(jmesserly): we may want to change these APIs to use the
+    // "hot restart" terminology for consistency with Flutter. In Flutter,
+    // "hot reload" refers to keeping the application state and attempting to
+    // patch the code for the application while it is executing
+    // (https://flutter.io/hot-reload/), whereas "hot restart" refers to what
+    // dartdevc supports: tear down the app, update the code, and rerun the app.
     if (!window || !window.$dartWarmReload) {
-      console.warn('Warm reload not supported in this environment.');
+      console.warn('Hot restart not supported in this environment.');
       return;
     }
-    var result;
+
+    // Call the application's `onReloadStart()` function, if provided.
+    let result;
     if (_lastLibrary && _lastLibrary.onReloadStart) {
       result = _lastLibrary.onReloadStart();
     }
-    if (result && result.then) {
-      let sdk = _libraries.get("dart_sdk");
-      result.then(sdk._library.dart.Dynamic)(function() {
-        window.$dartWarmReload(_restart);
+
+    let sdk = _libraries.get("dart_sdk");
+
+    /// Once the `onReloadStart()` completes, this finishes the restart.
+    function finishHotRestart() {
+      if (clearState) {
+        // This resets all initialized fields and clears type caches and other
+        // temporary data structures used by the compiler/SDK.
+        sdk.dart.hotRestart();
+      }
+      // Call the module loader to reload the necessary modules.
+      window.$dartWarmReload(() => {
+        // Once the modules are loaded, rerun `main()`.
+        start(_lastModuleName, _lastLibraryName, true);
       });
+    }
+
+    if (result && result.then) {
+      result.then(sdk._library.dart.Dynamic)(finishHotRestart);
     } else {
-      window.$dartWarmReload(_restart);
+      finishHotRestart();
     }
   }
   dart_library.reload = reload;
 
 
-  var _lastModuleName;
-  var _lastLibraryName;
-  var _lastLibrary;
-  var _originalBody;
+  let _lastModuleName;
+  let _lastLibraryName;
+  let _lastLibrary;
+  let _originalBody;
 
   function start(moduleName, libraryName, isReload) {
     if (libraryName == null) libraryName = moduleName;
@@ -234,14 +280,13 @@
     _lastLibrary = library;
     let dart_sdk = import_('dart_sdk');
 
-    if (!_currentIsolate) {
+    if (!_debuggerInitialized) {
       // This import is only needed for chrome debugging. We should provide an
       // option to compile without it.
       dart_sdk._debugger.registerDevtoolsFormatter();
 
       // Create isolate.
-      _currentIsolate = true;
-      dart_sdk._isolate_helper.startRootIsolate(() => {}, []);
+      _debuggerInitialized = true;
     }
     if (isReload) {
       if (library.onReloadEnd) {
diff --git a/pkg/dev_compiler/test/sourcemap/ddc_common.dart b/pkg/dev_compiler/test/sourcemap/ddc_common.dart
index afb62f9..7dbf654 100644
--- a/pkg/dev_compiler/test/sourcemap/ddc_common.dart
+++ b/pkg/dev_compiler/test/sourcemap/ddc_common.dart
@@ -147,10 +147,8 @@
 void createHtmlWrapper(File sdkJsFile, Uri outputFile, String jsContent,
     String outputFilename, Uri outDir) {
   // For debugging via HTML, Chrome and ./tools/testing/dart/http_server.dart.
-  Directory sdkPath = sdkRoot;
-  String jsRootDart =
-      "/root_dart/${new File(path.relative(sdkJsFile.path, from: sdkPath.path))
-      .uri}";
+  var sdkFile = File(path.relative(sdkJsFile.path, from: sdkRoot.path));
+  String jsRootDart = "/root_dart/${sdkFile.uri}";
   File.fromUri(outputFile.resolve("$outputFilename.html.js")).writeAsStringSync(
       jsContent.replaceFirst("from 'dart_sdk'", "from '$jsRootDart'"));
   File.fromUri(outputFile.resolve("$outputFilename.html.html"))
@@ -177,7 +175,6 @@
     import { test } from '$outFileRootBuild';
     let main = test.main;
     dart.ignoreWhitelistedErrors(false);
-    _isolate_helper.startRootIsolate(() => {}, []);
     main();
     </script>
   </head>
diff --git a/pkg/dev_compiler/tool/ddb b/pkg/dev_compiler/tool/ddb
index 60f32c8..52fe107 100755
--- a/pkg/dev_compiler/tool/ddb
+++ b/pkg/dev_compiler/tool/ddb
@@ -166,7 +166,6 @@
     'use strict';
     sdk._debugger.registerDevtoolsFormatter();
     sdk.dart.ignoreWhitelistedErrors($ignoreWhitelistedErrors);
-    sdk._isolate_helper.startRootIsolate(() => {}, []);
     app.$basename.main();
   });
 </script>
diff --git a/pkg/dev_compiler/tool/input_sdk/private/ddc_runtime/classes.dart b/pkg/dev_compiler/tool/input_sdk/private/ddc_runtime/classes.dart
index c8b2e39..3f848d6 100644
--- a/pkg/dev_compiler/tool/input_sdk/private/ddc_runtime/classes.dart
+++ b/pkg/dev_compiler/tool/input_sdk/private/ddc_runtime/classes.dart
@@ -127,6 +127,7 @@
     $throwInternalError('must have at least one generic type argument');
   }
   let resultMap = new Map();
+  $_cacheMaps.push(resultMap);
   function makeGenericType(...args) {
     if (args.length != length && args.length != 0) {
       $throwInternalError('requires ' + length + ' or 0 type arguments');
@@ -446,7 +447,7 @@
 /// Link the extension to the type it's extending as a base class.
 setBaseClass(derived, base) {
   JS('', '#.prototype.__proto__ = #.prototype', derived, base);
-  // We use __proto__ to track the superclass hierarchy (see isSubtype).
+  // We use __proto__ to track the superclass hierarchy (see isSubtypeOf).
   JS('', '#.__proto__ = #', derived, base);
 }
 
diff --git a/pkg/dev_compiler/tool/input_sdk/private/ddc_runtime/operations.dart b/pkg/dev_compiler/tool/input_sdk/private/ddc_runtime/operations.dart
index 09cf403..46eb700 100644
--- a/pkg/dev_compiler/tool/input_sdk/private/ddc_runtime/operations.dart
+++ b/pkg/dev_compiler/tool/input_sdk/private/ddc_runtime/operations.dart
@@ -390,73 +390,73 @@
 dsetindex(obj, index, value) =>
     callMethod(obj, '_set', null, [index, value], null, '[]=');
 
-/// TODO(leafp): This duplicates code in types.dart.
-/// I haven't found a way to factor it out that makes the
-/// code generator happy though.
-_ignoreMemo(f) => JS('', '''(() => {
-  let memo = new Map();
-  return (t1, t2) => {
-    let map = memo.get(t1);
-    let result;
-    if (map) {
-      result = map.get(t2);
-      if (result !== void 0) return result;
-    } else {
-      memo.set(t1, map = new Map());
-    }
-    result = $f(t1, t2);
-    map.set(t2, result);
-    return result;
-  };
-})()''');
+final _ignoreSubtypeCache = JS('', 'new Map()');
 
-final Object _ignoreTypeFailure = JS('', '''(() => {
-  return $_ignoreMemo((actual, type) => {
-      // TODO(vsm): Remove this hack ...
-      // This is primarily due to the lack of generic methods,
-      // but we need to triage all the types.
-    if ($_isFutureOr(type)) {
-      // Ignore if we would ignore either side of union.
-      let typeArg = $getGenericArgs(type)[0];
-      let typeFuture = ${getGenericClass(Future)}(typeArg);
-      return $_ignoreTypeFailure(actual, typeFuture) ||
-        $_ignoreTypeFailure(actual, typeArg);
-    }
+/// Whether [t1] <: [t2], or if [isImplicit] is set and we should ignore the
+/// cast failure from t1 to t2.
+///
+/// See [_isSubtypeOrLegacySubtype] and [ignoreWhitelistedErrors].
+@notNull
+bool _isSubtypeOrIgnorableCastFailure(
+    Object t1, Object t2, @notNull bool isImplicit) {
+  var result = _isSubtypeOrLegacySubtype(t1, t2);
+  return result == true ||
+      result == null &&
+          isImplicit &&
+          JS<bool>('!', 'dart.__ignoreWhitelistedErrors') &&
+          _ignoreTypeFailure(t1, t2);
+}
 
-    if (!!$isSubtype(type, $Iterable) && !!$isSubtype(actual, $Iterable) ||
-        !!$isSubtype(type, $Future) && !!$isSubtype(actual, $Future)) {
-      console.warn('Ignoring cast fail from ' + $typeName(actual) +
-                   ' to ' + $typeName(type));
-      return true;
+@notNull
+bool _ignoreTypeFailure(Object t1, Object t2) {
+  var map = JS('', '#.get(#)', _ignoreSubtypeCache, t1);
+  if (map != null) {
+    bool result = JS('', '#.get(#)', map, t2);
+    if (JS('!', '# !== void 0', result)) return result;
+  } else {
+    map = JS('', 'new Map()');
+    JS('', '#.set(#, #)', _ignoreSubtypeCache, t1, map);
+  }
+
+  // TODO(vsm): Remove this hack ...
+  // This is primarily due to the lack of generic methods,
+  // but we need to triage all the types.
+  @notNull
+  bool result;
+  if (_isFutureOr(t2)) {
+    // Ignore if we would ignore either side of union.
+    var typeArg = getGenericArgs(t2)[0];
+    var typeFuture = JS('', '#(#)', getGenericClass(Future), typeArg);
+    result =
+        _ignoreTypeFailure(t1, typeFuture) || _ignoreTypeFailure(t1, typeArg);
+  } else {
+    result = t1 is FunctionType && t2 is FunctionType ||
+        isSubtypeOf(t2, unwrapType(Iterable)) &&
+            isSubtypeOf(t1, unwrapType(Iterable)) ||
+        isSubtypeOf(t2, unwrapType(Future)) &&
+            isSubtypeOf(t1, unwrapType(Future));
+    if (result) {
+      _warn('Ignoring cast fail from ${typeName(t1)} to ${typeName(t2)}');
     }
-    return false;
-  });
-})()''');
+  }
+  JS('', '#.set(#, #)', map, t2, result);
+  return result;
+}
 
 @notNull
 @JSExportName('is')
 bool instanceOf(obj, type) {
   if (obj == null) {
-    return JS('!', '# == # || #', type, Null, _isTop(type));
+    return identical(type, unwrapType(Null)) || _isTop(type);
   }
-  return JS('!', '!!#', isSubtype(getReifiedType(obj), type));
+  return isSubtypeOf(getReifiedType(obj), type);
 }
 
 @JSExportName('as')
 cast(obj, type, @notNull bool isImplicit) {
   if (obj == null) return obj;
   var actual = getReifiedType(obj);
-  var result = isSubtype(actual, type);
-  if (JS(
-      '!',
-      '# === true || # === null && # && '
-      'dart.__ignoreWhitelistedErrors && #(#, #)',
-      result,
-      result,
-      isImplicit,
-      _ignoreTypeFailure,
-      actual,
-      type)) {
+  if (_isSubtypeOrIgnorableCastFailure(actual, type, isImplicit)) {
     return obj;
   }
   return castError(obj, type, isImplicit);
diff --git a/pkg/dev_compiler/tool/input_sdk/private/ddc_runtime/rtti.dart b/pkg/dev_compiler/tool/input_sdk/private/ddc_runtime/rtti.dart
index 3797606..ef023d2 100644
--- a/pkg/dev_compiler/tool/input_sdk/private/ddc_runtime/rtti.dart
+++ b/pkg/dev_compiler/tool/input_sdk/private/ddc_runtime/rtti.dart
@@ -114,17 +114,17 @@
 }
 
 /// Return the module name for a raw library object.
-getModuleName(value) => JS('', '#[#]', value, _moduleName);
+String getModuleName(Object module) => JS('', '#[#]', module, _moduleName);
 
-var _loadedModules = JS('', 'new Map()');
-var _loadedSourceMaps = JS('', 'new Map()');
+final _loadedModules = JS('', 'new Map()');
+final _loadedSourceMaps = JS('', 'new Map()');
 
-List getModuleNames() {
-  return JS('', 'Array.from(#.keys())', _loadedModules);
+List<String> getModuleNames() {
+  return JSArray<String>.of(JS('', 'Array.from(#.keys())', _loadedModules));
 }
 
-String getSourceMap(module) {
-  return JS<String>('!', '#.get(#)', _loadedSourceMaps, module);
+String getSourceMap(String moduleName) {
+  return JS('!', '#.get(#)', _loadedSourceMaps, moduleName);
 }
 
 /// Return all library objects in the specified module.
@@ -136,7 +136,7 @@
 }
 
 /// Track all libraries
-void trackLibraries(String moduleName, libraries, sourceMap) {
+void trackLibraries(String moduleName, Object libraries, String sourceMap) {
   JS('', '#.set(#, #)', _loadedSourceMaps, moduleName, sourceMap);
   JS('', '#.set(#, #)', _loadedModules, moduleName, libraries);
 }
diff --git a/pkg/dev_compiler/tool/input_sdk/private/ddc_runtime/runtime.dart b/pkg/dev_compiler/tool/input_sdk/private/ddc_runtime/runtime.dart
index 39e38c1..82289ddd 100644
--- a/pkg/dev_compiler/tool/input_sdk/private/ddc_runtime/runtime.dart
+++ b/pkg/dev_compiler/tool/input_sdk/private/ddc_runtime/runtime.dart
@@ -154,3 +154,31 @@
 void setStartAsyncSynchronously([bool value = true]) {
   startAsyncSynchronously = value;
 }
+
+/// A list of all JS Maps used for caching results, such as by [isSubtypeOf] and
+/// [generic].
+///
+/// This is used by [hotRestart] to ensure we don't leak types from previous
+/// libraries.
+@notNull
+final List<Object> _cacheMaps = JS('!', '[]');
+
+/// A list of functions to reset static fields back to their uninitialized
+/// state.
+///
+/// This is populated by [defineLazyField].
+@notNull
+final List<void Function()> _resetFields = JS('', '[]');
+
+/// Clears out runtime state in `dartdevc` so we can hot-restart.
+///
+/// This should be called when the user requests a hot-restart, when the UI is
+/// handling that user action.
+void hotRestart() {
+  for (var f in _resetFields) f();
+  _resetFields.clear();
+  for (var m in _cacheMaps) JS('', '#.clear()', m);
+  _cacheMaps.clear();
+  JS('', '#.clear()', constantMaps);
+  JS('', '#.clear()', _ignoreSubtypeCache);
+}
diff --git a/pkg/dev_compiler/tool/input_sdk/private/ddc_runtime/types.dart b/pkg/dev_compiler/tool/input_sdk/private/ddc_runtime/types.dart
index c7ab6d8..4602bc2 100644
--- a/pkg/dev_compiler/tool/input_sdk/private/ddc_runtime/types.dart
+++ b/pkg/dev_compiler/tool/input_sdk/private/ddc_runtime/types.dart
@@ -242,7 +242,7 @@
 final _typeObject = JS('', 'Symbol("typeObject")');
 
 /// Given a WrappedType, return the internal runtime type object.
-Object unwrapType(_Type obj) => obj._type;
+Object unwrapType(Type obj) => JS<_Type>('', '#', obj)._type;
 
 // Marker class for generic functions, typedefs, and non-generic functions.
 abstract class AbstractFunctionType extends DartType {}
@@ -465,18 +465,11 @@
       var actual = JS('', '#[#]', obj, _runtimeType);
       // If there's no actual type, it's a JS function.
       // Allow them to subtype all Dart function types.
-      return JS('!', '# == null || !!#', actual, isSubtype(actual, this));
+      return actual == null || isSubtypeOf(actual, this);
     }
     return false;
   }
 
-  static final void Function(Object, Object) _logIgnoredCast =
-      JS('', '''(() => $_ignoreMemo((actual, expected) => {
-        console.warn('Ignoring cast fail from ' + $typeName(actual) +
-                     ' to ' + $typeName(expected));
-        return null;
-        }))()''');
-
   @JSExportName('as')
   as_T(obj, [@notNull bool isImplicit = false]) {
     if (obj == null) return obj;
@@ -484,13 +477,8 @@
       var actual = JS('', '#[#]', obj, _runtimeType);
       // If there's no actual type, it's a JS function.
       // Allow them to subtype all Dart function types.
-      if (actual == null) return obj;
-      var result = isSubtype(actual, this);
-      if (result == true) return obj;
-      if (result == null &&
-          isImplicit &&
-          JS<bool>('!', 'dart.__ignoreWhitelistedErrors')) {
-        _logIgnoredCast(actual, this);
+      if (actual == null ||
+          _isSubtypeOrIgnorableCastFailure(actual, this, isImplicit)) {
         return obj;
       }
     }
@@ -535,7 +523,7 @@
     var bounds = instantiateTypeBounds(typeArgs);
     var typeFormals = this.typeFormals;
     for (var i = 0; i < typeArgs.length; i++) {
-      checkTypeBound(typeArgs[i], bounds[i], typeFormals[i]);
+      checkTypeBound(typeArgs[i], bounds[i], typeFormals[i].name);
     }
   }
 
@@ -658,7 +646,7 @@
   bool is_T(obj) {
     if (JS('!', 'typeof # == "function"', obj)) {
       var actual = JS('', '#[#]', obj, _runtimeType);
-      return JS('!', '# != null && !!#', actual, isSubtype(actual, this));
+      return actual != null && isSubtypeOf(actual, this);
     }
     return false;
   }
@@ -732,12 +720,11 @@
 @notNull
 bool isType(obj) => JS('', '#[#] === #', obj, _runtimeType, Type);
 
-void checkTypeBound(type, bound, name) {
-  // TODO(jmesserly): we've optimized `is`/`as`/implicit type checks, it would
-  // be nice to have similar optimizations for the subtype relation.
-  if (JS('!', '#', isSubtype(type, bound))) return;
-
-  throwTypeError('type `$type` does not extend `$bound` of `$name`.');
+void checkTypeBound(
+    @notNull Object type, @notNull Object bound, @notNull String name) {
+  if (!isSubtypeOf(type, bound)) {
+    throwTypeError('type `$type` does not extend `$bound` of `$name`.');
+  }
 }
 
 @notNull
@@ -793,7 +780,7 @@
 
   for (let i = 0; i < args1.length; ++i) {
     if (!$_isSubtype(args2[i], args1[i], !$isCovariant)) {
-      // Even if isSubtype returns false, assignability
+      // Even if isSubtypeOf returns false, assignability
       // means that we can't be definitive
       return null;
     }
@@ -845,25 +832,29 @@
   return true;
 })()''');
 
+/// Whether [t1] <: [t2].
+@notNull
+bool isSubtypeOf(Object t1, Object t2) {
+  // TODO(jmesserly): we've optimized `is`/`as`/implicit type checks, so they're
+  // dispatched on the type. Can we optimize the subtype relation too?
+  return JS('!', '!!#', _isSubtypeOrLegacySubtype(t1, t2));
+}
+
 /// Returns true if [t1] <: [t2].
-/// Returns false if [t1] </: [t2] in both spec and strong mode
-/// Returns undefined if [t1] </: [t2] in strong mode, but spec
-///  mode may differ
-bool isSubtype(t1, t2) {
-  // TODO(leafp): This duplicates code in operations.dart.
-  // I haven't found a way to factor it out that makes the
-  // code generator happy though.
-  var map;
-  bool result;
+/// Returns false if [t1] </: [t2] and we should not ignore this cast failure.
+/// Returns null if [t1] </: [t2] and we should ignore this cast failure when
+/// the appropriate flags are set.
+bool _isSubtypeOrLegacySubtype(Object t1, Object t2) {
+  Object map;
   if (JS('!', '!#.hasOwnProperty(#)', t1, _subtypeCache)) {
     JS('', '#[#] = # = new Map()', t1, _subtypeCache, map);
+    _cacheMaps.add(map);
   } else {
     map = JS('', '#[#]', t1, _subtypeCache);
-    result = JS('bool|Null', '#.get(#)', map, t2);
+    bool result = JS('', '#.get(#)', map, t2);
     if (JS('!', '# !== void 0', result)) return result;
   }
-  result =
-      JS('bool|Null', '# === # || #(#, #, true)', t1, t2, _isSubtype, t1, t2);
+  var result = _isSubtype(t1, t2, true);
   JS('', '#.set(#, #)', map, t2, result);
   return result;
 }
@@ -882,6 +873,7 @@
       type, void_);
 }
 
+@notNull
 bool _isFutureOr(type) =>
     identical(getGenericClass(type), getGenericClass(FutureOr));
 
@@ -1399,11 +1391,11 @@
 
   void _constrainLower(Object type) {
     if (lower != null) {
-      if (isSubtype(lower, type)) {
+      if (isSubtypeOf(lower, type)) {
         // nothing to do, existing lower bound is lower than the new one.
         return;
       }
-      if (!isSubtype(type, lower)) {
+      if (!isSubtypeOf(type, lower)) {
         // Neither bound is lower and we don't have GLB, so use bottom type.
         type = unwrapType(Null);
       }
@@ -1413,11 +1405,11 @@
 
   void _constrainUpper(Object type) {
     if (upper != null) {
-      if (isSubtype(type, upper)) {
+      if (isSubtypeOf(type, upper)) {
         // nothing to do, existing upper bound is higher than the new one.
         return;
       }
-      if (!isSubtype(upper, type)) {
+      if (!isSubtypeOf(upper, type)) {
         // Neither bound is higher and we don't have LUB, so use top type.
         type = unwrapType(Object);
       }
diff --git a/pkg/dev_compiler/tool/input_sdk/private/ddc_runtime/utils.dart b/pkg/dev_compiler/tool/input_sdk/private/ddc_runtime/utils.dart
index 3ee1721..dcaf067 100644
--- a/pkg/dev_compiler/tool/input_sdk/private/ddc_runtime/utils.dart
+++ b/pkg/dev_compiler/tool/input_sdk/private/ddc_runtime/utils.dart
@@ -68,7 +68,8 @@
 // TODO(jmesserly): reusing descriptor objects has been shown to improve
 // performance in other projects (e.g. webcomponents.js ShadowDOM polyfill).
 defineLazyField(to, name, desc) => JS('', '''(() => {
-  let init = $desc.get;
+  const initializer = $desc.get;
+  let init = initializer;
   let value = null;
   $desc.get = function() {
     if (init == null) return value;
@@ -92,6 +93,10 @@
       value = x;
     };
   }
+  $_resetFields.push(() => {
+    init = initializer;
+    value = null;
+  });
   return ${defineProperty(to, name, desc)};
 })()''');
 
diff --git a/pkg/dev_compiler/tool/input_sdk/private/debugger.dart b/pkg/dev_compiler/tool/input_sdk/private/debugger.dart
index 86592dc..6ee554b 100644
--- a/pkg/dev_compiler/tool/input_sdk/private/debugger.dart
+++ b/pkg/dev_compiler/tool/input_sdk/private/debugger.dart
@@ -967,14 +967,14 @@
   JS('', '#.devtoolsFormatters = [#]', dart.global_, _devtoolsFormatter);
 }
 
-// Expose these methods here to facilitate writing debugger tests.
-// If export worked for private SDK libraries we could just export
-// these methods from dart:_runtime.
-
-getModuleNames() {
-  return dart.getModuleNames();
-}
-
-getModuleLibraries(String name) {
-  return dart.getModuleLibraries(name);
-}
+// These methods are exposed here for debugger tests.
+//
+// TODO(jmesserly): these are not exports because there is existing code that
+// calls into them from JS. Currently `dartdevc` always resolves exports at
+// compile time, so there is no need to make exports available at runtime by
+// copying properties. For that reason we cannot use re-export.
+//
+// If these methods are only for tests, we should move them here, or change the
+// tests to call the methods directly on dart:_runtime.
+List<String> getModuleNames() => dart.getModuleNames();
+getModuleLibraries(String name) => dart.getModuleLibraries(name);