blob: 5d522bebd92aaa0e760520b9326f5c008283c01d [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?');
late SsaNode<Var, Type> ssaBeforePromotion;
h.run([
declare(x, initialized: true),
getSsaNodes((nodes) => ssaBeforePromotion = nodes[x]!),
x.read.as_('int').stmt,
checkPromoted(x, 'int'),
getSsaNodes((nodes) => expect(nodes[x], same(ssaBeforePromotion))),
]);
});
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('declare() sets Ssa', () {
var h = Harness();
var x = Var('x', 'Object');
h.run([
declare(x, initialized: false),
getSsaNodes((nodes) {
expect(nodes[x], isNotNull);
}),
]);
});
test('equalityOp(x != null) promotes true branch', () {
var h = Harness();
var x = Var('x', 'int?');
late SsaNode<Var, Type> ssaBeforePromotion;
h.run([
declare(x, initialized: true),
getSsaNodes((nodes) => ssaBeforePromotion = nodes[x]!),
if_(x.read.notEq(nullLiteral), [
checkReachable(true),
checkPromoted(x, 'int'),
getSsaNodes((nodes) => expect(nodes[x], same(ssaBeforePromotion))),
], [
checkReachable(true),
checkNotPromoted(x),
getSsaNodes((nodes) => expect(nodes[x], same(ssaBeforePromotion))),
]),
]);
});
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?');
late SsaNode<Var, Type> ssaBeforePromotion;
h.run([
declare(x, initialized: true),
getSsaNodes((nodes) => ssaBeforePromotion = nodes[x]!),
if_(x.read.eq(nullLiteral), [
checkReachable(true),
checkNotPromoted(x),
getSsaNodes((nodes) => expect(nodes[x], same(ssaBeforePromotion))),
], [
checkReachable(true),
checkPromoted(x, 'int'),
getSsaNodes((nodes) => expect(nodes[x], same(ssaBeforePromotion))),
]),
]);
});
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?');
late SsaNode<Var, Type> ssaBeforePromotion;
h.run([
declare(x, initialized: true),
getSsaNodes((nodes) => ssaBeforePromotion = nodes[x]!),
if_(nullLiteral.notEq(x.read), [
checkPromoted(x, 'int'),
getSsaNodes((nodes) => expect(nodes[x], same(ssaBeforePromotion))),
], [
checkNotPromoted(x),
getSsaNodes((nodes) => expect(nodes[x], same(ssaBeforePromotion))),
]),
]);
});
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?');
late SsaNode<Var, Type> ssaBeforePromotion;
h.run([
declare(x, initialized: true),
getSsaNodes((nodes) => ssaBeforePromotion = nodes[x]!),
if_(nullLiteral.eq(x.read), [
checkNotPromoted(x),
getSsaNodes((nodes) => expect(nodes[x], same(ssaBeforePromotion))),
], [
checkPromoted(x, 'int'),
getSsaNodes((nodes) => expect(nodes[x], same(ssaBeforePromotion))),
]),
]);
});
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('declare(initialized: false) assigns new SSA ids', () {
var h = Harness();
var x = Var('x', 'int?');
var y = Var('y', 'int?');
h.run([
declare(x, initialized: false),
declare(y, initialized: false),
getSsaNodes((nodes) => expect(nodes[y], isNot(nodes[x]))),
]);
});
test('declare(initialized: true) assigns new SSA ids', () {
var h = Harness();
var x = Var('x', 'int?');
var y = Var('y', 'int?');
h.run([
declare(x, initialized: true),
declare(y, initialized: true),
getSsaNodes((nodes) => expect(nodes[y], isNot(nodes[x]))),
]);
});
test('doStatement_bodyBegin() un-promotes', () {
var h = Harness();
var x = Var('x', 'int?');
late SsaNode<Var, Type> ssaBeforeLoop;
h.run([
declare(x, initialized: true),
x.read.as_('int').stmt,
checkPromoted(x, 'int'),
getSsaNodes((nodes) => ssaBeforeLoop = nodes[x]!),
branchTarget((t) => do_([
getSsaNodes((nodes) => expect(nodes[x], isNot(ssaBeforeLoop))),
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?');
late SsaNode<Var, Type> ssaBeforeLoop;
h.run([
declare(x, initialized: true),
x.read.as_('int').stmt,
checkPromoted(x, 'int'),
getSsaNodes((nodes) => ssaBeforeLoop = nodes[x]!),
for_(
null,
block([
checkNotPromoted(x),
getSsaNodes((nodes) => expect(nodes[x], isNot(ssaBeforeLoop))),
]).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('for_end() with break updates Ssa of modified vars', () {
var h = Harness();
var x = Var('x', 'int?');
var y = Var('x', 'int?');
late SsaNode<Var, Type> xSsaInsideLoop;
late SsaNode<Var, Type> ySsaInsideLoop;
h.run([
declare(x, initialized: true),
declare(y, initialized: true),
branchTarget((t) => for_(null, expr('bool'), null, [
x.write(expr('int?')).stmt,
if_(expr('bool'), [break_(t)]),
getSsaNodes((nodes) {
xSsaInsideLoop = nodes[x]!;
ySsaInsideLoop = nodes[y]!;
}),
])),
getSsaNodes((nodes) {
// x's Ssa should have been changed because of the join at the end of
// of the loop. y's should not, since it retains the value it had
// prior to the loop.
expect(nodes[x], isNot(xSsaInsideLoop));
expect(nodes[y], same(ySsaInsideLoop));
}),
]);
});
test(
'for_end() with break updates Ssa of modified vars when types were '
'tested', () {
var h = Harness();
var x = Var('x', 'int?');
var y = Var('x', 'int?');
late SsaNode<Var, Type> xSsaInsideLoop;
late SsaNode<Var, Type> ySsaInsideLoop;
h.run([
declare(x, initialized: true),
declare(y, initialized: true),
branchTarget((t) => for_(null, expr('bool'), null, [
x.write(expr('int?')).stmt,
if_(expr('bool'), [break_(t)]),
if_(x.read.is_('int'), []),
getSsaNodes((nodes) {
xSsaInsideLoop = nodes[x]!;
ySsaInsideLoop = nodes[y]!;
}),
])),
getSsaNodes((nodes) {
// x's Ssa should have been changed because of the join at the end of
// the loop. y's should not, since it retains the value it had prior
// to the loop.
expect(nodes[x], isNot(xSsaInsideLoop));
expect(nodes[y], same(ySsaInsideLoop));
}),
]);
});
test('forEach_bodyBegin() un-promotes', () {
var h = Harness();
var x = Var('x', 'int?');
late SsaNode<Var, Type> ssaBeforeLoop;
h.run([
declare(x, initialized: true),
x.read.as_('int').stmt,
checkPromoted(x, 'int'),
getSsaNodes((nodes) => ssaBeforeLoop = nodes[x]!),
forEachWithNonVariable(expr('List<int?>'), [
checkNotPromoted(x),
getSsaNodes((nodes) => expect(nodes[x], isNot(ssaBeforeLoop))),
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() does not write capture 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),
if_(x.read.notEq(nullLiteral), [checkPromoted(x, 'int')]),
]),
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'),
getSsaNodes((nodes) {
expect(nodes[x], isNotNull);
expect(nodes[y], isNotNull);
}),
localFunction([
// x is unpromoted within the local function
checkNotPromoted(x), checkPromoted(y, 'int'),
getSsaNodes((nodes) {
expect(nodes[x], isNull);
expect(nodes[y], isNotNull);
}),
x.write(expr('int?')).stmt, x.read.as_('int').stmt,
]),
// x is unpromoted after the local function too
checkNotPromoted(x), checkPromoted(y, 'int'),
getSsaNodes((nodes) {
expect(nodes[x], isNull);
expect(nodes[y], isNotNull);
}),
]);
});
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?');
late SsaNode<Var, Type> ssaBeforeFunction;
h.run([
declare(x, initialized: true), declare(y, initialized: true),
x.read.as_('int').stmt, y.read.as_('int').stmt,
checkPromoted(x, 'int'),
getSsaNodes((nodes) => ssaBeforeFunction = nodes[x]!),
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),
getSsaNodes((nodes) => expect(nodes[x], isNot(ssaBeforeFunction))),
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'),
getSsaNodes((nodes) => expect(nodes[x], same(ssaBeforeFunction))),
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,
getSsaNodes((nodes) => expect(nodes[x], null)),
localFunction([
getSsaNodes((nodes) => expect(nodes[x], isNot(nodes[y]))),
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'),
]);
});
test(
'ifStatement_end() discards non-matching expression info from joined '
'branches', () {
var h = Harness();
var w = Var('w', 'Object');
var x = Var('x', 'bool');
var y = Var('y', 'bool');
var z = Var('z', 'bool');
late SsaNode<Var, Type> xSsaNodeBeforeIf;
h.run([
declare(w, initialized: true),
declare(x, initialized: true),
declare(y, initialized: true),
declare(z, initialized: true),
x.write(w.read.is_('int')).stmt,
getSsaNodes((nodes) {
xSsaNodeBeforeIf = nodes[x]!;
expect(xSsaNodeBeforeIf.expressionInfo, isNotNull);
}),
if_(expr('bool'), [
y.write(w.read.is_('String')).stmt,
], [
z.write(w.read.is_('bool')).stmt,
]),
getSsaNodes((nodes) {
expect(nodes[x], same(xSsaNodeBeforeIf));
expect(nodes[y]!.expressionInfo, isNull);
expect(nodes[z]!.expressionInfo, isNull);
}),
]);
});
test(
'ifStatement_end() ignores non-matching SSA info from "then" path if '
'unreachable', () {
var h = Harness();
var x = Var('x', 'Object');
late SsaNode<Var, Type> xSsaNodeBeforeIf;
h.run([
declare(x, initialized: true),
getSsaNodes((nodes) {
xSsaNodeBeforeIf = nodes[x]!;
}),
if_(expr('bool'), [
x.write(expr('Object')).stmt,
return_(),
]),
getSsaNodes((nodes) {
expect(nodes[x], same(xSsaNodeBeforeIf));
}),
]);
});
test(
'ifStatement_end() ignores non-matching SSA info from "else" path if '
'unreachable', () {
var h = Harness();
var x = Var('x', 'Object');
late SsaNode<Var, Type> xSsaNodeBeforeIf;
h.run([
declare(x, initialized: true),
getSsaNodes((nodes) {
xSsaNodeBeforeIf = nodes[x]!;
}),
if_(expr('bool'), [], [
x.write(expr('Object')).stmt,
return_(),
]),
getSsaNodes((nodes) {
expect(nodes[x], same(xSsaNodeBeforeIf));
}),
]);
});
test('initialize() promotes when not final', () {
var h = Harness();
var x = Var('x', 'int?');
h.run([
declareInitialized(x, expr('int')),
checkPromoted(x, 'int'),
]);
});
test('initialize() does not promote when final', () {
var h = Harness();
var x = Var('x', 'int?');
h.run([
declareInitialized(x, expr('int'), isFinal: true),
checkNotPromoted(x),
]);
});
test('initialize() stores expressionInfo when not late', () {
var h = Harness();
var x = Var('x', 'Object');
var y = Var('y', 'int?');
late ExpressionInfo<Var, Type> writtenValueInfo;
h.run([
declareInitialized(
x,
y.read.eq(nullLiteral).getExpressionInfo((info) {
expect(info, isNotNull);
writtenValueInfo = info!;
})),
getSsaNodes((nodes) {
expect(nodes[x]!.expressionInfo, same(writtenValueInfo));
}),
]);
});
test('initialize() does not store expressionInfo when late', () {
var h = Harness();
var x = Var('x', 'Object');
var y = Var('y', 'int?');
h.run([
declareInitialized(x, y.read.eq(nullLiteral), isLate: true),
getSsaNodes((nodes) {
expect(nodes[x]!.expressionInfo, isNull);
}),
]);
});
test('initialize() does not store expressionInfo for trivial expressions',
() {
var h = Harness();
var x = Var('x', 'Object');
var y = Var('y', 'int?');
h.run([
declare(y, initialized: true),
localFunction([
y.write(expr('int?')).stmt,
]),
declareInitialized(
x,
// `y == null` is a trivial expression because y has been write
// captured.
y.read
.eq(nullLiteral)
.getExpressionInfo((info) => expect(info, isNotNull))),
getSsaNodes((nodes) {
expect(nodes[x]!.expressionInfo, isNull);
}),
]);
});
void _checkIs(String declaredType, String tryPromoteType,
String? expectedPromotedTypeThen, String? expectedPromotedTypeElse,
{bool inverted = false}) {
var h = Harness();
var x = Var('x', declaredType);
late SsaNode<Var, Type> ssaBeforePromotion;
h.run([
declare(x, initialized: true),
getSsaNodes((nodes) => ssaBeforePromotion = nodes[x]!),
if_(x.read.is_(tryPromoteType, isInverted: inverted), [
checkReachable(true),
checkPromoted(x, expectedPromotedTypeThen),
getSsaNodes((nodes) => expect(nodes[x], same(ssaBeforePromotion))),
], [
checkReachable(true),
checkPromoted(x, expectedPromotedTypeElse),
getSsaNodes((nodes) => expect(nodes[x], same(ssaBeforePromotion))),
])
]);
}
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('logicalNot_end() inverts a condition', () {
var h = Harness();
var x = Var('x', 'int?');
h.run([
declare(x, initialized: true),
if_(x.read.eq(nullLiteral).not, [
checkPromoted(x, 'int'),
], [
checkNotPromoted(x),
]),
]);
});
test('logicalNot_end() handles null literals', () {
var h = Harness();
h.run([
// `!null` would be a compile error, but we need to make sure we don't
// crash.
if_(nullLiteral.not, [], []),
]);
});
test('nonNullAssert_end(x) promotes', () {
var h = Harness();
var x = Var('x', 'int?');
late SsaNode<Var, Type> ssaBeforePromotion;
h.run([
declare(x, initialized: true),
getSsaNodes((nodes) => ssaBeforePromotion = nodes[x]!),
x.read.nonNullAssert.stmt,
checkPromoted(x, 'int'),
getSsaNodes((nodes) => expect(nodes[x], same(ssaBeforePromotion))),
]);
});
test('nullAwareAccess temporarily promotes', () {
var h = Harness();
var x = Var('x', 'int?');
late SsaNode<Var, Type> ssaBeforePromotion;
h.run([
declare(x, initialized: true),
getSsaNodes((nodes) => ssaBeforePromotion = nodes[x]!),
x.read
.nullAwareAccess(block([
checkReachable(true),
checkPromoted(x, 'int'),
getSsaNodes(
(nodes) => expect(nodes[x], same(ssaBeforePromotion))),
]).thenExpr(expr('Null')))
.stmt,
checkNotPromoted(x),
getSsaNodes((nodes) => expect(nodes[x], same(ssaBeforePromotion))),
]);
});
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?');
late SsaNode<Var, Type> ssaBeforeSwitch;
h.run([
declare(x, initialized: true),
x.read.as_('int').stmt,
switch_(
expr('Null').thenStmt(block([
checkPromoted(x, 'int'),
getSsaNodes((nodes) => ssaBeforeSwitch = nodes[x]!),
])),
[
case_([
checkNotPromoted(x),
getSsaNodes(
(nodes) => expect(nodes[x], isNot(ssaBeforeSwitch))),
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?');
late SsaNode<Var, Type> ssaAfterTry;
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'),
getSsaNodes((nodes) => ssaAfterTry = nodes[x]!),
], [
catch_(body: [
checkNotPromoted(x),
getSsaNodes((nodes) => expect(nodes[x], isNot(ssaAfterTry))),
]),
]),
]);
});
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?');
late SsaNode<Var, Type> ssaAtStartOfTry;
late SsaNode<Var, Type> ssaAfterTry;
h.run([
declare(x, initialized: true),
x.read.as_('int').stmt,
checkPromoted(x, 'int'),
tryFinally([
getSsaNodes((nodes) => ssaAtStartOfTry = nodes[x]!),
x.write(expr('int?')).stmt,
x.read.as_('int').stmt,
checkPromoted(x, 'int'),
getSsaNodes((nodes) => ssaAfterTry = nodes[x]!),
], [
checkNotPromoted(x),
// The SSA node for X should be different from what it was at any time
// during the try block, because there is no telling at what point an
// exception might have occurred.
getSsaNodes((nodes) {
expect(nodes[x], isNot(ssaAtStartOfTry));
expect(nodes[x], isNot(ssaAfterTry));
}),
]),
]);
});
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?');
late SsaNode<Var, Type> xSsaAtEndOfFinally;
late SsaNode<Var, Type> ySsaAtEndOfFinally;
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'),
getSsaNodes((nodes) {
xSsaAtEndOfFinally = nodes[x]!;
ySsaAtEndOfFinally = nodes[y]!;
}),
]),
// 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'),
// Both x and y should have the same SSA nodes they had at the end of
// the finally block, since the finally block is guaranteed to have
// executed.
getSsaNodes((nodes) {
expect(nodes[x], same(xSsaAtEndOfFinally));
expect(nodes[y], same(ySsaAtEndOfFinally));
}),
]);
});
group('allowLocalBooleanVarsToPromote', () {
test(
'tryFinallyStatement_end() restores SSA nodes from try block when it'
'is sound to do so', () {
var h = Harness(allowLocalBooleanVarsToPromote: true);
var x = Var('x', 'int?');
var y = Var('y', 'int?');
late SsaNode<Var, Type> xSsaAtEndOfTry;
late SsaNode<Var, Type> ySsaAtEndOfTry;
late SsaNode<Var, Type> xSsaAtEndOfFinally;
late SsaNode<Var, Type> ySsaAtEndOfFinally;
h.run([
declare(x, initialized: true), declare(y, initialized: true),
tryFinally([
x.write(expr('int?')).stmt,
y.write(expr('int?')).stmt,
getSsaNodes((nodes) {
xSsaAtEndOfTry = nodes[x]!;
ySsaAtEndOfTry = nodes[y]!;
}),
], [
if_(expr('bool'), [
x.write(expr('int?')).stmt,
]),
if_(expr('bool'), [
y.write(expr('int?')).stmt,
return_(),
]),
getSsaNodes((nodes) {
xSsaAtEndOfFinally = nodes[x]!;
ySsaAtEndOfFinally = nodes[y]!;
expect(xSsaAtEndOfFinally, isNot(same(xSsaAtEndOfTry)));
expect(ySsaAtEndOfFinally, isNot(same(ySsaAtEndOfTry)));
}),
]),
// x's SSA node should still match what it was at the end of the
// finally block, because it might have been written to. But y
// can't have been written to, because once we reach here, we know
// that the finally block completed normally, and the write to y
// always leads to the explicit return. So y's SSA node should be
// restored back to match that from the end of the try block.
getSsaNodes((nodes) {
expect(nodes[x], same(xSsaAtEndOfFinally));
expect(nodes[y], same(ySsaAtEndOfTry));
}),
]);
});
test(
'tryFinallyStatement_end() sets unreachable if end of try block '
'unreachable', () {
var h = Harness(allowLocalBooleanVarsToPromote: true);
h.run([
tryFinally([
return_(),
checkReachable(false),
], [
checkReachable(true),
]),
checkReachable(false),
]);
});
test(
'tryFinallyStatement_end() sets unreachable if end of finally block '
'unreachable', () {
var h = Harness(allowLocalBooleanVarsToPromote: true);
h.run([
tryFinally([
checkReachable(true),
], [
return_(),
checkReachable(false),
]),
checkReachable(false),
]);
});
test(
'tryFinallyStatement_end() handles a variable declared only in the '
'try block', () {
var h = Harness(allowLocalBooleanVarsToPromote: true);
var x = Var('x', 'int?');
h.run([
tryFinally([
declare(x, initialized: true),
], []),
]);
});
test(
'tryFinallyStatement_end() handles a variable declared only in the '
'finally block', () {
var h = Harness(allowLocalBooleanVarsToPromote: true);
var x = Var('x', 'int?');
h.run([
tryFinally([], [
declare(x, initialized: true),
]),
]);
});
test(
'tryFinallyStatement_end() handles a variable that was write '
'captured in the try block', () {
var h = Harness(allowLocalBooleanVarsToPromote: true);
var x = Var('x', 'int?');
h.run([
declare(x, initialized: true),
tryFinally([
localFunction([
x.write(expr('int?')).stmt,
]),
], []),
if_(x.read.notEq(nullLiteral), [
checkNotPromoted(x),
]),
]);
});
test(
'tryFinallyStatement_end() handles a variable that was write '
'captured in the finally block', () {
var h = Harness(allowLocalBooleanVarsToPromote: true);
var x = Var('x', 'int?');
h.run([
declare(x, initialized: true),
tryFinally([], [
localFunction([
x.write(expr('int?')).stmt,
]),
]),
if_(x.read.notEq(nullLiteral), [
checkNotPromoted(x),
]),
]);
});
test(
'tryFinallyStatement_end() handles a variable that was promoted in '
'the try block and write captured in the finally block', () {
var h = Harness(allowLocalBooleanVarsToPromote: true);
var x = Var('x', 'int?');
h.run([
declare(x, initialized: true),
tryFinally([
if_(x.read.eq(nullLiteral), [
return_(),
]),
checkPromoted(x, 'int'),
], [
localFunction([
x.write(expr('int?')).stmt,
]),
]),
// The capture in the `finally` cancels old promotions and prevents
// future promotions.
checkNotPromoted(x),
if_(x.read.notEq(nullLiteral), [
checkNotPromoted(x),
]),
]);
});
test(
'tryFinallyStatement_end() keeps promotions from both try and '
'finally blocks when there is no write in the finally block', () {
var h = Harness(allowLocalBooleanVarsToPromote: true);
var x = Var('x', 'Object');
h.run([
declare(x, initialized: true),
tryFinally([
if_(x.read.is_('num', isInverted: true), [
return_(),
]),
checkPromoted(x, 'num'),
], [
if_(x.read.is_('int', isInverted: true), [
return_(),
]),
]),
// The promotion chain now contains both `num` and `int`.
checkPromoted(x, 'int'),
x.write(expr('num')).stmt,
checkPromoted(x, 'num'),
]);
});
test(
'tryFinallyStatement_end() keeps promotions from the finally block '
'when there is a write in the finally block', () {
var h = Harness(allowLocalBooleanVarsToPromote: true);
var x = Var('x', 'Object');
h.run([
declare(x, initialized: true),
tryFinally([
if_(x.read.is_('String', isInverted: true), [
return_(),
]),
checkPromoted(x, 'String'),
], [
x.write(expr('Object')).stmt,
if_(x.read.is_('int', isInverted: true), [
return_(),
]),
]),
checkPromoted(x, 'int'),
]);
});
test(
'tryFinallyStatement_end() keeps tests from both the try and finally '
'blocks', () {
var h = Harness(allowLocalBooleanVarsToPromote: true);
var x = Var('x', 'Object');
h.run([
declare(x, initialized: true),
tryFinally([
if_(x.read.is_('String', isInverted: true), []),
checkNotPromoted(x),
], [
if_(x.read.is_('int', isInverted: true), []),
checkNotPromoted(x),
]),
checkNotPromoted(x),
if_(expr('bool'), [
x.write(expr('String')).stmt,
checkPromoted(x, 'String'),
], [
x.write(expr('int')).stmt,
checkPromoted(x, 'int'),
]),
]);
});
test(
'tryFinallyStatement_end() handles variables not definitely assigned '
'in either the try or finally block', () {
var h = Harness(allowLocalBooleanVarsToPromote: true);
var x = Var('x', 'Object');
h.run([
declare(x, initialized: false),
checkAssigned(x, false),
tryFinally([
if_(expr('bool'), [
x.write(expr('Object')).stmt,
]),
checkAssigned(x, false),
], [
if_(expr('bool'), [
x.write(expr('Object')).stmt,
]),
checkAssigned(x, false),
]),
checkAssigned(x, false),
]);
});
test(
'tryFinallyStatement_end() handles variables definitely assigned in '
'the try block', () {
var h = Harness(allowLocalBooleanVarsToPromote: true);
var x = Var('x', 'Object');
h.run([
declare(x, initialized: false),
checkAssigned(x, false),
tryFinally([
x.write(expr('Object')).stmt,
checkAssigned(x, true),
], [
if_(expr('bool'), [
x.write(expr('Object')).stmt,
]),
checkAssigned(x, false),
]),
checkAssigned(x, true),
]);
});
test(
'tryFinallyStatement_end() handles variables definitely assigned in '
'the finally block', () {
var h = Harness(allowLocalBooleanVarsToPromote: true);
var x = Var('x', 'Object');
h.run([
declare(x, initialized: false),
checkAssigned(x, false),
tryFinally([
if_(expr('bool'), [
x.write(expr('Object')).stmt,
]),
checkAssigned(x, false),
], [
x.write(expr('Object')).stmt,
checkAssigned(x, true),
]),
checkAssigned(x, true),
]);
});
test(
'tryFinallyStatement_end() handles variables definitely unassigned '
'in both the try and finally blocks', () {
var h = Harness(allowLocalBooleanVarsToPromote: true);
var x = Var('x', 'Object');
h.run([
declare(x, initialized: false),
checkUnassigned(x, true),
tryFinally([
checkUnassigned(x, true),
], [
checkUnassigned(x, true),
]),
checkUnassigned(x, true),
]);
});
test(
'tryFinallyStatement_end() handles variables definitely unassigned '
'in the try but not the finally block', () {
var h = Harness(allowLocalBooleanVarsToPromote: true);
var x = Var('x', 'Object');
h.run([
declare(x, initialized: false),
checkUnassigned(x, true),
tryFinally([
checkUnassigned(x, true),
], [
if_(expr('bool'), [
x.write(expr('Object')).stmt,
]),
checkUnassigned(x, false),
]),
checkUnassigned(x, false),
]);
});
test(
'tryFinallyStatement_end() handles variables definitely unassigned '
'in the finally but not the try block', () {
var h = Harness(allowLocalBooleanVarsToPromote: true);
var x = Var('x', 'Object');
h.run([
declare(x, initialized: false),
checkUnassigned(x, true),
tryFinally([
if_(expr('bool'), [
x.write(expr('Object')).stmt,
]),
checkUnassigned(x, false),
], [
checkUnassigned(x, false),
]),
checkUnassigned(x, false),
]);
});
});
test('variableRead() restores promotions from previous write()', () {
var h = Harness(allowLocalBooleanVarsToPromote: true);
var x = Var('x', 'int?');
var y = Var('y', 'int?');
var z = Var('z', 'bool');
h.run([
declare(x, initialized: true),
declare(y, initialized: true),
declare(z, initialized: true),
// Create a variable that promotes x if its value is true, and y if its
// value is false.
z
.write(x.read.notEq(nullLiteral).conditional(
booleanLiteral(true),
y.read.notEq(nullLiteral).conditional(
booleanLiteral(false), throw_(expr('Object')))))
.stmt,
checkNotPromoted(x),
checkNotPromoted(y),
// Simply reading the variable shouldn't promote anything.
z.read.stmt,
checkNotPromoted(x),
checkNotPromoted(y),
// But reading it in an "if" condition should promote.
if_(z.read, [
checkPromoted(x, 'int'),
checkNotPromoted(y),
], [
checkNotPromoted(x),
checkPromoted(y, 'int'),
]),
]);
});
test('variableRead() restores promotions from previous initialization', () {
var h = Harness(allowLocalBooleanVarsToPromote: true);
var x = Var('x', 'int?');
var y = Var('y', 'int?');
var z = Var('z', 'bool');
h.run([
declare(x, initialized: true),
declare(y, initialized: true),
// Create a variable that promotes x if its value is true, and y if its
// value is false.
declareInitialized(
z,
x.read.notEq(nullLiteral).conditional(
booleanLiteral(true),
y.read.notEq(nullLiteral).conditional(
booleanLiteral(false), throw_(expr('Object'))))),
checkNotPromoted(x),
checkNotPromoted(y),
// Simply reading the variable shouldn't promote anything.
z.read.stmt,
checkNotPromoted(x),
checkNotPromoted(y),
// But reading it in an "if" condition should promote.
if_(z.read, [
checkPromoted(x, 'int'),
checkNotPromoted(y),
], [
checkNotPromoted(x),
checkPromoted(y, 'int'),
]),
]);
});
test('variableRead() rebases old promotions', () {
var h = Harness(allowLocalBooleanVarsToPromote: true);
var w = Var('w', 'int?');
var x = Var('x', 'int?');
var y = Var('y', 'int?');
var z = Var('z', 'bool');
h.run([
declare(w, initialized: true),
declare(x, initialized: true),
declare(y, initialized: true),
declare(z, initialized: true),
// Create a variable that promotes x if its value is true, and y if its
// value is false.
z
.write(x.read.notEq(nullLiteral).conditional(
booleanLiteral(true),
y.read.notEq(nullLiteral).conditional(
booleanLiteral(false), throw_(expr('Object')))))
.stmt,
checkNotPromoted(w),
checkNotPromoted(x),
checkNotPromoted(y),
w.read.nonNullAssert.stmt,
checkPromoted(w, 'int'),
// Reading the value of z in an "if" condition should promote x or y,
// and keep the promotion of w.
if_(z.read, [
checkPromoted(w, 'int'),
checkPromoted(x, 'int'),
checkNotPromoted(y),
], [
checkPromoted(w, 'int'),
checkNotPromoted(x),
checkPromoted(y, 'int'),
]),
]);
});
test("variableRead() doesn't restore the notion of whether a value is null",
() {
// Note: we have the available infrastructure to do this if we want, but
// we think it will give an inconsistent feel because comparisons like
// `if (i == null)` *don't* promote.
var h = Harness(allowLocalBooleanVarsToPromote: true);
var x = Var('x', 'int?');
var y = Var('y', 'int?');
h.run([
declare(x, initialized: true),
declare(y, initialized: true),
y.write(nullLiteral).stmt,
checkNotPromoted(x),
checkNotPromoted(y),
if_(x.read.eq(y.read), [
checkNotPromoted(x),
checkNotPromoted(y),
], [
// Even though x != y and y is known to contain the value `null`, we
// don't promote x.
checkNotPromoted(x),
checkNotPromoted(y),
]),
]);
});
test('whileStatement_conditionBegin() un-promotes', () {
var h = Harness();
var x = Var('x', 'int?');
late SsaNode<Var, Type> ssaBeforeLoop;
h.run([
declare(x, initialized: true),
x.read.as_('int').stmt,
checkPromoted(x, 'int'),
getSsaNodes((nodes) => ssaBeforeLoop = nodes[x]!),
while_(
block([
checkNotPromoted(x),
getSsaNodes((nodes) => expect(nodes[x], isNot(ssaBeforeLoop))),
]).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('whileStatement_end() with break updates Ssa of modified vars', () {
var h = Harness();
var x = Var('x', 'int?');
var y = Var('x', 'int?');
late SsaNode<Var, Type> xSsaInsideLoop;
late SsaNode<Var, Type> ySsaInsideLoop;
h.run([
declare(x, initialized: true),
declare(y, initialized: true),
branchTarget((t) => while_(expr('bool'), [
x.write(expr('int?')).stmt,
if_(expr('bool'), [break_(t)]),
getSsaNodes((nodes) {
xSsaInsideLoop = nodes[x]!;
ySsaInsideLoop = nodes[y]!;
}),
])),
getSsaNodes((nodes) {
// x's Ssa should have been changed because of the join at the end of
// the loop. y's should not, since it retains the value it had prior
// to the loop.
expect(nodes[x], isNot(xSsaInsideLoop));
expect(nodes[y], same(ySsaInsideLoop));
}),
]);
});
test(
'whileStatement_end() with break updates Ssa of modified vars when '
'types were tested', () {
var h = Harness();
var x = Var('x', 'int?');
var y = Var('x', 'int?');
late SsaNode<Var, Type> xSsaInsideLoop;
late SsaNode<Var, Type> ySsaInsideLoop;
h.run([
declare(x, initialized: true),
declare(y, initialized: true),
branchTarget((t) => while_(expr('bool'), [
x.write(expr('int?')).stmt,
if_(expr('bool'), [break_(t)]),
if_(x.read.is_('int'), []),
getSsaNodes((nodes) {
xSsaInsideLoop = nodes[x]!;
ySsaInsideLoop = nodes[y]!;
}),
])),
getSsaNodes((nodes) {
// x's Ssa should have been changed because of the join at the end of
// the loop. y's should not, since it retains the value it had prior
// to the loop.
expect(nodes[x], isNot(xSsaInsideLoop));
expect(nodes[y], same(ySsaInsideLoop));
}),
]);
});
test('write() de-promotes and updates Ssa of a promoted variable', () {
var h = Harness();
var x = Var('x', 'Object');
var y = Var('y', 'int?');
late SsaNode<Var, Type> ssaBeforeWrite;
late ExpressionInfo<Var, Type> writtenValueInfo;
h.run([
declare(x, initialized: true),
declare(y, initialized: true),
x.read.as_('int').stmt,
checkPromoted(x, 'int'),
getSsaNodes((nodes) => ssaBeforeWrite = nodes[x]!),
x
.write(y.read.eq(nullLiteral).getExpressionInfo((info) {
expect(info, isNotNull);
writtenValueInfo = info!;
}))
.stmt,
checkNotPromoted(x),
getSsaNodes((nodes) {
expect(nodes[x], isNot(ssaBeforeWrite));
expect(nodes[x]!.expressionInfo, same(writtenValueInfo));
}),
]);
});
test('write() updates Ssa', () {
var h = Harness();
var x = Var('x', 'Object');
var y = Var('y', 'int?');
late SsaNode<Var, Type> ssaBeforeWrite;
late ExpressionInfo<Var, Type> writtenValueInfo;
h.run([
declare(x, initialized: true),
getSsaNodes((nodes) => ssaBeforeWrite = nodes[x]!),
x
.write(y.read.eq(nullLiteral).getExpressionInfo((info) {
expect(info, isNotNull);
writtenValueInfo = info!;
}))
.stmt,
getSsaNodes((nodes) {
expect(nodes[x], isNot(ssaBeforeWrite));
expect(nodes[x]!.expressionInfo, same(writtenValueInfo));
}),
]);
});
test('write() does not copy Ssa from one variable to another', () {
// We could do so, and it would enable us to promote in slightly more
// situations, e.g.:
// bool b = x != null;
// if (b) { /* x promoted here */ }
// var tmp = x;
// x = ...;
// if (b) { /* x not promoted here */ }
// x = tmp;
// if (b) { /* x promoted again */ }
// But there are a lot of corner cases to test and it's not clear how much
// the benefit will be, so for now we're not doing it.
var h = Harness(allowLocalBooleanVarsToPromote: true);
var x = Var('x', 'int?');
var y = Var('y', 'int?');
late SsaNode<Var, Type> xSsaBeforeWrite;
late SsaNode<Var, Type> ySsa;
h.run([
declare(x, initialized: true),
declare(y, initialized: true),
getSsaNodes((nodes) {
xSsaBeforeWrite = nodes[x]!;
ySsa = nodes[y]!;
}),
x.write(y.read).stmt,
getSsaNodes((nodes) {
expect(nodes[x], isNot(xSsaBeforeWrite));
expect(nodes[x], isNot(ySsa));
}),
]);
});
test('write() does not store expressionInfo for trivial expressions', () {
var h = Harness();
var x = Var('x', 'Object');
var y = Var('y', 'int?');
late SsaNode<Var, Type> ssaBeforeWrite;
h.run([
declare(x, initialized: true),
declare(y, initialized: true),
localFunction([
y.write(expr('int?')).stmt,
]),
getSsaNodes((nodes) => ssaBeforeWrite = nodes[x]!),
// `y == null` is a trivial expression because y has been write
// captured.
x
.write(y.read
.eq(nullLiteral)