Version 2.12.0-165.0.dev

Merge commit 'f1e8b702689cb40e4b98749b6602087db9ef76b1' into 'dev'
diff --git a/pkg/_fe_analyzer_shared/lib/src/flow_analysis/flow_analysis.dart b/pkg/_fe_analyzer_shared/lib/src/flow_analysis/flow_analysis.dart
index 6239f0b..818914f 100644
--- a/pkg/_fe_analyzer_shared/lib/src/flow_analysis/flow_analysis.dart
+++ b/pkg/_fe_analyzer_shared/lib/src/flow_analysis/flow_analysis.dart
@@ -4,6 +4,16 @@
 
 import 'package:meta/meta.dart';
 
+/// Set this boolean to `true` to permanently enable the feature of allowing
+/// local boolean variables to influence promotion (see
+/// https://github.com/dart-lang/language/issues/1274).  While this boolean is
+/// `false`, the feature remains experimental and can be activated via an
+/// optional boolean parameter to the [FlowAnalysis] constructor.
+///
+/// Changing this value to `true` will cause some dead code warnings to appear
+/// for code that only exists to support the old behavior.
+const bool allowLocalBooleanVarsToPromoteByDefault = false;
+
 /// [AssignedVariables] is a helper class capable of computing the set of
 /// variables that are potentially written to, and potentially captured by
 /// closures, at various locations inside the code being analyzed.  This class
@@ -283,15 +293,20 @@
 
   ExpressionInfo(this.after, this.ifTrue, this.ifFalse);
 
+  /// Computes a new [ExpressionInfo] based on this one, but with the roles of
+  /// [ifTrue] and [ifFalse] reversed.
+  ExpressionInfo<Variable, Type> invert() =>
+      new ExpressionInfo<Variable, Type>(after, ifFalse, ifTrue);
+
+  ExpressionInfo<Variable, Type> rebaseForward(
+          TypeOperations<Variable, Type> typeOperations,
+          FlowModel<Variable, Type> base) =>
+      new ExpressionInfo(base, ifTrue.rebaseForward(typeOperations, base),
+          ifFalse.rebaseForward(typeOperations, base));
+
   @override
   String toString() =>
       'ExpressionInfo(after: $after, _ifTrue: $ifTrue, ifFalse: $ifFalse)';
-
-  /// Compute a new [ExpressionInfo] based on this one, but with the roles of
-  /// [ifTrue] and [ifFalse] reversed.
-  static ExpressionInfo<Variable, Type> invert<Variable extends Object,
-          Type extends Object>(ExpressionInfo<Variable, Type> info) =>
-      new ExpressionInfo<Variable, Type>(info.after, info.ifFalse, info.ifTrue);
 }
 
 /// Implementation of flow analysis to be shared between the analyzer and the
@@ -303,8 +318,10 @@
 abstract class FlowAnalysis<Node extends Object, Statement extends Node,
     Expression extends Object, Variable extends Object, Type extends Object> {
   factory FlowAnalysis(TypeOperations<Variable, Type> typeOperations,
-      AssignedVariables<Node, Variable> assignedVariables) {
-    return new _FlowAnalysisImpl(typeOperations, assignedVariables);
+      AssignedVariables<Node, Variable> assignedVariables,
+      {bool allowLocalBooleanVarsToPromote = false}) {
+    return new _FlowAnalysisImpl(typeOperations, assignedVariables,
+        allowLocalBooleanVarsToPromote: allowLocalBooleanVarsToPromote);
   }
 
   /// Return `true` if the current state is reachable.
@@ -397,6 +414,12 @@
   /// or `!=` expression.
   void equalityOp_rightBegin(Expression leftOperand, Type leftOperandType);
 
+  /// Retrieves the [ExpressionInfo] associated with [target], if known.  Will
+  /// return `null` if (a) no info is associated with [target], or (b) another
+  /// expression with info has been visited more recently than [target].  For
+  /// testing only.
+  ExpressionInfo<Variable, Type>? expressionInfoForTesting(Expression target);
+
   /// This method should be called at the conclusion of flow analysis for a top
   /// level function or method.  Performs assertion checks.
   void finish();
@@ -555,6 +578,11 @@
   /// [condition] should be the if statement's condition.
   void ifStatement_thenBegin(Expression condition);
 
+  /// Call this method after visiting the initializer of a variable declaration.
+  void initialize(
+      Variable variable, Type initializerType, Expression initializerExpression,
+      {required bool isFinal, required bool isLate});
+
   /// Return whether the [variable] is definitely assigned in the current state.
   bool isAssigned(Variable variable);
 
@@ -655,7 +683,7 @@
   /// is not associated with an SSA node because it is write captured.  For
   /// testing only.
   @visibleForTesting
-  SsaNode? ssaNodeForTesting(Variable variable);
+  SsaNode<Variable, Type>? ssaNodeForTesting(Variable variable);
 
   /// Call this method just before visiting one of the cases in the body of a
   /// switch statement.  See [switchStatement_expressionEnd] for details.
@@ -793,12 +821,16 @@
 
   /// Register write of the given [variable] in the current state.
   /// [writtenType] should be the type of the value that was written.
+  /// [writtenExpression] should be the expression that was written, or `null`
+  /// if the expression that was written is not directly represented in the
+  /// source code (this happens, for example, with compound assignments and with
+  /// for-each loops).
   ///
   /// This should also be used for the implicit write to a non-final variable in
   /// its initializer, to ensure that the type is promoted to non-nullable if
   /// necessary; in this case, [viaInitializer] should be `true`.
-  void write(Variable variable, Type writtenType,
-      {bool viaInitializer = false});
+  void write(
+      Variable variable, Type writtenType, Expression? writtenExpression);
 }
 
 /// Alternate implementation of [FlowAnalysis] that prints out inputs and output
@@ -811,10 +843,12 @@
   bool _exceptionOccurred = false;
 
   factory FlowAnalysisDebug(TypeOperations<Variable, Type> typeOperations,
-      AssignedVariables<Node, Variable> assignedVariables) {
+      AssignedVariables<Node, Variable> assignedVariables,
+      {bool allowLocalBooleanVarsToPromote = false}) {
     print('FlowAnalysisDebug()');
-    return new FlowAnalysisDebug._(
-        new _FlowAnalysisImpl(typeOperations, assignedVariables));
+    return new FlowAnalysisDebug._(new _FlowAnalysisImpl(
+        typeOperations, assignedVariables,
+        allowLocalBooleanVarsToPromote: allowLocalBooleanVarsToPromote));
   }
 
   FlowAnalysisDebug._(this._wrapped);
@@ -919,6 +953,13 @@
   }
 
   @override
+  ExpressionInfo<Variable, Type>? expressionInfoForTesting(Expression target) {
+    return _wrap('expressionInfoForTesting($target)',
+        () => _wrapped.expressionInfoForTesting(target),
+        isQuery: true);
+  }
+
+  @override
   void finish() {
     if (_exceptionOccurred) {
       _wrap('finish() (skipped)', () {}, isPure: true);
@@ -1030,6 +1071,18 @@
   }
 
   @override
+  void initialize(
+      Variable variable, Type initializerType, Expression initializerExpression,
+      {required bool isFinal, required bool isLate}) {
+    _wrap(
+        'initialize($variable, $initializerType, $initializerExpression, '
+        'isFinal: $isFinal, isLate: $isLate)',
+        () => _wrapped.initialize(
+            variable, initializerType, initializerExpression,
+            isFinal: isFinal, isLate: isLate));
+  }
+
+  @override
   bool isAssigned(Variable variable) {
     return _wrap('isAssigned($variable)', () => _wrapped.isAssigned(variable),
         isQuery: true);
@@ -1145,7 +1198,7 @@
   }
 
   @override
-  SsaNode? ssaNodeForTesting(Variable variable) {
+  SsaNode<Variable, Type>? ssaNodeForTesting(Variable variable) {
     return _wrap('ssaNodeForTesting($variable)',
         () => _wrapped.ssaNodeForTesting(variable),
         isQuery: true);
@@ -1246,12 +1299,10 @@
   }
 
   @override
-  void write(Variable variable, Type writtenType,
-      {bool viaInitializer = false}) {
-    _wrap(
-        'write($variable, $writtenType, viaInitializer: $viaInitializer)',
-        () => _wrapped.write(variable, writtenType,
-            viaInitializer: viaInitializer));
+  void write(
+      Variable variable, Type writtenType, Expression? writtenExpression) {
+    _wrap('write($variable, $writtenType, $writtenExpression)',
+        () => _wrapped.write(variable, writtenType, writtenExpression));
   }
 
   T _wrap<T>(String description, T callback(),
@@ -1325,6 +1376,120 @@
     }());
   }
 
+  /// Computes the effect of executing a try/finally's `try` and `finally`
+  /// blocks in sequence.  `this` is the flow analysis state from the end of the
+  /// `try` block; [beforeFinally] and [afterFinally] are the flow analysis
+  /// states from the top and bottom of the `finally` block, respectively.
+  ///
+  /// Initially the `finally` block is analyzed under the conservative
+  /// assumption that the `try` block might have been interrupted at any point
+  /// by an exception occurring, therefore no variable assignments or promotions
+  /// that occurred in the `try` block can be relied upon.  As a result, when we
+  /// get to the end of processing the `finally` block, the only promotions and
+  /// variable assignments accounted for by flow analysis are the ones performed
+  /// within the `finally` block itself.  However, when we analyze code that
+  /// follows the `finally` block, we know that the `try` block did *not* throw
+  /// an exception, so we want to reinstate the results of any promotions and
+  /// assignments that occurred during the `try` block, to the extent that they
+  /// weren't invalidated by later assignments in the `finally` block.
+  FlowModel<Variable, Type> attachFinally(
+      TypeOperations<Variable, Type> typeOperations,
+      FlowModel<Variable, Type> beforeFinally,
+      FlowModel<Variable, Type> afterFinally) {
+    // Code that follows the `try/finally` is reachable iff the end of the `try`
+    // block is reachable _and_ the end of the `finally` block is reachable.
+    Reachability newReachable = afterFinally.reachable.rebaseForward(reachable);
+
+    // Consider each variable that is common to all three models.
+    Map<Variable, VariableModel<Variable, Type>> newVariableInfo =
+        <Variable, VariableModel<Variable, Type>>{};
+    bool variableInfoMatchesThis = true;
+    bool variableInfoMatchesAfterFinally = true;
+    for (MapEntry<Variable, VariableModel<Variable, Type>> entry
+        in variableInfo.entries) {
+      Variable variable = entry.key;
+      VariableModel<Variable, Type> thisModel = entry.value;
+      VariableModel<Variable, Type>? beforeFinallyModel =
+          beforeFinally.variableInfo[variable];
+      VariableModel<Variable, Type>? afterFinallyModel =
+          afterFinally.variableInfo[variable];
+      if (beforeFinallyModel == null || afterFinallyModel == null) {
+        // The variable is in `this` model but not in one of the `finally`
+        // models.  This happens when the variable is declared inside the `try`
+        // block.  We can just drop the variable because it won't be in scope
+        // after the try/finally statement.
+        variableInfoMatchesThis = false;
+        continue;
+      }
+      // We can just use the "write captured" state from the `finally` block,
+      // because any write captures in the `try` block are conservatively
+      // considered to take effect in the `finally` block too.
+      List<Type>? newPromotedTypes;
+      SsaNode<Variable, Type>? newSsaNode;
+      if (beforeFinallyModel.ssaNode == afterFinallyModel.ssaNode) {
+        // The finally clause doesn't write to the variable, so we want to keep
+        // all promotions that were done to it in both the try and finally
+        // blocks.
+        newPromotedTypes = VariableModel.rebasePromotedTypes(typeOperations,
+            thisModel.promotedTypes, afterFinallyModel.promotedTypes);
+        // And we can safely restore the SSA node from the end of the try block.
+        newSsaNode = thisModel.ssaNode;
+      } else {
+        // A write to the variable occurred in the finally block, so promotions
+        // from the try block aren't necessarily valid.
+        newPromotedTypes = afterFinallyModel.promotedTypes;
+        // And we can't safely restore the SSA node from the end of the try
+        // block; we need to keep the one from the end of the finally block.
+        newSsaNode = afterFinallyModel.ssaNode;
+      }
+      // The `finally` block inherited all tests from the `try` block so we can
+      // just inherit tests from it.
+      List<Type> newTested = afterFinallyModel.tested;
+      // The variable is definitely assigned if it was definitely assigned in
+      // either the `try` or the `finally` block.
+      bool newAssigned = thisModel.assigned || afterFinallyModel.assigned;
+      // The `finally` block inherited the "unassigned" state from the `try`
+      // block so we can just inherit from it.
+      bool newUnassigned = afterFinallyModel.unassigned;
+      VariableModel<Variable, Type> newModel = VariableModel._identicalOrNew(
+          thisModel,
+          afterFinallyModel,
+          newPromotedTypes,
+          newTested,
+          newAssigned,
+          newUnassigned,
+          newSsaNode);
+      newVariableInfo[variable] = newModel;
+      if (!identical(newModel, thisModel)) variableInfoMatchesThis = false;
+      if (!identical(newModel, afterFinallyModel)) {
+        variableInfoMatchesAfterFinally = false;
+      }
+    }
+    // newVariableInfo is now correct.  However, if there are any variables
+    // present in `afterFinally` that aren't present in `this`, we may
+    // erroneously think that `newVariableInfo` matches `afterFinally`.  If so,
+    // correct that.
+    if (variableInfoMatchesAfterFinally) {
+      for (Variable variable in afterFinally.variableInfo.keys) {
+        if (!variableInfo.containsKey(variable)) {
+          variableInfoMatchesAfterFinally = false;
+          break;
+        }
+      }
+    }
+    assert(variableInfoMatchesThis ==
+        _variableInfosEqual(newVariableInfo, variableInfo));
+    assert(variableInfoMatchesAfterFinally ==
+        _variableInfosEqual(newVariableInfo, afterFinally.variableInfo));
+    if (variableInfoMatchesThis) {
+      newVariableInfo = variableInfo;
+    } else if (variableInfoMatchesAfterFinally) {
+      newVariableInfo = afterFinally.variableInfo;
+    }
+
+    return _identicalOrNew(this, afterFinally, newReachable, newVariableInfo);
+  }
+
   /// Updates the state to indicate that the given [writtenVariables] are no
   /// longer promoted and are no longer definitely unassigned, and the given
   /// [capturedVariables] have been captured by closures.
@@ -1367,7 +1532,11 @@
         (newVariableInfo ??=
             new Map<Variable, VariableModel<Variable, Type>>.from(
                 variableInfo))[variable] = new VariableModel<Variable, Type>(
-            null, const [], false, false, null);
+            promotedTypes: null,
+            tested: const [],
+            assigned: false,
+            unassigned: false,
+            ssaNode: null);
       } else if (!info.writeCaptured) {
         (newVariableInfo ??=
             new Map<Variable, VariableModel<Variable, Type>>.from(
@@ -1433,6 +1602,106 @@
     }
   }
 
+  /// Updates `this` flow model to account for any promotions and assignments
+  /// present in [base].
+  ///
+  /// This is called "rebasing" the flow model by analogy to "git rebase"; in
+  /// effect, it rewinds any flow analysis state present in `this` but not in
+  /// the history of [base], and then reapplies that state using [base] as a
+  /// starting point, to the extent possible without creating unsoundness.  For
+  /// example, if a variable is promoted in `this` but not in [base], then it
+  /// will be promoted in the output model, provided that hasn't been reassigned
+  /// since then (which would make the promotion unsound).
+  FlowModel<Variable, Type> rebaseForward(
+      TypeOperations<Variable, Type> typeOperations,
+      FlowModel<Variable, Type> base) {
+    // The rebased model is reachable iff both `this` and the new base are
+    // reachable.
+    Reachability newReachable = reachable.rebaseForward(base.reachable);
+
+    // Consider each variable in the new base model.
+    Map<Variable, VariableModel<Variable, Type>> newVariableInfo =
+        <Variable, VariableModel<Variable, Type>>{};
+    bool variableInfoMatchesThis = true;
+    bool variableInfoMatchesBase = true;
+    for (MapEntry<Variable, VariableModel<Variable, Type>> entry
+        in base.variableInfo.entries) {
+      Variable variable = entry.key;
+      VariableModel<Variable, Type> baseModel = entry.value;
+      VariableModel<Variable, Type>? thisModel = variableInfo[variable];
+      if (thisModel == null) {
+        // The variable has newly came into scope since `thisModel`, so the
+        // information in `baseModel` is up to date.
+        newVariableInfo[variable] = baseModel;
+        variableInfoMatchesThis = false;
+        continue;
+      }
+      // If the variable was write captured in either `this` or the new base,
+      // it's captured now.
+      bool newWriteCaptured =
+          thisModel.writeCaptured || baseModel.writeCaptured;
+      List<Type>? newPromotedTypes;
+      if (newWriteCaptured) {
+        // Write captured variables can't be promoted.
+        newPromotedTypes = null;
+      } else if (baseModel.ssaNode != thisModel.ssaNode) {
+        // The variable may have been written to since `thisModel`, so we can't
+        // use any of the promotions from `thisModel`.
+        newPromotedTypes = baseModel.promotedTypes;
+      } else {
+        // The variable hasn't been written to since `thisModel`, so we can keep
+        // all of the promotions from `thisModel`, provided that we retain the
+        // usual "promotion chain" invariant (each promoted type is a subtype of
+        // the previous).
+        newPromotedTypes = VariableModel.rebasePromotedTypes(
+            typeOperations, thisModel.promotedTypes, baseModel.promotedTypes);
+      }
+      // Tests are kept regardless of whether they are in `this` model or the
+      // new base model.
+      List<Type> newTested = VariableModel.joinTested(
+          thisModel.tested, baseModel.tested, typeOperations);
+      // The variable is definitely assigned if it was definitely assigned
+      // either in `this` model or the new base model.
+      bool newAssigned = thisModel.assigned || baseModel.assigned;
+      // The variable is definitely unassigned if it was definitely unassigned
+      // in both `this` model and the new base model.
+      bool newUnassigned = thisModel.unassigned && baseModel.unassigned;
+      VariableModel<Variable, Type> newModel = VariableModel._identicalOrNew(
+          thisModel,
+          baseModel,
+          newPromotedTypes,
+          newTested,
+          newAssigned,
+          newUnassigned,
+          newWriteCaptured ? null : baseModel.ssaNode);
+      newVariableInfo[variable] = newModel;
+      if (!identical(newModel, thisModel)) variableInfoMatchesThis = false;
+      if (!identical(newModel, baseModel)) variableInfoMatchesBase = false;
+    }
+    // newVariableInfo is now correct.  However, if there are any variables
+    // present in `this` that aren't present in `base`, we may erroneously think
+    // that `newVariableInfo` matches `this`.  If so, correct that.
+    if (variableInfoMatchesThis) {
+      for (Variable variable in variableInfo.keys) {
+        if (!base.variableInfo.containsKey(variable)) {
+          variableInfoMatchesThis = false;
+          break;
+        }
+      }
+    }
+    assert(variableInfoMatchesThis ==
+        _variableInfosEqual(newVariableInfo, variableInfo));
+    assert(variableInfoMatchesBase ==
+        _variableInfosEqual(newVariableInfo, base.variableInfo));
+    if (variableInfoMatchesThis) {
+      newVariableInfo = variableInfo;
+    } else if (variableInfoMatchesBase) {
+      newVariableInfo = base.variableInfo;
+    }
+
+    return _identicalOrNew(this, base, newReachable, newVariableInfo);
+  }
+
   /// Updates the state to reflect a control path that is known to have
   /// previously passed through some [other] state.
   ///
@@ -1457,6 +1726,12 @@
       TypeOperations<Variable, Type> typeOperations,
       FlowModel<Variable, Type> other,
       Set<Variable> unsafe) {
+    if (allowLocalBooleanVarsToPromoteByDefault) {
+      // TODO(paulberry): when we hardcode
+      // allowLocalBooleanVarsToPromoteByDefault to `true`, we should remove
+      // this method entirely.
+      throw new StateError('This method should not be called anymore');
+    }
     Reachability newReachable =
         Reachability.restrict(reachable, other.reachable);
 
@@ -1525,7 +1800,7 @@
       TypeOperations<Variable, Type> typeOperations, Variable variable) {
     VariableModel<Variable, Type> info = infoFor(variable);
     if (info.writeCaptured) {
-      return new ExpressionInfo<Variable, Type>(this, this, this);
+      return new _TrivialExpressionInfo<Variable, Type>(this);
     }
 
     Type? previousType = info.promotedTypes?.last;
@@ -1533,7 +1808,7 @@
 
     Type newType = typeOperations.promoteToNonNull(previousType);
     if (typeOperations.isSameType(newType, previousType)) {
-      return new ExpressionInfo<Variable, Type>(this, this, this);
+      return new _TrivialExpressionInfo<Variable, Type>(this);
     }
     assert(typeOperations.isSubtypeOf(newType, previousType));
 
@@ -1591,7 +1866,7 @@
       Type type) {
     VariableModel<Variable, Type> info = infoFor(variable);
     if (info.writeCaptured) {
-      return new ExpressionInfo<Variable, Type>(this, this, this);
+      return new _TrivialExpressionInfo<Variable, Type>(this);
     }
 
     Type? previousType = info.promotedTypes?.last;
@@ -1645,13 +1920,16 @@
   /// Updates the state to indicate that an assignment was made to the given
   /// [variable].  The variable is marked as definitely assigned, and any
   /// previous type promotion is removed.
-  FlowModel<Variable, Type> write(Variable variable, Type writtenType,
+  FlowModel<Variable, Type> write(
+      Variable variable,
+      Type writtenType,
+      SsaNode<Variable, Type> newSsaNode,
       TypeOperations<Variable, Type> typeOperations) {
     VariableModel<Variable, Type>? infoForVar = variableInfo[variable];
     if (infoForVar == null) return this;
 
     VariableModel<Variable, Type> newInfoForVar =
-        infoForVar.write(variable, writtenType, typeOperations);
+        infoForVar.write(variable, writtenType, typeOperations, newSsaNode);
     if (identical(newInfoForVar, infoForVar)) return this;
 
     return _updateVariableInfo(variable, newInfoForVar);
@@ -1697,8 +1975,12 @@
         ? this
         : _updateVariableInfo(
             variable,
-            new VariableModel<Variable, Type>(newPromotedTypes, newTested,
-                info.assigned, info.unassigned, info.ssaNode),
+            new VariableModel<Variable, Type>(
+                promotedTypes: newPromotedTypes,
+                tested: newTested,
+                assigned: info.assigned,
+                unassigned: info.unassigned,
+                ssaNode: info.ssaNode),
             reachable: newReachable);
   }
 
@@ -1892,7 +2174,11 @@
   /// beginning of the function being analyzed.
   final bool overallReachable;
 
-  Reachability._(this.parent, this.locallyReachable, this.overallReachable) {
+  /// The number of `parent` links between this node and [initial].
+  final int depth;
+
+  Reachability._(this.parent, this.locallyReachable, this.overallReachable)
+      : depth = parent == null ? 0 : parent.depth + 1 {
     assert(overallReachable ==
         (locallyReachable && (parent?.overallReachable ?? true)));
   }
@@ -1900,7 +2186,30 @@
   const Reachability._initial()
       : parent = null,
         locallyReachable = true,
-        overallReachable = true;
+        overallReachable = true,
+        depth = 0;
+
+  /// Updates `this` reachability to account for the reachability of [base].
+  ///
+  /// This is the reachability component of the algorithm in
+  /// [FlowModel.rebaseForward].
+  Reachability rebaseForward(Reachability base) {
+    // If [base] is not reachable, then the result is not reachable.
+    if (!base.locallyReachable) return base;
+    // If any of the reachability nodes between `this` and its common ancestor
+    // with [base] are locally unreachable, that means that there was an exit in
+    // the flow control path from the point at which `this` and [base] diverged
+    // up to the current point of `this`; therefore we want to mark [base] as
+    // unreachable.
+    Reachability? ancestor = commonAncestor(this, base);
+    for (Reachability? self = this;
+        self != null && !identical(self, ancestor);
+        self = self.parent) {
+      if (!self.locallyReachable) return base.setUnreachable();
+    }
+    // Otherwise, the result is as reachable as [base] was.
+    return base;
+  }
 
   /// Returns a reachability with the same checkpoint as `this`, but where the
   /// current point in the program is considered locally unreachable.
@@ -1933,6 +2242,24 @@
     }
   }
 
+  /// Finds the common ancestor node of [r1] and [r2], if any such node exists;
+  /// otherwise `null`.  If [r1] and [r2] are the same node, that node is
+  /// returned.
+  static Reachability? commonAncestor(Reachability? r1, Reachability? r2) {
+    if (r1 == null || r2 == null) return null;
+    while (r1!.depth > r2.depth) {
+      r1 = r1.parent!;
+    }
+    while (r2!.depth > r1.depth) {
+      r2 = r2.parent!;
+    }
+    while (!identical(r1, r2)) {
+      r1 = r1!.parent;
+      r2 = r2!.parent;
+    }
+    return r1;
+  }
+
   /// Combines two reachabilities (both of which must be based on the same
   /// checkpoint), where the code is considered reachable from the checkpoint
   /// iff either argument is reachable from the checkpoint.
@@ -1971,12 +2298,25 @@
 /// (https://en.wikipedia.org/wiki/Static_single_assignment_form) except that it
 /// does not store a complete IR of the code being analyzed.
 @visibleForTesting
-class SsaNode {
+class SsaNode<Variable extends Object, Type extends Object> {
   /// Expando mapping SSA nodes to debug ids.  Only used by `toString`.
   static final Expando<int> _debugIds = new Expando<int>();
 
   static int _nextDebugId = 0;
 
+  /// Flow analysis information was associated with the expression that
+  /// produced the value represented by this SSA node, if it was non-trivial.
+  /// This can be used at a later time to perform promotions if the value is
+  /// used in a control flow construct.
+  ///
+  /// We don't bother storing flow analysis information if it's trivial (see
+  /// [_TrivialExpressionInfo]) because such information does not lead to
+  /// promotions.
+  @visibleForTesting
+  final ExpressionInfo<Variable, Type>? expressionInfo;
+
+  SsaNode(this.expressionInfo);
+
   @override
   String toString() {
     SsaNode self = this; // Work around #44475
@@ -2086,10 +2426,14 @@
   /// value has changed since a time in the past.
   ///
   /// `null` if the variable has been write captured.
-  final SsaNode? ssaNode;
+  final SsaNode<Variable, Type>? ssaNode;
 
-  VariableModel(this.promotedTypes, this.tested, this.assigned, this.unassigned,
-      this.ssaNode) {
+  VariableModel(
+      {required this.promotedTypes,
+      required this.tested,
+      required this.assigned,
+      required this.unassigned,
+      required this.ssaNode}) {
     assert(!(assigned && unassigned),
         "Can't be both definitely assigned and unassigned");
     assert(promotedTypes == null || promotedTypes!.isNotEmpty);
@@ -2107,7 +2451,7 @@
       : promotedTypes = null,
         tested = const [],
         unassigned = !assigned,
-        ssaNode = new SsaNode();
+        ssaNode = new SsaNode<Variable, Type>(null);
 
   /// Indicates whether the variable has been write captured.
   bool get writeCaptured => ssaNode == null;
@@ -2119,7 +2463,11 @@
   /// loops whose bodies write to them.
   VariableModel<Variable, Type> discardPromotionsAndMarkNotUnassigned() {
     return new VariableModel<Variable, Type>(
-        null, tested, assigned, false, writeCaptured ? null : new SsaNode());
+        promotedTypes: null,
+        tested: tested,
+        assigned: assigned,
+        unassigned: false,
+        ssaNode: writeCaptured ? null : new SsaNode<Variable, Type>(null));
   }
 
   /// Returns an updated model reflect a control path that is known to have
@@ -2129,6 +2477,12 @@
       TypeOperations<Variable, Type> typeOperations,
       VariableModel<Variable, Type> otherModel,
       bool unsafe) {
+    if (allowLocalBooleanVarsToPromoteByDefault) {
+      // TODO(paulberry): when we hardcode
+      // allowLocalBooleanVarsToPromoteByDefault to `true`, we should remove
+      // this method entirely.
+      throw new StateError('This method should not be called anymore');
+    }
     List<Type>? thisPromotedTypes = promotedTypes;
     List<Type>? otherPromotedTypes = otherModel.promotedTypes;
     bool newAssigned = assigned || otherModel.assigned;
@@ -2161,29 +2515,9 @@
       // There was an assignment to the variable in the "this" path, so none of
       // the promotions from the "other" path can be used.
       newPromotedTypes = thisPromotedTypes;
-    } else if (otherPromotedTypes == null) {
-      // The other promotion chain contributes nothing so we just use this
-      // promotion chain directly.
-      newPromotedTypes = thisPromotedTypes;
-    } else if (thisPromotedTypes == null) {
-      // This promotion chain contributes nothing so we just use the other
-      // promotion chain directly.
-      newPromotedTypes = otherPromotedTypes;
     } else {
-      // Start with otherPromotedTypes and apply each of the promotions in
-      // thisPromotedTypes (discarding any that don't follow the ordering
-      // invariant)
-      newPromotedTypes = otherPromotedTypes;
-      Type otherPromotedType = otherPromotedTypes.last;
-      for (int i = 0; i < thisPromotedTypes.length; i++) {
-        Type nextType = thisPromotedTypes[i];
-        if (typeOperations.isSubtypeOf(nextType, otherPromotedType) &&
-            !typeOperations.isSameType(nextType, otherPromotedType)) {
-          newPromotedTypes = otherPromotedTypes.toList()
-            ..addAll(thisPromotedTypes.skip(i));
-          break;
-        }
-      }
+      newPromotedTypes = rebasePromotedTypes(
+          typeOperations, thisPromotedTypes, otherPromotedTypes);
     }
     return _identicalOrNew(this, otherModel, newPromotedTypes, tested,
         newAssigned, newUnassigned, newWriteCaptured ? null : ssaNode);
@@ -2191,7 +2525,7 @@
 
   @override
   String toString() {
-    List<String> parts = [];
+    List<String> parts = [ssaNode.toString()];
     if (promotedTypes != null) {
       parts.add('promotedTypes: $promotedTypes');
     }
@@ -2212,11 +2546,18 @@
 
   /// Returns a new [VariableModel] reflecting the fact that the variable was
   /// just written to.
-  VariableModel<Variable, Type> write(Variable variable, Type writtenType,
-      TypeOperations<Variable, Type> typeOperations) {
+  VariableModel<Variable, Type> write(
+      Variable variable,
+      Type writtenType,
+      TypeOperations<Variable, Type> typeOperations,
+      SsaNode<Variable, Type> newSsaNode) {
     if (writeCaptured) {
       return new VariableModel<Variable, Type>(
-          promotedTypes, tested, true, false, null);
+          promotedTypes: promotedTypes,
+          tested: tested,
+          assigned: true,
+          unassigned: false,
+          ssaNode: null);
     }
 
     List<Type>? newPromotedTypes = _demoteViaAssignment(
@@ -2229,7 +2570,11 @@
         typeOperations, declaredType, newPromotedTypes, writtenType);
     if (identical(promotedTypes, newPromotedTypes) && assigned) {
       return new VariableModel<Variable, Type>(
-          promotedTypes, tested, assigned, unassigned, new SsaNode());
+          promotedTypes: promotedTypes,
+          tested: tested,
+          assigned: assigned,
+          unassigned: unassigned,
+          ssaNode: newSsaNode);
     }
 
     List<Type> newTested;
@@ -2240,14 +2585,22 @@
     }
 
     return new VariableModel<Variable, Type>(
-        newPromotedTypes, newTested, true, false, new SsaNode());
+        promotedTypes: newPromotedTypes,
+        tested: newTested,
+        assigned: true,
+        unassigned: false,
+        ssaNode: newSsaNode);
   }
 
   /// Returns a new [VariableModel] reflecting the fact that the variable has
   /// been write-captured.
   VariableModel<Variable, Type> writeCapture() {
     return new VariableModel<Variable, Type>(
-        null, const [], assigned, false, null);
+        promotedTypes: null,
+        tested: const [],
+        assigned: assigned,
+        unassigned: false,
+        ssaNode: null);
   }
 
   List<Type>? _demoteViaAssignment(
@@ -2403,8 +2756,12 @@
           List<Type> tested) {
     List<Type> newTested = joinTested(tested, model.tested, typeOperations);
     if (identical(newTested, model.tested)) return model;
-    return new VariableModel<Variable, Type>(model.promotedTypes, newTested,
-        model.assigned, model.unassigned, model.ssaNode);
+    return new VariableModel<Variable, Type>(
+        promotedTypes: model.promotedTypes,
+        tested: newTested,
+        assigned: model.assigned,
+        unassigned: model.unassigned,
+        ssaNode: model.ssaNode);
   }
 
   /// Joins two variable models.  See [FlowModel.join] for details.
@@ -2423,11 +2780,11 @@
     List<Type> newTested = newWriteCaptured
         ? const []
         : joinTested(first.tested, second.tested, typeOperations);
-    SsaNode? newSsaNode = newWriteCaptured
+    SsaNode<Variable, Type>? newSsaNode = newWriteCaptured
         ? null
         : first.ssaNode == second.ssaNode
             ? first.ssaNode
-            : new SsaNode();
+            : new SsaNode<Variable, Type>(null);
     return _identicalOrNew(first, second, newPromotedTypes, newTested,
         newAssigned, newUnassigned, newWriteCaptured ? null : newSsaNode);
   }
@@ -2515,6 +2872,45 @@
     return types2;
   }
 
+  /// Forms a promotion chain by starting with [basePromotedTypes] and applying
+  /// promotions from [thisPromotedTypes] to it, to the extent possible without
+  /// violating the usual ordering invariant (each promoted type must be a
+  /// subtype of the previous).
+  ///
+  /// In degenerate cases, the returned chain will be identical to
+  /// [thisPromotedTypes] or [basePromotedTypes] (to make it easier for the
+  /// caller to detect when data structures may be re-used).
+  static List<Type>? rebasePromotedTypes<Type extends Object>(
+      TypeOperations<Object, Type> typeOperations,
+      List<Type>? thisPromotedTypes,
+      List<Type>? basePromotedTypes) {
+    if (basePromotedTypes == null) {
+      // The base promotion chain contributes nothing so we just use this
+      // promotion chain directly.
+      return thisPromotedTypes;
+    } else if (thisPromotedTypes == null) {
+      // This promotion chain contributes nothing so we just use the base
+      // promotion chain directly.
+      return basePromotedTypes;
+    } else {
+      // Start with basePromotedTypes and apply each of the promotions in
+      // thisPromotedTypes (discarding any that don't follow the ordering
+      // invariant)
+      List<Type> newPromotedTypes = basePromotedTypes;
+      Type otherPromotedType = basePromotedTypes.last;
+      for (int i = 0; i < thisPromotedTypes.length; i++) {
+        Type nextType = thisPromotedTypes[i];
+        if (typeOperations.isSubtypeOf(nextType, otherPromotedType) &&
+            !typeOperations.isSameType(nextType, otherPromotedType)) {
+          newPromotedTypes = basePromotedTypes.toList()
+            ..addAll(thisPromotedTypes.skip(i));
+          break;
+        }
+      }
+      return newPromotedTypes;
+    }
+  }
+
   static List<Type> _addToPromotedTypes<Type extends Object>(
           List<Type>? promotedTypes, Type promoted) =>
       promotedTypes == null
@@ -2540,7 +2936,7 @@
           List<Type> newTested,
           bool newAssigned,
           bool newUnassigned,
-          SsaNode? newSsaNode) {
+          SsaNode<Variable, Type>? newSsaNode) {
     if (identical(first.promotedTypes, newPromotedTypes) &&
         identical(first.tested, newTested) &&
         first.assigned == newAssigned &&
@@ -2555,7 +2951,11 @@
       return second;
     } else {
       return new VariableModel<Variable, Type>(
-          newPromotedTypes, newTested, newAssigned, newUnassigned, newSsaNode);
+          promotedTypes: newPromotedTypes,
+          tested: newTested,
+          assigned: newAssigned,
+          unassigned: newUnassigned,
+          ssaNode: newSsaNode);
     }
   }
 
@@ -2696,7 +3096,18 @@
 
   final AssignedVariables<Node, Variable> _assignedVariables;
 
-  _FlowAnalysisImpl(this.typeOperations, this._assignedVariables) {
+  /// Set this boolean to `true` to temporarily enable the feature of allowing
+  /// local boolean variables to influence promotion, for this flow analysis
+  /// session (see https://github.com/dart-lang/language/issues/1274).  Once the
+  /// top level const [allowLocalBooleanVarsToPromoteByDefault] is changed to
+  /// `true`, this field will always be `true`, so it can be safely removed.
+  final bool allowLocalBooleanVarsToPromote;
+
+  _FlowAnalysisImpl(this.typeOperations, this._assignedVariables,
+      {bool allowLocalBooleanVarsToPromote = false})
+      : allowLocalBooleanVarsToPromote =
+            allowLocalBooleanVarsToPromoteByDefault ||
+                allowLocalBooleanVarsToPromote {
     _current = new FlowModel<Variable, Type>(Reachability.initial);
   }
 
@@ -2836,17 +3247,15 @@
       // analysis behavior to depend on mode, so we conservatively assume that
       // either result is possible.
     } else if (lhsInfo is _NullInfo<Variable, Type> && rhsVariable != null) {
-      assert(
-          leftOperandTypeClassification == TypeClassification.nullOrEquivalent);
       ExpressionInfo<Variable, Type> equalityInfo =
           _current.tryMarkNonNullable(typeOperations, rhsVariable);
-      _storeExpressionInfo(wholeExpression,
-          notEqual ? equalityInfo : ExpressionInfo.invert(equalityInfo));
+      _storeExpressionInfo(
+          wholeExpression, notEqual ? equalityInfo : equalityInfo.invert());
     } else if (rhsInfo is _NullInfo<Variable, Type> && lhsVariable != null) {
       ExpressionInfo<Variable, Type> equalityInfo =
           _current.tryMarkNonNullable(typeOperations, lhsVariable);
-      _storeExpressionInfo(wholeExpression,
-          notEqual ? equalityInfo : ExpressionInfo.invert(equalityInfo));
+      _storeExpressionInfo(
+          wholeExpression, notEqual ? equalityInfo : equalityInfo.invert());
     }
   }
 
@@ -2859,6 +3268,10 @@
   }
 
   @override
+  ExpressionInfo<Variable, Type>? expressionInfoForTesting(Expression target) =>
+      identical(target, _expressionWithInfo) ? _expressionInfo : null;
+
+  @override
   void finish() {
     assert(_stack.isEmpty);
     assert(_current.reachable.parent == null);
@@ -2914,7 +3327,8 @@
             _current.reachable.parent!, _current);
     _stack.add(context);
     if (loopVariable != null) {
-      _current = _current.write(loopVariable, writtenType, typeOperations);
+      _current = _current.write(loopVariable, writtenType,
+          new SsaNode<Variable, Type>(null), typeOperations);
     }
   }
 
@@ -3041,6 +3455,26 @@
   }
 
   @override
+  void initialize(
+      Variable variable, Type initializerType, Expression initializerExpression,
+      {required bool isFinal, required bool isLate}) {
+    ExpressionInfo<Variable, Type>? expressionInfo =
+        _getExpressionInfo(initializerExpression);
+    SsaNode<Variable, Type> newSsaNode = new SsaNode<Variable, Type>(isLate
+        ? null
+        : expressionInfo is _TrivialExpressionInfo
+            ? null
+            : expressionInfo);
+    if (isFinal) {
+      // We don't promote final variables on initialization, so pretend the
+      // written type is the variable's declared type.
+      initializerType = typeOperations.variableType(variable);
+    }
+    _current =
+        _current.write(variable, initializerType, newSsaNode, typeOperations);
+  }
+
+  @override
   bool isAssigned(Variable variable) {
     return _current.infoFor(variable).assigned;
   }
@@ -3052,8 +3486,8 @@
     if (subExpressionVariable != null) {
       ExpressionInfo<Variable, Type> expressionInfo = _current
           .tryPromoteForTypeCheck(typeOperations, subExpressionVariable, type);
-      _storeExpressionInfo(isExpression,
-          isNot ? ExpressionInfo.invert(expressionInfo) : expressionInfo);
+      _storeExpressionInfo(
+          isExpression, isNot ? expressionInfo.invert() : expressionInfo);
     }
   }
 
@@ -3136,7 +3570,7 @@
   @override
   void logicalNot_end(Expression notExpression, Expression operand) {
     ExpressionInfo<Variable, Type> conditionInfo = _expressionEnd(operand);
-    _storeExpressionInfo(notExpression, ExpressionInfo.invert(conditionInfo));
+    _storeExpressionInfo(notExpression, conditionInfo.invert());
   }
 
   @override
@@ -3191,7 +3625,7 @@
   }
 
   @override
-  SsaNode? ssaNodeForTesting(Variable variable) =>
+  SsaNode<Variable, Type>? ssaNodeForTesting(Variable variable) =>
       _current.variableInfo[variable]?.ssaNode;
 
   @override
@@ -3288,34 +3722,49 @@
 
   @override
   void tryFinallyStatement_bodyBegin() {
-    _stack.add(new _TryContext<Variable, Type>(_current));
+    _stack.add(new _TryFinallyContext<Variable, Type>(_current));
   }
 
   @override
   void tryFinallyStatement_end(Node finallyBlock) {
     AssignedVariablesNodeInfo<Variable> info =
         _assignedVariables._getInfoForNode(finallyBlock);
-    _TryContext<Variable, Type> context =
-        _stack.removeLast() as _TryContext<Variable, Type>;
-    _current = _current.restrict(
-        typeOperations, context._afterBodyAndCatches!, info._written);
+    _TryFinallyContext<Variable, Type> context =
+        _stack.removeLast() as _TryFinallyContext<Variable, Type>;
+    if (allowLocalBooleanVarsToPromote) {
+      _current = context._afterBodyAndCatches!
+          .attachFinally(typeOperations, context._beforeFinally, _current);
+    } else {
+      _current = _current.restrict(
+          typeOperations, context._afterBodyAndCatches!, info._written);
+    }
   }
 
   @override
   void tryFinallyStatement_finallyBegin(Node body) {
     AssignedVariablesNodeInfo<Variable> info =
         _assignedVariables._getInfoForNode(body);
-    _TryContext<Variable, Type> context =
-        _stack.last as _TryContext<Variable, Type>;
+    _TryFinallyContext<Variable, Type> context =
+        _stack.last as _TryFinallyContext<Variable, Type>;
     context._afterBodyAndCatches = _current;
     _current = _join(_current,
         context._previous.conservativeJoin(info._written, info._captured));
+    context._beforeFinally = _current;
   }
 
   @override
   Type? variableRead(Expression expression, Variable variable) {
     _storeExpressionVariable(expression, variable);
-    return _current.infoFor(variable).promotedTypes?.last;
+    VariableModel<Variable, Type> variableModel = _current.infoFor(variable);
+    if (allowLocalBooleanVarsToPromote) {
+      ExpressionInfo<Variable, Type>? expressionInfo =
+          variableModel.ssaNode?.expressionInfo;
+      if (expressionInfo != null) {
+        _storeExpressionInfo(
+            expression, expressionInfo.rebaseForward(typeOperations, _current));
+      }
+    }
+    return variableModel.promotedTypes?.last;
   }
 
   @override
@@ -3346,15 +3795,15 @@
   }
 
   @override
-  void write(Variable variable, Type writtenType,
-      {bool viaInitializer = false}) {
-    if (!viaInitializer) {
-      assert(
-          _assignedVariables._anywhere._written.contains(variable),
-          "Variable is written to, but was not included in "
-          "_variablesWrittenAnywhere: $variable");
-    }
-    _current = _current.write(variable, writtenType, typeOperations);
+  void write(
+      Variable variable, Type writtenType, Expression? writtenExpression) {
+    ExpressionInfo<Variable, Type>? expressionInfo = writtenExpression == null
+        ? null
+        : _getExpressionInfo(writtenExpression);
+    SsaNode<Variable, Type> newSsaNode = new SsaNode<Variable, Type>(
+        expressionInfo is _TrivialExpressionInfo ? null : expressionInfo);
+    _current =
+        _current.write(variable, writtenType, newSsaNode, typeOperations);
   }
 
   void _dumpState() {
@@ -3374,8 +3823,7 @@
   /// [ExpressionInfo] associated with the [expression], then a fresh
   /// [ExpressionInfo] is created recording the current flow analysis state.
   ExpressionInfo<Variable, Type> _expressionEnd(Expression expression) =>
-      _getExpressionInfo(expression) ??
-      new ExpressionInfo(_current, _current, _current);
+      _getExpressionInfo(expression) ?? new _TrivialExpressionInfo(_current);
 
   /// Gets the [ExpressionInfo] associated with the [expression] (which should
   /// be the last expression that was traversed).  If there is no
@@ -3493,6 +3941,20 @@
 
   @override
   FlowModel<Variable, Type> get ifTrue => after;
+
+  @override
+  ExpressionInfo<Variable, Type> invert() {
+    // This should only happen if `!null` is encountered.  That should never
+    // happen for a properly typed program, but we need to handle it so we can
+    // give reasonable errors for an improperly typed program.
+    return this;
+  }
+
+  @override
+  ExpressionInfo<Variable, Type> rebaseForward(
+          TypeOperations<Variable, Type> typeOperations,
+          FlowModel<Variable, Type> base) =>
+      new _NullInfo(base);
 }
 
 /// [_FlowContext] representing a language construct for which flow analysis
@@ -3530,6 +3992,32 @@
       'checkpoint: $_checkpoint)';
 }
 
+/// Specialization of [ExpressionInfo] for the case where the information we
+/// have about the expression is trivial (meaning we know by construction that
+/// the expression's [after], [ifTrue], and [ifFalse] models are all the same).
+class _TrivialExpressionInfo<Variable extends Object, Type extends Object>
+    implements ExpressionInfo<Variable, Type> {
+  @override
+  final FlowModel<Variable, Type> after;
+
+  _TrivialExpressionInfo(this.after);
+
+  @override
+  FlowModel<Variable, Type> get ifFalse => after;
+
+  @override
+  FlowModel<Variable, Type> get ifTrue => after;
+
+  @override
+  ExpressionInfo<Variable, Type> invert() => this;
+
+  @override
+  ExpressionInfo<Variable, Type> rebaseForward(
+          TypeOperations<Variable, Type> typeOperations,
+          FlowModel<Variable, Type> base) =>
+      new _TrivialExpressionInfo(base);
+}
+
 /// [_FlowContext] representing a try statement.
 class _TryContext<Variable extends Object, Type extends Object>
     extends _SimpleContext<Variable, Type> {
@@ -3552,6 +4040,15 @@
       'afterBodyAndCatches: $_afterBodyAndCatches)';
 }
 
+class _TryFinallyContext<Variable extends Object, Type extends Object>
+    extends _TryContext<Variable, Type> {
+  /// The flow model representing program state at the top of the `finally`
+  /// block.
+  late FlowModel<Variable, Type> _beforeFinally;
+
+  _TryFinallyContext(FlowModel<Variable, Type> previous) : super(previous);
+}
+
 /// [_FlowContext] representing a `while` loop (or a C-style `for` loop, which
 /// is functionally similar).
 class _WhileContext<Variable extends Object, Type extends Object>
diff --git a/pkg/_fe_analyzer_shared/lib/src/parser/parser_impl.dart b/pkg/_fe_analyzer_shared/lib/src/parser/parser_impl.dart
index 746d7ef..3135c01 100644
--- a/pkg/_fe_analyzer_shared/lib/src/parser/parser_impl.dart
+++ b/pkg/_fe_analyzer_shared/lib/src/parser/parser_impl.dart
@@ -2839,10 +2839,25 @@
     if (getOrSet != null && !inPlainSync && optional("set", getOrSet)) {
       reportRecoverableError(asyncToken, codes.messageSetterNotSync);
     }
-    bool isExternal = externalToken != null;
-    if (isExternal && !optional(';', token.next!)) {
-      reportRecoverableError(
-          externalToken!, codes.messageExternalMethodWithBody);
+    // TODO(paulberry): code below is slightly hacky to allow for implementing
+    // the feature "Infer non-nullability from local boolean variables"
+    // (https://github.com/dart-lang/language/issues/1274).  Since the version
+    // of Dart that is used for presubmit checks lags slightly behind master,
+    // we need the code to analyze correctly regardless of whether local boolean
+    // variables cause promotion or not.  Once the version of dart used for
+    // presubmit checks has been updated, this can be cleaned up to:
+    //   bool isExternal = externalToken != null;
+    //   if (externalToken != null && !optional(';', token.next!)) {
+    //     reportRecoverableError(
+    //         externalToken, codes.messageExternalMethodWithBody);
+    //   }
+    bool isExternal = false;
+    if (externalToken != null) {
+      isExternal = true;
+      if (!optional(';', token.next!)) {
+        reportRecoverableError(
+            externalToken, codes.messageExternalMethodWithBody);
+      }
     }
     token = parseFunctionBody(
         token, /* ofFunctionExpression = */ false, isExternal);
diff --git a/pkg/_fe_analyzer_shared/test/flow_analysis/flow_analysis_mini_ast.dart b/pkg/_fe_analyzer_shared/test/flow_analysis/flow_analysis_mini_ast.dart
index ac6b69b..dd7b1ff 100644
--- a/pkg/_fe_analyzer_shared/test/flow_analysis/flow_analysis_mini_ast.dart
+++ b/pkg/_fe_analyzer_shared/test/flow_analysis/flow_analysis_mini_ast.dart
@@ -68,13 +68,15 @@
     new _Continue(branchTargetPlaceholder);
 
 Statement declare(Var variable,
-        {required bool initialized, bool isFinal = false}) =>
-    new _Declare(
-        variable, initialized ? expr(variable.type.type) : null, isFinal);
+        {required bool initialized,
+        bool isFinal = false,
+        bool isLate = false}) =>
+    new _Declare(variable, initialized ? expr(variable.type.type) : null,
+        isFinal, isLate);
 
 Statement declareInitialized(Var variable, Expression initializer,
-        {bool isFinal = false}) =>
-    new _Declare(variable, initializer, isFinal);
+        {bool isFinal = false, bool isLate = false}) =>
+    new _Declare(variable, initializer, isFinal, isLate);
 
 Statement do_(List<Statement> body, Expression condition) =>
     _Do(body, condition);
@@ -152,6 +154,8 @@
         {required bool isExhaustive}) =>
     new _Switch(expression, cases, isExhaustive);
 
+Expression throw_(Expression operand) => new _Throw(operand);
+
 Statement tryCatch(List<Statement> body, List<CatchClause> catches) =>
     new _TryCatch(body, catches);
 
@@ -211,6 +215,9 @@
   /// If `this` is an expression `x`, creates the expression `x!`.
   Expression get nonNullAssert => new _NonNullAssert(this);
 
+  /// If `this` is an expression `x`, creates the expression `!x`.
+  Expression get not => new _Not(this);
+
   /// If `this` is an expression `x`, creates the expression `(x)`.
   Expression get parenthesized => new _ParenthesizedExpression(this);
 
@@ -231,6 +238,15 @@
   /// If `this` is an expression `x`, creates the expression `x == other`.
   Expression eq(Expression other) => new _Equal(this, other, false);
 
+  /// Creates an [Expression] that, when analyzed, will behave the same as
+  /// `this`, but after visiting it, will cause [callback] to be passed the
+  /// [ExpressionInfo] associated with it.  If the expression has no flow
+  /// analysis information associated with it, `null` will be passed to
+  /// [callback].
+  Expression getExpressionInfo(
+          void Function(ExpressionInfo<Var, Type>?) callback) =>
+      new _GetExpressionInfo(this, callback);
+
   /// If `this` is an expression `x`, creates the expression `x ?? other`.
   Expression ifNull(Expression other) => new _IfNull(this, other);
 
@@ -334,6 +350,7 @@
     'Object <: num': false,
     'Object <: num?': false,
     'Object <: Object?': true,
+    'Object <: String': false,
     'Object? <: Object': false,
     'Object? <: int': false,
     'Object? <: int?': false,
@@ -379,12 +396,16 @@
     'num* - Object': Type('Never'),
   };
 
+  final bool allowLocalBooleanVarsToPromote;
+
   final Map<String, bool> _subtypes = Map.of(_coreSubtypes);
 
   final Map<String, Type> _factorResults = Map.of(_coreFactors);
 
   Node? _currentSwitch;
 
+  Harness({this.allowLocalBooleanVarsToPromote = false});
+
   /// Updates the harness so that when a [factor] query is invoked on types
   /// [from] and [what], [result] will be returned.
   void addFactor(Type from, Type what, Type result) {
@@ -450,7 +471,8 @@
     var assignedVariables = AssignedVariables<Node, Var>();
     statements._preVisit(assignedVariables);
     var flow = FlowAnalysis<Node, Statement, Expression, Var, Type>(
-        this, assignedVariables);
+        this, assignedVariables,
+        allowLocalBooleanVarsToPromote: allowLocalBooleanVarsToPromote);
     statements._visit(this, flow);
     flow.finish();
   }
@@ -493,6 +515,10 @@
         !isSameType(promoteToNonNull(type1), type1)) {
       // type1 is already nullable
       return type1;
+    } else if (type1.type == 'Never') {
+      return type2;
+    } else if (type2.type == 'Never') {
+      return type1;
     } else {
       throw UnimplementedError(
           'TODO(paulberry): least upper bound of $type1 and $type2');
@@ -514,7 +540,8 @@
 
   /// Gets the SSA node associated with [variable] at the current point in
   /// control flow, or `null` if the variable has been write captured.
-  SsaNode? operator [](Var variable) => _flow.ssaNodeForTesting(variable);
+  SsaNode<Var, Type>? operator [](Var variable) =>
+      _flow.ssaNodeForTesting(variable);
 }
 
 /// Representation of a statement in the pseudo-Dart language used for flow
@@ -585,7 +612,7 @@
   String toString() => '$type $name';
 
   /// Creates an expression representing a write to this variable.
-  Expression write(Expression value) => new _Write(this, value);
+  Expression write(Expression? value) => new _Write(this, value);
 }
 
 class _As extends Expression {
@@ -842,14 +869,17 @@
   final Var variable;
   final Expression? initializer;
   final bool isFinal;
+  final bool isLate;
 
-  _Declare(this.variable, this.initializer, this.isFinal) : super._();
+  _Declare(this.variable, this.initializer, this.isFinal, this.isLate)
+      : super._();
 
   @override
   String toString() {
+    var latePart = isLate ? 'late ' : '';
     var finalPart = isFinal ? 'final ' : '';
     var initializerPart = initializer != null ? ' = $initializer' : '';
-    return '$finalPart$variable${initializerPart};';
+    return '$latePart$finalPart$variable${initializerPart};';
   }
 
   @override
@@ -864,8 +894,10 @@
     if (initializer == null) {
       flow.declare(variable, false);
     } else {
-      initializer._visit(h, flow);
+      var initializerType = initializer._visit(h, flow);
       flow.declare(variable, true);
+      flow.initialize(variable, initializerType, initializer,
+          isFinal: isFinal, isLate: isLate);
     }
   }
 }
@@ -1047,6 +1079,28 @@
   }
 }
 
+class _GetExpressionInfo extends Expression {
+  final Expression target;
+
+  final void Function(ExpressionInfo<Var, Type>?) callback;
+
+  _GetExpressionInfo(this.target, this.callback);
+
+  @override
+  void _preVisit(AssignedVariables<Node, Var> assignedVariables) {
+    target._preVisit(assignedVariables);
+  }
+
+  @override
+  Type _visit(
+      Harness h, FlowAnalysis<Node, Statement, Expression, Var, Type> flow) {
+    var type = target._visit(h, flow);
+    flow.forwardExpression(this, target);
+    callback(flow.expressionInfoForTesting(this));
+    return type;
+  }
+}
+
 class _GetSsaNodes extends Statement {
   final void Function(SsaNodeHarness) callback;
 
@@ -1240,6 +1294,27 @@
   }
 }
 
+class _Not extends Expression {
+  final Expression operand;
+
+  _Not(this.operand);
+
+  @override
+  String toString() => '!$operand';
+
+  @override
+  void _preVisit(AssignedVariables<Node, Var> assignedVariables) {
+    operand._preVisit(assignedVariables);
+  }
+
+  @override
+  Type _visit(
+      Harness h, FlowAnalysis<Node, Statement, Expression, Var, Type> flow) {
+    flow.logicalNot_end(this, operand.._visit(h, flow));
+    return Type('bool');
+  }
+}
+
 class _NullAwareAccess extends Expression {
   final Expression lhs;
   final Expression rhs;
@@ -1380,6 +1455,28 @@
   }
 }
 
+class _Throw extends Expression {
+  final Expression operand;
+
+  _Throw(this.operand);
+
+  @override
+  String toString() => 'throw ...';
+
+  @override
+  void _preVisit(AssignedVariables<Node, Var> assignedVariables) {
+    operand._preVisit(assignedVariables);
+  }
+
+  @override
+  Type _visit(
+      Harness h, FlowAnalysis<Node, Statement, Expression, Var, Type> flow) {
+    operand._visit(h, flow);
+    flow.handleExit();
+    return Type('Never');
+  }
+}
+
 class _TryCatch extends Statement {
   final List<Statement> body;
   final List<CatchClause> catches;
@@ -1534,7 +1631,7 @@
 
 class _Write extends Expression {
   final Var variable;
-  final Expression rhs;
+  final Expression? rhs;
 
   _Write(this.variable, this.rhs);
 
@@ -1544,14 +1641,15 @@
   @override
   void _preVisit(AssignedVariables<Node, Var> assignedVariables) {
     assignedVariables.write(variable);
-    rhs._preVisit(assignedVariables);
+    rhs?._preVisit(assignedVariables);
   }
 
   @override
   Type _visit(
       Harness h, FlowAnalysis<Node, Statement, Expression, Var, Type> flow) {
-    var type = rhs._visit(h, flow);
-    flow.write(variable, type);
+    var rhs = this.rhs;
+    var type = rhs == null ? variable.type : rhs._visit(h, flow);
+    flow.write(variable, type, rhs);
     return type;
   }
 }
diff --git a/pkg/_fe_analyzer_shared/test/flow_analysis/flow_analysis_test.dart b/pkg/_fe_analyzer_shared/test/flow_analysis/flow_analysis_test.dart
index f9dfea6..e9740e2 100644
--- a/pkg/_fe_analyzer_shared/test/flow_analysis/flow_analysis_test.dart
+++ b/pkg/_fe_analyzer_shared/test/flow_analysis/flow_analysis_test.dart
@@ -12,7 +12,7 @@
     test('asExpression_end promotes variables', () {
       var h = Harness();
       var x = Var('x', 'int?');
-      late SsaNode ssaBeforePromotion;
+      late SsaNode<Var, Type> ssaBeforePromotion;
       h.run([
         declare(x, initialized: true),
         getSsaNodes((nodes) => ssaBeforePromotion = nodes[x]!),
@@ -170,9 +170,8 @@
     test('declare() sets Ssa', () {
       var h = Harness();
       var x = Var('x', 'Object');
-      var y = Var('y', 'int?');
       h.run([
-        declareInitialized(x, y.read.eq(nullLiteral)),
+        declare(x, initialized: false),
         getSsaNodes((nodes) {
           expect(nodes[x], isNotNull);
         }),
@@ -182,7 +181,7 @@
     test('equalityOp(x != null) promotes true branch', () {
       var h = Harness();
       var x = Var('x', 'int?');
-      late SsaNode ssaBeforePromotion;
+      late SsaNode<Var, Type> ssaBeforePromotion;
       h.run([
         declare(x, initialized: true),
         getSsaNodes((nodes) => ssaBeforePromotion = nodes[x]!),
@@ -251,7 +250,7 @@
     test('equalityOp(x == null) promotes false branch', () {
       var h = Harness();
       var x = Var('x', 'int?');
-      late SsaNode ssaBeforePromotion;
+      late SsaNode<Var, Type> ssaBeforePromotion;
       h.run([
         declare(x, initialized: true),
         getSsaNodes((nodes) => ssaBeforePromotion = nodes[x]!),
@@ -285,7 +284,7 @@
     test('equalityOp(null != x) promotes true branch', () {
       var h = Harness();
       var x = Var('x', 'int?');
-      late SsaNode ssaBeforePromotion;
+      late SsaNode<Var, Type> ssaBeforePromotion;
       h.run([
         declare(x, initialized: true),
         getSsaNodes((nodes) => ssaBeforePromotion = nodes[x]!),
@@ -315,7 +314,7 @@
     test('equalityOp(null == x) promotes false branch', () {
       var h = Harness();
       var x = Var('x', 'int?');
-      late SsaNode ssaBeforePromotion;
+      late SsaNode<Var, Type> ssaBeforePromotion;
       h.run([
         declare(x, initialized: true),
         getSsaNodes((nodes) => ssaBeforePromotion = nodes[x]!),
@@ -437,7 +436,7 @@
     test('doStatement_bodyBegin() un-promotes', () {
       var h = Harness();
       var x = Var('x', 'int?');
-      late SsaNode ssaBeforeLoop;
+      late SsaNode<Var, Type> ssaBeforeLoop;
       h.run([
         declare(x, initialized: true),
         x.read.as_('int').stmt,
@@ -513,7 +512,7 @@
     test('for_conditionBegin() un-promotes', () {
       var h = Harness();
       var x = Var('x', 'int?');
-      late SsaNode ssaBeforeLoop;
+      late SsaNode<Var, Type> ssaBeforeLoop;
       h.run([
         declare(x, initialized: true),
         x.read.as_('int').stmt,
@@ -656,8 +655,8 @@
       var h = Harness();
       var x = Var('x', 'int?');
       var y = Var('x', 'int?');
-      late SsaNode xSsaInsideLoop;
-      late SsaNode ySsaInsideLoop;
+      late SsaNode<Var, Type> xSsaInsideLoop;
+      late SsaNode<Var, Type> ySsaInsideLoop;
       h.run([
         declare(x, initialized: true),
         declare(y, initialized: true),
@@ -685,8 +684,8 @@
       var h = Harness();
       var x = Var('x', 'int?');
       var y = Var('x', 'int?');
-      late SsaNode xSsaInsideLoop;
-      late SsaNode ySsaInsideLoop;
+      late SsaNode<Var, Type> xSsaInsideLoop;
+      late SsaNode<Var, Type> ySsaInsideLoop;
       h.run([
         declare(x, initialized: true),
         declare(y, initialized: true),
@@ -712,7 +711,7 @@
     test('forEach_bodyBegin() un-promotes', () {
       var h = Harness();
       var x = Var('x', 'int?');
-      late SsaNode ssaBeforeLoop;
+      late SsaNode<Var, Type> ssaBeforeLoop;
       h.run([
         declare(x, initialized: true),
         x.read.as_('int').stmt,
@@ -872,7 +871,7 @@
       var h = Harness();
       var x = Var('x', 'int?');
       var y = Var('y', 'int?');
-      late SsaNode ssaBeforeFunction;
+      late SsaNode<Var, Type> ssaBeforeFunction;
       h.run([
         declare(x, initialized: true), declare(y, initialized: true),
         x.read.as_('int').stmt, y.read.as_('int').stmt,
@@ -1135,7 +1134,7 @@
       var x = Var('x', 'bool');
       var y = Var('y', 'bool');
       var z = Var('z', 'bool');
-      late SsaNode xSsaNodeBeforeIf;
+      late SsaNode<Var, Type> xSsaNodeBeforeIf;
       h.run([
         declare(w, initialized: true),
         declare(x, initialized: true),
@@ -1144,6 +1143,7 @@
         x.write(w.read.is_('int')).stmt,
         getSsaNodes((nodes) {
           xSsaNodeBeforeIf = nodes[x]!;
+          expect(xSsaNodeBeforeIf.expressionInfo, isNotNull);
         }),
         if_(expr('bool'), [
           y.write(w.read.is_('String')).stmt,
@@ -1152,8 +1152,8 @@
         ]),
         getSsaNodes((nodes) {
           expect(nodes[x], same(xSsaNodeBeforeIf));
-          expect(nodes[y], isNotNull);
-          expect(nodes[z], isNotNull);
+          expect(nodes[y]!.expressionInfo, isNull);
+          expect(nodes[z]!.expressionInfo, isNull);
         }),
       ]);
     });
@@ -1163,7 +1163,7 @@
         'unreachable', () {
       var h = Harness();
       var x = Var('x', 'Object');
-      late SsaNode xSsaNodeBeforeIf;
+      late SsaNode<Var, Type> xSsaNodeBeforeIf;
       h.run([
         declare(x, initialized: true),
         getSsaNodes((nodes) {
@@ -1184,7 +1184,7 @@
         'unreachable', () {
       var h = Harness();
       var x = Var('x', 'Object');
-      late SsaNode xSsaNodeBeforeIf;
+      late SsaNode<Var, Type> xSsaNodeBeforeIf;
       h.run([
         declare(x, initialized: true),
         getSsaNodes((nodes) {
@@ -1200,12 +1200,83 @@
       ]);
     });
 
+    test('initialize() promotes when not final', () {
+      var h = Harness();
+      var x = Var('x', 'int?');
+      h.run([
+        declareInitialized(x, expr('int')),
+        checkPromoted(x, 'int'),
+      ]);
+    });
+
+    test('initialize() does not promote when final', () {
+      var h = Harness();
+      var x = Var('x', 'int?');
+      h.run([
+        declareInitialized(x, expr('int'), isFinal: true),
+        checkNotPromoted(x),
+      ]);
+    });
+
+    test('initialize() stores expressionInfo when not late', () {
+      var h = Harness();
+      var x = Var('x', 'Object');
+      var y = Var('y', 'int?');
+      late ExpressionInfo<Var, Type> writtenValueInfo;
+      h.run([
+        declareInitialized(
+            x,
+            y.read.eq(nullLiteral).getExpressionInfo((info) {
+              expect(info, isNotNull);
+              writtenValueInfo = info!;
+            })),
+        getSsaNodes((nodes) {
+          expect(nodes[x]!.expressionInfo, same(writtenValueInfo));
+        }),
+      ]);
+    });
+
+    test('initialize() does not store expressionInfo when late', () {
+      var h = Harness();
+      var x = Var('x', 'Object');
+      var y = Var('y', 'int?');
+      h.run([
+        declareInitialized(x, y.read.eq(nullLiteral), isLate: true),
+        getSsaNodes((nodes) {
+          expect(nodes[x]!.expressionInfo, isNull);
+        }),
+      ]);
+    });
+
+    test('initialize() does not store expressionInfo for trivial expressions',
+        () {
+      var h = Harness();
+      var x = Var('x', 'Object');
+      var y = Var('y', 'int?');
+      h.run([
+        declare(y, initialized: true),
+        localFunction([
+          y.write(expr('int?')).stmt,
+        ]),
+        declareInitialized(
+            x,
+            // `y == null` is a trivial expression because y has been write
+            // captured.
+            y.read
+                .eq(nullLiteral)
+                .getExpressionInfo((info) => expect(info, isNotNull))),
+        getSsaNodes((nodes) {
+          expect(nodes[x]!.expressionInfo, isNull);
+        }),
+      ]);
+    });
+
     void _checkIs(String declaredType, String tryPromoteType,
         String? expectedPromotedTypeThen, String? expectedPromotedTypeElse,
         {bool inverted = false}) {
       var h = Harness();
       var x = Var('x', declaredType);
-      late SsaNode ssaBeforePromotion;
+      late SsaNode<Var, Type> ssaBeforePromotion;
       h.run([
         declare(x, initialized: true),
         getSsaNodes((nodes) => ssaBeforePromotion = nodes[x]!),
@@ -1410,10 +1481,32 @@
       ]);
     });
 
+    test('logicalNot_end() inverts a condition', () {
+      var h = Harness();
+      var x = Var('x', 'int?');
+      h.run([
+        declare(x, initialized: true),
+        if_(x.read.eq(nullLiteral).not, [
+          checkPromoted(x, 'int'),
+        ], [
+          checkNotPromoted(x),
+        ]),
+      ]);
+    });
+
+    test('logicalNot_end() handles null literals', () {
+      var h = Harness();
+      h.run([
+        // `!null` would be a compile error, but we need to make sure we don't
+        // crash.
+        if_(nullLiteral.not, [], []),
+      ]);
+    });
+
     test('nonNullAssert_end(x) promotes', () {
       var h = Harness();
       var x = Var('x', 'int?');
-      late SsaNode ssaBeforePromotion;
+      late SsaNode<Var, Type> ssaBeforePromotion;
       h.run([
         declare(x, initialized: true),
         getSsaNodes((nodes) => ssaBeforePromotion = nodes[x]!),
@@ -1426,7 +1519,7 @@
     test('nullAwareAccess temporarily promotes', () {
       var h = Harness();
       var x = Var('x', 'int?');
-      late SsaNode ssaBeforePromotion;
+      late SsaNode<Var, Type> ssaBeforePromotion;
       h.run([
         declare(x, initialized: true),
         getSsaNodes((nodes) => ssaBeforePromotion = nodes[x]!),
@@ -1625,7 +1718,7 @@
     test('switchStatement_beginCase(true) un-promotes', () {
       var h = Harness();
       var x = Var('x', 'int?');
-      late SsaNode ssaBeforeSwitch;
+      late SsaNode<Var, Type> ssaBeforeSwitch;
       h.run([
         declare(x, initialized: true),
         x.read.as_('int').stmt,
@@ -1778,7 +1871,7 @@
         () {
       var h = Harness();
       var x = Var('x', 'int?');
-      late SsaNode ssaAfterTry;
+      late SsaNode<Var, Type> ssaAfterTry;
       h.run([
         declare(x, initialized: true),
         x.read.as_('int').stmt,
@@ -1927,8 +2020,8 @@
         'body', () {
       var h = Harness();
       var x = Var('x', 'int?');
-      late SsaNode ssaAtStartOfTry;
-      late SsaNode ssaAfterTry;
+      late SsaNode<Var, Type> ssaAtStartOfTry;
+      late SsaNode<Var, Type> ssaAfterTry;
       h.run([
         declare(x, initialized: true),
         x.read.as_('int').stmt,
@@ -1998,8 +2091,8 @@
       var h = Harness();
       var x = Var('x', 'int?');
       var y = Var('y', 'int?');
-      late SsaNode xSsaAtEndOfFinally;
-      late SsaNode ySsaAtEndOfFinally;
+      late SsaNode<Var, Type> xSsaAtEndOfFinally;
+      late SsaNode<Var, Type> ySsaAtEndOfFinally;
       h.run([
         declare(x, initialized: true), declare(y, initialized: true),
         tryFinally([
@@ -2030,10 +2123,504 @@
       ]);
     });
 
+    group('allowLocalBooleanVarsToPromote', () {
+      test(
+          'tryFinallyStatement_end() restores SSA nodes from try block when it'
+          'is sound to do so', () {
+        var h = Harness(allowLocalBooleanVarsToPromote: true);
+        var x = Var('x', 'int?');
+        var y = Var('y', 'int?');
+        late SsaNode<Var, Type> xSsaAtEndOfTry;
+        late SsaNode<Var, Type> ySsaAtEndOfTry;
+        late SsaNode<Var, Type> xSsaAtEndOfFinally;
+        late SsaNode<Var, Type> ySsaAtEndOfFinally;
+        h.run([
+          declare(x, initialized: true), declare(y, initialized: true),
+          tryFinally([
+            x.write(expr('int?')).stmt,
+            y.write(expr('int?')).stmt,
+            getSsaNodes((nodes) {
+              xSsaAtEndOfTry = nodes[x]!;
+              ySsaAtEndOfTry = nodes[y]!;
+            }),
+          ], [
+            if_(expr('bool'), [
+              x.write(expr('int?')).stmt,
+            ]),
+            if_(expr('bool'), [
+              y.write(expr('int?')).stmt,
+              return_(),
+            ]),
+            getSsaNodes((nodes) {
+              xSsaAtEndOfFinally = nodes[x]!;
+              ySsaAtEndOfFinally = nodes[y]!;
+              expect(xSsaAtEndOfFinally, isNot(same(xSsaAtEndOfTry)));
+              expect(ySsaAtEndOfFinally, isNot(same(ySsaAtEndOfTry)));
+            }),
+          ]),
+          // x's SSA node should still match what it was at the end of the
+          // finally block, because it might have been written to.  But y
+          // can't have been written to, because once we reach here, we know
+          // that the finally block completed normally, and the write to y
+          // always leads to the explicit return.  So y's SSA node should be
+          // restored back to match that from the end of the try block.
+          getSsaNodes((nodes) {
+            expect(nodes[x], same(xSsaAtEndOfFinally));
+            expect(nodes[y], same(ySsaAtEndOfTry));
+          }),
+        ]);
+      });
+
+      test(
+          'tryFinallyStatement_end() sets unreachable if end of try block '
+          'unreachable', () {
+        var h = Harness(allowLocalBooleanVarsToPromote: true);
+        h.run([
+          tryFinally([
+            return_(),
+            checkReachable(false),
+          ], [
+            checkReachable(true),
+          ]),
+          checkReachable(false),
+        ]);
+      });
+
+      test(
+          'tryFinallyStatement_end() sets unreachable if end of finally block '
+          'unreachable', () {
+        var h = Harness(allowLocalBooleanVarsToPromote: true);
+        h.run([
+          tryFinally([
+            checkReachable(true),
+          ], [
+            return_(),
+            checkReachable(false),
+          ]),
+          checkReachable(false),
+        ]);
+      });
+
+      test(
+          'tryFinallyStatement_end() handles a variable declared only in the '
+          'try block', () {
+        var h = Harness(allowLocalBooleanVarsToPromote: true);
+        var x = Var('x', 'int?');
+        h.run([
+          tryFinally([
+            declare(x, initialized: true),
+          ], []),
+        ]);
+      });
+
+      test(
+          'tryFinallyStatement_end() handles a variable declared only in the '
+          'finally block', () {
+        var h = Harness(allowLocalBooleanVarsToPromote: true);
+        var x = Var('x', 'int?');
+        h.run([
+          tryFinally([], [
+            declare(x, initialized: true),
+          ]),
+        ]);
+      });
+
+      test(
+          'tryFinallyStatement_end() handles a variable that was write '
+          'captured in the try block', () {
+        var h = Harness(allowLocalBooleanVarsToPromote: true);
+        var x = Var('x', 'int?');
+        h.run([
+          declare(x, initialized: true),
+          tryFinally([
+            localFunction([
+              x.write(expr('int?')).stmt,
+            ]),
+          ], []),
+          if_(x.read.notEq(nullLiteral), [
+            checkNotPromoted(x),
+          ]),
+        ]);
+      });
+
+      test(
+          'tryFinallyStatement_end() handles a variable that was write '
+          'captured in the finally block', () {
+        var h = Harness(allowLocalBooleanVarsToPromote: true);
+        var x = Var('x', 'int?');
+        h.run([
+          declare(x, initialized: true),
+          tryFinally([], [
+            localFunction([
+              x.write(expr('int?')).stmt,
+            ]),
+          ]),
+          if_(x.read.notEq(nullLiteral), [
+            checkNotPromoted(x),
+          ]),
+        ]);
+      });
+
+      test(
+          'tryFinallyStatement_end() handles a variable that was promoted in '
+          'the try block and write captured in the finally block', () {
+        var h = Harness(allowLocalBooleanVarsToPromote: true);
+        var x = Var('x', 'int?');
+        h.run([
+          declare(x, initialized: true),
+          tryFinally([
+            if_(x.read.eq(nullLiteral), [
+              return_(),
+            ]),
+            checkPromoted(x, 'int'),
+          ], [
+            localFunction([
+              x.write(expr('int?')).stmt,
+            ]),
+          ]),
+          // The capture in the `finally` cancels old promotions and prevents
+          // future promotions.
+          checkNotPromoted(x),
+          if_(x.read.notEq(nullLiteral), [
+            checkNotPromoted(x),
+          ]),
+        ]);
+      });
+
+      test(
+          'tryFinallyStatement_end() keeps promotions from both try and '
+          'finally blocks when there is no write in the finally block', () {
+        var h = Harness(allowLocalBooleanVarsToPromote: true);
+        var x = Var('x', 'Object');
+        h.run([
+          declare(x, initialized: true),
+          tryFinally([
+            if_(x.read.is_('num', isInverted: true), [
+              return_(),
+            ]),
+            checkPromoted(x, 'num'),
+          ], [
+            if_(x.read.is_('int', isInverted: true), [
+              return_(),
+            ]),
+          ]),
+          // The promotion chain now contains both `num` and `int`.
+          checkPromoted(x, 'int'),
+          x.write(expr('num')).stmt,
+          checkPromoted(x, 'num'),
+        ]);
+      });
+
+      test(
+          'tryFinallyStatement_end() keeps promotions from the finally block '
+          'when there is a write in the finally block', () {
+        var h = Harness(allowLocalBooleanVarsToPromote: true);
+        var x = Var('x', 'Object');
+        h.run([
+          declare(x, initialized: true),
+          tryFinally([
+            if_(x.read.is_('String', isInverted: true), [
+              return_(),
+            ]),
+            checkPromoted(x, 'String'),
+          ], [
+            x.write(expr('Object')).stmt,
+            if_(x.read.is_('int', isInverted: true), [
+              return_(),
+            ]),
+          ]),
+          checkPromoted(x, 'int'),
+        ]);
+      });
+
+      test(
+          'tryFinallyStatement_end() keeps tests from both the try and finally '
+          'blocks', () {
+        var h = Harness(allowLocalBooleanVarsToPromote: true);
+        var x = Var('x', 'Object');
+        h.run([
+          declare(x, initialized: true),
+          tryFinally([
+            if_(x.read.is_('String', isInverted: true), []),
+            checkNotPromoted(x),
+          ], [
+            if_(x.read.is_('int', isInverted: true), []),
+            checkNotPromoted(x),
+          ]),
+          checkNotPromoted(x),
+          if_(expr('bool'), [
+            x.write(expr('String')).stmt,
+            checkPromoted(x, 'String'),
+          ], [
+            x.write(expr('int')).stmt,
+            checkPromoted(x, 'int'),
+          ]),
+        ]);
+      });
+
+      test(
+          'tryFinallyStatement_end() handles variables not definitely assigned '
+          'in either the try or finally block', () {
+        var h = Harness(allowLocalBooleanVarsToPromote: true);
+        var x = Var('x', 'Object');
+        h.run([
+          declare(x, initialized: false),
+          checkAssigned(x, false),
+          tryFinally([
+            if_(expr('bool'), [
+              x.write(expr('Object')).stmt,
+            ]),
+            checkAssigned(x, false),
+          ], [
+            if_(expr('bool'), [
+              x.write(expr('Object')).stmt,
+            ]),
+            checkAssigned(x, false),
+          ]),
+          checkAssigned(x, false),
+        ]);
+      });
+
+      test(
+          'tryFinallyStatement_end() handles variables definitely assigned in '
+          'the try block', () {
+        var h = Harness(allowLocalBooleanVarsToPromote: true);
+        var x = Var('x', 'Object');
+        h.run([
+          declare(x, initialized: false),
+          checkAssigned(x, false),
+          tryFinally([
+            x.write(expr('Object')).stmt,
+            checkAssigned(x, true),
+          ], [
+            if_(expr('bool'), [
+              x.write(expr('Object')).stmt,
+            ]),
+            checkAssigned(x, false),
+          ]),
+          checkAssigned(x, true),
+        ]);
+      });
+
+      test(
+          'tryFinallyStatement_end() handles variables definitely assigned in '
+          'the finally block', () {
+        var h = Harness(allowLocalBooleanVarsToPromote: true);
+        var x = Var('x', 'Object');
+        h.run([
+          declare(x, initialized: false),
+          checkAssigned(x, false),
+          tryFinally([
+            if_(expr('bool'), [
+              x.write(expr('Object')).stmt,
+            ]),
+            checkAssigned(x, false),
+          ], [
+            x.write(expr('Object')).stmt,
+            checkAssigned(x, true),
+          ]),
+          checkAssigned(x, true),
+        ]);
+      });
+
+      test(
+          'tryFinallyStatement_end() handles variables definitely unassigned '
+          'in both the try and finally blocks', () {
+        var h = Harness(allowLocalBooleanVarsToPromote: true);
+        var x = Var('x', 'Object');
+        h.run([
+          declare(x, initialized: false),
+          checkUnassigned(x, true),
+          tryFinally([
+            checkUnassigned(x, true),
+          ], [
+            checkUnassigned(x, true),
+          ]),
+          checkUnassigned(x, true),
+        ]);
+      });
+
+      test(
+          'tryFinallyStatement_end() handles variables definitely unassigned '
+          'in the try but not the finally block', () {
+        var h = Harness(allowLocalBooleanVarsToPromote: true);
+        var x = Var('x', 'Object');
+        h.run([
+          declare(x, initialized: false),
+          checkUnassigned(x, true),
+          tryFinally([
+            checkUnassigned(x, true),
+          ], [
+            if_(expr('bool'), [
+              x.write(expr('Object')).stmt,
+            ]),
+            checkUnassigned(x, false),
+          ]),
+          checkUnassigned(x, false),
+        ]);
+      });
+
+      test(
+          'tryFinallyStatement_end() handles variables definitely unassigned '
+          'in the finally but not the try block', () {
+        var h = Harness(allowLocalBooleanVarsToPromote: true);
+        var x = Var('x', 'Object');
+        h.run([
+          declare(x, initialized: false),
+          checkUnassigned(x, true),
+          tryFinally([
+            if_(expr('bool'), [
+              x.write(expr('Object')).stmt,
+            ]),
+            checkUnassigned(x, false),
+          ], [
+            checkUnassigned(x, false),
+          ]),
+          checkUnassigned(x, false),
+        ]);
+      });
+    });
+
+    test('variableRead() restores promotions from previous write()', () {
+      var h = Harness(allowLocalBooleanVarsToPromote: true);
+      var x = Var('x', 'int?');
+      var y = Var('y', 'int?');
+      var z = Var('z', 'bool');
+      h.run([
+        declare(x, initialized: true),
+        declare(y, initialized: true),
+        declare(z, initialized: true),
+        // Create a variable that promotes x if its value is true, and y if its
+        // value is false.
+        z
+            .write(x.read.notEq(nullLiteral).conditional(
+                booleanLiteral(true),
+                y.read.notEq(nullLiteral).conditional(
+                    booleanLiteral(false), throw_(expr('Object')))))
+            .stmt,
+        checkNotPromoted(x),
+        checkNotPromoted(y),
+        // Simply reading the variable shouldn't promote anything.
+        z.read.stmt,
+        checkNotPromoted(x),
+        checkNotPromoted(y),
+        // But reading it in an "if" condition should promote.
+        if_(z.read, [
+          checkPromoted(x, 'int'),
+          checkNotPromoted(y),
+        ], [
+          checkNotPromoted(x),
+          checkPromoted(y, 'int'),
+        ]),
+      ]);
+    });
+
+    test('variableRead() restores promotions from previous initialization', () {
+      var h = Harness(allowLocalBooleanVarsToPromote: true);
+      var x = Var('x', 'int?');
+      var y = Var('y', 'int?');
+      var z = Var('z', 'bool');
+      h.run([
+        declare(x, initialized: true),
+        declare(y, initialized: true),
+        // Create a variable that promotes x if its value is true, and y if its
+        // value is false.
+        declareInitialized(
+            z,
+            x.read.notEq(nullLiteral).conditional(
+                booleanLiteral(true),
+                y.read.notEq(nullLiteral).conditional(
+                    booleanLiteral(false), throw_(expr('Object'))))),
+        checkNotPromoted(x),
+        checkNotPromoted(y),
+        // Simply reading the variable shouldn't promote anything.
+        z.read.stmt,
+        checkNotPromoted(x),
+        checkNotPromoted(y),
+        // But reading it in an "if" condition should promote.
+        if_(z.read, [
+          checkPromoted(x, 'int'),
+          checkNotPromoted(y),
+        ], [
+          checkNotPromoted(x),
+          checkPromoted(y, 'int'),
+        ]),
+      ]);
+    });
+
+    test('variableRead() rebases old promotions', () {
+      var h = Harness(allowLocalBooleanVarsToPromote: true);
+      var w = Var('w', 'int?');
+      var x = Var('x', 'int?');
+      var y = Var('y', 'int?');
+      var z = Var('z', 'bool');
+      h.run([
+        declare(w, initialized: true),
+        declare(x, initialized: true),
+        declare(y, initialized: true),
+        declare(z, initialized: true),
+        // Create a variable that promotes x if its value is true, and y if its
+        // value is false.
+        z
+            .write(x.read.notEq(nullLiteral).conditional(
+                booleanLiteral(true),
+                y.read.notEq(nullLiteral).conditional(
+                    booleanLiteral(false), throw_(expr('Object')))))
+            .stmt,
+        checkNotPromoted(w),
+        checkNotPromoted(x),
+        checkNotPromoted(y),
+        w.read.nonNullAssert.stmt,
+        checkPromoted(w, 'int'),
+        // Reading the value of z in an "if" condition should promote x or y,
+        // and keep the promotion of w.
+        if_(z.read, [
+          checkPromoted(w, 'int'),
+          checkPromoted(x, 'int'),
+          checkNotPromoted(y),
+        ], [
+          checkPromoted(w, 'int'),
+          checkNotPromoted(x),
+          checkPromoted(y, 'int'),
+        ]),
+      ]);
+    });
+
+    test('variableRead() restores the notion of whether a value is null', () {
+      // This is not a necessary part of the design of
+      // https://github.com/dart-lang/language/issues/1274; it's more of a happy
+      // accident of how it was implemented: since we store an ExpressionInfo
+      // object in the SSA node to keep track of the consequences of the
+      // variable's value on flow analysis, and since the _NullInfo type is a
+      // subtype of ExpressionInfo, that means that when a literal `null` is
+      // written to a variable, and we read it later, we recognize the value as
+      // being `null` for purposes of comparison to other other variables.  Even
+      // though this feature is a happy accident of the implementation strategy,
+      // it's important to test it to make sure it doesn't regress, since users
+      // might come to rely on it.
+      var h = Harness(allowLocalBooleanVarsToPromote: true);
+      var x = Var('x', 'int?');
+      var y = Var('y', 'int?');
+      h.run([
+        declare(x, initialized: true),
+        declare(y, initialized: true),
+        y.write(nullLiteral).stmt,
+        checkNotPromoted(x),
+        checkNotPromoted(y),
+        if_(x.read.eq(y.read), [
+          checkNotPromoted(x),
+          checkNotPromoted(y),
+        ], [
+          checkPromoted(x, 'int'),
+          checkNotPromoted(y),
+        ]),
+      ]);
+    });
+
     test('whileStatement_conditionBegin() un-promotes', () {
       var h = Harness();
       var x = Var('x', 'int?');
-      late SsaNode ssaBeforeLoop;
+      late SsaNode<Var, Type> ssaBeforeLoop;
       h.run([
         declare(x, initialized: true),
         x.read.as_('int').stmt,
@@ -2124,8 +2711,8 @@
       var h = Harness();
       var x = Var('x', 'int?');
       var y = Var('x', 'int?');
-      late SsaNode xSsaInsideLoop;
-      late SsaNode ySsaInsideLoop;
+      late SsaNode<Var, Type> xSsaInsideLoop;
+      late SsaNode<Var, Type> ySsaInsideLoop;
       h.run([
         declare(x, initialized: true),
         declare(y, initialized: true),
@@ -2153,8 +2740,8 @@
       var h = Harness();
       var x = Var('x', 'int?');
       var y = Var('x', 'int?');
-      late SsaNode xSsaInsideLoop;
-      late SsaNode ySsaInsideLoop;
+      late SsaNode<Var, Type> xSsaInsideLoop;
+      late SsaNode<Var, Type> ySsaInsideLoop;
       h.run([
         declare(x, initialized: true),
         declare(y, initialized: true),
@@ -2181,18 +2768,24 @@
       var h = Harness();
       var x = Var('x', 'Object');
       var y = Var('y', 'int?');
-      late SsaNode ssaBeforeWrite;
+      late SsaNode<Var, Type> ssaBeforeWrite;
+      late ExpressionInfo<Var, Type> writtenValueInfo;
       h.run([
         declare(x, initialized: true),
         declare(y, initialized: true),
         x.read.as_('int').stmt,
         checkPromoted(x, 'int'),
         getSsaNodes((nodes) => ssaBeforeWrite = nodes[x]!),
-        x.write(y.read.eq(nullLiteral)).stmt,
+        x
+            .write(y.read.eq(nullLiteral).getExpressionInfo((info) {
+              expect(info, isNotNull);
+              writtenValueInfo = info!;
+            }))
+            .stmt,
         checkNotPromoted(x),
         getSsaNodes((nodes) {
           expect(nodes[x], isNot(ssaBeforeWrite));
-          expect(nodes[x], isNotNull);
+          expect(nodes[x]!.expressionInfo, same(writtenValueInfo));
         }),
       ]);
     });
@@ -2201,13 +2794,93 @@
       var h = Harness();
       var x = Var('x', 'Object');
       var y = Var('y', 'int?');
-      late SsaNode ssaBeforeWrite;
+      late SsaNode<Var, Type> ssaBeforeWrite;
+      late ExpressionInfo<Var, Type> writtenValueInfo;
       h.run([
         declare(x, initialized: true),
         getSsaNodes((nodes) => ssaBeforeWrite = nodes[x]!),
-        x.write(y.read.eq(nullLiteral)).stmt,
+        x
+            .write(y.read.eq(nullLiteral).getExpressionInfo((info) {
+              expect(info, isNotNull);
+              writtenValueInfo = info!;
+            }))
+            .stmt,
         getSsaNodes((nodes) {
           expect(nodes[x], isNot(ssaBeforeWrite));
+          expect(nodes[x]!.expressionInfo, same(writtenValueInfo));
+        }),
+      ]);
+    });
+
+    test('write() does not copy Ssa from one variable to another', () {
+      // We could do so, and it would enable us to promote in slightly more
+      // situations, e.g.:
+      //   bool b = x != null;
+      //   if (b) { /* x promoted here */ }
+      //   var tmp = x;
+      //   x = ...;
+      //   if (b) { /* x not promoted here */ }
+      //   x = tmp;
+      //   if (b) { /* x promoted again */ }
+      // But there are a lot of corner cases to test and it's not clear how much
+      // the benefit will be, so for now we're not doing it.
+      var h = Harness(allowLocalBooleanVarsToPromote: true);
+      var x = Var('x', 'int?');
+      var y = Var('y', 'int?');
+      late SsaNode<Var, Type> xSsaBeforeWrite;
+      late SsaNode<Var, Type> ySsa;
+      h.run([
+        declare(x, initialized: true),
+        declare(y, initialized: true),
+        getSsaNodes((nodes) {
+          xSsaBeforeWrite = nodes[x]!;
+          ySsa = nodes[y]!;
+        }),
+        x.write(y.read).stmt,
+        getSsaNodes((nodes) {
+          expect(nodes[x], isNot(xSsaBeforeWrite));
+          expect(nodes[x], isNot(ySsa));
+        }),
+      ]);
+    });
+
+    test('write() does not store expressionInfo for trivial expressions', () {
+      var h = Harness();
+      var x = Var('x', 'Object');
+      var y = Var('y', 'int?');
+      late SsaNode<Var, Type> ssaBeforeWrite;
+      h.run([
+        declare(x, initialized: true),
+        declare(y, initialized: true),
+        localFunction([
+          y.write(expr('int?')).stmt,
+        ]),
+        getSsaNodes((nodes) => ssaBeforeWrite = nodes[x]!),
+        // `y == null` is a trivial expression because y has been write
+        // captured.
+        x
+            .write(y.read
+                .eq(nullLiteral)
+                .getExpressionInfo((info) => expect(info, isNotNull)))
+            .stmt,
+        getSsaNodes((nodes) {
+          expect(nodes[x], isNot(ssaBeforeWrite));
+          expect(nodes[x]!.expressionInfo, isNull);
+        }),
+      ]);
+    });
+
+    test('write() permits expression to be null', () {
+      var h = Harness();
+      var x = Var('x', 'Object');
+      late SsaNode<Var, Type> ssaBeforeWrite;
+      h.run([
+        declare(x, initialized: true),
+        getSsaNodes((nodes) => ssaBeforeWrite = nodes[x]!),
+        x.write(null).stmt,
+        getSsaNodes((nodes) {
+          expect(nodes[x], isNot(ssaBeforeWrite));
+          expect(nodes[x]!.expressionInfo, isNull);
         }),
       ]);
     });
@@ -2344,6 +3017,30 @@
           Reachability.restrict(unreachable, unreachable), same(unreachable));
     });
 
+    test('rebaseForward', () {
+      var previous = Reachability.initial;
+      var reachable = previous.split();
+      var reachable2 = previous.split();
+      var unreachable = reachable.setUnreachable();
+      var unreachablePrevious = previous.setUnreachable();
+      var reachable3 = unreachablePrevious.split();
+      expect(reachable.rebaseForward(reachable), same(reachable));
+      expect(reachable.rebaseForward(reachable2), same(reachable2));
+      expect(reachable.rebaseForward(unreachable), same(unreachable));
+      expect(unreachable.rebaseForward(reachable).parent, same(previous));
+      expect(unreachable.rebaseForward(reachable).locallyReachable, false);
+      expect(unreachable.rebaseForward(unreachable), same(unreachable));
+      expect(reachable.rebaseForward(unreachablePrevious),
+          same(unreachablePrevious));
+      expect(
+          unreachablePrevious.rebaseForward(reachable).parent, same(previous));
+      expect(
+          unreachablePrevious.rebaseForward(reachable).locallyReachable, false);
+      expect(reachable.rebaseForward(reachable3), same(reachable3));
+      expect(reachable3.rebaseForward(reachable).parent, same(previous));
+      expect(reachable3.rebaseForward(reachable).locallyReachable, false);
+    });
+
     test('join', () {
       var previous = Reachability.initial.split();
       var reachable = previous.split();
@@ -2353,6 +3050,40 @@
       expect(Reachability.join(unreachable, reachable), same(reachable));
       expect(Reachability.join(unreachable, unreachable), same(unreachable));
     });
+
+    test('commonAncestor', () {
+      var parent1 = Reachability.initial;
+      var parent2 = parent1.setUnreachable();
+      var child1 = parent1.split();
+      var child2 = parent1.split();
+      var child3 = child1.split();
+      var child4 = child2.split();
+      expect(Reachability.commonAncestor(null, null), null);
+      expect(Reachability.commonAncestor(null, parent1), null);
+      expect(Reachability.commonAncestor(parent1, null), null);
+      expect(Reachability.commonAncestor(null, child1), null);
+      expect(Reachability.commonAncestor(child1, null), null);
+      expect(Reachability.commonAncestor(null, child3), null);
+      expect(Reachability.commonAncestor(child3, null), null);
+      expect(Reachability.commonAncestor(parent1, parent1), same(parent1));
+      expect(Reachability.commonAncestor(parent1, parent2), null);
+      expect(Reachability.commonAncestor(parent2, child1), null);
+      expect(Reachability.commonAncestor(child1, parent2), null);
+      expect(Reachability.commonAncestor(parent2, child3), null);
+      expect(Reachability.commonAncestor(child3, parent2), null);
+      expect(Reachability.commonAncestor(parent1, child1), same(parent1));
+      expect(Reachability.commonAncestor(child1, parent1), same(parent1));
+      expect(Reachability.commonAncestor(parent1, child3), same(parent1));
+      expect(Reachability.commonAncestor(child3, parent1), same(parent1));
+      expect(Reachability.commonAncestor(child1, child1), same(child1));
+      expect(Reachability.commonAncestor(child1, child2), same(parent1));
+      expect(Reachability.commonAncestor(child1, child3), same(child1));
+      expect(Reachability.commonAncestor(child3, child1), same(child1));
+      expect(Reachability.commonAncestor(child1, child4), same(parent1));
+      expect(Reachability.commonAncestor(child4, child1), same(parent1));
+      expect(Reachability.commonAncestor(child3, child3), same(child3));
+      expect(Reachability.commonAncestor(child3, child4), same(parent1));
+    });
   });
 
   group('State', () {
@@ -2522,8 +3253,8 @@
       test('without declaration', () {
         // This should not happen in valid code, but test that we don't crash.
         var h = Harness();
-        var s = FlowModel<Var, Type>(Reachability.initial)
-            .write(objectQVar, Type('Object?'), h);
+        var s = FlowModel<Var, Type>(Reachability.initial).write(
+            objectQVar, Type('Object?'), new SsaNode<Var, Type>(null), h);
         expect(s.variableInfo[objectQVar], isNull);
       });
 
@@ -2531,7 +3262,8 @@
         var h = Harness();
         var s1 = FlowModel<Var, Type>(Reachability.initial)
             .declare(objectQVar, true);
-        var s2 = s1.write(objectQVar, Type('Object?'), h);
+        var s2 = s1.write(
+            objectQVar, Type('Object?'), new SsaNode<Var, Type>(null), h);
         expect(s2, isNot(same(s1)));
         expect(s2.reachable, same(s1.reachable));
         expect(
@@ -2547,7 +3279,8 @@
         var h = Harness();
         var s1 = FlowModel<Var, Type>(Reachability.initial)
             .declare(objectQVar, false);
-        var s2 = s1.write(objectQVar, Type('int?'), h);
+        var s2 =
+            s1.write(objectQVar, Type('int?'), new SsaNode<Var, Type>(null), h);
         expect(s2.reachable.overallReachable, true);
         expect(
             s2.infoFor(objectQVar),
@@ -2565,7 +3298,8 @@
             .tryPromoteForTypeCheck(h, objectQVar, Type('int'))
             .ifTrue;
         expect(s1.variableInfo, contains(objectQVar));
-        var s2 = s1.write(objectQVar, Type('int?'), h);
+        var s2 =
+            s1.write(objectQVar, Type('int?'), new SsaNode<Var, Type>(null), h);
         expect(s2.reachable.overallReachable, true);
         expect(s2.variableInfo, {
           objectQVar: _matchVariableModel(
@@ -2591,7 +3325,8 @@
               assigned: true,
               unassigned: false)
         });
-        var s2 = s1.write(objectQVar, Type('num'), h);
+        var s2 =
+            s1.write(objectQVar, Type('num'), new SsaNode<Var, Type>(null), h);
         expect(s2.reachable.overallReachable, true);
         expect(s2.variableInfo, {
           objectQVar: _matchVariableModel(
@@ -2619,7 +3354,8 @@
               assigned: true,
               unassigned: false)
         });
-        var s2 = s1.write(objectQVar, Type('num'), h);
+        var s2 =
+            s1.write(objectQVar, Type('num'), new SsaNode<Var, Type>(null), h);
         expect(s2.reachable.overallReachable, true);
         expect(s2.variableInfo, {
           objectQVar: _matchVariableModel(
@@ -2645,7 +3381,8 @@
               assigned: true,
               unassigned: false)
         });
-        var s2 = s1.write(objectQVar, Type('num'), h);
+        var s2 =
+            s1.write(objectQVar, Type('num'), new SsaNode<Var, Type>(null), h);
         expect(s2.reachable.overallReachable, true);
         expect(s2.variableInfo, isNot(same(s1.variableInfo)));
         expect(s2.variableInfo, {
@@ -2672,7 +3409,8 @@
               assigned: true,
               unassigned: false)
         });
-        var s2 = s1.write(objectQVar, Type('int'), h);
+        var s2 =
+            s1.write(objectQVar, Type('int'), new SsaNode<Var, Type>(null), h);
         expect(s2.reachable.overallReachable, true);
         expect(s2.variableInfo, isNot(same(s1.variableInfo)));
         expect(s2.variableInfo, {
@@ -2694,7 +3432,7 @@
             x: _matchVariableModel(chain: null),
           });
 
-          var s2 = s1.write(x, Type('int'), h);
+          var s2 = s1.write(x, Type('int'), new SsaNode<Var, Type>(null), h);
           expect(s2.variableInfo, {
             x: _matchVariableModel(chain: ['int']),
           });
@@ -2715,7 +3453,7 @@
           });
 
           // 'x' is write-captured, so not promoted
-          var s3 = s2.write(x, Type('int'), h);
+          var s3 = s2.write(x, Type('int'), new SsaNode<Var, Type>(null), h);
           expect(s3.variableInfo, {
             x: _matchVariableModel(chain: null, writeCaptured: true),
           });
@@ -2733,7 +3471,8 @@
               ofInterest: ['int?'],
             ),
           });
-          var s2 = s1.write(objectQVar, Type('int'), h);
+          var s2 = s1.write(
+              objectQVar, Type('int'), new SsaNode<Var, Type>(null), h);
           expect(s2.variableInfo, {
             objectQVar: _matchVariableModel(
               chain: ['int?', 'int'],
@@ -2754,7 +3493,8 @@
               ofInterest: ['int?'],
             ),
           });
-          var s2 = s1.write(objectQVar, Type('int'), h);
+          var s2 = s1.write(
+              objectQVar, Type('int'), new SsaNode<Var, Type>(null), h);
           expect(s2.variableInfo, {
             objectQVar: _matchVariableModel(
               chain: ['Object', 'int'],
@@ -2776,7 +3516,8 @@
             ofInterest: ['num?'],
           ),
         });
-        var s2 = s1.write(objectQVar, Type('num?'), h);
+        var s2 =
+            s1.write(objectQVar, Type('num?'), new SsaNode<Var, Type>(null), h);
         expect(s2.variableInfo, {
           objectQVar: _matchVariableModel(
             chain: ['num?'],
@@ -2799,7 +3540,8 @@
             ofInterest: ['num?', 'int?'],
           ),
         });
-        var s2 = s1.write(objectQVar, Type('int?'), h);
+        var s2 =
+            s1.write(objectQVar, Type('int?'), new SsaNode<Var, Type>(null), h);
         expect(s2.variableInfo, {
           objectQVar: _matchVariableModel(
             chain: ['num?', 'int?'],
@@ -2868,7 +3610,7 @@
               ),
             });
 
-            var s2 = s1.write(x, Type('C'), h);
+            var s2 = s1.write(x, Type('C'), new SsaNode<Var, Type>(null), h);
             expect(s2.variableInfo, {
               x: _matchVariableModel(
                 chain: ['Object', 'B'],
@@ -2893,7 +3635,7 @@
               ),
             });
 
-            var s2 = s1.write(x, Type('C'), h);
+            var s2 = s1.write(x, Type('C'), new SsaNode<Var, Type>(null), h);
             expect(s2.variableInfo, {
               x: _matchVariableModel(
                 chain: ['Object', 'B'],
@@ -2918,7 +3660,7 @@
               ),
             });
 
-            var s2 = s1.write(x, Type('B'), h);
+            var s2 = s1.write(x, Type('B'), new SsaNode<Var, Type>(null), h);
             expect(s2.variableInfo, {
               x: _matchVariableModel(
                 chain: ['Object', 'A'],
@@ -2943,7 +3685,8 @@
                 ofInterest: ['num?', 'num*'],
               ),
             });
-            var s2 = s1.write(objectQVar, Type('int'), h);
+            var s2 = s1.write(
+                objectQVar, Type('int'), new SsaNode<Var, Type>(null), h);
             // It's ambiguous whether to promote to num? or num*, so we don't
             // promote.
             expect(s2, isNot(same(s1)));
@@ -2970,7 +3713,8 @@
               ofInterest: ['num?', 'num*'],
             ),
           });
-          var s2 = s1.write(objectQVar, Type('num?'), h);
+          var s2 = s1.write(
+              objectQVar, Type('num?'), new SsaNode<Var, Type>(null), h);
           // It's ambiguous whether to promote to num? or num*, but since the
           // written type is exactly num?, we use that.
           expect(s2.variableInfo, {
@@ -3002,7 +3746,7 @@
           ),
         });
 
-        var s2 = s1.write(x, Type('double'), h);
+        var s2 = s1.write(x, Type('double'), new SsaNode<Var, Type>(null), h);
         expect(s2.variableInfo, {
           x: _matchVariableModel(
             chain: ['num?', 'num'],
@@ -3151,8 +3895,12 @@
             .declare(b, false)
             .declare(c, false)
             .declare(d, false);
-        var s1 = s0.write(a, Type('int'), h).write(b, Type('int'), h);
-        var s2 = s1.write(a, Type('int'), h).write(c, Type('int'), h);
+        var s1 = s0
+            .write(a, Type('int'), new SsaNode<Var, Type>(null), h)
+            .write(b, Type('int'), new SsaNode<Var, Type>(null), h);
+        var s2 = s1
+            .write(a, Type('int'), new SsaNode<Var, Type>(null), h)
+            .write(c, Type('int'), new SsaNode<Var, Type>(null), h);
         var result = s2.restrict(h, s1, Set());
         expect(result.infoFor(a).assigned, true);
         expect(result.infoFor(b).assigned, true);
@@ -3312,6 +4060,233 @@
         expect(s1.restrict(h, s0, {x}), same(s0));
       });
     });
+
+    group('rebaseForward', () {
+      test('reachability', () {
+        var h = Harness();
+        var reachable = FlowModel<Var, Type>(Reachability.initial);
+        var unreachable = reachable.setUnreachable();
+        expect(reachable.rebaseForward(h, reachable), same(reachable));
+        expect(reachable.rebaseForward(h, unreachable), same(unreachable));
+        expect(
+            unreachable.rebaseForward(h, reachable).reachable.overallReachable,
+            false);
+        expect(unreachable.rebaseForward(h, reachable).variableInfo,
+            same(unreachable.variableInfo));
+        expect(unreachable.rebaseForward(h, unreachable), same(unreachable));
+      });
+
+      test('assignments', () {
+        var h = Harness();
+        var a = Var('a', 'int');
+        var b = Var('b', 'int');
+        var c = Var('c', 'int');
+        var d = Var('d', 'int');
+        var s0 = FlowModel<Var, Type>(Reachability.initial)
+            .declare(a, false)
+            .declare(b, false)
+            .declare(c, false)
+            .declare(d, false);
+        var s1 = s0
+            .write(a, Type('int'), new SsaNode<Var, Type>(null), h)
+            .write(b, Type('int'), new SsaNode<Var, Type>(null), h);
+        var s2 = s0
+            .write(a, Type('int'), new SsaNode<Var, Type>(null), h)
+            .write(c, Type('int'), new SsaNode<Var, Type>(null), h);
+        var result = s1.rebaseForward(h, s2);
+        expect(result.infoFor(a).assigned, true);
+        expect(result.infoFor(b).assigned, true);
+        expect(result.infoFor(c).assigned, true);
+        expect(result.infoFor(d).assigned, false);
+      });
+
+      test('write captured', () {
+        var h = Harness();
+        var a = Var('a', 'int');
+        var b = Var('b', 'int');
+        var c = Var('c', 'int');
+        var d = Var('d', 'int');
+        var s0 = FlowModel<Var, Type>(Reachability.initial)
+            .declare(a, false)
+            .declare(b, false)
+            .declare(c, false)
+            .declare(d, false);
+        // In s1, a and b are write captured.  In s2, a and c are.
+        var s1 = s0.conservativeJoin([a, b], [a, b]);
+        var s2 = s1.conservativeJoin([a, c], [a, c]);
+        var result = s1.rebaseForward(h, s2);
+        expect(
+          result.infoFor(a),
+          _matchVariableModel(writeCaptured: true, unassigned: false),
+        );
+        expect(
+          result.infoFor(b),
+          _matchVariableModel(writeCaptured: true, unassigned: false),
+        );
+        expect(
+          result.infoFor(c),
+          _matchVariableModel(writeCaptured: true, unassigned: false),
+        );
+        expect(
+          result.infoFor(d),
+          _matchVariableModel(writeCaptured: false, unassigned: true),
+        );
+      });
+
+      test('write captured and promoted', () {
+        var h = Harness();
+        var a = Var('a', 'num');
+        var s0 = FlowModel<Var, Type>(Reachability.initial).declare(a, false);
+        // In s1, a is write captured.  In s2 it's promoted.
+        var s1 = s0.conservativeJoin([a], [a]);
+        var s2 = s0.tryPromoteForTypeCheck(h, a, Type('int')).ifTrue;
+        expect(
+          s1.rebaseForward(h, s2).infoFor(a),
+          _matchVariableModel(writeCaptured: true, chain: isNull),
+        );
+        expect(
+          s2.rebaseForward(h, s1).infoFor(a),
+          _matchVariableModel(writeCaptured: true, chain: isNull),
+        );
+      });
+
+      test('promotion', () {
+        void _check(String? thisType, String? otherType, bool unsafe,
+            List<String>? expectedChain) {
+          var h = Harness();
+          var x = Var('x', 'Object?');
+          var s0 = FlowModel<Var, Type>(Reachability.initial).declare(x, true);
+          var s1 = s0;
+          if (unsafe) {
+            s1 = s1.write(x, Type('Object?'), new SsaNode<Var, Type>(null), h);
+          }
+          if (thisType != null) {
+            s1 = s1.tryPromoteForTypeCheck(h, x, Type(thisType)).ifTrue;
+          }
+          var s2 = otherType == null
+              ? s0
+              : s0.tryPromoteForTypeCheck(h, x, Type(otherType)).ifTrue;
+          var result = s2.rebaseForward(h, s1);
+          if (expectedChain == null) {
+            expect(result.variableInfo, contains(x));
+            expect(result.infoFor(x).promotedTypes, isNull);
+          } else {
+            expect(result.infoFor(x).promotedTypes!.map((t) => t.type).toList(),
+                expectedChain);
+          }
+        }
+
+        _check(null, null, false, null);
+        _check(null, null, true, null);
+        _check('int', null, false, ['int']);
+        _check('int', null, true, ['int']);
+        _check(null, 'int', false, ['int']);
+        _check(null, 'int', true, null);
+        _check('int?', 'int', false, ['int?', 'int']);
+        _check('int', 'int?', false, ['int']);
+        _check('int', 'String', false, ['int']);
+        _check('int?', 'int', true, ['int?']);
+        _check('int', 'int?', true, ['int']);
+        _check('int', 'String', true, ['int']);
+      });
+
+      test('promotion chains', () {
+        // Verify that the given promotion chain matches the expected list of
+        // strings.
+        void _checkChain(List<Type>? chain, List<String> expected) {
+          var strings = (chain ?? <Type>[]).map((t) => t.type).toList();
+          expect(strings, expected);
+        }
+
+        // Test the following scenario:
+        // - Prior to the try/finally block, the sequence of promotions in
+        //   [before] is done.
+        // - During the try block, the sequence of promotions in [inTry] is
+        //   done.
+        // - During the finally block, the sequence of promotions in
+        //   [inFinally] is done.
+        // - After calling `restrict` to refine the state from the finally
+        //   block, the expected promotion chain is [expectedResult].
+        void _check(List<String> before, List<String> inTry,
+            List<String> inFinally, List<String> expectedResult) {
+          var h = Harness();
+          var x = Var('x', 'Object?');
+          var initialModel =
+              FlowModel<Var, Type>(Reachability.initial).declare(x, true);
+          for (var t in before) {
+            initialModel =
+                initialModel.tryPromoteForTypeCheck(h, x, Type(t)).ifTrue;
+          }
+          _checkChain(initialModel.infoFor(x).promotedTypes, before);
+          var tryModel = initialModel;
+          for (var t in inTry) {
+            tryModel = tryModel.tryPromoteForTypeCheck(h, x, Type(t)).ifTrue;
+          }
+          var expectedTryChain = before.toList()..addAll(inTry);
+          _checkChain(tryModel.infoFor(x).promotedTypes, expectedTryChain);
+          var finallyModel = initialModel;
+          for (var t in inFinally) {
+            finallyModel =
+                finallyModel.tryPromoteForTypeCheck(h, x, Type(t)).ifTrue;
+          }
+          var expectedFinallyChain = before.toList()..addAll(inFinally);
+          _checkChain(
+              finallyModel.infoFor(x).promotedTypes, expectedFinallyChain);
+          var result = tryModel.rebaseForward(h, finallyModel);
+          _checkChain(result.infoFor(x).promotedTypes, expectedResult);
+          // And verify that the inputs are unchanged.
+          _checkChain(initialModel.infoFor(x).promotedTypes, before);
+          _checkChain(tryModel.infoFor(x).promotedTypes, expectedTryChain);
+          _checkChain(
+              finallyModel.infoFor(x).promotedTypes, expectedFinallyChain);
+        }
+
+        _check(['Object'], ['num', 'int'], ['Iterable', 'List'],
+            ['Object', 'Iterable', 'List']);
+        _check([], ['num', 'int'], ['Iterable', 'List'], ['Iterable', 'List']);
+        _check(['Object'], [], ['Iterable', 'List'],
+            ['Object', 'Iterable', 'List']);
+        _check([], [], ['Iterable', 'List'], ['Iterable', 'List']);
+        _check(['Object'], ['num', 'int'], [], ['Object', 'num', 'int']);
+        _check([], ['num', 'int'], [], ['num', 'int']);
+        _check(['Object'], [], [], ['Object']);
+        _check([], [], [], []);
+        _check(
+            [], ['num', 'int'], ['Object', 'Iterable'], ['Object', 'Iterable']);
+        _check([], ['num', 'int'], ['Object'], ['Object', 'num', 'int']);
+        _check([], ['Object', 'Iterable'], ['num', 'int'], ['num', 'int']);
+        _check([], ['Object'], ['num', 'int'], ['num', 'int']);
+        _check([], ['num'], ['Object', 'int'], ['Object', 'int']);
+        _check([], ['int'], ['Object', 'num'], ['Object', 'num', 'int']);
+        _check([], ['Object', 'int'], ['num'], ['num', 'int']);
+        _check([], ['Object', 'num'], ['int'], ['int']);
+      });
+
+      test('types of interest', () {
+        var h = Harness();
+        var a = Var('a', 'Object');
+        var s0 = FlowModel<Var, Type>(Reachability.initial).declare(a, false);
+        var s1 = s0.tryPromoteForTypeCheck(h, a, Type('int')).ifFalse;
+        var s2 = s0.tryPromoteForTypeCheck(h, a, Type('String')).ifFalse;
+        expect(
+          s1.rebaseForward(h, s2).infoFor(a),
+          _matchVariableModel(ofInterest: ['int', 'String']),
+        );
+        expect(
+          s2.rebaseForward(h, s1).infoFor(a),
+          _matchVariableModel(ofInterest: ['int', 'String']),
+        );
+      });
+
+      test('variable present in one state but not the other', () {
+        var h = Harness();
+        var x = Var('x', 'Object?');
+        var s0 = FlowModel<Var, Type>(Reachability.initial);
+        var s1 = s0.declare(x, true);
+        expect(s1.rebaseForward(h, s0), same(s0));
+        expect(s0.rebaseForward(h, s1), same(s1));
+      });
+    });
   });
 
   group('joinPromotionChains', () {
@@ -3487,11 +4462,11 @@
     VariableModel<Var, Type> model(List<Type>? promotionChain,
             {List<Type>? typesOfInterest, bool assigned = false}) =>
         VariableModel<Var, Type>(
-            promotionChain,
-            typesOfInterest ?? promotionChain ?? [],
-            assigned,
-            !assigned,
-            new SsaNode());
+            promotedTypes: promotionChain,
+            tested: typesOfInterest ?? promotionChain ?? [],
+            assigned: assigned,
+            unassigned: !assigned,
+            ssaNode: new SsaNode<Var, Type>(null));
 
     group('without input reuse', () {
       test('promoted with unpromoted', () {
@@ -3670,8 +4645,12 @@
 
     VariableModel<Var, Type> varModel(List<Type>? promotionChain,
             {bool assigned = false}) =>
-        VariableModel<Var, Type>(promotionChain, promotionChain ?? [], assigned,
-            !assigned, new SsaNode());
+        VariableModel<Var, Type>(
+            promotedTypes: promotionChain,
+            tested: promotionChain ?? [],
+            assigned: assigned,
+            unassigned: !assigned,
+            ssaNode: new SsaNode<Var, Type>(null));
 
     test('first is null', () {
       var h = Harness();
@@ -3759,7 +4738,11 @@
 
     VariableModel<Var, Type> model(List<Type> typesOfInterest) =>
         VariableModel<Var, Type>(
-            null, typesOfInterest, true, false, new SsaNode());
+            promotedTypes: null,
+            tested: typesOfInterest,
+            assigned: true,
+            unassigned: false,
+            ssaNode: new SsaNode<Var, Type>(null));
 
     test('inherits types of interest from other', () {
       var h = Harness();
diff --git a/pkg/analyzer/lib/src/dart/resolver/assignment_expression_resolver.dart b/pkg/analyzer/lib/src/dart/resolver/assignment_expression_resolver.dart
index ebe7c47..25c3137 100644
--- a/pkg/analyzer/lib/src/dart/resolver/assignment_expression_resolver.dart
+++ b/pkg/analyzer/lib/src/dart/resolver/assignment_expression_resolver.dart
@@ -91,7 +91,7 @@
 
     if (flow != null) {
       if (writeElement is VariableElement) {
-        flow.write(writeElement, node.staticType);
+        flow.write(writeElement, node.staticType, hasRead ? null : right);
       }
       if (isIfNull) {
         flow.ifNullExpression_end();
diff --git a/pkg/analyzer/lib/src/dart/resolver/postfix_expression_resolver.dart b/pkg/analyzer/lib/src/dart/resolver/postfix_expression_resolver.dart
index 2f7d76f..3a8cef1 100644
--- a/pkg/analyzer/lib/src/dart/resolver/postfix_expression_resolver.dart
+++ b/pkg/analyzer/lib/src/dart/resolver/postfix_expression_resolver.dart
@@ -180,7 +180,7 @@
       if (operand is SimpleIdentifier) {
         var element = operand.staticElement;
         if (element is PromotableElement) {
-          _flowAnalysis?.flow?.write(element, operatorReturnType);
+          _flowAnalysis?.flow?.write(element, operatorReturnType, null);
         }
       }
     }
diff --git a/pkg/analyzer/lib/src/dart/resolver/prefix_expression_resolver.dart b/pkg/analyzer/lib/src/dart/resolver/prefix_expression_resolver.dart
index cfaaf15..a88e693 100644
--- a/pkg/analyzer/lib/src/dart/resolver/prefix_expression_resolver.dart
+++ b/pkg/analyzer/lib/src/dart/resolver/prefix_expression_resolver.dart
@@ -217,7 +217,7 @@
         if (operand is SimpleIdentifier) {
           var element = operand.staticElement;
           if (element is PromotableElement) {
-            _flowAnalysis?.flow?.write(element, staticType);
+            _flowAnalysis?.flow?.write(element, staticType, null);
           }
         }
       }
diff --git a/pkg/analyzer/lib/src/generated/resolver.dart b/pkg/analyzer/lib/src/generated/resolver.dart
index ba5b449..fed64ed 100644
--- a/pkg/analyzer/lib/src/generated/resolver.dart
+++ b/pkg/analyzer/lib/src/generated/resolver.dart
@@ -2028,10 +2028,10 @@
           _flowAnalysis?.flow?.promote(
               declaredElement as PromotableElement, initializerStaticType);
         }
-      } else if (!parent.isFinal) {
-        _flowAnalysis?.flow?.write(
-            declaredElement as PromotableElement, initializerStaticType,
-            viaInitializer: true);
+      } else {
+        _flowAnalysis?.flow?.initialize(declaredElement as PromotableElement,
+            initializerStaticType, initializer,
+            isFinal: parent.isFinal, isLate: parent.isLate);
       }
     }
   }
diff --git a/pkg/front_end/lib/src/fasta/kernel/inference_visitor.dart b/pkg/front_end/lib/src/fasta/kernel/inference_visitor.dart
index 2c8ad78..c5f9600 100644
--- a/pkg/front_end/lib/src/fasta/kernel/inference_visitor.dart
+++ b/pkg/front_end/lib/src/fasta/kernel/inference_visitor.dart
@@ -6227,7 +6227,8 @@
         declaredOrInferredType, rhsResult,
         fileOffset: node.fileOffset,
         isVoidAllowed: declaredOrInferredType is VoidType);
-    inferrer.flowAnalysis.write(variable, rhsResult.inferredType);
+    inferrer.flowAnalysis
+        .write(variable, rhsResult.inferredType, rhsResult.expression);
     DartType resultType = rhsResult.inferredType;
     Expression resultExpression;
     if (variable.lateSetter != null) {
@@ -6324,12 +6325,13 @@
         if (initializerType is TypeParameterType) {
           inferrer.flowAnalysis.promote(node, initializerType);
         }
-      } else if (!node.isFinal) {
+      } else {
         // TODO(paulberry): `initializerType` is sometimes `null` during top
         // level inference.  Figure out how to prevent this.
         if (initializerType != null) {
-          inferrer.flowAnalysis
-              .write(node, initializerType, viaInitializer: true);
+          inferrer.flowAnalysis.initialize(
+              node, initializerType, initializerResult.expression,
+              isFinal: node.isFinal, isLate: node.isLate);
         }
       }
       Expression initializer = inferrer.ensureAssignableResult(
@@ -6947,7 +6949,7 @@
         isVoidAllowed: true);
 
     variableSet.value = rhs..parent = variableSet;
-    inferrer.flowAnalysis.write(variableSet.variable, rhsType);
+    inferrer.flowAnalysis.write(variableSet.variable, rhsType, null);
     return variableSet;
   }
 }
diff --git a/pkg/front_end/lib/src/fasta/type_inference/type_inferrer.dart b/pkg/front_end/lib/src/fasta/type_inference/type_inferrer.dart
index fccdf9b..0324ac4 100644
--- a/pkg/front_end/lib/src/fasta/type_inference/type_inferrer.dart
+++ b/pkg/front_end/lib/src/fasta/type_inference/type_inferrer.dart
@@ -625,6 +625,9 @@
             fileOffset, helper.uri);
     }
 
+    if (!identical(result, expression)) {
+      flowAnalysis?.forwardExpression(result, expression);
+    }
     return result;
   }
 
diff --git a/pkg/front_end/test/spell_checking_list_code.txt b/pkg/front_end/test/spell_checking_list_code.txt
index 4cbae00..0e85d97 100644
--- a/pkg/front_end/test/spell_checking_list_code.txt
+++ b/pkg/front_end/test/spell_checking_list_code.txt
@@ -20,6 +20,7 @@
 acon
 acov
 across
+activated
 adi
 affecting
 afterwards
@@ -35,6 +36,7 @@
 allocate
 altered
 analogous
+analogy
 annotate
 ansi
 answering
@@ -327,6 +329,7 @@
 disjoint
 dispatched
 distribute
+diverged
 divided
 dmitryas
 doc
@@ -419,6 +422,7 @@
 fieldformal
 file's
 filenames
+finally's
 finv
 firsts
 fishy
@@ -483,8 +487,10 @@
 gzip
 gzipped
 h
+hacky
 hadn't
 hang
+hardcode
 harness
 hashes
 hashing
@@ -498,6 +504,7 @@
 highlights
 hillerstrom
 histogram
+history
 hit
 hoc
 hopefully
@@ -526,6 +533,7 @@
 implying
 importantly
 imprecise
+improperly
 inapplicable
 inc
 incomparable
@@ -537,6 +545,7 @@
 indirection
 individual
 inequality
+influence
 informative
 infos
 init
@@ -558,6 +567,7 @@
 interleaved
 intermediate
 interpolations
+interrupted
 intersects
 interval
 intervals
@@ -599,6 +609,7 @@
 k’s
 l
 lacks
+lags
 lang
 largest
 lattice
@@ -841,6 +852,7 @@
 preexisted
 preexisting
 preorder
+presubmit
 prev
 prime
 printer
@@ -899,7 +911,10 @@
 react
 realign
 realise
+reapplies
 reassigned
+rebased
+rebasing
 rebind
 rebuild
 rebuilds
@@ -932,6 +947,7 @@
 registering
 rehash
 reindexed
+reinstate
 reissued
 rejoin
 relatively
@@ -962,6 +978,7 @@
 resumed
 ret
 reusage
+rewinds
 rewrites
 rewrote
 rf
@@ -1003,6 +1020,7 @@
 serve
 server
 service
+session
 setaf
 sh
 sha1hash
@@ -1049,6 +1067,7 @@
 sparse
 spec
 spec'ed
+specialization
 specially
 specifics
 speeding
@@ -1270,6 +1289,7 @@
 variant
 variants
 variation
+vars
 vary
 vb
 vector
@@ -1279,6 +1299,7 @@
 vice
 video
 violated
+violating
 visit*
 visitors
 visits
diff --git a/pkg/front_end/test/spell_checking_list_common.txt b/pkg/front_end/test/spell_checking_list_common.txt
index 7806bde..e78f36c 100644
--- a/pkg/front_end/test/spell_checking_list_common.txt
+++ b/pkg/front_end/test/spell_checking_list_common.txt
@@ -2385,6 +2385,7 @@
 reasoning
 reasons
 reassign
+rebase
 receive
 receiver
 recent
diff --git a/pkg/front_end/testcases/late_lowering/late_final_local_without_initializer.dart b/pkg/front_end/testcases/late_lowering/late_final_local_without_initializer.dart
index 31db7ec..65d28e2 100644
--- a/pkg/front_end/testcases/late_lowering/late_final_local_without_initializer.dart
+++ b/pkg/front_end/testcases/late_lowering/late_final_local_without_initializer.dart
@@ -3,7 +3,7 @@
 // BSD-style license that can be found in the LICENSE file.
 
 main() {
-  bool b = false;
+  bool b = (() => false)();
   late final int lateLocal;
 
   if (b) {
diff --git a/pkg/front_end/testcases/late_lowering/late_final_local_without_initializer.dart.strong.expect b/pkg/front_end/testcases/late_lowering/late_final_local_without_initializer.dart.strong.expect
index 2240a48..c5acb90 100644
--- a/pkg/front_end/testcases/late_lowering/late_final_local_without_initializer.dart.strong.expect
+++ b/pkg/front_end/testcases/late_lowering/late_final_local_without_initializer.dart.strong.expect
@@ -4,7 +4,7 @@
 import "dart:_internal" as _in;
 
 static method main() → dynamic {
-  core::bool b = false;
+  core::bool b = (() → core::bool => false).call();
   lowered final core::int? #lateLocal;
   function #lateLocal#get() → core::int
     return let final core::int? #t1 = #lateLocal in #t1.==(null) ?{core::int} throw new _in::LateError::localNI("lateLocal") : #t1{core::int};
diff --git a/pkg/front_end/testcases/late_lowering/late_final_local_without_initializer.dart.strong.transformed.expect b/pkg/front_end/testcases/late_lowering/late_final_local_without_initializer.dart.strong.transformed.expect
index 2240a48..c5acb90 100644
--- a/pkg/front_end/testcases/late_lowering/late_final_local_without_initializer.dart.strong.transformed.expect
+++ b/pkg/front_end/testcases/late_lowering/late_final_local_without_initializer.dart.strong.transformed.expect
@@ -4,7 +4,7 @@
 import "dart:_internal" as _in;
 
 static method main() → dynamic {
-  core::bool b = false;
+  core::bool b = (() → core::bool => false).call();
   lowered final core::int? #lateLocal;
   function #lateLocal#get() → core::int
     return let final core::int? #t1 = #lateLocal in #t1.==(null) ?{core::int} throw new _in::LateError::localNI("lateLocal") : #t1{core::int};
diff --git a/pkg/front_end/testcases/late_lowering/late_final_local_without_initializer.dart.weak.expect b/pkg/front_end/testcases/late_lowering/late_final_local_without_initializer.dart.weak.expect
index 8f08ee7..5976395 100644
--- a/pkg/front_end/testcases/late_lowering/late_final_local_without_initializer.dart.weak.expect
+++ b/pkg/front_end/testcases/late_lowering/late_final_local_without_initializer.dart.weak.expect
@@ -4,7 +4,7 @@
 import "dart:_internal" as _in;
 
 static method main() → dynamic {
-  core::bool b = false;
+  core::bool b = (() → core::bool => false).call();
   lowered final core::int? #lateLocal;
   lowered core::bool #lateLocal#isSet = false;
   function #lateLocal#get() → core::int
diff --git a/pkg/front_end/testcases/late_lowering/late_final_local_without_initializer.dart.weak.transformed.expect b/pkg/front_end/testcases/late_lowering/late_final_local_without_initializer.dart.weak.transformed.expect
index 8f08ee7..5976395 100644
--- a/pkg/front_end/testcases/late_lowering/late_final_local_without_initializer.dart.weak.transformed.expect
+++ b/pkg/front_end/testcases/late_lowering/late_final_local_without_initializer.dart.weak.transformed.expect
@@ -4,7 +4,7 @@
 import "dart:_internal" as _in;
 
 static method main() → dynamic {
-  core::bool b = false;
+  core::bool b = (() → core::bool => false).call();
   lowered final core::int? #lateLocal;
   lowered core::bool #lateLocal#isSet = false;
   function #lateLocal#get() → core::int
diff --git a/pkg/front_end/testcases/nnbd/issue42743.dart b/pkg/front_end/testcases/nnbd/issue42743.dart
index 2ac0239..174904c 100644
--- a/pkg/front_end/testcases/nnbd/issue42743.dart
+++ b/pkg/front_end/testcases/nnbd/issue42743.dart
@@ -3,7 +3,7 @@
 // BSD-style license that can be found in the LICENSE file.
 
 main() async {
-  bool b = true;
+  bool b = (() => true)();
   (_) {
     if (b) return 42;
   };
diff --git a/pkg/front_end/testcases/nnbd/issue42743.dart.strong.expect b/pkg/front_end/testcases/nnbd/issue42743.dart.strong.expect
index ca71c73..1d21b07 100644
--- a/pkg/front_end/testcases/nnbd/issue42743.dart.strong.expect
+++ b/pkg/front_end/testcases/nnbd/issue42743.dart.strong.expect
@@ -4,7 +4,7 @@
 import "dart:async" as asy;
 
 static method main() → dynamic async {
-  core::bool b = true;
+  core::bool b = (() → core::bool => true).call();
   (dynamic _) → core::int? {
     if(b)
       return 42;
diff --git a/pkg/front_end/testcases/nnbd/issue42743.dart.strong.transformed.expect b/pkg/front_end/testcases/nnbd/issue42743.dart.strong.transformed.expect
index e489262..0ab4a75 100644
--- a/pkg/front_end/testcases/nnbd/issue42743.dart.strong.transformed.expect
+++ b/pkg/front_end/testcases/nnbd/issue42743.dart.strong.transformed.expect
@@ -15,7 +15,7 @@
     try {
       #L1:
       {
-        core::bool b = true;
+        core::bool b = (() → core::bool => true).call();
         (dynamic _) → core::int? {
           if(b)
             return 42;
diff --git a/pkg/front_end/testcases/nnbd/issue42743.dart.weak.expect b/pkg/front_end/testcases/nnbd/issue42743.dart.weak.expect
index ca71c73..1d21b07 100644
--- a/pkg/front_end/testcases/nnbd/issue42743.dart.weak.expect
+++ b/pkg/front_end/testcases/nnbd/issue42743.dart.weak.expect
@@ -4,7 +4,7 @@
 import "dart:async" as asy;
 
 static method main() → dynamic async {
-  core::bool b = true;
+  core::bool b = (() → core::bool => true).call();
   (dynamic _) → core::int? {
     if(b)
       return 42;
diff --git a/pkg/front_end/testcases/nnbd/issue42743.dart.weak.transformed.expect b/pkg/front_end/testcases/nnbd/issue42743.dart.weak.transformed.expect
index e489262..0ab4a75 100644
--- a/pkg/front_end/testcases/nnbd/issue42743.dart.weak.transformed.expect
+++ b/pkg/front_end/testcases/nnbd/issue42743.dart.weak.transformed.expect
@@ -15,7 +15,7 @@
     try {
       #L1:
       {
-        core::bool b = true;
+        core::bool b = (() → core::bool => true).call();
         (dynamic _) → core::int? {
           if(b)
             return 42;
diff --git a/pkg/nnbd_migration/lib/src/edge_builder.dart b/pkg/nnbd_migration/lib/src/edge_builder.dart
index 15215b6..58f484b 100644
--- a/pkg/nnbd_migration/lib/src/edge_builder.dart
+++ b/pkg/nnbd_migration/lib/src/edge_builder.dart
@@ -1347,7 +1347,7 @@
       if (operand is SimpleIdentifier) {
         var element = getWriteOrReadElement(operand);
         if (element is PromotableElement) {
-          _flowAnalysis.write(element, writeType);
+          _flowAnalysis.write(element, writeType, null);
         }
       }
       return targetType;
@@ -1398,7 +1398,7 @@
         if (operand is SimpleIdentifier) {
           var element = getWriteOrReadElement(operand);
           if (element is PromotableElement) {
-            _flowAnalysis.write(element, staticType);
+            _flowAnalysis.write(element, staticType, null);
           }
         }
       }
@@ -2342,7 +2342,8 @@
         }
       }
       if (destinationLocalVariable != null) {
-        _flowAnalysis.write(destinationLocalVariable, sourceType);
+        _flowAnalysis.write(destinationLocalVariable, sourceType,
+            compoundOperatorInfo == null ? expression : null);
       }
       if (questionAssignNode != null) {
         _flowAnalysis.ifNullExpression_end();
diff --git a/runtime/platform/utils.h b/runtime/platform/utils.h
index baa4ff4..12bb667 100644
--- a/runtime/platform/utils.h
+++ b/runtime/platform/utils.h
@@ -267,26 +267,29 @@
 
   // Adds two int64_t values with wrapping around
   // (two's complement arithmetic).
-  static inline int64_t AddWithWrapAround(int64_t a, int64_t b) {
+  template <typename T = int64_t>
+  static inline T AddWithWrapAround(T a, T b) {
     // Avoid undefined behavior by doing arithmetic in the unsigned type.
-    return static_cast<int64_t>(static_cast<uint64_t>(a) +
-                                static_cast<uint64_t>(b));
+    using Unsigned = typename std::make_unsigned<T>::type;
+    return static_cast<T>(static_cast<Unsigned>(a) + static_cast<Unsigned>(b));
   }
 
   // Subtracts two int64_t values with wrapping around
   // (two's complement arithmetic).
-  static inline int64_t SubWithWrapAround(int64_t a, int64_t b) {
+  template <typename T = int64_t>
+  static inline T SubWithWrapAround(T a, T b) {
     // Avoid undefined behavior by doing arithmetic in the unsigned type.
-    return static_cast<int64_t>(static_cast<uint64_t>(a) -
-                                static_cast<uint64_t>(b));
+    using Unsigned = typename std::make_unsigned<T>::type;
+    return static_cast<T>(static_cast<Unsigned>(a) - static_cast<Unsigned>(b));
   }
 
   // Multiplies two int64_t values with wrapping around
   // (two's complement arithmetic).
-  static inline int64_t MulWithWrapAround(int64_t a, int64_t b) {
+  template <typename T = int64_t>
+  static inline T MulWithWrapAround(T a, T b) {
     // Avoid undefined behavior by doing arithmetic in the unsigned type.
-    return static_cast<int64_t>(static_cast<uint64_t>(a) *
-                                static_cast<uint64_t>(b));
+    using Unsigned = typename std::make_unsigned<T>::type;
+    return static_cast<T>(static_cast<Unsigned>(a) * static_cast<Unsigned>(b));
   }
 
   // Shifts int64_t value left. Supports any non-negative number of bits and
diff --git a/runtime/vm/code_descriptors.cc b/runtime/vm/code_descriptors.cc
index 6d6ef31..c52c525 100644
--- a/runtime/vm/code_descriptors.cc
+++ b/runtime/vm/code_descriptors.cc
@@ -4,6 +4,7 @@
 
 #include "vm/code_descriptors.h"
 
+#include "platform/utils.h"
 #include "vm/compiler/api/deopt_id.h"
 #include "vm/log.h"
 #include "vm/object_store.h"
@@ -11,6 +12,22 @@
 
 namespace dart {
 
+DescriptorList::DescriptorList(
+    Zone* zone,
+    const GrowableArray<const Function*>* inline_id_to_function)
+    : function_(Function::Handle(
+          zone,
+          FLAG_check_token_positions && (inline_id_to_function != nullptr)
+              ? inline_id_to_function->At(0)->raw()
+              : Function::null())),
+      script_(Script::Handle(
+          zone,
+          function_.IsNull() ? Script::null() : function_.script())),
+      encoded_data_(zone, kInitialStreamSize),
+      prev_pc_offset(0),
+      prev_deopt_id(0),
+      prev_token_pos(0) {}
+
 void DescriptorList::AddDescriptor(PcDescriptorsLayout::Kind kind,
                                    intptr_t pc_offset,
                                    intptr_t deopt_id,
@@ -40,9 +57,30 @@
     prev_pc_offset = pc_offset;
 
     if (!FLAG_precompiled_mode) {
+      if (FLAG_check_token_positions && token_pos.IsReal()) {
+        if (!function_.IsNull() &&
+            !token_pos.IsWithin(function_.token_pos(),
+                                function_.end_token_pos())) {
+          FATAL("Token position %s for PC descriptor %s at offset 0x%" Px
+                " invalid for function %s (%s, %s)",
+                token_pos.ToCString(), PcDescriptorsLayout::KindToCString(kind),
+                pc_offset, function_.ToFullyQualifiedCString(),
+                function_.token_pos().ToCString(),
+                function_.end_token_pos().ToCString());
+        }
+        intptr_t line;
+        if (!script_.IsNull() && !script_.GetTokenLocation(token_pos, &line)) {
+          FATAL("Token position %s for PC descriptor %s at offset 0x%" Px
+                " invalid for script %s of function %s",
+                token_pos.ToCString(), PcDescriptorsLayout::KindToCString(kind),
+                pc_offset, script_.ToCString(),
+                function_.ToFullyQualifiedCString());
+        }
+      }
       const int32_t encoded_pos = token_pos.Serialize();
       encoded_data_.WriteSLEB128(deopt_id - prev_deopt_id);
-      encoded_data_.WriteSLEB128(encoded_pos - prev_token_pos);
+      encoded_data_.WriteSLEB128(
+          Utils::SubWithWrapAround(encoded_pos, prev_token_pos));
       prev_deopt_id = deopt_id;
       prev_token_pos = encoded_pos;
     }
@@ -218,6 +256,7 @@
       inline_id_to_function_(inline_id_to_function),
       inlined_functions_(
           GrowableObjectArray::Handle(GrowableObjectArray::New(Heap::kOld))),
+      script_(Script::Handle(zone)),
       stream_(zone, 64),
       stack_traces_only_(stack_traces_only) {
   buffered_inline_id_stack_.Add(0);
@@ -283,7 +322,8 @@
     return;
   }
   if (inline_id == -1) {
-    // Basic blocking missing an inline_id.
+    // We're missing an inlining ID for some reason: for now, just assume it
+    // should have the current inlining ID.
     return;
   }
 
@@ -327,10 +367,18 @@
   }
 }
 
-void CodeSourceMapBuilder::BeginCodeSourceRange(int32_t pc_offset) {}
+void CodeSourceMapBuilder::BeginCodeSourceRange(int32_t pc_offset,
+                                                intptr_t inline_id,
+                                                const TokenPosition& pos) {
+  if (pos.IsReal() || pos.IsSynthetic()) {
+    // Only record inlining intervals for token positions that might need
+    // to be checked against the appropriate function and/or script.
+    StartInliningInterval(pc_offset, inline_id);
+  }
+}
 
 void CodeSourceMapBuilder::EndCodeSourceRange(int32_t pc_offset,
-                                              TokenPosition pos) {
+                                              const TokenPosition& pos) {
   if (pc_offset == buffered_pc_offset_) {
     return;  // Empty intermediate instruction.
   }
@@ -395,6 +443,39 @@
   return map.raw();
 }
 
+void CodeSourceMapBuilder::BufferChangePosition(TokenPosition pos) {
+  if (FLAG_check_token_positions && pos.IsReal()) {
+    const intptr_t inline_id = buffered_inline_id_stack_.Last();
+    const auto& function = *inline_id_to_function_[inline_id];
+    if (!pos.IsWithin(function.token_pos(), function.end_token_pos())) {
+      TextBuffer buffer(256);
+      buffer.Printf("Token position %s is invalid for function %s (%s, %s)",
+                    pos.ToCString(), function.ToFullyQualifiedCString(),
+                    function.token_pos().ToCString(),
+                    function.end_token_pos().ToCString());
+      if (inline_id > 0) {
+        buffer.Printf(" while compiling function %s",
+                      inline_id_to_function_[0]->ToFullyQualifiedCString());
+      }
+      FATAL("%s", buffer.buffer());
+    }
+    script_ = function.script();
+    intptr_t line;
+    if (!script_.IsNull() && !script_.GetTokenLocation(pos, &line)) {
+      TextBuffer buffer(256);
+      buffer.Printf("Token position %s is invalid for script %s of function %s",
+                    pos.ToCString(), script_.ToCString(),
+                    function.ToFullyQualifiedCString());
+      if (inline_id > 0) {
+        buffer.Printf(" while compiling function %s",
+                      inline_id_to_function_[0]->ToFullyQualifiedCString());
+      }
+      FATAL("%s", buffer.buffer());
+    }
+  }
+  buffered_token_pos_stack_.Last() = pos;
+}
+
 void CodeSourceMapBuilder::WriteChangePosition(TokenPosition pos) {
   stream_.Write<uint8_t>(kChangePosition);
   intptr_t position_or_line = pos.Serialize();
@@ -405,12 +486,10 @@
     // use the value of kNoSource as a fallback when no line or column
     // information is found.
     position_or_line = TokenPosition::kNoSource.Serialize();
-    intptr_t inline_id = buffered_inline_id_stack_.Last();
-    if (inline_id < inline_id_to_function_.length()) {
-      const Function* function = inline_id_to_function_[inline_id];
-      Script& script = Script::Handle(function->script());
-      script.GetTokenLocation(pos, &position_or_line, &column);
-    }
+    const intptr_t inline_id = written_inline_id_stack_.Last();
+    ASSERT(inline_id < inline_id_to_function_.length());
+    script_ = inline_id_to_function_[inline_id]->script();
+    script_.GetTokenLocation(pos, &position_or_line, &column);
   }
 #endif
   stream_.Write<int32_t>(position_or_line);
diff --git a/runtime/vm/code_descriptors.h b/runtime/vm/code_descriptors.h
index 8bc6350..014b900 100644
--- a/runtime/vm/code_descriptors.h
+++ b/runtime/vm/code_descriptors.h
@@ -18,11 +18,9 @@
 
 class DescriptorList : public ZoneAllocated {
  public:
-  explicit DescriptorList(Zone* zone)
-      : encoded_data_(zone, kInitialStreamSize),
-        prev_pc_offset(0),
-        prev_deopt_id(0),
-        prev_token_pos(0) {}
+  explicit DescriptorList(
+      Zone* zone,
+      const GrowableArray<const Function*>* inline_id_to_function = nullptr);
 
   ~DescriptorList() {}
 
@@ -38,6 +36,8 @@
  private:
   static constexpr intptr_t kInitialStreamSize = 64;
 
+  const Function& function_;
+  const Script& script_;
   ZoneWriteStream encoded_data_;
 
   intptr_t prev_pc_offset;
@@ -194,9 +194,10 @@
   static const uint8_t kPopFunction = 3;
   static const uint8_t kNullCheck = 4;
 
-  void StartInliningInterval(int32_t pc_offset, intptr_t inline_id);
-  void BeginCodeSourceRange(int32_t pc_offset);
-  void EndCodeSourceRange(int32_t pc_offset, TokenPosition pos);
+  void BeginCodeSourceRange(int32_t pc_offset,
+                            intptr_t inline_id,
+                            const TokenPosition& token_pos);
+  void EndCodeSourceRange(int32_t pc_offset, const TokenPosition& token_pos);
   void NoteDescriptor(PcDescriptorsLayout::Kind kind,
                       int32_t pc_offset,
                       TokenPosition pos);
@@ -205,12 +206,15 @@
   ArrayPtr InliningIdToFunction();
   CodeSourceMapPtr Finalize();
 
+  const GrowableArray<const Function*>& inline_id_to_function() const {
+    return inline_id_to_function_;
+  }
+
  private:
   intptr_t GetFunctionId(intptr_t inline_id);
+  void StartInliningInterval(int32_t pc_offset, intptr_t inline_id);
 
-  void BufferChangePosition(TokenPosition pos) {
-    buffered_token_pos_stack_.Last() = pos;
-  }
+  void BufferChangePosition(TokenPosition pos);
   void WriteChangePosition(TokenPosition pos);
   void BufferAdvancePC(int32_t distance) { buffered_pc_offset_ += distance; }
   void WriteAdvancePC(int32_t distance) {
@@ -268,6 +272,7 @@
 
   const GrowableObjectArray& inlined_functions_;
 
+  Script& script_;
   ZoneWriteStream stream_;
 
   const bool stack_traces_only_;
diff --git a/runtime/vm/compiler/aot/precompiler.cc b/runtime/vm/compiler/aot/precompiler.cc
index f94e95b..b4f91a6 100644
--- a/runtime/vm/compiler/aot/precompiler.cc
+++ b/runtime/vm/compiler/aot/precompiler.cc
@@ -2561,16 +2561,6 @@
       } else if (optimized()) {
         TIMELINE_DURATION(thread(), CompilerVerbose, "OptimizationPasses");
 
-        pass_state.inline_id_to_function.Add(&function);
-        // We do not add the token position now because we don't know the
-        // position of the inlined call until later. A side effect of this
-        // is that the length of |inline_id_to_function| is always larger
-        // than the length of |inline_id_to_token_pos| by one.
-        // Top scope function has no caller (-1). We do this because we expect
-        // all token positions to be at an inlined call.
-        // Top scope function has no caller (-1).
-        pass_state.caller_inline_id.Add(-1);
-
         AotCallSpecializer call_specializer(precompiler_, flow_graph,
                                             &speculative_policy);
         pass_state.call_specializer = &call_specializer;
diff --git a/runtime/vm/compiler/backend/flow_graph_checker.cc b/runtime/vm/compiler/backend/flow_graph_checker.cc
index 5b753ce..e6f5b05 100644
--- a/runtime/vm/compiler/backend/flow_graph_checker.cc
+++ b/runtime/vm/compiler/backend/flow_graph_checker.cc
@@ -274,6 +274,45 @@
          instruction->deopt_id() != DeoptId::kNone);
 #endif  // !defined(DART_PRECOMPILER)
 
+  // If checking token positions and the flow graph has an inlining ID,
+  // check the inlining ID and token position for instructions with real or
+  // synthetic token positions.
+  if (FLAG_check_token_positions && flow_graph_->inlining_id() >= 0) {
+    const TokenPosition& pos = instruction->token_pos();
+    if (pos.IsReal() || pos.IsSynthetic()) {
+      ASSERT(instruction->has_inlining_id());
+      const intptr_t inlining_id = instruction->inlining_id();
+      const auto& function = *inline_id_to_function_[inlining_id];
+      if (!pos.IsWithin(function.token_pos(), function.end_token_pos())) {
+        TextBuffer buffer(256);
+        buffer.Printf("Token position %s is invalid for function %s (%s, %s)",
+                      pos.ToCString(), function.ToFullyQualifiedCString(),
+                      function.token_pos().ToCString(),
+                      function.end_token_pos().ToCString());
+        if (inlining_id > 0) {
+          buffer.Printf(" while compiling function %s",
+                        inline_id_to_function_[0]->ToFullyQualifiedCString());
+        }
+        FATAL("%s", buffer.buffer());
+      }
+      script_ = function.script();
+      intptr_t line;
+      if (!script_.IsNull() && pos.IsReal() &&
+          !script_.GetTokenLocation(pos, &line)) {
+        TextBuffer buffer(256);
+        buffer.Printf(
+            "Token position %s is invalid for script %s of function %s",
+            pos.ToCString(), script_.ToCString(),
+            function.ToFullyQualifiedCString());
+        if (inlining_id > 0) {
+          buffer.Printf(" while compiling function %s",
+                        inline_id_to_function_[0]->ToFullyQualifiedCString());
+        }
+        FATAL("%s", buffer.buffer());
+      }
+    }
+  }
+
   // Check all regular inputs.
   for (intptr_t i = 0, n = instruction->InputCount(); i < n; ++i) {
     VisitUseDef(instruction, instruction->InputAt(i), i, /*is_env*/ false);
diff --git a/runtime/vm/compiler/backend/flow_graph_checker.h b/runtime/vm/compiler/backend/flow_graph_checker.h
index 0079d98..175fedd 100644
--- a/runtime/vm/compiler/backend/flow_graph_checker.h
+++ b/runtime/vm/compiler/backend/flow_graph_checker.h
@@ -32,9 +32,13 @@
   // Constructs graph checker. The checker uses some custom-made
   // visitation to perform additional checks, and uses the
   // FlowGraphVisitor structure for anything else.
-  explicit FlowGraphChecker(FlowGraph* flow_graph)
+  explicit FlowGraphChecker(
+      FlowGraph* flow_graph,
+      const GrowableArray<const Function*>& inline_id_to_function)
       : FlowGraphVisitor(flow_graph->preorder()),
         flow_graph_(flow_graph),
+        inline_id_to_function_(inline_id_to_function),
+        script_(Script::Handle(flow_graph_->zone())),
         current_block_(nullptr) {}
 
   // Performs a sanity check on the flow graph.
@@ -66,6 +70,8 @@
       PolymorphicInstanceCallInstr* call) override;
 
   FlowGraph* const flow_graph_;
+  const GrowableArray<const Function*>& inline_id_to_function_;
+  Script& script_;
   BlockEntryInstr* current_block_;
 };
 
diff --git a/runtime/vm/compiler/backend/flow_graph_compiler.cc b/runtime/vm/compiler/backend/flow_graph_compiler.cc
index 7962ff2..8ba00e0 100644
--- a/runtime/vm/compiler/backend/flow_graph_compiler.cc
+++ b/runtime/vm/compiler/backend/flow_graph_compiler.cc
@@ -228,7 +228,8 @@
 void FlowGraphCompiler::InitCompiler() {
   compressed_stackmaps_builder_ =
       new (zone()) CompressedStackMapsBuilder(zone());
-  pc_descriptors_list_ = new (zone()) DescriptorList(zone());
+  pc_descriptors_list_ = new (zone()) DescriptorList(
+      zone(), &code_source_map_builder_->inline_id_to_function());
   exception_handlers_list_ = new (zone()) ExceptionHandlerList();
 #if defined(DART_PRECOMPILER)
   catch_entry_moves_maps_builder_ = new (zone()) CatchEntryMovesMapBuilder();
@@ -617,7 +618,7 @@
       }
     }
 
-    BeginCodeSourceRange();
+    BeginCodeSourceRange(entry->inlining_id(), entry->token_pos());
     ASSERT(pending_deoptimization_env_ == NULL);
     pending_deoptimization_env_ = entry->env();
     set_current_instruction(entry);
@@ -639,9 +640,6 @@
       set_current_instruction(instr);
       StatsBegin(instr);
 
-      // Compose intervals.
-      code_source_map_builder_->StartInliningInterval(assembler()->CodeSize(),
-                                                      instr->inlining_id());
       if (FLAG_code_comments || FLAG_disassemble ||
           FLAG_disassemble_optimized) {
         if (FLAG_source_lines) {
@@ -652,7 +650,7 @@
       if (instr->IsParallelMove()) {
         parallel_move_resolver_.EmitNativeCode(instr->AsParallelMove());
       } else {
-        BeginCodeSourceRange();
+        BeginCodeSourceRange(instr->inlining_id(), instr->token_pos());
         EmitInstructionPrologue(instr);
         ASSERT(pending_deoptimization_env_ == NULL);
         pending_deoptimization_env_ = instr->env();
@@ -757,7 +755,8 @@
 #endif  // defined(TARGET_ARCH_ARM) || defined(TARGET_ARCH_ARM64)
     set_current_instruction(slow_path->instruction());
     SpecialStatsBegin(stats_tag);
-    BeginCodeSourceRange();
+    BeginCodeSourceRange(slow_path->instruction()->inlining_id(),
+                         slow_path->instruction()->token_pos());
     DEBUG_ONLY(current_instruction_ = slow_path->instruction());
     slow_path->GenerateCode(this);
     DEBUG_ONLY(current_instruction_ = nullptr);
@@ -766,7 +765,9 @@
     set_current_instruction(nullptr);
   }
   for (intptr_t i = 0; i < deopt_infos_.length(); i++) {
-    BeginCodeSourceRange();
+    // Code generated from CompilerDeoptInfo objects is considered in the
+    // root function.
+    BeginCodeSourceRange(/*inline_id=*/0, TokenPosition::kDeferredDeoptInfo);
 #if defined(TARGET_ARCH_ARM) || defined(TARGET_ARCH_ARM64)
     assembler()->set_lr_state(lr_state);
 #endif  // defined(TARGET_ARCH_ARM) || defined(TARGET_ARCH_ARM64)
@@ -1686,10 +1687,12 @@
 }
 
 ParallelMoveResolver::ParallelMoveResolver(FlowGraphCompiler* compiler)
-    : compiler_(compiler), moves_(32) {}
+    : compiler_(compiler), moves_(32), inlining_id_(-1) {}
 
 void ParallelMoveResolver::EmitNativeCode(ParallelMoveInstr* parallel_move) {
   ASSERT(moves_.is_empty());
+  inlining_id_ = parallel_move->inlining_id();
+
   // Build up a worklist of moves.
   BuildInitialMoveList(parallel_move);
 
@@ -1698,7 +1701,9 @@
     // Skip constants to perform them last.  They don't block other moves
     // and skipping such moves with register destinations keeps those
     // registers free for the whole algorithm.
-    if (!move.IsEliminated() && !move.src().IsConstant()) PerformMove(i);
+    if (!move.IsEliminated() && !move.src().IsConstant()) {
+      PerformMove(i);
+    }
   }
 
   // Perform the moves with constant sources.
@@ -1706,13 +1711,15 @@
     const MoveOperands& move = *moves_[i];
     if (!move.IsEliminated()) {
       ASSERT(move.src().IsConstant());
-      compiler_->BeginCodeSourceRange();
+      compiler_->BeginCodeSourceRange(inlining_id_,
+                                      TokenPosition::kParallelMove);
       EmitMove(i);
       compiler_->EndCodeSourceRange(TokenPosition::kParallelMove);
     }
   }
 
   moves_.Clear();
+  inlining_id_ = -1;
 }
 
 void ParallelMoveResolver::BuildInitialMoveList(
@@ -1782,7 +1789,8 @@
     const MoveOperands& other_move = *moves_[i];
     if (other_move.Blocks(destination)) {
       ASSERT(other_move.IsPending());
-      compiler_->BeginCodeSourceRange();
+      compiler_->BeginCodeSourceRange(inlining_id_,
+                                      TokenPosition::kParallelMove);
       EmitSwap(index);
       compiler_->EndCodeSourceRange(TokenPosition::kParallelMove);
       return;
@@ -1790,7 +1798,7 @@
   }
 
   // This move is not blocked.
-  compiler_->BeginCodeSourceRange();
+  compiler_->BeginCodeSourceRange(inlining_id_, TokenPosition::kParallelMove);
   EmitMove(index);
   compiler_->EndCodeSourceRange(TokenPosition::kParallelMove);
 }
@@ -2067,11 +2075,13 @@
   }
 }
 
-void FlowGraphCompiler::BeginCodeSourceRange() {
-  code_source_map_builder_->BeginCodeSourceRange(assembler()->CodeSize());
+void FlowGraphCompiler::BeginCodeSourceRange(intptr_t inline_id,
+                                             const TokenPosition& token_pos) {
+  code_source_map_builder_->BeginCodeSourceRange(assembler()->CodeSize(),
+                                                 inline_id, token_pos);
 }
 
-void FlowGraphCompiler::EndCodeSourceRange(TokenPosition token_pos) {
+void FlowGraphCompiler::EndCodeSourceRange(const TokenPosition& token_pos) {
   code_source_map_builder_->EndCodeSourceRange(assembler()->CodeSize(),
                                                token_pos);
 }
diff --git a/runtime/vm/compiler/backend/flow_graph_compiler.h b/runtime/vm/compiler/backend/flow_graph_compiler.h
index 0e0815b..13d9fda 100644
--- a/runtime/vm/compiler/backend/flow_graph_compiler.h
+++ b/runtime/vm/compiler/backend/flow_graph_compiler.h
@@ -162,6 +162,8 @@
 
   // List of moves not yet resolved.
   GrowableArray<MoveOperands*> moves_;
+  // Inlining id for the current instruction.
+  intptr_t inlining_id_;
 };
 
 // Used for describing a deoptimization point after call (lazy deoptimization).
@@ -954,8 +956,8 @@
 
   ArrayPtr InliningIdToFunction() const;
 
-  void BeginCodeSourceRange();
-  void EndCodeSourceRange(TokenPosition token_pos);
+  void BeginCodeSourceRange(intptr_t inline_id, const TokenPosition& token_pos);
+  void EndCodeSourceRange(const TokenPosition& token_pos);
 
   static bool LookupMethodFor(int class_id,
                               const String& name,
diff --git a/runtime/vm/compiler/backend/flow_graph_compiler_arm.cc b/runtime/vm/compiler/backend/flow_graph_compiler_arm.cc
index f6f4ece..d1a3a1c 100644
--- a/runtime/vm/compiler/backend/flow_graph_compiler_arm.cc
+++ b/runtime/vm/compiler/backend/flow_graph_compiler_arm.cc
@@ -330,6 +330,9 @@
 }
 
 void FlowGraphCompiler::EmitPrologue() {
+  // Prologue is in the root function.
+  BeginCodeSourceRange(/*inlining_id=*/0, TokenPosition::kDartCodePrologue);
+
   EmitFrameEntry();
   ASSERT(assembler()->constant_pool_allowed());
 
diff --git a/runtime/vm/compiler/backend/flow_graph_compiler_arm64.cc b/runtime/vm/compiler/backend/flow_graph_compiler_arm64.cc
index 92dd916..a4d8403 100644
--- a/runtime/vm/compiler/backend/flow_graph_compiler_arm64.cc
+++ b/runtime/vm/compiler/backend/flow_graph_compiler_arm64.cc
@@ -322,6 +322,9 @@
 }
 
 void FlowGraphCompiler::EmitPrologue() {
+  // Prologue is in the root function.
+  BeginCodeSourceRange(/*inlining_id=*/0, TokenPosition::kDartCodePrologue);
+
   EmitFrameEntry();
   ASSERT(assembler()->constant_pool_allowed());
 
diff --git a/runtime/vm/compiler/backend/flow_graph_compiler_ia32.cc b/runtime/vm/compiler/backend/flow_graph_compiler_ia32.cc
index 0b012e5..63bad46 100644
--- a/runtime/vm/compiler/backend/flow_graph_compiler_ia32.cc
+++ b/runtime/vm/compiler/backend/flow_graph_compiler_ia32.cc
@@ -475,6 +475,9 @@
 }
 
 void FlowGraphCompiler::EmitPrologue() {
+  // Prologue is in the root function.
+  BeginCodeSourceRange(/*inlining_id=*/0, TokenPosition::kDartCodePrologue);
+
   EmitFrameEntry();
 
   // In unoptimized code, initialize (non-argument) stack allocated slots.
diff --git a/runtime/vm/compiler/backend/flow_graph_compiler_x64.cc b/runtime/vm/compiler/backend/flow_graph_compiler_x64.cc
index 29adb56..01d836c 100644
--- a/runtime/vm/compiler/backend/flow_graph_compiler_x64.cc
+++ b/runtime/vm/compiler/backend/flow_graph_compiler_x64.cc
@@ -331,7 +331,8 @@
 }
 
 void FlowGraphCompiler::EmitPrologue() {
-  BeginCodeSourceRange();
+  // Prologue is in the root function.
+  BeginCodeSourceRange(/*inlining_id=*/0, TokenPosition::kDartCodePrologue);
 
   EmitFrameEntry();
   ASSERT(assembler()->constant_pool_allowed());
diff --git a/runtime/vm/compiler/backend/il.h b/runtime/vm/compiler/backend/il.h
index e09dd09..4c26d9d 100644
--- a/runtime/vm/compiler/backend/il.h
+++ b/runtime/vm/compiler/backend/il.h
@@ -1080,6 +1080,7 @@
   intptr_t inlining_id() const { return inlining_id_; }
   void set_inlining_id(intptr_t value) {
     ASSERT(value >= 0);
+    ASSERT_EQUAL(inlining_id_, -1);
     inlining_id_ = value;
   }
   bool has_inlining_id() const { return inlining_id_ >= 0; }
diff --git a/runtime/vm/compiler/backend/il_test_helper.cc b/runtime/vm/compiler/backend/il_test_helper.cc
index 1a514be..a3f5664 100644
--- a/runtime/vm/compiler/backend/il_test_helper.cc
+++ b/runtime/vm/compiler/backend/il_test_helper.cc
@@ -125,15 +125,6 @@
   pass_state_->reorder_blocks = reorder_blocks;
 
   if (optimized) {
-    pass_state_->inline_id_to_function.Add(&function_);
-    // We do not add the token position now because we don't know the
-    // position of the inlined call until later. A side effect of this
-    // is that the length of |inline_id_to_function| is always larger
-    // than the length of |inline_id_to_token_pos| by one.
-    // Top scope function has no caller (-1). We do this because we expect
-    // all token positions to be at an inlined call.
-    pass_state_->caller_inline_id.Add(-1);
-
     JitCallSpecializer jit_call_specializer(flow_graph_, &speculative_policy);
     AotCallSpecializer aot_call_specializer(/*precompiler=*/nullptr,
                                             flow_graph_, &speculative_policy);
diff --git a/runtime/vm/compiler/backend/inliner.cc b/runtime/vm/compiler/backend/inliner.cc
index 7b04675..73ee62c 100644
--- a/runtime/vm/compiler/backend/inliner.cc
+++ b/runtime/vm/compiler/backend/inliner.cc
@@ -1052,7 +1052,12 @@
         {
           callee_graph = builder.BuildGraph();
 #if defined(DEBUG)
-          FlowGraphChecker(callee_graph).Check("Builder (callee)");
+          // The inlining IDs of instructions in the callee graph are unset
+          // until we call SetInliningID later.
+          GrowableArray<const Function*> callee_inline_id_to_function;
+          callee_inline_id_to_function.Add(&function);
+          FlowGraphChecker(callee_graph, callee_inline_id_to_function)
+              .Check("Builder (callee)");
 #endif
           CalleeGraphValidator::Validate(callee_graph);
         }
@@ -1135,7 +1140,12 @@
           callee_graph->ComputeSSA(caller_graph_->max_virtual_register_number(),
                                    param_stubs);
 #if defined(DEBUG)
-          FlowGraphChecker(callee_graph).Check("SSA (callee)");
+          // The inlining IDs of instructions in the callee graph are unset
+          // until we call SetInliningID later.
+          GrowableArray<const Function*> callee_inline_id_to_function;
+          callee_inline_id_to_function.Add(&function);
+          FlowGraphChecker(callee_graph, callee_inline_id_to_function)
+              .Check("SSA (callee)");
 #endif
         }
 
diff --git a/runtime/vm/compiler/compiler_pass.cc b/runtime/vm/compiler/compiler_pass.cc
index 97af823..2a6f91f 100644
--- a/runtime/vm/compiler/compiler_pass.cc
+++ b/runtime/vm/compiler/compiler_pass.cc
@@ -48,6 +48,31 @@
 
 namespace dart {
 
+CompilerPassState::CompilerPassState(
+    Thread* thread,
+    FlowGraph* flow_graph,
+    SpeculativeInliningPolicy* speculative_policy,
+    Precompiler* precompiler)
+    : thread(thread),
+      precompiler(precompiler),
+      inlining_depth(0),
+      sinking(NULL),
+      call_specializer(NULL),
+      speculative_policy(speculative_policy),
+      reorder_blocks(false),
+      sticky_flags(0),
+      flow_graph_(flow_graph) {
+  // Top scope function is at inlining id 0.
+  inline_id_to_function.Add(&flow_graph->parsed_function().function());
+  // Top scope function has no caller (-1).
+  caller_inline_id.Add(-1);
+  // We do not add a token position for the top scope function to
+  // |inline_id_to_token_pos| because it is not (currently) inlined into
+  // another graph at a given token position. A side effect of this is that
+  // the length of |inline_id_to_function| and |caller_inline_id| is always
+  // larger than the length of |inline_id_to_token_pos| by one.
+}
+
 CompilerPass* CompilerPass::passes_[CompilerPass::kNumPasses] = {NULL};
 
 DEFINE_OPTION_HANDLER(CompilerPass::ParseFilters,
@@ -197,7 +222,8 @@
     }
     PrintGraph(state, kTraceAfter, round);
 #if defined(DEBUG)
-    FlowGraphChecker(state->flow_graph()).Check(name());
+    FlowGraphChecker(state->flow_graph(), state->inline_id_to_function)
+        .Check(name());
 #endif
   }
 }
@@ -263,6 +289,7 @@
   if (FLAG_early_round_trip_serialization) {
     INVOKE_PASS(RoundTripSerialization);
   }
+  INVOKE_PASS(SetOuterInliningId);
   INVOKE_PASS(TypePropagation);
   INVOKE_PASS(Canonicalize);
   INVOKE_PASS(BranchSimplify);
diff --git a/runtime/vm/compiler/compiler_pass.h b/runtime/vm/compiler/compiler_pass.h
index 2897609..1904561 100644
--- a/runtime/vm/compiler/compiler_pass.h
+++ b/runtime/vm/compiler/compiler_pass.h
@@ -69,16 +69,7 @@
   CompilerPassState(Thread* thread,
                     FlowGraph* flow_graph,
                     SpeculativeInliningPolicy* speculative_policy,
-                    Precompiler* precompiler = NULL)
-      : thread(thread),
-        precompiler(precompiler),
-        inlining_depth(0),
-        sinking(NULL),
-        call_specializer(NULL),
-        speculative_policy(speculative_policy),
-        reorder_blocks(false),
-        sticky_flags(0),
-        flow_graph_(flow_graph) {}
+                    Precompiler* precompiler = NULL);
 
   FlowGraph* flow_graph() const { return flow_graph_; }
 
diff --git a/runtime/vm/compiler/jit/compiler.cc b/runtime/vm/compiler/jit/compiler.cc
index 9e31bf0..16ec8c5 100644
--- a/runtime/vm/compiler/jit/compiler.cc
+++ b/runtime/vm/compiler/jit/compiler.cc
@@ -587,15 +587,6 @@
       } else if (optimized()) {
         TIMELINE_DURATION(thread(), CompilerVerbose, "OptimizationPasses");
 
-        pass_state.inline_id_to_function.Add(&function);
-        // We do not add the token position now because we don't know the
-        // position of the inlined call until later. A side effect of this
-        // is that the length of |inline_id_to_function| is always larger
-        // than the length of |inline_id_to_token_pos| by one.
-        // Top scope function has no caller (-1). We do this because we expect
-        // all token positions to be at an inlined call.
-        pass_state.caller_inline_id.Add(-1);
-
         JitCallSpecializer call_specializer(flow_graph, &speculative_policy);
         pass_state.call_specializer = &call_specializer;
 
diff --git a/runtime/vm/flag_list.h b/runtime/vm/flag_list.h
index 36303f8..087b781 100644
--- a/runtime/vm/flag_list.h
+++ b/runtime/vm/flag_list.h
@@ -90,6 +90,8 @@
     "-1 means never.")                                                         \
   P(background_compilation, bool, kDartUseBackgroundCompilation,               \
     "Run optimizing compilation in background")                                \
+  P(check_token_positions, bool, false,                                        \
+    "Check validity of token positions while compiling flow graphs")           \
   R(code_comments, false, bool, false,                                         \
     "Include comments into code and disassembly.")                             \
   P(collect_code, bool, false, "Attempt to GC infrequently used code.")        \
diff --git a/runtime/vm/object.h b/runtime/vm/object.h
index ae41899..d789af2 100644
--- a/runtime/vm/object.h
+++ b/runtime/vm/object.h
@@ -5594,7 +5594,8 @@
 
         if (!FLAG_precompiled_mode) {
           cur_deopt_id_ += stream.ReadSLEB128();
-          cur_token_pos_ += stream.ReadSLEB128<int32_t>();
+          cur_token_pos_ = Utils::AddWithWrapAround(
+              cur_token_pos_, stream.ReadSLEB128<int32_t>());
         }
         byte_index_ = stream.Position();
 
diff --git a/sdk/lib/async/future_impl.dart b/sdk/lib/async/future_impl.dart
index 335aedf..5b3b77b 100644
--- a/sdk/lib/async/future_impl.dart
+++ b/sdk/lib/async/future_impl.dart
@@ -846,5 +846,5 @@
       errorHandler,
       "onError",
       "Error handler must accept one Object or one Object and a StackTrace"
-          " as arguments, and return a a valid result");
+          " as arguments, and return a valid result");
 }
diff --git a/tools/VERSION b/tools/VERSION
index 3b233a1..45bf2ca 100644
--- a/tools/VERSION
+++ b/tools/VERSION
@@ -27,5 +27,5 @@
 MAJOR 2
 MINOR 12
 PATCH 0
-PRERELEASE 164
+PRERELEASE 165
 PRERELEASE_PATCH 0
\ No newline at end of file