blob: b7e5b8eba72b4eea00ac7a12cb224b36e165c112 [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:test/test.dart';
import 'flow_analysis_mini_ast.dart';
main() {
group('API', () {
test('asExpression_end promotes variables', () {
var h = Harness();
var x = Var('x', 'int?');
h.run([
declare(x, initialized: true),
x.read.as_('int').stmt,
checkPromoted(x, 'int'),
]);
});
test('asExpression_end handles other expressions', () {
var h = Harness();
h.run([
expr('Object').as_('int').stmt,
]);
});
test('assert_afterCondition promotes', () {
var h = Harness();
var x = Var('x', 'int?');
h.run([
assert_(x.read.eq(nullLiteral),
checkPromoted(x, 'int').thenExpr(expr('String'))),
]);
});
test('assert_end joins previous and ifTrue states', () {
var h = Harness();
var x = Var('x', 'int?');
var y = Var('x', 'int?');
var z = Var('x', 'int?');
h.run([
x.read.as_('int').stmt,
z.read.as_('int').stmt,
assert_(block([
x.write(expr('int?')).stmt,
z.write(expr('int?')).stmt,
]).thenExpr(x.read.notEq(nullLiteral).and(y.read.notEq(nullLiteral)))),
// x should be promoted because it was promoted before the assert, and
// it is re-promoted within the assert (if it passes)
checkPromoted(x, 'int'),
// y should not be promoted because it was not promoted before the
// assert.
checkNotPromoted(y),
// z should not be promoted because it is demoted in the assert
// condition.
checkNotPromoted(z),
]);
});
test('conditional_thenBegin promotes true branch', () {
var h = Harness();
var x = Var('x', 'int?');
h.run([
declare(x, initialized: true),
x.read
.notEq(nullLiteral)
.conditional(checkPromoted(x, 'int').thenExpr(expr('int')),
checkNotPromoted(x).thenExpr(expr('int')))
.stmt,
checkNotPromoted(x),
]);
});
test('conditional_elseBegin promotes false branch', () {
var h = Harness();
var x = Var('x', 'int?');
h.run([
declare(x, initialized: true),
x.read
.eq(nullLiteral)
.conditional(checkNotPromoted(x).thenExpr(expr('Null')),
checkPromoted(x, 'int').thenExpr(expr('Null')))
.stmt,
checkNotPromoted(x),
]);
});
test('conditional_end keeps promotions common to true and false branches',
() {
var h = Harness();
var x = Var('x', 'int?');
var y = Var('y', 'int?');
var z = Var('z', 'int?');
h.run([
declare(x, initialized: true),
declare(y, initialized: true),
declare(z, initialized: true),
expr('bool')
.conditional(
block([
x.read.as_('int').stmt,
y.read.as_('int').stmt,
]).thenExpr(expr('Null')),
block([
x.read.as_('int').stmt,
z.read.as_('int').stmt,
]).thenExpr(expr('Null')))
.stmt,
checkPromoted(x, 'int'),
checkNotPromoted(y),
checkNotPromoted(z),
]);
});
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 = Var('x', 'int?');
var y = Var('y', 'int?');
var z = Var('z', 'int?');
h.run([
declare(x, initialized: true),
declare(y, initialized: true),
declare(z, initialized: true),
if_(
expr('bool').conditional(
x.read.notEq(nullLiteral).and(y.read.notEq(nullLiteral)),
x.read.notEq(nullLiteral).and(z.read.notEq(nullLiteral))),
[
checkPromoted(x, 'int'),
checkNotPromoted(y),
checkNotPromoted(z),
]),
]);
});
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 = Var('x', 'int?');
var y = Var('y', 'int?');
var z = Var('z', 'int?');
h.run([
declare(x, initialized: true),
declare(y, initialized: true),
declare(z, initialized: true),
if_(
expr('bool').conditional(
x.read.eq(nullLiteral).or(y.read.eq(nullLiteral)),
x.read.eq(nullLiteral).or(z.read.eq(nullLiteral))),
[],
[
checkPromoted(x, 'int'),
checkNotPromoted(y),
checkNotPromoted(z),
]),
]);
});
test('equalityOp(x != null) promotes true branch', () {
var h = Harness();
var x = Var('x', 'int?');
h.run([
declare(x, initialized: true),
if_(x.read.notEq(nullLiteral), [
checkReachable(true),
checkPromoted(x, 'int'),
], [
checkReachable(true),
checkNotPromoted(x),
]),
]);
});
test('equalityOp(x != null) when x is non-nullable', () {
var h = Harness();
var x = Var('x', 'int');
h.run([
declare(x, initialized: true),
if_(x.read.notEq(nullLiteral), [
checkReachable(true),
checkNotPromoted(x),
], [
checkReachable(true),
checkNotPromoted(x),
])
]);
});
test('equalityOp(<expr> == <expr>) has no special effect', () {
var h = Harness();
h.run([
if_(expr('int?').eq(expr('int?')), [
checkReachable(true),
], [
checkReachable(true),
]),
]);
});
test('equalityOp(<expr> != <expr>) has no special effect', () {
var h = Harness();
h.run([
if_(expr('int?').notEq(expr('int?')), [
checkReachable(true),
], [
checkReachable(true),
]),
]);
});
test('equalityOp(x != <null expr>) does not promote', () {
var h = Harness();
var x = Var('x', 'int?');
h.run([
declare(x, initialized: true),
if_(x.read.notEq(expr('Null')), [
checkNotPromoted(x),
], [
checkNotPromoted(x),
]),
]);
});
test('equalityOp(x == null) promotes false branch', () {
var h = Harness();
var x = Var('x', 'int?');
h.run([
declare(x, initialized: true),
if_(x.read.eq(nullLiteral), [
checkReachable(true),
checkNotPromoted(x),
], [
checkReachable(true),
checkPromoted(x, 'int'),
]),
]);
});
test('equalityOp(x == null) when x is non-nullable', () {
var h = Harness();
var x = Var('x', 'int');
h.run([
declare(x, initialized: true),
if_(x.read.eq(nullLiteral), [
checkReachable(true),
checkNotPromoted(x),
], [
checkReachable(true),
checkNotPromoted(x),
])
]);
});
test('equalityOp(null != x) promotes true branch', () {
var h = Harness();
var x = Var('x', 'int?');
h.run([
declare(x, initialized: true),
if_(nullLiteral.notEq(x.read), [
checkPromoted(x, 'int'),
], [
checkNotPromoted(x),
]),
]);
});
test('equalityOp(<null expr> != x) does not promote', () {
var h = Harness();
var x = Var('x', 'int?');
h.run([
declare(x, initialized: true),
if_(expr('Null').notEq(x.read), [
checkNotPromoted(x),
], [
checkNotPromoted(x),
]),
]);
});
test('equalityOp(null == x) promotes false branch', () {
var h = Harness();
var x = Var('x', 'int?');
h.run([
declare(x, initialized: true),
if_(nullLiteral.eq(x.read), [
checkNotPromoted(x),
], [
checkPromoted(x, 'int'),
]),
]);
});
test('equalityOp(null == null) equivalent to true', () {
var h = Harness();
h.run([
if_(expr('Null').eq(expr('Null')), [
checkReachable(true),
], [
checkReachable(false),
]),
]);
});
test('equalityOp(null != null) equivalent to false', () {
var h = Harness();
h.run([
if_(expr('Null').notEq(expr('Null')), [
checkReachable(false),
], [
checkReachable(true),
]),
]);
});
test('equalityOp(null == non-null) is not equivalent to false', () {
var h = Harness();
h.run([
if_(expr('Null').eq(expr('int')), [
checkReachable(true),
], [
checkReachable(true),
]),
]);
});
test('equalityOp(null != non-null) is not equivalent to true', () {
var h = Harness();
h.run([
if_(expr('Null').notEq(expr('int')), [
checkReachable(true),
], [
checkReachable(true),
]),
]);
});
test('equalityOp(non-null == null) is not equivalent to false', () {
var h = Harness();
h.run([
if_(expr('int').eq(expr('Null')), [
checkReachable(true),
], [
checkReachable(true),
]),
]);
});
test('equalityOp(non-null != null) is not equivalent to true', () {
var h = Harness();
h.run([
if_(expr('int').notEq(expr('Null')), [
checkReachable(true),
], [
checkReachable(true),
]),
]);
});
test('conditionEqNull() does not promote write-captured vars', () {
var h = Harness();
var x = Var('x', 'int?');
h.run([
declare(x, initialized: true),
if_(x.read.notEq(nullLiteral), [
checkPromoted(x, 'int'),
]),
localFunction([
x.write(expr('int?')).stmt,
]),
if_(x.read.notEq(nullLiteral), [
checkNotPromoted(x),
]),
]);
});
test('doStatement_bodyBegin() un-promotes', () {
var h = Harness();
var x = Var('x', 'int?');
h.run([
declare(x, initialized: true),
x.read.as_('int').stmt,
checkPromoted(x, 'int'),
branchTarget((t) => do_([
checkNotPromoted(x),
x.write(expr('Null')).stmt,
], expr('bool'))),
]);
});
test('doStatement_bodyBegin() handles write captures in the loop', () {
var h = Harness();
var x = Var('x', 'int?');
h.run([
declare(x, initialized: true),
do_([
x.read.as_('int').stmt,
// The promotion should have no effect, because the second time
// through the loop, x has been write-captured.
checkNotPromoted(x),
localFunction([
x.write(expr('int?')).stmt,
]),
], expr('bool')),
]);
});
test('doStatement_conditionBegin() joins continue state', () {
var h = Harness();
var x = Var('x', 'int?');
h.run([
declare(x, initialized: true),
branchTarget((t) => do_(
[
if_(x.read.notEq(nullLiteral), [
continue_(t),
]),
return_(),
checkReachable(false),
checkNotPromoted(x),
],
block([
checkReachable(true),
checkPromoted(x, 'int'),
]).thenExpr(expr('bool')))),
]);
});
test('doStatement_end() promotes', () {
var h = Harness();
var x = Var('x', 'int?');
h.run([
declare(x, initialized: true),
branchTarget((t) =>
do_([], checkNotPromoted(x).thenExpr(x.read.eq(nullLiteral)))),
checkPromoted(x, 'int'),
]);
});
test('finish checks proper nesting', () {
var h = Harness();
var e = expr('Null');
var flow = FlowAnalysis<Node, Statement, Expression, Var, Type>(
h, AssignedVariables<Node, Var>());
flow.ifStatement_conditionBegin();
flow.ifStatement_thenBegin(e);
expect(() => flow.finish(), _asserts);
});
test('for_conditionBegin() un-promotes', () {
var h = Harness();
var x = Var('x', 'int?');
h.run([
declare(x, initialized: true),
x.read.as_('int').stmt,
checkPromoted(x, 'int'),
for_(null, checkNotPromoted(x).thenExpr(expr('bool')), null, [
x.write(expr('int?')).stmt,
]),
]);
});
test('for_conditionBegin() handles write captures in the loop', () {
var h = Harness();
var x = Var('x', 'int?');
h.run([
declare(x, initialized: true),
x.read.as_('int').stmt,
checkPromoted(x, 'int'),
for_(
null,
block([
x.read.as_('int').stmt,
checkNotPromoted(x),
localFunction([
x.write(expr('int?')).stmt,
]),
]).thenExpr(expr('bool')),
null,
[]),
]);
});
test('for_conditionBegin() handles not-yet-seen variables', () {
var h = Harness();
var x = Var('x', 'int?');
var y = Var('y', 'int?');
h.run([
declare(y, initialized: true),
y.read.as_('int').stmt,
for_(null, declare(x, initialized: true).thenExpr(expr('bool')), null, [
x.write(expr('Null')).stmt,
]),
]);
});
test('for_bodyBegin() handles empty condition', () {
var h = Harness();
h.run([
for_(null, null, checkReachable(true).thenExpr(expr('Null')), []),
checkReachable(false),
]);
});
test('for_bodyBegin() promotes', () {
var h = Harness();
var x = Var('x', 'int?');
h.run([
for_(declare(x, initialized: true), x.read.notEq(nullLiteral), null, [
checkPromoted(x, 'int'),
]),
]);
});
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 = Var('x', 'int?');
h.run([
for_(declare(x, initialized: true), x.read.notEq(nullLiteral), null, [],
forCollection: true),
]);
});
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 = Var('x', 'int?');
var y = Var('y', 'int?');
var z = Var('z', 'int?');
h.run([
declare(x, initialized: true),
declare(y, initialized: true),
declare(z, initialized: true),
branchTarget((t) => for_(
null,
expr('bool'),
block([
checkPromoted(x, 'int'),
checkNotPromoted(y),
checkNotPromoted(z),
]).thenExpr(expr('Null')),
[
if_(expr('bool'), [
x.read.as_('int').stmt,
y.read.as_('int').stmt,
continue_(t),
]),
x.read.as_('int').stmt,
z.read.as_('int').stmt,
])),
]);
});
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 = Var('x', 'int?');
var y = Var('y', 'int?');
var z = Var('z', 'int?');
h.run([
declare(x, initialized: true),
declare(y, initialized: true),
declare(z, initialized: true),
branchTarget((t) => for_(
null, x.read.eq(nullLiteral).or(z.read.eq(nullLiteral)), null, [
if_(expr('bool'), [
x.read.as_('int').stmt,
y.read.as_('int').stmt,
break_(t),
]),
])),
checkPromoted(x, 'int'),
checkNotPromoted(y),
checkNotPromoted(z),
]);
});
test('forEach_bodyBegin() un-promotes', () {
var h = Harness();
var x = Var('x', 'int?');
h.run([
declare(x, initialized: true),
x.read.as_('int').stmt,
checkPromoted(x, 'int'),
forEachWithNonVariable(expr('List<int?>'), [
checkNotPromoted(x),
x.write(expr('int?')).stmt,
]),
]);
});
test('forEach_bodyBegin() handles write captures in the loop', () {
var h = Harness();
var x = Var('x', 'int?');
h.run([
declare(x, initialized: true),
x.read.as_('int').stmt,
checkPromoted(x, 'int'),
forEachWithNonVariable(expr('List<int?>'), [
x.read.as_('int').stmt,
checkNotPromoted(x),
localFunction([
x.write(expr('int?')).stmt,
]),
]),
]);
});
test('forEach_bodyBegin() writes to loop variable', () {
var h = Harness();
var x = Var('x', 'int?');
h.run([
declare(x, initialized: false),
checkAssigned(x, false),
forEachWithVariableSet(x, expr('List<int?>'), [
checkAssigned(x, true),
]),
checkAssigned(x, false),
]);
});
test('forEach_bodyBegin() pushes conservative join state', () {
var h = Harness();
var x = Var('x', 'int');
h.run([
declare(x, initialized: false),
checkUnassigned(x, true),
branchTarget((t) => forEachWithNonVariable(expr('List<int>'), [
// Since a write to x occurs somewhere in the loop, x should no
// longer be considered unassigned.
checkUnassigned(x, false),
break_(t), x.write(expr('int')).stmt,
])),
// 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.
checkUnassigned(x, false),
]);
});
test('forEach_end() restores state before loop', () {
var h = Harness();
var x = Var('x', 'int?');
h.run([
declare(x, initialized: true),
forEachWithNonVariable(expr('List<int?>'), [
x.read.as_('int').stmt,
checkPromoted(x, 'int'),
]),
checkNotPromoted(x),
]);
});
test('functionExpression_begin() cancels promotions of self-captured vars',
() {
var h = Harness();
var x = Var('x', 'int?');
var y = Var('y', 'int?');
h.run([
declare(x, initialized: true),
declare(y, initialized: true),
x.read.as_('int').stmt,
y.read.as_('int').stmt,
checkPromoted(x, 'int'),
checkPromoted(y, 'int'),
localFunction([
// x is unpromoted within the local function
checkNotPromoted(x), checkPromoted(y, 'int'),
x.write(expr('int?')).stmt, x.read.as_('int').stmt,
]),
// x is unpromoted after the local function too
checkNotPromoted(x), checkPromoted(y, 'int'),
]);
});
test('functionExpression_begin() cancels promotions of other-captured vars',
() {
var h = Harness();
var x = Var('x', 'int?');
var y = Var('y', 'int?');
h.run([
declare(x, initialized: true), declare(y, initialized: true),
x.read.as_('int').stmt, y.read.as_('int').stmt,
checkPromoted(x, 'int'), checkPromoted(y, 'int'),
localFunction([
// x is unpromoted within the local function, because the write
// might have been captured by the time the local function executes.
checkNotPromoted(x), checkPromoted(y, 'int'),
// And any effort to promote x fails, because there is no way of
// knowing when the captured write might occur.
x.read.as_('int').stmt,
checkNotPromoted(x), checkPromoted(y, 'int'),
]),
// x is still promoted after the local function, though, because the
// write hasn't been captured yet.
checkPromoted(x, 'int'), checkPromoted(y, 'int'),
localFunction([
// x is unpromoted inside this local function too.
checkNotPromoted(x), checkPromoted(y, 'int'),
x.write(expr('int?')).stmt,
]),
// And since the second local function captured x, it remains
// unpromoted.
checkNotPromoted(x), checkPromoted(y, 'int'),
]);
});
test('functionExpression_begin() cancels promotions of written vars', () {
var h = Harness();
var x = Var('x', 'int?');
var y = Var('y', 'int?');
h.run([
declare(x, initialized: true), declare(y, initialized: true),
x.read.as_('int').stmt, y.read.as_('int').stmt,
checkPromoted(x, 'int'), checkPromoted(y, 'int'),
localFunction([
// x is unpromoted within the local function, because the write
// might have happened by the time the local function executes.
checkNotPromoted(x), checkPromoted(y, 'int'),
// But it can be re-promoted because the write isn't captured.
x.read.as_('int').stmt,
checkPromoted(x, 'int'), checkPromoted(y, 'int'),
]),
// x is still promoted after the local function, though, because the
// write hasn't occurred yet.
checkPromoted(x, 'int'), checkPromoted(y, 'int'),
x.write(expr('int?')).stmt,
// x is unpromoted now.
checkNotPromoted(x), checkPromoted(y, 'int'),
]);
});
test('functionExpression_begin() handles not-yet-seen variables', () {
var h = Harness();
var x = Var('x', 'int?');
h.run([
localFunction([]),
// x is declared after the local function, so the local function
// cannot possibly write to x.
declare(x, initialized: true), x.read.as_('int').stmt,
checkPromoted(x, 'int'), x.write(expr('Null')).stmt,
]);
});
test('functionExpression_begin() handles not-yet-seen write-captured vars',
() {
var h = Harness();
var x = Var('x', 'int?');
var y = Var('y', 'int?');
h.run([
declare(y, initialized: true),
y.read.as_('int').stmt,
localFunction([
x.read.as_('int').stmt,
// Promotion should not occur, because x might be write-captured by
// the time this code is reached.
checkNotPromoted(x),
]),
localFunction([
declare(x, initialized: true),
x.write(expr('Null')).stmt,
]),
]);
});
test(
'functionExpression_end does not propagate "definitely unassigned" '
'data', () {
var h = Harness();
var x = Var('x', 'int');
h.run([
declare(x, initialized: false),
checkUnassigned(x, true),
localFunction([
// The function expression could be called at any time, so x might
// be assigned now.
checkUnassigned(x, false),
]),
// But now that we are back outside the function expression, we once
// again know that x is unassigned.
checkUnassigned(x, true),
x.write(expr('int')).stmt,
checkUnassigned(x, false),
]);
});
test('handleBreak handles deep nesting', () {
var h = Harness();
h.run([
branchTarget((t) => while_(booleanLiteral(true), [
if_(expr('bool'), [
if_(expr('bool'), [
break_(t),
]),
]),
return_(),
checkReachable(false),
])),
checkReachable(true),
]);
});
test('handleBreak handles mixed nesting', () {
var h = Harness();
h.run([
branchTarget((t) => while_(booleanLiteral(true), [
if_(expr('bool'), [
if_(expr('bool'), [
break_(t),
]),
break_(t),
]),
break_(t),
checkReachable(false),
])),
checkReachable(true),
]);
});
test('handleContinue handles deep nesting', () {
var h = Harness();
h.run([
branchTarget((t) => do_([
if_(expr('bool'), [
if_(expr('bool'), [
continue_(t),
]),
]),
return_(),
checkReachable(false),
], checkReachable(true).thenExpr(booleanLiteral(true)))),
checkReachable(false),
]);
});
test('handleContinue handles mixed nesting', () {
var h = Harness();
h.run([
branchTarget((t) => do_([
if_(expr('bool'), [
if_(expr('bool'), [
continue_(t),
]),
continue_(t),
]),
continue_(t),
checkReachable(false),
], checkReachable(true).thenExpr(booleanLiteral(true)))),
checkReachable(false),
]);
});
test('ifNullExpression allows ensure guarding', () {
var h = Harness();
var x = Var('x', 'int?');
h.run([
declare(x, initialized: true),
x.read
.ifNull(block([
checkReachable(true),
x.write(expr('int')).stmt,
checkPromoted(x, 'int'),
]).thenExpr(expr('int?')))
.thenStmt(block([
checkReachable(true),
checkPromoted(x, 'int'),
]))
.stmt,
]);
});
test('ifNullExpression allows promotion of tested var', () {
var h = Harness();
var x = Var('x', 'int?');
h.run([
declare(x, initialized: true),
x.read
.ifNull(block([
checkReachable(true),
x.read.as_('int').stmt,
checkPromoted(x, 'int'),
]).thenExpr(expr('int?')))
.thenStmt(block([
checkReachable(true),
checkPromoted(x, 'int'),
]))
.stmt,
]);
});
test('ifNullExpression discards promotions unrelated to tested expr', () {
var h = Harness();
var x = Var('x', 'int?');
h.run([
declare(x, initialized: true),
expr('int?')
.ifNull(block([
checkReachable(true),
x.read.as_('int').stmt,
checkPromoted(x, 'int'),
]).thenExpr(expr('int?')))
.thenStmt(block([
checkReachable(true),
checkNotPromoted(x),
]))
.stmt,
]);
});
test('ifNullExpression does not detect when RHS is unreachable', () {
var h = Harness();
h.run([
expr('int')
.ifNull(checkReachable(true).thenExpr(expr('int')))
.thenStmt(checkReachable(true))
.stmt,
]);
});
test('ifNullExpression determines reachability correctly for `Null` type',
() {
var h = Harness();
h.run([
expr('Null')
.ifNull(checkReachable(true).thenExpr(expr('Null')))
.thenStmt(checkReachable(true))
.stmt,
]);
});
test('ifStatement with early exit promotes in unreachable code', () {
var h = Harness();
var x = Var('x', 'int?');
h.run([
declare(x, initialized: true),
return_(),
checkReachable(false),
if_(x.read.eq(nullLiteral), [
return_(),
]),
checkReachable(false),
checkPromoted(x, 'int'),
]);
});
test('ifStatement_end(false) keeps else branch if then branch exits', () {
var h = Harness();
var x = Var('x', 'int?');
h.run([
declare(x, initialized: true),
if_(x.read.eq(nullLiteral), [
return_(),
]),
checkPromoted(x, 'int'),
]);
});
void _checkIs(String declaredType, String tryPromoteType,
String expectedPromotedTypeThen, String expectedPromotedTypeElse,
{bool inverted = false}) {
var h = Harness();
var x = Var('x', declaredType);
h.run([
declare(x, initialized: true),
if_(x.read.is_(tryPromoteType, isInverted: inverted), [
checkReachable(true),
checkPromoted(x, expectedPromotedTypeThen),
], [
checkReachable(true),
checkPromoted(x, expectedPromotedTypeElse),
])
]);
}
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([
if_(expr('Null').is_('int'), [
checkReachable(true),
], [
checkReachable(true),
]),
]);
});
test('isExpression_end does nothing if applied to a non-variable, inverted',
() {
var h = Harness();
h.run([
if_(expr('Null').isNot('int'), [
checkReachable(true),
], [
checkReachable(true),
]),
]);
});
test('isExpression_end() does not promote write-captured vars', () {
var h = Harness();
var x = Var('x', 'int?');
h.run([
declare(x, initialized: true),
if_(x.read.is_('int'), [
checkPromoted(x, 'int'),
]),
localFunction([
x.write(expr('int?')).stmt,
]),
if_(x.read.is_('int'), [
checkNotPromoted(x),
]),
]);
});
test('isExpression_end() handles not-yet-seen variables', () {
var h = Harness();
var x = Var('x', 'int?');
h.run([
if_(x.read.is_('int'), [
checkPromoted(x, 'int'),
]),
declare(x, initialized: true),
localFunction([
x.write(expr('Null')).stmt,
]),
]);
});
test('labeledBlock without break', () {
var h = Harness();
var x = Var('x', 'int?');
h.run([
declare(x, initialized: true),
if_(x.read.isNot('int'), [
labeled(return_()),
]),
checkPromoted(x, 'int'),
]);
});
test('labeledBlock with break joins', () {
var h = Harness();
var x = Var('x', 'int?');
h.run([
declare(x, initialized: true),
if_(x.read.isNot('int'), [
branchTarget((t) => labeled(block([
if_(expr('bool'), [
break_(t),
]),
return_(),
]))),
]),
checkNotPromoted(x),
]);
});
test('logicalBinaryOp_rightBegin(isAnd: true) promotes in RHS', () {
var h = Harness();
var x = Var('x', 'int?');
h.run([
declare(x, initialized: true),
x.read
.notEq(nullLiteral)
.and(checkPromoted(x, 'int').thenExpr(expr('bool')))
.stmt,
]);
});
test('logicalBinaryOp_rightEnd(isAnd: true) keeps promotions from RHS', () {
var h = Harness();
var x = Var('x', 'int?');
h.run([
declare(x, initialized: true),
if_(expr('bool').and(x.read.notEq(nullLiteral)), [
checkPromoted(x, 'int'),
]),
]);
});
test('logicalBinaryOp_rightEnd(isAnd: false) keeps promotions from RHS',
() {
var h = Harness();
var x = Var('x', 'int?');
h.run([
declare(x, initialized: true),
if_(expr('bool').or(x.read.eq(nullLiteral)), [], [
checkPromoted(x, 'int'),
]),
]);
});
test('logicalBinaryOp_rightBegin(isAnd: false) promotes in RHS', () {
var h = Harness();
var x = Var('x', 'int?');
h.run([
declare(x, initialized: true),
x.read
.eq(nullLiteral)
.or(checkPromoted(x, 'int').thenExpr(expr('bool')))
.stmt,
]);
});
test('logicalBinaryOp(isAnd: true) joins promotions', () {
// if (x != null && y != null) {
// promotes x and y
// }
var h = Harness();
var x = Var('x', 'int?');
var y = Var('y', 'int?');
h.run([
declare(x, initialized: true),
declare(y, initialized: true),
if_(x.read.notEq(nullLiteral).and(y.read.notEq(nullLiteral)), [
checkPromoted(x, 'int'),
checkPromoted(y, 'int'),
]),
]);
});
test('logicalBinaryOp(isAnd: false) joins promotions', () {
// if (x == null || y == null) {} else {
// promotes x and y
// }
var h = Harness();
var x = Var('x', 'int?');
var y = Var('y', 'int?');
h.run([
declare(x, initialized: true),
declare(y, initialized: true),
if_(x.read.eq(nullLiteral).or(y.read.eq(nullLiteral)), [], [
checkPromoted(x, 'int'),
checkPromoted(y, 'int'),
]),
]);
});
test('nonNullAssert_end(x) promotes', () {
var h = Harness();
var x = Var('x', 'int?');
h.run([
declare(x, initialized: true),
x.read.nonNullAssert.stmt,
checkPromoted(x, 'int'),
]);
});
test('nullAwareAccess temporarily promotes', () {
var h = Harness();
var x = Var('x', 'int?');
h.run([
declare(x, initialized: true),
x.read
.nullAwareAccess(block([
checkReachable(true),
checkPromoted(x, 'int'),
]).thenExpr(expr('Null')))
.stmt,
checkNotPromoted(x),
]);
});
test('nullAwareAccess does not promote the target of a cascade', () {
var h = Harness();
var x = Var('x', 'int?');
h.run([
declare(x, initialized: true),
x.read
.nullAwareAccess(
block([
checkReachable(true),
checkNotPromoted(x),
]).thenExpr(expr('Null')),
isCascaded: true)
.stmt,
]);
});
test('nullAwareAccess preserves demotions', () {
var h = Harness();
var x = Var('x', 'int?');
h.run([
declare(x, initialized: true),
x.read.as_('int').stmt,
expr('int')
.nullAwareAccess(block([
checkReachable(true),
checkPromoted(x, 'int'),
]).thenExpr(x.write(expr('int?'))).thenStmt(checkNotPromoted(x)))
.stmt,
checkNotPromoted(x),
]);
});
test('nullAwareAccess_end ignores shorting if target is non-nullable', () {
var h = Harness();
var x = Var('x', 'int?');
h.run([
declare(x, initialized: true),
expr('int')
.nullAwareAccess(block([
checkReachable(true),
x.read.as_('int').stmt,
checkPromoted(x, 'int'),
]).thenExpr(expr('Null')))
.stmt,
// Since the null-shorting path was reachable, promotion of `x` should
// be cancelled.
checkNotPromoted(x),
]);
});
test('parenthesizedExpression preserves promotion behaviors', () {
var h = Harness();
var x = Var('x', 'int?');
h.run([
if_(
x.read.parenthesized.notEq(nullLiteral.parenthesized).parenthesized,
[
checkPromoted(x, 'int'),
]),
]);
});
test('promote promotes to a subtype and sets type of interest', () {
var h = Harness();
var x = Var('x', 'num?');
h.run([
declare(x, initialized: true),
checkNotPromoted(x),
x.read.as_('num').stmt,
checkPromoted(x, 'num'),
// Check that it's a type of interest by promoting and de-promoting.
if_(x.read.is_('int'), [
checkPromoted(x, 'int'),
x.write(expr('num')).stmt,
checkPromoted(x, 'num'),
]),
]);
});
test('promote does not promote to a non-subtype', () {
var h = Harness();
var x = Var('x', 'num?');
h.run([
declare(x, initialized: true),
checkNotPromoted(x),
x.read.as_('String').stmt,
checkNotPromoted(x),
]);
});
test('promote does not promote if variable is write-captured', () {
var h = Harness();
var x = Var('x', 'num?');
h.run([
declare(x, initialized: true),
checkNotPromoted(x),
localFunction([
x.write(expr('num')).stmt,
]),
x.read.as_('num').stmt,
checkNotPromoted(x),
]);
});
test('promotedType handles not-yet-seen variables', () {
// Note: this is needed for error recovery in the analyzer.
var h = Harness();
var x = Var('x', 'int');
h.run([
checkNotPromoted(x),
declare(x, initialized: true),
]);
});
test('switchStatement_beginCase(false) restores previous promotions', () {
var h = Harness();
var x = Var('x', 'int?');
h.run([
declare(x, initialized: true),
x.read.as_('int').stmt,
switch_(
expr('Null'),
[
case_([
checkPromoted(x, 'int'),
x.write(expr('int?')).stmt,
checkNotPromoted(x),
]),
case_([
checkPromoted(x, 'int'),
x.write(expr('int?')).stmt,
checkNotPromoted(x),
]),
],
isExhaustive: false),
]);
});
test('switchStatement_beginCase(false) does not un-promote', () {
var h = Harness();
var x = Var('x', 'int?');
h.run([
declare(x, initialized: true),
x.read.as_('int').stmt,
switch_(
expr('Null'),
[
case_([
checkPromoted(x, 'int'),
x.write(expr('int?')).stmt,
checkNotPromoted(x),
])
],
isExhaustive: false),
]);
});
test('switchStatement_beginCase(false) handles write captures in cases',
() {
var h = Harness();
var x = Var('x', 'int?');
h.run([
declare(x, initialized: true),
x.read.as_('int').stmt,
switch_(
expr('Null'),
[
case_([
checkPromoted(x, 'int'),
localFunction([
x.write(expr('int?')).stmt,
]),
checkNotPromoted(x),
]),
],
isExhaustive: false),
]);
});
test('switchStatement_beginCase(true) un-promotes', () {
var h = Harness();
var x = Var('x', 'int?');
h.run([
declare(x, initialized: true),
x.read.as_('int').stmt,
switch_(
expr('Null'),
[
case_([
checkNotPromoted(x),
x.write(expr('int?')).stmt,
checkNotPromoted(x),
], hasLabel: true),
],
isExhaustive: false),
]);
});
test('switchStatement_beginCase(true) handles write captures in cases', () {
var h = Harness();
var x = Var('x', 'int?');
h.run([
declare(x, initialized: true),
x.read.as_('int').stmt,
switch_(
expr('Null'),
[
case_([
x.read.as_('int').stmt,
checkNotPromoted(x),
localFunction([
x.write(expr('int?')).stmt,
]),
checkNotPromoted(x),
], hasLabel: true),
],
isExhaustive: false),
]);
});
test('switchStatement_end(false) joins break and default', () {
var h = Harness();
var x = Var('x', 'int?');
var y = Var('y', 'int?');
var z = Var('z', 'int?');
h.run([
declare(x, initialized: true),
declare(y, initialized: true),
declare(z, initialized: true),
y.read.as_('int').stmt,
z.read.as_('int').stmt,
branchTarget((t) => switch_(
expr('Null'),
[
case_([
x.read.as_('int').stmt,
y.write(expr('int?')).stmt,
break_(t),
]),
],
isExhaustive: false)),
checkNotPromoted(x),
checkNotPromoted(y),
checkPromoted(z, 'int'),
]);
});
test('switchStatement_end(true) joins breaks', () {
var h = Harness();
var w = Var('w', 'int?');
var x = Var('x', 'int?');
var y = Var('y', 'int?');
var z = Var('z', 'int?');
h.run([
declare(w, initialized: true),
declare(x, initialized: true),
declare(y, initialized: true),
declare(z, initialized: true),
x.read.as_('int').stmt,
y.read.as_('int').stmt,
z.read.as_('int').stmt,
branchTarget((t) => switch_(
expr('Null'),
[
case_([
w.read.as_('int').stmt,
y.read.as_('int').stmt,
x.write(expr('int?')).stmt,
break_(t),
]),
case_([
w.read.as_('int').stmt,
x.read.as_('int').stmt,
y.write(expr('int?')).stmt,
break_(t),
]),
],
isExhaustive: true)),
checkPromoted(w, 'int'),
checkNotPromoted(x),
checkNotPromoted(y),
checkPromoted(z, 'int'),
]);
});
test('switchStatement_end(true) allows fall-through of last case', () {
var h = Harness();
var x = Var('x', 'int?');
h.run([
declare(x, initialized: true),
branchTarget((t) => switch_(
expr('Null'),
[
case_([
x.read.as_('int').stmt,
break_(t),
]),
case_([]),
],
isExhaustive: true)),
checkNotPromoted(x),
]);
});
test('tryCatchStatement_bodyEnd() restores pre-try state', () {
var h = Harness();
var x = Var('x', 'int?');
var y = Var('y', 'int?');
h.run([
declare(x, initialized: true),
declare(y, initialized: true),
y.read.as_('int').stmt,
tryCatch([
x.read.as_('int').stmt,
checkPromoted(x, 'int'),
checkPromoted(y, 'int'),
], [
catch_(body: [
checkNotPromoted(x),
checkPromoted(y, 'int'),
])
]),
]);
});
test('tryCatchStatement_bodyEnd() un-promotes variables assigned in body',
() {
var h = Harness();
var x = Var('x', 'int?');
h.run([
declare(x, initialized: true),
x.read.as_('int').stmt,
checkPromoted(x, 'int'),
tryCatch([
x.write(expr('int?')).stmt,
x.read.as_('int').stmt,
checkPromoted(x, 'int'),
], [
catch_(body: [
checkNotPromoted(x),
]),
]),
]);
});
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 = Var('x', 'int?');
h.run([
declare(x, initialized: true),
x.read.as_('int').stmt,
checkPromoted(x, 'int'),
tryCatch([
localFunction([
x.write(expr('int?')).stmt,
]),
return_(),
], [
catch_(body: [
x.read.as_('int').stmt,
checkNotPromoted(x),
])
]),
]);
});
test('tryCatchStatement_catchBegin() restores previous post-body state',
() {
var h = Harness();
var x = Var('x', 'int?');
h.run([
declare(x, initialized: true),
tryCatch([], [
catch_(body: [
x.read.as_('int').stmt,
checkPromoted(x, 'int'),
]),
catch_(body: [
checkNotPromoted(x),
]),
]),
]);
});
test('tryCatchStatement_catchBegin() initializes vars', () {
var h = Harness();
var e = Var('e', 'int');
var st = Var('st', 'StackTrace');
h.run([
tryCatch([], [
catch_(exception: e, stackTrace: st, body: [
checkAssigned(e, true),
checkAssigned(st, true),
]),
]),
]);
});
test('tryCatchStatement_catchEnd() joins catch state with after-try state',
() {
var h = Harness();
var x = Var('x', 'int?');
var y = Var('y', 'int?');
var z = Var('z', 'int?');
h.run([
declare(x, initialized: true), declare(y, initialized: true),
declare(z, initialized: true),
tryCatch([
x.read.as_('int').stmt,
y.read.as_('int').stmt,
], [
catch_(body: [
x.read.as_('int').stmt,
z.read.as_('int').stmt,
]),
]),
// Only x should be promoted, because it's the only variable
// promoted in both the try body and the catch handler.
checkPromoted(x, 'int'), checkNotPromoted(y), checkNotPromoted(z),
]);
});
test('tryCatchStatement_catchEnd() joins catch states', () {
var h = Harness();
var x = Var('x', 'int?');
var y = Var('y', 'int?');
var z = Var('z', 'int?');
h.run([
declare(x, initialized: true), declare(y, initialized: true),
declare(z, initialized: true),
tryCatch([
return_(),
], [
catch_(body: [
x.read.as_('int').stmt,
y.read.as_('int').stmt,
]),
catch_(body: [
x.read.as_('int').stmt,
z.read.as_('int').stmt,
]),
]),
// Only x should be promoted, because it's the only variable promoted
// in both catch handlers.
checkPromoted(x, 'int'), checkNotPromoted(y), checkNotPromoted(z),
]);
});
test('tryFinallyStatement_finallyBegin() restores pre-try state', () {
var h = Harness();
var x = Var('x', 'int?');
var y = Var('y', 'int?');
h.run([
declare(x, initialized: true),
declare(y, initialized: true),
y.read.as_('int').stmt,
tryFinally([
x.read.as_('int').stmt,
checkPromoted(x, 'int'),
checkPromoted(y, 'int'),
], [
checkNotPromoted(x),
checkPromoted(y, 'int'),
]),
]);
});
test(
'tryFinallyStatement_finallyBegin() un-promotes variables assigned in '
'body', () {
var h = Harness();
var x = Var('x', 'int?');
h.run([
declare(x, initialized: true),
x.read.as_('int').stmt,
checkPromoted(x, 'int'),
tryFinally([
x.write(expr('int?')).stmt,
x.read.as_('int').stmt,
checkPromoted(x, 'int'),
], [
checkNotPromoted(x),
]),
]);
});
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 = Var('x', 'int?');
h.run([
declare(x, initialized: true),
tryFinally([
localFunction([
x.write(expr('int?')).stmt,
]),
return_(),
], [
x.read.as_('int').stmt,
checkNotPromoted(x),
]),
]);
});
test('tryFinallyStatement_end() restores promotions from try body', () {
var h = Harness();
var x = Var('x', 'int?');
var y = Var('y', 'int?');
h.run([
declare(x, initialized: true), declare(y, initialized: true),
tryFinally([
x.read.as_('int').stmt,
checkPromoted(x, 'int'),
], [
checkNotPromoted(x),
y.read.as_('int').stmt,
checkPromoted(y, 'int'),
]),
// Both x and y should now be promoted.
checkPromoted(x, 'int'), checkPromoted(y, 'int'),
]);
});
test(
'tryFinallyStatement_end() does not restore try body promotions for '
'variables assigned in finally', () {
var h = Harness();
var x = Var('x', 'int?');
var y = Var('y', 'int?');
h.run([
declare(x, initialized: true), declare(y, initialized: true),
tryFinally([
x.read.as_('int').stmt,
checkPromoted(x, 'int'),
], [
checkNotPromoted(x),
x.write(expr('int?')).stmt,
y.write(expr('int?')).stmt,
y.read.as_('int').stmt,
checkPromoted(y, 'int'),
]),
// 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.
checkNotPromoted(x), checkPromoted(y, 'int'),
]);
});
test('whileStatement_conditionBegin() un-promotes', () {
var h = Harness();
var x = Var('x', 'int?');
h.run([
declare(x, initialized: true),
x.read.as_('int').stmt,
checkPromoted(x, 'int'),
while_(checkNotPromoted(x).thenExpr(expr('bool')), [
x.write(expr('Null')).stmt,
]),
]);
});
test('whileStatement_conditionBegin() handles write captures in the loop',
() {
var h = Harness();
var x = Var('x', 'int?');
h.run([
declare(x, initialized: true),
x.read.as_('int').stmt,
checkPromoted(x, 'int'),
while_(
block([
x.read.as_('int').stmt,
checkNotPromoted(x),
localFunction([
x.write(expr('int?')).stmt,
]),
]).thenExpr(expr('bool')),
[]),
]);
});
test('whileStatement_conditionBegin() handles not-yet-seen variables', () {
var h = Harness();
var x = Var('x', 'int?');
var y = Var('y', 'int?');
h.run([
declare(y, initialized: true),
y.read.as_('int').stmt,
while_(declare(x, initialized: true).thenExpr(expr('bool')), [
x.write(expr('Null')).stmt,
]),
]);
});
test('whileStatement_bodyBegin() promotes', () {
var h = Harness();
var x = Var('x', 'int?');
h.run([
declare(x, initialized: true),
while_(x.read.notEq(nullLiteral), [
checkPromoted(x, 'int'),
]),
]);
});
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 = Var('x', 'int?');
var y = Var('y', 'int?');
var z = Var('z', 'int?');
h.run([
declare(x, initialized: true),
declare(y, initialized: true),
declare(z, initialized: true),
branchTarget(
(t) => while_(x.read.eq(nullLiteral).or(z.read.eq(nullLiteral)), [
if_(expr('bool'), [
x.read.as_('int').stmt,
y.read.as_('int').stmt,
break_(t),
]),
])),
checkPromoted(x, 'int'),
checkNotPromoted(y),
checkNotPromoted(z),
]);
});
test('Infinite loop does not implicitly assign variables', () {
var h = Harness();
var x = Var('x', 'int');
h.run([
declare(x, initialized: false),
while_(booleanLiteral(true), [
x.write(expr('Null')).stmt,
]),
checkAssigned(x, false),
]);
});
test('If(false) does not discard promotions', () {
var h = Harness();
var x = Var('x', 'Object');
h.run([
declare(x, initialized: true),
x.read.as_('int').stmt,
checkPromoted(x, 'int'),
if_(booleanLiteral(false), [
checkPromoted(x, 'int'),
]),
]);
});
test('Promotions do not occur when a variable is write-captured', () {
var h = Harness();
var x = Var('x', 'Object');
h.run([
declare(x, initialized: true),
localFunction([
x.write(expr('Object')).stmt,
]),
x.read.as_('int').stmt,
checkNotPromoted(x),
]);
});
test('Promotion cancellation of write-captured vars survives join', () {
var h = Harness();
var x = Var('x', 'Object');
h.run([
declare(x, initialized: true),
if_(expr('bool'), [
localFunction([
x.write(expr('Object')).stmt,
]),
], [
// Promotion should work here because the write capture is in the
// other branch.
x.read.as_('int').stmt, checkPromoted(x, 'int'),
]),
// But the promotion should be cancelled now, after the join.
checkNotPromoted(x),
// And further attempts to promote should fail due to the write capture.
x.read.as_('int').stmt, checkNotPromoted(x),
]);
});
});
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', 'int');
var intQVar = Var('x', 'int?');
var objectQVar = Var('x', 'Object?');
var nullVar = Var('x', '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;
var s2 =
s1.tryPromoteForTypeCheck(h, objectQVar, Type('Object')).ifTrue;
expect(s2, same(s1));
});
test('promoted -> unchanged (unrelated)', () {
var h = Harness();
var s1 = FlowModel<Var, Type>(Reachability.initial)
.tryPromoteForTypeCheck(h, objectQVar, Type('int'))
.ifTrue;
var s2 =
s1.tryPromoteForTypeCheck(h, objectQVar, Type('String')).ifTrue;
expect(s2, same(s1));
});
test('promoted -> subtype', () {
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.reachable.overallReachable, true);
expect(s2.variableInfo, {
objectQVar: _matchVariableModel(
chain: ['int?', 'int'], ofInterest: ['int?', 'int'])
});
});
});
group('write', () {
var objectQVar = Var('x', 'Object?');
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);
expect(s.variableInfo[objectQVar], isNull);
});
test('unchanged', () {
var h = Harness();
var s1 = FlowModel<Var, Type>(Reachability.initial)
.declare(objectQVar, true);
var s2 = s1.write(objectQVar, Type('Object?'), h);
expect(s2, same(s1));
});
test('marks as assigned', () {
var h = Harness();
var s1 = FlowModel<Var, Type>(Reachability.initial)
.declare(objectQVar, false);
var s2 = s1.write(objectQVar, Type('int?'), h);
expect(s2.reachable.overallReachable, true);
expect(
s2.infoFor(objectQVar),
_matchVariableModel(
chain: null,
ofInterest: isEmpty,
assigned: true,
unassigned: false));
});
test('un-promotes fully', () {
var h = Harness();
var s1 = FlowModel<Var, Type>(Reachability.initial)
.declare(objectQVar, true)
.tryPromoteForTypeCheck(h, objectQVar, Type('int'))
.ifTrue;
expect(s1.variableInfo, contains(objectQVar));
var s2 = s1.write(objectQVar, Type('int?'), h);
expect(s2.reachable.overallReachable, true);
expect(s2.variableInfo, {
objectQVar: _matchVariableModel(
chain: null,
ofInterest: isEmpty,
assigned: true,
unassigned: false)
});
});
test('un-promotes partially, when no exact match', () {
var h = Harness();
var s1 = FlowModel<Var, Type>(Reachability.initial)
.declare(objectQVar, true)
.tryPromoteForTypeCheck(h, objectQVar, Type('num?'))
.ifTrue
.tryPromoteForTypeCheck(h, objectQVar, Type('int'))
.ifTrue;
expect(s1.variableInfo, {
objectQVar: _matchVariableModel(
chain: ['num?', 'int'],
ofInterest: ['num?', 'int'],
assigned: true,
unassigned: false)
});
var s2 = s1.write(objectQVar, Type('num'), h);
expect(s2.reachable.overallReachable, true);
expect(s2.variableInfo, {
objectQVar: _matchVariableModel(
chain: ['num?', 'num'],
ofInterest: ['num?', 'int'],
assigned: true,
unassigned: false)
});
});
test('un-promotes partially, when exact match', () {
var h = Harness();
var s1 = FlowModel<Var, Type>(Reachability.initial)
.declare(objectQVar, true)
.tryPromoteForTypeCheck(h, objectQVar, Type('num?'))
.ifTrue
.tryPromoteForTypeCheck(h, objectQVar, Type('num'))
.ifTrue
.tryPromoteForTypeCheck(h, objectQVar, Type('int'))
.ifTrue;
expect(s1.variableInfo, {
objectQVar: _matchVariableModel(
chain: ['num?', 'num', 'int'],
ofInterest: ['num?', 'num', 'int'],
assigned: true,
unassigned: false)
});
var s2 = s1.write(objectQVar, Type('num'), h);
expect(s2.reachable.overallReachable, true);
expect(s2.variableInfo, {
objectQVar: _matchVariableModel(
chain: ['num?', 'num'],
ofInterest: ['num?', 'num', 'int'],
assigned: true,
unassigned: false)
});
});
test('leaves promoted, when exact match', () {
var h = Harness();
var s1 = FlowModel<Var, Type>(Reachability.initial)
.declare(objectQVar, true)
.tryPromoteForTypeCheck(h, objectQVar, Type('num?'))
.ifTrue
.tryPromoteForTypeCheck(h, objectQVar, Type('num'))
.ifTrue;
expect(s1.variableInfo, {
objectQVar: _matchVariableModel(
chain: ['num?', 'num'],
ofInterest: ['num?', 'num'],
assigned: true,
unassigned: false)
});
var s2 = s1.write(objectQVar, Type('num'), h);
expect(s2.reachable.overallReachable, true);
expect(s2.variableInfo, same(s1.variableInfo));
});
test('leaves promoted, when writing a subtype', () {
var h = Harness();
var s1 = FlowModel<Var, Type>(Reachability.initial)
.declare(objectQVar, true)
.tryPromoteForTypeCheck(h, objectQVar, Type('num?'))
.ifTrue
.tryPromoteForTypeCheck(h, objectQVar, Type('num'))
.ifTrue;
expect(s1.variableInfo, {
objectQVar: _matchVariableModel(
chain: ['num?', 'num'],
ofInterest: ['num?', 'num'],
assigned: true,
unassigned: false)
});
var s2 = s1.write(objectQVar, Type('int'), h);
expect(s2.reachable.overallReachable, true);
expect(s2.variableInfo, same(s1.variableInfo));
});
group('Promotes to NonNull of a type of interest', () {
test('when declared type', () {
var h = Harness();
var x = Var('x', 'int?');
var s1 = FlowModel<Var, Type>(Reachability.initial).declare(x, true);
expect(s1.variableInfo, {
x: _matchVariableModel(chain: null),
});
var s2 = s1.write(x, Type('int'), h);
expect(s2.variableInfo, {
x: _matchVariableModel(chain: ['int']),
});
});
test('when declared type, if write-captured', () {
var h = Harness();
var x = Var('x', 'int?');
var s1 = FlowModel<Var, Type>(Reachability.initial).declare(x, true);
expect(s1.variableInfo, {
x: _matchVariableModel(chain: null),
});
var s2 = s1.conservativeJoin([], [x]);
expect(s2.variableInfo, {
x: _matchVariableModel(chain: null, writeCaptured: true),
});
// 'x' is write-captured, so not promoted
var s3 = s2.write(x, Type('int'), h);
expect(s3.variableInfo, {
x: _matchVariableModel(chain: null, writeCaptured: true),
});
});
test('when promoted', () {
var h = Harness();
var s1 = FlowModel<Var, Type>(Reachability.initial)
.declare(objectQVar, true)
.tryPromoteForTypeCheck(h, objectQVar, Type('int?'))
.ifTrue;
expect(s1.variableInfo, {
objectQVar: _matchVariableModel(
chain: ['int?'],
ofInterest: ['int?'],
),
});
var s2 = s1.write(objectQVar, Type('int'), h);
expect(s2.variableInfo, {
objectQVar: _matchVariableModel(
chain: ['int?', 'int'],
ofInterest: ['int?'],
),
});
});
test('when not promoted', () {
var h = Harness();
var s1 = FlowModel<Var, Type>(Reachability.initial)
.declare(objectQVar, true)
.tryPromoteForTypeCheck(h, objectQVar, Type('int?'))
.ifFalse;
expect(s1.variableInfo, {
objectQVar: _matchVariableModel(
chain: ['Object'],
ofInterest: ['int?'],
),
});
var s2 = s1.write(objectQVar, Type('int'), h);
expect(s2.variableInfo, {
objectQVar: _matchVariableModel(
chain: ['Object', 'int'],
ofInterest: ['int?'],
),
});
});
});
test('Promotes to type of interest when not previously promoted', () {
var h = Harness();
var s1 = FlowModel<Var, Type>(Reachability.initial)
.declare(objectQVar, true)
.tryPromoteForTypeCheck(h, objectQVar, Type('num?'))
.ifFalse;
expect(s1.variableInfo, {
objectQVar: _matchVariableModel(
chain: ['Object'],
ofInterest: ['num?'],
),
});
var s2 = s1.write(objectQVar, Type('num?'), h);
expect(s2.variableInfo, {
objectQVar: _matchVariableModel(
chain: ['num?'],
ofInterest: ['num?'],
),
});
});
test('Promotes to type of interest when previously promoted', () {
var h = Harness();
var s1 = FlowModel<Var, Type>(Reachability.initial)
.declare(objectQVar, true)
.tryPromoteForTypeCheck(h, objectQVar, Type('num?'))
.ifTrue
.tryPromoteForTypeCheck(h, objectQVar, Type('int?'))
.ifFalse;
expect(s1.variableInfo, {
objectQVar: _matchVariableModel(
chain: ['num?', 'num'],
ofInterest: ['num?', 'int?'],
),
});
var s2 = s1.write(objectQVar, Type('int?'), h);
expect(s2.variableInfo, {
objectQVar: _matchVariableModel(
chain: ['num?', 'int?'],
ofInterest: ['num?', 'int?'],
),
});
});
group('Multiple candidate types of interest', () {
group('; choose most specific', () {
Harness h;
setUp(() {
h = Harness();
// class A {}
// class B extends A {}
// class C extends B {}
h.addSubtype(Type('Object'), Type('A'), false);
h.addSubtype(Type('Object'), Type('A?'), false);
h.addSubtype(Type('Object'), Type('B?'), false);
h.addSubtype(Type('A'), Type('Object'), true);
h.addSubtype(Type('A'), Type('Object?'), true);
h.addSubtype(Type('A'), Type('A?'), true);
h.addSubtype(Type('A'), Type('B'), false);
h.addSubtype(Type('A'), Type('B?'), false);
h.addSubtype(Type('A?'), Type('Object'), false);
h.addSubtype(Type('A?'), Type('Object?'), true);
h.addSubtype(Type('A?'), Type('A'), false);
h.addSubtype(Type('A?'), Type('B?'), false);
h.addSubtype(Type('B'), Type('Object'), true);
h.addSubtype(Type('B'), Type('A'), true);
h.addSubtype(Type('B'), Type('A?'), true);
h.addSubtype(Type('B'), Type('B?'), true);
h.addSubtype(Type('B?'), Type('Object'), false);
h.addSubtype(Type('B?'), Type('Object?'), true);
h.addSubtype(Type('B?'), Type('A'), false);
h.addSubtype(Type('B?'), Type('A?'), true);
h.addSubtype(Type('B?'), Type('B'), false);
h.addSubtype(Type('C'), Type('Object'), true);
h.addSubtype(Type('C'), Type('A'), true);
h.addSubtype(Type('C'), Type('A?'), true);
h.addSubtype(Type('C'), Type('B'), true);
h.addSubtype(Type('C'), Type('B?'), true);
h.addFactor(Type('Object'), Type('A?'), Type('Object'));
h.addFactor(Type('Object'), Type('B?'), Type('Object'));
h.addFactor(Type('Object?'), Type('A'), Type('Object?'));
h.addFactor(Type('Object?'), Type('A?'), Type('Object'));
h.addFactor(Type('Object?'), Type('B?'), Type('Object'));
});
test('; first', () {
var x = Var('x', 'Object?');
var s1 = FlowModel<Var, Type>(Reachability.initial)
.declare(x, true)
.tryPromoteForTypeCheck(h, x, Type('B?'))
.ifFalse
.tryPromoteForTypeCheck(h, x, Type('A?'))
.ifFalse;
expect(s1.variableInfo, {
x: _matchVariableModel(
chain: ['Object'],
ofInterest: ['A?', 'B?'],
),
});
var s2 = s1.write(x, Type('C'), h);
expect(s2.variableInfo, {
x: _matchVariableModel(
chain: ['Object', 'B'],
ofInterest: ['A?', 'B?'],
),
});
});
test('; second', () {
var x = Var('x', 'Object?');
var s1 = FlowModel<Var, Type>(Reachability.initial)
.declare(x, true)
.tryPromoteForTypeCheck(h, x, Type('A?'))
.ifFalse
.tryPromoteForTypeCheck(h, x, Type('B?'))
.ifFalse;
expect(s1.variableInfo, {
x: _matchVariableModel(
chain: ['Object'],
ofInterest: ['A?', 'B?'],
),
});
var s2 = s1.write(x, Type('C'), h);
expect(s2.variableInfo, {
x: _matchVariableModel(
chain: ['Object', 'B'],
ofInterest: ['A?', 'B?'],
),
});
});
test('; nullable and non-nullable', () {
var x = Var('x', 'Object?');
var s1 = FlowModel<Var, Type>(Reachability.initial)
.declare(x, true)
.tryPromoteForTypeCheck(h, x, Type('A'))
.ifFalse
.tryPromoteForTypeCheck(h, x, Type('A?'))
.ifFalse;
expect(s1.variableInfo, {
x: _matchVariableModel(
chain: ['Object'],
ofInterest: ['A', 'A?'],
),
});
var s2 = s1.write(x, Type('B'), h);
expect(s2.variableInfo, {
x: _matchVariableModel(
chain: ['Object', 'A'],
ofInterest: ['A', 'A?'],
),
});
});
});
group('; ambiguous', () {
test('; no promotion', () {
var h = Harness();
var s1 = FlowModel<Var, Type>(Reachability.initial)
.declare(objectQVar, true)
.tryPromoteForTypeCheck(h, objectQVar, Type('num?'))
.ifFalse
.tryPromoteForTypeCheck(h, objectQVar, Type('num*'))
.ifFalse;
expect(s1.variableInfo, {
objectQVar: _matchVariableModel(
chain: ['Object'],
ofInterest: ['num?', 'num*'],
),
});
var s2 = s1.write(objectQVar, Type('int'), h);
// It's ambiguous whether to promote to num? or num*, so we don't
// promote.
expect(s2, same(s1));
});
});
test('exact match', () {
var h = Harness();
var s1 = FlowModel<Var, Type>(Reachability.initial)
.declare(objectQVar, true)
.tryPromoteForTypeCheck(h, objectQVar, Type('num?'))
.ifFalse
.tryPromoteForTypeCheck(h, objectQVar, Type('num*'))
.ifFalse;
expect(s1.variableInfo, {
objectQVar: _matchVariableModel(
chain: ['Object'],
ofInterest: ['num?', 'num*'],
),
});
var s2 = s1.write(objectQVar, Type('num?'), 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, {
objectQVar: _matchVariableModel(
chain: ['num?'],
ofInterest: ['num?', 'num*'],
),
});
});
});
});
group('demotion, to NonNull', () {
test('when promoted via test', () {
var x = Var('x', 'Object?');
var h = Harness();
var s1 = FlowModel<Var, Type>(Reachability.initial)
.declare(x, true)
.tryPromoteForTypeCheck(h, x, Type('num?'))
.ifTrue
.tryPromoteForTypeCheck(h, x, Type('int?'))
.ifTrue;
expect(s1.variableInfo, {
x: _matchVariableModel(
chain: ['num?', 'int?'],
ofInterest: ['num?', 'int?'],
),
});
var s2 = s1.write(x, Type('double'), h);
expect(s2.variableInfo, {
x: _matchVariableModel(
chain: ['num?', 'num'],
ofInterest: ['num?', 'int?'],
),
});
});
});
group('declare', () {
var objectQVar = Var('x', 'Object?');
test('initialized', () {
var s = FlowModel<Var, Type>(Reachability.initial)
.declare(objectQVar, true);
expect(s.variableInfo, {
objectQVar: _matchVariableModel(assigned: true, unassigned: false),
});
});
test('not initialized', () {
var s = FlowModel<Var, Type>(Reachability.initial)
.declare(objectQVar, false);
expect(s.variableInfo, {
objectQVar: _matchVariableModel(assigned: false, unassigned: true),
});
});
});
group('markNonNullable', () {
test('unpromoted -> unchanged', () {
var h = Harness();
var s1 = FlowModel<Var, Type>(Reachability.initial);
var s2 = s1.tryMarkNonNullable(h, intVar).ifTrue;
expect(s2, same(s1));
});
test('unpromoted -> promoted', () {
var h = Harness();
var s1 = FlowModel<Var, Type>(Reachability.initial);
var s2 = s1.tryMarkNonNullable(h, intQVar).ifTrue;
expect(s2.reachable.overallReachable, true);
expect(s2.infoFor(intQVar),
_matchVariableModel(chain: ['int'], ofInterest: []));
});
test('promoted -> unchanged', () {
var h = Harness();
var s1 = FlowModel<Var, Type>(Reachability.initial)
.tryPromoteForTypeCheck(h, objectQVar, Type('int'))
.ifTrue;
var s2 = s1.tryMarkNonNullable(h, objectQVar).ifTrue;
expect(s2, same(s1));
});
test('promoted -> re-promoted', () {
var h = Harness();
var s1 = FlowModel<Var, Type>(Reachability.initial)
.tryPromoteForTypeCheck(h, objectQVar, Type('int?'))
.ifTrue;
var s2 = s1.tryMarkNonNullable(h, objectQVar).ifTrue;
expect(s2.reachable.overallReachable, true);
expect(s2.variableInfo, {
objectQVar:
_matchVariableModel(chain: ['int?', 'int'], ofInterest: ['int?'])
});
});
test('promote to Never', () {
var h = Harness();
var s1 = FlowModel<Var, Type>(Reachability.initial);
var s2 = s1.tryMarkNonNullable(h, nullVar).ifTrue;
expect(s2.reachable.overallReachable, false);
expect(s2.infoFor(nullVar),
_matchVariableModel(chain: ['Never'], ofInterest: []));
});
});
group('joinUnassigned', () {
group('other', () {
test('unchanged', () {
var h = Harness();
var a = Var('a', 'int');
var b = Var('b', 'int');
var s1 = FlowModel<Var, Type>(Reachability.initial)
.declare(a, false)
.declare(b, false)
.write(a, Type('int'), h);
expect(s1.variableInfo, {
a: _matchVariableModel(assigned: true, unassigned: false),
b: _matchVariableModel(assigned: false, unassigned: true),
});
var s2 = s1.write(a, Type('int'), h);
expect(s2.variableInfo, {
a: _matchVariableModel(assigned: true, unassigned: false),
b: _matchVariableModel(assigned: false, unassigned: true),
});
var s3 = s1.joinUnassigned(s2);
expect(s3, same(s1));
});
test('changed', () {
var h = Harness();
var a = Var('a', 'int');
var b = Var('b', 'int');
var c = Var('c', 'int');
var s1 = FlowModel<Var, Type>(Reachability.initial)
.declare(a, false)
.declare(b, false)
.declare(c, false)
.write(a, Type('int'), h);
expect(s1.variableInfo, {
a: _matchVariableModel(assigned: true, unassigned: false),
b: _matchVariableModel(assigned: false, unassigned: true),
c: _matchVariableModel(assigned: false, unassigned: true),
});
var s2 = s1.write(b, Type('int'), h);
expect(s2.variableInfo, {
a: _matchVariableModel(assigned: true, unassigned: false),
b: _matchVariableModel(assigned: true, unassigned: false),
c: _matchVariableModel(assigned: false, unassigned: true),
});
var s3 = s1.joinUnassigned(s2);
expect(s3.variableInfo, {
a: _matchVariableModel(assigned: true, unassigned: false),
b: _matchVariableModel(assigned: false, unassigned: false),
c: _matchVariableModel(assigned: false, unassigned: true),
});
});
});
});
group('conservativeJoin', () {
test('unchanged', () {
var h = Harness();
var s1 = FlowModel<Var, Type>(Reachability.initial)
.declare(intQVar, true)
.tryPromoteForTypeCheck(h, objectQVar, Type('int'))
.ifTrue;
var s2 = s1.conservativeJoin([intQVar], []);
expect(s2, same(s1));
});
test('written', () {
var h = Harness();
var s1 = FlowModel<Var, Type>(Reachability.initial)
.tryPromoteForTypeCheck(h, objectQVar, Type('int'))
.ifTrue
.tryPromoteForTypeCheck(h, intQVar, Type('int'))
.ifTrue;
var s2 = s1.conservativeJoin([intQVar], []);
expect(s2.reachable.overallReachable, true);
expect(s2.variableInfo, {
objectQVar: _matchVariableModel(chain: ['int'], ofInterest: ['int']),
intQVar: _matchVariableModel(chain: null, ofInterest: ['int'])
});
});
test('write captured', () {
var h = Harness();
var s1 = FlowModel<Var, Type>(Reachability.initial)
.tryPromoteForTypeCheck(h, objectQVar, Type('int'))
.ifTrue
.tryPromoteForTypeCheck(h, intQVar, Type('int'))
.ifTrue;
var s2 = s1.conservativeJoin([], [intQVar]);