Reland: [dart2js] Refactor closure data to use AST nodes instead of locals

This CL avoids creating locals using KernelToLocalsMap when computing
closure data for the J-model. Instead the closure data uses
ir.VariableDeclaration, JTypeVariable, and internal locals (ThisLocal
and BoxLocal) internally, and the accessor function require passes
a KernelToLocalsMap to convert the internal model to a Locals upon use.
The converted Locals are cached internal for performance only.

This prepares for splitting the GlobalLocalsMap and KernelToLocalsMap
from the JClosedWorld.

Previously landed in https://dart-review.googlesource.com/c/sdk/+/180820
but reverted.

Change-Id: Ic6a2ab63de7cb31c89dd50ca2af3f205a792fd74
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/183000
Reviewed-by: Sigmund Cherem <sigmund@google.com>
diff --git a/pkg/compiler/lib/src/closure.dart b/pkg/compiler/lib/src/closure.dart
index 89edda7..2d0efa6 100644
--- a/pkg/compiler/lib/src/closure.dart
+++ b/pkg/compiler/lib/src/closure.dart
@@ -68,7 +68,7 @@
       case ScopeInfoKind.capturedLoopScope:
         return new JsCapturedLoopScope.readFromDataSource(source);
       case ScopeInfoKind.closureRepresentationInfo:
-        return new KernelClosureClassInfo.readFromDataSource(source);
+        return new JsClosureClassInfo.readFromDataSource(source);
     }
     throw new UnsupportedError('Unexpected ScopeInfoKind $kind');
   }
@@ -92,7 +92,8 @@
   /// Also parameters to a `sync*` generator must be boxed, because of the way
   /// we rewrite sync* functions. See also comments in
   /// [ClosureClassMap.useLocal].
-  bool localIsUsedInTryOrSync(Local variable) => false;
+  bool localIsUsedInTryOrSync(KernelToLocalsMap localsMap, Local variable) =>
+      false;
 
   /// Loop through each variable that has been defined in this scope, modified
   /// anywhere (this scope or another scope) and used in another scope. Because
@@ -104,10 +105,11 @@
   /// In the case of loops, this is the set of iteration variables (or any
   /// variables declared in the for loop expression (`for (...here...)`) that
   /// need to be boxed to snapshot their value.
-  void forEachBoxedVariable(f(Local local, FieldEntity field)) {}
+  void forEachBoxedVariable(
+      KernelToLocalsMap localsMap, f(Local local, FieldEntity field)) {}
 
   /// True if [variable] has been mutated and is also used in another scope.
-  bool isBoxedVariable(Local variable) => false;
+  bool isBoxedVariable(KernelToLocalsMap localsMap, Local variable) => false;
 }
 
 /// Class representing the usage of a scope that has been captured in the
@@ -141,7 +143,7 @@
   /// executed. This will encapsulate the value of any variables that have been
   /// scoped into this context from outside. This is an accessor to the
   /// contextBox that [requiresContextBox] is testing is required.
-  Local get context => null;
+  Local get contextBox => null;
 }
 
 /// Class that describes the actual mechanics of how values of variables
@@ -195,7 +197,8 @@
   ///
   /// `i` would be a part of the boxedLoopVariables AND boxedVariables, but b
   /// would only be a part of boxedVariables.
-  List<Local> get boxedLoopVariables => const <Local>[];
+  List<Local> getBoxedLoopVariables(KernelToLocalsMap localsMap) =>
+      const <Local>[];
 }
 
 /// Class that describes the actual mechanics of how the converted, rewritten
@@ -239,7 +242,7 @@
         throw new UnsupportedError(
             'Unexpected ClosureRepresentationInfo kind $kind');
       case ScopeInfoKind.closureRepresentationInfo:
-        return new KernelClosureClassInfo.readFromDataSource(source);
+        return new JsClosureClassInfo.readFromDataSource(source);
     }
     throw new UnsupportedError('Unexpected ScopeInfoKind $kind');
   }
@@ -247,7 +250,7 @@
   /// The original local function before any translation.
   ///
   /// Will be null for methods.
-  Local get closureEntity => null;
+  Local getClosureEntity(KernelToLocalsMap localsMap) => null;
 
   /// The entity for the class used to represent the rewritten closure in the
   /// emitted JavaScript.
@@ -266,13 +269,14 @@
   /// List of locals that this closure class has created corresponding field
   /// entities for.
   @deprecated
-  List<Local> get createdFieldEntities => const <Local>[];
+  List<Local> getCreatedFieldEntities(KernelToLocalsMap localsMap) =>
+      const <Local>[];
 
   /// As shown in the example in the comments at the top of this class, we
   /// create fields in the closure class for each captured variable. This is an
   /// accessor the [local] for which [field] was created.
   /// Returns the [local] for which [field] was created.
-  Local getLocalForField(FieldEntity field) {
+  Local getLocalForField(KernelToLocalsMap localsMap, FieldEntity field) {
     failedAt(field, "No local for $field.");
     return null;
   }
@@ -289,12 +293,14 @@
   /// strictly variables defined in this closure, unlike the behavior in
   /// the superclass ScopeInfo.
   @override
-  void forEachBoxedVariable(f(Local local, FieldEntity field)) {}
+  void forEachBoxedVariable(
+      KernelToLocalsMap localsMap, f(Local local, FieldEntity field)) {}
 
   /// Loop through each free variable in this closure. Free variables are the
   /// variables that have been captured *just* in this closure, not in nested
   /// scopes.
-  void forEachFreeVariable(f(Local variable, FieldEntity field)) {}
+  void forEachFreeVariable(
+      KernelToLocalsMap localsMap, f(Local variable, FieldEntity field)) {}
 
   // TODO(efortuna): Remove this method. The old system was using
   // ClosureClassMaps for situations other than closure class maps, and that's
diff --git a/pkg/compiler/lib/src/inferrer/builder_kernel.dart b/pkg/compiler/lib/src/inferrer/builder_kernel.dart
index 9793cc5..337c547 100644
--- a/pkg/compiler/lib/src/inferrer/builder_kernel.dart
+++ b/pkg/compiler/lib/src/inferrer/builder_kernel.dart
@@ -218,7 +218,7 @@
     // each update, and reading them yields the type that was found in a
     // previous analysis of [outermostElement].
     ScopeInfo scopeInfo = _closureDataLookup.getScopeInfo(_analyzedMember);
-    scopeInfo.forEachBoxedVariable((variable, field) {
+    scopeInfo.forEachBoxedVariable(_localsMap, (variable, field) {
       _capturedAndBoxed[variable] = field;
     });
 
@@ -1722,8 +1722,8 @@
     // Record the types of captured non-boxed variables. Types of
     // these variables may already be there, because of an analysis of
     // a previous closure.
-    info.forEachFreeVariable((Local variable, FieldEntity field) {
-      if (!info.isBoxedVariable(variable)) {
+    info.forEachFreeVariable(_localsMap, (Local variable, FieldEntity field) {
+      if (!info.isBoxedVariable(_localsMap, variable)) {
         if (variable == info.thisLocal) {
           _inferrer.recordTypeOfField(field, thisType);
         }
diff --git a/pkg/compiler/lib/src/inferrer/type_graph_inferrer.dart b/pkg/compiler/lib/src/inferrer/type_graph_inferrer.dart
index d1e4239..7ab25ce 100644
--- a/pkg/compiler/lib/src/inferrer/type_graph_inferrer.dart
+++ b/pkg/compiler/lib/src/inferrer/type_graph_inferrer.dart
@@ -134,7 +134,9 @@
       if (member is JClosureCallMethod) {
         ClosureRepresentationInfo info =
             closedWorld.closureDataLookup.getScopeInfo(member);
-        info.forEachFreeVariable((Local from, FieldEntity to) {
+        info.forEachFreeVariable(
+            closedWorld.globalLocalsMap.getLocalsMap(member),
+            (Local from, FieldEntity to) {
           freeVariables.add(to);
         });
       }
diff --git a/pkg/compiler/lib/src/ir/closure.dart b/pkg/compiler/lib/src/ir/closure.dart
index 8d0c0c1..a9d5604 100644
--- a/pkg/compiler/lib/src/ir/closure.dart
+++ b/pkg/compiler/lib/src/ir/closure.dart
@@ -343,7 +343,7 @@
 /// A fake ir.Node that holds the TypeParameterType as well as the context in
 /// which it occurs.
 class TypeVariableTypeWithContext implements ir.Node {
-  final ir.Node context;
+  final ir.TreeNode context;
   final ir.TypeParameterType type;
   final TypeVariableKind kind;
   final ir.TreeNode typeDeclaration;
diff --git a/pkg/compiler/lib/src/js_model/closure.dart b/pkg/compiler/lib/src/js_model/closure.dart
index 570da8a..8f71fb8 100644
--- a/pkg/compiler/lib/src/js_model/closure.dart
+++ b/pkg/compiler/lib/src/js_model/closure.dart
@@ -302,24 +302,23 @@
       ClosureRtiNeed rtiNeed,
       List<FunctionEntity> callMethods) {
     closureModels.forEach((MemberEntity member, ClosureScopeModel model) {
-      KernelToLocalsMap localsMap = _globalLocalsMap.getLocalsMap(member);
-      Map<Local, JRecordField> allBoxedVariables =
-          _elementMap.makeRecordContainer(model.scopeInfo, member, localsMap);
+      Map<ir.VariableDeclaration, JRecordField> allBoxedVariables =
+          _elementMap.makeRecordContainer(model.scopeInfo, member);
       _scopeMap[member] = new JsScopeInfo.from(
-          allBoxedVariables, model.scopeInfo, localsMap, _elementMap);
+          allBoxedVariables, model.scopeInfo, member.enclosingClass);
 
       model.capturedScopesMap
           .forEach((ir.Node node, KernelCapturedScope scope) {
-        Map<Local, JRecordField> boxedVariables =
-            _elementMap.makeRecordContainer(scope, member, localsMap);
+        Map<ir.VariableDeclaration, JRecordField> boxedVariables =
+            _elementMap.makeRecordContainer(scope, member);
         _updateScopeBasedOnRtiNeed(scope, rtiNeed, member);
 
         if (scope is KernelCapturedLoopScope) {
           _capturedScopesMap[node] = new JsCapturedLoopScope.from(
-              boxedVariables, scope, localsMap, _elementMap);
+              boxedVariables, scope, member.enclosingClass);
         } else {
           _capturedScopesMap[node] = new JsCapturedScope.from(
-              boxedVariables, scope, localsMap, _elementMap);
+              boxedVariables, scope, member.enclosingClass);
         }
         allBoxedVariables.addAll(boxedVariables);
       });
@@ -328,7 +327,7 @@
           model.closuresToGenerate;
       for (ir.LocalFunction node in closuresToGenerate.keys) {
         ir.FunctionNode functionNode = node.function;
-        KernelClosureClassInfo closureClassInfo = _produceSyntheticElements(
+        JsClosureClassInfo closureClassInfo = _produceSyntheticElements(
             closedWorldBuilder,
             member,
             functionNode,
@@ -355,7 +354,7 @@
             _updateScopeBasedOnRtiNeed(signatureCapturedScope, rtiNeed, member);
             _capturedScopeForSignatureMap[closureClassInfo.signatureMethod] =
                 new JsCapturedScope.from(
-                    {}, signatureCapturedScope, localsMap, _elementMap);
+                    {}, signatureCapturedScope, member.enclosingClass);
           }
         }
         callMethods.add(closureClassInfo.callMethod);
@@ -371,20 +370,19 @@
   /// the closure accesses a variable that gets accessed at some point), then
   /// boxForCapturedVariables stores the local context for those variables.
   /// If no variables are captured, this parameter is null.
-  KernelClosureClassInfo _produceSyntheticElements(
+  JsClosureClassInfo _produceSyntheticElements(
       JsClosedWorldBuilder closedWorldBuilder,
       MemberEntity member,
       ir.FunctionNode node,
       KernelScopeInfo info,
-      Map<Local, JRecordField> boxedVariables,
+      Map<ir.VariableDeclaration, JRecordField> boxedVariables,
       ClosureRtiNeed rtiNeed,
       {bool createSignatureMethod}) {
     _updateScopeBasedOnRtiNeed(info, rtiNeed, member);
     KernelToLocalsMap localsMap = _globalLocalsMap.getLocalsMap(member);
-    KernelClosureClassInfo closureClassInfo =
-        closedWorldBuilder.buildClosureClass(
-            member, node, member.library, boxedVariables, info, localsMap,
-            createSignatureMethod: createSignatureMethod);
+    JsClosureClassInfo closureClassInfo = closedWorldBuilder.buildClosureClass(
+        member, node, member.library, boxedVariables, info,
+        createSignatureMethod: createSignatureMethod);
 
     // We want the original declaration where that function is used to point
     // to the correct closure class.
@@ -403,98 +401,102 @@
   }
 }
 
-/// Helper method to get or create a Local variable out of a variable
-/// declaration or type parameter.
-Local _getLocal(
-    ir.Node variable, KernelToLocalsMap localsMap, JsToElementMap elementMap) {
-  assert(variable is ir.VariableDeclaration ||
-      variable is TypeVariableTypeWithContext);
-  if (variable is ir.VariableDeclaration) {
-    return localsMap.getLocalVariable(variable);
-  } else if (variable is TypeVariableTypeWithContext) {
-    return localsMap.getLocalTypeVariable(variable.type, elementMap);
-  }
-  throw new ArgumentError('Only know how to get/create locals for '
-      'VariableDeclarations or TypeParameterTypeWithContext. Recieved '
-      '${variable.runtimeType}');
-}
-
 class JsScopeInfo extends ScopeInfo {
   /// Tag used for identifying serialized [JsScopeInfo] objects in a
   /// debugging data stream.
   static const String tag = 'scope-info';
 
-  final Iterable<Local> localsUsedInTryOrSync;
+  final Iterable<ir.VariableDeclaration> _localsUsedInTryOrSync;
+
+  Set<Local> _localsUsedInTryOrSyncCache;
+
   @override
   final Local thisLocal;
-  final Map<Local, JRecordField> boxedVariables;
 
-  /// The set of variables that were defined in another scope, but are used in
-  /// this scope.
-  final Set<Local> freeVariables;
+  final Map<ir.VariableDeclaration, JRecordField> _boxedVariables;
 
-  JsScopeInfo.internal(this.localsUsedInTryOrSync, this.thisLocal,
-      this.boxedVariables, this.freeVariables);
+  Map<Local, JRecordField> _boxedVariablesCache;
 
-  JsScopeInfo.from(this.boxedVariables, KernelScopeInfo info,
-      KernelToLocalsMap localsMap, JsToElementMap elementMap)
-      : this.thisLocal = info.hasThisLocal
-            ? new ThisLocal(localsMap.currentMember.enclosingClass)
-            : null,
-        this.localsUsedInTryOrSync =
-            info.localsUsedInTryOrSync.map(localsMap.getLocalVariable).toSet(),
-        this.freeVariables = info.freeVariables
-            .map((ir.Node node) => _getLocal(node, localsMap, elementMap))
-            .toSet() {
-    if (info.thisUsedAsFreeVariable) {
-      this.freeVariables.add(this.thisLocal);
+  JsScopeInfo.internal(
+      this._localsUsedInTryOrSync, this.thisLocal, this._boxedVariables);
+
+  JsScopeInfo.from(
+      this._boxedVariables, KernelScopeInfo info, ClassEntity enclosingClass)
+      : this.thisLocal =
+            info.hasThisLocal ? new ThisLocal(enclosingClass) : null,
+        this._localsUsedInTryOrSync = info.localsUsedInTryOrSync;
+
+  void _ensureBoxedVariableCache(KernelToLocalsMap localsMap) {
+    if (_boxedVariablesCache == null) {
+      if (_boxedVariables.isEmpty) {
+        _boxedVariablesCache = const {};
+      } else {
+        _boxedVariablesCache = {};
+        _boxedVariables
+            .forEach((ir.VariableDeclaration node, JRecordField field) {
+          _boxedVariablesCache[localsMap.getLocalVariable(node)] = field;
+        });
+      }
     }
   }
 
   @override
-  void forEachBoxedVariable(f(Local local, FieldEntity field)) {
-    boxedVariables.forEach((Local l, JRecordField box) {
-      f(l, box);
-    });
+  void forEachBoxedVariable(
+      KernelToLocalsMap localsMap, f(Local local, FieldEntity field)) {
+    _ensureBoxedVariableCache(localsMap);
+    _boxedVariablesCache.forEach(f);
   }
 
   @override
-  bool localIsUsedInTryOrSync(Local variable) =>
-      localsUsedInTryOrSync.contains(variable);
+  bool localIsUsedInTryOrSync(KernelToLocalsMap localsMap, Local variable) {
+    if (_localsUsedInTryOrSyncCache == null) {
+      if (_localsUsedInTryOrSync.isEmpty) {
+        _localsUsedInTryOrSyncCache = const {};
+      } else {
+        _localsUsedInTryOrSyncCache = {};
+        for (ir.VariableDeclaration node in _localsUsedInTryOrSync) {
+          _localsUsedInTryOrSyncCache.add(localsMap.getLocalVariable(node));
+        }
+      }
+    }
+    return _localsUsedInTryOrSyncCache.contains(variable);
+  }
 
   @override
   String toString() {
     StringBuffer sb = new StringBuffer();
     sb.write('this=$thisLocal,');
-    sb.write('localsUsedInTryOrSync={${localsUsedInTryOrSync.join(', ')}}');
+    sb.write('localsUsedInTryOrSync={${_localsUsedInTryOrSync.join(', ')}}');
     return sb.toString();
   }
 
   @override
-  bool isBoxedVariable(Local variable) => boxedVariables.containsKey(variable);
+  bool isBoxedVariable(KernelToLocalsMap localsMap, Local variable) {
+    _ensureBoxedVariableCache(localsMap);
+    return _boxedVariablesCache.containsKey(variable);
+  }
 
   factory JsScopeInfo.readFromDataSource(DataSource source) {
     source.begin(tag);
-    Iterable<Local> localsUsedInTryOrSync = source.readLocals();
+    Iterable<ir.VariableDeclaration> localsUsedInTryOrSync =
+        source.readTreeNodes<ir.VariableDeclaration>();
     Local thisLocal = source.readLocalOrNull();
-    Map<Local, JRecordField> boxedVariables =
-        source.readLocalMap<Local, JRecordField>(() => source.readMember());
-    Set<Local> freeVariables = source.readLocals().toSet();
+    Map<ir.VariableDeclaration, JRecordField> boxedVariables =
+        source.readTreeNodeMap<ir.VariableDeclaration, JRecordField>(
+            () => source.readMember());
     source.end(tag);
     if (boxedVariables.isEmpty) boxedVariables = const {};
-    if (freeVariables.isEmpty) freeVariables = const {};
     return JsScopeInfo.internal(
-        localsUsedInTryOrSync, thisLocal, boxedVariables, freeVariables);
+        localsUsedInTryOrSync, thisLocal, boxedVariables);
   }
 
   @override
   void writeToDataSink(DataSink sink) {
     sink.writeEnum(ScopeInfoKind.scopeInfo);
     sink.begin(tag);
-    sink.writeLocals(localsUsedInTryOrSync);
+    sink.writeTreeNodes(_localsUsedInTryOrSync);
     sink.writeLocalOrNull(thisLocal);
-    sink.writeLocalMap(boxedVariables, sink.writeMember);
-    sink.writeLocals(freeVariables);
+    sink.writeTreeNodeMap(_boxedVariables, sink.writeMember);
     sink.end(tag);
   }
 }
@@ -505,51 +507,46 @@
   static const String tag = 'captured-scope';
 
   @override
-  final Local context;
+  final Local contextBox;
 
   JsCapturedScope.internal(
-      Iterable<Local> localsUsedInTryOrSync,
+      Iterable<ir.VariableDeclaration> localsUsedInTryOrSync,
       Local thisLocal,
-      Map<Local, JRecordField> boxedVariables,
-      Set<Local> freeVariables,
-      this.context)
-      : super.internal(
-            localsUsedInTryOrSync, thisLocal, boxedVariables, freeVariables);
+      Map<ir.VariableDeclaration, JRecordField> boxedVariables,
+      this.contextBox)
+      : super.internal(localsUsedInTryOrSync, thisLocal, boxedVariables);
 
-  JsCapturedScope.from(
-      Map<Local, JRecordField> boxedVariables,
-      KernelCapturedScope capturedScope,
-      KernelToLocalsMap localsMap,
-      JsToElementMap elementMap)
-      : this.context =
+  JsCapturedScope.from(Map<ir.VariableDeclaration, JRecordField> boxedVariables,
+      KernelCapturedScope capturedScope, ClassEntity enclosingClass)
+      : this.contextBox =
             boxedVariables.isNotEmpty ? boxedVariables.values.first.box : null,
-        super.from(boxedVariables, capturedScope, localsMap, elementMap);
+        super.from(boxedVariables, capturedScope, enclosingClass);
 
   @override
-  bool get requiresContextBox => boxedVariables.isNotEmpty;
+  bool get requiresContextBox => _boxedVariables.isNotEmpty;
 
   factory JsCapturedScope.readFromDataSource(DataSource source) {
     source.begin(tag);
-    Iterable<Local> localsUsedInTryOrSync = source.readLocals();
+    Iterable<ir.VariableDeclaration> localsUsedInTryOrSync =
+        source.readTreeNodes<ir.VariableDeclaration>();
     Local thisLocal = source.readLocalOrNull();
-    Map<Local, JRecordField> boxedVariables =
-        source.readLocalMap<Local, JRecordField>(() => source.readMember());
-    Set<Local> freeVariables = source.readLocals().toSet();
+    Map<ir.VariableDeclaration, JRecordField> boxedVariables =
+        source.readTreeNodeMap<ir.VariableDeclaration, JRecordField>(
+            () => source.readMember());
     Local context = source.readLocalOrNull();
     source.end(tag);
-    return new JsCapturedScope.internal(localsUsedInTryOrSync, thisLocal,
-        boxedVariables, freeVariables, context);
+    return new JsCapturedScope.internal(
+        localsUsedInTryOrSync, thisLocal, boxedVariables, context);
   }
 
   @override
   void writeToDataSink(DataSink sink) {
     sink.writeEnum(ScopeInfoKind.capturedScope);
     sink.begin(tag);
-    sink.writeLocals(localsUsedInTryOrSync);
+    sink.writeTreeNodes(_localsUsedInTryOrSync);
     sink.writeLocalOrNull(thisLocal);
-    sink.writeLocalMap(boxedVariables, sink.writeMember);
-    sink.writeLocals(freeVariables);
-    sink.writeLocalOrNull(context);
+    sink.writeTreeNodeMap(_boxedVariables, sink.writeMember);
+    sink.writeLocalOrNull(contextBox);
     sink.end(tag);
   }
 }
@@ -559,65 +556,69 @@
   /// debugging data stream.
   static const String tag = 'captured-loop-scope';
 
-  @override
-  final List<Local> boxedLoopVariables;
+  final List<ir.VariableDeclaration> _boxedLoopVariables;
 
   JsCapturedLoopScope.internal(
-      Iterable<Local> localsUsedInTryOrSync,
+      Iterable<ir.VariableDeclaration> localsUsedInTryOrSync,
       Local thisLocal,
-      Map<Local, JRecordField> boxedVariables,
-      Set<Local> freeVariables,
+      Map<ir.VariableDeclaration, JRecordField> boxedVariables,
       Local context,
-      this.boxedLoopVariables)
-      : super.internal(localsUsedInTryOrSync, thisLocal, boxedVariables,
-            freeVariables, context);
+      this._boxedLoopVariables)
+      : super.internal(
+            localsUsedInTryOrSync, thisLocal, boxedVariables, context);
 
   JsCapturedLoopScope.from(
-      Map<Local, JRecordField> boxedVariables,
+      Map<ir.VariableDeclaration, JRecordField> boxedVariables,
       KernelCapturedLoopScope capturedScope,
-      KernelToLocalsMap localsMap,
-      JsToElementMap elementMap)
-      : this.boxedLoopVariables = capturedScope.boxedLoopVariables
-            .map(localsMap.getLocalVariable)
-            .toList(),
-        super.from(boxedVariables, capturedScope, localsMap, elementMap);
+      ClassEntity enclosingClass)
+      : this._boxedLoopVariables = capturedScope.boxedLoopVariables,
+        super.from(boxedVariables, capturedScope, enclosingClass);
 
   @override
-  bool get hasBoxedLoopVariables => boxedLoopVariables.isNotEmpty;
+  bool get hasBoxedLoopVariables => _boxedLoopVariables.isNotEmpty;
 
   factory JsCapturedLoopScope.readFromDataSource(DataSource source) {
     source.begin(tag);
-    Iterable<Local> localsUsedInTryOrSync = source.readLocals();
+    Iterable<ir.VariableDeclaration> localsUsedInTryOrSync =
+        source.readTreeNodes<ir.VariableDeclaration>();
     Local thisLocal = source.readLocalOrNull();
-    Map<Local, JRecordField> boxedVariables =
-        source.readLocalMap<Local, JRecordField>(() => source.readMember());
-    Set<Local> freeVariables = source.readLocals().toSet();
+    Map<ir.VariableDeclaration, JRecordField> boxedVariables =
+        source.readTreeNodeMap<ir.VariableDeclaration, JRecordField>(
+            () => source.readMember());
     Local context = source.readLocalOrNull();
-    List<Local> boxedLoopVariables = source.readLocals();
+    List<ir.VariableDeclaration> boxedLoopVariables =
+        source.readTreeNodes<ir.VariableDeclaration>();
     source.end(tag);
     return new JsCapturedLoopScope.internal(localsUsedInTryOrSync, thisLocal,
-        boxedVariables, freeVariables, context, boxedLoopVariables);
+        boxedVariables, context, boxedLoopVariables);
   }
 
   @override
   void writeToDataSink(DataSink sink) {
     sink.writeEnum(ScopeInfoKind.capturedLoopScope);
     sink.begin(tag);
-    sink.writeLocals(localsUsedInTryOrSync);
+    sink.writeTreeNodes(_localsUsedInTryOrSync);
     sink.writeLocalOrNull(thisLocal);
-    sink.writeLocalMap(boxedVariables, sink.writeMember);
-    sink.writeLocals(freeVariables);
-    sink.writeLocalOrNull(context);
-    sink.writeLocals(boxedLoopVariables);
+    sink.writeTreeNodeMap(_boxedVariables, sink.writeMember);
+    sink.writeLocalOrNull(contextBox);
+    sink.writeTreeNodes(_boxedLoopVariables);
     sink.end(tag);
   }
+
+  @override
+  List<Local> getBoxedLoopVariables(KernelToLocalsMap localsMap) {
+    List<Local> locals = [];
+    for (ir.VariableDeclaration boxedLoopVariable in _boxedLoopVariables) {
+      locals.add(localsMap.getLocalVariable(boxedLoopVariable));
+    }
+    return locals;
+  }
 }
 
-// TODO(johnniwinther): Rename this class.
 // TODO(johnniwinther): Add unittest for the computed [ClosureClass].
-class KernelClosureClassInfo extends JsScopeInfo
+class JsClosureClassInfo extends JsScopeInfo
     implements ClosureRepresentationInfo {
-  /// Tag used for identifying serialized [KernelClosureClassInfo] objects in a
+  /// Tag used for identifying serialized [JsClosureClassInfo] objects in a
   /// debugging data stream.
   static const String tag = 'closure-representation-info';
 
@@ -625,110 +626,194 @@
   JFunction callMethod;
   @override
   JSignatureMethod signatureMethod;
-  @override
-  final Local closureEntity;
+
+  /// The local used for this closure, if it is an anonymous closure, i.e.
+  /// an `ir.FunctionExpression`.
+  final Local _closureEntity;
+
+  /// The local variable that defines this closure, if it is a local function
+  /// declaration.
+  final ir.VariableDeclaration _closureEntityVariable;
+
   @override
   final Local thisLocal;
+
   @override
   final JClass closureClassEntity;
 
-  final Map<Local, JField> localToFieldMap;
+  final Map<ir.VariableDeclaration, JField> _variableToFieldMap;
+  final Map<JTypeVariable, JField> _typeVariableToFieldMap;
+  final Map<Local, JField> _localToFieldMap;
+  Map<JField, Local> _fieldToLocalsMap;
 
-  KernelClosureClassInfo.internal(
-      Iterable<Local> localsUsedInTryOrSync,
+  JsClosureClassInfo.internal(
+      Iterable<ir.VariableDeclaration> localsUsedInTryOrSync,
       this.thisLocal,
-      Map<Local, JRecordField> boxedVariables,
-      Set<Local> freeVariables,
+      Map<ir.VariableDeclaration, JRecordField> boxedVariables,
       this.callMethod,
       this.signatureMethod,
-      this.closureEntity,
+      this._closureEntity,
+      this._closureEntityVariable,
       this.closureClassEntity,
-      this.localToFieldMap)
-      : super.internal(
-            localsUsedInTryOrSync, thisLocal, boxedVariables, freeVariables);
+      this._variableToFieldMap,
+      this._typeVariableToFieldMap,
+      this._localToFieldMap)
+      : super.internal(localsUsedInTryOrSync, thisLocal, boxedVariables);
 
-  KernelClosureClassInfo.fromScopeInfo(
+  JsClosureClassInfo.fromScopeInfo(
       this.closureClassEntity,
       ir.FunctionNode closureSourceNode,
-      Map<Local, JRecordField> boxedVariables,
+      Map<ir.VariableDeclaration, JRecordField> boxedVariables,
       KernelScopeInfo info,
-      KernelToLocalsMap localsMap,
-      this.closureEntity,
-      this.thisLocal,
-      JsToElementMap elementMap)
-      : localToFieldMap = new Map<Local, JField>(),
-        super.from(boxedVariables, info, localsMap, elementMap);
+      ClassEntity enclosingClass,
+      this._closureEntity,
+      this._closureEntityVariable,
+      this.thisLocal)
+      : _variableToFieldMap = {},
+        _typeVariableToFieldMap = {},
+        _localToFieldMap = {},
+        super.from(boxedVariables, info, enclosingClass);
 
-  factory KernelClosureClassInfo.readFromDataSource(DataSource source) {
+  factory JsClosureClassInfo.readFromDataSource(DataSource source) {
     source.begin(tag);
-    Iterable<Local> localsUsedInTryOrSync = source.readLocals();
+    Iterable<ir.VariableDeclaration> localsUsedInTryOrSync =
+        source.readTreeNodes<ir.VariableDeclaration>();
     Local thisLocal = source.readLocalOrNull();
-    Map<Local, JRecordField> boxedVariables =
-        source.readLocalMap<Local, JRecordField>(() => source.readMember());
-    Set<Local> freeVariables = source.readLocals().toSet();
+    Map<ir.VariableDeclaration, JRecordField> boxedVariables =
+        source.readTreeNodeMap<ir.VariableDeclaration, JRecordField>(
+            () => source.readMember());
     JFunction callMethod = source.readMember();
     JSignatureMethod signatureMethod = source.readMemberOrNull();
     Local closureEntity = source.readLocalOrNull();
+    ir.VariableDeclaration closureEntityVariable = source.readTreeNodeOrNull();
     JClass closureClassEntity = source.readClass();
-    Map<Local, JField> localToFieldMap =
+    Map<ir.VariableDeclaration, JField> localToFieldMap =
+        source.readTreeNodeMap<ir.VariableDeclaration, JField>(
+            () => source.readMember());
+    Map<JTypeVariable, JField> typeVariableToFieldMap = source
+        .readTypeVariableMap<JTypeVariable, JField>(() => source.readMember());
+    Map<Local, JField> thisLocalToFieldMap =
         source.readLocalMap(() => source.readMember());
     source.end(tag);
     if (boxedVariables.isEmpty) boxedVariables = const {};
-    if (freeVariables.isEmpty) freeVariables = const {};
     if (localToFieldMap.isEmpty) localToFieldMap = const {};
-    return KernelClosureClassInfo.internal(
+    return JsClosureClassInfo.internal(
         localsUsedInTryOrSync,
         thisLocal,
         boxedVariables,
-        freeVariables,
         callMethod,
         signatureMethod,
         closureEntity,
+        closureEntityVariable,
         closureClassEntity,
-        localToFieldMap);
+        localToFieldMap,
+        typeVariableToFieldMap,
+        thisLocalToFieldMap);
   }
 
   @override
   void writeToDataSink(DataSink sink) {
     sink.writeEnum(ScopeInfoKind.closureRepresentationInfo);
     sink.begin(tag);
-    sink.writeLocals(localsUsedInTryOrSync);
+    sink.writeTreeNodes(_localsUsedInTryOrSync);
     sink.writeLocalOrNull(thisLocal);
-    sink.writeLocalMap(boxedVariables, sink.writeMember);
-    sink.writeLocals(freeVariables);
+    sink.writeTreeNodeMap(_boxedVariables, sink.writeMember);
     sink.writeMember(callMethod);
     sink.writeMemberOrNull(signatureMethod);
-    sink.writeLocalOrNull(closureEntity);
+    sink.writeLocalOrNull(_closureEntity);
+    sink.writeTreeNodeOrNull(_closureEntityVariable);
     sink.writeClass(closureClassEntity);
-    sink.writeLocalMap(localToFieldMap, sink.writeMember);
+    sink.writeTreeNodeMap(_variableToFieldMap, sink.writeMember);
+    sink.writeTypeVariableMap(_typeVariableToFieldMap, sink.writeMember);
+    sink.writeLocalMap(_localToFieldMap, sink.writeMember);
     sink.end(tag);
   }
 
-  @override
-  List<Local> get createdFieldEntities => localToFieldMap.keys.toList();
+  bool hasFieldForLocal(Local local) => _localToFieldMap.containsKey(local);
 
-  @override
-  Local getLocalForField(FieldEntity field) {
-    for (Local local in localToFieldMap.keys) {
-      if (localToFieldMap[local] == field) {
-        return local;
+  void registerFieldForLocal(Local local, JField field) {
+    assert(_fieldToLocalsMap == null);
+    _localToFieldMap[local] = field;
+  }
+
+  void registerFieldForVariable(ir.VariableDeclaration node, JField field) {
+    assert(_fieldToLocalsMap == null);
+    _variableToFieldMap[node] = field;
+  }
+
+  bool hasFieldForTypeVariable(JTypeVariable typeVariable) =>
+      _typeVariableToFieldMap.containsKey(typeVariable);
+
+  void registerFieldForTypeVariable(JTypeVariable typeVariable, JField field) {
+    assert(_fieldToLocalsMap == null);
+    _typeVariableToFieldMap[typeVariable] = field;
+  }
+
+  void registerFieldForBoxedVariable(
+      ir.VariableDeclaration node, JField field) {
+    assert(_boxedVariablesCache == null);
+    _boxedVariables[node] = field;
+  }
+
+  void _ensureFieldToLocalsMap(KernelToLocalsMap localsMap) {
+    if (_fieldToLocalsMap == null) {
+      _fieldToLocalsMap = {};
+      _variableToFieldMap.forEach((ir.VariableDeclaration node, JField field) {
+        _fieldToLocalsMap[field] = localsMap.getLocalVariable(node);
+      });
+      _typeVariableToFieldMap
+          .forEach((TypeVariableEntity typeVariable, JField field) {
+        _fieldToLocalsMap[field] =
+            localsMap.getLocalTypeVariableEntity(typeVariable);
+      });
+      _localToFieldMap.forEach((Local local, JField field) {
+        _fieldToLocalsMap[field] = local;
+      });
+      if (_fieldToLocalsMap.isEmpty) {
+        _fieldToLocalsMap = const {};
       }
     }
-    failedAt(field, "No local for $field. Options: $localToFieldMap");
-    return null;
   }
 
   @override
-  FieldEntity get thisFieldEntity => localToFieldMap[thisLocal];
+  List<Local> getCreatedFieldEntities(KernelToLocalsMap localsMap) {
+    _ensureFieldToLocalsMap(localsMap);
+    return _fieldToLocalsMap.values.toList();
+  }
 
   @override
-  void forEachFreeVariable(f(Local variable, JField field)) {
-    localToFieldMap.forEach(f);
-    boxedVariables.forEach(f);
+  Local getLocalForField(KernelToLocalsMap localsMap, FieldEntity field) {
+    _ensureFieldToLocalsMap(localsMap);
+    Local local = _fieldToLocalsMap[field];
+    if (local == null) {
+      failedAt(field, "No local for $field. Options: $_fieldToLocalsMap");
+    }
+    return local;
+  }
+
+  @override
+  FieldEntity get thisFieldEntity => _localToFieldMap[thisLocal];
+
+  @override
+  void forEachFreeVariable(
+      KernelToLocalsMap localsMap, f(Local variable, JField field)) {
+    _ensureFieldToLocalsMap(localsMap);
+    _ensureBoxedVariableCache(localsMap);
+    _fieldToLocalsMap.forEach((JField field, Local local) {
+      f(local, field);
+    });
+    _boxedVariablesCache.forEach(f);
   }
 
   @override
   bool get isClosure => true;
+
+  @override
+  Local getClosureEntity(KernelToLocalsMap localsMap) {
+    return _closureEntityVariable != null
+        ? localsMap.getLocalVariable(_closureEntityVariable)
+        : _closureEntity;
+  }
 }
 
 class JClosureClass extends JClass {
@@ -795,7 +880,7 @@
   final String declaredName;
 
   JClosureField(
-      String name, KernelClosureClassInfo containingClass, String declaredName,
+      String name, JsClosureClassInfo containingClass, String declaredName,
       {bool isConst, bool isAssignable})
       : this.internal(
             containingClass.closureClassEntity.library,
diff --git a/pkg/compiler/lib/src/js_model/element_map.dart b/pkg/compiler/lib/src/js_model/element_map.dart
index 622ee68..4d6febd 100644
--- a/pkg/compiler/lib/src/js_model/element_map.dart
+++ b/pkg/compiler/lib/src/js_model/element_map.dart
@@ -150,8 +150,8 @@
 
   /// Make a record to ensure variables that are are declared in one scope and
   /// modified in another get their values updated correctly.
-  Map<Local, JRecordField> makeRecordContainer(
-      KernelScopeInfo info, MemberEntity member, KernelToLocalsMap localsMap);
+  Map<ir.VariableDeclaration, JRecordField> makeRecordContainer(
+      KernelScopeInfo info, MemberEntity member);
 
   /// Returns a provider for static types for [member].
   StaticTypeProvider getStaticTypeProvider(MemberEntity member);
@@ -217,8 +217,8 @@
   /// Returns the [Local] for [node].
   Local getLocalVariable(ir.VariableDeclaration node);
 
-  Local getLocalTypeVariable(
-      ir.TypeParameterType node, JsToElementMap elementMap);
+  /// Returns the [Local] for the [typeVariable].
+  Local getLocalTypeVariableEntity(TypeVariableEntity typeVariable);
 
   /// Returns the [ir.FunctionNode] that declared [parameter].
   ir.FunctionNode getFunctionNodeForParameter(Local parameter);
diff --git a/pkg/compiler/lib/src/js_model/element_map_impl.dart b/pkg/compiler/lib/src/js_model/element_map_impl.dart
index ec6d67f..1f4cd96 100644
--- a/pkg/compiler/lib/src/js_model/element_map_impl.dart
+++ b/pkg/compiler/lib/src/js_model/element_map_impl.dart
@@ -1725,11 +1725,9 @@
       InterfaceType memberThisType,
       ir.VariableDeclaration variable,
       BoxLocal boxLocal,
-      Map<String, MemberEntity> memberMap,
-      KernelToLocalsMap localsMap) {
-    Local local = localsMap.getLocalVariable(variable);
+      Map<String, MemberEntity> memberMap) {
     JRecordField boxedField =
-        new JRecordField(local.name, boxLocal, isConst: variable.isConst);
+        new JRecordField(variable.name, boxLocal, isConst: variable.isConst);
     members.register(
         boxedField,
         new ClosureFieldData(
@@ -1745,9 +1743,9 @@
   /// are accessed in different scopes. This function creates the container
   /// and returns a map of locals to the corresponding records created.
   @override
-  Map<Local, JRecordField> makeRecordContainer(
-      KernelScopeInfo info, MemberEntity member, KernelToLocalsMap localsMap) {
-    Map<Local, JRecordField> boxedFields = {};
+  Map<ir.VariableDeclaration, JRecordField> makeRecordContainer(
+      KernelScopeInfo info, MemberEntity member) {
+    Map<ir.VariableDeclaration, JRecordField> boxedFields = {};
     if (info.boxedVariables.isNotEmpty) {
       NodeBox box = info.capturedVariablesAccessor;
 
@@ -1768,18 +1766,13 @@
           ? elementEnvironment.getThisType(member.enclosingClass)
           : null;
       for (ir.VariableDeclaration variable in info.boxedVariables) {
-        boxedFields[localsMap.getLocalVariable(variable)] =
-            _constructRecordFieldEntry(
-                memberThisType, variable, boxLocal, memberMap, localsMap);
+        boxedFields[variable] = _constructRecordFieldEntry(
+            memberThisType, variable, boxLocal, memberMap);
       }
     }
     return boxedFields;
   }
 
-  bool _isInRecord(
-          Local local, Map<Local, JRecordField> recordFieldsVisibleInScope) =>
-      recordFieldsVisibleInScope.containsKey(local);
-
   ParameterStructure _getParameterStructureFromFunctionNode(
       ir.FunctionNode node) {
     int requiredPositionalParameters = node.requiredParameterCount;
@@ -1802,13 +1795,12 @@
         typeParameters);
   }
 
-  KernelClosureClassInfo constructClosureClass(
+  JsClosureClassInfo constructClosureClass(
       MemberEntity member,
       ir.FunctionNode node,
       JLibrary enclosingLibrary,
-      Map<Local, JRecordField> recordFieldsVisibleInScope,
+      Map<ir.VariableDeclaration, JRecordField> recordFieldsVisibleInScope,
       KernelScopeInfo info,
-      KernelToLocalsMap localsMap,
       InterfaceType supertype,
       {bool createSignatureMethod}) {
     InterfaceType memberThisType = member.enclosingClass != null
@@ -1839,9 +1831,10 @@
     classes.register(classEntity, closureData, new ClosureClassEnv(memberMap));
 
     Local closureEntity;
+    ir.VariableDeclaration closureEntityNode;
     if (node.parent is ir.FunctionDeclaration) {
       ir.FunctionDeclaration parent = node.parent;
-      closureEntity = localsMap.getLocalVariable(parent.variable);
+      closureEntityNode = parent.variable;
     } else if (node.parent is ir.FunctionExpression) {
       closureEntity = new AnonymousClosureLocal(classEntity);
     }
@@ -1862,20 +1855,17 @@
       index++;
     }
 
-    KernelClosureClassInfo closureClassInfo =
-        new KernelClosureClassInfo.fromScopeInfo(
-            classEntity,
-            node,
-            <Local, JRecordField>{},
-            info,
-            localsMap,
-            closureEntity,
-            info.hasThisLocal
-                ? new ThisLocal(localsMap.currentMember.enclosingClass)
-                : null,
-            this);
+    JsClosureClassInfo closureClassInfo = new JsClosureClassInfo.fromScopeInfo(
+        classEntity,
+        node,
+        <ir.VariableDeclaration, JRecordField>{},
+        info,
+        member.enclosingClass,
+        closureEntity,
+        closureEntityNode,
+        info.hasThisLocal ? new ThisLocal(member.enclosingClass) : null);
     _buildClosureClassFields(closureClassInfo, member, memberThisType, info,
-        localsMap, recordFieldsVisibleInScope, memberMap);
+        recordFieldsVisibleInScope, memberMap);
 
     if (createSignatureMethod) {
       _constructSignatureMethod(closureClassInfo, memberMap, node,
@@ -1898,12 +1888,11 @@
   }
 
   void _buildClosureClassFields(
-      KernelClosureClassInfo closureClassInfo,
+      JsClosureClassInfo closureClassInfo,
       MemberEntity member,
       InterfaceType memberThisType,
       KernelScopeInfo info,
-      KernelToLocalsMap localsMap,
-      Map<Local, JRecordField> recordFieldsVisibleInScope,
+      Map<ir.VariableDeclaration, JRecordField> recordFieldsVisibleInScope,
       Map<String, MemberEntity> memberMap) {
     // TODO(efortuna): Limit field number usage to when we need to distinguish
     // between two variables with the same name from different scopes.
@@ -1917,10 +1906,9 @@
 
     for (ir.Node variable in info.freeVariables) {
       if (variable is ir.VariableDeclaration) {
-        Local capturedLocal = localsMap.getLocalVariable(variable);
-        if (_isInRecord(capturedLocal, recordFieldsVisibleInScope)) {
+        if (recordFieldsVisibleInScope.containsKey(variable)) {
           bool constructedField = _constructClosureFieldForRecord(
-              capturedLocal,
+              variable,
               closureClassInfo,
               memberThisType,
               memberMap,
@@ -1934,15 +1922,17 @@
 
     // Add a field for the captured 'this'.
     if (info.thisUsedAsFreeVariable) {
-      _constructClosureField(
+      closureClassInfo.registerFieldForLocal(
           closureClassInfo.thisLocal,
-          closureClassInfo,
-          memberThisType,
-          memberMap,
-          getClassDefinition(member.enclosingClass).node,
-          true,
-          false,
-          fieldNumber);
+          _constructClosureField(
+              closureClassInfo.thisLocal.name,
+              closureClassInfo,
+              memberThisType,
+              memberMap,
+              getClassDefinition(member.enclosingClass).node,
+              true,
+              false,
+              fieldNumber));
       fieldNumber++;
     }
 
@@ -1950,29 +1940,40 @@
       // Make a corresponding field entity in this closure class for the
       // free variables in the KernelScopeInfo.freeVariable.
       if (variable is ir.VariableDeclaration) {
-        Local capturedLocal = localsMap.getLocalVariable(variable);
-        if (!_isInRecord(capturedLocal, recordFieldsVisibleInScope)) {
-          _constructClosureField(
-              capturedLocal,
-              closureClassInfo,
-              memberThisType,
-              memberMap,
+        if (!recordFieldsVisibleInScope.containsKey(variable)) {
+          closureClassInfo.registerFieldForVariable(
               variable,
-              variable.isConst,
-              false, // Closure field is never assigned (only box fields).
-              fieldNumber);
+              _constructClosureField(
+                  variable.name,
+                  closureClassInfo,
+                  memberThisType,
+                  memberMap,
+                  variable,
+                  variable.isConst,
+                  false, // Closure field is never assigned (only box fields).
+                  fieldNumber));
           fieldNumber++;
         }
       } else if (variable is TypeVariableTypeWithContext) {
-        Local capturedLocal =
-            localsMap.getLocalTypeVariable(variable.type, this);
+        TypeVariableEntity typeVariable =
+            getTypeVariable(variable.type.parameter);
         // We can have distinct TypeVariableTypeWithContexts that have the same
         // local variable but with different nullabilities. We only want to
         // construct a closure field once for each local variable.
-        if (closureClassInfo.localToFieldMap.containsKey(capturedLocal))
+        if (closureClassInfo.hasFieldForTypeVariable(typeVariable)) {
           continue;
-        _constructClosureField(capturedLocal, closureClassInfo, memberThisType,
-            memberMap, variable.type.parameter, true, false, fieldNumber);
+        }
+        closureClassInfo.registerFieldForTypeVariable(
+            typeVariable,
+            _constructClosureField(
+                variable.type.parameter.name,
+                closureClassInfo,
+                memberThisType,
+                memberMap,
+                variable.type.parameter,
+                true,
+                false,
+                fieldNumber));
         fieldNumber++;
       } else {
         throw new UnsupportedError("Unexpected field node type: $variable");
@@ -1988,19 +1989,20 @@
   /// locals they contain may be). Returns `true` if we constructed a new field
   /// in the closure class.
   bool _constructClosureFieldForRecord(
-      Local capturedLocal,
-      KernelClosureClassInfo closureClassInfo,
+      ir.VariableDeclaration capturedLocal,
+      JsClosureClassInfo closureClassInfo,
       InterfaceType memberThisType,
       Map<String, MemberEntity> memberMap,
       ir.TreeNode sourceNode,
-      Map<Local, JRecordField> recordFieldsVisibleInScope,
+      Map<ir.VariableDeclaration, JRecordField> recordFieldsVisibleInScope,
       int fieldNumber) {
     JRecordField recordField = recordFieldsVisibleInScope[capturedLocal];
 
     // Don't construct a new field if the box that holds this local already has
     // a field in the closure class.
-    if (closureClassInfo.localToFieldMap.containsKey(recordField.box)) {
-      closureClassInfo.boxedVariables[capturedLocal] = recordField;
+    if (closureClassInfo.hasFieldForLocal(recordField.box)) {
+      closureClassInfo.registerFieldForBoxedVariable(
+          capturedLocal, recordField);
       return false;
     }
 
@@ -2017,13 +2019,13 @@
                 sourceNode),
             memberThisType));
     memberMap[closureField.name] = closureField;
-    closureClassInfo.localToFieldMap[recordField.box] = closureField;
-    closureClassInfo.boxedVariables[capturedLocal] = recordField;
+    closureClassInfo.registerFieldForLocal(recordField.box, closureField);
+    closureClassInfo.registerFieldForBoxedVariable(capturedLocal, recordField);
     return true;
   }
 
   void _constructSignatureMethod(
-      KernelClosureClassInfo closureClassInfo,
+      JsClosureClassInfo closureClassInfo,
       Map<String, MemberEntity> memberMap,
       ir.FunctionNode closureSourceNode,
       InterfaceType memberThisType,
@@ -2043,21 +2045,18 @@
         closureClassInfo.signatureMethod = signatureMethod;
   }
 
-  void _constructClosureField(
-      Local capturedLocal,
-      KernelClosureClassInfo closureClassInfo,
+  JField _constructClosureField(
+      String name,
+      JsClosureClassInfo closureClassInfo,
       InterfaceType memberThisType,
       Map<String, MemberEntity> memberMap,
       ir.TreeNode sourceNode,
       bool isConst,
       bool isAssignable,
       int fieldNumber) {
-    FieldEntity closureField = new JClosureField(
-        _getClosureVariableName(capturedLocal.name, fieldNumber),
-        closureClassInfo,
-        capturedLocal.name,
-        isConst: isConst,
-        isAssignable: isAssignable);
+    JField closureField = new JClosureField(
+        _getClosureVariableName(name, fieldNumber), closureClassInfo, name,
+        isConst: isConst, isAssignable: isAssignable);
 
     members.register<IndexedField, JFieldData>(
         closureField,
@@ -2068,7 +2067,7 @@
                 sourceNode),
             memberThisType));
     memberMap[closureField.name] = closureField;
-    closureClassInfo.localToFieldMap[capturedLocal] = closureField;
+    return closureField;
   }
 
   // Returns a non-unique name for the given closure element.
diff --git a/pkg/compiler/lib/src/js_model/js_world_builder.dart b/pkg/compiler/lib/src/js_model/js_world_builder.dart
index 07086db..b1a80ce 100644
--- a/pkg/compiler/lib/src/js_model/js_world_builder.dart
+++ b/pkg/compiler/lib/src/js_model/js_world_builder.dart
@@ -34,7 +34,6 @@
 import '../world.dart';
 import 'closure.dart';
 import 'elements.dart';
-import 'element_map.dart';
 import 'element_map_impl.dart';
 import 'js_world.dart';
 import 'locals.dart';
@@ -414,23 +413,21 @@
 
   /// Construct a closure class and set up the necessary class inference
   /// hierarchy.
-  KernelClosureClassInfo buildClosureClass(
+  JsClosureClassInfo buildClosureClass(
       MemberEntity member,
       ir.FunctionNode originalClosureFunctionNode,
       JLibrary enclosingLibrary,
-      Map<Local, JRecordField> boxedVariables,
+      Map<ir.VariableDeclaration, JRecordField> boxedVariables,
       KernelScopeInfo info,
-      KernelToLocalsMap localsMap,
       {bool createSignatureMethod}) {
     ClassEntity superclass = _commonElements.closureClass;
 
-    KernelClosureClassInfo closureClassInfo = _elementMap.constructClosureClass(
+    JsClosureClassInfo closureClassInfo = _elementMap.constructClosureClass(
         member,
         originalClosureFunctionNode,
         enclosingLibrary,
         boxedVariables,
         info,
-        localsMap,
         _dartTypes.interfaceType(superclass, const []),
         createSignatureMethod: createSignatureMethod);
 
diff --git a/pkg/compiler/lib/src/js_model/locals.dart b/pkg/compiler/lib/src/js_model/locals.dart
index c783b25..0e458ed 100644
--- a/pkg/compiler/lib/src/js_model/locals.dart
+++ b/pkg/compiler/lib/src/js_model/locals.dart
@@ -272,12 +272,11 @@
   }
 
   @override
-  Local getLocalTypeVariable(
-      ir.TypeParameterType node, JsToElementMap elementMap) {
+  Local getLocalTypeVariableEntity(TypeVariableEntity typeVariable) {
     // TODO(efortuna, johnniwinther): We're not registering the type variables
     // like we are for the variable declarations. Is that okay or do we need to
     // make TypeVariableLocal a JLocal?
-    return TypeVariableLocal(elementMap.getTypeVariableType(node).element);
+    return TypeVariableLocal(typeVariable);
   }
 
   @override
diff --git a/pkg/compiler/lib/src/serialization/mixins.dart b/pkg/compiler/lib/src/serialization/mixins.dart
index d7d5d87..0b48464 100644
--- a/pkg/compiler/lib/src/serialization/mixins.dart
+++ b/pkg/compiler/lib/src/serialization/mixins.dart
@@ -185,6 +185,20 @@
   }
 
   @override
+  Map<K, V> readTypeVariableMap<K extends IndexedTypeVariable, V>(V f(),
+      {bool emptyAsNull: false}) {
+    int count = readInt();
+    if (count == 0 && emptyAsNull) return null;
+    Map<K, V> map = {};
+    for (int i = 0; i < count; i++) {
+      IndexedTypeVariable node = readTypeVariable();
+      V value = f();
+      map[node] = value;
+    }
+    return map;
+  }
+
+  @override
   List<E> readLocals<E extends Local>({bool emptyAsNull: false}) {
     int count = readInt();
     if (count == 0 && emptyAsNull) return null;
@@ -606,6 +620,21 @@
   }
 
   @override
+  void writeTypeVariableMap<V>(Map<IndexedTypeVariable, V> map, void f(V value),
+      {bool allowNull: false}) {
+    if (map == null) {
+      assert(allowNull);
+      writeInt(0);
+    } else {
+      writeInt(map.length);
+      map.forEach((IndexedTypeVariable key, V value) {
+        writeTypeVariable(key);
+        f(value);
+      });
+    }
+  }
+
+  @override
   void writeList<E>(Iterable<E> values, void f(E value),
       {bool allowNull: false}) {
     if (values == null) {
diff --git a/pkg/compiler/lib/src/serialization/serialization.dart b/pkg/compiler/lib/src/serialization/serialization.dart
index 6cc9a51..14fb2f7 100644
--- a/pkg/compiler/lib/src/serialization/serialization.dart
+++ b/pkg/compiler/lib/src/serialization/serialization.dart
@@ -329,6 +329,15 @@
   /// Writes a reference to the indexed type variable [value] to this data sink.
   void writeTypeVariable(IndexedTypeVariable value);
 
+  /// Writes the [map] from references to indexed type variables to [V] values
+  /// to this data sink, calling [f] to write each value to the data sink. If
+  /// [allowNull] is `true`, [map] is allowed to be `null`.
+  ///
+  /// This is a convenience method to be used together with
+  /// [DataSource.readTypeVariableMap].
+  void writeTypeVariableMap<V>(Map<IndexedTypeVariable, V> map, void f(V value),
+      {bool allowNull: false});
+
   /// Writes a reference to the local [value] to this data sink.
   void writeLocal(Local local);
 
@@ -742,6 +751,15 @@
   /// Reads a reference to an indexed type variable from this data source.
   IndexedTypeVariable readTypeVariable();
 
+  /// Reads a map from indexed type variable to [V] values from this data
+  /// source, calling [f] to read each value from the data source. If
+  /// [emptyAsNull] is `true`, `null` is returned instead of an empty map.
+  ///
+  /// This is a convenience method to be used together with
+  /// [DataSink.writeTypeVariableMap].
+  Map<K, V> readTypeVariableMap<K extends IndexedTypeVariable, V>(V f(),
+      {bool emptyAsNull: false});
+
   /// Reads a reference to a local from this data source.
   Local readLocal();
 
diff --git a/pkg/compiler/lib/src/ssa/builder_kernel.dart b/pkg/compiler/lib/src/ssa/builder_kernel.dart
index 20e835b..f3b714a 100644
--- a/pkg/compiler/lib/src/ssa/builder_kernel.dart
+++ b/pkg/compiler/lib/src/ssa/builder_kernel.dart
@@ -862,7 +862,7 @@
         CapturedScope scopeData =
             _closureDataLookup.getCapturedScope(constructorBody);
         if (scopeData.requiresContextBox) {
-          bodyCallInputs.add(localsHandler.readLocal(scopeData.context));
+          bodyCallInputs.add(localsHandler.readLocal(scopeData.contextBox));
         }
 
         // Pass type arguments.
@@ -1166,7 +1166,7 @@
     var index = 0;
 
     ConstructorEntity element = _elementMap.getConstructor(constructor);
-    ScopeInfo oldScopeInfo = localsHandler.scopeInfo;
+    MemberEntity oldScopeMember = localsHandler.scopeMember;
 
     _inlinedFrom(
         element, _sourceInformationBuilder.buildCall(initializer, initializer),
@@ -1190,13 +1190,12 @@
           constructorData, element.enclosingClass);
 
       // Set the locals handler state as if we were inlining the constructor.
-      ScopeInfo newScopeInfo = _closureDataLookup.getScopeInfo(element);
-      localsHandler.scopeInfo = newScopeInfo;
+      localsHandler.setupScope(element);
       localsHandler.enterScope(_closureDataLookup.getCapturedScope(element),
           _sourceInformationBuilder.buildDeclaration(element));
       _buildInitializers(constructor, constructorData);
     });
-    localsHandler.scopeInfo = oldScopeInfo;
+    localsHandler.setupScope(oldScopeMember);
   }
 
   /// Constructs a special signature function for a closure.
@@ -1391,7 +1390,7 @@
       if (nodeIsConstructorBody &&
           _closureDataLookup
               .getCapturedScope(targetElement)
-              .isBoxedVariable(local)) {
+              .isBoxedVariable(_localsMap, local)) {
         // If local is boxed, then `variable` will be a field inside the box
         // passed as the last parameter, so no need to update our locals
         // handler or check types at this point.
@@ -1430,9 +1429,10 @@
         closedWorld.annotationsData.getParameterCheckPolicy(method).isEmitted) {
       ir.FunctionNode function = getFunctionNode(_elementMap, method);
       for (ir.TypeParameter typeParameter in function.typeParameters) {
-        Local local = _localsMap.getLocalTypeVariable(
-            new ir.TypeParameterType(typeParameter, ir.Nullability.nonNullable),
-            _elementMap);
+        Local local = _localsMap.getLocalTypeVariableEntity(_elementMap
+            .getTypeVariableType(new ir.TypeParameterType(
+                typeParameter, ir.Nullability.nonNullable))
+            .element);
         HInstruction newParameter = localsHandler.directLocals[local];
         DartType bound = _getDartTypeIfValid(typeParameter.bound);
         if (!dartTypes.isTopType(bound)) {
@@ -1629,12 +1629,7 @@
     close(new HGoto(_abstractValueDomain)).addSuccessor(block);
     open(block);
 
-    localsHandler.startFunction(
-        targetElement,
-        _closureDataLookup.getScopeInfo(targetElement),
-        _closureDataLookup.getCapturedScope(targetElement),
-        parameterMap,
-        elidedParameterSet,
+    localsHandler.startFunction(targetElement, parameterMap, elidedParameterSet,
         _sourceInformationBuilder.buildDeclaration(targetElement),
         isGenerativeConstructorBody: targetElement is ConstructorBodyEntity);
 
@@ -5113,8 +5108,8 @@
     _elementEnvironment.forEachInstanceField(closureClassEntity,
         (_, FieldEntity field) {
       if (_fieldAnalysis.getFieldData(field).isElided) return;
-      capturedVariables
-          .add(localsHandler.readLocal(closureInfo.getLocalForField(field)));
+      capturedVariables.add(localsHandler
+          .readLocal(closureInfo.getLocalForField(_localsMap, field)));
     });
 
     AbstractValue type =
@@ -6017,7 +6012,7 @@
         instanceType ?? _elementMap.getMemberThisType(function),
         _nativeData,
         _interceptorData);
-    localsHandler.scopeInfo = _closureDataLookup.getScopeInfo(function);
+    localsHandler.setupScope(function);
 
     CapturedScope scopeData = _closureDataLookup.getCapturedScope(function);
     bool forGenerativeConstructorBody = function is ConstructorBodyEntity;
@@ -6029,8 +6024,8 @@
 
     int argumentIndex = 0;
     if (function.isInstanceMember) {
-      localsHandler.updateLocal(localsHandler.scopeInfo.thisLocal,
-          compiledArguments[argumentIndex++]);
+      localsHandler.updateLocal(
+          localsHandler.thisLocal, compiledArguments[argumentIndex++]);
     }
 
     ir.Member memberContextNode = _elementMap.getMemberContextNode(function);
@@ -6045,7 +6040,8 @@
             local, _defaultValueForParameter(memberContextNode, variable));
         return;
       }
-      if (forGenerativeConstructorBody && scopeData.isBoxedVariable(local)) {
+      if (forGenerativeConstructorBody &&
+          scopeData.isBoxedVariable(_localsMap, local)) {
         // The parameter will be a field in the box passed as the last
         // parameter. So no need to have it.
         hasBox = true;
diff --git a/pkg/compiler/lib/src/ssa/locals_handler.dart b/pkg/compiler/lib/src/ssa/locals_handler.dart
index 3798e19..e1edc64 100644
--- a/pkg/compiler/lib/src/ssa/locals_handler.dart
+++ b/pkg/compiler/lib/src/ssa/locals_handler.dart
@@ -2,6 +2,8 @@
 // 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 'package:compiler/src/js_model/element_map.dart';
+
 import '../closure.dart';
 import '../common.dart';
 import '../elements/entities.dart';
@@ -33,7 +35,11 @@
   Map<Local, HInstruction> directLocals = new Map<Local, HInstruction>();
   Map<Local, FieldEntity> redirectionMapping = new Map<Local, FieldEntity>();
   final KernelSsaGraphBuilder builder;
-  ScopeInfo scopeInfo;
+
+  MemberEntity _scopeInfoMember;
+  ScopeInfo _scopeInfo;
+  KernelToLocalsMap _localsMap;
+
   Map<TypeVariableEntity, TypeVariableLocal> typeVariableLocals =
       new Map<TypeVariableEntity, TypeVariableLocal>();
   final Entity executableContext;
@@ -103,13 +109,33 @@
         memberContext = other.memberContext,
         instanceType = other.instanceType,
         builder = other.builder,
-        scopeInfo = other.scopeInfo,
+        _scopeInfo = other._scopeInfo,
+        _scopeInfoMember = other._scopeInfoMember,
+        _localsMap = other._localsMap,
         _nativeData = other._nativeData,
         _interceptorData = other._interceptorData,
         activationVariables = other.activationVariables,
         cachedTypeOfThis = other.cachedTypeOfThis,
         cachedTypesOfCapturedVariables = other.cachedTypesOfCapturedVariables;
 
+  /// Sets up the scope to use the scope and locals from [member].
+  void setupScope(MemberEntity member) {
+    _scopeInfoMember = member;
+    if (member != null) {
+      _scopeInfo = _closedWorld.closureDataLookup.getScopeInfo(member);
+      _localsMap = _closedWorld.globalLocalsMap.getLocalsMap(member);
+    } else {
+      _scopeInfo = null;
+      _localsMap = null;
+    }
+  }
+
+  /// Returns the member that currently defines the scope as setup in
+  /// [setupScope].
+  MemberEntity get scopeMember => _scopeInfoMember;
+
+  Local get thisLocal => _scopeInfo.thisLocal;
+
   /// Redirects accesses from element [from] to element [to]. The [to] element
   /// must be a boxed variable or a variable that is stored in a closure-field.
   void redirectElement(Local from, FieldEntity to) {
@@ -139,15 +165,15 @@
       // The box is passed as a parameter to a generative constructor body.
       box = inlinedBox ??
           builder.addParameter(
-              closureInfo.context, _abstractValueDomain.nonNullType);
+              closureInfo.contextBox, _abstractValueDomain.nonNullType);
     } else {
       box = createBox(sourceInformation);
     }
     // Add the box to the known locals.
-    directLocals[closureInfo.context] = box;
+    directLocals[closureInfo.contextBox] = box;
     // Make sure that accesses to the boxed locals go into the box. We also
     // need to make sure that parameters are copied into the box if necessary.
-    closureInfo.forEachBoxedVariable((Local from, FieldEntity to) {
+    closureInfo.forEachBoxedVariable(_localsMap, (Local from, FieldEntity to) {
       // The [from] can only be a parameter for function-scopes and not
       // loop scopes.
       JLocal jFrom = from;
@@ -192,19 +218,17 @@
   /// Documentation wanted -- johnniwinther
   ///
   /// Invariant: [function] must be an implementation element.
-  void startFunction(
-      MemberEntity element,
-      ScopeInfo scopeInfo,
-      CapturedScope scopeData,
-      Map<Local, AbstractValue> parameters,
-      Set<Local> elidedParameters,
-      SourceInformation sourceInformation,
+  void startFunction(MemberEntity element, Map<Local, AbstractValue> parameters,
+      Set<Local> elidedParameters, SourceInformation sourceInformation,
       {bool isGenerativeConstructorBody}) {
-    this.scopeInfo = scopeInfo;
+    setupScope(element);
+
+    CapturedScope scopeData =
+        _closedWorld.closureDataLookup.getCapturedScope(element);
 
     parameters.forEach((Local local, AbstractValue typeMask) {
       if (isGenerativeConstructorBody) {
-        if (scopeData.isBoxedVariable(local)) {
+        if (scopeData.isBoxedVariable(_localsMap, local)) {
           // The parameter will be a field in the box passed as the
           // last parameter. So no need to have it.
           return;
@@ -223,12 +247,13 @@
     // When we remove the element model, we can just use the first check
     // (because the underlying elements won't all be *both* ScopeInfos and
     // ClosureRepresentationInfos).
+    ScopeInfo scopeInfo = _scopeInfo;
     if (scopeInfo is ClosureRepresentationInfo && scopeInfo.isClosure) {
       ClosureRepresentationInfo closureData = scopeInfo;
       // If the freeVariableMapping is not empty, then this function was a
       // nested closure that captures variables. Redirect the captured
       // variables to fields in the closure.
-      closureData.forEachFreeVariable((Local from, FieldEntity to) {
+      closureData.forEachFreeVariable(_localsMap, (Local from, FieldEntity to) {
         redirectElement(from, to);
       });
       // Inside closure redirect references to itself to [:this:].
@@ -236,7 +261,7 @@
           new HThis(closureData.thisLocal, _abstractValueDomain.nonNullType);
       builder.graph.thisInstruction = thisInstruction;
       builder.graph.entry.addAtEntry(thisInstruction);
-      updateLocal(closureData.closureEntity, thisInstruction);
+      updateLocal(closureData.getClosureEntity(_localsMap), thisInstruction);
     } else if (element.isInstanceMember) {
       // Once closures have been mapped to classes their instance members might
       // not have any thisElement if the closure was created inside a static
@@ -294,13 +319,13 @@
   bool isAccessedDirectly(Local local) {
     assert(local != null);
     return !redirectionMapping.containsKey(local) &&
-        !scopeInfo.localIsUsedInTryOrSync(local);
+        !_scopeInfo.localIsUsedInTryOrSync(_localsMap, local);
   }
 
   bool isStoredInClosureField(Local local) {
     assert(local != null);
     if (isAccessedDirectly(local)) return false;
-    if (scopeInfo is! ClosureRepresentationInfo) return false;
+    if (_scopeInfo is! ClosureRepresentationInfo) return false;
     FieldEntity redirectTarget = redirectionMapping[local];
     if (redirectTarget == null) return false;
     return redirectTarget is JClosureField;
@@ -313,7 +338,7 @@
   }
 
   bool _isUsedInTryOrGenerator(Local local) {
-    return scopeInfo.localIsUsedInTryOrSync(local);
+    return _scopeInfo.localIsUsedInTryOrSync(_localsMap, local);
   }
 
   /// Returns an [HInstruction] for the given element. If the element is
@@ -341,9 +366,10 @@
       }
       return value;
     } else if (isStoredInClosureField(local)) {
-      ClosureRepresentationInfo closureData = scopeInfo;
+      ClosureRepresentationInfo closureData = _scopeInfo;
       FieldEntity redirect = redirectionMapping[local];
-      HInstruction receiver = readLocal(closureData.closureEntity);
+      HInstruction receiver =
+          readLocal(closureData.getClosureEntity(_localsMap));
       AbstractValue type = local is BoxLocal
           ? _abstractValueDomain.nonNullType
           : getTypeOfCapturedVariable(redirect);
@@ -381,7 +407,7 @@
 
   HInstruction readThis({SourceInformation sourceInformation}) {
     HInstruction res =
-        readLocal(scopeInfo.thisLocal, sourceInformation: sourceInformation);
+        readLocal(_scopeInfo.thisLocal, sourceInformation: sourceInformation);
     if (res.instructionType == null) {
       res.instructionType = getTypeOfThis();
     }
@@ -514,7 +540,7 @@
     savedDirectLocals.forEach((Local local, HInstruction instruction) {
       if (isAccessedDirectly(local)) {
         // We know 'this' cannot be modified.
-        if (local != scopeInfo.thisLocal) {
+        if (local != _scopeInfo.thisLocal) {
           HPhi phi = new HPhi.singleInput(
               local, instruction, _abstractValueDomain.dynamicType);
           loopEntry.addPhi(phi);
@@ -543,8 +569,8 @@
     // In all other cases a new box will be created when entering the body of
     // the next iteration.
     if (loopInfo.hasBoxedLoopVariables) {
-      updateCaptureBox(
-          loopInfo.context, loopInfo.boxedLoopVariables, sourceInformation);
+      updateCaptureBox(loopInfo.contextBox,
+          loopInfo.getBoxedLoopVariables(_localsMap), sourceInformation);
     }
   }
 
@@ -574,7 +600,7 @@
     Map<Local, HInstruction> joinedLocals = new Map<Local, HInstruction>();
     otherLocals.directLocals.forEach((Local local, HInstruction instruction) {
       // We know 'this' cannot be modified.
-      if (local == scopeInfo.thisLocal) {
+      if (local == _scopeInfo.thisLocal) {
         assert(directLocals[local] == instruction);
         joinedLocals[local] = instruction;
       } else {
@@ -607,7 +633,7 @@
     Map<Local, HInstruction> joinedLocals = new Map<Local, HInstruction>();
     HInstruction thisValue = null;
     directLocals.forEach((Local local, HInstruction instruction) {
-      if (local != scopeInfo.thisLocal) {
+      if (local != _scopeInfo.thisLocal) {
         HPhi phi = new HPhi.noInputs(local, _abstractValueDomain.dynamicType);
         joinedLocals[local] = phi;
         joinBlock.addPhi(phi);
@@ -628,13 +654,13 @@
     }
     if (thisValue != null) {
       // If there was a "this" for the scope, add it to the new locals.
-      joinedLocals[scopeInfo.thisLocal] = thisValue;
+      joinedLocals[_scopeInfo.thisLocal] = thisValue;
     }
 
     // Remove locals that are not in all handlers.
     directLocals = new Map<Local, HInstruction>();
     joinedLocals.forEach((Local local, HInstruction instruction) {
-      if (local != scopeInfo.thisLocal &&
+      if (local != _scopeInfo.thisLocal &&
           instruction.inputs.length != localsHandlers.length) {
         joinBlock.removePhi(instruction);
       } else {
@@ -649,7 +675,7 @@
   AbstractValue getTypeOfThis() {
     AbstractValue result = cachedTypeOfThis;
     if (result == null) {
-      ThisLocal local = scopeInfo.thisLocal;
+      ThisLocal local = _scopeInfo.thisLocal;
       ClassEntity cls = local.enclosingClass;
       if (_closedWorld.isUsedAsMixin(cls)) {
         // If the enclosing class is used as a mixin, [:this:] can be
diff --git a/pkg/compiler/test/closure/closure_test.dart b/pkg/compiler/test/closure/closure_test.dart
index 0701d19..f1ed2bd 100644
--- a/pkg/compiler/test/closure/closure_test.dart
+++ b/pkg/compiler/test/closure/closure_test.dart
@@ -158,7 +158,7 @@
     capturedScopeStack =
         capturedScopeStack.prepend(closureDataLookup.getCapturedScope(member));
     if (capturedScope.requiresContextBox) {
-      boxNames[capturedScope.context] = 'box${boxNames.length}';
+      boxNames[capturedScope.contextBox] = 'box${boxNames.length}';
     }
     dump(member);
   }
@@ -174,7 +174,7 @@
     capturedScopeStack = capturedScopeStack
         .prepend(closureDataLookup.getCapturedLoopScope(node));
     if (capturedScope.requiresContextBox) {
-      boxNames[capturedScope.context] = 'box${boxNames.length}';
+      boxNames[capturedScope.contextBox] = 'box${boxNames.length}';
     }
     dump(node);
   }
@@ -199,37 +199,38 @@
     print('object: $object');
     if (object is MemberEntity) {
       print(' capturedScope (${capturedScope.runtimeType})');
-      capturedScope.forEachBoxedVariable((a, b) => print('  boxed: $a->$b'));
+      capturedScope.forEachBoxedVariable(
+          _localsMap, (a, b) => print('  boxed: $a->$b'));
     }
     print(
         ' closureRepresentationInfo (${closureRepresentationInfo.runtimeType})');
-    closureRepresentationInfo
-        ?.forEachFreeVariable((a, b) => print('  free: $a->$b'));
-    closureRepresentationInfo
-        ?.forEachBoxedVariable((a, b) => print('  boxed: $a->$b'));
+    closureRepresentationInfo?.forEachFreeVariable(
+        _localsMap, (a, b) => print('  free: $a->$b'));
+    closureRepresentationInfo?.forEachBoxedVariable(
+        _localsMap, (a, b) => print('  boxed: $a->$b'));
   }
 
   /// Compute a string representation of the data stored for [local] in [info].
   String computeLocalValue(Local local) {
     Features features = new Features();
-    if (scopeInfo.localIsUsedInTryOrSync(local)) {
+    if (scopeInfo.localIsUsedInTryOrSync(_localsMap, local)) {
       features.add('inTry');
       // TODO(johnniwinther,efortuna): Should this be enabled and checked?
       //Expect.isTrue(capturedScope.localIsUsedInTryOrSync(local));
     } else {
       //Expect.isFalse(capturedScope.localIsUsedInTryOrSync(local));
     }
-    if (capturedScope.isBoxedVariable(local)) {
+    if (capturedScope.isBoxedVariable(_localsMap, local)) {
       features.add('boxed');
     }
-    if (capturedScope.context == local) {
+    if (capturedScope.contextBox == local) {
       // TODO(johnniwinther): This shouldn't happen! Remove branch/throw error
       // when we verify it can't happen.
       features.add('error-box');
     }
     if (capturedScope is CapturedLoopScope) {
       CapturedLoopScope loopScope = capturedScope;
-      if (loopScope.boxedLoopVariables.contains(local)) {
+      if (loopScope.getBoxedLoopVariables(_localsMap).contains(local)) {
         features.add('loop');
       }
     }
@@ -240,9 +241,10 @@
   String computeObjectValue(MemberEntity member) {
     Features features = new Features();
 
-    void addLocals(String name, forEach(f(Local local, _))) {
+    void addLocals(
+        String name, forEach(KernelToLocalsMap localsMap, f(Local local, _))) {
       List<String> names = <String>[];
-      forEach((Local local, _) {
+      forEach(_localsMap, (Local local, _) {
         if (local is BoxLocal) {
           names.add(boxNames[local]);
         } else {
@@ -263,7 +265,7 @@
     if (capturedScope.requiresContextBox) {
       var keyword = 'boxed';
       addLocals(keyword, capturedScope.forEachBoxedVariable);
-      features['box'] = '(${boxNames[capturedScope.context]} which holds '
+      features['box'] = '(${boxNames[capturedScope.contextBox]} which holds '
           '${features[keyword]})';
       features.remove(keyword);
     }
@@ -271,12 +273,13 @@
     if (closureRepresentationInfo != null) {
       addLocals('free', closureRepresentationInfo.forEachFreeVariable);
       if (closureRepresentationInfo.closureClassEntity != null) {
-        addLocals('fields', (f(Local local, _)) {
+        addLocals('fields', (KernelToLocalsMap localsMap, f(Local local, _)) {
           _closedWorld.elementEnvironment.forEachInstanceField(
               closureRepresentationInfo.closureClassEntity,
               (_, FieldEntity field) {
             if (_closedWorld.fieldAnalysis.getFieldData(field).isElided) return;
-            f(closureRepresentationInfo.getLocalForField(field), field);
+            f(closureRepresentationInfo.getLocalForField(localsMap, field),
+                field);
           });
         });
       }
diff --git a/tests/web/regress/scope_info_field_loop_test.dart b/tests/web/regress/scope_info_field_loop_test.dart
new file mode 100644
index 0000000..cf4c44b
--- /dev/null
+++ b/tests/web/regress/scope_info_field_loop_test.dart
@@ -0,0 +1,19 @@
+// Copyright (c) 2021, 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.
+
+// Regression test for failure caused by refactoring of handling of scope info
+// locals map in the ssa locals handler.
+
+class Class {
+  static const list = [];
+  var field = {for (final key in list) key: null};
+
+  Class(parameter) {
+    parameter;
+  }
+}
+
+main() {
+  new Class(null);
+}
diff --git a/tests/web_2/regress/scope_info_field_loop_test.dart b/tests/web_2/regress/scope_info_field_loop_test.dart
new file mode 100644
index 0000000..c756dfa
--- /dev/null
+++ b/tests/web_2/regress/scope_info_field_loop_test.dart
@@ -0,0 +1,21 @@
+// Copyright (c) 2021, 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.
+
+// Regression test for failure caused by refactoring of handling of scope info
+// locals map in the ssa locals handler.
+
+// @dart=2.9
+
+class Class {
+  static const list = [];
+  var field = {for (final key in list) key: null};
+
+  Class(parameter) {
+    parameter;
+  }
+}
+
+main() {
+  new Class(null);
+}