blob: 979be2e1d9caf6df72e45d256320d6595d01f91c [file] [log] [blame]
// Copyright (c) 2019, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
import 'package:_fe_analyzer_shared/src/flow_analysis/flow_analysis.dart';
import 'package:meta/meta.dart';
import 'package:test/test.dart';
main() {
group('API', () {
test('asExpression_end promotes variables', () {
var h = _Harness();
var x = h.addVar('x', 'int?');
h.run((flow) {
h.declare(x, initialized: true);
var expr = _Expression();
flow.variableRead(expr, x);
flow.asExpression_end(expr, _Type('int'));
expect(flow.promotedType(x).type, 'int');
});
});
test('asExpression_end handles other expressions', () {
var h = _Harness();
h.run((flow) {
var expr = _Expression();
flow.asExpression_end(expr, _Type('int'));
});
});
test('assert_afterCondition promotes', () {
var h = _Harness();
var x = h.addVar('x', 'int?');
h.run((flow) {
flow.assert_begin();
var expr = h.eqNull(x, _Type('int?'))();
flow.assert_afterCondition(expr);
expect(flow.promotedType(x).type, 'int');
flow.assert_end();
});
});
test('assert_end joins previous and ifTrue states', () {
var h = _Harness();
var x = h.addVar('x', 'int?');
var y = h.addVar('x', 'int?');
var z = h.addVar('x', 'int?');
h.assignedVariables((vars) {
vars.write(x);
vars.write(z);
});
h.run((flow) {
h.promote(x, 'int');
h.promote(z, 'int');
flow.assert_begin();
flow.write(x, _Type('int?'));
flow.write(z, _Type('int?'));
var expr =
h.and(h.notNull(x, _Type('int?')), h.notNull(y, _Type('int?')))();
flow.assert_afterCondition(expr);
flow.assert_end();
// x should be promoted because it was promoted before the assert, and
// it is re-promoted within the assert (if it passes)
expect(flow.promotedType(x).type, 'int');
// y should not be promoted because it was not promoted before the
// assert.
expect(flow.promotedType(y), null);
// z should not be promoted because it is demoted in the assert
// condition.
expect(flow.promotedType(z), null);
});
});
test('conditional_thenBegin promotes true branch', () {
var h = _Harness();
var x = h.addVar('x', 'int?');
h.run((flow) {
h.declare(x, initialized: true);
flow.conditional_conditionBegin();
flow.conditional_thenBegin(h.notNull(x, _Type('int?'))());
expect(flow.promotedType(x).type, 'int');
flow.conditional_elseBegin(_Expression());
expect(flow.promotedType(x), isNull);
flow.conditional_end(_Expression(), _Expression());
expect(flow.promotedType(x), isNull);
});
});
test('conditional_elseBegin promotes false branch', () {
var h = _Harness();
var x = h.addVar('x', 'int?');
h.run((flow) {
h.declare(x, initialized: true);
flow.conditional_conditionBegin();
flow.conditional_thenBegin(h.eqNull(x, _Type('int?'))());
expect(flow.promotedType(x), isNull);
flow.conditional_elseBegin(_Expression());
expect(flow.promotedType(x).type, 'int');
flow.conditional_end(_Expression(), _Expression());
expect(flow.promotedType(x), isNull);
});
});
test('conditional_end keeps promotions common to true and false branches',
() {
var h = _Harness();
var x = h.addVar('x', 'int?');
var y = h.addVar('y', 'int?');
var z = h.addVar('z', 'int?');
h.run((flow) {
h.declare(x, initialized: true);
h.declare(y, initialized: true);
h.declare(z, initialized: true);
flow.conditional_conditionBegin();
flow.conditional_thenBegin(_Expression());
h.promote(x, 'int');
h.promote(y, 'int');
flow.conditional_elseBegin(_Expression());
h.promote(x, 'int');
h.promote(z, 'int');
flow.conditional_end(_Expression(), _Expression());
expect(flow.promotedType(x).type, 'int');
expect(flow.promotedType(y), isNull);
expect(flow.promotedType(z), isNull);
});
});
test('conditional joins true states', () {
// if (... ? (x != null && y != null) : (x != null && z != null)) {
// promotes x, but not y or z
// }
var h = _Harness();
var x = h.addVar('x', 'int?');
var y = h.addVar('y', 'int?');
var z = h.addVar('z', 'int?');
h.run((flow) {
h.declare(x, initialized: true);
h.declare(y, initialized: true);
h.declare(z, initialized: true);
h.if_(
h.conditional(
h.expr,
h.and(h.notNull(x, _Type('int?')), h.notNull(y, _Type('int?'))),
h.and(
h.notNull(x, _Type('int?')), h.notNull(z, _Type('int?')))),
() {
expect(flow.promotedType(x).type, 'int');
expect(flow.promotedType(y), isNull);
expect(flow.promotedType(z), isNull);
});
});
});
test('conditional joins false states', () {
// if (... ? (x == null || y == null) : (x == null || z == null)) {
// } else {
// promotes x, but not y or z
// }
var h = _Harness();
var x = h.addVar('x', 'int?');
var y = h.addVar('y', 'int?');
var z = h.addVar('z', 'int?');
h.run((flow) {
h.declare(x, initialized: true);
h.declare(y, initialized: true);
h.declare(z, initialized: true);
h.ifElse(
h.conditional(
h.expr,
h.or(h.eqNull(x, _Type('int?')), h.eqNull(y, _Type('int?'))),
h.or(h.eqNull(x, _Type('int?')), h.eqNull(z, _Type('int?')))),
() {}, () {
expect(flow.promotedType(x).type, 'int');
expect(flow.promotedType(y), isNull);
expect(flow.promotedType(z), isNull);
});
});
});
test('equalityOp(x != null) promotes true branch', () {
var h = _Harness();
var x = h.addVar('x', 'int?');
h.run((flow) {
h.declare(x, initialized: true);
flow.ifStatement_conditionBegin();
var varExpr = _Expression();
flow.variableRead(varExpr, x);
flow.equalityOp_rightBegin(varExpr, _Type('int?'));
var nullExpr = _Expression();
flow.nullLiteral(nullExpr);
var expr = _Expression();
var successIsReachable =
flow.equalityOp_end(expr, nullExpr, _Type('Null'), notEqual: true);
expect(successIsReachable, true);
flow.ifStatement_thenBegin(expr);
expect(flow.isReachable, true);
expect(flow.promotedType(x).type, 'int');
flow.ifStatement_elseBegin();
expect(flow.isReachable, true);
expect(flow.promotedType(x), isNull);
flow.ifStatement_end(true);
});
});
test('equalityOp(x != null) when x is non-nullable', () {
var h = _Harness();
var x = h.addVar('x', 'int');
h.run((flow) {
h.declare(x, initialized: true);
flow.ifStatement_conditionBegin();
var varExpr = _Expression();
flow.variableRead(varExpr, x);
flow.equalityOp_rightBegin(varExpr, _Type('int'));
var nullExpr = _Expression();
flow.nullLiteral(nullExpr);
var expr = _Expression();
var successIsReachable =
flow.equalityOp_end(expr, nullExpr, _Type('Null'), notEqual: true);
// The `== null` case is considered reachable due to mixed mode
// unsoundness
expect(successIsReachable, true);
flow.ifStatement_thenBegin(expr);
expect(flow.isReachable, true);
expect(flow.promotedType(x), isNull);
flow.ifStatement_elseBegin();
expect(flow.isReachable, true);
expect(flow.promotedType(x), isNull);
flow.ifStatement_end(true);
});
});
test('equalityOp(<expr> == <expr>) has no special effect', () {
var h = _Harness();
h.run((flow) {
flow.ifStatement_conditionBegin();
flow.equalityOp_rightBegin(_Expression(), _Type('int?'));
var expr = _Expression();
var successIsReachable = flow.equalityOp_end(
expr, _Expression(), _Type('int?'),
notEqual: false);
expect(successIsReachable, true);
flow.ifStatement_thenBegin(expr);
expect(flow.isReachable, true);
flow.ifStatement_elseBegin();
expect(flow.isReachable, true);
flow.ifStatement_end(true);
});
});
test('equalityOp(<expr> != <expr>) has no special effect', () {
var h = _Harness();
h.run((flow) {
flow.ifStatement_conditionBegin();
flow.equalityOp_rightBegin(_Expression(), _Type('int?'));
var expr = _Expression();
var successIsReachable = flow
.equalityOp_end(expr, _Expression(), _Type('int?'), notEqual: true);
expect(successIsReachable, true);
flow.ifStatement_thenBegin(expr);
expect(flow.isReachable, true);
flow.ifStatement_elseBegin();
expect(flow.isReachable, true);
flow.ifStatement_end(true);
});
});
test('equalityOp(x != <null expr>) does not promote', () {
var h = _Harness();
var x = h.addVar('x', 'int?');
h.run((flow) {
h.declare(x, initialized: true);
flow.ifStatement_conditionBegin();
var varExpr = _Expression();
flow.variableRead(varExpr, x);
flow.equalityOp_rightBegin(varExpr, _Type('int?'));
var nullExpr = _Expression();
var expr = _Expression();
flow.equalityOp_end(expr, nullExpr, _Type('Null'), notEqual: true);
flow.ifStatement_thenBegin(expr);
expect(flow.promotedType(x), isNull);
flow.ifStatement_elseBegin();
expect(flow.promotedType(x), isNull);
flow.ifStatement_end(true);
});
});
test('equalityOp(x == null) promotes false branch', () {
var h = _Harness();
var x = h.addVar('x', 'int?');
h.run((flow) {
h.declare(x, initialized: true);
flow.ifStatement_conditionBegin();
var varExpr = _Expression();
flow.variableRead(varExpr, x);
flow.equalityOp_rightBegin(varExpr, _Type('int?'));
var nullExpr = _Expression();
flow.nullLiteral(nullExpr);
var expr = _Expression();
var successIsReachable =
flow.equalityOp_end(expr, nullExpr, _Type('Null'), notEqual: false);
expect(successIsReachable, true);
flow.ifStatement_thenBegin(expr);
expect(flow.isReachable, true);
expect(flow.promotedType(x), isNull);
flow.ifStatement_elseBegin();
expect(flow.isReachable, true);
expect(flow.promotedType(x).type, 'int');
flow.ifStatement_end(true);
});
});
test('equalityOp(x == null) when x is non-nullable', () {
var h = _Harness();
var x = h.addVar('x', 'int');
h.run((flow) {
h.declare(x, initialized: true);
flow.ifStatement_conditionBegin();
var varExpr = _Expression();
flow.variableRead(varExpr, x);
flow.equalityOp_rightBegin(varExpr, _Type('int'));
var nullExpr = _Expression();
flow.nullLiteral(nullExpr);
var expr = _Expression();
var successIsReachable =
flow.equalityOp_end(expr, nullExpr, _Type('Null'), notEqual: false);
// The `== null` case is considered reachable due to mixed mode
// unsoundness
expect(successIsReachable, true);
flow.ifStatement_thenBegin(expr);
expect(flow.isReachable, true);
expect(flow.promotedType(x), isNull);
flow.ifStatement_elseBegin();
expect(flow.isReachable, true);
expect(flow.promotedType(x), isNull);
flow.ifStatement_end(true);
});
});
test('equalityOp(null != x) promotes true branch', () {
var h = _Harness();
var x = h.addVar('x', 'int?');
h.run((flow) {
h.declare(x, initialized: true);
flow.ifStatement_conditionBegin();
var nullExpr = _Expression();
flow.nullLiteral(nullExpr);
flow.equalityOp_rightBegin(nullExpr, _Type('Null'));
var varExpr = _Expression();
flow.variableRead(varExpr, x);
var expr = _Expression();
flow.equalityOp_end(expr, varExpr, _Type('int?'), notEqual: true);
flow.ifStatement_thenBegin(expr);
expect(flow.promotedType(x).type, 'int');
flow.ifStatement_elseBegin();
expect(flow.promotedType(x), isNull);
flow.ifStatement_end(true);
});
});
test('equalityOp(<null expr> != x) does not promote', () {
var h = _Harness();
var x = h.addVar('x', 'int?');
h.run((flow) {
h.declare(x, initialized: true);
flow.ifStatement_conditionBegin();
var nullExpr = _Expression();
flow.equalityOp_rightBegin(nullExpr, _Type('Null'));
var varExpr = _Expression();
flow.variableRead(varExpr, x);
var expr = _Expression();
flow.equalityOp_end(expr, varExpr, _Type('int?'), notEqual: true);
flow.ifStatement_thenBegin(expr);
expect(flow.promotedType(x), isNull);
flow.ifStatement_elseBegin();
expect(flow.promotedType(x), isNull);
flow.ifStatement_end(true);
});
});
test('equalityOp(null == x) promotes false branch', () {
var h = _Harness();
var x = h.addVar('x', 'int?');
h.run((flow) {
h.declare(x, initialized: true);
flow.ifStatement_conditionBegin();
var nullExpr = _Expression();
flow.nullLiteral(nullExpr);
flow.equalityOp_rightBegin(nullExpr, _Type('Null'));
var varExpr = _Expression();
flow.variableRead(varExpr, x);
var expr = _Expression();
flow.equalityOp_end(expr, varExpr, _Type('int?'), notEqual: false);
flow.ifStatement_thenBegin(expr);
expect(flow.promotedType(x), isNull);
flow.ifStatement_elseBegin();
expect(flow.promotedType(x).type, 'int');
flow.ifStatement_end(true);
});
});
test('equalityOp(null == null) equivalent to true', () {
var h = _Harness();
h.run((flow) {
flow.ifStatement_conditionBegin();
var null1 = _Expression();
flow.equalityOp_rightBegin(null1, _Type('Null'));
var null2 = _Expression();
var expr = _Expression();
var successIsReachable =
flow.equalityOp_end(expr, null2, _Type('Null'));
expect(successIsReachable, true);
flow.ifStatement_thenBegin(expr);
expect(flow.isReachable, true);
flow.ifStatement_elseBegin();
expect(flow.isReachable, false);
flow.ifStatement_end(true);
});
});
test('equalityOp(null != null) equivalent to false', () {
var h = _Harness();
h.run((flow) {
flow.ifStatement_conditionBegin();
var null1 = _Expression();
flow.equalityOp_rightBegin(null1, _Type('Null'));
var null2 = _Expression();
var expr = _Expression();
var successIsReachable =
flow.equalityOp_end(expr, null2, _Type('Null'), notEqual: true);
expect(successIsReachable, true);
flow.ifStatement_thenBegin(expr);
expect(flow.isReachable, false);
flow.ifStatement_elseBegin();
expect(flow.isReachable, true);
flow.ifStatement_end(true);
});
});
test('equalityOp(null == non-null) is not equivalent to false', () {
var h = _Harness();
h.run((flow) {
flow.ifStatement_conditionBegin();
var null1 = _Expression();
flow.equalityOp_rightBegin(null1, _Type('Null'));
var null2 = _Expression();
var expr = _Expression();
var successIsReachable = flow.equalityOp_end(expr, null2, _Type('int'));
// The `== null` case is considered reachable due to mixed mode
// unsoundness
expect(successIsReachable, true);
flow.ifStatement_thenBegin(expr);
expect(flow.isReachable, true);
flow.ifStatement_elseBegin();
expect(flow.isReachable, true);
flow.ifStatement_end(true);
});
});
test('equalityOp(null != non-null) is not equivalent to true', () {
var h = _Harness();
h.run((flow) {
flow.ifStatement_conditionBegin();
var null1 = _Expression();
flow.equalityOp_rightBegin(null1, _Type('Null'));
var null2 = _Expression();
var expr = _Expression();
var successIsReachable =
flow.equalityOp_end(expr, null2, _Type('int'), notEqual: true);
// The `== null` case is considered reachable due to mixed mode
// unsoundness
expect(successIsReachable, true);
flow.ifStatement_thenBegin(expr);
expect(flow.isReachable, true);
flow.ifStatement_elseBegin();
expect(flow.isReachable, true);
flow.ifStatement_end(true);
});
});
test('equalityOp(non-null == null) is not equivalent to false', () {
var h = _Harness();
h.run((flow) {
flow.ifStatement_conditionBegin();
var null1 = _Expression();
flow.equalityOp_rightBegin(null1, _Type('int'));
var null2 = _Expression();
var expr = _Expression();
var successIsReachable =
flow.equalityOp_end(expr, null2, _Type('Null'));
// The `== null` case is considered reachable due to mixed mode
// unsoundness
expect(successIsReachable, true);
flow.ifStatement_thenBegin(expr);
expect(flow.isReachable, true);
flow.ifStatement_elseBegin();
expect(flow.isReachable, true);
flow.ifStatement_end(true);
});
});
test('equalityOp(non-null != null) is not equivalent to true', () {
var h = _Harness();
h.run((flow) {
flow.ifStatement_conditionBegin();
var null1 = _Expression();
flow.equalityOp_rightBegin(null1, _Type('int'));
var null2 = _Expression();
var expr = _Expression();
var successIsReachable =
flow.equalityOp_end(expr, null2, _Type('Null'), notEqual: true);
// The `== null` case is considered reachable due to mixed mode
// unsoundness
expect(successIsReachable, true);
flow.ifStatement_thenBegin(expr);
expect(flow.isReachable, true);
flow.ifStatement_elseBegin();
expect(flow.isReachable, true);
flow.ifStatement_end(true);
});
});
test('conditionEqNull() does not promote write-captured vars', () {
var h = _Harness();
var x = h.addVar('x', 'int?');
var functionNode = _Node();
h.assignedVariables(
(vars) => vars.function(functionNode, () => vars.write(x)));
h.run((flow) {
h.declare(x, initialized: true);
h.if_(h.notNull(x, _Type('int?')), () {
expect(flow.promotedType(x).type, 'int');
});
h.function(functionNode, () {
flow.write(x, _Type('int?'));
});
h.if_(h.notNull(x, _Type('int?')), () {
expect(flow.promotedType(x), isNull);
});
});
});
test('doStatement_bodyBegin() un-promotes', () {
var h = _Harness();
var x = h.addVar('x', 'int?');
var doStatement = _Statement();
h.assignedVariables(
(vars) => vars.nest(doStatement, () => vars.write(x)));
h.run((flow) {
h.declare(x, initialized: true);
h.promote(x, 'int');
expect(flow.promotedType(x).type, 'int');
flow.doStatement_bodyBegin(doStatement);
expect(flow.promotedType(x), isNull);
flow.doStatement_conditionBegin();
flow.doStatement_end(_Expression());
});
});
test('doStatement_bodyBegin() handles write captures in the loop', () {
var h = _Harness();
var x = h.addVar('x', 'int?');
var doStatement = _Statement();
var functionNode = _Node();
h.assignedVariables((vars) => vars.nest(
doStatement, () => vars.function(functionNode, () => vars.write(x))));
h.run((flow) {
h.declare(x, initialized: true);
flow.doStatement_bodyBegin(doStatement);
h.promote(x, 'int');
// The promotion should have no effect, because the second time through
// the loop, x has been write-captured.
expect(flow.promotedType(x), isNull);
h.function(functionNode, () {
flow.write(x, _Type('int?'));
});
flow.doStatement_conditionBegin();
flow.doStatement_end(_Expression());
});
});
test('doStatement_conditionBegin() joins continue state', () {
var h = _Harness();
var x = h.addVar('x', 'int?');
var stmt = _Statement();
h.assignedVariables((vars) => vars.nest(stmt, () {}));
h.run((flow) {
h.declare(x, initialized: true);
flow.doStatement_bodyBegin(stmt);
h.if_(h.notNull(x, _Type('int?')), () {
flow.handleContinue(stmt);
});
flow.handleExit();
expect(flow.isReachable, false);
expect(flow.promotedType(x), isNull);
flow.doStatement_conditionBegin();
expect(flow.isReachable, true);
expect(flow.promotedType(x).type, 'int');
flow.doStatement_end(_Expression());
});
});
test('doStatement_end() promotes', () {
var h = _Harness();
var x = h.addVar('x', 'int?');
var stmt = _Statement();
h.assignedVariables((vars) => vars.nest(stmt, () {}));
h.run((flow) {
h.declare(x, initialized: true);
flow.doStatement_bodyBegin(stmt);
flow.doStatement_conditionBegin();
expect(flow.promotedType(x), isNull);
flow.doStatement_end(h.eqNull(x, _Type('int?'))());
expect(flow.promotedType(x).type, 'int');
});
});
test('finish checks proper nesting', () {
var h = _Harness();
var expr = _Expression();
var flow = h.createFlow();
flow.ifStatement_conditionBegin();
flow.ifStatement_thenBegin(expr);
expect(() => flow.finish(), _asserts);
});
test('for_conditionBegin() un-promotes', () {
var h = _Harness();
var x = h.addVar('x', 'int?');
var forStatement = _Statement();
h.assignedVariables(
(vars) => vars.nest(forStatement, () => vars.write(x)));
h.run((flow) {
h.declare(x, initialized: true);
h.promote(x, 'int');
expect(flow.promotedType(x).type, 'int');
flow.for_conditionBegin(forStatement);
expect(flow.promotedType(x), isNull);
flow.for_bodyBegin(_Statement(), _Expression());
flow.write(x, _Type('int?'));
flow.for_updaterBegin();
flow.for_end();
});
});
test('for_conditionBegin() handles write captures in the loop', () {
var h = _Harness();
var x = h.addVar('x', 'int?');
var forStatement = _Statement();
var functionNode = _Node();
h.assignedVariables((vars) => vars.nest(forStatement,
() => vars.function(functionNode, () => vars.write(x))));
h.run((flow) {
h.declare(x, initialized: true);
h.promote(x, 'int');
expect(flow.promotedType(x).type, 'int');
flow.for_conditionBegin(forStatement);
h.promote(x, 'int');
expect(flow.promotedType(x), isNull);
h.function(functionNode, () {
flow.write(x, _Type('int?'));
});
flow.for_bodyBegin(_Statement(), _Expression());
flow.for_updaterBegin();
flow.for_end();
});
});
test('for_conditionBegin() handles not-yet-seen variables', () {
var h = _Harness();
var x = h.addVar('x', 'int?');
var y = h.addVar('y', 'int?');
var forStatement = _Statement();
h.assignedVariables(
(vars) => vars.nest(forStatement, () => vars.write(x)));
h.run((flow) {
h.declare(y, initialized: true);
h.promote(y, 'int');
flow.for_conditionBegin(forStatement);
flow.declare(x, true);
flow.for_bodyBegin(_Statement(), _Expression());
flow.for_updaterBegin();
flow.for_end();
});
});
test('for_bodyBegin() handles empty condition', () {
var h = _Harness();
var stmt = _Statement();
h.assignedVariables((vars) => vars.nest(stmt, () {}));
h.run((flow) {
flow.for_conditionBegin(stmt);
flow.for_bodyBegin(stmt, null);
flow.for_updaterBegin();
expect(flow.isReachable, isTrue);
flow.for_end();
expect(flow.isReachable, isFalse);
});
});
test('for_bodyBegin() promotes', () {
var h = _Harness();
var x = h.addVar('x', 'int?');
var stmt = _Statement();
h.assignedVariables((vars) => vars.nest(stmt, () {}));
h.run((flow) {
h.declare(x, initialized: true);
flow.for_conditionBegin(stmt);
flow.for_bodyBegin(stmt, h.notNull(x, _Type('int?'))());
expect(flow.promotedType(x).type, 'int');
flow.for_updaterBegin();
flow.for_end();
});
});
test('for_bodyBegin() can be used with a null statement', () {
// This is needed for collection elements that are for-loops.
var h = _Harness();
var x = h.addVar('x', 'int?');
var node = _Node();
h.assignedVariables((vars) => vars.nest(node, () {}));
h.run((flow) {
h.declare(x, initialized: true);
flow.for_conditionBegin(node);
flow.for_bodyBegin(null, h.notNull(x, _Type('int?'))());
flow.for_updaterBegin();
flow.for_end();
});
});
test('for_updaterBegin() joins current and continue states', () {
// To test that the states are properly joined, we have three variables:
// x, y, and z. We promote x and y in the continue path, and x and z in
// the current path. Inside the updater, only x should be promoted.
var h = _Harness();
var x = h.addVar('x', 'int?');
var y = h.addVar('y', 'int?');
var z = h.addVar('z', 'int?');
var stmt = _Statement();
h.assignedVariables((vars) => vars.nest(stmt, () {}));
h.run((flow) {
h.declare(x, initialized: true);
h.declare(y, initialized: true);
h.declare(z, initialized: true);
flow.for_conditionBegin(stmt);
flow.for_bodyBegin(stmt, h.expr());
h.if_(h.expr, () {
h.promote(x, 'int');
h.promote(y, 'int');
flow.handleContinue(stmt);
});
h.promote(x, 'int');
h.promote(z, 'int');
flow.for_updaterBegin();
expect(flow.promotedType(x).type, 'int');
expect(flow.promotedType(y), isNull);
expect(flow.promotedType(z), isNull);
flow.for_end();
});
});
test('for_end() joins break and condition-false states', () {
// To test that the states are properly joined, we have three variables:
// x, y, and z. We promote x and y in the break path, and x and z in the
// condition-false path. After the loop, only x should be promoted.
var h = _Harness();
var x = h.addVar('x', 'int?');
var y = h.addVar('y', 'int?');
var z = h.addVar('z', 'int?');
var stmt = _Statement();
h.assignedVariables((vars) => vars.nest(stmt, () {}));
h.run((flow) {
h.declare(x, initialized: true);
h.declare(y, initialized: true);
h.declare(z, initialized: true);
flow.for_conditionBegin(stmt);
flow.for_bodyBegin(stmt,
h.or(h.eqNull(x, _Type('int?')), h.eqNull(z, _Type('int?')))());
h.if_(h.expr, () {
h.promote(x, 'int');
h.promote(y, 'int');
flow.handleBreak(stmt);
});
flow.for_updaterBegin();
flow.for_end();
expect(flow.promotedType(x).type, 'int');
expect(flow.promotedType(y), isNull);
expect(flow.promotedType(z), isNull);
});
});
test('forEach_bodyBegin() un-promotes', () {
var h = _Harness();
var x = h.addVar('x', 'int?');
var forStatement = _Statement();
h.assignedVariables(
(vars) => vars.nest(forStatement, () => vars.write(x)));
h.run((flow) {
h.declare(x, initialized: true);
h.promote(x, 'int');
expect(flow.promotedType(x).type, 'int');
flow.forEach_bodyBegin(forStatement, null, _Type('int?'));
expect(flow.promotedType(x), isNull);
flow.write(x, _Type('int?'));
flow.forEach_end();
});
});
test('forEach_bodyBegin() handles write captures in the loop', () {
var h = _Harness();
var x = h.addVar('x', 'int?');
var forStatement = _Statement();
var functionNode = _Node();
h.assignedVariables((vars) => vars.nest(forStatement,
() => vars.function(functionNode, () => vars.write(x))));
h.run((flow) {
h.declare(x, initialized: true);
h.promote(x, 'int');
expect(flow.promotedType(x).type, 'int');
flow.forEach_bodyBegin(forStatement, null, _Type('int?'));
h.promote(x, 'int');
expect(flow.promotedType(x), isNull);
h.function(functionNode, () {
flow.write(x, _Type('int?'));
});
flow.forEach_end();
});
});
test('forEach_bodyBegin() writes to loop variable', () {
var h = _Harness();
var x = h.addVar('x', 'int?');
var forStatement = _Statement();
h.assignedVariables(
(vars) => vars.nest(forStatement, () => vars.write(x)));
h.run((flow) {
h.declare(x, initialized: false);
expect(flow.isAssigned(x), false);
flow.forEach_bodyBegin(forStatement, x, _Type('int?'));
expect(flow.isAssigned(x), true);
flow.forEach_end();
expect(flow.isAssigned(x), false);
});
});
test('forEach_bodyBegin() pushes conservative join state', () {
var h = _Harness();
var x = h.addVar('x', 'int');
var forStatement = _Statement();
h.assignedVariables(
(vars) => vars.nest(forStatement, () => vars.write(x)));
h.run((flow) {
h.declare(x, initialized: false);
expect(flow.isUnassigned(x), true);
flow.forEach_bodyBegin(forStatement, null, _Type('int'));
// Since a write to x occurs somewhere in the loop, x should no longer
// be considered unassigned.
expect(flow.isUnassigned(x), false);
flow.handleBreak(forStatement);
flow.write(x, _Type('int'));
flow.forEach_end();
// Even though the write to x is unreachable (since it occurs after a
// break), x should still be considered "possibly assigned" because of
// the conservative join done at the top of the loop.
expect(flow.isUnassigned(x), false);
});
});
test('forEach_end() restores state before loop', () {
var h = _Harness();
var x = h.addVar('x', 'int?');
var stmt = _Statement();
h.assignedVariables((vars) => vars.nest(stmt, () {}));
h.run((flow) {
h.declare(x, initialized: true);
flow.forEach_bodyBegin(stmt, null, _Type('int?'));
h.promote(x, 'int');
expect(flow.promotedType(x).type, 'int');
flow.forEach_end();
expect(flow.promotedType(x), isNull);
});
});
test('functionExpression_begin() cancels promotions of self-captured vars',
() {
var h = _Harness();
var x = h.addVar('x', 'int?');
var y = h.addVar('y', 'int?');
var functionNode = _Node();
h.assignedVariables(
(vars) => vars.function(functionNode, () => vars.write(x)));
h.run((flow) {
h.declare(x, initialized: true);
h.declare(y, initialized: true);
h.promote(x, 'int');
h.promote(y, 'int');
expect(flow.promotedType(x).type, 'int');
expect(flow.promotedType(y).type, 'int');
flow.functionExpression_begin(functionNode);
// x is unpromoted within the local function
expect(flow.promotedType(x), isNull);
expect(flow.promotedType(y).type, 'int');
flow.write(x, _Type('int?'));
h.promote(x, 'int');
flow.functionExpression_end();
// x is unpromoted after the local function too
expect(flow.promotedType(x), isNull);
expect(flow.promotedType(y).type, 'int');
});
});
test('functionExpression_begin() cancels promotions of other-captured vars',
() {
var h = _Harness();
var x = h.addVar('x', 'int?');
var y = h.addVar('y', 'int?');
var functionNode1 = _Node();
var functionNode2 = _Node();
h.assignedVariables((vars) {
vars.function(functionNode1, () {});
vars.function(functionNode2, () => vars.write(x));
});
h.run((flow) {
h.declare(x, initialized: true);
h.declare(y, initialized: true);
h.promote(x, 'int');
h.promote(y, 'int');
expect(flow.promotedType(x).type, 'int');
expect(flow.promotedType(y).type, 'int');
flow.functionExpression_begin(functionNode1);
// x is unpromoted within the local function, because the write
// might have been captured by the time the local function executes.
expect(flow.promotedType(x), isNull);
expect(flow.promotedType(y).type, 'int');
// And any effort to promote x fails, because there is no way of knowing
// when the captured write might occur.
h.promote(x, 'int');
expect(flow.promotedType(x), isNull);
expect(flow.promotedType(y).type, 'int');
flow.functionExpression_end();
// x is still promoted after the local function, though, because the
// write hasn't been captured yet.
expect(flow.promotedType(x).type, 'int');
expect(flow.promotedType(y).type, 'int');
flow.functionExpression_begin(functionNode2);
// x is unpromoted inside this local function too.
expect(flow.promotedType(x), isNull);
expect(flow.promotedType(y).type, 'int');
flow.write(x, _Type('int?'));
flow.functionExpression_end();
// And since the second local function captured x, it remains
// unpromoted.
expect(flow.promotedType(x), isNull);
expect(flow.promotedType(y).type, 'int');
});
});
test('functionExpression_begin() cancels promotions of written vars', () {
var h = _Harness();
var x = h.addVar('x', 'int?');
var y = h.addVar('y', 'int?');
var node = _Node();
h.assignedVariables((vars) {
vars.function(node, () {});
vars.write(x);
});
h.run((flow) {
h.declare(x, initialized: true);
h.declare(y, initialized: true);
h.promote(x, 'int');
h.promote(y, 'int');
expect(flow.promotedType(x).type, 'int');
expect(flow.promotedType(y).type, 'int');
flow.functionExpression_begin(node);
// x is unpromoted within the local function, because the write
// might have happened by the time the local function executes.
expect(flow.promotedType(x), isNull);
expect(flow.promotedType(y).type, 'int');
// But it can be re-promoted because the write isn't captured.
h.promote(x, 'int');
expect(flow.promotedType(x).type, 'int');
expect(flow.promotedType(y).type, 'int');
flow.functionExpression_end();
// x is still promoted after the local function, though, because the
// write hasn't occurred yet.
expect(flow.promotedType(x).type, 'int');
expect(flow.promotedType(y).type, 'int');
flow.write(x, _Type('int?'));
// x is unpromoted now.
expect(flow.promotedType(x), isNull);
expect(flow.promotedType(y).type, 'int');
});
});
test('functionExpression_begin() handles not-yet-seen variables', () {
var h = _Harness();
var x = h.addVar('x', 'int?');
var functionNode = _Node();
h.assignedVariables(
(vars) => vars.function(functionNode, () => vars.write(x)));
h.run((flow) {
flow.functionExpression_begin(functionNode);
flow.functionExpression_end();
// x is declared after the local function, so the local function
// cannot possibly write to x.
h.declare(x, initialized: true);
h.promote(x, 'int');
expect(flow.promotedType(x).type, 'int');
});
});
test('functionExpression_begin() handles not-yet-seen write-captured vars',
() {
var h = _Harness();
var x = h.addVar('x', 'int?');
var y = h.addVar('y', 'int?');
var functionNode1 = _Node();
var functionNode2 = _Node();
h.assignedVariables((vars) {
vars.function(functionNode1, () {});
vars.function(functionNode2, () => vars.write(x));
});
h.run((flow) {
h.declare(y, initialized: true);
h.promote(y, 'int');
flow.functionExpression_begin(functionNode1);
h.promote(x, 'int');
// Promotion should not occur, because x might be write-captured by the
// time this code is reached.
expect(flow.promotedType(x), isNull);
flow.functionExpression_end();
flow.functionExpression_begin(functionNode2);
h.declare(x, initialized: true);
flow.functionExpression_end();
});
});
test(
'functionExpression_end does not propagate "definitely unassigned" '
'data', () {
var h = _Harness();
var x = h.addVar('x', 'int');
var functionNode = _Node();
h.assignedVariables((vars) {
vars.function(functionNode, () {});
vars.write(x);
});
h.run((flow) {
flow.declare(x, false);
expect(flow.isUnassigned(x), true);
flow.functionExpression_begin(functionNode);
// The function expression could be called at any time, so x might be
// assigned now.
expect(flow.isUnassigned(x), false);
flow.functionExpression_end();
// But now that we are back outside the function expression, we once
// again know that x is unassigned.
expect(flow.isUnassigned(x), true);
flow.write(x, _Type('int'));
expect(flow.isUnassigned(x), false);
});
});
test('handleBreak handles deep nesting', () {
var h = _Harness();
var whileStatement = _Statement();
h.assignedVariables((vars) {
vars.nest(whileStatement, () {});
});
h.run((flow) {
flow.whileStatement_conditionBegin(whileStatement);
flow.whileStatement_bodyBegin(whileStatement, h.booleanLiteral(true)());
h.if_(h.expr, () {
h.if_(h.expr, () {
flow.handleBreak(whileStatement);
});
});
flow.handleExit();
expect(flow.isReachable, false);
flow.whileStatement_end();
expect(flow.isReachable, true);
});
});
test('handleBreak handles mixed nesting', () {
var h = _Harness();
var whileStatement = _Statement();
h.assignedVariables((vars) {
vars.nest(whileStatement, () {});
});
h.run((flow) {
flow.whileStatement_conditionBegin(whileStatement);
flow.whileStatement_bodyBegin(whileStatement, h.booleanLiteral(true)());
h.if_(h.expr, () {
h.if_(h.expr, () {
flow.handleBreak(whileStatement);
});
flow.handleBreak(whileStatement);
});
flow.handleBreak(whileStatement);
expect(flow.isReachable, false);
flow.whileStatement_end();
expect(flow.isReachable, true);
});
});
test('handleContinue handles deep nesting', () {
var h = _Harness();
var doStatement = _Statement();
h.assignedVariables((vars) {
vars.nest(doStatement, () {});
});
h.run((flow) {
flow.doStatement_bodyBegin(doStatement);
h.if_(h.expr, () {
h.if_(h.expr, () {
flow.handleContinue(doStatement);
});
});
flow.handleExit();
expect(flow.isReachable, false);
flow.doStatement_conditionBegin();
expect(flow.isReachable, true);
flow.doStatement_end(h.booleanLiteral(true)());
expect(flow.isReachable, false);
});
});
test('handleContinue handles mixed nesting', () {
var h = _Harness();
var doStatement = _Statement();
h.assignedVariables((vars) {
vars.nest(doStatement, () {});
});
h.run((flow) {
flow.doStatement_bodyBegin(doStatement);
h.if_(h.expr, () {
h.if_(h.expr, () {
flow.handleContinue(doStatement);
});
flow.handleContinue(doStatement);
});
flow.handleContinue(doStatement);
expect(flow.isReachable, false);
flow.doStatement_conditionBegin();
expect(flow.isReachable, true);
flow.doStatement_end(h.booleanLiteral(true)());
expect(flow.isReachable, false);
});
});
test('ifNullExpression allows ensure guarding', () {
var h = _Harness();
var x = h.addVar('x', 'int?');
h.assignedVariables((vars) => vars.write(x));
h.run((flow) {
h.declare(x, initialized: true);
var rhsIsReachable = flow.ifNullExpression_rightBegin(
h.variableRead(x)(), _Type('int?'));
expect(rhsIsReachable, true);
expect(flow.isReachable, true);
flow.write(x, _Type('int'));
expect(flow.promotedType(x).type, 'int');
flow.ifNullExpression_end();
expect(flow.isReachable, true);
expect(flow.promotedType(x).type, 'int');
});
});
test('ifNullExpression allows promotion of tested var', () {
var h = _Harness();
var x = h.addVar('x', 'int?');
h.run((flow) {
h.declare(x, initialized: true);
var rhsIsReachable = flow.ifNullExpression_rightBegin(
h.variableRead(x)(), _Type('int?'));
expect(rhsIsReachable, true);
expect(flow.isReachable, true);
h.promote(x, 'int');
expect(flow.promotedType(x).type, 'int');
flow.ifNullExpression_end();
expect(flow.isReachable, true);
expect(flow.promotedType(x).type, 'int');
});
});
test('ifNullExpression discards promotions unrelated to tested expr', () {
var h = _Harness();
var x = h.addVar('x', 'int?');
h.run((flow) {
h.declare(x, initialized: true);
var rhsIsReachable =
flow.ifNullExpression_rightBegin(h.expr(), _Type('int?'));
expect(rhsIsReachable, true);
expect(flow.isReachable, true);
h.promote(x, 'int');
expect(flow.promotedType(x).type, 'int');
flow.ifNullExpression_end();
expect(flow.isReachable, true);
expect(flow.promotedType(x), null);
});
});
test('ifNullExpression does not detect when RHS is unreachable', () {
var h = _Harness();
h.run((flow) {
var rhsIsReachable =
flow.ifNullExpression_rightBegin(h.expr(), _Type('int'));
// We can't treat the RHS as unreachable because of mixed mode
// unsoundness
expect(rhsIsReachable, true);
expect(flow.isReachable, true);
flow.ifNullExpression_end();
expect(flow.isReachable, true);
});
});
test('ifNullExpression determines reachability correctly for `Null` type',
() {
var h = _Harness();
h.run((flow) {
var rhsIsReachable =
flow.ifNullExpression_rightBegin(h.expr(), _Type('Null'));
expect(rhsIsReachable, true);
expect(flow.isReachable, true);
flow.ifNullExpression_end();
expect(flow.isReachable, true);
});
});
test('ifStatement with early exit promotes in unreachable code', () {
var h = _Harness();
var x = h.addVar('x', 'int?');
h.run((flow) {
h.declare(x, initialized: true);
flow.handleExit();
expect(flow.isReachable, false);
flow.ifStatement_conditionBegin();
flow.ifStatement_thenBegin(h.eqNull(x, _Type('int?'))());
flow.handleExit();
flow.ifStatement_end(false);
expect(flow.isReachable, false);
expect(flow.promotedType(x).type, 'int');
});
});
test('ifStatement_end(false) keeps else branch if then branch exits', () {
var h = _Harness();
var x = h.addVar('x', 'int?');
h.run((flow) {
h.declare(x, initialized: true);
flow.ifStatement_conditionBegin();
flow.ifStatement_thenBegin(h.eqNull(x, _Type('int?'))());
flow.handleExit();
flow.ifStatement_end(false);
expect(flow.promotedType(x).type, 'int');
});
});
void _checkIs(String declaredType, String tryPromoteType,
String expectedPromotedTypeThen, String expectedPromotedTypeElse,
{bool inverted = false}) {
var h = _Harness();
var x = h.addVar('x', declaredType);
h.run((flow) {
h.declare(x, initialized: true);
flow.ifStatement_conditionBegin();
var read = _Expression();
flow.variableRead(read, x);
var expr = _Expression();
var failureReachable =
flow.isExpression_end(expr, read, inverted, _Type(tryPromoteType));
expect(failureReachable, true);
flow.ifStatement_thenBegin(expr);
expect(flow.isReachable, true);
if (expectedPromotedTypeThen == null) {
expect(flow.promotedType(x), isNull);
} else {
expect(flow.promotedType(x).type, expectedPromotedTypeThen);
}
flow.ifStatement_elseBegin();
expect(flow.isReachable, true);
if (expectedPromotedTypeElse == null) {
expect(flow.promotedType(x), isNull);
} else {
expect(flow.promotedType(x).type, expectedPromotedTypeElse);
}
flow.ifStatement_end(true);
});
}
test('isExpression_end promotes to a subtype', () {
_checkIs('int?', 'int', 'int', 'Never?');
});
test('isExpression_end promotes to a subtype, inverted', () {
_checkIs('int?', 'int', 'Never?', 'int', inverted: true);
});
test('isExpression_end does not promote to a supertype', () {
_checkIs('int', 'int?', null, null);
});
test('isExpression_end does not promote to a supertype, inverted', () {
_checkIs('int', 'int?', null, null, inverted: true);
});
test('isExpression_end does not promote to an unrelated type', () {
_checkIs('int', 'String', null, null);
});
test('isExpression_end does not promote to an unrelated type, inverted',
() {
_checkIs('int', 'String', null, null, inverted: true);
});
test('isExpression_end does nothing if applied to a non-variable', () {
var h = _Harness();
h.run((flow) {
flow.ifStatement_conditionBegin();
var subExpr = _Expression();
var expr = _Expression();
var failureReachable =
flow.isExpression_end(expr, subExpr, false, _Type('int'));
expect(failureReachable, true);
flow.ifStatement_thenBegin(expr);
expect(flow.isReachable, true);
flow.ifStatement_elseBegin();
expect(flow.isReachable, true);
flow.ifStatement_end(true);
});
});
test('isExpression_end does nothing if applied to a non-variable, inverted',
() {
var h = _Harness();
h.run((flow) {
flow.ifStatement_conditionBegin();
var subExpr = _Expression();
var expr = _Expression();
var failureReachable =
flow.isExpression_end(expr, subExpr, true, _Type('int'));
expect(failureReachable, true);
flow.ifStatement_thenBegin(expr);
expect(flow.isReachable, true);
flow.ifStatement_elseBegin();
expect(flow.isReachable, true);
flow.ifStatement_end(true);
});
});
test('isExpression_end() does not promote write-captured vars', () {
var h = _Harness();
var x = h.addVar('x', 'int?');
var functionNode = _Node();
h.assignedVariables(
(vars) => vars.function(functionNode, () => vars.write(x)));
h.run((flow) {
h.declare(x, initialized: true);
h.if_(h.isType(h.variableRead(x), 'int'), () {
expect(flow.promotedType(x).type, 'int');
});
h.function(functionNode, () {
flow.write(x, _Type('int?'));
});
h.if_(h.isType(h.variableRead(x), 'int'), () {
expect(flow.promotedType(x), isNull);
});
});
});
test('isExpression_end() handles not-yet-seen variables', () {
var h = _Harness();
var x = h.addVar('x', 'int?');
h.assignedVariables(
(vars) => vars.function(_Node(), () => vars.write(x)));
h.run((flow) {
h.if_(h.isType(h.variableRead(x), 'int'), () {
expect(flow.promotedType(x).type, 'int');
});
h.declare(x, initialized: true);
});
});
test('labeledBlock without break', () {
var h = _Harness();
var x = h.addVar('x', 'int?');
var block = _Statement();
h.run((flow) {
h.declare(x, initialized: true);
h.ifIsNotType(x, 'int', () {
h.labeledBlock(block, () {
flow.handleExit();
});
});
expect(flow.promotedType(x).type, 'int');
});
});
test('labeledBlock with break joins', () {
var h = _Harness();
var x = h.addVar('x', 'int?');
var block = _Statement();
h.run((flow) {
h.declare(x, initialized: true);
h.ifIsNotType(x, 'int', () {
h.labeledBlock(block, () {
h.if_(h.expr, () {
flow.handleBreak(block);
});
flow.handleExit();
});
});
expect(flow.promotedType(x), isNull);
});
});
test('logicalBinaryOp_rightBegin(isAnd: true) promotes in RHS', () {
var h = _Harness();
var x = h.addVar('x', 'int?');
h.run((flow) {
h.declare(x, initialized: true);
flow.logicalBinaryOp_rightBegin(h.notNull(x, _Type('int?'))(),
isAnd: true);
expect(flow.promotedType(x).type, 'int');
flow.logicalBinaryOp_end(_Expression(), _Expression(), isAnd: true);
});
});
test('logicalBinaryOp_rightEnd(isAnd: true) keeps promotions from RHS', () {
var h = _Harness();
var x = h.addVar('x', 'int?');
h.run((flow) {
h.declare(x, initialized: true);
flow.ifStatement_conditionBegin();
flow.logicalBinaryOp_rightBegin(_Expression(), isAnd: true);
var wholeExpr = _Expression();
flow.logicalBinaryOp_end(wholeExpr, h.notNull(x, _Type('int?'))(),
isAnd: true);
flow.ifStatement_thenBegin(wholeExpr);
expect(flow.promotedType(x).type, 'int');
flow.ifStatement_end(false);
});
});
test('logicalBinaryOp_rightEnd(isAnd: false) keeps promotions from RHS',
() {
var h = _Harness();
var x = h.addVar('x', 'int?');
h.run((flow) {
h.declare(x, initialized: true);
flow.ifStatement_conditionBegin();
flow.logicalBinaryOp_rightBegin(_Expression(), isAnd: false);
var wholeExpr = _Expression();
flow.logicalBinaryOp_end(wholeExpr, h.eqNull(x, _Type('int?'))(),
isAnd: false);
flow.ifStatement_thenBegin(wholeExpr);
flow.ifStatement_elseBegin();
expect(flow.promotedType(x).type, 'int');
flow.ifStatement_end(true);
});
});
test('logicalBinaryOp_rightBegin(isAnd: false) promotes in RHS', () {
var h = _Harness();
var x = h.addVar('x', 'int?');
h.run((flow) {
h.declare(x, initialized: true);
flow.logicalBinaryOp_rightBegin(h.eqNull(x, _Type('int?'))(),
isAnd: false);
expect(flow.promotedType(x).type, 'int');
flow.logicalBinaryOp_end(_Expression(), _Expression(), isAnd: false);
});
});
test('logicalBinaryOp(isAnd: true) joins promotions', () {
// if (x != null && y != null) {
// promotes x and y
// }
var h = _Harness();
var x = h.addVar('x', 'int?');
var y = h.addVar('y', 'int?');
h.run((flow) {
h.declare(x, initialized: true);
h.declare(y, initialized: true);
h.if_(h.and(h.notNull(x, _Type('int?')), h.notNull(y, _Type('int?'))),
() {
expect(flow.promotedType(x).type, 'int');
expect(flow.promotedType(y).type, 'int');
});
});
});
test('logicalBinaryOp(isAnd: false) joins promotions', () {
// if (x == null || y == null) {} else {
// promotes x and y
// }
var h = _Harness();
var x = h.addVar('x', 'int?');
var y = h.addVar('y', 'int?');
h.run((flow) {
h.declare(x, initialized: true);
h.declare(y, initialized: true);
h.ifElse(
h.or(h.eqNull(x, _Type('int?')), h.eqNull(y, _Type('int?'))), () {},
() {
expect(flow.promotedType(x).type, 'int');
expect(flow.promotedType(y).type, 'int');
});
});
});
test('nonNullAssert_end(x) promotes', () {
var h = _Harness();
var x = h.addVar('x', 'int?');
h.run((flow) {
h.declare(x, initialized: true);
var varExpr = _Expression();
flow.variableRead(varExpr, x);
flow.nonNullAssert_end(varExpr);
expect(flow.promotedType(x).type, 'int');
});
});
test('nullAwareAccess temporarily promotes', () {
var h = _Harness();
var x = h.addVar('x', 'int?');
h.run((flow) {
h.declare(x, initialized: true);
var varExpr = _Expression();
flow.variableRead(varExpr, x);
var shortIsReachable =
flow.nullAwareAccess_rightBegin(varExpr, _Type('int?'));
expect(shortIsReachable, true);
expect(flow.isReachable, true);
expect(flow.promotedType(x).type, 'int');
flow.nullAwareAccess_end();
expect(flow.promotedType(x), isNull);
});
});
test('nullAwareAccess does not promote the target of a cascade', () {
var h = _Harness();
var x = h.addVar('x', 'int?');
h.run((flow) {
h.declare(x, initialized: true);
var varExpr = _Expression();
flow.variableRead(varExpr, x);
var shortIsReachable =
flow.nullAwareAccess_rightBegin(null, _Type('int?'));
expect(shortIsReachable, true);
expect(flow.isReachable, true);
expect(flow.promotedType(x), isNull);
flow.nullAwareAccess_end();
});
});
test('nullAwareAccess preserves demotions', () {
var h = _Harness();
var x = h.addVar('x', 'int?');
h.assignedVariables((vars) => vars.write(x));
h.run((flow) {
h.declare(x, initialized: true);
h.promote(x, 'int');
var lhs = _Expression();
var shortIsReachable =
flow.nullAwareAccess_rightBegin(lhs, _Type('int'));
// We can't treat the short as unreachable because of mixed mode
// unsoundness
expect(shortIsReachable, true);
expect(flow.isReachable, true);
expect(flow.promotedType(x).type, 'int');
flow.write(x, _Type('int?'));
expect(flow.promotedType(x), isNull);
flow.nullAwareAccess_end();
expect(flow.promotedType(x), isNull);
});
});
test('nullAwareAccess_end ignores shorting if target is non-nullable', () {
var h = _Harness();
var x = h.addVar('x', 'int?');
h.run((flow) {
h.declare(x, initialized: true);
var shortIsReachable =
flow.nullAwareAccess_rightBegin(_Expression(), _Type('int'));
// We can't treat the RHS as unreachable because of mixed mode
// unsoundness
expect(shortIsReachable, true);
expect(flow.isReachable, true);
h.promote(x, 'int');
expect(flow.promotedType(x).type, 'int');
flow.nullAwareAccess_end();
// Since the null-shorting path was reachable, promotion of `x` should
// be cancelled.
expect(flow.promotedType(x), isNull);
});
});
test('parenthesizedExpression preserves promotion behaviors', () {
var h = _Harness();
var x = h.addVar('x', 'int?');
h.run((flow) {
h.if_(
h.parenthesized(h.notEqual(
h.parenthesized(h.variableRead(x)),
_Type('int?'),
h.parenthesized(h.nullLiteral),
_Type('Null'))), () {
expect(flow.promotedType(x).type, 'int');
});
});
});
test('promote promotes to a subtype and sets type of interest', () {
var h = _Harness();
var x = h.addVar('x', 'num?');
h.assignedVariables((vars) {
vars.write(x);
});
h.run((flow) {
flow.declare(x, true);
expect(flow.promotedType(x), isNull);
flow.promote(x, _Type('num'));
expect(flow.promotedType(x).type, 'num');
// Check that it's a type of interest by promoting and de-promoting.
h.if_(h.isType(h.variableRead(x), 'int'), () {
expect(flow.promotedType(x).type, 'int');
flow.write(x, _Type('num'));
expect(flow.promotedType(x).type, 'num');
});
});
});
test('promote does not promote to a non-subtype', () {
var h = _Harness();
var x = h.addVar('x', 'num?');
h.run((flow) {
flow.declare(x, true);
expect(flow.promotedType(x), isNull);
flow.promote(x, _Type('String'));
expect(flow.promotedType(x), isNull);
});
});
test('promote does not promote if variable is write-captured', () {
var h = _Harness();
var x = h.addVar('x', 'num?');
var functionNode = _Node();
h.assignedVariables(
(vars) => vars.function(functionNode, () => vars.write(x)));
h.run((flow) {
flow.declare(x, true);
expect(flow.promotedType(x), isNull);
flow.functionExpression_begin(functionNode);
flow.write(x, _Type('num'));
flow.functionExpression_end();
flow.promote(x, _Type('num'));
expect(flow.promotedType(x), isNull);
});
});
test('promotedType handles not-yet-seen variables', () {
// Note: this is needed for error recovery in the analyzer.
var h = _Harness();
var x = h.addVar('x', 'int');
h.run((flow) {
expect(flow.promotedType(x), isNull);
h.declare(x, initialized: true);
});
});
test('switchStatement_beginCase(false) restores previous promotions', () {
var h = _Harness();
var x = h.addVar('x', 'int?');
var switchStatement = _Statement();
h.assignedVariables(
(vars) => vars.nest(switchStatement, () => vars.write(x)));
h.run((flow) {
h.declare(x, initialized: true);
h.promote(x, 'int');
flow.switchStatement_expressionEnd(_Statement());
flow.switchStatement_beginCase(false, switchStatement);
expect(flow.promotedType(x).type, 'int');
flow.write(x, _Type('int?'));
expect(flow.promotedType(x), isNull);
flow.switchStatement_beginCase(false, switchStatement);
expect(flow.promotedType(x).type, 'int');
flow.write(x, _Type('int?'));
expect(flow.promotedType(x), isNull);
flow.switchStatement_end(false);
});
});
test('switchStatement_beginCase(false) does not un-promote', () {
var h = _Harness();
var x = h.addVar('x', 'int?');
var switchStatement = _Statement();
h.assignedVariables(
(vars) => vars.nest(switchStatement, () => vars.write(x)));
h.run((flow) {
h.declare(x, initialized: true);
h.promote(x, 'int');
flow.switchStatement_expressionEnd(_Statement());
flow.switchStatement_beginCase(false, switchStatement);
expect(flow.promotedType(x).type, 'int');
flow.write(x, _Type('int?'));
expect(flow.promotedType(x), isNull);
flow.switchStatement_end(false);
});
});
test('switchStatement_beginCase(false) handles write captures in cases',
() {
var h = _Harness();
var x = h.addVar('x', 'int?');
var switchStatement = _Statement();
var functionNode = _Node();
h.assignedVariables((vars) => vars.nest(switchStatement,
() => vars.function(functionNode, () => vars.write(x))));
h.run((flow) {
h.declare(x, initialized: true);
h.promote(x, 'int');
flow.switchStatement_expressionEnd(_Statement());
flow.switchStatement_beginCase(false, switchStatement);
expect(flow.promotedType(x).type, 'int');
h.function(functionNode, () {
flow.write(x, _Type('int?'));
});
expect(flow.promotedType(x), isNull);
flow.switchStatement_end(false);
});
});
test('switchStatement_beginCase(true) un-promotes', () {
var h = _Harness();
var x = h.addVar('x', 'int?');
var switchStatement = _Statement();
h.assignedVariables(
(vars) => vars.nest(switchStatement, () => vars.write(x)));
h.run((flow) {
h.declare(x, initialized: true);
h.promote(x, 'int');
flow.switchStatement_expressionEnd(_Statement());
flow.switchStatement_beginCase(true, switchStatement);
expect(flow.promotedType(x), isNull);
flow.write(x, _Type('int?'));
expect(flow.promotedType(x), isNull);
flow.switchStatement_end(false);
});
});
test('switchStatement_beginCase(true) handles write captures in cases', () {
var h = _Harness();
var x = h.addVar('x', 'int?');
var switchStatement = _Statement();
var functionNode = _Node();
h.assignedVariables((vars) => vars.nest(switchStatement,
() => vars.function(functionNode, () => vars.write(x))));
h.run((flow) {
h.declare(x, initialized: true);
h.promote(x, 'int');
flow.switchStatement_expressionEnd(_Statement());
flow.switchStatement_beginCase(true, switchStatement);
h.promote(x, 'int');
expect(flow.promotedType(x), isNull);
h.function(functionNode, () {
flow.write(x, _Type('int?'));
});
expect(flow.promotedType(x), isNull);
flow.switchStatement_end(false);
});
});
test('switchStatement_end(false) joins break and default', () {
var h = _Harness();
var x = h.addVar('x', 'int?');
var y = h.addVar('y', 'int?');
var z = h.addVar('z', 'int?');
var switchStatement = _Statement();
h.assignedVariables(
(vars) => vars.nest(switchStatement, () => vars.write(y)));
h.run((flow) {
h.declare(x, initialized: true);
h.declare(y, initialized: true);
h.declare(z, initialized: true);
h.promote(y, 'int');
h.promote(z, 'int');
var stmt = _Statement();
flow.switchStatement_expressionEnd(stmt);
flow.switchStatement_beginCase(false, switchStatement);
h.promote(x, 'int');
flow.write(y, _Type('int?'));
flow.handleBreak(stmt);
flow.switchStatement_end(false);
expect(flow.promotedType(x), isNull);
expect(flow.promotedType(y), isNull);
expect(flow.promotedType(z).type, 'int');
});
});
test('switchStatement_end(true) joins breaks', () {
var h = _Harness();
var w = h.addVar('w', 'int?');
var x = h.addVar('x', 'int?');
var y = h.addVar('y', 'int?');
var z = h.addVar('z', 'int?');
var switchStatement = _Statement();
h.assignedVariables((vars) => vars.nest(switchStatement, () {
vars.write(x);
vars.write(y);
}));
h.run((flow) {
h.declare(w, initialized: true);
h.declare(x, initialized: true);
h.declare(y, initialized: true);
h.declare(z, initialized: true);
h.promote(x, 'int');
h.promote(y, 'int');
h.promote(z, 'int');
var stmt = _Statement();
flow.switchStatement_expressionEnd(stmt);
flow.switchStatement_beginCase(false, switchStatement);
h.promote(w, 'int');
h.promote(y, 'int');
flow.write(x, _Type('int?'));
flow.handleBreak(stmt);
flow.switchStatement_beginCase(false, switchStatement);
h.promote(w, 'int');
h.promote(x, 'int');
flow.write(y, _Type('int?'));
flow.handleBreak(stmt);
flow.switchStatement_end(true);
expect(flow.promotedType(w).type, 'int');
expect(flow.promotedType(x), isNull);
expect(flow.promotedType(y), isNull);
expect(flow.promotedType(z).type, 'int');
});
});
test('switchStatement_end(true) allows fall-through of last case', () {
var h = _Harness();
var x = h.addVar('x', 'int?');
var stmt = _Statement();
h.assignedVariables((vars) => vars.nest(stmt, () {}));
h.run((flow) {
h.declare(x, initialized: true);
flow.switchStatement_expressionEnd(stmt);
flow.switchStatement_beginCase(false, stmt);
h.promote(x, 'int');
flow.handleBreak(stmt);
flow.switchStatement_beginCase(false, stmt);
flow.switchStatement_end(true);
expect(flow.promotedType(x), isNull);
});
});
test('tryCatchStatement_bodyEnd() restores pre-try state', () {
var h = _Harness();
var x = h.addVar('x', 'int?');
var y = h.addVar('y', 'int?');
var stmt = _Statement();
h.assignedVariables((vars) => vars.nest(stmt, () {}));
h.run((flow) {
h.declare(x, initialized: true);
h.declare(y, initialized: true);
h.promote(y, 'int');
flow.tryCatchStatement_bodyBegin();
h.promote(x, 'int');
expect(flow.promotedType(x).type, 'int');
expect(flow.promotedType(y).type, 'int');
flow.tryCatchStatement_bodyEnd(stmt);
flow.tryCatchStatement_catchBegin(null, null);
expect(flow.promotedType(x), isNull);
expect(flow.promotedType(y).type, 'int');
flow.tryCatchStatement_catchEnd();
flow.tryCatchStatement_end();
});
});
test('tryCatchStatement_bodyEnd() un-promotes variables assigned in body',
() {
var h = _Harness();
var x = h.addVar('x', 'int?');
var body = _Statement();
h.assignedVariables((vars) => vars.nest(body, () => vars.write(x)));
h.run((flow) {
h.declare(x, initialized: true);
h.promote(x, 'int');
expect(flow.promotedType(x).type, 'int');
flow.tryCatchStatement_bodyBegin();
flow.write(x, _Type('int?'));
h.promote(x, 'int');
expect(flow.promotedType(x).type, 'int');
flow.tryCatchStatement_bodyEnd(body);
flow.tryCatchStatement_catchBegin(null, null);
expect(flow.promotedType(x), isNull);
flow.tryCatchStatement_catchEnd();
flow.tryCatchStatement_end();
});
});
test('tryCatchStatement_bodyEnd() preserves write captures in body', () {
// Note: it's not necessary for the write capture to survive to the end of
// the try body, because an exception could occur at any time. We check
// this by putting an exit in the try body.
var h = _Harness();
var x = h.addVar('x', 'int?');
var body = _Statement();
var functionNode = _Node();
h.assignedVariables((vars) => vars.nest(
body, () => vars.function(functionNode, () => vars.write(x))));
h.run((flow) {
h.declare(x, initialized: true);
h.promote(x, 'int');
expect(flow.promotedType(x).type, 'int');
flow.tryCatchStatement_bodyBegin();
h.function(functionNode, () {
flow.write(x, _Type('int?'));
});
flow.handleExit();
flow.tryCatchStatement_bodyEnd(body);
flow.tryCatchStatement_catchBegin(null, null);
h.promote(x, 'int');
expect(flow.promotedType(x), isNull);
flow.tryCatchStatement_catchEnd();
flow.tryCatchStatement_end();
});
});
test('tryCatchStatement_catchBegin() restores previous post-body state',
() {
var h = _Harness();
var x = h.addVar('x', 'int?');
var stmt = _Statement();
h.assignedVariables((vars) => vars.nest(stmt, () {}));
h.run((flow) {
h.declare(x, initialized: true);
flow.tryCatchStatement_bodyBegin();
flow.tryCatchStatement_bodyEnd(stmt);
flow.tryCatchStatement_catchBegin(null, null);
h.promote(x, 'int');
expect(flow.promotedType(x).type, 'int');
flow.tryCatchStatement_catchEnd();
flow.tryCatchStatement_catchBegin(null, null);
expect(flow.promotedType(x), isNull);
flow.tryCatchStatement_catchEnd();
flow.tryCatchStatement_end();
});
});
test('tryCatchStatement_catchBegin() initializes vars', () {
var h = _Harness();
var e = h.addVar('e', 'int');
var st = h.addVar('st', 'StackTrace');
var stmt = _Statement();
h.assignedVariables((vars) => vars.nest(stmt, () {}));
h.run((flow) {
flow.tryCatchStatement_bodyBegin();
flow.tryCatchStatement_bodyEnd(stmt);
flow.tryCatchStatement_catchBegin(e, st);
expect(flow.isAssigned(e), true);
expect(flow.isAssigned(st), true);
flow.tryCatchStatement_catchEnd();
flow.tryCatchStatement_end();
});
});
test('tryCatchStatement_catchEnd() joins catch state with after-try state',
() {
var h = _Harness();
var x = h.addVar('x', 'int?');
var y = h.addVar('y', 'int?');
var z = h.addVar('z', 'int?');
var stmt = _Statement();
h.assignedVariables((vars) => vars.nest(stmt, () {}));
h.run((flow) {
h.declare(x, initialized: true);
h.declare(y, initialized: true);
h.declare(z, initialized: true);
flow.tryCatchStatement_bodyBegin();
h.promote(x, 'int');
h.promote(y, 'int');
flow.tryCatchStatement_bodyEnd(stmt);
flow.tryCatchStatement_catchBegin(null, null);
h.promote(x, 'int');
h.promote(z, 'int');
flow.tryCatchStatement_catchEnd();
flow.tryCatchStatement_end();
// Only x should be promoted, because it's the only variable
// promoted in both the try body and the catch handler.
expect(flow.promotedType(x).type, 'int');
expect(flow.promotedType(y), isNull);
expect(flow.promotedType(z), isNull);
});
});
test('tryCatchStatement_catchEnd() joins catch states', () {
var h = _Harness();
var x = h.addVar('x', 'int?');
var y = h.addVar('y', 'int?');
var z = h.addVar('z', 'int?');
var stmt = _Statement();
h.assignedVariables((vars) => vars.nest(stmt, () {}));
h.run((flow) {
h.declare(x, initialized: true);
h.declare(y, initialized: true);
h.declare(z, initialized: true);
flow.tryCatchStatement_bodyBegin();
flow.handleExit();
flow.tryCatchStatement_bodyEnd(stmt);
flow.tryCatchStatement_catchBegin(null, null);
h.promote(x, 'int');
h.promote(y, 'int');
flow.tryCatchStatement_catchEnd();
flow.tryCatchStatement_catchBegin(null, null);
h.promote(x, 'int');
h.promote(z, 'int');
flow.tryCatchStatement_catchEnd();
flow.tryCatchStatement_end();
// Only x should be promoted, because it's the only variable promoted
// in both catch handlers.
expect(flow.promotedType(x).type, 'int');
expect(flow.promotedType(y), isNull);
expect(flow.promotedType(z), isNull);
});
});
test('tryFinallyStatement_finallyBegin() restores pre-try state', () {
var h = _Harness();
var x = h.addVar('x', 'int?');
var y = h.addVar('y', 'int?');
var body = _Node();
var finallyBlock = _Node();
h.assignedVariables((vars) {
vars.nest(body, () {});
vars.nest(finallyBlock, () {});
});
h.run((flow) {
h.declare(x, initialized: true);
h.declare(y, initialized: true);
h.promote(y, 'int');
flow.tryFinallyStatement_bodyBegin();
h.promote(x, 'int');
expect(flow.promotedType(x).type, 'int');
expect(flow.promotedType(y).type, 'int');
flow.tryFinallyStatement_finallyBegin(body);
expect(flow.promotedType(x), isNull);
expect(flow.promotedType(y).type, 'int');
flow.tryFinallyStatement_end(finallyBlock);
});
});
test(
'tryFinallyStatement_finallyBegin() un-promotes variables assigned in '
'body', () {
var h = _Harness();
var x = h.addVar('x', 'int?');
var body = _Node();
var finallyBlock = _Node();
h.assignedVariables((vars) {
vars.nest(body, () => vars.write(x));
vars.nest(finallyBlock, () {});
});
h.run((flow) {
h.declare(x, initialized: true);
h.promote(x, 'int');
expect(flow.promotedType(x).type, 'int');
flow.tryFinallyStatement_bodyBegin();
flow.write(x, _Type('int?'));
h.promote(x, 'int');
expect(flow.promotedType(x).type, 'int');
flow.tryFinallyStatement_finallyBegin(body);
expect(flow.promotedType(x), isNull);
flow.tryFinallyStatement_end(finallyBlock);
});
});
test('tryFinallyStatement_finallyBegin() preserves write captures in body',
() {
// Note: it's not necessary for the write capture to survive to the end of
// the try body, because an exception could occur at any time. We check
// this by putting an exit in the try body.
var h = _Harness();
var x = h.addVar('x', 'int?');
var body = _Statement();
var functionNode = _Node();
var finallyBlock = _Node();
h.assignedVariables((vars) => vars.nest(body, () {
vars.function(functionNode, () => vars.write(x));
vars.nest(finallyBlock, () {});
}));
h.run((flow) {
h.declare(x, initialized: true);
flow.tryFinallyStatement_bodyBegin();
h.function(functionNode, () {
flow.write(x, _Type('int?'));
});
flow.handleExit();
flow.tryFinallyStatement_finallyBegin(body);
h.promote(x, 'int');
expect(flow.promotedType(x), isNull);
flow.tryFinallyStatement_end(finallyBlock);
});
});
test('tryFinallyStatement_end() restores promotions from try body', () {
var h = _Harness();
var x = h.addVar('x', 'int?');
var y = h.addVar('y', 'int?');
var body = _Statement();
var finallyBlock = _Node();
h.assignedVariables((vars) => vars.nest(body, () {
vars.nest(body, () {});
vars.nest(finallyBlock, () {});
}));
h.run((flow) {
h.declare(x, initialized: true);
h.declare(y, initialized: true);
flow.tryFinallyStatement_bodyBegin();
h.promote(x, 'int');
expect(flow.promotedType(x).type, 'int');
flow.tryFinallyStatement_finallyBegin(body);
expect(flow.promotedType(x), isNull);
h.promote(y, 'int');
expect(flow.promotedType(y).type, 'int');
flow.tryFinallyStatement_end(finallyBlock);
// Both x and y should now be promoted.
expect(flow.promotedType(x).type, 'int');
expect(flow.promotedType(y).type, 'int');
});
});
test(
'tryFinallyStatement_end() does not restore try body promotions for '
'variables assigned in finally', () {
var h = _Harness();
var x = h.addVar('x', 'int?');
var y = h.addVar('y', 'int?');
var body = _Node();
var finallyBlock = _Node();
h.assignedVariables((vars) {
vars.nest(body, () {});
vars.nest(finallyBlock, () {
vars.write(x);
vars.write(y);
});
});
h.run((flow) {
h.declare(x, initialized: true);
h.declare(y, initialized: true);
flow.tryFinallyStatement_bodyBegin();
h.promote(x, 'int');
expect(flow.promotedType(x).type, 'int');
flow.tryFinallyStatement_finallyBegin(body);
expect(flow.promotedType(x), isNull);
flow.write(x, _Type('int?'));
flow.write(y, _Type('int?'));
h.promote(y, 'int');
expect(flow.promotedType(y).type, 'int');
flow.tryFinallyStatement_end(finallyBlock);
// x should not be re-promoted, because it might have been assigned a
// non-promoted value in the "finally" block. But y's promotion still
// stands, because y was promoted in the finally block.
expect(flow.promotedType(x), isNull);
expect(flow.promotedType(y).type, 'int');
});
});
test('whileStatement_conditionBegin() un-promotes', () {
var h = _Harness();
var x = h.addVar('x', 'int?');
var whileStatement = _Statement();
h.assignedVariables(
(vars) => vars.nest(whileStatement, () => vars.write(x)));
h.run((flow) {
h.declare(x, initialized: true);
h.promote(x, 'int');
expect(flow.promotedType(x).type, 'int');
flow.whileStatement_conditionBegin(whileStatement);
expect(flow.promotedType(x), isNull);
flow.whileStatement_bodyBegin(_Statement(), _Expression());
flow.whileStatement_end();
});
});
test('whileStatement_conditionBegin() handles write captures in the loop',
() {
var h = _Harness();
var x = h.addVar('x', 'int?');
var whileStatement = _Statement();
var functionNode = _Node();
h.assignedVariables((vars) => vars.nest(whileStatement,
() => vars.function(functionNode, () => vars.write(x))));
h.run((flow) {
h.declare(x, initialized: true);
h.promote(x, 'int');
expect(flow.promotedType(x).type, 'int');
flow.whileStatement_conditionBegin(whileStatement);
h.promote(x, 'int');
expect(flow.promotedType(x), isNull);
h.function(functionNode, () {
flow.write(x, _Type('int?'));
});
flow.whileStatement_bodyBegin(_Statement(), _Expression());
flow.whileStatement_end();
});
});
test('whileStatement_conditionBegin() handles not-yet-seen variables', () {
var h = _Harness();
var x = h.addVar('x', 'int?');
var y = h.addVar('y', 'int?');
var whileStatement = _Statement();
h.assignedVariables(
(vars) => vars.nest(whileStatement, () => vars.write(x)));
h.run((flow) {
h.declare(y, initialized: true);
h.promote(y, 'int');
flow.whileStatement_conditionBegin(whileStatement);
flow.declare(x, true);
flow.whileStatement_bodyBegin(_Statement(), _Expression());
flow.whileStatement_end();
});
});
test('whileStatement_bodyBegin() promotes', () {
var h = _Harness();
var x = h.addVar('x', 'int?');
var stmt = _Statement();
h.assignedVariables((vars) => vars.nest(stmt, () {}));
h.run((flow) {
h.declare(x, initialized: true);
flow.whileStatement_conditionBegin(stmt);
flow.whileStatement_bodyBegin(stmt, h.notNull(x, _Type('int?'))());
expect(flow.promotedType(x).type, 'int');
flow.whileStatement_end();
});
});
test('whileStatement_end() joins break and condition-false states', () {
// To test that the states are properly joined, we have three variables:
// x, y, and z. We promote x and y in the break path, and x and z in the
// condition-false path. After the loop, only x should be promoted.
var h = _Harness();
var x = h.addVar('x', 'int?');
var y = h.addVar('y', 'int?');
var z = h.addVar('z', 'int?');
var stmt = _Statement();
h.assignedVariables((vars) => vars.nest(stmt, () {}));
h.run((flow) {
h.declare(x, initialized: true);
h.declare(y, initialized: true);
h.declare(z, initialized: true);
flow.whileStatement_conditionBegin(stmt);
flow.whileStatement_bodyBegin(stmt,
h.or(h.eqNull(x, _Type('int?')), h.eqNull(z, _Type('int?')))());
h.if_(h.expr, () {
h.promote(x, 'int');
h.promote(y, 'int');
flow.handleBreak(stmt);
});
flow.whileStatement_end();
expect(flow.promotedType(x).type, 'int');
expect(flow.promotedType(y), isNull);
expect(flow.promotedType(z), isNull);
});
});
test('Infinite loop does not implicitly assign variables', () {
var h = _Harness();
var x = h.addVar('x', 'int');
var whileStatement = _Statement();
h.assignedVariables(
(vars) => vars.nest(whileStatement, () => vars.write(x)));
h.run((flow) {
h.declare(x, initialized: false);
var trueCondition = _Expression();
flow.whileStatement_conditionBegin(whileStatement);
flow.booleanLiteral(trueCondition, true);
flow.whileStatement_bodyBegin(whileStatement, trueCondition);
flow.whileStatement_end();
expect(flow.isAssigned(x), false);
});
});
test('If(false) does not discard promotions', () {
var h = _Harness();
var x = h.addVar('x', 'Object');
h.run((flow) {
h.declare(x, initialized: true);
h.promote(x, 'int');
expect(flow.promotedType(x).type, 'int');
// if (false) {
flow.ifStatement_conditionBegin();
var falseExpression = _Expression();
flow.booleanLiteral(falseExpression, false);
flow.ifStatement_thenBegin(falseExpression);
expect(flow.promotedType(x).type, 'int');
flow.ifStatement_end(false);
});
});
test('Promotions do not occur when a variable is write-captured', () {
var h = _Harness();
var x = h.addVar('x', 'Object');
var functionNode = _Node();
h.assignedVariables(
(vars) => vars.function(functionNode, () => vars.write(x)));
h.run((flow) {
h.declare(x, initialized: true);
h.function(functionNode, () {});
h.promote(x, 'int');
expect(flow.promotedType(x), isNull);
});
});
test('Promotion cancellation of write-captured vars survives join', () {
var h = _Harness();
var x = h.addVar('x', 'Object');
var functionNode = _Node();
h.assignedVariables(
(vars) => vars.function(functionNode, () => vars.write(x)));
h.run((flow) {
h.declare(x, initialized: true);
h.ifElse(h.expr, () {
h.function(functionNode, () {});
}, () {
// Promotion should work here because the write capture is in the
// other branch.
h.promote(x, 'int');
expect(flow.promotedType(x).type, 'int');
});
// But the promotion should be cancelled now, after the join.
expect(flow.promotedType(x), isNull);
// And further attempts to promote should fail due to the write capture.
h.promote(x, 'int');
expect(flow.promotedType(x), isNull);
});
});
});
group('Reachability', () {
test('initial state', () {
expect(Reachability.initial.parent, isNull);
expect(Reachability.initial.locallyReachable, true);
expect(Reachability.initial.overallReachable, true);
});
test('split', () {
var reachableSplit = Reachability.initial.split();
expect(reachableSplit.parent, same(Reachability.initial));
expect(reachableSplit.overallReachable, true);
expect(reachableSplit.locallyReachable, true);
var unreachable = reachableSplit.setUnreachable();
var unreachableSplit = unreachable.split();
expect(unreachableSplit.parent, same(unreachable));
expect(unreachableSplit.overallReachable, false);
expect(unreachableSplit.locallyReachable, true);
});
test('unsplit', () {
var base = Reachability.initial.split();
var reachableSplit = base.split();
var reachableSplitUnsplit = reachableSplit.unsplit();
expect(reachableSplitUnsplit.parent, same(base.parent));
expect(reachableSplitUnsplit.overallReachable, true);
expect(reachableSplitUnsplit.locallyReachable, true);
var reachableSplitUnreachable = reachableSplit.setUnreachable();
var reachableSplitUnreachableUnsplit =
reachableSplitUnreachable.unsplit();
expect(reachableSplitUnreachableUnsplit.parent, same(base.parent));
expect(reachableSplitUnreachableUnsplit.overallReachable, false);
expect(reachableSplitUnreachableUnsplit.locallyReachable, false);
var unreachable = base.setUnreachable();
var unreachableSplit = unreachable.split();
var unreachableSplitUnsplit = unreachableSplit.unsplit();
expect(unreachableSplitUnsplit, same(unreachable));
var unreachableSplitUnreachable = unreachableSplit.setUnreachable();
var unreachableSplitUnreachableUnsplit =
unreachableSplitUnreachable.unsplit();
expect(unreachableSplitUnreachableUnsplit, same(unreachable));
});
test('setUnreachable', () {
var reachable = Reachability.initial.split();
var unreachable = reachable.setUnreachable();
expect(unreachable.parent, same(reachable.parent));
expect(unreachable.locallyReachable, false);
expect(unreachable.overallReachable, false);
expect(unreachable.setUnreachable(), same(unreachable));
var provisionallyReachable = unreachable.split();
var provisionallyUnreachable = provisionallyReachable.setUnreachable();
expect(
provisionallyUnreachable.parent, same(provisionallyReachable.parent));
expect(provisionallyUnreachable.locallyReachable, false);
expect(provisionallyUnreachable.overallReachable, false);
expect(provisionallyUnreachable.setUnreachable(),
same(provisionallyUnreachable));
});
test('restrict', () {
var previous = Reachability.initial.split();
var reachable = previous.split();
var unreachable = reachable.setUnreachable();
expect(Reachability.restrict(reachable, reachable), same(reachable));
expect(Reachability.restrict(reachable, unreachable), same(unreachable));
expect(Reachability.restrict(unreachable, reachable), same(unreachable));
expect(
Reachability.restrict(unreachable, unreachable), same(unreachable));
});
test('join', () {
var previous = Reachability.initial.split();
var reachable = previous.split();
var unreachable = reachable.setUnreachable();
expect(Reachability.join(reachable, reachable), same(reachable));
expect(Reachability.join(reachable, unreachable), same(reachable));
expect(Reachability.join(unreachable, reachable), same(reachable));
expect(Reachability.join(unreachable, unreachable), same(unreachable));
});
});
group('State', () {
var intVar = _Var('x', _Type('int'));
var intQVar = _Var('x', _Type('int?'));
var objectQVar = _Var('x', _Type('Object?'));
var nullVar = _Var('x', _Type('Null'));
group('setUnreachable', () {
var unreachable =
FlowModel<_Var, _Type>(Reachability.initial.setUnreachable());
var reachable = FlowModel<_Var, _Type>(Reachability.initial);
test('unchanged', () {
expect(unreachable.setUnreachable(), same(unreachable));
});
test('changed', () {
void _check(FlowModel<_Var, _Type> initial) {
var s = initial.setUnreachable();
expect(s, isNot(same(initial)));
expect(s.reachable.overallReachable, false);
expect(s.variableInfo, same(initial.variableInfo));
}
_check(reachable);
});
});
test('split', () {
var s1 = FlowModel<_Var, _Type>(Reachability.initial);
var s2 = s1.split();
expect(s2.reachable.parent, same(s1.reachable));
});
test('unsplit', () {
var s1 = FlowModel<_Var, _Type>(Reachability.initial.split());
var s2 = s1.unsplit();
expect(s2.reachable, same(Reachability.initial));
});
group('unsplitTo', () {
test('no change', () {
var s1 = FlowModel<_Var, _Type>(Reachability.initial.split());
var result = s1.unsplitTo(s1.reachable.parent);
expect(result, same(s1));
});
test('unsplit once, reachable', () {
var s1 = FlowModel<_Var, _Type>(Reachability.initial.split());
var s2 = s1.split();
var result = s2.unsplitTo(s1.reachable.parent);
expect(result.reachable, same(s1.reachable));
});
test('unsplit once, unreachable', () {
var s1 = FlowModel<_Var, _Type>(Reachability.initial.split());
var s2 = s1.split().setUnreachable();
var result = s2.unsplitTo(s1.reachable.parent);
expect(result.reachable.locallyReachable, false);
expect(result.reachable.parent, same(s1.reachable.parent));
});
test('unsplit twice, reachable', () {
var s1 = FlowModel<_Var, _Type>(Reachability.initial.split());
var s2 = s1.split();
var s3 = s2.split();
var result = s3.unsplitTo(s1.reachable.parent);
expect(result.reachable, same(s1.reachable));
});
test('unsplit twice, top unreachable', () {
var s1 = FlowModel<_Var, _Type>(Reachability.initial.split());
var s2 = s1.split();
var s3 = s2.split().setUnreachable();
var result = s3.unsplitTo(s1.reachable.parent);
expect(result.reachable.locallyReachable, false);
expect(result.reachable.parent, same(s1.reachable.parent));
});
test('unsplit twice, previous unreachable', () {
var s1 = FlowModel<_Var, _Type>(Reachability.initial.split());
var s2 = s1.split().setUnreachable();
var s3 = s2.split();
var result = s3.unsplitTo(s1.reachable.parent);
expect(result.reachable.locallyReachable, false);
expect(result.reachable.parent, same(s1.reachable.parent));
});
});
group('tryPromoteForTypeCheck', () {
test('unpromoted -> unchanged (same)', () {
var h = _Harness();
var s1 = FlowModel<_Var, _Type>(Reachability.initial);
var s2 = s1.tryPromoteForTypeCheck(h, intVar, _Type('int')).ifTrue;
expect(s2, same(s1));
});
test('unpromoted -> unchanged (supertype)', () {
var h = _Harness();
var s1 = FlowModel<_Var, _Type>(Reachability.initial);
var s2 = s1.tryPromoteForTypeCheck(h, intVar, _Type('Object')).ifTrue;
expect(s2, same(s1));
});
test('unpromoted -> unchanged (unrelated)', () {
var h = _Harness();
var s1 = FlowModel<_Var, _Type>(Reachability.initial);
var s2 = s1.tryPromoteForTypeCheck(h, intVar, _Type('String')).ifTrue;
expect(s2, same(s1));
});
test('unpromoted -> subtype', () {
var h = _Harness();
var s1 = FlowModel<_Var, _Type>(Reachability.initial);
var s2 = s1.tryPromoteForTypeCheck(h, intQVar, _Type('int')).ifTrue;
expect(s2.reachable.overallReachable, true);
expect(s2.variableInfo, {
intQVar: _matchVariableModel(chain: ['int'], ofInterest: ['int'])
});
});
test('promoted -> unchanged (same)', () {
var h = _Harness();
var s1 = FlowModel<_Var, _Type>(Reachability.initial)
.tryPromoteForTypeCheck(h, objectQVar, _Type('int'))
.ifTrue;
var s2 = s1.tryPromoteForTypeCheck(h, objectQVar, _Type('int')).ifTrue;
expect(s2, same(s1));
});
test('promoted -> unchanged (supertype)', () {
var h = _Harness();
var s1 = FlowModel<_Var, _Type>(Reachability.initial)
.tryPromoteForTypeCheck(h, objectQVar, _Type('int'))
.ifTrue;