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