[dart2js] Implement Cell lowering for uninitialized locals.

Change-Id: I7233e1484cc19e5f24d215bfd2c1e34c8f12961b
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/193588
Commit-Queue: Mayank Patke <fishythefish@google.com>
Reviewed-by: Stephen Adams <sra@google.com>
Reviewed-by: Johnni Winther <johnniwinther@google.com>
diff --git a/pkg/compiler/lib/src/kernel/dart2js_target.dart b/pkg/compiler/lib/src/kernel/dart2js_target.dart
index 878f2be..d41438e 100644
--- a/pkg/compiler/lib/src/kernel/dart2js_target.dart
+++ b/pkg/compiler/lib/src/kernel/dart2js_target.dart
@@ -62,10 +62,6 @@
 
 /// Late lowerings which the frontend performs for dart2js.
 const List<int> _allEnabledLateLowerings = [
-  LateLowering.nullableUninitializedNonFinalLocal,
-  LateLowering.nonNullableUninitializedNonFinalLocal,
-  LateLowering.nullableUninitializedFinalLocal,
-  LateLowering.nonNullableUninitializedFinalLocal,
   LateLowering.nullableInitializedNonFinalLocal,
   LateLowering.nonNullableInitializedNonFinalLocal,
   LateLowering.nullableInitializedFinalLocal,
@@ -167,8 +163,7 @@
               _nativeClasses)
           .visitLibrary(library);
     }
-    lowering.transformLibraries(
-        libraries, coreTypes, hierarchy, flags.enableNullSafety);
+    lowering.transformLibraries(libraries, coreTypes, hierarchy);
     logger?.call("Lowering transformations performed");
   }
 
diff --git a/pkg/compiler/lib/src/kernel/transformations/late_lowering.dart b/pkg/compiler/lib/src/kernel/transformations/late_lowering.dart
new file mode 100644
index 0000000..cf5c62c
--- /dev/null
+++ b/pkg/compiler/lib/src/kernel/transformations/late_lowering.dart
@@ -0,0 +1,112 @@
+// 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.
+
+import 'package:kernel/ast.dart';
+import 'package:kernel/library_index.dart';
+import 'package:kernel/type_algebra.dart';
+
+bool _shouldLowerVariable(VariableDeclaration node) =>
+    node.initializer == null && node.isLate;
+
+class LateLowering {
+  final Class _cellClass;
+  final Constructor _cellConstructor;
+
+  final Procedure _readLocal;
+  List<TypeParameter> _readLocalTypeParameters;
+  FunctionType _readLocalTypeWithoutTypeParameters;
+
+  final Procedure _setValue;
+  final Procedure _setFinalLocalValue;
+
+  final Map<VariableDeclaration, VariableDeclaration> _cells = {};
+
+  Member _contextMember;
+
+  LateLowering(LibraryIndex index)
+      : _cellClass = index.getClass('dart:_late_helper', '_Cell'),
+        _cellConstructor = index.getMember('dart:_late_helper', '_Cell', ''),
+        _readLocal = index.getMember('dart:_late_helper', '_Cell', 'readLocal'),
+        _setValue = index.getMember('dart:_late_helper', '_Cell', 'set:value'),
+        _setFinalLocalValue = index.getMember(
+            'dart:_late_helper', '_Cell', 'set:finalLocalValue') {
+    FunctionType _readLocalType = _readLocal.getterType;
+    _readLocalTypeParameters = _readLocalType.typeParameters;
+    _readLocalTypeWithoutTypeParameters = _readLocalType.withoutTypeParameters;
+  }
+
+  VariableDeclaration _variableCell(VariableDeclaration variable) {
+    assert(_shouldLowerVariable(variable));
+    return _cells.putIfAbsent(variable, () {
+      int fileOffset = variable.fileOffset;
+      return VariableDeclaration(variable.name,
+          initializer:
+              ConstructorInvocation(_cellConstructor, Arguments.empty())
+                ..fileOffset = fileOffset,
+          type: InterfaceType(
+              _cellClass, _contextMember.enclosingLibrary.nonNullable),
+          isFinal: true)
+        ..fileOffset = fileOffset;
+    });
+  }
+
+  VariableGet _variableCellAccess(
+          VariableDeclaration variable, int fileOffset) =>
+      VariableGet(_variableCell(variable))..fileOffset = fileOffset;
+
+  TreeNode transformVariableDeclaration(
+      VariableDeclaration node, Member contextMember) {
+    _contextMember = contextMember;
+
+    if (!_shouldLowerVariable(node)) return node;
+
+    // A [VariableDeclaration] being used as a statement must be a direct child
+    // of a [Block].
+    if (node.parent is! Block) return node;
+
+    return _variableCell(node);
+  }
+
+  TreeNode transformVariableGet(VariableGet node, Member contextMember) {
+    _contextMember = contextMember;
+
+    VariableDeclaration variable = node.variable;
+    if (!_shouldLowerVariable(variable)) return node;
+
+    int fileOffset = node.fileOffset;
+    VariableGet cell = _variableCellAccess(variable, fileOffset);
+    List<DartType> typeArguments = [node.promotedType ?? variable.type];
+    return InstanceInvocation(
+        InstanceAccessKind.Instance,
+        cell,
+        _readLocal.name,
+        Arguments(const [], types: typeArguments)..fileOffset = fileOffset,
+        interfaceTarget: _readLocal,
+        functionType:
+            Substitution.fromPairs(_readLocalTypeParameters, typeArguments)
+                .substituteType(_readLocalTypeWithoutTypeParameters))
+      ..fileOffset = fileOffset;
+  }
+
+  TreeNode transformVariableSet(VariableSet node, Member contextMember) {
+    _contextMember = contextMember;
+
+    VariableDeclaration variable = node.variable;
+    if (!_shouldLowerVariable(variable)) return node;
+
+    int fileOffset = node.fileOffset;
+    VariableGet cell = _variableCellAccess(variable, fileOffset);
+    if (variable.isFinal) {
+      return InstanceSet(InstanceAccessKind.Instance, cell,
+          _setFinalLocalValue.name, node.value,
+          interfaceTarget: _setFinalLocalValue)
+        ..fileOffset = fileOffset;
+    } else {
+      return InstanceSet(
+          InstanceAccessKind.Instance, cell, _setValue.name, node.value,
+          interfaceTarget: _setValue)
+        ..fileOffset = fileOffset;
+    }
+  }
+}
diff --git a/pkg/compiler/lib/src/kernel/transformations/lowering.dart b/pkg/compiler/lib/src/kernel/transformations/lowering.dart
index 72c4f15..915c5fb 100644
--- a/pkg/compiler/lib/src/kernel/transformations/lowering.dart
+++ b/pkg/compiler/lib/src/kernel/transformations/lowering.dart
@@ -5,52 +5,57 @@
 import 'package:kernel/ast.dart';
 import 'package:kernel/class_hierarchy.dart' show ClassHierarchy;
 import 'package:kernel/core_types.dart' show CoreTypes;
-import 'package:kernel/type_environment.dart'
-    show StaticTypeContext, TypeEnvironment;
 import 'factory_specializer.dart';
+import 'late_lowering.dart';
 
 /// dart2js-specific lowering transformations and optimizations combined into a
 /// single transformation pass.
 ///
 /// Each transformation is applied locally to AST nodes of certain types after
 /// transforming children nodes.
-void transformLibraries(List<Library> libraries, CoreTypes coreTypes,
-    ClassHierarchy hierarchy, bool nullSafety) {
-  final transformer = _Lowering(coreTypes, hierarchy, nullSafety);
+void transformLibraries(
+    List<Library> libraries, CoreTypes coreTypes, ClassHierarchy hierarchy) {
+  final transformer = _Lowering(coreTypes, hierarchy);
   libraries.forEach(transformer.visitLibrary);
 }
 
 class _Lowering extends Transformer {
-  final TypeEnvironment env;
-  final bool nullSafety;
   final FactorySpecializer factorySpecializer;
+  final LateLowering _lateLowering;
 
   Member _currentMember;
-  StaticTypeContext _cachedStaticTypeContext;
 
-  _Lowering(CoreTypes coreTypes, ClassHierarchy hierarchy, this.nullSafety)
-      : env = TypeEnvironment(coreTypes, hierarchy),
-        factorySpecializer = FactorySpecializer(coreTypes, hierarchy);
-
-  // ignore: unused_element
-  StaticTypeContext get _staticTypeContext =>
-      _cachedStaticTypeContext ??= StaticTypeContext(_currentMember, env);
+  _Lowering(CoreTypes coreTypes, ClassHierarchy hierarchy)
+      : factorySpecializer = FactorySpecializer(coreTypes, hierarchy),
+        _lateLowering = LateLowering(coreTypes.index);
 
   @override
-  defaultMember(Member node) {
+  TreeNode defaultMember(Member node) {
     _currentMember = node;
-    _cachedStaticTypeContext = null;
-
-    final result = super.defaultMember(node);
-
-    _currentMember = null;
-    _cachedStaticTypeContext = null;
-    return result;
+    return super.defaultMember(node);
   }
 
   @override
-  visitStaticInvocation(StaticInvocation node) {
+  TreeNode visitStaticInvocation(StaticInvocation node) {
     node.transformChildren(this);
     return factorySpecializer.transformStaticInvocation(node, _currentMember);
   }
+
+  @override
+  TreeNode visitVariableDeclaration(VariableDeclaration node) {
+    node.transformChildren(this);
+    return _lateLowering.transformVariableDeclaration(node, _currentMember);
+  }
+
+  @override
+  TreeNode visitVariableGet(VariableGet node) {
+    node.transformChildren(this);
+    return _lateLowering.transformVariableGet(node, _currentMember);
+  }
+
+  @override
+  TreeNode visitVariableSet(VariableSet node) {
+    node.transformChildren(this);
+    return _lateLowering.transformVariableSet(node, _currentMember);
+  }
 }
diff --git a/pkg/front_end/testcases/dart2js/late_locals.dart.strong.expect b/pkg/front_end/testcases/dart2js/late_locals.dart.strong.expect
index fe43261..e90385e 100644
--- a/pkg/front_end/testcases/dart2js/late_locals.dart.strong.expect
+++ b/pkg/front_end/testcases/dart2js/late_locals.dart.strong.expect
@@ -14,61 +14,39 @@
   self::testNonNullableInitializedFinalLocal();
 }
 static method testNullableUninitializedNonFinalLocal() → void {
-  lowered core::int? #x = _in::createSentinel<core::int?>();
-  function #x#get() → core::int?
-    return let final core::int? #t1 = #x in _in::isSentinel(#t1) ?{core::int?} throw new _in::LateError::localNI("x") : #t1{core::int?};
-  function #x#set(core::int? #t2) → dynamic
-    return #x = #t2;
-  #x#set(42){(core::int?) → dynamic};
-  core::print(#x#get(){() → core::int?});
+  late core::int? x;
+  x = 42;
+  core::print(x{core::int});
 }
 static method testNonNullableUninitializedNonFinalLocal() → void {
-  lowered core::int? #x;
-  function #x#get() → core::int
-    return let final core::int? #t3 = #x in #t3 == null ?{core::int} throw new _in::LateError::localNI("x") : #t3{core::int};
-  function #x#set(core::int #t4) → dynamic
-    return #x = #t4;
-  #x#set(42){(core::int) → dynamic};
-  core::print(#x#get(){() → core::int});
+  late core::int x;
+  x = 42;
+  core::print(x);
 }
 static method testNullableUninitializedFinalLocal() → void {
-  lowered final core::int? #x = _in::createSentinel<core::int?>();
-  function #x#get() → core::int?
-    return let final core::int? #t5 = #x in _in::isSentinel(#t5) ?{core::int?} throw new _in::LateError::localNI("x") : #t5{core::int?};
-  function #x#set(core::int? #t6) → dynamic
-    if(_in::isSentinel(#x))
-      return #x = #t6;
-    else
-      throw new _in::LateError::localAI("x");
-  #x#set(42){(core::int?) → dynamic};
-  core::print(#x#get(){() → core::int?});
+  late final core::int? x;
+  x = 42;
+  core::print(x{core::int});
 }
 static method testNonNullableUninitializedFinalLocal() → void {
-  lowered final core::int? #x;
-  function #x#get() → core::int
-    return let final core::int? #t7 = #x in #t7 == null ?{core::int} throw new _in::LateError::localNI("x") : #t7{core::int};
-  function #x#set(core::int #t8) → dynamic
-    if(#x == null)
-      return #x = #t8;
-    else
-      throw new _in::LateError::localAI("x");
-  #x#set(42){(core::int) → dynamic};
-  core::print(#x#get(){() → core::int});
+  late final core::int x;
+  x = 42;
+  core::print(x);
 }
 static method testNullableInitializedNonFinalLocal() → void {
   lowered core::int? #x = _in::createSentinel<core::int?>();
   function #x#get() → core::int?
-    return let final core::int? #t9 = #x in _in::isSentinel(#t9) ?{core::int?} #x = 1.{core::int::unary-}(){() → core::int} : #t9{core::int?};
-  function #x#set(core::int? #t10) → dynamic
-    return #x = #t10;
+    return let final core::int? #t1 = #x in _in::isSentinel(#t1) ?{core::int?} #x = 1.{core::int::unary-}(){() → core::int} : #t1{core::int?};
+  function #x#set(core::int? #t2) → dynamic
+    return #x = #t2;
   core::print(#x#get(){() → core::int?});
   #x#set(42){(core::int?) → dynamic};
   core::print(#x#get(){() → core::int?});
   lowered core::int? #y = _in::createSentinel<core::int?>();
   function #y#get() → core::int?
-    return let final core::int? #t11 = #y in _in::isSentinel(#t11) ?{core::int?} #y = null : #t11{core::int?};
-  function #y#set(core::int? #t12) → dynamic
-    return #y = #t12;
+    return let final core::int? #t3 = #y in _in::isSentinel(#t3) ?{core::int?} #y = null : #t3{core::int?};
+  function #y#set(core::int? #t4) → dynamic
+    return #y = #t4;
   core::print(#y#get(){() → core::int?});
   #y#set(42){(core::int?) → dynamic};
   core::print(#y#get(){() → core::int?});
@@ -76,9 +54,9 @@
 static method testNonNullableInitializedNonFinalLocal() → void {
   lowered core::int? #x;
   function #x#get() → core::int
-    return let final core::int? #t13 = #x in #t13 == null ?{core::int} #x = 1.{core::int::unary-}(){() → core::int} : #t13{core::int};
-  function #x#set(core::int #t14) → dynamic
-    return #x = #t14;
+    return let final core::int? #t5 = #x in #t5 == null ?{core::int} #x = 1.{core::int::unary-}(){() → core::int} : #t5{core::int};
+  function #x#set(core::int #t6) → dynamic
+    return #x = #t6;
   core::print(#x#get(){() → core::int});
   #x#set(42){(core::int) → dynamic};
   core::print(#x#get(){() → core::int});
@@ -86,16 +64,16 @@
 static method testNullableInitializedFinalLocal() → void {
   lowered final core::int? #x = _in::createSentinel<core::int?>();
   function #x#get() → core::int?
-    return let final core::int? #t15 = #x in _in::isSentinel(#t15) ?{core::int?} let final core::int? #t16 = 1.{core::int::unary-}(){() → core::int} in _in::isSentinel(#x) ?{core::int?} #x = #t16 : throw new _in::LateError::localADI("x") : #t15;
+    return let final core::int? #t7 = #x in _in::isSentinel(#t7) ?{core::int?} let final core::int? #t8 = 1.{core::int::unary-}(){() → core::int} in _in::isSentinel(#x) ?{core::int?} #x = #t8 : throw new _in::LateError::localADI("x") : #t7;
   core::print(#x#get(){() → core::int?});
   lowered final core::int? #y = _in::createSentinel<core::int?>();
   function #y#get() → core::int?
-    return let final core::int? #t17 = #y in _in::isSentinel(#t17) ?{core::int?} let final core::int? #t18 = null in _in::isSentinel(#y) ?{core::int?} #y = #t18 : throw new _in::LateError::localADI("y") : #t17;
+    return let final core::int? #t9 = #y in _in::isSentinel(#t9) ?{core::int?} let final core::int? #t10 = null in _in::isSentinel(#y) ?{core::int?} #y = #t10 : throw new _in::LateError::localADI("y") : #t9;
   core::print(#y#get(){() → core::int?});
 }
 static method testNonNullableInitializedFinalLocal() → void {
   lowered final core::int? #x;
   function #x#get() → core::int
-    return let final core::int? #t19 = #x in #t19 == null ?{core::int} let final core::int #t20 = 1.{core::int::unary-}(){() → core::int} in #x == null ?{core::int} #x = #t20 : throw new _in::LateError::localADI("x") : #t19{core::int};
+    return let final core::int? #t11 = #x in #t11 == null ?{core::int} let final core::int #t12 = 1.{core::int::unary-}(){() → core::int} in #x == null ?{core::int} #x = #t12 : throw new _in::LateError::localADI("x") : #t11{core::int};
   core::print(#x#get(){() → core::int});
 }
diff --git a/pkg/front_end/testcases/dart2js/late_locals.dart.strong.transformed.expect b/pkg/front_end/testcases/dart2js/late_locals.dart.strong.transformed.expect
index ac86677..54ef534 100644
--- a/pkg/front_end/testcases/dart2js/late_locals.dart.strong.transformed.expect
+++ b/pkg/front_end/testcases/dart2js/late_locals.dart.strong.transformed.expect
@@ -1,5 +1,6 @@
 library /*isNonNullableByDefault*/;
 import self as self;
+import "dart:_late_helper" as _la;
 import "dart:core" as core;
 import "dart:_internal" as _in;
 
@@ -14,61 +15,39 @@
   self::testNonNullableInitializedFinalLocal();
 }
 static method testNullableUninitializedNonFinalLocal() → void {
-  lowered core::int? #x = _in::createSentinel<core::int?>();
-  function #x#get() → core::int?
-    return let final core::int? #t1 = #x in _in::isSentinel(#t1) ?{core::int?} throw new _in::LateError::localNI("x") : #t1{core::int?};
-  function #x#set(core::int? #t2) → dynamic
-    return #x = #t2;
-  #x#set(42){(core::int?) → dynamic};
-  core::print(#x#get(){() → core::int?});
+  final _la::_Cell x = new _la::_Cell::•();
+  x.{_la::_Cell::value} = 42;
+  core::print(x.{_la::_Cell::readLocal}<core::int>(){() → core::int});
 }
 static method testNonNullableUninitializedNonFinalLocal() → void {
-  lowered core::int? #x;
-  function #x#get() → core::int
-    return let final core::int? #t3 = #x in #t3 == null ?{core::int} throw new _in::LateError::localNI("x") : #t3{core::int};
-  function #x#set(core::int #t4) → dynamic
-    return #x = #t4;
-  #x#set(42){(core::int) → dynamic};
-  core::print(#x#get(){() → core::int});
+  final _la::_Cell x = new _la::_Cell::•();
+  x.{_la::_Cell::value} = 42;
+  core::print(x.{_la::_Cell::readLocal}<core::int>(){() → core::int});
 }
 static method testNullableUninitializedFinalLocal() → void {
-  lowered final core::int? #x = _in::createSentinel<core::int?>();
-  function #x#get() → core::int?
-    return let final core::int? #t5 = #x in _in::isSentinel(#t5) ?{core::int?} throw new _in::LateError::localNI("x") : #t5{core::int?};
-  function #x#set(core::int? #t6) → dynamic
-    if(_in::isSentinel(#x))
-      return #x = #t6;
-    else
-      throw new _in::LateError::localAI("x");
-  #x#set(42){(core::int?) → dynamic};
-  core::print(#x#get(){() → core::int?});
+  final _la::_Cell x = new _la::_Cell::•();
+  x.{_la::_Cell::finalLocalValue} = 42;
+  core::print(x.{_la::_Cell::readLocal}<core::int>(){() → core::int});
 }
 static method testNonNullableUninitializedFinalLocal() → void {
-  lowered final core::int? #x;
-  function #x#get() → core::int
-    return let final core::int? #t7 = #x in #t7 == null ?{core::int} throw new _in::LateError::localNI("x") : #t7{core::int};
-  function #x#set(core::int #t8) → dynamic
-    if(#x == null)
-      return #x = #t8;
-    else
-      throw new _in::LateError::localAI("x");
-  #x#set(42){(core::int) → dynamic};
-  core::print(#x#get(){() → core::int});
+  final _la::_Cell x = new _la::_Cell::•();
+  x.{_la::_Cell::finalLocalValue} = 42;
+  core::print(x.{_la::_Cell::readLocal}<core::int>(){() → core::int});
 }
 static method testNullableInitializedNonFinalLocal() → void {
   lowered core::int? #x = _in::createSentinel<core::int?>();
   function #x#get() → core::int?
-    return let final core::int? #t9 = #x in _in::isSentinel(#t9) ?{core::int?} #x = 1.{core::int::unary-}(){() → core::int} : #t9{core::int?};
-  function #x#set(core::int? #t10) → dynamic
-    return #x = #t10;
+    return let final core::int? #t1 = #x in _in::isSentinel(#t1) ?{core::int?} #x = 1.{core::int::unary-}(){() → core::int} : #t1{core::int?};
+  function #x#set(core::int? #t2) → dynamic
+    return #x = #t2;
   core::print(#x#get(){() → core::int?});
   #x#set(42){(core::int?) → dynamic};
   core::print(#x#get(){() → core::int?});
   lowered core::int? #y = _in::createSentinel<core::int?>();
   function #y#get() → core::int?
-    return let final core::int? #t11 = #y in _in::isSentinel(#t11) ?{core::int?} #y = null : #t11{core::int?};
-  function #y#set(core::int? #t12) → dynamic
-    return #y = #t12;
+    return let final core::int? #t3 = #y in _in::isSentinel(#t3) ?{core::int?} #y = null : #t3{core::int?};
+  function #y#set(core::int? #t4) → dynamic
+    return #y = #t4;
   core::print(#y#get(){() → core::int?});
   #y#set(42){(core::int?) → dynamic};
   core::print(#y#get(){() → core::int?});
@@ -76,9 +55,9 @@
 static method testNonNullableInitializedNonFinalLocal() → void {
   lowered core::int? #x;
   function #x#get() → core::int
-    return let final core::int? #t13 = #x in #t13 == null ?{core::int} #x = 1.{core::int::unary-}(){() → core::int} : #t13{core::int};
-  function #x#set(core::int #t14) → dynamic
-    return #x = #t14;
+    return let final core::int? #t5 = #x in #t5 == null ?{core::int} #x = 1.{core::int::unary-}(){() → core::int} : #t5{core::int};
+  function #x#set(core::int #t6) → dynamic
+    return #x = #t6;
   core::print(#x#get(){() → core::int});
   #x#set(42){(core::int) → dynamic};
   core::print(#x#get(){() → core::int});
@@ -86,17 +65,17 @@
 static method testNullableInitializedFinalLocal() → void {
   lowered final core::int? #x = _in::createSentinel<core::int?>();
   function #x#get() → core::int?
-    return let final core::int? #t15 = #x in _in::isSentinel(#t15) ?{core::int?} let final core::int? #t16 = 1.{core::int::unary-}(){() → core::int} in _in::isSentinel(#x) ?{core::int?} #x = #t16 : throw new _in::LateError::localADI("x") : #t15;
+    return let final core::int? #t7 = #x in _in::isSentinel(#t7) ?{core::int?} let final core::int? #t8 = 1.{core::int::unary-}(){() → core::int} in _in::isSentinel(#x) ?{core::int?} #x = #t8 : throw new _in::LateError::localADI("x") : #t7;
   core::print(#x#get(){() → core::int?});
   lowered final core::int? #y = _in::createSentinel<core::int?>();
   function #y#get() → core::int?
-    return let final core::int? #t17 = #y in _in::isSentinel(#t17) ?{core::int?} let final core::int? #t18 = null in _in::isSentinel(#y) ?{core::int?} #y = #t18 : throw new _in::LateError::localADI("y") : #t17;
+    return let final core::int? #t9 = #y in _in::isSentinel(#t9) ?{core::int?} let final core::int? #t10 = null in _in::isSentinel(#y) ?{core::int?} #y = #t10 : throw new _in::LateError::localADI("y") : #t9;
   core::print(#y#get(){() → core::int?});
 }
 static method testNonNullableInitializedFinalLocal() → void {
   lowered final core::int? #x;
   function #x#get() → core::int
-    return let final core::int? #t19 = #x in #t19 == null ?{core::int} let final core::int #t20 = 1.{core::int::unary-}(){() → core::int} in #x == null ?{core::int} #x = #t20 : throw new _in::LateError::localADI("x") : #t19{core::int};
+    return let final core::int? #t11 = #x in #t11 == null ?{core::int} let final core::int #t12 = 1.{core::int::unary-}(){() → core::int} in #x == null ?{core::int} #x = #t12 : throw new _in::LateError::localADI("x") : #t11{core::int};
   core::print(#x#get(){() → core::int});
 }
 
@@ -109,4 +88,4 @@
 Evaluated: VariableGet @ org-dartlang-testcase:///late_locals.dart:63:19 -> NullConstant(null)
 Evaluated: InstanceInvocation @ org-dartlang-testcase:///late_locals.dart:68:22 -> DoubleConstant(-1.0)
 Evaluated: VariableGet @ org-dartlang-testcase:///late_locals.dart:68:18 -> DoubleConstant(-1.0)
-Extra constant evaluation: evaluated: 168, effectively constant: 7
+Extra constant evaluation: evaluated: 130, effectively constant: 7
diff --git a/pkg/front_end/testcases/dart2js/late_locals.dart.weak.expect b/pkg/front_end/testcases/dart2js/late_locals.dart.weak.expect
index 26979f0..0e44ebb 100644
--- a/pkg/front_end/testcases/dart2js/late_locals.dart.weak.expect
+++ b/pkg/front_end/testcases/dart2js/late_locals.dart.weak.expect
@@ -14,61 +14,39 @@
   self::testNonNullableInitializedFinalLocal();
 }
 static method testNullableUninitializedNonFinalLocal() → void {
-  lowered core::int? #x = _in::createSentinel<core::int?>();
-  function #x#get() → core::int?
-    return let final core::int? #t1 = #x in _in::isSentinel(#t1) ?{core::int?} throw new _in::LateError::localNI("x") : #t1{core::int?};
-  function #x#set(core::int? #t2) → dynamic
-    return #x = #t2;
-  #x#set(42){(core::int?) → dynamic};
-  core::print(#x#get(){() → core::int?});
+  late core::int? x;
+  x = 42;
+  core::print(x{core::int});
 }
 static method testNonNullableUninitializedNonFinalLocal() → void {
-  lowered core::int? #x = _in::createSentinel<core::int>();
-  function #x#get() → core::int
-    return let final core::int? #t3 = #x in _in::isSentinel(#t3) ?{core::int} throw new _in::LateError::localNI("x") : #t3{core::int};
-  function #x#set(core::int #t4) → dynamic
-    return #x = #t4;
-  #x#set(42){(core::int) → dynamic};
-  core::print(#x#get(){() → core::int});
+  late core::int x;
+  x = 42;
+  core::print(x);
 }
 static method testNullableUninitializedFinalLocal() → void {
-  lowered final core::int? #x = _in::createSentinel<core::int?>();
-  function #x#get() → core::int?
-    return let final core::int? #t5 = #x in _in::isSentinel(#t5) ?{core::int?} throw new _in::LateError::localNI("x") : #t5{core::int?};
-  function #x#set(core::int? #t6) → dynamic
-    if(_in::isSentinel(#x))
-      return #x = #t6;
-    else
-      throw new _in::LateError::localAI("x");
-  #x#set(42){(core::int?) → dynamic};
-  core::print(#x#get(){() → core::int?});
+  late final core::int? x;
+  x = 42;
+  core::print(x{core::int});
 }
 static method testNonNullableUninitializedFinalLocal() → void {
-  lowered final core::int? #x = _in::createSentinel<core::int>();
-  function #x#get() → core::int
-    return let final core::int? #t7 = #x in _in::isSentinel(#t7) ?{core::int} throw new _in::LateError::localNI("x") : #t7{core::int};
-  function #x#set(core::int #t8) → dynamic
-    if(_in::isSentinel(#x))
-      return #x = #t8;
-    else
-      throw new _in::LateError::localAI("x");
-  #x#set(42){(core::int) → dynamic};
-  core::print(#x#get(){() → core::int});
+  late final core::int x;
+  x = 42;
+  core::print(x);
 }
 static method testNullableInitializedNonFinalLocal() → void {
   lowered core::int? #x = _in::createSentinel<core::int?>();
   function #x#get() → core::int?
-    return let final core::int? #t9 = #x in _in::isSentinel(#t9) ?{core::int?} #x = 1.{core::int::unary-}(){() → core::int} : #t9{core::int?};
-  function #x#set(core::int? #t10) → dynamic
-    return #x = #t10;
+    return let final core::int? #t1 = #x in _in::isSentinel(#t1) ?{core::int?} #x = 1.{core::int::unary-}(){() → core::int} : #t1{core::int?};
+  function #x#set(core::int? #t2) → dynamic
+    return #x = #t2;
   core::print(#x#get(){() → core::int?});
   #x#set(42){(core::int?) → dynamic};
   core::print(#x#get(){() → core::int?});
   lowered core::int? #y = _in::createSentinel<core::int?>();
   function #y#get() → core::int?
-    return let final core::int? #t11 = #y in _in::isSentinel(#t11) ?{core::int?} #y = null : #t11{core::int?};
-  function #y#set(core::int? #t12) → dynamic
-    return #y = #t12;
+    return let final core::int? #t3 = #y in _in::isSentinel(#t3) ?{core::int?} #y = null : #t3{core::int?};
+  function #y#set(core::int? #t4) → dynamic
+    return #y = #t4;
   core::print(#y#get(){() → core::int?});
   #y#set(42){(core::int?) → dynamic};
   core::print(#y#get(){() → core::int?});
@@ -76,9 +54,9 @@
 static method testNonNullableInitializedNonFinalLocal() → void {
   lowered core::int? #x = _in::createSentinel<core::int>();
   function #x#get() → core::int
-    return let final core::int? #t13 = #x in _in::isSentinel(#t13) ?{core::int} #x = 1.{core::int::unary-}(){() → core::int} : #t13{core::int};
-  function #x#set(core::int #t14) → dynamic
-    return #x = #t14;
+    return let final core::int? #t5 = #x in _in::isSentinel(#t5) ?{core::int} #x = 1.{core::int::unary-}(){() → core::int} : #t5{core::int};
+  function #x#set(core::int #t6) → dynamic
+    return #x = #t6;
   core::print(#x#get(){() → core::int});
   #x#set(42){(core::int) → dynamic};
   core::print(#x#get(){() → core::int});
@@ -86,16 +64,16 @@
 static method testNullableInitializedFinalLocal() → void {
   lowered final core::int? #x = _in::createSentinel<core::int?>();
   function #x#get() → core::int?
-    return let final core::int? #t15 = #x in _in::isSentinel(#t15) ?{core::int?} let final core::int? #t16 = 1.{core::int::unary-}(){() → core::int} in _in::isSentinel(#x) ?{core::int?} #x = #t16 : throw new _in::LateError::localADI("x") : #t15;
+    return let final core::int? #t7 = #x in _in::isSentinel(#t7) ?{core::int?} let final core::int? #t8 = 1.{core::int::unary-}(){() → core::int} in _in::isSentinel(#x) ?{core::int?} #x = #t8 : throw new _in::LateError::localADI("x") : #t7;
   core::print(#x#get(){() → core::int?});
   lowered final core::int? #y = _in::createSentinel<core::int?>();
   function #y#get() → core::int?
-    return let final core::int? #t17 = #y in _in::isSentinel(#t17) ?{core::int?} let final core::int? #t18 = null in _in::isSentinel(#y) ?{core::int?} #y = #t18 : throw new _in::LateError::localADI("y") : #t17;
+    return let final core::int? #t9 = #y in _in::isSentinel(#t9) ?{core::int?} let final core::int? #t10 = null in _in::isSentinel(#y) ?{core::int?} #y = #t10 : throw new _in::LateError::localADI("y") : #t9;
   core::print(#y#get(){() → core::int?});
 }
 static method testNonNullableInitializedFinalLocal() → void {
   lowered final core::int? #x = _in::createSentinel<core::int>();
   function #x#get() → core::int
-    return let final core::int #t19 = #x in _in::isSentinel(#t19) ?{core::int} let final core::int #t20 = 1.{core::int::unary-}(){() → core::int} in _in::isSentinel(#x) ?{core::int} #x = #t20 : throw new _in::LateError::localADI("x") : #t19;
+    return let final core::int #t11 = #x in _in::isSentinel(#t11) ?{core::int} let final core::int #t12 = 1.{core::int::unary-}(){() → core::int} in _in::isSentinel(#x) ?{core::int} #x = #t12 : throw new _in::LateError::localADI("x") : #t11;
   core::print(#x#get(){() → core::int});
 }
diff --git a/pkg/front_end/testcases/dart2js/late_locals.dart.weak.transformed.expect b/pkg/front_end/testcases/dart2js/late_locals.dart.weak.transformed.expect
index 640d4d4..0d8bf0e 100644
--- a/pkg/front_end/testcases/dart2js/late_locals.dart.weak.transformed.expect
+++ b/pkg/front_end/testcases/dart2js/late_locals.dart.weak.transformed.expect
@@ -1,5 +1,6 @@
 library /*isNonNullableByDefault*/;
 import self as self;
+import "dart:_late_helper" as _la;
 import "dart:core" as core;
 import "dart:_internal" as _in;
 
@@ -14,61 +15,39 @@
   self::testNonNullableInitializedFinalLocal();
 }
 static method testNullableUninitializedNonFinalLocal() → void {
-  lowered core::int? #x = _in::createSentinel<core::int?>();
-  function #x#get() → core::int?
-    return let final core::int? #t1 = #x in _in::isSentinel(#t1) ?{core::int?} throw new _in::LateError::localNI("x") : #t1{core::int?};
-  function #x#set(core::int? #t2) → dynamic
-    return #x = #t2;
-  #x#set(42){(core::int?) → dynamic};
-  core::print(#x#get(){() → core::int?});
+  final _la::_Cell x = new _la::_Cell::•();
+  x.{_la::_Cell::value} = 42;
+  core::print(x.{_la::_Cell::readLocal}<core::int>(){() → core::int});
 }
 static method testNonNullableUninitializedNonFinalLocal() → void {
-  lowered core::int? #x = _in::createSentinel<core::int>();
-  function #x#get() → core::int
-    return let final core::int? #t3 = #x in _in::isSentinel(#t3) ?{core::int} throw new _in::LateError::localNI("x") : #t3{core::int};
-  function #x#set(core::int #t4) → dynamic
-    return #x = #t4;
-  #x#set(42){(core::int) → dynamic};
-  core::print(#x#get(){() → core::int});
+  final _la::_Cell x = new _la::_Cell::•();
+  x.{_la::_Cell::value} = 42;
+  core::print(x.{_la::_Cell::readLocal}<core::int>(){() → core::int});
 }
 static method testNullableUninitializedFinalLocal() → void {
-  lowered final core::int? #x = _in::createSentinel<core::int?>();
-  function #x#get() → core::int?
-    return let final core::int? #t5 = #x in _in::isSentinel(#t5) ?{core::int?} throw new _in::LateError::localNI("x") : #t5{core::int?};
-  function #x#set(core::int? #t6) → dynamic
-    if(_in::isSentinel(#x))
-      return #x = #t6;
-    else
-      throw new _in::LateError::localAI("x");
-  #x#set(42){(core::int?) → dynamic};
-  core::print(#x#get(){() → core::int?});
+  final _la::_Cell x = new _la::_Cell::•();
+  x.{_la::_Cell::finalLocalValue} = 42;
+  core::print(x.{_la::_Cell::readLocal}<core::int>(){() → core::int});
 }
 static method testNonNullableUninitializedFinalLocal() → void {
-  lowered final core::int? #x = _in::createSentinel<core::int>();
-  function #x#get() → core::int
-    return let final core::int? #t7 = #x in _in::isSentinel(#t7) ?{core::int} throw new _in::LateError::localNI("x") : #t7{core::int};
-  function #x#set(core::int #t8) → dynamic
-    if(_in::isSentinel(#x))
-      return #x = #t8;
-    else
-      throw new _in::LateError::localAI("x");
-  #x#set(42){(core::int) → dynamic};
-  core::print(#x#get(){() → core::int});
+  final _la::_Cell x = new _la::_Cell::•();
+  x.{_la::_Cell::finalLocalValue} = 42;
+  core::print(x.{_la::_Cell::readLocal}<core::int>(){() → core::int});
 }
 static method testNullableInitializedNonFinalLocal() → void {
   lowered core::int? #x = _in::createSentinel<core::int?>();
   function #x#get() → core::int?
-    return let final core::int? #t9 = #x in _in::isSentinel(#t9) ?{core::int?} #x = 1.{core::int::unary-}(){() → core::int} : #t9{core::int?};
-  function #x#set(core::int? #t10) → dynamic
-    return #x = #t10;
+    return let final core::int? #t1 = #x in _in::isSentinel(#t1) ?{core::int?} #x = 1.{core::int::unary-}(){() → core::int} : #t1{core::int?};
+  function #x#set(core::int? #t2) → dynamic
+    return #x = #t2;
   core::print(#x#get(){() → core::int?});
   #x#set(42){(core::int?) → dynamic};
   core::print(#x#get(){() → core::int?});
   lowered core::int? #y = _in::createSentinel<core::int?>();
   function #y#get() → core::int?
-    return let final core::int? #t11 = #y in _in::isSentinel(#t11) ?{core::int?} #y = null : #t11{core::int?};
-  function #y#set(core::int? #t12) → dynamic
-    return #y = #t12;
+    return let final core::int? #t3 = #y in _in::isSentinel(#t3) ?{core::int?} #y = null : #t3{core::int?};
+  function #y#set(core::int? #t4) → dynamic
+    return #y = #t4;
   core::print(#y#get(){() → core::int?});
   #y#set(42){(core::int?) → dynamic};
   core::print(#y#get(){() → core::int?});
@@ -76,9 +55,9 @@
 static method testNonNullableInitializedNonFinalLocal() → void {
   lowered core::int? #x = _in::createSentinel<core::int>();
   function #x#get() → core::int
-    return let final core::int? #t13 = #x in _in::isSentinel(#t13) ?{core::int} #x = 1.{core::int::unary-}(){() → core::int} : #t13{core::int};
-  function #x#set(core::int #t14) → dynamic
-    return #x = #t14;
+    return let final core::int? #t5 = #x in _in::isSentinel(#t5) ?{core::int} #x = 1.{core::int::unary-}(){() → core::int} : #t5{core::int};
+  function #x#set(core::int #t6) → dynamic
+    return #x = #t6;
   core::print(#x#get(){() → core::int});
   #x#set(42){(core::int) → dynamic};
   core::print(#x#get(){() → core::int});
@@ -86,17 +65,17 @@
 static method testNullableInitializedFinalLocal() → void {
   lowered final core::int? #x = _in::createSentinel<core::int?>();
   function #x#get() → core::int?
-    return let final core::int? #t15 = #x in _in::isSentinel(#t15) ?{core::int?} let final core::int? #t16 = 1.{core::int::unary-}(){() → core::int} in _in::isSentinel(#x) ?{core::int?} #x = #t16 : throw new _in::LateError::localADI("x") : #t15;
+    return let final core::int? #t7 = #x in _in::isSentinel(#t7) ?{core::int?} let final core::int? #t8 = 1.{core::int::unary-}(){() → core::int} in _in::isSentinel(#x) ?{core::int?} #x = #t8 : throw new _in::LateError::localADI("x") : #t7;
   core::print(#x#get(){() → core::int?});
   lowered final core::int? #y = _in::createSentinel<core::int?>();
   function #y#get() → core::int?
-    return let final core::int? #t17 = #y in _in::isSentinel(#t17) ?{core::int?} let final core::int? #t18 = null in _in::isSentinel(#y) ?{core::int?} #y = #t18 : throw new _in::LateError::localADI("y") : #t17;
+    return let final core::int? #t9 = #y in _in::isSentinel(#t9) ?{core::int?} let final core::int? #t10 = null in _in::isSentinel(#y) ?{core::int?} #y = #t10 : throw new _in::LateError::localADI("y") : #t9;
   core::print(#y#get(){() → core::int?});
 }
 static method testNonNullableInitializedFinalLocal() → void {
   lowered final core::int? #x = _in::createSentinel<core::int>();
   function #x#get() → core::int
-    return let final core::int #t19 = #x in _in::isSentinel(#t19) ?{core::int} let final core::int #t20 = 1.{core::int::unary-}(){() → core::int} in _in::isSentinel(#x) ?{core::int} #x = #t20 : throw new _in::LateError::localADI("x") : #t19;
+    return let final core::int #t11 = #x in _in::isSentinel(#t11) ?{core::int} let final core::int #t12 = 1.{core::int::unary-}(){() → core::int} in _in::isSentinel(#x) ?{core::int} #x = #t12 : throw new _in::LateError::localADI("x") : #t11;
   core::print(#x#get(){() → core::int});
 }
 
@@ -109,4 +88,4 @@
 Evaluated: VariableGet @ org-dartlang-testcase:///late_locals.dart:63:19 -> NullConstant(null)
 Evaluated: InstanceInvocation @ org-dartlang-testcase:///late_locals.dart:68:22 -> DoubleConstant(-1.0)
 Evaluated: VariableGet @ org-dartlang-testcase:///late_locals.dart:68:18 -> DoubleConstant(-1.0)
-Extra constant evaluation: evaluated: 172, effectively constant: 7
+Extra constant evaluation: evaluated: 132, effectively constant: 7
diff --git a/tests/language/nnbd/late/chained_assignment_test.dart b/tests/language/nnbd/late/chained_assignment_test.dart
new file mode 100644
index 0000000..1ca1c65
--- /dev/null
+++ b/tests/language/nnbd/late/chained_assignment_test.dart
@@ -0,0 +1,104 @@
+// 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.
+
+import 'package:expect/expect.dart';
+
+class Foo {
+  int x = 0;
+
+  int _y = 0;
+  int get y => _y;
+  void set y(int y) => _y = y;
+
+  static late int z;
+  late int w;
+}
+
+void main() {
+  final foo = Foo();
+  int a = 0;
+  late int b;
+  late int c;
+  late int d;
+
+  b = a = 1;
+  Expect.equals(a, 1);
+  Expect.equals(b, 1);
+
+  a = b = 2;
+  Expect.equals(a, 2);
+  Expect.equals(b, 2);
+
+  b = foo.x = 3;
+  Expect.equals(foo.x, 3);
+  Expect.equals(b, 3);
+
+  foo.x = b = 4;
+  Expect.equals(foo.x, 4);
+  Expect.equals(b, 4);
+
+  b = foo.y = 5;
+  Expect.equals(foo.y, 5);
+  Expect.equals(b, 5);
+
+  foo.y = b = 6;
+  Expect.equals(foo.y, 6);
+  Expect.equals(b, 6);
+
+  b = c = 7;
+  Expect.equals(b, 7);
+  Expect.equals(c, 7);
+
+  d = b = 8;
+  Expect.equals(b, 8);
+  Expect.equals(d, 8);
+
+  Foo.z = a = 9;
+  Expect.equals(a, 9);
+  Expect.equals(Foo.z, 9);
+
+  a = Foo.z = 10;
+  Expect.equals(a, 10);
+  Expect.equals(Foo.z, 10);
+
+  Foo.z = foo.x = 11;
+  Expect.equals(foo.x, 11);
+  Expect.equals(Foo.z, 11);
+
+  foo.x = Foo.z = 12;
+  Expect.equals(foo.x, 12);
+  Expect.equals(Foo.z, 12);
+
+  Foo.z = foo.y = 13;
+  Expect.equals(foo.y, 13);
+  Expect.equals(Foo.z, 13);
+
+  foo.y = Foo.z = 14;
+  Expect.equals(foo.y, 14);
+  Expect.equals(Foo.z, 14);
+
+  foo.w = a = 15;
+  Expect.equals(a, 15);
+  Expect.equals(foo.w, 15);
+
+  a = foo.w = 16;
+  Expect.equals(a, 16);
+  Expect.equals(foo.w, 16);
+
+  foo.w = foo.x = 17;
+  Expect.equals(foo.x, 17);
+  Expect.equals(foo.w, 17);
+
+  foo.x = foo.w = 18;
+  Expect.equals(foo.x, 18);
+  Expect.equals(foo.w, 18);
+
+  foo.w = foo.y = 19;
+  Expect.equals(foo.y, 19);
+  Expect.equals(foo.w, 19);
+
+  foo.y = foo.w = 20;
+  Expect.equals(foo.y, 20);
+  Expect.equals(foo.w, 20);
+}